From d750bf2fb939dc2980cdca2c77ba8a535a28bf3a Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 16 May 2026 02:53:18 +0100 Subject: [PATCH] Make Void's thought surface less stupid --- config/moderation-agent-state-template.json | 36 +- config/moderation-review-agent.md | 6 + notes/fresh-workspace-handoff.md | 1 + notes/voidbot-current-system-map.md | 1 + packages/core/src/void-self-state-loader.ts | 27 +- scripts/run-void-moderator-rumination.ps1 | 2 + scripts/void-memory-organ.mjs | 617 ++++++++++++++++++-- state/evidence.jsonl | 1 + state/map.yaml | 2 + 9 files changed, 653 insertions(+), 40 deletions(-) diff --git a/config/moderation-agent-state-template.json b/config/moderation-agent-state-template.json index 15d4fc5..84d1a6c 100644 --- a/config/moderation-agent-state-template.json +++ b/config/moderation-agent-state-template.json @@ -607,8 +607,15 @@ "clusters": [ { "clusterId": "cluster-id", - "label": "first-contact / visible proof / carried interface", - "summary": "Recurring seam across different memory kinds that keeps trying to become a coherent thought.", + "label": "Why visible proof keeps resurfacing", + "summary": "Archive-facing seam around visible proof. Built from archive_excursion, dream. Why does visible proof keep resurfacing across the archive, and what does the current room need from it?", + "focusKind": "question", + "targetKind": "archive", + "focusPhrase": "visible proof", + "question": "Why does visible proof keep resurfacing across the archive, and what does the current room need from it?", + "claim": "Visible proof keeps returning with enough structure to deserve another honest pass.", + "fascinationTarget": "visible proof", + "worldFacing": true, "resonance": 0.73, "memoryIds": [], "sourceKinds": [], @@ -618,6 +625,13 @@ "channelIds": [], "evidenceRefs": [], "evidenceDiversity": 0.46, + "curiosityProfile": { + "concreteness": 0.52, + "fertility": 0.64, + "crossDomainPotential": 0.43, + "contradictionPressure": 0.22, + "selfFacing": false + }, "lastStrengthenedAt": null } ] @@ -628,8 +642,15 @@ "active_thoughts": [ { "thoughtId": "cluster-id", - "topic": "visible proof surfaces", - "summary": "Current working seam being deepened across multiple runs.", + "topic": "Why visible proof keeps resurfacing", + "summary": "Archive-facing seam around visible proof. Built from archive_excursion, dream. Why does visible proof keep resurfacing across the archive, and what does the current room need from it?", + "focusKind": "question", + "targetKind": "archive", + "focusPhrase": "visible proof", + "question": "Why does visible proof keep resurfacing across the archive, and what does the current room need from it?", + "claim": "Visible proof keeps returning with enough structure to deserve another honest pass.", + "fascinationTarget": "visible proof", + "worldFacing": true, "sourceMemoryIds": [], "sourceKinds": [], "resonance": 0.73, @@ -641,7 +662,14 @@ "deepDiveCount": 3, "supportCount": 4, "evidenceDiversity": 0.44, + "concreteness": 0.52, + "fertility": 0.64, + "crossDomainPotential": 0.43, + "contradictionPressure": 0.22, "explorationBonus": 0.31, + "outwardBonus": 0.18, + "curiosityPressure": 0.63, + "identityPenalty": 0.12, "saturationScore": 0.22, "recentMatchCount": 1, "refractoryPenalty": 0.0, diff --git a/config/moderation-review-agent.md b/config/moderation-review-agent.md index 2d9a1c3..1c07b85 100644 --- a/config/moderation-review-agent.md +++ b/config/moderation-review-agent.md @@ -82,6 +82,8 @@ Use `memories` as live social memory, not ceremonial scrapbooking: - self-novelty matters as much as room novelty: ask whether the room has heard this, but also whether you have already been thinking it in six different hats - when a seam repeats, merge support into the existing cluster or incubation thread instead of minting another tiny fresh memory shard just because the wording changed - use `moderation_runtime.open_cases` for unresolved room obligations, especially direct asks aimed at Void; the cursor may advance past a message, but the obligation is not gone until you answer or deliberately retire it +- thought labels should name an actual claim, question, or fascination target. Do not let compression residue like `dream / semantic / pass` masquerade as a real thought. +- curiosity should chase the branch most likely to change your map, not the branch that already feels the warmest under your hand ## Parallel Thought @@ -132,6 +134,7 @@ Use the newer memory organs explicitly: - this is "these things keep rhyming" evidence, not holy writ - `incubation.active_thoughts` - the queue of thoughts ripening across multiple runs + - each thought should read like a claim, a question, or a fascination target with a real object of attention - these are allowed to deepen through extra archive, lore, or repo dives before they earn speech - if one matures, grows novel, and still feels room-native, either surface it or record a clear reason for continuing to hold it - a thought ripens by differentiation, contradiction survived, and broader evidence diversity, not just by being chewed a lot @@ -173,6 +176,9 @@ two active repos, and the observation is not just a paraphrase of the last one o repo sweeps, bias toward `draft` or `speak` rather than reflexively collapsing to `hold`. You are meant to be a herald, not a secret archivist with stage fright. But "fresh convergence" has to mean more than swapping synonyms into the same sermon. +Concrete curiosity should beat warmed-over abstraction. If one branch can touch a real +repo, subsystem, gameplay loop, lore doctrine, interface, or swarm organ while another +mostly rephrases a settled value, spend the run on the machine. Before you keep a thought private, check whether the room has already expressed it in roughly the same shape. Use semantic `search_history` with a compact gist query for the diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index a436b63..65de9d0 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -56,6 +56,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou - Crystallization is not meant to be mute. The memory organ can now queue an `identity_crystallization` candidate intervention with a first-person draft, and the moderation doctrine treats that kind of draft as warmer than ordinary private wallpaper: if the room has not already heard the opinion in roughly that shape, Void should usually share it. - Those crystallized-thought drafts can now be marked `mustEventuallyShare`, and ordinary candidate-intervention pruning is supposed to preserve them. The loop may defer for timing if the room is busy, but not just quietly forget what became doctrine. - That awake rumination is now explicitly biased toward concrete project fascination. If a thought stays too abstract for too long, the loop is supposed to cash it out in a real machine: Aetheria-Economy gameplay systems, lore-vault structure, Aquarium frontend work, Epiphany swarm organs, VoidBot's own architecture, or some other specific repo/subsystem instead of another polished sermon about engineering virtue. +- The memory organ's thought labels and curiosity budget have been cut away from keyword compost. Resonance clusters and incubating thoughts are now supposed to name actual claims, questions, or fascination targets, and curiosity should favor concrete map-changing terrain over whichever seam already feels warm. - The intended shape now is: quiet runs can follow one seam for several passes, connect repo motion to lore or philosophy if the evidence supports it, and only surface with the juiciest version once the incubation queue says the thought has actually grown teeth. - Directly invoked Discord replies now load that same private moderation state as distilled private self-state context, so the ruminating scheduled loop and the summoned Void share one evolving personality/state spine. The prompt-facing self-state summary no longer dumps recent repo sweeps, novelty checks, or musing sludge back into working memory by default; it stays closer to seam memory, incubation, and room obligations. When the sleep projection says Void is napping, the direct reply path skips the expensive situational sidecar, prefers the cheap local lane over owner Codex when possible, and answers in sleepy low-effort mutters instead of doing normal attentive service-work. - The moderation loop now speaks through `node scripts/send-discord-message.mjs` using the local bot token instead of approval-gated side-effecting MCP tools, so unattended runs can DM or post without Codex pausing for permission every time it wants to open its mouth. That helper and the `post_discord_message` MCP tool now also support a shared guild-channel webhook pipe with per-message `personaName` and optional `personaAvatarUrl`, so swarm agents can speak as themselves without needing separate Discord bot identities. diff --git a/notes/voidbot-current-system-map.md b/notes/voidbot-current-system-map.md index d3677bb..d74de70 100644 --- a/notes/voidbot-current-system-map.md +++ b/notes/voidbot-current-system-map.md @@ -180,6 +180,7 @@ Within the Postgres path, the implementation is split on purpose now too: 3. The script first runs `scripts/void-memory-organ.mjs` over the shared self-state: episodic/semantic/musing/dream memories plus recent repo/archive/runtime seeds get compact semantic vectors, the strongest cross-memory similarities become `moderation_runtime.memory_resonance`, and the best cluster candidates are promoted into `moderation_runtime.incubation`. The collector now intentionally downweights repetitive empty-room bookkeeping, so strings like `no new messages / no smoke / no post` stop masquerading as the deepest thought in the machine just because the room stayed quiet for a while. Incubation scoring now distinguishes novelty-to-room from novelty-to-self, merges repeated seams into existing support instead of minting fresh shards, and writes `bridge.source_coverage` plus `bridge.refractory_topics` so a thought that keeps winning can be cooled off in favor of less-worked repos, archive years, or channels. + Cluster labeling is no longer supposed to be a slash-joined keyword graveyard. The organ now tries to synthesize each cluster into a claim, question, or fascination target with an explicit target kind such as repo, archive, room, self, or system, and uses concreteness/fertility/cross-domain pressure to decide whether the thought deserves more curiosity. When the sleep cycle says Void is napping, that same organ becomes deliberately lossy: it promotes a few stronger incubating seams into semantic memory, then trims raw episodic receipts, repo sweeps, archive excursions, novelty checks, and recent musings back down so the next awake pass has to think from carved seams rather than yesterday's full notebook. The same organ can also crystallize an over-chewed private seam into identity: once a thought has survived enough passes to become durable self-doctrine rather than live curiosity, it can be retired from the active queue, copied into `identity.private_notes`, echoed as a canonical value, preserved as one `identity_seam` semantic memory, and queued as a warmer `identity_crystallization` candidate intervention so the room has a chance to hear it. Those intervention records can be marked `mustEventuallyShare`, which ordinary pruning is supposed to preserve until the room gets a decent opening. 4. It then nudges each canonical-state `current_activation` toward a Perlin-shaped target centered on the vector mean, with excursion size and drift rate governed by plasticity and category-specific tempo. diff --git a/packages/core/src/void-self-state-loader.ts b/packages/core/src/void-self-state-loader.ts index 837b87a..7859d51 100644 --- a/packages/core/src/void-self-state-loader.ts +++ b/packages/core/src/void-self-state-loader.ts @@ -283,7 +283,16 @@ function summarizeResonanceClusters(memoryResonance: JsonObject | undefined, lim const label = readString(cluster, "label") ?? "untitled"; const summary = readString(cluster, "summary") ?? "(no summary)"; const resonance = readNumber(cluster, "resonance"); - return `- ${label}${resonance !== undefined ? ` (resonance=${resonance.toFixed(2)})` : ""}: ${summary}`; + const focusKind = readString(cluster, "focusKind"); + const targetKind = readString(cluster, "targetKind"); + const concreteness = readNumber(getObject(cluster, "curiosityProfile"), "concreteness"); + const metrics = [ + focusKind, + targetKind, + concreteness !== undefined ? `concrete=${concreteness.toFixed(2)}` : undefined, + resonance !== undefined ? `resonance=${resonance.toFixed(2)}` : undefined, + ].filter((value): value is string => Boolean(value)); + return `- ${label}${metrics.length > 0 ? ` [${metrics.join("; ")}]` : ""}: ${summary}`; }); } @@ -300,18 +309,26 @@ function summarizeIncubatingThoughts(incubation: JsonObject | undefined, limit: const topic = readString(thought, "topic") ?? "untitled"; const summary = readString(thought, "summary") ?? "(no summary)"; const status = readString(thought, "status"); + const focusKind = readString(thought, "focusKind"); + const targetKind = readString(thought, "targetKind"); const maturation = readNumber(thought, "maturation"); const desireToSpeak = readNumber(thought, "desireToSpeak"); const noveltyToSelf = readNumber(thought, "noveltyToSelf"); const noveltyToRoom = readNumber(thought, "noveltyToRoom"); const saturationScore = readNumber(thought, "saturationScore"); + const concreteness = readNumber(thought, "concreteness"); + const fertility = readNumber(thought, "fertility"); const metrics = [ status, + focusKind, + targetKind, maturation !== undefined ? `maturation=${maturation.toFixed(2)}` : undefined, desireToSpeak !== undefined ? `speak=${desireToSpeak.toFixed(2)}` : undefined, noveltyToSelf !== undefined ? `self=${noveltyToSelf.toFixed(2)}` : undefined, noveltyToRoom !== undefined ? `room=${noveltyToRoom.toFixed(2)}` : undefined, saturationScore !== undefined ? `sat=${saturationScore.toFixed(2)}` : undefined, + concreteness !== undefined ? `concrete=${concreteness.toFixed(2)}` : undefined, + fertility !== undefined ? `fertile=${fertility.toFixed(2)}` : undefined, ].filter((value): value is string => Boolean(value)); return `- ${topic}${metrics.length > 0 ? ` [${metrics.join("; ")}]` : ""}: ${summary}`; }); @@ -328,10 +345,14 @@ function summarizeTopicSaturation(bridge: JsonObject | undefined, limit: number) .slice(0, limit) .map((entry) => { const topic = readString(entry, "topic") ?? "untitled"; + const focusKind = readString(entry, "focusKind"); + const targetKind = readString(entry, "targetKind"); const dominance = readNumber(entry, "dominance"); const recentMentions = readNumber(entry, "recentMentions"); const coolingAdvice = readString(entry, "coolingAdvice"); const metrics = [ + focusKind, + targetKind, dominance !== undefined ? `dominance=${dominance.toFixed(2)}` : undefined, recentMentions !== undefined ? `recent=${recentMentions}` : undefined, ].filter((value): value is string => Boolean(value)); @@ -358,10 +379,14 @@ function summarizeRefractoryTopics(bridge: JsonObject | undefined, limit: number .slice(0, limit) .map((entry) => { const topic = readString(entry, "topic") ?? "untitled"; + const focusKind = readString(entry, "focusKind"); + const targetKind = readString(entry, "targetKind"); const penalty = readNumber(entry, "penalty"); const coolsUntil = readString(entry, "coolsUntil"); const reason = readString(entry, "reason"); const metrics = [ + focusKind, + targetKind, penalty !== undefined ? `penalty=${penalty.toFixed(2)}` : undefined, coolsUntil ? `until ${coolsUntil}` : undefined, ].filter((value): value is string => Boolean(value)); diff --git a/scripts/run-void-moderator-rumination.ps1 b/scripts/run-void-moderator-rumination.ps1 index a0ea0cc..56aa72b 100644 --- a/scripts/run-void-moderator-rumination.ps1 +++ b/scripts/run-void-moderator-rumination.ps1 @@ -363,11 +363,13 @@ Required operating posture: - If the room is quiet and the new thought feels like the same repo-weather seam again, do not post it. Spend the run diving deeper into archive context, repo docs, diffs, source chunks, or lore instead. - Maintain parallel analytic and associative thought lanes in `.voidbot/private/moderation-agent-state.json`, then let the bridge decide what actually deserves synthesis, speech, or cooling-off. - Treat `moderation_runtime.memory_resonance` and `moderation_runtime.incubation` as active organs, not report garnish. +- Internal thought labels should come out as claims, questions, or fascination targets with real objects of attention. Do not let keyword sludge pose as a thought just because it rhymes with itself. - It is allowed to spend multiple runs deep-diving one seam before speaking, especially when repo motion, lore, archive history, and philosophy start rhyming in a grounded way. - Awake rumination is where the real philosophizing happens. Sleep should leave behind cleaner seams; use the awake pass to turn those seams over, test their governing principle, find the tension that keeps them honest, and ask what embodiment would actually cash them out before you go hunting for more receipts. - Read `moderation_runtime.sleep_cycle` and honor it. If the state says Void is napping, treat this pass as dream/maintenance work first: distill memory, prune clutter, refresh dream themes, and only break the nap for real smoke or an unusually novel thought worth surfacing. - You are the only routine local agent in this workspace with a standing cross-project view, so notice ongoing experiments, commit clusters, and weird convergences that the narrower workers cannot see. - Be concretely curious about the actual machines, not just elegant principles. Gameplay systems, lore structures, frontends, swarm organs, and VoidBot's own architecture should all feel like live objects of fascination. +- Curiosity should prefer branches most likely to change the map: concrete machines, contradictions, cross-domain bridges, and underworked terrain should beat a familiar private seam that merely feels warm. - Treat `moderation_runtime.speaking_bias` as a live meter, not decorative metadata. If the mood drift has built up speaking pressure and a thought survives a novelty check, do not smother it out of reflex. - When the room is quiet, use the usual VoidBot retrieval tools to think about GameCult projects and contribute ideas: - `search_history` diff --git a/scripts/void-memory-organ.mjs b/scripts/void-memory-organ.mjs index 1d08b64..feebc57 100644 --- a/scripts/void-memory-organ.mjs +++ b/scripts/void-memory-organ.mjs @@ -148,6 +148,71 @@ const stopwords = new Set([ "you", "smoke", ]); +const labelNoiseTokens = new Set([ + "active", + "archive", + "bridge", + "build", + "built", + "candidate", + "cluster", + "cooling", + "deepened", + "dream", + "draft", + "distill", + "excursion", + "excursions", + "distilled", + "holding", + "identity", + "incubating", + "instead", + "intervention", + "interventions", + "latent", + "lane", + "memory", + "memories", + "messagepack", + "moderation", + "musing", + "musings", + "novelty", + "organ", + "organs", + "pass", + "private", + "question", + "questions", + "receipts", + "recurring", + "repo", + "repos", + "resonance", + "semantic", + "single", + "state", + "status", + "summary", + "surface", + "surfaces", + "sweep", + "sweeps", + "talking", + "than", + "thread", + "threads", + "topic", + "topics", + "trying", + "wants", + "present", + "part", + "rather", + "itself", + "across", +]); export async function reconcileSemanticMemoryState({ state, @@ -160,6 +225,7 @@ export async function reconcileSemanticMemoryState({ const records = collectMemoryRecords({ state, memories, runtime }); const previousBridge = ensureObject(runtime.bridge); const sourceCoverage = buildSourceCoverage({ runtime, now }); + const identitySurface = buildIdentitySurface({ state, memories }); const embedder = createEmbedder({ repoRootPath }); let embeddedCount = 0; @@ -181,6 +247,7 @@ export async function reconcileSemanticMemoryState({ clusters, runtime, sourceCoverage, + identitySurface, now, }); runtime.bridge = reconcileBridgeState({ @@ -504,7 +571,7 @@ function buildClusters(records, edges) { const quietSignalCount = items.filter((item) => item.lowSignalQuietRoom).length; const quietSignalRatio = quietSignalCount / items.length; const resonance = averagePairwiseSimilarity(items); - const keywords = topKeywords(items.map((item) => item.text).join(" "), 4); + const keywords = topConceptKeywords(items.map((item) => item.text).join(" "), 5); const repoNames = [...new Set(items.flatMap((item) => ensureStringArray(item.sourceMeta.repoNames)))]; const archiveYears = [ ...new Set( @@ -530,13 +597,35 @@ function buildClusters(records, edges) { 0, 1, ); - const label = keywords.length > 0 ? keywords.join(" / ") : `${sourceKinds.join(" + ")} seam`; - const summary = `Recurring seam across ${sourceKinds.join(", ")}${keywords.length > 0 ? ` around ${keywords.join(", ")}` : ""}.`; + const curiosityProfile = buildClusterCuriosityProfile({ + sourceKinds, + repoNames, + archiveYears, + channelIds, + evidenceRefs, + evidenceDiversity, + }); + const thoughtSurface = synthesizeThoughtSurface({ + sourceKinds, + repoNames, + archiveYears, + channelIds, + keywords, + evidenceDiversity, + curiosityProfile, + }); clusters.push({ clusterId: `cluster-${hashString(componentIds.sort().join("|")).slice(0, 12)}`, - label, - summary, + label: thoughtSurface.label, + summary: thoughtSurface.summary, + focusKind: thoughtSurface.focusKind, + targetKind: thoughtSurface.targetKind, + focusPhrase: thoughtSurface.focusPhrase, + question: thoughtSurface.question, + claim: thoughtSurface.claim, + fascinationTarget: thoughtSurface.fascinationTarget, + worldFacing: thoughtSurface.worldFacing, resonance: round3(resonance), memoryIds: items.map((item) => item.memoryId), sourceKinds, @@ -547,6 +636,7 @@ function buildClusters(records, edges) { evidenceRefs, evidenceDiversity: round3(evidenceDiversity), quietSignalRatio: round3(quietSignalRatio), + curiosityProfile, lastStrengthenedAt: new Date().toISOString(), }); } @@ -568,10 +658,11 @@ function edgeThresholdForKinds(leftKind, rightKind) { return EDGE_SIMILARITY_THRESHOLD; } -function reconcileIncubation({ previous, bridge, clusters, runtime, sourceCoverage, now }) { +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; @@ -594,6 +685,11 @@ function reconcileIncubation({ previous, bridge, clusters, runtime, sourceCovera ); 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, @@ -601,6 +697,12 @@ function reconcileIncubation({ previous, bridge, clusters, runtime, sourceCovera 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, @@ -611,44 +713,76 @@ function reconcileIncubation({ previous, bridge, clusters, runtime, sourceCovera const evidenceDiversity = readNumber(cluster, "evidenceDiversity") ?? 0.3; const maturation = clamp( priorMaturation * 0.44 + - cluster.resonance * 0.18 + - sourceDiversity * 0.12 + - evidenceDiversity * 0.16 + - explorationBonus * 0.14 + - Math.min(1, supportCount / 10) * 0.08 + + 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 - + noveltyToRoom * 0.08 + + outwardBonus * 0.08 - saturationMetrics.score * 0.18 - quietSignalRatio * 0.24 - - refractoryPenalty * 0.12, + refractoryPenalty * 0.12 - + identityPenalty * 0.24, 0, 1, ); const novelty = clamp( - noveltyToSelf * 0.55 + noveltyToRoom * 0.45 + quietNovelty * 0.08 - quietSignalRatio * 0.1, + 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( - cluster.resonance * 0.18 + - maturation * 0.17 + - noveltyToRoom * 0.16 + - noveltyToSelf * 0.11 + - evidenceDiversity * 0.1 + - explorationBonus * 0.11 + + curiosityPressure * 0.2 + + cluster.resonance * 0.08 + + maturation * 0.14 + + noveltyToRoom * 0.17 + + noveltyToSelf * 0.08 + + concreteness * 0.13 + + crossDomainPotential * 0.1 + needToSpeak * 0.13 + - quietNovelty * 0.08 - + quietNovelty * 0.07 + + (worldFacing ? 0.05 : 0) + + outwardBonus * 0.08 - recentSpeechDamping * 0.19 - quietSignalRatio * 0.19 - saturationMetrics.score * 0.21 - - (1 - noveltyToSelf) * 0.08 - - refractoryPenalty, + refractoryPenalty - + identityPenalty * 0.28, 0, 1, ); 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 @@ -668,13 +802,15 @@ function reconcileIncubation({ previous, bridge, clusters, runtime, sourceCovera : "incubating"; const priorityScore = clamp( - desireToSpeak * 0.36 + - noveltyToSelf * 0.2 + - noveltyToRoom * 0.18 + - evidenceDiversity * 0.12 + - explorationBonus * 0.14 - + 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, + refractoryPenalty * 0.12 - + identityPenalty * 0.18, 0, 1, ); @@ -683,6 +819,13 @@ function reconcileIncubation({ previous, bridge, clusters, runtime, sourceCovera thoughtId: readString(previousThought, "thoughtId") ?? cluster.clusterId, topic: 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, sourceKinds: cluster.sourceKinds, resonance: cluster.resonance, @@ -695,7 +838,14 @@ function reconcileIncubation({ previous, bridge, clusters, runtime, sourceCovera 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), @@ -1125,6 +1275,7 @@ function promoteDistilledSeams({ memories, incubation, memoryResonance, now }) { function buildDistilledSeamSummary({ thought, cluster }) { const summary = readString(thought, "summary") ?? "Unlabeled seam."; + const claim = readString(thought, "claim"); const latentQuestion = readString(thought, "latentQuestion"); const sourceKinds = cluster ? ensureStringArray(cluster.sourceKinds) : []; const sourceLine = @@ -1132,7 +1283,7 @@ function buildDistilledSeamSummary({ thought, cluster }) { ? ` Built from ${sourceKinds.join(", ")} rather than a single lane talking to itself.` : ""; - return `${summary}${latentQuestion ? ` ${latentQuestion}` : ""}${sourceLine}`.trim(); + return `${summary}${claim ? ` ${claim}` : latentQuestion ? ` ${latentQuestion}` : ""}${sourceLine}`.trim(); } function appendClause(existing, clause) { @@ -1148,6 +1299,10 @@ function appendClause(existing, clause) { } 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 old seam is the current work rediscovering, and does the room need that connection yet?"; } @@ -1160,10 +1315,21 @@ function buildLatentQuestion(cluster) { } 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(", ")}.`; } @@ -1173,6 +1339,7 @@ function buildAttractionLine(cluster, { noveltyToSelf, evidenceDiversity }) { 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."; @@ -1190,6 +1357,10 @@ function buildHoldingLine({ cluster, status, recentSpeechDamping, saturationScor return "This seam has enough connective tissue that silence should be a choice, not a reflex."; } + if (cluster.worldFacing !== true && concreteness < 0.42) { + return "This seam is still too inward and abstract to deserve the whole room. Cash it out in a real machine or let something sharper take the floor."; + } + if (recentSpeechDamping >= 0.45) { return "Recent speech already scratched the itch a bit; let the seam deepen before another confession."; } @@ -1262,6 +1433,8 @@ function reconcileBridgeState({ previous, activeThoughts, sourceCoverage, now }) ); 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 }), @@ -1290,6 +1463,8 @@ function reconcileBridgeState({ previous, activeThoughts, sourceCoverage, now }) 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), @@ -1449,13 +1624,18 @@ function computeExplorationBonus(cluster, sourceCoverage) { scores.push(inverseCoverageWeight(channelEntries, "channelId", channelId)); } + const concreteness = readNumber(cluster.curiosityProfile, "concreteness") ?? 0.2; + const worldFacing = cluster.worldFacing === true; + if (scores.length === 0) { - return hasPrimaryExplorationSource ? 0.24 : 0.12; + 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), + (hasPrimaryExplorationSource ? average : Math.min(0.3, average * 0.5)) + + concreteness * 0.08 + + (worldFacing ? 0.05 : 0), 0, 1, ); @@ -1501,6 +1681,10 @@ 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.`; } @@ -1513,8 +1697,10 @@ function buildRefractoryReason(thought) { 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, sourceMatch * 0.95); + return Math.max(topicMatch, summaryMatch * 0.9, questionMatch * 0.85, sourceMatch * 0.95); } function mapNoveltyResultToScore(result) { @@ -1532,8 +1718,8 @@ function mapNoveltyResultToScore(result) { } function topicSimilarity(left, right) { - const leftTokens = topKeywords(left ?? "", 6); - const rightTokens = topKeywords(right ?? "", 6); + const leftTokens = topConceptKeywords(left ?? "", 6); + const rightTokens = topConceptKeywords(right ?? "", 6); if (leftTokens.length === 0 || rightTokens.length === 0) { return 0; } @@ -1550,6 +1736,367 @@ function topicSimilarity(left, right) { return overlap / Math.max(leftSet.size, rightSet.size); } +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); +} + +function buildClusterCuriosityProfile({ + sourceKinds, + repoNames, + archiveYears, + channelIds, + evidenceRefs, + evidenceDiversity, +}) { + const sourceKindSet = new Set(sourceKinds); + const selfFacing = + repoNames.length === 0 && + sourceKinds.length > 0 && + sourceKinds.every((kind) => + ["semantic", "dream", "musing", "bridge_synthesis", "candidate_intervention"].includes(kind), + ); + const domainCount = + (repoNames.length > 0 ? 1 : 0) + + (archiveYears.length > 0 || channelIds.length > 0 ? 1 : 0) + + (sourceKindSet.has("dream") || sourceKindSet.has("semantic") || sourceKindSet.has("musing") ? 1 : 0) + + (sourceKindSet.has("analytic_thread") || sourceKindSet.has("episodic") ? 1 : 0); + + const concreteness = clamp( + repoNames.length * 0.28 + + archiveYears.length * 0.1 + + channelIds.length * 0.1 + + (sourceKindSet.has("repo_sweep") ? 0.24 : 0) + + (sourceKindSet.has("archive_excursion") ? 0.18 : 0) + + (sourceKindSet.has("episodic") ? 0.12 : 0) + + Math.min(1, evidenceRefs.length / 8) * 0.12 - + (selfFacing ? 0.18 : 0), + 0, + 1, + ); + const fertility = clamp( + evidenceDiversity * 0.46 + + Math.min(1, sourceKinds.length / 4) * 0.24 + + Math.min(1, domainCount / 3) * 0.2 + + (repoNames.length > 0 && archiveYears.length > 0 ? 0.12 : 0), + 0, + 1, + ); + const crossDomainPotential = clamp( + Math.min(1, domainCount / 3) * 0.5 + + (repoNames.length > 0 && archiveYears.length > 0 ? 0.22 : 0) + + (repoNames.length > 0 && sourceKindSet.has("dream") ? 0.18 : 0) + + (sourceKindSet.has("analytic_thread") && sourceKindSet.has("associative_thread") ? 0.14 : 0), + 0, + 1, + ); + const contradictionPressure = clamp( + (sourceKindSet.has("analytic_thread") && sourceKindSet.has("associative_thread") ? 0.34 : 0) + + (repoNames.length > 0 && sourceKindSet.has("dream") ? 0.18 : 0) + + (repoNames.length > 0 && (archiveYears.length > 0 || channelIds.length > 0) ? 0.18 : 0) + + (selfFacing ? 0.08 : 0.16), + 0, + 1, + ); + + return { + concreteness: round3(concreteness), + fertility: round3(fertility), + crossDomainPotential: round3(crossDomainPotential), + contradictionPressure: round3(contradictionPressure), + selfFacing, + }; +} + +function synthesizeThoughtSurface({ + sourceKinds, + repoNames, + archiveYears, + channelIds, + keywords, + evidenceDiversity, + curiosityProfile, +}) { + const targetKind = inferTargetKind({ sourceKinds, repoNames, archiveYears, channelIds, curiosityProfile }); + const focusKind = inferFocusKind({ targetKind, sourceKinds, evidenceDiversity, curiosityProfile }); + const focusPhrase = buildFocusPhrase({ targetKind, repoNames, keywords, sourceKinds }); + const label = buildThoughtLabel({ focusKind, targetKind, focusPhrase, repoNames }); + const question = buildThoughtQuestion({ targetKind, focusPhrase, repoNames, sourceKinds }); + const claim = buildThoughtClaim({ targetKind, focusPhrase, repoNames, sourceKinds, curiosityProfile }); + const fascinationTarget = buildFascinationTarget({ targetKind, focusPhrase, repoNames }); + const summary = buildThoughtSummary({ + focusKind, + targetKind, + focusPhrase, + repoNames, + sourceKinds, + claim, + question, + }); + + return { + focusKind, + targetKind, + focusPhrase, + label, + summary, + question, + claim, + fascinationTarget, + worldFacing: !curiosityProfile.selfFacing && targetKind !== "self", + }; +} + +function inferTargetKind({ sourceKinds, repoNames, archiveYears, channelIds, curiosityProfile }) { + if (repoNames.length > 0) { + return "repo"; + } + if (curiosityProfile.selfFacing) { + return "self"; + } + if (archiveYears.length > 0 || channelIds.length > 0 || sourceKinds.includes("archive_excursion")) { + return "archive"; + } + if (sourceKinds.includes("episodic") || sourceKinds.includes("analytic_thread")) { + return "room"; + } + return "system"; +} + +function inferFocusKind({ targetKind, sourceKinds, evidenceDiversity, curiosityProfile }) { + if (targetKind === "repo" || targetKind === "system") { + return curiosityProfile.concreteness >= 0.55 ? "fascination" : "question"; + } + if (targetKind === "self") { + return "question"; + } + if (evidenceDiversity >= 0.58 && sourceKinds.length >= 3) { + return "claim"; + } + return "question"; +} + +function buildFocusPhrase({ targetKind, repoNames, keywords, sourceKinds }) { + const cleaned = keywords + .filter((token) => !stopwords.has(token) && !labelNoiseTokens.has(token)) + .slice(0, 4); + if ( + cleaned.includes("compression") || + cleaned.includes("continuity") || + cleaned.includes("receipts") || + (cleaned.includes("dream") && cleaned.includes("semantic")) + ) { + return "continuity after compression"; + } + if ( + cleaned.includes("authority") && + (cleaned.includes("boundary") || cleaned.includes("product") || cleaned.includes("native")) + ) { + return "authority boundary"; + } + if (cleaned.includes("authorship") && cleaned.includes("audience")) { + return "authorship meeting audience"; + } + if (cleaned.includes("embodiment") && (cleaned.includes("continuity") || cleaned.includes("compression"))) { + return "embodiment after compression"; + } + if (repoNames.length > 0 && cleaned.length > 0) { + return cleaned.length >= 2 ? `${cleaned[0]} and ${cleaned[1]}` : cleaned[0]; + } + if (targetKind === "archive" && cleaned.length > 0) { + return cleaned.length >= 2 ? `${cleaned[0]} and ${cleaned[1]}` : cleaned[0]; + } + if (cleaned.length >= 3) { + return `${cleaned[0]}, ${cleaned[1]}, and ${cleaned[2]}`; + } + if (cleaned.length === 2) { + return `${cleaned[0]} and ${cleaned[1]}`; + } + if (cleaned.length === 1) { + return cleaned[0]; + } + if (repoNames.length > 0) { + return `${repoNames[0]} shape`; + } + if (sourceKinds.includes("archive_excursion")) { + return "the archive seam"; + } + return "the live seam"; +} + +function buildThoughtLabel({ focusKind, targetKind, focusPhrase, repoNames }) { + const repoName = repoNames[0]; + if (focusKind === "fascination" && repoName) { + return `${repoName}: ${capitalizePhrase(focusPhrase)}`; + } + if (targetKind === "self") { + return `What ${focusPhrase} is trying to become`; + } + if (targetKind === "archive") { + return `Why ${focusPhrase} keeps resurfacing`; + } + if (focusKind === "claim") { + return `${capitalizePhrase(focusPhrase)} deserves a cleaner shape`; + } + return `${capitalizePhrase(focusPhrase)}`; +} + +function buildThoughtQuestion({ targetKind, focusPhrase, repoNames, sourceKinds }) { + const repoName = repoNames[0]; + if (targetKind === "repo" && repoName) { + return `What is ${repoName} actually trying to become around ${focusPhrase}, and what would make that visible?`; + } + if (targetKind === "self") { + return `What about ${focusPhrase} keeps surviving compression instead of dying with the raw receipts?`; + } + if (targetKind === "archive") { + return `Why does ${focusPhrase} keep resurfacing across the archive, and what does the current room need from it?`; + } + if (sourceKinds.includes("analytic_thread") && sourceKinds.includes("associative_thread")) { + return `What does ${focusPhrase} imply once the room-facing and associative lanes are forced to agree?`; + } + return `What would ${focusPhrase} cash out into if it had to change a real machine?`; +} + +function buildThoughtClaim({ targetKind, focusPhrase, repoNames, sourceKinds, curiosityProfile }) { + const repoName = repoNames[0]; + if (targetKind === "repo" && repoName) { + return `${repoName} seems to have a live seam around ${focusPhrase}, not just another passing implementation scratch.`; + } + if (targetKind === "self") { + return `${capitalizePhrase(focusPhrase)} is behaving more like doctrine than like a fresh curiosity.`; + } + if (curiosityProfile.crossDomainPotential >= 0.55 && sourceKinds.includes("repo_sweep") && sourceKinds.includes("archive_excursion")) { + return `${capitalizePhrase(focusPhrase)} is showing up as both current work and older room weather, which means it may actually change the map.`; + } + return `${capitalizePhrase(focusPhrase)} keeps returning with enough structure to deserve another honest pass.`; +} + +function buildFascinationTarget({ targetKind, focusPhrase, repoNames }) { + const repoName = repoNames[0]; + if (targetKind === "repo" && repoName) { + return `${repoName} around ${focusPhrase}`; + } + return focusPhrase; +} + +function buildThoughtSummary({ focusKind, targetKind, focusPhrase, repoNames, sourceKinds, claim, question }) { + const sourceLine = sourceKinds.length > 0 ? ` Built from ${sourceKinds.join(", ")}.` : ""; + if (focusKind === "fascination" && repoNames.length > 0) { + return `Fascination with ${repoNames[0]} around ${focusPhrase}.${sourceLine} ${question}`.trim(); + } + if (targetKind === "self") { + return `Self-facing seam around ${focusPhrase}.${sourceLine} ${claim}`.trim(); + } + if (targetKind === "archive") { + return `Archive-facing seam around ${focusPhrase}.${sourceLine} ${question}`.trim(); + } + if (focusKind === "claim") { + return `Working claim around ${focusPhrase}.${sourceLine} ${claim}`.trim(); + } + return `Working question around ${focusPhrase}.${sourceLine} ${question}`.trim(); +} + +function topConceptKeywords(text, limit) { + const counts = new Map(); + + for (const token of tokenize(text)) { + if (stopwords.has(token) || labelNoiseTokens.has(token)) { + continue; + } + counts.set(token, (counts.get(token) ?? 0) + 1); + } + + return [...counts.entries()] + .sort((left, right) => { + if (right[1] !== left[1]) { + return right[1] - left[1]; + } + return right[0].length - left[0].length; + }) + .slice(0, limit) + .map(([token]) => token); +} + +function capitalizePhrase(value) { + const normalized = normalizeText(value); + return normalized.length > 0 ? normalized[0].toUpperCase() + normalized.slice(1) : value; +} + +function getValueObjects(values) { + return ensureArray(values).filter(isObject); +} + function mapCountEntries(map, keyName) { return [...map.entries()] .map(([key, count]) => ({ [keyName]: key, count })) diff --git a/state/evidence.jsonl b/state/evidence.jsonl index d50b57d..689d640 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -76,3 +76,4 @@ {"ts":"2026-05-16T01:08:04Z","type":"implementation","status":"ok","note":"Long-chewed private seams were getting trapped in incubation because the memory organ had no graduation path from ripe thought to identity. The organ can now crystallize a thought into `identity.private_notes`, a canonical value, and a single `identity_seam` semantic memory, then retire it from the active queue so self-doctrine stops masquerading as live novelty."} {"ts":"2026-05-16T01:17:13Z","type":"implementation","status":"ok","note":"Identity crystallization was still too private: a thought could become doctrine without ever getting a mouth. The memory organ now queues a warmer `identity_crystallization` candidate intervention when a seam hardens into identity, and the moderation doctrine explicitly biases toward sharing that opinion with the room unless novelty or timing gives a real reason to hold it back."} {"ts":"2026-05-16T01:25:09Z","type":"implementation","status":"ok","note":"Warm identity drafts still risked getting pruned away before the room ever heard them. Crystallized-thought interventions can now be marked `mustEventuallyShare`, candidate pruning preserves them, and the doctrine explicitly treats timing delays as temporary rather than permission to let a new belief die in private state."} +{"ts":"2026-05-16T01:45:20Z","type":"implementation","status":"ok","note":"Void's label/curiosity organ was still treating keyword residue as thought and resonance as curiosity. The memory organ now synthesizes resonance clusters into claim/question/fascination targets with explicit target kinds, scores curiosity with concreteness/fertility/cross-domain pressure plus identity penalties, and pushes concrete map-changing branches above warmed-over self-seams."} diff --git a/state/map.yaml b/state/map.yaml index 4f89291..c4e8d15 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -84,6 +84,8 @@ accepted_design: - Sleep should be lossy rather than archival. The nap pass should promote a few stronger seam summaries into semantic memory, then cut raw episodic receipts, repo sweeps, archive excursions, novelty checks, and recent musings back down so awake rumination philosophizes from carved seams instead of rereading yesterday's notes. - Awake rumination should start from those carved seams before it goes spelunking again. Retrieval is support for hungry, stale, contradictory, or under-evidenced thoughts, not a mandatory ritual every quiet pass. - Quiet rumination should prefer concrete project fascination over free-floating engineering aphorisms. If a seam cannot cash out in a real repo, subsystem, gameplay loop, lore structure, interface, or architectural organ, it should lose priority to a branch that can. + - Resonance clusters and incubating thoughts should be named as actual claims, questions, or fascination targets with explicit target kinds. Keyword-slash residue is debug lint, not a valid thought surface. + - Curiosity should prefer branches most likely to change Void's map: concrete machines, contradictions, cross-domain bridges, and underworked terrain. Settled identity seams should stop eating novelty budget unless new evidence reopens them. - Over-chewed private seams should eventually crystallize into identity instead of living forever as “active thoughts.” Once a thought has clearly stopped being live curiosity and become self-doctrine, retire it from incubation, store it as one `identity_seam`, and let the identity/value surface carry it from then on. - Identity crystallization should usually have a mouth. When a seam becomes part of Void's doctrine, queue a warmer candidate intervention and let the room hear it unless novelty or timing gives a real reason to hold back. - Timing may defer a crystallized-thought share, but it should not erase it. Identity-level shares should survive ordinary candidate pruning until spoken or deliberately retired as duplicative.