Skip to content

feat(coding-agent): expose ACP quiescence metadata - #806

Closed
sethkarten wants to merge 6 commits into
feat/acp-resident-lifecyclefrom
feat/acp-quiescence-meta
Closed

feat(coding-agent): expose ACP quiescence metadata#806
sethkarten wants to merge 6 commits into
feat/acp-resident-lifecyclefrom
feat/acp-quiescence-meta

Conversation

@sethkarten

@sethkarten sethkarten commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

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_turn can therefore cut off live work, and the result depends on a race.

This is not derivable client-side: tool_execution_update, auto_retry_*, auth_stale and recap_update have no ACP representation, so there is no event stream a client could reconstruct quiescence from.

What changed

Adds quiescence to the existing namespaced envelope:

_meta["ai.primeintellect.prime-agent"].quiescence = {
  outstandingSubagents: number,
  remainingAutonomousContinuations: number,
}

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/prompt on 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 pass
  • test/suite/acp-mode.test.ts: 2/2
  • mutation: changing the outstanding count to + 1 fails the assertion; restoring passes

Stacked on #805.


Note

Medium Risk
Changes ACP session/prompt and session/new failure modes (roster read failures now reject prompts) and introduce authoritative child-roster plumbing used by external clients; behavior is additive in _meta but correctness-sensitive for scoring/automation.

Overview
ACP clients get a quiescence signal in namespaced _meta after each session/prompt turn completes headless work: outstanding subagents (queued/running from a live roster) and remaining autonomous continuations. The turn still reports end_turn without 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 merge rlm_child_update events so empty attach snapshots do not erase live children.

ACP session lifecycle hardening: subscribe before the initial snapshot, reject session/new on snapshot failure (unsubscribe cleanup), guard concurrent session/new with an in-flight flag, and fail session/prompt if 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/prompt responses

  • session/prompt now emits _meta.quiescence after completion, reporting outstandingSubagents and remainingAutonomousContinuations from a live authoritative child roster snapshot.
  • AgentSession.getRlmChildSnapshots provides a recursive, flattened list of child agent snapshots (id, label, status, model, sessionDir) from both the active run registry and retained child sessions.
  • DaemonAgentConnection now updates its cached latestSnapshot.children as rlm_child_update events arrive, keeping the in-memory roster current.
  • session/new now rejects concurrent creation attempts while a first request is pending, and rejects (without claiming the session slot) if the initial roster snapshot read fails.
  • Risk: session/prompt and session/new now fail hard if the live roster snapshot read fails, rather than succeeding with incomplete metadata.

Macroscope summarized 6447725.

@sethkarten
sethkarten requested review from alexzhang13 and snimu August 7, 2026 00:14
Comment thread packages/coding-agent/src/modes/acp/acp-mode.ts Outdated
Comment thread packages/coding-agent/src/modes/acp/acp-mode.ts Outdated
Comment thread packages/coding-agent/src/modes/acp/acp-mode.ts Outdated
@sethkarten
sethkarten force-pushed the feat/acp-quiescence-meta branch from f1e49eb to a0ec4a5 Compare August 7, 2026 00:44
Comment thread packages/coding-agent/src/modes/acp/acp-mode.ts Outdated
Comment thread packages/coding-agent/src/modes/acp/acp-mode.ts Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

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

Comment thread packages/coding-agent/src/main.ts
@sethkarten
sethkarten force-pushed the feat/acp-quiescence-meta branch 3 times, most recently from b6e6fa6 to 2efaa15 Compare August 7, 2026 08:12
Comment thread packages/coding-agent/src/modes/acp/acp-mode.ts
@snimu

snimu commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

One more thing: this adds user-visible ACP surface (quiescence in _meta, and session/prompt now rejecting when the emission-time roster read fails even though the turn completed) but has no CHANGELOG entry. Please add a bullet under ## [Unreleased] in packages/coding-agent/CHANGELOG.md.

sethkarten and others added 6 commits August 10, 2026 10:42
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
sethkarten force-pushed the feat/acp-quiescence-meta branch from 2efaa15 to 6447725 Compare August 10, 2026 17:44
@sethkarten

Copy link
Copy Markdown
Contributor Author

Addressed in 6447725: packages/coding-agent/CHANGELOG.md now documents ACP _meta.quiescence and the prompt rejection behavior when the emission-time live roster read fails.

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.
@sethkarten

Copy link
Copy Markdown
Contributor Author

Closing this superseded ACP implementation. Resident recovery coverage continues in #1236, with the exact causal prompt lifecycle successor in #1239. This PR remains available as design and review history.

@sethkarten sethkarten closed this Aug 14, 2026
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