From 810d614cb877bba06aefdaa2260cea6fc1ca29ba Mon Sep 17 00:00:00 2001 From: Altay Date: Sun, 8 Mar 2026 18:32:53 +0300 Subject: [PATCH] fix(hooks): prefer reset workspace in session-memory --- src/auto-reply/reply/commands-core.ts | 1 + src/auto-reply/reply/commands.test.ts | 3 + .../bundled/session-memory/handler.test.ts | 63 ++++++++++++++++--- src/hooks/bundled/session-memory/handler.ts | 12 +++- 4 files changed, 68 insertions(+), 11 deletions(-) diff --git a/src/auto-reply/reply/commands-core.ts b/src/auto-reply/reply/commands-core.ts index 6548ea40afd..894724bcfb0 100644 --- a/src/auto-reply/reply/commands-core.ts +++ b/src/auto-reply/reply/commands-core.ts @@ -63,6 +63,7 @@ export async function emitResetCommandHooks(params: { previousSessionEntry: params.previousSessionEntry, commandSource: params.command.surface, senderId: params.command.senderId, + workspaceDir: params.workspaceDir, cfg: params.cfg, // Pass config for LLM slug generation }); await triggerInternalHook(hookEvent); diff --git a/src/auto-reply/reply/commands.test.ts b/src/auto-reply/reply/commands.test.ts index 2c05690ebd0..38be7c43531 100644 --- a/src/auto-reply/reply/commands.test.ts +++ b/src/auto-reply/reply/commands.test.ts @@ -1138,6 +1138,9 @@ describe("handleCommands hooks", () => { type: "command", action: "new", sessionKey: "agent:main:telegram:direct:123", + context: expect.objectContaining({ + workspaceDir: testWorkspaceDir, + }), }), ); spy.mockRestore(); diff --git a/src/hooks/bundled/session-memory/handler.test.ts b/src/hooks/bundled/session-memory/handler.test.ts index 7f29c58b128..dbd68045d63 100644 --- a/src/hooks/bundled/session-memory/handler.test.ts +++ b/src/hooks/bundled/session-memory/handler.test.ts @@ -65,15 +65,23 @@ async function runNewWithPreviousSessionEntry(params: { previousSessionEntry: { sessionId: string; sessionFile?: string }; cfg?: OpenClawConfig; action?: "new" | "reset"; + sessionKey?: string; + workspaceDirOverride?: string; }): Promise<{ files: string[]; memoryContent: string }> { - const event = createHookEvent("command", params.action ?? "new", "agent:main:main", { - cfg: - params.cfg ?? - ({ - agents: { defaults: { workspace: params.tempDir } }, - } satisfies OpenClawConfig), - previousSessionEntry: params.previousSessionEntry, - }); + const event = createHookEvent( + "command", + params.action ?? "new", + params.sessionKey ?? "agent:main:main", + { + cfg: + params.cfg ?? + ({ + agents: { defaults: { workspace: params.tempDir } }, + } satisfies OpenClawConfig), + previousSessionEntry: params.previousSessionEntry, + ...(params.workspaceDirOverride ? { workspaceDir: params.workspaceDirOverride } : {}), + }, + ); await handler(event); @@ -242,6 +250,45 @@ describe("session-memory hook", () => { expect(memoryContent).toContain("assistant: Captured before reset"); }); + it("prefers workspaceDir from hook context when sessionKey points at main", async () => { + const mainWorkspace = await createCaseWorkspace("workspace-main"); + const naviWorkspace = await createCaseWorkspace("workspace-navi"); + const naviSessionsDir = path.join(naviWorkspace, "sessions"); + await fs.mkdir(naviSessionsDir, { recursive: true }); + + const sessionFile = await writeWorkspaceFile({ + dir: naviSessionsDir, + name: "navi-session.jsonl", + content: createMockSessionContent([ + { role: "user", content: "Remember this under Navi" }, + { role: "assistant", content: "Stored in the bound workspace" }, + ]), + }); + + const { files, memoryContent } = await runNewWithPreviousSessionEntry({ + tempDir: naviWorkspace, + cfg: { + agents: { + defaults: { workspace: mainWorkspace }, + list: { + navi: { workspace: naviWorkspace }, + }, + }, + } satisfies OpenClawConfig, + sessionKey: "agent:main:main", + workspaceDirOverride: naviWorkspace, + previousSessionEntry: { + sessionId: "navi-session", + sessionFile, + }, + }); + + expect(files.length).toBe(1); + expect(memoryContent).toContain("user: Remember this under Navi"); + expect(memoryContent).toContain("assistant: Stored in the bound workspace"); + await expect(fs.access(path.join(mainWorkspace, "memory"))).rejects.toThrow(); + }); + it("filters out non-message entries (tool calls, system)", async () => { // Create session with mixed entry types const sessionContent = createMockSessionContent([ diff --git a/src/hooks/bundled/session-memory/handler.ts b/src/hooks/bundled/session-memory/handler.ts index 79bfa1cf329..d068b64ba4b 100644 --- a/src/hooks/bundled/session-memory/handler.ts +++ b/src/hooks/bundled/session-memory/handler.ts @@ -182,10 +182,16 @@ const saveSessionToMemory: HookHandler = async (event) => { const context = event.context || {}; const cfg = context.cfg as OpenClawConfig | undefined; + const contextWorkspaceDir = + typeof context.workspaceDir === "string" && context.workspaceDir.trim().length > 0 + ? context.workspaceDir + : undefined; const agentId = resolveAgentIdFromSessionKey(event.sessionKey); - const workspaceDir = cfg - ? resolveAgentWorkspaceDir(cfg, agentId) - : path.join(resolveStateDir(process.env, os.homedir), "workspace"); + const workspaceDir = + contextWorkspaceDir || + (cfg + ? resolveAgentWorkspaceDir(cfg, agentId) + : path.join(resolveStateDir(process.env, os.homedir), "workspace")); const memoryDir = path.join(workspaceDir, "memory"); await fs.mkdir(memoryDir, { recursive: true });