Skip to content

Commit 61383af

Browse files
authored
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.
1 parent f3d5314 commit 61383af

3 files changed

Lines changed: 59 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ Docs: https://docs.openclaw.ai
7373
- 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.
7474
- 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.
7575
- WhatsApp responsiveness: stop only verified stale local TUI clients when they degrade the Gateway event loop and delay replies. Thanks @vincentkoc.
76+
- 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.
7677
- 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.
7778
- 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.
7879
- 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.

src/hooks/bundled/session-memory/handler.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,41 @@ describe("session-memory hook", () => {
413413
});
414414
});
415415

416+
it("keeps same-minute fallback timestamp captures by adding a filename suffix", async () => {
417+
await withEnvAsync({ TZ: "UTC" }, async () => {
418+
const tempDir = await createCaseWorkspace("workspace");
419+
const timestamp = new Date("2026-01-01T04:30:15.000Z");
420+
421+
await runNewWithPreviousSessionEntry({
422+
tempDir,
423+
timestamp,
424+
previousSessionEntry: {
425+
sessionId: "first-session",
426+
},
427+
});
428+
await runNewWithPreviousSessionEntry({
429+
tempDir,
430+
timestamp,
431+
previousSessionEntry: {
432+
sessionId: "second-session",
433+
},
434+
});
435+
436+
const memoryDir = path.join(tempDir, "memory");
437+
const files = await fs.readdir(memoryDir);
438+
expect(files).toHaveLength(2);
439+
expect(files).toContain("2026-01-01-0430.md");
440+
expect(files).toContain("2026-01-01-0430-2.md");
441+
442+
await expect(
443+
fs.readFile(path.join(memoryDir, "2026-01-01-0430.md"), "utf-8"),
444+
).resolves.toContain("- **Session ID**: first-session");
445+
await expect(
446+
fs.readFile(path.join(memoryDir, "2026-01-01-0430-2.md"), "utf-8"),
447+
).resolves.toContain("- **Session ID**: second-session");
448+
});
449+
});
450+
416451
it("prefers workspaceDir from hook context when sessionKey points at main", async () => {
417452
const mainWorkspace = await createCaseWorkspace("workspace-main");
418453
const naviWorkspace = await createCaseWorkspace("workspace-navi");

src/hooks/bundled/session-memory/handler.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,28 @@ function formatLocalSessionTimestamp(date: Date): {
8585
};
8686
}
8787

88+
async function resolveAvailableMemoryFilename(params: {
89+
memoryDir: string;
90+
dateStr: string;
91+
slug: string;
92+
}): Promise<string> {
93+
const basename = `${params.dateStr}-${params.slug}`;
94+
let suffix = 1;
95+
96+
while (true) {
97+
const filename = suffix === 1 ? `${basename}.md` : `${basename}-${suffix}.md`;
98+
try {
99+
await fs.access(path.join(params.memoryDir, filename));
100+
suffix += 1;
101+
} catch (err) {
102+
if ((err as { code?: string }).code === "ENOENT") {
103+
return filename;
104+
}
105+
throw err;
106+
}
107+
}
108+
}
109+
88110
function resolveDisplaySessionKey(params: {
89111
cfg?: OpenClawConfig;
90112
workspaceDir?: string;
@@ -223,7 +245,7 @@ async function saveSessionMemoryNow(event: Parameters<HookHandler>[0]): Promise<
223245
}
224246

225247
// Create filename with date and slug
226-
const filename = `${dateStr}-${slug}.md`;
248+
const filename = await resolveAvailableMemoryFilename({ memoryDir, dateStr, slug });
227249
const memoryFilePath = path.join(memoryDir, filename);
228250
log.debug("Memory file path resolved", {
229251
filename,

0 commit comments

Comments
 (0)