From 61383aff4b8d9cc5def678d01dce9d5d2f111e79 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 5 May 2026 01:03:59 -0700 Subject: [PATCH] fix(hooks): avoid session memory filename collisions Add collision suffixes for session-memory fallback filenames so repeated same-minute reset/new captures do not overwrite earlier archives. --- CHANGELOG.md | 1 + .../bundled/session-memory/handler.test.ts | 35 +++++++++++++++++++ src/hooks/bundled/session-memory/handler.ts | 24 ++++++++++++- 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c341a186f56..8d5d14b66fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,7 @@ Docs: https://docs.openclaw.ai - Gateway/status: show recent supervisor restart handoffs in `openclaw gateway status --deep`, including JSON details, so clean service-managed restarts are reported as restart handoffs instead of opaque stopped-service diagnostics. Thanks @shakkernerd. - Providers/Fireworks: expose Kimi models as thinking-off-only and keep K2.5/K2.6 requests on `thinking: disabled`, so manual model switches do not send Fireworks-rejected `reasoning*` parameters. Refs #74289. Thanks @frankekn. - WhatsApp responsiveness: stop only verified stale local TUI clients when they degrade the Gateway event loop and delay replies. Thanks @vincentkoc. +- Hooks/session-memory: add collision suffixes to fallback memory filenames so repeated `/new` or `/reset` captures in the same minute do not overwrite the earlier session archive. Thanks @vincentkoc. - Video generation: wait up to 20 minutes for slow fal/MiniMax queue-backed jobs, stop forwarding unsupported Google Veo generated-audio options, and normalize MiniMax `720P` requests to its supported `768P` resolution with the usual override warning/details instead of failing fallback. - Video generation: accept provider-specific aspect-ratio and resolution hints at the tool boundary, normalize `720P` to MiniMax's supported `768P`, and stop sending Google `generateAudio` on Gemini video requests so provider fallback can recover from model-specific parameter differences. Thanks @vincentkoc. - OpenAI/Google Meet: fail realtime voice connection attempts when the socket closes before `session.updated`, avoiding stuck Meet joins waiting on a bridge that never became ready. Thanks @vincentkoc. diff --git a/src/hooks/bundled/session-memory/handler.test.ts b/src/hooks/bundled/session-memory/handler.test.ts index d824d5c54cf..69afdd8d44d 100644 --- a/src/hooks/bundled/session-memory/handler.test.ts +++ b/src/hooks/bundled/session-memory/handler.test.ts @@ -413,6 +413,41 @@ describe("session-memory hook", () => { }); }); + it("keeps same-minute fallback timestamp captures by adding a filename suffix", async () => { + await withEnvAsync({ TZ: "UTC" }, async () => { + const tempDir = await createCaseWorkspace("workspace"); + const timestamp = new Date("2026-01-01T04:30:15.000Z"); + + await runNewWithPreviousSessionEntry({ + tempDir, + timestamp, + previousSessionEntry: { + sessionId: "first-session", + }, + }); + await runNewWithPreviousSessionEntry({ + tempDir, + timestamp, + previousSessionEntry: { + sessionId: "second-session", + }, + }); + + const memoryDir = path.join(tempDir, "memory"); + const files = await fs.readdir(memoryDir); + expect(files).toHaveLength(2); + expect(files).toContain("2026-01-01-0430.md"); + expect(files).toContain("2026-01-01-0430-2.md"); + + await expect( + fs.readFile(path.join(memoryDir, "2026-01-01-0430.md"), "utf-8"), + ).resolves.toContain("- **Session ID**: first-session"); + await expect( + fs.readFile(path.join(memoryDir, "2026-01-01-0430-2.md"), "utf-8"), + ).resolves.toContain("- **Session ID**: second-session"); + }); + }); + it("prefers workspaceDir from hook context when sessionKey points at main", async () => { const mainWorkspace = await createCaseWorkspace("workspace-main"); const naviWorkspace = await createCaseWorkspace("workspace-navi"); diff --git a/src/hooks/bundled/session-memory/handler.ts b/src/hooks/bundled/session-memory/handler.ts index 3d0de6ec652..3d6847f4371 100644 --- a/src/hooks/bundled/session-memory/handler.ts +++ b/src/hooks/bundled/session-memory/handler.ts @@ -85,6 +85,28 @@ function formatLocalSessionTimestamp(date: Date): { }; } +async function resolveAvailableMemoryFilename(params: { + memoryDir: string; + dateStr: string; + slug: string; +}): Promise { + const basename = `${params.dateStr}-${params.slug}`; + let suffix = 1; + + while (true) { + const filename = suffix === 1 ? `${basename}.md` : `${basename}-${suffix}.md`; + try { + await fs.access(path.join(params.memoryDir, filename)); + suffix += 1; + } catch (err) { + if ((err as { code?: string }).code === "ENOENT") { + return filename; + } + throw err; + } + } +} + function resolveDisplaySessionKey(params: { cfg?: OpenClawConfig; workspaceDir?: string; @@ -223,7 +245,7 @@ async function saveSessionMemoryNow(event: Parameters[0]): Promise< } // Create filename with date and slug - const filename = `${dateStr}-${slug}.md`; + const filename = await resolveAvailableMemoryFilename({ memoryDir, dateStr, slug }); const memoryFilePath = path.join(memoryDir, filename); log.debug("Memory file path resolved", { filename,