From a77762d5d02c46162293311de6040ae17e04b369 Mon Sep 17 00:00:00 2001 From: youzini Date: Sun, 31 May 2026 12:08:14 +0000 Subject: [PATCH] refactor(runtime): extract auto-extraction defer factory (Phase 4e) --- index.js | 342 +++---------------- runtime/auto-extraction-defer.js | 346 ++++++++++++++++++++ tests/graph-persistence.mjs | 2 + tests/helpers/generation-recall-harness.mjs | 4 +- 4 files changed, 391 insertions(+), 303 deletions(-) create mode 100644 runtime/auto-extraction-defer.js diff --git a/index.js b/index.js index bc76983..3570f5b 100644 --- a/index.js +++ b/index.js @@ -164,6 +164,7 @@ import { createRecallInputState } from "./runtime/recall-input-state.js"; import { createRerollRecallInput } from "./runtime/reroll-recall-input.js"; import { createGenerationRecallTransactions } from "./runtime/generation-recall-transactions.js"; import { createFinalRecallInjection } from "./runtime/final-recall-injection.js"; +import { createAutoExtractionDefer } from "./runtime/auto-extraction-defer.js"; import { extractMemories, generateReflection, @@ -208,7 +209,7 @@ import { LUKER_GRAPH_SIDECAR_V2_FORMAT, MODULE_NAME, cloneGraphForPersistence, - cloneRuntimeDebugValue, + cloneRuntimeDebugValue: (...args) => cloneRuntimeDebugValue(...args), getGraphPersistedRevision, getGraphPersistenceMeta, getGraphIdentityAliasCandidates, @@ -1432,20 +1433,10 @@ let pendingGraphLoadRetryChatId = ""; let pendingGraphPersistRetryTimer = null; let pendingGraphPersistRetryChatId = ""; let pendingGraphPersistRetryAttempt = 0; -let pendingAutoExtractionTimer = null; let authorityJobPollAbortController = null; let authorityJobPollJobId = ""; let authorityJobPollChatId = ""; let authorityJobPollPromise = null; -let pendingAutoExtraction = { - chatId: "", - messageId: null, - reason: "", - requestedAt: 0, - attempts: 0, - targetEndFloor: null, - strategy: "normal", -}; let isHostGenerationRunning = false; let lastHostGenerationEndedAt = 0; let skipBeforeCombineRecallUntil = 0; @@ -1514,6 +1505,30 @@ const finalRecallInjectionRuntime = createFinalRecallInjection({ triggerChatMetadataSave, writePersistedRecallToUserMessage, }); +const autoExtractionDeferRuntime = createAutoExtractionDefer({ + clearTimeout, + cloneRuntimeDebugValue: (...args) => cloneRuntimeDebugValue(...args), + console, + ensureGraphMutationReady: (...args) => ensureGraphMutationReady(...args), + getContext, + getCurrentChatId, + getGraphPersistenceState: () => graphPersistenceState, + getIsExtracting: () => isExtracting, + getIsHostGenerationRunning: () => isHostGenerationRunning, + getIsRecoveringHistory: () => isRecoveringHistory, + getLastHostGenerationEndedAt: () => lastHostGenerationEndedAt, + getSettings, + isAssistantChatMessage: (...args) => isAssistantChatMessage(...args), + isRestoreLockActive: (...args) => isRestoreLockActive(...args), + normalizeChatIdCandidate, + normalizeRestoreLockState: (...args) => normalizeRestoreLockState(...args), + notifyExtractionIssue: (...args) => notifyExtractionIssue(...args), + resolveAutoExtractionPlan: (...args) => resolveAutoExtractionPlan(...args), + runExtraction: (...args) => runExtraction(...args), + setTimeout, + AUTO_EXTRACTION_DEFER_RETRY_DELAYS_MS, + AUTO_EXTRACTION_HOST_SETTLE_MS, +}); const PERSISTED_RECALL_UI_REFRESH_RETRY_DELAYS_MS = [ 0, 80, @@ -12747,22 +12762,7 @@ function isGraphLoadRetryPending(chatId = getCurrentChatId()) { } function clearPendingAutoExtraction({ resetState = true } = {}) { - if (pendingAutoExtractionTimer) { - clearTimeout(pendingAutoExtractionTimer); - pendingAutoExtractionTimer = null; - } - - if (resetState) { - pendingAutoExtraction = { - chatId: "", - messageId: null, - reason: "", - requestedAt: 0, - attempts: 0, - targetEndFloor: null, - strategy: "normal", - }; - } + return autoExtractionDeferRuntime.clearPendingAutoExtraction({ resetState }); } function deferAutoExtraction( @@ -12775,96 +12775,22 @@ function deferAutoExtraction( strategy = "", } = {}, ) { - const normalizedChatId = normalizeChatIdCandidate(chatId); - if (!normalizedChatId) { - clearPendingAutoExtraction(); - return { - scheduled: false, - reason: "missing-chat-id", - chatId: "", - }; - } - - const sameChat = normalizedChatId === pendingAutoExtraction.chatId; - const previousAttempts = sameChat - ? Math.max(0, Math.floor(Number(pendingAutoExtraction.attempts) || 0)) - : 0; - const nextAttempts = previousAttempts + 1; - const resolvedDelayMs = - delayMs !== null && - delayMs !== undefined && - Number.isFinite(Number(delayMs)) - ? Math.max(0, Math.floor(Number(delayMs))) - : AUTO_EXTRACTION_DEFER_RETRY_DELAYS_MS[ - Math.min( - previousAttempts, - AUTO_EXTRACTION_DEFER_RETRY_DELAYS_MS.length - 1, - ) - ]; - - pendingAutoExtraction = { - chatId: normalizedChatId, - messageId: Number.isFinite(Number(messageId)) - ? Math.floor(Number(messageId)) - : sameChat - ? pendingAutoExtraction.messageId - : null, - reason: String(reason || "auto-extraction-deferred"), - requestedAt: - sameChat && pendingAutoExtraction.requestedAt > 0 - ? pendingAutoExtraction.requestedAt - : Date.now(), - attempts: nextAttempts, - targetEndFloor: Number.isFinite(Number(targetEndFloor)) - ? sameChat && - Number.isFinite(Number(pendingAutoExtraction.targetEndFloor)) - ? Math.max( - Math.floor(Number(targetEndFloor)), - Math.floor(Number(pendingAutoExtraction.targetEndFloor)), - ) - : Math.floor(Number(targetEndFloor)) - : sameChat - ? pendingAutoExtraction.targetEndFloor - : null, - strategy: String(strategy || "") - ? String(strategy || "") - : sameChat - ? String(pendingAutoExtraction.strategy || "normal") - : "normal", - }; - - if (pendingAutoExtractionTimer) { - clearTimeout(pendingAutoExtractionTimer); - } - - pendingAutoExtractionTimer = setTimeout(() => { - pendingAutoExtractionTimer = null; - void maybeResumePendingAutoExtraction( - `retry:${pendingAutoExtraction.reason || "auto-extraction-deferred"}`, - ); - }, resolvedDelayMs); - console.debug?.("[ST-BME] auto extraction deferred", { - reason: pendingAutoExtraction.reason, - chatId: normalizedChatId, - messageId: pendingAutoExtraction.messageId, - targetEndFloor: pendingAutoExtraction.targetEndFloor, - strategy: pendingAutoExtraction.strategy, - attempts: nextAttempts, - delayMs: resolvedDelayMs, + return autoExtractionDeferRuntime.deferAutoExtraction(reason, { + chatId, + messageId, + delayMs, + targetEndFloor, + strategy, }); - - return { - scheduled: true, - chatId: normalizedChatId, - messageId: pendingAutoExtraction.messageId, - reason: pendingAutoExtraction.reason, - targetEndFloor: pendingAutoExtraction.targetEndFloor, - strategy: pendingAutoExtraction.strategy, - attempts: nextAttempts, - delayMs: resolvedDelayMs, - }; } +function getPendingAutoExtraction() { + return autoExtractionDeferRuntime.getPendingAutoExtraction(); +} + +function maybeResumePendingAutoExtraction(source = "auto-extraction-resume") { + return autoExtractionDeferRuntime.maybeResumePendingAutoExtraction(source); +} function resolveAutoExtractionPlan({ chat = null, settings = null, @@ -12888,194 +12814,6 @@ function resolveAutoExtractionPlan({ ); } -function maybeResumePendingAutoExtraction(source = "auto-extraction-resume") { - const pendingChatId = normalizeChatIdCandidate(pendingAutoExtraction.chatId); - if (!pendingChatId) { - return { - resumed: false, - reason: "no-pending-auto-extraction", - }; - } - - if (isRestoreLockActive()) { - return { - resumed: false, - reason: "restore-lock-active", - restoreLock: cloneRuntimeDebugValue( - normalizeRestoreLockState(graphPersistenceState.restoreLock), - null, - ), - }; - } - - const currentChatId = normalizeChatIdCandidate(getCurrentChatId()); - if (!currentChatId || currentChatId !== pendingChatId) { - clearPendingAutoExtraction(); - return { - resumed: false, - reason: "chat-switched", - chatId: pendingChatId, - currentChatId, - }; - } - - if (isExtracting) { - return deferAutoExtraction("extracting", { - chatId: pendingChatId, - messageId: pendingAutoExtraction.messageId, - targetEndFloor: pendingAutoExtraction.targetEndFloor, - strategy: pendingAutoExtraction.strategy, - }); - } - - if (isHostGenerationRunning) { - return deferAutoExtraction("generation-running", { - chatId: pendingChatId, - messageId: pendingAutoExtraction.messageId, - targetEndFloor: pendingAutoExtraction.targetEndFloor, - strategy: pendingAutoExtraction.strategy, - }); - } - - const hostGenerationSettleRemainingMs = - lastHostGenerationEndedAt > 0 - ? AUTO_EXTRACTION_HOST_SETTLE_MS - - (Date.now() - lastHostGenerationEndedAt) - : 0; - if (hostGenerationSettleRemainingMs > 0) { - return deferAutoExtraction("generation-settling", { - chatId: pendingChatId, - messageId: pendingAutoExtraction.messageId, - delayMs: hostGenerationSettleRemainingMs, - targetEndFloor: pendingAutoExtraction.targetEndFloor, - strategy: pendingAutoExtraction.strategy, - }); - } - - if (isRecoveringHistory) { - return deferAutoExtraction("history-recovering", { - chatId: pendingChatId, - messageId: pendingAutoExtraction.messageId, - targetEndFloor: pendingAutoExtraction.targetEndFloor, - strategy: pendingAutoExtraction.strategy, - }); - } - - if (!ensureGraphMutationReady("自动提取", { notify: false })) { - console.debug?.( - "[ST-BME] pending auto extraction resume blocked: graph-not-ready", - { - source, - chatId: pendingChatId, - attempts: pendingAutoExtraction.attempts || 0, - loadState: graphPersistenceState.loadState || "", - }, - ); - return deferAutoExtraction("graph-not-ready", { - chatId: pendingChatId, - messageId: pendingAutoExtraction.messageId, - targetEndFloor: pendingAutoExtraction.targetEndFloor, - strategy: pendingAutoExtraction.strategy, - }); - } - - const resumeContext = getContext(); - const resumeChat = resumeContext?.chat; - const settings = getSettings(); - let lockedEndFloor = Number.isFinite(Number(pendingAutoExtraction.targetEndFloor)) - ? Math.floor(Number(pendingAutoExtraction.targetEndFloor)) - : null; - if ( - Array.isArray(resumeChat) && - Number.isFinite(Number(pendingAutoExtraction.messageId)) - ) { - const pendingMessageIndex = Math.floor( - Number(pendingAutoExtraction.messageId), - ); - const pendingMessage = resumeChat[pendingMessageIndex]; - if ( - isAssistantChatMessage(pendingMessage, { - index: pendingMessageIndex, - chat: resumeChat, - }) && - !String(pendingMessage?.mes ?? "").trim() - ) { - return deferAutoExtraction("assistant-message-empty", { - chatId: pendingChatId, - messageId: pendingMessageIndex, - delayMs: AUTO_EXTRACTION_HOST_SETTLE_MS, - targetEndFloor: pendingAutoExtraction.targetEndFloor, - strategy: pendingAutoExtraction.strategy, - }); - } - } - - if (Array.isArray(resumeChat) && resumeChat.length > 0 && lockedEndFloor != null) { - const lockedPlan = resolveAutoExtractionPlan({ - chat: resumeChat, - settings, - lockedEndFloor, - }); - if ( - !lockedPlan.canRun && - lockedPlan.candidateAssistantTurns.length === 0 - ) { - const fallbackPlan = resolveAutoExtractionPlan({ - chat: resumeChat, - settings, - }); - lockedEndFloor = fallbackPlan.canRun - ? fallbackPlan.plannedBatchEndFloor - : null; - } - } - - const pendingRequest = { ...pendingAutoExtraction }; - clearPendingAutoExtraction(); - if (lockedEndFloor == null) { - const currentPlan = resolveAutoExtractionPlan({ - chat: resumeChat, - settings, - }); - if (!currentPlan.canRun) { - return { - resumed: false, - reason: "no-runnable-auto-extraction", - source, - ...pendingRequest, - }; - } - lockedEndFloor = currentPlan.plannedBatchEndFloor; - } - console.debug?.("[ST-BME] resuming pending auto extraction", { - source, - chatId: pendingRequest.chatId, - messageId: pendingRequest.messageId, - targetEndFloor: lockedEndFloor, - attempts: pendingRequest.attempts || 0, - }); - const enqueueMicrotask = - typeof globalThis.queueMicrotask === "function" - ? globalThis.queueMicrotask.bind(globalThis) - : (task) => Promise.resolve().then(task); - enqueueMicrotask(() => { - void runExtraction({ - lockedEndFloor, - triggerSource: source, - }).catch((error) => { - console.error("[ST-BME] 延迟自动提取失败:", error); - notifyExtractionIssue(error?.message || String(error) || "自动提取失败"); - }); - }); - - return { - resumed: true, - source, - lockedEndFloor, - ...pendingRequest, - }; -} - function markDryRunPromptPreview(ttlMs = GENERATION_RECALL_HOOK_BRIDGE_MS) { const resolvedTtlMs = Math.max( 100, diff --git a/runtime/auto-extraction-defer.js b/runtime/auto-extraction-defer.js new file mode 100644 index 0000000..823a653 --- /dev/null +++ b/runtime/auto-extraction-defer.js @@ -0,0 +1,346 @@ +export function createAutoExtractionDefer(deps = {}) { + let pendingAutoExtractionTimer = null; + let pendingAutoExtraction = { + chatId: "", + messageId: null, + reason: "", + requestedAt: 0, + attempts: 0, + targetEndFloor: null, + strategy: "normal", + }; + + const normalizeChatIdCandidate = (value = "") => + deps.normalizeChatIdCandidate?.(value) ?? String(value ?? "").trim(); + const getCurrentChatId = (...args) => deps.getCurrentChatId?.(...args); + const getContext = (...args) => deps.getContext?.(...args); + const getSettings = (...args) => deps.getSettings?.(...args); + const clearTimeoutImpl = deps.clearTimeout || globalThis.clearTimeout; + const setTimeoutImpl = deps.setTimeout || globalThis.setTimeout; + const consoleImpl = deps.console || console; + const deferRetryDelays = Array.isArray(deps.AUTO_EXTRACTION_DEFER_RETRY_DELAYS_MS) + ? deps.AUTO_EXTRACTION_DEFER_RETRY_DELAYS_MS + : [120, 320, 800, 1600, 2800]; + const hostSettleMs = Number.isFinite(Number(deps.AUTO_EXTRACTION_HOST_SETTLE_MS)) + ? Number(deps.AUTO_EXTRACTION_HOST_SETTLE_MS) + : 120; + + function getPendingAutoExtraction() { + return { ...pendingAutoExtraction }; + } + + function clearPendingAutoExtraction({ resetState = true } = {}) { + if (pendingAutoExtractionTimer) { + clearTimeoutImpl(pendingAutoExtractionTimer); + pendingAutoExtractionTimer = null; + } + + if (resetState) { + pendingAutoExtraction = { + chatId: "", + messageId: null, + reason: "", + requestedAt: 0, + attempts: 0, + targetEndFloor: null, + strategy: "normal", + }; + } + } + + function deferAutoExtraction( + reason = "auto-extraction-deferred", + { + chatId = getCurrentChatId(), + messageId = null, + delayMs = null, + targetEndFloor = null, + strategy = "", + } = {}, + ) { + const normalizedChatId = normalizeChatIdCandidate(chatId); + if (!normalizedChatId) { + clearPendingAutoExtraction(); + return { + scheduled: false, + reason: "missing-chat-id", + chatId: "", + }; + } + + const sameChat = normalizedChatId === pendingAutoExtraction.chatId; + const previousAttempts = sameChat + ? Math.max(0, Math.floor(Number(pendingAutoExtraction.attempts) || 0)) + : 0; + const nextAttempts = previousAttempts + 1; + const resolvedDelayMs = + delayMs !== null && + delayMs !== undefined && + Number.isFinite(Number(delayMs)) + ? Math.max(0, Math.floor(Number(delayMs))) + : deferRetryDelays[ + Math.min( + previousAttempts, + deferRetryDelays.length - 1, + ) + ]; + + pendingAutoExtraction = { + chatId: normalizedChatId, + messageId: Number.isFinite(Number(messageId)) + ? Math.floor(Number(messageId)) + : sameChat + ? pendingAutoExtraction.messageId + : null, + reason: String(reason || "auto-extraction-deferred"), + requestedAt: + sameChat && pendingAutoExtraction.requestedAt > 0 + ? pendingAutoExtraction.requestedAt + : Date.now(), + attempts: nextAttempts, + targetEndFloor: Number.isFinite(Number(targetEndFloor)) + ? sameChat && + Number.isFinite(Number(pendingAutoExtraction.targetEndFloor)) + ? Math.max( + Math.floor(Number(targetEndFloor)), + Math.floor(Number(pendingAutoExtraction.targetEndFloor)), + ) + : Math.floor(Number(targetEndFloor)) + : sameChat + ? pendingAutoExtraction.targetEndFloor + : null, + strategy: String(strategy || "") + ? String(strategy || "") + : sameChat + ? String(pendingAutoExtraction.strategy || "normal") + : "normal", + }; + + if (pendingAutoExtractionTimer) { + clearTimeoutImpl(pendingAutoExtractionTimer); + } + + pendingAutoExtractionTimer = setTimeoutImpl(() => { + pendingAutoExtractionTimer = null; + void maybeResumePendingAutoExtraction( + `retry:${pendingAutoExtraction.reason || "auto-extraction-deferred"}`, + ); + }, resolvedDelayMs); + consoleImpl.debug?.("[ST-BME] auto extraction deferred", { + reason: pendingAutoExtraction.reason, + chatId: normalizedChatId, + messageId: pendingAutoExtraction.messageId, + targetEndFloor: pendingAutoExtraction.targetEndFloor, + strategy: pendingAutoExtraction.strategy, + attempts: nextAttempts, + delayMs: resolvedDelayMs, + }); + + return { + scheduled: true, + chatId: normalizedChatId, + messageId: pendingAutoExtraction.messageId, + reason: pendingAutoExtraction.reason, + targetEndFloor: pendingAutoExtraction.targetEndFloor, + strategy: pendingAutoExtraction.strategy, + attempts: nextAttempts, + delayMs: resolvedDelayMs, + }; + } + + function maybeResumePendingAutoExtraction(source = "auto-extraction-resume") { + const pendingChatId = normalizeChatIdCandidate(pendingAutoExtraction.chatId); + if (!pendingChatId) { + return { + resumed: false, + reason: "no-pending-auto-extraction", + }; + } + + if (deps.isRestoreLockActive()) { + return { + resumed: false, + reason: "restore-lock-active", + restoreLock: deps.cloneRuntimeDebugValue( + deps.normalizeRestoreLockState(deps.getGraphPersistenceState?.()?.restoreLock), + null, + ), + }; + } + + const currentChatId = normalizeChatIdCandidate(getCurrentChatId()); + if (!currentChatId || currentChatId !== pendingChatId) { + clearPendingAutoExtraction(); + return { + resumed: false, + reason: "chat-switched", + chatId: pendingChatId, + currentChatId, + }; + } + + if (deps.getIsExtracting?.()) { + return deferAutoExtraction("extracting", { + chatId: pendingChatId, + messageId: pendingAutoExtraction.messageId, + targetEndFloor: pendingAutoExtraction.targetEndFloor, + strategy: pendingAutoExtraction.strategy, + }); + } + + if (deps.getIsHostGenerationRunning?.()) { + return deferAutoExtraction("generation-running", { + chatId: pendingChatId, + messageId: pendingAutoExtraction.messageId, + targetEndFloor: pendingAutoExtraction.targetEndFloor, + strategy: pendingAutoExtraction.strategy, + }); + } + + const lastHostGenerationEndedAt = Number(deps.getLastHostGenerationEndedAt?.() || 0); + const hostGenerationSettleRemainingMs = + lastHostGenerationEndedAt > 0 + ? hostSettleMs - + (Date.now() - lastHostGenerationEndedAt) + : 0; + if (hostGenerationSettleRemainingMs > 0) { + return deferAutoExtraction("generation-settling", { + chatId: pendingChatId, + messageId: pendingAutoExtraction.messageId, + delayMs: hostGenerationSettleRemainingMs, + targetEndFloor: pendingAutoExtraction.targetEndFloor, + strategy: pendingAutoExtraction.strategy, + }); + } + + if (deps.getIsRecoveringHistory?.()) { + return deferAutoExtraction("history-recovering", { + chatId: pendingChatId, + messageId: pendingAutoExtraction.messageId, + targetEndFloor: pendingAutoExtraction.targetEndFloor, + strategy: pendingAutoExtraction.strategy, + }); + } + + if (!deps.ensureGraphMutationReady("自动提取", { notify: false })) { + consoleImpl.debug?.( + "[ST-BME] pending auto extraction resume blocked: graph-not-ready", + { + source, + chatId: pendingChatId, + attempts: pendingAutoExtraction.attempts || 0, + loadState: deps.getGraphPersistenceState?.()?.loadState || "", + }, + ); + return deferAutoExtraction("graph-not-ready", { + chatId: pendingChatId, + messageId: pendingAutoExtraction.messageId, + targetEndFloor: pendingAutoExtraction.targetEndFloor, + strategy: pendingAutoExtraction.strategy, + }); + } + + const resumeContext = getContext(); + const resumeChat = resumeContext?.chat; + const settings = getSettings(); + let lockedEndFloor = Number.isFinite(Number(pendingAutoExtraction.targetEndFloor)) + ? Math.floor(Number(pendingAutoExtraction.targetEndFloor)) + : null; + if ( + Array.isArray(resumeChat) && + Number.isFinite(Number(pendingAutoExtraction.messageId)) + ) { + const pendingMessageIndex = Math.floor( + Number(pendingAutoExtraction.messageId), + ); + const pendingMessage = resumeChat[pendingMessageIndex]; + if ( + deps.isAssistantChatMessage(pendingMessage, { + index: pendingMessageIndex, + chat: resumeChat, + }) && + !String(pendingMessage?.mes ?? "").trim() + ) { + return deferAutoExtraction("assistant-message-empty", { + chatId: pendingChatId, + messageId: pendingMessageIndex, + delayMs: hostSettleMs, + targetEndFloor: pendingAutoExtraction.targetEndFloor, + strategy: pendingAutoExtraction.strategy, + }); + } + } + + if (Array.isArray(resumeChat) && resumeChat.length > 0 && lockedEndFloor != null) { + const lockedPlan = deps.resolveAutoExtractionPlan({ + chat: resumeChat, + settings, + lockedEndFloor, + }); + if ( + !lockedPlan.canRun && + lockedPlan.candidateAssistantTurns.length === 0 + ) { + const fallbackPlan = deps.resolveAutoExtractionPlan({ + chat: resumeChat, + settings, + }); + lockedEndFloor = fallbackPlan.canRun + ? fallbackPlan.plannedBatchEndFloor + : null; + } + } + + const pendingRequest = { ...pendingAutoExtraction }; + clearPendingAutoExtraction(); + if (lockedEndFloor == null) { + const currentPlan = deps.resolveAutoExtractionPlan({ + chat: resumeChat, + settings, + }); + if (!currentPlan.canRun) { + return { + resumed: false, + reason: "no-runnable-auto-extraction", + source, + ...pendingRequest, + }; + } + lockedEndFloor = currentPlan.plannedBatchEndFloor; + } + consoleImpl.debug?.("[ST-BME] resuming pending auto extraction", { + source, + chatId: pendingRequest.chatId, + messageId: pendingRequest.messageId, + targetEndFloor: lockedEndFloor, + attempts: pendingRequest.attempts || 0, + }); + const enqueueMicrotask = + typeof globalThis.queueMicrotask === "function" + ? globalThis.queueMicrotask.bind(globalThis) + : (task) => Promise.resolve().then(task); + enqueueMicrotask(() => { + void deps.runExtraction({ + lockedEndFloor, + triggerSource: source, + }).catch((error) => { + consoleImpl.error("[ST-BME] 延迟自动提取失败:", error); + deps.notifyExtractionIssue(error?.message || String(error) || "自动提取失败"); + }); + }); + + return { + resumed: true, + source, + lockedEndFloor, + ...pendingRequest, + }; + } + + return { + clearPendingAutoExtraction, + deferAutoExtraction, + maybeResumePendingAutoExtraction, + getPendingAutoExtraction, + }; +} diff --git a/tests/graph-persistence.mjs b/tests/graph-persistence.mjs index 9bb27b6..b286116 100644 --- a/tests/graph-persistence.mjs +++ b/tests/graph-persistence.mjs @@ -168,6 +168,7 @@ import { createRecallInputState } from "../runtime/recall-input-state.js"; import { createRerollRecallInput } from "../runtime/reroll-recall-input.js"; import { createGenerationRecallTransactions } from "../runtime/generation-recall-transactions.js"; import { createFinalRecallInjection } from "../runtime/final-recall-injection.js"; +import { createAutoExtractionDefer } from "../runtime/auto-extraction-defer.js"; import { consumeRerollRecallReuseMarker, createRerollRecallReuseMarker, @@ -797,6 +798,7 @@ async function createGraphPersistenceHarness({ createRerollRecallInput, createGenerationRecallTransactions, createFinalRecallInjection, + createAutoExtractionDefer, consumeRerollRecallReuseMarker, createRerollRecallReuseMarker, createRecallMessageUiController() { diff --git a/tests/helpers/generation-recall-harness.mjs b/tests/helpers/generation-recall-harness.mjs index 1f6f3c6..a53c9ed 100644 --- a/tests/helpers/generation-recall-harness.mjs +++ b/tests/helpers/generation-recall-harness.mjs @@ -66,6 +66,7 @@ import { createRecallInputState } from "../../runtime/recall-input-state.js"; import { createRerollRecallInput } from "../../runtime/reroll-recall-input.js"; import { createGenerationRecallTransactions } from "../../runtime/generation-recall-transactions.js"; import { createFinalRecallInjection } from "../../runtime/final-recall-injection.js"; +import { createAutoExtractionDefer } from "../../runtime/auto-extraction-defer.js"; const moduleDir = path.dirname(fileURLToPath(import.meta.url)); const indexPath = path.resolve(moduleDir, "../../index.js"); @@ -126,6 +127,7 @@ export function createGenerationRecallHarness(options = {}) { createRerollRecallInput, createGenerationRecallTransactions, createFinalRecallInjection, + createAutoExtractionDefer, settings: {}, graphPersistenceState: createGraphPersistenceState(), extension_settings: { [MODULE_NAME]: {} }, @@ -317,7 +319,7 @@ export function createGenerationRecallHarness(options = {}) { }; vm.createContext(context); vm.runInContext( - `${snippet}\nresult = { hashRecallInput, buildPreGenerationRecallKey, buildGenerationAfterCommandsRecallInput, buildNormalGenerationRecallInput, cleanupGenerationRecallTransactions, buildGenerationRecallTransactionId, beginGenerationRecallTransaction, markGenerationRecallTransactionHookState, shouldRunRecallForTransaction, createGenerationRecallContext, onGenerationStarted, onGenerationEnded, onGenerationAfterCommands, onBeforeCombinePrompts, applyFinalRecallInjectionForGeneration, persistRecallInjectionRecord, ensurePersistedRecallRecordForGeneration, findRecentGenerationRecallTransactionForChat, getGenerationRecallTransactionResult, generationRecallTransactions, freezeHostGenerationInputSnapshot, consumeHostGenerationInputSnapshot, getPendingHostGenerationInputSnapshot, clearPendingHostGenerationInputSnapshot, prepareRerollRecallReuse, getPendingRerollRecallReuse, clearPendingRerollRecallReuse, recordRecallSendIntent, clearPendingRecallSendIntent, recordRecallSentUserMessage, getPendingRecallSendIntent: () => pendingRecallSendIntent, getLastRecallSentUserMessage: () => lastRecallSentUserMessage, getCurrentGenerationTrivialSkip, markCurrentGenerationTrivialSkip, clearCurrentGenerationTrivialSkip, consumeCurrentGenerationTrivialSkip, deferAutoExtraction, maybeResumePendingAutoExtraction, clearPendingAutoExtraction, getPendingAutoExtraction: () => ({ ...pendingAutoExtraction }), getIsHostGenerationRunning: () => isHostGenerationRunning, preparePlannerRecallHandoff, runPlannerRecallForEna, getGraphPersistenceState: () => graphPersistenceState, setGraphPersistenceState: (value = {}) => { graphPersistenceState = { ...graphPersistenceState, ...(value || {}) }; return graphPersistenceState; } };`, + `${snippet}\nresult = { hashRecallInput, buildPreGenerationRecallKey, buildGenerationAfterCommandsRecallInput, buildNormalGenerationRecallInput, cleanupGenerationRecallTransactions, buildGenerationRecallTransactionId, beginGenerationRecallTransaction, markGenerationRecallTransactionHookState, shouldRunRecallForTransaction, createGenerationRecallContext, onGenerationStarted, onGenerationEnded, onGenerationAfterCommands, onBeforeCombinePrompts, applyFinalRecallInjectionForGeneration, persistRecallInjectionRecord, ensurePersistedRecallRecordForGeneration, findRecentGenerationRecallTransactionForChat, getGenerationRecallTransactionResult, generationRecallTransactions, freezeHostGenerationInputSnapshot, consumeHostGenerationInputSnapshot, getPendingHostGenerationInputSnapshot, clearPendingHostGenerationInputSnapshot, prepareRerollRecallReuse, getPendingRerollRecallReuse, clearPendingRerollRecallReuse, recordRecallSendIntent, clearPendingRecallSendIntent, recordRecallSentUserMessage, getPendingRecallSendIntent: () => pendingRecallSendIntent, getLastRecallSentUserMessage: () => lastRecallSentUserMessage, getCurrentGenerationTrivialSkip, markCurrentGenerationTrivialSkip, clearCurrentGenerationTrivialSkip, consumeCurrentGenerationTrivialSkip, deferAutoExtraction, maybeResumePendingAutoExtraction, clearPendingAutoExtraction, getPendingAutoExtraction, getIsHostGenerationRunning: () => isHostGenerationRunning, preparePlannerRecallHandoff, runPlannerRecallForEna, getGraphPersistenceState: () => graphPersistenceState, setGraphPersistenceState: (value = {}) => { graphPersistenceState = { ...graphPersistenceState, ...(value || {}) }; return graphPersistenceState; } };`, context, { filename: indexPath }, );