Skip to content

Chat message transcript has no reconnect/resync self-heal — stale rail heals only on reload (the #1617 fourth root) #1646

Description

@jeonghun-jj-lee

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)

  1. 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).
  2. 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(() => {}).
  3. app/.../session.tsx:762 — thread streamConnected: () => serverSDK().event.status() === "connected" and forceResync: () => serverSDK().event.resyncCount() into createTimelineModel (serverSDK already bound at :543).
  4. 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

  • The transcript force-syncs on the disconnect→connect rising edge, and on any resyncCount advance (the fan-in wedge case where status stays "connected").
  • It does not fire on the initial undefined→true connect, on true→true, on an unchanged token, or when sessionID() is undefined.
  • No feedback loop: mutating the message store does not re-fire the effect.
  • A force-sync mid-streaming-turn preserves optimistic/in-flight parts (via existing reconcileFetched).
  • Orphan-part: a genuine orphan outside a load triggers at most one force-sync per burst; a known-removed message's orphan part still short-circuits.
  • pnpm -r build green (proves the shim import resolves), pnpm --filter amicode test green, app-bundle drift-gate PASS after manifest refresh.

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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:uibugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions