diff --git a/_bmad-output/prds/prd-mardonar-encounter-engine-2026-06-20/addendum-2026-06-22-lobby-all-and-expiry.md b/_bmad-output/prds/prd-mardonar-encounter-engine-2026-06-20/addendum-2026-06-22-lobby-all-and-expiry.md new file mode 100644 index 0000000..6d33702 --- /dev/null +++ b/_bmad-output/prds/prd-mardonar-encounter-engine-2026-06-20/addendum-2026-06-22-lobby-all-and-expiry.md @@ -0,0 +1,88 @@ +# Addendum 2 — Lobby on All Encounters + Encounter End/Expiry Event + +**Date:** 2026-06-22 +**Amends:** the Group Encounters PRD (`prd.md`) — FR-19, FR-20, FR-26, and the +`phase: open | resolved` model. These are requirements the PRD missed, raised +after the feature set shipped. + +--- + +## A. Lobby on ALL encounters (amends FR-19/FR-20) + +**Today:** `/encounter start` opens a lobby **only** when `minPlayers >= 2` +(FR-20); solo encounters (`minPlayers <= 1`) begin immediately with implicit +join (FR-19). + +**New requirement:** **every** `/encounter start` opens a lobby, including solo. +The `minPlayers` gate still controls when **Begin** enables, but there is no +"begin immediately" path — the lobby is the single entry point for all +encounters. + +- Solo (`minPlayers: 1`): lobby opens → 0 joined → Begin disabled → one player + Joins → 1 joined (min met) → Begin enabled → begin. +- Group (`minPlayers >= 2`): unchanged gating, just no longer a separate branch. + +## B. Starter is NOT pre-joined (amends FR-26) + +**Today:** `/encounter start` pre-joins the starter (`joined: [interaction.user.id]`, +`joinedNames: [starterName]`). + +**New requirement:** the starter (the DM, "usually me") is **not** added to the +roster. The lobby opens **empty** (`joined: []`, `joinedNames: []`). The starter +creates the lobby; players Join; **Begin** is pressable by any joined player +once the minimum is met (FR-22 unchanged). The starter's `starterId` is still +recorded (for Cancel authorization, FR-23) but they are not a participant. + +**Impact on existing specs/behavior:** solo play now requires the single player +to Join + Begin (one extra click) rather than implicit-join-on-chat. This is an +intentional behavior change to shipped behavior (call it out in the changelog + +the release-playtest checklist, same family as FR-43 ship-and-break). + +## C. TTL auto-expiry + event (new) + +**New requirement:** encounters **auto-expire** after an inactivity TTL — no +player message and no LLM turn for a configurable period (default TBD, e.g. 24h, +env-configurable `ENCOUNTER_INACTIVITY_TTL_HOURS`). On expiry: + +1. `session.phase` transitions to a new **`expired`** state (alongside + `open` / `resolved`). +2. A **GraphMCP `log_encounter` event** is emitted with `kind: "expired"` + (distinct from `kind: "ended"` below), recording the encounter + participants + + a summary like "Encounter expired due to inactivity." +3. A **Discord notice** is posted in the thread (in-world voice: e.g. *"The + moment passes unresolved, and the road moves on…"*) — not a raw system string. +4. The thread is **archived**. + +## D. End vs expired — the two events + +- **`/encounter end`** (existing) → `phase: resolved`, GraphMCP event + `kind: "ended"` (the DM explicitly ended it). Today `log_encounter` fires on + end but does not tag `kind`; add the `kind` field so end vs expired are + distinguishable in the graph. +- **Auto-expiry** (new) → `phase: expired`, GraphMCP event `kind: "expired"`. + +Both post a Discord notice + archive the thread. + +--- + +## Implementation notes (for the stories) + +- **A + B** live in `src/bot/commands/encounter.ts` `handleStart`: remove the + `if (spec.minPlayers >= 2)` branch (always lobby); change `setLobby` to + `joined: []`, `joinedNames: []` (drop the starter pre-join). `lobbyHandler` + is unchanged (Join/Begin already work on the joined list). Update the lobby + embed initial state (0 joined) + the live E2E test's lobby AC. +- **C + D** need: a `lastActivityAt` on `SessionState` (bumped on each message + + LLM turn); a periodic sweep (or extension of `restartSweep`) that finalizes + sessions past the TTL as `expired`; a `phase: 'expired'` literal in the type; + a `kind` field on `log_encounter`; the in-world expiry notice (centralize in + `src/lib/systemStrings.ts` if it exists — see FU-11). Boot sweep should also + catch expired-by-inactivity on restart. +- **Follow-up stories:** FU-13 (A + B), FU-14 (C + D) — see `follow-up-stories.md`. + +## Open questions +- **OQ-A1** Default inactivity TTL value (24h? 12h to match session TTL?). +- **OQ-A2** Does auto-expiry also clear an in-flight `pendingGroupCheck` / + `pendingSkillCheck` (finalize as expired-failure)? Recommend yes. +- **OQ-A3** Should the expiry sweep run on a periodic timer (in-process) or only + at boot + on activity? Periodic is more responsive; boot-only is simpler. \ No newline at end of file diff --git a/_bmad-output/prds/prd-mardonar-encounter-engine-2026-06-20/follow-up-stories.md b/_bmad-output/prds/prd-mardonar-encounter-engine-2026-06-20/follow-up-stories.md index 41e063f..0c557a7 100644 --- a/_bmad-output/prds/prd-mardonar-encounter-engine-2026-06-20/follow-up-stories.md +++ b/_bmad-output/prds/prd-mardonar-encounter-engine-2026-06-20/follow-up-stories.md @@ -121,6 +121,37 @@ deferred, accepted as ship-and-break trade-offs, or left open. The feature set --- +## FU-13 — Lobby on ALL encounters + starter NOT pre-joined +- **status:** backlog (PRD addendum 2, items A + B) · **confidence: high** +- **Source:** addendum-2026-06-22-lobby-all-and-expiry.md (amends FR-19/FR-20/FR-26). +- **Today:** `/encounter start` lobbies only for `minPlayers >= 2` and pre-joins the starter (`joined: [interaction.user.id]`). +- **Change:** + - Every `/encounter start` opens a lobby (remove the `minPlayers >= 2` branch in `handleStart`). Solo (`minPlayers: 1`) also lobbies. + - The starter is NOT pre-joined — lobby opens empty (`joined: []`, `joinedNames: []`). `starterId` is still recorded (for Cancel). Players Join; Begin at min. +- **Acceptance:** + - Solo spec (`minPlayers: 1`): `/encounter start` → lobby (0 joined, Begin disabled) → one Join → Begin enabled → begin. + - Group spec: unchanged gating, lobby flow only. + - The live multiplayer E2E test's lobby AC is updated (no pre-join: 0 → player1 join → player2 join → Begin). + - Changelog + release-playtest checklist note the solo behavior change (ship-and-break family). +- **Depends on:** none. Touches `src/bot/commands/encounter.ts` + the lobby live test. + +## FU-14 — TTL auto-expiry + end/expired event +- **status:** backlog (PRD addendum 2, items C + D) · **confidence: high** +- **Source:** addendum-2026-06-22-lobby-all-and-expiry.md. +- **Today:** encounters end only via `/encounter end` (`phase: resolved`); no inactivity expiry; `log_encounter` has no `kind`. +- **Change:** + - `SessionState.lastActivityAt` (bumped on each message + LLM turn). + - Inactivity TTL (env `ENCOUNTER_INACTIVITY_TTL_HOURS`, default TBD — OQ-A1). + - A sweep finalizes sessions past the TTL as `phase: 'expired'` (new phase literal); clears any in-flight `pendingGroupCheck`/`pendingSkillCheck` as expired-failure (OQ-A2). + - On expiry: GraphMCP `log_encounter` with `kind: "expired"`; an in-world Discord notice (centralized system string); thread archived. + - `/encounter end` tags `log_encounter` `kind: "ended"` (so end vs expired are distinguishable). + - Boot sweep also catches expired-by-inactivity on restart. +- **Acceptance:** + - An idle session past the TTL is finalized as `expired` with a GraphMCP `kind: "expired"` event + a Discord notice + archived thread. + - `/encounter end` emits `kind: "ended"`. + - A unit test covers the expiry sweep (clock-injected TTL) + the kind tagging. +- **Open:** OQ-A1 (default TTL), OQ-A2 (clear in-flight checks on expiry — recommend yes), OQ-A3 (periodic timer vs boot-only sweep). + ## Not deferred (intentionally closed — do not re-open) - **FR-43 deprecation window / `SKILL_CHECK_SURFACE` flag (FR-49/FR-50):** RETIRED by the round-3 party-mode decision (Mary's trim). Ship-and-break in one release, no flag, plain system notice for old buttons, pinned Discord message. This is *done by design*, not a follow-up. - **Straggler deadline as a spec field:** retracted → became a DM "resolve with current rolls" UX affordance (FR-47), not a spec field.