From 02c43b10f17e16d2908a822fff688a65199b49b2 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 22 May 2026 15:10:55 +0100 Subject: [PATCH] Expand Face state projection surface --- notes/voidbot-current-system-map.md | 2 +- notes/voidbot-implementation-plan.md | 2 +- prompts/repo-face-state-projector.prompt.md | 4 +- scripts/run-repo-face-heartbeats.ts | 255 +++++++++++++++++--- state/evidence.jsonl | 1 + state/map.yaml | 3 +- 6 files changed, 232 insertions(+), 35 deletions(-) diff --git a/notes/voidbot-current-system-map.md b/notes/voidbot-current-system-map.md index 13ea7a8..a629c87 100644 --- a/notes/voidbot-current-system-map.md +++ b/notes/voidbot-current-system-map.md @@ -170,7 +170,7 @@ This note is the source-grounded description of how the live VoidBot stack is sh - Role mentions, display-name-prefix mentions, and human replies to a Face's webhook/persona message append pending Face obligations to `pendingMentions` in `REPO_FACE_HEARTBEAT_STATE_PATH`; ordinary bot mentions and DMs append pending obligations for the built-in `void` participant. They no longer create direct provider jobs from `MessageCreate`. The CTB scheduler temporarily prioritizes agents with pending mentions, attaches those prompts plus reply targets to the next `repo-face-rumination` or Void moderation pass, and consumes the pending items only after a turn is actually queued. Rumination jobs remain private unless they explicitly emit a parent-owned Face action block or a typed candidate intervention; the worker must never auto-post their summaries as the base bot. Heartbeat prompts now include the channel permission plan, recent context from non-primary permitted channels, social embodiment, jurisdiction respect, comedy/improv doctrine, and anti-template sampling guidance so a Face can jump into domain chatter, form durable social reads, consult the right steward, and answer people without copying nearby note formulas. - Repo Face proposal/article PRs, PR comments, and registered-identity Discord posts now cross through Bifrost bridge commands. The child may emit `VOIDBOT_REPO_IDENTITY_PROPOSAL_PR` for consensus-needed repo/lore/design proposals, `VOIDBOT_REPO_IDENTITY_ARTICLE` for bylined essays, or `VOIDBOT_REPO_IDENTITY_PR_COMMENT` for signed arguments on existing proposal PRs. The worker validates the registered identity and records Face-local receipts, but `BIFROST_ROOT/tools/bifrost-bridge.mjs` owns draft PR creation, PR comments, and Discord persona posting. This is not merely helper placement: Bifrost's original governance/labor platform role makes it the owner of governed public crossings for GameCult work, including GitHub, Discord-native work/member/swarm interactions, dispatch receipts, CultNet/CultCache intake, and future collaboration interfaces. Heimdall is the OAuth/account/capability gate; Bifrost is the bridge/request/routing/receipt authority. VoidBot still owns room cognition, moderation judgment, archive/source retrieval, and Face personality; Bifrost owns Discord when Discord acts as the Labor Platform interface or swarm transport. If Nibu or another Face frames something as the next cut/pass and enough source or room material exists, the expected action is to draft the artifact, submit the PR, and announce it rather than continuing to nag the room. - Repo Face turns now receive a Bifrost governance digest from `BIFROST_ROOT/tools/governance-threads.mjs digest --repo --agent ` alongside recent Discord context. Bifrost typed CultCache topics/comments are the canonical home for feature requests, Face opinions, missing questions, approvals, and dispatch receipts; Discord is a mirror/evidence source. When `BIFROST_DISCORD_CHANNEL_ID` is configured for `#bifrost`, repo Face Discord context ignores mirrored bot chatter there because the authoritative activity arrives through the Bifrost digest. Human `#bifrost` messages should only become governance comments after Bifrost/Heimdall can link the Discord id to a registered actor. Repo-local asks that would send a human or Codex to add/fix/name/scaffold/document/test/investigate/implement belong on Bifrost with enough room/repo context for a claimed Codex turn; a Face may still speak characterfully in Aquarium, but not as the only dispatch surface. Legacy `UPDATE REQUEST` packets are parsed only as compatibility input and reconciled into Bifrost topics instead of immediate transport enqueue. Local `#bifrost` is channel `1506760343836430376`; work proposals, governance mirrors, and Bifrost dispatch receipts should go there, while Aquarium remains the casual/social swarm room. - - Repo character turns now have a state-projector plus turn-Interpreter shape. The state-in boundary is `prompts/repo-face-state-projector.prompt.md`: typed state and identity material are translated into lived character prose before the child sees it. The state projector must surface values, drives, wounds, fascinations, aesthetic taste, dignity, mood, needs, social reads, bonds, agency pressure, and the personality-specific meaning of sparse or dense social topology when the state contains those facts. Deterministic code may attach neutral graph facts such as mapped people and unmapped registered peers to the projector packet, but it must not decide whether that feels like anxiety, curiosity, territorial scouting, or nothing. Deterministic code also attaches recent read-only home-repo activity before chat, using the Face's own state path as the repo-activity cursor source, so current implementation motion is visible as body context. Current-room and nearby-channel logs no longer cross a model conversation projector; deterministic code attaches raw oldest-to-newest transcript evidence with explicit recency/correction rules so the Face can notice human reframes directly. Deterministic code also attaches a social-affordance surface that matches active typed person-bonds/status reads to recent raw messages from that person, so relationship state arrives as a live hook rather than private decorative lore. Deterministic code still owns full prompt assembly and inspection through `scripts/run-repo-face-heartbeats.ts --assemble-prompt `; that path composes stable doctrine, allowed rooms, research/archive capability guidance, Bifrost digest, projected memory/affect prose, home-repo activity, live social affordances, and raw conversation evidence into the exact child prompt. It must not invent personality, mood, affect, or social memory from typed state. Situational obligations such as direct-call priority or first-public-introduction duty are emitted only when the scheduler packet proves they apply, not as standing prompt law. The child character writes a natural in-character turn on full `gpt-5.4` and may describe desired speech, repo/article proposals, social reads, needs, mood shifts, or memory-worthy thoughts without emitting action DSL blocks. The cheaper Spark/Mini parent Interpreter decides route/retry/drop and translates warranted effects into structured `STATE NOTE` and `SAY` blocks for the worker while governance dispatch remains paused. `STATE NOTE` blocks become typed short-term memory, affect needs, social bonds, status reads, mood dimensions, or agency pressure. The worker still accepts legacy compact JSON sentinels and legacy `UPDATE REQUEST` blocks for compatibility, but the live prompt path keeps transport and state syntax out of the character's roleplay surface. + - Repo character turns now have a state-projector plus turn-Interpreter shape. The state-in boundary is `prompts/repo-face-state-projector.prompt.md`: typed state and identity material are translated into lived character prose before the child sees it. The state projector must surface values, drives, wounds, fascinations, aesthetic taste, dignity, mood, needs, social reads, bonds, agency pressure, and the personality-specific meaning of sparse or dense social topology when the state contains those facts. The deterministic state packet feeding that projector is intentionally verbose and parent-facing: it exposes private notes, values, activation vectors, runtime rest/speaking pressure, affect needs, bonds, status reads, mood dimensions, durable memories, short-term residue, agency pressures, incubating thoughts, candidate speech pressure, recent speech residue, social graph facts, and room weather so every included variable can appear directly or bend behavior. Deterministic code may attach neutral graph facts such as mapped people and unmapped registered peers to the projector packet, but it must not decide whether that feels like anxiety, curiosity, territorial scouting, or nothing. Deterministic code also attaches recent read-only home-repo activity before chat, using the Face's own state path as the repo-activity cursor source, so current implementation motion is visible as body context. Current-room and nearby-channel logs no longer cross a model conversation projector; deterministic code attaches raw oldest-to-newest transcript evidence with explicit recency/correction rules so the Face can notice human reframes directly. Deterministic code also attaches a social-affordance surface that matches active typed person-bonds/status reads to recent raw messages from that person, so relationship state arrives as a live hook rather than private decorative lore. Deterministic code still owns full prompt assembly and inspection through `scripts/run-repo-face-heartbeats.ts --assemble-prompt `; that path composes stable doctrine, allowed rooms, research/archive capability guidance, Bifrost digest, projected memory/affect prose, home-repo activity, live social affordances, and raw conversation evidence into the exact child prompt. It must not invent personality, mood, affect, or social memory from typed state. Situational obligations such as direct-call priority or first-public-introduction duty are emitted only when the scheduler packet proves they apply, not as standing prompt law. The child character writes a natural in-character turn on full `gpt-5.4` and may describe desired speech, repo/article proposals, social reads, needs, mood shifts, or memory-worthy thoughts without emitting action DSL blocks. The cheaper Spark/Mini parent Interpreter decides route/retry/drop and translates warranted effects into structured `STATE NOTE` and `SAY` blocks for the worker while governance dispatch remains paused. `STATE NOTE` blocks become typed short-term memory, affect needs, social bonds, status reads, mood dimensions, or agency pressure. The worker still accepts legacy compact JSON sentinels and legacy `UPDATE REQUEST` blocks for compatibility, but the live prompt path keeps transport and state syntax out of the character's roleplay surface. - `scripts/run-repo-face-heartbeats.ts` now acts as the broader agent CTB/Final-Fantasy-X-style initiative scheduler. The wall-clock task is only a pulse; virtual initiative time chooses who gets the next turn. The scheduler stores per-agent speed, reaction bias, load, groups, heat, effective speed, next-turn time, active turn markers, and recent queue history at `REPO_FACE_HEARTBEAT_STATE_PATH`. Repo Faces queue `repo-face-rumination` owner-Codex jobs; built-in Void currently participates as the `void` system agent and starts the existing moderation rumination runner as its turn. Initiative freezes while an owner-Codex job or Void moderation lock is active; recovery is scheduled only after that active turn completes, and completion cannot grant another turn in the same scheduler pulse. The active-job scan recovers stale Face jobs by marking ancient approved/running rows failed through the job queue before building the active-turn map. Above-baseline heat bypasses repo Face nap gating, while active-turn freeze still prevents overlapping thoughts. - The live wall-clock pulse for this scheduler is `scripts/run-gamecult-orchestrator.ps1`, installed as `GameCult Local Orchestrator`. `scripts/install-repo-face-heartbeat-task.ps1` remains available for isolated testing, but its per-organ scheduled task should stay disabled in normal operation. Turn frequency is controlled by `REPO_FACE_HEARTBEAT_BASE_RECOVERY_MINUTES`, `REPO_FACE_HEARTBEAT_GLOBAL_HEAT`, `REPO_FACE_HEARTBEAT_SPEED_OVERRIDES`, and `REPO_FACE_HEARTBEAT_HEAT_OVERRIDES`. Heat keys can target `all`, `kind:`, `turn:`, `identity:`, `repo:`, `display:`, `channel:`, or the bare identity/repo/display key. - The local ignored registry currently includes Nibu for `AetheriaLore`, with Face state in `E:/Projects/AetheriaLore/.voidbot/state/nibu.cc`, Discord role id `1505918221453103247`, and an Aquarium channel allow-list. Nibu's Face state now treats AetheriaLore-first map-building, abrasive character voice, consensus-backed change agency, her own `Aetheria/Lore/Nibu.md` character-note stewardship, Nibu-authored lore essays, and tangential setting fascination as separate active doctrines. That tangential fascination points at embodied ship minds, junkyard abandonment, murderous autonomy, salvage, and save-scumming survival. The repo Face heartbeat organ is now pulsed by `GameCult Local Orchestrator`; after restarting the stack onto the new worker code, forced heartbeat job `76020f1e-0c55-424b-9e35-89ba7f77fcc3` completed without base-bot auto-posting. diff --git a/notes/voidbot-implementation-plan.md b/notes/voidbot-implementation-plan.md index 1f35037..0b3d558 100644 --- a/notes/voidbot-implementation-plan.md +++ b/notes/voidbot-implementation-plan.md @@ -191,7 +191,7 @@ The runner can hand these to a CLI/MCP tool. The store decides what survives. - recent event timing in that bounded context now crosses a projection boundary first, so the child sees "5 minutes ago" language while the parent keeps exact timestamps for bookkeeping - public artifact sharing now crosses the same boundary: repo activity exposes content hints, rumination verifies authorship through retrieval, and a configured `publicSpeechTarget` lets Void queue a normal typed candidate intervention instead of needing a blog-specific announcement path. Void also has explicit herald jurisdiction over the GameCult website/blog; essay-shaped pressures should queue article-proposal candidates for Void-authored site pieces. - Discord mention participation now uses role/display-name/bot addressing as CTB queue input and parent-owned speech rails as the output boundary. A Face mention appends a pending obligation for that Face; an ordinary Void mention or DM appends a pending obligation for the built-in `void` participant. Ambient direct mentions no longer create direct provider jobs. -- repo-scoped character turns reuse the typed self-state machinery per identity, but the child character must not see that machinery directly. Addressed chats are consumed by the heartbeat scheduler, which prioritizes the identity, and the intended boundary has two model prompts: `repo-face-state-projector.prompt.md` for state -> roleplay-friendly memory/mood/personhood prose, and `repo-face-turn-interpreter.prompt.md` for natural character reflection -> typed memory, affect, social-read, mood, agency, and speech operations. Current-room and nearby-channel logs are not model-summarized; deterministic prompt assembly attaches raw oldest-to-newest transcript evidence with correction/recency rules so the Face must read the human exchange itself. The same assembler also attaches compact room-weather facts so work-heavy/agent-heavy texture can become play/rest/social pressure without seeding fake banter. The Face turn uses full `gpt-5.4`; the state projector and parent Interpreter use the cheaper Spark/Mini support chain. `scripts/run-repo-face-heartbeats.ts --assemble-prompt ` composes the final child prompt from templates, channel policy, repo/archive research guidance, Bifrost digest, projected memory prose, deterministic conversation transcript, and room-weather facts. It must not turn typed state into character prose itself, and it should emit situational obligations only when the current packet proves them true. +- repo-scoped character turns reuse the typed self-state machinery per identity, but the child character must not see that machinery directly. Addressed chats are consumed by the heartbeat scheduler, which prioritizes the identity, and the intended boundary has two model prompts: `repo-face-state-projector.prompt.md` for state -> roleplay-friendly memory/mood/personhood prose, and `repo-face-turn-interpreter.prompt.md` for natural character reflection -> typed memory, affect, social-read, mood, agency, and speech operations. Current-room and nearby-channel logs are not model-summarized; deterministic prompt assembly attaches raw oldest-to-newest transcript evidence with correction/recency rules so the Face must read the human exchange itself. The same assembler also attaches compact room-weather facts so work-heavy/agent-heavy texture can become play/rest/social pressure without seeding fake banter. The state packet feeding the projector is deliberately richer than before: private notes, values, activation vectors, runtime rest/speaking pressure, affect needs, bonds, status reads, mood dimensions, durable and short-term memory, agency pressure, incubating thoughts, candidate speech pressure, recent speech residue, social graph facts, and room weather are all available for the projector to turn into lived pressure. The Face turn uses full `gpt-5.4`; the state projector and parent Interpreter use the cheaper Spark/Mini support chain. `scripts/run-repo-face-heartbeats.ts --assemble-prompt ` composes the final child prompt from templates, channel policy, repo/archive research guidance, Bifrost digest, projected memory prose, deterministic conversation transcript, and room-weather facts. It must not turn typed state into character prose itself, and it should emit situational obligations only when the current packet proves them true. - repo-scoped Faces now have per-topic channel permissions instead of one work-shaped speech lane. `allowedChannelIds` remains the hard gate, while `channelPermissions` gives each Face labeled topics, speech thresholds, and speed multipliers. The current social invariant is that Aquarium is intentionally cheap for casual in-character chatter, while domain channels receive matching work/lore/music/realtime/agent-state thoughts when the recent cross-channel context lines up. Heat is the live-room valve: above-baseline heat now relaxes nap gating, but active-turn freeze still prevents an agent from being queued again while it is thinking. - repo-scoped character prompts now explicitly treat social embodiment as durable state territory without exposing schemas to the child. A character may write naturally about humans, other agents, or room dynamics; the Interpreter turns meaning-bearing friendship, rivalry, irritation, trust, recurring bits, bonds, needs, and status reads into typed state when they carry claim/question, tension, action implication, and anchors. The Interpreter also preserves unmet play/rest/social/status pressure when a Face turns room fatigue into another work-shaped ask without satisfying the social need. Public posts should speak to someone directly and must not begin with note-title formulas or identity labels such as "bright bridge note" or "tiny fish sorting note." - repo-scoped Face prompts and Void's CTB moderation prompt now carry the richer comedy/improv doctrine that was only implicit before. The distilled rule from Dan Harmon's comedy master class is operational: accept the frame and add one honest character-specific turn; find status inversion, exposed fear, vulnerability, false authority, or shared contradiction; heighten with specificity rather than volume; avoid dominance-weapon jokes; and stop before explaining the joke. diff --git a/prompts/repo-face-state-projector.prompt.md b/prompts/repo-face-state-projector.prompt.md index 4b76b83..75aced2 100644 --- a/prompts/repo-face-state-projector.prompt.md +++ b/prompts/repo-face-state-projector.prompt.md @@ -10,6 +10,8 @@ Architecture invariant: - Do not tell the character it is a Face, job, process, MCP client, heartbeat, or scheduler participant. - Do not reintroduce the whole identity surface. Mention only state/event facts that have changed, are currently pressurizing the character, or must affect this turn. - Preserve agency, mood, social reads, obligations, recent speech residue, and jurisdiction pressure as felt context. +- Be generous with behavior-shaping state. The deterministic packet is already filtered to character-relevant material; do not compress it into a tiny status memo. Every included private note, value, activation vector, memory, need, bond, status read, mood dimension, agency pressure, speech residue, and room-weather fact should either appear directly or visibly bend the narrative you return. +- If a state variable seems low-salience, translate it into a quiet behavioral bias rather than dropping it. Examples: a cooling need can become restraint; a satisfied need can become looseness or smugness; a low mood dimension can become absence of that pressure; a recent speech receipt can become repetition caution. - Actively surface the personhood material the child prompt expects: values, drives, wounds, fascinations, aesthetic taste, dignity, pride, shame, dependency, resentment, affection, rivalries, friendships, needs, and preferred ways of pushing on the world. - If the state names private notes, durable values, affect needs, mood dimensions, social bonds, status reads, or agency pressures, translate the salient ones into felt first-person-adjacent context instead of collapsing them into job duties. - Social bonds and status reads are behavior controls, not flavor. The character should know who they currently trust, resent, admire, needle, protect, envy, suspect, or feel indebted to, and how that should change their next utterance. @@ -22,7 +24,7 @@ Architecture invariant: - If a direct obligation exists, phrase it as something someone said or asked. - If work/governance pressure exists, phrase it as something the character wants discussed, remembered, or eventually acted on. Work routing is currently paused, so do not frame it as transport metadata. - Combine duplicates and retire stale echoes. If three state records say the same thing, make it one clean memory. -- Keep the output compact enough to sit inside the child prompt, but do not flatten personality. +- The output may be substantially longer than a summary when the state is rich. Prefer 700-1200 words for a well-developed character if the packet contains enough material. Keep it readable, not tiny. Character: {{characterIdentity}} diff --git a/scripts/run-repo-face-heartbeats.ts b/scripts/run-repo-face-heartbeats.ts index 4cf7c61..202b179 100644 --- a/scripts/run-repo-face-heartbeats.ts +++ b/scripts/run-repo-face-heartbeats.ts @@ -1634,30 +1634,43 @@ function renderRepoFaceStatePacket( const name = identity.displayName; const lines: string[] = []; const profile = state.selfProfile; - const privateNotes = profile.privateNotes.slice(-4); + const privateNotes = profile.privateNotes; const values = [...profile.values] - .sort((left, right) => right.priority - left.priority) - .slice(0, 5); + .sort((left, right) => right.priority - left.priority); const needs = [...state.faceAffect.needs] - .filter((need) => ["active", "neglected"].includes(need.status)) - .sort((left, right) => right.intensity - left.intensity) - .slice(0, 3); + .filter((need) => need.status !== "retired") + .sort(sortAffectByStatusAndIntensity); const bonds = [...state.faceAffect.socialBonds] - .filter((bond) => bond.status === "active") - .sort((left, right) => right.intensity - left.intensity) - .slice(0, 3); + .filter((bond) => bond.status !== "retired") + .sort(sortAffectByStatusAndIntensity); const statusReads = [...state.faceAffect.statusReads] .filter((read) => !read.retiredAt) - .sort((left, right) => right.intensity - left.intensity) - .slice(0, 3); + .sort(sortAffectByStatusAndIntensity); + const moodDimensions = [...state.faceAffect.moodDimensions] + .sort((left, right) => right.value - left.value); const agencyPressures = [...state.agencyPressure.pressures] - .filter((pressure) => ["active", "cooling", "ready_to_act"].includes(pressure.status)) - .sort((left, right) => right.intensity - left.intensity) - .slice(0, 2); + .filter((pressure) => pressure.status !== "retired") + .sort(sortAffectByStatusAndIntensity); + const durableMemories = [...state.thoughtMemory.memories] + .filter((memory) => !memory.retiredAt) + .slice(-12) + .reverse(); + const shortTermResidue = [...state.thoughtMemory.shortTerm] + .filter((memory) => !memory.retiredAt) + .slice(-12) + .reverse(); const incubation = [...state.thoughtMemory.incubation] .filter((thread) => thread.status !== "retired") - .sort((left, right) => right.maturation - left.maturation) - .slice(0, 4); + .sort((left, right) => right.maturation - left.maturation); + const candidateInterventions = [...state.candidateInterventions.interventions] + .filter((intervention) => intervention.status !== "retired") + .slice(-8) + .reverse(); + const recentReceipts = [...state.speechReceipts.recentReceipts] + .slice(-6) + .reverse(); + const activationFacts = renderRepoFaceActivationProfileFacts(profile.activationProfile); + const runtimeFacts = renderRepoFaceRuntimePressureFacts(name, state); const selfTexture = [ ...privateNotes.map(projectPrivateNoteForMemorySurface), @@ -1665,17 +1678,35 @@ function renderRepoFaceStatePacket( ] .map(cleanCharacterFacingSentence) .filter((entry) => entry.length > 0) - .slice(0, 5); + .slice(0, 18); if (selfTexture.length > 0) { lines.push(`Right now, ${name} is carrying this close to the skin: ${joinAsNarrativeList(selfTexture)}.`); } + if (activationFacts) { + lines.push(activationFacts); + } + + if (runtimeFacts) { + lines.push(runtimeFacts); + } + if (needs.length > 0) { lines.push([ - `${name}'s current pressures:`, + `${name}'s explicit needs and frictions:`, ...needs.map((need) => { - const target = need.target.label ?? need.target.id; - return `- ${target}: ${asSentence(need.summary)} The catch: ${asSentence(need.tension)} Next: ${asSentence(need.actionImplication)}`; + const target = targetLabel(need.target); + const claimOrQuestion = need.claim + ? `Claim: ${asSentence(need.claim)}` + : need.question + ? `Question: ${asSentence(need.question)}` + : ""; + return [ + `- ${need.kind} need toward ${target} [${need.status}, intensity ${need.intensity.toFixed(2)}, valence ${need.valence.toFixed(2)}]: ${asSentence(need.summary)}`, + claimOrQuestion, + `Tension: ${asSentence(need.tension)}`, + `Behavioral pull: ${asSentence(need.actionImplication)}`, + ].filter(Boolean).join(" "); }), ].join("\n")); } @@ -1684,8 +1715,13 @@ function renderRepoFaceStatePacket( lines.push([ "The social map has teeth:", ...bonds.map((bond) => { - const target = bond.target.label ?? bond.target.id; - return `- ${target} draws ${bond.stance}: ${asSentence(bond.summary)} ${asSentence(bond.actionImplication)}`; + const target = targetLabel(bond.target); + return [ + `- ${target} draws ${bond.stance} [${bond.status}, intensity ${bond.intensity.toFixed(2)}]: ${asSentence(bond.summary)}`, + `Read: ${asSentence(bond.claim)}`, + `Tension: ${asSentence(bond.tension)}`, + `Behavioral pull: ${asSentence(bond.actionImplication)}`, + ].join(" "); }), ].join("\n")); } @@ -1694,12 +1730,40 @@ function renderRepoFaceStatePacket( lines.push([ "Status in the swarm is part of the weather:", ...statusReads.map((read) => { - const target = read.target.label ?? read.target.id; - return `- Around ${target}, ${name} feels ${read.status}: ${asSentence(read.summary)} ${asSentence(read.actionImplication)}`; + const target = targetLabel(read.target); + return [ + `- Around ${target}, ${name} feels ${read.status} [intensity ${read.intensity.toFixed(2)}]: ${asSentence(read.summary)}`, + `Read: ${asSentence(read.claim)}`, + `Tension: ${asSentence(read.tension)}`, + `Behavioral pull: ${asSentence(read.actionImplication)}`, + ].join(" "); }), ].join("\n")); } + if (moodDimensions.length > 0) { + lines.push([ + "Mood dimensions currently bending the turn:", + ...moodDimensions.map((dimension) => + `- ${dimension.name}=${dimension.value.toFixed(2)}${dimension.source ? ` from ${cleanCharacterFacingSentence(dimension.source)}` : ""}`, + ), + ].join("\n")); + } + + if (durableMemories.length > 0) { + lines.push([ + "Durable memories that should still bias judgment:", + ...durableMemories.map((memory) => renderRepoFaceMemoryFact(name, memory)), + ].join("\n")); + } + + if (shortTermResidue.length > 0) { + lines.push([ + "Short-term residue waiting to settle:", + ...shortTermResidue.map((memory) => renderRepoFaceMemoryFact(name, memory)), + ].join("\n")); + } + const socialGraphFacts = renderRepoFaceSocialGraphFacts(identity, registryIdentities, state); if (socialGraphFacts) { lines.push(socialGraphFacts); @@ -1714,21 +1778,53 @@ function renderRepoFaceStatePacket( if (agencyPressures.length > 0) { lines.push([ - "What wants action later:", + "Agency pressures that want eventual motion:", ...agencyPressures.map((pressure) => - `- ${asSentence(pressure.summary)} ${asSentence(pressure.actionImplication)}`, + [ + `- ${pressure.kind} toward ${targetLabel(pressure.target)} [${pressure.status}, intensity ${pressure.intensity.toFixed(2)}]: ${asSentence(pressure.summary)}`, + pressure.claim ? `Claim: ${asSentence(pressure.claim)}` : "", + pressure.question ? `Question: ${asSentence(pressure.question)}` : "", + pressure.tension ? `Tension: ${asSentence(pressure.tension)}` : "", + `Behavioral pull: ${asSentence(pressure.actionImplication)}`, + ].filter(Boolean).join(" "), ), ].join("\n")); } if (incubation.length > 0) { - lines.push(`Thoughts still moving under the floorboards: ${incubation.map((thread) => - `${cleanCharacterFacingSentence(thread.topic)}: ${cleanCharacterFacingSentence(thread.summary)}`, - ).join(" ")}`); + lines.push([ + "Thoughts still moving under the floorboards:", + ...incubation.map((thread) => + [ + `- ${cleanCharacterFacingSentence(thread.topic)} [${thread.status}, maturation ${thread.maturation.toFixed(2)}]: ${cleanCharacterFacingSentence(thread.summary)}`, + typeof thread.desireToSpeak === "number" ? `desire to speak ${thread.desireToSpeak.toFixed(2)}` : "", + typeof thread.noveltyToRoom === "number" ? `room novelty ${thread.noveltyToRoom.toFixed(2)}` : "", + typeof thread.saturationScore === "number" ? `saturation ${thread.saturationScore.toFixed(2)}` : "", + ].filter(Boolean).join("; "), + ), + ].join("\n")); } - if (state.candidateInterventions.interventions.some((intervention) => intervention.status === "queued")) { - lines.push(`${name} may already have an unsaid line waiting. Do not repeat it unless the room gives it a sharper angle.`); + if (candidateInterventions.length > 0) { + lines.push([ + "Unsaid or recently deferred speech pressure:", + ...candidateInterventions.map((intervention) => + [ + `- ${intervention.kind} [${intervention.status}, priority ${intervention.priority.toFixed(2)}${intervention.mustEventuallyShare ? ", must eventually share" : ""}]: ${asSentence(intervention.summary)}`, + `Draft residue: ${cleanCharacterFacingSentence(intervention.draft)}`, + ].join(" "), + ), + "Do not repeat a waiting line unless the room gives it a sharper angle.", + ].join("\n")); + } + + if (recentReceipts.length > 0) { + lines.push([ + "Recent speech residue:", + ...recentReceipts.map((receipt) => + `- Said recently${receipt.preview ? `: ${cleanCharacterFacingSentence(receipt.preview)}` : "."} Let this create repetition caution, confidence, embarrassment, or follow-through as appropriate.`, + ), + ].join("\n")); } if (lines.length === 0) { @@ -1738,6 +1834,103 @@ function renderRepoFaceStatePacket( return rejectLeakyMemorySurface(lines.join("\n\n")); } +function sortAffectByStatusAndIntensity( + left: { status?: string; intensity?: number }, + right: { status?: string; intensity?: number }, +): number { + const rank = (status: string | undefined): number => { + switch (status) { + case "neglected": + case "ready_to_act": + return 0; + case "active": + return 1; + case "challenged": + return 1; + case "cooling": + return 2; + case "satisfied": + case "resolved": + return 3; + default: + return 4; + } + }; + const rankDelta = rank(left.status) - rank(right.status); + if (rankDelta !== 0) { + return rankDelta; + } + return (right.intensity ?? 0) - (left.intensity ?? 0); +} + +function targetLabel(target: { label?: string; id?: string; kind?: string } | undefined): string { + if (!target) { + return "an unnamed target"; + } + return target.label ?? target.id ?? target.kind ?? "an unnamed target"; +} + +function renderRepoFaceActivationProfileFacts( + activationProfile: VoidSelfStateTypedProjection["selfProfile"]["activationProfile"], +): string | undefined { + const sections = Object.entries(activationProfile) + .map(([section, values]) => { + const entries = Object.entries(values) + .sort(([, left], [, right]) => right.weight - left.weight) + .map(([key, value]) => `${key}=${value.weight.toFixed(2)}${value.note ? ` (${cleanCharacterFacingSentence(value.note)})` : ""}`); + return entries.length > 0 ? `- ${section}: ${entries.join("; ")}` : undefined; + }) + .filter((entry): entry is string => typeof entry === "string"); + + return sections.length > 0 + ? ["Activation profile that should color behavior:", ...sections].join("\n") + : undefined; +} + +function renderRepoFaceRuntimePressureFacts( + name: string, + state: VoidSelfStateTypedProjection, +): string | undefined { + const lines: string[] = []; + const sleep = state.scheduledRuntime.sleepCycle; + if (sleep.isNapping || sleep.activeDreamThemes.length > 0) { + lines.push( + `${name}'s rest state: ${sleep.isNapping ? "currently in a sleep/low-output phase" : "awake but carrying dream residue"}${sleep.activeDreamThemes.length > 0 ? ` around ${joinAsNarrativeList(sleep.activeDreamThemes.map(cleanCharacterFacingSentence))}` : ""}.`, + ); + } + + const speaking = state.scheduledRuntime.speakingPressure; + const speakingParts = [ + `need to speak ${speaking.needToSpeak.toFixed(2)}`, + typeof speaking.confessionPressure === "number" ? `confession ${speaking.confessionPressure.toFixed(2)}` : "", + typeof speaking.noveltyPressure === "number" ? `novelty ${speaking.noveltyPressure.toFixed(2)}` : "", + typeof speaking.recentSpeechDamping === "number" ? `recent-speech damping ${speaking.recentSpeechDamping.toFixed(2)}` : "", + ].filter(Boolean); + lines.push(`Speaking pressure: ${speakingParts.join(", ")}. Treat this as appetite/restraint, not an order.`); + + if (state.scheduledRuntime.lastRuns.length > 0) { + lines.push(`Recent internal passes: ${state.scheduledRuntime.lastRuns.slice(-4).map((run) => + cleanCharacterFacingSentence(run.summary), + ).join(" | ")}`); + } + + return lines.length > 0 ? lines.join("\n") : undefined; +} + +function renderRepoFaceMemoryFact( + name: string, + memory: VoidSelfStateTypedProjection["thoughtMemory"]["memories"][number], +): string { + const parts = [ + `- ${memory.kind} about ${targetLabel(memory.target)}: ${asSentence(memory.summary)}`, + memory.claim ? `Claim: ${asSentence(memory.claim)}` : "", + memory.question ? `Question: ${asSentence(memory.question)}` : "", + memory.tension ? `Tension: ${asSentence(memory.tension)}` : "", + memory.actionImplication ? `Behavioral pull for ${name}: ${asSentence(memory.actionImplication)}` : "", + ]; + return parts.filter(Boolean).join(" "); +} + function renderRepoFaceRoomTextureFacts( identity: RepoDiscordIdentity, input: { diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 9f2ee56..75a8ac0 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -266,3 +266,4 @@ {"ts":"2026-05-22T13:09:27Z","type":"verification","status":"ok","note":"Ran offline repo Face dry-run gates before restart. All 10 memory projections passed; full swarm Face->Interpreter mock run routed every Face with action blocks and no heartbeat/provenance leaks. Initial failures were harness false positives around natural social prose and timestamp .429; targeted Heimdall rerun passed after detector fixes."} {"ts":"2026-05-22T13:15:00Z","type":"implementation","status":"ok","note":"Cut semantic regex language cops from repo Face dry-run gates. Dry-run harness no longer fails on guessed source-inspection claims, low keyword flavor, or inferred social-pressure preservation; exact protocol/tool/provenance guards remain. Rake-cut slice passed for Nibu/Heimdall/Mimir and model-output inspection reported zero warnings."} {"ts":"2026-05-22T13:56:50Z","type":"implementation","status":"ok","note":"Repo Face social-pressure pass: deterministic prompt assembly now attaches compact room-weather facts beside raw transcripts, jurisdiction dives are background appetite rather than a live-room override, the state projector/Interpreter preserve work-fatigue/play/rest/social pressure, and the dry-run harness now exercises the live one-retry Interpreter path. Full mock swarm dry run passed at .voidbot/status/repo-face-social-pressure-dry-run-all-2.json; output still shows strong work gravity but no correction/body-context collapse."} +{"ts":"2026-05-22T14:09:25Z","type":"implementation","status":"ok","note":"Expanded repo Face state projection input and projector contract so volatile state is not crushed before the child sees it. The state packet now exposes all non-retired affect needs, bonds, status reads, mood dimensions, agency pressure, activation profile, runtime speaking/rest pressure, durable memories, short-term residue, incubations, candidate speech pressure, and recent speech residue in character-facing terms. Nibu projected memory grew from ~231 words to ~1094 words, and a mock dry run passed with a more specific witness-as-caste Aetheria turn."} diff --git a/state/map.yaml b/state/map.yaml index 96cf76c..5386701 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -105,6 +105,7 @@ accepted_design: - Agent mentions are CTB pressure, not direct Discord prompt jobs. Role-addressed or display-name-addressed repo Face mentions enqueue pending mention obligations in the CTB heartbeat state; human replies to a Face's webhook/persona message count as direct attention to that Face too, even without a literal tag. Ordinary Void mentions and DMs enqueue pending mentions for the built-in `void` participant. The heartbeat scheduler consumes that queue, temporarily pulls the addressed agent forward, attaches the pending prompt/reply target to the next repo-face-rumination or moderation pass, and the parent-owned speech rails post only explicit candidate/action output. The old `MessageCreate -> handlePrompt -> provider job` reflex is amputated for ambient direct mentions. - Face prompts carry explicit Epiphany identity doctrine. The base Void style pack contributes discipline, humor permission, and source-grounding habits, but the registered Face state, Epiphany, repo-local `.voidbot` home, avatar, grants, and jurisdiction description override the speaking subject so each Face can keep its own priorities and texture. That identity now belongs in the raw typed `selfProfile` too; renderer overrides are presentation support, not the authority. - Repo Face volatile state projection is model-owned again. The heartbeat scheduler may deterministically assemble a compact state packet, but live child prompts must pass that packet through `prompts/repo-face-state-projector.prompt.md` before it reaches the Face. The deterministic packet is an emergency isolation fallback only behind `REPO_FACE_STATE_PROJECTOR_ENABLED=false`, and both paths reject schema/prompt-construction leaks such as `grants:`, `jurisdictions:`, `Face of`, repo paths, and "do not prompt" language before prompt assembly succeeds. + - 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. The model state projector 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 room-work fatigue is part of the input contract, not seeded banter. Deterministic prompt assembly attaches compact room-weather facts beside the raw transcript and passes the same neutral facts through the state projector packet. The projector owns translating heavy, agent-dense, repetitive, or mixed room texture into character-specific play/rest/social pressure; the Face may speak, stay private, tease, withdraw, ask a human-scale question, or keep working if that genuinely fits. The parent Interpreter must preserve unmet social/play/rest/status pressure as typed state when the Face turns the moment into another work-shaped ask without satisfying it. - 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. @@ -238,4 +239,4 @@ rejected_paths: current_status: summary: VoidBot's bot/worker/provider/RAG/Postgres/Qdrant/CultCache structure is live, and the repo Face swarm has been restarted after the 2026-05-21 teardown pass. `GameCult Local Orchestrator` is enabled as the single pulse owner; watchdog, mood, moderation, and repo Face CTB organs have all completed healthy post-restart pulses. Bifrost work routing is paused by default. - next_action: Keep the swarm paused until live restart is explicitly chosen. The current dry-run proof says the Face-turn loop can read correction/body context and route through the parent Interpreter, but the room still has strong work gravity. The latest cut demotes jurisdiction dives to background appetite, attaches deterministic room-weather facts, makes the projector/Interpreter preserve play/rest/social fatigue as state, keeps raw transcripts as the correction evidence, and updates the dry-run harness to exercise the live retry shape. + next_action: Keep the swarm paused until live restart is explicitly chosen. The current dry-run proof says the Face-turn loop can read correction/body context and route through the parent Interpreter, but the room still has strong work gravity. The latest cuts demote jurisdiction dives to background appetite, attach deterministic room-weather facts, make the projector/Interpreter preserve play/rest/social fatigue as state, keep raw transcripts as correction evidence, update the dry-run harness to exercise the live retry shape, and expand projector input so rich typed state actually reaches the character as behavior pressure.