From 058e2f4df4a1cdd4c620d0cc440774e113bfefc5 Mon Sep 17 00:00:00 2001 From: Space-Banane Date: Wed, 8 Apr 2026 20:12:36 +0200 Subject: [PATCH] Implement unique filename generation to prevent overwriting existing session files --- HOOK.md | 3 +++ README.md | 7 +++++++ handler.ts | 22 +++++++++++++++++++++- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/HOOK.md b/HOOK.md index c95c455..cebf1d0 100644 --- a/HOOK.md +++ b/HOOK.md @@ -17,6 +17,9 @@ metadata: Writes full user/assistant transcript context from the previous session into `/memory/YYYY-MM-DD-slug.md` when `/new` or `/reset` runs. +This hook avoids overwriting existing files by choosing a unique filename +when a collision occurs (it appends `-1`, `-2`, etc. to the base name). + Differences from bundled session-memory: - Uses full session context in output file diff --git a/README.md b/README.md index 4d488c7..63842da 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ Custom OpenClaw session-memory hook. - Saves the **full** user/assistant session context into `workspace/memory/` - Uses `agents.defaults.userTimezone` for date/time formatting - Uses a slugged filename, with slug generation based on a small excerpt + - Avoids overwriting existing files: if a filename already exists the hook + appends `-1`, `-2`, ... to produce a unique filename. ## Disable the bundled hook @@ -42,3 +44,8 @@ Or set it false in config: ## Notes This repo is intentionally lightweight, it exists so the custom hook can live in the managed hooks directory and stay separate from the bundled one. + +If you prefer atomic creation to fully eliminate a race window (two processes +creating the same new filename simultaneously), replace the simple existence +check with an atomic open/write using `fs.open(..., 'wx')` or write to a +temporary file and `fs.rename()` it into place. diff --git a/handler.ts b/handler.ts index 4438685..f7679ce 100644 --- a/handler.ts +++ b/handler.ts @@ -197,6 +197,26 @@ function buildSlugExcerpt(full: string): string { return tail.slice(-3000); } +async function getUniqueFilePath(dir: string, filename: string): Promise { + const candidate = path.join(dir, filename); + try { + await fs.access(candidate); + } catch { + return candidate; + } + + const parsed = path.parse(filename); + for (let i = 1; ; i += 1) { + const name = `${parsed.name}-${i}${parsed.ext}`; + const p = path.join(dir, name); + try { + await fs.access(p); + } catch { + return p; + } + } +} + async function generateSlug(params: { excerpt: string; cfg: AnyObj; @@ -274,7 +294,7 @@ const handler = async (event: { await fs.mkdir(memoryDir, { recursive: true }); const filename = `${dateStr}-${slug}.md`; - const memoryFilePath = path.join(memoryDir, filename); + const memoryFilePath = await getUniqueFilePath(memoryDir, filename); const source = typeof context.commandSource === "string" ? context.commandSource : "unknown";