From 575202b06e93874a9aad80580357723c5c72eefb Mon Sep 17 00:00:00 2001 From: Subash Natarajan Date: Tue, 14 Apr 2026 06:49:07 +0530 Subject: [PATCH] fix(hooks): pass workspaceDir in gateway session reset internal hook context (#64735) * fix(hooks): pass workspaceDir in gateway session reset internal hook context The gateway path (performGatewaySessionReset) omitted workspaceDir when creating the internal hook event, while the plugin hook path (emitGatewayBeforeResetPluginHook) in the same file correctly resolved and passed it. This caused the session-memory handler to fall back to resolveAgentWorkspaceDir from the session key, which for default-agent keys resolves to the shared default workspace instead of the per-agent workspace. Daily notes and memory files were written to the wrong workspace in multi-agent setups. Closes #64528 * docs(changelog): add session-memory workspace reset note * fix(changelog): remove conflict markers --------- Co-authored-by: Vincent Koc --- CHANGELOG.md | 1 + src/gateway/session-reset-service.ts | 3 ++ .../bundled/session-memory/handler.test.ts | 43 +++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c69d6e89f86..559c09308ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ Docs: https://docs.openclaw.ai - Agents/OpenAI: recover embedded GPT-style runs when reasoning-only or empty turns need bounded continuation, with replay-safe retry gating and incomplete-turn fallback when no visible answer arrives. (#66167) thanks @jalehman - Outbound/relay-status: suppress internal relay-status placeholder payloads (`No channel reply.`, `Replied in-thread.`, `Replied in #...`, wiki-update status variants ending in `No channel reply.`) before channel delivery so internal housekeeping text does not leak to users. - Slack/doctor: add a dedicated doctor-contract sidecar so config warmup paths such as `openclaw cron` no longer fall back to Slack's broader contract surface, which could trigger Slack-related config-read crashes on affected setups. (#63192) Thanks @shhtheonlyperson. +- Hooks/session-memory: pass the resolved agent workspace into gateway `/new` and `/reset` session-memory hooks so reset snapshots stay scoped to the right agent workspace instead of leaking into the default workspace. (#64735) Thanks @suboss87 and @vincentkoc. ## 2026.4.12 diff --git a/src/gateway/session-reset-service.ts b/src/gateway/session-reset-service.ts index bbf98dc2b75..c7bdbff9625 100644 --- a/src/gateway/session-reset-service.ts +++ b/src/gateway/session-reset-service.ts @@ -514,6 +514,8 @@ export async function performGatewaySessionReset(params: { })(); const { entry, legacyKey, canonicalKey } = loadSessionEntry(params.key); const hadExistingEntry = Boolean(entry); + const agentId = normalizeAgentId(target.agentId ?? resolveDefaultAgentId(cfg)); + const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId); const hookEvent = createInternalHookEvent( "command", params.reason, @@ -523,6 +525,7 @@ export async function performGatewaySessionReset(params: { previousSessionEntry: entry, commandSource: params.commandSource, cfg, + workspaceDir, }, ); await triggerInternalHook(hookEvent); diff --git a/src/hooks/bundled/session-memory/handler.test.ts b/src/hooks/bundled/session-memory/handler.test.ts index 13ca735601e..36dcb5438a2 100644 --- a/src/hooks/bundled/session-memory/handler.test.ts +++ b/src/hooks/bundled/session-memory/handler.test.ts @@ -533,6 +533,49 @@ describe("session-memory hook", () => { expect(files.length).toBe(1); }); + it("uses agent-specific workspace when workspaceDir is provided for non-default agent (gateway path regression)", async () => { + const defaultWorkspace = await createCaseWorkspace("workspace-default"); + const customAgentWorkspace = await createCaseWorkspace("workspace-custom-agent"); + const sessionsDir = path.join(customAgentWorkspace, "sessions"); + await fs.mkdir(sessionsDir, { recursive: true }); + + const sessionFile = await writeWorkspaceFile({ + dir: sessionsDir, + name: "custom-agent-session.jsonl", + content: createMockSessionContent([ + { role: "user", content: "Custom agent conversation" }, + { role: "assistant", content: "Stored in agent workspace" }, + ]), + }); + + // Simulate the gateway internal hook path: workspaceDir is resolved and + // passed explicitly in context (fix for #64528). Without the fix, the + // gateway path omitted workspaceDir, causing the handler to fall back to + // the default workspace via resolveAgentWorkspaceDir — which for a + // default-agent sessionKey would resolve to the shared default workspace. + const { files, memoryContent } = await runNewWithPreviousSessionEntry({ + tempDir: customAgentWorkspace, + cfg: { + agents: { + defaults: { workspace: defaultWorkspace }, + list: [{ id: "custom-agent", workspace: customAgentWorkspace }], + }, + } satisfies OpenClawConfig, + sessionKey: "agent:main:main", + workspaceDirOverride: customAgentWorkspace, + previousSessionEntry: { + sessionId: "custom-agent-session", + sessionFile, + }, + }); + + expect(files.length).toBe(1); + expect(memoryContent).toContain("user: Custom agent conversation"); + expect(memoryContent).toContain("assistant: Stored in agent workspace"); + // Verify memory did NOT leak to the default workspace + await expect(fs.access(path.join(defaultWorkspace, "memory"))).rejects.toThrow(); + }); + it("handles session files with fewer messages than requested", async () => { const sessionContent = createMockSessionContent([ { role: "user", content: "Only message 1" },