Skip to content

Commit 862e95b

Browse files
authored
Merge pull request CortexReach#159 from Disaster-Terminator/fork-submit/clean-recall-text-final
Validated locally. - Host functional test passed - Agent E2E passed - Web dashboard / WebChat check passed - Visible recall text and auto-recall injected text are now cleaned up as expected - No blocking regression found from this PR I did observe one non-blocking model response inconsistency in a JSON prompt during manual chat testing, but the underlying recall output was correct and it does not appear to be caused by this PR.
2 parents aa0ec8d + cc0f337 commit 862e95b

File tree

4 files changed

+243
-8
lines changed

4 files changed

+243
-8
lines changed

index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2060,7 +2060,7 @@ const memoryLanceDBProPlugin = {
20602060
const displayTier = tierOverrides.get(r.entry.id) || metaObj.tier || "";
20612061
const tierPrefix = displayTier ? `[${displayTier.charAt(0).toUpperCase()}]` : "";
20622062
const abstract = metaObj.l0_abstract || r.entry.text;
2063-
return `- ${tierPrefix}[${displayCategory}:${r.entry.scope}] ${sanitizeForContext(abstract)} (${(r.score * 100).toFixed(0)}%${r.sources?.bm25 ? ", vector+BM25" : ""}${r.sources?.reranked ? "+reranked" : ""})`;
2063+
return `- ${tierPrefix}[${displayCategory}:${r.entry.scope}] ${sanitizeForContext(abstract)}`;
20642064
})
20652065
.join("\n");
20662066

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
]
3636
},
3737
"scripts": {
38-
"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",
38+
"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",
3939
"test:openclaw-host": "node test/openclaw-host-functional.mjs"
4040
},
4141
"devDependencies": {

src/tools.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -482,13 +482,8 @@ export function registerMemoryRecallTool(
482482

483483
const text = results
484484
.map((r, i) => {
485-
const sources = [];
486-
if (r.sources.vector) sources.push("vector");
487-
if (r.sources.bm25) sources.push("BM25");
488-
if (r.sources.reranked) sources.push("reranked");
489-
490485
const categoryTag = getDisplayCategoryTag(r.entry);
491-
return `${i + 1}. [${r.entry.id}] [${categoryTag}] ${r.entry.text} (${(r.score * 100).toFixed(0)}%${sources.length > 0 ? `, ${sources.join("+")}` : ""})`;
486+
return `${i + 1}. [${r.entry.id}] [${categoryTag}] ${r.entry.text}`;
492487
})
493488
.join("\n");
494489

test/recall-text-cleanup.test.mjs

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
import { describe, it, beforeEach, afterEach } from "node:test";
2+
import assert from "node:assert/strict";
3+
import { mkdtempSync, rmSync } from "node:fs";
4+
import { tmpdir } from "node:os";
5+
import path from "node:path";
6+
import { fileURLToPath } from "node:url";
7+
import jitiFactory from "jiti";
8+
9+
const testDir = path.dirname(fileURLToPath(import.meta.url));
10+
const pluginSdkStubPath = path.resolve(testDir, "helpers", "openclaw-plugin-sdk-stub.mjs");
11+
const jiti = jitiFactory(import.meta.url, {
12+
interopDefault: true,
13+
alias: {
14+
"openclaw/plugin-sdk": pluginSdkStubPath,
15+
},
16+
});
17+
18+
const pluginModule = jiti("../index.ts");
19+
const memoryLanceDBProPlugin = pluginModule.default || pluginModule;
20+
const { registerMemoryRecallTool } = jiti("../src/tools.ts");
21+
const { MemoryRetriever } = jiti("../src/retriever.ts");
22+
23+
function makeApiCapture() {
24+
let capturedCreator = null;
25+
const api = {
26+
registerTool(cb) {
27+
capturedCreator = cb;
28+
},
29+
logger: { info: () => {}, warn: () => {}, debug: () => {} },
30+
};
31+
return { api, getCreator: () => capturedCreator };
32+
}
33+
34+
function createPluginApiHarness({ pluginConfig, resolveRoot }) {
35+
const eventHandlers = new Map();
36+
37+
const api = {
38+
pluginConfig,
39+
resolvePath(target) {
40+
if (typeof target !== "string") return target;
41+
if (path.isAbsolute(target)) return target;
42+
return path.join(resolveRoot, target);
43+
},
44+
logger: {
45+
info() {},
46+
warn() {},
47+
debug() {},
48+
},
49+
registerTool() {},
50+
registerCli() {},
51+
registerService() {},
52+
on(eventName, handler, meta) {
53+
const list = eventHandlers.get(eventName) || [];
54+
list.push({ handler, meta });
55+
eventHandlers.set(eventName, list);
56+
},
57+
registerHook() {},
58+
};
59+
60+
return { api, eventHandlers };
61+
}
62+
63+
function makeResults() {
64+
return [
65+
{
66+
entry: {
67+
id: "m1",
68+
text: "remember this",
69+
category: "fact",
70+
scope: "global",
71+
importance: 0.7,
72+
timestamp: Date.now(),
73+
},
74+
score: 0.82,
75+
sources: {
76+
vector: { score: 0.82, rank: 1 },
77+
bm25: { score: 0.88, rank: 2 },
78+
reranked: { score: 0.91 },
79+
},
80+
},
81+
{
82+
entry: {
83+
id: "m2",
84+
text: "prefer concise diffs",
85+
category: "preference",
86+
scope: "global",
87+
importance: 0.8,
88+
timestamp: Date.now(),
89+
},
90+
score: 0.77,
91+
sources: {
92+
vector: { score: 0.77, rank: 2 },
93+
bm25: { score: 0.71, rank: 3 },
94+
},
95+
},
96+
];
97+
}
98+
99+
function makeExpandedResults() {
100+
return [
101+
...makeResults(),
102+
{
103+
entry: {
104+
id: "m3",
105+
text: "third item stays clean",
106+
category: "note",
107+
scope: "project",
108+
importance: 0.5,
109+
timestamp: Date.now(),
110+
},
111+
score: 0.65,
112+
sources: {
113+
vector: { score: 0.65, rank: 3 },
114+
},
115+
},
116+
];
117+
}
118+
119+
function makeRecallContext(results = makeResults()) {
120+
return {
121+
retriever: {
122+
async retrieve() {
123+
return results;
124+
},
125+
getConfig() {
126+
return { mode: "hybrid" };
127+
},
128+
},
129+
store: {
130+
patchMetadata: async () => null,
131+
},
132+
scopeManager: {
133+
getAccessibleScopes: () => ["global"],
134+
isAccessible: () => true,
135+
getDefaultScope: () => "global",
136+
},
137+
embedder: { embedPassage: async () => [] },
138+
agentId: "main",
139+
workspaceDir: "/tmp",
140+
mdMirror: null,
141+
};
142+
}
143+
144+
function createTool(registerTool, context) {
145+
const { api, getCreator } = makeApiCapture();
146+
registerTool(api, context);
147+
const creator = getCreator();
148+
assert.ok(typeof creator === "function");
149+
return creator({});
150+
}
151+
152+
function extractRenderedMemoryRecallLines(text) {
153+
return text
154+
.split(/\r?\n/)
155+
.map((line) => line.trim())
156+
.filter((line) => /^\d+\.\s\[/.test(line));
157+
}
158+
159+
describe("recall text cleanup", () => {
160+
let workspaceDir;
161+
let originalRetrieve;
162+
163+
beforeEach(() => {
164+
workspaceDir = mkdtempSync(path.join(tmpdir(), "recall-text-cleanup-test-"));
165+
originalRetrieve = MemoryRetriever.prototype.retrieve;
166+
});
167+
168+
afterEach(() => {
169+
MemoryRetriever.prototype.retrieve = originalRetrieve;
170+
rmSync(workspaceDir, { recursive: true, force: true });
171+
});
172+
173+
it("removes retrieval metadata from memory_recall content text but preserves details fields", async () => {
174+
const tool = createTool(registerMemoryRecallTool, makeRecallContext());
175+
const res = await tool.execute(null, { query: "test" });
176+
177+
assert.deepEqual(extractRenderedMemoryRecallLines(res.content[0].text), [
178+
"1. [m1] [fact:global] remember this",
179+
"2. [m2] [preference:global] prefer concise diffs",
180+
]);
181+
182+
assert.equal(typeof res.details.memories[0].score, "number");
183+
assert.ok(res.details.memories[0].sources.vector);
184+
assert.ok(res.details.memories[0].sources.bm25);
185+
assert.ok(res.details.memories[0].sources.reranked);
186+
assert.equal(typeof res.details.memories[1].score, "number");
187+
assert.ok(res.details.memories[1].sources.vector);
188+
assert.ok(res.details.memories[1].sources.bm25);
189+
});
190+
191+
it("removes retrieval metadata from every rendered memory_recall line", async () => {
192+
const tool = createTool(registerMemoryRecallTool, makeRecallContext(makeExpandedResults()));
193+
const res = await tool.execute(null, { query: "test with multiple memories" });
194+
195+
const lines = extractRenderedMemoryRecallLines(res.content[0].text);
196+
197+
assert.equal(lines.length, 3, "expected three rendered memory lines");
198+
assert.match(lines[2], /third item stays clean/);
199+
for (const line of lines) {
200+
assert.doesNotMatch(line, /\d+%/);
201+
assert.doesNotMatch(line, /\bvector\b|\bBM25\b|\breranked\b/);
202+
}
203+
});
204+
205+
it("removes retrieval metadata from auto-recall injected text", async () => {
206+
MemoryRetriever.prototype.retrieve = async () => makeResults();
207+
208+
const harness = createPluginApiHarness({
209+
resolveRoot: workspaceDir,
210+
pluginConfig: {
211+
dbPath: path.join(workspaceDir, "db"),
212+
embedding: { apiKey: "test-api-key" },
213+
smartExtraction: false,
214+
autoCapture: false,
215+
autoRecall: true,
216+
autoRecallMinLength: 1,
217+
selfImprovement: { enabled: false, beforeResetNote: false, ensureLearningFiles: false },
218+
},
219+
});
220+
221+
memoryLanceDBProPlugin.register(harness.api);
222+
223+
const hooks = harness.eventHandlers.get("before_agent_start") || [];
224+
assert.equal(hooks.length, 1, "expected exactly one before_agent_start hook for this config");
225+
const [{ handler: autoRecallHook }] = hooks;
226+
assert.equal(typeof autoRecallHook, "function");
227+
228+
const output = await autoRecallHook(
229+
{ prompt: "Please recall what I mentioned before about this task." },
230+
{ sessionId: "auto-clean", sessionKey: "agent:main:session:auto-clean", agentId: "main" }
231+
);
232+
233+
assert.ok(output);
234+
assert.match(output.prependContext, /remember this/);
235+
assert.match(output.prependContext, /prefer concise diffs/);
236+
assert.doesNotMatch(output.prependContext, /vector\+BM25/);
237+
assert.doesNotMatch(output.prependContext, /reranked/);
238+
assert.doesNotMatch(output.prependContext, /\d+%/);
239+
});
240+
});

0 commit comments

Comments
 (0)