mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-06-14 02:40:45 +08:00
Refine automatic consolidation and compression triggers
This commit is contained in:
112
compressor.js
112
compressor.js
@@ -58,6 +58,28 @@ function throwIfAborted(signal) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveCompressionWindow(compression = {}, force = false) {
|
||||||
|
const fanIn = Number.isFinite(Number(compression?.fanIn))
|
||||||
|
? Math.max(2, Number(compression.fanIn))
|
||||||
|
: 2;
|
||||||
|
const threshold = force
|
||||||
|
? fanIn
|
||||||
|
: Number.isFinite(Number(compression?.threshold))
|
||||||
|
? Math.max(2, Number(compression.threshold))
|
||||||
|
: fanIn;
|
||||||
|
const keepRecent = force
|
||||||
|
? 0
|
||||||
|
: Number.isFinite(Number(compression?.keepRecentLeaves))
|
||||||
|
? Math.max(0, Number(compression.keepRecentLeaves))
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
fanIn,
|
||||||
|
threshold,
|
||||||
|
keepRecent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 对指定类型执行层级压缩
|
* 对指定类型执行层级压缩
|
||||||
*
|
*
|
||||||
@@ -126,9 +148,10 @@ async function compressLevel({
|
|||||||
settings = {},
|
settings = {},
|
||||||
}) {
|
}) {
|
||||||
const compression = typeDef.compression;
|
const compression = typeDef.compression;
|
||||||
const fanIn = Number.isFinite(Number(compression.fanIn))
|
const { fanIn, threshold, keepRecent } = resolveCompressionWindow(
|
||||||
? Math.max(2, Number(compression.fanIn))
|
compression,
|
||||||
: 2;
|
force,
|
||||||
|
);
|
||||||
throwIfAborted(signal);
|
throwIfAborted(signal);
|
||||||
|
|
||||||
// 获取该层级的活跃叶子节点
|
// 获取该层级的活跃叶子节点
|
||||||
@@ -138,17 +161,6 @@ async function compressLevel({
|
|||||||
let created = 0;
|
let created = 0;
|
||||||
let archived = 0;
|
let archived = 0;
|
||||||
|
|
||||||
const threshold = force
|
|
||||||
? fanIn
|
|
||||||
: Number.isFinite(Number(compression.threshold))
|
|
||||||
? Math.max(2, Number(compression.threshold))
|
|
||||||
: fanIn;
|
|
||||||
const keepRecent = force
|
|
||||||
? 0
|
|
||||||
: Number.isFinite(Number(compression.keepRecentLeaves))
|
|
||||||
? Math.max(0, Number(compression.keepRecentLeaves))
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
for (const group of groupCompressionCandidates(levelNodes)) {
|
for (const group of groupCompressionCandidates(levelNodes)) {
|
||||||
if (force ? group.length < fanIn : group.length <= threshold) {
|
if (force ? group.length < fanIn : group.length <= threshold) {
|
||||||
continue;
|
continue;
|
||||||
@@ -237,6 +249,78 @@ function groupCompressionCandidates(nodes = []) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function inspectCompressibleGroup(group = [], compression = {}, force = false) {
|
||||||
|
const { fanIn, threshold, keepRecent } = resolveCompressionWindow(
|
||||||
|
compression,
|
||||||
|
force,
|
||||||
|
);
|
||||||
|
if (force ? group.length < fanIn : group.length <= threshold) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const compressible = group.slice(0, Math.max(0, group.length - keepRecent));
|
||||||
|
if (compressible.length < fanIn) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
candidateCount: compressible.length,
|
||||||
|
fanIn,
|
||||||
|
threshold,
|
||||||
|
keepRecent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function inspectAutoCompressionCandidates(
|
||||||
|
graph,
|
||||||
|
schema = [],
|
||||||
|
force = false,
|
||||||
|
) {
|
||||||
|
const safeSchema = Array.isArray(schema) ? schema : [];
|
||||||
|
for (const typeDef of safeSchema) {
|
||||||
|
if (typeDef?.compression?.mode !== "hierarchical") continue;
|
||||||
|
const maxDepth = Number.isFinite(Number(typeDef?.compression?.maxDepth))
|
||||||
|
? Math.max(1, Number(typeDef.compression.maxDepth))
|
||||||
|
: 1;
|
||||||
|
|
||||||
|
for (let level = 0; level < maxDepth; level++) {
|
||||||
|
const levelNodes = getActiveNodes(graph, typeDef.id)
|
||||||
|
.filter((node) => Number(node?.level || 0) === level)
|
||||||
|
.sort((a, b) => a.seq - b.seq);
|
||||||
|
|
||||||
|
for (const group of groupCompressionCandidates(levelNodes)) {
|
||||||
|
const summary = inspectCompressibleGroup(
|
||||||
|
group,
|
||||||
|
typeDef.compression,
|
||||||
|
force,
|
||||||
|
);
|
||||||
|
if (!summary) continue;
|
||||||
|
return {
|
||||||
|
hasCandidates: true,
|
||||||
|
typeId: String(typeDef.id || ""),
|
||||||
|
level,
|
||||||
|
candidateCount: summary.candidateCount,
|
||||||
|
threshold: summary.threshold,
|
||||||
|
fanIn: summary.fanIn,
|
||||||
|
keepRecent: summary.keepRecent,
|
||||||
|
reason: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasCandidates: false,
|
||||||
|
typeId: "",
|
||||||
|
level: null,
|
||||||
|
candidateCount: 0,
|
||||||
|
threshold: 0,
|
||||||
|
fanIn: 0,
|
||||||
|
keepRecent: 0,
|
||||||
|
reason: "已到自动压缩周期,但当前没有达到内部压缩阈值的候选组",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function migrateBatchEdges(graph, batch, compressedNode) {
|
function migrateBatchEdges(graph, batch, compressedNode) {
|
||||||
const batchIds = new Set(batch.map((node) => node.id));
|
const batchIds = new Set(batch.map((node) => node.id));
|
||||||
|
|
||||||
|
|||||||
147
consolidator.js
147
consolidator.js
@@ -111,6 +111,153 @@ const CONSOLIDATION_SYSTEM_PROMPT = `你是一个记忆整合分析器。当新
|
|||||||
- 不要对无关记忆强行建立联系
|
- 不要对无关记忆强行建立联系
|
||||||
- neighbor_updates 中每条必须有实际意义的修改`;
|
- neighbor_updates 中每条必须有实际意义的修改`;
|
||||||
|
|
||||||
|
function normalizeLatestOnlyIdentityValue(value) {
|
||||||
|
return String(value ?? "")
|
||||||
|
.trim()
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function analyzeAutoConsolidationGate({
|
||||||
|
graph,
|
||||||
|
newNodeIds,
|
||||||
|
embeddingConfig,
|
||||||
|
schema = [],
|
||||||
|
conflictThreshold = 0.85,
|
||||||
|
signal,
|
||||||
|
} = {}) {
|
||||||
|
const normalizedThreshold = Number.isFinite(Number(conflictThreshold))
|
||||||
|
? Math.max(0, Math.min(1, Number(conflictThreshold)))
|
||||||
|
: 0.85;
|
||||||
|
const safeNewNodeIds = Array.isArray(newNodeIds) ? newNodeIds : [];
|
||||||
|
|
||||||
|
if (!graph || safeNewNodeIds.length === 0) {
|
||||||
|
return {
|
||||||
|
triggered: false,
|
||||||
|
reason: "本批新增少且无明显重复风险,跳过自动整合",
|
||||||
|
matchedScore: null,
|
||||||
|
matchedNodeId: "",
|
||||||
|
detection: "none",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const schemaByType = new Map(
|
||||||
|
(Array.isArray(schema) ? schema : [])
|
||||||
|
.filter((typeDef) => typeDef?.id)
|
||||||
|
.map((typeDef) => [String(typeDef.id), typeDef]),
|
||||||
|
);
|
||||||
|
const activeNodes = getActiveNodes(graph).filter((node) => !node?.archived);
|
||||||
|
const vectorConfigValid = validateVectorConfig(embeddingConfig).valid;
|
||||||
|
let bestVectorMatch = null;
|
||||||
|
|
||||||
|
for (const newNodeId of safeNewNodeIds) {
|
||||||
|
throwIfAborted(signal);
|
||||||
|
const node = getNode(graph, newNodeId);
|
||||||
|
if (!node || node.archived) continue;
|
||||||
|
|
||||||
|
const typeDef = schemaByType.get(String(node.type || ""));
|
||||||
|
const scopedCandidates = activeNodes.filter(
|
||||||
|
(candidate) =>
|
||||||
|
candidate?.id !== node.id && canMergeScopedMemories(node, candidate),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (typeDef?.latestOnly) {
|
||||||
|
for (const field of ["name", "title"]) {
|
||||||
|
const normalizedIdentity = normalizeLatestOnlyIdentityValue(
|
||||||
|
node?.fields?.[field],
|
||||||
|
);
|
||||||
|
if (!normalizedIdentity) continue;
|
||||||
|
const matchedNode = scopedCandidates.find(
|
||||||
|
(candidate) =>
|
||||||
|
candidate?.type === node.type &&
|
||||||
|
normalizeLatestOnlyIdentityValue(candidate?.fields?.[field]) ===
|
||||||
|
normalizedIdentity,
|
||||||
|
);
|
||||||
|
if (matchedNode) {
|
||||||
|
return {
|
||||||
|
triggered: true,
|
||||||
|
reason: `本批仅新增 ${safeNewNodeIds.length} 个节点,但 latestOnly 的 ${field} 与旧记忆完全一致,已触发自动整合`,
|
||||||
|
matchedScore: 1,
|
||||||
|
matchedNodeId: matchedNode.id,
|
||||||
|
detection: `latestOnly:${field}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!vectorConfigValid) continue;
|
||||||
|
const text = buildNodeVectorText(node);
|
||||||
|
if (!text) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const neighbors = await findSimilarNodesByText(
|
||||||
|
graph,
|
||||||
|
text,
|
||||||
|
embeddingConfig,
|
||||||
|
1,
|
||||||
|
scopedCandidates,
|
||||||
|
signal,
|
||||||
|
);
|
||||||
|
const topNeighbor = Array.isArray(neighbors) ? neighbors[0] : null;
|
||||||
|
if (!topNeighbor?.nodeId) continue;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!bestVectorMatch ||
|
||||||
|
Number(topNeighbor.score || 0) > Number(bestVectorMatch.score || 0)
|
||||||
|
) {
|
||||||
|
bestVectorMatch = {
|
||||||
|
score: Number(topNeighbor.score || 0),
|
||||||
|
nodeId: topNeighbor.nodeId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number(topNeighbor.score || 0) >= normalizedThreshold) {
|
||||||
|
return {
|
||||||
|
triggered: true,
|
||||||
|
reason: `本批仅新增 ${safeNewNodeIds.length} 个节点,但与旧记忆高度相似(${Number(topNeighbor.score || 0).toFixed(3)} >= ${normalizedThreshold.toFixed(2)}),已触发自动整合`,
|
||||||
|
matchedScore: Number(topNeighbor.score || 0),
|
||||||
|
matchedNodeId: topNeighbor.nodeId,
|
||||||
|
detection: "vector",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (isAbortError(error)) throw error;
|
||||||
|
console.warn(
|
||||||
|
`[ST-BME] 自动整合门禁近邻查询失败 (${newNodeId}):`,
|
||||||
|
error?.message || error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestVectorMatch) {
|
||||||
|
return {
|
||||||
|
triggered: false,
|
||||||
|
reason: `本批新增少且最高相似度 ${bestVectorMatch.score.toFixed(3)} 未达到阈值 ${normalizedThreshold.toFixed(2)},跳过自动整合`,
|
||||||
|
matchedScore: bestVectorMatch.score,
|
||||||
|
matchedNodeId: bestVectorMatch.nodeId,
|
||||||
|
detection: "vector",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!vectorConfigValid) {
|
||||||
|
return {
|
||||||
|
triggered: false,
|
||||||
|
reason: "本批新增少且当前向量不可用,未检测到明确重复风险,跳过自动整合",
|
||||||
|
matchedScore: null,
|
||||||
|
matchedNodeId: "",
|
||||||
|
detection: "vector-unavailable",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
triggered: false,
|
||||||
|
reason: "本批新增少且无明显重复风险,跳过自动整合",
|
||||||
|
matchedScore: null,
|
||||||
|
matchedNodeId: "",
|
||||||
|
detection: "none",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 统一记忆整合主函数(批量化版)
|
* 统一记忆整合主函数(批量化版)
|
||||||
*
|
*
|
||||||
|
|||||||
361
index.js
361
index.js
@@ -38,8 +38,15 @@ import {
|
|||||||
resolveDirtyFloorFromMutationMeta,
|
resolveDirtyFloorFromMutationMeta,
|
||||||
rollbackAffectedJournals,
|
rollbackAffectedJournals,
|
||||||
} from "./chat-history.js";
|
} from "./chat-history.js";
|
||||||
import { compressAll, sleepCycle } from "./compressor.js";
|
import {
|
||||||
import { consolidateMemories } from "./consolidator.js";
|
compressAll,
|
||||||
|
inspectAutoCompressionCandidates,
|
||||||
|
sleepCycle,
|
||||||
|
} from "./compressor.js";
|
||||||
|
import {
|
||||||
|
analyzeAutoConsolidationGate,
|
||||||
|
consolidateMemories,
|
||||||
|
} from "./consolidator.js";
|
||||||
import {
|
import {
|
||||||
installSendIntentHooksController,
|
installSendIntentHooksController,
|
||||||
onBeforeCombinePromptsController,
|
onBeforeCombinePromptsController,
|
||||||
@@ -465,7 +472,8 @@ const defaultSettings = {
|
|||||||
// ⑩ 反思条目(P2)
|
// ⑩ 反思条目(P2)
|
||||||
enableReflection: true, // 启用反思
|
enableReflection: true, // 启用反思
|
||||||
reflectEveryN: 10, // 每 N 次提取后反思
|
reflectEveryN: 10, // 每 N 次提取后反思
|
||||||
maintenanceAutoMinNewNodes: 3,
|
consolidationAutoMinNewNodes: 2,
|
||||||
|
compressionEveryN: 10,
|
||||||
|
|
||||||
// UI 面板
|
// UI 面板
|
||||||
panelTheme: "crimson", // 面板主题 crimson|cyan|amber|violet
|
panelTheme: "crimson", // 面板主题 crimson|cyan|amber|violet
|
||||||
@@ -2677,10 +2685,37 @@ function installSendIntentHooks() {
|
|||||||
|
|
||||||
// ==================== 设置管理 ====================
|
// ==================== 设置管理 ====================
|
||||||
|
|
||||||
|
function migrateLegacyAutoMaintenanceSettings(loaded = {}) {
|
||||||
|
if (!loaded || typeof loaded !== "object" || Array.isArray(loaded)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const migrated = { ...loaded };
|
||||||
|
if (
|
||||||
|
!Object.prototype.hasOwnProperty.call(
|
||||||
|
migrated,
|
||||||
|
"consolidationAutoMinNewNodes",
|
||||||
|
) &&
|
||||||
|
Object.prototype.hasOwnProperty.call(migrated, "maintenanceAutoMinNewNodes")
|
||||||
|
) {
|
||||||
|
migrated.consolidationAutoMinNewNodes = clampInt(
|
||||||
|
migrated.maintenanceAutoMinNewNodes,
|
||||||
|
defaultSettings.consolidationAutoMinNewNodes,
|
||||||
|
1,
|
||||||
|
50,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
delete migrated.maintenanceAutoMinNewNodes;
|
||||||
|
return migrated;
|
||||||
|
}
|
||||||
|
|
||||||
function getSettings() {
|
function getSettings() {
|
||||||
|
const loadedSettings = migrateLegacyAutoMaintenanceSettings(
|
||||||
|
extension_settings[MODULE_NAME] || {},
|
||||||
|
);
|
||||||
const mergedSettings = {
|
const mergedSettings = {
|
||||||
...defaultSettings,
|
...defaultSettings,
|
||||||
...(extension_settings[MODULE_NAME] || {}),
|
...loadedSettings,
|
||||||
};
|
};
|
||||||
const migrated = migrateLegacyTaskProfiles(mergedSettings);
|
const migrated = migrateLegacyTaskProfiles(mergedSettings);
|
||||||
mergedSettings.taskProfilesVersion = migrated.taskProfilesVersion;
|
mergedSettings.taskProfilesVersion = migrated.taskProfilesVersion;
|
||||||
@@ -4958,25 +4993,86 @@ function noteMaintenanceGate(status, action, reason) {
|
|||||||
.join(" | ");
|
.join(" | ");
|
||||||
}
|
}
|
||||||
|
|
||||||
function evaluateAutoMaintenanceGate(action, newNodeCount, settings = {}) {
|
function evaluateAutoConsolidationGate(
|
||||||
const normalizedAction = String(action || "").trim();
|
newNodeCount,
|
||||||
if (!["consolidate", "compress"].includes(normalizedAction)) {
|
analysis = null,
|
||||||
return { blocked: false, reason: "", minNewNodes: 0 };
|
settings = {},
|
||||||
}
|
) {
|
||||||
if (settings?.maintenanceAutoMinNewNodes == null) {
|
const minNewNodes = clampInt(
|
||||||
return { blocked: false, reason: "", minNewNodes: 0 };
|
settings.consolidationAutoMinNewNodes,
|
||||||
}
|
2,
|
||||||
|
1,
|
||||||
const minNewNodes = clampInt(settings.maintenanceAutoMinNewNodes, 3, 1, 50);
|
50,
|
||||||
|
);
|
||||||
const safeNewNodeCount = Math.max(0, Number(newNodeCount) || 0);
|
const safeNewNodeCount = Math.max(0, Number(newNodeCount) || 0);
|
||||||
if (safeNewNodeCount >= minNewNodes) {
|
if (safeNewNodeCount >= minNewNodes) {
|
||||||
return { blocked: false, reason: "", minNewNodes };
|
return {
|
||||||
|
shouldRun: true,
|
||||||
|
minNewNodes,
|
||||||
|
reason: `本批新增 ${safeNewNodeCount} 个节点,达到自动整合门槛 ${minNewNodes}`,
|
||||||
|
matchedScore: null,
|
||||||
|
matchedNodeId: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (analysis?.triggered) {
|
||||||
|
return {
|
||||||
|
shouldRun: true,
|
||||||
|
minNewNodes,
|
||||||
|
reason:
|
||||||
|
String(analysis.reason || "").trim() ||
|
||||||
|
"检测到高重复风险,已触发自动整合",
|
||||||
|
matchedScore: Number.isFinite(Number(analysis?.matchedScore))
|
||||||
|
? Number(analysis.matchedScore)
|
||||||
|
: null,
|
||||||
|
matchedNodeId: String(analysis?.matchedNodeId || ""),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
blocked: true,
|
shouldRun: false,
|
||||||
minNewNodes,
|
minNewNodes,
|
||||||
reason: `本批只新增 ${safeNewNodeCount} 个节点,低于门槛 ${minNewNodes}`,
|
reason:
|
||||||
|
String(analysis?.reason || "").trim() ||
|
||||||
|
`本批只新增 ${safeNewNodeCount} 个节点,低于自动整合门槛 ${minNewNodes}`,
|
||||||
|
matchedScore: Number.isFinite(Number(analysis?.matchedScore))
|
||||||
|
? Number(analysis.matchedScore)
|
||||||
|
: null,
|
||||||
|
matchedNodeId: String(analysis?.matchedNodeId || ""),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateAutoCompressionSchedule(
|
||||||
|
currentExtractionCount,
|
||||||
|
settings = {},
|
||||||
|
) {
|
||||||
|
const everyN = clampInt(settings.compressionEveryN, 10, 0, 500);
|
||||||
|
const safeExtractionCount = Math.max(0, Number(currentExtractionCount) || 0);
|
||||||
|
|
||||||
|
if (everyN <= 0) {
|
||||||
|
return {
|
||||||
|
scheduled: false,
|
||||||
|
everyN,
|
||||||
|
nextExtractionCount: null,
|
||||||
|
reason: "自动压缩已关闭",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const remainder = safeExtractionCount % everyN;
|
||||||
|
if (remainder !== 0) {
|
||||||
|
return {
|
||||||
|
scheduled: false,
|
||||||
|
everyN,
|
||||||
|
nextExtractionCount: safeExtractionCount + (everyN - remainder),
|
||||||
|
reason: `当前为第 ${safeExtractionCount} 次提取,未到每 ${everyN} 次自动压缩周期`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
scheduled: true,
|
||||||
|
everyN,
|
||||||
|
nextExtractionCount: safeExtractionCount + everyN,
|
||||||
|
reason: "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5238,10 +5334,11 @@ function getPersistedSettingsSnapshot(settings = getSettings()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function mergePersistedSettings(loaded = {}) {
|
function mergePersistedSettings(loaded = {}) {
|
||||||
|
const compatibleLoaded = migrateLegacyAutoMaintenanceSettings(loaded);
|
||||||
const merged = { ...defaultSettings };
|
const merged = { ...defaultSettings };
|
||||||
for (const key of Object.keys(defaultSettings)) {
|
for (const key of Object.keys(defaultSettings)) {
|
||||||
if (Object.prototype.hasOwnProperty.call(loaded, key)) {
|
if (Object.prototype.hasOwnProperty.call(compatibleLoaded, key)) {
|
||||||
merged[key] = loaded[key];
|
merged[key] = compatibleLoaded[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return merged;
|
return merged;
|
||||||
@@ -7108,33 +7205,106 @@ async function handleExtractionSuccess(
|
|||||||
const newNodeCount = Array.isArray(result?.newNodeIds)
|
const newNodeCount = Array.isArray(result?.newNodeIds)
|
||||||
? result.newNodeIds.length
|
? result.newNodeIds.length
|
||||||
: 0;
|
: 0;
|
||||||
const resolveAutoMaintenanceGate =
|
const resolveAutoConsolidationGate =
|
||||||
typeof evaluateAutoMaintenanceGate === "function"
|
typeof evaluateAutoConsolidationGate === "function"
|
||||||
? evaluateAutoMaintenanceGate
|
? evaluateAutoConsolidationGate
|
||||||
: (action, count, localSettings = {}) => {
|
: (count, analysis = null, localSettings = {}) => {
|
||||||
const normalizedAction = String(action || "").trim();
|
const minNewNodes = Math.max(
|
||||||
if (!["consolidate", "compress"].includes(normalizedAction)) {
|
1,
|
||||||
return { blocked: false, reason: "", minNewNodes: 0 };
|
Math.min(
|
||||||
}
|
50,
|
||||||
if (localSettings?.maintenanceAutoMinNewNodes == null) {
|
Math.floor(
|
||||||
return { blocked: false, reason: "", minNewNodes: 0 };
|
Number(localSettings?.consolidationAutoMinNewNodes ?? 2),
|
||||||
}
|
) || 2,
|
||||||
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);
|
const safeCount = Math.max(0, Number(count) || 0);
|
||||||
return safeCount >= minNewNodes
|
if (safeCount >= minNewNodes) {
|
||||||
? { blocked: false, reason: "", minNewNodes }
|
return {
|
||||||
: {
|
shouldRun: true,
|
||||||
blocked: true,
|
minNewNodes,
|
||||||
minNewNodes,
|
reason: `本批新增 ${safeCount} 个节点,达到自动整合门槛 ${minNewNodes}`,
|
||||||
reason: `本批只新增 ${safeCount} 个节点,低于门槛 ${minNewNodes}`,
|
matchedScore: null,
|
||||||
};
|
matchedNodeId: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (analysis?.triggered) {
|
||||||
|
return {
|
||||||
|
shouldRun: true,
|
||||||
|
minNewNodes,
|
||||||
|
reason:
|
||||||
|
String(analysis.reason || "").trim() ||
|
||||||
|
"检测到高重复风险,已触发自动整合",
|
||||||
|
matchedScore: Number.isFinite(Number(analysis?.matchedScore))
|
||||||
|
? Number(analysis.matchedScore)
|
||||||
|
: null,
|
||||||
|
matchedNodeId: String(analysis?.matchedNodeId || ""),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
shouldRun: false,
|
||||||
|
minNewNodes,
|
||||||
|
reason:
|
||||||
|
String(analysis?.reason || "").trim() ||
|
||||||
|
`本批新增少且无明显重复风险,跳过自动整合`,
|
||||||
|
matchedScore: Number.isFinite(Number(analysis?.matchedScore))
|
||||||
|
? Number(analysis.matchedScore)
|
||||||
|
: null,
|
||||||
|
matchedNodeId: String(analysis?.matchedNodeId || ""),
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
const analyzeConsolidationGate =
|
||||||
|
typeof analyzeAutoConsolidationGate === "function"
|
||||||
|
? analyzeAutoConsolidationGate
|
||||||
|
: async () => ({
|
||||||
|
triggered: false,
|
||||||
|
reason: "本批新增少且无明显重复风险,跳过自动整合",
|
||||||
|
matchedScore: null,
|
||||||
|
matchedNodeId: "",
|
||||||
|
});
|
||||||
|
const resolveAutoCompressionSchedule =
|
||||||
|
typeof evaluateAutoCompressionSchedule === "function"
|
||||||
|
? evaluateAutoCompressionSchedule
|
||||||
|
: (currentCount, localSettings = {}) => {
|
||||||
|
const parsedEveryN = Math.floor(
|
||||||
|
Number(localSettings?.compressionEveryN),
|
||||||
|
);
|
||||||
|
const everyN =
|
||||||
|
Number.isFinite(parsedEveryN) && parsedEveryN >= 0
|
||||||
|
? Math.min(500, parsedEveryN)
|
||||||
|
: 10;
|
||||||
|
const safeCount = Math.max(0, Number(currentCount) || 0);
|
||||||
|
if (everyN <= 0) {
|
||||||
|
return {
|
||||||
|
scheduled: false,
|
||||||
|
everyN,
|
||||||
|
nextExtractionCount: null,
|
||||||
|
reason: "自动压缩已关闭",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const remainder = safeCount % everyN;
|
||||||
|
if (remainder !== 0) {
|
||||||
|
return {
|
||||||
|
scheduled: false,
|
||||||
|
everyN,
|
||||||
|
nextExtractionCount: safeCount + (everyN - remainder),
|
||||||
|
reason: `当前为第 ${safeCount} 次提取,未到每 ${everyN} 次自动压缩周期`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
scheduled: true,
|
||||||
|
everyN,
|
||||||
|
nextExtractionCount: safeCount + everyN,
|
||||||
|
reason: "",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const inspectCompressionCandidates =
|
||||||
|
typeof inspectAutoCompressionCandidates === "function"
|
||||||
|
? inspectAutoCompressionCandidates
|
||||||
|
: () => ({
|
||||||
|
hasCandidates: false,
|
||||||
|
reason: "已到自动压缩周期,但当前没有达到内部压缩阈值的候选组",
|
||||||
|
});
|
||||||
const applyMaintenanceGateNote =
|
const applyMaintenanceGateNote =
|
||||||
typeof noteMaintenanceGate === "function"
|
typeof noteMaintenanceGate === "function"
|
||||||
? noteMaintenanceGate
|
? noteMaintenanceGate
|
||||||
@@ -7185,12 +7355,38 @@ async function handleExtractionSuccess(
|
|||||||
setBatchStageOutcome(status, "core", "success");
|
setBatchStageOutcome(status, "core", "success");
|
||||||
|
|
||||||
if (settings.enableConsolidation && result.newNodeIds?.length > 0) {
|
if (settings.enableConsolidation && result.newNodeIds?.length > 0) {
|
||||||
const gate = resolveAutoMaintenanceGate(
|
let consolidationAnalysis = null;
|
||||||
"consolidate",
|
const minNewNodes = Math.max(
|
||||||
|
1,
|
||||||
|
Math.min(
|
||||||
|
50,
|
||||||
|
Math.floor(Number(settings?.consolidationAutoMinNewNodes ?? 2)) || 2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (newNodeCount < minNewNodes) {
|
||||||
|
consolidationAnalysis = await analyzeConsolidationGate({
|
||||||
|
graph: currentGraph,
|
||||||
|
newNodeIds: result.newNodeIds,
|
||||||
|
embeddingConfig: getEmbeddingConfig(),
|
||||||
|
schema: getSchema(),
|
||||||
|
conflictThreshold: settings.consolidationThreshold,
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const gate = resolveAutoConsolidationGate(
|
||||||
newNodeCount,
|
newNodeCount,
|
||||||
|
consolidationAnalysis,
|
||||||
settings,
|
settings,
|
||||||
);
|
);
|
||||||
if (gate.blocked) {
|
status.consolidationGateTriggered = Boolean(gate.shouldRun);
|
||||||
|
status.consolidationGateReason = String(gate.reason || "");
|
||||||
|
status.consolidationGateSimilarity = Number.isFinite(
|
||||||
|
Number(gate.matchedScore),
|
||||||
|
)
|
||||||
|
? Number(gate.matchedScore)
|
||||||
|
: null;
|
||||||
|
status.consolidationGateMatchedNodeId = String(gate.matchedNodeId || "");
|
||||||
|
if (!gate.shouldRun) {
|
||||||
applyMaintenanceGateNote(status, "consolidate", gate.reason);
|
applyMaintenanceGateNote(status, "consolidate", gate.reason);
|
||||||
pushBatchStageArtifact(status, "structural", "consolidation-skipped");
|
pushBatchStageArtifact(status, "structural", "consolidation-skipped");
|
||||||
} else {
|
} else {
|
||||||
@@ -7315,40 +7511,57 @@ async function handleExtractionSuccess(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const compressionSchedule = resolveAutoCompressionSchedule(
|
||||||
|
extractionCount,
|
||||||
|
settings,
|
||||||
|
);
|
||||||
|
status.autoCompressionScheduled = Boolean(compressionSchedule.scheduled);
|
||||||
|
status.nextCompressionAtExtractionCount =
|
||||||
|
compressionSchedule.nextExtractionCount;
|
||||||
|
status.autoCompressionSkippedReason = compressionSchedule.reason || "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
throwIfAborted(signal, "提取已终止");
|
throwIfAborted(signal, "提取已终止");
|
||||||
const gate = resolveAutoMaintenanceGate(
|
if (compressionSchedule.scheduled) {
|
||||||
"compress",
|
const compressionInspection = inspectCompressionCandidates(
|
||||||
newNodeCount,
|
|
||||||
settings,
|
|
||||||
);
|
|
||||||
if (gate.blocked) {
|
|
||||||
applyMaintenanceGateNote(status, "compress", gate.reason);
|
|
||||||
pushBatchStageArtifact(status, "structural", "compression-skipped");
|
|
||||||
} else {
|
|
||||||
const beforeSnapshot = cloneMaintenanceSnapshot(currentGraph);
|
|
||||||
const compressionResult = await compressAll(
|
|
||||||
currentGraph,
|
currentGraph,
|
||||||
getSchema(),
|
getSchema(),
|
||||||
getEmbeddingConfig(),
|
|
||||||
false,
|
false,
|
||||||
undefined,
|
|
||||||
signal,
|
|
||||||
settings,
|
|
||||||
);
|
);
|
||||||
if (compressionResult.created > 0 || compressionResult.archived > 0) {
|
if (!compressionInspection?.hasCandidates) {
|
||||||
persistMaintenanceAction({
|
status.autoCompressionSkippedReason =
|
||||||
action: "compress",
|
String(compressionInspection?.reason || "").trim() ||
|
||||||
beforeSnapshot,
|
"已到自动压缩周期,但当前没有达到内部压缩阈值的候选组";
|
||||||
mode: "auto",
|
pushBatchStageArtifact(status, "structural", "compression-skipped");
|
||||||
summary: summarizeMaintenance(
|
} else {
|
||||||
"compress",
|
status.autoCompressionSkippedReason = "";
|
||||||
compressionResult,
|
const beforeSnapshot = cloneMaintenanceSnapshot(currentGraph);
|
||||||
"auto",
|
const compressionResult = await compressAll(
|
||||||
),
|
currentGraph,
|
||||||
});
|
getSchema(),
|
||||||
postProcessArtifacts.push("compression");
|
getEmbeddingConfig(),
|
||||||
pushBatchStageArtifact(status, "structural", "compression");
|
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");
|
||||||
|
} else {
|
||||||
|
status.autoCompressionSkippedReason =
|
||||||
|
"已尝试自动压缩,但本轮未产生可持久化变化";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
31
panel.html
31
panel.html
@@ -1916,18 +1916,18 @@
|
|||||||
<div class="bme-config-card">
|
<div class="bme-config-card">
|
||||||
<div class="bme-config-card-head">
|
<div class="bme-config-card-head">
|
||||||
<div>
|
<div>
|
||||||
<div class="bme-config-card-title">自动维护门槛</div>
|
<div class="bme-config-card-title">自动整合触发</div>
|
||||||
<div class="bme-config-card-subtitle">
|
<div class="bme-config-card-subtitle">
|
||||||
新增节点太少时,自动整合和自动压缩直接跳过,避免小批次也跑重维护。
|
本批新增节点不足时,仍会检查是否与旧记忆高度重复;命中后照样自动整合。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bme-config-row">
|
<div class="bme-config-row">
|
||||||
<label for="bme-setting-maintenance-auto-min-new-nodes"
|
<label for="bme-setting-consolidation-auto-min-new-nodes"
|
||||||
>最少新增节点数</label
|
>最少新增节点数</label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
id="bme-setting-maintenance-auto-min-new-nodes"
|
id="bme-setting-consolidation-auto-min-new-nodes"
|
||||||
class="bme-config-input"
|
class="bme-config-input"
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
@@ -1936,6 +1936,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="bme-config-card">
|
||||||
|
<div class="bme-config-card-head">
|
||||||
|
<div>
|
||||||
|
<div class="bme-config-card-title">自动压缩周期</div>
|
||||||
|
<div class="bme-config-card-subtitle">
|
||||||
|
按提取次数计数;到周期点时才尝试自动压缩。填 0 表示关闭。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bme-config-row">
|
||||||
|
<label for="bme-setting-compression-every"
|
||||||
|
>每 N 次提取尝试一次</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="bme-setting-compression-every"
|
||||||
|
class="bme-config-input"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="bme-config-card bme-guarded-card"
|
class="bme-config-card bme-guarded-card"
|
||||||
data-guard-settings="enableProbRecall"
|
data-guard-settings="enableProbRecall"
|
||||||
|
|||||||
21
panel.js
21
panel.js
@@ -1765,8 +1765,12 @@ function _refreshConfigTab() {
|
|||||||
settings.forgetThreshold ?? 0.5,
|
settings.forgetThreshold ?? 0.5,
|
||||||
);
|
);
|
||||||
_setInputValue(
|
_setInputValue(
|
||||||
"bme-setting-maintenance-auto-min-new-nodes",
|
"bme-setting-consolidation-auto-min-new-nodes",
|
||||||
settings.maintenanceAutoMinNewNodes ?? 3,
|
settings.consolidationAutoMinNewNodes ?? 2,
|
||||||
|
);
|
||||||
|
_setInputValue(
|
||||||
|
"bme-setting-compression-every",
|
||||||
|
settings.compressionEveryN ?? 10,
|
||||||
);
|
);
|
||||||
_setInputValue("bme-setting-sleep-every", settings.sleepEveryN ?? 10);
|
_setInputValue("bme-setting-sleep-every", settings.sleepEveryN ?? 10);
|
||||||
_setInputValue(
|
_setInputValue(
|
||||||
@@ -2127,11 +2131,18 @@ function _bindConfigControls() {
|
|||||||
_patchSettings({ forgetThreshold: value }),
|
_patchSettings({ forgetThreshold: value }),
|
||||||
);
|
);
|
||||||
bindNumber(
|
bindNumber(
|
||||||
"bme-setting-maintenance-auto-min-new-nodes",
|
"bme-setting-consolidation-auto-min-new-nodes",
|
||||||
3,
|
2,
|
||||||
1,
|
1,
|
||||||
50,
|
50,
|
||||||
(value) => _patchSettings({ maintenanceAutoMinNewNodes: value }),
|
(value) => _patchSettings({ consolidationAutoMinNewNodes: value }),
|
||||||
|
);
|
||||||
|
bindNumber(
|
||||||
|
"bme-setting-compression-every",
|
||||||
|
10,
|
||||||
|
0,
|
||||||
|
500,
|
||||||
|
(value) => _patchSettings({ compressionEveryN: value }),
|
||||||
);
|
);
|
||||||
bindNumber("bme-setting-sleep-every", 10, 1, 200, (value) =>
|
bindNumber("bme-setting-sleep-every", 10, 1, 200, (value) =>
|
||||||
_patchSettings({ sleepEveryN: value }),
|
_patchSettings({ sleepEveryN: value }),
|
||||||
|
|||||||
@@ -34,7 +34,53 @@ this.defaultSettings = defaultSettings;
|
|||||||
return context.defaultSettings;
|
return context.defaultSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadSettingsCompatHelpers() {
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const indexPath = path.resolve(__dirname, "../index.js");
|
||||||
|
const source = await fs.readFile(indexPath, "utf8");
|
||||||
|
const settingsMatch = source.match(/const defaultSettings = \{[\s\S]*?^\};/m);
|
||||||
|
const compatMatch = source.match(
|
||||||
|
/function migrateLegacyAutoMaintenanceSettings\(loaded = \{\}\) \{[\s\S]*?^}\n/m,
|
||||||
|
);
|
||||||
|
const mergeMatch = source.match(
|
||||||
|
/function mergePersistedSettings\(loaded = \{\}\) \{[\s\S]*?^}\n/m,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!settingsMatch || !compatMatch || !mergeMatch) {
|
||||||
|
throw new Error("无法从 index.js 提取设置兼容辅助函数");
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = vm.createContext({
|
||||||
|
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)));
|
||||||
|
},
|
||||||
|
createDefaultTaskProfiles() {
|
||||||
|
return {
|
||||||
|
extract: { activeProfileId: "default", profiles: [] },
|
||||||
|
recall: { activeProfileId: "default", profiles: [] },
|
||||||
|
compress: { activeProfileId: "default", profiles: [] },
|
||||||
|
synopsis: { activeProfileId: "default", profiles: [] },
|
||||||
|
reflection: { activeProfileId: "default", profiles: [] },
|
||||||
|
consolidation: { activeProfileId: "default", profiles: [] },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const script = new vm.Script(`
|
||||||
|
${settingsMatch[0]}
|
||||||
|
${compatMatch[0]}
|
||||||
|
${mergeMatch[0]}
|
||||||
|
this.mergePersistedSettings = mergePersistedSettings;
|
||||||
|
`);
|
||||||
|
script.runInContext(context);
|
||||||
|
return {
|
||||||
|
mergePersistedSettings: context.mergePersistedSettings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const defaultSettings = await loadDefaultSettings();
|
const defaultSettings = await loadDefaultSettings();
|
||||||
|
const { mergePersistedSettings } = await loadSettingsCompatHelpers();
|
||||||
|
|
||||||
assert.equal(defaultSettings.extractContextTurns, 2);
|
assert.equal(defaultSettings.extractContextTurns, 2);
|
||||||
assert.equal(defaultSettings.recallTopK, 20);
|
assert.equal(defaultSettings.recallTopK, 20);
|
||||||
@@ -79,11 +125,20 @@ assert.equal(defaultSettings.injectObjectiveGlobalMemory, true);
|
|||||||
assert.equal(defaultSettings.injectDepth, 9999);
|
assert.equal(defaultSettings.injectDepth, 9999);
|
||||||
assert.equal(defaultSettings.enabled, true);
|
assert.equal(defaultSettings.enabled, true);
|
||||||
assert.equal(defaultSettings.enableReflection, true);
|
assert.equal(defaultSettings.enableReflection, true);
|
||||||
assert.equal(defaultSettings.maintenanceAutoMinNewNodes, 3);
|
assert.equal(defaultSettings.consolidationAutoMinNewNodes, 2);
|
||||||
|
assert.equal(defaultSettings.compressionEveryN, 10);
|
||||||
|
assert.equal("maintenanceAutoMinNewNodes" in defaultSettings, false);
|
||||||
assert.equal(defaultSettings.embeddingTransportMode, "direct");
|
assert.equal(defaultSettings.embeddingTransportMode, "direct");
|
||||||
assert.equal(defaultSettings.taskProfilesVersion, 3);
|
assert.equal(defaultSettings.taskProfilesVersion, 3);
|
||||||
assert.ok(defaultSettings.taskProfiles);
|
assert.ok(defaultSettings.taskProfiles);
|
||||||
assert.ok(defaultSettings.taskProfiles.extract);
|
assert.ok(defaultSettings.taskProfiles.extract);
|
||||||
assert.ok(defaultSettings.taskProfiles.recall);
|
assert.ok(defaultSettings.taskProfiles.recall);
|
||||||
|
|
||||||
|
const migratedSettings = mergePersistedSettings({
|
||||||
|
maintenanceAutoMinNewNodes: 7,
|
||||||
|
});
|
||||||
|
assert.equal(migratedSettings.consolidationAutoMinNewNodes, 7);
|
||||||
|
assert.equal(migratedSettings.compressionEveryN, 10);
|
||||||
|
assert.equal("maintenanceAutoMinNewNodes" in migratedSettings, false);
|
||||||
|
|
||||||
console.log("default-settings tests passed");
|
console.log("default-settings tests passed");
|
||||||
|
|||||||
@@ -231,6 +231,16 @@ function createBatchStageHarness() {
|
|||||||
getSchema: () => schema,
|
getSchema: () => schema,
|
||||||
getEmbeddingConfig: () => null,
|
getEmbeddingConfig: () => null,
|
||||||
getVectorIndexStats: () => ({ pending: 0 }),
|
getVectorIndexStats: () => ({ pending: 0 }),
|
||||||
|
analyzeAutoConsolidationGate: async () => ({
|
||||||
|
triggered: false,
|
||||||
|
reason: "本批新增少且无明显重复风险,跳过自动整合",
|
||||||
|
matchedScore: null,
|
||||||
|
matchedNodeId: "",
|
||||||
|
}),
|
||||||
|
inspectAutoCompressionCandidates: () => ({
|
||||||
|
hasCandidates: false,
|
||||||
|
reason: "已到自动压缩周期,但当前没有达到内部压缩阈值的候选组",
|
||||||
|
}),
|
||||||
updateLastExtractedItems: () => {},
|
updateLastExtractedItems: () => {},
|
||||||
ensureCurrentGraphRuntimeState: () => {},
|
ensureCurrentGraphRuntimeState: () => {},
|
||||||
throwIfAborted: () => {},
|
throwIfAborted: () => {},
|
||||||
@@ -2530,6 +2540,10 @@ async function testBatchStatusStructuralPartialRemainsRecoverable() {
|
|||||||
harness.currentGraph.historyState ||= {};
|
harness.currentGraph.historyState ||= {};
|
||||||
harness.currentGraph.vectorIndexState ||= {};
|
harness.currentGraph.vectorIndexState ||= {};
|
||||||
};
|
};
|
||||||
|
harness.inspectAutoCompressionCandidates = () => ({
|
||||||
|
hasCandidates: true,
|
||||||
|
reason: "",
|
||||||
|
});
|
||||||
harness.compressAll = async () => {
|
harness.compressAll = async () => {
|
||||||
throw new Error("compression down");
|
throw new Error("compression down");
|
||||||
};
|
};
|
||||||
@@ -2550,6 +2564,7 @@ async function testBatchStatusStructuralPartialRemainsRecoverable() {
|
|||||||
enableSynopsis: false,
|
enableSynopsis: false,
|
||||||
enableReflection: false,
|
enableReflection: false,
|
||||||
enableSleepCycle: false,
|
enableSleepCycle: false,
|
||||||
|
compressionEveryN: 1,
|
||||||
synopsisEveryN: 1,
|
synopsisEveryN: 1,
|
||||||
reflectEveryN: 1,
|
reflectEveryN: 1,
|
||||||
sleepEveryN: 1,
|
sleepEveryN: 1,
|
||||||
@@ -2614,6 +2629,333 @@ async function testBatchStatusSemanticFailureDoesNotHideCoreSuccess() {
|
|||||||
assert.match(effects.batchStatus.errors[0], /概要生成失败/);
|
assert.match(effects.batchStatus.errors[0], /概要生成失败/);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function testAutoConsolidationRunsOnHighDuplicateRiskSingleNode() {
|
||||||
|
const harness = await createBatchStageHarness();
|
||||||
|
const { createBatchStatusSkeleton, handleExtractionSuccess } = harness.result;
|
||||||
|
harness.currentGraph = {
|
||||||
|
historyState: { extractionCount: 0 },
|
||||||
|
vectorIndexState: {},
|
||||||
|
};
|
||||||
|
harness.ensureCurrentGraphRuntimeState = () => {
|
||||||
|
harness.currentGraph.historyState ||= {};
|
||||||
|
harness.currentGraph.vectorIndexState ||= {};
|
||||||
|
};
|
||||||
|
let gateCalls = 0;
|
||||||
|
let consolidateCalls = 0;
|
||||||
|
harness.analyzeAutoConsolidationGate = async () => {
|
||||||
|
gateCalls += 1;
|
||||||
|
return {
|
||||||
|
triggered: true,
|
||||||
|
reason:
|
||||||
|
"本批仅新增 1 个节点,但与旧记忆高度相似(0.930 >= 0.85),已触发自动整合",
|
||||||
|
matchedScore: 0.93,
|
||||||
|
matchedNodeId: "old-1",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
harness.consolidateMemories = async () => {
|
||||||
|
consolidateCalls += 1;
|
||||||
|
return {
|
||||||
|
merged: 1,
|
||||||
|
skipped: 0,
|
||||||
|
kept: 0,
|
||||||
|
evolved: 0,
|
||||||
|
connections: 0,
|
||||||
|
updates: 0,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
harness.syncVectorState = async () => ({
|
||||||
|
insertedHashes: [],
|
||||||
|
stats: { pending: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const batchStatus = createBatchStatusSkeleton({
|
||||||
|
processedRange: [6, 6],
|
||||||
|
extractionCountBefore: 0,
|
||||||
|
});
|
||||||
|
const effects = await handleExtractionSuccess(
|
||||||
|
{ newNodeIds: ["node-dup"] },
|
||||||
|
6,
|
||||||
|
{
|
||||||
|
enableConsolidation: true,
|
||||||
|
consolidationAutoMinNewNodes: 2,
|
||||||
|
consolidationThreshold: 0.85,
|
||||||
|
compressionEveryN: 0,
|
||||||
|
enableSynopsis: false,
|
||||||
|
enableReflection: false,
|
||||||
|
enableSleepCycle: false,
|
||||||
|
synopsisEveryN: 1,
|
||||||
|
reflectEveryN: 1,
|
||||||
|
sleepEveryN: 1,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
batchStatus,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(gateCalls, 1);
|
||||||
|
assert.equal(consolidateCalls, 1);
|
||||||
|
assert.equal(effects.batchStatus.consolidationGateTriggered, true);
|
||||||
|
assert.equal(
|
||||||
|
effects.batchStatus.consolidationGateMatchedNodeId,
|
||||||
|
"old-1",
|
||||||
|
);
|
||||||
|
assert.equal(effects.batchStatus.consolidationGateSimilarity, 0.93);
|
||||||
|
assert.match(
|
||||||
|
effects.batchStatus.consolidationGateReason,
|
||||||
|
/高度相似/,
|
||||||
|
);
|
||||||
|
assert.equal(effects.batchStatus.autoCompressionScheduled, false);
|
||||||
|
assert.match(
|
||||||
|
effects.batchStatus.autoCompressionSkippedReason,
|
||||||
|
/自动压缩已关闭/,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testAutoConsolidationSkipsLowRiskSingleNode() {
|
||||||
|
const harness = await createBatchStageHarness();
|
||||||
|
const { createBatchStatusSkeleton, handleExtractionSuccess } = harness.result;
|
||||||
|
harness.currentGraph = {
|
||||||
|
historyState: { extractionCount: 0 },
|
||||||
|
vectorIndexState: {},
|
||||||
|
};
|
||||||
|
harness.ensureCurrentGraphRuntimeState = () => {
|
||||||
|
harness.currentGraph.historyState ||= {};
|
||||||
|
harness.currentGraph.vectorIndexState ||= {};
|
||||||
|
};
|
||||||
|
let consolidateCalls = 0;
|
||||||
|
harness.analyzeAutoConsolidationGate = async () => ({
|
||||||
|
triggered: false,
|
||||||
|
reason:
|
||||||
|
"本批新增少且最高相似度 0.420 未达到阈值 0.85,跳过自动整合",
|
||||||
|
matchedScore: 0.42,
|
||||||
|
matchedNodeId: "old-2",
|
||||||
|
});
|
||||||
|
harness.consolidateMemories = async () => {
|
||||||
|
consolidateCalls += 1;
|
||||||
|
return {
|
||||||
|
merged: 0,
|
||||||
|
skipped: 0,
|
||||||
|
kept: 1,
|
||||||
|
evolved: 0,
|
||||||
|
connections: 0,
|
||||||
|
updates: 0,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
harness.syncVectorState = async () => ({
|
||||||
|
insertedHashes: [],
|
||||||
|
stats: { pending: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const batchStatus = createBatchStatusSkeleton({
|
||||||
|
processedRange: [7, 7],
|
||||||
|
extractionCountBefore: 0,
|
||||||
|
});
|
||||||
|
const effects = await handleExtractionSuccess(
|
||||||
|
{ newNodeIds: ["node-low-risk"] },
|
||||||
|
7,
|
||||||
|
{
|
||||||
|
enableConsolidation: true,
|
||||||
|
consolidationAutoMinNewNodes: 2,
|
||||||
|
consolidationThreshold: 0.85,
|
||||||
|
compressionEveryN: 0,
|
||||||
|
enableSynopsis: false,
|
||||||
|
enableReflection: false,
|
||||||
|
enableSleepCycle: false,
|
||||||
|
synopsisEveryN: 1,
|
||||||
|
reflectEveryN: 1,
|
||||||
|
sleepEveryN: 1,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
batchStatus,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(consolidateCalls, 0);
|
||||||
|
assert.equal(effects.batchStatus.consolidationGateTriggered, false);
|
||||||
|
assert.equal(
|
||||||
|
effects.batchStatus.consolidationGateMatchedNodeId,
|
||||||
|
"old-2",
|
||||||
|
);
|
||||||
|
assert.equal(effects.batchStatus.consolidationGateSimilarity, 0.42);
|
||||||
|
assert.match(
|
||||||
|
effects.batchStatus.consolidationGateReason,
|
||||||
|
/跳过自动整合/,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
effects.batchStatus.stages.structural.artifacts.includes(
|
||||||
|
"consolidation-skipped",
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testAutoCompressionRunsOnlyOnConfiguredInterval() {
|
||||||
|
const harness = await createBatchStageHarness();
|
||||||
|
const { createBatchStatusSkeleton, handleExtractionSuccess } = harness.result;
|
||||||
|
harness.currentGraph = {
|
||||||
|
historyState: { extractionCount: 9 },
|
||||||
|
vectorIndexState: {},
|
||||||
|
};
|
||||||
|
harness.ensureCurrentGraphRuntimeState = () => {
|
||||||
|
harness.currentGraph.historyState ||= {};
|
||||||
|
harness.currentGraph.vectorIndexState ||= {};
|
||||||
|
};
|
||||||
|
harness.extractionCount = 9;
|
||||||
|
let compressionCalls = 0;
|
||||||
|
harness.inspectAutoCompressionCandidates = () => ({
|
||||||
|
hasCandidates: true,
|
||||||
|
reason: "",
|
||||||
|
});
|
||||||
|
harness.compressAll = async () => {
|
||||||
|
compressionCalls += 1;
|
||||||
|
return { created: 1, archived: 2 };
|
||||||
|
};
|
||||||
|
harness.syncVectorState = async () => ({
|
||||||
|
insertedHashes: [],
|
||||||
|
stats: { pending: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const batchStatus = createBatchStatusSkeleton({
|
||||||
|
processedRange: [8, 8],
|
||||||
|
extractionCountBefore: 9,
|
||||||
|
});
|
||||||
|
const effects = await handleExtractionSuccess(
|
||||||
|
{ newNodeIds: ["node-for-compress"] },
|
||||||
|
8,
|
||||||
|
{
|
||||||
|
enableConsolidation: false,
|
||||||
|
compressionEveryN: 10,
|
||||||
|
enableSynopsis: false,
|
||||||
|
enableReflection: false,
|
||||||
|
enableSleepCycle: false,
|
||||||
|
synopsisEveryN: 1,
|
||||||
|
reflectEveryN: 1,
|
||||||
|
sleepEveryN: 1,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
batchStatus,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(compressionCalls, 1);
|
||||||
|
assert.equal(effects.batchStatus.autoCompressionScheduled, true);
|
||||||
|
assert.equal(effects.batchStatus.nextCompressionAtExtractionCount, 20);
|
||||||
|
assert.equal(effects.batchStatus.autoCompressionSkippedReason, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testAutoCompressionSkipsWhenNotScheduledOrNoCandidates() {
|
||||||
|
const offCycleHarness = await createBatchStageHarness();
|
||||||
|
const {
|
||||||
|
createBatchStatusSkeleton: createOffCycleBatchStatus,
|
||||||
|
handleExtractionSuccess: handleOffCycleExtractionSuccess,
|
||||||
|
} = offCycleHarness.result;
|
||||||
|
offCycleHarness.currentGraph = {
|
||||||
|
historyState: { extractionCount: 0 },
|
||||||
|
vectorIndexState: {},
|
||||||
|
};
|
||||||
|
offCycleHarness.ensureCurrentGraphRuntimeState = () => {
|
||||||
|
offCycleHarness.currentGraph.historyState ||= {};
|
||||||
|
offCycleHarness.currentGraph.vectorIndexState ||= {};
|
||||||
|
};
|
||||||
|
let offCycleCompressionCalls = 0;
|
||||||
|
offCycleHarness.compressAll = async () => {
|
||||||
|
offCycleCompressionCalls += 1;
|
||||||
|
return { created: 1, archived: 1 };
|
||||||
|
};
|
||||||
|
offCycleHarness.syncVectorState = async () => ({
|
||||||
|
insertedHashes: [],
|
||||||
|
stats: { pending: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const offCycleStatus = createOffCycleBatchStatus({
|
||||||
|
processedRange: [9, 9],
|
||||||
|
extractionCountBefore: 0,
|
||||||
|
});
|
||||||
|
const offCycleEffects = await handleOffCycleExtractionSuccess(
|
||||||
|
{ newNodeIds: ["node-off-cycle"] },
|
||||||
|
9,
|
||||||
|
{
|
||||||
|
enableConsolidation: false,
|
||||||
|
compressionEveryN: 10,
|
||||||
|
enableSynopsis: false,
|
||||||
|
enableReflection: false,
|
||||||
|
enableSleepCycle: false,
|
||||||
|
synopsisEveryN: 1,
|
||||||
|
reflectEveryN: 1,
|
||||||
|
sleepEveryN: 1,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
offCycleStatus,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(offCycleCompressionCalls, 0);
|
||||||
|
assert.equal(offCycleEffects.batchStatus.autoCompressionScheduled, false);
|
||||||
|
assert.match(
|
||||||
|
offCycleEffects.batchStatus.autoCompressionSkippedReason,
|
||||||
|
/未到每 10 次自动压缩周期/,
|
||||||
|
);
|
||||||
|
assert.equal(offCycleEffects.batchStatus.nextCompressionAtExtractionCount, 10);
|
||||||
|
|
||||||
|
const scheduledHarness = await createBatchStageHarness();
|
||||||
|
const {
|
||||||
|
createBatchStatusSkeleton: createScheduledBatchStatus,
|
||||||
|
handleExtractionSuccess: handleScheduledExtractionSuccess,
|
||||||
|
} = scheduledHarness.result;
|
||||||
|
scheduledHarness.currentGraph = {
|
||||||
|
historyState: { extractionCount: 9 },
|
||||||
|
vectorIndexState: {},
|
||||||
|
};
|
||||||
|
scheduledHarness.ensureCurrentGraphRuntimeState = () => {
|
||||||
|
scheduledHarness.currentGraph.historyState ||= {};
|
||||||
|
scheduledHarness.currentGraph.vectorIndexState ||= {};
|
||||||
|
};
|
||||||
|
scheduledHarness.extractionCount = 9;
|
||||||
|
let scheduledCompressionCalls = 0;
|
||||||
|
scheduledHarness.inspectAutoCompressionCandidates = () => ({
|
||||||
|
hasCandidates: false,
|
||||||
|
reason: "已到自动压缩周期,但当前没有达到内部压缩阈值的候选组",
|
||||||
|
});
|
||||||
|
scheduledHarness.compressAll = async () => {
|
||||||
|
scheduledCompressionCalls += 1;
|
||||||
|
return { created: 1, archived: 1 };
|
||||||
|
};
|
||||||
|
scheduledHarness.syncVectorState = async () => ({
|
||||||
|
insertedHashes: [],
|
||||||
|
stats: { pending: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const scheduledStatus = createScheduledBatchStatus({
|
||||||
|
processedRange: [10, 10],
|
||||||
|
extractionCountBefore: 9,
|
||||||
|
});
|
||||||
|
const scheduledEffects = await handleScheduledExtractionSuccess(
|
||||||
|
{ newNodeIds: ["node-scheduled"] },
|
||||||
|
10,
|
||||||
|
{
|
||||||
|
enableConsolidation: false,
|
||||||
|
compressionEveryN: 10,
|
||||||
|
enableSynopsis: false,
|
||||||
|
enableReflection: false,
|
||||||
|
enableSleepCycle: false,
|
||||||
|
synopsisEveryN: 1,
|
||||||
|
reflectEveryN: 1,
|
||||||
|
sleepEveryN: 1,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
scheduledStatus,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(scheduledCompressionCalls, 0);
|
||||||
|
assert.equal(scheduledEffects.batchStatus.autoCompressionScheduled, true);
|
||||||
|
assert.match(
|
||||||
|
scheduledEffects.batchStatus.autoCompressionSkippedReason,
|
||||||
|
/没有达到内部压缩阈值的候选组/,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
scheduledEffects.batchStatus.stages.structural.artifacts.includes(
|
||||||
|
"compression-skipped",
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function testBatchStatusFinalizeFailureIsNotCompleteSuccess() {
|
async function testBatchStatusFinalizeFailureIsNotCompleteSuccess() {
|
||||||
const harness = await createBatchStageHarness();
|
const harness = await createBatchStageHarness();
|
||||||
const { createBatchStatusSkeleton, handleExtractionSuccess } = harness.result;
|
const { createBatchStatusSkeleton, handleExtractionSuccess } = harness.result;
|
||||||
@@ -4685,6 +5027,10 @@ await testReverseJournalRollbackStateFormsReplayClosure();
|
|||||||
await testReverseJournalRecoveryPlanMixedLegacyAndCurrentRetainsRepairSet();
|
await testReverseJournalRecoveryPlanMixedLegacyAndCurrentRetainsRepairSet();
|
||||||
await testBatchStatusStructuralPartialRemainsRecoverable();
|
await testBatchStatusStructuralPartialRemainsRecoverable();
|
||||||
await testBatchStatusSemanticFailureDoesNotHideCoreSuccess();
|
await testBatchStatusSemanticFailureDoesNotHideCoreSuccess();
|
||||||
|
await testAutoConsolidationRunsOnHighDuplicateRiskSingleNode();
|
||||||
|
await testAutoConsolidationSkipsLowRiskSingleNode();
|
||||||
|
await testAutoCompressionRunsOnlyOnConfiguredInterval();
|
||||||
|
await testAutoCompressionSkipsWhenNotScheduledOrNoCandidates();
|
||||||
await testBatchStatusFinalizeFailureIsNotCompleteSuccess();
|
await testBatchStatusFinalizeFailureIsNotCompleteSuccess();
|
||||||
await testProcessedHistoryAdvanceTracksCoreExtractionSuccess();
|
await testProcessedHistoryAdvanceTracksCoreExtractionSuccess();
|
||||||
await testGenerationRecallTransactionDedupesDoubleHookBySameKey();
|
await testGenerationRecallTransactionDedupesDoubleHookBySameKey();
|
||||||
|
|||||||
Reference in New Issue
Block a user