mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
Fix stale runtime model reuse on session reset (#41173)
Merged via squash.
Prepared head SHA: d8a04a466a
Co-authored-by: PonyX-lab <266766228+PonyX-lab@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
This commit is contained in:
@@ -76,6 +76,7 @@ Docs: https://docs.openclaw.ai
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
## 2026.3.8
|
||||
|
||||
|
||||
@@ -1255,6 +1255,79 @@ 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",
|
||||
contextTokens: 123456,
|
||||
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.contextTokens).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.contextTokens).toBeUndefined();
|
||||
expect(persisted.main.systemPromptReport).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("surfaces overflow fallback when embedded run returns empty payloads", async () => {
|
||||
state.runEmbeddedPiAgentMock.mockImplementationOnce(async () => ({
|
||||
payloads: [],
|
||||
|
||||
@@ -278,6 +278,10 @@ export async function runReplyAgent(params: {
|
||||
updatedAt: Date.now(),
|
||||
systemSent: false,
|
||||
abortedLastRun: false,
|
||||
modelProvider: undefined,
|
||||
model: undefined,
|
||||
contextTokens: undefined,
|
||||
systemPromptReport: undefined,
|
||||
fallbackNoticeSelectedModel: undefined,
|
||||
fallbackNoticeActiveModel: undefined,
|
||||
fallbackNoticeReason: undefined,
|
||||
|
||||
@@ -128,6 +128,19 @@ 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,
|
||||
contextTokens: undefined,
|
||||
systemPromptReport: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function archiveSessionTranscriptsForSession(params: {
|
||||
sessionId: string | undefined;
|
||||
storePath: string;
|
||||
@@ -507,9 +520,10 @@ export const sessionsHandlers: GatewayRequestHandlers = {
|
||||
const next = await updateSessionStore(storePath, (store) => {
|
||||
const { primaryKey } = migrateAndPruneSessionStoreKey({ cfg, key, store });
|
||||
const entry = store[primaryKey];
|
||||
const resetEntry = stripRuntimeModelState(entry);
|
||||
const parsed = parseAgentSessionKey(primaryKey);
|
||||
const sessionAgentId = normalizeAgentId(parsed?.agentId ?? resolveDefaultAgentId(cfg));
|
||||
const resolvedModel = resolveSessionModelRef(cfg, entry, sessionAgentId);
|
||||
const resolvedModel = resolveSessionModelRef(cfg, resetEntry, sessionAgentId);
|
||||
oldSessionId = entry?.sessionId;
|
||||
oldSessionFile = entry?.sessionFile;
|
||||
const now = Date.now();
|
||||
@@ -524,7 +538,7 @@ export const sessionsHandlers: GatewayRequestHandlers = {
|
||||
responseUsage: entry?.responseUsage,
|
||||
model: resolvedModel.model,
|
||||
modelProvider: resolvedModel.provider,
|
||||
contextTokens: entry?.contextTokens,
|
||||
contextTokens: resetEntry?.contextTokens,
|
||||
sendPolicy: entry?.sendPolicy,
|
||||
label: entry?.label,
|
||||
origin: snapshotSessionOrigin(entry),
|
||||
|
||||
@@ -591,6 +591,43 @@ 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",
|
||||
contextTokens: 123456,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { ws } = await openClient();
|
||||
const reset = await rpcReq<{
|
||||
ok: true;
|
||||
key: string;
|
||||
entry: { sessionId: string; modelProvider?: string; model?: string; contextTokens?: number };
|
||||
}>(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");
|
||||
expect(reset.payload?.entry.contextTokens).toBeUndefined();
|
||||
|
||||
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 }] };
|
||||
|
||||
Reference in New Issue
Block a user