Repository navigation
Description
Feature hasn't been suggested before.
- I have verified this feature I'm about to request hasn't been suggested before. (Related: feat(plugin): add session retry hook #45999 added the
session.retryhook; [FEATURE]: call the session.retry hook when the built-in retries run out #54862 asks for the hook after built-in exhaustion; [FEATURE]: Configurable model fallback on transient errors and timeouts #48991 asks for configurable fallback chains. This request is the minimal hook-level contract needed to implement safe failover on top of the existing hook.)
Describe the enhancement you want to request
Extend the session.retry plugin hook so a plugin can (a) select the model for the next attempt and (b) see whether the failed attempt was safely settled before output. Together these let a plugin implement primary → secondary model failover inside a single step, without replaying tool side effects.
Use case
We run three agent tiers (planner / coordinator / worker), each configured with a primary and secondary model. Policy:
- same model for at most 3 attempts (initial + 2 retries), same failure category;
- then switch to the secondary for at most 3 attempts;
- no ping-pong, no tertiary;
- after primary exhaustion, a 300s cooldown; later real dispatches use the secondary; after cooldown exactly one dispatch probes primary recovery.
The plugin owns this policy. The host owns the attempt lifecycle. Today the plugin can veto/authorize retries (#45999) but cannot redirect the next attempt, and cannot tell whether a retry would replay partial output or completed tools.
Why the existing surfaces fall short
SessionRetryDecisionhas no model field.modelon the event isreadonly; the decision is only{ retry: false } | { retry: true, delay }. There is no way to say "retry, but with this model".session.switchModelis not an attempt-scoped override. It mutates session state via an event; nothing documents that calling it from inside the retry callback is safe (ordering vs.prepareContext/context.load, deadlock in the runner fiber) or that it applies to the next attempt only.- The safe-retry boundary is internal.
runner/step.tsalready distinguishes pre-outputRetryfrom post-outputContinue(with a synthetic continuation). The retry hook event does not expose that boundary, so a plugin cannot implement "never fall back after text/reasoning/tool input started". - The hook is skipped after the built-in schedule exhausts ([FEATURE]: call the session.retry hook when the built-in retries run out #54862), so a plugin cannot extend the budget or redirect after the host gives up.
Proposed minimal change
export type SessionRetryDecision =
| { retry: false }
| { retry: true; delay: number; model?: Model.Ref } // model override for the next attempt
export interface SessionRetry {
readonly sessionID: Session.ID
readonly agent: Agent.ID
readonly model: Model.Ref
readonly error: SessionError.Error
readonly attempt: number
/** True when the failed attempt published no text/reasoning/tool parts and its tool fibers settled. */
readonly settled: boolean
decision: SessionRetryDecision
}Semantics we need:
modelon a retry decision applies only to the next attempt: the runner resolves the model, credentials, provider options, and request transforms from it before building the request. It must not mutate the session's selected model (no persistent side effect, no interference with concurrent user model switches).attemptstays the proposed physical attempt number (initial = 1), so plugins can enforce per-slot ceilings including host-initiated retries.settledis host-derived:trueonly when the failed attempt emitted no text/reasoning/tool-input/tool-call parts and any tool fibers it started have settled. Whensettledis false the runner must not transparently retry the attempt on any model; keep the existing post-output continuation semantics.- Call the hook when the built-in schedule is exhausted too, with
decision: { retry: false }as the default (that is [FEATURE]: call the session.retry hook when the built-in retries run out #54862), so a plugin can extend the run within its own budget. - Precedence: an explicit safety veto (
retry: falsebecausesettled === false) must win over an earlierretry: truefrom a lower-precedence hook; document the ordering so a later plugin cannot silently undo an attempt ceiling.
Non-goals
- Fallback chains in config (
fallback: [...]in [FEATURE]: Configurable model fallback on transient errors and timeouts #48991) — plugins can hold the policy. - Changing built-in retry counts/backoff defaults.
- Unbounded retries past the host's schedule without an explicit plugin decision.
Verification sketch
A plugin implementing the policy above completes a dispatch on the secondary after 3 failed primary attempts, with:
- exactly 3 provider calls per slot (host-initiated retries counted in the same budget),
- tools from earlier steps executed once and preserved in history,
- an attempt that published partial output never replayed (dispatch stops with the error),
- after the cooldown, one probe attempt on primary while concurrent dispatches use secondary.
Related: #45999 (hook), #54862 (exhaustion), #48991 / #49125 (fallback chains).
Activity
opencode-agent commentedon Oct 25, 2026
Thanks for the detailed proposal, @dpereira! To restate it: you'd like the
session.retryhook to let a plugin pick the model for the next attempt (for that attempt only, without changing the session's selected model), and to expose a host-derivedsettledflag so a plugin never falls back after an attempt has already published output or started tools. You'd also like the hook to be called when the built-in retries run out, and the precedence between plugins to be documented.Today
SessionRetryDecisiononly allows{ retry: false }or{ retry: true, delay }, and the hook isn't called once the built-in schedule is exhausted. So this can't be built as a plugin yet; it needs these additions to the plugin API. Maintainers will consider it.Related issues:
session.retryafter the built-in retries run out (one part of this request)--model/-mpins the model and disables plugin fallbackdpereira commentedon Oct 25, 2026
Sorry, my coding agent filed this by mistake.