akm-cli 0.9.0-rc.8 → 0.9.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 (450) hide show
  1. package/CHANGELOG.md +1063 -44
  2. package/README.md +51 -25
  3. package/SECURITY.md +14 -1
  4. package/STABILITY.md +497 -0
  5. package/dist/akm +148 -35
  6. package/dist/{akm-migrate-storage → akm-migrate} +6 -9
  7. package/dist/assets/hints/cli-hints-full.md +223 -95
  8. package/dist/assets/hints/cli-hints-short.md +85 -22
  9. package/dist/assets/improve-strategies/default.json +1 -1
  10. package/dist/assets/improve-strategies/reflect-distill.json +1 -1
  11. package/dist/assets/prompts/memory-infer-user.md +2 -3
  12. package/dist/assets/stash-skeleton/README.md +6 -5
  13. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +2 -0
  14. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +2 -0
  15. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +2 -0
  16. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +2 -0
  17. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +2 -0
  18. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +2 -0
  19. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +2 -0
  20. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +2 -0
  21. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +2 -0
  22. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +2 -0
  23. package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -0
  24. package/dist/assets/stash-skeleton/facts/conventions/organization.md +20 -9
  25. package/dist/assets/tasks/core/extract.yml +1 -1
  26. package/dist/assets/tasks/core/version-check.yml +1 -1
  27. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +5 -0
  28. package/dist/assets/tasks/improve/akm-improve-catchup.yml +8 -0
  29. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +5 -0
  30. package/dist/assets/tasks/improve/akm-improve-frequent.yml +5 -0
  31. package/dist/assets/tasks/improve/akm-improve-nightly.yml +5 -0
  32. package/dist/assets/templates/html/health.html +1 -3
  33. package/dist/assets/workflows/workflow-template.md +32 -15
  34. package/dist/cli/invocation.js +40 -15
  35. package/dist/cli/parse-args.js +0 -22
  36. package/dist/cli/retired-commands.js +121 -0
  37. package/dist/cli/shared.js +154 -22
  38. package/dist/cli/unknown-flags.js +236 -0
  39. package/dist/cli-node.mjs +2 -1
  40. package/dist/cli.js +696 -258
  41. package/dist/commands/agent/agent-dispatch.js +14 -3
  42. package/dist/commands/agent/contribute-cli.js +73 -88
  43. package/dist/commands/completions.js +79 -22
  44. package/dist/commands/config-cli.js +17 -150
  45. package/dist/commands/env/env-cli.js +59 -143
  46. package/dist/commands/env/env.js +12 -163
  47. package/dist/commands/env/marker-path.js +6 -0
  48. package/dist/commands/env/secret-cli.js +36 -66
  49. package/dist/commands/env/secret.js +24 -57
  50. package/dist/commands/feedback-cli.js +141 -87
  51. package/dist/commands/health/accept-rate.js +58 -0
  52. package/dist/commands/health/advisories.js +3 -4
  53. package/dist/commands/health/checks.js +85 -23
  54. package/dist/commands/health/html-report.js +7 -10
  55. package/dist/commands/health/improve-metrics.js +25 -83
  56. package/dist/commands/health/md-report.js +5 -9
  57. package/dist/commands/health/metrics.js +62 -20
  58. package/dist/commands/health/renderers.js +47 -0
  59. package/dist/commands/health/report-view-model.js +4 -5
  60. package/dist/commands/health/stash-exposure.js +1 -1
  61. package/dist/commands/health/surfaces.js +3 -48
  62. package/dist/commands/health/task-runs.js +3 -67
  63. package/dist/commands/health/types-improve.js +7 -0
  64. package/dist/commands/health.js +99 -28
  65. package/dist/commands/improve/anti-collapse.js +2 -2
  66. package/dist/commands/improve/autonomy-gate.js +68 -0
  67. package/dist/commands/improve/collapse-detector.js +41 -40
  68. package/dist/commands/improve/consolidate/eligibility.js +1 -23
  69. package/dist/commands/improve/consolidate/merge.js +4 -0
  70. package/dist/commands/improve/consolidate.js +140 -1000
  71. package/dist/commands/improve/distill/promote-memory.js +12 -12
  72. package/dist/commands/improve/distill/quality-gate.js +6 -6
  73. package/dist/commands/improve/distill.js +58 -69
  74. package/dist/commands/improve/eligibility.js +105 -57
  75. package/dist/commands/improve/extract-cli.js +14 -133
  76. package/dist/commands/improve/improve-cli.js +98 -114
  77. package/dist/commands/improve/improve-result-file.js +1 -28
  78. package/dist/commands/improve/improve-strategies.js +8 -5
  79. package/dist/commands/improve/improve.js +128 -91
  80. package/dist/commands/improve/loop-stages.js +182 -20
  81. package/dist/commands/improve/memory/derived-ref.js +45 -43
  82. package/dist/commands/improve/memory/memory-belief.js +1 -1
  83. package/dist/commands/improve/memory/memory-contradiction-detect.js +4 -12
  84. package/dist/commands/improve/memory/memory-improve.js +6 -5
  85. package/dist/commands/improve/outcome-loop.js +22 -65
  86. package/dist/commands/improve/preparation.js +114 -123
  87. package/dist/commands/improve/proactive-maintenance.js +2 -5
  88. package/dist/commands/improve/reflect.js +56 -160
  89. package/dist/commands/improve/salience.js +11 -122
  90. package/dist/commands/improve/source-identity.js +10 -38
  91. package/dist/commands/lint/base-linter.js +20 -124
  92. package/dist/commands/lint/env-key-rules.js +31 -47
  93. package/dist/commands/lint/index.js +249 -43
  94. package/dist/commands/{events.js → log.js} +33 -38
  95. package/dist/commands/migrate-cli.js +92 -12
  96. package/dist/commands/migration-tool.js +46 -0
  97. package/dist/commands/observability-cli.js +70 -209
  98. package/dist/commands/proposal/drain.js +101 -29
  99. package/dist/commands/proposal/proposal-cli.js +76 -48
  100. package/dist/commands/proposal/proposal.js +54 -18
  101. package/dist/commands/proposal/propose-cli.js +88 -0
  102. package/dist/commands/proposal/propose.js +23 -15
  103. package/dist/commands/proposal/repository.js +701 -278
  104. package/dist/commands/proposal/validators/proposal-quality-validators.js +2 -8
  105. package/dist/commands/proposal/validators/proposal-validators.js +55 -7
  106. package/dist/commands/proposal/validators/proposals.js +4 -7
  107. package/dist/commands/read/curate.js +34 -53
  108. package/dist/commands/read/knowledge.js +150 -95
  109. package/dist/commands/read/registry-search.js +2 -2
  110. package/dist/commands/read/remember-cli.js +42 -15
  111. package/dist/commands/read/search-cli.js +180 -78
  112. package/dist/commands/read/search.js +58 -43
  113. package/dist/commands/read/show.js +197 -141
  114. package/dist/commands/registry-cli.js +12 -51
  115. package/dist/commands/remember.js +14 -57
  116. package/dist/commands/sources/add-cli.js +100 -31
  117. package/dist/commands/sources/bundle-cli.js +166 -0
  118. package/dist/commands/sources/bundle-config-ops.js +7 -2
  119. package/dist/commands/sources/info.js +18 -5
  120. package/dist/commands/sources/init.js +12 -12
  121. package/dist/commands/sources/installed-stashes.js +382 -98
  122. package/dist/commands/sources/schema-repair.js +3 -2
  123. package/dist/commands/sources/self-update.js +131 -38
  124. package/dist/commands/sources/source-add.js +72 -17
  125. package/dist/commands/sources/source-clone.js +129 -45
  126. package/dist/commands/sources/source-manage.js +43 -23
  127. package/dist/commands/sources/sources-cli.js +57 -208
  128. package/dist/commands/sources/stash-cli.js +46 -53
  129. package/dist/commands/tasks/tasks-cli.js +91 -97
  130. package/dist/commands/tasks/tasks.js +276 -421
  131. package/dist/commands/workflow-cli.js +175 -450
  132. package/dist/core/adapter/adapters/akm-adapter.js +47 -28
  133. package/dist/core/adapter/adapters/akm-lint.js +42 -27
  134. package/dist/core/adapter/adapters/akm-metadata.js +15 -44
  135. package/dist/core/adapter/adapters/akm-task-adapter.js +15 -13
  136. package/dist/core/adapter/adapters/akm-workflow-adapter.js +55 -71
  137. package/dist/core/adapter/adapters/dotenv-adapter.js +1 -1
  138. package/dist/core/adapter/adapters/generic-files-adapter.js +2 -0
  139. package/dist/core/adapter/adapters/index.js +6 -6
  140. package/dist/core/adapter/adapters/llm-wiki-adapter.js +14 -8
  141. package/dist/core/adapter/adapters/okf-adapter.js +187 -19
  142. package/dist/core/adapter/adapters/shared.js +3 -19
  143. package/dist/core/adapter/adapters/tool-dir-shared.js +8 -3
  144. package/dist/core/adapter/adapters/website-snapshot-adapter.js +1 -0
  145. package/dist/core/adapter/detect-adapter.js +17 -0
  146. package/dist/core/adapter/recognize-match.js +6 -4
  147. package/dist/core/adapter/validate-context.js +214 -0
  148. package/dist/core/asset/akm-markdown.js +63 -0
  149. package/dist/core/asset/asset-placement.js +20 -6
  150. package/dist/core/asset/asset-ref.js +11 -9
  151. package/dist/core/asset/frontmatter-lint.js +30 -0
  152. package/dist/core/asset/frontmatter.js +37 -9
  153. package/dist/core/asset/markdown.js +40 -51
  154. package/dist/core/asset/resolve-ref.js +89 -18
  155. package/dist/core/asset/stash-meta.js +1 -1
  156. package/dist/core/bundle-id.js +51 -0
  157. package/dist/core/common.js +152 -38
  158. package/dist/core/config/config-io.js +12 -1
  159. package/dist/core/config/config-schema.js +35 -8
  160. package/dist/core/config/config-sources.js +55 -11
  161. package/dist/core/config/config-walker.js +25 -9
  162. package/dist/core/config/config.js +9 -48
  163. package/dist/core/config/experimental.js +21 -0
  164. package/dist/core/config/schema/embedding.js +5 -1
  165. package/dist/core/config/schema/experimental.js +30 -0
  166. package/dist/core/config/schema/improve-processes.js +0 -6
  167. package/dist/core/config/schema/improve.js +21 -3
  168. package/dist/core/config/schema/index-config.js +8 -15
  169. package/dist/core/config/schema/output.js +4 -1
  170. package/dist/core/config/schema/setup.js +9 -18
  171. package/dist/core/config/schema/sources-bundles.js +49 -33
  172. package/dist/core/config/schema/workflow.js +3 -3
  173. package/dist/core/env-secret-ref.js +76 -46
  174. package/dist/core/errors.js +18 -12
  175. package/dist/core/events.js +46 -128
  176. package/dist/core/file-change.js +6 -5
  177. package/dist/core/fs-txn.js +83 -7
  178. package/dist/core/git-message.js +2 -2
  179. package/dist/core/improve-result.js +1 -100
  180. package/dist/core/lesson-lint.js +1 -17
  181. package/dist/core/logs-db.js +2 -1
  182. package/dist/core/migration-operation.js +16 -0
  183. package/dist/core/mutation-target.js +78 -0
  184. package/dist/core/parse.js +4 -1
  185. package/dist/core/paths.js +17 -20
  186. package/dist/core/recognition-util.js +12 -14
  187. package/dist/core/redaction.js +34 -0
  188. package/dist/core/standards/resolve-standards-context.js +2 -14
  189. package/dist/core/standards/resolve-stash-standards.js +2 -2
  190. package/dist/core/standards/resolve-type-conventions.js +2 -2
  191. package/dist/core/state/migrations.js +41 -18
  192. package/dist/core/state-db.js +5 -14
  193. package/dist/core/structured.js +1 -1
  194. package/dist/core/subprocess.js +6 -4
  195. package/dist/core/text-truncation.js +9 -5
  196. package/dist/core/type-presentation.js +3 -3
  197. package/dist/core/warn.js +0 -3
  198. package/dist/core/write-source.js +771 -95
  199. package/dist/indexer/bundle-identity-guard.js +3 -2
  200. package/dist/indexer/db/graph-db.js +0 -24
  201. package/dist/indexer/ensure-index.js +1 -0
  202. package/dist/indexer/graph/graph-boost.js +9 -34
  203. package/dist/indexer/graph/graph-extraction.js +8 -5
  204. package/dist/indexer/index-writer-lock.js +53 -17
  205. package/dist/indexer/index-written-assets.js +16 -22
  206. package/dist/indexer/indexer.js +497 -239
  207. package/dist/indexer/installations.js +14 -96
  208. package/dist/indexer/passes/dir-staleness.js +16 -9
  209. package/dist/indexer/passes/memory-inference.js +11 -9
  210. package/dist/indexer/passes/metadata.js +113 -47
  211. package/dist/indexer/scan/doc-to-entry.js +38 -1
  212. package/dist/indexer/scan/drain-dir.js +13 -23
  213. package/dist/indexer/search/db-search.js +99 -54
  214. package/dist/indexer/search/fts-query.js +47 -24
  215. package/dist/indexer/search/ranking-contributors.js +42 -20
  216. package/dist/indexer/search/ranking.js +18 -99
  217. package/dist/indexer/search/search-fields.js +7 -2
  218. package/dist/indexer/search/search-source.js +82 -93
  219. package/dist/indexer/usage/usage-events.js +0 -89
  220. package/dist/indexer/walk/file-context.js +2 -1
  221. package/dist/indexer/walk/matchers.js +30 -43
  222. package/dist/indexer/walk/path-resolver.js +7 -2
  223. package/dist/indexer/walk/walker.js +38 -12
  224. package/dist/integrations/agent/builders.js +0 -6
  225. package/dist/integrations/agent/config.js +2 -2
  226. package/dist/integrations/agent/detect.js +49 -19
  227. package/dist/integrations/agent/engine-fallback.js +76 -0
  228. package/dist/integrations/agent/profiles.js +14 -0
  229. package/dist/integrations/agent/prompts.js +12 -8
  230. package/dist/integrations/agent/runner-dispatch.js +4 -2
  231. package/dist/integrations/agent/runner.js +0 -1
  232. package/dist/integrations/agent/spawn.js +5 -6
  233. package/dist/integrations/github.js +1 -1
  234. package/dist/integrations/harnesses/aider/agent-builder.js +6 -4
  235. package/dist/integrations/harnesses/amazonq/agent-builder.js +7 -4
  236. package/dist/integrations/harnesses/claude/session-log.js +0 -10
  237. package/dist/integrations/harnesses/codex/agent-builder.js +5 -2
  238. package/dist/integrations/harnesses/copilot/agent-builder.js +5 -3
  239. package/dist/integrations/harnesses/gemini/agent-builder.js +5 -3
  240. package/dist/integrations/harnesses/index.js +3 -7
  241. package/dist/integrations/harnesses/opencode/agent-builder.js +21 -2
  242. package/dist/integrations/harnesses/opencode/session-log.js +0 -15
  243. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +13 -4
  244. package/dist/integrations/harnesses/openhands/agent-builder.js +9 -6
  245. package/dist/integrations/harnesses/pi/agent-builder.js +6 -4
  246. package/dist/integrations/lockfile.js +101 -6
  247. package/dist/integrations/session-logs/index.js +3 -28
  248. package/dist/llm/client.js +136 -100
  249. package/dist/llm/embedders/remote.js +13 -5
  250. package/dist/llm/feature-gate.js +4 -12
  251. package/dist/llm/graph-extract.js +5 -11
  252. package/dist/llm/memory-infer.js +144 -1
  253. package/dist/llm/metadata-enhance.js +5 -7
  254. package/dist/llm/structured-call.js +1 -1
  255. package/dist/llm/usage-persist.js +26 -5
  256. package/dist/llm/usage-telemetry.js +25 -2
  257. package/dist/output/cli-hints.js +1 -2
  258. package/dist/output/context.js +22 -7
  259. package/dist/output/format-exempt.js +80 -0
  260. package/dist/output/generic-render.js +259 -0
  261. package/dist/output/render-registry.js +57 -0
  262. package/dist/output/renderers.js +14 -36
  263. package/dist/output/shapes/curate.js +10 -1
  264. package/dist/output/shapes/events.js +12 -7
  265. package/dist/output/shapes/helpers.js +56 -83
  266. package/dist/output/shapes/migrate.js +8 -0
  267. package/dist/output/shapes/passthrough.js +7 -41
  268. package/dist/output/shapes/proposal/producer.js +15 -7
  269. package/dist/output/shapes.js +2 -9
  270. package/dist/output/text/{init.js → bundle-create.js} +3 -1
  271. package/dist/output/text/bundle-show.js +7 -0
  272. package/dist/output/text/command-format.js +164 -96
  273. package/dist/output/text/env.js +1 -3
  274. package/dist/output/text/events.js +8 -7
  275. package/dist/output/text/health-format.js +103 -0
  276. package/dist/output/text/health.js +7 -0
  277. package/dist/output/text/helpers.js +10 -8
  278. package/dist/output/text/lint-format.js +43 -0
  279. package/dist/output/text/{save.js → lint.js} +2 -2
  280. package/dist/output/text/migrate.js +88 -0
  281. package/dist/output/text/proposal/producer.js +4 -2
  282. package/dist/output/text/proposal-format.js +44 -72
  283. package/dist/output/text/registry-commands.js +1 -2
  284. package/dist/output/text/show-directives.js +15 -7
  285. package/dist/output/text/status-list.js +32 -0
  286. package/dist/output/text/sync.js +5 -0
  287. package/dist/output/text/workflow-format.js +24 -203
  288. package/dist/output/text/workflow.js +1 -7
  289. package/dist/output/text.js +16 -17
  290. package/dist/registry/factory.js +4 -6
  291. package/dist/registry/origin-resolve.js +16 -27
  292. package/dist/registry/providers/skills-sh.js +3 -3
  293. package/dist/registry/providers/static-index.js +13 -23
  294. package/dist/registry/resolve.js +42 -7
  295. package/dist/registry/semver.js +34 -84
  296. package/dist/runtime.js +2 -23
  297. package/dist/scripts/akm-migrate-node.js +60290 -0
  298. package/dist/scripts/akm-migrate.js +59628 -0
  299. package/dist/setup/detect.js +42 -15
  300. package/dist/setup/registry-stash-loader.js +2 -2
  301. package/dist/setup/setup.js +236 -136
  302. package/dist/setup/steps/connection.js +7 -9
  303. package/dist/setup/steps/platforms.js +9 -9
  304. package/dist/setup/steps/semantic.js +15 -3
  305. package/dist/setup/steps/sources.js +12 -13
  306. package/dist/setup/steps/stashdir.js +2 -3
  307. package/dist/setup/steps/tasks.js +237 -120
  308. package/dist/sources/freshness.js +1 -1
  309. package/dist/sources/provider-factory.js +11 -17
  310. package/dist/sources/providers/filesystem.js +2 -3
  311. package/dist/sources/providers/git-install.js +278 -34
  312. package/dist/sources/providers/git-provider.js +25 -23
  313. package/dist/sources/providers/git-stash.js +395 -106
  314. package/dist/sources/providers/git.js +2 -2
  315. package/dist/sources/providers/npm.js +16 -19
  316. package/dist/sources/providers/provider-utils.js +7 -4
  317. package/dist/sources/providers/sync-from-ref.js +3 -9
  318. package/dist/sources/providers/website.js +6 -1
  319. package/dist/sources/resolve.js +6 -5
  320. package/dist/sources/snapshot-fetchers/bluesky.js +146 -0
  321. package/dist/sources/snapshot-fetchers/content-extract.js +566 -0
  322. package/dist/sources/snapshot-fetchers/fetcher-util.js +41 -0
  323. package/dist/sources/snapshot-fetchers/github.js +100 -0
  324. package/dist/sources/snapshot-fetchers/host-guard.js +291 -0
  325. package/dist/sources/snapshot-fetchers/registry.js +17 -1
  326. package/dist/sources/snapshot-fetchers/robots.js +348 -0
  327. package/dist/sources/snapshot-fetchers/rss.js +282 -0
  328. package/dist/sources/snapshot-fetchers/secret-seam.js +42 -0
  329. package/dist/sources/snapshot-fetchers/website-ingest.js +566 -268
  330. package/dist/sources/snapshot-fetchers/x.js +910 -0
  331. package/dist/storage/database.js +7 -0
  332. package/dist/storage/engines/sqlite-migrations.js +23 -111
  333. package/dist/storage/managed-db.js +2 -2
  334. package/dist/storage/repositories/canaries-repository.js +1 -1
  335. package/dist/storage/repositories/events-repository.js +27 -11
  336. package/dist/storage/repositories/improve-runs-repository.js +6 -12
  337. package/dist/storage/repositories/index-connection.js +17 -6
  338. package/dist/storage/repositories/index-entries-repository.js +151 -240
  339. package/dist/storage/repositories/index-entry-mapper.js +15 -11
  340. package/dist/storage/repositories/index-fts-repository.js +5 -2
  341. package/dist/storage/repositories/index-llm-cache-repository.js +0 -1
  342. package/dist/storage/repositories/index-meta-repository.js +2 -3
  343. package/dist/storage/repositories/index-schema.js +10 -25
  344. package/dist/storage/repositories/index-utility-repository.js +15 -28
  345. package/dist/storage/repositories/index-vec-repository.js +6 -1
  346. package/dist/storage/repositories/outcome-repository.js +119 -0
  347. package/dist/storage/repositories/proposals-repository.js +296 -59
  348. package/dist/storage/repositories/registry-cache.js +19 -0
  349. package/dist/storage/repositories/salience-repository.js +172 -0
  350. package/dist/storage/repositories/task-history-repository.js +15 -13
  351. package/dist/storage/repositories/workflow-runs-repository.js +52 -40
  352. package/dist/tasks/backends/cron.js +105 -15
  353. package/dist/tasks/backends/index.js +1 -1
  354. package/dist/tasks/backends/launchd.js +85 -38
  355. package/dist/tasks/backends/schtasks.js +135 -15
  356. package/dist/tasks/embedded.js +56 -40
  357. package/dist/tasks/parser.js +7 -157
  358. package/dist/tasks/resolve-akm-bin.js +137 -59
  359. package/dist/tasks/runner.js +79 -42
  360. package/dist/tasks/scheduler-invocation.js +220 -10
  361. package/dist/tasks/schema.js +24 -1
  362. package/dist/tasks/task-id.js +1 -3
  363. package/dist/tasks/validator.js +20 -6
  364. package/dist/workflows/authoring/authoring.js +94 -143
  365. package/dist/workflows/authoring/scope-key.js +1 -1
  366. package/dist/workflows/exec/frozen-judge.js +28 -2
  367. package/dist/workflows/exec/native-executor.js +77 -57
  368. package/dist/workflows/exec/param-secrets.js +9 -9
  369. package/dist/workflows/exec/run-workflow.js +133 -79
  370. package/dist/workflows/exec/step-work.js +219 -346
  371. package/dist/{migrate-storage-node.mjs → workflows/exec/unit-dispatch.js} +1 -5
  372. package/dist/workflows/ir/compile.js +141 -270
  373. package/dist/workflows/ir/freeze.js +40 -30
  374. package/dist/workflows/ir/params.js +135 -11
  375. package/dist/workflows/ir/plan-hash.js +1 -1
  376. package/dist/workflows/ir/schema.js +25 -26
  377. package/dist/workflows/parser.js +872 -307
  378. package/dist/workflows/program/expressions.js +20 -208
  379. package/dist/workflows/program/schema.js +7 -10
  380. package/dist/workflows/renderer.js +95 -68
  381. package/dist/workflows/resource-limits.js +2 -0
  382. package/dist/workflows/runtime/checkin.js +3 -3
  383. package/dist/workflows/runtime/plan-classifier.js +16 -75
  384. package/dist/workflows/runtime/runs.js +186 -127
  385. package/dist/workflows/runtime/unit-checkin.js +1 -1
  386. package/dist/workflows/runtime/unit-phases.js +2 -2
  387. package/dist/workflows/runtime/workflow-asset-loader.js +232 -83
  388. package/dist/workflows/schema.js +1 -11
  389. package/dist/workflows/validate-summary.js +30 -36
  390. package/dist/workflows/validator.js +21 -62
  391. package/docs/README.md +68 -0
  392. package/docs/migration/README.md +8 -0
  393. package/docs/migration/release-notes/0.7.0.md +11 -11
  394. package/docs/migration/release-notes/0.9.0.md +208 -27
  395. package/docs/migration/v0.7-to-v0.8.md +46 -47
  396. package/docs/migration/v0.8-to-v0.9.md +564 -208
  397. package/docs/migration/v0.9.0-troubleshooting.md +561 -0
  398. package/docs/reference/README.md +12 -0
  399. package/docs/reference/cli.md +2253 -0
  400. package/docs/reference/configuration.md +358 -0
  401. package/docs/reference/data-and-telemetry.md +105 -42
  402. package/docs/reference/workflows.md +647 -0
  403. package/package.json +22 -11
  404. package/schemas/akm-asset-envelope.json +93 -0
  405. package/schemas/akm-config.json +81 -128
  406. package/schemas/akm-workflow.json +74 -73
  407. package/dist/assets/tasks/core/backup.yml +0 -5
  408. package/dist/assets/tasks/graph-refresh-weekly.yml +0 -10
  409. package/dist/cli/config-migrate.js +0 -1806
  410. package/dist/cli/config-validate.js +0 -41
  411. package/dist/commands/backup-cli.js +0 -56
  412. package/dist/commands/bundle/bundle-cli.js +0 -68
  413. package/dist/commands/bundle/bundle.js +0 -219
  414. package/dist/commands/graph/graph-cli.js +0 -124
  415. package/dist/commands/graph/graph.js +0 -489
  416. package/dist/commands/improve/extract-watch.js +0 -140
  417. package/dist/commands/mv-cli.js +0 -1221
  418. package/dist/commands/sources/history.js +0 -201
  419. package/dist/commands/tasks/default-tasks.js +0 -186
  420. package/dist/core/migration-backup.js +0 -1234
  421. package/dist/indexer/usage/unmigrated-vaults-guard.js +0 -95
  422. package/dist/llm/memory-infer-impl.js +0 -138
  423. package/dist/migrate/legacy/config-source-migration.js +0 -223
  424. package/dist/migrate/legacy/content-migration.js +0 -305
  425. package/dist/migrate/legacy/legacy-layout.js +0 -779
  426. package/dist/migrate/legacy/legacy-paths.js +0 -25
  427. package/dist/migrate/legacy/legacy-stash-json.js +0 -72
  428. package/dist/migrate/legacy/proposal-fs-import.js +0 -168
  429. package/dist/migrate/legacy/task-target-ref-migration.js +0 -272
  430. package/dist/migrate/legacy/three-db-cutover.js +0 -841
  431. package/dist/migrate/legacy/workflow-migrations-bodies.js +0 -52
  432. package/dist/migrate/legacy/workflow-migrations-frozen.js +0 -21
  433. package/dist/migrate/legacy-ref-grammar.js +0 -214
  434. package/dist/output/shapes/distill.js +0 -14
  435. package/dist/output/shapes/history.js +0 -11
  436. package/dist/output/text/distill.js +0 -6
  437. package/dist/output/text/enable-disable.js +0 -8
  438. package/dist/output/text/history.js +0 -6
  439. package/dist/registry/build-index.js +0 -382
  440. package/dist/schemas/akm-config.json +0 -4704
  441. package/dist/schemas/akm-task.json +0 -87
  442. package/dist/schemas/akm-workflow.json +0 -372
  443. package/dist/scripts/migrate-storage.js +0 -3816
  444. package/dist/workflows/authoring/workflow-program-template.yaml +0 -31
  445. package/dist/workflows/cli.js +0 -53
  446. package/dist/workflows/exec/brief.js +0 -481
  447. package/dist/workflows/exec/report.js +0 -1460
  448. package/dist/workflows/exec/watch.js +0 -116
  449. package/dist/workflows/program/parser.js +0 -813
  450. package/dist/workflows/program/project.js +0 -104
@@ -3,22 +3,23 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
+ import { detectAdapterId } from "../core/adapter/detect-adapter.js";
6
7
  import { adapterForId } from "../core/adapter/registry.js";
7
8
  import { isHttpUrl, toErrorMessage } from "../core/common.js";
8
9
  import { concurrentMap } from "../core/concurrent.js";
9
- import { recoverTxnsForRoot } from "../core/fs-txn.js";
10
10
  import { getDbPath } from "../core/paths.js";
11
11
  import { SCRIPT_EXTENSIONS } from "../core/recognition-util.js";
12
12
  import { withStateDb } from "../core/state-db.js";
13
13
  import { isVerbose, warn, warnVerbose } from "../core/warn.js";
14
14
  import { resolveIndexPassLLM } from "../llm/index-passes.js";
15
+ import { resolveSourcesForOrigin } from "../registry/origin-resolve.js";
15
16
  import { closeDatabase, openExistingDatabase, openIndexDatabase } from "../storage/repositories/index-connection.js";
16
- import { deleteEntriesByDirExceptKeys, deleteEntriesByIds, deleteEntriesByStashDir, getEmbeddableEntryCount, getEntryCount, relinkUsageEvents, upsertEntry, upsertWorkflowDocument, } from "../storage/repositories/index-entries-repository.js";
17
+ import { deleteEntriesByDirAndStash, deleteEntriesByDirExceptKeys, deleteEntriesByIds, deleteEntriesByStashDir, deleteUsageEventsByEntryIds, findEntryIdByRef, getAllEntries, getEmbeddableEntryCount, getEntryCount, getIndexedDirPathsByStashDir, getIndexedStashDirsByDir, relinkUsageEvents, upsertEntry, upsertWorkflowDocument, } from "../storage/repositories/index-entries-repository.js";
17
18
  import { rebuildFts } from "../storage/repositories/index-fts-repository.js";
18
19
  import { clearStaleCacheEntries } from "../storage/repositories/index-llm-cache-repository.js";
19
- import { deleteIndexDirStatesByStashDir, getMeta, setMeta, upsertIndexDirState, } from "../storage/repositories/index-meta-repository.js";
20
+ import { deleteIndexDirState, deleteIndexDirStatesByStashDir, getMeta, setMeta, upsertIndexDirState, } from "../storage/repositories/index-meta-repository.js";
20
21
  import { upsertUtilityScore } from "../storage/repositories/index-utility-repository.js";
21
- import { getAllEntriesForEmbedding, getEmbeddingCount, isVecAvailable, purgeEmbeddings, setVecFastPathReady, upsertEmbedding, warnIfVecMissing, } from "../storage/repositories/index-vec-repository.js";
22
+ import { getAllEntriesForEmbedding, getEmbeddingCount, isVecAvailable, isVecFastPathReady, purgeEmbeddings, setVecFastPathReady, upsertEmbedding, warnIfVecMissing, } from "../storage/repositories/index-vec-repository.js";
22
23
  import { takeWorkflowDocument } from "../workflows/runtime/document-cache.js";
23
24
  import { deleteStoredGraph } from "./db/graph-db.js";
24
25
  import { withIndexWriterLease } from "./index-writer-lock.js";
@@ -29,7 +30,7 @@ import { drainDirDocuments } from "./scan/drain-dir.js";
29
30
  import { buildSearchText } from "./search/search-fields.js";
30
31
  import { classifySemanticFailure, clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticStatus, } from "./search/semantic-status.js";
31
32
  import { purgeOldUsageEvents } from "./usage/usage-events.js";
32
- import { walkStashFlat } from "./walk/walker.js";
33
+ import { walkStashFlatWithStatus } from "./walk/walker.js";
33
34
  function throwIfAborted(signal) {
34
35
  if (signal?.aborted) {
35
36
  throw signal.reason instanceof Error ? signal.reason : new Error("index interrupted");
@@ -54,7 +55,11 @@ export function getDefaultLlmConcurrency(llmConfig) {
54
55
  // Remote endpoints default to a modest 2-wide pool (owner ruling 2026-07-21):
55
56
  // enough to overlap request latency without hammering rate-limited APIs.
56
57
  // Local model servers stay at 1 (single loaded model; parallel requests
57
- // trigger reload thrash). `llm.concurrency` in config.json overrides both.
58
+ // trigger reload thrash). The explicit-override branch above only fires for
59
+ // callers that put `concurrency` on the connection themselves —
60
+ // `engines.<name>.concurrency` is a valid schema field but `resolveLlmEngineUse`
61
+ // does NOT copy it into the resolved connection, so on the enrichment path the
62
+ // auto-derived 1/2 is what runs (see docs/architecture/internals/indexing.md).
58
63
  return 2;
59
64
  }
60
65
  // ── Phase functions ──────────────────────────────────────────────────────────
@@ -86,9 +91,7 @@ async function runSourceCachePhase(ctx) {
86
91
  for (const dir of prevStashDirs) {
87
92
  if (!currentSet.has(dir)) {
88
93
  ctx.hadRemovedSources = true;
89
- deleteEntriesByStashDir(db, dir);
90
- deleteIndexDirStatesByStashDir(db, dir);
91
- deleteStoredGraph(db, dir);
94
+ ctx.removedSourceDirs.push(dir);
92
95
  }
93
96
  }
94
97
  }
@@ -97,6 +100,15 @@ async function runSourceCachePhase(ctx) {
97
100
  // further to do here. The flag is exposed on ctx for runWalkPhase().
98
101
  void config;
99
102
  }
103
+ function applyRemovedSources(ctx) {
104
+ if (!ctx.scanComplete)
105
+ return;
106
+ for (const dir of ctx.removedSourceDirs) {
107
+ deleteEntriesByStashDir(ctx.db, dir);
108
+ deleteIndexDirStatesByStashDir(ctx.db, dir);
109
+ deleteStoredGraph(ctx.db, dir);
110
+ }
111
+ }
100
112
  /**
101
113
  * Walk phase: scan the filesystem, generate metadata, and persist entries to
102
114
  * the database. Also kicks off LLM enrichment for directories that need it.
@@ -105,16 +117,17 @@ async function runSourceCachePhase(ctx) {
105
117
  * `ctx.walkWarnings`, and `ctx.dirsNeedingLlm` for downstream phases.
106
118
  */
107
119
  async function runWalkPhase(ctx) {
108
- const { db, sources, isIncremental, builtAtMs, hadRemovedSources, full, reEnrich, signal, onProgress, config } = ctx;
120
+ const { db, sources, isIncremental, builtAtMs, hadRemovedSources, full, clean, signal, onProgress, config } = ctx;
109
121
  throwIfAborted(signal);
110
122
  ctx.timing.tWalkStart = Date.now();
111
123
  const doFullDelete = full || !isIncremental;
112
- const { scannedDirs, skippedDirs, generatedCount, dirsNeedingLlm, warnings } = await indexEntries(db, sources, isIncremental, builtAtMs, hadRemovedSources, doFullDelete, onProgress);
124
+ const { scannedDirs, skippedDirs, generatedCount, dirsNeedingLlm, warnings, complete } = await indexEntries(db, sources, isIncremental, builtAtMs, hadRemovedSources, doFullDelete, onProgress, !clean);
113
125
  ctx.scannedDirs = scannedDirs;
114
126
  ctx.skippedDirs = skippedDirs;
115
127
  ctx.generatedCount = generatedCount;
116
128
  ctx.walkWarnings = warnings;
117
129
  ctx.dirsNeedingLlm = dirsNeedingLlm;
130
+ ctx.scanComplete = complete;
118
131
  onProgress({
119
132
  phase: "scan",
120
133
  message: `Scanned ${scannedDirs} ${scannedDirs === 1 ? "directory" : "directories"} and skipped ${skippedDirs}.`,
@@ -135,7 +148,7 @@ async function runWalkPhase(ctx) {
135
148
  ctx.timing.tWalkEnd = Date.now();
136
149
  throwIfAborted(signal);
137
150
  // LLM enrichment for directories that need it
138
- await enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal, reEnrich);
151
+ await enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal);
139
152
  onProgress({
140
153
  phase: "llm",
141
154
  message: resolveIndexPassLLM("enrichment", config)
@@ -193,15 +206,19 @@ async function runFinalizePhase(ctx) {
193
206
  /* ignore */
194
207
  }
195
208
  throwIfAborted(signal);
196
- // Update index metadata
209
+ // An incomplete run preserves the prior freshness watermark. Advancing it
210
+ // could make a recovered source look unchanged even though this run never
211
+ // persisted its files.
197
212
  const embeddingResult = ctx.embeddingResult ?? { success: false };
198
- setMeta(db, "builtAt", new Date().toISOString());
199
- setMeta(db, "stashDir", stashDir);
200
- setMeta(db, "stashDirs", JSON.stringify(sourceDirs));
213
+ if (ctx.scanComplete) {
214
+ setMeta(db, "builtAt", new Date().toISOString());
215
+ setMeta(db, "stashDir", stashDir);
216
+ setMeta(db, "stashDirs", JSON.stringify(sourceDirs));
217
+ }
201
218
  setMeta(db, "hasEmbeddings", embeddingResult.success ? "1" : "0");
202
219
  // Stash-organization conventions (SPEC-8): track which `index.indexBodyOpening`
203
220
  // state the index was built with, and warn while the flag diverges from it.
204
- const bodyOpeningWarning = reconcileBodyOpeningIndexState(db, config.index?.indexBodyOpening === true, ctx.full || !isIncremental);
221
+ const bodyOpeningWarning = reconcileBodyOpeningIndexState(db, config.index?.indexBodyOpening === true, (ctx.full || !isIncremental) && ctx.scanComplete);
205
222
  if (bodyOpeningWarning)
206
223
  warn(bodyOpeningWarning);
207
224
  warnIfVecMissing(db);
@@ -264,7 +281,7 @@ export function reconcileBodyOpeningIndexState(db, flagEnabled, isFullWalk) {
264
281
  `${flagEnabled ? "disabled" : "enabled"}. Incremental runs only re-extract changed files, so ` +
265
282
  "indexed text and embeddings are stale for unchanged entries. Run `akm index --full` to apply the new " +
266
283
  "setting everywhere (embeddings regenerate), and re-mint collapse-detector canary baselines via " +
267
- "`akm improve canary --refresh` if you use them.");
284
+ "`bun scripts/refresh-canary-set.ts --refresh` if you use them.");
268
285
  }
269
286
  // ── Clean pass ───────────────────────────────────────────────────────────────
270
287
  /**
@@ -302,7 +319,82 @@ export async function akmIndex(options) {
302
319
  return akmIndexOverride(options);
303
320
  return akmIndexReal(options);
304
321
  }
322
+ /**
323
+ * Detect an adapter for every resolvable source that does not declare one, and
324
+ * persist each detection into `config.json`.
325
+ *
326
+ * R-056: this config write previously had zero disclosure — it appeared in no
327
+ * result, on no stream, and in no doc. The returned `persistedAdapters` records
328
+ * exactly which bundle→adapter pairs the mutate callback actually applied, so
329
+ * the caller can surface them in the result envelope; a stderr notice is
330
+ * emitted here. The map is cleared at the top of every callback invocation
331
+ * because `mutateConfig` may retry optimistically, and a retry must not report
332
+ * a superseded attempt.
333
+ *
334
+ * Extracted from `akmIndexReal` as one self-contained named pass, both to keep
335
+ * that function under the src-wide function-size bar and because the detection
336
+ * and its disclosure belong together.
337
+ */
338
+ function detectAndPersistBundleAdapters(allSourceEntries, config, mutateConfig, opts) {
339
+ const detectedByBundle = new Map();
340
+ for (const source of allSourceEntries) {
341
+ if (source.adapterId || source.unresolved)
342
+ continue;
343
+ if (allSourceRootsReadable([source.path])) {
344
+ source.adapterId = detectAdapterId(source.path);
345
+ if (source.registryId)
346
+ detectedByBundle.set(source.registryId, source.adapterId);
347
+ }
348
+ }
349
+ const persistedAdapters = {};
350
+ if (detectedByBundle.size === 0)
351
+ return { config, persistedAdapters };
352
+ const nextConfig = mutateConfig((current) => {
353
+ if (!current.bundles)
354
+ return current;
355
+ let changed = false;
356
+ const bundles = { ...current.bundles };
357
+ for (const key of Object.keys(persistedAdapters))
358
+ delete persistedAdapters[key];
359
+ for (const [bundleId, adapter] of detectedByBundle) {
360
+ const bundle = bundles[bundleId];
361
+ if (!bundle)
362
+ continue;
363
+ const componentEntries = Object.entries(bundle.components ?? {});
364
+ const [componentId, component] = componentEntries[0] ?? ["main", {}];
365
+ if (component.adapter)
366
+ continue;
367
+ bundles[bundleId] = { ...bundle, components: { [componentId]: { ...component, adapter } } };
368
+ changed = true;
369
+ persistedAdapters[bundleId] = adapter;
370
+ }
371
+ return changed ? { ...current, bundles } : current;
372
+ }, { absentNoop: true }).config;
373
+ const persistedCount = Object.keys(persistedAdapters).length;
374
+ if (persistedCount > 0 && opts.announce) {
375
+ const summary = Object.entries(persistedAdapters)
376
+ .map(([bundleId, adapter]) => `${bundleId} → ${adapter}`)
377
+ .join(", ");
378
+ warn(`[index] Detected adapter${persistedCount === 1 ? "" : "s"} for ${summary}; persisted to config.json ` +
379
+ "(bundles.<id>.components.<component>.adapter).");
380
+ }
381
+ return { config: nextConfig, persistedAdapters };
382
+ }
305
383
  async function akmIndexReal(options) {
384
+ // R-022: `dryRun` only ever gated the `--clean` stale-entry removal pass
385
+ // (see `runCleanPass` below) — every other phase (walk, LLM enrichment,
386
+ // embeddings, FTS, the adapter-detection config write) ran for real
387
+ // regardless, so `akm index --dry-run` alone silently performed a full,
388
+ // real index. The flag's own docs (`IndexOptions.dryRun` above, and the
389
+ // CLI help in stash-cli.ts) already scope it to `--clean`; reject the
390
+ // combination that was never implemented instead of quietly doing
391
+ // something other than what "dry run" promised. Checked before the writer
392
+ // lease is even requested so a bad invocation fails instantly.
393
+ if (options?.dryRun === true && options?.clean !== true) {
394
+ const { UsageError } = await import("../core/errors.js");
395
+ throw new UsageError("`--dry-run` only applies together with `--clean` (it previews which stale entries `--clean` would remove). " +
396
+ "Pass `akm index --clean --dry-run`, or drop `--dry-run` to run a real index.", "INVALID_FLAG_VALUE", "Run `akm index --clean --dry-run` to preview, or `akm index --clean` to apply.");
397
+ }
306
398
  const requestedAt = Date.now();
307
399
  let acquiredAt = requestedAt;
308
400
  return withIndexWriterLease({
@@ -321,33 +413,41 @@ async function akmIndexReal(options) {
321
413
  const stashDir = options.stashDir;
322
414
  const onProgress = options?.onProgress ?? (() => { });
323
415
  const signal = options?.signal;
324
- const reEnrich = options?.reEnrich === true;
325
416
  const full = options?.full === true;
326
417
  const clean = options?.clean === true;
327
418
  const dryRun = options?.dryRun === true;
328
419
  // Load config and resolve all stash sources
329
- const { loadConfig } = await import("../core/config/config.js");
330
- const config = loadConfig();
331
- // One-time, read-only guard: warn if the writable stash still holds an
332
- // un-migrated `vaults/` directory. In 0.9.0 the indexer skips `vaults/`
333
- // entirely, so an unmigrated vault's `.env` data would silently never be
334
- // indexed. Non-destructive — only stats, never reads/writes/deletes.
335
- const { warnOnUnmigratedVaults } = await import("./usage/unmigrated-vaults-guard.js");
336
- warnOnUnmigratedVaults(stashDir);
420
+ const { loadConfig, mutateConfig } = await import("../core/config/config.js");
421
+ let config = loadConfig();
422
+ // Durable state must be runtime-compatible before source hydration,
423
+ // adapter persistence, or index.db creation can mutate the installation.
424
+ onProgress({ phase: "preflight", message: "Validating durable state." });
425
+ withStateDb(() => undefined);
337
426
  // Ensure git stash caches are extracted before resolving stash dirs,
338
427
  // so their content directories exist on disk for the walker to discover.
339
428
  const sourceCacheStart = Date.now();
340
429
  onProgress({ phase: "preflight", message: "Hydrating source caches." });
341
430
  const { ensureSourceCaches, resolveSourceEntries } = await import("./search/search-source.js");
342
- await ensureSourceCaches(config, { force: full, materialize: options.hydrateSources !== false });
431
+ // Inject the store-backed secret resolver from here a composition root
432
+ // ABOVE the provider/fetcher import cycle (this module reaches
433
+ // search-source only via dynamic import). This is what lets a website
434
+ // source's X fetcher resolve `secrets/x-bearer-token` during
435
+ // bundle-update / hydrate, not just from the command-layer URL-ingest
436
+ // path. `secret-seam` is imported here, never from inside the cycle.
437
+ const { storeSecretResolver } = await import("../sources/snapshot-fetchers/secret-seam.js");
438
+ await ensureSourceCaches(config, {
439
+ force: full,
440
+ materialize: options.hydrateSources !== false,
441
+ secrets: storeSecretResolver,
442
+ });
343
443
  const sourceCacheEnd = Date.now();
344
444
  const allSourceEntries = resolveSourceEntries(stashDir, config);
445
+ const detected = detectAndPersistBundleAdapters(allSourceEntries, config, mutateConfig, {
446
+ announce: options.implicit !== true,
447
+ });
448
+ config = detected.config;
449
+ const persistedAdapters = detected.persistedAdapters;
345
450
  const allSourceDirs = allSourceEntries.map((s) => s.path);
346
- for (const sourceDir of new Set([stashDir, ...allSourceDirs])) {
347
- // Unified fs-txn engine (WI-6.3): finish/roll back interrupted mv
348
- // transactions for every source root before indexing walks it.
349
- await recoverTxnsForRoot(sourceDir, (journal) => journal.kind === "mv");
350
- }
351
451
  onProgress({
352
452
  phase: "preflight",
353
453
  message: `Resolved ${allSourceDirs.length} stash source${allSourceDirs.length === 1 ? "" : "s"}.`,
@@ -370,7 +470,7 @@ async function akmIndexReal(options) {
370
470
  sources: allSourceEntries,
371
471
  sourceDirs: allSourceDirs,
372
472
  full,
373
- reEnrich,
473
+ clean,
374
474
  stashDir,
375
475
  onProgress,
376
476
  signal,
@@ -387,6 +487,8 @@ async function akmIndexReal(options) {
387
487
  isIncremental,
388
488
  builtAtMs,
389
489
  hadRemovedSources: false,
490
+ removedSourceDirs: [],
491
+ scanComplete: true,
390
492
  scannedDirs: 0,
391
493
  skippedDirs: 0,
392
494
  generatedCount: 0,
@@ -408,6 +510,7 @@ async function akmIndexReal(options) {
408
510
  // ── Phase sequence ───────────────────────────────────────────────────────
409
511
  await runSourceCachePhase(ctx);
410
512
  await runWalkPhase(ctx);
513
+ applyRemovedSources(ctx);
411
514
  await runEmbeddingPhase(ctx);
412
515
  await runFinalizePhase(ctx);
413
516
  // ────────────────────────────────────────────────────────────────────────
@@ -425,7 +528,13 @@ async function akmIndexReal(options) {
425
528
  phase: "finalize",
426
529
  message: dryRun ? "Scanning for stale index entries (dry run)." : "Removing stale index entries.",
427
530
  });
428
- cleanResult = runCleanPass(db, dryRun);
531
+ if (ctx.scanComplete) {
532
+ cleanResult = runCleanPass(db, dryRun);
533
+ }
534
+ else {
535
+ warn("[index] --clean skipped because one or more configured sources were not scanned completely.");
536
+ cleanResult = { checked: 0, removed: 0, removedRefs: [], dryRun };
537
+ }
429
538
  }
430
539
  const cleanEnd = Date.now();
431
540
  // ────────────────────────────────────────────────────────────────────────
@@ -438,6 +547,9 @@ async function akmIndexReal(options) {
438
547
  directoriesScanned: ctx.scannedDirs,
439
548
  directoriesSkipped: ctx.skippedDirs,
440
549
  ...(ctx.walkWarnings.length > 0 ? { warnings: ctx.walkWarnings } : {}),
550
+ ...(Object.keys(persistedAdapters).length > 0
551
+ ? { configUpdated: { detectedAdapters: persistedAdapters } }
552
+ : {}),
441
553
  verification,
442
554
  timing: {
443
555
  totalMs: Date.now() - timing.t0,
@@ -460,6 +572,13 @@ async function akmIndexReal(options) {
460
572
  }
461
573
  });
462
574
  }
575
+ function removalsFirst(records) {
576
+ return [...records.filter((record) => record.remove), ...records.filter((record) => !record.remove)];
577
+ }
578
+ function addEntryIds(target, ids) {
579
+ for (const id of ids)
580
+ target.add(id);
581
+ }
463
582
  /**
464
583
  * Map each source root → its durable `BundleComponent` (`deriveInstallations`,
465
584
  * batch-unique bundle ids, source order preserved). The per-dir document drain
@@ -478,6 +597,134 @@ function buildComponentBySource(sources) {
478
597
  });
479
598
  return map;
480
599
  }
600
+ function componentForSource(components, sourcePath) {
601
+ return (components.get(sourcePath) ?? {
602
+ id: sourcePath,
603
+ adapter: "akm",
604
+ root: sourcePath,
605
+ writable: false,
606
+ });
607
+ }
608
+ function groupFileContextsByDir(fileContexts) {
609
+ const groups = new Map();
610
+ for (const ctx of fileContexts) {
611
+ const group = groups.get(ctx.parentDirAbs);
612
+ if (group)
613
+ group.push(ctx);
614
+ else
615
+ groups.set(ctx.parentDirAbs, [ctx]);
616
+ }
617
+ return groups;
618
+ }
619
+ function sourceSnapshotRemovals(db, currentStashDir, currentDirs, allIndexedDirsBySource) {
620
+ const indexedDirs = allIndexedDirsBySource?.get(path.resolve(currentStashDir)) ?? getIndexedDirPathsByStashDir(db, currentStashDir);
621
+ return [...indexedDirs]
622
+ .map((dirPath) => path.resolve(dirPath))
623
+ .filter((dirPath) => !currentDirs.has(dirPath))
624
+ .map((dirPath) => ({
625
+ dirPath,
626
+ currentStashDir,
627
+ files: [],
628
+ stash: null,
629
+ skip: false,
630
+ remove: true,
631
+ reason: { kind: "not-in-source-snapshot" },
632
+ }));
633
+ }
634
+ function buildSourceScanPlans(db, allSourceEntries, isIncremental, reconcileMissingDirs) {
635
+ const componentBySource = buildComponentBySource(allSourceEntries);
636
+ const handoffDirs = new Set();
637
+ const plans = allSourceEntries.map((sourceAdded) => {
638
+ const currentStashDir = sourceAdded.path;
639
+ const component = componentForSource(componentBySource, currentStashDir);
640
+ if (sourceAdded.unresolved) {
641
+ return {
642
+ currentStashDir,
643
+ component,
644
+ adapter: undefined,
645
+ indexVariant: undefined,
646
+ dirGroups: new Map(),
647
+ removals: [],
648
+ walkComplete: false,
649
+ };
650
+ }
651
+ const walked = walkStashFlatWithStatus(currentStashDir, {
652
+ includeAllDirectories: component.adapter === "okf",
653
+ });
654
+ const dirGroups = groupFileContextsByDir(walked.files);
655
+ const adapter = adapterForId(component.adapter);
656
+ return {
657
+ currentStashDir,
658
+ component,
659
+ adapter,
660
+ indexVariant: adapter ? `${adapter.id}@${adapter.version}` : undefined,
661
+ dirGroups,
662
+ removals: [],
663
+ walkComplete: walked.complete,
664
+ };
665
+ });
666
+ const removalKeys = new Set();
667
+ const addRemoval = (plan, dirPath, stashDir) => {
668
+ const resolvedDir = path.resolve(dirPath);
669
+ const key = `${resolvedDir}\0${path.resolve(stashDir)}`;
670
+ if (removalKeys.has(key))
671
+ return;
672
+ removalKeys.add(key);
673
+ plan.removals.push({
674
+ dirPath,
675
+ currentStashDir: stashDir,
676
+ files: [],
677
+ stash: null,
678
+ skip: false,
679
+ remove: true,
680
+ reason: { kind: "not-in-source-snapshot" },
681
+ });
682
+ handoffDirs.add(resolvedDir);
683
+ };
684
+ const allComplete = plans.every((plan) => plan.walkComplete && plan.adapter !== undefined);
685
+ // A full, globally-complete run uses the atomic table wipe below. Every
686
+ // other run reconciles only sources that produced trustworthy snapshots.
687
+ if (reconcileMissingDirs && (isIncremental || !allComplete)) {
688
+ const allIndexedDirsBySource = !isIncremental ? new Map() : undefined;
689
+ if (allIndexedDirsBySource) {
690
+ for (const entry of getAllEntries(db)) {
691
+ const sourceRoot = path.resolve(entry.stashDir);
692
+ const dirs = allIndexedDirsBySource.get(sourceRoot) ?? new Set();
693
+ dirs.add(path.resolve(entry.dirPath));
694
+ allIndexedDirsBySource.set(sourceRoot, dirs);
695
+ }
696
+ }
697
+ for (const plan of plans) {
698
+ if (!plan.walkComplete || !plan.adapter)
699
+ continue;
700
+ const currentDirs = new Set([...plan.dirGroups.keys()].map((dirPath) => path.resolve(dirPath)));
701
+ for (const removal of sourceSnapshotRemovals(db, plan.currentStashDir, currentDirs, allIndexedDirsBySource)) {
702
+ addRemoval(plan, removal.dirPath, removal.currentStashDir);
703
+ }
704
+ }
705
+ }
706
+ // Cross-source ownership handoffs can delete another source's rows, so they
707
+ // still require every possible owner to have completed its scan.
708
+ if (!allComplete)
709
+ return { plans, handoffDirs };
710
+ // The first configured source that exposes a physical directory owns it.
711
+ // Remove rows left by a prior owner even when both adapters are identical.
712
+ const claimedDirs = new Set();
713
+ for (const plan of plans) {
714
+ for (const dirPath of plan.dirGroups.keys()) {
715
+ const resolvedDir = path.resolve(dirPath);
716
+ if (claimedDirs.has(resolvedDir))
717
+ continue;
718
+ claimedDirs.add(resolvedDir);
719
+ for (const priorOwner of getIndexedStashDirsByDir(db, dirPath)) {
720
+ if (path.resolve(priorOwner) !== path.resolve(plan.currentStashDir)) {
721
+ addRemoval(plan, dirPath, priorOwner);
722
+ }
723
+ }
724
+ }
725
+ }
726
+ return { plans, handoffDirs };
727
+ }
481
728
  /**
482
729
  * Phase 1 (async): walk every source directory and pre-generate all metadata
483
730
  * outside any transaction, producing the per-directory scan records that
@@ -488,24 +735,20 @@ function buildComponentBySource(sources) {
488
735
  * outside `db.transaction()` so the persist pass can be a single synchronous
489
736
  * transaction.
490
737
  */
491
- async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, onProgress) {
738
+ function reportSourceScanProgress(onProgress, processed, total, message) {
739
+ onProgress?.({ phase: "scan", message, processed, total });
740
+ }
741
+ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, onProgress, reconcileMissingDirs = true) {
492
742
  let scannedDirs = 0;
493
743
  let skippedDirs = 0;
494
744
  let generatedCount = 0;
495
745
  const warnings = [];
496
746
  const seenPaths = new Set();
497
- const componentBySource = buildComponentBySource(allSourceEntries);
747
+ const { plans, handoffDirs } = buildSourceScanPlans(db, allSourceEntries, isIncremental, reconcileMissingDirs);
498
748
  const dirRecords = [];
499
749
  let processedDirs = 0;
500
750
  let priorDirsChanged = hadRemovedSources;
501
- const reportScanProgress = (message) => {
502
- onProgress?.({
503
- phase: "scan",
504
- message,
505
- processed: processedDirs,
506
- total: allSourceEntries.length,
507
- });
508
- };
751
+ const reportScanProgress = (message) => reportSourceScanProgress(onProgress, processedDirs, allSourceEntries.length, message);
509
752
  const reportDirDecision = (kind, dirPath, currentStashDir, reason, persistedRowCount) => {
510
753
  if (!isVerbose())
511
754
  return;
@@ -514,10 +757,7 @@ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, ha
514
757
  reportScanProgress(`${kind === "scan" ? "Rescanning" : "Skipping"} ${path.relative(currentStashDir, dirPath) || "."} ` +
515
758
  `from ${currentStashDir}: ${reason.kind}${detail}${rowInfo}`);
516
759
  };
517
- // Duplicate-directory guard for the per-source scan. A
518
- // dir may surface via multiple stash roots; only the first occurrence is
519
- // indexed, and the later ones are recorded as skips. Returns true when the
520
- // dir was already seen (caller should skip further processing).
760
+ // Only the first source that exposes a physical directory may index it.
521
761
  const markSeenOrSkipDuplicate = (dirPath, currentStashDir, files) => {
522
762
  const resolved = path.resolve(dirPath);
523
763
  if (seenPaths.has(resolved)) {
@@ -532,9 +772,9 @@ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, ha
532
772
  // Incremental freshness gate shared by both branches: consult the persisted
533
773
  // dir state and record either a skip (unchanged + eligible for incremental
534
774
  // skip) or a scan record carrying the candidate stash.
535
- const recordFreshnessDecision = (dirPath, currentStashDir, stateFiles, stash, hashByFile, conceptIdByFile) => {
536
- const previousState = getDirIndexState(db, dirPath, stateFiles, builtAtMs);
537
- if (isIncremental && !previousState.stale && canUseIncrementalSkip(previousState, priorDirsChanged)) {
775
+ const recordFreshnessDecision = (dirPath, currentStashDir, stateFiles, stash, hashByFile, conceptIdByFile, indexVariant, forceScan, pruneMissing) => {
776
+ const previousState = getDirIndexState(db, dirPath, stateFiles, builtAtMs, indexVariant);
777
+ if (isIncremental && !forceScan && !previousState.stale && canUseIncrementalSkip(previousState, priorDirsChanged)) {
538
778
  skippedDirs++;
539
779
  dirRecords.push({
540
780
  dirPath,
@@ -543,6 +783,7 @@ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, ha
543
783
  stash: null,
544
784
  skip: true,
545
785
  reason: previousState.reason,
786
+ indexVariant,
546
787
  });
547
788
  reportDirDecision("skip", dirPath, currentStashDir, previousState.reason, previousState.persistedRowCount);
548
789
  return;
@@ -560,46 +801,44 @@ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, ha
560
801
  persistedRowCount: previousState.persistedRowCount,
561
802
  hashByFile,
562
803
  conceptIdByFile,
804
+ indexVariant,
805
+ pruneMissing,
563
806
  });
564
807
  reportDirDecision("scan", dirPath, currentStashDir, reason, previousState.persistedRowCount);
565
808
  };
566
- for (const sourceAdded of allSourceEntries) {
567
- const currentStashDir = sourceAdded.path;
568
- const fileContexts = walkStashFlat(currentStashDir);
809
+ for (const plan of plans) {
810
+ const { currentStashDir, component, adapter, dirGroups, removals, walkComplete } = plan;
569
811
  processedDirs++;
570
812
  reportScanProgress(`Processed ${processedDirs}/${allSourceEntries.length} source${allSourceEntries.length === 1 ? "" : "s"}.`);
571
- // Group by parent dir, keeping the FileContexts so the drain can hand each
572
- // to the component's dispatched `adapter.recognize` (F4a M-core-2). The
573
- // freshness gate still keys on the plain path list derived from these.
574
- const dirGroups = new Map();
575
- for (const ctx of fileContexts) {
576
- const dir = ctx.parentDirAbs;
577
- const group = dirGroups.get(dir);
578
- if (group)
579
- group.push(ctx);
580
- else
581
- dirGroups.set(dir, [ctx]);
813
+ if (!walkComplete) {
814
+ for (const dirPath of dirGroups.keys())
815
+ seenPaths.add(path.resolve(dirPath));
816
+ warn(`[index] source "${component.id}" was not scanned completely; preserving its last-known-good rows.`);
817
+ continue;
582
818
  }
583
- const component = componentBySource.get(currentStashDir) ?? {
584
- id: currentStashDir,
585
- adapter: "akm",
586
- root: currentStashDir,
587
- writable: false,
588
- };
589
819
  // Owner ruling 2026-07-21: dispatch each component's DETECTED adapter (§4).
590
820
  // An unknown adapter id has no `adapterForId` match → skip the whole
591
821
  // component with a warning (one bundle = one component = one adapter).
592
- const adapter = adapterForId(component.adapter);
593
822
  if (!adapter) {
823
+ for (const dirPath of dirGroups.keys())
824
+ seenPaths.add(path.resolve(dirPath));
594
825
  warn(`Skipping component "${component.id}": unknown adapter id "${component.adapter}".`);
595
826
  continue;
596
827
  }
828
+ const indexVariant = plan.indexVariant ?? `${adapter.id}@${adapter.version}`;
829
+ for (const removal of removals) {
830
+ dirRecords.push(removal);
831
+ scannedDirs++;
832
+ priorDirsChanged = true;
833
+ reportDirDecision("scan", removal.dirPath, currentStashDir, removal.reason);
834
+ }
597
835
  for (const [dirPath, ctxs] of dirGroups) {
598
836
  // Adapter-owned filtering (owner ruling 2026-07-21): the drain no longer
599
837
  // pre-filters with AKM-stash policy — each adapter's `recognize` claims or
600
838
  // abstains on its own bundle's walked files. The core walk keeps only the
601
839
  // universal hygiene `walkStashFlat` already applies (.git/dot-dirs/etc.).
602
840
  const indexableFiles = ctxs.map((ctx) => ctx.absPath);
841
+ const forceScan = handoffDirs.has(path.resolve(dirPath));
603
842
  if (markSeenOrSkipDuplicate(dirPath, currentStashDir, indexableFiles))
604
843
  continue;
605
844
  if (indexableFiles.length === 0) {
@@ -609,7 +848,9 @@ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, ha
609
848
  reportDirDecision("skip", dirPath, currentStashDir, reason);
610
849
  continue;
611
850
  }
612
- const cachedZeroRowState = isIncremental && getCachedZeroRowDirState(db, dirPath, indexableFiles, builtAtMs, priorDirsChanged);
851
+ const cachedZeroRowState = isIncremental &&
852
+ !forceScan &&
853
+ getCachedZeroRowDirState(db, dirPath, indexableFiles, builtAtMs, priorDirsChanged, indexVariant);
613
854
  if (cachedZeroRowState) {
614
855
  skippedDirs++;
615
856
  dirRecords.push({
@@ -619,6 +860,7 @@ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, ha
619
860
  stash: null,
620
861
  skip: true,
621
862
  reason: cachedZeroRowState.reason,
863
+ indexVariant,
622
864
  });
623
865
  reportDirDecision("skip", dirPath, currentStashDir, cachedZeroRowState.reason, cachedZeroRowState.persistedRowCount);
624
866
  continue;
@@ -639,10 +881,27 @@ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, ha
639
881
  if (generated.entries.length > 0) {
640
882
  generatedCount += generated.entries.length;
641
883
  }
642
- recordFreshnessDecision(dirPath, currentStashDir, staleFiles, stash, drained.hashByFile, drained.conceptIdByFile);
884
+ recordFreshnessDecision(dirPath, currentStashDir, staleFiles, stash, drained.hashByFile, drained.conceptIdByFile, indexVariant, forceScan, walkComplete);
643
885
  }
644
886
  }
645
- return { dirRecords, scannedDirs, skippedDirs, generatedCount, warnings };
887
+ return {
888
+ dirRecords: removalsFirst(dirRecords),
889
+ scannedDirs,
890
+ skippedDirs,
891
+ generatedCount,
892
+ warnings,
893
+ complete: plans.every((plan) => plan.walkComplete && plan.adapter !== undefined),
894
+ };
895
+ }
896
+ function preserveExistingIndex(doFullDelete, dirRecords, sourceRoots) {
897
+ if (!doFullDelete)
898
+ return false;
899
+ const incomingDocCount = dirRecords.reduce((n, record) => n + (record.skip ? 0 : (record.stash?.entries.length ?? 0)), 0);
900
+ if (incomingDocCount > 0 || allSourceRootsReadable(sourceRoots))
901
+ return false;
902
+ warn("[index] --full produced zero documents while one or more source roots are missing or unreadable — " +
903
+ "preserving the existing index (last-known-good) rather than wiping it. Re-run once the sources are available.");
904
+ return true;
646
905
  }
647
906
  /**
648
907
  * #624-P1 zero-document preflight probe. A source root counts as "readable"
@@ -671,8 +930,9 @@ function allSourceRootsReadable(roots) {
671
930
  * Phase 2 (sync): write all pre-generated scan records inside a single
672
931
  * transaction, returning the directories that still need LLM enrichment.
673
932
  */
674
- function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots, bundleByRoot) {
933
+ function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots, scanComplete, bundleByRoot) {
675
934
  const dirsNeedingLlm = [];
935
+ const fullDelete = doFullDelete && scanComplete;
676
936
  // #624-P1 zero-document preflight (spec §4). A full-rebuild wipe is a
677
937
  // legitimate mass-delete ONLY when the scan legitimately found nothing. If
678
938
  // the walk produced zero documents AND any configured source root is missing
@@ -681,25 +941,19 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots,
681
941
  // last-known-good index (entries + embeddings + utility/usage). Preserve it
682
942
  // and warn instead; the next successful run reconciles. A genuinely empty
683
943
  // stash whose roots ARE readable still wipes, as before.
684
- if (doFullDelete) {
685
- const incomingDocCount = dirRecords.reduce((n, r) => n + (r.skip ? 0 : (r.stash?.entries.length ?? 0)), 0);
686
- if (incomingDocCount === 0 && !allSourceRootsReadable(sourceRoots)) {
687
- warn("[index] --full produced zero documents while one or more source roots are missing or unreadable " +
688
- "preserving the existing index (last-known-good) rather than wiping it. Re-run once the sources are available.");
689
- return { dirsNeedingLlm };
690
- }
691
- }
692
- // Cross-stash dedup: track indexed assets by type + entry.name so a
693
- // lower-priority stash row with the same logical index identity is skipped
694
- // when a higher-priority root already covers it.
695
- // Sources are ordered by priority (primary stash first), so the first
696
- // occurrence wins.
944
+ if (preserveExistingIndex(fullDelete, dirRecords, sourceRoots))
945
+ return { dirsNeedingLlm };
946
+ // Per-source dedup: the same logical asset can appear more than once within
947
+ // one owning source, where source order still makes the first occurrence win.
948
+ // The owner is part of the key so identical concepts in different bundles
949
+ // remain distinct indexed rows.
697
950
  const indexedAssetIdentities = new Set();
951
+ const deletedUsageEntryIds = new Set();
698
952
  const insertTransaction = db.transaction(() => {
699
953
  // Perform the full-rebuild wipe as the FIRST step of the insert
700
954
  // transaction so delete and re-insert are atomic — a concurrent reader
701
955
  // never observes an empty database between the two operations.
702
- if (doFullDelete) {
956
+ if (fullDelete) {
703
957
  try {
704
958
  db.exec("DELETE FROM embeddings");
705
959
  }
@@ -723,10 +977,16 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots,
723
977
  // re-resolves the rest by entry_ref — subsuming the old detach.
724
978
  db.exec("DELETE FROM entries");
725
979
  }
726
- for (const { dirPath, currentStashDir, files, stash, skip, reason, hashByFile, conceptIdByFile } of dirRecords) {
980
+ for (const { dirPath, currentStashDir, files, stash, skip, reason, hashByFile, conceptIdByFile, indexVariant, remove, pruneMissing, } of dirRecords) {
981
+ if (remove) {
982
+ const removedIds = deleteEntriesByDirAndStash(db, dirPath, currentStashDir, { cleanupUsageEvents: false });
983
+ addEntryIds(deletedUsageEntryIds, removedIds);
984
+ deleteIndexDirState(db, dirPath);
985
+ continue;
986
+ }
727
987
  if (skip) {
728
988
  if (reason?.kind === "unchanged") {
729
- const fingerprint = computeDirFingerprint(dirPath, files);
989
+ const fingerprint = computeDirFingerprint(dirPath, files, indexVariant);
730
990
  upsertIndexDirState(db, {
731
991
  dirPath,
732
992
  fileSetHash: fingerprint.fileSetHash,
@@ -745,36 +1005,39 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots,
745
1005
  let persistedRows = 0;
746
1006
  let dedupedRows = 0;
747
1007
  if (stash) {
1008
+ const bundle = bundleByRoot.get(path.resolve(currentStashDir));
1009
+ if (!bundle)
1010
+ throw new Error(`Missing bundle provenance for indexed source ${currentStashDir}`);
1011
+ const ownerIdentity = bundle.bundleId;
748
1012
  for (const entry of stash.entries) {
749
1013
  const entryPath = entry.filename ? path.join(dirPath, entry.filename) : null;
750
1014
  if (!entryPath) {
751
1015
  warn(`Skipping entry with no resolvable path in ${dirPath}`);
752
1016
  continue;
753
1017
  }
754
- // Skip if a higher-priority stash root already indexed this asset
755
- const identityKey = `${entry.type}\0${entry.name}`;
1018
+ const adapterConceptId = conceptIdByFile?.get(entryPath);
1019
+ if (!adapterConceptId) {
1020
+ warn(`Skipping entry without adapter-owned concept identity: ${entryPath}`);
1021
+ continue;
1022
+ }
1023
+ // Adapter-owned concept identity is path-based and cannot be replaced
1024
+ // by presentation fields such as type/title.
1025
+ const identityKey = `${ownerIdentity}\0${adapterConceptId}`;
756
1026
  if (indexedAssetIdentities.has(identityKey)) {
757
1027
  dedupedRows++;
758
1028
  continue;
759
1029
  }
760
1030
  indexedAssetIdentities.add(identityKey);
761
- const entryKey = `${currentStashDir}:${entry.type}:${entry.name}`;
1031
+ const entryKey = bundle?.adapterId !== "akm" && adapterConceptId
1032
+ ? `${currentStashDir}:concept:${adapterConceptId}`
1033
+ : `${currentStashDir}:${entry.type}:${entry.name}`;
762
1034
  keptEntryKeys.add(entryKey);
763
1035
  const searchText = buildSearchText(entry);
764
1036
  const entryWithSize = attachFileSize(entry, entryPath);
765
1037
  // content_hash = doc.hash from the drain, keyed by the recognized
766
- // file's path (stable across the legacy-sidecar merge). NULL preserves
767
- // any existing hash (upsert COALESCE) for sidecar-only entries.
1038
+ // file's path. A missing hash preserves the existing value on upsert.
768
1039
  const contentHash = hashByFile?.get(entryPath);
769
- // Chunk-5 Step 2 (spec §14.4): derive the durable bundle identity
770
- // (the D-R2 qualified conceptId + `<bundle>//<conceptId>` item_ref) via
771
- // the shared helper that the write-path fast path also uses. NULL
772
- // provenance when the source root has no bundle mapping (e.g. write-back
773
- // paths) — healed on next index.
774
- const bundle = bundleByRoot?.get(path.resolve(currentStashDir));
775
- const provenance = bundle
776
- ? deriveEntryProvenance(bundle, entry.type, entry.name, conceptIdByFile?.get(entryPath))
777
- : undefined;
1040
+ const provenance = deriveEntryProvenance(bundle, entry.type, entry.name, adapterConceptId);
778
1041
  const entryId = upsertEntry(db, entryKey, dirPath, entryPath, currentStashDir, entryWithSize, searchText, provenance, contentHash);
779
1042
  persistedRows++;
780
1043
  if (entry.type === "workflow") {
@@ -785,8 +1048,7 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots,
785
1048
  }
786
1049
  }
787
1050
  // Collect dirs needing LLM enhancement during the first walk.
788
- // Only dirs with "generated" entries need enrichment (unless reEnrich
789
- // forces re-processing of already-enriched entries).
1051
+ // Only dirs with "generated" entries need enrichment.
790
1052
  if (stash.entries.some((e) => e.quality === "generated")) {
791
1053
  dirsNeedingLlm.push({ dirPath, files, currentStashDir, stash });
792
1054
  }
@@ -795,8 +1057,10 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots,
795
1057
  // (files deleted, deduped away, or abstained on by the adapter). With
796
1058
  // an empty kept-set this deletes every row for the dir — the exact net
797
1059
  // effect of the old unconditional `deleteEntriesByDir`, minus the id churn.
798
- deleteEntriesByDirExceptKeys(db, dirPath, keptEntryKeys);
799
- const fingerprint = computeDirFingerprint(dirPath, files);
1060
+ if (pruneMissing !== false) {
1061
+ addEntryIds(deletedUsageEntryIds, deleteEntriesByDirExceptKeys(db, dirPath, currentStashDir, keptEntryKeys, { cleanupUsageEvents: false }));
1062
+ }
1063
+ const fingerprint = computeDirFingerprint(dirPath, files, indexVariant);
800
1064
  const persistedReason = persistedRows === 0
801
1065
  ? inferZeroRowReason(stash, reason, warnings, dirPath, dedupedRows)
802
1066
  : reason?.kind === "full-rebuild"
@@ -827,12 +1091,13 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots,
827
1091
  }
828
1092
  });
829
1093
  insertTransaction();
1094
+ deleteUsageEventsByEntryIds([...deletedUsageEntryIds]);
830
1095
  return { dirsNeedingLlm };
831
1096
  }
832
- async function indexEntries(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, doFullDelete = false, onProgress) {
1097
+ async function indexEntries(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, doFullDelete = false, onProgress, reconcileMissingDirs = true) {
833
1098
  // Phase 1 (async): walk directories and pre-generate all metadata outside the
834
1099
  // transaction.
835
- const { dirRecords, scannedDirs, skippedDirs, generatedCount, warnings } = await scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, onProgress);
1100
+ const { dirRecords, scannedDirs, skippedDirs, generatedCount, warnings, complete } = await scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, onProgress, reconcileMissingDirs);
836
1101
  // Phase 2 (sync): write all pre-generated metadata inside a single transaction.
837
1102
  // Source roots feed the #624-P1 zero-document preflight (a full-rebuild wipe
838
1103
  // is suppressed when the scan is empty because roots are unreadable).
@@ -855,10 +1120,26 @@ async function indexEntries(db, allSourceEntries, isIncremental, builtAtMs, hadR
855
1120
  adapterId: component?.adapter ?? "akm",
856
1121
  });
857
1122
  });
858
- const { dirsNeedingLlm } = persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots, bundleByRoot);
859
- return { scannedDirs, skippedDirs, generatedCount, warnings, dirsNeedingLlm };
1123
+ const { dirsNeedingLlm } = persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots, complete, bundleByRoot);
1124
+ return { scannedDirs, skippedDirs, generatedCount, warnings, dirsNeedingLlm, complete };
860
1125
  }
861
- async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal, reEnrich = false) {
1126
+ function indexedProvenanceForFile(db, filePath) {
1127
+ const row = db
1128
+ .prepare("SELECT item_ref AS itemRef, bundle_id AS bundleId, component_id AS componentId, " +
1129
+ "concept_id AS conceptId, adapter_id AS adapterId FROM entries WHERE file_path = ? LIMIT 1")
1130
+ .get(filePath);
1131
+ if (!row?.itemRef || !row.bundleId || !row.componentId || !row.conceptId || !row.adapterId) {
1132
+ throw new Error(`Missing indexed provenance for ${filePath}`);
1133
+ }
1134
+ return {
1135
+ itemRef: row.itemRef,
1136
+ bundleId: row.bundleId,
1137
+ componentId: row.componentId,
1138
+ conceptId: row.conceptId,
1139
+ adapterId: row.adapterId,
1140
+ };
1141
+ }
1142
+ async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal) {
862
1143
  // Resolve per-pass LLM config via the unified shim. Returns undefined when
863
1144
  // either no `akm.llm` is configured or the user opted this pass out via
864
1145
  // `index.enrichment.llm = false`. (#208)
@@ -874,17 +1155,18 @@ async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal
874
1155
  const totalDirs = dirsNeedingLlm.length;
875
1156
  const totalEntries = dirsNeedingLlm.reduce((sum, { stash }) => {
876
1157
  const entriesToEnhance = stash.entries.filter((e) => {
877
- if (e.quality !== "generated" && !(reEnrich && e.quality === "enriched"))
1158
+ if (e.quality !== "generated")
878
1159
  return false;
879
- if (!reEnrich && isEnrichmentComplete(e))
1160
+ if (isEnrichmentComplete(e))
880
1161
  return false;
881
1162
  return true;
882
1163
  });
883
1164
  return sum + entriesToEnhance.length;
884
1165
  }, 0);
885
- // P3 — wall-clock budget for the enrichment pass. Defaults to llm.timeoutMs
886
- // (or 10 minutes if not set). Users can extend this via llm.timeoutMs in
887
- // config no separate knob needed.
1166
+ // P3 — wall-clock budget for the enrichment pass. Defaults to the resolved
1167
+ // engine's timeoutMs (or 10 minutes if not set). Users can extend it via
1168
+ // `index.enrichment.timeoutMs` (or `index.defaults.timeoutMs`, or the
1169
+ // engine's own `engines.<name>.timeoutMs`) — no separate knob needed.
888
1170
  const enrichDeadline = createEnrichmentDeadline(llmConfig.timeoutMs, totalEntries);
889
1171
  let deadlineHit = false;
890
1172
  const enrichSignal = (() => {
@@ -933,14 +1215,13 @@ async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal
933
1215
  await concurrentMap(dirsNeedingLlm, async ({ dirPath, files, currentStashDir, stash: originalStash }) => {
934
1216
  if (enrichSignal.aborted)
935
1217
  return undefined;
936
- // Only enhance generated entries (or all when reEnrich=true);
937
- // user-provided overrides should not be overwritten.
938
- // Skip entries that are already fully enriched (description + tags + searchHints)
939
- // unless the caller explicitly requests re-enrichment via reEnrich=true.
1218
+ // Only enhance generated entries; user-provided overrides should not
1219
+ // be overwritten. Skip entries that are already fully enriched
1220
+ // (description + tags + searchHints).
940
1221
  const entriesToEnhance = originalStash.entries.filter((e) => {
941
- if (e.quality !== "generated" && !(reEnrich && e.quality === "enriched"))
1222
+ if (e.quality !== "generated")
942
1223
  return false;
943
- if (!reEnrich && isEnrichmentComplete(e)) {
1224
+ if (isEnrichmentComplete(e)) {
944
1225
  warnVerbose(`[akm] skipping LLM enrichment for "${e.name}" — entry already complete`);
945
1226
  return false;
946
1227
  }
@@ -959,7 +1240,7 @@ async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal
959
1240
  lastProgressAt = Date.now();
960
1241
  const targetStash = { entries: entriesToEnhance };
961
1242
  const entryKeys = entriesToEnhance.map((e) => `${currentStashDir}:${e.type}:${e.name}`);
962
- const enhanced = await enhanceStashWithLlm(llmConfig, targetStash, files, summary, enrichSignal, db, entryKeys, reEnrich, config, (event) => {
1243
+ const enhanced = await enhanceStashWithLlm(llmConfig, targetStash, files, summary, enrichSignal, db, entryKeys, config, (event) => {
963
1244
  completedEntries++;
964
1245
  lastProgressAt = Date.now();
965
1246
  onProgress?.({
@@ -980,7 +1261,8 @@ async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal
980
1261
  const entryPath = entry.filename ? path.join(dirPath, entry.filename) : files[0] || dirPath;
981
1262
  const entryKey = `${currentStashDir}:${entry.type}:${entry.name}`;
982
1263
  const searchText = buildSearchText(entry);
983
- upsertEntry(db, entryKey, dirPath, entryPath, currentStashDir, attachFileSize(entry, entryPath), searchText);
1264
+ const provenance = indexedProvenanceForFile(db, entryPath);
1265
+ upsertEntry(db, entryKey, dirPath, entryPath, currentStashDir, attachFileSize(entry, entryPath), searchText, provenance);
984
1266
  }
985
1267
  })();
986
1268
  completedDirs++;
@@ -996,7 +1278,8 @@ async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal
996
1278
  },
997
1279
  // Defaults: 2 for remote LLM APIs, 1 for local model servers (LM
998
1280
  // Studio, Ollama run one inference at a time — parallel requests cause
999
- // "Model reloaded" / 500 errors). `llm.concurrency` overrides.
1281
+ // "Model reloaded" / 500 errors). No config override reaches this path:
1282
+ // `resolveLlmEngineUse` does not forward `engines.<name>.concurrency`.
1000
1283
  getDefaultLlmConcurrency(llmConfig));
1001
1284
  }
1002
1285
  finally {
@@ -1004,7 +1287,7 @@ async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal
1004
1287
  clearInterval(heartbeatTimer);
1005
1288
  }
1006
1289
  if (deadlineHit) {
1007
- warn("[akm] LLM enrichment budget exceeded. Re-run `akm index` to continue. Increase llm.timeoutMs for a larger budget.");
1290
+ warn("[akm] LLM enrichment budget exceeded. Re-run `akm index` to continue. Increase index.enrichment.timeoutMs for a larger budget.");
1008
1291
  }
1009
1292
  // Gate-closed (`skipped`) entries are not failures — exclude them so a
1010
1293
  // deliberately disabled feature never surfaces as an enrichment error.
@@ -1186,12 +1469,23 @@ function verifyIndexState(db, config, embeddableEntries, embeddingResult) {
1186
1469
  };
1187
1470
  }
1188
1471
  if (embeddingCount >= embeddableEntries) {
1472
+ // "ready-vec" must reflect the path search will ACTUALLY take: the vec
1473
+ // extension being loaded is not enough when the embedding phase recorded
1474
+ // fast-path insert failures (searchVec then routes to the JS-cosine
1475
+ // fallback via isVecFastPathReady). Reporting vec health from
1476
+ // isVecAvailable alone overstated `akm info` after partial vec failures
1477
+ // (§24.2 "Semantic" gate — truthful ready-vec).
1478
+ const vecActive = vecAvailable && isVecFastPathReady(db);
1189
1479
  return {
1190
1480
  ok: true,
1191
- message: `Semantic search ready (${embeddingCount}/${embeddableEntries} embeddings, ${vecAvailable ? "sqlite-vec active" : "JS fallback active"}).`,
1481
+ message: `Semantic search ready (${embeddingCount}/${embeddableEntries} embeddings, ${vecActive
1482
+ ? "sqlite-vec active"
1483
+ : vecAvailable
1484
+ ? "JS fallback active — vec fast path degraded, run 'akm index --full' to restore"
1485
+ : "JS fallback active"}).`,
1192
1486
  semanticSearchEnabled: true,
1193
1487
  semanticSearchMode: config.semanticSearchMode,
1194
- semanticStatus: vecAvailable ? "ready-vec" : "ready-js",
1488
+ semanticStatus: vecActive ? "ready-vec" : "ready-js",
1195
1489
  embeddingProvider,
1196
1490
  entryCount: embeddableEntries,
1197
1491
  embeddingCount,
@@ -1220,18 +1514,14 @@ function buildIndexedDirCandidate(dirPath, indexableFiles, generated) {
1220
1514
  return { stash, staleFiles };
1221
1515
  }
1222
1516
  function resolveIndexedFiles(dirPath, files, stash) {
1223
- const fileBasenameMap = buildFileBasenameMap(files);
1224
1517
  const resolved = new Set();
1225
1518
  for (const entry of stash.entries) {
1226
- const entryPath = entry.filename
1227
- ? path.join(dirPath, entry.filename)
1228
- : matchEntryToFile(entry.name, fileBasenameMap);
1229
- if (entryPath)
1230
- resolved.add(entryPath);
1519
+ if (entry.filename)
1520
+ resolved.add(path.join(dirPath, entry.filename));
1231
1521
  }
1232
1522
  return resolved.size > 0 ? [...resolved] : files;
1233
1523
  }
1234
- async function enhanceStashWithLlm(llmConfig, stash, files, summary, signal, db, entryKeys, reEnrich, akmConfig, onEntryDone) {
1524
+ async function enhanceStashWithLlm(llmConfig, stash, files, summary, signal, db, entryKeys, akmConfig, onEntryDone) {
1235
1525
  const { enhanceMetadata } = await import("../llm/metadata-enhance.js");
1236
1526
  const { computeBodyHash, getLlmCacheEntry, upsertLlmCacheEntry } = await import("../storage/repositories/index-llm-cache-repository.js");
1237
1527
  const results = await concurrentMap(stash.entries, async (entry, idx) => {
@@ -1251,13 +1541,13 @@ async function enhanceStashWithLlm(llmConfig, stash, files, summary, signal, db,
1251
1541
  warn(`Could not read file for LLM enrichment: ${entry.filename ?? entry.name}`);
1252
1542
  }
1253
1543
  }
1254
- // Incremental cache: skip LLM call when file body is unchanged and
1255
- // --re-enrich was not requested. The cache key is the entry_key
1256
- // (stashDir:type:name) which is stable across index runs.
1544
+ // Incremental cache: skip LLM call when file body is unchanged. The
1545
+ // cache key is the entry_key (stashDir:type:name) which is stable
1546
+ // across index runs.
1257
1547
  const cacheBody = fileContent ?? `${entry.name}\n${entry.description ?? ""}`;
1258
1548
  const bodyHash = computeBodyHash(cacheBody);
1259
1549
  const cacheKey = entryKeys?.[idx] ?? `${entry.type}:${entry.name}`;
1260
- if (db && !reEnrich) {
1550
+ if (db) {
1261
1551
  const cached = getLlmCacheEntry(db, cacheKey, bodyHash);
1262
1552
  if (cached) {
1263
1553
  try {
@@ -1337,8 +1627,8 @@ async function enhanceStashWithLlm(llmConfig, stash, files, summary, signal, db,
1337
1627
  return entry;
1338
1628
  }
1339
1629
  },
1340
- // Defaults: 2 for remote LLM APIs, 1 for local model servers.
1341
- // `llm.concurrency` in config.json overrides.
1630
+ // Defaults: 2 for remote LLM APIs, 1 for local model servers. No config
1631
+ // override reaches this path (see getDefaultLlmConcurrency).
1342
1632
  getDefaultLlmConcurrency(llmConfig));
1343
1633
  // concurrentMap returns Array<T | undefined>; filter out undefined slots
1344
1634
  // (which can only occur if the callback itself returned undefined, which
@@ -1346,41 +1636,68 @@ async function enhanceStashWithLlm(llmConfig, stash, files, summary, signal, db,
1346
1636
  const enhanced = results.map((r, i) => r ?? stash.entries[i]);
1347
1637
  return { entries: enhanced };
1348
1638
  }
1349
- /**
1350
- * Build a map from base filename (without extension) to full path for quick lookups.
1351
- */
1352
- export function buildFileBasenameMap(files) {
1353
- const map = new Map();
1354
- for (const file of files) {
1355
- const base = path.basename(file, path.extname(file));
1356
- // Only keep first match per base name to avoid ambiguity
1357
- if (!map.has(base))
1358
- map.set(base, file);
1359
- }
1360
- return map;
1639
+ // ── lookup ─────────────────────────────────────────────────────────────────
1640
+ import { makeBundleRef } from "../core/asset/asset-ref.js";
1641
+ import { conceptIdFromTypeName } from "../core/asset/resolve-ref.js";
1642
+ async function resolveLookupSources() {
1643
+ const { loadConfig } = await import("../core/config/config.js");
1644
+ const { resolveSourceEntries } = await import("./search/search-source.js");
1645
+ return resolveSourceEntries(undefined, loadConfig());
1361
1646
  }
1362
- /**
1363
- * Try to match a filename-less entry to an actual file in the directory.
1364
- *
1365
- * Matching strategy (in priority order):
1366
- * 1. Exact basename match: entry.name === filename without extension
1367
- * 2. Last path segment match: for entries with names like "dir/sub-entry",
1368
- * try matching the last segment
1369
- * 3. No implicit file fallback: ambiguous legacy entries are skipped
1370
- */
1371
- export function matchEntryToFile(entryName, fileMap) {
1372
- // Exact match on entry name
1373
- const exact = fileMap.get(entryName);
1374
- if (exact)
1375
- return exact;
1376
- // Try last segment for hierarchical names (e.g. "corpus/agentic-patterns/foo")
1377
- const lastSegment = entryName.split("/").pop() ?? entryName;
1378
- if (lastSegment !== entryName) {
1379
- const segmentMatch = fileMap.get(lastSegment);
1380
- if (segmentMatch)
1381
- return segmentMatch;
1647
+ function resolveLookupScope(bundle, sources) {
1648
+ if (!bundle)
1649
+ return { candidateDirs: sources.map((source) => source.path), qualified: false };
1650
+ return { candidateDirs: resolveSourcesForOrigin(bundle, sources).map((source) => source.path), qualified: true };
1651
+ }
1652
+ /** Resolve an adapter-owned `[bundle//]conceptId` without interpreting its path as an AKM type. */
1653
+ export async function lookupBundleRef(ref) {
1654
+ const sources = await resolveLookupSources();
1655
+ if (sources.length === 0)
1656
+ return null;
1657
+ const { candidateDirs, qualified } = resolveLookupScope(ref.bundle, sources);
1658
+ if (candidateDirs.length === 0)
1659
+ return null;
1660
+ const db = openExistingDatabase(getDbPath());
1661
+ try {
1662
+ const inputRef = makeBundleRef(qualified ? ref.bundle : undefined, ref.conceptId);
1663
+ for (const dir of candidateDirs) {
1664
+ const id = findEntryIdByRef(db, inputRef, dir);
1665
+ if (id === undefined)
1666
+ continue;
1667
+ const row = db
1668
+ .prepare("SELECT entry_key AS entryKey, file_path AS filePath, stash_dir AS stashDir, entry_type AS type, " +
1669
+ "entry_json AS entryJson, item_ref AS itemRef, bundle_id AS bundleId, concept_id AS conceptId, " +
1670
+ "adapter_id AS adapterId FROM entries WHERE id = ?")
1671
+ .get(id);
1672
+ if (!row)
1673
+ continue;
1674
+ let document;
1675
+ try {
1676
+ document = JSON.parse(row.entryJson);
1677
+ }
1678
+ catch {
1679
+ // Corrupt optional projection does not erase the durable path identity.
1680
+ }
1681
+ if (!row.itemRef || !row.bundleId || !row.conceptId || !row.adapterId)
1682
+ continue;
1683
+ return {
1684
+ entryKey: row.entryKey,
1685
+ filePath: row.filePath,
1686
+ stashDir: row.stashDir,
1687
+ type: row.type,
1688
+ name: document?.name ?? ref.conceptId.split("/").pop() ?? ref.conceptId,
1689
+ adapterId: row.adapterId,
1690
+ document,
1691
+ itemRef: row.itemRef,
1692
+ bundleId: row.bundleId,
1693
+ conceptId: row.conceptId,
1694
+ };
1695
+ }
1696
+ return null;
1697
+ }
1698
+ finally {
1699
+ closeDatabase(db);
1382
1700
  }
1383
- return null;
1384
1701
  }
1385
1702
  /**
1386
1703
  * Look up a single asset by ref. Spec §6.2 — `akm show` queries this and
@@ -1388,70 +1705,11 @@ export function matchEntryToFile(entryName, fileMap) {
1388
1705
  * file corresponds to which ref; the indexer walks `provider.path()` for
1389
1706
  * every configured source, so this query covers all source kinds.
1390
1707
  *
1391
- * Match rules:
1392
- * - `ref.origin === undefined` → first match across all sources (primary
1393
- * source first, then in declared order — same priority as the indexer's
1394
- * write order).
1395
- * - `ref.origin === "local"` → primary source only (entry_key prefix is
1396
- * the primary stash dir).
1397
- * - `ref.origin === <name>` → restrict to the matching source name. We
1398
- * resolve the source's directory and match on `entry_key` prefix.
1399
- *
1400
1708
  * Returns `null` when no row matches — callers translate that into a
1401
1709
  * `NotFoundError` with their own messaging.
1402
1710
  */
1403
1711
  export async function lookup(ref) {
1404
- const { loadConfig } = await import("../core/config/config.js");
1405
- const { resolveSourceEntries } = await import("./search/search-source.js");
1406
- const config = loadConfig();
1407
- const sources = resolveSourceEntries(undefined, config);
1408
- if (sources.length === 0)
1409
- return null;
1410
- const dbPath = getDbPath();
1411
- const db = openExistingDatabase(dbPath);
1412
- try {
1413
- const escapeLike = (value) => value.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
1414
- // Canonical names strip .md for markdown assets, but users often pass
1415
- // refs with .md (e.g. command:release.md). Normalize by trying both.
1416
- const nameVariants = [ref.name];
1417
- if (ref.name.endsWith(".md")) {
1418
- nameVariants.push(ref.name.slice(0, -3));
1419
- }
1420
- const candidateDirs = (() => {
1421
- if (!ref.origin)
1422
- return sources.map((s) => s.path);
1423
- if (ref.origin === "local")
1424
- return [sources[0].path];
1425
- const named = sources.find((s) => s.registryId === ref.origin);
1426
- return named ? [named.path] : [];
1427
- })();
1428
- if (candidateDirs.length === 0)
1429
- return null;
1430
- for (const name of nameVariants) {
1431
- const suffix = `:${ref.type}:${name}`;
1432
- const escapedSuffix = escapeLike(suffix);
1433
- for (const dir of candidateDirs) {
1434
- const escapedDir = escapeLike(dir);
1435
- const row = db
1436
- .prepare("SELECT entry_key AS entryKey, file_path AS filePath, stash_dir AS stashDir, entry_type AS type FROM entries " +
1437
- "WHERE entry_key LIKE ? ESCAPE '\\' AND entry_type = ? LIMIT 1")
1438
- .get(`${escapedDir}${escapedSuffix}`, ref.type);
1439
- if (row) {
1440
- return {
1441
- entryKey: row.entryKey,
1442
- filePath: row.filePath,
1443
- stashDir: row.stashDir,
1444
- type: row.type,
1445
- name: ref.name,
1446
- };
1447
- }
1448
- }
1449
- }
1450
- return null;
1451
- }
1452
- finally {
1453
- closeDatabase(db);
1454
- }
1712
+ return lookupBundleRef({ bundle: ref.origin, conceptId: conceptIdFromTypeName(ref.type, ref.name) });
1455
1713
  }
1456
1714
  // ── Utility score recomputation ──────────────────────────────────────────────
1457
1715
  /** Retention window for usage events: events older than this are purged. */