From b62d24e40ba4c99f3aa318c3c9e8ff6c26de2b0d Mon Sep 17 00:00:00 2001 From: Simon Klee Date: Fri, 3 Jul 2026 13:21:06 +0200 Subject: [PATCH] fix(run/cli): wait for selected model catalog (#35130) --- packages/opencode/src/cli/cmd/run.ts | 54 +++++++++++-------- .../src/cli/cmd/run/catalog.shared.ts | 21 ++++++++ 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 235dce64be..fc495213c4 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -26,7 +26,7 @@ import { createOpencodeClient, type OpencodeClient, type ToolPart } from "@openc import { FormatError, FormatUnknownError } from "../error" import { INTERACTIVE_INPUT_ERROR, resolveInteractiveStdin } from "./run/runtime.stdin" import { isImageAttachment, isPdfAttachment } from "@/util/media" -import { loadRunAgents } from "./run/catalog.shared" +import { loadRunAgents, waitForCatalogReady } from "./run/catalog.shared" type ModelInput = Parameters[0]["model"] @@ -400,22 +400,22 @@ export const RunCommand = effectCmd({ const rules: PermissionV1.Ruleset = interactive ? [] : [ - { - permission: "question", - action: "deny", - pattern: "*", - }, - { - permission: "plan_enter", - action: "deny", - pattern: "*", - }, - { - permission: "plan_exit", - action: "deny", - pattern: "*", - }, - ] + { + permission: "question", + action: "deny", + pattern: "*", + }, + { + permission: "plan_enter", + action: "deny", + pattern: "*", + }, + { + permission: "plan_exit", + action: "deny", + pattern: "*", + }, + ] const currentPrompt = !interactive && !args.command && fileInputs.every((file) => !file.isDirectory) const inlineFiles = interactive || currentPrompt @@ -669,10 +669,10 @@ export const RunCommand = effectCmd({ agent: input.agent, model: input.model ? { - providerID: input.model.providerID, - id: input.model.modelID, - variant: input.variant, - } + providerID: input.model.providerID, + id: input.model.modelID, + variant: input.variant, + } : undefined, location: { directory: await current(sdk) }, }) @@ -917,7 +917,7 @@ export const RunCommand = effectCmd({ UI.println( UI.Style.TEXT_WARNING_BOLD + "!", UI.Style.TEXT_NORMAL + - `permission requested: ${permission.permission} (${permission.patterns.join(", ")}); auto-rejecting`, + `permission requested: ${permission.permission} (${permission.patterns.join(", ")}); auto-rejecting`, ) await client.permission.reply({ requestID: permission.id, @@ -931,12 +931,20 @@ export const RunCommand = effectCmd({ const cwd = args.attach ? (directory ?? sess.directory ?? (await current(sdk))) : (directory ?? root) const client = args.attach ? attachSDK(cwd) : sdk + // Current-session flows resolve explicit --model refs from a location + // catalog that populates asynchronously after boot. Wait only when the + // user supplied a model; default-model races. + const selectedModel = pick(args.model) + if (selectedModel && (interactive || (currentPrompt && sess.current !== false))) { + await waitForCatalogReady({ sdk: client, directory: cwd, model: selectedModel }) + } + // Validate agent if specified const agent = await pickAgent(client) if (!interactive) { if (currentPrompt && sess.current !== false) { - const model = pick(args.model) + const model = selectedModel const { runNonInteractivePrompt } = await import("./run/noninteractive") try { await runNonInteractivePrompt({ diff --git a/packages/opencode/src/cli/cmd/run/catalog.shared.ts b/packages/opencode/src/cli/cmd/run/catalog.shared.ts index de3e719821..e4c8277fd9 100644 --- a/packages/opencode/src/cli/cmd/run/catalog.shared.ts +++ b/packages/opencode/src/cli/cmd/run/catalog.shared.ts @@ -84,6 +84,27 @@ export function runProviders(providers: CurrentProvider[], models: CurrentModel[ return [...grouped.values()] } +// A location boots its plugins in a deferred background batch after the layer +// is built, so first-turn model resolution can observe empty catalog state. +// For explicit --model flows, wait for that exact ref to appear before prompt +// admission. On timeout, return and let the real execution error surface. +export async function waitForCatalogReady(input: { + sdk: OpencodeClient + directory: string + model: { providerID: string; modelID: string } + timeoutMs?: number +}) { + const deadline = Date.now() + (input.timeoutMs ?? 5_000) + while (Date.now() < deadline) { + const models = await input.sdk.v2.model + .list(location(input.directory), { throwOnError: true }) + .then((result) => result.data?.data ?? []) + .catch(() => undefined) + if (models?.some((model) => model.providerID === input.model.providerID && model.id === input.model.modelID)) return + await new Promise((resolve) => setTimeout(resolve, 25)) + } +} + export async function loadRunAgents(sdk: OpencodeClient, directory: string): Promise { const result = await sdk.v2.agent.list(location(directory), { throwOnError: true }) return (result.data?.data ?? []).map(runAgent)