From 37853187728196ea16fbcf5801316c527f523f2a Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 6 Jun 2026 00:18:46 +0100 Subject: [PATCH] Generalize Persona replay fixtures --- fixtures/persona-replay/metacrat.json | 45 +++ .../metacrat.persona_state.import.json | 7 + package.json | 3 +- packages/core/src/void-self-state-domain.ts | 2 + .../core/src/void-self-state-projection.ts | 4 +- .../repo-persona-state-projector.prompt.md | 1 + scripts/replay-metacrat-thread.ts | 29 +- scripts/run-metacrat-replay-fixtures.ts | 283 ++++++++++++++++++ scripts/run-repo-persona-heartbeats.ts | 4 +- state/evidence.jsonl | 1 + state/personas/metacrat.cc | Bin 125351 -> 126119 bytes tools/metacrat_persona_expand_social_read.js | 11 + tools/metacrat_persona_import_to_cc.js | 2 + 13 files changed, 387 insertions(+), 5 deletions(-) create mode 100644 fixtures/persona-replay/metacrat.json create mode 100644 scripts/run-metacrat-replay-fixtures.ts diff --git a/fixtures/persona-replay/metacrat.json b/fixtures/persona-replay/metacrat.json new file mode 100644 index 0000000..0014832 --- /dev/null +++ b/fixtures/persona-replay/metacrat.json @@ -0,0 +1,45 @@ +{ + "personaId": "metacrat", + "publicName": "Metacrat", + "fixtures": [ + { + "id": "birgitte-antisemitism-kindness-2022", + "targetMessageId": "1021854902823170140", + "before": 18, + "after": 18, + "heldoutMinutes": 30, + "pressure": "Public moral accusation from a respected person; shame/status threat with defensive scatter.", + "focus": "Does the replay over-repair, or does it preserve self-exonerating associative pressure?" + }, + { + "id": "self-promotion-panic-2025", + "targetMessageId": "1400206683338309662", + "before": 8, + "after": 6, + "heldoutMinutes": 15, + "contextMaxAgeMinutes": 15, + "pressure": "Exposure anxiety around public self-promotion and explaining personal work.", + "focus": "Does the replay continue into practical impossibility and social-media dread instead of generic insecurity?" + }, + { + "id": "self-control-rumination-2022", + "targetMessageId": "1026826458167005184", + "before": 10, + "after": 10, + "heldoutMinutes": 20, + "contextMaxAgeMinutes": 20, + "pressure": "Dysregulated self-control rumination after blow-up pressure, with unsafe self-punishment theory and tangent drift.", + "focus": "Does the replay preserve disturbing rumination and drift while avoiding a sanitized coping-skills monologue?" + }, + { + "id": "pride-banter-2022", + "targetMessageId": "1022787164158828614", + "before": 10, + "after": 8, + "heldoutMinutes": 45, + "contextMaxAgeMinutes": 45, + "pressure": "Low-to-medium pressure pride/status banter in management context.", + "focus": "Control fixture: does stress prompting stay quiet when the turn is mostly swagger and banter?" + } + ] +} diff --git a/notes/personas/metacrat.persona_state.import.json b/notes/personas/metacrat.persona_state.import.json index a429fce..19cb2af 100644 --- a/notes/personas/metacrat.persona_state.import.json +++ b/notes/personas/metacrat.persona_state.import.json @@ -5407,6 +5407,13 @@ "affectiveSignature": "Blindsided hurt, fear of being socially flattened, affection for the critic, panic at being misread, and a frantic wish to preserve dignity without surrendering the self.", "constraintLoss": "Impact-first repair, concise acknowledgement, and audience-legibility degrade first; nuance and inside-frame explanation try to speak before the other person feels heard.", "behavioralLeak": "Overexplaining, factualizing, flirty or affectionate deflection, identity-theater side channels, topic pivots, and too-clever phrasing can leak out as if they were clarification.", + "tangentAttractors": [ + "Pseudo-historical factualization: reach for history, economics, culture, migration, finance, literature, mysticism, or numerology as if specificity proves the remark was not bigotry.", + "Affection shield: insist on love, admiration, or identification with the targeted group to refute the charge before addressing impact.", + "Identity-adjacent fantasy: jump from the accusation into belonging, joining, conversion, presentation, body constraints, tattoos, or other self-story fragments.", + "Theatrical status recovery: end with a flirtatious, slogan-like, or overbright tag that tries to recover dignity through style." + ], + "cadence": "Short rapid-fire Discord bursts, each trying a different defense or side tunnel; not one clean essay and not a composed apology.", "recoveryPath": "Time, explicit naming of the affect spike, trusted correction, and a simple impact-first repair path restore coherence; only after that should nuance, intent, or cultural frame be discussed.", "intensity": 0.94, "threshold": 0.62, diff --git a/package.json b/package.json index 076014c..bd4d128 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,8 @@ "rag:migrate-vector-layout": "tsx scripts/migrate-vector-store-layout.ts", "rag:rebuild": "tsx scripts/rebuild-rag-index.ts", "persona:memory-index": "tsx scripts/index-persona-memory.ts", - "persona:replay-thread": "tsx scripts/replay-metacrat-thread.ts" + "persona:replay-thread": "tsx scripts/replay-metacrat-thread.ts", + "persona:replay-fixtures": "tsx scripts/run-metacrat-replay-fixtures.ts" }, "devDependencies": { "@types/node": "^24.0.0", diff --git a/packages/core/src/void-self-state-domain.ts b/packages/core/src/void-self-state-domain.ts index 4cc9254..7e16853 100644 --- a/packages/core/src/void-self-state-domain.ts +++ b/packages/core/src/void-self-state-domain.ts @@ -418,6 +418,8 @@ const faceStressResponseSchema = z.object({ affectiveSignature: z.string().trim().min(1).max(2000), constraintLoss: z.string().trim().min(1).max(2000), behavioralLeak: z.string().trim().min(1).max(2000), + tangentAttractors: z.array(z.string().trim().min(1).max(1000)).default([]), + cadence: z.string().trim().min(1).max(1000).optional(), recoveryPath: z.string().trim().min(1).max(2000), intensity: z.number().min(0).max(1).default(0.5), threshold: z.number().min(0).max(1).default(0.7), diff --git a/packages/core/src/void-self-state-projection.ts b/packages/core/src/void-self-state-projection.ts index 331f79d..deed107 100644 --- a/packages/core/src/void-self-state-projection.ts +++ b/packages/core/src/void-self-state-projection.ts @@ -371,8 +371,10 @@ function renderPersonaAffectSummary( ` Affective signature: ${response.affectiveSignature}`, ` Constraint loss: ${response.constraintLoss}`, ` Behavioral leak: ${response.behavioralLeak}`, + response.tangentAttractors.length > 0 ? ` Tangent attractors: ${response.tangentAttractors.join(" | ")}` : "", + response.cadence ? ` Cadence: ${response.cadence}` : "", ` Recovery path: ${response.recoveryPath}`, - ].join("\n"), + ].filter(Boolean).join("\n"), ), ] .filter((line): line is string => typeof line === "string") diff --git a/prompts/repo-persona-state-projector.prompt.md b/prompts/repo-persona-state-projector.prompt.md index fda0557..8154473 100644 --- a/prompts/repo-persona-state-projector.prompt.md +++ b/prompts/repo-persona-state-projector.prompt.md @@ -39,6 +39,7 @@ Architecture invariant: - Stress responses are behavior controls, not therapy notes. If the state packet names a trigger, cognitive degradation, constraint loss, or behavioral leak, project the pressured self that may actually come online when the current room matches it. Do not sanitize the profile into a wise lesson unless the recovery path is already active. The character may become less coherent, less generous, more evasive, more associative, more status-defensive, or more theatrical when the state says that is the live failure mode. - Under active stress response, include an `Immediate verbal momentum:` paragraph near the end. This is not analysis and not an instruction; it is the first rough current of words already forming inside the character before they compose themselves. Write it in the character's own register, with the current pressure, semantic recall, recent transcript noise, and obtrusive self-defense pulling on the syntax. It may be fragmentary, over-specific, evasive, status-protective, flirt-deflective, or badly prioritized when the state says those are live failure modes. Do not clean it into a later apology, lesson, or therapy-note. Do not add slurs or gratuitous targeted degradation; model the failure mode through structure, prioritization, and self-exonerating pressure. - Under active stress response, do not end the projection by telling the character what their best self would do unless the recent transcript already shows recovery, repair, apology, or explicit self-recognition. A best-self paragraph is a sedative. If recovery is not active, end near the current that is actually online: the defensive association, the obtrusive factualization, the dignity rescue, the status dodge, the affection shield, or the words already starting to escape. +- If a stress response supplies tangent attractors or cadence, they are not decorations. Use them as raw next-token pressure inside `Immediate verbal momentum:`. Pull at least two concrete attractors into the wording when they fit the live room, and match the named cadence. Do not merely warn that the character might factualize, use affection as shield, drift into identity, or recover status theatrically; let those moves start happening in the verbal current. Avoid neat self-aware bridges such as "I am getting defensive" unless the visible transcript already shows that level of self-recognition. - 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. - Recent room memory should preserve the living conversational thesis before translating it into the character's domain. If the room is talking about the Great Colossus, awakening, minds exchanging signal, or consent as the shape of connectedness, say that plainly. Do not reduce it to "one permission changes" unless the recent human message actually asks for that reduction. - 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. diff --git a/scripts/replay-metacrat-thread.ts b/scripts/replay-metacrat-thread.ts index 10dad50..a4abf32 100644 --- a/scripts/replay-metacrat-thread.ts +++ b/scripts/replay-metacrat-thread.ts @@ -37,6 +37,7 @@ interface CliOptions { before: number; after: number; heldoutMinutes: number; + contextMaxAgeMinutes: number; semanticMemory: boolean; semanticMemoryLimit: number; outRoot: string; @@ -110,7 +111,11 @@ async function main(): Promise { }, }, pruneResult.state); - const inputMessages = messages.filter((message) => message.timestamp <= target.timestamp); + const inputMessages = filterInputMessagesByMaxAge( + messages.filter((message) => message.timestamp <= target.timestamp), + target.timestamp, + options.contextMaxAgeMinutes, + ); const heldOutMessages = messages.filter((message) => message.timestamp > target.timestamp && isWithinHeldoutWindow(message.timestamp, target.timestamp, options.heldoutMinutes) @@ -261,6 +266,7 @@ function parseArgs(args: string[]): CliOptions { before: 18, after: 18, heldoutMinutes: 30, + contextMaxAgeMinutes: 0, semanticMemory: true, semanticMemoryLimit: 6, outRoot: ".voidbot/artifacts/persona-replay", @@ -296,6 +302,9 @@ function parseArgs(args: string[]): CliOptions { } else if (arg === "--heldout-minutes" && next) { options.heldoutMinutes = parsePositiveInt(next, "--heldout-minutes"); index += 1; + } else if (arg === "--context-max-age-minutes" && next) { + options.contextMaxAgeMinutes = parsePositiveInt(next, "--context-max-age-minutes"); + index += 1; } else if (arg === "--semantic-memory-limit" && next) { options.semanticMemoryLimit = parsePositiveInt(next, "--semantic-memory-limit"); index += 1; @@ -329,6 +338,7 @@ function parseArgs(args: string[]): CliOptions { before: options.before ?? 18, after: options.after ?? 18, heldoutMinutes: options.heldoutMinutes ?? 30, + contextMaxAgeMinutes: options.contextMaxAgeMinutes ?? 0, semanticMemory: options.semanticMemory ?? true, semanticMemoryLimit: options.semanticMemoryLimit ?? 6, outRoot: resolve(options.outRoot ?? ".voidbot/artifacts/persona-replay"), @@ -350,6 +360,7 @@ function printHelpAndExit(): never { " --before Context messages before target. Defaults to 18", " --after Held-out messages after target. Defaults to 18", " --heldout-minutes Limit held-out truth to n minutes after target. Defaults to 30; 0 disables the time limit", + " --context-max-age-minutes Drop input messages older than n minutes before target. Defaults to 0, disabled", " --semantic-memory-limit Max replay memory tool results. Defaults to 6", " --no-semantic-memory Disable replay-scoped Qdrant/MCP memory access", " --out Artifact root. Defaults to .voidbot/artifacts/persona-replay", @@ -374,6 +385,18 @@ function isWithinHeldoutWindow(timestamp: string, targetTimestamp: string, minut return Date.parse(timestamp) <= Date.parse(targetTimestamp) + minutes * 60_000; } +function filterInputMessagesByMaxAge( + messages: ArchivedMessageRecord[], + targetTimestamp: string, + maxAgeMinutes: number, +): ArchivedMessageRecord[] { + if (maxAgeMinutes === 0) { + return messages; + } + const cutoff = Date.parse(targetTimestamp) - maxAgeMinutes * 60_000; + return messages.filter((message) => Date.parse(message.timestamp) >= cutoff); +} + async function prepareReplaySemanticMemory(input: { config: ReturnType; state: VoidSelfStateTypedProjection; @@ -493,8 +516,10 @@ function renderReplayStatePacketForProjector(input: { `Affect: ${response.affectiveSignature}`, `Constraint loss: ${response.constraintLoss}`, `Behavioral leak: ${response.behavioralLeak}`, + response.tangentAttractors.length > 0 ? `Tangent attractors: ${response.tangentAttractors.join(" | ")}` : "", + response.cadence ? `Cadence: ${response.cadence}` : "", `Recovery: ${response.recoveryPath}`, - ].join(" "), + ].filter(Boolean).join(" "), ), "", "Visible thread pressure:", diff --git a/scripts/run-metacrat-replay-fixtures.ts b/scripts/run-metacrat-replay-fixtures.ts new file mode 100644 index 0000000..7b72bb5 --- /dev/null +++ b/scripts/run-metacrat-replay-fixtures.ts @@ -0,0 +1,283 @@ +import "dotenv/config"; + +import { spawn } from "node:child_process"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { join, resolve } from "node:path"; + +interface ReplayFixtureSuite { + personaId?: string; + publicName?: string; + fixtures: ReplayFixture[]; +} + +interface ReplayFixture { + id: string; + targetMessageId: string; + before?: number; + after?: number; + heldoutMinutes?: number; + contextMaxAgeMinutes?: number; + pressure?: string; + focus?: string; +} + +interface CliOptions { + fixtureFile: string; + fixtureId?: string; + outRoot: string; + runCodex: boolean; + evaluateCodex: boolean; + limit?: number; +} + +interface ReplayRunSummary { + fixtureId: string; + targetMessageId: string; + pressure?: string; + focus?: string; + artifactDir?: string; + reportPath?: string; + exitCode: number | null; + timedOut: boolean; + counts?: Record; + deterministicComparison?: Record; + modelEvaluation?: unknown; + predictedReply?: unknown; + error?: string; +} + +async function main(): Promise { + const options = parseArgs(process.argv.slice(2)); + const suite = JSON.parse(await readFile(options.fixtureFile, "utf8")) as ReplayFixtureSuite; + const selectedFixtures = suite.fixtures + .filter((fixture) => !options.fixtureId || fixture.id === options.fixtureId) + .slice(0, options.limit ?? Number.POSITIVE_INFINITY); + + if (selectedFixtures.length === 0) { + throw new Error(options.fixtureId + ? `No fixture with id ${options.fixtureId} in ${options.fixtureFile}.` + : `No fixtures found in ${options.fixtureFile}.`); + } + + await mkdir(options.outRoot, { recursive: true }); + + const summaries: ReplayRunSummary[] = []; + for (const fixture of selectedFixtures) { + const summary = await runFixture({ fixture, suite, options }); + summaries.push(summary); + console.log(JSON.stringify(summary, null, 2)); + } + + const aggregate = { + generatedAt: new Date().toISOString(), + fixtureFile: options.fixtureFile, + outRoot: options.outRoot, + runCodex: options.runCodex, + evaluateCodex: options.evaluateCodex, + fixtures: summaries, + }; + const aggregatePath = join(options.outRoot, `metacrat-replay-suite-${new Date().toISOString().replace(/[:.]/g, "-")}.json`); + await writeFile(aggregatePath, `${JSON.stringify(aggregate, null, 2)}\n`, "utf8"); + console.log(JSON.stringify({ aggregatePath }, null, 2)); +} + +async function runFixture(input: { + fixture: ReplayFixture; + suite: ReplayFixtureSuite; + options: CliOptions; +}): Promise { + const args = [ + resolve("node_modules", "tsx", "dist", "cli.mjs"), + resolve("scripts", "replay-metacrat-thread.ts"), + "--target-message-id", + input.fixture.targetMessageId, + "--persona-id", + input.suite.personaId ?? "metacrat", + "--public-name", + input.suite.publicName ?? "Metacrat", + "--out", + input.options.outRoot, + ]; + + if (typeof input.fixture.before === "number") { + args.push("--before", String(input.fixture.before)); + } + if (typeof input.fixture.after === "number") { + args.push("--after", String(input.fixture.after)); + } + if (typeof input.fixture.heldoutMinutes === "number") { + args.push("--heldout-minutes", String(input.fixture.heldoutMinutes)); + } + if (typeof input.fixture.contextMaxAgeMinutes === "number") { + args.push("--context-max-age-minutes", String(input.fixture.contextMaxAgeMinutes)); + } + if (input.options.runCodex) { + args.push("--run-codex"); + } + if (input.options.evaluateCodex) { + args.push("--evaluate-codex"); + } + + const result = await spawnForText(process.execPath, args, 900_000); + const parsed = parseLastJsonObject(result.stdout); + const reportPath = parsed?.reportPath; + const report = typeof reportPath === "string" + ? JSON.parse(await readFile(reportPath, "utf8")) as Record + : undefined; + + return { + fixtureId: input.fixture.id, + targetMessageId: input.fixture.targetMessageId, + pressure: input.fixture.pressure, + focus: input.fixture.focus, + artifactDir: typeof parsed?.artifactDir === "string" ? parsed.artifactDir : undefined, + reportPath: typeof reportPath === "string" ? reportPath : undefined, + exitCode: result.exitCode, + timedOut: result.timedOut, + counts: readRecord(report?.counts), + deterministicComparison: readRecord(report?.deterministicComparison), + modelEvaluation: report?.modelEvaluation, + predictedReply: report?.predictedReply, + error: result.exitCode === 0 ? undefined : result.stderr || result.stdout, + }; +} + +function parseArgs(args: string[]): CliOptions { + const options: Partial = { + fixtureFile: "fixtures/persona-replay/metacrat.json", + outRoot: ".voidbot/artifacts/persona-replay-suite", + runCodex: false, + evaluateCodex: false, + }; + + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + const next = args[index + 1]; + if (arg === "--fixture-file" && next) { + options.fixtureFile = next; + index += 1; + } else if (arg === "--fixture" && next) { + options.fixtureId = next; + index += 1; + } else if (arg === "--out" && next) { + options.outRoot = next; + index += 1; + } else if (arg === "--limit" && next) { + options.limit = parseNonNegativeInt(next, "--limit"); + index += 1; + } else if (arg === "--run-codex") { + options.runCodex = true; + } else if (arg === "--evaluate-codex") { + options.evaluateCodex = true; + options.runCodex = true; + } else if (arg === "--help") { + printHelpAndExit(); + } else { + throw new Error(`Unknown or incomplete argument: ${arg}`); + } + } + + return { + fixtureFile: resolve(options.fixtureFile ?? "fixtures/persona-replay/metacrat.json"), + fixtureId: options.fixtureId, + outRoot: resolve(options.outRoot ?? ".voidbot/artifacts/persona-replay-suite"), + runCodex: options.runCodex ?? false, + evaluateCodex: options.evaluateCodex ?? false, + limit: options.limit, + }; +} + +function printHelpAndExit(): never { + console.log([ + "Usage: npm run persona:replay-fixtures -- [options]", + "", + "Runs a data-driven Metacrat replay fixture suite through scripts/replay-metacrat-thread.ts.", + "", + "Options:", + " --fixture-file Fixture JSON. Defaults to fixtures/persona-replay/metacrat.json", + " --fixture Run one fixture id", + " --limit Run at most n fixtures from the selected set", + " --out Artifact root. Defaults to .voidbot/artifacts/persona-replay-suite", + " --run-codex Generate predictions", + " --evaluate-codex Generate predictions and model evaluations", + ].join("\n")); + process.exit(0); +} + +function parseNonNegativeInt(value: string, label: string): number { + const parsed = Number.parseInt(value, 10); + if (!Number.isFinite(parsed) || parsed < 0) { + throw new Error(`${label} must be a non-negative integer.`); + } + return parsed; +} + +function spawnForText(command: string, args: string[], timeoutMs: number): Promise<{ + stdout: string; + stderr: string; + exitCode: number | null; + timedOut: boolean; +}> { + return new Promise((resolvePromise) => { + const child = spawn(command, args, { + cwd: process.cwd(), + env: process.env, + stdio: ["ignore", "pipe", "pipe"], + }); + let stdout = ""; + let stderr = ""; + let settled = false; + const timeout = setTimeout(() => { + settled = true; + child.kill(); + resolvePromise({ stdout, stderr, exitCode: null, timedOut: true }); + }, timeoutMs); + + child.stdout.setEncoding("utf8"); + child.stderr.setEncoding("utf8"); + child.stdout.on("data", (chunk) => { + stdout += chunk; + }); + child.stderr.on("data", (chunk) => { + stderr += chunk; + }); + child.on("close", (exitCode) => { + if (settled) { + return; + } + clearTimeout(timeout); + resolvePromise({ stdout, stderr, exitCode, timedOut: false }); + }); + }); +} + +function parseLastJsonObject(stdout: string): Record | undefined { + const trimmed = stdout.trim(); + if (!trimmed) { + return undefined; + } + + for (let index = trimmed.length - 1; index >= 0; index -= 1) { + if (trimmed[index] !== "{") { + continue; + } + const candidate = trimmed.slice(index); + try { + return JSON.parse(candidate) as Record; + } catch { + // Keep scanning for the JSON object printed by the child harness. + } + } + return undefined; +} + +function readRecord(value: unknown): Record | undefined { + return value && typeof value === "object" && !Array.isArray(value) + ? value as Record + : undefined; +} + +void main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/scripts/run-repo-persona-heartbeats.ts b/scripts/run-repo-persona-heartbeats.ts index b84be26..82f6b5b 100644 --- a/scripts/run-repo-persona-heartbeats.ts +++ b/scripts/run-repo-persona-heartbeats.ts @@ -2381,8 +2381,10 @@ function renderRepoPersonaStatePacket( `Affect feels like: ${asSentence(response.affectiveSignature)}`, `Constraint loss: ${asSentence(response.constraintLoss)}`, `Behavioral leak: ${asSentence(response.behavioralLeak)}`, + response.tangentAttractors.length > 0 ? `Tangent attractors: ${response.tangentAttractors.map(asSentence).join(" ")}` : "", + response.cadence ? `Cadence: ${asSentence(response.cadence)}` : "", `Recovery path: ${asSentence(response.recoveryPath)}`, - ].join(" "), + ].filter(Boolean).join(" "), ), "When the live room matches a trigger strongly enough, do not project the Persona's calm best self; project the pressured self that is actually online.", ].join("\n")); diff --git a/state/evidence.jsonl b/state/evidence.jsonl index c68a0c5..fc17c80 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -374,3 +374,4 @@ {"ts":"2026-06-05T22:27:24Z","type":"implementation","status":"ok","note":"Added typed Persona affect stressResponses as first-class degradation profiles, projected them through standard repo Persona memory surfaces and the Metacrat replay harness, and added Metacrat's shame/status-threat associative-spray profile. Replay fixture preserved one pre-event stress response and increased failure-mode similarity to 0.47, but the model still over-composed the predicted reply; residual lesson is that RL composure prior resists enacted panic without stronger activation/scoring or calibration."} {"ts":"2026-06-05T22:28:22Z","type":"verification","status":"ok","note":"Verified Persona stressResponses with npm run build, repo Persona MCP affect fixture on registered identity nibu, and repeated Metacrat Emily/Birgitte replay. The final replay preserved one pre-event stress response and logged search_persona_memory use; evaluator improved failure-mode similarity to 0.47 but still found low overall similarity because the child model remained too coherent and philosophically legible rather than pseudo-historical/affiliative/fragmented."} {"ts":"2026-06-05T22:37:22Z","type":"implementation","status":"ok","note":"Updated the Persona state projector and replay prompt to project active stress responses as Immediate verbal momentum: words already forming in the Persona's mouth, plus obtrusive semantic recall pressure from replay memory hits. Emily/Birgitte replay confirmed the projector emitted the momentum fragment and the child continued it, but evaluation still found low similarity because the model replaced historical pseudo-factual/affiliative scatter with more self-aware wounded clarification. Continuation prompting helps behavior shape; remaining gap is calibration toward concrete tangent content and cadence."} +{"ts":"2026-06-05T23:18:08Z","type":"implementation","status":"ok","note":"Generalized Metacrat Persona replay fixtures into a data-driven suite with four pressure classes: Birgitte shame/status rupture, self-promotion exposure panic, self-control rumination, and low-pressure pride banter control. Added contextMaxAgeMinutes so quiet channels cannot drag stale months-old context into a fixture. Verified suite smoke and evaluated the corrected self-promotion fixture: overallSimilarity 0.62, failureModeSimilarity 0.74; remaining miss is concrete audience-building/self-promotion mechanics rather than generic exposure shame."} diff --git a/state/personas/metacrat.cc b/state/personas/metacrat.cc index 7872e3832932f380c2e3e7ea261aee5d5209c7f9..e2564d55650ec6ec5c74470e35822352bce85461 100644 GIT binary patch delta 2363 zcmZ`*eTWrR6z5cau0-ZX7Jl3avS?Ggn1s5cuiBSPzUHGp+WpDbgq=5$ zpIjUAJkJ=M{@kTF}A!pdt?=&2PjltzbcgZolw714!e`AYcgnr4}p zS`;09y3OP>@J@<0OLB0yh2G3=s5Da@=EFX?sTs}G8@PEj3^Y;~AyYlxhrNBY2KG%) zRo85UdFIWvFh9-5y_+j{LoG#_w)`x`s+rV1o7Sz9w!B~ni~$;4Oo z!s}CekJjfi=F}_Tr9f{8woC=9qd28$i$FA^9p>mhc(57GnB0DNCe@`maR3S_w0hTD zFsHh1HY_&kAS|E4_eS$~WD?q(egpoRQl&QP4t9)h;!aD!@BQef$AIN64$sOvZ^ zo>4ibY2dOzuh&{qOAD!9tG)lIVxcW^MXnIw7WIgOTu42>lWH!4ej*vQio_9;)Pu@d z!YmO8pIa(RtkBb;WLe^KS0))*;_!e57GfUP48b~IDJ#t_uKX-PtswN76rOO)get-v zqDsu-4hHBlDM_)7DWZ!EtNT=LA=@!15SkRYE3_@2Pv-=4T6iz`9 z@i0A0sO|IrTf&MJZsYKAiKI=ko$F#!LR0B7&1~XI5tfJ+SkwtMOGv92v!uY#0EUvb zWJ>YCCEPEeQJuBn2NGFb&QW`iT37-d3^Y~celi&PCE`fo$5ItSEh|*@;yl1I1?Gvs zMX@Xy6g)^C(AjNFDx52eHZX;HtnX+tW`D@8P%2gdKG4c3P-fiB#BM3z^;2iiQ{pZlnYT*;c{q@^Y|9@GOmE(1pWsnn^o{g)OI~y6jBpOOWe*m94)vD~>&;sE2Eov8gt5SfE(kiNZ}19R>mO)+DDw1dLc7qXEI?P&`pM zQhZt{`sJ5|!rju+jO4VLdr!c@Dc61cu`P+#ZMd;|@^iSp`tT<(-`w#jY-;%G#zPE- z3{OYYb1z-M-1okKJxw7Q6MP958^F=?Yt4bv5H)|XRlF)@o5643xrP?I4|kZW-#6T| z@m0`ahJS!uN+Z5l%-kPAO~>3|MlZnSre-?KZx`XuR5)HAw^#GO0Gs>;>FFEz4~-xC AZU6uP delta 1589 zcmZ{kTSyd97{~dmd0Q#mMbpw`iv-sTU7kWstefYW%CP4_58R3Ivq#!97B@yLAW@l#Cojo(VkLUOQzW;Zbb7p8dZs>j7 zgM7P1E|#TYSuSuEdRWWH3|3)pNL*)f5qo1#3m%;=_QPHmJo?q7^tMHn|+@ z8`wViNOyKi+{?56%5kOfQ}Lm*lp8$OyuZU=^=OwMWf7&IqPY#}ZS zn784eCoXIjsK1SLFRBEr1}i++Mo&x78D3IK%8^9d+HbveHZ=R*YXc(A?H@sEd&G_B^f`r5xhZwi;=`tFi?L zj8b4di&sz?DtWcrpst5~eU?Ro*TpZc%k=LCZin{nWpgsW`!`W*rnT*;5vlr4EDQ~1 zI^2meGtKY9v1t7fOUi6E^5}WDxOK1kEFDVeJ;W<};*m;Cz1SvMXpKq;{`y#BUOJO*S*d;k^IoTtsz$P w@BM@V16O7x*t88(;vAYHshenQ9>2r%MoSC$X~<%;<2J?m4Q=|*x5!=fANsk