diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index b4f013569..4bd16d8a7 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -1056,26 +1056,29 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} const taskDepth = options.taskDepth ?? 0; - let thinkingLevel = options.thinkingLevel; - - // If session has data and includes a thinking entry, restore it - if (thinkingLevel === undefined && hasExistingSession && hasThinkingEntry) { - thinkingLevel = parseThinkingLevel(existingSession.thinkingLevel); - } - - if (thinkingLevel === undefined && !hasExplicitModel && !hasThinkingEntry && defaultRoleSpec.explicitThinkingLevel) { - thinkingLevel = defaultRoleSpec.thinkingLevel; - } - - // Prefer the selected model's configured defaultLevel, otherwise fall back - // to the global settings default. - if (thinkingLevel === undefined && model?.thinking?.defaultLevel !== undefined) { - thinkingLevel = model.thinking.defaultLevel; - } - if (thinkingLevel === undefined) { - thinkingLevel = settings.get("defaultThinkingLevel"); - } - const autoThinking = thinkingLevel === AUTO_THINKING; + // Resolves the session/agent thinking level using the same precedence we + // apply at startup: explicit option → persisted session entry → default + // role's explicit selector → selected model's defaultLevel → global + // settings default. Run again after extension role reclaim so the final + // model's own defaults aren't masked by an earlier fallback model's. + const pickInitialThinkingLevel = (selectedModel: Model | undefined): ConfiguredThinkingLevel | undefined => { + let level = options.thinkingLevel; + if (level === undefined && hasExistingSession && hasThinkingEntry) { + level = parseThinkingLevel(existingSession.thinkingLevel); + } + if (level === undefined && !hasExplicitModel && !hasThinkingEntry && defaultRoleSpec.explicitThinkingLevel) { + level = defaultRoleSpec.thinkingLevel; + } + if (level === undefined && selectedModel?.thinking?.defaultLevel !== undefined) { + level = selectedModel.thinking.defaultLevel; + } + if (level === undefined) { + level = settings.get("defaultThinkingLevel"); + } + return level; + }; + let thinkingLevel = pickInitialThinkingLevel(model); + let autoThinking = thinkingLevel === AUTO_THINKING; // Concrete level the agent/session start with. With `auto` this is the // provisional level shown until the first per-turn classification resolves; // `auto` itself stays a session-only concept handled by AgentSession. @@ -1461,13 +1464,16 @@ 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++) { + // Retry session-model candidates now that extension providers are + // registered. The initial restore runs before extensions load, so a role + // model supplied by an extension would have either fallen back to the + // saved default (`restoredSessionModelIndex > 0`) or failed entirely + // (`restoredSessionModelIndex === -1`, with the settings default or + // downstream fallback filling `model`). Reclaim it here so resume + // honors the last active role in either case. + const sessionRetryLimit = restoredSessionModelIndex >= 0 ? restoredSessionModelIndex : sessionModelStrings.length; + if (!hasExplicitModel && sessionRetryLimit > 0) { + for (let i = 0; i < sessionRetryLimit; i++) { const sessionModelStr = sessionModelStrings[i]; const parsedModel = parseModelString(sessionModelStr); if (!parsedModel) continue; @@ -1476,6 +1482,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} model = restoredModel; modelFallbackMessage = undefined; restoredSessionModelIndex = i; + // Recompute thinking-level from scratch against the reclaimed + // model: any value derived from the earlier fallback model's + // `thinking.defaultLevel` must not become sticky. + thinkingLevel = pickInitialThinkingLevel(restoredModel); + autoThinking = thinkingLevel === AUTO_THINKING; + effectiveThinkingLevel = thinkingLevel === AUTO_THINKING ? undefined : thinkingLevel; effectiveThinkingLevel = logger.time("resolveThinkingLevelForModel", () => autoThinking ? resolveProvisionalAutoLevel(restoredModel) diff --git a/packages/coding-agent/test/sdk-model-selection.test.ts b/packages/coding-agent/test/sdk-model-selection.test.ts index a88ed3a22..c702edf29 100644 --- a/packages/coding-agent/test/sdk-model-selection.test.ts +++ b/packages/coding-agent/test/sdk-model-selection.test.ts @@ -214,4 +214,75 @@ describe("createAgentSession deferred model pattern resolution", () => { authStorage.close(); } }); + + test("restores extension role model when saved default cannot be restored before extensions load", async () => { + const settingsDefaultModel = getBundledModel("anthropic", "claude-sonnet-4-5"); + if (!settingsDefaultModel) { + throw new Error("Expected bundled anthropic default model"); + } + + const authStorage = await AuthStorage.create(path.join(tempDir, "testauth.db")); + authStorage.setRuntimeApiKey(settingsDefaultModel.provider, "test-key"); + const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml")); + + // Saved default points at a provider that has no usable credentials. The + // last active role (`smol`) is supplied by the inline extension and is + // only resolvable once provider registrations are processed. + const targetSessionFile = path.join(tempDir, "resume-extension-default-missing.jsonl"); + const timestamp = "2026-06-01T00:00:00.000Z"; + await Bun.write( + targetSessionFile, + `${[ + { type: "session", version: 3, id: "resume-ext-no-default", timestamp, cwd: tempDir }, + { + type: "model_change", + id: "default-model", + parentId: null, + timestamp, + model: "anthropic/not-available", + 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-no-default")); + + const settings = Settings.isolated(); + settings.setModelRole("default", `${settingsDefaultModel.provider}/${settingsDefaultModel.id}`); + + const { session } = await createAgentSession({ + cwd: tempDir, + agentDir: tempDir, + authStorage, + modelRegistry, + sessionManager, + settings, + 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(); + } + }); });