Lane 5, PR 3 (server + bot): the quick note, recording starts the session, the start window, who's here (#731; D10, D112, D116, D120, D123) #828

Merged
claude-bot merged 8 commits from build/lane5-p3-live into main 2026-09-26 08:36:44 +00:00
Collaborator

This is the rest of lane 5's server side. Migration d4e6f8a0b2c3 is re-chained onto c3d5e7f9a1b2 (#827), and the one-head pin moves to it.

The quick note (IS-381, D112)

  • POST /api/sessions/{id}/notes/quick, for any member.
  • A GM's line goes to the GMs-only note; a player's goes to their own.
  • The server stamps [h:mm:ss] only while this session is recording.
  • The note row is locked while the line is added, so two GMs writing at once both keep their line.
  • Note versions gain the source quick_note, and a run of quick notes by the same person updates one version.

Recording starts the session (D116), with D10 and the start window (D123)

  • /record/start checks, in order:
    • D10: the session must be confirmed or in progress;
    • D123: the start must fall within the window;
    • the bot: then the bot is asked.
  • After the bot's 200: one conditional update makes the session in progress, with started_at.
  • When it's refused: the 409 carries detail ("Session 14 starts at 7:00 PM CDT. Recording opens at 6:30 PM CDT."), starts_at and opens_at.
  • The setting: campaigns.recording_start_window_minutes, 0–1440, default 30. It's on the campaign read and PATCH (GM only), and is copied by "from one of yours".
  • started_at elsewhere: the poller stamps it, the 8-hour completion fallback counts from it, and reopening for a vote clears it.

Who's here (D120) and the other session

  • /record/live and the /record/stream SSE carry:
    • here: {players_present, players_total}, from verified links, only while this session is recording;
    • other: {number, title, campaign_name, owner_name} while the bot records a different session.
  • The stream's lookup of the other session uses task_session() (#814's guard).

The bot (BOT_CONTRACT_VERSION stays 2, and check_version_sync.py passes)

  • Each speaker in the recording status carries in_channel.
  • Discord's /record start now calls a new additive endpoint, POST /api/bot/sessions/{id}/recording-started, before it connects. It applies D10, D123 and D116, and the bot shows the GM the backend's refusal.
  • An older backend's bare 404 means "record as before" and is logged once. "Session not found" refuses.

Tests

  • Backend: the full suite on the merged tree gave 4558 passed and 1 failed, the pooled-session guard. That's fixed here, and the guard plus the live tests re-ran: 60 passed.
  • Bot: the full suite, 461 passed.
  • Migrations: up, down and up again on postgres 16.
  • Lint: ruff is clean.

Docs: CHANGELOG and docs/API.md, including the stale /live number sentence (D86).

🤖 Generated with Claude Code

This is the rest of lane 5's server side. Migration `d4e6f8a0b2c3` is re-chained onto `c3d5e7f9a1b2` (#827), and the one-head pin moves to it. **The quick note (IS-381, D112)** - `POST /api/sessions/{id}/notes/quick`, for any member. - A GM's line goes to the GMs-only note; a player's goes to their own. - The server stamps `[h:mm:ss]` only while this session is recording. - The note row is locked while the line is added, so two GMs writing at once both keep their line. - Note versions gain the source `quick_note`, and a run of quick notes by the same person updates one version. **Recording starts the session (D116), with D10 and the start window (D123)** - `/record/start` checks, in order: - **D10:** the session must be confirmed or in progress; - **D123:** the start must fall within the window; - **the bot:** then the bot is asked. - **After the bot's 200:** one conditional update makes the session in progress, with `started_at`. - **When it's refused:** the 409 carries `detail` ("Session 14 starts at 7:00 PM CDT. Recording opens at 6:30 PM CDT."), `starts_at` and `opens_at`. - **The setting:** `campaigns.recording_start_window_minutes`, 0–1440, default 30. It's on the campaign read and PATCH (GM only), and is copied by "from one of yours". - **`started_at` elsewhere:** the poller stamps it, the 8-hour completion fallback counts from it, and reopening for a vote clears it. **Who's here (D120) and the other session** - `/record/live` and the `/record/stream` SSE carry: - `here: {players_present, players_total}`, from verified links, only while this session is recording; - `other: {number, title, campaign_name, owner_name}` while the bot records a different session. - The stream's lookup of the other session uses `task_session()` (#814's guard). **The bot** (`BOT_CONTRACT_VERSION` stays 2, and `check_version_sync.py` passes) - Each speaker in the recording status carries `in_channel`. - Discord's `/record start` now calls a new additive endpoint, `POST /api/bot/sessions/{id}/recording-started`, before it connects. It applies D10, D123 and D116, and the bot shows the GM the backend's refusal. - An older backend's bare 404 means "record as before" and is logged once. "Session not found" refuses. **Tests** - **Backend:** the full suite on the merged tree gave 4558 passed and 1 failed, the pooled-session guard. That's fixed here, and the guard plus the live tests re-ran: 60 passed. - **Bot:** the full suite, 461 passed. - **Migrations:** up, down and up again on postgres 16. - **Lint:** ruff is clean. **Docs:** CHANGELOG and `docs/API.md`, including the stale `/live` `number` sentence (D86). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Lane 5, PR 3: Table mode's live server. No pages.

- POST /api/sessions/{id}/notes/quick: one line added to the GMs' note (a
  player's to their own), stamped [h:mm:ss] by the server from the bot's
  status while this session is being recorded. A run of quick notes by one
  person is one version (new source quick_note); any other save ends it.
- POST /record/start enforces D10 (409 unless confirmed or in progress, before
  the bot is asked) and, on success, makes a confirmed session in_progress
  with started_at (D116). The poller stamps started_at with the confirmed time
  when it flips, and its eight-hour fallback counts from started_at. Reopening
  for a vote clears it. The live bar and GET /sessions/{id} carry it.
- /record/live and the /record/stream SSE add `here` (players in the voice
  channel, matched through verified Discord links, D120) and `other` (the
  session the bot is busy with instead, SE-033). The bot, its status payload
  and /api/bot/* are untouched; BOT_CONTRACT_VERSION stays 2.
- Migration d4e6f8a0b2c3 (down a9b0c1d2e3f5): sessions.started_at (backfilled
  from confirmed_time for sessions in progress) and the versions' source CHECK
  gains quick_note. The one-head pin moves to its test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"4 of 5 here" counted everyone heard this recording, including someone who
spoke and then went home, because the status payload never said who had left.
Each speaker now carries `in_channel`, read from the voice channel's members
on every two-second rebuild (discord.py keeps that list current from
voice-state updates). Left off while the voice client has no channel, which
the backend reads as still here rather than as everyone leaving.

Additive and in the Redis recording-status payload, not /api/bot/*: no
contract bump. The backend already skipped `in_channel: false`; its test for
a speaker with no key (an older bot) is added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The web page's Start refused a session that was not confirmed or being played
and made a confirmed one live; /record start in Discord went straight to the
voice channel and did neither.

- Backend: POST /api/bot/sessions/{id}/recording-started {guild_id}. A session
  not bound to the guild is the usual 404 "Session not found"; D10 is a 409 in
  the web path's words; otherwise start_by_recording (D116). Additive.
- Bot: QuestBoardClient.post_recording_started, called by /record start after
  the session lookup and before it joins the channel. A 409 is shown to the GM
  as the backend wrote it and nothing is recorded; a 404 "Session not found"
  gets the existing not-found copy. FastAPI's bare 404 "Not Found" means an
  older backend: the client returns None, the cog logs it once at info and
  records as before. A backend that cannot be reached does not stop capture.
- A test pins the bare "Not Found" body the bot relies on.

Additive on both sides: BOT_CONTRACT_VERSION stays 2 (check-versions OK).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /campaigns/{id}/live has returned the session's own number since #760, but
the docs still said it was always null.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A confirmed session can now be recorded only from its campaign's
recording_start_window_minutes before its confirmed start (default 30,
0-1440); a session being played can always record, and a confirmed one with
no time is refused with its own sentence.

- session_service.recording_refusal is the one check (D10 and D123) that both
  the web /record/start and the bot's recording-started use, before the bot is
  asked and before anything is flipped. The 409 says when recording opens in
  the campaign's zone, with the bell's clock and the zone's abbreviation
  ("Session 14 starts at 7:00 PM CDT. Recording opens at 6:30 PM CDT."), the
  day as well when it is not today there, and starts_at / opens_at as ISO
  beside detail. The bot already shows a 409's detail as it is.
- campaigns.recording_start_window_minutes INT NOT NULL DEFAULT 30 with a
  0-1440 CHECK, added to the unmerged migration d4e6f8a0b2c3 (up/down/up
  checked). On CampaignResponse; GM-editable on PATCH /campaigns/{id}
  (0-1440; null refused); copies with "from one of yours". No page yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The live migration was written beside scenes, on a9b0c1d2e3f5. It now follows
the cards migration, and the one-head pin moves to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pooled-session guard (test_task_session_hygiene) covers all of app/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merge remote-tracking branch 'origin/main' into build/lane5-p3-live
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 54s
CI / Backend lint (ruff) (pull_request) Successful in 29s
CI / Feedback site tests (pull_request) Successful in 42s
CI / Bot/backend version sync (pull_request) Successful in 39s
CI / Bot tests and audit (pull_request) Successful in 2m18s
CI / Does this change touch the recording path? (pull_request) Successful in 24s
CI / Docker image build (pull_request) Successful in 2m12s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 12m58s
CI / Frontend tests, audit, and build (pull_request) Successful in 17m26s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 15m31s
CI / Backend migration, tests, and audit (pull_request) Successful in 29m8s
ec46f8cd6f
Sign in to join this conversation.
No description provided.