From 4bbbd4d09d81db2527bdbcf1025c6101d22eb246 Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Sat, 4 Apr 2026 02:56:04 +0800 Subject: [PATCH] fix recall card binding lag --- event-binding.js | 27 ++++++++++-- index.js | 14 +++++++ tests/p0-regressions.mjs | 89 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 126 insertions(+), 4 deletions(-) diff --git a/event-binding.js b/event-binding.js index c03b73d..d7c0253 100644 --- a/event-binding.js +++ b/event-binding.js @@ -219,11 +219,32 @@ export function onChatLoadedController(runtime) { export function onMessageSentController(runtime, messageId) { const context = runtime.getContext(); const chat = context?.chat; - const message = - Array.isArray(chat) && Number.isFinite(messageId) ? chat[messageId] : null; + const normalizedMessageId = + messageId === null || messageId === undefined || messageId === "" + ? null + : Number(messageId); + let resolvedMessageId = Number.isFinite(normalizedMessageId) + ? normalizedMessageId + : null; + let message = + Array.isArray(chat) && Number.isFinite(resolvedMessageId) + ? chat[resolvedMessageId] + : null; + + if (!message?.is_user && Array.isArray(chat)) { + for (let index = chat.length - 1; index >= 0; index--) { + if (!chat[index]?.is_user) continue; + resolvedMessageId = index; + message = chat[index]; + break; + } + } if (!message?.is_user) return; - runtime.recordRecallSentUserMessage(messageId, message.mes || ""); + runtime.recordRecallSentUserMessage( + resolvedMessageId, + message.mes || "", + ); runtime.refreshPersistedRecallMessageUi?.(); } diff --git a/index.js b/index.js index c66edfa..f5b77e8 100644 --- a/index.js +++ b/index.js @@ -8802,6 +8802,20 @@ function onGenerationStarted(type, params = {}, dryRun = false) { } function onGenerationEnded(_chatLength = null) { + const recentTransaction = findRecentGenerationRecallTransactionForChat(); + const recentRecallResult = + getGenerationRecallTransactionResult(recentTransaction); + ensurePersistedRecallRecordForGeneration({ + generationType: recentTransaction?.generationType || "normal", + recallResult: recentRecallResult, + transaction: recentTransaction, + recallOptions: recentTransaction?.frozenRecallOptions || null, + hookName: + recentRecallResult?.hookName || + recentTransaction?.lastRecallMeta?.hookName || + "", + }); + schedulePersistedRecallMessageUiRefresh(320); if (typeof scheduleMessageHideApply === "function") { scheduleMessageHideApply("generation-ended", 180); } diff --git a/tests/p0-regressions.mjs b/tests/p0-regressions.mjs index b0a08f3..750607d 100644 --- a/tests/p0-regressions.mjs +++ b/tests/p0-regressions.mjs @@ -10,6 +10,7 @@ import { onChatChangedController, onGenerationAfterCommandsController, onGenerationStartedController, + onMessageSentController, onMessageReceivedController, onMessageSwipedController, registerCoreEventHooksController, @@ -344,6 +345,7 @@ function createGenerationRecallHarness(options = {}) { moduleInjectionCalls: [], recordedInjectionSnapshots: [], refreshPanelCalls: 0, + hideScheduleCalls: [], createRecallInputRecord, createRecallRunResult, hashRecallInput, @@ -380,6 +382,7 @@ function createGenerationRecallHarness(options = {}) { }; }, getSettings: () => ({}), + $: () => ({}), triggerChatMetadataSave: () => { context.metadataSaveCalls += 1; return "debounced"; @@ -393,6 +396,11 @@ function createGenerationRecallHarness(options = {}) { schedulePersistedRecallMessageUiRefresh: () => { context.recallUiRefreshCalls += 1; }, + getMessageHideSettings: () => ({}), + getHideRuntimeAdapters: () => ({}), + scheduleHideSettingsApply: (...args) => { + context.hideScheduleCalls.push(args); + }, estimateTokens: (text = "") => normalizeRecallInputText(text) .split(/\s+/) @@ -414,7 +422,7 @@ function createGenerationRecallHarness(options = {}) { }; vm.createContext(context); vm.runInContext( - `${snippet}\nresult = { hashRecallInput, buildPreGenerationRecallKey, buildGenerationAfterCommandsRecallInput, cleanupGenerationRecallTransactions, buildGenerationRecallTransactionId, beginGenerationRecallTransaction, markGenerationRecallTransactionHookState, shouldRunRecallForTransaction, createGenerationRecallContext, onGenerationStarted, onGenerationAfterCommands, onBeforeCombinePrompts, applyFinalRecallInjectionForGeneration, generationRecallTransactions, freezeHostGenerationInputSnapshot, consumeHostGenerationInputSnapshot, getPendingHostGenerationInputSnapshot, recordRecallSendIntent, recordRecallSentUserMessage, getPendingRecallSendIntent: () => pendingRecallSendIntent, getLastRecallSentUserMessage: () => lastRecallSentUserMessage };`, + `${snippet}\nresult = { hashRecallInput, buildPreGenerationRecallKey, buildGenerationAfterCommandsRecallInput, cleanupGenerationRecallTransactions, buildGenerationRecallTransactionId, beginGenerationRecallTransaction, markGenerationRecallTransactionHookState, shouldRunRecallForTransaction, createGenerationRecallContext, onGenerationStarted, onGenerationEnded, onGenerationAfterCommands, onBeforeCombinePrompts, applyFinalRecallInjectionForGeneration, ensurePersistedRecallRecordForGeneration, findRecentGenerationRecallTransactionForChat, getGenerationRecallTransactionResult, generationRecallTransactions, freezeHostGenerationInputSnapshot, consumeHostGenerationInputSnapshot, getPendingHostGenerationInputSnapshot, recordRecallSendIntent, recordRecallSentUserMessage, getPendingRecallSendIntent: () => pendingRecallSendIntent, getLastRecallSentUserMessage: () => lastRecallSentUserMessage };`, context, { filename: indexPath }, ); @@ -3143,6 +3151,39 @@ async function testSwipeRoutesToRerollWithoutHistoryRecoveryFallback() { assert.equal(result.recoveryPath, "reverse-journal"); } +async function testMessageSentFallsBackToLatestUserWhenHostMessageIdInvalid() { + const recorded = []; + let refreshCalls = 0; + + onMessageSentController( + { + getContext: () => ({ + chat: [ + { is_user: true, mes: "较早用户楼层" }, + { is_user: false, mes: "assistant-tail" }, + { is_user: true, mes: "最新用户楼层" }, + ], + }), + recordRecallSentUserMessage(messageId, text, source = "message-sent") { + recorded.push({ messageId, text, source }); + }, + refreshPersistedRecallMessageUi() { + refreshCalls += 1; + }, + }, + null, + ); + + assert.deepEqual(recorded, [ + { + messageId: 2, + text: "最新用户楼层", + source: "message-sent", + }, + ]); + assert.equal(refreshCalls, 1); +} + async function testMessageReceivedQueuesExtractionWithoutRuntimeQueueMicrotask() { let runExtractionCalls = 0; let refreshCalls = 0; @@ -3825,6 +3866,50 @@ async function testGenerationRecallImmediateAfterCommandsBackfillsPersistedRecor assert.equal(harness.metadataSaveCalls > 0, true); } +async function testGenerationEndedBackfillsRecentRecallAndSchedulesHideRefresh() { + const harness = await createGenerationRecallHarness({ realApplyFinal: true }); + harness.chat = [{ is_user: true, mes: "生成结束后补写目标" }]; + const transaction = harness.result.beginGenerationRecallTransaction({ + chatId: "chat-main", + generationType: "normal", + recallKey: "chat-main:normal:test-generation-ended", + forceNew: true, + }); + transaction.frozenRecallOptions = { + generationType: "normal", + targetUserMessageIndex: null, + overrideUserMessage: "生成结束后补写目标", + lockedSource: "send-intent", + hookName: "GENERATION_AFTER_COMMANDS", + }; + harness.result.generationRecallTransactions.set(transaction.id, transaction); + harness.result.markGenerationRecallTransactionHookState( + transaction, + "GENERATION_AFTER_COMMANDS", + "completed", + ); + harness.result.getGenerationRecallTransactionResult(transaction); + transaction.lastRecallResult = { + status: "completed", + didRecall: true, + injectionText: "generation-ended-memory", + selectedNodeIds: ["node-z"], + sourceCandidates: [{ text: "生成结束后补写目标" }], + hookName: "GENERATION_AFTER_COMMANDS", + }; + transaction.updatedAt = Date.now(); + harness.result.generationRecallTransactions.set(transaction.id, transaction); + + harness.result.onGenerationEnded(); + + assert.equal( + harness.chat[0]?.extra?.bme_recall?.injectionText, + "generation-ended-memory", + ); + assert.equal(harness.hideScheduleCalls.length, 1); + assert.equal(harness.hideScheduleCalls[0]?.[2], 180); +} + async function testRecallSubGraphAndDataLayerEntryPoints() { // Sub-graph build test (pure function, no DOM needed) const { buildRecallSubGraph } = await import("../recall-message-ui.js"); @@ -4624,6 +4709,7 @@ await testGenerationRecallSentMessageClearsStaleTransactionForSameKey(); await testRegisterCoreEventHooksIsIdempotent(); await testChatChangedDoesNotClearCoreEventBindings(); await testSwipeRoutesToRerollWithoutHistoryRecoveryFallback(); +await testMessageSentFallsBackToLatestUserWhenHostMessageIdInvalid(); await testMessageReceivedQueuesExtractionWithoutRuntimeQueueMicrotask(); await testAutoExtractionDefersWhenGraphNotReady(); await testAutoExtractionDefersWhenAlreadyExtracting(); @@ -4636,6 +4722,7 @@ await testPersistentRecallSourceResolutionAndTargetRouting(); await testGenerationRecallFinalInjectionRebindsLatestMatchingUserFloor(); await testGenerationRecallFinalInjectionBackfillsPersistedRecord(); await testGenerationRecallImmediateAfterCommandsBackfillsPersistedRecord(); +await testGenerationEndedBackfillsRecentRecallAndSchedulesHideRefresh(); await testRecallCardMountsOnStandardUserMessageDom(); await testRecallCardSkipsMountWithoutStableMessageIndex(); await testRecallCardDelayedDomInsertionEventuallyRenders();