diff --git a/openclaw/src/index.test.ts b/openclaw/src/index.test.ts index d62f5d4c..da2ff908 100644 --- a/openclaw/src/index.test.ts +++ b/openclaw/src/index.test.ts @@ -103,6 +103,8 @@ describe("claudeMemPlugin", () => { assert.equal(getService().id, "claude-mem-observation-feed"); assert.ok(getCommand("claude-mem-feed"), "feed command should be registered"); assert.ok(getCommand("claude-mem-status"), "status command should be registered"); + assert.ok(getEventHandlers("session_start").length > 0, "session_start handler registered"); + assert.ok(getEventHandlers("after_compaction").length > 0, "after_compaction handler registered"); assert.ok(getEventHandlers("before_agent_start").length > 0, "before_agent_start handler registered"); assert.ok(getEventHandlers("tool_result_persist").length > 0, "tool_result_persist handler registered"); assert.ok(getEventHandlers("agent_end").length > 0, "agent_end handler registered"); @@ -304,12 +306,12 @@ describe("Observation I/O event handlers", () => { workerServer?.close(); }); - it("before_agent_start sends session init to worker", async () => { + it("session_start sends session init to worker", async () => { const { api, logs, fireEvent } = createMockApi({ workerPort }); claudeMemPlugin(api); - await fireEvent("before_agent_start", { - prompt: "Help me write a function that parses JSON", + await fireEvent("session_start", { + sessionId: "test-session-1", }, { sessionKey: "agent-1" }); // Wait for HTTP request @@ -319,31 +321,48 @@ describe("Observation I/O event handlers", () => { assert.ok(initRequest, "should send init request to worker"); assert.equal(initRequest!.body.project, "openclaw"); assert.ok(initRequest!.body.contentSessionId.startsWith("openclaw-agent-1-")); - assert.equal(initRequest!.body.prompt, "Help me write a function that parses JSON"); assert.ok(logs.some((l) => l.includes("Session initialized"))); }); - it("before_agent_start skips short prompts", async () => { + it("session_start calls init on worker", async () => { const { api, fireEvent } = createMockApi({ workerPort }); claudeMemPlugin(api); - await fireEvent("before_agent_start", { prompt: "hi" }, {}); - + await fireEvent("session_start", { sessionId: "test-session-1" }, {}); await new Promise((resolve) => setTimeout(resolve, 100)); - const initRequest = receivedRequests.find((r) => r.url === "/api/sessions/init"); - assert.ok(!initRequest, "should not send init for short prompts"); + const initRequests = receivedRequests.filter((r) => r.url === "/api/sessions/init"); + assert.equal(initRequests.length, 1, "should init on session_start"); + }); + + it("after_compaction re-inits session on worker", async () => { + const { api, fireEvent } = createMockApi({ workerPort }); + claudeMemPlugin(api); + + await fireEvent("after_compaction", { messageCount: 5, compactedCount: 3 }, {}); + await new Promise((resolve) => setTimeout(resolve, 100)); + + const initRequests = receivedRequests.filter((r) => r.url === "/api/sessions/init"); + assert.equal(initRequests.length, 1, "should re-init after compaction"); + }); + + it("before_agent_start does not call init", async () => { + const { api, fireEvent } = createMockApi({ workerPort }); + claudeMemPlugin(api); + + await fireEvent("before_agent_start", { prompt: "hello" }, {}); + await new Promise((resolve) => setTimeout(resolve, 100)); + + const initRequests = receivedRequests.filter((r) => r.url === "/api/sessions/init"); + assert.equal(initRequests.length, 0, "before_agent_start should not init"); }); it("tool_result_persist sends observation to worker", async () => { const { api, fireEvent } = createMockApi({ workerPort }); claudeMemPlugin(api); - // Init session first to establish contentSessionId - await fireEvent("before_agent_start", { - prompt: "Help me write a function that parses JSON", - }, { sessionKey: "test-agent" }); - + // Establish contentSessionId via session_start + await fireEvent("session_start", { sessionId: "s1" }, { sessionKey: "test-agent" }); await new Promise((resolve) => setTimeout(resolve, 100)); // Fire tool result event @@ -404,11 +423,8 @@ describe("Observation I/O event handlers", () => { const { api, fireEvent } = createMockApi({ workerPort }); claudeMemPlugin(api); - // Init session - await fireEvent("before_agent_start", { - prompt: "Help me write a function that parses JSON", - }, { sessionKey: "summarize-test" }); - + // Establish session + await fireEvent("session_start", { sessionId: "s1" }, { sessionKey: "summarize-test" }); await new Promise((resolve) => setTimeout(resolve, 100)); // Fire agent end @@ -435,10 +451,7 @@ describe("Observation I/O event handlers", () => { const { api, fireEvent } = createMockApi({ workerPort }); claudeMemPlugin(api); - await fireEvent("before_agent_start", { - prompt: "Help me write a function that parses JSON", - }, { sessionKey: "array-content" }); - + await fireEvent("session_start", { sessionId: "s1" }, { sessionKey: "array-content" }); await new Promise((resolve) => setTimeout(resolve, 100)); await fireEvent("agent_end", { @@ -464,10 +477,7 @@ describe("Observation I/O event handlers", () => { const { api, fireEvent } = createMockApi({ workerPort, project: "my-project" }); claudeMemPlugin(api); - await fireEvent("before_agent_start", { - prompt: "Help me write a function that parses JSON", - }, {}); - + await fireEvent("session_start", { sessionId: "s1" }, {}); await new Promise((resolve) => setTimeout(resolve, 100)); const initRequest = receivedRequests.find((r) => r.url === "/api/sessions/init"); @@ -503,10 +513,7 @@ describe("Observation I/O event handlers", () => { const { api, fireEvent } = createMockApi({ workerPort }); claudeMemPlugin(api); - await fireEvent("before_agent_start", { - prompt: "Help me write a function that parses JSON", - }, { sessionKey: "reuse-test" }); - + await fireEvent("session_start", { sessionId: "s1" }, { sessionKey: "reuse-test" }); await new Promise((resolve) => setTimeout(resolve, 100)); await fireEvent("tool_result_persist", { diff --git a/openclaw/src/index.ts b/openclaw/src/index.ts index 1a088af9..426085ea 100644 --- a/openclaw/src/index.ts +++ b/openclaw/src/index.ts @@ -50,6 +50,17 @@ interface AgentEndEvent { }>; } +interface SessionStartEvent { + sessionId: string; + resumedFrom?: string; +} + +interface AfterCompactionEvent { + messageCount: number; + tokenCount?: number; + compactedCount: number; +} + interface EventContext { sessionKey?: string; workspaceDir?: string; @@ -81,6 +92,8 @@ interface OpenClawPluginApi { on: ((event: "before_agent_start", callback: EventCallback) => void) & ((event: "tool_result_persist", callback: EventCallback) => void) & ((event: "agent_end", callback: EventCallback) => void) & + ((event: "session_start", callback: EventCallback) => void) & + ((event: "after_compaction", callback: EventCallback) => void) & ((event: "gateway_start", callback: EventCallback>) => void); runtime: { channel: Record Promise>>; @@ -400,31 +413,48 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void { } // ------------------------------------------------------------------ - // Event: before_agent_start — init session + sync MEMORY.md + // Event: session_start — init claude-mem session (fires on /new, /reset) // ------------------------------------------------------------------ - api.on("before_agent_start", async (event, ctx) => { + api.on("session_start", async (_event, ctx) => { const contentSessionId = getContentSessionId(ctx.sessionKey); - const prompt = event.prompt || ""; + await workerPost(workerPort, "/api/sessions/init", { + contentSessionId, + project: projectName, + prompt: "", + }, api.logger); + + api.logger.info(`[claude-mem] Session initialized: ${contentSessionId}`); + }); + + // ------------------------------------------------------------------ + // Event: after_compaction — re-init session after context compaction + // ------------------------------------------------------------------ + api.on("after_compaction", async (_event, ctx) => { + const contentSessionId = getContentSessionId(ctx.sessionKey); + + await workerPost(workerPort, "/api/sessions/init", { + contentSessionId, + project: projectName, + prompt: "", + }, api.logger); + + api.logger.info(`[claude-mem] Session re-initialized after compaction: ${contentSessionId}`); + }); + + // ------------------------------------------------------------------ + // Event: before_agent_start — sync MEMORY.md + track workspace + // ------------------------------------------------------------------ + api.on("before_agent_start", async (_event, ctx) => { // Track workspace dir so tool_result_persist can sync MEMORY.md later if (ctx.workspaceDir) { workspaceDirsBySessionKey.set(ctx.sessionKey || "default", ctx.workspaceDir); } - // Sync MEMORY.md before session init (provides context to agent) + // Sync MEMORY.md before agent runs (provides context to agent) if (syncMemoryFile && ctx.workspaceDir) { await syncMemoryToWorkspace(ctx.workspaceDir); } - - if (prompt.length < 10) return; - - await workerPost(workerPort, "/api/sessions/init", { - contentSessionId, - project: projectName, - prompt, - }, api.logger); - - api.logger.info(`[claude-mem] Session initialized: ${contentSessionId}`); }); // ------------------------------------------------------------------