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
When an LLM call fails after the retry policy in session/retry.ts is exhausted, session/processor.ts halt() updates internal state (ctx.assistantMessage.error, Bus.Session.Event.Error, EventV2.SessionEvent.Step.Failed.Sync) but never emits any ACP session/update notification or other frame to the connected client.
From an ACP client's perspective the turn is silently stuck:
No stopReason
No error notification
No final session/update
The turn was started by session/prompt, the agent emitted some agent_message_chunks (or zero, for an immediate failure), and then nothing.
Reproduction
Configure any model with a deterministic non-retriable failure (the simplest is a backend that returns 429 with a body the AI SDK retries on, until SessionRetry's budget is exhausted; a 502 storm works the same way).
Send session/prompt.
Observe: client receives no terminating frame after the retry window.
The retry policy itself is fine — the issue is purely in what happens after Effect.retry(SessionRetry.policy(...)) gives up and the retry error reaches halt() via Effect.catch(halt).
Expected
When halt() runs, the connected ACP client should receive a session/update (or equivalent ACP frame) that:
Indicates the turn ended in an error state (stopReason: "error").
Carries enough information for the client to render an error to the user instead of waiting forever.
Actual
halt() (packages/opencode/src/session/processor.ts, around the function definition Effect.fn("SessionProcessor.halt")):
Both bus.publish(Session.Event.Error, ...) and EventV2.run(SessionEvent.Step.Failed.Sync, ...) are internal to the opencode process. Neither produces an ACP wire frame — the ACP connection.sessionUpdate(...) callsite in packages/opencode/src/acp/agent.ts only translates a subset of bus events into ACP frames (message.part.updated, permission.asked, compact, etc.); there is no case for session.error.
Suggested fix
Add a handler in packages/opencode/src/acp/agent.ts handleEvent() for event.type === "session.error" that:
Reads the error payload (MessageV2.APIError carries responseHeaders / responseBody / statusCode, sufficient to characterize the failure).
Calls this.connection.sessionUpdate({ sessionId, update: { sessionUpdate: ..., ... } }) to notify the client.
Two design choices for the update shape:
Minimal: emit a session/update of an existing kind with stopReason: "error" and the error message inline. No protocol change required.
Typed (preferred): introduce a new agent_errorSessionUpdate kind with a structured payload (type, message, retryable, optional retry-after / reset-at / etc.) so clients can render type-specific copy (rate-limit retry timer, budget reset time, context-overflow vs auth, …) instead of just a generic error string. I have a separate PR proposing this kind: feat(acp): add AgentErrorUpdate session/update kind for typed LLM error propagation #26306.
Happy to send a PR for whichever direction you'd prefer. My current draft goes with the typed kind from #26306; if #26306 is unwanted, the same fix lands easily on a stopReason: "error"-on-existing-kind variant.
Environment
opencode dev branch as of this issue. The halt() location and bus event names referenced above match the current source; the relevant control flow (Effect.retry(SessionRetry.policy(...)) → Effect.catch(halt)) lives in packages/opencode/src/session/processor.ts's process function.
Summary
When an LLM call fails after the retry policy in
session/retry.tsis exhausted,session/processor.ts halt()updates internal state (ctx.assistantMessage.error,Bus.Session.Event.Error,EventV2.SessionEvent.Step.Failed.Sync) but never emits any ACPsession/updatenotification or other frame to the connected client.From an ACP client's perspective the turn is silently stuck:
stopReasonsession/updateThe turn was started by
session/prompt, the agent emitted someagent_message_chunks (or zero, for an immediate failure), and then nothing.Reproduction
429with a body the AI SDK retries on, untilSessionRetry's budget is exhausted; a502storm works the same way).session/prompt.The retry policy itself is fine — the issue is purely in what happens after
Effect.retry(SessionRetry.policy(...))gives up and the retry error reacheshalt()viaEffect.catch(halt).Expected
When
halt()runs, the connected ACP client should receive asession/update(or equivalent ACP frame) that:stopReason: "error").Actual
halt()(packages/opencode/src/session/processor.ts, around the function definitionEffect.fn("SessionProcessor.halt")):Both
bus.publish(Session.Event.Error, ...)andEventV2.run(SessionEvent.Step.Failed.Sync, ...)are internal to the opencode process. Neither produces an ACP wire frame — the ACPconnection.sessionUpdate(...)callsite inpackages/opencode/src/acp/agent.tsonly translates a subset of bus events into ACP frames (message.part.updated,permission.asked,compact, etc.); there is no case forsession.error.Suggested fix
Add a handler in
packages/opencode/src/acp/agent.ts handleEvent()forevent.type === "session.error"that:MessageV2.APIErrorcarriesresponseHeaders/responseBody/statusCode, sufficient to characterize the failure).this.connection.sessionUpdate({ sessionId, update: { sessionUpdate: ..., ... } })to notify the client.Two design choices for the
updateshape:session/updateof an existing kind withstopReason: "error"and the error message inline. No protocol change required.agent_errorSessionUpdatekind with a structured payload (type,message,retryable, optional retry-after / reset-at / etc.) so clients can render type-specific copy (rate-limit retry timer, budget reset time, context-overflow vs auth, …) instead of just a generic error string. I have a separate PR proposing this kind: feat(acp): add AgentErrorUpdate session/update kind for typed LLM error propagation #26306.Happy to send a PR for whichever direction you'd prefer. My current draft goes with the typed kind from #26306; if #26306 is unwanted, the same fix lands easily on a
stopReason: "error"-on-existing-kind variant.Environment
opencode
devbranch as of this issue. Thehalt()location and bus event names referenced above match the current source; the relevant control flow (Effect.retry(SessionRetry.policy(...)) → Effect.catch(halt)) lives inpackages/opencode/src/session/processor.ts'sprocessfunction.