Replay-on-rehydrate: re-run the bound render selection on page reload
Story-1794 shipped the render substrate with replay-re-run
deferred: the reference persists
({component_id, query_ref, capturedAt, renderCatalogVersion},
no rows, HR-23) but a cold page load showed only an empty
placeholder — the ponytail: note in the dock. This change
closes that gap. On rehydrate the dock re-runs the bound
selection by replaying the run that produced it through the
existing rail; the broker (#1799, merged) re-binds the
persisted selection with FRESH RLS at replay time, so the redelivered
render.data frame repopulates the component — never a stale
row (none are stored).
The split (why this is client-only, no functions change)
The render design is reference-only, so the fix is NOT to replay a
stored data frame — it is to re-bind the persisted
selection with fresh RLS at replay time. That re-bind happens
server-side and is the paired functions ticket #1799
(now MERGED to functions main). Because #1799 re-binds the bound
selection on replay, the client only needs to trigger the existing
run-replay by run id — the broker returns RLS-fresh data
for that selection. Re-emitting the selection from the app would
duplicate the bound-selection source of truth, so the app does NOT.
This app ticket is the page-reload side: kick the
existing #356 query reconnect on mount. No new outbound
message, no contract change, no functions change in this repo.
What changed
A persisted render item is keyed render-${runId}. The
new pure runIdFromRenderItemId (inverse of
renderItemId) recovers the run to replay — and because
it is the SAME id, the redelivered live item overrides the
placeholder in place (mergeLiveRender, unchanged).
resume(runId)
AgentRailConnection.resume adopts the run id, shows the
loading state (status: "launching", streaming), and sends
the EXISTING buildDockChatQuery reconnect — the same
path a mid-run socket close already uses. #1799 re-binds the selection
with fresh RLS, so run id alone returns fresh data. Idempotent per
run.
On mount, if there is no live render and the thread holds a persisted
componentResult reference, the dock calls
rail.resume for that run — once. A same-session render
(rail already has it) needs no replay and is skipped.
In-flight → the dock's existing loading/thinking status. The
redelivered frame flows back through reduceFrame →
rail.render → the unchanged denied /
empty / unknown-id text fallback branches
(#1794 AC5). No new component state, no new framework.
ponytail: notes are gone
Both #1794 deferral notes (the persist-effect block and the
toThreadItem componentResult case) now
describe the real re-run. The empty placeholder is explicitly only
the pre-redelivery state, never a stored stale row.
No row data reaches sessionStorage (HR-23) — the
#1794 persist path is untouched. Replay re-authorizes server-side,
so the rows are always RLS-fresh on reload.
Where it lives
src/lib/agents/dock-render-bridge.ts— NEW purerunIdFromRenderItemId(inverse ofrenderItemId); the run to replay is read off therender-${runId}id, which also keeps the redelivered live item's id matching the placeholder.src/lib/agents/atlas-rail-store.ts— NEWAgentRailConnection.resume(runId): adopts the run, shows loading, sends the existingbuildDockChatQueryreconnect (a comment cites #1799 for why run id is sufficient); idempotent per run (resumedRunId).src/lib/agents/use-dock-chat-rail.ts— exposesrail.resume.src/components/agents/agent-dock-mount.tsx— rehydrate effect: re-run (replay) the persisted reference's run once on mount when no live render is present. Bothponytail:deferral notes replaced with the real re-run.*.test.*— bridge round-trip; rail-storeresumeasserts the rehydrate TRIGGERS the replay query AND that a redeliveredrender.dataframe REPOPULATESstate.renderwith RLS-fresh rows (+ idempotent); mounted-dock rehydrate e2e (persist → reload →resumefires; denied frame shows the VISIBLE denied state).
Verify
pnpm lint # clean (1 pre-existing avatar.tsx warning, unrelated) pnpm test # 903 passed pnpm build # green git diff --check # clean # NOTE: do NOT run pnpm codegen — no contract change; import from src/gen.
- AC1 (re-run triggers + repopulates) the rail-store test drives the full path:
resume(runId)shows loading and sends thetype:"query"/agentKey:"atlas_dock_chat"/runIdreconnect, THEN a redeliveredrender.dataframe REPOPULATESstate.renderwith RLS-fresh rows (render.rows[0].matterRef === "M-001") — proving the trigger produces a fresh component, not a stale placeholder.agent-dock-mount.render.test.tsxmounts the real dock: persist → cold reload →rail.resume("run_1")fires. - AC2 (states) in-flight → loading (status); a redelivered
state="denied"frame overrides the placeholder and renders the VISIBLE denied note ("This result is no longer available."), not the silent empty placeholder; unknown id → text fallback (the #1794 path, unchanged). - AC3 (reference-only kept) the persist path is untouched; no row data reaches
sessionStorage; theponytail:notes are replaced with the real re-run. - No wasted re-run a same-session live render skips replay (
rail.resumenot called); the replay is idempotent per run. - No regression all tests green; the bridge round-trip + rail-store replay coverage is new.
Design (accepted): the re-run replays the run by id via
the existing dock_chat query reconnect rather than re-emitting
the selection. #1799 (merged to functions main) re-binds the persisted
render selection with fresh RLS at replay time, so run id alone returns
RLS-scoped data on rehydrate; re-emitting the selection would duplicate
the bound-selection source of truth.
Non-goals (per the ticket): no functions changes (server re-bind is #1799), no row-data persistence, no new framework — the existing rail + loading/denied/empty/fallback states are reused.
Files changed
7 file(s), +299 / -11 lines
| Path | Role | + | - |
|---|---|---|---|
| src/components/agents/agent-dock-mount.render.test.tsx | Component | +96 | -0 |
| src/components/agents/agent-dock-mount.tsx | Component | +29 | -8 |
| src/lib/agents/__tests__/atlas-rail-store.test.ts | Library | +101 | -1 |
| src/lib/agents/atlas-rail-store.ts | Library | +33 | -0 |
| src/lib/agents/dock-render-bridge.test.ts | Library | +21 | -1 |
| src/lib/agents/dock-render-bridge.ts | Library | +15 | -1 |
| src/lib/agents/use-dock-chat-rail.ts | Library | +4 | -0 |