Restore VoidBot swarm speech and dashboard publication
This commit is contained in:
parent
167a214098
commit
12e566b62f
17 changed files with 2679 additions and 32 deletions
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
BIN
assets/native-personas/tengu.png
Normal file
BIN
assets/native-personas/tengu.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -141,6 +141,8 @@ export function createEmptyVoidSelfState(
|
|||
socialBonds: [],
|
||||
statusReads: [],
|
||||
moodDimensions: [],
|
||||
socialBiases: [],
|
||||
doctrineStances: [],
|
||||
updatedAt: createdAt,
|
||||
}),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
2276
scripts/render-voidbot-swarm-dashboard.mjs
Normal file
2276
scripts/render-voidbot-swarm-dashboard.mjs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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."}
|
||||
|
|
|
|||
Loading…
Reference in a new issue