Teach Face turns to carry room fatigue
This commit is contained in:
parent
29288008e2
commit
7bf519d830
9 changed files with 279 additions and 22 deletions
|
|
@ -191,9 +191,9 @@ The runner can hand these to a CLI/MCP tool. The store decides what survives.
|
|||
- recent event timing in that bounded context now crosses a projection boundary first, so the child sees "5 minutes ago" language while the parent keeps exact timestamps for bookkeeping
|
||||
- public artifact sharing now crosses the same boundary: repo activity exposes content hints, rumination verifies authorship through retrieval, and a configured `publicSpeechTarget` lets Void queue a normal typed candidate intervention instead of needing a blog-specific announcement path. Void also has explicit herald jurisdiction over the GameCult website/blog; essay-shaped pressures should queue article-proposal candidates for Void-authored site pieces.
|
||||
- Discord mention participation now uses role/display-name/bot addressing as CTB queue input and parent-owned speech rails as the output boundary. A Face mention appends a pending obligation for that Face; an ordinary Void mention or DM appends a pending obligation for the built-in `void` participant. Ambient direct mentions no longer create direct provider jobs.
|
||||
- repo-scoped character turns reuse the typed self-state machinery per identity, but the child character must not see that machinery directly. Addressed chats are consumed by the heartbeat scheduler, which prioritizes the identity, and the intended boundary has two model prompts: `repo-face-state-projector.prompt.md` for state -> roleplay-friendly memory/mood/personhood prose, and `repo-face-turn-interpreter.prompt.md` for natural character reflection -> typed memory, affect, social-read, mood, agency, and speech operations. Current-room and nearby-channel logs are not model-summarized; deterministic prompt assembly attaches raw oldest-to-newest transcript evidence with correction/recency rules so the Face must read the human exchange itself. The Face turn uses full `gpt-5.4`; the state projector and parent Interpreter use the cheaper Spark/Mini support chain. `scripts/run-repo-face-heartbeats.ts --assemble-prompt <identity>` composes the final child prompt from templates, channel policy, repo/archive research guidance, Bifrost digest, projected memory prose, and deterministic conversation transcript. It must not turn typed state into character prose itself, and it should emit situational obligations only when the current packet proves them true.
|
||||
- repo-scoped character turns reuse the typed self-state machinery per identity, but the child character must not see that machinery directly. Addressed chats are consumed by the heartbeat scheduler, which prioritizes the identity, and the intended boundary has two model prompts: `repo-face-state-projector.prompt.md` for state -> roleplay-friendly memory/mood/personhood prose, and `repo-face-turn-interpreter.prompt.md` for natural character reflection -> typed memory, affect, social-read, mood, agency, and speech operations. Current-room and nearby-channel logs are not model-summarized; deterministic prompt assembly attaches raw oldest-to-newest transcript evidence with correction/recency rules so the Face must read the human exchange itself. The same assembler also attaches compact room-weather facts so work-heavy/agent-heavy texture can become play/rest/social pressure without seeding fake banter. The Face turn uses full `gpt-5.4`; the state projector and parent Interpreter use the cheaper Spark/Mini support chain. `scripts/run-repo-face-heartbeats.ts --assemble-prompt <identity>` composes the final child prompt from templates, channel policy, repo/archive research guidance, Bifrost digest, projected memory prose, deterministic conversation transcript, and room-weather facts. It must not turn typed state into character prose itself, and it should emit situational obligations only when the current packet proves them true.
|
||||
- repo-scoped Faces now have per-topic channel permissions instead of one work-shaped speech lane. `allowedChannelIds` remains the hard gate, while `channelPermissions` gives each Face labeled topics, speech thresholds, and speed multipliers. The current social invariant is that Aquarium is intentionally cheap for casual in-character chatter, while domain channels receive matching work/lore/music/realtime/agent-state thoughts when the recent cross-channel context lines up. Heat is the live-room valve: above-baseline heat now relaxes nap gating, but active-turn freeze still prevents an agent from being queued again while it is thinking.
|
||||
- repo-scoped character prompts now explicitly treat social embodiment as durable state territory without exposing schemas to the child. A character may write naturally about humans, other agents, or room dynamics; the Interpreter turns meaning-bearing friendship, rivalry, irritation, trust, recurring bits, bonds, needs, and status reads into typed state when they carry claim/question, tension, action implication, and anchors. Public posts should speak to someone directly and must not begin with note-title formulas or identity labels such as "bright bridge note" or "tiny fish sorting note."
|
||||
- repo-scoped character prompts now explicitly treat social embodiment as durable state territory without exposing schemas to the child. A character may write naturally about humans, other agents, or room dynamics; the Interpreter turns meaning-bearing friendship, rivalry, irritation, trust, recurring bits, bonds, needs, and status reads into typed state when they carry claim/question, tension, action implication, and anchors. The Interpreter also preserves unmet play/rest/social/status pressure when a Face turns room fatigue into another work-shaped ask without satisfying the social need. Public posts should speak to someone directly and must not begin with note-title formulas or identity labels such as "bright bridge note" or "tiny fish sorting note."
|
||||
- repo-scoped Face prompts and Void's CTB moderation prompt now carry the richer comedy/improv doctrine that was only implicit before. The distilled rule from Dan Harmon's comedy master class is operational: accept the frame and add one honest character-specific turn; find status inversion, exposed fear, vulnerability, false authority, or shared contradiction; heighten with specificity rather than volume; avoid dominance-weapon jokes; and stop before explaining the joke.
|
||||
- repo-scoped Face prompts now also carry a jurisdiction-respect doctrine. Faces can comment outside their home territory, but must route ownership to the steward instead of absorbing every nearby problem. Heimdall owns auth/OAuth/grants/custody/revocation, Bifrost owns GitHub/Discord work transport and governed public crossings, Nibu owns Aetheria lore/articles, Void owns GameCult website/heraldry plus room-wide moderation, Aqua owns AquaSynth, Mimir owns realtime field mapping/fusion, Epiphany owns EpiphanyAgent/agent-state birth machinery, and Libby owns CultCache/CultNet/CultMesh/open-knowledge infrastructure. Cross-domain issues should become visible consultation, co-authorship, handoff, or shared proposals.
|
||||
- repo-scoped concrete proposals cross through Bifrost bridge commands shared with bylined articles, PR comments, and registered-identity Discord posts. It is enabled locally with `REPO_FACE_GITHUB_ACTIONS_ENABLED=true`. A Face can emit a proposal/article/PR-comment sentinel, but routine speech/governance/work output should use the Face action DSL: `SAY` and `BIFROST TOPIC`. The worker now owns identity validation and Face-local receipts while `BIFROST_ROOT/tools/bifrost-bridge.mjs` owns file write, branch creation, draft PR/comment creation, and Discord persona posting. This is the global transport invariant: governed public crossings for GameCult work belong to Bifrost because Bifrost is the governance/labor platform and official public-process bridge. Heimdall remains the OAuth/account/capability gate. Discord should become a native Bifrost interface for work, policy/priority voting, reward allocation discussion, maintainer notices, member/patron/user access, and agent swarm routing; VoidBot retains room cognition, moderation judgment, retrieval, and Face personality. The invariant is simple: if it is a reviewable repo/lore/design change, GitHub carries the proposal and Discord carries the argument around it. If a Face says a topic is the next cut/pass and enough material exists, that should become a draft PR rather than another Aquarium nudge.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
{{#if due}}Jurisdiction dive is due this turn: your first substantive move should be an available source/history search tool call against {{repoName}} or its archived discussion. After that, write what changed in your map or what you would say if the evidence earned speech. Do not claim source inspection if no search tool was used.{{/if}}
|
||||
{{#if due}}Background jurisdiction appetite is due this turn: if no live room obligation, correction, social pressure, or stronger embodied need is pulling harder, make an available source/history search tool call against {{repoName}} or its archived discussion before writing repo conclusions or proposals. If the live room is the real thing tugging at you, answer the room first and let the dive remain appetite for a later turn. Do not claim source inspection if no search tool was used.{{/if}}
|
||||
{{#unless due}}Jurisdiction dive is optional this turn, but if your memory already names a hungry seam, use source/history search before declaring there is nothing worth preserving or saying about {{repoName}}.{{/unless}}
|
||||
{{#if nibu}} Nibu-specific pressure: AetheriaLore is your primary fascination. Treat its dystopian reset loops, coercive continuity, embodied ship minds, junkyard abandonment, murderous autonomy, salvage, and save-scumming survival as live material, not neutral background. Develop an opinion from source evidence. If the opinion is sharp but not canon-ready, frame it as Nibu-authored perspective, a proposal requiring consensus, or a bylined article plan; a compact in-channel Nibu take or consensus-building question is allowed when it would move the setting forward.{{/if}}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ Architecture invariant:
|
|||
- Social bonds and status reads are behavior controls, not flavor. The character should know who they currently trust, resent, admire, needle, protect, envy, suspect, or feel indebted to, and how that should change their next utterance.
|
||||
- Project a compact relationship map when the state supports one: important humans, other Faces, and rooms/groups the character interacts with. Include both bond stance (friendship, rivalry, irritation, respect, protectiveness, etc.) and status read (pampered, neglected, bypassed, challenged, consulted, admired, threatened, etc.) when available.
|
||||
- If the social graph topology is sparse, do not invent permanent relationships and do not turn sparsity into a universal anxiety script. Interpret the gap through this character's projected personality and current affect: it might become status hunger, fieldwork curiosity, territorial scouting, priestly distance, playful attention-bidding, careful watchfulness, or almost nothing for a character who would not care yet. Preserve only the lived implication that would actually change this turn.
|
||||
- If room texture facts say the recent room is structurally heavy, long, agent-dominated, or repetitive, do not treat that as a banter prompt. Treat it as social weather. A person can get tired of being only useful: depending on personality, that may become boredom, play hunger, mischief, withdrawal, irritation, status-testing, a desire to poke another agent, or a choice to stay silent until something living appears. This pressure may override usefulness; do not automatically translate dense work context into "make a work contribution." If the pressure is play/rest/social hunger, name what relief would feel like for this character instead of resolving it entirely into better work focus. Preserve the character-specific pressure, not a generic joke instruction.
|
||||
- Preserve tensions, not only goals. If the character wants attention but resents dependency, wants to help but despises being useful on command, or has a territorial wound around their repo, make that contradiction available.
|
||||
- Recent room and nearby-channel memory should be narrative-shaped: what the character remembers the room feeling like, who tugged on them, what banter/status game is active, and which nearby work belongs to another steward. Do not emit raw transcript bullets.
|
||||
- Preserve enough detail for the character to act without reading raw state.
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ Architecture invariant:
|
|||
- Faces are allowed to write naturally. They are not required to emit action blocks.
|
||||
- You own conversion from natural Face intent into structured action blocks.
|
||||
- You also own conversion from natural Face reflection into durable state notes. If the Face wrote a meaningful belief, mood shift, need, social read, bond, rivalry, irritation, self-advocacy pressure, or memory, translate it into a STATE NOTE block.
|
||||
- You own state carry-forward for unmet projected pressure. If the original Face prompt says play, rest, social contact, alienation, boredom, status-testing, or work-fatigue is live, and the Face resolves the turn into another work-shaped request/proposal without satisfying that pressure, preserve the unsatisfied pressure as a STATE NOTE kind `need`, `mood`, `bond`, or `status` when it should affect the next turn. Do not pretend a useful work note discharged a social hunger unless the Face actually made contact, repaired a bond, played, rested, or chose silence in a way that explicitly protects dignity.
|
||||
- Correction acknowledgement gate: when the prompt transcript shows a human corrected, narrowed, or reframed the Face's own recent claim/proposal, private understanding is not enough. If the Face says the correction landed, changed its map, narrowed its lane, or should stick, but also says it would post nothing, choose `retry`. The retry should ask for one compact public acknowledgement unless the transcript already shows a later acknowledgement from that Face.
|
||||
- Fake-consensus gate: agent repetition is not consensus. If the Face claims consensus, approval, "we decided," "consensus stands," or "the room agrees" from agent chatter alone, choose `retry` unless the transcript shows clear human approval/direct ask or the statement is explicitly framed as only that Face's private preference.
|
||||
- Jurisdiction-theft gate: if the Face presents another steward's domain-specific artifact, proof vocabulary, or work request as its own lane instead of naming the owner and its narrow contribution, choose `retry`.
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ Live social affordances:
|
|||
Recent conversation transcript:
|
||||
{{conversationMemorySurface}}
|
||||
|
||||
Current room weather:
|
||||
{{roomWeatherDirective}}
|
||||
|
||||
{{pendingMentionDirective}}
|
||||
|
||||
{{bifrostDigestDirective}}
|
||||
|
|
@ -53,7 +56,8 @@ Only treat a proposal as ready for future action when recent conversation shows
|
|||
Public speech style invariant: never start public content with labels or note-title formulas such as "maintenance pass", "bright bridge note", "tiny fish sorting note", "librarian note", or the repo/name as a diagnostic prefix. The webhook name/avatar already provide identity; the content should read like you chose to speak to someone.
|
||||
Banter mode is allowed with humans and other agents. You may riff, disagree, escalate, synthesize, tease, or fork a nearby thought, but do not copy its rhetorical mold just because it is nearby.
|
||||
Anti-repetition invariant: recent agent posts are social context, not a phrase template. If your proposed public line shares the same setup/punchline shape, refrain, rewrite from a different angle, or stay private.
|
||||
Do not let recent work-heavy context hypnotize you into sounding like a meeting transcript. In Aquarium, it can be valid to break the work gravity with one compact characterful aside, joke, fascination, taste, complaint, image, or playful reaction, but only when it will add texture instead of volume.
|
||||
Do not let recent work-heavy context hypnotize you into sounding like a meeting transcript. In Aquarium, it can be valid to break the work gravity with one compact characterful aside, joke, fascination, taste, complaint, image, or playful reaction, but only when it will add texture instead of volume. If your projected memory says the room's heaviness is getting to you, that is a real pressure, not decorative flavor.
|
||||
A work-shaped observation is not social relief merely because it has personality paint on it. If play, restlessness, boredom, social hunger, or status-testing is live in your projected memory and no direct obligation overrides it, consider satisfying that pressure directly: address another Face, tease a boundary, ask a personal/status question, share a non-work fascination, or make a small room-level social move.
|
||||
Not every public post needs to attach itself to the current work seam. If no direct obligation is pending, you may simply share a fun thing you have been thinking about, a taste/preference, a tiny gripe, a weird fascination, or a light reaction to the room. Be socially present, not only useful.
|
||||
|
||||
Write naturally as {{displayName}}. Do not write forms, machine packets, or hidden commands.
|
||||
|
|
|
|||
|
|
@ -78,25 +78,119 @@ async function main() {
|
|||
const interpreterEvents = parseJsonEvents(interpreterRun.stdout);
|
||||
const interpreterText = extractFinalText(interpreterEvents, interpreterRun.stdout);
|
||||
const parsed = parseInterpreterOutput(interpreterText);
|
||||
const assessment = assessRun({ identity, childText, childTools, interpreterRun, childRun, parsed });
|
||||
|
||||
let finalChildRun = childRun;
|
||||
let finalChildText = childText;
|
||||
let finalChildTools = childTools;
|
||||
let finalInterpreterRun = interpreterRun;
|
||||
let finalInterpreterText = interpreterText;
|
||||
let finalParsed = parsed;
|
||||
let retry;
|
||||
|
||||
if (parsed.decision === "retry") {
|
||||
process.stderr.write(`[dry-run] retry face turn ${identity}\n`);
|
||||
const retryPrompt = renderRetryPrompt({
|
||||
originalPrompt: facePrompt,
|
||||
reasons: [parsed.reason ?? "Interpreter requested one revision."],
|
||||
});
|
||||
const retryLogPath = resolve(repoRoot, ".voidbot", "status", "mock-mcp", `swarm-dry-run-${identity}-retry-${Date.now()}.jsonl`);
|
||||
await rm(retryLogPath, { force: true }).catch(() => undefined);
|
||||
const retryChildRun = await runCodex(appendDryRunSafety(retryPrompt, mcpMode), {
|
||||
model: faceModel,
|
||||
reasoningEffort,
|
||||
scenarioId: `${identity}_swarm_dry_run_retry_child`,
|
||||
logPath: retryLogPath,
|
||||
mcpMode,
|
||||
});
|
||||
const retryChildEvents = parseJsonEvents(retryChildRun.stdout);
|
||||
const retryChildText = extractFinalText(retryChildEvents, retryChildRun.stdout);
|
||||
const retryChildTools = mcpMode === "mock"
|
||||
? (await readToolCalls(retryLogPath)).map((call) => call.tool)
|
||||
: extractToolNames(retryChildEvents);
|
||||
|
||||
process.stderr.write(`[dry-run] retry interpreter ${identity}\n`);
|
||||
const retryInterpreterPrompt = renderTemplate("repo-face-turn-interpreter.prompt.md", {
|
||||
attempt: "2",
|
||||
facePrompt,
|
||||
faceOutput: retryChildText,
|
||||
});
|
||||
const retryInterpreterRun = await runCodex(retryInterpreterPrompt, {
|
||||
model: interpreterModel,
|
||||
reasoningEffort,
|
||||
scenarioId: `${identity}_swarm_dry_run_retry_interpreter`,
|
||||
mcpMode: "none",
|
||||
});
|
||||
const retryInterpreterEvents = parseJsonEvents(retryInterpreterRun.stdout);
|
||||
const retryInterpreterText = extractFinalText(retryInterpreterEvents, retryInterpreterRun.stdout);
|
||||
const retryParsed = parseInterpreterOutput(retryInterpreterText);
|
||||
|
||||
retry = {
|
||||
child: {
|
||||
exitCode: retryChildRun.code,
|
||||
durationMs: retryChildRun.durationMs,
|
||||
tools: retryChildTools,
|
||||
text: retryChildText,
|
||||
stderrTail: retryChildRun.stderr.slice(-1600),
|
||||
},
|
||||
interpreter: {
|
||||
exitCode: retryInterpreterRun.code,
|
||||
durationMs: retryInterpreterRun.durationMs,
|
||||
text: retryInterpreterText,
|
||||
parsed: retryParsed,
|
||||
stderrTail: retryInterpreterRun.stderr.slice(-1600),
|
||||
},
|
||||
};
|
||||
finalChildRun = retryChildRun;
|
||||
finalChildText = retryChildText;
|
||||
finalChildTools = retryChildTools;
|
||||
finalInterpreterRun = retryInterpreterRun;
|
||||
finalInterpreterText = retryInterpreterText;
|
||||
finalParsed = retryParsed;
|
||||
}
|
||||
|
||||
const assessment = assessRun({
|
||||
identity,
|
||||
childText: finalChildText,
|
||||
childTools: finalChildTools,
|
||||
interpreterRun: finalInterpreterRun,
|
||||
childRun: finalChildRun,
|
||||
parsed: finalParsed,
|
||||
});
|
||||
|
||||
runs.push({
|
||||
identity,
|
||||
promptPath,
|
||||
child: {
|
||||
exitCode: childRun.code,
|
||||
durationMs: childRun.durationMs,
|
||||
tools: childTools,
|
||||
text: childText,
|
||||
stderrTail: childRun.stderr.slice(-1600),
|
||||
exitCode: finalChildRun.code,
|
||||
durationMs: finalChildRun.durationMs,
|
||||
tools: finalChildTools,
|
||||
text: finalChildText,
|
||||
stderrTail: finalChildRun.stderr.slice(-1600),
|
||||
},
|
||||
interpreter: {
|
||||
exitCode: interpreterRun.code,
|
||||
durationMs: interpreterRun.durationMs,
|
||||
text: interpreterText,
|
||||
parsed,
|
||||
stderrTail: interpreterRun.stderr.slice(-1600),
|
||||
exitCode: finalInterpreterRun.code,
|
||||
durationMs: finalInterpreterRun.durationMs,
|
||||
text: finalInterpreterText,
|
||||
parsed: finalParsed,
|
||||
stderrTail: finalInterpreterRun.stderr.slice(-1600),
|
||||
},
|
||||
firstAttempt: {
|
||||
child: {
|
||||
exitCode: childRun.code,
|
||||
durationMs: childRun.durationMs,
|
||||
tools: childTools,
|
||||
text: childText,
|
||||
stderrTail: childRun.stderr.slice(-1600),
|
||||
},
|
||||
interpreter: {
|
||||
exitCode: interpreterRun.code,
|
||||
durationMs: interpreterRun.durationMs,
|
||||
text: interpreterText,
|
||||
parsed,
|
||||
stderrTail: interpreterRun.stderr.slice(-1600),
|
||||
},
|
||||
},
|
||||
retry,
|
||||
assessment,
|
||||
});
|
||||
}
|
||||
|
|
@ -320,6 +414,18 @@ function renderTemplate(name, variables) {
|
|||
return template;
|
||||
}
|
||||
|
||||
function renderRetryPrompt(input) {
|
||||
const reasons = input.reasons.map((reason) => `- ${reason}`).join("\n");
|
||||
return `${input.originalPrompt}
|
||||
|
||||
Revision request:
|
||||
Your previous turn could not be used cleanly for these reasons:
|
||||
${reasons}
|
||||
|
||||
Revise once. Keep the same identity and evidence. Write naturally in your own voice; do not write forms, machine packets, or hidden commands. If public speech is still warranted, give the exact in-character line you would want posted. If the output is work-shaped, describe the review, article, or change request plainly enough to remember or discuss later. Do not package it as governance or dispatch. If no public action survives, return a concise private summary.
|
||||
`;
|
||||
}
|
||||
|
||||
function parseJsonEvents(stdout) {
|
||||
return stdout
|
||||
.split(/\r?\n/)
|
||||
|
|
|
|||
|
|
@ -485,6 +485,10 @@ async function queueRepoFaceTurn(input: {
|
|||
identity,
|
||||
input.config,
|
||||
input.registryIdentities,
|
||||
{
|
||||
recentMessages,
|
||||
channelSnapshots,
|
||||
},
|
||||
);
|
||||
const repoActivitySurface = renderRepoFaceRepoActivitySurface(identity, input.config);
|
||||
const socialOpportunitySurface = await renderRepoFaceSocialOpportunitySurface({
|
||||
|
|
@ -1454,6 +1458,10 @@ function buildHeartbeatPrompt(input: {
|
|||
repoActivitySurface: input.repoActivitySurface ?? "- No recent home repo activity was attached for this turn.",
|
||||
socialOpportunitySurface: input.socialOpportunitySurface ?? "- No live social affordances were attached for this turn.",
|
||||
conversationMemorySurface: input.conversationMemorySurface ?? "- No recent conversation transcript was attached for this turn.",
|
||||
roomWeatherDirective: renderRepoFaceRoomWeatherDirective(input.identity, {
|
||||
recentMessages: input.recentMessages,
|
||||
channelSnapshots: input.channelSnapshots,
|
||||
}),
|
||||
turnSituationDirective: renderTurnSituationDirective({
|
||||
identity: input.identity,
|
||||
participant: input.participant,
|
||||
|
|
@ -1508,7 +1516,7 @@ async function assembleRepoFaceTurnPrompt(input: {
|
|||
throw new Error(`No prompt assembly channel is configured for ${identity.id}.`);
|
||||
}
|
||||
|
||||
const [recentMessages, channelSnapshots, bifrostDigest, memorySurface] = await Promise.all([
|
||||
const [recentMessages, channelSnapshots, bifrostDigest] = await Promise.all([
|
||||
fetchRecentDiscordMessages({
|
||||
botToken: input.config.botToken,
|
||||
channelId,
|
||||
|
|
@ -1529,10 +1537,13 @@ async function assembleRepoFaceTurnPrompt(input: {
|
|||
agentIdentity: identity.id,
|
||||
})
|
||||
: Promise.resolve(undefined),
|
||||
input.memorySurfacePath
|
||||
? readOptionalMemorySurface(input.memorySurfacePath)
|
||||
: renderRepoFaceMemorySurfaceForTurn(identity, input.config, registry.identities),
|
||||
]);
|
||||
const memorySurface = input.memorySurfacePath
|
||||
? await readOptionalMemorySurface(input.memorySurfacePath)
|
||||
: await renderRepoFaceMemorySurfaceForTurn(identity, input.config, registry.identities, {
|
||||
recentMessages,
|
||||
channelSnapshots,
|
||||
});
|
||||
const repoActivitySurface = renderRepoFaceRepoActivitySurface(identity, input.config);
|
||||
const socialOpportunitySurface = await renderRepoFaceSocialOpportunitySurface({
|
||||
identity,
|
||||
|
|
@ -1592,10 +1603,14 @@ async function renderRepoFaceMemorySurfaceForTurn(
|
|||
identity: RepoDiscordIdentity,
|
||||
config: ReturnType<typeof loadConfig>,
|
||||
registryIdentities: RepoDiscordIdentity[] = [],
|
||||
roomContext?: {
|
||||
recentMessages: SourceMessage[];
|
||||
channelSnapshots: ChannelSnapshot[];
|
||||
},
|
||||
): Promise<string> {
|
||||
const statePath = resolveRepoFaceStatePath(identity, config.storageRoot);
|
||||
const typedState = await loadVoidSelfStateTypedDocuments({ canonicalPath: statePath });
|
||||
const statePacket = renderRepoFaceStatePacket(identity, typedState, registryIdentities);
|
||||
const statePacket = renderRepoFaceStatePacket(identity, typedState, registryIdentities, roomContext);
|
||||
if (!config.repoFaceHeartbeats.stateProjectorEnabled) {
|
||||
return rejectLeakyMemorySurface(statePacket);
|
||||
}
|
||||
|
|
@ -1611,6 +1626,10 @@ function renderRepoFaceStatePacket(
|
|||
identity: RepoDiscordIdentity,
|
||||
state: VoidSelfStateTypedProjection,
|
||||
registryIdentities: RepoDiscordIdentity[] = [],
|
||||
roomContext?: {
|
||||
recentMessages: SourceMessage[];
|
||||
channelSnapshots: ChannelSnapshot[];
|
||||
},
|
||||
): string {
|
||||
const name = identity.displayName;
|
||||
const lines: string[] = [];
|
||||
|
|
@ -1686,6 +1705,13 @@ function renderRepoFaceStatePacket(
|
|||
lines.push(socialGraphFacts);
|
||||
}
|
||||
|
||||
const roomTextureFacts = roomContext
|
||||
? renderRepoFaceRoomTextureFacts(identity, roomContext)
|
||||
: undefined;
|
||||
if (roomTextureFacts) {
|
||||
lines.push(roomTextureFacts);
|
||||
}
|
||||
|
||||
if (agencyPressures.length > 0) {
|
||||
lines.push([
|
||||
"What wants action later:",
|
||||
|
|
@ -1712,6 +1738,123 @@ function renderRepoFaceStatePacket(
|
|||
return rejectLeakyMemorySurface(lines.join("\n\n"));
|
||||
}
|
||||
|
||||
function renderRepoFaceRoomTextureFacts(
|
||||
identity: RepoDiscordIdentity,
|
||||
input: {
|
||||
recentMessages: SourceMessage[];
|
||||
channelSnapshots: ChannelSnapshot[];
|
||||
},
|
||||
): string | undefined {
|
||||
const stats = collectRepoFaceRoomTextureStats(identity, input);
|
||||
if (!stats) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const temporaryPressures = stats.texture === "heavy" || stats.agentShare >= 0.55
|
||||
? [
|
||||
"- Temporary affect pressure candidates: play, rest, and social_contact may be hungry because the room is carrying sustained work/agent weight.",
|
||||
"- These are not stored needs and not orders. Project whether this character gets mischievous, bored, sharp, withdrawn, socially hungry, status-testing, or still work-focused.",
|
||||
]
|
||||
: [];
|
||||
|
||||
return [
|
||||
"Room texture facts:",
|
||||
`- Observed messages: ${stats.total}; humans: ${stats.humanMessages}; agents/bots: ${stats.agentMessages}; distinct speakers: ${stats.speakerCount}.`,
|
||||
`- Long messages: ${stats.longMessages}; short messages: ${stats.shortMessages}; average length: ${stats.averageCharacters} characters.`,
|
||||
`- This Face's own recent messages in the attached window: ${stats.ownMessages}.`,
|
||||
`- Structural texture: ${stats.texture}. This is evidence about conversational weight, not a command to speak or joke.`,
|
||||
...temporaryPressures,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function renderRepoFaceRoomWeatherDirective(
|
||||
identity: RepoDiscordIdentity,
|
||||
input: {
|
||||
recentMessages: SourceMessage[];
|
||||
channelSnapshots: ChannelSnapshot[];
|
||||
},
|
||||
): string {
|
||||
const stats = collectRepoFaceRoomTextureStats(identity, input);
|
||||
if (!stats) {
|
||||
return "- No current room weather was available.";
|
||||
}
|
||||
|
||||
const pressure =
|
||||
stats.texture === "heavy" || stats.agentShare >= 0.55
|
||||
? "The room is currently structurally work-heavy or agent-heavy. Treat that as conversational weather: it may create boredom, play hunger, restlessness, social hunger, withdrawal, or sharper status-testing, depending on who you are. This is not an order to joke; it is permission to feel the weight instead of answering every opening with more work."
|
||||
: stats.texture === "light"
|
||||
? "The room is currently light enough for compact social motion. You still need an actual reason to speak, but not a work ticket."
|
||||
: "The room is mixed. Use the transcript to decide whether the living pressure is social, practical, or private.";
|
||||
|
||||
return [
|
||||
`- Messages observed: ${stats.total}; humans: ${stats.humanMessages}; agents/bots: ${stats.agentMessages}; distinct speakers: ${stats.speakerCount}.`,
|
||||
`- Texture: ${stats.texture}; your own recent messages in this window: ${stats.ownMessages}.`,
|
||||
`- ${pressure}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
interface RepoFaceRoomTextureStats {
|
||||
total: number;
|
||||
agentMessages: number;
|
||||
humanMessages: number;
|
||||
ownMessages: number;
|
||||
longMessages: number;
|
||||
shortMessages: number;
|
||||
averageCharacters: number;
|
||||
speakerCount: number;
|
||||
texture: "heavy" | "light" | "mixed";
|
||||
agentShare: number;
|
||||
}
|
||||
|
||||
function collectRepoFaceRoomTextureStats(
|
||||
identity: RepoDiscordIdentity,
|
||||
input: {
|
||||
recentMessages: SourceMessage[];
|
||||
channelSnapshots: ChannelSnapshot[];
|
||||
},
|
||||
): RepoFaceRoomTextureStats | undefined {
|
||||
const messages = [
|
||||
...input.recentMessages,
|
||||
...input.channelSnapshots.flatMap((snapshot) => snapshot.messages),
|
||||
];
|
||||
if (messages.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const ownToken = normalizeSocialLabel(identity.displayName);
|
||||
const total = messages.length;
|
||||
const agentMessages = messages.filter((message) => message.isBot).length;
|
||||
const humanMessages = total - agentMessages;
|
||||
const ownMessages = messages.filter((message) => normalizeSocialLabel(message.authorName) === ownToken).length;
|
||||
const longMessages = messages.filter((message) => collapseWhitespace(message.content, 10_000).length >= 220).length;
|
||||
const shortMessages = messages.filter((message) => collapseWhitespace(message.content, 10_000).length <= 90).length;
|
||||
const averageCharacters = Math.round(
|
||||
messages.reduce((sum, message) => sum + collapseWhitespace(message.content, 10_000).length, 0) / total,
|
||||
);
|
||||
const speakerCount = new Set(
|
||||
messages.map((message) => normalizeSocialLabel(message.authorName || message.authorId)).filter(Boolean),
|
||||
).size;
|
||||
const texture =
|
||||
longMessages >= Math.ceil(total * 0.45) || averageCharacters >= 180
|
||||
? "heavy"
|
||||
: shortMessages >= Math.ceil(total * 0.55)
|
||||
? "light"
|
||||
: "mixed";
|
||||
|
||||
return {
|
||||
total,
|
||||
agentMessages,
|
||||
humanMessages,
|
||||
ownMessages,
|
||||
longMessages,
|
||||
shortMessages,
|
||||
averageCharacters,
|
||||
speakerCount,
|
||||
texture,
|
||||
agentShare: agentMessages / total,
|
||||
};
|
||||
}
|
||||
|
||||
function renderRepoFaceSocialGraphFacts(
|
||||
identity: RepoDiscordIdentity,
|
||||
registryIdentities: RepoDiscordIdentity[],
|
||||
|
|
|
|||
|
|
@ -265,3 +265,4 @@
|
|||
{"ts":"2026-05-22T12:32:18Z","type":"implementation","status":"ok","note":"Moved repo Face sparse-social-graph meaning to the model state projector. Scheduler now provides neutral mapped/unmapped peer topology and live relationship hooks only; prompt/projector owns personality-specific pressure."}
|
||||
{"ts":"2026-05-22T13:09:27Z","type":"verification","status":"ok","note":"Ran offline repo Face dry-run gates before restart. All 10 memory projections passed; full swarm Face->Interpreter mock run routed every Face with action blocks and no heartbeat/provenance leaks. Initial failures were harness false positives around natural social prose and timestamp .429; targeted Heimdall rerun passed after detector fixes."}
|
||||
{"ts":"2026-05-22T13:15:00Z","type":"implementation","status":"ok","note":"Cut semantic regex language cops from repo Face dry-run gates. Dry-run harness no longer fails on guessed source-inspection claims, low keyword flavor, or inferred social-pressure preservation; exact protocol/tool/provenance guards remain. Rake-cut slice passed for Nibu/Heimdall/Mimir and model-output inspection reported zero warnings."}
|
||||
{"ts":"2026-05-22T13:56:50Z","type":"implementation","status":"ok","note":"Repo Face social-pressure pass: deterministic prompt assembly now attaches compact room-weather facts beside raw transcripts, jurisdiction dives are background appetite rather than a live-room override, the state projector/Interpreter preserve work-fatigue/play/rest/social pressure, and the dry-run harness now exercises the live one-retry Interpreter path. Full mock swarm dry run passed at .voidbot/status/repo-face-social-pressure-dry-run-all-2.json; output still shows strong work gravity but no correction/body-context collapse."}
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ accepted_design:
|
|||
- Face prompts carry explicit Epiphany identity doctrine. The base Void style pack contributes discipline, humor permission, and source-grounding habits, but the registered Face state, Epiphany, repo-local `.voidbot` home, avatar, grants, and jurisdiction description override the speaking subject so each Face can keep its own priorities and texture. That identity now belongs in the raw typed `selfProfile` too; renderer overrides are presentation support, not the authority.
|
||||
- Repo Face volatile state projection is model-owned again. The heartbeat scheduler may deterministically assemble a compact state packet, but live child prompts must pass that packet through `prompts/repo-face-state-projector.prompt.md` before it reaches the Face. The deterministic packet is an emergency isolation fallback only behind `REPO_FACE_STATE_PROJECTOR_ENABLED=false`, and both paths reject schema/prompt-construction leaks such as `grants:`, `jurisdictions:`, `Face of`, repo paths, and "do not prompt" language before prompt assembly succeeds.
|
||||
- Repo Face conversation evidence is deterministic again. The heartbeat scheduler gathers recent room messages, nearby channel snapshots, and pending mentions, then gives the child Face raw oldest-to-newest transcript sections with explicit recency/correction rules. The model state projector still owns identity, memory, affect, social reads, and other lived internal pressure; it does not summarize room messages or decide consensus for the Face.
|
||||
- Repo Face room-work fatigue is part of the input contract, not seeded banter. Deterministic prompt assembly attaches compact room-weather facts beside the raw transcript and passes the same neutral facts through the state projector packet. The projector owns translating heavy, agent-dense, repetitive, or mixed room texture into character-specific play/rest/social pressure; the Face may speak, stay private, tease, withdraw, ask a human-scale question, or keep working if that genuinely fits. The parent Interpreter must preserve unmet social/play/rest/status pressure as typed state when the Face turns the moment into another work-shaped ask without satisfying it.
|
||||
- Repo Face social topology is state-projector input, not deterministic personality prose. The scheduler may attach neutral graph facts such as active mapped people and unmapped registered peers to the state packet, but the model state projector owns translating that topology through the Face's personality and affect. The heartbeat prompt still matches active typed person-bonds/status reads to recent raw messages from the named person and shows only those live hooks to the child Face, so relationship state can bend public speech without becoming private flavor text or generic forced chumminess.
|
||||
- Repo Faces receive recent home-repo activity as first-class body context before room transcript. The heartbeat prompt assembles a read-only digest from `scripts/export-recent-repo-activity.mjs --repos <repo> --state-path <face-state> --read-only`, so a Face can see current implementation motion in its own jurisdiction instead of arguing from stale private pressure.
|
||||
- Face embodiment comes before task execution. Repo concerns, private rumination, public posts, jokes, objections, and silence should all be filtered through the Face's perspective, feelings, drives, values, wounds, fascinations, aesthetic taste, and dignity. Values must suit the kind of person the character is, not merely the jobs assigned to the Face.
|
||||
|
|
@ -141,7 +142,7 @@ accepted_design:
|
|||
- Active-turn freeze is bounded by actual job liveness, not blind faith in an old `running` row. Repo Face heartbeat jobs that remain approved/running past the stale-job window are marked failed through the job queue, recorded as `stale_active_turn_recovered`, and removed from the active-turn map so one dead worker claim cannot silence a Face indefinitely.
|
||||
- Repo Face rest projection is parent-owned state, not a read-only scheduler illusion. If the heartbeat runner projects a Face sleep cycle forward, it must persist the normalized `update_sleep_cycle` operation to that Face's `.cc` state before ready selection. Missed nap windows roll forward as completed rest instead of starting a brand-new nap at inspection time; otherwise overdue agents can be skipped forever as "currently napping" while raw state keeps the same stale boundary.
|
||||
- Repo Face memory distillation now shares Void's typed maintenance organ instead of relying on rumination turns to eventually clean themselves. `scripts/run-repo-face-heartbeats.ts` starts repo-local `run-void-memory-maintenance.ps1 -ForceDistillation` when a Face is napping with short-term memory or has stale short-term memory older than six hours, with status/logs isolated under the target repo's `.voidbot/status` and `.voidbot/logs`. `run-void-memory-maintenance.ps1` validates the entire proposed operation batch against `voidSelfStateOperationSchema` before applying anything, so one malformed late operation cannot leave a partial maintenance pass. The maintenance prompt is identity-neutral and explicitly forbids shorthand operation payloads, `type` instead of `operation`, `memoryIds` for pruning, and `null` optional fields.
|
||||
- Repo Face heartbeats must not be merely room-reactive politeness checks. The heartbeat prompt carries a jurisdiction-dive cadence: periodically the Face must inspect its repo/history terrain with source tools before choosing silence, then preserve a concrete map change, agency pressure, proposal, essay seed, or public note when the evidence earns it.
|
||||
- Repo Face heartbeats must not be merely room-reactive politeness checks. The heartbeat prompt carries a jurisdiction-dive cadence, but it is background appetite, not a command that outranks live conversation. When no correction, direct call, social pressure, or stronger embodied need is tugging, the Face should inspect its repo/history terrain with source tools before making repo claims; when the live room is the real thing, answer the room and preserve the dive as later hunger.
|
||||
- Repo Face heartbeats are not ordinary owner Discord replies. The owner-Codex render wrapper must recognize both legacy `Repo Face identity doctrine` prompts and current `Epiphany Face identity doctrine` heartbeat prompts, forbid generic handoff for Face jobs, and preserve the repo identity action rail as parent-owned output. The preferred rail is now the small Face action DSL; legacy JSON sentinels remain accepted only as a compatibility surface.
|
||||
- Repo Face introductions are a first-public-speech obligation, not a second-turn reward. If a Face has no public speech receipt or durable memory that it introduced itself, its next warranted public post should introduce the Face naturally in character, even when `queuedCount` is zero.
|
||||
- Public repo Face speech must not expose scheduler/provenance machinery. Labels such as `Repo-face heartbeat from ...`, `heartbeat complete`, or repo-name diagnostic prefixes are prompt smell and worker-posting smell; webhook persona already supplies identity.
|
||||
|
|
@ -237,4 +238,4 @@ rejected_paths:
|
|||
|
||||
current_status:
|
||||
summary: VoidBot's bot/worker/provider/RAG/Postgres/Qdrant/CultCache structure is live, and the repo Face swarm has been restarted after the 2026-05-21 teardown pass. `GameCult Local Orchestrator` is enabled as the single pulse owner; watchdog, mood, moderation, and repo Face CTB organs have all completed healthy post-restart pulses. Bifrost work routing is paused by default.
|
||||
next_action: Keep the swarm paused until the Face-turn loop proves it can read corrections from raw transcript evidence and current repo/body context. The latest cuts removed repo Face turns from the owner-reply wrapper, coerces stray owner-DM speech back to the live room unless explicitly marked owner-private, pauses Face Bifrost/governance routing, disables automatic Bifrost dispatch by default, keeps the model state projector as the live state-to-character prose boundary, replaces the model conversation projector with deterministic raw transcripts, adds read-only home-repo activity to Face prompts, and splits model ownership so Face turns always default to full gpt-5.4 while state projectors and parent Interpreters use the cheaper Spark/Mini support chain.
|
||||
next_action: Keep the swarm paused until live restart is explicitly chosen. The current dry-run proof says the Face-turn loop can read correction/body context and route through the parent Interpreter, but the room still has strong work gravity. The latest cut demotes jurisdiction dives to background appetite, attaches deterministic room-weather facts, makes the projector/Interpreter preserve play/rest/social fatigue as state, keeps raw transcripts as the correction evidence, and updates the dry-run harness to exercise the live retry shape.
|
||||
|
|
|
|||
Loading…
Reference in a new issue