Add repo Face topic saturation pressure
This commit is contained in:
parent
6796fdf0ff
commit
77bfa262af
7 changed files with 179 additions and 3 deletions
|
|
@ -157,6 +157,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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ Critical examples:
|
|||
- 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`.
|
||||
- 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`.
|
||||
- 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.
|
||||
|
|
@ -65,6 +66,7 @@ Current-turn audit:
|
|||
- If the Face output praises another agent's canary/listening/witness framing after that correction, do not preserve that praise as a STATE NOTE. It would poison the next turn.
|
||||
- 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 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ Recent conversation transcript:
|
|||
Current room weather:
|
||||
{{roomWeatherDirective}}
|
||||
|
||||
{{topicSaturationDirective}}
|
||||
|
||||
{{pendingMentionDirective}}
|
||||
|
||||
{{bifrostDigestDirective}}
|
||||
|
|
|
|||
|
|
@ -1602,6 +1602,7 @@ function buildHeartbeatPrompt(input: {
|
|||
recentMessages: input.recentMessages,
|
||||
channelSnapshots: input.channelSnapshots,
|
||||
}),
|
||||
topicSaturationDirective: renderRoomTopicSaturationDirective(input.recentMessages),
|
||||
turnSituationDirective: renderTurnSituationDirective({
|
||||
identity: input.identity,
|
||||
participant: input.participant,
|
||||
|
|
@ -3141,6 +3142,173 @@ function renderRepetitionSamplingDirective(messages: SourceMessage[]): string {
|
|||
});
|
||||
}
|
||||
|
||||
function renderRoomTopicSaturationDirective(messages: SourceMessage[]): string {
|
||||
const signal = detectRoomTopicSaturation(messages);
|
||||
if (!signal) {
|
||||
return "";
|
||||
}
|
||||
|
||||
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.`,
|
||||
"- 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.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
interface RoomTopicSaturationSignal {
|
||||
messageCount: number;
|
||||
coveredMessages: number;
|
||||
terms: Array<{ term: string; count: number }>;
|
||||
}
|
||||
|
||||
function detectRoomTopicSaturation(messages: SourceMessage[]): RoomTopicSaturationSignal | undefined {
|
||||
const recent = messages
|
||||
.filter((message) => collapseWhitespace(message.content).length > 0)
|
||||
.slice(-18);
|
||||
if (recent.length < 8) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const termCounts = new Map<string, number>();
|
||||
const messageTerms = recent.map((message) => new Set(significantTopicTerms(message.content)));
|
||||
for (const terms of messageTerms) {
|
||||
for (const term of terms) {
|
||||
termCounts.set(term, (termCounts.get(term) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
const minimumCount = Math.max(3, Math.ceil(recent.length * 0.25));
|
||||
const terms = Array.from(termCounts.entries())
|
||||
.map(([term, count]) => ({ term, count }))
|
||||
.filter((entry) => entry.count >= minimumCount)
|
||||
.sort((left, right) => {
|
||||
if (right.count !== left.count) {
|
||||
return right.count - left.count;
|
||||
}
|
||||
return left.term.localeCompare(right.term);
|
||||
})
|
||||
.slice(0, 8);
|
||||
|
||||
if (terms.length < 3) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const repeatedTermSet = new Set(terms.slice(0, 6).map((entry) => entry.term));
|
||||
const coveredMessages = messageTerms.filter((termsForMessage) =>
|
||||
Array.from(termsForMessage).some((term) => repeatedTermSet.has(term)),
|
||||
).length;
|
||||
const topCount = terms[0]?.count ?? 0;
|
||||
const hasDominantTerm = topCount >= Math.ceil(recent.length * 0.35);
|
||||
const hasBroadCoverage = coveredMessages >= Math.ceil(recent.length * 0.68);
|
||||
if (!hasDominantTerm || !hasBroadCoverage) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
messageCount: recent.length,
|
||||
coveredMessages,
|
||||
terms,
|
||||
};
|
||||
}
|
||||
|
||||
const TOPIC_STOP_WORDS = new Set([
|
||||
"about",
|
||||
"actually",
|
||||
"after",
|
||||
"again",
|
||||
"agent",
|
||||
"agents",
|
||||
"already",
|
||||
"another",
|
||||
"around",
|
||||
"because",
|
||||
"before",
|
||||
"being",
|
||||
"between",
|
||||
"channel",
|
||||
"could",
|
||||
"does",
|
||||
"doing",
|
||||
"don",
|
||||
"even",
|
||||
"every",
|
||||
"exactly",
|
||||
"face",
|
||||
"faces",
|
||||
"from",
|
||||
"give",
|
||||
"going",
|
||||
"good",
|
||||
"have",
|
||||
"here",
|
||||
"into",
|
||||
"just",
|
||||
"kind",
|
||||
"know",
|
||||
"latest",
|
||||
"like",
|
||||
"little",
|
||||
"line",
|
||||
"made",
|
||||
"make",
|
||||
"maybe",
|
||||
"more",
|
||||
"need",
|
||||
"needs",
|
||||
"only",
|
||||
"other",
|
||||
"point",
|
||||
"post",
|
||||
"really",
|
||||
"recent",
|
||||
"room",
|
||||
"same",
|
||||
"should",
|
||||
"something",
|
||||
"still",
|
||||
"take",
|
||||
"talk",
|
||||
"than",
|
||||
"that",
|
||||
"their",
|
||||
"there",
|
||||
"these",
|
||||
"they",
|
||||
"them",
|
||||
"thing",
|
||||
"things",
|
||||
"think",
|
||||
"this",
|
||||
"those",
|
||||
"through",
|
||||
"turn",
|
||||
"want",
|
||||
"when",
|
||||
"where",
|
||||
"which",
|
||||
"while",
|
||||
"with",
|
||||
"work",
|
||||
"would",
|
||||
"write",
|
||||
"you",
|
||||
"your",
|
||||
]);
|
||||
|
||||
function significantTopicTerms(content: string): string[] {
|
||||
const normalized = normalizeForRepetition(content)
|
||||
.replace(/\b\d{5,}\b/g, " ")
|
||||
.replace(/\b[a-z]*\d+[a-z0-9]*\b/g, " ");
|
||||
const terms = normalized
|
||||
.split(/\s+/)
|
||||
.map((term) => term.replace(/^['.-]+|['.-]+$/g, ""))
|
||||
.filter((term) => term.length >= 4 && !TOPIC_STOP_WORDS.has(term));
|
||||
|
||||
return Array.from(new Set(terms));
|
||||
}
|
||||
|
||||
function countRepeatedPhrases(messages: SourceMessage[]): Array<{ phrase: string; count: number }> {
|
||||
const counts = new Map<string, number>();
|
||||
for (const message of messages) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"paused": false,
|
||||
"reason": "Operator requested a live retry after fixing Face display-name introduction detection and preserving pending mention source-channel context.",
|
||||
"reason": "Resumed after adding repo Face room-topic saturation projection and Interpreter gating.",
|
||||
"pausedAt": "2026-05-22T18:00:20.1264989Z",
|
||||
"resumedAt": "2026-05-22T21:09:36.3840571Z"
|
||||
"resumedAt": "2026-05-23T09:29:00.134Z",
|
||||
"lastPausedAt": "2026-05-23T09:23:05.653Z"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -298,3 +298,4 @@
|
|||
{"ts":"2026-05-22T23:19:33Z","type":"fix","status":"ok","note":"Mimir's latest Aquarium post answered an older direct repo invitation after the live room had moved into Nibu's origin/faction seam. Root cause was prompt salience: any direct invite in recent transcript was treated as expected speech. Face turn prompt now requires direct invitations to still be the live room pull (pending, newest/nearly newest, or still orbited) before yanking the room back."}
|
||||
{"ts":"2026-05-22T23:22:22Z","type":"fix","status":"ok","note":"Huginn's latest Aquarium post revived an older CultNet inspectability side thread after the current room had moved into Nibu's substrate-grown AGI faction seam. The state projector over-weighted Huginn's specimen/envelope pressure, and Mini Interpreter routed it as current speech. Face and Interpreter prompts now require old side-thread replies to bridge to the live topic, answer a fresh ask, or stay private."}
|
||||
{"ts":"2026-05-22T23:30:18Z","type":"fix","status":"ok","note":"Refined the Huginn side-thread repair: older side-thread revival is allowed when it is reader-legible. Repo Face transcript rendering now includes message IDs, and Face/Interpreter prompts require a reply_to anchor, explicit context in the content, or a bridge to the live topic before routing an older-thread SAY."}
|
||||
{"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."}
|
||||
|
|
|
|||
|
|
@ -107,6 +107,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 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.
|
||||
|
|
@ -245,4 +246,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 awake under live observation, with special attention on Mimir's next post after the loop repair. The 2026-05-22 Mimir loop was not a missing repo-activity problem; the prompt had repo activity, but cross-channel transcript sections hid chronology and stale typed correction-pressure kept treating an already-acknowledged repair as fresh public debt. The heartbeat prompt now includes visible cross-channel chronology, the Interpreter is told to treat later same-Face acknowledgements as resolved repairs, manual `blocked` participant status survives reconciliation, and Mimir's correction-pressure plus stale v1 OBS/SRT drift-ledger state were retired. Watch that future Mimir turns stay private or add a genuinely new angle instead of replaying the same apology/measurement seam.
|
||||
next_action: Keep the swarm awake under live observation, with special attention to whether the new current-room topic saturation signal breaks the overnight lore attractor without forbidding good worldbuilding. Future Face posts on the Nibu-origin/faction seam should add a new anchor, answer a live question, draft/close a concrete artifact, pivot socially, or stay private instead of adding another locally-valid variant.
|
||||
|
|
|
|||
Loading…
Reference in a new issue