Isolate Persona memory projection policy

This commit is contained in:
Meta Vens 2026-07-16 01:00:46 +01:00
commit 85d60b6155
7 changed files with 88 additions and 52 deletions

View file

@ -62,6 +62,7 @@ import {
readPersonaMemoryRecall,
type PersonaMemoryRecallObservation,
} from "../apps/persona-scheduler/dist/persona-memory-context-source.js";
import { projectPersonaMemorySurface } from "../apps/persona-scheduler/dist/persona-memory-projector.js";
import {
readDiscordActivitySnapshot,
type IdleCoolingSnapshot,
@ -878,13 +879,26 @@ async function renderRepoFaceMemorySurfaceForTurn(
curiosityGraphFacts,
);
if (!config.repoFaceHeartbeats.stateProjectorEnabled) {
return rejectLeakyMemorySurface(statePacket);
return projectPersonaMemorySurface({
identityId: identity.id,
characterIdentity: renderRepoCharacterIdentityDoctrine(identity),
statePacket,
modelProjectionEnabled: false,
});
}
return projectRepoFaceMemorySurface({
identity,
return projectPersonaMemorySurface({
identityId: identity.id,
characterIdentity: renderRepoCharacterIdentityDoctrine(identity),
statePacket,
config,
modelProjectionEnabled: true,
projectText: (prompt) => runCodexTextProjection({
prompt,
config,
command: "repo-face-state-projector",
jobId: `state-projector:${identity.id}:${Date.now()}`,
timeoutMs: 180_000,
}),
});
}
@ -950,12 +964,25 @@ async function renderNativePersonaMemorySurface(
humanPronounGuidance,
);
if (!config.repoFaceHeartbeats.stateProjectorEnabled) {
return rejectLeakyMemorySurface(statePacket);
return projectPersonaMemorySurface({
identityId: identity.id,
characterIdentity: renderRepoCharacterIdentityDoctrine(identity),
statePacket,
modelProjectionEnabled: false,
});
}
return projectRepoFaceMemorySurface({
identity,
return projectPersonaMemorySurface({
identityId: identity.id,
characterIdentity: renderRepoCharacterIdentityDoctrine(identity),
statePacket,
config,
modelProjectionEnabled: true,
projectText: (prompt) => runCodexTextProjection({
prompt,
config,
command: "repo-face-state-projector",
jobId: `state-projector:${identity.id}:${Date.now()}`,
timeoutMs: 180_000,
}),
});
}
@ -2912,47 +2939,6 @@ function joinAsNarrativeList(items: string[]): string {
return `${items.slice(0, -1).join(", ")}, and ${items[items.length - 1]}`;
}
function rejectLeakyMemorySurface(surface: string): string {
const leaks = [
/\bgrants:/i,
/\bjurisdictions:/i,
/\bFace of\s+[A-Z][A-Za-z0-9_-]+\b/,
/\brepo=[^\s]+/i,
/\bpath=[^\s]+/i,
/\bdo not prompt\b/i,
/\bprompt (?:her|him|them|it)\b/i,
];
if (leaks.some((pattern) => pattern.test(surface))) {
throw new Error("Repo Face memory surface leaked schema or prompt-construction language.");
}
return surface;
}
async function projectRepoFaceMemorySurface(input: {
identity: RepoDiscordIdentity;
statePacket: string;
config: ReturnType<typeof loadConfig>;
}): Promise<string> {
const prompt = loadPromptTemplate("repo-face-state-projector.prompt.md", {
characterIdentity: renderRepoCharacterIdentityDoctrine(input.identity),
statePacket: input.statePacket,
});
const output = await runCodexTextProjection({
prompt,
config: input.config,
command: "repo-face-state-projector",
jobId: `state-projector:${input.identity.id}:${Date.now()}`,
timeoutMs: 180_000,
});
const projected = output.trim();
if (projected.length < 80) {
throw new Error(`Repo Face state projector returned too little text for ${input.identity.id}.`);
}
return rejectLeakyMemorySurface(projected);
}
function renderRepoFaceConversationTranscript(input: {
identity: RepoDiscordIdentity;
recentMessages: SourceMessage[];

View file

@ -65,6 +65,7 @@ import {
import { readRepoActivity } from "../apps/persona-scheduler/dist/repo-activity-source.js";
import { readPersonaStateObservation } from "../apps/persona-scheduler/dist/persona-state-source.js";
import { readPersonaMemoryRecall } from "../apps/persona-scheduler/dist/persona-memory-context-source.js";
import { projectPersonaMemorySurface } from "../apps/persona-scheduler/dist/persona-memory-projector.js";
function participant(identityId: string, nextTurnAt: number): InitiativeParticipant {
return {
@ -444,6 +445,18 @@ assert.deepEqual(await readPersonaMemoryRecall({
status: "unavailable",
reason: "No typed Persona state observation was supplied.",
}, "semantic recall fails explicitly before touching vector infrastructure when the typed Mind observation is absent");
assert.equal(await projectPersonaMemorySurface({
identityId: "nibu",
characterIdentity: "Nibu",
statePacket: "Nibu remembers the ship mind and remains irritated by careless continuity.",
modelProjectionEnabled: false,
}), "Nibu remembers the ship mind and remains irritated by careless continuity.", "disabled model projection returns the validated deterministic memory surface without invoking a model");
await assert.rejects(projectPersonaMemorySurface({
identityId: "nibu",
characterIdentity: "Nibu",
statePacket: "path=private-state",
modelProjectionEnabled: false,
}), /leaked schema/, "the projector rejects state-path leakage on the model-free path");
const stateSourceDirectory = await mkdtemp(join(tmpdir(), "voidbot-persona-state-source-"));
try {
const statePath = join(stateSourceDirectory, "nibu.cc");