fix(runtime-fallback): preserve attemptCount when our own abort is the cause (closes #4006)
When runtime-fallback aborts an in-flight request to swap in a fallback
model, opencode emits session.error{isAbort:true} as a consequence. The
existing event handler treated that as a user cancellation and called
resetRetryState — wiping attemptCount. Every subsequent provider
auto-retry signal then started over at attempt:1, never reaching
max_fallback_attempts, producing an infinite retry loop firing a new
fallback every ~2 seconds.
The bug only surfaces when the configured fallback target itself
silently fails (e.g. github-copilot quota exhausted): the original
model keeps re-emitting retry signals, our handler keeps "fixing"
them, the counter never advances. Reproducible on upstream/dev HEAD
(5ffbe0e24e).
Fix:
- New `internallyAbortedSessions: Set<string>` on HookDeps tracks
sessions whose abort we triggered ourselves.
- abortSessionRequest in auto-retry.ts adds the session to the set
when called with one of our internal sources:
"session.status.retry-signal", "message.updated.retry-signal",
"session.timeout". The "session.stop" source (user-initiated) is
intentionally NOT marked — that path must still wipe state.
- handleSessionError in event-handler.ts checks the set before the
cancellation branch. If the session is marked, consume the flag
(delete it so a later user-abort still gets the reset) and skip
resetRetryState. The state's attemptCount is preserved, so the
next iteration progresses 1→2→3→... until max_fallback_attempts.
- dispose() clears the new set alongside the other per-session maps.
Tests: 3 new event-handler integration tests cover the fix
(internal-abort preserves state, external-abort still resets,
consecutive internal-abort cycles advance attemptCount). Existing
tests pass: 7/7 on event-handler. Pre-existing 2 dispose-test flakes
on the full runtime-fallback suite were verified to exist on
upstream/dev without this patch — unrelated.
bun run build: pass. bun run typecheck: pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,16 @@ export function createAutoRetryHelpers(deps: HookDeps) {
|
||||
} = deps
|
||||
|
||||
const abortSessionRequest = async (sessionID: string, source: string): Promise<void> => {
|
||||
// Sources we trigger ourselves to swap in a fallback model. Marking the
|
||||
// session lets handleSessionError tell our abort apart from a user stop
|
||||
// so it doesn't wipe attemptCount and re-enter the retry loop.
|
||||
if (
|
||||
source === "session.status.retry-signal" ||
|
||||
source === "message.updated.retry-signal" ||
|
||||
source === "session.timeout"
|
||||
) {
|
||||
deps.internallyAbortedSessions.add(sessionID)
|
||||
}
|
||||
try {
|
||||
await ctx.client.session.abort({ path: { id: sessionID } })
|
||||
log(`[${HOOK_NAME}] Aborted in-flight session request (${source})`, { sessionID })
|
||||
|
||||
Reference in New Issue
Block a user