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:
Alex Newman
2026-02-09 21:04:52 -05:00
parent 11532a36fb
commit c7f7f87321
2 changed files with 82 additions and 45 deletions
+38 -31
View File
@@ -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
View File
@@ -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}`);
});
// ------------------------------------------------------------------