From cd8d1728f0b72b63023cbbf1d96c3946676465ba Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 4 Jun 2026 23:20:20 +0100 Subject: [PATCH] Require reply anchors for stale Face threads --- apps/worker/src/index.ts | 5 +++++ apps/worker/src/repo-face-speech.ts | 18 +++++++++++++++ scripts/smoke-repo-face-speech-boundary.ts | 26 ++++++++++++++++++++++ state/evidence.jsonl | 1 + 4 files changed, 50 insertions(+) diff --git a/apps/worker/src/index.ts b/apps/worker/src/index.ts index 9c518ba..b9ea8fe 100644 --- a/apps/worker/src/index.ts +++ b/apps/worker/src/index.ts @@ -1208,6 +1208,11 @@ async function postRepoIdentityIntent(job: JobRecord, intent: RepoIdentityPostIn `Coerced repo identity ${identity.id} speech for job ${job.id} from "${requestedChannelId}" to conversation channel ${channelId}.`, ); } + if (intent.requiresExplicitReplyTo && !intent.replyToMessageId) { + throw new Error( + `Repo identity ${identity.id} SAY for job ${job.id} revived older context but did not include an explicit reply_to anchor.`, + ); + } const replyToMessageId = intent.replyToMessageId ?? repoIdentityDefaultReplyToMessageId(job, channelId); const rawContent = intent.content.trim(); if (isNonPublicRepoIdentitySpeech(rawContent)) { diff --git a/apps/worker/src/repo-face-speech.ts b/apps/worker/src/repo-face-speech.ts index 4db961b..58ab648 100644 --- a/apps/worker/src/repo-face-speech.ts +++ b/apps/worker/src/repo-face-speech.ts @@ -5,6 +5,7 @@ export interface RepoIdentityPostIntent { channelId?: string; content: string; replyToMessageId?: string; + requiresExplicitReplyTo?: boolean; } type RepoFaceActionBlock = { @@ -13,6 +14,7 @@ type RepoFaceActionBlock = { }; export function parseRepoIdentityPostIntents(finalResponse: string): RepoIdentityPostIntent[] { + const requiresExplicitReplyTo = repoFaceInterpretationRequiresExplicitReplyAnchor(finalResponse); const intents: RepoIdentityPostIntent[] = parseRepoFaceActionBlocks(finalResponse) .filter((block) => block.kind === "say") .flatMap((block): RepoIdentityPostIntent[] => { @@ -25,6 +27,7 @@ export function parseRepoIdentityPostIntents(finalResponse: string): RepoIdentit identity: optionalDslString(block.fields.identity), channelId: optionalDslString(block.fields.channel) ?? optionalDslString(block.fields.channelId), replyToMessageId: optionalDslString(block.fields.reply_to) ?? optionalDslString(block.fields.replyToMessageId), + requiresExplicitReplyTo, content, }, ]; @@ -48,6 +51,7 @@ export function parseRepoIdentityPostIntents(finalResponse: string): RepoIdentit channelId: typeof parsed.channelId === "string" ? parsed.channelId.trim() : undefined, replyToMessageId: typeof parsed.replyToMessageId === "string" ? parsed.replyToMessageId.trim() : undefined, + requiresExplicitReplyTo, content, }); } catch { @@ -58,6 +62,20 @@ export function parseRepoIdentityPostIntents(finalResponse: string): RepoIdentit return intents; } +export function repoFaceInterpretationRequiresExplicitReplyAnchor(finalResponse: string): boolean { + const interpretation = finalResponse.match(/INTERPRETATION\s*([\s\S]*?)\s*END/i)?.[1] ?? ""; + if (!interpretation.trim()) { + return false; + } + + return [ + /\bnewest\b[\s\S]{0,160}\b(moved|elsewhere|topic)\b/i, + /\bolder side thread\b/i, + /\brevives?\b[\s\S]{0,120}\b(older|stale|side thread)\b/i, + /\bstale\b[\s\S]{0,120}\b(room|context|topic|thread)\b/i, + ].some((pattern) => pattern.test(interpretation)); +} + export function isNonPublicRepoIdentitySpeech(value: string): boolean { const trimmed = value.trim(); if (!trimmed) { diff --git a/scripts/smoke-repo-face-speech-boundary.ts b/scripts/smoke-repo-face-speech-boundary.ts index ec8254f..8ef20c7 100644 --- a/scripts/smoke-repo-face-speech-boundary.ts +++ b/scripts/smoke-repo-face-speech-boundary.ts @@ -62,6 +62,32 @@ const cases: Case[] = [ "show \"rear pair drifting 38 ms\", \"geometry confidence low\", \"world model provisional\".", detail: "Repo Faces can mention technical labels without speaking in code spans.", }, + { + name: "older side-thread route requires explicit reply anchor", + run: () => { + const posts = parseRepoIdentityPostIntents([ + "INTERPRETATION", + "correction_check: retry", + "doctrine_check: none", + "decision: route", + "reason:", + " The newest room context has moved to CNC workflow; this revives an older side thread.", + "END", + "", + "SAY", + "identity: current_face_id", + "channel: current_room", + "reply_to:", + "content:", + " A stale field can lie very politely.", + "END", + ].join("\n")); + return posts.length === 1 && + posts[0]?.requiresExplicitReplyTo === true && + posts[0]?.replyToMessageId === undefined; + }, + detail: "A routed old-topic reply without reply_to must be marked so the worker refuses an orphan post.", + }, ]; const failures = cases.filter((entry) => !entry.run()); diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 8877210..7fa4258 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -344,3 +344,4 @@ {"ts":"2026-06-04T19:28:18Z","type":"implementation","status":"ok","note":"Moderation consequences now live in typed void.moderation_cursor userStatuses with one-week strike expiry and pending user-status notices; unattended Discord egress defaults through Bifrost bridge posts/DMs with direct VoidBot send helper retained only behind VOID_DISCORD_TRANSPORT=direct fallback."} {"ts":"2026-06-04T20:43:18Z","type":"verification","status":"ok","note":"Rumination parent normalizes legacy top-level userStatuses model output into upsert_moderation_user_status operations; fixtures cover the stale shape and moderation status DM delivery."} {"ts":"2026-06-04T21:00:32Z","type":"implementation","status":"ok","note":"Legacy bare moderation cursor documents with top-level userStatuses now decode through voidModerationCursorSchema into canonical cursor shape; verified build, state-boundary fixture, rumination fixture, targeted bot/worker restart, and restored a lost Nibu summon through the heartbeat path (Discord message 1512199001309446295). Separate CultCache/CultMesh Extra-byte decode still remains if it recurs outside state-note-only jobs."} +{"ts":"2026-06-04T22:20:09Z","type":"implementation","status":"ok","note":"Repo Face SAY routing now marks parent-Interpreter stale/older-side-thread routes as requiring an explicit reply_to anchor; worker refuses those SAY intents instead of defaulting to an unanchored current_room post. Verified build, agents:speech-boundary-smoke, moderation state-boundary fixture, and restarted worker PID 2488."}