@kontextmind/kxm 0.7.78 → 0.7.79

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.79",
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.79",
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.79",
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",
@@ -24095,7 +24095,7 @@ function restoreBackup(manifestPathOrDir, options = {}) {
24095
24095
  if (store.storeId === "registry" || store.storeId === "binding-store") {
24096
24096
  maxSupported = 1;
24097
24097
  } else if (store.storeId.startsWith("events:")) {
24098
- maxSupported = 6;
24098
+ maxSupported = 7;
24099
24099
  }
24100
24100
  let targetPath = store.sourcePath;
24101
24101
  if (options.projectRoot && manifest.projectRoot && targetPath.startsWith(manifest.projectRoot)) {
@@ -24449,7 +24449,17 @@ var EVENT_STORE_TABLES = {
24449
24449
  "record"
24450
24450
  ],
24451
24451
  project_controls: ["project_id", "paused", "reason", "updated_at", "actor", "schema", "record"],
24452
- outbox: ["seq", "run_id", "sequence", "sync_event", "attempted_at", "acked_at"]
24452
+ outbox: [
24453
+ "seq",
24454
+ "run_id",
24455
+ "sequence",
24456
+ "sync_event",
24457
+ "attempted_at",
24458
+ "attempt_count",
24459
+ "acked_at",
24460
+ "refused_code",
24461
+ "refused_at"
24462
+ ]
24453
24463
  };
24454
24464
  var KXM_EVENT_STORE_TABLE_NAMES = Object.keys(EVENT_STORE_TABLES).sort();
24455
24465
 
@@ -25970,6 +25980,9 @@ async function probeHubHealth(url, fetchImpl, timeoutMs = HUB_HEALTH_PROBE_MS) {
25970
25980
  }
25971
25981
  }
25972
25982
 
25983
+ // plugins/kxm/src/logger.ts
25984
+ var DEFAULT_LOG_MAX_BYTES = 10 * 1024 * 1024;
25985
+
25973
25986
  // plugins/kxm/src/runtime-supervisor.ts
25974
25987
  function kxmSupervisorTokenFile(paths) {
25975
25988
  return join13(paths.runtimeDir, "supervisor.token");
@@ -26117,6 +26130,7 @@ async function ensureKxmSupervisor(options = {}) {
26117
26130
  }
26118
26131
  throw runtimeError("runtime_supervisor_start_failed", scriptPath, `runtime supervisor (pid ${pid}) did not become ready in time`);
26119
26132
  }
26133
+ var RUNTIME_SYNC_MAX_BACKOFF_MS = 5 * 6e4;
26120
26134
  async function kxmRuntimeRequest(handle, method, path4, body) {
26121
26135
  const response = await fetch(`http://127.0.0.1:${handle.port}${path4}`, {
26122
26136
  method,
@@ -32758,9 +32772,32 @@ async function cmdKxmRuntime(runtime, action) {
32758
32772
  }
32759
32773
  if (action === "status") {
32760
32774
  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");
32775
+ const sync = status.running ? await readKxmSupervisorSync(runtime) : void 0;
32776
+ print(runtime.io, runtime.json, { ok: true, command: "runtime status", ...status, ...sync ? { sync } : {} }, [
32777
+ status.running ? `runtime supervisor running: ${status.runtimeId} pid ${status.pid} on 127.0.0.1:${status.port}` : "runtime supervisor is not running",
32778
+ ...formatKxmSyncStatus(sync)
32779
+ ].join("\n"));
32762
32780
  return status.running ? 0 : 1;
32763
32781
  }
32782
+ if (action === "sync-retry") {
32783
+ const supervisor = await attachKxmSupervisor({ env: runtime.env });
32784
+ if (!supervisor) {
32785
+ print(runtime.io, runtime.json, { ok: false, command: "runtime sync-retry", error: "runtime_not_running" }, "runtime supervisor is not running");
32786
+ return 1;
32787
+ }
32788
+ const projectRoot = discoverKxmProjectRoot(runtime.cwd);
32789
+ if (!projectRoot) {
32790
+ 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)");
32791
+ return 1;
32792
+ }
32793
+ if (runtime.dryRun) {
32794
+ print(runtime.io, runtime.json, { ok: true, command: "runtime sync-retry", projectRoot, dryRun: true }, "would re-queue rows the hub durably refused");
32795
+ return 0;
32796
+ }
32797
+ const result = await kxmRuntimeRequest(supervisor, "POST", "/v1/sync/retry", { projectRoot });
32798
+ 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)}`);
32799
+ return 0;
32800
+ }
32764
32801
  if (action === "stop") {
32765
32802
  const status = kxmSupervisorStatus(paths);
32766
32803
  if (!status.running || !status.port) {
@@ -32787,6 +32824,26 @@ async function cmdKxmRuntime(runtime, action) {
32787
32824
  return 1;
32788
32825
  }
32789
32826
  }
32827
+ async function readKxmSupervisorSync(runtime) {
32828
+ const supervisor = await attachKxmSupervisor({ env: runtime.env });
32829
+ if (!supervisor) return void 0;
32830
+ try {
32831
+ const response = await kxmRuntimeRequest(supervisor, "GET", "/v1/sync/status");
32832
+ return response.projects;
32833
+ } catch {
32834
+ return void 0;
32835
+ }
32836
+ }
32837
+ function formatKxmSyncStatus(sync) {
32838
+ if (sync === void 0) return ["sync: the supervisor did not answer /v1/sync/status"];
32839
+ if (sync.length === 0) return ["sync: no project registered with this Runtime yet"];
32840
+ return sync.map((project) => {
32841
+ const codes = project.outbox.refusals.map((refusal) => `${refusal.code} x${refusal.count}`).join(", ");
32842
+ const counts = `pending ${project.outbox.pending}, acked ${project.outbox.acked}, refused ${project.outbox.refused}`;
32843
+ 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}` : ""}` : "";
32844
+ return `sync ${project.projectId}: ${project.state} (${counts})${tail}`;
32845
+ });
32846
+ }
32790
32847
  async function cmdKxmRunReceipt(runtime, runId, options = {}) {
32791
32848
  try {
32792
32849
  const projectRoot = discoverKxmProjectRoot(runtime.cwd);
@@ -47847,6 +47904,9 @@ function createProgram(ctx, result) {
47847
47904
  addGlobalOptions(runtimeCmd.command("status").description("Show Runtime supervisor liveness")).action(async function runtimeStatusAction() {
47848
47905
  result.code = await cmdKxmRuntime(runtimeFrom(ctx, this), "status");
47849
47906
  });
47907
+ 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() {
47908
+ result.code = await cmdKxmRuntime(runtimeFrom(ctx, this), "sync-retry");
47909
+ });
47850
47910
  addGlobalOptions(runtimeCmd.command("stop").description("Gracefully stop the Runtime supervisor")).action(async function runtimeStopAction() {
47851
47911
  result.code = await cmdKxmRuntime(runtimeFrom(ctx, this), "stop");
47852
47912
  });
@@ -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.79";
17186
17186
  var inbox = /* @__PURE__ */ new Map();
17187
17187
  var notifiedInbox = /* @__PURE__ */ new Set();
17188
17188
  var meshClient;