fix(cli): recompute model scope after resume project switch
- Re-resolve settings-derived model scope once a resume switches projects. - Extract resolveScopedModels shared across startup, explicit resume, and picker. - Assert destination enabledModels drive the resumed scope. Fixes #6752
This commit is contained in:
@@ -624,6 +624,29 @@ async function switchToResumedProject(
|
||||
return cwd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the effective model allow-list from an explicit `--models` scope or,
|
||||
* failing that, the active project's `enabledModels`. Re-run after a resume
|
||||
* switches projects so the destination project's settings-derived scope wins
|
||||
* over the launch directory's.
|
||||
*/
|
||||
async function resolveScopedModels(
|
||||
parsed: Args,
|
||||
modelRegistry: ModelRegistry,
|
||||
activeSettings: Settings,
|
||||
): Promise<ScopedModel[]> {
|
||||
const modelPatterns = parsed.models ?? activeSettings.get("enabledModels");
|
||||
if (!modelPatterns || modelPatterns.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return await resolveModelScope(
|
||||
modelPatterns,
|
||||
modelRegistry,
|
||||
getModelMatchPreferences(activeSettings),
|
||||
activeSettings,
|
||||
);
|
||||
}
|
||||
|
||||
async function getChangelogForDisplay(parsed: Args): Promise<string | undefined> {
|
||||
if (parsed.continue || parsed.resume) {
|
||||
return undefined;
|
||||
@@ -1242,19 +1265,13 @@ export async function runRootCommand(
|
||||
settingsInstance.get("theme.light"),
|
||||
);
|
||||
|
||||
let scopedModels: ScopedModel[] = [];
|
||||
const modelPatterns = parsedArgs.models ?? settingsInstance.get("enabledModels");
|
||||
const modelMatchPreferences = getModelMatchPreferences(settingsInstance);
|
||||
if (modelPatterns && modelPatterns.length > 0) {
|
||||
scopedModels = await logger.time(
|
||||
"resolveModelScope",
|
||||
resolveModelScope,
|
||||
modelPatterns,
|
||||
modelRegistry,
|
||||
modelMatchPreferences,
|
||||
settingsInstance,
|
||||
);
|
||||
}
|
||||
let scopedModels = await logger.time(
|
||||
"resolveModelScope",
|
||||
resolveScopedModels,
|
||||
parsedArgs,
|
||||
modelRegistry,
|
||||
settingsInstance,
|
||||
);
|
||||
|
||||
// Resolve an explicit `--continue <id>` before extension flags are loaded.
|
||||
// Reading the token immediately after `--continue` distinguishes the session
|
||||
@@ -1286,7 +1303,14 @@ export async function runRootCommand(
|
||||
}
|
||||
|
||||
if (typeof parsedArgs.resume === "string" && sessionManager) {
|
||||
const previousCwd = cwd;
|
||||
cwd = await switchToResumedProject(sessionManager.getCwd(), settingsInstance, pluginPreloadPromise);
|
||||
if (cwd !== previousCwd) {
|
||||
// Destination project may scope a different `enabledModels`; re-resolve
|
||||
// so the model UI and session options reflect it (explicit `--models`
|
||||
// stays fixed inside resolveScopedModels).
|
||||
scopedModels = await resolveScopedModels(parsedArgs, modelRegistry, settingsInstance);
|
||||
}
|
||||
}
|
||||
|
||||
// User declined the missing-directory move prompt — exit cleanly instead of
|
||||
@@ -1332,7 +1356,11 @@ export async function runRootCommand(
|
||||
process.exit(0);
|
||||
}
|
||||
// Re-scope every cwd-derived input before building the resumed session.
|
||||
const previousCwd = cwd;
|
||||
cwd = await switchToResumedProject(selected.cwd, settingsInstance, pluginPreloadPromise);
|
||||
if (cwd !== previousCwd) {
|
||||
scopedModels = await resolveScopedModels(parsedArgs, modelRegistry, settingsInstance);
|
||||
}
|
||||
sessionManager = await SessionManager.open(selected.path);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import * as fsp from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { type Args, parseArgs } from "@oh-my-pi/pi-coding-agent/cli/args";
|
||||
import * as modelResolverModule from "@oh-my-pi/pi-coding-agent/config/model-resolver";
|
||||
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
||||
import { createSessionManager, runRootCommand } from "@oh-my-pi/pi-coding-agent/main";
|
||||
import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage";
|
||||
@@ -163,6 +164,49 @@ describe("runRootCommand — cross-project --resume", () => {
|
||||
expect(reloadForCwd).toHaveBeenCalledWith(resumedProject);
|
||||
expect(resumedManager?.getCwd()).toBe(resumedProject);
|
||||
}, 15_000);
|
||||
|
||||
it("re-resolves the model scope from the resumed project's enabledModels after the switch", async () => {
|
||||
const match = buildGlobalMatch(resumedProject);
|
||||
vi.spyOn(sessionListingModule, "resolveResumableSession").mockResolvedValue(match);
|
||||
// enabledModels scoped only to the resumed project: the launch scope
|
||||
// yields no patterns, so any resolveModelScope call proves the recompute
|
||||
// ran against the destination settings rather than the launch directory.
|
||||
const settings = Settings.isolated({
|
||||
"marketplace.autoUpdate": "off",
|
||||
enabledModels: [{ paths: [resumedProject], models: ["model-resumed"] }],
|
||||
});
|
||||
const resolveModelScope = vi.spyOn(modelResolverModule, "resolveModelScope").mockResolvedValue([]);
|
||||
const authStorage = await AuthStorage.create(path.join(root, "auth.db"));
|
||||
const parsed = parseArgs(["--resume", "019e84ed", "--print"]);
|
||||
parsed.noExtensions = true;
|
||||
parsed.noSkills = true;
|
||||
parsed.noRules = true;
|
||||
parsed.noTools = true;
|
||||
parsed.noLsp = true;
|
||||
let resumedManager: SessionManager | undefined;
|
||||
|
||||
try {
|
||||
await runRootCommand(parsed, ["--resume", "019e84ed", "--print"], {
|
||||
discoverAuthStorage: async () => authStorage,
|
||||
settings,
|
||||
createAgentSession: async options => {
|
||||
if (!options) throw new Error("Expected session options");
|
||||
resumedManager = options.sessionManager;
|
||||
throw new Error("stop after session options");
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (!(error instanceof Error) || error.message !== "stop after session options") throw error;
|
||||
} finally {
|
||||
authStorage.close();
|
||||
await resumedManager?.close();
|
||||
}
|
||||
|
||||
// Launch scope had no patterns, so the only resolution is the post-switch
|
||||
// one; the pre-fix code never recomputed and would not call it at all.
|
||||
expect(resolveModelScope).toHaveBeenCalledTimes(1);
|
||||
expect(resolveModelScope.mock.calls[0]?.[0]).toEqual(["model-resumed"]);
|
||||
}, 15_000);
|
||||
});
|
||||
|
||||
describe("createSessionManager — cross-project --resume relocation (moved worktree)", () => {
|
||||
|
||||
Reference in New Issue
Block a user