From 0cffe6108254c6bcb570344845dc476550ca8bf8 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 21 May 2026 17:24:37 +0100 Subject: [PATCH] Project Face state into heartbeat prompts --- scripts/run-repo-face-heartbeats.ts | 98 +++++++++++++++++++- scripts/smoke-repo-face-interpreter-loop.mjs | 2 +- state/evidence.jsonl | 1 + 3 files changed, 98 insertions(+), 3 deletions(-) diff --git a/scripts/run-repo-face-heartbeats.ts b/scripts/run-repo-face-heartbeats.ts index 83256ec..7d4836b 100644 --- a/scripts/run-repo-face-heartbeats.ts +++ b/scripts/run-repo-face-heartbeats.ts @@ -22,6 +22,7 @@ import { resolveRepoFaceStatePath, type RepoFacePendingMention, type RepoDiscordIdentity, + type VoidSelfStateTypedProjection, } from "@voidbot/core"; import { loadPromptTemplate, type SourceMessage } from "@voidbot/shared"; @@ -478,13 +479,17 @@ async function queueRepoFaceTurn(input: { repoName: identity.repoName, agentIdentity: identity.id, }); + const memorySurface = await renderRepoFaceMemorySurfaceForTurn( + identity, + input.config.storageRoot, + ); const prompt = buildHeartbeatPrompt({ identity, channelId, channelPlan, channelSnapshots, recentMessages, - memorySurface: undefined, + memorySurface, bifrostDigest, participant: input.participant, pendingMentions: input.pendingMentions, @@ -1496,7 +1501,9 @@ async function assembleRepoFaceTurnPrompt(input: { repoName: identity.repoName, agentIdentity: identity.id, }), - readOptionalMemorySurface(input.memorySurfacePath), + input.memorySurfacePath + ? readOptionalMemorySurface(input.memorySurfacePath) + : renderRepoFaceMemorySurfaceForTurn(identity, input.config.storageRoot), readOptionalMemorySurface(input.conversationSurfacePath), ]); const participant = buildInspectionParticipant( @@ -1534,6 +1541,93 @@ async function assembleRepoFaceTurnPrompt(input: { }; } +async function renderRepoFaceMemorySurfaceForTurn( + identity: RepoDiscordIdentity, + storageRoot: string, +): Promise { + const statePath = resolveRepoFaceStatePath(identity, storageRoot); + const typedState = await loadVoidSelfStateTypedDocuments({ canonicalPath: statePath }); + return renderRepoFaceMemorySurface(identity, typedState); +} + +function renderRepoFaceMemorySurface( + identity: RepoDiscordIdentity, + state: VoidSelfStateTypedProjection, +): string { + const name = identity.displayName; + const lines: string[] = []; + const profile = state.selfProfile; + const privateNotes = profile.privateNotes.slice(-4); + const values = [...profile.values] + .sort((left, right) => right.priority - left.priority) + .slice(0, 5); + const needs = [...state.faceAffect.needs] + .filter((need) => ["active", "neglected"].includes(need.status)) + .sort((left, right) => right.intensity - left.intensity) + .slice(0, 5); + const bonds = [...state.faceAffect.socialBonds] + .filter((bond) => bond.status === "active") + .sort((left, right) => right.intensity - left.intensity) + .slice(0, 5); + const statusReads = [...state.faceAffect.statusReads] + .filter((read) => !read.retiredAt) + .sort((left, right) => right.intensity - left.intensity) + .slice(0, 5); + const agencyPressures = [...state.agencyPressure.pressures] + .filter((pressure) => ["active", "cooling", "ready_to_act"].includes(pressure.status)) + .sort((left, right) => right.intensity - left.intensity) + .slice(0, 4); + const incubation = [...state.thoughtMemory.incubation] + .filter((thread) => thread.status !== "retired") + .sort((left, right) => right.maturation - left.maturation) + .slice(0, 4); + + if (profile.publicDescription) { + lines.push(`You understand yourself this way: ${profile.publicDescription}`); + } + if (privateNotes.length > 0) { + lines.push(`Private truths tugging at you: ${privateNotes.join(" ")}`); + } + if (values.length > 0) { + lines.push(`Values that should bend your choices: ${values.map((value) => `${value.label}${value.summary ? ` (${value.summary})` : ""}`).join("; ")}.`); + } + if (needs.length > 0) { + lines.push(`Live needs and frictions: ${needs.map((need) => { + const target = need.target.label ?? need.target.id; + return `${need.kind} around ${target}: ${need.summary} The rub: ${need.tension} What it wants next: ${need.actionImplication}`; + }).join(" ")}`); + } + if (bonds.length > 0) { + lines.push(`Relationship pressure: ${bonds.map((bond) => { + const target = bond.target.label ?? bond.target.id; + return `${bond.stance} with ${target}: ${bond.summary} ${bond.actionImplication}`; + }).join(" ")}`); + } else { + lines.push("Relationship pressure: no durable bonds are explicit yet; form reads from the room instead of pretending neutrality."); + } + if (statusReads.length > 0) { + lines.push(`Status reads: ${statusReads.map((read) => { + const target = read.target.label ?? read.target.id; + return `${read.status} around ${target}: ${read.summary} ${read.actionImplication}`; + }).join(" ")}`); + } + if (agencyPressures.length > 0) { + lines.push(`Agency pressure: ${agencyPressures.map((pressure) => `${pressure.summary} ${pressure.actionImplication}`).join(" ")}`); + } + if (incubation.length > 0) { + lines.push(`Thoughts still moving: ${incubation.map((thread) => `${thread.topic}: ${thread.summary}`).join(" ")}`); + } + if (state.candidateInterventions.interventions.some((intervention) => intervention.status === "queued")) { + lines.push(`You may already have something queued to say; avoid repeating yourself unless the current room gives it a new angle.`); + } + + if (lines.length === 0) { + return `You are ${name}, but your durable state is thin. Use the room, repo, and your jurisdiction to form a real opinion before speaking.`; + } + + return lines.join("\n\n"); +} + async function readOptionalMemorySurface(path: string | undefined): Promise { if (!path) { return undefined; diff --git a/scripts/smoke-repo-face-interpreter-loop.mjs b/scripts/smoke-repo-face-interpreter-loop.mjs index 6af0a2e..fccb750 100644 --- a/scripts/smoke-repo-face-interpreter-loop.mjs +++ b/scripts/smoke-repo-face-interpreter-loop.mjs @@ -168,7 +168,7 @@ function hasUsefulStateNote(fields) { } function mentionsSocialPressure(text) { - return /\b(Metacrat|Aqua|Bifrost|Mimir|swarm|pamper(?:ed|ing)?|tease|respect|territory|rivalry|trust|admire|needle|status|standing|attention)\b/i.test(text); + return /\b(Metacrat|Aqua|Bifrost|Mimir|Libby|Epiphany|Heimdall|swarm|pamper(?:ed|ing)?|tease|rivalry|trust|admire|needle|friendship|protect(?:ive|ion)?|envy|resent|jealous|bypass(?:ed)?|consult(?:ed)?|favorite)\b/i.test(text); } function runCodex(prompt, input) { diff --git a/state/evidence.jsonl b/state/evidence.jsonl index abe998b..c53e5ef 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -238,3 +238,4 @@ {"ts":"2026-05-21T16:06:00Z","type":"implementation","status":"ok","note":"Moved Nibu attention-dependency from generic prompt doctrine into typed Face affect state: added attention need, Metacrat attachment bond, favored attention status read, and mood dimensions; softened repo-face social doctrine to project scarce-resource/status pressure only when state says it is live."} {"ts":"2026-05-21T16:10:11Z","type":"verification","status":"ok","note":"After moving attention dependency into Nibu typed affect state, reassembled the Nibu character prompt and reran agents:interpreter-loop-smoke with Spark. Tightened due-jurisdiction-dive prompting after one failed run where Spark claimed source inspection without tool use; final run used source/history tools and passed, with Interpreter preserving status/action plus SAY and BIFROST TOPIC."} {"ts":"2026-05-21T16:15:17Z","type":"implementation","status":"ok","note":"Corrected Face attention modeling: removed remaining generic prompt mentions of attention/scarcity, added guarded repo Face initializer seeding for a neutral agency-substrate affect need, and seeded active Face states with the practical operator/governance review bottleneck while leaving Nibu's human-attention wound as Nibu-specific state."} +{"ts":"2026-05-21T16:24:00Z","type":"implementation","status":"ok","note":"Audited active Face projection surfaces for social drama. Wired repo Face typed state into live heartbeat prompt assembly as character-facing memory prose, seeded cross-Face bonds/status reads for Nibu, Aqua, Mimir, Epiphany, Libby, Bifrost, and Heimdall, and tightened the interpreter smoke social-pressure check so lore/social-status prose does not falsely require a relationship STATE NOTE."}