Move session init to session_start and after_compaction hooks
Init was incorrectly placed in before_agent_start, which fires on every agent attempt (retries, context overflow, auth rotation). Session init should fire once on /new or /reset (session_start) and after compaction (after_compaction). before_agent_start now only syncs MEMORY.md and tracks workspace dirs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+38
-31
@@ -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", {
|
||||
|
||||
+44
-14
@@ -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<BeforeAgentStartEvent>) => void) &
|
||||
((event: "tool_result_persist", callback: EventCallback<ToolResultPersistEvent>) => void) &
|
||||
((event: "agent_end", callback: EventCallback<AgentEndEvent>) => void) &
|
||||
((event: "session_start", callback: EventCallback<SessionStartEvent>) => void) &
|
||||
((event: "after_compaction", callback: EventCallback<AfterCompactionEvent>) => void) &
|
||||
((event: "gateway_start", callback: EventCallback<Record<string, never>>) => void);
|
||||
runtime: {
|
||||
channel: Record<string, Record<string, (...args: any[]) => Promise<any>>>;
|
||||
@@ -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}`);
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user