Route mood runtime updates through typed operations

This commit is contained in:
Meta Vens 2026-05-16 12:21:00 +01:00
commit 82859d3fee
4 changed files with 43 additions and 3 deletions

View file

@ -30,6 +30,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
- `scripts/export-recent-repo-activity.mjs` no longer writes the JSON working projection for cursor advancement. It applies `update_repo_activity_cursor` through `scripts/void-self-state.mjs`; old `--cursor-file .json` arguments are only translated to the sibling `.msgpack` path for compatibility.
- `scripts/run-void-moderator-rumination.ps1` now records reviewed Discord cursors and speech delivery receipts through typed self-state operations. The old `set-cursor` and `record-delivery-receipt` commands were removed from `scripts/moderation-state-store.mjs`.
- The unattended moderation child no longer gets to commit whole-state JSON edits. It reads `.voidbot/private/moderation-agent-state.json` as a compatibility projection, writes proposed operation payloads to `.voidbot/status/moderation-rumination-operations.json`, and the parent runner applies them through `scripts/void-self-state.mjs apply-operation`.
- `scripts/simulate-void-mood.mjs` now sends sleep-cycle and speaking-pressure results through typed operations after its legacy save, so scheduled-runtime typed documents stay current. The memory organ itself still mutates the legacy monolith and remains the next major surgery.
- Most of the deterministic cleanup in `scripts/void-memory-organ.mjs` is recent compensator cruft from failed attempts to manually clean an exploded state file. Do not preserve it as an earned organ. Architect the state boundary so that cleanup pile is unnecessary.
- Sleep/distillation must be rewritten with a meaning-preservation contract: concrete subject, claim/question, evidence refs, live tension, and future-action implication survive compression. Bulk can die. Meaning does not get put through the office shredder and called a dream.
- The owner Codex lane is no longer one swollen file; orchestration, runtime/parsing, rendering, and shared helpers are split under `packages/providers/src/owner-codex-*.ts`.
@ -88,7 +89,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
## Likely Next Bounded Move
- Stop feature work on moderation, mood, agency, and public-lane behavior until the private-state boundary is rebuilt.
- First bounded move after the current commit: move mood-drift, scheduled-runtime pressure/sleep updates, and memory-maintenance writes behind typed operations instead of direct canonical/working-projection writes.
- First bounded move after the current commit: move memory-maintenance, incubation, and candidate-intervention outputs behind typed operations instead of direct canonical/working-projection writes.
- Next cuts should delete unstable foundations early after concrete writers move: remove `legacyJsonPath`/working-projection authority, then cut top-level mirror fields and cleanup code that only compensated for those broken boundaries.
- Rebuild sleep/distillation before trusting it again. Fixture check: a memory like `AquariumSynthCSharp: Workflow cannot own the body` must survive sleep with its concrete subject, claim, evidence, tension, and implication intact.
- Do not build adapters around the JSON projection. That is how the heap learned to stand upright and ask for snacks.

View file

@ -8,11 +8,12 @@ import { reconcileSemanticMemoryState } from "./void-memory-organ.mjs";
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const require = createRequire(import.meta.url);
const {
applyVoidSelfStateOperation,
ensureModerationStateStore,
getModerationStateWorkingPath,
materializeModerationStateWorkingView,
saveModerationState,
} = require(resolve(repoRoot, "packages/core/dist/moderation-state-store.js"));
} = require(resolve(repoRoot, "packages/core/dist/index.js"));
const statePath = resolve(repoRoot, ".voidbot/private/moderation-agent-state.msgpack");
const stateWorkingPath = getModerationStateWorkingPath(statePath);
const stateTemplatePath = resolve(repoRoot, "config/moderation-agent-state-template.json");
@ -183,6 +184,20 @@ async function main() {
state.moderation_runtime = runtime;
await saveModerationState(statePath, state);
await applyVoidSelfStateOperation(
{ canonicalPath: statePath },
{
operation: "update_sleep_cycle",
sleepCycle: projectTypedSleepCycle(sleepCycle),
},
);
await applyVoidSelfStateOperation(
{ canonicalPath: statePath },
{
operation: "update_speaking_pressure",
speakingPressure: projectTypedSpeakingPressure(speakingBias),
},
);
await materializeModerationStateWorkingView({
state,
workingPath: stateWorkingPath,
@ -224,6 +239,29 @@ async function main() {
});
}
function projectTypedSleepCycle(sleepCycle) {
return {
isNapping: sleepCycle.isNapping === true,
currentNapStartedAt: typeof sleepCycle.currentNapStartedAt === "string" ? sleepCycle.currentNapStartedAt : undefined,
currentNapEndsAt: typeof sleepCycle.currentNapEndsAt === "string" ? sleepCycle.currentNapEndsAt : undefined,
nextNapStartsAt: typeof sleepCycle.nextNapStartsAt === "string" ? sleepCycle.nextNapStartsAt : undefined,
activeDreamThemes: Array.isArray(sleepCycle.activeDreamThemes)
? sleepCycle.activeDreamThemes.filter((value) => typeof value === "string" && value.trim().length > 0)
: [],
};
}
function projectTypedSpeakingPressure(speakingBias) {
return {
needToSpeak: typeof speakingBias.needToSpeak === "number" ? speakingBias.needToSpeak : 0,
confessionPressure: typeof speakingBias.confessionPressure === "number" ? speakingBias.confessionPressure : undefined,
noveltyPressure: typeof speakingBias.noveltyPressure === "number" ? speakingBias.noveltyPressure : undefined,
recentSpeechDamping: typeof speakingBias.recentSpeechDamping === "number" ? speakingBias.recentSpeechDamping : undefined,
lastSpokeAt: typeof speakingBias.lastSpokeAt === "string" ? speakingBias.lastSpokeAt : undefined,
lastHeraldAt: typeof speakingBias.lastHeraldAt === "string" ? speakingBias.lastHeraldAt : undefined,
};
}
function topThreadDesire(runtime, limit) {
const thoughtLanes = isObject(runtime.thought_lanes) ? runtime.thought_lanes : {};
const desires = [];

View file

@ -95,3 +95,4 @@
{"ts":"2026-05-16T11:09:34Z","type":"implementation","status":"ok","note":"Repo activity cursor writes now go through scripts/void-self-state.mjs apply-operation with update_repo_activity_cursor instead of editing moderation-agent-state.json and committing the whole projection. export-recent-repo-activity still accepts old --cursor-file JSON paths by deriving the sibling .msgpack path, but the runner/doctrine now point at --state-path .voidbot/private/moderation-agent-state.msgpack. Temp-state repo sweep smoke passed."}
{"ts":"2026-05-16T11:14:12Z","type":"implementation","status":"ok","note":"Moderation runner now records reviewed cursor and delivered speech receipts through typed self-state operations instead of moderation-state-store mutation commands. The old set-cursor and record-delivery-receipt commands/functions were removed from the legacy moderation-state-store surface. Receipt operation smoke verifies it records one receipt and closes the matching open case."}
{"ts":"2026-05-16T11:17:09Z","type":"implementation","status":"ok","note":"The unattended moderation runner no longer commits child edits from moderation-agent-state.json. The child is instructed to treat the JSON as read-only and write a JSON array of typed operation payloads to .voidbot/status/moderation-rumination-operations.json; the parent applies each operation through scripts/void-self-state.mjs before recording cursor/receipt operations. Cheap parser/typecheck verification passed; full scheduled dry run was not run to avoid mutating live moderation state during code verification."}
{"ts":"2026-05-16T11:20:42Z","type":"implementation","status":"ok","note":"Mood drift now sends sleep-cycle and speaking-pressure results through typed self-state operations after saving the legacy monolith, so scheduled-runtime typed documents stay current. The larger memory organ still mutates legacy state directly; next surgery is to move memory/incubation/candidate outputs behind operations and then remove working-projection materialization."}

View file

@ -149,4 +149,4 @@ rejected_paths:
current_status:
summary: VoidBot's ordinary bot/worker/provider/RAG/Postgres/Qdrant structure is still conceptually sound, and the moderation/mood/private-self-state rebuild has cut the worst mutation path: routine runner writes now cross a typed operation service instead of direct JSON projection commits. Remaining risk is that the legacy monolith and read-only projection still exist for compatibility, mood drift still refreshes that projection, and memory/sleep cleanup remains swollen compensator code.
next_action: JSON projection authority is no longer a routine moderation-runner write path. Next move mood-drift/scheduled-runtime and memory-maintenance writes behind typed operations, then delete `legacyJsonPath`/working-projection materialization and cut legacy mirror fields. Redesign sleep/distillation around a meaning-preservation contract before trusting it to compress memory.
next_action: JSON projection authority is no longer a routine moderation-runner write path, and mood drift now updates typed scheduled-runtime documents for sleep/speaking pressure. Next move memory-maintenance/incubation/candidate outputs behind typed operations, then delete `legacyJsonPath`/working-projection materialization and cut legacy mirror fields. Redesign sleep/distillation around a meaning-preservation contract before trusting it to compress memory.