feat: correct an event and regenerate the summary (#424) #602

Merged
claude-bot merged 2 commits from feat/424-correct-and-recompose into main 2026-09-11 15:18:23 +00:00
Contributor

Part of #424, the correct-and-recompose half; the design decisions are on the issue.

The motivating case. Some errors are true to the transcript, so no model or detector can catch them. The GM said "You're in Ironroot Hold", meaning "you were". The event verified, and every summary since has put the party in Ironroot Hold. Now the GM rewrites that one event and regenerates.

What it does

  • The GM can strike or rewrite events. On the session page's event log (GM only), each event gets Strike or Correct (inline). Corrections are held on the page until Regenerate summary (N corrections), which asks for confirmation.
  • Regenerating creates a new run and re-runs only the compose step.
    • The new summarisation run copies the source run's beats with the corrections applied: gm_action ∈ {struck, rewritten}, plus gm_original_summary, recomposed_from_run_id and corrected_by_id.
    • It makes one model call, with no re-extraction, using the source run's stored speaker legend.
    • The source run is untouched, because runs are append-only.
  • The replaced summary is stored as the new run's previous_summary, and "Restore the summary from before" puts it back.
  • Visibility:
    • The event log now depends on who's reading. Players see corrected text and never a struck event; the GM sees struck events struck through, and the original wording of rewritten ones.
    • GM-rewritten events count as narratable even if their original failed verification; the original problems stay recorded.
  • The compose step is shared, not copied. Everything after validation in _summarise_from_beats moved verbatim into compose_from_validations, and a test pins that process_audio and a recompose send the same compose request for the same beats.

Async status

Follows the session lore run (POST /sessions/{id}/lore-proposals/generate), the closest GM-triggered LLM job with a status the page can poll:

  • Status columns on sessions: status, error, requested_at, run_id.
  • Status written before queueing. An unreachable broker gives a 503 and marks the job failed.
  • A 30-minute stale threshold after which the GM may retry.
  • Its own GM-only status endpoint, because the error text can quote the provider's reply.
  • Superseded results are discarded. A worker overtaken by a retry throws its result away, and if a reprocess finishes mid-job, the job fails rather than overwrite the newer summary.

Endpoints (GM-only, documented in docs/API.md)

  • POST /api/sessions/{id}/summarisation-runs/{run_id}/recompose → 202
  • GET /api/sessions/{id}/summary-recompose
  • POST /api/sessions/{id}/summarisation-runs/{run_id}/restore-previous-summary

Worth reviewing

  • Approval: nothing blocks editing an approved summary today, since withdrawing approval only exists to repost to Discord. So recompose and restore follow the same rule. The confirmation says the Discord copy won't change and points at Withdraw approval, and a test pins both halves.
  • Restore doesn't swap. It puts back previous_summary without saving the text it replaces. Hand edits made after a regenerate are lost on restore, and the confirmation says so.
  • Character names are approximated. The stored speaker legend has no character_name, so the member's first character is used. The prompt can only differ where a per-session character override was in effect.
  • Migration 9e0f1a2b3c4d (down 8d9e0f1a2b3c). summary_recompose_run_id is deliberately not a foreign key, to avoid a sessionssummarisation_runs cycle.

Checked

  • Full backend suite: 3281 passed, 13 skipped. That includes 38 new tests, and the request-level one stubs llama.cpp over HTTP and asserts the struck text is absent and the rewrite present.
  • Frontend: vitest 863 passed (10 new), and ESLint is clean.
  • Lint: ruff 0.4.4 check and format --check are clean.

🤖 Generated with Claude Code

Part of #424, the correct-and-recompose half; the design decisions are on the issue. **The motivating case.** Some errors are true to the transcript, so no model or detector can catch them. The GM said "You're in Ironroot Hold", meaning "you were". The event verified, and every summary since has put the party in Ironroot Hold. Now the GM rewrites that one event and regenerates. ## What it does - **The GM can strike or rewrite events.** On the session page's event log (GM only), each event gets **Strike** or **Correct** (inline). Corrections are held on the page until **Regenerate summary (N corrections)**, which asks for confirmation. - **Regenerating creates a new run and re-runs only the compose step.** - The new summarisation run copies the source run's beats with the corrections applied: `gm_action` ∈ {struck, rewritten}, plus `gm_original_summary`, `recomposed_from_run_id` and `corrected_by_id`. - It makes one model call, with no re-extraction, using the source run's stored speaker legend. - The source run is untouched, because runs are append-only. - **The replaced summary is stored** as the new run's `previous_summary`, and **"Restore the summary from before"** puts it back. - **Visibility:** - The event log now depends on who's reading. Players see corrected text and never a struck event; the GM sees struck events struck through, and the original wording of rewritten ones. - GM-rewritten events count as narratable even if their original failed verification; the original `problems` stay recorded. - **The compose step is shared, not copied.** Everything after validation in `_summarise_from_beats` moved verbatim into `compose_from_validations`, and a test pins that process_audio and a recompose send the same compose request for the same beats. ## Async status Follows the session lore run (`POST /sessions/{id}/lore-proposals/generate`), the closest GM-triggered LLM job with a status the page can poll: - **Status columns on `sessions`:** status, error, requested_at, run_id. - **Status written before queueing.** An unreachable broker gives a 503 and marks the job failed. - **A 30-minute stale threshold** after which the GM may retry. - **Its own GM-only status endpoint**, because the error text can quote the provider's reply. - **Superseded results are discarded.** A worker overtaken by a retry throws its result away, and if a reprocess finishes mid-job, the job fails rather than overwrite the newer summary. ## Endpoints (GM-only, documented in `docs/API.md`) - `POST /api/sessions/{id}/summarisation-runs/{run_id}/recompose` → 202 - `GET /api/sessions/{id}/summary-recompose` - `POST /api/sessions/{id}/summarisation-runs/{run_id}/restore-previous-summary` ## Worth reviewing - **Approval:** nothing blocks editing an approved summary today, since withdrawing approval only exists to repost to Discord. So recompose and restore follow the same rule. The confirmation says the Discord copy won't change and points at Withdraw approval, and a test pins both halves. - **Restore doesn't swap.** It puts back `previous_summary` without saving the text it replaces. Hand edits made after a regenerate are lost on restore, and the confirmation says so. - **Character names are approximated.** The stored speaker legend has no `character_name`, so the member's first character is used. The prompt can only differ where a per-session character override was in effect. - **Migration `9e0f1a2b3c4d`** (down `8d9e0f1a2b3c`). `summary_recompose_run_id` is deliberately not a foreign key, to avoid a `sessions` ↔ `summarisation_runs` cycle. ## Checked - **Full backend suite:** 3281 passed, 13 skipped. That includes 38 new tests, and the request-level one stubs llama.cpp over HTTP and asserts the struck text is absent and the rewrite present. - **Frontend:** vitest 863 passed (10 new), and ESLint is clean. - **Lint:** ruff 0.4.4 `check` and `format --check` are clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(webapp): correct an event, then regenerate the summary (#424)
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 49s
CI / Backend lint (ruff) (pull_request) Successful in 27s
CI / Bot tests and audit (pull_request) Successful in 1m45s
CI / Docker image build (pull_request) Successful in 29s
CI / Bot/backend version sync (pull_request) Successful in 31s
CI / Frontend tests, audit, and build (pull_request) Failing after 2m7s
CI / Backend migration, tests, and audit (pull_request) Successful in 7m33s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 21m19s
be17b6a443
Some summary errors are true to the transcript, so no model or detector can
catch them: the GM said "You're in Ironroot Hold", meaning "you were", the
event verified, and every summary since put the party there. A GM can now
strike an event or rewrite its text in the event log, and regenerate the
summary from the corrected list.

Runs stay append-only. Regenerate writes a new run: a copy of the source
run's beats with the corrections applied (gm_action struck/rewritten plus
gm_original_summary), recomposed_from_run_id, corrected_by_id, and
previous_summary holding the summary it replaced. The source run is never
touched. Only the compose step runs, using the source run's stored speaker
legend: no re-extraction, no transcription.

The compose step is shared, not copied: everything after validation in
_summarise_from_beats moved verbatim into compose_from_validations, and the
prompt header into summary_header, so process_audio and a recompose send the
same compose request for the same beats (pinned on the wire). A rewritten
beat is narrated even if it failed validation, keeping its problems; struck
beats never reach compose.

Asynchronous, GM-only (get_session_for_gm):
- POST /sessions/{id}/summarisation-runs/{run_id}/recompose -> 202, a Celery
  task composes. 409 while a job is pending and fresh; a job pending over 30
  minutes may be superseded and the old worker discards its result. A reprocess
  that lands meanwhile fails the job rather than being overwritten.
- GET /sessions/{id}/summary-recompose: pending/done/failed, error,
  requested_at, run_id, stale.
- POST /sessions/{id}/summarisation-runs/{run_id}/restore-previous-summary.
Status follows the lore-proposals/generate precedent (status columns on the
session, stale supersede, enqueue failure recorded as failed), exposed on its
own GM-only endpoint rather than SessionResponse.

Approval mirrors a hand edit: PATCH can change an approved summary without
withdrawing approval, so regenerate and restore are not blocked either; the
confirmation points to "Withdraw approval" for a Discord repost. A failure
leaves sessions.summary untouched and is shown.

The event log renders per viewer: players get corrected text and never a
struck event; the GM sees struck events struck through and the original
wording of rewritten ones.

Migration 9e0f1a2b3c4d, raw DDL. sessions.summary_recompose_run_id is
deliberately not a foreign key, to avoid a sessions <-> summarisation_runs
cycle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
claude-bot scheduled this pull request to auto merge when all checks succeed 2026-09-11 14:24:11 +00:00
ci: re-run after an unrelated CampaignDetail test flake (#424)
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 51s
CI / Backend lint (ruff) (pull_request) Successful in 28s
CI / Docker image build (pull_request) Successful in 20s
CI / Bot tests and audit (pull_request) Successful in 1m45s
CI / Bot/backend version sync (pull_request) Successful in 36s
CI / Frontend tests, audit, and build (pull_request) Successful in 2m10s
CI / Backend migration, tests, and audit (pull_request) Successful in 7m16s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 18m32s
f59d896b26
The frontend job failed on two content-packs tests in CampaignDetail.test.jsx,
a file this branch does not touch: the GM test saw no listPacks call and the
player test straight after it saw exactly the GM's call, so an async fetch
from one test landed in the next. The same tests passed on main's last run
and in this branch's local run of all 863. Tracked separately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
claude-bot deleted branch feat/424-correct-and-recompose 2026-09-11 15:18:23 +00:00
Sign in to join this conversation.
No description provided.