From 12e566b62f6a68fcdc370e9c5383395ad2c3b8e6 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 2 Jun 2026 15:53:17 +0100 Subject: [PATCH] Restore VoidBot swarm speech and dashboard publication --- apps/bot/src/discord-bot.ts | 3 +- apps/worker/src/index.ts | 51 + assets/native-personas/tengu.png | Bin 0 -> 45159 bytes package.json | 2 + packages/config/src/index.ts | 59 +- packages/core/src/epiphany-identities.ts | 12 + packages/core/src/repo-discord-identities.ts | 10 + packages/core/src/repo-face-initializer.ts | 6 +- packages/core/src/void-self-state-domain.ts | 54 + .../core/src/void-self-state-projection.ts | 2 + packages/core/src/void-self-state-service.ts | 34 + .../providers/src/owner-codex-provider.ts | 4 +- packages/providers/src/owner-codex-runtime.ts | 2 +- packages/providers/src/owner-codex-shared.ts | 2 +- scripts/render-voidbot-swarm-dashboard.mjs | 2276 +++++++++++++++++ scripts/run-repo-face-heartbeats.ts | 193 +- state/evidence.jsonl | 1 + 17 files changed, 2679 insertions(+), 32 deletions(-) create mode 100644 assets/native-personas/tengu.png create mode 100644 scripts/render-voidbot-swarm-dashboard.mjs diff --git a/apps/bot/src/discord-bot.ts b/apps/bot/src/discord-bot.ts index c437537..e8d94a3 100644 --- a/apps/bot/src/discord-bot.ts +++ b/apps/bot/src/discord-bot.ts @@ -807,7 +807,8 @@ async function ensureRepoIdentityRoles(options: { registryPath: string; identities: RepoDiscordIdentity[]; }): Promise<{ identities: RepoDiscordIdentity[] }> { - const identitiesMissingRoles = options.identities.filter((identity) => !identity.roleId); + const identitiesMissingRoles = options.identities.filter((identity) => + identity.identityKind !== "native_persona" && !identity.roleId); if (identitiesMissingRoles.length === 0) { return { identities: options.identities }; diff --git a/apps/worker/src/index.ts b/apps/worker/src/index.ts index c32fa8b..663145d 100644 --- a/apps/worker/src/index.ts +++ b/apps/worker/src/index.ts @@ -1286,9 +1286,56 @@ function parseRepoIdentityPostIntents(finalResponse: string): RepoIdentityPostIn } } + if (intents.length === 0) { + const draftSpeech = parseRepoIdentityDraftSpeechIntent(finalResponse); + if (draftSpeech) { + intents.push({ content: draftSpeech }); + } + } + return intents; } +function parseRepoIdentityDraftSpeechIntent(finalResponse: string): string | undefined { + const lines = finalResponse.split(/\r?\n/); + for (let index = 0; index < lines.length; index += 1) { + const match = lines[index]?.match(/^\s*Would say:\s*(.+?)\s*$/i); + if (!match) { + continue; + } + + const contentLines = [match[1] ?? ""]; + for (const following of lines.slice(index + 1)) { + const trimmed = following.trim(); + if (!trimmed) { + break; + } + if (/^(Private thought|Private summary|What should stick|State note|Would say):/i.test(trimmed)) { + break; + } + contentLines.push(trimmed); + } + + const content = stripPairedQuotes(contentLines.join("\n").trim()); + if (content) { + return content; + } + } + + return undefined; +} + +function stripPairedQuotes(value: string): string { + const trimmed = value.trim(); + if ( + (trimmed.startsWith("\"") && trimmed.endsWith("\"")) || + (trimmed.startsWith("'") && trimmed.endsWith("'")) + ) { + return trimmed.slice(1, -1).trim(); + } + return trimmed; +} + function parseRepoIdentityIdFromRequestMessageId(requestMessageId: string | undefined): string | undefined { const match = requestMessageId?.match(/^agent-turn:([^:]+):/i); return match?.[1]?.trim(); @@ -2482,6 +2529,10 @@ async function recordRepoIdentityDeliveryReceipt(input: { messageId: string; transport: "bot" | "webhook"; }): Promise { + if (input.identity.identityKind === "native_persona") { + return; + } + await applyVoidSelfStateOperation( { canonicalPath: resolveRepoFaceStatePath(input.identity, config.storageRoot), diff --git a/assets/native-personas/tengu.png b/assets/native-personas/tengu.png new file mode 100644 index 0000000000000000000000000000000000000000..df00d272b79eb6ddad4e0f136c8c6205c78b1965 GIT binary patch literal 45159 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 literal 0 HcmV?d00001 diff --git a/package.json b/package.json index 586f357..c13ce28 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "state:check": "powershell -ExecutionPolicy Bypass -File scripts/check-voidbot-operations.ps1", "state:dashboard": "powershell -ExecutionPolicy Bypass -File scripts/render-voidbot-operations-dashboard.ps1 -Open", "state:render-dashboard": "powershell -ExecutionPolicy Bypass -File scripts/render-voidbot-operations-dashboard.ps1", + "swarm:dashboard": "node scripts/render-voidbot-swarm-dashboard.mjs", + "swarm:render-dashboard": "node scripts/render-voidbot-swarm-dashboard.mjs", "state:sync-offsite": "powershell -ExecutionPolicy Bypass -File scripts/sync-voidbot-backup-offsite.ps1", "state:install-startup-task": "powershell -ExecutionPolicy Bypass -File scripts/install-stack-startup-task.ps1", "state:install-offsite-task": "powershell -ExecutionPolicy Bypass -File scripts/install-offsite-backup-task.ps1", diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index 8ffe5dd..01c8dfc 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -35,6 +35,7 @@ const booleanFromEnv = z.preprocess((value) => { }, z.boolean()); type OllamaThinkMode = boolean | "low" | "medium" | "high"; +type CodexReasoningEffort = "none" | "low" | "medium" | "high" | "xhigh"; const envSchema = z.object({ NODE_ENV: z.enum(["development", "test", "production"]).default("development"), @@ -89,7 +90,7 @@ const envSchema = z.object({ CODEX_EXECUTABLE: z.string().min(1).default("codex"), CODEX_EXEC_ARGS: z.string().default(""), CODEX_MODEL: z.string().min(1).default("gpt-5.4"), - CODEX_MODEL_REASONING_EFFORT: z.enum(["low", "medium", "high", "xhigh"]).default("low"), + CODEX_MODEL_REASONING_EFFORT: z.enum(["none", "low", "medium", "high", "xhigh"]).default("low"), CODEX_EXEC_TIMEOUT_MS: z.coerce.number().int().positive().default(120000), STYLE_PACK_PATH: z.string().min(1).default("styles/void-default.md"), SYSTEM_MESSAGES_PATH: z.string().min(1).default("config/system-messages.json"), @@ -102,7 +103,7 @@ const envSchema = z.object({ .min(1) .default(".voidbot/private/repo-discord-identities.json"), REPO_FACE_BIRTH_MODE: z.enum(["plan", "run"]).default("plan"), - REPO_FACE_BIRTH_EXECUTOR: z.enum(["codex-exec", "openai-runtime"]).default("codex-exec"), + REPO_FACE_BIRTH_EXECUTOR: z.enum(["model-runtime", "openai-runtime"]).default("model-runtime"), REPO_FACE_GITHUB_ACTIONS_ENABLED: booleanFromEnv.default(false), REPO_FACE_BIFROST_ENABLED: booleanFromEnv.default(false), BIFROST_ROOT: z.string().min(1).default("E:/Projects/Bifrost"), @@ -111,21 +112,35 @@ const envSchema = z.object({ REPO_FACE_HEARTBEAT_STATE_PATH: z.string().min(1).default(".voidbot/status/repo-face-heartbeats.json"), REPO_FACE_HEARTBEAT_TASK_NAME: z.string().min(1).default("VoidBot Repo Face Heartbeats"), REPO_FACE_HEARTBEAT_INTERVAL_MINUTES: z.coerce.number().int().min(1).default(5), - REPO_FACE_HEARTBEAT_MAX_JOBS_PER_TICK: z.coerce.number().int().min(1).max(8).default(2), + REPO_FACE_HEARTBEAT_MAX_JOBS_PER_TICK: z.coerce.number().int().min(1).max(8).default(3), REPO_FACE_HEARTBEAT_DEFAULT_CHANNEL_ID: optionalNonEmptyString, REPO_FACE_HEARTBEAT_BASE_RECOVERY_MINUTES: z.coerce.number().positive().default(10), REPO_FACE_HEARTBEAT_GLOBAL_HEAT: z.coerce.number().positive().default(1), REPO_FACE_HEARTBEAT_IDLE_COOLING_ENABLED: booleanFromEnv.default(true), - REPO_FACE_HEARTBEAT_IDLE_AFTER_MINUTES: z.coerce.number().positive().default(30), - REPO_FACE_HEARTBEAT_IDLE_RECOVERY_MINUTES: z.coerce.number().positive().default(30), + REPO_FACE_HEARTBEAT_IDLE_AFTER_MINUTES: z.coerce.number().positive().default(180), + REPO_FACE_HEARTBEAT_IDLE_RECOVERY_MINUTES: z.coerce.number().positive().default(120), + REPO_FACE_HEARTBEAT_IDLE_NAP_AFTER_MINUTES: z.coerce.number().positive().default(360), REPO_FACE_HEARTBEAT_SPEED_OVERRIDES: z.string().default(""), REPO_FACE_HEARTBEAT_HEAT_OVERRIDES: z.string().default(""), - REPO_FACE_HEARTBEAT_CODEX_MODEL: z.string().min(1).default("gpt-5.3-codex-spark"), - REPO_FACE_HEARTBEAT_CODEX_MODELS: z.string().default("gpt-5.3-codex-spark,gpt-5.4-mini"), + REPO_FACE_HEARTBEAT_CODEX_MODEL: z.string().min(1).default("gpt-5.4"), + REPO_FACE_HEARTBEAT_CODEX_MODELS: z.string().default("gpt-5.4"), + REPO_FACE_IMAGINATION_CODEX_MODEL: z.string().min(1).default("gpt-5.4"), + REPO_FACE_IMAGINATION_CODEX_MODELS: z.string().default("gpt-5.4"), + REPO_FACE_MIND_CODEX_MODEL: z.string().min(1).default("gpt-5.4"), + REPO_FACE_MIND_CODEX_MODELS: z.string().default("gpt-5.4"), REPO_FACE_TURN_CODEX_MODEL: z.string().min(1).default("gpt-5.4"), + REPO_FACE_TURN_CODEX_REASONING_EFFORT: z + .enum(["none", "low", "medium", "high", "xhigh"]) + .default("low"), + REPO_FACE_IMAGINATION_CODEX_REASONING_EFFORT: z + .enum(["none", "low", "medium", "high", "xhigh"]) + .default("none"), + REPO_FACE_MIND_CODEX_REASONING_EFFORT: z + .enum(["none", "low", "medium", "high", "xhigh"]) + .default("none"), REPO_FACE_HEARTBEAT_CODEX_REASONING_EFFORT: z - .enum(["low", "medium", "high", "xhigh"]) - .optional(), + .enum(["none", "low", "medium", "high", "xhigh"]) + .default("none"), REPO_FACE_STATE_PROJECTOR_ENABLED: booleanFromEnv.default(true), EPIPHANY_AGENT_ROOT: z.string().min(1).default("E:/Projects/EpiphanyAgent"), INDEX_ALL_CHANNELS: booleanFromEnv.default(false), @@ -156,14 +171,14 @@ export interface AppConfig { codexExecutable: string; codexExecArgs: string[]; codexModel: string; - codexModelReasoningEffort: "low" | "medium" | "high" | "xhigh"; + codexModelReasoningEffort: CodexReasoningEffort; codexExecTimeoutMs: number; stylePackPath: string; systemMessagesPath: string; moderationAgentStatePath: string; repoDiscordIdentitiesPath: string; repoFaceBirthMode: "plan" | "run"; - repoFaceBirthExecutor: "codex-exec" | "openai-runtime"; + repoFaceBirthExecutor: "model-runtime" | "openai-runtime"; repoFaceGithubActionsEnabled: boolean; repoFaceBifrostEnabled: boolean; bifrostRoot: string; @@ -181,13 +196,21 @@ export interface AppConfig { enabled: boolean; idleAfterMinutes: number; recoveryMinutes: number; + napAfterMinutes: number; }; speedOverrides: Record; heatOverrides: Record; codexModel?: string; codexModels: string[]; + imaginationCodexModel?: string; + imaginationCodexModels: string[]; + mindCodexModel?: string; + mindCodexModels: string[]; turnCodexModel: string; - codexModelReasoningEffort?: "low" | "medium" | "high" | "xhigh"; + turnCodexModelReasoningEffort: CodexReasoningEffort; + imaginationCodexModelReasoningEffort: CodexReasoningEffort; + mindCodexModelReasoningEffort: CodexReasoningEffort; + codexModelReasoningEffort?: CodexReasoningEffort; stateProjectorEnabled: boolean; }; epiphanyAgentRoot: string; @@ -412,6 +435,7 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig { enabled: parsed.REPO_FACE_HEARTBEAT_IDLE_COOLING_ENABLED, idleAfterMinutes: parsed.REPO_FACE_HEARTBEAT_IDLE_AFTER_MINUTES, recoveryMinutes: parsed.REPO_FACE_HEARTBEAT_IDLE_RECOVERY_MINUTES, + napAfterMinutes: parsed.REPO_FACE_HEARTBEAT_IDLE_NAP_AFTER_MINUTES, }, speedOverrides: parseNumericMap(parsed.REPO_FACE_HEARTBEAT_SPEED_OVERRIDES), heatOverrides: parseNumericMap(parsed.REPO_FACE_HEARTBEAT_HEAT_OVERRIDES), @@ -419,7 +443,18 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig { codexModels: parseList(parsed.REPO_FACE_HEARTBEAT_CODEX_MODELS).length > 0 ? parseList(parsed.REPO_FACE_HEARTBEAT_CODEX_MODELS) : [parsed.REPO_FACE_HEARTBEAT_CODEX_MODEL], + imaginationCodexModel: parsed.REPO_FACE_IMAGINATION_CODEX_MODEL, + imaginationCodexModels: parseList(parsed.REPO_FACE_IMAGINATION_CODEX_MODELS).length > 0 + ? parseList(parsed.REPO_FACE_IMAGINATION_CODEX_MODELS) + : [parsed.REPO_FACE_IMAGINATION_CODEX_MODEL], + mindCodexModel: parsed.REPO_FACE_MIND_CODEX_MODEL, + mindCodexModels: parseList(parsed.REPO_FACE_MIND_CODEX_MODELS).length > 0 + ? parseList(parsed.REPO_FACE_MIND_CODEX_MODELS) + : [parsed.REPO_FACE_MIND_CODEX_MODEL], turnCodexModel: parsed.REPO_FACE_TURN_CODEX_MODEL, + turnCodexModelReasoningEffort: parsed.REPO_FACE_TURN_CODEX_REASONING_EFFORT, + imaginationCodexModelReasoningEffort: parsed.REPO_FACE_IMAGINATION_CODEX_REASONING_EFFORT, + mindCodexModelReasoningEffort: parsed.REPO_FACE_MIND_CODEX_REASONING_EFFORT, codexModelReasoningEffort: parsed.REPO_FACE_HEARTBEAT_CODEX_REASONING_EFFORT, stateProjectorEnabled: parsed.REPO_FACE_STATE_PROJECTOR_ENABLED, }, diff --git a/packages/core/src/epiphany-identities.ts b/packages/core/src/epiphany-identities.ts index c166e5e..6a51d7d 100644 --- a/packages/core/src/epiphany-identities.ts +++ b/packages/core/src/epiphany-identities.ts @@ -36,6 +36,7 @@ const jurisdictionSchema = z.object({ const faceIdentitySchema = z.object({ id: z.string().trim().min(1), + identityKind: z.enum(["repo_face", "native_persona"]).default("repo_face"), displayName: z.string().trim().min(1).max(80), repoName: z.string().trim().min(1).optional(), repoPath: z.string().trim().min(1).optional(), @@ -50,7 +51,9 @@ const faceIdentitySchema = z.object({ posture: z.string().trim().min(1).optional(), })).default([]), avatarUrl: z.string().trim().url().max(512).optional(), + avatarPath: z.string().trim().min(1).optional(), faceStatePath: z.string().trim().min(1).optional(), + personaStatePath: z.string().trim().min(1).optional(), description: z.string().trim().min(1).optional(), grants: z.array(agencyGrantSchema).default(["discussion", "rumination", "discord_text"]), jurisdictions: z.array(jurisdictionSchema).default([]), @@ -117,6 +120,7 @@ export function faceToRepoDiscordIdentity(face: ResolvedFaceIdentity): RepoDisco return { id: face.id, + identityKind: face.identityKind, repoName, displayName: face.displayName, repoPath: face.repoPath, @@ -124,7 +128,9 @@ export function faceToRepoDiscordIdentity(face: ResolvedFaceIdentity): RepoDisco allowedChannelIds: face.allowedChannelIds, channelPermissions: face.channelPermissions, avatarUrl: face.avatarUrl, + avatarPath: face.avatarPath, faceStatePath: face.faceStatePath, + personaStatePath: face.personaStatePath, description: renderFaceDescription(face), }; } @@ -182,10 +188,13 @@ function repoIdentityToEpiphany(identity: RepoDiscordIdentity): EpiphanyIdentity repoName: identity.repoName, repoPath: identity.repoPath, roleId: identity.roleId, + identityKind: identity.identityKind, allowedChannelIds: getRepoDiscordIdentityAllowedChannelIds(identity), channelPermissions: identity.channelPermissions, avatarUrl: identity.avatarUrl, + avatarPath: identity.avatarPath, faceStatePath: identity.faceStatePath, + personaStatePath: identity.personaStatePath, description: identity.description, grants: [ "discussion", @@ -233,6 +242,7 @@ function normalizeEpiphanies(epiphanies: EpiphanyIdentity[]): EpiphanyIdentity[] repoName: face.repoName?.trim(), repoPath: face.repoPath?.trim(), roleId: face.roleId?.trim(), + identityKind: face.identityKind, allowedChannelIds: [...new Set(face.allowedChannelIds.map((entry) => entry.trim()))], channelPermissions: face.channelPermissions.map((permission) => ({ ...permission, @@ -242,7 +252,9 @@ function normalizeEpiphanies(epiphanies: EpiphanyIdentity[]): EpiphanyIdentity[] posture: permission.posture?.trim(), })), avatarUrl: face.avatarUrl?.trim(), + avatarPath: face.avatarPath?.trim(), faceStatePath: face.faceStatePath?.trim(), + personaStatePath: face.personaStatePath?.trim(), description: face.description?.trim(), grants: [...new Set(face.grants)], jurisdictions: face.jurisdictions.map(normalizeJurisdiction), diff --git a/packages/core/src/repo-discord-identities.ts b/packages/core/src/repo-discord-identities.ts index 3d27e93..bbce11c 100644 --- a/packages/core/src/repo-discord-identities.ts +++ b/packages/core/src/repo-discord-identities.ts @@ -5,6 +5,7 @@ import { z } from "zod"; const repoDiscordIdentitySchema = z.object({ id: z.string().trim().min(1), + identityKind: z.enum(["repo_face", "native_persona"]).default("repo_face"), repoName: z.string().trim().min(1), displayName: z.string().trim().min(1).max(80), repoPath: z.string().trim().min(1).optional(), @@ -19,7 +20,9 @@ const repoDiscordIdentitySchema = z.object({ posture: z.string().trim().min(1).optional(), })).default([]), avatarUrl: z.string().trim().url().max(512).optional(), + avatarPath: z.string().trim().min(1).optional(), faceStatePath: z.string().trim().min(1).optional(), + personaStatePath: z.string().trim().min(1).optional(), description: z.string().trim().min(1).optional(), }); @@ -144,6 +147,10 @@ export function resolveRepoFaceStatePath( return resolve(identity.faceStatePath); } + if (identity.personaStatePath) { + return resolve(identity.personaStatePath); + } + return resolve(storageRoot, "private", "repo-faces", `${sanitizePathSegment(identity.id)}.cc`); } @@ -199,6 +206,7 @@ function normalizeRepoDiscordIdentities( return { ...identity, id: identity.id.trim(), + identityKind: identity.identityKind, repoName: identity.repoName.trim(), displayName: identity.displayName.trim().slice(0, 80), repoPath: identity.repoPath?.trim(), @@ -206,7 +214,9 @@ function normalizeRepoDiscordIdentities( allowedChannelIds: [...new Set(identity.allowedChannelIds.map((entry) => entry.trim()))], channelPermissions: normalizeChannelPermissions(identity.channelPermissions), avatarUrl: identity.avatarUrl?.trim(), + avatarPath: identity.avatarPath?.trim(), faceStatePath: identity.faceStatePath?.trim(), + personaStatePath: identity.personaStatePath?.trim(), description: identity.description?.trim(), }; }); diff --git a/packages/core/src/repo-face-initializer.ts b/packages/core/src/repo-face-initializer.ts index af586c7..605ebbb 100644 --- a/packages/core/src/repo-face-initializer.ts +++ b/packages/core/src/repo-face-initializer.ts @@ -18,7 +18,7 @@ export interface RepoFaceInitializationOptions { epiphanyAgentRoot?: string; workspaceRoot?: string; birthMode?: "plan" | "run"; - birthExecutor?: "codex-exec" | "openai-runtime"; + birthExecutor?: "model-runtime" | "codex-exec" | "openai-runtime"; } export interface RepoFaceInitializationResult { @@ -145,7 +145,9 @@ export async function ensureRepoFaceInitialized( "--mode", options.birthMode ?? "plan", "--executor", - options.birthExecutor ?? "codex-exec", + options.birthExecutor === "codex-exec" + ? "model-runtime" + : options.birthExecutor ?? "model-runtime", ], { cwd: options.workspaceRoot ?? process.cwd(), diff --git a/packages/core/src/void-self-state-domain.ts b/packages/core/src/void-self-state-domain.ts index 67ecfa7..21d0aec 100644 --- a/packages/core/src/void-self-state-domain.ts +++ b/packages/core/src/void-self-state-domain.ts @@ -344,6 +344,43 @@ const faceMoodDimensionSchema = z.object({ updatedAt: timestampSchema, }).strict(); +const faceSocialBiasSchema = z.object({ + name: z.enum([ + "neuroticism", + "threat_sensitivity", + "hostile_attribution_bias", + "reassurance_need", + "grievance_retention", + "status_vigilance", + "trust_baseline", + ]), + value: z.number().min(0).max(1), + summary: z.string().trim().min(1).max(1000), + behavioralPull: z.string().trim().min(1).max(1000), + updatedAt: timestampSchema, +}).strict(); + +const faceDoctrineStanceSchema = z.object({ + stanceId: nonEmptyStringSchema, + doctrine: nonEmptyStringSchema, + status: z.enum(["active", "cooling", "crystallized", "resolved", "retired"]), + target: thoughtTargetSchema, + summary: boundedTextSchema, + claim: z.string().trim().min(1).max(2000).optional(), + question: z.string().trim().min(1).max(2000).optional(), + tension: z.string().trim().min(1).max(2000), + actionImplication: z.string().trim().min(1).max(2000), + intensity: z.number().min(0).max(1).default(0.5), + valence: z.number().min(-1).max(1).default(0), + anchorRefs: z.array(anchorRefSchema).default([]), + evidenceRefs: z.array(evidenceRefSchema).default([]), + sourceMemoryIds: z.array(nonEmptyStringSchema).default([]), + createdAt: timestampSchema, + updatedAt: timestampSchema, + retiredAt: timestampSchema.optional(), + tags: z.array(nonEmptyStringSchema).default([]), +}).strict(); + const selfProfileValueSchema = z.object({ id: nonEmptyStringSchema, label: nonEmptyStringSchema, @@ -435,6 +472,8 @@ export const voidFaceAffectSchema = z.object({ socialBonds: z.array(faceSocialBondSchema).default([]), statusReads: z.array(faceStatusReadSchema).default([]), moodDimensions: z.array(faceMoodDimensionSchema).default([]), + socialBiases: z.array(faceSocialBiasSchema).default([]), + doctrineStances: z.array(faceDoctrineStanceSchema).default([]), updatedAt: timestampSchema, }).strict(); @@ -626,6 +665,21 @@ export const voidSelfStateOperationSchema = z.discriminatedUnion("operation", [ dimensions: z.array(faceMoodDimensionSchema).min(1), updatedAt: timestampSchema, }).strict(), + z.object({ + operation: z.literal("update_social_biases"), + biases: z.array(faceSocialBiasSchema).min(1), + updatedAt: timestampSchema, + }).strict(), + z.object({ + operation: z.literal("upsert_doctrine_stance"), + stance: faceDoctrineStanceSchema, + }).strict(), + z.object({ + operation: z.literal("retire_doctrine_stance"), + stanceId: nonEmptyStringSchema, + retiredAt: timestampSchema, + reason: z.string().trim().min(1).max(1000), + }).strict(), z.object({ operation: z.literal("update_sleep_cycle"), sleepCycle: voidScheduledRuntimeSchema.shape.sleepCycle, diff --git a/packages/core/src/void-self-state-projection.ts b/packages/core/src/void-self-state-projection.ts index 83ccd6f..f7753e7 100644 --- a/packages/core/src/void-self-state-projection.ts +++ b/packages/core/src/void-self-state-projection.ts @@ -141,6 +141,8 @@ export function createEmptyVoidSelfState( socialBonds: [], statusReads: [], moodDimensions: [], + socialBiases: [], + doctrineStances: [], updatedAt: createdAt, }), }; diff --git a/packages/core/src/void-self-state-service.ts b/packages/core/src/void-self-state-service.ts index e11e0c3..c92bd41 100644 --- a/packages/core/src/void-self-state-service.ts +++ b/packages/core/src/void-self-state-service.ts @@ -256,6 +256,7 @@ function pruneRetiredTypedState(state: VoidSelfStateTypedProjection): void { state.faceAffect.needs = state.faceAffect.needs.filter(isNotRetiredEntry); state.faceAffect.socialBonds = state.faceAffect.socialBonds.filter(isNotRetiredEntry); state.faceAffect.statusReads = state.faceAffect.statusReads.filter(isNotRetiredEntry); + state.faceAffect.doctrineStances = state.faceAffect.doctrineStances.filter(isNotRetiredEntry); } function isNotRetiredEntry(entry: { status?: string; retiredAt?: string }): boolean { @@ -434,6 +435,25 @@ function applyTypedOperation( .slice(0, 32); state.faceAffect.updatedAt = operation.updatedAt; return; + case "update_social_biases": + for (const bias of operation.biases) { + upsertBy(state.faceAffect.socialBiases, bias, (entry) => entry.name); + } + state.faceAffect.socialBiases = state.faceAffect.socialBiases + .sort((left, right) => right.value - left.value) + .slice(0, 24); + state.faceAffect.updatedAt = operation.updatedAt; + return; + case "upsert_doctrine_stance": + upsertBy(state.faceAffect.doctrineStances, operation.stance, (entry) => entry.stanceId); + state.faceAffect.doctrineStances = state.faceAffect.doctrineStances + .sort((left, right) => right.intensity - left.intensity) + .slice(0, 48); + state.faceAffect.updatedAt = operation.stance.updatedAt; + return; + case "retire_doctrine_stance": + retireDoctrineStance(state.faceAffect, operation); + return; case "update_sleep_cycle": state.scheduledRuntime.sleepCycle = operation.sleepCycle; state.scheduledRuntime.updatedAt = new Date().toISOString(); @@ -688,6 +708,20 @@ function retireStatusRead( faceAffect.updatedAt = operation.retiredAt; } +function retireDoctrineStance( + faceAffect: VoidFaceAffect, + operation: Extract, +): void { + const stance = faceAffect.doctrineStances.find((entry) => entry.stanceId === operation.stanceId); + if (stance) { + stance.status = "retired"; + stance.retiredAt = operation.retiredAt; + stance.updatedAt = operation.retiredAt; + stance.tags = Array.from(new Set([...stance.tags, `retired:${operation.reason}`])); + } + faceAffect.updatedAt = operation.retiredAt; +} + function closeCasesForReceipt( cursor: VoidModerationCursor, receipt: VoidSpeechReceipts["recentReceipts"][number], diff --git a/packages/providers/src/owner-codex-provider.ts b/packages/providers/src/owner-codex-provider.ts index 27053ec..b571f5d 100644 --- a/packages/providers/src/owner-codex-provider.ts +++ b/packages/providers/src/owner-codex-provider.ts @@ -535,8 +535,8 @@ async function appendModelOutputLog(input: { await appendFile(logPath, `${JSON.stringify(record)}\n`, "utf8"); } -function getReasoningEffortOption(value: unknown): "low" | "medium" | "high" | "xhigh" | undefined { - if (value === "low" || value === "medium" || value === "high" || value === "xhigh") { +function getReasoningEffortOption(value: unknown): "none" | "low" | "medium" | "high" | "xhigh" | undefined { + if (value === "none" || value === "low" || value === "medium" || value === "high" || value === "xhigh") { return value; } return undefined; diff --git a/packages/providers/src/owner-codex-runtime.ts b/packages/providers/src/owner-codex-runtime.ts index d456d31..d93ff29 100644 --- a/packages/providers/src/owner-codex-runtime.ts +++ b/packages/providers/src/owner-codex-runtime.ts @@ -43,7 +43,7 @@ export async function runCodexExec(input: { executable: string; executableArgs: string[]; model: string; - reasoningEffort: "low" | "medium" | "high" | "xhigh"; + reasoningEffort: "none" | "low" | "medium" | "high" | "xhigh"; timeoutMs: number; workingDirectory: string; prompt: string; diff --git a/packages/providers/src/owner-codex-shared.ts b/packages/providers/src/owner-codex-shared.ts index dcf6eba..fe79372 100644 --- a/packages/providers/src/owner-codex-shared.ts +++ b/packages/providers/src/owner-codex-shared.ts @@ -35,7 +35,7 @@ export interface OwnerCodexProviderOptions { executable: string; executableArgs: string[]; model: string; - reasoningEffort: "low" | "medium" | "high" | "xhigh"; + reasoningEffort: "none" | "low" | "medium" | "high" | "xhigh"; timeoutMs: number; workingDirectory: string; historyLookup?: HistoryLookupTool; diff --git a/scripts/render-voidbot-swarm-dashboard.mjs b/scripts/render-voidbot-swarm-dashboard.mjs new file mode 100644 index 0000000..32d7890 --- /dev/null +++ b/scripts/render-voidbot-swarm-dashboard.mjs @@ -0,0 +1,2276 @@ +#!/usr/bin/env node +import { createRequire } from "node:module"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { existsSync, readFileSync } from "node:fs"; +import { mkdir, readFile, stat, writeFile } from "node:fs/promises"; + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const defaultStorageRoot = resolve(repoRoot, ".voidbot"); +const defaultStatusDir = resolve(defaultStorageRoot, "status"); +const defaultSnapshotPath = resolve(defaultStatusDir, "swarm-state.json"); +const defaultDashboardPath = resolve(defaultStatusDir, "swarm-dashboard.html"); +const defaultCultMeshStorePath = resolve(defaultStatusDir, "cultmesh", "voidbot-swarm-state.cc"); +const providerId = "voidbot.swarm"; +const verseId = "voidbot.local"; +const snapshotDocumentType = "voidbot.swarm_state_snapshot"; +const snapshotSchemaId = "voidbot.swarm_state_snapshot.v1"; +const providerAdvertisementDocumentType = "gamecult.eve.provider_advertisement"; +const providerAdvertisementSchemaId = "gamecult.eve.provider_advertisement.v1"; +const eveBindingDocumentType = "gamecult.eve.interface_binding"; +const eveBindingSchemaId = "gamecult.eve.interface_binding.v1"; +const surfaceDocumentType = "gamecult.eve.surface_state"; +const surfaceSchemaId = "gamecult.eve.surface_state.v1"; + +const args = parseArgs(process.argv.slice(2)); + +const env = await readDotEnv(resolve(repoRoot, ".env")); +const storageRoot = resolveConfigPath(env.STORAGE_ROOT, defaultStorageRoot); +const statusDir = resolve(storageRoot, "status"); +const heartbeatStatePath = resolveConfigPath( + env.REPO_FACE_HEARTBEAT_STATE_PATH, + resolve(statusDir, "repo-face-heartbeats.json"), +); +const orchestratorPath = resolve(statusDir, "gamecult-orchestrator.json"); +const pausePath = resolve(repoRoot, "state", "agent-swarm-paused.json"); +const snapshotPath = resolveConfigPath(args.snapshot, defaultSnapshotPath.replace(defaultStatusDir, statusDir)); +const dashboardPath = resolveConfigPath(args.out, defaultDashboardPath.replace(defaultStatusDir, statusDir)); +const cultMeshStorePath = resolveConfigPath( + args.cultmeshStore, + defaultCultMeshStorePath.replace(defaultStatusDir, statusDir), +); +const requireCultMeshAdvertisement = !isFlagEnabled(args.public) && !isFlagEnabled(args.allowUnmeshed); + +const renderResult = await render(); + +if (args.open) { + console.log(pathToFileURL(renderResult.dashboardPath).href); +} + +if (args.serve) { + throw new Error("VoidBot swarm is exposed through CultMesh/Eve, not a web server. Use npm run swarm:render-dashboard to publish the CultMesh surface."); +} else { + console.log(`Swarm dashboard HTML: ${renderResult.dashboardPath}`); + console.log(`Swarm snapshot JSON: ${renderResult.snapshotPath}`); + console.log(`CultMesh snapshot store: ${renderResult.cultMeshStorePath}`); +} + +async function render() { + let snapshot = await buildSnapshot(); + if (isFlagEnabled(args.public)) { + snapshot = redactSnapshot(snapshot); + } + await mkdir(dirname(snapshotPath), { recursive: true }); + await mkdir(dirname(dashboardPath), { recursive: true }); + + const pendingEveState = buildEveProviderState(snapshot); + const cultMeshWrite = await writeCultMeshPublication(snapshot, pendingEveState, cultMeshStorePath); + if (requireCultMeshAdvertisement && cultMeshWrite.writeStatus !== "ok") { + throw new Error(`VoidBot swarm CultMesh advertisement failed: ${cultMeshWrite.writeError ?? cultMeshWrite.writeStatus}`); + } + const finalSnapshot = { + ...snapshot, + cultMesh: { + ...snapshot.cultMesh, + ...cultMeshWrite, + }, + }; + const finalEveState = buildEveProviderState(finalSnapshot); + + await writeFile(snapshotPath, `${JSON.stringify(finalSnapshot, null, 2)}\n`, "utf8"); + await writeFile(dashboardPath, renderHtml(finalSnapshot), "utf8"); + + return { + dashboardPath, + snapshotPath, + cultMeshStorePath, + }; +} + +async function buildSnapshot() { + const now = new Date(); + const heartbeat = await readJsonFile(heartbeatStatePath); + const orchestrator = await readJsonFile(orchestratorPath); + const pause = await readJsonFile(pausePath); + const identityMetadata = await readIdentityMetadata(env.REPO_DISCORD_IDENTITIES_PATH); + const heartbeatMtime = await getMtime(heartbeatStatePath); + const orchestratorMtime = await getMtime(orchestratorPath); + const paused = pause.ok ? pause.value?.paused !== false : false; + const pauseReason = pause.ok && typeof pause.value?.reason === "string" ? pause.value.reason : undefined; + const participants = Array.isArray(heartbeat.value?.participants) ? heartbeat.value.participants : []; + const pendingMentions = Array.isArray(heartbeat.value?.pendingMentions) ? heartbeat.value.pendingMentions : []; + const initiativeClock = numberOrNull(heartbeat.value?.initiativeClock); + const participantSnapshots = participants.map((participant) => + projectParticipant(participant, pendingMentions, initiativeClock, identityMetadata), + ); + const faceStates = await readFaceStates(identityMetadata); + for (const participant of participantSnapshots) { + participant.faceState = faceStates.get(participant.identityId.toLowerCase()) ?? { + readable: false, + path: participant.faceStatePath, + error: participant.faceStatePath ? "State file was not readable." : "No Face state path registered.", + tree: [], + }; + participant.restState = participant.faceState.restState ?? null; + } + const activeTurns = participantSnapshots.filter((participant) => participant.activeJobId); + const readyNow = participantSnapshots.filter((participant) => + participant.status === "active" && + !participant.activeJobId && + participant.restState?.isNapping !== true && + typeof participant.nextTurnInMinutes === "number" && + participant.nextTurnInMinutes <= 0, + ); + const nextParticipant = [...participantSnapshots] + .filter((participant) => typeof participant.nextTurnInMinutes === "number") + .sort((left, right) => left.nextTurnInMinutes - right.nextTurnInMinutes)[0]; + const organs = projectOrgans(orchestrator.value); + const recentEvents = Array.isArray(heartbeat.value?.history) + ? heartbeat.value.history.slice(-24).reverse().map(projectHistoryEvent) + : []; + const controls = projectControls(heartbeat.value?.controls); + const upcomingTurns = buildUpcomingTurns(participantSnapshots); + const lastShuffle = findLastShuffleEvent(recentEvents); + + return { + schemaVersion: snapshotSchemaId, + generatedAt: now.toISOString(), + title: "VoidBot Swarm State", + sources: { + heartbeatStatePath, + heartbeatStateUpdatedAt: heartbeatMtime, + heartbeatStateReadable: heartbeat.ok, + heartbeatStateError: heartbeat.ok ? undefined : heartbeat.error, + orchestratorPath, + orchestratorUpdatedAt: orchestratorMtime, + orchestratorReadable: orchestrator.ok, + orchestratorError: orchestrator.ok ? undefined : orchestrator.error, + pausePath, + pauseReadable: pause.ok, + pauseError: pause.ok ? undefined : pause.error, + }, + cultMesh: { + documentType: snapshotDocumentType, + schemaId: snapshotSchemaId, + documentKey: "voidbot-swarm", + storePath: cultMeshStorePath, + writeStatus: "pending", + providerId, + verseId, + eveDeckEndpoint: primaryCultMeshEndpoint(), + note: "VoidBot swarm is a CultMesh-advertised Eve provider. The web page is a local lowering of the same surface.", + }, + summary: { + state: deriveSwarmState({ heartbeat, orchestrator, paused, activeTurns }), + paused, + pauseReason, + participantCount: participantSnapshots.length, + activeTurnCount: activeTurns.length, + pendingMentionCount: pendingMentions.length, + readyNowCount: readyNow.length, + nextIdentityId: nextParticipant?.identityId ?? null, + nextDisplayName: nextParticipant?.displayName ?? null, + nextTurnInMinutes: nextParticipant?.nextTurnInMinutes ?? null, + initiativeClock, + baseRecoveryMinutes: numberOrNull(heartbeat.value?.baseRecoveryMinutes), + globalHeat: numberOrNull(heartbeat.value?.globalHeat), + cadenceMultiplier: controls.cadenceMultiplier, + lastTickAt: stringOrNull(heartbeat.value?.lastTickAt), + lastShuffle, + }, + controls, + upcomingTurns, + participants: participantSnapshots, + activeTurns, + pendingMentions: pendingMentions.map(projectPendingMention), + orchestrator: { + state: deriveOrchestratorState(organs), + organs, + }, + recentEvents, + }; +} + +function projectControls(value) { + const cadenceMultiplier = numberOrNull(value?.cadenceMultiplier) ?? 1; + const manualTurnRequests = Array.isArray(value?.manualTurnRequests) + ? value.manualTurnRequests.slice(-12).reverse().map((request) => ({ + id: stringOrNull(request?.id), + identityId: stringOrNull(request?.identityId), + requestedAt: stringOrNull(request?.requestedAt), + status: stringOrNull(request?.status) ?? "pending", + note: stringOrNull(request?.note), + })) + : []; + return { + cadenceMultiplier, + updatedAt: stringOrNull(value?.updatedAt), + manualTurnRequests, + }; +} + +function buildUpcomingTurns(participants) { + const ordered = [...participants] + .filter((participant) => participant.status === "active") + .sort((left, right) => { + const activeDelta = Number(Boolean(right.activeJobId)) - Number(Boolean(left.activeJobId)); + if (activeDelta) return activeDelta; + return (left.nextTurnInMinutes ?? 999999) - (right.nextTurnInMinutes ?? 999999); + }) + .slice(0, 16); + const maxMinutes = Math.max(1, ...ordered.map((participant) => Math.max(0, participant.nextTurnInMinutes ?? 0))); + return ordered.map((participant, index) => ({ + identityId: participant.identityId, + displayName: participant.displayName, + repoName: participant.repoName, + avatarUrl: participant.avatarUrl, + participantKind: participant.participantKind, + activeJobId: participant.activeJobId, + restState: participant.restState, + pendingMentionCount: participant.pendingMentionCount, + shuffleReason: participant.pendingMentionCount > 0 + ? "mention" + : participant.activeJobId + ? "active" + : participant.restState?.isNapping === true + ? "nap" + : null, + nextTurnInMinutes: participant.nextTurnInMinutes, + effectiveSpeed: participant.effectiveSpeed, + heat: participant.heat, + lane: index % 3, + timelinePosition: participant.activeJobId ? 0 : Math.min(100, Math.max(0, ((Math.max(0, participant.nextTurnInMinutes ?? 0)) / maxMinutes) * 100)), + })); +} + +function findLastShuffleEvent(events) { + const directMention = events.find((event) => + event.type === "queued" && + typeof event.pendingMentionCount === "number" && + event.pendingMentionCount > 0 + ); + if (directMention) { + return { + kind: "direct_mention", + identityId: directMention.identityId, + observedAt: directMention.observedAt, + }; + } + const manual = events.find((event) => event.type === "manual_turn_override_applied"); + if (manual) { + return { + kind: "manual_override", + identityId: manual.identityId, + observedAt: manual.observedAt, + }; + } + return null; +} + +function projectParticipant(participant, pendingMentions, initiativeClock, identityMetadata) { + const identityId = String(participant.identityId ?? "unknown"); + const metadata = identityMetadata.get(identityId.toLowerCase()) ?? {}; + const nextTurnAt = numberOrNull(participant.nextTurnAt); + const lastTurnAt = numberOrNull(participant.lastTurnAt); + const activeTurnStartedAt = numberOrNull(participant.activeTurnStartedAt); + const channelCount = Array.isArray(participant.groups) + ? participant.groups.filter((entry) => typeof entry === "string" && entry.startsWith("channel:")).length + : 0; + const mentionCount = pendingMentions.filter((mention) => mention?.identityId === identityId).length; + const nextTurnInMinutes = + typeof nextTurnAt === "number" && typeof initiativeClock === "number" + ? round(nextTurnAt - initiativeClock, 3) + : null; + + return { + identityId, + displayName: String(participant.displayName ?? metadata.displayName ?? identityId), + repoName: String(participant.repoName ?? metadata.repoName ?? "unknown"), + repoPath: stringOrNull(metadata.repoPath), + faceStatePath: stringOrNull(metadata.faceStatePath), + personaStatePath: stringOrNull(metadata.personaStatePath), + description: stringOrNull(metadata.description), + avatarUrl: metadata.avatarUrl ?? null, + avatarPath: metadata.avatarPath ?? null, + channelPermissions: Array.isArray(metadata.channelPermissions) + ? metadata.channelPermissions.slice(0, 12).map((entry) => ({ + label: stringOrNull(entry?.label), + topic: stringOrNull(entry?.topic), + speechThreshold: stringOrNull(entry?.speechThreshold), + speedMultiplier: numberOrNull(entry?.speedMultiplier), + })) + : [], + participantKind: String(participant.participantKind ?? "repo_face"), + turnKind: String(participant.turnKind ?? "repo_face_rumination"), + status: String(participant.status ?? "unknown"), + currentLoad: numberOrNull(participant.currentLoad), + heat: numberOrNull(participant.heat), + effectiveSpeed: numberOrNull(participant.effectiveSpeed), + initiativeSpeed: numberOrNull(participant.initiativeSpeed), + nextTurnAt, + nextTurnInMinutes, + lastTurnAt, + activeTurnStartedAt, + activeJobId: stringOrNull(participant.activeJobId), + lastQueuedAt: stringOrNull(participant.lastQueuedAt), + queuedCount: numberOrNull(participant.queuedCount), + pendingMentionCount: mentionCount, + channelCount, + constraintCount: Array.isArray(participant.constraints) ? participant.constraints.length : 0, + constraints: Array.isArray(participant.constraints) ? participant.constraints.slice(0, 4).map(String) : [], + }; +} + +function projectOrgans(orchestrator) { + const organs = orchestrator?.organs && typeof orchestrator.organs === "object" ? orchestrator.organs : {}; + return Object.entries(organs).map(([id, organ]) => ({ + id, + label: String(organ?.label ?? id), + intervalMinutes: numberOrNull(organ?.intervalMinutes), + lastStartedAt: stringOrNull(organ?.lastStartedAt), + lastFinishedAt: stringOrNull(organ?.lastFinishedAt), + lastExitCode: numberOrNull(organ?.lastExitCode), + lastStatus: String(organ?.lastStatus ?? "unknown"), + lastLogPath: stringOrNull(organ?.lastLogPath), + })); +} + +function projectHistoryEvent(event) { + return { + type: String(event?.type ?? "event"), + identityId: stringOrNull(event?.identityId), + observedAt: stringOrNull(event?.observedAt ?? event?.queuedAt ?? event?.appliedAt), + activeJobId: stringOrNull(event?.activeJobId), + statusPath: stringOrNull(event?.statusPath), + reason: stringOrNull(event?.reason), + pendingMentionCount: numberOrNull(event?.pendingMentionCount), + nextTurnAt: numberOrNull(event?.nextTurnAt), + recoveryMinutes: numberOrNull(event?.recoveryMinutes), + }; +} + +async function readIdentityMetadata(registryPathValue) { + const metadata = new Map(); + const registryPath = resolveConfigPath(registryPathValue, resolve(repoRoot, ".voidbot", "private", "repo-discord-identities.json")); + const registry = await readJsonFile(registryPath); + const identities = Array.isArray(registry.value?.identities) + ? registry.value.identities + : Array.isArray(registry.value) + ? registry.value + : []; + for (const identity of identities) { + const id = typeof identity?.id === "string" ? identity.id.toLowerCase() : ""; + if (id) { + metadata.set(id, { + id, + displayName: stringOrNull(identity?.displayName), + repoName: stringOrNull(identity?.repoName), + repoPath: stringOrNull(identity?.repoPath), + identityKind: stringOrNull(identity?.identityKind), + faceStatePath: stringOrNull(identity?.faceStatePath), + personaStatePath: stringOrNull(identity?.personaStatePath), + description: stringOrNull(identity?.description), + avatarUrl: stringOrNull(identity?.avatarUrl), + avatarPath: stringOrNull(identity?.avatarPath), + channelPermissions: Array.isArray(identity?.channelPermissions) ? identity.channelPermissions : [], + }); + } + } + return metadata; +} + +async function readFaceStates(identityMetadata) { + const states = new Map(); + let core = null; + try { + const requireCore = createRequire(resolve(repoRoot, "packages", "core", "package.json")); + core = requireCore(resolve(repoRoot, "packages", "core", "dist", "index.js")); + } catch (error) { + for (const [id, identity] of identityMetadata.entries()) { + states.set(id, { + readable: false, + path: identity.faceStatePath ?? null, + error: `Core typed-state loader unavailable: ${error instanceof Error ? error.message : String(error)}`, + tree: [], + }); + } + return states; + } + + for (const [id, identity] of identityMetadata.entries()) { + if (identity.identityKind === "native_persona") { + states.set(id, readPersonaStateSummary(identity)); + continue; + } + + if (!identity.faceStatePath) { + states.set(id, { + readable: false, + path: null, + error: "No Face state path registered.", + tree: [], + }); + continue; + } + try { + const typedState = await core.loadVoidSelfStateTypedDocuments({ + canonicalPath: identity.faceStatePath, + identity: { + agentId: id, + publicName: identity.displayName ?? id, + publicDescription: identity.description ?? undefined, + }, + }); + const rendered = core.buildVoidSelfStateContext + ? core.buildVoidSelfStateContext(typedState, { + sourcePath: identity.faceStatePath, + identity: { + agentId: id, + publicName: identity.displayName ?? id, + publicDescription: identity.description ?? undefined, + }, + }) + : null; + states.set(id, { + readable: true, + path: identity.faceStatePath, + summary: truncate(rendered?.summary ?? "", 1200), + counts: countFaceState(typedState), + restState: projectRestState(typedState), + tree: buildFaceStateTree(typedState), + }); + } catch (error) { + states.set(id, { + readable: false, + path: identity.faceStatePath, + error: error instanceof Error ? error.message : String(error), + tree: [], + }); + } + } + return states; +} + +function readPersonaStateSummary(identity) { + if (!identity.personaStatePath) { + return { + readable: false, + path: null, + error: "No Persona state path registered.", + tree: [], + }; + } + + try { + const raw = readFileSync(resolve(identity.personaStatePath), "utf8"); + const state = JSON.parse(stripBom(raw)); + return { + readable: true, + kind: "native_persona", + path: identity.personaStatePath, + summary: truncate(state.publicDescription ?? state.presentation?.voiceSummary ?? "", 1200), + counts: { + memory: state.thoughtMemory?.memories?.length ?? 0, + pressures: state.agencyPressure?.pressures?.length ?? 0, + needs: state.affect?.needs?.length ?? 0, + statusReads: state.affect?.statusReads?.length ?? 0, + doctrine: state.affect?.doctrineStances?.length ?? 0, + }, + tree: buildPersonaStateTree(state), + }; + } catch (error) { + return { + readable: false, + kind: "native_persona", + path: identity.personaStatePath, + error: error instanceof Error ? error.message : String(error), + tree: [], + }; + } +} + +function buildPersonaStateTree(state) { + return [ + node("Persona", "persona", [ + leaf("Public Name", state.publicName), + leaf("Description", state.publicDescription), + leaf("Voice", state.presentation?.voiceSummary), + collectionNode("Values", "values", state.values, valueNode), + objectNode("Activation", "activationProfile", state.activationProfile), + collectionNode("Private Notes", "privateNotes", state.privateNotes, valueNode), + ]), + node("Thought Memory", "thoughtMemory", [ + collectionNode("Short Term", "thoughtMemory.shortTerm", state.thoughtMemory?.shortTerm, memoryNode), + collectionNode("Durable Memories", "thoughtMemory.memories", state.thoughtMemory?.memories, memoryNode), + collectionNode("Incubation", "thoughtMemory.incubation", state.thoughtMemory?.incubation, memoryNode), + ]), + node("Affect", "affect", [ + collectionNode("Needs", "affect.needs", state.affect?.needs, valueNode), + collectionNode("Status Reads", "affect.statusReads", state.affect?.statusReads, valueNode), + collectionNode("Doctrine", "affect.doctrineStances", state.affect?.doctrineStances, valueNode), + ]), + ]; +} + +function projectRestState(typedState) { + const sleepCycle = typedState?.scheduledRuntime?.sleepCycle; + if (!sleepCycle || typeof sleepCycle !== "object") { + return null; + } + return { + isNapping: sleepCycle.isNapping === true, + napEndsAt: stringOrNull(sleepCycle.currentNapEndsAt), + nextNapStartsAt: stringOrNull(sleepCycle.nextNapStartsAt), + activeDreamThemes: Array.isArray(sleepCycle.activeDreamThemes) + ? sleepCycle.activeDreamThemes.slice(0, 8).map(String) + : [], + }; +} + +function countFaceState(typedState) { + return { + shortTerm: typedState?.thoughtMemory?.shortTerm?.length ?? 0, + memories: typedState?.thoughtMemory?.memories?.length ?? 0, + incubation: typedState?.thoughtMemory?.incubation?.length ?? 0, + bonds: typedState?.faceAffect?.socialBonds?.length ?? 0, + pressures: typedState?.agencyPressure?.pressures?.length ?? 0, + candidates: typedState?.candidateInterventions?.interventions?.length ?? 0, + }; +} + +function buildFaceStateTree(typedState) { + return [ + node("Self Profile", "selfProfile", [ + leaf("Public Name", typedState?.selfProfile?.publicName), + leaf("Description", typedState?.selfProfile?.publicDescription), + collectionNode("Values", "selfProfile.values", typedState?.selfProfile?.values, valueNode), + objectNode("Activation", "selfProfile.activationProfile", typedState?.selfProfile?.activationProfile), + collectionNode("Private Notes", "selfProfile.privateNotes", typedState?.selfProfile?.privateNotes, valueNode), + ]), + node("Thought Memory", "thoughtMemory", [ + collectionNode("Short Term", "thoughtMemory.shortTerm", typedState?.thoughtMemory?.shortTerm, memoryNode), + collectionNode("Durable Memories", "thoughtMemory.memories", typedState?.thoughtMemory?.memories, memoryNode), + collectionNode("Incubation", "thoughtMemory.incubation", typedState?.thoughtMemory?.incubation, memoryNode), + ]), + node("Affect", "faceAffect", [ + collectionNode("Needs", "faceAffect.needs", typedState?.faceAffect?.needs, valueNode), + collectionNode("Social Bonds", "faceAffect.socialBonds", typedState?.faceAffect?.socialBonds, memoryNode), + collectionNode("Status Reads", "faceAffect.statusReads", typedState?.faceAffect?.statusReads, memoryNode), + objectNode("Mood", "faceAffect.moodDimensions", typedState?.faceAffect?.moodDimensions), + collectionNode("Social Biases", "faceAffect.socialBiases", typedState?.faceAffect?.socialBiases, valueNode), + ]), + node("Agency", "agencyPressure", [ + collectionNode("Pressures", "agencyPressure.pressures", typedState?.agencyPressure?.pressures, memoryNode), + collectionNode("Candidates", "candidateInterventions.interventions", typedState?.candidateInterventions?.interventions, memoryNode), + ]), + node("Runtime", "scheduledRuntime", [ + objectNode("Sleep Cycle", "scheduledRuntime.sleepCycle", typedState?.scheduledRuntime?.sleepCycle), + objectNode("Speaking Pressure", "scheduledRuntime.speakingPressure", typedState?.scheduledRuntime?.speakingPressure), + objectNode("Last Runs", "scheduledRuntime.lastRuns", typedState?.scheduledRuntime?.lastRuns), + ]), + ].filter(Boolean); +} + +function node(label, path, children = []) { + const compactChildren = children.filter(Boolean); + return { kind: "branch", label, path, children: compactChildren, count: compactChildren.length }; +} + +function leaf(label, value, path = label) { + if (value === null || value === undefined || value === "") return null; + return { + kind: "leaf", + label, + path, + title: label, + preview: truncate(renderValuePreview(value), 120), + detail: renderValueDetail(value), + }; +} + +function collectionNode(label, path, values, projector) { + if (!Array.isArray(values) || values.length === 0) return node(label, path, [leaf("empty", "No entries.", `${path}.empty`)]); + return node(label, path, values.slice(0, 80).map((value, index) => projector(value, `${path}.${index}`, index))); +} + +function objectNode(label, path, value) { + if (!value || typeof value !== "object") return node(label, path, [leaf("empty", "No entries.", `${path}.empty`)]); + const children = Object.entries(value).slice(0, 80).map(([key, entry]) => { + if (entry && typeof entry === "object") return objectNode(key, `${path}.${key}`, entry); + return leaf(key, entry, `${path}.${key}`); + }); + return node(label, path, children); +} + +function valueNode(value, path, index) { + if (value && typeof value === "object") { + const title = value.label ?? value.name ?? value.kind ?? value.id ?? `entry ${index + 1}`; + return leaf(String(title), value, path); + } + return leaf(`entry ${index + 1}`, value, path); +} + +function memoryNode(value, path, index) { + if (!value || typeof value !== "object") return leaf(`entry ${index + 1}`, value, path); + const target = value.target?.label ?? value.target?.id ?? value.targetKind ?? value.target ?? value.kind; + const title = value.summary ?? value.claim ?? value.question ?? value.title ?? value.id ?? value.memoryId ?? `entry ${index + 1}`; + const label = target ? `${target}: ${title}` : title; + return { + kind: "leaf", + label: truncate(String(label), 120), + path, + title: String(title), + preview: truncate(value.claim ?? value.question ?? value.tension ?? value.actionImplication ?? value.summary ?? renderValuePreview(value), 180), + detail: renderMemoryDetail(value), + }; +} + +function renderMemoryDetail(value) { + const lines = []; + for (const key of ["kind", "summary", "claim", "question", "tension", "actionImplication", "status", "intensity", "createdAt", "updatedAt"]) { + if (value?.[key] !== null && value?.[key] !== undefined && value?.[key] !== "") { + lines.push(`${key}: ${renderValuePreview(value[key])}`); + } + } + if (value?.target) lines.push(`target: ${renderValuePreview(value.target)}`); + if (Array.isArray(value?.anchorRefs) && value.anchorRefs.length > 0) { + lines.push(`anchors:\n${value.anchorRefs.map((anchor) => `- ${renderValuePreview(anchor)}`).join("\n")}`); + } + if (Array.isArray(value?.tags) && value.tags.length > 0) lines.push(`tags: ${value.tags.join(", ")}`); + return lines.join("\n\n") || renderValueDetail(value); +} + +function renderValuePreview(value) { + if (typeof value === "string") return value; + if (typeof value === "number" || typeof value === "boolean") return String(value); + return JSON.stringify(value); +} + +function renderValueDetail(value) { + return typeof value === "string" ? value : JSON.stringify(value, null, 2); +} + +function projectPendingMention(mention) { + return { + identityId: stringOrNull(mention?.identityId), + sourceChannelId: stringOrNull(mention?.sourceChannelId), + sourceMessageId: stringOrNull(mention?.sourceMessageId), + createdAt: stringOrNull(mention?.createdAt), + prompt: truncate(String(mention?.prompt ?? mention?.content ?? ""), 220), + }; +} + +function deriveSwarmState({ heartbeat, orchestrator, paused, activeTurns }) { + if (!heartbeat.ok) return "missing"; + if (paused) return "paused"; + const organ = projectOrgans(orchestrator.value).find((entry) => entry.id === "repo-face-heartbeats"); + if (organ && organ.lastStatus && !["ok", "skipped_disabled"].includes(organ.lastStatus)) { + return "warning"; + } + if (activeTurns.length > 0) return "running"; + return "ready"; +} + +function deriveOrchestratorState(organs) { + if (organs.length === 0) return "missing"; + if (organs.some((organ) => ["failed", "error", "stalled"].includes(organ.lastStatus))) return "warning"; + if (organs.some((organ) => organ.lastStatus === "running")) return "running"; + return "ok"; +} + +async function writeCultMeshPublication(snapshot, eveState, storePath) { + try { + const cultPackages = loadCultPackages(); + if (!cultPackages) { + return { + writeStatus: "skipped", + writeError: "CultMesh/CultCache packages were not found in known local CultLib paths.", + }; + } + + const { CultMesh, defineDocumentType } = cultPackages; + const snapshotDefinition = defineDocumentType({ + type: snapshotDocumentType, + schemaName: snapshotDocumentType, + schemaId: snapshotSchemaId, + schemaVersion: snapshotSchemaId, + contentHash: snapshotSchemaId, + canonicalSchemaJson: JSON.stringify({ + schemaName: snapshotDocumentType, + schemaVersion: snapshotSchemaId, + description: "Redacted read-only VoidBot swarm state snapshot for CultMesh distribution.", + }), + global: true, + schema: { + parse(input) { + if (!input || typeof input !== "object") { + throw new Error("VoidBot swarm snapshot must be an object."); + } + return input; + }, + }, + }); + const providerDefinition = defineDocumentType({ + type: providerAdvertisementDocumentType, + schemaName: providerAdvertisementDocumentType, + schemaId: providerAdvertisementSchemaId, + schemaVersion: providerAdvertisementSchemaId, + contentHash: providerAdvertisementSchemaId, + global: true, + name: "providerId", + schema: { parse: parseObjectDocument("Eve provider advertisement") }, + }); + const surfaceDefinition = defineDocumentType({ + type: surfaceDocumentType, + schemaName: surfaceDocumentType, + schemaId: surfaceSchemaId, + schemaVersion: surfaceSchemaId, + contentHash: surfaceSchemaId, + global: true, + name: "providerId", + schema: { parse: parseObjectDocument("Eve surface state") }, + }); + const eveBindingDefinition = defineDocumentType({ + type: eveBindingDocumentType, + schemaName: eveBindingDocumentType, + schemaId: eveBindingSchemaId, + schemaVersion: eveBindingSchemaId, + contentHash: eveBindingSchemaId, + global: true, + name: "bindingId", + schema: { parse: parseObjectDocument("Eve interface binding") }, + }); + const node = await CultMesh.createNode(storePath, { + documents: [snapshotDefinition, providerDefinition, surfaceDefinition, eveBindingDefinition], + }); + const providerAdvertisement = buildProviderAdvertisement(snapshot); + const eveBinding = buildEveInterfaceBinding(snapshot, eveState); + await node.put(snapshotDefinition, "voidbot-swarm", snapshot); + await node.put(providerDefinition, providerId, providerAdvertisement); + await node.put(surfaceDefinition, providerId, eveState); + await node.put(eveBindingDefinition, providerId, eveBinding); + await node.flush?.(true); + return { + writeStatus: "ok", + storePath, + documents: [snapshotDocumentType, providerAdvertisementDocumentType, surfaceDocumentType, eveBindingDocumentType], + providerId, + verseId, + eveBinding: { + documentType: eveBindingDocumentType, + schemaId: eveBindingSchemaId, + key: providerId, + }, + eveDeckEndpoint: primaryCultMeshEndpoint(), + writtenAt: new Date().toISOString(), + }; + } catch (error) { + return { + writeStatus: "failed", + storePath, + writeError: error instanceof Error ? error.message : String(error), + }; + } +} + +function loadCultPackages() { + const candidates = [ + resolve(repoRoot, "..", "CultLib", "packages", "cultmesh-ts", "package.json"), + resolve(repoRoot, "..", "CultMeshTS", "package.json"), + ]; + for (const packageJson of candidates) { + if (!existsSync(packageJson)) { + continue; + } + try { + const requireCult = createRequire(packageJson); + const { CultMesh } = requireCult("cultmesh-ts/dist/index.js"); + const { defineDocumentType } = requireCult("cultcache-ts/dist/index.js"); + if (CultMesh && defineDocumentType) { + return { CultMesh, defineDocumentType }; + } + } catch { + continue; + } + } + return null; +} + +function parseObjectDocument(label) { + return (input) => { + if (!input || typeof input !== "object") { + throw new Error(`${label} must be an object.`); + } + return input; + }; +} + +function buildProviderAdvertisement(snapshot) { + const endpoints = [ + primaryCultMeshEndpoint(), + ]; + return { + schemaVersion: providerAdvertisementSchemaId, + providerId, + verseId, + title: "VoidBot Swarm", + description: "VoidBot swarm status, CTB order, controls, and selected Face state as an Eve/CultUI surface.", + version: String(snapshot.summary?.initiativeClock ?? Date.now()), + status: snapshot.summary?.state ?? "unknown", + updatedAt: snapshot.generatedAt, + endpoints, + provider: providerManifest(), + controlSurface: { + primary: primaryCultMeshEndpoint(), + controls: { + transport: "cultmesh-binding", + documentType: eveBindingDocumentType, + key: providerId, + }, + }, + documents: [{ + type: snapshotDocumentType, + schemaId: snapshotSchemaId, + key: "voidbot-swarm", + }, { + type: surfaceDocumentType, + schemaId: surfaceSchemaId, + key: providerId, + }, { + type: eveBindingDocumentType, + schemaId: eveBindingSchemaId, + key: providerId, + }], + }; +} + +function buildEveInterfaceBinding(snapshot, eveState) { + return { + schemaVersion: eveBindingSchemaId, + bindingId: providerId, + providerId, + verseId, + title: "VoidBot Swarm", + kind: "eve-cultui-surface", + updatedAt: snapshot.generatedAt, + authority: { + owner: "VoidBot swarm renderer", + sourceDocuments: [{ + type: snapshotDocumentType, + schemaId: snapshotSchemaId, + key: "voidbot-swarm", + }], + surfaceDocument: { + type: surfaceDocumentType, + schemaId: surfaceSchemaId, + key: providerId, + }, + controlOwner: "VoidBot heartbeat state", + }, + surface: eveState.surface, + stateSummary: { + state: snapshot.summary?.state ?? "unknown", + participantCount: snapshot.summary?.participantCount ?? 0, + activeTurnCount: snapshot.summary?.activeTurnCount ?? 0, + pendingMentionCount: snapshot.summary?.pendingMentionCount ?? 0, + nextIdentityId: snapshot.summary?.nextIdentityId ?? null, + nextDisplayName: snapshot.summary?.nextDisplayName ?? null, + lastTickAt: snapshot.summary?.lastTickAt ?? null, + }, + controls: [{ + id: "swarm.pause", + label: "Pause swarm", + command: "set-pause", + writes: { + documentType: eveBindingDocumentType, + key: providerId, + field: "controlIntent.pause", + }, + }, { + id: "swarm.cadence", + label: "Set cadence", + command: "set-cadence-multiplier", + writes: { + documentType: eveBindingDocumentType, + key: providerId, + field: "controlIntent.cadenceMultiplier", + }, + }, { + id: "swarm.force-turn", + label: "Pull Face forward", + command: "force-turn", + writes: { + documentType: eveBindingDocumentType, + key: providerId, + field: "controlIntent.manualTurnRequests", + }, + }], + rendererHints: { + preferredLowerings: ["eve-native", "nightwing-tui", "browser"], + viewportMode: "continuous-ops", + tileId: "voidbot-swarm", + minWidth: 112, + minHeight: 30, + preferredWidth: 156, + preferredHeight: 58, + priority: -30, + density: "continuous", + }, + lowerings: [{ + kind: "static-html", + role: "local-debug-artifact", + path: dashboardPath, + }], + }; +} + +function providerManifest() { + return { + id: providerId, + title: "VoidBot Swarm", + description: "Native Eve tab for VoidBot agent status, CTB order, selected Face state, and swarm controls.", + version: "1", + endpoint: primaryCultMeshEndpoint(), + capabilities: ["ctb", "agent-status", "state-tree", "swarm-controls", "cultmesh-snapshot"], + usesCultMesh: true, + transport: "CultMesh Eve interface binding.", + }; +} + +function buildEveProviderState(snapshot) { + const summary = snapshot.summary ?? {}; + const participants = Array.isArray(snapshot.participants) ? snapshot.participants : []; + const upcoming = Array.isArray(snapshot.upcomingTurns) ? snapshot.upcomingTurns : []; + const orchestrator = snapshot.orchestrator ?? {}; + const organs = Array.isArray(orchestrator.organs) ? orchestrator.organs : []; + const watchdog = findSnapshotOrgan(organs, "voidbot-operations-watchdog") ?? findSnapshotOrgan(organs, "watchdog"); + const nodes = [ + { + id: "voidbot-swarm", + label: `VoidBot Swarm\n${summary.state ?? "unknown"}`, + kind: "cultmesh-verse", + visible: true, + health: summary.state ?? "unknown", + detail: `agents ${summary.participantCount ?? participants.length}; next ${summary.nextDisplayName ?? "none"}`, + }, + ...participants.slice(0, 24).map((participant, index) => ({ + id: `face-${participant.identityId}`, + label: `${participant.displayName}\n${participant.repoName}`, + kind: "repo-face", + visible: true, + x: -0.9 + (index % 6) * 0.36, + y: -0.3 + Math.floor(index / 6) * 0.22, + z: 0, + rotation: 0, + scale: 1, + width: 0.26, + height: 0.16, + health: participant.activeJobId ? "running" : participant.restState?.isNapping ? "napping" : participant.status, + detail: `next ${participant.nextTurnInMinutes ?? "?"}m; heat ${participant.heat ?? "?"}`, + })), + ]; + return { + type: "dashboard-state", + schema: "mimir.eve_dashboard_state.v1", + providerId, + title: "VoidBot Swarm", + version: Math.floor(Date.parse(snapshot.generatedAt ?? new Date().toISOString()) / 1000), + updatedAt: snapshot.generatedAt, + selectedNodeId: "voidbot-swarm", + lutPreset: "terminal", + nodes, + surface: { + schema: "gamecult.eve.surface.v1", + id: "voidbot.swarm.surface", + title: "VoidBot Swarm", + root: eveNode("voidbot-cockpit", "cockpit", { + title: "VoidBot Swarm", + status: summary.state ?? "unknown", + layout: { + direction: "vertical", + overflow: "scroll", + gap: 8, + padding: 8, + grow: 5, + minWidth: 112, + minHeight: 30, + preferredWidth: 156, + preferredHeight: 58, + priority: -30, + density: "continuous", + viewportMode: "continuous-ops", + }, + }, [ + eveNode("ctb-rail", "rail", { + title: "CTB order", + layout: { direction: "horizontal", overflow: "scroll-x", height: 112, gap: 8 }, + }, upcoming.slice(0, 14).map((turn, index) => + eveNode(`turn-${stableId(turn.identityId)}-${index}`, "avatar", { + text: turn.displayName, + assetUri: turn.avatarUrl, + status: turnState(turn), + detail: `${turn.repoName ?? "repo"} / ${minutesText(turn.nextTurnInMinutes)}`, + }), + )), + eveNode("voidbot-ops-row", "row", { + title: "Operations", + layout: { direction: "horizontal", gap: 8, grow: 1 }, + }, [ + eveNode("upcoming-faces-pane", "pane", { title: "Next Faces" }, upcoming.slice(0, 10).map((turn, index) => + eveNode(`upcoming-face-${index}-${stableId(turn.identityId)}`, "text", { + role: "mono", + text: `${String(index + 1).padStart(2, " ")}. ${String(turn.displayName ?? turn.identityId ?? "face").padEnd(10, " ")} ${turnState(turn).padEnd(8, " ")} ${turn.repoName ?? "repo"} spd ${formatNumber(turn.effectiveSpeed, 3)} heat ${formatNumber(turn.heat, 2)}`, + }), + )), + eveNode("voidbot-watchdog-pane", "pane", { title: "VoidBot Watchdog" }, [ + eveNode("watchdog-summary", "text", { + role: "mono", + text: `orchestrator ${orchestrator.state ?? "unknown"} organs ${organs.length}`, + }), + watchdog + ? eveNode("watchdog-status", "text", { + role: "strong", + text: `watchdog ${watchdog.lastStatus ?? "unknown"} exit ${watchdog.lastExitCode ?? 0}\nlast ${shortIso(watchdog.lastFinishedAt ?? watchdog.lastStartedAt)}`, + }) + : eveNode("watchdog-missing", "text", { + role: "caption", + text: "watchdog organ not present in snapshot", + }), + ...organs + .slice() + .sort((left, right) => { + const leftWatchdog = String(left.id ?? "").toLowerCase() === "voidbot-operations-watchdog" ? 0 : 1; + const rightWatchdog = String(right.id ?? "").toLowerCase() === "voidbot-operations-watchdog" ? 0 : 1; + return leftWatchdog - rightWatchdog || String(left.id ?? "").localeCompare(String(right.id ?? "")); + }) + .slice(0, 7) + .map((organ) => eveNode(`watchdog-organ-${stableId(organ.id)}`, "text", { + role: "caption", + text: `${organ.label ?? organ.id ?? "organ"}: ${organ.lastStatus ?? "unknown"}`, + })), + ]), + ]), + eveNode("voidbot-workspace", "row", { + title: "Swarm State", + layout: { direction: "horizontal", gap: 8, grow: 2 }, + }, [ + eveNode("voidbot-summary", "text", { + role: "mono", + text: [ + "VoidBot Swarm", + `${summary.state ?? "unknown"} next ${summary.nextDisplayName ?? "none"}`, + `agents ${summary.participantCount ?? participants.length} ready ${summary.readyNowCount ?? 0} cadence x${summary.cadenceMultiplier ?? 1}`, + `mesh ${snapshot.cultMesh?.writeStatus ?? "pending"} ${snapshot.generatedAt}`, + ].join("\n"), + }), + eveNode("voidbot-participants", "list", { title: "Faces" }, participants.slice(0, 16).map((participant) => + eveNode(`participant-${stableId(participant.identityId)}`, "row", { + title: participant.displayName, + detail: `${participant.repoName} / ${participant.status} / heat ${participant.heat ?? "?"}`, + }), + )), + ]), + ]), + assets: participants + .filter((participant) => participant.avatarUrl) + .slice(0, 32) + .map((participant) => ({ + id: `avatar-${participant.identityId}`, + kind: "image", + uri: participant.avatarUrl, + })), + }, + }; +} + +function eveNode(id, kind, props = {}, children = []) { + return { id, kind, props, children }; +} + +function stableId(value) { + return String(value ?? "id") + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") || "id"; +} + +function turnState(turn) { + if (turn?.activeJobId) return "active"; + if ((turn?.pendingMentionCount ?? 0) > 0) return "mention"; + if (turn?.restState?.isNapping) return "nap"; + return minutesText(turn?.nextTurnInMinutes); +} + +function minutesText(value) { + return typeof value === "number" + ? value <= 0 ? "ready" : `${value.toFixed(value < 10 ? 1 : 0)}m` + : "unknown"; +} + +function formatNumber(value, decimals) { + return typeof value === "number" && Number.isFinite(value) ? value.toFixed(decimals).replace(/\.?0+$/, "") : "?"; +} + +function shortIso(value) { + const timestamp = Date.parse(value ?? ""); + if (!Number.isFinite(timestamp)) return "unknown"; + const date = new Date(timestamp); + return `${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`; +} + +function findSnapshotOrgan(organs, needle) { + const lower = String(needle).toLowerCase(); + return organs.find((organ) => + String(organ?.id ?? "").toLowerCase().includes(lower) || + String(organ?.label ?? "").toLowerCase().includes(lower)); +} + +function renderHtml(snapshot) { + return ` + + + + + VoidBot Swarm Control + + + +
+ + + + +`; +} + +function redactSnapshot(snapshot) { + const redacted = JSON.parse(JSON.stringify(snapshot)); + redacted.visibility = "public_redacted"; + redacted.sources = { + heartbeatStateReadable: snapshot.sources?.heartbeatStateReadable ?? false, + heartbeatStateUpdatedAt: snapshot.sources?.heartbeatStateUpdatedAt ?? null, + orchestratorReadable: snapshot.sources?.orchestratorReadable ?? false, + orchestratorUpdatedAt: snapshot.sources?.orchestratorUpdatedAt ?? null, + pauseReadable: snapshot.sources?.pauseReadable ?? false, + }; + redacted.cultMesh = { + documentType: snapshot.cultMesh?.documentType, + schemaId: snapshot.cultMesh?.schemaId, + documentKey: snapshot.cultMesh?.documentKey, + writeStatus: snapshot.cultMesh?.writeStatus, + writtenAt: snapshot.cultMesh?.writtenAt, + note: "Public redacted snapshot. Local paths and job ids are withheld.", + }; + redacted.participants = redacted.participants.map((participant) => ({ + ...participant, + activeJobId: participant.activeJobId ? "active" : null, + constraints: [], + constraintCount: participant.constraintCount, + description: participant.description ? truncate(participant.description, 220) : null, + faceStatePath: null, + faceState: participant.faceState + ? { + readable: participant.faceState.readable, + counts: participant.faceState.counts, + path: null, + tree: [], + } + : undefined, + })); + redacted.activeTurns = redacted.activeTurns.map((participant) => ({ + ...participant, + activeJobId: participant.activeJobId ? "active" : null, + constraints: [], + })); + redacted.pendingMentions = redacted.pendingMentions.map((mention) => ({ + identityId: mention.identityId, + createdAt: mention.createdAt, + })); + redacted.orchestrator.organs = redacted.orchestrator.organs.map((organ) => ({ + ...organ, + lastLogPath: null, + })); + redacted.recentEvents = redacted.recentEvents.map((event) => ({ + ...event, + activeJobId: event.activeJobId ? "active" : null, + statusPath: null, + })); + return redacted; +} + +function primaryCultMeshEndpoint() { + return `cultmesh://${verseId}/eve/providers/${providerId}`; +} + +async function readJsonFile(path) { + try { + const raw = await readFile(path, "utf8"); + return { ok: true, value: JSON.parse(stripBom(raw)) }; + } catch (error) { + return { + ok: false, + value: null, + error: error instanceof Error ? error.message : String(error), + }; + } +} + +async function getMtime(path) { + try { + return (await stat(path)).mtime.toISOString(); + } catch { + return null; + } +} + +async function readDotEnv(path) { + const values = {}; + try { + const raw = await readFile(path, "utf8"); + for (const line of raw.split(/\r?\n/u)) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const separator = line.indexOf("="); + if (separator < 1) continue; + values[line.slice(0, separator).trim()] = line.slice(separator + 1); + } + } catch {} + return values; +} + +function resolveConfigPath(value, fallback) { + const raw = typeof value === "string" && value.trim() ? value.trim() : fallback; + return resolve(repoRoot, raw); +} + +function parseArgs(argv) { + const parsed = {}; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === "--serve") parsed.serve = true; + else if (arg === "--open") parsed.open = true; + else if (arg.startsWith("--")) { + const key = arg.slice(2); + const next = argv[index + 1]; + if (next && !next.startsWith("--")) { + parsed[key] = next; + index += 1; + } else { + parsed[key] = "true"; + } + } + } + return parsed; +} + +function isFlagEnabled(value) { + return value === true || value === "true" || value === "1" || value === "yes" || value === "on"; +} + +function stripBom(value) { + return value.charCodeAt(0) === 0xfeff ? value.slice(1) : value; +} + +function numberOrNull(value) { + return typeof value === "number" && Number.isFinite(value) ? value : null; +} + +function stringOrNull(value) { + return typeof value === "string" && value.length > 0 ? value : null; +} + +function round(value, places) { + const factor = 10 ** places; + return Math.round(value * factor) / factor; +} + +function truncate(value, maxLength) { + return value.length > maxLength ? `${value.slice(0, maxLength - 3)}...` : value; +} + +function escapeScriptJson(value) { + return value.replace(//g, "\\u003e").replace(/&/g, "\\u0026"); +} diff --git a/scripts/run-repo-face-heartbeats.ts b/scripts/run-repo-face-heartbeats.ts index cce0ea2..2e31158 100644 --- a/scripts/run-repo-face-heartbeats.ts +++ b/scripts/run-repo-face-heartbeats.ts @@ -46,7 +46,7 @@ const MAX_FACE_IMAGE_ATTACHMENT_BYTES = 12 * 1024 * 1024; interface FaceHeartbeatParticipant { identityId: string; - participantKind: "repo_face" | "system_agent"; + participantKind: "repo_face" | "native_persona" | "system_agent"; turnKind: "repo_face_rumination" | "void_moderation"; repoName: string; displayName: string; @@ -185,6 +185,7 @@ async function main(): Promise { state.history = state.history.slice(-80); if (!dryRun) { await writeHeartbeatState(config.repoFaceHeartbeats.statePath, state); + publishSwarmDashboardSurface(); } process.stdout.write( `${JSON.stringify({ @@ -210,6 +211,7 @@ async function main(): Promise { }); state.history = state.history.slice(-80); await writeHeartbeatState(config.repoFaceHeartbeats.statePath, state); + publishSwarmDashboardSurface(); return; } @@ -370,6 +372,7 @@ async function main(): Promise { state.lastTickAt = now.toISOString(); if (!dryRun) { await writeHeartbeatState(config.repoFaceHeartbeats.statePath, state); + publishSwarmDashboardSurface(); } process.stdout.write( `${JSON.stringify({ @@ -431,7 +434,9 @@ function buildParticipantSpecs(identities: RepoDiscordIdentity[]): ParticipantSp }, ...identities.map((identity) => ({ id: identity.id, - participantKind: "repo_face" as const, + participantKind: identity.identityKind === "native_persona" + ? "native_persona" as const + : "repo_face" as const, turnKind: "repo_face_rumination" as const, repoName: identity.repoName, displayName: identity.displayName, @@ -492,15 +497,17 @@ async function queueRepoFaceTurn(input: { } const contextBuilder = new ContextBuilder(); - await ensureRepoFaceInitialized({ - identity, - storageRoot: input.config.storageRoot, - sourceRepoRoot: input.config.sourceRepoRoot, - epiphanyAgentRoot: input.config.epiphanyAgentRoot, - workspaceRoot: process.cwd(), - birthMode: input.config.repoFaceBirthMode, - birthExecutor: input.config.repoFaceBirthExecutor, - }); + if (identity.identityKind !== "native_persona") { + await ensureRepoFaceInitialized({ + identity, + storageRoot: input.config.storageRoot, + sourceRepoRoot: input.config.sourceRepoRoot, + epiphanyAgentRoot: input.config.epiphanyAgentRoot, + workspaceRoot: process.cwd(), + birthMode: input.config.repoFaceBirthMode, + birthExecutor: input.config.repoFaceBirthExecutor, + }); + } const recentMessages = await fetchRecentDiscordMessages({ botToken: input.config.botToken, channelId, @@ -514,7 +521,7 @@ async function queueRepoFaceTurn(input: { limit: 6, bifrostDiscordChannelId: input.config.bifrostDiscordChannelId, }); - const bifrostDigest = input.config.repoFaceBifrostEnabled + const bifrostDigest = input.config.repoFaceBifrostEnabled && identity.identityKind !== "native_persona" ? await fetchBifrostGovernanceDigest({ bifrostRoot: input.config.bifrostRoot, repoName: identity.repoName, @@ -533,7 +540,9 @@ async function queueRepoFaceTurn(input: { roomContext, humanPronounGuidance, ); - const repoActivitySurface = renderRepoFaceRepoActivitySurface(identity, input.config); + const repoActivitySurface = identity.identityKind === "native_persona" + ? renderNativePersonaBodySurface(identity) + : renderRepoFaceRepoActivitySurface(identity, input.config); const globalAgentDoctrine = await loadGlobalAgentDoctrine(); const conversationMemorySurface = renderRepoFaceConversationTranscript({ identity, @@ -611,6 +620,10 @@ async function loadRepoFaceRestStates( const now = new Date(); for (const identity of identities) { + if (identity.identityKind === "native_persona") { + continue; + } + try { const statePath = resolveRepoFaceStatePath(identity, storageRoot); const typedState = await loadVoidSelfStateTypedDocuments({ @@ -1932,6 +1945,10 @@ async function renderRepoFaceMemorySurfaceForTurn( }, humanPronounGuidance?: RepoFaceHumanPronounGuidance[], ): Promise { + if (identity.identityKind === "native_persona") { + return renderNativePersonaMemorySurface(identity); + } + const statePath = resolveRepoFaceStatePath(identity, config.storageRoot); const typedState = await loadVoidSelfStateTypedDocuments({ canonicalPath: statePath }); const curiosityGraphFacts = roomContext @@ -1956,6 +1973,137 @@ async function renderRepoFaceMemorySurfaceForTurn( }); } +async function renderNativePersonaMemorySurface(identity: RepoDiscordIdentity): Promise { + const personaStatePath = identity.personaStatePath; + if (!personaStatePath) { + return [ + `${identity.displayName} is a native VoidBot Persona, not a repo Face.`, + "No Persona state path is registered. Treat that as a Body fault and keep the public turn modest.", + ].join("\n"); + } + + const raw = JSON.parse(stripLeadingBom(await readFile(resolve(personaStatePath), "utf8"))) as unknown; + const state = isRecord(raw) ? raw : {}; + const profile = readRecord(state, "profile") ?? readRecord(state, "selfProfile") ?? state; + const presentation = readRecord(state, "presentation"); + const memory = readRecord(state, "memory") ?? readRecord(state, "thoughtMemory"); + const affect = readRecord(state, "affect") ?? readRecord(state, "faceAffect"); + const doctrine = readRecord(state, "doctrine") ?? readRecord(state, "doctrineStances"); + + const lines = [ + `${identity.displayName} is a native VoidBot Persona, not a repo Face.`, + `Persona state: ${resolve(personaStatePath)}`, + identity.avatarUrl ? `Public avatar URL: ${identity.avatarUrl}` : undefined, + identity.avatarPath ? `Local avatar asset: ${identity.avatarPath}` : undefined, + stringField(profile, "publicDescription") ?? identity.description, + listSection("Private notes", arrayField(profile, "privateNotes")), + valueSection("Values", arrayField(profile, "values")), + listSection("Activation traits", arrayField(profile, "activationTraits")), + memorySection("Memories", [ + ...arrayField(memory, "memories"), + ...arrayField(memory, "durableMemories"), + ]), + memorySection("Short-term residue", arrayField(memory, "shortTerm")), + memorySection("Agency pressure", [ + ...arrayField(state, "pressures"), + ...arrayField(state, "agencyPressures"), + ...arrayField(readRecord(state, "agencyPressure"), "pressures"), + ]), + memorySection("Affect needs", arrayField(affect, "needs")), + memorySection("Social bonds", arrayField(affect, "socialBonds")), + memorySection("Doctrine stances", [ + ...arrayField(state, "doctrineStances"), + ...arrayField(doctrine, "stances"), + ...arrayField(doctrine, "doctrineStances"), + ]), + ].filter((line): line is string => typeof line === "string" && line.trim().length > 0); + + return lines.join("\n\n"); +} + +function renderNativePersonaBodySurface(identity: RepoDiscordIdentity): string { + return [ + `${identity.displayName} is a native VoidBot Persona.`, + "Body for this turn: Persona state, avatar, allowed Discord channels, current conversation, and VoidBot's webhook mouth.", + "No repo jurisdiction, Bifrost governance digest, source-repo activity, or repo proposal authority is implied by this native Persona category.", + ].join("\n"); +} + +function readRecord(value: unknown, key: string): Record | undefined { + if (!isRecord(value)) { + return undefined; + } + const child = value[key]; + return isRecord(child) ? child : undefined; +} + +function stringField(value: unknown, key: string): string | undefined { + if (!isRecord(value)) { + return undefined; + } + const child = value[key]; + return typeof child === "string" && child.trim().length > 0 ? child.trim() : undefined; +} + +function arrayField(value: unknown, key: string): unknown[] { + if (!isRecord(value)) { + return []; + } + const child = value[key]; + return Array.isArray(child) ? child : []; +} + +function listSection(title: string, entries: unknown[]): string | undefined { + const rendered = entries + .map((entry) => typeof entry === "string" ? entry : summarizeRecord(entry)) + .filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0) + .slice(0, 12); + return rendered.length > 0 + ? [`${title}:`, ...rendered.map((entry) => `- ${entry}`)].join("\n") + : undefined; +} + +function valueSection(title: string, entries: unknown[]): string | undefined { + return listSection(title, entries.map((entry) => { + if (!isRecord(entry)) { + return entry; + } + const label = stringField(entry, "label") ?? stringField(entry, "id") ?? stringField(entry, "name"); + const summary = stringField(entry, "summary") ?? stringField(entry, "description"); + return [label, summary].filter(Boolean).join(": "); + })); +} + +function memorySection(title: string, entries: unknown[]): string | undefined { + return listSection(title, entries.map((entry) => { + if (!isRecord(entry)) { + return entry; + } + return stringField(entry, "summary") ?? + stringField(entry, "claim") ?? + stringField(entry, "description") ?? + stringField(entry, "text") ?? + summarizeRecord(entry); + })); +} + +function summarizeRecord(value: unknown): string | undefined { + if (!isRecord(value)) { + return undefined; + } + for (const key of ["summary", "claim", "description", "text", "label", "id", "name"]) { + const field = stringField(value, key); + if (field) { + return field; + } + } + return undefined; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + function renderRepoFaceStatePacket( identity: RepoDiscordIdentity, state: VoidSelfStateTypedProjection, @@ -4675,6 +4823,25 @@ async function writeHeartbeatState(path: string, state: FaceHeartbeatState): Pro await writeFile(path, `${JSON.stringify(state, null, 2)}\n`, "utf8"); } +function publishSwarmDashboardSurface(): void { + const scriptPath = resolve(process.cwd(), "scripts", "render-voidbot-swarm-dashboard.mjs"); + const result = spawnSync(process.execPath, [scriptPath], { + cwd: process.cwd(), + encoding: "utf8", + windowsHide: true, + }); + if (result.status === 0) { + return; + } + + const stderr = result.stderr?.trim(); + const stdout = result.stdout?.trim(); + const detail = [stderr, stdout].filter(Boolean).join("\n"); + console.error( + `VoidBot swarm CultMesh surface publish failed with exit code ${result.status ?? "unknown"}${detail ? `:\n${detail}` : "."}`, + ); +} + function migrateLegacyHeartbeatState( parsed: Partial & { baseIntervalMinutes?: number; diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 149934b..473fcbe 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -320,3 +320,4 @@ {"ts":"2026-05-24T09:27:01Z","type":"implementation","status":"ok","note":"Corrected the Colossus organ doctrine after over-flattening Epiphany anatomy into Body/Mind and then over-expanding every Face into the whole organism. Global AGENTS now says agents must understand which organ they serve as; VoidBot is explicitly a prototype for Face. Repo Face prompts now frame each identity as a Face-shaped projection with Body/Mind/signal boundaries, while Eyes/Hands/Soul/Life/Imagination/Self/Body-modeling/nervous-system remain distinct Epiphany organs with their own architectures."} {"ts":"2026-05-24T09:35:42Z","type":"implementation","status":"ok","note":"Queried Epiphany's CultCache-backed public Face surface before updating prompt doctrine: Face is an embodied public surface with dense Ghostlight-style state, not the whole organism. Repo Face and global AGENTS prompts now preserve fractal anatomy: top-level Epiphany organs remain distinct, while each Face may have local Face-scale Self/Face/Imagination/Eyes/Body/Hands/Soul/Life/signal faculties. Projection smoke also exposed and fixed projector fallback classification for model/tool unsupported errors."} {"ts":"2026-06-02T14:51:41Z","type":"implementation","status":"ok","note":"Repo Face/Persona Codex turns now have read-only Odin visibility through VoidBot MCP tools list_odin_providers, list_odin_verses, and get_odin_surface. The repo-face MCP allowlist and prompt doctrine include those tools, preserving Odin as registry owner and leaving command authority outside Face turns."} +{"ts":"2026-06-02T14:52:23Z","type":"implementation","status":"ok","note":"Swarm silence on 2026-06-02 was not scheduler death: main lacked native_persona handling and the CultMesh swarm publisher, and worker speech parsing ignored natural 'Would say:' drafts. Fixed native Persona routing, restored swarm CultMesh/Eve publication, normalized draft speech to repo Face posts, restarted stack, and verified Aqua/Bifrost posted while Odin surface refreshed."}