@kontextmind/kxm 0.7.97 → 0.7.99

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.kxm/workflows/default.yaml +2 -0
  3. package/CHANGELOG.md +46 -2
  4. package/docs/concepts/architecture.md +1 -1
  5. package/docs/concepts/data-and-storage.md +1 -1
  6. package/docs/contracts/routing.md +1 -1
  7. package/docs/contributing/test-matrix.md +6 -5
  8. package/docs/guides/peer-messaging.md +2 -2
  9. package/docs/operations/backup-and-restore.md +43 -24
  10. package/docs/operations/deploy.md +1 -1
  11. package/docs/reference/cli-reference.md +65 -28
  12. package/docs/reference/config-reference.md +24 -13
  13. package/docs/reference/harness-routing.md +3 -3
  14. package/docs/reference/http-api.md +2 -1
  15. package/docs/reference/tools.md +3 -3
  16. package/docs/start/first-workflow.md +4 -4
  17. package/docs/start/quickstart-claude-code.md +2 -2
  18. package/package.json +1 -1
  19. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  20. package/plugins/kxm/dist/claude-hook.js +4 -1
  21. package/plugins/kxm/dist/cli.js +407 -156
  22. package/plugins/kxm/dist/client.js +9 -0
  23. package/plugins/kxm/dist/core.js +9 -3
  24. package/plugins/kxm/dist/extension.js +13 -1
  25. package/plugins/kxm/dist/mcp-server.js +14 -2
  26. package/plugins/kxm/dist/runtime-supervisor.js +231 -48
  27. package/plugins/kxm/dist/runtime.js +415 -92
  28. package/plugins/kxm/dist/server.js +19 -1
  29. package/plugins/kxm/package.json +1 -1
  30. package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +4 -3
  31. package/plugins/kxm/skills/kxm-peer/SKILL.md +5 -3
  32. package/plugins/kxm/skills/kxm-project-setup/SKILL.md +3 -2
  33. package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +3 -0
  34. package/plugins/kxm/skills/kxm-runs/SKILL.md +8 -7
  35. package/plugins/kxm/src/cli/project.ts +22 -13
  36. package/plugins/kxm/src/cli/system.ts +22 -1
  37. package/plugins/kxm/src/cli.ts +14 -4
  38. package/plugins/kxm/src/client.ts +10 -0
  39. package/plugins/kxm/src/commands.ts +10 -1
  40. package/plugins/kxm/src/database.ts +210 -36
  41. package/plugins/kxm/src/engine.ts +117 -2
  42. package/plugins/kxm/src/extension.ts +1 -0
  43. package/plugins/kxm/src/harness.ts +29 -0
  44. package/plugins/kxm/src/hub.ts +12 -0
  45. package/plugins/kxm/src/init-guide-setup.ts +43 -28
  46. package/plugins/kxm/src/mcp-server.ts +1 -1
  47. package/plugins/kxm/src/oneshot-producer.ts +16 -8
  48. package/plugins/kxm/src/prices.ts +33 -2
  49. package/plugins/kxm/src/routing.ts +13 -7
  50. package/plugins/kxm/src/studio-layout.ts +5 -4
  51. package/plugins/kxm/src/template.ts +31 -0
  52. package/plugins/kxm/src/worktree-witness.ts +71 -0
  53. package/schemas/backup-manifest.schema.json +33 -0
@@ -11,7 +11,7 @@
11
11
  "name": "kxm",
12
12
  "source": "./plugins/kxm",
13
13
  "description": "Durable workflows, peer agents, and kxm tui",
14
- "version": "0.7.97",
14
+ "version": "0.7.99",
15
15
  "category": "development",
16
16
  "tags": ["kxm", "multi-agent", "workflows", "mcp"]
17
17
  }
@@ -7,6 +7,8 @@ steps:
7
7
  - id: implement
8
8
  kind: agent
9
9
  agent: implementer
10
+ repositories:
11
+ control: write
10
12
  maxAttempts: 3
11
13
  on:
12
14
  passed: review-arch
package/CHANGELOG.md CHANGED
@@ -69,6 +69,18 @@ All notable user-facing changes are documented here. The project follows [Semant
69
69
 
70
70
  ### Changed
71
71
 
72
+ - **The rotation surface drive uses is the one setup writes.** Guide setup appends only
73
+ reviewed selectors to `.kxm/routes.yaml` and routes Google through Pi `antigravity`.
74
+
75
+ - **CLI and Studio stop reporting work they did not do.** Drive help no longer
76
+ describes the default as a model-free simulation. A Studio mutation with no handler returns 501 and `mappedToCli: false`.
77
+
78
+ - **Missing cost stays unknown.** `kxm prices acknowledge` restamps the local catalog
79
+ as today without fetching vendor rates, which is what an estimate will accept.
80
+ Routing totals are null when any attempt has no cost, and those rows sort after
81
+ complete-cost rows. `kxm improve` stays proposal-only. Wiki compile writes a file
82
+ only with `--out` and does not ingest.
83
+
72
84
  - **The workflow loader refuses a gate step that can never settle
73
85
  (`gate_outcome_impossible`).** A gate step settles only on `passed` or
74
86
  `implementation-failure` when `expect` is `pass`, and only on `passed` or `repro-missing`
@@ -90,8 +102,8 @@ All notable user-facing changes are documented here. The project follows [Semant
90
102
  execute until the run engine lands. The JSON result and its
91
103
  `phase` are unchanged. A `run_handoff_required` refusal from `kxm runs drive` now ends
92
104
  with `(handoff reason …; field …; detail …)`, each part capped at 200 characters; a run of
93
- the `default` workflow that `kxm init` writes, for example, reports `limit_unsupported`
94
- on `limits.maxAgentTimeMs`. Top-level help names the product KXM instead of KontextMind,
105
+ a workflow that declares `limits.maxAgentTimeMs`, for example, reports `limit_unsupported`
106
+ on that field. Top-level help names the product KXM instead of KontextMind,
95
107
  and `kxm init` text output lists each validation issue as `file: code: message`.
96
108
  - **`kxm suggest` recommends only KXM command skills.** Suggested skills come from the
97
109
  command skills shipped in `plugins/kxm/skills` (such as `kxm-workflow`, `kxm-runs`,
@@ -307,6 +319,31 @@ All notable user-facing changes are documented here. The project follows [Semant
307
319
 
308
320
  ### Fixed
309
321
 
322
+ - **Live `kxm runs drive` can author on an audited writer profile.** A write-repository
323
+ step on pi (`-a`, with extensions, skills, and the session off) or grok
324
+ (`--always-approve`, with subagents and web search off) runs against the checkout.
325
+ A live write that leaves the tree unchanged settles `failed` with `authored: false`.
326
+ A read-only step that changes the tree cannot settle `passed`. Harnesses without a
327
+ writer profile still hand off. Simulated drive does not require a diff. The witness
328
+ fingerprints the one checkout, so a live write step must be a single assignment
329
+ (`assignments.maximum: 1`) in a project whose `limits.maxConcurrentRuns` is 1; a
330
+ panel of writers or a project that admits concurrent runs hands off with
331
+ `step_unsupported` instead of crediting one writer's change to another.
332
+
333
+ - **Fresh `kxm init` can be driven.** The current template drops `limits.maxAgentTimeMs`,
334
+ names coordinator `claude` / `anthropic/fable` and implementer `grok` / `xai/grok-4.6`,
335
+ and admits those two routes. One-shot production no longer falls through to an
336
+ unadmitted `claude-3-7-sonnet`.
337
+
338
+ - **`kxm backup` includes the Runtime stores under the user-state root.** Discovery
339
+ copies `$S/runtime/registry.db` and `$S/runtime/projects/<projectKey>/run-events.db`,
340
+ plus each `run-events.db.run-prompts.json` sidecar. A copy that misses a discovered
341
+ store is `complete: false`: `kxm backup` exits 1 with `ok: false`, and restore
342
+ refuses that manifest. Cross-box remap of absolute `$S` paths is still the file recipe.
343
+ The Runtime stores are machine-wide, so a backup holds every project's run store and a
344
+ restore rolls all of them back. `kxm backup --help` no longer says Runtime stores are
345
+ left out.
346
+
310
347
  - **Signed webhooks cannot be replayed.** KXM's own webhook senders now sign the
311
348
  timestamp, delivery ID, definition, run and signal key along with the body
312
349
  (`x-kxm-signature`, `x-kxm-timestamp`, `x-kxm-delivery-id`; see
@@ -328,6 +365,13 @@ All notable user-facing changes are documented here. The project follows [Semant
328
365
  - **Workflow prompts no longer point agents at `.kxm/config`**, a path KXM refuses.
329
366
  - **`kxm gate signal` and `kxm workflow wait` inside a KXM project reach the hub for hub
330
367
  runs.** They go to the local Runtime only for a run its store holds.
368
+ - **`kxm peer inbox` lists the requests waiting for a named CLI agent.** It returned
369
+ `{"messages":[]}` every time. The hub now serves `GET /v1/agents/:id/inbox`
370
+ (agent-authenticated, project-scoped): the caller's queued and delivered requests,
371
+ oldest first, acknowledging nothing. Run with a stable `KXM_AGENT_NAME` (for example
372
+ `codex`) to list requests peers queued for it while it was offline, then answer them
373
+ with `kxm peer reply`. The Pi extension's `kxm_inbox` tool now refuses instead of
374
+ returning an empty list, because Pi activates each inbound request as a turn itself.
331
375
 
332
376
  - **The Claude plugin's SessionStart hook is one bundled, read-only, project-scoped
333
377
  script.** The two shell hooks it replaces (`kxm session brief --status` and
@@ -204,7 +204,7 @@ The package ships a directory of `SKILL.md` suites that Pi and Claude Code both
204
204
 
205
205
  `kxm dash` draws live terminal screens (agents, tasks, workflows, plans, inbox, processes, and spend, which is always empty today) from the admin-only operations stream, a presence-only fallback, and a read-only snapshot of `kxm.db`. Treat it as an observer: its action keys `a`, `r`, `s` and `c` post to hub routes that do not exist, so they change nothing even though the status line reports success, and `d` creates a git branch and worktree.
206
206
 
207
- `kxm studio layout` renders a workflow as DAG, stepper, and swimlane JSON, and `kxm studio serve` hosts a local viewer on `127.0.0.1:4242`. The Studio mutation endpoint is not wired to commands: it acknowledges requests without running them.
207
+ `kxm studio layout` renders a workflow as DAG, stepper, and swimlane JSON, and `kxm studio serve` hosts a local viewer on `127.0.0.1:4242`. The Studio mutation endpoint is not wired to commands: it answers every allowed request with HTTP 501 `mutation_handler_missing` and runs nothing.
208
208
 
209
209
  ## Configuration is reviewed Git YAML
210
210
 
@@ -169,7 +169,7 @@ Every store opens in WAL mode with a 5-second busy timeout, `synchronous=NORMAL`
169
169
  ## Reset a store
170
170
 
171
171
  > [!CAUTION]
172
- > Deleting a store erases its history for good. Back it up first if you might need it. `kxm backup` copies the hub store, but it does not find the Runtime stores in the user state root, so copy those with the Runtime stopped.
172
+ > Deleting a store erases its history for good. Back it up first if you might need it. `kxm backup` copies the hub store and the Runtime stores in the user state root, for every project on the machine.
173
173
 
174
174
  Stop every writer first, then delete each database together with its sidecars:
175
175
 
@@ -227,7 +227,7 @@ backfilled: they still resolve an outcome, but they group per run.
227
227
  - **Price catalog:** `.kxm/prices.yaml` (`kxm.prices.v1`, dated and hashed) defines input, output, cache-read, cache-write rates, and context tiers for active models. Missing rows or uncataloged models evaluate to `costBasis: "unknown"`.
228
228
  - **Ranked report:** `kxm routing report` (`plugins/kxm/src/routing.ts`, CLI command `kxm routing report`) groups records by `(harness, model, thinking, role)`.
229
229
  - **Ranking order:** Quality first (`verifyPassRate` descending, then `reworkRate` ascending where rework measures back-edge re-entries `transitions > 0`), followed by `costPerAcceptedUsd` ascending.
230
- - **Underquote prevention:** Routes with unknown cost are flagged (`*`). Any unknown-cost attempt makes a route's cost a lower bound, so the route ranks after every route of equal quality that has no unknown-cost attempt. Displayed values are unchanged: a route that mixes unmetered and unknown-cost attempts still shows `$0` per accepted attempt, so read the flag and the population counts before comparing cost.
230
+ - **Underquote prevention:** Routes with unknown cost are flagged (`*`). Any unknown-cost attempt makes a route's cost unknown: `costPerAcceptedUsd` is `null` (displayed `-`), never a partial sum, and the route ranks after every route of equal quality that has no unknown-cost attempt. Per-configuration comparisons likewise report `totalCostUsd: null` with a `missingCostRuns` count when any record lacks a cost.
231
231
  - **Population separation:** Reports metered cost, unmetered attempt counts, unknown-cost attempt counts, and quota-exhausted attempt counts as separate metrics rather than a single misleading total.
232
232
  - **List prices flag:** Supports `--equivalent-list-cost` / `--list-prices` to display estimated list rates for comparison alongside actual recorded spend.
233
233
  - **Rework column:** reads `transitions`, which Runtime records never set, so Runtime rework shows up only as a resolved `reworked` outcome, which the report does not count as a pass.
@@ -133,16 +133,17 @@ The context suites in detail:
133
133
  | Metadata-only dashboard: ops mode, presence-only fallback, observer filtering, keys, body-free local projection | `tui.test.ts`, `hub-api.test.ts` |
134
134
  | Restricted loader, deterministic bundle hash, init classification, atomic creation, three-way repair, crash resumption | `project-config.test.ts`, `cli.test.ts`, `package-install.test.ts` |
135
135
  | Legacy `.kxm/config` JSON is refused with `legacy_state_unsupported`; `kxm migrate` is unknown; older stores are refused | `project-config.test.ts`, `cli.test.ts`, `e6-backup-restore-migrations.test.ts` |
136
- | `kxm backup` and `kxm restore` round-trip the hub store and Runtime stores seeded under `.kxm/runtime/`; tampered or newer backups are refused | `e6-backup-restore-migrations.test.ts` |
136
+ | `kxm backup` and `kxm restore` round-trip the hub store and Runtime stores seeded under `.kxm/runtime/`; Runtime stores and prompt sidecars under `KXM_STATE_HOME` are discovered; tampered, newer or incomplete (`complete: false`) backups are refused | `e6-backup-restore-migrations.test.ts` |
137
137
  | Permission-diff trust: authority lattice, prose neutrality, Git base shadowing, CLI diff and check | `permission.test.ts`, `cli.test.ts`, `contracts.test.ts`, `package-install.test.ts` |
138
138
  | KXM schemas, restricted YAML fixtures, cross-resource semantics and sync-safe rejection | `contracts.test.ts`, `restricted-yaml.test.ts` |
139
139
  | Harness detection, auth and dispatch for the built-in catalog, including Windows launch rules | `harness.test.ts` |
140
140
  | `kxm update`: `update.yaml` validation, GitHub or npm version checks, install-kind detection, `kxm-<v>.tgz` asset selection | `kxm-update.test.ts`, `kxm-update-cli.test.ts`, `kxm-install-kind.test.ts` |
141
141
 
142
- The backup round trip seeds its Runtime stores in a project-local
143
- `.kxm/runtime/` layout that the Runtime never writes. Production Runtime stores
144
- live under the user state root, which `kxm backup` does not discover, so no test
145
- backs up the real Runtime stores.
142
+ The round trip seeds its Runtime stores in a project-local `.kxm/runtime/`
143
+ layout that the Runtime never writes. A separate test seeds a registry, an event
144
+ store and its prompt sidecar under a temporary `KXM_STATE_HOME` and checks that
145
+ `kxm backup` finds them and that a partial manifest is refused. No test backs up
146
+ stores a running supervisor wrote.
146
147
 
147
148
  ## Packaging, release and repository gates
148
149
 
@@ -210,7 +210,7 @@ CLI (run with the recipient's `KXM_AGENT_NAME`):
210
210
  kxm peer reply msg_779f5e0f22e04ac1af6078589a874971 "The plan is sound. Add a test for a zero discount."
211
211
  ```
212
212
 
213
- Only the recipient can reply, and only once. From the CLI, `kxm peer inbox` always returns an empty list, because a one-shot command has no long-running inbox. Use `kxm dash --screen inbox` to see pending requests.
213
+ Only the recipient can reply, and only once. From the CLI, `kxm peer inbox` lists the requests still waiting for the agent named by `KXM_AGENT_NAME`, including ones queued while it was offline, without acknowledging them. The default `cli-<pid>` name is a new agent on every call, so its list is empty.
214
214
 
215
215
  ## Choose a delivery mode
216
216
 
@@ -291,7 +291,7 @@ Errors about `workflowContext` are covered in [Peer provenance and quorum gates]
291
291
  | A request stays `delivered` | The recipient's turn, tool, or provider call is still running, or a Claude Code session restarted after acknowledging it. | Wait, or cancel and send it again with a new idempotency key. |
292
292
  | `kxm_fanout` returns `pending` | The local wait ended before a reply. | Use the returned message IDs with `kxm_get`, or repeat the exact call. |
293
293
  | Claude Code never sees requests | Channel mode is off or blocked by policy. | Use `kxm_inbox` and `kxm_reply`. |
294
- | `kxm peer inbox` is always empty | The CLI has no long-running inbox. | Use `kxm dash --screen inbox`. |
294
+ | `kxm peer inbox` is always empty | `KXM_AGENT_NAME` is unset, so each call registers a new `cli-<pid>` agent that nobody has addressed. | Set `KXM_AGENT_NAME` to the name peers send to. |
295
295
 
296
296
  For hub-level problems, see [Troubleshoot KXM](../operations/troubleshooting.md).
297
297
 
@@ -59,17 +59,17 @@ Record every override with the backup. A restore that lands where the running se
59
59
 
60
60
  ## Know what each backup covers
61
61
 
62
- `kxm backup` protects SQLite stores it can find from the current directory. Everything else needs the stopped-state copy described below.
62
+ `kxm backup` copies the SQLite stores it can find: the hub store under the current directory, and the Runtime stores and their prompt sidecars under the user state root. Everything else needs the stopped-state copy described below.
63
63
 
64
64
  > [!WARNING]
65
- > `kxm backup` does not back up the Runtime. It looks for Runtime stores under `.kxm/runtime/` in the checkout, but the Runtime writes them under the user state root (`$S/runtime/registry.db` and `$S/runtime/projects/<key>/run-events.db`). In practice a backup holds only the hub store. Back up the Runtime stores and their prompt sidecars by hand, stopped, as shown in [Back up everything else](#back-up-everything-else).
65
+ > The Runtime stores are shared by every project on the machine. `kxm backup` copies `$S/runtime/registry.db` and the `run-events.db` of every project under `$S/runtime/projects/`, not only the project you run it from, and `kxm restore` writes all of them back. Restoring one project's backup returns every project's runs to the moment of that backup.
66
66
 
67
67
  | Path | Holds | In `kxm backup` |
68
68
  |---|---|---|
69
69
  | `$W/kxm.db` | Hub store: agents, messages, workflow runs, journals, context items, leases, synced run facts | Yes, at `<current directory>/.kxm/state/kxm.db` only |
70
- | `$S/runtime/registry.db` | Runtime registry: projects, their roots, the supervisor identity and claim | No |
71
- | `$S/runtime/projects/<key>/run-events.db` | Event-sourced runs, drive receipts, gate evidence, the sync outbox | No |
72
- | `$S/runtime/projects/<key>/run-events.db.run-prompts.json` | Run prompt text; restoring a store without it loses every prompt | No |
70
+ | `$S/runtime/registry.db` | Runtime registry: projects, their roots, the supervisor identity and claim | Yes (`registry`) |
71
+ | `$S/runtime/projects/<key>/run-events.db` | Event-sourced runs, drive receipts, gate evidence, the sync outbox | Yes, for every project (`events:<key>`) |
72
+ | `$S/runtime/projects/<key>/run-events.db.run-prompts.json` | Run prompt text; restoring a store without it loses every prompt | Yes, as a plain file (`events:<key>:run-prompts`) |
73
73
  | `$S/projects/<hash>/repository-bindings.json`, `$S/update.yaml` | Member repository paths; updater settings | No |
74
74
  | `$S/hub-env.json`, `$S/hub-binding.json`, `$C/session.token` | Credentials and the machine's hub binding | No; prefer regenerating secrets to copying them |
75
75
  | `$R/.kxm/` definition files and durable records | Project, roles, routes, prices, roster, memory, skills, goals, tasks, candidates | No; commit them to Git or copy the checkout |
@@ -82,9 +82,9 @@ A restore without `roster.yaml`, `routes.yaml` or `prices.yaml` comes back healt
82
82
 
83
83
  These files are disposable and need no backup: `hub.pid`, `hub.stop`, `worker-*.pid`, `session-brief.json`, `update-check.json`, `runtime/supervisor.token`, `runtime/supervisor.error`.
84
84
 
85
- ## Back up the hub store with `kxm backup`
85
+ ## Back up the SQLite stores with `kxm backup`
86
86
 
87
- Run it from the checkout root. It resolves stores from the current directory and ignores `--workspace`, `KXM_WORKDIR`, `KXM_STATE_DIR` and `KXM_DATA_PATH`, so a relocated hub database is not found.
87
+ Run it from the checkout root. It finds the hub store from the current directory and ignores `--workspace`, `KXM_WORKDIR`, `KXM_STATE_DIR` and `KXM_DATA_PATH`, so a relocated hub database is not found. It finds the Runtime stores under the user state root, including one moved with `KXM_STATE_HOME`.
88
88
 
89
89
  Preview first. A dry run lists what it would write, opens no store and writes nothing:
90
90
 
@@ -96,31 +96,40 @@ kxm backup --dry-run
96
96
  Expected output:
97
97
 
98
98
  ```text
99
- dry run: back up 1 store(s) to /srv/kxm/product/.kxm/backups/backup-2026-09-23T18-29-09-107Z (sources are not opened, so their WAL is not checkpointed)
99
+ dry run: back up 3 store(s) and 1 file(s) to /srv/kxm/product/.kxm/backups/backup-2026-09-23T18-29-09-107Z (sources are not opened, so their WAL is not checkpointed)
100
100
  would write /srv/kxm/product/.kxm/backups/backup-2026-09-23T18-29-09-107Z/kxm.db
101
+ would write /srv/kxm/product/.kxm/backups/backup-2026-09-23T18-29-09-107Z/registry.db
102
+ would write /srv/kxm/product/.kxm/backups/backup-2026-09-23T18-29-09-107Z/run-events.db
103
+ would write /srv/kxm/product/.kxm/backups/backup-2026-09-23T18-29-09-107Z/run-events.db.run-prompts.json
101
104
  would write /srv/kxm/product/.kxm/backups/backup-2026-09-23T18-29-09-107Z/manifest.json
102
105
  ```
103
106
 
104
107
  Then write the backup outside the checkout:
105
108
 
106
109
  ```bash
107
- kxm backup --out /backups/kxm/2026-09-23/hub
110
+ kxm backup --out /backups/kxm/2026-09-23/sqlite
108
111
  ```
109
112
 
110
113
  Expected output:
111
114
 
112
115
  ```text
113
- Created SQLite backup with 1 store(s):
116
+ Created SQLite backup with 3 store(s):
114
117
  - hub-store: /srv/kxm/product/.kxm/state/kxm.db -> kxm.db (schema v5, 110592 bytes, sha256 sha256:86549...)
115
- Manifest: /backups/kxm/2026-09-23/hub/manifest.json
118
+ - registry: /home/kxm/.local/state/kxm/runtime/registry.db -> registry.db (schema v1, 20480 bytes, sha256 sha256:a5bde...)
119
+ - events:38ed26cb8eeaa297f3b0b452: /home/kxm/.local/state/kxm/runtime/projects/38ed26cb8eeaa297f3b0b452/run-events.db -> run-events.db (schema v7, 348160 bytes, sha256 sha256:27c13...)
120
+ Manifest: /backups/kxm/2026-09-23/sqlite/manifest.json
116
121
  ```
117
122
 
118
- For each store, `kxm backup` checkpoints the write-ahead log, runs `PRAGMA integrity_check`, copies the database with `VACUUM INTO`, checks the copy's integrity, sets mode `0600`, and records its schema version and SHA-256 in `manifest.json` (`kxm.backup-manifest.v1`). `VACUUM INTO` reads one consistent snapshot, so the hub can keep running during this step.
123
+ The summary lists stores only; the prompt sidecar is in the manifest's `files`. When two projects' event stores share a file name, the second copy is prefixed with its store id.
124
+
125
+ For each store, `kxm backup` checkpoints the write-ahead log, runs `PRAGMA integrity_check`, copies the database with `VACUUM INTO`, checks the copy's integrity, sets mode `0600`, and records its schema version and SHA-256 in `manifest.json` (`kxm.backup-manifest.v1`). `VACUUM INTO` reads one consistent snapshot, so the hub and the Runtime can keep running during this step. Each prompt sidecar is copied as a regular file with mode `0600` and its SHA-256 recorded.
126
+
127
+ A backup is complete only when it copied everything it found. If a store or sidecar cannot be copied, or one appears while the backup runs, the manifest lists it under `omitted` and records `complete: false`, the command prints `Backup is incomplete (<n> omitted); not ok:` and exits 1, and `kxm restore` refuses that manifest.
119
128
 
120
129
  > [!NOTE]
121
130
  > Without `--out`, backups go to `.kxm/backups/` inside the checkout. Keep that directory out of Git, or always pass `--out`.
122
131
 
123
- It fails with `backup_no_stores` when `.kxm/state/kxm.db` does not exist under the current directory, and with `database_corrupted` when an integrity check fails.
132
+ It fails with `backup_no_stores` when it finds no store at all, neither `.kxm/state/kxm.db` under the current directory nor a Runtime store under the user state root, and with `database_corrupted` when an integrity check fails.
124
133
 
125
134
  ## Back up everything else
126
135
 
@@ -142,11 +151,11 @@ Copy the remaining state with both services stopped. SQLite runs in write-ahead-
142
151
  S="${KXM_STATE_HOME:-$HOME/.local/state/kxm}" # macOS: "$HOME/Library/Application Support/KXM"
143
152
  B=/backups/kxm/2026-09-23
144
153
  mkdir -p "$B"
145
- kxm backup --out "$B/hub"
154
+ kxm backup --out "$B/sqlite"
146
155
  tar -C "$S" --exclude 'supervisor.token' --exclude 'hub-env.json' -czf "$B/user-state.tgz" .
147
156
  ```
148
157
 
149
- The archive holds `registry.db`, every project's `run-events.db` with any `-wal` and `-shm` files and its `.run-prompts.json` sidecar, the repository bindings, the hub binding and `update.yaml`. It leaves out the credential file; keep tokens in your secret store, or include `hub-env.json` and protect the archive as a secret.
158
+ `kxm backup` already holds the Runtime stores and sidecars. The archive holds them again, as files, together with what `kxm backup` does not copy: the repository bindings, the hub binding and `update.yaml`. It leaves out the credential file; keep tokens in your secret store, or include `hub-env.json` and protect the archive as a secret.
150
159
 
151
160
  If `KXM_STATE_DIR` or `KXM_DATA_PATH` moved the hub database, `kxm backup` fails with `backup_no_stores`. With both services stopped, copy that database file and any `-wal` and `-shm` files instead.
152
161
  4. Copy the other roots your recovery needs: the checkout's untracked `.kxm/` records, `$D/assets/`, `$W/worker-*.json` (and `$W/pi-sessions/` only if your policy keeps model history), and `$C`.
@@ -157,40 +166,45 @@ Keep at least one previous backup, and bound retention: run events and prompt si
157
166
 
158
167
  ## Restore with `kxm restore`
159
168
 
160
- `kxm restore` overwrites the live database and deletes its `-wal` and `-shm` files, and it does not check whether the hub is running. Stop the hub and the Runtime first, and move the current state aside rather than deleting it.
169
+ `kxm restore` overwrites every live database in the manifest, including the Runtime stores of every project on the machine, and deletes their `-wal` and `-shm` files. It does not check whether the hub or the Runtime is running. Stop the hub and the Runtime first, and move the current state aside rather than deleting it.
161
170
 
162
171
  Preview the restore. The dry run performs every check below and lists what it would overwrite:
163
172
 
164
173
  ```bash
165
174
  cd /srv/kxm/product
166
- kxm restore /backups/kxm/2026-09-23/hub/manifest.json --dry-run
175
+ kxm restore /backups/kxm/2026-09-23/sqlite/manifest.json --dry-run
167
176
  ```
168
177
 
169
178
  Expected output:
170
179
 
171
180
  ```text
172
- dry run: restore 1 store(s) from /backups/kxm/2026-09-23/hub/manifest.json; digests verified against the manifest
181
+ dry run: restore 3 store(s) and 1 file(s) from /backups/kxm/2026-09-23/sqlite/manifest.json; digests verified against the manifest
173
182
  would write /srv/kxm/product/.kxm/state/kxm.db
174
183
  would delete /srv/kxm/product/.kxm/state/kxm.db-wal
175
184
  would delete /srv/kxm/product/.kxm/state/kxm.db-shm
185
+ would write /home/kxm/.local/state/kxm/runtime/registry.db
186
+ would write /home/kxm/.local/state/kxm/runtime/projects/38ed26cb8eeaa297f3b0b452/run-events.db
187
+ would write /home/kxm/.local/state/kxm/runtime/projects/38ed26cb8eeaa297f3b0b452/run-events.db.run-prompts.json
176
188
  ```
177
189
 
178
190
  Then run it without `--dry-run`:
179
191
 
180
192
  ```bash
181
- kxm restore /backups/kxm/2026-09-23/hub/manifest.json
193
+ kxm restore /backups/kxm/2026-09-23/sqlite/manifest.json
182
194
  ```
183
195
 
184
196
  Expected output:
185
197
 
186
198
  ```text
187
- Restored 1 SQLite store(s) from /backups/kxm/2026-09-23/hub/manifest.json:
199
+ Restored 3 SQLite store(s) from /backups/kxm/2026-09-23/sqlite/manifest.json:
188
200
  - hub-store: -> /srv/kxm/product/.kxm/state/kxm.db (schema v5, integrity ok)
201
+ - registry: -> /home/kxm/.local/state/kxm/runtime/registry.db (schema v1, integrity ok)
202
+ - events:38ed26cb8eeaa297f3b0b452: -> /home/kxm/.local/state/kxm/runtime/projects/38ed26cb8eeaa297f3b0b452/run-events.db (schema v7, integrity ok)
189
203
  ```
190
204
 
191
- Before it overwrites anything, `kxm restore` checks that the manifest is a `kxm.backup-manifest.v1` document, that every listed file exists, that each file's SHA-256 matches the manifest, and that no store is newer than this build supports. The manifest's own `manifestSha256` is not checked.
205
+ Before it overwrites anything, `kxm restore` checks that the manifest is a `kxm.backup-manifest.v1` document that does not record `complete: false` (`restore_incomplete`), that every listed file exists, that each file's SHA-256 matches the manifest, and that no store is newer than this build supports. The manifest's own `manifestSha256` is not checked. A manifest from an older build that has no `complete` field still restores.
192
206
 
193
- It then checks each backup's integrity and schema version again, copies it into place with mode `0600`, and checks the result. Each store returns to its recorded path, rebased onto the current directory when the manifest came from another checkout.
207
+ It then checks each backup's integrity and schema version again, copies it into place with mode `0600`, checks the result, and then copies the prompt sidecars back. A store under the checkout is rebased onto the current directory when the manifest came from another checkout. The Runtime stores and sidecars return to their recorded absolute paths under the user state root; restore does not move them to a different machine's or a different `KXM_STATE_HOME`.
194
208
 
195
209
  ### Restore ceilings
196
210
 
@@ -207,6 +221,8 @@ The ceilings come from `KXM_BACKUP_CEILINGS` in `plugins/kxm/src/database.ts` an
207
221
 
208
222
  ### Restore the Runtime stores by hand
209
223
 
224
+ `kxm restore` puts the Runtime stores back at the paths they came from. Use the archive instead when the user state root moved, or when you also need the bindings and `update.yaml`.
225
+
210
226
  1. Stop the Runtime and the hub, as in the backup procedure.
211
227
  2. Move `$S/runtime/registry.db` and `$S/runtime/projects/` aside.
212
228
  3. Extract the archive into the user state root:
@@ -232,8 +248,11 @@ Test a full restore on a spare machine before you rely on it, and repeat the tes
232
248
 
233
249
  | Symptom | Cause | Fix |
234
250
  |---|---|---|
235
- | `backup_no_stores` | No `.kxm/state/kxm.db` under the current directory, or it was moved with `KXM_STATE_DIR` or `KXM_DATA_PATH` | Run from the checkout root; copy a relocated database with the stopped-state procedure |
236
- | Runs are missing after a restore | `kxm backup` never contained the Runtime stores | Restore `$S/runtime/` from the stopped-state archive |
251
+ | `backup_no_stores` | No `.kxm/state/kxm.db` under the current directory (or it was moved with `KXM_STATE_DIR` or `KXM_DATA_PATH`) and no Runtime store under the user state root | Run from the checkout root; copy a relocated database with the stopped-state procedure |
252
+ | `Backup is incomplete (<n> omitted); not ok:`, exit 1 | A store or sidecar could not be copied, or appeared while the backup ran | Fix the source named after `omitted`, then run `kxm backup` again |
253
+ | `restore_incomplete` | The manifest records `complete: false` | Restore a complete backup; the partial one is not restorable |
254
+ | Runs are missing after a restore | The checkout moved to another absolute path, so the Runtime derives a different store key, or the user state root changed | Keep the checkout at its original path and restore under the same `KXM_STATE_HOME` |
255
+ | Another project's recent runs disappeared after a restore | `kxm restore` rolled back every project's Runtime store to the backup's moment | Restore that project's store from a newer backup or from the moved-aside copy |
237
256
  | `restore_manifest_digest_mismatch` | A backup file changed after the manifest was written | Use another backup; do not edit files in a backup set |
238
257
  | `restore_file_missing` | A file listed in the manifest is not beside it | Copy the whole backup directory, not only `manifest.json` |
239
258
  | `runtime_schema_mismatch` | A backup file's schema version differs from the one its manifest records | Use another backup set; never mix files between sets |
@@ -237,7 +237,7 @@ flowchart LR
237
237
  Hold these invariants on every tenant box:
238
238
 
239
239
  1. **A service account, not root.** Session isolation routes model context; it is not a sandbox against a hostile process under the same account. Give untrusted workers separate accounts or containers.
240
- 2. **Explicit, stable paths.** Set `KXM_WORKSPACE_DIR`, `KXM_STATE_DIR`, `KXM_DATA_PATH`, `KXM_LOG_PATH` and `KXM_STATE_HOME` instead of inheriting a home directory, and pin `KXM_HOST=127.0.0.1`. `kxm backup` ignores `KXM_STATE_DIR` and `KXM_DATA_PATH`, so on such a box it finds no hub store and fails with `backup_no_stores`; use the [stopped-state backup](backup-and-restore.md#back-up-everything-else) instead.
240
+ 2. **Explicit, stable paths.** Set `KXM_WORKSPACE_DIR`, `KXM_STATE_DIR`, `KXM_DATA_PATH`, `KXM_LOG_PATH` and `KXM_STATE_HOME` instead of inheriting a home directory, and pin `KXM_HOST=127.0.0.1`. `kxm backup` ignores `KXM_STATE_DIR` and `KXM_DATA_PATH`, so on such a box it finds no hub store: it copies only the Runtime stores under `KXM_STATE_HOME`, or fails with `backup_no_stores` when there are none. Copy the hub database with the [stopped-state backup](backup-and-restore.md#back-up-everything-else).
241
241
  3. **Loopback listeners only.** Neither the hub nor the supervisor has a public port, and nothing is load-balanced across hubs.
242
242
  4. **Edge authentication.** The proxy authenticates browsers (for example with Authentik forward auth) on the portal's routes only. The hub never interprets browser identity; see [ADR-0004](../adr/ADR-0004-edge-identity-authentik.md).
243
243
  5. **Server-side machine credentials.** The portal backend keeps the hub credentials and calls the hub itself. `kxm tenant status`, which uses the admin token, gives it one labelled view of hub metadata and Runtime runs.