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" },