fix(session-memory): ensure memoryDir exists in post-hook write path

When blockSessionSave is true initially, the inline write is skipped —
including the fs.mkdir that creates memoryDir. If a later hook clears
blockSessionSave and sets sessionSaveContent, the post-hook
writeFileWithinRoot call would fail with ENOENT because the directory
was never created. The error was silently swallowed, causing the content
override to be lost.

Add fs.mkdir(memoryDir, { recursive: true }) before the post-hook write.
Add regression test for the block-then-clear-with-content scenario.
This commit is contained in:
zeroaltitude 2026-03-07 10:09:43 -07:00
parent 64d52cf2ea
commit 4040a25bb1
No known key found for this signature in database
GPG Key ID: 77592FB1C703882E
2 changed files with 47 additions and 0 deletions

View File

@ -747,6 +747,49 @@ describe("session-memory hook", () => {
expect(content).toContain("important data");
});
it("blockSessionSave pre-set then cleared with sessionSaveContent creates file (mkdir edge case)", async () => {
// Regression: when blockSessionSave is true initially, the inline write
// is skipped — including the fs.mkdir. If a later hook clears the flag
// and sets sessionSaveContent, the post-hook write must create the
// directory itself or it fails with ENOENT.
const tempDir = await createCaseWorkspace("block-then-clear");
const sessionsDir = path.join(tempDir, "sessions");
await fs.mkdir(sessionsDir, { recursive: true });
const sessionFile = await writeWorkspaceFile({
dir: sessionsDir,
name: "test-session.jsonl",
content: createMockSessionContent([{ role: "user", content: "secret" }]),
});
const event = createHookEvent("command", "new", "agent:main:main", {
cfg: { agents: { defaults: { workspace: tempDir } } } satisfies OpenClawConfig,
previousSessionEntry: { sessionId: "s1", sessionFile },
});
event.context.blockSessionSave = true;
// Handler runs — inline write is skipped, memoryDir never created
await handler(event);
const memoryDir = path.join(tempDir, "memory");
const existsBefore = await fs
.stat(memoryDir)
.then(() => true)
.catch(() => false);
expect(existsBefore).toBe(false);
// A later hook clears blockSessionSave and sets custom content
event.context.blockSessionSave = false;
event.context.sessionSaveContent = "Replacement content from policy hook";
// Post-hook should create the directory and write the file
await drainPostHookActions(event);
const files = (await fs.readdir(memoryDir)).filter((f) => f.endsWith(".md"));
expect(files.length).toBeGreaterThan(0);
const content = await fs.readFile(path.join(memoryDir, files[0]), "utf-8");
expect(content).toBe("Replacement content from policy hook");
});
it("blockSessionSave takes precedence over sessionSaveContent (both pre-set)", async () => {
const tempDir = await createCaseWorkspace("block-beats-content-pre");
const sessionsDir = path.join(tempDir, "sessions");

View File

@ -414,6 +414,10 @@ const saveSessionToMemory: HookHandler = async (event) => {
typeof postContent === "string" &&
postContent !== writtenEntry
) {
// Ensure memoryDir exists — the inline write may have been
// skipped (e.g. blockSessionSave was true initially) so mkdir
// might never have run.
await fs.mkdir(memoryDir, { recursive: true });
await writeFileWithinRoot({
rootDir: memoryDir,
relativePath: filename,