Files
ST-Bionic-Memory-Ecology/runtime/reroll-recall-input.js

446 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

export function createRerollRecallInput(deps = {}) {
let pendingRerollRecallReuse = null;
const plannerRecallHandoffs = new Map();
const getContext = (...args) => deps.getContext?.(...args);
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 hashRecallInput = (value = "") => deps.hashRecallInput?.(value) ?? "";
const getLastRecallSentUserMessage = () =>
deps.getLastRecallSentUserMessage?.() || {};
const getPendingRecallSendIntent = () =>
deps.getPendingRecallSendIntent?.() || {};
const getGenerationRecallTransactionTtlMs = () =>
Number.isFinite(Number(deps.GENERATION_RECALL_TRANSACTION_TTL_MS))
? Number(deps.GENERATION_RECALL_TRANSACTION_TTL_MS)
: 60000;
const getPlannerRecallHandoffTtlMs = () =>
Number.isFinite(Number(deps.PLANNER_RECALL_HANDOFF_TTL_MS))
? Number(deps.PLANNER_RECALL_HANDOFF_TTL_MS)
: 60000;
function getPendingRerollRecallReuse() {
return pendingRerollRecallReuse;
}
function clearPendingRerollRecallReuse(reason = "") {
const previous = pendingRerollRecallReuse;
pendingRerollRecallReuse = null;
return previous;
}
function prepareRerollRecallReuse({ fromFloor = null, meta = null } = {}) {
const context = getContext();
const chat = context?.chat;
if (!Array.isArray(chat) || chat.length === 0) {
pendingRerollRecallReuse = null;
return null;
}
const latestUser = deps.findLatestUserChatMessageWithIndex(chat);
const targetUserMessageIndex = Number.isFinite(latestUser?.index)
? latestUser.index
: null;
if (!Number.isFinite(targetUserMessageIndex)) {
pendingRerollRecallReuse = null;
return null;
}
const userMessage = chat[targetUserMessageIndex];
const userText = normalizeRecallInputText(userMessage?.mes || "");
if (!userText) {
pendingRerollRecallReuse = null;
return null;
}
const persistedRecord = deps.readPersistedRecallFromUserMessage(
chat,
targetUserMessageIndex,
);
const chatId = normalizeChatIdCandidate(getCurrentChatId(context));
const prepared = deps.createRerollRecallReuseMarker({
chatId,
fromFloor,
targetUserMessageIndex,
userText,
persistedRecord,
hashRecallInput,
now: Date.now(),
meta,
});
if (!prepared.marker) {
pendingRerollRecallReuse = null;
return null;
}
pendingRerollRecallReuse = prepared.marker;
return pendingRerollRecallReuse;
}
function consumePendingRerollRecallReuse(chat = getContext()?.chat) {
const reuse = pendingRerollRecallReuse;
if (!reuse) return null;
const activeChatId = normalizeChatIdCandidate(getCurrentChatId());
const latestUser = deps.findLatestUserChatMessageWithIndex(chat);
const targetUserMessageIndex = Number.isFinite(latestUser?.index)
? latestUser.index
: reuse.targetUserMessageIndex;
const userText = normalizeRecallInputText(chat?.[targetUserMessageIndex]?.mes || "");
const consumed = deps.consumeRerollRecallReuseMarker({
marker: reuse,
activeChatId,
latestUserMessageIndex: targetUserMessageIndex,
currentUserText: userText,
hashRecallInput,
now: Date.now(),
ttlMs: getGenerationRecallTransactionTtlMs(),
});
pendingRerollRecallReuse = consumed.marker;
return consumed.consumed ? consumed.override : null;
}
function buildGenerationAfterCommandsRecallInput(type, params = {}, chat) {
if (params?.automatic_trigger || params?.quiet_prompt) {
return null;
}
const generationType = String(type || "").trim() || "normal";
if (!["normal", "continue", "regenerate", "swipe"].includes(generationType)) {
return null;
}
const targetUserMessageIndex = deps.resolveGenerationTargetUserMessageIndex(chat, {
generationType,
});
// 对于 history 类型continue/regenerate/swipe必须依赖 chat 中的用户消息
if (generationType !== "normal") {
if (!Number.isFinite(targetUserMessageIndex)) {
return {
generationType,
targetUserMessageIndex: null,
};
}
const historyInput = buildHistoryGenerationRecallInput(chat);
if (!historyInput) {
return {
generationType,
targetUserMessageIndex,
};
}
return {
...historyInput,
generationType,
targetUserMessageIndex,
};
}
// 对于 normal 类型GENERATION_AFTER_COMMANDS 触发时用户消息可能不在 chat 末尾
// ST 可能已追加空 assistant 消息)。如果 chat 中存在任何用户消息,
// 继续走 buildNormalGenerationRecallInput它会通过 latestUserText 兜底找到。
// 如果 chat 中完全没有用户消息,则延迟到 BEFORE_COMBINE_PROMPTS 处理。
if (!Number.isFinite(targetUserMessageIndex) && !deps.getLatestUserChatMessage(chat)) {
return {
generationType,
targetUserMessageIndex: null,
};
}
const normalInput = buildNormalGenerationRecallInput(chat, {
frozenInputSnapshot: params?.frozenInputSnapshot,
});
return normalInput;
}
function buildNormalGenerationRecallInput(chat, options = {}) {
const rerollReuse = consumePendingRerollRecallReuse(chat);
if (rerollReuse) {
return rerollReuse;
}
const lastNonSystemMessage = deps.getLastNonSystemChatMessage(chat);
const tailUserText = lastNonSystemMessage?.is_user
? normalizeRecallInputText(lastNonSystemMessage?.mes || "")
: "";
// 当 GENERATION_AFTER_COMMANDS 触发时ST 可能已追加了空 assistant 消息。
// 导致 lastNonSystemMessage 不是 user。用 getLatestUserChatMessage 反向扫描
// 定位真正的用户消息(与 shujuku 参考实现一致)。
const latestUserMessage = !tailUserText ? deps.getLatestUserChatMessage(chat) : null;
const latestUserText = latestUserMessage
? normalizeRecallInputText(latestUserMessage?.mes || "")
: "";
const targetUserMessageIndex = deps.resolveGenerationTargetUserMessageIndex(chat, {
generationType: "normal",
});
const frozenInputSnapshot = deps.isFreshRecallInputRecord(
options?.frozenInputSnapshot,
)
? options.frozenInputSnapshot
: null;
const pendingRecallSendIntent = getPendingRecallSendIntent();
const pendingSendIntent = deps.isFreshRecallInputRecord(pendingRecallSendIntent)
? pendingRecallSendIntent
: null;
const sendIntentText = normalizeRecallInputText(
pendingSendIntent?.text || "",
);
const hostSnapshotText = normalizeRecallInputText(
frozenInputSnapshot?.text || "",
);
const textareaText = normalizeRecallInputText(deps.getSendTextareaValue());
const sourceCandidates = [
sendIntentText
? {
text: sendIntentText,
source: "send-intent",
sourceLabel: "发送意图",
reason: tailUserText
? "send-intent-overrides-chat-tail"
: "send-intent-captured",
includeSyntheticUserMessage: !tailUserText,
}
: null,
hostSnapshotText
? {
text: hostSnapshotText,
source: String(
frozenInputSnapshot?.source || "host-generation-lifecycle",
),
sourceLabel: "宿主发送快照",
reason: sendIntentText
? "host-snapshot-suppressed-by-send-intent"
: tailUserText
? "host-snapshot-suppressed-by-chat-tail"
: "host-snapshot-captured",
includeSyntheticUserMessage: !tailUserText,
}
: null,
tailUserText
? {
text: tailUserText,
source: "chat-tail-user",
sourceLabel: "当前用户楼层",
reason:
sendIntentText || hostSnapshotText
? "chat-tail-deprioritized"
: "chat-tail-fallback",
includeSyntheticUserMessage: false,
}
: null,
latestUserText
? {
text: latestUserText,
source: "chat-latest-user",
sourceLabel: "最近用户消息",
reason:
sendIntentText || hostSnapshotText || tailUserText
? "latest-user-deprioritized"
: "latest-user-fallback",
includeSyntheticUserMessage: false,
}
: null,
textareaText
? {
text: textareaText,
source: "textarea-live",
sourceLabel: "输入框当前文本",
reason:
sendIntentText || hostSnapshotText || tailUserText
? "textarea-live-deprioritized"
: "textarea-live-fallback",
includeSyntheticUserMessage: !tailUserText,
}
: null,
].filter(Boolean);
const activeTrivialSkip = deps.getCurrentGenerationTrivialSkip();
if (activeTrivialSkip) {
deps.clearPendingRecallSendIntent();
deps.clearPendingHostGenerationInputSnapshot();
return deps.createTrivialRecallSkipSentinel(activeTrivialSkip.reason);
}
const selectedCandidate = sourceCandidates[0] || null;
if (!selectedCandidate?.text) return null;
const trivialInputResult = deps.isTrivialUserInput(selectedCandidate.text);
if (trivialInputResult.trivial) {
deps.clearPendingRecallSendIntent();
deps.clearPendingHostGenerationInputSnapshot();
deps.markCurrentGenerationTrivialSkip({
reason: trivialInputResult.reason,
chatId: getCurrentChatId(),
chatLength: Array.isArray(chat) ? chat.length : 0,
});
deps.console?.info?.(
`[ST-BME] trivial-input skip: reason=${trivialInputResult.reason} len=${trivialInputResult.normalizedText.length} hook=build-normal-input`,
);
return deps.createTrivialRecallSkipSentinel(trivialInputResult.reason);
}
return {
overrideUserMessage: selectedCandidate.text,
generationType: "normal",
targetUserMessageIndex,
overrideSource: selectedCandidate.source,
overrideSourceLabel: selectedCandidate.sourceLabel,
overrideReason: selectedCandidate.reason,
sourceCandidates,
includeSyntheticUserMessage: selectedCandidate.includeSyntheticUserMessage,
};
}
function buildHistoryGenerationRecallInput(chat) {
const lastRecallSentUserMessage = getLastRecallSentUserMessage();
const latestUserText = normalizeRecallInputText(
deps.getLatestUserChatMessage(chat)?.mes || lastRecallSentUserMessage.text,
);
if (!latestUserText) return null;
const targetUserMessageIndex = deps.resolveGenerationTargetUserMessageIndex(chat, {
generationType: "history",
});
return {
overrideUserMessage: latestUserText,
generationType: "history",
targetUserMessageIndex,
overrideSource: Number.isFinite(targetUserMessageIndex)
? "chat-last-user"
: "chat-last-user-missing",
overrideSourceLabel: Number.isFinite(targetUserMessageIndex)
? "历史最后用户楼层"
: "历史用户楼层缺失",
includeSyntheticUserMessage: false,
};
}
function cleanupPlannerRecallHandoffs(now = Date.now()) {
for (const [chatId, handoff] of plannerRecallHandoffs.entries()) {
if (
!handoff ||
String(handoff.chatId || "") !== String(chatId || "") ||
now - Number(handoff.updatedAt || handoff.createdAt || 0) >
getPlannerRecallHandoffTtlMs()
) {
plannerRecallHandoffs.delete(chatId);
}
}
}
function peekPlannerRecallHandoff(
chatId = getCurrentChatId(),
now = Date.now(),
) {
cleanupPlannerRecallHandoffs(now);
const normalizedChatId = normalizeChatIdCandidate(chatId);
if (!normalizedChatId) return null;
const handoff = plannerRecallHandoffs.get(normalizedChatId) || null;
if (!handoff) return null;
if (
now - Number(handoff.updatedAt || handoff.createdAt || 0) >
getPlannerRecallHandoffTtlMs()
) {
plannerRecallHandoffs.delete(normalizedChatId);
return null;
}
return handoff;
}
function clearPlannerRecallHandoffsForChat(
chatId = getCurrentChatId(),
{ clearAll = false } = {},
) {
cleanupPlannerRecallHandoffs();
if (clearAll) {
const removed = plannerRecallHandoffs.size;
plannerRecallHandoffs.clear();
return removed;
}
const normalizedChatId = normalizeChatIdCandidate(chatId);
if (!normalizedChatId) return 0;
return plannerRecallHandoffs.delete(normalizedChatId) ? 1 : 0;
}
function consumePlannerRecallHandoff(
chatId = getCurrentChatId(),
{ handoffId = "" } = {},
) {
const normalizedChatId = normalizeChatIdCandidate(chatId);
if (!normalizedChatId) return null;
const handoff = peekPlannerRecallHandoff(normalizedChatId);
if (!handoff) return null;
if (handoffId && String(handoff.id || "") !== String(handoffId || "")) {
return null;
}
plannerRecallHandoffs.delete(normalizedChatId);
return handoff;
}
function preparePlannerRecallHandoff({
rawUserInput = "",
plannerAugmentedMessage = "",
plannerRecall = null,
chatId = getCurrentChatId(),
} = {}) {
const normalizedChatId = normalizeChatIdCandidate(chatId);
const normalizedRawUserInput = normalizeRecallInputText(rawUserInput);
const normalizedPlannerAugmentedMessage = normalizeRecallInputText(
plannerAugmentedMessage,
);
const result = plannerRecall?.result || null;
if (!normalizedChatId || !normalizedRawUserInput || !result) {
return null;
}
cleanupPlannerRecallHandoffs();
const createdAt = Date.now();
const injectionText = normalizeRecallInputText(
plannerRecall?.memoryBlock || deps.formatInjection(result, deps.getSchema()),
);
const handoff = {
id: [
normalizedChatId,
hashRecallInput(normalizedRawUserInput),
createdAt,
].join(":"),
chatId: normalizedChatId,
rawUserInput: normalizedRawUserInput,
plannerAugmentedMessage: normalizedPlannerAugmentedMessage,
result,
recentMessages: Array.isArray(plannerRecall?.recentMessages)
? plannerRecall.recentMessages.map((item) => String(item || ""))
: [],
injectionText,
source: "planner-handoff",
sourceLabel: "Planner handoff",
createdAt,
updatedAt: createdAt,
};
plannerRecallHandoffs.set(normalizedChatId, handoff);
return handoff;
}
return {
prepareRerollRecallReuse,
getPendingRerollRecallReuse,
clearPendingRerollRecallReuse,
consumePendingRerollRecallReuse,
buildNormalGenerationRecallInput,
buildHistoryGenerationRecallInput,
buildGenerationAfterCommandsRecallInput,
preparePlannerRecallHandoff,
cleanupPlannerRecallHandoffs,
peekPlannerRecallHandoff,
clearPlannerRecallHandoffsForChat,
consumePlannerRecallHandoff,
};
}