From d583399c92401f64261da28a6531f72f052e96d8 Mon Sep 17 00:00:00 2001 From: Vignesh Natarajan Date: Fri, 20 Feb 2026 20:19:29 -0800 Subject: [PATCH] Hooks: persist session memory on /reset --- CHANGELOG.md | 1 + src/commands/onboard-hooks.e2e.test.ts | 6 +++--- src/commands/onboard-hooks.ts | 2 +- src/hooks/bundled/README.md | 4 ++-- src/hooks/bundled/session-memory/HOOK.md | 8 ++++---- .../bundled/session-memory/handler.test.ts | 20 ++++++++++++++++++- src/hooks/bundled/session-memory/handler.ts | 11 +++++----- src/hooks/frontmatter.test.ts | 6 +++--- 8 files changed, 39 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0aed689853..e397567e2bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ Docs: https://docs.openclaw.ai - Gateway/Pairing: clear persisted paired-device state when the gateway client closes with `device token mismatch` (`1008`) so reconnect flows can cleanly re-enter pairing. (#22071) Thanks @mbelinky. - Memory/QMD: respect per-agent `memorySearch.enabled=false` during gateway QMD startup initialization, split multi-collection QMD searches into per-collection queries (`search`/`vsearch`/`query`) to avoid sparse-term drops, prefer collection-hinted doc resolution to avoid stale-hash collisions, retry boot updates on transient lock/timeout failures, skip `qmd embed` in BM25-only `search` mode (including `memory index --force`), and serialize embed runs globally with failure backoff to prevent CPU storms on multi-agent hosts. (#20581, #21590, #20513, #20001, #21266, #21583, #20346, #19493) Thanks @danielrevivo, @zanderkrause, @sunyan034-cmd, @tilleulenspiegel, @dae-oss, @adamlongcreativellc, @jonathanadams96, and @kiliansitel. - Memory/Builtin: prevent automatic sync races with manager shutdown by skipping post-close sync starts and waiting for in-flight sync before closing SQLite, so `onSearch`/`onSessionStart` no longer fail with `database is not open` in ephemeral CLI flows. (#20556, #7464) Thanks @FuzzyTG and @henrybottter. +- Hooks/Session memory: trigger bundled `session-memory` persistence on both `/new` and `/reset` so reset flows no longer skip markdown transcript capture before archival. (#21382) Thanks @mofesolapaul. - Signal/Outbound: preserve case for Base64 group IDs during outbound target normalization so cross-context routing and policy checks no longer break when group IDs include uppercase characters. (#5578) Thanks @heyhudson. - Providers/Copilot: drop persisted assistant `thinking` blocks for Claude models (while preserving turn structure/tool blocks) so follow-up requests no longer fail on invalid `thinkingSignature` payloads. (#19459) Thanks @jackheuberger. - Providers/Copilot: add `claude-sonnet-4.6` and `claude-sonnet-4.5` to the default GitHub Copilot model catalog and add coverage for model-list/definition helpers. (#20270, fixes #20091) Thanks @Clawborn. diff --git a/src/commands/onboard-hooks.e2e.test.ts b/src/commands/onboard-hooks.e2e.test.ts index 02b82b22fc5..9ba530aeb3b 100644 --- a/src/commands/onboard-hooks.e2e.test.ts +++ b/src/commands/onboard-hooks.e2e.test.ts @@ -86,13 +86,13 @@ describe("onboard-hooks", () => { createMockHook( { name: "session-memory", - description: "Save session context to memory when /new command is issued", + description: "Save session context to memory when /new or /reset command is issued", filePath: "/mock/workspace/hooks/session-memory/HOOK.md", baseDir: "/mock/workspace/hooks/session-memory", handlerPath: "/mock/workspace/hooks/session-memory/handler.js", hookKey: "session-memory", emoji: "💾", - events: ["command:new"], + events: ["command:new", "command:reset"], }, eligible, ), @@ -147,7 +147,7 @@ describe("onboard-hooks", () => { { value: "session-memory", label: "💾 session-memory", - hint: "Save session context to memory when /new command is issued", + hint: "Save session context to memory when /new or /reset command is issued", }, { value: "command-logger", diff --git a/src/commands/onboard-hooks.ts b/src/commands/onboard-hooks.ts index 575c3590da6..b2086161511 100644 --- a/src/commands/onboard-hooks.ts +++ b/src/commands/onboard-hooks.ts @@ -13,7 +13,7 @@ export async function setupInternalHooks( await prompter.note( [ "Hooks let you automate actions when agent commands are issued.", - "Example: Save session context to memory when you issue /new.", + "Example: Save session context to memory when you issue /new or /reset.", "", "Learn more: https://docs.openclaw.ai/automation/hooks", ].join("\n"), diff --git a/src/hooks/bundled/README.md b/src/hooks/bundled/README.md index e948beb40c7..ad520d16d02 100644 --- a/src/hooks/bundled/README.md +++ b/src/hooks/bundled/README.md @@ -6,9 +6,9 @@ This directory contains hooks that ship with OpenClaw. These hooks are automatic ### 💾 session-memory -Automatically saves session context to memory when you issue `/new`. +Automatically saves session context to memory when you issue `/new` or `/reset`. -**Events**: `command:new` +**Events**: `command:new`, `command:reset` **What it does**: Creates a dated memory file with LLM-generated slug based on conversation content. **Output**: `/memory/YYYY-MM-DD-slug.md` (defaults to `~/.openclaw/workspace`) diff --git a/src/hooks/bundled/session-memory/HOOK.md b/src/hooks/bundled/session-memory/HOOK.md index 1e938656ec2..6969fb2b8be 100644 --- a/src/hooks/bundled/session-memory/HOOK.md +++ b/src/hooks/bundled/session-memory/HOOK.md @@ -1,13 +1,13 @@ --- name: session-memory -description: "Save session context to memory when /new command is issued" +description: "Save session context to memory when /new or /reset command is issued" homepage: https://docs.openclaw.ai/automation/hooks#session-memory metadata: { "openclaw": { "emoji": "💾", - "events": ["command:new"], + "events": ["command:new", "command:reset"], "requires": { "config": ["workspace.dir"] }, "install": [{ "id": "bundled", "kind": "bundled", "label": "Bundled with OpenClaw" }], }, @@ -16,11 +16,11 @@ metadata: # Session Memory Hook -Automatically saves session context to your workspace memory when you issue the `/new` command. +Automatically saves session context to your workspace memory when you issue `/new` or `/reset`. ## What It Does -When you run `/new` to start a fresh session: +When you run `/new` or `/reset` to start a fresh session: 1. **Finds the previous session** - Uses the pre-reset session entry to locate the correct transcript 2. **Extracts conversation** - Reads the last N user/assistant messages from the session (default: 15, configurable) diff --git a/src/hooks/bundled/session-memory/handler.test.ts b/src/hooks/bundled/session-memory/handler.test.ts index 57d300f824f..4ddec40ac1d 100644 --- a/src/hooks/bundled/session-memory/handler.test.ts +++ b/src/hooks/bundled/session-memory/handler.test.ts @@ -44,8 +44,9 @@ async function runNewWithPreviousSessionEntry(params: { tempDir: string; previousSessionEntry: { sessionId: string; sessionFile?: string }; cfg?: OpenClawConfig; + action?: "new" | "reset"; }): Promise<{ files: string[]; memoryContent: string }> { - const event = createHookEvent("command", "new", "agent:main:main", { + const event = createHookEvent("command", params.action ?? "new", "agent:main:main", { cfg: params.cfg ?? ({ @@ -66,6 +67,7 @@ async function runNewWithPreviousSessionEntry(params: { async function runNewWithPreviousSession(params: { sessionContent: string; cfg?: (tempDir: string) => OpenClawConfig; + action?: "new" | "reset"; }): Promise<{ tempDir: string; files: string[]; memoryContent: string }> { const tempDir = await makeTempWorkspace("openclaw-session-memory-"); const sessionsDir = path.join(tempDir, "sessions"); @@ -86,6 +88,7 @@ async function runNewWithPreviousSession(params: { const { files, memoryContent } = await runNewWithPreviousSessionEntry({ tempDir, cfg, + action: params.action, previousSessionEntry: { sessionId: "test-123", sessionFile, @@ -158,6 +161,21 @@ describe("session-memory hook", () => { expect(memoryContent).toContain("assistant: 2+2 equals 4"); }); + it("creates memory file with session content on /reset command", async () => { + const sessionContent = createMockSessionContent([ + { role: "user", content: "Please reset and keep notes" }, + { role: "assistant", content: "Captured before reset" }, + ]); + const { files, memoryContent } = await runNewWithPreviousSession({ + sessionContent, + action: "reset", + }); + + expect(files.length).toBe(1); + expect(memoryContent).toContain("user: Please reset and keep notes"); + expect(memoryContent).toContain("assistant: Captured before reset"); + }); + it("filters out non-message entries (tool calls, system)", async () => { // Create session with mixed entry types const sessionContent = createMockSessionContent([ diff --git a/src/hooks/bundled/session-memory/handler.ts b/src/hooks/bundled/session-memory/handler.ts index f35938124cb..8c45f01777f 100644 --- a/src/hooks/bundled/session-memory/handler.ts +++ b/src/hooks/bundled/session-memory/handler.ts @@ -1,7 +1,7 @@ /** * Session memory hook handler * - * Saves session context to memory when /new command is triggered + * Saves session context to memory when /new or /reset command is triggered * Creates a new dated memory file with LLM-generated slug */ @@ -167,16 +167,17 @@ async function findPreviousSessionFile(params: { } /** - * Save session context to memory when /new command is triggered + * Save session context to memory when /new or /reset command is triggered */ const saveSessionToMemory: HookHandler = async (event) => { - // Only trigger on 'new' command - if (event.type !== "command" || event.action !== "new") { + // Only trigger on reset/new commands + const isResetCommand = event.action === "new" || event.action === "reset"; + if (event.type !== "command" || !isResetCommand) { return; } try { - log.debug("Hook triggered for /new command"); + log.debug("Hook triggered for reset/new command", { action: event.action }); const context = event.context || {}; const cfg = context.cfg as OpenClawConfig | undefined; diff --git a/src/hooks/frontmatter.test.ts b/src/hooks/frontmatter.test.ts index 18fb6e0d974..5c8303c2360 100644 --- a/src/hooks/frontmatter.test.ts +++ b/src/hooks/frontmatter.test.ts @@ -232,14 +232,14 @@ describe("resolveOpenClawMetadata", () => { // This is the actual format used in the bundled hooks const content = `--- name: session-memory -description: "Save session context to memory when /new command is issued" +description: "Save session context to memory when /new or /reset command is issued" homepage: https://docs.openclaw.ai/automation/hooks#session-memory metadata: { "openclaw": { "emoji": "💾", - "events": ["command:new"], + "events": ["command:new", "command:reset"], "requires": { "config": ["workspace.dir"] }, "install": [{ "id": "bundled", "kind": "bundled", "label": "Bundled with OpenClaw" }], }, @@ -256,7 +256,7 @@ metadata: const openclaw = resolveOpenClawMetadata(frontmatter); expect(openclaw).toBeDefined(); expect(openclaw?.emoji).toBe("💾"); - expect(openclaw?.events).toEqual(["command:new"]); + expect(openclaw?.events).toEqual(["command:new", "command:reset"]); expect(openclaw?.requires?.config).toEqual(["workspace.dir"]); expect(openclaw?.install?.[0].kind).toBe("bundled"); });