feat(coding-agent): expose ACP quiescence metadata - #806
Closed
sethkarten wants to merge 6 commits into
Closed
Conversation
sethkarten
force-pushed
the
feat/acp-quiescence-meta
branch
from
August 7, 2026 00:44
f1e49eb to
a0ec4a5
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 0e33b3f. Configure here.
sethkarten
force-pushed
the
feat/acp-quiescence-meta
branch
3 times, most recently
from
August 7, 2026 08:12
b6e6fa6 to
2efaa15
Compare
snimu
reviewed
Aug 10, 2026
Contributor
|
One more thing: this adds user-visible ACP surface ( |
The roster fix added coverage for a snapshot failure during session/new, but not for one at emission time. Those are separate call sites, and only the first was guarded: re-adding a swallowing `.catch(() => undefined)` on the emission read still passed the suite. That is the failure mode the metadata exists to prevent. Degrading to an empty roster reports `outstandingSubagents: 0` while children are still running, so a consumer scores a turn that has not finished -- wrong, and silently so. The new test drives the emission read to fail and asserts the prompt rejects rather than answering a clean end_turn with fabricated quiescence. Re-adding the swallow now fails it.
sethkarten
force-pushed
the
feat/acp-quiescence-meta
branch
from
August 10, 2026 17:44
2efaa15 to
6447725
Compare
Contributor
Author
|
Addressed in 6447725: |
parkerpettit
added a commit
that referenced
this pull request
Aug 10, 2026
… 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.
parkerpettit
added a commit
that referenced
this pull request
Aug 10, 2026
… 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.
This was referenced Aug 11, 2026
Contributor
Author
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.

Why
An ACP client cannot currently tell whether a completed turn is safe to act on. Prime Agent subagents are fire-and-forget and keep working after the spawning turn ends, and autonomous mode may still have continuation budget left. A consumer that scores or ends the session at
end_turncan therefore cut off live work, and the result depends on a race.This is not derivable client-side:
tool_execution_update,auto_retry_*,auth_staleandrecap_updatehave no ACP representation, so there is no event stream a client could reconstruct quiescence from.What changed
Adds
quiescenceto the existing namespaced envelope:Counted from the authoritative live roster (queued plus running) rather than reconstructed from child deltas, and emitted after headless completion so it describes the state at the point the turn is reported done.
The turn reports rather than waits. Blocking
session/prompton quiescence would deadlock against detached subagent semantics — a child can legitimately outlive many turns — so the client decides what to do with the signal.Additive and namespaced: vanilla ACP clients ignore unknown
_meta. No previously-declared-but-unemitted field is touched.Validation
npm run check: Biome 898 files, installer render, browser smoke all passtest/suite/acp-mode.test.ts: 2/2+ 1fails the assertion; restoring passesStacked on #805.
Note
Medium Risk
Changes ACP
session/promptandsession/newfailure modes (roster read failures now reject prompts) and introduce authoritative child-roster plumbing used by external clients; behavior is additive in_metabut correctness-sensitive for scoring/automation.Overview
ACP clients get a quiescence signal in namespaced
_metaafter eachsession/promptturn completes headless work: outstanding subagents (queued/running from a live roster) and remaining autonomous continuations. The turn still reportsend_turnwithout blocking on detached children; clients choose how to act on the signal.AgentSession.getRlmChildSnapshots()builds a recursive live child roster from active runs and retained child sessions (with model on runs), and connection snapshots/daemon clients mergerlm_child_updateevents so empty attach snapshots do not erase live children.ACP session lifecycle hardening: subscribe before the initial snapshot, reject
session/newon snapshot failure (unsubscribe cleanup), guard concurrentsession/newwith an in-flight flag, and failsession/promptif the emission-time roster read fails instead of emitting a false zero count.Reviewed by Cursor Bugbot for commit 6447725. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Expose ACP quiescence metadata with live subagent roster in
session/promptresponsessession/promptnow emits_meta.quiescenceafter completion, reportingoutstandingSubagentsandremainingAutonomousContinuationsfrom a live authoritative child roster snapshot.AgentSession.getRlmChildSnapshotsprovides a recursive, flattened list of child agent snapshots (id, label, status, model, sessionDir) from both the active run registry and retained child sessions.DaemonAgentConnectionnow updates its cachedlatestSnapshot.childrenasrlm_child_updateevents arrive, keeping the in-memory roster current.session/newnow rejects concurrent creation attempts while a first request is pending, and rejects (without claiming the session slot) if the initial roster snapshot read fails.session/promptandsession/newnow fail hard if the live roster snapshot read fails, rather than succeeding with incomplete metadata.Macroscope summarized 6447725.