From 8cfeae04610a3380594dfa1bc307576abf9555d3 Mon Sep 17 00:00:00 2001 From: youzini Date: Sun, 31 May 2026 20:22:13 +0000 Subject: [PATCH] fix(recall): reuse reroll recall from host generation type --- index.js | 18 --- package.json | 1 - retrieval/recall-controller.js | 11 +- retrieval/recall-persistence.js | 9 +- runtime/reroll-recall-input.js | 116 +++----------------- tests/graph-persistence.mjs | 6 - tests/helpers/generation-recall-harness.mjs | 10 -- tests/recall-reroll-reuse.mjs | 25 ++--- tests/reroll-transaction-boundary.mjs | 81 -------------- 9 files changed, 43 insertions(+), 234 deletions(-) delete mode 100644 tests/reroll-transaction-boundary.mjs diff --git a/index.js b/index.js index bbe8b16..42f18e4 100644 --- a/index.js +++ b/index.js @@ -156,10 +156,6 @@ import { resolvePersistenceChatIdCore, resolveRuntimeGraphFallbackIdentityCore, } from "./runtime/identity-resolver.js"; -import { - consumeRerollRecallReuseMarker, - createRerollRecallReuseMarker, -} from "./runtime/reroll-transaction-boundary.js"; import { createRecallInputState } from "./runtime/recall-input-state.js"; import { createRerollRecallInput } from "./runtime/reroll-recall-input.js"; import { createGenerationContextTracker } from "./runtime/generation-context.js"; @@ -1666,8 +1662,6 @@ const rerollRecallInput = createRerollRecallInput({ clearPendingHostGenerationInputSnapshot(...args), clearPendingRecallSendIntent: (...args) => clearPendingRecallSendIntent(...args), console, - consumeRerollRecallReuseMarker, - createRerollRecallReuseMarker, createTrivialRecallSkipSentinel: (...args) => createTrivialRecallSkipSentinel(...args), findLatestUserChatMessageWithIndex: (...args) => @@ -14581,22 +14575,10 @@ function getLastNonSystemChatMessage(chat) { return null; } -function getPendingRerollRecallReuse() { - return rerollRecallInput.getPendingRerollRecallReuse(); -} - function clearPendingRerollRecallReuse(reason = "") { return rerollRecallInput.clearPendingRerollRecallReuse(reason); } -function prepareRerollRecallReuse({ fromFloor = null, meta = null } = {}) { - return rerollRecallInput.prepareRerollRecallReuse({ fromFloor, meta }); -} - -function consumePendingRerollRecallReuse(chat = getContext()?.chat) { - return rerollRecallInput.consumePendingRerollRecallReuse(chat); -} - function buildRecallRecentMessages(chat, limit, syntheticUserMessage = "") { return buildRecallRecentMessagesController( chat, diff --git a/package.json b/package.json index 1b99485..c4be88f 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ "test:graph-snapshot-upgrade": "node tests/graph-snapshot-upgrade.mjs", "test:snapshot-forward-compat": "node tests/snapshot-forward-compat.mjs", "test:luker-snapshot-forward-compat": "node tests/luker-snapshot-forward-compat.mjs", - "test:reroll-transaction-boundary": "node tests/reroll-transaction-boundary.mjs", "test:vector-gate": "node tests/vector-gate.mjs", "test:hide-engine": "node tests/hide-engine.mjs", "test:maintenance-journal": "node tests/maintenance-journal.mjs", diff --git a/retrieval/recall-controller.js b/retrieval/recall-controller.js index f34255b..3b3fe49 100644 --- a/retrieval/recall-controller.js +++ b/retrieval/recall-controller.js @@ -118,7 +118,14 @@ function resolveReusablePersistedRecallRecord(chat, recallInput, runtime) { "planner-handoff", ]); const isActiveInputSource = activeInputSources.has(recallSource); - if (isActiveInputSource) { + const noNewUserGenerationTypes = new Set([ + "swipe", + "regenerate", + "continue", + "history", + ]); + const isNoNewUserGeneration = noNewUserGenerationTypes.has(generationType); + if (isActiveInputSource && !isNoNewUserGeneration) { return null; } @@ -203,7 +210,7 @@ function resolveReusablePersistedRecallRecord(chat, recallInput, runtime) { "persisted-user-floor", ]); const canTrustUserFloorRecord = Boolean( - !isActiveInputSource && + (!isActiveInputSource || isNoNewUserGeneration) && !boundUserFloorText && (generationType !== "normal" || userFloorSources.has(recallSource)), ); diff --git a/retrieval/recall-persistence.js b/retrieval/recall-persistence.js index f1fa370..638699d 100644 --- a/retrieval/recall-persistence.js +++ b/retrieval/recall-persistence.js @@ -134,10 +134,15 @@ export function bumpPersistedRecallGenerationCount(chat, userMessageIndex) { export function resolveGenerationTargetUserMessageIndex( chat, - { generationType = "normal" } = {}, + { generationType = "normal", generationContext = null } = {}, ) { if (!Array.isArray(chat) || chat.length === 0) return null; - return resolveGenerationParentUserFloor(chat, { type: generationType }); + return resolveGenerationParentUserFloor( + chat, + generationContext && typeof generationContext === "object" + ? { ...generationContext, type: generationContext.type || generationType } + : { type: generationType }, + ); } export function resolveFinalRecallInjectionSource({ diff --git a/runtime/reroll-recall-input.js b/runtime/reroll-recall-input.js index 8239db8..df7cde7 100644 --- a/runtime/reroll-recall-input.js +++ b/runtime/reroll-recall-input.js @@ -1,8 +1,6 @@ export function createRerollRecallInput(deps = {}) { - let pendingRerollRecallReuse = null; const plannerRecallHandoffs = new Map(); - const getContext = (...args) => deps.getContext?.(...args); const getCurrentChatId = (...args) => deps.getCurrentChatId?.(...args); const normalizeChatIdCandidate = (value = "") => deps.normalizeChatIdCandidate?.(value) ?? String(value ?? "").trim(); @@ -13,94 +11,13 @@ export function createRerollRecallInput(deps = {}) { deps.getLastRecallSentUserMessage?.() || {}; const getPendingRecallSendIntent = () => deps.getPendingRecallSendIntent?.() || {}; - const getGenerationRecallTransactionTtlMs = () => - Number.isFinite(Number(deps.GENERATION_RECALL_TRANSACTION_TTL_MS)) - ? Number(deps.GENERATION_RECALL_TRANSACTION_TTL_MS) - : 60000; const getPlannerRecallHandoffTtlMs = () => Number.isFinite(Number(deps.PLANNER_RECALL_HANDOFF_TTL_MS)) ? Number(deps.PLANNER_RECALL_HANDOFF_TTL_MS) : 60000; - function getPendingRerollRecallReuse() { - return pendingRerollRecallReuse; - } - function clearPendingRerollRecallReuse(reason = "") { - const previous = pendingRerollRecallReuse; - pendingRerollRecallReuse = null; - return previous; - } - - function prepareRerollRecallReuse({ fromFloor = null, meta = null } = {}) { - const context = getContext(); - const chat = context?.chat; - if (!Array.isArray(chat) || chat.length === 0) { - pendingRerollRecallReuse = null; - return null; - } - - const latestUser = deps.findLatestUserChatMessageWithIndex(chat); - const targetUserMessageIndex = Number.isFinite(latestUser?.index) - ? latestUser.index - : null; - if (!Number.isFinite(targetUserMessageIndex)) { - pendingRerollRecallReuse = null; - return null; - } - - const userMessage = chat[targetUserMessageIndex]; - const userText = normalizeRecallInputText(userMessage?.mes || ""); - if (!userText) { - pendingRerollRecallReuse = null; - return null; - } - - const persistedRecord = deps.readPersistedRecallFromUserMessage( - chat, - targetUserMessageIndex, - ); - const chatId = normalizeChatIdCandidate(getCurrentChatId(context)); - const prepared = deps.createRerollRecallReuseMarker({ - chatId, - fromFloor, - targetUserMessageIndex, - userText, - persistedRecord, - hashRecallInput, - now: Date.now(), - meta, - }); - if (!prepared.marker) { - pendingRerollRecallReuse = null; - return null; - } - - pendingRerollRecallReuse = prepared.marker; - return pendingRerollRecallReuse; - } - - function consumePendingRerollRecallReuse(chat = getContext()?.chat) { - const reuse = pendingRerollRecallReuse; - if (!reuse) return null; - - const activeChatId = normalizeChatIdCandidate(getCurrentChatId()); - const latestUser = deps.findLatestUserChatMessageWithIndex(chat); - const targetUserMessageIndex = Number.isFinite(latestUser?.index) - ? latestUser.index - : reuse.targetUserMessageIndex; - const userText = normalizeRecallInputText(chat?.[targetUserMessageIndex]?.mes || ""); - const consumed = deps.consumeRerollRecallReuseMarker({ - marker: reuse, - activeChatId, - latestUserMessageIndex: targetUserMessageIndex, - currentUserText: userText, - hashRecallInput, - now: Date.now(), - ttlMs: getGenerationRecallTransactionTtlMs(), - }); - pendingRerollRecallReuse = consumed.marker; - return consumed.consumed ? consumed.override : null; + return null; } function buildGenerationAfterCommandsRecallInput(type, params = {}, chat) { @@ -115,6 +32,7 @@ export function createRerollRecallInput(deps = {}) { const targetUserMessageIndex = deps.resolveGenerationTargetUserMessageIndex(chat, { generationType, + generationContext: params?.generationContext, }); // 对于 history 类型(continue/regenerate/swipe),必须依赖 chat 中的用户消息 @@ -125,7 +43,10 @@ export function createRerollRecallInput(deps = {}) { targetUserMessageIndex: null, }; } - const historyInput = buildHistoryGenerationRecallInput(chat); + const historyInput = buildHistoryGenerationRecallInput(chat, { + generationType, + generationContext: params?.generationContext, + }); if (!historyInput) { return { generationType, @@ -157,11 +78,6 @@ export function createRerollRecallInput(deps = {}) { } function buildNormalGenerationRecallInput(chat, options = {}) { - const rerollReuse = consumePendingRerollRecallReuse(chat); - if (rerollReuse) { - return rerollReuse; - } - const lastNonSystemMessage = deps.getLastNonSystemChatMessage(chat); const tailUserText = lastNonSystemMessage?.is_user ? normalizeRecallInputText(lastNonSystemMessage?.mes || "") @@ -294,19 +210,24 @@ export function createRerollRecallInput(deps = {}) { }; } - function buildHistoryGenerationRecallInput(chat) { + function buildHistoryGenerationRecallInput(chat, options = {}) { + const generationType = String(options?.generationType || "history").trim() || "history"; const lastRecallSentUserMessage = getLastRecallSentUserMessage(); + const targetUserMessageIndex = deps.resolveGenerationTargetUserMessageIndex(chat, { + generationType, + generationContext: options?.generationContext, + }); + const targetUserText = Number.isFinite(targetUserMessageIndex) + ? normalizeRecallInputText(chat?.[targetUserMessageIndex]?.mes || "") + : ""; const latestUserText = normalizeRecallInputText( - deps.getLatestUserChatMessage(chat)?.mes || lastRecallSentUserMessage.text, + targetUserText || deps.getLatestUserChatMessage(chat)?.mes || lastRecallSentUserMessage.text, ); if (!latestUserText) return null; - const targetUserMessageIndex = deps.resolveGenerationTargetUserMessageIndex(chat, { - generationType: "history", - }); return { overrideUserMessage: latestUserText, - generationType: "history", + generationType, targetUserMessageIndex, overrideSource: Number.isFinite(targetUserMessageIndex) ? "chat-last-user" @@ -429,10 +350,7 @@ export function createRerollRecallInput(deps = {}) { } return { - prepareRerollRecallReuse, - getPendingRerollRecallReuse, clearPendingRerollRecallReuse, - consumePendingRerollRecallReuse, buildNormalGenerationRecallInput, buildHistoryGenerationRecallInput, buildGenerationAfterCommandsRecallInput, diff --git a/tests/graph-persistence.mjs b/tests/graph-persistence.mjs index ad8e78c..316c745 100644 --- a/tests/graph-persistence.mjs +++ b/tests/graph-persistence.mjs @@ -201,10 +201,6 @@ import { syncGraphLoadFromLiveContextImpl, writeAuthorityCheckpointFromCurrentGraphImpl, } from "../sync/graph-load-persist.js"; -import { - consumeRerollRecallReuseMarker, - createRerollRecallReuseMarker, -} from "../runtime/reroll-transaction-boundary.js"; function isAuthorityVectorConfig(config = null) { return config?.mode === "authority" || config?.source === "authority-trivium"; @@ -892,8 +888,6 @@ async function createGraphPersistenceHarness({ shouldUseAuthorityJobsImpl, syncGraphLoadFromLiveContextImpl, writeAuthorityCheckpointFromCurrentGraphImpl, - consumeRerollRecallReuseMarker, - createRerollRecallReuseMarker, createRecallMessageUiController() { return { refreshPersistedRecallMessageUi: () => ({ diff --git a/tests/helpers/generation-recall-harness.mjs b/tests/helpers/generation-recall-harness.mjs index eb52035..ada8bf8 100644 --- a/tests/helpers/generation-recall-harness.mjs +++ b/tests/helpers/generation-recall-harness.mjs @@ -34,10 +34,6 @@ import { shouldRunRecallForTransaction, } from "../../ui/ui-status.js"; import { defaultSettings, mergePersistedSettings } from "../../runtime/settings-defaults.js"; -import { - consumeRerollRecallReuseMarker, - createRerollRecallReuseMarker, -} from "../../runtime/reroll-transaction-boundary.js"; import { createRecallInputState } from "../../runtime/recall-input-state.js"; import { createRerollRecallInput } from "../../runtime/reroll-recall-input.js"; import { createGenerationRecallTransactions } from "../../runtime/generation-recall-transactions.js"; @@ -487,8 +483,6 @@ export async function createGenerationRecallHarness(options = {}) { clearPendingRecallSendIntent: (...args) => recallInputState.clearPendingRecallSendIntent(...args), console, - consumeRerollRecallReuseMarker, - createRerollRecallReuseMarker, createTrivialRecallSkipSentinel, findLatestUserChatMessageWithIndex, formatInjection, @@ -840,10 +834,6 @@ export async function createGenerationRecallHarness(options = {}) { recallInputState.getPendingHostGenerationInputSnapshot(...args), clearPendingHostGenerationInputSnapshot: (...args) => recallInputState.clearPendingHostGenerationInputSnapshot(...args), - prepareRerollRecallReuse: (...args) => - rerollRecallInput.prepareRerollRecallReuse(...args), - getPendingRerollRecallReuse: (...args) => - rerollRecallInput.getPendingRerollRecallReuse(...args), clearPendingRerollRecallReuse, recordRecallSendIntent: (...args) => recallInputState.recordRecallSendIntent(...args), diff --git a/tests/recall-reroll-reuse.mjs b/tests/recall-reroll-reuse.mjs index a018cb0..edae416 100644 --- a/tests/recall-reroll-reuse.mjs +++ b/tests/recall-reroll-reuse.mjs @@ -534,11 +534,6 @@ writePersistedRecallToUserMessage( }), ); -const preparedRerollReuse = rerollInputHarness.result.prepareRerollRecallReuse({ - fromFloor: 1, -}); -assert.ok(preparedRerollReuse, "assistant-only reroll should prepare recall reuse marker"); - rerollInputHarness.result.recordRecallSendIntent( "错误的主动输入不应覆盖 reroll 用户楼", "send-intent", @@ -548,11 +543,16 @@ rerollInputHarness.result.freezeHostGenerationInputSnapshot( "host-generation-lifecycle", ); -const rerollReplacementInput = rerollInputHarness.result.buildNormalGenerationRecallInput( - rerollInputHarness.chat, +const rerollReplacementInput = rerollInputHarness.result.buildGenerationAfterCommandsRecallInput( + "swipe", { - frozenInputSnapshot: rerollInputHarness.result.getPendingHostGenerationInputSnapshot(), + generationContext: { + type: "swipe", + kind: "no-new-user", + swipedAssistantFloor: 1, + }, }, + rerollInputHarness.chat, ); assert.equal( rerollReplacementInput.overrideUserMessage, @@ -560,14 +560,9 @@ assert.equal( "reroll replacement should ignore stale live input sources and bind to stable user floor", ); assert.equal(rerollReplacementInput.overrideSource, "chat-last-user"); -assert.equal(rerollReplacementInput.overrideReason, "reroll-user-floor-reuse"); -assert.equal( - rerollInputHarness.result.getPendingRerollRecallReuse(), - null, - "reroll reuse marker should be one-shot after binding recall input", -); +assert.equal(rerollReplacementInput.generationType, "swipe"); -console.log(" ✓ reroll replacement normal input is forced to stable user-floor recall source"); +console.log(" ✓ reroll replacement input is forced by host type to stable user-floor recall source"); const legacyUnboundReuseChat = [ { is_user: true, mes: "旧记录没有绑定楼层" }, diff --git a/tests/reroll-transaction-boundary.mjs b/tests/reroll-transaction-boundary.mjs deleted file mode 100644 index def4601..0000000 --- a/tests/reroll-transaction-boundary.mjs +++ /dev/null @@ -1,81 +0,0 @@ -// ST-BME restrained rebirth — Phase 4 reroll transaction boundary tests. - -import assert from "node:assert/strict"; -import { - consumeRerollRecallReuseMarker, - createRerollRecallReuseMarker, -} from "../runtime/reroll-transaction-boundary.js"; - -const hashRecallInput = (text) => `h:${String(text || "").trim()}`; - -const prepared = createRerollRecallReuseMarker({ - chatId: "chat-a", - fromFloor: 4, - targetUserMessageIndex: 2, - userText: " hello\n", - persistedRecord: { - injectionText: "memory", - boundUserFloorText: "hello", - }, - hashRecallInput, - now: 1000, -}); -assert.equal(prepared.reason, "prepared"); -assert.equal(prepared.marker.chatId, "chat-a"); -assert.equal(prepared.marker.fromFloor, 4); -assert.equal(prepared.marker.targetUserMessageIndex, 2); -assert.equal(prepared.marker.userHash, "h:hello"); - -const consumed = consumeRerollRecallReuseMarker({ - marker: prepared.marker, - activeChatId: "chat-a", - latestUserMessageIndex: 2, - currentUserText: "hello", - hashRecallInput, - now: 1500, - ttlMs: 5000, -}); -assert.equal(consumed.consumed, true); -assert.equal(consumed.override.rerollRecallReuse, true); -assert.equal(consumed.override.targetUserMessageIndex, 2); - -console.log(" ✓ reroll recall reuse marker is one-shot and floor-bound"); - -assert.equal( - createRerollRecallReuseMarker({ - userText: "changed", - persistedRecord: { injectionText: "memory", boundUserFloorText: "original" }, - }).reason, - "bound-user-floor-mismatch", -); -assert.equal( - createRerollRecallReuseMarker({ - userText: "hello", - persistedRecord: { injectionText: "" }, - }).reason, - "missing-persisted-recall", -); - -for (const [caseName, options, reason] of [ - ["chat", { activeChatId: "other-chat" }, "chat-mismatch"], - ["ttl", { now: 7001, ttlMs: 5000 }, "expired"], - ["floor", { latestUserMessageIndex: 3 }, "target-user-floor-changed"], - ["text", { currentUserText: "changed" }, "user-text-changed"], -]) { - const result = consumeRerollRecallReuseMarker({ - marker: prepared.marker, - activeChatId: "chat-a", - latestUserMessageIndex: 2, - currentUserText: "hello", - hashRecallInput, - now: 1500, - ttlMs: 5000, - ...options, - }); - assert.equal(result.consumed, false, `${caseName} mismatch must reject reuse`); - assert.equal(result.reason, reason); - assert.equal(result.marker, null, `${caseName} mismatch must clear marker`); -} - -console.log(" ✓ reroll marker rejects stale, cross-chat, changed-floor, changed-text reuse"); -console.log("reroll-transaction-boundary tests passed");