feat(web): a transcript's call says when and why it ended, what it metered, and the model it ran on

Merged
DA
7d ago
+334-22

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/transcript now gives each call these optional fields:

    • endedAt, from closed_at
    • ending, the Live API's close reason from close_reason
    • usage, the metered { seconds, confirmed } from usage
    • model

    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. Migration 0089 adds a nullable voice_sessions.model. register writes 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.updated and session.closed report only seconds, so there are no tokens to store or answer. No cost is said anywhere.

Evidence

  • Platform-independent checks: ./scripts/check.sh exit 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.sh green; no renderer change in this PR.
  • JSON Schema goldens: transcript-wire-transcriptCallSchema.json and the two schemas that contain it (planTranscriptSchema, planTranscriptAnswerSchema) each gain the four optional properties endedAt (number ≄ 0), ending (the five Live close reasons), usage (seconds ≄ 0, confirmed boolean, both required within it), and model (string). Nothing else changed. Regenerated with LUKE_UPDATE_FIXTURES=1.
  • Every behavior change has its failing-without test:
    • plans-app.test.ts adds a new test: a call with a recorded close answers endedAt/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.md now lists the model among what the voice_sessions row records.
  • PRIVACY.md unchanged (the model id is not account data).
  • Package deps unchanged.

Notes

  • register now requires model, so 16 test call sites pass LIVE_DEFAULTS.MODEL.
  • Migration number 0089: if another PR lands an 0089 first, I'll renumber on rebase.

šŸ¤– Generated with Claude Code


Open workspace in Conductor


Devin Review