Hooks: persist session memory on /reset

This commit is contained in:
Vignesh Natarajan
2026-02-20 20:19:29 -08:00
parent 544c213d42
commit d583399c92
8 changed files with 39 additions and 19 deletions

View File

@@ -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. - 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/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. - 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. - 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: 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. - 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.

View File

@@ -86,13 +86,13 @@ describe("onboard-hooks", () => {
createMockHook( createMockHook(
{ {
name: "session-memory", 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", filePath: "/mock/workspace/hooks/session-memory/HOOK.md",
baseDir: "/mock/workspace/hooks/session-memory", baseDir: "/mock/workspace/hooks/session-memory",
handlerPath: "/mock/workspace/hooks/session-memory/handler.js", handlerPath: "/mock/workspace/hooks/session-memory/handler.js",
hookKey: "session-memory", hookKey: "session-memory",
emoji: "💾", emoji: "💾",
events: ["command:new"], events: ["command:new", "command:reset"],
}, },
eligible, eligible,
), ),
@@ -147,7 +147,7 @@ describe("onboard-hooks", () => {
{ {
value: "session-memory", value: "session-memory",
label: "💾 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", value: "command-logger",

View File

@@ -13,7 +13,7 @@ export async function setupInternalHooks(
await prompter.note( await prompter.note(
[ [
"Hooks let you automate actions when agent commands are issued.", "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", "Learn more: https://docs.openclaw.ai/automation/hooks",
].join("\n"), ].join("\n"),

View File

@@ -6,9 +6,9 @@ This directory contains hooks that ship with OpenClaw. These hooks are automatic
### 💾 session-memory ### 💾 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. **What it does**: Creates a dated memory file with LLM-generated slug based on conversation content.
**Output**: `<workspace>/memory/YYYY-MM-DD-slug.md` (defaults to `~/.openclaw/workspace`) **Output**: `<workspace>/memory/YYYY-MM-DD-slug.md` (defaults to `~/.openclaw/workspace`)

View File

@@ -1,13 +1,13 @@
--- ---
name: session-memory 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 homepage: https://docs.openclaw.ai/automation/hooks#session-memory
metadata: metadata:
{ {
"openclaw": "openclaw":
{ {
"emoji": "💾", "emoji": "💾",
"events": ["command:new"], "events": ["command:new", "command:reset"],
"requires": { "config": ["workspace.dir"] }, "requires": { "config": ["workspace.dir"] },
"install": [{ "id": "bundled", "kind": "bundled", "label": "Bundled with OpenClaw" }], "install": [{ "id": "bundled", "kind": "bundled", "label": "Bundled with OpenClaw" }],
}, },
@@ -16,11 +16,11 @@ metadata:
# Session Memory Hook # 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 ## 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 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) 2. **Extracts conversation** - Reads the last N user/assistant messages from the session (default: 15, configurable)

View File

@@ -44,8 +44,9 @@ async function runNewWithPreviousSessionEntry(params: {
tempDir: string; tempDir: string;
previousSessionEntry: { sessionId: string; sessionFile?: string }; previousSessionEntry: { sessionId: string; sessionFile?: string };
cfg?: OpenClawConfig; cfg?: OpenClawConfig;
action?: "new" | "reset";
}): Promise<{ files: string[]; memoryContent: string }> { }): Promise<{ files: string[]; memoryContent: string }> {
const event = createHookEvent("command", "new", "agent:main:main", { const event = createHookEvent("command", params.action ?? "new", "agent:main:main", {
cfg: cfg:
params.cfg ?? params.cfg ??
({ ({
@@ -66,6 +67,7 @@ async function runNewWithPreviousSessionEntry(params: {
async function runNewWithPreviousSession(params: { async function runNewWithPreviousSession(params: {
sessionContent: string; sessionContent: string;
cfg?: (tempDir: string) => OpenClawConfig; cfg?: (tempDir: string) => OpenClawConfig;
action?: "new" | "reset";
}): Promise<{ tempDir: string; files: string[]; memoryContent: string }> { }): Promise<{ tempDir: string; files: string[]; memoryContent: string }> {
const tempDir = await makeTempWorkspace("openclaw-session-memory-"); const tempDir = await makeTempWorkspace("openclaw-session-memory-");
const sessionsDir = path.join(tempDir, "sessions"); const sessionsDir = path.join(tempDir, "sessions");
@@ -86,6 +88,7 @@ async function runNewWithPreviousSession(params: {
const { files, memoryContent } = await runNewWithPreviousSessionEntry({ const { files, memoryContent } = await runNewWithPreviousSessionEntry({
tempDir, tempDir,
cfg, cfg,
action: params.action,
previousSessionEntry: { previousSessionEntry: {
sessionId: "test-123", sessionId: "test-123",
sessionFile, sessionFile,
@@ -158,6 +161,21 @@ describe("session-memory hook", () => {
expect(memoryContent).toContain("assistant: 2+2 equals 4"); 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 () => { it("filters out non-message entries (tool calls, system)", async () => {
// Create session with mixed entry types // Create session with mixed entry types
const sessionContent = createMockSessionContent([ const sessionContent = createMockSessionContent([

View File

@@ -1,7 +1,7 @@
/** /**
* Session memory hook handler * 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 * 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) => { const saveSessionToMemory: HookHandler = async (event) => {
// Only trigger on 'new' command // Only trigger on reset/new commands
if (event.type !== "command" || event.action !== "new") { const isResetCommand = event.action === "new" || event.action === "reset";
if (event.type !== "command" || !isResetCommand) {
return; return;
} }
try { try {
log.debug("Hook triggered for /new command"); log.debug("Hook triggered for reset/new command", { action: event.action });
const context = event.context || {}; const context = event.context || {};
const cfg = context.cfg as OpenClawConfig | undefined; const cfg = context.cfg as OpenClawConfig | undefined;

View File

@@ -232,14 +232,14 @@ describe("resolveOpenClawMetadata", () => {
// This is the actual format used in the bundled hooks // This is the actual format used in the bundled hooks
const content = `--- const content = `---
name: session-memory 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 homepage: https://docs.openclaw.ai/automation/hooks#session-memory
metadata: metadata:
{ {
"openclaw": "openclaw":
{ {
"emoji": "💾", "emoji": "💾",
"events": ["command:new"], "events": ["command:new", "command:reset"],
"requires": { "config": ["workspace.dir"] }, "requires": { "config": ["workspace.dir"] },
"install": [{ "id": "bundled", "kind": "bundled", "label": "Bundled with OpenClaw" }], "install": [{ "id": "bundled", "kind": "bundled", "label": "Bundled with OpenClaw" }],
}, },
@@ -256,7 +256,7 @@ metadata:
const openclaw = resolveOpenClawMetadata(frontmatter); const openclaw = resolveOpenClawMetadata(frontmatter);
expect(openclaw).toBeDefined(); expect(openclaw).toBeDefined();
expect(openclaw?.emoji).toBe("💾"); 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?.requires?.config).toEqual(["workspace.dir"]);
expect(openclaw?.install?.[0].kind).toBe("bundled"); expect(openclaw?.install?.[0].kind).toBe("bundled");
}); });