fix(recall): reuse persisted user-floor recall before reroll

This commit is contained in:
opencode
2026-05-15 18:59:31 +00:00
parent ef2a719c69
commit 7b5e35eca2
2 changed files with 221 additions and 163 deletions

View File

@@ -118,9 +118,12 @@ function resolveReusablePersistedRecallRecord(chat, recallInput, runtime) {
"planner-handoff",
]);
const isActiveInputSource = activeInputSources.has(recallSource);
if (isActiveInputSource) {
return null;
}
if (!Number.isFinite(targetUserMessageIndex)) {
if (!currentRecallInputText || isActiveInputSource || !Array.isArray(chat)) {
if (!currentRecallInputText || !Array.isArray(chat)) {
return null;
}
for (let index = chat.length - 1; index >= 0; index--) {
@@ -539,6 +542,154 @@ export async function runRecallController(runtime, options = {}) {
});
}
const recentContextMessageLimit = runtime.clampInt(
settings.recallLlmContextMessages,
4,
0,
20,
);
const recallInput = runtime.resolveRecallInput(
chat,
recentContextMessageLimit,
options,
);
const userMessage = recallInput.userMessage;
const recentMessages = recallInput.recentMessages;
if (!userMessage) {
return runtime.createRecallRunResult("skipped", {
reason: "当前没有可用于召回的用户输入",
});
}
recallInput.hookName = options.hookName || "";
recallInput.deliveryMode =
String(options.deliveryMode || "immediate").trim() || "immediate";
const cachedRecallPayload =
options.cachedRecallPayload && typeof options.cachedRecallPayload === "object"
? options.cachedRecallPayload
: null;
if (cachedRecallPayload?.result) {
runtime.setPendingRecallSendIntent?.(runtime.createRecallInputRecord());
const cachedResult = cachedRecallPayload.result;
const cachedRecentMessages = Array.isArray(cachedRecallPayload.recentMessages)
? cachedRecallPayload.recentMessages.map((item) => String(item || ""))
: recentMessages;
const applied = runtime.applyRecallInjection(
settings,
recallInput,
cachedRecentMessages,
cachedResult,
);
runtime.consumePlannerRecallHandoff?.(cachedRecallPayload.chatId, {
handoffId: cachedRecallPayload.handoffId,
});
return runtime.createRecallRunResult("completed", {
reason: cachedRecallPayload.reason || "planner-handoff-reused",
selectedNodeIds: cachedResult.selectedNodeIds || [],
injectionText: applied?.injectionText || "",
retrievalMeta: applied?.retrievalMeta || {},
llmMeta: applied?.llmMeta || {},
transport: applied?.transport || {
applied: false,
source: "none",
mode: "none",
},
deliveryMode:
applied?.deliveryMode ||
String(recallInput?.deliveryMode || "immediate").trim() ||
"immediate",
source: recallInput?.source || cachedRecallPayload.source || "",
sourceLabel: recallInput?.sourceLabel || cachedRecallPayload.sourceLabel || "",
authoritativeInputUsed: Boolean(recallInput?.authoritativeInputUsed),
boundUserFloorText: String(recallInput?.boundUserFloorText || ""),
hookName: recallInput?.hookName || "",
sourceCandidates: Array.isArray(recallInput?.sourceCandidates)
? recallInput.sourceCandidates.map((candidate) => ({ ...candidate }))
: [],
stats: cachedResult?.stats || {},
});
}
const earlyPersistedReuse = resolveReusablePersistedRecallRecord(
chat,
recallInput,
runtime,
);
if (earlyPersistedReuse) {
const normalizedBoundUserFloorText =
typeof runtime.normalizeRecallInputText === "function"
? runtime.normalizeRecallInputText(
earlyPersistedReuse.record.boundUserFloorText ||
recallInput.boundUserFloorText ||
"",
)
: String(
earlyPersistedReuse.record.boundUserFloorText ||
recallInput.boundUserFloorText ||
"",
)
.replace(/\r\n/g, "\n")
.trim();
const effectiveRecallInput = {
...recallInput,
source: "persisted-user-floor",
sourceLabel: "复用用户楼层召回",
reason: "persisted-user-floor-reuse",
authoritativeInputUsed: Boolean(
earlyPersistedReuse.record.authoritativeInputUsed ||
recallInput.authoritativeInputUsed,
),
boundUserFloorText: normalizedBoundUserFloorText,
};
const reusedResult = buildPersistedRecallReuseResult(earlyPersistedReuse.record);
const applied = runtime.applyRecallInjection(
settings,
effectiveRecallInput,
recentMessages,
reusedResult,
);
const bumpedRecord =
typeof runtime.bumpPersistedRecallGenerationCount === "function"
? runtime.bumpPersistedRecallGenerationCount(
chat,
earlyPersistedReuse.targetUserMessageIndex,
)
: null;
if (bumpedRecord) {
runtime.triggerChatMetadataSave?.(context, { immediate: false });
runtime.schedulePersistedRecallMessageUiRefresh?.();
}
return runtime.createRecallRunResult("completed", {
reason: "persisted-user-floor-reused",
selectedNodeIds: reusedResult.selectedNodeIds || [],
injectionText: applied?.injectionText || reusedResult.injectionText || "",
retrievalMeta: applied?.retrievalMeta || reusedResult.meta?.retrieval || {},
llmMeta: applied?.llmMeta || reusedResult.meta?.retrieval?.llm || {},
transport: applied?.transport || {
applied: false,
source: "none",
mode: "none",
},
deliveryMode:
applied?.deliveryMode ||
String(effectiveRecallInput?.deliveryMode || "immediate").trim() ||
"immediate",
source: effectiveRecallInput.source || "",
sourceLabel: effectiveRecallInput.sourceLabel || "",
authoritativeInputUsed: Boolean(effectiveRecallInput.authoritativeInputUsed),
boundUserFloorText: String(effectiveRecallInput.boundUserFloorText || ""),
hookName: effectiveRecallInput.hookName || "",
sourceCandidates: Array.isArray(effectiveRecallInput.sourceCandidates)
? effectiveRecallInput.sourceCandidates.map((candidate) => ({ ...candidate }))
: [],
stats: reusedResult?.stats || {},
recallInput: String(earlyPersistedReuse.record.recallInput || ""),
});
}
const runId = runtime.nextRecallRunSequence();
let recallPromise = null;
recallPromise = (async () => {
@@ -565,29 +716,6 @@ export async function runRecallController(runtime, options = {}) {
try {
await runtime.ensureVectorReadyIfNeeded("pre-recall", recallSignal);
const recentContextMessageLimit = runtime.clampInt(
settings.recallLlmContextMessages,
4,
0,
20,
);
const recallInput = runtime.resolveRecallInput(
chat,
recentContextMessageLimit,
options,
);
const userMessage = recallInput.userMessage;
const recentMessages = recallInput.recentMessages;
if (!userMessage) {
return runtime.createRecallRunResult("skipped", {
reason: "当前没有可用于召回的用户输入",
});
}
recallInput.hookName = options.hookName || "";
recallInput.deliveryMode =
String(options.deliveryMode || "immediate").trim() || "immediate";
debugLog("[ST-BME] 开始召回", {
source: recallInput.source,
@@ -614,143 +742,6 @@ export async function runRecallController(runtime, options = {}) {
runtime.setPendingRecallSendIntent(runtime.createRecallInputRecord());
}
const cachedRecallPayload =
options.cachedRecallPayload &&
typeof options.cachedRecallPayload === "object"
? options.cachedRecallPayload
: null;
if (cachedRecallPayload?.result) {
// Cached planner handoff is already the authoritative source for this
// generation, so any leftover send-intent snapshot must be cleared to
// avoid leaking stale input into a later fallback recall path.
runtime.setPendingRecallSendIntent?.(runtime.createRecallInputRecord());
const cachedResult = cachedRecallPayload.result;
const recentMessages = Array.isArray(cachedRecallPayload.recentMessages)
? cachedRecallPayload.recentMessages.map((item) => String(item || ""))
: recallInput.recentMessages;
const applied = runtime.applyRecallInjection(
settings,
recallInput,
recentMessages,
cachedResult,
);
runtime.consumePlannerRecallHandoff?.(cachedRecallPayload.chatId, {
handoffId: cachedRecallPayload.handoffId,
});
return runtime.createRecallRunResult("completed", {
reason: cachedRecallPayload.reason || "planner-handoff-reused",
selectedNodeIds: cachedResult.selectedNodeIds || [],
injectionText: applied?.injectionText || "",
retrievalMeta: applied?.retrievalMeta || {},
llmMeta: applied?.llmMeta || {},
transport: applied?.transport || {
applied: false,
source: "none",
mode: "none",
},
deliveryMode:
applied?.deliveryMode ||
String(recallInput?.deliveryMode || "immediate").trim() ||
"immediate",
source: recallInput?.source || cachedRecallPayload.source || "",
sourceLabel:
recallInput?.sourceLabel || cachedRecallPayload.sourceLabel || "",
authoritativeInputUsed: Boolean(recallInput?.authoritativeInputUsed),
boundUserFloorText: String(recallInput?.boundUserFloorText || ""),
hookName: recallInput?.hookName || "",
sourceCandidates: Array.isArray(recallInput?.sourceCandidates)
? recallInput.sourceCandidates.map((candidate) => ({
...candidate,
}))
: [],
stats: cachedResult?.stats || {},
});
}
const persistedReuse = resolveReusablePersistedRecallRecord(
chat,
recallInput,
runtime,
);
if (persistedReuse) {
const normalizedBoundUserFloorText =
typeof runtime.normalizeRecallInputText === "function"
? runtime.normalizeRecallInputText(
persistedReuse.record.boundUserFloorText ||
recallInput.boundUserFloorText ||
"",
)
: String(
persistedReuse.record.boundUserFloorText ||
recallInput.boundUserFloorText ||
"",
)
.replace(/\r\n/g, "\n")
.trim();
const effectiveRecallInput = {
...recallInput,
source: "persisted-user-floor",
sourceLabel: "复用用户楼层召回",
reason: "persisted-user-floor-reuse",
authoritativeInputUsed: Boolean(
persistedReuse.record.authoritativeInputUsed ||
recallInput.authoritativeInputUsed,
),
boundUserFloorText: normalizedBoundUserFloorText,
};
const reusedResult = buildPersistedRecallReuseResult(persistedReuse.record);
const applied = runtime.applyRecallInjection(
settings,
effectiveRecallInput,
recentMessages,
reusedResult,
);
const bumpedRecord =
typeof runtime.bumpPersistedRecallGenerationCount === "function"
? runtime.bumpPersistedRecallGenerationCount(
chat,
persistedReuse.targetUserMessageIndex,
)
: null;
if (bumpedRecord) {
runtime.triggerChatMetadataSave?.(context, { immediate: false });
runtime.schedulePersistedRecallMessageUiRefresh?.();
}
return runtime.createRecallRunResult("completed", {
reason: "persisted-user-floor-reused",
selectedNodeIds: reusedResult.selectedNodeIds || [],
injectionText: applied?.injectionText || reusedResult.injectionText || "",
retrievalMeta: applied?.retrievalMeta || reusedResult.meta?.retrieval || {},
llmMeta:
applied?.llmMeta || reusedResult.meta?.retrieval?.llm || {},
transport: applied?.transport || {
applied: false,
source: "none",
mode: "none",
},
deliveryMode:
applied?.deliveryMode ||
String(effectiveRecallInput?.deliveryMode || "immediate").trim() ||
"immediate",
source: effectiveRecallInput.source || "",
sourceLabel: effectiveRecallInput.sourceLabel || "",
authoritativeInputUsed: Boolean(
effectiveRecallInput.authoritativeInputUsed,
),
boundUserFloorText: String(
effectiveRecallInput.boundUserFloorText || "",
),
hookName: effectiveRecallInput.hookName || "",
sourceCandidates: Array.isArray(effectiveRecallInput.sourceCandidates)
? effectiveRecallInput.sourceCandidates.map((candidate) => ({
...candidate,
}))
: [],
stats: reusedResult?.stats || {},
recallInput: String(persistedReuse.record.recallInput || ""),
});
}
const result = await runtime.retrieve({
graph: runtime.getCurrentGraph(),
userMessage,

View File

@@ -312,6 +312,8 @@ const validRecord = buildPersistedRecallRecord({
writePersistedRecallToUserMessage(rerollChat, 0, validRecord);
let retrieveCalled = false;
let rerollEnsureVectorReadyCalled = false;
const rerollStatusLabels = [];
const rerollRuntime = {
getIsRecalling: () => false,
getCurrentGraph: () => ({ nodes: [], edges: [] }),
@@ -331,7 +333,9 @@ const rerollRuntime = {
setIsRecalling: () => {},
setActiveRecallPromise: () => {},
getActiveRecallPromise: () => null,
setLastRecallStatus: () => {},
setLastRecallStatus: (label) => {
rerollStatusLabels.push(String(label || ""));
},
clampInt: (v, f, mn, mx) => {
const n = Number(v);
if (!Number.isFinite(n)) return f;
@@ -355,7 +359,9 @@ const rerollRuntime = {
triggerChatMetadataSave: () => {},
schedulePersistedRecallMessageUiRefresh: () => {},
refreshPanelLiveState: () => {},
ensureVectorReadyIfNeeded: async () => {},
ensureVectorReadyIfNeeded: async () => {
rerollEnsureVectorReadyCalled = true;
},
resolveRecallInput: (chat, limit, override) => {
// Simulate resolveRecallInputController override path
const overrideText = normalizeRecallInputText(
@@ -424,6 +430,16 @@ assert.equal(
false,
"retrieve() should NOT be called when persisted record is reused",
);
assert.equal(
rerollEnsureVectorReadyCalled,
false,
"persisted reroll reuse should not even prepare vectors before reusing the user-floor record",
);
assert.equal(
rerollStatusLabels.includes("召回中"),
false,
"persisted reroll reuse should not enter visible fresh recall state",
);
assert.equal(
rerollResult.injectionText,
"注入:明日去摩耶山看夜景",
@@ -583,6 +599,57 @@ assert.equal(
console.log(" ✓ runRecallController does not reuse unbound record for active input");
const activeInputBoundChat = [
{ is_user: true, mes: "主动新输入绑定记录也不应复用" },
{ is_user: false, mes: "上一条回复。", is_system: false },
];
const activeInputBoundRecord = buildPersistedRecallRecord({
injectionText: "旧注入:主动新输入绑定记录也不应复用",
selectedNodeIds: ["node-active-bound-old"],
recallInput: "主动新输入绑定记录也不应复用",
recallSource: "chat-last-user",
hookName: "GENERATION_AFTER_COMMANDS",
tokenEstimate: 5,
manuallyEdited: false,
boundUserFloorText: "主动新输入绑定记录也不应复用",
});
writePersistedRecallToUserMessage(activeInputBoundChat, 0, activeInputBoundRecord);
let activeInputBoundRetrieveCalled = false;
const activeInputBoundRuntime = {
...rerollRuntime,
getContext: () => ({ chat: activeInputBoundChat, chatId: "chat-active-input-bound" }),
retrieve: async () => {
activeInputBoundRetrieveCalled = true;
return {
injectionText: "新召回:主动新输入绑定记录也不应复用",
selectedNodeIds: ["node-active-bound-new"],
};
},
};
const activeInputBoundResult = await runRecallController(activeInputBoundRuntime, {
overrideUserMessage: "主动新输入绑定记录也不应复用",
generationType: "normal",
targetUserMessageIndex: 0,
overrideSource: "send-intent",
hookName: "GENERATION_AFTER_COMMANDS",
deliveryMode: "immediate",
});
assert.equal(
activeInputBoundRetrieveCalled,
true,
"active send-intent input should not reuse even a bound target user-floor record",
);
assert.equal(
activeInputBoundResult.injectionText,
"新召回:主动新输入绑定记录也不应复用",
"active send-intent input should force fresh recall for bound records",
);
console.log(" ✓ runRecallController does not reuse bound record for active input");
const mismatchedBoundChat = [
{ is_user: true, mes: "已经编辑过的新楼层" },
{ is_user: false, mes: "上一条回复。", is_system: false },