[Workbench] Beat notes never reach the frontend — "Append to beat notes" silently replaces, and Beat Planner can wipe saved notes #303

Closed
opened 2026-08-12 00:44:55 +00:00 by claude-bot · 3 comments
Contributor

Symptom

"Append to beat notes" on a generated Session Prep sheet appears to do nothing. Reported during real session prep.

Cause

beat_notes is not in the session list response schema. SessionListItem exposes id, campaign_id, title, scheduling_mode, status, confirmed_time, end_time, created_at, has_summary — no beat_notes. Nor is it on the detail schema SessionResponse. The only endpoint that touches it is PATCH /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_notes is therefore always undefined:

1. "Append" is actually "replace"workbenchTools.jsx:542-545:

const current = sessions.find((s) => s.id === sessionId)?.beat_notes || "";  // always ""
const next = current ? `${current}\n\n${formatted}` : formatted;             // always `formatted`
await updateBeatNotes(campaignId, sessionId, next);

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 null over 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

  • Add beat_notes: str | None to SessionResponse (GM-visible detail) and either to SessionListItem or a dedicated planning-scoped read (e.g. return the current value from a GET .../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. Check get_session gating: SessionResponse is served to all campaign members, so a GM-only projection is likely the cleaner route.
  • Make append genuinely append against a freshly-read value rather than a possibly-stale list entry, and only enable the button once the current value is known.
  • Have Beat Planner load the stored notes into its draft and refresh after a Workbench write, so the two surfaces cannot clobber each other. Consider guarding "Save Notes" against submitting an untouched empty draft.
  • Frontend test coverage: workbenchTools.test.jsx:507-530 asserts the current (broken) append call shape — it passes because the mock session fixture has no beat_notes either. Update it to assert real concatenation.

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.

## Symptom "Append to beat notes" on a generated Session Prep sheet appears to do nothing. Reported during real session prep. ## Cause `beat_notes` is **not in the session list response schema**. [`SessionListItem`](webapp/backend/app/schemas/session.py#L92-L111) exposes `id`, `campaign_id`, `title`, `scheduling_mode`, `status`, `confirmed_time`, `end_time`, `created_at`, `has_summary` — no `beat_notes`. Nor is it on the detail schema [`SessionResponse`](webapp/backend/app/schemas/session.py#L135-L177). The only endpoint that touches it is `PATCH /api/campaigns/{id}/planning/sessions/{sid}/beat-notes` ([campaigns.py:4499-4520](webapp/backend/app/routers/campaigns.py#L4499-L4520)) — **write-only; there is no read path at all.** Every frontend read of `beat_notes` is therefore always `undefined`: **1. "Append" is actually "replace"** — [workbenchTools.jsx:542-545](webapp/frontend/src/components/workbenchTools.jsx#L542-L545): ```js const current = sessions.find((s) => s.id === sessionId)?.beat_notes || ""; // always "" const next = current ? `${current}\n\n${formatted}` : formatted; // always `formatted` await updateBeatNotes(campaignId, sessionId, next); ``` 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](webapp/frontend/src/pages/CampaignPlanning.jsx#L256-L269)), 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 `null` over 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 - Add `beat_notes: str | None` to `SessionResponse` (GM-visible detail) and either to `SessionListItem` or a dedicated planning-scoped read (e.g. return the current value from a `GET .../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. Check `get_session` gating: `SessionResponse` is served to all campaign members, so a GM-only projection is likely the cleaner route. - Make append genuinely append against a freshly-read value rather than a possibly-stale list entry, and only enable the button once the current value is known. - Have Beat Planner load the stored notes into its draft and refresh after a Workbench write, so the two surfaces cannot clobber each other. Consider guarding "Save Notes" against submitting an untouched empty draft. - Frontend test coverage: [workbenchTools.test.jsx:507-530](webapp/frontend/src/components/workbenchTools.test.jsx#L507-L530) asserts the current (broken) append call shape — it passes because the mock session fixture has no `beat_notes` either. 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](webapp/backend/app/services/generation_service.py#L1094-L1105)), and in practice they are being wiped before it can read them. See the companion prep-context issue.
Author
Contributor

Cluster cross-reference — all four came out of the same session-prep run on 2026-08-11:

  • #302 — session pickers default to the furthest-out session
  • #303 (this) — beat notes round-trip / append-is-really-replace / Beat Planner wipe
  • #304 — generated output lost on tool switch; "Re-open" doesn't restore it
  • #305 — prep sheet regurgitates last session; no working channel for GM intent

This one is the root cause for most of #305 and carries a live data-loss risk, so it's the natural first fix.

Cluster cross-reference — all four came out of the same session-prep run on 2026-08-11: - **#302** — session pickers default to the furthest-out session - **#303** (this) — beat notes round-trip / append-is-really-replace / Beat Planner wipe - **#304** — generated output lost on tool switch; "Re-open" doesn't restore it - **#305** — prep sheet regurgitates last session; no working channel for GM intent This one is the root cause for most of #305 and carries a live data-loss risk, so it's the natural first fix.
Author
Contributor

Picking this up as v4.3.0 phase 3 (#514). Decision: a GM-only GET …/planning/sessions/{sid}/beat-notes mirroring 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.

Picking this up as v4.3.0 phase 3 (#514). Decision: a GM-only `GET …/planning/sessions/{sid}/beat-notes` mirroring 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.
Author
Contributor

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_notes is on no read schema. That was true when it was written, but #454 has since landed: the field is on SessionResponse, GM-gated in build_session_response exactly as transcript is, 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 the SessionResponse field rather than removing it. transcript sets 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 one BeatNotesEditor used 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.jsx is 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 as null rather than 404.

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_notes` is on no read schema. That was true when it was written, but **#454 has since landed**: the field is on `SessionResponse`, GM-gated in `build_session_response` exactly as `transcript` is, 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** the `SessionResponse` field rather than removing it. `transcript` sets 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 one `BeatNotesEditor` used 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.jsx` is 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 as `null` rather than 404.
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Quest-Board#303
No description provided.