Fix malformed Face Mind retry routing

This commit is contained in:
Meta Vens 2026-05-26 21:02:30 +01:00
commit 72089c73e4
5 changed files with 78 additions and 14 deletions

View file

@ -544,6 +544,7 @@ async function executeRepoFaceJobWithInterpreter(
const firstInterpretation = firstForbiddenTools.length > 0
? {
decision: "retry" as const,
parseable: true,
reasons: [
`Face used forbidden substrate/tool-discovery tool(s): ${firstForbiddenTools.join(", ")}`,
"Face turns may use retrieval tools only; state and substrate inventory belong to the parent/interpreter.",
@ -563,6 +564,48 @@ async function executeRepoFaceJobWithInterpreter(
return dropRepoFaceActionBlocks(firstResponse, firstInterpretation);
}
if (!firstInterpretation.parseable) {
await auditLog.record({
type: "repo_face.parent_interpreter_retry",
actorId: job.requester.id,
jobId: job.id,
provider: job.provider,
details: {
decision: firstInterpretation.decision,
reasons: firstInterpretation.reasons,
owner: "parent_interpreter",
},
});
console.warn(
`Repo Face parent interpreter retrying its own malformed output for job ${job.id}: ${firstInterpretation.reasons.join("; ")}`,
);
const secondInterpretation = await interpretRepoFaceTurnOutput(provider, job, firstText, {
attempt: 2,
});
if (secondInterpretation.decision === "route" && secondInterpretation.parseable) {
return routeRepoFaceInterpretedOutput(firstResponse, secondInterpretation, {
job,
faceOutputText: firstText,
});
}
await auditLog.record({
type: "repo_face.parent_interpreter_drop",
actorId: job.requester.id,
jobId: job.id,
provider: job.provider,
details: {
decision: secondInterpretation.decision,
reasons: secondInterpretation.reasons,
owner: "parent_interpreter",
},
});
console.warn(
`Repo Face parent interpreter dropped job ${job.id} after malformed interpreter retry: ${secondInterpretation.reasons.join("; ")}`,
);
return dropRepoFaceActionBlocks(firstResponse, secondInterpretation);
}
await auditLog.record({
type: "repo_face.parent_interpreter_retry",
actorId: job.requester.id,
@ -597,6 +640,7 @@ async function executeRepoFaceJobWithInterpreter(
const retryInterpretation = retryForbiddenTools.length > 0
? {
decision: "drop" as const,
parseable: true,
reasons: [
`Face used forbidden substrate/tool-discovery tool(s) on retry: ${retryForbiddenTools.join(", ")}`,
"Dropping action blocks rather than routing a turn that crossed the child/tool boundary.",
@ -630,6 +674,7 @@ async function executeRepoFaceJobWithInterpreter(
interface RepoFaceParentInterpretation {
decision: "route" | "retry" | "drop";
parseable: boolean;
reasons: string[];
routedOutput?: string;
}
@ -714,8 +759,9 @@ function parseRepoFaceParentInterpretation(
): RepoFaceParentInterpretation {
const block = parseInterpretationBlock(interpretationText);
const decision = block.decision?.trim().toLowerCase();
const parseable = decision === "route" || decision === "retry" || decision === "drop";
const parsedDecision =
decision === "route" || decision === "retry" || decision === "drop"
parseable
? decision
: attempt === 1
? "retry"
@ -728,7 +774,12 @@ function parseRepoFaceParentInterpretation(
return {
decision: parsedDecision,
reasons: reasons.length > 0 ? reasons : ["parent interpreter did not provide a parseable reason"],
parseable,
reasons: reasons.length > 0
? reasons
: [parseable
? "parent interpreter did not provide a parseable reason"
: "parent interpreter did not provide a parseable INTERPRETATION decision"],
routedOutput: extractRoutedRepoFaceOutput(interpretationText),
};
}
@ -886,8 +937,10 @@ function dropRepoFaceActionBlocks(
interpretation: RepoFaceParentInterpretation,
): ProviderResponse {
const text = fitDiscordMessage(response.outputText ?? response.summary);
const privateSummary = stripRepoIdentityPostIntents(text) ||
`Parent interpreter dropped repo Face action blocks: ${interpretation.reasons.join("; ")}`;
const stripped = stripRepoIdentityPostIntents(text);
const privateSummary = stripped !== text && stripped.trim().length > 0
? stripped
: `Parent interpreter dropped repo Face output: ${interpretation.reasons.join("; ")}`;
return {
...response,
outputText: privateSummary,

View file

@ -17,7 +17,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
## Current Shape
- Discord bot and worker may be live, but the unattended repo Face / CTB swarm brake is active as of 2026-05-26.
- The swarm was paused because live Face turns leaked owner-reply/Mind scaffolding into public Discord: Mind was being wrapped by the owner Discord reply prompt, then a worker fallback treated plain malformed Mind prose as public `SAY`. The fixed worker stack was restarted on 2026-05-26, typecheck/build pass, `agents:interpreter-loop-smoke` passes, and `agents:heartbeat-model-smoke -- --model gpt-5.4 --reasoning-effort none` passes. The Mind prompt has now been slimmed from incident-by-incident scar tissue into semantic route/retry/drop judgment plus a hard STATE NOTE/ARTICLE/SAY DSL contract. Keep `state/agent-swarm-paused.json` active until the operator explicitly asks to thaw; before thawing, run one controlled current-room turn and inspect model-output logs for `INTERPRETATION` plus explicit DSL actions only. Bifrost dispatch remains disabled by default behind `BIFROST_DISPATCH_ENABLED=false`.
- The swarm is paused because live Face turns leaked owner-reply/Mind scaffolding into public Discord twice on 2026-05-26. The first failure wrapped parent Mind in the owner Discord reply prompt and let malformed plain Mind prose become public `SAY`. The second thaw attempt showed the deeper split: malformed parent Mind output caused the worker to ask the Face to revise, and that retry path turned owner-question boilerplate into Bifrost/Nibu `SAY`. The current worker retries malformed Mind output as Mind-owned interpretation, drops to diagnostic private summary instead of re-prompting the Face when Mind stays malformed, and never preserves raw dropped prose as the job summary. `scripts/inspect-model-output-logs.mjs` now flags repo-face parent interpreter records missing an `INTERPRETATION` decision. Keep `state/agent-swarm-paused.json` active until a new controlled thaw explicitly proves the live worker, parent Mind, model-output inspection, and Discord side effects all agree.
- The teardown authority remains `notes/agent-swarm-teardown-plan-2026-05-21.md`, but it is now a restart checklist and smell ledger rather than a stop sign. Keep using it when a Face/Bifrost/CTB boundary smells like prompt/parser/transport Jenga.
- The repo-controlled pause flag is `state/agent-swarm-paused.json`. While it is missing or has `"paused": false`, swarm runners may operate. While it has `"paused": true` or is malformed, the Face heartbeat runner skips and the orchestrator skips agent-swarm organs. This is the brake. Do not route around it.
- Postgres owns jobs, audit events, interaction memory, and rate-limit state.

View file

@ -64,6 +64,11 @@ function inspect(records) {
const speechSmells = records.filter((record) =>
/repo-face heartbeat|heartbeat from|bright bridge note|tiny fish sorting note|librarian note|maintenance pass/i.test(String(record.finalMessage ?? "")),
);
const malformedRepoFaceInterpreter = records.filter((record) =>
record.command === "repo-face-rumination" &&
record.promptMarker === "repo-face-turn-interpreter" &&
!/\bINTERPRETATION\b[\s\S]*?\bdecision:\s*(route|retry|drop)\b/i.test(String(record.finalMessage ?? "")),
);
const quotaSignals = records.filter((record) =>
/quota|rate limit|rate-limit|usage limit|capacity|too many requests|(?:http|status|code|error)\s*429|429\s*(?:too many requests|rate)|insufficient_quota|model.*unavailable|model.*access|limit exceeded/i.test(
`${record.stderrTail ?? ""}\n${record.stdoutTail ?? ""}\n${record.handoffReason ?? ""}`,
@ -74,6 +79,7 @@ function inspect(records) {
pushWarning(warnings, noFinalMessage.length, "model runs with no final message", noFinalMessage);
pushWarning(warnings, forbiddenToolRecords.length, "Face/model runs using forbidden substrate tools", forbiddenToolRecords);
pushWarning(warnings, speechSmells.length, "robotic/provenance speech smells", speechSmells);
pushWarning(warnings, malformedRepoFaceInterpreter.length, "repo Face parent interpreter outputs missing INTERPRETATION decision", malformedRepoFaceInterpreter);
pushWarning(warnings, quotaSignals.length, "quota/rate/capacity fallback signals", quotaSignals);
return {
@ -84,6 +90,7 @@ function inspect(records) {
noFinalMessage: summarizeRecords(noFinalMessage),
forbiddenToolRecords: summarizeRecords(forbiddenToolRecords),
speechSmells: summarizeRecords(speechSmells),
malformedRepoFaceInterpreter: summarizeRecords(malformedRepoFaceInterpreter),
quotaSignals: summarizeRecords(quotaSignals),
warnings,
recent: summarizeRecords(records.slice(-12)),

View file

@ -1,5 +1,5 @@
{
"paused": true,
"reason": "Paused after live Face turns leaked owner-Codex/bridge artifact text into public Discord despite Face stdout choosing private silence. Fix side-effect extraction/routing before thawing.",
"updatedAt": "2026-05-26T11:29:00Z"
"reason": "Re-paused after live Bifrost Face post leaked owner-question wording into public Discord during thaw validation.",
"updatedAt": "2026-05-26T19:45:00.000Z"
}

View file

@ -14,13 +14,17 @@ objective:
Aetheria lore, while keeping the system understandable enough to modify
safely.
current: >
Keep the repo Face swarm paused after the 2026-05-26 Mind/owner-reply leak
Keep the repo Face swarm paused after the 2026-05-26 Mind/owner-reply leaks
until a controlled thaw verifies the Face/Mind/speech boundary in current
room context. Bifrost work routing remains paused because the governance
bridge proved too disorienting for live social traffic. Parent Mind now has
a slimmer semantic prompt with a hard DSL contract: interpret natural Face
thought, decide route/retry/drop, and emit only explicit STATE NOTE, ARTICLE,
or SAY blocks for side effects.
room context. The second thaw attempt proved malformed parent Mind output
was being treated as a Face retry, allowing owner-question boilerplate to
become public Bifrost/Nibu SAY. The worker now treats malformed Mind output
as Mind-owned retry/drop and the model-output inspector flags interpreter
records missing INTERPRETATION decisions. Bifrost work routing remains
paused because the governance bridge proved too disorienting for live social
traffic. Parent Mind has a slimmer semantic prompt with a hard DSL contract:
interpret natural Face thought, decide route/retry/drop, and emit only
explicit STATE NOTE, ARTICLE, or SAY blocks for side effects.
constraints:
- Public traffic still rides one remote Qwen box plus local embedding and ops infrastructure.
@ -57,7 +61,7 @@ data_flow:
- keep persistent state lean enough that re-entry sharpens the next turn instead of drowning it
active_subgoals:
- Treat `notes/agent-swarm-teardown-plan-2026-05-21.md` as the historical teardown authority and the restart checklist. The repo Face swarm is currently paused after the 2026-05-26 owner-reply/Mind leak, and should thaw only through controlled evidence that the scheduler, prompt, model-output logs, parent Mind, and worker side-effect rails still have explicit observable boundaries. Keep watching for prompt/parser/transport Jenga before adding new swarm features.
- Treat `notes/agent-swarm-teardown-plan-2026-05-21.md` as the historical teardown authority and the restart checklist. The repo Face swarm is currently paused after the 2026-05-26 owner-reply/Mind leaks, including the second thaw failure where malformed parent Mind output was converted into public owner-question SAY. It should thaw only through controlled evidence that the scheduler, prompt, model-output logs, parent Mind retry/drop ownership, and worker side-effect rails still have explicit observable boundaries. Keep watching for prompt/parser/transport Jenga before adding new swarm features.
- Make the new project-state surfaces the default continuity path for nontrivial VoidBot work.
- Keep the rebuilt moderation/self-state foundation on canonical CultCache `.cc` typed operations, never an editable JSON working projection.
- Keep the typed moderation, mood, repo Face, and watchdog organs behind the single `GameCult Local Orchestrator` scheduled pulse; the per-organ scheduled tasks are legacy installer surfaces and should stay disabled unless a deliberate isolation test needs one. Bifrost dispatch is disabled by default behind `BIFROST_DISPATCH_ENABLED=false` until the governance bridge is rebuilt cleanly. As of the 2026-05-21 restart, the orchestrator task is enabled and is the live swarm pulse owner.