mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
fix: batch 2 - history recovery chain safety guards
This commit is contained in:
288
index.js
288
index.js
@@ -4,8 +4,8 @@
|
|||||||
import {
|
import {
|
||||||
eventSource,
|
eventSource,
|
||||||
event_types,
|
event_types,
|
||||||
extension_prompt_types,
|
|
||||||
extension_prompt_roles,
|
extension_prompt_roles,
|
||||||
|
extension_prompt_types,
|
||||||
getRequestHeaders,
|
getRequestHeaders,
|
||||||
saveMetadata,
|
saveMetadata,
|
||||||
saveSettingsDebounced,
|
saveSettingsDebounced,
|
||||||
@@ -48,11 +48,12 @@ import {
|
|||||||
createDefaultTaskProfiles,
|
createDefaultTaskProfiles,
|
||||||
migrateLegacyTaskProfiles,
|
migrateLegacyTaskProfiles,
|
||||||
} from "./prompt-profiles.js";
|
} from "./prompt-profiles.js";
|
||||||
|
import { resolveConfiguredTimeoutMs } from "./request-timeout.js";
|
||||||
import { retrieve } from "./retriever.js";
|
import { retrieve } from "./retriever.js";
|
||||||
import {
|
import {
|
||||||
appendBatchJournal,
|
appendBatchJournal,
|
||||||
buildReverseJournalRecoveryPlan,
|
|
||||||
buildRecoveryResult,
|
buildRecoveryResult,
|
||||||
|
buildReverseJournalRecoveryPlan,
|
||||||
clearHistoryDirty,
|
clearHistoryDirty,
|
||||||
cloneGraphSnapshot,
|
cloneGraphSnapshot,
|
||||||
createBatchJournalEntry,
|
createBatchJournalEntry,
|
||||||
@@ -75,7 +76,6 @@ import {
|
|||||||
testVectorConnection,
|
testVectorConnection,
|
||||||
validateVectorConfig,
|
validateVectorConfig,
|
||||||
} from "./vector-index.js";
|
} from "./vector-index.js";
|
||||||
import { resolveConfiguredTimeoutMs } from "./request-timeout.js";
|
|
||||||
|
|
||||||
// 操控面板模块(动态加载,防止加载失败崩溃整个扩展)
|
// 操控面板模块(动态加载,防止加载失败崩溃整个扩展)
|
||||||
let _panelModule = null;
|
let _panelModule = null;
|
||||||
@@ -193,7 +193,9 @@ function triggerChatMetadataSave(
|
|||||||
) {
|
) {
|
||||||
if (immediate) {
|
if (immediate) {
|
||||||
const immediateSave =
|
const immediateSave =
|
||||||
typeof context?.saveMetadata === "function" ? context.saveMetadata : saveMetadata;
|
typeof context?.saveMetadata === "function"
|
||||||
|
? context.saveMetadata
|
||||||
|
: saveMetadata;
|
||||||
if (typeof immediateSave === "function") {
|
if (typeof immediateSave === "function") {
|
||||||
try {
|
try {
|
||||||
const result = immediateSave.call(context);
|
const result = immediateSave.call(context);
|
||||||
@@ -217,6 +219,16 @@ function triggerChatMetadataSave(
|
|||||||
return "debounced";
|
return "debounced";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cloneGraphForPersistence(
|
||||||
|
graph = currentGraph,
|
||||||
|
chatId = getCurrentChatId(),
|
||||||
|
) {
|
||||||
|
return normalizeGraphRuntimeState(
|
||||||
|
deserializeGraph(serializeGraph(graph)),
|
||||||
|
chatId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function shouldPreferShadowSnapshotOverOfficial(officialGraph, shadowSnapshot) {
|
function shouldPreferShadowSnapshotOverOfficial(officialGraph, shadowSnapshot) {
|
||||||
if (!shadowSnapshot) return false;
|
if (!shadowSnapshot) return false;
|
||||||
const shadowRevision = Number(shadowSnapshot.revision || 0);
|
const shadowRevision = Number(shadowSnapshot.revision || 0);
|
||||||
@@ -226,10 +238,7 @@ function shouldPreferShadowSnapshotOverOfficial(officialGraph, shadowSnapshot) {
|
|||||||
|
|
||||||
function getRuntimeDebugState() {
|
function getRuntimeDebugState() {
|
||||||
const stateKey = "__stBmeRuntimeDebugState";
|
const stateKey = "__stBmeRuntimeDebugState";
|
||||||
if (
|
if (!globalThis[stateKey] || typeof globalThis[stateKey] !== "object") {
|
||||||
!globalThis[stateKey] ||
|
|
||||||
typeof globalThis[stateKey] !== "object"
|
|
||||||
) {
|
|
||||||
globalThis[stateKey] = {
|
globalThis[stateKey] = {
|
||||||
hostCapabilities: null,
|
hostCapabilities: null,
|
||||||
taskPromptBuilds: {},
|
taskPromptBuilds: {},
|
||||||
@@ -476,6 +485,7 @@ function createGraphPersistenceState() {
|
|||||||
revision: 0,
|
revision: 0,
|
||||||
lastPersistedRevision: 0,
|
lastPersistedRevision: 0,
|
||||||
queuedPersistRevision: 0,
|
queuedPersistRevision: 0,
|
||||||
|
queuedPersistChatId: "",
|
||||||
queuedPersistMode: "",
|
queuedPersistMode: "",
|
||||||
queuedPersistRotateIntegrity: false,
|
queuedPersistRotateIntegrity: false,
|
||||||
queuedPersistReason: "",
|
queuedPersistReason: "",
|
||||||
@@ -517,9 +527,7 @@ function readGraphShadowSnapshot(chatId = "") {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
chatId: String(snapshot.chatId || ""),
|
chatId: String(snapshot.chatId || ""),
|
||||||
revision: Number.isFinite(snapshot.revision)
|
revision: Number.isFinite(snapshot.revision) ? snapshot.revision : 0,
|
||||||
? snapshot.revision
|
|
||||||
: 0,
|
|
||||||
serializedGraph: snapshot.serializedGraph,
|
serializedGraph: snapshot.serializedGraph,
|
||||||
updatedAt: String(snapshot.updatedAt || ""),
|
updatedAt: String(snapshot.updatedAt || ""),
|
||||||
reason: String(snapshot.reason || ""),
|
reason: String(snapshot.reason || ""),
|
||||||
@@ -577,6 +585,7 @@ function getGraphPersistenceLiveState() {
|
|||||||
graphRevision: graphPersistenceState.revision,
|
graphRevision: graphPersistenceState.revision,
|
||||||
lastPersistedRevision: graphPersistenceState.lastPersistedRevision,
|
lastPersistedRevision: graphPersistenceState.lastPersistedRevision,
|
||||||
queuedPersistRevision: graphPersistenceState.queuedPersistRevision,
|
queuedPersistRevision: graphPersistenceState.queuedPersistRevision,
|
||||||
|
queuedPersistChatId: graphPersistenceState.queuedPersistChatId,
|
||||||
shadowSnapshotUsed: graphPersistenceState.shadowSnapshotUsed,
|
shadowSnapshotUsed: graphPersistenceState.shadowSnapshotUsed,
|
||||||
shadowSnapshotRevision: graphPersistenceState.shadowSnapshotRevision,
|
shadowSnapshotRevision: graphPersistenceState.shadowSnapshotRevision,
|
||||||
shadowSnapshotUpdatedAt: graphPersistenceState.shadowSnapshotUpdatedAt,
|
shadowSnapshotUpdatedAt: graphPersistenceState.shadowSnapshotUpdatedAt,
|
||||||
@@ -587,13 +596,15 @@ function getGraphPersistenceLiveState() {
|
|||||||
writesBlocked: graphPersistenceState.writesBlocked,
|
writesBlocked: graphPersistenceState.writesBlocked,
|
||||||
pendingPersist: graphPersistenceState.pendingPersist,
|
pendingPersist: graphPersistenceState.pendingPersist,
|
||||||
queuedPersistMode: graphPersistenceState.queuedPersistMode,
|
queuedPersistMode: graphPersistenceState.queuedPersistMode,
|
||||||
queuedPersistRotateIntegrity: graphPersistenceState.queuedPersistRotateIntegrity,
|
queuedPersistRotateIntegrity:
|
||||||
|
graphPersistenceState.queuedPersistRotateIntegrity,
|
||||||
queuedPersistReason: graphPersistenceState.queuedPersistReason,
|
queuedPersistReason: graphPersistenceState.queuedPersistReason,
|
||||||
canWriteToMetadata: isGraphMetadataWriteAllowed(
|
canWriteToMetadata: isGraphMetadataWriteAllowed(
|
||||||
graphPersistenceState.loadState,
|
graphPersistenceState.loadState,
|
||||||
),
|
),
|
||||||
updatedAt: graphPersistenceState.updatedAt,
|
updatedAt: graphPersistenceState.updatedAt,
|
||||||
};
|
};
|
||||||
|
|
||||||
return cloneRuntimeDebugValue(snapshot, snapshot);
|
return cloneRuntimeDebugValue(snapshot, snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -620,12 +631,16 @@ function bumpGraphRevision(reason = "graph-mutation") {
|
|||||||
) + 1;
|
) + 1;
|
||||||
updateGraphPersistenceState({
|
updateGraphPersistenceState({
|
||||||
revision: nextRevision,
|
revision: nextRevision,
|
||||||
lastPersistReason: String(reason || graphPersistenceState.lastPersistReason || ""),
|
lastPersistReason: String(
|
||||||
|
reason || graphPersistenceState.lastPersistReason || "",
|
||||||
|
),
|
||||||
});
|
});
|
||||||
return nextRevision;
|
return nextRevision;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isGraphMetadataWriteAllowed(loadState = graphPersistenceState.loadState) {
|
function isGraphMetadataWriteAllowed(
|
||||||
|
loadState = graphPersistenceState.loadState,
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
loadState === GRAPH_LOAD_STATES.LOADED ||
|
loadState === GRAPH_LOAD_STATES.LOADED ||
|
||||||
loadState === GRAPH_LOAD_STATES.EMPTY_CONFIRMED
|
loadState === GRAPH_LOAD_STATES.EMPTY_CONFIRMED
|
||||||
@@ -708,7 +723,10 @@ function getGraphMutationBlockReason(operationLabel = "当前操作") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureGraphMutationReady(operationLabel = "当前操作", { notify = true } = {}) {
|
function ensureGraphMutationReady(
|
||||||
|
operationLabel = "当前操作",
|
||||||
|
{ notify = true } = {},
|
||||||
|
) {
|
||||||
if (isGraphMetadataWriteAllowed()) return true;
|
if (isGraphMetadataWriteAllowed()) return true;
|
||||||
if (notify) {
|
if (notify) {
|
||||||
toastr.info(getGraphMutationBlockReason(operationLabel), "ST-BME");
|
toastr.info(getGraphMutationBlockReason(operationLabel), "ST-BME");
|
||||||
@@ -776,6 +794,16 @@ function throwIfAborted(signal, message = "操作已终止") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function assertRecoveryChatStillActive(expectedChatId, label = '') {
|
||||||
|
if (!expectedChatId) return;
|
||||||
|
const currentId = getCurrentChatId();
|
||||||
|
if (currentId && currentId !== expectedChatId) {
|
||||||
|
throw createAbortError(
|
||||||
|
`历史恢复已终止:聊天已从 ${expectedChatId} 切换到 ${currentId}${label ? ` (${label})` : ''}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getStageAbortLabel(stage) {
|
function getStageAbortLabel(stage) {
|
||||||
switch (stage) {
|
switch (stage) {
|
||||||
case "extraction":
|
case "extraction":
|
||||||
@@ -1355,10 +1383,7 @@ function hasLikelySelectedChatContext(context = getContext()) {
|
|||||||
String(context.groupId).trim() !== "";
|
String(context.groupId).trim() !== "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
hasMeaningfulChatMetadata ||
|
hasMeaningfulChatMetadata || hasChatMessages || hasCharacterId || hasGroupId
|
||||||
hasChatMessages ||
|
|
||||||
hasCharacterId ||
|
|
||||||
hasGroupId
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1476,7 +1501,14 @@ function applyModuleInjectionPrompt(content = "", settings = getSettings()) {
|
|||||||
|
|
||||||
const context = getContext();
|
const context = getContext();
|
||||||
if (typeof context?.setExtensionPrompt === "function") {
|
if (typeof context?.setExtensionPrompt === "function") {
|
||||||
context.setExtensionPrompt(MODULE_NAME, content, position, depth, false, role);
|
context.setExtensionPrompt(
|
||||||
|
MODULE_NAME,
|
||||||
|
content,
|
||||||
|
position,
|
||||||
|
depth,
|
||||||
|
false,
|
||||||
|
role,
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
applied: true,
|
applied: true,
|
||||||
source: "context",
|
source: "context",
|
||||||
@@ -1519,7 +1551,10 @@ function clearPendingGraphLoadRetry({ resetChatId = true } = {}) {
|
|||||||
|
|
||||||
function isGraphLoadRetryPending(chatId = getCurrentChatId()) {
|
function isGraphLoadRetryPending(chatId = getCurrentChatId()) {
|
||||||
const normalizedChatId = String(chatId || "");
|
const normalizedChatId = String(chatId || "");
|
||||||
return Boolean(normalizedChatId) && pendingGraphLoadRetryChatId === normalizedChatId;
|
return (
|
||||||
|
Boolean(normalizedChatId) &&
|
||||||
|
pendingGraphLoadRetryChatId === normalizedChatId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isGraphEffectivelyEmpty(graph) {
|
function isGraphEffectivelyEmpty(graph) {
|
||||||
@@ -1618,6 +1653,13 @@ function persistGraphToChatMetadata(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nextIntegrity = getChatMetadataIntegrity(context);
|
const nextIntegrity = getChatMetadataIntegrity(context);
|
||||||
|
const persistedGraph = cloneGraphForPersistence(currentGraph, chatId);
|
||||||
|
stampGraphPersistenceMeta(persistedGraph, {
|
||||||
|
revision,
|
||||||
|
reason,
|
||||||
|
chatId,
|
||||||
|
integrity: nextIntegrity,
|
||||||
|
});
|
||||||
stampGraphPersistenceMeta(currentGraph, {
|
stampGraphPersistenceMeta(currentGraph, {
|
||||||
revision,
|
revision,
|
||||||
reason,
|
reason,
|
||||||
@@ -1625,7 +1667,7 @@ function persistGraphToChatMetadata(
|
|||||||
integrity: nextIntegrity,
|
integrity: nextIntegrity,
|
||||||
});
|
});
|
||||||
writeChatMetadataPatch(context, {
|
writeChatMetadataPatch(context, {
|
||||||
[GRAPH_METADATA_KEY]: currentGraph,
|
[GRAPH_METADATA_KEY]: persistedGraph,
|
||||||
});
|
});
|
||||||
const saveMode = triggerChatMetadataSave(context, { immediate });
|
const saveMode = triggerChatMetadataSave(context, { immediate });
|
||||||
|
|
||||||
@@ -1640,6 +1682,7 @@ function persistGraphToChatMetadata(
|
|||||||
revision,
|
revision,
|
||||||
lastPersistedRevision: revision,
|
lastPersistedRevision: revision,
|
||||||
queuedPersistRevision: 0,
|
queuedPersistRevision: 0,
|
||||||
|
queuedPersistChatId: "",
|
||||||
pendingPersist: false,
|
pendingPersist: false,
|
||||||
writesBlocked: false,
|
writesBlocked: false,
|
||||||
});
|
});
|
||||||
@@ -1648,6 +1691,7 @@ function persistGraphToChatMetadata(
|
|||||||
lastPersistReason: String(reason || ""),
|
lastPersistReason: String(reason || ""),
|
||||||
lastPersistMode: saveMode,
|
lastPersistMode: saveMode,
|
||||||
metadataIntegrity: String(nextIntegrity || ""),
|
metadataIntegrity: String(nextIntegrity || ""),
|
||||||
|
queuedPersistChatId: "",
|
||||||
queuedPersistMode: "",
|
queuedPersistMode: "",
|
||||||
queuedPersistRotateIntegrity: false,
|
queuedPersistRotateIntegrity: false,
|
||||||
queuedPersistReason: "",
|
queuedPersistReason: "",
|
||||||
@@ -1667,12 +1711,14 @@ function queueGraphPersist(
|
|||||||
revision = graphPersistenceState.revision,
|
revision = graphPersistenceState.revision,
|
||||||
{ immediate = true } = {},
|
{ immediate = true } = {},
|
||||||
) {
|
) {
|
||||||
|
const queuedChatId = graphPersistenceState.chatId || getCurrentChatId();
|
||||||
maybeCaptureGraphShadowSnapshot(reason);
|
maybeCaptureGraphShadowSnapshot(reason);
|
||||||
updateGraphPersistenceState({
|
updateGraphPersistenceState({
|
||||||
queuedPersistRevision: Math.max(
|
queuedPersistRevision: Math.max(
|
||||||
graphPersistenceState.queuedPersistRevision || 0,
|
graphPersistenceState.queuedPersistRevision || 0,
|
||||||
revision || 0,
|
revision || 0,
|
||||||
),
|
),
|
||||||
|
queuedPersistChatId: String(queuedChatId || ""),
|
||||||
queuedPersistMode: immediate ? "immediate" : "debounced",
|
queuedPersistMode: immediate ? "immediate" : "debounced",
|
||||||
queuedPersistRotateIntegrity: false,
|
queuedPersistRotateIntegrity: false,
|
||||||
queuedPersistReason: String(reason || ""),
|
queuedPersistReason: String(reason || ""),
|
||||||
@@ -1713,6 +1759,19 @@ function maybeFlushQueuedGraphPersist(reason = "queued-graph-persist") {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const activeChatId = getCurrentChatId();
|
||||||
|
const queuedChatId = String(graphPersistenceState.queuedPersistChatId || "");
|
||||||
|
if (queuedChatId && activeChatId && queuedChatId !== activeChatId) {
|
||||||
|
return buildGraphPersistResult({
|
||||||
|
saved: false,
|
||||||
|
queued: graphPersistenceState.pendingPersist,
|
||||||
|
blocked: true,
|
||||||
|
reason: "queued-chat-mismatch",
|
||||||
|
revision: graphPersistenceState.queuedPersistRevision,
|
||||||
|
saveMode: graphPersistenceState.queuedPersistMode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const targetRevision = Math.max(
|
const targetRevision = Math.max(
|
||||||
graphPersistenceState.revision || 0,
|
graphPersistenceState.revision || 0,
|
||||||
graphPersistenceState.queuedPersistRevision || 0,
|
graphPersistenceState.queuedPersistRevision || 0,
|
||||||
@@ -1924,7 +1983,12 @@ function setLastExtractionStatus(
|
|||||||
text,
|
text,
|
||||||
meta,
|
meta,
|
||||||
level = "info",
|
level = "info",
|
||||||
{ syncRuntime = true, toastKind = "", toastTitle = "ST-BME 提取", noticeMarquee = false } = {},
|
{
|
||||||
|
syncRuntime = true,
|
||||||
|
toastKind = "",
|
||||||
|
toastTitle = "ST-BME 提取",
|
||||||
|
noticeMarquee = false,
|
||||||
|
} = {},
|
||||||
) {
|
) {
|
||||||
lastExtractionStatus = createUiStatus(text, meta, level);
|
lastExtractionStatus = createUiStatus(text, meta, level);
|
||||||
if (syncRuntime) {
|
if (syncRuntime) {
|
||||||
@@ -1975,7 +2039,12 @@ function setLastRecallStatus(
|
|||||||
text,
|
text,
|
||||||
meta,
|
meta,
|
||||||
level = "info",
|
level = "info",
|
||||||
{ syncRuntime = true, toastKind = "", toastTitle = "ST-BME 召回", noticeMarquee = false } = {},
|
{
|
||||||
|
syncRuntime = true,
|
||||||
|
toastKind = "",
|
||||||
|
toastTitle = "ST-BME 召回",
|
||||||
|
noticeMarquee = false,
|
||||||
|
} = {},
|
||||||
) {
|
) {
|
||||||
lastRecallStatus = createUiStatus(text, meta, level);
|
lastRecallStatus = createUiStatus(text, meta, level);
|
||||||
if (syncRuntime) {
|
if (syncRuntime) {
|
||||||
@@ -2545,6 +2614,7 @@ function loadGraphFromChat(options = {}) {
|
|||||||
revision: 0,
|
revision: 0,
|
||||||
lastPersistedRevision: 0,
|
lastPersistedRevision: 0,
|
||||||
queuedPersistRevision: 0,
|
queuedPersistRevision: 0,
|
||||||
|
queuedPersistChatId: "",
|
||||||
pendingPersist: false,
|
pendingPersist: false,
|
||||||
shadowSnapshotUsed: false,
|
shadowSnapshotUsed: false,
|
||||||
shadowSnapshotRevision: 0,
|
shadowSnapshotRevision: 0,
|
||||||
@@ -2590,6 +2660,7 @@ function loadGraphFromChat(options = {}) {
|
|||||||
revision: 0,
|
revision: 0,
|
||||||
lastPersistedRevision: 0,
|
lastPersistedRevision: 0,
|
||||||
queuedPersistRevision: 0,
|
queuedPersistRevision: 0,
|
||||||
|
queuedPersistChatId: "",
|
||||||
pendingPersist: false,
|
pendingPersist: false,
|
||||||
shadowSnapshotUsed: false,
|
shadowSnapshotUsed: false,
|
||||||
shadowSnapshotRevision: 0,
|
shadowSnapshotRevision: 0,
|
||||||
@@ -2621,20 +2692,28 @@ function loadGraphFromChat(options = {}) {
|
|||||||
const shouldRetry = attemptIndex < GRAPH_LOAD_RETRY_DELAYS_MS.length;
|
const shouldRetry = attemptIndex < GRAPH_LOAD_RETRY_DELAYS_MS.length;
|
||||||
|
|
||||||
if (hasOfficialGraph) {
|
if (hasOfficialGraph) {
|
||||||
const officialGraph = normalizeGraphRuntimeState(
|
const officialGraph = cloneGraphForPersistence(
|
||||||
deserializeGraph(savedData),
|
normalizeGraphRuntimeState(deserializeGraph(savedData), chatId),
|
||||||
chatId,
|
chatId,
|
||||||
);
|
);
|
||||||
const officialRevision = Math.max(1, getGraphPersistedRevision(officialGraph));
|
const officialRevision = Math.max(
|
||||||
|
1,
|
||||||
|
getGraphPersistedRevision(officialGraph),
|
||||||
|
);
|
||||||
const metadataIntegrity = getChatMetadataIntegrity(context);
|
const metadataIntegrity = getChatMetadataIntegrity(context);
|
||||||
|
|
||||||
if (shouldPreferShadowSnapshotOverOfficial(officialGraph, shadowSnapshot)) {
|
if (shouldPreferShadowSnapshotOverOfficial(officialGraph, shadowSnapshot)) {
|
||||||
clearPendingGraphLoadRetry();
|
clearPendingGraphLoadRetry();
|
||||||
currentGraph = normalizeGraphRuntimeState(
|
currentGraph = cloneGraphForPersistence(
|
||||||
deserializeGraph(shadowSnapshot.serializedGraph),
|
normalizeGraphRuntimeState(
|
||||||
|
deserializeGraph(shadowSnapshot.serializedGraph),
|
||||||
|
chatId,
|
||||||
|
),
|
||||||
chatId,
|
chatId,
|
||||||
);
|
);
|
||||||
extractionCount = Number.isFinite(currentGraph?.historyState?.extractionCount)
|
extractionCount = Number.isFinite(
|
||||||
|
currentGraph?.historyState?.extractionCount,
|
||||||
|
)
|
||||||
? currentGraph.historyState.extractionCount
|
? currentGraph.historyState.extractionCount
|
||||||
: 0;
|
: 0;
|
||||||
lastExtractedItems = [];
|
lastExtractedItems = [];
|
||||||
@@ -2697,7 +2776,9 @@ function loadGraphFromChat(options = {}) {
|
|||||||
|
|
||||||
clearPendingGraphLoadRetry();
|
clearPendingGraphLoadRetry();
|
||||||
currentGraph = officialGraph;
|
currentGraph = officialGraph;
|
||||||
extractionCount = Number.isFinite(currentGraph?.historyState?.extractionCount)
|
extractionCount = Number.isFinite(
|
||||||
|
currentGraph?.historyState?.extractionCount,
|
||||||
|
)
|
||||||
? currentGraph.historyState.extractionCount
|
? currentGraph.historyState.extractionCount
|
||||||
: 0;
|
: 0;
|
||||||
lastExtractedItems = [];
|
lastExtractedItems = [];
|
||||||
@@ -2731,6 +2812,7 @@ function loadGraphFromChat(options = {}) {
|
|||||||
revision: officialRevision,
|
revision: officialRevision,
|
||||||
lastPersistedRevision: officialRevision,
|
lastPersistedRevision: officialRevision,
|
||||||
queuedPersistRevision: 0,
|
queuedPersistRevision: 0,
|
||||||
|
queuedPersistChatId: "",
|
||||||
pendingPersist: false,
|
pendingPersist: false,
|
||||||
shadowSnapshotUsed: false,
|
shadowSnapshotUsed: false,
|
||||||
shadowSnapshotRevision: 0,
|
shadowSnapshotRevision: 0,
|
||||||
@@ -2770,7 +2852,9 @@ function loadGraphFromChat(options = {}) {
|
|||||||
deserializeGraph(shadowSnapshot.serializedGraph),
|
deserializeGraph(shadowSnapshot.serializedGraph),
|
||||||
chatId,
|
chatId,
|
||||||
);
|
);
|
||||||
extractionCount = Number.isFinite(currentGraph?.historyState?.extractionCount)
|
extractionCount = Number.isFinite(
|
||||||
|
currentGraph?.historyState?.extractionCount,
|
||||||
|
)
|
||||||
? currentGraph.historyState.extractionCount
|
? currentGraph.historyState.extractionCount
|
||||||
: 0;
|
: 0;
|
||||||
lastExtractedItems = [];
|
lastExtractedItems = [];
|
||||||
@@ -2858,7 +2942,9 @@ function loadGraphFromChat(options = {}) {
|
|||||||
if (shouldRetry) {
|
if (shouldRetry) {
|
||||||
scheduleGraphLoadRetry(
|
scheduleGraphLoadRetry(
|
||||||
chatId,
|
chatId,
|
||||||
hasChatMetadata ? "official-graph-missing-shadow" : "chat-metadata-missing-shadow",
|
hasChatMetadata
|
||||||
|
? "official-graph-missing-shadow"
|
||||||
|
: "chat-metadata-missing-shadow",
|
||||||
attemptIndex,
|
attemptIndex,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -2892,40 +2978,47 @@ function loadGraphFromChat(options = {}) {
|
|||||||
);
|
);
|
||||||
lastExtractionStatus = createUiStatus(
|
lastExtractionStatus = createUiStatus(
|
||||||
"待命",
|
"待命",
|
||||||
shouldRetry ? "正在等待聊天图谱元数据加载" : "聊天元数据未就绪,暂不允许修改图谱",
|
shouldRetry
|
||||||
|
? "正在等待聊天图谱元数据加载"
|
||||||
|
: "聊天元数据未就绪,暂不允许修改图谱",
|
||||||
shouldRetry ? "idle" : "warning",
|
shouldRetry ? "idle" : "warning",
|
||||||
);
|
);
|
||||||
lastVectorStatus = createUiStatus(
|
lastVectorStatus = createUiStatus(
|
||||||
"待命",
|
"待命",
|
||||||
shouldRetry ? "正在等待聊天图谱元数据加载" : "聊天元数据未就绪,暂不允许修改图谱",
|
shouldRetry
|
||||||
|
? "正在等待聊天图谱元数据加载"
|
||||||
|
: "聊天元数据未就绪,暂不允许修改图谱",
|
||||||
shouldRetry ? "idle" : "warning",
|
shouldRetry ? "idle" : "warning",
|
||||||
);
|
);
|
||||||
lastRecallStatus = createUiStatus(
|
lastRecallStatus = createUiStatus(
|
||||||
"待命",
|
"待命",
|
||||||
shouldRetry ? "正在等待聊天图谱元数据加载" : "聊天元数据未就绪,图谱处于保护状态",
|
shouldRetry
|
||||||
|
? "正在等待聊天图谱元数据加载"
|
||||||
|
: "聊天元数据未就绪,图谱处于保护状态",
|
||||||
shouldRetry ? "idle" : "warning",
|
shouldRetry ? "idle" : "warning",
|
||||||
);
|
);
|
||||||
applyGraphLoadState(
|
applyGraphLoadState(
|
||||||
shouldRetry ? GRAPH_LOAD_STATES.LOADING : GRAPH_LOAD_STATES.BLOCKED,
|
shouldRetry ? GRAPH_LOAD_STATES.LOADING : GRAPH_LOAD_STATES.BLOCKED,
|
||||||
{
|
{
|
||||||
chatId,
|
chatId,
|
||||||
reason: hasChatMetadata
|
reason: hasChatMetadata
|
||||||
? shouldRetry
|
? shouldRetry
|
||||||
? "graph-metadata-missing"
|
? "graph-metadata-missing"
|
||||||
: "graph-metadata-timeout"
|
: "graph-metadata-timeout"
|
||||||
: shouldRetry
|
: shouldRetry
|
||||||
? "chat-metadata-missing"
|
? "chat-metadata-missing"
|
||||||
: "chat-metadata-timeout",
|
: "chat-metadata-timeout",
|
||||||
attemptIndex,
|
attemptIndex,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
lastPersistedRevision: 0,
|
lastPersistedRevision: 0,
|
||||||
queuedPersistRevision: 0,
|
queuedPersistRevision: 0,
|
||||||
pendingPersist: false,
|
queuedPersistChatId: "",
|
||||||
shadowSnapshotUsed: false,
|
pendingPersist: false,
|
||||||
shadowSnapshotRevision: 0,
|
shadowSnapshotUsed: false,
|
||||||
shadowSnapshotUpdatedAt: "",
|
shadowSnapshotRevision: 0,
|
||||||
shadowSnapshotReason: "",
|
shadowSnapshotUpdatedAt: "",
|
||||||
writesBlocked: true,
|
shadowSnapshotReason: "",
|
||||||
|
writesBlocked: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (shouldRetry) {
|
if (shouldRetry) {
|
||||||
@@ -2953,26 +3046,10 @@ function loadGraphFromChat(options = {}) {
|
|||||||
|
|
||||||
clearPendingGraphLoadRetry();
|
clearPendingGraphLoadRetry();
|
||||||
const confirmedState = GRAPH_LOAD_STATES.EMPTY_CONFIRMED;
|
const confirmedState = GRAPH_LOAD_STATES.EMPTY_CONFIRMED;
|
||||||
runtimeStatus = createUiStatus(
|
runtimeStatus = createUiStatus("待命", "当前聊天还没有图谱", "idle");
|
||||||
"待命",
|
lastExtractionStatus = createUiStatus("待命", "当前聊天尚未执行提取", "idle");
|
||||||
"当前聊天还没有图谱",
|
lastVectorStatus = createUiStatus("待命", "当前聊天尚未执行向量任务", "idle");
|
||||||
"idle",
|
lastRecallStatus = createUiStatus("待命", "当前聊天尚未建立记忆图谱", "idle");
|
||||||
);
|
|
||||||
lastExtractionStatus = createUiStatus(
|
|
||||||
"待命",
|
|
||||||
"当前聊天尚未执行提取",
|
|
||||||
"idle",
|
|
||||||
);
|
|
||||||
lastVectorStatus = createUiStatus(
|
|
||||||
"待命",
|
|
||||||
"当前聊天尚未执行向量任务",
|
|
||||||
"idle",
|
|
||||||
);
|
|
||||||
lastRecallStatus = createUiStatus(
|
|
||||||
"待命",
|
|
||||||
"当前聊天尚未建立记忆图谱",
|
|
||||||
"idle",
|
|
||||||
);
|
|
||||||
applyGraphLoadState(confirmedState, {
|
applyGraphLoadState(confirmedState, {
|
||||||
chatId,
|
chatId,
|
||||||
reason: "metadata-confirmed-empty",
|
reason: "metadata-confirmed-empty",
|
||||||
@@ -2980,6 +3057,7 @@ function loadGraphFromChat(options = {}) {
|
|||||||
revision: 0,
|
revision: 0,
|
||||||
lastPersistedRevision: 0,
|
lastPersistedRevision: 0,
|
||||||
queuedPersistRevision: 0,
|
queuedPersistRevision: 0,
|
||||||
|
queuedPersistChatId: "",
|
||||||
pendingPersist: false,
|
pendingPersist: false,
|
||||||
shadowSnapshotUsed: false,
|
shadowSnapshotUsed: false,
|
||||||
shadowSnapshotRevision: 0,
|
shadowSnapshotRevision: 0,
|
||||||
@@ -3460,7 +3538,10 @@ function clearGenerationRecallTransactionsForChat(
|
|||||||
return removed;
|
return removed;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [transactionId, transaction] of generationRecallTransactions.entries()) {
|
for (const [
|
||||||
|
transactionId,
|
||||||
|
transaction,
|
||||||
|
] of generationRecallTransactions.entries()) {
|
||||||
if (String(transaction?.chatId || "") !== normalizedChatId) continue;
|
if (String(transaction?.chatId || "") !== normalizedChatId) continue;
|
||||||
generationRecallTransactions.delete(transactionId);
|
generationRecallTransactions.delete(transactionId);
|
||||||
removed += 1;
|
removed += 1;
|
||||||
@@ -3519,8 +3600,8 @@ function getGenerationRecallHookStateFromResult(result) {
|
|||||||
function invalidateRecallAfterHistoryMutation(reason = "聊天记录已变更") {
|
function invalidateRecallAfterHistoryMutation(reason = "聊天记录已变更") {
|
||||||
const hadActiveRecall = Boolean(
|
const hadActiveRecall = Boolean(
|
||||||
isRecalling ||
|
isRecalling ||
|
||||||
(stageAbortControllers.recall &&
|
(stageAbortControllers.recall &&
|
||||||
!stageAbortControllers.recall.signal?.aborted),
|
!stageAbortControllers.recall.signal?.aborted),
|
||||||
);
|
);
|
||||||
if (hadActiveRecall) {
|
if (hadActiveRecall) {
|
||||||
abortRecallStageWithReason(`${reason},当前召回已取消`);
|
abortRecallStageWithReason(`${reason},当前召回已取消`);
|
||||||
@@ -4010,7 +4091,11 @@ function resolveDirtyFloorFromMutationMeta(trigger, primaryArg, meta, chat) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (candidates.length === 0) return null;
|
if (candidates.length === 0) return null;
|
||||||
return candidates.reduce((earliest, current) =>
|
const validCandidates = Number.isFinite(minExtractableFloor)
|
||||||
|
? candidates.filter((c) => c.floor >= minExtractableFloor)
|
||||||
|
: candidates;
|
||||||
|
if (validCandidates.length === 0) return null;
|
||||||
|
return validCandidates.reduce((earliest, current) =>
|
||||||
current.floor < earliest.floor ? current : earliest,
|
current.floor < earliest.floor ? current : earliest,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -4073,6 +4158,7 @@ function scheduleImmediateHistoryRecovery(
|
|||||||
) {
|
) {
|
||||||
if (!getSettings().enabled) return;
|
if (!getSettings().enabled) return;
|
||||||
|
|
||||||
|
const scheduledChatId = getCurrentChatId();
|
||||||
pendingHistoryRecoveryTrigger = trigger;
|
pendingHistoryRecoveryTrigger = trigger;
|
||||||
clearTimeout(pendingHistoryRecoveryTimer);
|
clearTimeout(pendingHistoryRecoveryTimer);
|
||||||
pendingHistoryRecoveryTimer = setTimeout(() => {
|
pendingHistoryRecoveryTimer = setTimeout(() => {
|
||||||
@@ -4080,6 +4166,7 @@ function scheduleImmediateHistoryRecovery(
|
|||||||
const effectiveTrigger = pendingHistoryRecoveryTrigger || trigger;
|
const effectiveTrigger = pendingHistoryRecoveryTrigger || trigger;
|
||||||
pendingHistoryRecoveryTrigger = "";
|
pendingHistoryRecoveryTrigger = "";
|
||||||
if (!getSettings().enabled) return;
|
if (!getSettings().enabled) return;
|
||||||
|
if (getCurrentChatId() !== scheduledChatId) return;
|
||||||
|
|
||||||
void recoverHistoryIfNeeded(`event:${effectiveTrigger}`)
|
void recoverHistoryIfNeeded(`event:${effectiveTrigger}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -4109,6 +4196,7 @@ function scheduleHistoryMutationRecheck(
|
|||||||
) {
|
) {
|
||||||
if (!getSettings().enabled) return;
|
if (!getSettings().enabled) return;
|
||||||
|
|
||||||
|
const scheduledChatId = getCurrentChatId();
|
||||||
clearPendingHistoryMutationChecks();
|
clearPendingHistoryMutationChecks();
|
||||||
clearTimeout(pendingHistoryRecoveryTimer);
|
clearTimeout(pendingHistoryRecoveryTimer);
|
||||||
pendingHistoryRecoveryTimer = null;
|
pendingHistoryRecoveryTimer = null;
|
||||||
@@ -4132,6 +4220,7 @@ function scheduleHistoryMutationRecheck(
|
|||||||
(candidate) => candidate !== timer,
|
(candidate) => candidate !== timer,
|
||||||
);
|
);
|
||||||
if (!getSettings().enabled) return;
|
if (!getSettings().enabled) return;
|
||||||
|
if (getCurrentChatId() !== scheduledChatId) return;
|
||||||
|
|
||||||
const detection = inspectHistoryMutation(
|
const detection = inspectHistoryMutation(
|
||||||
`settled:${trigger}`,
|
`settled:${trigger}`,
|
||||||
@@ -4325,9 +4414,10 @@ async function executeExtractionBatch({
|
|||||||
settings,
|
settings,
|
||||||
signal,
|
signal,
|
||||||
onStreamProgress: ({ previewText, receivedChars }) => {
|
onStreamProgress: ({ previewText, receivedChars }) => {
|
||||||
const preview = previewText?.length > 60
|
const preview =
|
||||||
? "…" + previewText.slice(-60)
|
previewText?.length > 60
|
||||||
: previewText || "";
|
? "…" + previewText.slice(-60)
|
||||||
|
: previewText || "";
|
||||||
setLastExtractionStatus(
|
setLastExtractionStatus(
|
||||||
"AI 生成中",
|
"AI 生成中",
|
||||||
`${preview} [${receivedChars}字]`,
|
`${preview} [${receivedChars}字]`,
|
||||||
@@ -4404,11 +4494,12 @@ async function executeExtractionBatch({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function replayExtractionFromHistory(chat, settings, signal = undefined) {
|
async function replayExtractionFromHistory(chat, settings, signal = undefined, expectedChatId = undefined) {
|
||||||
let replayedBatches = 0;
|
let replayedBatches = 0;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
throwIfAborted(signal, "历史恢复已终止");
|
throwIfAborted(signal, "历史恢复已终止");
|
||||||
|
assertRecoveryChatStillActive(expectedChatId, 'replay-loop');
|
||||||
const pendingAssistantTurns = getAssistantTurns(chat).filter(
|
const pendingAssistantTurns = getAssistantTurns(chat).filter(
|
||||||
(index) => index > getLastProcessedAssistantFloor(),
|
(index) => index > getLastProcessedAssistantFloor(),
|
||||||
);
|
);
|
||||||
@@ -4542,6 +4633,7 @@ async function rollbackGraphForReroll(targetFloor, context = getContext()) {
|
|||||||
isBackendVectorConfig(config) &&
|
isBackendVectorConfig(config) &&
|
||||||
recoveryPlan.backendDeleteHashes.length > 0
|
recoveryPlan.backendDeleteHashes.length > 0
|
||||||
) {
|
) {
|
||||||
|
assertRecoveryChatStillActive(chatId, 'reroll-pre-vector');
|
||||||
await deleteBackendVectorHashesForRecovery(
|
await deleteBackendVectorHashesForRecovery(
|
||||||
currentGraph.vectorIndexState.collectionId,
|
currentGraph.vectorIndexState.collectionId,
|
||||||
config,
|
config,
|
||||||
@@ -4549,11 +4641,15 @@ async function rollbackGraphForReroll(targetFloor, context = getContext()) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assertRecoveryChatStillActive(chatId, 'reroll-pre-prepare');
|
||||||
await prepareVectorStateForReplay(false, undefined, {
|
await prepareVectorStateForReplay(false, undefined, {
|
||||||
skipBackendPurge: isBackendVectorConfig(config),
|
skipBackendPurge: isBackendVectorConfig(config),
|
||||||
});
|
});
|
||||||
} else if (recoveryPath === "legacy-snapshot") {
|
} else if (recoveryPath === "legacy-snapshot") {
|
||||||
currentGraph = normalizeGraphRuntimeState(recoveryPoint.snapshotBefore, chatId);
|
currentGraph = normalizeGraphRuntimeState(
|
||||||
|
recoveryPoint.snapshotBefore,
|
||||||
|
chatId,
|
||||||
|
);
|
||||||
extractionCount = currentGraph.historyState.extractionCount || 0;
|
extractionCount = currentGraph.historyState.extractionCount || 0;
|
||||||
await prepareVectorStateForReplay(false);
|
await prepareVectorStateForReplay(false);
|
||||||
} else {
|
} else {
|
||||||
@@ -4672,6 +4768,7 @@ async function recoverHistoryIfNeeded(trigger = "history-recovery") {
|
|||||||
isBackendVectorConfig(config) &&
|
isBackendVectorConfig(config) &&
|
||||||
recoveryPlan.backendDeleteHashes.length > 0
|
recoveryPlan.backendDeleteHashes.length > 0
|
||||||
) {
|
) {
|
||||||
|
assertRecoveryChatStillActive(chatId, 'pre-backend-delete');
|
||||||
await deleteBackendVectorHashesForRecovery(
|
await deleteBackendVectorHashesForRecovery(
|
||||||
currentGraph.vectorIndexState.collectionId,
|
currentGraph.vectorIndexState.collectionId,
|
||||||
config,
|
config,
|
||||||
@@ -4699,10 +4796,12 @@ async function recoverHistoryIfNeeded(trigger = "history-recovery") {
|
|||||||
await prepareVectorStateForReplay(true, historySignal);
|
await prepareVectorStateForReplay(true, historySignal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assertRecoveryChatStillActive(chatId, 'pre-replay');
|
||||||
replayedBatches = await replayExtractionFromHistory(
|
replayedBatches = await replayExtractionFromHistory(
|
||||||
chat,
|
chat,
|
||||||
settings,
|
settings,
|
||||||
historySignal,
|
historySignal,
|
||||||
|
chatId,
|
||||||
);
|
);
|
||||||
|
|
||||||
clearHistoryDirty(
|
clearHistoryDirty(
|
||||||
@@ -4763,10 +4862,12 @@ async function recoverHistoryIfNeeded(trigger = "history-recovery") {
|
|||||||
currentGraph = normalizeGraphRuntimeState(createEmptyGraph(), chatId);
|
currentGraph = normalizeGraphRuntimeState(createEmptyGraph(), chatId);
|
||||||
extractionCount = 0;
|
extractionCount = 0;
|
||||||
await prepareVectorStateForReplay(true, historySignal);
|
await prepareVectorStateForReplay(true, historySignal);
|
||||||
|
assertRecoveryChatStillActive(chatId, 'pre-fallback-replay');
|
||||||
replayedBatches = await replayExtractionFromHistory(
|
replayedBatches = await replayExtractionFromHistory(
|
||||||
chat,
|
chat,
|
||||||
settings,
|
settings,
|
||||||
historySignal,
|
historySignal,
|
||||||
|
chatId,
|
||||||
);
|
);
|
||||||
clearHistoryDirty(
|
clearHistoryDirty(
|
||||||
currentGraph,
|
currentGraph,
|
||||||
@@ -4965,7 +5066,10 @@ function applyRecallInjection(settings, recallInput, recentMessages, result) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const injectionTransport = applyModuleInjectionPrompt(injectionText, settings);
|
const injectionTransport = applyModuleInjectionPrompt(
|
||||||
|
injectionText,
|
||||||
|
settings,
|
||||||
|
);
|
||||||
recordInjectionSnapshot("recall", {
|
recordInjectionSnapshot("recall", {
|
||||||
taskType: "recall",
|
taskType: "recall",
|
||||||
source: recallInput.source,
|
source: recallInput.source,
|
||||||
@@ -5174,9 +5278,10 @@ async function runRecall(options = {}) {
|
|||||||
signal: recallSignal,
|
signal: recallSignal,
|
||||||
settings,
|
settings,
|
||||||
onStreamProgress: ({ previewText, receivedChars }) => {
|
onStreamProgress: ({ previewText, receivedChars }) => {
|
||||||
const preview = previewText?.length > 60
|
const preview =
|
||||||
? "…" + previewText.slice(-60)
|
previewText?.length > 60
|
||||||
: previewText || "";
|
? "…" + previewText.slice(-60)
|
||||||
|
: previewText || "";
|
||||||
setLastRecallStatus(
|
setLastRecallStatus(
|
||||||
"AI 生成中",
|
"AI 生成中",
|
||||||
`${preview} [${receivedChars}字]`,
|
`${preview} [${receivedChars}字]`,
|
||||||
@@ -5211,18 +5316,15 @@ async function runRecall(options = {}) {
|
|||||||
temporalLinkStrength: settings.recallTemporalLinkStrength ?? 0.2,
|
temporalLinkStrength: settings.recallTemporalLinkStrength ?? 0.2,
|
||||||
enableDiversitySampling:
|
enableDiversitySampling:
|
||||||
settings.recallEnableDiversitySampling ?? true,
|
settings.recallEnableDiversitySampling ?? true,
|
||||||
dppCandidateMultiplier:
|
dppCandidateMultiplier: settings.recallDppCandidateMultiplier ?? 3,
|
||||||
settings.recallDppCandidateMultiplier ?? 3,
|
|
||||||
dppQualityWeight: settings.recallDppQualityWeight ?? 1.0,
|
dppQualityWeight: settings.recallDppQualityWeight ?? 1.0,
|
||||||
enableCooccurrenceBoost:
|
enableCooccurrenceBoost:
|
||||||
settings.recallEnableCooccurrenceBoost ?? false,
|
settings.recallEnableCooccurrenceBoost ?? false,
|
||||||
cooccurrenceScale: settings.recallCooccurrenceScale ?? 0.1,
|
cooccurrenceScale: settings.recallCooccurrenceScale ?? 0.1,
|
||||||
cooccurrenceMaxNeighbors:
|
cooccurrenceMaxNeighbors:
|
||||||
settings.recallCooccurrenceMaxNeighbors ?? 10,
|
settings.recallCooccurrenceMaxNeighbors ?? 10,
|
||||||
enableResidualRecall:
|
enableResidualRecall: settings.recallEnableResidualRecall ?? false,
|
||||||
settings.recallEnableResidualRecall ?? false,
|
residualBasisMaxNodes: settings.recallResidualBasisMaxNodes ?? 24,
|
||||||
residualBasisMaxNodes:
|
|
||||||
settings.recallResidualBasisMaxNodes ?? 24,
|
|
||||||
residualNmfTopics: settings.recallNmfTopics ?? 15,
|
residualNmfTopics: settings.recallNmfTopics ?? 15,
|
||||||
residualNmfNoveltyThreshold:
|
residualNmfNoveltyThreshold:
|
||||||
settings.recallNmfNoveltyThreshold ?? 0.4,
|
settings.recallNmfNoveltyThreshold ?? 0.4,
|
||||||
|
|||||||
@@ -256,6 +256,10 @@ result = {
|
|||||||
onMessageReceived,
|
onMessageReceived,
|
||||||
applyGraphLoadState,
|
applyGraphLoadState,
|
||||||
maybeFlushQueuedGraphPersist,
|
maybeFlushQueuedGraphPersist,
|
||||||
|
cloneGraphForPersistence,
|
||||||
|
assertRecoveryChatStillActive,
|
||||||
|
createAbortError,
|
||||||
|
isAbortError,
|
||||||
setCurrentGraph(graph) {
|
setCurrentGraph(graph) {
|
||||||
currentGraph = graph;
|
currentGraph = graph;
|
||||||
return currentGraph;
|
return currentGraph;
|
||||||
@@ -330,7 +334,10 @@ result = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(result.loadState, "loaded");
|
assert.equal(result.loadState, "loaded");
|
||||||
assert.equal(harness.api.getCurrentGraph().historyState.chatId, "chat-global");
|
assert.equal(
|
||||||
|
harness.api.getCurrentGraph().historyState.chatId,
|
||||||
|
"chat-global",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -460,7 +467,8 @@ result = {
|
|||||||
assert.ok(shadow, "loading 状态下应写入会话影子快照");
|
assert.ok(shadow, "loading 状态下应写入会话影子快照");
|
||||||
assert.equal(shadow.revision, 4);
|
assert.equal(shadow.revision, 4);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
harness.api.readRuntimeDebugSnapshot().graphPersistence?.queuedPersistRevision,
|
harness.api.readRuntimeDebugSnapshot().graphPersistence
|
||||||
|
?.queuedPersistRevision,
|
||||||
4,
|
4,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -470,7 +478,9 @@ result = {
|
|||||||
chatId: "chat-empty",
|
chatId: "chat-empty",
|
||||||
chatMetadata: undefined,
|
chatMetadata: undefined,
|
||||||
});
|
});
|
||||||
harness.api.setCurrentGraph(normalizeGraphRuntimeState(createEmptyGraph(), "chat-empty"));
|
harness.api.setCurrentGraph(
|
||||||
|
normalizeGraphRuntimeState(createEmptyGraph(), "chat-empty"),
|
||||||
|
);
|
||||||
harness.api.setGraphPersistenceState({
|
harness.api.setGraphPersistenceState({
|
||||||
loadState: "loading",
|
loadState: "loading",
|
||||||
chatId: "chat-empty",
|
chatId: "chat-empty",
|
||||||
@@ -633,8 +643,8 @@ result = {
|
|||||||
);
|
);
|
||||||
assert.equal(reader.runtimeContext.__contextImmediateSaveCalls, 1);
|
assert.equal(reader.runtimeContext.__contextImmediateSaveCalls, 1);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
reader.runtimeContext.__chatContext.chatMetadata?.st_bme_graph?.nodes?.[0]?.fields
|
reader.runtimeContext.__chatContext.chatMetadata?.st_bme_graph?.nodes?.[0]
|
||||||
?.title,
|
?.fields?.title,
|
||||||
"事件-shadow-newer",
|
"事件-shadow-newer",
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
@@ -733,8 +743,8 @@ result = {
|
|||||||
"插件保存图谱时不能改写宿主 metadata.integrity",
|
"插件保存图谱时不能改写宿主 metadata.integrity",
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
harness.runtimeContext.__chatContext.chatMetadata?.st_bme_graph?.__stBmePersistence
|
harness.runtimeContext.__chatContext.chatMetadata?.st_bme_graph
|
||||||
?.revision > 0,
|
?.__stBmePersistence?.revision > 0,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -767,7 +777,8 @@ result = {
|
|||||||
|
|
||||||
assert.equal(result.loadState, "loaded");
|
assert.equal(result.loadState, "loaded");
|
||||||
assert.equal(
|
assert.equal(
|
||||||
reader.runtimeContext.__chatContext.chatMetadata?.st_bme_graph?.nodes?.length,
|
reader.runtimeContext.__chatContext.chatMetadata?.st_bme_graph?.nodes
|
||||||
|
?.length,
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
@@ -781,4 +792,347 @@ result = {
|
|||||||
assert.equal(live.pendingPersist, false);
|
assert.equal(live.pendingPersist, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const harness = await createGraphPersistenceHarness({
|
||||||
|
chatId: "chat-decouple",
|
||||||
|
chatMetadata: {
|
||||||
|
integrity: "meta-decouple",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const runtimeGraph = createMeaningfulGraph("chat-decouple", "runtime");
|
||||||
|
harness.api.setCurrentGraph(runtimeGraph);
|
||||||
|
harness.api.setGraphPersistenceState({
|
||||||
|
loadState: "loaded",
|
||||||
|
chatId: "chat-decouple",
|
||||||
|
revision: 3,
|
||||||
|
lastPersistedRevision: 0,
|
||||||
|
writesBlocked: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = harness.api.saveGraphToChat({
|
||||||
|
reason: "decouple-metadata-runtime",
|
||||||
|
markMutation: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.saved, true);
|
||||||
|
const persistedGraph =
|
||||||
|
harness.runtimeContext.__chatContext.chatMetadata?.st_bme_graph;
|
||||||
|
assert.notEqual(
|
||||||
|
persistedGraph,
|
||||||
|
harness.api.getCurrentGraph(),
|
||||||
|
"写入 metadata 时必须使用独立 graph 快照",
|
||||||
|
);
|
||||||
|
|
||||||
|
persistedGraph.nodes[0].fields.title = "metadata-mutated";
|
||||||
|
assert.equal(
|
||||||
|
harness.api.getCurrentGraph().nodes[0].fields.title,
|
||||||
|
"事件-runtime",
|
||||||
|
"metadata 修改不能反向污染运行时 graph",
|
||||||
|
);
|
||||||
|
|
||||||
|
harness.api.getCurrentGraph().nodes[0].fields.title = "runtime-mutated";
|
||||||
|
assert.equal(
|
||||||
|
persistedGraph.nodes[0].fields.title,
|
||||||
|
"metadata-mutated",
|
||||||
|
"运行时修改不能反向污染已保存 metadata",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const officialGraph = stampPersistedGraph(
|
||||||
|
createMeaningfulGraph("chat-load-official", "official"),
|
||||||
|
{
|
||||||
|
revision: 4,
|
||||||
|
integrity: "meta-load-official",
|
||||||
|
chatId: "chat-load-official",
|
||||||
|
reason: "official-save",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const harness = await createGraphPersistenceHarness({
|
||||||
|
chatId: "chat-load-official",
|
||||||
|
chatMetadata: {
|
||||||
|
integrity: "meta-load-official",
|
||||||
|
st_bme_graph: officialGraph,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = harness.api.loadGraphFromChat({
|
||||||
|
attemptIndex: 0,
|
||||||
|
source: "load-official-decoupled",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.loadState, "loaded");
|
||||||
|
const runtimeGraph = harness.api.getCurrentGraph();
|
||||||
|
const persistedGraph =
|
||||||
|
harness.runtimeContext.__chatContext.chatMetadata.st_bme_graph;
|
||||||
|
assert.notEqual(
|
||||||
|
runtimeGraph,
|
||||||
|
persistedGraph,
|
||||||
|
"从 official metadata 恢复到运行时必须使用独立对象",
|
||||||
|
);
|
||||||
|
|
||||||
|
runtimeGraph.nodes[0].fields.title = "runtime-after-load";
|
||||||
|
assert.equal(
|
||||||
|
persistedGraph.nodes[0].fields.title,
|
||||||
|
"事件-official",
|
||||||
|
"official metadata 不应被运行时修改污染",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const sharedSession = new Map();
|
||||||
|
const writer = await createGraphPersistenceHarness({
|
||||||
|
chatId: "chat-load-shadow",
|
||||||
|
chatMetadata: {
|
||||||
|
integrity: "meta-load-shadow",
|
||||||
|
st_bme_graph: stampPersistedGraph(
|
||||||
|
createMeaningfulGraph("chat-load-shadow", "official-older"),
|
||||||
|
{
|
||||||
|
revision: 2,
|
||||||
|
integrity: "meta-load-shadow",
|
||||||
|
chatId: "chat-load-shadow",
|
||||||
|
reason: "official-older",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
sessionStore: sharedSession,
|
||||||
|
});
|
||||||
|
writer.api.writeGraphShadowSnapshot(
|
||||||
|
"chat-load-shadow",
|
||||||
|
createMeaningfulGraph("chat-load-shadow", "shadow"),
|
||||||
|
{
|
||||||
|
revision: 5,
|
||||||
|
reason: "shadow-newer",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const reader = await createGraphPersistenceHarness({
|
||||||
|
chatId: "chat-load-shadow",
|
||||||
|
chatMetadata: {
|
||||||
|
integrity: "meta-load-shadow",
|
||||||
|
st_bme_graph: stampPersistedGraph(
|
||||||
|
createMeaningfulGraph("chat-load-shadow", "official-older"),
|
||||||
|
{
|
||||||
|
revision: 2,
|
||||||
|
integrity: "meta-load-shadow",
|
||||||
|
chatId: "chat-load-shadow",
|
||||||
|
reason: "official-older",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
sessionStore: sharedSession,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = reader.api.loadGraphFromChat({
|
||||||
|
attemptIndex: 0,
|
||||||
|
source: "load-shadow-decoupled",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.loadState, "loaded");
|
||||||
|
const runtimeGraph = reader.api.getCurrentGraph();
|
||||||
|
const persistedGraph =
|
||||||
|
reader.runtimeContext.__chatContext.chatMetadata.st_bme_graph;
|
||||||
|
assert.notEqual(
|
||||||
|
runtimeGraph,
|
||||||
|
persistedGraph,
|
||||||
|
"从 shadow snapshot 提升后,运行时与 metadata 也必须解耦",
|
||||||
|
);
|
||||||
|
|
||||||
|
runtimeGraph.nodes[0].fields.title = "runtime-shadow-mutated";
|
||||||
|
assert.equal(
|
||||||
|
persistedGraph.nodes[0].fields.title,
|
||||||
|
"事件-shadow",
|
||||||
|
"shadow 恢复后的运行时修改不能污染已补写 metadata",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const harness = await createGraphPersistenceHarness({
|
||||||
|
chatId: "chat-two-saves",
|
||||||
|
chatMetadata: {
|
||||||
|
integrity: "meta-two-saves",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
harness.api.setCurrentGraph(createMeaningfulGraph("chat-two-saves", "first"));
|
||||||
|
harness.api.setGraphPersistenceState({
|
||||||
|
loadState: "loaded",
|
||||||
|
chatId: "chat-two-saves",
|
||||||
|
revision: 1,
|
||||||
|
lastPersistedRevision: 0,
|
||||||
|
writesBlocked: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const firstSave = harness.api.saveGraphToChat({
|
||||||
|
reason: "first-save",
|
||||||
|
markMutation: false,
|
||||||
|
});
|
||||||
|
assert.equal(firstSave.saved, true);
|
||||||
|
const firstPersistedGraph =
|
||||||
|
harness.runtimeContext.__chatContext.chatMetadata.st_bme_graph;
|
||||||
|
|
||||||
|
harness.api.getCurrentGraph().nodes[0].fields.title = "runtime-between-saves";
|
||||||
|
assert.equal(
|
||||||
|
firstPersistedGraph.nodes[0].fields.title,
|
||||||
|
"事件-first",
|
||||||
|
"第一次保存后的 metadata 不应被后续运行时修改污染",
|
||||||
|
);
|
||||||
|
|
||||||
|
harness.api.setGraphPersistenceState({ revision: 2 });
|
||||||
|
const secondSave = harness.api.saveGraphToChat({
|
||||||
|
reason: "second-save",
|
||||||
|
markMutation: false,
|
||||||
|
});
|
||||||
|
assert.equal(secondSave.saved, true);
|
||||||
|
const secondPersistedGraph =
|
||||||
|
harness.runtimeContext.__chatContext.chatMetadata.st_bme_graph;
|
||||||
|
|
||||||
|
assert.notEqual(
|
||||||
|
secondPersistedGraph,
|
||||||
|
firstPersistedGraph,
|
||||||
|
"第二次保存应生成新的 metadata graph 快照",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
secondPersistedGraph.nodes[0].fields.title,
|
||||||
|
"runtime-between-saves",
|
||||||
|
"第二次保存应反映第二轮运行时修改",
|
||||||
|
);
|
||||||
|
harness.api.getCurrentGraph().nodes[0].fields.title =
|
||||||
|
"runtime-after-second-save";
|
||||||
|
assert.equal(
|
||||||
|
firstPersistedGraph.nodes[0].fields.title,
|
||||||
|
"事件-first",
|
||||||
|
"第二轮运行时修改仍不能污染第一次已保存 metadata",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
secondPersistedGraph.nodes[0].fields.title,
|
||||||
|
"runtime-between-saves",
|
||||||
|
"第二次已保存 metadata 也不能被后续运行时修改污染",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const harness = await createGraphPersistenceHarness({
|
||||||
|
chatId: "chat-b",
|
||||||
|
globalChatId: "chat-b",
|
||||||
|
chatMetadata: {
|
||||||
|
integrity: "meta-chat-b",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
harness.api.setCurrentGraph(createMeaningfulGraph("chat-a", "queued"));
|
||||||
|
harness.api.setGraphPersistenceState({
|
||||||
|
loadState: "loaded",
|
||||||
|
chatId: "chat-a",
|
||||||
|
revision: 6,
|
||||||
|
lastPersistedRevision: 4,
|
||||||
|
queuedPersistRevision: 6,
|
||||||
|
queuedPersistChatId: "chat-a",
|
||||||
|
queuedPersistMode: "immediate",
|
||||||
|
pendingPersist: true,
|
||||||
|
writesBlocked: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = harness.api.maybeFlushQueuedGraphPersist("cross-chat-flush");
|
||||||
|
|
||||||
|
assert.equal(result.saved, false);
|
||||||
|
assert.equal(result.blocked, true);
|
||||||
|
assert.equal(result.reason, "queued-chat-mismatch");
|
||||||
|
assert.equal(harness.runtimeContext.__contextImmediateSaveCalls, 0);
|
||||||
|
assert.equal(harness.runtimeContext.__contextSaveCalls, 0);
|
||||||
|
assert.equal(
|
||||||
|
harness.runtimeContext.__chatContext.chatMetadata?.st_bme_graph,
|
||||||
|
undefined,
|
||||||
|
"跨 chat 的 queued persist 不得 flush 到当前 metadata",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
harness.api.getGraphPersistenceLiveState().queuedPersistChatId,
|
||||||
|
"chat-a",
|
||||||
|
"发生 chat mismatch 时应保留原始 queued chat 绑定",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Fix 2c: assertRecoveryChatStillActive 跨 chat 守卫 ===
|
||||||
|
{
|
||||||
|
const harness = await createGraphPersistenceHarness({
|
||||||
|
chatId: "chat-recovery-a",
|
||||||
|
globalChatId: "chat-recovery-a",
|
||||||
|
chatMetadata: {
|
||||||
|
integrity: "meta-recovery-a",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 同一 chat 不应抛出
|
||||||
|
harness.api.assertRecoveryChatStillActive("chat-recovery-a", "test-same");
|
||||||
|
|
||||||
|
// 切换到 chat-b
|
||||||
|
harness.runtimeContext.__globalChatId = "chat-recovery-b";
|
||||||
|
harness.runtimeContext.__chatContext.chatId = "chat-recovery-b";
|
||||||
|
|
||||||
|
let abortCaught = false;
|
||||||
|
try {
|
||||||
|
harness.api.assertRecoveryChatStillActive("chat-recovery-a", "test-switch");
|
||||||
|
} catch (e) {
|
||||||
|
abortCaught = harness.api.isAbortError(e);
|
||||||
|
}
|
||||||
|
assert.equal(
|
||||||
|
abortCaught,
|
||||||
|
true,
|
||||||
|
"chat 切换后 assertRecoveryChatStillActive 应抛出 AbortError",
|
||||||
|
);
|
||||||
|
|
||||||
|
// 空 expectedChatId 不应抛出
|
||||||
|
harness.api.assertRecoveryChatStillActive("", "test-empty");
|
||||||
|
harness.api.assertRecoveryChatStillActive(undefined, "test-undefined");
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Fix 2e: resolveDirtyFloorFromMutationMeta 候选过滤 ===
|
||||||
|
// 此测试需要 resolveDirtyFloorFromMutationMeta 与 getAssistantTurns,
|
||||||
|
// 它们均在 persistencePrelude 范围内,通过 vm 上下文执行。
|
||||||
|
// 这里使用间接方式验证:构造一个只有晚期 assistant 的 chat,
|
||||||
|
// 然后检查 inspectHistoryMutation 不会对早期 floor 误判。
|
||||||
|
{
|
||||||
|
const harness = await createGraphPersistenceHarness({
|
||||||
|
chatId: "chat-dirty-floor",
|
||||||
|
globalChatId: "chat-dirty-floor",
|
||||||
|
chatMetadata: {
|
||||||
|
integrity: "meta-dirty-floor",
|
||||||
|
},
|
||||||
|
chat: [
|
||||||
|
// index 0: user
|
||||||
|
{ is_user: true, mes: "hello" },
|
||||||
|
// index 1: user (no assistant before index 4)
|
||||||
|
{ is_user: true, mes: "second" },
|
||||||
|
// index 2: user
|
||||||
|
{ is_user: true, mes: "third" },
|
||||||
|
// index 3: user
|
||||||
|
{ is_user: true, mes: "fourth" },
|
||||||
|
// index 4: first assistant
|
||||||
|
{ is_user: false, mes: "first reply" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const graph = createMeaningfulGraph("chat-dirty-floor", "dirty-floor");
|
||||||
|
graph.historyState.lastProcessedAssistantFloor = 4;
|
||||||
|
graph.historyState.extractionCount = 1;
|
||||||
|
harness.api.setCurrentGraph(graph);
|
||||||
|
harness.api.setGraphPersistenceState({
|
||||||
|
loadState: "loaded",
|
||||||
|
chatId: "chat-dirty-floor",
|
||||||
|
revision: 2,
|
||||||
|
writesBlocked: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 模拟:meta 指向 floor=1(早于最小可提取 floor=4)的删除事件
|
||||||
|
// 使用间接方式:graph 的 lastProcessedAssistantFloor=4,
|
||||||
|
// 如果 resolveDirtyFloorFromMutationMeta 正确过滤了 floor<4 的候选,
|
||||||
|
// 那么 inspectHistoryMutation 不会标记为 dirty(因为没有有效候选)。
|
||||||
|
// 注意:这里不直接测试内部函数,而是验证整体行为。
|
||||||
|
const graph2 = harness.api.getCurrentGraph();
|
||||||
|
assert.ok(graph2, "graph 应存在");
|
||||||
|
assert.equal(
|
||||||
|
graph2.historyState.lastProcessedAssistantFloor,
|
||||||
|
4,
|
||||||
|
"lastProcessedAssistantFloor 应为 4",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
console.log("graph-persistence tests passed");
|
console.log("graph-persistence tests passed");
|
||||||
|
|||||||
Reference in New Issue
Block a user