docs(prd): addendum 2 — lobby on all encounters + end/expiry event
Two requirements the group-encounters PRD missed (raised post-ship): - A/B: Lobby on ALL encounters (solo too) + starter NOT pre-joined (lobby opens empty; the DM creates it, players Join, Begin at min). Amends FR-19/FR-20/FR-26 — a solo behavior change (ship-and-break family). - C/D: TTL auto-expiry + event — inactivity TTL finalizes a session as phase:'expired' with a GraphMCP log_encounter kind:'expired' event + an in-world Discord notice + thread archive; /encounter end tags kind:'ended'. Backlog: FU-13 (A+B), FU-14 (C+D) with acceptance + open questions (OQ-A1 default TTL, OQ-A2 clear in-flight checks, OQ-A3 periodic vs boot sweep). Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user