@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.
@@ -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
- checkedParent(file, description);
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 registry/event stores); \`kxm init\` is project-only and rebuilds no database \u2014 upgrading old state in place is deliberately unsupported`
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) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plugin",
3
- "version": "0.7.128",
3
+ "version": "0.7.130",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -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 { attachKxmSupervisor, ensureKxmSupervisor, kxmRuntimeRequest } from "../runtime-supervisor.ts";
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
- checkedParent(file, description);
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. This is a single-operator tool: an older database is
203
- // re-initialised, not upgraded in place, and the code never carries two schema
204
- // shapes at once. Silently accepting an older file would mean every query has to
205
- // work against shapes it no longer tests.
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 registry/event stores); \`kxm init\` is project-only and rebuilds no database — upgrading old state in place is deliberately unsupported`,
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: 1,
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.128";
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);