import { getScopeOwnerKey, getScopeRegionKey, normalizeMemoryScope, } from "./memory-scope.js"; export const KNOWLEDGE_STATE_VERSION = 1; export const REGION_STATE_VERSION = 1; const OWNER_TYPE_CHARACTER = "character"; const OWNER_TYPE_USER = "user"; const KNOWLEDGE_OWNER_PREFIX = { [OWNER_TYPE_CHARACTER]: "character", [OWNER_TYPE_USER]: "user", }; const DEFAULT_VISIBILITY_SCORE = 0; const KNOWLEDGE_ENTRY_LIMIT = 512; const RECENT_REGION_LIMIT = 8; const RECENT_RECALL_OWNER_LIMIT = 8; function normalizeString(value) { return String(value ?? "").trim(); } function normalizeKey(value) { return normalizeString(value).toLowerCase(); } function clampScore(value, fallback = DEFAULT_VISIBILITY_SCORE) { const parsed = Number(value); if (!Number.isFinite(parsed)) return fallback; return Math.max(0, Math.min(1, parsed)); } function uniqueStrings(values = []) { const result = []; const seen = new Set(); for (const value of Array.isArray(values) ? values : [values]) { const normalized = normalizeString(value); const key = normalizeKey(normalized); if (!normalized || seen.has(key)) continue; seen.add(key); result.push(normalized); } return result; } function uniqueIds(values = []) { const result = []; const seen = new Set(); for (const value of Array.isArray(values) ? values : [values]) { const normalized = normalizeString(value); if (!normalized || seen.has(normalized)) continue; seen.add(normalized); result.push(normalized); } return result.slice(0, KNOWLEDGE_ENTRY_LIMIT); } function normalizeOwnerType(ownerType = "") { const normalized = normalizeString(ownerType); if (normalized === OWNER_TYPE_CHARACTER) return OWNER_TYPE_CHARACTER; if (normalized === OWNER_TYPE_USER) return OWNER_TYPE_USER; return ""; } function getCharacterNodes(graph) { return Array.isArray(graph?.nodes) ? graph.nodes.filter( (node) => node && !node.archived && node.type === "character" && normalizeString(node?.fields?.name), ) : []; } function buildCharacterNameCountMap(graph) { const counts = new Map(); for (const node of getCharacterNodes(graph)) { const key = normalizeKey(node?.fields?.name); if (!key) continue; counts.set(key, (counts.get(key) || 0) + 1); } return counts; } function findCharacterNodeByName(graph, ownerName = "") { const normalizedOwnerName = normalizeKey(ownerName); if (!normalizedOwnerName) return []; return getCharacterNodes(graph).filter( (node) => normalizeKey(node?.fields?.name) === normalizedOwnerName, ); } function findCharacterNodeById(graph, nodeId = "") { const normalizedNodeId = normalizeString(nodeId); if (!normalizedNodeId) return null; return getCharacterNodes(graph).find((node) => node.id === normalizedNodeId) || null; } function buildOwnerKey(ownerType, ownerNameOrId = "", nodeId = "", graph = null) { const normalizedOwnerType = normalizeOwnerType(ownerType); const normalizedOwnerNameOrId = normalizeKey(ownerNameOrId); if (!normalizedOwnerType || !normalizedOwnerNameOrId) return ""; if (normalizedOwnerType === OWNER_TYPE_USER) { return `${KNOWLEDGE_OWNER_PREFIX[OWNER_TYPE_USER]}:${normalizedOwnerNameOrId}`; } const duplicateCounts = graph ? buildCharacterNameCountMap(graph) : new Map(); const isDuplicated = (duplicateCounts.get(normalizedOwnerNameOrId) || 0) > 1; const normalizedNodeId = normalizeString(nodeId); if (isDuplicated && normalizedNodeId) { return `${KNOWLEDGE_OWNER_PREFIX[OWNER_TYPE_CHARACTER]}:${normalizedOwnerNameOrId}#${normalizedNodeId}`; } return `${KNOWLEDGE_OWNER_PREFIX[OWNER_TYPE_CHARACTER]}:${normalizedOwnerNameOrId}`; } export function createDefaultKnowledgeOwnerState(overrides = {}) { const ownerType = normalizeOwnerType(overrides.ownerType); const ownerName = normalizeString(overrides.ownerName); const nodeId = normalizeString(overrides.nodeId); const ownerKey = normalizeString(overrides.ownerKey) || buildOwnerKey(ownerType, ownerName || overrides.ownerId, nodeId); const visibilityScores = {}; if ( overrides.visibilityScores && typeof overrides.visibilityScores === "object" && !Array.isArray(overrides.visibilityScores) ) { for (const [nodeIdKey, score] of Object.entries(overrides.visibilityScores)) { const normalizedNodeId = normalizeString(nodeIdKey); if (!normalizedNodeId) continue; visibilityScores[normalizedNodeId] = clampScore(score); } } return { ownerType, ownerKey, ownerName, nodeId, aliases: uniqueStrings(overrides.aliases || [ownerName]), knownNodeIds: uniqueIds(overrides.knownNodeIds), mistakenNodeIds: uniqueIds(overrides.mistakenNodeIds), visibilityScores, manualKnownNodeIds: uniqueIds(overrides.manualKnownNodeIds), manualHiddenNodeIds: uniqueIds(overrides.manualHiddenNodeIds), updatedAt: Number.isFinite(overrides.updatedAt) ? overrides.updatedAt : 0, lastSource: normalizeString(overrides.lastSource), }; } export function createDefaultKnowledgeState(overrides = {}) { return { version: KNOWLEDGE_STATE_VERSION, owners: overrides.owners && typeof overrides.owners === "object" && !Array.isArray(overrides.owners) ? { ...overrides.owners } : {}, }; } function normalizeRegionAdjacencyEntry(regionName = "", entry = {}) { return { adjacent: uniqueStrings(entry.adjacent), aliases: uniqueStrings(entry.aliases), source: normalizeString(entry.source), updatedAt: Number.isFinite(entry.updatedAt) ? entry.updatedAt : 0, region: normalizeString(regionName), }; } export function createDefaultRegionState(overrides = {}) { return { version: REGION_STATE_VERSION, adjacencyMap: overrides.adjacencyMap && typeof overrides.adjacencyMap === "object" && !Array.isArray(overrides.adjacencyMap) ? { ...overrides.adjacencyMap } : {}, manualActiveRegion: normalizeString(overrides.manualActiveRegion), recentRegions: uniqueStrings(overrides.recentRegions).slice(0, RECENT_REGION_LIMIT), }; } function mergeKnowledgeOwnerEntries(baseEntry, incomingEntry) { const merged = createDefaultKnowledgeOwnerState({ ...baseEntry, ...incomingEntry, aliases: uniqueStrings([ ...(baseEntry?.aliases || []), ...(incomingEntry?.aliases || []), incomingEntry?.ownerName || "", baseEntry?.ownerName || "", ]), knownNodeIds: uniqueIds([ ...(baseEntry?.knownNodeIds || []), ...(incomingEntry?.knownNodeIds || []), ]), mistakenNodeIds: uniqueIds([ ...(baseEntry?.mistakenNodeIds || []), ...(incomingEntry?.mistakenNodeIds || []), ]), manualKnownNodeIds: uniqueIds([ ...(baseEntry?.manualKnownNodeIds || []), ...(incomingEntry?.manualKnownNodeIds || []), ]), manualHiddenNodeIds: uniqueIds([ ...(baseEntry?.manualHiddenNodeIds || []), ...(incomingEntry?.manualHiddenNodeIds || []), ]), updatedAt: Math.max( Number(baseEntry?.updatedAt || 0), Number(incomingEntry?.updatedAt || 0), ), lastSource: normalizeString(incomingEntry?.lastSource) || normalizeString(baseEntry?.lastSource), }); const visibilityScores = { ...(baseEntry?.visibilityScores || {}), }; for (const [nodeId, value] of Object.entries(incomingEntry?.visibilityScores || {})) { visibilityScores[nodeId] = Math.max( clampScore(visibilityScores[nodeId]), clampScore(value), ); } merged.visibilityScores = visibilityScores; return merged; } function resolveCanonicalKnowledgeEntry(graph, ownerKey, entry) { const normalizedEntry = createDefaultKnowledgeOwnerState({ ...entry, ownerKey, }); const resolvedOwner = resolveKnowledgeOwner(graph, { ownerType: normalizedEntry.ownerType, ownerName: normalizedEntry.ownerName, ownerId: normalizedEntry.ownerName, nodeId: normalizedEntry.nodeId, aliases: normalizedEntry.aliases, }); return createDefaultKnowledgeOwnerState({ ...normalizedEntry, ownerType: resolvedOwner.ownerType || normalizedEntry.ownerType, ownerKey: resolvedOwner.ownerKey || normalizedEntry.ownerKey, ownerName: resolvedOwner.ownerName || normalizedEntry.ownerName, nodeId: resolvedOwner.nodeId || normalizedEntry.nodeId, aliases: uniqueStrings([ ...(normalizedEntry.aliases || []), ...(resolvedOwner.aliases || []), resolvedOwner.ownerName || "", ]), }); } export function normalizeKnowledgeState(state = {}, graph = null) { const normalized = createDefaultKnowledgeState(state); const owners = {}; for (const [ownerKey, rawEntry] of Object.entries(normalized.owners || {})) { const canonicalEntry = resolveCanonicalKnowledgeEntry(graph, ownerKey, rawEntry); if (!canonicalEntry.ownerKey) continue; owners[canonicalEntry.ownerKey] = owners[canonicalEntry.ownerKey] ? mergeKnowledgeOwnerEntries(owners[canonicalEntry.ownerKey], canonicalEntry) : canonicalEntry; } return { version: KNOWLEDGE_STATE_VERSION, owners, }; } export function normalizeRegionState(state = {}) { const normalized = createDefaultRegionState(state); const adjacencyMap = {}; for (const [regionName, entry] of Object.entries(normalized.adjacencyMap || {})) { const normalizedRegion = normalizeString(regionName || entry?.region); if (!normalizedRegion) continue; adjacencyMap[normalizedRegion] = normalizeRegionAdjacencyEntry( normalizedRegion, entry, ); } return { version: REGION_STATE_VERSION, adjacencyMap, manualActiveRegion: normalized.manualActiveRegion, recentRegions: uniqueStrings(normalized.recentRegions).slice(0, RECENT_REGION_LIMIT), }; } export function normalizeGraphCognitiveState(graph) { if (!graph || typeof graph !== "object") return graph; graph.knowledgeState = normalizeKnowledgeState(graph.knowledgeState, graph); graph.regionState = normalizeRegionState(graph.regionState); return graph; } export function resolveKnowledgeOwner(graph, input = {}) { const ownerType = normalizeOwnerType(input.ownerType); if (!ownerType) { return { ownerType: "", ownerKey: "", ownerName: "", nodeId: "", aliases: [], }; } if (ownerType === OWNER_TYPE_USER) { const ownerName = normalizeString( input.ownerName || input.ownerId || input.ownerKey, ); return { ownerType, ownerKey: buildOwnerKey(ownerType, ownerName), ownerName, nodeId: "", aliases: uniqueStrings(input.aliases || [ownerName]), }; } let ownerName = normalizeString(input.ownerName || input.ownerId); let nodeId = normalizeString(input.nodeId || input.ownerNodeId); const explicitNode = findCharacterNodeById(graph, nodeId); if (explicitNode) { nodeId = explicitNode.id; ownerName = ownerName || normalizeString(explicitNode?.fields?.name); } if (!nodeId && ownerName) { const matches = findCharacterNodeByName(graph, ownerName); if (matches.length === 1) { nodeId = matches[0].id; } } const aliases = uniqueStrings(input.aliases || [ownerName]); const ownerKey = buildOwnerKey(ownerType, ownerName || input.ownerId, nodeId, graph); return { ownerType, ownerKey, ownerName, nodeId, aliases, }; } export function resolveKnowledgeOwnerKeyFromScope(graph, scope = {}) { const normalizedScope = normalizeMemoryScope(scope); return resolveKnowledgeOwner(graph, { ownerType: normalizedScope.ownerType, ownerName: normalizedScope.ownerName, ownerId: normalizedScope.ownerId, }).ownerKey; } export function ensureKnowledgeOwnerState(graph, input = {}, patch = {}) { normalizeGraphCognitiveState(graph); const requestedOwnerKey = normalizeString(input.ownerKey); if (requestedOwnerKey && graph.knowledgeState.owners[requestedOwnerKey]) { const existingEntry = graph.knowledgeState.owners[requestedOwnerKey]; const nextEntry = mergeKnowledgeOwnerEntries( existingEntry, createDefaultKnowledgeOwnerState({ ...existingEntry, ...patch, ownerKey: requestedOwnerKey, }), ); graph.knowledgeState.owners[requestedOwnerKey] = nextEntry; return { ownerKey: requestedOwnerKey, ownerState: nextEntry, owner: { ownerKey: requestedOwnerKey, ownerType: existingEntry.ownerType, ownerName: existingEntry.ownerName, nodeId: existingEntry.nodeId, aliases: existingEntry.aliases || [], }, }; } const owner = resolveKnowledgeOwner(graph, input); if (!owner.ownerKey) { return { ownerKey: "", ownerState: null, owner }; } const existing = graph.knowledgeState.owners[owner.ownerKey]; const nextEntry = mergeKnowledgeOwnerEntries( existing || createDefaultKnowledgeOwnerState(owner), createDefaultKnowledgeOwnerState({ ...(existing || {}), ...owner, ...patch, aliases: uniqueStrings([ ...(existing?.aliases || []), ...(owner.aliases || []), ...(patch.aliases || []), ]), }), ); graph.knowledgeState.owners[owner.ownerKey] = nextEntry; return { ownerKey: owner.ownerKey, ownerState: nextEntry, owner, }; } function pushRecentRegion(regionState, region) { const normalizedRegion = normalizeString(region); if (!normalizedRegion) return; regionState.recentRegions = uniqueStrings([ normalizedRegion, ...(regionState.recentRegions || []), ]).slice(0, RECENT_REGION_LIMIT); } function buildRegionAliasLookup(regionState = {}) { const lookup = new Map(); for (const [regionName, entry] of Object.entries(regionState.adjacencyMap || {})) { const normalizedRegionName = normalizeString(regionName); if (!normalizedRegionName) continue; lookup.set(normalizeKey(normalizedRegionName), normalizedRegionName); for (const alias of uniqueStrings(entry?.aliases)) { lookup.set(normalizeKey(alias), normalizedRegionName); } } return lookup; } export function resolveCanonicalRegionName(regionState = {}, region = "") { const normalizedRegion = normalizeString(region); if (!normalizedRegion) return ""; const aliasLookup = buildRegionAliasLookup(regionState); return aliasLookup.get(normalizeKey(normalizedRegion)) || normalizedRegion; } export function resolveAdjacentRegions(graph, activeRegion = "") { const regionState = normalizeRegionState(graph?.regionState); const canonicalRegion = resolveCanonicalRegionName(regionState, activeRegion); const entry = canonicalRegion ? regionState.adjacencyMap?.[canonicalRegion] || null : null; return { canonicalRegion, adjacentRegions: uniqueStrings(entry?.adjacent || []), }; } export function resolveActiveRegionContext(graph, preferredRegion = "") { const regionState = normalizeRegionState(graph?.regionState); const manualActiveRegion = normalizeString(regionState.manualActiveRegion); if (manualActiveRegion) { return { activeRegion: resolveCanonicalRegionName(regionState, manualActiveRegion), source: "manual", }; } const preferred = normalizeString(preferredRegion); if (preferred) { return { activeRegion: resolveCanonicalRegionName(regionState, preferred), source: normalizeString(graph?.historyState?.activeRegionSource) || "runtime", }; } const historyRegion = normalizeString(graph?.historyState?.activeRegion); if (historyRegion) { return { activeRegion: resolveCanonicalRegionName(regionState, historyRegion), source: normalizeString(graph?.historyState?.activeRegionSource) || "history", }; } const extractedRegion = normalizeString(graph?.historyState?.lastExtractedRegion); if (extractedRegion) { return { activeRegion: resolveCanonicalRegionName(regionState, extractedRegion), source: "extract", }; } const recentRegion = uniqueStrings(regionState.recentRegions)[0] || ""; if (recentRegion) { return { activeRegion: resolveCanonicalRegionName(regionState, recentRegion), source: "recent", }; } const fallbackRegion = (Array.isArray(graph?.nodes) ? graph.nodes : []) .filter((node) => node && !node.archived) .map((node) => getScopeRegionKey(node?.scope)) .map((region) => normalizeString(region)) .find(Boolean); return { activeRegion: resolveCanonicalRegionName(regionState, fallbackRegion), source: fallbackRegion ? "graph" : "", }; } function resolveNodeIdRef(ref, refMap = null) { const normalizedRef = normalizeString(ref); if (!normalizedRef) return ""; if (refMap instanceof Map && refMap.has(normalizedRef)) { return normalizeString(refMap.get(normalizedRef)); } return normalizedRef; } function collectRefNodeIds(refs = [], refMap = null) { return uniqueIds( (Array.isArray(refs) ? refs : [refs]).map((ref) => resolveNodeIdRef(ref, refMap)), ); } function normalizeVisibilityEntries(entries = [], refMap = null) { const result = []; for (const entry of Array.isArray(entries) ? entries : []) { const nodeId = resolveNodeIdRef(entry?.ref || entry?.nodeId || "", refMap); if (!nodeId) continue; result.push({ nodeId, score: clampScore(entry?.score, DEFAULT_VISIBILITY_SCORE), reason: normalizeString(entry?.reason), }); } return result; } function collectCharacterMentionOwners(graph, node) { const text = [ node?.fields?.name, node?.fields?.title, node?.fields?.summary, node?.fields?.participants, node?.fields?.state, node?.fields?.belief, node?.fields?.attitude, node?.fields?.about, ] .filter((value) => value != null) .join(" "); if (!text) return []; const loweredText = normalizeKey(text); const owners = []; for (const characterNode of getCharacterNodes(graph)) { const ownerName = normalizeString(characterNode?.fields?.name); if (!ownerName) continue; if (!loweredText.includes(normalizeKey(ownerName))) continue; owners.push( resolveKnowledgeOwner(graph, { ownerType: OWNER_TYPE_CHARACTER, ownerName, nodeId: characterNode.id, }), ); } return owners.filter((owner) => owner.ownerKey); } function applyVisibilityPatch(ownerState, nodeId, score) { const normalizedNodeId = normalizeString(nodeId); if (!normalizedNodeId) return; ownerState.visibilityScores[normalizedNodeId] = Math.max( clampScore(ownerState.visibilityScores[normalizedNodeId]), clampScore(score), ); } function applyKnowledgeNodeIds(ownerState, fieldName, nodeIds = []) { ownerState[fieldName] = uniqueIds([ ...(ownerState[fieldName] || []), ...nodeIds, ]); } function inferKnowledgeFromChangedNodes( graph, { changedNodeIds = [], scopeRuntime = {}, refMap = null, source = "extract-infer", } = {}, ) { const normalizedChangedNodeIds = uniqueIds(changedNodeIds); if (normalizedChangedNodeIds.length === 0) return; const activeCharacterOwner = resolveKnowledgeOwner(graph, { ownerType: OWNER_TYPE_CHARACTER, ownerName: scopeRuntime.activeCharacterOwner, }); const activeUserOwner = resolveKnowledgeOwner(graph, { ownerType: OWNER_TYPE_USER, ownerName: scopeRuntime.activeUserOwner, }); for (const nodeId of normalizedChangedNodeIds) { const node = Array.isArray(graph?.nodes) ? graph.nodes.find((item) => item?.id === nodeId) : null; if (!node || node.archived) continue; const nodeScope = normalizeMemoryScope(node.scope); if (node.type === "pov_memory" && nodeScope.layer === "pov") { const ownerResult = ensureKnowledgeOwnerState( graph, { ownerType: nodeScope.ownerType, ownerName: nodeScope.ownerName, ownerId: nodeScope.ownerId, }, { updatedAt: Date.now(), lastSource: source, }, ); if (!ownerResult.ownerState) continue; applyKnowledgeNodeIds(ownerResult.ownerState, "knownNodeIds", [node.id]); applyVisibilityPatch(ownerResult.ownerState, node.id, 1); const aboutNodeIds = collectRefNodeIds(node?.fields?.about, refMap); if (String(node?.fields?.certainty || "").trim() === "mistaken") { applyKnowledgeNodeIds( ownerResult.ownerState, "mistakenNodeIds", aboutNodeIds, ); } else { applyKnowledgeNodeIds(ownerResult.ownerState, "knownNodeIds", aboutNodeIds); aboutNodeIds.forEach((aboutNodeId) => applyVisibilityPatch(ownerResult.ownerState, aboutNodeId, 0.95), ); } continue; } if (node.type === "character") { const selfOwner = ensureKnowledgeOwnerState( graph, { ownerType: OWNER_TYPE_CHARACTER, ownerName: node?.fields?.name, nodeId: node.id, }, { updatedAt: Date.now(), lastSource: source, }, ); if (selfOwner.ownerState) { applyKnowledgeNodeIds(selfOwner.ownerState, "knownNodeIds", [node.id]); applyVisibilityPatch(selfOwner.ownerState, node.id, 1); } } const mentionedOwners = collectCharacterMentionOwners(graph, node); for (const mentionedOwner of mentionedOwners) { const ownerResult = ensureKnowledgeOwnerState(graph, mentionedOwner, { updatedAt: Date.now(), lastSource: source, }); if (!ownerResult.ownerState) continue; applyKnowledgeNodeIds(ownerResult.ownerState, "knownNodeIds", [node.id]); applyVisibilityPatch(ownerResult.ownerState, node.id, 0.92); } if (activeCharacterOwner.ownerKey && !mentionedOwners.length) { const ownerResult = ensureKnowledgeOwnerState(graph, activeCharacterOwner, { updatedAt: Date.now(), lastSource: source, }); if (ownerResult.ownerState) { applyVisibilityPatch(ownerResult.ownerState, node.id, 0.55); } } if (activeUserOwner.ownerKey) { const ownerResult = ensureKnowledgeOwnerState(graph, activeUserOwner, { updatedAt: Date.now(), lastSource: source, }); if (ownerResult.ownerState) { applyVisibilityPatch(ownerResult.ownerState, node.id, 0.7); } } } } export function applyCognitionUpdates( graph, cognitionUpdates = [], { refMap = null, changedNodeIds = [], scopeRuntime = {}, source = "extract", } = {}, ) { normalizeGraphCognitiveState(graph); const now = Date.now(); for (const update of Array.isArray(cognitionUpdates) ? cognitionUpdates : []) { const ownerResult = ensureKnowledgeOwnerState( graph, { ownerType: update?.ownerType, ownerName: update?.ownerName, ownerId: update?.ownerId, nodeId: update?.ownerNodeId, }, { updatedAt: now, lastSource: source, }, ); const ownerState = ownerResult.ownerState; if (!ownerState) continue; const knownNodeIds = collectRefNodeIds(update?.knownRefs, refMap); const mistakenNodeIds = collectRefNodeIds(update?.mistakenRefs, refMap); const visibilityEntries = normalizeVisibilityEntries(update?.visibility, refMap); applyKnowledgeNodeIds(ownerState, "knownNodeIds", knownNodeIds); applyKnowledgeNodeIds(ownerState, "mistakenNodeIds", mistakenNodeIds); for (const nodeId of knownNodeIds) { applyVisibilityPatch(ownerState, nodeId, 1); } for (const entry of visibilityEntries) { applyVisibilityPatch(ownerState, entry.nodeId, entry.score); } } inferKnowledgeFromChangedNodes(graph, { changedNodeIds, scopeRuntime, refMap, source: `${source}-heuristic`, }); graph.knowledgeState = normalizeKnowledgeState(graph.knowledgeState, graph); return graph.knowledgeState; } function mergeAdjacencyEntry(regionState, regionName, adjacent = [], source = "") { const normalizedRegionName = normalizeString(regionName); if (!normalizedRegionName) return; const existingEntry = regionState.adjacencyMap[normalizedRegionName]; regionState.adjacencyMap[normalizedRegionName] = normalizeRegionAdjacencyEntry( normalizedRegionName, { ...(existingEntry || {}), adjacent: uniqueStrings([...(existingEntry?.adjacent || []), ...adjacent]), source: normalizeString(source) || normalizeString(existingEntry?.source), updatedAt: Date.now(), }, ); } export function applyRegionUpdates( graph, regionUpdates = null, { changedNodeIds = [], source = "extract", } = {}, ) { normalizeGraphCognitiveState(graph); const regionState = graph.regionState; const historyState = graph.historyState || {}; const normalizedUpdates = regionUpdates && typeof regionUpdates === "object" && !Array.isArray(regionUpdates) ? regionUpdates : {}; for (const entry of Array.isArray(normalizedUpdates.adjacency) ? normalizedUpdates.adjacency : []) { const regionName = normalizeString(entry?.region); const adjacent = uniqueStrings(entry?.adjacent); if (!regionName || adjacent.length === 0) continue; mergeAdjacencyEntry(regionState, regionName, adjacent, entry?.source || source); for (const adjacentRegion of adjacent) { mergeAdjacencyEntry(regionState, adjacentRegion, [regionName], entry?.source || source); } } const candidateRegion = normalizeString(normalizedUpdates.activeRegionHint) || uniqueStrings( changedNodeIds .map((nodeId) => (Array.isArray(graph?.nodes) ? graph.nodes : []).find( (node) => node?.id === nodeId, ), ) .filter(Boolean) .map((node) => getScopeRegionKey(node?.scope)), )[0] || ""; if (candidateRegion) { const canonicalRegion = resolveCanonicalRegionName(regionState, candidateRegion); pushRecentRegion(regionState, canonicalRegion); historyState.lastExtractedRegion = canonicalRegion; if (!normalizeString(regionState.manualActiveRegion)) { historyState.activeRegion = canonicalRegion; historyState.activeRegionSource = source; } } else if (normalizeString(regionState.manualActiveRegion)) { historyState.activeRegion = resolveCanonicalRegionName( regionState, regionState.manualActiveRegion, ); historyState.activeRegionSource = "manual"; } graph.historyState = historyState; graph.regionState = normalizeRegionState(regionState); return graph.regionState; } function listToSet(values = []) { return new Set(uniqueIds(values)); } function normalizeOwnerKeyList(ownerKeys = []) { return uniqueIds(Array.isArray(ownerKeys) ? ownerKeys : [ownerKeys]); } function computeKnowledgeGateForSingleOwner( graph, node, ownerKey = "", { vectorScore = 0, graphScore = 0, lexicalScore = 0, scopeBucket = "", injectLowConfidenceObjectiveMemory = false, } = {}, ) { const normalizedOwnerKey = normalizeString(ownerKey); const scope = normalizeMemoryScope(node?.scope); const knowledgeState = normalizeKnowledgeState(graph?.knowledgeState, graph); const ownerState = knowledgeState.owners?.[normalizedOwnerKey] || null; if (!normalizedOwnerKey || !ownerState) { return { visible: true, anchored: false, rescued: false, suppressed: false, suppressedReason: "", visibilityScore: 0, mode: "no-owner-state", threshold: 0, }; } const manualKnownSet = listToSet(ownerState.manualKnownNodeIds); const knownSet = listToSet(ownerState.knownNodeIds); const mistakenSet = listToSet(ownerState.mistakenNodeIds); const manualHiddenSet = listToSet(ownerState.manualHiddenNodeIds); const ownerNodeKey = ownerState.nodeId ? `${OWNER_TYPE_CHARACTER}:${normalizeKey(ownerState.ownerName)}#${ownerState.nodeId}` : ownerState.ownerKey; const nodeOwnerKey = scope.layer === "pov" ? getScopeOwnerKey(scope) : ""; if (manualHiddenSet.has(node.id)) { return { visible: false, anchored: false, rescued: false, suppressed: true, suppressedReason: "manual-hidden", visibilityScore: 0, mode: "manual-hidden", threshold: 1, }; } if (scope.layer === "objective" && mistakenSet.has(node.id)) { return { visible: false, anchored: false, rescued: false, suppressed: true, suppressedReason: "mistaken-objective", visibilityScore: 0, mode: "mistaken-objective", threshold: 1, }; } const manualKnown = manualKnownSet.has(node.id); const anchored = manualKnown || knownSet.has(node.id) || (node.type === "character" && ownerState.nodeId && ownerState.nodeId === node.id) || (scope.layer === "pov" && nodeOwnerKey && nodeOwnerKey === ownerNodeKey); const baseVisibility = Math.max( clampScore(ownerState.visibilityScores?.[node.id]), anchored ? 1 : 0, ); let threshold = 0.4; if (scope.layer === "pov") { threshold = 0.18; } else if (scopeBucket === "objectiveCurrentRegion") { threshold = 0.34; } else if (scopeBucket === "objectiveAdjacentRegion") { threshold = 0.42; } else if (scopeBucket === "objectiveGlobal") { threshold = injectLowConfidenceObjectiveMemory ? 0 : 0.56; } const strongVector = Number(vectorScore) >= 0.82; const strongLexical = Number(lexicalScore) >= 0.85; const strongGraph = Number(graphScore) >= 1.1; const regionRescue = scopeBucket === "objectiveCurrentRegion" && (Number(vectorScore) >= 0.48 || Number(graphScore) >= 0.82); const rescued = !anchored && scope.layer === "objective" && !mistakenSet.has(node.id) && (strongVector || strongLexical || strongGraph || regionRescue); const visibilityScore = rescued ? Math.max(baseVisibility, 0.68) : baseVisibility; const visible = anchored || visibilityScore >= threshold || rescued; return { visible, anchored, rescued, suppressed: !visible, suppressedReason: visible ? "" : "low-visibility", visibilityScore, mode: anchored ? manualKnown ? "manual-known" : "anchored" : rescued ? "rescued" : visible ? "soft-visible" : "suppressed", threshold, }; } export function computeKnowledgeGateForNode( graph, node, ownerKey = "", { vectorScore = 0, graphScore = 0, lexicalScore = 0, scopeBucket = "", injectLowConfidenceObjectiveMemory = false, } = {}, ) { const normalizedOwnerKeys = normalizeOwnerKeyList(ownerKey); if (normalizedOwnerKeys.length <= 1) { const singleGate = computeKnowledgeGateForSingleOwner( graph, node, normalizedOwnerKeys[0] || "", { vectorScore, graphScore, lexicalScore, scopeBucket, injectLowConfidenceObjectiveMemory, }, ); return { ...singleGate, ownerCoverage: singleGate.visible ? 1 : 0, visibleOwnerKeys: singleGate.visible && normalizedOwnerKeys[0] ? [normalizedOwnerKeys[0]] : [], suppressedOwnerKeys: singleGate.visible || !normalizedOwnerKeys[0] ? [] : [normalizedOwnerKeys[0]], ownerResults: normalizedOwnerKeys[0] ? { [normalizedOwnerKeys[0]]: singleGate } : {}, }; } const ownerResults = {}; const visibleOwnerKeys = []; const suppressedOwnerKeys = []; let bestVisibilityScore = 0; let bestThreshold = 0; let anchored = false; let rescued = false; let bestMode = "suppressed"; let bestSuppressedReason = ""; for (const candidateOwnerKey of normalizedOwnerKeys) { const result = computeKnowledgeGateForSingleOwner( graph, node, candidateOwnerKey, { vectorScore, graphScore, lexicalScore, scopeBucket, injectLowConfidenceObjectiveMemory, }, ); ownerResults[candidateOwnerKey] = result; bestVisibilityScore = Math.max( bestVisibilityScore, Number(result.visibilityScore || 0), ); bestThreshold = Math.max(bestThreshold, Number(result.threshold || 0)); if (result.visible) { visibleOwnerKeys.push(candidateOwnerKey); anchored ||= Boolean(result.anchored); rescued ||= Boolean(result.rescued); if ( bestMode === "suppressed" || (result.anchored && bestMode !== "manual-known") || (result.mode === "manual-known") ) { bestMode = String(result.mode || bestMode); } } else { suppressedOwnerKeys.push(candidateOwnerKey); if (!bestSuppressedReason && result.suppressedReason) { bestSuppressedReason = String(result.suppressedReason || ""); } } } const visible = visibleOwnerKeys.length > 0; return { visible, anchored, rescued, suppressed: !visible, suppressedReason: visible ? "" : bestSuppressedReason || "low-visibility", visibilityScore: bestVisibilityScore, mode: visible ? bestMode : "suppressed", threshold: bestThreshold, ownerCoverage: normalizedOwnerKeys.length ? visibleOwnerKeys.length / normalizedOwnerKeys.length : 0, visibleOwnerKeys, suppressedOwnerKeys, ownerResults, }; } export function applyManualKnowledgeOverride( graph, { ownerKey = "", ownerType = "", ownerName = "", nodeId = "", mode = "known" } = {}, ) { normalizeGraphCognitiveState(graph); const resolvedOwner = normalizeString(ownerKey) || resolveKnowledgeOwner(graph, { ownerType, ownerName, }).ownerKey; if (!resolvedOwner || !normalizeString(nodeId)) { return { ok: false, reason: "missing-owner-or-node" }; } const ownerState = ensureKnowledgeOwnerState( graph, { ownerKey: resolvedOwner, ownerType, ownerName }, { updatedAt: Date.now(), lastSource: "manual", }, ).ownerState; if (!ownerState) { return { ok: false, reason: "owner-not-found" }; } ownerState.manualKnownNodeIds = uniqueIds(ownerState.manualKnownNodeIds); ownerState.manualHiddenNodeIds = uniqueIds(ownerState.manualHiddenNodeIds); ownerState.mistakenNodeIds = uniqueIds(ownerState.mistakenNodeIds); ownerState.manualKnownNodeIds = ownerState.manualKnownNodeIds.filter( (value) => value !== nodeId, ); ownerState.manualHiddenNodeIds = ownerState.manualHiddenNodeIds.filter( (value) => value !== nodeId, ); ownerState.mistakenNodeIds = ownerState.mistakenNodeIds.filter( (value) => value !== nodeId, ); if (mode === "known") { ownerState.manualKnownNodeIds.push(nodeId); } else if (mode === "hidden") { ownerState.manualHiddenNodeIds.push(nodeId); } else if (mode === "mistaken") { ownerState.mistakenNodeIds.push(nodeId); } ownerState.updatedAt = Date.now(); ownerState.lastSource = "manual"; graph.knowledgeState = normalizeKnowledgeState(graph.knowledgeState, graph); return { ok: true, ownerKey: resolvedOwner }; } export function clearManualKnowledgeOverride( graph, { ownerKey = "", ownerType = "", ownerName = "", nodeId = "" } = {}, ) { normalizeGraphCognitiveState(graph); const resolvedOwner = normalizeString(ownerKey) || resolveKnowledgeOwner(graph, { ownerType, ownerName, }).ownerKey; if (!resolvedOwner || !normalizeString(nodeId)) { return { ok: false, reason: "missing-owner-or-node" }; } const ownerState = graph.knowledgeState.owners?.[resolvedOwner]; if (!ownerState) { return { ok: false, reason: "owner-not-found" }; } ownerState.manualKnownNodeIds = (ownerState.manualKnownNodeIds || []).filter( (value) => value !== nodeId, ); ownerState.manualHiddenNodeIds = (ownerState.manualHiddenNodeIds || []).filter( (value) => value !== nodeId, ); ownerState.mistakenNodeIds = (ownerState.mistakenNodeIds || []).filter( (value) => value !== nodeId, ); ownerState.updatedAt = Date.now(); ownerState.lastSource = "manual-clear"; graph.knowledgeState = normalizeKnowledgeState(graph.knowledgeState, graph); return { ok: true, ownerKey: resolvedOwner }; } export function setManualActiveRegion(graph, region = "") { normalizeGraphCognitiveState(graph); const normalizedRegion = normalizeString(region); graph.regionState.manualActiveRegion = normalizedRegion; if (normalizedRegion) { graph.historyState.activeRegion = resolveCanonicalRegionName( graph.regionState, normalizedRegion, ); graph.historyState.activeRegionSource = "manual"; pushRecentRegion(graph.regionState, graph.historyState.activeRegion); } else if (graph.historyState.activeRegionSource === "manual") { graph.historyState.activeRegion = normalizeString( graph.historyState.lastExtractedRegion, ); graph.historyState.activeRegionSource = graph.historyState.activeRegion ? "extract" : ""; } graph.regionState = normalizeRegionState(graph.regionState); return { ok: true, activeRegion: normalizeString(graph.historyState.activeRegion), }; } export function updateRegionAdjacencyManual(graph, region = "", adjacent = []) { normalizeGraphCognitiveState(graph); const normalizedRegion = normalizeString(region); if (!normalizedRegion) { return { ok: false, reason: "missing-region" }; } mergeAdjacencyEntry(graph.regionState, normalizedRegion, adjacent, "manual"); for (const adjacentRegion of uniqueStrings(adjacent)) { mergeAdjacencyEntry(graph.regionState, adjacentRegion, [normalizedRegion], "manual"); } graph.regionState = normalizeRegionState(graph.regionState); return { ok: true, region: normalizedRegion }; } export function getKnowledgeOwnerEntry(graph, ownerKey = "") { normalizeGraphCognitiveState(graph); const normalizedOwnerKey = normalizeString(ownerKey); return normalizedOwnerKey ? graph.knowledgeState.owners?.[normalizedOwnerKey] || null : null; } export function listKnowledgeOwners(graph) { normalizeGraphCognitiveState(graph); const owners = new Map(); for (const entry of Object.values(graph.knowledgeState.owners || {})) { const normalizedEntry = createDefaultKnowledgeOwnerState(entry); if (!normalizedEntry.ownerKey) continue; owners.set(normalizedEntry.ownerKey, { ownerKey: normalizedEntry.ownerKey, ownerType: normalizedEntry.ownerType, ownerName: normalizedEntry.ownerName, nodeId: normalizedEntry.nodeId, aliases: [...(normalizedEntry.aliases || [])], knownCount: uniqueIds(normalizedEntry.knownNodeIds).length, mistakenCount: uniqueIds(normalizedEntry.mistakenNodeIds).length, manualKnownCount: uniqueIds(normalizedEntry.manualKnownNodeIds).length, manualHiddenCount: uniqueIds(normalizedEntry.manualHiddenNodeIds).length, updatedAt: Number(normalizedEntry.updatedAt || 0), lastSource: normalizeString(normalizedEntry.lastSource), }); } for (const characterNode of getCharacterNodes(graph)) { const resolvedOwner = resolveKnowledgeOwner(graph, { ownerType: OWNER_TYPE_CHARACTER, ownerName: characterNode?.fields?.name, nodeId: characterNode?.id, }); if (!resolvedOwner.ownerKey || owners.has(resolvedOwner.ownerKey)) continue; owners.set(resolvedOwner.ownerKey, { ownerKey: resolvedOwner.ownerKey, ownerType: resolvedOwner.ownerType, ownerName: resolvedOwner.ownerName, nodeId: resolvedOwner.nodeId, aliases: [...(resolvedOwner.aliases || [])], knownCount: 0, mistakenCount: 0, manualKnownCount: 0, manualHiddenCount: 0, updatedAt: 0, lastSource: "", }); } return Array.from(owners.values()).sort((left, right) => { const updatedDelta = Number(right.updatedAt || 0) - Number(left.updatedAt || 0); if (updatedDelta !== 0) return updatedDelta; return String(left.ownerName || "").localeCompare( String(right.ownerName || ""), "zh-Hans-CN", ); }); } export function pushRecentRecallOwner(historyState, ownerKey = "") { if (!historyState || typeof historyState !== "object") return; const normalizedOwnerKey = normalizeString(ownerKey); if (!normalizedOwnerKey) return; historyState.recentRecallOwnerKeys = uniqueStrings([ normalizedOwnerKey, ...(historyState.recentRecallOwnerKeys || []), ]).slice(0, RECENT_RECALL_OWNER_LIMIT); historyState.activeRecallOwnerKey = normalizedOwnerKey; }