From 828a22712932ac558660dca0587a8fb89185dbe1 Mon Sep 17 00:00:00 2001 From: Disaster-Terminator <2557058999@qq.com> Date: Tue, 10 Mar 2026 20:17:31 +0800 Subject: [PATCH 1/2] fix(recall): remove score/source suffixes from visible recall text --- index.ts | 2 +- src/tools.ts | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/index.ts b/index.ts index 48d7fb9..f2bd6bb 100644 --- a/index.ts +++ b/index.ts @@ -2060,7 +2060,7 @@ const memoryLanceDBProPlugin = { const displayTier = tierOverrides.get(r.entry.id) || metaObj.tier || ""; const tierPrefix = displayTier ? `[${displayTier.charAt(0).toUpperCase()}]` : ""; const abstract = metaObj.l0_abstract || r.entry.text; - return `- ${tierPrefix}[${displayCategory}:${r.entry.scope}] ${sanitizeForContext(abstract)} (${(r.score * 100).toFixed(0)}%${r.sources?.bm25 ? ", vector+BM25" : ""}${r.sources?.reranked ? "+reranked" : ""})`; + return `- ${tierPrefix}[${displayCategory}:${r.entry.scope}] ${sanitizeForContext(abstract)}`; }) .join("\n"); diff --git a/src/tools.ts b/src/tools.ts index 4736c27..d64e949 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -482,13 +482,8 @@ export function registerMemoryRecallTool( const text = results .map((r, i) => { - const sources = []; - if (r.sources.vector) sources.push("vector"); - if (r.sources.bm25) sources.push("BM25"); - if (r.sources.reranked) sources.push("reranked"); - const categoryTag = getDisplayCategoryTag(r.entry); - return `${i + 1}. [${r.entry.id}] [${categoryTag}] ${r.entry.text} (${(r.score * 100).toFixed(0)}%${sources.length > 0 ? `, ${sources.join("+")}` : ""})`; + return `${i + 1}. [${r.entry.id}] [${categoryTag}] ${r.entry.text}`; }) .join("\n"); From cc0f337f0be234f19d4f04d8e8e66b0d46180091 Mon Sep 17 00:00:00 2001 From: Disaster-Terminator <2557058999@qq.com> Date: Tue, 10 Mar 2026 21:39:34 +0800 Subject: [PATCH 2/2] test: cover recall text cleanup regressions --- package.json | 2 +- test/recall-text-cleanup.test.mjs | 240 ++++++++++++++++++++++++++++++ 2 files changed, 241 insertions(+), 1 deletion(-) create mode 100644 test/recall-text-cleanup.test.mjs diff --git a/package.json b/package.json index 7bb0dab..3d6582a 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ ] }, "scripts": { - "test": "node test/embedder-error-hints.test.mjs && node test/migrate-legacy-schema.test.mjs && node --test test/config-session-strategy-migration.test.mjs && node test/update-consistency-lancedb.test.mjs && node test/cli-smoke.mjs && node test/functional-e2e.mjs && node test/retriever-rerank-regression.mjs && node test/smart-memory-lifecycle.mjs && node test/smart-extractor-branches.mjs && node test/plugin-manifest-regression.mjs", + "test": "node test/embedder-error-hints.test.mjs && node test/migrate-legacy-schema.test.mjs && node --test test/config-session-strategy-migration.test.mjs && node --test test/recall-text-cleanup.test.mjs && node test/update-consistency-lancedb.test.mjs && node test/cli-smoke.mjs && node test/functional-e2e.mjs && node test/retriever-rerank-regression.mjs && node test/smart-memory-lifecycle.mjs && node test/smart-extractor-branches.mjs && node test/plugin-manifest-regression.mjs", "test:openclaw-host": "node test/openclaw-host-functional.mjs" }, "devDependencies": { diff --git a/test/recall-text-cleanup.test.mjs b/test/recall-text-cleanup.test.mjs new file mode 100644 index 0000000..d50dd30 --- /dev/null +++ b/test/recall-text-cleanup.test.mjs @@ -0,0 +1,240 @@ +import { describe, it, beforeEach, afterEach } from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import jitiFactory from "jiti"; + +const testDir = path.dirname(fileURLToPath(import.meta.url)); +const pluginSdkStubPath = path.resolve(testDir, "helpers", "openclaw-plugin-sdk-stub.mjs"); +const jiti = jitiFactory(import.meta.url, { + interopDefault: true, + alias: { + "openclaw/plugin-sdk": pluginSdkStubPath, + }, +}); + +const pluginModule = jiti("../index.ts"); +const memoryLanceDBProPlugin = pluginModule.default || pluginModule; +const { registerMemoryRecallTool } = jiti("../src/tools.ts"); +const { MemoryRetriever } = jiti("../src/retriever.ts"); + +function makeApiCapture() { + let capturedCreator = null; + const api = { + registerTool(cb) { + capturedCreator = cb; + }, + logger: { info: () => {}, warn: () => {}, debug: () => {} }, + }; + return { api, getCreator: () => capturedCreator }; +} + +function createPluginApiHarness({ pluginConfig, resolveRoot }) { + const eventHandlers = new Map(); + + const api = { + pluginConfig, + resolvePath(target) { + if (typeof target !== "string") return target; + if (path.isAbsolute(target)) return target; + return path.join(resolveRoot, target); + }, + logger: { + info() {}, + warn() {}, + debug() {}, + }, + registerTool() {}, + registerCli() {}, + registerService() {}, + on(eventName, handler, meta) { + const list = eventHandlers.get(eventName) || []; + list.push({ handler, meta }); + eventHandlers.set(eventName, list); + }, + registerHook() {}, + }; + + return { api, eventHandlers }; +} + +function makeResults() { + return [ + { + entry: { + id: "m1", + text: "remember this", + category: "fact", + scope: "global", + importance: 0.7, + timestamp: Date.now(), + }, + score: 0.82, + sources: { + vector: { score: 0.82, rank: 1 }, + bm25: { score: 0.88, rank: 2 }, + reranked: { score: 0.91 }, + }, + }, + { + entry: { + id: "m2", + text: "prefer concise diffs", + category: "preference", + scope: "global", + importance: 0.8, + timestamp: Date.now(), + }, + score: 0.77, + sources: { + vector: { score: 0.77, rank: 2 }, + bm25: { score: 0.71, rank: 3 }, + }, + }, + ]; +} + +function makeExpandedResults() { + return [ + ...makeResults(), + { + entry: { + id: "m3", + text: "third item stays clean", + category: "note", + scope: "project", + importance: 0.5, + timestamp: Date.now(), + }, + score: 0.65, + sources: { + vector: { score: 0.65, rank: 3 }, + }, + }, + ]; +} + +function makeRecallContext(results = makeResults()) { + return { + retriever: { + async retrieve() { + return results; + }, + getConfig() { + return { mode: "hybrid" }; + }, + }, + store: { + patchMetadata: async () => null, + }, + scopeManager: { + getAccessibleScopes: () => ["global"], + isAccessible: () => true, + getDefaultScope: () => "global", + }, + embedder: { embedPassage: async () => [] }, + agentId: "main", + workspaceDir: "/tmp", + mdMirror: null, + }; +} + +function createTool(registerTool, context) { + const { api, getCreator } = makeApiCapture(); + registerTool(api, context); + const creator = getCreator(); + assert.ok(typeof creator === "function"); + return creator({}); +} + +function extractRenderedMemoryRecallLines(text) { + return text + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => /^\d+\.\s\[/.test(line)); +} + +describe("recall text cleanup", () => { + let workspaceDir; + let originalRetrieve; + + beforeEach(() => { + workspaceDir = mkdtempSync(path.join(tmpdir(), "recall-text-cleanup-test-")); + originalRetrieve = MemoryRetriever.prototype.retrieve; + }); + + afterEach(() => { + MemoryRetriever.prototype.retrieve = originalRetrieve; + rmSync(workspaceDir, { recursive: true, force: true }); + }); + + it("removes retrieval metadata from memory_recall content text but preserves details fields", async () => { + const tool = createTool(registerMemoryRecallTool, makeRecallContext()); + const res = await tool.execute(null, { query: "test" }); + + assert.deepEqual(extractRenderedMemoryRecallLines(res.content[0].text), [ + "1. [m1] [fact:global] remember this", + "2. [m2] [preference:global] prefer concise diffs", + ]); + + assert.equal(typeof res.details.memories[0].score, "number"); + assert.ok(res.details.memories[0].sources.vector); + assert.ok(res.details.memories[0].sources.bm25); + assert.ok(res.details.memories[0].sources.reranked); + assert.equal(typeof res.details.memories[1].score, "number"); + assert.ok(res.details.memories[1].sources.vector); + assert.ok(res.details.memories[1].sources.bm25); + }); + + it("removes retrieval metadata from every rendered memory_recall line", async () => { + const tool = createTool(registerMemoryRecallTool, makeRecallContext(makeExpandedResults())); + const res = await tool.execute(null, { query: "test with multiple memories" }); + + const lines = extractRenderedMemoryRecallLines(res.content[0].text); + + assert.equal(lines.length, 3, "expected three rendered memory lines"); + assert.match(lines[2], /third item stays clean/); + for (const line of lines) { + assert.doesNotMatch(line, /\d+%/); + assert.doesNotMatch(line, /\bvector\b|\bBM25\b|\breranked\b/); + } + }); + + it("removes retrieval metadata from auto-recall injected text", async () => { + MemoryRetriever.prototype.retrieve = async () => makeResults(); + + const harness = createPluginApiHarness({ + resolveRoot: workspaceDir, + pluginConfig: { + dbPath: path.join(workspaceDir, "db"), + embedding: { apiKey: "test-api-key" }, + smartExtraction: false, + autoCapture: false, + autoRecall: true, + autoRecallMinLength: 1, + selfImprovement: { enabled: false, beforeResetNote: false, ensureLearningFiles: false }, + }, + }); + + memoryLanceDBProPlugin.register(harness.api); + + const hooks = harness.eventHandlers.get("before_agent_start") || []; + assert.equal(hooks.length, 1, "expected exactly one before_agent_start hook for this config"); + const [{ handler: autoRecallHook }] = hooks; + assert.equal(typeof autoRecallHook, "function"); + + const output = await autoRecallHook( + { prompt: "Please recall what I mentioned before about this task." }, + { sessionId: "auto-clean", sessionKey: "agent:main:session:auto-clean", agentId: "main" } + ); + + assert.ok(output); + assert.match(output.prependContext, /remember this/); + assert.match(output.prependContext, /prefer concise diffs/); + assert.doesNotMatch(output.prependContext, /vector\+BM25/); + assert.doesNotMatch(output.prependContext, /reranked/); + assert.doesNotMatch(output.prependContext, /\d+%/); + }); +});