mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
fix(history): pause recovery for render-limited chat slices
This commit is contained in:
127
index.js
127
index.js
@@ -5060,6 +5060,102 @@ function applyMessageRenderLimit(settings = null, options = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getActiveMessageRenderLimitForHistoryGuard(settings = null) {
|
||||||
|
const normalized = getMessageRenderLimitSettings(settings);
|
||||||
|
const configuredLimit =
|
||||||
|
normalized.enabled && normalized.render_last_n > 0
|
||||||
|
? normalized.render_last_n
|
||||||
|
: 0;
|
||||||
|
let hostLimit = 0;
|
||||||
|
try {
|
||||||
|
const powerUserSettings = getHostPowerUserSettings();
|
||||||
|
hostLimit = Math.max(
|
||||||
|
0,
|
||||||
|
Math.trunc(Number(powerUserSettings?.chat_truncation ?? 0) || 0),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
hostLimit = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuredLimit > 0 && hostLimit > 0) {
|
||||||
|
return Math.min(configuredLimit, hostLimit);
|
||||||
|
}
|
||||||
|
return Math.max(configuredLimit, hostLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHighestTrackedProcessedHistoryFloor(historyState = {}) {
|
||||||
|
const lastProcessed = Number.isFinite(
|
||||||
|
Number(historyState?.lastProcessedAssistantFloor),
|
||||||
|
)
|
||||||
|
? Math.floor(Number(historyState.lastProcessedAssistantFloor))
|
||||||
|
: -1;
|
||||||
|
const hashes =
|
||||||
|
historyState?.processedMessageHashes &&
|
||||||
|
typeof historyState.processedMessageHashes === "object" &&
|
||||||
|
!Array.isArray(historyState.processedMessageHashes)
|
||||||
|
? historyState.processedMessageHashes
|
||||||
|
: {};
|
||||||
|
const maxHashFloor = Object.keys(hashes).reduce((maxFloor, key) => {
|
||||||
|
const floor = Number.parseInt(key, 10);
|
||||||
|
return Number.isFinite(floor) ? Math.max(maxFloor, floor) : maxFloor;
|
||||||
|
}, -1);
|
||||||
|
|
||||||
|
return Math.max(lastProcessed, maxHashFloor);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRenderLimitedHistoryRecoveryGuard(
|
||||||
|
chat,
|
||||||
|
{ settings = null, historyState = currentGraph?.historyState } = {},
|
||||||
|
) {
|
||||||
|
const renderLimit = getActiveMessageRenderLimitForHistoryGuard(settings);
|
||||||
|
if (!Array.isArray(chat) || renderLimit <= 0) {
|
||||||
|
return { blocked: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const chatLength = chat.length;
|
||||||
|
const highestProcessedFloor =
|
||||||
|
getHighestTrackedProcessedHistoryFloor(historyState);
|
||||||
|
const renderWindowTolerance = renderLimit + 1;
|
||||||
|
if (
|
||||||
|
chatLength > renderWindowTolerance ||
|
||||||
|
highestProcessedFloor < chatLength
|
||||||
|
) {
|
||||||
|
return { blocked: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
blocked: true,
|
||||||
|
chatLength,
|
||||||
|
highestProcessedFloor,
|
||||||
|
renderLimit,
|
||||||
|
reason: "render-limited-chat-slice",
|
||||||
|
message:
|
||||||
|
`当前聊天区最多只渲染最近 ${renderLimit} 条消息,当前可见 ${chatLength} 条;` +
|
||||||
|
`图谱已处理到楼层 ${highestProcessedFloor}。为避免把截断视图误判为历史删除并清空运行时图谱,已暂停历史恢复。` +
|
||||||
|
"请临时关闭“限制聊天区渲染楼层”或调大渲染数量并刷新后再提取。",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyRenderLimitedHistoryRecoveryBlocked(guard, trigger = "") {
|
||||||
|
if (!guard?.blocked) return;
|
||||||
|
console.warn?.("[ST-BME] 历史恢复因聊天渲染限制暂停:", {
|
||||||
|
trigger,
|
||||||
|
chatLength: guard.chatLength,
|
||||||
|
highestProcessedFloor: guard.highestProcessedFloor,
|
||||||
|
renderLimit: guard.renderLimit,
|
||||||
|
});
|
||||||
|
updateStageNotice(
|
||||||
|
"history",
|
||||||
|
"历史恢复已暂停",
|
||||||
|
guard.message,
|
||||||
|
"warning",
|
||||||
|
{
|
||||||
|
busy: false,
|
||||||
|
persist: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function getHideRuntimeAdapters() {
|
function getHideRuntimeAdapters() {
|
||||||
return {
|
return {
|
||||||
$,
|
$,
|
||||||
@@ -16900,6 +16996,17 @@ function inspectHistoryMutation(
|
|||||||
ensureCurrentGraphRuntimeState();
|
ensureCurrentGraphRuntimeState();
|
||||||
const context = getContext();
|
const context = getContext();
|
||||||
const chat = context?.chat;
|
const chat = context?.chat;
|
||||||
|
const renderLimitedGuard = getRenderLimitedHistoryRecoveryGuard(chat);
|
||||||
|
if (renderLimitedGuard.blocked) {
|
||||||
|
notifyRenderLimitedHistoryRecoveryBlocked(renderLimitedGuard, trigger);
|
||||||
|
return {
|
||||||
|
dirty: false,
|
||||||
|
earliestAffectedFloor: null,
|
||||||
|
reason: renderLimitedGuard.reason,
|
||||||
|
source: "render-limit-guard",
|
||||||
|
skipped: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
Array.isArray(chat) &&
|
Array.isArray(chat) &&
|
||||||
currentGraph.historyState?.processedMessageHashesNeedRefresh === true
|
currentGraph.historyState?.processedMessageHashesNeedRefresh === true
|
||||||
@@ -17479,6 +17586,26 @@ async function recoverHistoryIfNeeded(trigger = "history-recovery") {
|
|||||||
const context = getContext();
|
const context = getContext();
|
||||||
const chat = context?.chat;
|
const chat = context?.chat;
|
||||||
if (!Array.isArray(chat)) return true;
|
if (!Array.isArray(chat)) return true;
|
||||||
|
const renderLimitedGuard = getRenderLimitedHistoryRecoveryGuard(chat);
|
||||||
|
if (renderLimitedGuard.blocked) {
|
||||||
|
currentGraph.historyState.lastRecoveryResult = buildRecoveryResult(
|
||||||
|
"paused",
|
||||||
|
{
|
||||||
|
fromFloor: currentGraph.historyState?.historyDirtyFrom ?? null,
|
||||||
|
path: "render-limit-guard",
|
||||||
|
detectionSource:
|
||||||
|
currentGraph.historyState?.lastMutationSource || "render-limit-guard",
|
||||||
|
reason: renderLimitedGuard.message,
|
||||||
|
resultCode: "history.recovery.paused.render-limit",
|
||||||
|
chatLength: renderLimitedGuard.chatLength,
|
||||||
|
renderLimit: renderLimitedGuard.renderLimit,
|
||||||
|
highestProcessedFloor: renderLimitedGuard.highestProcessedFloor,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
notifyRenderLimitedHistoryRecoveryBlocked(renderLimitedGuard, trigger);
|
||||||
|
refreshPanelLiveState();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const detection = inspectHistoryMutation(trigger);
|
const detection = inspectHistoryMutation(trigger);
|
||||||
const dirtyFrom = currentGraph?.historyState?.historyDirtyFrom;
|
const dirtyFrom = currentGraph?.historyState?.historyDirtyFrom;
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ let powerUser = { chat_truncation: 0 };
|
|||||||
let reloadCount = 0;
|
let reloadCount = 0;
|
||||||
let inputValue = "";
|
let inputValue = "";
|
||||||
let counterValue = "";
|
let counterValue = "";
|
||||||
|
let currentGraph = null;
|
||||||
const triggeredEvents = [];
|
const triggeredEvents = [];
|
||||||
|
|
||||||
function getContext() {
|
function getContext() {
|
||||||
@@ -83,10 +84,16 @@ function getState() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setCurrentGraph(graph) {
|
||||||
|
currentGraph = graph;
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
applyMessageRenderLimit,
|
applyMessageRenderLimit,
|
||||||
|
getRenderLimitedHistoryRecoveryGuard,
|
||||||
getMessageRenderLimitSettings,
|
getMessageRenderLimitSettings,
|
||||||
getState,
|
getState,
|
||||||
|
setCurrentGraph,
|
||||||
};
|
};
|
||||||
`,
|
`,
|
||||||
"utf8",
|
"utf8",
|
||||||
@@ -133,6 +140,51 @@ try {
|
|||||||
reloadCount: 1,
|
reloadCount: 1,
|
||||||
triggeredEvents: ["change"],
|
triggeredEvents: ["change"],
|
||||||
});
|
});
|
||||||
|
const guarded = module.getRenderLimitedHistoryRecoveryGuard(
|
||||||
|
new Array(10).fill({ mes: "visible" }),
|
||||||
|
{
|
||||||
|
settings: {
|
||||||
|
enabled: true,
|
||||||
|
hideOldMessagesRenderLimitEnabled: true,
|
||||||
|
hideOldMessagesRenderLimit: 10,
|
||||||
|
},
|
||||||
|
historyState: {
|
||||||
|
lastProcessedAssistantFloor: 30,
|
||||||
|
processedMessageHashes: { 0: "a", 30: "b" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert.equal(guarded.blocked, true);
|
||||||
|
assert.equal(guarded.renderLimit, 10);
|
||||||
|
assert.equal(guarded.highestProcessedFloor, 30);
|
||||||
|
|
||||||
|
const notGuardedWhenFullerThanRenderWindow =
|
||||||
|
module.getRenderLimitedHistoryRecoveryGuard(new Array(20).fill({}), {
|
||||||
|
settings: {
|
||||||
|
enabled: true,
|
||||||
|
hideOldMessagesRenderLimitEnabled: true,
|
||||||
|
hideOldMessagesRenderLimit: 10,
|
||||||
|
},
|
||||||
|
historyState: {
|
||||||
|
lastProcessedAssistantFloor: 30,
|
||||||
|
processedMessageHashes: { 30: "b" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert.equal(notGuardedWhenFullerThanRenderWindow.blocked, false);
|
||||||
|
|
||||||
|
const notGuardedWhenHistoryFitsVisibleChat =
|
||||||
|
module.getRenderLimitedHistoryRecoveryGuard(new Array(10).fill({}), {
|
||||||
|
settings: {
|
||||||
|
enabled: true,
|
||||||
|
hideOldMessagesRenderLimitEnabled: true,
|
||||||
|
hideOldMessagesRenderLimit: 10,
|
||||||
|
},
|
||||||
|
historyState: {
|
||||||
|
lastProcessedAssistantFloor: 5,
|
||||||
|
processedMessageHashes: { 5: "b" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert.equal(notGuardedWhenHistoryFitsVisibleChat.blocked, false);
|
||||||
|
|
||||||
const skipped = module.applyMessageRenderLimit({
|
const skipped = module.applyMessageRenderLimit({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
@@ -358,6 +358,7 @@ function createHistoryRecoveryHarness() {
|
|||||||
prepareVectorStateCalls: [],
|
prepareVectorStateCalls: [],
|
||||||
saveGraphToChatCalls: 0,
|
saveGraphToChatCalls: 0,
|
||||||
refreshPanelCalls: 0,
|
refreshPanelCalls: 0,
|
||||||
|
renderLimitBlockedCalls: [],
|
||||||
notices: [],
|
notices: [],
|
||||||
toastCalls: {
|
toastCalls: {
|
||||||
success: [],
|
success: [],
|
||||||
@@ -471,6 +472,12 @@ function createHistoryRecoveryHarness() {
|
|||||||
getSettings() {
|
getSettings() {
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
|
getRenderLimitedHistoryRecoveryGuard() {
|
||||||
|
return context.renderLimitedGuard || { blocked: false };
|
||||||
|
},
|
||||||
|
notifyRenderLimitedHistoryRecoveryBlocked(guard, trigger) {
|
||||||
|
context.renderLimitBlockedCalls.push({ guard, trigger });
|
||||||
|
},
|
||||||
isBackendVectorConfig(config) {
|
isBackendVectorConfig(config) {
|
||||||
return config?.mode === "backend";
|
return config?.mode === "backend";
|
||||||
},
|
},
|
||||||
@@ -6324,6 +6331,56 @@ async function testHistoryRecoveryStandardSuffixReplayDoesNotEmitCompletionToast
|
|||||||
assert.equal(harness.toastCalls.error.length, 0);
|
assert.equal(harness.toastCalls.error.length, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function testHistoryRecoveryPausesWhenRenderLimitedChatSlice() {
|
||||||
|
const harness = await createHistoryRecoveryHarness();
|
||||||
|
harness.chat = new Array(10).fill(null).map((_, index) => ({
|
||||||
|
is_user: index % 2 === 0,
|
||||||
|
mes: `visible-${index}`,
|
||||||
|
}));
|
||||||
|
harness.currentGraph = {
|
||||||
|
historyState: {
|
||||||
|
lastProcessedAssistantFloor: 30,
|
||||||
|
processedMessageHashes: { 30: "hash-30" },
|
||||||
|
historyDirtyFrom: 10,
|
||||||
|
lastMutationSource: "hash-recheck",
|
||||||
|
lastMutationReason: "已处理楼层超出当前聊天长度,检测到历史截断",
|
||||||
|
extractionCount: 8,
|
||||||
|
},
|
||||||
|
vectorIndexState: {
|
||||||
|
collectionId: "col-1",
|
||||||
|
dirty: false,
|
||||||
|
dirtyReason: "",
|
||||||
|
pendingRepairFromFloor: null,
|
||||||
|
replayRequiredNodeIds: [],
|
||||||
|
lastWarning: "",
|
||||||
|
lastIntegrityIssue: null,
|
||||||
|
},
|
||||||
|
batchJournal: [],
|
||||||
|
lastProcessedSeq: 30,
|
||||||
|
};
|
||||||
|
harness.renderLimitedGuard = {
|
||||||
|
blocked: true,
|
||||||
|
chatLength: 10,
|
||||||
|
renderLimit: 10,
|
||||||
|
highestProcessedFloor: 30,
|
||||||
|
reason: "render-limited-chat-slice",
|
||||||
|
message: "render limited",
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await harness.result.recoverFromHistoryMutation("manual-extract");
|
||||||
|
|
||||||
|
assert.equal(result, false);
|
||||||
|
assert.equal(harness.prepareVectorStateCalls.length, 0);
|
||||||
|
assert.equal(harness.saveGraphToChatCalls, 0);
|
||||||
|
assert.equal(harness.refreshPanelCalls, 1);
|
||||||
|
assert.equal(harness.renderLimitBlockedCalls.length, 1);
|
||||||
|
assert.equal(
|
||||||
|
harness.currentGraph.historyState.lastRecoveryResult?.resultCode,
|
||||||
|
"history.recovery.paused.render-limit",
|
||||||
|
);
|
||||||
|
assert.equal(harness.currentGraph.historyState.lastProcessedAssistantFloor, 30);
|
||||||
|
}
|
||||||
|
|
||||||
async function testHistoryRecoveryFullRebuildStillWarnsUser() {
|
async function testHistoryRecoveryFullRebuildStillWarnsUser() {
|
||||||
const harness = await createHistoryRecoveryHarness();
|
const harness = await createHistoryRecoveryHarness();
|
||||||
harness.chat = [
|
harness.chat = [
|
||||||
@@ -7525,6 +7582,7 @@ await testRecallSubGraphAndDataLayerEntryPoints();
|
|||||||
await testRerollUsesBatchBoundaryRollbackAndPersistsState();
|
await testRerollUsesBatchBoundaryRollbackAndPersistsState();
|
||||||
await testNotifyHistoryDirtyUsesStageNoticeWithoutGenericWarningToast();
|
await testNotifyHistoryDirtyUsesStageNoticeWithoutGenericWarningToast();
|
||||||
await testHistoryRecoveryStandardSuffixReplayDoesNotEmitCompletionToast();
|
await testHistoryRecoveryStandardSuffixReplayDoesNotEmitCompletionToast();
|
||||||
|
await testHistoryRecoveryPausesWhenRenderLimitedChatSlice();
|
||||||
await testHistoryRecoveryFullRebuildStillWarnsUser();
|
await testHistoryRecoveryFullRebuildStillWarnsUser();
|
||||||
await testHistoryRecoveryAbortClearsVectorRepairState();
|
await testHistoryRecoveryAbortClearsVectorRepairState();
|
||||||
await testHistoryRecoveryFallbackFullRebuildCarriesResultCode();
|
await testHistoryRecoveryFallbackFullRebuildCarriesResultCode();
|
||||||
|
|||||||
Reference in New Issue
Block a user