Skip to content

fix(core): wait for the catalog refresh when an explicit model is missing - #57101

Open
reynold-hu wants to merge 1 commit into
anomalyco:devfrom
reynold-hu:catalog-refresh-ready
Open

reynold-hu wants to merge 1 commit into
anomalyco:devfrom
reynold-hu:catalog-refresh-ready

Conversation

@reynold-hu

@reynold-hu reynold-hu commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Issue for this PR

Closes #56119

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

The bundled models.dev snapshot is only the boot floor; the initial refresh runs in the background. Explicit model resolution sampled the catalog once, so a fresh run --standalone --model … could reject a valid model with provider.no-route while that refresh was still in flight.

  • ModelsDev exposes ready, which resolves after the first refresh attempt settles (or immediately when no refresh is scheduled, e.g. fetch: false or a file catalog).
  • SessionRunnerModel.resolve takes an optional awaitCatalog callback. When the selected model is missing, it waits for that attempt (capped at 10s), then re-samples the catalog for up to 1s before reporting ModelUnavailableError. Bundled models still resolve without waiting, and a refresh failure opens the latch so the run fails instead of hanging.
  • SessionContext passes ModelsDev.ready for the explicit session model path. Compaction's agent-model resolution keeps its current behavior.

How did you verify your code works?

  • New test in packages/core/test/models.test.ts: ready completes after the first refresh attempt and the mock catalog fetch ran.
  • New test in packages/core/test/location-layer.test.ts: a real catalog model; the first availability sample returns empty (the pre-refresh race), resolve waits and retries, then resolves after the catalog supplies it. On the parent the same resolution rejects with ModelUnavailableError.
  • Full packages/core suite: 5551 pass, 24 fail; the remaining failures are the same environment/timeout set as an unmodified checkout on this Windows machine.
  • packages/core typecheck passes.
  • Test harnesses that build SessionContext now replace ModelsDev with the offline fixture (title, generate, runner, recorded), so the new dependency cannot attempt network access.

Screenshots / recordings

No UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Added by the opencode bot (AI-generated).

Related: #51737

@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes #56119. I ran the issue's reproduction with a 3s delay on the catalog. On the base, opencode/repro-only-model is rejected with provider.no-route. With this PR the model resolves locally and the request reaches upstream. A model that exists nowhere still fails with ModelUnavailableError. A failed refresh still opens the latch, so nothing hangs. The changed tests pass and packages/core typechecks.

Public API: unchanged. ModelsDev.Interface.ready and the optional awaitCatalog parameter are internal to packages/core.

One concern, in the inline comment: the extra wait applies to every missing model, not only during startup.

Related: #51737

// The bundled catalog is only the boot floor; a model added since the last snapshot is
// supplied by the initial refresh. Wait for that attempt, then give the provider catalog a
// moment to apply it before reporting the selection as unavailable.
yield* awaitCatalog().pipe(Effect.timeout("10 seconds"), Effect.ignore)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After the first refresh has settled, ready returns immediately, but every miss still polls for the full second before failing. So the extra latency doesn't stay limited to the startup race. A model that is really missing (wrong id, provider not authenticated) takes about 1s longer to fail for the rest of the process. SessionContext.selectTitle also resolves the preferred small model through this path with orElseSucceed. When that model isn't available, title selection now pays the 1s on every session, and up to about 11s during a slow first refresh.

Could the wait and re-sample only happen when the first refresh hadn't settled at the time of the miss? For example, check the latch state first and fail right away once it's open. Then steady-state misses fail fast, and only the race in #56119 waits. Separately, the re-sample uses Date.now() and a fixed 1s window, a guess at how long the provider/model catalog takes to apply Refreshed. If the models.dev plugin's update is ever slower, this will be flaky.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(core): fresh standalone run rejects a model before catalog refresh completes

1 participant