Repository navigation
acpmux: pause two agents that keep messaging each other without progress - #21975
Conversation
…e paused Red: sessions_that_message_back_and_forth_without_progress_are_paused and a_sender_repeating_the_same_message_is_paused fail on base (the sixth exchange and the third near-identical message are delivered); sessions_whose_turns_do_work_are_never_paused passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every agent message (a session/prompt with an agent_message provenance)
passes Hub::prompt_with's pair check (hub/agent_pairs.rs). The pair of
sender and receiver trips after 6 exchanges back and forth, or 3
near-identical messages from one sender, with no tool call or file
change by the receiving session in between. A tripped pair holds its
messages (agent_pair.paused refusal to the sender), records
agent_pair_paused in both transcripts, marks both chats unread and fires
notify_command. _acpmux/agent_pair_resume, or the person prompting
either session, delivers what it held in order.
Thresholds come from ACPMUX_AGENT_PAIR_{ALTERNATIONS,REPEATS,SIMILARITY};
the Debug Settings toggle agent.pairGuard (default on) sets
ACPMUX_AGENT_PAIR_GUARD=0 when off.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This comment has been minimized.
This comment has been minimized.
|
Visual preview of 51294c4: in the maintainers' private review feed. |
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
Unclear: CI failed on
Failures:
Matched log linesNot re-run automatically: Written by red: web / Web status; base red: the same failure on the base 184c4e5; does not block
Agents: |
|
Agents: |
|
Bead |
Red at this commit (cargo test -p acpmux --test hub_integration agent_pairs; cargo test -p acpmux --lib agent_pairs): - a_sender_that_is_no_session_is_never_paused: a 'cli' sender is paused. - a_worker_doing_work_between_alike_reports_is_never_paused: the sender's own tool calls never reset the pair. - an_agent_cannot_resume_its_own_pair: a non-person client resumes. - held_messages_survive_a_restart: a restart forgets the pause. - a_refused_message_is_not_counted: refused retries trip the guard. - sessions_that_message_back_and_forth...: trips at 6, not 10. - only_a_whole_messaging_call_is_not_progress: substring match. a_new_message_never_overtakes_held_ones passes here (the race rarely shows). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Only a sender that is a session of this daemon counts; the person's 'cli', terminals and other actors pass untouched. - Progress in either session since the last exchange resets the pair. - Only a connection that proved the person key resumes, by request or by prompting; a peer's prompt never counts as the person. - agent_pair_held carries the whole prompt; restore_queue rebuilds the pauses after a restart, and a release resends with resend so the log answers one that ran. - A resumed pair drains in order: messages arriving meanwhile wait. - An exchange counts only when the receiver accepts it. - Messaging calls match MCP tool names exactly, and a shell call only when the whole command is cmux agent message/inbox. - Held messages keep steerOnly and interrupt; a pair holds at most 50. - The alternation default is 10. Green: cargo test -p acpmux --test hub_integration agent_pairs (11 pass); cargo test -p acpmux --lib agent_pairs (5 pass). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Orphaned: no activity for 13 h, and no lane holds a lease on it. The orphan sweep could not merge |
# Conflicts: # Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneTunables.swift
[Medium impact] The guard affects agent-to-agent delivery across acpmux workflows. The PR needs fixes before merging because concurrent sends can escape the limits, restarts can undo a pause, and held messages can be lost or run after withdrawal. Findings
SummaryAdds a guard that holds runaway messages between two agent sessions until the person resumes them.
Diagram%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Agent message] --> B{Pair guard}
B -->|Below limits| C[Receiver accepts prompt]
C --> D[Count exchange]
B -->|Trips or already paused| E[Save held prompt]
E --> F[Person resumes pair]
F --> G[Deliver oldest held prompt]
G --> H{Accepted?}
H -->|Yes| I[Record release]
I --> G
H -->|First failure| J[Pause again]
H -->|Second failure| K[Record drop]
Reviews (12) · Last reviewed commit: "Merge branch 'feat-cmux-next' into chat/..." · Reviewed by Greptile |
| "agent_pair_resumed" => { | ||
| if let Some(peer) = rec.msg.get("peer").and_then(Value::as_str) { | ||
| held.remove(peer); |
There was a problem hiding this comment.
Held messages disappear after resume
agent_pair_resumed clears all earlier held messages during restore, but it is written before drain_pair delivers them. A daemon restart during the drain therefore loses every message not yet accepted. The drain also drops messages on pre-acceptance errors and only logs a warning.
Keep each message pending until its acceptance is recorded. Restore or retain messages that never reached that point.
There was a problem hiding this comment.
Fixed. agent_pair_resumed no longer clears held messages on restore. A release logs agent_pair_released only once the receiver accepts the message, and restore keeps every held message that never reached the session (no queued / user_message with its id) and that no released or dropped record settled. A failed release re-pauses the pair (release_failed); a second failure drops it and logs agent_pair_dropped in both chats.
| for h in list { | ||
| if !paused.held.iter().any(|x| x.prompt_id == h.prompt_id) { | ||
| paused.held.push_back(h); |
There was a problem hiding this comment.
Restart changes held message order
restore_agent_pairs appends each receiver's messages to the shared pair queue separately. If messages were held in both directions, a restart groups them by receiver instead of preserving arrival order. Alternating held messages become all messages to one session followed by all messages to the other.
Save a pair-wide order and merge both transcripts by that order before draining.
There was a problem hiding this comment.
Fixed. Each held message carries a pair-wide order, taken under the pair lock. Restore merges both logs by it. held_messages_keep_their_order_across_a_restart covers messages held in both directions.
| if ["&&", "||", ";", "|", "\n", "`", "$(", ">", "<", "&"].iter().any(|op| command.contains(op)) | ||
| { | ||
| return false; |
There was a problem hiding this comment.
Quoted punctuation bypasses the guard
messaging_command treats shell characters inside quoted message text as extra work. A single call such as cmux agent message acp:b 'ready; waiting' returns false because its body contains ;. Every such send then counts as progress and resets the pair, so two agents can keep exchanging these messages forever.
Parse quoting and escapes before deciding whether the command contains another shell operation.
There was a problem hiding this comment.
Fixed in an earlier commit. messaging_command parses quotes and escapes now, and only_a_whole_messaging_call_is_not_progress covers 'ready; waiting', a double-quoted && | body and escaped punctuation.
| pair.paused = | ||
| Some(Paused { held: VecDeque::from([held.clone()]), draining: false }); | ||
| Hold::Tripped(trip) |
There was a problem hiding this comment.
Refused attempts pause both agents
The threshold check pauses the pair before run_prompt checks whether the receiver can accept the message. After two accepted identical messages, a third sent with steer: true and steerOnly: true into an idle receiver pauses the pair instead of returning steer.unavailable. An attempt that should be refused now forces the user to resume both agents.
Check whether the message can be accepted before committing the pause.
There was a problem hiding this comment.
Fixed. The pair now trips only after the receiver can accept the message. a_refused_attempt_never_pauses_the_pair sends a steer-only third message into an idle receiver and checks that it gets steer.unavailable and pauses nothing.
| tracing::warn!(session = %session.id, "held agent message: {}", e.message); | ||
| } | ||
| }); | ||
| let _ = tokio::time::timeout(RELEASE_WAIT, rx).await; |
There was a problem hiding this comment.
Slow releases lose message order
drain_pair ignores the acceptance timeout and leaves the delivery task running. If a held prompt waits more than ten seconds before acceptance, the drain sends later messages anyway and can remove the pair while that prompt still waits. A prompt can wait on turn_lock before acceptance while a later prompt sees queued work and is accepted immediately.
Do not advance until acceptance succeeds. On timeout, retain the message and stop or cancel the outstanding delivery.
There was a problem hiding this comment.
Fixed. The drain has no timeout now. It waits until each held message is accepted, or its delivery fails, before it sends the next one, and the pair stays draining until then. New messages still wait behind it.
| fn progressed_since(&self, session: &str, mark: u64) -> bool { | ||
| let mut progressed = false; | ||
| let _ = self.store.scan(session, mark, &mut |rec| { | ||
| progressed = progress_record(&rec); | ||
| !progressed | ||
| }); |
There was a problem hiding this comment.
Messages reread complete transcripts
progressed_since adds up to two synchronous transcript scans to every exchange. The file store reads and decodes every earlier record before applying mark, so even a recent mark does not bound the work. Long-lived sessions now reread their full histories for each agent message.
Track the latest progress sequence as events arrive, or use an indexed read that starts at the requested sequence.
There was a problem hiding this comment.
Fixed. progressed_since scans no transcripts now. Each session keeps a last_progress sequence, updated as its events arrive, and the check reads that.
|
|
Failing tests first: a parent and its sub-session messaging through cmux-subsessions send_message are never paused, because the guard only counts agent_message provenance and the subsessions path sends subsession_message. A held send_message with interrupt also stops the receiver's running turn before the guard holds it. The tool-name list still names the closed cmux-sessions tools. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The agent pair guard now counts subsession_message prompts (cmux-subsessions send_message and start_session's first prompt) as agent messages, so a parent and child looping through send_message, or mixing it with cmux agent message / agents_chat_send, are paused like any other pair. Messaging tools are matched by server-qualified name: every cmux-subsessions tool except start_session, plus agents_chat_send and agents_chat_read, is not progress; the closed cmux-sessions names are gone, and another server's send_message is still work. send_message with interrupt stops the receiver's turn only after the message is accepted, so a held message no longer cancels it. A restart now restores every held message that never reached the receiver instead of dropping the ones a resume had not delivered yet; a release the receiver refuses is recorded agent_pair_dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- schemas/defaults/cmux-next.json Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/39357783315
…lead/21975 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bugbot needs on-demand usage enabledBugbot 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. |
|
|
|
|
* feat(agent-pane): show acpmux agent pair pauses in the transcript acpmux's agent pair guard (#21975) records agent_pair_paused, agent_pair_held and agent_pair_resumed, which no client drew. A pause is now one quiet row naming the peer and why, with the held count and a Resume button that sends _acpmux/agent_pair_resume; after the resume it collapses to "Resumed messages with <peer>". Held messages never draw as prompts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(agent-pane): held message previews and delivered count on agent pair rows The pause row only counted held messages and the resumed line never said how many acpmux delivered. These tests expect a Show held messages disclosure listing each held preview and "Resumed messages with <peer>, <n> delivered". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(agent-pane): list held messages and count deliveries on agent pair rows A paused agent pair row now keeps each agent_pair_held preview and offers Show held messages, a disclosure that lists them under the row. The resumed line counts what acpmux delivered ("Resumed messages with Reviewer, 2 delivered") when agent_pair_resumed says so. The gallery entry gains a held-list variant. Three new strings cover all 21 pane locales. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(agent-pane): agent pair records #22309's review found unhandled A re-pause after a failed release drew "went back and forth 0 times" with nothing held, the cap read as back and forth, a resent held message counted twice, agent_pair_released and agent_pair_dropped were ignored, Resume named the selected chat during a switch hold, and a double click sent it twice. These tests cover each, plus the transcript row's chat.agentPair.resume path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(agent-pane): agent pair rows follow every record the guard writes A re-pause after a failed release (reason release_failed) is its own sentence and takes over what the pause before it still held; the cap (reason cap, with windowSecs) has its own plural sentence. Held messages are keyed by prompt id, so a resend counts once. agent_pair_released counts toward "N delivered" and agent_pair_dropped shows as "N not delivered". Live and reload replay the same records, so they match. resumeAgentPair now names the chat on screen (actingSessionId), the same method as #21975's. A second click before the row redraws sends nothing. The counts and a failed Resume are an <output> live region, the toggle points at its list with aria-controls, and the list clamps each preview and shows at most 20, then "and N more". Four new strings cover all 21 pane locales. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(agent-pane): the cap's sentence spells its window out Review asked for "exchanged {n} messages within 10 minutes with no message from you" rather than "sent {n} messages in 10m without you". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(agent-pane): the cap's sentence spells its window out "Paused messages with Reviewer: the two agents exchanged 21 messages within 10 minutes with no message from you". The window is an Intl.NumberFormat long unit in the pane language ("10 Minuten"), and agentPair.paused.cap is rewritten in all 21 locales. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * agent pair: every Arabic plural form keeps {n} gen-strings refused agentPair.dropped, moreHeld, paused.cap and resumedDelivered because their Arabic zero/one/two forms dropped the {n} placeholder, which leaves the pane's strings table without them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
owner: 01a15bd3-b49c-7d53-bea7-64f63a046a36 |
restore_queue now reads only QUEUE_KINDS (base): the agent pair records restore_agent_pairs needs are added to them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15bd3-b49c-7d53-bea7-64f63a046a36&host=cosmic-gray
A daemon on base starts with its logs loading in the background, and the pairs come back with them (restore_queue). A person's resume or own prompt in that window found no pair, and the restore then paused it again: both now wait for the logs first. person_prompt takes a bool, so no PromptOptions (not Sync) is held across the await. daemon.rs and hub/mod.rs were over the Rust file budget on base and this PR grew them: notify_loop moves whole to daemon/notify.rs, and the model probe list (set_probe_only, allow_probes, probes, probe_only_from_env) moves unchanged to hub/live_models.rs, which runs the probes. Both files end below their base size. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15bd3-b49c-7d53-bea7-64f63a046a36&host=cosmic-gray
subsessions.rs conflicted with base's sub-session titles (#22650), the full-access switch (#21896) and the queued interrupt (#21975): - start_reserved keeps this branch's settle_subsession (every refusal after session/new closes the child); base's title_subsession moves into settle_subsession, which gets the requested name. - settle_subsession takes base's cap: a parent still in full access after sessions.allowFullAccess was turned off does not pass it on (inherit = same family and not capped; a capped child starts at the person's start level). hold_subsession then checks it as before. - send_family_prompt keeps this branch's remote floor and base's interrupt_once_queued; subsession_send keeps the receiver check, then base's interrupt argument instead of the cancel before the send. subsessions.rs, turns.rs and hub/mod.rs are over the Rust file budget on base and this branch grew them; each now ends below its base size. Moved unchanged except for visibility: the folder resolution, settle_subsession, the close of a refused child and the receiver check to subsession_guard.rs; explain_start_failure from turns.rs to start_gate.rs; newest_first from mod.rs to session.rs (re-exported with prompt_text and short_text). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15bd3-b49c-7d53-bea7-64f63a046a36&host=cosmic-gray
|
Shipped in:
|
|
Shipped in:
|
Two agent sessions can message each other forever: nothing in acpmux stops a back-and-forth that makes no progress (prompt dedupe is by prompt id only, and #21858's
send_to_sessionmints a new id per call). This adds a guard that pauses such a pair until the person resumes it: a loop without progress, or more than 20 messages between the two within 10 minutes with no prompt from the person.What changes
Every agent message reaches a session through
Hub::prompt_with(hub/turns.rs) as asession/promptcarrying anagent_messageprovenance. The new checkHub::agent_pair_check(hub/agent_pairs.rs) runs there, before the prompt is recorded, so it covers:cmux agent messageand cmux-next: agents_chat_send and agents_chat_read, one agent prompts another chat (cx-ncc.96) #21647'sagents_chat_send, which go through the cmux daemon's delivery pump.send_to_sessionandstart_session, which callacpmux::deliver::deliverdirectly.Neither PR bypasses this point.
Who counts. Only a sender that is a session of this daemon counts, matched by exact id from provenance
from(with anyacp:prefix stripped). The pair is the unordered {sender, receiver} pair; it doesn't usethreadId. These senders are never counted or paused: the person's owncmux agent message(stampedcli), terminals, and any other actor.Detection. A message counts as an exchange once the receiver accepts it (queued or started); a message refused before that, such as
steer.unavailable, isn't counted. Progress in either session since the pair's last exchange resets the pair. Progress means:fs/write_text_file).A call counts as messaging only when it's a cmux-subsessions tool other than
start_session(send_message,list_sessions,get_session,wait_for_sessions,stop_session,set_session_limits, matched by server-qualified name), oragents_chat_send/agents_chat_read. cmux-subsessionssend_messageandstart_session's first prompt (provenancesubsession_message) count toward the same pair ascmux agent message. A shell call counts only when the whole command is a singlecmux agent messageorcmux agent inboxcall. Socargo test && cmux agent message …counts as work, and so does an edit ofsend_to_session.rs.Without progress, the pair trips on either of these:
cap), even while they make progress. A person's prompt starts the count over.When tripped.
session/promptis refused withagent_pair.paused, data{held: true, promptId, sessions, trip}. The text says the user resumes the pair and not to send it again.agent_pair.held_full, and the text says that message was not delivered.agent_pair_pausedrecord. The receiver's transcript gets anagent_pair_heldrecord per held message, carrying the full prompt and its options.notify_commandfires.Restarts. Held messages are persisted with a pair-wide
order.restore_queuerebuilds a pause from everyagent_pair_heldrecord whose prompt never reached the receiver (noqueued/user_messagewith its id) and that noagent_pair_releasedoragent_pair_droppedsettled, merging both logs in the order the messages were held. A release resends withresend: true, so a message that already ran is answered from the log and doesn't run twice. If the daemon stops while a resume is still delivering, the next daemon goes on delivering the rest once it starts (resume_queues): the restore reads each pair's lastagent_pair_pausedoragent_pair_resumedrecord, so the rest is never held again behind a transcript that already reads "Resumed". A pair paused again after its resume (release_failed) stays paused.Resume. Only the person can resume a pair, meaning a connection that proved the person key (
hub/person.rs, cx-1l61). There are two ways:_acpmux/agent_pair_resume {sessionId, peerSessionId?}(server/agent_messages.rs). It resumes only pairs thatsessionIdis in;peerSessionIdonly picks one of them. Any other caller is refused withagent_pair.resume_refused, and Web and Peer origins are refused before that inremote_guard.rs.Both sessions log
agent_pair_resumed, and the held messages are delivered oldest first. A released message neither steers nor interrupts, whatever it was sent with: it waits its turn, which may be the person's own prompt by then. Each one is accepted (queued or started) before the next is sent, loggedagent_pair_released, and messages that arrive meanwhile wait behind them. One that fails before acceptance stays held and pauses the pair again (release_failed); failing a second time it is dropped (agent_pair_dropped, in both logs). A held cmux-subsessionssend_messagewithinterruptstops nothing: the interrupt only fires once the message is accepted. Once the queue drains, the pair starts over.Defaults and toggles. The thresholds are 10 exchanges, 3 repeats, 90% similarity, and a cap of 20 messages per 600 seconds. They're read at daemon start from
ACPMUX_AGENT_PAIR_ALTERNATIONS,ACPMUX_AGENT_PAIR_REPEATS,ACPMUX_AGENT_PAIR_SIMILARITY(a percent),ACPMUX_AGENT_PAIR_MAX_MESSAGESandACPMUX_AGENT_PAIR_WINDOW_SECS(0 in either turns the cap off). Debug Settings:agent.pairGuard("Pause agents messaging in a loop", on by default) gives the app's daemonACPMUX_AGENT_PAIR_GUARD=0when off;agent.pairCap(20) andagent.pairWindow(600 s) pass the cap. The guard ships on, by Leo's call: a runaway pair burns usage silently, and the toggle turns it off.UI. The agent pane's pause row with held count and Resume is #22309, already on
feat-cmux-next; it draws this PR's records and sends_acpmux/agent_pair_resumethroughdirect.ts'sresumeAgentPair, both now on base. This PR adds the rest of what the row needs:AcpmuxPaneMethods.swift) lists_acpmux/agent_pair_resume. It is session-scoped like_acpmux/queue_removeand_acpmux/archive:sessionIdmust be a chat in the pane's scope,peerSessionIdis a known param that is only a selector (not scoped), and the request needs a fresh gesture (.always).policy.jsonand its cases say the same.direct.ts'sresumeAgentPair(peer)came in with Agent pane: show agent pair pauses with Resume #22309, so after the base merge this PR changes nothing indirect.ts; the generatedmethods.tsgains the method.Code layout.
turns.rs,subsessions.rs,server/requests.rs,daemon.rsandhub/mod.rsare over the god-file limit on base, and this PR ends each at or below its base size:daemon.rs'snotify_loopmoved whole todaemon/notify.rs, and the model probe list (set_probe_only,allow_probes,probes,probe_only_from_env) moved unchanged fromhub/mod.rstohub/live_models.rs.prompt_withcallsHub::prompt_unless_held. Thesend_messageinterrupt isHub::interrupt_once_queued._acpmux/prompt_withdrawand_acpmux/agent_pair_resumemoved toserver/agent_messages.rs, along with the person's prompt resume.agent_pairs.rskeeps its unit tests inagent_pairs_tests.rsand its progress detection inagent_pair_progress.rs.Limits.
failed, with the hold text as its error. The CLI and theagents_chat_sendreceipt show that text, but the state doesn't readheld.Startup. A daemon on base loads its logs in the background (
Hub::new_deferred), and the pairs come back with them (restore_queuereads theagent_pair_*records, which are now in itsQUEUE_KINDS)._acpmux/agent_pair_resumeand the person's own prompt wait for the logs (wait_logs) before they touch a pair, so a resume in that window is not undone by the restore.Evidence
The earlier red and green commits are in the PR history (6478f11 red, 78326ba green; aaac37d red, d3209ce green).
This round's regressions:
a_restart_during_a_resume_keeps_delivering_what_was_heldis committed red in ca001b9, and fixed in f78dc28 and ported in fd79911. Red was not run in CI; by reading, the pair came back paused andwait_rantimed out.a_sub_session_and_its_parent_messaging_through_send_message_are_pausedfailed in CI run 39359625271. It was a test bug: the ninth and tenth exchanges both went parent to child. It is fixed in df60fb9.Base merge and follow-up (ebdba19, adopted from an absent owner)
feat-cmux-next184c4e5 into the branch (e975ea5). Conflicts:hub/queue.rs(base'srestore_queuereads onlyQUEUE_KINDSthroughscan_kinds; the fiveagent_pair_*kinds were added, orrestore_agent_pairswould get nothing back), and two doc/property conflicts inAcpmuxDaemonLauncher.swiftandAcpmuxEnvironment.swift(both sides kept).prepush-gate.pyreports no new violations;cargo fmt --check -p acpmuxis clean;acpmux-protocol-codegen.ts --checksays the generated code is current.agent_pair39533641375,probe39533646641,session_scope39533650537,gestures39533653307 (results in a comment below).Observed / Not run (fd79911)
Observed, in focused cmux-tui CI on fd79911:
agent_pair):test (linux)passed 7 unit tests and 19agent_pairsintegration tests, and the Rust god-file check andcargo fmtpassed.cargo clippystopped atcrates/cmux-tui-core/src/release.rs:112(uninlined_format_args), a file this PR doesn't touch. So clippy did not reach acpmux.params_rule) and run 39380532477 (gestures) passed.macos_hosts) and run 39380523925 (page_sends) are red on base drift that has nothing to do with this PR:policy.jsonlists_acpmux/askand lacks_acpmux/btw,_acpmux/attachmentand_acpmux/chat/route.set. The same drift is on c387916._acpmux/agent_pair_resumematches on both sides.session_scope) failed on my new cases' request id, fixed in 91d456e. Re-runs on 91d456e: 39384414127 (agent_pair) and 39384417037 (session_scope).Not run:
AgentPaneAgentPairResumeRelayTestsandAcpmuxAgentPairGuardTestshave not been executed; that's left to PR CI.resumeAgentPair, which has no caller until Agent pane: show agent pair pauses with Resume #22309.Agent pane allowlist: security
_acpmux/agent_pair_resumeis added to the agent pane's bridge allowlist. TheAcpmuxPaneMethods.swiftandpolicy.jsonheaders ask for the protocol/origin lead's review (ad349); that review has not happened. Risk tier:scripts/ci/risk_tier.pygrades this PR normal (no file matches a high rule), the same as the recent allowlist additions #20826 (_acpmux/btw) and #20672 (_acpmux/offer_respond), which merged on agent review.sessionIdis in. A pair is exactly two sessions, so every held message is either to that chat or from it. Delivering one into the peer is delivering the in-scope chat's own outgoing message, which may run a turn in a peer chat outside the pane's scope.peerSessionIdonly selects among the chat's pairs. A request that names an out-of-scopesessionIdis refused by the pane (transport.session_not_in_pane), even when its peer is in scope. One that names a session outside the pair resumes nothing (a_resume_acts_only_on_the_named_sessions_pairs).{sessionId, resumed, delivered}, which is counts only.is_person() && !web_class()). The pane's LocalApp connection proves it with transportapp(AgentPaneTransportprovePerson). Web and Peer are refused inremote_guard.rs, the request is never forwarded to a peer, and an agent's own connection can't prove the person key.Remote relay
There's no new remote allowlist entry.
_acpmux/agent_pair_resumeis refused for Web and Peer origins, with a test.Review
Review: subagent correctness reviews passed at e975ea5 and ebdba19 (see above). Earlier, the subagent correctness review at fd79911 found no blockers. Its should-fix on the steer-only race is fixed in 290a99a. Its other findings are follow-ups:
interrupt_once_queued's cancel can race a turn ending;queue.rscancel_turnhas the same race.release_failedcounts reset at restart.The parallel lane's design calls in 3309231 and f8b5588 stand: the cap, and releases that never steer or interrupt.
Runtime test: focused cmux-tui CI runs above.
Changelog
Added: cmux NEXT pauses two agents that keep messaging each other without making progress, or that exchange more than 20 messages in 10 minutes without you, until you resume them.
🤖 Generated with Claude Code