fix: clear reset skills snapshot (#78873)

This commit is contained in:
Christof
2026-05-07 11:18:39 +02:00
committed by GitHub
parent 3a901b5e95
commit afdf03b563
5 changed files with 96 additions and 1 deletions

View File

@@ -141,6 +141,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Gateway/sessions: clear cached skills snapshots during `/new` and `sessions.reset` so long-lived channel sessions rebuild the visible skill list after skills change. (#78873) Thanks @Evizero.
- fix(auto-reply): gate inline skill tool dispatch [AI]. (#78517) Thanks @pgondhi987.
- Canvas plugin: keep legacy root `canvasHost` configs valid until `openclaw doctor --fix` migrates them into `plugins.entries.canvas.config.host`, move Canvas/A2UI clients to gateway protocol v4 plugin surfaces, and refresh the generated A2UI bundle hash so normal builds stay clean.
- feishu: honor config write policy for dynamic agents [AI]. (#78520) Thanks @pgondhi987.

View File

@@ -729,6 +729,54 @@ describe("initSessionState RawBody", () => {
expect(result.triggerBodyNormalized).toBe("/NEW KeepThisCase");
});
it("drops cached skills snapshot when /new rotates an existing session", async () => {
const root = await makeCaseDir("openclaw-rawbody-reset-skills-");
const storePath = path.join(root, "sessions.json");
const sessionKey = "agent:main:signal:direct:uuid:reset-skills";
const existingSessionId = "session-with-stale-skills";
await writeSessionStoreFast(storePath, {
[sessionKey]: {
sessionId: existingSessionId,
updatedAt: Date.now(),
systemSent: true,
skillsSnapshot: {
prompt: "<available_skills><skill><name>stale</name></skill></available_skills>",
skills: [{ name: "stale" }],
version: 0,
},
},
});
const cfg = {
session: {
store: storePath,
resetTriggers: ["/new"],
},
} as OpenClawConfig;
const result = await initSessionState({
ctx: {
RawBody: "/new continue",
ChatType: "direct",
SessionKey: sessionKey,
},
cfg,
commandAuthorized: true,
});
expect(result.isNewSession).toBe(true);
expect(result.resetTriggered).toBe(true);
expect(result.sessionId).not.toBe(existingSessionId);
expect(result.sessionEntry.skillsSnapshot).toBeUndefined();
const store = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record<
string,
{ skillsSnapshot?: unknown }
>;
expect(store[sessionKey]?.skillsSnapshot).toBeUndefined();
});
it("drains stale system events when /new rotates an existing session", async () => {
const root = await makeCaseDir("openclaw-rawbody-reset-system-events-");
const storePath = path.join(root, "sessions.json");

View File

@@ -768,6 +768,9 @@ export async function initSessionState(params: {
sessionEntry.outputTokens = undefined;
sessionEntry.estimatedCostUsd = undefined;
sessionEntry.contextTokens = undefined;
// Skills snapshots are prompt/runtime caches. Do not preserve a stale
// snapshot through /new; the next turn must rebuild the visible skill list.
sessionEntry.skillsSnapshot = undefined;
}
// Preserve per-session overrides while resetting compaction state on /new.
sessionStore[sessionKey] = { ...sessionStore[sessionKey], ...sessionEntry };

View File

@@ -50,6 +50,46 @@ test("sessions.reset recomputes model from defaults instead of stale runtime mod
await expect(fs.stat(reset.payload?.entry.sessionFile as string)).resolves.toBeTruthy();
});
test("sessions.reset drops cached skills snapshot so /new rebuilds visible skills", async () => {
const { storePath } = await createSessionStoreDir();
testState.agentConfig = {
model: {
primary: "openai/gpt-test-a",
},
};
await writeSessionStore({
entries: {
main: sessionStoreEntry("sess-stale-skills", {
skillsSnapshot: {
prompt: "<available_skills><skill><name>stale</name></skill></available_skills>",
skills: [{ name: "stale" }],
version: 0,
},
}),
},
});
const reset = await directSessionReq<{
ok: true;
key: string;
entry: {
sessionId: string;
skillsSnapshot?: unknown;
};
}>("sessions.reset", { key: "main" });
expect(reset.ok).toBe(true);
expect(reset.payload?.entry.sessionId).not.toBe("sess-stale-skills");
expect(reset.payload?.entry.skillsSnapshot).toBeUndefined();
const store = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record<
string,
{ skillsSnapshot?: unknown }
>;
expect(store["agent:main:main"]?.skillsSnapshot).toBeUndefined();
});
test("sessions.reset preserves legacy explicit model overrides without modelOverrideSource", async () => {
const { storePath } = await createSessionStoreDir();
testState.agentConfig = {

View File

@@ -623,7 +623,10 @@ export async function performGatewaySessionReset(params: {
lastTo: currentEntry?.lastTo,
lastAccountId: currentEntry?.lastAccountId,
lastThreadId: currentEntry?.lastThreadId,
skillsSnapshot: currentEntry?.skillsSnapshot,
// Do not carry the cached skills catalog across /new. Long-lived channel
// sessions (Signal DMs/groups in particular) otherwise keep advertising a
// stale <available_skills> block even after reset/restart, because the
// skills snapshot version is runtime-local and may reset to 0.
acp: currentEntry?.acp,
inputTokens: 0,
outputTokens: 0,