@kontextmind/kxm 0.7.78 → 0.7.80

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.78",
14
+ "version": "0.7.80",
15
15
  "category": "development",
16
16
  "tags": ["kxm", "multi-agent", "workflows", "mcp"]
17
17
  }
@@ -1,8 +1,9 @@
1
1
  # Hub synchronization contract
2
2
 
3
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
4
+ > implementation** (cross-host P5): the event store `outbox` (v7, which adds
5
+ > `attempt_count` and the refusal columns named below), the `sync-transform.ts`
6
+ > derivation under the default policy, supervisor push to
6
7
  > `POST /v1/sync/events`, and hub ingestion into `sync_events` (hub store v5).
7
8
  > Custom project policies and on-demand content transfer remain unimplemented.
8
9
  > Phase 8 gate: [implementation plan](../../plans/implementation-plan.md#phase-8-multi-project-hub-kxm).
@@ -87,6 +88,12 @@ values. Extensions require a new reviewed schema revision.
87
88
  The outbox stores only the already-derived sync object plus retry transport
88
89
  metadata. It does not retain the full local source payload for later redaction.
89
90
 
91
+ A row sits in exactly one of three states: **pending** (retryable), **acked**
92
+ (the hub holds it), or **refused** (the hub answered in a way that re-sending the
93
+ same bytes cannot change). `refused_code` carries the hub's own code, and it is
94
+ what keeps a permanently unacceptable row from being re-pushed on every tick —
95
+ see Ordering and idempotency.
96
+
90
97
  ## Redaction
91
98
 
92
99
  Before transformation, the Runtime registers resolved secret values with an
@@ -108,13 +115,34 @@ values MUST NOT be persisted merely to support later redaction.
108
115
  - The hub accepts an exact event once by `{projectId, runId, sequence}`.
109
116
  - Repeating identical bytes is idempotent.
110
117
  - Reusing a sequence with different content is a conflict and security alert.
118
+ - The hub answers **one result per event, in request order**. The Runtime reads the
119
+ answer at its row's index and verifies the echoed `runId`/`sequence` against the
120
+ row it claims to describe; an answer that does not describe its row is treated as
121
+ no answer at all, never as an acknowledgement.
122
+ - The project a Runtime names on the wire MUST be the same identity its sync events
123
+ carry — the `prj_*` id in `.kxm/project.yaml`. The hub pins a project id to the
124
+ first hub project that claims it, so a second label on the same facts is refused
125
+ forever after, not merely once.
111
126
  - A gap remains pending until filled or explicitly declared unavailable.
112
127
  - Hub projections never invent missing events.
113
128
  - A hub acknowledgement advances the outbox cursor; loss before acknowledgement
114
129
  causes a safe transport retry.
130
+ - A **durable** refusal — `sync_sequence_reused`, `sync_project_mismatch`,
131
+ `sync_home_runtime_mismatch`, `sync_event_invalid`, or a row too large to carry —
132
+ moves that row out of the pending queue with the hub's code, because the next tick
133
+ would raise the same alert on the same bytes. Nothing is deleted: the row, its
134
+ code and its attempt count stay until an operator clears the refusal
135
+ (`kxm runtime sync-retry`) after correcting the hub-side state. Only an operator
136
+ decides that a refusal has become retryable.
137
+ - A **transient** failure (unreachable hub, refused credential, a batch that
138
+ isolation could not resolve) leaves every row pending, records the reason, and
139
+ backs the next attempt off exponentially.
115
140
 
116
141
  Synchronization delay does not pause local execution except when the next action
117
- requires a shared-operation lease.
142
+ requires a shared-operation lease. It is never silent either: the supervisor keeps a
143
+ per-project sync state (pending, acked and refused counts, the last failure and when
144
+ the next attempt is due) at `GET /v1/sync/status`, surfaced by `kxm runtime status`
145
+ and logged once per state change.
118
146
 
119
147
  ## Prompts and evidence on demand
120
148
 
@@ -264,6 +264,7 @@ a backup goes missing while looking complete:
264
264
  | `$S/runtime/registry.db` | Runtime registry, including the **supervisor identity and claim row** | which projects this Runtime knows; the claim is a registry row — there is no `supervisor.json` |
265
265
  | `$S/runtime/projects/<projectKey>/run-events.db` (+ `-wal`/`-shm`) | event-sourced run state, commands, drives, receipts, gate evidence, intake, coordinators, pause control | run history and every receipt that proves it |
266
266
  | `$S/runtime/projects/<projectKey>/run-events.db.run-prompts.json` | prompt text; the sidecar name appends to the **full** database filename | the prompts that explain the runs — restoring databases without sidecars is a partial restore |
267
+ | `$S/runtime/logs/kxm-runtime.jsonl` (+ rotated `.1`…) | the Runtime supervisor's own structured log, including every outbound-sync state change | the supervisor runs detached with no stdio: this file and `GET /v1/sync/status` are its only voice |
267
268
  | `$S/projects/<control-root-hash>/repository-bindings.json` | host-local member repository paths | member bindings are host state, outside the project tree |
268
269
  | `$S/update.yaml` | release/update configuration consumed by the updater | the box reverts to defaults on the next update path |
269
270
  | `$W/pi-sessions/<workerKey>/{default,runs/<runId>}/` | Pi model histories | **optional by existing policy** (see *Workflow-specific Pi sessions*): never a system of record — decide and record, do not silently widen scope |
@@ -449,7 +450,7 @@ holder. `kxm_leases_granted_total`, `kxm_leases_refused_total` and
449
450
  ### Runtime → hub run-fact sync
450
451
 
451
452
  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
+ `outbox` (event store v7), in the same transaction. The row holds only a derived
453
454
  `kxm.sync-event.v1` object — allowlisted fields, registered secret values and
454
455
  credential shapes replaced, absolute paths removed, text bounded, the default
455
456
  sync policy revision recorded — never the local event. A field the allowlist
@@ -467,6 +468,31 @@ project, or a push for another Runtime's events are refused and logged as
467
468
  `security_alert`. Out-of-order events are held, and the per-run cursor is the
468
469
  gapless prefix, so a gap stays pending until it is filled.
469
470
 
471
+ The project on the wire is the project the sync events carry — the `prj_*` id in
472
+ `.kxm/project.yaml`, not the package name. The hub pins a project id to the first
473
+ hub project that claims it, so a Runtime that ever pushed under a second label
474
+ leaves its own later pushes refused; the supervisor's sync status names that
475
+ refusal instead of hiding it.
476
+
477
+ Two kinds of hub answer come back, and they are not the same thing. A
478
+ **transient** failure (unreachable hub, refused credential) leaves every row
479
+ pending, records the reason and backs the next attempt off exponentially. A
480
+ **durable** refusal (`sync_sequence_reused`, `sync_project_mismatch`,
481
+ `sync_home_runtime_mismatch`, `sync_event_invalid`, a row too large to carry)
482
+ takes *that row* out of the pending queue with the hub's code, so one row the hub
483
+ will never accept can neither block the rows behind it nor re-alert the hub every
484
+ ten seconds. Refused rows are not deleted: `kxm runtime sync-retry` re-queues them
485
+ once the hub-side state is corrected, and only an operator decides that a refusal
486
+ has become retryable.
487
+
488
+ `kxm runtime status` prints what the tick last saw per project — `ok`, `no_hub`,
489
+ `blocked` or `refusing`, with pending/acked/refused counts, the refusal codes, the
490
+ last failure and the next attempt — read from the supervisor's
491
+ `GET /v1/sync/status`. The same transitions are logged to
492
+ `$S/runtime/logs/kxm-runtime.jsonl` (`runtime_sync_state`,
493
+ `runtime_sync_stalled`), one line per change: the supervisor runs detached with no
494
+ stdio, so that file and that endpoint are its only voice.
495
+
470
496
  `GET /v1/ops/snapshot` adds `homeRuntimes`: synchronized runs grouped by home
471
497
  Runtime, each with its bounded title/status, `lastSequence`, `pendingGap`, and
472
498
  `orphaned` once the Runtime's presence lease (`heartbeatAt + staleAfterMs`, hub
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontextmind/kxm",
3
- "version": "0.7.78",
3
+ "version": "0.7.80",
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.78",
5
+ "version": "0.7.80",
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",
@@ -23980,20 +23980,30 @@ function restoreDatabaseFile(backupPath, targetPath, storeId, expectedSchemaVers
23980
23980
  integrity: "ok"
23981
23981
  };
23982
23982
  }
23983
+ var KXM_BACKUP_CEILINGS = {
23984
+ "hub-store": 5,
23985
+ registry: 1,
23986
+ "binding-store": 1,
23987
+ events: 7
23988
+ };
23989
+ function kxmBackupCeiling(storeId) {
23990
+ if (storeId.startsWith("events:")) return KXM_BACKUP_CEILINGS.events;
23991
+ return KXM_BACKUP_CEILINGS[storeId] ?? KXM_BACKUP_CEILINGS["hub-store"];
23992
+ }
23983
23993
  function discoverProjectStores(projectRoot, options = {}) {
23984
23994
  const root = resolve5(projectRoot);
23985
23995
  const stores = [];
23986
23996
  const hubPath = options.hubDataPath ? resolve5(options.hubDataPath) : join8(root, ".kxm", "state", "kxm.db");
23987
23997
  if (existsSync8(hubPath)) {
23988
- stores.push({ storeId: "hub-store", sourcePath: hubPath, maxSupportedVersion: 5 });
23998
+ stores.push({ storeId: "hub-store", sourcePath: hubPath, maxSupportedVersion: kxmBackupCeiling("hub-store") });
23989
23999
  }
23990
24000
  const registryPath = join8(root, ".kxm", "runtime", "registry.db");
23991
24001
  if (existsSync8(registryPath)) {
23992
- stores.push({ storeId: "registry", sourcePath: registryPath, maxSupportedVersion: 1 });
24002
+ stores.push({ storeId: "registry", sourcePath: registryPath, maxSupportedVersion: kxmBackupCeiling("registry") });
23993
24003
  }
23994
24004
  const bindingsPath = join8(root, ".kxm", "runtime", "bindings.db");
23995
24005
  if (existsSync8(bindingsPath)) {
23996
- stores.push({ storeId: "binding-store", sourcePath: bindingsPath, maxSupportedVersion: 1 });
24006
+ stores.push({ storeId: "binding-store", sourcePath: bindingsPath, maxSupportedVersion: kxmBackupCeiling("binding-store") });
23997
24007
  }
23998
24008
  const eventsDir = join8(root, ".kxm", "runtime", "events");
23999
24009
  if (existsSync8(eventsDir)) {
@@ -24004,7 +24014,7 @@ function discoverProjectStores(projectRoot, options = {}) {
24004
24014
  stores.push({
24005
24015
  storeId: `events:${key}`,
24006
24016
  sourcePath: join8(eventsDir, entry.name),
24007
- maxSupportedVersion: 6
24017
+ maxSupportedVersion: kxmBackupCeiling(`events:${key}`)
24008
24018
  });
24009
24019
  }
24010
24020
  }
@@ -24091,12 +24101,7 @@ function restoreBackup(manifestPathOrDir, options = {}) {
24091
24101
  `backup file ${store.backupFile} sha256 ${actualSha256} does not match manifest hash ${store.sha256}`
24092
24102
  );
24093
24103
  }
24094
- let maxSupported = 5;
24095
- if (store.storeId === "registry" || store.storeId === "binding-store") {
24096
- maxSupported = 1;
24097
- } else if (store.storeId.startsWith("events:")) {
24098
- maxSupported = 6;
24099
- }
24104
+ const maxSupported = kxmBackupCeiling(store.storeId);
24100
24105
  let targetPath = store.sourcePath;
24101
24106
  if (options.projectRoot && manifest.projectRoot && targetPath.startsWith(manifest.projectRoot)) {
24102
24107
  const rel = targetPath.slice(manifest.projectRoot.length).replace(/^[\\/]+/, "");
@@ -24449,7 +24454,17 @@ var EVENT_STORE_TABLES = {
24449
24454
  "record"
24450
24455
  ],
24451
24456
  project_controls: ["project_id", "paused", "reason", "updated_at", "actor", "schema", "record"],
24452
- outbox: ["seq", "run_id", "sequence", "sync_event", "attempted_at", "acked_at"]
24457
+ outbox: [
24458
+ "seq",
24459
+ "run_id",
24460
+ "sequence",
24461
+ "sync_event",
24462
+ "attempted_at",
24463
+ "attempt_count",
24464
+ "acked_at",
24465
+ "refused_code",
24466
+ "refused_at"
24467
+ ]
24453
24468
  };
24454
24469
  var KXM_EVENT_STORE_TABLE_NAMES = Object.keys(EVENT_STORE_TABLES).sort();
24455
24470
 
@@ -25970,6 +25985,9 @@ async function probeHubHealth(url, fetchImpl, timeoutMs = HUB_HEALTH_PROBE_MS) {
25970
25985
  }
25971
25986
  }
25972
25987
 
25988
+ // plugins/kxm/src/logger.ts
25989
+ var DEFAULT_LOG_MAX_BYTES = 10 * 1024 * 1024;
25990
+
25973
25991
  // plugins/kxm/src/runtime-supervisor.ts
25974
25992
  function kxmSupervisorTokenFile(paths) {
25975
25993
  return join13(paths.runtimeDir, "supervisor.token");
@@ -26117,6 +26135,7 @@ async function ensureKxmSupervisor(options = {}) {
26117
26135
  }
26118
26136
  throw runtimeError("runtime_supervisor_start_failed", scriptPath, `runtime supervisor (pid ${pid}) did not become ready in time`);
26119
26137
  }
26138
+ var RUNTIME_SYNC_MAX_BACKOFF_MS = 5 * 6e4;
26120
26139
  async function kxmRuntimeRequest(handle, method, path4, body) {
26121
26140
  const response = await fetch(`http://127.0.0.1:${handle.port}${path4}`, {
26122
26141
  method,
@@ -32758,9 +32777,32 @@ async function cmdKxmRuntime(runtime, action) {
32758
32777
  }
32759
32778
  if (action === "status") {
32760
32779
  const status = kxmSupervisorStatus(paths);
32761
- print(runtime.io, runtime.json, { ok: true, command: "runtime status", ...status }, status.running ? `runtime supervisor running: ${status.runtimeId} pid ${status.pid} on 127.0.0.1:${status.port}` : "runtime supervisor is not running");
32780
+ const sync = status.running ? await readKxmSupervisorSync(runtime) : void 0;
32781
+ print(runtime.io, runtime.json, { ok: true, command: "runtime status", ...status, ...sync ? { sync } : {} }, [
32782
+ status.running ? `runtime supervisor running: ${status.runtimeId} pid ${status.pid} on 127.0.0.1:${status.port}` : "runtime supervisor is not running",
32783
+ ...formatKxmSyncStatus(sync)
32784
+ ].join("\n"));
32762
32785
  return status.running ? 0 : 1;
32763
32786
  }
32787
+ if (action === "sync-retry") {
32788
+ const supervisor = await attachKxmSupervisor({ env: runtime.env });
32789
+ if (!supervisor) {
32790
+ print(runtime.io, runtime.json, { ok: false, command: "runtime sync-retry", error: "runtime_not_running" }, "runtime supervisor is not running");
32791
+ return 1;
32792
+ }
32793
+ const projectRoot = discoverKxmProjectRoot(runtime.cwd);
32794
+ if (!projectRoot) {
32795
+ print(runtime.io, runtime.json, { ok: false, command: "runtime sync-retry", error: "project_required" }, "kxm runtime sync-retry requires a KXM project (run kxm init first)");
32796
+ return 1;
32797
+ }
32798
+ if (runtime.dryRun) {
32799
+ print(runtime.io, runtime.json, { ok: true, command: "runtime sync-retry", projectRoot, dryRun: true }, "would re-queue rows the hub durably refused");
32800
+ return 0;
32801
+ }
32802
+ const result = await kxmRuntimeRequest(supervisor, "POST", "/v1/sync/retry", { projectRoot });
32803
+ print(runtime.io, runtime.json, { ok: true, command: "runtime sync-retry", ...result }, `re-queued ${String(result.retried ?? 0)} refused outbox rows for ${String(result.projectId ?? projectRoot)}`);
32804
+ return 0;
32805
+ }
32764
32806
  if (action === "stop") {
32765
32807
  const status = kxmSupervisorStatus(paths);
32766
32808
  if (!status.running || !status.port) {
@@ -32787,6 +32829,26 @@ async function cmdKxmRuntime(runtime, action) {
32787
32829
  return 1;
32788
32830
  }
32789
32831
  }
32832
+ async function readKxmSupervisorSync(runtime) {
32833
+ const supervisor = await attachKxmSupervisor({ env: runtime.env });
32834
+ if (!supervisor) return void 0;
32835
+ try {
32836
+ const response = await kxmRuntimeRequest(supervisor, "GET", "/v1/sync/status");
32837
+ return response.projects;
32838
+ } catch {
32839
+ return void 0;
32840
+ }
32841
+ }
32842
+ function formatKxmSyncStatus(sync) {
32843
+ if (sync === void 0) return ["sync: the supervisor did not answer /v1/sync/status"];
32844
+ if (sync.length === 0) return ["sync: no project registered with this Runtime yet"];
32845
+ return sync.map((project) => {
32846
+ const codes = project.outbox.refusals.map((refusal) => `${refusal.code} x${refusal.count}`).join(", ");
32847
+ const counts = `pending ${project.outbox.pending}, acked ${project.outbox.acked}, refused ${project.outbox.refused}`;
32848
+ const tail = project.state === "refusing" ? ` (${codes || "see log"}) \u2014 fix the hub, then: kxm runtime sync-retry` : project.state === "blocked" ? ` \u2014 last error: ${project.lastError ?? "unreachable"}${project.nextAttemptAt ? `; next attempt ${project.nextAttemptAt}` : ""}` : "";
32849
+ return `sync ${project.projectId}: ${project.state} (${counts})${tail}`;
32850
+ });
32851
+ }
32790
32852
  async function cmdKxmRunReceipt(runtime, runId, options = {}) {
32791
32853
  try {
32792
32854
  const projectRoot = discoverKxmProjectRoot(runtime.cwd);
@@ -47847,6 +47909,9 @@ function createProgram(ctx, result) {
47847
47909
  addGlobalOptions(runtimeCmd.command("status").description("Show Runtime supervisor liveness")).action(async function runtimeStatusAction() {
47848
47910
  result.code = await cmdKxmRuntime(runtimeFrom(ctx, this), "status");
47849
47911
  });
47912
+ addGlobalOptions(runtimeCmd.command("sync-retry").description("Re-queue outbox rows the hub durably refused, after the hub-side state is corrected")).action(async function runtimeSyncRetryAction() {
47913
+ result.code = await cmdKxmRuntime(runtimeFrom(ctx, this), "sync-retry");
47914
+ });
47850
47915
  addGlobalOptions(runtimeCmd.command("stop").description("Gracefully stop the Runtime supervisor")).action(async function runtimeStopAction() {
47851
47916
  result.code = await cmdKxmRuntime(runtimeFrom(ctx, this), "stop");
47852
47917
  });
@@ -17182,7 +17182,7 @@ async function deliverInboxNotification(messageId, delivered, notify) {
17182
17182
  }
17183
17183
 
17184
17184
  // plugins/kxm/src/mcp-server.ts
17185
- var VERSION = "0.7.78";
17185
+ var VERSION = "0.7.80";
17186
17186
  var inbox = /* @__PURE__ */ new Map();
17187
17187
  var notifiedInbox = /* @__PURE__ */ new Set();
17188
17188
  var meshClient;