@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.
- 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 +63 -3
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime-supervisor.js +443 -47
- package/plugins/kxm/dist/runtime.js +444 -197
- 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 +1 -1
- 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.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",
|
package/plugins/kxm/dist/cli.js
CHANGED
|
@@ -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 =
|
|
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: [
|
|
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
|
-
|
|
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.
|
|
17185
|
+
var VERSION = "0.7.79";
|
|
17186
17186
|
var inbox = /* @__PURE__ */ new Map();
|
|
17187
17187
|
var notifiedInbox = /* @__PURE__ */ new Set();
|
|
17188
17188
|
var meshClient;
|