Add dynamic Persona memory recall to Face interpreter

This commit is contained in:
Meta Vens 2026-06-09 01:27:58 +01:00
commit d9524cbe35
3 changed files with 146 additions and 0 deletions

View file

@ -648,10 +648,12 @@ async function interpretRepoFaceTurnOutput(
outputText: string,
input: { attempt: 1 | 2 },
): Promise<RepoFaceParentInterpretation> {
const dynamicMemoryRecall = await renderRepoFaceDynamicMemoryRecall(job, outputText);
const interpreterPrompt = loadPromptTemplate("repo-face-turn-interpreter.prompt.md", {
attempt: input.attempt,
facePrompt: renderRepoFaceInterpreterPromptContext(job.contextBundle.prompt),
faceOutput: outputText.slice(0, 8000),
dynamicMemoryRecall,
});
const interpreterContext = {
...job.contextBundle,
@ -665,6 +667,73 @@ async function interpretRepoFaceTurnOutput(
return parseRepoFaceParentInterpretation(interpretationText, input.attempt);
}
async function renderRepoFaceDynamicMemoryRecall(
job: JobRecord,
outputText: string,
): Promise<string> {
const identityId = parseRepoIdentityIdFromRequestMessageId(job.requestMessageId)
?? parseRepoIdentityIdFromPrompt(job.prompt);
if (!identityId) {
return "- Dynamic self-memory recall skipped: no Face identity could be resolved for this job.";
}
const query = [
`Current train of thought from ${identityId}:`,
outputText.slice(0, 6000),
"Original turn pressure:",
extractSemanticRecallSeedFromPrompt(job.contextBundle.prompt),
].join("\n");
try {
const results = await retrievalService.searchPersonaMemory(query, 12, {
identityId,
});
if (results.length === 0) {
return "- Dynamic self-memory recall ran, but no nearby Persona memories were found.";
}
return [
"These are derived semantic hits from the Face's own typed memory, queried from the current Face output. They are hints for interpretation and state carry-forward; `.cc` remains the owner.",
...results.map((result, index) => {
const kind = result.metadata.memoryKind ? `/${result.metadata.memoryKind}` : "";
const target = result.metadata.targetLabel ?? result.metadata.targetId ?? "unknown target";
return `- ${index + 1}. ${target}${kind} score=${result.score.toFixed(3)}: ${collapseWhitespace(result.text, 560)}`;
}),
].join("\n");
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return [
"Dynamic self-memory recall unavailable:",
`- ${collapseWhitespace(message, 320)}`,
"- Do not pretend dynamic recall was available; interpret from the original prompt and Face output only.",
].join("\n");
}
}
function extractSemanticRecallSeedFromPrompt(prompt: string): string {
const markers = [
"Semantic Persona memory recall:",
"Fresh projected state for this turn:",
"Recent conversation transcript:",
];
const sections: string[] = [];
for (const marker of markers) {
const start = prompt.indexOf(marker);
if (start < 0) {
continue;
}
const nextStarts = markers
.map((candidate) => candidate === marker ? -1 : prompt.indexOf(candidate, start + marker.length))
.filter((index) => index > start)
.sort((left, right) => left - right);
const end = nextStarts[0] ?? Math.min(prompt.length, start + 2400);
sections.push(prompt.slice(start, Math.min(end, start + 2400)));
}
return sections.join("\n\n").slice(0, 5000);
}
function collapseWhitespace(input: string, maxLength = 1000): string {
const collapsed = input.replace(/\s+/g, " ").trim();
return collapsed.length > maxLength ? `${collapsed.slice(0, Math.max(0, maxLength - 3))}...` : collapsed;
}
function normalizeModelText(content: string): string {
return content.trim();
}

View file

@ -55,8 +55,15 @@ Face output to review:
{{faceOutput}}
```
Dynamic self-memory recall:
```
{{dynamicMemoryRecall}}
```
Current-turn audit:
- Look again at the Original Face prompt transcript and the Face output together.
- Use Dynamic self-memory recall as retrieved self-context awakened by the Face output's own train of thought. It is not authority by itself, but it can reveal relevant bonds, wounds, values, needs, status reads, and doctrine that the first prompt did not foreground.
- If recall strongly contradicts the Face output, treat that as a missing-context or stale-context problem before routing. If recall deepens the output, preserve the durable part in STATE NOTE blocks when appropriate.
- If recent conversation corrected the scale, owner, evidence shape, or implementation target of a proposal, check whether the Face output actually changed that shape. A smaller or prettier version of the rejected shape is still the old belief surviving.
- If recent conversation corrected the scale upward from local mechanics into collective mind / Colossus / consent-as-connectedness, check whether the Face actually responds at that scale. A reply can still be concrete, but it must not shrink the correction into the same local artifact it was correcting.
- If the Face praises or preserves a recently corrected framing from another agent, do not store that praise as durable state unless the transcript later re-grounds it. Echo should not become memory poison.

View file

@ -0,0 +1,70 @@
#!/usr/bin/env node
import "dotenv/config";
import { loadConfig } from "@voidbot/config";
import {
createTextEmbedder,
createVectorStores,
RetrievalService,
} from "@voidbot/rag";
if (!process.env.DISCORD_OWNER_ID) {
process.env.DISCORD_OWNER_ID = "__persona_dynamic_memory_smoke__";
}
const identityId = readArg("--identity") ?? "metame";
const query = readArg("--query")
?? "My week was about consent, body and home boundaries, Emily helping through dissociation, Wayne failing support, Lobo threatening retaliation, and evidence preservation.";
const config = loadConfig();
const embedder = createTextEmbedder({
backend: config.ragEmbeddingBackend,
hashDimensions: config.ragEmbeddingDimensions,
ollamaBaseUrl: config.ragOllamaBaseUrl,
ollamaModel: config.ragOllamaModel,
ollamaTimeoutMs: config.ragOllamaTimeoutMs,
queryInstruction: "Given a Face's current train of thought, retrieve relevant memories, bonds, needs, status reads, and doctrine from this Persona's own typed state.",
});
const stores = createVectorStores({
kind: config.vectorStore.kind,
historyPath: config.vectorStore.path,
personaMemoryPath: config.vectorStore.personaMemoryPath,
sourceRoot: config.sourceVectorStoreRoot,
qdrant: config.qdrant,
historyEmbedder: embedder,
sourceEmbedder: embedder,
personaMemoryEmbedder: embedder,
});
const retrieval = new RetrievalService(stores.history, stores.source, stores.personaMemory);
const results = await retrieval.searchPersonaMemory(query, 12, { identityId });
const combined = results.map((result) => result.text).join("\n");
const summary = {
ok: results.length > 0,
identityId,
vectorStoreKind: config.vectorStore.kind,
resultCount: results.length,
hasEmily: /Emily/i.test(combined),
hasWayne: /Wayne/i.test(combined),
hasLobo: /Lobo/i.test(combined),
hasConsent: /consent/i.test(combined),
};
console.log(JSON.stringify(summary, null, 2));
for (const [index, result] of results.entries()) {
console.log(`--- ${index + 1} ${result.score.toFixed(3)} ${result.metadata.targetLabel ?? result.metadata.targetId}/${result.metadata.memoryKind}`);
console.log(result.text.slice(0, 700));
}
if (!summary.ok || !summary.hasEmily || !summary.hasWayne || !summary.hasLobo || !summary.hasConsent) {
process.exitCode = 1;
}
function readArg(name) {
const index = process.argv.indexOf(name);
if (index < 0) {
return undefined;
}
const value = process.argv[index + 1];
return value && !value.startsWith("--") ? value : undefined;
}