Extract Persona memory turn coordinator

This commit is contained in:
Meta Vens 2026-07-16 04:09:44 +01:00
commit 489d06525c
7 changed files with 203 additions and 182 deletions

View file

@ -0,0 +1,26 @@
import type { PersonaMemoryRecallObservation } from "./persona-memory-context-source.js";
export function projectPersonaMemoryRecall(observation: PersonaMemoryRecallObservation): string {
if (observation.status === "unavailable") return [
"Semantic Persona memory recall unavailable:",
`- ${collapse(observation.reason, 320)}`,
"- Do not pretend semantic Persona memory retrieval was available this turn; use projected state and raw transcript instead.",
].join("\n");
if (observation.hits.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.",
...observation.hits.map((hit, index) => {
const kind = hit.memoryKind ? `/${hit.memoryKind}` : "";
const target = hit.targetLabel ?? hit.targetId ?? "unknown target";
return `- ${index + 1}. ${target}${kind} score=${hit.score.toFixed(3)}: ${collapse(hit.text, 520)}`;
}),
].join("\n");
}
function collapse(value: string, maxLength: number): string {
const normalized = value.replace(/\s+/g, " ").trim();
return normalized.length <= maxLength ? normalized : `${normalized.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
}

View file

@ -0,0 +1,127 @@
import type { loadConfig } from "@voidbot/config";
import { getRepoFaceSourceRepoName, type RepoDiscordIdentity } from "@voidbot/core";
import { createTextEmbedder, createVectorStores, RetrievalService } from "@voidbot/rag";
import type { SourceMessage } from "@voidbot/shared";
import { readPersonaCuriosityEvidence } from "./persona-curiosity-context-source.js";
import { projectPersonaCuriosityContext } from "./persona-curiosity-projector.js";
import { readPersonaMemoryRecall } from "./persona-memory-context-source.js";
import { projectPersonaMemorySurface } from "./persona-memory-projector.js";
import { projectPersonaMemoryRecall } from "./persona-memory-recall-projector.js";
import type { PersonaHumanPronounGuidance } from "./persona-social-context-projector.js";
import { projectPersonaStatePacket } from "./persona-state-packet-projector.js";
import { readPersonaStateObservation, type PersonaStateObservation } from "./persona-state-source.js";
import { projectGamecultPersonaState } from "./persona-standard-state-projector.js";
import { projectPersonaText } from "./persona-text-projection-actuator.js";
import { renderPersonaIdentityDoctrine } from "./persona-turn-prompt-projector.js";
import type { ChannelSnapshot } from "./turn-context-source.js";
export interface PersonaMemoryTurnContext {
stateObservation?: PersonaStateObservation;
memorySurface: string;
semanticMemoryRecallSurface: string;
}
interface PersonaMemoryTurnDependencies {
readState?: typeof readPersonaStateObservation;
readRecall?: typeof readPersonaMemoryRecall;
projectMemory?: typeof projectPersonaMemorySurface;
projectCuriosityFacts?: typeof projectCuriosity;
}
export async function coordinatePersonaMemoryTurn(input: {
identity: RepoDiscordIdentity;
config: ReturnType<typeof loadConfig>;
registryIdentities?: RepoDiscordIdentity[];
recentMessages: SourceMessage[];
channelSnapshots: ChannelSnapshot[];
humanPronounGuidance?: PersonaHumanPronounGuidance[];
stateObservation?: PersonaStateObservation;
projectedMemoryOverride?: string;
observedAt?: Date;
}, dependencies: PersonaMemoryTurnDependencies = {}): Promise<PersonaMemoryTurnContext> {
const observation = input.stateObservation ?? await (dependencies.readState ?? readPersonaStateObservation)({ identity: input.identity, storageRoot: input.config.storageRoot });
const observedAt = input.observedAt ?? new Date();
if (input.identity.identityKind === "native_persona" && !input.identity.personaStatePath) {
const memorySurface = `${input.identity.displayName} is a native VoidBot Persona, not a repo Face.\nNo Persona state path is registered. Treat that as a Body fault and keep the public turn modest.`;
return { stateObservation: observation, memorySurface, semanticMemoryRecallSurface: projectPersonaMemoryRecall({ status: "unavailable", reason: "No Persona state path is registered." }) };
}
if (observation.status !== "ok") throw new Error(`${input.identity.displayName} Persona state ${observation.status}: ${observation.reason}`);
let memorySurface: string;
if (input.projectedMemoryOverride) memorySurface = input.projectedMemoryOverride;
else if (observation.stateKind === "gamecult_persona") memorySurface = projectGamecultPersonaState(input.identity, observation.personaState);
else if (observation.stateKind === "persona_projection_import") memorySurface = projectGamecultPersonaState(input.identity, observation.projectionImport.payload);
else {
const roomContext = { recentMessages: input.recentMessages, channelSnapshots: input.channelSnapshots };
const curiosityGraphFacts = input.identity.identityKind !== "native_persona"
? await (dependencies.projectCuriosityFacts ?? projectCuriosity)({ identity: input.identity, config: input.config, state: observation.typedState, ...roomContext })
: undefined;
const statePacket = projectPersonaStatePacket({
identity: input.identity,
state: observation.typedState,
registryIdentities: input.registryIdentities ?? [],
roomContext,
humanPronounGuidance: input.humanPronounGuidance,
curiosityGraphFacts,
observedAt,
});
memorySurface = await (dependencies.projectMemory ?? projectPersonaMemorySurface)({
identityId: input.identity.id,
characterIdentity: renderPersonaIdentityDoctrine(input.identity),
statePacket,
modelProjectionEnabled: input.config.repoFaceHeartbeats.stateProjectorEnabled,
projectText: input.config.repoFaceHeartbeats.stateProjectorEnabled ? (prompt) => projectPersonaText({
prompt,
config: input.config,
command: "repo-face-state-projector",
jobId: `state-projector:${input.identity.id}:${observedAt.getTime()}`,
timeoutMs: 180_000,
}) : undefined,
});
}
const recall = await (dependencies.readRecall ?? readPersonaMemoryRecall)({
identity: input.identity,
config: input.config,
state: observation,
projectedMemory: memorySurface,
recentMessages: input.recentMessages,
channelSnapshots: input.channelSnapshots,
observedAt,
});
return { stateObservation: observation, memorySurface, semanticMemoryRecallSurface: projectPersonaMemoryRecall(recall) };
}
async function projectCuriosity(input: {
identity: RepoDiscordIdentity;
config: ReturnType<typeof loadConfig>;
state: Extract<PersonaStateObservation, { status: "ok"; stateKind: "void_self_state" }>["typedState"];
recentMessages: SourceMessage[];
channelSnapshots: ChannelSnapshot[];
}): Promise<string | undefined> {
const observation = await readPersonaCuriosityEvidence({
identity: input.identity,
state: input.state,
recentMessages: input.recentMessages,
channelSnapshots: input.channelSnapshots,
sourceRepoName: getRepoFaceSourceRepoName(input.identity),
retrieval: () => {
const retrieval = createCuriosityRetrieval(input.config);
return {
searchHistory: (query, limit) => retrieval.searchHistory(query, limit),
searchSources: (query, limit, repoName) => retrieval.searchRepositorySources(query, limit, repoName ? { repoName } : undefined),
};
},
});
const backendDescription = input.config.vectorStore.kind === "qdrant"
? `Qdrant collections ${input.config.qdrant.historyCollection} + ${input.config.qdrant.sourceCollection}`
: "local vector shards";
return projectPersonaCuriosityContext({ ...input, observation, backendDescription });
}
function createCuriosityRetrieval(config: ReturnType<typeof loadConfig>): RetrievalService {
const historyEmbedder = createTextEmbedder({ backend: config.ragEmbeddingBackend, hashDimensions: config.ragEmbeddingDimensions, ollamaBaseUrl: config.ragOllamaBaseUrl, ollamaModel: config.ragOllamaModel, ollamaTimeoutMs: config.ragOllamaTimeoutMs, queryInstruction: config.ragQueryInstruction });
const sourceEmbedder = createTextEmbedder({ backend: config.ragEmbeddingBackend, hashDimensions: config.ragEmbeddingDimensions, ollamaBaseUrl: config.ragOllamaBaseUrl, ollamaModel: config.ragOllamaModel, ollamaTimeoutMs: config.ragOllamaTimeoutMs, queryInstruction: config.ragSourceQueryInstruction || config.ragQueryInstruction });
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, stores.personaMemory);
}

View file

@ -215,6 +215,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
- Final Persona turn composition now crosses `persona-turn-assembler.ts` for both queued turns and `--assemble-prompt` inspection. It owns conversation projection, jurisdiction-dive cadence, final prompt composition, and the deduplicated eight-image evidence budget. The shell acquires facts and the turn actuator queues the assembled result; neither may maintain a parallel opinion about the final prompt or evidence bundle.
- Built-in Void CTB launch now crosses `void-moderation-turn-actuator.ts`. It alone writes the pending-mention handoff, quotes and launches the hidden PowerShell runner, polls lock/status witnesses, applies the handshake timeout, and returns the scheduler-facing receipt. The Persona shell decides only which participant kind to dispatch; it does not own platform process mechanics.
- Global Codex doctrine discovery now crosses `global-agent-doctrine-source.ts`, which reports the first readable `AGENTS.md` authority or a typed unavailable observation with attempted paths and errors. `persona-turn-assembler.ts` alone lowers that observation into prompt text, so source failure cannot silently become invented doctrine and the shell no longer crawls user-home paths.
- Persona Mind assembly now crosses `persona-memory-turn-coordinator.ts`. It sequences the existing typed state source, curiosity source/projector, state-packet projector, optional text-projection actuator, semantic memory source, and `persona-memory-recall-projector.ts` through narrow injected ports. Queued live turns and `--assemble-prompt` inspection now receive the same projected Mind and semantic-recall pair; the old shell accidentally omitted semantic recall from live turns. The coordinator does not select initiative, queue work, or mutate canonical Persona state.
- Direct Face `UPDATE REQUEST` and `BIFROST TOPIC` packets are ignored while `REPO_FACE_BIFROST_ENABLED=false`. Do not debug missing Bifrost jobs as a live failure unless that flag has been deliberately enabled again.
- Live model-facing prompt prose has started moving out of executable code. The shared loader is `packages/shared/src/prompt-template.ts`; extracted prompt surfaces live under `prompts/`, including repo Face turn/reviewer prompts, provider wrapper prompts, situational social read prompts, source-grounding reminders, consensus-feed prompts, and repo Face model-smoke scenarios. Code should project data and select prompt names; prompt files should own behavioral prose.
- Identity collapse has started. Active bot, worker, MCP repo-identity tools, and Face CTB scheduler paths now load through the canonical Face registry and treat repo Discord identities as compatibility projections. Next bounded move is to finish removing remaining direct conceptual dependence on repo identities, then split the Face turn loop into scheduler, context assembler, prompt renderer, candidate parser, policy gate, and transport adapter.

View file

@ -6,16 +6,13 @@ import { dirname, resolve } from "node:path";
import { loadConfig } from "@voidbot/config";
import {
createStateStorage,
getRepoFaceSourceRepoName,
getRepoDiscordIdentityAllowedChannelIds,
faceRegistryAsRepoDiscordRegistry,
loadFaceIdentityRegistry,
resolveRepoFaceStatePath,
type RepoFacePendingMention,
type RepoDiscordIdentity,
type VoidSelfStateTypedProjection,
} from "@voidbot/core";
import { createTextEmbedder, createVectorStores, RetrievalService } from "@voidbot/rag";
import {
type SourceMessage,
} from "@voidbot/shared";
@ -49,19 +46,10 @@ import {
readPersonaStateObservations,
type PersonaStateObservation,
} from "../apps/persona-scheduler/dist/persona-state-source.js";
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 { readPersonaCuriosityEvidence } from "../apps/persona-scheduler/dist/persona-curiosity-context-source.js";
import { projectPersonaCuriosityContext } from "../apps/persona-scheduler/dist/persona-curiosity-projector.js";
import { renderPersonaIdentityDoctrine } from "../apps/persona-scheduler/dist/persona-turn-prompt-projector.js";
import { projectGamecultPersonaState, projectNativePersonaBody } from "../apps/persona-scheduler/dist/persona-standard-state-projector.js";
import { projectNativePersonaBody } from "../apps/persona-scheduler/dist/persona-standard-state-projector.js";
import type { PersonaHumanPronounGuidance as RepoFaceHumanPronounGuidance } from "../apps/persona-scheduler/dist/persona-social-context-projector.js";
import { projectPersonaStatePacket } from "../apps/persona-scheduler/dist/persona-state-packet-projector.js";
import { projectPersonaText } from "../apps/persona-scheduler/dist/persona-text-projection-actuator.js";
import { assemblePersonaTurn } from "../apps/persona-scheduler/dist/persona-turn-assembler.js";
import { coordinatePersonaMemoryTurn } from "../apps/persona-scheduler/dist/persona-memory-turn-coordinator.js";
import { readGlobalAgentDoctrine } from "../apps/persona-scheduler/dist/global-agent-doctrine-source.js";
import { readPersonaHumanPronounGuidance } from "../apps/persona-scheduler/dist/persona-social-context-source.js";
import {
@ -420,14 +408,14 @@ async function queueRepoFaceTurn(input: {
channelSnapshots,
};
const humanPronounGuidance = await loadRepoFaceHumanPronounGuidance(input.config, roomContext);
const memorySurface = await renderRepoFaceMemorySurfaceForTurn(
const memoryContext = await coordinatePersonaMemoryTurn({
identity,
input.config,
input.registryIdentities,
roomContext,
config: input.config,
registryIdentities: input.registryIdentities,
...roomContext,
humanPronounGuidance,
input.personaStateObservation,
);
stateObservation: input.personaStateObservation,
});
const repoActivitySurface = identity.identityKind === "native_persona"
? projectNativePersonaBody(identity)
: renderRepoActivityObservation(readRepoActivity({ identity, storageRoot: input.config.storageRoot }));
@ -440,7 +428,8 @@ async function queueRepoFaceTurn(input: {
channelSnapshots,
participant: input.participant,
pendingMentions: input.pendingMentions,
memorySurface,
memorySurface: memoryContext.memorySurface,
semanticMemoryRecallSurface: memoryContext.semanticMemoryRecallSurface,
repoActivitySurface,
humanPronounGuidance,
bifrostDigest,
@ -547,27 +536,21 @@ async function assembleRepoFaceTurnPrompt(input: {
};
const humanPronounGuidance = await loadRepoFaceHumanPronounGuidance(input.config, roomContext);
const personaStateObservation = await readPersonaStateObservation({ identity, storageRoot: input.config.storageRoot });
const memorySurface = input.memorySurfacePath
const projectedMemoryOverride = input.memorySurfacePath
? await readOptionalMemorySurface(input.memorySurfacePath)
: await renderRepoFaceMemorySurfaceForTurn(
identity,
input.config,
registry.identities,
roomContext,
humanPronounGuidance,
personaStateObservation,
);
: undefined;
const memoryContext = await coordinatePersonaMemoryTurn({
identity,
config: input.config,
registryIdentities: registry.identities,
...roomContext,
humanPronounGuidance,
stateObservation: personaStateObservation,
projectedMemoryOverride,
});
const repoActivitySurface = renderRepoActivityObservation(
readRepoActivity({ identity, storageRoot: input.config.storageRoot }),
);
const semanticMemoryRecallSurface = renderPersonaMemoryRecallObservation(await readPersonaMemoryRecall({
identity,
config: input.config,
state: personaStateObservation,
projectedMemory: memorySurface,
recentMessages,
channelSnapshots,
}));
const globalAgentDoctrine = await readGlobalAgentDoctrine({ codexHome: process.env.CODEX_HOME, userProfile: process.env.USERPROFILE });
const conversationMemorySurface = input.conversationSurfacePath
? await readOptionalMemorySurface(input.conversationSurfacePath)
@ -582,8 +565,8 @@ async function assembleRepoFaceTurnPrompt(input: {
channelPlan,
channelSnapshots,
recentMessages,
memorySurface,
semanticMemoryRecallSurface,
memorySurface: memoryContext.memorySurface,
semanticMemoryRecallSurface: memoryContext.semanticMemoryRecallSurface,
repoActivitySurface,
conversationMemorySurface,
humanPronounGuidance,
@ -610,147 +593,6 @@ async function assembleRepoFaceTurnPrompt(input: {
};
}
async function renderRepoFaceMemorySurfaceForTurn(
identity: RepoDiscordIdentity,
config: ReturnType<typeof loadConfig>,
registryIdentities: RepoDiscordIdentity[] = [],
roomContext?: {
recentMessages: SourceMessage[];
channelSnapshots: ChannelSnapshot[];
},
humanPronounGuidance?: RepoFaceHumanPronounGuidance[],
observation?: PersonaStateObservation,
): Promise<string> {
if (identity.identityKind === "native_persona") {
if (!identity.personaStatePath) return [`${identity.displayName} is a native VoidBot Persona, not a repo Face.`, "No Persona state path is registered. Treat that as a Body fault and keep the public turn modest."].join("\n");
}
const acquired = observation ?? await readPersonaStateObservation({ identity, storageRoot: config.storageRoot });
if (acquired.status !== "ok") throw new Error(`${identity.displayName} Persona state ${acquired.status}: ${acquired.reason}`);
if (acquired.stateKind === "gamecult_persona") {
return projectGamecultPersonaState(identity, acquired.personaState);
}
if (acquired.stateKind === "persona_projection_import") {
return projectGamecultPersonaState(identity, acquired.projectionImport.payload);
}
const typedState = acquired.typedState;
const curiosityGraphFacts = roomContext && identity.identityKind !== "native_persona"
? await renderRepoFaceCuriosityGraphFacts(identity, config, typedState, roomContext)
: undefined;
const statePacket = projectPersonaStatePacket({
identity,
state: typedState,
registryIdentities,
roomContext,
humanPronounGuidance: humanPronounGuidance ?? await loadRepoFaceHumanPronounGuidance(config, roomContext),
curiosityGraphFacts,
observedAt: new Date(),
});
if (!config.repoFaceHeartbeats.stateProjectorEnabled) {
return projectPersonaMemorySurface({
identityId: identity.id,
characterIdentity: renderPersonaIdentityDoctrine(identity),
statePacket,
modelProjectionEnabled: false,
});
}
return projectPersonaMemorySurface({
identityId: identity.id,
characterIdentity: renderPersonaIdentityDoctrine(identity),
statePacket,
modelProjectionEnabled: true,
projectText: (prompt) => projectPersonaText({
prompt,
config,
command: "repo-face-state-projector",
jobId: `state-projector:${identity.id}:${Date.now()}`,
timeoutMs: 180_000,
}),
});
}
function renderPersonaMemoryRecallObservation(observation: PersonaMemoryRecallObservation): string {
if (observation.status === "unavailable") {
return [
"Semantic Persona memory recall unavailable:",
`- ${collapseWhitespace(observation.reason, 320)}`,
"- Do not pretend semantic Persona memory retrieval was available this turn; use projected state and raw transcript instead.",
].join("\n");
}
if (observation.hits.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.",
...observation.hits.map((hit, index) => {
const kind = hit.memoryKind ? `/${hit.memoryKind}` : "";
const target = hit.targetLabel ?? hit.targetId ?? "unknown target";
return `- ${index + 1}. ${target}${kind} score=${hit.score.toFixed(3)}: ${collapseWhitespace(hit.text, 520)}`;
}),
].join("\n");
}
async function renderRepoFaceCuriosityGraphFacts(
identity: RepoDiscordIdentity,
config: ReturnType<typeof loadConfig>,
state: VoidSelfStateTypedProjection,
roomContext: {
recentMessages: SourceMessage[];
channelSnapshots: ChannelSnapshot[];
},
): Promise<string | undefined> {
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 {
const historyEmbedder = createTextEmbedder({
backend: config.ragEmbeddingBackend,
hashDimensions: config.ragEmbeddingDimensions,
ollamaBaseUrl: config.ragOllamaBaseUrl,
ollamaModel: config.ragOllamaModel,
ollamaTimeoutMs: config.ragOllamaTimeoutMs,
queryInstruction: config.ragQueryInstruction,
});
const sourceEmbedder = createTextEmbedder({
backend: config.ragEmbeddingBackend,
hashDimensions: config.ragEmbeddingDimensions,
ollamaBaseUrl: config.ragOllamaBaseUrl,
ollamaModel: config.ragOllamaModel,
ollamaTimeoutMs: config.ragOllamaTimeoutMs,
queryInstruction: config.ragSourceQueryInstruction || config.ragQueryInstruction,
});
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, stores.personaMemory);
}
async function loadRepoFaceHumanPronounGuidance(
config: ReturnType<typeof loadConfig>,
roomContext?: {

View file

@ -65,6 +65,8 @@ 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 { coordinatePersonaMemoryTurn } from "../apps/persona-scheduler/dist/persona-memory-turn-coordinator.js";
import { projectPersonaMemoryRecall } from "../apps/persona-scheduler/dist/persona-memory-recall-projector.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";
@ -643,6 +645,27 @@ try {
observedAt: new Date("2026-07-15T21:00:00.000Z"),
}) : "";
assert.match(statePacket, /A witness artifact remains literal source evidence\./, "state packet projection does not rewrite natural language through a downstream vocabulary cop");
let recalledProjectedMemory = "";
const coordinatedMemory = await coordinatePersonaMemoryTurn({
identity: routedIdentity,
config: { storageRoot: stateSourceDirectory, repoFaceHeartbeats: { stateProjectorEnabled: false } } as never,
registryIdentities: [routedIdentity],
recentMessages: [],
channelSnapshots: [],
stateObservation: populatedObservation,
observedAt: new Date("2026-07-15T21:00:00.000Z"),
}, {
projectCuriosityFacts: async () => "Coordinator supplied curiosity facts.",
projectMemory: async ({ statePacket: packet }) => `Projected by coordinator:\n${packet}`,
readRecall: async ({ projectedMemory }) => {
recalledProjectedMemory = projectedMemory;
return { status: "ok", hits: [{ score: 0.91, text: "A recalled architectural promise.", targetLabel: "memory coordinator", memoryKind: "durable" }] };
},
});
assert.match(coordinatedMemory.memorySurface, /Coordinator supplied curiosity facts\./, "the coordinator sequences curiosity evidence into the state packet before memory projection");
assert.equal(recalledProjectedMemory, coordinatedMemory.memorySurface, "semantic recall consumes the exact projected Mind surface returned to both live and inspection turns");
assert.match(coordinatedMemory.semanticMemoryRecallSurface, /memory coordinator\/durable score=0\.910: A recalled architectural promise\./, "the coordinator returns prompt-ready semantic recall beside the projected Mind surface");
assert.match(projectPersonaMemoryRecall({ status: "unavailable", reason: "vector store asleep" }), /Semantic Persona memory recall unavailable:[\s\S]*vector store asleep/, "recall failure projection remains explicit and cannot masquerade as an empty successful search");
assert.deepEqual(await readFile(statePath), beforeObservation, "Persona state observation cannot rewrite sleep, speaking pressure, or any other Mind field");
const portableProjection = projectGamecultPersonaState({ ...routedIdentity, id: "muninn", displayName: "Muninn", identityKind: "native_persona" }, { schemaVersion: "gamecult.persona_state.v0", publicDescription: "Muninn remembers provenance.", privateNotes: ["Name missing evidence."], values: [{ label: "Source time", summary: "Keep observation time distinct." }], thoughtMemory: { shortTerm: [{ summary: "The last frame was stale." }] } });
assert.match(portableProjection, /Persona state authority is unreported; treat it as a non-canonical projection[\s\S]*Source time: Keep observation time distinct/, "the portable projector renders bounded state without inventing canonical authority");

View file

@ -433,3 +433,4 @@
{"ts":"2026-07-16T04:20:00Z","type":"implementation","status":"ok","note":"Queued and inspection Persona turns now share persona-turn-assembler.ts as the single owner of conversation projection, jurisdiction cadence, final prompt construction, and bounded deduplicated image evidence. The duplicate shell attachment collector was deleted; typecheck plus initiative and migration smokes pass."}
{"ts":"2026-07-16T04:35:00Z","type":"implementation","status":"ok","note":"Built-in Void CTB process mechanics moved into void-moderation-turn-actuator.ts: pending-mention handoff, PowerShell quoting, detached launch, handshake polling, timeout, and scheduler receipt. The shell lost 115 lines; focused smoke proves escaping, fail-closed missing handshake, exact mention handoff, and receipt semantics."}
{"ts":"2026-07-16T04:45:00Z","type":"implementation","status":"ok","note":"Global Codex doctrine discovery moved into global-agent-doctrine-source.ts with typed provenance and unavailable evidence. persona-turn-assembler.ts now owns failure-to-prompt projection; the shell no longer crawls CODEX_HOME/USERPROFILE/home paths or authors fallback doctrine. Focused smoke proves candidate deduplication, first-readable authority, and explicit failure projection."}
{"ts":"2026-07-16T05:05:00Z","type":"architecture","status":"ok","note":"Persona Mind assembly moved into persona-memory-turn-coordinator.ts with injected state, curiosity, projection, and recall ports; recall text moved into persona-memory-recall-projector.ts. This exposed and repaired a live/inspection divergence: queued turns previously omitted semantic Persona recall while inspection included it. Both paths now consume the same projected Mind plus recall pair; shell fell to 718 lines and typecheck/initiative/migration smokes pass."}

View file

@ -74,6 +74,7 @@ active_subgoals:
- `persona-turn-assembler.ts` is the single final-composition owner for queued and inspection Persona turns. It combines supplied observations through conversation projection and turn-prompt policy, derives jurisdiction cadence, and returns one prompt plus one bounded deduplicated image bundle. It cannot acquire source state, select initiative, or queue work; the shell and actuator cannot maintain parallel prompt opinions.
- `void-moderation-turn-actuator.ts` owns the Windows-specific built-in Void turn launch boundary: pending-attention handoff, PowerShell quoting and detached process launch, lock/status handshake polling, timeout, and scheduler receipt. The orchestration shell chooses the participant kind but cannot launch, poll, sleep, or infer success from process creation alone.
- `global-agent-doctrine-source.ts` owns read-only candidate discovery and acquisition for global Codex `AGENTS.md` doctrine. It returns typed `ok` or `unavailable` evidence with provenance and errors; `persona-turn-assembler.ts` owns the prompt-facing failure projection. The shell cannot search home directories or invent fallback doctrine.
- `persona-memory-turn-coordinator.ts` owns sequencing, not truth: it carries one typed Persona-state observation through curiosity evidence/projection, deterministic state-packet projection, optional text projection, and semantic recall, returning one projected Mind plus one recall surface. Each source/projector retains its own invariant and injected port. Live queued turns and inspection turns share this coordinator, so semantic recall can no longer exist only in inspection. `persona-memory-recall-projector.ts` alone lowers recall hits or unavailable evidence into prompt text.
- `bifrost-governance-source.ts` owns the remaining Bifrost digest CLI adapter and converts command or parse failures into typed error topics. Persona prompt composition receives governance facts without knowing Bifrost process mechanics; `scripts/run-repo-face-heartbeats.ts` may not invoke the Bifrost CLI directly.
- `initiative-engine.ts` is the only writer of scheduler state fields and history. Reconciliation, controls, pause/disabled skips, stale-turn recovery witnesses, semantic pressure, attention consumption, queue transitions, and tick finalization all cross named engine commit primitives. Queue/context actuators return results and cannot patch participants or history directly.
- `turn-actuator.ts` owns lowering an assembled Persona prompt and evidence bundle into the canonical worker `ContextBundle` and approved `repo-face-rumination` job. It owns the worker actor, command, request ID, approval defaults, and `createJob` call, then returns only a queue receipt. The orchestration shell chooses no queue mechanics and the actuator cannot read Persona or initiative state.
@ -287,4 +288,4 @@ rejected_paths:
current_status:
summary: VoidBot's bot/worker/provider/RAG/Postgres/Qdrant/CultCache structure is live. Repo Face/persona heartbeat jobs run through the direct `openai_api` provider configured for MiMo `mimo-v2.5-pro`, with the expensive state-projector pass still disabled. The 2026-07-04 transport repair replaced the old Bifrost HTTP/token bridge shortcut with typed Bifrost CultMesh Discord command and receipt documents advertised in the Bifrost provider store at `cultmesh://asgard.starfire.bifrost/commands/discord-post`. VoidBot stores the Bifrost target as a CultMesh URI, writes command docs to `E:/Projects/Bifrost/.bifrost/provider-store.cc`, optionally pumps Bifrost's command processor, and waits for typed receipts before completing repo Face Discord delivery. Bifrost's provider advertisement exposes `discord.post` command metadata for Odin/Eve agents, and a bounded Weksa live-fire completed visibly. Eve live state now runs end to end through CultLib dev retained publications, Hermodr ordered SSE, Eve component bindings, and VoidBot typed control state. Receipts acknowledge intent only. Heat initial load, provider-driven change, and browser-command change were visibly verified without reload through the same subscription.
next_action: Keep the Yggdrasil swarm paused. Map and extract the remaining Persona memory-context coordination from `renderRepoFaceMemorySurfaceForTurn` in `scripts/run-repo-face-heartbeats.ts`: state observation, semantic recall, curiosity evidence, and projection must cross a typed coordinator without stealing source, projection, or scheduler authority. Do not unpause without an explicit controlled-live-fire decision.
next_action: Keep the Yggdrasil swarm paused. Extract the remaining interaction-profile acquisition adapter from `loadRepoFaceHumanPronounGuidance` in `scripts/run-repo-face-heartbeats.ts` so the shell no longer opens state storage to feed the existing social-context source. Preserve provider ownership and close semantics; do not move social projection or scheduler authority into the adapter. Do not unpause without an explicit controlled-live-fire decision.