Merge branch 'Youzini-afk:main' into main

This commit is contained in:
Hao19911125
2026-04-06 21:31:44 +08:00
committed by GitHub
7 changed files with 314 additions and 41 deletions

View File

@@ -1,5 +1,3 @@
import { debugWarn } from "./debug-logging.js";
function getTimerApi(runtime) {
const rawSetTimeout =
typeof runtime?.setTimeout === "function"
@@ -40,14 +38,8 @@ export function registerBeforeCombinePromptsController(runtime, listener) {
export function registerGenerationAfterCommandsController(runtime, listener) {
const makeFirst = runtime.getEventMakeFirst();
const eventName = runtime.eventTypes.GENERATION_AFTER_COMMANDS;
debugWarn("[ST-BME:DIAG] Registering GENERATION_AFTER_COMMANDS:", {
eventName,
hasMakeFirst: typeof makeFirst === "function",
hasListener: typeof listener === "function",
});
if (typeof makeFirst === "function") {
const cleanup = makeFirst(eventName, listener);
debugWarn("[ST-BME:DIAG] Registered via makeFirst, cleanup:", typeof cleanup);
return cleanup;
}
@@ -261,6 +253,10 @@ export function onMessageSentController(runtime, messageId) {
resolvedMessageId,
message.mes || "",
);
// GENERATION_AFTER_COMMANDS 在 sendMessageAsUser 之前触发,此时新用户消息
// 尚未进入 chatrecall 记录会被写到上一条 user 上。这里用户消息刚入场,
// transaction 仍在桥接窗口内,立即把记录重新绑定到正确的楼层。
runtime.rebindRecallRecordToNewUserMessage?.(resolvedMessageId);
runtime.refreshPersistedRecallMessageUi?.();
}
@@ -408,9 +404,7 @@ export async function onGenerationAfterCommandsController(
params = {},
dryRun = false,
) {
debugWarn("[ST-BME:DIAG] GENERATION_AFTER_COMMANDS fired", { type, dryRun, paramsKeys: Object.keys(params || {}) });
if (dryRun) {
debugWarn("[ST-BME:DIAG] EXIT: dryRun=true");
return;
}
@@ -420,11 +414,9 @@ export async function onGenerationAfterCommandsController(
? runtime.consumeHostGenerationInputSnapshot?.({ preserve: true }) ||
runtime.consumeHostGenerationInputSnapshot?.()
: null;
debugWarn("[ST-BME:DIAG] frozenInputSnapshot:", frozenInputSnapshot?.text ? `"${frozenInputSnapshot.text.slice(0,50)}"` : "(empty)", "fresh:", !!frozenInputSnapshot?.at);
const context = runtime.getContext();
const chat = context?.chat;
debugWarn("[ST-BME:DIAG] chat length:", chat?.length, "last msg:", chat?.length ? { is_user: chat[chat.length-1]?.is_user, mes: (chat[chat.length-1]?.mes||"").slice(0,50) } : "(no chat)");
const recallOptions = runtime.buildGenerationAfterCommandsRecallInput(
type,
@@ -435,14 +427,11 @@ export async function onGenerationAfterCommandsController(
chat,
);
if (!recallOptions) {
debugWarn("[ST-BME:DIAG] EXIT: buildGenerationAfterCommandsRecallInput returned null");
return;
}
if (recallOptions?.__trivialSkip) {
debugWarn("[ST-BME:DIAG] EXIT: trivial-input-skip");
return;
}
debugWarn("[ST-BME:DIAG] recallOptions:", { generationType: recallOptions.generationType, overrideUserMessage: recallOptions.overrideUserMessage?.slice(0,50), overrideSource: recallOptions.overrideSource, targetIdx: recallOptions.targetUserMessageIndex });
const recallContext = runtime.createGenerationRecallContext({
hookName: "GENERATION_AFTER_COMMANDS",
@@ -450,10 +439,8 @@ export async function onGenerationAfterCommandsController(
recallOptions,
});
if (!recallContext.shouldRun && !recallContext.transaction) {
debugWarn("[ST-BME:DIAG] EXIT: shouldRun=false, no transaction. guardReason:", recallContext.guardReason);
return;
}
debugWarn("[ST-BME:DIAG] recallContext:", { shouldRun: recallContext.shouldRun, guardReason: recallContext.guardReason, transactionId: recallContext.transaction?.id });
const runtimeRecallOptions =
recallContext.recallOptions || recallOptions || {};
@@ -466,7 +453,6 @@ export async function onGenerationAfterCommandsController(
let recallResult = runtime.getGenerationRecallTransactionResult?.(
recallContext.transaction,
);
debugWarn("[ST-BME:DIAG] deliveryMode:", deliveryMode, "shouldRun:", recallContext.shouldRun);
if (recallContext.shouldRun) {
runtime.markGenerationRecallTransactionHookState(
@@ -477,7 +463,6 @@ export async function onGenerationAfterCommandsController(
if (deliveryMode === "deferred") {
runtime.clearLiveRecallInjectionPromptForRewrite?.();
}
debugWarn("[ST-BME:DIAG] >>> Starting runRecall...");
recallResult = await runtime.runRecall({
...runtimeRecallOptions,
deliveryMode,
@@ -485,7 +470,6 @@ export async function onGenerationAfterCommandsController(
hookName: recallContext.hookName,
signal: params?.signal,
});
debugWarn("[ST-BME:DIAG] <<< runRecall finished:", { status: recallResult?.status, ok: recallResult?.ok, reason: recallResult?.reason, injectionText: recallResult?.injectionText?.slice(0,80) });
runtime.storeGenerationRecallTransactionResult?.(
recallContext.transaction,
recallResult,
@@ -518,7 +502,6 @@ export async function onGenerationAfterCommandsController(
// 上面的兜底补写会把 fresh recall 绑定回最终 user 楼层。
// 这里再补一次 UI 刷新,避免需要等到消息编辑/历史恢复后才看到 Recall Card。
runtime.refreshPersistedRecallMessageUi?.();
debugWarn("[ST-BME:DIAG] DONE: immediate mode, injection via setExtensionPrompt in runRecall");
return recallResult;
}
@@ -552,7 +535,6 @@ export async function onBeforeCombinePromptsController(
frozenInputSnapshot,
});
if (normalInput?.__trivialSkip) {
debugWarn("[ST-BME:DIAG] EXIT: trivial-input-skip");
return {
skipped: true,
reason: `trivial:${normalInput.trivialReason || ""}`,

View File

@@ -95,6 +95,87 @@ function normalizeKeyForPartition(value) {
return String(value ?? '').trim().toLowerCase();
}
/**
* 宿主别名与 POV owner 比对:忽略大小写、多空格、常见中英文标点/符号差NFKC
* 不用于 charMap 主键,仅用于「是否同一用户」的宽松匹配。
*/
function normalizeAliasMatchKey(value) {
let s = String(value ?? '');
if (typeof s.normalize === 'function') {
try {
s = s.normalize('NFKC');
} catch {
/* ignore */
}
}
s = s.trim().toLowerCase();
// 标点、间隔号、各类空白等统一成空格,再压成单空格
s = s.replace(
/[\s!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~\u00b7\u3000-\u303f\uff01-\uff0f\uff1a-\uff20\uff3b-\uff40\uff5b-\uff65\u2000-\u206f\u2e00-\u2e7f]+/g,
' ',
);
s = s.replace(/\s+/g, ' ').trim();
return s;
}
/** 同一名称的多种可比形式(兼容老数据只做了 trim+lower */
function collectAliasMatchVariants(raw) {
const variants = [];
const leg = normalizeKeyForPartition(raw);
if (leg) variants.push(leg);
const soft = normalizeAliasMatchKey(raw);
if (soft) {
variants.push(soft);
const compact = soft.replace(/\s/g, '');
if (compact && compact !== soft) variants.push(compact);
}
return variants;
}
function addAliasMatchVariantsToSet(set, raw) {
for (const k of collectAliasMatchVariants(raw)) {
if (k) set.add(k);
}
}
/**
* 将宿主侧「用户显示名」候选归一为分区用 Set用于把误标为 character 的用户 POV 拉回用户区。
* @param {string|string[]|{name1?:string,userName?:string,personaName?:string,aliases?:string[]}|null|undefined} hints
* @returns {Set<string>}
*/
export function buildUserPovAliasNormalizedSet(hints) {
const set = new Set();
if (hints == null) return set;
const ingest = (v) => addAliasMatchVariantsToSet(set, v);
if (typeof hints === 'string') {
ingest(hints);
return set;
}
if (Array.isArray(hints)) {
for (const item of hints) ingest(item);
return set;
}
if (typeof hints === 'object') {
ingest(hints.name1);
ingest(hints.userName);
ingest(hints.personaName);
if (Array.isArray(hints.aliases)) {
for (const a of hints.aliases) ingest(a);
}
}
return set;
}
function scopeMatchesHostUserAliases(scope, aliasSet) {
if (!(aliasSet instanceof Set) || aliasSet.size === 0) return false;
for (const field of [scope.ownerName, scope.ownerId]) {
for (const k of collectAliasMatchVariants(field)) {
if (k && aliasSet.has(k)) return true;
}
}
return false;
}
function characterPovLabelFromNodes(arr) {
if (!arr?.length) return '·';
for (const n of arr) {
@@ -108,10 +189,12 @@ function characterPovLabelFromNodes(arr) {
return '·';
}
function partitionNodesByScope(nodes) {
function partitionNodesByScope(nodes, userPovAliasSet = null) {
const objective = [];
const userPov = [];
const charMap = new Map();
const aliasSet =
userPovAliasSet instanceof Set ? userPovAliasSet : new Set();
for (const node of nodes) {
const scope = normalizeMemoryScope(node.raw?.scope);
@@ -120,6 +203,12 @@ function partitionNodesByScope(nodes) {
node.regionKey = 'objective';
continue;
}
// 优先:宿主用户显示名与 ownerName/ownerId 一致时一律归用户 POV修正提取阶段误标 character
if (scopeMatchesHostUserAliases(scope, aliasSet)) {
userPov.push(node);
node.regionKey = 'user';
continue;
}
if (scope.ownerType === 'user') {
userPov.push(node);
node.regionKey = 'user';
@@ -166,6 +255,9 @@ export class GraphRenderer {
this.colors = getNodeColors(themeName);
this.themeName = themeName;
this.config = { ...DEFAULT_LAYOUT_CONFIG, ...fromForce, ...layoutOverride };
this._userPovAliasSet = buildUserPovAliasNormalizedSet(
isLegacy ? null : options?.userPovAliases,
);
this._regionPanels = [];
this._lastGraph = null;
@@ -200,10 +292,19 @@ export class GraphRenderer {
* 加载图谱数据
* @param {object} graph - 完整的 graph state
*/
loadGraph(graph) {
/**
* @param {object} graph
* @param {{ userPovAliases?: string|string[]|object }} [layoutHints]
*/
loadGraph(graph, layoutHints = {}) {
const prevSelectedId = this.selectedNode?.id || null;
this.nodeMap.clear();
this._lastGraph = graph;
if (layoutHints && Object.prototype.hasOwnProperty.call(layoutHints, 'userPovAliases')) {
this._userPovAliasSet = buildUserPovAliasNormalizedSet(
layoutHints.userPovAliases,
);
}
const dpr = window.devicePixelRatio || 1;
const W = this.canvas.width / dpr;
@@ -239,7 +340,7 @@ export class GraphRenderer {
relation: e.relation || 'related',
}));
const parts = partitionNodesByScope(this.nodes);
const parts = partitionNodesByScope(this.nodes, this._userPovAliasSet);
this._regionPanels = this._computeRegionPanels(W, H, parts);
this._layoutAllPartitions(parts);
this._simulateNeuralWithinRegions(this.config.neuralIterations);

View File

@@ -1543,6 +1543,53 @@ function doesChatUserMessageMatchRecallCandidates(message, candidateHashes) {
return candidateHashes.has(hashRecallInput(normalizedMessage));
}
function rebindRecallRecordToNewUserMessage(newUserMessageIndex) {
const chat = getContext()?.chat;
if (
!Array.isArray(chat) ||
!Number.isFinite(newUserMessageIndex) ||
!chat[newUserMessageIndex]?.is_user
) {
return;
}
if (readPersistedRecallFromUserMessage(chat, newUserMessageIndex)) {
return;
}
const recentTransaction = findRecentGenerationRecallTransactionForChat();
const recallResult = getGenerationRecallTransactionResult(recentTransaction);
if (
!recallResult ||
recallResult.status !== "completed" ||
!recallResult.didRecall ||
!String(recallResult.injectionText || "").trim()
) {
return;
}
const record = buildPersistedRecallRecord(
{
injectionText: String(recallResult.injectionText || "").trim(),
selectedNodeIds: recallResult.selectedNodeIds || [],
recallInput: String(
recallResult.recallInput || recallResult.userMessage || "",
),
recallSource: String(recallResult.source || ""),
hookName: String(
recallResult.hookName ||
recentTransaction?.lastRecallMeta?.hookName ||
"",
),
tokenEstimate: estimateTokens(
String(recallResult.injectionText || "").trim(),
),
manuallyEdited: false,
},
null,
);
if (writePersistedRecallToUserMessage(chat, newUserMessageIndex, record)) {
triggerChatMetadataSave(getContext(), { immediate: false });
}
}
function resolveRecallPersistenceTargetUserMessageIndex(
chat,
{
@@ -7638,12 +7685,27 @@ async function handleExtractionSuccess(
typeof recordMaintenanceAction === "function"
? recordMaintenanceAction
: () => null;
const updateExtractionPostProcessStatus = (
text,
meta,
{ noticeMarquee = false } = {},
) => {
if (typeof setLastExtractionStatus !== "function") return;
setLastExtractionStatus(text, meta, "running", {
syncRuntime: true,
noticeMarquee,
});
};
throwIfAborted(signal, "提取已终止");
extractionCount++;
ensureCurrentGraphRuntimeState();
currentGraph.historyState.extractionCount = extractionCount;
updateLastExtractedItems(result.newNodeIds || []);
setBatchStageOutcome(status, "core", "success");
updateExtractionPostProcessStatus(
"提取收尾中",
`已抽取 ${newNodeCount} 个新节点,正在处理后续阶段`,
);
if (settings.enableConsolidation && result.newNodeIds?.length > 0) {
let consolidationAnalysis = null;
@@ -7655,6 +7717,10 @@ async function handleExtractionSuccess(
),
);
if (newNodeCount < minNewNodes) {
updateExtractionPostProcessStatus(
"整合判定中",
`本批新增 ${newNodeCount} 个节点,正在检查是否需要自动整合/进化`,
);
consolidationAnalysis = await analyzeConsolidationGate({
graph: currentGraph,
newNodeIds: result.newNodeIds,
@@ -7682,6 +7748,10 @@ async function handleExtractionSuccess(
pushBatchStageArtifact(status, "structural", "consolidation-skipped");
} else {
try {
updateExtractionPostProcessStatus(
"整合/进化中",
String(gate.reason || "").trim() || "正在自动整合新旧记忆",
);
const beforeSnapshot = cloneMaintenanceSnapshot(currentGraph);
const consolidationResult = await consolidateMemories({
graph: currentGraph,
@@ -7725,6 +7795,10 @@ async function handleExtractionSuccess(
extractionCount % settings.synopsisEveryN === 0
) {
try {
updateExtractionPostProcessStatus(
"概要更新中",
`${extractionCount} 次提取,正在生成全局概要`,
);
await generateSynopsis({
graph: currentGraph,
schema: getSchema(),
@@ -7752,6 +7826,10 @@ async function handleExtractionSuccess(
extractionCount % settings.reflectEveryN === 0
) {
try {
updateExtractionPostProcessStatus(
"反思生成中",
`${extractionCount} 次提取,正在生成长期反思`,
);
await generateReflection({
graph: currentGraph,
currentSeq: endIdx,
@@ -7778,6 +7856,10 @@ async function handleExtractionSuccess(
extractionCount % settings.sleepEveryN === 0
) {
try {
updateExtractionPostProcessStatus(
"主动遗忘中",
`${extractionCount} 次提取,正在归档低价值记忆`,
);
const beforeSnapshot = cloneMaintenanceSnapshot(currentGraph);
const sleepResult = sleepCycle(currentGraph, settings);
if ((sleepResult?.forgotten || 0) > 0) {
@@ -7825,6 +7907,10 @@ async function handleExtractionSuccess(
"已到自动压缩周期,但当前没有达到内部压缩阈值的候选组";
pushBatchStageArtifact(status, "structural", "compression-skipped");
} else {
updateExtractionPostProcessStatus(
"自动压缩中",
`已到第 ${extractionCount} 次提取周期,正在压缩层级记忆`,
);
status.autoCompressionSkippedReason = "";
const beforeSnapshot = cloneMaintenanceSnapshot(currentGraph);
const compressionResult = await compressAll(
@@ -7869,6 +7955,10 @@ async function handleExtractionSuccess(
let vectorSync = null;
try {
updateExtractionPostProcessStatus(
"向量同步中",
"正在同步本批提取后的向量索引",
);
vectorSync = await syncVectorState({ signal });
} catch (error) {
if (isAbortError(error)) throw error;
@@ -9235,6 +9325,7 @@ function onMessageSent(messageId) {
getContext,
isTrivialUserInput,
recordRecallSentUserMessage,
rebindRecallRecordToNewUserMessage,
refreshPersistedRecallMessageUi: schedulePersistedRecallMessageUiRefresh,
},
messageId,

View File

@@ -1,6 +1,7 @@
// ST-BME: 操控面板交互逻辑
import { callGenericPopup, POPUP_TYPE } from "../../../popup.js";
import { getContext } from "../../../extensions.js";
import { renderTemplateAsync } from "../../../templates.js";
import { GraphRenderer } from "./graph-renderer.js";
import { getNodeDisplayName } from "./node-labels.js";
@@ -672,15 +673,19 @@ export function openPanel() {
const settings = _getSettings?.() || {};
const themeName = settings.panelTheme || "crimson";
const graphOpts = {
theme: themeName,
userPovAliases: _hostUserPovAliasHintsForGraph(),
};
const canvas = document.getElementById("bme-graph-canvas");
if (canvas && !graphRenderer && !isMobile) {
graphRenderer = new GraphRenderer(canvas, themeName);
graphRenderer = new GraphRenderer(canvas, graphOpts);
graphRenderer.onNodeSelect = (node) => _showNodeDetail(node);
}
const mobileCanvas = document.getElementById("bme-mobile-graph-canvas");
if (mobileCanvas && !mobileGraphRenderer && isMobile) {
mobileGraphRenderer = new GraphRenderer(mobileCanvas, themeName);
mobileGraphRenderer = new GraphRenderer(mobileCanvas, graphOpts);
mobileGraphRenderer.onNodeSelect = (node) => _showNodeDetail(node);
}
@@ -1203,11 +1208,26 @@ async function _refreshInjectionPreview() {
// ==================== 图谱 ====================
/** SillyTavern 用户显示名name1用于图谱分区误标为角色的用户 POV 强制归用户区 */
function _hostUserPovAliasHintsForGraph() {
try {
const ctx = typeof getContext === "function" ? getContext() : null;
const out = [];
if (ctx?.name1 && String(ctx.name1).trim()) {
out.push(String(ctx.name1).trim());
}
return out;
} catch {
return [];
}
}
function _refreshGraph() {
const graph = _getGraph?.();
if (!graph) return;
graphRenderer?.loadGraph(graph);
mobileGraphRenderer?.loadGraph(graph);
const hints = { userPovAliases: _hostUserPovAliasHintsForGraph() };
graphRenderer?.loadGraph(graph, hints);
mobileGraphRenderer?.loadGraph(graph, hints);
}
function _buildLegend() {

View File

@@ -1,6 +1,6 @@
// ST-BME: 召回输入解析与注入控制器(纯函数)
import { debugLog, debugWarn } from "./debug-logging.js";
import { debugLog } from "./debug-logging.js";
export function buildRecallRecentMessagesController(
chat,
@@ -289,12 +289,10 @@ export function applyRecallInjectionController(
}
export async function runRecallController(runtime, options = {}) {
debugWarn("[ST-BME:DIAG:RECALL] runRecallController entered");
if (runtime.getIsRecalling()) {
runtime.abortRecallStageWithReason("旧召回已取消,正在启动新的召回");
const settle = await runtime.waitForActiveRecallToSettle();
if (!settle.settled && runtime.getIsRecalling()) {
debugWarn("[ST-BME:DIAG:RECALL] EXIT: 上一轮召回仍在清理");
runtime.setLastRecallStatus(
"召回忙",
"上一轮召回仍在清理,请稍后重试",
@@ -310,18 +308,14 @@ export async function runRecallController(runtime, options = {}) {
}
const hasGraph = !!runtime.getCurrentGraph();
debugWarn("[ST-BME:DIAG:RECALL] hasGraph:", hasGraph);
if (!hasGraph) {
debugWarn("[ST-BME:DIAG:RECALL] EXIT: 当前无图谱");
return runtime.createRecallRunResult("skipped", {
reason: "当前无图谱",
});
}
const settings = runtime.getSettings();
debugWarn("[ST-BME:DIAG:RECALL] settings.enabled:", settings.enabled, "recallEnabled:", settings.recallEnabled);
if (!settings.enabled || !settings.recallEnabled) {
debugWarn("[ST-BME:DIAG:RECALL] EXIT: 召回功能未启用");
return runtime.createRecallRunResult("skipped", {
reason: "召回功能未启用",
});
@@ -330,12 +324,8 @@ export async function runRecallController(runtime, options = {}) {
typeof runtime.isGraphReadableForRecall === "function"
? runtime.isGraphReadableForRecall()
: runtime.isGraphReadable();
const chatId = typeof runtime.getCurrentChatId === "function" ? runtime.getCurrentChatId() : "(no fn)";
const loadState = runtime.getGraphPersistenceLoadState?.() || "(no fn)";
debugWarn("[ST-BME:DIAG:RECALL] isReadableForRecall:", isReadableForRecall, "chatId:", chatId, "loadState:", loadState);
if (!isReadableForRecall) {
const reason = runtime.getGraphMutationBlockReason("召回");
debugWarn("[ST-BME:DIAG:RECALL] EXIT: 图谱不可读 -", reason);
runtime.setLastRecallStatus("等待图谱加载", reason, "warning", {
syncRuntime: true,
});

View File

@@ -1,8 +1,22 @@
// ST-BME: 消息级召回卡片 UI
// 纯 DOM 构建模块,不含模块级 mutable state
import { getContext } from "../../../extensions.js";
import { GraphRenderer } from "./graph-renderer.js";
function _hostUserPovAliasHintsForRecallCanvas() {
try {
const ctx = typeof getContext === "function" ? getContext() : null;
const out = [];
if (ctx?.name1 && String(ctx.name1).trim()) {
out.push(String(ctx.name1).trim());
}
return out;
} catch {
return [];
}
}
// ==================== 常量 ====================
export const RECALL_CARD_FORCE_CONFIG = {
@@ -297,6 +311,7 @@ export function createRecallCardElement({
renderer = new GraphRenderer(canvas, {
theme: themeName,
forceConfig: RECALL_CARD_FORCE_CONFIG,
userPovAliases: _hostUserPovAliasHintsForRecallCanvas(),
onNodeClick: (node) => {
if (typeof activeCallbacks.onNodeClick === "function") {
activeCallbacks.onNodeClick(messageIndex, node);
@@ -308,7 +323,9 @@ export function createRecallCardElement({
}
},
});
renderer.loadGraph(resolvedSubGraph);
renderer.loadGraph(resolvedSubGraph, {
userPovAliases: _hostUserPovAliasHintsForRecallCanvas(),
});
}
// 元信息行

View File

@@ -236,6 +236,7 @@ function createBatchStageHarness() {
result: null,
extractionCount: 0,
currentGraph: null,
extractionStatuses: [],
consolidateMemories: async () => {},
generateSynopsis: async () => {},
generateReflection: async () => {},
@@ -271,6 +272,9 @@ function createBatchStageHarness() {
pushBatchStageArtifact,
finalizeBatchStatus,
createUiStatus,
setLastExtractionStatus(...args) {
context.extractionStatuses.push(args);
},
};
vm.createContext(context);
vm.runInContext(
@@ -2604,6 +2608,73 @@ async function testBatchStatusSemanticFailureDoesNotHideCoreSuccess() {
assert.match(effects.batchStatus.errors[0], /概要生成失败/);
}
async function testExtractionPostProcessStatusesExposeMaintenancePhases() {
const harness = await createBatchStageHarness();
const { createBatchStatusSkeleton, handleExtractionSuccess } = harness.result;
harness.currentGraph = {
historyState: { extractionCount: 0 },
vectorIndexState: {},
};
harness.ensureCurrentGraphRuntimeState = () => {
harness.currentGraph.historyState ||= {};
harness.currentGraph.vectorIndexState ||= {};
};
harness.consolidateMemories = async () => ({
merged: 1,
skipped: 0,
kept: 0,
evolved: 1,
connections: 0,
updates: 0,
});
harness.generateSynopsis = async () => ({ ok: true });
harness.generateReflection = async () => ({ ok: true });
harness.sleepCycle = () => ({ forgotten: 0 });
harness.inspectAutoCompressionCandidates = () => ({
hasCandidates: true,
reason: "",
});
harness.compressAll = async () => ({ created: 1, archived: 2 });
harness.syncVectorState = async () => ({
insertedHashes: ["hash-stage"],
stats: { pending: 0, indexed: 3 },
});
const batchStatus = createBatchStatusSkeleton({
processedRange: [8, 8],
extractionCountBefore: 0,
});
await handleExtractionSuccess(
{
newNodeIds: ["node-stage"],
},
8,
{
enableConsolidation: true,
consolidationAutoMinNewNodes: 1,
enableSynopsis: true,
synopsisEveryN: 1,
enableReflection: true,
reflectEveryN: 1,
enableSleepCycle: true,
sleepEveryN: 1,
enableAutoCompression: true,
compressionEveryN: 1,
},
undefined,
batchStatus,
);
const statusTexts = harness.extractionStatuses.map((entry) => entry[0]);
assert.ok(statusTexts.includes("提取收尾中"));
assert.ok(statusTexts.includes("整合/进化中"));
assert.ok(statusTexts.includes("概要更新中"));
assert.ok(statusTexts.includes("反思生成中"));
assert.ok(statusTexts.includes("主动遗忘中"));
assert.ok(statusTexts.includes("自动压缩中"));
assert.ok(statusTexts.includes("向量同步中"));
}
async function testAutoConsolidationRunsOnHighDuplicateRiskSingleNode() {
const harness = await createBatchStageHarness();
const { createBatchStatusSkeleton, handleExtractionSuccess } = harness.result;
@@ -5474,6 +5545,7 @@ await testReverseJournalRollbackStateFormsReplayClosure();
await testReverseJournalRecoveryPlanMixedLegacyAndCurrentRetainsRepairSet();
await testBatchStatusStructuralPartialRemainsRecoverable();
await testBatchStatusSemanticFailureDoesNotHideCoreSuccess();
await testExtractionPostProcessStatusesExposeMaintenancePhases();
await testAutoConsolidationRunsOnHighDuplicateRiskSingleNode();
await testAutoConsolidationSkipsLowRiskSingleNode();
await testAutoCompressionRunsOnlyOnConfiguredInterval();