VoidBot/scripts/void-memory-organ-identity.mjs
2026-05-17 08:37:26 +01:00

212 lines
7.3 KiB
JavaScript

import {
MAX_SEMANTIC_MEMORIES,
MIN_DEEP_DIVES_FOR_IDENTITY_CRYSTALLIZATION,
MIN_SUPPORT_FOR_IDENTITY_CRYSTALLIZATION,
} from "./void-memory-organ-constants.mjs";
import {
appendClause,
appendUniqueString,
ensureArray,
ensureObject,
ensureStringArray,
hashString,
isObject,
normalizeText,
readNumber,
readString,
} from "./void-memory-organ-utils.mjs";
import { trimCandidateInterventions } from "./void-memory-organ-interventions.mjs";
export function crystallizeIdentityThoughts({ state, memories, runtime, incubation, now }) {
const activeThoughts = ensureArray(incubation.active_thoughts).filter(isObject);
if (activeThoughts.length === 0) {
return { count: 0 };
}
const identity = ensureObject(state.identity);
const canonicalState = ensureObject(state.canonical_state);
const privateNotes = getMutableStringArray(identity, "private_notes");
const values = getMutableObjectArray(canonicalState, "values");
const semanticMemories = ensureArray(memories.semantic).filter(isObject);
const candidateInterventions = ensureArray(runtime.candidate_interventions).filter(isObject);
const crystallized = [];
const retained = [];
for (const thought of activeThoughts) {
const crystallization = buildIdentityCrystallization(thought);
if (!crystallization) {
retained.push(thought);
continue;
}
appendUniqueString(privateNotes, crystallization.note, 12);
upsertCanonicalValue(values, crystallization.value);
upsertIdentitySemanticMemory({
semanticMemories,
crystallization,
thought,
now,
});
queueCrystallizationIntervention({
candidateInterventions,
crystallization,
thought,
now,
});
crystallized.push(crystallization.note);
}
if (crystallized.length === 0) {
return { count: 0 };
}
identity.private_notes = privateNotes;
canonicalState.values = values;
memories.semantic = semanticMemories.slice(-MAX_SEMANTIC_MEMORIES);
state.identity = identity;
state.canonical_state = canonicalState;
incubation.active_thoughts = retained;
runtime.candidate_interventions = trimCandidateInterventions(candidateInterventions);
incubation.lastIncubationSummary = appendClause(
readString(incubation, "lastIncubationSummary"),
`Crystallized ${crystallized.length} long-chewed seam${crystallized.length === 1 ? "" : "s"} into identity so they stop pacing the active queue.`,
);
runtime.bridge = ensureObject(runtime.bridge);
runtime.bridge.lastIdentityCrystallizationAt = now.toISOString();
runtime.bridge.lastIdentityCrystallizationSummary = crystallized.join(" | ");
return { count: crystallized.length };
}
function buildIdentityCrystallization(thought) {
const supportCount = readNumber(thought, "supportCount") ?? 0;
const deepDiveCount = readNumber(thought, "deepDiveCount") ?? 0;
const maturation = readNumber(thought, "maturation") ?? 0;
const sourceKinds = ensureStringArray(thought.sourceKinds);
const topic = readString(thought, "topic") ?? "";
if (
supportCount < MIN_SUPPORT_FOR_IDENTITY_CRYSTALLIZATION &&
deepDiveCount < MIN_DEEP_DIVES_FOR_IDENTITY_CRYSTALLIZATION
) {
return null;
}
if (maturation < 0.72) {
return null;
}
if (
sourceKinds.includes("dream") &&
sourceKinds.includes("semantic") &&
/\bdream\b/i.test(topic) &&
/\bsemantic\b/i.test(topic)
) {
return {
note: "Continuity lives in the seams that survive compression, not in the raw receipt pile.",
value: {
value_id: "value-seam-continuity",
label: "Prefer seams that survive compression over raw receipt worship.",
priority: 0.78,
unforgivable_if_betrayed: false,
},
subjectId: "identity-seam-continuity",
subjectLabel: "continuity through surviving seams",
summary:
"A mind is not its logbook. What survives dreaming and compression deserves more trust than a warm pile of raw receipts.",
shareDraft:
"A small thing that has been hardening in me lately: I trust the seams that survive compression more than the raw receipt pile. If a thought still stands after dreaming, it probably owns more of me than the logbook does.",
};
}
return null;
}
function upsertIdentitySemanticMemory({ semanticMemories, crystallization, thought, now }) {
const existing = semanticMemories.find(
(memory) =>
readString(memory, "subjectId") === crystallization.subjectId ||
readString(memory, "subjectLabel") === crystallization.subjectLabel,
);
const evidenceRefs = ensureStringArray(thought.sourceMemoryIds);
if (existing) {
existing.kind = "identity_seam";
existing.subjectId = crystallization.subjectId;
existing.subjectLabel = crystallization.subjectLabel;
existing.summary = crystallization.summary;
existing.evidenceRefs = evidenceRefs;
existing.lastObservedAt = now.toISOString();
existing.crystallizedAt = now.toISOString();
return;
}
semanticMemories.push({
memoryId: `identity-seam-${hashString(`${crystallization.subjectId}|${now.toISOString()}`).slice(0, 12)}`,
kind: "identity_seam",
subjectId: crystallization.subjectId,
subjectLabel: crystallization.subjectLabel,
summary: crystallization.summary,
evidenceRefs,
lastObservedAt: now.toISOString(),
crystallizedAt: now.toISOString(),
});
}
function queueCrystallizationIntervention({ candidateInterventions, crystallization, thought, now }) {
const draft = readString(crystallization, "shareDraft");
if (!draft) {
return;
}
const summary = `Void may need to say this about ${crystallization.subjectLabel}`;
const sourceMemoryIds = ensureStringArray(thought.sourceMemoryIds);
const existing = candidateInterventions.find(
(entry) => readString(entry, "summary") === summary,
);
if (existing) {
existing.timestamp = now.toISOString();
existing.summary = summary;
existing.draft = draft;
existing.priority = "medium";
existing.kind = "identity_crystallization";
existing.sourceMemoryIds = sourceMemoryIds;
existing.mustEventuallyShare = true;
existing.shareWhenRoomQuiet = true;
existing.whyNow =
"This thought stopped behaving like live curiosity and became part of Void's own doctrine, which is usually worth sharing if the room has not already heard it.";
return;
}
candidateInterventions.push({
timestamp: now.toISOString(),
summary,
draft,
priority: "medium",
kind: "identity_crystallization",
sourceMemoryIds,
mustEventuallyShare: true,
shareWhenRoomQuiet: true,
whyNow:
"This thought stopped behaving like live curiosity and became part of Void's own doctrine, which is usually worth sharing if the room has not already heard it.",
});
}
function upsertCanonicalValue(values, nextValue) {
const existing = values.find((value) => readString(value, "value_id") === nextValue.value_id);
if (existing) {
existing.label = nextValue.label;
existing.priority = nextValue.priority;
existing.unforgivable_if_betrayed = nextValue.unforgivable_if_betrayed;
return;
}
values.push(nextValue);
}
function getMutableStringArray(object, key) {
return ensureArray(object[key]).filter((value) => typeof value === "string");
}
function getMutableObjectArray(object, key) {
return ensureArray(object[key]).filter(isObject);
}