Skip to content

Bracket agent-initiated ACP turns so unprompted work renders - #2220

Open
SawyerHood wants to merge 1 commit into
mainfrom
fix/2122-acp-agent-initiated-turns
Open

Bracket agent-initiated ACP turns so unprompted work renders#2220
SawyerHood wants to merge 1 commit into
mainfrom
fix/2122-acp-agent-initiated-turns

Conversation

@SawyerHood

Copy link
Copy Markdown
Collaborator

What was wrong

When an ACP agent streams work with no session/prompt in flight (the concrete case is OMP's async-job auto-delivery), handleAgentNotification in the provider-acp bridge forwarded every session/update to the translator without opening a turn. ACP has no turn bracket for agent-initiated work, the translator is context-free, and the runtime's delta assembler refuses to let item/stream deltas open a turn, so each unprompted chunk was demoted to a thread-scoped provider/unhandled row. Those rows are hidden unless showUnhandledProviderEvents is on, so the thread simply never answered. Root cause and live repro: https://get-bb.github.io/reports/issues/2122.html.

#2123 (thanks @bradhallett) correctly identified the layer and the shape (a bridge-emitted turn.open, per provider-bridge-protocol turn lifecycle rule 3). It was reviewed REQUEST CHANGES because it tracked the vouched turn in a parallel spontaneousTurnOpen boolean, so the existing readers of activePromptKind kept their old semantics: an agent exit left the turn open forever (thread hangs in Working), permission requests inside the turn were auto-cancelled even in full mode, and the 120 s quiet window buffered the last message and reported "Worked for 2m".

What changed

All in plugins/provider-acp (bridge-local; no wire shape changes, so no HOST_DAEMON_PROTOCOL_VERSION bump).

  • src/bridge/bridge.ts: agent-initiated work is folded into the bridge's existing open-turn mirror, activePromptKind: "turn" | "compaction" | "agent" | null, instead of a second flag, so every reader had to decide what an agent turn means:
    • handleAgentNotification opens the turn (ACP_TURN_STARTED_METHOD) when activePromptKind === null and the update is a work kind, and re-arms a 5 s quiet timer per work update. The timer does not close the turn while a permission is pending.
    • Settled on every exit path: turn/start settles a still-open agent turn (end_turn) before the check for an active turn, so the next user prompt or compaction never sees "A turn is already active"; thread/stop (interrupt) settles it as cancelled; agent exit emits the settling error through emitSessionError while the turn is still marked open, then clears the mirror, so the assembler closes the turn as failed; a release stop detaches without fabricating a terminal state (Release agent runtimes on thread stop #1584), same as a prompted turn.
    • handlePermissionRequest treats "agent" like "turn": full mode auto-allows, other modes forward to the user.
    • turn/steer during an agent turn still returns NO_ACTIVE_TURN; the runtime already maps that to a fresh turn/start, which settles the agent turn first.
  • src/visibility.ts: the work-kind set is derived from the visibility metadata (AGENT_WORK_ACP_UPDATE_KINDS = normalized kinds minus usage_update, exported as isAgentWorkAcpUpdateKind) rather than a second hand-maintained map.
  • src/bridge/fake-acp-agent.mjs: new agent-initiated prompt behavior (OMP wire shape: echoed user_message_chunk, agent chunks, a tool call that completes) with :permission, :exit, and :noise variants.

Differences from #2123: one mirror instead of two; settled on agent exit and on release; permission requests inside the turn are handled; 5 s quiet window instead of 120 s; the quiet window never closes a turn with an unanswered permission; no test-only timeout hook (the tests run against the real window).

How you verified

  • New tests in src/bridge/bridge.test.ts under "agent-initiated turns (provider-acp silently drops agent-initiated turns (unprompted session updates, e.g. OMP async-job delivery) #2122)". With the source change stashed, 6 of 7 fail (the unprompted text never becomes an agent message; no second turn); with it, all 7 pass:
    • brackets unprompted agent work as a turn (2 turn/started, 0 provider/unhandled, tool item rendered, exactly one input.accepted) and closes it completed after the quiet window
    • does not open a turn for unprompted non-work updates
    • settles the agent turn before the next user turn opens (3 turns, all completed)
    • interrupts the agent turn on thread/stop
    • fails the agent turn when the agent process exits mid-turn (terminal state instead of a hang)
    • auto-allows a permission request inside an agent turn in full mode
    • forwards a permission request inside an agent turn in ask mode and keeps the exchange in one turn
  • The report's issue-2122.repro.test.ts and both hostile probes in issue-2122.pr-edges.test.ts (agent exit, permission during vouched turn), which fail on Vouch agent-initiated ACP turns so async job delivery renders #2123, pass on this branch.
  • pnpm exec turbo run test typecheck lint --filter=bb-plugin-provider-acp --force: 7/7 tasks green, 15 test files / 203 tests passed.
  • Live on my own dev instance (scripts/bb-dev-app current), registering the fake agent via customAcpAgents: thread spawn --provider acp-fakeacp --prompt agent-initiated produced turn t2 with the agent message, a fileRead item, and the closing message, turn/completed completed 4.98 s after the last chunk, no provider/unhandled rows; the timeline API shows the turn and the final assistant message. --prompt agent-initiated:exit produced provider/error + turn/completed failed on turn t4 within 70 ms of the exit and the thread status is error, not a hung Working state.

Fixes #2122

AGENT GENERATED: by Claude Opus 5

When an ACP agent streams work with no session/prompt in flight (OMP's
async-job delivery), the bridge forwarded the updates without opening a
turn, so the runtime assembler demoted each one to a hidden thread-scoped
provider/unhandled row and the user saw nothing.

The bridge now folds agent-initiated work into its open-turn mirror
(activePromptKind: "turn" | "compaction" | "agent" | null). A work-kind
update arriving idle opens a turn; a 5 s quiet window ends it; the next
turn/start settles it first; thread/stop interrupts it; an agent exit fails
it through the settling error. Permission requests inside an agent turn are
handled like prompted ones instead of auto-cancelled.

Co-Authored-By: Claude <noreply@anthropic.com>
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.

provider-acp silently drops agent-initiated turns (unprompted session updates, e.g. OMP async-job delivery)

1 participant