Repository navigation
Fallback chain silently dies when session.abort() fails or hangs (no try/catch / timeout in abortSession) #6
Description
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
pluginarray, per-agent fallback chains,cooldownMs3600000,maxRetries2
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 errornor acancelentry 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