Skip to content

Retry session_busy prompt rejections once the agent goes idle - #3004

Open
bradhallett wants to merge 5 commits into
get-bb:mainfrom
bradhallett:feat/acp-session-busy-retry
Open

Retry session_busy prompt rejections once the agent goes idle#3004
bradhallett wants to merge 5 commits into
get-bb:mainfrom
bradhallett:feat/acp-session-busy-retry

Conversation

@bradhallett

@bradhallett bradhallett commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Human comments

What was wrong

When omp is streaming without an ACP-owned turn in flight — the completion-instant race (a steer lands in a turn's final second, bb closes its turn bookkeeping while omp is still finishing) or deferred agent-initiated continuations (#2122, #2461) — omp's ACP layer lets session/prompt fall through to a bare AgentSession.prompt(), which throws AgentBusyError; since omp#9825 that rejection is typed on the wire as -32003 with data.reason: "session_busy". The bridge had no path for that typed rejection, so it fell into the generic error path: the turn failed and dispatch re-sent into the still-busy session, producing rejection storms (evidence: +3 ms rejection after turn/input/accepted, retry turn 23 ms later, worst storm 8 rejections over 105 s until the ~8-minute client timeout; one thread terminally stuck in error). Full diagnosis and event-log evidence in #3009.

What changed

  • packages/provider-bridge-acp/src/bridge/agent-connection.tsAcpAgentResponseError now carries the JSON-RPC error data (optional constructor arg), so the typed reason is inspectable. No wire changes.
  • packages/provider-bridge-acp/src/bridge/bridge.ts
    • Branch only on code -32003 and data.reason === "session_busy"; any other shape falls through to the existing error path unchanged.
    • On the first such rejection for a prompt: wait for the agent to report idle, then retry the prompt exactly once. Idle signal = end-of-turn session_info_update notification (ACP v1 has no idle status variant; omp emits this last at end of turn). session/update notifications are counted unconditionally in handleAgentNotification, so the coalesced-chunk case — busy error and idle update arriving in one stdout chunk, before the wait is armed — is detected via a pre-request snapshot and retries immediately.
    • A second busy rejection for the same prompt, or no idle signal within the timeout (default 10 min, overridable via BB_ACP_SESSION_BUSY_RETRY_TIMEOUT_MS), surfaces the original busy error through the existing failure path.
    • The retry skips acceptTurnInput, so no duplicate input.accepted delta is emitted for the resent prompt; each new prompt gets a fresh retry budget; outstanding waits are settled on finishTurn/stopSession/removeSession so nothing dangles.
  • packages/provider-bridge-acp/src/bridge/fake-acp-agent.mjsFAKE_ACP_SESSION_BUSY_{PROMPTS,SKIP,REASON,SAME_CHUNK,IDLE_DELAY_MS} knobs reproducing omp's exact wire shape (code, message, data { reason, hint }, end-of-turn usage_updatesession_info_update ordering, single-chunk coalescing).
  • packages/plugin-sdk + packages/domain/src/plugin-sdk-version.ts — version bumped 0.4.40 → 0.4.41 (the bridge output is part of the published SDK artifact; the repo's version-bump check requires it).

How you verified

  • Six new bridge tests (318 total in the package), each mutation-verified by the author: busy prompt retried after the agent reports idle; second busy rejection fails the turn with the original error; wrong or missing reason fails immediately (no retry consumed); no idle before the retry timeout fails with the original error (BB_ACP_SESSION_BUSY_RETRY_TIMEOUT_MS injectable); busy error + idle update coalesced into one chunk retries immediately; each new prompt gets a fresh retry budget.
  • Independently re-verified after syncing with main at debb58d10 (merge commit 05b300885): pnpm exec turbo run test --filter=@bb/provider-bridge-acp — 318/318 passed; pnpm exec turbo run typecheck lint for @bb/provider-bridge-acp, @bb/domain, @get-bb/plugin-sdk — 6/6 green; pnpm exec oxfmt --check clean on all five touched files. Mutation re-check: inverting the data.reason === "session_busy" gate fails 4 of the 5 busy-matched tests; restoring it returns them to green.

Fixes #3009

AGENT GENERATED

… goes idle

Branch only on ACP -32003 with data.reason "session_busy": queue the
prompt instead of failing the turn, resend it exactly once on the next
end-of-turn session update, and fall back to the original error on a
second busy rejection or after a 10-minute wait (overridable via
BB_ACP_SESSION_BUSY_RETRY_TIMEOUT_MS for tests).
… the idle update

The busy wait is armed a microtask after the -32003 response lands, so a
session_info_update dispatched in the same stdout chunk was consumed
before the wait existed and the prompt stalled until the timeout. Count
session_info_update notifications unconditionally and treat one observed
after the prompt request as idle. The fake agent gains
FAKE_ACP_SESSION_BUSY_SAME_CHUNK to reproduce the coalesced delivery,
and the retry test now pins the input.accepted delta count.
@bradhallett

Copy link
Copy Markdown
Contributor Author

Upstream report for the omp-side half (typed session_busy rejection, no queueing on the ACP prompt path): can1357/oh-my-pi#10707. This PR remains the complete host-side fix regardless of how the upstream discussion lands — it only consumes the typed error omp ships today.

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: omp session_busy prompt rejections fail the turn and re-send in a loop (busy with no ACP turn)

1 participant