cmux NEXT: a new local chat carries its workspace to acpmux, so workspace subagent limits apply
Leo: "I want cmux next to be able to increase or decrease subagent amounts ad hoc, with the acpmux daemon and all that. Slide it, even per workspace. Right-click, edit subagent count."
A chat started in the app now tells acpmux which cmux workspace it belongs to, so a workspace's subagent limit applies to it.
acpmux (#23963) reads a workspace's maxRunning only through a chat's meta.workspace_id, which it records from _meta.acpmux.workspaceId on session/new (or from CMUX_WORKSPACE_ID in a unix-socket session env). Chats started from a terminal or the Chief already carry it. Chats started in the app's agent pane didn't: direct.ts sends no workspace id, the pane's params rule refuses the key, and AcpmuxPathPolicy filled in only cwd. So a workspace limit set from the app was saved but changed nothing for those chats (#24061 left out Workspace Subagent Limit… for this reason).
With this PR, the pane host adds the pane's own workspace to each local session/new:
AgentTabStore.localWorkspaceKey(of:)returns the durable key of the workspace that holds the tab, but only when that workspace's daemon is this Mac's (daemonForTab(key)?.isLocal).wireAgentFolderhands it to the transport asworkspaceID.AgentPaneTransport.checkPathsInScopeputs it inAcpmuxPathPolicy.Scope.workspaceID.checkNowcallsstampWorkspace, which writes_meta.acpmux.workspaceIdover anything already there.- The value is lowercased and must be a dashed UUID of ASCII hex digits, the same rule as acpmux's
session_env::is_workspace_id. Any other key (ahandle:stand-in, an empty or legacy key) is dropped, not sent, so acpmux can't refuse the start withworkspace.value_refused. - Its sub-sessions get the same workspace from acpmux, which already copies it (
hub/subsessions.rs~679), so the limit covers the chat's whole family.
Security (request-policy change; CLAUDE.md "Remote CLI relay")
This adds no method and allows no new key from the page. The host writes one field into a frame that the page already sends.
- The page can't choose the id.
knownParams["session/new"].acpmuxstill has noworkspaceId, so P1 (breaksParamsRule) refuses a page frame that names one before any other rule runs (intentInvalid). The new comment inAcpmuxPaneMethods+Params.swiftsays the key must stay out. As a second guard,stampWorkspacewrites over the key or removes it whenever the frame reaches it. The testthePageCannotNameAWorkspacechecks both. - Only trusted scope. The value comes from
AgentTabStorethrough a host closure (transport.workspaceID), the same path the pane's roots and agent home already take. It never comes from page state. It is resolved for the tab's own key (resolve(provisional)), so a pane can only name the workspace that holds it. - Local only.
- A start over a remote wire (
remoteWire) never reaches the path check, so it gets no stamp. - A start that names a non-empty
peergets none, and any value already there is removed: a peer's workspaces aren't this Mac's. - A tab whose workspace lives on another machine's daemon gets nil.
- acpmux itself clears
workspace_idwhen a chat transfers to another machine (hub/transfer_guard.rs~335).
- A start over a remote wire (
- No command-bearing params. The field is a validated UUID string. In acpmux it isn't session env and reaches no agent process (
agent/command.rsremovesACPMUX_AGENT_WORKSPACEbefore the harness starts). It can't start, read or reach any object. Its only effects are the limit lookup and the display pick below. - Local state. Nothing new is shown to the page. The page already sees its chat's summary, including
subsessionLimits.
Computer use display: what changes
acpmux reads meta.workspace_id in five places: the sub-session maxRunning lookup (subsessions.rs ~461), sub-session and side-chat starts that copy it (subsessions.rs ~679, btw.rs ~117), forks that inherit it (hub/fork.rs ~63), and agent_displays::mark at spawn and fork (hub/spawn.rs ~139, hub/fork.rs ~64).
mark puts the workspace in the spawn env only when the daemon runs with CMUX_AGENT_DISPLAYS=per-workspace. AgentTools::scoped picks a display only when agent_displays::per_workspace() is on. Only the Linux cloud machine image turns that mode on. Nothing in this repo sets it, and the Mac app's local acpmux runs with it off.
- On this Mac (the only daemon that gets the stamp): nothing changes.
markonly removes the key,AgentTools.displaysisNone, andscopedreturns before the display code. An app chat keeps the samecmux-cuaserver with noCMUX_CUA_DISPLAY, exactly as before. - If a daemon that gets the stamp ran in per-workspace mode: an app chat would change from no computer use server at all (fail closed: no workspace) to
cmux-cuawithCMUX_CUA_DISPLAY=:N.:Nis the workspace's own display from<ACPMUX_HOME>/agent-displays/displays.json, shared with that workspace's terminal and Chief chats. The v2 bridge would be dropped, as for every session in that mode. That is the mode's design: one display per workspace, and an agent never drives another workspace's display. It's the right result. It can't happen from the app today, because the only daemons in that mode are cloud machines, and their chats are remote-wire starts that this PR leaves unstamped. - Sub-sessions, side chats and forks of an app chat now inherit its workspace. In per-workspace mode they would share their parent's display, which is also right.
Known limit: acpmux records the workspace when the chat is created. A chat tab that later moves to another workspace keeps the first one's limit. A chat started before this change has no workspace, so it still follows its own and the global limit.
Changelog
- Fixed: a workspace's subagent limit now applies to chats started in the app, not only to chats started from a terminal.
Observed / Not run
cmux-ci compile-check --ref 98ccabba358a6e7ebc041c8f6ec84a7562d7688b --apppassed (fleet, 47 s, job 2e07e289ca2259944a9e7394).AcpmuxPathPolicyWorkspaceTests(new: stamp, lowercase, page value refused by P1 and replaced or dropped, invalid keys not sent, peer and other methods unstamped) and the existing path-policy suitesAgentPaneBrowsedFolderRootTests,AgentPanePickedFolderRootTests,AgentPaneSendPickedFolderPromptTests: submitted to the fleet (cmux-ci runpackage-test-lane suite, step 3345b0c03cab14fbc7e2a7d6), still queued behind the fleet's suite backlog (about 300 suite steps ahead, 1.5 h in queue) as of this edit. This PR's CI is the fast tier, which doesn't run the macOS package tests, so these suites haven't run yet.prepush-gate.py(base 8cdf8ca03e4, head 98ccabba358): no new violations.- The merge with the current feat-cmux-next (31afd278979) is clean (
git merge-tree). #23990 (same lane) edits the same Params file in a different table and doesn't conflict. - PR CI reds that aren't this PR's: cmux-next checks ("Fail on any red check"), release-notary and its Guard status are red at the same steps on #24061, #23990 and #24225 (other heads, same base). The prepush gate's matching scanners (concurrency, crash safety, string tables) found no new violations here. risk evidence is the
risk:highhold, waiting on review. - Not run against a live acpmux. Not run: an end-to-end check that a workspace limit set by
_acpmux/subsession/limits {workspaceId}holds an app chat's sub-sessions. acpmux's side of that is covered by #23963's tests.
No capture: no UI changes. The app adds a field to an acpmux request that no surface shows.
Agent session: cosmic-gray
đŸ¤– Generated with Claude Code