@kontextmind/kxm 0.7.180 → 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.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +9 -0
- package/docs/architecture/inventory.md +1 -1
- package/docs/contributing/test-matrix.md +2 -1
- package/docs/reference/cli-reference.md +9 -9
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +103 -42
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/src/improve-sources.ts +99 -13
- package/plugins/kxm/src/mcp-server.ts +1 -1
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
|
|
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
|
|
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
|
-
-
|
|
3510
|
-
- A telemetry record whose `attemptId`
|
|
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`;
|
|
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
|
|
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
|
|
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
|
@@ -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.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",
|
package/plugins/kxm/dist/cli.js
CHANGED
|
@@ -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
|
|
20764
|
-
const minimumPassed =
|
|
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
|
|
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:
|
|
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
|
|
25183
|
-
if (!
|
|
25184
|
-
const declared = stringValue2(
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 ??
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
56435
|
+
return join57(base, "completions", `kxm.${shell}`);
|
|
56375
56436
|
}
|
|
56376
56437
|
function bashRcCandidate(options) {
|
|
56377
56438
|
const home = effectiveHome(options);
|
|
56378
|
-
const candidates = [
|
|
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
|
|
56386
|
-
return
|
|
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
|
|
56392
|
-
return
|
|
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(
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
57041
|
+
path: join58(projectRoot, ".kxm", "models", `${routeId2}.yaml`),
|
|
56981
57042
|
content: (0, import_yaml23.stringify)(modelDocument)
|
|
56982
57043
|
});
|
|
56983
57044
|
files.push({
|
|
56984
|
-
path:
|
|
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:
|
|
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 =
|
|
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(
|
|
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) :
|
|
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 =
|
|
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) :
|
|
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.
|
|
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();
|
package/plugins/kxm/package.json
CHANGED
|
@@ -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 {
|
|
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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
|
194
|
-
* store (when cwd is inside a KXM project) comes first, then
|
|
195
|
-
* v2 records are de-duplicated by attemptId: the first
|
|
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)
|
|
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.
|
|
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>();
|