Post direct repo identity replies as personas
This commit is contained in:
parent
13dffa517e
commit
b7aeda1bbf
6 changed files with 70 additions and 6 deletions
|
|
@ -865,8 +865,8 @@ function buildRepoIdentityPrompt(
|
|||
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.",
|
||||
`The Discord role mention ${mention} is the addressable identity. Write the direct in-channel reply as ${identity.displayName}; the worker will post your final answer through the registered repo identity persona.`,
|
||||
"Do not answer as base VoidBot. Do not call post_repo_identity_message for this direct mention; that tool is for background rumination or proactive follow-up, not the immediate summons reply.",
|
||||
"",
|
||||
"User prompt:",
|
||||
visiblePrompt,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ import {
|
|||
type AuditLog,
|
||||
buildVoidMcpServerConfig,
|
||||
createStateStorage,
|
||||
findRepoDiscordIdentity,
|
||||
isRepoDiscordIdentityAllowedInChannel,
|
||||
type JobQueue,
|
||||
loadRepoDiscordIdentityRegistry,
|
||||
loadSystemMessageCatalog,
|
||||
type SystemMessageCatalog,
|
||||
} from "@voidbot/core";
|
||||
|
|
@ -35,6 +38,8 @@ import {
|
|||
type ProviderNotificationIntent,
|
||||
} from "@voidbot/shared";
|
||||
|
||||
import { postDiscordMessage } from "./mcp-server-discord";
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
const embedder = createTextEmbedder({
|
||||
|
|
@ -322,7 +327,26 @@ async function processJob(job: JobRecord): Promise<void> {
|
|||
return;
|
||||
}
|
||||
|
||||
if (job.command === "repo-identity-mention" || job.command === "repo-face-rumination") {
|
||||
if (job.command === "repo-identity-mention") {
|
||||
await postRepoIdentityJobResponse(job, finalResponse);
|
||||
await jobQueue.completeJobDirect(job.id, finalResponse);
|
||||
await deliverOwnerNotifications(job, response.notifications ?? []);
|
||||
await auditLog.record({
|
||||
type: "provider.completed",
|
||||
actorId: job.requester.id,
|
||||
jobId: job.id,
|
||||
provider: job.provider,
|
||||
details: {
|
||||
summary: response.summary,
|
||||
autoPosted: true,
|
||||
reason: "repo_identity_mention_posted_as_registered_identity",
|
||||
},
|
||||
});
|
||||
console.log(`Completed repo-identity-mention job ${job.id}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (job.command === "repo-face-rumination") {
|
||||
await jobQueue.completeJobDirect(job.id, finalResponse);
|
||||
await deliverOwnerNotifications(job, response.notifications ?? []);
|
||||
await auditLog.record({
|
||||
|
|
@ -488,6 +512,45 @@ async function postJobResponse(job: JobRecord, content: string): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
async function postRepoIdentityJobResponse(job: JobRecord, content: string): Promise<void> {
|
||||
if (!config.botToken) {
|
||||
throw new Error("DISCORD_BOT_TOKEN is required for the worker to post repo identity responses.");
|
||||
}
|
||||
|
||||
const identityId = parseRepoIdentityIdFromPrompt(job.prompt);
|
||||
if (!identityId) {
|
||||
throw new Error(`Could not resolve repo identity for job ${job.id}.`);
|
||||
}
|
||||
|
||||
const registry = await loadRepoDiscordIdentityRegistry(config.repoDiscordIdentitiesPath);
|
||||
const identity = findRepoDiscordIdentity(registry, identityId);
|
||||
if (!identity) {
|
||||
throw new Error(`No registered repo identity matched "${identityId}" for job ${job.id}.`);
|
||||
}
|
||||
|
||||
if (!isRepoDiscordIdentityAllowedInChannel(identity, job.outputChannelId)) {
|
||||
throw new Error(
|
||||
`Repo identity ${identity.id} is not registered for Discord channel ${job.outputChannelId}.`,
|
||||
);
|
||||
}
|
||||
|
||||
await postDiscordMessage(
|
||||
config.botToken,
|
||||
job.outputChannelId,
|
||||
content,
|
||||
job.requestMessageId,
|
||||
{
|
||||
personaName: identity.displayName,
|
||||
personaAvatarUrl: identity.avatarUrl,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function parseRepoIdentityIdFromPrompt(prompt: string): string | undefined {
|
||||
const match = prompt.match(/repo identity\s+.+?\(([^)]+)\)\s+for repo/i);
|
||||
return match?.[1]?.trim();
|
||||
}
|
||||
|
||||
async function postOwnerNotification(content: string): Promise<void> {
|
||||
if (!config.botToken) {
|
||||
throw new Error("DISCORD_BOT_TOKEN is required for the worker to send owner notifications.");
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
|
|||
- Directly invoked Discord replies now load that same private moderation state as distilled private self-state context, so the ruminating scheduled loop and the summoned Void share one evolving personality/state spine. The prompt-facing self-state summary no longer dumps recent repo sweeps, novelty checks, or musing sludge back into working memory by default; it stays closer to seam memory, incubation, and room obligations. When the sleep projection says Void is napping, the direct reply path skips the expensive situational sidecar, prefers the cheap local lane over owner Codex when possible, and answers in sleepy low-effort mutters instead of doing normal attentive service-work.
|
||||
- The moderation loop now speaks through `node scripts/send-discord-message.mjs` using the local bot token instead of approval-gated side-effecting MCP tools, so unattended runs can DM or post without Codex pausing for permission every time it wants to open its mouth. That helper and the `post_discord_message` MCP tool now also support a shared guild-channel webhook pipe with per-message `personaName` and optional `personaAvatarUrl`, so swarm agents can speak as themselves without needing separate Discord bot identities.
|
||||
- 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/<identity>.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 identities also have Face state now. The bot resolves each identity to `.voidbot/private/repo-faces/<identity>.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. Direct `repo-identity-mention` jobs are posted by the worker through the registered webhook persona; background Face jobs must use `read_repo_face_state` / `apply_repo_face_state_operation` and `post_repo_identity_message` if they choose to speak. 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/<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`.
|
||||
- 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.
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ This note is the source-grounded description of how the live VoidBot stack is sh
|
|||
- 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 <repo>` 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.
|
||||
- Role mentions create `repo-identity-mention` owner-Codex jobs whose final text is posted by the worker through the registered repo identity webhook persona, 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. Rumination jobs remain private unless they explicitly post through `post_repo_identity_message`; the worker must never auto-post their 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.
|
||||
|
|
|
|||
|
|
@ -142,3 +142,4 @@
|
|||
{"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."}
|
||||
{"ts":"2026-05-18T13:20:52Z","type":"implementation","status":"ok","note":"Repo identity direct mentions were queuing successfully, but visible Nibu delivery failed because child Codex side-effect MCP posts were cancelled while worker auto-posting was suppressed. Direct repo-identity-mention jobs now return final text for the worker to post via the registered persona; background repo-face-rumination jobs remain private unless they explicitly use the identity posting tool."}
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ accepted_design:
|
|||
- Repo weather should lead with particulars, not slogans. If two repos only rhyme at bumper-sticker altitude, keep the reports separate instead of forcing a fake unifying law over them.
|
||||
- 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.
|
||||
- A repo identity's Face is a typed `.cc` state file resolved from the identity registry. Role-addressed messages create direct repo-identity jobs whose final text is posted through the registered webhook persona, then queue a bounded series of owner-Codex Face rumination jobs; those rumination 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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue