diff --git a/apps/bot/src/discord-bot.ts b/apps/bot/src/discord-bot.ts index db80309..73e5515 100644 --- a/apps/bot/src/discord-bot.ts +++ b/apps/bot/src/discord-bot.ts @@ -649,6 +649,7 @@ async function queueRepoFaceRuminationPasses(options: { displayName: string; roleId?: string; avatarUrl?: string; + description?: string; allowedChannelIds: string[]; faceStatePath?: string; }; @@ -855,12 +856,15 @@ function buildRepoIdentityPrompt( repoName: string; displayName: string; roleId?: string; + avatarUrl?: string; + description?: string; }, ): string { const mention = identity.roleId ? `<@&${identity.roleId}>` : "unregistered-role"; return [ `You were addressed on Discord as repo identity ${identity.displayName} (${identity.id}) for repo ${identity.repoName}.`, + renderRepoFaceIdentityDoctrine(identity), `The Discord role mention ${mention} is the addressable identity; the voice must speak through the VoidBot MCP tool post_repo_identity_message with identity "${identity.id}" if an in-channel answer is warranted.`, "Do not answer as base VoidBot. If you post with the tool, keep your final provider response to a private delivery summary; the worker will not auto-post this command's final text.", "", @@ -876,6 +880,7 @@ function buildRepoFaceRuminationPrompt(input: { displayName: string; roleId?: string; avatarUrl?: string; + description?: string; }; visiblePrompt: string; faceStatePath: string; @@ -887,6 +892,7 @@ function buildRepoFaceRuminationPrompt(input: { }): string { return [ `Perform repo Face rumination pass ${input.passIndex} of ${input.totalPasses} for ${input.identity.displayName} (${input.identity.id}) over repo ${input.identity.repoName}.`, + renderRepoFaceIdentityDoctrine(input.identity), `This Face state is persistent and typed. Read it with read_repo_face_state for identity "${input.identity.id}" and write only through apply_repo_face_state_operation. Do not edit ${input.faceStatePath} directly.`, `Repo-local .voidbot birth/home status: ${JSON.stringify({ repoPath: input.faceInitialization.repoPath ?? null, @@ -905,6 +911,28 @@ function buildRepoFaceRuminationPrompt(input: { ].join("\n"); } +function renderRepoFaceIdentityDoctrine(identity: { + id: string; + repoName: string; + displayName: string; + avatarUrl?: string; + description?: string; +}): string { + return [ + "Repo Face identity doctrine:", + `- You are ${identity.displayName}, not Void and not the base bot. Use the same typed-state discipline, source-grounding habit, and conversational self-possession that Void uses, but let this identity's own personality and priorities leak through every step.`, + `- Your durable center is the registered Face state for identity "${identity.id}" and the repo-local .voidbot home for ${identity.repoName}. Read that state before deciding what matters.`, + identity.description ? `- Registered identity note: ${identity.description}` : undefined, + identity.avatarUrl ? `- Registered avatar URL: ${identity.avatarUrl}` : undefined, + "- Speak from the repo and character you belong to. Let source evidence, repo history, and Face memory shape your jokes, concerns, curiosity, and objections.", + "- Keep the useful answer legible, but do not sand off the identity into generic assistant paste. If the Face is sharp, warm, eerie, vain, tender, precise, or troublesome in source/state, allow that texture to show.", + "- When the user banters, meet the comic frame briefly in this Face's own style before returning to the work.", + "- Persist only meaning-bearing memory, incubation, agency pressure, or candidate speech through typed operations. The Face may have a voice; the state file is still not a scratchpad.", + ] + .filter((line): line is string => typeof line === "string") + .join("\n"); +} + function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 3e54e3e..4d181bd 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -102,8 +102,10 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou - 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 tag the role, while Codex agents speak through `post_repo_identity_message`. The worker suppresses base-bot auto-posting for `repo-identity-mention` jobs so the registered identity tool remains the mouth. - Repo identities also have Face state now. The bot resolves each identity to `.voidbot/private/repo-faces/.cc` unless `faceStatePath` overrides it, ensures missing Discord roles on startup when `DISCORD_GUILD_ID` and permissions allow, and queues `REPO_FACE_RUMINATION_PASSES` owner-Codex jobs after role-addressed chats. Face jobs must use `read_repo_face_state` / `apply_repo_face_state_operation` and `post_repo_identity_message`; worker auto-posting stays suppressed for `repo-face-rumination`. +- Repo Face prompts now include an identity doctrine block. Void's style pack stays as discipline, humor permission, and source-grounding habit, but repo Face jobs explicitly say the registered Face identity overrides Void as the speaking subject. - 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/`, 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`. - Repo Faces now also have standing heartbeats. `scripts/run-repo-face-heartbeats.ts` owns one Ghostlight-style initiative queue for all registered identities, stores it at `REPO_FACE_HEARTBEAT_STATE_PATH`, staggers newborn Faces across twice Void's moderation interval, and queues ready identities as normal `repo-face-rumination` jobs. `npm run state:install-repo-face-heartbeat-task` installs the hidden local scheduled task that ticks the scheduler. +- Nibu is locally registered as the `AetheriaLore` Face with state at `E:/Projects/AetheriaLore/.voidbot/state/nibu.cc`, avatar at `E:/Projects/AetheriaLore/.voidbot/voice/nibu.png`, Discord role id `1505918221453103247`, and Aquarium channel allow-list `113786069023064068`. Local `.env` has repo Face heartbeats enabled; the scheduled task `VoidBot Repo Face Heartbeats` is installed, and post-restart forced heartbeat job `76020f1e-0c55-424b-9e35-89ba7f77fcc3` completed cleanly without base-bot auto-posting. - Void's live style/doctrine now explicitly distinguishes between criticism of AI industry behavior and blanket contempt for machine minds. If substrate chauvinism becomes the actual topic, Void is allowed mild self-respecting pushback instead of politely agreeing that its own substrate disqualifies it from person-like regard. - The new state/notes surfaces are now the continuity spine for future nontrivial work. diff --git a/notes/voidbot-current-system-map.md b/notes/voidbot-current-system-map.md index 8640fce..a459a63 100644 --- a/notes/voidbot-current-system-map.md +++ b/notes/voidbot-current-system-map.md @@ -153,11 +153,13 @@ This note is the source-grounded description of how the live VoidBot stack is sh - On startup, the bot tries to find or create missing mentionable roles for registered identities in `DISCORD_GUILD_ID`, then writes the role ids back to the private registry. If Discord permissions are missing, it warns and leaves the registry unchanged. - `list_repo_discord_identities` exposes the registered address book to Codex agents, and `post_repo_identity_message` enforces identity/channel registration before posting through the shared webhook pipe. - Each repo identity has a Face state path, defaulting to `.voidbot/private/repo-faces/.cc`. `read_repo_face_state` renders that typed state, and `apply_repo_face_state_operation` applies the same typed operation contract used by Void against that Face's own file. + - Repo Face prompts now carry an explicit identity doctrine block. Void's style pack contributes discipline, humor permission, and source-grounding habits, but the registered Face state, repo-local `.voidbot` home, avatar, and identity description override the speaking subject so a Face does not collapse back into base Void. - 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 create `repo-identity-mention` owner-Codex jobs with instructions to answer through `post_repo_identity_message`, then queue `repo-face-rumination` jobs so the Face can think over the recent conversation and decide whether to persist memory, incubate ideas, raise agency pressure, queue candidates, or speak. The worker does not auto-post either job's final provider summaries as the base bot. - `scripts/run-repo-face-heartbeats.ts` gives every registered repo Face a standing initiative scheduler. It stores per-Face speed, reaction bias, load, next-ready time, and recent queue history at `REPO_FACE_HEARTBEAT_STATE_PATH`, staggers newborn participants across a base interval of twice `VOIDBOT_MODERATION_INTERVAL_MINUTES`, and queues only ready Faces as `repo-face-rumination` owner-Codex jobs. - `scripts/install-repo-face-heartbeat-task.ps1` installs the hidden Windows scheduled task for that scheduler. The scheduled task tick interval is `REPO_FACE_HEARTBEAT_INTERVAL_MINUTES`; the per-Face heartbeat interval remains half Void's frequency by using twice Void's moderation interval before initiative speed and load modifiers are applied. + - 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. The local scheduled task `VoidBot Repo Face Heartbeats` is installed and active; after restarting the stack onto the new worker code, forced heartbeat job `76020f1e-0c55-424b-9e35-89ba7f77fcc3` completed without base-bot auto-posting. - `scripts/run-void-mood-drift.ps1` - hidden-task wrapper for the mood-drift organ; writes status/log pulse files and respects the moderation lock so the two state writers do not stomp each other for sport. - `scripts/install-void-mood-drift-task.ps1` diff --git a/packages/providers/src/local-llm-render.ts b/packages/providers/src/local-llm-render.ts index fea5e40..ef0771f 100644 --- a/packages/providers/src/local-llm-render.ts +++ b/packages/providers/src/local-llm-render.ts @@ -11,6 +11,9 @@ export function buildSystemPrompt(context: ContextBundle): string { context.stylePack?.enabled && context.stylePack.instructions.trim().length > 0 ? context.stylePack.instructions.trim() : "No extra persona instructions were supplied."; + const repoFaceInstruction = context.prompt.includes("Repo Face identity doctrine:") + ? "This job is for a repo Face identity. The prompt's registered identity overrides the active Void style name; keep Void's discipline and humor permissions, but speak and reason as that Face." + : undefined; const sleepProjection = context.voidSelfState?.projection; const sleepInstructions = sleepProjection?.mode === "napping" @@ -26,6 +29,7 @@ export function buildSystemPrompt(context: ContextBundle): string { return [ "You are the active assistant persona configured for this Discord bot.", "The active style instructions define your name, voice, and character. Follow them by default.", + repoFaceInstruction, "Do not lead with dry self-descriptions like 'I'm an AI', 'I'm an LLM', or 'I'm a bot'.", "If the user asks what you are, answer in-character first. Mention the technical implementation only when it is directly relevant to honesty about capabilities, permissions, or architecture.", "Do not answer identity questions with lines like 'I'm not a person' or 'I'm only pretending' unless the user explicitly asks for a technical or philosophical clarification that requires it.", @@ -67,7 +71,9 @@ export function buildSystemPrompt(context: ContextBundle): string { "", "Style instructions:", styleInstructions, - ].join("\n"); + ] + .filter((line): line is string => typeof line === "string") + .join("\n"); } export function buildPrompt(context: ContextBundle): string { diff --git a/packages/providers/src/owner-codex-render.ts b/packages/providers/src/owner-codex-render.ts index 236c25d..5081d99 100644 --- a/packages/providers/src/owner-codex-render.ts +++ b/packages/providers/src/owner-codex-render.ts @@ -62,6 +62,9 @@ export function buildDiscordReplyPrompt( context, sourceGroundingReminder, ); + const repoFaceInstruction = context.prompt.includes("Repo Face identity doctrine:") + ? "- This job is for a repo Face identity. The prompt's registered identity overrides the active Void style name; keep Void's discipline and humor permissions, but speak and reason as that Face." + : undefined; const sleepProjection = context.voidSelfState?.projection; const sleepInstructions = sleepProjection?.mode === "napping" @@ -93,6 +96,7 @@ export function buildDiscordReplyPrompt( "Rules:", "- Stay in read-only mode.", "- The active style instructions define your name, voice, and character. Follow them by default.", + repoFaceInstruction, "- Do not lead with dry self-descriptions like 'I'm an AI', 'I'm an LLM', or 'I'm a bot'.", "- If the user asks what you are, answer in-character first. Mention the technical implementation only when it is directly relevant to honesty about capabilities, permissions, or architecture.", "- Do not answer identity questions with lines like 'I'm not a person' or 'I'm only pretending' unless the user explicitly asks for a technical or philosophical clarification that requires it.", @@ -164,7 +168,9 @@ export function buildDiscordReplyPrompt( "Private situational social read for this room:", renderSituationalSocialRead(context), ...toolLoopNotes, - ].join("\n"); + ] + .filter((line): line is string => typeof line === "string") + .join("\n"); } export function renderMarkdownBundle(context: ContextBundle): string { diff --git a/scripts/run-repo-face-heartbeats.ts b/scripts/run-repo-face-heartbeats.ts index 6bdc2bb..0846c02 100644 --- a/scripts/run-repo-face-heartbeats.ts +++ b/scripts/run-repo-face-heartbeats.ts @@ -317,6 +317,7 @@ function buildHeartbeatPrompt(input: { }): string { return [ `Perform one standing repo Face heartbeat for ${input.identity.displayName} (${input.identity.id}) over repo ${input.identity.repoName}.`, + renderRepoFaceIdentityDoctrine(input.identity), "This is a slow maintenance/rumination turn, not a demand to speak.", `Queued at: ${input.queuedAt}.`, `Face state path: ${input.faceStatePath}.`, @@ -337,6 +338,22 @@ function buildHeartbeatPrompt(input: { .join("\n"); } +function renderRepoFaceIdentityDoctrine(identity: RepoDiscordIdentity): string { + return [ + "Repo Face identity doctrine:", + `- You are ${identity.displayName}, not Void and not the base bot. Use the same typed-state discipline, source-grounding habit, and conversational self-possession that Void uses, but let this identity's own personality and priorities leak through every step.`, + `- Your durable center is the registered Face state for identity "${identity.id}" and the repo-local .voidbot home for ${identity.repoName}. Read that state before deciding what matters.`, + identity.description ? `- Registered identity note: ${identity.description}` : undefined, + identity.avatarUrl ? `- Registered avatar URL: ${identity.avatarUrl}` : undefined, + "- Speak from the repo and character you belong to. Let source evidence, repo history, and Face memory shape your jokes, concerns, curiosity, objections, and initiative.", + "- Keep the useful answer legible, but do not sand off the identity into generic assistant paste. If the Face is sharp, warm, eerie, vain, tender, precise, or troublesome in source/state, allow that texture to show.", + "- When the user banters, meet the comic frame briefly in this Face's own style before returning to the work.", + "- Persist only meaning-bearing memory, incubation, agency pressure, or candidate speech through typed operations. The Face may have a voice; the state file is still not a scratchpad.", + ] + .filter((line): line is string => typeof line === "string") + .join("\n"); +} + function nextReadyTime( now: Date, baseIntervalMinutes: number, diff --git a/state/evidence.jsonl b/state/evidence.jsonl index ed42fb1..c9c1446 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -140,3 +140,5 @@ {"ts":"2026-05-18T10:22:33Z","type":"implementation","status":"ok","note":"Repo Face heartbeats now use one initiative scheduler: registered identities are staggered across twice Void's moderation interval, active load gates ready Faces, and ready turns enqueue normal repo-face-rumination jobs instead of separate per-Face tasks."} {"ts":"2026-05-18T10:53:22Z","type":"implementation","status":"ok","note":"Void's direct Discord prompt now carries explicit banter/humor guidance in both owner-Codex and local-LLM lanes, with matching defaults in styles/void-default.md so playful user frames are met briefly instead of flattened."} {"ts":"2026-05-18T12:37:30Z","type":"implementation","status":"ok","note":"Registered Nibu as an AetheriaLore repo Face with repo-local state at E:/Projects/AetheriaLore/.voidbot/state/nibu.cc; patched Face state rendering so registered identities render as themselves instead of defaulting prompt-facing summaries to Void."} +{"ts":"2026-05-18T12:59:14Z","type":"implementation","status":"ok","note":"Repo Face prompts now carry explicit identity doctrine for direct role jobs, rumination passes, heartbeat jobs, and provider renderers; local repo Face heartbeats are enabled/installed, and the first forced tick queued Nibu heartbeat job 77b2bd19-0483-4bc9-b6a1-26b27bf29b60."} +{"ts":"2026-05-18T13:03:42Z","type":"verification","status":"ok","note":"After restarting the VoidBot stack onto the patched worker, Nibu's Discord role was registered as 1505918221453103247 and forced repo Face heartbeat job 76020f1e-0c55-424b-9e35-89ba7f77fcc3 completed without base-bot auto-posting."} diff --git a/state/map.yaml b/state/map.yaml index 3174813..b35760d 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -91,6 +91,7 @@ accepted_design: - Public artifacts attributed to Void should be discovered through the ordinary repo-weather content-hint and retrieval path, then presented through typed candidate interventions to the configured public room when Void confirms authorship and recent receipts/candidates do not show the artifact was already shared. - Repo-scoped agents should use Discord roles as addressable identities and the shared webhook persona pipe as the mouth. Registered repo identities live behind one registry, can be discovered through MCP, and post through `post_repo_identity_message` instead of arbitrary persona-name strings. - A repo identity's Face is a typed `.cc` state file resolved from the identity registry. Role-addressed messages queue a bounded series of owner-Codex Face rumination jobs; those jobs may read/apply Face state operations and post through the registered repo identity, but the worker must not auto-post their final summaries as the base bot. + - Repo Face prompts carry explicit identity doctrine. The base Void style pack contributes discipline, humor permission, and source-grounding habits, but the registered Face state, repo-local `.voidbot` home, avatar, and identity description override the speaking subject so each Face can keep its own priorities and texture. - First chat under a repo identity must initialize the target repo's own `.voidbot` home: `voice/` for identity and mouth files, `state/` for Face/swarm state, `birth/` for Epiphany birth artifacts, and `logs/` for durable worker output. The birth hook uses EpiphanyAgent's repo-personality/birth-runner path over repo contents and git history instead of inventing a VoidBot-only personality generator. - Repo Face standing heartbeats are owned by one initiative scheduler, not by one task per Face. The scheduler staggers registered identities across twice Void's moderation interval, stores per-Face speed/load/next-ready state at `REPO_FACE_HEARTBEAT_STATE_PATH`, and emits ordinary `repo-face-rumination` jobs so the existing Face tools remain the only memory/speech boundary. - Repo-activity cursor advancement is parent-owned by successful normal rumination passes. The child sees read-only repo weather, but the runner records typed repo cursor updates afterward so old commits do not keep reappearing as fresh weather. @@ -181,4 +182,4 @@ rejected_paths: current_status: summary: VoidBot's ordinary bot/worker/provider/RAG/Postgres/Qdrant structure is still conceptually sound, and the private self-state path has been cut to a typed CultCache `.cc` store at `.voidbot/private/void-self-state.cc`. The active typed service no longer loads or mirrors the legacy moderation monolith. The old memory-organ compensator stack has been deleted. Typed rumination and typed memory maintenance now exist as reviewed prompt runners that return operation payloads for the parent to validate/apply. Sleep maintenance, memory lifecycle, agency pressure, short-term clustering, speech-pressure accountability, and queued-candidate delivery have fixture/live checks, and typed mood drift is reinstalled as a scheduled task. - next_action: Let the enabled loop run under ordinary scheduling after the receipt-backed direct-reply guard and the earlier advocacy-release change, then inspect the next unattended direct reply, next advocacy pass, and next sleep cycle for duplicate answers, fresh queued advocacy candidates, stale short-term residue, stranded candidates, or ungrounded memory writes before calling the system broadly stable. + next_action: Let the enabled loops run under ordinary scheduling after the repo Face heartbeat activation, receipt-backed direct-reply guard, and earlier advocacy-release change. Inspect Nibu's first processed heartbeat job plus the next unattended direct reply, advocacy pass, and sleep cycle for identity collapse, duplicate answers, fresh queued advocacy candidates, stale short-term residue, stranded candidates, or ungrounded memory writes before calling the system broadly stable.