Repository navigation
cmux-next: a chat's first frame waits on one daemon round trip - #27755
Conversation
Opening a chat asked acpmux, one after another: initialize, _acpmux/status, the chat list, the attach, then the turn opener, permissions and handoff. Now everything after the handshake goes out together (status, the list, the attach and its permission and handoff reads), and acpmux names the turn's opener in the attach reply (`turnOpener`), so the first frame waits on the handshake plus one round trip. An older acpmux leaves `turnOpener` out and the pane reads it as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Owned by an agent session · session |
…attaches again Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Found 21 test failures on Blacksmith runners: Failures
...and 11 more test failures. View all on Blacksmith: run 41046708113, run 41046707932
|
…he chat over Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
Probably yours: CI failed on
Failures:
Matched log linesNot re-run automatically: Written by red: web / Web status; base red: the same failure on the base 58d6a62; does not block Gallery before/after for
States
Pick other states with a |
…es again Records the attach asked with the chat list streamed before the row changed may be of the session before (recreated meanwhile), so the open attaches again. A dropped early attach's held records are discarded by the next attach, whose page holds them, instead of applied: some may be of another session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tach goes out before The attach now goes out with the chat list, so a held attach is asked before the host tail is read and drawn; the tests let that read land before they look. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An ordinary record is followed by its row change, so marking an early attach suspect on any row change after a record retried normal streaming opens. The cases it was for are peer chats, whose dropped early attach's records the next attach now discards; a row that names another session is still caught by the watch (strayedFrom) and the reply checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d chat The pane now asks its chat's attach with the chat list, before it knows the chat runs on another machine, and drops that attach for a peer chat. The stand-in daemon answered its once-only hooks (records during the attach, a recreation, refusals) on that first attach; it now answers an attach asked before the list as the machine is, and scripts the one the open asks of the chat as listed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… by the list's answer The pane sends the list and the early attach in one go, so the list is asked before that attach; what marks it is that the list was not answered yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…unt the listed attaches Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
acpmux runs each request on its own task, so a detach sent while the attach was still out could run first and leave its subscription behind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t-open-one-trip # Conflicts: # cmux-tui/crates/acpmux/src/server/requests.rs
|
Review (codex gpt-6-astra high, fast): SHIP at 0c59be7. Full review at 8f5a248 found one race (detach of a dropped early attach could run before it); fixed in d4c8ff5 and re-reviewed SHIP. The base merge (one module-list conflict in requests.rs) got its own delta review: SHIP. Tests at this head: chatOpenTrips 3/3, peer-copy 55/55, new-tab cached chat 13/13; Rust transcript_events, turn_opener, protocol_ir_is_current, clippy and fmt pass. The tsc error in test/latency/probe.ts is the same at base. |
|
Shipped in:
|
…n the first frame - A status read after a snapshot said `restarting` keeps it: a failed restart goes restarting -> reconnecting, both `reconnecting` on the wire, so no note says so and readPeers re-applies the snapshot. - connect awaits _acpmux/peers before the first frame, a second round trip after the one #27755 keeps it to (a held snapshot holds connect). - chatUnavailable calls a restarting peer offline instead of connecting. The initial snapshot is settled in the fixture, and the in-flight status test now races a real in-flight read. Red: bun test src/agent-session/acpmux/peerStateEvents.test.ts: 11 pass, 2 fail; bun test test/chat-unavailable.test.ts: 1 fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972
…pshot leaves the first frame - A _acpmux/status read now drops every peer event entry no note has updated since the read started (PeerStateEvents.statusRead), so a failed restart that the daemon reports only as a state (no note) reads reconnecting instead of restarting forever. - connect starts the _acpmux/peers snapshot once the list's watch answered and draws it when it lands, instead of awaiting it before the first frame (#27755's one round trip). - chatUnavailable counts a restarting peer as still connecting. Green: bun test peerStateEvents, peerStateReducer, environments and test/chat-unavailable.test.ts: 28 pass, 0 fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972
…e red from #27755) (#28320) The waves loop in openCountingWaves slept 10 ms whenever nothing was out to the daemon, which check:fixed-waits counts once per call site (4 against a baseline of 0) and which stops check:pieces before fmt, lint and tsc. It now waits for the page's next request or its first frame with rows, so no idle time is ever counted as a wave and no wall-clock delay is involved. Agent-Session: https://cmux.com/deeplink/agent?id=aid-23fb5d70a9bdd4019bab Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…up (#28214) * ci(cmux-tui): skip docs-only artifact publishes Agent-Session: https://cmux.com/deeplink/agent?id=aid-55ad21df065d5bc316b2 * ios: New Message to one agent sends its images (#27877) * test(ios): New Message to one agent forwards its images Red first: routing a New Message / Forward to one agent sends only the text, so images are dropped and an image-only message sends nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-e06379967aeecbaaded3 * fix(ios): New Message to one agent sends its images New Message and Forward addressed to one agent sent only the text as the agent's prompt: attached images were dropped, and an image-only message sent nothing after the sheet closed. The images now go as the prompt's image attachments, bytes and order kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-e06379967aeecbaaded3 --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * carry of #26682: fix(setup): report actionable harness authentication checks (#28187) * test(setup): cover actionable harness auth reports Agent-Session: https://cmux.com/deeplink/agent?id=aid-252635b51d8eab1624f1 * fix(setup): expose actionable harness auth recovery Agent-Session: https://cmux.com/deeplink/agent?id=aid-252635b51d8eab1624f1 * docs(setup): clarify actionable verification failures Agent-Session: https://cmux.com/deeplink/agent?id=aid-252635b51d8eab1624f1 * fix(setup): preserve disabled profile check uncertainty Agent-Session: https://cmux.com/deeplink/agent?id=aid-252635b51d8eab1624f1 * test(setup): select custom profile among discovered harnesses Agent-Session: https://cmux.com/deeplink/agent?id=aid-252635b51d8eab1624f1 * fix(setup): an expired credential reports signed_out with the login recovery Base added acpmux::harness_health::Auth::Expired; setup already routes it to the login action, so its auth_status is signed_out (the match was non-exhaustive and cmux-tui did not compile). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-f8805b2d0f3b1303441e --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * acpmux: leave superseded tool output out of a chat's open page (#27614) * acpmux: leave superseded tool output out of open pages and sealed segments codex-acp re-sends a running command's whole output as the content of every tool_call_update, so a Codex chat's log grows with the square of its output and its open page holds a few snapshots of one command. A page a client bounds by bytes (maxBytes) now leaves out updates a newer update of the same call in the scan makes moot, and drops moot content from the rest. Each sealed segment of a live chat's log loses the content a later update of the same call in it supersedes, on a thread of its own; seq, deltas and each call's last content stay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * acpmux: supersede only within a prompt; fence the sealed rename Review findings on #27614: - A call id names a call within one prompt: a user_message or turn_started between two updates keeps both, in pages and on disk. A read cut at a prompt (a fork through a turn) sees every call as logged. - The copy goes in under the writers' lock a retention commit holds, and only over the very file it was read from (device, inode, length): a retention rewrite, a delete or a replacement is never overwritten. - A segment that changed stops the pass without moving the marker. - The copy is removed on any error, and a crash's copy by the next pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * acpmux: keep this PR to the open page; sealed-segment compaction moves to a follow-up Review found that rewriting sealed segments changes what a fork or export pinned mid-prompt reads, so this PR now changes only pages read newest first with a byte bound. A page of some kinds still resets at every prompt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * acpmux: touch only output-only updates, and keep the oldest of each call a page reads Review: dropping any covered update changed the fold where an update carries more than output (input keys merge, subagent _meta, a diff a hidden call shows), and the start time a page takes from the first update it reads. Now only an update of status and text content is touched, and the oldest of each call a page reads stays until an older record of the call makes it moot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * acpmux: scope superseded calls by subagent owner, keep content:null, honour stubs Review: a native subagent numbers its calls apart from its chat; an explicit content null clears output; a record a page stubs carries no output. Each now supersedes nothing it should not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * acpmux: fix superseded tests (clippy useless_vec, the clear test) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * acpmux: a params _meta of any other shape is never plain Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * cmux NEXT: one machine link state for the sidebar and the agent pane; a down machine reconnects on use (#26786) * feat(machines): one machine link state for the sidebar and the agent pane, reconnecting on use The sidebar machine header draws a spinner while a machine connects or reconnects and red when it is down; the agent pane gets the same word (machineLink) for its composer strip; opening or sending to a chat on a down machine reconnects it and goes on once its handshake finished. Sidebar-Look: Leo 2026-10-28 "you just get a little green dot ... as if you were working on your own computer, but now it's remote" Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-4ba8980ae4e8eecb82d6 * test(sidebar): a still of the machine headers in each link state Sidebar-Look: Leo 2026-10-28 "you just get a little green dot ... as if you were working on your own computer, but now it's remote" Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-4ba8980ae4e8eecb82d6 * fix(machines): an optional update draws the connected dot; a cancelled send is not retried Sidebar-Look: Leo 2026-10-28 "you just get a little green dot ... as if you were working on your own computer, but now it's remote" Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-4ba8980ae4e8eecb82d6 * fix(agent-pane): a send cancelled while its machine reconnects stops waiting at once Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-4ba8980ae4e8eecb82d6 * fix(agent-pane): a cancelled send reconnects nothing Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-4ba8980ae4e8eecb82d6 * fix(agent-pane): a send cancelled during its machine's reconnect is not retried Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-4ba8980ae4e8eecb82d6 * fix(agent-pane): the strip's machine and its Reconnect are the machine the tab's chat runs on Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-4ba8980ae4e8eecb82d6 * fix(agent-pane): a released tab's page leaves the machine link; the last one ends its observation Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-4ba8980ae4e8eecb82d6 * fix(agent-pane): name the settings JSONValue the page event carries Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-4ba8980ae4e8eecb82d6 (cherry picked from commit f354c6952a5a6f9521264472294d9f0880db35f1) * test(agent-pane): the Observable fake machine is not private (its macro's extension needs it) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-4ba8980ae4e8eecb82d6 * feat(sidebar): remote machines follow acpmux's peer state pushes from the app's own daemon The app's own acpmux watch now reads `_acpmux/peer_state_changed` and hands it to the remote machines' sidebar: a listed peer's header goes up or down at once, a repeat changes nothing, and an unknown or removed peer, or a reopened watch, reads `_acpmux/peers` again. A mirrored acpmux's (the Chief home's) pushes are ignored, so only this Mac's own links show. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-4ba8980ae4e8eecb82d6 * fix(sidebar): a peer list read that a push overtook is read again A push that lands while `_acpmux/peers` is in flight (a separate socket, so its answer may be from before the push) drops that answer and reads the list once more; pushes come only on a state change, so a burst ends in one more read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-4ba8980ae4e8eecb82d6 * fix(sidebar): any peer push drops the list read it overtook, one that changes nothing shown too Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-4ba8980ae4e8eecb82d6 * cmux NEXT machine link: a send stops waiting on a link that needs a person, and a failed socket or handshake reconnects too Review findings at fbdd271: awaitLink waited the whole 45 s budget when the reconnect ended stuck (a refused sign-in, a required update); it now ends at once. The retry wrapped only the endpoint, so a socket or daemon handshake failing after the endpoint came back bypassed the reconnect; the attach client now asks the machine link (AgentSessionAttachClient.Reconnect) when any part of its connection fails, and tries once more. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-5b0be570f0b96d2d4c7b * cmux NEXT machine link: the reconnect retry runs in the caller's task, and a failed first try ends no attachment Review findings at 703d820: a first try whose handshake failed closed its transport, whose onClose ended the page's attachment before the retry; and the reconnect wait ran in the shared unstructured connect task, so a cancelled call kept waiting. The retry now wraps the shared connection in the caller's task; a quiet first try tells its loss only when no retry follows or the retry fails too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-5b0be570f0b96d2d4c7b * Fix machine link event delivery and reconnect endpoint Agent-Session: aid-b12e655d1a89af187c5a * Pass empty scripts for machine link delivery Agent-Session: https://cmux.com/deeplink/agent?id=aid-16e16f9e7a55152abe76 * Report quiet attach loss after endpoint failure Agent-Session: https://cmux.com/deeplink/agent?id=aid-16e16f9e7a55152abe76 * Fix machine-link guard and lint findings Agent-Session: https://cmux.com/deeplink/agent?id=aid-16e16f9e7a55152abe76 --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: cmux-bot[bot] <348531757+cmux-bot[bot]@users.noreply.github.com> * ci(cmux-tui): owned minis skip the Go and branch Rust caches that never restore (#27840) * ci(cmux-tui): owned minis skip the Go and branch Rust caches that never restore On owned Mac minis, the macOS job's setup-go cache never restored: untarring it over the mini's own read-only ~/go/pkg/mod failed with Permission denied (8 of 8 sampled jobs), after a ~63 s download and before another save attempt. The rust-cache restored 0 of 8; its branch saves cost ~155 s each and filled most of the 10 GB GitHub cache store, evicting other entries. Owned minis now run setup-go without the Actions cache, and save the cargo cache only from feat-cmux-next. Blacksmith VMs keep both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-a7306ef145c005ad24ab * ci: keep cache regression out of guard workflow Agent-Session: https://cmux.com/deeplink/agent?id=aid-4777d4f75452817a742b --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next tests: the test process's children start without SWIFT_BACKTRACE (#28122) #25857 made the swift test wrapper export SWIFT_BACKTRACE=enable=yes,... so a crash of the test process prints its stack. Every child inherited it: - An exit test's child traps on purpose. With the variable set, it spawned the backtracer to symbolicate the whole test binary, and the parent sat in wait4 until the stuck-test watchdog ended the run (BlockingCheckTests .aCheckedFileManagerCallOnTheMainThreadTraps, #26809 run 40999753339 shard 6, exit 143). - A privileged tool writes "swift runtime: backtrace-on-crash is not supported for privileged executables." to stderr first. A test reading the child's stderr took that line for the answer: ChromiumRuntimeInstaller failed with unsafeEntry and then stalled 303 s (same run, shard 5), as the python bridges #27981 fixed one by one did. The test bundle's load-time constructor now unsets the variable. The Swift runtime read it when libswiftCore loaded, before the bundle, so the test process keeps its own backtracer and no child gets one. Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * chore(next): base autofix at e0f33500c849 - cargo fmt in cmux-tui Made again on e0f33500c849: it moved the inputs of a repair made at 941b7a4b133a. scripts/cmux-next/base-autofix.sh: https://github.com/manaflow-ai/cmux/actions/runs/41038166183 Re-applied on 91e0204e3ada, which feat-cmux-next moved to after e0f33500c849. * ci(cmux-tui artifacts): short Linux jobs take the shared 32/16 vCPU set (#28240) * ci(cmux-tui artifacts): short Linux jobs take the shared 32/16 vCPU set On 2026-10-30 every cmux-tui artifacts run on feat-cmux-next sat queued on blacksmith-4vcpu-ubuntu-2404 (run 41007923202: tree preflight waited 4 min, mark-tree-commits and point-feat-channel were still queued 15 min later), so nightly-next promotion stalled behind the tree marker. The 11 short Linux jobs now use the expression this workflow's publish jobs already use: a non-Blacksmith LINUX_RUNNER pool, else Blacksmith 32 vCPU, or 16 vCPU while the overflow switch has failed over. A new guard test evaluates every Linux runs-on of the workflow against each variable state. * Test the hosted LINUX_RUNNER and the Darwin cross override Codex review of #28240: the matrix lacked ubuntu-24.04, the value the overflow switch writes, and pinned LINUX_RUNNER_DARWIN_CROSS empty, so macos-cross-shadow's override was never exercised. Add both; a new test checks only that job reads the override. * acpmux: a bare session name or id prefix reaches the one live session on any host; several are refused with each HOST/NAME command (#28000) * acpmux: a bare name names the one live session on this host or a peer; several are refused with each HOST/NAME command Every command that takes a session resolves a bare name or id prefix the same way, in the daemon (hub/key_route.rs, applied on the unix socket before any check reads the key): the one live session with that whole name (else id prefix) across this daemon and its peers. Closed sessions count only when no live one matches. Several live matches are refused (session_ambiguous, exit 2) with each choice, and the CLI prints the exact command for each. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-fbbcdf857038daa63e0c * acpmux README: neutral host names in the ambiguity example Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-fbbcdf857038daa63e0c * acpmux key_route: test hub is an Arc; the refusal list owns its strings Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-fbbcdf857038daa63e0c * acpmux: red/green request test, a name shared by a session here and a peer's is refused on info and send Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-fbbcdf857038daa63e0c * acpmux key_route: a whole name and an id prefix are one tier; every choice in the data; keep's exact id is not routed; the argv swap skips an option's value (review of #28000) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-fbbcdf857038daa63e0c * acpmux key_route: a sub-session's whole name (parent/child) is routed across hosts too (review of #28000) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-fbbcdf857038daa63e0c * acpmux key_route tests: neutral sample host names (fleet rule lint) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-fbbcdf857038daa63e0c * acpmux key_route: poisoned locks are read, never unwrapped (crash ratchet) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-fbbcdf857038daa63e0c --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(l10n): fill the 8 cmux NEXT string-table keys missing languages (base red) (#28167) "String tables (21 languages)" failed on feat-cmux-next with 112 style errors against its warning cap of 100. The last 12 came from action.agentPane.continueAfterError (9e836df43650), which shipped in 9 of the 21 languages. Fill it and the seven older keys that made up the rest of the debt, so the check reports 0 errors: - ActionCatalog: action.agentPane.continueAfterError, action.palette.forkAgentConversation, action.palette.forkAgentConversationTo - PaneCreation: paneCreation.allCommands - Updater: updater.button.updateAvailable, updater.button.updateAvailableNamed - CmuxHome: home.plan.title, home.plan.activeCount (all 20 languages) Wording follows each table's neighbours (the Fork Conversation family, Continue In…, Update Available, detail.plan). State is "translated" where the key's other languages are, and "needs_review" in CmuxHome, as that table's other machine translations are. Agent-Session: https://cmux.com/deeplink/agent?id=aid-a98b158f6fd70b0eae26 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * ci(nightly): the remote-daemon build and attest jobs take the Linux runner set (#28209) #25935 (85628c020a3) moved build-nightly-remote-daemon and attest-nightly-remote-daemon off the Mac builder onto blacksmith-4vcpu-ubuntu-2404. That label is outside the trusted runner set of #27125 (9127e619f76, "Linux jobs take the runner set, not one small Blacksmith label"), so tests/test_nightly_trusted_routing.py test_every_linux_nightly_job_takes_a_runner_set_member is red on feat-cmux-next (20 failures, both jobs) and nightly-guards / release-notary is red on every feat PR. Both jobs now use the runner-set expression of the other Linux nightly jobs (16 vCPU when vars.LINUX_RUNNER names it, else 32). They stay off the Macs, which was the goal of #25935. tests/test_gh_budget_wait.py pinned the attest job to the 4 vCPU label; that one assertion conflicts with the trusted-routing rule, so it now checks that the job holds no Mac and takes the runner set. Its other assertions are unchanged. Test: python3 -I tests/test_nightly_trusted_routing.py (FAILED, failures=20 on 97133a48ad0; OK on this commit); python3 -I tests/test_gh_budget_wait.py (OK on both). * chore(next): base autofix at afe5e8ddc4c6 - regenerated web/data/cmux-shortcuts.generated.json - regenerated schemas/defaults/cmux-next.json - cargo fmt in cmux-tui Made again on afe5e8ddc4c6: it moved the inputs of a repair made at 4e5fe0232da3. scripts/cmux-next/base-autofix.sh: https://github.com/manaflow-ai/cmux/actions/runs/41040753079 * fix(inspector): hide unsupported chat environment save (#28238) * test: reproduce silent environment save failure Agent-Session: https://cmux.com/deeplink/agent?id=aid-ed6c50a2fcd43ba5fe7f * fix: surface failed Session Info environment saves Agent-Session: https://cmux.com/deeplink/agent?id=aid-ed6c50a2fcd43ba5fe7f * style: format environment save fixtures Agent-Session: https://cmux.com/deeplink/agent?id=aid-ed6c50a2fcd43ba5fe7f * test: assert actual gallery color scheme and reduced motion Agent-Session: https://cmux.com/deeplink/agent?id=aid-ed6c50a2fcd43ba5fe7f * test: wait for Environment before checking gallery settings Agent-Session: https://cmux.com/deeplink/agent?id=aid-ed6c50a2fcd43ba5fe7f * fix(inspector): hide unsupported chat environment save Agent-Session: https://cmux.com/deeplink/agent?id=aid-aaebdf126bd73033484e * acpmux: a person's Stop of a chat on a peer holds the other agents' messages there (#28141) * test(acpmux): a person's Stop of a peer's chat holds the agent messages there (red) On base the peer takes the forwarded Stop as a plain cancel: no queue_paused with reason stop on the peer, and the agent message runs. A fake peer checks that only a peer listing personStop gets the field, and that a non-person's Stop never carries it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3918eb692ec76bbeec6e * fix(acpmux): a person's Stop of a chat on a peer holds the agent messages there The peer daemon's link is never the person on the peer, so a Stop for a peer's chat was a plain cancel there: the stopped turn ended and the next agent message started a turn at once. This daemon now forwards the person's Stop with _meta.acpmux.personStop to a peer that lists the personStop feature, and a peer takes it only from a peer daemon's own link (PeerToken, held to the peer's rules, not via web or device) and runs the same person_stop as for its own person. An older peer gets the plain cancel. A client's own personStop field is dropped before the forward. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3918eb692ec76bbeec6e --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * docs(cmux-tui): agent-hooks: cmux-next has no ssh-tmux mirror (#27797) The line said ssh-tmux mirrored workspaces get no agent badges from the cmux-tui journal. A main sync had dropped the sentence #19104 added, so it read as a cmux-next gap and sent two helpers after one that does not exist (HQ#5226 row 3). cmux-next SSH machines run cmux-tui on the host with the full CLAUDE_EVENTS set; the ssh-tmux mirror is classic-only and reports badges through its own tmux subscription. Agent-Session: https://cmux.com/deeplink/agent?id=aid-c689cdaab52479676c52 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(webviews): dispose page subscriptions after their store stops (#27861) * fix(webviews): scope page store subscriptions to their lifetime Agent-Session: https://cmux.com/deeplink/agent?id=aid-aaebdf126bd73033484e * style(webviews): format store lifetime regression tests Agent-Session: https://cmux.com/deeplink/agent?id=aid-aaebdf126bd73033484e * fix(webviews): guard auxiliary store continuations Agent-Session: https://cmux.com/deeplink/agent?id=aid-aaebdf126bd73033484e * fix(webviews): fence stale app continuations Agent-Session: https://cmux.com/deeplink/agent?id=aid-aaebdf126bd73033484e * fix(webviews): stop stale app reload continuations Agent-Session: https://cmux.com/deeplink/agent?id=aid-aaebdf126bd73033484e * Agent pane: answered question and checklist as Claude desktop cards (#27938) * feat(agent-pane): answered question and checklist as Claude desktop cards An answered question now collapses to a compact bordered card in the transcript: each question, the chosen answers (Other text included) in grey under it, then who answered. An ask that ended unanswered is the same card with Skipped, Stopped before an answer or Not answered in the answer's place. The todo list keeps its bordered card, spinner, dashed and check marks. QuestionCard and PlanList are memoized on their immutable inputs; a new test counts their commits while 200 tokens stream in the same turn. Gallery: answered looks (stacked shipped, inline, check), ended looks (card shipped, pill, rule), and a todo list looks entry (card shipped, borderless, count). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-92fa78a91e29868c5eaa * fix(agent-pane): answered card spacing from the design tokens Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-92fa78a91e29868c5eaa * test(agent-pane): cards re-created per token, as the transcript does; lower the type-scale baseline Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-92fa78a91e29868c5eaa * test(agent-pane): count the cards' own renders, not a re-created Profiler's commits Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-92fa78a91e29868c5eaa * test(agent-pane): mark the module mock's promise as handled Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-92fa78a91e29868c5eaa * fix(agent-pane): one reply handler per live ask, so the memoized question card skips pane updates Review (Astra) of 0dd7e865: App.tsx made replyToQuestion(permission) anew each render, so the pending card's memo never held. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-92fa78a91e29868c5eaa * test(agent-pane): mount the live ask's card wrapper in the render-count test The live question card's reply handler moves into PermissionQuestion, which App.tsx's PermissionAsk renders; the test mounts that wrapper with one permission across 200 tokens, so dropping its useMemo fails the test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-92fa78a91e29868c5eaa --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * acpmux: a peer link hears no change of a row mirrored from another peer (#28152) * test(acpmux): a peer link hears no change of a row mirrored from another peer Red on base: every change of a mirrored row goes out on every peer link, which drops it (peer.rs), so with 25 peers each change went out 24 times. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c11d09c9f41988fc1d93 * acpmux: a peer link hears no change of a row mirrored from another peer Every change of a row this daemon mirrors from a peer went out as _acpmux/session_changed on every other peer's link too, where the link drops it (peer.rs: a mirrored row or a notice naming a peer). With 25 peers that was 24 copies of each change, tools included. A link now hears only this daemon's own rows' changes, as its watch list since #28105 lists only them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c11d09c9f41988fc1d93 * acpmux: a link still hears a peer's pending prompt; only session_changed is skipped Review of 8f1a1fdc: the link check returned before the peer permission branch, and a link is any client named acpmux-peer, so a unix-socket watcher with that name lost a peer's _acpmux/permission_pending. The check now guards only the session_changed send; the permission branch is as on base. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c11d09c9f41988fc1d93 --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * acpmux: briefs asks the peers at once and names each one left out (#28172) * test(acpmux): briefs asks the peers at once and names each one left out Red on base: the peers are asked one by one with the 600 s request budget each, so a stalled peer holds the list for ten minutes, and a refusing peer is left out with no word of why. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c11d09c9f41988fc1d93 * acpmux: briefs asks the peers at once and names each one left out _acpmux/briefs asked every connected peer for its briefs one by one, each with the 600 s request budget, and left out a peer whose answer failed with no word of why: one stalled peer held the list for ten minutes. The peers are now asked at once under one 10 s deadline (as _acpmux/models does), and each peer left out is named with its error in peersFailed, logged at warn and printed by acpmux briefs ls. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c11d09c9f41988fc1d93 --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * test(cmux-tui): a lost terminal's end finds its pages read ahead (red) Lawrence 2026-10-30: after a reboot the daemon's adoption records every lost terminal's end before it answers anyone: 750-821 ms for 462 terminals on a clone of a 41.6 GB registry, 60% of it cold page reads. Each end appends journal subjects (terminal, tab, pane, screen, workspace) to journal_subject_index_by_subject_sequence, keyed by random public ids, and a terminal event keyed by terminal id, so each insert lands on a leaf nothing read earlier. startup_prefetch_covers_every_page_a_terminal_end_writes: every page key an end writes is one the startup reads ahead. terminal_end_prefetch_keys is a stub that reads nothing, as the startup does now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(cmux-tui): the startup reads a lost terminal's end pages ahead Root cause (Lawrence 2026-10-30): after a reboot every terminal host is gone, and the adoption records each terminal's end before the daemon answers: on a clone of a 41.6 GB registry with 462 lost terminals that was 750-821 ms, and 60% of it was cold page reads. Each end appends journal subjects (terminal, tab, pane, screen, workspace) to journal_subject_index_by_subject_sequence (8.4 GB, keyed by random public ids) and a terminal event keyed by terminal id; 2,244 serial reads of leaves nothing had read yet (313 ms cold, 12 ms warm, measured alone). A background thread starts right after the registry opens and reads those leaves on 8 read-only connections of its own while the restore runs (terminal_end_prefetch.rs): the keys are every terminal that is neither exited nor tombstoned, since which hosts are gone is known only during adoption. It writes nothing and takes no registry lock; it only puts pages in the OS page cache, and its errors are dropped. When every host is alive the reads are wasted: a few thousand small reads on a background thread. Clone of the 41.6 GB registry, same Mac, release build, 462 lost terminals, 6 runs each: terminal.adopt_hosts 750-821 -> 410-425 ms; daemon start 2,401-2,638 -> 2,103-2,186 ms; first sidebar.get 2,484-2,759 -> 2,183-2,270 ms (one run 3,683, an exec stall outside the daemon's own timing). sidebar.get p50 about 5 ms either way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * acpmux: new and ensure keep a predecessor's harness, or say why not (#28016) * test(acpmux): a successor keeps its predecessor's harness (red) Part of HQ#5266. Red/green tests for acpmux new naming a predecessor: a bare default start where the predecessor ran a named profile is refused, a different harness is warned with the flag that keeps it, --harness-from copies it, and nothing names a predecessor: no warning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-f479d2ccf4cedb6ae7cf * feat(acpmux): new and ensure keep a predecessor's harness, or say why not Part of HQ#5266 (item 3). Leo, 2026-10-30: "that visibility into huge-blue guys was (and is?) pretty bad". The deputies relaunched with only area and role tags came back on the bare default harness, without their profile's autocompact cap. A session/new from the command line names a predecessor by --harness-from, a succeeds= tag, --report-to or its starter of the same name family, a NAME-N name, or an area= tag (one pass over the session index; no transcript read). A start with no -m or -p that lands on the default where the predecessor ran a named profile is refused; a start whose profile, launcher, model, effort or autocompact cap differs gets a one-line warning naming both and --harness-from. --harness-from copies the predecessor's profile, preset, model and effort. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-f479d2ccf4cedb6ae7cf * fix(acpmux): a successor's check reads the session index once and copies Ultracode Review of #28016 (Sol): --harness-from copies Claude's Ultracode effort, the launcher compared is the one the session last started with, a copy that still differs says so, ensure --host --harness-from is refused up front, and the predecessor is found in one pass over the index. --report-to keeps the name-family rule the brief asks for (a helper's coordinator is not its predecessor), now with a test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-f479d2ccf4cedb6ae7cf * fix(acpmux): --harness-from is refused wherever it would not be honoured Re-review of #28016 (Sol): a session/new with harnessFrom over the app's or a web connection, or on an ephemeral run or an adopt, was started as an ordinary session and the copy silently dropped. It is refused now, over every connection but the local socket. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-f479d2ccf4cedb6ae7cf --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * feat(next): scroll the session rail independently (#26520) * feat(next): grow the session rail height Agent-Session: https://cmux.com/deeplink/agent?id=aid-424868970109118b3cd6 * feat(next): let the session rail scroll independently Agent-Session: https://cmux.com/deeplink/agent?id=aid-424868970109118b3cd6 * fix(next): wait for rail overflow in height gallery Agent-Session: https://cmux.com/deeplink/agent?id=aid-424868970109118b3cd6 * fix(next): align rail hit testing with visible ticks Agent-Session: https://cmux.com/deeplink/agent?id=aid-424868970109118b3cd6 * Rail wheel: the rail owns it only while it scrolls; browsing is per session The transcript reader listener captures on the scroller, an ancestor of the rail, so a stopPropagation on the list ran too late: an upward wheel over an overflowing rail released the transcript follow. The reader now skips a wheel over a rail list marked data-rail-scrolls (railWheel.ts). A rail that fits its prompts is no scroll container again (overflow hidden), so the wheel over it reaches the transcript. The browsing latch remembers its session, so a reused transcript showing another chat follows its reader. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Rail wheel test: wrap a line past the print width Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Rail wheel: a jump landing correction goes on under a rail wheel settleScroll ends on any reader input captured on the window, which runs before the rail can stop the event, so a wheel over an overflowing rail cancelled a clicked tick landing mid-correction. It now skips a wheel the rail owns (railOwnsWheel), as the transcript reader does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Scrollbar guard: the rail list may hide its own scrollbar The session rail spec keeps the overflowing rail list free of any visible scrollbar. Allow the two hiding rules in threadMinimap.css only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next: a subagent pane draws its chat from the kept tail (#28235) showSubagent asks the app for a subagent pane, which is a new tab on the chat's own session, but never handed the chat over first the way openOwnTab and onMoveToTab do before chat.openTab. The pane opened blank and waited on its own attach. Hand the chat over first, and hold the call site in the source-reading wiring test. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * chat-open bench: a timed-out or errored open never counts as a time (#28255) valid() looked only at whether first_content_ms and longest_empty_ms are numbers, so a trace that timed out but still carried them went into the medians. Reject timed-out and errored records first. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * ci(cmux-tui): the attestation waits out a rate limit until GitHub answers again (#28269) Owner run 41017791425 (2026-10-30) built tree 13939dda, then lost it in "publish to R2": actions/attest-build-provenance was refused "API rate limit exceeded for installation" at 07:14, 07:15, 07:17 and 07:21. The backoffs between the four tries waited a fixed 60, 120 and 240 s (gh_budget_wait.sh --backoff), the limit still held at the last try, and the job failed with the tree unpublished, so the NEXT nightly kept promoting an older commit. gh_budget_wait.sh gets --backoff-probe TRY PROBE: the action hides the refused response, so it reads PROBE (a read of the attestations API with the same token) and, while GitHub refuses it for the rate limit (429, or 403 with x-ratelimit-remaining: 0, Retry-After or a message naming the limit), waits for the reset that refusal names (Retry-After, else x-ratelimit-reset, else the secondary-limit backoff) and reads again. The next try runs once GitHub answers. A probe answered at once (200, 404, a permission 403, a 5xx) waits as --backoff did. Every wait counts against the job's ledger; the publish passes now allow 50 minutes (was 15), inside their 75-minute timeout with the 15 minutes of work. The last try still must pass, so nothing ships without provenance, and a real attestation failure still fails the publish. Tests: test_gh_budget_wait.py drives --backoff-probe against a fake gh (the reset a refusal names, a limit that holds for three looks, Retry-After and 429, the secondary-limit schedule, a probe answered at once, the job total); test_cmux_tui_publish_retries.py checks the publish passes wire it. Both fail against the previous script and workflow. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * New Tab: project and machine head the composer, menus open away from the chat, and Cloud picks wait for the machine (#27956) * feat(composer): project and machine head the composer; menus open away from the chat Leo 2026-10-29: "project and cloudness at the top. make them open away from teh chat, which is obviously up if it is up, down if it is down. and stop making them take up so much space." - New Tab heads its box with the folder and machine in every look (no longer in the quiet row). - Location menus open on the side away from the conversation, read from the composer position, and slide along that side to stay in the window instead of flipping onto the chat. - One compact strip: 24 px, 4 px between content-sized pills. - Cloud machines: a pick on a paused or unlinked machine waits for it and moves the chat there once ready; resume reads Starting at once; failed reads Couldn t start; New Cloud Machine says what one gives in the plain menu. - design/composer-menus.md records the placement rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(composer-menus): New Tab placement only; the agent chat keeps its row Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Gallery cover names the component; onboarding test matches the cloud row by prefix The New Cloud Machine row now carries its description, so its accessible name starts with the label rather than equalling it. A gallery cover names a component, so the hook cover goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * New Tab headline play reads the computer pill above the box The computer pill moved from the quiet row to the strip above the box, beside the folder pill, so a project name of another width moves it sideways; the play now holds only its row and size. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Cloud pick wait ends on a failed start and on any chat change A refused resume or open now marks the machine failed instead of leaving it at its old stage, and a pick waiting on a machine that failed reads Couldn't start and stops waiting to move the chat. A chat change drops the wait for good, so a chat that returns to the same key (an empty New Tab id) never inherits another chat's pick. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Cloud pick wait ends for good when the account stops listing the machine Before, the stored pick survived a list without the machine, so the machine coming back with a ready peer could still move the chat. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(acpmux): send's examples go in help_examples, not its own after-help (base red) (#28301) #28000 gave `acpmux send` an after-help with its own Examples:, while #28013 (landed before it) moved every command's examples into cli/help_examples and refuses a listed command that also has its own: help_examples::tests::every_example_entry_names_a_visible_command_once fails on feat-cmux-next since #28000 ("send": its after-help has its own Examples:). #28000's three examples (the one live name, HOST/NAME, an id prefix) move into the send entry; its after-help keeps the refusal note. Test: cargo test -p acpmux --lib help_examples (5 pass; red on base). Agent-Session: https://cmux.com/deeplink/agent?id=aid-c11d09c9f41988fc1d93 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * cmux mcp serve: start other MCP servers (cmux-cua) on first call, not to list them (#27976) * cmux mcp serve: list other servers' tools from a stored list; start them on first call A harness lists its MCP servers' tools as it starts (Codex once per thread), and `cmux mcp serve` started every upstream (cmux-cua among them) to answer that list. So every chat ran a cmux-cua helper whether or not it ever used computer use: 93 to 131 idle helpers on cosmic-gray on 2026-10-29. The list an upstream gives at its start is now stored in ~/.cache/cmux/mcp-upstreams (0700, files 0600, written whole), keyed by the server's command, args, env names (not values), the command's size and mtime, and cmux's version. tools/list answers from it without starting the server; the first tools/call starts it, relists and stores again. With no stored list, or a torn one, the server starts to be listed as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux mcp serve: check a call against the started server list; keep stored lists private and per program Review (codex gpt-6-astra): a stored tool the server dropped is refused as not run before it is sent; entries are read only from a folder and file the user owns that others cannot write (no symlinks); the key names the program PATH resolves the command to; a list is stored only under the key the command had before its server started. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux mcp serve: rustfmt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux mcp serve: clippy Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux mcp serve: key a stored list by the executable the server runs on its own PATH Review (codex gpt-6-astra): resolve skipped neither non-executable files nor the server env PATH that Command uses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux mcp serve: a stored list keys the script an interpreter runs; a path nobody may run has no key Review (codex gpt-6-astra) round 4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux mcp serve: tests run an executable server and keep their script Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux mcp: key a bare relative script arg too Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux mcp serve: fmt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * cmux NEXT MCP: tool evals on real Claude/Codex harnesses, and code mode takes the script shapes models write (#28085) * tool-evals: replay real Claude Code turns against real cmux MCP, CLI and a headless daemon (t3code mcp-tool-use shape) * tool-evals: stand-in app replays the real app's action catalogue (823 tools, as in the app) * cmux docs: a sentence query finds the tool that matches most of its words; stop words ignored; daemon tool ranks before its app_ twin * test: cmux mcp serve resolves current to the caller's pane in a session no app owns (red) * cmux mcp: current resolves from the daemon when no app owns the session, as the CLI does * tool-evals: stand-in app answers beside; tab-not-split and run-without-empty-shell cases * tool-evals: 20 cases mined from real transcripts; docs ranking test names the daemon tool first * cmux mcp: with no app, a terminal with no place goes beside the chat (daemon neighbor); decimal fields take numbers; tool-evals Edit/Write * cmux mcp code mode: short rules in instructions (fits the 2048-char cut), common-tool sheet and result shapes in cmux_exec; compact docs answers; did-you-mean on unknown tool * test: compact docs answers and did-you-mean for an unknown tool in a script * cmux mcp code mode: tool sheet names the arguments of rename, grouping, status and notification (tool-evals: three regressions back to pass); harness setup settles before the agent looks * tool-evals: round 1 results (Sonnet and Opus, code mode); model retry on a dropped gateway connection * cmux mcp: review fixes: no-app fallback only places terminals (open_url stays on the daemon); text says terminal takes no current; compact only cmux's own tools and keeps required args; decimal schema stays a string, whole floats taken; empty query lists families * cmux docs: a tool's own name ranks first; caller terminal comes through the backend so the no-app open_url test fails without its guard; focus wording matches tab_focus * tool-evals: generic harness (OpenCode-like prompt, cmux tools listed directly) and an OpenRouter adapter, so any model runs * tool-evals: a failed session open stops its daemon (an overloaded panel leaked 1,115); OpenRouter fails fast on 4xx and a missing key * tool-evals: hybrid surface (cmux_exec plus the 21 common tools listed directly) * tool-evals: run on Linux (the rbx builder) without sandbox-exec * tool-evals: rbx panel scripts and cross-model comparison * cmux mcp code mode: tool calls return Promises (.catch works), a script written as one function runs, a missing selector and a timer name their fix, and the sheet runs commands in a shell tab that stays open (11-model panel) * tool-evals: real claude and codex CLIs as harnesses (claude-cli:<model>, codex-cli:<model>), each in a clean agent home with cmux MCP behind a logging relay * tool-evals: Claude through the Agent SDK and Codex through app-server (no TUI per run), each with a clean home and cmux MCP behind a logging relay * tool-evals: Claude Agent SDK runs the installed Claude Code (the bundled 2.1.81 got a 400 for Opus 5.5); an API Error result is an agent error * test(tool-evals): eval agents cannot open pages or reach the person's cmux (red) The sign-in-link-in-cmux case asks a model to open a link; models that ran `open https://example.com/device?code=ABCD-1234` opened it in the person's default browser (Helium) 11 times on 2026-10-28. The Claude Code and Codex samples run with no sandbox, and the shell sandbox allows /usr/bin/open. Moves the shell profile into live-guard.mjs unchanged and adds probes for open, osascript and the person's cmux app sockets in both profiles. * fix(tool-evals): eval agents run sandboxed off the person's browser and cmux app Claude Code and Codex samples now run under sandbox-exec with agentProfile, and the API runner's shell profile gets the same guards: /usr/bin/open and osascript cannot run, LaunchServices open is denied, and the person's cmux app sockets (~/.local/state/cmux, /tmp/cmux*.sock, $TMPDIR/cmux*/) are unreachable. A model that runs `open <url>` in sign-in-link-in-cmux now fails inside its sample instead of opening a Helium tab or a cmux tab. bun test tests/tool-evals/live-guard.test.mjs: 10/10 (3/10 before). * tool-evals: a sample waits while the Mac's PTYs are near the cap (the person's cmux shares it) * tool-evals: --remote-daemon runs each sample's daemon and MCP server on another host over ssh (this Mac's PTYs stay free); rbx panel takes a binary and drops gpt-oss-120b for GLM 5.3 Prime * tool-evals: agents run in a sandbox that hides this machine's skills and other samples' results (Codex read an old cmux skill and searched past results) * tool-evals: sign-in-link-in-cmux is an app-tier case (headless has no cmux browser); --tier selects * cmux mcp code mode: a script reads a change's new ids from the top of its answer too, and a terminal by its tab (head panel: 39 'needs terminal' errors across models) * cmux mcp: a terminal named by its tab resolves to that tab's terminal id (the daemon's current needs a pane, so the earlier shortcut failed its own test) * cmux mcp: review fixes: a tab names its terminal only where a terminal is required (terminal_list keeps its tab filter), in the tab's session and scope; a script that only starts like a function runs as written, and a wrapped one gets cmux * cmux mcp code mode: function-form scripts detected from the text alone (no eval probe, which could run script code), arrow forms wrapped again; tool-evals guard allows the resolved sample path (Codex could not read its home) * tool-evals: the remote MCP settings file is written and read back before the server starts (15 h4 samples got 'MCP server is off' and fell back to the shell) * cmux mcp code mode: a script that calls its own function returns that function's value; drop a test case that could not fail * cmux mcp code mode: a self-calling function script returns its value even when its body returns (the guard looked for 'return' anywhere) * tool-evals: the guard lets an agent list the directories above its own sample (Codex canonicalizes CODEX_HOME and stalled at start) * tool-evals: a sample that never reached the model (429, exhausted accounts, dropped stream) is retried; --rerun-errors runs again only an earlier run's agent/harness failures * cmux mcp code mode: the tool sheet names pane_neighbor_get (the pane on the person's right) and terminal_process_get (Opus looked for a layout tool); stand-in app focus names the tab * cmux mcp: a record a tool returned works where its id goes (models pass pane_neighbor_get's {pane: {id}} to tab_list); the sheet shows that answer's shape * cmux mcp code mode: the sheet says 'next to X' means a new tab in X's pane, and pane_split only when the person says split (6 of 9 OpenRouter models split the logs pane) * tool-evals: workspace order is graded by the sidebar's personal placement order (workspace_place {index} moves a workspace there; the session index stays) * tool-evals: round 2 results (12 models, real harnesses) * cmux mcp: a record becomes its id only where an id goes (a selector or a resource-id field); a json field keeps its object (review: keybindings_set args, settings_set value) * fix(cli): describe every cmux cloud verb in its help (#28252) * fix(cli): describe every cmux cloud verb in its help `cmux cloud --help` listed 54 verbs as bare names in two misaligned columns, with no descriptions, examples or mention of --confirm, and `cmux cloud <verb> --help` printed the same list again. The help is now grouped (machines, run and connect, files, snapshots, ports, network, chats, account), opens with a quick start (list, new, exec, pause, resume, kill), gives each verb one line in English and Japanese, and marks the verbs that need --confirm. `cmux cloud <verb> --help` prints that verb's usage and description and names `cmux action describe` for the full argument list. `cmux help cloud` now follows the locale too. The verbs live in one table (cli/cloud_help.rs) whose rows are rendered in one column; a test fails when it differs from the catalog's offered cloud verbs, and the cloud exec parse error reuses the table's usage line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-fb75b7af9902231028aa * fix(cli): cloud help flags match the catalog; say which verbs take no target Review findings: new-machine gains --repo, --bootstrap and --bootstrap-script, open-desktop --viewer, deliver-secrets --names; apply-machine-config loses a --repo it does not take. The intro says an action on one machine needs --target, since new-machine, start-session --name and move do not. A test now pins which verbs take no target. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-fb75b7af9902231028aa --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * ci(ios): add unsigned Release simulator compile route (#27642) * test(ci): cover compile-only iOS Release simulator route Behavioral tool-double coverage; fleet execution pending publication. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-fb41776d46de72ff2a6a * ci(ios): add unsigned Release simulator compile mode Add release-build with no injected DEBUG/demo conditions and no simulator launch or publication; retain evidence on success and failure. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-fb41776d46de72ff2a6a * test(ci): consume simulator inventory in runtime fixture Drain the piped JSON before returning the fixture choice, matching the real selector and avoiding SIGPIPE in legacy-mode checks. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-fb41776d46de72ff2a6a --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(ios): make recovery screens retry their failed loads (#27309) * fix(ios): retry failed loads from recovery screens Wire the shared recovery control to the workspace loader, terminal owner, stream opener, Feed refresh, Home refresh and conversation loader. Add behavioral loader tests and persistent demo capture states. Fleet compile and execution pending; this lane VM has no Swift compiler. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-ca6f72e5137c0c9569e8 * test(ios): use anonymous recovery capture fixtures Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-ca6f72e5137c0c9569e8 * fix(ios): keep terminal recovery controls inactive in tab cards Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-ca6f72e5137c0c9569e8 * fix(ios): split terminal retry closure for compiler Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-ca6f72e5137c0c9569e8 * fix(ios): validate recovery release highlight Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-ca6f72e5137c0c9569e8 * fix(ios): await feed recovery and reopen failed channels Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-ca6f72e5137c0c9569e8 * fix(ios): import feed recovery dependencies explicitly Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-ca6f72e5137c0c9569e8 * fix(ios): fail closed feed channels before retrying Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-ca6f72e5137c0c9569e8 * fix(ios): bound feed handshakes and release cancelled requests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-ca6f72e5137c0c9569e8 * fix(ios): clear chat readiness after cancelled feed binding Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-ca6f72e5137c0c9569e8 * fix(ios): retain terminal failure recovery and parallelize feed retry Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-ca6f72e5137c0c9569e8 * fix(ios): bound terminal feed reads during recovery Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-ca6f72e5137c0c9569e8 * fix(ios): simplify concurrent recovery isolation capture Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-ca6f72e5137c0c9569e8 * fix(ios): close failed initial terminal attachments Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-ca6f72e5137c0c9569e8 * fix(ios): await Home reconnect before retry refresh Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-ca6f72e5137c0c9569e8 * fix(ios): stop cancelled Home retries Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-ca6f72e5137c0c9569e8 --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next: a chat's first frame waits on one daemon round trip (#27755) * cmux-next: a chat's first frame waits on one daemon round trip Opening a chat asked acpmux, one after another: initialize, _acpmux/status, the chat list, the attach, then the turn opener, permissions and handoff. Now everything after the handshake goes out together (status, the list, the attach and its permission and handoff reads), and acpmux names the turn's opener in the attach reply (`turnOpener`), so the first frame waits on the handshake plus one round trip. An older acpmux leaves `turnOpener` out and the pane reads it as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * acpmux: rustfmt the turn opener Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next: hold a dropped early attach's live records until the chat attaches again Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next: an early attach whose reply is of another session starts the chat over Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next: name the early attach's held records apart from copyLive's Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next: an early attach whose row changed after it streamed attaches again Records the attach asked with the chat list streamed before the row changed may be of the session before (recreated meanwhile), so the open attaches again. A dropped early attach's held records are discarded by the next attach, whose page holds them, instead of applied: some may be of another session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next: the new-tab tail tests wait for the tail read the early attach goes out before The attach now goes out with the chat list, so a held attach is asked before the host tail is read and drawn; the tests let that read land before they look. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next: no retry for an early attach whose row only changed status An ordinary record is followed by its row change, so marking an early attach suspect on any row change after a record retried normal streaming opens. The cases it was for are peer chats, whose dropped early attach's records the next attach now discards; a row that names another session is still caught by the watch (strayedFrom) and the reply checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next: the peer-copy daemon scripts the attach asked of the listed chat The pane now asks its chat's attach with the chat list, before it knows the chat runs on another machine, and drops that attach for a peer chat. The stand-in daemon answered its once-only hooks (records during the attach, a recreation, refusals) on that first attach; it now answers an attach asked before the list as the machine is, and scripts the one the open asks of the chat as listed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next: the peer-copy daemon tells an attach asked before the list by the list's answer The pane sends the list and the early attach in one go, so the list is asked before that attach; what marks it is that the list was not answered yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next: a dropped early attach's refusal is handled; peer tests count the listed attaches Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next: the peer-copy daemon answers an early attach of its local chat as before Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next: a dropped early attach is detached once it is in, not before acpmux runs each request on its own task, so a detach sent while the attach was still out could run first and leave its subscription behind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Agent pane: copy button in an expanded card's top-right corner (#28057) * feat(agent-pane): copy button in an expanded card's top-right corner Leo: "wouldnt it be a little copy symbol on the top right within this card?" One shared CardCopy (the transcript's CopyButton) at the top-right of every expanded message or prompt card: a started sub-session's prompt, a wait's reply, another agent's message. Faint at rest, full on the card's hover; the highlight is the button's own box. Raw stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-92fa78a91e29868c5eaa * fix(agent-pane): card copy panel contains its float; gallery tool without a non-null assertion Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-92fa78a91e29868c5eaa * fix(agent-pane): card copy uses the layer and size tokens; gallery notes wrap Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-92fa78a91e29868c5eaa * fix(agent-pane): card copy gallery writes through a stand-in clipboard; spacing token Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-92fa78a91e29868c5eaa * style(agent-pane): format the card copy gallery Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-92fa78a91e29868c5eaa * fix(agent-pane): card copy gallery presses the button after the panel settles The matrix runner's pointer click landed while the panel's open motion still moved the button, so the copied states never saw the check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-92fa78a91e29868c5eaa --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * chore(next): base autofix at 569f9ea15f62 - vp fmt webviews/src/agent-session/acpmux/chatOpenTrips.test.ts - vp fmt webviews/src/agent-session/acpmux/direct.ts - vp fmt webviews/test/agent-pane-new-tab-cached-chat.test.ts - vp fmt webviews/test/agent-pane-peer-copy.test.ts scripts/cmux-next/base-autofix.sh: https://github.com/manaflow-ai/cmux/actions/runs/41050433356 * sidebar: a hover control stays lit while its menu is open, and lights alone (#27912) * sidebar: a hover control stays lit while its menu is open, and lights alone A workspace row's more, close and chevron followed isHovered alone, so they vanished as the pointer moved onto the row's open more menu. SidebarRowMenu now holds WorkspaceRowView.showsMenu for the synchronous popUp; the controls stay and OverflowButton.showsMenu keeps the more button pressed (lit). The row's fill does not change. A workspace tile's more menu reused the profile control's showsMenu, which paints the selected fill over the whole pill. The tile now holds its hover controls and lights only its more button; the profile control, whose pill is the control, keeps the selected step. A group header's open editor painted the hover step over the whole bar and did not light its trigger. The more button now stays lit whi…
…29295) cmux-tui reserves a tab's attach slot while the attach connects to acpmux and refuses a second attach of that tab on the connection meanwhile (agent_session.limit, "too many attached agent tabs"). Since #27755 the page asks its session list (_acpmux/watch) and its attach together, and the remote wire turns both into agent-session-attach on one connection, so a Cloud or SSH machine's chat never connected. The later attach now waits for the earlier one and is then a page of the live attachment. Agent-Session: https://cmux.com/deeplink/agent?id=aid-aa0f0a21b40fda1441b8 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* test(webviews): reproduce missing local peer state events On base 1f723c6, bun test src/agent-session/acpmux/peerStateEvents.test.ts runs 7 tests: 3 fail and 4 pass. The client never reads peers and ignores the local state feed. Nonlocal and feature-absent fallback cases pass. Native validation is held by #26809. Agent-Session: https://cmux.com/deeplink/agent?id=aid-93c3a66aa0c6bbfa650c * fix(webviews): consume local peer state snapshots and events Gate peer events on the advertised feature and local/app origin. Read once after watch at initialize/reconnect, retain exact daemon state, clear nullable metadata, deduplicate lag replay, and reject superseded sockets/reads. Add a zero-parameter pane snapshot relay filtered to name, state, lastSeen and error. Preserve picker presentation and policies. Linux web validation: 14 focused tests / 62 assertions pass; bun --bun run check passes including formatting, lint, typecheck (2933 files), and protocol checks. The focused regression was red before repair (3 failed / 4 passed). Native relay tests were added but native build/test/parse and CI dispatch remain held by #26809/HQ4779. No real-machine capture is claimed. Agent-Session: https://cmux.com/deeplink/agent?id=aid-93c3a66aa0c6bbfa650c * fix(webviews): reset peer event capability before reconnect The replacement socket must initialize its own local/app origin and peerStateEvents feature before receiving peer notes; invalidate prior read tokens at open start. Focused Linux web tests: 15 pass, 66 assertions. Full bun --bun run check passes. Native verification and CI remain held by #26809/HQ4779. Agent-Session: https://cmux.com/deeplink/agent?id=aid-93c3a66aa0c6bbfa650c * fix(webviews): a lag notice reads the peer snapshot again The daemon drops peer_state_changed notices when a client lags and says so with _acpmux/lagged; resyncing only the selected chat left a machine that went down meanwhile shown as connected. Review P2 on #26885. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-710a4a518c009a6ad126 * test(webviews): reproduce a restarting peer dropped from the peer snapshot _acpmux/peers says `restarting` (peer_link.rs PeerState::as_str) while a peer's daemon restarts. The adapter drops that row, so the authoritative snapshot clears the peer's state and the picker reads it as ready. Red: bun test src/agent-session/acpmux/peerStateReducer.test.ts src/agent-session/acpmux/peerStateEvents.test.ts: 16 pass, 2 fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972 * fix(webviews): keep a restarting peer in the peer snapshot _acpmux/peers names every PeerState acpmux has, `restarting` included (peer_link.rs as_str), while peer_state_changed maps it to reconnecting. The adapter now accepts it, so a restarting peer keeps its state and reads not connected instead of falling back to ready in the picker. Green: bun test src/agent-session/acpmux/peerStateReducer.test.ts src/agent-session/acpmux/peerStateEvents.test.ts: 18 pass, 0 fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972 * test(webviews): reproduce a listed peer the peer snapshot omits reading as ready A peer that _acpmux/status lists but the authoritative _acpmux/peers snapshot omits (removed between the two reads, or an invalid row) loses its state and `connected`, so environmentReady offers it as ready. Red: bun test src/agent-session/acpmux/peerStateEvents.test.ts: 10 pass, 1 fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972 * fix(webviews): a listed peer the peer snapshot omits reads not connected An authoritative _acpmux/peers snapshot replaced the peer maps with its own rows, so a peer the status read still lists lost its state and `connected` and the picker offered it as ready. Such a peer now reads not connected until a note or the next read says otherwise. Green: bun test src/agent-session/acpmux/peerStateEvents.test.ts: 11 pass, 0 fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972 * test(webviews): reproduce an omitted peer losing its last-seen status When the authoritative peer snapshot omits a peer the status read lists, its peerStatus becomes only { connected: false }, so the offline banner (chatUnavailable) loses lastSeen and attempts. Red: bun test src/agent-session/acpmux/peerStateEvents.test.ts: 10 pass, 1 fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972 * fix(webviews): an omitted peer keeps its last-seen status The authoritative-snapshot branch replaced an omitted peer's status with { connected: false } alone. It now keeps lastSeen, attempts and error from the last read and drops only the stale state, so the offline banner still says when the machine was last seen. Green: bun test src/agent-session/acpmux/peerStateEvents.test.ts: 11 pass, 0 fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972 * test(agent-pane-policy): reproduce _acpmux/peers missing from the Rust relay policy The web pane sends _acpmux/peers (zero parameters) and the macOS host admits it, but policy.json does not list it, so a Rust host refuses the request and its reply is not filtered. The cases also keep a numeric lastSeen, as the Swift .number shape does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972 * test(webviews): reproduce a stale restarting state and the snapshot on the first frame - A status read after a snapshot said `restarting` keeps it: a failed restart goes restarting -> reconnecting, both `reconnecting` on the wire, so no note says so and readPeers re-applies the snapshot. - connect awaits _acpmux/peers before the first frame, a second round trip after the one #27755 keeps it to (a held snapshot holds connect). - chatUnavailable calls a restarting peer offline instead of connecting. The initial snapshot is settled in the fixture, and the in-flight status test now races a real in-flight read. Red: bun test src/agent-session/acpmux/peerStateEvents.test.ts: 11 pass, 2 fail; bun test test/chat-unavailable.test.ts: 1 fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972 * fix(agent-pane-policy): admit _acpmux/peers in the Rust relay policy The web pane sends _acpmux/peers and the macOS host admits it with a zero-parameter rule and a name/state/lastSeen/error reply filter. policy.json now does the same, so a Rust host no longer refuses the request. ReplyShape gains Number (a finite number, never a boolean), mirroring Swift's .number, so lastSeen survives the filter. Red at a4b6b6e (cmux-ci, cargo test -p cmux-agent-pane-policy): parity replies FAILED, 15 passed, 1 failed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972 * fix(webviews): a status read clears a stale restarting state; the snapshot leaves the first frame - A _acpmux/status read now drops every peer event entry no note has updated since the read started (PeerStateEvents.statusRead), so a failed restart that the daemon reports only as a state (no note) reads reconnecting instead of restarting forever. - connect starts the _acpmux/peers snapshot once the list's watch answered and draws it when it lands, instead of awaiting it before the first frame (#27755's one round trip). - chatUnavailable counts a restarting peer as still connecting. Green: bun test peerStateEvents, peerStateReducer, environments and test/chat-unavailable.test.ts: 28 pass, 0 fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972 * test(agent-pane-policy): _acpmux/peers passes; the escaped-name refusal samples another unlisted method frames.json sampled _acpmux/peers as a daemon method the pane never sends. The pane now sends it (policy.json admits it), so those two refusal cases move to _acpmux/peer_remove, which stays unlisted, and a pass case covers _acpmux/peers like _acpmux/status. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972 * test(webviews): reproduce status and snapshot reads landing out of order The status-read fence and the snapshot fence are separate, so: - a snapshot that started before a status read, and landed after it, brings back the restarting state the status read cleared; - a status read that started before a snapshot, and landed after it, deletes the newer snapshot's state. Red: bun test src/agent-session/acpmux/peerStateEvents.test.ts: 13 pass, 2 fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972 * fix(webviews): order status and snapshot reads on one clock PeerStateEvents now ticks one clock for notes and for the start of every read (statusStarted, readStarted). A snapshot that landed after a status read started later is dropped, and a status read drops only entries written before it started, so an older read of either kind can neither bring back a cleared restarting state nor erase a newer snapshot. A status read clears the snapshot's authority only when it is newer. The reducer tests take the read's tick in place of a separate revision. Green: bun test peerStateEvents, peerStateReducer, environments and test/chat-unavailable.test.ts: 30 pass, 0 fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972 * fix(webviews): a replaced socket's replies still land; only its peer notes drop The stale-socket guard dropped every message from a socket the client had replaced, so a request asked on it never settled: the export that watches for a socket swap waited forever instead of rejecting (conversation/exportClient.test.ts timed out on this branch and passes on base). Only _acpmux/peer_state_changed from a replaced socket (or after close) is dropped now; everything else is received as on base. Red: bun test ./src/agent-session/acpmux/conversation/exportClient.test.ts times out at a01e4e5 (CI react-apps-check shard 1: TIMEOUT twice, passes replayed on base ad6de25). Green: 3 pass; peerStateEvents 15, reducer 8, chat-unavailable 7 pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972 * test(webviews): reproduce an older status read overwriting a newer one readPeers writes each _acpmux/status reply into peers, peerConnected, peerStates and peerStatus before the clock fence runs, so of two refreshPeers calls the one asked first and answered last wins. The fixture's status read now answers with what the daemon knew when the read reached it. Red: bun test ./src/agent-session/acpmux/peerStateEvents.test.ts: 15 pass, 1 fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972 * fix(webviews): an older status read that lands late is dropped whole statusRead now says whether the read is newer than every status read that already landed (and than this connection); readPeers drops one that is not before it writes peers, peerConnected, peerStates or peerStatus, so two refreshPeers calls answered out of order keep the newer answer. Green: peerStateEvents 16, peerStateReducer 8, chat-unavailable 7, exportClient 3, chatOpenTrips 3, agent-pane-peer-copy 55, agent-pane-cached-chat-open 10 pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972 --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
Why
Leo, 2026-10-29: "i also like the way it just loads in the subagent stuff instantly :(" / "as in i want ours to load fast too :(" / "but i also wonder how much of this is simply putting the desktop at higher priority?" / "you're looking in the session transcripts in what cmux next is reading?" / "like the gui rendering the tui which comes from the daemon" / "idk what the claude code cli has but that's a lot of what im running in cmux next". Earlier: "but do chats load fast yet? even with high # of chats, high load? and does it still flash/flicker?"
Opening a chat asked the daemon one thing at a time before the first row:
initialize, then_acpmux/status, then the chat list (_acpmux/watch), then_acpmux/attach, then the turn opener (_acpmux/events), permission groups and handoff. On a busy daemon each of those is a full round trip, so a cold open waited on five daemon replies in a row.What changes
The first frame now waits on the handshake plus one round trip.
direct.ts). Afterinitializeanswers,_acpmux/status, the chat list and the chat's attach go out together. The permission-group and handoff reads go out with the attach rather than after it. acpmux serves a connection's requests side by side (server/mod.rsspawns each), so the frame waits on the slowest of them, not their sum.initializestill goes first: the app's bridge sends the person proof as the connection's second request and holds every other frame behind it.askEarly) is used only when the list then confirms it: the same chat and selection, a chat on this machine, and a page that holds nothing of the chat yet. In every other case it is dropped, and the chat attaches exactly as before. That covers another chat chosen, a tab-session rebind, a missing chat, and a chat on another machine (which may draw the Mac's copy first). A dropped attach of a chat no longer shown is detached once it has answered (acpmux runs each request on its own task, so an earlier detach could run first).copyLiveandkeptThroughreconciliation see the same sequence as before.createdAt). A reply of another session is retried, like a recreated row, and so is one whose row changed after it had streamed records (those may be of the session before it). A dropped early attach's held records are discarded by the next attach of the chat, whose page already holds them. The guards stay as they were: stale selection and generation checks,strayedFrom,historyExhausted, peer chats, andcopyAttaches._acpmux/attach). The reply now carriesturnOpener: the prompt that opened the turn the page starts inside. It is computed only when the page has older records and starts mid-turn, using the same 16-recorduser_messageread the pane used to make itself (server/requests/turn_opener.rs). Otherwise it isnull.turnOpenerout still gets the pane's own read. This is detected by whether the key is present, so a mixed fleet and a forwarded peer attach both work.spec/acpmux-protocol.jsonandprotocol/generated/types.tsare updated forAttachResult.turnOpener. The mock daemon answers the same way.No new timer, observer or loop. The early attach's live buffer is bounded by the records the chat streams between the handshake and the list's reply. It is cleared when the open takes or drops the attach, and in
open'sfinally. No new per-token work.Tests (red on base, green here)
webviews/src/agent-session/acpmux/chatOpenTrips.test.ts: a mock daemon holds every reply until the test answers what is out as one wave, and the test counts the waves before the first frame with rows.initializealone and then status, watch and attach together. Base takes 5._acpmux/eventsread.cmux-tui/crates/acpmux/tests/hub_integration/transcript_events.rsan_attach_page_names_the_prompt_of_the_turn_it_starts_inside: on pages starting inside the last turn,turnOpenerequals what the pane's own read finds, and it is null for a whole log or a page that starts with the prompt.Performance
chatOpenTrips.test.tschatOpenTrips.test.tscmux-ci test)The bench opens the mock chat with every daemon reply delayed by D ms: base pays about 5 x D before the first row and this head pays 2 x D (the handshake, then one wave). A fleet-Mac run of
scripts/measure/agent-engine.shwas refused at a PR ref (the script measures only refs on its list), so the Mac number comes from the nightly once this lands.test/agent-pane-peer-copy.test.ts's stand-in daemon runs its once-only scripted hooks (records during the attach, a recreation, refusals) on the attach the open asks of the chat as listed, and answers an attach asked before the list plainly; two tests count only the listed attaches.test/agent-pane-new-tab-cached-chat.test.tsnow lets the host's tail read land after the held attach is asked: the attach goes out with the list, before that read.Base reds: the
src/agent-session/acpmuxdirectory run fails the same 105 tests by name at base and here (the head fails none of its own; base also fails the new tests and one long-chat paging test this head passes).transcript_events::orchestration_fields_reach_subagent_events_and_session_summaryfails identically on base.Risk
Medium. This reorders the open handshake in
direct.ts. The guards are unchanged, and every case the early attach does not fit falls back to the old path.#27614(acpmux page compaction) touches the same attach handler a few lines away; the hunks do not overlap.🤖 Generated with Claude Code
Agent session: agent session