export function createRecallInputState(deps = {}) { let currentGenerationTrivialSkip = null; const getPendingRecallSendIntent = () => deps.getPendingRecallSendIntent?.() ?? deps.createRecallInputRecord?.(); const setPendingRecallSendIntent = (record) => { deps.setPendingRecallSendIntent?.(record); return record; }; const getPendingHostGenerationInputSnapshot = () => deps.getPendingHostGenerationInputSnapshot?.() ?? deps.createRecallInputRecord?.(); const setPendingHostGenerationInputSnapshot = (record) => { deps.setPendingHostGenerationInputSnapshot?.(record); return record; }; const getLastRecallSentUserMessage = () => deps.getLastRecallSentUserMessage?.() ?? deps.createRecallInputRecord?.(); const setLastRecallSentUserMessage = (record) => { deps.setLastRecallSentUserMessage?.(record); return record; }; const getCurrentChatId = (...args) => deps.getCurrentChatId?.(...args); const normalizeChatIdCandidate = (value = "") => deps.normalizeChatIdCandidate?.(value) ?? String(value ?? "").trim(); const normalizeRecallInputText = (value = "") => deps.normalizeRecallInputText?.(value) ?? String(value || "").trim(); const createRecallInputRecord = (record = {}) => deps.createRecallInputRecord?.(record) ?? { ...(record || {}) }; const hashRecallInput = (value = "") => deps.hashRecallInput?.(value) ?? ""; const isFreshRecallInputRecord = (record) => deps.isFreshRecallInputRecord?.(record) ?? Boolean(record?.text); const getTrivialGenerationSkipTtlMs = () => Number.isFinite(Number(deps.TRIVIAL_GENERATION_SKIP_TTL_MS)) ? Number(deps.TRIVIAL_GENERATION_SKIP_TTL_MS) : 60000; function freezeHostGenerationInputSnapshot( text, source = "host-generation-lifecycle", ) { const normalized = normalizeRecallInputText(text); if (!normalized) return null; const nextSnapshot = createRecallInputRecord({ text: normalized, hash: hashRecallInput(normalized), source, at: Date.now(), }); setPendingHostGenerationInputSnapshot(nextSnapshot); return nextSnapshot; } function consumeHostGenerationInputSnapshot(options = {}) { const { preserve = false } = options; const pendingHostGenerationInputSnapshot = getPendingHostGenerationInputSnapshot(); if (!isFreshRecallInputRecord(pendingHostGenerationInputSnapshot)) { if (!preserve) { setPendingHostGenerationInputSnapshot(createRecallInputRecord()); } return createRecallInputRecord(); } const snapshot = createRecallInputRecord({ ...pendingHostGenerationInputSnapshot, }); if (!preserve) { setPendingHostGenerationInputSnapshot(createRecallInputRecord()); } return snapshot; } function readPendingHostGenerationInputSnapshot() { return getPendingHostGenerationInputSnapshot(); } function clearPendingRecallSendIntent() { const nextRecord = createRecallInputRecord(); setPendingRecallSendIntent(nextRecord); return nextRecord; } function clearPendingHostGenerationInputSnapshot() { const nextSnapshot = createRecallInputRecord(); setPendingHostGenerationInputSnapshot(nextSnapshot); return nextSnapshot; } function getCurrentGenerationTrivialSkip( chatId = getCurrentChatId(), now = Date.now(), ) { if (!currentGenerationTrivialSkip) return null; const setAtMs = Number(currentGenerationTrivialSkip.setAtMs) || 0; if ( !setAtMs || now - setAtMs > getTrivialGenerationSkipTtlMs() ) { currentGenerationTrivialSkip = null; return null; } const normalizedChatId = normalizeChatIdCandidate(chatId); const activeChatId = normalizeChatIdCandidate( currentGenerationTrivialSkip.chatId, ); if (normalizedChatId && activeChatId && normalizedChatId !== activeChatId) { return null; } return currentGenerationTrivialSkip; } function markCurrentGenerationTrivialSkip({ reason = "", chatId = getCurrentChatId(), chatLength = 0, } = {}) { currentGenerationTrivialSkip = { chatId: normalizeChatIdCandidate(chatId), setAtMs: Date.now(), reason: String(reason || ""), generationStartMinChatIndex: Math.max( 0, Math.floor(Number(chatLength) || 0), ), }; return currentGenerationTrivialSkip; } function clearCurrentGenerationTrivialSkip(_reason = "") { const previous = currentGenerationTrivialSkip; currentGenerationTrivialSkip = null; return previous; } function consumeCurrentGenerationTrivialSkip( targetMessageIndex, chatId = getCurrentChatId(), now = Date.now(), ) { const activeSkip = getCurrentGenerationTrivialSkip(chatId, now); if (!activeSkip) return false; const normalizedTargetIndex = Number.isFinite(Number(targetMessageIndex)) ? Math.floor(Number(targetMessageIndex)) : null; if (!Number.isFinite(normalizedTargetIndex)) { return false; } if ( normalizedTargetIndex < Math.max(0, Math.floor(Number(activeSkip.generationStartMinChatIndex) || 0)) ) { return false; } currentGenerationTrivialSkip = null; return true; } function recordRecallSendIntent(text, source = "dom-intent") { const normalized = normalizeRecallInputText(text); if (!normalized) return createRecallInputRecord(); const hash = hashRecallInput(normalized); const pendingRecallSendIntent = getPendingRecallSendIntent(); const previousRecord = isFreshRecallInputRecord(pendingRecallSendIntent) ? pendingRecallSendIntent : null; const previousHash = String(previousRecord?.hash || ""); const previousText = String(previousRecord?.text || ""); if (previousHash && previousHash === hash && previousText === normalized) { const nextRecord = createRecallInputRecord({ ...previousRecord, at: Date.now(), source: String(source || previousRecord.source || "dom-intent"), }); setPendingRecallSendIntent(nextRecord); return nextRecord; } const nextRecord = createRecallInputRecord({ text: normalized, hash, source, at: Date.now(), }); setPendingRecallSendIntent(nextRecord); return nextRecord; } function recordRecallSentUserMessage(messageId, text, source = "message-sent") { const normalized = normalizeRecallInputText(text); if (!normalized) return createRecallInputRecord(); const hash = hashRecallInput(normalized); const nextRecord = createRecallInputRecord({ text: normalized, hash, messageId: Number.isFinite(messageId) ? messageId : null, source, at: Date.now(), }); setLastRecallSentUserMessage(nextRecord); if (typeof deps.recordMessageTraceSnapshot === "function") { deps.recordMessageTraceSnapshot({ lastSentUserMessage: { text: normalized, hash, messageId: Number.isFinite(messageId) ? messageId : null, source, updatedAt: new Date().toISOString(), }, }); } // 注意:不再在 MESSAGE_SENT 阶段清空 pendingRecallSendIntent / // pendingHostGenerationInputSnapshot / transactions。 // 这些数据在 GENERATION_AFTER_COMMANDS 中被消费;MESSAGE_SENT 先于 // GENERATION_AFTER_COMMANDS 触发,提前清空会导致召回拿不到用户输入。 // 真正的消费发生在 recall 执行后(runRecallController 内部)。 return nextRecord; } function clearRecallInputTracking() { clearPendingRecallSendIntent(); setLastRecallSentUserMessage(createRecallInputRecord()); clearPendingHostGenerationInputSnapshot(); deps.clearPendingRerollRecallReuse?.("recall-input-tracking-cleared"); if (typeof deps.recordMessageTraceSnapshot === "function") { deps.recordMessageTraceSnapshot({ lastSentUserMessage: null, }); } deps.clearPlannerRecallHandoffsForChat?.("", { clearAll: true }); } return { freezeHostGenerationInputSnapshot, consumeHostGenerationInputSnapshot, getPendingHostGenerationInputSnapshot: readPendingHostGenerationInputSnapshot, clearPendingHostGenerationInputSnapshot, recordRecallSendIntent, clearPendingRecallSendIntent, recordRecallSentUserMessage, getCurrentGenerationTrivialSkip, markCurrentGenerationTrivialSkip, clearCurrentGenerationTrivialSkip, consumeCurrentGenerationTrivialSkip, clearRecallInputTracking, getLastRecallSentUserMessage, getPendingRecallSendIntent, }; }