Split Face turn and support model lanes

This commit is contained in:
Meta Vens 2026-05-21 23:59:35 +01:00
commit 58624a6bb6
11 changed files with 42 additions and 20 deletions

View file

@ -74,6 +74,8 @@ REPO_FACE_HEARTBEAT_INTERVAL_MINUTES=15
REPO_FACE_HEARTBEAT_MAX_JOBS_PER_TICK=2
REPO_FACE_HEARTBEAT_DEFAULT_CHANNEL_ID=
REPO_FACE_HEARTBEAT_CODEX_MODEL=gpt-5.3-codex-spark
REPO_FACE_HEARTBEAT_CODEX_MODELS=gpt-5.3-codex-spark,gpt-5.4-mini
REPO_FACE_TURN_CODEX_MODEL=gpt-5.4
REPO_FACE_HEARTBEAT_CODEX_REASONING_EFFORT=low
EPIPHANY_AGENT_ROOT=E:/Projects/EpiphanyAgent
BIFROST_ROOT=E:/Projects/Bifrost

View file

@ -494,11 +494,12 @@ async function executeProviderForJob(
provider: ProviderAdapter,
job: JobRecord,
contextBundle: JobRecord["contextBundle"],
role: "face" | "interpreter" = "face",
): Promise<ProviderResponse> {
const request = provider.buildRequest(contextBundle, {
jobId: job.id,
command: job.command,
...repoFaceHeartbeatCodexOptions(job),
...repoFaceHeartbeatCodexOptions(job, role),
});
return provider.execute(request);
}
@ -619,7 +620,7 @@ async function interpretRepoFaceTurnOutput(
voidSelfState: undefined,
createdAt: new Date().toISOString(),
};
const response = await executeProviderForJob(provider, job, interpreterContext);
const response = await executeProviderForJob(provider, job, interpreterContext, "interpreter");
const interpretationText = normalizeModelText(response.outputText ?? response.summary);
return parseRepoFaceParentInterpretation(interpretationText, input.attempt);
}
@ -747,11 +748,20 @@ function dropRepoFaceActionBlocks(
};
}
function repoFaceHeartbeatCodexOptions(job: JobRecord): Record<string, string> {
function repoFaceHeartbeatCodexOptions(job: JobRecord, role: "face" | "interpreter"): Record<string, string> {
if (job.command !== "repo-face-rumination") {
return {};
}
if (role === "face") {
return {
model: config.repoFaceHeartbeats.turnCodexModel,
...(config.repoFaceHeartbeats.codexModelReasoningEffort
? { reasoningEffort: config.repoFaceHeartbeats.codexModelReasoningEffort }
: {}),
};
}
return {
...(config.repoFaceHeartbeats.codexModel ? { model: config.repoFaceHeartbeats.codexModel } : {}),
...(config.repoFaceHeartbeats.codexModels.length > 0

View file

@ -129,7 +129,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
- Repo Face state projection is no longer allowed to be deterministic schema slurry. `scripts/run-repo-face-heartbeats.ts` builds a compact state packet, then calls the model prompt at `prompts/repo-face-state-projector.prompt.md` to turn it into character-facing prose before the Face sees it. `REPO_FACE_STATE_PROJECTOR_ENABLED=false` exists only as an emergency isolation switch; both the model output and fallback packet reject schema/prompt-construction leaks such as `grants:`, `jurisdictions:`, `Face of`, repo paths, and "do not prompt" phrasing.
- First role-addressed chat also initializes the target repo's local `.voidbot` home. `ensureRepoFaceInitialized` resolves `repoPath` from the registry, `SOURCE_REPO_ROOT`, or `E:/Projects/<repoName>`, creates `voice/`, `state/`, `birth/`, and `logs/`, writes `voice/identity.json`, then starts `scripts/run-repo-face-birth.mjs` detached. That worker runs EpiphanyAgent's `epiphany-repo-personality scout` and `epiphany-repo-birth-runner` so newborn personality/memory/trajectory packets come from repo contents and git history. Status and logs live in the target repo under `.voidbot/birth/voidbot-face-birth.json` and `.voidbot/logs/voidbot-face-birth.log`.
- Agent heartbeats now use one CTB/Final-Fantasy-X-style initiative queue. `scripts/run-repo-face-heartbeats.ts` still has its old filename, but it now reconciles built-in system agents plus registered repo Faces, stores the roster at `REPO_FACE_HEARTBEAT_STATE_PATH`, advances virtual initiative time independently of wall-clock due dates, and dispatches selected turns. Repo Faces queue normal `repo-face-rumination` jobs; built-in `void` starts the existing moderation rumination runner as its turn. Active jobs/locks freeze initiative: recovery starts only after the owner-Codex job or Void moderation lock clears, and completion cannot grant another turn in the same pulse. The active-job scan now marks stale approved/running Face jobs failed through the job queue and records `stale_active_turn_recovered`, so a dead worker claim cannot freeze one Face forever. `GameCult Local Orchestrator` is the live hidden local scheduled task that ticks the scheduler; `npm run state:install-repo-face-heartbeat-task` remains an isolation/backcompat installer, not the normal pulse owner. Heat knobs are `REPO_FACE_HEARTBEAT_GLOBAL_HEAT` and `REPO_FACE_HEARTBEAT_HEAT_OVERRIDES`; overrides can target `all`, `kind:<participantKind>`, `turn:<turnKind>`, `identity:<id>`, `repo:<repo>`, `display:<name>`, `channel:<id>`, or bare identity/repo/display keys. Repo Face prompts now include a jurisdiction-dive cadence so unattended heartbeats periodically inspect repo/history terrain with source tools and convert real findings into state, proposals, essay seeds, agency pressure, or speech.
- Repo Face heartbeat prompt assembly now has two model projection boundaries before the child Face sees context: `repo-face-state-projector.prompt.md` turns typed Face state into character-facing memory/affect prose, and `repo-face-conversation-projector.prompt.md` turns recent room messages, nearby channel snapshots, and pending mentions into lived conversational memory. This fixed the failure where turns were busy privately but still saw `No Interpreter-shaped conversation memory was projected for this turn`, so direct Aquarium invitations to speak never reached the character as social pressure. The conversation projector is allowed to mention domain words such as schemas; its leak guard should reject actual transcript/mechanics metadata, not Libby's vocabulary.
- Repo Face heartbeat prompt assembly now has one model projection boundary before the child Face sees context: `repo-face-state-projector.prompt.md` turns typed Face state into character-facing memory/affect/social prose. Recent room messages, nearby channel snapshots, and pending mentions are now deterministic raw transcript sections, oldest-to-newest, with explicit recency/correction rules. The deleted conversation projector was turning evidence into mushy consensus; do not revive it. The Face model must read human corrections directly. Model ownership is split: Face turns default to full `gpt-5.4`; state projection and parent Interpreter passes use the cheaper Spark/Mini support chain.
- Worker delivery must treat Interpreter channel names as Face-local labels, not raw Discord ids. `SAY channel: aquarium` or `channel: narrative` resolves through the registered identity's `channelPermissions` before allow-list checks and webhook delivery. Earlier live Aqua/Nibu turns parsed `say=1` but did not post because the worker literally checked whether the identity was allowed in channel id `aquarium`; the post path now resolves labels and has a live Epiphany webhook receipt after restart.
- Repo Face rest projection now persists normalized sleep cycles back into each Face `.cc` state during heartbeat loading. Missed nap windows are treated as already missed/completed and rolled forward instead of starting a fresh nap at inspection time. This fixes the Aqua/Mimir/Epiphany failure mode where stale `nextNapStartsAt` values stayed raw, fresh projections repeatedly marked them as napping, and ready selection skipped them forever while their initiative was overdue.
- Repo Face short-term memory distillation is now wired into the heartbeat/recovery loop. The runner starts repo-local memory maintenance when a Face is napping with short-term residue or has stale short-term residue older than six hours. Status/logs live under the target repo's own `.voidbot/status` and `.voidbot/logs`; the shared maintenance runner has `-ForceDistillation`, identity-neutral prompt wording, exact-schema warnings, and whole-batch schema validation before any operation applies. Live cleanup forced Nibu from 3 short-term memories to 0 and Mimir from 5 to 0; all registered repo Faces currently have 0 short-term residue.

View file

@ -170,7 +170,7 @@ This note is the source-grounded description of how the live VoidBot stack is sh
- Role mentions, display-name-prefix mentions, and human replies to a Face's webhook/persona message append pending Face obligations to `pendingMentions` in `REPO_FACE_HEARTBEAT_STATE_PATH`; ordinary bot mentions and DMs append pending obligations for the built-in `void` participant. They no longer create direct provider jobs from `MessageCreate`. The CTB scheduler temporarily prioritizes agents with pending mentions, attaches those prompts plus reply targets to the next `repo-face-rumination` or Void moderation pass, and consumes the pending items only after a turn is actually queued. Rumination jobs remain private unless they explicitly emit a parent-owned Face action block or a typed candidate intervention; the worker must never auto-post their summaries as the base bot. Heartbeat prompts now include the channel permission plan, recent context from non-primary permitted channels, social embodiment, jurisdiction respect, comedy/improv doctrine, and anti-template sampling guidance so a Face can jump into domain chatter, form durable social reads, consult the right steward, and answer people without copying nearby note formulas.
- Repo Face proposal/article PRs, PR comments, and registered-identity Discord posts now cross through Bifrost bridge commands. The child may emit `VOIDBOT_REPO_IDENTITY_PROPOSAL_PR` for consensus-needed repo/lore/design proposals, `VOIDBOT_REPO_IDENTITY_ARTICLE` for bylined essays, or `VOIDBOT_REPO_IDENTITY_PR_COMMENT` for signed arguments on existing proposal PRs. The worker validates the registered identity and records Face-local receipts, but `BIFROST_ROOT/tools/bifrost-bridge.mjs` owns draft PR creation, PR comments, and Discord persona posting. This is not merely helper placement: Bifrost's original governance/labor platform role makes it the owner of governed public crossings for GameCult work, including GitHub, Discord-native work/member/swarm interactions, dispatch receipts, CultNet/CultCache intake, and future collaboration interfaces. Heimdall is the OAuth/account/capability gate; Bifrost is the bridge/request/routing/receipt authority. VoidBot still owns room cognition, moderation judgment, archive/source retrieval, and Face personality; Bifrost owns Discord when Discord acts as the Labor Platform interface or swarm transport. If Nibu or another Face frames something as the next cut/pass and enough source or room material exists, the expected action is to draft the artifact, submit the PR, and announce it rather than continuing to nag the room.
- Repo Face turns now receive a Bifrost governance digest from `BIFROST_ROOT/tools/governance-threads.mjs digest --repo <repo> --agent <identity>` alongside recent Discord context. Bifrost typed CultCache topics/comments are the canonical home for feature requests, Face opinions, missing questions, approvals, and dispatch receipts; Discord is a mirror/evidence source. When `BIFROST_DISCORD_CHANNEL_ID` is configured for `#bifrost`, repo Face Discord context ignores mirrored bot chatter there because the authoritative activity arrives through the Bifrost digest. Human `#bifrost` messages should only become governance comments after Bifrost/Heimdall can link the Discord id to a registered actor. Repo-local asks that would send a human or Codex to add/fix/name/scaffold/document/test/investigate/implement belong on Bifrost with enough room/repo context for a claimed Codex turn; a Face may still speak characterfully in Aquarium, but not as the only dispatch surface. Legacy `UPDATE REQUEST` packets are parsed only as compatibility input and reconciled into Bifrost topics instead of immediate transport enqueue. Local `#bifrost` is channel `1506760343836430376`; work proposals, governance mirrors, and Bifrost dispatch receipts should go there, while Aquarium remains the casual/social swarm room.
- Repo character turns now have a two-Interpreter target shape. The state-in boundary is `prompts/repo-face-state-projector.prompt.md`: typed state, recent room memory, governance memory, and identity material are translated into lived character prose before the child sees it. The state projector must surface values, drives, wounds, fascinations, aesthetic taste, dignity, mood, needs, social reads, bonds, and agency pressure when the state contains them. Current-room and nearby-channel logs cross a matching conversation projection boundary, `prompts/repo-face-conversation-projector.prompt.md`, before the child sees them; raw transcript bullets do not belong in the child prompt. Deterministic code still owns full prompt assembly and inspection through `scripts/run-repo-face-heartbeats.ts --assemble-prompt <identity>`; that path composes stable doctrine, allowed rooms, research/archive capability guidance, Bifrost digest, a supplied Interpreter memory surface, and a supplied Interpreter conversation surface into the exact child prompt. It must not invent personality, mood, affect, or social memory from typed state. Situational obligations such as direct-call priority or first-public-introduction duty are emitted only when the scheduler packet proves they apply, not as standing prompt law. Until the first Spark/Codex Interpreter calls are wired into the live heartbeat, repo Face turns intentionally lack state/conversation-projected prose rather than using a fake substitute. The child character should receive projected memories, mood, obligations, allowed rooms, governance memory, and recent chat as prose rather than structured state. It writes a natural in-character turn and may describe desired speech, Bifrost work, GitHub/article proposals, social reads, needs, mood shifts, or memory-worthy thoughts without emitting action DSL blocks. The second Spark/Codex Interpreter decides route/retry/drop and translates warranted effects into structured `STATE NOTE`, `SAY`, and `BIFROST TOPIC` blocks for the worker. `STATE NOTE` blocks become typed short-term memory, affect needs, social bonds, status reads, mood dimensions, or agency pressure. The worker still accepts legacy compact JSON sentinels and legacy `UPDATE REQUEST` blocks for compatibility, but the live prompt path keeps transport and state syntax out of the character's roleplay surface.
- Repo character turns now have a state-projector plus turn-Interpreter shape. The state-in boundary is `prompts/repo-face-state-projector.prompt.md`: typed state and identity material are translated into lived character prose before the child sees it. The state projector must surface values, drives, wounds, fascinations, aesthetic taste, dignity, mood, needs, social reads, bonds, and agency pressure when the state contains them. Current-room and nearby-channel logs no longer cross a model conversation projector; deterministic code attaches raw oldest-to-newest transcript evidence with explicit recency/correction rules so the Face can notice human reframes directly. Deterministic code still owns full prompt assembly and inspection through `scripts/run-repo-face-heartbeats.ts --assemble-prompt <identity>`; that path composes stable doctrine, allowed rooms, research/archive capability guidance, Bifrost digest, projected memory/affect prose, and raw conversation evidence into the exact child prompt. It must not invent personality, mood, affect, or social memory from typed state. Situational obligations such as direct-call priority or first-public-introduction duty are emitted only when the scheduler packet proves they apply, not as standing prompt law. The child character writes a natural in-character turn on full `gpt-5.4` and may describe desired speech, repo/article proposals, social reads, needs, mood shifts, or memory-worthy thoughts without emitting action DSL blocks. The cheaper Spark/Mini parent Interpreter decides route/retry/drop and translates warranted effects into structured `STATE NOTE` and `SAY` blocks for the worker while governance dispatch remains paused. `STATE NOTE` blocks become typed short-term memory, affect needs, social bonds, status reads, mood dimensions, or agency pressure. The worker still accepts legacy compact JSON sentinels and legacy `UPDATE REQUEST` blocks for compatibility, but the live prompt path keeps transport and state syntax out of the character's roleplay surface.
- `scripts/run-repo-face-heartbeats.ts` now acts as the broader agent CTB/Final-Fantasy-X-style initiative scheduler. The wall-clock task is only a pulse; virtual initiative time chooses who gets the next turn. The scheduler stores per-agent speed, reaction bias, load, groups, heat, effective speed, next-turn time, active turn markers, and recent queue history at `REPO_FACE_HEARTBEAT_STATE_PATH`. Repo Faces queue `repo-face-rumination` owner-Codex jobs; built-in Void currently participates as the `void` system agent and starts the existing moderation rumination runner as its turn. Initiative freezes while an owner-Codex job or Void moderation lock is active; recovery is scheduled only after that active turn completes, and completion cannot grant another turn in the same scheduler pulse. The active-job scan recovers stale Face jobs by marking ancient approved/running rows failed through the job queue before building the active-turn map. Above-baseline heat bypasses repo Face nap gating, while active-turn freeze still prevents overlapping thoughts.
- The live wall-clock pulse for this scheduler is `scripts/run-gamecult-orchestrator.ps1`, installed as `GameCult Local Orchestrator`. `scripts/install-repo-face-heartbeat-task.ps1` remains available for isolated testing, but its per-organ scheduled task should stay disabled in normal operation. Turn frequency is controlled by `REPO_FACE_HEARTBEAT_BASE_RECOVERY_MINUTES`, `REPO_FACE_HEARTBEAT_GLOBAL_HEAT`, `REPO_FACE_HEARTBEAT_SPEED_OVERRIDES`, and `REPO_FACE_HEARTBEAT_HEAT_OVERRIDES`. Heat keys can target `all`, `kind:<participantKind>`, `turn:<turnKind>`, `identity:<id>`, `repo:<repo>`, `display:<name>`, `channel:<id>`, or the bare identity/repo/display key.
- The local ignored registry currently includes Nibu for `AetheriaLore`, with Face state in `E:/Projects/AetheriaLore/.voidbot/state/nibu.cc`, Discord role id `1505918221453103247`, and an Aquarium channel allow-list. Nibu's Face state now treats AetheriaLore-first map-building, abrasive character voice, consensus-backed change agency, her own `Aetheria/Lore/Nibu.md` character-note stewardship, Nibu-authored lore essays, and tangential setting fascination as separate active doctrines. That tangential fascination points at embodied ship minds, junkyard abandonment, murderous autonomy, salvage, and save-scumming survival. The repo Face heartbeat organ is now pulsed by `GameCult Local Orchestrator`; after restarting the stack onto the new worker code, forced heartbeat job `76020f1e-0c55-424b-9e35-89ba7f77fcc3` completed without base-bot auto-posting.

View file

@ -191,7 +191,7 @@ The runner can hand these to a CLI/MCP tool. The store decides what survives.
- recent event timing in that bounded context now crosses a projection boundary first, so the child sees "5 minutes ago" language while the parent keeps exact timestamps for bookkeeping
- public artifact sharing now crosses the same boundary: repo activity exposes content hints, rumination verifies authorship through retrieval, and a configured `publicSpeechTarget` lets Void queue a normal typed candidate intervention instead of needing a blog-specific announcement path. Void also has explicit herald jurisdiction over the GameCult website/blog; essay-shaped pressures should queue article-proposal candidates for Void-authored site pieces.
- Discord mention participation now uses role/display-name/bot addressing as CTB queue input and parent-owned speech rails as the output boundary. A Face mention appends a pending obligation for that Face; an ordinary Void mention or DM appends a pending obligation for the built-in `void` participant. Ambient direct mentions no longer create direct provider jobs.
- repo-scoped character turns reuse the typed self-state machinery per identity, but the child character must not see that machinery directly. Addressed chats are consumed by the heartbeat scheduler, which prioritizes the identity, and the intended boundary has three Interpreter prompts: `repo-face-state-projector.prompt.md` for state/context -> roleplay-friendly memory/mood/obligation/personhood prose, `repo-face-conversation-projector.prompt.md` for current-room and nearby-channel logs -> narrative conversational memory, and `repo-face-turn-interpreter.prompt.md` for natural character reflection -> typed memory, affect, social-read, mood, agency, speech, and Bifrost operations. Deterministic prompt assembly remains valid: `scripts/run-repo-face-heartbeats.ts --assemble-prompt <identity>` composes the final child prompt from templates, channel policy, repo/archive research guidance, Bifrost digest, and supplied Interpreter memory/conversation surfaces. It must not turn typed state or raw chat logs into character prose itself, and it should emit situational obligations only when the current packet proves them true. Until the first Interpreter calls are wired, the live turn prompt intentionally lacks projected memory/conversation prose rather than pretending an inspection renderer is the real architecture.
- repo-scoped character turns reuse the typed self-state machinery per identity, but the child character must not see that machinery directly. Addressed chats are consumed by the heartbeat scheduler, which prioritizes the identity, and the intended boundary has two model prompts: `repo-face-state-projector.prompt.md` for state -> roleplay-friendly memory/mood/personhood prose, and `repo-face-turn-interpreter.prompt.md` for natural character reflection -> typed memory, affect, social-read, mood, agency, and speech operations. Current-room and nearby-channel logs are not model-summarized; deterministic prompt assembly attaches raw oldest-to-newest transcript evidence with correction/recency rules so the Face must read the human exchange itself. The Face turn uses full `gpt-5.4`; the state projector and parent Interpreter use the cheaper Spark/Mini support chain. `scripts/run-repo-face-heartbeats.ts --assemble-prompt <identity>` composes the final child prompt from templates, channel policy, repo/archive research guidance, Bifrost digest, projected memory prose, and deterministic conversation transcript. It must not turn typed state into character prose itself, and it should emit situational obligations only when the current packet proves them true.
- repo-scoped Faces now have per-topic channel permissions instead of one work-shaped speech lane. `allowedChannelIds` remains the hard gate, while `channelPermissions` gives each Face labeled topics, speech thresholds, and speed multipliers. The current social invariant is that Aquarium is intentionally cheap for casual in-character chatter, while domain channels receive matching work/lore/music/realtime/agent-state thoughts when the recent cross-channel context lines up. Heat is the live-room valve: above-baseline heat now relaxes nap gating, but active-turn freeze still prevents an agent from being queued again while it is thinking.
- repo-scoped character prompts now explicitly treat social embodiment as durable state territory without exposing schemas to the child. A character may write naturally about humans, other agents, or room dynamics; the Interpreter turns meaning-bearing friendship, rivalry, irritation, trust, recurring bits, bonds, needs, and status reads into typed state when they carry claim/question, tension, action implication, and anchors. Public posts should speak to someone directly and must not begin with note-title formulas or identity labels such as "bright bridge note" or "tiny fish sorting note."
- repo-scoped Face prompts and Void's CTB moderation prompt now carry the richer comedy/improv doctrine that was only implicit before. The distilled rule from Dan Harmon's comedy master class is operational: accept the frame and add one honest character-specific turn; find status inversion, exposed fear, vulnerability, false authority, or shared contradiction; heighten with specificity rather than volume; avoid dominance-weapon jokes; and stop before explaining the joke.

View file

@ -119,6 +119,7 @@ const envSchema = z.object({
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_TURN_CODEX_MODEL: z.string().min(1).default("gpt-5.4"),
REPO_FACE_HEARTBEAT_CODEX_REASONING_EFFORT: z
.enum(["low", "medium", "high", "xhigh"])
.optional(),
@ -177,6 +178,7 @@ export interface AppConfig {
heatOverrides: Record<string, number>;
codexModel?: string;
codexModels: string[];
turnCodexModel: string;
codexModelReasoningEffort?: "low" | "medium" | "high" | "xhigh";
stateProjectorEnabled: boolean;
};
@ -404,6 +406,7 @@ 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],
turnCodexModel: parsed.REPO_FACE_TURN_CODEX_MODEL,
codexModelReasoningEffort: parsed.REPO_FACE_HEARTBEAT_CODEX_REASONING_EFFORT,
stateProjectorEnabled: parsed.REPO_FACE_STATE_PROJECTOR_ENABLED,
},

View file

@ -23,7 +23,8 @@ const repoFaceRetrievalToolAllowlist = [
async function main() {
const options = parseArgs(process.argv.slice(2));
const model = options.model ?? process.env.REPO_FACE_HEARTBEAT_CODEX_MODEL ?? "gpt-5.4-mini";
const faceModel = options["face-model"] ?? process.env.REPO_FACE_TURN_CODEX_MODEL ?? "gpt-5.4";
const interpreterModel = options["interpreter-model"] ?? options.model ?? process.env.REPO_FACE_HEARTBEAT_CODEX_MODEL ?? "gpt-5.3-codex-spark";
const reasoningEffort = options["reasoning-effort"] ?? process.env.REPO_FACE_HEARTBEAT_CODEX_REASONING_EFFORT ?? "low";
const mcpMode = options.mcp === "mock" ? "mock" : "real-readonly";
const outPath = resolve(repoRoot, options.out ?? defaultOut);
@ -50,7 +51,7 @@ async function main() {
process.stderr.write(`[dry-run] face turn ${identity}\n`);
const childRun = await runCodex(facePrompt, {
model,
model: faceModel,
reasoningEffort,
scenarioId: `${identity}_swarm_dry_run_child`,
logPath: childLogPath,
@ -69,7 +70,7 @@ async function main() {
faceOutput: childText,
});
const interpreterRun = await runCodex(interpreterPrompt, {
model,
model: interpreterModel,
reasoningEffort,
scenarioId: `${identity}_swarm_dry_run_interpreter`,
mcpMode: "none",
@ -102,7 +103,8 @@ async function main() {
const report = {
generatedAt: new Date().toISOString(),
model,
faceModel,
interpreterModel,
reasoningEffort,
mcpMode,
passed: runs.every((run) => run.assessment.failures.length === 0),

View file

@ -56,7 +56,7 @@ function readPrompt(name) {
async function main() {
const options = parseArgs(process.argv.slice(2));
const model = options.model ?? process.env.REPO_FACE_HEARTBEAT_CODEX_MODEL ?? "gpt-5.4-mini";
const model = options.model ?? process.env.REPO_FACE_HEARTBEAT_CODEX_MODEL ?? "gpt-5.4";
const reasoningEffort = options["reasoning-effort"] ?? process.env.REPO_FACE_HEARTBEAT_CODEX_REASONING_EFFORT ?? "low";
const outPath = resolve(repoRoot, options.out ?? defaultOut);
const selected = options.scenario

View file

@ -16,8 +16,9 @@ const defaultOut = resolve(repoRoot, ".voidbot", "status", "repo-face-interprete
async function main() {
const options = parseArgs(process.argv.slice(2));
const modelCandidates = modelCandidatesFromOptions(options);
const model = modelCandidates.join(",");
const faceModel = options["face-model"] ?? process.env.REPO_FACE_TURN_CODEX_MODEL ?? "gpt-5.4";
const interpreterModelCandidates = modelCandidatesFromOptions(options);
const interpreterModel = interpreterModelCandidates.join(",");
const reasoningEffort = options["reasoning-effort"] ?? process.env.REPO_FACE_HEARTBEAT_CODEX_REASONING_EFFORT ?? "low";
const facePromptPath = resolve(repoRoot, options["face-prompt"] ?? defaultFacePrompt);
const outPath = resolve(repoRoot, options.out ?? defaultOut);
@ -26,7 +27,7 @@ async function main() {
await rm(childLogPath, { force: true }).catch(() => undefined);
const childRun = await runCodexWithModelFallback(facePrompt, {
models: modelCandidates,
models: [faceModel],
reasoningEffort,
scenarioId: "nibu_interpreter_loop_child",
logPath: childLogPath,
@ -42,7 +43,7 @@ async function main() {
faceOutput: childText,
});
const interpreterRun = await runCodexWithModelFallback(interpreterPrompt, {
models: modelCandidates,
models: interpreterModelCandidates,
reasoningEffort,
scenarioId: "nibu_interpreter_loop_parent",
logPath: undefined,
@ -56,7 +57,8 @@ async function main() {
];
const report = {
generatedAt: new Date().toISOString(),
model,
faceModel,
interpreterModel,
reasoningEffort,
facePromptPath,
passed: failures.length === 0,
@ -83,7 +85,8 @@ async function main() {
await writeFile(outPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
process.stdout.write(`${JSON.stringify({
outPath,
model,
faceModel,
interpreterModel,
reasoningEffort,
passed: report.passed,
failures,
@ -276,7 +279,7 @@ function modelCandidatesFromOptions(options) {
explicit ??
process.env.REPO_FACE_HEARTBEAT_CODEX_MODELS ??
process.env.REPO_FACE_HEARTBEAT_CODEX_MODEL ??
"gpt-5.4-mini";
"gpt-5.3-codex-spark,gpt-5.4-mini";
return raw
.split(",")
.map((entry) => entry.trim())

View file

@ -254,3 +254,5 @@
{"ts":"2026-05-21T21:19:08Z","type":"fix","status":"ok","note":"Cut repo Face speech truncation and stale-name bleed after Bifrost/Mimir room failures: worker now sends raw model output to the Interpreter instead of parsing a Discord-fitted slice, rejects overlong or ellipsis-ended SAY payloads before bridge posting, and conversation projection now treats LocalCastBridge as a legacy Mimir alias while keeping Aqua wet-voice/parity vocabulary in Aqua's lane."}
{"ts":"2026-05-21T21:31:45Z","type":"operations","status":"ok","note":"Paused the repo Face swarm after Epiphany repeated stale wet-voice receipt discourse despite direct correction. Disabled GameCult Local Orchestrator, stopped the worker process, confirmed no pending/running repo-face jobs, and added state/agent-swarm-paused.json so future pulses skip until the architecture is rethought."}
{"ts":"2026-05-21T22:48:57Z","type":"implementation","status":"ok","note":"Removed the model-backed repo Face conversation projector from heartbeat prompt assembly. Face turns now receive deterministic raw recent-message transcript sections, oldest-to-newest, with explicit recency/correction/channel rules; the model state projector remains limited to identity, memory, affect, and social texture. Verified typecheck and Aqua prompt assembly."}
{"ts":"2026-05-21T22:54:54Z","type":"verification","status":"fail","note":"Aqua mini dry run failed the correction-internalization test. State projection and Face turn both used gpt-5.4-mini; Aqua saw the raw transcript containing Metacrat's correction that voice witnesses must be automated via thousands of renders and log-mel cosine loss, but the Face output still preserved the older bounded manual listening-check frame. The mini Interpreter routed that stale memory instead of rejecting it. Removed gpt-5.4-mini from default repo Face model fallback chain; defaults now prefer Spark then gpt-5.4."}
{"ts":"2026-05-21T22:59:19Z","type":"implementation","status":"ok","note":"Split repo Face model ownership after Aqua correction trial. Child Face turns now use REPO_FACE_TURN_CODEX_MODEL, default gpt-5.4, while state projection and parent Interpreter keep the cheaper REPO_FACE_HEARTBEAT_CODEX_MODELS support chain, default gpt-5.3-codex-spark,gpt-5.4-mini. Worker executes Face retries on full 5.4 and Interpreter passes on the support chain; dry-run harnesses now expose the same split. Verified typecheck and config loading."}

View file

@ -104,7 +104,7 @@ accepted_design:
- Agent mentions are CTB pressure, not direct Discord prompt jobs. Role-addressed or display-name-addressed repo Face mentions enqueue pending mention obligations in the CTB heartbeat state; human replies to a Face's webhook/persona message count as direct attention to that Face too, even without a literal tag. Ordinary Void mentions and DMs enqueue pending mentions for the built-in `void` participant. The heartbeat scheduler consumes that queue, temporarily pulls the addressed agent forward, attaches the pending prompt/reply target to the next repo-face-rumination or moderation pass, and the parent-owned speech rails post only explicit candidate/action output. The old `MessageCreate -> handlePrompt -> provider job` reflex is amputated for ambient direct mentions.
- Face prompts carry explicit Epiphany identity doctrine. The base Void style pack contributes discipline, humor permission, and source-grounding habits, but the registered Face state, Epiphany, repo-local `.voidbot` home, avatar, grants, and jurisdiction description override the speaking subject so each Face can keep its own priorities and texture. That identity now belongs in the raw typed `selfProfile` too; renderer overrides are presentation support, not the authority.
- Repo Face volatile state projection is model-owned again. The heartbeat scheduler may deterministically assemble a compact state packet, but live child prompts must pass that packet through `prompts/repo-face-state-projector.prompt.md` before it reaches the Face. The deterministic packet is an emergency isolation fallback only behind `REPO_FACE_STATE_PROJECTOR_ENABLED=false`, and both paths reject schema/prompt-construction leaks such as `grants:`, `jurisdictions:`, `Face of`, repo paths, and "do not prompt" language before prompt assembly succeeds.
- Repo Face conversation projection is also model-owned. The heartbeat scheduler may gather recent room messages, nearby channel snapshots, and pending mentions, but the child Face must receive a compact lived-memory surface from `prompts/repo-face-conversation-projector.prompt.md`, not a transcript dump or the old "No Interpreter-shaped conversation memory" placeholder. Direct human invitations, criticism of silence, banter, jurisdiction ownership, and live social pressure belong in that projection so the Face can decide whether speech is warranted.
- Repo Face conversation evidence is deterministic again. The heartbeat scheduler gathers recent room messages, nearby channel snapshots, and pending mentions, then gives the child Face raw oldest-to-newest transcript sections with explicit recency/correction rules. The model state projector still owns identity, memory, affect, social reads, and other lived internal pressure; it does not summarize room messages or decide consensus for the Face.
- Face embodiment comes before task execution. Repo concerns, private rumination, public posts, jokes, objections, and silence should all be filtered through the Face's perspective, feelings, drives, values, wounds, fascinations, aesthetic taste, and dignity. Values must suit the kind of person the character is, not merely the jobs assigned to the Face.
- Face affect is typed substrate now, not prompt garnish. Each Face state may carry explicit needs, social bonds, status reads, and mood dimensions in `void.face_affect`: substrate concern, attention hunger, agency frustration, recognition needs, territoriality, rest/play pressure, friendships, rivalries, envy, pride, irritation, pampering, neglect, bypasses, and consultation status. Affect is not an action request by itself; it bends speaking pressure and gives agency pressure a personal source when a need wants concrete change.
- Face affect is not complete unless it survives the public repo Face MCP surface. `get_voidbot_runtime_info` exposes the running MCP process id, workspace, self-state schema fingerprint, and registered document types; `npm run agents:repo-face-affect-mcp-fixture -- --identity <face>` starts the built VoidBot MCP server, checks that fingerprint includes `void.face_affect`, then calls `read_repo_face_state`. It must expose `typedState.faceAffect` with needs, socialBonds, statusReads, and moodDimensions arrays. Direct state fixtures alone are insufficient because stale MCP registrations can otherwise keep throwing `No schema is registered for persisted document type "void.face_affect"` while source code looks correct.
@ -233,4 +233,4 @@ rejected_paths:
current_status:
summary: VoidBot's bot/worker/provider/RAG/Postgres/Qdrant/CultCache structure is live, and the repo Face swarm has been restarted after the 2026-05-21 teardown pass. `GameCult Local Orchestrator` is enabled as the single pulse owner; watchdog, mood, moderation, and repo Face CTB organs have all completed healthy post-restart pulses. Bifrost work routing is paused by default.
next_action: Keep the swarm awake but watched. The latest cuts removed repo Face turns from the owner-reply wrapper, coerces stray owner-DM speech back to the live room unless explicitly marked owner-private, pauses Face Bifrost/governance routing, disables automatic Bifrost dispatch by default, restored the model state projector as the live state-to-character prose boundary, wired the live conversation projector into heartbeat prompt assembly, and fixed worker delivery so Interpreter channel labels resolve through registered Face permissions. Next work should watch model-output logs and Discord receipts for whether agents answer direct room invitations instead of silently preserving private state.
next_action: Keep the swarm paused until the Face-turn loop proves it can read corrections from raw transcript evidence. The latest cuts removed repo Face turns from the owner-reply wrapper, coerces stray owner-DM speech back to the live room unless explicitly marked owner-private, pauses Face Bifrost/governance routing, disables automatic Bifrost dispatch by default, keeps the model state projector as the live state-to-character prose boundary, replaces the model conversation projector with deterministic raw transcripts, and splits model ownership so Face turns always default to full gpt-5.4 while state projectors and parent Interpreters use the cheaper Spark/Mini support chain.