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.equal(getService().id, "claude-mem-observation-feed");
|
||||||
assert.ok(getCommand("claude-mem-feed"), "feed command should be registered");
|
assert.ok(getCommand("claude-mem-feed"), "feed command should be registered");
|
||||||
assert.ok(getCommand("claude-mem-status"), "status 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("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("tool_result_persist").length > 0, "tool_result_persist handler registered");
|
||||||
assert.ok(getEventHandlers("agent_end").length > 0, "agent_end 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();
|
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 });
|
const { api, logs, fireEvent } = createMockApi({ workerPort });
|
||||||
claudeMemPlugin(api);
|
claudeMemPlugin(api);
|
||||||
|
|
||||||
await fireEvent("before_agent_start", {
|
await fireEvent("session_start", {
|
||||||
prompt: "Help me write a function that parses JSON",
|
sessionId: "test-session-1",
|
||||||
}, { sessionKey: "agent-1" });
|
}, { sessionKey: "agent-1" });
|
||||||
|
|
||||||
// Wait for HTTP request
|
// Wait for HTTP request
|
||||||
@@ -319,31 +321,48 @@ describe("Observation I/O event handlers", () => {
|
|||||||
assert.ok(initRequest, "should send init request to worker");
|
assert.ok(initRequest, "should send init request to worker");
|
||||||
assert.equal(initRequest!.body.project, "openclaw");
|
assert.equal(initRequest!.body.project, "openclaw");
|
||||||
assert.ok(initRequest!.body.contentSessionId.startsWith("openclaw-agent-1-"));
|
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")));
|
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 });
|
const { api, fireEvent } = createMockApi({ workerPort });
|
||||||
claudeMemPlugin(api);
|
claudeMemPlugin(api);
|
||||||
|
|
||||||
await fireEvent("before_agent_start", { prompt: "hi" }, {});
|
await fireEvent("session_start", { sessionId: "test-session-1" }, {});
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
const initRequest = receivedRequests.find((r) => r.url === "/api/sessions/init");
|
const initRequests = receivedRequests.filter((r) => r.url === "/api/sessions/init");
|
||||||
assert.ok(!initRequest, "should not send init for short prompts");
|
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 () => {
|
it("tool_result_persist sends observation to worker", async () => {
|
||||||
const { api, fireEvent } = createMockApi({ workerPort });
|
const { api, fireEvent } = createMockApi({ workerPort });
|
||||||
claudeMemPlugin(api);
|
claudeMemPlugin(api);
|
||||||
|
|
||||||
// Init session first to establish contentSessionId
|
// Establish contentSessionId via session_start
|
||||||
await fireEvent("before_agent_start", {
|
await fireEvent("session_start", { sessionId: "s1" }, { sessionKey: "test-agent" });
|
||||||
prompt: "Help me write a function that parses JSON",
|
|
||||||
}, { sessionKey: "test-agent" });
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
// Fire tool result event
|
// Fire tool result event
|
||||||
@@ -404,11 +423,8 @@ describe("Observation I/O event handlers", () => {
|
|||||||
const { api, fireEvent } = createMockApi({ workerPort });
|
const { api, fireEvent } = createMockApi({ workerPort });
|
||||||
claudeMemPlugin(api);
|
claudeMemPlugin(api);
|
||||||
|
|
||||||
// Init session
|
// Establish session
|
||||||
await fireEvent("before_agent_start", {
|
await fireEvent("session_start", { sessionId: "s1" }, { sessionKey: "summarize-test" });
|
||||||
prompt: "Help me write a function that parses JSON",
|
|
||||||
}, { sessionKey: "summarize-test" });
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
// Fire agent end
|
// Fire agent end
|
||||||
@@ -435,10 +451,7 @@ describe("Observation I/O event handlers", () => {
|
|||||||
const { api, fireEvent } = createMockApi({ workerPort });
|
const { api, fireEvent } = createMockApi({ workerPort });
|
||||||
claudeMemPlugin(api);
|
claudeMemPlugin(api);
|
||||||
|
|
||||||
await fireEvent("before_agent_start", {
|
await fireEvent("session_start", { sessionId: "s1" }, { sessionKey: "array-content" });
|
||||||
prompt: "Help me write a function that parses JSON",
|
|
||||||
}, { sessionKey: "array-content" });
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
await fireEvent("agent_end", {
|
await fireEvent("agent_end", {
|
||||||
@@ -464,10 +477,7 @@ describe("Observation I/O event handlers", () => {
|
|||||||
const { api, fireEvent } = createMockApi({ workerPort, project: "my-project" });
|
const { api, fireEvent } = createMockApi({ workerPort, project: "my-project" });
|
||||||
claudeMemPlugin(api);
|
claudeMemPlugin(api);
|
||||||
|
|
||||||
await fireEvent("before_agent_start", {
|
await fireEvent("session_start", { sessionId: "s1" }, {});
|
||||||
prompt: "Help me write a function that parses JSON",
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
const initRequest = receivedRequests.find((r) => r.url === "/api/sessions/init");
|
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 });
|
const { api, fireEvent } = createMockApi({ workerPort });
|
||||||
claudeMemPlugin(api);
|
claudeMemPlugin(api);
|
||||||
|
|
||||||
await fireEvent("before_agent_start", {
|
await fireEvent("session_start", { sessionId: "s1" }, { sessionKey: "reuse-test" });
|
||||||
prompt: "Help me write a function that parses JSON",
|
|
||||||
}, { sessionKey: "reuse-test" });
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
await fireEvent("tool_result_persist", {
|
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 {
|
interface EventContext {
|
||||||
sessionKey?: string;
|
sessionKey?: string;
|
||||||
workspaceDir?: string;
|
workspaceDir?: string;
|
||||||
@@ -81,6 +92,8 @@ interface OpenClawPluginApi {
|
|||||||
on: ((event: "before_agent_start", callback: EventCallback<BeforeAgentStartEvent>) => void) &
|
on: ((event: "before_agent_start", callback: EventCallback<BeforeAgentStartEvent>) => void) &
|
||||||
((event: "tool_result_persist", callback: EventCallback<ToolResultPersistEvent>) => void) &
|
((event: "tool_result_persist", callback: EventCallback<ToolResultPersistEvent>) => void) &
|
||||||
((event: "agent_end", callback: EventCallback<AgentEndEvent>) => 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);
|
((event: "gateway_start", callback: EventCallback<Record<string, never>>) => void);
|
||||||
runtime: {
|
runtime: {
|
||||||
channel: Record<string, Record<string, (...args: any[]) => Promise<any>>>;
|
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 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
|
// Track workspace dir so tool_result_persist can sync MEMORY.md later
|
||||||
if (ctx.workspaceDir) {
|
if (ctx.workspaceDir) {
|
||||||
workspaceDirsBySessionKey.set(ctx.sessionKey || "default", 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) {
|
if (syncMemoryFile && ctx.workspaceDir) {
|
||||||
await syncMemoryToWorkspace(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