diff --git a/src/agents/utils.test.ts b/src/agents/utils.test.ts index 98c740c35..975b28c2d 100644 --- a/src/agents/utils.test.ts +++ b/src/agents/utils.test.ts @@ -228,11 +228,12 @@ describe("createBuiltinAgents without systemDefaultModel", () => { }) describe("createBuiltinAgents with requiresModel gating", () => { - test("hephaestus is not created when gpt-5.2-codex is unavailable", async () => { + test("hephaestus is not created when gpt-5.2-codex is unavailable and provider not connected", async () => { // #given const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue( new Set(["anthropic/claude-opus-4-5"]) ) + const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue([]) try { // #when @@ -242,6 +243,7 @@ describe("createBuiltinAgents with requiresModel gating", () => { expect(agents.hephaestus).toBeUndefined() } finally { fetchSpy.mockRestore() + cacheSpy.mockRestore() } }) @@ -355,11 +357,12 @@ describe("createBuiltinAgents with requiresAnyModel gating (sisyphus)", () => { } }) - test("sisyphus is not created when no fallback model is available (unrelated model only)", async () => { + test("sisyphus is not created when no fallback model is available and provider not connected", async () => { // #given - only openai/gpt-5.2 available, not in sisyphus fallback chain const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue( new Set(["openai/gpt-5.2"]) ) + const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue([]) try { // #when @@ -369,6 +372,7 @@ describe("createBuiltinAgents with requiresAnyModel gating (sisyphus)", () => { expect(agents.sisyphus).toBeUndefined() } finally { fetchSpy.mockRestore() + cacheSpy.mockRestore() } }) }) diff --git a/src/agents/utils.ts b/src/agents/utils.ts index 7c1a236d5..35b17887c 100644 --- a/src/agents/utils.ts +++ b/src/agents/utils.ts @@ -395,7 +395,7 @@ export async function createBuiltinAgents( !hephaestusRequirement?.requiresModel || hasHephaestusExplicitConfig || isFirstRunNoCache || - (availableModels.size > 0 && isModelAvailable(hephaestusRequirement.requiresModel, availableModels)) + isAnyFallbackModelAvailable(hephaestusRequirement.fallbackChain, availableModels) if (hasRequiredModel) { let hephaestusResolution = applyModelResolution({ diff --git a/src/shared/agent-tool-restrictions.ts b/src/shared/agent-tool-restrictions.ts index 02b908f7f..0a34d8427 100644 --- a/src/shared/agent-tool-restrictions.ts +++ b/src/shared/agent-tool-restrictions.ts @@ -22,6 +22,7 @@ const AGENT_RESTRICTIONS: Record> = { edit: false, task: false, delegate_task: false, + call_omo_agent: false, }, metis: { diff --git a/src/shared/model-availability.ts b/src/shared/model-availability.ts index 62a72e8f5..483c2a56b 100644 --- a/src/shared/model-availability.ts +++ b/src/shared/model-availability.ts @@ -2,7 +2,7 @@ import { existsSync, readFileSync } from "fs" import { join } from "path" import { log } from "./logger" import { getOpenCodeCacheDir } from "./data-path" -import { readProviderModelsCache, hasProviderModelsCache } from "./connected-providers-cache" +import { readProviderModelsCache, hasProviderModelsCache, readConnectedProvidersCache } from "./connected-providers-cache" /** * Fuzzy match a target model name against available models @@ -278,19 +278,35 @@ export function isAnyFallbackModelAvailable( fallbackChain: Array<{ providers: string[]; model: string }>, availableModels: Set, ): boolean { - if (availableModels.size === 0) { - return false - } - - for (const entry of fallbackChain) { - const hasAvailableProvider = entry.providers.some((provider) => { - return fuzzyMatchModel(entry.model, availableModels, [provider]) !== null - }) - if (hasAvailableProvider) { - return true + // If we have models, check them first + if (availableModels.size > 0) { + for (const entry of fallbackChain) { + const hasAvailableProvider = entry.providers.some((provider) => { + return fuzzyMatchModel(entry.model, availableModels, [provider]) !== null + }) + if (hasAvailableProvider) { + return true + } } } - log("[isAnyFallbackModelAvailable] no model available in chain", { chainLength: fallbackChain.length }) + + // Fallback: check if any provider in the chain is connected + // This handles race conditions where availableModels is empty or incomplete + // but we know the provider is connected. + const connectedProviders = readConnectedProvidersCache() + if (connectedProviders) { + const connectedSet = new Set(connectedProviders) + for (const entry of fallbackChain) { + if (entry.providers.some((p) => connectedSet.has(p))) { + log("[isAnyFallbackModelAvailable] model not in available set, but provider is connected", { + model: entry.model, + availableCount: availableModels.size, + }) + return true + } + } + } + return false }