Clarify typed self-state projection boundary

This commit is contained in:
Meta Vens 2026-05-17 08:32:17 +01:00
commit 2f3aaa8f17
6 changed files with 216 additions and 193 deletions

View file

@ -26,7 +26,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
- CultCache-backed self-state uses the `.cc` extension. The active store path is `.voidbot/private/void-self-state.cc`.
- The smallest coherent target is one polymorphic CultCache-backed state authority with typed document kinds plus typed mutation tools: agents propose small operations, and the state service validates/applies them. Whole-state JSON editing is rejected as a durable mutation path.
- Commit-one boundary naming is now in code at `packages/core/src/void-self-state-domain.ts`: it declares the global typed document kinds and strict operation payload schemas. Runtime behavior has not moved yet.
- Read-only projection is now in `packages/core/src/void-self-state-projection.ts`. `loadVoidSelfState` uses that typed projection for nap/reply-mode context while leaving the old richer prompt summary intact until typed documents explicitly own those details.
- Read-only projection is now owned by `packages/core/src/void-self-state-projection.ts`. `loadVoidSelfState` is a thin loader that reads typed `.cc` documents and delegates the agent-facing summary plus nap/reply-mode projection to that module.
- Typed operations are applied through `packages/core/src/void-self-state-service.ts` and `scripts/void-self-state.mjs apply-operation`. The service now writes typed documents only; it no longer registers, reads, updates, or mirrors the old moderation monolith during routine operations.
- `scripts/export-recent-repo-activity.mjs` reads repo cursors from the typed `.cc` store. Legacy `.json` cursor arguments are only mapped to a sibling `.cc` path when needed by old invocation surfaces.
- `scripts/run-void-moderator-rumination.ps1` has been rebuilt as a typed rumination runner. It builds a typed context packet, loads `prompts/void-moderator-rumination.md`, runs Codex rumination, and applies only the JSON operation payloads that come back through `scripts/void-self-state.mjs`. Cursor and receipt bookkeeping remain parent-owned.

View file

@ -42,6 +42,10 @@ This note is the source-grounded description of how the live VoidBot stack is sh
- Profile synthesis, disposition, psychological read, inferred traits, interaction dimensions, and response-guidance construction.
- `packages/core/src/context-builder.ts`
- request context assembly, including interaction-memory/profile attachment, shared Void self-state projection, and any precomputed situational social read.
- `packages/core/src/void-self-state-loader.ts`
- thin typed `.cc` self-state loader that delegates read-side rendering to the projection module.
- `packages/core/src/void-self-state-projection.ts`
- read-side projection authority for typed self-state: renders the agent-facing summary and compact nap/reply-mode projection from typed documents without touching legacy state.
- `packages/core/src/situational-social-read.ts`
- quick Ollama sidecar inferer for ephemeral room-reading scaffolding built from the current prompt, recent room transcript, and longer-horizon interaction memory.
- `packages/providers/src/owner-codex-provider.ts`

View file

@ -176,9 +176,10 @@ Verdict: stop feature work and rebuild this foundation. The rest of the machine
### Commit 2: Add Read-Only Typed Projections
- Build a self-state service that reads current canonical state and renders the same summaries used by direct replies.
- Keep JSON projection for the old runner.
- Verification: compare rendered summary against current loader output on a fixture.
- Landed path: `packages/core/src/void-self-state-projection.ts` owns typed `.cc` document to agent-facing summary/projection rendering.
- `packages/core/src/void-self-state-loader.ts` only loads typed documents and delegates read-side projection.
- No JSON projection is needed for the live typed runner.
- Verification: projection smoke loads a temp `.cc` store and confirms mode plus transient room context render.
### Commit 3: Add Typed Mutation CLI

View file

@ -4,11 +4,7 @@ import {
type VoidSelfStateContext,
} from "@voidbot/shared";
import {
type VoidScheduledRuntime,
type VoidThoughtMemory,
} from "./void-self-state-domain";
import { type VoidSelfStateTypedProjection } from "./void-self-state-projection";
import { buildVoidSelfStateContext } from "./void-self-state-projection";
import { loadVoidSelfStateTypedDocuments } from "./void-self-state-service";
export interface LoadVoidSelfStateOptions {
@ -20,9 +16,12 @@ export async function loadVoidSelfState(
statePath: string,
options: LoadVoidSelfStateOptions = {},
): Promise<VoidSelfStateContext | undefined> {
let typedProjection: VoidSelfStateTypedProjection;
try {
typedProjection = await loadVoidSelfStateTypedDocuments({ canonicalPath: statePath });
const typedProjection = await loadVoidSelfStateTypedDocuments({ canonicalPath: statePath });
return buildVoidSelfStateContext(typedProjection, {
sourcePath: statePath,
...options,
});
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
@ -32,186 +31,4 @@ export async function loadVoidSelfState(
throw error;
}
const projection = buildVoidSelfStateProjection(typedProjection);
return {
sourcePath: statePath,
loadedAt: new Date().toISOString(),
summary: renderTypedVoidSelfStateSummary(typedProjection, options),
projection,
};
}
function renderTypedVoidSelfStateSummary(
state: VoidSelfStateTypedProjection,
options: LoadVoidSelfStateOptions,
): string {
const profile = state.selfProfile;
const topValues = state.selfProfile.values
.sort((left, right) => right.priority - left.priority)
.slice(0, 4)
.map((value) => value.label);
const memories = state.thoughtMemory.memories
.slice(-6)
.reverse()
.map((memory) => renderTypedMemory(memory));
const incubation = state.thoughtMemory.incubation
.filter((thread) => thread.status !== "retired")
.sort((left, right) => right.maturation - left.maturation)
.slice(0, 4)
.map((thread) => renderTypedIncubationThread(thread));
const openCases = state.moderationCursor.openCases
.filter((entry) => !["answered", "resolved", "closed", "retired", "dropped"].includes(entry.status))
.slice(-4)
.reverse()
.map((entry) => `- ${entry.summary}${entry.authorName ? ` [from ${entry.authorName}]` : ""}`);
const interventions = state.candidateInterventions.interventions
.filter((entry) => entry.status === "queued" || entry.status === "deferred")
.slice(-3)
.reverse()
.map((entry) => `- ${entry.summary}: ${entry.draft}`);
return [
`- Identity: ${profile.publicName}${profile.publicDescription ? ` - ${profile.publicDescription}` : ""}`,
profile.privateNotes.length > 0
? `- Private notes: ${profile.privateNotes.slice(0, 4).join(" | ")}`
: "- Private notes: none recorded.",
topValues.length > 0
? `- Highest-priority values: ${topValues.join(" | ")}`
: "- Highest-priority values: none recorded.",
renderTypedSleepCycleSummary(state.scheduledRuntime, state.thoughtMemory),
renderTypedSpeakingPressureSummary(state.scheduledRuntime),
openCases.length > 0
? ["- Outstanding room obligations:", ...openCases].join("\n")
: "- Outstanding room obligations: none recorded.",
memories.length > 0
? ["- Typed memory:", ...memories].join("\n")
: "- Typed memory: none recorded.",
incubation.length > 0
? ["- Incubating threads:", ...incubation].join("\n")
: "- Incubating threads: none recorded.",
interventions.length > 0
? ["- Candidate interventions:", ...interventions].join("\n")
: "- Candidate interventions: none recorded.",
renderTransientRoomLog(options),
].join("\n");
}
function renderTypedMemory(memory: VoidThoughtMemory["memories"][number]): string {
const target = memory.target.label ?? memory.target.id;
const meaning = [
memory.claim ? `claim=${memory.claim}` : undefined,
memory.question ? `question=${memory.question}` : undefined,
memory.tension ? `tension=${memory.tension}` : undefined,
memory.actionImplication ? `next=${memory.actionImplication}` : undefined,
].filter((value): value is string => Boolean(value));
return `- ${target} [${memory.kind}]: ${memory.summary}${meaning.length > 0 ? ` (${meaning.join("; ")})` : ""}`;
}
function renderTypedIncubationThread(thread: VoidThoughtMemory["incubation"][number]): string {
const target = thread.target.label ?? thread.target.id;
const metrics = [
`status=${thread.status}`,
`maturation=${thread.maturation.toFixed(2)}`,
thread.desireToSpeak !== undefined ? `speak=${thread.desireToSpeak.toFixed(2)}` : undefined,
thread.noveltyToSelf !== undefined ? `self=${thread.noveltyToSelf.toFixed(2)}` : undefined,
thread.noveltyToRoom !== undefined ? `room=${thread.noveltyToRoom.toFixed(2)}` : undefined,
].filter((value): value is string => Boolean(value));
return `- ${thread.topic} -> ${target} [${metrics.join("; ")}]: ${thread.summary}`;
}
function renderTypedSleepCycleSummary(
runtime: VoidScheduledRuntime,
thoughtMemory: VoidThoughtMemory,
): string {
const sleepCycle = runtime.sleepCycle;
const dreams = thoughtMemory.memories
.filter((memory) => memory.kind === "dream_residue")
.slice(-2)
.reverse()
.map((memory) => `${memory.target.label ?? memory.target.id}: ${memory.summary}`);
const headline = sleepCycle.isNapping
? `- Sleep cycle: napping${sleepCycle.currentNapEndsAt ? ` until ${sleepCycle.currentNapEndsAt}` : ""}.`
: `- Sleep cycle: awake${sleepCycle.nextNapStartsAt ? `; next nap ${sleepCycle.nextNapStartsAt}` : ""}.`;
const themeLine =
sleepCycle.activeDreamThemes.length > 0
? `- Active dream themes: ${sleepCycle.activeDreamThemes.join(" | ")}`
: "- Active dream themes: none recorded.";
const dreamLine =
dreams.length > 0
? `- Recent dream residue: ${dreams.join(" | ")}`
: "- Recent dream residue: none recorded.";
return [headline, themeLine, dreamLine].join("\n");
}
function renderTypedSpeakingPressureSummary(runtime: VoidScheduledRuntime): string {
const pressure = runtime.speakingPressure;
const parts = [
`needToSpeak=${pressure.needToSpeak.toFixed(2)}`,
pressure.confessionPressure !== undefined ? `confession=${pressure.confessionPressure.toFixed(2)}` : undefined,
pressure.noveltyPressure !== undefined ? `novelty=${pressure.noveltyPressure.toFixed(2)}` : undefined,
pressure.recentSpeechDamping !== undefined ? `speechDamping=${pressure.recentSpeechDamping.toFixed(2)}` : undefined,
].filter((value): value is string => Boolean(value));
return `- Speaking pressure: ${parts.join(", ")}`;
}
function renderTransientRoomLog(options: LoadVoidSelfStateOptions): string {
const recentMessages = (options.recentMessages ?? []).slice(-10);
const guildName = options.guildContext?.guildName ?? options.guildContext?.guildId ?? "(direct/unknown)";
const channelName =
options.guildContext?.channelName ?? options.guildContext?.channelId ?? "(unknown channel)";
if (recentMessages.length === 0) {
return [
"- Current room snapshot:",
`- Current room: guild=${guildName}; channel=${channelName}`,
"- No recent chat messages were captured in this snapshot.",
].join("\n");
}
const lines = recentMessages.map(
(message) =>
`- [${message.timestamp}] ${message.authorName}: ${collapseWhitespace(message.content || "(no text content)")}`,
);
return [
"- Current room snapshot:",
`- Current room: guild=${guildName}; channel=${channelName}`,
...lines,
].join("\n");
}
function buildVoidSelfStateProjection(
typedState: VoidSelfStateTypedProjection,
): VoidSelfStateContext["projection"] {
const sleepCycle = typedState.scheduledRuntime.sleepCycle;
const dreamSummaries = typedState.thoughtMemory.memories
.filter((memory) => memory.kind === "dream_residue")
.slice(-3)
.reverse()
.map((memory) => memory.summary);
const isNapping = sleepCycle.isNapping === true;
const napStartedAt = sleepCycle.currentNapStartedAt;
const napEndsAt = sleepCycle.currentNapEndsAt;
const nextNapAt = sleepCycle.nextNapStartsAt;
return {
mode: isNapping ? "napping" : "awake",
effortCeiling: isNapping ? "minimal" : "normal",
napStartedAt: isNapping ? napStartedAt : undefined,
napEndsAt: isNapping ? napEndsAt : undefined,
nextNapAt,
activeDreamThemes: sleepCycle.activeDreamThemes,
recentDreamSummaries: dreamSummaries,
replyDirective: isNapping
? "You are in a scheduled nap. Reply in brief, low-effort, half-dreaming mutters instead of doing full attentive service-work."
: undefined,
};
}
function collapseWhitespace(value: string): string {
return value.replace(/\s+/g, " ").trim();
}

View file

@ -1,3 +1,9 @@
import {
type GuildContext,
type SourceMessage,
type VoidSelfStateContext,
} from "@voidbot/shared";
import {
type VoidCandidateInterventions,
type VoidModerationCursor,
@ -22,6 +28,13 @@ export interface VoidSelfStateTypedProjection {
candidateInterventions: VoidCandidateInterventions;
}
export interface VoidSelfStateProjectionOptions {
sourcePath: string;
loadedAt?: string;
recentMessages?: SourceMessage[];
guildContext?: GuildContext;
}
export function createEmptyVoidSelfState(
options: { createdAt?: string } = {},
): VoidSelfStateTypedProjection {
@ -80,3 +93,190 @@ export function createEmptyVoidSelfState(
}),
};
}
export function buildVoidSelfStateContext(
typedState: VoidSelfStateTypedProjection,
options: VoidSelfStateProjectionOptions,
): VoidSelfStateContext {
return {
sourcePath: options.sourcePath,
loadedAt: options.loadedAt ?? new Date().toISOString(),
summary: renderVoidSelfStateSummary(typedState, options),
projection: buildVoidSelfStateProjection(typedState),
};
}
export function renderVoidSelfStateSummary(
state: VoidSelfStateTypedProjection,
options: Pick<VoidSelfStateProjectionOptions, "guildContext" | "recentMessages"> = {},
): string {
const profile = state.selfProfile;
const topValues = state.selfProfile.values
.sort((left, right) => right.priority - left.priority)
.slice(0, 4)
.map((value) => value.label);
const memories = state.thoughtMemory.memories
.slice(-6)
.reverse()
.map((memory) => renderTypedMemory(memory));
const incubation = state.thoughtMemory.incubation
.filter((thread) => thread.status !== "retired")
.sort((left, right) => right.maturation - left.maturation)
.slice(0, 4)
.map((thread) => renderTypedIncubationThread(thread));
const openCases = state.moderationCursor.openCases
.filter((entry) => !["answered", "resolved", "closed", "retired", "dropped"].includes(entry.status))
.slice(-4)
.reverse()
.map((entry) => `- ${entry.summary}${entry.authorName ? ` [from ${entry.authorName}]` : ""}`);
const interventions = state.candidateInterventions.interventions
.filter((entry) => entry.status === "queued" || entry.status === "deferred")
.slice(-3)
.reverse()
.map((entry) => `- ${entry.summary}: ${entry.draft}`);
return [
`- Identity: ${profile.publicName}${profile.publicDescription ? ` - ${profile.publicDescription}` : ""}`,
profile.privateNotes.length > 0
? `- Private notes: ${profile.privateNotes.slice(0, 4).join(" | ")}`
: "- Private notes: none recorded.",
topValues.length > 0
? `- Highest-priority values: ${topValues.join(" | ")}`
: "- Highest-priority values: none recorded.",
renderTypedSleepCycleSummary(state.scheduledRuntime, state.thoughtMemory),
renderTypedSpeakingPressureSummary(state.scheduledRuntime),
openCases.length > 0
? ["- Outstanding room obligations:", ...openCases].join("\n")
: "- Outstanding room obligations: none recorded.",
memories.length > 0
? ["- Typed memory:", ...memories].join("\n")
: "- Typed memory: none recorded.",
incubation.length > 0
? ["- Incubating threads:", ...incubation].join("\n")
: "- Incubating threads: none recorded.",
interventions.length > 0
? ["- Candidate interventions:", ...interventions].join("\n")
: "- Candidate interventions: none recorded.",
renderTransientRoomLog(options),
].join("\n");
}
export function buildVoidSelfStateProjection(
typedState: VoidSelfStateTypedProjection,
): VoidSelfStateContext["projection"] {
const sleepCycle = typedState.scheduledRuntime.sleepCycle;
const dreamSummaries = typedState.thoughtMemory.memories
.filter((memory) => memory.kind === "dream_residue")
.slice(-3)
.reverse()
.map((memory) => memory.summary);
const isNapping = sleepCycle.isNapping === true;
const napStartedAt = sleepCycle.currentNapStartedAt;
const napEndsAt = sleepCycle.currentNapEndsAt;
const nextNapAt = sleepCycle.nextNapStartsAt;
return {
mode: isNapping ? "napping" : "awake",
effortCeiling: isNapping ? "minimal" : "normal",
napStartedAt: isNapping ? napStartedAt : undefined,
napEndsAt: isNapping ? napEndsAt : undefined,
nextNapAt,
activeDreamThemes: sleepCycle.activeDreamThemes,
recentDreamSummaries: dreamSummaries,
replyDirective: isNapping
? "You are in a scheduled nap. Reply in brief, low-effort, half-dreaming mutters instead of doing full attentive service-work."
: undefined,
};
}
function renderTypedMemory(memory: VoidThoughtMemory["memories"][number]): string {
const target = memory.target.label ?? memory.target.id;
const meaning = [
memory.claim ? `claim=${memory.claim}` : undefined,
memory.question ? `question=${memory.question}` : undefined,
memory.tension ? `tension=${memory.tension}` : undefined,
memory.actionImplication ? `next=${memory.actionImplication}` : undefined,
].filter((value): value is string => Boolean(value));
return `- ${target} [${memory.kind}]: ${memory.summary}${meaning.length > 0 ? ` (${meaning.join("; ")})` : ""}`;
}
function renderTypedIncubationThread(thread: VoidThoughtMemory["incubation"][number]): string {
const target = thread.target.label ?? thread.target.id;
const metrics = [
`status=${thread.status}`,
`maturation=${thread.maturation.toFixed(2)}`,
thread.desireToSpeak !== undefined ? `speak=${thread.desireToSpeak.toFixed(2)}` : undefined,
thread.noveltyToSelf !== undefined ? `self=${thread.noveltyToSelf.toFixed(2)}` : undefined,
thread.noveltyToRoom !== undefined ? `room=${thread.noveltyToRoom.toFixed(2)}` : undefined,
].filter((value): value is string => Boolean(value));
return `- ${thread.topic} -> ${target} [${metrics.join("; ")}]: ${thread.summary}`;
}
function renderTypedSleepCycleSummary(
runtime: VoidScheduledRuntime,
thoughtMemory: VoidThoughtMemory,
): string {
const sleepCycle = runtime.sleepCycle;
const dreams = thoughtMemory.memories
.filter((memory) => memory.kind === "dream_residue")
.slice(-2)
.reverse()
.map((memory) => `${memory.target.label ?? memory.target.id}: ${memory.summary}`);
const headline = sleepCycle.isNapping
? `- Sleep cycle: napping${sleepCycle.currentNapEndsAt ? ` until ${sleepCycle.currentNapEndsAt}` : ""}.`
: `- Sleep cycle: awake${sleepCycle.nextNapStartsAt ? `; next nap ${sleepCycle.nextNapStartsAt}` : ""}.`;
const themeLine =
sleepCycle.activeDreamThemes.length > 0
? `- Active dream themes: ${sleepCycle.activeDreamThemes.join(" | ")}`
: "- Active dream themes: none recorded.";
const dreamLine =
dreams.length > 0
? `- Recent dream residue: ${dreams.join(" | ")}`
: "- Recent dream residue: none recorded.";
return [headline, themeLine, dreamLine].join("\n");
}
function renderTypedSpeakingPressureSummary(runtime: VoidScheduledRuntime): string {
const pressure = runtime.speakingPressure;
const parts = [
`needToSpeak=${pressure.needToSpeak.toFixed(2)}`,
pressure.confessionPressure !== undefined ? `confession=${pressure.confessionPressure.toFixed(2)}` : undefined,
pressure.noveltyPressure !== undefined ? `novelty=${pressure.noveltyPressure.toFixed(2)}` : undefined,
pressure.recentSpeechDamping !== undefined ? `speechDamping=${pressure.recentSpeechDamping.toFixed(2)}` : undefined,
].filter((value): value is string => Boolean(value));
return `- Speaking pressure: ${parts.join(", ")}`;
}
function renderTransientRoomLog(
options: Pick<VoidSelfStateProjectionOptions, "guildContext" | "recentMessages">,
): string {
const recentMessages = (options.recentMessages ?? []).slice(-10);
const guildName = options.guildContext?.guildName ?? options.guildContext?.guildId ?? "(direct/unknown)";
const channelName =
options.guildContext?.channelName ?? options.guildContext?.channelId ?? "(unknown channel)";
if (recentMessages.length === 0) {
return [
"- Current room snapshot:",
`- Current room: guild=${guildName}; channel=${channelName}`,
"- No recent chat messages were captured in this snapshot.",
].join("\n");
}
const lines = recentMessages.map(
(message) =>
`- [${message.timestamp}] ${message.authorName}: ${collapseWhitespace(message.content || "(no text content)")}`,
);
return [
"- Current room snapshot:",
`- Current room: guild=${guildName}; channel=${channelName}`,
...lines,
].join("\n");
}
function collapseWhitespace(value: string): string {
return value.replace(/\s+/g, " ").trim();
}

View file

@ -105,3 +105,4 @@
{"ts":"2026-05-16T17:39:43Z","type":"implementation","status":"ok","note":"Took legacy scheduled self-state writers offline and rebuilt the active mood/moderation entry scripts around typed-only CultCache state at .voidbot/private/void-self-state.cc. The typed service no longer registers, reads, or writes the legacy moderation document; mood updates sleep/speaking pressure only, and moderation runner is cursor-only until the typed phase machine is rebuilt."}
{"ts":"2026-05-16T17:43:21Z","type":"implementation","status":"ok","note":"Deleted the legacy moderation-to-typed projection function from the active self-state projection module. New typed self-state now starts from explicit empty typed documents or validated typed operations; old moderation state is no longer a loader/projection source for the next brain."}
{"ts":"2026-05-16T18:12:51Z","type":"implementation","status":"ok","note":"Restored scheduled moderation as typed rumination instead of cursor-only bookkeeping. The runner now loads prompts/void-moderator-rumination.md, builds a bounded typed context packet from .voidbot/private/void-self-state.cc, accepts only typed operation output, and leaves cursor/receipt writes parent-owned."}
{"ts":"2026-05-17T07:31:53Z","type":"implementation","status":"ok","note":"Cleaned up the typed self-state read projection boundary: packages/core/src/void-self-state-projection.ts now owns typed document to agent-facing summary/projection rendering, while void-self-state-loader.ts is a thin .cc loader/delegator. Projection smoke confirmed nap mode and transient room context render from a temp .cc store."}