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 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.
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.
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.
chatTurnsFromPersistedTurns() maps the wire onto the rail's vocabulary, and
connection.hydrateThread(sessionId, turns) seeds the thread.
| File | Change |
|---|---|
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.tssrc/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. |
user/assistant; the rail speaks counsel/atlas.
Anything that is not user renders as the agent, so a future
system/tool role degrades to agent prose rather than vanishing.
content is null when the body overflowed to S3.
contentPreview is always inline, so it is the honest fallback: a truncated
turn beats a blank one.
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.
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.
| Story | Purpose |
|---|---|
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. |
mira-rail.test.ts — role mapping, S3-overflow preview fallback, unknown role renders as agent.atlas-rail-store.test.ts — seeds an empty thread; idempotent per session; never
overwrites live turns; an empty session is marked hydrated without touching the thread.
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.
READY, both persisted turns rendered, no greeting.
GET /api/internal/matters/ea90a732-…/agent-sessions?agentKey=mira_matter_chat → 200,
then GET /api/internal/agent-sessions/5029bd10-…/turns → 200 with 2 turns.
{"sessions":[]}, no turns request is issued, and the
greeting shows. (The doctrine's "fixtures lie in your favour" case, exercised for real.)
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.
11 file(s), +441 / -10 lines
| Path | Role | + | - |
|---|---|---|---|
| src/app/api/internal/agent-sessions/[sessionId]/turns/route.ts | App route | +45 | -0 |
| src/app/api/internal/matters/[matterId]/agent-sessions/route.ts | App route | +64 | -0 |
| src/components/agents/mira-dock-mount.tsx | Component | +4 | -1 |
| src/components/matter-detail/ccb/mira-dock.stories.tsx | Component | +40 | -2 |
| src/lib/agent/gateway.ts | Library | +48 | -5 |
| src/lib/agents/__tests__/atlas-rail-store.test.ts | Library | +69 | -0 |
| src/lib/agents/atlas-rail-store.ts | Library | +35 | -0 |
| src/lib/agents/mira-rail.test.ts | Library | +32 | -0 |
| src/lib/agents/mira-rail.ts | Library | +35 | -0 |
| src/lib/agents/use-mira-conversation-rail.ts | Library | +47 | -2 |
| src/lib/kubb-client.ts | Library | +22 | -0 |
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.