From a5c4e12a76c07b72c729fe41d783828484f4b58a Mon Sep 17 00:00:00 2001 From: opencode Date: Fri, 15 May 2026 19:17:30 +0000 Subject: [PATCH] fix(recall): skip pre-recall recovery on reroll reuse --- retrieval/recall-controller.js | 41 +++++++++++++++++----------------- tests/recall-reroll-reuse.mjs | 17 +++++++++++++- 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/retrieval/recall-controller.js b/retrieval/recall-controller.js index 4d07c45..f34255b 100644 --- a/retrieval/recall-controller.js +++ b/retrieval/recall-controller.js @@ -513,26 +513,6 @@ export async function runRecallController(runtime, options = {}) { reason: "召回功能未启用", }); } - const isReadableForRecall = - typeof runtime.isGraphReadableForRecall === "function" - ? runtime.isGraphReadableForRecall() - : runtime.isGraphReadable(); - if (!isReadableForRecall) { - const reason = runtime.getGraphMutationBlockReason("召回"); - runtime.setLastRecallStatus("等待图谱加载", reason, "warning", { - syncRuntime: true, - }); - return runtime.createRecallRunResult("skipped", { - reason, - }); - } - if (runtime.isGraphMetadataWriteAllowed()) { - if (!(await runtime.recoverHistoryIfNeeded("pre-recall"))) { - return runtime.createRecallRunResult("skipped", { - reason: "历史恢复未就绪", - }); - } - } const context = runtime.getContext(); const chat = context.chat; @@ -690,6 +670,27 @@ export async function runRecallController(runtime, options = {}) { }); } + const isReadableForRecall = + typeof runtime.isGraphReadableForRecall === "function" + ? runtime.isGraphReadableForRecall() + : runtime.isGraphReadable(); + if (!isReadableForRecall) { + const reason = runtime.getGraphMutationBlockReason("召回"); + runtime.setLastRecallStatus("等待图谱加载", reason, "warning", { + syncRuntime: true, + }); + return runtime.createRecallRunResult("skipped", { + reason, + }); + } + if (runtime.isGraphMetadataWriteAllowed()) { + if (!(await runtime.recoverHistoryIfNeeded("pre-recall"))) { + return runtime.createRecallRunResult("skipped", { + reason: "历史恢复未就绪", + }); + } + } + const runId = runtime.nextRecallRunSequence(); let recallPromise = null; recallPromise = (async () => { diff --git a/tests/recall-reroll-reuse.mjs b/tests/recall-reroll-reuse.mjs index 17c0717..71c89be 100644 --- a/tests/recall-reroll-reuse.mjs +++ b/tests/recall-reroll-reuse.mjs @@ -313,6 +313,7 @@ writePersistedRecallToUserMessage(rerollChat, 0, validRecord); let retrieveCalled = false; let rerollEnsureVectorReadyCalled = false; +let rerollRecoverHistoryCalled = false; const rerollStatusLabels = []; const rerollRuntime = { getIsRecalling: () => false, @@ -325,7 +326,10 @@ const rerollRuntime = { }), isGraphReadableForRecall: () => true, isGraphMetadataWriteAllowed: () => true, - recoverHistoryIfNeeded: async () => true, + recoverHistoryIfNeeded: async () => { + rerollRecoverHistoryCalled = true; + return true; + }, getContext: () => ({ chat: rerollChat, chatId: "chat-reroll" }), nextRecallRunSequence: () => 1, beginStageAbortController: () => ({ signal: { aborted: false } }), @@ -435,6 +439,11 @@ assert.equal( false, "persisted reroll reuse should not even prepare vectors before reusing the user-floor record", ); +assert.equal( + rerollRecoverHistoryCalled, + false, + "persisted reroll reuse should not trigger pre-recall history recovery", +); assert.equal( rerollStatusLabels.includes("召回中"), false, @@ -599,6 +608,7 @@ assert.equal( console.log(" ✓ runRecallController does not reuse unbound record for active input"); +rerollRecoverHistoryCalled = false; const activeInputBoundChat = [ { is_user: true, mes: "主动新输入绑定记录也不应复用" }, { is_user: false, mes: "上一条回复。", is_system: false }, @@ -642,6 +652,11 @@ assert.equal( true, "active send-intent input should not reuse even a bound target user-floor record", ); +assert.equal( + rerollRecoverHistoryCalled, + true, + "active send-intent input should still run normal pre-recall history recovery before fresh recall", +); assert.equal( activeInputBoundResult.injectionText, "新召回:主动新输入绑定记录也不应复用",