@kontextmind/kxm 0.7.71 → 0.7.73

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.71",
14
+ "version": "0.7.73",
15
15
  "category": "development",
16
16
  "tags": ["kxm", "multi-agent", "workflows", "mcp"]
17
17
  }
package/CHANGELOG.md CHANGED
@@ -6,6 +6,21 @@ All notable user-facing changes are documented here. The project follows [Semant
6
6
 
7
7
  ### Added
8
8
 
9
+ - **Fenced hub leases, and shared external effects that will not run without one.**
10
+ `POST /v1/leases/:resource/acquire|renew|release` are agent-authenticated and
11
+ project-scoped (the hub prefixes the caller's project onto the resource name). Each call
12
+ is a compare-and-set inside one store transaction on the **hub clock**, with TTLs bounded
13
+ to 5 s–10 min. The fencing token starts at 1, survives renewal unchanged, and increments
14
+ only when a new holder takes over an expired lease, so a holder that returns after its
15
+ deadline is told its token was superseded instead of writing behind its replacement.
16
+ Shared external effects (`git-push` to a ref the run does not own, `pr-create`,
17
+ `tracker-issue`, `webhook`) acquire a lease keyed by `targetRef` before executing, record
18
+ `{leaseResource, fencingToken}` in the receipt, renew on the existing Q6 heartbeat, and
19
+ re-present the token at commit; a superseded token leaves the effect `in-flight` and the
20
+ attempt `blocked_uncertain` with nothing retrying. An unreachable hub refuses the effect
21
+ (`effect_lease_unavailable`) rather than executing it unfenced. Unique-namespace kinds
22
+ (`git-branch`, `git-commit`, and a push to the run's own branch) stay lease-free.
23
+
9
24
  - **`kxm peer send --allow-offline` queues to a registered offline peer.**
10
25
  `POST /v1/messages` accepts `allowOffline: true` (also `kxm_send.allowOffline`): a
11
26
  registered agent in the same project is stored `queued` instead of `target_not_found`,
@@ -42,6 +57,13 @@ All notable user-facing changes are documented here. The project follows [Semant
42
57
 
43
58
  ### Changed
44
59
 
60
+ - **Hub store schema v3 → v4, external-effects ledger v1 → v2.** The hub store gains a
61
+ `leases` table and the ledger gains `lease_resource`/`fencing_token` columns. Neither has
62
+ a migration lane: an older file is refused at open with `runtime_schema_outdated`, and the
63
+ hub backup/restore ceiling in `database.ts` moves to 4 with the bump, so a v3 backup must
64
+ be restored with the release that produced it. Delete the state file to start fresh and
65
+ let `kxm hub start` recreate it.
66
+
45
67
  - **`kxm tenant status`: one composed read for the portal, with the authorities labelled.**
46
68
  The portal needs hub metadata (roster, message queue, the hub's run projection) *and* the
47
69
  Runtime's authoritative run state, and the failure mode S2 exists to prevent is rendering
@@ -1,8 +1,11 @@
1
1
  # Hub synchronization contract
2
2
 
3
- > **Status.** `kxm.sync-event.v1` is a **schema-tested contract**. There is no
4
- > outbox table, sync transform, or hub ingestion in source. Implementation
5
- > begins in [Phase 8](../../plans/implementation-plan.md#phase-8-multi-project-hub-kxm).
3
+ > **Status.** `kxm.sync-event.v1` is a **schema-tested contract with an
4
+ > implementation** (cross-host P5): the event store `outbox` (v6), the
5
+ > `sync-transform.ts` derivation under the default policy, supervisor push to
6
+ > `POST /v1/sync/events`, and hub ingestion into `sync_events` (hub store v5).
7
+ > Custom project policies and on-demand content transfer remain unimplemented.
8
+ > Phase 8 gate: [implementation plan](../../plans/implementation-plan.md#phase-8-multi-project-hub-kxm).
6
9
 
7
10
  Synchronization is summary-first, project-scoped, at-least-once, and
8
11
  allowlist-based. The full local event is never placed directly in the outbox.
@@ -315,7 +315,11 @@ restore lands somewhere the running service will not look.
315
315
  copies, not the file copy**: configuration, repository bindings, prompt sidecars,
316
316
  routing manifests and update configuration are not databases, so a snapshot-only backup
317
317
  reproduces exactly the failure this section exists to remove. The hub's own backup path already writes a hashed manifest and records
318
- a schema version ceiling; keep that manifest with the files.
318
+ a schema version ceiling; keep that manifest with the files. That ceiling is
319
+ **hub store v5 and event store v6** as of the sync-outbox release: a backup taken by
320
+ an earlier build records hub v4 or event store v5 and is refused by this one, because
321
+ there is no migration lane.
322
+ Restore such a backup with the release that produced it, or start fresh.
319
323
  3. Record the package version, configuration revision and schema versions beside the copy.
320
324
  A restore that cannot state which release produced it is not a restore path.
321
325
  4. Keep at least one rotation, and bound retention explicitly — run events and prompt
@@ -392,10 +396,11 @@ Broader deployments need shared state and coordination, external identity and fi
392
396
 
393
397
  ## v0.5 context/state storage
394
398
 
395
- The hub database (schema version 3) carries `context_items` alongside
396
- agents, messages, workflow runs, and the journal. Temporal state, knowledge
397
- records, and their audit trails live in the same SQLite file and upgrade in
398
- place from v0.4 databases.
399
+ The hub database (schema version 5) carries `context_items`, `leases`,
400
+ `sync_events` and `runtime_presence` alongside agents, messages, workflow runs,
401
+ and the journal. Temporal state,
402
+ knowledge records, and their audit trails live in the same SQLite file and
403
+ upgrade in place from v0.4 databases.
399
404
 
400
405
  - **Backup and restore**: include the hub database file and, if used, the
401
406
  `.kxm/skills/` and `.kxm/knowledge/` trees. The wiki is a compiled view and
@@ -412,6 +417,64 @@ place from v0.4 databases.
412
417
  to open on an older runtime). Restore a database backup taken before the
413
418
  upgrade instead.
414
419
 
420
+ ### Fenced leases over shared resources
421
+
422
+ `leases` holds one row per project-scoped resource: the holder, a monotonic
423
+ fencing token, and a deadline. `POST /v1/leases/:resource/acquire|renew|release`
424
+ are agent-authenticated and scoped to the caller's project, which the hub
425
+ prefixes onto the resource name — two projects naming the same branch never
426
+ contend. TTLs are bounded to 5 s–10 min and every decision is made on the **hub
427
+ clock** inside one store transaction, so a skewed client cannot extend its own
428
+ grip.
429
+
430
+ The token is the safety property. It starts at 1, stays put across renewals, and
431
+ increments only when a new holder takes over an expired lease. A holder that
432
+ comes back after its deadline is therefore told its token was superseded rather
433
+ than allowed to write behind whoever replaced it. Shared external effects
434
+ (`git-push` to a ref the run does not own, `pr-create`, `tracker-issue`,
435
+ `webhook`) take a lease before executing and re-present the token at commit; a
436
+ superseded token leaves the effect `in-flight` and the attempt
437
+ `blocked_uncertain` for an operator to resolve, and nothing retries it. An
438
+ unreachable hub refuses the effect (`effect_lease_unavailable`) rather than
439
+ running it unfenced.
440
+
441
+ Expired rows are **not** reaped immediately — their token is what the next
442
+ takeover has to increment past. The retention sweep drops rows whose deadline is
443
+ older than the run-retention window (7 days by default), far beyond any live
444
+ holder. `kxm_leases_granted_total`, `kxm_leases_refused_total` and
445
+ `kxm_leases_released_total` in `/metrics` report contention;
446
+ `lease_acquired`, `lease_renewed`, `lease_released`, `lease_denied` and
447
+ `lease_purged` are the structured log events.
448
+
449
+ ### Runtime → hub run-fact sync
450
+
451
+ Every event the Runtime commits also writes one row to the event store's
452
+ `outbox` (event store v6), in the same transaction. The row holds only a derived
453
+ `kxm.sync-event.v1` object — allowlisted fields, registered secret values and
454
+ credential shapes replaced, absolute paths removed, text bounded, the default
455
+ sync policy revision recorded — never the local event. A field the allowlist
456
+ does not name is listed by name in `redaction.fieldsOmitted` and its value is
457
+ dropped.
458
+
459
+ The supervisor pushes outbound only: every `KXM_RUNTIME_SYNC_INTERVAL_MS` (10 s
460
+ by default) it posts `POST /v1/runtime/presence` for each open project, then
461
+ `POST /v1/sync/events` in outbox order, to `KXM_SERVER_URL` or the `kxm hub bind`
462
+ URL with that project's token. No bound hub means nothing is sent and rows stay
463
+ pending; local execution never waits on sync. The hub accepts each event once by
464
+ `{projectId, runId, sequence}`: the same bytes again are an idempotent
465
+ `duplicate`; different bytes under a used sequence, a run claimed by another
466
+ project, or a push for another Runtime's events are refused and logged as
467
+ `security_alert`. Out-of-order events are held, and the per-run cursor is the
468
+ gapless prefix, so a gap stays pending until it is filled.
469
+
470
+ `GET /v1/ops/snapshot` adds `homeRuntimes`: synchronized runs grouped by home
471
+ Runtime, each with its bounded title/status, `lastSequence`, `pendingGap`, and
472
+ `orphaned` once the Runtime's presence lease (`heartbeatAt + staleAfterMs`, hub
473
+ clock) has lapsed. Orphaned is view state; nothing is migrated.
474
+ `kxm_sync_events_accepted_total`, `kxm_sync_events_duplicate_total`,
475
+ `kxm_sync_events_refused_total`, `kxm_sync_conflicts_total` and
476
+ `kxm_runtime_heartbeats_total` are in `/metrics`.
477
+
415
478
  ## Hub Q&A / knowledge base
416
479
 
417
480
  - [What is all stored on the hub?](kb/qa-what-the-hub-stores.md)
@@ -23,6 +23,7 @@ npm run verify
23
23
  | Queue, acknowledgement, visibility, reply, and authorization | `test/core/hub-api.test.ts`, `test/core/hub.test.ts` |
24
24
  | Queued/delivered replay after recipient restart reuses one message record | `test/core/hub.test.ts`, `test/core/extension.test.ts`, `test/core/mcp.test.ts` |
25
25
  | Known-offline peer send with `allowOffline` queues, delivers once on resumption, and expires unread by TTL | `test/core/hub-api.test.ts` |
26
+ | Fenced hub leases: CAS acquire/renew/release, monotonic token on takeover only, hub-clocked expiry, and a shared external effect refused at commit under a superseded token | `test/core/hub-api.test.ts`, `test/core/store.test.ts`, `test/core/external-effects.test.ts` |
26
27
  | One-to-three-peer fanout, recoverable local timeouts/aborts, exact retries, and partial-error collection | `test/core/client.test.ts`, `test/core/hub-api.test.ts`, `test/core/extension.test.ts`, `test/core/mcp.test.ts` |
27
28
  | TTL expiry, sender cancellation, and terminal retention | `test/core/hub-api.test.ts` |
28
29
  | Terminal inbound cleanup and next-request activation | `test/core/extension.test.ts`, `test/core/mcp.test.ts` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontextmind/kxm",
3
- "version": "0.7.71",
3
+ "version": "0.7.73",
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.71",
5
+ "version": "0.7.73",
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",