VoidBot/notes/voidbot-implementation-plan.md
2026-05-16 08:50:18 +01:00

230 lines
10 KiB
Markdown

# VoidBot Implementation Plan
This is the current forward plan for the next larger organs. It is not a changelog and it is not allowed to become a duplicate brain for the canonical map.
## Current Aim
Stop feature work on moderation, mood, and self-state until the state boundary is rebuilt. The live product goal still stands: Void should answer Discord, remember useful social/project context, retrieve GameCult history/source/lore, and run an unattended moderation/participation loop. The current implementation reaches that goal through JSON projection edits, legacy mirrors, and a swollen memory organ. That is architectural debt wearing a little paper crown.
The next priority is to remove JSON from the mutation loop entirely. Agents and scheduled workers should mutate typed CultCache-backed state through explicit tools/APIs, not by editing a whole-state JSON working copy.
## Smallest Coherent Architecture
### Durable State Model
- Postgres owns operational bot state:
- jobs
- audit events
- interaction memory
- provider runs
- public usage/rate limits
- Qdrant owns semantic vectors:
- Discord history vectors
- source/lore vectors
- `.voidbot/` owns operational files:
- raw Discord archive
- source archive manifest and repo shards
- provider artifacts/traces
- logs/status/backups
- One polymorphic CultCache-backed state authority owns private Void self-state through typed document kinds:
- `void.self_profile`: identity, stable values, voice/personality configuration
- `void.moderation_cursor`: reviewed Discord cursor, open room obligations
- `void.speech_receipts`: recent delivered replies and dedupe keys
- `void.thought_memory`: distilled memories, incubation threads, resonance summaries
- `void.scheduled_runtime`: sleep cycle, speaking pressure, last run summaries
- `void.candidate_interventions`: drafts or requests that may become speech
CultCache does not need a separate physical store per state type; it is already polymorphic. The intended split is ownership by typed document kind and mutation contract, not a herd of little stores in fake mustaches. Each durable fact still has one owner. No top-level mirrors. No editable shadow copy pretending to be a harmless view.
### Runtime State Model
- Discord request runtime is an immutable request context:
- prompt/message
- actor/guild/channel
- recent room transcript
- interaction profile
- optional situational read
- retrieval hints/results
- self-state summary projection
- Scheduled moderation runtime is a bounded pass context:
- current cursor window
- outstanding obligations
- selected memory/thought summaries
- selected repo/archive evidence
- proposed state mutations
- optional speech request
- Mood drift runtime is deterministic maintenance:
- read stable self profile
- update scheduled runtime pressure/sleep fields
- request memory maintenance through typed operations
Runtime context dies at the end of the pass unless an explicit typed mutation preserves a durable fact.
### Authoritative Boundaries
- Bot/worker/provider/RAG boundaries survive.
- CultCache self-state has one mutation authority: a typed state service over one polymorphic cache.
- Agents do not edit state files. They call tools such as:
- `record_reviewed_messages`
- `upsert_open_case`
- `close_open_case`
- `record_delivery_receipt`
- `append_distilled_memory`
- `merge_incubation_support`
- `queue_candidate_intervention`
- `retire_candidate_intervention`
- `update_sleep_cycle`
- `update_speaking_pressure`
- Model/agent output crosses the boundary as proposed operations, not as rewritten state.
- The state service validates, normalizes, dedupes, and writes.
### Boundary Messages
The clean crossing surface is small JSON command payloads, not a whole JSON state document:
```json
{
"operation": "upsert_open_case",
"sourceMessageId": "discord-message-id",
"status": "pending",
"summary": "What Void owes the room",
"lastTouchedAt": "2026-05-16T00:00:00.000Z"
}
```
```json
{
"operation": "append_distilled_memory",
"kind": "project_seam",
"subject": "AquariumSynthCSharp",
"summary": "Workflow cannot own the body.",
"evidenceRefs": ["source:..."],
"observedAt": "2026-05-16T00:00:00.000Z"
}
```
The runner can hand these to a CLI/MCP tool. The store decides what survives.
### Derived, Not Stored
- Self-state prompt summaries are derived from typed documents.
- Recent room context is derived from Discord archive and the current poll.
- Topic saturation is derived from incubation/memory support, not separately stored as another truth.
- Source coverage is derived from recent evidence refs and repo/archive metadata.
- Need-to-speak can keep a small pressure field, but its inputs remain receipts, candidates, and time since last speech.
- Legacy top-level mirrors are deleted. If a consumer needs a projection, render it on demand.
### Delete Because It Compensates For Bad Ownership
- `.voidbot/private/moderation-agent-state.json` as an editable working projection.
- `legacyJsonPath`/working-path duality for canonical self-state.
- Top-level mirror fields beside `moderation_runtime`.
- Any helper that mutates the JSON projection and commits it back.
- Prompt doctrine that tells the child agent how to manually preserve state shape.
- Regex/template police that try to repair semantic sludge after the store allowed sludge to become durable.
### Impossible By Construction
- An unattended agent cannot corrupt the entire self-state by rewriting one JSON file.
- Two fields cannot both claim to be the cursor.
- Repo activity cannot advance by bypassing the state service.
- Delivery receipts cannot exist only in a status file while state forgets the bot spoke.
- A projection cannot become authoritative.
- Unknown fields cannot silently become doctrine just because `.passthrough()` let them in.
## Current VoidBot Compared To The Ideal
- The bot/worker/provider/RAG/Postgres/Qdrant shape is basically sound.
- Source archive sharding is sound.
- Interaction memory being Postgres-backed is sound.
- The private moderation/self-state foundation is not sound:
- canonical MessagePack exists, but JSON is still a mutation surface
- schema validation is too permissive to protect invariants
- helpers mutate projection files and then commit whole-state changes
- legacy mirrors keep reintroducing stale truth surfaces
- `scripts/void-memory-organ.mjs` owns storage cleanup, semantic interpretation, vectors, incubation, sleep consolidation, identity crystallization, agency, and speech candidates in one file
- the scheduled moderation runner owns lifecycle through a huge prompt instead of a typed runner contract
Verdict: stop feature work and rebuild this foundation. The rest of the machine can keep running, but moderation/mood/self-state work should cut toward typed state tools before adding new behavior.
## Migration Plan
### Commit 1: Name The Boundary
- Add typed self-state domain interfaces in core.
- Define document types and operation payload types.
- Keep current storage untouched.
- Verification: typecheck only.
### 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.
### Commit 3: Add Typed Mutation CLI
- Extend `scripts/moderation-state-store.mjs` or replace it with a typed command surface for cursor, open cases, receipts, candidates, sleep, speaking pressure, and distilled memories.
- The CLI writes CultCache directly.
- Verification: fixture tests for each operation.
### Commit 4: Move Repo Activity Cursor Behind The Store
- Change `scripts/export-recent-repo-activity.mjs` so it never writes `.voidbot/private/moderation-agent-state.json`.
- It should emit observed activity and call/store a typed `update_repo_activity_cursor` operation.
- Verification: run with a temp fixture store; cursor advances without JSON projection writes.
### Commit 5: Replace Agent Whole-State Editing With Operation Output
- Scheduled moderation child may propose operations and speech, not edit state.
- Runner applies operations through the typed store.
- Keep old JSON runner behind a fallback flag for one commit only.
- Verification: dry run with `-NoPost` applies only allowed operations.
### Commit 6: Delete JSON Projection Authority
- Remove routine materialization/commit of `.voidbot/private/moderation-agent-state.json`.
- Keep an explicit debug export command if humans need to inspect state.
- Remove `legacyJsonPath` from normal paths.
- Verification: mood drift and moderation dry runs pass without touching JSON.
### Commit 7: Cut Legacy Mirrors
- Remove top-level mirror reconciliation.
- Migrate any consumer to typed projections.
- Delete mirror fields from canonical state.
- Verification: projection summaries still contain required information; no code references mirror keys.
### Commit 8: Split The Memory Organ
- Split deterministic cleanup from semantic interpretation.
- Keep vector/resonance/incubation maintenance behind typed state operations.
- Move speech-candidate queuing into a candidate intervention service.
- Verification: existing fixture state produces bounded, typed operations and no whole-state rewrite.
### Commit 9: Rebuild Scheduled Runner As A Phase Machine
- Replace prompt-owned lifecycle with explicit phases:
- poll chronology
- read typed state summary
- collect evidence
- ask model for decisions/operations
- validate/apply operations
- optionally send speech
- record receipts/cursor
- Verification: durable logs show each phase and mutation count.
### Commit 10: Resume Product Hardening
- Only after JSON mutation and legacy mirrors are gone:
- public-lane moderation policy
- admin/forensics surfaces
- restore drills
- provider/sandbox scaffolding cleanup
## Rules For Editing This Plan
- update this note when the larger sequence of work changes
- do not dump volatile minute-by-minute status here
- if a lesson changes future belief, record it in `state/evidence.jsonl`
- if the current next action changes, update `state/map.yaml` and `notes/fresh-workspace-handoff.md`