One notes model: just me and the GM, the table, GMs only — with history; the journal entry stops (D30, D97, D98, D106, lane 4) #811

Merged
claude-bot merged 8 commits from build/lane4-notes into main 2026-09-24 16:37:50 +00:00
Collaborator

Lane 4 (#730).

⚠ For self-hosters: GMs can now read every session note, including notes that were private before (D97, D106)

The owner decided this on 2026-09-24: nothing is hidden from a campaign's GMs, and that includes notes written before the upgrade.

  • A player's private note becomes their Just me and the GM note.
  • A GM's private note joins the campaign's GMs only note.
  • The bot's /note now says "Your GM can read them."

The model

  • session_notes.visibility is now mine, table or gm (text with a CHECK; the enum type is dropped).

    • mine is one per person per session.
    • table and gm are one shared note per session, with updated_by for "Last changed by Marcus on Tuesday".
  • Every save of a shared note is kept. session_note_versions is built on #808's TextVersionMixin, and any version can be put back.

  • The data migration:

    • a player's private note becomes mine;
    • sessions.session_notes and every GM's public note, including the journal task's entries, become the table note;
    • every GM's private note becomes the GMs only note.

    Several sources are joined under — {name} lines, so no words are lost and it's clear who wrote what. sessions.session_notes is dropped. Session.session_notes is now a read-only column_property holding the table note, so the session response, the bot's summary and the export keep working.

  • The downgrade keeps every word. The table note goes back to the column. The GMs-only note goes to its creator's private note, never to a public one, so a downgrade can't expose GMs-only notes to players.

Endpoints

  • GET /api/sessions/{id}/notes:
    • a player gets {mine, table};
    • a GM gets {mine, table, gm, players}, where players is every player's own note (D97/D106).
  • PUT /api/sessions/{id}/notes/{audience} takes {content, expected_updated_at, source}.
    • A 409 returns current, so the page can offer "Show me his".
    • A player writing gm gets a 403.
    • A GM writing mine gets a 422: "A GM's notes are for GMs only or the table."
  • GET …/notes/{audience}/versions and POST …/put-back, for table (any member) and gm (GMs).

Compatibility until lane 4's pages replace the old ones

  • The session response's session_notes and PATCH {session_notes} read and write the table note, with a version.
  • /my-notes and /my-note keep their shapes.
  • The retired journal (/campaigns/{id}/my-notes) answers from the notes.
  • pages/Sessions.jsx's "Move these notes" writes the target's table note, recorded as a moved version.
  • The bot's /note appends to your own note, or to the GMs-only note for a GM.
  • /recap labels the field "Table notes". The request and response shapes are unchanged, so there's no contract bump.
  • Export is schema 11. Import accepts versions 2–11.
  • Account deletion:
    • your own notes go;
    • shared notes you created move to the tombstone user, and ones you last edited keep their words with no editor;
    • your versions move to the tombstone user.

The journal entry stops (D98)

  • It is no longer queued. The tasks are registered no-ops, so queued messages don't crash.
  • Its toggles are gone from Settings → Recording and from the old manage page. The field stays inert in the API.

Migration

e9a0b1c2d3f4, chained after d6e7f8a9b1c2 (#810). The single-head pin moved to test_notes_migration.

Checks, on the tree merged with main (#807–#810)

  • Backend, full suite: 4345 passed. The 8 failures were one merge interaction with #810: the bot summary's db.get lazy-loaded the new column outside the greenlet. That's fixed in c5967d4c, and those modules pass (107).
  • Bot: 450 passed.
  • Frontend: 2031 passed; lint shows 0 errors (226 warnings, one fewer than main).
  • The migration chain was run for real: up, down one, and up again. The data migration's 9 tests cover every old row kind, the downgrade and the round trip.
  • Ruff and version sync are clean. D106 is in the decisions table, with the catalogue rebuilt.

🤖 Generated with Claude Code

Lane 4 (#730). ## ⚠ For self-hosters: GMs can now read every session note, including notes that were private before (D97, D106) The owner decided this on 2026-09-24: nothing is hidden from a campaign's GMs, and that includes notes written before the upgrade. - A player's private note becomes their **Just me and the GM** note. - A GM's private note joins the campaign's **GMs only** note. - The bot's `/note` now says "Your GM can read them." ## The model - `session_notes.visibility` is now `mine`, `table` or `gm` (text with a CHECK; the enum type is dropped). - `mine` is one per person per session. - `table` and `gm` are **one shared note per session**, with `updated_by` for "Last changed by Marcus on Tuesday". - **Every save of a shared note is kept.** `session_note_versions` is built on #808's `TextVersionMixin`, and any version can be put back. - **The data migration:** - a player's private note becomes `mine`; - `sessions.session_notes` and every GM's public note, including the journal task's entries, become the **table** note; - every GM's private note becomes the **GMs only** note. Several sources are joined under `— {name}` lines, so no words are lost and it's clear who wrote what. `sessions.session_notes` is dropped. `Session.session_notes` is now a read-only `column_property` holding the table note, so the session response, the bot's summary and the export keep working. - **The downgrade keeps every word.** The table note goes back to the column. The GMs-only note goes to its creator's private note, **never to a public one**, so a downgrade can't expose GMs-only notes to players. ## Endpoints - **`GET /api/sessions/{id}/notes`**: - a player gets `{mine, table}`; - a GM gets `{mine, table, gm, players}`, where `players` is every player's own note (D97/D106). - **`PUT /api/sessions/{id}/notes/{audience}`** takes `{content, expected_updated_at, source}`. - A 409 returns `current`, so the page can offer "Show me his". - A player writing `gm` gets a 403. - A GM writing `mine` gets a 422: "A GM's notes are for GMs only or the table." - **`GET …/notes/{audience}/versions`** and **`POST …/put-back`**, for `table` (any member) and `gm` (GMs). ## Compatibility until lane 4's pages replace the old ones - The session response's `session_notes` and `PATCH {session_notes}` read and write the table note, with a version. - `/my-notes` and `/my-note` keep their shapes. - The retired journal (`/campaigns/{id}/my-notes`) answers from the notes. - `pages/Sessions.jsx`'s "Move these notes" writes the target's table note, recorded as a `moved` version. - The bot's `/note` appends to your own note, or to the GMs-only note for a GM. - `/recap` labels the field "Table notes". The request and response shapes are unchanged, so there's **no contract bump**. - Export is schema 11. Import accepts versions 2–11. - Account deletion: - your own notes go; - shared notes you created move to the tombstone user, and ones you last edited keep their words with no editor; - your versions move to the tombstone user. ## The journal entry stops (D98) - It is no longer queued. The tasks are registered no-ops, so queued messages don't crash. - Its toggles are gone from Settings → Recording and from the old manage page. The field stays inert in the API. ## Migration `e9a0b1c2d3f4`, chained after `d6e7f8a9b1c2` (#810). The single-head pin moved to `test_notes_migration`. ## Checks, on the tree merged with main (#807–#810) - Backend, full suite: 4345 passed. The 8 failures were one merge interaction with #810: the bot summary's `db.get` lazy-loaded the new column outside the greenlet. That's fixed in `c5967d4c`, and those modules pass (107). - Bot: 450 passed. - Frontend: 2031 passed; lint shows 0 errors (226 warnings, one fewer than main). - The migration chain was run for real: up, down one, and up again. The data migration's 9 tests cover every old row kind, the downgrade and the round trip. - Ruff and version sync are clean. D106 is in the decisions table, with the catalogue rebuilt. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
It wrote a paragraph after each approved session as a public note under the
session creator's name, for a journal D47 already retired. Under D97 that note
is a table note "last changed by Dana" that Dana never wrote, so it stops.

- The summary fan-out no longer queues generate_journal_entry, so
  post_session_tasks gets three "queued" marks, not four.
- generate_journal_entry and backfill_journal_entries stay registered, as
  no-ops that write nothing and record nothing, so a message already on the
  broker when the upgrade lands is acknowledged rather than refused. The
  worker no longer queues the backfill as it starts.
- The journal_entry key stays a known post_session_tasks key, so sessions
  that recorded one before still read as they did.
- The "Write a journal entry" switch goes from Settings -> Recording and the
  old campaign page's "Auto-generate session recaps" checkbox goes too;
  neither form sends journal_entry_enabled any more, so whatever it holds is
  left alone. The column and the API field stay.

The entries already written become ordinary table notes in the notes
upgrade that follows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A session had three note stores: sessions.session_notes, and per-person
private and public rows. They are one model now, with three audiences:
"Just me and the GM" (mine, a player's own), "The table" (one per session,
anyone at the table edits it) and "GMs only" (one per session). Nothing is
hidden from the GMs: they read every player's own note too (D97, D106).

The server:
- session_notes.visibility is TEXT + CHECK (mine, table, gm); the
  notevisibility enum is dropped. One mine per (session, user) and one table
  and one gm per session, by partial unique indexes. updated_by_id is who saved
  a note last; on a shared note user_id is who created it.
- session_note_versions, on #808's TextVersionMixin: every save of a table or
  gm note is kept, in the same transaction, with who; mine keeps no history.
- GET /api/sessions/{id}/notes -> {mine, table} for a player, plus {gm,
  players} for a GM. PUT .../notes/{audience} with a required, nullable
  expected_updated_at: a save over someone else's newer note is a 409 whose
  body carries theirs as `current`. A player writing gm is a 403; a GM writing
  mine a 422. GET .../notes/{audience}/versions and POST
  .../versions/{id}/put-back, reusing text_versions as the summary does.

Migration e9a0b1c2d3f4 (down_revision d7e8f9a0b1c3), in order: a player's
private note becomes their mine; sessions.session_notes becomes the table note
(a legacy version); every public note, the journal task's included, is added
to it; every GM's private note makes the gm note. With more than one text,
each is headed by a line "— {name}" and the combined text is a migrated
version. sessions.session_notes is dropped: Session.session_notes is a
read-only column_property over the table note, so SessionResponse, the bot's
summary and the export keep the field. The downgrade keeps every word, but not
who wrote it: the table note goes back into sessions.session_notes and the gm
note becomes its creator's private note (not public, which the old journal
showed to every member).

Compatibility until lane 4's pages replace the old ones:
- PATCH /sessions/{id} {session_notes} saves the table note, versioned.
- /my-notes and /my-note: a player's private is their mine; a GM's private is
  the gm note and their public the table note; a player's public is still
  downgraded. Same shapes, same optional #403 token.
- GET /api/campaigns/{id}/my-notes answers from the reader's mine and the
  table note.
- The bot's POST /api/bot/sessions/{id}/notes appends to a player's mine and
  to the gm note for a GM (who has no mine); its summary's session_notes is
  the table note. Same shapes: no contract bump.

Account deletion deletes the person's mine notes; shared notes they created
move to the tombstone, ones they saved last keep their text and forget who,
and their versions read "Deleted user". The export is schema 11 (the three
audiences, no session_notes); the import takes 2-11, and puts a member's own
note into the GMs' note under their name, since a stub has no account.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Sessions list's merge read the GM's own public note from the cancelled
session and wrote it as the GM's public note on the target. Both are the
table note now: it reads the source's table note through
GET /api/sessions/{id}/notes and saves it over the target's with
PUT .../notes/table, source "moved", so the target's history says where the
text came from. The modal says it replaces the target's table note, and the
save claims the version it is replacing rather than being refused for it.

api/sessions.js gains fetchSessionNotes and saveSessionNote for the new
endpoints; fetchMyNotes and upsertMyNote stay for the old session page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
/note's description and the /help line called it a private note. Since D97 a
player's notes are "Just me and the GM" — every GM of the campaign can read
them — so the reply is now "Saved to your notes for <session>. Your GM can
read them.", and the description, /help and docs/INTEGRATIONS.md say the same.
/recap labels the session's notes "Table notes" rather than "GM Notes": they
are the table note now, which anyone at the table can write.

Text only. The request and the response are unchanged, so no contract bump.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The owner's answer of 2026-09-24: from the upgrade, every GM of the campaign
can read the session notes that were private. A player's private note becomes
their "Just me and the GM" note; a GM's joins the campaign's "GMs only" note.
Added after D104 in sources/intro.md and carried into CATALOGUE.md; every
coverage/lane*.py still classifies all of its placements.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts:
#	CHANGELOG.md
#	docs/design/redesign-surfaces/CATALOGUE.md
#	docs/design/redesign-surfaces/sources/intro.md
#	webapp/backend/app/routers/bot.py
#	webapp/backend/tests/test_cancel_reason_migration.py
#	webapp/backend/tests/test_notifications.py
The D96 test merged from main still read the field as GM Notes; the
notes model renamed it (D97).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(backend): the bot's summary loads the session row whole
All checks were successful
CI / Does this change touch the recording path? (pull_request) Successful in 34s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Backend lint (ruff) (pull_request) Successful in 39s
CI / Feedback site tests (pull_request) Successful in 50s
CI / Bot/backend version sync (pull_request) Successful in 1m4s
CI / Docker image build (pull_request) Successful in 1m15s
CI / Bot tests and audit (pull_request) Successful in 1m57s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 2m51s
CI / Frontend tests, audit, and build (pull_request) Successful in 7m27s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 9m1s
CI / Backend migration, tests, and audit (pull_request) Successful in 20m36s
c5967d4cb9
After merging D96, the endpoint fetched the session with db.get, which
can return an identity-map instance that lacks the table-note
column_property (D97); reading it then lazy-loaded outside the
greenlet (MissingGreenlet). A populate_existing select loads it with
the row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign in to join this conversation.
No description provided.