From 1e6e5853e7275aa7ac64282e385abf7742aa0291 Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Sun, 29 Mar 2026 15:25:06 +0800 Subject: [PATCH] refactor: batch 5 phase 1 - extract ui-status.js (25 pure functions, ~280 lines) from index.js --- index.js | 319 ++++------------------------------- tests/graph-persistence.mjs | 28 ++++ tests/p0-regressions.mjs | 53 ++++++ ui-status.js | 324 ++++++++++++++++++++++++++++++++++++ 4 files changed, 435 insertions(+), 289 deletions(-) create mode 100644 ui-status.js diff --git a/index.js b/index.js index 7f62e95..a90aa49 100644 --- a/index.js +++ b/index.js @@ -76,6 +76,32 @@ import { testVectorConnection, validateVectorConfig, } from "./vector-index.js"; +import { + BATCH_STAGE_ORDER, + BATCH_STAGE_SEVERITY, + clampFloat, + clampInt, + createBatchStageStatus, + createBatchStatusSkeleton, + createGraphPersistenceState, + createRecallInputRecord, + createRecallRunResult, + createUiStatus, + finalizeBatchStatus, + formatRecallContextLine, + getGenerationRecallHookStateFromResult, + getRecallHookLabel, + getStageNoticeDuration, + getStageNoticeTitle, + hashRecallInput, + isFreshRecallInputRecord, + isTerminalGenerationRecallHookState, + normalizeRecallInputText, + normalizeStageNoticeLevel, + pushBatchStageArtifact, + setBatchStageOutcome, + shouldRunRecallForTransaction, +} from "./ui-status.js"; // 操控面板模块(动态加载,防止加载失败崩溃整个扩展) let _panelModule = null; @@ -467,41 +493,6 @@ const stageAbortControllers = { history: null, }; -function createUiStatus(text = "待命", meta = "", level = "idle") { - return { - text: String(text || "待命"), - meta: String(meta || ""), - level, - updatedAt: Date.now(), - }; -} - -function createGraphPersistenceState() { - return { - loadState: "no-chat", - chatId: "", - reason: "当前尚未进入聊天", - attemptIndex: 0, - revision: 0, - lastPersistedRevision: 0, - queuedPersistRevision: 0, - queuedPersistChatId: "", - queuedPersistMode: "", - queuedPersistRotateIntegrity: false, - queuedPersistReason: "", - shadowSnapshotUsed: false, - shadowSnapshotRevision: 0, - shadowSnapshotUpdatedAt: "", - shadowSnapshotReason: "", - lastPersistReason: "", - lastPersistMode: "", - metadataIntegrity: "", - writesBlocked: false, - pendingPersist: false, - updatedAt: new Date().toISOString(), - }; -} - function getGraphShadowSnapshotStorageKey(chatId = "") { const normalizedChatId = String(chatId || "").trim(); if (!normalizedChatId) return ""; @@ -768,14 +759,6 @@ function applyGraphLoadState( }); } -function normalizeStageNoticeLevel(level = "info") { - if (level === "running" || level === "idle") return "info"; - if (level === "success" || level === "warning" || level === "error") { - return level; - } - return "info"; -} - function createAbortError(message = "操作已终止") { const error = new Error(message); error.name = "AbortError"; @@ -900,34 +883,6 @@ function buildAbortStageAction(stage) { }; } -function getStageNoticeTitle(stage) { - switch (stage) { - case "extraction": - return "ST-BME 提取"; - case "vector": - return "ST-BME 向量"; - case "recall": - return "ST-BME 召回"; - case "history": - return "ST-BME 历史恢复"; - default: - return "ST-BME"; - } -} - -function getStageNoticeDuration(level = "info") { - switch (level) { - case "error": - return 6000; - case "warning": - return 5000; - case "success": - return 3000; - default: - return 3200; - } -} - function createNoticePanelAction() { if (!_panelModule?.openPanel) return undefined; return { @@ -1016,17 +971,6 @@ function updateStageNotice( currentHandle.update(input); } -function createRecallInputRecord(overrides = {}) { - return { - text: "", - hash: "", - messageId: null, - source: "", - at: 0, - ...overrides, - }; -} - function toPanelNodeItem(node, meta = "") { return { id: node.id, @@ -1075,29 +1019,6 @@ function updateLastRecalledItems(nodeIds = []) { ); } -function normalizeRecallInputText(value) { - return String(value ?? "") - .replace(/\r\n/g, "\n") - .trim(); -} - -function hashRecallInput(text) { - let hash = 0; - const normalized = normalizeRecallInputText(text); - for (let index = 0; index < normalized.length; index++) { - hash = (hash * 31 + normalized.charCodeAt(index)) >>> 0; - } - return normalized ? String(hash) : ""; -} - -function isFreshRecallInputRecord(record) { - return Boolean( - record?.text && - record.at && - Date.now() - record.at <= RECALL_INPUT_RECORD_TTL_MS, - ); -} - function clearRecallInputTracking() { pendingRecallSendIntent = createRecallInputRecord(); lastRecallSentUserMessage = createRecallInputRecord(); @@ -3260,22 +3181,6 @@ export function getSmartTriggerDecision(chat, lastProcessed, settings) { }; } -function clampInt(value, fallback, min = 0, max = Number.MAX_SAFE_INTEGER) { - const num = Number.parseInt(value, 10); - if (!Number.isFinite(num)) return fallback; - return Math.min(max, Math.max(min, num)); -} - -function clampFloat(value, fallback, min = 0, max = 1) { - const num = Number.parseFloat(value); - if (!Number.isFinite(num)) return fallback; - return Math.min(max, Math.max(min, num)); -} - -function formatRecallContextLine(message) { - return `[${message.is_user ? "user" : "assistant"}]: ${message.mes || ""}`; -} - function getLatestUserChatMessage(chat) { if (!Array.isArray(chat)) return null; @@ -3550,53 +3455,6 @@ function clearGenerationRecallTransactionsForChat( return removed; } -function isTerminalGenerationRecallHookState(state = "") { - return ["completed", "failed", "aborted", "skipped"].includes( - String(state || ""), - ); -} - -function shouldRunRecallForTransaction(transaction, hookName) { - if (!hookName) return true; - if (!transaction) return true; - const hookStates = transaction.hookStates || {}; - if (isTerminalGenerationRecallHookState(hookStates[hookName])) { - return false; - } - if ( - hookName === "GENERATE_BEFORE_COMBINE_PROMPTS" && - isTerminalGenerationRecallHookState(hookStates.GENERATION_AFTER_COMMANDS) - ) { - return false; - } - return true; -} - -function createRecallRunResult(status = "completed", extra = {}) { - const normalizedStatus = String(status || "skipped").trim() || "skipped"; - return { - ok: normalizedStatus === "completed", - didRecall: normalizedStatus === "completed", - status: normalizedStatus, - ...extra, - }; -} - -function getGenerationRecallHookStateFromResult(result) { - const status = String(result?.status || "").trim(); - switch (status) { - case "completed": - return "completed"; - case "failed": - return "failed"; - case "aborted": - case "superseded": - return "aborted"; - default: - return "skipped"; - } -} - function invalidateRecallAfterHistoryMutation(reason = "聊天记录已变更") { const hadActiveRecall = Boolean( isRecalling || @@ -3659,112 +3517,6 @@ function getCurrentChatSeq(context = getContext()) { return currentGraph?.lastProcessedSeq ?? 0; } -const BATCH_STAGE_ORDER = ["core", "structural", "semantic", "finalize"]; -const BATCH_STAGE_SEVERITY = { - success: 0, - partial: 1, - failed: 2, -}; - -function createBatchStageStatus(stage, consistency = "strong") { - return { - stage, - outcome: "success", - consistency, - warnings: [], - errors: [], - artifacts: [], - }; -} - -function createBatchStatusSkeleton({ processedRange, extractionCountBefore }) { - return { - model: "layered-batch-v1", - processedRange: Array.isArray(processedRange) - ? [...processedRange] - : [-1, -1], - extractionCountBefore: Number.isFinite(extractionCountBefore) - ? extractionCountBefore - : extractionCount, - extractionCountAfter: Number.isFinite(extractionCount) - ? extractionCount - : 0, - stages: { - core: createBatchStageStatus("core", "strong"), - structural: createBatchStageStatus("structural", "weak"), - semantic: createBatchStageStatus("semantic", "weak"), - finalize: createBatchStageStatus("finalize", "strong"), - }, - outcome: "success", - consistency: "strong", - completed: false, - warnings: [], - errors: [], - }; -} - -function setBatchStageOutcome(status, stage, outcome, message = "") { - const stageStatus = status?.stages?.[stage]; - if (!stageStatus) return; - const nextSeverity = BATCH_STAGE_SEVERITY[outcome] ?? 0; - const previousSeverity = BATCH_STAGE_SEVERITY[stageStatus.outcome] ?? 0; - if (nextSeverity >= previousSeverity) { - stageStatus.outcome = outcome; - } - if (!message) return; - if (outcome === "failed") { - stageStatus.errors.push(message); - } else if (outcome === "partial") { - stageStatus.warnings.push(message); - } -} - -function pushBatchStageArtifact(status, stage, artifact) { - const stageStatus = status?.stages?.[stage]; - if (!stageStatus || !artifact) return; - if (!stageStatus.artifacts.includes(artifact)) { - stageStatus.artifacts.push(artifact); - } -} - -function finalizeBatchStatus(status) { - const stages = status?.stages || {}; - const structuralOutcome = stages.structural?.outcome || "success"; - const semanticOutcome = stages.semantic?.outcome || "success"; - const finalizeOutcome = stages.finalize?.outcome || "failed"; - const outcomeList = BATCH_STAGE_ORDER.map( - (stage) => stages[stage]?.outcome || "success", - ); - - if (finalizeOutcome !== "success") { - status.outcome = "failed"; - } else if (outcomeList.includes("failed")) { - status.outcome = "failed"; - } else if (structuralOutcome === "partial" || semanticOutcome === "partial") { - status.outcome = "partial"; - } else { - status.outcome = "success"; - } - - status.consistency = - finalizeOutcome === "success" && - stages.core?.outcome === "success" && - stages.structural?.outcome === "success" - ? "strong" - : "weak"; - status.completed = finalizeOutcome === "success"; - status.extractionCountAfter = Number.isFinite(extractionCount) - ? extractionCount - : status.extractionCountAfter; - status.warnings = BATCH_STAGE_ORDER.flatMap( - (stage) => stages[stage]?.warnings || [], - ); - status.errors = BATCH_STAGE_ORDER.flatMap( - (stage) => stages[stage]?.errors || [], - ); - return status; -} - async function handleExtractionSuccess( result, endIdx, @@ -3929,7 +3681,7 @@ async function handleExtractionSuccess( vectorStats: getVectorIndexStats(currentGraph), vectorError: message, warnings: status.warnings, - batchStatus: finalizeBatchStatus(status), + batchStatus: finalizeBatchStatus(status, extractionCount), }; } @@ -3953,7 +3705,7 @@ async function handleExtractionSuccess( vectorStats: vectorSync?.stats || getVectorIndexStats(currentGraph), vectorError: vectorSync?.error || "", warnings: status.warnings, - batchStatus: finalizeBatchStatus(status), + batchStatus: finalizeBatchStatus(status, extractionCount), }; } @@ -4434,7 +4186,7 @@ async function executeExtractionBatch({ "failed", result?.error || "提取阶段未返回有效操作", ); - finalizeBatchStatus(batchStatus); + finalizeBatchStatus(batchStatus, extractionCount); currentGraph.historyState.lastBatchStatus = batchStatus; return { success: false, @@ -4454,7 +4206,7 @@ async function executeExtractionBatch({ batchStatus, ); const finalizedBatchStatus = - effects?.batchStatus || finalizeBatchStatus(batchStatus); + effects?.batchStatus || finalizeBatchStatus(batchStatus, extractionCount); currentGraph.historyState.lastBatchStatus = { ...finalizedBatchStatus, historyAdvanced: shouldAdvanceProcessedHistory(finalizedBatchStatus), @@ -5038,17 +4790,6 @@ async function runExtraction() { } } -function getRecallHookLabel(hookName = "") { - switch (hookName) { - case "GENERATION_AFTER_COMMANDS": - return "hook GENERATION_AFTER_COMMANDS"; - case "GENERATE_BEFORE_COMBINE_PROMPTS": - return "hook GENERATE_BEFORE_COMBINE_PROMPTS"; - default: - return ""; - } -} - function applyRecallInjection(settings, recallInput, recentMessages, result) { const injectionText = formatInjection(result, getSchema()).trim(); lastInjectionContent = injectionText; diff --git a/tests/graph-persistence.mjs b/tests/graph-persistence.mjs index b7208d7..6baa3d9 100644 --- a/tests/graph-persistence.mjs +++ b/tests/graph-persistence.mjs @@ -12,6 +12,21 @@ import { serializeGraph, } from "../graph.js"; import { normalizeGraphRuntimeState } from "../runtime-state.js"; +import { + createUiStatus, + createGraphPersistenceState, + createRecallInputRecord, + createRecallRunResult, + normalizeStageNoticeLevel, + getStageNoticeTitle, + getStageNoticeDuration, + normalizeRecallInputText, + hashRecallInput, + isFreshRecallInputRecord, + clampInt, + clampFloat, + formatRecallContextLine, +} from "../ui-status.js"; const moduleDir = path.dirname(fileURLToPath(import.meta.url)); const indexPath = path.resolve(moduleDir, "../index.js"); @@ -176,6 +191,19 @@ async function createGraphPersistenceHarness({ deserializeGraph, getGraphStats, getNode, + createUiStatus, + createGraphPersistenceState, + createRecallInputRecord, + createRecallRunResult, + normalizeStageNoticeLevel, + getStageNoticeTitle, + getStageNoticeDuration, + normalizeRecallInputText, + hashRecallInput, + isFreshRecallInputRecord, + clampInt, + clampFloat, + formatRecallContextLine, createDefaultTaskProfiles() { return { extract: { activeProfileId: "default", profiles: [] }, diff --git a/tests/p0-regressions.mjs b/tests/p0-regressions.mjs index 7575944..3bff7ab 100644 --- a/tests/p0-regressions.mjs +++ b/tests/p0-regressions.mjs @@ -4,6 +4,32 @@ import { createRequire, registerHooks } from "node:module"; import path from "node:path"; import { fileURLToPath } from "node:url"; import vm from "node:vm"; +import { + BATCH_STAGE_ORDER, + BATCH_STAGE_SEVERITY, + clampFloat, + clampInt, + createBatchStageStatus, + createBatchStatusSkeleton, + createGraphPersistenceState, + createRecallInputRecord, + createRecallRunResult, + createUiStatus, + finalizeBatchStatus, + formatRecallContextLine, + getGenerationRecallHookStateFromResult, + getRecallHookLabel, + getStageNoticeDuration, + getStageNoticeTitle, + hashRecallInput, + isFreshRecallInputRecord, + isTerminalGenerationRecallHookState, + normalizeRecallInputText, + normalizeStageNoticeLevel, + pushBatchStageArtifact, + setBatchStageOutcome, + shouldRunRecallForTransaction, +} from "../ui-status.js"; const extensionsShimSource = [ "export const extension_settings = globalThis.__p0ExtensionSettings || {};", @@ -171,6 +197,14 @@ function createBatchStageHarness() { throwIfAborted: () => {}, isAbortError: () => false, createAbortError: (message) => new Error(message), + BATCH_STAGE_ORDER, + BATCH_STAGE_SEVERITY, + createBatchStageStatus, + createBatchStatusSkeleton, + setBatchStageOutcome, + pushBatchStageArtifact, + finalizeBatchStatus, + createUiStatus, }; vm.createContext(context); vm.runInContext( @@ -228,6 +262,20 @@ function createGenerationRecallHarness() { }), chat: [], runRecallCalls: [], + createRecallInputRecord, + createRecallRunResult, + hashRecallInput, + normalizeRecallInputText, + isFreshRecallInputRecord, + isTerminalGenerationRecallHookState, + shouldRunRecallForTransaction, + getGenerationRecallHookStateFromResult, + createUiStatus, + createGraphPersistenceState, + getRecallHookLabel, + getStageNoticeTitle, + getStageNoticeDuration, + normalizeStageNoticeLevel, }; vm.createContext(context); vm.runInContext( @@ -382,6 +430,11 @@ function createRerollHarness() { context.onManualExtractCalls += 1; context.lastExtractionStatus = { level: context.manualExtractLevel }; }, + createUiStatus, + isAbortError: (e) => e?.name === "AbortError", + assertRecoveryChatStillActive() { + // no-op in test + }, toastr: { info() {}, error() {}, diff --git a/ui-status.js b/ui-status.js new file mode 100644 index 0000000..0ee6e78 --- /dev/null +++ b/ui-status.js @@ -0,0 +1,324 @@ +// ST-BME: UI 状态工厂、纯工具函数 +// 此模块中的函数均不依赖 index.js 模块级可变状态, +// 可被 index.js 及其他模块安全导入。 + +// ═══════════════════════════════════════════════════════════ +// 常量 +// ═══════════════════════════════════════════════════════════ + +export const BATCH_STAGE_ORDER = ["core", "structural", "semantic", "finalize"]; +export const BATCH_STAGE_SEVERITY = { + success: 0, + partial: 1, + failed: 2, +}; + +// ═══════════════════════════════════════════════════════════ +// UI 状态工厂 +// ═══════════════════════════════════════════════════════════ + +export function createUiStatus(text = "待命", meta = "", level = "idle") { + return { + text: String(text || "待命"), + meta: String(meta || ""), + level, + updatedAt: Date.now(), + }; +} + +export function createGraphPersistenceState() { + return { + loadState: "no-chat", + chatId: "", + reason: "当前尚未进入聊天", + attemptIndex: 0, + revision: 0, + lastPersistedRevision: 0, + queuedPersistRevision: 0, + queuedPersistChatId: "", + queuedPersistMode: "", + queuedPersistRotateIntegrity: false, + queuedPersistReason: "", + shadowSnapshotUsed: false, + shadowSnapshotRevision: 0, + shadowSnapshotUpdatedAt: "", + shadowSnapshotReason: "", + lastPersistReason: "", + lastPersistMode: "", + metadataIntegrity: "", + writesBlocked: false, + pendingPersist: false, + updatedAt: new Date().toISOString(), + }; +} + +export function createRecallInputRecord(overrides = {}) { + return { + text: "", + hash: "", + messageId: null, + source: "", + at: 0, + ...overrides, + }; +} + +export function createRecallRunResult(status = "completed", extra = {}) { + const normalizedStatus = String(status || "skipped").trim() || "skipped"; + return { + ok: normalizedStatus === "completed", + didRecall: normalizedStatus === "completed", + status: normalizedStatus, + ...extra, + }; +} + +// ═══════════════════════════════════════════════════════════ +// 批次状态 +// ═══════════════════════════════════════════════════════════ + +export function createBatchStageStatus(stage, consistency = "strong") { + return { + stage, + outcome: "success", + consistency, + warnings: [], + errors: [], + artifacts: [], + }; +} + +/** + * @param {object} opts + * @param {number[]} opts.processedRange + * @param {number} opts.extractionCountBefore + * @param {number} [opts.extractionCountAfter] — 如未提供,fallback 为 extractionCountBefore + */ +export function createBatchStatusSkeleton({ + processedRange, + extractionCountBefore, + extractionCountAfter, +}) { + const countBefore = Number.isFinite(extractionCountBefore) + ? extractionCountBefore + : 0; + const countAfter = Number.isFinite(extractionCountAfter) + ? extractionCountAfter + : countBefore; + return { + model: "layered-batch-v1", + processedRange: Array.isArray(processedRange) + ? [...processedRange] + : [-1, -1], + extractionCountBefore: countBefore, + extractionCountAfter: countAfter, + stages: { + core: createBatchStageStatus("core", "strong"), + structural: createBatchStageStatus("structural", "weak"), + semantic: createBatchStageStatus("semantic", "weak"), + finalize: createBatchStageStatus("finalize", "strong"), + }, + outcome: "success", + consistency: "strong", + completed: false, + warnings: [], + errors: [], + }; +} + +export function setBatchStageOutcome(status, stage, outcome, message = "") { + const stageStatus = status?.stages?.[stage]; + if (!stageStatus) return; + const nextSeverity = BATCH_STAGE_SEVERITY[outcome] ?? 0; + const previousSeverity = BATCH_STAGE_SEVERITY[stageStatus.outcome] ?? 0; + if (nextSeverity >= previousSeverity) { + stageStatus.outcome = outcome; + } + if (!message) return; + if (outcome === "failed") { + stageStatus.errors.push(message); + } else if (outcome === "partial") { + stageStatus.warnings.push(message); + } +} + +export function pushBatchStageArtifact(status, stage, artifact) { + const stageStatus = status?.stages?.[stage]; + if (!stageStatus || !artifact) return; + if (!stageStatus.artifacts.includes(artifact)) { + stageStatus.artifacts.push(artifact); + } +} + +/** + * @param {object} status + * @param {number} [currentExtractionCount] — 传入调用方的 extractionCount + */ +export function finalizeBatchStatus(status, currentExtractionCount) { + const stages = status?.stages || {}; + const structuralOutcome = stages.structural?.outcome || "success"; + const semanticOutcome = stages.semantic?.outcome || "success"; + const finalizeOutcome = stages.finalize?.outcome || "failed"; + const outcomeList = BATCH_STAGE_ORDER.map( + (stage) => stages[stage]?.outcome || "success", + ); + + if (finalizeOutcome !== "success") { + status.outcome = "failed"; + } else if (outcomeList.includes("failed")) { + status.outcome = "failed"; + } else if (structuralOutcome === "partial" || semanticOutcome === "partial") { + status.outcome = "partial"; + } else { + status.outcome = "success"; + } + + status.consistency = + finalizeOutcome === "success" && + stages.core?.outcome === "success" && + stages.structural?.outcome === "success" + ? "strong" + : "weak"; + status.completed = finalizeOutcome === "success"; + if (Number.isFinite(currentExtractionCount)) { + status.extractionCountAfter = currentExtractionCount; + } + status.warnings = BATCH_STAGE_ORDER.flatMap( + (stage) => stages[stage]?.warnings || [], + ); + status.errors = BATCH_STAGE_ORDER.flatMap( + (stage) => stages[stage]?.errors || [], + ); + return status; +} + +// ═══════════════════════════════════════════════════════════ +// 纯映射 / 纯变换 +// ═══════════════════════════════════════════════════════════ + +export function normalizeStageNoticeLevel(level = "info") { + if (level === "running" || level === "idle") return "info"; + if (level === "success" || level === "warning" || level === "error") { + return level; + } + return "info"; +} + +export function getStageNoticeTitle(stage) { + switch (stage) { + case "extraction": + return "ST-BME 提取"; + case "vector": + return "ST-BME 向量"; + case "recall": + return "ST-BME 召回"; + case "history": + return "ST-BME 历史恢复"; + default: + return "ST-BME"; + } +} + +export function getStageNoticeDuration(level = "info") { + switch (level) { + case "error": + return 6000; + case "warning": + return 5000; + case "success": + return 3000; + default: + return 3200; + } +} + +export function getRecallHookLabel(hookName = "") { + switch (hookName) { + case "GENERATION_AFTER_COMMANDS": + return "hook GENERATION_AFTER_COMMANDS"; + case "GENERATE_BEFORE_COMBINE_PROMPTS": + return "hook GENERATE_BEFORE_COMBINE_PROMPTS"; + default: + return ""; + } +} + +export function getGenerationRecallHookStateFromResult(result) { + const status = String(result?.status || "").trim(); + switch (status) { + case "completed": + return "completed"; + case "failed": + return "failed"; + case "aborted": + case "superseded": + return "aborted"; + default: + return "skipped"; + } +} + +export function isTerminalGenerationRecallHookState(state = "") { + return ["completed", "failed", "aborted", "skipped"].includes( + String(state || ""), + ); +} + +export function shouldRunRecallForTransaction(transaction, hookName) { + if (!hookName) return true; + if (!transaction) return true; + const hookStates = transaction.hookStates || {}; + if (isTerminalGenerationRecallHookState(hookStates[hookName])) { + return false; + } + if ( + hookName === "GENERATE_BEFORE_COMBINE_PROMPTS" && + isTerminalGenerationRecallHookState(hookStates.GENERATION_AFTER_COMMANDS) + ) { + return false; + } + return true; +} + +export function formatRecallContextLine(message) { + return `[${message.is_user ? "user" : "assistant"}]: ${message.mes || ""}`; +} + +// ═══════════════════════════════════════════════════════════ +// 文本 / 数值 工具 +// ═══════════════════════════════════════════════════════════ + +export function normalizeRecallInputText(value) { + return String(value ?? "") + .replace(/\r\n/g, "\n") + .trim(); +} + +export function hashRecallInput(text) { + let hash = 0; + const normalized = normalizeRecallInputText(text); + for (let index = 0; index < normalized.length; index++) { + hash = (hash * 31 + normalized.charCodeAt(index)) >>> 0; + } + return normalized ? String(hash) : ""; +} + +export function isFreshRecallInputRecord(record, ttlMs = 60000) { + return Boolean( + record?.text && + record.at && + Date.now() - record.at <= ttlMs, + ); +} + +export function clampInt(value, fallback, min = 0, max = Number.MAX_SAFE_INTEGER) { + const num = Number.parseInt(value, 10); + if (!Number.isFinite(num)) return fallback; + return Math.min(max, Math.max(min, num)); +} + +export function clampFloat(value, fallback, min = 0, max = 1) { + const num = Number.parseFloat(value); + if (!Number.isFinite(num)) return fallback; + return Math.min(max, Math.max(min, num)); +}