fix(coding-agent): answer ACP prompts only once the session admits the next turn - #800
Open
parkerpettit wants to merge 4 commits into
Open
fix(coding-agent): answer ACP prompts only once the session admits the next turn#800parkerpettit wants to merge 4 commits into
parkerpettit wants to merge 4 commits into
Conversation
parkerpettit
marked this pull request as ready for review
August 7, 2026 20:22
parkerpettit
force-pushed
the
fix/acp-prompt-waits-for-idle
branch
from
August 7, 2026 20:40
6a6ab06 to
ac97743
Compare
parkerpettit
marked this pull request as draft
August 7, 2026 20:40
parkerpettit
marked this pull request as ready for review
August 7, 2026 20:42
Contributor
|
Missing CHANGELOG entry: this fixes a user-visible ACP race (immediate follow-up prompt rejected with "Agent is already processing"). Please add a bullet under |
snimu
reviewed
Aug 10, 2026
Contributor
Author
|
Reworked per review: ACP prompts now queue behind in-flight work ( |
…e next turn session/prompt returned after waitForHeadlessCompletion while the agent session could still be streaming the turn's residue. A client that prompts right after end_turn then gets rejected with "Agent is already processing. Specify streamingBehavior" - advice it cannot follow, since the ACP surface never reads that option. The response is the client's admission signal, so hold it until the session's own admission gate (waitForIdle) clears. Seen in production by the verifiers ACP harness driving multi-turn panel seats: 16 rejected turns in one overnight campaign.
… waiting for idle The final waitForIdle() before the ACP response was whole-session quiescence, not an admission gate: it held the session/prompt response open for any work injected after the first idle observation (subagent replies, heartbeats), could defer it indefinitely behind a chain of detached-child turns, and blocked the same boundary #806 deliberately kept non-blocking. The stop reason was also computed from the status snapshot captured before that wait, so a turn that exhausted an autonomous limit while the response waited still reported end_turn. Queue the host turn instead: pass streamingBehavior followUp with queueIfBusy so a prompt that arrives while injected work keeps the session busy runs after it rather than being rejected with "Agent is already processing". promptAndWait resolves once the queued turn has run, so waitForHeadlessCompletion captures the status the response actually gates and the stop reason stays fresh. Regression tests inject real work after the first headless-completion idle observation and drive a real ACP client: the follow-up prompt is queued (never rejected, never resolved early), the stop reason reflects the queued turn's fresh autonomous status, and the response is not held open for detached work.
session/prompt passes abort.signal to promptAndWait, and AgentSession promptAndWait now cancels a not-yet-started followUp action when that signal fires instead of leaving it to run after the busy turn drains. session/cancel on the daemon path propagates its admission-controller abort to the queued prompt (cancel_prompt_admission aborts the owned controller), so a cancelled prompt neither runs later nor leaves the session/prompt request hanging.
Register the queued-prompt cancel listener before re-checking signal aborted so an abort landing between the check and the registration still cancels the action.
parkerpettit
force-pushed
the
fix/acp-prompt-waits-for-idle
branch
2 times, most recently
from
August 10, 2026 22:38
3f68e8d to
bf2c00e
Compare
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.
ACP could answer
session/promptafter its first idle check even though injected work had restarted the session. An immediate follow-up was then rejected asAgent is already processing.Without this change:
waitForHeadlessCompletion()observes an idle session.end_turn.Re-check
connection.waitForIdle()before returning the ACP response. This closes the admission race seen in multi-turn Verifiers runs.#881 fixes a different gap:
waitForIdle()did not include a continuation already scheduled bycompact.run. One makes the idle check complete; this PR puts a final check at the response boundary.Verification:
Note
Queue ACP follow-up prompts behind in-flight work instead of rejecting
session/promptrequests now passqueueIfBusy: trueso they wait behind ongoing injected work rather than immediately failing with a busy error.AbortControllerthrough topromptAndWait, allowing a queued-but-not-yet-started prompt to be dropped whensession/cancelis received.AgentDaemonnow callsadmission.controller?.abort()when cancelling a session-owned admission, wiring the abort signal through to the queued action.Macroscope summarized bf2c00e.
Note
Medium Risk
Changes daemon worker lifecycle, persisted launch environment, and ACP turn-boundary semantics that verifiers and embedders depend on; behavior is heavily tested but mis-timed idle or env handling could still break reconnect or scoring.
Overview
ACP
session/promptnow awaitsconnection.waitForIdle()before returningend_turn, so clients are not told the turn finished while injected work (e.g. subagent messages) has already restarted the session and would reject an immediate follow-up with “Agent is already processing.”ACP session lifecycle and scoring metadata:
session/newreserves the single-session slot before the first await, subscribes beforegetInitialSnapshot(), and fails setup cleanly if the snapshot read errors. After headless completion, asession_info_updatecarries namespacedquiescence(outstandingSubagents,remainingAutonomousContinuations) plus autonomous state from a live roster snapshot; snapshot failures at emission time propagate instead of reporting a false zero.Daemon / ACP residency: Normal ACP sessions (with a session file) use resident workers so disconnect/reconnect can reattach;
--no-sessionACP stays client-owned. Resident creates always forwardlaunchEnvfrom the caller (model endpoint, tokens, proxy, etc.). The supervisor persistslaunchEnvon resident worker descriptors for recovery after restart and strips it when promoting to client-owned, with integration tests for env across worker recovery and live IPython namespace across ACP reconnect.Reviewed by Cursor Bugbot for commit ac97743. Bugbot is set up for automated code reviews on this repo. Configure here.