Repository navigation
Remote chats: show the sync, not an outage, while a room catches up - #1038
Conversation
ca40ee6 to
b74296b
Compare
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>
b74296b to
dbede68
Compare
There was a problem hiding this comment.
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.
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.
ChatPersistencecertifies the cursor on its first verified write, so the legacy cursor repair runs once per snapshot instead of once per rejoin.AppState::chat_link_atreturnsChatLink { sync, host_unreachable }. An unsynced remote chat isSyncing, captioned after 4 s, andStalledonly once its room dial has stayed down 35 s (the engine's 20 s connect plus 15 s hello).ring_spinnerwith "Opening…", "Syncing…", "Reconnecting…" or "Offline".Captured from the production shell by the new
remote-sync-fixtureexample, 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.Test. Every rule has a test that fails with the rule reverted. Without the engine change,
rejoin_after_idle_retirement_resumes_from_its_cursorfails (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_panefails (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), whichan_unverified_cursor_opens_at_zeropins: without that line it fails (left: 500,right: 0), and every other test passes. On a localwrangler devedge with two headless engines, a rejoin after idle retirement logschat2: one-time legacy cursor repair from=1 to=0without 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 undercargo 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).Need help on this PR? Tag
@codesmith-botwith what you need.