Skip to content

acpmux: turn cap, so a session taking many agents' messages can end its turn - #27520

Merged
teamleaderleo merged 12 commits into
feat-cmux-nextfrom
acpmux-turn-cap
Oct 29, 2026
Merged

teamleaderleo merged 12 commits into
feat-cmux-nextfrom
acpmux-turn-cap

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Why

Leo, 2026-10-29: "What are we gonna do with these 200 fing messages? You never end a fing turn. As soon as it comes in, it fing, like, occupies you for another fing turn." and "you can't you just deal with this? Like ACPmux can't be this stuff. How do swarms work?"

A coordinator session gets messages from 30+ lanes. Each one steered into its running turn, so the turn never ended (one ran about 17 h). This is the turn cap from the swarm-inbox plan (P3): past the cap, a turn stops taking other agents' steers, so it can finish.

What

  • Setting: SessionMeta.turnCapMs (ms; 0 turns it off; absent means the default).
    • Set and read with _acpmux/set_turn_cap {sessionId, turnCapMs?}. A number sets it, null clears it, and leaving it out reads it. The reply is {sessionId, turnCapMs, effectiveTurnCapMs}.
    • The CLI is acpmux session turn-cap NAME [--minutes N | --off | --clear].
    • Only the person can write it: the app, or the local unix socket with no agent caller token, proven or not. A session's agent can't lift any cap, including its own.
    • A fork doesn't copy it.
  • Default: none for ordinary sessions. hub::turn_cap::default_turn_cap_ms(&SessionMeta) is the single place that links the coordinator setting to a default cap (COORDINATOR_TURN_CAP_MS, 20 min). It returns None until the coordinator setting (P2) lands; that PR changes the body of this one function.
  • Past the cap (hub/turn_cap.rs, wired in turns.rs run_prompt_joined):
    • Agent messages to the running turn don't steer. "Agent message" means any prompt digest::agent_sender counts as another agent's: agent_message, sub-session messages, lane reports, or a proven caller.
    • They wait in the queue. The next turn takes them together through the existing digest join (digest.rs).
    • An agent message sent with interrupt waits like the rest and stops nothing.
    • A steerOnly agent message can't wait, so it is refused steer.unavailable with data.turnCap: true.
    • The person's own steers still go in. So do the queue's Send now (PromptOptions::send_now, also for a held agent message), forum pushes, offer results and auto-resume.
  • Notice: the hub steers one notice into the capped turn, once per turn:
    • Text: "Turn cap reached (N min): finish the current step and end your turn; your inbox digest comes next turn."
    • Provenance {kind: "turn_cap", turnId, capMs}, hub-set only.
    • It is sent on the first agent message past the cap. If an agent message was already queued under the cap, the cap's timer sends it at the cap instead.
    • A turn with nothing waiting gets no notice.
    • The notice goes only into the turn it names. turn_cap::notice_reaches checks this in steer_turn before the notice is logged. agent_prompt_hook (bound) checks again while it holds input_handoff, which every prompt write takes, so no later turn's prompt can be written between the check and the notice.
    • A notice for a turn that has ended never reaches the next one. One that was logged but never written leaves steer_withdrawn in the log.
    • The turn the notice went into is saved in the session's meta (turnCapNoticed). It is set only while that turn still runs, so an attempt for a turn that has ended never replaces a later turn's marker.
    • The notice is sent with resend, so the log also dedupes it: a turn adopted after a restart gets no second notice, even if the marker's save was lost.
    • An agent message that is queued as the turn crosses its cap re-checks the cap after it is queued. The timer's earlier look at the queue can't leave the turn without its notice.
  • The hub never cancels the turn.
  • Measured from: the daemon turn's startedAt. Once the turn-boundary change (P0, harness end_turn ends the daemon turn) lands, the cap covers only one genuinely long harness turn. This PR doesn't touch P0's functions.

Timer: stop condition and cleanup

  • What: one tokio task per session, armed in arm_turn_cap when a turn starts, and only when the session has a cap.
  • Stop condition: it sleeps once until the cap, checks once (is the same turn still running past its cap, with an agent message queued?), and ends. It never loops or re-arms itself.
  • Cleanup:
    • Its AbortHandle lives in Session::turn_cap_timer. The next turn's arm_turn_cap aborts the previous timer before it arms a new one.
    • It holds the hub and the session through Weak, so a closed session or a dropped hub makes it a no-op.
    • A turn that ends before the cap leaves the timer to wake once and exit.
    • A turn restored by adoption after a restart has no timer. Its messages are still held by the per-message check, which reads the restored startedAt.

Stack headroom

run_prompt_joined now awaits steer_turn through Box::pin. On base, the prompt future is already close to a debug test thread's stack limit. The turn cap's few extra lines pushed two hub_integration tests (steer_now::send_now_for_a_queued_prompt_steers_it_in_at_once, fork_through::btw::a_codex_side_session_runs_sandboxed_read_only_and_the_adapter_is_refused) into a stack overflow. Boxing the steer's future makes the prompt future smaller than it is on base. The cost is one allocation per steer.

Wire changes

  • docs/acpmux-schema.json: _acpmux/set_turn_cap, and the turn_cap provenance kind on user_message.
  • spec/acpmux-protocol.json and webviews/.../generated/methods.ts: regenerated with ACPMUX_PROTOCOL_BLESS=1 and bun run acpmux-protocol:codegen.
    • The regeneration also adds _acpmux/mesh_control, which base's committed IR was missing. protocol_ir_is_current fails that way on base too.
    • Those are the only two generated lines besides this PR's method.

Tests (red first)

hub/turn_cap_tests.rs uses the fake agent steering the way Claude Code does:

  • Under the cap, an agent message steers in, and no notice is sent.
  • Past the cap:
    • Agent messages are queued.
    • Exactly one notice goes in, into the capped turn.
    • A steer-only agent message is refused with the reason.
    • The person's steer still goes in.
    • The turn keeps running.
  • The held messages reach the next turn as one joined prompt, delivered once. The next turn is under its own cap again, and past it gets its own single notice (the cap state resets).
  • The timer sends the notice at the cap only when an agent message waits. A quiet turn past the cap gets none.
  • Past the cap, the queue's Send now still steers a held agent message in, and an agent's interrupt stops nothing.
  • A notice naming another turn is refused, and a saved notice marker (as a restart restores it) sends no second notice.
  • A notice the log holds is not sent again when its meta marker is lost.
  • A notice held after it is logged (by the copy_seam test seam), whose turn ends and the next one starts meanwhile, is refused, never written, and leaves steer_withdrawn.
  • Also: the setting's precedence (its own value wins, 0 turns it off) and the CLI's request shapes and wording.

Runs (fast checks on Blacksmith 32 vCPU Linux):

No capture: daemon-only change (the acpmux turn cap). Nothing the app draws changes; the generated webview method list gains one untyped entry.

Risk

  • Medium. It changes steering for sessions that have a cap.
  • Stack headroom: steer_turn is boxed inside the prompt future (see above), and base's acpmux: run a prompt's turn on the heap so awaiting it cannot overflow a stack #27577 boxes run_prompt_joined itself. No session has one by default, so behaviour changes only after the person sets turnCapMs (or after P2 turns it on for coordinators).
  • The new check on the prompt path is one meta read and one turn read, and only while a turn runs.
  • What could go wrong: a capped session's agent messages wait for the turn to end. If the agent ignores the notice, they wait until it does. That is the intent, and the person can still steer or stop the turn.

This PR is about 800 changed lines: one setting, its RPC and CLI, the hold, and its tests.

Agent session: agent session

🤖 Generated with Claude Code

teamleaderleo and others added 3 commits October 29, 2026 11:59
…red)

The turn cap's setting (SessionMeta.turnCapMs, _acpmux/set_turn_cap,
acpmux session turn-cap) and its tests, with the cap not yet applied to
prompts: the tests past the cap fail.

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

A session with a turn cap whose running turn is older than the cap takes
no other agent's steer: their messages wait in the queue and the next turn
takes them together. The person's own steers still go in. The hub steers
one notice into that turn, once, when an agent message waits, and never
cancels the turn itself.

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

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

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

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

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

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr27520 -f ref=acpmux-turn-cap -f platform=macos -f backend_mode=local

Gallery before/after for a405b91: 16 states rendered, 1 images differ, 2301 more touched states not rendered. Images are in the run's artifact, not on this public PR.

gh run download 40960097711 -R manaflow-ai/cmux -n gallery-advisory-27520-a405b91aaea3a6af95776cace63eb6bd005f8d3f (then read gallery-review.json)

States
State Theme Status Diff Picked by
agent-pane.auth-health/signed-in Apple System Colors Light unchanged 0% imports methods.ts (2 hops)
agent-pane.auth-health/signed-in Apple System Colors unchanged 0% imports methods.ts (2 hops)
agent-pane.auth-health/signed-out Apple System Colors Light unchanged 0% imports methods.ts (2 hops)
agent-pane.auth-health/signed-out Apple System Colors unchanged 0% imports methods.ts (2 hops)
agent-pane.auth-health/expired Apple System Colors Light unchanged 0% imports methods.ts (2 hops)
agent-pane.auth-health/expired Apple System Colors unchanged 0% imports methods.ts (2 hops)
agent-pane.auth-health/unknown Apple System Colors Light unchanged 0% imports methods.ts (2 hops)
agent-pane.auth-health/unknown Apple System Colors unchanged 0% imports methods.ts (2 hops)
agent-pane.chat-width/standard Apple System Colors Light unchanged 0% imports methods.ts (2 hops)
agent-pane.chat-width/standard Apple System Colors unchanged 0% imports methods.ts (2 hops)
agent-pane.chat-width/wide Apple System Colors Light unchanged 0% imports methods.ts (2 hops)
agent-pane.chat-width/wide Apple System Colors unchanged 0% imports methods.ts (2 hops)
agent-pane.chat-width/wide-blocks Apple System Colors Light unchanged 0% imports methods.ts (2 hops)
agent-pane.chat-width/wide-blocks Apple System Colors unchanged 0% imports methods.ts (2 hops)
agent-pane.composer-edge/short-edge Apple System Colors changed 0.0051% imports methods.ts (2 hops)
agent-pane.composer-edge/short-edge Apple System Colors Light unchanged 0% imports methods.ts (2 hops)
agent-pane.composer-edge/wide-edge Apple System Colors Light unchanged 0% imports methods.ts (2 hops)
agent-pane.composer-edge/wide-edge Apple System Colors unchanged 0% imports methods.ts (2 hops)
agent-pane.composer-edge/no-edge Apple System Colors Light unchanged 0% imports methods.ts (2 hops)
agent-pane.composer-edge/no-edge Apple System Colors unchanged 0% imports methods.ts (2 hops)
agent-pane.composer-floats/scrolled-to-end Apple System Colors Light unchanged 0% imports methods.ts (2 hops)
agent-pane.composer-floats/scrolled-to-end Apple System Colors unchanged 0% imports methods.ts (2 hops)
agent-pane.composer-floats/composer-grown Apple System Colors Light unchanged 0% imports methods.ts (2 hops)
agent-pane.composer-floats/composer-grown Apple System Colors unchanged 0% imports methods.ts (2 hops)
agent-pane.continuation-spacing/shipped Apple System Colors Light unchanged 0% imports methods.ts (2 hops)
agent-pane.continuation-spacing/shipped Apple System Colors unchanged 0% imports methods.ts (2 hops)
agent-pane.continuation-spacing/two-thirds Apple System Colors Light unchanged 0% imports methods.ts (2 hops)
agent-pane.continuation-spacing/two-thirds Apple System Colors unchanged 0% imports methods.ts (2 hops)
agent-pane.continuation-spacing/half-tucked Apple System Colors Light unchanged 0% imports methods.ts (2 hops)
agent-pane.continuation-spacing/half-tucked Apple System Colors unchanged 0% imports methods.ts (2 hops)
agent-pane.continuation-spacing/before Apple System Colors Light unchanged 0% imports methods.ts (2 hops)
agent-pane.continuation-spacing/before Apple System Colors unchanged 0% imports methods.ts (2 hops)

Pick other states with a Gallery: entry.id, entry.id/variant line in the PR body.

red: cmux-next checks (god files, concurrency, crash safety, l10n); base red: the same failure on the base 1c243fe; does not block

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Owned by an agent session · session

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

Copy link
Copy Markdown
Collaborator Author

Bead cx-7ljsjg created for this PR by the cmux coordinator.

@blacksmith-sh

blacksmith-sh Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Found 6 test failures on Blacksmith runners:

Failures

Test View Logs
/tmp/tmpxql0kp1c/summary-fail.json (live):/summary-fail.json (live): View Logs
/tmp/tmpznds3q4k/summary-missing.json (live):/summary-missing.json (live): View Logs
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows/
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows
View Logs
[new_workspaces] the window draws no frames/[new_workspaces] the window draws no frames View Logs
[tabs] debug.tab_switch timed 0 of 40 switches/
[tabs] debug.tab_switch timed 0 of 40 switches
View Logs
[tabs] the window draws no frames/[tabs] the window draws no frames View Logs

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

The cap's sender lookup and refusal text lived in run_prompt_joined, whose
future grew past the debug test thread's stack in
steer_now::send_now_for_a_queued_prompt_steers_it_in_at_once.

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

cursor Bot commented Oct 29, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

…, restarts, stale callers

- The queue's Send now of a held agent message still steers in past the cap.
- An agent's interrupt held past the cap waits without stopping the turn.
- The notice goes only into the turn it names, checked before it is logged
  and again before it is written.
- The turn the notice went into is saved in the session's meta, so a turn a
  restart adopts gets no second notice.
- A local connection with a caller token this daemon does not know cannot
  write the cap.
- The timer test checks "nothing before the cap" only while the cap is ahead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
The prompt future sits near a debug test thread's stack limit on base; the
turn cap's additions pushed two hub_integration tests past it.

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

cursor Bot commented Oct 29, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

teamleaderleo and others added 2 commits October 29, 2026 13:26
…-cap

# Conflicts:
#	cmux-tui/crates/acpmux/docs/acpmux-schema.json
…is written

The notice's turn is checked again under the input handoff, which every
prompt's write takes, so no later turn's prompt is written between the
check and the notice. A notice logged but never written leaves
steer_withdrawn in the log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
teamleaderleo and others added 3 commits October 29, 2026 14:08
…dependent test

- The notice marker is set only while the turn it names still runs, read
  under the meta lock, so an attempt for an ended turn never replaces a
  later turn's marker.
- The timer test reads only logged notices before the cap: a notice may be
  logged and still on its way on a slow machine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
… the notice's log dedupe

- An agent message queued as the running turn crosses its cap re-checks the
  cap after it is queued, so the timer's earlier look at the queue cannot
  leave the turn without its notice.
- The notice goes with resend: a notice the log already holds (its meta
  marker's save was lost) is answered from the log, never sent again.

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

@teamleaderleo teamleaderleo left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review (codex gpt-6-astra high fast, independent review): SHIP at a405b91

Five rounds. Rounds 1 and 2 were BLOCK (the notice could reach a later turn; an agent's interrupt past the cap could stop the turn), and both were fixed. Rounds 3, 4 and 5 were SHIP.

Round 5's two should-fixes are left for a follow-up. Each can lose only the notice, never a held message:

  • An internal agent prompt that has no acceptance callback and crosses the deadline doesn't re-check the cap.
  • A crash between saving the notice marker and logging the notice leaves that turn without one.

Reds at this head: only "No new SwiftUI" inside cmux-next checks. It flags CmuxNextHome/ChatColors/HomeChatColors{Panel,View}.swift and fails identically on base 1c243fe. This PR changes no Swift.

Tests: https://github.com/manaflow-ai/cmux/actions/runs/40960102893

  • hub_integration fails the same 2 tests as base.
  • durable_agents passes 124/124, as on base.
  • The turn cap suite passes 3x.

@teamleaderleo
teamleaderleo merged commit d530240 into feat-cmux-next Oct 29, 2026
177 of 182 checks passed
@teamleaderleo
teamleaderleo deleted the acpmux-turn-cap branch October 29, 2026 22:08
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

lawrencecchen added a commit that referenced this pull request Oct 30, 2026
- tests/agent_tools.rs: the background-shell hook test starts acpmux through
  daemon_env (hermetic helper); #26447 tripped hermetic_daemon_guard.
- hub/turn_cap.rs: its tests gate turns with libc::mkfifo, so they are Unix-only;
  test (windows) compiles again (#27520).
- 12 new JSON index writes/reads and one slice (#26447, #27512, #26108) use
  JsonSet::set / Value::get / slice::get; panic ratchet back to baseline 697.

This branch had an error being deployed

1 failed and 2 inactive deployments
sentry-dsyms — a405b91a Deployed Oct 29, 2026 by teamleaderleo via upload cmux-tui dSYMs to Sentry #78477
artifacts — a405b91a Deployed Oct 29, 2026 by teamleaderleo via publish to R2 #78477
ci-routing-agent — a405b91a Deployed Oct 29, 2026 by teamleaderleo via cmux-tui tree preflight #78477
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant