You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
acp/agent: prompt() silently swallows SDK errors — end_turn returned with no content on failure #27779
When the HTTP call fails (e.g. returns 400 or 500), response.data is undefined. The handler has no error check and returns a bare end_turn with usage: undefined — no exception, no error event, no diagnostic.
Reproduction
Upload an image that causes image.normalize() to fail (e.g. DecodeError or SizeError on an unusual PNG). The ACP client receives { stopReason: "end_turn", usage: undefined } with no preceding events, indistinguishable from a normal empty turn.
Impact
ACP clients (e.g. the AgentBridge IntelliJ plugin) cannot distinguish this failure from a "session corrupted" case. Without a real error signal they can only apply heuristics — ultimately leading to misleading UX (session-was-reset message) and lost session context.
Proposed Fix
Add { throwOnError: true } to the sdk.session.prompt() call:
When the SDK throws, the ACP JSON-RPC framework will surface it as a proper error response to the client. This gives ACP clients a real signal to act on (show a targeted error, keep the session alive) rather than silently pretending the turn succeeded with no output.
Note: other SDK calls in the same file already use throwOnError: true (e.g. listSessions, forkSession, message), so this pattern is already established.
If your issue is distinct (e.g. specific to the throwOnError fix path rather than the msg.error check path), please clarify the difference so this can be tracked separately.
Thanks for the pointer to #24494 — the root cause is the same: sdk.session.prompt() in acp/agent.ts doesn't surface errors over ACP, so clients can't distinguish a clean empty turn from a failed one.
Our specific trigger is image normalization, not LLM retry exhaustion, so it's worth clarifying whether PRs #26306 + #26352 cover it:
Image normalization failure path:
image.normalize() in session/prompt.ts throws DecodeError/SizeError
These propagate as unhandled Effect failures → HTTP 500 from the internal REST API
sdk.session.prompt() (no throwOnError) returns {data: undefined}
No session.error bus event is published — the failure is pure HTTP-level, before processor.ts halt() is ever called
What #26352 covers:halt() failures (retry-exhausted LLM calls). These do publish session.error to the bus, so the new case "session.error": in handleEvent() will catch them.
What #26352 does NOT cover (our case): prompt-preprocessing failures like image.normalize(), which die at the sdk.session.prompt() HTTP call layer, never touching the bus. The fix there would be either:
Related failure mode worth folding in: when the underlying error is a retryable one that never succeeds (a hard opencode-goGoUsageLimitError 429), prompt() doesn't even return a bare end_turn — it never returns at all, because SessionRetry.policy() keeps sleeping on the multi-hour retry-after indefinitely (#21960). Over ACP that's total silence:
→ session/prompt … no response, no session/update, no stopReason
So there are two layers here: once the retry loop is bounded (#21960 / #26369), this degrades into exactly the bare-end_turn-with-no-content symptom you describe — which means adding throwOnError / surfacing the assistant message's internal error is necessary for the failure to ever become visible to the client. Both fixes are needed; this issue is the one that makes the turn-level provider error protocol-visible.
PR #29061 implements this (session.prompt(..., { throwOnError: true })) and declares it Closes this issue — though note it currently surfaces the error as a text chunk and still returns stopReason: "end_turn", and it only fires once the prompt RPC returns (which the unbounded retry in #21960 prevents for hard Go limits).
Activity
github-actions commentedon May 15, 2026
This issue might be a duplicate of existing issues. Please check:
acp/agent.tswheresdk.session.prompt()result is not checked for errors, causing a successfulend_turnto be returned on failure.If your issue is distinct (e.g. specific to the
throwOnErrorfix path rather than themsg.errorcheck path), please clarify the difference so this can be tracked separately.catatafishen commentedon May 15, 2026
Thanks for the pointer to #24494 — the root cause is the same:
sdk.session.prompt()inacp/agent.tsdoesn't surface errors over ACP, so clients can't distinguish a clean empty turn from a failed one.Our specific trigger is image normalization, not LLM retry exhaustion, so it's worth clarifying whether PRs #26306 + #26352 cover it:
Image normalization failure path:
image.normalize()insession/prompt.tsthrowsDecodeError/SizeErrorsdk.session.prompt()(nothrowOnError) returns{data: undefined}session.errorbus event is published — the failure is pure HTTP-level, beforeprocessor.ts halt()is ever calledWhat #26352 covers:
halt()failures (retry-exhausted LLM calls). These do publishsession.errorto the bus, so the newcase "session.error":inhandleEvent()will catch them.What #26352 does NOT cover (our case): prompt-preprocessing failures like
image.normalize(), which die at thesdk.session.prompt()HTTP call layer, never touching the bus. The fix there would be either:response.data?.info.erroraftersdk.session.prompt()returns and emitagent_errorthere too (as ACP adapter returns end_turn even when assistant message has internal error #24494 suggested), orprompt.tscatchDecodeError/SizeErrorand publishbus.Session.Event.ErrorexplicitlySo these are complementary. #24494 + #26352 fix the processor side; the prompt-preprocessing side is still a gap.
carlosflorencio commentedon May 31, 2026
Related failure mode worth folding in: when the underlying error is a retryable one that never succeeds (a hard
opencode-goGoUsageLimitError429),prompt()doesn't even return a bareend_turn— it never returns at all, becauseSessionRetry.policy()keeps sleeping on the multi-hourretry-afterindefinitely (#21960). Over ACP that's total silence:So there are two layers here: once the retry loop is bounded (#21960 / #26369), this degrades into exactly the bare-
end_turn-with-no-content symptom you describe — which means addingthrowOnError/ surfacing the assistant message's internalerroris necessary for the failure to ever become visible to the client. Both fixes are needed; this issue is the one that makes the turn-level provider error protocol-visible.PR #29061 implements this (
session.prompt(..., { throwOnError: true })) and declares it Closes this issue — though note it currently surfaces the error as a text chunk and still returnsstopReason: "end_turn", and it only fires once the prompt RPC returns (which the unbounded retry in #21960 prevents for hard Go limits).