mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 19:30:42 +00:00
fix: clear reset skills snapshot (#78873)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user