diff --git a/CHANGELOG.md b/CHANGELOG.md
index 52698c9a182..ffd0b8e3cc3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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.
diff --git a/src/auto-reply/reply/session.test.ts b/src/auto-reply/reply/session.test.ts
index 461ebef2f58..d044335915c 100644
--- a/src/auto-reply/reply/session.test.ts
+++ b/src/auto-reply/reply/session.test.ts
@@ -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: "stale",
+ 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");
diff --git a/src/auto-reply/reply/session.ts b/src/auto-reply/reply/session.ts
index cc2e31af08d..78b82029196 100644
--- a/src/auto-reply/reply/session.ts
+++ b/src/auto-reply/reply/session.ts
@@ -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 };
diff --git a/src/gateway/server.sessions.reset-models.test.ts b/src/gateway/server.sessions.reset-models.test.ts
index bec49f0bb8c..5095540865f 100644
--- a/src/gateway/server.sessions.reset-models.test.ts
+++ b/src/gateway/server.sessions.reset-models.test.ts
@@ -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: "stale",
+ 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 = {
diff --git a/src/gateway/session-reset-service.ts b/src/gateway/session-reset-service.ts
index d53b31491ba..69e7e7f7522 100644
--- a/src/gateway/session-reset-service.ts
+++ b/src/gateway/session-reset-service.ts
@@ -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 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,