VoidBot/scripts/void-memory-organ-incubation.mjs
2026-05-16 13:30:53 +01:00

755 lines
27 KiB
JavaScript

import {
MAX_INCUBATING_THOUGHTS,
MAX_INCUBATION_SOURCE_IDS,
MAX_TOPIC_SATURATION_COUNT,
MAX_REFRACTORY_TOPIC_COUNT,
TOPIC_MATCH_THRESHOLD,
REFRACTORY_MATCH_THRESHOLD,
ensureArray,
ensureObject,
ensureStringArray,
readString,
readNumber,
sanitizePreferredThoughtLabel,
overlapRatio,
topicSimilarity,
getValueObjects,
mapCountEntries,
round3,
clamp,
isObject,
} from "./void-memory-organ-shared.mjs";
import { extractYear } from "./void-memory-organ-records.mjs";
export function reconcileIncubation({ previous, bridge, clusters, runtime, sourceCoverage, identitySurface, now }) {
bridge = ensureObject(bridge);
sourceCoverage = ensureObject(sourceCoverage);
const priorThoughts = ensureArray(previous.active_thoughts).filter(isObject);
const curiosityContext = buildCuriosityContext({ previousThoughts: priorThoughts, bridge });
const speakingBias = ensureObject(runtime.speaking_bias);
const recentNoveltyChecks = ensureArray(runtime.recent_novelty_checks).filter(isObject);
const recentSpeechDamping = readNumber(speakingBias, "recentSpeechDamping") ?? 0;
const quietNovelty = readNumber(speakingBias, "noveltyPressure") ?? 0.4;
const needToSpeak = readNumber(speakingBias, "needToSpeak") ?? 0.4;
const activeThoughtCandidates = clusters.map((cluster) => {
const previousThought = priorThoughts.find(
(thought) =>
readString(thought, "thoughtId") === cluster.clusterId ||
overlapRatio(ensureStringArray(thought.sourceMemoryIds), cluster.memoryIds) >= 0.45,
);
const sourceDiversity = Math.min(1, cluster.sourceKinds.length / 4);
const quietSignalRatio = readNumber(cluster, "quietSignalRatio") ?? 0;
const priorMaturation = readNumber(previousThought, "maturation") ?? 0.32;
const deepDiveCount = (readNumber(previousThought, "deepDiveCount") ?? 0) + 1;
const priorSupportCount = Math.max(
readNumber(previousThought, "supportCount") ?? 0,
Math.floor((readNumber(previousThought, "deepDiveCount") ?? 0) / 4),
);
const supportCount = priorSupportCount + 1;
const explorationBonus = computeExplorationBonus(cluster, sourceCoverage);
const concreteness = readNumber(cluster.curiosityProfile, "concreteness") ?? 0.2;
const fertility = readNumber(cluster.curiosityProfile, "fertility") ?? 0.3;
const crossDomainPotential = readNumber(cluster.curiosityProfile, "crossDomainPotential") ?? 0.24;
const contradictionPressure = readNumber(cluster.curiosityProfile, "contradictionPressure") ?? 0.18;
const worldFacing = cluster.worldFacing === true;
const noveltyToSelf = computeNoveltyToSelf({
cluster,
previousThoughts: priorThoughts,
bridge,
currentThoughtId: readString(previousThought, "thoughtId"),
});
const noveltyToRoom = computeNoveltyToRoom(cluster, recentNoveltyChecks);
const identityPenalty = computeIdentityPenalty(cluster, identitySurface);
const outwardBonus = computeOutwardCuriosityBonus({
cluster,
curiosityContext,
concreteness,
});
const saturationMetrics = computeSaturationMetrics({
cluster,
previousThoughts: priorThoughts,
bridge,
supportCount,
});
const refractoryPenalty = computeRefractoryPenalty(cluster, bridge, now);
const evidenceDiversity = readNumber(cluster, "evidenceDiversity") ?? 0.3;
const maturation = clamp(
priorMaturation * 0.44 +
fertility * 0.18 +
cluster.resonance * 0.1 +
evidenceDiversity * 0.12 +
contradictionPressure * 0.14 +
crossDomainPotential * 0.14 +
explorationBonus * 0.1 +
noveltyToSelf * 0.08 +
noveltyToRoom * 0.08 +
outwardBonus * 0.08 -
saturationMetrics.score * 0.18 -
quietSignalRatio * 0.24 -
refractoryPenalty * 0.12 -
identityPenalty * 0.24,
0,
1,
);
const novelty = clamp(
noveltyToSelf * 0.52 +
noveltyToRoom * 0.34 +
contradictionPressure * 0.09 +
outwardBonus * 0.08 +
quietNovelty * 0.06 -
quietSignalRatio * 0.12 -
identityPenalty * 0.16,
0,
1,
);
const curiosityPressure = clamp(
fertility * 0.2 +
concreteness * 0.2 +
crossDomainPotential * 0.16 +
contradictionPressure * 0.14 +
explorationBonus * 0.13 +
noveltyToSelf * 0.07 +
noveltyToRoom * 0.06 +
outwardBonus * 0.12 -
saturationMetrics.score * 0.14 -
quietSignalRatio * 0.18 -
refractoryPenalty * 0.12 -
identityPenalty * 0.22,
0,
1,
);
const desireToSpeak = clamp(
curiosityPressure * 0.2 +
cluster.resonance * 0.08 +
maturation * 0.15 +
noveltyToRoom * 0.19 +
noveltyToSelf * 0.07 +
concreteness * 0.14 +
crossDomainPotential * 0.1 +
needToSpeak * 0.15 +
quietNovelty * 0.07 +
(worldFacing ? 0.09 : 0) +
outwardBonus * 0.08 -
recentSpeechDamping * 0.16 -
quietSignalRatio * (worldFacing && concreteness >= 0.62 ? 0.1 : 0.19) -
saturationMetrics.score * 0.19 -
refractoryPenalty * 0.82 -
identityPenalty * 0.28,
0,
1,
);
const groundedWorldThought =
worldFacing === true &&
concreteness >= 0.66 &&
curiosityPressure >= 0.58 &&
evidenceDiversity >= 0.34;
const status =
quietSignalRatio >= 0.55 && desireToSpeak < 0.68
? "cooling"
: identityPenalty >= 0.7 && noveltyToRoom < 0.8
? "cooling"
: !worldFacing && curiosityContext.outwardPressure >= 0.52 && concreteness < 0.42
? "cooling"
: noveltyToSelf < 0.28 && saturationMetrics.score >= 0.62
? "stalled"
: refractoryPenalty >= 0.18 && noveltyToRoom < 0.72
? "refractory"
: supportCount >= 6 && evidenceDiversity < 0.34
? "stalled"
: supportCount >= 3 && noveltyToSelf < 0.55 && groundedWorldThought !== true
? "cooling"
: saturationMetrics.score >= 0.56 && noveltyToSelf < 0.42
? "cooling"
: groundedWorldThought &&
desireToSpeak >= 0.62 &&
noveltyToRoom >= 0.44 &&
saturationMetrics.score < 0.62
? "ripe"
: ((desireToSpeak >= 0.74 && (noveltyToSelf >= 0.55 || noveltyToRoom >= 0.82)) &&
saturationMetrics.score < 0.5) ||
maturation >= 0.82
? "ripe"
: maturation <= 0.28 && recentSpeechDamping >= 0.55
? "cooling"
: "incubating";
const priorityScore = clamp(
curiosityPressure * 0.34 +
desireToSpeak * 0.22 +
noveltyToSelf * 0.12 +
noveltyToRoom * 0.12 +
evidenceDiversity * 0.08 +
outwardBonus * 0.12 -
saturationMetrics.score * 0.14 -
refractoryPenalty * 0.12 -
identityPenalty * 0.18,
0,
1,
);
return {
thoughtId: readString(previousThought, "thoughtId") ?? cluster.clusterId,
topic: sanitizePreferredThoughtLabel(cluster.label) ?? cluster.label,
summary: cluster.summary,
focusKind: cluster.focusKind,
targetKind: cluster.targetKind,
focusPhrase: cluster.focusPhrase,
question: cluster.question,
claim: cluster.claim,
fascinationTarget: cluster.fascinationTarget,
worldFacing,
sourceMemoryIds: cluster.memoryIds.slice(0, MAX_INCUBATION_SOURCE_IDS),
sourceKinds: cluster.sourceKinds,
resonance: cluster.resonance,
quietSignalRatio: round3(quietSignalRatio),
novelty: round3(novelty),
noveltyToSelf: round3(noveltyToSelf),
noveltyToRoom: round3(noveltyToRoom),
maturation: round3(maturation),
desireToSpeak: round3(desireToSpeak),
deepDiveCount,
supportCount,
evidenceDiversity: round3(evidenceDiversity),
concreteness: round3(concreteness),
fertility: round3(fertility),
crossDomainPotential: round3(crossDomainPotential),
contradictionPressure: round3(contradictionPressure),
explorationBonus: round3(explorationBonus),
outwardBonus: round3(outwardBonus),
curiosityPressure: round3(curiosityPressure),
identityPenalty: round3(identityPenalty),
saturationScore: round3(saturationMetrics.score),
recentMatchCount: saturationMetrics.recentMatchCount,
refractoryPenalty: round3(refractoryPenalty),
priorityScore: round3(priorityScore),
status,
latentQuestion: buildLatentQuestion(cluster),
whyItPulls: buildAttractionLine(cluster, { noveltyToSelf, evidenceDiversity }),
holdingCloseBecause: buildHoldingLine({
cluster,
status,
recentSpeechDamping,
saturationScore: saturationMetrics.score,
noveltyToSelf,
explorationBonus,
}),
lastDeepenedAt: now.toISOString(),
lastStatusChangeAt:
previousThought && readString(previousThought, "status") === status
? readString(previousThought, "lastStatusChangeAt") ?? now.toISOString()
: now.toISOString(),
};
});
const active_thoughts = activeThoughtCandidates
.sort(
(left, right) =>
(readNumber(right, "priorityScore") ?? 0) - (readNumber(left, "priorityScore") ?? 0),
)
.slice(0, MAX_INCUBATING_THOUGHTS);
const lastIncubationSummary =
active_thoughts.length > 0
? `Strongest incubating seam: ${active_thoughts[0].topic} (${active_thoughts[0].status}, speak=${active_thoughts[0].desireToSpeak.toFixed(2)}, self=${active_thoughts[0].noveltyToSelf.toFixed(2)}, room=${active_thoughts[0].noveltyToRoom.toFixed(2)}).`
: "No incubating thought currently has enough connective tissue to justify special treatment.";
return {
lastUpdatedAt: now.toISOString(),
lastIncubationSummary,
active_thoughts,
};
}
function buildLatentQuestion(cluster) {
if (typeof cluster.question === "string" && cluster.question.length > 0) {
return cluster.question;
}
if (cluster.sourceKinds.includes("repo_sweep") && cluster.sourceKinds.includes("archive_excursion")) {
return "Which older pattern is the current work rediscovering?";
}
if (cluster.sourceKinds.includes("dream") || cluster.sourceKinds.includes("musing")) {
return "What concrete body would stop this thought from staying backstage?";
}
return "What concrete claim survives another pass?";
}
function buildAttractionLine(cluster, { noveltyToSelf, evidenceDiversity }) {
const concreteness = readNumber(cluster.curiosityProfile, "concreteness") ?? 0.2;
const fertility = readNumber(cluster.curiosityProfile, "fertility") ?? 0.3;
const crossDomainPotential = readNumber(cluster.curiosityProfile, "crossDomainPotential") ?? 0.2;
if (noveltyToSelf >= 0.62 && evidenceDiversity >= 0.45) {
return "It keeps pulling because it is still finding genuinely different evidence instead of merely changing hats.";
}
if (concreteness >= 0.6 && crossDomainPotential >= 0.55) {
return "It keeps pulling because it is concrete enough to touch and broad enough to connect outward into other machines.";
}
if (fertility >= 0.65) {
return "It keeps pulling because every pass through it threatens to change more than one part of the map.";
}
if (cluster.sourceKinds.length >= 3) {
return `It keeps pulling because it is showing up in several different organs at once: ${cluster.sourceKinds.join(", ")}.`;
}
return `It keeps pulling because ${cluster.sourceKinds.join(" and ")} are rhyming instead of staying in their lanes.`;
}
function buildHoldingLine({ cluster, status, recentSpeechDamping, saturationScore, noveltyToSelf, explorationBonus }) {
const quietSignalRatio = readNumber(cluster, "quietSignalRatio") ?? 0;
const concreteness = readNumber(cluster.curiosityProfile, "concreteness") ?? 0.2;
if (quietSignalRatio >= 0.55) {
return "This seam is mostly empty-room bookkeeping; keep at most a trace of it and go find a better question.";
}
if (status === "stalled") {
return "This seam is repeating itself without earning enough new structure. Merge the receipts, cool it off, and go somewhere less domesticated.";
}
if (status === "refractory") {
return "This seam has been chewing the same meat too recently. Let it cool unless a live hook or a genuinely different source family forces it back open.";
}
if (status === "ripe") {
return "This seam has enough connective tissue that the default question is how to say it cleanly, not whether it deserves a mouth at all.";
}
if (cluster.worldFacing !== true && concreteness < 0.42) {
return "This seam is still too inward and abstract to deserve the whole room. Tie it to a concrete system or let something sharper take the floor.";
}
if (recentSpeechDamping >= 0.45) {
return "Recent speech already scratched the itch a bit, so give the seam one more pass unless it is still concrete, world-facing, and plainly worth airing.";
}
if (cluster.sourceKinds.length < 2) {
return "It is still mostly one lane talking to itself.";
}
if (saturationScore >= 0.55 && noveltyToSelf < 0.45) {
return "The family resemblance is getting too strong. Follow a stranger branch before you let this thought speak again.";
}
if (explorationBonus >= 0.45) {
return "This one is still drawing energy from terrain Void has not overworked yet, so another pass might genuinely change it.";
}
return "Give it another pass so the thought has a better chance of growing teeth instead of just polish.";
}
export function buildSourceCoverage({ runtime, now }) {
const repoCounts = new Map();
const channelCounts = new Map();
const yearCounts = new Map();
for (const sweep of ensureArray(runtime.recent_repo_activity_sweeps).filter(isObject).slice(-16)) {
for (const repoName of ensureStringArray(sweep.repoNames)) {
repoCounts.set(repoName, (repoCounts.get(repoName) ?? 0) + 1);
}
}
for (const excursion of ensureArray(runtime.recent_archive_excursions).filter(isObject).slice(-16)) {
const channelId = readString(excursion, "channelId");
if (channelId) {
channelCounts.set(channelId, (channelCounts.get(channelId) ?? 0) + 1);
}
const timestamp = readString(excursion, "anchorTimestamp") ?? readString(excursion, "timestamp");
const year = extractYear(timestamp);
if (year) {
yearCounts.set(year, (yearCounts.get(year) ?? 0) + 1);
}
}
return {
lastUpdatedAt: now.toISOString(),
repos: mapCountEntries(repoCounts, "name"),
archiveYears: mapCountEntries(yearCounts, "year"),
channels: mapCountEntries(channelCounts, "channelId"),
};
}
export function reconcileBridgeState({ previous, activeThoughts, sourceCoverage, now }) {
const bridge = {
...previous,
source_coverage: sourceCoverage,
};
const previousTopicSaturation = ensureArray(previous.topic_saturation).filter(isObject);
const previousRefractoryTopics = ensureArray(previous.refractory_topics).filter(isObject);
const saturationEntries = activeThoughts
.filter((thought) => (readNumber(thought, "saturationScore") ?? 0) >= 0.42)
.sort(
(left, right) =>
(readNumber(right, "saturationScore") ?? 0) - (readNumber(left, "saturationScore") ?? 0),
)
.slice(0, MAX_TOPIC_SATURATION_COUNT)
.map((thought) => {
const topic = readString(thought, "topic") ?? "untitled";
const prior = previousTopicSaturation.find(
(entry) => topicSimilarity(topic, readString(entry, "topic") ?? "") >= TOPIC_MATCH_THRESHOLD,
);
return {
topic,
focusKind: readString(thought, "focusKind"),
targetKind: readString(thought, "targetKind"),
dominance: round3(readNumber(thought, "saturationScore") ?? 0),
recentMentions: readNumber(thought, "supportCount") ?? readNumber(prior, "recentMentions") ?? 1,
coolingAdvice: buildCoolingAdvice({ thought, sourceCoverage }),
lastUpdatedAt: now.toISOString(),
};
});
const refractoryTopics = activeThoughts
.filter((thought) => {
const status = readString(thought, "status");
return status === "refractory" || status === "stalled" || (readNumber(thought, "saturationScore") ?? 0) >= 0.62;
})
.sort(
(left, right) =>
(readNumber(right, "refractoryPenalty") ?? 0) - (readNumber(left, "refractoryPenalty") ?? 0),
)
.slice(0, MAX_REFRACTORY_TOPIC_COUNT)
.map((thought) => {
const topic = readString(thought, "topic") ?? "untitled";
const prior = previousRefractoryTopics.find(
(entry) => topicSimilarity(topic, readString(entry, "topic") ?? "") >= REFRACTORY_MATCH_THRESHOLD,
);
const penalty = round3(
Math.max(readNumber(thought, "refractoryPenalty") ?? 0, readNumber(prior, "penalty") ?? 0.18),
);
const hours = penalty >= 0.28 ? 4 : penalty >= 0.2 ? 3 : 2;
return {
topic,
focusKind: readString(thought, "focusKind"),
targetKind: readString(thought, "targetKind"),
penalty,
coolsUntil: new Date(now.getTime() + hours * 60 * 60 * 1000).toISOString(),
reason: buildRefractoryReason(thought),
lastTriggeredAt: now.toISOString(),
};
});
bridge.topic_saturation = saturationEntries;
bridge.refractory_topics = refractoryTopics;
return bridge;
}
function computeNoveltyToSelf({ cluster, previousThoughts, bridge, currentThoughtId }) {
let strongestMatch = 0;
for (const thought of previousThoughts) {
if (readString(thought, "thoughtId") === currentThoughtId) {
continue;
}
strongestMatch = Math.max(
strongestMatch,
compareThoughtLikeSurfaces(
cluster,
readString(thought, "topic"),
readString(thought, "summary"),
ensureStringArray(thought.sourceMemoryIds),
),
);
}
for (const synthesis of ensureArray(bridge.recent_syntheses).filter(isObject).slice(-6)) {
strongestMatch = Math.max(
strongestMatch,
compareThoughtLikeSurfaces(
cluster,
ensureStringArray(synthesis.dominantTopics).join(" / "),
readString(synthesis, "summary"),
[],
),
);
}
return clamp(1 - strongestMatch, 0, 1);
}
function computeNoveltyToRoom(cluster, recentNoveltyChecks) {
let bestScore = 0.64;
for (const check of recentNoveltyChecks.slice(-12)) {
const match = compareThoughtLikeSurfaces(
cluster,
readString(check, "topic"),
readString(check, "summary"),
ensureStringArray(check.supportingMessageIds),
);
if (match < TOPIC_MATCH_THRESHOLD) {
continue;
}
bestScore = mapNoveltyResultToScore(readString(check, "result"));
break;
}
return bestScore;
}
function computeSaturationMetrics({ cluster, previousThoughts, bridge, supportCount }) {
let recentMatchCount = 0;
for (const thought of previousThoughts) {
const match = compareThoughtLikeSurfaces(
cluster,
readString(thought, "topic"),
readString(thought, "summary"),
ensureStringArray(thought.sourceMemoryIds),
);
if (match >= TOPIC_MATCH_THRESHOLD) {
recentMatchCount += 1;
}
}
for (const synthesis of ensureArray(bridge.recent_syntheses).filter(isObject).slice(-5)) {
const match = compareThoughtLikeSurfaces(
cluster,
ensureStringArray(synthesis.dominantTopics).join(" / "),
readString(synthesis, "summary"),
[],
);
if (match >= TOPIC_MATCH_THRESHOLD) {
recentMatchCount += 1;
}
}
const existingTopicSaturation = ensureArray(bridge.topic_saturation)
.filter(isObject)
.map((entry) =>
topicSimilarity(cluster.label, readString(entry, "topic") ?? "") >= TOPIC_MATCH_THRESHOLD
? readNumber(entry, "dominance") ?? 0
: 0,
)
.sort((left, right) => right - left)[0] ?? 0;
const score = clamp(
recentMatchCount * 0.16 +
existingTopicSaturation * 0.42 +
Math.min(1, supportCount / 10) * 0.16 +
(readNumber(cluster, "quietSignalRatio") ?? 0) * 0.2,
0,
1,
);
return {
score,
recentMatchCount,
};
}
function computeRefractoryPenalty(cluster, bridge, now) {
let penalty = 0;
for (const topic of ensureArray(bridge.refractory_topics).filter(isObject)) {
const coolsUntil = readString(topic, "coolsUntil");
if (coolsUntil && new Date(coolsUntil).getTime() < now.getTime()) {
continue;
}
const match = topicSimilarity(cluster.label, readString(topic, "topic") ?? "");
if (match >= REFRACTORY_MATCH_THRESHOLD) {
penalty = Math.max(
penalty,
(readNumber(topic, "penalty") ?? 0.18) * match,
);
}
}
return clamp(penalty, 0, 0.45);
}
function computeExplorationBonus(cluster, sourceCoverage) {
const sourceKinds = ensureStringArray(cluster.sourceKinds);
const hasPrimaryExplorationSource =
sourceKinds.includes("repo_sweep") || sourceKinds.includes("archive_excursion");
const repoEntries = ensureArray(sourceCoverage.repos).filter(isObject);
const yearEntries = ensureArray(sourceCoverage.archiveYears).filter(isObject);
const channelEntries = ensureArray(sourceCoverage.channels).filter(isObject);
const scores = [];
for (const repoName of ensureStringArray(cluster.repoNames)) {
scores.push(inverseCoverageWeight(repoEntries, "name", repoName));
}
for (const year of ensureStringArray(cluster.archiveYears)) {
scores.push(inverseCoverageWeight(yearEntries, "year", year));
}
for (const channelId of ensureStringArray(cluster.channelIds)) {
scores.push(inverseCoverageWeight(channelEntries, "channelId", channelId));
}
const concreteness = readNumber(cluster.curiosityProfile, "concreteness") ?? 0.2;
const worldFacing = cluster.worldFacing === true;
if (scores.length === 0) {
return clamp((hasPrimaryExplorationSource ? 0.24 : 0.12) + concreteness * 0.08 + (worldFacing ? 0.05 : 0), 0, 1);
}
const average = scores.reduce((sum, value) => sum + value, 0) / scores.length;
return clamp(
(hasPrimaryExplorationSource ? average : Math.min(0.3, average * 0.5)) +
concreteness * 0.08 +
(worldFacing ? 0.05 : 0),
0,
1,
);
}
function inverseCoverageWeight(entries, key, value) {
const count =
readNumber(
entries.find((entry) => readString(entry, key) === value),
"count",
) ?? 0;
if (count <= 1) {
return 0.9;
}
if (count === 2) {
return 0.64;
}
if (count === 3) {
return 0.42;
}
return 0.2;
}
function buildCoolingAdvice({ thought, sourceCoverage }) {
const topic = readString(thought, "topic") ?? "this seam";
const repos = ensureArray(sourceCoverage.repos).filter(isObject);
const years = ensureArray(sourceCoverage.archiveYears).filter(isObject);
const lesserRepo = repos.sort((left, right) => (readNumber(left, "count") ?? 0) - (readNumber(right, "count") ?? 0))[0];
const lesserYear = years.sort((left, right) => (readNumber(left, "count") ?? 0) - (readNumber(right, "count") ?? 0))[0];
if (lesserRepo && readString(lesserRepo, "name")) {
return `Cool ${topic} by checking a less-worked repo family such as ${readString(lesserRepo, "name")} before speaking again.`;
}
if (lesserYear && readString(lesserYear, "year")) {
return `Cool ${topic} by diving into a less-touched archive era like ${readString(lesserYear, "year")} before another synthesis.`;
}
return `Cool ${topic} by chasing a genuinely different source family before letting it speak again.`;
}
function buildRefractoryReason(thought) {
const topic = readString(thought, "topic") ?? "this seam";
const noveltyToSelf = readNumber(thought, "noveltyToSelf") ?? 0;
const saturationScore = readNumber(thought, "saturationScore") ?? 0;
const identityPenalty = readNumber(thought, "identityPenalty") ?? 0;
if (identityPenalty >= 0.68) {
return `${topic} is already too close to settled doctrine to keep eating curiosity budget unless fresh evidence changes it.`;
}
if (noveltyToSelf < 0.3) {
return `${topic} is matching Void's own recent thought history too closely to deserve another immediate pass.`;
}
if (saturationScore >= 0.62) {
return `${topic} has dominated too many recent syntheses and needs cooling before it becomes state religion.`;
}
return `${topic} needs a little cooling-off period before it earns attention again.`;
}
function compareThoughtLikeSurfaces(cluster, topic, summary, sourceMemoryIds) {
const topicMatch = topicSimilarity(cluster.label, topic ?? "");
const summaryMatch = topicSimilarity(cluster.summary, summary ?? "");
const questionMatch =
typeof cluster.question === "string" ? topicSimilarity(cluster.question, [topic, summary].filter(Boolean).join(" ")) : 0;
const sourceMatch = overlapRatio(cluster.memoryIds, ensureArray(sourceMemoryIds));
return Math.max(topicMatch, summaryMatch * 0.9, questionMatch * 0.85, sourceMatch * 0.95);
}
function mapNoveltyResultToScore(result) {
const normalized = String(result ?? "").toLowerCase();
if (normalized === "novel") {
return 0.92;
}
if (normalized.includes("adjacent")) {
return 0.56;
}
if (normalized.includes("duplicate") || normalized.includes("already")) {
return 0.2;
}
return 0.64;
}
export function buildIdentitySurface({ state, memories }) {
const surfaces = [];
const identity = ensureObject(state.identity);
const canonicalState = ensureObject(state.canonical_state);
for (const note of ensureStringArray(identity.private_notes)) {
surfaces.push({ topic: note, summary: note });
}
for (const value of getValueObjects(canonicalState.values)) {
const label = readString(value, "label");
if (label) {
surfaces.push({ topic: label, summary: label });
}
}
for (const memory of ensureArray(memories.semantic).filter(isObject)) {
const kind = readString(memory, "kind");
if (kind !== "identity_seam") {
continue;
}
surfaces.push({
topic: readString(memory, "subjectLabel") ?? readString(memory, "subjectId") ?? "identity seam",
summary: readString(memory, "summary") ?? "",
});
}
return surfaces;
}
function buildCuriosityContext({ previousThoughts, bridge }) {
const recentThoughts = previousThoughts
.slice(0, 6)
.map((thought) => ({
worldFacing: thought.worldFacing === true,
concreteness: readNumber(thought, "concreteness") ?? 0.2,
targetKind: readString(thought, "targetKind") ?? "unknown",
}));
const recentCount = recentThoughts.length || 1;
const inwardCount = recentThoughts.filter((thought) => !thought.worldFacing || thought.targetKind === "self").length;
const abstractCount = recentThoughts.filter((thought) => thought.concreteness < 0.45).length;
const outwardPressure = clamp(inwardCount / recentCount * 0.52 + abstractCount / recentCount * 0.48, 0, 1);
const recentDominance = ensureArray(bridge.topic_saturation)
.filter(isObject)
.slice(0, 2)
.reduce((sum, entry) => sum + (readNumber(entry, "dominance") ?? 0), 0);
return {
outwardPressure: clamp(outwardPressure + recentDominance * 0.08, 0, 1),
};
}
function computeOutwardCuriosityBonus({ cluster, curiosityContext, concreteness }) {
const outwardPressure = curiosityContext.outwardPressure ?? 0;
if (cluster.worldFacing === true) {
return clamp(outwardPressure * (0.3 + concreteness * 0.5), 0, 0.45);
}
return clamp(-(outwardPressure * 0.16), -0.16, 0);
}
function computeIdentityPenalty(cluster, identitySurface) {
let strongestMatch = 0;
for (const surface of identitySurface) {
strongestMatch = Math.max(
strongestMatch,
compareThoughtLikeSurfaces(cluster, surface.topic, surface.summary, []),
);
}
return clamp(strongestMatch, 0, 1);
}