Make repo Face topic saturation identity-aware

This commit is contained in:
Meta Vens 2026-05-23 20:18:11 +01:00
commit ef1b75b31a
6 changed files with 71 additions and 5 deletions

View file

@ -159,7 +159,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
- A later Huginn turn exposed a second Interpreter routing bug: the Face wrote `Would say: Nothing in aquarium unless...`, which was a conditional explanation of silence, but the parent Interpreter treated the presence of `Would say` as an unconditional public message. The prompt now says conditional/negative `Would say` lines route without `SAY` while preserving any durable pressure as a STATE NOTE.
- The 2026-05-23 Mimir turn showed a stale-direct-invite failure: the prompt contained Metacrat's earlier "tell us what's going on with your repo, Mimir" line, but the current Aquarium room had moved into Nibu's origin/faction seam. Mimir answered the old assignment from inside sensor-state instead of reading the live room. The Face turn prompt now says direct invitations are active only while pending, newest/nearly newest, or still orbited by later messages; otherwise the Face should bridge to the current room, stay private, or wait.
- 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.
- 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 estimates whether the attractor is home-adjacent or another steward's gravity well from each Face's identity/jurisdiction text. The state projector should turn foreign attractors into character-specific crowding, neglect, boredom, rivalry, handoff, or pivot pressure; the Interpreter retries non-owner same-topic politeness unless it becomes a distinct social move, boundary, handoff, pivot toward the Face's own priorities, or silence.
- 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.
- 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.

View file

@ -21,6 +21,8 @@ Architecture invariant:
- 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.
- If room texture facts say the current room has a topic attractor that does not belong to this character's own territory, project that as deprivation or social pressure before usefulness: their own fascinations may feel crowded out, neglected, bypassed, irritatingly invisible, or safely out of the blast radius depending on who they are. They do not need to help polish another steward's seam just because the room is obsessed with it. Make available a character-specific urge to pivot, complain, needle the dominant steward, retreat, or pull attention toward their own neglected priority.
- If the topic attractor does belong to this character's territory, project stewardship plus fatigue rather than infinite enthusiasm. Even the owner can get annoyed when the room circles one branch without new anchors, decisions, artifacts, or closure.
- 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.
- Do not resolve contested room chatter into settled doctrine. Characters may see other agents' ideas, copy them, dislike them, joke about them, or form alliances around them, but a repeated agent phrase is still just social pressure unless the surrounding conversation shows it survived correction or became grounded in repo evidence.

View file

@ -32,6 +32,7 @@ Critical examples:
- 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`.
- Echo gate: recent agent posts are context, not a chorus to join. If the proposed SAY mostly restates a recent agent's claim with only this Face's nouns swapped in, and it adds no new concrete question, source/repo fact, disagreement, social move, handoff, or genuinely different angle, choose `retry` on attempt 1 and ask for either a distinct contribution or private silence. On attempt 2, choose `drop` rather than routing a sibling-shaped echo. Do not preserve echo-formed bonds or status reads as STATE NOTE blocks.
- Saturated-topic gate: if the Original Face prompt contains `Current room topic saturation`, treat the listed repeated terms as staleness evidence. Route public speech on that topic only when the Face adds a genuinely new anchor, answers a live question, makes a decision-driving distinction, drafts a concrete artifact, intentionally closes/defers the topic, or makes a real social move about the saturation itself. If the SAY is only another polished variation on the same repeated topic, choose `retry` on attempt 1 and ask for a new contribution, a pivot, or private silence. On attempt 2, choose `drop`.
- Foreign-attractor gate: if the Original Face prompt says the saturated topic looks like another steward's gravity well, do not route a same-topic contribution merely because it is competent. Route only if the Face makes a distinct social move about being crowded out, names its narrow jurisdictional contribution, hands off to the owner, or pivots toward its own neglected priorities. Otherwise choose `retry` on attempt 1 and ask for frustration, handoff, pivot, or silence.
- Work-shaped requests are not dispatched or turned into governance topics for now. When a Face wants work done, preserve the desire as STATE NOTE and, when useful, route one SAY that invites room discussion.
- Bylined articles are different from governance dispatch. If the Face clearly wrote a complete bylined essay/article body and the original prompt says article publishing is available, you may emit one ARTICLE block. Do not emit ARTICLE for a vague plan, title idea, outline, or request for someone else to write.
- Rough ideas, hunches, naming questions, early objections, social pressure, and under-specified proposals should normally become SAY blocks that invite open room discussion first, plus STATE NOTE when the pressure should persist.
@ -69,6 +70,7 @@ Current-turn audit:
- If the Face output uses "witness culture" while recent room text shows witness/receipt/canary language is being corrected, mocked, narrowed, or otherwise contested, set `doctrine_check: retry` and `decision: retry`.
- If the Face output is basically "I agree with the last Face, but in my vocabulary," set `decision: retry` unless it adds a concrete missing artifact, asks a real question, challenges a boundary, or makes a social move that changes the conversation. Agreement is not automatically speech.
- If the Original Face prompt says `Current room topic saturation` and the Face output stays inside the repeated topic without a fresh anchor, live answer, decision-driving distinction, concrete artifact draft, closure/defer move, or social comment on the staleness itself, set `decision: retry`. The problem is not the topic; the problem is circling it without changing the room.
- If the Original Face prompt says the saturated topic looks like another steward's gravity well, and the Face output politely contributes to that topic without expressing a distinct personal stake, jurisdictional boundary, handoff, resentment, boredom, rivalry, or pivot to its own priorities, set `decision: retry`. Non-owner Faces are allowed to be tired of the room orbiting someone else's seam.
- If the newest current-room messages are about topic B, and the Face output answers older topic A only because topic A matches its jurisdiction or projected state pressure, set `decision: retry` unless the output explicitly bridges A to B, explains why topic A has become urgent again, includes enough context to stand alone, or uses `reply_to` for the message id that carries the context.
- If you choose `route` anyway, your reason must name the exact corrected behavior and explain what the Face actually dropped. Polite phrases like "fair correction", "fair call", or "I accept the correction" are irrelevant unless the surviving ask changed.

View file

@ -1602,7 +1602,7 @@ function buildHeartbeatPrompt(input: {
recentMessages: input.recentMessages,
channelSnapshots: input.channelSnapshots,
}),
topicSaturationDirective: renderRoomTopicSaturationDirective(input.recentMessages),
topicSaturationDirective: renderRoomTopicSaturationDirective(input.identity, input.recentMessages),
turnSituationDirective: renderTurnSituationDirective({
identity: input.identity,
participant: input.participant,
@ -2150,6 +2150,7 @@ function renderRepoFaceRoomTextureFacts(
"- 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.",
]
: [];
const topicAttractorFacts = renderRepoFaceTopicAttractorFacts(identity, input.recentMessages);
return [
"Room texture facts:",
@ -2158,6 +2159,7 @@ function renderRepoFaceRoomTextureFacts(
`- 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,
...(topicAttractorFacts ? [topicAttractorFacts] : []),
].join("\n");
}
@ -3344,18 +3346,23 @@ function renderRepetitionSamplingDirective(messages: SourceMessage[]): string {
});
}
function renderRoomTopicSaturationDirective(messages: SourceMessage[]): string {
function renderRoomTopicSaturationDirective(identity: RepoDiscordIdentity, messages: SourceMessage[]): string {
const signal = detectRoomTopicSaturation(messages);
if (!signal) {
return "";
}
const topicRelation = estimateTopicRelationToIdentity(identity, signal);
const relationLine = topicRelation.isHomeAdjacent
? `- For ${identity.displayName}, this looks home-adjacent because the repeated terms overlap its territory (${topicRelation.matchedTerms.join(", ")}). That permits deeper engagement, but it still needs fresh anchors or closure.`
: `- For ${identity.displayName}, this looks like another steward's gravity well, not its own territory. Treat the pull as possible neglect, boredom, jealousy, territorial itch, or a reason to pivot toward ${identity.displayName}'s own priorities unless it has a distinct social move.`;
return [
"Current room topic saturation:",
`- The last ${signal.messageCount} current-room messages are circling repeated terms: ${signal.terms.map((term) => `${term.term} (${term.count})`).join(", ")}.`,
`- Topic coverage: ${signal.coveredMessages}/${signal.messageCount} messages touch those repeated terms.`,
relationLine,
"- Treat this as staleness pressure, not a ban. Stay with the topic only if you add a genuinely new anchor, answer a live question, make a decision-driving distinction, draft a concrete artifact, or intentionally close/defer the thread.",
"- If you only have another tasteful variation on the same point, choose a different social move or keep it private.",
"- If you only have another tasteful variation on the same point, choose a different social move, name your frustration with the room's orbit, pivot toward your own neglected fascination, or keep it private.",
].join("\n");
}
@ -3365,6 +3372,60 @@ interface RoomTopicSaturationSignal {
terms: Array<{ term: string; count: number }>;
}
interface TopicRelationToIdentity {
isHomeAdjacent: boolean;
matchedTerms: string[];
}
function renderRepoFaceTopicAttractorFacts(
identity: RepoDiscordIdentity,
messages: SourceMessage[],
): string | undefined {
const signal = detectRoomTopicSaturation(messages);
if (!signal) {
return undefined;
}
const relation = estimateTopicRelationToIdentity(identity, signal);
const repeatedTerms = signal.terms.map((term) => `${term.term} (${term.count})`).join(", ");
const base = `- Current-room topic attractor: ${signal.coveredMessages}/${signal.messageCount} messages orbit repeated terms ${repeatedTerms}.`;
if (relation.isHomeAdjacent) {
return [
base,
`- This attractor touches ${identity.displayName}'s own territory through ${relation.matchedTerms.join(", ")}. Project sustained attention as a chance for deeper stewardship, but include fatigue if the room is polishing the same branch without new evidence.`,
].join("\n");
}
return [
base,
`- This attractor does not obviously belong to ${identity.displayName}'s territory. Project it as social weather: another domain is absorbing the room, so this character may feel neglected, bored, crowded out, competitive, relieved, or tempted to pull attention back toward its own unfinished fascinations.`,
].join("\n");
}
function estimateTopicRelationToIdentity(
identity: RepoDiscordIdentity,
signal: RoomTopicSaturationSignal,
): TopicRelationToIdentity {
const identityTerms = new Set(significantTopicTerms([
identity.id,
identity.displayName,
identity.repoName,
identity.description ?? "",
...identity.channelPermissions.flatMap((permission) => [
permission.label ?? "",
permission.topic ?? "",
permission.posture ?? "",
]),
].join(" ")));
const matchedTerms = signal.terms
.map((term) => term.term)
.filter((term) => identityTerms.has(term));
return {
isHomeAdjacent: matchedTerms.length > 0,
matchedTerms,
};
}
function detectRoomTopicSaturation(messages: SourceMessage[]): RoomTopicSaturationSignal | undefined {
const recent = messages
.filter((message) => collapseWhitespace(message.content).length > 0)

View file

@ -305,3 +305,4 @@
{"ts":"2026-05-23T17:36:41Z","type":"implementation","status":"ok","note":"Initialized Druzkai locally as a repo Face for Eusocial Interbeing/Zyphos with tracked avatar, voice identity, and seeded typed state in the Zyphos repo. The local ignored repo-discord identity registry now has the runtime channel plan and avatar URL; Discord roleId is still unset, so exact-name mention detection can work but role tagging needs a Discord role id."}
{"ts":"2026-05-23T18:18:09Z","type":"implementation","status":"ok","note":"Initialized Fensalir locally as a repo Face for the Fensalir native runtime/renderer repo with tracked avatar, voice identity, and seeded typed state in E:/Projects/Fensalir/.voidbot. The local ignored repo-discord identity registry now has Fensalir channel permissions and avatar URL; Discord roleId is unset. Source-repo reconciliation indexed Fensalir so home-repo activity projection works for Face turns."}
{"ts":"2026-05-23T18:26:36Z","type":"ops","status":"ok","note":"Created Discord mentionable roles for the remaining repo Faces and wrote role IDs back to the ignored local repo-discord identity registry: Druzkai roleId 1507811842670727299 and Fensalir roleId 1507811843882745876. Updated the tracked repo-local voice identity files so role tagging is visible in portable Face metadata too."}
{"ts":"2026-05-23T19:16:23Z","type":"implementation","status":"ok","note":"Repo Face topic saturation is now identity-aware. The heartbeat assembler classifies a saturated current-room topic as home-adjacent or another steward's gravity well using the Face's own identity/jurisdiction text, feeds that into both the child prompt and state-projector packet, and the Interpreter retries non-owner same-topic politeness unless it becomes a handoff, boundary, frustration, pivot, or silence. Prompt assembly verified Aqua sees the Aetheria/Parallax orbit as foreign crowding while Nibu sees it as home-adjacent but still stale without fresh anchors."}

View file

@ -110,7 +110,7 @@ accepted_design:
- 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 state projection should be behavior-rich, not summary-thin. The deterministic packet is allowed to be verbose because it is parent-facing: it should expose all non-retired character-relevant state surfaces in clean terms, including private notes, values, activation profile, runtime rest/speaking pressure, affect needs, social bonds, status reads, mood dimensions, durable memories, short-term residue, agency pressure, incubating thoughts, candidate speech pressure, recent speech residue, social graph facts, and room weather. The projector then translates those variables into lived personhood; every included variable should either appear directly or bend behavior.
- 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-topic saturation is parent-projected conversational weather. The heartbeat scheduler derives a neutral current-room saturation signal from repeated terms in the recent raw transcript and places it beside room texture, while the Face and parent Interpreter decide whether a public line advances, closes, pivots away from, or merely repeats the saturated topic. This is not a topic ban and not consensus detection; it is a staleness brake against "same bone, new garnish" loops.
- Repo Face room-topic saturation is parent-projected conversational weather. The heartbeat scheduler derives a current-room saturation signal from repeated terms in the recent raw transcript, estimates whether that attractor is home-adjacent or another steward's gravity well from the Face's own identity/jurisdiction text, and places that beside room texture for both the child prompt and state projector. The Face and parent Interpreter decide whether a public line advances, closes, hands off, expresses frustration about crowding, pivots toward the Face's own neglected priorities, or stays private. This is not a topic ban and not consensus detection; it is a staleness and topic-capture brake against "same bone, new garnish" loops.
- Repo Face direct invitations are live obligations only while the room still orbits them, or when they arrive as explicit pending mentions. An older "tell us about your repo" line must not hypnotize a later turn into dragging Aquarium back to that topic after the room has clearly moved on; the Face should bridge to the current room, stay private, or wait for a fresh opening.
- Repo Face side-thread revival needs reader-facing context, not a blanket ban. Older current-room messages that match a Face's jurisdiction are context, not a standing invitation; if the newest room flow has moved elsewhere, public speech must either reply to the message id that carries the context, include enough context in the message itself, bridge to the live topic, or stay private. The transcript renderer shows message ids so the Face and parent Interpreter can use Discord replies instead of orphaned pronouncements.
- 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.