fix(coding-agent): retried role restore after extension providers

Initial startup resume runs before extension providers register, so a role model supplied by an extension fell back to the saved default. Retry the preferred session-model candidates once provider registrations are processed and re-resolve thinking level for the new model.\n\nFixes #1649
This commit is contained in:
roboomp
2026-06-01 16:56:32 +00:00
committed by can1357
parent cf9eb5b0f9
commit 0494454528
2 changed files with 104 additions and 8 deletions
+39 -8
View File
@@ -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();
@@ -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();
}
});
});