Merge pull request #8 from Hao19911125/main

用户输入小于2token不会启用插件
This commit is contained in:
youzini
2026-04-05 22:57:45 +08:00
committed by GitHub
10 changed files with 922 additions and 261 deletions

3
.gitignore vendored
View File

@@ -6,3 +6,6 @@ yarn-error.log*
pnpm-debug.log*
.DS_Store
Thumbs.db
skip-trivial-user-input-plan.md
CLAUDE.md
AGENTS.md

View File

@@ -1274,6 +1274,10 @@ async function runPlanningOnce(rawUserInput, silent = false, options = {}) {
function getSendTextarea() { return document.getElementById('send_textarea'); }
function getSendButton() { return document.getElementById('send_but') || document.getElementById('send_button'); }
function isTrivialPlannerInput(text) {
return _bmeRuntime?.isTrivialUserInput?.(text)?.trivial === true;
}
function shouldInterceptNow() {
const s = ensureSettings();
if (!s.enabled || state.isPlanning) return false;
@@ -1281,6 +1285,7 @@ function shouldInterceptNow() {
if (!ta) return false;
const txt = String(ta.value ?? '').trim();
if (!txt) return false;
if (isTrivialPlannerInput(txt)) return false;
if (state.bypassNextSend) return false;
if (s.skipIfPlotPresent && /<plot\b/i.test(txt)) return false;
return true;
@@ -1293,6 +1298,7 @@ async function doInterceptAndPlanThenSend() {
const raw = String(ta.value ?? '').trim();
if (!raw) return;
if (isTrivialPlannerInput(raw)) return;
state.isPlanning = true;
setSendUIBusy(true);

View File

@@ -212,6 +212,7 @@ export function onChatChangedController(runtime) {
source: "chat-changed",
force: true,
});
runtime.clearCurrentGenerationTrivialSkip?.("chat-changed");
runtime.clearInjectionState();
runtime.clearRecallInputTracking();
runtime.installSendIntentHooks();
@@ -250,6 +251,10 @@ export function onMessageSentController(runtime, messageId) {
}
if (!message?.is_user) return;
if (runtime.isTrivialUserInput?.(message.mes || "")?.trivial) {
runtime.refreshPersistedRecallMessageUi?.();
return;
}
runtime.recordRecallSentUserMessage(
resolvedMessageId,
message.mes || "",
@@ -310,8 +315,22 @@ export function onGenerationStartedController(
: "";
const snapshotText =
runtime.normalizeRecallInputText?.(pendingIntentText || textareaText) || "";
if (!snapshotText) return null;
const trivialInputResult = runtime.isTrivialUserInput?.(snapshotText);
if (trivialInputResult?.trivial) {
const context = runtime.getContext?.() || {};
runtime.markCurrentGenerationTrivialSkip?.({
reason: trivialInputResult.reason,
chatId: context?.chatId || "",
chatLength: Array.isArray(context?.chat) ? context.chat.length : 0,
});
runtime.clearPendingRecallSendIntent?.();
runtime.clearPendingHostGenerationInputSnapshot?.();
console.info?.(
`[ST-BME] trivial-input skip: reason=${trivialInputResult.reason} len=${trivialInputResult.normalizedText.length} hook=GENERATION_STARTED`,
);
return null;
}
runtime.clearCurrentGenerationTrivialSkip?.("new-non-trivial-generation");
return runtime.freezeHostGenerationInputSnapshot(
snapshotText,
pendingIntentText
@@ -417,6 +436,10 @@ export async function onGenerationAfterCommandsController(
console.warn("[ST-BME:DIAG] EXIT: buildGenerationAfterCommandsRecallInput returned null");
return;
}
if (recallOptions?.__trivialSkip) {
console.warn("[ST-BME:DIAG] EXIT: trivial-input-skip");
return;
}
console.warn("[ST-BME:DIAG] recallOptions:", { generationType: recallOptions.generationType, overrideUserMessage: recallOptions.overrideUserMessage?.slice(0,50), overrideSource: recallOptions.overrideSource, targetIdx: recallOptions.targetUserMessageIndex });
const recallContext = runtime.createGenerationRecallContext({
@@ -523,10 +546,18 @@ export async function onBeforeCombinePromptsController(
{};
const context = runtime.getContext();
const chat = context?.chat;
const normalInput = runtime.buildNormalGenerationRecallInput(chat, {
frozenInputSnapshot,
});
if (normalInput?.__trivialSkip) {
console.warn("[ST-BME:DIAG] EXIT: trivial-input-skip");
return {
skipped: true,
reason: `trivial:${normalInput.trivialReason || ""}`,
};
}
const recallOptions =
runtime.buildNormalGenerationRecallInput(chat, {
frozenInputSnapshot,
}) ||
normalInput ||
runtime.buildHistoryGenerationRecallInput(chat) ||
{};
const recallContext = runtime.createGenerationRecallContext({
@@ -644,12 +675,27 @@ export function onMessageReceivedController(
const targetMessage = runtime.isAssistantChatMessage(receivedMessage)
? receivedMessage
: lastMessage;
const targetMessageIndex = runtime.isAssistantChatMessage(receivedMessage)
? Number(messageId)
: runtime.isAssistantChatMessage(lastMessage)
? chat.length - 1
: null;
if (runtime.isAssistantChatMessage(targetMessage)) {
if (runtime.consumeCurrentGenerationTrivialSkip?.(targetMessageIndex)) {
runtime.console?.info?.(
"[ST-BME] trivial-input skip: extraction bypassed",
{ messageId: targetMessageIndex },
);
runtime.refreshPersistedRecallMessageUi?.();
return;
}
runtime.console?.debug?.(
"[ST-BME] assistant message received, queueing auto extraction",
{
messageId: Number.isFinite(Number(messageId)) ? Number(messageId) : null,
messageId: Number.isFinite(Number(targetMessageIndex))
? Number(targetMessageIndex)
: null,
chatLength: Array.isArray(chat) ? chat.length : 0,
loadState,
dbReady,

144
index.js
View File

@@ -204,6 +204,7 @@ import {
getStageNoticeTitle,
hashRecallInput,
isFreshRecallInputRecord,
isTrivialUserInput,
normalizeRecallInputText,
normalizeStageNoticeLevel,
pushBatchStageArtifact,
@@ -521,6 +522,7 @@ let lastExtractionWarningAt = 0;
const LOCAL_VECTOR_TIMEOUT_MS = 300000;
const STATUS_TOAST_THROTTLE_MS = 1500;
const RECALL_INPUT_RECORD_TTL_MS = 60000;
const TRIVIAL_GENERATION_SKIP_TTL_MS = 60000;
const HISTORY_RECOVERY_SETTLE_MS = 80;
const HISTORY_MUTATION_RETRY_DELAYS_MS = [80, 220, 500, 900];
const GRAPH_LOAD_RETRY_DELAYS_MS = [120, 450, 1200, 2500];
@@ -534,6 +536,7 @@ const lastStatusToastAt = {};
let pendingRecallSendIntent = createRecallInputRecord();
let lastRecallSentUserMessage = createRecallInputRecord();
let pendingHostGenerationInputSnapshot = createRecallInputRecord();
let currentGenerationTrivialSkip = null;
let coreEventBindingState = {
registered: false,
cleanups: [],
@@ -1246,9 +1249,9 @@ function restoreRecallUiStateFromPersistence(chat = getContext()?.chat) {
}
function clearRecallInputTracking() {
pendingRecallSendIntent = createRecallInputRecord();
clearPendingRecallSendIntent();
lastRecallSentUserMessage = createRecallInputRecord();
pendingHostGenerationInputSnapshot = createRecallInputRecord();
clearPendingHostGenerationInputSnapshot();
if (typeof recordMessageTraceSnapshot === "function") {
recordMessageTraceSnapshot({
lastSentUserMessage: null,
@@ -1330,6 +1333,91 @@ function getPendingHostGenerationInputSnapshot() {
return pendingHostGenerationInputSnapshot;
}
function clearPendingRecallSendIntent() {
pendingRecallSendIntent = createRecallInputRecord();
return pendingRecallSendIntent;
}
function clearPendingHostGenerationInputSnapshot() {
pendingHostGenerationInputSnapshot = createRecallInputRecord();
return pendingHostGenerationInputSnapshot;
}
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;
}
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();
@@ -6467,9 +6555,17 @@ function buildGenerationAfterCommandsRecallInput(type, params = {}, chat) {
};
}
return buildNormalGenerationRecallInput(chat, {
const normalInput = buildNormalGenerationRecallInput(chat, {
frozenInputSnapshot: params?.frozenInputSnapshot,
});
return normalInput;
}
function createTrivialRecallSkipSentinel(reason = "") {
return {
__trivialSkip: true,
trivialReason: String(reason || ""),
};
}
function buildNormalGenerationRecallInput(chat, options = {}) {
@@ -6566,9 +6662,32 @@ function buildNormalGenerationRecallInput(chat, options = {}) {
}
: null,
].filter(Boolean);
const activeTrivialSkip = getCurrentGenerationTrivialSkip();
if (activeTrivialSkip) {
clearPendingRecallSendIntent();
clearPendingHostGenerationInputSnapshot();
return createTrivialRecallSkipSentinel(activeTrivialSkip.reason);
}
const selectedCandidate = sourceCandidates[0] || null;
if (!selectedCandidate?.text) return null;
const trivialInputResult = isTrivialUserInput(selectedCandidate.text);
if (trivialInputResult.trivial) {
clearPendingRecallSendIntent();
clearPendingHostGenerationInputSnapshot();
markCurrentGenerationTrivialSkip({
reason: trivialInputResult.reason,
chatId: getCurrentChatId(),
chatLength: Array.isArray(chat) ? chat.length : 0,
});
console.info?.(
`[ST-BME] trivial-input skip: reason=${trivialInputResult.reason} len=${trivialInputResult.normalizedText.length} hook=build-normal-input`,
);
return createTrivialRecallSkipSentinel(trivialInputResult.reason);
}
return {
overrideUserMessage: selectedCandidate.text,
generationType: "normal",
@@ -7119,6 +7238,7 @@ function invalidateRecallAfterHistoryMutation(reason = "聊天记录已变更")
clearGenerationRecallTransactionsForChat();
clearRecallInputTracking();
clearCurrentGenerationTrivialSkip("history-mutation");
clearInjectionState({
preserveRecallStatus: hadActiveRecall,
preserveRuntimeStatus: hadActiveRecall,
@@ -8843,10 +8963,14 @@ async function runPlannerRecallForEna({
disableLlmRecall = false,
} = {}) {
const userMessage = normalizeRecallInputText(rawUserInput || "");
if (!userMessage) {
const trivialInputResult = isTrivialUserInput(userMessage);
if (trivialInputResult.trivial) {
console.info?.(
`[ST-BME] trivial-input skip: reason=${trivialInputResult.reason} len=${trivialInputResult.normalizedText.length} hook=ena-planner`,
);
return {
ok: false,
reason: "empty-user-input",
reason: `trivial-user-input:${trivialInputResult.reason}`,
memoryBlock: "",
recentMessages: [],
result: null,
@@ -9018,6 +9142,7 @@ function onChatChanged() {
clearPendingAutoExtraction,
clearPendingGraphLoadRetry,
clearPendingHistoryMutationChecks,
clearCurrentGenerationTrivialSkip,
clearRecallInputTracking,
clearTimeout,
dismissAllStageNotices,
@@ -9090,6 +9215,7 @@ function onMessageSent(messageId) {
const result = onMessageSentController(
{
getContext,
isTrivialUserInput,
recordRecallSentUserMessage,
refreshPersistedRecallMessageUi: schedulePersistedRecallMessageUiRefresh,
},
@@ -9173,11 +9299,17 @@ function onGenerationStarted(type, params = {}, dryRun = false) {
return onGenerationStartedController(
{
clearDryRunPromptPreview,
clearCurrentGenerationTrivialSkip,
clearPendingHostGenerationInputSnapshot,
clearPendingRecallSendIntent,
freezeHostGenerationInputSnapshot,
getContext,
getPendingRecallSendIntent: () => pendingRecallSendIntent,
getSendTextareaValue,
isFreshRecallInputRecord,
isTrivialUserInput,
markDryRunPromptPreview,
markCurrentGenerationTrivialSkip,
normalizeRecallInputText,
},
type,
@@ -9254,6 +9386,7 @@ async function onBeforeCombinePrompts(promptData = null) {
function onMessageReceived(messageId = null, type = "") {
const result = onMessageReceivedController({
console,
consumeCurrentGenerationTrivialSkip,
createRecallInputRecord,
getContext,
getCurrentGraph: () => currentGraph,
@@ -9699,6 +9832,7 @@ async function onReembedDirect() {
await initEnaPlanner({
getContext,
getExtensionPath: () => `scripts/extensions/third-party/${MODULE_NAME}`,
isTrivialUserInput,
preparePlannerRecallHandoff,
runPlannerRecallForEna,
});

View File

@@ -7,9 +7,10 @@
"test:indexeddb-persistence": "node tests/indexeddb-persistence.mjs",
"test:indexeddb-sync": "node tests/indexeddb-sync.mjs",
"test:indexeddb-migration": "node tests/indexeddb-migration.mjs",
"test:trivial-input": "node tests/trivial-user-input.mjs",
"test:indexeddb": "npm run test:indexeddb-persistence && npm run test:indexeddb-sync && npm run test:indexeddb-migration",
"test:persistence-matrix": "npm run test:p0 && npm run test:runtime-history && npm run test:graph-persistence && npm run test:indexeddb",
"test:all": "npm run test:persistence-matrix",
"test:all": "npm run test:persistence-matrix && npm run test:trivial-input",
"check": "node --check index.js && node --check bme-db.js && node --check hide-engine.js && node --check panel.js && node --check ui-status.js && node --check event-binding.js"
},
"dependencies": {

View File

@@ -0,0 +1,363 @@
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import vm from "node:vm";
import {
onBeforeCombinePromptsController,
onGenerationAfterCommandsController,
onGenerationStartedController,
onMessageReceivedController,
onMessageSentController,
} from "../../event-binding.js";
import {
GRAPH_LOAD_STATES,
GRAPH_METADATA_KEY,
GRAPH_PERSISTENCE_META_KEY,
MODULE_NAME,
} from "../../graph-persistence.js";
import {
buildPersistedRecallRecord,
bumpPersistedRecallGenerationCount,
readPersistedRecallFromUserMessage,
resolveFinalRecallInjectionSource,
writePersistedRecallToUserMessage,
} from "../../recall-persistence.js";
import {
createGraphPersistenceState,
createRecallInputRecord,
createRecallRunResult,
createUiStatus,
getGenerationRecallHookStateFromResult,
getRecallHookLabel,
getStageNoticeDuration,
getStageNoticeTitle,
hashRecallInput,
isFreshRecallInputRecord,
isTerminalGenerationRecallHookState,
isTrivialUserInput,
normalizeRecallInputText,
normalizeStageNoticeLevel,
shouldRunRecallForTransaction,
} from "../../ui-status.js";
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
const indexPath = path.resolve(moduleDir, "../../index.js");
export function createGenerationRecallHarness(options = {}) {
const { realApplyFinal = false } = options;
return fs.readFile(indexPath, "utf8").then((source) => {
const start = source.indexOf("const RECALL_INPUT_RECORD_TTL_MS = 60000;");
const end = source.indexOf(
'function onMessageReceived(messageId = null, type = "") {',
);
const endFallback = source.indexOf("async function runExtraction()");
const resolvedEnd = end >= 0 ? end : endFallback;
if (start < 0 || resolvedEnd < 0 || resolvedEnd <= start) {
throw new Error("无法从 index.js 提取生成召回事务定义");
}
const snippet = source
.slice(start, resolvedEnd)
.replace(/^export\s+/gm, "");
const context = {
console,
Date,
Map,
setTimeout,
clearTimeout,
__sendTextareaValue: "",
document: {
getElementById(id) {
if (
id === "send_textarea" &&
typeof context.__sendTextareaValue === "string" &&
context.__sendTextareaValue
) {
return { value: context.__sendTextareaValue };
}
return null;
},
},
result: null,
currentGraph: {},
_panelModule: null,
defaultSettings: {},
settings: {},
graphPersistenceState: createGraphPersistenceState(),
extension_settings: { [MODULE_NAME]: {} },
extension_prompt_types: {
NONE: 0,
BEFORE_PROMPT: 1,
IN_PROMPT: 2,
IN_CHAT: 3,
},
extension_prompt_roles: {
SYSTEM: 0,
USER: 1,
ASSISTANT: 2,
},
clampInt: (value, fallback = 0, min = 0, max = 9999) => {
const numeric = Number(value);
if (!Number.isFinite(numeric)) return fallback;
return Math.min(max, Math.max(min, Math.trunc(numeric)));
},
getHostAdapter: () => null,
migrateLegacyTaskProfiles: (settings = {}) => ({
taskProfilesVersion: settings?.taskProfilesVersion || 0,
taskProfiles: settings?.taskProfiles || {},
}),
refreshPanelLiveStateController: () => {
context.refreshPanelCalls += 1;
},
isRecalling: false,
getCurrentChatId: () => "chat-main",
normalizeRecallInputText: (text = "") => String(text || "").trim(),
isTrivialUserInput,
getLatestUserChatMessage: (chat = []) =>
[...chat].reverse().find((message) => message?.is_user) || null,
getLastNonSystemChatMessage: (chat = []) =>
[...chat].reverse().find((message) => !message?.is_system) || null,
getSendTextareaValue: () => context.__sendTextareaValue,
getRecallUserMessageSourceLabel: (source = "") => source,
getRecallUserMessageSourceLabelController: (source = "") => source,
buildRecallRecentMessages: (
chat = [],
_limit,
syntheticUserMessage = "",
) =>
syntheticUserMessage
? [...chat, { is_user: true, mes: syntheticUserMessage }]
: [...chat],
getContext: () => ({
chatId: "chat-main",
chat: context.chat,
}),
chat: [],
runRecallCalls: [],
runExtractionCalls: [],
extractionIssues: [],
applyFinalCalls: [],
moduleInjectionCalls: [],
recordedInjectionSnapshots: [],
refreshPanelCalls: 0,
hideScheduleCalls: [],
createRecallInputRecord,
createRecallRunResult,
hashRecallInput,
isFreshRecallInputRecord,
isTerminalGenerationRecallHookState,
shouldRunRecallForTransaction,
getGenerationRecallHookStateFromResult,
createUiStatus,
createGraphPersistenceState,
getRecallHookLabel,
getStageNoticeTitle,
getStageNoticeDuration,
normalizeStageNoticeLevel,
MODULE_NAME,
GRAPH_LOAD_STATES,
GRAPH_METADATA_KEY,
GRAPH_PERSISTENCE_META_KEY,
onBeforeCombinePromptsController,
onGenerationAfterCommandsController,
onGenerationStartedController,
readPersistedRecallFromUserMessage,
writePersistedRecallToUserMessage,
buildPersistedRecallRecord,
resolveFinalRecallInjectionSource,
bumpPersistedRecallGenerationCount,
applyModuleInjectionPrompt: (text = "") => {
const normalizedText = String(text || "");
context.moduleInjectionCalls.push(normalizedText);
return {
applied: Boolean(normalizedText.trim()),
source: normalizedText.trim() ? "module-injection" : "rewrite-clear",
mode: normalizedText.trim() ? "module-injection" : "rewrite-clear",
};
},
getSettings: () => context.settings,
$: () => ({}),
triggerChatMetadataSave: () => {
context.metadataSaveCalls += 1;
return "debounced";
},
refreshPanelLiveState: () => {
context.refreshPanelCalls += 1;
},
recordInjectionSnapshot: (_kind, snapshot = {}) => {
context.recordedInjectionSnapshots.push({ ...snapshot });
},
schedulePersistedRecallMessageUiRefresh: () => {
context.recallUiRefreshCalls += 1;
},
getMessageHideSettings: () => ({}),
getHideRuntimeAdapters: () => ({}),
scheduleHideSettingsApply: (...args) => {
context.hideScheduleCalls.push(args);
},
estimateTokens: (text = "") =>
normalizeRecallInputText(text)
.split(/\s+/)
.filter(Boolean).length || (normalizeRecallInputText(text) ? 1 : 0),
resolveGenerationTargetUserMessageIndex: (
chat = [],
{ generationType } = {},
) => {
const normalized = String(generationType || "normal");
if (!Array.isArray(chat) || chat.length === 0) return null;
if (normalized === "normal")
return chat[chat.length - 1]?.is_user ? chat.length - 1 : null;
for (let index = chat.length - 1; index >= 0; index--)
if (chat[index]?.is_user) return index;
return null;
},
metadataSaveCalls: 0,
recallUiRefreshCalls: 0,
};
vm.createContext(context);
vm.runInContext(
`${snippet}\nresult = { hashRecallInput, buildPreGenerationRecallKey, buildGenerationAfterCommandsRecallInput, buildNormalGenerationRecallInput, cleanupGenerationRecallTransactions, buildGenerationRecallTransactionId, beginGenerationRecallTransaction, markGenerationRecallTransactionHookState, shouldRunRecallForTransaction, createGenerationRecallContext, onGenerationStarted, onGenerationEnded, onGenerationAfterCommands, onBeforeCombinePrompts, applyFinalRecallInjectionForGeneration, ensurePersistedRecallRecordForGeneration, findRecentGenerationRecallTransactionForChat, getGenerationRecallTransactionResult, generationRecallTransactions, freezeHostGenerationInputSnapshot, consumeHostGenerationInputSnapshot, getPendingHostGenerationInputSnapshot, clearPendingHostGenerationInputSnapshot, recordRecallSendIntent, clearPendingRecallSendIntent, recordRecallSentUserMessage, getPendingRecallSendIntent: () => pendingRecallSendIntent, getLastRecallSentUserMessage: () => lastRecallSentUserMessage, getCurrentGenerationTrivialSkip, markCurrentGenerationTrivialSkip, clearCurrentGenerationTrivialSkip, consumeCurrentGenerationTrivialSkip, runPlannerRecallForEna, getGraphPersistenceState: () => graphPersistenceState, setGraphPersistenceState: (value = {}) => { graphPersistenceState = { ...graphPersistenceState, ...(value || {}) }; return graphPersistenceState; } };`,
context,
{ filename: indexPath },
);
Object.defineProperties(context, {
pendingRecallSendIntent: {
get() {
return context.result.getPendingRecallSendIntent();
},
set(value) {
if (value?.text) {
context.result.recordRecallSendIntent(
value?.text || "",
value?.source,
);
return;
}
context.result.clearPendingRecallSendIntent();
},
configurable: true,
},
lastRecallSentUserMessage: {
get() {
return context.result.getLastRecallSentUserMessage();
},
set(value) {
context.result.recordRecallSentUserMessage(
value?.messageId,
value?.text || "",
value?.source,
);
},
configurable: true,
},
});
const originalApplyFinalRecallInjectionForGeneration =
context.result.applyFinalRecallInjectionForGeneration;
context.applyFinalRecallInjectionForGeneration = (payload = {}) => {
context.applyFinalCalls.push({ ...payload });
if (realApplyFinal) {
return originalApplyFinalRecallInjectionForGeneration(payload);
}
return {
source: "fresh",
targetUserMessageIndex: null,
};
};
context.runRecall = async (options = {}) => {
context.runRecallCalls.push({ ...options });
const overrideUserMessage = String(
options.overrideUserMessage || options.userMessage || "",
);
return {
status: "completed",
didRecall: true,
ok: true,
injectionText: `注入:${overrideUserMessage}`,
deliveryMode: String(options.deliveryMode || "immediate"),
source: options.overrideSource,
sourceLabel: options.overrideSourceLabel,
reason: options.overrideReason,
sourceCandidates: Array.isArray(options.sourceCandidates)
? options.sourceCandidates.map((candidate) => ({ ...candidate }))
: [],
selectedNodeIds: ["node-test-1"],
retrievalMeta: {
vectorHits: 1,
vectorMergedHits: 0,
diffusionHits: 0,
candidatePoolAfterDpp: 1,
},
llmMeta: {
status: "disabled",
reason: "test-disabled",
candidatePool: 0,
},
stats: {
coreCount: 1,
recallCount: 1,
},
};
};
context.runExtraction = async (...args) => {
context.runExtractionCalls.push(args);
return {
ok: true,
};
};
context.invokeOnMessageSent = (messageId = null) =>
onMessageSentController(
{
getContext: context.getContext,
isTrivialUserInput,
recordRecallSentUserMessage: context.result.recordRecallSentUserMessage,
refreshPersistedRecallMessageUi: () => {
context.recallUiRefreshCalls += 1;
},
},
messageId,
);
context.invokeOnMessageReceived = (messageId = null, type = "") =>
onMessageReceivedController(
{
console,
consumeCurrentGenerationTrivialSkip:
context.result.consumeCurrentGenerationTrivialSkip,
createRecallInputRecord,
getContext: context.getContext,
getCurrentGraph: () => context.currentGraph,
getGraphPersistenceState: () => context.result.getGraphPersistenceState(),
getPendingHostGenerationInputSnapshot:
context.result.getPendingHostGenerationInputSnapshot,
getPendingRecallSendIntent: () => context.result.getPendingRecallSendIntent(),
isAssistantChatMessage: (message) =>
Boolean(message) && !message.is_user && !message.is_system,
isFreshRecallInputRecord,
isGraphMetadataWriteAllowed: () => true,
syncGraphLoadFromLiveContext: () => {},
maybeCaptureGraphShadowSnapshot: () => {},
maybeFlushQueuedGraphPersist: () => {},
notifyExtractionIssue: (message) => {
context.extractionIssues.push(String(message || ""));
},
queueMicrotask: (task) => task(),
runExtraction: context.runExtraction,
refreshPersistedRecallMessageUi: () => {
context.recallUiRefreshCalls += 1;
},
setPendingHostGenerationInputSnapshot: () => {},
setPendingRecallSendIntent: (record) => {
if (record?.text) {
context.result.recordRecallSendIntent(
record.text || "",
record.source,
);
return;
}
context.result.clearPendingRecallSendIntent();
},
},
messageId,
type,
);
return context;
});
}

View File

@@ -66,6 +66,7 @@ import {
onManualEvolveController,
onManualSleepController,
} from "../ui-actions-controller.js";
import { createGenerationRecallHarness } from "./helpers/generation-recall-harness.mjs";
const waitForTick = () => new Promise((resolve) => setTimeout(resolve, 0));
const extensionsShimSource = [
@@ -281,255 +282,6 @@ function createBatchStageHarness() {
});
}
function createGenerationRecallHarness(options = {}) {
const { realApplyFinal = false } = options;
return fs.readFile(indexPath, "utf8").then((source) => {
const start = source.indexOf("const RECALL_INPUT_RECORD_TTL_MS = 60000;");
const end = source.indexOf(
'function onMessageReceived(messageId = null, type = "") {',
);
const resolvedEnd = end >= 0 ? end : endFallback;
if (start < 0 || resolvedEnd < 0 || resolvedEnd <= start) {
throw new Error("无法从 index.js 提取生成召回事务定义");
}
const snippet = source
.slice(start, resolvedEnd)
.replace(/^export\s+/gm, "");
const context = {
console,
Date,
Map,
setTimeout,
clearTimeout,
__sendTextareaValue: "",
document: {
getElementById(id) {
if (
id === "send_textarea" &&
typeof context.__sendTextareaValue === "string" &&
context.__sendTextareaValue
) {
return { value: context.__sendTextareaValue };
}
return null;
},
},
result: null,
currentGraph: {},
_panelModule: null,
defaultSettings: {},
extension_settings: { [MODULE_NAME]: {} },
extension_prompt_types: {
NONE: 0,
BEFORE_PROMPT: 1,
IN_PROMPT: 2,
IN_CHAT: 3,
},
extension_prompt_roles: {
SYSTEM: 0,
USER: 1,
ASSISTANT: 2,
},
clampInt: (value, fallback = 0, min = 0, max = 9999) => {
const numeric = Number(value);
if (!Number.isFinite(numeric)) return fallback;
return Math.min(max, Math.max(min, Math.trunc(numeric)));
},
getHostAdapter: () => null,
migrateLegacyTaskProfiles: (settings = {}) => ({
taskProfilesVersion: settings?.taskProfilesVersion || 0,
taskProfiles: settings?.taskProfiles || {},
}),
refreshPanelLiveStateController: () => {
context.refreshPanelCalls += 1;
},
isRecalling: false,
getCurrentChatId: () => "chat-main",
normalizeRecallInputText: (text = "") => String(text || "").trim(),
getLatestUserChatMessage: (chat = []) =>
[...chat].reverse().find((message) => message?.is_user) || null,
getLastNonSystemChatMessage: (chat = []) =>
[...chat].reverse().find((message) => !message?.is_system) || null,
getSendTextareaValue: () => context.__sendTextareaValue,
getRecallUserMessageSourceLabel: (source = "") => source,
getRecallUserMessageSourceLabelController: (source = "") => source,
buildRecallRecentMessages: (
chat = [],
_limit,
syntheticUserMessage = "",
) =>
syntheticUserMessage
? [...chat, { is_user: true, mes: syntheticUserMessage }]
: [...chat],
getContext: () => ({
chatId: "chat-main",
chat: context.chat,
}),
chat: [],
runRecallCalls: [],
applyFinalCalls: [],
moduleInjectionCalls: [],
recordedInjectionSnapshots: [],
refreshPanelCalls: 0,
hideScheduleCalls: [],
createRecallInputRecord,
createRecallRunResult,
hashRecallInput,
normalizeRecallInputText,
isFreshRecallInputRecord,
isTerminalGenerationRecallHookState,
shouldRunRecallForTransaction,
getGenerationRecallHookStateFromResult,
createUiStatus,
createGraphPersistenceState,
getRecallHookLabel,
getStageNoticeTitle,
getStageNoticeDuration,
normalizeStageNoticeLevel,
MODULE_NAME,
GRAPH_LOAD_STATES,
GRAPH_METADATA_KEY,
GRAPH_PERSISTENCE_META_KEY,
onBeforeCombinePromptsController,
onGenerationAfterCommandsController,
onGenerationStartedController,
readPersistedRecallFromUserMessage,
writePersistedRecallToUserMessage,
buildPersistedRecallRecord,
resolveFinalRecallInjectionSource,
bumpPersistedRecallGenerationCount,
applyModuleInjectionPrompt: (text = "") => {
const normalizedText = String(text || "");
context.moduleInjectionCalls.push(normalizedText);
return {
applied: Boolean(normalizedText.trim()),
source: normalizedText.trim() ? "module-injection" : "rewrite-clear",
mode: normalizedText.trim() ? "module-injection" : "rewrite-clear",
};
},
getSettings: () => ({}),
$: () => ({}),
triggerChatMetadataSave: () => {
context.metadataSaveCalls += 1;
return "debounced";
},
refreshPanelLiveState: () => {
context.refreshPanelCalls += 1;
},
recordInjectionSnapshot: (_kind, snapshot = {}) => {
context.recordedInjectionSnapshots.push({ ...snapshot });
},
schedulePersistedRecallMessageUiRefresh: () => {
context.recallUiRefreshCalls += 1;
},
getMessageHideSettings: () => ({}),
getHideRuntimeAdapters: () => ({}),
scheduleHideSettingsApply: (...args) => {
context.hideScheduleCalls.push(args);
},
estimateTokens: (text = "") =>
normalizeRecallInputText(text)
.split(/\s+/)
.filter(Boolean).length || (normalizeRecallInputText(text) ? 1 : 0),
resolveGenerationTargetUserMessageIndex: (
chat = [],
{ generationType } = {},
) => {
const normalized = String(generationType || "normal");
if (!Array.isArray(chat) || chat.length === 0) return null;
if (normalized === "normal")
return chat[chat.length - 1]?.is_user ? chat.length - 1 : null;
for (let index = chat.length - 1; index >= 0; index--)
if (chat[index]?.is_user) return index;
return null;
},
metadataSaveCalls: 0,
recallUiRefreshCalls: 0,
};
vm.createContext(context);
vm.runInContext(
`${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 },
);
Object.defineProperties(context, {
pendingRecallSendIntent: {
get() {
return context.result.getPendingRecallSendIntent();
},
set(value) {
context.result.recordRecallSendIntent(
value?.text || "",
value?.source,
);
},
configurable: true,
},
lastRecallSentUserMessage: {
get() {
return context.result.getLastRecallSentUserMessage();
},
set(value) {
context.result.recordRecallSentUserMessage(
value?.messageId,
value?.text || "",
value?.source,
);
},
configurable: true,
},
});
const originalApplyFinalRecallInjectionForGeneration =
context.result.applyFinalRecallInjectionForGeneration;
context.applyFinalRecallInjectionForGeneration = (payload = {}) => {
context.applyFinalCalls.push({ ...payload });
if (realApplyFinal) {
return originalApplyFinalRecallInjectionForGeneration(payload);
}
return {
source: "fresh",
targetUserMessageIndex: null,
};
};
context.runRecall = async (options = {}) => {
context.runRecallCalls.push({ ...options });
const overrideUserMessage = String(
options.overrideUserMessage || options.userMessage || "",
);
return {
status: "completed",
didRecall: true,
ok: true,
injectionText: `注入:${overrideUserMessage}`,
deliveryMode: String(options.deliveryMode || "immediate"),
source: options.overrideSource,
sourceLabel: options.overrideSourceLabel,
reason: options.overrideReason,
sourceCandidates: Array.isArray(options.sourceCandidates)
? options.sourceCandidates.map((candidate) => ({ ...candidate }))
: [],
selectedNodeIds: ["node-test-1"],
retrievalMeta: {
vectorHits: 1,
vectorMergedHits: 0,
diffusionHits: 0,
candidatePoolAfterDpp: 1,
},
llmMeta: {
status: "disabled",
reason: "test-disabled",
candidatePool: 0,
},
stats: {
coreCount: 1,
recallCount: 1,
},
};
};
return context;
});
}
function createHistoryRecoveryHarness() {
return fs.readFile(indexPath, "utf8").then((source) => {
const start = source.indexOf("async function recoverHistoryIfNeeded(");

View File

@@ -22,12 +22,14 @@ const chat = [
const hashes = snapshotProcessedMessageHashes(chat, 3);
const cleanDetection = detectHistoryMutation(chat, {
lastProcessedAssistantFloor: 3,
processedMessageHashVersion: PROCESSED_MESSAGE_HASH_VERSION,
processedMessageHashes: hashes,
});
assert.equal(cleanDetection.dirty, false);
const missingHashesDetection = detectHistoryMutation(chat, {
lastProcessedAssistantFloor: 3,
processedMessageHashVersion: PROCESSED_MESSAGE_HASH_VERSION,
processedMessageHashes: {},
});
assert.equal(missingHashesDetection.dirty, true);
@@ -35,6 +37,7 @@ assert.equal(missingHashesDetection.earliestAffectedFloor, 0);
const sparseHashesDetection = detectHistoryMutation(chat, {
lastProcessedAssistantFloor: 3,
processedMessageHashVersion: PROCESSED_MESSAGE_HASH_VERSION,
processedMessageHashes: {
0: hashes[0],
2: hashes[2],
@@ -48,6 +51,7 @@ const editedChat = structuredClone(chat);
editedChat[1].mes = "我改过内容了。";
const editedDetection = detectHistoryMutation(editedChat, {
lastProcessedAssistantFloor: 3,
processedMessageHashVersion: PROCESSED_MESSAGE_HASH_VERSION,
processedMessageHashes: hashes,
});
assert.equal(editedDetection.dirty, true);
@@ -58,6 +62,7 @@ bmeHiddenChat[1].is_system = true;
bmeHiddenChat[1].extra = { __st_bme_hide_managed: true };
const bmeHiddenDetection = detectHistoryMutation(bmeHiddenChat, {
lastProcessedAssistantFloor: 3,
processedMessageHashVersion: PROCESSED_MESSAGE_HASH_VERSION,
processedMessageHashes: hashes,
});
assert.equal(bmeHiddenDetection.dirty, false);
@@ -66,6 +71,7 @@ const realSystemFlipChat = structuredClone(chat);
realSystemFlipChat[1].is_system = true;
const realSystemFlipDetection = detectHistoryMutation(realSystemFlipChat, {
lastProcessedAssistantFloor: 3,
processedMessageHashVersion: PROCESSED_MESSAGE_HASH_VERSION,
processedMessageHashes: hashes,
});
assert.equal(realSystemFlipDetection.dirty, false);
@@ -91,6 +97,7 @@ assert.equal(migratedDetection.dirty, false);
const truncatedChat = chat.slice(0, 2);
const truncatedDetection = detectHistoryMutation(truncatedChat, {
lastProcessedAssistantFloor: 3,
processedMessageHashVersion: PROCESSED_MESSAGE_HASH_VERSION,
processedMessageHashes: hashes,
});
assert.equal(truncatedDetection.dirty, true);

View File

@@ -0,0 +1,299 @@
// wired into npm run test:all
import assert from "node:assert/strict";
import { MODULE_NAME } from "../graph-persistence.js";
import { isTrivialUserInput } from "../ui-status.js";
import { createGenerationRecallHarness } from "./helpers/generation-recall-harness.mjs";
function assertEmptyRecallInputRecord(record) {
assert.deepEqual(record, {
text: "",
hash: "",
messageId: null,
source: "",
at: 0,
});
}
function testIsTrivialUserInputTable() {
const cases = [
["", true, "empty"],
[" \n\t ", true, "empty"],
["/echo hello", true, "slash-command"],
["/", true, "slash-command"],
[" /echo", true, "slash-command"],
["a", true, "under-min-tokens"],
["好", true, "under-min-tokens"],
["ok", true, "under-min-tokens"],
["ok a", false, ""],
["好的", false, ""],
["好的呀", false, ""],
["hello world", false, ""],
["你好", false, ""],
];
for (const [input, trivial, reason] of cases) {
const result = isTrivialUserInput(input);
assert.equal(result.trivial, trivial, `trivial mismatch for ${JSON.stringify(input)}`);
assert.equal(result.reason, reason, `reason mismatch for ${JSON.stringify(input)}`);
}
}
async function testSlashCommandSkipsRecallAndExtraction() {
const harness = await createGenerationRecallHarness();
harness.chat = [];
harness.__sendTextareaValue = "/echo test";
const startResult = harness.result.onGenerationStarted("normal", {}, false);
assert.equal(startResult, null);
assertEmptyRecallInputRecord(harness.result.getPendingHostGenerationInputSnapshot());
assertEmptyRecallInputRecord(harness.pendingRecallSendIntent);
assert.equal(
harness.result.getCurrentGenerationTrivialSkip()?.generationStartMinChatIndex,
0,
);
await harness.result.onGenerationAfterCommands("normal", {}, false);
assert.equal(harness.runRecallCalls.length, 0);
const beforeCombine = await harness.result.onBeforeCombinePrompts();
assert.deepEqual(beforeCombine, {
skipped: true,
reason: "trivial:slash-command",
});
assert.equal(harness.runRecallCalls.length, 0);
harness.chat.push({ is_user: false, mes: "assistant reply" });
harness.invokeOnMessageReceived(0, "");
assert.equal(harness.runExtractionCalls.length, 0);
assert.equal(harness.result.getCurrentGenerationTrivialSkip(), null);
}
async function testUnderMinTokensSkipsRecallAndExtraction() {
const harness = await createGenerationRecallHarness();
harness.chat = [];
harness.__sendTextareaValue = "a";
const startResult = harness.result.onGenerationStarted("normal", {}, false);
assert.equal(startResult, null);
assert.equal(
harness.result.getCurrentGenerationTrivialSkip()?.reason,
"under-min-tokens",
);
await harness.result.onGenerationAfterCommands("normal", {}, false);
assert.equal(harness.runRecallCalls.length, 0);
harness.chat.push({ is_user: false, mes: "assistant reply" });
harness.invokeOnMessageReceived(0, "");
assert.equal(harness.runExtractionCalls.length, 0);
assert.equal(harness.result.getCurrentGenerationTrivialSkip(), null);
}
async function testEmptyInputSkipsPriorHistoryFallback() {
const harness = await createGenerationRecallHarness();
harness.chat = [{ is_user: true, mes: "older real user message" }];
harness.__sendTextareaValue = " ";
const startResult = harness.result.onGenerationStarted("normal", {}, false);
assert.equal(startResult, null);
assert.equal(
harness.result.getCurrentGenerationTrivialSkip()?.reason,
"empty",
);
await harness.result.onGenerationAfterCommands("normal", {}, false);
assert.equal(harness.runRecallCalls.length, 0);
const beforeCombine = await harness.result.onBeforeCombinePrompts();
assert.deepEqual(beforeCombine, {
skipped: true,
reason: "trivial:empty",
});
}
async function testNormalInputStillRecalls() {
const harness = await createGenerationRecallHarness();
harness.chat = [];
harness.__sendTextareaValue = "好的呀";
const snapshot = harness.result.onGenerationStarted("normal", {}, false);
assert.equal(snapshot?.text, "好的呀");
assert.equal(harness.result.getCurrentGenerationTrivialSkip(), null);
const beforeCombine = await harness.result.onBeforeCombinePrompts();
assert.equal(beforeCombine?.source, "fresh");
assert.equal(harness.runRecallCalls.length, 1);
assert.equal(harness.runRecallCalls[0].overrideUserMessage, "好的呀");
}
async function testSentinelBlocksHistoryFallback() {
const harness = await createGenerationRecallHarness();
harness.chat = [{ is_user: true, mes: "真实旧消息" }];
harness.pendingRecallSendIntent = {
text: "/echo hidden",
source: "send-button",
at: Date.now(),
};
const beforeCombine = await harness.result.onBeforeCombinePrompts();
assert.deepEqual(beforeCombine, {
skipped: true,
reason: "trivial:slash-command",
});
assert.equal(harness.runRecallCalls.length, 0);
}
async function testAfterCommandsTrivialSentinelMarksExtractionBypass() {
const harness = await createGenerationRecallHarness();
harness.chat = [{ is_user: true, mes: "/echo from chat tail" }];
await harness.result.onGenerationAfterCommands("normal", {}, false);
assert.equal(harness.runRecallCalls.length, 0);
assert.equal(
harness.result.getCurrentGenerationTrivialSkip()?.generationStartMinChatIndex,
1,
);
harness.chat.push({ is_user: false, mes: "assistant after bypass flag" });
harness.invokeOnMessageReceived(1, "");
assert.equal(harness.runExtractionCalls.length, 0);
assert.equal(harness.result.getCurrentGenerationTrivialSkip(), null);
}
async function testPlannerRecallTrivialAndNonTrivialPaths() {
const harness = await createGenerationRecallHarness();
let recall = await harness.result.runPlannerRecallForEna({
rawUserInput: "",
});
assert.equal(recall.reason, "trivial-user-input:empty");
recall = await harness.result.runPlannerRecallForEna({
rawUserInput: "/echo",
});
assert.equal(recall.reason, "trivial-user-input:slash-command");
harness.extension_settings[MODULE_NAME] = {
enabled: true,
recallEnabled: true,
};
harness.result.setGraphPersistenceState({
loadState: "loaded",
dbReady: true,
});
harness.currentGraph = {
nodes: [],
edges: [],
historyState: {},
};
recall = await harness.result.runPlannerRecallForEna({
rawUserInput: "好的呀",
});
assert.equal(recall.reason, "graph-empty");
}
async function testOnMessageSentSkipsTrivialText() {
const harness = await createGenerationRecallHarness();
harness.chat = [{ is_user: true, mes: "/echo" }];
harness.invokeOnMessageSent(0);
assert.equal(harness.lastRecallSentUserMessage.text, "");
}
async function testNonTrivialGenerationClearsResidualTrivialSkip() {
const harness = await createGenerationRecallHarness();
harness.chat = [];
harness.__sendTextareaValue = "/echo";
harness.result.onGenerationStarted("normal", {}, false);
assert.ok(harness.result.getCurrentGenerationTrivialSkip());
harness.__sendTextareaValue = "hello world";
const snapshot = harness.result.onGenerationStarted("normal", {}, false);
assert.equal(snapshot?.text, "hello world");
assert.equal(harness.result.getCurrentGenerationTrivialSkip(), null);
harness.chat.push({ is_user: false, mes: "assistant after non-trivial" });
harness.invokeOnMessageReceived(0, "");
await Promise.resolve();
assert.equal(harness.runExtractionCalls.length, 1);
}
async function testNonTargetMessageIdDoesNotConsumeFlag() {
const harness = await createGenerationRecallHarness();
harness.chat = [
{ is_user: true, mes: "u0" },
{ is_user: false, mes: "a1" },
{ is_user: true, mes: "u2" },
{ is_user: false, mes: "old assistant" },
{ is_user: true, mes: "u4" },
];
harness.__sendTextareaValue = "/echo";
harness.result.onGenerationStarted("normal", {}, false);
assert.equal(
harness.result.getCurrentGenerationTrivialSkip()?.generationStartMinChatIndex,
5,
);
harness.invokeOnMessageReceived(3, "");
await Promise.resolve();
assert.equal(harness.runExtractionCalls.length, 1);
assert.ok(harness.result.getCurrentGenerationTrivialSkip());
harness.chat.push({ is_user: false, mes: "target assistant" });
harness.invokeOnMessageReceived(5, "");
assert.equal(harness.runExtractionCalls.length, 1);
assert.equal(harness.result.getCurrentGenerationTrivialSkip(), null);
}
async function testNullMessageIdFallsBackToLastAssistantIndex() {
const harness = await createGenerationRecallHarness();
harness.chat = [
{ is_user: true, mes: "u0" },
{ is_user: false, mes: "a1" },
{ is_user: true, mes: "u2" },
{ is_user: false, mes: "a3" },
{ is_user: true, mes: "u4" },
];
harness.__sendTextareaValue = "/echo";
harness.result.onGenerationStarted("normal", {}, false);
harness.chat.push({ is_user: false, mes: "latest assistant" });
harness.invokeOnMessageReceived(null, "");
assert.equal(harness.runExtractionCalls.length, 0);
assert.equal(harness.result.getCurrentGenerationTrivialSkip(), null);
}
async function testSkipFlagTtlExpires() {
const harness = await createGenerationRecallHarness();
harness.result.markCurrentGenerationTrivialSkip({
reason: "slash-command",
chatId: "chat-main",
chatLength: 2,
});
const originalNow = Date.now;
Date.now = () => originalNow() + 60001;
try {
assert.equal(harness.result.consumeCurrentGenerationTrivialSkip(2), false);
assert.equal(harness.result.getCurrentGenerationTrivialSkip(), null);
} finally {
Date.now = originalNow;
}
}
await Promise.resolve();
testIsTrivialUserInputTable();
await testSlashCommandSkipsRecallAndExtraction();
await testUnderMinTokensSkipsRecallAndExtraction();
await testEmptyInputSkipsPriorHistoryFallback();
await testNormalInputStillRecalls();
await testSentinelBlocksHistoryFallback();
await testAfterCommandsTrivialSentinelMarksExtractionBypass();
await testPlannerRecallTrivialAndNonTrivialPaths();
await testOnMessageSentSkipsTrivialText();
await testNonTrivialGenerationClearsResidualTrivialSkip();
await testNonTargetMessageIdDoesNotConsumeFlag();
await testNullMessageIdFallsBackToLastAssistantIndex();
await testSkipFlagTtlExpires();
console.log("trivial-user-input tests passed");

View File

@@ -323,6 +323,56 @@ export function normalizeRecallInputText(value) {
.trim();
}
const TRIVIAL_INPUT_MIN_TOKENS = 2;
const TRIVIAL_INPUT_CJK_TOKEN_REGEX =
/\p{Script=Han}|\p{Script=Hiragana}|\p{Script=Katakana}|\p{Script=Hangul}/gu;
function estimateTrivialInputTokens(text = "") {
const normalized = normalizeRecallInputText(text);
if (!normalized) return 0;
const cjkMatches = normalized.match(TRIVIAL_INPUT_CJK_TOKEN_REGEX) || [];
const nonCjkText = normalized.replace(TRIVIAL_INPUT_CJK_TOKEN_REGEX, " ");
const wordTokens = nonCjkText
.split(/\s+/)
.filter(Boolean);
return cjkMatches.length + wordTokens.length;
}
export function isTrivialUserInput(text) {
const normalizedText = normalizeRecallInputText(text);
if (!normalizedText) {
return {
trivial: true,
reason: "empty",
normalizedText,
};
}
if (normalizedText.startsWith("/")) {
return {
trivial: true,
reason: "slash-command",
normalizedText,
};
}
if (estimateTrivialInputTokens(normalizedText) < TRIVIAL_INPUT_MIN_TOKENS) {
return {
trivial: true,
reason: "under-min-tokens",
normalizedText,
};
}
return {
trivial: false,
reason: "",
normalizedText,
};
}
export function hashRecallInput(text) {
let hash = 0;
const normalized = normalizeRecallInputText(text);