Skip to content

acpmux: a peer handshake survives a watch list too large to send; a peer link's watch lists only own sessions - #28105

Merged
teamleaderleo merged 5 commits into
feat-cmux-nextfrom
acpmux-peer-watch-too-large
Oct 30, 2026
Merged

teamleaderleo merged 5 commits into
feat-cmux-nextfrom
acpmux-peer-watch-too-large

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

What broke

A daemon's peer link never finished its handshake when the peer's session list was over the 15 MiB message limit. It failed every ~2 minutes with peer disconnected: handshake timed out after 90s and showed handshaking: true, error: null in host ls the whole time.

We hit this on 2026-10-30. The peer's _acpmux/watch {"withTools": true} reply listed 5241 sessions. Only 426 were the peer's own; the other 4815 were rows it mirrors from its own peers. With tools that came to 17,181,757 bytes, over MAX_MESSAGE_BYTES (15,728,640), so the peer answered -32000 message.too_large. A read-only probe against that daemon got the error in 2.4 s. The same watch without tools is 13,635,206 bytes and still fits.

On the client, the handshake task's link_request_on(MUX_WATCH, ...).await? ended the task without a word. The read loop never looked at the task, so it waited out the 90 s timings.handshake deadline, and the peer then logged Connection reset without closing handshake.

Fix

  • Client (peer.rs)
    • When the handshake's watch is refused with message.too_large, it asks again without tools. The link connects, and its rows lack tools, where before it never connected. This works against peers on older builds, so a client update alone unblocks the link without restarting the peer.
    • The read loop now polls the handshake task. When a step fails (refused, or failed on this link), the link ends at once with handshake failed: <the peer's error>, not a silent 90 s timeout.
  • Server (server/list_reply.rs)
    • Another daemon's link (view::a_link: served as Peer, or as Web while named acpmux-peer) now gets only this daemon's own sessions in its watch reply (localOnly).
    • The link already drops every mirrored row (peer.rs watch_sessions / is_mirrored_session), so nothing it uses goes away. On the daemon above that cuts the reply from 17 MB to about 1.1 MB.
    • Other clients (CLI, app, Web) still read every row.

Tests (red, then green on cmux-ci test)

Test commit 385deed is red; run https://github.com/manaflow-ai/cmux/actions/runs/41020853418:

  • peer::handshake_tests::a_watch_list_too_large_with_tools_is_read_without_them failed with Elapsed (no Connected within 5 s).
  • peer::handshake_tests::a_refused_handshake_step_ends_the_link_with_its_error failed with Elapsed (the link kept waiting).
  • server::list_reply::link_tests::a_peer_link_watch_lists_only_this_daemons_own_sessions failed with ["m1", "own-chat"] != ["own-chat"].

Fix commit 565ffb9 is green on all three, together with watch_order_tests and the rest of peer:: / list_reply (27 passed); run https://github.com/manaflow-ai/cmux/actions/runs/41021044500. A full cargo test -p acpmux and clippy -D warnings run follows in a comment.

Risk

Low to medium.

  • The watch retry only fires on message.too_large.
  • Ending the link on a failed handshake step changes how fast a broken link redials (at once, not after 90 s), and the existing redial backoff still applies.
  • A peer link's watch list no longer carries mirrored rows. Its client discarded them already, but a third-party client that names itself acpmux-peer would also stop seeing them.

Agent session: agent session

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Fixes peer handshakes that never completed when a peer's watch list exceeded the 15 MiB message limit — the client now retries the watch without tools, and a peer link's watch list now only carries the daemon's own sessions.

  • On a message.too_large refusal, the handshake asks for the watch again without tools so the link connects instead of silently timing out. This works against older peers, so a client-side update alone unblocks the link.
  • Any handshake that ends without a ready link now ends the link immediately with the reason — a refused step, a panicked task, or a link that dropped mid-handshake — instead of a silent 90 s timeout.
  • The server restricts a peer-token link's watch reply to this daemon's own sessions, built from those sessions alone; on the affected daemon the reply shrank from ~17 MB to ~1.1 MB.
  • All other clients keep reading every row; a client naming itself acpmux-peer only stays out of presence.

Written for commit ab1d1cf. Summary will update on new commits.

View guided diff Turn on auto-fix

teamleaderleo and others added 2 commits October 30, 2026 00:33
…ds on a refused step; a peer link's watch lists only own sessions

Red: huge-blue's link to big-red sat 90 s per try (2026-10-30) because big-red's
_acpmux/watch reply with tools (17 MB, 4815 of 5241 rows mirrored from its peers)
came back as message.too_large and the handshake hid the error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
…nd ends on a refused step; a peer link's watch lists only own sessions

huge-blue's link to big-red failed every ~2 min on 2026-10-30 with
'handshake timed out after 90s'. big-red's _acpmux/watch reply with tools was
17,181,757 bytes (5241 rows, 4815 of them mirrored from its peers), over
MAX_MESSAGE_BYTES (15 MiB), so it answered message.too_large; the handshake
task's '?' ended it silently and the read loop waited out the deadline.

- peer.rs: on message.too_large the handshake asks for the watch again
  without tools (13.6 MB on big-red today), and a handshake task that ends
  with an error ends the link at once with that error.
- server/list_reply.rs: another daemon's link (view::a_link) reads only this
  daemon's own sessions in its watch reply (localOnly); it drops mirrored rows
  anyway (peer.rs watch_sessions).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Owned by an agent session · session

@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review ab1d1cf: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr28105 -f ref=acpmux-peer-watch-too-large -f platform=macos -f backend_mode=local

red: cmux-next checks (god files, concurrency, crash safety, l10n); NEW: red check: No literal state roots (per-build isolation S7, cx-is1kf.8) is not red on the base 9e836df; do not merge until green; see lane-rules

@github-actions github-actions Bot added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Oct 30, 2026
@blacksmith-sh

blacksmith-sh Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Found 22 test failures on Blacksmith runners:

Failures

Test View Logs
test_every_linux_nightly_job_takes_a_runner_set_member (main.NightlyTrustedRouting.
test_every_linux_nightly_job_takes_a_runner_set_member) (job='attest-nightly-remote-dae
mon', runner='', primary='failure')/
test_every_linux_nightly_job_takes_a_runner_set_member (main.NightlyTrustedRouting.
test_every_linux_nightly_job_takes_a_runner_set_member) (job='attest-nightly-remote-dae
mon', runner='', primary='failure')
View Logs
test_every_linux_nightly_job_takes_a_runner_set_member (main.NightlyTrustedRouting.
test_every_linux_nightly_job_takes_a_runner_set_member) (job='attest-nightly-remote-dae
mon', runner='', primary='success')/
test_every_linux_nightly_job_takes_a_runner_set_member (main.NightlyTrustedRouting.
test_every_linux_nightly_job_takes_a_runner_set_member) (job='attest-nightly-remote-dae
mon', runner='', primary='success')
View Logs
test_every_linux_nightly_job_takes_a_runner_set_member (main.NightlyTrustedRouting.
test_every_linux_nightly_job_takes_a_runner_set_member) (job='attest-nightly-remote-dae
mon', runner='blacksmith-16vcpu-ubuntu-2404', primary='failure')/
test_every_linux_nightly_job_takes_a_runner_set_member (main.NightlyTrustedRouting.
test_every_linux_nightly_job_takes_a_runner_set_member) (job='attest-nightly-remote-dae
mon', runner='blacksmith-16vcpu-ubuntu-2404', primary='failure')
View Logs
test_every_linux_nightly_job_takes_a_runner_set_member (main.NightlyTrustedRouting.
test_every_linux_nightly_job_takes_a_runner_set_member) (job='attest-nightly-remote-dae
mon', runner='blacksmith-16vcpu-ubuntu-2404', primary='success')/
test_every_linux_nightly_job_takes_a_runner_set_member (main.NightlyTrustedRouting.
test_every_linux_nightly_job_takes_a_runner_set_member) (job='attest-nightly-remote-dae
mon', runner='blacksmith-16vcpu-ubuntu-2404', primary='success')
View Logs
test_every_linux_nightly_job_takes_a_runner_set_member (main.NightlyTrustedRouting.
test_every_linux_nightly_job_takes_a_runner_set_member) (job='attest-nightly-remote-dae
mon', runner='blacksmith-32vcpu-ubuntu-2404', primary='failure')/
test_every_linux_nightly_job_takes_a_runner_set_member (main.NightlyTrustedRouting.
test_every_linux_nightly_job_takes_a_runner_set_member) (job='attest-nightly-remote-dae
mon', runner='blacksmith-32vcpu-ubuntu-2404', primary='failure')
View Logs
test_every_linux_nightly_job_takes_a_runner_set_member (main.NightlyTrustedRouting.
test_every_linux_nightly_job_takes_a_runner_set_member) (job='attest-nightly-remote-dae
mon', runner='blacksmith-32vcpu-ubuntu-2404', primary='success')/
test_every_linux_nightly_job_takes_a_runner_set_member (main.NightlyTrustedRouting.
test_every_linux_nightly_job_takes_a_runner_set_member) (job='attest-nightly-remote-dae
mon', runner='blacksmith-32vcpu-ubuntu-2404', primary='success')
View Logs
test_every_linux_nightly_job_takes_a_runner_set_member (main.NightlyTrustedRouting.
test_every_linux_nightly_job_takes_a_runner_set_member) (job='attest-nightly-remote-dae
mon', runner='blacksmith-4vcpu-ubuntu-2404', primary='failure')/
test_every_linux_nightly_job_takes_a_runner_set_member (main.NightlyTrustedRouting.
test_every_linux_nightly_job_takes_a_runner_set_member) (job='attest-nightly-remote-dae
mon', runner='blacksmith-4vcpu-ubuntu-2404', primary='failure')
View Logs
test_every_linux_nightly_job_takes_a_runner_set_member (main.NightlyTrustedRouting.
test_every_linux_nightly_job_takes_a_runner_set_member) (job='attest-nightly-remote-dae
mon', runner='blacksmith-4vcpu-ubuntu-2404', primary='success')/
test_every_linux_nightly_job_takes_a_runner_set_member (main.NightlyTrustedRouting.
test_every_linux_nightly_job_takes_a_runner_set_member) (job='attest-nightly-remote-dae
mon', runner='blacksmith-4vcpu-ubuntu-2404', primary='success')
View Logs
test_every_linux_nightly_job_takes_a_runner_set_member (main.NightlyTrustedRouting.
test_every_linux_nightly_job_takes_a_runner_set_member) (job='attest-nightly-remote-dae
mon', runner='ubuntu-24.04', primary='failure')/
test_every_linux_nightly_job_takes_a_runner_set_member (main.NightlyTrustedRouting.
test_every_linux_nightly_job_takes_a_runner_set_member) (job='attest-nightly-remote-dae
mon', runner='ubuntu-24.04', primary='failure')
View Logs
test_every_linux_nightly_job_takes_a_runner_set_member (main.NightlyTrustedRouting.
test_every_linux_nightly_job_takes_a_runner_set_member) (job='attest-nightly-remote-dae
mon', runner='ubuntu-24.04', primary='success')/
test_every_linux_nightly_job_takes_a_runner_set_member (main.NightlyTrustedRouting.
test_every_linux_nightly_job_takes_a_runner_set_member) (job='attest-nightly-remote-dae
mon', runner='ubuntu-24.04', primary='success')
View Logs

...and 12 more test failures. View all on Blacksmith

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

…ns, built without the whole list

- The own-sessions watch keys on Origin::Peer (the peer token proved), not on
  view::a_link: a client that names itself acpmux-peer only stays out of
  presence and still reads every row.
- Its rows are built from this daemon's own sessions alone (Snapshot::own_whole,
  Hub::visit_own_session_summaries), once per snapshot (made), not by parsing
  the whole tools-expanded list and filtering it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
teamleaderleo added a commit that referenced this pull request Oct 30, 2026
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
teamleaderleo and others added 2 commits October 30, 2026 02:16
…ds the link

A panicked handshake task, or one that returned Ok after its link's epoch moved
on, was ignored and the link waited out the 90 s deadline. Every completion
that leaves the link not ready now ends it with why; the stale-token reason
is still the one given when that path ended it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Evidence at head ab1d1cf:

  • Review. Round 1 (Astra) returned BLOCK: the filter keyed on a_link, and the tools path parsed the whole list. Fixed in 6d36777. Round 2 ran on gpt-6-sol because the Astra lane hadn't produced a verdict after more than 15 minutes. Sol returned BLOCK: the JoinHandle arm missed the panic and Ok(())-without-ready cases. Fixed in 44e6065 (test, red at that commit) and ab1d1cf. Round 3 (gpt-6-astra, high, fast, through review-router) returned SHIP at ab1d1cf.
  • Red/green.
    • The handshake tests and the link test are red at 385deed (run 41020853418) and green at the fix.
    • a_handshake_that_ends_without_a_ready_link_ends_the_link is red at 44e6065 and green at ab1d1cf.
  • Full cargo test -p acpmux --no-fail-fast plus clippy -D warnings at ab1d1cf. Clippy passes (exit 0). Three test reds, compared against base one by one:
    • hermetic_daemon_guard::every_test_binary_runs_in_the_hermetic_environment fails the same way on base 84705bd (tests/web_image_attachments.rs lacks the hermetic setup), so it is a base red.
    • agent_digest::agent_messages_a_failed_turn_held_go_in_after_the_next_completed_turn (notification timeout) and person_auto_start::auto_start_a_session_never_starts_one_in_another_mode_that_does_not_ask both pass 6/6 at this head and 6/6 on base. Both also passed in the full run at 6d36777. Neither opens a peer link, so they are load flakes and not caused by this PR.
  • Other CI reds.
    • cmux-next checks fails on check-state-roots, from cli/help_examples/session.rs:179 (acpmux: every command's --help ends with examples #28013, on base), and on string tables (CmuxHome.xcstrings).
    • release-notary and the Familiar warn job are also red.
    • This PR changes none of the files behind those reds; it touches only six files under cmux-tui/crates/acpmux/src.

@teamleaderleo
teamleaderleo merged commit dda9018 into feat-cmux-next Oct 30, 2026
181 of 194 checks passed
@teamleaderleo
teamleaderleo deleted the acpmux-peer-watch-too-large branch October 30, 2026 09:45
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

teamleaderleo added a commit that referenced this pull request Oct 30, 2026
…er (#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 8f1a1fd: 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>
teamleaderleo added a commit that referenced this pull request Oct 30, 2026
…ist sorts pages back and refuses a repeated cursor

Sol review of #28182:
- A no-limit _acpmux/sessions with tools or a filter went through the
  byte pager even when it fit, so it came back in hash order. bounded
  now answers a list that fits whole, newest first, as before paging,
  and pages only one too large for one message.
- The whole unfiltered list checked only its raw size, so a peer's
  masked copy could pass the message limit. It is paged when the text
  the client reads does not fit.
- Client::whole_list refuses a cursor it was given before and a page
  with no sessions, and sorts joined pages newest first, so acpmux ls
  and MCP ls list as before.
- acpmux ls (moved to cli/run/ls.rs on base) and fanout read the whole
  list with whole_list.

A peer link's whole watch is #28105's own-sessions reply, never paged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-c11d09c9f41988fc1d93
teamleaderleo added a commit that referenced this pull request Oct 30, 2026
* test(acpmux): a fleet-sized ACP session/list goes in pages under the message limit

Red on base: 5250 mirrored rows with tools are one 17 MB reply, refused as
message.too_large, as one daemon's 5241 were on 2026-10-30.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-c11d09c9f41988fc1d93

* acpmux: ACP session/list goes in pages when the whole list is too large to send

session/list answered every session, local and mirrored, with tools, in
one reply: one daemon's 5241 rows were 17 MB on 2026-10-30, over the
15 MiB message limit, so it was refused as message.too_large. A list
over 14 MiB now goes in pages of at most that, each with the nextCursor
ACP's client follows (the session list's keyset cursor, so a row that
moves between pages neither repeats nor skips another), and cursor is
read. A list that fits is one reply as before, with no nextCursor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-c11d09c9f41988fc1d93

* test(acpmux): a fleet-sized session list goes in pages under the message limit

Red on the base: _acpmux/sessions and _acpmux/watch with no limit answer
5250 mirrored rows in one reply, refused as message.too_large, with tools
or without, filtered or not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-c11d09c9f41988fc1d93

* acpmux: a session list too large for one message goes in pages

_acpmux/sessions and _acpmux/watch with no limit answered the whole list
in one reply, so on a daemon mirroring a fleet (one daemon's 5241 rows
were 17 MB with tools, 2026-10-30) it was refused as message.too_large,
with tools or without, filtered or not. Such a reply now carries the rows
that fit in 14 MiB and the nextCursor of the rest, read with
_acpmux/sessions {cursor} and the same filters (Snapshot::bounded, cut
from the shared serialized list). A list that fits is one reply as
before, with no cursor; a census in its own order (orderBy) still pages
by limit. Every caller in this crate (ls, pending, the TUI, orchestrate,
place, session move, the MCP ls) reads every page (Client::whole_list).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-c11d09c9f41988fc1d93

* acpmux: a session/list page that masking lengthens is cut again; an oversized row goes as a stub

Review of 669f622: a peer's page was sized before masking, which can
lengthen text; a row larger than a page alone was still sent, page and
all over the limit; and two rows with one id and updatedAt share a cursor,
so a one-row page of the second came forever. A masked page over the
budget is cut again at half the budget (down to 64 KiB), a row larger than
the page goes as a stub with its size (as an oversized record does,
hub/paging.rs), and a cursor the page cannot get past is refused
(list.cursor_stuck) instead of answered again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-c11d09c9f41988fc1d93

* acpmux: byte pages never repeat rows one cursor names; an oversized ACP row's stub keeps no large or masked text

byte_page ends a page before rows a cursor cannot tell apart (one id, one
updatedAt) or, when they are all it holds, after all of them, and a cursor
starts after every such row: each comes once, so list.cursor_stuck is gone.
The stub for a row larger than its page keeps only its id and cwd, each at
most 4 KiB, and no title, so it stays small whatever field was large and
carries no text a peer's reply masks.

Tests: byte_pages_read_rows_one_cursor_names_once (four budgets, the pair
on and across a page edge) and an_acp_row_larger_than_its_page_goes_as_a_stub
(a huge title and cwd); cargo test -p acpmux --lib passes them locally.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-c11d09c9f41988fc1d93

* acpmux: a session row larger than its page goes as a stub with its keys

Snapshot::bounded sends a row larger than the page alone as {sessionId, updatedAt,
peer, status, omitted {bytes, reason: too_large}}, as session/list and history pages
do, rather than a page too large to send.

Test: a_row_larger_than_its_page_goes_as_a_stub_with_its_keys (cargo test -p acpmux
--lib list_guard passes locally).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-c11d09c9f41988fc1d93

* acpmux: a byte page may end between rows one session cursor names

Rows with one id and one updatedAt are told apart by their order: a page
that ends on the n-th of them names it <cursor>.<n>, and the next page
starts after it. A page no longer has to hold the whole group, which could
pass the budget (two 7.5 MiB rows on one page).

Test: byte_pages_read_rows_one_cursor_names_once now checks that no page
over its budget holds more than one row (red on the group code at budget 5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-c11d09c9f41988fc1d93

* acpmux: a session cursor tells a chat's copies on peers apart

A chat here and its copies on peers share an id and an updatedAt. The
cursor told them apart only by their order, so a page ending on the
second of three skipped the third once the first left. The cursor key
now hashes the peer too, and ties sort by peer, so each copy has its
own cursor. This daemon's own rows keep the key they had.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-c11d09c9f41988fc1d93

* acpmux: a list snapshot orders a chat's copies by peer, as their cursors do

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-c11d09c9f41988fc1d93

* acpmux: page an oversized session/list by sessionId and peer, not newest first

A newest-first page cursor skipped an unread session that updated
between pages (it moved above the cursor), and an ordinal cursor for
rows listed twice could skip one when another left. Pages of an
oversized ACP session/list now go by (sessionId, peer), and the cursor
names that pair: an update or a leave moves no other row. A row listed
twice comes once. A list that fits one message is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-c11d09c9f41988fc1d93

* acpmux: an ACP list page's cursor is a 33-byte hash, and a page parses only its rows

The (sessionId, peer) cursor carried both ids whole, so a huge id made
a page plus its cursor larger than the message limit. Pages now go in
the order of a 128-bit hash of the pair, and the cursor is that hash.
A page cuts its rows from the snapshot's own spans, so it parses only
those rows, and a list whose rows alone are over the budget no longer
builds a whole reply only to drop it. Fleet test (5250 rows, 20.6 MB):
page 1 ~370 to ~183 ms, page 2 ~72 to ~41 ms (release).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-c11d09c9f41988fc1d93

* acpmux: a session list that fits goes whole and newest first; whole_list sorts pages back and refuses a repeated cursor

Sol review of #28182:
- A no-limit _acpmux/sessions with tools or a filter went through the
  byte pager even when it fit, so it came back in hash order. bounded
  now answers a list that fits whole, newest first, as before paging,
  and pages only one too large for one message.
- The whole unfiltered list checked only its raw size, so a peer's
  masked copy could pass the message limit. It is paged when the text
  the client reads does not fit.
- Client::whole_list refuses a cursor it was given before and a page
  with no sessions, and sorts joined pages newest first, so acpmux ls
  and MCP ls list as before.
- acpmux ls (moved to cli/run/ls.rs on base) and fanout read the whole
  list with whole_list.

A peer link's whole watch is #28105's own-sessions reply, never paged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-c11d09c9f41988fc1d93

* acpmux schema: the pages of an oversized session list go in hash order; whole_list sorts them back

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-c11d09c9f41988fc1d93

* acpmux: a peer link reads every page of its watch list; a session cursor's cutoff holds across byte pages

Sol review of #28182:
- A peer over plain ws:// to a non-loopback host is served as Web, not
  Peer, so it gets the whole list, paged when too large; the handshake
  published only the first page. It now reads every page with
  _acpmux/sessions {cursor} and publishes the whole list before
  Connected; a cursor given twice is an error, never a loop.
- A no-limit list read from a session cursor (k...) applied that
  cutoff to its first byte page only: the h cursor replaced it. A page
  of such a list now carries it on (h...~k...).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-c11d09c9f41988fc1d93

* test(acpmux): a paged watch list is published whole, after its last page, and a bad cursor ends it

Red: the first page is published at once (a link that drops midway leaves
the hub a partial list; a change sent while paging is overwritten by the
whole list after it), and a cursor that is not a string ends paging with a
short list, in the peer handshake and in Client::whole_list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-c11d09c9f41988fc1d93

* fix(acpmux): publish a paged watch list whole, after its last page, and refuse a bad cursor

The reader gathers the pages of the handshake's watch list (watch_paging
marks the _acpmux/sessions page requests) and publishes the list once,
then the session changes the peer sent meanwhile. A link that drops before
the last page publishes nothing, so the hub keeps its last whole list. A
nextCursor that is neither a string nor null ends the handshake, and
Client::whole_list refuses it, instead of a short list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-c11d09c9f41988fc1d93

* test(acpmux): a request of a link gone down takes no page of the next link's watch list

Red: a _acpmux/sessions admitted on an earlier link, sent while the next
link reads its watch list in pages, takes the page key (watch_paging is
peer-wide, and the key is set before the epoch check), so the last page is
never gathered and the link connects with no list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-c11d09c9f41988fc1d93

* fix(acpmux): only the handshake's own watch requests take the list key, on their own link

watch_request_on marks the handshake's _acpmux/watch and its page requests
explicitly, in place of the peer-wide watch_paging flag (which an aborted
handshake left set), and the key is set only after the request's epoch check,
with no await between, so a request of a link gone down never takes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-c11d09c9f41988fc1d93

* fix(acpmux): recheck a watch request's link under the watch key's lock

A page request of an aborted handshake may still run on another worker
after its epoch check. Checking the epoch again while holding the
watch_key lock orders it against the next handshake: the epoch moves on
(finish_link) before the next link's handshake sets its key under that
lock, so the old request either set its key first (and is overwritten)
or sees the new epoch and is refused. This is a preemption race on a
multithreaded runtime, so it has no deterministic test; the handshake
tests still pass.

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>
teamleaderleo added a commit that referenced this pull request Oct 31, 2026
…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…
teamleaderleo added a commit that referenced this pull request Nov 2, 2026
* ci guards: depth-limited fetches only in shallow checkouts (base red)

scripts/measure/fresh-home-launch.sh and workers/cmux-vm/scripts/test-mesh-label-compat.ts passed
--depth to a fetch in the caller's checkout, which turns a full clone shallow. Both now pass it only
when git rev-parse --is-shallow-repository says the checkout is already shallow.

Regressed by #30495 (fresh-home-launch.sh) and #30501 (test-mesh-label-compat.ts).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-fc739fd2f4e557ae7189

* ci guards: map the transcript's TurnRows.tsx in place of the deleted MessageGroupRow.tsx (base red)

#29800 removed MessageGroupRow.tsx (every sent message is its own card); TurnRows.tsx now draws
each sent message row in order (SentMessageLine.tsx is already mapped), so chat-transcript maps it.

Regressed by #29800 (deleted the file #28698 had mapped).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-fc739fd2f4e557ae7189

* ci guards: scan cmux-next-ios/verify/ for errno-after-assertion (base red)

cmux-next-ios/verify/imsg-replies/GestureServerUITests.swift is an XCTest source outside every
TEST_ROOTS entry. Scan cmux-next-ios/verify/ (it has no errno use, so nothing else changes).

Regressed by 201101a (added the verify/imsg-replies rig; landed by a safe-push train, no PR).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-fc739fd2f4e557ae7189

* ci guards: list the four manual-lane workflow readers in UNWIRED (base red)

tests/test_ci_blacksmith_linux_sizes.py, test_ci_overflow_pick_reads_pool.py,
test_nightly_next_control_rescue.py and test_ci_cloud_probe_rescue.py read .github/workflows/
but are lane = "manual" in tests/test-execution.toml (no new CI tests; wiring them as linux-guard
reddened tooling-tests on every PR). They join UNWIRED with the same reason as the other
manual-lane entries, and each says when to run it by hand.

Regressed by #30360, #30878, #30657 and #30796 (each added its test without an UNWIRED entry).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-fc739fd2f4e557ae7189

* ci guards: fleet rule lints pass again (base red)

"ci: Validate fleet rule lints" failed on feat-cmux-next in three rules.

big-red: the personal machine's name left product code and tests. Doc
comments say "one lane VM" / "<host>"; the webview tests use my-vm (its
machine hue still differs from gpu-box's).
  Regressed by #28105 (peer_handshake_tests.rs, list_reply.rs,
  list_reply_tests.rs), #29495 (host_update_tests.rs), #29126
  (chatOpenPacedList.test.ts), #31108 (project-chooser-machine-tag).

pkill: the iOS verify rigs and the tool-evals remote daemon stop only
what they started. imsg-replies records the conversation-sim, the
recorders (simctl itself, not the xcrun shim) and the gesture servers in
pidfiles and signals those PIDs after checking the command line still
matches; pq_browser_events records its two site servers' PIDs; the
tool-evals remote stop kills the setsid group whose PID the launcher
recorded beside the socket.
  Regressed by 201101a (imsg-replies, first in #22328), #25915
  (pq_browser_events.py), #29080 (tool-evals/session.mjs).

Allowlisted as safe, with reasons: scripts/ci/macos-stubs/Security.tbd
(a linker stub listing symbol names, no keychain read; #27603) and
tests/tool-evals/cases/hard.ndjson (an eval case forbidding pkill;
#28738).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-fc739fd2f4e557ae7189

* ci guards: determinize python tests flagged by test-determinism (base red)

Regressed by the commits introducing each assertion (see report).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-fc739fd2f4e557ae7189

* ci guards: nightly Xcode selection, unused anchors, daemon retention key (base red)

- Validate macOS jobs select a pinned Xcode: the nine flagged nightly.yml
  jobs already run select-ci-xcode.sh through the `- *select-xcode` step
  alias; the guard did not expand step aliases. It now resolves `- *name`
  to the `- &name` step, with self-tests for both an alias to the selector
  and an alias to another step. Regressed by #30471 (steps turned into
  aliases to keep nightly.yml under its size limit).
- Lint every workflow: drop the five sign-nightly-identity-legacy-* anchors
  left after the Blacksmith twin that aliased them was removed
  (a7b6a61, "legacy sign and deltas run on owned Macs only"; the twin
  came from #27357). No behaviour change.
- upload-artifact retention: the 14-day cmux-nightly-remote-daemon upload
  moved from build-nightly-app to build-nightly-remote-daemon in #25935
  with its retention unchanged; move the allowlist key with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-fc739fd2f4e557ae7189

* ci guards: media tours and visual-gate state match their validators (base red)

Drop the unknown step key "record" (re-added after #26888 removed it by #27933 and f283a2e), the unimplemented top-level frameInterval in home-chief-shimmer (c022070), the focusLeft right after closePane in palette-chat-toggles (#25405), split pane-polish (31 steps) into pane-polish and pane-polish-themes, and give the app-store-consent-deploy state a synthetic host that is not an email shape (4a05792).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-fc739fd2f4e557ae7189

* ci guards: allowlist remaining test-determinism findings with reasons (base red)

Swift/web/tool-evals findings cannot be determinized without Mac or webview runs; each entry names its reason and adding PR.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-fc739fd2f4e557ae7189

* ci guards: Linux pool for the push kicks; record pool readers as unrescued (base red)

- cmux-next-base-tier-kick, cmux-next-dev-kick and ios-internal-nightly-trigger waited on
  one fixed Blacksmith Linux size (!startsWith(LINUX_RUNNER) || overflow 8 || 2 vCPU). They
  now read fromJSON(vars.RUNNER_POOLS).linux after the fork branches. Their pinning tests
  (base tier, DEV kick, iOS internal builder) assert the pool read instead. Regressed by #30837.
- owned_pool_rescue.py: feat-cmux-next's UNRESCUED_WORKFLOW_PATHS records the secretless
  Linux pool readers the owned-pool sources test found unlisted (fast gates, gallery health,
  PR checks, cloud backend check, web validation, and the three kicks above), with the existing
  _POOL_READ_WHY. Regressed by #29844 and #31138.
- test_ci_linux_single_size.py: drop cmux-next-fast-gates.yml from FIXED_PAIR (moved by #31138);
  test_ci_overflow_pick_reads_pool.py: drop the two kicks from PICKS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-fc739fd2f4e557ae7189

* ci guards: signing-runner and retry-attempt guards follow the owned-Mac signing rule (base red)

Signing keychain guard (rule 7): accept owned-pool runner variables only where
safety is proven in the job itself:
- 7a: a job in glaeda's exclusive signing class whose first step is the
  verbatim claim gate may fall back to CI_NIGHTLY_NEXT_BUILD_RUNNER /
  CI_SIDE_LANE_RUNNER (nightly.yml sign-nightly-identity-legacy and
  generate-nightly-deltas, owned-only by design, Lawrence a7b6a61;
  deltas' Sparkle key added in #29261). test_ci_signing_owned_mac.py holds the
  claim script, always() keychain delete and twin/picker rules for them.
- 7b: stable-next.yml sign (#27898) reads every signing secret only under
  env.SIGN == 'developer-id', whose condition picks a literal Blacksmith label
  before any variable; its owned dry-run holds no key. Proven structurally.

Retry guard: cmux-next.yml swift-test/daemon-test/generated-files' side-lane
fallback (#29814) is now gated at run_attempt <= 3 for the bot, so the bot's
attempt 4 takes blacksmith-6vcpu-macos-26 like every other cmux-next fleet
fallback (#22565). The three owned-only nightly jobs are exempt while they keep
exactly their owned route. The textual check now forbids naming attempts after
2 (== 3+), as its comment says, instead of also == 2 (#29847 nightly.yml,
#29130 cmux-next-guards.yml name the bot's attempt 2, which may stay owned).

Regressed by #29814, #27898, #29261, #29847, #29130 and a7b6a61.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-fc739fd2f4e557ae7189

* ci guards: owned Mac pools through the picker/vars pattern; follow nightly step scripts for sentry-cli (base red)

check_no_self_hosted_fleet_runners
The guard told every workflow to "use a cloud label so required jobs never
land on a mini". That is outdated policy: owned fleet Macs now come first for
macOS jobs (Linux, then owned Macs through the picker, Blacksmith last),
release signing may run on owned Macs, display tests gate nothing, and the
NEXT nightly runs on the fleet with the nightly-only minis dedicated to it.
The guard kept up only through exact per-workflow selector strings, which
drifted with every routing change and turned the guard red on base:
- cmux-next.yml swift-test/daemon-test/generated-files: the glaeda-side
  deferred fallback (regressed by #29814) and the attempt-1 gui selector
  (#28605, #27853)
- cmux-next-base-autofix.yml / cmux-next-pr-autofix.yml: the gui selector
  gained a re-run predicate (#27853)
- nightly.yml sign/prepare jobs: owned pool as the final fleet fallback
  (#29154)

Fix the guard instead of moving those jobs to Blacksmith: an owned POOL label
(glaeda-[<lane>-]<class>-xcode-<version>, served by many minis, never one
host) is accepted as a quoted literal in a ${{ }} runs-on that routes through
the declared picker/vars pattern (reads needs.<job>.outputs.* or vars.*).
In a workflow a pull request can trigger, the fork pull request branch
(fork heads to Blacksmith) must still come first on the line, so fork code
never reaches an owned Mac (runner trust unchanged). Still refused: bare
self-hosted/macOS/ARM64, single-host labels (cmux-macos, mac-mini, slot-N,
tart-*, ...), macos-26 outside the fork branch, and an owned label named
directly with no picker or variable route. The exact gui selector strings are
replaced by this rule; self-tests cover each accept and refuse case.

check_sentry_cli_install_portability
#31013 moved nightly.yml's "Upload dSYMs to Sentry" body into
scripts/ci/nightly-steps/app-upload-dsyms.sh (size budget), so the guard no
longer saw the ./scripts/upload-sentry-dsyms.sh call. The guard now reads
the step's step script too. Regressed by #31013.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-fc739fd2f4e557ae7189

* ci guards: change-area tests follow the RUNNER_POOLS and devEngines routing; helper walk skips non-running namers (base red)

- release-admission and remote-daemon-admission read fromJSON(vars.RUNNER_POOLS).linux by
  design since #30878; the two tests now assert that instead of vars.LINUX_RUNNER.
- fast-checks.yml's setup-bun reads the bun pin from webviews/package.json devEngines at run
  time (#29757); the setup-bun test accepts a steps.<id>.outputs.version whose step reads
  devEngines, and still requires the literal pin everywhere else.
- indexnow.yml pinned bun 1.3.14 when #30697 backported it; it now installs devEngines' 1.4.4.
- ci_helper_areas() failed open on files that name a helper without running it, so
  owned_pool_rescue.py, owned_build_state.py and e2e_warm_derived_data.py stopped routing by
  where they run. Regressed by #28528 (test_cmux_next_route.py names owned_pool_rescue.py as a
  routing input and itself), #28513 (docs/ci/cmux-next-tiers.md names that test), #30657
  (nightly_next_control_rescue.py's only test is manual-lane), #28568 (.github/warm-sources.json
  prose) and #30467 (base_red_excuse.py's guard-test table and its test's fixtures). The walk
  now ignores a test naming itself, docs, a Python test naming it only in a docstring, the
  listed path tables and warm-sources.json, and treats a test the registry declares in an inert
  lane that nothing imports as a dead end. An unregistered test nothing names still fails open.
- cache_restore_receipt.py now routes precisely (macOS + Release, via ci-macos.yml's
  cache-restore action) instead of failing open; its assertion says so.

python3 tests/test_ci_change_areas.py: 4 of 234 failed before, 0 after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-fc739fd2f4e557ae7189

* ci guards: review fixes for #31366 (daemon pgid, fork-first owned pools, stale pidfiles)

- tests/tool-evals/session.mjs: the remote launcher recorded $! of the backgrounded && chain
  (the wrapping subshell), so the stop signalled a group that did not exist. setsid now starts
  a shell that writes its own PID and execs the daemon, so the recorded PID is the daemon's
  process group. Checked locally: the recorded PID equals the daemon's PGID and SID, and
  kill -TERM -- -PID stops it.
- tests/test_ci_self_hosted_guard.sh (security): the owned-pool exemption in pull request
  workflows only required the fork branch somewhere before the owned literal, so
  `needs.x.outputs.runner || <fork branch> || 'glaeda-...'` passed. The fork branch must now be
  the first selector of the expression (after only the hosted repository_owner clause). New
  negative self-tests for reordered expressions; red with the old match, green with the new.
- cmux-next-ios/verify/parity/pq_browser_events.py: stop_site() now stops only servers this run
  started (serve() reports each pidfile it wrote), kills a PID only while its command line is
  still that server's, and removes the pidfile after. Checked with a local stub harness.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-fc739fd2f4e557ae7189

* ci guards: universal nightly helper runs-on match works under mawk (base red)

The helper runs-on expected string passed to awk -v carried `\&\&`, which gawk reads as `&&`
(with a warning) but mawk, the default awk on Ubuntu runners, keeps as `\&\&`, so the whole
expression never matched and "nightly must build and hand off the macOS 26 helper" failed.
Plain `&&` means the same in both. Regressed by #30824.

bash tests/test_nightly_universal_build.sh: FAIL before, PASS after (mawk 1.3.4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-fc739fd2f4e557ae7189

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

This branch was previously deployed

3 inactive deployments
artifacts — ab1d1cf5 Deployed Oct 30, 2026 by teamleaderleo via add the Windows daemon to the tree #82049
sentry-dsyms — ab1d1cf5 Deployed Oct 30, 2026 by teamleaderleo via upload cmux-tui dSYMs to Sentry #82049
ci-routing-agent — ab1d1cf5 Deployed Oct 30, 2026 by teamleaderleo via cmux-tui tree preflight #82049
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant