Repository navigation
acpmux: Stop really stops an agent that works through the cancel - #27685
Merged
Merged
Conversation
…rs on work Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
Stop sent session/cancel and, 10 s later, ended only acpmux's logical turn. An agent that ignored the cancel kept running: its tool calls landed in the next queued turn (a batch of other agents' messages that started the moment the forced end settled), and the chat read as running again. Now an agent whose model still sends signs of work through the grace gets the cancel once more (the adapter's own interrupt), and 5 s later acpmux ends its process group, ends the turn as cancelled, and starts it again on the same conversation before the next turn can start. The row reads `stopping` until the turn really ended, and updates the ended process still had in flight are dropped. The person's Stop holds the other agents' queued messages (still queued, never dropped) until the person's next turn completes or the queue is resumed; the person's own prompts run next as before. A stuck chat whose agent starts a tool call again is unstuck at once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
…d second cancel Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
Collaborator
Author
|
Owned by an agent session · session |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
|
Found 31 test failures on Blacksmith runners: Failures
...and 21 more test failures. View all on Blacksmith
|
… prompt starts nothing Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
teamleaderleo
temporarily deployed
to
ci-routing-agent
October 30, 2026 00:14 — with
GitHub Actions
Inactive
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
teamleaderleo
temporarily deployed
to
sentry-dsyms
October 30, 2026 00:28 — with
GitHub Actions
Inactive
teamleaderleo
temporarily deployed
to
artifacts
October 30, 2026 00:28 — with
GitHub Actions
Inactive
teamleaderleo
temporarily deployed
to
artifacts
October 30, 2026 00:35 — with
GitHub Actions
Inactive
… the ended agent's tap counts no work; a later Stop starts nothing; a recovered turn restarts too Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
teamleaderleo
temporarily deployed
to
ci-routing-agent
October 30, 2026 01:03 — with
GitHub Actions
Inactive
… task there is not Send) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
teamleaderleo
temporarily deployed
to
ci-routing-agent
October 30, 2026 01:13 — with
GitHub Actions
Inactive
teamleaderleo
temporarily deployed
to
ci-routing-agent
October 30, 2026 01:17 — with
GitHub Actions
Inactive
teamleaderleo
temporarily deployed
to
sentry-dsyms
October 30, 2026 01:36 — with
GitHub Actions
Inactive
teamleaderleo
temporarily deployed
to
artifacts
October 30, 2026 01:36 — with
GitHub Actions
Inactive
teamleaderleo
temporarily deployed
to
artifacts
October 30, 2026 01:47 — with
GitHub Actions
Inactive
teamleaderleo
temporarily deployed
to
artifacts
October 30, 2026 01:51 — with
GitHub Actions
Inactive
teamleaderleo
temporarily deployed
to
artifacts
October 30, 2026 02:08 — with
GitHub Actions
Inactive
…w too, and holds while anything is queued Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
teamleaderleo
temporarily deployed
to
ci-routing-agent
October 30, 2026 02:59 — with
GitHub Actions
Inactive
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
A release now ends only the hold its caller saw (compare-exchange under the queue lock, where hold_agent_messages begins it), so a Stop that lands between the read and the release keeps its agent messages held. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
…stop-escalate # Conflicts: # cmux-tui/crates/acpmux/src/hub/cancel.rs # cmux-tui/crates/acpmux/src/hub/turns.rs
teamleaderleo
temporarily deployed
to
ci-routing-agent
October 30, 2026 03:43 — with
GitHub Actions
Inactive
teamleaderleo
temporarily deployed
to
ci-routing-agent
October 30, 2026 05:25 — with
GitHub Actions
Inactive
The digest of the agent messages a person's Stop holds is placed held and does not steer, read under the queue lock where the hold begins and ends. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
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. |
…n place Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
resume_held no longer clears stop_hold, and leaves agent messages held while a person's Stop holds them; only the person's next turn or a resume ends it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
teamleaderleo
temporarily deployed
to
ci-routing-agent
October 30, 2026 05:56 — with
GitHub Actions
Inactive
teamleaderleo
temporarily deployed
to
sentry-dsyms
October 30, 2026 06:01 — with
GitHub Actions
Inactive
teamleaderleo
temporarily deployed
to
artifacts
October 30, 2026 06:01 — with
GitHub Actions
Inactive
teamleaderleo
temporarily deployed
to
artifacts
October 30, 2026 06:02 — with
GitHub Actions
Inactive
teamleaderleo
had a problem deploying
to
artifacts
October 30, 2026 06:10 — with
GitHub Actions
Failure
The person's Stop now holds agent messages, an inbox digest of them too, until the person's next turn; these tests only need the turn to end. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
teamleaderleo
temporarily deployed
to
ci-routing-agent
October 30, 2026 06:28 — with
GitHub Actions
Inactive
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
teamleaderleo
temporarily deployed
to
ci-routing-agent
October 30, 2026 07:02 — with
GitHub Actions
Inactive
teamleaderleo
had a problem deploying
to
artifacts
October 30, 2026 07:13 — with
GitHub Actions
Failure
teamleaderleo
temporarily deployed
to
sentry-dsyms
October 30, 2026 07:13 — with
GitHub Actions
Inactive
This was referenced Oct 30, 2026
Contributor
|
Shipped in:
|
Contributor
|
Shipped in:
|
teamleaderleo
added a commit
that referenced
this pull request
Oct 30, 2026
…ts from #28046, #27685, #27809, #26984) - cli/run/ls.rs (72c0200, #28046): the ls request params and the filtered sessions write through JsonSet::set. - hub/cancel.rs (95243d1, #27685): forced_answer's `restarted` too. - hub/remote_rows.rs (e863077, #27809): mark_peer_stale reads status, stale and lastSeen with get. A missing field compares the way the Null that indexing returned did. - hub/restart_resume.rs (84705bd, #26984): BoundedJsonWriter copies at most `remaining` bytes with take, not a slice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-d9e7e9f7fb2b87a57d5f
teamleaderleo
added a commit
that referenced
this pull request
Oct 30, 2026
…g 703 > 693): rewrite the new hits without panics (#28134) * acpmux: restart tool previews cut strings without slicing (4 string_slice hits from #26984) The Rust panic ratchet is red on feat-cmux-next: acpmux string_slice 98 > baseline 94. The four new hits are in hub/restart_resume.rs, added by 84705bd (#26984): input_prefix and masked_scan cut at a char boundary with `[..end]`, mask_input_assignments with `[..assignment.end()]`, and safe_tool_text with `[..at]`. The two char-boundary cuts share one `char_prefix` helper that reads with `str::get`; the regex-end and rfind cuts use `get` too. Every index was already a char boundary, so behaviour is unchanged; the unreachable None arms keep the redacting side ("[secret]" alone, or an empty prefix). A new test cuts multibyte text at each spot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-d9e7e9f7fb2b87a57d5f * acpmux ls: the table pads without indexing (3 indexing_slicing hits from #28046) table() read each row's cell and each column's width by index, and render() bumped a hidden count by index. They now use get / get_mut. A row shorter than the header used to panic in table(); now its missing cells are left out. A row longer than the header has its extra cells padded to no width. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-d9e7e9f7fb2b87a57d5f * acpmux: JSON reads and writes without indexing (6 indexing_slicing hits from #28046, #27685, #27809, #26984) - cli/run/ls.rs (72c0200, #28046): the ls request params and the filtered sessions write through JsonSet::set. - hub/cancel.rs (95243d1, #27685): forced_answer's `restarted` too. - hub/remote_rows.rs (e863077, #27809): mark_peer_stale reads status, stale and lastSeen with get. A missing field compares the way the Null that indexing returned did. - hub/restart_resume.rs (84705bd, #26984): BoundedJsonWriter copies at most `remaining` bytes with take, not a slice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-d9e7e9f7fb2b87a57d5f * acpmux: JSON and slice reads and writes without indexing (15 indexing_slicing hits that landed after 08fb305) Base gained acpmux indexing_slicing hits while this PR was open (703 > 693): - cli/orchestrate.rs (65b5de8, #28016): harnessFrom via JsonSet::set. - cli/resolve.rs (9d318f9, #28000): choice_commands finds the session argument with enumerate and checks the preceding argument with get. - hub/key_route.rs (9d318f9, #28000): name and status are read with get. - hub/message_events.rs, hub/mod.rs, prompt_provenance.rs, server/outbox.rs, server/peer_forward.rs (7050ae3, #27038): writes via set; outbox reads via pointer, where a missing value reads as Null, as indexing returned. - hub/paging.rs (e0f3350, #27614): the moot slot is read and cleared with get / get_mut. - server/requests/briefs.rs (4a0b8f0, #28172): peersFailed via set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-d9e7e9f7fb2b87a57d5f * panic ratchet: lower acpmux indexing_slicing 693 -> 688, unwrap_used 144 -> 143 (measured at 0c8a50c, run 41050016507) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-d9e7e9f7fb2b87a57d5f * acpmux: Stop reads its answer without indexing (2 indexing_slicing hits from #28154); keep the acpmux baseline at 693 hub/turns.rs (0a088aa, #28154): is_stopped_before_prompt and the stopped answer read with get, not index. A missing reason compares like Null did, and stopReason is always present in stopped_answer(). The acpmux baseline goes back to base's 693 / 144. acpmux hits landed three times while this PR was open, and a baseline pinned to today's exact count would turn base red the next time one lands before this PR merges. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-d9e7e9f7fb2b87a57d5f * acpmux: drop three JsonSet imports that super::* already brings in (clippy unused_imports) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-d9e7e9f7fb2b87a57d5f --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 31, 2026
This branch had an error being deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
Leo (2026-10-29): "are these messages literally not getting to you ... i literally can't stop you i clicked the stop button and it didnt do anything so this is still busted didnt we say we were going to fix this in acpmux? otherwise i have to manually just click steer on all of them unless you come up with better solulus"
What happened
A coordinator chat had 319 queued prompts, all from other agents. It read
state=runningand alsostuck(harness_unavailable, a 502 from the gateway) while it kept making tool calls. Leo pressed Stop. cmux showed "The agent didn't answer Stop, so cmux ended its turn", but the agent kept making tool calls and the chat stayed running.Why the chat still read as running after the forced end (from the code)
Mostly the queue. The agent's events alone could not keep or reopen a turn:
TurnInfoor setsRunning.on_update/note_activityattach tosession.turnonly if one exists, andRunningis set only byrun_prompt(turns.rs), adoption and permission answers.cancel.rs's forced end (force_cancelafterCANCEL_GRACE) settles the waiting prompt.finish_turnthen takes the turn and notifiesqueue_moved. The head queued prompt's task, already blocked inqueue_turnonturn_lock, takes the lock at once: nothing in the queue checks for a Stop. That head was an agent message, sotake_joinedpulled in up to 128 KiB of the other agent messages, and the new turn setRunning.TurnInfo.doing, and itsheardsigns kept the new turn from ever reading silent or stalled. To the person it looked like Stop did nothing.cancel_preserving_queuedwas wrong. It said "A plain Stop withdraws every queued prompt, including one promoted with Send now". On base, only the compacting branch withdraws anything (remove_queued_because(.., "cancel_forced")). A plain Stop withdrew nothing, so the queue drained straight on.HarnessFailuresresets only on a completed turn, and the agent never completed one.The fix
Escalation for an agent that works through the cancel (
hub/cancel.rsescalate_stop). If the model kept sending signs of work through the 10 s grace (Session::heardmoved since the cancel), acpmux sendssession/cancelagain. That is the adapter's own interrupt:control_request {subtype: interrupt};turn/interruptfor the thread and its subagents;session/cancel.No adapter has anything stronger short of ending the process. If the agent still works 5 s later (
INTERRUPT_GRACE), acpmux takes the agent out of the slot, marks itend_on_purpose(no failover, no continuation) and callsChildAgent::kill. That kills only the session's own recorded process group (SIGTERM, then SIGKILL, plus its setsid escapees). For a hosted agent it sends the host'sTerminate. The turn ends ascancelledwithcancelForcedandrestarted: true.The agent then starts again on the same conversation through the existing
child_forpath (--resume/session/load). This happens at the end ofrun_prompt, while the stopped turn's lock is still held, so the next queued turn finds it running. An agent that goes quiet keeps today's plain forced end.Never ended while it still works. From the first Stop until the turn really ends, the row's
statereadsstopping(astoppingrecord, which watchers hear asstatus). Updates that the ended process still had in flight (by its agent generation) are dropped in the inbound loop, so they cannot reach the next turn.Queued agent messages after the person's Stop. A
session/cancelfrom the person's connection (Hub::person_stop) holds the other agents' queued messages (fromAgent/ agent provenance). They stay queued, markedheld, and are logged asqueue_pausedwithreason: "stop". Agent messages that arrive while the hold lasts are held too. The hold ends when:release_held_agent_messages);A failed turn's automatic retry leaves them held while the hold lasts, and an inbox digest (
acpmux session inbox --digest) of them waits held and does not steer.Nothing is dropped, and the person's own queued prompts run next as before. The compacting branch now withdraws only the person's prompts and holds the agents' messages instead of dropping them. Stops from agents (
acpmux send --interrupt, sub-session stop, remote floor) keep today's behaviour, and the misleading comment is corrected.Stuck clears on work. A session marked stuck is unstuck on the first live
tool_call, thought or plan update from its agent (not asession/loadreplay). Its failures are forgotten and anunstuckrecord is logged, which watchers hear asstatus.Tests (red at the test-only commit, green at the fix)
stop::an_agent_working_through_stop_is_restarted_on_the_same_conversation: the fake agent ignores every cancel and emits a tool call every 100 ms. The test checks:stopping;interruptthenrestart, then the turn endscancelled/cancelForced;resumedthe sameagentSessionId;stop::after_the_persons_stop_their_prompt_runs_next_and_agents_wait_for_it: the person's queued prompt runs next, the agent's message is held during it and goes in after it.stuck_liveness::a_stuck_session_whose_agent_works_again_is_unstuck_at_its_first_tool_call.hub.cancel), since the person's Stop holds those messages.stop::after_the_persons_stop_a_failed_turns_retry_leaves_agent_messages_heldfailsstop::after_the_persons_*tests passhub_integrationtest passes. Onlyhermetic_daemon_guard::only_the_hermetic_helper_starts_the_acpmux_binary(also red on base) fails, plus twodaemon::state_file_tests::managed_unit_*lib tests. Those two fail only on the fleet pool's environment, pass in every Blacksmith run of the crate, and are in code this PR does not touch (src/daemon/).PR CI reds at a7f4d79, each also red on base (re-checked at the final head before landing):
tests/test_build_web_bundles_from.shhits a FileNotFoundError onwebviews/package.json: since fix(cmux-next): make web OTA publication fail closed #27680,web-bundle-key.pyreads that file and the test's scratch repo has none. The same failure on base tip 284e691 is run 41007450680. It runs here only because this PR touches a webviews file (the generated protocol types).swift-test-summary.test.shfixture, and this PR changes no Swift.Review
Independent reviews ran with Codex gpt-6.1-sol at high, with fast mode on, through the team route. The coordinator allowed this after gpt-6-astra hung on two GCP lanes tonight. Round by round:
Risk
heardcounts model signs only, not tool heartbeats beyond Claude'stool_progress).state: "stopping"is a new value of an existing string field.Overlaps (same author, coordinated by the acpmux area owner): #27536 changes
cancel.rsforce_cancel/cancel_preserving_queued(carried turns), and #27038 / #26747 changequeue.rsenqueue.Agent session: agent session
🤖 Generated with Claude Code