acpmux + agent pane: paint a paused or offline peer's chat from the Mac's copy (_acpmux/peer_snapshot)
Lawrence's list item, word for word: "start new VMs from acp chat - decouple persistence of acp trajectories from the VM itself, in case the VM is resuming".
This is the client-snapshot piece of manaflow-ai/cmuxterm-hq#4381 (docs/remote-acpmux.md, "Persistence, pause and resume, and history hydration"), stacked on #25323 (the daemon's durable, bounded copy of each peer session's events). It adds the hook that reads that copy and the agent pane that paints it.
The hook
_acpmux/peer_snapshot {sessionId, host?, limit?, maxBytes?} returns {peer, sessionId, qualifiedSessionId, events, lastSeq, hostLastSeq, generation, capturedAt, hasMore, stale: true, summary}.
sessionIdis a peer session id orHOST/name. Withhost, it is the peer's own id, so a session whose lease ended can still be read.eventsare the newestlimitrecords (default 400, at most 1000) withinmaxBytes(the same page budget_acpmux/attachuses), oldest first.- It answers whether or not the peer is reachable, and the reply is always marked
stale. generationis the session'screatedAt. When the peer's current row names another generation, the copy is discarded and nothing is returned.hasMoreis false only when the whole chat from its first record is in the reply. A segment that cannot be read never reads as the whole chat.- It runs on a blocking thread under the store's disk lock. It is refused to web-origin connections and never forwarded to a peer (
SESSION_SCOPED_EXCLUDED). It is registered inspec/acpmux-protocol.jsonand the generated methods.
The VM start and resume work (#4381) is meant to call this same hook, to show a resuming VM's chat before the VM answers.
The app
In webviews/src/agent-session/acpmux/direct.ts and peerCopy.ts:
- Opening a peer chat with no kept tail: the pane asks the daemon for its copy, raced against 400 ms. It paints the copy at once under a "possibly stale" banner (
PeerCopyBanner.tsx), then attaches. - The attach succeeds: the pane catches up with
_acpmux/events {afterSeq: copy.lastSeq}, drops duplicates byseq, and removes the banner. - The attach fails with peer_unreachable: the copy stays, the banner says the machine is offline, and the cursor is kept. When the session row says the peer is back, the pane attaches and catches up from that same cursor.
- A recreated session (another
createdAt, from the copy or from a kept tail of any cache) replaces what is drawn instead of joining it. Its sequence state resets at once, so its live records are kept. - A gap read that fails for another reason drops the rows the page does not continue, so no hole stays on screen. Older history pages back in.
- Local chats do not change.
AgentPaneTailCache (Swift) is unchanged. The app now prefers the daemon's copy, read through the hook, which is the one durable source. Teaching the Swift cache to persist too would make a second copy that can disagree with the first. The webview tail cache (tailCache.ts) only carries the generation forward.
New timers and loops
- The 400 ms race in
drawPeerCopy: a one-shotsetTimeoutraced against the hook's reply.- Stop condition: it fires once, at 400 ms. It holds nothing but a resolve that is a no-op once the race has settled.
- Cleanup: none is needed. A reply that arrives after a session switch is ignored (the selection generation is checked).
- No other timers, observers or loops.
Captures
Before and after of an offline peer's chat, from the gallery entry agent-pane.peer-copy (variants before, after, and after-no-time), at https://cmux-lawrences-mac-mini.tail137216.ts.net:18796/compare/p0-machines-peer-snapshot-hook/?entry=agent-pane.peer-copy (tailnet only).
- Before: the pane has nothing of the chat to draw and says the machine is unreachable.
- After: the Mac's copy is painted at once under the offline banner, with its last-updated time.
Size
About 900 changed lines, 440 of them tests and generated registry entries. The hook and its first reader land together, so the hook does not ship unused.
Review
Codex gpt-6-astra at xhigh, through SR2. Round 1 returned FIX with four findings, all fixed: cached tails losing their generation, the catch-up cursor lost on a failed gap read, a recreated session dropping live records, and a partial disk read claiming the whole chat. Round 2 is running at this head.
Tests
On the fleet (Linux, 32 vCPU):
cargo test -p acpmux --libpasses (987; the one failure is a base red, listed below), andcargo fmt --checkandcargo clippy --all-targets -D warningsare clean.webviews/test/agent-pane-peer-copy.test.ts: 6/6 pass.tscreports no errors andvp lintreports no new warnings.
read_tests.rs covers:
- a down peer's chat read stale across a restart, without the row's
retrycountdown; - a row with another generation reading nothing and dropping the copy;
- a segment that cannot be read never reading as the whole chat.
agent-pane-peer-copy.test.ts covers:
- paint, then catch up by
afterSeqwith dedupe; - an offline peer keeping the copy and its cursor;
- a recreated session replacing a page-kept tail while keeping its live records;
- a machine that goes down again keeping the cursor;
- a chat on this Mac never asking for a peer copy.
Warnings, not from this PR (they fail the same way on base):
agent_tools::tests::own_servers_run_under_the_daemon_prefix(an MCP server list on base);tests/daemon_chat_open.rs(open-bytes budget);- two gallery-coverage tests;
agent-pane-no-capsulesandagent-pane-hairline-focus;- the agent-pane Playwright files (17 fail and 52 pass on both base and branch);
vp lintwarnings.
Risk: low. Only peer chats are affected. When the hook fails or times out, the pane opens as before.
Agent session: agent session
š¤ Generated with Claude Code
![View with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v3-3x.png)
![Autofix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
Need help on this PR? Tag @codesmith-bot with what you need.