VoidBot is a Discord-native assistant for GameCult.
It can answer from archived Discord history, indexed GameCult repos, and Aetheria lore, then hand deeper work off to Codex when Discord stops being a sane place to do it. The current build already supports real Discord replies, semantic retrieval, source and lore indexing, and Codex handoff flows.
## What VoidBot Can Do
- answer owner questions through a Discord-safe `codex exec` lane
- use real MCP tools for archived Discord history, repo search, lore lookup, and owner notifications
The durable local state is split on purpose: Postgres holds jobs, audit events, and interaction memory, while `.voidbot/` keeps artifacts, archives, logs, and detached indexing status.
There is also a project-memory spine now for future Codex sessions: `state/map.yaml`, `state/scratch.md`, `state/evidence.jsonl`, `notes/fresh-workspace-handoff.md`, and the helper CLIs under `tools/`. The point is to rehydrate from canonical files instead of pretending the transcript will stay coherent forever.
`LOCAL_LLM_SOCIAL_READ_OLLAMA_MODEL` is optional. Leave it blank to reuse the main local reply model, or set it when you want the room-reading sidecar on a different model than the public/local response lane.
The default shape is simple on purpose: everyone gets the base cooldown and daily cap, listed users or roles can be made unlimited, and listed users or roles can get a boosted lane that cuts cooldowns and multiplies daily allowance by `VOID_USAGE_BOOST_MULTIPLIER` (default `10`).
VoidBot now keeps queue state, audit events, and interaction memory in Postgres. `npm run stack:start` will launch the local Docker Postgres container automatically for the default local DSN, but you can start it yourself if you want the database up first:
```bash
docker compose -f infra/postgres/docker-compose.yml up -d
Qdrant is the recommended vector backend now. It avoids the pain that comes from stuffing everything into local JSON and then acting surprised when the file turns into a small moon.
Windows helper path:
```bash
npm run qdrant:setup
```
Normal day-to-day startup after that:
```bash
npm run qdrant:up
```
Or run Qdrant manually:
```powershell
docker pull qdrant/qdrant
docker volume create voidbot_qdrant_storage
docker run -p 6333:6333 -p 6334:6334 -v voidbot_qdrant_storage:/qdrant/storage qdrant/qdrant
If you do not want a reboot or fresh logon to leave the bot dead on the floor, install the startup task once:
```bash
npm run state:install-startup-task
```
That creates the `VoidBot Stack Startup` scheduled task, which runs `scripts/start-voidbot-stack.ps1` at logon through the same hidden launcher shim the other ops tasks use.
For `owner_codex`, the worker also needs a working local Codex CLI. If plain `codex` is not invokable from Node on your machine, point `CODEX_EXECUTABLE` and `CODEX_EXEC_ARGS` at the actual install. `CODEX_MODEL` pins the owner Discord lane to a specific model; the default is `gpt-5.4` so older local Codex installs do not faceplant on `gpt-5.5`.
-`@Void what did we decide about the website roadmap last year?`
-`@Void summarize prior discussion about ship movement in nautical`
-`/ask provider:local_llm question: where is the MCP server registering search_sources?`
-`/ask provider:local_llm question: what does Aetheria lore say about FTL corporations?`
-`/search-history query: favorite algorithm`
## How The Main Flows Work
### Owner Discord flow
Default mode is `local_exec_owner_only`:
1. The owner uses `@Void ...` or `/ask`.
2. The bot queues an auto-approved owner job.
3. The worker runs `codex exec` in a read-only, Discord-safe lane.
4. Codex can call Void MCP tools such as `search_history`, `get_message_context`, `search_sources`, `get_source_context`, and `list_indexed_repos`.
5. If the answer is short and safe, the worker posts it back to Discord.
6. If the task needs broader tools, file edits, or a deeper investigation, the worker posts a handoff notice and writes a bundle under `.voidbot/artifacts/<job-id>/`.
`/queue-codex` skips the direct-reply attempt and forces the handoff path.
### Manual fallback
`manual_package` still exists when you want the stricter, explicit workflow:
1. Queue a job.
2. Approve it with `/approve-job`.
3. Let the worker write `request.md` and `request.json`.
4. Run the deeper work manually.
5. Approve any final public reply separately.
### Local Ollama flow
The `local_llm` provider uses Ollama chat completion plus a bounded host-managed read-only tool loop. It can use:
-`search_history`
-`get_message_context`
-`search_sources`
-`get_source_context`
-`list_indexed_repos`
Unlike the owner Codex lane, this is not MCP. It is a tighter host-controlled loop and deliberately excludes side-effecting tools.
Project, repo, and lore questions should lean on the source-side lookup tools when the answer depends on indexed repo or lore facts. The providers now treat source-grounding hints as advisory instead of letting a dumb lexical hall monitor veto the whole reply before the model can think.
- bot-directed prompts stay in the raw archive but are excluded from default semantic history indexing and lexical fallback, so the bot stops poisoning its own memory with repeated summons
Source indexing is incremental when you run it. The pipeline crawls the repo, compares the current documents to the archived source snapshot, deletes vectors for changed or removed source IDs, and upserts only the changed chunks.
Configure:
-`SOURCE_REPO_ROOT`
-`SOURCE_REPO_PATTERNS`
-`SOURCE_REPO_INCLUDE_PREFIXES`
-`RAG_SOURCE_ARCHIVE_PATH`
-`RAG_SOURCE_QUERY_INSTRUCTION`
Index everything that matches your repo patterns:
```bash
npm run rag:index-sources
```
Target specific repos:
```bash
npm run rag:index-sources -- --repo gamecult-site,AetheriaLore
```
Force a clean reindex for specific repos:
```bash
npm run rag:index-sources -- --repo AetheriaLore --force
That keeps `AetheriaLore` focused on the useful subtree instead of indexing every random byproduct in the repo.
### Git push hook for source indexing
If you want local pushes to kick off source reindexing automatically:
```bash
npm run rag:install-push-hooks
```
Git does not provide a client-side `post-push` hook, so VoidBot installs a chained `pre-push` hook instead. That hook returns quickly, launches a detached incremental reindex for the pushed repo, and preserves any existing `pre-push` hook by backing it up to `pre-push.voidbot.prev` and running it first.
VoidBot now also reconciles the repo set automatically:
-`npm run stack:start` runs `npm run rag:reconcile-sources` before the bot and worker come back
-`npm run state:check` compares discovered repos under `SOURCE_REPO_ROOT` to the indexed archive and will launch detached repair work if new repos exist
- the reconcile pass refreshes hooks, indexes newly discovered repos, and prunes stale repo shards for repos that no longer exist under the configured root
The old “install hooks once and hope forever” story is dead. New repos under the configured root should get picked up by normal startup and watchdog operations without manual babysitting.
### Source archive layout
The source archive is no longer one giant `source-documents.json` blob trying to die dramatically at scale.
-`RAG_SOURCE_ARCHIVE_PATH` now stores a small repo-summary manifest
- per-repo source documents live under the sibling shard directory `source-documents.repos/`
That keeps incremental repo writes bounded to the repo that actually changed instead of rewriting the whole corpus every time one new project appears.
Manual local state backups now have a real path instead of vibes:
```bash
npm run state:backup
```
Optional label:
```bash
npm run state:backup -- -Label before-big-reindex
```
That backup contains:
- a `pg_dump` custom-format dump for Postgres jobs, audit events, and interaction memory
- Qdrant snapshots for every collection currently on the local node
- copies of `.voidbot/rag/*` so archive-backed rebuild provenance does not evaporate
Backups are written under `.voidbot/backups/<timestamp>[-label]/` with a `manifest.json`.
Restore the newest backup:
```bash
npm run state:restore
```
Restore a specific backup directory:
```bash
npm run state:restore -- -BackupPath E:\Projects\VoidBot\.voidbot\backups\<timestamp>
```
The backup and restore scripts stop the local bot/worker first and restart them afterward if they were already running. Because the local Qdrant node is shared, the backup path snapshots every collection on that node, not just the two VoidBot ones.
- compresses the resulting backup directory into a zip archive
- uploads it over `sftp`
- keeps a bounded number of scheduled local backup directories
- prunes older remote zip archives after upload
- writes status to `.voidbot/status/offsite-backup.json`
- appends logs to `.voidbot/logs/offsite-backup.log`
Install the daily scheduled task:
```bash
npm run state:install-offsite-task
```
Defaults come from `.env` / `.env.example`:
-`OFFSITE_BACKUP_SSH_TARGET`
-`OFFSITE_BACKUP_REMOTE_WINDOWS_DIR`
-`OFFSITE_BACKUP_REMOTE_SFTP_DIR`
-`OFFSITE_BACKUP_LOCAL_KEEP_LATEST`
-`OFFSITE_BACKUP_REMOTE_KEEP_LATEST`
-`OFFSITE_BACKUP_LABEL`
-`OFFSITE_BACKUP_TASK_NAME`
-`OFFSITE_BACKUP_TASK_TIME`
The scheduled task uses `Interactive` logon on Windows so it can see the current user's SSH key material. That means the backup job is intended for a workstation session that stays logged in; it is not pretending to be a hardened headless backup appliance.
If you want an actual local dashboard instead of spelunking JSON by candlelight:
```bash
npm run state:dashboard
```
That renders `.voidbot/status/operations-dashboard.html`, opens it in your default browser, and auto-refreshes every 10 seconds. The watchdog now also writes a live run-state file while it is executing, so a hidden task that wedges should stay visibly stuck on its last known step instead of failing in perfect silence.
That helper exists because semantic `search_history` is good at recall and bad at simple heartbeat duty. The moderator loop can use it to see what changed since its last cursor, then fall back to `search_history`, `get_message_context`, `list_indexed_repos`, `search_sources`, and `get_source_context` when it wants deeper social or project context.
npm run moderation:recent-repo-activity -- --hours 96 --max-commits 3
```
That gives the loop a compact digest of recent commit motion across every tracked source repo, so Void can notice the broader experiment zoo instead of only sniffing whatever one thread happened to say out loud.
The moderator state is now Ghostlight-shaped on purpose: identity, canonical state, goals, memories, perceived overlays, and then a `moderation_runtime` block for cursors, thought lanes, bridge syntheses, archive excursions, repo-activity sweeps, novelty checks, speaking bias, and candidate interventions. The canonical store now rides through the local `vendor/cultcache-ts` submodule using schema-backed MessagePack, while the ignored JSON file under `.voidbot/private/` is only the editable working projection the unattended Codex loop sees.
Within that runtime block, the unattended loop now keeps parallel analytic and associative thought lanes plus a bridge that tracks syntheses, topic saturation, and unresolved tensions, so one rewarding theme does not get to annex the whole mind by squatter's rights.
The real loop now runs as a local scheduled task every 15 minutes, using `scripts/run-void-moderator-rumination.ps1` to launch `codex exec` inside the VoidBot workspace with the usual MCP/tool surface. That way it can actually think about GameCult projects, archived Discord seams, indexed repos, recent cross-repo commit motion, and lore instead of sitting inside a decorative heartbeat card pretending to be alive.
The loop is also explicitly biased toward brief herald notes when a fresh repo sweep reveals a grounded convergence across active projects. The point is not to keep a perfect private diary; it is to occasionally say something useful about the broader swarm that no narrower worker can see.
There is also a separate five-minute mood-drift task now. It nudges the shared personality-vector activations with Perlin-shaped drift, damps them back toward their means according to plasticity, tracks how recently Void actually spoke, and feeds a little speak/confession/novelty pressure into the same self-state so the herald does not stay emotionally flat between room scans. That same organ also keeps a four-hour sleep cycle with one-hour naps, so the state knows when Void is supposed to turn inward instead of performing bright little office tricks.
During those naps, the moderation loop is told to treat the pass as maintenance: distill memory, prune clutter, and write one salient dream memory when the archive or repo weather compresses into something worth keeping. The dream layer lives in the same ignored state file rather than some second mystical scrapbook.
Directly invoked live replies now also read that same private moderation state as a distilled self-state attachment, so the summoned Void and the scheduled ruminating/participating Void share one evolving personality/state layer instead of roleplaying continuity from scratch each time. When the sleep projection says Void is napping, the direct reply path skips the expensive room-read sidecar, avoids normal attentive service-work, and answers in brief sleepy grumbles instead.
The moderation loop now speaks through the local bot token instead of approval-gated side-effecting MCP tools, so unattended runs can DM or post without making Codex stop and ask your permission every time it finds a mouthful of words.
VoidBot is now large enough that transcript memory alone is a stupid way to steer it. The repo carries a small canonical project-state scaffold for future sessions:
-`state/map.yaml`: slow-changing canonical project map and current next action
-`state/scratch.md`: disposable one-subgoal working memory
Some commands are full working paths. Some are still thin MVPs. They exist because the structure is real, not because the system is pretending to be finished.