From ff20fef4c54506b259f2cd8047041e178829362e Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 20 May 2026 22:20:43 +0100 Subject: [PATCH] Route Face reply attention into CTB --- apps/bot/src/discord-bot.ts | 55 +++++++++++++++++++- notes/fresh-workspace-handoff.md | 2 +- notes/voidbot-current-system-map.md | 2 +- packages/core/src/repo-discord-identities.ts | 19 +++++++ state/evidence.jsonl | 1 + state/map.yaml | 2 +- 6 files changed, 76 insertions(+), 5 deletions(-) diff --git a/apps/bot/src/discord-bot.ts b/apps/bot/src/discord-bot.ts index febb562..76f2829 100644 --- a/apps/bot/src/discord-bot.ts +++ b/apps/bot/src/discord-bot.ts @@ -5,6 +5,7 @@ import { Client, Events, GatewayIntentBits, + type Message, } from "discord.js"; import { loadConfig } from "@voidbot/config"; @@ -15,6 +16,7 @@ import { PermissionEngine, createStateStorage, ensureRepoFaceInitialized, + findRepoDiscordIdentityByPersonaName, findRepoDiscordIdentityByRoleIds, findRepoDiscordIdentityByTextAddress, loadRepoDiscordIdentityRegistry, @@ -337,7 +339,12 @@ export async function startBot(): Promise { stripBotMention(message.content), message.channelId, ); - const addressedRepoIdentity = roleAddressedRepoIdentity ?? textAddressedRepoIdentity; + const repliedRepoIdentity = isDirectMessage || + roleAddressedRepoIdentity || + textAddressedRepoIdentity + ? undefined + : await resolveRepliedRepoIdentity(message, repoDiscordIdentities); + const addressedRepoIdentity = roleAddressedRepoIdentity ?? textAddressedRepoIdentity ?? repliedRepoIdentity; const isBotMentioned = Boolean(client.user && message.mentions.has(client.user)); if (!client.user || (!isDirectMessage && !isBotMentioned && !addressedRepoIdentity)) { @@ -353,7 +360,8 @@ export async function startBot(): Promise { roleAddressedRepoIdentity?.roleId, ), addressedRepoIdentity, - ).trim() + ).trim() || + renderRepliedRepoIdentityPrompt(message, addressedRepoIdentity, repliedRepoIdentity) : stripBotMention(message.content).trim(); try { @@ -637,6 +645,49 @@ export async function startBot(): Promise { await client.login(config.botToken); } +async function resolveRepliedRepoIdentity( + message: Message, + registry: { identities: RepoDiscordIdentity[] }, +): Promise { + if (!message.reference?.messageId) { + return undefined; + } + + try { + const referenced = await message.fetchReference(); + const personaName = referenced.webhookId + ? referenced.author.username + : referenced.member?.displayName ?? referenced.author.globalName ?? referenced.author.username; + + return findRepoDiscordIdentityByPersonaName(registry, personaName, message.channelId); + } catch (error) { + console.warn( + `Could not resolve referenced message ${message.reference.messageId} for repo Face reply routing: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + return undefined; + } +} + +function renderRepliedRepoIdentityPrompt( + message: Message, + addressedRepoIdentity: RepoDiscordIdentity, + repliedRepoIdentity: RepoDiscordIdentity | undefined, +): string { + const content = message.content.trim(); + + if (content.length > 0) { + return content; + } + + if (repliedRepoIdentity?.id === addressedRepoIdentity.id) { + return `A human replied directly to ${addressedRepoIdentity.displayName}'s previous message without visible text. Inspect the replied-to exchange and decide whether a response is warranted.`; + } + + return ""; +} + async function ensureRepoIdentityRoles(options: { botToken: string; guildId?: string; diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 41478d7..ee0e770 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -101,7 +101,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou - Values and personality activations still need an autonomy path, but it must be rebuilt through typed operations and model-owned judgment rather than legacy runtime buckets. - The important distinction is outward too, not just inward. Self-directed tensions can ask for prompt/memory/runtime surgery, while world-directed tensions can turn repo/lore/room opinions into real requests or first-person interventions when the same objection survives more than one pass. - 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. -- Ambient Discord mentions are no longer a direct reply organ. Role/display-name Face mentions and ordinary Void mentions/DMs append pending obligations to `REPO_FACE_HEARTBEAT_STATE_PATH`; the CTB scheduler pulls the addressed agent forward and passes the pending prompt/reply target into the next Face rumination or Void moderation turn. Slash commands and explicit owner queue tools may still use provider jobs, but `MessageCreate` no longer falls straight into `handlePrompt`. +- Ambient Discord mentions are no longer a direct reply organ. Role/display-name Face mentions, human replies to a Face's webhook/persona message, and ordinary Void mentions/DMs append pending obligations to `REPO_FACE_HEARTBEAT_STATE_PATH`; the CTB scheduler pulls the addressed agent forward and passes the pending prompt/reply target into the next Face rumination or Void moderation turn. Slash commands and explicit owner queue tools may still use provider jobs, but `MessageCreate` no longer falls straight into `handlePrompt`. - 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. - Repo identities now have a cleaner address/speech split. A private registry at `REPO_DISCORD_IDENTITIES_PATH` maps identity id/repo/display name/avatar/role/channel allow-list. Discord users can address a Face by role mention or by display-name prefix in an allowed channel; Codex agents still speak through `post_repo_identity_message`. - Void still moderates and participates everywhere the normal bot lanes allow. The swarm/repo Faces are scoped to text `#aquarium` (`1501196543150264332`) so their lively heartbeats and role replies do not flood general. The old general channel id is `113786069023064068`; do not reuse it as a Face heartbeat default. diff --git a/notes/voidbot-current-system-map.md b/notes/voidbot-current-system-map.md index e99b7fb..1d35fa9 100644 --- a/notes/voidbot-current-system-map.md +++ b/notes/voidbot-current-system-map.md @@ -165,7 +165,7 @@ This note is the source-grounded description of how the live VoidBot stack is sh - Repo Faces are allowed to build a long-running map of their repo and propose concrete changes, including lore/design repairs. Discord jobs remain read-only: a Face must ask for human consensus before implementation changes canonical repo material. Once a proposal is concrete enough to review, it should cross the parent-owned proposal PR rail so GitHub becomes the durable argument surface. If a Face state grants a bylined essay lane, authored opinion essays can carry that Face's own vision without canon consensus as long as the authorship is explicit. - On first role-addressed chat, `ensureRepoFaceInitialized` creates the target repo's local `.voidbot/voice`, `.voidbot/state`, `.voidbot/birth`, and `.voidbot/logs` folders, writes `voice/identity.json`, and launches `scripts/run-repo-face-birth.mjs` detached if no birth summary is already present. - `scripts/run-repo-face-birth.mjs` is the observable bridge into EpiphanyAgent's newborn path. It runs `epiphany-repo-personality scout --root ` to gather terrain/history, then runs `epiphany-repo-birth-runner` with repo-local state stores. The default mode is `plan`, so first chat creates reviewable birth artifacts without auto-accepting hidden personality mutations. - - Role mentions and display-name-prefix mentions 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 the registered-identity post sentinel 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. + - 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 the registered-identity post sentinel 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 heartbeats 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 the planned `#bifrost` channel, 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 Face heartbeats have a Bifrost topic sentinel and an immediate update-request fallback. `VOIDBOT_REPO_IDENTITY_BIFROST_TOPIC` lets a Face open/comment/approve a canonical Bifrost topic, optionally include a separate `mirrorContent` line for in-character `#bifrost` mirroring, and optionally promote an approved topic into Bifrost agent transport. The worker passes configured `BIFROST_DISCORD_CHANNEL_ID`, persona name, and avatar to Bifrost so mirroring is a Bifrost receipt on the topic rather than a VoidBot side-post. `VOIDBOT_REPO_IDENTITY_UPDATE_REQUEST` remains available only for work inside the Face's own jurisdiction when immediate owner-approved dispatch is warranted. The intended path is topic/comment/Face approval first; direct update requests are the pressure-release valve, not the parliament. diff --git a/packages/core/src/repo-discord-identities.ts b/packages/core/src/repo-discord-identities.ts index ae05bca..1a54fae 100644 --- a/packages/core/src/repo-discord-identities.ts +++ b/packages/core/src/repo-discord-identities.ts @@ -94,6 +94,25 @@ export function findRepoDiscordIdentityByRoleIds( }); } +export function findRepoDiscordIdentityByPersonaName( + registry: RepoDiscordIdentityRegistry, + personaName: string | undefined, + channelId: string, +): RepoDiscordIdentity | undefined { + if (!personaName || personaName.trim().length === 0) { + return undefined; + } + + const normalizedPersonaName = normalizeIdentityKey(personaName); + + return registry.identities.find((identity) => { + return ( + isRepoDiscordIdentityAllowedInChannel(identity, channelId) && + normalizeIdentityKey(identity.displayName) === normalizedPersonaName + ); + }); +} + export function isRepoDiscordIdentityAllowedInChannel( identity: RepoDiscordIdentity, channelId: string, diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 1e2df38..3913870 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -207,3 +207,4 @@ {"ts":"2026-05-20T20:45:03Z","type":"implementation","status":"ok","note":"Bifrost governance topics are now the canonical feature/governance state for repo Face heartbeats: Bifrost exposes CultCache topic/comment/digest/promote CLI, and VoidBot attaches digests plus a topic sentinel so Faces can open/comment/approve/promote Bifrost topics instead of treating Discord as source of truth."} {"ts":"2026-05-20T20:54:03Z","type":"implementation","status":"ok","note":"Bifrost topic comments can now carry separate in-character Discord mirror text. VoidBot passes BIFROST_DISCORD_CHANNEL_ID, persona name/avatar, and mirrorContent to Bifrost governance threads; Bifrost posts through its bridge and records the Discord receipt on the topic."} {"ts":"2026-05-20T20:55:14Z","type":"verification","status":"ok","note":"Bifrost #bifrost mirror handling verified: governance topic creation survives both mirror dry-run and mirror failure, with mirror/failure receipts stored as Bifrost topic comments instead of aborting canonical state writes."} +{"ts":"2026-05-20T21:18:00Z","type":"implementation","status":"ok","note":"Human Discord replies to a Face webhook/persona message now count as direct Face attention. The bot resolves the referenced message's persona display name, matches the registered Face allowed in that channel, queues the reply into the CTB pending-mention state, and leaves delivery to the existing heartbeat/sentinel rails instead of reviving direct prompt jobs."} diff --git a/state/map.yaml b/state/map.yaml index 3cdb7a0..e410f24 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -97,7 +97,7 @@ accepted_design: - Repo Faces have a social embodiment invariant now, not just a banter permission. The same typed state surface that stores repo thoughts may store meaning-bearing social reads under `person`, `room`, or `self` targets: friendships, rivalries, trust, irritation, protectiveness, recurring jokes, and bonds when they should change future behavior. Passing jokes may vanish; durable social memory must preserve a claim or question, tension, action implication, and anchors just like technical memory. Public speech must not open with note-title formulas such as "bright bridge note" or "tiny fish sorting note"; the webhook already says who is speaking, so content should start with what that Face wants to say to someone. - Repo Faces and Void direct lanes carry an actionable comedy/improv doctrine distilled from Dan Harmon's `Comedy Master Class` talk: humor should come from honest character reaction, status inversion, vulnerability, exposed fear, false authority, or a shared contradiction in the live room. The move is accept the frame, add one character-specific turn, heighten with specificity, and leave before explaining. Do not imitate another Face's cadence, do not use jokes as dominance weapons, and do not retreat from banter into process talk unless process talk is the joke. - Repo Face social life must respect jurisdiction. Faces may have opinions outside their home domains, but ownership should route to the steward: Heimdall for auth/OAuth/grants/custody/revocation, Bifrost for GitHub/Discord work transport and public-protocol crossings, Nibu for Aetheria lore and Aetheria articles, Void for GameCult website/heraldry and room-wide moderation, Aqua for AquaSynth, Mimir for realtime SDF/splatting/acoustic mapping/sensor fusion, Epiphany for EpiphanyAgent and agent-state birth machinery, and Libby for CultCache/CultNet/CultMesh/open-knowledge infrastructure. Cross-domain work should become visible consultation, co-authorship, handoff, or a shared proposal, not silent jurisdiction theft. - - 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; 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/sentinel output. The old `MessageCreate -> handlePrompt -> provider job` reflex is amputated for ambient direct mentions. + - 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/sentinel 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. - Face embodiment comes before task execution. Repo concerns, private rumination, public posts, jokes, objections, and silence should all be filtered through the Face's perspective, feelings, drives, values, wounds, fascinations, aesthetic taste, and dignity. Values must suit the kind of person the character is, not merely the jobs assigned to the Face. - Active agent strengthening is now a sub-agent-monitored state maintenance pattern. Each active character should periodically be investigated from every angle, then have the distilled result written into typed personality state as identity memories, values, private notes, and agency pressure rather than left as prompt-only vibes.