fix: preserve original OpenClaw prompts

This commit is contained in:
jinli.yl 2026-08-21 12:11:38 +08:00
parent 4c3bdec430
commit 18f914454b
4 changed files with 82 additions and 11 deletions

View file

@ -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({

View file

@ -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 = '<reme-context source="auto-recall">';
if (!value.startsWith(opening)) return value;
const start = value.indexOf(opening);
if (start === -1) return value;
const closing = "</reme-context>";
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 {

View file

@ -14,6 +14,7 @@ export interface OpenClawAgentContext {
export class OpenClawReMeRuntime {
private writes = Promise.resolve();
private controller = new AbortController();
private readonly prompts = new Map<string, string>();
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}`
: "";
}

View file

@ -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" +
'<reme-context source="auto-recall">\nremembered deployment\n</reme-context>\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;
}