akm-cli 0.9.0-beta.9 → 0.9.0-rc.0

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 (325) hide show
  1. package/CHANGELOG.md +592 -0
  2. package/README.md +12 -4
  3. package/dist/akm +38 -0
  4. package/dist/akm-migrate-storage +38 -0
  5. package/dist/assets/help/help-improve.md +9 -6
  6. package/dist/assets/hints/cli-hints-full.md +6 -5
  7. package/dist/assets/profiles/default.json +9 -4
  8. package/dist/assets/profiles/frequent.json +1 -1
  9. package/dist/assets/profiles/memory-focus.json +1 -1
  10. package/dist/assets/profiles/proactive-maintenance.json +25 -0
  11. package/dist/assets/profiles/quick.json +1 -1
  12. package/dist/assets/profiles/recombine-only.json +21 -0
  13. package/dist/assets/profiles/reflect-distill.json +30 -0
  14. package/dist/assets/profiles/synthesize.json +15 -0
  15. package/dist/assets/profiles/thorough.json +1 -1
  16. package/dist/assets/prompts/consolidate-system.md +23 -0
  17. package/dist/assets/prompts/contradiction-judge.md +33 -0
  18. package/dist/assets/prompts/distill-knowledge-system.md +22 -0
  19. package/dist/assets/prompts/distill-lesson-system.md +36 -0
  20. package/dist/assets/prompts/extract-session.md +11 -3
  21. package/dist/assets/prompts/graph-extract-system.md +1 -0
  22. package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
  23. package/dist/assets/prompts/memory-infer-system.md +1 -0
  24. package/dist/assets/prompts/memory-infer-user.md +5 -0
  25. package/dist/assets/prompts/metadata-enhance-system.md +1 -0
  26. package/dist/assets/prompts/procedural-system.md +44 -0
  27. package/dist/assets/prompts/recombine-system.md +40 -0
  28. package/dist/assets/prompts/staleness-detect-system.md +6 -0
  29. package/dist/assets/prompts/validate-summary-judge.md +1 -0
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
  34. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
  35. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
  36. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
  37. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
  38. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
  39. package/dist/assets/templates/html/health.html +281 -111
  40. package/dist/assets/wiki/ingest-workflow-template.md +45 -16
  41. package/dist/assets/wiki/schema-template.md +4 -4
  42. package/dist/cli/clack.js +56 -0
  43. package/dist/cli/config-migrate.js +7 -1
  44. package/dist/cli/confirm.js +1 -1
  45. package/dist/cli/parse-args.js +46 -1
  46. package/dist/cli/shared.js +28 -0
  47. package/dist/cli.js +25 -21
  48. package/dist/commands/agent/agent-dispatch.js +3 -2
  49. package/dist/commands/agent/agent-support.js +0 -7
  50. package/dist/commands/agent/contribute-cli.js +26 -7
  51. package/dist/commands/config-cli.js +26 -13
  52. package/dist/commands/env/child-env.js +47 -0
  53. package/dist/commands/env/env-cli.js +220 -227
  54. package/dist/commands/env/env.js +14 -67
  55. package/dist/commands/env/secret-cli.js +140 -138
  56. package/dist/commands/feedback-cli.js +156 -155
  57. package/dist/commands/graph/graph-cli.js +5 -13
  58. package/dist/commands/graph/graph.js +3 -3
  59. package/dist/commands/health/advisories.js +151 -0
  60. package/dist/commands/health/checks.js +103 -16
  61. package/dist/commands/health/html-report.js +447 -81
  62. package/dist/commands/health/improve-metrics.js +771 -0
  63. package/dist/commands/health/llm-usage.js +65 -0
  64. package/dist/commands/health/md-report.js +103 -0
  65. package/dist/commands/health/metrics.js +278 -0
  66. package/dist/commands/health/stash-exposure.js +46 -0
  67. package/dist/commands/health/surfaces.js +216 -0
  68. package/dist/commands/health/task-runs.js +135 -0
  69. package/dist/commands/health/types.js +26 -0
  70. package/dist/commands/health/windows.js +195 -0
  71. package/dist/commands/health.js +91 -1091
  72. package/dist/commands/improve/anti-collapse.js +170 -0
  73. package/dist/commands/improve/calibration.js +161 -0
  74. package/dist/commands/improve/collapse-detector.js +421 -0
  75. package/dist/commands/improve/consolidate/chunking.js +141 -0
  76. package/dist/commands/improve/consolidate/eligibility.js +64 -0
  77. package/dist/commands/improve/consolidate/merge.js +145 -0
  78. package/dist/commands/improve/consolidate/sanitize.js +231 -0
  79. package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
  80. package/dist/commands/improve/consolidate.js +1295 -1277
  81. package/dist/commands/improve/dedup.js +482 -0
  82. package/dist/commands/improve/distill/content-repair.js +202 -0
  83. package/dist/commands/improve/distill/promote-memory.js +229 -0
  84. package/dist/commands/improve/distill/quality-gate.js +236 -0
  85. package/dist/commands/improve/distill-guards.js +127 -0
  86. package/dist/commands/improve/distill-promotion-policy.js +826 -167
  87. package/dist/commands/improve/distill.js +228 -605
  88. package/dist/commands/improve/eligibility.js +434 -0
  89. package/dist/commands/improve/encoding-salience.js +205 -0
  90. package/dist/commands/improve/extract-cli.js +179 -59
  91. package/dist/commands/improve/extract-prompt.js +54 -3
  92. package/dist/commands/improve/extract-watch.js +140 -0
  93. package/dist/commands/improve/extract.js +409 -43
  94. package/dist/commands/improve/feedback-valence.js +54 -0
  95. package/dist/commands/improve/hot-probation.js +45 -0
  96. package/dist/commands/improve/improve-auto-accept.js +157 -10
  97. package/dist/commands/improve/improve-cli.js +115 -73
  98. package/dist/commands/improve/improve-profiles.js +28 -8
  99. package/dist/commands/improve/improve-result-file.js +15 -25
  100. package/dist/commands/improve/improve-session.js +58 -0
  101. package/dist/commands/improve/improve.js +485 -2764
  102. package/dist/commands/improve/locks.js +154 -0
  103. package/dist/commands/improve/loop-stages.js +1100 -0
  104. package/dist/commands/improve/memory/memory-belief.js +14 -15
  105. package/dist/commands/improve/memory/memory-contradiction-detect.js +83 -60
  106. package/dist/commands/improve/memory/memory-improve.js +27 -27
  107. package/dist/commands/improve/outcome-loop.js +270 -0
  108. package/dist/commands/improve/preparation.js +2002 -0
  109. package/dist/commands/improve/proactive-maintenance.js +37 -35
  110. package/dist/commands/improve/procedural.js +398 -0
  111. package/dist/commands/improve/recombine.js +818 -0
  112. package/dist/commands/improve/reflect-noise.js +0 -0
  113. package/dist/commands/improve/reflect.js +206 -45
  114. package/dist/commands/improve/salience.js +455 -0
  115. package/dist/commands/improve/schema-similarity-gate.js +168 -0
  116. package/dist/commands/improve/shared.js +51 -0
  117. package/dist/commands/improve/triage.js +93 -0
  118. package/dist/commands/lint/agent-linter.js +19 -24
  119. package/dist/commands/lint/base-linter.js +173 -60
  120. package/dist/commands/lint/command-linter.js +19 -24
  121. package/dist/commands/lint/env-key-rules.js +38 -1
  122. package/dist/commands/lint/fact-linter.js +39 -0
  123. package/dist/commands/lint/index.js +31 -13
  124. package/dist/commands/lint/memory-linter.js +1 -1
  125. package/dist/commands/lint/registry.js +7 -2
  126. package/dist/commands/lint/task-linter.js +3 -3
  127. package/dist/commands/lint/workflow-linter.js +26 -1
  128. package/dist/commands/observability-cli.js +4 -4
  129. package/dist/commands/proposal/drain-policies.js +13 -4
  130. package/dist/commands/proposal/drain.js +45 -51
  131. package/dist/commands/proposal/legacy-import.js +115 -0
  132. package/dist/commands/proposal/proposal-cli.js +24 -34
  133. package/dist/commands/proposal/proposal.js +2 -1
  134. package/dist/commands/proposal/propose.js +8 -3
  135. package/dist/commands/proposal/repository.js +829 -0
  136. package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
  137. package/dist/commands/proposal/validators/proposals.js +93 -895
  138. package/dist/commands/read/curate.js +410 -111
  139. package/dist/commands/read/knowledge.js +10 -3
  140. package/dist/commands/read/remember-cli.js +133 -138
  141. package/dist/commands/read/search-cli.js +15 -8
  142. package/dist/commands/read/search.js +22 -11
  143. package/dist/commands/read/show.js +106 -14
  144. package/dist/commands/registry-cli.js +76 -87
  145. package/dist/commands/remember.js +11 -12
  146. package/dist/commands/sources/add-cli.js +91 -95
  147. package/dist/commands/sources/history.js +1 -1
  148. package/dist/commands/sources/init.js +66 -18
  149. package/dist/commands/sources/installed-stashes.js +11 -3
  150. package/dist/commands/sources/schema-repair.js +44 -46
  151. package/dist/commands/sources/self-update.js +2 -2
  152. package/dist/commands/sources/source-add.js +7 -3
  153. package/dist/commands/sources/sources-cli.js +3 -3
  154. package/dist/commands/sources/stash-cli.js +19 -39
  155. package/dist/commands/sources/stash-skeleton.js +57 -8
  156. package/dist/commands/tasks/default-tasks.js +15 -2
  157. package/dist/commands/tasks/tasks-cli.js +20 -29
  158. package/dist/commands/tasks/tasks.js +39 -11
  159. package/dist/commands/wiki-cli.js +23 -38
  160. package/dist/commands/workflow-cli.js +15 -1
  161. package/dist/core/asset/asset-registry.js +3 -1
  162. package/dist/core/asset/asset-spec.js +21 -4
  163. package/dist/core/asset/frontmatter.js +188 -167
  164. package/dist/core/asset/markdown.js +8 -0
  165. package/dist/core/authoring-rules.js +92 -0
  166. package/dist/core/common.js +4 -23
  167. package/dist/core/concurrent.js +10 -1
  168. package/dist/core/config/config-io.js +10 -1
  169. package/dist/core/config/config-migration.js +18 -40
  170. package/dist/core/config/config-schema.js +382 -62
  171. package/dist/core/config/config-types.js +3 -3
  172. package/dist/core/config/config.js +67 -22
  173. package/dist/core/deep-merge.js +38 -0
  174. package/dist/core/errors.js +1 -0
  175. package/dist/core/eval/rank-metrics.js +113 -0
  176. package/dist/core/events.js +4 -7
  177. package/dist/core/improve-types.js +47 -8
  178. package/dist/core/logs-db.js +14 -75
  179. package/dist/core/parse.js +36 -16
  180. package/dist/core/paths.js +18 -18
  181. package/dist/core/standards/resolve-standards-context.js +87 -0
  182. package/dist/core/standards/resolve-stash-standards.js +99 -0
  183. package/dist/core/standards/resolve-type-conventions.js +66 -0
  184. package/dist/core/state/migrations.js +770 -0
  185. package/dist/core/state-db.js +132 -1126
  186. package/dist/core/structured.js +69 -0
  187. package/dist/core/time.js +53 -0
  188. package/dist/core/warn.js +21 -0
  189. package/dist/core/write-source.js +37 -0
  190. package/dist/indexer/db/db.js +259 -769
  191. package/dist/indexer/db/entry-mapper.js +41 -0
  192. package/dist/indexer/db/graph-db.js +129 -86
  193. package/dist/indexer/db/llm-cache.js +2 -2
  194. package/dist/indexer/db/schema.js +516 -0
  195. package/dist/indexer/ensure-index.js +36 -92
  196. package/dist/indexer/feedback/utility-policy.js +75 -0
  197. package/dist/indexer/graph/graph-boost.js +51 -41
  198. package/dist/indexer/graph/graph-extraction.js +207 -4
  199. package/dist/indexer/index-writer-lock.js +18 -11
  200. package/dist/indexer/index-written-assets.js +105 -0
  201. package/dist/indexer/indexer.js +182 -204
  202. package/dist/indexer/passes/dir-staleness.js +114 -0
  203. package/dist/indexer/passes/memory-inference.js +13 -5
  204. package/dist/indexer/passes/metadata.js +20 -0
  205. package/dist/indexer/read-preflight.js +23 -0
  206. package/dist/indexer/search/db-search.js +89 -13
  207. package/dist/indexer/search/fts-query.js +51 -0
  208. package/dist/indexer/search/ranking-contributors.js +95 -9
  209. package/dist/indexer/search/ranking.js +79 -3
  210. package/dist/indexer/search/search-fields.js +6 -0
  211. package/dist/indexer/search/search-source.js +32 -21
  212. package/dist/indexer/search/semantic-status.js +4 -0
  213. package/dist/indexer/walk/matchers.js +9 -0
  214. package/dist/indexer/walk/walker.js +21 -13
  215. package/dist/integrations/agent/builders.js +39 -13
  216. package/dist/integrations/agent/config.js +20 -59
  217. package/dist/integrations/agent/detect.js +9 -0
  218. package/dist/integrations/agent/index.js +3 -19
  219. package/dist/integrations/agent/model-aliases.js +7 -2
  220. package/dist/integrations/agent/profiles.js +7 -1
  221. package/dist/integrations/agent/prompts.js +75 -9
  222. package/dist/integrations/agent/runner-dispatch.js +59 -0
  223. package/dist/integrations/agent/runner.js +13 -9
  224. package/dist/integrations/agent/spawn.js +69 -67
  225. package/dist/integrations/harnesses/claude/agent-builder.js +1 -1
  226. package/dist/integrations/harnesses/claude/index.js +2 -0
  227. package/dist/integrations/harnesses/claude/session-log.js +10 -0
  228. package/dist/integrations/harnesses/index.js +2 -3
  229. package/dist/integrations/harnesses/opencode/agent-builder.js +1 -1
  230. package/dist/integrations/harnesses/opencode/index.js +2 -0
  231. package/dist/integrations/harnesses/opencode/session-log.js +173 -3
  232. package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
  233. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +98 -17
  234. package/dist/integrations/harnesses/types.js +1 -0
  235. package/dist/integrations/session-logs/index.js +16 -0
  236. package/dist/llm/call-ai.js +2 -2
  237. package/dist/llm/client.js +34 -11
  238. package/dist/llm/embedder.js +67 -4
  239. package/dist/llm/embedders/cache.js +3 -1
  240. package/dist/llm/embedders/deterministic.js +66 -0
  241. package/dist/llm/embedders/local.js +73 -3
  242. package/dist/llm/feature-gate.js +16 -15
  243. package/dist/llm/graph-extract.js +67 -44
  244. package/dist/llm/memory-infer-impl.js +138 -0
  245. package/dist/llm/memory-infer.js +1 -127
  246. package/dist/llm/metadata-enhance.js +44 -31
  247. package/dist/llm/structured-call.js +49 -0
  248. package/dist/migrate-storage-node.mjs +8 -0
  249. package/dist/output/context.js +5 -5
  250. package/dist/output/renderers.js +85 -14
  251. package/dist/output/shapes/curate.js +14 -2
  252. package/dist/output/shapes/helpers.js +0 -3
  253. package/dist/output/shapes/passthrough.js +2 -1
  254. package/dist/output/text/helpers.js +29 -1
  255. package/dist/output/text/workflow.js +1 -0
  256. package/dist/registry/providers/skills-sh.js +21 -147
  257. package/dist/registry/providers/static-index.js +15 -157
  258. package/dist/registry/resolve.js +27 -9
  259. package/dist/runtime.js +25 -1
  260. package/dist/scripts/migrate-storage.js +2661 -2369
  261. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +883 -596
  262. package/dist/setup/detect.js +9 -0
  263. package/dist/setup/legacy-config.js +106 -0
  264. package/dist/setup/prompt.js +57 -0
  265. package/dist/setup/providers.js +14 -0
  266. package/dist/setup/registry-stash-loader.js +12 -0
  267. package/dist/setup/semantic-assets.js +124 -0
  268. package/dist/setup/setup.js +52 -1614
  269. package/dist/setup/steps/connection.js +734 -0
  270. package/dist/setup/steps/output.js +31 -0
  271. package/dist/setup/steps/platforms.js +124 -0
  272. package/dist/setup/steps/semantic.js +27 -0
  273. package/dist/setup/steps/sources.js +222 -0
  274. package/dist/setup/steps/stashdir.js +42 -0
  275. package/dist/setup/steps/tasks.js +152 -0
  276. package/dist/sources/include.js +6 -2
  277. package/dist/sources/providers/filesystem.js +0 -1
  278. package/dist/sources/providers/git-install.js +210 -0
  279. package/dist/sources/providers/git-provider.js +234 -0
  280. package/dist/sources/providers/git-stash.js +248 -0
  281. package/dist/sources/providers/git.js +10 -661
  282. package/dist/sources/providers/npm.js +2 -6
  283. package/dist/sources/providers/provider-utils.js +13 -7
  284. package/dist/sources/providers/sync-from-ref.js +9 -1
  285. package/dist/sources/providers/website.js +9 -5
  286. package/dist/sources/website-ingest.js +187 -29
  287. package/dist/sources/wiki-fetchers/registry.js +53 -0
  288. package/dist/sources/wiki-fetchers/youtube.js +239 -0
  289. package/dist/storage/database.js +45 -10
  290. package/dist/storage/managed-db.js +82 -0
  291. package/dist/storage/repositories/canaries-repository.js +107 -0
  292. package/dist/storage/repositories/consolidation-repository.js +38 -0
  293. package/dist/storage/repositories/embeddings-repository.js +72 -0
  294. package/dist/storage/repositories/events-repository.js +187 -0
  295. package/dist/storage/repositories/extract-sessions-repository.js +96 -0
  296. package/dist/storage/repositories/improve-runs-repository.js +146 -0
  297. package/dist/storage/repositories/index-db.js +14 -8
  298. package/dist/storage/repositories/proposals-repository.js +220 -0
  299. package/dist/storage/repositories/recombine-repository.js +213 -0
  300. package/dist/storage/repositories/registry-cache.js +93 -0
  301. package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
  302. package/dist/storage/repositories/task-history-repository.js +93 -0
  303. package/dist/storage/sqlite-pragmas.js +146 -0
  304. package/dist/tasks/backends/cron.js +1 -1
  305. package/dist/tasks/backends/index.js +9 -0
  306. package/dist/tasks/backends/launchd.js +1 -1
  307. package/dist/tasks/backends/schtasks.js +1 -1
  308. package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
  309. package/dist/tasks/runner.js +15 -13
  310. package/dist/text-import-hook.mjs +0 -0
  311. package/dist/wiki/wiki.js +52 -11
  312. package/dist/workflows/cli.js +1 -0
  313. package/dist/workflows/db.js +3 -4
  314. package/dist/workflows/runtime/runs.js +43 -118
  315. package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
  316. package/dist/workflows/validate-summary.js +2 -7
  317. package/docs/README.md +69 -18
  318. package/docs/data-and-telemetry.md +5 -4
  319. package/docs/migration/release-notes/0.7.0.md +1 -1
  320. package/docs/migration/release-notes/0.9.0.md +39 -0
  321. package/package.json +10 -10
  322. package/dist/assets/tasks/core/update-stashes.yml +0 -4
  323. package/dist/commands/db-cli.js +0 -23
  324. package/dist/indexer/db/db-backup.js +0 -376
  325. package/dist/indexer/passes/staleness-detect.js +0 -488
@@ -40,13 +40,15 @@ import path from "node:path";
40
40
  import { TYPE_DIRS } from "../../core/asset/asset-spec.js";
41
41
  import { parseFrontmatter } from "../../core/asset/frontmatter.js";
42
42
  import { concurrentMap } from "../../core/concurrent.js";
43
- import { getIndexPassConfig, resolveBatchSize } from "../../core/config/config.js";
43
+ import { getIndexPassConfig, loadConfig, resolveBatchSize } from "../../core/config/config.js";
44
+ import { rethrowIfTestIsolationError } from "../../core/errors.js";
44
45
  import { warn, warnVerbose } from "../../core/warn.js";
45
46
  import { isProcessEnabled } from "../../llm/feature-gate.js";
46
47
  import * as graphExtract from "../../llm/graph-extract.js";
47
48
  import { resolveIndexPassLLM } from "../../llm/index-passes.js";
48
- import { computeBodyHash, GRAPH_SCHEMA_VERSION, getLlmCacheEntriesByRefs, getLlmCacheEntry, upsertLlmCacheEntry, } from "../db/db.js";
49
- import { loadStoredGraphSnapshot, replaceStoredGraph } from "../db/graph-db.js";
49
+ import { computeBodyHash, getLlmCacheEntriesByRefs, getLlmCacheEntry, upsertLlmCacheEntry, } from "../db/db.js";
50
+ import { drainExtractionQueue, loadStoredGraphSnapshot, replaceStoredGraph } from "../db/graph-db.js";
51
+ import { GRAPH_SCHEMA_VERSION } from "../db/schema.js";
50
52
  import { walkMarkdownFiles } from "../walk/walker.js";
51
53
  import { deduplicateGraph } from "./graph-dedup.js";
52
54
  /** Schema version for the persisted artifact — bumps trigger a full rebuild. */
@@ -92,6 +94,12 @@ function computeGraphQualityTelemetry(consideredFiles, extractedFiles, entityCou
92
94
  };
93
95
  }
94
96
  export const DEFAULT_GRAPH_EXTRACTION_INCLUDE_TYPES = ["memory", "knowledge"];
97
+ /**
98
+ * Max number of lazy-extraction queue rows drained per pass (#624-P3). Bounds
99
+ * per-run work so a large backlog is spread across runs rather than processed
100
+ * all at once. Generous default — the queue is normally near-empty.
101
+ */
102
+ const GRAPH_EXTRACTION_QUEUE_DRAIN_LIMIT = 100;
95
103
  const SUPPORTED_GRAPH_EXTRACTION_INCLUDE_TYPES = new Set([
96
104
  "memory",
97
105
  "knowledge",
@@ -291,8 +299,29 @@ export async function runGraphExtractionPass(ctx) {
291
299
  warnVerbose("graph extraction: skipped because no primary stash source is available.");
292
300
  return { ...EMPTY_RESULT };
293
301
  }
302
+ // #624-P3: drain the lazy-extraction queue BEFORE the ranked sweep, highest
303
+ // priority first. Queued paths are extracted individually (per-file merge,
304
+ // other files untouched) so they are processed even when they fall outside
305
+ // the normal candidate set. Default (empty queue) is a byte-identical no-op:
306
+ // drainExtractionQueue returns [] and the loop body never runs.
307
+ if (db) {
308
+ const drained = drainExtractionQueue(db, primary.path, GRAPH_EXTRACTION_QUEUE_DRAIN_LIMIT);
309
+ for (const queued of drained) {
310
+ if (signal?.aborted)
311
+ break;
312
+ await extractGraphForSingleFile(db, primary.path, queued.filePath, queued.bodyHash, { config, signal });
313
+ }
314
+ }
294
315
  const includeTypes = getGraphExtractionIncludeTypes(config);
295
- const eligible = collectEligibleFiles(primary.path, includeTypes).filter((candidate) => !options.candidatePaths || options.candidatePaths.has(candidate.absPath));
316
+ let eligible = collectEligibleFiles(primary.path, includeTypes).filter((candidate) => !options.candidatePaths || options.candidatePaths.has(candidate.absPath));
317
+ // P2 (#624): when topN is set and a DB is available, rank the (already
318
+ // candidate-filtered) eligible set by utility_scores DESC and keep only the
319
+ // top-N. Default (topN unset) is byte-identical to today — no ranking query
320
+ // is issued and the eligible set is untouched. Ranking composes WITH the
321
+ // candidatePaths filter: scoped-then-ranked-then-sliced.
322
+ if (db && options.topN != null && options.topN >= 0) {
323
+ eligible = rankCandidatesByUtility(db, eligible, primary.path).slice(0, options.topN);
324
+ }
296
325
  const considered = eligible.length;
297
326
  if (considered === 0) {
298
327
  const scoped = options.candidatePaths ? ` matching ${options.candidatePaths.size} candidate path(s)` : "";
@@ -344,6 +373,7 @@ export async function runGraphExtractionPass(ctx) {
344
373
  failureCount: 0,
345
374
  htmlErrorCount: 0,
346
375
  retryAttempts: 0,
376
+ nonArrayBatchFailures: 0,
347
377
  };
348
378
  const canReusePreviousGraph = previousGraph.telemetry?.extractorId === extractorId;
349
379
  const runtimeTelemetry = {
@@ -597,6 +627,7 @@ export async function runGraphExtractionPass(ctx) {
597
627
  telemetry.failureCount = runtimeTelemetry.failureCount ?? 0;
598
628
  telemetry.htmlErrorCount = runtimeTelemetry.htmlErrorCount ?? 0;
599
629
  telemetry.retryAttempts = runtimeTelemetry.retryAttempts ?? 0;
630
+ telemetry.nonArrayBatchFailures = runtimeTelemetry.nonArrayBatchFailures ?? 0;
600
631
  const qualityConsidered = mergedNodes.length;
601
632
  const qualityExtracted = mergedNodes.filter((node) => node.status === "extracted" && node.entities.length > 0).length;
602
633
  const quality = computeGraphQualityTelemetry(qualityConsidered, qualityExtracted, deduped.entities.length, deduped.relations.length);
@@ -628,6 +659,178 @@ export async function runGraphExtractionPass(ctx) {
628
659
  warnings,
629
660
  };
630
661
  }
662
+ /**
663
+ * Infer the asset type (`memory`, `knowledge`, …) for a path from the stash
664
+ * directory segment it lives under. Returns the matching include-type, or
665
+ * `undefined` when the path is not under a known graph-eligible type dir.
666
+ */
667
+ function inferGraphTypeForPath(stashRoot, absPath) {
668
+ const rel = path.relative(stashRoot, absPath);
669
+ const firstSeg = rel.split(path.sep)[0];
670
+ if (!firstSeg)
671
+ return undefined;
672
+ for (const type of SUPPORTED_GRAPH_EXTRACTION_INCLUDE_TYPES) {
673
+ if (TYPE_DIRS[type] === firstSeg)
674
+ return type;
675
+ }
676
+ return undefined;
677
+ }
678
+ /**
679
+ * #624-P3 — extract graph data for a SINGLE file and merge it into the stored
680
+ * graph WITHOUT clobbering other files' rows.
681
+ *
682
+ * Re-reads the body from disk at call time (the queued body_hash is NOT trusted
683
+ * blindly — the file may have been deleted or changed since enqueue) and skips
684
+ * silently (returns `false`) when the file is gone or empty. Resolves the LLM
685
+ * via {@link resolveIndexPassLLM} (model-available guard: returns `false` when
686
+ * no provider is configured) UNLESS `opts.llmOverride` is supplied, in which
687
+ * case the override is the extractor seam (used by tests and by callers that
688
+ * already hold a resolved model).
689
+ *
690
+ * Returns `true` when a graph row was written for the file, `false` on any
691
+ * skip (missing file, empty body, unknown type, no model, or extraction error).
692
+ */
693
+ export async function extractGraphForSingleFile(db, stashRoot, filePath, bodyHash, opts) {
694
+ try {
695
+ // Re-read from disk — never trust a stale queued body.
696
+ let raw;
697
+ try {
698
+ raw = fs.readFileSync(filePath, "utf8");
699
+ }
700
+ catch {
701
+ return false; // file gone / unreadable → silent skip
702
+ }
703
+ const parsed = parseFrontmatter(raw);
704
+ const body = parsed.content.trim();
705
+ if (!body)
706
+ return false;
707
+ const type = inferGraphTypeForPath(stashRoot, filePath) ?? "memory";
708
+ const effectiveHash = bodyHash ?? computeBodyHash(body);
709
+ // Extract — via the injected seam, or the real per-asset path.
710
+ let extraction;
711
+ if (opts?.llmOverride) {
712
+ const out = await opts.llmOverride(body);
713
+ extraction = {
714
+ entities: out.entities,
715
+ relations: out.relations,
716
+ ...(out.confidence !== undefined ? { confidence: out.confidence } : {}),
717
+ };
718
+ }
719
+ else {
720
+ const config = opts?.config ?? loadConfig();
721
+ if (!isProcessEnabled("index", "graph_extraction", config))
722
+ return false;
723
+ const llmConfig = resolveIndexPassLLM("graph", config);
724
+ if (!llmConfig)
725
+ return false; // model-available guard
726
+ const result = await graphExtract.extractGraphFromBody(llmConfig, body, opts?.signal, config);
727
+ extraction = {
728
+ entities: result.entities,
729
+ relations: result.relations,
730
+ ...(result.confidence !== undefined ? { confidence: result.confidence } : {}),
731
+ };
732
+ }
733
+ const entities = [...new Set(extraction.entities.map((e) => e.trim()).filter(Boolean))];
734
+ const relations = extraction.relations
735
+ .map((r) => ({
736
+ from: r.from.trim(),
737
+ to: r.to.trim(),
738
+ ...(r.type ? { type: r.type.trim() } : {}),
739
+ ...(normalizeConfidence(r.confidence) !== undefined ? { confidence: normalizeConfidence(r.confidence) } : {}),
740
+ }))
741
+ .filter((r) => r.from && r.to);
742
+ const node = {
743
+ path: filePath,
744
+ type,
745
+ bodyHash: effectiveHash,
746
+ entities,
747
+ relations,
748
+ ...(normalizeConfidence(extraction.confidence) !== undefined
749
+ ? { confidence: normalizeConfidence(extraction.confidence) }
750
+ : {}),
751
+ status: entities.length > 0 ? "extracted" : "empty",
752
+ reason: entities.length > 0 ? "none" : "no_graph_content",
753
+ extractionRunId: crypto.randomUUID(),
754
+ };
755
+ // Merge with the previously-stored nodes, scoping the refresh to JUST this
756
+ // path so other files' rows are preserved (and graph_meta counts refresh).
757
+ const previousGraph = loadGraphFile(stashRoot, db);
758
+ const candidatePaths = new Set([filePath]);
759
+ const mergedNodes = mergeGraphNodes(previousGraph.files, [node], candidatePaths);
760
+ const assetRefs = mergedNodes.map((n) => n.path);
761
+ const deduped = deduplicateGraph(mergedNodes.map((n) => ({ entities: n.entities, relations: n.relations })), assetRefs);
762
+ const qualityExtracted = mergedNodes.filter((n) => n.status === "extracted" && n.entities.length > 0).length;
763
+ const quality = computeGraphQualityTelemetry(mergedNodes.length, qualityExtracted, deduped.entities.length, deduped.relations.length);
764
+ const graph = {
765
+ schemaVersion: GRAPH_FILE_SCHEMA_VERSION,
766
+ generatedAt: new Date().toISOString(),
767
+ stashRoot,
768
+ files: mergedNodes,
769
+ entities: deduped.entities,
770
+ relations: deduped.relations,
771
+ quality,
772
+ ...(previousGraph.telemetry ? { telemetry: previousGraph.telemetry } : {}),
773
+ };
774
+ return writeGraphFile(stashRoot, graph, db);
775
+ }
776
+ catch (err) {
777
+ rethrowIfTestIsolationError(err);
778
+ return false;
779
+ }
780
+ }
781
+ // ── Eligible-file detection ─────────────────────────────────────────────────
782
+ /**
783
+ * Rank eligible graph-extraction candidates by their entry `utility_scores`,
784
+ * highest first, for the incremental high-signal-first sweep (P2 of #624).
785
+ *
786
+ * The join is READ-ONLY (`entries.file_path = candidate.absPath`, then
787
+ * `entries.id -> utility_scores.entry_id`) and does NOT re-couple the graph
788
+ * rows to `entries`. It reads the GLOBAL `utility_scores` table (not the
789
+ * per-scope `utility_scores_scoped`), so ranking is corpus-wide; `stashRoot`
790
+ * is accepted for call-site symmetry/future scoping but is not used to filter
791
+ * (the global table has no `stash_root` column).
792
+ *
793
+ * Candidates with no matching `entries` row, or an entry with no
794
+ * `utility_scores` row, get an effective utility of 0 (LEFT JOIN + COALESCE)
795
+ * and sort LAST — they are deprioritized, never dropped, so a `topN >= total`
796
+ * slice still includes them and they remain reachable on later runs.
797
+ *
798
+ * Ties (equal utility) break by `file_path` ASC for deterministic output.
799
+ * Returns a NEW array; the input is not mutated. SQLite's ~999 bound-parameter
800
+ * cap is respected by chunking the `IN (...)` lookup at 500.
801
+ *
802
+ * Exported for direct unit testing.
803
+ */
804
+ export function rankCandidatesByUtility(db, candidates, _stashRoot) {
805
+ // Cannot rank without a DB → return the input unranked rather than throw.
806
+ // Keeps the DB-less code path (reuse-from-memory) working when topN is set.
807
+ if (!db || candidates.length === 0)
808
+ return candidates;
809
+ const utilityByPath = new Map();
810
+ const CHUNK = 500;
811
+ for (let start = 0; start < candidates.length; start += CHUNK) {
812
+ const chunk = candidates.slice(start, start + CHUNK);
813
+ const paths = chunk.map((c) => c.absPath);
814
+ const placeholders = paths.map(() => "?").join(", ");
815
+ const rows = db
816
+ .prepare(`SELECT e.file_path AS file_path, COALESCE(MAX(u.utility), 0) AS utility
817
+ FROM entries e
818
+ LEFT JOIN utility_scores u ON u.entry_id = e.id
819
+ WHERE e.file_path IN (${placeholders})
820
+ GROUP BY e.file_path`)
821
+ .all(...paths);
822
+ for (const row of rows) {
823
+ utilityByPath.set(row.file_path, row.utility ?? 0);
824
+ }
825
+ }
826
+ return [...candidates].sort((a, b) => {
827
+ const ua = utilityByPath.get(a.absPath) ?? 0;
828
+ const ub = utilityByPath.get(b.absPath) ?? 0;
829
+ if (ub !== ua)
830
+ return ub - ua; // utility DESC
831
+ return a.absPath < b.absPath ? -1 : a.absPath > b.absPath ? 1 : 0; // tie-break: path ASC
832
+ });
833
+ }
631
834
  /**
632
835
  * Scan the primary stash for `memory:` and `knowledge:` markdown files
633
836
  * suitable for graph extraction. The directory layout convention is the
@@ -7,6 +7,7 @@ import { probeLock, releaseLock, releaseLockIfOwned, tryAcquireLockSync } from "
7
7
  import { getDbPath, getIndexWriterLockPath } from "../core/paths.js";
8
8
  const INDEX_WRITER_LOCK_STALE_AFTER_MS = 12 * 60 * 60 * 1000;
9
9
  const INDEX_WRITER_WAIT_MS = 100;
10
+ const DEFAULT_INDEX_WRITER_MAX_WAIT_MS = 10 * 60 * 1000;
10
11
  const heldLocks = new Map();
11
12
  function buildPayload(purpose, pid = process.pid) {
12
13
  return JSON.stringify({
@@ -46,27 +47,26 @@ function retainHeldLock(lockPath) {
46
47
  heldLocks.set(lockPath, { depth: 1, exitHandler });
47
48
  return { lockPath, release: () => releaseHeldLock(lockPath) };
48
49
  }
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
50
  export async function acquireIndexWriterLease(options) {
57
51
  const mode = options.mode ?? "wait";
58
52
  const lockPath = getIndexWriterLockPath();
53
+ const startedAt = Date.now();
54
+ const maxWaitMs = options.maxWaitMs ?? DEFAULT_INDEX_WRITER_MAX_WAIT_MS;
59
55
  fs.mkdirSync(path.dirname(lockPath), { recursive: true });
60
56
  if (heldLocks.has(lockPath)) {
57
+ options.onAcquired?.({ waitedMs: 0 });
61
58
  return retainHeldLock(lockPath);
62
59
  }
60
+ let lastWaitNoticeMs = 0;
63
61
  while (true) {
64
62
  throwIfAborted(options.signal);
65
63
  if (tryAcquireLockSync(lockPath, buildPayload(options.purpose))) {
64
+ options.onAcquired?.({ waitedMs: Date.now() - startedAt });
66
65
  return retainHeldLock(lockPath);
67
66
  }
68
67
  const probe = probeLock(lockPath, { staleAfterMs: INDEX_WRITER_LOCK_STALE_AFTER_MS });
69
68
  if (probe.state === "held" && probe.holderPid === process.pid) {
69
+ options.onAcquired?.({ waitedMs: Date.now() - startedAt });
70
70
  return retainHeldLock(lockPath);
71
71
  }
72
72
  if (probe.state === "stale") {
@@ -75,6 +75,17 @@ export async function acquireIndexWriterLease(options) {
75
75
  }
76
76
  if (mode === "try")
77
77
  return undefined;
78
+ // Held by another live process. Time out only *after* a real acquisition
79
+ // attempt, so a caller with maxWaitMs:0 still gets one chance at a free lock
80
+ // instead of throwing before it ever tries.
81
+ if (maxWaitMs >= 0 && Date.now() - startedAt >= maxWaitMs) {
82
+ throw new Error(`timed out waiting for index writer lease for ${options.purpose}`);
83
+ }
84
+ const waitedMs = Date.now() - startedAt;
85
+ if (waitedMs - lastWaitNoticeMs >= 15000) {
86
+ options.onWait?.({ waitedMs });
87
+ lastWaitNoticeMs = waitedMs;
88
+ }
78
89
  await delay(INDEX_WRITER_WAIT_MS);
79
90
  }
80
91
  }
@@ -90,10 +101,6 @@ export async function withIndexWriterLease(options, run) {
90
101
  lease.release();
91
102
  }
92
103
  }
93
- export function handoffIndexWriterLeaseToPid(lease, pid, purpose) {
94
- fs.writeFileSync(lease.lockPath, buildPayload(purpose, pid), "utf8");
95
- detachHeldLock(lease.lockPath);
96
- }
97
104
  export function probeIndexWriterLease() {
98
105
  return probeLock(getIndexWriterLockPath(), { staleAfterMs: INDEX_WRITER_LOCK_STALE_AFTER_MS });
99
106
  }
@@ -0,0 +1,105 @@
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
+ /**
5
+ * Write-path indexing: targeted single-file index updates for asset writers.
6
+ *
7
+ * The index is maintained eagerly by every first-class mutation command
8
+ * (`source add`, `wiki`, `workflow`, `setup` all run `akmIndex()` after
9
+ * writing). The memory write paths — `akm remember` / `writeMarkdownAsset`
10
+ * and extract's session assets — historically did not, which is why reads
11
+ * used to compensate with stale-triggered background reindexes (the
12
+ * lock-contention footgun removed alongside this module's introduction; see
13
+ * docs/design/read-path-reindex-contention-findings.md §7).
14
+ *
15
+ * This is NOT a general reindex. It upserts exactly the files the caller just
16
+ * wrote: frontmatter/metadata via the shared matcher pipeline, the `entries`
17
+ * row, and an incremental FTS refresh. Embeddings, index-time LLM passes,
18
+ * graph extraction, `builtAt`, and the per-dir walk cache are all deliberately
19
+ * untouched — the next full run heals them (the opportunistic-recovery
20
+ * strategy of docs/technical/index-consistency-adr.md).
21
+ */
22
+ import fs from "node:fs";
23
+ import path from "node:path";
24
+ import { getDbPath } from "../core/paths.js";
25
+ import { warnVerbose } from "../core/warn.js";
26
+ import { closeDatabase, getEntryCount, openExistingDatabase, rebuildFts, upsertEntry } from "./db/db.js";
27
+ import { generateMetadataFlat } from "./passes/metadata.js";
28
+ import { buildSearchText } from "./search/search-fields.js";
29
+ /**
30
+ * Busy-timeout (ms) for write-path index upserts. A real write — unlike the
31
+ * 250ms telemetry inserts — but it must not hang `akm remember` for the full
32
+ * default 30s behind a running full reindex. When it times out, the upsert is
33
+ * skipped and the asset becomes searchable after that reindex instead.
34
+ */
35
+ export const WRITE_PATH_INDEX_BUSY_TIMEOUT_MS = 5_000;
36
+ /**
37
+ * Index the given just-written asset files into the existing local index.
38
+ *
39
+ * FAIL-OPEN at every step: any error (index.db absent, empty, locked past the
40
+ * busy timeout, unparseable file) is reduced to a verbose-only warning and the
41
+ * write command succeeds untouched. The degraded outcome is exactly the
42
+ * pre-write-path-indexing behavior: the asset appears after the next full
43
+ * `akm index` / improve-cron run.
44
+ *
45
+ * An absent or empty index is skipped on purpose — bootstrap belongs to the
46
+ * first read (`ensureIndex`) or an explicit `akm index`, which also cover
47
+ * embeddings and the other passes this fast path skips.
48
+ */
49
+ export async function indexWrittenAssets(stashDir, filePaths) {
50
+ try {
51
+ const dbPath = getDbPath();
52
+ if (!fs.existsSync(dbPath))
53
+ return;
54
+ // The full walk never descends into dot-directories (they hold state like
55
+ // `.meta/`, `.stash.json`), and `shouldIndexStashFile` relies on the walker
56
+ // for that — mirror it here so this fast path indexes exactly what a full
57
+ // run would.
58
+ const files = filePaths.filter((f) => {
59
+ if (!fs.existsSync(f))
60
+ return false;
61
+ const rel = path.relative(stashDir, f);
62
+ return !rel.split(/[\\/]+/).some((segment) => segment.startsWith("."));
63
+ });
64
+ if (files.length === 0)
65
+ return;
66
+ // Generate metadata BEFORE opening the DB so the write window stays
67
+ // short. One call per file keeps the entry↔path pairing exact.
68
+ const pairs = [];
69
+ for (const file of files) {
70
+ const generated = await generateMetadataFlat(stashDir, [file]);
71
+ const entry = generated.entries[0];
72
+ // Workflows carry a side-table document upsert this fast path doesn't
73
+ // do; no current caller writes them, but guard so one never lands
74
+ // half-indexed.
75
+ if (entry && entry.type !== "workflow")
76
+ pairs.push({ file, entry });
77
+ }
78
+ if (pairs.length === 0)
79
+ return;
80
+ const db = openExistingDatabase(dbPath);
81
+ try {
82
+ db.exec(`PRAGMA busy_timeout = ${WRITE_PATH_INDEX_BUSY_TIMEOUT_MS}`);
83
+ if (getEntryCount(db) === 0)
84
+ return;
85
+ for (const { file, entry } of pairs) {
86
+ const entryKey = `${stashDir}:${entry.type}:${entry.name}`;
87
+ let entryWithSize = entry;
88
+ try {
89
+ entryWithSize = { ...entry, fileSize: fs.statSync(file).size };
90
+ }
91
+ catch {
92
+ // stat raced a delete — index without the size, like the full walk does.
93
+ }
94
+ upsertEntry(db, entryKey, path.dirname(file), file, stashDir, entryWithSize, buildSearchText(entry));
95
+ }
96
+ rebuildFts(db, { incremental: true });
97
+ }
98
+ finally {
99
+ closeDatabase(db);
100
+ }
101
+ }
102
+ catch (error) {
103
+ warnVerbose("Write-path index update skipped (asset appears after the next full index):", error instanceof Error ? error.message : String(error));
104
+ }
105
+ }