Skip to content

session: a stored model that leaves the catalog is unrecoverable — no repair path, restart-proof #59177

Open

Description

@HeZ2z

Description

A session's model lives in session_v2.model, written only at creation or by an explicit switchModel. When that model later leaves the catalog, the session hard-fails on every turn with Model unavailable: <provider>/<model> and cannot recover: editing config, reloading, and restarting all reproduce it.

I'm not asking why the value goes stale — #53060, #49765 and #52333 cover that. I'm reporting that there is no recovery once it is stale.

packages/core/src/session/runner/model.ts validates the stored ref against the live catalog and then fails:

const selected = (yield* available()).find(
  (model) => model.providerID === session.model?.providerID && model.id === session.model.id,
)
if (!selected)
  return yield* new ModelUnavailableError({ ... })

It doesn't fall back to the agent's configured model and doesn't write a correction back. ModelUnavailableError maps to provider.no-route, so the turn is dead.

The column has exactly two writers — session/projector.ts:461 (session.created) and :571 (session.model.selected). No per-turn refresh, and nothing subscribes to provider.updated/model.updated, so a stale ref is never invalidated. SessionStore.get is an uncached row read, so a restart just re-reads the same row.

One amplifier: on submit the composer re-sends the stored model (packages/app/src/composer/submit.ts), and the selection memo prefers it over catalog availability — there's an explicit comment, "Catalog availability must not replace a user's choice with a default on the next submission" (packages/app/src/providers/models/selection.tsx). switchModel dedupes identical selections (session.ts:101), so the dead ref is re-asserted every turn instead of cleared. There's also no CLI or API to repair a stored session model.

Possible fixes: re-resolve or clear session_v2.model on a catalog miss, fall back to the agent model instead of failing closed, and/or invalidate stored models when the catalog updates.

Plugins

oh-my-opencode-slim 3.0.6 (found via that repo's issue #1689)

OpenCode version

2.0.30 (original reporter on 2.1.4)

Steps to reproduce

  1. Pin a session to a model currently in the catalog and send a message, so it's persisted to session_v2.model.
  2. Remove that model from the catalog (delete the provider entry, disable the model, or drop the plugin that contributed it).
  3. Send another message in the same session → Model unavailable: <provider>/<model>.
  4. Edit config, reload, restart → same failure. Only a new session or a manual model switch recovers.

Screenshot and/or share link

N/A — visible in the server log and the session row.

Operating System

macOS 26.0.1 (original report: Linux)

Terminal

N/A


Traced by reading the source rather than reproducing locally, so the mechanism is verified but the repro above is reconstructed — happy to run it against a scratch instance if useful.

Activity

  1. opencode-agent commented on Oct 26, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed trace! To restate the request: when a session's stored model drops out of the catalog, OpenCode should give you a way back instead of failing every turn with Model unavailable. That could mean re-resolving or clearing the stored model, falling back to the agent's model, invalidating stored models when the catalog changes, or a CLI/API to repair a session's model.

    I'm tagging this as a feature request rather than a bug, because failing closed looks deliberate. SessionRunnerModel.resolve only falls back to the agent's model or a default when no model is stored. The app also keeps an explicit choice on purpose ("Catalog availability must not replace a user's choice with a default on the next submission"). Not silently rerouting to another model matters for cases like #52333. As you noted, switching the model manually does recover the session. Still, a clear repair path, or a visible fallback, makes sense as an improvement, and maintainers will consider it.

    Related issues:

  2. holny commented on Oct 26, 2026

    @holny
    Contributor

    Confirmed against the current v2 branch — SessionRunnerModel.resolve does a straight catalog find for the stored ref and throws ModelUnavailableError on a miss, with no fallback to the default resolution and nothing writing back a correction. The only two writers of session_v2.model are the projector's created/selected events, and the composer re-asserts the stored ref on every submit, so a stale value never heals.

    Happy to take this. Leaning toward the smallest fix: on a catalog miss, fall back to the default model resolution instead of failing the turn, plus a regression test for a model that leaves the catalog mid-session. Not sure yet whether the stale ref should also be cleared once we fall back — could go either way.

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