Fix stale runtime model reuse on session reset

This commit is contained in:
小马哥
2026-03-09 23:01:04 +08:00
committed by Josh Lehman
parent 0c17e7c225
commit f7137ae821
4 changed files with 125 additions and 1 deletions

View File

@@ -1255,6 +1255,76 @@ describe("runReplyAgent typing (heartbeat)", () => {
});
});
it("clears stale runtime model fields when resetSession retries after compaction failure", async () => {
await withTempStateDir(async (stateDir) => {
const sessionId = "session-stale-model";
const storePath = path.join(stateDir, "sessions", "sessions.json");
const transcriptPath = sessions.resolveSessionTranscriptPath(sessionId);
const sessionEntry: SessionEntry = {
sessionId,
updatedAt: Date.now(),
sessionFile: transcriptPath,
modelProvider: "qwencode",
model: "qwen3.5-plus-2026-02-15",
systemPromptReport: {
source: "run",
generatedAt: Date.now(),
sessionId,
sessionKey: "main",
provider: "qwencode",
model: "qwen3.5-plus-2026-02-15",
workspaceDir: stateDir,
bootstrapMaxChars: 1000,
bootstrapTotalMaxChars: 2000,
systemPrompt: {
chars: 10,
projectContextChars: 5,
nonProjectContextChars: 5,
},
injectedWorkspaceFiles: [],
skills: {
promptChars: 0,
entries: [],
},
tools: {
listChars: 0,
schemaChars: 0,
entries: [],
},
},
};
const sessionStore = { main: sessionEntry };
await fs.mkdir(path.dirname(storePath), { recursive: true });
await fs.writeFile(storePath, JSON.stringify(sessionStore), "utf-8");
await fs.mkdir(path.dirname(transcriptPath), { recursive: true });
await fs.writeFile(transcriptPath, "ok", "utf-8");
state.runEmbeddedPiAgentMock.mockImplementationOnce(async () => {
throw new Error(
'Context overflow: Summarization failed: 400 {"message":"prompt is too long"}',
);
});
const { run } = createMinimalRun({
sessionEntry,
sessionStore,
sessionKey: "main",
storePath,
});
await run();
expect(sessionStore.main.modelProvider).toBeUndefined();
expect(sessionStore.main.model).toBeUndefined();
expect(sessionStore.main.systemPromptReport).toBeUndefined();
const persisted = JSON.parse(await fs.readFile(storePath, "utf-8"));
expect(persisted.main.modelProvider).toBeUndefined();
expect(persisted.main.model).toBeUndefined();
expect(persisted.main.systemPromptReport).toBeUndefined();
});
});
it("surfaces overflow fallback when embedded run returns empty payloads", async () => {
state.runEmbeddedPiAgentMock.mockImplementationOnce(async () => ({
payloads: [],

View File

@@ -278,6 +278,9 @@ export async function runReplyAgent(params: {
updatedAt: Date.now(),
systemSent: false,
abortedLastRun: false,
modelProvider: undefined,
model: undefined,
systemPromptReport: undefined,
fallbackNoticeSelectedModel: undefined,
fallbackNoticeActiveModel: undefined,
fallbackNoticeReason: undefined,

View File

@@ -128,6 +128,18 @@ function migrateAndPruneSessionStoreKey(params: {
return { target, primaryKey, entry: params.store[primaryKey] };
}
function stripRuntimeModelState(entry?: SessionEntry): SessionEntry | undefined {
if (!entry) {
return entry;
}
return {
...entry,
model: undefined,
modelProvider: undefined,
systemPromptReport: undefined,
};
}
function archiveSessionTranscriptsForSession(params: {
sessionId: string | undefined;
storePath: string;
@@ -509,7 +521,11 @@ export const sessionsHandlers: GatewayRequestHandlers = {
const entry = store[primaryKey];
const parsed = parseAgentSessionKey(primaryKey);
const sessionAgentId = normalizeAgentId(parsed?.agentId ?? resolveDefaultAgentId(cfg));
const resolvedModel = resolveSessionModelRef(cfg, entry, sessionAgentId);
const resolvedModel = resolveSessionModelRef(
cfg,
stripRuntimeModelState(entry),
sessionAgentId,
);
oldSessionId = entry?.sessionId;
oldSessionFile = entry?.sessionFile;
const now = Date.now();

View File

@@ -591,6 +591,41 @@ describe("gateway server sessions", () => {
ws.close();
});
test("sessions.reset recomputes model from defaults instead of stale runtime model", async () => {
await createSessionStoreDir();
testState.agentConfig = {
model: {
primary: "openai/gpt-test-a",
},
};
await writeSessionStore({
entries: {
main: {
sessionId: "sess-stale-model",
updatedAt: Date.now(),
modelProvider: "qwencode",
model: "qwen3.5-plus-2026-02-15",
},
},
});
const { ws } = await openClient();
const reset = await rpcReq<{
ok: true;
key: string;
entry: { sessionId: string; modelProvider?: string; model?: string };
}>(ws, "sessions.reset", { key: "main" });
expect(reset.ok).toBe(true);
expect(reset.payload?.key).toBe("agent:main:main");
expect(reset.payload?.entry.sessionId).not.toBe("sess-stale-model");
expect(reset.payload?.entry.modelProvider).toBe("openai");
expect(reset.payload?.entry.model).toBe("gpt-test-a");
ws.close();
});
test("sessions.preview resolves legacy mixed-case main alias with custom mainKey", async () => {
const { dir, storePath } = await createSessionStoreDir();
testState.agentsConfig = { list: [{ id: "ops", default: true }] };