fix(recall): reuse persisted recall on reroll

This commit is contained in:
Youzini-afk
2026-04-30 03:30:06 +08:00
parent aad44c1280
commit a9f575d98a
4 changed files with 195 additions and 18 deletions

View File

@@ -5250,6 +5250,12 @@ function persistRecallInjectionRecord({
return null;
}
const targetUserFloorText = normalizeRecallInputText(
chat[resolvedTargetIndex]?.mes || "",
);
const boundUserFloorText = normalizeRecallInputText(
recallInput?.boundUserFloorText || targetUserFloorText,
);
const record = buildPersistedRecallRecord(
{
injectionText,
@@ -5259,6 +5265,8 @@ function persistRecallInjectionRecord({
hookName: String(recallInput?.hookName || ""),
tokenEstimate,
manuallyEdited: false,
authoritativeInputUsed: Boolean(recallInput?.authoritativeInputUsed),
boundUserFloorText,
},
readPersistedRecallFromUserMessage(chat, resolvedTargetIndex),
);
@@ -5379,11 +5387,34 @@ function ensurePersistedRecallRecordForGeneration({
chat,
targetUserMessageIndex,
);
const nextAuthoritativeInputUsed = Boolean(
recallResult?.authoritativeInputUsed ??
frozenRecallOptions?.authoritativeInputUsed ??
recallOptions?.authoritativeInputUsed,
);
const targetUserFloorText = normalizeRecallInputText(
chat[targetUserMessageIndex]?.mes || "",
);
const nextBoundUserFloorText = normalizeRecallInputText(
recallResult?.boundUserFloorText ||
frozenRecallOptions?.boundUserFloorText ||
recallOptions?.boundUserFloorText ||
targetUserFloorText ||
"",
);
const existingBoundUserFloorText = normalizeRecallInputText(
existingRecord?.boundUserFloorText || "",
);
const existingMetadataUpToDate =
Boolean(existingRecord?.authoritativeInputUsed) === nextAuthoritativeInputUsed &&
(!nextBoundUserFloorText ||
existingBoundUserFloorText === nextBoundUserFloorText);
if (
existingRecord &&
String(existingRecord.injectionText || "").trim() === injectionText &&
areRecallNodeIdListsEqual(existingRecord.selectedNodeIds, selectedNodeIds) &&
String(existingRecord.recallInput || "").trim()
String(existingRecord.recallInput || "").trim() &&
existingMetadataUpToDate
) {
return {
persisted: false,
@@ -5421,17 +5452,8 @@ function ensurePersistedRecallRecordForGeneration({
),
tokenEstimate: estimateTokens(injectionText),
manuallyEdited: false,
authoritativeInputUsed: Boolean(
recallResult?.authoritativeInputUsed ??
frozenRecallOptions?.authoritativeInputUsed ??
recallOptions?.authoritativeInputUsed,
),
boundUserFloorText: String(
recallResult?.boundUserFloorText ||
frozenRecallOptions?.boundUserFloorText ||
recallOptions?.boundUserFloorText ||
"",
),
authoritativeInputUsed: nextAuthoritativeInputUsed,
boundUserFloorText: nextBoundUserFloorText,
},
existingRecord,
);
@@ -20042,10 +20064,14 @@ function createGenerationRecallContext({
transaction.chatId,
);
const transactionRecallKey = String(transaction.recallKey || "").trim();
const peerHookName = getGenerationRecallPeerHookName(hookName);
const hasPeerHookState = Boolean(
peerHookName && transaction.hookStates?.[peerHookName],
);
if (
normalizedTransactionChatId !== normalizedChatId ||
!transactionRecallKey ||
transactionRecallKey !== String(fallbackRecallKey)
(!hasPeerHookState && transactionRecallKey !== String(fallbackRecallKey))
) {
return {
hookName,

View File

@@ -132,8 +132,8 @@ function resolveReusablePersistedRecallRecord(chat, recallInput, runtime) {
);
const matchesCurrentUserFloor = Boolean(
currentUserFloorText &&
recordRecallInput &&
currentUserFloorText === recordRecallInput,
boundUserFloorText &&
currentUserFloorText === boundUserFloorText,
);
if (record.authoritativeInputUsed) {

View File

@@ -269,7 +269,7 @@ export function createGenerationRecallHarness(options = {}) {
};
vm.createContext(context);
vm.runInContext(
`${snippet}\nresult = { hashRecallInput, buildPreGenerationRecallKey, buildGenerationAfterCommandsRecallInput, buildNormalGenerationRecallInput, cleanupGenerationRecallTransactions, buildGenerationRecallTransactionId, beginGenerationRecallTransaction, markGenerationRecallTransactionHookState, shouldRunRecallForTransaction, createGenerationRecallContext, onGenerationStarted, onGenerationEnded, onGenerationAfterCommands, onBeforeCombinePrompts, applyFinalRecallInjectionForGeneration, ensurePersistedRecallRecordForGeneration, findRecentGenerationRecallTransactionForChat, getGenerationRecallTransactionResult, generationRecallTransactions, freezeHostGenerationInputSnapshot, consumeHostGenerationInputSnapshot, getPendingHostGenerationInputSnapshot, clearPendingHostGenerationInputSnapshot, recordRecallSendIntent, clearPendingRecallSendIntent, recordRecallSentUserMessage, getPendingRecallSendIntent: () => pendingRecallSendIntent, getLastRecallSentUserMessage: () => lastRecallSentUserMessage, getCurrentGenerationTrivialSkip, markCurrentGenerationTrivialSkip, clearCurrentGenerationTrivialSkip, consumeCurrentGenerationTrivialSkip, deferAutoExtraction, maybeResumePendingAutoExtraction, clearPendingAutoExtraction, getPendingAutoExtraction: () => ({ ...pendingAutoExtraction }), getIsHostGenerationRunning: () => isHostGenerationRunning, preparePlannerRecallHandoff, runPlannerRecallForEna, getGraphPersistenceState: () => graphPersistenceState, setGraphPersistenceState: (value = {}) => { graphPersistenceState = { ...graphPersistenceState, ...(value || {}) }; return graphPersistenceState; } };`,
`${snippet}\nresult = { hashRecallInput, buildPreGenerationRecallKey, buildGenerationAfterCommandsRecallInput, buildNormalGenerationRecallInput, cleanupGenerationRecallTransactions, buildGenerationRecallTransactionId, beginGenerationRecallTransaction, markGenerationRecallTransactionHookState, shouldRunRecallForTransaction, createGenerationRecallContext, onGenerationStarted, onGenerationEnded, onGenerationAfterCommands, onBeforeCombinePrompts, applyFinalRecallInjectionForGeneration, persistRecallInjectionRecord, ensurePersistedRecallRecordForGeneration, findRecentGenerationRecallTransactionForChat, getGenerationRecallTransactionResult, generationRecallTransactions, freezeHostGenerationInputSnapshot, consumeHostGenerationInputSnapshot, getPendingHostGenerationInputSnapshot, clearPendingHostGenerationInputSnapshot, recordRecallSendIntent, clearPendingRecallSendIntent, recordRecallSentUserMessage, getPendingRecallSendIntent: () => pendingRecallSendIntent, getLastRecallSentUserMessage: () => lastRecallSentUserMessage, getCurrentGenerationTrivialSkip, markCurrentGenerationTrivialSkip, clearCurrentGenerationTrivialSkip, consumeCurrentGenerationTrivialSkip, deferAutoExtraction, maybeResumePendingAutoExtraction, clearPendingAutoExtraction, getPendingAutoExtraction: () => ({ ...pendingAutoExtraction }), getIsHostGenerationRunning: () => isHostGenerationRunning, preparePlannerRecallHandoff, runPlannerRecallForEna, getGraphPersistenceState: () => graphPersistenceState, setGraphPersistenceState: (value = {}) => { graphPersistenceState = { ...graphPersistenceState, ...(value || {}) }; return graphPersistenceState; } };`,
context,
{ filename: indexPath },
);

View File

@@ -38,6 +38,40 @@ Object.assign(harness.settings, {
recallEnabled: true,
});
harness.chat = [
{ is_user: true, mes: "楼层里的稳定用户输入" },
{ is_user: false, mes: "好的。", is_system: false },
];
const persistedWriteResult = harness.result.persistRecallInjectionRecord({
recallInput: {
userMessage: "发送前捕获的权威输入",
source: "send-intent",
hookName: "GENERATION_AFTER_COMMANDS",
targetUserMessageIndex: 0,
authoritativeInputUsed: true,
boundUserFloorText: "楼层里的稳定用户输入",
},
result: {
selectedNodeIds: ["node-write-1"],
},
injectionText: "注入:楼层里的稳定用户输入",
tokenEstimate: 8,
});
assert.ok(persistedWriteResult?.record, "persistRecallInjectionRecord should write a record");
assert.equal(
persistedWriteResult.record.authoritativeInputUsed,
true,
"initial persisted record should keep authoritativeInputUsed",
);
assert.equal(
persistedWriteResult.record.boundUserFloorText,
"楼层里的稳定用户输入",
"initial persisted record should keep boundUserFloorText",
);
console.log(" ✓ persistRecallInjectionRecord stores authoritative input metadata");
// Set up chat: user + assistant
harness.chat = [
{ is_user: true, mes: "去摩耶山看夜景" },
@@ -118,10 +152,77 @@ assert.equal(
console.log(" ✓ ensurePersistedRecallRecordForGeneration overwrites record with empty recallInput");
harness.chat = [
{ is_user: true, mes: "稳定楼层文本" },
{ is_user: false, mes: "好的。", is_system: false },
];
const staleMetadataRecord = buildPersistedRecallRecord({
injectionText: "注入:稳定楼层文本",
selectedNodeIds: ["node-stale-meta"],
recallInput: "发送前捕获文本",
recallSource: "send-intent",
hookName: "GENERATION_AFTER_COMMANDS",
tokenEstimate: 4,
manuallyEdited: false,
});
writePersistedRecallToUserMessage(harness.chat, 0, staleMetadataRecord);
const staleMetadataEnsureResult = harness.result.ensurePersistedRecallRecordForGeneration({
generationType: "regenerate",
recallResult: {
status: "completed",
didRecall: true,
ok: true,
injectionText: "注入:稳定楼层文本",
selectedNodeIds: ["node-stale-meta"],
recallInput: "发送前捕获文本",
source: "send-intent",
hookName: "GENERATION_AFTER_COMMANDS",
authoritativeInputUsed: false,
boundUserFloorText: "稳定楼层文本",
},
transaction: {
frozenRecallOptions: {
generationType: "regenerate",
targetUserMessageIndex: 0,
overrideUserMessage: "稳定楼层文本",
overrideSource: "chat-last-user",
authoritativeInputUsed: false,
boundUserFloorText: "稳定楼层文本",
},
},
recallOptions: {
generationType: "regenerate",
targetUserMessageIndex: 0,
overrideUserMessage: "稳定楼层文本",
authoritativeInputUsed: false,
boundUserFloorText: "稳定楼层文本",
},
hookName: "GENERATION_AFTER_COMMANDS",
});
assert.equal(
staleMetadataEnsureResult.persisted,
true,
"ensure should rewrite records whose metadata is stale even when text/nodeIds match",
);
const repairedMetadataRecord = readPersistedRecallFromUserMessage(harness.chat, 0);
assert.equal(
repairedMetadataRecord.boundUserFloorText,
"稳定楼层文本",
"ensure should repair missing boundUserFloorText",
);
console.log(" ✓ ensurePersistedRecallRecordForGeneration repairs stale metadata");
// ═══════════════════════════════════════════════════════════════
// 2. ensurePersistedRecallRecordForGeneration: populated recallInput skip
// ═══════════════════════════════════════════════════════════════
harness.chat = [
{ is_user: true, mes: "去摩耶山看夜景" },
{ is_user: false, mes: "好的,我们出发吧。", is_system: false },
];
writePersistedRecallToUserMessage(harness.chat, 0, afterRecord);
// Now the record has proper recallInput — calling ensure again should skip
const ensureResult2 = harness.result.ensurePersistedRecallRecordForGeneration({
generationType: "regenerate",
@@ -138,6 +239,56 @@ assert.equal(
console.log(" ✓ ensurePersistedRecallRecordForGeneration skips when recallInput is populated");
harness.chat = [
{ is_user: true, mes: "继续写摩耶山夜景" },
{ is_user: false, mes: "前一次回复。", is_system: false },
];
harness.result.cleanupGenerationRecallTransactions(Date.now() + 60000);
const afterCommandsRecallOptions = harness.result.buildGenerationAfterCommandsRecallInput(
"regenerate",
{},
harness.chat,
);
const afterCommandsContext = harness.result.createGenerationRecallContext({
hookName: "GENERATION_AFTER_COMMANDS",
generationType: "regenerate",
recallOptions: afterCommandsRecallOptions,
});
assert.ok(
afterCommandsContext.transaction,
"after-commands should create a history transaction",
);
harness.result.markGenerationRecallTransactionHookState(
afterCommandsContext.transaction,
"GENERATION_AFTER_COMMANDS",
"completed",
);
const beforeCombineNormalFallback = harness.result.buildNormalGenerationRecallInput(
harness.chat,
);
const beforeCombineContext = harness.result.createGenerationRecallContext({
hookName: "GENERATE_BEFORE_COMBINE_PROMPTS",
generationType: "normal",
recallOptions: beforeCombineNormalFallback,
});
assert.equal(
beforeCombineContext.transaction,
afterCommandsContext.transaction,
"before-combine should reuse the existing history transaction despite normal fallback input",
);
assert.equal(
beforeCombineContext.generationType,
"regenerate",
"before-combine should keep the transaction's history generation type",
);
assert.equal(
beforeCombineContext.shouldRun,
false,
"before-combine should not run recall again after after-commands completed",
);
console.log(" ✓ before-combine reuses existing history transaction");
// ═══════════════════════════════════════════════════════════════
// 3. runRecallController: regenerate reuses persisted record
// ═══════════════════════════════════════════════════════════════
@@ -151,8 +302,8 @@ const rerollChat = [
const validRecord = buildPersistedRecallRecord({
injectionText: "注入:明日去摩耶山看夜景",
selectedNodeIds: ["node-a"],
recallInput: "明日去摩耶山看夜景",
recallSource: "chat-tail-user",
recallInput: "发送意图中的扩展文本,不等于当前用户楼层",
recallSource: "send-intent",
hookName: "GENERATION_AFTER_COMMANDS",
tokenEstimate: 5,
manuallyEdited: false,