diff --git a/dynamics.js b/dynamics.js index 53597f0..07f933d 100644 --- a/dynamics.js +++ b/dynamics.js @@ -53,19 +53,31 @@ export function timeDecayFactor(createdTime, now = Date.now()) { export function hybridScore({ graphScore = 0, vectorScore = 0, + lexicalScore = 0, importance = 5, createdTime = Date.now(), }, weights = {}) { const alpha = weights.graphWeight ?? 0.6; const beta = weights.vectorWeight ?? 0.3; const gamma = weights.importanceWeight ?? 0.1; + const delta = weights.lexicalWeight ?? 0; // 归一化 const normGraph = Math.max(0, Math.min(1, graphScore / 2.0)); // PEDSA 能量范围 [-2, 2] → [0, 1] const normVec = Math.max(0, Math.min(1, vectorScore)); + const normLexical = Math.max(0, Math.min(1, lexicalScore)); const normImportance = Math.max(0, Math.min(1, importance / 10.0)); + const totalWeight = Math.max( + 1e-6, + Math.max(0, alpha) + Math.max(0, beta) + Math.max(0, gamma) + Math.max(0, delta), + ); - const baseScore = normGraph * alpha + normVec * beta + normImportance * gamma; + const baseScore = + (normGraph * alpha + + normVec * beta + + normLexical * delta + + normImportance * gamma) / + totalWeight; const decay = timeDecayFactor(createdTime); return baseScore * decay; diff --git a/index.js b/index.js index 61ef707..bbb05fe 100644 --- a/index.js +++ b/index.js @@ -136,16 +136,19 @@ import { resolveConfiguredTimeoutMs } from "./request-timeout.js"; import { retrieve } from "./retriever.js"; import { appendBatchJournal, + appendMaintenanceJournal, buildRecoveryResult, buildReverseJournalRecoveryPlan, clearHistoryDirty, cloneGraphSnapshot, createBatchJournalEntry, + createMaintenanceJournalEntry, detectHistoryMutation, findJournalRecoveryPoint, markHistoryDirty, normalizeGraphRuntimeState, snapshotProcessedMessageHashes, + undoLatestMaintenance, } from "./runtime-state.js"; import { DEFAULT_NODE_SCHEMA, validateSchema } from "./schema.js"; import { @@ -157,6 +160,7 @@ import { onManualEvolveController, onManualSleepController, onManualSynopsisController, + onUndoLastMaintenanceController, onRebuildController, onRebuildVectorIndexController, onReembedDirectController, @@ -249,6 +253,10 @@ function getRuntimeDebugState() { taskPromptBuilds: {}, taskLlmRequests: {}, injections: {}, + maintenance: { + lastAction: null, + lastUndoResult: null, + }, graphPersistence: null, updatedAt: "", }; @@ -281,6 +289,18 @@ function recordGraphPersistenceSnapshot(snapshot = null) { state.graphPersistence = cloneRuntimeDebugValue(snapshot, null); } +function recordMaintenanceDebugSnapshot(patch = {}) { + const state = touchRuntimeDebugState(); + const previous = state.maintenance || { + lastAction: null, + lastUndoResult: null, + }; + state.maintenance = { + ...previous, + ...cloneRuntimeDebugValue(patch, {}), + }; +} + function readRuntimeDebugSnapshot() { const state = getRuntimeDebugState(); return cloneRuntimeDebugValue( @@ -289,6 +309,7 @@ function readRuntimeDebugSnapshot() { taskPromptBuilds: state.taskPromptBuilds, taskLlmRequests: state.taskLlmRequests, injections: state.injections, + maintenance: state.maintenance, graphPersistence: state.graphPersistence, updatedAt: state.updatedAt, }, @@ -297,6 +318,10 @@ function readRuntimeDebugSnapshot() { taskPromptBuilds: {}, taskLlmRequests: {}, injections: {}, + maintenance: { + lastAction: null, + lastUndoResult: null, + }, graphPersistence: null, updatedAt: "", }, @@ -325,6 +350,11 @@ const defaultSettings = { recallLlmContextMessages: 4, // 传给 LLM 精排的最近非系统消息数 recallEnableMultiIntent: true, recallMultiIntentMaxSegments: 4, + recallEnableContextQueryBlend: true, + recallContextAssistantWeight: 0.2, + recallContextPreviousUserWeight: 0.1, + recallEnableLexicalBoost: true, + recallLexicalWeight: 0.18, recallTeleportAlpha: 0.15, recallEnableTemporalLinks: true, recallTemporalLinkStrength: 0.2, @@ -412,6 +442,7 @@ const defaultSettings = { // ⑩ 反思条目(P2) enableReflection: true, // 启用反思 reflectEveryN: 10, // 每 N 次提取后反思 + maintenanceAutoMinNewNodes: 3, // UI 面板 panelTheme: "crimson", // 面板主题 crimson|cyan|amber|violet @@ -4148,6 +4179,117 @@ async function recordGraphMutation({ return vectorSync; } +function noteMaintenanceGate(status, action, reason) { + if (!status || typeof status !== "object") return; + const normalizedAction = String(action || "").trim() || "unknown"; + const normalizedReason = String(reason || "").trim(); + if (!normalizedReason) return; + + const nextDetail = { + action: normalizedAction, + reason: normalizedReason, + }; + const previousDetails = Array.isArray(status.maintenanceGateDetails) + ? status.maintenanceGateDetails + : []; + status.maintenanceGateApplied = true; + status.maintenanceGateDetails = [...previousDetails, nextDetail]; + status.maintenanceGateReason = status.maintenanceGateDetails + .map((item) => `${item.action}: ${item.reason}`) + .join(" | "); +} + +function evaluateAutoMaintenanceGate(action, newNodeCount, settings = {}) { + const normalizedAction = String(action || "").trim(); + if (!["consolidate", "compress"].includes(normalizedAction)) { + return { blocked: false, reason: "", minNewNodes: 0 }; + } + if (settings?.maintenanceAutoMinNewNodes == null) { + return { blocked: false, reason: "", minNewNodes: 0 }; + } + + const minNewNodes = clampInt(settings.maintenanceAutoMinNewNodes, 3, 1, 50); + const safeNewNodeCount = Math.max(0, Number(newNodeCount) || 0); + if (safeNewNodeCount >= minNewNodes) { + return { blocked: false, reason: "", minNewNodes }; + } + + return { + blocked: true, + minNewNodes, + reason: `本批只新增 ${safeNewNodeCount} 个节点,低于门槛 ${minNewNodes}`, + }; +} + +function buildMaintenanceSummary(action, result, mode = "manual") { + const prefix = mode === "auto" ? "自动" : "手动"; + switch (String(action || "")) { + case "compress": + return `${prefix}压缩:新建 ${result?.created || 0},归档 ${result?.archived || 0}`; + case "consolidate": + return `${prefix}整合:合并 ${result?.merged || 0},跳过 ${result?.skipped || 0},保留 ${result?.kept || 0},进化 ${result?.evolved || 0},新链接 ${result?.connections || 0},回溯更新 ${result?.updates || 0}`; + case "sleep": + return `${prefix}遗忘:归档 ${result?.forgotten || 0} 个节点`; + default: + return `${prefix}维护已执行`; + } +} + +function recordMaintenanceAction({ + action, + beforeSnapshot, + mode = "manual", + summary = "", +} = {}) { + if (!currentGraph || !beforeSnapshot) return null; + ensureCurrentGraphRuntimeState(); + + const entry = createMaintenanceJournalEntry( + beforeSnapshot, + cloneGraphSnapshot(currentGraph), + { + action, + mode, + summary, + }, + ); + if (!entry) return null; + + appendMaintenanceJournal(currentGraph, entry); + recordMaintenanceDebugSnapshot({ + lastAction: { + id: entry.id, + action: entry.action, + mode: entry.mode, + summary: entry.summary, + createdAt: entry.createdAt, + maintenanceJournalSize: currentGraph.maintenanceJournal?.length || 0, + }, + }); + return entry; +} + +function undoLastMaintenanceAction() { + if (!currentGraph) { + return { ok: false, reason: "当前没有加载的图谱", entry: null }; + } + + ensureCurrentGraphRuntimeState(); + const result = undoLatestMaintenance(currentGraph); + recordMaintenanceDebugSnapshot({ + lastUndoResult: { + ok: Boolean(result?.ok), + reason: String(result?.reason || ""), + action: result?.entry?.action || "", + summary: result?.entry?.summary || "", + createdAt: result?.entry?.createdAt || 0, + maintenanceJournalSize: currentGraph.maintenanceJournal?.length || 0, + updatedAt: new Date().toISOString(), + }, + }); + return result; +} + function markVectorStateDirty(reason = "向量状态已标记为待重建") { if (!currentGraph) return; ensureCurrentGraphRuntimeState(); @@ -6027,6 +6169,78 @@ async function handleExtractionSuccess( }), ) { const postProcessArtifacts = []; + const newNodeCount = Array.isArray(result?.newNodeIds) + ? result.newNodeIds.length + : 0; + const resolveAutoMaintenanceGate = + typeof evaluateAutoMaintenanceGate === "function" + ? evaluateAutoMaintenanceGate + : (action, count, localSettings = {}) => { + const normalizedAction = String(action || "").trim(); + if (!["consolidate", "compress"].includes(normalizedAction)) { + return { blocked: false, reason: "", minNewNodes: 0 }; + } + if (localSettings?.maintenanceAutoMinNewNodes == null) { + return { blocked: false, reason: "", minNewNodes: 0 }; + } + const parsedMinNewNodes = Math.floor( + Number(localSettings.maintenanceAutoMinNewNodes), + ); + const minNewNodes = + Number.isFinite(parsedMinNewNodes) && parsedMinNewNodes >= 1 + ? Math.min(50, parsedMinNewNodes) + : 3; + const safeCount = Math.max(0, Number(count) || 0); + return safeCount >= minNewNodes + ? { blocked: false, reason: "", minNewNodes } + : { + blocked: true, + minNewNodes, + reason: `本批只新增 ${safeCount} 个节点,低于门槛 ${minNewNodes}`, + }; + }; + const applyMaintenanceGateNote = + typeof noteMaintenanceGate === "function" + ? noteMaintenanceGate + : (batchStatus, action, reason) => { + if (!batchStatus || !reason) return; + batchStatus.maintenanceGateApplied = true; + const details = Array.isArray(batchStatus.maintenanceGateDetails) + ? batchStatus.maintenanceGateDetails + : []; + details.push({ + action: String(action || "").trim() || "unknown", + reason: String(reason || ""), + }); + batchStatus.maintenanceGateDetails = details; + batchStatus.maintenanceGateReason = details + .map((item) => `${item.action}: ${item.reason}`) + .join(" | "); + }; + const summarizeMaintenance = + typeof buildMaintenanceSummary === "function" + ? buildMaintenanceSummary + : (action, maintenanceResult, mode = "manual") => { + const prefix = mode === "auto" ? "自动" : "手动"; + switch (String(action || "")) { + case "compress": + return `${prefix}压缩:新建 ${maintenanceResult?.created || 0},归档 ${maintenanceResult?.archived || 0}`; + case "consolidate": + return `${prefix}整合:合并 ${maintenanceResult?.merged || 0},跳过 ${maintenanceResult?.skipped || 0},保留 ${maintenanceResult?.kept || 0},进化 ${maintenanceResult?.evolved || 0},新链接 ${maintenanceResult?.connections || 0},回溯更新 ${maintenanceResult?.updates || 0}`; + case "sleep": + return `${prefix}遗忘:归档 ${maintenanceResult?.forgotten || 0} 个节点`; + default: + return `${prefix}维护已执行`; + } + }; + const cloneMaintenanceSnapshot = + typeof cloneGraphSnapshot === "function" + ? cloneGraphSnapshot + : (value) => JSON.parse(JSON.stringify(value ?? null)); + const persistMaintenanceAction = + typeof recordMaintenanceAction === "function" + ? recordMaintenanceAction + : () => null; throwIfAborted(signal, "提取已终止"); extractionCount++; ensureCurrentGraphRuntimeState(); @@ -6035,30 +6249,51 @@ async function handleExtractionSuccess( setBatchStageOutcome(status, "core", "success"); if (settings.enableConsolidation && result.newNodeIds?.length > 0) { - try { - await consolidateMemories({ - graph: currentGraph, - newNodeIds: result.newNodeIds, - embeddingConfig: getEmbeddingConfig(), - options: { - neighborCount: settings.consolidationNeighborCount, - conflictThreshold: settings.consolidationThreshold, - }, - settings, - signal, - }); - postProcessArtifacts.push("consolidation"); - pushBatchStageArtifact(status, "structural", "consolidation"); - } catch (e) { - if (isAbortError(e)) throw e; - const message = e?.message || String(e) || "记忆整合阶段失败"; - setBatchStageOutcome( - status, - "structural", - "partial", - `记忆整合失败: ${message}`, - ); - console.error("[ST-BME] 记忆整合失败:", e); + const gate = resolveAutoMaintenanceGate( + "consolidate", + newNodeCount, + settings, + ); + if (gate.blocked) { + applyMaintenanceGateNote(status, "consolidate", gate.reason); + pushBatchStageArtifact(status, "structural", "consolidation-skipped"); + } else { + try { + const beforeSnapshot = cloneMaintenanceSnapshot(currentGraph); + const consolidationResult = await consolidateMemories({ + graph: currentGraph, + newNodeIds: result.newNodeIds, + embeddingConfig: getEmbeddingConfig(), + options: { + neighborCount: settings.consolidationNeighborCount, + conflictThreshold: settings.consolidationThreshold, + }, + settings, + signal, + }); + persistMaintenanceAction({ + action: "consolidate", + beforeSnapshot, + mode: "auto", + summary: summarizeMaintenance( + "consolidate", + consolidationResult, + "auto", + ), + }); + postProcessArtifacts.push("consolidation"); + pushBatchStageArtifact(status, "structural", "consolidation"); + } catch (e) { + if (isAbortError(e)) throw e; + const message = e?.message || String(e) || "记忆整合阶段失败"; + setBatchStageOutcome( + status, + "structural", + "partial", + `记忆整合失败: ${message}`, + ); + console.error("[ST-BME] 记忆整合失败:", e); + } } } @@ -6120,9 +6355,18 @@ async function handleExtractionSuccess( extractionCount % settings.sleepEveryN === 0 ) { try { - sleepCycle(currentGraph, settings); - postProcessArtifacts.push("sleep"); - pushBatchStageArtifact(status, "semantic", "sleep"); + const beforeSnapshot = cloneMaintenanceSnapshot(currentGraph); + const sleepResult = sleepCycle(currentGraph, settings); + if ((sleepResult?.forgotten || 0) > 0) { + persistMaintenanceAction({ + action: "sleep", + beforeSnapshot, + mode: "auto", + summary: summarizeMaintenance("sleep", sleepResult, "auto"), + }); + postProcessArtifacts.push("sleep"); + pushBatchStageArtifact(status, "semantic", "sleep"); + } } catch (e) { const message = e?.message || String(e) || "主动遗忘阶段失败"; setBatchStageOutcome( @@ -6137,18 +6381,39 @@ async function handleExtractionSuccess( try { throwIfAborted(signal, "提取已终止"); - const compressionResult = await compressAll( - currentGraph, - getSchema(), - getEmbeddingConfig(), - false, - undefined, - signal, + const gate = resolveAutoMaintenanceGate( + "compress", + newNodeCount, settings, ); - if (compressionResult.created > 0 || compressionResult.archived > 0) { - postProcessArtifacts.push("compression"); - pushBatchStageArtifact(status, "structural", "compression"); + if (gate.blocked) { + applyMaintenanceGateNote(status, "compress", gate.reason); + pushBatchStageArtifact(status, "structural", "compression-skipped"); + } else { + const beforeSnapshot = cloneMaintenanceSnapshot(currentGraph); + const compressionResult = await compressAll( + currentGraph, + getSchema(), + getEmbeddingConfig(), + false, + undefined, + signal, + settings, + ); + if (compressionResult.created > 0 || compressionResult.archived > 0) { + persistMaintenanceAction({ + action: "compress", + beforeSnapshot, + mode: "auto", + summary: summarizeMaintenance( + "compress", + compressionResult, + "auto", + ), + }); + postProcessArtifacts.push("compression"); + pushBatchStageArtifact(status, "structural", "compression"); + } } } catch (error) { if (isAbortError(error)) throw error; @@ -6198,6 +6463,18 @@ async function handleExtractionSuccess( setBatchStageOutcome(status, "finalize", "success"); } + status.maintenanceJournalSize = + currentGraph?.maintenanceJournal?.length || 0; + if ( + status.maintenanceGateApplied && + !status.maintenanceGateReason && + Array.isArray(status.maintenanceGateDetails) + ) { + status.maintenanceGateReason = status.maintenanceGateDetails + .map((item) => `${item.action}: ${item.reason}`) + .join(" | "); + } + return { postProcessArtifacts, vectorHashesInserted: vectorSync?.insertedHashes || [], @@ -7107,6 +7384,12 @@ function buildRecallRetrieveOptions(settings, context) { probRecallChance: settings.probRecallChance ?? 0.15, enableMultiIntent: settings.recallEnableMultiIntent ?? true, multiIntentMaxSegments: settings.recallMultiIntentMaxSegments ?? 4, + enableContextQueryBlend: settings.recallEnableContextQueryBlend ?? true, + contextAssistantWeight: settings.recallContextAssistantWeight ?? 0.2, + contextPreviousUserWeight: + settings.recallContextPreviousUserWeight ?? 0.1, + enableLexicalBoost: settings.recallEnableLexicalBoost ?? true, + lexicalWeight: settings.recallLexicalWeight ?? 0.18, teleportAlpha: settings.recallTeleportAlpha ?? 0.15, enableTemporalLinks: settings.recallEnableTemporalLinks ?? true, temporalLinkStrength: settings.recallTemporalLinkStrength ?? 0.2, @@ -7426,6 +7709,7 @@ async function onRebuild() { async function onManualCompress() { return await onManualCompressController({ + buildMaintenanceSummary, cloneGraphSnapshot, compressAll, ensureGraphMutationReady, @@ -7433,6 +7717,7 @@ async function onManualCompress() { getEmbeddingConfig, getSchema, getSettings, + recordMaintenanceAction, recordGraphMutation, toastr, }); @@ -7577,10 +7862,12 @@ async function onReroll({ fromFloor } = {}) { async function onManualSleep() { return await onManualSleepController({ + buildMaintenanceSummary, cloneGraphSnapshot, ensureGraphMutationReady, getCurrentGraph: () => currentGraph, getSettings, + recordMaintenanceAction, recordGraphMutation, sleepCycle, toastr, @@ -7603,6 +7890,7 @@ async function onManualSynopsis() { async function onManualEvolve() { return await onManualEvolveController({ + buildMaintenanceSummary, cloneGraphSnapshot, consolidateMemories, ensureGraphMutationReady, @@ -7610,11 +7898,24 @@ async function onManualEvolve() { getEmbeddingConfig, getLastExtractedItems: () => lastExtractedItems, getSettings, + recordMaintenanceAction, recordGraphMutation, toastr, }); } +async function onUndoLastMaintenance() { + return await onUndoLastMaintenanceController({ + ensureGraphMutationReady, + getCurrentGraph: () => currentGraph, + markVectorStateDirty, + refreshPanelLiveState, + saveGraphToChat, + toastr, + undoLastMaintenance: undoLastMaintenanceAction, + }); +} + async function onRebuildVectorIndex(range = null) { return await onRebuildVectorIndexController( { @@ -7713,6 +8014,7 @@ async function onReembedDirect() { import: onImportGraph, rebuild: onRebuild, evolve: onManualEvolve, + undoMaintenance: onUndoLastMaintenance, testEmbedding: onTestEmbedding, testMemoryLLM: onTestMemoryLLM, fetchMemoryLLMModels: onFetchMemoryLLMModels, diff --git a/panel.html b/panel.html index 76c8b4b..b94409a 100644 --- a/panel.html +++ b/panel.html @@ -269,6 +269,10 @@ 执行遗忘 +