acpmux peer snapshots (6): the _acpmux/peer_snapshot read hook
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".
Part of the client-snapshot piece of manaflow-ai/cmuxterm-hq#4381 ("Persistence, pause and resume, and history hydration"): the Mac's acpmux keeps a durable, bounded copy of each peer session's events, so the app paints a paused or offline peer chat at once, marked possibly stale, and catches up from the host with afterSeq. The host's journal stays the authority; the copy is not a write lease, an execution checkpoint or a backup. The shared fenced-writer database from #4381 is not part of this work.
This replaces #25323 (store) and #25378 (hook and pane), split so each piece lands on its own under about 800 changed lines, tests included. The series:
- the on-disk form of a copy (#25448, landed)
- the store's thread: bounds, writes, removals (#25445)
2t. the store's tests (#25505) - what starts or grows a copy: generations, joins, links (#25446)
- purges, removed peers, the queue's bound (#25447)
- the hub's taps (wiring) (#25449)
- the read hook
_acpmux/peer_snapshot, which the VM start/resume work calls too (#25473) - the pane's offline banner and the reply's reading (#25474)
- the pane paints the copy, then catches up (#25475)
8t. the pane's tests (#25506)
8t2. tests of 8's attach fixes (#25620)
This PR (6): the _acpmux/peer_snapshot read hook
This is the one hook a client reads a peer session's kept copy through, whether or not the peer answers. Clients are the app's agent pane (7 and 8) and the VM start and resume path.
_acpmux/peer_snapshot {sessionId, host?, limit?, maxBytes?} returns:
{peer, sessionId, qualifiedSessionId, events, lastSeq, hostLastSeq, generation, capturedAt, hasMore, stale: true, summary}
- Session.
sessionIdis an id orHOST/name. Withhost, it is the peer's own id, which covers a lease that has ended. - Records. It returns the copy's newest
limitrecords (default 400, at most 1000) withinmaxBytes, oldest first. - Whole history.
hasMoreis false only when the copy starts at seq 1 and every record was returned. - Staleness. The reply is always
stale: true.summaryis the row now if the peer still lists the session, else the row the copy kept, with noretrycountdown. - Generation. A
generationother than the copy's (the row'screatedAtnow) reads nothing and drops nothing. The listing decides which copy goes. - Limits. A
limitthat is not a whole number from 1 is refused: 0, -1, 1.5,trueand "2" are all refused (H4, from the last review of #25378). - Where it is answered. The store's thread answers from memory or the copy's file (
Op::Read). A read reserves queue room like a capture; with the queue full, the reply says nothing is kept. The caller waits at most 5 s. The method is answered locally and never forwarded (SESSION_SCOPED_EXCLUDED). - Docs. It is documented in the acpmux README. The protocol spec and the generated
methods.tslist it.
Findings from the earlier reviews this piece answers
These are H1 to H4 of the ledger (Codex gpt-6-astra xhigh, rounds 1 to 6 of #25378):
- a partial read that claimed full history;
- an obsolete read that deleted a newer copy;
limit: 0answered with one event;- a negative, fractional or boolean
limittaken as omitted; - 2's queued ops (
Queued): a read the thread never took (it ended) answers its reader at once that there is nothing. - this PR's sixth review: a read sends every loss not sent yet before it, as
flush_nowdoes, so a read that starts the thread again never reads what a lost purge removes (H7). - this PR's eighth review (nonblocking): the README and
docs/acpmux-schema.jsondescribe the store as it is, one file a session underpeer-snapshots/(H8).
Tests
On the fleet:
read_tests.rs: a down peer's copy is read whole, then within a limit; bad limits are refused; the copy is read again after this daemon restarts with the peer still down; a loss not sent yet goes before a read of its peer.- The
peer_snapshottests pass (48). cargo fmt --checkandcargo clippy -p acpmux --all-targets -D warningsare clean.bun run acpmux-protocol:checkis current.- Release compile (cmux-next, Xcode 26.6) on the fleet through the controller at head 7b544f5520ec3a866d8ee668ec88feb054a1bf1a: green, step
e0218986d73056c4ab66a5d7. - Rebased onto 5 as landed (a8a6f7f237f): the full
cargo test -p acpmuxsuite passes except the base reds below; clippy and fmt are clean. - Review: Codex (gpt-6-astra, xhigh) SHIP at 7b544f5520ec3a866d8ee668ec88feb054a1bf1a, a confirming review of the rebase and the one-line test change it took (
RemoteRowshas noclear, so the test empties the rows withretain).
No capture: this PR changes no UI. Its only webview file is the generated protocol method list (methods.ts), which gains _acpmux/peer_snapshot; the pane first reads it in 7 and 8, whose PRs carry the captures.
Timers, threads and loops
None new. The read waits on the store's thread (2) for at most ANSWER_WITHIN (5 s).
Base reds, not from this PR (they fail the same way on feat-cmux-next): agent_tools::tests::own_servers_run_under_the_daemon_prefix, daemon_chat_open, ephemeral_run::a_probe_from_an_agents_shell_is_not_its_subagent, hermetic_daemon_guard (2), and the flaky session_pool::pooled_sessions_end_with_the_daemon_and_after_a_crash, subagent_retry, prompt_supersede::a_prompt_queued_before_a_restart_is_still_replaced (fails 1 run in 2 of the full suite on feat-cmux-next) and cli::run::launch::tests::a_launched_session_never_gets_ask_user_question (its tests share one pid-named brief file, so a read can see another test's truncate; it passes alone and in later full runs). The gallery health and guard status reds are on feat-cmux-next too.
Agent session: agent session
š¤ Generated with Claude Code