From 6773f21341d00a5f9f49347883d5fe99f1437283 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 14 Jun 2026 21:02:38 +0100 Subject: [PATCH] Block ambiguous repo Face current-room routing --- apps/worker/src/index.ts | 30 +++++++++++++++++++++--------- notes/fresh-workspace-handoff.md | 1 + state/agent-swarm-paused.json | 6 +++--- state/evidence.jsonl | 1 + 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/apps/worker/src/index.ts b/apps/worker/src/index.ts index 3576d2d..784df71 100644 --- a/apps/worker/src/index.ts +++ b/apps/worker/src/index.ts @@ -692,7 +692,7 @@ async function interpretRepoFaceTurnOutput( const response = await executeProviderForJob(provider, job, interpreterContext, "interpreter"); const interpretationText = normalizeModelText(response.outputText ?? response.summary); return { - ...parseRepoFaceParentInterpretation(interpretationText, input.attempt, outputText), + ...parseRepoFaceParentInterpretation(interpretationText, input.attempt, outputText, job), artifacts: prefixProviderArtifacts(`interpreter-attempt-${input.attempt}`, response.artifacts ?? []), }; } @@ -784,9 +784,10 @@ function parseRepoFaceParentInterpretation( interpretationText: string, attempt: 1 | 2, faceOutputText: string, + job?: JobRecord, ): RepoFaceParentInterpretation { const block = parseInterpretationBlock(interpretationText); - const fallback = buildBareInterpreterSayFallback(interpretationText, faceOutputText); + const fallback = buildBareInterpreterSayFallback(interpretationText, faceOutputText, job); if (!block.decision && fallback) { return { decision: "route", @@ -814,10 +815,17 @@ function parseRepoFaceParentInterpretation( }; } -function buildBareInterpreterSayFallback(interpretationText: string, faceOutputText: string): string | undefined { +function buildBareInterpreterSayFallback( + interpretationText: string, + faceOutputText: string, + job?: JobRecord, +): string | undefined { if (!faceOutputHasUnconditionalWouldSay(faceOutputText)) { return undefined; } + if (job && repoFaceHasMultipleActiveConversationChannels(job)) { + return undefined; + } if (parseRepoFaceActionBlocks(interpretationText).length > 0) { return undefined; } @@ -955,7 +963,9 @@ function normalizeInterpretedRepoFaceSpeechDestinations( } function repoFaceHasMultipleActiveConversationChannels(job: JobRecord): boolean { - return repoFaceHasMultipleActiveConversationChannels(job); + const activeThreads = job.contextBundle.repoFaceConversationThreads ?? []; + const representedChannels = new Set(activeThreads.map((thread) => thread.channelId)); + return representedChannels.size > 1; } function repoFaceOutputHasExplicitSayDestination(outputText: string): boolean { @@ -1386,7 +1396,7 @@ async function postRepoIdentityIntent(job: JobRecord, intent: RepoIdentityPostIn } if (repoIdentityRequiresExplicitConversationContext(job, intent) && !conversationContext) { console.warn( - `Rejected repo identity ${identity.id} speech for job ${job.id}: multiple active conversation contexts require context, reply_to, or channel.`, + `Rejected repo identity ${identity.id} speech for job ${job.id}: multiple active conversation contexts require context, reply_to, or a concrete channel id.`, ); return false; } @@ -3098,13 +3108,15 @@ function repoIdentityRequiresExplicitConversationContext( job: JobRecord, intent: RepoIdentityPostIntent, ): boolean { - if (intent.channelId || intent.replyToMessageId || intent.contextId) { + if (intent.replyToMessageId || intent.contextId) { return false; } - const activeThreads = job.contextBundle.repoFaceConversationThreads ?? []; - const representedChannels = new Set(activeThreads.map((thread) => thread.channelId)); - return representedChannels.size > 1; + if (intent.channelId && !isCurrentRoomChannelAlias(intent.channelId)) { + return false; + } + + return repoFaceHasMultipleActiveConversationChannels(job); } function resolveRepoIdentityReplyTargetChannel( diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index c7c1e8e..47d9cda 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -175,6 +175,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou - The overnight 2026-05-23 run showed a softer version of the previous loop: Faces did not collapse into nonsense, but the Aquarium room stayed captured by the Nibu-origin/faction lore seam for hours. Existing `saturationScore` lived on private thought/incubation surfaces, and the repetition prompt mostly caught copied openings/endings, not semantic topic attractors. The heartbeat prompt now projects current-room topic saturation from repeated raw-transcript terms and estimates whether the attractor is home-adjacent or another steward's gravity well from each Face's identity/jurisdiction text. The state projector should turn foreign attractors into character-specific crowding, neglect, boredom, rivalry, handoff, or pivot pressure; the Interpreter diagnoses non-owner same-topic politeness as missing personal stake, ownership, freshness, handoff, or pivot rather than applying a topic ban or token blacklist. - Void's broader moderation authority had become venue leakage: recent-history projection stripped `channelId`, leaving the configured `publicSpeechTarget` as the obvious place for replies, so Aquarium-shaped talk could land in `#general`. The rumination context now preserves message `channelId` and projects `recentConversationTarget`; prompt doctrine says source-channel replies belong in that source channel, while `publicSpeechTarget` is for unanchored herald/artifact announcements. - Repo Face speech routing now uses active conversation contexts instead of one room-focus guess. Heartbeat prompts project up to six `ctx__` records across current and nearby channels; the Interpreter can emit `context:` on a `SAY`; the worker resolves that context to the source channel/message and rejects unknown contexts or ambiguous bare SAY blocks when multiple active channels are visible. A Persona can carry separate #aquarium/#pics/#general conversations at once, but public speech has to keep its context attached. +- Follow-up leak found on 2026-06-14: `channel: current_room` still counted as an explicit destination, and bare parent-Interpreter text could still be wrapped into a `SAY channel: current_room`. Those were obsolete owners. They are now blocked when multiple active conversation channels are visible; multi-context speech needs `context`, `reply_to`, or a concrete channel id. The swarm pause flag is currently set true while this repair is verified live. - The Nibu/Void turf-crossing discussion exposed a social-state projection gap: existing bonds/status reads project well once typed state already contains them, but the live pipeline did not own "this recent event should become relationship/status pressure." The repair adds bounded raw relationship-pressure evidence to the state packet before the projector: recent messages that touch the Face, peer Faces, existing social targets, jurisdiction/domain terms, or status/territory/consultation language. The projector may turn that evidence into tentative felt pressure, and the Interpreter may persist a bond/status/mood note only when the projected memory or Face output shows a character-specific reaction. This should let resentment, rivalry, consultation offense, and territorial tension emerge without manual state edits. - The swarm brake currently reads `state/agent-swarm-paused.json` with `"paused": false`. If scheduled Face turns start overrunning again, use that brake rather than letting the orchestrator churn. The known 2026-06-09 timeout source was the model state-projector child; local runtime currently keeps it disabled while the deterministic fallback and Interpreter/worker speech rails are watched. - Direct Face `UPDATE REQUEST` and `BIFROST TOPIC` packets are ignored while `REPO_FACE_BIFROST_ENABLED=false`. Do not debug missing Bifrost jobs as a live failure unless that flag has been deliberately enabled again. diff --git a/state/agent-swarm-paused.json b/state/agent-swarm-paused.json index e3f56df..d460a80 100644 --- a/state/agent-swarm-paused.json +++ b/state/agent-swarm-paused.json @@ -1,5 +1,5 @@ { - "paused": false, - "reason": "Resumed by Codex on 2026-06-09 after local runtime config disabled the expensive repo Face state-projector pass and reduced heartbeat selection to one job per tick. Watch scheduled pulses for clean exits before raising cadence.", - "updatedAt": "2026-06-09T15:00:00.0000000Z" + "paused": true, + "reason": "Paused by Codex on 2026-06-14 after repo Faces still routed source-channel context through channel: current_room into Aquarium. Keep paused until current_room is no longer accepted as explicit context when multiple active conversation contexts are visible.", + "updatedAt": "2026-06-14T18:55:00.0000000Z" } diff --git a/state/evidence.jsonl b/state/evidence.jsonl index be9ee7d..7c3c666 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -364,3 +364,4 @@ {"ts":"2026-06-14T12:26:21Z","type":"implementation","status":"ok","note":"Made repo Face self-maintenance grievances public-first: prompt projection now surfaces Persona/system substrate, tools, voice/avatar/access, state-projection, semantic-memory, and agency-path pressure under a Self-maintenance public advocacy section; Face and Interpreter prompts now require a compact public-safe SAY for body/mind/substrate asks instead of burying them as private state. Detector was narrowed after prompt verification so sensitive human body/home trauma does not get auto-promoted by this machinery."} {"ts":"2026-06-14T12:43:35Z","type":"implementation","status":"ok","note":"Fixed repo Face cross-channel reply routing: prompt transcripts now include source channel ids for current, nearby, and chronology messages; Face prompts explicitly forbid answering nearby-room posts in the current room; worker post routing now derives the destination channel from reply_to message ids and also infers a non-current room when a bare Face output names that room label (e.g. general thread). Verified against Libby prompt showing Metacrat general messages followed by the bad Aquarium post."} {"ts":"2026-06-14T17:40:23Z","type":"implementation","status":"ok","note":"Repo Face speech routing now carries typed active conversation contexts across current and nearby Discord channels. Heartbeat prompts emit ctx__ records; SAY can include context; worker resolves context to source channel/reply target and rejects unknown or ambiguous unqualified SAY when multiple channels are active."} +{"ts":"2026-06-14T20:01:05Z","type":"implementation","status":"ok","note":"Closed the surviving repo Face context leak: channel: current_room no longer counts as explicit routing when multiple active conversation channels are visible, and bare parent-interpreter text is no longer wrapped into SAY fallback under multi-context jobs. Swarm paused while this fix is verified live."}