Harden repo Face public speech boundary
This commit is contained in:
parent
a016eb6534
commit
8f53f00a77
7 changed files with 296 additions and 104 deletions
|
|
@ -55,9 +55,15 @@ import {
|
|||
resolveArticleRepoRoot,
|
||||
validateRenderedArticleMarkdown,
|
||||
} from "./repo-face-article.js";
|
||||
import {
|
||||
REPO_IDENTITY_POST_SENTINEL,
|
||||
isNonPublicRepoIdentitySpeech,
|
||||
normalizePublicRepoIdentitySpeech,
|
||||
parseRepoIdentityPostIntents,
|
||||
type RepoIdentityPostIntent,
|
||||
} from "./repo-face-speech.js";
|
||||
|
||||
const config = loadConfig();
|
||||
const REPO_IDENTITY_POST_SENTINEL = "VOIDBOT_REPO_IDENTITY_POST:";
|
||||
const REPO_IDENTITY_PROPOSAL_PR_SENTINEL = "VOIDBOT_REPO_IDENTITY_PROPOSAL_PR:";
|
||||
const REPO_IDENTITY_PR_COMMENT_SENTINEL = "VOIDBOT_REPO_IDENTITY_PR_COMMENT:";
|
||||
const REPO_IDENTITY_UPDATE_REQUEST_SENTINEL = "VOIDBOT_REPO_IDENTITY_UPDATE_REQUEST:";
|
||||
|
|
@ -1013,13 +1019,6 @@ function parseRepoIdentityIdFromPrompt(prompt: string): string | undefined {
|
|||
);
|
||||
}
|
||||
|
||||
interface RepoIdentityPostIntent {
|
||||
identity?: string;
|
||||
channelId?: string;
|
||||
content: string;
|
||||
replyToMessageId?: string;
|
||||
}
|
||||
|
||||
interface RepoIdentityStateNoteIntent {
|
||||
identity?: string;
|
||||
kind: "memory" | "need" | "bond" | "status" | "mood" | "agency";
|
||||
|
|
@ -1129,8 +1128,15 @@ async function postRepoIdentityIntent(job: JobRecord, intent: RepoIdentityPostIn
|
|||
`Coerced repo identity ${identity.id} speech for job ${job.id} from "${requestedChannelId}" to conversation channel ${channelId}.`,
|
||||
);
|
||||
}
|
||||
const rawContent = intent.content.trim();
|
||||
if (isNonPublicRepoIdentitySpeech(rawContent)) {
|
||||
throw new Error(`Repo identity ${identity.id} SAY content is not public speech.`);
|
||||
}
|
||||
const content = normalizePublicRepoIdentitySpeech(rawContent);
|
||||
if (isNonPublicRepoIdentitySpeech(content)) {
|
||||
throw new Error(`Repo identity ${identity.id} SAY content is not public speech.`);
|
||||
}
|
||||
if (isOwnerDmChannelAlias(channelId)) {
|
||||
const content = intent.content.trim();
|
||||
const dmChannelId = await openOwnerDmChannel();
|
||||
const postedMessages = await postDiscordBotMessageChunks(
|
||||
dmChannelId,
|
||||
|
|
@ -1163,7 +1169,6 @@ async function postRepoIdentityIntent(job: JobRecord, intent: RepoIdentityPostIn
|
|||
return false;
|
||||
}
|
||||
|
||||
const content = intent.content.trim();
|
||||
if (content.length > 1900) {
|
||||
throw new Error(
|
||||
`Repo identity ${identity.id} SAY content is too long for one Discord message (${content.length} characters).`,
|
||||
|
|
@ -1172,9 +1177,6 @@ async function postRepoIdentityIntent(job: JobRecord, intent: RepoIdentityPostIn
|
|||
if (/\S(?:\.\.\.|…)$/.test(content)) {
|
||||
throw new Error(`Repo identity ${identity.id} SAY content appears mechanically truncated.`);
|
||||
}
|
||||
if (isNonPublicRepoIdentitySpeech(content)) {
|
||||
throw new Error(`Repo identity ${identity.id} SAY content is not public speech.`);
|
||||
}
|
||||
const contentFile = await writeBifrostPayloadFile(job, `${identity.id}-discord-post.md`, content);
|
||||
const posted = runBifrostBridge([
|
||||
"discord-post",
|
||||
|
|
@ -1244,92 +1246,6 @@ async function loadRegisteredFaceRepoRegistry() {
|
|||
);
|
||||
}
|
||||
|
||||
function parseRepoIdentityPostIntents(finalResponse: string): RepoIdentityPostIntent[] {
|
||||
const intents: RepoIdentityPostIntent[] = parseRepoFaceActionBlocks(finalResponse)
|
||||
.filter((block) => block.kind === "say")
|
||||
.flatMap((block): RepoIdentityPostIntent[] => {
|
||||
const content = requiredDslString(block.fields.content);
|
||||
if (!content) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
identity: optionalDslString(block.fields.identity),
|
||||
channelId: optionalDslString(block.fields.channel) ?? optionalDslString(block.fields.channelId),
|
||||
replyToMessageId: optionalDslString(block.fields.reply_to) ?? optionalDslString(block.fields.replyToMessageId),
|
||||
content,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
for (const line of finalResponse.split(/\r?\n/)) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed.startsWith(REPO_IDENTITY_POST_SENTINEL)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const payload = trimmed.slice(REPO_IDENTITY_POST_SENTINEL.length).trim();
|
||||
try {
|
||||
const parsed = JSON.parse(payload) as Record<string, unknown>;
|
||||
const content = typeof parsed.content === "string" ? parsed.content.trim() : "";
|
||||
if (!content) {
|
||||
continue;
|
||||
}
|
||||
intents.push({
|
||||
identity: typeof parsed.identity === "string" ? parsed.identity.trim() : undefined,
|
||||
channelId: typeof parsed.channelId === "string" ? parsed.channelId.trim() : undefined,
|
||||
replyToMessageId:
|
||||
typeof parsed.replyToMessageId === "string" ? parsed.replyToMessageId.trim() : undefined,
|
||||
content,
|
||||
});
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return intents;
|
||||
}
|
||||
|
||||
function isNonPublicRepoIdentitySpeech(value: string): boolean {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return true;
|
||||
}
|
||||
if (isSingleMarkdownFence(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
const unfenced = stripSingleMarkdownFence(trimmed);
|
||||
const normalized = unfenced
|
||||
.toLowerCase()
|
||||
.replace(/[.!?]+$/g, "")
|
||||
.trim();
|
||||
return [
|
||||
"nothing",
|
||||
"nothing right now",
|
||||
"nothing public",
|
||||
"nothing in aquarium",
|
||||
"stay private",
|
||||
"no public speech",
|
||||
].includes(normalized);
|
||||
}
|
||||
|
||||
function stripSingleMarkdownFence(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (!isSingleMarkdownFence(trimmed)) {
|
||||
return trimmed;
|
||||
}
|
||||
const singleLine = trimmed.match(/^`([^`\r\n]+)`$/);
|
||||
if (singleLine) {
|
||||
return singleLine[1].trim();
|
||||
}
|
||||
const block = trimmed.match(/^```[A-Za-z0-9_-]*\s*\r?\n([\s\S]*?)\r?\n```$/);
|
||||
return block ? block[1].trim() : trimmed;
|
||||
}
|
||||
|
||||
function isSingleMarkdownFence(value: string): boolean {
|
||||
return /^`[^`\r\n]+`$/.test(value) || /^```[A-Za-z0-9_-]*\s*\r?\n[\s\S]*?\r?\n```$/.test(value);
|
||||
}
|
||||
|
||||
function parseRepoIdentityIdFromRequestMessageId(requestMessageId: string | undefined): string | undefined {
|
||||
const match = requestMessageId?.match(/^agent-turn:([^:]+):/i);
|
||||
return match?.[1]?.trim();
|
||||
|
|
|
|||
194
apps/worker/src/repo-face-speech.ts
Normal file
194
apps/worker/src/repo-face-speech.ts
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
export const REPO_IDENTITY_POST_SENTINEL = "VOIDBOT_REPO_IDENTITY_POST:";
|
||||
|
||||
export interface RepoIdentityPostIntent {
|
||||
identity?: string;
|
||||
channelId?: string;
|
||||
content: string;
|
||||
replyToMessageId?: string;
|
||||
}
|
||||
|
||||
type RepoFaceActionBlock = {
|
||||
kind: "say" | "state_note" | "article" | "bifrost_topic" | "update_request";
|
||||
fields: Record<string, string>;
|
||||
};
|
||||
|
||||
export function parseRepoIdentityPostIntents(finalResponse: string): RepoIdentityPostIntent[] {
|
||||
const intents: RepoIdentityPostIntent[] = parseRepoFaceActionBlocks(finalResponse)
|
||||
.filter((block) => block.kind === "say")
|
||||
.flatMap((block): RepoIdentityPostIntent[] => {
|
||||
const content = requiredDslString(block.fields.content);
|
||||
if (!content) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
identity: optionalDslString(block.fields.identity),
|
||||
channelId: optionalDslString(block.fields.channel) ?? optionalDslString(block.fields.channelId),
|
||||
replyToMessageId: optionalDslString(block.fields.reply_to) ?? optionalDslString(block.fields.replyToMessageId),
|
||||
content,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
for (const line of finalResponse.split(/\r?\n/)) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed.startsWith(REPO_IDENTITY_POST_SENTINEL)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const payload = trimmed.slice(REPO_IDENTITY_POST_SENTINEL.length).trim();
|
||||
try {
|
||||
const parsed = JSON.parse(payload) as Record<string, unknown>;
|
||||
const content = typeof parsed.content === "string" ? parsed.content.trim() : "";
|
||||
if (!content) {
|
||||
continue;
|
||||
}
|
||||
intents.push({
|
||||
identity: typeof parsed.identity === "string" ? parsed.identity.trim() : undefined,
|
||||
channelId: typeof parsed.channelId === "string" ? parsed.channelId.trim() : undefined,
|
||||
replyToMessageId:
|
||||
typeof parsed.replyToMessageId === "string" ? parsed.replyToMessageId.trim() : undefined,
|
||||
content,
|
||||
});
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return intents;
|
||||
}
|
||||
|
||||
export function isNonPublicRepoIdentitySpeech(value: string): boolean {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return true;
|
||||
}
|
||||
if (isSingleMarkdownFence(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
const unfenced = stripSingleMarkdownFence(trimmed);
|
||||
const normalized = unfenced
|
||||
.toLowerCase()
|
||||
.replace(/[.!?]+$/g, "")
|
||||
.trim();
|
||||
return [
|
||||
"nothing",
|
||||
"nothing right now",
|
||||
"nothing public",
|
||||
"nothing in aquarium",
|
||||
"stay private",
|
||||
"no public speech",
|
||||
].includes(normalized);
|
||||
}
|
||||
|
||||
export function normalizePublicRepoIdentitySpeech(value: string): string {
|
||||
return value
|
||||
.replace(/```[A-Za-z0-9_-]*\s*\r?\n([\s\S]*?)\r?\n```/g, (_match, body: string) => body.trim())
|
||||
.replace(/`([^`\r\n]+)`/g, (_match, body: string) => `"${body.trim()}"`)
|
||||
.trim();
|
||||
}
|
||||
|
||||
function stripSingleMarkdownFence(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (!isSingleMarkdownFence(trimmed)) {
|
||||
return trimmed;
|
||||
}
|
||||
const singleLine = trimmed.match(/^`([^`\r\n]+)`$/);
|
||||
if (singleLine) {
|
||||
return singleLine[1].trim();
|
||||
}
|
||||
const block = trimmed.match(/^```[A-Za-z0-9_-]*\s*\r?\n([\s\S]*?)\r?\n```$/);
|
||||
return block ? block[1].trim() : trimmed;
|
||||
}
|
||||
|
||||
function isSingleMarkdownFence(value: string): boolean {
|
||||
return /^`[^`\r\n]+`$/.test(value) || /^```[A-Za-z0-9_-]*\s*\r?\n[\s\S]*?\r?\n```$/.test(value);
|
||||
}
|
||||
|
||||
function parseRepoFaceActionBlocks(finalResponse: string): RepoFaceActionBlock[] {
|
||||
const lines = finalResponse.split(/\r?\n/);
|
||||
const blocks: RepoFaceActionBlock[] = [];
|
||||
for (let index = 0; index < lines.length; index += 1) {
|
||||
const kind = parseRepoFaceActionKind(lines[index]);
|
||||
if (!kind) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bodyLines: string[] = [];
|
||||
index += 1;
|
||||
while (index < lines.length && lines[index].trim() !== "END") {
|
||||
if (parseRepoFaceActionKind(lines[index])) {
|
||||
index -= 1;
|
||||
break;
|
||||
}
|
||||
bodyLines.push(lines[index]);
|
||||
index += 1;
|
||||
}
|
||||
blocks.push({
|
||||
kind,
|
||||
fields: parseRepoFaceActionFields(bodyLines),
|
||||
});
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
function parseRepoFaceActionKind(line: string): RepoFaceActionBlock["kind"] | undefined {
|
||||
switch (line.trim().toUpperCase()) {
|
||||
case "SAY":
|
||||
return "say";
|
||||
case "STATE NOTE":
|
||||
return "state_note";
|
||||
case "ARTICLE":
|
||||
return "article";
|
||||
case "BIFROST TOPIC":
|
||||
return "bifrost_topic";
|
||||
case "UPDATE REQUEST":
|
||||
return "update_request";
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function parseRepoFaceActionFields(lines: string[]): Record<string, string> {
|
||||
const fields: Record<string, string> = {};
|
||||
let currentKey: string | undefined;
|
||||
let currentValue: string[] = [];
|
||||
|
||||
const flush = (): void => {
|
||||
if (!currentKey) {
|
||||
return;
|
||||
}
|
||||
fields[currentKey] = currentValue.join("\n").trim();
|
||||
};
|
||||
|
||||
for (const line of lines) {
|
||||
const match = line.match(/^([A-Za-z][A-Za-z0-9_]*):\s*(.*)$/);
|
||||
if (match) {
|
||||
flush();
|
||||
currentKey = match[1].trim();
|
||||
const inlineValue = match[2].trim();
|
||||
currentValue = inlineValue.length > 0 && inlineValue !== "|" && inlineValue !== ">" ? [match[2]] : [];
|
||||
continue;
|
||||
}
|
||||
if (currentKey) {
|
||||
currentValue.push(line.replace(/^\s{2}/, ""));
|
||||
}
|
||||
}
|
||||
flush();
|
||||
return fields;
|
||||
}
|
||||
|
||||
function optionalDslString(value: string | undefined): string | undefined {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed || trimmed.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (["0", "null", "none", "undefined"].includes(trimmed.toLowerCase())) {
|
||||
return undefined;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function requiredDslString(value: string | undefined): string | undefined {
|
||||
return optionalDslString(value);
|
||||
}
|
||||
|
|
@ -35,6 +35,7 @@
|
|||
"agents:assemble-character-prompt": "tsx scripts/run-repo-face-heartbeats.ts --assemble-prompt",
|
||||
"agents:heartbeat-model-smoke": "node scripts/smoke-repo-face-heartbeat-model-scenarios.mjs",
|
||||
"agents:interpreter-loop-smoke": "node scripts/smoke-repo-face-interpreter-loop.mjs",
|
||||
"agents:speech-boundary-smoke": "tsx scripts/smoke-repo-face-speech-boundary.ts",
|
||||
"agents:memory-projection-smoke": "node scripts/smoke-repo-face-memory-projection.mjs",
|
||||
"agents:article-contract-smoke": "tsx scripts/smoke-repo-face-article-contract.ts",
|
||||
"postgres:up": "docker compose -f infra/postgres/docker-compose.yml up -d",
|
||||
|
|
|
|||
79
scripts/smoke-repo-face-speech-boundary.ts
Normal file
79
scripts/smoke-repo-face-speech-boundary.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import {
|
||||
isNonPublicRepoIdentitySpeech,
|
||||
normalizePublicRepoIdentitySpeech,
|
||||
parseRepoIdentityPostIntents,
|
||||
} from "../apps/worker/src/repo-face-speech.js";
|
||||
|
||||
type Case = {
|
||||
name: string;
|
||||
run: () => boolean;
|
||||
detail: string;
|
||||
};
|
||||
|
||||
const cases: Case[] = [
|
||||
{
|
||||
name: "raw Would say fallback stays private",
|
||||
run: () => parseRepoIdentityPostIntents([
|
||||
"Private thought: I should stay quiet.",
|
||||
"Would say: `nothing right now.`",
|
||||
"What should stick: silence is correct.",
|
||||
].join("\n")).length === 0,
|
||||
detail: "A child Face draft must not create public speech unless the parent Interpreter emits SAY.",
|
||||
},
|
||||
{
|
||||
name: "structured silence SAY rejected",
|
||||
run: () => isNonPublicRepoIdentitySpeech("nothing right now."),
|
||||
detail: "Even a malformed Interpreter SAY cannot send silence markers as public content.",
|
||||
},
|
||||
{
|
||||
name: "single-line markdown fence rejected",
|
||||
run: () => isNonPublicRepoIdentitySpeech("`the whole message is a code span`"),
|
||||
detail: "A whole Face message wrapped as code is transport-shaped, not public speech.",
|
||||
},
|
||||
{
|
||||
name: "block markdown fence rejected",
|
||||
run: () => isNonPublicRepoIdentitySpeech("```text\nnothing right now\n```"),
|
||||
detail: "A whole Face message wrapped in a code block is not public speech.",
|
||||
},
|
||||
{
|
||||
name: "legitimate SAY normalizes inline code",
|
||||
run: () => {
|
||||
const posts = parseRepoIdentityPostIntents([
|
||||
"SAY",
|
||||
"identity: current_face_id",
|
||||
"channel: current_room",
|
||||
"content:",
|
||||
" `Wavecrafter` needs owners, costs, and a leash before it earns the noun.",
|
||||
"END",
|
||||
].join("\n"));
|
||||
return posts.length === 1 &&
|
||||
posts[0]?.content === "`Wavecrafter` needs owners, costs, and a leash before it earns the noun." &&
|
||||
!isNonPublicRepoIdentitySpeech(posts[0].content) &&
|
||||
normalizePublicRepoIdentitySpeech(posts[0].content) ===
|
||||
"\"Wavecrafter\" needs owners, costs, and a leash before it earns the noun.";
|
||||
},
|
||||
detail: "Inline code inside a real sentence remains valid, but the public mouth strips Markdown code styling.",
|
||||
},
|
||||
{
|
||||
name: "inline telemetry labels normalize",
|
||||
run: () => normalizePublicRepoIdentitySpeech(
|
||||
"show `rear pair drifting 38 ms`, `geometry confidence low`, `world model provisional`."
|
||||
) ===
|
||||
"show \"rear pair drifting 38 ms\", \"geometry confidence low\", \"world model provisional\".",
|
||||
detail: "Repo Faces can mention technical labels without speaking in code spans.",
|
||||
},
|
||||
];
|
||||
|
||||
const failures = cases.filter((entry) => !entry.run());
|
||||
if (failures.length > 0) {
|
||||
console.error(JSON.stringify({
|
||||
ok: false,
|
||||
failures: failures.map(({ name, detail }) => ({ name, detail })),
|
||||
}, null, 2));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
checked: cases.map(({ name }) => name),
|
||||
}, null, 2));
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"paused": true,
|
||||
"reason": "Operator reported repo Faces saying nothing or speaking in code blocks; pause while Face prompt and interpreter logs are inspected.",
|
||||
"updatedAt": "2026-06-03T00:36:00.0000000Z"
|
||||
"paused": false,
|
||||
"reason": "Resumed after live inspection showed the code-block report referred to pre-repair Discord messages; the public Face mouth now also normalizes inline Markdown code spans before Bifrost posting.",
|
||||
"updatedAt": "2026-06-03T01:52:05.1079871Z"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -326,3 +326,5 @@
|
|||
{"ts":"2026-06-02T16:54:34Z","type":"prompt","status":"ok","note":"Recast the Odin/Verse poem in the Face turn prompt toward a translated-Edda register while preserving the authority lesson: Odin witnesses Verse surfaces, local providers retain ownership, and agents touch commands only by explicit boundary."}
|
||||
{"ts":"2026-06-02T16:56:33Z","type":"prompt","status":"ok","note":"After reading public-domain Poetic Edda translations around Odin in Havamal, Vafthruthnismol, and Grimnismol, revised the Odin/Verse prompt poem toward Eddic devices: wisdom-travel formulae, counsel/riddle address, Hugin/Munin memory, rune/rede language, measured gifts, and alliterative half-lines."}
|
||||
{"ts":"2026-06-03T00:41:06Z","type":"implementation","status":"ok","note":"Paused repo Face swarm after operator reported silence/code-block public posts. Root cause: worker still allowed raw child Would say fallback and interpreter route fallback to write public speech; public mouth is now owned only by parent Interpreter SAY blocks, with transport guard rejecting full-message markdown fences and silence markers."}
|
||||
{"ts":"2026-06-03T01:46:49Z","type":"verification","status":"ok","note":"Repo Face speech boundary proof passed before unpause: agents:speech-boundary-smoke verified raw Would say drafts stay private, silence markers/full-message code fences are rejected, and legitimate SAY with inline code parses; typecheck, build, interpreter-loop smoke, and stack restart passed with fresh worker PID 21572."}
|
||||
{"ts":"2026-06-03T01:53:24Z","type":"verification","status":"ok","note":"Follow-up operator report was traced to pre-repair Discord messages; post-repair Mimir logs showed Interpreter-owned public text, and the repo Face mouth was hardened further so inline Markdown code spans normalize to quoted text before Bifrost posting."}
|
||||
|
|
|
|||
|
|
@ -251,5 +251,5 @@ rejected_paths:
|
|||
reason: sleep/distillation that collapses concrete claims into generic abstraction creates memory nonsense and then forces later cleanup compensators
|
||||
|
||||
current_status:
|
||||
summary: VoidBot's bot/worker/provider/RAG/Postgres/Qdrant/CultCache structure is live, but the repo Face swarm is paused after operator-reported silence/code-block public posts. The immediate fault was an obsolete worker fallback that let raw child draft speech write the public mouth when the parent Interpreter did not produce a normalized SAY block; that fallback has been cut, and transport now rejects full-message markdown fences plus silence markers before Discord posting. GameCult Local Orchestrator remains the single pulse owner, and the repo Face heartbeat path obeys `state/agent-swarm-paused.json`. Bifrost work routing remains paused by default.
|
||||
next_action: Keep the swarm paused until a dry/mock repo Face turn proves public speech is owned only by parent Interpreter `SAY` blocks, raw child silence/code-fence text cannot bypass the Interpreter, and the repaired worker build is the running worker. Then resume ordinary orchestrator pulses cautiously and watch first live posts.
|
||||
summary: VoidBot's bot/worker/provider/RAG/Postgres/Qdrant/CultCache structure is live, and the repo Face swarm has been resumed after the silence/code-block public-post fault. Public speech is now owned by parent Interpreter SAY blocks: the obsolete raw child draft-speech fallback is cut, silence markers and full-message markdown fences are rejected at the transport boundary, and inline Markdown code spans are normalized to quoted human text before Bifrost posting. Live inspection after the follow-up operator report showed the visible code-block examples were pre-repair Discord messages, not fresh post-boundary output. GameCult Local Orchestrator remains the single pulse owner, and the repo Face heartbeat path still obeys `state/agent-swarm-paused.json`. Bifrost work routing remains paused by default.
|
||||
next_action: Watch the next Discord-visible repo Face heartbeat turns for actual public behavior: no raw silence phrases, no full-message code fences, no child draft speech bypassing Interpreter SAY, and no revival of Bifrost/governance routing while that lane is disabled.
|
||||
|
|
|
|||
Loading…
Reference in a new issue