Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -604,8 +604,8 @@ export function registerMemoryRecallTool(
const categoryTag = getDisplayCategoryTag(r.entry);
const metadata = parseSmartMetadata(r.entry.metadata, r.entry);
const base = includeFullText
? r.entry.text
: metadata.l0_abstract || r.entry.text;
? (metadata.l2_content || metadata.l1_overview || r.entry.text)
: (metadata.l0_abstract || r.entry.text);
const inline = normalizeInlineText(base);
const rendered = includeFullText
? inline
Expand All @@ -614,6 +614,15 @@ export function registerMemoryRecallTool(
})
.join("\n");

const serializedMemories = sanitizeMemoryForSerialization(results);
if (includeFullText) {
for (let i = 0; i < results.length; i++) {
const metadata = parseSmartMetadata(results[i].entry.metadata, results[i].entry);
(serializedMemories[i] as Record<string, unknown>).fullText =
metadata.l2_content || metadata.l1_overview || results[i].entry.text;
}
}

return {
content: [
{
Expand All @@ -623,7 +632,7 @@ export function registerMemoryRecallTool(
],
details: {
count: results.length,
memories: sanitizeMemoryForSerialization(results),
memories: serializedMemories,
query,
scopes: scopeFilter,
retrievalMode: runtimeContext.retriever.getConfig().mode,
Expand Down
120 changes: 120 additions & 0 deletions test/recall-text-cleanup.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,126 @@ describe("recall text cleanup", () => {
assert.doesNotMatch(lines[0], /…$/, "full text mode should not force preview truncation");
});

it("includeFullText=true renders L2 content in output, not L0 abstract", async () => {
const l0 = "short L0 abstract";
const l2 = "Full L2 narrative: the user resolved a concurrent-write conflict by adding proper-lockfile as a write guard around all LanceDB mutation calls. Prevention: always acquire the lock before any store.add / store.update call.";

const results = [
{
entry: {
id: "case-1",
text: l0,
category: "fact",
scope: "global",
importance: 0.85,
timestamp: Date.now(),
metadata: stringifySmartMetadata(
buildSmartMetadata(
{ text: l0, category: "fact", importance: 0.85 },
{
l0_abstract: l0,
l1_overview: "## Conflict\n- LanceDB concurrent write resolved via proper-lockfile",
l2_content: l2,
memory_category: "cases",
fact_key: "cases:lancedb-write-conflict",
},
),
),
},
score: 0.95,
sources: { vector: { score: 0.95, rank: 1 } },
},
];

// default (summary) mode should show L0
const toolSummary = createTool(registerMemoryRecallTool, makeRecallContext(results));
const resSummary = await toolSummary.execute(null, { query: "lancedb conflict" });
const summaryLines = extractRenderedMemoryRecallLines(resSummary.content[0].text);
assert.equal(summaryLines.length, 1);
assert.match(summaryLines[0], new RegExp(l0.slice(0, 20)));
assert.doesNotMatch(summaryLines[0], /Full L2 narrative/);

// includeFullText=true should show L2 in rendered output
const toolFull = createTool(registerMemoryRecallTool, makeRecallContext(results));
const resFull = await toolFull.execute(null, { query: "lancedb conflict", includeFullText: true });
const fullLines = extractRenderedMemoryRecallLines(resFull.content[0].text);
assert.equal(fullLines.length, 1);
assert.match(fullLines[0], /Full L2 narrative/, "rendered line should contain L2 content");
assert.doesNotMatch(fullLines[0], new RegExp(`^.*\\[case-1\\].*${l0.slice(0, 15)}`), "rendered line should not be the L0 abstract");

// details.memories[].fullText should carry L2
assert.equal(resFull.details.memories[0].fullText, l2, "details.memories[0].fullText should be L2 content");
// details.memories[].text still carries L0 for backwards compatibility
assert.equal(resFull.details.memories[0].text, l0, "details.memories[0].text should still be L0 for compatibility");
});

it("includeFullText=false does not expose fullText in details.memories", async () => {
const l0 = "short L0 abstract";
const l2 = "Full L2 narrative that should not appear when includeFullText is false.";

const results = [
{
entry: {
id: "case-2",
text: l0,
category: "fact",
scope: "global",
importance: 0.85,
timestamp: Date.now(),
metadata: stringifySmartMetadata(
buildSmartMetadata(
{ text: l0, category: "fact", importance: 0.85 },
{
l0_abstract: l0,
l1_overview: "## Overview\n- some overview",
l2_content: l2,
memory_category: "cases",
fact_key: "cases:opt-in-check",
},
),
),
},
score: 0.9,
sources: { vector: { score: 0.9, rank: 1 } },
},
];

const tool = createTool(registerMemoryRecallTool, makeRecallContext(results));
const res = await tool.execute(null, { query: "opt-in check" });

assert.equal(res.details.memories[0].fullText, undefined, "fullText should be absent when includeFullText=false");
assert.equal(res.details.memories[0].text, l0, "text should still carry L0");
});

it("includeFullText=true falls back to entry.text for legacy memories without smart metadata", async () => {
const legacyText = "legacy memory with no smart metadata at all";

const results = [
{
entry: {
id: "legacy-1",
text: legacyText,
category: "fact",
scope: "global",
importance: 0.6,
timestamp: Date.now(),
// no metadata field — simulates pre-smart-extraction records
},
score: 0.75,
sources: { vector: { score: 0.75, rank: 1 } },
},
];

const tool = createTool(registerMemoryRecallTool, makeRecallContext(results));
const res = await tool.execute(null, { query: "legacy fallback", includeFullText: true });
const lines = extractRenderedMemoryRecallLines(res.content[0].text);

assert.equal(lines.length, 1);
assert.match(lines[0], /legacy memory with no smart metadata/, "should render entry.text as fallback for legacy memories");
assert.equal(res.details.memories[0].fullText, legacyText, "details.memories[0].fullText should fall back to entry.text");
});


it("applies auto-recall item/char budgets before injecting context", async () => {
MemoryRetriever.prototype.retrieve = async () => makeManyResults(5);

Expand Down
Loading