mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-06-13 18:31:16 +08:00
refactor(rebirth): extract identity resolver core
This commit is contained in:
277
index.js
277
index.js
@@ -131,6 +131,17 @@ import {
|
||||
debugDebug,
|
||||
debugLog,
|
||||
} from "./runtime/debug-logging.js";
|
||||
import {
|
||||
areChatIdsEquivalentForIdentityCore,
|
||||
canMutateRuntimeGraphForIdentityCore,
|
||||
doesChatIdMatchIdentityCore,
|
||||
planRuntimeGraphIdentityRepairCore,
|
||||
resolveActiveHostChatIdCore,
|
||||
resolveCurrentChatIdentityCore,
|
||||
resolveGraphOwnerIdentityCore,
|
||||
resolvePersistenceChatIdCore,
|
||||
resolveRuntimeGraphFallbackIdentityCore,
|
||||
} from "./runtime/identity-resolver.js";
|
||||
import {
|
||||
extractMemories,
|
||||
generateReflection,
|
||||
@@ -540,55 +551,17 @@ function getChatCommitMarker(context = getContext()) {
|
||||
}
|
||||
|
||||
function resolveCurrentHostChatId(context = getContext()) {
|
||||
const candidates = [
|
||||
context?.chatId,
|
||||
context?.getCurrentChatId?.(),
|
||||
readGlobalCurrentChatId(),
|
||||
context?.chatMetadata?.chat_id,
|
||||
context?.chatMetadata?.chatId,
|
||||
context?.chatMetadata?.session_id,
|
||||
context?.chatMetadata?.sessionId,
|
||||
];
|
||||
|
||||
return (
|
||||
candidates
|
||||
.map((candidate) => normalizeChatIdCandidate(candidate))
|
||||
.find(Boolean) || ""
|
||||
);
|
||||
return resolveActiveHostChatIdCore({ context, readGlobalCurrentChatId });
|
||||
}
|
||||
|
||||
function resolveCurrentChatIdentity(context = getContext()) {
|
||||
const hostChatId = resolveCurrentHostChatId(context);
|
||||
const integrity =
|
||||
typeof getChatMetadataIntegrity === "function"
|
||||
? getChatMetadataIntegrity(context)
|
||||
: normalizeChatIdCandidate(
|
||||
context?.chatMetadata?.integrity ||
|
||||
context?.chatMetadata?.chat_id ||
|
||||
context?.chatMetadata?.chatId ||
|
||||
"",
|
||||
);
|
||||
const aliasedChatId =
|
||||
!integrity &&
|
||||
hostChatId &&
|
||||
typeof resolveGraphIdentityAliasByHostChatId === "function"
|
||||
? resolveGraphIdentityAliasByHostChatId(hostChatId)
|
||||
: "";
|
||||
const chatId = integrity || aliasedChatId || hostChatId;
|
||||
|
||||
return {
|
||||
chatId,
|
||||
hostChatId,
|
||||
integrity,
|
||||
identitySource: integrity
|
||||
? "integrity"
|
||||
: aliasedChatId
|
||||
? "alias"
|
||||
: hostChatId
|
||||
? "host-chat-id"
|
||||
: "",
|
||||
hasLikelySelectedChat: hasLikelySelectedChatContext(context),
|
||||
};
|
||||
return resolveCurrentChatIdentityCore({
|
||||
context,
|
||||
readGlobalCurrentChatId,
|
||||
resolveAliasByHostChatId: resolveGraphIdentityAliasByHostChatId,
|
||||
resolveIntegrity: getChatMetadataIntegrity,
|
||||
hasLikelySelectedChat: hasLikelySelectedChatContext,
|
||||
});
|
||||
}
|
||||
|
||||
function getCurrentChatId(context = getContext()) {
|
||||
@@ -597,29 +570,16 @@ function getCurrentChatId(context = getContext()) {
|
||||
|
||||
function getRuntimeGraphChatIdFallback(graph = currentGraph) {
|
||||
const graphMeta = getGraphPersistenceMeta(graph) || {};
|
||||
const fallbackCandidates = [
|
||||
graph?.historyState?.chatId,
|
||||
graphMeta.chatId,
|
||||
graphPersistenceState.chatId,
|
||||
graphPersistenceState.queuedPersistChatId,
|
||||
graphPersistenceState.commitMarker?.chatId,
|
||||
];
|
||||
|
||||
return (
|
||||
fallbackCandidates
|
||||
.map((candidate) => normalizeChatIdCandidate(candidate))
|
||||
.find(Boolean) || ""
|
||||
);
|
||||
return resolveRuntimeGraphFallbackIdentityCore({
|
||||
graph,
|
||||
graphMeta,
|
||||
persistenceState: graphPersistenceState,
|
||||
}).chatId;
|
||||
}
|
||||
|
||||
function getGraphOwnedChatId(graph = currentGraph) {
|
||||
const graphMeta = getGraphPersistenceMeta(graph) || {};
|
||||
const ownedCandidates = [graph?.historyState?.chatId, graphMeta.chatId];
|
||||
return (
|
||||
ownedCandidates
|
||||
.map((candidate) => normalizeChatIdCandidate(candidate))
|
||||
.find(Boolean) || ""
|
||||
);
|
||||
return resolveGraphOwnerIdentityCore({ graph, graphMeta }).chatId;
|
||||
}
|
||||
|
||||
function resolveOperationalChatId(
|
||||
@@ -639,34 +599,16 @@ function resolvePersistenceChatId(
|
||||
graph = currentGraph,
|
||||
explicitChatId = "",
|
||||
) {
|
||||
const directChatId = normalizeChatIdCandidate(explicitChatId);
|
||||
if (directChatId) return directChatId;
|
||||
|
||||
const resolvedIdentity = resolveCurrentChatIdentity(context);
|
||||
const resolvedChatId = normalizeChatIdCandidate(resolvedIdentity.chatId);
|
||||
if (resolvedChatId) return resolvedChatId;
|
||||
|
||||
const graphMeta = getGraphPersistenceMeta(graph) || {};
|
||||
const fallbackCandidates = [
|
||||
graph?.historyState?.chatId,
|
||||
graphMeta.chatId,
|
||||
currentGraph?.historyState?.chatId,
|
||||
getGraphPersistenceMeta(currentGraph)?.chatId,
|
||||
graphPersistenceState.chatId,
|
||||
graphPersistenceState.queuedPersistChatId,
|
||||
graphPersistenceState.commitMarker?.chatId,
|
||||
context?.chatMetadata?.integrity,
|
||||
context?.chatMetadata?.chat_id,
|
||||
context?.chatMetadata?.chatId,
|
||||
context?.chatMetadata?.session_id,
|
||||
context?.chatMetadata?.sessionId,
|
||||
];
|
||||
|
||||
return (
|
||||
fallbackCandidates
|
||||
.map((candidate) => normalizeChatIdCandidate(candidate))
|
||||
.find(Boolean) || ""
|
||||
);
|
||||
return resolvePersistenceChatIdCore({
|
||||
explicitChatId,
|
||||
activeIdentity: resolveCurrentChatIdentity(context),
|
||||
graph,
|
||||
graphMeta: getGraphPersistenceMeta(graph) || {},
|
||||
currentGraph,
|
||||
currentGraphMeta: getGraphPersistenceMeta(currentGraph) || {},
|
||||
persistenceState: graphPersistenceState,
|
||||
context,
|
||||
});
|
||||
}
|
||||
|
||||
function rememberResolvedGraphIdentityAlias(
|
||||
@@ -689,32 +631,14 @@ function doesChatIdMatchResolvedGraphIdentity(
|
||||
candidateChatId,
|
||||
identity = resolveCurrentChatIdentity(getContext()),
|
||||
) {
|
||||
const normalizedCandidate = normalizeChatIdCandidate(candidateChatId);
|
||||
if (!normalizedCandidate || !identity || typeof identity !== "object") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const knownChatIds = new Set();
|
||||
const addKnownChatId = (value) => {
|
||||
const normalized = normalizeChatIdCandidate(value);
|
||||
if (normalized) {
|
||||
knownChatIds.add(normalized);
|
||||
}
|
||||
};
|
||||
|
||||
addKnownChatId(identity.chatId);
|
||||
addKnownChatId(identity.hostChatId);
|
||||
addKnownChatId(identity.integrity);
|
||||
|
||||
for (const aliasCandidate of getGraphIdentityAliasCandidates({
|
||||
integrity: identity.integrity,
|
||||
hostChatId: identity.hostChatId,
|
||||
persistenceChatId: identity.chatId,
|
||||
})) {
|
||||
addKnownChatId(aliasCandidate);
|
||||
}
|
||||
|
||||
return knownChatIds.has(normalizedCandidate);
|
||||
return doesChatIdMatchIdentityCore(candidateChatId, {
|
||||
identity,
|
||||
aliasCandidates: getGraphIdentityAliasCandidates({
|
||||
integrity: identity?.integrity,
|
||||
hostChatId: identity?.hostChatId,
|
||||
persistenceChatId: identity?.chatId,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function areChatIdsEquivalentForResolvedIdentity(
|
||||
@@ -722,18 +646,14 @@ function areChatIdsEquivalentForResolvedIdentity(
|
||||
referenceChatId,
|
||||
identity = resolveCurrentChatIdentity(getContext()),
|
||||
) {
|
||||
const normalizedCandidate = normalizeChatIdCandidate(candidateChatId);
|
||||
const normalizedReference = normalizeChatIdCandidate(referenceChatId);
|
||||
if (!normalizedCandidate || !normalizedReference) {
|
||||
return normalizedCandidate === normalizedReference;
|
||||
}
|
||||
if (normalizedCandidate === normalizedReference) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
doesChatIdMatchResolvedGraphIdentity(normalizedCandidate, identity) &&
|
||||
doesChatIdMatchResolvedGraphIdentity(normalizedReference, identity)
|
||||
);
|
||||
return areChatIdsEquivalentForIdentityCore(candidateChatId, referenceChatId, {
|
||||
identity,
|
||||
aliasCandidates: getGraphIdentityAliasCandidates({
|
||||
integrity: identity?.integrity,
|
||||
hostChatId: identity?.hostChatId,
|
||||
persistenceChatId: identity?.chatId,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function syncCommitMarkerToPersistenceState(context = getContext()) {
|
||||
@@ -4472,34 +4392,24 @@ function hasRuntimeGraphMutationContext(
|
||||
}
|
||||
|
||||
const identity = resolveCurrentChatIdentity(context);
|
||||
const liveChatId = normalizeChatIdCandidate(identity.chatId);
|
||||
const graphOwnedChatId = getGraphOwnedChatId(graph);
|
||||
if (!graphOwnedChatId) return false;
|
||||
|
||||
if (liveChatId) {
|
||||
return (
|
||||
areChatIdsEquivalentForResolvedIdentity(graphOwnedChatId, liveChatId, identity) ||
|
||||
areChatIdsEquivalentForResolvedIdentity(liveChatId, graphOwnedChatId, identity)
|
||||
);
|
||||
}
|
||||
|
||||
const stateChatId = normalizeChatIdCandidate(graphPersistenceState.chatId);
|
||||
if (!stateChatId || stateChatId !== graphOwnedChatId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const markerChatId = normalizeChatIdCandidate(graphPersistenceState.commitMarker?.chatId);
|
||||
if (markerChatId && markerChatId !== graphOwnedChatId) return false;
|
||||
|
||||
if (
|
||||
graphPersistenceState.loadState === GRAPH_LOAD_STATES.LOADED ||
|
||||
graphPersistenceState.loadState === GRAPH_LOAD_STATES.EMPTY_CONFIRMED ||
|
||||
graphPersistenceState.dbReady === true
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return allowNoChatState === true && graphPersistenceState.loadState === GRAPH_LOAD_STATES.NO_CHAT;
|
||||
return canMutateRuntimeGraphForIdentityCore({
|
||||
graph,
|
||||
activeIdentity: identity,
|
||||
graphOwnedChatId,
|
||||
persistenceState: graphPersistenceState,
|
||||
aliasCandidates: getGraphIdentityAliasCandidates({
|
||||
integrity: identity.integrity,
|
||||
hostChatId: identity.hostChatId,
|
||||
persistenceChatId: identity.chatId,
|
||||
}),
|
||||
loadedStates: [
|
||||
GRAPH_LOAD_STATES.LOADED,
|
||||
GRAPH_LOAD_STATES.EMPTY_CONFIRMED,
|
||||
],
|
||||
allowNoChatState,
|
||||
noChatState: GRAPH_LOAD_STATES.NO_CHAT,
|
||||
});
|
||||
}
|
||||
|
||||
function repairRuntimeGraphIdentityFromPersistence(
|
||||
@@ -4522,55 +4432,46 @@ function repairRuntimeGraphIdentityFromPersistence(
|
||||
}
|
||||
|
||||
const graphOwnedChatId = getGraphOwnedChatId(graph);
|
||||
if (graphOwnedChatId) {
|
||||
return { repaired: false, reason: "graph-identity-present", chatId: graphOwnedChatId };
|
||||
}
|
||||
|
||||
const stateChatId = normalizeChatIdCandidate(graphPersistenceState.chatId);
|
||||
if (!stateChatId) {
|
||||
return { repaired: false, reason: "missing-persistence-chat-id" };
|
||||
}
|
||||
|
||||
const identity = resolveCurrentChatIdentity(context);
|
||||
const liveChatId = normalizeChatIdCandidate(identity.chatId);
|
||||
if (
|
||||
liveChatId &&
|
||||
!areChatIdsEquivalentForResolvedIdentity(stateChatId, liveChatId, identity) &&
|
||||
!areChatIdsEquivalentForResolvedIdentity(liveChatId, stateChatId, identity)
|
||||
) {
|
||||
return {
|
||||
repaired: false,
|
||||
reason: "live-chat-mismatch",
|
||||
chatId: stateChatId,
|
||||
liveChatId,
|
||||
};
|
||||
}
|
||||
|
||||
const markerChatId = normalizeChatIdCandidate(graphPersistenceState.commitMarker?.chatId);
|
||||
if (markerChatId && markerChatId !== stateChatId) {
|
||||
const repairPlan = planRuntimeGraphIdentityRepairCore({
|
||||
graph,
|
||||
graphOwnedChatId,
|
||||
stateChatId,
|
||||
activeIdentity: identity,
|
||||
markerChatId,
|
||||
aliasCandidates: getGraphIdentityAliasCandidates({
|
||||
integrity: identity.integrity,
|
||||
hostChatId: identity.hostChatId,
|
||||
persistenceChatId: identity.chatId,
|
||||
}),
|
||||
});
|
||||
if (!repairPlan.shouldRepair) {
|
||||
return {
|
||||
repaired: false,
|
||||
reason: "commit-marker-chat-mismatch",
|
||||
chatId: stateChatId,
|
||||
markerChatId,
|
||||
reason: repairPlan.reason,
|
||||
chatId: repairPlan.chatId,
|
||||
liveChatId: repairPlan.liveChatId,
|
||||
markerChatId: repairPlan.markerChatId,
|
||||
};
|
||||
}
|
||||
|
||||
graph.historyState.chatId = stateChatId;
|
||||
graph.historyState.chatId = repairPlan.chatId;
|
||||
stampGraphPersistenceMeta(graph, {
|
||||
revision: graphPersistenceState.revision || graph?.meta?.revision || graph?.revision || 0,
|
||||
reason: String(reason || operationLabel || "runtime-graph-identity-repair"),
|
||||
chatId: stateChatId,
|
||||
chatId: repairPlan.chatId,
|
||||
integrity:
|
||||
normalizeChatIdCandidate(graphPersistenceState.commitMarker?.integrity) ||
|
||||
getChatMetadataIntegrity(context),
|
||||
});
|
||||
debugDebug("[ST-BME] 已补齐运行时图谱聊天身份", {
|
||||
operationLabel,
|
||||
chatId: stateChatId,
|
||||
chatId: repairPlan.chatId,
|
||||
reason,
|
||||
});
|
||||
return { repaired: true, reason: "repaired", chatId: stateChatId };
|
||||
return { repaired: true, reason: "repaired", chatId: repairPlan.chatId };
|
||||
}
|
||||
|
||||
function isGraphReadableForRecall(
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"test:runtime-history": "node tests/runtime-history.mjs",
|
||||
"test:graph-persistence": "node tests/graph-persistence.mjs",
|
||||
"test:rebirth-phase0": "node tests/rebirth-phase0.mjs",
|
||||
"test:identity-resolver": "node tests/identity-resolver.mjs",
|
||||
"test:hide-engine": "node tests/hide-engine.mjs",
|
||||
"test:maintenance-journal": "node tests/maintenance-journal.mjs",
|
||||
"test:indexeddb-persistence": "node tests/indexeddb-persistence.mjs",
|
||||
|
||||
333
runtime/identity-resolver.js
Normal file
333
runtime/identity-resolver.js
Normal file
@@ -0,0 +1,333 @@
|
||||
// ST-BME identity resolver core.
|
||||
//
|
||||
// Phase 1 keeps this module pure: callers provide context-like objects,
|
||||
// graph-owned metadata, alias callbacks, and persistence state snapshots.
|
||||
// The module separates active identity, graph-owner identity, queued/runtime
|
||||
// fallback identity, marker identity, and equivalence checks so later phases
|
||||
// can stop promoting recovery evidence into the active chat identity.
|
||||
|
||||
export function normalizeIdentityValue(value = "") {
|
||||
return String(value ?? "").trim();
|
||||
}
|
||||
|
||||
export function hasLikelySelectedChatContextCore(context = null) {
|
||||
if (!context || typeof context !== "object") return false;
|
||||
const metadata = context.chatMetadata;
|
||||
const hasMeaningfulChatMetadata = Boolean(
|
||||
metadata &&
|
||||
typeof metadata === "object" &&
|
||||
Object.keys(metadata).some((key) => metadata[key] != null && metadata[key] !== ""),
|
||||
);
|
||||
const hasChatMessages = Array.isArray(context.chat) && context.chat.length > 0;
|
||||
const hasCharacterId =
|
||||
context.characterId !== undefined &&
|
||||
context.characterId !== null &&
|
||||
String(context.characterId).trim() !== "";
|
||||
const hasGroupId =
|
||||
context.groupId !== undefined &&
|
||||
context.groupId !== null &&
|
||||
String(context.groupId).trim() !== "";
|
||||
return hasMeaningfulChatMetadata || hasChatMessages || hasCharacterId || hasGroupId;
|
||||
}
|
||||
|
||||
export function resolveActiveHostChatIdCore({
|
||||
context = null,
|
||||
readGlobalCurrentChatId = null,
|
||||
} = {}) {
|
||||
const candidates = [
|
||||
context?.chatId,
|
||||
typeof context?.getCurrentChatId === "function" ? context.getCurrentChatId() : "",
|
||||
typeof readGlobalCurrentChatId === "function" ? readGlobalCurrentChatId() : "",
|
||||
context?.chatMetadata?.chat_id,
|
||||
context?.chatMetadata?.chatId,
|
||||
context?.chatMetadata?.session_id,
|
||||
context?.chatMetadata?.sessionId,
|
||||
];
|
||||
|
||||
return candidates.map((candidate) => normalizeIdentityValue(candidate)).find(Boolean) || "";
|
||||
}
|
||||
|
||||
export function getContextIntegrityCore(context = null) {
|
||||
return normalizeIdentityValue(context?.chatMetadata?.integrity);
|
||||
}
|
||||
|
||||
export function resolveActiveChatIdentityCore({
|
||||
context = null,
|
||||
hostChatId = "",
|
||||
integrity = "",
|
||||
resolveAliasByHostChatId = null,
|
||||
hasLikelySelectedChat = null,
|
||||
} = {}) {
|
||||
const normalizedHostChatId = normalizeIdentityValue(hostChatId);
|
||||
const normalizedIntegrity = normalizeIdentityValue(integrity);
|
||||
const aliasedChatId =
|
||||
!normalizedIntegrity &&
|
||||
normalizedHostChatId &&
|
||||
typeof resolveAliasByHostChatId === "function"
|
||||
? normalizeIdentityValue(resolveAliasByHostChatId(normalizedHostChatId))
|
||||
: "";
|
||||
const chatId = normalizedIntegrity || aliasedChatId || normalizedHostChatId;
|
||||
const hasLikely =
|
||||
typeof hasLikelySelectedChat === "function"
|
||||
? hasLikelySelectedChat(context)
|
||||
: hasLikelySelectedChatContextCore(context);
|
||||
|
||||
return {
|
||||
chatId,
|
||||
hostChatId: normalizedHostChatId,
|
||||
integrity: normalizedIntegrity,
|
||||
identitySource: normalizedIntegrity
|
||||
? "integrity"
|
||||
: aliasedChatId
|
||||
? "alias"
|
||||
: normalizedHostChatId
|
||||
? "host-chat-id"
|
||||
: "",
|
||||
hasLikelySelectedChat: hasLikely,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveCurrentChatIdentityCore({
|
||||
context = null,
|
||||
readGlobalCurrentChatId = null,
|
||||
resolveAliasByHostChatId = null,
|
||||
resolveIntegrity = null,
|
||||
hasLikelySelectedChat = null,
|
||||
} = {}) {
|
||||
const hostChatId = resolveActiveHostChatIdCore({ context, readGlobalCurrentChatId });
|
||||
const integrity =
|
||||
typeof resolveIntegrity === "function"
|
||||
? normalizeIdentityValue(resolveIntegrity(context))
|
||||
: getContextIntegrityCore(context) ||
|
||||
normalizeIdentityValue(
|
||||
context?.chatMetadata?.chat_id || context?.chatMetadata?.chatId || "",
|
||||
);
|
||||
return resolveActiveChatIdentityCore({
|
||||
context,
|
||||
hostChatId,
|
||||
integrity,
|
||||
resolveAliasByHostChatId,
|
||||
hasLikelySelectedChat,
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveGraphOwnerIdentityCore({ graph = null, graphMeta = null } = {}) {
|
||||
const ownedCandidates = [graph?.historyState?.chatId, graphMeta?.chatId];
|
||||
const chatId = ownedCandidates.map((candidate) => normalizeIdentityValue(candidate)).find(Boolean) || "";
|
||||
return {
|
||||
chatId,
|
||||
source: normalizeIdentityValue(graph?.historyState?.chatId)
|
||||
? "history-state"
|
||||
: normalizeIdentityValue(graphMeta?.chatId)
|
||||
? "graph-meta"
|
||||
: "",
|
||||
integrity: normalizeIdentityValue(graphMeta?.integrity),
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveRuntimeGraphFallbackIdentityCore({
|
||||
graph = null,
|
||||
graphMeta = null,
|
||||
persistenceState = null,
|
||||
} = {}) {
|
||||
const fallbackCandidates = [
|
||||
graph?.historyState?.chatId,
|
||||
graphMeta?.chatId,
|
||||
persistenceState?.chatId,
|
||||
persistenceState?.queuedPersistChatId,
|
||||
persistenceState?.commitMarker?.chatId,
|
||||
];
|
||||
const chatId = fallbackCandidates.map((candidate) => normalizeIdentityValue(candidate)).find(Boolean) || "";
|
||||
return {
|
||||
chatId,
|
||||
source: chatId ? "runtime-fallback" : "",
|
||||
};
|
||||
}
|
||||
|
||||
export function resolvePersistenceChatIdCore({
|
||||
explicitChatId = "",
|
||||
activeIdentity = null,
|
||||
graph = null,
|
||||
graphMeta = null,
|
||||
currentGraph = null,
|
||||
currentGraphMeta = null,
|
||||
persistenceState = null,
|
||||
context = null,
|
||||
} = {}) {
|
||||
const directChatId = normalizeIdentityValue(explicitChatId);
|
||||
if (directChatId) return directChatId;
|
||||
|
||||
const resolvedChatId = normalizeIdentityValue(activeIdentity?.chatId);
|
||||
if (resolvedChatId) return resolvedChatId;
|
||||
|
||||
const fallbackCandidates = [
|
||||
graph?.historyState?.chatId,
|
||||
graphMeta?.chatId,
|
||||
currentGraph?.historyState?.chatId,
|
||||
currentGraphMeta?.chatId,
|
||||
persistenceState?.chatId,
|
||||
persistenceState?.queuedPersistChatId,
|
||||
persistenceState?.commitMarker?.chatId,
|
||||
context?.chatMetadata?.integrity,
|
||||
context?.chatMetadata?.chat_id,
|
||||
context?.chatMetadata?.chatId,
|
||||
context?.chatMetadata?.session_id,
|
||||
context?.chatMetadata?.sessionId,
|
||||
];
|
||||
|
||||
return fallbackCandidates.map((candidate) => normalizeIdentityValue(candidate)).find(Boolean) || "";
|
||||
}
|
||||
|
||||
export function getKnownChatIdsForIdentityCore({ identity = null, aliasCandidates = [] } = {}) {
|
||||
const knownChatIds = new Set();
|
||||
const addKnownChatId = (value) => {
|
||||
const normalized = normalizeIdentityValue(value);
|
||||
if (normalized) knownChatIds.add(normalized);
|
||||
};
|
||||
addKnownChatId(identity?.chatId);
|
||||
addKnownChatId(identity?.hostChatId);
|
||||
addKnownChatId(identity?.integrity);
|
||||
for (const aliasCandidate of Array.isArray(aliasCandidates) ? aliasCandidates : []) {
|
||||
addKnownChatId(aliasCandidate);
|
||||
}
|
||||
return knownChatIds;
|
||||
}
|
||||
|
||||
export function doesChatIdMatchIdentityCore(candidateChatId, { identity = null, aliasCandidates = [] } = {}) {
|
||||
const normalizedCandidate = normalizeIdentityValue(candidateChatId);
|
||||
if (!normalizedCandidate || !identity || typeof identity !== "object") return false;
|
||||
return getKnownChatIdsForIdentityCore({ identity, aliasCandidates }).has(normalizedCandidate);
|
||||
}
|
||||
|
||||
export function areChatIdsEquivalentForIdentityCore(
|
||||
candidateChatId,
|
||||
referenceChatId,
|
||||
{ identity = null, aliasCandidates = [] } = {},
|
||||
) {
|
||||
const normalizedCandidate = normalizeIdentityValue(candidateChatId);
|
||||
const normalizedReference = normalizeIdentityValue(referenceChatId);
|
||||
if (!normalizedCandidate || !normalizedReference) {
|
||||
return normalizedCandidate === normalizedReference;
|
||||
}
|
||||
if (normalizedCandidate === normalizedReference) return true;
|
||||
return (
|
||||
doesChatIdMatchIdentityCore(normalizedCandidate, { identity, aliasCandidates }) &&
|
||||
doesChatIdMatchIdentityCore(normalizedReference, { identity, aliasCandidates })
|
||||
);
|
||||
}
|
||||
|
||||
export function canMutateRuntimeGraphForIdentityCore({
|
||||
graph = null,
|
||||
activeIdentity = null,
|
||||
graphOwnedChatId = "",
|
||||
persistenceState = null,
|
||||
aliasCandidates = [],
|
||||
loadedStates = ["loaded", "empty-confirmed"],
|
||||
allowNoChatState = false,
|
||||
noChatState = "no-chat",
|
||||
} = {}) {
|
||||
if (
|
||||
!graph ||
|
||||
typeof graph !== "object" ||
|
||||
!graph.historyState ||
|
||||
typeof graph.historyState !== "object" ||
|
||||
Array.isArray(graph.historyState)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const ownedChatId = normalizeIdentityValue(graphOwnedChatId);
|
||||
if (!ownedChatId) return false;
|
||||
|
||||
const liveChatId = normalizeIdentityValue(activeIdentity?.chatId);
|
||||
if (liveChatId) {
|
||||
return (
|
||||
areChatIdsEquivalentForIdentityCore(ownedChatId, liveChatId, {
|
||||
identity: activeIdentity,
|
||||
aliasCandidates,
|
||||
}) ||
|
||||
areChatIdsEquivalentForIdentityCore(liveChatId, ownedChatId, {
|
||||
identity: activeIdentity,
|
||||
aliasCandidates,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const stateChatId = normalizeIdentityValue(persistenceState?.chatId);
|
||||
if (!stateChatId || stateChatId !== ownedChatId) return false;
|
||||
|
||||
const markerChatId = normalizeIdentityValue(persistenceState?.commitMarker?.chatId);
|
||||
if (markerChatId && markerChatId !== ownedChatId) return false;
|
||||
|
||||
const loadState = String(persistenceState?.loadState || "");
|
||||
if (
|
||||
loadedStates.includes(loadState) ||
|
||||
persistenceState?.dbReady === true
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return allowNoChatState === true && loadState === noChatState;
|
||||
}
|
||||
|
||||
export function planRuntimeGraphIdentityRepairCore({
|
||||
graph = null,
|
||||
graphOwnedChatId = "",
|
||||
stateChatId = "",
|
||||
activeIdentity = null,
|
||||
markerChatId = "",
|
||||
aliasCandidates = [],
|
||||
} = {}) {
|
||||
if (
|
||||
!graph ||
|
||||
typeof graph !== "object" ||
|
||||
Array.isArray(graph) ||
|
||||
!graph.historyState ||
|
||||
typeof graph.historyState !== "object" ||
|
||||
Array.isArray(graph.historyState)
|
||||
) {
|
||||
return { shouldRepair: false, reason: "missing-runtime-graph" };
|
||||
}
|
||||
|
||||
const ownedChatId = normalizeIdentityValue(graphOwnedChatId);
|
||||
if (ownedChatId) {
|
||||
return { shouldRepair: false, reason: "graph-identity-present", chatId: ownedChatId };
|
||||
}
|
||||
|
||||
const normalizedStateChatId = normalizeIdentityValue(stateChatId);
|
||||
if (!normalizedStateChatId) {
|
||||
return { shouldRepair: false, reason: "missing-persistence-chat-id" };
|
||||
}
|
||||
|
||||
const liveChatId = normalizeIdentityValue(activeIdentity?.chatId);
|
||||
if (
|
||||
liveChatId &&
|
||||
!areChatIdsEquivalentForIdentityCore(normalizedStateChatId, liveChatId, {
|
||||
identity: activeIdentity,
|
||||
aliasCandidates,
|
||||
}) &&
|
||||
!areChatIdsEquivalentForIdentityCore(liveChatId, normalizedStateChatId, {
|
||||
identity: activeIdentity,
|
||||
aliasCandidates,
|
||||
})
|
||||
) {
|
||||
return {
|
||||
shouldRepair: false,
|
||||
reason: "live-chat-mismatch",
|
||||
chatId: normalizedStateChatId,
|
||||
liveChatId,
|
||||
};
|
||||
}
|
||||
|
||||
const normalizedMarkerChatId = normalizeIdentityValue(markerChatId);
|
||||
if (normalizedMarkerChatId && normalizedMarkerChatId !== normalizedStateChatId) {
|
||||
return {
|
||||
shouldRepair: false,
|
||||
reason: "commit-marker-chat-mismatch",
|
||||
chatId: normalizedStateChatId,
|
||||
markerChatId: normalizedMarkerChatId,
|
||||
};
|
||||
}
|
||||
|
||||
return { shouldRepair: true, reason: "repair", chatId: normalizedStateChatId };
|
||||
}
|
||||
@@ -101,6 +101,17 @@ import {
|
||||
getPersistedSettingsSnapshot,
|
||||
mergePersistedSettings,
|
||||
} from "../runtime/settings-defaults.js";
|
||||
import {
|
||||
areChatIdsEquivalentForIdentityCore,
|
||||
canMutateRuntimeGraphForIdentityCore,
|
||||
doesChatIdMatchIdentityCore,
|
||||
planRuntimeGraphIdentityRepairCore,
|
||||
resolveActiveHostChatIdCore,
|
||||
resolveCurrentChatIdentityCore,
|
||||
resolveGraphOwnerIdentityCore,
|
||||
resolvePersistenceChatIdCore,
|
||||
resolveRuntimeGraphFallbackIdentityCore,
|
||||
} from "../runtime/identity-resolver.js";
|
||||
import {
|
||||
createDefaultAuthorityCapabilityState,
|
||||
normalizeAuthoritySettings,
|
||||
@@ -882,7 +893,9 @@ async function createGraphPersistenceHarness({
|
||||
return serializeBmeChatStateTarget(target);
|
||||
},
|
||||
readPersistedRecallFromUserMessage,
|
||||
areChatIdsEquivalentForIdentityCore,
|
||||
cloneGraphForPersistence,
|
||||
canMutateRuntimeGraphForIdentityCore,
|
||||
buildGraphCommitMarker,
|
||||
buildGraphChatStateSnapshot,
|
||||
buildLukerGraphCheckpointV2,
|
||||
@@ -892,6 +905,7 @@ async function createGraphPersistenceHarness({
|
||||
canUseGraphChatState,
|
||||
cloneRuntimeDebugValue,
|
||||
deleteGraphChatStateNamespace,
|
||||
doesChatIdMatchIdentityCore,
|
||||
detectIndexedDbSnapshotCommitMarkerMismatch,
|
||||
onMessageReceivedController,
|
||||
GRAPH_CHAT_STATE_NAMESPACE,
|
||||
@@ -916,6 +930,7 @@ async function createGraphPersistenceHarness({
|
||||
GRAPH_SHADOW_SNAPSHOT_STORAGE_PREFIX,
|
||||
GRAPH_STARTUP_RECONCILE_DELAYS_MS,
|
||||
MODULE_NAME,
|
||||
planRuntimeGraphIdentityRepairCore,
|
||||
findGraphShadowSnapshotByIntegrity,
|
||||
normalizeGraphCommitMarker,
|
||||
readGraphChatStateNamespaces,
|
||||
@@ -925,6 +940,11 @@ async function createGraphPersistenceHarness({
|
||||
readGraphShadowSnapshot,
|
||||
rememberGraphIdentityAlias,
|
||||
removeGraphShadowSnapshot,
|
||||
resolveActiveHostChatIdCore,
|
||||
resolveCurrentChatIdentityCore,
|
||||
resolveGraphOwnerIdentityCore,
|
||||
resolvePersistenceChatIdCore,
|
||||
resolveRuntimeGraphFallbackIdentityCore,
|
||||
resolveGraphIdentityAliasByHostChatId,
|
||||
shouldPreferShadowSnapshotOverOfficial,
|
||||
stampGraphPersistenceMeta,
|
||||
|
||||
198
tests/identity-resolver.mjs
Normal file
198
tests/identity-resolver.mjs
Normal file
@@ -0,0 +1,198 @@
|
||||
// ST-BME restrained rebirth — Phase 1 identity resolver characterization.
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
areChatIdsEquivalentForIdentityCore,
|
||||
canMutateRuntimeGraphForIdentityCore,
|
||||
doesChatIdMatchIdentityCore,
|
||||
planRuntimeGraphIdentityRepairCore,
|
||||
resolveActiveHostChatIdCore,
|
||||
resolveCurrentChatIdentityCore,
|
||||
resolveGraphOwnerIdentityCore,
|
||||
resolvePersistenceChatIdCore,
|
||||
resolveRuntimeGraphFallbackIdentityCore,
|
||||
} from "../runtime/identity-resolver.js";
|
||||
|
||||
const context = {
|
||||
chatId: "host-chat",
|
||||
chatMetadata: {
|
||||
integrity: "integrity-chat",
|
||||
},
|
||||
chat: [{ mes: "hello" }],
|
||||
};
|
||||
|
||||
assert.equal(resolveActiveHostChatIdCore({ context }), "host-chat");
|
||||
|
||||
const activeIdentity = resolveCurrentChatIdentityCore({
|
||||
context,
|
||||
resolveAliasByHostChatId: () => "alias-chat",
|
||||
});
|
||||
assert.deepEqual(activeIdentity, {
|
||||
chatId: "integrity-chat",
|
||||
hostChatId: "host-chat",
|
||||
integrity: "integrity-chat",
|
||||
identitySource: "integrity",
|
||||
hasLikelySelectedChat: true,
|
||||
});
|
||||
|
||||
const aliasIdentity = resolveCurrentChatIdentityCore({
|
||||
context: { chatId: "host-only", chatMetadata: {}, characterId: "1" },
|
||||
resolveAliasByHostChatId: () => "persisted-by-alias",
|
||||
});
|
||||
assert.equal(aliasIdentity.chatId, "persisted-by-alias");
|
||||
assert.equal(aliasIdentity.identitySource, "alias");
|
||||
|
||||
console.log(" ✓ active identity is resolved from context and aliases only");
|
||||
|
||||
const graph = { historyState: { chatId: "graph-chat" } };
|
||||
const graphMeta = { chatId: "meta-chat", integrity: "meta-integrity" };
|
||||
assert.deepEqual(resolveGraphOwnerIdentityCore({ graph, graphMeta }), {
|
||||
chatId: "graph-chat",
|
||||
source: "history-state",
|
||||
integrity: "meta-integrity",
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
resolveRuntimeGraphFallbackIdentityCore({
|
||||
graph: { historyState: {} },
|
||||
graphMeta: {},
|
||||
persistenceState: {
|
||||
chatId: "state-chat",
|
||||
queuedPersistChatId: "queued-chat",
|
||||
commitMarker: { chatId: "marker-chat" },
|
||||
},
|
||||
}),
|
||||
{ chatId: "state-chat", source: "runtime-fallback" },
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
resolvePersistenceChatIdCore({
|
||||
explicitChatId: "",
|
||||
activeIdentity: { chatId: "" },
|
||||
graph: { historyState: { chatId: "graph-owned" } },
|
||||
graphMeta: {},
|
||||
persistenceState: { chatId: "state-chat" },
|
||||
}),
|
||||
"graph-owned",
|
||||
);
|
||||
|
||||
console.log(" ✓ graph-owner and runtime fallback identities stay separate");
|
||||
|
||||
const identity = {
|
||||
chatId: "integrity-chat",
|
||||
hostChatId: "host-chat",
|
||||
integrity: "integrity-chat",
|
||||
};
|
||||
const aliasCandidates = ["alias-chat", "old-host-chat"];
|
||||
assert.equal(doesChatIdMatchIdentityCore("old-host-chat", { identity, aliasCandidates }), true);
|
||||
assert.equal(doesChatIdMatchIdentityCore("other-chat", { identity, aliasCandidates }), false);
|
||||
assert.equal(
|
||||
areChatIdsEquivalentForIdentityCore("host-chat", "old-host-chat", {
|
||||
identity,
|
||||
aliasCandidates,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
|
||||
console.log(" ✓ equivalence uses explicit identity evidence and aliases");
|
||||
|
||||
assert.equal(
|
||||
canMutateRuntimeGraphForIdentityCore({
|
||||
graph: { historyState: { chatId: "integrity-chat" } },
|
||||
activeIdentity: identity,
|
||||
graphOwnedChatId: "integrity-chat",
|
||||
persistenceState: { loadState: "loaded" },
|
||||
}),
|
||||
true,
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
canMutateRuntimeGraphForIdentityCore({
|
||||
graph: { historyState: { chatId: "graph-chat" } },
|
||||
activeIdentity: { chatId: "" },
|
||||
graphOwnedChatId: "graph-chat",
|
||||
persistenceState: {
|
||||
chatId: "graph-chat",
|
||||
commitMarker: { chatId: "other-chat" },
|
||||
loadState: "no-chat",
|
||||
dbReady: false,
|
||||
},
|
||||
allowNoChatState: true,
|
||||
}),
|
||||
false,
|
||||
"wrong-chat commit marker must block no-chat mutation fallback",
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
canMutateRuntimeGraphForIdentityCore({
|
||||
graph: { historyState: { chatId: "graph-chat" } },
|
||||
activeIdentity: { chatId: "" },
|
||||
graphOwnedChatId: "graph-chat",
|
||||
persistenceState: {
|
||||
chatId: "graph-chat",
|
||||
commitMarker: { chatId: "graph-chat" },
|
||||
loadState: "no-chat",
|
||||
dbReady: false,
|
||||
},
|
||||
allowNoChatState: true,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
|
||||
console.log(" ✓ runtime mutation fallback preserves no-chat safety checks");
|
||||
|
||||
assert.deepEqual(
|
||||
planRuntimeGraphIdentityRepairCore({
|
||||
graph: { historyState: {} },
|
||||
graphOwnedChatId: "",
|
||||
stateChatId: "state-chat",
|
||||
activeIdentity: { chatId: "" },
|
||||
markerChatId: "state-chat",
|
||||
}),
|
||||
{ shouldRepair: true, reason: "repair", chatId: "state-chat" },
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
planRuntimeGraphIdentityRepairCore({
|
||||
graph: { historyState: {} },
|
||||
graphOwnedChatId: "",
|
||||
stateChatId: "state-chat",
|
||||
activeIdentity: { chatId: "live-chat" },
|
||||
markerChatId: "state-chat",
|
||||
}).reason,
|
||||
"live-chat-mismatch",
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
planRuntimeGraphIdentityRepairCore({
|
||||
graph: { historyState: {} },
|
||||
graphOwnedChatId: "",
|
||||
stateChatId: "state-chat",
|
||||
activeIdentity: { chatId: "" },
|
||||
markerChatId: "other-chat",
|
||||
}).reason,
|
||||
"commit-marker-chat-mismatch",
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
planRuntimeGraphIdentityRepairCore({
|
||||
graph: { historyState: { chatId: "already-owned" } },
|
||||
graphOwnedChatId: "already-owned",
|
||||
stateChatId: "state-chat",
|
||||
activeIdentity: { chatId: "" },
|
||||
}),
|
||||
{ shouldRepair: false, reason: "graph-identity-present", chatId: "already-owned" },
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
planRuntimeGraphIdentityRepairCore({
|
||||
graph: { historyState: {} },
|
||||
graphOwnedChatId: "",
|
||||
stateChatId: "",
|
||||
activeIdentity: { chatId: "" },
|
||||
}),
|
||||
{ shouldRepair: false, reason: "missing-persistence-chat-id" },
|
||||
);
|
||||
|
||||
console.log(" ✓ graph identity repair is planned only with non-conflicting evidence");
|
||||
console.log("identity-resolver tests passed");
|
||||
Reference in New Issue
Block a user