Skip to content

Remote chats: show the sync, not an outage, while a room catches up - #1038

Merged
zeronsh[bot] merged 7 commits into
zeronsh:mainfrom
alexandre-vl:fix/remote-chat-sync-truth
Oct 27, 2026
Merged

zeronsh[bot] merged 7 commits into
zeronsh:mainfrom
alexandre-vl:fix/remote-chat-sync-truth

Conversation

@alexandre-vl

@alexandre-vl alexandre-vl commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Opening a chat hosted on another device over a slow edge showed a blank transcript, then "Messages will send once the connection recovers." with nothing sent, and every revisit repeated it with no loading cue. Side chats inherited both, and a fork on a slow host showed nothing until it answered, then put its error in the parent's composer. Each rejoin of a chat first opened cold also rewound its cursor and downloaded its rows again.

Fix.

  • Engine: ChatPersistence certifies the cursor on its first verified write, so the legacy cursor repair runs once per snapshot instead of once per rejoin.
  • Link state: AppState::chat_link_at returns ChatLink { sync, host_unreachable }. An unsynced remote chat is Syncing, captioned after 4 s, and Stalled only once its room dial has stayed down 35 s (the engine's 20 s connect plus 15 s hello).
  • Composer: the "will send" clause needs something to send (a draft, a queued message or a pending send); otherwise it says "Syncing…", "Reconnecting…" or "Offline". The sidebar pill reads global connectivity only.
  • Transcript: a blank remote transcript that has not caught up shows the house ring_spinner with "Opening…", "Syncing…", "Reconnecting…" or "Offline".
  • Side chats get both. A fork opens its tab at once with "Forking on {host}…", and a failure stays in that tab with Retry.

Captured from the production shell by the new remote-sync-fixture example, run unchanged on main (c04e3749) and on this branch. It attaches the shell to a real engine as at launch and scripts the edge and the host "omarchy" over it. Each capture is 5 s after opening, past the composer's 4 s hold.

Before After
Remote chat opened cold, room still dialing
Revisit while its room rejoins
This device lost the edge, nothing typed
The same, with a draft
Side chat opened cold
Fork, 2.5 s in, host still working
Fork fails

Test. Every rule has a test that fails with the rule reverted. Without the engine change, rejoin_after_idle_retirement_resumes_from_its_cursor fails (left: Some(2), right: Some(5): the rejoin rewound to the checkpoint). With the loader laid out as before, transcript_loader_renders_centered_in_its_pane fails (origin: Point { x: 371px, y: 600px } in an 800×600 pane). The engine change relies on an unverified row opening at cursor 0 (chat_persistence.rs:49), which an_unverified_cursor_opens_at_zero pins: without that line it fails (left: 500, right: 0), and every other test passes. On a local wrangler dev edge with two headless engines, a rejoin after idle retirement logs chat2: one-time legacy cursor repair from=1 to=0 without the engine change and no repair with it. cargo test -p zeron-engine --lib: 635 passed, 5 ignored. cargo test -p zeron-ui --lib: 1897 passed, 0 failed; two existing attachment tests race each other under cargo test, so they ran on their own (CI's nextest runs each test in its own process).

Limit. For a chat opened from an unverified row, a rejoin before its first write lands (about a second after its first rows), or while writes fail, still downloads from the checkpoint again. It costs a download, never data; the regression doc has the cases.

Not verified: the installed app against a real slow host, and iOS, which keeps its 4 s grace. Everything else, including every mutation and its failing line, is in docs/regressions/remote-chat-opening-outage.md.

Reproduce a capture: cargo run -p zeron-ui --features appshots-fixture --example remote-sync-fixture -- <dir> cold-open (scenes: cold-open, revisit, outage, outage-draft, side-chat, fork).


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@alexandre-vl
alexandre-vl marked this pull request as ready for review October 25, 2026 12:41
@zeronsh
zeronsh Bot force-pushed the fix/remote-chat-sync-truth branch from ca40ee6 to b74296b Compare October 27, 2026 01:52
alexandre-vl and others added 7 commits October 27, 2026 01:53
Opening a chat hosted on another device over a slow edge showed a blank
transcript, then "Reconnecting — messages will send once the connection
recovers." with nothing sent, and every revisit repeated it. Each rejoin
of a chat first opened cold also rewound its cursor and downloaded its
rows again.

A remote chat that has not caught up now shows a loader in its
transcript and "Syncing…" in its composer. It reads as stalled only once
its room dial has stayed down past the engine's own connect and hello
budget, and the composer mentions sending only when there is something
to send. The engine certifies the cursor on its first verified write, so
the legacy repair runs once per snapshot. Side chats get the same, and a
fork opens its tab at once with its progress, error and Retry.
`remote-sync-fixture` attaches the production shell to a real engine, as
at launch, and plays the edge and the host over it: a chat opened cold,
revisited while its room rejoins, an outage with and without a draft, a
side chat opened cold, and a fork on a slow host. The same fixture ran
on main and on this branch; the captures are in
docs/screenshots/remote-chat-sync.
Once a write lands, the legacy cursor repair no longer runs for the
doc, so a stored cursor above the rows it holds would never be fetched
again. ChatPersistence::new opens a row whose cursor is not verified at
0, and no test covered it. With the zeroing removed, the new test fails
(left: 500, right: 0), and every other test still passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oin still rewinds

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e and fork-failure states

- The stall clock no longer restarts when the engine moves a room back to
  waiting for a sync slot (contention handoff), so a room that never joins
  reads "Reconnecting…" after 35 s instead of "Syncing…" forever, and a
  pending send can still surface "Not delivered". Opening the chat still
  restarts the hold and the stall; only delivery ends the clock.
- Transcript loader: hard offline shows the amber dot the composer and
  sidebar use, not a spinner.
- Pending fork failure: a plain empty state ("Couldn't fork on {host}" with
  the engine's reason as detail) and a ghost Retry, instead of the raw error
  in danger red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: zeronsh[bot] <346184242+zeronsh[bot]@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: zeronsh[bot] <346184242+zeronsh[bot]@users.noreply.github.com>
@zeronsh
zeronsh Bot force-pushed the fix/remote-chat-sync-truth branch from b74296b to dbede68 Compare October 27, 2026 01:53

@zeronsh zeronsh Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security: no edge/, CI, build.rs or dependency changes. The 14 PNGs are clean (only IHDR/IDAT/IEND, nothing after IEND). The fixture example is localhost-only. The engine change only makes the cursor-verified flag follow a successful verified snapshot write, with no auth, room or tenant impact.

Rebased onto main (one Cargo.toml example-list conflict). Maintainer fixes on top:

  • A requeue to Waiting under slot contention no longer restarts the stall clock, so a sync that never delivers stalls after 35 s instead of spinning "Syncing…" forever. Opening the chat still restarts it.
  • The transcript loader uses the amber dot when hard offline, matching the composer and sidebar.
  • The fork failure is now a plain empty state with a ghost Retry instead of the raw error in red.

Local, on the rebased head: zeron-ui lib 1973/1973. zeron-engine lib 634 passed; the one failure (harness_updates ownership) is environmental: this machine's npm-installed codex is at /usr/local/bin/codex. Clippy is clean on PR-added lines apart from one field_reassign_with_default in a test helper, the same pattern as its neighbours. The remote-sync-fixture example builds and is clippy-clean.

@zeronsh
zeronsh Bot merged commit 00cc2e0 into zeronsh:main Oct 27, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants