feat(web): a transcript's call says when and why it ended, what it metered, and the model it ran on
Summary
Dean asked for the Transcript tab to say how long a call was and what it used, the way a turn ends on "Worked for ā¦" with its card. This is the data half; the desktop footer is the stacked PR on top of it.
-
The transcript read answers what each call's row already records.
GET /api/plans/transcriptnow gives each call these optional fields:endedAt, fromclosed_atending, the Live API's close reason fromclose_reasonusage, the metered{ seconds, confirmed }fromusagemodel
A row missing a field answers without it. That covers an earlier build's call, a call still running, or one never closed. They come from one extra query over the calls the read already gathered (
findCallRecords), so the 4,000-segment join carries no per-row duplicates. -
The model is now recorded. A deployment can pin
LIVE_MODEL, so the model a call ran on was not knowable after the fact. Migration0089adds a nullablevoice_sessions.model.registerwrites the session config's model there (options.model ?? LIVE_DEFAULTS.MODEL). Rows written before this stay null. -
No tokens. The Live API's
session.usage.updatedandsession.closedreport onlyseconds, so there are no tokens to store or answer. No cost is said anywhere.
Evidence
- Platform-independent checks:
./scripts/check.shexit 0 - macOS Electron verification (
./scripts/verify.sh): not run. The change is server and wire only; nothing renders differently in this PR. CI could not verify the Mac UI (LUKE-159 is the manual Mac pass).
Visual evidence
- Fixture screenshots inspected:
not applicable(no UI change; see the stacked desktop PR)
Physical-device evidence
- Screenshot or screen recording:
not attached - Device/display configuration:
not recorded
Invariants
-
./scripts/check.shgreen; no renderer change in this PR. - JSON Schema goldens:
transcript-wire-transcriptCallSchema.jsonand the two schemas that contain it (planTranscriptSchema,planTranscriptAnswerSchema) each gain the four optional propertiesendedAt(number ā„ 0),ending(the five Live close reasons),usage(secondsā„ 0,confirmedboolean, both required within it), andmodel(string). Nothing else changed. Regenerated withLUKE_UPDATE_FIXTURES=1. - Every behavior change has its failing-without test:
plans-app.test.tsadds a new test: a call with a recorded close answersendedAt/ending/usage/model, and an earlier build's call answers only its words.voice-session-record.test.ts: the registered row holds the model.voice-service.test.ts: the opener registers the session with the config's model.
- Gateway envelope goldens unchanged.
- No new
as, run outside edges, raw async primitive, or discarded effect. - Test count: +1 (
plans-app.test.ts). - Docs:
apps/web/server/README.mdnow lists the model among what thevoice_sessionsrow records. -
PRIVACY.mdunchanged (the model id is not account data). - Package deps unchanged.
Notes
registernow requiresmodel, so 16 test call sites passLIVE_DEFAULTS.MODEL.- Migration number
0089: if another PR lands an0089first, I'll renumber on rebase.
š¤ Generated with Claude Code