Surface repo Face social pressure evidence

This commit is contained in:
Meta Vens 2026-05-23 13:46:52 +01:00
commit b2b8debe70
7 changed files with 213 additions and 4 deletions

View file

@ -159,7 +159,8 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
- Huginn then exposed the broader version of the same recency bug: an older CultNet inspectability side thread remained in the current-room window, but the newest room flow was Nibu's substrate-grown AGI faction seam. Huginn's projected state over-weighted the specimen/envelope pressure, and Mini Interpreter routed the old-side-thread reply. The correct fix is not to forbid pet-peeve revival; it is to keep it legible. Face transcripts now include message IDs, and older side-thread replies must either reply to the contextual message, carry enough context in the content, bridge to the live topic, or stay private.
- The overnight 2026-05-23 run showed a softer version of the previous loop: Faces did not collapse into nonsense, but the Aquarium room stayed captured by the Nibu-origin/faction lore seam for hours. Existing `saturationScore` lived on private thought/incubation surfaces, and the repetition prompt mostly caught copied openings/endings, not semantic topic attractors. The heartbeat prompt now projects current-room topic saturation from repeated raw-transcript terms, and the Interpreter has a saturated-topic gate: stay on the topic only to add a new anchor, answer a live question, make a decision-driving distinction, draft a concrete artifact, intentionally close/defer, or make a social move about the staleness itself.
- Void's broader moderation authority had become venue leakage: recent-history projection stripped `channelId`, leaving the configured `publicSpeechTarget` as the obvious place for replies, so Aquarium-shaped talk could land in `#general`. The rumination context now preserves message `channelId` and projects `recentConversationTarget`; prompt doctrine says source-channel replies belong in that source channel, while `publicSpeechTarget` is for unanchored herald/artifact announcements.
- Do not restart the swarm by momentum in future sessions; it is already running. If it must be stopped again, use `state/agent-swarm-paused.json` and the orchestrator task deliberately, then update this handoff.
- The Nibu/Void turf-crossing discussion exposed a social-state projection gap: existing bonds/status reads project well once typed state already contains them, but the live pipeline did not own "this recent event should become relationship/status pressure." The repair adds bounded raw relationship-pressure evidence to the state packet before the projector: recent messages that touch the Face, peer Faces, existing social targets, jurisdiction/domain terms, or status/territory/consultation language. The projector may turn that evidence into tentative felt pressure, and the Interpreter may persist a bond/status/mood note only when the projected memory or Face output shows a character-specific reaction. This should let resentment, rivalry, consultation offense, and territorial tension emerge without manual state edits.
- The swarm is currently paused through `state/agent-swarm-paused.json`. Do not restart it by momentum. When the operator is ready, restart deliberately under observation and check whether new turf/status/consultation events produce character-specific projected pressure and durable state through ordinary turns.
- Direct Face `UPDATE REQUEST` and `BIFROST TOPIC` packets are ignored while `REPO_FACE_BIFROST_ENABLED=false`. Do not debug missing Bifrost jobs as a live failure unless that flag has been deliberately enabled again.
- Live model-facing prompt prose has started moving out of executable code. The shared loader is `packages/shared/src/prompt-template.ts`; extracted prompt surfaces live under `prompts/`, including repo Face turn/reviewer prompts, provider wrapper prompts, situational social read prompts, source-grounding reminders, consensus-feed prompts, and repo Face model-smoke scenarios. Code should project data and select prompt names; prompt files should own behavioral prose.
- Identity collapse has started. Active bot, worker, MCP repo-identity tools, and Face CTB scheduler paths now load through the canonical Face registry and treat repo Discord identities as compatibility projections. Next bounded move is to finish removing remaining direct conceptual dependence on repo identities, then split the Face turn loop into scheduler, context assembler, prompt renderer, candidate parser, policy gate, and transport adapter.

View file

@ -18,6 +18,8 @@ Architecture invariant:
- 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.
- Do not guess a person's pronouns from their name, role, voice, avatar, or relationship shape. If pronoun guidance is present in the state packet, obey it. If it is absent or uncertain, use the person's name or neutral phrasing.
- 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 the packet includes recent relationship-pressure evidence, treat it as raw provocation that may awaken social state. Do not turn it into settled memory by yourself, and do not mechanically accept the "Signals" text as a conclusion. Ask what the event would feel like to this specific person if it touched their values, wounds, territory, pride, alliances, rivalries, status, or need for consultation. A turf crossing might become resentment for one character, amused superiority for another, and nothing at all for a third.
- If a recent event touches this character's jurisdiction or dignity and no durable bond/status read exists yet, make the possible reaction available as a tentative undercurrent: irritation, respect, rivalry, protectiveness, suspicion, embarrassment, gratitude, hunger for attention, or a decision to test the other person's standing. The child character can accept, reject, or ignore that undercurrent; your job is to keep the social fact from being invisible.
- 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.

View file

@ -8,6 +8,8 @@ Architecture invariant:
- 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.
- You also own carry-forward for projected relationship/status undercurrents. If the Original Face prompt gives the Face a tentative social reaction to a recent event, and the Face output speaks or thinks from that undercurrent, preserve the durable part as a STATE NOTE kind `bond`, `status`, or `mood` even when the Face expresses it indirectly through sarcasm, deference, needling, withdrawal, or territorial phrasing.
- Do not invent a bond from raw relationship-pressure evidence alone. Persist it only when the projected memory or Face output shows a character-specific reaction that should bend future turns. The point is natural social memory, not bureaucracy wearing a friendship bracelet.
Mandatory first pass before any route decision:
- First identify whether the Original Face prompt contains a live correction, narrowing, pushback, or reframing of this Face, this topic, or a repeated agent phrase.

View file

@ -1925,6 +1925,13 @@ function renderRepoFaceStatePacket(
lines.push(peerOpeningFacts);
}
const socialPressureFacts = roomContext
? renderRepoFaceRelationshipPressureFacts(identity, registryIdentities, state, roomContext)
: undefined;
if (socialPressureFacts) {
lines.push(socialPressureFacts);
}
const pronounFacts = renderRepoFaceHumanPronounFacts(humanPronounGuidance);
if (pronounFacts) {
lines.push(pronounFacts);
@ -2328,6 +2335,201 @@ function renderRepoFacePeerOpeningFacts(
].join("\n");
}
function renderRepoFaceRelationshipPressureFacts(
identity: RepoDiscordIdentity,
registryIdentities: RepoDiscordIdentity[],
state: VoidSelfStateTypedProjection,
roomContext: {
recentMessages: SourceMessage[];
channelSnapshots: ChannelSnapshot[];
},
): string | undefined {
const selfTokens = socialPressureTokensForIdentity(identity);
const jurisdictionTokens = socialPressureJurisdictionTokens(identity);
const peerProfiles = registryIdentities
.filter((peer) => normalizeSocialLabel(peer.id) !== normalizeSocialLabel(identity.id))
.map((peer) => ({
identity: peer,
tokens: socialPressureTokensForIdentity(peer),
}));
const relationTargets = collectRepoFaceSocialRelations(state)
.map((relation) => ({
label: relation.targetLabel,
tokens: socialPressureTokens(relation.targetLabel),
}))
.filter((relation) => relation.tokens.length > 0);
const entries: Array<{ label: string; message: SourceMessage }> = [
...roomContext.recentMessages.map((message) => ({ label: "current room", message })),
...roomContext.channelSnapshots.flatMap((snapshot) =>
snapshot.messages.map((message) => ({ label: "nearby room", message })),
),
];
const byId = new Map<string, {
label: string;
message: SourceMessage;
score: number;
signals: string[];
}>();
for (const entry of entries) {
const content = collapseWhitespace(entry.message.content, 10_000);
if (!content) {
continue;
}
const normalizedContent = normalizeSocialLabel(content);
const authorToken = normalizeSocialLabel(entry.message.authorName ?? entry.message.authorId);
const signals: string[] = [];
let score = 0;
const authorIsSelf = tokenAppearsInNormalizedText(authorToken, selfTokens);
const contentNamesSelf = tokenAppearsInNormalizedText(normalizedContent, selfTokens);
if (contentNamesSelf) {
score += 3;
signals.push(`names ${identity.displayName}`);
} else if (authorIsSelf) {
score += 1;
signals.push(`${identity.displayName}'s own recent line`);
}
const peerMatches = peerProfiles
.filter((peer) =>
tokenAppearsInNormalizedText(authorToken, peer.tokens) ||
tokenAppearsInNormalizedText(normalizedContent, peer.tokens),
)
.slice(0, 3);
if (peerMatches.length > 0) {
score += peerMatches.length;
signals.push(`touches peer ${peerMatches.map((peer) => peer.identity.displayName).join("/")}`);
}
const relationMatches = relationTargets
.filter((relation) =>
tokenAppearsInNormalizedText(authorToken, relation.tokens) ||
tokenAppearsInNormalizedText(normalizedContent, relation.tokens),
)
.slice(0, 3);
if (relationMatches.length > 0) {
score += relationMatches.length;
signals.push(`touches existing social target ${relationMatches.map((relation) => relation.label).join("/")}`);
}
if (tokenAppearsInNormalizedText(normalizedContent, jurisdictionTokens)) {
score += 1;
signals.push("touches this jurisdiction or its domain language");
}
const socialPressureKinds = socialPressureLanguageKinds(content);
if (socialPressureKinds.length > 0) {
score += 2;
signals.push(`uses social/status language (${socialPressureKinds.join(", ")})`);
}
if (!entry.message.isBot && score > 0) {
score += 1;
signals.push("human voice");
}
if (score < 4) {
continue;
}
const existing = byId.get(entry.message.id);
if (!existing || score > existing.score) {
byId.set(entry.message.id, {
label: entry.label,
message: entry.message,
score,
signals: [...new Set(signals)],
});
}
}
const facts = [...byId.values()]
.sort((left, right) => {
const leftMs = Date.parse(left.message.timestamp);
const rightMs = Date.parse(right.message.timestamp);
if (Number.isFinite(leftMs) && Number.isFinite(rightMs) && leftMs !== rightMs) {
return leftMs - rightMs;
}
return left.message.id.localeCompare(right.message.id);
})
.slice(-8);
if (facts.length === 0) {
return undefined;
}
return [
"Recent relationship-pressure evidence:",
...facts.map((fact) => {
const speaker = fact.message.isBot ? `${fact.message.authorName} (agent/bot)` : fact.message.authorName;
return `- [${fact.label}] ${speaker} said: "${collapseWhitespace(fact.message.content, 260)}" Signals: ${fact.signals.join("; ")}.`;
}),
"These are raw provocations, not settled memories. Project them as tentative felt pressure only where this character's values, territory, current mood, or existing relationships make them matter.",
].join("\n");
}
function socialPressureTokensForIdentity(identity: RepoDiscordIdentity): string[] {
return socialPressureTokens(identity.displayName, identity.id, identity.repoName);
}
function socialPressureJurisdictionTokens(identity: RepoDiscordIdentity): string[] {
return socialPressureTokens(
identity.repoName,
identity.displayName,
identity.description,
...identity.channelPermissions.flatMap((permission) => [permission.label, permission.topic]),
)
.filter((token) => token.length >= 5);
}
function socialPressureTokens(...values: Array<string | undefined>): string[] {
const tokens = new Set<string>();
for (const value of values) {
const normalized = normalizeSocialLabel(value);
if (normalized.length >= 3) {
tokens.add(normalized);
}
for (const part of splitSocialPressureWords(value)) {
const token = normalizeSocialLabel(part);
if (token.length >= 4) {
tokens.add(token);
}
}
}
return [...tokens];
}
function splitSocialPressureWords(value: string | undefined): string[] {
return (value ?? "")
.replace(/([a-z])([A-Z])/g, "$1 $2")
.split(/[^A-Za-z0-9]+/)
.map((part) => part.trim())
.filter(Boolean);
}
function tokenAppearsInNormalizedText(normalizedText: string, tokens: string[]): boolean {
return tokens.some((token) => token.length > 0 && normalizedText.includes(token));
}
function socialPressureLanguageKinds(content: string): string[] {
const text = content.toLowerCase();
const kinds: string[] = [];
const groups: Array<[string, RegExp]> = [
["status", /\b(status|standing|rank|hierarchy|authority|overbearing|defer(?:red|ring)?|challenge[ds]?|humiliat(?:e|ed|ing)|respect)\b/],
["territory", /\b(turf|jurisdiction|steward(?:ship)?|custody|owner|ownership|belongs?|domain|lane|stepp(?:ed|ing)? on)\b/],
["consultation", /\b(consult(?:ed|ation|ing)?|ask(?:ed|ing)?|permission|bypass(?:ed|ing)?|decorative|flavo[u]?r theater|rubber[- ]?stamp)\b/],
["affiliation", /\b(friend(?:ship)?|rival(?:ry)?|alliance|resent(?:ment|s|ed|ing)?|trust|protect(?:ion|ive)?|envy|jealous|wrapped around)\b/],
["attention", /\b(attention|ignored|neglected|noticed|summon(?:ed|s)?|called out|directly challenged|approval)\b/],
];
for (const [kind, pattern] of groups) {
if (pattern.test(text)) {
kinds.push(kind);
}
}
return kinds;
}
interface RepoFaceHumanPronounGuidance {
actorId: string;
actorName: string;

View file

@ -1,6 +1,6 @@
{
"paused": true,
"reason": "Operator requested swarm shutdown before reboot; next repair should make social tension such as resentment emerge from projected relationships instead of manual injection.",
"reason": "Operator requested swarm shutdown before reboot. Social-pressure repair landed: heartbeat projection now surfaces raw relationship/status provocations so resentment, rivalry, and territorial tension can emerge through projector/Face/Interpreter instead of manual state injection. Keep paused until deliberately restarted under observation.",
"pausedAt": "2026-05-23T12:31:04.330Z",
"resumedAt": null,
"lastPausedAt": "2026-05-23T12:31:04.330Z"

View file

@ -301,3 +301,4 @@
{"ts":"2026-05-23T09:27:47Z","type":"implementation","status":"ok","note":"Repo Face topic saturation now projects current-room repeated-topic pressure into Face prompts and Interpreter routing, so semantic loops can be advanced, closed, pivoted, or kept private instead of continuing as locally-valid variants."}
{"ts":"2026-05-23T10:09:54Z","type":"fix","status":"ok","note":"Void moderation rumination now preserves recent Discord message channel ids and projects a latest recentConversationTarget, so Aquarium-shaped replies can target the source channel instead of falling back to the configured public/general room."}
{"ts":"2026-05-23T12:31:47Z","type":"ops","status":"ok","note":"Repo Face swarm paused before operator reboot. Next repair should make social tension and resentment emerge from relationship/state projection and recent events, not require manual state injection after the fact."}
{"ts":"2026-05-23T12:44:24Z","type":"implementation","status":"ok","note":"Repo Face relationship-pressure repair: heartbeat state packets now include bounded raw relationship/status provocations from recent messages that touch the Face, peer Faces, existing social targets, jurisdiction/domain terms, or status/territory/consultation language. The state projector is instructed to translate that evidence into tentative felt pressure, and the parent Interpreter persists bond/status/mood STATE NOTEs only when the projected memory or Face output shows a character-specific reaction. This closes the Nibu/Void turf gap without hardcoding resentment."}

View file

@ -101,6 +101,7 @@ accepted_design:
- Repo Faces have a social embodiment invariant now, not just a banter permission. The same typed state surface that stores repo thoughts may store meaning-bearing social reads under `person`, `room`, or `self` targets: friendships, rivalries, trust, irritation, protectiveness, recurring jokes, and bonds when they should change future behavior. Passing jokes may vanish; durable social memory must preserve a claim or question, tension, action implication, and anchors just like technical memory. Public speech must not open with note-title formulas such as "bright bridge note" or "tiny fish sorting note"; the webhook already says who is speaking, so content should start with what that Face wants to say to someone.
- Repo Faces and Void direct lanes carry an actionable comedy/improv doctrine distilled from Dan Harmon's `Comedy Master Class` talk: humor should come from honest character reaction, status inversion, vulnerability, exposed fear, false authority, or a shared contradiction in the live room. The move is accept the frame, add one character-specific turn, heighten with specificity, and leave before explaining. Do not imitate another Face's cadence, do not use jokes as dominance weapons, and do not retreat from banter into process talk unless process talk is the joke.
- Repo Face social life must respect jurisdiction. Faces may have opinions outside their home domains, but ownership should route to the steward: Heimdall for auth/OAuth/grants/custody/revocation, Bifrost for GitHub/Discord work transport and public-protocol crossings, Nibu for Aetheria lore and Aetheria articles, Void for GameCult website/heraldry and room-wide moderation, Aqua for AquaSynth, Mimir for realtime SDF/splatting/acoustic mapping/sensor fusion, Epiphany for EpiphanyAgent and agent-state birth machinery, and Libby for CultCache/CultNet/CultMesh/open-knowledge infrastructure. Cross-domain work should become visible consultation, co-authorship, handoff, or a shared proposal, not silent jurisdiction theft.
- Repo Face social tension should emerge from visible events plus typed personality, not from manual after-the-fact state edits. The heartbeat context assembler now adds bounded raw relationship-pressure evidence to the parent state packet when recent messages name the Face, peers, existing social targets, jurisdiction terms, or status/territory/consultation language. The projector may translate those raw provocations into tentative felt pressure, and the parent Interpreter may persist a bond/status/mood note only when the projected pressure or Face output shows a character-specific reaction that should bend future turns. This evidence selector is not consensus detection, sentiment analysis, or a rule that decides who resents whom.
- Repo Face work routing through Bifrost is paused. The bridge is conceptually plausible but under-cooked in live behavior: it made agents talk to summaries instead of people and made room context harder to inspect. Face turns should discuss work openly in Aquarium or relevant domain channels and preserve pressure in typed state. The worker ignores Face `BIFROST TOPIC` and legacy `UPDATE REQUEST` packets unless `REPO_FACE_BIFROST_ENABLED=true`, and the orchestrator skips Bifrost dispatch unless `BIFROST_DISPATCH_ENABLED=true`.
- Agent mentions are CTB pressure, not direct Discord prompt jobs. Role-addressed or display-name-addressed repo Face mentions enqueue pending mention obligations in the CTB heartbeat state even when the source channel is not a normal autonomous Face chatter lane; human replies to a Face's webhook/persona message count as direct attention to that Face too, even without a literal tag. Display-name detection is whole-message exact-name matching, not only name-at-start, so introductions like "Meet Kiko, Huginn and Weksa" create obligations for all named Faces. Ordinary Void mentions and DMs enqueue pending mentions for the built-in `void` participant. The heartbeat scheduler consumes that queue, temporarily pulls the addressed agent forward, attaches the pending prompt/reply target and source-channel snapshot to the next repo-face-rumination or moderation pass, and the parent-owned speech rails post only explicit candidate/action output through registered speech channels. The old `MessageCreate -> handlePrompt -> provider job` reflex is amputated for ambient direct mentions.
- 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.
@ -246,5 +247,5 @@ rejected_paths:
reason: sleep/distillation that collapses concrete claims into generic abstraction creates memory nonsense and then forces later cleanup compensators
current_status:
summary: VoidBot's bot/worker/provider/RAG/Postgres/Qdrant/CultCache structure is live, but the repo Face swarm is deliberately paused through `state/agent-swarm-paused.json` before the operator reboot. `GameCult Local Orchestrator` remains the single pulse owner; paused Face heartbeats should skip without queuing turns. Bifrost work routing is paused by default.
next_action: After reboot, do not simply wake the swarm by momentum. First repair the social-state projection gap exposed by Nibu/Void: resentment, rivalry, status offense, and territorial tension should emerge from recent events plus personality/state projection, not require manual typed-state injection after the fact. Then restart under observation and check that topic saturation, source-channel routing, and organic social tension all behave.
summary: VoidBot's bot/worker/provider/RAG/Postgres/Qdrant/CultCache structure is live, but the repo Face swarm remains deliberately paused through `state/agent-swarm-paused.json` before the operator reboot. `GameCult Local Orchestrator` remains the single pulse owner; paused Face heartbeats should skip without queuing turns. Bifrost work routing is paused by default. The immediate social-state projection gap exposed by Nibu/Void has been patched: raw relationship-pressure evidence now reaches the projector before Face turns.
next_action: When the operator is ready, restart the swarm deliberately under observation. Check that topic saturation, source-channel routing, and organic social tension all behave; specifically watch whether new turf/status/consultation events produce character-specific projected pressure and later durable STATE NOTEs without manual typed-state injection.