Skip to content

feat(operator): promote the room to a column on jobs [D-646] - #237

Merged
delasous merged 7 commits into
mainfrom
feat/d-646-job-room-columns
Sep 8, 2026
Merged

delasous merged 7 commits into
mainfrom
feat/d-646-job-room-columns

Conversation

@delasous

@delasous delasous commented Sep 2, 2026 •

Copy link
Copy Markdown
Member

Part of D-646. Takes the two pieces with a named consumer; the rest is deferred with reasons on the ticket.

What changes

The room becomes a real column. Migration 0008 adds room_token and room_name to jobs and recovers them from talk_binding for every job already recorded. SetJobTalkBinding derives them from the same JSON on every write, so the blob and the columns cannot disagree. A blank value means "nothing to say", never "set to empty" — the room name arrives on a second write, so without that rule every job whose name lookup was slow or failed would have its name blanked by its own re-persist.

GET /jobs returns room_name. room_token gets a column but is left out of the response, for the same reason talk_binding is. Note this is consistency, not a new boundary: the token is already reachable on this endpoint inside request_json, which has always been returned.

The control panel names a recording after its conversation. As of D-711 a recording is labelled by its room token, Call 3qbs6123vxx — which identifies the call but not which conversation it was. Rows and the detail heading now prefer the conversation's display name where the operator has it, and D-711's labels remain the fallback. This adds a third optional argument to D-711's meetingLabel; the tests live beside its own in Operator.test.ts.

The roundtrip e2e asserts the published catalog carries the room. Every hop was unit-tested and no two had been run in sequence; the chain has broken here before (D-640: a catalog room silently dropped by the next re-seal). roomId is recomputed from the token rather than read back, so the assertion proves the whole token → id → catalog path. The catalog needs its own wait — the local sink writes it after the .opus phase 7 polls for.

Relationship to D-711

D-711 landed on main (#253) while this branch was open and rebuilt the same two helpers, so the merge conflict was a real overlap. Main's shape won: parseRequestJSON / requestUrlLabel / meetingLabel exported from Operator.svelte's module script and covered by Operator.test.ts. This branch dropped its own extraction of those helpers into viewState.ts and now adds only what D-711 cannot know — the conversation's name, preferred over any token-derived label.

Not in this PR

  • Owner as a field. D-622's Done-when is room only; "and owner" is one clause in its setup paragraph that never became an acceptance criterion. talkRoomState.Owner is the identity the operator impersonates to read the room name and list participants, and both already work off talk_binding. No screen, query or user wants it as a field.
  • Rewriting the backfill scripts to read the new column. They work, ~1500 lines of shell tests cover them, and a DB from an older operator has no column.
  • The four backfill ergonomics items — polish on a maintenance script already run against prod.
  • The ACL rerun item → D-705, retracted: it is not a bug. D-594 already made a re-publish update the bytes and leave the rules alone.

Verification

go vet and go test ./... in cassini-operator; npm test (90) and npm run build:all in cassini-app; bash -n and shellcheck -x --severity=style on the roundtrip script. The e2e assertion's derivation was checked against the Go implementation for the same token, and against catalogs with a wrong name, a wrong id, no room fields, no meetings and two meetings.

Upgrade path checked end to end against a database built by the pre-change binary: seeded with a Talk job carrying a resolved room name, one without, and a non-Talk job; the new binary applied 0008, backfilled both room columns, and GET /jobs returned room_name for the first and null for the other two, with room_token, talk_binding, backend_url and owner all absent from the response.

The roundtrip itself does not run on an ordinary PR — it needs the full-ci label (self-hosted GPU, ~25 min).

Migration 0008 applies on operator start (OpenStore → ensureSchema, one transaction). No manual step, no script to run, and no change to any already-published catalog.

… [D-646]

A job's room existed only inside the opaque talk_binding blob, so every reader
parsed JSON to get at it and GET /jobs could not return it at all. Migration
0008 adds room_token and room_name and recovers them for every job already
recorded; SetJobTalkBinding keeps them in step on every write, deriving them
from the same JSON the migration read so the two cannot disagree.

The name is resolved asynchronously and arrives on a second write, so a blank
value means "nothing to say" and never "set to empty" — the rule
setMeetingBundleFields already documents. Without it every job whose name
lookup was slow, or failed, would have its name blanked by its own re-persist.

The token gets a column but not the API. For a public conversation the token is
also the link that joins it, so what leaves the operator stays a one-way
derivation of it and never the token itself (D-622) — the same reason
talk_binding has always been withheld.

The two migration-count assertions now compare against the embedded migration
set instead of a hand-written list, so they assert what they mean and do not
have to be edited by every migration that follows.
Every row in the job list read "Call a1b2c3d4" — the last path segment of the
request URL, which tells an operator nothing about which conversation it was.
Now that the room name reaches GET /jobs, the row and the detail heading use
it, and keep the token as the fallback for a non-Talk job, a job whose
room-name lookup never completed, and every job recorded before the column
existed.

meetingLabel and requestUrlLabel move into viewState.ts on the way, so the
preference is covered by a test rather than living untested inside the markup.
… [D-646]

Every hop from Talk to catalog.json was unit-tested and no two had been run in
sequence. The chain has broken here before: the exporter re-derives a catalog
entry's room from the file on every republish, so a room that reached the
catalog once could be silently dropped by the next re-seal (D-640).

The roundtrip already creates a conversation with a known token and name and
already drives record → build → seal → publish, so the assertion is the cheapest
place to close the gap. roomId is recomputed here from the token rather than
read back, which is the point: it proves the whole token → id → catalog path
instead of proving that some string round-tripped.

The catalog needs its own wait — the local sink writes it after the .opus that
phase 7 polls for, so the existing poll does not imply it is on disk.
@delasous
delasous marked this pull request as ready for review September 3, 2026 15:34
D-711 landed on main while this branch was open and rebuilt the same two label
helpers, so the conflict is a real overlap rather than a textual one.

Main's shape wins: parseRequestJSON / requestUrlLabel / meetingLabel exported
from Operator.svelte's module script and covered by Operator.test.ts. It labels
a Talk-triggered job `Call <roomToken>` instead of the generic "Recording" this
branch was written against, and it also fixes the Meeting URL field and the red
error banner — none of which this branch touched.

So this branch drops its own extraction of the two helpers into viewState.ts and
adds only what D-711 cannot know: an optional room name, preferred over every
token-derived label. `Call <roomToken>` says which call; the conversation's own
name says which conversation.

The changelog fragment is restated against the new baseline — the panel labelled
a recording by its token, not "Recording".
@github-actions

github-actions Bot commented Sep 5, 2026 •

Copy link
Copy Markdown

Merging this PR using merge would make these currently mergeable PRs conflict:

@delasous
delasous merged commit e4c9e7a into main Sep 8, 2026
31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant