A session has a number: given at creation, frozen, never reused (#760, D86, lane 4) #807

Merged
claude-bot merged 6 commits from build/760-session-number into main 2026-09-24 06:45:03 +00:00
Collaborator

Closes #760. This is lane 4's first server PR (#730). D86: a number, given when the session is created, frozen and never reused; a cancelled session keeps its number, so the sequence has gaps.

Server

  • Migration f3a4b5c6d7e8 (on e1f2a3b4c5d6, single head):

    • Adds campaigns.next_session_number and sessions.number (NOT NULL) with UNIQUE (campaign_id, number).
    • Backfills in created_at order, as #760 asks. A tie, such as an import's shared created_at, is broken by when the session took place. Cancelled sessions are numbered.
    • The docstring says the backfill is a reconstruction, not a record.
    • It is re-runnable, and the downgrade drops everything.
  • A counter, not max + 1, so a deleted newest session's number is never reused. One UPDATE … RETURNING in the creating transaction, whose row lock serialises concurrent creates. session_numbering.allocate_session_number is called by:

    • create_session (and the one-shot);
    • the import;
    • series materialisation, which skips existing occurrences before allocating, so its hourly run doesn't burn numbers.
  • A before_flush safety net numbers any other ORM-created session from the same counter. That covers a future creation path, the eval seed and test fixtures.

  • Import and export: export writes number. Import keeps valid, unclaimed numbers, numbers the rest in the backfill's order, and leaves the counter above them all.

  • number is exposed on:

    • the session and session-list responses;
    • This week;
    • Home and Needs you (session_number);
    • the live endpoint;
    • search;
    • the cold open;
    • series sessions;
    • the timeline;
    • notifications.

    It is also on /api/bot/*, additively: no contract bump, and the bot is untouched.

The UI: "Session 14" where the drawing is tight

Everything goes through sessionLabel() in vocabulary.js:

  • the breadcrumb and phone title, and the tab title (Session 14 · Editing it · The Amberwake Accord · Quest Board);
  • the Contents sub-head;
  • the live bar;
  • the bell's sentences ("Session 14 is on: Mon 22 Sep, 8:00 PM");
  • Home's Needs you and campaign card ("Last time: Session 13, “The Drowned Halls”");
  • This week (label This week · Session 14 above the title heading);
  • the Sessions list (a number column; the title stays the row's name and link).

Journeys

  • The fixture titles lose their "Session N: " prefixes, and the seed checks the server's numbers (1/2/3).
  • J0, J1, J2 and J6 now assert the real numbers. J6's "there is no numbering (#760)" becomes This week · Session 3.

Checks

  • Backend, full suite: 4172 passed, 13 skipped.
  • Frontend: 144 files, 2027 tests. Lint: 0 errors, and the warning count is unchanged at 228.
  • Journeys: 27 passed and 3 skipped (J2's lane-4 half, by design), on desktop, phone and keyboard.
  • The migration was run for real on seeded data: upgrade, downgrade, upgrade. The numbers came out as expected and the counters were right.
  • After merging main (#805, #806): ruff clean, and the numbering, erasure, migration and campaign tests pass (178).

Not in this PR

  • The Storyline page, the Search panel's rows and MergeNotes' options don't draw the number yet; they're outside D86's places.
  • docs/API.md doesn't list the new fields.

Lane 4's page PRs pick these up.

🤖 Generated with Claude Code

Closes #760. This is lane 4's first server PR (#730). D86: a number, given when the session is created, frozen and never reused; a cancelled session keeps its number, so the sequence has gaps. ## Server - **Migration `f3a4b5c6d7e8`** (on `e1f2a3b4c5d6`, single head): - Adds `campaigns.next_session_number` and `sessions.number` (NOT NULL) with `UNIQUE (campaign_id, number)`. - Backfills in `created_at` order, as #760 asks. A tie, such as an import's shared `created_at`, is broken by when the session took place. Cancelled sessions are numbered. - The docstring says the backfill is a reconstruction, not a record. - It is re-runnable, and the downgrade drops everything. - **A counter, not `max + 1`,** so a deleted newest session's number is never reused. One `UPDATE … RETURNING` in the creating transaction, whose row lock serialises concurrent creates. `session_numbering.allocate_session_number` is called by: - `create_session` (and the one-shot); - the import; - series materialisation, which skips existing occurrences before allocating, so its hourly run doesn't burn numbers. - **A `before_flush` safety net** numbers any other ORM-created session from the same counter. That covers a future creation path, the eval seed and test fixtures. - **Import and export:** export writes `number`. Import keeps valid, unclaimed numbers, numbers the rest in the backfill's order, and leaves the counter above them all. - **`number` is exposed** on: - the session and session-list responses; - This week; - Home and Needs you (`session_number`); - the live endpoint; - search; - the cold open; - series sessions; - the timeline; - notifications. It is also on `/api/bot/*`, additively: **no contract bump**, and the bot is untouched. ## The UI: "Session 14" where the drawing is tight Everything goes through `sessionLabel()` in `vocabulary.js`: - the breadcrumb and phone title, and the tab title (`Session 14 · Editing it · The Amberwake Accord · Quest Board`); - the Contents sub-head; - the live bar; - the bell's sentences ("Session 14 is on: Mon 22 Sep, 8:00 PM"); - Home's Needs you and campaign card ("Last time: Session 13, “The Drowned Halls”"); - This week (label `This week · Session 14` above the title heading); - the Sessions list (a number column; the title stays the row's name and link). ## Journeys - The fixture titles lose their "Session N: " prefixes, and the seed checks the server's numbers (1/2/3). - J0, J1, J2 and J6 now assert the real numbers. J6's "there is no numbering (#760)" becomes `This week · Session 3`. ## Checks - Backend, full suite: 4172 passed, 13 skipped. - Frontend: 144 files, 2027 tests. Lint: 0 errors, and the warning count is unchanged at 228. - Journeys: 27 passed and 3 skipped (J2's lane-4 half, by design), on desktop, phone and keyboard. - The migration was run for real on seeded data: upgrade, downgrade, upgrade. The numbers came out as expected and the counters were right. - After merging main (#805, #806): ruff clean, and the numbering, erasure, migration and campaign tests pass (178). ## Not in this PR - The Storyline page, the Search panel's rows and MergeNotes' options don't draw the number yet; they're outside D86's places. - `docs/API.md` doesn't list the new fields. Lane 4's page PRs pick these up. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
`sessions.number` (NOT NULL, UNIQUE per campaign) and
`campaigns.next_session_number`, in migration f3a4b5c6d7e8 on top of
e1f2a3b4c5d6. The backfill numbers every session, cancelled ones
included, by created_at and then by when it was (confirmed time, else
its earliest proposed time) to break an import's tie, and says it is a
reconstruction rather than a record.

Numbers come from a counter, not max + 1, so deleting the newest session
never hands its number on: one UPDATE ... RETURNING in the creating
transaction, whose row lock serialises concurrent creates.
`allocate_session_number` is called by create_session (and so the
one-shot), the import and series materialisation; a before-flush hook
numbers any other ORM insert from the same counter. Materialisation asks
which occurrences exist before it allocates, so the hourly run burns
nothing.

Export writes the number; import keeps a usable one and numbers the rest
after it in the backfill's order. The number is on the session detail
and list, Home, This week, Needs you, the live endpoint, both searches,
the cold open, a series' sessions, the timeline and the bell, whose rows
now say "Session 14". Additive on /api/bot/*: no contract bump.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`sessionLabel(session)` in vocabulary.js: "Session N", the title only when
the number is missing (an old cached response), else "Session".

- The crumb and the tab: a session page declares its session with its
  campaign, and the frame names it "Session 14" (shelf, table and edit
  alike); "Session" until the page has said which.
- The Contents: on a session's own pages Sessions is current, with the
  session as its sub-head. Contents now draws a sub-head on its own.
- The live bar reads through sessionLabel.
- Home: Needs you rows name the session by number beside the campaign;
  a card's This week leads with it; Last time reads
  Session 13, "The Drowned Halls".
- This week: the title stays the heading, with "This week · Session 14"
  above it; an untitled session is headed by its number; Last time and
  the table card name it by number.
- Sessions list: the drawing's leading "S14" column is back, read out as
  "Session 14"; an untitled row reads "Session 15 · no title yet".

The bell's rows are the server's sentences, which now say "Session 14".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The fixture titles carried "Session 12: " and so on because nothing
numbered a session. Now the product does, so the prefixes go (they would
read "Session 1 · Session 12: The Salt Marshal") and identities.json
carries the numbers the seed's three sessions get in a fresh campaign —
1, 2 and 3 — which the seed checks against what the server said.

J0 and J2 find the upcoming session on Home's card, in Needs you and in
the bell by its number; J6 turns "there is no numbering" into the label
"This week · Session 3" and reads Last time as number and title; J1
asserts its fresh campaign's sessions are Session 1 and Session 2, on
This week and in the Sessions list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Merge remote-tracking branch 'origin/main' into build/760-session-number
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 3m2s
CI / Backend lint (ruff) (pull_request) Successful in 26s
CI / Feedback site tests (pull_request) Successful in 41s
CI / Bot/backend version sync (pull_request) Successful in 24s
CI / Does this change touch the recording path? (pull_request) Successful in 16s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Frontend tests, audit, and build (pull_request) Failing after 5m54s
CI / Bot tests and audit (pull_request) Failing after 2m52s
CI / Backend migration, tests, and audit (pull_request) Failing after 6m0s
CI / Docker image build (pull_request) Successful in 3m4s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Has been cancelled
c6f4d3b24a
ci: re-run after the runner's Docker daemon dropped mid-job
All checks were successful
CI / Bot/backend version sync (pull_request) Successful in 28s
CI / Does this change touch the recording path? (pull_request) Successful in 25s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Feedback site tests (pull_request) Successful in 40s
CI / Backend lint (ruff) (pull_request) Successful in 1m15s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 2m46s
CI / Docker image build (pull_request) Successful in 3m37s
CI / Bot tests and audit (pull_request) Successful in 4m57s
CI / Frontend tests, audit, and build (pull_request) Successful in 7m16s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 6m2s
CI / Backend migration, tests, and audit (pull_request) Successful in 19m44s
070250d738
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign in to join this conversation.
No description provided.