Skip to content
Open
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: 13 additions & 2 deletions dist/src/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,11 @@ function createMemoryRecallTool(runtimeContext, options) {
};
}
const now = Date.now();
await Promise.allSettled(results.map((result) => {
// Access metadata is best-effort usage telemetry. Keep it off the
// manual recall response path, matching auto-recall, so write-lock
// contention cannot turn an otherwise successful read into a tool
// timeout.
void Promise.allSettled(results.map((result) => {
const meta = parseSmartMetadata(result.entry.metadata, result.entry);
return runtimeContext.store.patchMetadata(result.entry.id, {
access_count: meta.access_count + 1,
Expand All @@ -683,7 +687,14 @@ function createMemoryRecallTool(runtimeContext, options) {
// memory the user just explicitly searched for.
suppressed_until_ms: 0,
}, scopeFilter);
}));
})).then((settled) => {
const rejected = settled.filter((result) => result.status === "rejected");
if (rejected.length > 0) {
console.warn(`[memory-lancedb-pro] background manual recall metadata patch failed for ${rejected.length}/${settled.length} memories`);
}
}).catch((error) => {
console.warn(`[memory-lancedb-pro] background manual recall metadata patch crashed: ${String(error)}`);
});
const text = results
.map((r, i) => {
const categoryTag = getDisplayCategoryTag(r.entry);
Expand Down
19 changes: 17 additions & 2 deletions src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -896,7 +896,11 @@ function createMemoryRecallTool(
}

const now = Date.now();
await Promise.allSettled(
// Access metadata is best-effort usage telemetry. Keep it off the
// manual recall response path, matching auto-recall, so write-lock
// contention cannot turn an otherwise successful read into a tool
// timeout.
void Promise.allSettled(
results.map((result) => {
const meta = parseSmartMetadata(result.entry.metadata, result.entry);
return runtimeContext.store.patchMetadata(
Expand All @@ -917,7 +921,18 @@ function createMemoryRecallTool(
scopeFilter,
);
}),
);
).then((settled) => {
const rejected = settled.filter((result) => result.status === "rejected");
if (rejected.length > 0) {
console.warn(
`[memory-lancedb-pro] background manual recall metadata patch failed for ${rejected.length}/${settled.length} memories`,
);
}
}).catch((error) => {
console.warn(
`[memory-lancedb-pro] background manual recall metadata patch crashed: ${String(error)}`,
);
});

const text = results
.map((r, i) => {
Expand Down
85 changes: 85 additions & 0 deletions test/recall-text-cleanup.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,91 @@ describe("recall text cleanup", () => {
assert.ok(res.details.memories[1].sources.bm25);
});

it("returns manual recall results before access metadata patches settle", async () => {
const context = makeRecallContext();
let releasePatchGate;
const patchGate = new Promise((resolve) => {
releasePatchGate = resolve;
});
const patchPromises = [];
const patchCalls = [];
let patchMetadataCalls = 0;
let patchMetadataSettled = 0;

context.store.patchMetadata = (id, patch, scopeFilter) => {
patchMetadataCalls++;
patchCalls.push({ id, patch, scopeFilter });
const patchPromise = patchGate.then(() => {
patchMetadataSettled++;
return null;
});
patchPromises.push(patchPromise);
return patchPromise;
};

const tool = createTool(registerMemoryRecallTool, context);
const recallPromise = tool.execute(null, { query: "test" });
const timeoutMarker = Symbol("timeout");
const output = await Promise.race([
recallPromise,
new Promise((resolve) => setTimeout(() => resolve(timeoutMarker), 500)),
]);

try {
assert.notEqual(output, timeoutMarker, "access metadata patches must not block manual recall");
assert.match(output.content[0].text, /<relevant-memories>/);
assert.equal(patchMetadataCalls, 2, "background access metadata patches should still start");
assert.equal(patchMetadataSettled, 0, "manual recall should return before access metadata patches settle");
assert.deepEqual(
patchCalls.map(({ id, scopeFilter }) => ({ id, scopeFilter })),
[
{ id: "m1", scopeFilter: ["global"] },
{ id: "m2", scopeFilter: ["global"] },
],
);
assert.deepEqual(patchCalls[0].patch, {
access_count: 1,
last_accessed_at: patchCalls[0].patch.last_accessed_at,
last_confirmed_use_at: patchCalls[0].patch.last_confirmed_use_at,
bad_recall_count: 0,
suppressed_until_turn: 0,
suppressed_until_ms: 0,
});
assert.equal(patchCalls[0].patch.last_accessed_at, patchCalls[0].patch.last_confirmed_use_at);
} finally {
releasePatchGate();
await Promise.all(patchPromises);
await recallPromise;
}

assert.equal(patchMetadataSettled, 2, "background access metadata patches should eventually settle");
});

it("logs rejected background manual recall metadata patches", async () => {
const context = makeRecallContext();
context.store.patchMetadata = async (id) => {
if (id === "m1") throw new Error("simulated metadata write failure");
return null;
};
const warnings = [];
const originalWarn = console.warn;
console.warn = (...args) => warnings.push(args.join(" "));

try {
const tool = createTool(registerMemoryRecallTool, context);
const output = await tool.execute(null, { query: "test" });
await new Promise((resolve) => setImmediate(resolve));

assert.match(output.content[0].text, /<relevant-memories>/);
assert.ok(
warnings.some((line) => /background manual recall metadata patch failed for 1\/2 memories/.test(line)),
`expected background patch warning; warnings=${JSON.stringify(warnings)}`,
);
} finally {
console.warn = originalWarn;
}
});

it("keeps memory_recall neighbor summaries within the per-item character budget", async () => {
const tool = createTool(registerMemoryRecallTool, makeRecallContext(makeNeighborSummaryResults()));
const res = await tool.execute(null, {
Expand Down
Loading