@kontextmind/kxm 0.7.179 → 0.7.181

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.
@@ -11,7 +11,7 @@
11
11
  "name": "kxm",
12
12
  "source": "./plugins/kxm",
13
13
  "description": "Durable workflows, peer agents, and kxm tui",
14
- "version": "0.7.179",
14
+ "version": "0.7.181",
15
15
  "category": "development",
16
16
  "tags": ["kxm", "multi-agent", "workflows", "mcp"]
17
17
  }
package/CHANGELOG.md CHANGED
@@ -651,6 +651,15 @@ All notable user-facing changes are documented here. The project follows [Semant
651
651
 
652
652
  ### Fixed
653
653
 
654
+ - **`kxm improve report` and `kxm routing report` include attempts settled in a registered lane.**
655
+ Inside a KXM project, a present user-state `registry.db` is opened read-only at schema version 2. One query loads the home row and every lane row for that project id. Event-store paths come from the stored project keys, so a stale checkout path cannot hide a lane. A missing store stays a zero-record source. An absent registry, or a checkout that is not registered, still reads only that checkout's own store. `--file` still reads only the named file. See
656
+ [kxm improve report](docs/reference/cli-reference.md#kxm-improve-report).
657
+
658
+ - **Browser integration tests isolate the Steel credential environment.**
659
+ Each test restores the operator's variables afterward, so ambient Authentik
660
+ credentials cannot override a case that deliberately exercises the legacy
661
+ API-key path.
662
+
654
663
  - **`kxm supervise` keeps supervisor state across a restart.**
655
664
  `status`, `record`, and `tick` store in-flight lanes, the last-seen pull
656
665
  request and CI state, and backoff timers in `.kxm/state/supervisor.json`.
@@ -17,7 +17,7 @@ Read from `plugins/kxm/src/cli.ts`, `plugins/kxm/src/config-editor.ts`,
17
17
  | Hub cloud binding | `kxm hub bind <url> --cloud --token-env/--token-command` | no listener; writes the binding record and never stores the token | binding record; token stays in the named env or command | `plugins/kxm/src/cli/hub.ts` |
18
18
  | Runtime supervisor | `kxm runtime` / `scripts/kxm-runtime-supervisor.mjs` | `127.0.0.1` and the requested port, or ephemeral | registry and event stores below | `plugins/kxm/src/runtime-supervisor.ts` |
19
19
  | Runtime registry | opened by the supervisor | `runtime/registry.db` under the user state root | `registry.db`. A lane worktree registers under its project: same project id, its own control root, `lane_of` set. A registering root is the primary worktree when its git common dir is `<root>/.git`; if that primary registers while the live home row is not primary, the primary becomes the home row and every other live row of that repository becomes its lane. | `plugins/kxm/src/runtime-paths.ts` |
20
- | Runtime event store | opened by the supervisor | `runtime/projects/<key>/run-events.db` | `run-events.db` | `plugins/kxm/src/runtime-paths.ts` |
20
+ | Runtime event store | opened by the supervisor | `runtime/projects/<key>/run-events.db` | `run-events.db`. `kxm improve report` and `kxm routing report` read the home store and every lane store for the registered project id through one read-only query of `registry.db` (`plugins/kxm/src/improve-sources.ts`). An absent registry, or a checkout that is not registered, reads only that checkout's own store. Paths come from the stored project keys. | `plugins/kxm/src/runtime-paths.ts` |
21
21
  | MCP server | Claude plugin or stdio launch | stdio | none | `plugins/kxm/src/mcp-server.ts` |
22
22
  | One-shot harness | supervisor producer | child process, no listener | none | `plugins/kxm/src/oneshot-producer.ts` |
23
23
  | Pi worker | `kxm agent worker` | no HTTP listener in `scripts/kxm-worker.mjs` | none of its own | `scripts/kxm-worker.mjs` |
@@ -84,6 +84,7 @@ to run one file is in [Develop KXM](development.md#run-one-file-or-one-test).
84
84
  | Promotion needs the configured admin token with no loopback bypass; proposed items never reach a packet | `e5-memory-floor.test.ts` |
85
85
  | `kxm memory`: candidates, marker-delimited sync blocks, and one brief across CLI, Pi and the Claude hook | `e5b-harness-agnostic-memory.test.ts` |
86
86
  | `kxm improve report` reads Runtime-settled attempts, drops simulated and duplicate ones, and flags only same-ask repeats | `improve.test.ts` ("kxm improve report resolves Runtime-settled attempts from the event log and flags only same-ask cross-run repeats"), `cli.test.ts`, `cli-experience.test.ts`, `commands-policy.test.ts` |
87
+ | A registered lane's attempt appears in `kxm improve report` and `kxm routing report` run from the primary checkout; a duplicate still drops and `--file` stays isolated | `improve.test.ts` ("kxm improve report and kxm routing report from the primary include an attempt recorded only in a registered lane") |
87
88
  | Improvement candidates exclude write steps; promotion readiness never authorizes | `improve.test.ts` |
88
89
  | Telemetry classification and JSONL recovery | `telemetry.test.ts`, `cli.test.ts` |
89
90
 
@@ -100,7 +101,7 @@ The context suites in detail:
100
101
  | `wiki.test.ts` | Wiki compilation determinism, lifecycle preservation, contradiction visibility, lint |
101
102
  | `skills.test.ts` | Skill candidate lifecycle, quarantine, immutability, CLI |
102
103
  | `routing.test.ts` | Behavioral hash, record parsing, comparisons, `kxm routing report` |
103
- | `improve.test.ts` | Routing-record sources, event-log outcomes, same-ask candidacy, candidate files, promotion readiness |
104
+ | `improve.test.ts` | Routing-record sources, registered lane stores, event-log outcomes, same-ask candidacy, candidate files, promotion readiness |
104
105
 
105
106
  ## Claude Code plugin and MCP
106
107
 
@@ -3499,21 +3499,21 @@ Proposes coded-repeat candidates from this project's Runtime routing records and
3499
3499
  kxm improve report [--file <path>] [--out-dir <path>]
3500
3500
  ```
3501
3501
 
3502
- Generate the improvement report and candidates from routing records. Inside a KXM project (the current directory's Git root holds `.kxm/project.yaml`) it reads the project's Runtime event store, then `telemetry.jsonl` in the workspace logs directory; `--file` reads only the named file. Routing records (`kxm.routing-record.v1` and `v2`, bare or nested under `routing`, `envelope.routing`, or a `routing.attempt.recorded` event) are grouped by workflow, step, agent role, and ask. Groups that qualify become proposed candidates, each written as a `.diff` and a `.json` file. Nothing is applied.
3502
+ Generate the improvement report and candidates from routing records. Inside a KXM project (the current directory's Git root holds `.kxm/project.yaml`) it reads that project's Runtime event stores, then `telemetry.jsonl` in the workspace logs directory; `--file` reads only the named file. Routing records (`kxm.routing-record.v1` and `v2`, bare or nested under `routing`, `envelope.routing`, or a `routing.attempt.recorded` event) are grouped by workflow, step, agent role, and ask. Groups that qualify become proposed candidates, each written as a `.diff` and a `.json` file. Nothing is applied.
3503
3503
 
3504
3504
  | Option | Argument | Default | Description |
3505
3505
  |---|---|---|---|
3506
- | `--file` | `<path>` | the Runtime store, then `.kxm/logs/telemetry.jsonl` | Read only this routing-record JSONL instead of the project's Runtime store and telemetry |
3506
+ | `--file` | `<path>` | the Runtime stores, then `.kxm/logs/telemetry.jsonl` | Read only this routing-record JSONL instead of the project's Runtime stores and telemetry |
3507
3507
  | `--out-dir` | `<path>` | `<project>/.kxm/candidates` | Directory for candidates (default .kxm/candidates) |
3508
3508
 
3509
- - The Runtime store is `<state root>/runtime/projects/<key>/run-events.db`, with the key derived from the checkout's real path, so each checkout and worktree reads only its own. It is opened read-only for one query over its events table and is never created, written, or migrated. Outside a KXM project only telemetry is read, and the text output says `Runtime store not read`.
3510
- - A telemetry record whose `attemptId` the store already supplied is dropped (`duplicatesDropped`). Attempts from simulated drives are excluded (`excludedSimulated`). Each Runtime attempt's outcome is resolved from the event log and never written back: `accepted` when the run completed and the step was not re-entered, `reworked` when the step was entered again, `failed` when the run failed, undecided (`undecided`) when the run was cancelled or is still running.
3509
+ - Each Runtime store is `<state root>/runtime/projects/<key>/run-events.db`. When `runtime/registry.db` is absent, or this checkout's project key is not in it, the command reads only the store derived from this checkout's real path and does not create the registry. When the registry is present and this checkout is registered, it is opened read-only with no migration and no write lock, requires schema version 2, and one query returns the home row and every lane row for that project id. The home store is listed first, then lanes by registration time and key. Store paths are built from those stored keys and `runtime/projects`, not from the checkout path saved on the row, so a stale path cannot change the key. A missing store is an engine source with `exists=false` and zero records. A registry that is not a regular database file, is not schema version 2, or does not have exactly one home row and 24-character lowercase hex keys exits 1 with `improve_source_unreadable`. Each store is opened read-only for one query over its events table and is never created, written, or migrated. Outside a KXM project only telemetry is read, and the text output says `Runtime store not read`. The example below is an unregistered checkout, so it lists one engine source.
3510
+ - A telemetry record whose `attemptId` an earlier source already supplied is dropped (`duplicatesDropped`). The same drop applies between the home store and a lane store: the first source wins. Attempts from simulated drives are excluded (`excludedSimulated`). Each Runtime attempt's outcome is resolved from the event log and never written back: `accepted` when the run completed and the step was not re-entered, `reworked` when the step was entered again, `failed` when the run failed, undecided (`undecided`) when the run was cancelled or is still running.
3511
3511
  - A group becomes a candidate only when the same objective was decided in at least 2 runs, at least 0.75 of its decided records were accepted, and its step writes no repository. A group that passes but misses shows `no (writes-repository)` or `no (ask-not-repeated)`. `Weighted` is the recency-weighted record count (`improvement.telemetryHalfLifeDays`); it orders rows and never decides candidacy.
3512
3512
  - Reads `improvement.*` from the project and user configuration (see [`kxm.config.v1`](config-reference.md#personalization-settings-kxmconfigv1)) and reports each candidate's promotion readiness under `improvement.promotionPolicy`. Readiness never authorizes anything. Under `critic_quorum` every candidate reports not ready, because this command cites no critic receipts. A configuration that cannot be loaded exits 1 with `config_invalid`.
3513
- - A Runtime store that exists but cannot be read exits 1 with `improve_source_unreadable`; `detail` names the path.
3513
+ - A Runtime store or registry that exists but cannot be read, and a registry whose identity is corrupt, exits 1 with `improve_source_unreadable`; `detail` names the path.
3514
3514
  - Writes candidate files under `--out-dir` (relative to the current directory) and the report at `.kxm/assets/improvements/<timestamp>.json`. Honors `--dry-run` (writes neither). No hub needed.
3515
3515
  - JSON keys: `path`, `events`, `recordsCount`, `groupsCount`, `candidatesCount`, `candidates`, `report` (`schema`, `createdAt`, `reviewDecision`, `recordsCount`, `groups`, `candidates`, `promotionPolicy`, `promotion`, and `routeSwitches` when the store recorded any), `sources`, `projectRoot` (`null` outside a project). `routeSwitches` lists opted-in mid-attempt walks (`from`, `to`, `reason`, optional `effort`, `attemptId`, `stepId`). It is omitted when there are none.
3516
- - Each `sources` entry has `kind` (`engine`, `telemetry`, or `file`), `path`, `exists`, `records`, and `duplicatesDropped`; the `engine` entry also has `skippedInvalid`, `excludedSimulated`, and `undecided`.
3516
+ - Each `sources` entry has `kind` (`engine`, `telemetry`, or `file`), `path`, `exists`, `records`, and `duplicatesDropped`; each `engine` entry also has `skippedInvalid`, `excludedSimulated`, and `undecided`. A registered project has one `engine` entry for the home store and one for each lane store.
3517
3517
  - Each `report.groups` row has `workflowHash`, `workflowId` (Runtime records), `stepId`, `agentRole`, `promptHash`, `recurrence`, `distinctRuns`, `askRecurrence`, `undecidedRecords`, `meanCost`, `meanLatency`, `verifyPassRate` (accepted share of decided records), `rework`, `weightedRecurrence`, `undatedRecords`, `costSamples`, `writesRepository`, `evidenceRefs`, `isCandidate`, and, when they apply, `excludedReason`, `candidateKind`, and `candidateId`.
3518
3518
  - Each `report.promotion` entry has `candidateId`, `policy`, `readyForReview`, and `reason`.
3519
3519
 
@@ -3565,18 +3565,18 @@ kxm routing report [-f <path>] [-l] [--prices <path>]
3565
3565
 
3566
3566
  Compare verified completion, cost, and rework per behavioral configuration. The table ranks routes quality-first with columns Harness, Model, Effort, Role, Att, Pass%, Rwk%, p50(ms), p95(ms), CtxTok, Metered($), $/Acc, Unm, Unk, Quota, and optionally ListEquiv($).
3567
3567
 
3568
- Without `--file` it reads the same sources as [`kxm improve report`](#kxm-improve-report): the current project's Runtime event store (read-only), then the workspace `telemetry.jsonl`, dropping a telemetry copy of an attempt the store already supplied and excluding simulated drives. A Runtime attempt counts toward Pass% only when the event log shows its run completed without the step being re-entered.
3568
+ Without `--file` it reads the same sources as [`kxm improve report`](#kxm-improve-report): the registered home store and every lane store for that project (read-only), or only this checkout's store when the registry is absent or this checkout is unregistered, then the workspace `telemetry.jsonl`, dropping a later copy of an attempt an earlier source already supplied and excluding simulated drives. A Runtime attempt counts toward Pass% only when the event log shows its run completed without the step being re-entered.
3569
3569
 
3570
3570
  | Option | Argument | Default | Description |
3571
3571
  |---|---|---|---|
3572
- | `-f`, `--file` | `<path>` | the Runtime store, then workspace telemetry | Read only this telemetry or event log JSONL file (default: this project's Runtime event store plus workspace telemetry) |
3572
+ | `-f`, `--file` | `<path>` | the Runtime stores, then workspace telemetry | Read only this telemetry or event log JSONL file (default: this project's Runtime event stores plus workspace telemetry) |
3573
3573
  | `-l`, `--equivalent-list-cost` | none | off | Include equivalent list price column using price catalog |
3574
3574
  | `--list-prices` | none | off | Alias for --equivalent-list-cost |
3575
3575
  | `--prices` | `<path>` | `.kxm/prices.yaml` | Path to price catalog (default: .kxm/prices.yaml) |
3576
3576
 
3577
3577
  - Reads only. A price catalog that cannot be loaded is skipped silently.
3578
3578
  - `--equivalent-list-cost` loads the catalog without the freshness check the producers apply, so it prices with a catalog of any date, including one the producers treat as stale. Check the catalog `date` before you rely on `ListEquiv($)`.
3579
- - A Runtime store that exists but cannot be read exits 1 with `improve_source_unreadable`.
3579
+ - A Runtime store or registry that exists but cannot be read exits 1 with `improve_source_unreadable`. `--file` does not read the registry or any event store.
3580
3580
  - Ranking: quality first (Pass%, then Rwk%), then cost per accepted attempt. Among routes of equal quality, any route with an unknown-cost attempt ranks after every route without one. Such a route's cost is unknown, not a partial sum: `$/Acc` prints `-` (JSON `costPerAcceptedUsd: null`) and the `*` in `Unk` marks it.
3581
3581
  - The `Quota` column counts attempts whose metadata looks quota-exhausted (a quota failure class, a `quota` flag, or text such as `rate limit` or `HTTP 429`). It is a count only. Quota is not an `onError` class, and the column does not choose a route. Walks a role opted into are listed under Route switches when the store has `routing.route_switched` events (`from`, `to`, `reason`, optional `effort`, `stepId`).
3582
3582
  - The text output does not list the sources, and prints `no routing records in telemetry` when no source holds a record. The Rwk% column counts records with `transitions` greater than 0, which Runtime records never set.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontextmind/kxm",
3
- "version": "0.7.179",
3
+ "version": "0.7.181",
4
4
  "description": "KXM local-first multi-agent orchestration and operator dashboard",
5
5
  "type": "module",
6
6
  "author": "KontextMind",
@@ -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.179",
5
+ "version": "0.7.181",
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",
@@ -20760,8 +20760,8 @@ function validateWorkflow(workflow, agents, models, repositories, gates, issues)
20760
20760
  const writable = Object.values(objectValue(step.repositories) ?? {}).filter((access) => access === "write").length;
20761
20761
  if (maxWriteRepositories > writable) issues.push(issue3("semantic", "write_repository_bound_invalid", file, `${stepId} maxWriteRepositories exceeds writable repository scope`));
20762
20762
  }
20763
- const join60 = objectValue(step.join);
20764
- const minimumPassed = join60 && typeof join60.minimumPassed === "number" ? join60.minimumPassed : void 0;
20763
+ const join61 = objectValue(step.join);
20764
+ const minimumPassed = join61 && typeof join61.minimumPassed === "number" ? join61.minimumPassed : void 0;
20765
20765
  if (minimumPassed !== void 0 && minimumPassed > maximum) issues.push(issue3("semantic", "join_impossible", file, `${stepId} minimumPassed exceeds assignment maximum`));
20766
20766
  const distinctBy = names(assignment?.distinctBy);
20767
20767
  if (distinctBy.length > 0) {
@@ -25101,7 +25101,7 @@ function compileStep(step, index, stepIndex, requirePlanHash, sink) {
25101
25101
  const maxAttempts = compileCountField(step.maxAttempts, 1, `${id}.maxAttempts`, id, sink);
25102
25102
  const timeoutMs = compileOptionalDuration(step.timeoutMs, `${id}.timeoutMs`, id, sink);
25103
25103
  const assignments = compileAssignments(step, id, agent, sink);
25104
- const join60 = compileJoin(step, id, sink);
25104
+ const join61 = compileJoin(step, id, sink);
25105
25105
  const requiredEvidence = compileEvidence(step, id, sink);
25106
25106
  const transitions = compileTransitions(step, id, index, stepIndex, sink);
25107
25107
  const outcomes = Object.keys(transitions).sort(compareCodeUnits5);
@@ -25129,7 +25129,7 @@ function compileStep(step, index, stepIndex, requirePlanHash, sink) {
25129
25129
  transitions: orderedTransitions,
25130
25130
  requiresPlanHash: requirePlanHash.includes(id),
25131
25131
  assignments,
25132
- join: join60
25132
+ join: join61
25133
25133
  };
25134
25134
  if (kind === "agent" || kind === "moa") {
25135
25135
  if (!agent) return void 0;
@@ -25179,15 +25179,15 @@ function compileAssignments(step, stepId, primaryAgentId, sink) {
25179
25179
  };
25180
25180
  }
25181
25181
  function compileJoin(step, stepId, sink) {
25182
- const join60 = objectValue2(step.join);
25183
- if (!join60) return { strategy: "all" };
25184
- const declared = stringValue2(join60.strategy);
25182
+ const join61 = objectValue2(step.join);
25183
+ if (!join61) return { strategy: "all" };
25184
+ const declared = stringValue2(join61.strategy);
25185
25185
  const strategy = declared && JOIN_STRATEGIES.has(declared) ? declared : "all";
25186
- const minimumPassed = compileOptionalCount(join60.minimumPassed, `${stepId}.join.minimumPassed`, stepId, sink);
25186
+ const minimumPassed = compileOptionalCount(join61.minimumPassed, `${stepId}.join.minimumPassed`, stepId, sink);
25187
25187
  const compiled = {
25188
25188
  strategy,
25189
25189
  ...minimumPassed !== void 0 ? { minimumPassed } : {},
25190
- ...typeof join60.cancelRemaining === "boolean" ? { cancelRemaining: join60.cancelRemaining } : {}
25190
+ ...typeof join61.cancelRemaining === "boolean" ? { cancelRemaining: join61.cancelRemaining } : {}
25191
25191
  };
25192
25192
  return compiled;
25193
25193
  }
@@ -54567,7 +54567,7 @@ import { spawnSync as spawnSync15 } from "node:child_process";
54567
54567
  import { createHash as createHash19 } from "node:crypto";
54568
54568
  import { existsSync as existsSync50, mkdtempSync as mkdtempSync3, readFileSync as readFileSync44, rmSync as rmSync13 } from "node:fs";
54569
54569
  import { tmpdir as tmpdir3 } from "node:os";
54570
- import { basename as basename11, extname as extname3, join as join58, resolve as resolve33 } from "node:path";
54570
+ import { basename as basename11, extname as extname3, join as join59, resolve as resolve33 } from "node:path";
54571
54571
  import { createInterface as createInterface3 } from "node:readline";
54572
54572
 
54573
54573
  // plugins/kxm/src/improve.ts
@@ -54992,13 +54992,15 @@ function evaluatePromotionPolicy(candidate, policy = "manual_pr", options = {})
54992
54992
  // plugins/kxm/src/improve-sources.ts
54993
54993
  init_sqlite();
54994
54994
  init_project_config();
54995
- init_runtime_store();
54995
+ init_runtime_paths();
54996
54996
  init_route_switch();
54997
54997
  init_routing();
54998
54998
  init_telemetry();
54999
- import { existsSync as existsSync44 } from "node:fs";
55000
- import { resolve as resolve30 } from "node:path";
54999
+ import { existsSync as existsSync44, lstatSync as lstatSync8 } from "node:fs";
55000
+ import { join as join54, resolve as resolve30 } from "node:path";
55001
55001
  var ENGINE_EVENTS_SQL = "SELECT run_id, sequence, event_type, payload FROM events WHERE event_type IN ('routing.attempt.recorded','routing.route_switched','step.entered','run.status_changed') ORDER BY run_id, sequence";
55002
+ var REGISTRY_SIBLING_SQL = "SELECT sibling.project_key AS project_key, sibling.lane_of AS lane_of FROM projects AS current JOIN projects AS sibling ON sibling.project_id = current.project_id WHERE current.project_key = ? ORDER BY CASE WHEN sibling.lane_of IS NULL THEN 0 ELSE 1 END, sibling.registered_at, sibling.project_key";
55003
+ var PROJECT_KEY = /^[0-9a-f]{24}$/;
55002
55004
  var SIMULATED_HARNESS = "driver-simulated";
55003
55005
  function parsePayload(text) {
55004
55006
  try {
@@ -55104,6 +55106,61 @@ function readEngineRoutingRecords(path4) {
55104
55106
  source: { ...source, records: records.length, skippedInvalid, excludedSimulated, undecided }
55105
55107
  };
55106
55108
  }
55109
+ function registryUnreadable(registryDb, detail) {
55110
+ return new Error(`improve_source_unreadable: ${registryDb}: ${detail}`);
55111
+ }
55112
+ function discoverProjectEventStores(projectRoot, env) {
55113
+ const paths = kxmRuntimePaths({ env });
55114
+ const ownStore = join54(paths.projectsDir, projectRuntimeKey(projectRoot), "run-events.db");
55115
+ const registryDb = paths.registryDb;
55116
+ const info = lstatSync8(registryDb, { throwIfNoEntry: false });
55117
+ if (!info) return [ownStore];
55118
+ if (!info.isFile()) throw registryUnreadable(registryDb, "runtime registry must be a regular database file");
55119
+ for (const suffix of ["-wal", "-shm"]) {
55120
+ const sidecar = lstatSync8(`${registryDb}${suffix}`, { throwIfNoEntry: false });
55121
+ if (sidecar?.isSymbolicLink()) throw registryUnreadable(registryDb, "runtime registry sidecar must not be a link");
55122
+ }
55123
+ let database;
55124
+ try {
55125
+ database = openReadOnlyDatabase(registryDb);
55126
+ database.exec("PRAGMA busy_timeout = 5000");
55127
+ const versionRow = database.prepare("PRAGMA user_version").get();
55128
+ const version = versionRow?.user_version ?? 0;
55129
+ if (version !== 2) throw registryUnreadable(registryDb, `registry schema version ${version}; this reader requires 2`);
55130
+ const rows = database.prepare(REGISTRY_SIBLING_SQL).all(projectRuntimeKey(projectRoot));
55131
+ if (rows.length === 0) return [ownStore];
55132
+ return eventStoresFromRegistryRows(registryDb, paths.projectsDir, rows);
55133
+ } catch (error) {
55134
+ const message = error instanceof Error ? error.message : String(error);
55135
+ if (message.startsWith("improve_source_unreadable:")) throw error;
55136
+ throw new Error(`improve_source_unreadable: ${registryDb}: ${message}`, { cause: error });
55137
+ } finally {
55138
+ try {
55139
+ database?.close();
55140
+ } catch {
55141
+ }
55142
+ }
55143
+ }
55144
+ function eventStoresFromRegistryRows(registryDb, projectsDir, rows) {
55145
+ const seen = /* @__PURE__ */ new Set();
55146
+ const parsed = [];
55147
+ for (const row of rows) {
55148
+ const projectKey = typeof row.project_key === "string" ? row.project_key : "";
55149
+ const laneRaw = row.lane_of;
55150
+ const laneOf = laneRaw === null || laneRaw === void 0 ? null : typeof laneRaw === "string" ? laneRaw : "";
55151
+ if (!PROJECT_KEY.test(projectKey) || seen.has(projectKey) || laneOf !== null && !PROJECT_KEY.test(laneOf)) {
55152
+ throw registryUnreadable(registryDb, "registry identity is corrupt");
55153
+ }
55154
+ seen.add(projectKey);
55155
+ parsed.push({ projectKey, laneOf });
55156
+ }
55157
+ const homes = parsed.filter((row) => row.laneOf === null);
55158
+ const homeKey = homes[0]?.projectKey;
55159
+ if (homes.length !== 1 || homeKey === void 0 || parsed.some((row) => row.laneOf !== null && row.laneOf !== homeKey)) {
55160
+ throw registryUnreadable(registryDb, "registry identity is corrupt");
55161
+ }
55162
+ return parsed.map((row) => join54(projectsDir, row.projectKey, "run-events.db"));
55163
+ }
55107
55164
  function readJsonlSource(kind, path4) {
55108
55165
  const exists = existsSync44(path4);
55109
55166
  const records = exists ? readRoutingRecords(path4).map((entry) => entry.routing) : [];
@@ -55120,7 +55177,11 @@ function loadRoutingSources(options) {
55120
55177
  if (options.file !== void 0) {
55121
55178
  reads.push(readJsonlSource("file", resolve30(options.cwd, options.file)));
55122
55179
  } else {
55123
- if (projectRoot !== void 0) reads.push(readEngineRoutingRecords(kxmProjectRunEventsPath(projectRoot, options.env)));
55180
+ if (projectRoot !== void 0) {
55181
+ for (const path4 of discoverProjectEventStores(projectRoot, options.env)) {
55182
+ reads.push(readEngineRoutingRecords(path4));
55183
+ }
55184
+ }
55124
55185
  reads.push(readJsonlSource("telemetry", telemetryPath(options.logsDir)));
55125
55186
  }
55126
55187
  const seen = /* @__PURE__ */ new Set();
@@ -55152,7 +55213,7 @@ function loadRoutingSources(options) {
55152
55213
  var import_yaml21 = __toESM(require_dist(), 1);
55153
55214
  init_prices();
55154
55215
  import { existsSync as existsSync45, readFileSync as readFileSync40 } from "node:fs";
55155
- import { join as join54 } from "node:path";
55216
+ import { join as join55 } from "node:path";
55156
55217
  var DEFAULT_MODES_CONFIG = Object.freeze({
55157
55218
  schema: "kxm.modes.v1",
55158
55219
  majorModes: {
@@ -55215,7 +55276,7 @@ function loadModesConfig(projectRoot) {
55215
55276
  if (!projectRoot) {
55216
55277
  return DEFAULT_MODES_CONFIG;
55217
55278
  }
55218
- const modesPath = join54(projectRoot, ".kxm", "modes.yaml");
55279
+ const modesPath = join55(projectRoot, ".kxm", "modes.yaml");
55219
55280
  if (!existsSync45(modesPath)) {
55220
55281
  return DEFAULT_MODES_CONFIG;
55221
55282
  }
@@ -55306,7 +55367,7 @@ function calculatePromptFootprint(resolved, projectRoot = process.cwd(), catalog
55306
55367
  estimatedTokens: estimateTokens(baseSystemPromptChars)
55307
55368
  });
55308
55369
  for (const relPath of resolved.contextFiles) {
55309
- const fullPath = join54(projectRoot, relPath);
55370
+ const fullPath = join55(projectRoot, relPath);
55310
55371
  let chars = 0;
55311
55372
  if (existsSync45(fullPath)) {
55312
55373
  try {
@@ -55445,7 +55506,7 @@ init_safety_integrity();
55445
55506
  import { spawnSync as spawnSync14 } from "node:child_process";
55446
55507
  import { existsSync as existsSync46, mkdirSync as mkdirSync32, readFileSync as readFileSync41, readdirSync as readdirSync14, rmSync as rmSync12, statSync as statSync7 } from "node:fs";
55447
55508
  import { homedir as homedir8 } from "node:os";
55448
- import { join as join55, resolve as resolve31 } from "node:path";
55509
+ import { join as join56, resolve as resolve31 } from "node:path";
55449
55510
  var MAX_SSH_OUTPUT_BYTES = 50 * 1024;
55450
55511
  var MAX_SSH_OUTPUT_LINES = 2e3;
55451
55512
  var DEFAULT_SOCKET_DIR = ".kxm/run/ssh-sockets";
@@ -55471,7 +55532,7 @@ function truncateSshOutput(raw) {
55471
55532
  return { text, truncated };
55472
55533
  }
55473
55534
  function parseSshConfig(configPath) {
55474
- const targetPath = configPath ?? join55(homedir8(), ".ssh", "config");
55535
+ const targetPath = configPath ?? join56(homedir8(), ".ssh", "config");
55475
55536
  if (!existsSync46(targetPath)) {
55476
55537
  return [];
55477
55538
  }
@@ -55561,7 +55622,7 @@ function ensureSocketDir(socketDir = DEFAULT_SOCKET_DIR) {
55561
55622
  }
55562
55623
  function buildSshArgs(options) {
55563
55624
  const socketDir = ensureSocketDir(options.socketDir ?? DEFAULT_SOCKET_DIR);
55564
- const controlPath = join55(socketDir, "%C");
55625
+ const controlPath = join56(socketDir, "%C");
55565
55626
  const persist = options.controlPersist ?? DEFAULT_CONTROL_PERSIST;
55566
55627
  const args = [
55567
55628
  "-o",
@@ -55586,7 +55647,7 @@ function buildSshArgs(options) {
55586
55647
  }
55587
55648
  function checkControlSocket(host, socketDir = DEFAULT_SOCKET_DIR, execFn = spawnSync14) {
55588
55649
  const resolvedDir = ensureSocketDir(socketDir);
55589
- const controlPath = join55(resolvedDir, "%C");
55650
+ const controlPath = join56(resolvedDir, "%C");
55590
55651
  try {
55591
55652
  const result = execFn("ssh", ["-O", "check", "-o", `ControlPath=${controlPath}`, host], {
55592
55653
  encoding: "utf-8"
@@ -55598,7 +55659,7 @@ function checkControlSocket(host, socketDir = DEFAULT_SOCKET_DIR, execFn = spawn
55598
55659
  }
55599
55660
  function closeControlSocket(host, socketDir = DEFAULT_SOCKET_DIR, execFn = spawnSync14) {
55600
55661
  const resolvedDir = ensureSocketDir(socketDir);
55601
- const controlPath = join55(resolvedDir, "%C");
55662
+ const controlPath = join56(resolvedDir, "%C");
55602
55663
  try {
55603
55664
  const result = execFn("ssh", ["-O", "stop", "-o", `ControlPath=${controlPath}`, host], {
55604
55665
  encoding: "utf-8"
@@ -56349,7 +56410,7 @@ complete -c kxm -n "__fish_seen_subcommand_from goal" -a "create list get"
56349
56410
  // plugins/kxm/src/completion-install.ts
56350
56411
  import { existsSync as existsSync48, mkdirSync as mkdirSync33, readFileSync as readFileSync43, writeFileSync as writeFileSync28 } from "node:fs";
56351
56412
  import { homedir as homedir9 } from "node:os";
56352
- import { basename as basename10, delimiter, dirname as dirname24, isAbsolute as isAbsolute12, join as join56, resolve as resolve32 } from "node:path";
56413
+ import { basename as basename10, delimiter, dirname as dirname24, isAbsolute as isAbsolute12, join as join57, resolve as resolve32 } from "node:path";
56353
56414
  var COMPLETION_MARKER = "# kxm completion";
56354
56415
  var PATH_MARKER = "# kxm path";
56355
56416
  function detectShell(env = process.env, platform = process.platform) {
@@ -56371,25 +56432,25 @@ function completionScriptPath(shell, options = {}) {
56371
56432
  const env = options.env ?? process.env;
56372
56433
  const explicit = options.configDir ?? env.KXM_USER_CONFIG_DIR?.trim();
56373
56434
  const base = explicit && explicit.length > 0 ? resolve32(explicit) : resolve32(effectiveHome(options), ".config", "kxm");
56374
- return join56(base, "completions", `kxm.${shell}`);
56435
+ return join57(base, "completions", `kxm.${shell}`);
56375
56436
  }
56376
56437
  function bashRcCandidate(options) {
56377
56438
  const home = effectiveHome(options);
56378
- const candidates = [join56(home, ".bashrc"), join56(home, ".bash_profile")];
56439
+ const candidates = [join57(home, ".bashrc"), join57(home, ".bash_profile")];
56379
56440
  const existing = candidates.find((candidate) => existsSync48(candidate));
56380
56441
  return existing ?? candidates[0];
56381
56442
  }
56382
56443
  function zshRcCandidate(options) {
56383
56444
  const env = options.env ?? process.env;
56384
56445
  const home = effectiveHome(options);
56385
- if (env.ZDOTDIR?.trim()) return join56(resolve32(env.ZDOTDIR.trim()), ".zshrc");
56386
- return join56(home, ".zshrc");
56446
+ if (env.ZDOTDIR?.trim()) return join57(resolve32(env.ZDOTDIR.trim()), ".zshrc");
56447
+ return join57(home, ".zshrc");
56387
56448
  }
56388
56449
  function fishCompletionTarget(shell, options) {
56389
56450
  const env = options.env ?? process.env;
56390
56451
  const home = effectiveHome(options);
56391
- if (env.XDG_CONFIG_HOME?.trim()) return join56(resolve32(env.XDG_CONFIG_HOME.trim()), "fish", "completions", "kxm.fish");
56392
- return join56(home, ".config", "fish", "completions", "kxm.fish");
56452
+ if (env.XDG_CONFIG_HOME?.trim()) return join57(resolve32(env.XDG_CONFIG_HOME.trim()), "fish", "completions", "kxm.fish");
56453
+ return join57(home, ".config", "fish", "completions", "kxm.fish");
56393
56454
  }
56394
56455
  function completionRcTarget(shell, options = {}) {
56395
56456
  if (shell === "fish") {
@@ -56477,7 +56538,7 @@ function kxmBinDir(env = process.env) {
56477
56538
  const argv1 = env.KXM_ENTRY ?? process.argv[1];
56478
56539
  if (argv1 && isAbsolute12(argv1)) {
56479
56540
  const dir = dirname24(argv1);
56480
- if (existsSync48(join56(dir, process.platform === "win32" ? "kxm.cmd" : "kxm"))) return dir;
56541
+ if (existsSync48(join57(dir, process.platform === "win32" ? "kxm.cmd" : "kxm"))) return dir;
56481
56542
  }
56482
56543
  const pathEnv = env.PATH ?? "";
56483
56544
  for (const part of pathEnv.split(delimiter)) {
@@ -56517,7 +56578,7 @@ ${line}
56517
56578
  var import_yaml23 = __toESM(require_dist(), 1);
56518
56579
  init_routes();
56519
56580
  import { existsSync as existsSync49, mkdirSync as mkdirSync34, writeFileSync as writeFileSync29 } from "node:fs";
56520
- import { dirname as dirname25, join as join57 } from "node:path";
56581
+ import { dirname as dirname25, join as join58 } from "node:path";
56521
56582
  var ADMITTED_GUIDE_BINDINGS = Object.freeze({
56522
56583
  "anthropic/claude-fable-5.1": { harness: "claude", provider: "anthropic", model: "fable" },
56523
56584
  "anthropic/fable": { harness: "claude", provider: "anthropic", model: "fable" },
@@ -56948,7 +57009,7 @@ function renderGuideSetupFiles(projectRoot, plan) {
56948
57009
  const piMatch = binding.harness === "pi" && guidePiNoteMatches(binding);
56949
57010
  if (binding.harness === "pi" && !piMatch) {
56950
57011
  files.push({
56951
- path: join57(projectRoot, ".kxm", "agents", `${role}.yaml`),
57012
+ path: join58(projectRoot, ".kxm", "agents", `${role}.yaml`),
56952
57013
  content: (0, import_yaml23.stringify)(agentDocument(role, stage))
56953
57014
  });
56954
57015
  continue;
@@ -56966,22 +57027,22 @@ function renderGuideSetupFiles(projectRoot, plan) {
56966
57027
  modelDocument.origin = { source: GUIDE_PI_EVIDENCE_SOURCE, sha256: GUIDE_PI_EVIDENCE_SHA256 };
56967
57028
  if (!evidenceQueued) {
56968
57029
  files.push({
56969
- path: join57(projectRoot, GUIDE_PI_EVIDENCE_SOURCE),
57030
+ path: join58(projectRoot, GUIDE_PI_EVIDENCE_SOURCE),
56970
57031
  content: GUIDE_PI_EVIDENCE_NOTE
56971
57032
  });
56972
57033
  evidenceQueued = true;
56973
57034
  }
56974
57035
  }
56975
57036
  files.push({
56976
- path: join57(projectRoot, ".kxm", "agents", `${role}.yaml`),
57037
+ path: join58(projectRoot, ".kxm", "agents", `${role}.yaml`),
56977
57038
  content: (0, import_yaml23.stringify)(agentDocument(role, stage))
56978
57039
  });
56979
57040
  files.push({
56980
- path: join57(projectRoot, ".kxm", "models", `${routeId2}.yaml`),
57041
+ path: join58(projectRoot, ".kxm", "models", `${routeId2}.yaml`),
56981
57042
  content: (0, import_yaml23.stringify)(modelDocument)
56982
57043
  });
56983
57044
  files.push({
56984
- path: join57(projectRoot, ".kxm", "roles", `${role}.yaml`),
57045
+ path: join58(projectRoot, ".kxm", "roles", `${role}.yaml`),
56985
57046
  content: (0, import_yaml23.stringify)({
56986
57047
  schema: "kxm.role.v2",
56987
57048
  id: role,
@@ -56994,7 +57055,7 @@ function renderGuideSetupFiles(projectRoot, plan) {
56994
57055
  }
56995
57056
  for (const workflow of plan.workflows) {
56996
57057
  files.push({
56997
- path: join57(projectRoot, ".kxm", "workflows", `${workflow.slug}.yaml`),
57058
+ path: join58(projectRoot, ".kxm", "workflows", `${workflow.slug}.yaml`),
56998
57059
  content: (0, import_yaml23.stringify)(workflowDocument(workflow))
56999
57060
  });
57000
57061
  }
@@ -57011,7 +57072,7 @@ function mergeGuideRouteAdmission(projectRoot, plan) {
57011
57072
  }
57012
57073
  if (added.length === 0) return added;
57013
57074
  policy.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
57014
- const path4 = join57(projectRoot, ".kxm", "routes.yaml");
57075
+ const path4 = join58(projectRoot, ".kxm", "routes.yaml");
57015
57076
  mkdirSync34(dirname25(path4), { recursive: true });
57016
57077
  writeFileSync29(path4, (0, import_yaml23.stringify)(policy), "utf8");
57017
57078
  return added;
@@ -57091,7 +57152,7 @@ function applyKxmPackageUpdate(runtime, notice) {
57091
57152
  detail: `release v${notice.latest} has no sha256 digest for ${name}; refusing to install`
57092
57153
  };
57093
57154
  }
57094
- const releaseDir = mkdtempSync3(join58(tmpdir3(), "kxm-pkg-update-"));
57155
+ const releaseDir = mkdtempSync3(join59(tmpdir3(), "kxm-pkg-update-"));
57095
57156
  try {
57096
57157
  const downloader = notice.source === "github" && !ghReleaseDownloadAvailable(runtime.env) ? "curl" : "gh";
57097
57158
  const planned = planKxmPackageUpdate(notice.source, notice.latest, releaseDir, notice.asset, downloader, notice.localRoot);
@@ -57529,7 +57590,7 @@ async function cmdImprove(runtime, options = {}) {
57529
57590
  print(runtime.io, runtime.json, { ok: false, command: "improve", error: "config_invalid", detail: message }, `config_invalid: ${message}`);
57530
57591
  return 1;
57531
57592
  }
57532
- const candidatesDir = options.outDir ? resolve33(runtime.cwd, options.outDir) : join58(root, ".kxm", "candidates");
57593
+ const candidatesDir = options.outDir ? resolve33(runtime.cwd, options.outDir) : join59(root, ".kxm", "candidates");
57533
57594
  const report2 = buildImprovementReport(loaded2.records, {
57534
57595
  candidatesDir,
57535
57596
  projectRoot: root,
@@ -57539,7 +57600,7 @@ async function cmdImprove(runtime, options = {}) {
57539
57600
  halfLifeDays: config.improvement.telemetryHalfLifeDays,
57540
57601
  routeSwitches: loaded2.routeSwitches
57541
57602
  });
57542
- const reportDir = join58(runtime.dirs.assets, "improvements");
57603
+ const reportDir = join59(runtime.dirs.assets, "improvements");
57543
57604
  const reportPath = writeImprovementReport(reportDir, report2, runtime.dryRun);
57544
57605
  const text = [
57545
57606
  "Sources:",
@@ -57881,7 +57942,7 @@ async function cmdRoutingReport(runtime, options) {
57881
57942
  let catalog;
57882
57943
  if (includeEquivalentListCost) {
57883
57944
  try {
57884
- const pricesPath = options.prices ? resolve33(runtime.cwd, options.prices) : join58(runtime.dirs.workspace, "prices.yaml");
57945
+ const pricesPath = options.prices ? resolve33(runtime.cwd, options.prices) : join59(runtime.dirs.workspace, "prices.yaml");
57885
57946
  catalog = loadPriceCatalog(pricesPath);
57886
57947
  } catch {
57887
57948
  }
@@ -25462,7 +25462,7 @@ function sessionTokenFixHint(policy) {
25462
25462
  }
25463
25463
 
25464
25464
  // plugins/kxm/src/mcp-server.ts
25465
- var VERSION = "0.7.179";
25465
+ var VERSION = "0.7.181";
25466
25466
  var CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
25467
25467
  var inbox = /* @__PURE__ */ new Map();
25468
25468
  var notifiedInbox = /* @__PURE__ */ new Set();
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plugin",
3
- "version": "0.7.179",
3
+ "version": "0.7.181",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -1,8 +1,8 @@
1
- import { existsSync } from "node:fs";
2
- import { resolve } from "node:path";
3
- import { DatabaseSync } from "./sqlite.ts";
1
+ import { existsSync, lstatSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { DatabaseSync, openReadOnlyDatabase } from "./sqlite.ts";
4
4
  import { discoverKxmProjectRoot } from "./project-config.ts";
5
- import { kxmProjectRunEventsPath } from "./runtime-store.ts";
5
+ import { kxmRuntimePaths, projectRuntimeKey } from "./runtime-paths.ts";
6
6
  import { parseRouteSwitchRecord, type RouteSwitchRecord } from "./route-switch.ts";
7
7
  import { parseRoutingRecordV2, ROUTING_RECORD_V2_SCHEMA, type RoutingRecord, type RoutingRecordV2 } from "./routing.ts";
8
8
  import { readRoutingRecords, telemetryPath } from "./telemetry.ts";
@@ -10,10 +10,14 @@ import { readRoutingRecords, telemetryPath } from "./telemetry.ts";
10
10
  /**
11
11
  * Routing-record sources for `kxm improve` and `kxm routing report`.
12
12
  *
13
- * The project's Runtime event store is read read-only: one SELECT over the
14
- * events table, never the runs table, the run plans, or the prompts sidecar,
15
- * and never a write or a migration. Each settled attempt's outcome is resolved
16
- * in memory from the event log; nothing resolved here is stored.
13
+ * Each Runtime event store is read read-only: one SELECT over the events
14
+ * table, never the runs table, the run plans, or the prompts sidecar, and
15
+ * never a write or a migration. When the user-state registry is present and
16
+ * this checkout is registered, the home store and every lane store for that
17
+ * project id are read. The registry itself is opened read-only at schema
18
+ * version 2: no constructor, migration, write lock, directory, or sidecar.
19
+ * Each settled attempt's outcome is resolved in memory from the event log;
20
+ * nothing resolved here is stored.
17
21
  */
18
22
 
19
23
  export interface RoutingSourceSummary {
@@ -45,6 +49,15 @@ const ENGINE_EVENTS_SQL = "SELECT run_id, sequence, event_type, payload FROM eve
45
49
  + " WHERE event_type IN ('routing.attempt.recorded','routing.route_switched','step.entered','run.status_changed')"
46
50
  + " ORDER BY run_id, sequence";
47
51
 
52
+ /** Home row first, then lanes by registration time and project key. One query. */
53
+ const REGISTRY_SIBLING_SQL = "SELECT sibling.project_key AS project_key, sibling.lane_of AS lane_of"
54
+ + " FROM projects AS current"
55
+ + " JOIN projects AS sibling ON sibling.project_id = current.project_id"
56
+ + " WHERE current.project_key = ?"
57
+ + " ORDER BY CASE WHEN sibling.lane_of IS NULL THEN 0 ELSE 1 END, sibling.registered_at, sibling.project_key";
58
+
59
+ const PROJECT_KEY = /^[0-9a-f]{24}$/;
60
+
48
61
  /** The simulated producer's harness label; its attempts measure nothing. */
49
62
  const SIMULATED_HARNESS = "driver-simulated";
50
63
 
@@ -175,6 +188,75 @@ export function readEngineRoutingRecords(path: string): EngineRoutingRead {
175
188
  };
176
189
  }
177
190
 
191
+ function registryUnreadable(registryDb: string, detail: string): Error {
192
+ return new Error(`improve_source_unreadable: ${registryDb}: ${detail}`);
193
+ }
194
+
195
+ /**
196
+ * Event stores for this checkout.
197
+ *
198
+ * An absent registry, or a registry that has no row for this checkout's
199
+ * project key, keeps the single store derived from the checkout path. A
200
+ * present registry must be a regular schema-v2 database. Paths then come
201
+ * from the stored keys and the Runtime projects directory, so a stale
202
+ * checkout path in the registry cannot choose the store.
203
+ */
204
+ function discoverProjectEventStores(projectRoot: string, env: NodeJS.ProcessEnv): string[] {
205
+ const paths = kxmRuntimePaths({ env });
206
+ const ownStore = join(paths.projectsDir, projectRuntimeKey(projectRoot), "run-events.db");
207
+ const registryDb = paths.registryDb;
208
+ const info = lstatSync(registryDb, { throwIfNoEntry: false });
209
+ if (!info) return [ownStore];
210
+ if (!info.isFile()) throw registryUnreadable(registryDb, "runtime registry must be a regular database file");
211
+ for (const suffix of ["-wal", "-shm"]) {
212
+ const sidecar = lstatSync(`${registryDb}${suffix}`, { throwIfNoEntry: false });
213
+ if (sidecar?.isSymbolicLink()) throw registryUnreadable(registryDb, "runtime registry sidecar must not be a link");
214
+ }
215
+
216
+ let database: DatabaseSync | undefined;
217
+ try {
218
+ database = openReadOnlyDatabase(registryDb);
219
+ database.exec("PRAGMA busy_timeout = 5000");
220
+ const versionRow = database.prepare("PRAGMA user_version").get() as { user_version?: number } | undefined;
221
+ const version = versionRow?.user_version ?? 0;
222
+ if (version !== 2) throw registryUnreadable(registryDb, `registry schema version ${version}; this reader requires 2`);
223
+ const rows = database.prepare(REGISTRY_SIBLING_SQL).all(projectRuntimeKey(projectRoot));
224
+ if (rows.length === 0) return [ownStore];
225
+ return eventStoresFromRegistryRows(registryDb, paths.projectsDir, rows);
226
+ } catch (error) {
227
+ const message = error instanceof Error ? error.message : String(error);
228
+ if (message.startsWith("improve_source_unreadable:")) throw error;
229
+ throw new Error(`improve_source_unreadable: ${registryDb}: ${message}`, { cause: error });
230
+ } finally {
231
+ try {
232
+ database?.close();
233
+ } catch {
234
+ // Closing a read-only handle has nothing to flush.
235
+ }
236
+ }
237
+ }
238
+
239
+ function eventStoresFromRegistryRows(registryDb: string, projectsDir: string, rows: Array<Record<string, unknown>>): string[] {
240
+ const seen = new Set<string>();
241
+ const parsed: Array<{ projectKey: string; laneOf: string | null }> = [];
242
+ for (const row of rows) {
243
+ const projectKey = typeof row.project_key === "string" ? row.project_key : "";
244
+ const laneRaw = row.lane_of;
245
+ const laneOf = laneRaw === null || laneRaw === undefined ? null : typeof laneRaw === "string" ? laneRaw : "";
246
+ if (!PROJECT_KEY.test(projectKey) || seen.has(projectKey) || (laneOf !== null && !PROJECT_KEY.test(laneOf))) {
247
+ throw registryUnreadable(registryDb, "registry identity is corrupt");
248
+ }
249
+ seen.add(projectKey);
250
+ parsed.push({ projectKey, laneOf });
251
+ }
252
+ const homes = parsed.filter((row) => row.laneOf === null);
253
+ const homeKey = homes[0]?.projectKey;
254
+ if (homes.length !== 1 || homeKey === undefined || parsed.some((row) => row.laneOf !== null && row.laneOf !== homeKey)) {
255
+ throw registryUnreadable(registryDb, "registry identity is corrupt");
256
+ }
257
+ return parsed.map((row) => join(projectsDir, row.projectKey, "run-events.db"));
258
+ }
259
+
178
260
  function readJsonlSource(kind: "telemetry" | "file", path: string): { records: Array<RoutingRecord | RoutingRecordV2>; source: RoutingSourceSummary } {
179
261
  const exists = existsSync(path);
180
262
  const records = exists ? readRoutingRecords(path).map((entry) => entry.routing) : [];
@@ -190,10 +272,10 @@ function attemptKey(record: RoutingRecord | RoutingRecordV2): string | undefined
190
272
  /**
191
273
  * Load routing records for improvement and routing reports.
192
274
  *
193
- * With `file`, only that file is read. Otherwise the project's Runtime event
194
- * store (when cwd is inside a KXM project) comes first, then telemetry.jsonl.
195
- * v2 records are de-duplicated by attemptId: the first occurrence wins and the
196
- * later source counts the drop.
275
+ * With `file`, only that file is read. Otherwise every discovered Runtime
276
+ * event store (when cwd is inside a KXM project) comes first, then
277
+ * telemetry.jsonl. v2 records are de-duplicated by attemptId: the first
278
+ * occurrence wins and the later source counts the drop.
197
279
  */
198
280
  export function loadRoutingSources(options: {
199
281
  cwd: string;
@@ -206,7 +288,11 @@ export function loadRoutingSources(options: {
206
288
  if (options.file !== undefined) {
207
289
  reads.push(readJsonlSource("file", resolve(options.cwd, options.file)));
208
290
  } else {
209
- if (projectRoot !== undefined) reads.push(readEngineRoutingRecords(kxmProjectRunEventsPath(projectRoot, options.env)));
291
+ if (projectRoot !== undefined) {
292
+ for (const path of discoverProjectEventStores(projectRoot, options.env)) {
293
+ reads.push(readEngineRoutingRecords(path));
294
+ }
295
+ }
210
296
  reads.push(readJsonlSource("telemetry", telemetryPath(options.logsDir)));
211
297
  }
212
298
 
@@ -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.179";
14
+ const VERSION = "0.7.181";
15
15
  const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
16
16
  const inbox = new Map<string, MessageRecord>();
17
17
  const notifiedInbox = new Set<string>();