Keep the native model when importing sessions (#8948)

* fix(server): preserve the selected model when importing sessions

Import preparation resolved a new-session catalog default and sent it as an explicit resume override. Keep import selections with the resumed provider and persist its reported model for subsequent reloads.

* fix(codex): allow the first reply after importing a session

Resolve collaboration settings again when default model and effort are applied so imported sessions send a complete first-turn request.

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
paseo-bot[bot]
2026-11-07 00:08:25 +00:00
committed by GitHub
parent 9f68af0274
commit ec3ae09926
4 changed files with 91 additions and 2 deletions
@@ -4699,6 +4699,51 @@ test("resumeAgentFromPersistence keeps metadata config, applies overrides, and p
});
});
test("importProviderSession preserves the native model instead of applying the new-session default", async () => {
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-import-model-"));
onTestFinished(() => rmSync(workdir, { recursive: true, force: true }));
const storage = new AgentStorage(join(workdir, "agents"), logger);
const nativeModel = "native-selected-model";
class ImportClient extends TestAgentClient {
importConfig: AgentSessionConfig | undefined;
async importSession(input: ImportProviderSessionInput, context: ImportProviderSessionContext) {
this.importConfig = context.config;
// A resumable provider applies explicit selections but otherwise retains its model.
const session = new TestAgentSession({
...context.config,
model: context.config.model ?? nativeModel,
});
return {
session,
config: context.storedConfig,
persistence: { provider: this.provider, sessionId: input.providerHandleId },
timeline: [],
};
}
}
const client = new ImportClient();
const manager = new AgentManager({ clients: { codex: client }, registry: storage, logger });
const imported = await manager.importProviderSession({
provider: "codex",
providerHandleId: "native-session",
cwd: workdir,
workspaceId: "import-model",
});
await manager.flush();
expect(imported.runtimeInfo?.model).toBe(nativeModel);
expect(imported.config.model).toBe(nativeModel);
expect(client.importConfig?.model).toBeUndefined();
expect((await storage.get(imported.id))?.config.model).toBe(nativeModel);
await manager.reloadAgentSession(imported.id);
expect(client.resumeOverrides.at(-1)?.model).toBe(nativeModel);
await manager.getAgent(imported.id)?.session?.close();
});
test("importProviderSession imports the selected session without listing and publishes ready state", async () => {
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-import-session-"));
const storagePath = join(workdir, "agents");
@@ -1679,6 +1679,7 @@ export class AgentManager {
cwd: input.cwd,
},
resolvedAgentId,
{ resolveDefaultModel: false },
);
this.paseoToolPolicies.set(resolvedAgentId, paseoToolPolicy);
const launchContext = await this.buildLaunchContext(
@@ -1705,8 +1706,13 @@ export class AgentManager {
await this.requireExternalMcpSupport(imported.session, providerLaunchConfig, launchContext);
let handedToRegistration = false;
try {
const runtimeInfo = await imported.session.getRuntimeInfo();
const importedConfig = await this.normalizeConfig(
stripInternalPaseoMcpServer(imported.config),
stripInternalPaseoMcpServer({
...imported.config,
model: runtimeInfo.model ?? imported.config.model,
}),
{ resolveDefaultModel: false },
);
const timelineRows = buildImportedTimelineRows(imported.timeline);
const initialTitle = resolveImportedAgentTitle(importedConfig, timelineRows);
@@ -5723,11 +5729,12 @@ export class AgentManager {
private async prepareSessionConfig(
config: AgentSessionConfig,
agentId: string,
options: { env?: Record<string, string>; purpose?: AgentResumePurpose } = {},
options: NormalizeConfigOptions = {},
): Promise<PreparedSessionConfig> {
const storedConfig = await this.normalizeConfig(stripInternalPaseoMcpServer(config), {
env: options.env,
purpose: options.purpose,
resolveDefaultModel: options.resolveDefaultModel,
});
const paseoToolPolicy = this.paseoToolsEnabled
? this.resolvePaseoToolPolicy(storedConfig.provider)
@@ -1379,6 +1379,42 @@ describe("Codex app-server provider", () => {
}
});
test.each([undefined, "configured-model"])(
"import resolves model %s before the first collaboration turn",
async (configuredModel) => {
const appServer = createFakeCodexAppServer({
"config/read": () => ({
config: { model: configuredModel, model_reasoning_effort: "low" },
}),
"collaborationMode/list": () => ({
data: [{ name: "Code", mode: "default", model: null, reasoning_effort: null }],
}),
});
const provider = createProviderWithFakeAppServer(appServer);
const config = createConfig({ model: undefined, thinkingOptionId: undefined });
const imported = await provider.importSession(
{ providerHandleId: "native-thread", cwd: config.cwd! },
{ config, storedConfig: config },
);
try {
await imported.session.startTurn("first follow-up after import");
await expect(appServer.waitForTurnStart()).resolves.toMatchObject({
threadId: "native-thread",
model: configuredModel ?? "gpt-5.4",
effort: "low",
collaborationMode: {
mode: "default",
settings: { model: configuredModel ?? "gpt-5.4", reasoning_effort: "low" },
},
});
appServer.assertNoErrors();
} finally {
await imported.session.close();
}
},
);
test("provider persistence leaves options owned by the stored agent config", async () => {
const appServer = createFakeCodexAppServer();
const provider = createProviderWithFakeAppServer(appServer);
@@ -5554,6 +5554,7 @@ export class CodexAppServerAgentSession implements AgentSession {
this.config.model = model;
this.reconcileServiceTier();
this.config.thinkingOptionId = thinkingOptionId;
this.refreshResolvedCollaborationMode();
return model;
}