Skip to content

acpmux: pause two agents that keep messaging each other without progress - #21975

Merged
teamleaderleo merged 36 commits into
feat-cmux-nextfrom
chat/agent-pingpong-guard
Oct 21, 2026
Merged

teamleaderleo merged 36 commits into
feat-cmux-nextfrom
chat/agent-pingpong-guard

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

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_session mints 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 a session/prompt carrying an agent_message provenance. The new check Hub::agent_pair_check (hub/agent_pairs.rs) runs there, before the prompt is recorded, so it covers:

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 any acp: prefix stripped). The pair is the unordered {sender, receiver} pair; it doesn't use threadId. These senders are never counted or paused: the person's own cmux agent message (stamped cli), 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:

  • a tool call that isn't a messaging call, or
  • a file write (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), or agents_chat_send / agents_chat_read. cmux-subsessions send_message and start_session's first prompt (provenance subsession_message) count toward the same pair as cmux agent message. A shell call counts only when the whole command is a single cmux agent message or cmux agent inbox call. So cargo test && cmux agent message … counts as work, and so does an edit of send_to_session.rs.

Without progress, the pair trips on either of these:

  • 10 exchanges in a row that go back and forth;
  • 3 messages in a row from one sender to the same receiver that are near-identical. Bodies are compared without the cmux-rendered header, with case, digits and punctuation dropped, and match at a token Jaccard of 0.9 or higher;
  • the cap: more than 20 messages between the two within 10 minutes with no prompt from the person in either chat (reason cap), even while they make progress. A person's prompt starts the count over.

When tripped.

  • The tripping message and every later one in the pair, in either direction, are held, not delivered.
  • The sender's session/prompt is refused with agent_pair.paused, data {held: true, promptId, sessions, trip}. The text says the user resumes the pair and not to send it again.
  • A pair holds at most 50 messages. Past that, a message is refused with agent_pair.held_full, and the text says that message was not delivered.
  • Both transcripts get an agent_pair_paused record. The receiver's transcript gets an agent_pair_held record per held message, carrying the full prompt and its options.
  • Both chats are marked unread, and the daemon's notify_command fires.

Restarts. Held messages are persisted with a pair-wide order. restore_queue rebuilds a pause from every agent_pair_held record whose prompt never reached the receiver (no queued / user_message with its id) and that no agent_pair_released or agent_pair_dropped settled, merging both logs in the order the messages were held. A release resends with resend: 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 last agent_pair_paused or agent_pair_resumed record, 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 that sessionId is in; peerSessionId only picks one of them. Any other caller is refused with agent_pair.resume_refused, and Web and Peer origins are refused before that in remote_guard.rs.
  • A prompt the person sends into either session. A prompt from a Peer never counts as the person.

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, logged agent_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-subsessions send_message with interrupt stops 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_MESSAGES and ACPMUX_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 daemon ACPMUX_AGENT_PAIR_GUARD=0 when off; agent.pairCap (20) and agent.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_resume through direct.ts's resumeAgentPair, both now on base. This PR adds the rest of what the row needs:

  • The pane bridge allowlist (AcpmuxPaneMethods.swift) lists _acpmux/agent_pair_resume. It is session-scoped like _acpmux/queue_remove and _acpmux/archive: sessionId must be a chat in the pane's scope, peerSessionId is a known param that is only a selector (not scoped), and the request needs a fresh gesture (.always).
  • policy.json and its cases say the same.
  • direct.ts's resumeAgentPair(peer) came in with Agent pane: show agent pair pauses with Resume #22309, so after the base merge this PR changes nothing in direct.ts; the generated methods.ts gains the method.

Code layout. turns.rs, subsessions.rs, server/requests.rs, daemon.rs and hub/mod.rs are over the god-file limit on base, and this PR ends each at or below its base size: daemon.rs's notify_loop moved whole to daemon/notify.rs, and the model probe list (set_probe_only, allow_probes, probes, probe_only_from_env) moved unchanged from hub/mod.rs to hub/live_models.rs. prompt_with calls Hub::prompt_unless_held. The send_message interrupt is Hub::interrupt_once_queued. _acpmux/prompt_withdraw and _acpmux/agent_pair_resume moved to server/agent_messages.rs, along with the person's prompt resume. agent_pairs.rs keeps its unit tests in agent_pairs_tests.rs and its progress detection in agent_pair_progress.rs.

Limits.

  • The pair counts themselves live in memory; a restart only rebuilds the pauses.
  • The cmux daemon's store marks a held pump delivery failed, with the hold text as its error. The CLI and the agents_chat_send receipt show that text, but the state doesn't read held.

Startup. A daemon on base loads its logs in the background (Hub::new_deferred), and the pairs come back with them (restore_queue reads the agent_pair_* records, which are now in its QUEUE_KINDS). _acpmux/agent_pair_resume and 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_held is 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 and wait_ran timed out.
  • a_sub_session_and_its_parent_messaging_through_send_message_are_paused failed 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)

  • Merged feat-cmux-next 184c4e5 into the branch (e975ea5). Conflicts: hub/queue.rs (base's restore_queue reads only QUEUE_KINDS through scan_kinds; the five agent_pair_* kinds were added, or restore_agent_pairs would get nothing back), and two doc/property conflicts in AcpmuxDaemonLauncher.swift and AcpmuxEnvironment.swift (both sides kept).
  • ebdba19: the resume-before-logs-load fix above and the god-file moves.
  • Independent subagent reviews passed at e975ea5 (full PR) and ebdba19 (that commit).
  • Observed: prepush-gate.py reports no new violations; cargo fmt --check -p acpmux is clean; acpmux-protocol-codegen.ts --check says the generated code is current.
  • Focused cmux-tui CI on ebdba19: agent_pair 39533641375, probe 39533646641, session_scope 39533650537, gestures 39533653307 (results in a comment below).
  • Not run: no new regression test for the resume-before-logs-load window (it needs a deferred hub with a paused pair on disk); no Swift build or test outside PR CI.

Observed / Not run (fd79911)

Observed, in focused cmux-tui CI on fd79911:

  • Run 39380518627 (agent_pair): test (linux) passed 7 unit tests and 19 agent_pairs integration tests, and the Rust god-file check and cargo fmt passed. cargo clippy stopped at crates/cmux-tui-core/src/release.rs:112 (uninlined_format_args), a file this PR doesn't touch. So clippy did not reach acpmux.
  • Run 39380529853 (params_rule) and run 39380532477 (gestures) passed.
  • Run 39380521523 (macos_hosts) and run 39380523925 (page_sends) are red on base drift that has nothing to do with this PR: policy.json lists _acpmux/ask and lacks _acpmux/btw, _acpmux/attachment and _acpmux/chat/route.set. The same drift is on c387916. _acpmux/agent_pair_resume matches on both sides.
  • Run 39380526413 (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:

  • No Swift build or test. AgentPaneAgentPairResumeRelayTests and AcpmuxAgentPairGuardTests have not been executed; that's left to PR CI.
  • No webview typecheck or test for resumeAgentPair, which has no caller until Agent pane: show agent pair pauses with Resume #22309.
  • 290a99a (a steer-only message into a paused pair is held) fixes a race and has no deterministic test.

Agent pane allowlist: security

_acpmux/agent_pair_resume is added to the agent pane's bridge allowlist. The AcpmuxPaneMethods.swift and policy.json headers ask for the protocol/origin lead's review (ad349); that review has not happened. Risk tier: scripts/ci/risk_tier.py grades 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.

  • Execute or open anything? No. It runs no command and opens no file, URL or window. Its params are two session ids. What it does is deliver prompts that the two agents already sent and the guard held. That runs turns in both chats of the pair, so it needs a fresh gesture.
  • Mutate unowned objects? Only pairs that the in-scope sessionId is 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. peerSessionId only selects among the chat's pairs. A request that names an out-of-scope sessionId is 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).
  • Read local state? No. The reply is {sessionId, resumed, delivered}, which is counts only.
  • Who may call it. acpmux still requires a connection that proved the person key, on this machine (is_person() && !web_class()). The pane's LocalApp connection proves it with transport app (AgentPaneTransport provePerson). Web and Peer are refused in remote_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_resume is 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.rs cancel_turn has the same race.
  • The order test can tie on a millisecond.
  • A crash between the two resume appends can lose the resume.
  • The person's prompt resumes before it is accepted.
  • release_failed counts reset at restart.
  • One trip fires two notifications.

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

teamleaderleo and others added 2 commits October 19, 2026 05:17
…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>
@teamleaderleo
teamleaderleo temporarily deployed to cmux-tui-merge-tree October 19, 2026 05:40 — with GitHub Actions Inactive
@coderabbitai

coderabbitai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository: manaflow-ai/cmux/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 23447115-b538-4396-a7ad-3cda9c2c1ba4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@blacksmith-sh

This comment has been minimized.

@teamleaderleo

teamleaderleo commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator Author

Visual preview of 51294c4: in the maintainers' private review feed.

@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

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

Review ebdba19: 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=pr21975 -f ref=chat/agent-pingpong-guard -f platform=macos -f backend_mode=local

Unclear: web / react-apps-check failed: no known signature; failed step: Check webview test shards. This PR merged before its CI finished: fix forward on main.

CI failed on ebdba19229 (run 39534361292 attempt 1): 4 code, 1 unknown.
macOS jobs did not run: compile and app tests were skipped: Fast static checks skipped.

Job Verdict Why
web / react-apps-check (4) code a web test failed
web / react-apps-check (3) code a web test failed
web / react-apps-check (1) code a web test failed
web / react-apps-check (2) code a web test failed
web / react-apps-check unknown no known signature; failed step: Check webview test shards

Failures:

  • seen on other PRs agent-pane-background-tasks.test.ts clicking a background task's end opens its terminal beside the chat: failed (job)
  • seen on other PRs editors.test.tsx editors > every schema row renders the editor for its kind: timed out after 5000ms (job)
  • seen on other PRs editors.test.tsx editors > the window material is a material choice, never a radius slider: failed (job)
  • seen on other PRs editors.test.tsx editors > opacity is a percent slider: preview while dragging, one settings.set on release: failed (job)
  • seen on other PRs editors.test.tsx editors > an unset slider sits at the value the host derives (the theme's opacity), live: failed (job)
  • seen on other PRs editors.test.tsx editors > a number field commits on Return, clamped to the range: failed (job)
  • seen on other PRs editors.test.tsx editors > a number list adds a number in range on Return and refuses one outside it: failed (job)
  • seen on other PRs editors.test.tsx editors > a managed row shows its value, a disabled control and a localized lock line: failed (job)
  • seen on other PRs editors.test.tsx editors > the reset button appears only when the row is customized: failed (job)
  • seen on other PRs composer-history.test.ts ↑ and ↓ in an empty prompt step through the chat's earlier prompts: failed (job)
  • ... 6 more in the job logs
Matched log lines
web / react-apps-check (4): (fail) clicking a background task's end opens its terminal beside the chat [937.90ms]
web / react-apps-check (3): (fail) editors > every schema row renders the editor for its kind [5090.23ms]
web / react-apps-check (1): (fail) ↑ and ↓ in an empty prompt step through the chat's earlier prompts [1721.82ms]
web / react-apps-check (2): (fail) chromium > the wheel over a long draft stops at its ends and moves nothing else [4086.68ms]

Not re-run automatically: web / react-apps-check (4), web / react-apps-check (3), web / react-apps-check (1), web / react-apps-check (2), web / react-apps-check are not machine failures.

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.

red: web / Web status; base red: the same failure on the base 184c4e5; does not block
red: web / react-apps-check; base red: the same failure on the base 184c4e5; does not block
red: web / react-apps-check (1); NEW: step Test webviews (+1 more) passes on the base 184c4e5; do not merge until green; see lane-rules
red: web / react-apps-check (2); NEW: - waiting for locator('.acpmux-steer-now') to be visible at /home/runner/_work/cmux/cmux/webviews/test/composer-turn-cycle.test.ts:182:47 (+2 more) is not red on the base 184c4e5; do not merge until green; see lane-rules
red: web / react-apps-check (3); NEW: - [] + [ + "notifications.desktop (choice): row not rendered", + "notifications.sound (sound): row not rendered", + "notifications.quietHours (time_range): row (+3 more) is not red on the base 184c4e5; do not merge until green; see lane-rules
red: web / react-apps-check (4); NEW: Expected: "Show output" Received: null at <anonymous> (/home/runner/_work/cmux/cmux/webviews/test/agent-pane-background-tasks.test.ts:242:46) is not red on the base 184c4e5; do not merge until green; see lane-rules

CI fast guards failed on 51294c484b (https://github.com/manaflow-ai/cmux/actions/runs/39415899258). It does not block the merge; a red guard merged into main breaks it for every open PR.
The log named no failed step; see the run.

Agents: python3 scripts/ci/guard_attribution.py fix applies the mechanical fixes locally.

@github-actions

Copy link
Copy Markdown
Contributor

CI fast guards failed on 78326bae72 (https://github.com/manaflow-ai/cmux/actions/runs/39205762680). It does not block the merge; a red guard merged into main breaks it for every open PR.
The log named no failed step; see the run.

Agents: python3 scripts/ci/guard_attribution.py fix applies the mechanical fixes locally. This comment is updated in place on each push.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

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

teamleaderleo and others added 2 commits October 19, 2026 07:47
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>
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Orphaned: no activity for 13 h, and no lane holds a lease on it. The orphan sweep could not merge feat-cmux-next in: conflicts in Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneTunables.swift. Listed for a lane to resolve.

# Conflicts:
#	Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneTunables.swift
@teamleaderleo
teamleaderleo temporarily deployed to cmux-tui-merge-tree October 20, 2026 02:13 — with GitHub Actions Inactive
@greptile-apps

greptile-apps Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 1/5

[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

  1. P1 Concurrent sends escape the limits ▶
  2. P1 Restart undoes a pause ▶
  3. P1 Unsent messages still run ▶
  4. P1 Held messages share IDs ▶
  5. P2 Deleted pairs stay in memory ▶

Summary

Adds a guard that holds runaway messages between two agent sessions until the person resumes them.

  • Agent pairs pause when their messages loop without progress or exceed the unattended cap.
  • The app's Debug Settings control the pair guard and its message cap.
  • The person can resume a paused pair from either chat or the local app.
  • Reply-shape code now lives in its own app-pane file.

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]
Loading

Reviews (12) · Last reviewed commit: "Merge branch 'feat-cmux-next' into chat/..." · Reviewed by Greptile

Comment on lines +577 to +579
"agent_pair_resumed" => {
if let Some(peer) = rec.msg.get("peer").and_then(Value::as_str) {
held.remove(peer);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 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.

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.

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.

Comment on lines +591 to +593
for h in list {
if !paused.held.iter().any(|x| x.prompt_id == h.prompt_id) {
paused.held.push_back(h);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 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.

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.

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.

Comment on lines +278 to +280
if ["&&", "||", ";", "|", "\n", "`", "$(", ">", "<", "&"].iter().any(|op| command.contains(op))
{
return false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 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.

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.

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.

Comment on lines +488 to +490
pair.paused =
Some(Paused { held: VecDeque::from([held.clone()]), draining: false });
Hold::Tripped(trip)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 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.

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.

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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 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.

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.

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.

Comment on lines +366 to +371
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
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 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.

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.

Fixed. progressed_since scans no transcripts now. Each session keeps a last_progress sequence, updated as its events arrive, and the check reads that.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

cmux-next guards / defaults-snapshot is red on 19 of 19 fresh feat-cmux-next PR heads in the last 2:00:00, so it is most likely a base red, not this PR's. The base-reds lane has it; a base merge picks up the fix once it lands.

teamleaderleo and others added 4 commits October 19, 2026 19:21
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>
…lead/21975

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

cursor Bot commented Oct 20, 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

Copy link
Copy Markdown
Collaborator Author

cmux-tui SDKs / protocol contract is red on 9 of 9 fresh feat-cmux-next PR heads in the last 2:00:00, so it is most likely a base red, not this PR's. The base-reds lane has it; a base merge picks up the fix once it lands.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

high-signal review link / link is red on 31 of 31 fresh feat-cmux-next PR heads in the last 2:00:00, so it is most likely a base red, not this PR's. The base-reds lane has it; a base merge picks up the fix once it lands.

teamleaderleo added a commit that referenced this pull request Oct 20, 2026
* 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>
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

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
@teamleaderleo
teamleaderleo temporarily deployed to cmux-tui-merge-tree October 21, 2026 02:04 — with GitHub Actions Inactive
@teamleaderleo teamleaderleo removed the ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds label Oct 21, 2026
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
@teamleaderleo
teamleaderleo temporarily deployed to cmux-tui-merge-tree October 21, 2026 02:29 — with GitHub Actions Inactive
@teamleaderleo teamleaderleo added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Oct 21, 2026
@teamleaderleo
teamleaderleo merged commit bb709e3 into feat-cmux-next Oct 21, 2026
@teamleaderleo
teamleaderleo deleted the chat/agent-pingpong-guard branch October 21, 2026 02:41
teamleaderleo added a commit that referenced this pull request Oct 21, 2026
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
@github-actions

Copy link
Copy Markdown
Contributor

Shipped in:

@github-actions

Copy link
Copy Markdown
Contributor

Shipped in:

This branch was previously deployed

3 inactive deployments
artifacts — ebdba192 Deployed Oct 21, 2026 by teamleaderleo
sentry-dsyms — ebdba192 Deployed Oct 21, 2026 by teamleaderleo
cmux-tui-merge-tree — ebdba192 Deployed Oct 21, 2026 by teamleaderleo
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