diff --git a/apps/worker/src/index.ts b/apps/worker/src/index.ts index f73a329..bb29e22 100644 --- a/apps/worker/src/index.ts +++ b/apps/worker/src/index.ts @@ -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, diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index b32c21f..7a04dea 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -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. diff --git a/scripts/inspect-model-output-logs.mjs b/scripts/inspect-model-output-logs.mjs index dc78cf5..3141642 100644 --- a/scripts/inspect-model-output-logs.mjs +++ b/scripts/inspect-model-output-logs.mjs @@ -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)), diff --git a/state/agent-swarm-paused.json b/state/agent-swarm-paused.json index 2b9ad25..efdedd1 100644 --- a/state/agent-swarm-paused.json +++ b/state/agent-swarm-paused.json @@ -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" } diff --git a/state/map.yaml b/state/map.yaml index e9939fb..e5b603f 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -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.