@kontextmind/kxm 0.7.43 → 0.7.45
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/CHANGELOG.md +4 -0
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime-supervisor.js +8 -3
- package/plugins/kxm/dist/runtime.js +8 -3
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/src/engine.ts +17 -2
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/runtime-service.ts +19 -1
- package/scripts/kxm-hub.mjs +36 -0
package/CHANGELOG.md
CHANGED
|
@@ -38,6 +38,10 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
38
38
|
happens to share the product's version is left alone, and a workspace package
|
|
39
39
|
with no lock entry fails loudly instead of releasing half-bumped.
|
|
40
40
|
|
|
41
|
+
- **`kxm hub start` no longer generates and persists an admin token when it is
|
|
42
|
+
about to refuse** because another hub already owns the claim. A refused start
|
|
43
|
+
used to leave behind credentials the running hub never issued.
|
|
44
|
+
|
|
41
45
|
## 0.7.0 - 2026-09-11
|
|
42
46
|
|
|
43
47
|
### Added
|
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.45",
|
|
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",
|
|
@@ -17121,7 +17121,7 @@ async function deliverInboxNotification(messageId, delivered, notify) {
|
|
|
17121
17121
|
}
|
|
17122
17122
|
|
|
17123
17123
|
// plugins/kxm/src/mcp-server.ts
|
|
17124
|
-
var VERSION = "0.7.
|
|
17124
|
+
var VERSION = "0.7.45";
|
|
17125
17125
|
var inbox = /* @__PURE__ */ new Map();
|
|
17126
17126
|
var notifiedInbox = /* @__PURE__ */ new Set();
|
|
17127
17127
|
var meshClient;
|
|
@@ -21865,7 +21865,8 @@ function openKxmRuntimeContext(projectRoot, options) {
|
|
|
21865
21865
|
projectRoot: bundle.projectRoot,
|
|
21866
21866
|
projectId: registration.projectId,
|
|
21867
21867
|
homeRuntimeId: registration.homeRuntimeId,
|
|
21868
|
-
eventStore
|
|
21868
|
+
eventStore,
|
|
21869
|
+
...options.budgetClock ? { budgetClock: options.budgetClock } : {}
|
|
21869
21870
|
};
|
|
21870
21871
|
} catch (error) {
|
|
21871
21872
|
registry.close();
|
|
@@ -25576,7 +25577,11 @@ function declaredRunDurationBudget(context, runId) {
|
|
|
25576
25577
|
return void 0;
|
|
25577
25578
|
}
|
|
25578
25579
|
}
|
|
25579
|
-
function
|
|
25580
|
+
function budgetNowIso(context) {
|
|
25581
|
+
if (process.env.KXM_DETERMINISTIC_TEST_CLOCK === "1" && context.budgetClock) return context.budgetClock();
|
|
25582
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
25583
|
+
}
|
|
25584
|
+
function runDurationOverrunPayload(context, runId, nowIso = budgetNowIso(context)) {
|
|
25580
25585
|
const declared = declaredRunDurationBudget(context, runId);
|
|
25581
25586
|
if (!declared) return void 0;
|
|
25582
25587
|
const run = context.eventStore.run(runId);
|
|
@@ -25639,7 +25644,7 @@ function armRunDurationBudgetTimer(context, runId) {
|
|
|
25639
25644
|
const fire = () => {
|
|
25640
25645
|
timer = void 0;
|
|
25641
25646
|
if (cleared || isKxmRuntimeContextClosed(context)) return;
|
|
25642
|
-
const nowIso = (
|
|
25647
|
+
const nowIso = budgetNowIso(context);
|
|
25643
25648
|
const elapsedMs = elapsedMsBetween(state.runningSince, nowIso);
|
|
25644
25649
|
if (elapsedMs < declared.budgetMs) {
|
|
25645
25650
|
arm(declared.budgetMs - elapsedMs);
|
|
@@ -22308,7 +22308,8 @@ function openKxmRuntimeContext(projectRoot, options) {
|
|
|
22308
22308
|
projectRoot: bundle.projectRoot,
|
|
22309
22309
|
projectId: registration.projectId,
|
|
22310
22310
|
homeRuntimeId: registration.homeRuntimeId,
|
|
22311
|
-
eventStore
|
|
22311
|
+
eventStore,
|
|
22312
|
+
...options.budgetClock ? { budgetClock: options.budgetClock } : {}
|
|
22312
22313
|
};
|
|
22313
22314
|
} catch (error) {
|
|
22314
22315
|
registry.close();
|
|
@@ -26088,7 +26089,11 @@ function declaredRunDurationBudget(context, runId) {
|
|
|
26088
26089
|
return void 0;
|
|
26089
26090
|
}
|
|
26090
26091
|
}
|
|
26091
|
-
function
|
|
26092
|
+
function budgetNowIso(context) {
|
|
26093
|
+
if (process.env.KXM_DETERMINISTIC_TEST_CLOCK === "1" && context.budgetClock) return context.budgetClock();
|
|
26094
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
26095
|
+
}
|
|
26096
|
+
function runDurationOverrunPayload(context, runId, nowIso2 = budgetNowIso(context)) {
|
|
26092
26097
|
const declared = declaredRunDurationBudget(context, runId);
|
|
26093
26098
|
if (!declared) return void 0;
|
|
26094
26099
|
const run = context.eventStore.run(runId);
|
|
@@ -26151,7 +26156,7 @@ function armRunDurationBudgetTimer(context, runId) {
|
|
|
26151
26156
|
const fire = () => {
|
|
26152
26157
|
timer = void 0;
|
|
26153
26158
|
if (cleared || isKxmRuntimeContextClosed(context)) return;
|
|
26154
|
-
const nowIso2 = (
|
|
26159
|
+
const nowIso2 = budgetNowIso(context);
|
|
26155
26160
|
const elapsedMs = elapsedMsBetween(state.runningSince, nowIso2);
|
|
26156
26161
|
if (elapsedMs < declared.budgetMs) {
|
|
26157
26162
|
arm(declared.budgetMs - elapsedMs);
|
package/plugins/kxm/package.json
CHANGED
|
@@ -2352,10 +2352,25 @@ function declaredRunDurationBudget(context: KxmRuntimeContext, runId: string): K
|
|
|
2352
2352
|
}
|
|
2353
2353
|
}
|
|
2354
2354
|
|
|
2355
|
+
/**
|
|
2356
|
+
* "Now" for run-duration budget accounting.
|
|
2357
|
+
*
|
|
2358
|
+
* Budgets are always measured from the log-derived `runningSince`; this only
|
|
2359
|
+
* supplies the other side of that subtraction. An injected `budgetClock` is
|
|
2360
|
+
* honoured only under `KXM_DETERMINISTIC_TEST_CLOCK=1`, so deterministic engine
|
|
2361
|
+
* tests can pin a budget boundary while no production path — including the
|
|
2362
|
+
* supervisor, which never passes a clock — can postpone a cancellation by
|
|
2363
|
+
* feeding the engine a stale time.
|
|
2364
|
+
*/
|
|
2365
|
+
function budgetNowIso(context: KxmRuntimeContext): string {
|
|
2366
|
+
if (process.env.KXM_DETERMINISTIC_TEST_CLOCK === "1" && context.budgetClock) return context.budgetClock();
|
|
2367
|
+
return new Date().toISOString();
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2355
2370
|
function runDurationOverrunPayload(
|
|
2356
2371
|
context: KxmRuntimeContext,
|
|
2357
2372
|
runId: string,
|
|
2358
|
-
nowIso =
|
|
2373
|
+
nowIso = budgetNowIso(context),
|
|
2359
2374
|
): { budgetMs: number; elapsedMs: number; source: KxmRunDurationBudget["source"] } | undefined {
|
|
2360
2375
|
const declared = declaredRunDurationBudget(context, runId);
|
|
2361
2376
|
if (!declared) return undefined;
|
|
@@ -2432,7 +2447,7 @@ function armRunDurationBudgetTimer(context: KxmRuntimeContext, runId: string): (
|
|
|
2432
2447
|
const fire = (): void => {
|
|
2433
2448
|
timer = undefined;
|
|
2434
2449
|
if (cleared || isKxmRuntimeContextClosed(context)) return;
|
|
2435
|
-
const nowIso =
|
|
2450
|
+
const nowIso = budgetNowIso(context);
|
|
2436
2451
|
const elapsedMs = elapsedMsBetween(state.runningSince!, nowIso);
|
|
2437
2452
|
if (elapsedMs < declared.budgetMs) {
|
|
2438
2453
|
arm(declared.budgetMs - elapsedMs);
|
|
@@ -8,7 +8,7 @@ import { AGENT_COMMANDS_MAP, enforceToolPolicy, getMcpTools, reconcileInbox } fr
|
|
|
8
8
|
import { deliverInboxNotification } from "./inbox.ts";
|
|
9
9
|
import type { HubEvent, MessageRecord } from "./protocol.ts";
|
|
10
10
|
|
|
11
|
-
const VERSION = "0.7.
|
|
11
|
+
const VERSION = "0.7.45";
|
|
12
12
|
const inbox = new Map<string, MessageRecord>();
|
|
13
13
|
const notifiedInbox = new Set<string>();
|
|
14
14
|
let meshClient: HubClient | undefined;
|
|
@@ -234,6 +234,18 @@ export interface KxmRuntimeContext {
|
|
|
234
234
|
projectId: string;
|
|
235
235
|
homeRuntimeId: string;
|
|
236
236
|
eventStore: KxmRunEventStore;
|
|
237
|
+
/**
|
|
238
|
+
* Optional clock for **run-duration budget accounting only**. Budgets are
|
|
239
|
+
* measured from the log's `runningSince`; this answers "what time is it now"
|
|
240
|
+
* at the far end of that subtraction so engine tests can exercise a budget
|
|
241
|
+
* boundary without racing a loaded event loop.
|
|
242
|
+
*
|
|
243
|
+
* It is honoured only when `KXM_DETERMINISTIC_TEST_CLOCK=1` is set (see
|
|
244
|
+
* `budgetNowIso` in `engine.ts`), it is never consulted for event timestamps,
|
|
245
|
+
* and the Runtime supervisor does not pass one — so no production path can
|
|
246
|
+
* freeze a budget by supplying a stale clock.
|
|
247
|
+
*/
|
|
248
|
+
readonly budgetClock?: () => string;
|
|
237
249
|
}
|
|
238
250
|
|
|
239
251
|
const closedRuntimeContexts = new WeakSet<KxmRuntimeContext>();
|
|
@@ -267,7 +279,12 @@ export function registerKxmRuntimeCloseHook(context: KxmRuntimeContext, hook: ()
|
|
|
267
279
|
*/
|
|
268
280
|
export function openKxmRuntimeContext(
|
|
269
281
|
projectRoot: string,
|
|
270
|
-
options: KxmConfigOptions & {
|
|
282
|
+
options: KxmConfigOptions & {
|
|
283
|
+
stateRoot?: string;
|
|
284
|
+
now?: string;
|
|
285
|
+
homeRuntimeId: string;
|
|
286
|
+
budgetClock?: () => string;
|
|
287
|
+
},
|
|
271
288
|
): KxmRuntimeContext {
|
|
272
289
|
const paths = kxmRuntimePaths(options.stateRoot !== undefined ? { stateRoot: options.stateRoot } : {});
|
|
273
290
|
const registry = new KxmRuntimeRegistry(paths.registryDb);
|
|
@@ -289,6 +306,7 @@ export function openKxmRuntimeContext(
|
|
|
289
306
|
projectId: registration.projectId,
|
|
290
307
|
homeRuntimeId: registration.homeRuntimeId,
|
|
291
308
|
eventStore,
|
|
309
|
+
...(options.budgetClock ? { budgetClock: options.budgetClock } : {}),
|
|
292
310
|
};
|
|
293
311
|
} catch (error) {
|
|
294
312
|
registry.close();
|
package/scripts/kxm-hub.mjs
CHANGED
|
@@ -39,6 +39,29 @@ function processExists(pid) {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Live hub claim, if any. Read-only: returns the pid of a well-formed
|
|
44
|
+
* `hub.pid` claim whose wrapper is still alive, otherwise null.
|
|
45
|
+
*
|
|
46
|
+
* Claims that are missing, malformed, dead, or stale return null so the
|
|
47
|
+
* atomic `claimPidFile()` below keeps sole authority over those cases — it is
|
|
48
|
+
* the real single-instance guard and fails closed on them. This exists only so
|
|
49
|
+
* a start that is certainly going to be refused can refuse *before* it
|
|
50
|
+
* persists credentials.
|
|
51
|
+
*/
|
|
52
|
+
function liveHubClaimPid() {
|
|
53
|
+
let existing;
|
|
54
|
+
try {
|
|
55
|
+
existing = JSON.parse(readFileSync(pidPath, "utf8"));
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
if (existing?.version === 1 && existing.role === "hub" && Number.isInteger(existing.pid) && processExists(existing.pid)) {
|
|
60
|
+
return existing.pid;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
42
65
|
function parseHubEnvFile() {
|
|
43
66
|
const file = join(resolveUserStateRoot(), "hub-env.json");
|
|
44
67
|
let raw;
|
|
@@ -122,6 +145,19 @@ function resolveCredentials() {
|
|
|
122
145
|
return { authToken, authTokenSource, projectTokens, projectTokensSource, file };
|
|
123
146
|
}
|
|
124
147
|
|
|
148
|
+
// Pre-flight: refuse a start that a live hub already owns before
|
|
149
|
+
// resolveCredentials() can generate and persist an admin token that the
|
|
150
|
+
// running hub never issued. `kxm hub start` used to mint the credential at
|
|
151
|
+
// module load and only then report "already managed by PID <n>", leaving a
|
|
152
|
+
// hub-env.json whose token no process trusts.
|
|
153
|
+
try {
|
|
154
|
+
const livePid = liveHubClaimPid();
|
|
155
|
+
if (livePid) {
|
|
156
|
+
process.stderr.write(`kxm hub: KXM hub is already managed by PID ${livePid}. Run kxm hub stop before starting another hub.\n`);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
} catch { /* the atomic claim below still guards startup */ }
|
|
160
|
+
|
|
125
161
|
let credentials;
|
|
126
162
|
try {
|
|
127
163
|
credentials = resolveCredentials();
|