# FE handoff — persisted replay metrics (Pass 2) + metrics endpoint > **Courier doc** (backend → `../dispatch-web`, via the user). Per ORCHESTRATOR §7 > the backend does NOT write the FE repo; the FE orchestrator applies this delta > on its side (regenerate the in-repo `.dispatch/*.reference.md` snapshots + bump > the `file:` dep). `lsp references` does not span the two repos. Backend commit: > `6db12ff`. ## Versions - `@dispatch/wire` `0.3.0 → 0.4.0` (additive) - `@dispatch/transport-contract` `0.3.0 → 0.4.0` (additive) Pure-type, additive change — no breaking edits to existing types. ## New wire types (`@dispatch/wire`, re-exported by `@dispatch/transport-contract`) ```ts interface StepMetrics { stepId: StepId; // `#`, join key to the live stream usage: Usage; // { inputTokens, outputTokens, cacheReadTokens?, cacheWriteTokens? } ttftMs?: number; // time to first token (optional — clock + first-token gated) decodeMs?: number; // first token → stream end genTotalMs?: number; // stream start → end (== ttftMs + decodeMs when a first token was seen) } interface TurnMetrics { turnId: string; // plain wire turn id, join key to AgentEvents usage: Usage; // aggregate across all steps durationMs?: number; // turn wall-clock (optional — clock gated) steps: readonly StepMetrics[]; // per-step, in step order } ``` These are the **persisted, replayable** counterparts of the live `usage` / `step-complete` / `done` events (which remain transient and unchanged). ## New read endpoint `GET /conversations/:id/metrics` → `ConversationMetricsResponse`: ```ts interface ConversationMetricsResponse { turns: readonly TurnMetrics[] } ``` Semantics: - `turns` = every **sealed** turn's `TurnMetrics`, in **turn-append order**. - A turn appears only **after seal** (post-persist); an in-flight/unsealed turn is absent. - This is a **separate axis** from `GET /conversations/:id?sinceSeq=` (which returns seq-cursor chunk CONTENT). Metrics are keyed per **turn**, not per chunk, so they are **not** seq-filtered — hence a sibling route, not a field on the history response. - Unknown / metric-less conversation → `{ turns: [] }`. - CORS: same wildcard as the other routes. ## Suggested FE consumption On (re)opening a conversation, the chat feature can `GET /conversations/:id/metrics` once alongside the history hydrate (`?sinceSeq=`), then render historical tokens/latency per turn (and per step via `stepId`) — identical fields to what it already routes from the live `step-complete` / `usage` / `done` stream. TPS is still derived FE-side (`usage.outputTokens / decodeMs`); context-size proxy = `usage.inputTokens`. ## Invariants (confirmed live) - Persisted `TurnMetrics.usage` / `durationMs` and each `StepMetrics` (`stepId` + `usage` + `ttftMs`/`decodeMs`/`genTotalMs`) **byte-match** what the live stream emitted for the same turn (verified end-to-end against flash). - `stepId` is the SAME value on the live `step-complete`/`usage` events, the persisted `StepMetrics`, and the tool chunks — one grouping key across live + replay.