akm-cli 0.9.0-beta.52 → 0.9.0-beta.53

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.
Files changed (42) hide show
  1. package/dist/assets/hints/cli-hints-full.md +6 -5
  2. package/dist/cli.js +0 -7
  3. package/dist/commands/env/env-cli.js +3 -2
  4. package/dist/commands/env/env.js +14 -67
  5. package/dist/commands/health/checks.js +28 -15
  6. package/dist/commands/health.js +68 -1
  7. package/dist/commands/improve/collapse-detector.js +419 -0
  8. package/dist/commands/improve/consolidate.js +72 -54
  9. package/dist/commands/improve/distill.js +79 -13
  10. package/dist/commands/improve/extract.js +13 -6
  11. package/dist/commands/improve/homeostatic.js +109 -79
  12. package/dist/commands/improve/improve-cli.js +67 -1
  13. package/dist/commands/improve/improve.js +10 -0
  14. package/dist/commands/improve/loop-stages.js +39 -1
  15. package/dist/commands/improve/outcome-loop.js +15 -3
  16. package/dist/commands/improve/preparation.js +17 -8
  17. package/dist/commands/improve/salience.js +49 -32
  18. package/dist/commands/read/curate.js +5 -9
  19. package/dist/commands/read/knowledge.js +4 -0
  20. package/dist/commands/read/search.js +5 -2
  21. package/dist/commands/read/show.js +3 -3
  22. package/dist/core/asset/asset-spec.js +3 -2
  23. package/dist/core/config/config-schema.js +39 -17
  24. package/dist/core/eval/rank-metrics.js +113 -0
  25. package/dist/core/state/migrations.js +56 -0
  26. package/dist/core/state-db.js +146 -19
  27. package/dist/indexer/ensure-index.js +33 -90
  28. package/dist/indexer/index-writer-lock.js +0 -11
  29. package/dist/indexer/index-written-assets.js +105 -0
  30. package/dist/indexer/passes/metadata.js +20 -0
  31. package/dist/indexer/search/db-search.js +29 -1
  32. package/dist/indexer/search/ranking-contributors.js +33 -1
  33. package/dist/indexer/search/ranking.js +66 -0
  34. package/dist/indexer/search/search-fields.js +6 -0
  35. package/dist/llm/feature-gate.js +6 -2
  36. package/dist/output/renderers.js +8 -13
  37. package/dist/output/shapes/helpers.js +0 -3
  38. package/dist/output/shapes/passthrough.js +1 -0
  39. package/dist/scripts/migrate-storage.js +152 -33
  40. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +41 -18
  41. package/dist/storage/repositories/index-db.js +10 -1
  42. package/package.json +2 -4
@@ -1,8 +1,68 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ import fs from "node:fs";
5
+ import { makeAssetRef } from "../../core/asset/asset-ref.js";
6
+ import { getStateDbPath } from "../../core/state-db.js";
7
+ import { openDatabase } from "../../storage/database.js";
4
8
  import { getUtilityScoresByIds } from "../db/db.js";
5
9
  import { applyScoreContributors, applyUtilityContributors } from "./ranking-contributors.js";
10
+ /**
11
+ * R2 — best-effort load of `asset_salience.rank_score` from state.db for the
12
+ * ranked items. Fail-open: any error (state.db locked by a concurrent improve
13
+ * run, missing table, unreadable path) returns an empty map, which makes the
14
+ * salience contributor a no-op — byte-identical to pre-R2 ranking.
15
+ *
16
+ * Deliberately NOT `openStateDatabase()`: that helper runs migrations and sets
17
+ * a 30 s busy timeout — too heavy for a search hot path. This opens read-only,
18
+ * never creates or migrates state.db (missing file / missing table = empty
19
+ * map), and caps lock waits at 250 ms so a concurrent improve run can only
20
+ * ever cost the search a quarter second, not a stall.
21
+ */
22
+ export function loadSalienceRankScores(items) {
23
+ const result = new Map();
24
+ if (items.length === 0)
25
+ return result;
26
+ try {
27
+ const dbPath = getStateDbPath();
28
+ if (!fs.existsSync(dbPath))
29
+ return result; // improve loop has never run here
30
+ const idByRef = new Map();
31
+ for (const item of items) {
32
+ idByRef.set(makeAssetRef(item.entry.type, item.entry.name), item.id);
33
+ }
34
+ const stateDb = openDatabase(dbPath, { readonly: true });
35
+ try {
36
+ try {
37
+ stateDb.exec("PRAGMA busy_timeout = 250");
38
+ }
39
+ catch {
40
+ // pragma failure on a readonly handle is fine — default timeout applies
41
+ }
42
+ const refs = [...idByRef.keys()];
43
+ const CHUNK = 500;
44
+ for (let i = 0; i < refs.length; i += CHUNK) {
45
+ const chunk = refs.slice(i, i + CHUNK);
46
+ const placeholders = chunk.map(() => "?").join(",");
47
+ const rows = stateDb
48
+ .prepare(`SELECT asset_ref, rank_score FROM asset_salience WHERE asset_ref IN (${placeholders})`)
49
+ .all(...chunk);
50
+ for (const row of rows) {
51
+ const id = idByRef.get(row.asset_ref);
52
+ if (id !== undefined)
53
+ result.set(id, row.rank_score);
54
+ }
55
+ }
56
+ }
57
+ finally {
58
+ stateDb.close();
59
+ }
60
+ }
61
+ catch {
62
+ // Fail open — search must never break because state.db is unavailable.
63
+ }
64
+ return result;
65
+ }
6
66
  export function normalizeFtsScores(results) {
7
67
  const ftsScoreMap = new Map();
8
68
  if (results.length === 0)
@@ -71,12 +131,18 @@ export function applyRankingRules(options) {
71
131
  applyScoreContributors(item, rankingContext);
72
132
  }
73
133
  const { global: utilScoresMap, scoped: scopedUtilScoresMap } = getUtilityScoresByIds(options.db, options.items.map((item) => item.id), options.scopeKey);
134
+ // R2 — compose the improve loop's salience into user-facing ranking.
135
+ // undefined = load from state.db (default); null = explicitly disabled.
136
+ const salienceRankScores = options.salienceRankScores === null
137
+ ? new Map()
138
+ : (options.salienceRankScores ?? loadSalienceRankScores(options.items));
74
139
  const utilityContext = {
75
140
  ...rankingContext,
76
141
  utilityScores: utilScoresMap,
77
142
  scopedUtilityScores: scopedUtilScoresMap,
78
143
  utilityDecayConfig: options.utilityDecayConfig,
79
144
  positiveFeedbackCounts: options.positiveFeedbackCounts,
145
+ salienceRankScores,
80
146
  };
81
147
  for (const item of options.items) {
82
148
  applyUtilityContributors(item, utilityContext);
@@ -11,6 +11,12 @@
11
11
  * - hints: searchHints + examples + usage + intent fields
12
12
  * - content: TOC headings (lowest-weight catch-all)
13
13
  */
14
+ // NOTE (R5): the collapse detector's frozen canary queries are built from the
15
+ // same surface this function indexes (name tokens / tags / description) and
16
+ // scored via FTS against it. Changing what buildSearchFields includes shifts
17
+ // the detector's recall baseline for ALL existing canary sets — coordinate
18
+ // with src/commands/improve/collapse-detector.ts (buildCanaryQuery) and expect
19
+ // operators to re-mint via `akm improve canary --refresh` after such a change.
14
20
  export function buildSearchFields(entry) {
15
21
  const name = entry.name.replace(/[-_]/g, " ").toLowerCase();
16
22
  const description = (entry.description ?? "").toLowerCase();
@@ -24,8 +24,12 @@ const FEATURE_LOCATION = {
24
24
  metadata_enhance: (cfg) => cfg.index?.metadataEnhance?.enabled ?? false,
25
25
  // Legacy default: false
26
26
  curate_rerank: (cfg) => cfg.search?.curateRerank?.enabled ?? false,
27
- // Legacy default: false
28
- lesson_quality_gate: (cfg) => cfg.profiles?.improve?.default?.processes?.distill?.qualityGate?.enabled ?? false,
27
+ // Default ON since R3 (docs/design/improve-self-learning-analysis.md G5):
28
+ // distill is a primary acquisition path and the judge fails open (no LLM /
29
+ // timeout / parse failure all pass through), so the gate only ever filters
30
+ // when a judge verdict actually exists. Opt out via
31
+ // profiles.improve.default.processes.distill.qualityGate.enabled: false.
32
+ lesson_quality_gate: (cfg) => cfg.profiles?.improve?.default?.processes?.distill?.qualityGate?.enabled ?? true,
29
33
  // Legacy default: false
30
34
  proposal_quality_gate: (cfg) => cfg.profiles?.improve?.default?.processes?.reflect?.qualityGate?.enabled ?? false,
31
35
  // Legacy default: false
@@ -364,23 +364,21 @@ const scriptSourceRenderer = {
364
364
  };
365
365
  // ── 8. env-file ───────────────────────────────────────────────────────────────
366
366
  /**
367
- * Env renderer. Returns ONLY key names and start-of-line comments never
368
- * values. Deliberately omits content/template/prompt so env values cannot leak
369
- * through `akm show`.
367
+ * Env renderer. Returns ONLY key names never values, and never comment
368
+ * text (comments routinely contain commented-out credentials). Deliberately
369
+ * omits content/template/prompt so env values cannot leak through `akm show`.
370
370
  */
371
371
  const envFileRenderer = {
372
372
  name: "env-file",
373
373
  buildShowResponse(ctx) {
374
374
  const name = deriveName(ctx);
375
- const { keys, comments } = listVaultKeys(ctx.absPath);
375
+ const { keys } = listVaultKeys(ctx.absPath);
376
376
  return {
377
377
  type: "env",
378
378
  name,
379
379
  path: ctx.absPath,
380
- action: "Environment — keys + comments only. Use `akm env run <ref> -- <command>` to run with the whole .env injected; prefer `--clean` to minimize inherited parent env. AKM itself does not print values, but child stdout/stderr is not redacted. `akm env export <ref> --out <file>` writes a sourceable script to a file. Never `source` the raw file. Values stay on disk and are never written to akm's stdout.",
381
- description: comments.length > 0 ? comments.join("\n") : undefined,
380
+ action: "Environment — key names only. Use `akm env run <ref> -- <command>` to run with the whole .env injected; prefer `--clean` to minimize inherited parent env. AKM itself does not print values, but child stdout/stderr is not redacted. `akm env export <ref> --out <file>` writes a sourceable script to a file. Never `source` the raw file. Values stay on disk and are never written to akm's stdout.",
382
381
  keys,
383
- comments,
384
382
  };
385
383
  },
386
384
  enrichSearchHit(hit, _stashDir) {
@@ -647,12 +645,9 @@ function applyScriptMetadata(entry, ctx) {
647
645
  }
648
646
  }
649
647
  function applyEnvMetadata(entry, ctx) {
650
- const { keys, comments } = listVaultKeys(ctx.absPath);
651
- if (comments.length > 0 && !entry.description) {
652
- entry.description = comments.join(" ").slice(0, 500);
653
- entry.source = "comments";
654
- entry.confidence = 0.7;
655
- }
648
+ // Key names only comment text must never reach description/search_text
649
+ // (comments routinely contain commented-out credentials).
650
+ const { keys } = listVaultKeys(ctx.absPath);
656
651
  if (keys.length > 0) {
657
652
  entry.searchHints = keys;
658
653
  }
@@ -369,7 +369,6 @@ export function shapeShowOutput(result, detail, shape = "human") {
369
369
  "workflowParameters",
370
370
  "steps",
371
371
  "keys",
372
- "comments",
373
372
  "related",
374
373
  ]);
375
374
  }
@@ -385,7 +384,6 @@ export function shapeShowOutput(result, detail, shape = "human") {
385
384
  "run",
386
385
  "origin",
387
386
  "keys",
388
- "comments",
389
387
  "related",
390
388
  ]);
391
389
  }
@@ -411,7 +409,6 @@ export function shapeShowOutput(result, detail, shape = "human") {
411
409
  "cwd",
412
410
  "activeRun",
413
411
  "keys",
414
- "comments",
415
412
  "related",
416
413
  // path and editable are always projected so JSON consumers can locate and
417
414
  // edit the asset without needing --detail full (QA #7).
@@ -42,6 +42,7 @@ const PASSTHROUGH_COMMANDS = [
42
42
  "extract",
43
43
  "health",
44
44
  "improve",
45
+ "improve-canary",
45
46
  "lessons-coverage",
46
47
  "import",
47
48
  "index",
@@ -468,21 +468,11 @@ function scanKeys(text) {
468
468
  }
469
469
  return keys;
470
470
  }
471
- function scanComments(text) {
472
- const comments = [];
473
- for (const line of text.split(/\r?\n/)) {
474
- const trimmed = line.trimStart();
475
- if (trimmed.startsWith("#")) {
476
- comments.push(trimmed.slice(1).trimStart());
477
- }
478
- }
479
- return comments;
480
- }
481
471
  function listKeys(envPath) {
482
472
  if (!fs.existsSync(envPath))
483
- return { keys: [], comments: [] };
473
+ return { keys: [] };
484
474
  const text = fs.readFileSync(envPath, "utf8");
485
- return { keys: scanKeys(text), comments: scanComments(text) };
475
+ return { keys: scanKeys(text) };
486
476
  }
487
477
  var import_dotenv, ASSIGN_RE;
488
478
  var init_env = __esm(() => {
@@ -8325,12 +8315,7 @@ function applyScriptMetadata(entry, ctx) {
8325
8315
  }
8326
8316
  }
8327
8317
  function applyEnvMetadata(entry, ctx) {
8328
- const { keys, comments } = listKeys(ctx.absPath);
8329
- if (comments.length > 0 && !entry.description) {
8330
- entry.description = comments.join(" ").slice(0, 500);
8331
- entry.source = "comments";
8332
- entry.confidence = 0.7;
8333
- }
8318
+ const { keys } = listKeys(ctx.absPath);
8334
8319
  if (keys.length > 0) {
8335
8320
  entry.searchHints = keys;
8336
8321
  }
@@ -9401,6 +9386,44 @@ var init_migrations = __esm(() => {
9401
9386
  up: `
9402
9387
  ALTER TABLE asset_salience ADD COLUMN encoding_source TEXT DEFAULT NULL;
9403
9388
  `
9389
+ },
9390
+ {
9391
+ id: "016-collapse-churn-detector",
9392
+ up: `
9393
+ CREATE TABLE IF NOT EXISTS canary_queries (
9394
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
9395
+ canary_set_id TEXT NOT NULL,
9396
+ anchor_ref TEXT NOT NULL,
9397
+ query TEXT NOT NULL,
9398
+ source TEXT NOT NULL DEFAULT 'auto',
9399
+ active INTEGER NOT NULL DEFAULT 1,
9400
+ created_at TEXT NOT NULL
9401
+ );
9402
+ CREATE INDEX IF NOT EXISTS idx_canary_queries_active
9403
+ ON canary_queries(active, canary_set_id);
9404
+
9405
+ CREATE TABLE IF NOT EXISTS improve_cycle_metrics (
9406
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
9407
+ run_id TEXT NOT NULL,
9408
+ ts TEXT NOT NULL,
9409
+ pass TEXT NOT NULL,
9410
+ canary_set_id TEXT NOT NULL,
9411
+ mean_recall REAL NOT NULL,
9412
+ mean_ndcg REAL NOT NULL,
9413
+ mean_mrr REAL NOT NULL,
9414
+ canary_ranks_json TEXT NOT NULL,
9415
+ store_total INTEGER NOT NULL,
9416
+ store_by_type_json TEXT NOT NULL,
9417
+ distinct_content_ratio REAL NOT NULL,
9418
+ mean_bigram_diversity REAL NOT NULL,
9419
+ over_generation_count INTEGER NOT NULL,
9420
+ accepted_actions INTEGER NOT NULL,
9421
+ merge_floor_violations INTEGER NOT NULL DEFAULT 0,
9422
+ alerts_json TEXT NOT NULL DEFAULT '[]'
9423
+ );
9424
+ CREATE INDEX IF NOT EXISTS idx_improve_cycle_metrics_ts
9425
+ ON improve_cycle_metrics(ts);
9426
+ `
9404
9427
  }
9405
9428
  ];
9406
9429
  });
@@ -9423,10 +9446,12 @@ __export(exports_state_db, {
9423
9446
  recordFsProposalsImport: () => recordFsProposalsImport,
9424
9447
  readStateEvents: () => readStateEvents,
9425
9448
  queryTaskHistory: () => queryTaskHistory,
9449
+ queryRecentCycleMetrics: () => queryRecentCycleMetrics,
9426
9450
  queryImproveRuns: () => queryImproveRuns,
9427
9451
  queryCompletedTaskIntervals: () => queryCompletedTaskIntervals,
9428
9452
  purgeOldImproveRuns: () => purgeOldImproveRuns,
9429
9453
  purgeOldEvents: () => purgeOldEvents,
9454
+ purgeOldCycleMetrics: () => purgeOldCycleMetrics,
9430
9455
  proposalToRowValues: () => proposalToRowValues,
9431
9456
  proposalRowToProposal: () => proposalRowToProposal,
9432
9457
  persistPhaseThreshold: () => persistPhaseThreshold,
@@ -9436,8 +9461,11 @@ __export(exports_state_db, {
9436
9461
  listStateProposalIdsByPrefix: () => listStateProposalIdsByPrefix,
9437
9462
  listProposalGateDecisions: () => listProposalGateDecisions,
9438
9463
  listExistingTableNames: () => listExistingTableNames,
9464
+ listActiveCanarySetIds: () => listActiveCanarySetIds,
9439
9465
  insertProposalIfAbsent: () => insertProposalIfAbsent,
9440
9466
  insertEvent: () => insertEvent,
9467
+ insertCycleMetrics: () => insertCycleMetrics,
9468
+ insertCanaries: () => insertCanaries,
9441
9469
  importEventsJsonl: () => importEventsJsonl,
9442
9470
  hasImportedFsProposals: () => hasImportedFsProposals,
9443
9471
  getTaskHistoryRuns: () => getTaskHistoryRuns,
@@ -9446,15 +9474,19 @@ __export(exports_state_db, {
9446
9474
  getStateDbPath: () => getStateDbPath,
9447
9475
  getRecombineHypothesis: () => getRecombineHypothesis,
9448
9476
  getPhaseThreshold: () => getPhaseThreshold,
9477
+ getLatestCycleMetrics: () => getLatestCycleMetrics,
9449
9478
  getLastExtractRunAt: () => getLastExtractRunAt,
9450
9479
  getExtractedSessionsMap: () => getExtractedSessionsMap,
9451
9480
  getExtractedSession: () => getExtractedSession,
9452
9481
  getConsolidationJudgedMap: () => getConsolidationJudgedMap,
9482
+ getCanariesBySetId: () => getCanariesBySetId,
9453
9483
  getBodyEmbeddings: () => getBodyEmbeddings,
9484
+ getActiveCanaries: () => getActiveCanaries,
9454
9485
  findMatchingRecombineHypothesis: () => findMatchingRecombineHypothesis,
9455
9486
  eventRowToEnvelope: () => eventRowToEnvelope,
9456
9487
  embeddingToBlob: () => embeddingToBlob,
9457
9488
  decayUnseenRecombineHypotheses: () => decayUnseenRecombineHypotheses,
9489
+ deactivateCanarySet: () => deactivateCanarySet,
9458
9490
  computeImproveRunMetrics: () => computeImproveRunMetrics,
9459
9491
  blobToEmbedding: () => blobToEmbedding
9460
9492
  });
@@ -9686,7 +9718,7 @@ function insertProposalIfAbsent(db, proposal, stashDir) {
9686
9718
  }
9687
9719
  function isRetryableBeginError(err) {
9688
9720
  const msg = (err instanceof Error ? err.message : String(err)).toLowerCase();
9689
- return msg.includes("within a transaction") || msg.includes("database is locked") || msg.includes("database table is locked") || msg.includes("did not open a transaction");
9721
+ return msg.includes("database is locked") || msg.includes("database table is locked") || msg.includes("did not open a transaction");
9690
9722
  }
9691
9723
  function sleepSyncMs(ms) {
9692
9724
  if (ms <= 0)
@@ -9694,6 +9726,9 @@ function sleepSyncMs(ms) {
9694
9726
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
9695
9727
  }
9696
9728
  function withImmediateTransaction(db, fn) {
9729
+ if (db.inTransaction) {
9730
+ return fn();
9731
+ }
9697
9732
  let lastBeginErr;
9698
9733
  for (let attempt = 1;attempt <= WITH_IMMEDIATE_TX_MAX_ATTEMPTS; attempt++) {
9699
9734
  try {
@@ -9704,9 +9739,11 @@ function withImmediateTransaction(db, fn) {
9704
9739
  } catch (err) {
9705
9740
  lastBeginErr = err;
9706
9741
  if (isRetryableBeginError(err) && attempt < WITH_IMMEDIATE_TX_MAX_ATTEMPTS) {
9707
- try {
9708
- db.exec("ROLLBACK");
9709
- } catch {}
9742
+ if (db.inTransaction) {
9743
+ try {
9744
+ db.exec("ROLLBACK");
9745
+ } catch {}
9746
+ }
9710
9747
  sleepSyncMs(2 ** (attempt - 1));
9711
9748
  continue;
9712
9749
  }
@@ -9714,12 +9751,17 @@ function withImmediateTransaction(db, fn) {
9714
9751
  }
9715
9752
  try {
9716
9753
  const result = fn();
9754
+ if (!db.inTransaction) {
9755
+ throw new Error("withImmediateTransaction invariant violated: transaction opened by BEGIN IMMEDIATE was no longer active after the transaction body ran; refusing to COMMIT (writes may have escaped serialization)");
9756
+ }
9717
9757
  db.exec("COMMIT");
9718
9758
  return result;
9719
9759
  } catch (err) {
9720
- try {
9721
- db.exec("ROLLBACK");
9722
- } catch {}
9760
+ if (db.inTransaction) {
9761
+ try {
9762
+ db.exec("ROLLBACK");
9763
+ } catch {}
9764
+ }
9723
9765
  throw err;
9724
9766
  }
9725
9767
  }
@@ -10091,6 +10133,75 @@ function upsertBodyEmbeddings(db, entries) {
10091
10133
  }
10092
10134
  })();
10093
10135
  }
10136
+ function insertCanaries(db, canarySetId, canaries, now) {
10137
+ if (canaries.length === 0)
10138
+ return;
10139
+ const ts = now ?? new Date().toISOString();
10140
+ const stmt = db.prepare(`
10141
+ INSERT INTO canary_queries (canary_set_id, anchor_ref, query, source, active, created_at)
10142
+ VALUES (?, ?, ?, ?, 1, ?)
10143
+ `);
10144
+ db.transaction(() => {
10145
+ for (const c of canaries) {
10146
+ stmt.run(canarySetId, c.anchorRef, c.query, c.source ?? "auto", ts);
10147
+ }
10148
+ })();
10149
+ }
10150
+ function getActiveCanaries(db) {
10151
+ return db.prepare(`SELECT * FROM canary_queries
10152
+ WHERE active = 1 AND canary_set_id = (
10153
+ SELECT canary_set_id FROM canary_queries WHERE active = 1
10154
+ ORDER BY created_at DESC, id DESC LIMIT 1
10155
+ )
10156
+ ORDER BY id`).all();
10157
+ }
10158
+ function getCanariesBySetId(db, canarySetId) {
10159
+ return db.prepare(`SELECT * FROM canary_queries WHERE canary_set_id = ? ORDER BY id`).all(canarySetId);
10160
+ }
10161
+ function listActiveCanarySetIds(db) {
10162
+ const rows = db.prepare(`SELECT DISTINCT canary_set_id FROM canary_queries WHERE active = 1`).all();
10163
+ return rows.map((r) => r.canary_set_id);
10164
+ }
10165
+ function deactivateCanarySet(db, canarySetId) {
10166
+ const result = db.prepare(`UPDATE canary_queries SET active = 0 WHERE canary_set_id = ? AND active = 1`).run(canarySetId);
10167
+ const changes = result.changes ?? 0;
10168
+ return typeof changes === "bigint" ? Number(changes) : changes;
10169
+ }
10170
+ function insertCycleMetrics(db, row) {
10171
+ db.prepare(`
10172
+ INSERT INTO improve_cycle_metrics
10173
+ (run_id, ts, pass, canary_set_id, mean_recall, mean_ndcg, mean_mrr,
10174
+ canary_ranks_json, store_total, store_by_type_json, distinct_content_ratio,
10175
+ mean_bigram_diversity, over_generation_count, accepted_actions,
10176
+ merge_floor_violations, alerts_json)
10177
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
10178
+ `).run(row.run_id, row.ts, row.pass, row.canary_set_id, row.mean_recall, row.mean_ndcg, row.mean_mrr, row.canary_ranks_json, row.store_total, row.store_by_type_json, row.distinct_content_ratio, row.mean_bigram_diversity, row.over_generation_count, row.accepted_actions, row.merge_floor_violations, row.alerts_json);
10179
+ }
10180
+ function queryRecentCycleMetrics(db, canarySetId, limit) {
10181
+ const rows = db.prepare(`SELECT run_id, ts, pass, canary_set_id, mean_recall, mean_ndcg, mean_mrr,
10182
+ canary_ranks_json, store_total, store_by_type_json, distinct_content_ratio,
10183
+ mean_bigram_diversity, over_generation_count, accepted_actions,
10184
+ merge_floor_violations, alerts_json
10185
+ FROM improve_cycle_metrics WHERE canary_set_id = ?
10186
+ ORDER BY ts DESC, id DESC LIMIT ?`).all(canarySetId, Math.max(0, limit));
10187
+ return rows.reverse();
10188
+ }
10189
+ function getLatestCycleMetrics(db) {
10190
+ const row = db.prepare(`SELECT run_id, ts, pass, canary_set_id, mean_recall, mean_ndcg, mean_mrr,
10191
+ canary_ranks_json, store_total, store_by_type_json, distinct_content_ratio,
10192
+ mean_bigram_diversity, over_generation_count, accepted_actions,
10193
+ merge_floor_violations, alerts_json
10194
+ FROM improve_cycle_metrics ORDER BY ts DESC, id DESC LIMIT 1`).get();
10195
+ return row == null ? undefined : row;
10196
+ }
10197
+ function purgeOldCycleMetrics(db, retentionDays = 365) {
10198
+ if (!Number.isFinite(retentionDays) || retentionDays <= 0)
10199
+ return 0;
10200
+ const cutoff = new Date(Date.now() - retentionDays * 86400000).toISOString();
10201
+ const result = db.prepare("DELETE FROM improve_cycle_metrics WHERE ts < ?").run(cutoff);
10202
+ const changes = result.changes ?? 0;
10203
+ return typeof changes === "bigint" ? Number(changes) : changes;
10204
+ }
10094
10205
  var WITH_IMMEDIATE_TX_MAX_ATTEMPTS = 5;
10095
10206
  var init_state_db = __esm(() => {
10096
10207
  init_managed_db();
@@ -15790,7 +15901,7 @@ var init_config_types = __esm(() => {
15790
15901
  });
15791
15902
 
15792
15903
  // src/core/config/config-schema.ts
15793
- var positiveInt, nonNegativeNumber, nonEmptyString, httpUrl, LlmCapabilitiesSchema, LlmConnectionConfigSchema, LlmProfileConfigSchema, EmbeddingOllamaOptionsSchema, EmbeddingConnectionConfigSchema, AgentPlatformSchema, AgentProfileConfigSchema, ImproveProcessConfigSchema, ImproveProfileProcessesSchema, ImproveProfileConfigSchema, ProfilesSchema, DefaultsSchema, SourceConfigEntryOptionsSchema, SourceConfigEntrySchema, RegistryConfigEntrySchema, KitSourceSchema, InstalledStashEntrySchema, OutputConfigSchema, SearchGraphBoostSchema, SearchConfigSchema, FeedbackConfigSchema, ImproveUtilityDecaySchema, ImproveCalibrationSchema, ImproveExplorationSchema, ImproveSalienceSchema, ImproveConfigSchema, GRAPH_EXTRACTION_INCLUDE_TYPES_ALLOWED, INDEX_PASS_PROVIDER_KEYS, INDEX_PASS_KNOWN_KEYS, IndexPassConfigSchema, MetadataEnhanceSchema, StalenessDetectionSchema, IndexConfigSchema, SetupTaskSchedulesSchema, SetupConfigSchema, AkmConfigShape, AkmConfigBaseSchema, AkmConfigSchema;
15904
+ var positiveInt, nonNegativeNumber, nonEmptyString, httpUrl, LlmCapabilitiesSchema, LlmConnectionConfigSchema, LlmProfileConfigSchema, EmbeddingOllamaOptionsSchema, EmbeddingConnectionConfigSchema, AgentPlatformSchema, AgentProfileConfigSchema, ImproveProcessConfigSchema, ImproveProfileProcessesSchema, ImproveProfileConfigSchema, ProfilesSchema, DefaultsSchema, SourceConfigEntryOptionsSchema, SourceConfigEntrySchema, RegistryConfigEntrySchema, KitSourceSchema, InstalledStashEntrySchema, OutputConfigSchema, SearchGraphBoostSchema, SearchConfigSchema, FeedbackConfigSchema, ImproveUtilityDecaySchema, ImproveCalibrationSchema, ImproveExplorationSchema, ImproveSalienceSchema, ImproveCollapseDetectorSchema, ImproveConfigSchema, GRAPH_EXTRACTION_INCLUDE_TYPES_ALLOWED, INDEX_PASS_PROVIDER_KEYS, INDEX_PASS_KNOWN_KEYS, IndexPassConfigSchema, MetadataEnhanceSchema, StalenessDetectionSchema, IndexConfigSchema, SetupTaskSchedulesSchema, SetupConfigSchema, AkmConfigShape, AkmConfigBaseSchema, AkmConfigSchema;
15794
15905
  var init_config_schema = __esm(() => {
15795
15906
  init_zod();
15796
15907
  init_config_types();
@@ -15883,11 +15994,6 @@ var init_config_schema = __esm(() => {
15883
15994
  indexSessions: exports_external.boolean().optional(),
15884
15995
  minSessionDuration: exports_external.number().min(0).optional(),
15885
15996
  p90ChunkSecondsDefault: exports_external.number().finite().positive().optional(),
15886
- homeostaticDemotion: exports_external.object({
15887
- enabled: exports_external.boolean().optional(),
15888
- staleDays: exports_external.number().int().min(0).optional(),
15889
- demotionFactor: exports_external.number().min(0).max(1).optional()
15890
- }).passthrough().optional(),
15891
15997
  schemaSimilarity: exports_external.object({
15892
15998
  enabled: exports_external.boolean().optional(),
15893
15999
  epsilon: exports_external.number().min(0).max(1).optional(),
@@ -15900,7 +16006,9 @@ var init_config_schema = __esm(() => {
15900
16006
  enabled: exports_external.boolean().optional(),
15901
16007
  maxGeneration: exports_external.number().int().min(1).optional(),
15902
16008
  lexicalDiversityCheck: exports_external.boolean().optional(),
15903
- randomClusterFraction: exports_external.number().min(0).max(1).optional()
16009
+ randomClusterFraction: exports_external.number().min(0).max(1).optional(),
16010
+ mergeInformationFloor: exports_external.boolean().optional(),
16011
+ minSpecificityRetention: exports_external.number().min(0).max(1).optional()
15904
16012
  }).passthrough().optional(),
15905
16013
  cls: exports_external.object({
15906
16014
  enabled: exports_external.boolean().optional(),
@@ -16068,12 +16176,23 @@ var init_config_schema = __esm(() => {
16068
16176
  salienceThreshold: exports_external.number().min(0).max(1).optional(),
16069
16177
  replayBudget: exports_external.number().int().min(0).optional()
16070
16178
  }).passthrough();
16179
+ ImproveCollapseDetectorSchema = exports_external.object({
16180
+ enabled: exports_external.boolean().optional(),
16181
+ canaryCount: exports_external.number().int().min(3).max(200).optional(),
16182
+ k: exports_external.number().int().min(1).max(100).optional(),
16183
+ windowCycles: exports_external.number().int().min(2).max(50).optional(),
16184
+ recallDropThreshold: exports_external.number().min(0).max(1).optional(),
16185
+ entropyDropThreshold: exports_external.number().min(0).max(1).optional(),
16186
+ churnMinAcceptedActions: exports_external.number().int().min(1).optional(),
16187
+ retentionDays: exports_external.number().int().min(1).optional()
16188
+ }).passthrough();
16071
16189
  ImproveConfigSchema = exports_external.object({
16072
16190
  utilityDecay: ImproveUtilityDecaySchema.optional(),
16073
16191
  eventRetentionDays: nonNegativeNumber.optional(),
16074
16192
  calibration: ImproveCalibrationSchema.optional(),
16075
16193
  exploration: ImproveExplorationSchema.optional(),
16076
- salience: ImproveSalienceSchema.optional()
16194
+ salience: ImproveSalienceSchema.optional(),
16195
+ collapseDetector: ImproveCollapseDetectorSchema.optional()
16077
16196
  }).passthrough();
16078
16197
  GRAPH_EXTRACTION_INCLUDE_TYPES_ALLOWED = [
16079
16198
  "memory",
@@ -455,21 +455,11 @@ function scanKeys(text) {
455
455
  }
456
456
  return keys;
457
457
  }
458
- function scanComments(text) {
459
- const comments = [];
460
- for (const line of text.split(/\r?\n/)) {
461
- const trimmed = line.trimStart();
462
- if (trimmed.startsWith("#")) {
463
- comments.push(trimmed.slice(1).trimStart());
464
- }
465
- }
466
- return comments;
467
- }
468
458
  function listKeys(envPath) {
469
459
  if (!fs3.existsSync(envPath))
470
- return { keys: [], comments: [] };
460
+ return { keys: [] };
471
461
  const text = fs3.readFileSync(envPath, "utf8");
472
- return { keys: scanKeys(text), comments: scanComments(text) };
462
+ return { keys: scanKeys(text) };
473
463
  }
474
464
  var import_dotenv, ASSIGN_RE;
475
465
  var init_env = __esm(() => {
@@ -8211,12 +8201,7 @@ function applyScriptMetadata(entry, ctx) {
8211
8201
  }
8212
8202
  }
8213
8203
  function applyEnvMetadata(entry, ctx) {
8214
- const { keys, comments } = listKeys(ctx.absPath);
8215
- if (comments.length > 0 && !entry.description) {
8216
- entry.description = comments.join(" ").slice(0, 500);
8217
- entry.source = "comments";
8218
- entry.confidence = 0.7;
8219
- }
8204
+ const { keys } = listKeys(ctx.absPath);
8220
8205
  if (keys.length > 0) {
8221
8206
  entry.searchHints = keys;
8222
8207
  }
@@ -9142,6 +9127,44 @@ var MIGRATIONS = [
9142
9127
  up: `
9143
9128
  ALTER TABLE asset_salience ADD COLUMN encoding_source TEXT DEFAULT NULL;
9144
9129
  `
9130
+ },
9131
+ {
9132
+ id: "016-collapse-churn-detector",
9133
+ up: `
9134
+ CREATE TABLE IF NOT EXISTS canary_queries (
9135
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
9136
+ canary_set_id TEXT NOT NULL,
9137
+ anchor_ref TEXT NOT NULL,
9138
+ query TEXT NOT NULL,
9139
+ source TEXT NOT NULL DEFAULT 'auto',
9140
+ active INTEGER NOT NULL DEFAULT 1,
9141
+ created_at TEXT NOT NULL
9142
+ );
9143
+ CREATE INDEX IF NOT EXISTS idx_canary_queries_active
9144
+ ON canary_queries(active, canary_set_id);
9145
+
9146
+ CREATE TABLE IF NOT EXISTS improve_cycle_metrics (
9147
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
9148
+ run_id TEXT NOT NULL,
9149
+ ts TEXT NOT NULL,
9150
+ pass TEXT NOT NULL,
9151
+ canary_set_id TEXT NOT NULL,
9152
+ mean_recall REAL NOT NULL,
9153
+ mean_ndcg REAL NOT NULL,
9154
+ mean_mrr REAL NOT NULL,
9155
+ canary_ranks_json TEXT NOT NULL,
9156
+ store_total INTEGER NOT NULL,
9157
+ store_by_type_json TEXT NOT NULL,
9158
+ distinct_content_ratio REAL NOT NULL,
9159
+ mean_bigram_diversity REAL NOT NULL,
9160
+ over_generation_count INTEGER NOT NULL,
9161
+ accepted_actions INTEGER NOT NULL,
9162
+ merge_floor_violations INTEGER NOT NULL DEFAULT 0,
9163
+ alerts_json TEXT NOT NULL DEFAULT '[]'
9164
+ );
9165
+ CREATE INDEX IF NOT EXISTS idx_improve_cycle_metrics_ts
9166
+ ON improve_cycle_metrics(ts);
9167
+ `
9145
9168
  }
9146
9169
  ];
9147
9170
  function runMigrations2(db) {
@@ -3,6 +3,12 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import { closeDatabase, openExistingDatabase } from "../../indexer/db/db.js";
5
5
  import { resolveStorageLocations } from "../locations.js";
6
+ /**
7
+ * Busy-timeout (ms) for read-path telemetry writers. Small on purpose: a
8
+ * usage-event insert contending with a background reindex should be dropped,
9
+ * not waited on for the default 30s.
10
+ */
11
+ export const TELEMETRY_BUSY_TIMEOUT_MS = 250;
6
12
  /**
7
13
  * Scoped-resource (loan pattern) helper for the index database (`index.db`).
8
14
  *
@@ -32,9 +38,12 @@ import { resolveStorageLocations } from "../locations.js";
32
38
  * @param fn Receives the open index database; must finish all DB work before returning.
33
39
  * @returns Whatever `fn` returns.
34
40
  */
35
- export function withIndexDb(fn) {
41
+ export function withIndexDb(fn, opts) {
36
42
  const db = openExistingDatabase(resolveStorageLocations().indexDb);
37
43
  try {
44
+ if (opts?.busyTimeoutMs !== undefined) {
45
+ db.exec(`PRAGMA busy_timeout = ${Math.max(0, Math.floor(opts.busyTimeoutMs))}`);
46
+ }
38
47
  return fn(db);
39
48
  }
40
49
  finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-cli",
3
- "version": "0.9.0-beta.52",
3
+ "version": "0.9.0-beta.53",
4
4
  "type": "module",
5
5
  "description": "akm (Agent Knowledge Management) — A package manager for AI agent skills, commands, tools, and knowledge. Works with Claude Code, OpenCode, Cursor, and any AI coding assistant.",
6
6
  "keywords": [
@@ -58,9 +58,7 @@
58
58
  "sweep:tmp": "bun scripts/sweep-test-tmp.ts",
59
59
  "test": "bash scripts/test-unit.sh",
60
60
  "test:unit": "bash scripts/test-unit.sh",
61
- "test:integration": "bun run sweep:tmp && bun test --isolate --timeout=30000 ./tests/integration ./tests/commands ./tests/workflows",
62
- "test:unit:shard": "bun run sweep:tmp && bun test --isolate --timeout=30000 ./tests --path-ignore-patterns=tests/integration --shard=${SHARD:?set SHARD=k/N}",
63
- "test:integration:shard": "bun run sweep:tmp && bun test --isolate --timeout=30000 ./tests/integration ./tests/commands ./tests/workflows --shard=${SHARD:?set SHARD=k/N}",
61
+ "test:integration": "bun run sweep:tmp && bun test --isolate --timeout=30000 ./tests/integration",
64
62
  "test:node-smoke": "bun scripts/node-smoke.ts",
65
63
  "test:node-compat": "AKM_NODE_COMPAT_TESTS=1 bun test --timeout=120000 tests/integration/node-compat.test.ts",
66
64
  "test:time": "bun scripts/test-timing-report.ts",