anomalyco / anomalyco/opencode
Session permanently frozen: drain() failures are logged but never surfaced to the client (no session.error event)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 209k
- Forks
- 27.5k
- PR merge metrics
- PR metrics pending
Description
Summary
SessionRunner.drain() failures are caught server-side and logged (Failed to drain Session <cause>), but the failure is never surfaced to the client through any session event — no session.error, no durable Step.Failed write, nothing a TUI or downstream consumer can react to. A session that hits this path is left permanently open: no assistant turn ever completes, no error is shown, and nothing about the session's own state indicates it needs attention. The only fix from the user side is to notice the freeze out-of-band (watching the server log) and manually send a new prompt/"continue" to the same session.
This looks closely related to #34839 ("V2: pre-runner errors are not surfaced as assistant message failures"), which was reported against a single error family (SessionRunnerModel.ModelUnavailableError) at pre-stream/model-resolution time and is tracked as likely addressed by #35272 ("finalize session event lifecycle", merged 2026-07-06, bf01264). Filing this separately because on v2.0.8 (which is far ahead of bf01264, ahead_by: 3256 commits per the compare API — the fix is present) the same log-only, never-surfaced behavior still reproduces, and across a noticeably wider set of causes than the original report covered.
Evidence — a real instance's log, one server, ~4 days
Scanning one long-running server's log turned up 54 occurrences of "Failed to drain Session", breaking down by cause:
| Cause | Count |
|---|---|
AI.Error (various — see below) |
28 |
Session.StepFailedError: Compaction summary reached the output token limit |
16 |
SessionRunnerModel.ModelUnavailableError |
1 |
(uncategorized/other AI.Error subtypes not itemized above) |
9 |
The AI.Error family alone spans at least these distinct underlying causes, all reaching the identical dead-end:
litellm.Timeout: BedrockException: Timeout Error ...(connection timeout to the upstream provider) — 10 occurrenceslitellm.BadRequestError: BedrockException - {"message":"...does not support assistant message prefill..."}— 7 occurrencesAPI key is invalid.— 4 occurrencesProvider request failed with HTTP 403— 3 occurrencesConnectionRefused: Unable to connect. Is the computer able to access the url?— 2 occurrencesProvider request failed with HTTP 502— 1 occurrenceECONNRESET: The socket connection was closed unexpectedly.— 1 occurrence
Every one of these is caught in SessionRunner.drain() / SessionStep.attempt() (per the stack traces in the log), logged with level=ERROR, and then dropped — the session simply stops advancing, with no user-visible signal that anything went wrong. Session.StepFailedError: Compaction summary reached the output token limit is a second recurring cause with the exact same dead-end, and it isn't a provider/auth/network problem at all — it's an internal failure during automatic compaction, which makes it clearly in-scope for packages/core/src/session/runner's own control flow rather than an upstream-provider edge case.
This is not one flaky provider or one flaky config — it's the general shape of the bug: any exception that reaches SessionRunner.drain()'s catch path, regardless of origin, is swallowed into a log line instead of becoming a durable session event.
Corroborating evidence: a third-party plugin exists purely to work around this
The community plugin opencode-auto-resume exists specifically to detect a stalled/silently-dead session and re-send a "continue" prompt automatically — i.e., it is a plugin-level workaround for the exact absence of a surfaced failure event described here. That a third party built and maintains a whole plugin around "the server goes silent and someone has to resend the message" is itself evidence this is a real, user-facing gap, independent of my own repro.
(Separately, and not the subject of this issue: as of npm's published 1.1.17 and the unreleased main/1.1.18, this plugin's own dist/index.js still exports the pre-V2 function-style (ctx, options) => Hooks plugin shape rather than the object-style Plugin interface @opencode-ai/plugin's V2 SDK (dist/v2/promise/plugin.d.ts / dist/v2/effect/plugin.d.ts) now requires via define({ id, setup }) / define({ id, effect }). It fails to load against 2.0.8 with PluginModule.LoadError: Plugin must export a default definition with an id and an effect or setup function. (cause: SchemaError(Expected object at ["default"])). Mentioned only because it means the community workaround is currently non-functional against 2.0.x, which raises the stakes on fixing the underlying gap directly rather than relying on that plugin.)
Expected behavior
Whenever SessionRunner.drain()'s step loop fails for any reason — pre-stream setup, mid-stream provider error, or a post-stream failure like a compaction overflow — the runner should still write a durable failure event (a Step.Failed-equivalent, or whatever toSessionError/the SessionError envelope added in #35272 produces) before returning, not just call log.error. Clients/TUIs need something in the session's own event stream to render, and downstream integrations (channel bots, automation, anything polling session state) need it even more, since they have no log tail to watch.
Repro steps (generic — this reproduces from any cause that reaches the drain catch path)
- Start a session against a provider/model configuration that will fail mid-drain — easiest reliable trigger is an invalid/expired provider credential, or a model temporarily returning 403/503, or forcing automatic compaction to overflow its own output token limit on a very long session.
- Send a prompt and let the runner begin draining.
- Watch the server log:
Failed to drain Session <cause>appears atlevel=ERROR. - Check the client / TUI / any subscriber of session events: no error is visible, the session just stops progressing.
GET /api/session/{sessionID}and the message list show no failure — the last user message simply has no completed assistant turn. - The only way to unstick it is to notice this out-of-band and send a fresh prompt into the same session (what
opencode-auto-resumeattempts to automate, and what I ended up scripting myself against the local HTTP API'ssession.promptoperation as a stopgap).
Version info
- OpenCode:
v2.0.8(confirmed current —opencode upgradereports up to date, and I diffedv2.0.7...v2.0.8/v2.0.0...v2.0.8via the GitHub compare API for anything touching drain/compaction/session-event code; nothing in that range addresses this) - Provider: an OpenAI-compatible gateway in front of Amazon Bedrock Anthropic models (litellm), reached through a local proxy — but per the cause breakdown above the failure shape is provider-agnostic; the same dead-end is hit by auth errors, network errors, HTTP-level provider errors, and a purely-internal compaction failure.
- Confirmed
bf01264(#35272's merge commit) is an ancestor ofv2.0.8(ahead_by: 3256, not diverged), so this is not the pre-fix behavior #34839 was about — it's the same class of gap surviving past that fix, for a wider set of causes.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in packages/core/src/session/runner at SessionRunner.drain() and trace the SessionStep.attempt() failure path, including compaction failures. Compare the existing session event and SessionError handling added by #35272. Done means every drain failure produces a durable Step.Failed-equivalent or SessionError event that clients and session-state consumers can observe instead of only a log entry.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 64/100