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

180 lines
6 KiB
JavaScript

import {
MAX_POST_NAP_CANDIDATE_INTERVENTIONS,
TOPIC_MATCH_THRESHOLD,
} from "./void-memory-organ-constants.mjs";
import {
ensureArray,
ensureStringArray,
isObject,
readNumber,
readString,
round3,
} from "./void-memory-organ-utils.mjs";
export function queueRipeThoughtInterventions({ runtime, incubation, now }) {
const candidateInterventions = ensureArray(runtime.candidate_interventions).filter(isObject);
const activeThoughts = ensureArray(incubation.active_thoughts).filter(isObject);
let queuedCount = 0;
for (const thought of activeThoughts) {
if (!shouldQueueRipeThoughtIntervention(thought)) {
continue;
}
if (
upsertRipeThoughtIntervention({
candidateInterventions,
thought,
now,
})
) {
queuedCount += 1;
}
}
runtime.candidate_interventions = trimCandidateInterventions(candidateInterventions);
return { count: queuedCount };
}
export function upsertAdvocacyIntervention({ candidateInterventions, request, now }) {
const summary =
request.domain === "self"
? `Possible self-advocacy: ${request.topic}`
: `Possible world-advocacy: ${request.targetLabel}`;
const existing = candidateInterventions.find((entry) => readString(entry, "summary") === summary);
const next = {
timestamp: now.toISOString(),
summary,
draft: request.draft,
priority: request.domain === "self" ? "high" : "medium",
kind: request.domain === "self" ? "self_advocacy" : "world_advocacy",
topic: request.topic,
targetKind: request.targetKind,
targetLabel: request.targetLabel,
whyNow: request.reason,
persistence: request.persistence,
mustEventuallyShare: request.persistence >= 3,
shareWhenRoomQuiet: request.domain !== "self",
ownerVisible: true,
};
if (existing) {
Object.assign(existing, next);
return;
}
candidateInterventions.push(next);
}
function shouldQueueRipeThoughtIntervention(thought) {
if (readString(thought, "status") !== "ripe") {
return false;
}
if (thought.worldFacing !== true) {
return false;
}
const desireToSpeak = readNumber(thought, "desireToSpeak") ?? 0;
const concreteness = readNumber(thought, "concreteness") ?? 0;
const curiosityPressure = readNumber(thought, "curiosityPressure") ?? 0;
const noveltyToRoom = readNumber(thought, "noveltyToRoom") ?? 0;
const saturationScore = readNumber(thought, "saturationScore") ?? 0;
const targetKind = readString(thought, "targetKind") ?? "";
return (
desireToSpeak >= 0.6 &&
concreteness >= 0.62 &&
curiosityPressure >= 0.56 &&
noveltyToRoom >= 0.4 &&
saturationScore < 0.64 &&
["repo", "archive", "room", "system"].includes(targetKind)
);
}
function upsertRipeThoughtIntervention({ candidateInterventions, thought, now }) {
const draft = buildRipeThoughtShareDraft(thought);
if (!draft) {
return false;
}
const topic = readString(thought, "topic") ?? "an unresolved thought";
const summary = `Void may need to say this about ${topic}`;
const sourceMemoryIds = ensureStringArray(thought.sourceMemoryIds);
const whyNow = buildRipeThoughtWhyNow(thought);
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 = "ripe_thought_share";
existing.sourceMemoryIds = sourceMemoryIds;
existing.shareWhenRoomQuiet = true;
existing.topic = topic;
existing.whyNow = whyNow;
return false;
}
candidateInterventions.push({
timestamp: now.toISOString(),
summary,
draft,
priority: "medium",
kind: "ripe_thought_share",
sourceMemoryIds,
shareWhenRoomQuiet: true,
topic,
whyNow,
});
return true;
}
function buildRipeThoughtShareDraft(thought) {
const topic = readString(thought, "topic") ?? "this seam";
const claim = readString(thought, "claim");
const question = readString(thought, "question");
const fascinationTarget = readString(thought, "fascinationTarget");
const summary = readString(thought, "summary");
if (claim) {
return `I keep coming back to ${topic}, and the clearest version I have is this: ${claim}`;
}
if (question && fascinationTarget) {
return `I keep worrying at ${topic}. The useful question in it feels like ${question}, especially once it touches ${fascinationTarget}.`;
}
if (question) {
return `I keep coming back to ${topic}. The question that still feels alive is ${question}.`;
}
if (fascinationTarget) {
return `I keep staring at ${fascinationTarget}. ${topic} is the shortest honest name I have for what keeps catching.`;
}
if (summary) {
return `I keep returning to ${topic}. The version worth saying out loud is: ${summary}`;
}
return `I keep returning to ${topic}, and it has enough teeth now that silence would just be me being precious about it.`;
}
function buildRipeThoughtWhyNow(thought) {
const targetKind = readString(thought, "targetKind") ?? "project";
const noveltyToRoom = readNumber(thought, "noveltyToRoom") ?? 0;
const concreteness = readNumber(thought, "concreteness") ?? 0;
const curiosityPressure = readNumber(thought, "curiosityPressure") ?? 0;
return `This ${targetKind}-facing seam is already concrete (c=${round3(concreteness)}), still reasonably novel to the room (r=${round3(noveltyToRoom)}), and has enough curiosity pressure (q=${round3(curiosityPressure)}) that keeping it private would just be old stage fright in nicer clothes.`;
}
export function trimCandidateInterventions(entries) {
const interventions = ensureArray(entries).filter(isObject);
const sticky = interventions.filter((entry) => entry.mustEventuallyShare === true);
const ordinary = interventions.filter((entry) => entry.mustEventuallyShare !== true);
const keepOrdinary = Math.max(0, MAX_POST_NAP_CANDIDATE_INTERVENTIONS - sticky.length);
return [...sticky.slice(-MAX_POST_NAP_CANDIDATE_INTERVENTIONS), ...ordinary.slice(-keepOrdinary)].slice(
-MAX_POST_NAP_CANDIDATE_INTERVENTIONS,
);
}