Require reply anchors for stale Face threads

This commit is contained in:
Meta Vens 2026-06-04 23:20:20 +01:00
commit cd8d1728f0
4 changed files with 50 additions and 0 deletions

View file

@ -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)) {

View file

@ -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) {

View file

@ -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());

View file

@ -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."}