fix: finalize deepfix p2 recall binding and p3 validation matrix

This commit is contained in:
Youzini-afk
2026-03-31 03:53:53 +08:00
parent a2bed39e28
commit b0f9d191bd
7 changed files with 465 additions and 20 deletions

View File

@@ -200,13 +200,14 @@ export async function onGenerationAfterCommandsController(
return;
}
const runtimeRecallOptions = recallContext.recallOptions || recallOptions || {};
runtime.markGenerationRecallTransactionHookState(
recallContext.transaction,
recallContext.hookName,
"running",
);
const recallResult = await runtime.runRecall({
...recallOptions,
...runtimeRecallOptions,
recallKey: recallContext.recallKey,
hookName: recallContext.hookName,
signal: params?.signal,
@@ -240,13 +241,14 @@ export async function onBeforeCombinePromptsController(runtime) {
return;
}
const runtimeRecallOptions = recallContext.recallOptions || recallOptions || {};
runtime.markGenerationRecallTransactionHookState(
recallContext.transaction,
recallContext.hookName,
"running",
);
const recallResult = await runtime.runRecall({
...recallOptions,
...runtimeRecallOptions,
recallKey: recallContext.recallKey,
hookName: recallContext.hookName,
});

272
index.js
View File

@@ -483,6 +483,7 @@ const PERSISTED_RECALL_UI_DIAGNOSTIC_THROTTLE_MS = 1500;
const persistedRecallUiDiagnosticTimestamps = new Map();
const persistedRecallPersistDiagnosticTimestamps = new Map();
const GENERATION_RECALL_TRANSACTION_TTL_MS = 15000;
const GENERATION_RECALL_HOOK_BRIDGE_MS = 1200;
const stageNoticeHandles = {
extraction: null,
vector: null,
@@ -4686,10 +4687,12 @@ function buildPreGenerationRecallKey(type, options = {}) {
const seedText =
options.overrideUserMessage || options.userMessage || `@target:${targetUserMessageIndex}`;
const normalizedChatId = normalizeChatIdCandidate(options.chatId || getCurrentChatId());
return [
getCurrentChatId(),
normalizedChatId,
String(type || "normal").trim() || "normal",
hashRecallInput(seedText),
hashRecallInput(seedText || ""),
].join(":");
}
@@ -4707,6 +4710,90 @@ function cleanupGenerationRecallTransactions(now = Date.now()) {
}
}
function getGenerationRecallPeerHookName(hookName = "") {
const normalized = String(hookName || "").trim();
if (normalized === "GENERATION_AFTER_COMMANDS") {
return "GENERATE_BEFORE_COMBINE_PROMPTS";
}
if (normalized === "GENERATE_BEFORE_COMBINE_PROMPTS") {
return "GENERATION_AFTER_COMMANDS";
}
return "";
}
function isGenerationRecallTransactionWithinBridgeWindow(
transaction,
now = Date.now(),
) {
if (!transaction) return false;
return now - Number(transaction.updatedAt || transaction.createdAt || 0) <= GENERATION_RECALL_HOOK_BRIDGE_MS;
}
function normalizeGenerationRecallTransactionType(generationType = "normal") {
const normalized = String(generationType || "normal").trim() || "normal";
return normalized === "normal" ? "normal" : "history";
}
function freezeGenerationRecallOptionsForTransaction(
chat,
generationType = "normal",
recallOptions = {},
) {
if (!Array.isArray(chat)) return null;
const optionGenerationType = String(
recallOptions?.generationType || generationType || "normal",
).trim() || "normal";
const normalizedGenerationType = optionGenerationType;
let targetUserMessageIndex = Number.isFinite(recallOptions?.targetUserMessageIndex)
? Math.floor(Number(recallOptions.targetUserMessageIndex))
: resolveGenerationTargetUserMessageIndex(chat, {
generationType: normalizedGenerationType,
});
if (!Number.isFinite(targetUserMessageIndex)) {
return null;
}
targetUserMessageIndex = Math.floor(targetUserMessageIndex);
const targetUserMessage = chat[targetUserMessageIndex];
if (!targetUserMessage?.is_user) {
return null;
}
const frozenUserMessage = normalizeRecallInputText(
targetUserMessage?.mes ||
recallOptions?.overrideUserMessage ||
recallOptions?.userMessage ||
"",
);
if (!frozenUserMessage) {
return null;
}
const source =
String(recallOptions?.overrideSource || recallOptions?.source || "").trim() ||
(normalizeGenerationRecallTransactionType(normalizedGenerationType) === "normal"
? "chat-tail-user"
: "chat-last-user");
const sourceLabel =
String(
recallOptions?.overrideSourceLabel ||
recallOptions?.sourceLabel ||
getRecallUserMessageSourceLabel(source),
).trim() || getRecallUserMessageSourceLabel(source);
return {
generationType: normalizedGenerationType,
targetUserMessageIndex,
overrideUserMessage: frozenUserMessage,
overrideSource: source,
overrideSourceLabel: sourceLabel,
includeSyntheticUserMessage: false,
};
}
function buildGenerationRecallTransactionId(chatId, generationType, recallKey) {
return [
String(chatId || ""),
@@ -4719,6 +4806,7 @@ function beginGenerationRecallTransaction({
chatId,
generationType = "normal",
recallKey = "",
forceNew = false,
} = {}) {
const normalizedChatId = String(chatId || "");
const normalizedGenerationType =
@@ -4732,20 +4820,98 @@ function beginGenerationRecallTransaction({
normalizedGenerationType,
normalizedRecallKey,
);
const now = Date.now();
const transaction = generationRecallTransactions.get(transactionId) || {
const existingTransaction = generationRecallTransactions.get(transactionId) || null;
if (
existingTransaction &&
isGenerationRecallTransactionWithinBridgeWindow(existingTransaction, now) &&
!forceNew
) {
existingTransaction.updatedAt = now;
generationRecallTransactions.set(transactionId, existingTransaction);
return existingTransaction;
}
const transaction = {
id: transactionId,
chatId: normalizedChatId,
generationType: normalizedGenerationType,
recallKey: normalizedRecallKey,
hookStates: {},
createdAt: now,
frozenRecallOptions: null,
};
transaction.updatedAt = now;
generationRecallTransactions.set(transactionId, transaction);
return transaction;
}
function findRecentGenerationRecallTransactionForChat(
chatId = getCurrentChatId(),
now = Date.now(),
) {
const normalizedChatId = normalizeChatIdCandidate(chatId);
if (!normalizedChatId) return null;
let latestTransaction = null;
for (const transaction of generationRecallTransactions.values()) {
if (!transaction || String(transaction.chatId || "") !== normalizedChatId) continue;
if (!isGenerationRecallTransactionWithinBridgeWindow(transaction, now)) continue;
if (!latestTransaction || Number(transaction.updatedAt || 0) > Number(latestTransaction.updatedAt || 0)) {
latestTransaction = transaction;
}
}
return latestTransaction;
}
function shouldReuseRecentGenerationRecallTransaction(
transaction,
hookName,
recallKey = "",
now = Date.now(),
) {
if (!transaction || !hookName) return false;
if (!isGenerationRecallTransactionWithinBridgeWindow(transaction, now)) {
return false;
}
const hookStates = transaction.hookStates || {};
const normalizedRecallKey = String(recallKey || "");
const transactionRecallKey = String(transaction.recallKey || "");
if (Object.values(hookStates).includes("running")) {
return true;
}
const peerHookName = getGenerationRecallPeerHookName(hookName);
const peerHookState = peerHookName ? hookStates[peerHookName] : "";
if (peerHookState) {
return true;
}
const ownState = hookStates[hookName];
if (ownState) {
return ownState === "running";
}
if (!Object.keys(hookStates).length) {
if (!transactionRecallKey) {
return true;
}
if (!normalizedRecallKey) {
return false;
}
if (normalizedRecallKey !== transactionRecallKey) {
return false;
}
return true;
}
return false;
}
function markGenerationRecallTransactionHookState(
transaction,
hookName,
@@ -4820,20 +4986,102 @@ function createGenerationRecallContext({
recallOptions = {},
chatId = getCurrentChatId(),
} = {}) {
const recallKey =
recallOptions.recallKey ||
buildPreGenerationRecallKey(generationType, recallOptions);
const transaction = beginGenerationRecallTransaction({
chatId,
const context = getContext();
const chat = context?.chat;
const normalizedChatId = normalizeChatIdCandidate(
chatId || context?.chatId || getCurrentChatId(),
);
const frozenRecallOptions = freezeGenerationRecallOptionsForTransaction(
chat,
generationType,
recallKey,
});
recallOptions,
);
if (!frozenRecallOptions) {
return {
hookName,
generationType,
recallKey: "",
transaction: null,
recallOptions: null,
shouldRun: false,
};
}
const transactionGenerationType = normalizeGenerationRecallTransactionType(
frozenRecallOptions.generationType || generationType,
);
const fallbackRecallKey =
recallOptions.recallKey ||
buildPreGenerationRecallKey(transactionGenerationType, {
...frozenRecallOptions,
chatId: normalizedChatId,
userMessage: frozenRecallOptions.overrideUserMessage,
});
const now = Date.now();
const recentTransaction = findRecentGenerationRecallTransactionForChat(
normalizedChatId,
now,
);
let transaction = recentTransaction;
if (
!shouldReuseRecentGenerationRecallTransaction(
transaction,
hookName,
fallbackRecallKey,
now,
)
) {
transaction = beginGenerationRecallTransaction({
chatId: normalizedChatId,
generationType: transactionGenerationType,
recallKey: fallbackRecallKey,
forceNew: true,
});
}
if (!transaction) {
return {
hookName,
generationType,
recallKey: "",
transaction: null,
recallOptions: null,
shouldRun: false,
};
}
if (!transaction.frozenRecallOptions || typeof transaction.frozenRecallOptions !== "object") {
transaction.frozenRecallOptions = {
...frozenRecallOptions,
};
}
if (!String(transaction.recallKey || "").trim()) {
transaction.recallKey = fallbackRecallKey;
}
if (!String(transaction.generationType || "").trim()) {
transaction.generationType = transactionGenerationType;
}
transaction.updatedAt = now;
generationRecallTransactions.set(transaction.id, transaction);
const boundRecallOptions = {
...(transaction.frozenRecallOptions || frozenRecallOptions),
recallKey: transaction.recallKey,
generationType: transaction.frozenRecallOptions?.generationType || generationType,
};
const recallKey = String(transaction.recallKey || fallbackRecallKey || "");
const shouldRun = shouldRunRecallForTransaction(transaction, hookName);
return {
hookName,
generationType,
generationType: boundRecallOptions.generationType,
recallKey,
transaction,
shouldRun: shouldRunRecallForTransaction(transaction, hookName),
recallOptions: boundRecallOptions,
shouldRun,
};
}

View File

@@ -1,12 +1,14 @@
{
"scripts": {
"test:p0": "node tests/p0-regressions.mjs",
"test:runtime-history": "node tests/runtime-history.mjs",
"test:graph-persistence": "node tests/graph-persistence.mjs",
"test:indexeddb-persistence": "node tests/indexeddb-persistence.mjs",
"test:indexeddb-sync": "node tests/indexeddb-sync.mjs",
"test:indexeddb-migration": "node tests/indexeddb-migration.mjs",
"test:indexeddb": "npm run test:indexeddb-persistence && npm run test:indexeddb-sync && npm run test:indexeddb-migration",
"test:all": "npm run test:p0 && npm run test:graph-persistence && npm run test:indexeddb",
"test:persistence-matrix": "npm run test:p0 && npm run test:runtime-history && npm run test:graph-persistence && npm run test:indexeddb",
"test:all": "npm run test:persistence-matrix",
"check": "node --check index.js && node --check bme-db.js && node --check panel.js && node --check ui-status.js && node --check event-binding.js"
},
"dependencies": {

View File

@@ -882,6 +882,86 @@ result = {
);
}
{
const harness = await createGraphPersistenceHarness({
chatId: "chat-sync-refresh-merge",
chatMetadata: {
integrity: "chat-sync-refresh-merge-ready",
},
});
harness.api.setCurrentGraph(
normalizeGraphRuntimeState(
createMeaningfulGraph("chat-sync-refresh-merge", "stale-runtime-merge"),
"chat-sync-refresh-merge",
),
);
harness.api.setGraphPersistenceState({
loadState: "loaded",
chatId: "chat-sync-refresh-merge",
reason: "runtime-stale",
revision: 3,
lastPersistedRevision: 3,
dbReady: true,
writesBlocked: false,
});
harness.api.setIndexedDbSnapshot(
buildSnapshotFromGraph(
createMeaningfulGraph("chat-sync-refresh-merge", "fresh-indexeddb-merge"),
{
chatId: "chat-sync-refresh-merge",
revision: 8,
},
),
);
const runtimeOptions = harness.api.buildBmeSyncRuntimeOptions();
await runtimeOptions.onSyncApplied({
chatId: "chat-sync-refresh-merge",
action: "merge",
});
assert.equal(
harness.api.getCurrentGraph().nodes[0]?.fields?.title,
"事件-fresh-indexeddb-merge",
"merge 后应刷新当前运行时图谱",
);
}
{
const harness = await createGraphPersistenceHarness({
chatId: "chat-sync-refresh-active",
chatMetadata: {
integrity: "chat-sync-refresh-active-ready",
},
});
harness.api.setCurrentGraph(
normalizeGraphRuntimeState(
createMeaningfulGraph("chat-sync-refresh-active", "active-runtime"),
"chat-sync-refresh-active",
),
);
harness.api.setGraphPersistenceState({
loadState: "loaded",
chatId: "chat-sync-refresh-active",
reason: "runtime-active",
revision: 4,
dbReady: true,
writesBlocked: false,
});
const runtimeOptions = harness.api.buildBmeSyncRuntimeOptions();
await runtimeOptions.onSyncApplied({
chatId: "chat-sync-refresh-other",
action: "download",
});
assert.equal(
harness.api.getCurrentGraph().nodes[0]?.fields?.title,
"事件-active-runtime",
"active chat 与 sync payload chat 不一致时不应覆盖当前运行时图谱",
);
}
{
const sharedSession = new Map();
const writer = await createGraphPersistenceHarness({

View File

@@ -712,7 +712,12 @@ async function testSyncAppliedHook() {
const mergeResult = await syncNow("chat-hook-merge", runtime);
assert.equal(mergeResult.action, "merge");
assert.equal(downloadResult.revision, 3);
assert.equal(mergeResult.revision, 5);
assert.deepEqual(hookCalls.map((item) => item.action), ["download", "merge"]);
assert.deepEqual(hookCalls.map((item) => item.chatId), ["chat-hook-download", "chat-hook-merge"]);
assert.deepEqual(hookCalls.map((item) => item.revision), [3, 5]);
}
async function main() {

View File

@@ -1929,6 +1929,91 @@ async function testGenerationRecallTransactionDedupesDoubleHookBySameKey() {
assert.equal(harness.runRecallCalls[0].hookName, "GENERATION_AFTER_COMMANDS");
}
async function testGenerationRecallTransactionDedupesReverseHookOrder() {
const harness = await createGenerationRecallHarness();
harness.chat = [{ is_user: true, mes: "逆序同轮输入" }];
await harness.result.onBeforeCombinePrompts();
await harness.result.onGenerationAfterCommands("normal", {}, false);
assert.equal(harness.runRecallCalls.length, 1);
assert.equal(
harness.runRecallCalls[0].hookName,
"GENERATE_BEFORE_COMBINE_PROMPTS",
);
}
async function testGenerationRecallHistoryModesUseSameBindingAcrossHooks() {
for (const generationType of ["continue", "regenerate", "swipe"]) {
const harness = await createGenerationRecallHarness();
const userMessage = `历史输入-${generationType}`;
harness.chat = [
{ is_user: true, mes: userMessage },
{ is_user: false, mes: "assistant-tail" },
];
await harness.result.onGenerationAfterCommands(generationType, {}, false);
await harness.result.onBeforeCombinePrompts();
assert.equal(harness.runRecallCalls.length, 1, `${generationType} 应只执行一次召回`);
assert.equal(harness.runRecallCalls[0].hookName, "GENERATION_AFTER_COMMANDS");
assert.equal(harness.runRecallCalls[0].targetUserMessageIndex, 0);
assert.equal(harness.runRecallCalls[0].overrideUserMessage, userMessage);
}
}
async function testGenerationRecallFrozenBindingSurvivesCrossHookInputDrift() {
const harness = await createGenerationRecallHarness();
harness.chat = [{ is_user: true, mes: "稳定输入-A" }];
await harness.result.onGenerationAfterCommands("normal", {}, false);
harness.chat = [{ is_user: true, mes: "稳定输入-B" }];
await harness.result.onBeforeCombinePrompts();
assert.equal(harness.runRecallCalls.length, 1);
assert.equal(harness.runRecallCalls[0].overrideUserMessage, "稳定输入-A");
}
async function testGenerationRecallSkipsUntilTargetUserFloorAvailable() {
const harness = await createGenerationRecallHarness();
harness.chat = [{ is_user: false, mes: "assistant-only" }];
await harness.result.onGenerationAfterCommands("normal", {}, false);
assert.equal(harness.runRecallCalls.length, 0);
harness.chat = [{ is_user: true, mes: "补齐 user 楼层" }];
await harness.result.onBeforeCombinePrompts();
assert.equal(harness.runRecallCalls.length, 1);
assert.equal(
harness.runRecallCalls[0].hookName,
"GENERATE_BEFORE_COMBINE_PROMPTS",
);
}
async function testGenerationRecallSameKeyCanRunAgainImmediatelyAsNewGeneration() {
const harness = await createGenerationRecallHarness();
harness.chat = [{ is_user: true, mes: "同 key 连续生成" }];
await harness.result.onGenerationAfterCommands("normal", {}, false);
await harness.result.onGenerationAfterCommands("normal", {}, false);
assert.equal(harness.runRecallCalls.length, 2);
assert.equal(harness.runRecallCalls[0].recallKey, harness.runRecallCalls[1].recallKey);
}
async function testGenerationRecallSameKeyCanRunAgainAfterBridgeWindow() {
const harness = await createGenerationRecallHarness();
harness.chat = [{ is_user: true, mes: "同 key 重复生成" }];
await harness.result.onGenerationAfterCommands("normal", {}, false);
const transaction = [...harness.result.generationRecallTransactions.values()][0];
transaction.updatedAt = Date.now() - 5000;
harness.result.generationRecallTransactions.set(transaction.id, transaction);
await harness.result.onGenerationAfterCommands("normal", {}, false);
assert.equal(harness.runRecallCalls.length, 2);
}
async function testGenerationRecallBeforeCombineRunsStandalone() {
const harness = await createGenerationRecallHarness();
harness.chat = [{ is_user: true, mes: "仅 before combine" }];
@@ -2471,6 +2556,12 @@ await testBatchStatusSemanticFailureDoesNotHideCoreSuccess();
await testBatchStatusFinalizeFailureIsNotCompleteSuccess();
await testProcessedHistoryAdvanceRequiresCompleteStrongSuccess();
await testGenerationRecallTransactionDedupesDoubleHookBySameKey();
await testGenerationRecallTransactionDedupesReverseHookOrder();
await testGenerationRecallHistoryModesUseSameBindingAcrossHooks();
await testGenerationRecallFrozenBindingSurvivesCrossHookInputDrift();
await testGenerationRecallSkipsUntilTargetUserFloorAvailable();
await testGenerationRecallSameKeyCanRunAgainImmediatelyAsNewGeneration();
await testGenerationRecallSameKeyCanRunAgainAfterBridgeWindow();
await testGenerationRecallBeforeCombineRunsStandalone();
await testGenerationRecallDifferentKeyCanRunAgain();
await testGenerationRecallSkippedStateDoesNotLoopToBeforeCombine();

View File

@@ -278,16 +278,33 @@ export function isTerminalGenerationRecallHookState(state = "") {
export function shouldRunRecallForTransaction(transaction, hookName) {
if (!hookName) return true;
if (!transaction) return true;
const hookStates = transaction.hookStates || {};
if (isTerminalGenerationRecallHookState(hookStates[hookName])) {
return false;
}
const currentHookState = hookStates[hookName];
if (
hookName === "GENERATE_BEFORE_COMBINE_PROMPTS" &&
isTerminalGenerationRecallHookState(hookStates.GENERATION_AFTER_COMMANDS)
currentHookState === "running" ||
isTerminalGenerationRecallHookState(currentHookState)
) {
return false;
}
const peerHookName =
hookName === "GENERATION_AFTER_COMMANDS"
? "GENERATE_BEFORE_COMBINE_PROMPTS"
: hookName === "GENERATE_BEFORE_COMBINE_PROMPTS"
? "GENERATION_AFTER_COMMANDS"
: "";
if (!peerHookName) return true;
const peerHookState = hookStates[peerHookName];
if (
peerHookState === "running" ||
isTerminalGenerationRecallHookState(peerHookState)
) {
return false;
}
return true;
}