Skip to content

fix(coding-agent): answer ACP prompts only once the session admits the next turn - #800

Open
parkerpettit wants to merge 4 commits into
feat/acp-quiescence-metafrom
fix/acp-prompt-waits-for-idle
Open

fix(coding-agent): answer ACP prompts only once the session admits the next turn#800
parkerpettit wants to merge 4 commits into
feat/acp-quiescence-metafrom
fix/acp-prompt-waits-for-idle

Conversation

@parkerpettit

@parkerpettit parkerpettit commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

ACP could answer session/prompt after its first idle check even though injected work had restarted the session. An immediate follow-up was then rejected as Agent is already processing.

Without this change:

  1. The current ACP turn finishes.
  2. waitForHeadlessCompletion() observes an idle session.
  3. Injected work, such as a subagent message, starts another turn.
  4. ACP returns end_turn.
  5. The client sends its next prompt and Prime Agent rejects it because the session is busy.

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 by compact.run. One makes the idle check complete; this PR puts a final check at the response boundary.

Verification:

  • ACP response waits for the final admission check
  • an immediate second prompt is accepted

Note

Queue ACP follow-up prompts behind in-flight work instead of rejecting

  • ACP session/prompt requests now pass queueIfBusy: true so they wait behind ongoing injected work rather than immediately failing with a busy error.
  • A cancellation signal is threaded from the request's AbortController through to promptAndWait, allowing a queued-but-not-yet-started prompt to be dropped when session/cancel is received.
  • AgentDaemon now calls admission.controller?.abort() when cancelling a session-owned admission, wiring the abort signal through to the queued action.
  • Behavioral Change: ACP callers that previously received an immediate busy rejection will now have their request queued; they must cancel explicitly to abort a queued prompt.

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/prompt now awaits connection.waitForIdle() before returning end_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/new reserves the single-session slot before the first await, subscribes before getInitialSnapshot(), and fails setup cleanly if the snapshot read errors. After headless completion, a session_info_update carries namespaced quiescence (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-session ACP stays client-owned. Resident creates always forward launchEnv from the caller (model endpoint, tokens, proxy, etc.). The supervisor persists launchEnv on 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.

@parkerpettit
parkerpettit marked this pull request as ready for review August 7, 2026 20:22
@parkerpettit
parkerpettit force-pushed the fix/acp-prompt-waits-for-idle branch from 6a6ab06 to ac97743 Compare August 7, 2026 20:40
@parkerpettit
parkerpettit changed the base branch from main to feat/acp-quiescence-meta August 7, 2026 20:40
@parkerpettit
parkerpettit marked this pull request as draft August 7, 2026 20:40
Comment thread packages/coding-agent/src/modes/acp/acp-mode.ts Outdated
@parkerpettit
parkerpettit marked this pull request as ready for review August 7, 2026 20:42
@snimu

snimu commented Aug 10, 2026

Copy link
Copy Markdown
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 ## [Unreleased] in packages/coding-agent/CHANGELOG.md.

Comment thread packages/coding-agent/src/modes/acp/acp-mode.ts Outdated
@parkerpettit

Copy link
Copy Markdown
Contributor Author

Reworked per review: ACP prompts now queue behind in-flight work (streamingBehavior: "followUp", queueIfBusy: true) instead of waiting for whole-session idleness; the final waitForIdle() is removed, so the stop reason comes from the status captured after the turn the response gates and the response is never held open by detached work. Regression tests inject real work after the first idle observation; CHANGELOG entry added.

Comment thread packages/coding-agent/src/modes/acp/acp-mode.ts
Comment thread packages/coding-agent/src/modes/daemon/daemon-mode.ts
Comment thread packages/coding-agent/src/core/agent-session.ts Outdated
…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
parkerpettit force-pushed the fix/acp-prompt-waits-for-idle branch 2 times, most recently from 3f68e8d to bf2c00e Compare August 10, 2026 22:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants