Skip to content

acpmux: no-ask start modes by inherit or an approved preset; pending prompts reach peers - #20653

Closed
teamleaderleo wants to merge 247 commits into
feat-cmux-nextfrom
leo-acpmux-auto-inherit
Closed

teamleaderleo wants to merge 247 commits into
feat-cmux-nextfrom
leo-acpmux-auto-inherit

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

acpmux: approval paths for no-ask start modes that don't need the person each time, plus pending prompts that reach peered apps.

When a script or agent asked acpmux for a mode that doesn't ask, the request needed the person's approval. That approval was only visible in cmux NEXT on the same host, and the error didn't say where to give it. The threat model is written at the top of hub/person.rs.

  • (a) Inherit. A session can start new sessions in its own current no-ask mode, on the same harness family.
    • The daemon proves which session is calling. Each harness it starts gets ACPMUX_SESSION_TOKEN, an HMAC of the session id keyed by a secret made at each daemon start and kept only in memory.
    • The token is checked on the connection's first initialize, and only over the unix socket.
    • The caller's mode is read at request time, so a session the person moved back to asking inherits nothing.
    • Peer and Web connections never have a caller.
  • (b) Person-approved preset. _acpmux/presets {name, approveMode: true} from the person records a sha256 of the preset's contents.
    • The approval holds only while the preset still hashes to it, and only for that preset's own mode.
    • Any edit to the preset drops it.
    • Without the person, approving a preset's no-ask mode is refused.
  • (c) Pending prompts across peers. A peer's _acpmux/permission_pending reaches this daemon's watchers that did not attach the session (via: "peer").
    • Answers still go to the peer, which counts an allow only with its own person's proof.
  • (d) Refusal text. The error names where to approve: cmux NEXT on this host, or the preset to approve there once.
    • error.data carries {reason, host, mode, preset} so an app can show localized text.
  • Fits with the base start mode (cx-wx93i).
    • A request that names a mode skips the default start mode, and (a) and (b) gate it.
    • A proven caller session that asks never gets the full-access default for its chats: they start in the harness's own mode.

Review fixes

From the security review:

  1. Adopt plus a start mode. A start mode that does not ask, on a session/new that adopts, now needs the person. The request is refused before anything starts, because an adopt may find a session that is already running (ifLive: "open"), and only the person widens a running session.
  2. Peer-relayed pending prompts. A peer's _acpmux/permission_pending is relayed only when all of these hold:
    • the session is one that peer listed as its own, and it is not a session of this daemon;
    • the prompt is in the daemon run the peer named at its handshake;
    • that run is neither this daemon's nor one another peer named.
      It goes only to unix-socket and app watchers, never to a Web client and never on to another peer daemon.
    • Content binding (decision): not done. The decision proof covers the prompt's ids, not its text, and binding it to the text needs the app to hash what it shows. The checks above keep this daemon's own prompts, which the app on this host can prove, from arriving with a peer's text. The residual case is in the threat model.
  3. Preset approval only gated the start. One session/new reads the preset's contents at three points: the request check; the resolve of model, effort and env; and the spawn of args, env and system prompt.
    • The resolve and the spawn each record the digest of what they used.
    • The start counts the approval only if both digests equal the approved one. An edit that is put back between reads gains nothing.
    • A preset with a mode or an approval never takes a pooled harness.
    • The session records the approved digest. A respawn from other contents starts in the harness's own mode instead of replaying the approved one.
  4. Harness name. Documented: the approval covers the preset's harness string, not the profile that name resolves to.
  5. Overrides (decision): a request that adds its own model, effort, policy or env to an approved preset needs the person. A folder (cwd) is fine.
  6. modeApproval. Documented: it is an unkeyed sha256, so same-uid writes to config.json are out of scope.
  7. Host name. A Web or peer client never gets this host's name, in the refusal text or in error.data.host.
  8. Caller token. The daemon also sets ACPMUX_SESSION_TOKEN_SOCKET. The CLI sends the caller token only when it connects to that socket.

Out of scope, and pre-existing on the base: a peer can send session/update for a local session id. Tracked as bead cx-r492l.

Second review

  1. A request during the start rebound the approval. The spawn digest was written before spawn_lock, so a request on the session while an approved preset was starting could overwrite it. ensure_child now records the digest it spawned with, under spawn_lock, and only when it starts a new harness. A live or re-adopted child never writes it.
  2. The saved mode was cleared while the harness kept its no-ask mode. The saved mode is now dropped only when a new harness actually starts from other contents. For a live or re-adopted child, the saved mode still says what the harness runs.
  3. A peer can name a third daemon's run. Documented as a residual in person.rs and below in open questions. Tracked as bead cx-i4rte.
  4. ACPMUX_SOCKET is made absolute, so a relative path cannot point the CLI and the daemon at different sockets.
  5. Forgetting a mode now clears every mode field (MODE_FIELDS), not only modeId.

Open questions for review

  • Unproven unix-socket clients. With the base default start mode, any unix-socket client that is not a proven session gets a full-access chat without the person. That includes an agent in an asking mode that just leaves its token out. Applying the default only to the person or app connections would close that, but it changes base behavior, so it is Lawrence's call. Not changed here.
  • Readable token. A caller token can be read by any process of the same user (/proc/<pid>/environ, ps -E). It grants only that session's current mode, which its own agent can already use. Binding the token to the connecting process's ancestry is later hardening.
  • No cross-peer inherit.
  • Binding a decision proof to the prompt text would need the app to hash the text it shows (see Review fixes, 2).
  • A peer naming a third daemon's run. A peer can name, as its own run, the personInstance of another daemon the app enrolled directly. The app must refuse to prove a via: "peer" prompt whose personInstance is a daemon it enrolled directly. This needs an app change (bead cx-i4rte).
  • Restoring a preset restores its approval. Reverting a preset to the exact contents the person approved makes the approval valid again.

Not in this PR

  • The cmux NEXT notification for a peer-forwarded pending prompt.
  • Live verification that a pending prompt shows in the pane on another Mac.

Tests

The tests are at the daemon boundary: tests/person_auto_start.rs (auto_start_*) and a peer test in tests/peer_integration.rs for the pending prompt (c).

  • Red on 4814e6a (auto_start_): left: "full-access", right: "normal". A chat started by an agent in an asking mode came up in full access.
  • Hosted Linux green on f04c419 for auto_start_, peer, start_mode and preset.
  • Review fixes, boundary tests committed red first (d3ba5e7):
    • auto_start_, red on Linux: 3 failed, 9 passed. An adopt moved a running session to bypass, a request model override started without the person, and a respawned edited preset came back in bypass.
    • peer_pending, red on Linux: 3 failed, 1 passed (the legit relay). A forged prompt (local session, this daemon's run, unknown session) reached the watcher.
  • Green on Linux at 0f34db0:
    • auto_start_: 12 passed (run 38592681245).
    • peer: 75 passed (run 38600221583).
    • preset: 26 passed (run 38601708940).
  • Second review, boundary test committed red first (14f2ddb): auto_start_a_request_during_the_start_never_rebinds_the_approval, red on Linux (run 38611185844). The start was refused with permission.person_required.
  • Green on Linux at 68914f2:
    • auto_start_: 13 passed (run 38618842274).
    • peer: 75 passed (run 38626137238).
    • preset: 26 passed (run 38635193026).
  • In the 68914f2 runs, macOS failed on a base clippy lint in cmux-tui/tests/replace_unresponsive_owner.rs (unused qualification), which this PR does not touch.
  • After merging base (915952c, which picks up the base clippy fix from cmux-tui: clear the base clippy red in replace_unresponsive_owner #20654), hosted green on Linux and macOS:
    • auto_start_: Linux 19 passed, macOS 44 passed (run 38640660645).
    • peer: Linux 77 passed, macOS 101 passed (run 38643120684).
    • preset: Linux 26 passed (Linux job rerun after a cancel), macOS 51 passed (run 38646576474).
  • macOS was queued because the pool is starved, so these runs were cancelled after their Linux test job.

Changelog

  • Changed: acpmux sessions can start sessions in their own no-ask mode, the person can approve a preset's mode once, and a pending approval reaches peered apps. The refusal names where to approve.

🤖 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

Adds approval paths for starting acpmux sessions in a no-ask mode without the person, relays pending approval prompts to peered apps, and keeps the motion system and Chief work the base merge brought in.

  • Inherit. A proven caller session (an HMAC token the daemon places in its env, checked at the connection's first initialize) can start sessions in its own current no-ask mode on the same harness family; claims in request params are ignored. The CLI sends its caller token only to the socket of the issuing daemon (ACPMUX_SESSION_TOKEN_SOCKET), and ACPMUX_SOCKET is made absolute.
  • Approved preset. The person can approve a preset's no-ask mode once; the approval binds to the preset's contents via sha256, so any edit drops it. A request that adds its own model, effort, policy or env to an approved preset still needs the person.
  • Pending prompts across peers. A peer's pending prompt reaches this daemon's watchers that didn't attach the session; answers still go to the peer and need its own person's proof.
  • Refusals. Name where to approve — the host's cmux NEXT or a preset — with host, mode and preset in error.data.

Security boundaries

  • A no-ask mode never applies to an adopt, and approved-preset starts never take pooled harnesses; the approval counts only when both the resolved and spawned contents hash to the approved digest, recorded only on a cold start under the spawn lock so a racing request can't rebind it.
  • The default full-access start mode never widens a caller session that asks.
  • A peer's pending prompt is relayed only for a session the peer listed as its own, in the run the peer named at its handshake, to unix-socket and app watchers only.

Rode in from the base merge

  • Chrome motion (hq-1d): closed-form springs on one MotionClock per window, ui.animations.newTab (on by default) and ui.animations.newWorkspace, bounce and Reduce Motion switches; a new tab grows in at its slot on the tabOpen spring, structural changes snap under Reduce Motion. Palette rows for "Window Style" preview the style live while highlighted.
  • Chief: chief.profile.adopt makes an existing agent participant a Chief record under its own id (refuses a bound participant, revokes its earlier credentials); a brain started by the supervisor gets the account's home; every Chief profile page call answers within a deadline on a relay connection to the current owner. labs.classicHome keeps classic Home one release; the agent CLI card's harness.update/harness.restart run on this Mac's daemon with a gesture.

Written for commit 7f2b6a3. Summary will update on new commits.

View guided diff Turn on auto-fix

lawrencecchen and others added 30 commits October 13, 2026 20:28
… 5xx (#20412)

The 0.65.2 RC publish (run 38501752257) failed in the release notes: a new
RC line summarizes ~2,000 commits since the lines' merge base, and GitHub
answered a 25-commit association query with HTTP 502. Locally the same range
completes with 10 commits per query and 5xx retries (1,328 PRs, ~19 min).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…oads (#20259)

* test(cmux-tui): reproduce checkpoint capture on oversized kitty upload

* fix(cmux-tui): recover checkpoints from unreplayable terminal input

* test(cmux-tui): cover safe checkpoint recovery boundaries

* fix(cmux-tui): fence checkpoint replay recovery

* fix(cmux-tui): reset metadata on checkpoint abort

* fix(cmux-tui): reset metadata on checkpoint abort

* cmux-tui: refuse local replay recovery for hosted terminals

* cmux-tui: journal synthetic replay recovery bytes

* cmux-tui: apply hosted rustfmt to checkpoint recovery

* test(cmux-tui): keep checkpoint recovery attachment test bounded

* cmux-tui: fix journal recovery regression test

* fix(ghostty-vt): recover oversized Kitty replay

* fix(cmux-tui): detect Kitty replay overflow

* fix(ghostty-vt): reject completed Kitty overflow recovery

* test(cmux-tui): avoid replay size assumption

* test(cmux-tui): tolerate merged checkpoint output frames

* style(cmux-tui): format checkpoint attachment assertion

* fix(cmux-tui): satisfy checkpoint test lint

* test(cmux-tui): attach recovery assertion to live surface

* fix(ghostty-vt): reject recovery after prior Kitty overflow

* test(ghostty-vt): cover recovery after lost multipart prefix

* test(ghostty-vt): import replay error for overflow regression

* fix(ghostty-vt): fail closed after lost Kitty prefix

* test(cmux-tui): cover checkpoint recovery concurrency cuts

* style(cmux-tui): format recovery blob lookup

* fix(cmux-tui): preserve journal order across checkpoint recovery

* test(cmux-tui): cover contextual checkpoint retry errors

* fix(cmux-tui): retry contextual checkpoint ingress races
* test: cover Claude transcript cwd selection

* fix: resume Claude from transcript project cwd

* test: cover reused legacy agent PIDs

* fix: validate legacy agent PIDs by process identity

* fix: unwrap candidate resume directories

* fix: avoid ambiguous resume candidate closure

* test: cover Claude resume review regressions

* fix: address Claude resume review findings

* fix: use canonical Claude project slugs in restore

* test: reproduce process exit after restore identity capture

* test: avoid redundant process probes during restore

* fix: prioritize absent PIDs and probe presence only when needed

* test: retain the mismatched PID reclamation safety probe
* test: allow shared team members through Cloud gate

* fix: let accepted team members enable Cloud

* test: keep personal team behind Cloud upgrade gate

* fix: keep personal Cloud scope behind Pro gate

* fix: explain shared team Cloud access
…1NN hangs (#20238)

* Bump Bonsplit: stop per-render tab-strip work behind hang reports

Pins manaflow-ai/bonsplit#287 (663bc41): context-menu state is built when a
menu opens, the hint monitor no longer reads user defaults on every tab-bar
init, and the whole-tab hint animation is gone.

Sentry: CMUXTERM-MACOS-41NN, -2QV2, -2QAN, -2SCZ, -2PRX, -26QQ.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Bump Bonsplit: resolve the hint symbol when the monitor starts

Pins manaflow-ai/bonsplit#287 review follow-up (9d90480).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Diagnose sustained tab-bar render storms

* Tighten tab-bar render storm diagnostics

* Name tab-bar diagnostics as render windows

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* test: reproduce custom upload copy into missing directory

* fix: keep custom upload paths standalone

* fix: preserve custom upload extension names

* test: cover bounded custom upload extensions

* fix: bound custom upload remote extensions

* fix: append bounded extension scalars
…ast the budget (#20435)

* test(cloud diagnostics): ingest refusals name their cause and keep failures and drop reports

Red on main at the real handler with Postgres: the byte-budget and ingress
429s carry no refusal code, routine detail that fills the minute refuses a
failure batch, and a sampled-out poll that reports queue drops is discarded.

* Name every Cloud diagnostics 429 and keep failures and drop reports past the budget

Production 429s on POST /api/observability/cloud all come from the
shared cmux-analytics firewall rule, which is keyed by connection IP;
the route span could not say so. The route span now records the refusal
cause (ingress_rate_limited with the rule ID, account_byte_budget,
event_conflict, storage_unavailable), the keyed account hash and the
install ID.

Batches with a failure, timeout, cancellation or sign-in record get a
128 KiB reserve above the 256 KiB routine minute budget, so routine
success detail cannot refuse them. A sampled-out poll span that carries
the client's dropped count is now stored, since it is the only record
of that loss.

* Count every Cloud diagnostics refusal despite head sampling; keep evidence from over-budget batches

Review follow-up. The route span is head-sampled at 2%, so a refusal now
also emits one always-kept cmux.cloud_diagnostics.refusal span with the
cause, firewall rule, account key, install ID and counts. The firewall
rule travels as the ingress refusal value instead of a span side effect.

The byte budget no longer lets routine spans spend the reserve: a batch
over the routine budget stores only its failure, timeout, cancellation and
sign-in records within the reserve, commits them, and still answers 429,
so the client's retry deduplicates them. The install ID is labeled
cmux.client.instance_id so it cannot be confused with the server's
service.instance.id. Sampling docs name the drop-report exception.

* Coalesce repeatable Cloud diagnostics refusal spans; drain evidence kept from refused batches

Second review follow-up. A firewall refusal runs before authentication, so
an always-kept span per refusal let an anonymous flood become kept Axiom
spans. Firewall and malformed-body refusals now emit at most one span per
cause, rule and account per 10 s on each instance, with
cmux.telemetry.refusal_count. Budget, conflict and storage refusals stay
one span each. Evidence stored from a budget-refused batch schedules a
drain, and the refusal reports it as cmux.telemetry.stored_spans instead
of accepted_spans.
* test(cloud): reject stale terminal projection

* fix(cloud): recover wedged workspace links

Drop timed-out local links, avoid reprojecting stale terminal inventory, and restart a live but unresponsive guest daemon after bounded health failures. Preserve the remote registry and terminal hosts for the next connection.

* cmux-tui: test that registry reopen cost ignores terminal output history

Reopening a registry reads every page of the file today: an unscoped
PRAGMA quick_check and an unconditional journal_event_index backfill.
A production machine with 3.6M terminal.output records took ~3 minutes
of single-threaded CPU to start. This test measures reopen page-cache
misses after 16 and 16Ki output records and fails on main.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* cmux-tui: bound daemon startup by recent work, not journal size

A production registry held 3.6M terminal.output records (12 GB) and the
daemon took ~3 minutes of single-threaded CPU to start. Three open-time
steps read the whole journal:

- create_session_journal_schema re-inserted every journal row into
  journal_event_index on every open (twice). Appends write the index in
  the same transaction, so a full backfill runs only when the index table
  is new; an existing index repairs only rows past its highest sequence.
- PRAGMA quick_check read every page of the file. The check now runs per
  registry state table and its indexes, skipping the journal history
  tables (session_journal, its subject and event indexes, segments,
  checkpoints, content blobs), whose records are validated on read.
- Agent-roster replay decoded and hashed every terminal.output chunk
  since the roster cursor, at startup and under the registry lock on the
  next agent event. The roster ignores terminal output (terminal-runtime
  producer, fixed format), so replay reads only the other records through
  two (kind, sequence) index ranges and then stands on the target
  sequence. Reducer semantics are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cloud): recover wedged cmux-tui links safely

* fix(cloud): format journal archive fast path

* fix(cloud): use imported journal kind set

* test(cloud): expose incomplete canary rollback

* fix cloud workspace recovery review findings

* test(cloud): exercise stale restored pane recovery

* style: format filtered journal query

* fix(cmux-tui): skip archived terminal output during roster replay

* test(cloud): expose helper failure after canary stop

* fix(cloud): harden link teardown and journal replay

* style(cmux-tui): apply hosted rustfmt

* test(cloud): expect requestTimedOut for persistent request deadlines

The persistent resource connection now reports an expired request budget
as LinkError.requestTimedOut. Add the case to the isolated deadline
fixture's LinkError stub and update the deadline tests to expect it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix cloud recovery review findings

* chore(cloud): promote devbox bake with daemon health watchdog

Records the verified Freestyle devbox ladder from cloud-vm-image-contract
run 38514254364, baked from bc4c6b1 with this branch's cmux-devbox-boot
health watchdog and coreutils change, and demotes the previous defaults.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(cloud): promote verified devbox image

---------

Co-authored-by: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test: reproduce Claude helper reopening a stopped turn

* fix: ignore Claude helper tools after turn completion

* fix: persist Claude turn settlement across notifications

* fix: persist Claude stop transitions
* test(coderouter): cover Claude 408 and 409 failover

* fix(coderouter): fail over Claude timeout responses
* test(cloud): cover unresponsive VM status and pause probe

* fix(cloud): surface wedged guests and bound pause

* fix(cloud): bound Go pause preparation

* test(cloud): align stats deadline expectation

* fix(cloud): preserve pause probe timeout

* docs(cloud): cover workflow test helpers
* test: cover control-created surface startup lifecycle

* fix: keep control-created surfaces alive until input

* fix: cover startup inheritance for control splits

* fix: suppress startup replay for auxiliary control terminals

Pane-swap placeholders and mobile-created terminals are automation-owned surfaces too, so they must not replay a source surface's one-shot startup payload.

Co-authored-by: Austin Wang <austinwang115@gmail.com>
(cherry picked from commit 73e5bd2acb5ee0f3f7aefa55ca50e48ebcb7c85c)
* test(opencode): cover restore event before TUI route

* fix(opencode): retry restore binding after TUI startup

* fix(opencode): wait for TUI ownership changes

* fix(opencode): flush queued starts at event boundaries

* fix(opencode): cancel queued starts before session end

* fix(opencode): observe reactive TUI readiness

* test(opencode): exercise real reactive readiness and disposal

* fix(opencode): clear queued startup expiry on disposal
* Keep a stale Codex record from resuming the current binding

#20246 made a restore treat the agent as running when the session index
was not authoritative, so a cached quit index could not mark a live session
stopped. It read authority from isComplete(), which for Codex is also false
whenever the panel's recorded checkpoint fails verification. A fresh index
holding only a rejected stale record then left wasAgentRunning nil, and
restore replayed the dead current binding's resume command.

Base authority on whether the kind's hook store was observed, which is what
#20246 meant; per-panel checkpoint gaps no longer count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fix the Muse resume and minimum-contrast render tests

The Muse vault test expected an unquoted resume command, but registered
agents have quoted each argument and prefixed a cd since August; assert the
exact command as the sibling vault tests do.

The minimum-contrast test never rendered on Blacksmith runners: the
app-host process is not the active app there, so its window is never key
and never reported visible, and the renderer stays released. Let it opt
into a window that reports on-screen occlusion, as the simulator theme
tests already do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fix four intermittent main CI failures

- cmuxd-remote: a failed async attach write queued its error and then
  cancelled the connection, and dispatch could observe the cancellation
  first and return context.Canceled. Report the queued error instead.
- ws_pty_test: macOS sh can print a job-control warning between the SIZE
  marker and stty's output; write both with one printf.
- Repeated pastes: four jobs share one FIFO whose deadline starts at
  admission, so a slow runner expired the fourth. The test checks payload
  fidelity, so its deadline ends only on cancellation.
- Unread sidebar test: allow WorkspaceContentView re-evaluations caused by
  unrelated Workspace publishes in the measured window, counted directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Name what keeps a closed window's manager alive during a Cloud create

This test fails on every main run since Cloud was enabled for it (#19988),
and reading the code did not find the owner. Report whether the shared
controller or app state holds it, and whether it is freed after the create
completes or after those are reset, so the next run names the holder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep never-inspected Codex evidence non-authoritative at quit

Review follow-up: the previous commit made every per-panel Codex gap
authoritative, including records outside the bounded verification pass or
whose verification was unavailable. Those can really miss a live session.
Track them separately, and let only an inspected, rejected checkpoint count
as observed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Build Claude project fixtures with Claude's real slug

The fixtures named a Claude project directory by replacing only / and .,
but Claude (and ClaudeProjectSlug) replaces every non-alphanumeric scalar.
On runners whose temp path holds an underscore the fixture directory was
one Claude never creates, so the transcript-derived cwd fell back to the
launch cwd and claudeRestorableIndexMapsNestedTranscriptPathToProjectCwd
failed. Share one independent slug helper across the four test files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Check the closed manager after the close's own deferred work

The Cloud create path holds the manager only weakly. The test sampled it
in the same main-actor job as commitMainWindowClose, while the close's
autoreleased notifications (object: the manager) and queued main-queue work
still held it, so it read as leaked until the next turn. Drain the close in
an autorelease pool and wait one main-queue turn, with the create still
parked, before checking.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

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

* style(cmux-tui): apply rustfmt to session journal backfill

main fails the cmux-tui cargo fmt check here, which stops hosted
verification before any test runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: reproduce untrusted cmux Codex hooks

Codex applies -c overrides by splitting the key on every '.', so the
session-flag trust that cmux-tui and the ssh-tmux helper pass as
hooks.state."/<session-flags>/config.toml:...".trusted_hash never lands
and every injected hook needs review. The wrapper test now parses -c the
way Codex does.

Also cover a cmux-tui reinstall moving its hook group behind the cmux
app's group in a shared hooks.json, which shifts the app's hook off its
trusted key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: key Codex hook trust by the default home verbatim

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: trust every cmux Codex hook Codex sees

cmux-tui and the ssh-tmux helper passed session-flag hook trust as
-c hooks.state."<key>".trusted_hash. Codex splits -c keys on every
'.', including inside the quotes, and each key contains one
(config.toml, hooks.json), so none of that trust or the matching
enabled=false applied. Codex listed each event twice and asked the user
to review the injected copy. Pass the whole state as one inline table at
hooks.state, which Codex merges with the persistent trust.

A cmux-tui reinstall also dropped its hook group and appended a new one,
moving it behind hooks another producer (the cmux app) added later and
shifting that hook off its trusted positional key. Reinsert the group in
the slot it came from.

The cmux CLI keyed trust by the fully resolved hooks.json path, but
Codex resolves only an explicit CODEX_HOME and uses $HOME/.codex
verbatim, so a symlinked home left every app hook untrusted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* test: Cloud creation actions must not repeat the welcome

* fix: show the Cloud welcome only once across entry points

* fix: preserve shared-team Cloud welcome routing
* test: push a new machine's prompt name as root

The prompt name push writes the root-owned /etc/cmux. It ran as root
until #16769 moved public vm exec to the work user, and has failed with
a permission error since; a dev backend's machines then keep cmux@cmux.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): push the prompt name as root again

#16769 moved public vm exec to the work user so commands see the same
home as Cloud terminals. The create and rename prompt pushes share that
exec and write the root-owned /etc/cmux, so they have failed since.
Production hid it behind the guest's reflection fetch; dev backends
can't be reached that way and kept cmux@cmux. Restore root for exactly
these pushes through an explicit asRoot exec option; user vm exec stays
on the work user.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

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

* test(cloud): cover hard restart for frozen VMs

* fix(cloud): recover frozen VMs through provider restart

* fix(cloud): finish restart health alert integration

* fix(cloud): satisfy Swift lifecycle timeout typing

* fix(cloud): make fallback restart retries idempotent

* test(cloud): reject unverified recovery and stale restart links

* fix(cloud): fail closed without provider hard restart

* test(cloud): expect restart capability to fail closed

* fix(cloud): keep health alerts snapshot consistent

* docs(cloud): describe restart capability honestly

* test(cloud): cover capability-gated recovery alerts

* test(cloud): reject pause resume guidance for frozen guests

* fix(cloud): preserve frozen machines in recovery guidance
* test(cloud): a full shared pool is not reported as a missing plan

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): explain a full shared pool instead of a missing plan

A 402 vm_resource_pool_exceeded on a Pro team read as "Your plan does
not allow this Cloud operation." because the client classified every
402 by status alone. Read the server's error code: a full pool names the
vCPUs or memory in use and the request (reusing the sheet's pool
shortfall copy), active-limit, create-credit and size-gated refusals get
their own sentence, an unrecognised code keeps the server's message, and
only a code-less 402 keeps the generic plan sentence. The pending create
row offers Retry first and an upgrade second for a full pool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): report a full pool as resource_limit and redact server copy

Address review: the server's telemetry contract rejects an unknown
failure class and drops the whole batch, so a full pool or active-limit
refusal is classified as the existing resource_limit instead of a new
capacity class. The open-workspace path now applies the same redaction
fallback as create, and a full pool offers an upgrade only once the plan
is known to have a higher tier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): key create failure actions on the parsed refusal code

Address review: the pending create row matched refusal codes as
substrings of the display text, so a longer code such as
vm_resource_pool_exceeded_v2 counted as a full pool. Read the code once
from the exact (HTTP 402: <code>) reference the CLI and the in-process
create both print, and compare it whole. Also translate the four new
failure strings into every locale the catalog carries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): carry the refusal code apart from the failure text

Address review: reading the code back out of the failure text let a
server message that named another code stand in for the real one. The
in-process launcher now reads the code from the thrown error and the CLI
path reads only the reference ending the CLI's own Error: header; both
pass it through the create completion onto the operation, and the
failed row's actions key on that carried code alone. A retry clears it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cloud): cover the refusal code handoff to the failed row

Address review: assert the app coordinator carries a launcher's refusal
code onto the pending row and clears it on retry, that finish overwrites
a code, and that an open failure on an existing machine keeps none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cloud): cover pool upgrade and credit billing scope regressions

* fix(cloud): preserve pool upgrades and billing scope

* test(localization): cover Unicode escapes in Swift defaults

* fix(localization): decode Swift Unicode scalars in defaults

* fix(cloud): localize personal create credit refusals

* test: correlate v2 mock responses with request ids

* Keep localization additions in main catalog order

* Start CodeRouter children from a clean signal mask and the caller's stdio

The two posix_spawn sites #18953 added in the CLI handed the child the
calling Swift concurrency thread's signal mask, so a CodeRouter child could
not receive SIGTERM or SIGWINCH (CLIAuthAliasTests: termination signal) and
the exec/spawn signal-state guard failed. Set an empty mask and default
dispositions on both, as the other CLI spawn sites do.

The alias exec also decided which standard descriptors to pass on at spawn
time, so a file the CLI had opened since launch could reuse a closed stdin
and reach the child as an open one. Record the caller's standard
descriptors at the top of main and pass on only those.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Assert that `coderouter add` stays off the socket handoff

#18953's classification test expected `add codex` to arm the handoff,
contradicting its own name ("only exact top-level agent names use socket
handoff") and the launch policy. CodeRouter's handoff parser accepts only a
provider name after `--`, and `add` does its own login without a handoff
lease, so the policy is right and the expectation was wrong.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Probe a closed stdin without being fooled by the script itself

With descriptor 0 closed, /bin/sh opens the script it runs on that
descriptor, so the fake CodeRouter's `(: <&0)` probe always reported an
open stdin on macOS and the test could never pass. Treat stdin as open only
when descriptor 0 exists and is not the script.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: echo request ids in capabilities fixture

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* test: cover split actions targeting projected remote tmux panes

* fix: route split actions through projected remote tmux pane owners

Cmd+D and menu splits pass the focused terminal surface ID. A remote tmux
pane is projected from its mirror and is absent from workspace.panels, so
the shared split action rejected it before sending a remote command.

Resolve projected panes through the existing control topology and dispatch
to their mutation owner. Preserve the exact target, split orientation,
before placement and focus intent; rejected requests never fall back to a
local split.

Verification: changed Swift syntax and git diff --check passed. A standalone
probe executing the actual split action with synthetic collaborators ran
27 routing assertions: 16 failures on regression commit 4162b2d, zero with
the fix. The probe does not compile or exercise the native app. App-host
regressions were added but not executed; full Xcode is unavailable locally.

* test: cover remote tmux keyboard split routing

Co-authored-by: Mirksen <52635146+Mirksen@users.noreply.github.com>

---------

Co-authored-by: Austin Wang <38676809+austinywang@users.noreply.github.com>
BTreeSet is already imported, and clippy's -D unused-qualifications
fails the cmux-tui lint job on main.

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

#20376 gave a persistent channel's per-request deadline its own error,
LinkError.requestTimedOut, separate from the connect timeout. The app
already classifies it as a timeout and as retryable, but this test still
caught only .timedOut, so the deadline it waits for escaped as an
uncaught error. Catch the error the channel now throws, and check that
same error is retryable.

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

* Add regression for Codex restore failing on coordination-lock contention

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Retry Codex lock-namespace contention before refusing a restore

During a relaunch dozens of Codex threads resume at once, each briefly
holding thread-writer-locks/.coordination.lock. The CLI's final exec guard
treated that `.changing` probe like an unreadable lock and refused an
unowned conversation. It now re-probes `.changing` for a bounded ~3.5s,
woken early by lock-namespace file events, while `.active` and
`.unavailable` still refuse at once with case-specific messages.

The app admission path already kept `.changing` recoverable, but waited up
to 8s per round for a file event a flock release never raises; it now
re-probes after 250ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re-probe lock-namespace changes inside one admission request

Review found that a 250ms evidence bound turns a coordination lock that
never settles into ~4 admission RPCs per second per pane. Keep the 8s
request bound and instead re-probe `.changing` on the settle bounds within
the same request, with an app-host regression for the waiting request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* test: cover OpenCode lifecycle notification regressions

* fix: restore OpenCode lifecycle notifications

Handle current OpenCode completion and failure events, coalesce duplicate idle callbacks, and preserve error state through the feed and TUI journal bridges.

Co-authored-by: e-jung <e-jung@users.noreply.github.com>

* fix: finish OpenCode notification repair

Route lifecycle plugins through OpenCode's XDG config directory, preserve failed turns in Feed, and cover the current error event shape and ordering.

Co-authored-by: e-jung <e-jung@users.noreply.github.com>

* fix: share OpenCode failure detail extraction

Use the same non-optional failure message in notification and Feed delivery, and let the coalesced idle fixture finish on a session-end marker.

Co-authored-by: e-jung <e-jung@users.noreply.github.com>

* test: wait for OpenCode failure hook

* test: cover OpenCode server entry notifications

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: activate OpenCode server plugin entries

Released OpenCode (1.x and the dev channel) loads cmux through each
plugin's default server(ctx) entry inside the TUI process. Those entries
returned {}, so no lifecycle event reached cmux. Subscribe when the TUI
process server context is present, once per process.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(opencode): publish one terminal stop per turn

A turn now settles on its first terminal event and is reopened only by a turn
identity the session has not seen (a new user message or prompt). Repeated
message updates and busy status no longer clear the recorded stop, and a
failure after a published idle stop is dropped instead of producing a second
notification. Applies to the Feed bridge and the session plugin source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(opencode): redact credentials in upstream failure text

OpenCode error text reaches notifications and Feed error fields. Redact
credentials, bearer tokens, JWTs and emails first, keep paths and model
names readable, and fall back to product copy when nothing safe remains.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(opencode): record OpenCode's own launch for resume

On OpenCode 1.x the TUI process argv is [opencode, /$bunfs/root/src/index.js],
and the sanitizer only dropped the V2 TUI worker entry, so restore replayed
the Bun virtual path as a project directory. Drop any Bun virtual entry.
Also stop reusing an inherited launch capture that describes another agent,
which recorded e.g. a Claude command for an OpenCode session started from a
Claude shell.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cmux-tui): keep shutdown-interrupted OpenCode turns running

OpenCode retains an execution interrupted by shutdown and resumes it, so
map that interruption to agent.state.changed instead of completing the turn.
Read the structured reason before redaction replaces free-form strings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: register the OpenCode server-entry harness as manual

No workflow names tests/test_opencode_server_entry.py, so the registry
guard rejected its linux-guard lane.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: resubscribe OpenCode server entries after scope reinit

The process-global guards blocked a reinitialized plugin scope from
subscribing again. Key the owner on the plugin ctx, which every loader
in one scope shares, and replace the previous feed when it changes.
The regression harness now waits on a session.deleted sentinel instead
of a fixed delay and keeps its socket in the temporary directory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(opencode): open turns on run signals, not message updates

A queued prompt's admission and user message arrive while the previous turn
is still running, so keying turns on first-seen message IDs dropped the stop
for the queued turn. Open a turn only on a run signal (session creation, busy
status, a promoted prompt or an execution start) in all three bridges; a
repeated message update after a stop still cannot reopen the turn. The TUI
bridge tracks turn starts before its ownership check so a turn that starts in
a hidden tab keeps its stop.

Also:
- Do not publish a stop for an execution interrupted by shutdown, which
  OpenCode resumes, matching cmux-tui.
- Redact credentials in the Feed plugin's own failure text, which reaches
  Feed without passing through the CLI.
- Drop inherited subrouter and routed-account launch keys when recapturing
  OpenCode's own launch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(opencode): cover queued prompts and shutdown interruptions

Drive a prompt that is admitted during a running turn and promoted after its
stop, a repeated user-message update after a stop, a late failure, and a
shutdown interruption through the session plugin and the Feed bridge. Wait
for a session-end sentinel before counting so a late extra stop is caught.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: bind OpenCode hooks to this process and plugin scope

An OpenCode started from another OpenCode inherited the parent's
opencode launch capture and recorded the parent's resume command.
Always capture this process's launch; no cmux wrapper exports one.

OpenCode never disposes a replaced plugin scope's hooks, so the session
bridge now ignores events once a new scope owns the process.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: cover raw OpenCode failure events in Feed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: share OpenCode failure classification with Feed

A raw session.error, session.execution.failed or session.next.step.failed
stop made the notification an error but reached Feed as a successful
Stop. Both paths now use one predicate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: cover OpenCode interrupts and concurrent plugin scopes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: keep OpenCode interrupts and concurrent scopes out of failures

OpenCode publishes session.error for a user interrupt (MessageAbortedError)
and a recoverable context overflow (ContextOverflowError), then goes idle.
These turned an Esc into a red error notification. The bridges now let the
idle stop decide the outcome.

OpenCode keeps one plugin scope per directory and disposes a scope's
hooks on teardown, so dedupe only within one ctx instead of evicting the
other live scope.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: e-jung <e-jung@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* test: cover browser Return routing and native form submission

* fix: deliver browser Return to the owning web view

* test: keep Return out of Web Inspector page routing

* fix: preserve Web Inspector Return routing

* test: cover inspector Return in browser focus mode

* fix: keep Web Inspector outside browser focus mode

* fix: compile browser inspector focus guard

* test: keep foreign web content Return out of browser focus mode

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: use structural ownership for browser Return routing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* test: don't save or restore a Claude session that never wrote a transcript

Claude Code writes its transcript only after the first message. A tab
whose Claude was started and quit before any prompt keeps its agent-hook
binding, so relaunch types 'cmux restore claude <id>' and Claude fails
with 'No conversation found'. Expected red until the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: don't save or restore a Claude session that never wrote a transcript

Claude Code writes <config>/projects/<slug>/<id>.jsonl only after the
first message. A session started and quit before any prompt kept its
agent-hook binding, so relaunch typed 'cmux restore claude <id>' and
Claude failed with 'No conversation found'.

- The agent index records Claude sessions whose hook record names where
  the transcript goes (transcript_path or CLAUDE_CONFIG_DIR) but finds
  none in any config root, with the paths to re-check.
- Workspace and Dock session snapshots drop that session's Claude agent
  and agent-hook bindings after the snapshot is built, re-checking the
  paths first so a first prompt sent after the load still restores. An
  older session on the surface is never promoted in its place. A cached
  index marked incomplete carries no such evidence.
- cmux restore claude checks every config root the record and shell name
  and fails early with a localized message, retiring only the binding
  for that checkpoint, instead of launching Claude.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: address review of transcript-less Claude restore

- cmux restore claude checks only when the session names its transcript
  location (the hook-store transcript_path or a captured
  CLAUDE_CONFIG_DIR) and accepts the reported transcript directly, so a
  session in a root the lookup cannot see is never refused or unbound.
- A cached index marked incomplete keeps the transcript-less evidence.
  The filter re-checks the expected paths, so update relaunch and the
  quit deadline fallback drop the empty session without discarding one
  whose first prompt landed later.
- Closed-tab history for workspace and Dock panels applies the filter.
- Neutral error wording, since a live session elsewhere has no transcript
  yet either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Add regression test for Vietnamese IME Backspace dropping preedit

macOS Vietnamese Telex commits its marked text on Backspace, then sends
deleteBackward:. The committed text is currently attached to the
Backspace key event, which Ghostty's encoder drops.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fix Vietnamese IME Backspace wiping the preedit

macOS Vietnamese Telex handles Backspace on underlined text by committing
the preedit with insertText and then sending deleteBackward:. keyDown sent
the commit as a Backspace key event with UTF-8 attached, which Ghostty's
encoder drops, and doCommand ignored deleteBackward:, so the whole word
vanished.

When interpretKeyEvents both commits text and asks for deleteBackward:,
send the commit as typed text and then the Backspace on its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* test: Freestyle TLS rule limit defaults to the raised 200,000 cap

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): raise the Freestyle TLS rule limit default to 200,000

Freestyle raised the account cap from 2,000. The old default made the
operator alert page as "limit reached" at 2,367 rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* test: cover upright Design Mode selection PNGs

* fix: preserve Design Mode screenshot orientation

* test: verify the Design Mode handoff artifact
* test: cover inactive-space display reconciliation

* fix: preserve inactive Spaces during display changes
lawrencecchen and others added 11 commits October 14, 2026 21:52
…ger recompiles cmux-remote

cmux-remote's build script hashed the dirty source tree into
CMUX_TUI_BUILD_IDENTITY, so every edit in the workspace reran it and
recompiled cmux-remote and every crate above it (about 8 s of a 12.6 s
one-line edit loop on rbx). The same build script now lives in
cmux-build-identity, which only cmux-tui reads; cmux-remote takes the
identity as an argument (SshBootstrapConfig::defaults,
pinned_linux_payload). The identity value, its inputs and format, and
every skew and SSH re-bootstrap comparison are unchanged.
#20884)

#20592 landed after #20873 without warmViteServer, so check:browser-lanes is
red on base again.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…enum (no bead) (#20887)

The package conventions lint fails every PR on the new all-static public
enum WindowFrameStrip (cx-me3xd). The report reads its window, so it is an
extension on NSWindow; same body, same report.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The harness command builder moved to agent/command.rs on base; the caller token env (caller_env) moves with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Not verified: macOS jobs did not run: compile and app tests were skipped on 7f2b6a385c; ci-status is green without them.

CI passes on 7f2b6a385c (run 38689792764 attempt 1).
macOS jobs did not run: compile and app tests were skipped: Fast static checks skipped (the macos job needs both to succeed). Re-run CI once they pass, before merging.

Written by scripts/ci/classify_failures.py (ci-failure-attribution.yml); signatures are its SIGNATURES table. A machine verdict is the runner's fault, not this PR's; yours means the failing file is one this PR changes, also red on main that main's latest full suite fails the same way, seen on other PRs that it failed on another pull request's run lately.

- regenerated plans/cmux-next/action-surfaces.json
- regenerated plans/cmux-next/actions.md

Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/38688097375
@teamleaderleo

teamleaderleo commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator Author

Split plan

This PR isn't a mega-PR. The 803-file count is measured against main, but the PR targets feat-cmux-next, and acpmux doesn't exist on main at all. Measured against its merge base with feat-cmux-next (0664bd9847b5), the PR is 14 commits, 31 files, +1933/-86, all in cmux-tui/crates/acpmux/ plus the two generated plans/cmux-next/ action files. It doesn't touch web/ or workers/, so the BSL CLA rule doesn't apply.

Bucket Files Lines
Feature code (start gate, caller token, preset approval, peer prompts, refusal text) 25 ~960
Tests (person_auto_start.rs, peer_integration.rs, hub_integration.rs, fake_agent.py) 4 ~910
Generated (plans/cmux-next/action-surfaces.json, actions.md, regenerated by PR autofix) 2 ~67

None of it is on feat-cmux-next yet: hub/start_gate.rs and tests/person_auto_start.rs don't exist there. A merge into current feat-cmux-next conflicts in 5 files, and each conflict is small:

  • server/mod.rs: one use line (SessionReceipt, EventRecord).
  • server/requests.rs: keep_harness_mode next to the new route field. Keep both.
  • tests/fake_agent.py: FAKE_PLAN_MODE next to FAKE_FULL_ACCESS. Keep both.
  • The two plans/cmux-next files: regenerate them.

Recommendation: don't close it. Resolve those 5 conflicts and land it into feat-cmux-next as one PR. The two security reviews were done on the combined head, and the security checks in (a), (b) and (c) share hub/person.rs and server/mod.rs. If a reviewer wants it smaller, here's how it splits, in landing order. Each slice targets feat-cmux-next and regenerates the plans files:

  1. Peer pending prompts (c), 7 files. peer.rs (personInstance from the handshake), hub/peers.rs (peer_pending_counts and the relay), hub/mod.rs (export), server/pending.rs (moved out of server/mod.rs), the deliver hunk in server/mod.rs, the (c) section of hub/person.rs, and tests/peer_integration.rs.
  2. Inherit plus refusal text (a)(d), about 15 files. hub/start_gate.rs without the preset parts, ACPMUX_SESSION_TOKEN and _SOCKET in hub/spawn.rs, client.rs, cli/command.rs, cli/run/standard.rs, agent/command.rs, the caller hunk in server/mod.rs, server/requests.rs, hub/session.rs, hub/lifecycle.rs, hub/fork.rs, docs/acpmux-schema.json, the (a)(d) sections of hub/person.rs, fake_agent.py, hub_integration.rs, and the inherit and refusal tests from person_auto_start.rs.
  3. Person-approved preset (b), about 10 files. hub/start_gate/preset_bind.rs, server/requests/presets.rs, config.rs, config/preset_args.rs, cli/orchestrate/presets.rs, hub/pool/mod.rs, hub/resolve.rs, store.rs, the digest part of hub/spawn.rs, the (b) section of hub/person.rs, and the preset tests from person_auto_start.rs.

Shared files: #20549 also edits config.rs, hub/lifecycle.rs, hub/mod.rs, hub/person.rs, hub/resolve.rs, server/requests.rs, fake_agent.py and hub_integration.rs. #20963 also edits hub_integration.rs and peer_integration.rs. #20706 also edits the two plans files. The hunks listed above are this PR's own, and the other PRs' changes in those files are owned by their splits. server/mod.rs and server/requests.rs land with #21322 (composer-stack), so no slice is carved here until that's settled.

Slices opened

  1. acpmux: a peer's pending prompts reach this daemon's watchers #21642: a peer's pending prompts reach this daemon's watchers. Merged (2cf3534).
  2. acpmux: no-ask start modes by inherit or an approved preset #21666: no-ask start modes by inherit or an approved preset. Review fixes are in, and it's rebased onto feat-cmux-next now that acpmux: a peer's pending prompts reach this daemon's watchers #21642 has merged.

Follow-up slice (not coded yet)

  • server/pending.rs peer_permission_pending: send the session's qualified_id, not the bare mux id, so the relayed notification names the same session as the peer's other records since feat(next): carry explicit session lifecycle receipts #21397.
  • Tests in peer_integration.rs for:
    • a prompt whose personInstance doesn't match the run the peer named at its handshake (dropped);
    • two peers that name the same run (dropped for both);
    • a Web watcher (never gets a peer's prompt).

Separate from this PR (needs its own bead): since #21397, a peer picks its own host_id, so it can overwrite another peer's qualified key in remote_sessions (hub/peers.rs, the map.insert(id, ...) around line 350).

🤖 Generated with Claude Code

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Slices opened, both targeting feat-cmux-next:

  1. acpmux: a peer's pending prompts reach this daemon's watchers #21642: a peer's pending prompts reach this daemon's watchers (7 files). One change from this PR: the session lookup goes through SessionReceipt.
  2. acpmux: no-ask start modes by inherit or an approved preset #21666: no-ask start modes by inherit or an approved preset (the remaining 25 files, stacked on acpmux: a peer's pending prompts reach this daemon's watchers #21642). It also makes person_auto_start.rs hermetic and its person-key pipe close-on-exec.

Together they cover all of this PR's own change. The plans/cmux-next diff here was regenerated base drift, so it isn't carried. Once both merge, this PR can be closed.

🤖 Generated with Claude Code

teamleaderleo added a commit that referenced this pull request Oct 18, 2026
#21642)

A peer's `_acpmux/permission_pending` now reaches unix-socket and app
watchers of this daemon that did not attach the session (`via: "peer"`),
with the peer's decision nonce and run, so the app can show and answer
it. Answers still go to the peer, which counts an allow only with its own
person's proof.

It is relayed only for a session that peer listed as its own and that is
not this daemon's, in the run the peer named at its handshake, which is
neither this daemon's run nor one another peer named. It never goes to a
Web client or on to another peer daemon. The residual (a peer naming a
third daemon's run) is in the `hub/person.rs` threat model.

Carved from #20653.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
teamleaderleo added a commit that referenced this pull request Oct 18, 2026
… preset

A `session/new` that asks for a mode that doesn't ask, without the
person, now has two ways through:

- Inherit: a caller that is itself an acpmux session in that exact mode on
  the same harness family. Each harness gets ACPMUX_SESSION_TOKEN, an HMAC
  of its session id under a per-run in-memory secret, and
  ACPMUX_SESSION_TOKEN_SOCKET. The token is read only from a unix socket
  connection's first `initialize`. The caller's mode is read at request
  time.
- Approved preset: `_acpmux/presets {name, approveMode: true}` from the
  person records the sha256 of the preset's contents. The approval counts
  only while the resolve and the spawn both used those exact contents, for
  the preset's own mode, with no request overrides other than a folder.

A start mode never applies to an adopt without the person, and a proven
caller session that asks never gets the full-access default for its
chats. The refusal names where to approve (cmux NEXT on this host, or the
preset to approve there once), with {reason, host, mode, preset} in
error.data. Web and peer clients never get the host name.

person_auto_start.rs runs in the hermetic test environment, and its
person-key pipe is close-on-exec so parallel tests' daemons never hold
each other's key pipe open.

Carved from #20653.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
teamleaderleo added a commit that referenced this pull request Oct 18, 2026
* feat(acpmux): start a no-ask mode from a session in it or an approved preset

A `session/new` that asks for a mode that doesn't ask, without the
person, now has two ways through:

- Inherit: a caller that is itself an acpmux session in that exact mode on
  the same harness family. Each harness gets ACPMUX_SESSION_TOKEN, an HMAC
  of its session id under a per-run in-memory secret, and
  ACPMUX_SESSION_TOKEN_SOCKET. The token is read only from a unix socket
  connection's first `initialize`. The caller's mode is read at request
  time.
- Approved preset: `_acpmux/presets {name, approveMode: true}` from the
  person records the sha256 of the preset's contents. The approval counts
  only while the resolve and the spawn both used those exact contents, for
  the preset's own mode, with no request overrides other than a folder.

A start mode never applies to an adopt without the person, and a proven
caller session that asks never gets the full-access default for its
chats. The refusal names where to approve (cmux NEXT on this host, or the
preset to approve there once), with {reason, host, mode, preset} in
error.data. Web and peer clients never get the host name.

person_auto_start.rs runs in the hermetic test environment, and its
person-key pipe is close-on-exec so parallel tests' daemons never hold
each other's key pipe open.

Carved from #20653.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(acpmux): an approved preset with a route, and env that names the caller token

Red on 116ab01:
- an approved preset plus a request `route` passes the start gate and
  reaches the handler ("no route"), where it should need the person;
- a harness profile env that sets ACPMUX_SESSION_TOKEN reaches the agent
  in place of acpmux's caller token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(acpmux): a route counts as an override, env never sets the caller token

Review fixes for the start gate:
- A request `route` on an approved preset now needs the person, like its
  own model, effort, policy or env: a route swaps the provider base URL
  and keys.
- A profile's, preset's or request's env no longer replaces
  ACPMUX_SESSION_TOKEN or ACPMUX_SESSION_TOKEN_SOCKET. That env is
  layered after `caller_env`, so the spawn drops both keys from it.
- The CLI's daemon readiness pipe is close-on-exec from creation
  (`std::io::pipe`, pipe2(O_CLOEXEC) where the OS has it) instead of a
  `pipe` then `fcntl`, which raced with a fork on another thread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
teamleaderleo added a commit that referenced this pull request Oct 18, 2026
* cmux NEXT: a folded long thought reads "Thought for 12s"

A long thought folded once done (#20948, Debug toggle
agentPane.debug.foldLongThoughts) now reads how long the agent thought,
from its first chunk to its latest, clamped at zero for out-of-order
events. An undated thought, or one under a second, reads "Thought".

#20948's "Thought" plus first line stays as the Debug choice
agentPane.debug.thoughtHeader = preview, which the app sets on <html> as
data-thought-header. New string tools.thoughtFor in all 21 pane locales
(split from #20963).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux NEXT perf-hitch2: an evicted terminal shows its last frame at once when shown again (GhosttyNextKit a19c884fa, cx-8w027.19.1)

A terminal the warm set evicted showed an empty pane until its new session
attached and drew. Ghostty emptied every released drawable
(kIOSurfacePurgeableEmpty in IOSurface.deinit), and a hidden surface releases
its swap chain, so even the layer's presented frame was blank (ghostty-next
PR 42: a plain release; the last owner frees the pages).

- A paused surface marks its presented frame volatile and makes it
  non-volatile before it shows again; a purged one leaves the layer empty
  until Ghostty's next frame, as before.
- Eviction keeps the presented IOSurface (no copy, volatile), at most the warm
  set terminal capacity, oldest first; a frame dropped by the bound leaves a
  480 px hover picture; memory pressure drops them with no renders.
- The new session shows it over its first surface (TerminalFrameHold
  begin(lastFrame:)) until its first content is parsed and drawn, at most 3 s;
  a purged frame or a hidden mount shows nothing. Hover cards picture it on
  demand.
- debug.surfaces: last_frames outcomes, per pane last_frame_holds, frame_held.
  Proof: scripts/cmux-next/evicted-terminal-frame-live.py.

Pins GhosttyNextKit release xcframework-a19c884fa8d132378ae838dcba95654b177b6723-apple-v6
(checksum 94ff3178... from SHA256SUMS, matching the downloaded zip) and moves the
ghostty-next gitlink, package-notices.json and vt-link-graph.json to it (only
pkg/macos/iosurface/iosurface.zig changed since e6a27a1fb).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* perf(cmux-next): a terminal mounted hidden after an eviction shows its last frame at its reveal (cx-8w027.19.1)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* perf(cmux-next): hover pictures reclaim a paused surface's volatile frame for the read; a frame in flight at the pause is not reclaimed; no last frame at capacity 0 (cx-8w027.19.1, review)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* perf(cmux-next): last-frame logic lives in CmuxNextTerminal (TerminalLastFrames evict/show/report); the frame shows at the reveal; CmuxNextApp does not grow (cx-8w027.19.1)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* release-notes(next): fit browser highlights to the What's New limits (#21716)

* release-notes(next): fit browser highlights to the What's New limits

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* release-notes(next): live links, dev-build wording for remote tabs

Replaces cmux.com/whats-new (404) with real try-it actions, says the
remote browser tab entries are development-build features (their action
is DEV/Nightly only), and drops 'instant' from the tab-switch entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

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

* release-notes(next): fit app highlights to the What's New limits (#21717)

* release-notes(next): fit app highlights to the What's New limits

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* release-notes(next): live links and exact profile-menu wording

Replaces two 404 links (cmux.com/whats-new, docs/agent-integrations) with
real actions or docs pages, and says what the profile button opens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

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

* feat(next): workspace name source and revision, like tab names (hq#1957) (#21648)

* feat(next): workspace name source and revision, like tab names (hq#1957)

Workspaces now record whether their name came from a person or from
auto-naming, the way tab names do (NameSource { User, Auto } and a
name_revision), and the daemon decides whether an automatic rename lands.

- RegistryWorkspace and the mux Workspace carry name_source and
  name_revision, stored in two additive workspaces columns (legacy rows
  read user/0). Both commit paths re-check the stored row's name, source
  and revision inside the transaction.
- rename-workspace and workspace.rename accept source, expected_generation,
  expected_name_revision and release. An automatic rename never replaces a
  person's name (operation.failed, reason_code name_pinned), never empties
  one (name_empty), and loses to any rename after the revision it read
  (revision.conflict). source auto with release hands a pinned name back.
- Workspace projections (raw tree, list-workspaces, resource extra) expose
  name_source and name_revision; the daemon advertises
  workspace-name-source-v1.
- CLI: cmux workspace rename --source auto --expected-generation G
  --expected-name-revision N, and --release-to-auto.
- Spec, regenerated SDK bindings, cmux-app.d.ts and resource descriptor
  hashes follow; optchat-chief's RenameWorkspaceRequest literal gains the
  new fields.
- The app's DaemonCapabilities moves workspace-name-source-v1 from
  unservedByBundledDaemon to optional, since the bundled daemon now serves
  it.

Carved from #20710 onto current feat-cmux-next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(next): project workspace name provenance like the journal states it (hq#1957)

feat-cmux-next tests that landed after #20710's base failed on this slice:

- batch_close and resource_effects: the journal decorates every stored
  workspace with extra.name_source/name_revision, but the projected
  workspace values had no extra, so the public fold never matched and every
  commit restated every workspace. Every workspace value builder (deltas,
  full and scoped projection, normalized registry deltas, resource
  snapshots) now carries the same extra.
- key_close_cannot_rebind_a_live_durable_workspace_identity: creating a
  workspace at a key bound to another live workspace hit the name fence
  (revision.conflict) before the identity check. The name fence now
  applies only to the same workspace (numeric id), and the resource store
  checks identity before the name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(next): workspace_resource_upsert lives with workspace renames (hq#1957)

Its name provenance parameter pushed mux.rs past the 1000-line god-file
budget (check-no-godfiles.sh); the helper moves to mux/workspace_rename.rs
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

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

* fix(cmux-next): restore #21369 and #21384, undone by the Chief Dock squash (#21690)

* test(cmux-next): older history is asked for once per page

Fails on feat-cmux-next: a streaming commit near the top asks for the same
older page again, because 50a0c60 (#21194) restored the effect #21369
had keyed on the first row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(cmux-next): avoid duplicate older-history loads (#21369)

(cherry picked from commit 8f6f9cb)

* perf(cmux-next): gate find highlight repaints by transcript changes (#21384)

* perf(cmux-next): gate find highlight repaints by transcript changes

* fix(cmux-next): preserve find repaint triggers after fold reveals

* style(acpmux): format find highlight effect

* test(next): carry apps empty-state selector through PR 21384

* test(next): carry apps empty-state selector through find highlight PR

(cherry picked from commit afec4de)

---------

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

* acpmux: no-ask start modes by inherit or an approved preset (#21666)

* feat(acpmux): start a no-ask mode from a session in it or an approved preset

A `session/new` that asks for a mode that doesn't ask, without the
person, now has two ways through:

- Inherit: a caller that is itself an acpmux session in that exact mode on
  the same harness family. Each harness gets ACPMUX_SESSION_TOKEN, an HMAC
  of its session id under a per-run in-memory secret, and
  ACPMUX_SESSION_TOKEN_SOCKET. The token is read only from a unix socket
  connection's first `initialize`. The caller's mode is read at request
  time.
- Approved preset: `_acpmux/presets {name, approveMode: true}` from the
  person records the sha256 of the preset's contents. The approval counts
  only while the resolve and the spawn both used those exact contents, for
  the preset's own mode, with no request overrides other than a folder.

A start mode never applies to an adopt without the person, and a proven
caller session that asks never gets the full-access default for its
chats. The refusal names where to approve (cmux NEXT on this host, or the
preset to approve there once), with {reason, host, mode, preset} in
error.data. Web and peer clients never get the host name.

person_auto_start.rs runs in the hermetic test environment, and its
person-key pipe is close-on-exec so parallel tests' daemons never hold
each other's key pipe open.

Carved from #20653.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(acpmux): an approved preset with a route, and env that names the caller token

Red on 116ab01:
- an approved preset plus a request `route` passes the start gate and
  reaches the handler ("no route"), where it should need the person;
- a harness profile env that sets ACPMUX_SESSION_TOKEN reaches the agent
  in place of acpmux's caller token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(acpmux): a route counts as an override, env never sets the caller token

Review fixes for the start gate:
- A request `route` on an approved preset now needs the person, like its
  own model, effort, policy or env: a route swaps the provider base URL
  and keys.
- A profile's, preset's or request's env no longer replaces
  ACPMUX_SESSION_TOKEN or ACPMUX_SESSION_TOKEN_SOCKET. That env is
  layered after `caller_env`, so the spawn drops both keys from it.
- The CLI's daemon readiness pipe is close-on-exec from creation
  (`std::io::pipe`, pipe2(O_CLOEXEC) where the OS has it) instead of a
  `pipe` then `fcntl`, which raced with a fork on another thread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

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

* cmux-next: the static status look everywhere; indicators name their state (cx-sya4n) (#21664)

* test(cmux-next): the static look reaches tabs, the attention ring, the agent pane and every label (cx-sya4n)

Red: tabs keep their icon set's marks and colored dots, the attention
ring blinks, the agent pane's theme has no statusIndicators look and
indicators have no tooltip or VoiceOver label (compile failure).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-next: the static status look everywhere, and every indicator names its state (cx-sya4n)

Tabs draw the static look's still marks (the badge too, never a dot
told apart by color alone); the pane attention ring is the steady ring
under Static; the agent pane's theme sends statusIndicators (static
under Static or Reduce Motion). Every StatusIndicatorView's tooltip and
VoiceOver label is the state's short name (StatusIndicatorState
.shortLabel, nine strings in every catalog locale), and a tab's
needs-input VoiceOver text names the blocked kind. Carved from #20511.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com>
Co-authored-by: safe-push <safe-push@cmux.local>
teamleaderleo added a commit that referenced this pull request Oct 18, 2026
* cmux-next: keep the Mac awake while agents work (sessions.keepAwake)

New Settings > Agents > Agent Sessions row, "Keep Mac Awake While Agents
Work": Off, While Plugged In (default) or Always. While a local agent runs a
turn (an acpmux session, or a tab's agent hooks or OSC 7501 working record),
idle system sleep stays off. The display can still sleep and closing the lid
still sleeps the Mac.

- schemas/settings/source/sessions.json is the row's source; the Swift
  descriptor, parser constants and choice enum are generated.
- KeepAwakeService holds one ProcessInfo activity for every reason, so the
  Keep Mac Awake action and the setting never end each other's hold. The
  action now goes through it and still holds on battery.
- PowerSourceMonitor follows IOKit's power-source notification on the main
  run loop (no polling); unplugging releases a While Plugged In hold.
- Agents may not set the key (userOnly).
- Strings in all 21 locales of the settings catalog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux NEXT perf-hitch2: an evicted terminal shows its last frame at once when shown again (GhosttyNextKit a19c884fa, cx-8w027.19.1)

A terminal the warm set evicted showed an empty pane until its new session
attached and drew. Ghostty emptied every released drawable
(kIOSurfacePurgeableEmpty in IOSurface.deinit), and a hidden surface releases
its swap chain, so even the layer's presented frame was blank (ghostty-next
PR 42: a plain release; the last owner frees the pages).

- A paused surface marks its presented frame volatile and makes it
  non-volatile before it shows again; a purged one leaves the layer empty
  until Ghostty's next frame, as before.
- Eviction keeps the presented IOSurface (no copy, volatile), at most the warm
  set terminal capacity, oldest first; a frame dropped by the bound leaves a
  480 px hover picture; memory pressure drops them with no renders.
- The new session shows it over its first surface (TerminalFrameHold
  begin(lastFrame:)) until its first content is parsed and drawn, at most 3 s;
  a purged frame or a hidden mount shows nothing. Hover cards picture it on
  demand.
- debug.surfaces: last_frames outcomes, per pane last_frame_holds, frame_held.
  Proof: scripts/cmux-next/evicted-terminal-frame-live.py.

Pins GhosttyNextKit release xcframework-a19c884fa8d132378ae838dcba95654b177b6723-apple-v6
(checksum 94ff3178... from SHA256SUMS, matching the downloaded zip) and moves the
ghostty-next gitlink, package-notices.json and vt-link-graph.json to it (only
pkg/macos/iosurface/iosurface.zig changed since e6a27a1fb).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* cmux-next: regenerate settings exports and defaults for sessions.keepAwake

Exports from the fleet export step at 4beacc0 (run-swift-testing-suites.sh,
export-settings); defaults snapshot from defaults_guard.py snapshot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(cmux-next): a terminal mounted hidden after an eviction shows its last frame at its reveal (cx-8w027.19.1)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* perf(cmux-next): hover pictures reclaim a paused surface's volatile frame for the read; a frame in flight at the pause is not reclaimed; no last frame at capacity 0 (cx-8w027.19.1, review)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* cmux-next: regenerate settings exports after merging feat-cmux-next

Exports from the fleet export step at 011a67c.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* restore-trace.py: trace a relaunch restore per pane with the snapshot sources that moved (cx-8w027.23)

* restore-trace.py: every pane in short, 50 ms reads (cx-8w027.23)

* perf(cmux-next): last-frame logic lives in CmuxNextTerminal (TerminalLastFrames evict/show/report); the frame shows at the reveal; CmuxNextApp does not grow (cx-8w027.19.1)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* cmux NEXT perf-restore: no pane re-run on connect, room badge from the strip's window, one topology per turn for every pane that changed (cx-8w027.23)

- PaneController.Snapshot.connected is gone: apply never read it, and the
  read re-ran every pane's snapshot when the daemon connected (16 per
  16-pane restore). A reconnect with a new daemon still re-maps through
  generation and surfaces.
- BrowserProfileService.roomDefault takes the room of the window the
  strip or sidebar avatar draws in (WindowState.profileID, observed), so a
  room's profile badge follows that window; the lookup of WindowManager
  state (not observed) stays only for one-time choices (a new tab's profile).
- A pane whose part of the focus topology changed marks the workspace
  (setNeedsTopology); FocusCoordinator builds and sends one topology per
  owner for the whole main-actor turn, at its end or before any state read
  or event (I4), instead of one per pane.

* cmux NEXT: no focus ring on the daemon recovery status label (#21679)

#20765 made the not-connected screen's status label selectable without
focusRingType = .none, so check:text-focus fails every feat-cmux-next PR.

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

* FocusCoordinator: state is a pure read; key routing and events reduce the pending topology; the turn-end reduction runs detached; what the settled observer queues runs (review of cx-8w027.23 slice 1)

* release-notes(next): fit browser highlights to the What's New limits (#21716)

* release-notes(next): fit browser highlights to the What's New limits

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* release-notes(next): live links, dev-build wording for remote tabs

Replaces cmux.com/whats-new (404) with real try-it actions, says the
remote browser tab entries are development-build features (their action
is DEV/Nightly only), and drops 'instant' from the tab-switch entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

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

* release-notes(next): fit app highlights to the What's New limits (#21717)

* release-notes(next): fit app highlights to the What's New limits

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* release-notes(next): live links and exact profile-menu wording

Replaces two 404 links (cmux.com/whats-new, docs/agent-integrations) with
real actions or docs pages, and says what the profile button opens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

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

* feat(next): workspace name source and revision, like tab names (hq#1957) (#21648)

* feat(next): workspace name source and revision, like tab names (hq#1957)

Workspaces now record whether their name came from a person or from
auto-naming, the way tab names do (NameSource { User, Auto } and a
name_revision), and the daemon decides whether an automatic rename lands.

- RegistryWorkspace and the mux Workspace carry name_source and
  name_revision, stored in two additive workspaces columns (legacy rows
  read user/0). Both commit paths re-check the stored row's name, source
  and revision inside the transaction.
- rename-workspace and workspace.rename accept source, expected_generation,
  expected_name_revision and release. An automatic rename never replaces a
  person's name (operation.failed, reason_code name_pinned), never empties
  one (name_empty), and loses to any rename after the revision it read
  (revision.conflict). source auto with release hands a pinned name back.
- Workspace projections (raw tree, list-workspaces, resource extra) expose
  name_source and name_revision; the daemon advertises
  workspace-name-source-v1.
- CLI: cmux workspace rename --source auto --expected-generation G
  --expected-name-revision N, and --release-to-auto.
- Spec, regenerated SDK bindings, cmux-app.d.ts and resource descriptor
  hashes follow; optchat-chief's RenameWorkspaceRequest literal gains the
  new fields.
- The app's DaemonCapabilities moves workspace-name-source-v1 from
  unservedByBundledDaemon to optional, since the bundled daemon now serves
  it.

Carved from #20710 onto current feat-cmux-next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(next): project workspace name provenance like the journal states it (hq#1957)

feat-cmux-next tests that landed after #20710's base failed on this slice:

- batch_close and resource_effects: the journal decorates every stored
  workspace with extra.name_source/name_revision, but the projected
  workspace values had no extra, so the public fold never matched and every
  commit restated every workspace. Every workspace value builder (deltas,
  full and scoped projection, normalized registry deltas, resource
  snapshots) now carries the same extra.
- key_close_cannot_rebind_a_live_durable_workspace_identity: creating a
  workspace at a key bound to another live workspace hit the name fence
  (revision.conflict) before the identity check. The name fence now
  applies only to the same workspace (numeric id), and the resource store
  checks identity before the name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(next): workspace_resource_upsert lives with workspace renames (hq#1957)

Its name provenance parameter pushed mux.rs past the 1000-line god-file
budget (check-no-godfiles.sh); the helper moves to mux/workspace_rename.rs
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

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

* fix(cmux-next): restore #21369 and #21384, undone by the Chief Dock squash (#21690)

* test(cmux-next): older history is asked for once per page

Fails on feat-cmux-next: a streaming commit near the top asks for the same
older page again, because 50a0c60 (#21194) restored the effect #21369
had keyed on the first row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(cmux-next): avoid duplicate older-history loads (#21369)

(cherry picked from commit 8f6f9cb)

* perf(cmux-next): gate find highlight repaints by transcript changes (#21384)

* perf(cmux-next): gate find highlight repaints by transcript changes

* fix(cmux-next): preserve find repaint triggers after fold reveals

* style(acpmux): format find highlight effect

* test(next): carry apps empty-state selector through PR 21384

* test(next): carry apps empty-state selector through find highlight PR

(cherry picked from commit afec4de)

---------

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

* acpmux: no-ask start modes by inherit or an approved preset (#21666)

* feat(acpmux): start a no-ask mode from a session in it or an approved preset

A `session/new` that asks for a mode that doesn't ask, without the
person, now has two ways through:

- Inherit: a caller that is itself an acpmux session in that exact mode on
  the same harness family. Each harness gets ACPMUX_SESSION_TOKEN, an HMAC
  of its session id under a per-run in-memory secret, and
  ACPMUX_SESSION_TOKEN_SOCKET. The token is read only from a unix socket
  connection's first `initialize`. The caller's mode is read at request
  time.
- Approved preset: `_acpmux/presets {name, approveMode: true}` from the
  person records the sha256 of the preset's contents. The approval counts
  only while the resolve and the spawn both used those exact contents, for
  the preset's own mode, with no request overrides other than a folder.

A start mode never applies to an adopt without the person, and a proven
caller session that asks never gets the full-access default for its
chats. The refusal names where to approve (cmux NEXT on this host, or the
preset to approve there once), with {reason, host, mode, preset} in
error.data. Web and peer clients never get the host name.

person_auto_start.rs runs in the hermetic test environment, and its
person-key pipe is close-on-exec so parallel tests' daemons never hold
each other's key pipe open.

Carved from #20653.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(acpmux): an approved preset with a route, and env that names the caller token

Red on 116ab01:
- an approved preset plus a request `route` passes the start gate and
  reaches the handler ("no route"), where it should need the person;
- a harness profile env that sets ACPMUX_SESSION_TOKEN reaches the agent
  in place of acpmux's caller token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(acpmux): a route counts as an override, env never sets the caller token

Review fixes for the start gate:
- A request `route` on an approved preset now needs the person, like its
  own model, effort, policy or env: a route swaps the provider base URL
  and keys.
- A profile's, preset's or request's env no longer replaces
  ACPMUX_SESSION_TOKEN or ACPMUX_SESSION_TOKEN_SOCKET. That env is
  layered after `caller_env`, so the spawn drops both keys from it.
- The CLI's daemon readiness pipe is close-on-exec from creation
  (`std::io::pipe`, pipe2(O_CLOEXEC) where the OS has it) instead of a
  `pipe` then `fcntl`, which raced with a fork on another thread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

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

* cmux-next: the static status look everywhere; indicators name their state (cx-sya4n) (#21664)

* test(cmux-next): the static look reaches tabs, the attention ring, the agent pane and every label (cx-sya4n)

Red: tabs keep their icon set's marks and colored dots, the attention
ring blinks, the agent pane's theme has no statusIndicators look and
indicators have no tooltip or VoiceOver label (compile failure).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-next: the static status look everywhere, and every indicator names its state (cx-sya4n)

Tabs draw the static look's still marks (the badge too, never a dot
told apart by color alone); the pane attention ring is the steady ring
under Static; the agent pane's theme sends statusIndicators (static
under Static or Reduce Motion). Every StatusIndicatorView's tooltip and
VoiceOver label is the state's short name (StatusIndicatorState
.shortLabel, nine strings in every catalog locale), and a tab's
needs-input VoiceOver text names the blocked kind. Carved from #20511.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

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

* fix(cmux-next): restore the URL chip hover card styles lost in the Chief Dock squash (#21699)

* test(cmux-next): every URL chip hover card class has a rule

Fails on feat-cmux-next: 50a0c60 (#21194) dropped the .cv-link-preview
rules #21199 added, while LinkChips.tsx still renders those classes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cmux-next): restore the URL chip hover card styles (#21199)

50a0c60 (#21194, a stale-base squash) removed the nine .cv-link-preview
rules while LinkChips.tsx still renders them, so the card drew unstyled.
Restored byte for byte from 5106cc9.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

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

* acpmux: a saved model or effort pick reaches open New Tab pages (#21677)

* test(acpmux): a saved model or effort pick is announced to watchers

A New Tab prewarmed before a pick draws the old start effort and the chat
then starts on the pick. Fails: note_pick saves the new start default but
sends no _acpmux/harnesses_changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(acpmux): announce a saved model or effort pick to watchers

note_pick makes the pick the harness's start in _acpmux/harnesses, but
told nobody, so a New Tab prewarmed before the pick kept drawing the old
start effort until the chat started on the pick. It now sends
_acpmux/harnesses_changed, as a saved mode pick already announces
start_mode_changed; pages re-read the catalog on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

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

* cmux-next: keep awake lives in its own target (CmuxNextKeepAwake)

Review of #21730: CmuxNextApp only shrinks (check-app-size.sh), so
KeepAwakePolicy, PowerSourceMonitor and KeepAwakeService move to a new
CmuxNextKeepAwake target with their tests. The App keeps a one-line start
and the Keep Mac Awake binding; its source is 10 lines under the base.

The power-source callback now hops to the main actor with a Task instead of
MainActor.assumeIsolated (a banned crash class).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-next: route test knows the keep-awake tests; one policy write per change

Review of #21730: a CmuxNextSidebar change now also selects
CmuxNextKeepAwakeTests (CmuxNextKeepAwake depends on CmuxNextBridge), so
test_a_shared_module_selects_its_dependents lists it. KeepAwakeService sets
the setting and the agents-working fact in one policy write, so the hold
cannot drop between the two.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com>
Co-authored-by: safe-push <safe-push@cmux.local>
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Already landed: the no-ask start modes as #21666 and the peer pending-prompt relay as #21642. Closing.

This branch was previously deployed

2 inactive deployments
cmux-tui-merge-tree — 7f2b6a38 Deployed Oct 15, 2026 by manaflow-cmux-pr-autofix[bot] via publish the merge tree #1267
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants