Skip to content

Operations

Where kew stores data, how to back it up and restore it, how retention works, and what forward-only migrations mean for upgrades.

Where data lives

kew keeps its entire durable record in one SQLite file:

<logging.dir>/dispatch.db          # all tables (see below)
<logging.dir>/dispatch.db-wal      # WAL file — exists at runtime
<logging.dir>/dispatch.db-shm      # shared-memory index — exists at runtime

logging.dir defaults to ./logs/dispatch (relative to your repo root). Override it in kew.toml:

[logging]
dir = "./logs/dispatch"   # default

or at runtime with the KEW_LOG_DIR (or DISPATCH_LOG_DIR) environment variable.

dispatch.db holds five logical datasets in one file:

Table Contents
runs Every dispatch run — outcome, cost, tokens, model, branch
audit_events Hash-chained governance event log (see Audit log & tamper-evidence)
policy_snapshots Retained governance config snapshots bound to each dispatch
loops Active and closed loop records
interactive_usage Off-chain interactive usage from kew ingest file / kew ingest serve (schema v15, #256); surfaced in kew report but not covered by kew audit verify
epic_graph_cache Off-chain per-epic cache of the last provider graph fetch (schema v28, #894); lets the Linear driver / kew epic status short-circuit the GraphQL fetch when tracker events show nothing changed. A disposable performance artifact, never covered by kew audit verify
review_rounds Off-chain per-node governed-review state (round counter + per-round {head_sha, verdict, sidecar_digest, sidecar_path}, schema v29 + v32, #977/#1030); lets the driver resume the round count across a restart and locate a NEEDS_CHANGES round's stored findings to remediate against. Operational state — the attested per-round fact is the adversarial_review_verdict audit event — so it is never covered by kew audit verify

The WAL (-wal, -shm) sidecars exist only while kew is running. A clean shutdown checkpoints WAL into the main file. For backups, kew backup uses SQLite's online-backup API, so the WAL is always included consistently regardless of checkpoint state.

Per-run log files (dispatch-*.log, dispatch-*.events.jsonl) live alongside dispatch.db in logging.dir and are not included in a backup by default (opt-in with --include-logs).

Ledger identity and fork refusal

kew records the canonical ledger for a project in a machine-local registry at .git/kew/ledger.json (in Git's common directory, so the primary checkout and all worktrees share it). Outside Git, the registry is .kew/ledger.json under the project root. The registry binds the canonical project root, database path, and ledger UUID; it is the authority for which dispatch.db belongs to the project.

Run kew audit verify as the routine health check. A healthy result exits 0 and reports Chain intact: <N> event(s).; when anchors exist, it also reports their match and anchor-chain status. Identity checks happen before the database is opened. kew fails closed with WRONG-LEDGER if the configured database, registry, UUID, project, or anchors disagree, and with REGISTERED-LEDGER-MISSING if recorded ledger history exists but the canonical registry or its registered database is missing. Do not copy, rename, or edit registry files to bypass either refusal.

Use explicit rebind only after identifying the one ledger that should be canonical:

# Recover an identity-aware ledger (use the complete canonical UUID):
kew audit ledger-rebind --target /path/to/dispatch.db \
  --expected-ledger-id 01234567-89ab-cdef-0123-456789abcdef \
  --reason "recover moved canonical ledger" --yes

# Adopt a legacy ledger (use the full SHA-256 of its latest legacy anchor):
kew audit ledger-rebind --target /path/to/dispatch.db \
  --expected-anchor-hash <64-character-latest-anchor-hash> \
  --reason "adopt verified legacy ledger" --yes

ledger-rebind is an explicit recovery or adoption decision, never a way to merge, splice, or reconcile two chains. It requires exactly one full expected identity (--expected-ledger-id) or legacy anchor hash (--expected-anchor-hash), plus a nonempty --reason and --yes; mismatches are refused before the registry is changed.

Backup

kew backup snapshots dispatch.db and kew.toml into a single compressed archive using SQLite's online-backup API — consistent and safe to run against a live database.

kew backup                          # write kew-backup-<ts>.tar.gz to logging.dir
kew backup -o /backups/             # write to a different directory
kew backup -o /backups/daily.tar.gz # explicit archive name
kew backup --include-logs           # also bundle per-run .log/.events.jsonl files

The archive contains:

File Always included
dispatch.db Yes
kew.toml Yes (if found)
manifest.json Yes — schema version, chain head, run/event counts, chain integrity flag
logs/ Only with --include-logs

What is NOT captured: environment variable overrides (KEW_LOG_DIR, KEW_RUNNER, etc.) and any config values supplied only via env vars are not persisted in the snapshot. If you rely on env-var overrides in CI, document them alongside your backup cadence.

See kew backup for the full flag reference.

Restore

kew restore validates an archive's schema compatibility and chain integrity, then places the DB non-destructively.

# Restore into the configured log dir (reads logging.dir from kew.toml):
kew restore /backups/kew-backup-20260624T120000Z.tar.gz

# Already have a dispatch.db? Pass --force; the existing file is moved aside:
kew restore --force kew-backup-20260624T120000Z.tar.gz

# Also restore kew.toml (existing one moved aside):
kew restore --force --with-config kew-backup-20260624T120000Z.tar.gz

# Skip chain verification (e.g. archive itself records chain_ok=false):
kew restore --no-verify kew-backup-20260624T120000Z.tar.gz

Stop kew before restoring. kew restore refuses to overwrite a database that another kew process is holding open. The check is a short BEGIN IMMEDIATE attempt; if it fails, the restore aborts with exit code 1 and a clear message. The automatic lock check is best-effort — under WAL a read-only connection (e.g. an idle TUI) does not hold a write lock, so it may pass the check; stopping kew is the real guarantee.

Non-destructive guarantees:

  • If dispatch.db exists at the target, restore refuses unless --force is passed.
  • With --force, the existing DB is moved aside to dispatch.db.bak-<ts> (and the -wal/-shm sidecars likewise) before the restored file is placed.
  • With --with-config, the existing kew.toml is moved to kew.toml.bak-<ts> before the archived one is placed.
  • The restored DB is schema-migrated forward to the running kew's schema version on first open (the migration runs once at restore time).

See kew restore for the full flag reference.

Suggested cadence

A daily backup run is enough for most teams. Example cron or CI step:

# cron: daily at 03:00
0 3 * * * cd /path/to/your/repo && kew backup -o /backups/kew/

In a GitHub Actions workflow:

- name: kew backup
  run: kew backup -o artifacts/
- uses: actions/upload-artifact@v4
  with:
    name: kew-backup-${{ github.run_id }}
    path: artifacts/kew-backup-*.tar.gz
    retention-days: 30

Rotate old archives with standard tools (find /backups/kew/ -mtime +30 -delete).

Retention

kew never prunes run rows automatically. retention_days sets a cutoff, but it is only enforced when you explicitly ask for it:

[logging]
dir = "./logs/dispatch"
retention_days = 90   # 0 = keep forever (default)

With retention_days > 0, run kew export --purge to delete records older than the cutoff. The purge happens after the export completes, so you always keep an exported copy first. There is no background or on-dispatch pruning — nothing is deleted unless you run that command (and with retention_days = 0, --purge refuses and tells you to set a cutoff first).

Cache Amortization Falsification Probe (D3)

Decision D3 (spec docs/superpowers/specs/2026-07-23-context-memory-layers-design.md): "Falsification test ships in Phase 0: two dispatches with equal prefix_key <5 min apart; if run 2's first-turn cache_read_tokens ≈ 0, the amortization language dies and D becomes purely per-run accounting."

This probe answers whether kew's fleet-amortization claim is real: do two agents dispatched in quick succession actually share a prompt-cache warm line, or does each pay full input cost?

What the probe measures

prefix_pair_probe from kew.analytics finds all pairs of completed runs where:

  • both runs share the same prefix_key (identical stable prompt prefix), and
  • the second run started within the provider's cache TTL of the first (default 300 s for Anthropic; pass ttl_seconds= to override).

Still-pending runs are excluded: their token counts are placeholders that would inject a spurious cache-miss verdict.

For each qualifying pair it returns run2_first_turn_cache_read_tokens — the cache-read tokens on the first turn of the second run, parsed from that run's stream-json transcript (log_file). First-turn semantics are essential: every multi-turn run self-caches from turn 2 onward, so the run-level total is almost always large and would report "amortization is real" even when the first turn served zero cross-agent cache read — the exact D3 kill scenario. Only the first turn's cache reads reflect a warm line left by a sibling run. A warm hit produces a large value; ≈ 0 means no cross-agent cache was served. The value is None when the second run's transcript is unavailable (the first-turn value cannot be determined — do not fall back to the run total).

Operator procedure

  1. Dispatch the same trivial issue twice, under 5 minutes apart, against the same runner and model so both runs share a prefix_key. A dummy issue that always takes < 2 min is ideal (e.g. "echo hello").

  2. Wait for both runs to finish, then open a Python REPL or script in your repo root:

from kew.run_store import SqliteRunStore
from kew.analytics import prefix_pair_probe

store = SqliteRunStore("logs/dispatch/dispatch.db")
pairs = prefix_pair_probe(store, ttl_seconds=300)
for p in pairs:
    print(
        f"run1={p['run1_id']}  run2={p['run2_id']}"
        f"  delta={p['delta_seconds']:.0f}s"
        f"  run2_first_turn_cache_read={p['run2_first_turn_cache_read_tokens']}"
    )
store.close()
  1. Interpret the result:
run2_first_turn_cache_read_tokens Verdict
≫ 0 (e.g. tens of thousands) Cache warm — amortization is real
≈ 0 Cache miss — delete fleet-amortization language from the spec (D3)
None Run 2's transcript is missing — re-run the probe with a completed run that has a log_file

Verdict recording

Record the outcome as a comment on spec decision D3 in docs/superpowers/specs/2026-07-23-context-memory-layers-design.md. If the result is a cache miss, remove all fleet-amortization language from that spec; leave per-run accounting only.

Switchyard passthrough operations

Switchyard is disabled by default. Keep [inference_transport].mode = "direct" until the human-reviewed passthrough UAT gate (docs/audit/2026-08-03-switchyard-passthrough-uat.md in the repository — internal audit packs are not published) records a GO for the exact Kew commit, runner, model, Switchyard binary/config digests, and corpus being used. A gate for one identity does not authorize another.

Startup and preflight

Before either lifecycle, verify the audit chain and configuration while the fleet is stopped:

kew stop --reason "Switchyard evidence or maintenance window"
kew audit verify

In external mode (managed = false), the endpoint operator owns startup, upstream credentials, availability, logs, retention, and shutdown. Start it by the reviewed vendor procedure, then run kew health --json. Kew performs bounded /health and /v1/models checks and refuses an absent route. It does not start, restart, or stop an external endpoint.

In managed mode (managed = true), do not start the configured binary by hand. For each Kew run, Kew revalidates the absolute executable's SHA-256 and reported version, reads the route config once, writes those bytes to a private run-owned snapshot, launches one new process group on the configured loopback endpoint, and waits up to startup_timeout_seconds. The runner starts only after health and exact-route admission. Final release, cancellation, or startup failure terminates and reaps the process group and deletes the snapshot.

Managed mode supplies the proxy a deny-by-default process environment. Routes that need no upstream authentication leave credential_env empty. For authenticated routes, export the approved value in the host environment and declare only its name in credential_env. Kew requires that declaration to match the exact route's api_key_env references, then launches the proxy with only those matched variables. Never place a secret value in Kew or Switchyard configuration. See Switchyard credential boundary.

Canary and benchmark window

The live commands are evidence tools, not routine dispatch checks:

install -d -m 700 /private/approved/switchyard-canary
kew canary --runner <runner> --model <model> \
  --record /private/approved/switchyard-canary --json
kew switchyard benchmark \
  --corpus /private/approved/switchyard-corpus.json \
  --runner <runner> --model <model> > /private/approved/benchmark.json

Create the record directory and retention deadline before the window. Run one approved model sequentially, exclude all other traffic from the managed endpoint, and stop on any identity/digest mismatch. Benchmark exit 1 means failed evidence; exit 2 means the request was refused or unavailable. Neither is partial rollout approval. Preserve only sanitized results and digests in the UAT report, then delete the raw canary directory by its deadline.

Failure response

Symptom Meaning and response
Config load refusal Fix the endpoint origin, exact version, absolute paths, binary digest, or fallback = "refuse"; do not weaken validation.
Credential declaration mismatch or unavailable Stop before launch. Confirm the exact route target/client references, reviewed name-only credential_env, and nonempty host variable. Never add unrelated names or copy values into configuration.
wrong_version, digest mismatch, or dry-run failure Treat as provenance failure. Keep direct mode and obtain a reviewed build/config.
unhealthy, missing_route, TLS error, or startup timeout No runner is admitted. Repair the owned endpoint; never retry through direct mode under the same admitted run.
transport_unavailable during a run The proxy was lost after admission. Treat the run as failed evidence; inspect sanitized Kew status and operator proxy logs separately.
Benchmark counter reset, overlap, or unexpected request delta All proxy observations for the window are invalid. Runner usage remains readable, but rerun the entire approved window.
Credential/authentication failure Permanent for that attempt. Revoke if exposure is suspected; do not print credentials or retry with a broader credential.
Raw evidence disclosure or unexpected model/route Stop the fleet, revoke affected credentials, quarantine/delete raw material under incident policy, and keep the rollout gate on HOLD.

Shutdown

Managed processes are run-scoped and close automatically. Confirm the canary's cleanup_complete check and benchmark exit status; a missing cleanup result is a failed gate. For an external endpoint, first stop Kew admissions, wait for or terminate its clients under the normal incident procedure, then use the endpoint operator's bounded shutdown procedure. Never kill processes by a broad name match.

After either lifecycle, run kew audit verify, reduce evidence, delete the raw record directory by its recorded deadline, and record deletion in the UAT report. Proxy logs have their own operator-defined retention and must not be copied into Kew's audit chain.

For routine rotation, latch dispatch first, allow or terminate existing runs, replace the provider key in the host environment, and resume only after a new managed canary. For suspected disclosure, revoke the provider key immediately, run kew stop, confirm the managed process is gone, inspect only sanitized logs/evidence, rotate, and repeat the full approval window. A same-user or privileged OS process may inspect a live child environment; treat host-account compromise as credential exposure.

Switchyard rollback

  1. Run kew stop --reason "Switchyard rollback" to terminate in-flight Kew agents and latch new dispatch.
  2. Change [inference_transport].mode to "direct". Leave historical run and audit rows intact.
  3. Stop an external proxy with its operator procedure. Managed processes are released with their Kew runs; confirm no approved endpoint remains in use.
  4. Run kew health and verify the Switchyard check is skipped, then run one approved direct runner canary.
  5. Run kew audit verify. Only a human operator should run kew resume after direct health, canary, and audit integrity pass.

Rollback does not erase prior transport evidence: transport fields are additive and old rows remain readable. Do not re-enable Switchyard until a human approves the evidence and any changed binary, route config, model, runner, or corpus digest.

V1 limitations

  • Passthrough is the only rollout-eligible route. Random, classifier, stage, and automatic fallback routing remain disabled.
  • Codex and Claude Code are the only proven transport adapters. Other runners refuse before launch.
  • Health proves endpoint/route presence, not provider authorization or future request success.
  • Proxy metrics are process-wide and are never authoritative per-run billing.
  • Managed processes are deliberately not reused between Kew runs.
  • Kew does not install/update Switchyard or automatically delete canary raw records.

Migration safety

kew uses forward-only SQLite migrations. Each release can only add schema steps, never remove them. Opening a database that was written by an older kew version applies any missing migration steps automatically — you never need to run a migration command manually.

Opening a database written by a newer kew is refused with a clear error:

SchemaTooNewError: dispatch.db is schema vN; this kew supports up to vM. Upgrade kew to open it.

kew never silently corrupts a database it cannot understand. To resolve this, upgrade the kew CLI to match or exceed the version that wrote the database.

The same schema guard applies during kew restore: if the archive's manifest records a schema version higher than the running kew supports, the restore is refused before any files are touched.