acpmux + agent pane: paint a paused or offline peer's chat from the Mac's copy (_acpmux/peer_snapshot)

Closed
TE
10d ago
+2737-16

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

  • sessionId is a peer session id or HOST/name. With host, it is the peer's own id, so a session whose lease ended can still be read.
  • events are the newest limit records (default 400, at most 1000) within maxBytes (the same page budget _acpmux/attach uses), oldest first.
  • It answers whether or not the peer is reachable, and the reply is always marked stale.
  • generation is the session's createdAt. When the peer's current row names another generation, the copy is discarded and nothing is returned.
  • hasMore is 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 in spec/acpmux-protocol.json and 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 by seq, 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-shot setTimeout raced 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 --lib passes (987; the one failure is a base red, listed below), and cargo fmt --check and cargo clippy --all-targets -D warnings are clean.
  • webviews/test/agent-pane-peer-copy.test.ts: 6/6 pass. tsc reports no errors and vp lint reports no new warnings.

read_tests.rs covers:

  • a down peer's chat read stale across a restart, without the row's retry countdown;
  • 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 afterSeq with 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-capsules and agent-pane-hairline-focus;
  • the agent-pane Playwright files (17 fail and 52 pass on both base and branch);
  • vp lint warnings.

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 Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.