From a81bd0a2de8baa69f85f75e3c72435ea30d350e3 Mon Sep 17 00:00:00 2001 From: MackDing Date: Fri, 22 May 2026 10:04:57 +0800 Subject: [PATCH 1/2] fix(openclaw): scope recall and capture by project --- integrations/openclaw/plugin.mjs | 28 +++++++++ test/openclaw-project-scope.test.ts | 97 +++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 test/openclaw-project-scope.test.ts diff --git a/integrations/openclaw/plugin.mjs b/integrations/openclaw/plugin.mjs index 332189d7e..48b539bdb 100644 --- a/integrations/openclaw/plugin.mjs +++ b/integrations/openclaw/plugin.mjs @@ -60,6 +60,29 @@ function latestUserText(messages) { return ""; } +function asNonEmptyString(value) { + return typeof value === "string" && value.trim() ? value.trim() : ""; +} + +function deriveProject(event) { + const candidates = [ + event?.project, + event?.session?.project, + event?.workspace?.project, + event?.agent?.project, + event?.metadata?.project, + event?.context?.project, + event?.cwd, + event?.session?.cwd, + event?.workspace?.cwd, + ]; + for (const value of candidates) { + const text = asNonEmptyString(value); + if (text) return text; + } + return ""; +} + function formatResults(results) { if (!Array.isArray(results) || results.length === 0) return ""; return results @@ -179,9 +202,11 @@ const plugin = { if (!cfg.enabled) return; const prompt = typeof event?.prompt === "string" ? event.prompt.trim() : ""; if (!prompt) return; + const project = deriveProject(event); const result = await client.postJson("/agentmemory/smart-search", { query: prompt, limit: 5, + ...(project ? { project } : {}), }); const block = formatResults(result?.results || []); if (!block) return; @@ -200,9 +225,12 @@ const plugin = { event.sessionKey || event.runId || `openclaw-${Date.now()}`; + const project = deriveProject(event); await client.postJson("/agentmemory/observe", { hookType: "post_tool_use", sessionId, + project: project || sessionId, + cwd: project || sessionId, timestamp: new Date().toISOString(), data: { tool_name: "conversation", diff --git a/test/openclaw-project-scope.test.ts b/test/openclaw-project-scope.test.ts new file mode 100644 index 000000000..2976fe0bb --- /dev/null +++ b/test/openclaw-project-scope.test.ts @@ -0,0 +1,97 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import openclawPlugin from "../integrations/openclaw/plugin.mjs"; + +type OpenClawHandler = (event: Record) => Promise; + +function mockFetch(calls: Array<{ url: string; body: any }>) { + const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { + const body = init?.body ? JSON.parse(String(init.body)) : undefined; + calls.push({ url: String(input), body }); + return new Response(JSON.stringify({ results: [] }), { + status: 200, + headers: { "content-type": "application/json" }, + }); + }); + (globalThis as { fetch: typeof fetch }).fetch = fetchMock as unknown as typeof fetch; + return fetchMock; +} + +function registerPlugin() { + const handlers = new Map(); + openclawPlugin.register({ + pluginConfig: { base_url: "http://localhost:3111" }, + logger: { warn: vi.fn() }, + on(event: string, handler: OpenClawHandler) { + handlers.set(event, handler); + }, + }); + return handlers; +} + +describe("OpenClaw plugin project scoping", () => { + const originalFetch = globalThis.fetch; + + beforeEach(() => { + delete process.env["AGENTMEMORY_SECRET"]; + }); + + afterEach(() => { + globalThis.fetch = originalFetch; + }); + + it("passes event project into smart-search when available", async () => { + const calls: Array<{ url: string; body: any }> = []; + mockFetch(calls); + const handlers = registerPlugin(); + + await handlers.get("before_agent_start")?.({ + prompt: "recall auth changes", + project: "team-alpha", + }); + + expect(calls).toHaveLength(1); + expect(calls[0].url).toContain("/agentmemory/smart-search"); + expect(calls[0].body).toMatchObject({ + query: "recall auth changes", + limit: 5, + project: "team-alpha", + }); + }); + + it("passes project and cwd into observe payload, falling back to session id when missing", async () => { + const calls: Array<{ url: string; body: any }> = []; + mockFetch(calls); + const handlers = registerPlugin(); + + await handlers.get("agent_end")?.({ + success: true, + sessionKey: "sess-123", + messages: [ + { role: "user", content: "hello" }, + { role: "assistant", content: "world" }, + ], + }); + + expect(calls).toHaveLength(1); + expect(calls[0].url).toContain("/agentmemory/observe"); + expect(calls[0].body).toMatchObject({ + hookType: "post_tool_use", + sessionId: "sess-123", + project: "sess-123", + cwd: "sess-123", + }); + }); + + it("uses nested workspace project metadata when top-level project is absent", async () => { + const calls: Array<{ url: string; body: any }> = []; + mockFetch(calls); + const handlers = registerPlugin(); + + await handlers.get("before_agent_start")?.({ + prompt: "open memory", + workspace: { project: "nested-proj" }, + }); + + expect(calls[0].body).toMatchObject({ project: "nested-proj" }); + }); +}); From 0c32986412c69bd20741806898a74cfc77e86992 Mon Sep 17 00:00:00 2001 From: Blossom Date: Tue, 26 May 2026 10:55:09 -0400 Subject: [PATCH 2/2] test: restore AGENTMEMORY_SECRET after OpenClaw tests --- test/openclaw-project-scope.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/openclaw-project-scope.test.ts b/test/openclaw-project-scope.test.ts index 2976fe0bb..92ca01942 100644 --- a/test/openclaw-project-scope.test.ts +++ b/test/openclaw-project-scope.test.ts @@ -30,6 +30,7 @@ function registerPlugin() { describe("OpenClaw plugin project scoping", () => { const originalFetch = globalThis.fetch; + const originalAgentMemorySecret = process.env["AGENTMEMORY_SECRET"]; beforeEach(() => { delete process.env["AGENTMEMORY_SECRET"]; @@ -37,6 +38,11 @@ describe("OpenClaw plugin project scoping", () => { afterEach(() => { globalThis.fetch = originalFetch; + if (originalAgentMemorySecret === undefined) { + delete process.env["AGENTMEMORY_SECRET"]; + } else { + process.env["AGENTMEMORY_SECRET"] = originalAgentMemorySecret; + } }); it("passes event project into smart-search when available", async () => {