Split Persona curiosity source and projection

This commit is contained in:
Meta Vens 2026-07-16 02:21:15 +01:00
commit 5149e35431
9 changed files with 293 additions and 551 deletions

View file

@ -25,7 +25,6 @@ import {
type InteractionMemoryProfile,
type PromptImageAttachment,
type RepoFaceConversationFocus,
type RetrievalResult,
type SourceMessage,
} from "@voidbot/shared";
import {
@ -63,6 +62,9 @@ import {
type PersonaMemoryRecallObservation,
} from "../apps/persona-scheduler/dist/persona-memory-context-source.js";
import { composePersonaMemoryPacket, projectPersonaMemorySurface, renderPersonaPressureSections, renderPersonaTypedStateSections } from "../apps/persona-scheduler/dist/persona-memory-projector.js";
import { readPersonaCuriosityEvidence } from "../apps/persona-scheduler/dist/persona-curiosity-context-source.js";
import { projectPersonaCuriosityContext } from "../apps/persona-scheduler/dist/persona-curiosity-projector.js";
import { significantPersonaTopicTerms } from "../apps/persona-scheduler/dist/persona-curiosity-terms.js";
import { observePersonaRoomTexture, projectPersonaSocialContext, renderPersonaHumanPronounFacts, renderPersonaRoomWeather, type PersonaHumanPronounGuidance as RepoFaceHumanPronounGuidance } from "../apps/persona-scheduler/dist/persona-social-context-projector.js";
import {
readDiscordActivitySnapshot,
@ -1183,27 +1185,6 @@ function targetLabel(target: { label?: string; id?: string; kind?: string } | un
return target.label ?? target.id ?? target.kind ?? "an unnamed target";
}
interface RepoFaceCuriosityNode {
id: string;
text: string;
sourceKind: RetrievalResult["sourceKind"];
score: number;
terms: string[];
metadata: Record<string, string>;
seedLabels: string[];
}
interface RepoFaceCuriosityCluster {
label: string;
nodes: RepoFaceCuriosityNode[];
prominence: number;
saturation: number;
novelty: number;
clusterDensity: number;
jurisdictionFit: number;
evidence: string[];
}
async function renderRepoFaceCuriosityGraphFacts(
identity: RepoDiscordIdentity,
config: ReturnType<typeof loadConfig>,
@ -1213,69 +1194,21 @@ async function renderRepoFaceCuriosityGraphFacts(
channelSnapshots: ChannelSnapshot[];
},
): Promise<string | undefined> {
const seedQueries = buildRepoFaceCuriositySeedQueries(identity, state, roomContext);
if (seedQueries.length === 0) {
return undefined;
}
try {
const retrieval = createRepoFaceCuriosityRetrievalService(config);
const nodesById = new Map<string, RepoFaceCuriosityNode>();
const sourceRepoName = getRepoFaceSourceRepoName(identity);
for (const seed of seedQueries) {
const [historyResults, sourceResults, homeSourceResults] = await Promise.all([
retrieval.searchHistory(seed.query, 8),
retrieval.searchRepositorySources(seed.query, 8),
sourceRepoName
? retrieval.searchRepositorySources(seed.query, 6, { repoName: sourceRepoName })
: Promise.resolve([]),
]);
for (const result of [...historyResults, ...sourceResults, ...homeSourceResults]) {
const id = `${result.sourceKind}:${result.chunkId}`;
const terms = significantTopicTerms(result.text);
if (terms.length < 3) {
continue;
}
const existing = nodesById.get(id);
if (existing) {
existing.score = Math.max(existing.score, result.score);
existing.seedLabels = mergeStrings(existing.seedLabels, seed.label);
continue;
}
nodesById.set(id, {
id,
text: result.text,
sourceKind: result.sourceKind,
score: result.score,
terms,
metadata: result.metadata,
seedLabels: [seed.label],
});
}
}
const nodes = [...nodesById.values()]
.sort((left, right) => right.score - left.score)
.slice(0, 32);
if (nodes.length < 3) {
return undefined;
}
const clusters = decodeRepoFaceCuriosityGraph(identity, state, roomContext, nodes);
if (clusters.length === 0) {
return undefined;
}
return renderRepoFaceCuriosityClusters(identity, config, clusters);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return [
"Curiosity graph unavailable:",
`- Semantic retrieval failed while decoding topic attractors: ${collapseWhitespace(message, 260)}`,
"- Do not pretend a semantic curiosity map was available this turn. Fall back to the raw transcript, home-repo activity, and typed memory instead of inventing ranked attractors.",
].join("\n");
}
const observation = await readPersonaCuriosityEvidence({
identity, state, ...roomContext,
sourceRepoName: getRepoFaceSourceRepoName(identity),
retrieval: () => {
const retrieval = createRepoFaceCuriosityRetrievalService(config);
return {
searchHistory: (query, limit) => retrieval.searchHistory(query, limit),
searchSources: (query, limit, repoName) => retrieval.searchRepositorySources(query, limit, repoName ? { repoName } : undefined),
};
},
});
const backendDescription = config.vectorStore.kind === "qdrant"
? `Qdrant collections ${config.qdrant.historyCollection} + ${config.qdrant.sourceCollection}`
: "local vector shards";
return projectPersonaCuriosityContext({ identity, state, ...roomContext, observation, backendDescription });
}
function createRepoFaceCuriosityRetrievalService(config: ReturnType<typeof loadConfig>): RetrievalService {
@ -1308,365 +1241,6 @@ function createRepoFaceCuriosityRetrievalService(config: ReturnType<typeof loadC
return new RetrievalService(stores.history, stores.source, stores.personaMemory);
}
function buildRepoFaceCuriositySeedQueries(
identity: RepoDiscordIdentity,
state: VoidSelfStateTypedProjection,
roomContext: {
recentMessages: SourceMessage[];
channelSnapshots: ChannelSnapshot[];
},
): Array<{ label: string; query: string }> {
const room = roomContext.recentMessages
.filter((message) => collapseWhitespace(message.content).length > 0)
.slice(-8)
.map((message) => `${message.authorName}: ${collapseWhitespace(message.content, 500)}`)
.join("\n");
const nearby = roomContext.channelSnapshots
.flatMap((snapshot) => snapshot.messages)
.filter((message) => collapseWhitespace(message.content).length > 0)
.slice(-8)
.map((message) => `${message.authorName}: ${collapseWhitespace(message.content, 320)}`)
.join("\n");
const privateThoughts = [
...state.selfProfile.privateNotes.slice(-8),
...state.selfProfile.values
.slice()
.sort((left, right) => right.priority - left.priority)
.slice(0, 8)
.map((value) => `${value.label}: ${value.summary}`),
...state.thoughtMemory.memories
.filter((memory) => !memory.retiredAt)
.slice(-8)
.map((memory) => `${memory.kind} ${targetLabel(memory.target)} ${memory.summary} ${memory.claim ?? memory.question ?? ""}`),
...state.thoughtMemory.shortTerm
.filter((memory) => !memory.retiredAt)
.slice(-8)
.map((memory) => `${memory.kind} ${targetLabel(memory.target)} ${memory.summary} ${memory.claim ?? memory.question ?? ""}`),
...state.thoughtMemory.incubation
.filter((thread) => thread.status !== "retired")
.sort((left, right) => right.maturation - left.maturation)
.slice(0, 8)
.map((thread) => `${thread.topic}: ${thread.summary}`),
].join("\n");
const identityQuery = [
identity.displayName,
identity.repoName,
identity.description ?? "",
...identity.channelPermissions.flatMap((permission) => [
permission.label ?? "",
permission.topic ?? "",
permission.posture ?? "",
]),
].join("\n");
return [
{ label: "current room", query: room },
{ label: "nearby rooms", query: nearby },
{ label: "private state", query: privateThoughts },
{ label: "home territory", query: identityQuery },
]
.map((seed) => ({ ...seed, query: collapseWhitespace(seed.query, 2800) }))
.filter((seed) => significantTopicTerms(seed.query).length >= 3)
.slice(0, 4);
}
function decodeRepoFaceCuriosityGraph(
identity: RepoDiscordIdentity,
state: VoidSelfStateTypedProjection,
roomContext: {
recentMessages: SourceMessage[];
channelSnapshots: ChannelSnapshot[];
},
nodes: RepoFaceCuriosityNode[],
): RepoFaceCuriosityCluster[] {
const edgeWeights = new Map<string, number>();
const adjacency = new Map<string, Set<string>>();
for (const node of nodes) {
adjacency.set(node.id, new Set());
}
for (let leftIndex = 0; leftIndex < nodes.length; leftIndex += 1) {
for (let rightIndex = leftIndex + 1; rightIndex < nodes.length; rightIndex += 1) {
const left = nodes[leftIndex];
const right = nodes[rightIndex];
const weight = repoFaceCuriosityEdgeWeight(left, right);
if (weight < 0.16) {
continue;
}
const key = curiosityEdgeKey(left.id, right.id);
edgeWeights.set(key, weight);
adjacency.get(left.id)?.add(right.id);
adjacency.get(right.id)?.add(left.id);
}
}
const visited = new Set<string>();
const clusters: RepoFaceCuriosityNode[][] = [];
const nodesById = new Map(nodes.map((node) => [node.id, node]));
for (const node of nodes) {
if (visited.has(node.id)) {
continue;
}
const stack = [node.id];
const cluster: RepoFaceCuriosityNode[] = [];
visited.add(node.id);
while (stack.length > 0) {
const id = stack.pop();
if (!id) {
continue;
}
const current = nodesById.get(id);
if (current) {
cluster.push(current);
}
for (const neighbor of adjacency.get(id) ?? []) {
if (!visited.has(neighbor)) {
visited.add(neighbor);
stack.push(neighbor);
}
}
}
clusters.push(cluster);
}
const recentTerms = collectRecentRoomTopicTermCounts(roomContext);
const stateTerms = collectRepoFaceStateTopicTermCounts(state);
const identityTerms = new Set(significantTopicTerms([
identity.id,
identity.displayName,
identity.repoName,
identity.description ?? "",
...identity.channelPermissions.flatMap((permission) => [
permission.label ?? "",
permission.topic ?? "",
permission.posture ?? "",
]),
].join(" ")));
return clusters
.filter((cluster) => cluster.length >= 2)
.map((cluster): RepoFaceCuriosityCluster => {
const clusterTerms = rankedClusterTerms(cluster).slice(0, 7);
const density = clusterDensity(cluster, edgeWeights);
const averageScore = average(cluster.map((node) => node.score));
const recentOverlap = weightedTermOverlap(clusterTerms, recentTerms);
const stateOverlap = weightedTermOverlap(clusterTerms, stateTerms);
const saturation = clamp((recentOverlap * 0.72) + (stateOverlap * 0.42), 0, 1);
const jurisdictionFit = clamp(
clusterTerms.filter((term) => identityTerms.has(term)).length / Math.max(1, Math.min(clusterTerms.length, 4))
+ cluster.filter((node) => normalizeKey(node.metadata.repoName ?? "") === normalizeKey(identity.repoName)).length / Math.max(1, cluster.length) * 0.55,
0,
1,
);
const novelty = clamp(1 - saturation + Math.max(0, 0.4 - stateOverlap), 0, 1);
const prominence = clamp((averageScore * 0.58) + (density * 0.28) + (Math.min(cluster.length, 8) / 8 * 0.18), 0, 1);
return {
label: clusterTerms.slice(0, 4).join(" / "),
nodes: cluster,
prominence,
saturation,
novelty,
clusterDensity: density,
jurisdictionFit,
evidence: cluster
.slice()
.sort((left, right) => right.score - left.score)
.slice(0, 3)
.map((node) => curiosityEvidenceLabel(node)),
};
})
.sort((left, right) => {
const leftRank = curiosityAttractorRank(left);
const rightRank = curiosityAttractorRank(right);
if (rightRank !== leftRank) {
return rightRank - leftRank;
}
return right.prominence - left.prominence;
})
.slice(0, 5);
}
function renderRepoFaceCuriosityClusters(
identity: RepoDiscordIdentity,
config: ReturnType<typeof loadConfig>,
clusters: RepoFaceCuriosityCluster[],
): string {
const backend = config.vectorStore.kind === "qdrant"
? `Qdrant collections ${config.qdrant.historyCollection} + ${config.qdrant.sourceCollection}`
: "local vector shards";
return [
"Curiosity graph attractors:",
`- Source: semantic retrieval from ${backend}; local graph decoding over retrieved chunks. This is curiosity weather, not orders or consensus.`,
...clusters.map((cluster) => {
const suggestedMotion = suggestCuriosityMotion(identity, cluster);
return [
`- ${cluster.label || "unnamed cluster"}: prominence ${formatSignal(cluster.prominence)}, saturation ${formatSignal(cluster.saturation)}, novelty ${formatSignal(cluster.novelty)}, cluster density ${formatSignal(cluster.clusterDensity)}, jurisdiction fit ${formatSignal(cluster.jurisdictionFit)}.`,
` Suggested motion: ${suggestedMotion}`,
` Evidence: ${cluster.evidence.join("; ")}.`,
].join("\n");
}),
"- The living use is intelligent curiosity: follow underexplored neighboring clusters, deepen home-territory clusters only with fresh anchors, and let saturated clusters create boredom, fatigue, or a deliberate closing move instead of another repetition.",
].join("\n");
}
function repoFaceCuriosityEdgeWeight(left: RepoFaceCuriosityNode, right: RepoFaceCuriosityNode): number {
const termSimilarity = jaccard(left.terms, right.terms);
const sameRepo = left.metadata.repoName && right.metadata.repoName && normalizeKey(left.metadata.repoName) === normalizeKey(right.metadata.repoName)
? 1
: 0;
const sameCorpus = left.sourceKind === right.sourceKind ? 1 : 0;
const sharedSeeds = left.seedLabels.filter((label) => right.seedLabels.includes(label)).length > 0 ? 1 : 0;
const scoreProximity = 1 - Math.min(1, Math.abs(left.score - right.score));
return clamp((termSimilarity * 0.58) + (sameRepo * 0.16) + (sameCorpus * 0.1) + (sharedSeeds * 0.08) + (scoreProximity * 0.08), 0, 1);
}
function rankedClusterTerms(cluster: RepoFaceCuriosityNode[]): string[] {
const counts = new Map<string, number>();
for (const node of cluster) {
for (const term of node.terms) {
counts.set(term, (counts.get(term) ?? 0) + 1);
}
}
return [...counts.entries()]
.sort((left, right) => {
if (right[1] !== left[1]) {
return right[1] - left[1];
}
return left[0].localeCompare(right[0]);
})
.map(([term]) => term);
}
function collectRecentRoomTopicTermCounts(input: {
recentMessages: SourceMessage[];
channelSnapshots: ChannelSnapshot[];
}): Map<string, number> {
const counts = new Map<string, number>();
const messages = [
...input.recentMessages,
...input.channelSnapshots.flatMap((snapshot) => snapshot.messages),
].slice(-36);
for (const message of messages) {
for (const term of significantTopicTerms(message.content)) {
counts.set(term, (counts.get(term) ?? 0) + 1);
}
}
return counts;
}
function collectRepoFaceStateTopicTermCounts(state: VoidSelfStateTypedProjection): Map<string, number> {
const counts = new Map<string, number>();
const surfaces = [
...state.selfProfile.privateNotes,
...state.selfProfile.values.map((value) => `${value.label} ${value.summary}`),
...state.thoughtMemory.memories.map((memory) => `${memory.summary} ${memory.claim ?? ""} ${memory.question ?? ""} ${memory.tension ?? ""}`),
...state.thoughtMemory.shortTerm.map((memory) => `${memory.summary} ${memory.claim ?? ""} ${memory.question ?? ""} ${memory.tension ?? ""}`),
...state.thoughtMemory.incubation.map((thread) => `${thread.topic} ${thread.summary}`),
...state.agencyPressure.pressures.map((pressure) => `${pressure.summary} ${pressure.claim ?? ""} ${pressure.question ?? ""} ${pressure.tension ?? ""}`),
].slice(-64);
for (const surface of surfaces) {
for (const term of significantTopicTerms(surface)) {
counts.set(term, (counts.get(term) ?? 0) + 1);
}
}
return counts;
}
function weightedTermOverlap(terms: string[], counts: Map<string, number>): number {
if (terms.length === 0 || counts.size === 0) {
return 0;
}
const maxCount = Math.max(...counts.values(), 1);
const overlap = terms.reduce((sum, term) => sum + ((counts.get(term) ?? 0) / maxCount), 0);
return clamp(overlap / Math.min(terms.length, 6), 0, 1);
}
function clusterDensity(cluster: RepoFaceCuriosityNode[], edgeWeights: Map<string, number>): number {
if (cluster.length < 2) {
return 0;
}
let sum = 0;
let pairs = 0;
for (let leftIndex = 0; leftIndex < cluster.length; leftIndex += 1) {
for (let rightIndex = leftIndex + 1; rightIndex < cluster.length; rightIndex += 1) {
sum += edgeWeights.get(curiosityEdgeKey(cluster[leftIndex].id, cluster[rightIndex].id)) ?? 0;
pairs += 1;
}
}
return pairs > 0 ? clamp(sum / pairs, 0, 1) : 0;
}
function curiosityAttractorRank(cluster: RepoFaceCuriosityCluster): number {
return clamp(
cluster.prominence * 0.42
+ cluster.novelty * 0.28
+ cluster.clusterDensity * 0.16
+ cluster.jurisdictionFit * 0.18
- cluster.saturation * 0.2,
0,
1,
);
}
function suggestCuriosityMotion(identity: RepoDiscordIdentity, cluster: RepoFaceCuriosityCluster): string {
if (cluster.saturation >= 0.68 && cluster.novelty <= 0.42) {
return `treat this as over-chewed; ${identity.displayName} should close, defer, or pivot to a neighboring question unless a new concrete anchor appears.`;
}
if (cluster.jurisdictionFit >= 0.55 && cluster.novelty >= 0.45) {
return `this is home-territory curiosity with room to grow; read, ask, draft, or make a fresh anchored distinction.`;
}
if (cluster.novelty >= 0.62) {
return `this is an underexplored neighboring trail; curiosity may pull ${identity.displayName} sideways instead of repeating the room's dominant topic.`;
}
if (cluster.jurisdictionFit < 0.3) {
return `this likely belongs to another steward; use it as social weather, consultation, or rivalry pressure rather than absorbing the work.`;
}
return "stay interested only if the turn adds a new anchor, concrete question, or relationship move.";
}
function curiosityEvidenceLabel(node: RepoFaceCuriosityNode): string {
const source = node.sourceKind === "source_document"
? [node.metadata.repoName, node.metadata.path].filter(Boolean).join(":") || node.sourceId
: [node.metadata.channelId ? `channel ${node.metadata.channelId}` : undefined, node.sourceId].filter(Boolean).join(":") || node.sourceId;
return `${source} (${node.score.toFixed(2)}, ${node.seedLabels.join("/")})`;
}
function curiosityEdgeKey(left: string, right: string): string {
return left < right ? `${left}::${right}` : `${right}::${left}`;
}
function jaccard(left: string[], right: string[]): number {
const leftSet = new Set(left);
const rightSet = new Set(right);
const union = new Set([...leftSet, ...rightSet]).size;
if (union === 0) {
return 0;
}
let intersection = 0;
for (const term of leftSet) {
if (rightSet.has(term)) {
intersection += 1;
}
}
return intersection / union;
}
function average(values: number[]): number {
return values.length > 0
? values.reduce((sum, value) => sum + value, 0) / values.length
: 0;
}
function formatSignal(value: number): string {
if (value >= 0.68) {
return `high ${value.toFixed(2)}`;
}
if (value >= 0.38) {
return `medium ${value.toFixed(2)}`;
}
return `low ${value.toFixed(2)}`;
}
async function loadRepoFaceHumanPronounGuidance(
config: ReturnType<typeof loadConfig>,
roomContext?: {
@ -2446,7 +2020,7 @@ function estimateTopicRelationToIdentity(
identity: RepoDiscordIdentity,
signal: RoomTopicSaturationSignal,
): TopicRelationToIdentity {
const identityTerms = new Set(significantTopicTerms([
const identityTerms = new Set(significantPersonaTopicTerms([
identity.id,
identity.displayName,
identity.repoName,
@ -2476,7 +2050,7 @@ function detectRoomTopicSaturation(messages: SourceMessage[]): RoomTopicSaturati
}
const termCounts = new Map<string, number>();
const messageTerms = recent.map((message) => new Set(significantTopicTerms(message.content)));
const messageTerms = recent.map((message) => new Set(significantPersonaTopicTerms(message.content)));
for (const terms of messageTerms) {
for (const term of terms) {
termCounts.set(term, (termCounts.get(term) ?? 0) + 1);
@ -2517,106 +2091,6 @@ function detectRoomTopicSaturation(messages: SourceMessage[]): RoomTopicSaturati
};
}
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",
"what",
"when",
"where",
"which",
"while",
"with",
"work",
"would",
"write",
"you",
"your",
"first",
"right",
"rights",
]);
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) {
@ -2793,10 +2267,6 @@ function projectCharacterDescription(description: string | undefined): string |
.join(" ");
}
function mergeStrings(values: string[], value: string): string[] {
return Array.from(new Set([...values, value]));
}
function normalizeKey(value: string): string {
return value.trim().toLowerCase();
}

View file

@ -66,6 +66,8 @@ import { readRepoActivity } from "../apps/persona-scheduler/dist/repo-activity-s
import { readPersonaStateObservation } from "../apps/persona-scheduler/dist/persona-state-source.js";
import { readPersonaMemoryRecall } from "../apps/persona-scheduler/dist/persona-memory-context-source.js";
import { composePersonaMemoryPacket, projectPersonaMemorySurface, renderPersonaPressureSections, renderPersonaSelfMaintenancePressure, renderPersonaTypedStateSections } from "../apps/persona-scheduler/dist/persona-memory-projector.js";
import { readPersonaCuriosityEvidence } from "../apps/persona-scheduler/dist/persona-curiosity-context-source.js";
import { projectPersonaCuriosityContext } from "../apps/persona-scheduler/dist/persona-curiosity-projector.js";
import { observePersonaRoomTexture, projectPersonaSocialContext, renderPersonaHumanClarityPressure, renderPersonaHumanPronounFacts, renderPersonaRoomWeather, renderPersonaSocialGraph } from "../apps/persona-scheduler/dist/persona-social-context-projector.js";
function participant(identityId: string, nextTurnAt: number): InitiativeParticipant {
@ -547,6 +549,37 @@ try {
} : undefined;
assert.match(maintenanceState ? renderPersonaSelfMaintenancePressure({ identityName: "Nibu", state: maintenanceState, clarityPressureActive: false }) ?? "" : "", /Self-maintenance pressure requiring public advocacy/, "the memory projector owns self-maintenance classification and rendering");
assert.equal(maintenanceState ? renderPersonaSelfMaintenancePressure({ identityName: "Nibu", state: maintenanceState, clarityPressureActive: true }) : undefined, undefined, "live human clarity pressure suppresses self-maintenance advocacy inside the owning projector");
const curiosityObservation = populatedObservation.status === "ok" ? await readPersonaCuriosityEvidence({
identity: routedIdentity,
state: populatedObservation.typedState,
recentMessages: [{ id: "curiosity-room", authorId: "human", authorName: "Operator", content: "memory topology needs inspectable semantic anchors", timestamp: "2026-07-15T21:00:00.000Z", isBot: false, channelId: "aquarium" }],
channelSnapshots: [],
sourceRepoName: "AetheriaLore",
retrieval: {
searchHistory: async () => [{ chunkId: "history-1", sourceId: "message-1", sourceKind: "discord_message", text: "semantic memory topology and inspectable anchors", score: 0.84, metadata: { channelId: "aquarium" } }],
searchSources: async (_query, _limit, repoName) => repoName
? [{ chunkId: "source-home", sourceId: "doc-home", sourceKind: "source_document", text: "semantic memory topology preserves inspectable provenance anchors", score: 0.82, metadata: { repoName, path: "memory.md" } }]
: [{ chunkId: "source-global", sourceId: "doc-global", sourceKind: "source_document", text: "inspectable semantic topology joins memory evidence anchors", score: 0.8, metadata: { repoName: "VoidBot", path: "map.md" } }],
},
}) : undefined;
assert.equal(curiosityObservation?.status, "ok", "the curiosity source plans queries and acquires bounded neutral evidence through an injected retrieval port");
const unavailableCuriosity = populatedObservation.status === "ok" ? await readPersonaCuriosityEvidence({
identity: routedIdentity,
state: populatedObservation.typedState,
recentMessages: [{ id: "curiosity-failure", authorId: "human", authorName: "Operator", content: "semantic memory topology needs inspectable anchors", timestamp: "2026-07-15T21:00:00.000Z", isBot: false, channelId: "aquarium" }],
channelSnapshots: [],
retrieval: () => { throw new Error("vector adapter unavailable"); },
}) : undefined;
assert.deepEqual(unavailableCuriosity, { status: "unavailable", reason: "vector adapter unavailable" }, "retrieval-adapter construction fails inside the curiosity source's explicit unavailable witness");
const curiosityProjection = populatedObservation.status === "ok" && curiosityObservation ? projectPersonaCuriosityContext({
identity: routedIdentity,
state: populatedObservation.typedState,
recentMessages: [{ id: "curiosity-room", authorId: "human", authorName: "Operator", content: "memory topology needs inspectable semantic anchors", timestamp: "2026-07-15T21:00:00.000Z", isBot: false, channelId: "aquarium" }],
channelSnapshots: [],
observation: curiosityObservation,
backendDescription: "injected smoke retrieval",
}) : undefined;
assert.match(curiosityProjection ?? "", /Curiosity graph attractors:[\s\S]*injected smoke retrieval/, "the pure curiosity projector decodes and renders supplied evidence without acquiring it");
const pressureSections = populatedObservation.status === "ok"
? renderPersonaPressureSections({ identityName: "Nibu", state: populatedObservation.typedState, clarityPressureActive: true })
: [];