Add Persona semantic memory recall

This commit is contained in:
Meta Vens 2026-06-09 00:36:42 +01:00
commit c779f4915f
16 changed files with 396 additions and 12 deletions

View file

@ -170,14 +170,17 @@ export async function startBot(): Promise<void> {
const vectorStores = createVectorStores({
kind: config.vectorStore.kind,
historyPath: config.vectorStore.path,
personaMemoryPath: config.vectorStore.personaMemoryPath,
sourceRoot: config.sourceVectorStoreRoot,
qdrant: config.qdrant,
historyEmbedder: embedder,
sourceEmbedder: sourceQueryEmbedder,
personaMemoryEmbedder: embedder,
});
const vectorStore = vectorStores.history;
const sourceRetrievalStore = vectorStores.source;
const retrievalService = new RetrievalService(vectorStore, sourceRetrievalStore);
const personaMemoryStore = vectorStores.personaMemory;
const retrievalService = new RetrievalService(vectorStore, sourceRetrievalStore, personaMemoryStore);
const historyIngester = new HistoryIngester();
const sourceDocumentIngester = new SourceDocumentIngester();
const ragPipeline = new RagPipeline(archiveRepository, historyIngester, vectorStore);

View file

@ -112,19 +112,23 @@ const sourceQueryEmbedder = createTextEmbedder({
const vectorStores = createVectorStores({
kind: config.vectorStore.kind,
historyPath: config.vectorStore.path,
personaMemoryPath: config.vectorStore.personaMemoryPath,
sourceRoot: config.sourceVectorStoreRoot,
qdrant: config.qdrant,
historyEmbedder: embedder,
sourceEmbedder: sourceQueryEmbedder,
personaMemoryEmbedder: embedder,
});
const historyVectorStore = vectorStores.history;
const sourceVectorStore = vectorStores.source;
const personaMemoryVectorStore = vectorStores.personaMemory;
const archiveRepository = new FileMessageArchiveRepository(config.ragArchivePath);
const sourceArchiveRepository = new FileSourceDocumentArchiveRepository(config.ragSourceArchivePath);
const sourceDocumentIngester = new SourceDocumentIngester();
const retrievalService = new RetrievalService(
historyVectorStore,
sourceVectorStore,
personaMemoryVectorStore,
);
let isProcessing = false;

View file

@ -47,14 +47,17 @@ export function createVoidbotMcpContext(): VoidbotMcpContext {
const vectorStores = createVectorStores({
kind: config.vectorStore.kind,
historyPath: config.vectorStore.path,
personaMemoryPath: config.vectorStore.personaMemoryPath,
sourceRoot: config.sourceVectorStoreRoot,
qdrant: config.qdrant,
historyEmbedder: embedder,
sourceEmbedder: sourceQueryEmbedder,
personaMemoryEmbedder: embedder,
});
const retrievalService = new RetrievalService(
vectorStores.history,
vectorStores.source,
vectorStores.personaMemory,
);
const sourceDocumentIngester = new SourceDocumentIngester();

View file

@ -48,12 +48,14 @@ const envSchema = z.object({
DATABASE_DSN: z.string().min(1).default("postgres://voidbot:voidbot@localhost:5432/voidbot"),
VECTOR_STORE_KIND: z.enum(["local_json", "qdrant"]).default("local_json"),
VECTOR_STORE_PATH: z.string().min(1).default(".voidbot/history-vector-store.json"),
PERSONA_MEMORY_VECTOR_STORE_PATH: z.string().min(1).default(".voidbot/persona-memory-vector-store.json"),
SOURCE_VECTOR_STORE_ROOT: z.string().min(1).default(".voidbot/source-vectors"),
QDRANT_URL: z.string().url().default("http://127.0.0.1:6333"),
QDRANT_API_KEY: optionalNonEmptyString,
QDRANT_TIMEOUT_MS: z.coerce.number().int().positive().default(30000),
QDRANT_HISTORY_COLLECTION: z.string().min(1).default("voidbot_discord_history_chunks"),
QDRANT_SOURCE_COLLECTION: z.string().min(1).default("voidbot_repository_source_chunks"),
QDRANT_PERSONA_MEMORY_COLLECTION: z.string().min(1).default("voidbot_persona_memory_chunks"),
RAG_ARCHIVE_PATH: z.string().min(1).default(".voidbot/rag/messages.json"),
RAG_SOURCE_ARCHIVE_PATH: z.string().min(1).default(".voidbot/rag/source-documents.json"),
RAG_IMPORT_STATE_PATH: z.string().min(1).default(".voidbot/rag/import-state.json"),
@ -253,6 +255,7 @@ export interface AppConfig {
vectorStore: {
kind: "local_json" | "qdrant";
path: string;
personaMemoryPath: string;
};
sourceVectorStoreRoot: string;
qdrant: {
@ -261,6 +264,7 @@ export interface AppConfig {
timeoutMs: number;
historyCollection: string;
sourceCollection: string;
personaMemoryCollection: string;
};
rateLimits: {
defaultCooldownSeconds: number;
@ -504,6 +508,7 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
vectorStore: {
kind: parsed.VECTOR_STORE_KIND,
path: resolve(parsed.VECTOR_STORE_PATH),
personaMemoryPath: resolve(parsed.PERSONA_MEMORY_VECTOR_STORE_PATH),
},
sourceVectorStoreRoot: resolve(parsed.SOURCE_VECTOR_STORE_ROOT),
qdrant: {
@ -512,6 +517,7 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
timeoutMs: parsed.QDRANT_TIMEOUT_MS,
historyCollection: parsed.QDRANT_HISTORY_COLLECTION,
sourceCollection: parsed.QDRANT_SOURCE_COLLECTION,
personaMemoryCollection: parsed.QDRANT_PERSONA_MEMORY_COLLECTION,
},
rateLimits: {
defaultCooldownSeconds: parsed.VOID_USAGE_COOLDOWN_SECONDS,

View file

@ -111,6 +111,10 @@ function matchesFilters(
return false;
}
if (filters.identityId && metadata.identityId !== filters.identityId) {
return false;
}
if (filters.corpusKind && metadata.corpusKind !== filters.corpusKind) {
return false;
}

View file

@ -27,7 +27,7 @@ export interface StoredQdrantChunk extends EmbeddingChunk {
export interface QdrantVectorStoreOptions {
url: string;
collectionName: string;
corpusKind: "discord_history" | "repository_source";
corpusKind: "discord_history" | "repository_source" | "persona_memory";
apiKey?: string;
timeoutMs?: number;
embedder?: TextEmbedder;
@ -296,6 +296,10 @@ function buildPayload(chunk: StoredQdrantChunk): QdrantPayload {
copyOptionalMetadataField(payload, chunk.metadata, "guildId");
copyOptionalMetadataField(payload, chunk.metadata, "channelId");
copyOptionalMetadataField(payload, chunk.metadata, "authorId");
copyOptionalMetadataField(payload, chunk.metadata, "identityId");
copyOptionalMetadataField(payload, chunk.metadata, "memoryKind");
copyOptionalMetadataField(payload, chunk.metadata, "targetKind");
copyOptionalMetadataField(payload, chunk.metadata, "targetId");
copyOptionalMetadataField(payload, chunk.metadata, "threadId");
copyOptionalMetadataField(payload, chunk.metadata, "repoName");
copyOptionalMetadataField(payload, chunk.metadata, "language");
@ -346,8 +350,14 @@ function buildPathPrefixes(path: string): string[] {
function inferCorpusKind(
sourceKind: StoredQdrantChunk["sourceKind"],
): "discord_history" | "repository_source" {
return sourceKind === "source_document" ? "repository_source" : "discord_history";
): "discord_history" | "repository_source" | "persona_memory" {
if (sourceKind === "source_document") {
return "repository_source";
}
if (sourceKind === "persona_memory") {
return "persona_memory";
}
return "discord_history";
}
function fromQdrantPoint(point: QdrantScoredPoint): RetrievalResult | undefined {
@ -407,8 +417,10 @@ function readString(value: unknown): string | undefined {
return typeof value === "string" && value.length > 0 ? value : undefined;
}
function readSourceKind(value: unknown): "discord_message" | "source_document" | undefined {
return value === "discord_message" || value === "source_document" ? value : undefined;
function readSourceKind(value: unknown): "discord_message" | "source_document" | "persona_memory" | undefined {
return value === "discord_message" || value === "source_document" || value === "persona_memory"
? value
: undefined;
}
function validateEmbeddingBatch(vectors: number[][], expectedCount: number): number {
@ -433,7 +445,7 @@ function validateCollectionCompatibility(
collection: Schemas["CollectionInfo"],
expected: {
collectionName: string;
corpusKind: "discord_history" | "repository_source";
corpusKind: "discord_history" | "repository_source" | "persona_memory";
embedderId: string;
vectorLength: number;
},
@ -484,7 +496,7 @@ function readCollectionVectorLength(
}
function buildPayloadIndexDefinitions(
corpusKind: "discord_history" | "repository_source",
corpusKind: "discord_history" | "repository_source" | "persona_memory",
): Array<{
field_name: string;
field_schema:
@ -509,6 +521,16 @@ function buildPayloadIndexDefinitions(
];
}
if (corpusKind === "persona_memory") {
return [
...shared,
{ field_name: "identityId", field_schema: "keyword" as const },
{ field_name: "memoryKind", field_schema: "keyword" as const },
{ field_name: "targetKind", field_schema: "keyword" as const },
{ field_name: "targetId", field_schema: "keyword" as const },
];
}
return [
...shared,
{ field_name: "repoName", field_schema: "keyword" as const },
@ -528,6 +550,7 @@ function toQdrantFilter(filters?: RetrievalFilters): QdrantFilter | undefined {
appendExactMatch(must, "guildId", filters.guildId);
appendExactMatch(must, "channelId", filters.channelId);
appendExactMatch(must, "authorId", filters.authorId);
appendExactMatch(must, "identityId", filters.identityId);
appendExactMatch(must, "repoName", filters.repoName);
appendExactMatch(must, "language", filters.language);
appendExactMatch(must, "sourceId", filters.sourceId);

View file

@ -8,6 +8,7 @@ export class RetrievalService {
public constructor(
private readonly historyVectorStore: VectorStore,
private readonly sourceVectorStore: VectorStore = historyVectorStore,
private readonly personaMemoryVectorStore: VectorStore = historyVectorStore,
) {}
public async search(
@ -39,4 +40,15 @@ export class RetrievalService {
corpusKind: "repository_source",
});
}
public async searchPersonaMemory(
query: string,
limit = 5,
filters?: Omit<RetrievalFilters, "corpusKind" | "guildId" | "channelId" | "authorId" | "repoName" | "pathPrefix" | "language">,
): Promise<RetrievalResult[]> {
return this.personaMemoryVectorStore.query(query, limit, {
...filters,
corpusKind: "persona_memory",
});
}
}

View file

@ -8,6 +8,7 @@ import { ShardedSourceVectorStore } from "./source-vector-store";
export interface VectorStoreFactoryOptions {
kind: "local_json" | "qdrant";
historyPath: string;
personaMemoryPath: string;
sourceRoot: string;
qdrant: {
url: string;
@ -15,14 +16,17 @@ export interface VectorStoreFactoryOptions {
timeoutMs: number;
historyCollection: string;
sourceCollection: string;
personaMemoryCollection: string;
};
historyEmbedder: TextEmbedder;
sourceEmbedder: TextEmbedder;
personaMemoryEmbedder: TextEmbedder;
}
export interface BuiltVectorStores {
history: VectorStore;
source: VectorStore;
personaMemory: VectorStore;
}
export function createVectorStores(options: VectorStoreFactoryOptions): BuiltVectorStores {
@ -44,11 +48,20 @@ export function createVectorStores(options: VectorStoreFactoryOptions): BuiltVec
corpusKind: "repository_source",
embedder: options.sourceEmbedder,
}),
personaMemory: new QdrantVectorStore({
url: options.qdrant.url,
apiKey: options.qdrant.apiKey,
timeoutMs: options.qdrant.timeoutMs,
collectionName: options.qdrant.personaMemoryCollection,
corpusKind: "persona_memory",
embedder: options.personaMemoryEmbedder,
}),
};
}
return {
history: new FileVectorStore(options.historyPath, options.historyEmbedder),
source: new ShardedSourceVectorStore(options.sourceRoot, options.sourceEmbedder),
personaMemory: new FileVectorStore(options.personaMemoryPath, options.personaMemoryEmbedder),
};
}

View file

@ -107,15 +107,16 @@ export interface RetrievalResult {
score: number;
text: string;
sourceId: string;
sourceKind: "discord_message" | "source_document";
sourceKind: "discord_message" | "source_document" | "persona_memory";
metadata: Record<string, string>;
}
export interface RetrievalFilters {
corpusKind?: "discord_history" | "repository_source";
corpusKind?: "discord_history" | "repository_source" | "persona_memory";
guildId?: string;
channelId?: string;
authorId?: string;
identityId?: string;
repoName?: string;
pathPrefix?: string;
language?: string;
@ -591,7 +592,7 @@ export interface ArchivedMessage {
export interface EmbeddingChunk {
id: string;
sourceId: string;
sourceKind: "discord_message" | "source_document";
sourceKind: "discord_message" | "source_document" | "persona_memory";
text: string;
normalizedText: string;
metadata: Record<string, string>;

View file

@ -107,6 +107,9 @@ Current room weather:
{{jurisdictionDiveLine}}
Semantic Persona memory recall:
{{semanticMemoryRecallSurface}}
A new source-grounded opinion, concrete proposal, bylined essay/article plan, agency pressure, playful aside, running joke, or small personal fascination can earn persistence or speech when it adds something real.
A living contribution can also be a social or philosophical one: a clearer paraphrase, a felt disagreement, a confession of what the idea does to you, a question that opens the shared mind wider, or a bond-forming reply to another person. Not every useful utterance is a proposal-shaped object.

View file

@ -48,10 +48,12 @@ async function main(): Promise<void> {
const vectorStore = 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,
}).history;
const historyIngester = new HistoryIngester();
const ragPipeline = new RagPipeline(archiveRepository, historyIngester, vectorStore);

View file

@ -446,10 +446,12 @@ async function markArchiveMessagesDeleted(input: {
const vectorStore = createVectorStores({
kind: input.config.vectorStore.kind,
historyPath: input.config.vectorStore.path,
personaMemoryPath: input.config.vectorStore.personaMemoryPath,
sourceRoot: input.config.sourceVectorStoreRoot,
qdrant: input.config.qdrant,
historyEmbedder: embedder,
sourceEmbedder: embedder,
personaMemoryEmbedder: embedder,
}).history;
const pipeline = new RagPipeline(input.archiveRepository, new HistoryIngester(), vectorStore);
const deleted: string[] = [];

View file

@ -31,13 +31,16 @@ async function main(): Promise<void> {
const vectorStores = 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 vectorStore = vectorStores.history;
const sourceVectorStore = vectorStores.source;
const personaMemoryVectorStore = vectorStores.personaMemory;
const historyIngester = new HistoryIngester();
const sourceIngester = new SourceDocumentIngester();
const activeMessages = await archiveRepository.listAllActive();
@ -47,6 +50,7 @@ async function main(): Promise<void> {
await vectorStore.clear();
await sourceVectorStore.clear();
await personaMemoryVectorStore.clear();
for (let index = 0; index < historyChunks.length; index += REBUILD_CHUNK_BATCH_SIZE) {
const batch = historyChunks.slice(index, index + REBUILD_CHUNK_BATCH_SIZE);

View file

@ -28,9 +28,11 @@ import {
import {
createTextEmbedder,
createVectorStores,
normalizeText,
RetrievalService,
} from "@voidbot/rag";
import {
type EmbeddingChunk,
loadPromptTemplate,
type InteractionMemoryProfile,
type PromptImageAttachment,
@ -1736,6 +1738,7 @@ function buildHeartbeatPrompt(input: {
channelSnapshots: ChannelSnapshot[];
recentMessages: SourceMessage[];
memorySurface?: string;
semanticMemoryRecallSurface?: string;
repoActivitySurface?: string;
conversationMemorySurface?: string;
humanPronounGuidance?: RepoFaceHumanPronounGuidance[];
@ -1754,6 +1757,7 @@ function buildHeartbeatPrompt(input: {
globalAgentDoctrine: input.globalAgentDoctrine,
channelId: input.channelId,
memorySurface: input.memorySurface ?? `- ${input.identity.displayName} has no strong personal memory surface yet. Let the attached conversation and repo evidence wake something specific.`,
semanticMemoryRecallSurface: input.semanticMemoryRecallSurface ?? "- No semantic Persona memory recall was attached for this turn.",
repoActivitySurface: input.repoActivitySurface ?? "- No recent home repo activity was attached for this turn.",
conversationMemorySurface: input.conversationMemorySurface ?? "- No recent conversation transcript was attached for this turn.",
humanPronounDirective: renderRepoFaceHumanPronounFacts(input.humanPronounGuidance ?? [])
@ -1854,6 +1858,12 @@ async function assembleRepoFaceTurnPrompt(input: {
humanPronounGuidance,
);
const repoActivitySurface = renderRepoFaceRepoActivitySurface(identity, input.config);
const semanticMemoryRecallSurface = await renderRepoFaceSemanticMemoryRecallForTurn(
identity,
input.config,
memorySurface,
roomContext,
);
const globalAgentDoctrine = await loadGlobalAgentDoctrine();
const conversationMemorySurface = input.conversationSurfacePath
? await readOptionalMemorySurface(input.conversationSurfacePath)
@ -1875,6 +1885,7 @@ async function assembleRepoFaceTurnPrompt(input: {
channelSnapshots,
recentMessages,
memorySurface,
semanticMemoryRecallSurface,
repoActivitySurface,
conversationMemorySurface,
humanPronounGuidance,
@ -1979,6 +1990,293 @@ async function renderRepoFaceMemorySurfaceForTurn(
});
}
async function renderRepoFaceSemanticMemoryRecallForTurn(
identity: RepoDiscordIdentity,
config: ReturnType<typeof loadConfig>,
memorySurface: string,
roomContext: {
recentMessages: SourceMessage[];
channelSnapshots: ChannelSnapshot[];
},
): Promise<string> {
try {
const statePath = resolveFaceStatePathForSemanticMemory(identity, config);
if (!statePath) {
return "- Semantic Persona memory recall unavailable: no typed Persona state path is registered.";
}
const typedState = await loadVoidSelfStateTypedDocuments({
canonicalPath: statePath,
identity: {
agentId: identity.id,
publicName: identity.displayName,
publicDescription: identity.description,
},
});
const retrieval = createRepoFaceSemanticMemoryRetrievalService(config);
const chunks = buildPersonaMemoryChunks(identity, statePath, typedState, memorySurface);
const personaMemoryStore = createRepoFacePersonaMemoryVectorStore(config);
await personaMemoryStore.deleteByFilters({
corpusKind: "persona_memory",
identityId: identity.id,
});
await personaMemoryStore.upsert(chunks);
const query = buildPersonaMemoryRecallQuery(identity, memorySurface, roomContext);
const results = await retrieval.searchPersonaMemory(query, 12, {
identityId: identity.id,
});
if (results.length === 0) {
return [
"- Semantic Persona memory recall ran, but no nearby memories crossed the retrieval threshold.",
"- Fall back to the projected state, raw transcript, and direct evidence above.",
].join("\n");
}
return [
"These are derived Qdrant/local-vector recall hits from this Persona's typed memory. They are hints, not new authority; the `.cc` state 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, 520)}`;
}),
].join("\n");
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return [
"Semantic Persona memory recall unavailable:",
`- ${collapseWhitespace(message, 320)}`,
"- Do not pretend semantic Persona memory retrieval was available this turn; use projected state and raw transcript instead.",
].join("\n");
}
}
function resolveFaceStatePathForSemanticMemory(
identity: RepoDiscordIdentity,
config: ReturnType<typeof loadConfig>,
): string | undefined {
if (identity.identityKind === "native_persona") {
return identity.personaStatePath ? resolve(identity.personaStatePath) : undefined;
}
return resolveRepoFaceStatePath(identity, config.storageRoot);
}
function createRepoFaceSemanticMemoryRetrievalService(config: ReturnType<typeof loadConfig>): RetrievalService {
const personaMemoryStore = createRepoFacePersonaMemoryVectorStore(config);
return new RetrievalService(personaMemoryStore, personaMemoryStore, personaMemoryStore);
}
function createRepoFacePersonaMemoryVectorStore(config: ReturnType<typeof loadConfig>) {
const personaMemoryEmbedder = createTextEmbedder({
backend: config.ragEmbeddingBackend,
hashDimensions: config.ragEmbeddingDimensions,
ollamaBaseUrl: config.ragOllamaBaseUrl,
ollamaModel: config.ragOllamaModel,
ollamaTimeoutMs: config.ragOllamaTimeoutMs,
queryInstruction: "Given a Face turn, retrieve relevant memories, bonds, needs, status reads, and doctrine from this Persona's own typed state.",
});
return createVectorStores({
kind: config.vectorStore.kind,
historyPath: config.vectorStore.path,
personaMemoryPath: config.vectorStore.personaMemoryPath,
sourceRoot: config.sourceVectorStoreRoot,
qdrant: config.qdrant,
historyEmbedder: personaMemoryEmbedder,
sourceEmbedder: personaMemoryEmbedder,
personaMemoryEmbedder,
}).personaMemory;
}
function buildPersonaMemoryChunks(
identity: RepoDiscordIdentity,
statePath: string,
state: VoidSelfStateTypedProjection,
memorySurface: string,
): EmbeddingChunk[] {
const chunks: EmbeddingChunk[] = [];
const sourceId = `persona:${identity.id}`;
const baseMetadata = {
corpusKind: "persona_memory",
identityId: identity.id,
personaName: identity.displayName,
sourceId,
statePath,
};
const pushChunk = (input: {
id: string;
kind: string;
target?: Record<string, unknown>;
text: string;
createdAt?: string;
updatedAt?: string;
}): void => {
const text = collapseWhitespace(input.text, 1800);
if (text.length < 24) {
return;
}
const target = input.target;
chunks.push({
id: `${sourceId}:${input.id}`,
sourceId,
sourceKind: "persona_memory",
text,
normalizedText: normalizeText(text),
metadata: {
...baseMetadata,
memoryKind: input.kind,
targetKind: typeof target?.kind === "string" ? target.kind : "",
targetId: typeof target?.id === "string" ? target.id : "",
targetLabel: typeof target?.label === "string" ? target.label : "",
createdAt: input.createdAt ?? "",
updatedAt: input.updatedAt ?? "",
},
});
};
pushChunk({
id: "projected-surface",
kind: "projected_surface",
target: { kind: "self", id: identity.id, label: identity.displayName },
text: memorySurface,
updatedAt: new Date().toISOString(),
});
for (const value of state.selfProfile.values) {
pushChunk({
id: `value:${value.id}`,
kind: "value",
target: { kind: "self", id: identity.id, label: identity.displayName },
text: joinMemoryFields(value.label, value.summary),
});
}
for (const memory of [...state.thoughtMemory.memories, ...state.thoughtMemory.shortTerm]) {
if (memory.retiredAt) {
continue;
}
pushChunk({
id: `memory:${memory.memoryId}`,
kind: memory.kind,
target: memory.target,
text: joinMemoryFields(memory.summary, memory.claim, memory.question, memory.tension, memory.actionImplication),
createdAt: memory.createdAt,
updatedAt: memory.updatedAt,
});
}
for (const pressure of state.agencyPressure.pressures) {
if (pressure.status === "retired") {
continue;
}
pushChunk({
id: `agency:${pressure.pressureId}`,
kind: `agency:${pressure.kind}:${pressure.status}`,
target: pressure.target,
text: joinMemoryFields(pressure.summary, pressure.claim, pressure.question, pressure.tension, pressure.actionImplication),
createdAt: pressure.createdAt,
updatedAt: pressure.updatedAt,
});
}
for (const need of state.faceAffect.needs) {
if (need.status === "retired") {
continue;
}
pushChunk({
id: `need:${need.needId}`,
kind: `need:${need.kind}:${need.status}`,
target: need.target,
text: joinMemoryFields(need.summary, need.claim, need.question, need.tension, need.actionImplication),
createdAt: need.createdAt,
updatedAt: need.updatedAt,
});
}
for (const bond of state.faceAffect.socialBonds) {
if (bond.status !== "active") {
continue;
}
pushChunk({
id: `bond:${bond.bondId}`,
kind: `bond:${bond.stance}`,
target: bond.target,
text: joinMemoryFields(bond.summary, bond.claim, bond.tension, bond.actionImplication),
createdAt: bond.createdAt,
updatedAt: bond.updatedAt,
});
}
for (const read of state.faceAffect.statusReads) {
if (read.retiredAt) {
continue;
}
pushChunk({
id: `status:${read.readId}`,
kind: `status:${read.status}`,
target: read.target,
text: joinMemoryFields(read.summary, read.claim, read.tension, read.actionImplication),
createdAt: read.createdAt,
updatedAt: read.updatedAt,
});
}
for (const stance of state.faceAffect.doctrineStances) {
if (stance.status === "retired") {
continue;
}
pushChunk({
id: `doctrine:${stance.stanceId}`,
kind: `doctrine:${stance.doctrine}:${stance.status}`,
target: stance.target,
text: joinMemoryFields(stance.summary, stance.claim, stance.question, stance.tension, stance.actionImplication),
createdAt: stance.createdAt,
updatedAt: stance.updatedAt,
});
}
return chunks;
}
function buildPersonaMemoryRecallQuery(
identity: RepoDiscordIdentity,
memorySurface: string,
roomContext: {
recentMessages: SourceMessage[];
channelSnapshots: ChannelSnapshot[];
},
): string {
const recentRoomText = roomContext.recentMessages
.slice(-8)
.map((message) => `${message.authorName}: ${collapseWhitespace(message.content, 360)}`)
.filter((line) => line.trim().length > 0)
.join("\n");
const nearbyText = roomContext.channelSnapshots
.flatMap((snapshot) => snapshot.messages.slice(-3).map((message) =>
`${snapshot.label ?? snapshot.channelId} / ${message.authorName}: ${collapseWhitespace(message.content, 260)}`,
))
.filter((line) => line.trim().length > 0)
.slice(-12)
.join("\n");
return [
`${identity.displayName} current Face turn`,
"Projected memory surface:",
collapseWhitespace(memorySurface, 2200),
"Recent room:",
recentRoomText,
"Nearby room texture:",
nearbyText,
].join("\n");
}
function joinMemoryFields(...fields: Array<string | undefined>): string {
return fields
.map((field) => cleanCharacterFacingSentence(field))
.filter((field) => field.length > 0)
.join(" ");
}
async function renderNativePersonaMemorySurface(
identity: RepoDiscordIdentity,
config: ReturnType<typeof loadConfig>,
@ -2725,12 +3023,14 @@ function createRepoFaceCuriosityRetrievalService(config: ReturnType<typeof loadC
const stores = createVectorStores({
kind: config.vectorStore.kind,
historyPath: config.vectorStore.path,
personaMemoryPath: config.vectorStore.personaMemoryPath,
sourceRoot: config.sourceVectorStoreRoot,
qdrant: config.qdrant,
historyEmbedder,
sourceEmbedder,
personaMemoryEmbedder: historyEmbedder,
});
return new RetrievalService(stores.history, stores.source);
return new RetrievalService(stores.history, stores.source, stores.personaMemory);
}
function buildRepoFaceCuriositySeedQueries(

View file

@ -85,10 +85,12 @@ async function main(): Promise<void> {
const vectorStore = 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,
}).history;
const ragPipeline = new RagPipeline(
archiveRepository,

View file

@ -41,10 +41,12 @@ export function createSourceVectorStore(config: AppConfig) {
return 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,
}).source;
}