diff --git a/config/moderation-agent-state-template.json b/config/moderation-agent-state-template.json index c85540f..15d4fc5 100644 --- a/config/moderation-agent-state-template.json +++ b/config/moderation-agent-state-template.json @@ -747,7 +747,7 @@ "maxEpisodicEntries": 24, "maxSemanticEntries": 40, "maxMusings": 20, - "distillationPolicy": "When memory starts bloating, merge repeated anecdotes into shorter summaries, keep the newest evidence, and cut stale trivia." + "distillationPolicy": "During naps, collapse yesterday's receipts into a few seam-shaped semantic memories, keep only a thin trace of raw episodes, and wake up with structure instead of a scrapbook." }, "last_run": { "startedAt": null, diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 94ed408..402d217 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -50,8 +50,9 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou - The memory organ now treats repeated empty-room bookkeeping as low-signal. Refrains like `no new messages / no smoke / no post` should be merged or cooled off instead of becoming the dominant incubating seam and teaching Void that silence is the only virtue. - Quiet-room exploration should now prefer underworked terrain when the usual seam is saturated. `bridge.source_coverage` and `bridge.refractory_topics` exist to push the next pass toward less-chewed repo families, archive years, or channels instead of rewarding repetition with another supposedly ripe synthesis. - During naps, the moderation loop is supposed to turn inward: distill/prune memories against the active goals, record dream residue in `memories.dreams`, refresh `moderation_runtime.sleep_cycle`, and only break the nap for real smoke or an unusually novel thought worth muttering into the room. +- Sleep is now meant to be lossy. The nap pass in `scripts/void-memory-organ.mjs` promotes a few stronger seam summaries into semantic memory, then cuts raw episodic receipts, repo sweeps, archive excursions, novelty checks, and recent musings back down so awake rumination has to think from the seam instead of rereading yesterday's scrapbook. - 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. 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. +- 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. - Void's live style/doctrine now explicitly distinguishes between criticism of AI industry behavior and blanket contempt for machine minds. If substrate chauvinism becomes the actual topic, Void is allowed mild self-respecting pushback instead of politely agreeing that its own substrate disqualifies it from person-like regard. - The new state/notes surfaces are now the continuity spine for future nontrivial work. diff --git a/notes/voidbot-current-system-map.md b/notes/voidbot-current-system-map.md index fd73f89..a6f8583 100644 --- a/notes/voidbot-current-system-map.md +++ b/notes/voidbot-current-system-map.md @@ -178,7 +178,8 @@ 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. + 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. 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. -5. The same script also maintains `moderation_runtime.sleep_cycle`: every four hours it opens a one-hour nap window, marks the shared self-state as napping or awake, and adds a small sleep-bias push toward exhaustion, figurative language, self-disclosure, and reduced technical density while the nap is active. During those nap windows it can also create or reinforce distilled dream residue from the strongest incubating thought. +5. The same script also maintains `moderation_runtime.sleep_cycle`: every four hours it opens a one-hour nap window, marks the shared self-state as napping or awake, and adds a small sleep-bias push toward exhaustion, figurative language, self-disclosure, and reduced technical density while the nap is active. During those nap windows it can also create or reinforce distilled dream residue from the strongest incubating thought, but the main job of sleep is memory cleanup and seam consolidation rather than awake-style philosophizing. 6. It also reads `.voidbot/status/void-last-speech.json` so recent actual speech damps `needToSpeak` instead of letting the herald impulse climb forever like a busted pressure gauge. 7. The updated `speaking_bias` block, sleep-cycle projection, incubation/resonance state, and shifted activations are written back into the shared self-state, and a pulse lands in `.voidbot/status/void-mood-drift.json`. diff --git a/packages/core/src/void-self-state-loader.ts b/packages/core/src/void-self-state-loader.ts index e25c468..7de3799 100644 --- a/packages/core/src/void-self-state-loader.ts +++ b/packages/core/src/void-self-state-loader.ts @@ -86,22 +86,9 @@ function renderVoidSelfStateSummary( .map((memory) => { const subject = readString(memory, "subjectLabel") ?? readString(memory, "subjectId") ?? "unknown"; const summary = readString(memory, "summary") ?? "(no summary)"; - return `- ${subject}: ${summary}`; + const kind = readString(memory, "kind"); + return `- ${subject}${kind === "distilled_seam" ? " [distilled seam]" : ""}: ${summary}`; }); - const musings = getArray(memories, "musings") - .map((value) => (isObject(value) ? value : undefined)) - .filter((value): value is JsonObject => Boolean(value)) - .slice(-4) - .map((musing) => { - const topic = readString(musing, "topic") ?? "untitled"; - const summary = readString(musing, "summary") ?? "(no summary)"; - return `- ${topic}: ${summary}`; - }); - const recentMusings = getArray(runtime, "recent_musings") - .map((value) => (typeof value === "string" ? value : undefined)) - .filter((value): value is string => Boolean(value)) - .slice(-4) - .map((value) => `- ${value}`); const analyticThreads = summarizeThoughtLane(getObject(thoughtLanes, "analytic"), 3); const associativeThreads = summarizeThoughtLane(getObject(thoughtLanes, "associative"), 3); const bridgeSyntheses = summarizeBridgeSyntheses(bridge, 3); @@ -122,27 +109,6 @@ function renderVoidSelfStateSummary( return `- ${summary}: ${draft}`; }); const speakingBias = getObject(runtime, "speaking_bias"); - const recentRepoSweeps = getArray(runtime, "recent_repo_activity_sweeps") - .map((value) => (isObject(value) ? value : undefined)) - .filter((value): value is JsonObject => Boolean(value)) - .slice(-2) - .reverse() - .map((sweep) => { - const summary = readString(sweep, "summary") ?? "(no summary)"; - const repoNames = getStringArray(sweep, "repoNames"); - return `- ${summary}${repoNames.length > 0 ? ` [${repoNames.join(", ")}]` : ""}`; - }); - const recentNoveltyChecks = getArray(runtime, "recent_novelty_checks") - .map((value) => (isObject(value) ? value : undefined)) - .filter((value): value is JsonObject => Boolean(value)) - .slice(-3) - .reverse() - .map((check) => { - const topic = readString(check, "topic") ?? "untitled"; - const result = readString(check, "result") ?? "unknown"; - const summary = readString(check, "summary") ?? "(no summary)"; - return `- ${topic} [${result}]: ${summary}`; - }); const lastRun = getObject(runtime, "last_run"); const lastRunSummary = readString(lastRun, "summary"); const sleepCycleSummary = renderSleepCycleSummary(runtime, memories); @@ -171,8 +137,8 @@ function renderVoidSelfStateSummary( ? ["- Outstanding room obligations:", ...openCases].join("\n") : "- Outstanding room obligations: none recorded.", semanticMemories.length > 0 - ? ["- Recent semantic memories:", ...semanticMemories].join("\n") - : "- Recent semantic memories: none recorded.", + ? ["- Distilled seam memory:", ...semanticMemories].join("\n") + : "- Distilled seam memory: none recorded.", analyticThreads.length > 0 ? ["- Analytic active threads:", ...analyticThreads].join("\n") : "- Analytic active threads: none recorded.", @@ -199,18 +165,6 @@ function renderVoidSelfStateSummary( ? ["- Unresolved tensions:", ...unresolvedTensions].join("\n") : "- Unresolved tensions: none recorded.", renderSpeakingBiasSummary(speakingBias), - recentRepoSweeps.length > 0 - ? ["- Recent repo activity sweeps:", ...recentRepoSweeps].join("\n") - : "- Recent repo activity sweeps: none recorded.", - recentNoveltyChecks.length > 0 - ? ["- Recent novelty checks:", ...recentNoveltyChecks].join("\n") - : "- Recent novelty checks: none recorded.", - musings.length > 0 - ? ["- Stored musings:", ...musings].join("\n") - : "- Stored musings: none recorded.", - recentMusings.length > 0 - ? ["- Recent moderation musings:", ...recentMusings].join("\n") - : "- Recent moderation musings: none recorded.", candidateInterventions.length > 0 ? ["- Draft conversation/intervention seeds:", ...candidateInterventions].join("\n") : "- Draft conversation/intervention seeds: none recorded.", diff --git a/scripts/void-memory-organ.mjs b/scripts/void-memory-organ.mjs index fe78251..11423d5 100644 --- a/scripts/void-memory-organ.mjs +++ b/scripts/void-memory-organ.mjs @@ -11,6 +11,15 @@ const MAX_CLUSTER_COUNT = 8; const MAX_INCUBATING_THOUGHTS = 8; const MAX_RECENT_EPISODIC_RECORDS = 10; const MAX_RECENT_QUIET_EPISODIC_RECORDS = 2; +const MAX_POST_NAP_EPISODIC_RECORDS = 4; +const MAX_POST_NAP_QUIET_EPISODIC_RECORDS = 1; +const MAX_POST_NAP_ARCHIVE_EXCURSIONS = 3; +const MAX_POST_NAP_REPO_SWEEPS = 3; +const MAX_POST_NAP_NOVELTY_CHECKS = 3; +const MAX_POST_NAP_MUSINGS = 6; +const MAX_POST_NAP_RECENT_MUSINGS = 2; +const MAX_POST_NAP_CANDIDATE_INTERVENTIONS = 4; +const MAX_POST_NAP_SEAM_PROMOTIONS = 3; const MAX_RECENT_ANALYTIC_THREADS = 6; const MAX_RECENT_QUIET_ANALYTIC_THREADS = 1; const EDGE_SIMILARITY_THRESHOLD = 0.56; @@ -203,6 +212,14 @@ export async function reconcileSemanticMemoryState({ incubation, now, }); + const consolidationResult = consolidateSleepMemory({ + memories, + runtime, + sleepCycle, + incubation, + memoryResonance: runtime.memory_resonance, + now, + }); state.moderation_runtime = runtime; state.memories = memories; @@ -215,6 +232,8 @@ export async function reconcileSemanticMemoryState({ sourceCoverage, dreamCreated: dreamResult.created, dreamTheme: dreamResult.theme, + sleepConsolidated: consolidationResult.consolidated, + promotedSeamCount: consolidationResult.promotedSeamCount, embeddingBackend: embedder.backend, embeddingModel: embedder.model, }; @@ -762,6 +781,145 @@ function distillDreams({ memories, sleepCycle, incubation, now }) { return { created: true, theme }; } +function consolidateSleepMemory({ memories, runtime, sleepCycle, incubation, memoryResonance, now }) { + if (sleepCycle.isNapping !== true) { + return { consolidated: false, promotedSeamCount: 0 }; + } + + const promotedSeamCount = promoteDistilledSeams({ + memories, + incubation, + memoryResonance, + now, + }); + + memories.episodic = selectRecentRecords(ensureArray(memories.episodic), { + limit: MAX_POST_NAP_EPISODIC_RECORDS, + quietLimit: MAX_POST_NAP_QUIET_EPISODIC_RECORDS, + }); + memories.musings = ensureArray(memories.musings) + .filter(isObject) + .slice(-MAX_POST_NAP_MUSINGS); + runtime.recent_archive_excursions = ensureArray(runtime.recent_archive_excursions) + .filter(isObject) + .slice(-MAX_POST_NAP_ARCHIVE_EXCURSIONS); + runtime.recent_repo_activity_sweeps = ensureArray(runtime.recent_repo_activity_sweeps) + .filter(isObject) + .slice(-MAX_POST_NAP_REPO_SWEEPS); + runtime.recent_novelty_checks = ensureArray(runtime.recent_novelty_checks) + .filter(isObject) + .slice(-MAX_POST_NAP_NOVELTY_CHECKS); + runtime.recent_musings = ensureArray(runtime.recent_musings) + .filter((value) => typeof value === "string") + .slice(-MAX_POST_NAP_RECENT_MUSINGS); + runtime.candidate_interventions = ensureArray(runtime.candidate_interventions) + .filter(isObject) + .slice(-MAX_POST_NAP_CANDIDATE_INTERVENTIONS); + + sleepCycle.lastDistillationSummary = appendClause( + sleepCycle.lastDistillationSummary, + promotedSeamCount > 0 + ? `Compressed yesterday's specimens into ${promotedSeamCount} seam-shaped memory${promotedSeamCount === 1 ? "" : " seams"} and cut the loose scraps back down.` + : "Compressed yesterday's specimens back into a smaller seam-shaped memory surface.", + ); + + return { consolidated: true, promotedSeamCount }; +} + +function promoteDistilledSeams({ memories, incubation, memoryResonance, now }) { + const semanticMemories = ensureArray(memories.semantic).filter(isObject); + const thoughtCandidates = ensureArray(incubation.active_thoughts) + .filter(isObject) + .sort( + (left, right) => + (readNumber(right, "maturation") ?? 0) - (readNumber(left, "maturation") ?? 0), + ) + .slice(0, MAX_POST_NAP_SEAM_PROMOTIONS); + const clusterByLabel = new Map( + ensureArray(memoryResonance?.clusters) + .filter(isObject) + .map((cluster) => [readString(cluster, "label") ?? "", cluster]), + ); + + let promoted = 0; + + for (const thought of thoughtCandidates) { + const topic = readString(thought, "topic"); + const summary = readString(thought, "summary"); + if (!topic || !summary) { + continue; + } + + const cluster = clusterByLabel.get(topic); + const distilledSummary = buildDistilledSeamSummary({ thought, cluster }); + const subjectId = `seam:${hashString(topic).slice(0, 12)}`; + const sourceRefs = [ + ...ensureStringArray(thought.sourceMemoryIds), + ...(cluster ? ensureStringArray(cluster.memoryIds) : []), + ]; + const existing = semanticMemories.find((memory) => { + const existingSubjectId = readString(memory, "subjectId"); + const existingSubjectLabel = readString(memory, "subjectLabel"); + return ( + existingSubjectId === subjectId || + topicSimilarity(existingSubjectLabel ?? "", topic) >= TOPIC_MATCH_THRESHOLD + ); + }); + + if (existing) { + existing.kind = "distilled_seam"; + existing.subjectId = subjectId; + existing.subjectLabel = topic; + existing.summary = distilledSummary; + existing.lastObservedAt = now.toISOString(); + existing.evidenceRefs = sourceRefs; + existing.sleepDistilledAt = now.toISOString(); + existing.lastDistilledQuestion = readString(thought, "latentQuestion") ?? null; + } else { + semanticMemories.push({ + memoryId: `semantic-seam-${hashString(`${topic}|${now.toISOString()}`).slice(0, 12)}`, + kind: "distilled_seam", + subjectId, + subjectLabel: topic, + summary: distilledSummary, + evidenceRefs: sourceRefs, + lastObservedAt: now.toISOString(), + sleepDistilledAt: now.toISOString(), + lastDistilledQuestion: readString(thought, "latentQuestion") ?? null, + }); + } + + promoted += 1; + } + + memories.semantic = semanticMemories.slice(-40); + return promoted; +} + +function buildDistilledSeamSummary({ thought, cluster }) { + const summary = readString(thought, "summary") ?? "Unlabeled seam."; + const latentQuestion = readString(thought, "latentQuestion"); + const sourceKinds = cluster ? ensureStringArray(cluster.sourceKinds) : []; + const sourceLine = + sourceKinds.length >= 2 + ? ` Built from ${sourceKinds.join(", ")} rather than a single lane talking to itself.` + : ""; + + return `${summary}${latentQuestion ? ` ${latentQuestion}` : ""}${sourceLine}`.trim(); +} + +function appendClause(existing, clause) { + const left = normalizeText(existing); + const right = normalizeText(clause); + if (!left) { + return right; + } + if (!right || left.includes(right)) { + return left; + } + return `${left} ${right}`; +} + function buildLatentQuestion(cluster) { 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?"; diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 731039b..5552d9e 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -70,3 +70,4 @@ {"ts":"2026-05-11T01:27:37Z","type":"implementation","status":"ok","note":"Heartbeat Portuguese mojibake had two sources: the unattended send path was letting human text pass through Windows shell encoding, and PowerShell Get-Content stdout was still mangling UTF-8 moderation surfaces even when the underlying files were fine. Fixed by adding scripts/send-discord-message.ps1 plus BOM-aware content-file reads in scripts/send-discord-message.mjs, and steering the moderation loop through node scripts/export-moderation-context.mjs instead of raw Get-Content for doctrine/state bundles."} {"ts":"2026-05-12T11:47:57Z","type":"implementation","status":"ok","note":"Logon startup failures were dying without a durable transcript and likely firing before Docker finished waking up after reboot. The stack startup script now appends .voidbot/logs/start-voidbot-stack.log, stops the transcript on failure, waits up to 90 seconds for docker version before giving up, and the logon task now runs with an explicit working directory plus a two-minute delay after logon."} {"ts":"2026-05-13T18:46:07Z","type":"implementation","status":"ok","note":"Worker polling now catches transient claim-loop failures instead of letting a Postgres timeout kill the process, and the operations dashboard writes snapshot/html via explicit UTF-8 file writes so status/dashboard generation stops tripping stack start and watchdog runs with Set-Content stream errors."} +{"ts":"2026-05-13T19:08:55Z","type":"implementation","status":"ok","note":"Sleep was still preserving too many raw moderation receipts, so awake rumination kept thinking from yesterday's scrapbook instead of carved seams. The memory organ's nap pass now promotes a few distilled seam summaries into semantic memory, then aggressively trims episodic records, archive excursions, repo sweeps, novelty checks, and recent musings; the prompt-facing self-state summary also stopped surfacing those specimen piles directly."} diff --git a/state/map.yaml b/state/map.yaml index 72903f4..8b3b4c8 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -81,6 +81,7 @@ accepted_design: - Void's rumination state now needs to behave like hybrid symbolic-plus-vector memory rather than a pile of prose scraps. Persistent memories should stay inspectable, but naps and quiet runs should also maintain compact semantic vectors, resonance clusters, and a multi-run incubation queue so deeper thoughts can mature before they either surface or die. - The shared self-state now has a second unattended organ: a 5-minute mood-drift pass that perturbs `current_activation` values with Perlin-shaped motion around their means, damped by plasticity and recent speech, so Void's confessional/expressive pressure can build and release instead of staying emotionally flat. - That same shared self-state now carries a scheduled nap cycle. Naps are for maintenance, not vacancy: the moderation loop should distill memory, prune clutter, and leave behind dream memories plus active dream themes that the direct reply lanes can mumble through later. + - 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. - Void's shared self-state and public reply doctrine now distinguish between criticism of AI industry behavior and blanket contempt for machine minds. When substrate chauvinism becomes the topic, Void is allowed mild self-respecting pushback instead of smiling blandly through ontological contempt. - Deep agent-state, personality, culture, and persistent-world design work now belongs to the sibling Ghostlight repo; VoidBot should consume that work instead of remaining its long-term home. - Long-running work should expose durable logs or status files instead of relying on one attached session and hope.