feat(bot,webapp): record even when the announcement can't be posted, and tell the GM (D171) #853

Merged
claude-bot merged 4 commits from fix/o3c-record-without-announcement into main 2026-09-26 21:23:00 +00:00
Collaborator

The owner's D171 (O3 option (c), confirmed 2026-09-26) amends #842 (#839).

What changes

  • Unchanged: both start paths (the web path and /record start) still go join → try to announce → listen. The announcement still goes to the campaign's channel, or the channel the command was run in, then falls back to the voice channel's own chat.
  • New: if neither post lands, the bot records anyway instead of disconnecting and refusing.
  • The GM is told plainly:
    • Web: announced_in is null in /record/start's reply and in /record/status and the SSE stream, so it survives a reload. The recording panel and the table's recording bar show: "Recording — but I couldn't post the announcement. Tell the table it's being recorded."
    • /record start: a private follow-up with the same words.
  • Logging: each failed post logs a WARNING with the session id and the reason, and so does the "nobody told" outcome. The key is never logged.
  • Contract: additive only (announced_in may be null). BOT_CONTRACT_VERSION stays 2, and check_version_sync.py passes.
  • Backend: sessions.py needed no change, since it already passes the bot's reply through.

Tests

  • Bot: the "both fail → refuse" tests are rewritten as "both fail → captured and warned". The ordering and fallback tests are kept, and there's a new HTTP relay test for null. Full suite: 487 passed, 1 skipped.
  • Backend: test_table_live.py gains "a start nobody could be told about still starts and warns"; 64 passed.
  • Frontend: RecordingPanel and GmTable banner tests, plus a describe() unit test. Full suite: 2,475 passed; eslint has 0 errors.

Note: CLAUDE.md's consent-model paragraph still describes the announcement as the one requirement. It's left for the owner to update.

🤖 Generated with Claude Code

The owner's D171 (O3 option (c), confirmed 2026-09-26) amends #842 (#839). ## What changes - **Unchanged:** both start paths (the web path and `/record start`) still go join → **try to announce** → listen. The announcement still goes to the campaign's channel, or the channel the command was run in, then falls back to the voice channel's own chat. - **New:** if neither post lands, the bot **records anyway** instead of disconnecting and refusing. - **The GM is told plainly:** - **Web:** `announced_in` is `null` in `/record/start`'s reply and in `/record/status` and the SSE stream, so it survives a reload. The recording panel and the table's recording bar show: *"Recording — but I couldn't post the announcement. Tell the table it's being recorded."* - **`/record start`:** a private follow-up with the same words. - **Logging:** each failed post logs a WARNING with the session id and the reason, and so does the "nobody told" outcome. The key is never logged. - **Contract:** additive only (`announced_in` may be `null`). `BOT_CONTRACT_VERSION` stays 2, and `check_version_sync.py` passes. - **Backend:** `sessions.py` needed no change, since it already passes the bot's reply through. ## Tests - **Bot:** the "both fail → refuse" tests are rewritten as "both fail → captured and warned". The ordering and fallback tests are kept, and there's a new HTTP relay test for `null`. Full suite: **487 passed, 1 skipped**. - **Backend:** `test_table_live.py` gains "a start nobody could be told about still starts and warns"; 64 passed. - **Frontend:** RecordingPanel and GmTable banner tests, plus a `describe()` unit test. Full suite: **2,475 passed**; eslint has 0 errors. **Note:** CLAUDE.md's consent-model paragraph still describes the announcement as the one requirement. It's left for the owner to update. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The owner has decided against #842's refusal (D171, O3 option (c)): a
recording start no longer disconnects and refuses when neither the
campaign's channel nor the voice channel's own chat will take the
announcement. It records anyway, since the group's standing consent
does not depend on the post landing, and warns the GM instead.

- bot/questboard_bot/cogs/recording.py: both start paths keep the
  announce-then-listen order and the channel-to-voice-chat fallback,
  but no longer leave/refuse when both fail. `announced_in` gains a
  third value, None, carried on `_RecordingSession` and threaded into
  every live-status read (not just the immediate reply) so the warning
  survives a page reload. `/record start` gets the same warning as a
  private ephemeral follow-up. Each failed post is logged with the
  session id and why, never the key.
- bot/questboard_bot/services/recording_status.py: `build_recording_status`
  carries `announced_in` through to `/record/status` and the SSE stream.
- webapp/frontend/src/pages/session/recording.js,RecordingPanel.jsx and
  webapp/frontend/src/pages/table/GmTable.jsx: a warning line while
  recording ("Recording — but I couldn't post the announcement. Tell
  the table it's being recorded."), read from `announced_in === null`
  strictly, so an older bot that never sent the key says nothing.
- Tests updated: bot's two "both fail" tests now assert the recording
  starts and warns instead of refusing; the slash-command test asserts
  the same ephemeral warning. Added tests for the null passthrough at
  the bot's HTTP layer, the backend proxy, and both frontend surfaces.
- CHANGELOG: a [bot][webapp] Changed bullet referencing #839.

Additive only — BOT_CONTRACT_VERSION stays 2 (verified with
scripts/check_version_sync.py). CLAUDE.md untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- bot/tests/test_recording.py: the API-start "records anyway" test
  expected the wrong fallback text_channel — the campaign channel
  object is kept when it exists (even though its post failed), not
  the voice channel; only an absent channel falls back to voice.
- webapp/backend/tests/test_table_live.py: ruff-format the new test's
  call to match the repo's pinned ruff 0.4.4 formatting.

Verified: bot suite 487 passed/1 skipped (python:3.12-slim, git +
requirements-git.txt); ruff check on the touched bot modules clean.
Backend tests/test_table_live.py 64 passed against qb-backend-test:latest
+ postgres:16; ruff format --check and ruff check clean. Frontend: full
suite via the throwaway node:24-alpine image — lint 0 errors (208
pre-existing warnings), 176 test files / 2475 tests passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
style(webapp): curly apostrophes in the D171 warning line, to match frontend copy
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 54s
CI / Bot/backend version sync (pull_request) Successful in 29s
CI / Does this change touch the recording path? (pull_request) Successful in 29s
CI / Backend lint (ruff) (pull_request) Successful in 1m43s
CI / Feedback site tests (pull_request) Successful in 2m36s
CI / Docker image build (pull_request) Successful in 5m11s
CI / Bot tests and audit (pull_request) Successful in 6m8s
CI / Backend migration, tests, and audit (pull_request) Successful in 15m25s
CI / Frontend tests, audit, and build (pull_request) Successful in 19m58s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 13m55s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Failing after 18m45s
ac93371621
Rendered UI text in this codebase uses curly apostrophes (VoteGrid.jsx
etc); the D171 warning banner and its test/rename tweak had gone in
with straight ones. The bot's own Discord-side warning stays straight,
matching its existing user-facing strings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merge origin/main (#854) into D171
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m37s
CI / Bot tests and audit (pull_request) Successful in 2m47s
CI / Backend lint (ruff) (pull_request) Successful in 27s
CI / Docker image build (pull_request) Successful in 18s
CI / Bot/backend version sync (pull_request) Successful in 49s
CI / Does this change touch the recording path? (pull_request) Successful in 26s
CI / Feedback site tests (pull_request) Successful in 1m29s
CI / Backend migration, tests, and audit (pull_request) Successful in 31m33s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 12m47s
CI / Frontend tests, audit, and build (pull_request) Successful in 37m58s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 16m28s
c44788b850
Sign in to join this conversation.
No description provided.