Use deterministic Face conversation transcripts
This commit is contained in:
parent
55be4b0ee7
commit
ccb590cfbd
4 changed files with 29 additions and 121 deletions
|
|
@ -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.
|
||||
|
|
@ -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}}
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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."}
|
||||
|
|
|
|||
Loading…
Reference in a new issue