@kontextmind/kxm 0.7.52 → 0.7.53

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.
@@ -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.52",
14
+ "version": "0.7.53",
15
15
  "category": "development",
16
16
  "tags": ["kxm", "multi-agent", "workflows", "mcp"]
17
17
  }
package/CHANGELOG.md CHANGED
@@ -35,6 +35,23 @@ All notable user-facing changes are documented here. The project follows [Semant
35
35
 
36
36
  ### Changed
37
37
 
38
+ - **No schema migration lanes, no legacy stamp tolerance (single-operator tool).** Stepwise
39
+ schema migration lanes are removed from the hub store and the per-project event store, and
40
+ the external-effects store no longer runs an in-place `ALTER TABLE ... ADD COLUMN` whose
41
+ failure it swallowed (that column is declared in the fresh schema, so the lane could only
42
+ ever fire on an older store; the store has no production caller yet, so this closes the
43
+ lane rather than fixing a live upgrade).
44
+ A database stamped behind this build now fails closed with `runtime_schema_outdated` and
45
+ a message that says to delete the state file or re-run `kxm init` — and the refusal never
46
+ advances `user_version`, so the store stays identifiably old (WAL sidecars may still be
47
+ checkpointed by opening the file, so the whole state set remains the backup unit — see
48
+ [`docs/operations.md`](docs/operations.md)). Relabelling a store it refused to open would
49
+ only hide the problem until a query hit a missing column. The coordinator fingerprint no
50
+ longer recomputes over stored authority to forgive rows written before set canonicalisation:
51
+ a stale coordinator is re-bound. Intake tests go from 23 to 22; the two forced-race tests
52
+ now win against ordinary current-format rows, and one refusal test asserts the untouched
53
+ stamp.
54
+
38
55
  - **`kxm hub bind` no longer stores a remote URL it cannot authenticate to.** A remote
39
56
  binding is a deliberate network decision, so it is now refused when no credential resolves
40
57
  (explicit `KXM_AUTH_TOKEN`, or the persisted hub record's admin or project tokens) —
@@ -16,7 +16,7 @@ Presence of KXM documents does not activate new behavior.
16
16
  | Long-lived manually started Pi workers | Runtime-managed run-scoped sessions | Existing worker mode remains available during compatibility release |
17
17
  | Shared/off workflow Pi history | `{run, agent, instance, scopeEpoch}` sessions | Never import shared conversation history into a narrower run scope |
18
18
  | Hub-owned workflow state | Home Runtime event log with hub projection | Import completed history as legacy records; active-run cutover requires quiescence |
19
- | SQLite schema v3 `kxm.db` | Runtime registry, per-project event stores, hub registry/project stores | Copy through versioned migration; never mutate the only database in place |
19
+ | SQLite schema v3 `kxm.db` | Runtime registry, per-project event stores, hub registry/project stores | **No in-place schema migration.** A store stamped behind the current build fails closed with `runtime_schema_outdated` and the refusal never advances `user_version` (opening the file may still checkpoint WAL sidecars, so the whole state set is the backup unit); re-init instead. Stepwise lanes were removed 2026-09-20 under the single-operator decision from the hub store, the per-project event store, and the external-effects `ALTER TABLE` add-column; the Runtime registry never carried a stepwise lane (see [implementation-plan.md](../../plans/implementation-plan.md) → Decided) |
20
20
  | Full peer message bodies in hub DB | Summary-first sync events | Existing bodies remain protected legacy data and are not re-emitted automatically |
21
21
  | Project tokens/manual environment auth | Runtime enrollment and scoped credentials | Preserve current mode until enrollment is confirmed; never copy tokens into Git |
22
22
  | `.kxm/config/env.example` | Built-in defaults plus optional scoped env YAML | Import only explicit portable differences; secrets become references |
@@ -26,12 +26,27 @@ Presence of KXM documents does not activate new behavior.
26
26
 
27
27
  ## Compatibility releases and activation
28
28
 
29
+ > **Scope note (2026-09-20).** This matrix documents the Mesh/v0.5 → KXM cutover. KXM has
30
+ > one operator and no external installs, so **schema migration and old-state tolerance are
31
+ > out of scope** and the lanes that existed are gone: stores refuse an older stamp rather
32
+ > than upgrading, the external-effects store no longer adds a column in place, and the
33
+ > coordinator fingerprint no longer recomputes to forgive pre-canonicalisation rows. What
34
+ > remains here describes the **project/content** cutover, which the follow-up cut removes
35
+ > along with `kxm migrate`. Nothing here promises that the runtime will read an old
36
+ > database.
37
+
29
38
  Local Runtime support may ship publicly before hub KXM, but it remains beside
30
39
  existing hub contracts and stores. Old command names are not preserved. A project
31
40
  activates `kxm.*.v1` only by an explicit successful `kxm init`/migration receipt;
32
41
  file presence alone never activates it. Legacy hub runs continue on the legacy
33
42
  engine.
34
43
 
44
+ > **Superseded (2026-09-20).** The single-operator decision removes legacy readers
45
+ > **without** a compatibility release, so this transition-release list is no longer a plan of
46
+ > record. It stays here to name what was given up: no dual-read window, no `mesh_*` shim, and
47
+ > no period where legacy JSON stays loadable while KXM writes YAML. Anything still holding
48
+ > legacy state is refused rather than served from both shapes.
49
+
35
50
  When Phase 8 activates hub KXM, at least one hub transition release provides:
36
51
 
37
52
  - current `mesh_*` peer tools;
@@ -58,6 +73,12 @@ kxm migrate verify
58
73
 
59
74
  `kxm init` invokes the planning flow when it detects legacy state.
60
75
 
76
+ > **Superseded (2026-09-20).** The single-operator decision removed every schema
77
+ > migration lane, so "database/WAL migration … remain later-phase work" below is
78
+ > no longer the plan: there will be none. The `kxm migrate` commands described on
79
+ > this page are deleted in the follow-up cut, and a tree still holding legacy JSON
80
+ > fails closed at load instead of being converted.
81
+
61
82
  **Implementation status (Phase 1 slice):** the commands above are implemented
62
83
  for **configuration migration only** — legacy `agents.json`, `gates.json`, and
63
84
  workflow-definition JSON under `.kxm/config/`. Database/WAL migration,
@@ -120,6 +141,18 @@ installed resource bytes against the receipt. It performs no writes.
120
141
 
121
142
  ## Database migration
122
143
 
144
+ > **Superseded in part (2026-09-20).** What the code guarantees today is narrower than the
145
+ > checklist below and belongs to two different moments:
146
+ >
147
+ > - **Opening a store** verifies `user_version`, refuses a newer-than-known version, and
148
+ > enables WAL. That is implemented.
149
+ > - **Backing a store up** is where checkpointing, `-wal` handling, integrity verification,
150
+ > and hash recording happen — see the whole-state-set recipe in
151
+ > [`docs/operations.md`](../operations.md). Those are **not** properties of an ordinary open.
152
+ >
153
+ > The legacy-record import paragraphs describe a cutover that will not happen: this build
154
+ > migrates no database, and an older stamp is refused outright.
155
+
123
156
  Before any database operation:
124
157
 
125
158
  - verify SQLite `user_version`;
@@ -211,6 +244,11 @@ workflow history.
211
244
 
212
245
  ## Removal gate
213
246
 
247
+ > **Superseded (2026-09-20).** The operator decided removal happens **without** a
248
+ > compatibility release, so the list below no longer gates removal — it is kept only
249
+ > to record why the gate existed. Legacy readers are being deleted now, and old names
250
+ > fail closed by brake rather than alias.
251
+
214
252
  Legacy readers and command aliases are removed only after:
215
253
 
216
254
  - at least one compatibility release;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontextmind/kxm",
3
- "version": "0.7.52",
3
+ "version": "0.7.53",
4
4
  "description": "KXM local-first multi-agent orchestration and operator dashboard",
5
5
  "type": "module",
6
6
  "author": "KontextMind",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "kxm",
4
4
  "displayName": "KXM",
5
- "version": "0.7.52",
5
+ "version": "0.7.53",
6
6
  "description": "Headless multi-agent orchestration, durable workflows, and a live operator dashboard for Pi and Claude Code",
7
7
  "author": {
8
8
  "name": "KontextMind",
@@ -23767,20 +23767,11 @@ function openDatabase(file, description, spec) {
23767
23767
  database.exec(spec.schema);
23768
23768
  database.exec(`PRAGMA user_version = ${spec.version}`);
23769
23769
  } else if (version < spec.version) {
23770
- let currentVersion = version;
23771
- while (currentVersion < spec.version) {
23772
- const step = spec.migrations?.find((m2) => m2.fromVersion === currentVersion);
23773
- if (!step) {
23774
- throw databaseError(
23775
- "runtime_schema_outdated",
23776
- file,
23777
- `${description} schema version ${version} is older than ${spec.version}; no migration lane, backup and restore remain E6`
23778
- );
23779
- }
23780
- step.migrate(database);
23781
- currentVersion = step.toVersion;
23782
- database.exec(`PRAGMA user_version = ${currentVersion}`);
23783
- }
23770
+ throw databaseError(
23771
+ "runtime_schema_outdated",
23772
+ file,
23773
+ `${description} is schema version ${version}; this build requires ${spec.version}. Delete the state file (or re-run \`kxm init\`) to start fresh \u2014 upgrading old state in place is deliberately unsupported`
23774
+ );
23784
23775
  }
23785
23776
  if (spec.tables) {
23786
23777
  verifyExpectedTables(database, file, description, spec.tables);
@@ -17121,7 +17121,7 @@ async function deliverInboxNotification(messageId, delivered, notify) {
17121
17121
  }
17122
17122
 
17123
17123
  // plugins/kxm/src/mcp-server.ts
17124
- var VERSION = "0.7.52";
17124
+ var VERSION = "0.7.53";
17125
17125
  var inbox = /* @__PURE__ */ new Map();
17126
17126
  var notifiedInbox = /* @__PURE__ */ new Set();
17127
17127
  var meshClient;
@@ -17612,20 +17612,11 @@ function openDatabase(file, description, spec) {
17612
17612
  database.exec(spec.schema);
17613
17613
  database.exec(`PRAGMA user_version = ${spec.version}`);
17614
17614
  } else if (version < spec.version) {
17615
- let currentVersion = version;
17616
- while (currentVersion < spec.version) {
17617
- const step = spec.migrations?.find((m) => m.fromVersion === currentVersion);
17618
- if (!step) {
17619
- throw databaseError(
17620
- "runtime_schema_outdated",
17621
- file,
17622
- `${description} schema version ${version} is older than ${spec.version}; no migration lane, backup and restore remain E6`
17623
- );
17624
- }
17625
- step.migrate(database);
17626
- currentVersion = step.toVersion;
17627
- database.exec(`PRAGMA user_version = ${currentVersion}`);
17628
- }
17615
+ throw databaseError(
17616
+ "runtime_schema_outdated",
17617
+ file,
17618
+ `${description} is schema version ${version}; this build requires ${spec.version}. Delete the state file (or re-run \`kxm init\`) to start fresh \u2014 upgrading old state in place is deliberately unsupported`
17619
+ );
17629
17620
  }
17630
17621
  if (spec.tables) {
17631
17622
  verifyExpectedTables(database, file, description, spec.tables);
@@ -18270,71 +18261,6 @@ CREATE TABLE project_controls (
18270
18261
  record TEXT NOT NULL
18271
18262
  ) STRICT;
18272
18263
  `;
18273
- var COORDINATOR_INTAKE_DDL = `
18274
- CREATE TABLE IF NOT EXISTS coordinators (
18275
- coordinator_id TEXT PRIMARY KEY,
18276
- project_id TEXT NOT NULL,
18277
- role TEXT NOT NULL,
18278
- channel TEXT NOT NULL,
18279
- ceiling_hash TEXT NOT NULL,
18280
- config_revision TEXT NOT NULL,
18281
- bound_at TEXT NOT NULL,
18282
- schema TEXT NOT NULL,
18283
- record TEXT NOT NULL
18284
- ) STRICT;
18285
- CREATE UNIQUE INDEX IF NOT EXISTS coordinators_slot ON coordinators(project_id, role, channel);
18286
- CREATE TABLE IF NOT EXISTS intake_messages (
18287
- message_id TEXT PRIMARY KEY,
18288
- project_id TEXT NOT NULL,
18289
- coordinator_id TEXT NOT NULL,
18290
- idempotency_key TEXT NOT NULL,
18291
- content_hash TEXT NOT NULL,
18292
- received_at TEXT NOT NULL,
18293
- dispatch_state TEXT NOT NULL CHECK (dispatch_state IN ('ready','held_paused','admitted','refused')),
18294
- schema TEXT NOT NULL,
18295
- record TEXT NOT NULL
18296
- ) STRICT;
18297
- CREATE UNIQUE INDEX IF NOT EXISTS intake_idempotency ON intake_messages(project_id, coordinator_id, idempotency_key);
18298
- CREATE INDEX IF NOT EXISTS intake_dispatch ON intake_messages(project_id, dispatch_state, received_at, message_id);
18299
- CREATE TABLE IF NOT EXISTS project_controls (
18300
- project_id TEXT PRIMARY KEY,
18301
- paused INTEGER NOT NULL CHECK (paused IN (0, 1)),
18302
- reason TEXT,
18303
- updated_at TEXT NOT NULL,
18304
- actor TEXT NOT NULL,
18305
- schema TEXT NOT NULL,
18306
- record TEXT NOT NULL
18307
- ) STRICT;
18308
- `;
18309
- var DRIVE_RECEIPTS_DDL = `
18310
- CREATE TABLE IF NOT EXISTS drive_receipts (
18311
- drive_id TEXT PRIMARY KEY,
18312
- run_id TEXT NOT NULL,
18313
- project_id TEXT NOT NULL,
18314
- opened_sequence INTEGER NOT NULL,
18315
- last_sequence INTEGER NOT NULL,
18316
- closed_at TEXT NOT NULL,
18317
- schema TEXT NOT NULL,
18318
- receipt TEXT NOT NULL
18319
- ) STRICT;
18320
- CREATE INDEX IF NOT EXISTS drive_receipts_run ON drive_receipts(run_id);
18321
- `;
18322
- var EVENT_STORE_MIGRATIONS = [
18323
- {
18324
- fromVersion: 3,
18325
- toVersion: 4,
18326
- migrate(database) {
18327
- database.exec(DRIVE_RECEIPTS_DDL);
18328
- }
18329
- },
18330
- {
18331
- fromVersion: 4,
18332
- toVersion: 5,
18333
- migrate(database) {
18334
- database.exec(COORDINATOR_INTAKE_DDL);
18335
- }
18336
- }
18337
- ];
18338
18264
  var KxmRunEventStore = class {
18339
18265
  path;
18340
18266
  database;
@@ -18343,8 +18269,7 @@ var KxmRunEventStore = class {
18343
18269
  this.database = openDatabase(this.path, "run event store", {
18344
18270
  schema: EVENT_STORE_SCHEMA,
18345
18271
  version: KXM_EVENT_STORE_SCHEMA_VERSION,
18346
- tables: EVENT_STORE_TABLES,
18347
- migrations: EVENT_STORE_MIGRATIONS
18272
+ tables: EVENT_STORE_TABLES
18348
18273
  });
18349
18274
  }
18350
18275
  close() {
@@ -17783,20 +17783,11 @@ function openDatabase(file, description, spec) {
17783
17783
  database.exec(spec.schema);
17784
17784
  database.exec(`PRAGMA user_version = ${spec.version}`);
17785
17785
  } else if (version < spec.version) {
17786
- let currentVersion = version;
17787
- while (currentVersion < spec.version) {
17788
- const step = spec.migrations?.find((m) => m.fromVersion === currentVersion);
17789
- if (!step) {
17790
- throw databaseError(
17791
- "runtime_schema_outdated",
17792
- file,
17793
- `${description} schema version ${version} is older than ${spec.version}; no migration lane, backup and restore remain E6`
17794
- );
17795
- }
17796
- step.migrate(database);
17797
- currentVersion = step.toVersion;
17798
- database.exec(`PRAGMA user_version = ${currentVersion}`);
17799
- }
17786
+ throw databaseError(
17787
+ "runtime_schema_outdated",
17788
+ file,
17789
+ `${description} is schema version ${version}; this build requires ${spec.version}. Delete the state file (or re-run \`kxm init\`) to start fresh \u2014 upgrading old state in place is deliberately unsupported`
17790
+ );
17800
17791
  }
17801
17792
  if (spec.tables) {
17802
17793
  verifyExpectedTables(database, file, description, spec.tables);
@@ -18710,71 +18701,6 @@ CREATE TABLE project_controls (
18710
18701
  record TEXT NOT NULL
18711
18702
  ) STRICT;
18712
18703
  `;
18713
- var COORDINATOR_INTAKE_DDL = `
18714
- CREATE TABLE IF NOT EXISTS coordinators (
18715
- coordinator_id TEXT PRIMARY KEY,
18716
- project_id TEXT NOT NULL,
18717
- role TEXT NOT NULL,
18718
- channel TEXT NOT NULL,
18719
- ceiling_hash TEXT NOT NULL,
18720
- config_revision TEXT NOT NULL,
18721
- bound_at TEXT NOT NULL,
18722
- schema TEXT NOT NULL,
18723
- record TEXT NOT NULL
18724
- ) STRICT;
18725
- CREATE UNIQUE INDEX IF NOT EXISTS coordinators_slot ON coordinators(project_id, role, channel);
18726
- CREATE TABLE IF NOT EXISTS intake_messages (
18727
- message_id TEXT PRIMARY KEY,
18728
- project_id TEXT NOT NULL,
18729
- coordinator_id TEXT NOT NULL,
18730
- idempotency_key TEXT NOT NULL,
18731
- content_hash TEXT NOT NULL,
18732
- received_at TEXT NOT NULL,
18733
- dispatch_state TEXT NOT NULL CHECK (dispatch_state IN ('ready','held_paused','admitted','refused')),
18734
- schema TEXT NOT NULL,
18735
- record TEXT NOT NULL
18736
- ) STRICT;
18737
- CREATE UNIQUE INDEX IF NOT EXISTS intake_idempotency ON intake_messages(project_id, coordinator_id, idempotency_key);
18738
- CREATE INDEX IF NOT EXISTS intake_dispatch ON intake_messages(project_id, dispatch_state, received_at, message_id);
18739
- CREATE TABLE IF NOT EXISTS project_controls (
18740
- project_id TEXT PRIMARY KEY,
18741
- paused INTEGER NOT NULL CHECK (paused IN (0, 1)),
18742
- reason TEXT,
18743
- updated_at TEXT NOT NULL,
18744
- actor TEXT NOT NULL,
18745
- schema TEXT NOT NULL,
18746
- record TEXT NOT NULL
18747
- ) STRICT;
18748
- `;
18749
- var DRIVE_RECEIPTS_DDL = `
18750
- CREATE TABLE IF NOT EXISTS drive_receipts (
18751
- drive_id TEXT PRIMARY KEY,
18752
- run_id TEXT NOT NULL,
18753
- project_id TEXT NOT NULL,
18754
- opened_sequence INTEGER NOT NULL,
18755
- last_sequence INTEGER NOT NULL,
18756
- closed_at TEXT NOT NULL,
18757
- schema TEXT NOT NULL,
18758
- receipt TEXT NOT NULL
18759
- ) STRICT;
18760
- CREATE INDEX IF NOT EXISTS drive_receipts_run ON drive_receipts(run_id);
18761
- `;
18762
- var EVENT_STORE_MIGRATIONS = [
18763
- {
18764
- fromVersion: 3,
18765
- toVersion: 4,
18766
- migrate(database) {
18767
- database.exec(DRIVE_RECEIPTS_DDL);
18768
- }
18769
- },
18770
- {
18771
- fromVersion: 4,
18772
- toVersion: 5,
18773
- migrate(database) {
18774
- database.exec(COORDINATOR_INTAKE_DDL);
18775
- }
18776
- }
18777
- ];
18778
18704
  var KxmRunEventStore = class {
18779
18705
  path;
18780
18706
  database;
@@ -18783,8 +18709,7 @@ var KxmRunEventStore = class {
18783
18709
  this.database = openDatabase(this.path, "run event store", {
18784
18710
  schema: EVENT_STORE_SCHEMA,
18785
18711
  version: KXM_EVENT_STORE_SCHEMA_VERSION,
18786
- tables: EVENT_STORE_TABLES,
18787
- migrations: EVENT_STORE_MIGRATIONS
18712
+ tables: EVENT_STORE_TABLES
18788
18713
  });
18789
18714
  }
18790
18715
  close() {
@@ -11504,20 +11504,11 @@ function openDatabase(file, description, spec) {
11504
11504
  database.exec(spec.schema);
11505
11505
  database.exec(`PRAGMA user_version = ${spec.version}`);
11506
11506
  } else if (version < spec.version) {
11507
- let currentVersion = version;
11508
- while (currentVersion < spec.version) {
11509
- const step = spec.migrations?.find((m) => m.fromVersion === currentVersion);
11510
- if (!step) {
11511
- throw databaseError(
11512
- "runtime_schema_outdated",
11513
- file,
11514
- `${description} schema version ${version} is older than ${spec.version}; no migration lane, backup and restore remain E6`
11515
- );
11516
- }
11517
- step.migrate(database);
11518
- currentVersion = step.toVersion;
11519
- database.exec(`PRAGMA user_version = ${currentVersion}`);
11520
- }
11507
+ throw databaseError(
11508
+ "runtime_schema_outdated",
11509
+ file,
11510
+ `${description} is schema version ${version}; this build requires ${spec.version}. Delete the state file (or re-run \`kxm init\`) to start fresh \u2014 upgrading old state in place is deliberately unsupported`
11511
+ );
11521
11512
  }
11522
11513
  if (spec.tables) {
11523
11514
  verifyExpectedTables(database, file, description, spec.tables);
@@ -11683,100 +11674,10 @@ var HUB_STORE_SCHEMA_V3 = `
11683
11674
  ) STRICT;
11684
11675
  CREATE INDEX IF NOT EXISTS context_items_project ON context_items(project);
11685
11676
  `;
11686
- var HUB_STORE_MIGRATIONS = Object.freeze([
11687
- {
11688
- fromVersion: 1,
11689
- toVersion: 2,
11690
- migrate: (db) => {
11691
- db.exec(`
11692
- CREATE TABLE IF NOT EXISTS agents (
11693
- id TEXT PRIMARY KEY,
11694
- record TEXT NOT NULL
11695
- ) STRICT;
11696
- CREATE TABLE IF NOT EXISTS messages (
11697
- id TEXT PRIMARY KEY,
11698
- record TEXT NOT NULL
11699
- ) STRICT;
11700
- CREATE TABLE IF NOT EXISTS workflow_runs (
11701
- id TEXT PRIMARY KEY,
11702
- definition_id TEXT NOT NULL,
11703
- delivery_id TEXT NOT NULL,
11704
- record TEXT NOT NULL,
11705
- UNIQUE(definition_id, delivery_id)
11706
- ) STRICT;
11707
- CREATE TABLE IF NOT EXISTS workflow_journal (
11708
- id TEXT PRIMARY KEY,
11709
- run_id TEXT NOT NULL,
11710
- category TEXT NOT NULL,
11711
- area TEXT NOT NULL,
11712
- record TEXT NOT NULL
11713
- ) STRICT;
11714
- CREATE INDEX IF NOT EXISTS workflow_journal_run_id ON workflow_journal(run_id);
11715
- `);
11716
- }
11717
- },
11718
- {
11719
- fromVersion: 2,
11720
- toVersion: 3,
11721
- migrate: (db) => {
11722
- db.exec(`
11723
- CREATE TABLE IF NOT EXISTS agents (
11724
- id TEXT PRIMARY KEY,
11725
- record TEXT NOT NULL
11726
- ) STRICT;
11727
- CREATE TABLE IF NOT EXISTS messages (
11728
- id TEXT PRIMARY KEY,
11729
- record TEXT NOT NULL
11730
- ) STRICT;
11731
- CREATE TABLE IF NOT EXISTS workflow_runs (
11732
- id TEXT PRIMARY KEY,
11733
- definition_id TEXT NOT NULL,
11734
- delivery_id TEXT NOT NULL,
11735
- record TEXT NOT NULL,
11736
- UNIQUE(definition_id, delivery_id)
11737
- ) STRICT;
11738
- CREATE TABLE IF NOT EXISTS workflow_journal (
11739
- id TEXT PRIMARY KEY,
11740
- run_id TEXT NOT NULL,
11741
- category TEXT NOT NULL,
11742
- area TEXT NOT NULL,
11743
- record TEXT NOT NULL
11744
- ) STRICT;
11745
- CREATE INDEX IF NOT EXISTS workflow_journal_run_id ON workflow_journal(run_id);
11746
- CREATE TABLE IF NOT EXISTS context_items (
11747
- id TEXT PRIMARY KEY,
11748
- project TEXT NOT NULL,
11749
- kind TEXT NOT NULL,
11750
- record TEXT NOT NULL
11751
- ) STRICT;
11752
- CREATE INDEX IF NOT EXISTS context_items_project ON context_items(project);
11753
- CREATE UNIQUE INDEX IF NOT EXISTS messages_from_idempotency
11754
- ON messages(
11755
- json_extract(record, '$.from'),
11756
- json_extract(record, '$.idempotencyKey')
11757
- ) WHERE json_extract(record, '$.idempotencyKey') IS NOT NULL;
11758
- CREATE INDEX IF NOT EXISTS messages_to_seq
11759
- ON messages(
11760
- json_extract(record, '$.to'),
11761
- COALESCE(json_extract(record, '$.seq'), 0)
11762
- );
11763
- CREATE TABLE IF NOT EXISTS consumer_cursors (
11764
- agent_id TEXT PRIMARY KEY,
11765
- cursor INTEGER NOT NULL
11766
- ) STRICT;
11767
- CREATE TABLE IF NOT EXISTS agent_sequences (
11768
- agent_id TEXT PRIMARY KEY,
11769
- next_seq INTEGER NOT NULL
11770
- ) STRICT;
11771
- `);
11772
- }
11773
- }
11774
- ]);
11775
11677
  var HUB_STORE_SCHEMA_SPEC = Object.freeze({
11776
11678
  schema: HUB_STORE_SCHEMA_V3,
11777
11679
  version: HUB_STORE_SCHEMA_VERSION,
11778
- tables: HUB_STORE_TABLES,
11779
- migrations: HUB_STORE_MIGRATIONS
11680
+ tables: HUB_STORE_TABLES
11780
11681
  });
11781
11682
  var MessageMap = class extends Map {
11782
11683
  store;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plugin",
3
- "version": "0.7.52",
3
+ "version": "0.7.53",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -14,17 +14,10 @@ import { basename, dirname, join, resolve } from "node:path";
14
14
  import { DatabaseSync } from "./sqlite.ts";
15
15
  import { KxmConfigError, type KxmConfigIssue } from "./project-config.ts";
16
16
 
17
- export interface DatabaseMigrationStep {
18
- fromVersion: number;
19
- toVersion: number;
20
- migrate: (database: DatabaseSync) => void;
21
- }
22
-
23
17
  export interface DatabaseSchemaSpec {
24
18
  schema: string;
25
19
  version: number;
26
20
  tables?: Readonly<Record<string, readonly string[]>>;
27
- migrations?: readonly DatabaseMigrationStep[];
28
21
  timeoutMs?: number;
29
22
  }
30
23
 
@@ -179,20 +172,15 @@ export function openDatabase(file: string, description: string, spec: DatabaseSc
179
172
  database.exec(spec.schema);
180
173
  database.exec(`PRAGMA user_version = ${spec.version}`);
181
174
  } else if (version < spec.version) {
182
- let currentVersion = version;
183
- while (currentVersion < spec.version) {
184
- const step = spec.migrations?.find((m) => m.fromVersion === currentVersion);
185
- if (!step) {
186
- throw databaseError(
187
- "runtime_schema_outdated",
188
- file,
189
- `${description} schema version ${version} is older than ${spec.version}; no migration lane, backup and restore remain E6`,
190
- );
191
- }
192
- step.migrate(database);
193
- currentVersion = step.toVersion;
194
- database.exec(`PRAGMA user_version = ${currentVersion}`);
195
- }
175
+ // No migration lanes. This is a single-operator tool: an older database is
176
+ // re-initialised, not upgraded in place, and the code never carries two schema
177
+ // shapes at once. Silently accepting an older file would mean every query has to
178
+ // work against shapes it no longer tests.
179
+ throw databaseError(
180
+ "runtime_schema_outdated",
181
+ file,
182
+ `${description} is schema version ${version}; this build requires ${spec.version}. Delete the state file (or re-run \`kxm init\`) to start fresh — upgrading old state in place is deliberately unsupported`,
183
+ );
196
184
  }
197
185
 
198
186
  if (spec.tables) {
@@ -16,6 +16,7 @@ import {
16
16
  lstatSync,
17
17
  } from "node:fs";
18
18
  import { dirname, join, resolve } from "node:path";
19
+ import { openDatabase } from "./database.ts";
19
20
  import { spawnSync } from "node:child_process";
20
21
 
21
22
  export const EXTERNAL_EFFECT_SCHEMA = "kxm.external-effect-receipt.v1" as const;
@@ -114,6 +115,33 @@ export function computeEffectKey(
114
115
  return `eff_${createHash("sha256").update(raw).digest("hex").slice(0, 16)}`;
115
116
  }
116
117
 
118
+ const EXTERNAL_EFFECTS_SCHEMA_VERSION = 1;
119
+
120
+ const EXTERNAL_EFFECTS_DDL = `
121
+ CREATE TABLE IF NOT EXISTS external_effects (
122
+ effect_key TEXT PRIMARY KEY,
123
+ run_id TEXT NOT NULL,
124
+ step_id TEXT NOT NULL,
125
+ attempt_id TEXT NOT NULL,
126
+ action_kind TEXT NOT NULL,
127
+ target_ref TEXT NOT NULL,
128
+ status TEXT NOT NULL,
129
+ payload_hash TEXT NOT NULL,
130
+ receipt_payload TEXT NOT NULL,
131
+ executed_at TEXT NOT NULL,
132
+ last_heartbeat_at TEXT,
133
+ completed_at TEXT
134
+ );
135
+ CREATE INDEX IF NOT EXISTS idx_ext_effects_run ON external_effects(run_id);
136
+ `;
137
+
138
+ const EXTERNAL_EFFECTS_SHAPE = {
139
+ external_effects: [
140
+ "effect_key", "run_id", "step_id", "attempt_id", "action_kind", "target_ref",
141
+ "status", "payload_hash", "receipt_payload", "executed_at", "last_heartbeat_at", "completed_at",
142
+ ],
143
+ } as const;
144
+
117
145
  export class ExternalEffectsLedger {
118
146
  private db: DatabaseSync;
119
147
 
@@ -121,33 +149,15 @@ export class ExternalEffectsLedger {
121
149
  if (dbPath !== ":memory:") {
122
150
  mkdirSync(dirname(dbPath), { recursive: true });
123
151
  }
124
- this.db = new DatabaseSync(dbPath);
125
- this.initSchema();
126
- }
127
-
128
- private initSchema(): void {
129
- this.db.exec(`
130
- CREATE TABLE IF NOT EXISTS external_effects (
131
- effect_key TEXT PRIMARY KEY,
132
- run_id TEXT NOT NULL,
133
- step_id TEXT NOT NULL,
134
- attempt_id TEXT NOT NULL,
135
- action_kind TEXT NOT NULL,
136
- target_ref TEXT NOT NULL,
137
- status TEXT NOT NULL,
138
- payload_hash TEXT NOT NULL,
139
- receipt_payload TEXT NOT NULL,
140
- executed_at TEXT NOT NULL,
141
- last_heartbeat_at TEXT,
142
- completed_at TEXT
143
- );
144
- CREATE INDEX IF NOT EXISTS idx_ext_effects_run ON external_effects(run_id);
145
- `);
146
- try {
147
- this.db.exec(`ALTER TABLE external_effects ADD COLUMN last_heartbeat_at TEXT;`);
148
- } catch {
149
- // Column already present in schema
150
- }
152
+ // Goes through the shared opener, not a bare `new DatabaseSync`: that is what gives this
153
+ // store the same contract as every other one — path checks, WAL, the `user_version` gate
154
+ // that refuses an older stamp outright, and the column-shape check that turns a missing
155
+ // column into `runtime_schema_shape_invalid` here instead of a bare SQL error on first use.
156
+ this.db = openDatabase(dbPath, "external effects ledger", {
157
+ schema: EXTERNAL_EFFECTS_DDL,
158
+ version: EXTERNAL_EFFECTS_SCHEMA_VERSION,
159
+ tables: EXTERNAL_EFFECTS_SHAPE,
160
+ });
151
161
  }
152
162
 
153
163
  /**
@@ -102,20 +102,15 @@ export function kxmCeilingHash(authority: KxmCoordinatorAuthority): string {
102
102
  }
103
103
 
104
104
  /**
105
- * Does an already-stored coordinator express this ceiling?
105
+ * Does a stored coordinator already express the ceiling being requested?
106
106
  *
107
- * A row written before set normalisation existed carries a fingerprint that
108
- * `kxmCeilingHash` no longer reproduces, so comparing the stored hash alone is
109
- * not enough: recompute over the authority it kept. Every path that asks this
110
- * question — the initial slot lookup and **both** lost-write read-backs — must
111
- * go through here, or an upgrade makes the same row equivalent on lookup and a
112
- * `coordinator_write_lost` conflict on the race path.
113
- *
114
- * A legacy row is returned as stored, so its `ceilingHash` is historical: a
115
- * caller must not assume every persisted hash uses today's algorithm.
107
+ * Stored fingerprint only. Recomputing the fingerprint over the stored authority — the
108
+ * previous behaviour — existed to tolerate rows written before set canonicalisation, and
109
+ * that compatibility is gone with the single-operator decision: no migration lanes, no
110
+ * second schema or hash shape carried forward. A stale row is re-bound, not forgiven.
116
111
  */
117
112
  function ceilingsMatch(stored: KxmCoordinatorRecord, ceilingHash: string): boolean {
118
- return stored.ceilingHash === ceilingHash || kxmCeilingHash(stored.authority) === ceilingHash;
113
+ return stored.ceilingHash === ceilingHash;
119
114
  }
120
115
 
121
116
  function normalizeAuthority(authority: KxmCoordinatorAuthority): KxmCoordinatorAuthority {
@@ -8,7 +8,7 @@ import { AGENT_COMMANDS_MAP, enforceToolPolicy, getMcpTools, reconcileInbox } fr
8
8
  import { deliverInboxNotification } from "./inbox.ts";
9
9
  import type { HubEvent, MessageRecord } from "./protocol.ts";
10
10
 
11
- const VERSION = "0.7.52";
11
+ const VERSION = "0.7.53";
12
12
  const inbox = new Map<string, MessageRecord>();
13
13
  const notifiedInbox = new Set<string>();
14
14
  let meshClient: HubClient | undefined;
@@ -782,74 +782,6 @@ CREATE TABLE project_controls (
782
782
  ) STRICT;
783
783
  `;
784
784
 
785
- const COORDINATOR_INTAKE_DDL = `
786
- CREATE TABLE IF NOT EXISTS coordinators (
787
- coordinator_id TEXT PRIMARY KEY,
788
- project_id TEXT NOT NULL,
789
- role TEXT NOT NULL,
790
- channel TEXT NOT NULL,
791
- ceiling_hash TEXT NOT NULL,
792
- config_revision TEXT NOT NULL,
793
- bound_at TEXT NOT NULL,
794
- schema TEXT NOT NULL,
795
- record TEXT NOT NULL
796
- ) STRICT;
797
- CREATE UNIQUE INDEX IF NOT EXISTS coordinators_slot ON coordinators(project_id, role, channel);
798
- CREATE TABLE IF NOT EXISTS intake_messages (
799
- message_id TEXT PRIMARY KEY,
800
- project_id TEXT NOT NULL,
801
- coordinator_id TEXT NOT NULL,
802
- idempotency_key TEXT NOT NULL,
803
- content_hash TEXT NOT NULL,
804
- received_at TEXT NOT NULL,
805
- dispatch_state TEXT NOT NULL CHECK (dispatch_state IN ('ready','held_paused','admitted','refused')),
806
- schema TEXT NOT NULL,
807
- record TEXT NOT NULL
808
- ) STRICT;
809
- CREATE UNIQUE INDEX IF NOT EXISTS intake_idempotency ON intake_messages(project_id, coordinator_id, idempotency_key);
810
- CREATE INDEX IF NOT EXISTS intake_dispatch ON intake_messages(project_id, dispatch_state, received_at, message_id);
811
- CREATE TABLE IF NOT EXISTS project_controls (
812
- project_id TEXT PRIMARY KEY,
813
- paused INTEGER NOT NULL CHECK (paused IN (0, 1)),
814
- reason TEXT,
815
- updated_at TEXT NOT NULL,
816
- actor TEXT NOT NULL,
817
- schema TEXT NOT NULL,
818
- record TEXT NOT NULL
819
- ) STRICT;
820
- `;
821
-
822
- const DRIVE_RECEIPTS_DDL = `
823
- CREATE TABLE IF NOT EXISTS drive_receipts (
824
- drive_id TEXT PRIMARY KEY,
825
- run_id TEXT NOT NULL,
826
- project_id TEXT NOT NULL,
827
- opened_sequence INTEGER NOT NULL,
828
- last_sequence INTEGER NOT NULL,
829
- closed_at TEXT NOT NULL,
830
- schema TEXT NOT NULL,
831
- receipt TEXT NOT NULL
832
- ) STRICT;
833
- CREATE INDEX IF NOT EXISTS drive_receipts_run ON drive_receipts(run_id);
834
- `;
835
-
836
- const EVENT_STORE_MIGRATIONS = [
837
- {
838
- fromVersion: 3,
839
- toVersion: 4,
840
- migrate(database: DatabaseSync): void {
841
- database.exec(DRIVE_RECEIPTS_DDL);
842
- },
843
- },
844
- {
845
- fromVersion: 4,
846
- toVersion: 5,
847
- migrate(database: DatabaseSync): void {
848
- database.exec(COORDINATOR_INTAKE_DDL);
849
- },
850
- },
851
- ];
852
-
853
785
  /** One persisted coordinator identity (`kxm.coordinator.v1`). */
854
786
  export interface KxmCoordinatorRow {
855
787
  coordinatorId: string;
@@ -894,7 +826,6 @@ export class KxmRunEventStore {
894
826
  schema: EVENT_STORE_SCHEMA,
895
827
  version: KXM_EVENT_STORE_SCHEMA_VERSION,
896
828
  tables: EVENT_STORE_TABLES,
897
- migrations: EVENT_STORE_MIGRATIONS,
898
829
  });
899
830
  }
900
831
 
@@ -6,7 +6,6 @@ import type { WorkflowJournalEntry, WorkflowRun } from "./workflow.ts";
6
6
  import {
7
7
  openDatabase,
8
8
  withDatabaseTransaction,
9
- type DatabaseMigrationStep,
10
9
  type DatabaseSchemaSpec,
11
10
  } from "./database.ts";
12
11
 
@@ -73,101 +72,10 @@ export const HUB_STORE_SCHEMA_V3 = `
73
72
  CREATE INDEX IF NOT EXISTS context_items_project ON context_items(project);
74
73
  `;
75
74
 
76
- export const HUB_STORE_MIGRATIONS: readonly DatabaseMigrationStep[] = Object.freeze([
77
- {
78
- fromVersion: 1,
79
- toVersion: 2,
80
- migrate: (db: DatabaseSync) => {
81
- db.exec(`
82
- CREATE TABLE IF NOT EXISTS agents (
83
- id TEXT PRIMARY KEY,
84
- record TEXT NOT NULL
85
- ) STRICT;
86
- CREATE TABLE IF NOT EXISTS messages (
87
- id TEXT PRIMARY KEY,
88
- record TEXT NOT NULL
89
- ) STRICT;
90
- CREATE TABLE IF NOT EXISTS workflow_runs (
91
- id TEXT PRIMARY KEY,
92
- definition_id TEXT NOT NULL,
93
- delivery_id TEXT NOT NULL,
94
- record TEXT NOT NULL,
95
- UNIQUE(definition_id, delivery_id)
96
- ) STRICT;
97
- CREATE TABLE IF NOT EXISTS workflow_journal (
98
- id TEXT PRIMARY KEY,
99
- run_id TEXT NOT NULL,
100
- category TEXT NOT NULL,
101
- area TEXT NOT NULL,
102
- record TEXT NOT NULL
103
- ) STRICT;
104
- CREATE INDEX IF NOT EXISTS workflow_journal_run_id ON workflow_journal(run_id);
105
- `);
106
- },
107
- },
108
- {
109
- fromVersion: 2,
110
- toVersion: 3,
111
- migrate: (db: DatabaseSync) => {
112
- db.exec(`
113
- CREATE TABLE IF NOT EXISTS agents (
114
- id TEXT PRIMARY KEY,
115
- record TEXT NOT NULL
116
- ) STRICT;
117
- CREATE TABLE IF NOT EXISTS messages (
118
- id TEXT PRIMARY KEY,
119
- record TEXT NOT NULL
120
- ) STRICT;
121
- CREATE TABLE IF NOT EXISTS workflow_runs (
122
- id TEXT PRIMARY KEY,
123
- definition_id TEXT NOT NULL,
124
- delivery_id TEXT NOT NULL,
125
- record TEXT NOT NULL,
126
- UNIQUE(definition_id, delivery_id)
127
- ) STRICT;
128
- CREATE TABLE IF NOT EXISTS workflow_journal (
129
- id TEXT PRIMARY KEY,
130
- run_id TEXT NOT NULL,
131
- category TEXT NOT NULL,
132
- area TEXT NOT NULL,
133
- record TEXT NOT NULL
134
- ) STRICT;
135
- CREATE INDEX IF NOT EXISTS workflow_journal_run_id ON workflow_journal(run_id);
136
- CREATE TABLE IF NOT EXISTS context_items (
137
- id TEXT PRIMARY KEY,
138
- project TEXT NOT NULL,
139
- kind TEXT NOT NULL,
140
- record TEXT NOT NULL
141
- ) STRICT;
142
- CREATE INDEX IF NOT EXISTS context_items_project ON context_items(project);
143
- CREATE UNIQUE INDEX IF NOT EXISTS messages_from_idempotency
144
- ON messages(
145
- json_extract(record, '$.from'),
146
- json_extract(record, '$.idempotencyKey')
147
- ) WHERE json_extract(record, '$.idempotencyKey') IS NOT NULL;
148
- CREATE INDEX IF NOT EXISTS messages_to_seq
149
- ON messages(
150
- json_extract(record, '$.to'),
151
- COALESCE(json_extract(record, '$.seq'), 0)
152
- );
153
- CREATE TABLE IF NOT EXISTS consumer_cursors (
154
- agent_id TEXT PRIMARY KEY,
155
- cursor INTEGER NOT NULL
156
- ) STRICT;
157
- CREATE TABLE IF NOT EXISTS agent_sequences (
158
- agent_id TEXT PRIMARY KEY,
159
- next_seq INTEGER NOT NULL
160
- ) STRICT;
161
- `);
162
- },
163
- },
164
- ]);
165
-
166
75
  export const HUB_STORE_SCHEMA_SPEC: DatabaseSchemaSpec = Object.freeze({
167
76
  schema: HUB_STORE_SCHEMA_V3,
168
77
  version: HUB_STORE_SCHEMA_VERSION,
169
78
  tables: HUB_STORE_TABLES,
170
- migrations: HUB_STORE_MIGRATIONS,
171
79
  });
172
80
 
173
81
  export interface StoredAgent extends AgentRecord {