Skip to content

[BUG] Capacity eviction can drop recovery state for an in-flight fallback request #153

Description

@iceteaSA

In packages/opencode/src/fable-fallback.ts, prune() enforces the MAX_TRACKED_RECOVERIES (128) cap by evicting the oldest entry, with no exemption for a recovery whose downgraded request is still in flight:

while (this.recoveries.size > MAX_TRACKED_RECOVERIES) {
  const oldest = this.recoveries.keys().next().value
  ...
  this.recoveries.delete(oldest)
}

complete() then can't find the cycle and reports it as uncounted:

const state = this.recoveries.get(plan.recoveryKey)
if (!state) return { counted: false }

So: fill 128 recovery keys, start a downgraded Opus request against the oldest one, activate a 129th distinct recovery before that request returns, and the in-flight plan's completion is silently lost. The recovery turn is never decremented and its cache anchor is never recorded, which leaves that session with a wrong remaining-turn budget and a stale cache boundary — so it burns extra fallback turns and can bridge against the wrong anchor.

The plan-then-complete gap is a supported execution path rather than an edge case — the cycle-ID mechanism exists precisely because a late response can land after newer state has moved on. Capacity eviction is the one place that gap isn't accounted for.

Fix direction: track which recoveries have an outstanding plan and exempt those from capacity eviction (evicting the oldest evictable entry instead), or, if eviction has to win, explicitly invalidate the plan so its later completion is recognised as void rather than being indistinguishable from a stale-cycle no-op.

Found by reading b1d8f8c; I haven't driven a real 129-session workload to observe it, so treat the trigger as derived from the code rather than reproduced.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions