2026-05-16 18:12:35 +01:00
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 ;
}
2026-05-17 08:37:26 +01:00
const topic = readString ( thought , "topic" ) ? ? "an unresolved thought" ;
const summary = ` Void may need to say this about ${ topic } ` ;
2026-05-16 18:12:35 +01:00
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 ) {
2026-05-17 08:37:26 +01:00
return ` I keep coming back to ${ topic } , and the clearest version I have is this: ${ claim } ` ;
2026-05-16 18:12:35 +01:00
}
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 ) {
2026-05-17 08:37:26 +01:00
return ` I keep staring at ${ fascinationTarget } . ${ topic } is the shortest honest name I have for what keeps catching. ` ;
2026-05-16 18:12:35 +01:00
}
if ( summary ) {
2026-05-17 08:37:26 +01:00
return ` I keep returning to ${ topic } . The version worth saying out loud is: ${ summary } ` ;
2026-05-16 18:12:35 +01:00
}
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 ,
) ;
}