fix: freeze recall input from host lifecycle

This commit is contained in:
Youzini-afk
2026-03-31 16:33:40 +08:00
parent d8710f45f1
commit d5b4b7e1dc
3 changed files with 154 additions and 15 deletions

View File

@@ -8,7 +8,10 @@ export function registerBeforeCombinePromptsController(runtime, listener) {
} }
runtime.console.warn("[ST-BME] eventMakeFirst 不可用,回退到普通事件注册"); runtime.console.warn("[ST-BME] eventMakeFirst 不可用,回退到普通事件注册");
runtime.eventSource.on(runtime.eventTypes.GENERATE_BEFORE_COMBINE_PROMPTS, listener); runtime.eventSource.on(
runtime.eventTypes.GENERATE_BEFORE_COMBINE_PROMPTS,
listener,
);
return null; return null;
} }
@@ -21,7 +24,10 @@ export function registerGenerationAfterCommandsController(runtime, listener) {
runtime.console.warn( runtime.console.warn(
"[ST-BME] eventMakeFirst 不可用GENERATION_AFTER_COMMANDS 回退到普通事件注册", "[ST-BME] eventMakeFirst 不可用GENERATION_AFTER_COMMANDS 回退到普通事件注册",
); );
runtime.eventSource.on(runtime.eventTypes.GENERATION_AFTER_COMMANDS, listener); runtime.eventSource.on(
runtime.eventTypes.GENERATION_AFTER_COMMANDS,
listener,
);
return null; return null;
} }
@@ -48,7 +54,10 @@ export function installSendIntentHooksController(runtime) {
if (sendButton) { if (sendButton) {
const captureSendIntent = () => { const captureSendIntent = () => {
runtime.recordRecallSendIntent(runtime.getSendTextareaValue(), "send-button"); runtime.recordRecallSendIntent(
runtime.getSendTextareaValue(),
"send-button",
);
}; };
sendButton.addEventListener("click", captureSendIntent, true); sendButton.addEventListener("click", captureSendIntent, true);
@@ -182,25 +191,36 @@ export async function onGenerationAfterCommandsController(
) { ) {
if (dryRun) return; if (dryRun) return;
const generationType = String(type || "normal").trim() || "normal";
const frozenInputSnapshot =
generationType === "normal"
? runtime.consumeHostGenerationInputSnapshot?.({ preserve: true }) ||
runtime.consumeHostGenerationInputSnapshot?.()
: null;
const context = runtime.getContext(); const context = runtime.getContext();
const chat = context?.chat; const chat = context?.chat;
const recallOptions = runtime.buildGenerationAfterCommandsRecallInput( const recallOptions = runtime.buildGenerationAfterCommandsRecallInput(
type, type,
params, {
...params,
frozenInputSnapshot,
},
chat, chat,
); );
if (!recallOptions) return; if (!recallOptions) return;
const recallContext = runtime.createGenerationRecallContext({ const recallContext = runtime.createGenerationRecallContext({
hookName: "GENERATION_AFTER_COMMANDS", hookName: "GENERATION_AFTER_COMMANDS",
generationType: String(type || "normal").trim() || "normal", generationType,
recallOptions, recallOptions,
}); });
if (!recallContext.shouldRun) { if (!recallContext.shouldRun) {
return; return;
} }
const runtimeRecallOptions = recallContext.recallOptions || recallOptions || {}; const runtimeRecallOptions =
recallContext.recallOptions || recallOptions || {};
runtime.markGenerationRecallTransactionHookState( runtime.markGenerationRecallTransactionHookState(
recallContext.transaction, recallContext.transaction,
recallContext.hookName, recallContext.hookName,
@@ -226,10 +246,17 @@ export async function onGenerationAfterCommandsController(
} }
export async function onBeforeCombinePromptsController(runtime) { export async function onBeforeCombinePromptsController(runtime) {
const frozenInputSnapshot =
runtime.consumeHostGenerationInputSnapshot?.() ||
runtime.getPendingHostGenerationInputSnapshot?.() ||
runtime.createRecallInputRecord?.() ||
{};
const context = runtime.getContext(); const context = runtime.getContext();
const chat = context?.chat; const chat = context?.chat;
const recallOptions = const recallOptions =
runtime.buildNormalGenerationRecallInput(chat) || runtime.buildNormalGenerationRecallInput(chat, {
frozenInputSnapshot,
}) ||
runtime.buildHistoryGenerationRecallInput(chat) || runtime.buildHistoryGenerationRecallInput(chat) ||
{}; {};
const recallContext = runtime.createGenerationRecallContext({ const recallContext = runtime.createGenerationRecallContext({
@@ -241,7 +268,8 @@ export async function onBeforeCombinePromptsController(runtime) {
return; return;
} }
const runtimeRecallOptions = recallContext.recallOptions || recallOptions || {}; const runtimeRecallOptions =
recallContext.recallOptions || recallOptions || {};
runtime.markGenerationRecallTransactionHookState( runtime.markGenerationRecallTransactionHookState(
recallContext.transaction, recallContext.transaction,
recallContext.hookName, recallContext.hookName,
@@ -268,7 +296,8 @@ export function onMessageReceivedController(runtime) {
const persistenceState = runtime.getGraphPersistenceState?.() || {}; const persistenceState = runtime.getGraphPersistenceState?.() || {};
const loadState = persistenceState.loadState || ""; const loadState = persistenceState.loadState || "";
const dbReady = const dbReady =
persistenceState.dbReady ?? (loadState === "loaded" || loadState === "empty-confirmed"); persistenceState.dbReady ??
(loadState === "loaded" || loadState === "empty-confirmed");
if ( if (
!dbReady || !dbReady ||
loadState === "loading" || loadState === "loading" ||

View File

@@ -446,6 +446,7 @@ let graphPersistenceState = createGraphPersistenceState();
const lastStatusToastAt = {}; const lastStatusToastAt = {};
let pendingRecallSendIntent = createRecallInputRecord(); let pendingRecallSendIntent = createRecallInputRecord();
let lastRecallSentUserMessage = createRecallInputRecord(); let lastRecallSentUserMessage = createRecallInputRecord();
let pendingHostGenerationInputSnapshot = createRecallInputRecord();
let sendIntentHookCleanup = []; let sendIntentHookCleanup = [];
let sendIntentHookRetryTimer = null; let sendIntentHookRetryTimer = null;
let pendingHistoryRecoveryTimer = null; let pendingHistoryRecoveryTimer = null;
@@ -1027,6 +1028,45 @@ function updateLastRecalledItems(nodeIds = []) {
function clearRecallInputTracking() { function clearRecallInputTracking() {
pendingRecallSendIntent = createRecallInputRecord(); pendingRecallSendIntent = createRecallInputRecord();
lastRecallSentUserMessage = createRecallInputRecord(); lastRecallSentUserMessage = createRecallInputRecord();
pendingHostGenerationInputSnapshot = createRecallInputRecord();
}
function freezeHostGenerationInputSnapshot(
text,
source = "host-generation-lifecycle",
) {
const normalized = normalizeRecallInputText(text);
if (!normalized) return null;
pendingHostGenerationInputSnapshot = createRecallInputRecord({
text: normalized,
hash: hashRecallInput(normalized),
source,
at: Date.now(),
});
return pendingHostGenerationInputSnapshot;
}
function consumeHostGenerationInputSnapshot(options = {}) {
const { preserve = false } = options;
if (!isFreshRecallInputRecord(pendingHostGenerationInputSnapshot)) {
if (!preserve) {
pendingHostGenerationInputSnapshot = createRecallInputRecord();
}
return createRecallInputRecord();
}
const snapshot = createRecallInputRecord({
...pendingHostGenerationInputSnapshot,
});
if (!preserve) {
pendingHostGenerationInputSnapshot = createRecallInputRecord();
}
return snapshot;
}
function getPendingHostGenerationInputSnapshot() {
return pendingHostGenerationInputSnapshot;
} }
function recordRecallSendIntent(text, source = "dom-intent") { function recordRecallSendIntent(text, source = "dom-intent") {
@@ -1057,6 +1097,12 @@ function recordRecallSentUserMessage(messageId, text, source = "message-sent") {
if (pendingRecallSendIntent.hash && pendingRecallSendIntent.hash === hash) { if (pendingRecallSendIntent.hash && pendingRecallSendIntent.hash === hash) {
pendingRecallSendIntent = createRecallInputRecord(); pendingRecallSendIntent = createRecallInputRecord();
} }
if (
pendingHostGenerationInputSnapshot.hash &&
pendingHostGenerationInputSnapshot.hash === hash
) {
pendingHostGenerationInputSnapshot = createRecallInputRecord();
}
} }
function getMessageRecallRecord(messageIndex) { function getMessageRecallRecord(messageIndex) {
@@ -4830,10 +4876,12 @@ function buildGenerationAfterCommandsRecallInput(type, params = {}, chat) {
}; };
} }
return buildNormalGenerationRecallInput(chat); return buildNormalGenerationRecallInput(chat, {
frozenInputSnapshot: params?.frozenInputSnapshot,
});
} }
function buildNormalGenerationRecallInput(chat) { function buildNormalGenerationRecallInput(chat, options = {}) {
const lastNonSystemMessage = getLastNonSystemChatMessage(chat); const lastNonSystemMessage = getLastNonSystemChatMessage(chat);
const tailUserText = lastNonSystemMessage?.is_user const tailUserText = lastNonSystemMessage?.is_user
? normalizeRecallInputText(lastNonSystemMessage?.mes || "") ? normalizeRecallInputText(lastNonSystemMessage?.mes || "")
@@ -4841,18 +4889,38 @@ function buildNormalGenerationRecallInput(chat) {
const targetUserMessageIndex = resolveGenerationTargetUserMessageIndex(chat, { const targetUserMessageIndex = resolveGenerationTargetUserMessageIndex(chat, {
generationType: "normal", generationType: "normal",
}); });
const frozenInputSnapshot = isFreshRecallInputRecord(
options?.frozenInputSnapshot,
)
? options.frozenInputSnapshot
: null;
const hostSnapshotText = normalizeRecallInputText(
frozenInputSnapshot?.text || "",
);
const textareaText = normalizeRecallInputText( const textareaText = normalizeRecallInputText(
pendingRecallSendIntent.text || getSendTextareaValue(), pendingRecallSendIntent.text || getSendTextareaValue(),
); );
const userMessage = tailUserText || textareaText; const userMessage = tailUserText || hostSnapshotText || textareaText;
if (!userMessage) return null; if (!userMessage) return null;
let overrideSource = "send-intent";
let overrideSourceLabel = "发送意图";
if (tailUserText) {
overrideSource = "chat-tail-user";
overrideSourceLabel = "当前用户楼层";
} else if (hostSnapshotText) {
overrideSource = String(
frozenInputSnapshot?.source || "host-generation-lifecycle",
);
overrideSourceLabel = "宿主发送快照";
}
return { return {
overrideUserMessage: userMessage, overrideUserMessage: userMessage,
generationType: "normal", generationType: "normal",
targetUserMessageIndex, targetUserMessageIndex,
overrideSource: tailUserText ? "chat-tail-user" : "send-intent", overrideSource,
overrideSourceLabel: tailUserText ? "当前用户楼层" : "发送意图", overrideSourceLabel,
includeSyntheticUserMessage: !tailUserText, includeSyntheticUserMessage: !tailUserText,
}; };
} }
@@ -6514,6 +6582,7 @@ async function onGenerationAfterCommands(type, params = {}, dryRun = false) {
{ {
applyFinalRecallInjectionForGeneration, applyFinalRecallInjectionForGeneration,
buildGenerationAfterCommandsRecallInput, buildGenerationAfterCommandsRecallInput,
consumeHostGenerationInputSnapshot,
createGenerationRecallContext, createGenerationRecallContext,
getContext, getContext,
getGenerationRecallHookStateFromResult, getGenerationRecallHookStateFromResult,
@@ -6531,6 +6600,7 @@ async function onBeforeCombinePrompts() {
applyFinalRecallInjectionForGeneration, applyFinalRecallInjectionForGeneration,
buildHistoryGenerationRecallInput, buildHistoryGenerationRecallInput,
buildNormalGenerationRecallInput, buildNormalGenerationRecallInput,
consumeHostGenerationInputSnapshot,
createGenerationRecallContext, createGenerationRecallContext,
getContext, getContext,
getGenerationRecallHookStateFromResult, getGenerationRecallHookStateFromResult,
@@ -6546,6 +6616,7 @@ function onMessageReceived() {
getContext, getContext,
getCurrentGraph: () => currentGraph, getCurrentGraph: () => currentGraph,
getGraphPersistenceState: () => graphPersistenceState, getGraphPersistenceState: () => graphPersistenceState,
getPendingHostGenerationInputSnapshot,
getPendingRecallSendIntent: () => pendingRecallSendIntent, getPendingRecallSendIntent: () => pendingRecallSendIntent,
isAssistantChatMessage, isAssistantChatMessage,
isFreshRecallInputRecord, isFreshRecallInputRecord,
@@ -6557,6 +6628,9 @@ function onMessageReceived() {
queueMicrotask, queueMicrotask,
runExtraction, runExtraction,
refreshPersistedRecallMessageUi: schedulePersistedRecallMessageUiRefresh, refreshPersistedRecallMessageUi: schedulePersistedRecallMessageUiRefresh,
setPendingHostGenerationInputSnapshot: (record) => {
pendingHostGenerationInputSnapshot = record;
},
setPendingRecallSendIntent: (record) => { setPendingRecallSendIntent: (record) => {
pendingRecallSendIntent = record; pendingRecallSendIntent = record;
}, },

View File

@@ -270,6 +270,7 @@ function createGenerationRecallHarness() {
getCurrentChatId: () => "chat-main", getCurrentChatId: () => "chat-main",
normalizeRecallInputText: (text = "") => String(text || "").trim(), normalizeRecallInputText: (text = "") => String(text || "").trim(),
pendingRecallSendIntent: { text: "", hash: "", at: 0 }, pendingRecallSendIntent: { text: "", hash: "", at: 0 },
pendingHostGenerationInputSnapshot: { text: "", hash: "", at: 0 },
lastRecallSentUserMessage: { text: "", hash: "", at: 0 }, lastRecallSentUserMessage: { text: "", hash: "", at: 0 },
getLatestUserChatMessage: (chat = []) => getLatestUserChatMessage: (chat = []) =>
[...chat].reverse().find((message) => message?.is_user) || null, [...chat].reverse().find((message) => message?.is_user) || null,
@@ -341,7 +342,7 @@ function createGenerationRecallHarness() {
}; };
vm.createContext(context); vm.createContext(context);
vm.runInContext( vm.runInContext(
`${snippet}\nresult = { hashRecallInput, buildPreGenerationRecallKey, buildGenerationAfterCommandsRecallInput, cleanupGenerationRecallTransactions, buildGenerationRecallTransactionId, beginGenerationRecallTransaction, markGenerationRecallTransactionHookState, shouldRunRecallForTransaction, createGenerationRecallContext, onGenerationAfterCommands, onBeforeCombinePrompts, generationRecallTransactions };`, `${snippet}\nresult = { hashRecallInput, buildPreGenerationRecallKey, buildGenerationAfterCommandsRecallInput, cleanupGenerationRecallTransactions, buildGenerationRecallTransactionId, beginGenerationRecallTransaction, markGenerationRecallTransactionHookState, shouldRunRecallForTransaction, createGenerationRecallContext, onGenerationAfterCommands, onBeforeCombinePrompts, generationRecallTransactions, freezeHostGenerationInputSnapshot, consumeHostGenerationInputSnapshot, getPendingHostGenerationInputSnapshot };`,
context, context,
{ filename: indexPath }, { filename: indexPath },
); );
@@ -2264,6 +2265,40 @@ async function testGenerationRecallBeforeCombineCanUseProvisionalSendIntentBindi
assert.equal(harness.runRecallCalls[0].targetUserMessageIndex, null); assert.equal(harness.runRecallCalls[0].targetUserMessageIndex, null);
} }
async function testGenerationRecallHostLifecycleSnapshotSurvivesTextareaClearWithoutDomIntent() {
const harness = await createGenerationRecallHarness();
harness.chat = [{ is_user: false, mes: "assistant-tail" }];
harness.__sendTextareaValue = "宿主冻结输入";
const frozenSnapshot = harness.result.freezeHostGenerationInputSnapshot(
harness.__sendTextareaValue,
);
harness.__sendTextareaValue = "";
await harness.result.onGenerationAfterCommands(
"normal",
{ frozenInputSnapshot: frozenSnapshot },
false,
);
await harness.result.onBeforeCombinePrompts();
assert.equal(harness.runRecallCalls.length, 1);
assert.equal(
harness.runRecallCalls[0].hookName,
"GENERATE_BEFORE_COMBINE_PROMPTS",
);
assert.equal(harness.runRecallCalls[0].overrideUserMessage, "宿主冻结输入");
assert.equal(harness.runRecallCalls[0].overrideSourceLabel, "宿主发送快照");
assert.equal(harness.runRecallCalls[0].targetUserMessageIndex, null);
assert.deepEqual(harness.result.getPendingHostGenerationInputSnapshot(), {
text: "",
hash: "",
at: 0,
source: "",
messageId: null,
});
}
async function testGenerationRecallAfterCommandsStillSkipsWithoutStableUserFloor() { async function testGenerationRecallAfterCommandsStillSkipsWithoutStableUserFloor() {
const harness = await createGenerationRecallHarness(); const harness = await createGenerationRecallHarness();
harness.chat = [{ is_user: false, mes: "assistant-tail" }]; harness.chat = [{ is_user: false, mes: "assistant-tail" }];
@@ -3016,6 +3051,7 @@ await testGenerationRecallHistoryModesUseSameBindingAcrossHooks();
await testGenerationRecallFrozenBindingSurvivesCrossHookInputDrift(); await testGenerationRecallFrozenBindingSurvivesCrossHookInputDrift();
await testGenerationRecallSkipsUntilTargetUserFloorAvailable(); await testGenerationRecallSkipsUntilTargetUserFloorAvailable();
await testGenerationRecallBeforeCombineCanUseProvisionalSendIntentBinding(); await testGenerationRecallBeforeCombineCanUseProvisionalSendIntentBinding();
await testGenerationRecallHostLifecycleSnapshotSurvivesTextareaClearWithoutDomIntent();
await testGenerationRecallAfterCommandsStillSkipsWithoutStableUserFloor(); await testGenerationRecallAfterCommandsStillSkipsWithoutStableUserFloor();
await testGenerationRecallSameKeyCanRunAgainImmediatelyAsNewGeneration(); await testGenerationRecallSameKeyCanRunAgainImmediatelyAsNewGeneration();
await testGenerationRecallSameKeyCanRunAgainAfterBridgeWindow(); await testGenerationRecallSameKeyCanRunAgainAfterBridgeWindow();