Skip to content

Commit 5337439

Browse files
PonyX-labjalehman
andauthored
Fix stale runtime model reuse on session reset (openclaw#41173)
Merged via squash. Prepared head SHA: d8a04a4 Co-authored-by: PonyX-lab <[email protected]> Co-authored-by: jalehman <[email protected]> Reviewed-by: @jalehman
1 parent 0c17e7c commit 5337439

File tree

5 files changed

+131
-2
lines changed

5 files changed

+131
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ Docs: https://docs.openclaw.ai
7676
- Telegram/docs: clarify that `channels.telegram.groups` allowlists chats while `groupAllowFrom` allowlists users inside those chats, and point invalid negative chat IDs at the right config key. (#42451) Thanks @altaywtf.
7777
- Models/Alibaba Cloud Model Studio: wire `MODELSTUDIO_API_KEY` through shared env auth, implicit provider discovery, and shell-env fallback so onboarding works outside the wizard too. (#40634) Thanks @pomelo-nwu.
7878
- ACP/sessions_spawn: implicitly stream `mode="run"` ACP spawns to parent only for eligible subagent orchestrator sessions (heartbeat `target: "last"` with a usable session-local route), restoring parent progress relays without thread binding. (#42404) Thanks @davidguttman.
79+
- Sessions/reset model recompute: clear stale runtime model, context-token, and system-prompt metadata before session resets recompute the replacement session, so resets pick up current defaults and explicit overrides instead of reusing old runtime model state. (#41173) thanks @PonyX-lab.
7980

8081
## 2026.3.8
8182

src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1255,6 +1255,79 @@ describe("runReplyAgent typing (heartbeat)", () => {
12551255
});
12561256
});
12571257

1258+
it("clears stale runtime model fields when resetSession retries after compaction failure", async () => {
1259+
await withTempStateDir(async (stateDir) => {
1260+
const sessionId = "session-stale-model";
1261+
const storePath = path.join(stateDir, "sessions", "sessions.json");
1262+
const transcriptPath = sessions.resolveSessionTranscriptPath(sessionId);
1263+
const sessionEntry: SessionEntry = {
1264+
sessionId,
1265+
updatedAt: Date.now(),
1266+
sessionFile: transcriptPath,
1267+
modelProvider: "qwencode",
1268+
model: "qwen3.5-plus-2026-02-15",
1269+
contextTokens: 123456,
1270+
systemPromptReport: {
1271+
source: "run",
1272+
generatedAt: Date.now(),
1273+
sessionId,
1274+
sessionKey: "main",
1275+
provider: "qwencode",
1276+
model: "qwen3.5-plus-2026-02-15",
1277+
workspaceDir: stateDir,
1278+
bootstrapMaxChars: 1000,
1279+
bootstrapTotalMaxChars: 2000,
1280+
systemPrompt: {
1281+
chars: 10,
1282+
projectContextChars: 5,
1283+
nonProjectContextChars: 5,
1284+
},
1285+
injectedWorkspaceFiles: [],
1286+
skills: {
1287+
promptChars: 0,
1288+
entries: [],
1289+
},
1290+
tools: {
1291+
listChars: 0,
1292+
schemaChars: 0,
1293+
entries: [],
1294+
},
1295+
},
1296+
};
1297+
const sessionStore = { main: sessionEntry };
1298+
1299+
await fs.mkdir(path.dirname(storePath), { recursive: true });
1300+
await fs.writeFile(storePath, JSON.stringify(sessionStore), "utf-8");
1301+
await fs.mkdir(path.dirname(transcriptPath), { recursive: true });
1302+
await fs.writeFile(transcriptPath, "ok", "utf-8");
1303+
1304+
state.runEmbeddedPiAgentMock.mockImplementationOnce(async () => {
1305+
throw new Error(
1306+
'Context overflow: Summarization failed: 400 {"message":"prompt is too long"}',
1307+
);
1308+
});
1309+
1310+
const { run } = createMinimalRun({
1311+
sessionEntry,
1312+
sessionStore,
1313+
sessionKey: "main",
1314+
storePath,
1315+
});
1316+
await run();
1317+
1318+
expect(sessionStore.main.modelProvider).toBeUndefined();
1319+
expect(sessionStore.main.model).toBeUndefined();
1320+
expect(sessionStore.main.contextTokens).toBeUndefined();
1321+
expect(sessionStore.main.systemPromptReport).toBeUndefined();
1322+
1323+
const persisted = JSON.parse(await fs.readFile(storePath, "utf-8"));
1324+
expect(persisted.main.modelProvider).toBeUndefined();
1325+
expect(persisted.main.model).toBeUndefined();
1326+
expect(persisted.main.contextTokens).toBeUndefined();
1327+
expect(persisted.main.systemPromptReport).toBeUndefined();
1328+
});
1329+
});
1330+
12581331
it("surfaces overflow fallback when embedded run returns empty payloads", async () => {
12591332
state.runEmbeddedPiAgentMock.mockImplementationOnce(async () => ({
12601333
payloads: [],

src/auto-reply/reply/agent-runner.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,10 @@ export async function runReplyAgent(params: {
278278
updatedAt: Date.now(),
279279
systemSent: false,
280280
abortedLastRun: false,
281+
modelProvider: undefined,
282+
model: undefined,
283+
contextTokens: undefined,
284+
systemPromptReport: undefined,
281285
fallbackNoticeSelectedModel: undefined,
282286
fallbackNoticeActiveModel: undefined,
283287
fallbackNoticeReason: undefined,

src/gateway/server-methods/sessions.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,19 @@ function migrateAndPruneSessionStoreKey(params: {
128128
return { target, primaryKey, entry: params.store[primaryKey] };
129129
}
130130

131+
function stripRuntimeModelState(entry?: SessionEntry): SessionEntry | undefined {
132+
if (!entry) {
133+
return entry;
134+
}
135+
return {
136+
...entry,
137+
model: undefined,
138+
modelProvider: undefined,
139+
contextTokens: undefined,
140+
systemPromptReport: undefined,
141+
};
142+
}
143+
131144
function archiveSessionTranscriptsForSession(params: {
132145
sessionId: string | undefined;
133146
storePath: string;
@@ -507,9 +520,10 @@ export const sessionsHandlers: GatewayRequestHandlers = {
507520
const next = await updateSessionStore(storePath, (store) => {
508521
const { primaryKey } = migrateAndPruneSessionStoreKey({ cfg, key, store });
509522
const entry = store[primaryKey];
523+
const resetEntry = stripRuntimeModelState(entry);
510524
const parsed = parseAgentSessionKey(primaryKey);
511525
const sessionAgentId = normalizeAgentId(parsed?.agentId ?? resolveDefaultAgentId(cfg));
512-
const resolvedModel = resolveSessionModelRef(cfg, entry, sessionAgentId);
526+
const resolvedModel = resolveSessionModelRef(cfg, resetEntry, sessionAgentId);
513527
oldSessionId = entry?.sessionId;
514528
oldSessionFile = entry?.sessionFile;
515529
const now = Date.now();
@@ -524,7 +538,7 @@ export const sessionsHandlers: GatewayRequestHandlers = {
524538
responseUsage: entry?.responseUsage,
525539
model: resolvedModel.model,
526540
modelProvider: resolvedModel.provider,
527-
contextTokens: entry?.contextTokens,
541+
contextTokens: resetEntry?.contextTokens,
528542
sendPolicy: entry?.sendPolicy,
529543
label: entry?.label,
530544
origin: snapshotSessionOrigin(entry),

src/gateway/server.sessions.gateway-server-sessions-a.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,43 @@ describe("gateway server sessions", () => {
591591
ws.close();
592592
});
593593

594+
test("sessions.reset recomputes model from defaults instead of stale runtime model", async () => {
595+
await createSessionStoreDir();
596+
testState.agentConfig = {
597+
model: {
598+
primary: "openai/gpt-test-a",
599+
},
600+
};
601+
602+
await writeSessionStore({
603+
entries: {
604+
main: {
605+
sessionId: "sess-stale-model",
606+
updatedAt: Date.now(),
607+
modelProvider: "qwencode",
608+
model: "qwen3.5-plus-2026-02-15",
609+
contextTokens: 123456,
610+
},
611+
},
612+
});
613+
614+
const { ws } = await openClient();
615+
const reset = await rpcReq<{
616+
ok: true;
617+
key: string;
618+
entry: { sessionId: string; modelProvider?: string; model?: string; contextTokens?: number };
619+
}>(ws, "sessions.reset", { key: "main" });
620+
621+
expect(reset.ok).toBe(true);
622+
expect(reset.payload?.key).toBe("agent:main:main");
623+
expect(reset.payload?.entry.sessionId).not.toBe("sess-stale-model");
624+
expect(reset.payload?.entry.modelProvider).toBe("openai");
625+
expect(reset.payload?.entry.model).toBe("gpt-test-a");
626+
expect(reset.payload?.entry.contextTokens).toBeUndefined();
627+
628+
ws.close();
629+
});
630+
594631
test("sessions.preview resolves legacy mixed-case main alias with custom mainKey", async () => {
595632
const { dir, storePath } = await createSessionStoreDir();
596633
testState.agentsConfig = { list: [{ id: "ops", default: true }] };

0 commit comments

Comments
 (0)