[Workbench] Beat notes never reach the frontend — "Append to beat notes" silently replaces, and Beat Planner can wipe saved notes #303
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?
Symptom
"Append to beat notes" on a generated Session Prep sheet appears to do nothing. Reported during real session prep.
Cause
beat_notesis not in the session list response schema.SessionListItemexposesid,campaign_id,title,scheduling_mode,status,confirmed_time,end_time,created_at,has_summary— nobeat_notes. Nor is it on the detail schemaSessionResponse. The only endpoint that touches it isPATCH /api/campaigns/{id}/planning/sessions/{sid}/beat-notes(campaigns.py:4499-4520) — write-only; there is no read path at all.Every frontend read of
beat_notesis therefore alwaysundefined:1. "Append" is actually "replace" — workbenchTools.jsx:542-545:
The PATCH succeeds and the button reports "Appended to beat notes." — but it has overwritten the GM's notes with the prep sheet alone. Append and Replace are the same operation today.
2. Nothing displays beat notes, so the write looks like a no-op — the Beat Planner textarea seeds from the same absent field (CampaignPlanning.jsx:256-262, 265-269), so it opens empty even when notes are saved. Going to Beat Planner to check on the appended sheet shows a blank box.
3. Data loss: because the textarea always starts empty, pressing "Save Notes" in Beat Planner without typing PATCHes
nullover whatever was stored. Any prep sheet written by (1) is destroyed by the next visit to (2), and vice versa. This likely explains beat notes that seem to vanish between visits.Proposed fix
beat_notes: str | NonetoSessionResponse(GM-visible detail) and either toSessionListItemor a dedicated planning-scoped read (e.g. return the current value from aGET .../beat-notes, or include it in a GM planning session list). It's GM-only content — do not put it on a schema players read. Checkget_sessiongating:SessionResponseis served to all campaign members, so a GM-only projection is likely the cleaner route.beat_noteseither. Update it to assert real concatenation.Related
This also blocks the Session Prep tool from reading GM intent — beat notes are the only channel the prep prompt has for "what I want to happen next session" (generation_service.py:1094-1105), and in practice they are being wiped before it can read them. See the companion prep-context issue.
Cluster cross-reference — all four came out of the same session-prep run on 2026-08-11:
This one is the root cause for most of #305 and carries a live data-loss risk, so it's the natural first fix.
Picking this up as v4.3.0 phase 3 (#514). Decision: a GM-only
GET …/planning/sessions/{sid}/beat-notesmirroring the PATCH rather than putting GM content on the session response players read; append reads the fresh value right before writing; Beat Planner loads stored notes and refreshes after a Prep write; "Save Notes" refuses an untouched empty draft; the test that pinned the broken append shape asserts real concatenation instead.Done in the phase 3 PR (auto-merging on green); ships with v4.3.0. One deliberate divergence from the issue text, flagged.
The issue says
beat_notesis on no read schema. That was true when it was written, but #454 has since landed: the field is onSessionResponse, GM-gated inbuild_session_responseexactly astranscriptis, with three tests pinning the gate. So the append already read a real value, and the Beat Planner already loaded stored notes.This adds the endpoint the issue asks for,
GET /api/campaigns/{id}/planning/sessions/{sid}/beat-notes, campaign-scoped, GM-only at the routing layer, same{beat_notes, updated_at}body as the PATCH, but keeps theSessionResponsefield rather than removing it.transcriptsets the precedent for GM-only session content on that response and the gate is tested; a second, contradictory convention for the field right next to it seemed worse than either convention alone. A new test asserts the two reads agree so they cannot drift. Stripping the field is a small follow-up if preferred.Everything else here is done: the client marks the read
cache: false(the shared 15-second GET cache would defeat a read-before-write); the editor is oneBeatNotesEditorused by Beat Planner and, after #305, by the Session Prep panel; Save is disabled while the draft still matches the server, which is the untouched-empty-draft guard; a failed read disables the box and the button outright with a Try again, because an empty draft the GM never saw is the worst possible thing to write back and a failed read also means no concurrency token; "Append" reads through the new endpoint immediately before writing and is disabled until it knows the notes are readable.workbenchTools.test.jsxis rewritten: the append test asserts real concatenation against the endpoint, one proves the read happens at click time (notes edited in another tab after the sheet arrived still win), one proves the button fails closed. Backend: GM ok, player 403, unknown session 404, another campaign's session 404, empty notes read asnullrather than 404.