[Frontend] Show a GM the verified events behind a summary #424
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The last open criterion from #423, reframed. That issue closed with the coverage floor raised from 0.091 to 0.727, but one criterion unmet: "a run that covers drastically fewer events than expected is visible to the GM rather than silent."
Why this is a UI issue and not a detector
I tried twice in #423 to detect a lossy run in code, comparing the composed summary against the beats it was built from. Both failed on real data, in opposite directions:
Across six live runs the detector flagged nothing while the eval harness measured 1–2 verified events per run missing from the prose. The harness only manages it because its fixtures carry hand-authored per-event mention groups; production has only the beat's own summary text, which is a much weaker signal.
beat_service.unrepresented_beatsstill exists and is correct in clear-cut cases, but it is not a foundation to build a warning on.The reframing: the GM does not need a machine to tell them something is missing. They were at the session. Show them the verified events beside the summary and they will see a gap in seconds — and, unlike a heuristic, they will also spot the summary that covers everything but characterises it wrongly.
What already exists
beat_service.validate_beatsproduces aBeatValidationper beat, carrying.okand, when not ok,.problemsnaming why ('Kira' did not speak any cited line,cites 00:41:12, which is not in the transcript).t_start/t_end,actors,kind, andevidence— cited transcript timestamps, already validated against the transcript.render_beats_for_composerenders them in time order.What does not exist, and is the real work
Beats are never persisted. They live in
_summarise_from_beatsfor the duration of one Celery task and are gone. There is no model, no table, no API. Showing them means storing them, which is a migration plus a router, and is most of this issue.Worth deciding at design time whether they are stored per summarisation run (so a reprocess produces a second set and the GM can compare) or last-write-wins. #423 established that two runs over the same audio genuinely differ, which makes the comparison view more interesting than it sounds.
Acceptance criteria
Notes
unrepresented_beats. It is retained for the retry path, where being occasionally wrong is cheap; it is not accurate enough to drive anything a GM is shown.webapp/backend/evals/results/history.jsonland reproducible withpython -m evals --fixtures evals/fixtures/private --provider live --repeat 4.Picking up the backend half now, in parallel with the v4.4.0 design phases (#535 phase 4); the screen that shows the events beside the summary comes in the session lane of the rebuild, once the design direction is chosen.
Decisions, so the screen does not decide them by accident:
GET /api/sessions/{id}/beats(latest run, or?run_id=) andGET /api/sessions/{id}/beats/runs.unrepresented_beats, as the body says.One migration, chained on the v4.3.0 head.
Correction: the backend half already shipped in v4.0.0. The body's premise ("beats are never persisted") was true when it was filed and is not true now. Two commits landed it:
2791c24(the rows:summarisation_runs+session_beats, migrationd5e6f7a8b0c1, extended byf7a8b0c1d2e3for provenance) andaa4bff9("expose the events behind a summary over the API (#333, #424)"), whose message already says this issue becomes a frontend-only job.What exists on main today, for whoever builds the screen:
GET /api/sessions/{id}/summarisation-runs— every run, newest first, each with its beats inline (so an older run is selected from this list; there is no?run_id=).GET /api/sessions/{id}/summarisation-runs/latest— 404 when the session has no run; the list returns[]instead, deliberately, because sessions summarised before v4.0.0 have no record and none can be reconstructed.get_session_for_gm. Documented indocs/API.md.t_start/t_end, actors, kind, evidence,ok,problemsandrepairs(a repaired beat passed, and a silent correction would otherwise be indistinguishable from a correct one).extraction_passes > 1.Verified on a fresh database: the full chain applies, one head (
0b1c2d3e4f5a), both tables with cascading FKs and the partial index on problems; 45 tests pass acrosstest_summarisation_runs.pyandtest_capability_gaps.py.Two notes for the remaining work: the service tests drive
session_beat_servicedirectly and there is no HTTP-level 403 test for these endpoints (a codebase-wide gap, not this issue's); and the first acceptance criterion is met, so the four that remain are all screen work, which lands in the session lane of the v4.4.0 rebuild.A case for letting the GM correct an event and regenerate the summary, not just see the events.
On the dev replay (Gemma 4 26B, run
a0784433, 2026-09-11), the summary says the journal "revealed that the party was actually inside Ironroot Hold". That's wrong, and no model, prompt or detector could be expected to catch it, because it's what the GM said:Thirty seconds later: "the point that you think you entered at next to Ironroot Hold and this point that you are at now, they are a considerable distance apart."
The GM misspoke, saying "you're in" for "you were in". The extracted event, "The GM reveals the party is in Ironroot Hold" (00:49:28), verifies correctly against the transcript. #584's check doesn't flag it either: it isn't a short reply to a player's question.
It has survived every model and pipeline change since the first Qwen run: Qwen 9B, Gemma before #587, Gemma after #587, and now Gemma after #591. The robust fix is the GM: when the verified events sit beside the summary, let the GM strike or rewrite one event ("The map marks where they entered the caves, just outside Ironroot Hold, a long way from here") and recompose the summary from the corrected list.
That costs one compose call, not a re-extraction, and it's exactly the correction the event log makes possible. It also gives the eval work in #586 a source of labelled false events. Worth designing into the Review screen that v4.4.0 is mocking up now.
Picking up the correct-and-recompose half (2026-09-11, owner's choice), ahead of the v4.4.0 rebuild. The API is the part that lasts. The UI goes into today's event log on the session page, and the rebuild's Review screen will reuse the API.
Design decisions, so the screen doesn't make them by accident:
recomposed_from_run_idand who corrected it. The new run runs only the compose step, one model call and no re-extraction, using the source run's own speaker legend.The motivating case is Ironroot Hold: rewrite that one event, regenerate, and the error is gone.
Correct and regenerate is merged (PR #602, main
8f26886) and deployed to dev (migration9e0f1a2b3c4d). All three new routes answer 401 unauthenticated, and the foursummary_recompose_*columns are present.The design as built (details in the PR):
compose_from_validationsprocess_audio uses.For the owner to try on dev: rewrite the Ironroot Hold event (00:49:28) on the replay session and regenerate.
One known limitation: restore doesn't keep hand edits made after a regenerate. The confirmation says so.
Still open: the rest of this issue's original criteria (showing the verified events beside the summary on the Review screen) belongs to the v4.4.0 rebuild, which will reuse these endpoints. Leaving the issue open for that.