212 lines
7.3 KiB
JavaScript
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);
|
|
}
|