Symptom
A user sends a message to an Amicode chat; the live message transcript (the rail) does not update and stays stale until the window is reloaded. Reported as "a chat losing connection to the server." The #1617 family (#1636/#1637/#1638) fixed neighboring paths but this recurred.
Root cause (verified — subagent deliberation + direct code reads)
The chat message transcript has no reconnect/resync self-heal. On a silent SSE reconnect or fan-in wedge:
reconcileFromStatus (server-sync.tsx:309) refreshes only data.session_status, never data.message.
- The reconnect listener (
server-sync.tsx:580-622) refreshes the session list, status, and the global/directory bootstrap — none reload the viewed session's messages.
timeline/model.ts's only data trigger is a createResource keyed on session-ID change (lines 21-64). Staying on the same session across a reconnect never re-fetches messages, so the transcript freezes until reload (which re-runs that resource).
The entity rail already self-heals — entity-rail.tsx:214-237 has two createEffects: one on the disconnect→connect edge (shouldRefetchOnReconnect), one on the resync-token advance for the fan-in wedge (shouldRefetchOnResync). The heal signals (serverSDK().event.status(), serverSDK().event.resyncCount()) are already computed at message-timeline.tsx:2661,2666 — but only feed the entity rail. The transcript sitting right beside them ignores them.
session.sync(id, {force:true}) is the correct re-fetch: server-session.ts:1164 bypasses the cache guard, :1178 re-runs loadMessages, and the :1182 runInflight force-path does not coalesce onto a non-forced in-flight request (the #1646 fix, b3982642).
Fix (five files)
ui/.../amicode-entity-view.tsx:5 — re-export shouldRefetchOnReconnect / shouldRefetchOnResync from ../amicode/problem (they exist at problem.ts:414,428 but no shim re-exports them; @opencode-ai/ui/amicode-problem does not resolve — @opencode-ai/ui/* maps to src/components/*.tsx).
app/.../timeline/model.ts — accept optional streamConnected?: () => boolean and forceResync?: () => number; add two createEffects mirroring the entity-rail pattern (plain-closure prev, track only the signal — no feedback loop); on the reconnect rising edge and on any resync advance, if sessionID() is defined, void sync().session.sync(id, { force: true }).catch(() => {}).
app/.../session.tsx:762 — thread streamConnected: () => serverSDK().event.status() === "connected" and forceResync: () => serverSDK().event.resyncCount() into createTimelineModel (serverSDK already bound at :543).
app/.../server-session.ts:1547-1559 — harden the orphan-part drop: a genuine orphan (missing && !load, not a known-removed/cleared message) records resync intent and requests a debounced sync(sessionID, {force:true}) (dedup on orphanParts novelty + in-flight guard so a part burst yields at most one sync); the removed/cleared short-circuit is preserved (no resurrection of deleted messages).
Acceptance criteria
Non-goals (documented so they aren't chased)
home.tsx builds no timeline model — the Landing is an empty composer; there is no home transcript to freeze.
- Fleet/remote: the signals are global-stream signals. This achieves the same reconnect parity the entity rail already has; a remote peer's silent wedge that never reaches the local fan-in still won't heal — the same known limitation.
Lineage: the fourth root of the #1617 staleness family (after #1636/#1637/#1638).
Symptom
A user sends a message to an Amicode chat; the live message transcript (the rail) does not update and stays stale until the window is reloaded. Reported as "a chat losing connection to the server." The #1617 family (#1636/#1637/#1638) fixed neighboring paths but this recurred.
Root cause (verified — subagent deliberation + direct code reads)
The chat message transcript has no reconnect/resync self-heal. On a silent SSE reconnect or fan-in wedge:
reconcileFromStatus(server-sync.tsx:309) refreshes onlydata.session_status, neverdata.message.server-sync.tsx:580-622) refreshes the session list, status, and the global/directory bootstrap — none reload the viewed session's messages.timeline/model.ts's only data trigger is acreateResourcekeyed on session-ID change (lines 21-64). Staying on the same session across a reconnect never re-fetches messages, so the transcript freezes until reload (which re-runs that resource).The entity rail already self-heals —
entity-rail.tsx:214-237has twocreateEffects: one on the disconnect→connect edge (shouldRefetchOnReconnect), one on the resync-token advance for the fan-in wedge (shouldRefetchOnResync). The heal signals (serverSDK().event.status(),serverSDK().event.resyncCount()) are already computed atmessage-timeline.tsx:2661,2666— but only feed the entity rail. The transcript sitting right beside them ignores them.session.sync(id, {force:true})is the correct re-fetch:server-session.ts:1164bypasses the cache guard,:1178re-runsloadMessages, and the:1182runInflightforce-path does not coalesce onto a non-forced in-flight request (the #1646 fix,b3982642).Fix (five files)
ui/.../amicode-entity-view.tsx:5— re-exportshouldRefetchOnReconnect/shouldRefetchOnResyncfrom../amicode/problem(they exist atproblem.ts:414,428but no shim re-exports them;@opencode-ai/ui/amicode-problemdoes not resolve —@opencode-ai/ui/*maps tosrc/components/*.tsx).app/.../timeline/model.ts— accept optionalstreamConnected?: () => booleanandforceResync?: () => number; add twocreateEffects mirroring the entity-rail pattern (plain-closureprev, track only the signal — no feedback loop); on the reconnect rising edge and on any resync advance, ifsessionID()is defined,void sync().session.sync(id, { force: true }).catch(() => {}).app/.../session.tsx:762— threadstreamConnected: () => serverSDK().event.status() === "connected"andforceResync: () => serverSDK().event.resyncCount()intocreateTimelineModel(serverSDKalready bound at:543).app/.../server-session.ts:1547-1559— harden the orphan-part drop: a genuine orphan (missing && !load, not a known-removed/cleared message) records resync intent and requests a debouncedsync(sessionID, {force:true})(dedup onorphanPartsnovelty + in-flight guard so a part burst yields at most one sync); the removed/cleared short-circuit is preserved (no resurrection of deleted messages).Acceptance criteria
resyncCountadvance (the fan-in wedge case where status stays "connected").undefined→trueconnect, ontrue→true, on an unchanged token, or whensessionID()is undefined.reconcileFetched).pnpm -r buildgreen (proves the shim import resolves),pnpm --filter amicode testgreen, app-bundle drift-gate PASS after manifest refresh.Non-goals (documented so they aren't chased)
home.tsxbuilds no timeline model — the Landing is an empty composer; there is no home transcript to freeze.Lineage: the fourth root of the #1617 staleness family (after #1636/#1637/#1638).