refactor(runtime): extract recall input/intent state factory (Phase 4a)

This commit is contained in:
youzini
2026-05-31 11:40:46 +00:00
parent 5a2bf037f8
commit 42011201b9
4 changed files with 313 additions and 151 deletions

197
index.js
View File

@@ -160,6 +160,7 @@ import {
consumeRerollRecallReuseMarker,
createRerollRecallReuseMarker,
} from "./runtime/reroll-transaction-boundary.js";
import { createRecallInputState } from "./runtime/recall-input-state.js";
import {
extractMemories,
generateReflection,
@@ -1349,7 +1350,31 @@ let pendingRecallSendIntent = createRecallInputRecord();
let lastRecallSentUserMessage = createRecallInputRecord();
let pendingHostGenerationInputSnapshot = createRecallInputRecord();
let pendingRerollRecallReuse = null;
let currentGenerationTrivialSkip = null;
const recallInputState = createRecallInputState({
createRecallInputRecord,
getCurrentChatId,
getLastRecallSentUserMessage: () => lastRecallSentUserMessage,
getPendingHostGenerationInputSnapshot: () => pendingHostGenerationInputSnapshot,
getPendingRecallSendIntent: () => pendingRecallSendIntent,
hashRecallInput,
isFreshRecallInputRecord,
normalizeChatIdCandidate,
normalizeRecallInputText,
recordMessageTraceSnapshot: (patch) => recordMessageTraceSnapshot(patch),
setLastRecallSentUserMessage: (record) => {
lastRecallSentUserMessage = record;
},
setPendingHostGenerationInputSnapshot: (record) => {
pendingHostGenerationInputSnapshot = record;
},
setPendingRecallSendIntent: (record) => {
pendingRecallSendIntent = record;
},
clearPendingRerollRecallReuse: (...args) => clearPendingRerollRecallReuse(...args),
clearPlannerRecallHandoffsForChat: (...args) =>
clearPlannerRecallHandoffsForChat(...args),
TRIVIAL_GENERATION_SKIP_TTL_MS,
});
let coreEventBindingState = {
registered: false,
cleanups: [],
@@ -5138,18 +5163,8 @@ function restoreRecallUiStateFromPersistence(chat = getContext()?.chat) {
}
function clearRecallInputTracking() {
clearPendingRecallSendIntent();
lastRecallSentUserMessage = createRecallInputRecord();
clearPendingHostGenerationInputSnapshot();
clearPendingRerollRecallReuse("recall-input-tracking-cleared");
if (typeof recordMessageTraceSnapshot === "function") {
recordMessageTraceSnapshot({
lastSentUserMessage: null,
});
}
clearPlannerRecallHandoffsForChat("", { clearAll: true });
return recallInputState.clearRecallInputTracking();
}
function getCoreEventBindingState() {
return coreEventBindingState;
}
@@ -5189,74 +5204,30 @@ 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;
return recallInputState.freezeHostGenerationInputSnapshot(text, source);
}
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;
return recallInputState.consumeHostGenerationInputSnapshot(options);
}
function getPendingHostGenerationInputSnapshot() {
return pendingHostGenerationInputSnapshot;
return recallInputState.getPendingHostGenerationInputSnapshot();
}
function clearPendingRecallSendIntent() {
pendingRecallSendIntent = createRecallInputRecord();
return pendingRecallSendIntent;
return recallInputState.clearPendingRecallSendIntent();
}
function clearPendingHostGenerationInputSnapshot() {
pendingHostGenerationInputSnapshot = createRecallInputRecord();
return pendingHostGenerationInputSnapshot;
return recallInputState.clearPendingHostGenerationInputSnapshot();
}
function getCurrentGenerationTrivialSkip(
chatId = getCurrentChatId(),
now = Date.now(),
) {
if (!currentGenerationTrivialSkip) return null;
const setAtMs = Number(currentGenerationTrivialSkip.setAtMs) || 0;
if (
!setAtMs ||
now - setAtMs > TRIVIAL_GENERATION_SKIP_TTL_MS
) {
currentGenerationTrivialSkip = null;
return null;
}
const normalizedChatId = normalizeChatIdCandidate(chatId);
const activeChatId = normalizeChatIdCandidate(
currentGenerationTrivialSkip.chatId,
);
if (normalizedChatId && activeChatId && normalizedChatId !== activeChatId) {
return null;
}
return currentGenerationTrivialSkip;
return recallInputState.getCurrentGenerationTrivialSkip(chatId, now);
}
function markCurrentGenerationTrivialSkip({
@@ -5264,22 +5235,15 @@ function markCurrentGenerationTrivialSkip({
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;
return recallInputState.markCurrentGenerationTrivialSkip({
reason,
chatId,
chatLength,
});
}
function clearCurrentGenerationTrivialSkip(_reason = "") {
const previous = currentGenerationTrivialSkip;
currentGenerationTrivialSkip = null;
return previous;
return recallInputState.clearCurrentGenerationTrivialSkip(_reason);
}
function consumeCurrentGenerationTrivialSkip(
@@ -5287,89 +5251,20 @@ function consumeCurrentGenerationTrivialSkip(
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;
return recallInputState.consumeCurrentGenerationTrivialSkip(
targetMessageIndex,
chatId,
now,
);
}
function recordRecallSendIntent(text, source = "dom-intent") {
const normalized = normalizeRecallInputText(text);
if (!normalized) return createRecallInputRecord();
const hash = hashRecallInput(normalized);
const previousRecord = isFreshRecallInputRecord(pendingRecallSendIntent)
? pendingRecallSendIntent
: null;
const previousHash = String(previousRecord?.hash || "");
const previousText = String(previousRecord?.text || "");
if (previousHash && previousHash === hash && previousText === normalized) {
pendingRecallSendIntent = createRecallInputRecord({
...previousRecord,
at: Date.now(),
source: String(source || previousRecord.source || "dom-intent"),
});
return pendingRecallSendIntent;
}
pendingRecallSendIntent = createRecallInputRecord({
text: normalized,
hash,
source,
at: Date.now(),
});
return pendingRecallSendIntent;
return recallInputState.recordRecallSendIntent(text, source);
}
function recordRecallSentUserMessage(messageId, text, source = "message-sent") {
const normalized = normalizeRecallInputText(text);
if (!normalized) return createRecallInputRecord();
const hash = hashRecallInput(normalized);
lastRecallSentUserMessage = createRecallInputRecord({
text: normalized,
hash,
messageId: Number.isFinite(messageId) ? messageId : null,
source,
at: Date.now(),
});
if (typeof recordMessageTraceSnapshot === "function") {
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 lastRecallSentUserMessage;
return recallInputState.recordRecallSentUserMessage(messageId, text, source);
}
function getMessageRecallRecord(messageIndex) {
const chat = getContext()?.chat;
return readPersistedRecallFromUserMessage(chat, messageIndex);

View File

@@ -0,0 +1,259 @@
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,
};
}

View File

@@ -160,6 +160,7 @@ import {
normalizeRecallInputText,
normalizeStageNoticeLevel,
} from "../ui/ui-status.js";
import { createRecallInputState } from "../runtime/recall-input-state.js";
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
const indexPath = path.resolve(moduleDir, "../index.js");
@@ -781,6 +782,7 @@ async function createGraphPersistenceHarness({
return null;
},
},
createRecallInputState,
createRecallMessageUiController() {
return {
refreshPersistedRecallMessageUi: () => ({

View File

@@ -62,6 +62,7 @@ import {
consumeRerollRecallReuseMarker,
createRerollRecallReuseMarker,
} from "../../runtime/reroll-transaction-boundary.js";
import { createRecallInputState } from "../../runtime/recall-input-state.js";
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
const indexPath = path.resolve(moduleDir, "../../index.js");
@@ -102,6 +103,9 @@ export function createGenerationRecallHarness(options = {}) {
},
result: null,
currentGraph: {},
pendingRecallSendIntent: createRecallInputRecord(),
lastRecallSentUserMessage: createRecallInputRecord(),
pendingHostGenerationInputSnapshot: createRecallInputRecord(),
_panelModule: null,
defaultSettings,
mergePersistedSettings,
@@ -115,6 +119,7 @@ export function createGenerationRecallHarness(options = {}) {
recordAuthorityAcceptedRevision,
consumeRerollRecallReuseMarker,
createRerollRecallReuseMarker,
createRecallInputState,
settings: {},
graphPersistenceState: createGraphPersistenceState(),
extension_settings: { [MODULE_NAME]: {} },
@@ -273,6 +278,7 @@ export function createGenerationRecallHarness(options = {}) {
recordInjectionSnapshot: (_kind, snapshot = {}) => {
context.recordedInjectionSnapshots.push({ ...snapshot });
},
recordMessageTraceSnapshot() {},
schedulePersistedRecallMessageUiRefresh: () => {
context.recallUiRefreshCalls += 1;
},