Merge pull request #1981 from VespianRex/fix/fallback-sync-model-ui

Fix model fallback retries for main, background, and sync subagents + show runtime fallback model in task UI
This commit is contained in:
YeonGyu-Kim
2026-02-21 02:28:18 +09:00
committed by GitHub
46 changed files with 2506 additions and 622 deletions
+2 -1
View File
@@ -1,6 +1,6 @@
import type { CallOmoAgentArgs } from "./types"
import type { PluginInput } from "@opencode-ai/plugin"
import { subagentSessions } from "../../features/claude-code-session-state"
import { subagentSessions, syncSubagentSessions } from "../../features/claude-code-session-state"
import { log } from "../../shared"
export async function createOrGetSession(
@@ -64,6 +64,7 @@ Original error: ${createResult.error}`)
const sessionID = createResult.data.id
log(`[call_omo_agent] Created session: ${sessionID}`)
subagentSessions.add(sessionID)
syncSubagentSessions.add(sessionID)
return { sessionID, isNew: true }
}
}
@@ -1,7 +1,7 @@
import type { PluginInput } from "@opencode-ai/plugin"
import { log } from "../../shared"
import { resolveSessionDirectory } from "../../shared"
import { subagentSessions } from "../../features/claude-code-session-state"
import { subagentSessions, syncSubagentSessions } from "../../features/claude-code-session-state"
import type { CallOmoAgentArgs } from "./types"
import type { ToolContextWithMetadata } from "./tool-context-with-metadata"
@@ -69,5 +69,6 @@ Original error: ${createResult.error}`,
const sessionID = createResult.data.id
log(`[call_omo_agent] Created session: ${sessionID}`)
subagentSessions.add(sessionID)
syncSubagentSessions.add(sessionID)
return { ok: true, sessionID }
}
+4 -1
View File
@@ -1,5 +1,6 @@
import type { DelegateTaskArgs, ToolContextWithMetadata } from "./types"
import type { ExecutorContext, ParentContext } from "./executor-types"
import type { FallbackEntry } from "../../shared/model-requirements"
import { getTimingConfig } from "./timing"
import { storeToolMetadata } from "../../features/tool-metadata-store"
import { formatDetailedError } from "./error-formatting"
@@ -12,7 +13,8 @@ export async function executeBackgroundTask(
parentContext: ParentContext,
agentToUse: string,
categoryModel: { providerID: string; modelID: string; variant?: string } | undefined,
systemContent: string | undefined
systemContent: string | undefined,
fallbackChain?: FallbackEntry[],
): Promise<string> {
const { manager } = executorCtx
@@ -27,6 +29,7 @@ export async function executeBackgroundTask(
parentAgent: parentContext.agent,
parentTools: getSessionTools(parentContext.sessionID),
model: categoryModel,
fallbackChain,
skills: args.load_skills.length > 0 ? args.load_skills : undefined,
skillContent: systemContent,
category: args.category,
@@ -1,6 +1,7 @@
import type { ModelFallbackInfo } from "../../features/task-toast-manager/types"
import type { DelegateTaskArgs } from "./types"
import type { ExecutorContext } from "./executor-types"
import type { FallbackEntry } from "../../shared/model-requirements"
import { mergeCategories } from "../../shared/merge-categories"
import { SISYPHUS_JUNIOR_AGENT } from "./sisyphus-junior-agent"
import { resolveCategoryConfig } from "./categories"
@@ -16,6 +17,7 @@ export interface CategoryResolutionResult {
modelInfo: ModelFallbackInfo | undefined
actualModel: string | undefined
isUnstableAgent: boolean
fallbackChain?: FallbackEntry[] // For runtime retry on model errors
error?: string
}
@@ -177,5 +179,6 @@ Available categories: ${categoryNames.join(", ")}`,
modelInfo,
actualModel,
isUnstableAgent,
fallbackChain: requirement?.fallbackChain,
}
}
+5 -2
View File
@@ -8,6 +8,7 @@ import { getAgentDisplayName, getAgentConfigKey } from "../../shared/agent-displ
import { normalizeSDKResponse } from "../../shared"
import { log } from "../../shared/logger"
import { getAvailableModelsForDelegateTask } from "./available-models"
import type { FallbackEntry } from "../../shared/model-requirements"
import { resolveModelForDelegateTask } from "./model-selection"
export async function resolveSubagentExecution(
@@ -15,7 +16,7 @@ export async function resolveSubagentExecution(
executorCtx: ExecutorContext,
parentAgent: string | undefined,
categoryExamples: string
): Promise<{ agentToUse: string; categoryModel: { providerID: string; modelID: string; variant?: string } | undefined; error?: string }> {
): Promise<{ agentToUse: string; categoryModel: { providerID: string; modelID: string; variant?: string } | undefined; fallbackChain?: FallbackEntry[]; error?: string }> {
const { client, agentOverrides } = executorCtx
if (!args.subagent_type?.trim()) {
@@ -46,6 +47,7 @@ Create the work plan directly - that's your job as the planning agent.`,
let agentToUse = agentName
let categoryModel: { providerID: string; modelID: string; variant?: string } | undefined
let fallbackChain: FallbackEntry[] | undefined = undefined
try {
const agentsResult = await client.app.agents()
@@ -92,6 +94,7 @@ Create the work plan directly - that's your job as the planning agent.`,
const agentOverride = agentOverrides?.[agentConfigKey as keyof typeof agentOverrides]
?? (agentOverrides ? Object.entries(agentOverrides).find(([key]) => key.toLowerCase() === agentConfigKey)?.[1] : undefined)
const agentRequirement = AGENT_MODEL_REQUIREMENTS[agentConfigKey]
fallbackChain = agentRequirement?.fallbackChain
if (agentOverride?.model || agentRequirement || matchedAgent.model) {
const availableModels = await getAvailableModelsForDelegateTask(client)
@@ -135,5 +138,5 @@ Create the work plan directly - that's your job as the planning agent.`,
}
}
return { agentToUse, categoryModel }
return { agentToUse, categoryModel, fallbackChain }
}
+3 -3
View File
@@ -100,7 +100,7 @@ describe("executeSyncTask - cleanup on error paths", () => {
//#when - executeSyncTask with fetchSyncResult failing
const result = await executeSyncTask(args, mockCtx, mockExecutorCtx, {
sessionID: "parent-session",
}, "test-agent", undefined, undefined, undefined, deps)
}, "test-agent", undefined, undefined, undefined, undefined, deps)
//#then - should return error and cleanup resources
expect(result).toBe("Fetch failed")
@@ -150,7 +150,7 @@ describe("executeSyncTask - cleanup on error paths", () => {
//#when - executeSyncTask with pollSyncSession failing
const result = await executeSyncTask(args, mockCtx, mockExecutorCtx, {
sessionID: "parent-session",
}, "test-agent", undefined, undefined, undefined, deps)
}, "test-agent", undefined, undefined, undefined, undefined, deps)
//#then - should return error and cleanup resources
expect(result).toBe("Poll error")
@@ -200,7 +200,7 @@ describe("executeSyncTask - cleanup on error paths", () => {
//#when - executeSyncTask completes successfully
const result = await executeSyncTask(args, mockCtx, mockExecutorCtx, {
sessionID: "parent-session",
}, "test-agent", undefined, undefined, undefined, deps)
}, "test-agent", undefined, undefined, undefined, undefined, deps)
//#then - should complete and cleanup resources
expect(result).toContain("Task completed")
+9 -1
View File
@@ -3,11 +3,12 @@ import type { DelegateTaskArgs, ToolContextWithMetadata } from "./types"
import type { ExecutorContext, ParentContext } from "./executor-types"
import { getTaskToastManager } from "../../features/task-toast-manager"
import { storeToolMetadata } from "../../features/tool-metadata-store"
import { subagentSessions } from "../../features/claude-code-session-state"
import { subagentSessions, syncSubagentSessions, setSessionAgent } from "../../features/claude-code-session-state"
import { log } from "../../shared/logger"
import { formatDuration } from "./time-formatter"
import { formatDetailedError } from "./error-formatting"
import { syncTaskDeps, type SyncTaskDeps } from "./sync-task-deps"
import { setSessionFallbackChain, clearSessionFallbackChain } from "../../hooks/model-fallback/hook"
export async function executeSyncTask(
args: DelegateTaskArgs,
@@ -18,6 +19,7 @@ export async function executeSyncTask(
categoryModel: { providerID: string; modelID: string; variant?: string } | undefined,
systemContent: string | undefined,
modelInfo?: ModelFallbackInfo,
fallbackChain?: import("../../shared/model-requirements").FallbackEntry[],
deps: SyncTaskDeps = syncTaskDeps
): Promise<string> {
const { client, directory, onSyncSessionCreated } = executorCtx
@@ -40,6 +42,9 @@ export async function executeSyncTask(
const sessionID = createSessionResult.sessionID
syncSessionID = sessionID
subagentSessions.add(sessionID)
syncSubagentSessions.add(sessionID)
setSessionAgent(sessionID, agentToUse)
setSessionFallbackChain(sessionID, fallbackChain)
if (onSyncSessionCreated) {
log("[task] Invoking onSyncSessionCreated callback", { sessionID, parentID: parentContext.sessionID })
@@ -59,6 +64,7 @@ export async function executeSyncTask(
if (toastManager) {
toastManager.addTask({
id: taskId,
sessionID,
description: args.description,
agent: agentToUse,
isBackground: false,
@@ -145,6 +151,8 @@ session_id: ${sessionID}
} finally {
if (syncSessionID) {
subagentSessions.delete(syncSessionID)
syncSubagentSessions.delete(syncSessionID)
clearSessionFallbackChain(syncSessionID)
}
}
}
+5 -2
View File
@@ -164,6 +164,7 @@ Prompts MUST be in English.`
let modelInfo: import("../../features/task-toast-manager/types").ModelFallbackInfo | undefined
let actualModel: string | undefined
let isUnstableAgent = false
let fallbackChain: import("../../shared/model-requirements").FallbackEntry[] | undefined
if (args.category) {
const resolution = await resolveCategoryExecution(args, options, inheritedModel, systemDefaultModel)
@@ -176,6 +177,7 @@ Prompts MUST be in English.`
modelInfo = resolution.modelInfo
actualModel = resolution.actualModel
isUnstableAgent = resolution.isUnstableAgent
fallbackChain = resolution.fallbackChain
const isRunInBackgroundExplicitlyFalse = args.run_in_background === false || args.run_in_background === "false" as unknown as boolean
@@ -206,6 +208,7 @@ Prompts MUST be in English.`
}
agentToUse = resolution.agentToUse
categoryModel = resolution.categoryModel
fallbackChain = resolution.fallbackChain
}
const systemContent = buildSystemContent({
@@ -217,10 +220,10 @@ Prompts MUST be in English.`
})
if (runInBackground) {
return executeBackgroundTask(args, ctx, options, parentContext, agentToUse, categoryModel, systemContent)
return executeBackgroundTask(args, ctx, options, parentContext, agentToUse, categoryModel, systemContent, fallbackChain)
}
return executeSyncTask(args, ctx, options, parentContext, agentToUse, categoryModel, systemContent, modelInfo)
return executeSyncTask(args, ctx, options, parentContext, agentToUse, categoryModel, systemContent, modelInfo, fallbackChain)
},
})
}