Skip to content

Fallback chain silently dies when session.abort() fails or hangs (no try/catch / timeout in abortSession) #6

Closed

Description

@acaliebe

Fallback chain silently dies when session.abort() fails or hangs (no try/catch / timeout in abortSession)

Environment

  • opencode-auto-fallback: v0.4.59 (latest on npm)
  • opencode: v1.18.31 (also observed on v1.18.30)
  • OS: Linux (Debian 13), plugin loaded via plugin array, per-agent fallback chains, cooldownMs 3600000, maxRetries 2

Summary

abortSession() (dist index.js, ~L702) awaits context.client.session.abort() with no try/catch and no timeout. Both handleImmediate() (~L1386) and handleRetry() (~L1321) call it before starting the fallback chain. If that request rejects or hangs, handleImmediate dies silently: no fallback chain is started, nothing is logged, the session stays idle — and the original task is never executed.

Note: a safe variant abortSessionSafely() (with try/catch) already exists in the codebase and is used elsewhere (e.g. the large-context switch) — but the two most important paths (handleImmediate / handleRetry) use the unsafe one. And even abortSessionSafely has no timeout, so a hanging request would still block forever.

Observed failure (production, reproducible pattern)

Automated scheduled task, agent prod with a 2-model fallback chain. Plugin log:

[INFO] Permanent rate-limit detected, falling back immediately  {"message":"Free usage exceeded, subscribe to Go"}
[INFO] Model opencode/big-pickle in cooldown
# ... silence. No "Immediate fallback chain (N models)", no "Trying fallback 1/2", no error.

Findings from the post-mortem (session DB + opencode.log + plugin log):

  • The abort call never took effect: opencode's own log shows neither a stream error nor a cancel entry for this session — in every working fallback case, both appear.
  • The session went idle and stayed idle for ~2.5 hours. The scheduled task was never executed.
  • Aggravating factor: the task was handled by an opencode process that had booted ~0.3 seconds earlier (our scheduled runner effectively hits a freshly-initializing process). A failing/hanging abort request in a not-fully-initialized process is realistic in exactly this automated setup.
  • Re-sending a message later (healthy process) triggered the fallback chain normally — detection and chain config were fine; only the abort step killed the flow.

Why try/catch alone is not enough

A hanging HTTP request (the SDK call has no timeout) would still block handleImmediate forever. The abort is best-effort by nature — the fallback chain should always proceed regardless of the abort outcome.

Suggested fix

async function abortSession(sessionID, context) {
  await Promise.race([
    context.client.session.abort({ path: { id: sessionID } }),
    new Promise((resolve) => setTimeout(resolve, 5000))
  ]).catch(() => {});
  await new Promise((resolve) => setTimeout(resolve, ABORT_DELAY_MS));
}

(equivalently: race abortSessionSafely with a timeout and use it in handleImmediate / handleRetry)

We have been running this patch in production since 2026-09-17 — no regressions so far.

Happy to open a PR if you're interested.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions