@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/docs/contracts/synchronization.md +31 -3
- package/docs/operations.md +27 -1
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +77 -12
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime-supervisor.js +443 -47
- package/plugins/kxm/dist/runtime.js +460 -206
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/src/cli/project.ts +57 -3
- package/plugins/kxm/src/cli.ts +4 -0
- package/plugins/kxm/src/database.ts +32 -17
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/runtime-store.ts +121 -10
- package/plugins/kxm/src/runtime-supervisor.ts +323 -42
|
@@ -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` (
|
|
5
|
-
> `
|
|
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
|
|
package/docs/operations.md
CHANGED
|
@@ -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
|
|
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
|
@@ -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.
|
|
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",
|
package/plugins/kxm/dist/cli.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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: [
|
|
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
|
-
|
|
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.
|
|
17185
|
+
var VERSION = "0.7.80";
|
|
17186
17186
|
var inbox = /* @__PURE__ */ new Map();
|
|
17187
17187
|
var notifiedInbox = /* @__PURE__ */ new Set();
|
|
17188
17188
|
var meshClient;
|