Use deterministic Face conversation transcripts

This commit is contained in:
Meta Vens 2026-05-21 23:49:07 +01:00
commit ccb590cfbd
4 changed files with 29 additions and 121 deletions

View file

@ -1,32 +0,0 @@
<!-- prompt:repo-face-conversation-projector -->
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.

View file

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

View file

@ -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<typeof loadConfig>;
}): Promise<string> {
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<typeof loadConfig>;

View file

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