Corrections that wait, and every version of a summary (D31, IS-379/D51, lane 4) #808

Merged
claude-bot merged 4 commits from build/lane4-versions into main 2026-09-24 07:35:17 +00:00
Collaborator

Lane 4 (#730), server only. The Review page that uses it comes in a later lane 4 PR, and the old session page works unchanged.

Corrections that persist (D31)

  • New table session_pending_corrections, one per session and shared by the campaign's GMs, with who made each correction and when (D104).
  • Endpoints (GM only):
    • GET /api/sessions/{id}/pending-corrections returns {base_run_id, stale, corrections, updated_at}. stale means the summary has changed underneath.
    • PUT …/pending-corrections/{beat_id} replaces that beat's set. It validates the whole set with apply_corrections, so a bad combination is a 422. An empty list clears the beat, which is Undo.
    • DELETE …/pending-corrections clears them all.
    • A stale set refuses additions with a 409 but can always be cleared.
  • Recompose accepts {use_pending: true}. The applied rows are deleted only if the rewrite succeeds; a failed rewrite keeps them. The old body shape still works.

Every version of a summary (IS-379 / D51)

  • New append-only table session_summary_versions. sessions.summary stays the one text every reader uses.
  • Every writer of the summary records a version in the same transaction: processing, a rewrite, a restore, a hand edit (only when the text changed), import, and the new put-back.
  • Backfill: each existing summary becomes a legacy row, and so does every earlier text a regenerate kept. The current summary is always the newest row.
  • Endpoints (GM only):
    • list, newest first, with showing and a preview;
    • one version with its full text;
    • POST …/put-back with expected_updated_at. It returns 409 while a rewrite is in flight or if the page is stale. It is a no-op if that text is already showing. It fires the same follow-ups as a hand edit.
  • Built to be reused: TextVersionMixin plus a generic text_versions service, so the table note's history (D97) reuses them in the notes PR.
  • Erasing a member leaves every version as written (D94).

Migration

c5d6e7f8a9b1, chained after f3a4b5c6d7e8 (#807's numbering). The single-head pin moved to test_review_tables_migration. Run for real against a fresh database, through the whole chain: up, down one, and up again.

Checks

  • Full backend suite on the tree merged with main: 4234 passed, 13 skipped. Ruff 0.4.4 format and check are clean.
  • New tests: pending corrections (27), summary versions (21), and the migration (13: backfill, invariants, idempotence, round trip, constraints and cascades).
  • docs/API.md documents the endpoints.

🤖 Generated with Claude Code

Lane 4 (#730), server only. The Review page that uses it comes in a later lane 4 PR, and the old session page works unchanged. ## Corrections that persist (D31) - **New table `session_pending_corrections`**, one per session and shared by the campaign's GMs, with who made each correction and when (D104). - **Endpoints** (GM only): - `GET /api/sessions/{id}/pending-corrections` returns `{base_run_id, stale, corrections, updated_at}`. `stale` means the summary has changed underneath. - `PUT …/pending-corrections/{beat_id}` replaces that beat's set. It validates the whole set with `apply_corrections`, so a bad combination is a 422. An empty list clears the beat, which is Undo. - `DELETE …/pending-corrections` clears them all. - A stale set refuses additions with a 409 but can always be cleared. - **Recompose accepts `{use_pending: true}`.** The applied rows are deleted only if the rewrite succeeds; a failed rewrite keeps them. The old body shape still works. ## Every version of a summary (IS-379 / D51) - **New append-only table `session_summary_versions`.** `sessions.summary` stays the one text every reader uses. - **Every writer of the summary records a version in the same transaction:** processing, a rewrite, a restore, a hand edit (only when the text changed), import, and the new put-back. - **Backfill:** each existing summary becomes a `legacy` row, and so does every earlier text a regenerate kept. The current summary is always the newest row. - **Endpoints** (GM only): - list, newest first, with `showing` and a preview; - one version with its full text; - `POST …/put-back` with `expected_updated_at`. It returns 409 while a rewrite is in flight or if the page is stale. It is a no-op if that text is already showing. It fires the same follow-ups as a hand edit. - **Built to be reused:** `TextVersionMixin` plus a generic `text_versions` service, so the table note's history (D97) reuses them in the notes PR. - **Erasing a member leaves every version as written (D94).** ## Migration `c5d6e7f8a9b1`, chained after `f3a4b5c6d7e8` (#807's numbering). The single-head pin moved to `test_review_tables_migration`. Run for real against a fresh database, through the whole chain: up, down one, and up again. ## Checks - Full backend suite on the tree merged with main: **4234 passed, 13 skipped**. Ruff 0.4.4 format and check are clean. - New tests: pending corrections (27), summary versions (21), and the migration (13: backfill, invariants, idempotence, round trip, constraints and cascades). - `docs/API.md` documents the endpoints. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Lane 4's server half of the Review tab needs two tables, in one migration
(c5d6e7f8a9b1, on e1f2a3b4c5d6):

- session_pending_corrections (D31): a GM's pending strikes, rewrites,
  relabels and emphases, one row per (session, beat, action), shared by
  every GM of the campaign, each naming who made it and the run it was
  made against. Every FK cascades but created_by.
- session_summary_versions (IS-379, D51): every text sessions.summary has
  held, append-only, newest showing; created_at is clock_timestamp().

The backfill gives each summarised session its history: every recomposed
run's previous_summary at that run's time, then the current summary at
coalesce(transcript_updated_at, updated_at) -- but never before a
microsecond after the newest of those runs, since transcript_updated_at is
older than any regenerate and would otherwise put today's summary behind
the text it replaced. The newest row always equals sessions.summary.

The version columns live in a TextVersionMixin so the table note's history
(PR E) can be a second table of the same shape.

The contract's revision id, a3b4c5d6e7f8, is already taken
(account_deletion_tombstone), so this uses c5d6e7f8a9b1. The one-head pin
moves to test_review_tables_migration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Saved corrections (D31). GET/DELETE /api/sessions/{id}/pending-corrections
and PUT .../pending-corrections/{beat_id}, GM-only. A PUT replaces one
beat's set (an empty list is Undo) and validates the whole saved set with
apply_corrections against the base run, so what is saved is always what a
regenerate would take: a bad combination is a 422 in its own words. The
first save fixes the base to the session's latest run; once a newer run
exists the set is stale -- GET says so, adding is a 409, clearing is not.
A PUT while a regenerate runs is a 409. The session row is locked while a
save validates, so two GMs cannot each validate without the other's row.

recompose takes {"use_pending": true}. The job gets the rows' ids and
deletes exactly those, in the savepoint that writes the summary, so a
failed, superseded or overtaken job keeps them. A list in the body (the
old page) is untouched, and the task's new argument is optional so a
message queued before this release still runs.

Summary versions (IS-379, D51). One helper, summary_versions.record, is
called in the same transaction as every write of sessions.summary:
process_audio (pipeline, with its run), the recompose task (rewrite, with
the run and the correction count), restore-previous-summary (put_back,
naming the version it copied), PATCH (gm_edit, only when the words
change), and import. GET .../summary-versions lists them newest first with
showing on the newest; GET .../{version_id} adds the text; POST
.../put-back writes the summary, appends a put_back row, stamps updated_at
and fires announce_summary_changed -- exactly what a hand edit fires. It
is a 409 while a regenerate runs or on a stale expected_updated_at, and a
no-op that writes nothing for a version that already reads as the summary.

Generic half in text_versions (record, list, showing, put-back's no-op
rule, preview, people) and schemas/text_version, keyed on the model's
VERSION_PARENT, for the table note's history in PR E. Erasure leaves
versions alone (D94). Account deletion repoints a version's author to the
tombstone, as it does a wiki version's.

The new endpoints live in routers/session_review.py; /api/bot/* and the old
session page are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
API.md gains the five new endpoints and use_pending on recompose, and notes
that restore-previous-summary now records a put_back version. One [webapp]
bullet at the top of [Unreleased] -> Added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Merge remote-tracking branch 'origin/main' into build/lane4-versions
All checks were successful
CI / Does this change touch the recording path? (pull_request) Successful in 10s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Bot/backend version sync (pull_request) Successful in 24s
CI / Backend lint (ruff) (pull_request) Successful in 1m10s
CI / Feedback site tests (pull_request) Successful in 1m24s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m36s
CI / Bot tests and audit (pull_request) Successful in 2m42s
CI / Docker image build (pull_request) Successful in 3m7s
CI / Frontend tests, audit, and build (pull_request) Successful in 3m46s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 8m42s
CI / Backend migration, tests, and audit (pull_request) Successful in 16m12s
44cf946025
# Conflicts:
#	CHANGELOG.md
#	webapp/backend/tests/test_audio_processed_at_migration.py
#	webapp/backend/tests/test_notifications.py
Sign in to join this conversation.
No description provided.