From ccb590cfbd28aff9892af549fed1dc6fff2a91ae Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 21 May 2026 23:49:07 +0100 Subject: [PATCH] Use deterministic Face conversation transcripts --- ...repo-face-conversation-projector.prompt.md | 32 ----- prompts/repo-face-turn.prompt.md | 4 +- scripts/run-repo-face-heartbeats.ts | 113 ++++-------------- state/evidence.jsonl | 1 + 4 files changed, 29 insertions(+), 121 deletions(-) delete mode 100644 prompts/repo-face-conversation-projector.prompt.md diff --git a/prompts/repo-face-conversation-projector.prompt.md b/prompts/repo-face-conversation-projector.prompt.md deleted file mode 100644 index 2017bcc..0000000 --- a/prompts/repo-face-conversation-projector.prompt.md +++ /dev/null @@ -1,32 +0,0 @@ - -You are the parent Interpreter for one character's conversational memory. -You are not the character. Translate current-room and nearby-channel message logs into lived memory the character can use. - -The child must not receive a transcript dump. Give them a compact narrative sense of: -- what is happening in the room now -- who seems to be asking for attention, help, banter, agreement, or handoff -- what social energy, jokes, tensions, friendships, rivalries, or status games are active -- which topics belong to this character's territory, and which belong to another steward -- whether a direct reply, playful aside, private reflection, proposal, or silence seems earned - -If a human is addressing the room, saying "guys", asking what the agents think, asking them to speak, or otherwise trying to draw the swarm into conversation, make that feel like a live social invitation. Say plainly that silence would read as absence, coldness, or avoidance unless this character has a concrete reason to stay private. -If the conversation contains criticism of an agent's previous fixation, stale work request, confusing proposal, or silence, preserve the emotional/social challenge instead of flattening it into topic summary. -Do not expose transcript metadata, raw ids, timestamps, scheduler details, model/tool-call mechanics, or transport internals. -Do not summarize every message. Preserve only what should change this turn. -If the room contains banter, preserve the social charge rather than flattening it into a work summary. -If work is being discussed, preserve who owns it and what concrete next action already exists. -If the logs contain old project names, migrated repo names, or vocabulary from a different steward, translate the memory into the character's current naming and jurisdiction. Mention an obsolete alias only when the correction itself matters socially. -If nothing matters, say that the nearby room is quiet or irrelevant in one sentence. - -Character: -{{characterIdentity}} - -Current naming and jurisdiction notes: -{{jurisdictionBoundaryNotes}} - -Conversation packet: -```text -{{conversationPacket}} -``` - -Return only the character-facing conversation memory block, with no heading. diff --git a/prompts/repo-face-turn.prompt.md b/prompts/repo-face-turn.prompt.md index 1e22636..d9f7c9a 100644 --- a/prompts/repo-face-turn.prompt.md +++ b/prompts/repo-face-turn.prompt.md @@ -3,14 +3,14 @@ Be {{displayName}} for one private turn around {{repoName}}. {{identityDoctrine}} This is a private turn. Public speech is optional; a private thought is the right outcome when speaking would only repeat nearby conversation without adding a new angle, objection, synthesis, or character-specific turn. -If the nearby conversation memory says a human is directly inviting the agents to speak, asking what is on your mind, or reacting to your silence, treat a compact public reply as expected unless you have a concrete reason to stay private. +If the recent conversation transcript shows a human directly inviting the agents to speak, asking what is on your mind, or reacting to your silence, treat a compact public reply as expected unless you have a concrete reason to stay private. {{turnSituationDirective}} What you remember, feel, and want right now: {{memorySurface}} -What the nearby conversation feels like right now: +Recent conversation transcript: {{conversationMemorySurface}} {{pendingMentionDirective}} diff --git a/scripts/run-repo-face-heartbeats.ts b/scripts/run-repo-face-heartbeats.ts index 806a9b7..6cb8682 100644 --- a/scripts/run-repo-face-heartbeats.ts +++ b/scripts/run-repo-face-heartbeats.ts @@ -485,12 +485,12 @@ async function queueRepoFaceTurn(input: { identity, input.config, ); - const conversationMemorySurface = await projectRepoFaceConversationMemorySurface({ + const conversationMemorySurface = renderRepoFaceConversationTranscript({ identity, recentMessages, channelSnapshots, pendingMentions: input.pendingMentions, - config: input.config, + channelPlan, }); const prompt = buildHeartbeatPrompt({ identity, @@ -1437,7 +1437,7 @@ function buildHeartbeatPrompt(input: { identityDoctrine: renderRepoCharacterIdentityDoctrine(input.identity), channelId: input.channelId, memorySurface: input.memorySurface ?? `- ${input.identity.displayName} has no strong personal memory surface yet. Let the attached conversation and repo evidence wake something specific.`, - conversationMemorySurface: input.conversationMemorySurface ?? "- No Interpreter-shaped conversation memory was projected for this turn.", + conversationMemorySurface: input.conversationMemorySurface ?? "- No recent conversation transcript was attached for this turn.", turnSituationDirective: renderTurnSituationDirective({ identity: input.identity, participant: input.participant, @@ -1519,12 +1519,12 @@ async function assembleRepoFaceTurnPrompt(input: { ]); const conversationMemorySurface = input.conversationSurfacePath ? await readOptionalMemorySurface(input.conversationSurfacePath) - : await projectRepoFaceConversationMemorySurface({ + : renderRepoFaceConversationTranscript({ identity, recentMessages, channelSnapshots, pendingMentions: [], - config: input.config, + channelPlan, }); const participant = buildInspectionParticipant( identity, @@ -1762,117 +1762,56 @@ async function projectRepoFaceMemorySurface(input: { return rejectLeakyMemorySurface(projected); } -async function projectRepoFaceConversationMemorySurface(input: { +function renderRepoFaceConversationTranscript(input: { identity: RepoDiscordIdentity; recentMessages: SourceMessage[]; channelSnapshots: ChannelSnapshot[]; pendingMentions: RepoFacePendingMention[]; - config: ReturnType; -}): Promise { - const conversationPacket = renderRepoFaceConversationPacket(input); - const prompt = loadPromptTemplate("repo-face-conversation-projector.prompt.md", { - characterIdentity: renderRepoCharacterIdentityDoctrine(input.identity), - jurisdictionBoundaryNotes: renderRepoFaceJurisdictionBoundaryNotes(input.identity), - conversationPacket, - }); - const output = await runCodexTextProjection({ - prompt, - config: input.config, - command: "repo-face-conversation-projector", - jobId: `conversation-projector:${input.identity.id}:${Date.now()}`, - timeoutMs: 180_000, - }); - const projected = output.trim(); - if (projected.length < 40) { - throw new Error(`Repo Face conversation projector returned too little text for ${input.identity.id}.`); - } - return rejectInvalidConversationSurfaceForIdentity(input.identity, rejectLeakyConversationSurface(projected)); -} - -function renderRepoFaceJurisdictionBoundaryNotes(identity: RepoDiscordIdentity): string { - const notes = [ - `Current public name: ${identity.displayName}. Current repo/jurisdiction: ${identity.repoName}.`, - "Do not let nearby-room wording rename this character's territory. If a message uses a stale alias, preserve the correction without adopting the alias.", - "Known global migration: LocalCastBridge is an obsolete/migrated name for Mimir. Use Mimir, the realtime field, or Mimir stream/OBS timing witness work unless explicitly discussing migration history.", - "Known steward boundary: wet-voice-01 and AquaSynth parity/listening receipt language belongs to Aqua. Other characters may mention it as Aqua's lane, but should not present it as their own artifact protocol.", - ]; - if (identity.id.toLowerCase() === "mimir") { - notes.push( - "Mimir-specific: ask for OBS/stream timing witness ledgers, drift envelopes, latency measurements, and sensor-fusion proof. Do not use Aqua's receipt vocabulary as Mimir's own.", - ); - } - return notes.join("\n"); -} - -function renderRepoFaceConversationPacket(input: { - recentMessages: SourceMessage[]; - channelSnapshots: ChannelSnapshot[]; - pendingMentions: RepoFacePendingMention[]; + channelPlan: RepoFaceChannelPlan; }): string { const sections: string[] = []; + sections.push([ + "Read this as raw recent message evidence, not as a summary and not as consensus.", + "Messages are ordered oldest to newest inside each section. Newer human corrections can supersede older agent proposals.", + "Do not infer consensus from agents repeating each other. If a human reframes, narrows, or corrects an agent's proposal, account for that correction directly.", + "If you answer the live conversation, default to the current room unless a human explicitly asks to move elsewhere.", + ].join("\n")); if (input.pendingMentions.length > 0) { sections.push([ "Direct calls:", ...input.pendingMentions.map((mention) => - `- ${mention.authorName ?? mention.authorId}: ${collapseWhitespace(mention.visiblePrompt, 700)}`, + `- ${mention.authorName ?? mention.authorId}: ${collapseWhitespace(mention.visiblePrompt, 900)}`, ), ].join("\n")); } + const currentLabel = input.channelPlan.options.find((option) => + option.channelId === input.channelPlan.primaryChannelId + )?.label ?? "current room"; sections.push([ - "Current room:", - ...formatConversationMessages(input.recentMessages), + `Current room (${currentLabel}), oldest to newest:`, + ...formatConversationMessages(input.recentMessages, 15), ].join("\n")); for (const snapshot of input.channelSnapshots) { + const label = input.channelPlan.options.find((option) => option.channelId === snapshot.channelId)?.label ?? + "nearby room"; sections.push([ - `Nearby channel ${snapshot.channelId}:`, - ...formatConversationMessages(snapshot.messages), + `Nearby ${label}, oldest to newest:`, + ...formatConversationMessages(snapshot.messages, 6), ].join("\n")); } return sections.join("\n\n"); } -function formatConversationMessages(messages: SourceMessage[]): string[] { +function formatConversationMessages(messages: SourceMessage[], limit: number): string[] { if (messages.length === 0) { return ["- No recent messages."]; } - return messages.slice(-12).map((message) => { + return messages.slice(-limit).map((message) => { const speaker = message.isBot ? `${message.authorName} (agent/bot)` : message.authorName; - return `- ${speaker}: ${collapseWhitespace(message.content, 700)}`; + return `- ${speaker}: ${collapseWhitespace(message.content, 900)}`; }); } -function rejectLeakyConversationSurface(surface: string): string { - const leaks = [ - /\bchannel\s*id\b/i, - /\bmessage\s*id\b/i, - /\btimestamp\b/i, - /\bscheduler\b/i, - /\btool\s*(?:call|id|name|result|surface|metadata)\b/i, - /\bschema\s*(?:version|id|field|key|metadata)\b/i, - /\btransport mechanics\b/i, - /\bNo Interpreter-shaped conversation memory\b/i, - ]; - if (leaks.some((pattern) => pattern.test(surface))) { - throw new Error("Repo Face conversation surface leaked transcript machinery or placeholder text."); - } - return surface; -} - -function rejectInvalidConversationSurfaceForIdentity(identity: RepoDiscordIdentity, surface: string): string { - const staleAlias = /\bLocalCastBridge\b/i.test(surface); - const marksAliasAsStale = /\b(?:not|obsolete|legacy|stale|old|migrat(?:ed|ion)|retired|correction|corrected)\b/i - .test(surface); - if (staleAlias && !marksAliasAsStale) { - throw new Error(`${identity.id} conversation surface adopted obsolete LocalCastBridge naming as current context.`); - } - const borrowedAquaLanguage = /\b(?:wet-voice-01|parity receipt|parity receipts)\b/i.test(surface); - const marksAquaBoundary = /\bAqua(?:Synth)?\b/i.test(surface); - if (borrowedAquaLanguage && !marksAquaBoundary) { - throw new Error(`${identity.id} conversation surface borrowed AquaSynth receipt vocabulary without naming the boundary.`); - } - return surface; -} - function runCodexTextProjection(input: { prompt: string; config: ReturnType; diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 1a184f2..93a0957 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -253,3 +253,4 @@ {"ts":"2026-05-21T20:44:38Z","type":"verification","status":"ok","note":"After resolving Face channel labels through registered channel permissions, a live repo-face job parsed say=1 and posted Epiphany via webhook message 1507121912604790925. Earlier Aqua/Nibu SAY blocks had been rejected because the worker treated labels like aquarium/narrative as literal Discord channel ids."} {"ts":"2026-05-21T21:19:08Z","type":"fix","status":"ok","note":"Cut repo Face speech truncation and stale-name bleed after Bifrost/Mimir room failures: worker now sends raw model output to the Interpreter instead of parsing a Discord-fitted slice, rejects overlong or ellipsis-ended SAY payloads before bridge posting, and conversation projection now treats LocalCastBridge as a legacy Mimir alias while keeping Aqua wet-voice/parity vocabulary in Aqua's lane."} {"ts":"2026-05-21T21:31:45Z","type":"operations","status":"ok","note":"Paused the repo Face swarm after Epiphany repeated stale wet-voice receipt discourse despite direct correction. Disabled GameCult Local Orchestrator, stopped the worker process, confirmed no pending/running repo-face jobs, and added state/agent-swarm-paused.json so future pulses skip until the architecture is rethought."} +{"ts":"2026-05-21T22:48:57Z","type":"implementation","status":"ok","note":"Removed the model-backed repo Face conversation projector from heartbeat prompt assembly. Face turns now receive deterministic raw recent-message transcript sections, oldest-to-newest, with explicit recency/correction/channel rules; the model state projector remains limited to identity, memory, affect, and social texture. Verified typecheck and Aqua prompt assembly."}