Also published on the docs website: AMS operations runbook (same content, rendered with search and the rest of the maintainer docs nav). This file remains the canonical source and ships inside the published
@loopover/minerpackage.
Operator-facing runbook for local SQLite state: what the concurrency guarantees actually mean, how to recover from corruption, what to do when two miner processes collide on the same files, and how schema upgrades migrate your on-disk ledgers after a package update.
Scope: AMS local stores only. For laptop/fleet deployment layout see
../DEPLOYMENT.md. For Grafana setup see #5190. For the optional hosted discovery plane seediscovery-plane-operator-guide.md. This runbook does not cover the self-hosted review stack (Orb/API/LoopoverDB).
Every miner keeps independent SQLite files under one state directory (default ~/.config/loopover-miner/, override with LOOPOVER_MINER_CONFIG_DIR). Each store has its own file, table, and optional per-store env override — see the table in ../README.md and env-reference.md.
Common files you will touch in incidents:
| File | Purpose |
|---|---|
laptop-state.sqlite3 |
Bootstrap metadata (loopover-miner init) |
claim-ledger.sqlite3 |
Soft issue claims on this machine |
event-ledger.sqlite3 |
Append-only manage-loop audit trail |
portfolio-queue.sqlite3 |
Per-repo portfolio queue |
run-state.sqlite3 |
Discover/plan/prepare phase markers |
attempt-log.sqlite3 |
Per-attempt coding-agent driver events |
prediction-ledger.sqlite3 |
Predicted gate verdicts for self-improve |
plan-store.sqlite3 |
Persisted MCP plan DAGs |
governor-ledger.sqlite3 |
Governor allow/deny/throttle decisions |
Files are created with 0700 directories / 0600 database files on first open.
Every store opened through local-store.js sets:
PRAGMA busy_timeout = 5000;(default 5000 ms; overridable via openLocalStoreDb(path, { busyTimeoutMs }) in tests only — production stores use the default.)
| Situation | Expected behavior |
|---|---|
Two short-lived writers on the same file (e.g. CLI command finishing while loop is idle, or Grafana reading while the miner appends) |
SQLite waits up to 5 seconds for the lock, then proceeds or surfaces database is locked |
Append-only ledgers (event-ledger, attempt-log, …) |
Writes use BEGIN IMMEDIATE (or equivalent single-statement atomicity) so sequence allocation cannot interleave |
| Claim / queue stores | INSERT … ON CONFLICT and UPDATE … RETURNING patterns avoid read-then-write races within one file |
Two long-running loopover-miner loop daemons on the same LOOPOVER_MINER_CONFIG_DIR |
Unsupported. busy_timeout reduces transient lock errors; it does not make multi-process loop workers safe on one volume |
Invariant: one active loop (or one intentional writer set) per state directory. Horizontal scale = isolated state dirs (separate compose projects, separate LOOPOVER_MINER_CONFIG_DIR, or the k8s StatefulSet pattern in ../DEPLOYMENT.md).
loopover-miner doctor --json
loopover-miner status --jsondoctor includes laptop-state-sqlite (file exists + readable) and state-dir-writable. It performs no network I/O.
Symptoms
database is locked/SQLITE_BUSYin logs or stderr- Duplicate or out-of-order event sequences after an unclean shutdown
- Two systemd units, two
docker compose --scale miner=Nreplicas, or a manualloopplus a supervisedloopsharing one config dir - Claims or queue rows flipping unexpectedly
Diagnosis
-
List processes using the state dir:
STATE_DIR="$(loopover-miner status --json | jq -r .stateDir)" ls -la "$STATE_DIR" # Linux: lsof +D "$STATE_DIR" 2>/dev/null || fuser -v "$STATE_DIR"/*.sqlite3 2>/dev/null
-
Confirm only one long-lived miner should own that directory.
-
Inspect soft claims and queue without mutating:
loopover-miner claim list --json loopover-miner queue list --json loopover-miner ledger list --json | tail -20
Remediation
-
Stop all but one miner process targeting that state dir (
systemctl stop,docker compose down, kill strayloop). -
If you need N parallel workers, give each an isolated state path — do not share one volume:
# Example: two isolated compose projects docker compose -p miner-a -f docker-compose.miner.yml up -d docker compose -p miner-b -f docker-compose.miner.yml up -dOr set distinct
LOOPOVER_MINER_CONFIG_DIRper worker. -
Re-run
loopover-miner doctor. If locks persist with a single process, see Ledger corrupted below. -
Claims are local bookkeeping only. Two miners on different machines claiming the same GitHub issue is a fleet coordination problem (duplicate-cluster adjudication in the engine), not something SQLite resolves — split state dirs and use operational claim hygiene.
Proactive tooling (#4872), not just the reactive "ledger corrupted" scenario below — run backup-miner.sh on a
schedule (cron, systemd timer, etc.) so a good restore point always exists before anything goes wrong.
-
scripts/backup-miner.sh— backs up every*.sqlite3file currently present underLOOPOVER_MINER_CONFIG_DIRinto a new timestamped directory, using SQLite's own online.backupcommand (safe even while the miner is running — see the corruption scenario's warning below about why a plaincpis not) plus aPRAGMA integrity_checkon each resulting file before it's kept. Stores discovered by glob, not a hardcoded list, so a newly added store is backed up automatically without this doc or the script needing an update.sh scripts/backup-miner.sh # Env overrides: LOOPOVER_MINER_CONFIG_DIR (source), LOOPOVER_MINER_BACKUP_DIR (default # $LOOPOVER_MINER_CONFIG_DIR/backups), LOOPOVER_MINER_BACKUP_RETAIN (default 7 — oldest backups beyond # this count are pruned after a fully successful run; a run with any failed store skips pruning so no older, # good backup is ever lost to make room for a bad one).
-
scripts/restore-miner.sh— the read side. Stop the miner first (this script does not detect a running process). Validates every store file in the chosen backup withPRAGMA integrity_checkbefore copying anything into place — a half-good backup can never produce a half-restored state directory. Requires an explicit--yesflag (it overwrites live state) and defaults to the newest backup when no directory is given:sh scripts/restore-miner.sh --yes # newest backup sh scripts/restore-miner.sh --yes /path/to/backups/<ts> # a specific one loopover-miner doctor --json # verify afterward
Also removes any leftover
-wal/-shmsidecar files from the live directory after restoring each store — those hold in-flight writes from before the restore, and leaving them in place would let SQLite silently replay stale pre-restore writes back on top of the freshly restored file on next open.
Symptoms
- Command throws
corrupted_*_row(corrupted_attempt_log_row,corrupted_governor_row,corrupted_plan_row,corrupted_prediction_row, …) loopover-miner doctorreportslaptop-state-sqlitenot readablesqlite3reportsdatabase disk image is malformed- Partial writes after disk full, forced kill during a migration transaction, or copying a live
.sqlite3while the miner is writing
Diagnosis
-
Identify which file fails (error message or env override path).
-
Read-only probe:
DB="$STATE_DIR/event-ledger.sqlite3" # example sqlite3 "$DB" "PRAGMA integrity_check;" sqlite3 "$DB" "PRAGMA user_version;"
-
Check filesystem: disk space, permissions (
0600file,0700parent), backup tools copying mid-write.
Remediation
-
Stop the miner before any file surgery.
-
Backup the whole state directory (even damaged files help post-mortems):
cp -a "$STATE_DIR" "${STATE_DIR}.bak.$(date +%Y%m%d%H%M%S)"
-
Choose a recovery tier:
Tier When Action A — single store reset One ledger is corrupt; others healthy; you accept losing that store's history Remove only the bad *.sqlite3(and any-wal/-shmsiblings). Next command recreates an empty store.B — restore from backup You have a recent backup from backup-miner.sh(see Backup and restore above)Stop miner → sh scripts/restore-miner.sh --yes→ restart.C — full re-init Multiple files suspect or state is disposable Archive dir → loopover-miner init→ reconfigure env/goals. Rebuild claims/plans from GitHub metadata as needed. -
Never copy a live SQLite file from a running miner as backup — stop first, or use SQLite's
.backupcommand:sqlite3 "$DB" ".backup '${DB}.safe-copy'"
-
After recovery, run
loopover-miner doctor --jsonand spot-check read-only listings (claim list,ledger list).
Append-only stores do not repair individual bad rows in place — corrupted payload JSON is rejected on read by design so bad data cannot silently propagate.
How upgrades work
Stores use the lightweight schema-version.js convention (#4832):
- Bootstrap
CREATE TABLE IF NOT EXISTS …is schema version 1 (BASELINE_SCHEMA_VERSION). - Each store may register post-baseline migrations;
applySchemaMigrationsruns pending steps on every open. - Version is stamped in SQLite
PRAGMA user_version. - Migrations run once, in order, inside a transaction; a failed migration rolls back and retries on next open.
- Downgrade is not supported — older miner versions may not read files written by newer migrations.
Operator checklist
-
Before upgrading the
@loopover/minerpackage (npm, image tag, or git pull):loopover-miner doctor --json > /tmp/miner-pre-upgrade-doctor.json STATE_DIR="$(loopover-miner status --json | jq -r .stateDir)" tar -czf "/tmp/loopover-miner-state-$(date +%Y%m%d).tar.gz" -C "$(dirname "$STATE_DIR")" "$(basename "$STATE_DIR")"
-
Stop supervised loops (
systemctl stop loopover-miner.service,docker compose stop miner, etc.). -
Install the new version (
npm install -g @loopover/miner@latest, rebuild image, …). The CLI prints a one-line npm upgrade nudge when behind registry latest — informational only. -
Migrate, before starting any miner process, so every existing store is brought up to date in one deliberate pass instead of relying on whichever command happens to open a given store first:
loopover-miner migrate --json
(Pending migrations still apply automatically on first open regardless — e.g.
portfolio-queueaddsleased_atwhen upgrading from pre-#4827 files —migrateis the proactive, explicit alternative to waiting for that implicit path. A store file that hasn't been created yet is reported as skipped, not created;migratenever bootstraps fresh state.) -
Start one miner process, then verify:
loopover-miner doctor --json loopover-miner status --json loopover-miner migrate --json # re-run: every store should now report "up-to-date" -
If a migration throws on startup, do not delete files immediately — restore the pre-upgrade tarball, pin the previous package version, and file an issue with the failing
user_versionand store filename.
Rolling fleet upgrades: upgrade and restart one worker/state dir at a time so isolated workers never share a directory mid-migration.
../DEPLOYMENT.md— laptop vs fleet, volumes, systemd, scaling rules../README.md— store inventoryenv-reference.md— per-store path overridescoding-agent-driver.md— attempt log semantics- #5190 — Grafana + SQLite ledgers (observability doc)
discovery-plane-operator-guide.md— optional hosted plane (distinct from local ledger ops)