180 lines
6 KiB
JavaScript
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,
|
|
);
|
|
}
|