fix(deepseek-plugin): first-turn recall, native activation, and cancel-safe recall (#7284, #7302)

Ports #7285 onto current main and adds a canDeliver() gate so a canceled recall does not mark undelivered memories as seen.

Refs #7284, #7302. Supersedes #7285.
This commit is contained in:
Himanshu-Sangshetti
2026-10-05 09:48:32 +05:30
parent abb81c88e1
commit df7cd065da
11 changed files with 313 additions and 62 deletions
@@ -75,6 +75,8 @@ interface RecallOptions {
maxChars?: number;
seenIds?: Set<string>;
timeoutMs?: number;
/** Gate for marking memories as seen. Return false when the context will not reach the model. */
canDeliver?: () => boolean;
}
interface MemoryLifecycleOptions {
@@ -110,12 +112,14 @@ class MemoryLifecycle {
prompt: string,
enabled: boolean,
search: (query: string) => Promise<{ results?: unknown[] }>,
canDeliver?: () => boolean,
): Promise<string> {
return buildRecallContext(prompt, enabled, search, {
heading: this.#options.recallHeading,
maxChars: this.#options.maxContextChars,
seenIds: this.#seenMemoryIds,
timeoutMs: this.#options.recallTimeoutMs,
canDeliver,
});
}
}
@@ -156,6 +160,7 @@ export async function buildRecallContext(
const suffix = "\n</mem0-relevant-memories>";
const maxChars = options.maxChars ?? DEFAULT_MAX_CONTEXT_CHARS;
const lines: string[] = [];
const includedIds: string[] = [];
for (const memory of unseen) {
const line = `${lines.length + 1}. ${redactSecrets(formatMemoryCompact(memory))
.replace(/\s+/g, " ")
@@ -164,15 +169,20 @@ export async function buildRecallContext(
if (candidate.length > maxChars) {
if (!lines.length) {
const available = maxChars - prefix.length - suffix.length;
if (available > 1) lines.push(`${line.slice(0, available - 1).trimEnd()}…`);
if (available > 1) {
lines.push(`${line.slice(0, available - 1).trimEnd()}…`);
includedIds.push(memory.id);
}
}
break;
}
lines.push(line);
options.seenIds?.add(memory.id);
includedIds.push(memory.id);
}
if (!lines.length) return "";
if (unseen[0] && !options.seenIds?.has(unseen[0].id)) options.seenIds?.add(unseen[0].id);
if (options.canDeliver?.() ?? true) {
for (const id of includedIds) options.seenIds?.add(id);
}
return prefix + lines.join("\n") + suffix;
} catch {
return "";
@@ -20,6 +20,15 @@ test("one lifecycle owns recall state and resets it for a new session", async ()
assert.match(await lifecycle.recall("package manager", true, search), /Use pnpm/);
});
test("recall leaves memories unseen when delivery is not allowed", async () => {
const lifecycle = createMemoryLifecycle({ recallTimeoutMs: 50 });
const search = async () => ({ results: [{ id: "m1", memory: "Use pnpm" }] });
assert.match(await lifecycle.recall("package manager", true, search, () => false), /Use pnpm/);
assert.match(await lifecycle.recall("package manager", true, search), /Use pnpm/);
assert.equal(await lifecycle.recall("package manager", true, search), "");
});
test("one lifecycle owns capture preparation", () => {
const lifecycle = createMemoryLifecycle();