diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index 1b6512613..b4f013569 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -1008,16 +1008,21 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} ); let model = options.model; let modelFallbackMessage: string | undefined; - // If session has data, try to restore model from it. - // Skip restore when an explicit model was requested. - const sessionModelStrings = getRestorableSessionModels( - existingSession.models, - sessionManager.getLastModelChangeRole(), - ); - if (!hasExplicitModel && !model && hasExistingSession && sessionModelStrings.length > 0) { + // Identify session model strings to restore in fallback order. We do an + // initial pass here so model-dependent setup (thinking-level resolution, + // host preconnect) can use the restored model; extension-registered + // providers aren't visible yet, so we retry the preferred candidates once + // extensions register below. + const sessionModelStrings = + !hasExplicitModel && hasExistingSession + ? getRestorableSessionModels(existingSession.models, sessionManager.getLastModelChangeRole()) + : []; + let restoredSessionModelIndex = -1; + if (!hasExplicitModel && !model && sessionModelStrings.length > 0) { await logger.time("restoreSessionModel", async () => { let failedSessionModel: string | undefined; - for (const sessionModelStr of sessionModelStrings) { + for (let i = 0; i < sessionModelStrings.length; i++) { + const sessionModelStr = sessionModelStrings[i]; const parsedModel = parseModelString(sessionModelStr); if (!parsedModel) { failedSessionModel ??= sessionModelStr; @@ -1027,6 +1032,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} const restoredModel = modelRegistry.find(parsedModel.provider, parsedModel.id); if (restoredModel && (await hasModelApiKey(restoredModel))) { model = restoredModel; + restoredSessionModelIndex = i; break; } failedSessionModel ??= sessionModelStr; @@ -1455,6 +1461,31 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} extensionsResult.runtime.pendingProviderRegistrations = []; } + // Retry preferred session-model candidates now that extension providers + // are registered. The initial restore above runs before extensions load, + // so a role model supplied by an extension would have fallen back to the + // session's saved default; reclaim it here so resume honors the last + // active role. + if (!hasExplicitModel && restoredSessionModelIndex > 0 && sessionModelStrings.length > 0) { + for (let i = 0; i < restoredSessionModelIndex; i++) { + const sessionModelStr = sessionModelStrings[i]; + const parsedModel = parseModelString(sessionModelStr); + if (!parsedModel) continue; + const restoredModel = modelRegistry.find(parsedModel.provider, parsedModel.id); + if (restoredModel && (await hasModelApiKey(restoredModel))) { + model = restoredModel; + modelFallbackMessage = undefined; + restoredSessionModelIndex = i; + effectiveThinkingLevel = logger.time("resolveThinkingLevelForModel", () => + autoThinking + ? resolveProvisionalAutoLevel(restoredModel) + : resolveThinkingLevelForModel(restoredModel, effectiveThinkingLevel), + ); + preconnectModelHost(restoredModel.baseUrl); + break; + } + } + } // Resolve deferred --model pattern now that extension models are registered. if (!model && options.modelPattern) { const availableModels = modelRegistry.getAll(); diff --git a/packages/coding-agent/test/sdk-model-selection.test.ts b/packages/coding-agent/test/sdk-model-selection.test.ts index ee383200b..a88ed3a22 100644 --- a/packages/coding-agent/test/sdk-model-selection.test.ts +++ b/packages/coding-agent/test/sdk-model-selection.test.ts @@ -149,4 +149,69 @@ describe("createAgentSession deferred model pattern resolution", () => { authStorage.close(); } }); + + test("restores role model from extension provider after startup resume", async () => { + const defaultModel = getBundledModel("anthropic", "claude-sonnet-4-5"); + if (!defaultModel) { + throw new Error("Expected bundled anthropic default model"); + } + + const authStorage = await AuthStorage.create(path.join(tempDir, "testauth.db")); + authStorage.setRuntimeApiKey(defaultModel.provider, "test-key"); + const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml")); + + const targetSessionFile = path.join(tempDir, "resume-extension.jsonl"); + const timestamp = "2026-06-01T00:00:00.000Z"; + await Bun.write( + targetSessionFile, + `${[ + { type: "session", version: 3, id: "resume-ext", timestamp, cwd: tempDir }, + { + type: "model_change", + id: "default-model", + parentId: null, + timestamp, + model: `${defaultModel.provider}/${defaultModel.id}`, + role: "default", + }, + { + type: "model_change", + id: "smol-model", + parentId: "default-model", + timestamp, + model: "runtime-provider/runtime-model", + role: "smol", + }, + ] + .map(entry => JSON.stringify(entry)) + .join("\n")}\n`, + ); + const sessionManager = await SessionManager.open(targetSessionFile, path.join(tempDir, "sessions")); + + const { session } = await createAgentSession({ + cwd: tempDir, + agentDir: tempDir, + authStorage, + modelRegistry, + sessionManager, + settings: Settings.isolated(), + disableExtensionDiscovery: true, + extensions: [providerExtension], + skills: [], + contextFiles: [], + promptTemplates: [], + slashCommands: [], + enableMCP: false, + enableLsp: false, + skipPythonPreflight: true, + }); + + try { + expect(session.model?.provider).toBe("runtime-provider"); + expect(session.model?.id).toBe("runtime-model"); + } finally { + await session.dispose(); + authStorage.close(); + } + }); });