akm-cli 0.9.0-beta.2 → 0.9.0-beta.26

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 (111) hide show
  1. package/CHANGELOG.md +614 -0
  2. package/dist/assets/prompts/consolidate-system.md +23 -0
  3. package/dist/assets/prompts/contradiction-judge.md +33 -0
  4. package/dist/assets/prompts/distill-knowledge-system.md +22 -0
  5. package/dist/assets/prompts/distill-lesson-system.md +36 -0
  6. package/dist/assets/prompts/extract-session.md +5 -1
  7. package/dist/assets/prompts/graph-extract-system.md +1 -0
  8. package/dist/assets/prompts/memory-infer-system.md +1 -0
  9. package/dist/assets/prompts/memory-infer-user.md +5 -0
  10. package/dist/assets/prompts/metadata-enhance-system.md +1 -0
  11. package/dist/assets/prompts/procedural-system.md +44 -0
  12. package/dist/assets/prompts/recombine-system.md +40 -0
  13. package/dist/assets/prompts/staleness-detect-system.md +6 -0
  14. package/dist/assets/prompts/validate-summary-judge.md +1 -0
  15. package/dist/assets/templates/html/default.html +78 -0
  16. package/dist/assets/templates/html/health.html +730 -0
  17. package/dist/assets/templates/html/vendor/echarts.min.js +45 -0
  18. package/dist/cli/shared.js +21 -5
  19. package/dist/cli.js +47 -5
  20. package/dist/commands/agent/contribute-cli.js +16 -3
  21. package/dist/commands/feedback-cli.js +15 -6
  22. package/dist/commands/graph/graph.js +75 -71
  23. package/dist/commands/health/checks.js +48 -0
  24. package/dist/commands/health/html-report.js +790 -0
  25. package/dist/commands/health.js +478 -15
  26. package/dist/commands/improve/calibration.js +161 -0
  27. package/dist/commands/improve/consolidate.js +634 -111
  28. package/dist/commands/improve/dedup.js +482 -0
  29. package/dist/commands/improve/distill.js +145 -69
  30. package/dist/commands/improve/encoding-salience.js +205 -0
  31. package/dist/commands/improve/extract-cli.js +115 -1
  32. package/dist/commands/improve/extract-prompt.js +33 -2
  33. package/dist/commands/improve/extract-watch.js +140 -0
  34. package/dist/commands/improve/extract.js +280 -35
  35. package/dist/commands/improve/feedback-valence.js +54 -0
  36. package/dist/commands/improve/homeostatic.js +467 -0
  37. package/dist/commands/improve/improve-auto-accept.js +139 -6
  38. package/dist/commands/improve/improve-profiles.js +12 -0
  39. package/dist/commands/improve/improve.js +1851 -515
  40. package/dist/commands/improve/memory/memory-contradiction-detect.js +23 -28
  41. package/dist/commands/improve/outcome-loop.js +256 -0
  42. package/dist/commands/improve/proactive-maintenance.js +87 -0
  43. package/dist/commands/improve/procedural.js +409 -0
  44. package/dist/commands/improve/recombine.js +488 -0
  45. package/dist/commands/improve/reflect-noise.js +0 -0
  46. package/dist/commands/improve/reflect.js +51 -1
  47. package/dist/commands/improve/related-sessions.js +120 -0
  48. package/dist/commands/improve/salience.js +386 -0
  49. package/dist/commands/improve/triage.js +95 -0
  50. package/dist/commands/lint/agent-linter.js +19 -24
  51. package/dist/commands/lint/base-linter.js +173 -60
  52. package/dist/commands/lint/command-linter.js +19 -24
  53. package/dist/commands/lint/env-key-rules.js +34 -1
  54. package/dist/commands/lint/index.js +30 -13
  55. package/dist/commands/lint/memory-linter.js +1 -1
  56. package/dist/commands/lint/registry.js +5 -2
  57. package/dist/commands/lint/task-linter.js +3 -3
  58. package/dist/commands/lint/workflow-linter.js +26 -1
  59. package/dist/commands/proposal/drain.js +73 -6
  60. package/dist/commands/proposal/proposal-cli.js +22 -10
  61. package/dist/commands/proposal/proposal.js +17 -1
  62. package/dist/commands/proposal/validators/proposals.js +369 -329
  63. package/dist/commands/read/curate.js +294 -79
  64. package/dist/commands/read/search-cli.js +7 -0
  65. package/dist/commands/read/search.js +1 -0
  66. package/dist/commands/remember.js +6 -2
  67. package/dist/commands/sources/installed-stashes.js +5 -1
  68. package/dist/commands/sources/stash-cli.js +10 -2
  69. package/dist/core/asset/frontmatter.js +166 -167
  70. package/dist/core/asset/markdown.js +8 -0
  71. package/dist/core/config/config-schema.js +241 -0
  72. package/dist/core/config/config.js +2 -2
  73. package/dist/core/logs-db.js +305 -0
  74. package/dist/core/paths.js +3 -0
  75. package/dist/core/state-db.js +706 -42
  76. package/dist/indexer/db/db.js +347 -38
  77. package/dist/indexer/db/graph-db.js +81 -86
  78. package/dist/indexer/ensure-index.js +152 -17
  79. package/dist/indexer/graph/graph-boost.js +51 -41
  80. package/dist/indexer/index-writer-lock.js +99 -0
  81. package/dist/indexer/indexer.js +114 -111
  82. package/dist/indexer/passes/memory-inference.js +71 -25
  83. package/dist/indexer/passes/staleness-detect.js +2 -5
  84. package/dist/indexer/search/db-search.js +15 -4
  85. package/dist/indexer/search/ranking.js +4 -0
  86. package/dist/integrations/harnesses/claude/session-log.js +27 -5
  87. package/dist/integrations/harnesses/opencode/session-log.js +9 -0
  88. package/dist/integrations/session-logs/index.js +16 -0
  89. package/dist/llm/client.js +38 -4
  90. package/dist/llm/embedder.js +27 -3
  91. package/dist/llm/embedders/local.js +66 -2
  92. package/dist/llm/graph-extract.js +2 -1
  93. package/dist/llm/memory-infer.js +4 -8
  94. package/dist/llm/metadata-enhance.js +9 -1
  95. package/dist/llm/usage-persist.js +77 -0
  96. package/dist/llm/usage-telemetry.js +103 -0
  97. package/dist/output/context.js +3 -2
  98. package/dist/output/html-render.js +73 -0
  99. package/dist/output/shapes/curate.js +14 -2
  100. package/dist/output/shapes/helpers.js +17 -1
  101. package/dist/output/text/helpers.js +78 -1
  102. package/dist/runtime.js +25 -1
  103. package/dist/scripts/migrate-storage.js +1194 -607
  104. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +455 -270
  105. package/dist/sources/providers/tar-utils.js +16 -8
  106. package/dist/storage/sqlite-pragmas.js +146 -0
  107. package/dist/tasks/runner.js +99 -16
  108. package/dist/workflows/db.js +5 -2
  109. package/dist/workflows/validate-summary.js +2 -7
  110. package/docs/data-and-telemetry.md +1 -0
  111. package/package.json +7 -5
@@ -241,13 +241,18 @@ export function collectGraphRelatedHit(context, filePath) {
241
241
  * Find graph files that share entities with the given file.
242
242
  *
243
243
  * Implementation: SQL self-join on graph_file_entities, scoped by stash_root,
244
- * grouped by entry_id, ordered by shared-entity count desc. Touches ~50-200
244
+ * grouped by file_path, ordered by shared-entity count desc. Touches ~50-200
245
245
  * rows instead of loading the entire snapshot into memory. Cold-call latency
246
246
  * drops from ~30-60ms (full snapshot parse) to ~2-5ms on typical stashes.
247
247
  *
248
+ * #624-P1: the graph tables are keyed on (stash_root, file_path, body_hash) —
249
+ * NOT entries.id — so candidates are identified by file_path (the unique index
250
+ * idx_graph_files_path guarantees one graph_files row per path).
251
+ *
248
252
  * The returned `ref` field carries the canonical asset ref (`type:name`)
249
- * resolved from entries.entry_key when the entry is indexed. Callers should
250
- * fall back to formatting `path` when `ref` is undefined (orphan graph row).
253
+ * resolved from entries.entry_key when the file is indexed. Callers should
254
+ * fall back to formatting `path` when `ref` is undefined (graph row with no
255
+ * matching entries row).
251
256
  */
252
257
  export function listRelatedPathsForFile(stashRoot, filePath, limit = 5, db) {
253
258
  if (!db) {
@@ -255,113 +260,118 @@ export function listRelatedPathsForFile(stashRoot, filePath, limit = 5, db) {
255
260
  // callers pass a handle), so degrade to empty rather than reopening.
256
261
  return [];
257
262
  }
258
- // Resolve target's entry_id from the stash_root + file_path. The graph rows
259
- // are keyed on entry_id; without it we can't run the join.
260
- let targetEntryId;
263
+ // Confirm the target file has a graph row; without it there is nothing to
264
+ // relate. (Identity is file_path within the stash one row per path.)
261
265
  try {
262
266
  const row = db
263
- .prepare("SELECT entry_id FROM graph_files WHERE stash_root = ? AND file_path = ? LIMIT 1")
267
+ .prepare("SELECT 1 AS present FROM graph_files WHERE stash_root = ? AND file_path = ? LIMIT 1")
264
268
  .get(stashRoot, filePath);
265
- targetEntryId = row?.entry_id;
269
+ if (row === undefined)
270
+ return [];
266
271
  }
267
272
  catch {
268
273
  return [];
269
274
  }
270
- if (targetEntryId == null)
271
- return [];
272
275
  const effectiveLimit = Math.max(1, limit);
273
- // Shared-entity count per candidate entry_id.
276
+ // Shared-entity count per candidate file_path. The target's entities are the
277
+ // rows for `filePath`; candidates are any OTHER file_path in the stash that
278
+ // shares a normalized entity.
274
279
  let candidateRows;
275
280
  try {
276
281
  candidateRows = db
277
- .prepare(`SELECT gf.entry_id AS entry_id,
278
- gf.file_path AS file_path,
282
+ .prepare(`SELECT gf.file_path AS file_path,
279
283
  gf.file_type AS file_type,
280
284
  COUNT(*) AS shared
281
285
  FROM graph_file_entities target
282
286
  JOIN graph_file_entities e
283
287
  ON e.stash_root = target.stash_root
284
288
  AND e.entity_norm = target.entity_norm
285
- AND e.entry_id != target.entry_id
289
+ AND e.file_path != target.file_path
286
290
  JOIN graph_files gf
287
- ON gf.entry_id = e.entry_id
288
- WHERE target.entry_id = ?
291
+ ON gf.stash_root = e.stash_root
292
+ AND gf.file_path = e.file_path
293
+ AND gf.body_hash = e.body_hash
294
+ WHERE target.file_path = ?
289
295
  AND target.stash_root = ?
290
- GROUP BY gf.entry_id
296
+ GROUP BY gf.file_path
291
297
  ORDER BY shared DESC, gf.file_path ASC
292
298
  LIMIT ?`)
293
- .all(targetEntryId, stashRoot, effectiveLimit);
299
+ .all(filePath, stashRoot, effectiveLimit);
294
300
  }
295
301
  catch {
296
302
  return [];
297
303
  }
298
304
  if (candidateRows.length === 0)
299
305
  return [];
300
- const candidateIds = candidateRows.map((r) => r.entry_id);
301
- const placeholders = candidateIds.map(() => "?").join(",");
306
+ const candidatePaths = candidateRows.map((r) => r.file_path);
307
+ const placeholders = candidatePaths.map(() => "?").join(",");
302
308
  // Pull the shared entity names (joined by normalized casing) for display.
303
309
  const sharedRows = db
304
- .prepare(`SELECT e.entry_id AS entry_id, e.entity AS entity
310
+ .prepare(`SELECT e.file_path AS file_path, e.entity AS entity
305
311
  FROM graph_file_entities e
306
312
  JOIN graph_file_entities target
307
313
  ON target.stash_root = e.stash_root
308
314
  AND target.entity_norm = e.entity_norm
309
- WHERE e.entry_id IN (${placeholders})
310
- AND target.entry_id = ?
315
+ WHERE e.file_path IN (${placeholders})
316
+ AND e.stash_root = ?
317
+ AND target.file_path = ?
311
318
  AND target.stash_root = ?`)
312
- .all(...candidateIds, targetEntryId, stashRoot);
313
- const sharedByEntry = new Map();
319
+ .all(...candidatePaths, stashRoot, filePath, stashRoot);
320
+ const sharedByPath = new Map();
314
321
  for (const row of sharedRows) {
315
- let bucket = sharedByEntry.get(row.entry_id);
322
+ let bucket = sharedByPath.get(row.file_path);
316
323
  if (!bucket) {
317
324
  bucket = new Set();
318
- sharedByEntry.set(row.entry_id, bucket);
325
+ sharedByPath.set(row.file_path, bucket);
319
326
  }
320
327
  bucket.add(row.entity);
321
328
  }
322
329
  // Relation count for each candidate (relations where either endpoint
323
330
  // matches one of the shared entities).
324
- const relationCountByEntry = new Map();
331
+ const relationCountByPath = new Map();
325
332
  const relationRows = db
326
- .prepare(`SELECT entry_id, from_entity, to_entity
333
+ .prepare(`SELECT file_path, from_entity, to_entity
327
334
  FROM graph_file_relations
328
- WHERE entry_id IN (${placeholders})`)
329
- .all(...candidateIds);
335
+ WHERE file_path IN (${placeholders})
336
+ AND stash_root = ?`)
337
+ .all(...candidatePaths, stashRoot);
330
338
  for (const row of relationRows) {
331
- const shared = sharedByEntry.get(row.entry_id);
339
+ const shared = sharedByPath.get(row.file_path);
332
340
  if (!shared)
333
341
  continue;
334
342
  if (shared.has(row.from_entity) || shared.has(row.to_entity)) {
335
- relationCountByEntry.set(row.entry_id, (relationCountByEntry.get(row.entry_id) ?? 0) + 1);
343
+ relationCountByPath.set(row.file_path, (relationCountByPath.get(row.file_path) ?? 0) + 1);
336
344
  }
337
345
  }
338
346
  // Optional: ref lookup via entries.entry_key. entry_key is stored as
339
347
  // `${stash_dir}:${type}:${name}` — strip the stash-dir prefix to get the
340
- // user-facing `type:name`.
341
- const refByEntryId = new Map();
348
+ // user-facing `type:name`. Resolve by (stash_dir, file_path) now that the
349
+ // graph rows are no longer keyed on entries.id.
350
+ const refByPath = new Map();
342
351
  try {
343
352
  const entryRows = db
344
- .prepare(`SELECT id, entry_key, stash_dir FROM entries WHERE id IN (${placeholders})`)
345
- .all(...candidateIds);
353
+ .prepare(`SELECT entry_key, stash_dir, file_path FROM entries
354
+ WHERE file_path IN (${placeholders}) AND stash_dir = ?`)
355
+ .all(...candidatePaths, stashRoot);
346
356
  for (const row of entryRows) {
347
357
  const ref = stripStashPrefix(row.entry_key, row.stash_dir);
348
358
  if (ref)
349
- refByEntryId.set(row.id, ref);
359
+ refByPath.set(row.file_path, ref);
350
360
  }
351
361
  }
352
362
  catch {
353
363
  /* ignore — refs are best-effort */
354
364
  }
355
365
  return candidateRows.map((row) => {
356
- const sharedSet = sharedByEntry.get(row.entry_id) ?? new Set();
366
+ const sharedSet = sharedByPath.get(row.file_path) ?? new Set();
357
367
  const sharedEntities = [...sharedSet].sort((a, b) => a.localeCompare(b));
358
- const ref = refByEntryId.get(row.entry_id);
368
+ const ref = refByPath.get(row.file_path);
359
369
  return {
360
370
  ...(ref ? { ref } : {}),
361
371
  path: row.file_path,
362
372
  type: row.file_type,
363
373
  sharedEntities,
364
- relationCount: relationCountByEntry.get(row.entry_id) ?? 0,
374
+ relationCount: relationCountByPath.get(row.file_path) ?? 0,
365
375
  };
366
376
  });
367
377
  }
@@ -0,0 +1,99 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { probeLock, releaseLock, releaseLockIfOwned, tryAcquireLockSync } from "../core/file-lock.js";
7
+ import { getDbPath, getIndexWriterLockPath } from "../core/paths.js";
8
+ const INDEX_WRITER_LOCK_STALE_AFTER_MS = 12 * 60 * 60 * 1000;
9
+ const INDEX_WRITER_WAIT_MS = 100;
10
+ const heldLocks = new Map();
11
+ function buildPayload(purpose, pid = process.pid) {
12
+ return JSON.stringify({
13
+ pid,
14
+ purpose,
15
+ dbPath: getDbPath(),
16
+ startedAt: new Date().toISOString(),
17
+ });
18
+ }
19
+ function delay(ms) {
20
+ return new Promise((resolve) => setTimeout(resolve, ms));
21
+ }
22
+ function throwIfAborted(signal) {
23
+ if (!signal?.aborted)
24
+ return;
25
+ throw signal.reason instanceof Error ? signal.reason : new Error("index writer wait aborted");
26
+ }
27
+ function releaseHeldLock(lockPath) {
28
+ const held = heldLocks.get(lockPath);
29
+ if (!held)
30
+ return;
31
+ held.depth -= 1;
32
+ if (held.depth > 0)
33
+ return;
34
+ heldLocks.delete(lockPath);
35
+ process.off("exit", held.exitHandler);
36
+ releaseLockIfOwned(lockPath, process.pid);
37
+ }
38
+ function retainHeldLock(lockPath) {
39
+ const existing = heldLocks.get(lockPath);
40
+ if (existing) {
41
+ existing.depth += 1;
42
+ return { lockPath, release: () => releaseHeldLock(lockPath) };
43
+ }
44
+ const exitHandler = () => releaseLockIfOwned(lockPath, process.pid);
45
+ process.on("exit", exitHandler);
46
+ heldLocks.set(lockPath, { depth: 1, exitHandler });
47
+ return { lockPath, release: () => releaseHeldLock(lockPath) };
48
+ }
49
+ function detachHeldLock(lockPath) {
50
+ const held = heldLocks.get(lockPath);
51
+ if (!held)
52
+ return;
53
+ heldLocks.delete(lockPath);
54
+ process.off("exit", held.exitHandler);
55
+ }
56
+ export async function acquireIndexWriterLease(options) {
57
+ const mode = options.mode ?? "wait";
58
+ const lockPath = getIndexWriterLockPath();
59
+ fs.mkdirSync(path.dirname(lockPath), { recursive: true });
60
+ if (heldLocks.has(lockPath)) {
61
+ return retainHeldLock(lockPath);
62
+ }
63
+ while (true) {
64
+ throwIfAborted(options.signal);
65
+ if (tryAcquireLockSync(lockPath, buildPayload(options.purpose))) {
66
+ return retainHeldLock(lockPath);
67
+ }
68
+ const probe = probeLock(lockPath, { staleAfterMs: INDEX_WRITER_LOCK_STALE_AFTER_MS });
69
+ if (probe.state === "held" && probe.holderPid === process.pid) {
70
+ return retainHeldLock(lockPath);
71
+ }
72
+ if (probe.state === "stale") {
73
+ releaseLock(lockPath);
74
+ continue;
75
+ }
76
+ if (mode === "try")
77
+ return undefined;
78
+ await delay(INDEX_WRITER_WAIT_MS);
79
+ }
80
+ }
81
+ export async function withIndexWriterLease(options, run) {
82
+ const lease = await acquireIndexWriterLease(options);
83
+ if (!lease) {
84
+ throw new Error(`index writer lease unavailable for ${options.purpose}`);
85
+ }
86
+ try {
87
+ return await run();
88
+ }
89
+ finally {
90
+ lease.release();
91
+ }
92
+ }
93
+ export function handoffIndexWriterLeaseToPid(lease, pid, purpose) {
94
+ fs.writeFileSync(lease.lockPath, buildPayload(purpose, pid), "utf8");
95
+ detachHeldLock(lease.lockPath);
96
+ }
97
+ export function probeIndexWriterLease() {
98
+ return probeLock(getIndexWriterLockPath(), { staleAfterMs: INDEX_WRITER_LOCK_STALE_AFTER_MS });
99
+ }
@@ -12,6 +12,7 @@ import { resolveIndexPassLLM } from "../llm/index-passes.js";
12
12
  import { takeWorkflowDocument } from "../workflows/runtime/document-cache.js";
13
13
  import { clearStaleCacheEntries, closeDatabase, deleteEntriesByDir, deleteEntriesByIds, deleteEntriesByStashDir, deleteIndexDirStatesByStashDir, getAllEntriesForEmbedding, getEmbeddableEntryCount, getEmbeddingCount, getEntriesByDir, getEntryCount, getIndexDirState, getMeta, isVecAvailable, openDatabase, openExistingDatabase, rebuildFts, relinkUsageEvents, setMeta, upsertEmbedding, upsertEntry, upsertIndexDirState, upsertUtilityScore, upsertWorkflowDocument, warnIfVecMissing, } from "./db/db.js";
14
14
  import { deleteStoredGraph } from "./db/graph-db.js";
15
+ import { withIndexWriterLease } from "./index-writer-lock.js";
15
16
  import { applyCuratedFrontmatter, applyWikiFrontmatter, generateMetadataFlat, isEnrichmentComplete, isWorkflowSkipWarning, loadStashFile, shouldIndexStashFile, } from "./passes/metadata.js";
16
17
  import { buildSearchText } from "./search/search-fields.js";
17
18
  import { classifySemanticFailure, clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticStatus, } from "./search/semantic-status.js";
@@ -225,119 +226,121 @@ function runCleanPass(db, dryRun) {
225
226
  }
226
227
  // ── Indexer ──────────────────────────────────────────────────────────────────
227
228
  export async function akmIndex(options) {
228
- const stashDir = options?.stashDir || resolveStashDir();
229
- const onProgress = options?.onProgress ?? (() => { });
230
- const signal = options?.signal;
231
- const reEnrich = options?.reEnrich === true;
232
- const full = options?.full === true;
233
- const clean = options?.clean === true;
234
- const dryRun = options?.dryRun === true;
235
- // Load config and resolve all stash sources
236
- const { loadConfig } = await import("../core/config/config.js");
237
- const config = loadConfig();
238
- // One-time, read-only guard: warn if the writable stash still holds an
239
- // un-migrated `vaults/` directory. In 0.9.0 the indexer skips `vaults/`
240
- // entirely, so an unmigrated vault's `.env` data would silently never be
241
- // indexed. Non-destructive only stats, never reads/writes/deletes.
242
- const { warnOnUnmigratedVaults } = await import("./usage/unmigrated-vaults-guard.js");
243
- warnOnUnmigratedVaults(stashDir);
244
- // Ensure git stash caches are extracted before resolving stash dirs,
245
- // so their content directories exist on disk for the walker to discover.
246
- const { ensureSourceCaches, resolveSourceEntries } = await import("./search/search-source.js");
247
- await ensureSourceCaches(config, { force: full });
248
- const allSourceEntries = resolveSourceEntries(stashDir, config);
249
- const allSourceDirs = allSourceEntries.map((s) => s.path);
250
- const t0 = Date.now();
251
- // Open database — pass embedding dimension from config if available
252
- const dbPath = getDbPath();
253
- const embeddingDim = config.embedding?.dimension;
254
- const db = openDatabase(dbPath, embeddingDim ? { embeddingDim } : undefined);
255
- try {
256
- // Determine incremental vs full mode
257
- const prevStashDir = getMeta(db, "stashDir");
258
- const prevBuiltAt = getMeta(db, "builtAt");
259
- const isIncremental = !full && prevStashDir === stashDir && !!prevBuiltAt;
260
- const builtAtMs = isIncremental && prevBuiltAt ? new Date(prevBuiltAt).getTime() : 0;
261
- // Assemble the run context
262
- const ctx = {
263
- db,
264
- config,
265
- sources: allSourceEntries,
266
- sourceDirs: allSourceDirs,
267
- full,
268
- reEnrich,
269
- stashDir,
270
- onProgress,
271
- signal,
272
- timing: {
273
- t0,
274
- tWalkStart: t0,
275
- tWalkEnd: t0,
276
- tLlmEnd: t0,
277
- tFtsEnd: t0,
278
- tEmbedEnd: t0,
279
- },
280
- isIncremental,
281
- builtAtMs,
282
- hadRemovedSources: false,
283
- scannedDirs: 0,
284
- skippedDirs: 0,
285
- generatedCount: 0,
286
- walkWarnings: [],
287
- dirsNeedingLlm: [],
288
- embeddingResult: null,
289
- graphExtractionResult: null,
290
- };
291
- onProgress({
292
- phase: "summary",
293
- message: buildIndexSummaryMessage({
229
+ return withIndexWriterLease({ purpose: "akm-index", signal: options?.signal }, async () => {
230
+ const stashDir = options?.stashDir || resolveStashDir();
231
+ const onProgress = options?.onProgress ?? (() => { });
232
+ const signal = options?.signal;
233
+ const reEnrich = options?.reEnrich === true;
234
+ const full = options?.full === true;
235
+ const clean = options?.clean === true;
236
+ const dryRun = options?.dryRun === true;
237
+ // Load config and resolve all stash sources
238
+ const { loadConfig } = await import("../core/config/config.js");
239
+ const config = loadConfig();
240
+ // One-time, read-only guard: warn if the writable stash still holds an
241
+ // un-migrated `vaults/` directory. In 0.9.0 the indexer skips `vaults/`
242
+ // entirely, so an unmigrated vault's `.env` data would silently never be
243
+ // indexed. Non-destructive only stats, never reads/writes/deletes.
244
+ const { warnOnUnmigratedVaults } = await import("./usage/unmigrated-vaults-guard.js");
245
+ warnOnUnmigratedVaults(stashDir);
246
+ // Ensure git stash caches are extracted before resolving stash dirs,
247
+ // so their content directories exist on disk for the walker to discover.
248
+ const { ensureSourceCaches, resolveSourceEntries } = await import("./search/search-source.js");
249
+ await ensureSourceCaches(config, { force: full });
250
+ const allSourceEntries = resolveSourceEntries(stashDir, config);
251
+ const allSourceDirs = allSourceEntries.map((s) => s.path);
252
+ const t0 = Date.now();
253
+ // Open database — pass embedding dimension from config if available
254
+ const dbPath = getDbPath();
255
+ const embeddingDim = config.embedding?.dimension;
256
+ const db = openDatabase(dbPath, embeddingDim ? { embeddingDim } : undefined);
257
+ try {
258
+ // Determine incremental vs full mode
259
+ const prevStashDir = getMeta(db, "stashDir");
260
+ const prevBuiltAt = getMeta(db, "builtAt");
261
+ const isIncremental = !full && prevStashDir === stashDir && !!prevBuiltAt;
262
+ const builtAtMs = isIncremental && prevBuiltAt ? new Date(prevBuiltAt).getTime() : 0;
263
+ // Assemble the run context
264
+ const ctx = {
265
+ db,
266
+ config,
267
+ sources: allSourceEntries,
268
+ sourceDirs: allSourceDirs,
269
+ full,
270
+ reEnrich,
271
+ stashDir,
272
+ onProgress,
273
+ signal,
274
+ timing: {
275
+ t0,
276
+ tWalkStart: t0,
277
+ tWalkEnd: t0,
278
+ tLlmEnd: t0,
279
+ tFtsEnd: t0,
280
+ tEmbedEnd: t0,
281
+ },
282
+ isIncremental,
283
+ builtAtMs,
284
+ hadRemovedSources: false,
285
+ scannedDirs: 0,
286
+ skippedDirs: 0,
287
+ generatedCount: 0,
288
+ walkWarnings: [],
289
+ dirsNeedingLlm: [],
290
+ embeddingResult: null,
291
+ graphExtractionResult: null,
292
+ };
293
+ onProgress({
294
+ phase: "summary",
295
+ message: buildIndexSummaryMessage({
296
+ mode: isIncremental ? "incremental" : "full",
297
+ sourcesCount: allSourceDirs.length,
298
+ semanticSearchMode: config.semanticSearchMode,
299
+ embeddingProvider: getEmbeddingProvider(config.embedding),
300
+ llmEnabled: !!resolveIndexPassLLM("enrichment", config),
301
+ vecAvailable: isVecAvailable(db),
302
+ }),
303
+ });
304
+ // ── Phase sequence ───────────────────────────────────────────────────────
305
+ await runSourceCachePhase(ctx);
306
+ await runWalkPhase(ctx);
307
+ await runEmbeddingPhase(ctx);
308
+ await runFinalizePhase(ctx);
309
+ // ────────────────────────────────────────────────────────────────────────
310
+ const { _verification: verification, _totalEntries: totalEntries } = ctx;
311
+ const { timing } = ctx;
312
+ // ── Clean pass ───────────────────────────────────────────────────────────
313
+ // After the normal index completes, remove entries whose source files no
314
+ // longer exist on disk. Remote entries (empty file_path) are skipped.
315
+ let cleanResult;
316
+ if (clean) {
317
+ cleanResult = runCleanPass(db, dryRun);
318
+ }
319
+ // ────────────────────────────────────────────────────────────────────────
320
+ return {
321
+ stashDir,
322
+ totalEntries,
323
+ generatedMetadata: ctx.generatedCount,
324
+ indexPath: dbPath,
294
325
  mode: isIncremental ? "incremental" : "full",
295
- sourcesCount: allSourceDirs.length,
296
- semanticSearchMode: config.semanticSearchMode,
297
- embeddingProvider: getEmbeddingProvider(config.embedding),
298
- llmEnabled: !!resolveIndexPassLLM("enrichment", config),
299
- vecAvailable: isVecAvailable(db),
300
- }),
301
- });
302
- // ── Phase sequence ───────────────────────────────────────────────────────
303
- await runSourceCachePhase(ctx);
304
- await runWalkPhase(ctx);
305
- await runEmbeddingPhase(ctx);
306
- await runFinalizePhase(ctx);
307
- // ────────────────────────────────────────────────────────────────────────
308
- const { _verification: verification, _totalEntries: totalEntries } = ctx;
309
- const { timing } = ctx;
310
- // ── Clean pass ───────────────────────────────────────────────────────────
311
- // After the normal index completes, remove entries whose source files no
312
- // longer exist on disk. Remote entries (empty file_path) are skipped.
313
- let cleanResult;
314
- if (clean) {
315
- cleanResult = runCleanPass(db, dryRun);
326
+ directoriesScanned: ctx.scannedDirs,
327
+ directoriesSkipped: ctx.skippedDirs,
328
+ ...(ctx.walkWarnings.length > 0 ? { warnings: ctx.walkWarnings } : {}),
329
+ verification,
330
+ timing: {
331
+ totalMs: Date.now() - timing.t0,
332
+ walkMs: timing.tWalkEnd - timing.tWalkStart,
333
+ llmMs: timing.tLlmEnd - timing.tWalkEnd,
334
+ embedMs: timing.tEmbedEnd - timing.tLlmEnd,
335
+ ftsMs: timing.tFtsEnd - timing.tEmbedEnd,
336
+ },
337
+ ...(cleanResult !== undefined ? { clean: cleanResult } : {}),
338
+ };
316
339
  }
317
- // ────────────────────────────────────────────────────────────────────────
318
- return {
319
- stashDir,
320
- totalEntries,
321
- generatedMetadata: ctx.generatedCount,
322
- indexPath: dbPath,
323
- mode: isIncremental ? "incremental" : "full",
324
- directoriesScanned: ctx.scannedDirs,
325
- directoriesSkipped: ctx.skippedDirs,
326
- ...(ctx.walkWarnings.length > 0 ? { warnings: ctx.walkWarnings } : {}),
327
- verification,
328
- timing: {
329
- totalMs: Date.now() - timing.t0,
330
- walkMs: timing.tWalkEnd - timing.tWalkStart,
331
- llmMs: timing.tLlmEnd - timing.tWalkEnd,
332
- embedMs: timing.tEmbedEnd - timing.tLlmEnd,
333
- ftsMs: timing.tFtsEnd - timing.tEmbedEnd,
334
- },
335
- ...(cleanResult !== undefined ? { clean: cleanResult } : {}),
336
- };
337
- }
338
- finally {
339
- closeDatabase(db);
340
- }
340
+ finally {
341
+ closeDatabase(db);
342
+ }
343
+ });
341
344
  }
342
345
  // ── Extracted helpers for indexing ────────────────────────────────────────────
343
346
  async function indexEntries(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, doFullDelete = false, onProgress) {