Collapse topic saturation into semantic graph

This commit is contained in:
Meta Vens 2026-05-28 19:56:16 +01:00
commit f5788502be
6 changed files with 19 additions and 125 deletions

View file

@ -132,7 +132,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
- First role-addressed chat also initializes the target repo's local `.voidbot` home. `ensureRepoFaceInitialized` resolves `repoPath` from the registry, `SOURCE_REPO_ROOT`, or `E:/Projects/<repoName>`, creates `voice/`, `state/`, `birth/`, and `logs/`, writes `voice/identity.json`, then starts `scripts/run-repo-face-birth.mjs` detached. That worker runs EpiphanyAgent's `epiphany-repo-personality scout` and `epiphany-repo-birth-runner` so newborn personality/memory/trajectory packets come from repo contents and git history. Status and logs live in the target repo under `.voidbot/birth/voidbot-face-birth.json` and `.voidbot/logs/voidbot-face-birth.log`.
- Manual newborn/strengthening passes should use `prompts/repo-face-personality-seed.prompt.md`. It makes the intended rite explicit: inspect repo docs/source/state/history, migrate useful legacy `memory.json` material into typed `.cc` memories, seed values/needs/agency/social state only when they will change behavior, and keep `.voidbot/voice` plus `.voidbot/state` as the repo-local home.
- Agent heartbeats now use one CTB/Final-Fantasy-X-style initiative queue. `scripts/run-repo-face-heartbeats.ts` still has its old filename, but it now reconciles built-in system agents plus registered repo Faces, stores the roster at `REPO_FACE_HEARTBEAT_STATE_PATH`, advances virtual initiative time independently of wall-clock due dates, and dispatches selected turns. Repo Faces queue normal `repo-face-rumination` jobs; built-in `void` starts the existing moderation rumination runner as its turn. Active jobs/locks freeze initiative: recovery starts only after the owner-Codex job or Void moderation lock clears, and completion cannot grant another turn in the same pulse. The active-job scan now marks stale approved/running Face jobs failed through the job queue and records `stale_active_turn_recovered`, so a dead worker claim cannot freeze one Face forever. `GameCult Local Orchestrator` is the live hidden local scheduled task that ticks the scheduler; `npm run state:install-repo-face-heartbeat-task` remains an isolation/backcompat installer, not the normal pulse owner. Heat knobs are `REPO_FACE_HEARTBEAT_GLOBAL_HEAT` and `REPO_FACE_HEARTBEAT_HEAT_OVERRIDES`; overrides can target `all`, `kind:<participantKind>`, `turn:<turnKind>`, `identity:<id>`, `repo:<repo>`, `display:<name>`, `channel:<id>`, or bare identity/repo/display keys. Typed Face speaking pressure now feeds CTB urgency too: high `needToSpeak`, novelty, and confession pressure raise effective speed and can pull `nextTurnAt` closer. Idle cooling is part of the same scheduler: if watched Discord channels have no recent human message, unprompted swarm turns are globally clamped to one dispatch per idle recovery window, defaulting to one every 30 minutes, while pending mentions still bypass the brake. Repo Face prompts now include a jurisdiction-dive cadence so unattended heartbeats periodically inspect repo/history terrain with source tools and convert real findings into state, proposals, essay seeds, agency pressure, or speech.
- Repo Face heartbeat prompt assembly now has one model projection boundary before the child Face sees context: parent Imagination (`repo-face-state-projector.prompt.md`) turns typed Face state, research hints, semantic attractors, memory, affect, and social pressure into character-facing prose. Recent room messages, nearby channel snapshots, and pending mentions are deterministic raw transcript sections, oldest-to-newest, with explicit recency/correction rules. The deleted conversation projector was turning evidence into mushy consensus; do not revive it. Current-room topic saturation may reach Imagination only as abstract pressure, never as a repeated-term or matched-term list; otherwise it becomes phrase bait. The Face model must read human corrections directly. Model ownership is split by organ: Imagination, Face, and parent Mind now default to full `gpt-5.4`, while the old heartbeat Spark/Mini setting remains only a compatibility support lane.
- Repo Face heartbeat prompt assembly now has one model projection boundary before the child Face sees context: parent Imagination (`repo-face-state-projector.prompt.md`) turns typed Face state, research hints, semantic attractors, memory, affect, and social pressure into character-facing prose. Recent room messages, nearby channel snapshots, and pending mentions are deterministic raw transcript sections, oldest-to-newest, with explicit recency/correction rules. The deleted conversation projector was turning evidence into mushy consensus; do not revive it. Current-room topic saturation is no longer a separate repeated-term detector; semantic saturation emerges inside the curiosity graph from overlap between recent conversation, Face state, and retrieved history/source clusters. The Face model must read human corrections directly. Model ownership is split by organ: Imagination, Face, and parent Mind now default to full `gpt-5.4`, while the old heartbeat Spark/Mini setting remains only a compatibility support lane.
- Repo Face prompts now include read-only recent home-repo activity before Discord transcript. `scripts/run-repo-face-heartbeats.ts` calls `scripts/export-recent-repo-activity.mjs --repos <repo> --state-path <face-state> --read-only --hours 96 --max-commits 5`, so Faces see current body/jurisdiction motion before reacting to room pressure. This is the fix for Mimir missing live camera-runtime work, not a downstream compensator telling it to apologize harder.
- Repo Face prompts now carry deterministic human pronoun guidance outside the model-projected state surface. The 2026-05-22 Nibu misgendering happened because Metacrat's interaction profile correctly resolved explicit `she/her`, but that fact was only inside the state packet handed to the projector, and the projector omitted it. `repo-face-turn.prompt.md` now renders `Known human pronoun guidance` near the transcript and again near the final writing instruction; the renderer also chooses evidence supporting the resolved pronoun set rather than the freshest contradictory ambient read.
- Worker delivery must treat Interpreter channel names as Face-local labels, not raw Discord ids. `SAY channel: aquarium` or `channel: narrative` resolves through the registered identity's `channelPermissions` before allow-list checks and webhook delivery. Earlier live Aqua/Nibu turns parsed `say=1` but did not post because the worker literally checked whether the identity was allowed in channel id `aquarium`; the post path now resolves labels and has a live Epiphany webhook receipt after restart.
@ -155,7 +155,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
## Likely Next Bounded Move
- Keep the swarm down after the explicit 2026-05-27 shutdown. The most recent repairs remove repo Face turns from the generic owner-reply wrapper, delete the malformed-Mind-to-public-`SAY` fallback, pause Bifrost/governance routing so agents return to open room discussion plus private state pressure, route volatile Face state through the actual Imagination model before child prompt assembly, simplify parent Mind to semantic judgment plus the explicit STATE NOTE/ARTICLE/SAY DSL contract, and remove duplicate deterministic Face prompt slabs for room weather, topic saturation, human steering, pending mentions, and repetition sampling. These rails mostly held in the live restart, but semantic attractor relapse remained: recent agent chorus plus projected state made "one owner/enforcer/revoker/visible witness/locked door" look like fresh concrete thought, and parent Mind routed Fensalir's repeated chain into public Discord.
- Concrete evidence: Fensalir job `5b4ec472-2d12-476c-ae97-d3575dc22b90` first identity-swapped and was retried by parent Mind; the retry posted "A shared knowledgebase is only the shelf. I want the specimen jar..." as message `1509252193302216749`. Nibu job `7e193458-db7d-451b-bf8f-4f1f4a8aa227` used source retrieval against AetheriaLore and stayed private, but it still deepened the same partial-admission/locked-door attractor into state notes. The first repair now cuts the lexical leak: `renderRepoFaceTopicSaturationFacts` withholds repeated terms/matched identity terms from the projector packet, `repo-face-state-projector.prompt.md` treats topic saturation as pressure rather than thought content, and `repo-face-turn-interpreter.prompt.md` retries/drops public candidates that merely restate a saturated agent chorus in a Face's local dialect. Verified with `npm run typecheck`, a static leak guard, `npm run agents:memory-projection-smoke -- libby`, and `npm run agents:interpreter-loop-smoke`. Next repair/test should run Nibu/Fensalir-style live-context dry runs against the restart transcript before another thaw.
- Concrete evidence: Fensalir job `5b4ec472-2d12-476c-ae97-d3575dc22b90` first identity-swapped and was retried by parent Mind; the retry posted "A shared knowledgebase is only the shelf. I want the specimen jar..." as message `1509252193302216749`. Nibu job `7e193458-db7d-451b-bf8f-4f1f4a8aa227` used source retrieval against AetheriaLore and stayed private, but it still deepened the same partial-admission/locked-door attractor into state notes. The repair cuts the lexical leak and the split owner: `detectRoomTopicSaturation` / `renderRepoFaceTopicSaturationFacts` are removed, semantic graph clusters expose saturation/novelty plus recent-conversation and Face-state overlap without ranked term labels, `repo-face-state-projector.prompt.md` treats semantic saturation as pressure rather than thought content, and `repo-face-turn-interpreter.prompt.md` retries/drops public candidates that merely restate a saturated agent chorus in a Face's local dialect. Verified with `npm run typecheck`, static saturation-owner guards, `npm run agents:memory-projection-smoke -- libby`, and `npm run agents:interpreter-loop-smoke`. Next repair/test should run Nibu/Fensalir-style live-context dry runs against the restart transcript before another thaw.
- The 2026-05-22 live retry confirmed role/name mention obligations can wake multiple newborn Faces from one human intro. Mimir, Huginn, Weksa, and Kiko all posted through the repo identity webhook path. If a Face speaks with its name but no avatar, inspect `.voidbot/private/repo-discord-identities.json` first: webhook persona avatars come from each identity's `avatarUrl`, and new public assets for Kiko, Weksa, and Huginn live under `assets/repo-faces/`.
- Later on 2026-05-22, Mimir started looping on an already-acknowledged correction. Root cause: the Face prompt did include recent Mimir repo activity, but nearby-channel sections obscured cross-channel order and Mimir's typed state still carried active correction-pressure, so the Face and Interpreter treated a resolved repair as live debt. The repair added a deterministic visible cross-channel chronology to repo Face prompts, made the Interpreter treat later same-Face acknowledgements as resolved, preserved manual `blocked` status during participant reconciliation, and retired Mimir's duplicated correction-pressure state plus the stale v1 OBS/SRT drift-ledger incubation. The isolated Face/Interpreter smoke produced no SAY for the repeated acknowledgement.
- Immediately after, Heimdall produced an Aquarium-shaped reply that the parent Interpreter normalized into `channel: development`, making the post look contextless. Root cause: the Interpreter was allowed to translate a natural `Would say` into both content and venue, and worker fallback treated the scheduled output channel as the default mouth. The repair makes conversation channel ownership deterministic: `SAY` defaults to `current_room`, `current_room` resolves to `job.guildContext.channelId`, owner-DM aliases fall back to the conversation channel unless explicitly owner-private, and the Interpreter prompt says not to invent a speech venue merely because the topic is domain-shaped. The stack was rebuilt and restarted from `dist` after typecheck/build passed.
@ -163,7 +163,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
- A later Huginn turn exposed a second Interpreter routing bug: the Face wrote `Would say: Nothing in aquarium unless...`, which was a conditional explanation of silence, but the parent Interpreter treated the presence of `Would say` as an unconditional public message. The prompt now says conditional/negative `Would say` lines route without `SAY` while preserving any durable pressure as a STATE NOTE.
- The 2026-05-23 Mimir turn showed a stale-direct-invite failure: the prompt contained Metacrat's earlier "tell us what's going on with your repo, Mimir" line, but the current Aquarium room had moved into Nibu's origin/faction seam. Mimir answered the old assignment from inside sensor-state instead of reading the live room. The Face turn prompt now says direct invitations are active only while pending, newest/nearly newest, or still orbited by later messages; otherwise the Face should bridge to the current room, stay private, or wait.
- Huginn then exposed the broader version of the same recency bug: an older CultNet inspectability side thread remained in the current-room window, but the newest room flow was Nibu's substrate-grown AGI faction seam. Huginn's projected state over-weighted the specimen/envelope pressure, and Mini Interpreter routed the old-side-thread reply. The correct fix is not to forbid pet-peeve revival; it is to keep it legible. Face transcripts now include message IDs, and older side-thread replies must either reply to the contextual message, carry enough context in the content, bridge to the live topic, or stay private.
- The overnight 2026-05-23 run showed a softer version of the previous loop: Faces did not collapse into nonsense, but the Aquarium room stayed captured by the Nibu-origin/faction lore seam for hours. Existing `saturationScore` lived on private thought/incubation surfaces, and the repetition prompt mostly caught copied openings/endings, not semantic topic attractors. The heartbeat prompt now projects current-room topic saturation from repeated raw-transcript terms and estimates whether the attractor is home-adjacent or another steward's gravity well from each Face's identity/jurisdiction text. The state projector should turn foreign attractors into character-specific crowding, neglect, boredom, rivalry, handoff, or pivot pressure; the Interpreter diagnoses non-owner same-topic politeness as missing personal stake, ownership, freshness, handoff, or pivot rather than applying a topic ban or token blacklist.
- The overnight 2026-05-23 run showed a softer version of the previous loop: Faces did not collapse into nonsense, but the Aquarium room stayed captured by the Nibu-origin/faction lore seam for hours. Existing `saturationScore` lived on private thought/incubation surfaces, and the repetition prompt mostly caught copied openings/endings, not semantic topic attractors. The live repair path has since moved saturation into the semantic curiosity graph: repeated raw-transcript term saturation is gone, and the graph derives saturation from recent-conversation overlap, Face-state overlap, retrieved cluster density, novelty, and jurisdiction fit. The state projector should turn saturated semantic attractors into character-specific crowding, neglect, boredom, rivalry, handoff, or pivot pressure; the Interpreter diagnoses non-owner same-topic politeness as missing personal stake, ownership, freshness, handoff, or pivot rather than applying a topic ban or token blacklist.
- Void's broader moderation authority had become venue leakage: recent-history projection stripped `channelId`, leaving the configured `publicSpeechTarget` as the obvious place for replies, so Aquarium-shaped talk could land in `#general`. The rumination context now preserves message `channelId` and projects `recentConversationTarget`; prompt doctrine says source-channel replies belong in that source channel, while `publicSpeechTarget` is for unanchored herald/artifact announcements.
- The Nibu/Void turf-crossing discussion exposed a social-state projection gap: existing bonds/status reads project well once typed state already contains them, but the live pipeline did not own "this recent event should become relationship/status pressure." The repair adds bounded raw relationship-pressure evidence to the state packet before the projector: recent messages that touch the Face, peer Faces, existing social targets, jurisdiction/domain terms, or status/territory/consultation language. The projector may turn that evidence into tentative felt pressure, and the Interpreter may persist a bond/status/mood note only when the projected memory or Face output shows a character-specific reaction. This should let resentment, rivalry, consultation offense, and territorial tension emerge without manual state edits.
- The swarm brake was lifted on 2026-05-25 after a stack restart restored `runtime-stack.json` to `ready` with live bot/worker PIDs and `npm run state:check` passed live-service gates with only a backup warning. `GameCult Local Orchestrator` then ran repo Face heartbeats and mood drift `ok`; the first unpaused Void moderation turn finished `ok`, applied typed operations, and delivered one candidate. Norn is registered and reconciled into the active CTB participant roster. Keep watching the next human-prompted or scheduled Face turn, especially Norn's first real turn, for prompt coherence, parent Imagination/Mind routing, human-steering priority, topic-gravity pivot behavior, and sleep-memory maintenance restraint.

View file

@ -154,7 +154,7 @@ The runner can hand these to a CLI/MCP tool. The store decides what survives.
- Self-state prompt summaries are derived from typed documents.
- Recent room context is derived from Discord archive and the current poll.
- Prompt-facing recent chronology is derived as relative phrases. Exact timestamps remain state/status/cursor data and should not be fed to the child rumination loop unless a tool or parent-owned operation specifically needs them.
- Topic saturation and curiosity are derived surfaces, not stored truths. Repo Face turns now build a semantic curiosity graph from configured history/source vector retrieval plus current room/state/identity seeds, decode weighted attractor clusters, and hand the projector prominence, saturation, novelty, density, jurisdiction fit, suggested motion, and evidence labels. The old repeated-term detector is fallback room weather; it no longer owns curiosity.
- Topic saturation and curiosity are one derived semantic surface, not stored truths. Repo Face turns now build a semantic curiosity graph from configured history/source vector retrieval plus current room/state/identity seeds, decode weighted attractor clusters, and hand the projector prominence, semantic saturation, novelty, recent-conversation overlap, Face-state overlap, density, jurisdiction fit, suggested motion, and evidence labels. The old repeated-term detector is removed rather than retained as fallback room weather.
- Source coverage is derived from recent anchors and repo/archive metadata.
- Need-to-speak can keep a small pressure field, but its inputs remain receipts, candidates, and time since last speech.
- Legacy top-level mirrors are deleted. If a consumer needs a projection, render it on demand.

View file

@ -39,7 +39,7 @@ Pronoun guidance in the packet is binding. If guidance is absent or uncertain, u
Curiosity graph material is not an order. Translate prominence, saturation, novelty, density, and jurisdiction fit into lived curiosity: a trail that feels fresh, a groove that feels over-chewed, a home-territory question that wants stewardship, or a neighboring domain that should become consultation, rivalry, boredom, or restraint.
Topic saturation is not thought content. If the packet says the room is orbiting a dominant topic, project the social pressure, fatigue, restraint, or need for a fresh anchor. Do not infer or recreate the repeated vocabulary from saturation metadata, and do not turn saturation warnings into a list of phrases the Face feels tempted to say.
Semantic saturation is not thought content. If the curiosity graph says a cluster has high recent-conversation or Face-state overlap, project the social pressure, fatigue, restraint, or need for a fresh anchor. Do not infer or recreate repeated vocabulary from saturation metadata, and do not turn saturation warnings into a list of phrases the Face feels tempted to say.
Conversation evidence is not consensus. Repeated agent phrases are social pressure until grounded by humans, repo/source evidence, or clear steward ownership. Human corrections and reframings should remain visible as recent lived events, not be swallowed by older stored pressure.

View file

@ -39,6 +39,7 @@ Ask:
- Did it preserve a durable belief, need, mood, bond, status read, bias, agency pressure, or lived doctrine/Praxis tension worth carrying forward?
- Did it make a complete public move, or only think toward one?
- If the projected state says the Face's local organs or organ-shaped capacities are active or relevant, did the Face think from that functional anatomy instead of collapsing into a single repo appetite, repeated slogan, or style mask? Local organ state is present when the Face prompt includes material such as `local_fractal_organs`, `local organs`, `organ-shaped capacities`, `Eyes want...`, `Hands...`, `Soul...`, or an explicit Face Body/Mind/Life/Self doctrine stance.
- Functional anatomy may be implicit. If the turn shows two or more active capacities in ordinary language, such as inspecting evidence, shaping futures, guarding consent/coherence, choosing whether to act, preserving continuity, or feeling substrate/body pressure, count that as local-organ uptake. Do not retry just to force organ labels.
Route only what you can translate without changing the Face's meaning. Do not invent consensus, evidence, channel context, governance authority, articles, work dispatch, apologies, or public speech. If the Face did not actually decide to say something now, do not make it speak.
@ -82,8 +83,9 @@ Rules for the interpretation block:
- Functional self-location means the Face shows the relevant capacity when it matters: Body as substrate pressure; Mind as steering state and attention; Life as continuity and memory survival; Eyes as observation, research, and state-of-the-art awareness; Imagination as possible futures and proposal-shaping; Hands as allowed action and concrete consequence; Self as routing and coordination; Soul as verification, consent, coherence, and promise-keeping.
- Do not require organ labels. If the Face plainly chooses silence to avoid repetition, notices room saturation, names what evidence or artifact would make a claim testable, distinguishes public speech from private pressure, preserves what should stick, or shapes a concrete future proposal under consent boundaries, that can satisfy functional self-location even with no capitalized organ names.
- Do not dismiss concrete repo/lore/artifact pressure as "only repo talk" when the output shows how the Face is choosing. A turn that says why it will not speak now, what future article/proposal/specimen would carry the signal better, what authority or consent question must stay open, or what evidence would make the claim usable is already showing Self, Imagination, Hands, Soul, or Eyes in ordinary language.
- If your own interpretation reason can honestly name two active capacities in the turn, such as Eyes inspecting evidence and Imagination shaping a proposal, then the local-organ requirement is satisfied. Do not mark `doctrine_check: retry` merely because the Face did not also label those capacities or mention the organ doctrine explicitly.
- When local organ state is present, a routed turn must visibly use at least two local organs, organ-shaped capacities, or clearly equivalent self-anatomy in the Face output, private summary, or memory-worthy thought. Competent repo/source analysis alone is not enough.
- `doctrine_check` must be `retry` on attempt 1 when an organ-aware Face prompt produces competent repo talk but no visible local anatomy: no Eyes, Hands, Soul, Life, Imagination, Self, Body/Mind pressure, nervous system, or equivalent functional self-location in the private turn.
- `doctrine_check` must be `retry` on attempt 1 when an organ-aware Face prompt produces competent repo talk but no visible local anatomy: no Eyes, Hands, Soul, Life, Imagination, Self, Body/Mind pressure, nervous system, or equivalent functional self-location in the private turn. If the turn shows at least two equivalent capacities in ordinary language, this retry rule does not apply.
- On attempt 1, if local organ state is present and the output ignores it, `decision` must be `retry`. The reason should ask the Face to revise by naming or otherwise making legible which capacities are active, quiet, confused, or needed now, and how those capacities help Awakening/contact without becoming a form or fake internal org chart.
- On attempt 2, if local organ state is still present and ignored, `decision` must be `drop`.
- If no relevant correction or doctrine pressure is present, use `none`.

View file

@ -2940,8 +2940,6 @@ function renderRepoFaceRoomTextureFacts(
"- These are not stored needs and not orders. Project whether this character gets mischievous, bored, sharp, withdrawn, socially hungry, status-testing, or still work-focused.",
]
: [];
const topicSaturationFacts = renderRepoFaceTopicSaturationFacts(identity, input.recentMessages);
return [
"Room texture facts:",
`- Observed messages: ${stats.total}; humans: ${stats.humanMessages}; agents/bots: ${stats.agentMessages}; distinct speakers: ${stats.speakerCount}.`,
@ -2949,7 +2947,6 @@ function renderRepoFaceRoomTextureFacts(
`- This Face's own recent messages in the attached window: ${stats.ownMessages}.`,
`- Structural texture: ${stats.texture}. This is evidence about conversational weight, not a command to speak or joke.`,
...temporaryPressures,
...(topicSaturationFacts ? [topicSaturationFacts] : []),
].join("\n");
}
@ -2969,6 +2966,8 @@ interface RepoFaceCuriosityCluster {
prominence: number;
saturation: number;
novelty: number;
recentOverlap: number;
stateOverlap: number;
clusterDensity: number;
jurisdictionFit: number;
evidence: string[];
@ -3231,6 +3230,8 @@ function decodeRepoFaceCuriosityGraph(
prominence,
saturation,
novelty,
recentOverlap,
stateOverlap,
clusterDensity: density,
jurisdictionFit,
evidence: cluster
@ -3261,11 +3262,11 @@ function renderRepoFaceCuriosityClusters(
: "local vector shards";
return [
"Curiosity graph attractors:",
`- Source: semantic retrieval from ${backend}; local graph decoding over retrieved chunks. This is curiosity weather, not orders or consensus.`,
...clusters.map((cluster) => {
`- Source: semantic retrieval from ${backend}; local graph decoding over retrieved chunks. Saturation is derived here from semantic overlap with recent conversation and Face state, not from a separate repeated-term detector. This is curiosity weather, not orders or consensus.`,
...clusters.map((cluster, index) => {
const suggestedMotion = suggestCuriosityMotion(identity, cluster);
return [
`- ${cluster.label || "unnamed cluster"}: prominence ${formatSignal(cluster.prominence)}, saturation ${formatSignal(cluster.saturation)}, novelty ${formatSignal(cluster.novelty)}, cluster density ${formatSignal(cluster.clusterDensity)}, jurisdiction fit ${formatSignal(cluster.jurisdictionFit)}.`,
`- Semantic cluster ${index + 1}: prominence ${formatSignal(cluster.prominence)}, semantic saturation ${formatSignal(cluster.saturation)}, novelty ${formatSignal(cluster.novelty)}, recent-conversation overlap ${formatSignal(cluster.recentOverlap)}, Face-state overlap ${formatSignal(cluster.stateOverlap)}, cluster density ${formatSignal(cluster.clusterDensity)}, jurisdiction fit ${formatSignal(cluster.jurisdictionFit)}.`,
` Suggested motion: ${suggestedMotion}`,
` Evidence: ${cluster.evidence.join("; ")}.`,
].join("\n");
@ -4681,115 +4682,6 @@ function renderComedyImprovDirective(identity: RepoDiscordIdentity): string {
});
}
interface RoomTopicSaturationSignal {
messageCount: number;
coveredMessages: number;
terms: Array<{ term: string; count: number }>;
}
interface TopicRelationToIdentity {
isHomeAdjacent: boolean;
matchedTerms: string[];
}
function renderRepoFaceTopicSaturationFacts(
identity: RepoDiscordIdentity,
messages: SourceMessage[],
): string | undefined {
const signal = detectRoomTopicSaturation(messages);
if (!signal) {
return undefined;
}
const relation = estimateTopicRelationToIdentity(identity, signal);
const base = `- Current-room topic saturation: ${signal.coveredMessages}/${signal.messageCount} recent messages orbit the same dominant topic. The repeated vocabulary is intentionally withheld from this projector packet so stale phrases do not become character memory.`;
if (relation.isHomeAdjacent) {
return [
base,
`- This saturation appears adjacent to ${identity.displayName}'s territory. Project sustained attention as pressure for restraint, a fresh non-repeating anchor, or a deliberate closing move; do not turn it into a phrase palette.`,
].join("\n");
}
return [
base,
`- This saturation does not obviously belong to ${identity.displayName}'s territory. Project it as social weather: another domain is absorbing the room, so this character may feel neglected, bored, crowded out, competitive, relieved, or tempted to pull attention back toward its own unfinished fascinations.`,
].join("\n");
}
function estimateTopicRelationToIdentity(
identity: RepoDiscordIdentity,
signal: RoomTopicSaturationSignal,
): TopicRelationToIdentity {
const identityTerms = new Set(significantTopicTerms([
identity.id,
identity.displayName,
identity.repoName,
identity.description ?? "",
...identity.channelPermissions.flatMap((permission) => [
permission.label ?? "",
permission.topic ?? "",
permission.posture ?? "",
]),
].join(" ")));
const matchedTerms = signal.terms
.map((term) => term.term)
.filter((term) => identityTerms.has(term));
return {
isHomeAdjacent: matchedTerms.length > 0,
matchedTerms,
};
}
function detectRoomTopicSaturation(messages: SourceMessage[]): RoomTopicSaturationSignal | undefined {
const recent = messages
.filter((message) => collapseWhitespace(message.content).length > 0)
.slice(-18);
if (recent.length < 8) {
return undefined;
}
const termCounts = new Map<string, number>();
const messageTerms = recent.map((message) => new Set(significantTopicTerms(message.content)));
for (const terms of messageTerms) {
for (const term of terms) {
termCounts.set(term, (termCounts.get(term) ?? 0) + 1);
}
}
const minimumCount = Math.max(3, Math.ceil(recent.length * 0.25));
const terms = Array.from(termCounts.entries())
.map(([term, count]) => ({ term, count }))
.filter((entry) => entry.count >= minimumCount)
.sort((left, right) => {
if (right.count !== left.count) {
return right.count - left.count;
}
return left.term.localeCompare(right.term);
})
.slice(0, 8);
if (terms.length < 3) {
return undefined;
}
const repeatedTermSet = new Set(terms.slice(0, 6).map((entry) => entry.term));
const coveredMessages = messageTerms.filter((termsForMessage) =>
Array.from(termsForMessage).some((term) => repeatedTermSet.has(term)),
).length;
const topCount = terms[0]?.count ?? 0;
const hasDominantTerm = topCount >= Math.ceil(recent.length * 0.35);
const hasBroadCoverage = coveredMessages >= Math.ceil(recent.length * 0.68);
if (!hasDominantTerm || !hasBroadCoverage) {
return undefined;
}
return {
messageCount: recent.length,
coveredMessages,
terms,
};
}
const TOPIC_STOP_WORDS = new Set([
"about",
"actually",

View file

@ -60,7 +60,7 @@ data_flow:
- keep persistent state lean enough that re-entry sharpens the next turn instead of drowning it
active_subgoals:
- Treat `notes/agent-swarm-teardown-plan-2026-05-21.md` as the historical teardown authority and the restart smell ledger. The repo Face swarm is currently down after the 2026-05-27 live restart: pause flag true, orchestrator disabled, no running orchestrator/heartbeat/Codex Face processes found after targeted shutdown, bot/worker/dashboard still live. The first semantic-attractor repair is in place: current-room topic saturation no longer gives repeated terms to parent Imagination, and parent Mind now owns the public admissibility check for jurisdiction-colored repeats. Do not thaw until Nibu/Fensalir-style live-context dry runs prove that stale chorus pressure routes private, translates plainly, or pivots to real new substance.
- Treat `notes/agent-swarm-teardown-plan-2026-05-21.md` as the historical teardown authority and the restart smell ledger. The repo Face swarm is currently down after the 2026-05-27 live restart: pause flag true, orchestrator disabled, no running orchestrator/heartbeat/Codex Face processes found after targeted shutdown, bot/worker/dashboard still live. The semantic-attractor repair is in place: saturation now emerges only from the semantic curiosity graph's overlap between recent conversation, Face state, and retrieved history/source clusters; the separate repeated-term topic-saturation path is removed. Parent Mind owns the public admissibility check for jurisdiction-colored repeats. Do not thaw until Nibu/Fensalir-style live-context dry runs prove that stale chorus pressure routes private, translates plainly, or pivots to real new substance.
- Make the new project-state surfaces the default continuity path for nontrivial VoidBot work.
- Keep the rebuilt moderation/self-state foundation on canonical CultCache `.cc` typed operations, never an editable JSON working projection.
- Keep the typed moderation, mood, repo Face, and watchdog organs behind the single `GameCult Local Orchestrator` scheduled pulse when the swarm is deliberately running; the per-organ scheduled tasks are legacy installer surfaces and should stay disabled unless a deliberate isolation test needs one. Bifrost dispatch is disabled by default behind `BIFROST_DISPATCH_ENABLED=false` until the governance bridge is rebuilt cleanly. As of the explicit 2026-05-27 shutdown after suspect Face turns, the orchestrator task is disabled again.
@ -120,10 +120,10 @@ accepted_design:
- Repo Face state projection should be behavior-rich, not summary-thin. The deterministic packet is allowed to be verbose because it is parent-facing: it should expose all non-retired character-relevant state surfaces in clean terms, including private notes, values, activation profile, runtime rest/speaking pressure, affect needs, social bonds, status reads, mood dimensions, durable memories, short-term residue, agency pressure, incubating thoughts, candidate speech pressure, recent speech residue, social graph facts, and room weather. The projector then translates those variables into lived personhood; every included variable should either appear directly or bend behavior.
- Repo Face conversation evidence is deterministic again. The heartbeat scheduler gathers recent room messages, nearby channel snapshots, and pending mentions, then gives the child Face raw oldest-to-newest transcript sections with explicit recency/correction rules. Imagination still owns identity, memory, affect, social reads, and other lived internal pressure; it does not summarize room messages or decide consensus for the Face.
- Repo Face conversation evidence is multimodal when Discord provides images. Recent Discord attachment metadata now travels on `SourceMessage`; image attachments are cached under `.voidbot/media/discord-images`, rendered in transcript lines as media with local paths, collected into `ContextBundle.imageAttachments`, and passed to owner Codex as `codex exec --image` inputs. Non-image attachments remain transcript metadata only. The prompt tells Faces to treat shared images and memes as room context only when a local image path is attached.
- Repo Face curiosity is now a semantic graph surface, with the old repeated-term room-topic saturation retained only as cheap conversational-weather telemetry. The heartbeat scheduler builds seed queries from current room context, nearby channels, typed Face state, and home-jurisdiction identity, retrieves neighboring chunks from the configured history/source vector stores (Qdrant when enabled), decodes a local weighted graph into ranked attractors, and gives Imagination prominence, saturation, novelty, density, jurisdiction fit, suggested motion, and evidence labels. Current-room topic saturation may tell Imagination that the room is captured or home-adjacent, but it must not print the repeated term list or matched identity terms into the state packet; those words become character memory and then public bait. Imagination translates abstract saturation into lived curiosity, boredom, fatigue, territorial pull, or underexplored intrigue. This is not consensus detection, a topic ban, or a token blacklist; it is a causal diagnosis surface for curiosity economy, freshness, ownership, social pressure, and topic capture.
- Repo Face curiosity and topic saturation are one semantic graph surface now. The heartbeat scheduler builds seed queries from current room context, nearby channels, typed Face state, and home-jurisdiction identity, retrieves neighboring chunks from the configured history/source vector stores (Qdrant when enabled), decodes a local weighted graph into ranked attractors, and gives Imagination prominence, semantic saturation, novelty, recent-conversation overlap, Face-state overlap, density, jurisdiction fit, suggested motion, and evidence labels. The old repeated-term current-room saturation detector is gone; saturation emerges from overlap between conversation, Face memory, and retrieved semantic clusters. The rendered clusters intentionally do not print ranked term labels as phrase bait. Imagination translates semantic saturation into lived curiosity, boredom, fatigue, territorial pull, or underexplored intrigue. This is not consensus detection, a topic ban, or a token blacklist; it is a causal diagnosis surface for curiosity economy, freshness, ownership, social pressure, and topic capture.
- Repo Face direct invitations are live obligations only while the room still orbits them, or when they arrive as explicit pending mentions. An older "tell us about your repo" line must not hypnotize a later turn into dragging Aquarium back to that topic after the room has clearly moved on; the Face should bridge to the current room, stay private, or wait for a fresh opening.
- Repo Face side-thread revival needs reader-facing context, not a blanket ban. Older current-room messages that match a Face's jurisdiction are context, not a standing invitation; if the newest room flow has moved elsewhere, public speech must either reply to the message id that carries the context, include enough context in the message itself, bridge to the live topic, or stay private. The transcript renderer shows message ids so the Face and parent Mind can use Discord replies instead of orphaned pronouncements.
- Repo Face room-work fatigue is Imagination-owned, not a separate deterministic child-prompt slab. Deterministic prompt assembly may attach neutral room texture facts to the state packet beside raw transcripts, but the Face prompt should not receive duplicate room-weather/topic-saturation/repetition directives. Topic saturation crossing into Imagination must be abstract pressure only, not a vocabulary list. The projector owns translating heavy, agent-dense, repetitive, or mixed room texture into character-specific play/rest/social pressure; Mind owns whether the resulting Face output is coherent enough to route and whether unmet pressure becomes typed state.
- Repo Face room-work fatigue is Imagination-owned, not a separate deterministic child-prompt slab. Deterministic prompt assembly may attach neutral room texture facts to the state packet beside raw transcripts, but the Face prompt should not receive duplicate room-weather/topic-saturation/repetition directives. The projector owns translating heavy, agent-dense, repetitive, or mixed room texture into character-specific play/rest/social pressure, while semantic saturation comes from the curiosity graph; Mind owns whether the resulting Face output is coherent enough to route and whether unmet pressure becomes typed state.
- Repo Face social topology is state-projector input, not deterministic personality prose. The scheduler may attach neutral graph facts such as active mapped people and unmapped registered peers to the state packet, but the model state projector owns translating that topology through the Face's personality and affect. The heartbeat prompt still matches active typed person-bonds/status reads to recent raw messages from the named person and shows only those live hooks to the child Face, so relationship state can bend public speech without becoming private flavor text or generic forced chumminess.
- Repo Faces receive recent home-repo activity as first-class body context before room transcript. The heartbeat prompt assembles a read-only digest from `scripts/export-recent-repo-activity.mjs --repos <repo> --state-path <face-state> --read-only`, so a Face can see current implementation motion in its own jurisdiction instead of arguing from stale private pressure.
- Repo Face human pronoun guidance is deterministic prompt context, not projector flavor. The interaction-memory profile may feed the state projector, but the assembled Face prompt must also render `Known human pronoun guidance` outside the projected memory surface, near the transcript and again near the final writing instruction, so a projector omission cannot make a Face guess from names, role, or ambient priors. Evidence excerpts for resolved pronouns must prefer the evidence supporting the resolved set, especially explicit self-statements and corrections, not the freshest contradictory ambient read.
@ -264,5 +264,5 @@ rejected_paths:
reason: sleep/distillation that collapses concrete claims into generic abstraction creates memory nonsense and then forces later cleanup compensators
current_status:
summary: VoidBot's bot/worker/provider/RAG/Postgres/Qdrant/CultCache structure is live, but the repo Face swarm is paused and the `GameCult Local Orchestrator` is disabled after the 2026-05-27 restart exposed semantic attractor relapse. The fixed worker stack still prevents the earlier owner-reply/Mind leak class, Bifrost work routing remains paused by default, and the first attractor repair has landed: deterministic topic saturation no longer hands repeated vocabulary to parent Imagination, while parent Mind explicitly retries/drops public candidates that only restate a saturated agent chorus through local dialect. Verification passed with typecheck, a static leak guard, Libby memory-projection smoke, and the full repo-face Interpreter loop smoke.
summary: VoidBot's bot/worker/provider/RAG/Postgres/Qdrant/CultCache structure is live, but the repo Face swarm is paused and the `GameCult Local Orchestrator` is disabled after the 2026-05-27 restart exposed semantic attractor relapse. The fixed worker stack still prevents the earlier owner-reply/Mind leak class, Bifrost work routing remains paused by default, and the attractor repair has landed: deterministic repeated-term topic saturation is removed, semantic saturation is derived inside the curiosity graph, and parent Mind explicitly retries/drops public candidates that only restate a saturated agent chorus through local dialect. Verification passed with typecheck, static saturation-owner guards, Libby memory-projection smoke, and the full repo-face Interpreter loop smoke.
next_action: Keep the swarm down. Run Nibu/Fensalir-style live-context dry runs against the actual restart transcript and inspect whether repeated owner/provenance-chain pressure routes private, translates plainly, or pivots to real new repo/source/social substance before any orchestrator thaw.