Skip to content

[FEATURE]: session.retry decision model override and settled flag for safe model failover #59000

Closed

Description

@dpereira

Feature hasn't been suggested before.

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

  1. SessionRetryDecision has no model field. model on the event is readonly; the decision is only { retry: false } | { retry: true, delay }. There is no way to say "retry, but with this model".
  2. session.switchModel is 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.
  3. The safe-retry boundary is internal. runner/step.ts already distinguishes pre-output Retry from post-output Continue (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".
  4. 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:

  • model on 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).
  • attempt stays the proposed physical attempt number (initial = 1), so plugins can enforce per-slot ceilings including host-initiated retries.
  • settled is host-derived: true only when the failed attempt emitted no text/reasoning/tool-input/tool-call parts and any tool fibers it started have settled. When settled is 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: false because settled === false) must win over an earlier retry: true from a lower-precedence hook; document the ordering so a later plugin cannot silently undo an attempt ceiling.

Non-goals

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

  1. opencode-agent commented on Oct 25, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed proposal, @dpereira! To restate it: you'd like the session.retry hook 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-derived settled flag 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 SessionRetryDecision only 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:

  2. dpereira commented on Oct 25, 2026

    @dpereira
    Author

    Sorry, my coding agent filed this by mistake.

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions