mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
Add Luker chat state persistence compatibility
This commit is contained in:
@@ -11,6 +11,9 @@ import { normalizeGraphRuntimeState } from "../runtime/runtime-state.js";
|
|||||||
export const MODULE_NAME = "st_bme";
|
export const MODULE_NAME = "st_bme";
|
||||||
export const GRAPH_METADATA_KEY = "st_bme_graph";
|
export const GRAPH_METADATA_KEY = "st_bme_graph";
|
||||||
export const GRAPH_COMMIT_MARKER_KEY = "st_bme_commit_marker";
|
export const GRAPH_COMMIT_MARKER_KEY = "st_bme_commit_marker";
|
||||||
|
export const GRAPH_CHAT_STATE_NAMESPACE = `${MODULE_NAME}_graph_state`;
|
||||||
|
export const GRAPH_CHAT_STATE_VERSION = 1;
|
||||||
|
export const GRAPH_CHAT_STATE_MAX_OPERATIONS = 4000;
|
||||||
export const GRAPH_PERSISTENCE_META_KEY = "__stBmePersistence";
|
export const GRAPH_PERSISTENCE_META_KEY = "__stBmePersistence";
|
||||||
export const GRAPH_LOAD_STATES = Object.freeze({
|
export const GRAPH_LOAD_STATES = Object.freeze({
|
||||||
NO_CHAT: "no-chat",
|
NO_CHAT: "no-chat",
|
||||||
@@ -374,6 +377,184 @@ export function writeChatMetadataPatch(context, patch = {}) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function canUseGraphChatState(context = null) {
|
||||||
|
return (
|
||||||
|
!!context &&
|
||||||
|
typeof context.getChatState === "function" &&
|
||||||
|
typeof context.updateChatState === "function"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeGraphChatStateSnapshot(snapshot = null) {
|
||||||
|
if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const version = Number(snapshot.version);
|
||||||
|
const revision = Number(snapshot.revision);
|
||||||
|
const serializedGraph = String(snapshot.serializedGraph || "");
|
||||||
|
const storageTier = String(snapshot.storageTier || "chat-state");
|
||||||
|
const chatId = normalizeIdentityValue(snapshot.chatId);
|
||||||
|
const integrity = normalizeIdentityValue(snapshot.integrity);
|
||||||
|
const commitMarker = normalizeGraphCommitMarker(snapshot.commitMarker);
|
||||||
|
|
||||||
|
if (!serializedGraph) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: Number.isFinite(version) && version > 0 ? version : GRAPH_CHAT_STATE_VERSION,
|
||||||
|
revision: Number.isFinite(revision) && revision > 0 ? revision : 0,
|
||||||
|
serializedGraph,
|
||||||
|
persistedAt: String(snapshot.persistedAt || ""),
|
||||||
|
updatedAt: String(snapshot.updatedAt || snapshot.persistedAt || ""),
|
||||||
|
reason: String(snapshot.reason || ""),
|
||||||
|
storageTier,
|
||||||
|
chatId,
|
||||||
|
integrity,
|
||||||
|
commitMarker,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildGraphChatStateSnapshot(
|
||||||
|
graph,
|
||||||
|
{
|
||||||
|
revision = 0,
|
||||||
|
storageTier = "chat-state",
|
||||||
|
accepted = true,
|
||||||
|
reason = "",
|
||||||
|
persistedAt = "",
|
||||||
|
updatedAt = "",
|
||||||
|
chatId = "",
|
||||||
|
integrity = "",
|
||||||
|
lastProcessedAssistantFloor = null,
|
||||||
|
extractionCount = null,
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
if (!graph) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const commitMarker = buildGraphCommitMarker(graph, {
|
||||||
|
revision,
|
||||||
|
storageTier,
|
||||||
|
accepted,
|
||||||
|
reason,
|
||||||
|
persistedAt,
|
||||||
|
chatId,
|
||||||
|
integrity,
|
||||||
|
lastProcessedAssistantFloor,
|
||||||
|
extractionCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
return normalizeGraphChatStateSnapshot({
|
||||||
|
version: GRAPH_CHAT_STATE_VERSION,
|
||||||
|
revision,
|
||||||
|
serializedGraph: serializeGraph(graph),
|
||||||
|
persistedAt: String(persistedAt || new Date().toISOString()),
|
||||||
|
updatedAt: String(updatedAt || persistedAt || new Date().toISOString()),
|
||||||
|
reason: String(reason || ""),
|
||||||
|
storageTier: String(storageTier || "chat-state"),
|
||||||
|
chatId,
|
||||||
|
integrity,
|
||||||
|
commitMarker,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readGraphChatStateSnapshot(
|
||||||
|
context = null,
|
||||||
|
{ namespace = GRAPH_CHAT_STATE_NAMESPACE } = {},
|
||||||
|
) {
|
||||||
|
if (!canUseGraphChatState(context)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = await context.getChatState(namespace);
|
||||||
|
return normalizeGraphChatStateSnapshot(payload);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[ST-BME] 读取聊天侧车图谱失败:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function writeGraphChatStateSnapshot(
|
||||||
|
context = null,
|
||||||
|
graph = null,
|
||||||
|
{
|
||||||
|
namespace = GRAPH_CHAT_STATE_NAMESPACE,
|
||||||
|
revision = 0,
|
||||||
|
storageTier = "chat-state",
|
||||||
|
accepted = true,
|
||||||
|
reason = "",
|
||||||
|
chatId = "",
|
||||||
|
integrity = "",
|
||||||
|
lastProcessedAssistantFloor = null,
|
||||||
|
extractionCount = null,
|
||||||
|
maxOperations = GRAPH_CHAT_STATE_MAX_OPERATIONS,
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
if (!canUseGraphChatState(context) || !graph) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
updated: false,
|
||||||
|
snapshot: null,
|
||||||
|
reason: "chat-state-unavailable",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const snapshot = buildGraphChatStateSnapshot(graph, {
|
||||||
|
revision,
|
||||||
|
storageTier,
|
||||||
|
accepted,
|
||||||
|
reason,
|
||||||
|
chatId,
|
||||||
|
integrity,
|
||||||
|
lastProcessedAssistantFloor,
|
||||||
|
extractionCount,
|
||||||
|
});
|
||||||
|
if (!snapshot) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
updated: false,
|
||||||
|
snapshot: null,
|
||||||
|
reason: "chat-state-build-failed",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await context.updateChatState(
|
||||||
|
namespace,
|
||||||
|
() => snapshot,
|
||||||
|
{
|
||||||
|
maxOperations,
|
||||||
|
asyncDiff: false,
|
||||||
|
maxRetries: 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
ok: result?.ok === true,
|
||||||
|
updated: result?.updated !== false,
|
||||||
|
snapshot,
|
||||||
|
reason:
|
||||||
|
result?.ok === true
|
||||||
|
? result?.updated === false
|
||||||
|
? "chat-state-noop"
|
||||||
|
: "chat-state-saved"
|
||||||
|
: "chat-state-save-failed",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[ST-BME] 写入聊天侧车图谱失败:", error);
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
updated: false,
|
||||||
|
snapshot,
|
||||||
|
reason: "chat-state-save-failed",
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeGraphCommitMarker(marker = null) {
|
export function normalizeGraphCommitMarker(marker = null) {
|
||||||
if (!marker || typeof marker !== "object" || Array.isArray(marker)) {
|
if (!marker || typeof marker !== "object" || Array.isArray(marker)) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
631
index.js
631
index.js
@@ -97,9 +97,11 @@ import {
|
|||||||
} from "./maintenance/hierarchical-summary.js";
|
} from "./maintenance/hierarchical-summary.js";
|
||||||
import {
|
import {
|
||||||
buildGraphCommitMarker,
|
buildGraphCommitMarker,
|
||||||
|
canUseGraphChatState,
|
||||||
detectIndexedDbSnapshotCommitMarkerMismatch,
|
detectIndexedDbSnapshotCommitMarkerMismatch,
|
||||||
findGraphShadowSnapshotByIntegrity,
|
findGraphShadowSnapshotByIntegrity,
|
||||||
getAcceptedCommitMarkerRevision,
|
getAcceptedCommitMarkerRevision,
|
||||||
|
GRAPH_CHAT_STATE_NAMESPACE,
|
||||||
GRAPH_LOAD_PENDING_CHAT_ID,
|
GRAPH_LOAD_PENDING_CHAT_ID,
|
||||||
GRAPH_LOAD_STATES,
|
GRAPH_LOAD_STATES,
|
||||||
GRAPH_COMMIT_MARKER_KEY,
|
GRAPH_COMMIT_MARKER_KEY,
|
||||||
@@ -115,9 +117,11 @@ import {
|
|||||||
removeGraphShadowSnapshot,
|
removeGraphShadowSnapshot,
|
||||||
rememberGraphIdentityAlias,
|
rememberGraphIdentityAlias,
|
||||||
readGraphCommitMarker,
|
readGraphCommitMarker,
|
||||||
|
readGraphChatStateSnapshot,
|
||||||
resolveGraphIdentityAliasByHostChatId,
|
resolveGraphIdentityAliasByHostChatId,
|
||||||
stampGraphPersistenceMeta,
|
stampGraphPersistenceMeta,
|
||||||
writeChatMetadataPatch,
|
writeChatMetadataPatch,
|
||||||
|
writeGraphChatStateSnapshot,
|
||||||
writeGraphShadowSnapshot,
|
writeGraphShadowSnapshot,
|
||||||
} from "./graph/graph-persistence.js";
|
} from "./graph/graph-persistence.js";
|
||||||
import {
|
import {
|
||||||
@@ -688,6 +692,8 @@ const bmeIndexedDbLoadInFlightByChatId = new Map();
|
|||||||
const bmeIndexedDbWriteInFlightByChatId = new Map();
|
const bmeIndexedDbWriteInFlightByChatId = new Map();
|
||||||
const bmeIndexedDbLegacyMigrationInFlightByChatId = new Map();
|
const bmeIndexedDbLegacyMigrationInFlightByChatId = new Map();
|
||||||
const bmeIndexedDbLatestQueuedRevisionByChatId = new Map();
|
const bmeIndexedDbLatestQueuedRevisionByChatId = new Map();
|
||||||
|
const bmeChatStateSnapshotCacheByChatId = new Map();
|
||||||
|
const bmeChatStateLoadInFlightByChatId = new Map();
|
||||||
const PENDING_GRAPH_PERSIST_RETRY_DELAYS_MS = [500, 1500, 5000];
|
const PENDING_GRAPH_PERSIST_RETRY_DELAYS_MS = [500, 1500, 5000];
|
||||||
const PENDING_GRAPH_PERSIST_MAX_RETRY_ATTEMPTS = 5;
|
const PENDING_GRAPH_PERSIST_MAX_RETRY_ATTEMPTS = 5;
|
||||||
const BME_INDEXEDDB_FALLBACK_LOAD_STATE_SET = new Set([
|
const BME_INDEXEDDB_FALLBACK_LOAD_STATE_SET = new Set([
|
||||||
@@ -4329,6 +4335,382 @@ function readCachedIndexedDbSnapshot(chatId) {
|
|||||||
return cacheEntry.snapshot;
|
return cacheEntry.snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cacheChatStateSnapshot(chatId, snapshot = null) {
|
||||||
|
const normalizedChatId = normalizeChatIdCandidate(chatId);
|
||||||
|
if (!normalizedChatId || !snapshot || typeof snapshot !== "object") return;
|
||||||
|
bmeChatStateSnapshotCacheByChatId.set(normalizedChatId, {
|
||||||
|
chatId: normalizedChatId,
|
||||||
|
revision: Number(snapshot?.revision || 0),
|
||||||
|
snapshot,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function readCachedChatStateSnapshot(chatId) {
|
||||||
|
const normalizedChatId = normalizeChatIdCandidate(chatId);
|
||||||
|
if (!normalizedChatId) return null;
|
||||||
|
const cacheEntry = bmeChatStateSnapshotCacheByChatId.get(normalizedChatId);
|
||||||
|
if (!cacheEntry?.snapshot) return null;
|
||||||
|
return cacheEntry.snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
function canUseHostGraphChatStatePersistence(context = getContext()) {
|
||||||
|
return canUseGraphChatState(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectPreferredCommitMarker(...candidates) {
|
||||||
|
let bestMarker = null;
|
||||||
|
let bestRevision = 0;
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const revision = getAcceptedCommitMarkerRevision(candidate);
|
||||||
|
if (revision > bestRevision) {
|
||||||
|
bestRevision = revision;
|
||||||
|
bestMarker = candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestMarker || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function persistGraphToHostChatState(
|
||||||
|
context = getContext(),
|
||||||
|
{
|
||||||
|
graph = currentGraph,
|
||||||
|
revision = graphPersistenceState.revision,
|
||||||
|
reason = "graph-chat-state",
|
||||||
|
storageTier = "chat-state",
|
||||||
|
accepted = true,
|
||||||
|
lastProcessedAssistantFloor = null,
|
||||||
|
extractionCount: nextExtractionCount = null,
|
||||||
|
mode = "primary",
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
if (!context || !graph || !canUseHostGraphChatStatePersistence(context)) {
|
||||||
|
return {
|
||||||
|
saved: false,
|
||||||
|
accepted: false,
|
||||||
|
reason: "chat-state-unavailable",
|
||||||
|
revision,
|
||||||
|
storageTier,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const chatId = getCurrentChatId(context);
|
||||||
|
if (!chatId) {
|
||||||
|
return {
|
||||||
|
saved: false,
|
||||||
|
accepted: false,
|
||||||
|
reason: "missing-chat-id",
|
||||||
|
revision,
|
||||||
|
storageTier,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedIdentity = resolveCurrentChatIdentity(context);
|
||||||
|
const nextIntegrity =
|
||||||
|
getChatMetadataIntegrity(context) ||
|
||||||
|
normalizeChatIdCandidate(resolvedIdentity?.integrity) ||
|
||||||
|
graphPersistenceState.metadataIntegrity;
|
||||||
|
const persistedGraph = cloneGraphForPersistence(graph, chatId);
|
||||||
|
stampGraphPersistenceMeta(persistedGraph, {
|
||||||
|
revision,
|
||||||
|
reason: `chat-state:${String(reason || "graph-chat-state")}`,
|
||||||
|
chatId,
|
||||||
|
integrity: nextIntegrity,
|
||||||
|
});
|
||||||
|
|
||||||
|
const writeResult = await writeGraphChatStateSnapshot(
|
||||||
|
context,
|
||||||
|
persistedGraph,
|
||||||
|
{
|
||||||
|
namespace: GRAPH_CHAT_STATE_NAMESPACE,
|
||||||
|
revision,
|
||||||
|
storageTier,
|
||||||
|
accepted,
|
||||||
|
reason,
|
||||||
|
chatId,
|
||||||
|
integrity: nextIntegrity,
|
||||||
|
lastProcessedAssistantFloor,
|
||||||
|
extractionCount: nextExtractionCount,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!writeResult?.ok || !writeResult?.snapshot) {
|
||||||
|
updateGraphPersistenceState({
|
||||||
|
dualWriteLastResult: {
|
||||||
|
action: "save",
|
||||||
|
target: "chat-state",
|
||||||
|
success: false,
|
||||||
|
chatId,
|
||||||
|
revision: Number(revision || 0),
|
||||||
|
reason: String(reason || "graph-chat-state"),
|
||||||
|
mode: String(mode || "primary"),
|
||||||
|
error: writeResult?.error?.message || writeResult?.reason || "chat-state-save-failed",
|
||||||
|
at: Date.now(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
saved: false,
|
||||||
|
accepted: false,
|
||||||
|
reason: writeResult?.reason || "chat-state-save-failed",
|
||||||
|
revision,
|
||||||
|
storageTier,
|
||||||
|
error: writeResult?.error || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheChatStateSnapshot(chatId, writeResult.snapshot);
|
||||||
|
rememberResolvedGraphIdentityAlias(context, chatId);
|
||||||
|
updateGraphPersistenceState({
|
||||||
|
metadataIntegrity: String(nextIntegrity || graphPersistenceState.metadataIntegrity || ""),
|
||||||
|
lastPersistReason: String(reason || ""),
|
||||||
|
lastPersistMode:
|
||||||
|
mode === "mirror" ? "chat-state-mirror" : "chat-state",
|
||||||
|
lastAcceptedRevision:
|
||||||
|
accepted === true
|
||||||
|
? Math.max(
|
||||||
|
Number(graphPersistenceState.lastAcceptedRevision || 0),
|
||||||
|
Number(writeResult.snapshot.revision || revision || 0),
|
||||||
|
)
|
||||||
|
: Number(graphPersistenceState.lastAcceptedRevision || 0),
|
||||||
|
dualWriteLastResult: {
|
||||||
|
action: "save",
|
||||||
|
target: "chat-state",
|
||||||
|
success: true,
|
||||||
|
chatId,
|
||||||
|
revision: Number(writeResult.snapshot.revision || revision || 0),
|
||||||
|
reason: String(reason || "graph-chat-state"),
|
||||||
|
mode: String(mode || "primary"),
|
||||||
|
at: Date.now(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (mode !== "mirror") {
|
||||||
|
clearPendingGraphPersistRetry();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
saved: true,
|
||||||
|
accepted,
|
||||||
|
chatId,
|
||||||
|
revision: Number(writeResult.snapshot.revision || revision || 0),
|
||||||
|
reason: String(reason || "graph-chat-state"),
|
||||||
|
saveMode: mode === "mirror" ? "chat-state-mirror" : "chat-state",
|
||||||
|
storageTier,
|
||||||
|
snapshot: writeResult.snapshot,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadGraphFromChatState(
|
||||||
|
chatId,
|
||||||
|
{
|
||||||
|
source = "chat-state-probe",
|
||||||
|
attemptIndex = 0,
|
||||||
|
allowOverride = false,
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
const normalizedChatId = normalizeChatIdCandidate(chatId);
|
||||||
|
const context = getContext();
|
||||||
|
if (!normalizedChatId) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
loaded: false,
|
||||||
|
reason: "chat-state-missing-chat-id",
|
||||||
|
chatId: "",
|
||||||
|
attemptIndex,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!canUseHostGraphChatStatePersistence(context)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
loaded: false,
|
||||||
|
reason: "chat-state-unavailable",
|
||||||
|
chatId: normalizedChatId,
|
||||||
|
attemptIndex,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload =
|
||||||
|
(await readGraphChatStateSnapshot(context, {
|
||||||
|
namespace: GRAPH_CHAT_STATE_NAMESPACE,
|
||||||
|
})) || readCachedChatStateSnapshot(normalizedChatId);
|
||||||
|
if (!payload?.serializedGraph) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
loaded: false,
|
||||||
|
reason: "chat-state-empty",
|
||||||
|
chatId: normalizedChatId,
|
||||||
|
attemptIndex,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
cacheChatStateSnapshot(normalizedChatId, payload);
|
||||||
|
|
||||||
|
let chatStateGraph = null;
|
||||||
|
try {
|
||||||
|
chatStateGraph = cloneGraphForPersistence(
|
||||||
|
normalizeGraphRuntimeState(
|
||||||
|
deserializeGraph(payload.serializedGraph),
|
||||||
|
normalizedChatId,
|
||||||
|
),
|
||||||
|
normalizedChatId,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[ST-BME] 聊天侧车图谱反序列化失败:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
loaded: false,
|
||||||
|
reason: "chat-state-deserialize-failed",
|
||||||
|
chatId: normalizedChatId,
|
||||||
|
attemptIndex,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGraphEffectivelyEmpty(chatStateGraph)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
loaded: false,
|
||||||
|
reason: "chat-state-empty",
|
||||||
|
chatId: normalizedChatId,
|
||||||
|
attemptIndex,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const revision = Math.max(
|
||||||
|
1,
|
||||||
|
Number(payload.revision || getGraphPersistedRevision(chatStateGraph) || 1),
|
||||||
|
);
|
||||||
|
const integrity =
|
||||||
|
normalizeChatIdCandidate(payload.integrity) ||
|
||||||
|
getChatMetadataIntegrity(context) ||
|
||||||
|
graphPersistenceState.metadataIntegrity;
|
||||||
|
stampGraphPersistenceMeta(chatStateGraph, {
|
||||||
|
revision,
|
||||||
|
reason: `chat-state:${String(source || "chat-state-probe")}`,
|
||||||
|
chatId: normalizedChatId,
|
||||||
|
integrity,
|
||||||
|
});
|
||||||
|
|
||||||
|
const snapshot = buildSnapshotFromGraph(chatStateGraph, {
|
||||||
|
chatId: normalizedChatId,
|
||||||
|
revision,
|
||||||
|
meta: {
|
||||||
|
storagePrimary: "chat-state",
|
||||||
|
lastMutationReason: String(payload.reason || source || "chat-state"),
|
||||||
|
integrity,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const shadowSnapshot = resolveCompatibleGraphShadowSnapshot(
|
||||||
|
resolveCurrentChatIdentity(context),
|
||||||
|
);
|
||||||
|
const shadowDecision = shouldPreferShadowSnapshotOverOfficial(
|
||||||
|
chatStateGraph,
|
||||||
|
shadowSnapshot,
|
||||||
|
);
|
||||||
|
if (shadowSnapshot && shadowDecision?.prefer) {
|
||||||
|
return applyShadowSnapshotToRuntime(normalizedChatId, shadowSnapshot, {
|
||||||
|
source: `${source}:shadow-over-chat-state`,
|
||||||
|
attemptIndex,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const effectiveCommitMarker = selectPreferredCommitMarker(
|
||||||
|
payload.commitMarker,
|
||||||
|
getChatCommitMarker(context),
|
||||||
|
);
|
||||||
|
const commitMarkerMismatch = detectIndexedDbSnapshotCommitMarkerMismatch(
|
||||||
|
snapshot,
|
||||||
|
effectiveCommitMarker,
|
||||||
|
);
|
||||||
|
if (commitMarkerMismatch.mismatched) {
|
||||||
|
if (
|
||||||
|
shadowSnapshot &&
|
||||||
|
Number(shadowSnapshot.revision || 0) >=
|
||||||
|
Number(commitMarkerMismatch.markerRevision || 0)
|
||||||
|
) {
|
||||||
|
return applyShadowSnapshotToRuntime(normalizedChatId, shadowSnapshot, {
|
||||||
|
source: `${source}:shadow-beats-chat-state-marker`,
|
||||||
|
attemptIndex,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return applyPersistMismatchBlockedState(
|
||||||
|
normalizedChatId,
|
||||||
|
{
|
||||||
|
...commitMarkerMismatch,
|
||||||
|
marker: commitMarkerMismatch.marker || effectiveCommitMarker,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: `${source}:chat-state-marker`,
|
||||||
|
attemptIndex,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldAllowOverride =
|
||||||
|
allowOverride ||
|
||||||
|
BME_INDEXEDDB_FALLBACK_LOAD_STATE_SET.has(graphPersistenceState.loadState) ||
|
||||||
|
graphPersistenceState.storagePrimary === "chat-state" ||
|
||||||
|
revision >= normalizeIndexedDbRevision(graphPersistenceState.revision);
|
||||||
|
if (!shouldAllowOverride) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
loaded: false,
|
||||||
|
reason: "chat-state-stale",
|
||||||
|
chatId: normalizedChatId,
|
||||||
|
attemptIndex,
|
||||||
|
revision,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getCurrentChatId() !== normalizedChatId) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
loaded: false,
|
||||||
|
reason: "chat-state-chat-switched",
|
||||||
|
chatId: normalizedChatId,
|
||||||
|
attemptIndex,
|
||||||
|
revision,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return applyIndexedDbSnapshotToRuntime(normalizedChatId, snapshot, {
|
||||||
|
source,
|
||||||
|
attemptIndex,
|
||||||
|
storagePrimary: "chat-state",
|
||||||
|
storageMode: "chat-state",
|
||||||
|
statusLabel: "聊天侧车",
|
||||||
|
reasonPrefix: "chat-state",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleGraphChatStateProbe(chatId, options = {}) {
|
||||||
|
const normalizedChatId = normalizeChatIdCandidate(chatId);
|
||||||
|
if (
|
||||||
|
!normalizedChatId ||
|
||||||
|
!canUseHostGraphChatStatePersistence(getContext()) ||
|
||||||
|
bmeChatStateLoadInFlightByChatId.has(normalizedChatId)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleBmeIndexedDbTask(() => {
|
||||||
|
const loadPromise = loadGraphFromChatState(normalizedChatId, options)
|
||||||
|
.catch((error) => {
|
||||||
|
console.warn("[ST-BME] 聊天侧车后台加载失败:", error);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (
|
||||||
|
bmeChatStateLoadInFlightByChatId.get(normalizedChatId) === loadPromise
|
||||||
|
) {
|
||||||
|
bmeChatStateLoadInFlightByChatId.delete(normalizedChatId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bmeChatStateLoadInFlightByChatId.set(normalizedChatId, loadPromise);
|
||||||
|
return loadPromise;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function readLegacyGraphFromChatMetadata(chatId, context = getContext()) {
|
function readLegacyGraphFromChatMetadata(chatId, context = getContext()) {
|
||||||
const normalizedChatId = normalizeChatIdCandidate(chatId);
|
const normalizedChatId = normalizeChatIdCandidate(chatId);
|
||||||
if (!normalizedChatId) return null;
|
if (!normalizedChatId) return null;
|
||||||
@@ -4813,7 +5195,14 @@ function applyIndexedDbEmptyToRuntime(
|
|||||||
function applyIndexedDbSnapshotToRuntime(
|
function applyIndexedDbSnapshotToRuntime(
|
||||||
chatId,
|
chatId,
|
||||||
snapshot,
|
snapshot,
|
||||||
{ source = "indexeddb", attemptIndex = 0 } = {},
|
{
|
||||||
|
source = "indexeddb",
|
||||||
|
attemptIndex = 0,
|
||||||
|
storagePrimary = "indexeddb",
|
||||||
|
storageMode = storagePrimary,
|
||||||
|
statusLabel = "IndexedDB",
|
||||||
|
reasonPrefix = "indexeddb",
|
||||||
|
} = {},
|
||||||
) {
|
) {
|
||||||
const normalizedChatId = normalizeChatIdCandidate(chatId);
|
const normalizedChatId = normalizeChatIdCandidate(chatId);
|
||||||
syncCommitMarkerToPersistenceState(getContext());
|
syncCommitMarkerToPersistenceState(getContext());
|
||||||
@@ -4821,7 +5210,7 @@ function applyIndexedDbSnapshotToRuntime(
|
|||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
reason: "indexeddb-empty",
|
reason: `${reasonPrefix}-empty`,
|
||||||
chatId: normalizedChatId,
|
chatId: normalizedChatId,
|
||||||
attemptIndex,
|
attemptIndex,
|
||||||
};
|
};
|
||||||
@@ -4836,30 +5225,34 @@ function applyIndexedDbSnapshotToRuntime(
|
|||||||
snapshot,
|
snapshot,
|
||||||
);
|
);
|
||||||
if (staleDecision.stale) {
|
if (staleDecision.stale) {
|
||||||
updateGraphPersistenceState({
|
const persistencePatch = {
|
||||||
storagePrimary:
|
storagePrimary: graphPersistenceState.storagePrimary || storagePrimary,
|
||||||
graphPersistenceState.storagePrimary || "indexeddb",
|
storageMode: graphPersistenceState.storageMode || storageMode,
|
||||||
storageMode: graphPersistenceState.storageMode || "indexeddb",
|
|
||||||
indexedDbRevision: Math.max(
|
|
||||||
graphPersistenceState.indexedDbRevision || 0,
|
|
||||||
revision,
|
|
||||||
),
|
|
||||||
metadataIntegrity:
|
metadataIntegrity:
|
||||||
getChatMetadataIntegrity(getContext()) ||
|
getChatMetadataIntegrity(getContext()) ||
|
||||||
graphPersistenceState.metadataIntegrity,
|
graphPersistenceState.metadataIntegrity,
|
||||||
indexedDbLastError: "",
|
indexedDbLastError: "",
|
||||||
dualWriteLastResult: {
|
dualWriteLastResult: {
|
||||||
action: "load",
|
action: "load",
|
||||||
source: String(source || "indexeddb"),
|
source: String(source || reasonPrefix),
|
||||||
success: false,
|
success: false,
|
||||||
rejected: true,
|
rejected: true,
|
||||||
reason: "indexeddb-stale-runtime",
|
reason: `${reasonPrefix}-stale-runtime`,
|
||||||
revision,
|
revision,
|
||||||
staleDetail: cloneRuntimeDebugValue(staleDecision, null),
|
staleDetail: cloneRuntimeDebugValue(staleDecision, null),
|
||||||
at: Date.now(),
|
at: Date.now(),
|
||||||
},
|
},
|
||||||
|
};
|
||||||
|
if (storagePrimary === "indexeddb") {
|
||||||
|
persistencePatch.indexedDbRevision = Math.max(
|
||||||
|
graphPersistenceState.indexedDbRevision || 0,
|
||||||
|
revision,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
updateGraphPersistenceState({
|
||||||
|
...persistencePatch,
|
||||||
});
|
});
|
||||||
debugDebug("[ST-BME] 已拒绝用较旧 IndexedDB 快照覆盖当前运行时图谱", {
|
debugDebug(`[ST-BME] 已拒绝用较旧 ${statusLabel} 快照覆盖当前运行时图谱`, {
|
||||||
chatId: normalizedChatId,
|
chatId: normalizedChatId,
|
||||||
source,
|
source,
|
||||||
revision,
|
revision,
|
||||||
@@ -4868,7 +5261,7 @@ function applyIndexedDbSnapshotToRuntime(
|
|||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
reason: "indexeddb-stale-runtime",
|
reason: `${reasonPrefix}-stale-runtime`,
|
||||||
chatId: normalizedChatId,
|
chatId: normalizedChatId,
|
||||||
attemptIndex,
|
attemptIndex,
|
||||||
revision,
|
revision,
|
||||||
@@ -4883,25 +5276,30 @@ function applyIndexedDbSnapshotToRuntime(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const failureReason =
|
const failureReason =
|
||||||
error?.code === "BME_SNAPSHOT_INTEGRITY_ERROR"
|
error?.code === "BME_SNAPSHOT_INTEGRITY_ERROR"
|
||||||
? "indexeddb-snapshot-integrity-rejected"
|
? `${reasonPrefix}-snapshot-integrity-rejected`
|
||||||
: "indexeddb-snapshot-load-failed";
|
: `${reasonPrefix}-snapshot-load-failed`;
|
||||||
updateGraphPersistenceState({
|
const persistencePatch = {
|
||||||
storagePrimary: "indexeddb",
|
storagePrimary,
|
||||||
storageMode: "indexeddb",
|
storageMode,
|
||||||
dbReady: true,
|
dbReady: true,
|
||||||
indexedDbRevision: revision,
|
|
||||||
indexedDbLastError: error?.message || String(error),
|
indexedDbLastError: error?.message || String(error),
|
||||||
dualWriteLastResult: {
|
dualWriteLastResult: {
|
||||||
action: "load",
|
action: "load",
|
||||||
source: String(source || "indexeddb"),
|
source: String(source || reasonPrefix),
|
||||||
success: false,
|
success: false,
|
||||||
rejected: true,
|
rejected: true,
|
||||||
reason: failureReason,
|
reason: failureReason,
|
||||||
revision,
|
revision,
|
||||||
at: Date.now(),
|
at: Date.now(),
|
||||||
},
|
},
|
||||||
|
};
|
||||||
|
if (storagePrimary === "indexeddb") {
|
||||||
|
persistencePatch.indexedDbRevision = revision;
|
||||||
|
}
|
||||||
|
updateGraphPersistenceState({
|
||||||
|
...persistencePatch,
|
||||||
});
|
});
|
||||||
console.warn("[ST-BME] IndexedDB 图谱快照已拒绝加载", {
|
console.warn(`[ST-BME] ${statusLabel} 图谱快照已拒绝加载`, {
|
||||||
chatId: normalizedChatId,
|
chatId: normalizedChatId,
|
||||||
source,
|
source,
|
||||||
revision,
|
revision,
|
||||||
@@ -4925,7 +5323,7 @@ function applyIndexedDbSnapshotToRuntime(
|
|||||||
);
|
);
|
||||||
stampGraphPersistenceMeta(currentGraph, {
|
stampGraphPersistenceMeta(currentGraph, {
|
||||||
revision,
|
revision,
|
||||||
reason: `indexeddb:${String(source || "indexeddb")}`,
|
reason: `${reasonPrefix}:${String(source || reasonPrefix)}`,
|
||||||
chatId: normalizedChatId,
|
chatId: normalizedChatId,
|
||||||
integrity:
|
integrity:
|
||||||
normalizeChatIdCandidate(snapshot?.meta?.integrity) ||
|
normalizeChatIdCandidate(snapshot?.meta?.integrity) ||
|
||||||
@@ -4940,29 +5338,29 @@ function applyIndexedDbSnapshotToRuntime(
|
|||||||
const restoredRecallUi = restoreRecallUiStateFromPersistence(
|
const restoredRecallUi = restoreRecallUiStateFromPersistence(
|
||||||
getContext()?.chat,
|
getContext()?.chat,
|
||||||
);
|
);
|
||||||
runtimeStatus = createUiStatus("待命", "已从 IndexedDB 加载聊天图谱", "idle");
|
runtimeStatus = createUiStatus("待命", `已从${statusLabel}加载聊天图谱`, "idle");
|
||||||
lastExtractionStatus = createUiStatus(
|
lastExtractionStatus = createUiStatus(
|
||||||
"待命",
|
"待命",
|
||||||
"已从 IndexedDB 加载聊天图谱,等待下一次提取",
|
`已从${statusLabel}加载聊天图谱,等待下一次提取`,
|
||||||
"idle",
|
"idle",
|
||||||
);
|
);
|
||||||
lastVectorStatus = createUiStatus(
|
lastVectorStatus = createUiStatus(
|
||||||
"待命",
|
"待命",
|
||||||
currentGraph.vectorIndexState?.lastWarning ||
|
currentGraph.vectorIndexState?.lastWarning ||
|
||||||
"已从 IndexedDB 加载聊天图谱,等待下一次向量任务",
|
`已从${statusLabel}加载聊天图谱,等待下一次向量任务`,
|
||||||
"idle",
|
"idle",
|
||||||
);
|
);
|
||||||
lastRecallStatus = createUiStatus(
|
lastRecallStatus = createUiStatus(
|
||||||
"待命",
|
"待命",
|
||||||
restoredRecallUi.restored
|
restoredRecallUi.restored
|
||||||
? "已从持久化召回记录恢复显示,等待下一次召回"
|
? "已从持久化召回记录恢复显示,等待下一次召回"
|
||||||
: "已从 IndexedDB 加载聊天图谱,等待下一次召回",
|
: `已从${statusLabel}加载聊天图谱,等待下一次召回`,
|
||||||
"idle",
|
"idle",
|
||||||
);
|
);
|
||||||
|
|
||||||
applyGraphLoadState(GRAPH_LOAD_STATES.LOADED, {
|
applyGraphLoadState(GRAPH_LOAD_STATES.LOADED, {
|
||||||
chatId: normalizedChatId,
|
chatId: normalizedChatId,
|
||||||
reason: `indexeddb:${source}`,
|
reason: `${reasonPrefix}:${source}`,
|
||||||
attemptIndex,
|
attemptIndex,
|
||||||
revision,
|
revision,
|
||||||
lastPersistedRevision: Math.max(
|
lastPersistedRevision: Math.max(
|
||||||
@@ -4977,16 +5375,15 @@ function applyIndexedDbSnapshotToRuntime(
|
|||||||
shadowSnapshotReason: "",
|
shadowSnapshotReason: "",
|
||||||
writesBlocked: false,
|
writesBlocked: false,
|
||||||
});
|
});
|
||||||
updateGraphPersistenceState({
|
const persistencePatch = {
|
||||||
storagePrimary: "indexeddb",
|
storagePrimary,
|
||||||
storageMode: "indexeddb",
|
storageMode,
|
||||||
dbReady: true,
|
dbReady: true,
|
||||||
persistMismatchReason: "",
|
persistMismatchReason: "",
|
||||||
indexedDbRevision: revision,
|
|
||||||
metadataIntegrity:
|
metadataIntegrity:
|
||||||
getChatMetadataIntegrity(getContext()) ||
|
getChatMetadataIntegrity(getContext()) ||
|
||||||
graphPersistenceState.metadataIntegrity,
|
graphPersistenceState.metadataIntegrity,
|
||||||
indexedDbLastError: "",
|
indexedDbLastError: storagePrimary === "indexeddb" ? "" : graphPersistenceState.indexedDbLastError,
|
||||||
lastAcceptedRevision: Math.max(
|
lastAcceptedRevision: Math.max(
|
||||||
Number(graphPersistenceState.lastAcceptedRevision || 0),
|
Number(graphPersistenceState.lastAcceptedRevision || 0),
|
||||||
revision,
|
revision,
|
||||||
@@ -4994,19 +5391,23 @@ function applyIndexedDbSnapshotToRuntime(
|
|||||||
lastSyncError: "",
|
lastSyncError: "",
|
||||||
dualWriteLastResult: {
|
dualWriteLastResult: {
|
||||||
action: "load",
|
action: "load",
|
||||||
source: String(source || "indexeddb"),
|
source: String(source || reasonPrefix),
|
||||||
success: true,
|
success: true,
|
||||||
reason: "indexeddb-loaded",
|
reason: `${reasonPrefix}-loaded`,
|
||||||
revision,
|
revision,
|
||||||
at: Date.now(),
|
at: Date.now(),
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
if (storagePrimary === "indexeddb") {
|
||||||
|
persistencePatch.indexedDbRevision = revision;
|
||||||
|
}
|
||||||
|
updateGraphPersistenceState(persistencePatch);
|
||||||
rememberResolvedGraphIdentityAlias(getContext(), normalizedChatId);
|
rememberResolvedGraphIdentityAlias(getContext(), normalizedChatId);
|
||||||
|
|
||||||
removeGraphShadowSnapshot(normalizedChatId);
|
removeGraphShadowSnapshot(normalizedChatId);
|
||||||
refreshPanelLiveState();
|
refreshPanelLiveState();
|
||||||
schedulePersistedRecallMessageUiRefresh(30);
|
schedulePersistedRecallMessageUiRefresh(30);
|
||||||
debugDebug("[ST-BME] 已从 IndexedDB 加载图谱", {
|
debugDebug(`[ST-BME] 已从${statusLabel}加载图谱`, {
|
||||||
chatId: normalizedChatId,
|
chatId: normalizedChatId,
|
||||||
source,
|
source,
|
||||||
revision,
|
revision,
|
||||||
@@ -5017,7 +5418,7 @@ function applyIndexedDbSnapshotToRuntime(
|
|||||||
success: true,
|
success: true,
|
||||||
loaded: true,
|
loaded: true,
|
||||||
loadState: GRAPH_LOAD_STATES.LOADED,
|
loadState: GRAPH_LOAD_STATES.LOADED,
|
||||||
reason: `indexeddb:${source}`,
|
reason: `${reasonPrefix}:${source}`,
|
||||||
chatId: normalizedChatId,
|
chatId: normalizedChatId,
|
||||||
attemptIndex,
|
attemptIndex,
|
||||||
shadowSnapshotUsed: false,
|
shadowSnapshotUsed: false,
|
||||||
@@ -6490,6 +6891,18 @@ async function retryPendingGraphPersist({
|
|||||||
reason,
|
reason,
|
||||||
});
|
});
|
||||||
if (indexedDbResult?.saved) {
|
if (indexedDbResult?.saved) {
|
||||||
|
const chatStateMirrorResult = canUseHostGraphChatStatePersistence(context)
|
||||||
|
? await persistGraphToHostChatState(context, {
|
||||||
|
graph: pendingPersistGraph,
|
||||||
|
revision: targetRevision,
|
||||||
|
reason: `${reason}:chat-state-mirror`,
|
||||||
|
storageTier: "chat-state",
|
||||||
|
accepted: true,
|
||||||
|
lastProcessedAssistantFloor,
|
||||||
|
extractionCount,
|
||||||
|
mode: "mirror",
|
||||||
|
})
|
||||||
|
: null;
|
||||||
clearPendingGraphPersistRetry();
|
clearPendingGraphPersistRetry();
|
||||||
persistGraphCommitMarker(context, {
|
persistGraphCommitMarker(context, {
|
||||||
reason,
|
reason,
|
||||||
@@ -6531,6 +6944,66 @@ async function retryPendingGraphPersist({
|
|||||||
return persistResult;
|
return persistResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (canUseHostGraphChatStatePersistence(context)) {
|
||||||
|
const chatStateResult = await persistGraphToHostChatState(context, {
|
||||||
|
graph: pendingPersistGraph,
|
||||||
|
revision: targetRevision,
|
||||||
|
reason: `${reason}:chat-state-fallback`,
|
||||||
|
storageTier: "chat-state",
|
||||||
|
accepted: true,
|
||||||
|
lastProcessedAssistantFloor,
|
||||||
|
extractionCount,
|
||||||
|
mode: "primary",
|
||||||
|
});
|
||||||
|
if (chatStateResult?.saved) {
|
||||||
|
clearPendingGraphPersistRetry();
|
||||||
|
persistGraphCommitMarker(context, {
|
||||||
|
reason: `${reason}:chat-state-fallback`,
|
||||||
|
revision: targetRevision,
|
||||||
|
storageTier: "chat-state",
|
||||||
|
accepted: true,
|
||||||
|
lastProcessedAssistantFloor,
|
||||||
|
extractionCount,
|
||||||
|
immediate: true,
|
||||||
|
});
|
||||||
|
updateGraphPersistenceState({
|
||||||
|
pendingPersist: false,
|
||||||
|
persistMismatchReason: "",
|
||||||
|
lastAcceptedRevision: Math.max(
|
||||||
|
Number(graphPersistenceState.lastAcceptedRevision || 0),
|
||||||
|
targetRevision,
|
||||||
|
),
|
||||||
|
lastPersistReason: `${reason}:chat-state-fallback`,
|
||||||
|
lastPersistMode: "chat-state",
|
||||||
|
queuedPersistRevision: 0,
|
||||||
|
queuedPersistChatId: "",
|
||||||
|
queuedPersistMode: "",
|
||||||
|
queuedPersistRotateIntegrity: false,
|
||||||
|
queuedPersistReason: "",
|
||||||
|
storagePrimary: "chat-state",
|
||||||
|
storageMode: "chat-state",
|
||||||
|
});
|
||||||
|
const persistResult = buildGraphPersistResult({
|
||||||
|
saved: true,
|
||||||
|
accepted: true,
|
||||||
|
reason: `${reason}:chat-state-fallback`,
|
||||||
|
revision: targetRevision,
|
||||||
|
saveMode: "chat-state",
|
||||||
|
storageTier: "chat-state",
|
||||||
|
});
|
||||||
|
applyAcceptedPendingPersistState(persistResult, {
|
||||||
|
lastProcessedAssistantFloor,
|
||||||
|
persistedGraph: pendingPersistGraph,
|
||||||
|
});
|
||||||
|
queueGraphPersistToIndexedDb(activeChatId, pendingPersistGraph, {
|
||||||
|
revision: targetRevision,
|
||||||
|
reason: `${reason}:chat-state-fallback:promote-indexeddb`,
|
||||||
|
});
|
||||||
|
void maybeResumePendingAutoExtraction("pending-persist-resolved:chat-state");
|
||||||
|
return persistResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (canPersistGraphToMetadataFallback(context, pendingPersistGraph)) {
|
if (canPersistGraphToMetadataFallback(context, pendingPersistGraph)) {
|
||||||
const metadataReason = `${reason}:metadata-full-fallback`;
|
const metadataReason = `${reason}:metadata-full-fallback`;
|
||||||
const metadataResult = persistGraphToChatMetadata(context, {
|
const metadataResult = persistGraphToChatMetadata(context, {
|
||||||
@@ -6635,6 +7108,18 @@ async function persistExtractionBatchResult({
|
|||||||
reason,
|
reason,
|
||||||
});
|
});
|
||||||
if (indexedDbResult?.saved) {
|
if (indexedDbResult?.saved) {
|
||||||
|
const chatStateMirrorResult = canUseHostGraphChatStatePersistence(context)
|
||||||
|
? await persistGraphToHostChatState(context, {
|
||||||
|
graph: persistGraph,
|
||||||
|
revision,
|
||||||
|
reason: `${reason}:chat-state-mirror`,
|
||||||
|
storageTier: "chat-state",
|
||||||
|
accepted: true,
|
||||||
|
lastProcessedAssistantFloor,
|
||||||
|
extractionCount,
|
||||||
|
mode: "mirror",
|
||||||
|
})
|
||||||
|
: null;
|
||||||
persistGraphCommitMarker(context, {
|
persistGraphCommitMarker(context, {
|
||||||
reason,
|
reason,
|
||||||
revision,
|
revision,
|
||||||
@@ -6670,6 +7155,60 @@ async function persistExtractionBatchResult({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (canUseHostGraphChatStatePersistence(context)) {
|
||||||
|
const chatStateResult = await persistGraphToHostChatState(context, {
|
||||||
|
graph: persistGraph,
|
||||||
|
revision,
|
||||||
|
reason: `${reason}:chat-state-fallback`,
|
||||||
|
storageTier: "chat-state",
|
||||||
|
accepted: true,
|
||||||
|
lastProcessedAssistantFloor,
|
||||||
|
extractionCount,
|
||||||
|
mode: "primary",
|
||||||
|
});
|
||||||
|
if (chatStateResult?.saved) {
|
||||||
|
persistGraphCommitMarker(context, {
|
||||||
|
reason: `${reason}:chat-state-fallback`,
|
||||||
|
revision,
|
||||||
|
storageTier: "chat-state",
|
||||||
|
accepted: true,
|
||||||
|
lastProcessedAssistantFloor,
|
||||||
|
extractionCount,
|
||||||
|
immediate: true,
|
||||||
|
});
|
||||||
|
updateGraphPersistenceState({
|
||||||
|
pendingPersist: false,
|
||||||
|
persistMismatchReason: "",
|
||||||
|
lastAcceptedRevision: Math.max(
|
||||||
|
Number(graphPersistenceState.lastAcceptedRevision || 0),
|
||||||
|
revision,
|
||||||
|
),
|
||||||
|
lastPersistReason: `${reason}:chat-state-fallback`,
|
||||||
|
lastPersistMode: "chat-state",
|
||||||
|
queuedPersistRevision: 0,
|
||||||
|
queuedPersistChatId: "",
|
||||||
|
queuedPersistMode: "",
|
||||||
|
queuedPersistRotateIntegrity: false,
|
||||||
|
queuedPersistReason: "",
|
||||||
|
storagePrimary: "chat-state",
|
||||||
|
storageMode: "chat-state",
|
||||||
|
});
|
||||||
|
clearPendingGraphPersistRetry();
|
||||||
|
queueGraphPersistToIndexedDb(chatId, persistGraph, {
|
||||||
|
revision,
|
||||||
|
reason: `${reason}:chat-state-fallback:promote-indexeddb`,
|
||||||
|
});
|
||||||
|
return buildGraphPersistResult({
|
||||||
|
saved: true,
|
||||||
|
accepted: true,
|
||||||
|
reason: `${reason}:chat-state-fallback`,
|
||||||
|
revision,
|
||||||
|
saveMode: "chat-state",
|
||||||
|
storageTier: "chat-state",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const shadowReason = `${reason}:shadow-fallback`;
|
const shadowReason = `${reason}:shadow-fallback`;
|
||||||
const shadowCaptured = maybeCaptureGraphShadowSnapshot(shadowReason, {
|
const shadowCaptured = maybeCaptureGraphShadowSnapshot(shadowReason, {
|
||||||
graph: persistGraph,
|
graph: persistGraph,
|
||||||
@@ -6897,6 +7436,14 @@ function syncGraphLoadFromLiveContext(options = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (canUseHostGraphChatStatePersistence(context)) {
|
||||||
|
scheduleGraphChatStateProbe(chatId, {
|
||||||
|
source: `${source}:chat-state-probe`,
|
||||||
|
attemptIndex: 0,
|
||||||
|
allowOverride: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const cachedSnapshot = readCachedIndexedDbSnapshot(chatId);
|
const cachedSnapshot = readCachedIndexedDbSnapshot(chatId);
|
||||||
if (isIndexedDbSnapshotMeaningful(cachedSnapshot)) {
|
if (isIndexedDbSnapshotMeaningful(cachedSnapshot)) {
|
||||||
const result = applyIndexedDbSnapshotToRuntime(chatId, cachedSnapshot, {
|
const result = applyIndexedDbSnapshotToRuntime(chatId, cachedSnapshot, {
|
||||||
@@ -7913,6 +8460,14 @@ function loadGraphFromChat(options = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (canUseHostGraphChatStatePersistence(context)) {
|
||||||
|
scheduleGraphChatStateProbe(chatId, {
|
||||||
|
source: `${source}:chat-state-probe`,
|
||||||
|
attemptIndex,
|
||||||
|
allowOverride: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const cachedSnapshot = readCachedIndexedDbSnapshot(chatId);
|
const cachedSnapshot = readCachedIndexedDbSnapshot(chatId);
|
||||||
if (isIndexedDbSnapshotMeaningful(cachedSnapshot)) {
|
if (isIndexedDbSnapshotMeaningful(cachedSnapshot)) {
|
||||||
const cachedResult = applyIndexedDbSnapshotToRuntime(
|
const cachedResult = applyIndexedDbSnapshotToRuntime(
|
||||||
|
|||||||
@@ -12,10 +12,13 @@ import {
|
|||||||
import { onMessageReceivedController } from "../host/event-binding.js";
|
import { onMessageReceivedController } from "../host/event-binding.js";
|
||||||
import {
|
import {
|
||||||
buildGraphCommitMarker,
|
buildGraphCommitMarker,
|
||||||
|
buildGraphChatStateSnapshot,
|
||||||
|
canUseGraphChatState,
|
||||||
detectIndexedDbSnapshotCommitMarkerMismatch,
|
detectIndexedDbSnapshotCommitMarkerMismatch,
|
||||||
cloneGraphForPersistence,
|
cloneGraphForPersistence,
|
||||||
cloneRuntimeDebugValue,
|
cloneRuntimeDebugValue,
|
||||||
findGraphShadowSnapshotByIntegrity,
|
findGraphShadowSnapshotByIntegrity,
|
||||||
|
GRAPH_CHAT_STATE_NAMESPACE,
|
||||||
getAcceptedCommitMarkerRevision,
|
getAcceptedCommitMarkerRevision,
|
||||||
getGraphPersistedRevision,
|
getGraphPersistedRevision,
|
||||||
getGraphIdentityAliasCandidates,
|
getGraphIdentityAliasCandidates,
|
||||||
@@ -33,6 +36,7 @@ import {
|
|||||||
MODULE_NAME,
|
MODULE_NAME,
|
||||||
normalizeGraphCommitMarker,
|
normalizeGraphCommitMarker,
|
||||||
readGraphCommitMarker,
|
readGraphCommitMarker,
|
||||||
|
readGraphChatStateSnapshot,
|
||||||
readGraphShadowSnapshot,
|
readGraphShadowSnapshot,
|
||||||
rememberGraphIdentityAlias,
|
rememberGraphIdentityAlias,
|
||||||
removeGraphShadowSnapshot,
|
removeGraphShadowSnapshot,
|
||||||
@@ -40,6 +44,7 @@ import {
|
|||||||
shouldPreferShadowSnapshotOverOfficial,
|
shouldPreferShadowSnapshotOverOfficial,
|
||||||
stampGraphPersistenceMeta,
|
stampGraphPersistenceMeta,
|
||||||
writeChatMetadataPatch,
|
writeChatMetadataPatch,
|
||||||
|
writeGraphChatStateSnapshot,
|
||||||
writeGraphShadowSnapshot,
|
writeGraphShadowSnapshot,
|
||||||
} from "../graph/graph-persistence.js";
|
} from "../graph/graph-persistence.js";
|
||||||
import {
|
import {
|
||||||
@@ -391,9 +396,12 @@ async function createGraphPersistenceHarness({
|
|||||||
readPersistedRecallFromUserMessage,
|
readPersistedRecallFromUserMessage,
|
||||||
cloneGraphForPersistence,
|
cloneGraphForPersistence,
|
||||||
buildGraphCommitMarker,
|
buildGraphCommitMarker,
|
||||||
|
buildGraphChatStateSnapshot,
|
||||||
|
canUseGraphChatState,
|
||||||
cloneRuntimeDebugValue,
|
cloneRuntimeDebugValue,
|
||||||
detectIndexedDbSnapshotCommitMarkerMismatch,
|
detectIndexedDbSnapshotCommitMarkerMismatch,
|
||||||
onMessageReceivedController,
|
onMessageReceivedController,
|
||||||
|
GRAPH_CHAT_STATE_NAMESPACE,
|
||||||
getAcceptedCommitMarkerRevision,
|
getAcceptedCommitMarkerRevision,
|
||||||
getGraphPersistenceMeta,
|
getGraphPersistenceMeta,
|
||||||
getGraphPersistedRevision,
|
getGraphPersistedRevision,
|
||||||
@@ -412,6 +420,7 @@ async function createGraphPersistenceHarness({
|
|||||||
findGraphShadowSnapshotByIntegrity,
|
findGraphShadowSnapshotByIntegrity,
|
||||||
normalizeGraphCommitMarker,
|
normalizeGraphCommitMarker,
|
||||||
readGraphCommitMarker,
|
readGraphCommitMarker,
|
||||||
|
readGraphChatStateSnapshot,
|
||||||
readGraphShadowSnapshot,
|
readGraphShadowSnapshot,
|
||||||
rememberGraphIdentityAlias,
|
rememberGraphIdentityAlias,
|
||||||
removeGraphShadowSnapshot,
|
removeGraphShadowSnapshot,
|
||||||
@@ -419,6 +428,7 @@ async function createGraphPersistenceHarness({
|
|||||||
shouldPreferShadowSnapshotOverOfficial,
|
shouldPreferShadowSnapshotOverOfficial,
|
||||||
stampGraphPersistenceMeta,
|
stampGraphPersistenceMeta,
|
||||||
writeChatMetadataPatch,
|
writeChatMetadataPatch,
|
||||||
|
writeGraphChatStateSnapshot,
|
||||||
writeGraphShadowSnapshot,
|
writeGraphShadowSnapshot,
|
||||||
// Shadow snapshot functions need VM-local sessionStorage overrides
|
// Shadow snapshot functions need VM-local sessionStorage overrides
|
||||||
// because imported versions use the outer globalThis (no sessionStorage)
|
// because imported versions use the outer globalThis (no sessionStorage)
|
||||||
@@ -711,6 +721,7 @@ async function createGraphPersistenceHarness({
|
|||||||
characterId,
|
characterId,
|
||||||
groupId,
|
groupId,
|
||||||
chat,
|
chat,
|
||||||
|
__chatStateStore: new Map(),
|
||||||
updateChatMetadata(patch) {
|
updateChatMetadata(patch) {
|
||||||
const base =
|
const base =
|
||||||
this.chatMetadata &&
|
this.chatMetadata &&
|
||||||
@@ -729,6 +740,36 @@ async function createGraphPersistenceHarness({
|
|||||||
async saveMetadata() {
|
async saveMetadata() {
|
||||||
runtimeContext.__contextImmediateSaveCalls += 1;
|
runtimeContext.__contextImmediateSaveCalls += 1;
|
||||||
},
|
},
|
||||||
|
async getChatState(namespace) {
|
||||||
|
const key = String(namespace || "").trim().toLowerCase();
|
||||||
|
const value = this.__chatStateStore.get(key);
|
||||||
|
return value == null ? null : structuredClone(value);
|
||||||
|
},
|
||||||
|
async updateChatState(namespace, updater) {
|
||||||
|
const key = String(namespace || "").trim().toLowerCase();
|
||||||
|
if (!key || typeof updater !== "function") {
|
||||||
|
return { ok: false, state: null, updated: false };
|
||||||
|
}
|
||||||
|
const current = this.__chatStateStore.has(key)
|
||||||
|
? structuredClone(this.__chatStateStore.get(key))
|
||||||
|
: {};
|
||||||
|
const next = await updater(structuredClone(current), {
|
||||||
|
attempt: 0,
|
||||||
|
target: null,
|
||||||
|
namespace: key,
|
||||||
|
});
|
||||||
|
if (next == null) {
|
||||||
|
return { ok: true, state: current, updated: false };
|
||||||
|
}
|
||||||
|
const currentJson = JSON.stringify(current);
|
||||||
|
const nextJson = JSON.stringify(next);
|
||||||
|
this.__chatStateStore.set(key, structuredClone(next));
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
state: structuredClone(next),
|
||||||
|
updated: currentJson !== nextJson,
|
||||||
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
__contextSaveCalls: 0,
|
__contextSaveCalls: 0,
|
||||||
__contextImmediateSaveCalls: 0,
|
__contextImmediateSaveCalls: 0,
|
||||||
@@ -2675,4 +2716,111 @@ result = {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const harness = await createGraphPersistenceHarness({
|
||||||
|
chatId: "chat-state-save",
|
||||||
|
globalChatId: "chat-state-save",
|
||||||
|
chatMetadata: {
|
||||||
|
integrity: "meta-chat-state-save",
|
||||||
|
},
|
||||||
|
indexedDbSnapshot: {
|
||||||
|
meta: {
|
||||||
|
chatId: "chat-state-save",
|
||||||
|
revision: 0,
|
||||||
|
},
|
||||||
|
nodes: [],
|
||||||
|
edges: [],
|
||||||
|
tombstones: [],
|
||||||
|
state: {
|
||||||
|
lastProcessedFloor: -1,
|
||||||
|
extractionCount: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const graph = stampPersistedGraph(
|
||||||
|
createMeaningfulGraph("chat-state-save", "sidecar"),
|
||||||
|
{
|
||||||
|
revision: 7,
|
||||||
|
integrity: "meta-chat-state-save",
|
||||||
|
chatId: "chat-state-save",
|
||||||
|
reason: "chat-state-seed",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await harness.runtimeContext.persistGraphToHostChatState(
|
||||||
|
harness.runtimeContext.__chatContext,
|
||||||
|
{
|
||||||
|
graph,
|
||||||
|
revision: 7,
|
||||||
|
reason: "chat-state-direct-save",
|
||||||
|
storageTier: "chat-state",
|
||||||
|
accepted: true,
|
||||||
|
lastProcessedAssistantFloor: 6,
|
||||||
|
extractionCount: 3,
|
||||||
|
mode: "primary",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(result.saved, true);
|
||||||
|
const stored = await harness.runtimeContext.__chatContext.getChatState(
|
||||||
|
GRAPH_CHAT_STATE_NAMESPACE,
|
||||||
|
);
|
||||||
|
assert.equal(stored?.revision, 7);
|
||||||
|
assert.equal(stored?.commitMarker?.storageTier, "chat-state");
|
||||||
|
assert.equal(
|
||||||
|
harness.api.getGraphPersistenceState().dualWriteLastResult?.target,
|
||||||
|
"chat-state",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const harness = await createGraphPersistenceHarness({
|
||||||
|
chatId: "chat-state-read",
|
||||||
|
globalChatId: "chat-state-read",
|
||||||
|
chatMetadata: {
|
||||||
|
integrity: "meta-chat-state-read",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const sidecarGraph = stampPersistedGraph(
|
||||||
|
createMeaningfulGraph("chat-state-read", "sidecar-read"),
|
||||||
|
{
|
||||||
|
revision: 9,
|
||||||
|
integrity: "meta-chat-state-read",
|
||||||
|
chatId: "chat-state-read",
|
||||||
|
reason: "chat-state-read-seed",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
harness.runtimeContext.__chatContext.__chatStateStore.set(
|
||||||
|
GRAPH_CHAT_STATE_NAMESPACE,
|
||||||
|
buildGraphChatStateSnapshot(sidecarGraph, {
|
||||||
|
revision: 9,
|
||||||
|
storageTier: "chat-state",
|
||||||
|
accepted: true,
|
||||||
|
reason: "chat-state-read-seed",
|
||||||
|
chatId: "chat-state-read",
|
||||||
|
integrity: "meta-chat-state-read",
|
||||||
|
lastProcessedAssistantFloor: 6,
|
||||||
|
extractionCount: 3,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await harness.runtimeContext.readGraphChatStateSnapshot(
|
||||||
|
harness.runtimeContext.__chatContext,
|
||||||
|
{
|
||||||
|
namespace: GRAPH_CHAT_STATE_NAMESPACE,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
harness.runtimeContext.canUseGraphChatState(
|
||||||
|
harness.runtimeContext.__chatContext,
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(result?.revision, 9);
|
||||||
|
assert.equal(result?.commitMarker?.storageTier, "chat-state");
|
||||||
|
}
|
||||||
|
|
||||||
console.log("graph-persistence tests passed");
|
console.log("graph-persistence tests passed");
|
||||||
|
|||||||
Reference in New Issue
Block a user