PR: #555: feat(mira): hydrate the persisted thread on panel mount [Story-2223] by @ankitmaheshwari-com
View PR →

Story-2223: Mira panel hydrates the persisted thread on mount

Mira's conversation turns already persisted server-side (functions #623 — agents.chat_turn with inline content). The panel kept the thread across minimise/reopen because the rail connection lives in a module-scoped registry. But a module is per-page-load: a full reload wiped it, so Mira reopened empty while both turns sat in the database. This story makes the panel read that durable thread back on mount.

The shape of the fix

The rail's thread was only ever written by live WebSocket frames. Nothing read the durable copy. So the change is one new inbound path — two generated reads feeding one new store method — and no change at all to how live turns arrive.

  1. useAppListAgentSessions(matterUuid, { agentKey: "mira_matter_chat" }) finds the counsel's live Mira session for this matter. The list is newest-active first, so the head is the thread to replay.
  2. useAppLoadAgentSessionTurns(sessionId) loads that session's turns, oldest-first. The generated hook's enabled gate means a matter with no session issues no turns request at all.
  3. chatTurnsFromPersistedTurns() maps the wire onto the rail's vocabulary, and connection.hydrateThread(sessionId, turns) seeds the thread.

Files

FileChange
src/lib/agent/gateway.ts Two server reads (listMatterSessions, loadSessionTurns), each parsing the upstream through assertContract at the trust boundary (HR-21).
src/app/api/internal/matters/[matterId]/agent-sessions/route.ts
src/app/api/internal/agent-sessions/[sessionId]/turns/route.ts
New BFF proxy routes — the browser only ever talks to /api/internal/*. Query params are allow-listed; a missing agentKey is rejected here rather than round-tripped for a 422.
src/lib/kubb-client.ts Two toBrowserRoute mappings so the generated client's absolute /app/* paths land on those proxies.
src/lib/agents/mira-rail.ts MIRA_AGENT_KEY and the pure chatTurnsFromPersistedTurns() mapper.
src/lib/agents/atlas-rail-store.ts hydrateThread() on the rail connection.
src/lib/agents/use-mira-conversation-rail.ts Wires the two reads to hydrateThread; exposes isHydrating.
src/components/agents/mira-dock-mount.tsx Withholds the first-run greeting while hydrating.

Two boundary details the wire forces

Why the greeting disappears while loading

The panel's empty state says, in effect, "ask me anything". Showing that to a counsel whose conversation is one fetch away tells them their history is gone. So while isHydrating is true the thread renders nothing. This costs no new copy (and so no new i18n keys), and in practice is almost never seen: the dock's open state is ephemeral by design, so by the time a counsel reopens the panel after a reload, hydration has landed.

Never destroying live truth

hydrateThread carries two guards, both about the same worry:

Hydration also restores the history that the next launch sends the brain, so the first question after a reload is no longer asked in a vacuum.

Storybook coverage

StoryPurpose
MatterDetail/MiraDock / Hydrating The moment after a reload: thread area empty on purpose, no greeting.
MatterDetail/MiraDock / Restored The persisted turns landed — counsel's question and Mira's answer survive the reload.
MatterDetail/MiraDock / Open Unchanged: identity, thread, "Do it" chip, composer.

Tests

Verified live (system boundary, not fixtures)

Run against the staging gateway and staging data, signed in, on M-0013 — the matter from the issue's repro. Session 5029bd10-… holds exactly two inline turns.

AC 2 (streaming) could not be verified live, and is not blocked by this change. Every Mira run launched during verification hung and ended DISCONNECTED. Chased through the planes rather than guessed: the broker accepted the launch (websocket_message_enqueued in pramaan-ws-handler-staging), but the consumer pramaan-clerk-worker-staging dies on ConnectTimeoutError: Connect timeout on endpoint URL: "https://ssm.ap-south-1.amazonaws.com/" while resolving the agent runtime from the SSM registry, so Mira's AgentCore runtime is never invoked (no runtime log lines for these runs) and pramaan-agents-runs messages sit in flight. An com.amazonaws.ap-south-1.ssm interface endpoint was created at 2026-07-10T04:48:47Z, but its ENI exists in only one of the worker's three subnets.

This is a pramaan-functions/infra gap, independent of this repo: the launch message this change produces is byte-identical to the pre-change one except that history is now non-empty on the first message after a reload — and the broker enqueued it without complaint. Streaming, minimise/reopen, and the "Do it" escalation are untouched code paths.

Files changed

11 file(s), +441 / -10 lines

Path Role + -
src/app/api/internal/agent-sessions/[sessionId]/turns/route.tsApp route+45-0
src/app/api/internal/matters/[matterId]/agent-sessions/route.tsApp route+64-0
src/components/agents/mira-dock-mount.tsxComponent+4-1
src/components/matter-detail/ccb/mira-dock.stories.tsxComponent+40-2
src/lib/agent/gateway.tsLibrary+48-5
src/lib/agents/__tests__/atlas-rail-store.test.tsLibrary+69-0
src/lib/agents/atlas-rail-store.tsLibrary+35-0
src/lib/agents/mira-rail.test.tsLibrary+32-0
src/lib/agents/mira-rail.tsLibrary+35-0
src/lib/agents/use-mira-conversation-rail.tsLibrary+47-2
src/lib/kubb-client.tsLibrary+22-0

Unrelated defect noticed

The Brief page for M-0012 crashes the workspace with TypeError: Cannot read properties of undefined (reading 'value') at src/components/matter-detail/brief/frame-summary-history.tsx:387 (EditableFieldWithHistory), on a matter with a sparse frame. Out of scope here; surfaced for a separate Bug.