@kontextmind/kxm 0.7.128 → 0.7.130
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 +20 -0
- package/docs/architecture/inventory.md +1 -1
- package/docs/concepts/data-and-storage.md +1 -1
- package/docs/contracts/README.md +1 -1
- package/docs/contracts/migration.md +12 -8
- package/docs/contributing/development.md +3 -1
- package/docs/operations/backup-and-restore.md +3 -3
- package/docs/operations/upgrade.md +2 -2
- package/docs/reference/cli-reference.md +11 -6
- package/docs/reference/config-reference.md +5 -2
- package/package.json +8 -8
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +2284 -222
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime-supervisor.js +461 -106
- package/plugins/kxm/dist/runtime.js +470 -114
- package/plugins/kxm/dist/server.js +17 -14
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/src/cli/lanes.ts +109 -4
- package/plugins/kxm/src/cli/project.ts +2 -2
- package/plugins/kxm/src/database.ts +25 -19
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/runtime-service.ts +1 -0
- package/plugins/kxm/src/runtime-store.ts +390 -77
- package/plugins/kxm/src/runtime-supervisor.ts +87 -3
- package/scripts/run-bounded.mjs +53 -0
|
@@ -19567,22 +19567,25 @@ function ensureWalJournalMode(database, file, description, timeoutMs = 5e3) {
|
|
|
19567
19567
|
Atomics.wait(sleeper, 0, 0, 10);
|
|
19568
19568
|
}
|
|
19569
19569
|
}
|
|
19570
|
+
function assertDatabaseFile(file, description) {
|
|
19571
|
+
checkedParent(file, description);
|
|
19572
|
+
const stat = lstatSync2(file, { throwIfNoEntry: false });
|
|
19573
|
+
if (stat) {
|
|
19574
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
19575
|
+
throw databaseError("runtime_path_invalid", description, `${description} must be a regular file, not a link or directory`);
|
|
19576
|
+
}
|
|
19577
|
+
}
|
|
19578
|
+
for (const sidecar of [`${file}-wal`, `${file}-shm`]) {
|
|
19579
|
+
const info = lstatSync2(sidecar, { throwIfNoEntry: false });
|
|
19580
|
+
if (info?.isSymbolicLink()) {
|
|
19581
|
+
throw databaseError("runtime_path_invalid", description, `${description} sidecar must not be a link`);
|
|
19582
|
+
}
|
|
19583
|
+
}
|
|
19584
|
+
}
|
|
19570
19585
|
function openDatabase(file, description, spec) {
|
|
19571
19586
|
const isMemory = file === ":memory:";
|
|
19572
19587
|
if (!isMemory) {
|
|
19573
|
-
|
|
19574
|
-
const stat = lstatSync2(file, { throwIfNoEntry: false });
|
|
19575
|
-
if (stat) {
|
|
19576
|
-
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
19577
|
-
throw databaseError("runtime_path_invalid", description, `${description} must be a regular file, not a link or directory`);
|
|
19578
|
-
}
|
|
19579
|
-
}
|
|
19580
|
-
for (const sidecar of [`${file}-wal`, `${file}-shm`]) {
|
|
19581
|
-
const info = lstatSync2(sidecar, { throwIfNoEntry: false });
|
|
19582
|
-
if (info?.isSymbolicLink()) {
|
|
19583
|
-
throw databaseError("runtime_path_invalid", description, `${description} sidecar must not be a link`);
|
|
19584
|
-
}
|
|
19585
|
-
}
|
|
19588
|
+
assertDatabaseFile(file, description);
|
|
19586
19589
|
}
|
|
19587
19590
|
const database = new DatabaseSync(file);
|
|
19588
19591
|
let transaction = false;
|
|
@@ -19611,7 +19614,7 @@ function openDatabase(file, description, spec) {
|
|
|
19611
19614
|
throw databaseError(
|
|
19612
19615
|
"runtime_schema_outdated",
|
|
19613
19616
|
file,
|
|
19614
|
-
`${description} is schema version ${version}; this build requires ${spec.version}. Delete the state file to start fresh and let its owning process recreate it (\`kxm hub start\` for hub state, the Runtime for
|
|
19617
|
+
`${description} is schema version ${version}; this build requires ${spec.version}. Delete the state file to start fresh and let its owning process recreate it (\`kxm hub start\` for hub state, the Runtime for event stores). The Runtime registry copy from schema 1 to 2 is the one exception. \`kxm init\` is project-only and rebuilds no database. Upgrading any other old state in place is deliberately unsupported`
|
|
19615
19618
|
);
|
|
19616
19619
|
}
|
|
19617
19620
|
if (spec.tables) {
|
package/plugins/kxm/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
-
import { discoverKxmProjectRoot, kxmResourceIdentifier } from "../project-config.ts";
|
|
5
|
-
import {
|
|
4
|
+
import { discoverKxmProjectRoot, kxmResourceIdentifier, loadKxmProject } from "../project-config.ts";
|
|
5
|
+
import { KxmRunEventStore, KxmRuntimeRegistry, kxmProjectRunEventsPath, kxmRuntimePaths } from "../runtime-store.ts";
|
|
6
|
+
import { attachKxmSupervisor, ensureKxmSupervisor, kxmRuntimeRequest, kxmSupervisorRecordIsLive } from "../runtime-supervisor.ts";
|
|
6
7
|
import { print, printPlan, workspaceDirs, type Runtime } from "./types.ts";
|
|
7
8
|
|
|
8
9
|
/** One worktree lane recorded in the control checkout. The sha is the base,
|
|
@@ -33,6 +34,7 @@ const LANE_SCHEMA = "kxm.lanes.v1";
|
|
|
33
34
|
const SHA_RE = /^[a-f0-9]{40,64}$/;
|
|
34
35
|
const TIMESTAMP_RE = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]{1,9})?Z$/;
|
|
35
36
|
const SETTLED_RUN = new Set(["completed", "failed", "cancelled"]);
|
|
37
|
+
const LANE_RUN_LIST_LIMIT = 10_000;
|
|
36
38
|
|
|
37
39
|
function lanesFile(runtime: Runtime): string {
|
|
38
40
|
return join(runtime.dirs.state, "lanes.json");
|
|
@@ -316,6 +318,65 @@ function isSettled(status: string): boolean {
|
|
|
316
318
|
return SETTLED_RUN.has(status);
|
|
317
319
|
}
|
|
318
320
|
|
|
321
|
+
/** Runs in the lane event store. Undefined when the lane has no store and no supervisor list. */
|
|
322
|
+
async function laneStoreRuns(runtime: Runtime, lanePath: string): Promise<Array<{ runId: string; status: string }> | undefined> {
|
|
323
|
+
const project = await import("./project.ts");
|
|
324
|
+
const request = project.kxmDriveCliSeams.runtimeRequest ?? kxmRuntimeRequest;
|
|
325
|
+
const handle = await attachKxmSupervisor({ env: runtime.env });
|
|
326
|
+
const projectId = (): string => String(loadKxmProject(lanePath, {}).project.value.id);
|
|
327
|
+
if (handle) {
|
|
328
|
+
try {
|
|
329
|
+
const result = await request(
|
|
330
|
+
handle,
|
|
331
|
+
"GET",
|
|
332
|
+
`/v1/projects/${encodeURIComponent(projectId())}/runs?projectRoot=${encodeURIComponent(lanePath)}&limit=${LANE_RUN_LIST_LIMIT}`,
|
|
333
|
+
);
|
|
334
|
+
const runs = Array.isArray(result.runs) ? result.runs : [];
|
|
335
|
+
return runs.flatMap((run) => {
|
|
336
|
+
if (!run || typeof run !== "object" || Array.isArray(run)) return [];
|
|
337
|
+
const record = run as { runId?: unknown; status?: unknown };
|
|
338
|
+
if (typeof record.runId !== "string" || record.runId.length === 0) return [];
|
|
339
|
+
const status = typeof record.status === "string" && record.status.length > 0 ? record.status : "unknown";
|
|
340
|
+
return [{ runId: record.runId, status }];
|
|
341
|
+
});
|
|
342
|
+
} catch {
|
|
343
|
+
// The supervisor could not list this root. The file read below still can.
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const eventsPath = kxmProjectRunEventsPath(lanePath, runtime.env);
|
|
347
|
+
if (!existsSync(eventsPath)) return undefined;
|
|
348
|
+
const store = new KxmRunEventStore(eventsPath);
|
|
349
|
+
try {
|
|
350
|
+
return store.runsForProject(projectId(), LANE_RUN_LIST_LIMIT).map((run) => ({ runId: run.runId, status: run.status }));
|
|
351
|
+
} finally {
|
|
352
|
+
store.close();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function refuseIfLaneStoreRunsOpen(runtime: Runtime, unit: string, command: string): Promise<number | undefined> {
|
|
357
|
+
const loaded = loadLanes(runtime, command);
|
|
358
|
+
if (typeof loaded === "number") return loaded;
|
|
359
|
+
const lane = loaded.lanes[unit];
|
|
360
|
+
if (!lane) return undefined;
|
|
361
|
+
let runs: Array<{ runId: string; status: string }> | undefined;
|
|
362
|
+
try {
|
|
363
|
+
runs = await laneStoreRuns(runtime, lane.path);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
const detail = error instanceof Error ? error.message : "run list failed";
|
|
366
|
+
return refuse(runtime, command, "lane_run_open", `lane ${unit} runs could not be read: ${detail.slice(0, 200)}`, { unit });
|
|
367
|
+
}
|
|
368
|
+
if (!runs) return undefined;
|
|
369
|
+
const unsettled = runs.filter((run) => !isSettled(run.status)).map((run) => run.runId);
|
|
370
|
+
if (unsettled.length === 0) return undefined;
|
|
371
|
+
return refuse(
|
|
372
|
+
runtime,
|
|
373
|
+
command,
|
|
374
|
+
"lane_run_open",
|
|
375
|
+
`lane ${unit} run ${unsettled.join(", ")} is unsettled`,
|
|
376
|
+
{ unit, runIds: unsettled },
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
319
380
|
export async function refuseIfLaneRunOpen(runtime: Runtime, unit: string, command: string): Promise<number | undefined> {
|
|
320
381
|
const loaded = loadLanes(runtime, command);
|
|
321
382
|
if (typeof loaded === "number") return loaded;
|
|
@@ -431,12 +492,49 @@ export async function cmdLaneStatus(runtime: Runtime, unit: string): Promise<num
|
|
|
431
492
|
print(
|
|
432
493
|
runtime.io,
|
|
433
494
|
runtime.json,
|
|
434
|
-
{ ok: true, command, lane: { ...view, status: runStatus } },
|
|
435
|
-
`${formatLaneLine(view)} status=${runStatus}`,
|
|
495
|
+
{ ok: true, command, root: record.path, lane: { ...view, status: runStatus } },
|
|
496
|
+
`${formatLaneLine(view)} status=${runStatus} root=${record.path}`,
|
|
436
497
|
);
|
|
437
498
|
return 0;
|
|
438
499
|
}
|
|
439
500
|
|
|
501
|
+
/** Remove the lane root from the Runtime registry. A stopped supervisor is not
|
|
502
|
+
* an error: the row is deleted from the registry file when that file exists.
|
|
503
|
+
* The delete holds the write lock and rechecks the supervisor row; a live
|
|
504
|
+
* supervisor is left for the unregister route. */
|
|
505
|
+
async function unregisterLaneRoot(runtime: Runtime, projectRoot: string, force: boolean): Promise<string | undefined> {
|
|
506
|
+
const body = force ? { projectRoot, force: true as const } : { projectRoot };
|
|
507
|
+
try {
|
|
508
|
+
const project = await import("./project.ts");
|
|
509
|
+
const request = project.kxmDriveCliSeams.runtimeRequest ?? kxmRuntimeRequest;
|
|
510
|
+
const post = async (handle: { runtimeId: string; port: number; token: string; started: boolean }): Promise<void> => {
|
|
511
|
+
await request(handle, "POST", "/v1/projects/unregister", body);
|
|
512
|
+
};
|
|
513
|
+
const handle = await attachKxmSupervisor({ env: runtime.env });
|
|
514
|
+
if (handle) {
|
|
515
|
+
await post(handle);
|
|
516
|
+
return undefined;
|
|
517
|
+
}
|
|
518
|
+
const paths = kxmRuntimePaths({ env: runtime.env });
|
|
519
|
+
if (!existsSync(paths.registryDb)) return undefined;
|
|
520
|
+
const registry = new KxmRuntimeRegistry(paths.registryDb);
|
|
521
|
+
let outcome: "removed" | "absent" | "supervisor_live";
|
|
522
|
+
try {
|
|
523
|
+
outcome = registry.unregisterProjectIfIdle(projectRoot, kxmSupervisorRecordIsLive);
|
|
524
|
+
} finally {
|
|
525
|
+
registry.close();
|
|
526
|
+
}
|
|
527
|
+
if (outcome === "supervisor_live") {
|
|
528
|
+
const live = await attachKxmSupervisor({ env: runtime.env });
|
|
529
|
+
if (!live) return "runtime supervisor is live but could not be reached";
|
|
530
|
+
await post(live);
|
|
531
|
+
}
|
|
532
|
+
return undefined;
|
|
533
|
+
} catch (error) {
|
|
534
|
+
return error instanceof Error ? error.message : "lane root could not be unregistered";
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
440
538
|
export async function cmdLaneDrop(runtime: Runtime, unit: string, options: { force?: boolean } = {}): Promise<number> {
|
|
441
539
|
const command = "lane drop";
|
|
442
540
|
const invalid = refuseUnit(runtime, command, unit);
|
|
@@ -458,16 +556,23 @@ export async function cmdLaneDrop(runtime: Runtime, unit: string, options: { for
|
|
|
458
556
|
return refuse(runtime, command, "lane_dirty", `lane ${unit} has ${dirty} uncommitted change(s)`, { unit, dirty });
|
|
459
557
|
}
|
|
460
558
|
}
|
|
559
|
+
const listed = await refuseIfLaneStoreRunsOpen(runtime, unit, command);
|
|
560
|
+
if (listed !== undefined) return listed;
|
|
461
561
|
const open = await refuseIfLaneRunOpen(runtime, unit, command);
|
|
462
562
|
if (open !== undefined) return open;
|
|
463
563
|
}
|
|
464
564
|
if (runtime.dryRun) {
|
|
465
565
|
printPlan(runtime, { command, unit, branchKept: record.branch }, [
|
|
566
|
+
{ action: "delete", target: `runtime registry ${record.path}` },
|
|
466
567
|
{ action: "delete", target: record.path },
|
|
467
568
|
{ action: "write", target: lanesFile(runtime) },
|
|
468
569
|
], `drop lane ${unit}; branch ${record.branch} is not deleted`);
|
|
469
570
|
return 0;
|
|
470
571
|
}
|
|
572
|
+
const unregistered = await unregisterLaneRoot(runtime, record.path, force);
|
|
573
|
+
if (unregistered !== undefined) {
|
|
574
|
+
return refuse(runtime, command, "lane_unregister_failed", `lane ${unit} could not be unregistered: ${unregistered.slice(0, 300)}`, { unit });
|
|
575
|
+
}
|
|
471
576
|
if (existsSync(record.path)) {
|
|
472
577
|
const removed = git(root, ["worktree", "remove", ...(force ? ["--force"] : []), record.path]);
|
|
473
578
|
if (removed.status !== 0) {
|
|
@@ -600,8 +600,8 @@ export async function cmdKxmRunStatus(runtime: Runtime, runId: string): Promise<
|
|
|
600
600
|
print(
|
|
601
601
|
runtime.io,
|
|
602
602
|
runtime.json,
|
|
603
|
-
{ ok: true, command: "runs status", run, ...(drive !== undefined ? { drive } : {}) },
|
|
604
|
-
`${formatRunStatusLine(run, drive)}${driveLine ? `\n${driveLine}` : ""}`,
|
|
603
|
+
{ ok: true, command: "runs status", projectRoot, run, ...(drive !== undefined ? { drive } : {}) },
|
|
604
|
+
`${formatRunStatusLine(run, drive)}\nroot ${projectRoot}${driveLine ? `\n${driveLine}` : ""}`,
|
|
605
605
|
);
|
|
606
606
|
return 0;
|
|
607
607
|
} catch (error) {
|
|
@@ -154,22 +154,27 @@ export function ensureWalJournalMode(database: DatabaseSync, file: string, descr
|
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/** Parent, file, and WAL sidecar must be regular filesystem objects. A symlink is refused before the file is opened. */
|
|
158
|
+
export function assertDatabaseFile(file: string, description: string): void {
|
|
159
|
+
checkedParent(file, description);
|
|
160
|
+
const stat = lstatSync(file, { throwIfNoEntry: false });
|
|
161
|
+
if (stat) {
|
|
162
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
163
|
+
throw databaseError("runtime_path_invalid", description, `${description} must be a regular file, not a link or directory`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
for (const sidecar of [`${file}-wal`, `${file}-shm`]) {
|
|
167
|
+
const info = lstatSync(sidecar, { throwIfNoEntry: false });
|
|
168
|
+
if (info?.isSymbolicLink()) {
|
|
169
|
+
throw databaseError("runtime_path_invalid", description, `${description} sidecar must not be a link`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
157
174
|
export function openDatabase(file: string, description: string, spec: DatabaseSchemaSpec): DatabaseSync {
|
|
158
175
|
const isMemory = file === ":memory:";
|
|
159
176
|
if (!isMemory) {
|
|
160
|
-
|
|
161
|
-
const stat = lstatSync(file, { throwIfNoEntry: false });
|
|
162
|
-
if (stat) {
|
|
163
|
-
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
164
|
-
throw databaseError("runtime_path_invalid", description, `${description} must be a regular file, not a link or directory`);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
for (const sidecar of [`${file}-wal`, `${file}-shm`]) {
|
|
168
|
-
const info = lstatSync(sidecar, { throwIfNoEntry: false });
|
|
169
|
-
if (info?.isSymbolicLink()) {
|
|
170
|
-
throw databaseError("runtime_path_invalid", description, `${description} sidecar must not be a link`);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
177
|
+
assertDatabaseFile(file, description);
|
|
173
178
|
}
|
|
174
179
|
|
|
175
180
|
const database = new DatabaseSync(file);
|
|
@@ -199,14 +204,15 @@ export function openDatabase(file: string, description: string, spec: DatabaseSc
|
|
|
199
204
|
database.exec(spec.schema);
|
|
200
205
|
database.exec(`PRAGMA user_version = ${spec.version}`);
|
|
201
206
|
} else if (version < spec.version) {
|
|
202
|
-
// No migration lanes
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
207
|
+
// No migration lanes, except the Runtime registry copy from schema 1 to 2
|
|
208
|
+
// (migrateRegistryLanes). This is a single-operator tool: every other older
|
|
209
|
+
// database is re-initialised, not upgraded in place, and the code never carries
|
|
210
|
+
// two schema shapes at once. Silently accepting an older file would mean every
|
|
211
|
+
// query has to work against shapes it no longer tests.
|
|
206
212
|
throw databaseError(
|
|
207
213
|
"runtime_schema_outdated",
|
|
208
214
|
file,
|
|
209
|
-
`${description} is schema version ${version}; this build requires ${spec.version}. Delete the state file to start fresh and let its owning process recreate it (\`kxm hub start\` for hub state, the Runtime for
|
|
215
|
+
`${description} is schema version ${version}; this build requires ${spec.version}. Delete the state file to start fresh and let its owning process recreate it (\`kxm hub start\` for hub state, the Runtime for event stores). The Runtime registry copy from schema 1 to 2 is the one exception. \`kxm init\` is project-only and rebuilds no database. Upgrading any other old state in place is deliberately unsupported`,
|
|
210
216
|
);
|
|
211
217
|
}
|
|
212
218
|
|
|
@@ -628,7 +634,7 @@ export function restoreDatabaseFile(
|
|
|
628
634
|
*/
|
|
629
635
|
export const KXM_BACKUP_CEILINGS = {
|
|
630
636
|
"hub-store": 5,
|
|
631
|
-
registry:
|
|
637
|
+
registry: 2,
|
|
632
638
|
"binding-store": 1,
|
|
633
639
|
events: 7,
|
|
634
640
|
} as const;
|
|
@@ -11,7 +11,7 @@ import { deliverInboxNotification } from "./inbox.ts";
|
|
|
11
11
|
import type { HubEvent, MessageRecord } from "./protocol.ts";
|
|
12
12
|
import { sessionTokenFixHint } from "./session-token-hint.ts";
|
|
13
13
|
|
|
14
|
-
const VERSION = "0.7.
|
|
14
|
+
const VERSION = "0.7.130";
|
|
15
15
|
const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
|
|
16
16
|
const inbox = new Map<string, MessageRecord>();
|
|
17
17
|
const notifiedInbox = new Set<string>();
|
|
@@ -315,6 +315,7 @@ export function openKxmRuntimeContext(
|
|
|
315
315
|
homeRuntimeId: options.homeRuntimeId,
|
|
316
316
|
configRevision: bundle.configRevision,
|
|
317
317
|
now: options.now ?? new Date().toISOString(),
|
|
318
|
+
...(options.logger ? { logger: options.logger } : {}),
|
|
318
319
|
});
|
|
319
320
|
const eventStore = new KxmRunEventStore(join(paths.projectsDir, registration.projectKey, "run-events.db"));
|
|
320
321
|
registerKxmRuntimeHandle(eventStore.path);
|