Restore VoidBot swarm speech and dashboard publication

This commit is contained in:
Meta Vens 2026-06-02 15:53:17 +01:00
commit 12e566b62f
17 changed files with 2679 additions and 32 deletions

View file

@ -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 };

View file

@ -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<void> {
if (input.identity.identityKind === "native_persona") {
return;
}
await applyVoidSelfStateOperation(
{
canonicalPath: resolveRepoFaceStatePath(input.identity, config.storageRoot),

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View file

@ -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",

View file

@ -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<string, number>;
heatOverrides: Record<string, number>;
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,
},

View file

@ -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),

View file

@ -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(),
};
});

View file

@ -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(),

View file

@ -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,

View file

@ -141,6 +141,8 @@ export function createEmptyVoidSelfState(
socialBonds: [],
statusReads: [],
moodDimensions: [],
socialBiases: [],
doctrineStances: [],
updatedAt: createdAt,
}),
};

View file

@ -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<VoidSelfStateOperation, { operation: "retire_doctrine_stance" }>,
): 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],

View file

@ -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;

View file

@ -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;

View file

@ -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;

File diff suppressed because it is too large Load diff

View file

@ -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<void> {
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<void> {
});
state.history = state.history.slice(-80);
await writeHeartbeatState(config.repoFaceHeartbeats.statePath, state);
publishSwarmDashboardSurface();
return;
}
@ -370,6 +372,7 @@ async function main(): Promise<void> {
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<string> {
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<string> {
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<string, unknown> | 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<string, unknown> {
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<FaceHeartbeatState> & {
baseIntervalMinutes?: number;

View file

@ -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."}