diff --git a/packages/typescript/src/openclaw/index.ts b/packages/typescript/src/openclaw/index.ts index da8269ea..791787be 100644 --- a/packages/typescript/src/openclaw/index.ts +++ b/packages/typescript/src/openclaw/index.ts @@ -20,9 +20,11 @@ const plugin: OpenClawPluginDefinition = { const runtime = new OpenClawReMeRuntime(client, config, api.logger); registerOpenClawTools(api, client, config); - if (config.autoRecall) { + if (config.autoRecall || config.autoCapture) { api.on("before_agent_start", async (event, context) => { if (!capturesTrigger(context.trigger)) return; + runtime.rememberPrompt(event.prompt, context); + if (!config.autoRecall) return; const query = event.prompt.trim(); if (!query) return; const result = await client.search(query, { @@ -41,7 +43,8 @@ const plugin: OpenClawPluginDefinition = { } api.on("agent_end", (event, context) => { - if (event.success) runtime.capture(event.messages, context); + const prompt = runtime.takePrompt(context); + if (event.success) runtime.capture(event.messages, context, prompt); }); api.registerService({ diff --git a/packages/typescript/src/openclaw/messages.ts b/packages/typescript/src/openclaw/messages.ts index 82738d92..b7fd0b96 100644 --- a/packages/typescript/src/openclaw/messages.ts +++ b/packages/typescript/src/openclaw/messages.ts @@ -19,6 +19,7 @@ export function openClawSessionId(value: string): string { export function captureLastTurn( messages: unknown[], sessionId: string, + userPrompt?: string, ): ReMeMessage[] { let assistant: ReMeMessage | null = null; for (let index = messages.length - 1; index >= 0; index -= 1) { @@ -29,7 +30,13 @@ export function captureLastTurn( continue; } if (assistant && record.role === "user") { - const user = normalizeMessage(record, "user", sessionId, index); + const user = normalizeMessage( + record, + "user", + sessionId, + index, + userPrompt, + ); return user ? [user, assistant] : []; } } @@ -41,8 +48,10 @@ function normalizeMessage( role: "user" | "assistant", sessionId: string, index: number, + textOverride?: string, ): ReMeMessage | null { - const text = stripAutoRecallContext(messageText(value.content)); + const text = + textOverride?.trim() || stripAutoRecallContext(messageText(value.content)); if (!text) return null; const nativeId = typeof value.id === "string" && value.id ? value.id : `${index}\n${text}`; @@ -58,11 +67,12 @@ function normalizeMessage( function stripAutoRecallContext(value: string): string { const opening = ''; - if (!value.startsWith(opening)) return value; + const start = value.indexOf(opening); + if (start === -1) return value; const closing = ""; - const end = value.indexOf(closing, opening.length); + const end = value.indexOf(closing, start + opening.length); if (end === -1) return value; - return value.slice(end + closing.length).trim(); + return `${value.slice(0, start)}${value.slice(end + closing.length)}`.trim(); } function messageText(content: unknown): string { diff --git a/packages/typescript/src/openclaw/runtime.ts b/packages/typescript/src/openclaw/runtime.ts index 6a6fb9a7..6b26bc1b 100644 --- a/packages/typescript/src/openclaw/runtime.ts +++ b/packages/typescript/src/openclaw/runtime.ts @@ -14,6 +14,7 @@ export interface OpenClawAgentContext { export class OpenClawReMeRuntime { private writes = Promise.resolve(); private controller = new AbortController(); + private readonly prompts = new Map(); constructor( readonly client: ReMeClientLike, @@ -21,7 +22,26 @@ export class OpenClawReMeRuntime { readonly logger: LoggerLike, ) {} - capture(messages: unknown[], context: OpenClawAgentContext): void { + rememberPrompt(prompt: string, context: OpenClawAgentContext): void { + if (!this.config.autoCapture || !capturesTrigger(context.trigger)) return; + const key = promptKey(context); + const text = prompt.trim(); + if (key && text) this.prompts.set(key, text); + } + + takePrompt(context: OpenClawAgentContext): string | undefined { + const key = promptKey(context); + if (!key) return undefined; + const prompt = this.prompts.get(key); + this.prompts.delete(key); + return prompt; + } + + capture( + messages: unknown[], + context: OpenClawAgentContext, + prompt?: string, + ): void { if (!this.config.autoCapture || !capturesTrigger(context.trigger)) return; const nativeSessionId = context.sessionId || context.sessionKey; if (!nativeSessionId) { @@ -33,7 +53,7 @@ export class OpenClawReMeRuntime { const sessionId = openClawSessionId( `${context.agentId || "default"}\n${nativeSessionId}`, ); - const captured = captureLastTurn(messages, sessionId); + const captured = captureLastTurn(messages, sessionId, prompt); if (captured.length !== 2) return; this.writes = this.writes .then(async () => { @@ -71,3 +91,10 @@ export class OpenClawReMeRuntime { function capturesTrigger(trigger: string | undefined): boolean { return trigger === undefined || trigger === "user"; } + +function promptKey(context: OpenClawAgentContext): string { + const nativeSessionId = context.sessionId || context.sessionKey; + return nativeSessionId + ? `${context.agentId || "default"}\n${nativeSessionId}` + : ""; +} diff --git a/packages/typescript/tests/openclaw.test.mjs b/packages/typescript/tests/openclaw.test.mjs index 98710099..2966c67c 100644 --- a/packages/typescript/tests/openclaw.test.mjs +++ b/packages/typescript/tests/openclaw.test.mjs @@ -79,6 +79,30 @@ test("removes recalled context while preserving the current OpenClaw prompt", () ); }); +test("uses the original prompt when other plugins prepend context around recall", () => { + const messages = captureLastTurn( + [ + { + role: "user", + content: + "other plugin context\n\n" + + '\nremembered deployment\n\n\n' + + "another plugin context\n\nremember blue", + }, + { role: "assistant", content: "noted" }, + ], + "session", + "remember blue", + ); + assert.deepEqual( + messages.map((message) => [message.role, message.content[0].text]), + [ + ["user", "remember blue"], + ["assistant", "noted"], + ], + ); +}); + test("registers OpenClaw recall, capture, tool, and shutdown lifecycle", async () => { const originalFetch = globalThis.fetch; const calls = []; @@ -114,7 +138,7 @@ test("registers OpenClaw recall, capture, tool, and shutdown lifecycle", async ( assert.equal(tools[0].name, "reme_search"); const recalled = await hooks.get("before_agent_start")( { prompt: "deployment" }, - { trigger: "user" }, + { trigger: "user", agentId: "main", sessionId: "session-1" }, ); assert.match(recalled.prependContext, /remembered deployment/); @@ -122,7 +146,13 @@ test("registers OpenClaw recall, capture, tool, and shutdown lifecycle", async ( { success: true, messages: [ - { role: "user", content: "remember blue" }, + { + role: "user", + content: + "other plugin context\n\n" + + recalled.prependContext + + "\n\ndeployment", + }, { role: "assistant", content: "noted" }, ], }, @@ -134,6 +164,7 @@ test("registers OpenClaw recall, capture, tool, and shutdown lifecycle", async ( calls.map((call) => call.url), ["http://127.0.0.1:2333/search", "http://127.0.0.1:2333/auto_memory"], ); + assert.equal(calls[1].body.messages[0].content[0].text, "deployment"); } finally { globalThis.fetch = originalFetch; }