A summary reaches Discord only once the GM approves it; the accuracy check moves into Quest Board (D96, D105, bot contract 2, lane 4) #810

Merged
claude-bot merged 10 commits from build/lane4-discord-on-approval into main 2026-09-24 15:23:02 +00:00
Collaborator

Lane 4 (#730). D96, D105, and D95 (one approval, which the server already had).

⚠ Upgrade the backend and bot together: BOT_CONTRACT_VERSION 1 → 2

This is the first contract bump. A version-1 bot reads a withheld summary as "nothing was recorded". Its /recap would tell people to record the session again, and the session-completed post would say "No recap yet — Use /record". The bot is changed to say the summary is waiting for the GM's approval.

What changed

  • Processing posts nothing to Discord.

    • On success, the GMs are told in Quest Board, exactly as before: the bell's "A transcript to review · Session N", and Needs you.
    • On a failure or queue timeout, each GM gets a new in-app transcript_failed notification ("A recording could not be transcribed · Session N"), and nothing goes to the channel. It is on the grid under GM tasks, so email follows the GM's settings.
  • Approval posts the summary. It is the first post, or, for a session whose draft went out before this change, the old delete-then-repost.

  • No unapproved summary text reaches Discord by any route. One rule, in app/services/discord_summary.py (content_approved_at IS NOT NULL OR summary_discord_message_id IS NOT NULL), gates:

    • the final reminder's "Previously on";
    • /api/bot/sessions/{id}/summary, which feeds /recap and the session-completed excerpt, and carries an additive summary_status;
    • the ✦ in /history's list;
    • /history's summaries;
    • /ask's sources.

    The public party tally is no longer sent to the bot.

  • D105: summaries already posted before the upgrade count as cleared, since they are public already. Only a summary never posted waits for approval.

  • The accuracy check is in Quest Board.

    • PUT and DELETE /api/sessions/{id}/transcript-feedback take {rating: accurate|issues|wrong}.
    • Any member may answer until the summary is approved; after that it's a 409.
    • The session response carries your own my_feedback; the tally is for GMs only.
    • The Read tab (a later lane 4 PR) draws "Does this look right?". Until then players have no way to answer, which is accepted because no release is cut in between.
  • The bell draws transcript_failed (NOTE_KIND).

  • Docs: CLAUDE.md's and bot/CLAUDE.md's pipeline descriptions (owner-approved), ARCHITECTURE.md, docs/API.md, docs/INTEGRATIONS.md, and D105 in the decisions table, with the catalogue rebuilt and all coverage scripts passing.

Migration

d6e7f8a9b1c2 widens the notification-kind CHECK. It is chained after d7e8f9a0b1c3 (#809), and the single-head pin moved to test_summary_on_approval.

Checks, on the tree merged with main (#807–#809)

  • Backend, full suite: 4309 passed. The 2 failures were the new bell tests expecting titles; they're fixed to "Session N".
  • After the D105 commit, a targeted run passed: 354 across 19 modules.
  • Bot, full suite: 448 passed, 1 skipped.
  • Frontend: 144 files, 2030 tests; lint shows 0 errors.
  • Version sync: "app 4.4.1, bot contract v2". Ruff 0.4.4 is clean.

Known edges

  • A pre-D96 session whose summary is later rewritten stays cleared by its old draft's id. A rewrite is always the GM's or an admin's own act.
  • The GM's session_notes still show publicly in /recap, as before. The notes model PR (D97) feeds that from the table note.

🤖 Generated with Claude Code

Lane 4 (#730). D96, D105, and D95 (one approval, which the server already had). ## ⚠ Upgrade the backend and bot together: `BOT_CONTRACT_VERSION` 1 → 2 This is the first contract bump. A version-1 bot reads a withheld summary as "nothing was recorded". Its `/recap` would tell people to record the session again, and the session-completed post would say "No recap yet — Use `/record`". The bot is changed to say the summary is waiting for the GM's approval. ## What changed - **Processing posts nothing to Discord.** - On success, the GMs are told in Quest Board, exactly as before: the bell's "A transcript to review · Session N", and Needs you. - On a **failure or queue timeout**, each GM gets a new in-app `transcript_failed` notification ("A recording could not be transcribed · Session N"), and nothing goes to the channel. It is on the grid under GM tasks, so email follows the GM's settings. - **Approval posts the summary.** It is the first post, or, for a session whose draft went out before this change, the old delete-then-repost. - **No unapproved summary text reaches Discord by any route.** One rule, in `app/services/discord_summary.py` (`content_approved_at IS NOT NULL OR summary_discord_message_id IS NOT NULL`), gates: - the final reminder's "Previously on"; - `/api/bot/sessions/{id}/summary`, which feeds `/recap` and the session-completed excerpt, and carries an additive `summary_status`; - the ✦ in `/history`'s list; - `/history`'s summaries; - `/ask`'s sources. The public party tally is no longer sent to the bot. - **D105: summaries already posted before the upgrade count as cleared**, since they are public already. Only a summary never posted waits for approval. - **The accuracy check is in Quest Board.** - `PUT` and `DELETE /api/sessions/{id}/transcript-feedback` take `{rating: accurate|issues|wrong}`. - Any member may answer until the summary is approved; after that it's a 409. - The session response carries your own `my_feedback`; the tally is for GMs only. - The Read tab (a later lane 4 PR) draws "Does this look right?". **Until then players have no way to answer**, which is accepted because no release is cut in between. - **The bell** draws `transcript_failed` (`NOTE_KIND`). - **Docs:** `CLAUDE.md`'s and `bot/CLAUDE.md`'s pipeline descriptions (owner-approved), `ARCHITECTURE.md`, `docs/API.md`, `docs/INTEGRATIONS.md`, and D105 in the decisions table, with the catalogue rebuilt and all coverage scripts passing. ## Migration `d6e7f8a9b1c2` widens the notification-kind CHECK. It is chained after `d7e8f9a0b1c3` (#809), and the single-head pin moved to `test_summary_on_approval`. ## Checks, on the tree merged with main (#807–#809) - Backend, full suite: 4309 passed. The 2 failures were the new bell tests expecting titles; they're fixed to "Session N". - After the D105 commit, a targeted run passed: 354 across 19 modules. - Bot, full suite: 448 passed, 1 skipped. - Frontend: 144 files, 2030 tests; lint shows 0 errors. - Version sync: "app 4.4.1, bot contract v2". Ruff 0.4.4 is clean. ## Known edges - A pre-D96 session whose summary is later rewritten stays cleared by its old draft's id. A rewrite is always the GM's or an admin's own act. - The GM's `session_notes` still show publicly in `/recap`, as before. The notes model PR (D97) feeds that from the table note. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
A session's summary reaches the campaign's channel only when a GM approves
it. process_audio no longer publishes session_summarised on success, on
failure, or on the queue timeout:

- Success already wrote the GMs' "A transcript to review" row and everyone's
  recap row directly (#655), and Home's Needs you reads the session's
  `ready` status, so the GM still hears a summary is waiting without the
  Discord draft.
- Failure and the queue timeout now write a new GM-only notification kind,
  `transcript_failed` ("A recording could not be transcribed · <title>",
  gm_tasks row), instead of posting a failure notice to the whole table.
  The bot has no notification DM path yet (lane 10), so it is in-app only.
  Migration f3a4b5c6d7e8 widens the kind CHECK; the one-head pin moves to
  test_summary_on_approval.

Approval is unchanged on the backend: it already sends the stored draft id,
which is null for anything processed from now on. The bot's
_handle_summary_approved skips the delete when the id is falsy
(`if old_message_id:`) and then posts, so no bot change and no
BOT_CONTRACT_VERSION bump. Two bot tests pin both cases (delete then post;
post only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The table's accuracy check used to be reactions on the Discord draft, which
no longer exists. It moves to the app:

- PUT /api/sessions/{id}/transcript-feedback {rating: accurate|issues|wrong}
  upserts the caller's one row in transcript_feedback (a single
  INSERT ... ON CONFLICT, so a double click cannot 500 on the unique key);
  DELETE removes only the caller's row and is idempotent. Any member of the
  campaign (403 otherwise), only while the session has a summary, and 409
  once the GM has approved it. The bot's reaction endpoints stay, for drafts
  already in channels, and share the same row.
- Its own path, not /feedback: that is the star-and-wish prompt's (#727),
  where a DELETE would read as withdrawing a star, which the prompt forbids.
- GET /api/sessions/{id} carries `my_feedback` (the caller's own answer) for
  every member. `transcript_feedback_counts` is now GM-only: a counts object
  for a GM, zeros before anyone answers, and null for a player, who used to
  receive it too.

Approval's docstring and comment now say what it is: the first and only
post to Discord.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CHANGELOG [webapp] Changed bullet; API.md documents PUT/DELETE
/api/sessions/{id}/transcript-feedback, `my_feedback`, the GM-only
`transcript_feedback_counts`, and the `transcript_failed` kind in the bell;
ARCHITECTURE.md and INTEGRATIONS.md stop describing a draft posted when
processing finishes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
f3a4b5c6d7e8 is the session-numbering migration's (#807). Renamed to
d6e7f8a9b1c2, which nothing on origin/main or any fetched branch uses;
down_revision stays e1f2a3b4c5d6, to be re-chained at merge. The kind-CHECK
test, the model comment and the one-head pin follow it. Upgrade, downgrade
and upgrade again run clean on a fresh postgres 16.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
transcript_failed was the server's thirteenth kind and NOTE_KIND did not map
it, so the bell dropped the row while the badge counted it — "1 unread" over
"Nothing needs you". It takes the transcript icon, like transcript_ready. The
new AppFrame test fails with the mapping removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The draft post at processing was the loud route. These were the quiet ones,
now all on the GM's sign-off, `content_approved_at`:

- The final reminder's "Previously on" excerpt selected on the audio being
  trashed/approved, a stand-in for approval that "bin it once transcribed"
  broke by trashing audio at processing. It selects on content_approved_at.
- GET /api/bot/sessions/{id}/summary (/recap, and the session-completed
  embed's excerpt) returns `summary: null` until approval, with an additive
  `summary_status: "approved" | "awaiting_approval" | null`, and never the
  accuracy tally (`feedback_counts: null`), which /recap posted publicly.
- GET /api/bot/guilds/{g}/sessions/list marks has_summary (/history's ✦,
  which points at /recap) for approved summaries only.
- GET /api/bot/guilds/{g}/sessions/history returns approved summaries only.
- POST /api/bot/guilds/{g}/sessions/ask answers from approved summaries
  only, and says `reason: "awaiting_approval"` when summaries exist and none
  is approved yet.

Why the bump: a version-1 bot reads the withheld null as "nothing was
recorded". /recap (cogs/sessions.py, `if not has_summary and not has_notes`)
told the reader "No transcript or GM notes have been recorded ... Use
/record start", and the session-completed embed (cogs/notifications.py,
`else:` branch) nudged the GM to record — both false for a recorded session
awaiting approval, and both an invitation to record it twice. The bot now
says the summary is waiting for the GM's approval in both places, and in
/ask for the new reason. BOT_CONTRACT_VERSION goes to 2 on both sides, with
the pinning tests; check_version_sync passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts:
#	CHANGELOG.md
#	webapp/backend/tests/test_audio_processed_at_migration.py
#	webapp/backend/tests/test_notifications.py
Merging main brought session numbering, and the bell now says
'Session N' where these tests expected the title.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The owner's decision: a summary the bot posted as a draft before D96 is
public already, so gating it protects nothing. Only a summary that has never
been posted waits for approval.

One rule in one place, app/services/discord_summary.py:
summary_cleared_for_discord(session) and its SQL form,
content_approved_at IS NOT NULL OR summary_discord_message_id IS NOT NULL.
Every gated route now uses it: the final reminder's "Previously on",
GET /api/bot/sessions/{id}/summary (/recap and the session-completed
excerpt), /list's has_summary, /history, and /ask, including /ask's
"awaiting approval" count, which now counts only summaries not cleared.
summary_status stays "approved" for a cleared summary; the bot only ever
tests for "awaiting_approval", so it needs no third value and no bot change.

summary_discord_message_id is never cleared: only the bot's
summary-message-id endpoint writes it, approval sends it to the bot to delete
that draft and leaves the column as it is, and since D96 nothing asks for a
new draft.

Tests: every route has a case with no approval and a stored message id
(shown) beside one with neither (waits), plus the rule's truth table.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
docs: the pipeline says what D96 changed; D105, already-posted summaries
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 53s
CI / Bot/backend version sync (pull_request) Successful in 25s
CI / Does this change touch the recording path? (pull_request) Successful in 17s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Backend lint (ruff) (pull_request) Successful in 1m20s
CI / Feedback site tests (pull_request) Successful in 1m41s
CI / Bot tests and audit (pull_request) Successful in 3m22s
CI / Frontend tests, audit, and build (pull_request) Successful in 4m0s
CI / Docker image build (pull_request) Successful in 3m59s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 7m46s
CI / Backend migration, tests, and audit (pull_request) Successful in 22m17s
17abfc29d1
CLAUDE.md and bot/CLAUDE.md said processing fires session_summarised
back to the bot. Under D96 it tells the GMs in Quest Board, and the
summary reaches Discord only when a GM approves it. Both descriptions now
say so; the owner approved the edit on 2026-09-24.

D105 records the owner's rule for summaries already in Discord: one
already posted counts as cleared. The catalogue is rebuilt, and all
coverage scripts pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign in to join this conversation.
No description provided.