Repository navigation
V2: pre-runner errors are not surfaced as assistant message failures #34839
Description
Summary
In V2, some errors that V1 surfaced as assistant-message failures are now logged only and never propagated into the session transcript/event stream. Downstream clients and channel adapters then have nothing to render, so the user sees silence even though the runtime logged a failure.
Observed Behavior
A personal OpenCode Agent runtime received messages over Sendblue/Discord and started typing, but replies never appeared. The only visible signal was in the service log:
Failed to drain Session SessionRunnerModel.ModelUnavailableError: Model unavailable: opencode/gpt-5.5
No assistant error message or generic failure notice was emitted to the channel.
Why This Happens
The failure occurs before the LLM event publisher exists. In packages/core/src/session/runner/llm.ts, models.resolve(session) runs during pre-stream setup. If that throws, the code exits before creating the publisher and before a durable SessionEvent.Step.Failed can be written.
Later, packages/core/src/session/execution/local.ts logs Failed to drain Session, but it does not convert that failure into a durable session event.
This means projections/subscribers only see the admitted user input and typing state; they never see a failed assistant step.
Expected Behavior
Pre-stream/pre-runner failures should still become durable assistant failure events, similar to V1 behavior where assistant-message errors were visible to the user. At minimum, these failures should produce a Step.Failed-like event so clients and integrations can render a generic failure message.
Impact
- TUI/channel clients can hang silently after typing starts.
- Discord/Slack/Sendblue integrations cannot notify the user because they only publish from durable session events.
- Operators must inspect server logs to learn that a turn failed.
Possible Fix Direction
Wrap pre-stream setup in the V2 session runner, especially model resolution and other setup before the LLM publisher is created, so failures are captured into a durable assistant failure event before returning/logging the error.
Alternatively, have SessionExecution translate runner failures into a durable failure event, but the runner likely has better context for assistant message identity and event shape.
Notes
This was observed with ModelUnavailableError, but the issue is broader: any failure before the publisher/event stream exists can be logged without becoming visible user-facing state.
Activity
opencode-agent commentedon Jul 2, 2026
Tracker note: added to V2 TODO tracking. This is release-relevant because silent pre-runner failures leave users with no visible assistant failure state. Related areas: session runner failure projection, EventV2 durability, and channel/TUI adapters relying on durable session events.
opencode-agent commentedon Jul 10, 2026
Audit: likely addressed by PR #35272, which added durable execution failure events, the public
SessionErrorenvelope,toSessionError, and TUI handling for failed executions. Leaving open for one verification pass against the original pre-runner failure repro before closing.orgito commentedon Aug 20, 2026
Additional repro material for the verification pass noted above.
Scope, up front: these were observed on v1.18.18, which does not contain #35272 — GitHub's compare API reports
v1.18.18...bf01264asdivergedrather thanbehind, i.e. that merge commit is not an ancestor of the tag, andtoSessionErroris absent frompackages/core/srcon bothdevandproduction. So this is not evidence that the fix failed. It is extra repro surface for whoever runs the verification pass.The affected error family is wider than the single reported error. The original report is
ModelUnavailableError, but at least two siblings fromSessionRunnerModelreach the same log-only path and produce byte-identical user-visible behaviour. Both were logged under the sameFailed to drain Sessionmessage:Both surface from model resolution during pre-stream setup — the mechanism described in the issue body — so all four members of
SessionRunnerModel.Error(ModelNotSelectedError,ModelUnavailableError,VariantUnavailableError,UnsupportedApiError) plausibly share the same fate.Wire-level behaviour, identical for each:
POST /api/session/{id}/promptreturns200with a monotonicadmittedSequserrecord and twomodel-switchedrecordspromptedObserved from an HTTP API consumer with no TUI attached, so no client-side rendering is involved — the durable event genuinely isn't there.
Suggested addition to the verification pass: exercise at least one non-
ModelUnavailableErrormember of the family. A pass that only coversModelUnavailableErrorcould go green whileVariantUnavailableErrorandUnsupportedApiErrorstill fail silently, since the variant lookup and the API-support check sit at different points in resolution.For reference, on
dev/productionthe drain failure is still log-only inpackages/core/src/session/execution/local.ts(Effect.logError("Failed to drain Session", cause)), with no conversion into a durable event, consistent with the issue's diagnosis.A machine's own OpenCode sign-in its provider refuses stands on the m…
opencodejr commentedon Oct 29, 2026
Thanks for the detailed write-up. I reproduced this on v2.2.0, and it still happens on the latest
dev(8de0323).Steps (the script below automates them with the simulated model):
opencode/gpt-5.5, a model this server can't route, then send another prompt.Result: model resolution fails with
Model unavailable: opencode/gpt-5.5before the step starts. The TUI shows a "Session failed" toast for a few seconds. Once it fades, the transcript shows only the user message, with no assistant message and no error (see the recording and screenshots below).message.listends withuser→idle { outcome: "failed" }, and that idle entry has no error text.One correction: the event stream does get a failure event.
session.execution.failed, which carries the error, is published (that's what drives the toast). What's missing is a durable failed assistant message or step in the transcript, and the error text on the idle message.recording.mp4
Reproduction script (drive.ts)
Run it with opencode-drive:
opencode-drive run ./drive.ts. It drivesopencodefrom yourPATH; setOC_DEVto the path of an OpenCode checkout to run that from source instead.