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 0000000..df00d27 Binary files /dev/null and b/assets/native-personas/tengu.png differ 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."}