akm-cli 0.9.16 → 0.9.17-alpha.10

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 (403) hide show
  1. package/CHANGELOG.md +2101 -0
  2. package/STABILITY.md +11 -10
  3. package/dist/akm +124 -193
  4. package/dist/akm-migrate +38 -19
  5. package/dist/assets/hints/cli-hints-full.md +6 -7
  6. package/dist/assets/improve-strategies/catchup.json +0 -3
  7. package/dist/assets/improve-strategies/consolidate.json +0 -1
  8. package/dist/assets/improve-strategies/default.json +1 -2
  9. package/dist/assets/improve-strategies/proactive-maintenance.json +1 -2
  10. package/dist/assets/improve-strategies/quick.json +1 -2
  11. package/dist/assets/improve-strategies/reflect-distill.json +1 -2
  12. package/dist/assets/improve-strategies/thorough.json +0 -3
  13. package/dist/assets/prompts/consolidate-pair.md +20 -0
  14. package/dist/assets/prompts/consolidate-system.md +4 -11
  15. package/dist/assets/prompts/retrieval-relevance-judge.md +6 -0
  16. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +20 -20
  17. package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -2
  18. package/dist/assets/templates/html/health.html +3 -5
  19. package/dist/cli/retired-commands.js +1 -1
  20. package/dist/cli/shared.js +6 -2
  21. package/dist/cli/unknown-flags.js +24 -1
  22. package/dist/cli.js +68 -10
  23. package/dist/commands/agent/agent-dispatch.js +1 -1
  24. package/dist/commands/command/command-execution.js +24 -62
  25. package/dist/commands/feedback-cli.js +0 -1
  26. package/dist/commands/health/accept-rate.js +6 -0
  27. package/dist/commands/health/archive-usage.js +92 -0
  28. package/dist/commands/health/checks.js +83 -74
  29. package/dist/commands/health/config-skew.js +38 -0
  30. package/dist/commands/health/data-dir-usage.js +25 -13
  31. package/dist/commands/health/egress.js +54 -0
  32. package/dist/commands/health/html-report.js +1 -42
  33. package/dist/commands/health/improve-metrics.js +136 -591
  34. package/dist/commands/health/md-report.js +1 -6
  35. package/dist/commands/health/plugin-staleness.js +53 -3
  36. package/dist/commands/health/renderers.js +12 -4
  37. package/dist/commands/health/report-view-model.js +14 -120
  38. package/dist/commands/health/types-improve.js +4 -19
  39. package/dist/commands/health/windows.js +64 -74
  40. package/dist/commands/health.js +145 -143
  41. package/dist/commands/improve/consolidate/chunking.js +26 -117
  42. package/dist/commands/improve/consolidate/continuity-check.js +137 -0
  43. package/dist/commands/improve/consolidate/pair-pass.js +791 -0
  44. package/dist/commands/improve/consolidate/sanitize.js +54 -149
  45. package/dist/commands/improve/consolidate.js +589 -1127
  46. package/dist/commands/improve/content-hash.js +16 -24
  47. package/dist/commands/improve/distill/content-repair.js +18 -100
  48. package/dist/commands/improve/distill-guards.js +20 -81
  49. package/dist/commands/improve/distill-promotion-policy.js +23 -243
  50. package/dist/commands/improve/distill.js +608 -1041
  51. package/dist/commands/improve/eligibility.js +126 -390
  52. package/dist/commands/improve/execution.js +8 -10
  53. package/dist/commands/improve/extract-prompt.js +1 -2
  54. package/dist/commands/improve/extract.js +487 -1046
  55. package/dist/commands/improve/feedback-valence.js +0 -25
  56. package/dist/commands/improve/improve-cli.js +75 -169
  57. package/dist/commands/improve/improve-result-file.js +10 -66
  58. package/dist/commands/improve/improve-strategies.js +52 -4
  59. package/dist/commands/improve/improve-usage-report.js +18 -64
  60. package/dist/commands/improve/improve.js +480 -1074
  61. package/dist/commands/improve/ledger.js +119 -0
  62. package/dist/commands/improve/locks.js +2 -8
  63. package/dist/commands/improve/loop-stages.js +415 -1073
  64. package/dist/commands/improve/memory/derived-ref.js +12 -77
  65. package/dist/commands/improve/memory/memory-belief.js +16 -118
  66. package/dist/commands/improve/memory/memory-improve.js +266 -14
  67. package/dist/commands/improve/outcome-loop.js +28 -156
  68. package/dist/commands/improve/planner.js +5 -15
  69. package/dist/commands/improve/preparation.js +779 -2319
  70. package/dist/commands/improve/proactive-maintenance.js +34 -101
  71. package/dist/commands/improve/reflect-noise.js +104 -280
  72. package/dist/commands/improve/reflect.js +642 -1353
  73. package/dist/commands/improve/retrieval-gate.js +127 -0
  74. package/dist/commands/improve/retrieval-scope.js +92 -0
  75. package/dist/commands/improve/salience.js +41 -240
  76. package/dist/commands/improve/session-asset.js +19 -100
  77. package/dist/commands/improve/stage.js +322 -0
  78. package/dist/commands/lint/base-linter.js +37 -15
  79. package/dist/commands/proposal/drain.js +261 -578
  80. package/dist/commands/proposal/proposal-cli.js +19 -20
  81. package/dist/commands/proposal/proposal-types.js +31 -24
  82. package/dist/commands/proposal/proposal.js +38 -8
  83. package/dist/commands/proposal/propose.js +134 -160
  84. package/dist/commands/proposal/repository.js +1097 -1394
  85. package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
  86. package/dist/commands/proposal/validators/proposal-validators.js +1 -1
  87. package/dist/commands/proposal/validators/proposals.js +22 -89
  88. package/dist/commands/read/curate.js +105 -462
  89. package/dist/commands/read/knowledge.js +3 -2
  90. package/dist/commands/read/search-cli.js +16 -33
  91. package/dist/commands/read/search.js +17 -23
  92. package/dist/commands/read/show.js +57 -108
  93. package/dist/commands/sources/bundle-cli.js +25 -2
  94. package/dist/commands/sources/bundle-config-ops.js +4 -0
  95. package/dist/commands/sources/dangerous-env-audit.js +1 -2
  96. package/dist/commands/sources/info.js +127 -29
  97. package/dist/commands/sources/installed-stashes.js +197 -746
  98. package/dist/commands/sources/schema-repair.js +98 -129
  99. package/dist/commands/sources/source-add.js +62 -12
  100. package/dist/commands/sources/source-manage.js +9 -2
  101. package/dist/commands/sources/stash-cli.js +24 -4
  102. package/dist/commands/tasks/explain.js +10 -13
  103. package/dist/commands/tasks/tasks-cli.js +12 -13
  104. package/dist/commands/tasks/tasks.js +350 -936
  105. package/dist/commands/tasks/validate.js +26 -24
  106. package/dist/commands/workflow/plan.js +22 -29
  107. package/dist/commands/workflow-cli.js +4 -4
  108. package/dist/core/adapter/adapters/akm-adapter.js +2 -1
  109. package/dist/core/adapter/adapters/akm-lint.js +2 -3
  110. package/dist/core/adapter/adapters/akm-metadata.js +42 -12
  111. package/dist/core/adapter/adapters/akm-task-adapter.js +29 -8
  112. package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
  113. package/dist/core/adapter/execution-source.js +17 -29
  114. package/dist/core/asset/asset-placement.js +4 -13
  115. package/dist/core/asset/frontmatter.js +106 -1
  116. package/dist/core/asset/resolve-ref.js +1 -1
  117. package/dist/core/bundle-id.js +42 -5
  118. package/dist/core/bundle-rename.js +285 -0
  119. package/dist/core/config/config-io.js +1 -2
  120. package/dist/core/config/config-schema.js +9 -34
  121. package/dist/core/config/config-walker.js +1 -1
  122. package/dist/core/config/config.js +184 -111
  123. package/dist/core/config/engine-semantics.js +0 -2
  124. package/dist/core/config/legacy-source-shape-shim.js +38 -9
  125. package/dist/core/config/schema/embedding.js +20 -5
  126. package/dist/core/config/schema/engines.js +5 -0
  127. package/dist/core/config/schema/execution.js +1 -1
  128. package/dist/core/config/schema/experimental.js +1 -1
  129. package/dist/core/config/schema/improve-processes.js +54 -125
  130. package/dist/core/config/schema/improve.js +4 -42
  131. package/dist/core/config/schema/index-config.js +9 -48
  132. package/dist/core/config/schema/scheduler.js +12 -12
  133. package/dist/core/config/schema/search.js +6 -22
  134. package/dist/core/env-secret-ref.js +0 -1
  135. package/dist/core/errors.js +8 -9
  136. package/dist/core/file-change.js +13 -5
  137. package/dist/core/file-lock.js +76 -173
  138. package/dist/core/improve-result.js +35 -7
  139. package/dist/core/improve-types.js +0 -1
  140. package/dist/core/logs-db.js +2 -2
  141. package/dist/core/loopback.js +7 -12
  142. package/dist/core/non-task-input.js +20 -0
  143. package/dist/core/parse.js +13 -16
  144. package/dist/core/paths.js +0 -24
  145. package/dist/core/redaction.js +109 -2
  146. package/dist/core/run-lock.js +2 -5
  147. package/dist/core/spawn-env.js +1 -1
  148. package/dist/core/state/migrations.js +123 -61
  149. package/dist/core/state-db-scope.js +2 -4
  150. package/dist/core/state-db.js +126 -692
  151. package/dist/core/time.js +0 -20
  152. package/dist/core/type-presentation.js +1 -9
  153. package/dist/core/write-source.js +294 -1005
  154. package/dist/execution/input-contract.js +1 -1
  155. package/dist/execution/resolved-request.js +135 -689
  156. package/dist/execution/source.js +63 -257
  157. package/dist/execution/target-ref.js +1 -1
  158. package/dist/indexer/bundle-identity-guard.js +2 -2
  159. package/dist/indexer/db/llm-cache.js +2 -2
  160. package/dist/indexer/ensure-index.js +77 -73
  161. package/dist/indexer/index-rebuild-lock.js +3 -11
  162. package/dist/indexer/index-writer-lock.js +8 -17
  163. package/dist/indexer/index-written-assets.js +141 -154
  164. package/dist/indexer/indexer.js +400 -1124
  165. package/dist/indexer/links/declared-links.js +90 -0
  166. package/dist/indexer/materialize-embeddings.js +60 -397
  167. package/dist/indexer/passes/memory-inference.js +96 -90
  168. package/dist/indexer/passes/metadata.js +132 -219
  169. package/dist/indexer/read-preflight.js +0 -7
  170. package/dist/indexer/scan/doc-to-entry.js +2 -3
  171. package/dist/indexer/scan/drain-dir.js +1 -1
  172. package/dist/indexer/search/db-search.js +190 -590
  173. package/dist/indexer/search/fts-query.js +30 -41
  174. package/dist/indexer/search/ranking.js +28 -154
  175. package/dist/indexer/search/search-attribution.js +12 -32
  176. package/dist/indexer/search/search-fields.js +11 -15
  177. package/dist/indexer/search/search-hit-enrichers.js +54 -85
  178. package/dist/indexer/search/search-source.js +1 -4
  179. package/dist/indexer/usage/usage-events.js +36 -7
  180. package/dist/indexer/walk/walker.js +3 -4
  181. package/dist/integrations/agent/engine-fallback.js +23 -40
  182. package/dist/integrations/agent/engine-resolution.js +93 -183
  183. package/dist/integrations/agent/execution.js +507 -0
  184. package/dist/integrations/agent/model-map.js +28 -156
  185. package/dist/integrations/agent/request-lowering.js +66 -141
  186. package/dist/integrations/agent/runner-dispatch.js +143 -321
  187. package/dist/integrations/agent/runner.js +54 -14
  188. package/dist/integrations/lockfile.js +53 -101
  189. package/dist/llm/client.js +18 -6
  190. package/dist/llm/embedders/deterministic.js +2 -3
  191. package/dist/llm/embedders/profile.js +71 -0
  192. package/dist/llm/embedders/remote.js +11 -17
  193. package/dist/llm/feature-gate.js +0 -8
  194. package/dist/llm/index-passes.js +3 -5
  195. package/dist/llm/memory-infer.js +1 -2
  196. package/dist/llm/structured-call.js +5 -24
  197. package/dist/output/generic-render.js +23 -11
  198. package/dist/output/html-render.js +13 -10
  199. package/dist/output/render-registry.js +3 -32
  200. package/dist/output/shapes/helpers.js +25 -38
  201. package/dist/output/shapes/passthrough.js +1 -9
  202. package/dist/{indexer/graph/graph-types.js → output/text/bundle-rename.js} +4 -1
  203. package/dist/output/text/command-format.js +69 -31
  204. package/dist/output/text/helpers.js +1 -1
  205. package/dist/output/text/migrate.js +5 -14
  206. package/dist/output/text/proposal-format.js +48 -3
  207. package/dist/output/text/show-format.js +13 -17
  208. package/dist/output/text/workflow-format.js +0 -32
  209. package/dist/output/text.js +2 -0
  210. package/dist/registry/factory.js +4 -19
  211. package/dist/registry/network.js +66 -220
  212. package/dist/registry/providers/index.js +0 -2
  213. package/dist/registry/providers/skills-sh.js +3 -14
  214. package/dist/registry/providers/static-index.js +24 -26
  215. package/dist/registry/resolve.js +55 -131
  216. package/dist/scripts/akm-migrate-node.js +42948 -92369
  217. package/dist/scripts/akm-migrate.js +42935 -92354
  218. package/dist/setup/registry-stash-loader.js +4 -13
  219. package/dist/setup/semantic-assets.js +3 -44
  220. package/dist/setup/setup.js +1 -1
  221. package/dist/setup/steps/connection.js +5 -6
  222. package/dist/setup/steps/platforms.js +2 -2
  223. package/dist/setup/steps/tasks.js +25 -15
  224. package/dist/sources/provider-factory.js +17 -18
  225. package/dist/sources/providers/filesystem.js +2 -3
  226. package/dist/sources/providers/git-install.js +7 -1
  227. package/dist/sources/providers/git-provider.js +0 -3
  228. package/dist/sources/providers/git-stash.js +83 -21
  229. package/dist/sources/providers/npm.js +2 -4
  230. package/dist/sources/providers/provider-utils.js +5 -10
  231. package/dist/sources/providers/website.js +0 -2
  232. package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
  233. package/dist/sources/website-url.js +2 -2
  234. package/dist/storage/database.js +9 -35
  235. package/dist/storage/repositories/improve-ledger-repository.js +209 -0
  236. package/dist/storage/repositories/index-connection.js +39 -72
  237. package/dist/storage/repositories/index-entries-repository.js +131 -129
  238. package/dist/storage/repositories/index-entry-mapper.js +1 -2
  239. package/dist/storage/repositories/index-entry-schema.js +101 -268
  240. package/dist/storage/repositories/index-fts-repository.js +86 -256
  241. package/dist/storage/repositories/index-links-repository.js +143 -0
  242. package/dist/storage/repositories/index-llm-cache-repository.js +7 -9
  243. package/dist/storage/repositories/index-meta-repository.js +6 -4
  244. package/dist/storage/repositories/index-schema.js +257 -325
  245. package/dist/storage/repositories/index-utility-repository.js +8 -29
  246. package/dist/storage/repositories/index-vec-repository.js +133 -414
  247. package/dist/storage/repositories/outcome-repository.js +2 -1
  248. package/dist/storage/repositories/proposals-repository.js +104 -1
  249. package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
  250. package/dist/storage/repositories/salience-repository.js +1 -19
  251. package/dist/storage/repositories/task-history-repository.js +26 -4
  252. package/dist/storage/repositories/workflow-runs-repository.js +53 -244
  253. package/dist/storage/sqlite-migrations.js +136 -0
  254. package/dist/storage/sqlite-pragmas.js +11 -9
  255. package/dist/storage/sqlite-transaction.js +170 -0
  256. package/dist/storage/state-db-integrity.js +130 -0
  257. package/dist/tasks/activation-config.js +134 -62
  258. package/dist/tasks/backends/cron.js +191 -302
  259. package/dist/tasks/backends/exec-utils.js +2 -5
  260. package/dist/tasks/backends/launchd.js +141 -748
  261. package/dist/tasks/backends/schtasks.js +119 -623
  262. package/dist/tasks/prepare/prepare-support.js +5 -15
  263. package/dist/tasks/prepare/prepare.js +0 -2
  264. package/dist/tasks/resolve-akm-bin.js +20 -79
  265. package/dist/tasks/run/attempt-lifecycle.js +0 -1
  266. package/dist/tasks/run/load-task.js +1 -1
  267. package/dist/tasks/scheduler-binding.js +20 -238
  268. package/dist/tasks/scheduler-invocation.js +136 -244
  269. package/dist/tasks/scheduler-lock.js +53 -0
  270. package/dist/tasks/scheduler-sync.js +368 -679
  271. package/dist/tasks/source/parse-task-source.js +55 -9
  272. package/dist/tasks/source/task-source-v3-frozen.js +3 -4
  273. package/dist/tasks/source/task-to-v4.js +464 -88
  274. package/dist/workflows/authoring/authoring.js +3 -12
  275. package/dist/workflows/compile.js +211 -0
  276. package/dist/workflows/concurrency-policy.js +13 -74
  277. package/dist/workflows/exec/child-invocation.js +3 -17
  278. package/dist/workflows/exec/child-workflow.js +32 -141
  279. package/dist/workflows/exec/dispatch-redaction.js +13 -53
  280. package/dist/workflows/exec/environment.js +98 -0
  281. package/dist/workflows/exec/exec-unit.js +33 -140
  282. package/dist/workflows/exec/frozen-judge.js +7 -59
  283. package/dist/workflows/exec/native-executor.js +82 -341
  284. package/dist/workflows/exec/param-secrets.js +29 -47
  285. package/dist/workflows/exec/run-workflow.js +154 -387
  286. package/dist/workflows/exec/scheduler.js +9 -36
  287. package/dist/workflows/exec/step-work.js +127 -430
  288. package/dist/workflows/exec/unit-dispatch.js +11 -63
  289. package/dist/workflows/exec/unit-writer.js +8 -52
  290. package/dist/workflows/exec/worktree.js +39 -273
  291. package/dist/workflows/freeze/child-output-references.js +4 -15
  292. package/dist/workflows/freeze/environment.js +99 -92
  293. package/dist/workflows/freeze/freeze.js +172 -0
  294. package/dist/workflows/freeze/step-values.js +19 -21
  295. package/dist/workflows/freeze/targets/child-workflow.js +23 -92
  296. package/dist/workflows/freeze/targets/command.js +10 -33
  297. package/dist/workflows/freeze/targets/script.js +5 -12
  298. package/dist/workflows/freeze/targets/shell.js +3 -6
  299. package/dist/workflows/freeze/targets/task.js +25 -80
  300. package/dist/workflows/freeze/task-bindings.js +20 -67
  301. package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
  302. package/dist/workflows/ir/params.js +6 -51
  303. package/dist/workflows/ir/plan-hash.js +2 -34
  304. package/dist/workflows/parser.js +140 -43
  305. package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
  306. package/dist/workflows/renderer.js +36 -69
  307. package/dist/workflows/resource-limits.js +12 -120
  308. package/dist/workflows/runtime/agent-identity.js +8 -40
  309. package/dist/workflows/runtime/run-outputs.js +3 -6
  310. package/dist/workflows/runtime/run-plan.js +316 -0
  311. package/dist/workflows/runtime/runs.js +48 -200
  312. package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
  313. package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
  314. package/dist/workflows/validate-summary.js +2 -7
  315. package/docs/integration/bundling-akm.md +49 -42
  316. package/docs/migration/README.md +1 -0
  317. package/docs/migration/release-notes/0.9.17.md +43 -0
  318. package/docs/migration/v0.9.1-to-v0.9.2.md +23 -7
  319. package/docs/reference/cli.md +232 -135
  320. package/docs/reference/configuration.md +71 -57
  321. package/docs/reference/data-and-telemetry.md +20 -21
  322. package/docs/reference/tasks.md +105 -39
  323. package/docs/reference/workflow-schema.md +14 -18
  324. package/docs/reference/workflows.md +6 -9
  325. package/package.json +1 -1
  326. package/schemas/akm-config.json +115 -738
  327. package/schemas/akm-workflow.json +1 -0
  328. package/dist/assets/improve-strategies/graph-refresh.json +0 -15
  329. package/dist/assets/prompts/contradiction-judge.md +0 -33
  330. package/dist/assets/prompts/graph-extract-system.md +0 -1
  331. package/dist/assets/prompts/graph-extract-user-prompt.md +0 -35
  332. package/dist/assets/prompts/metadata-enhance-system.md +0 -1
  333. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +0 -4
  334. package/dist/commands/health/advisories.js +0 -150
  335. package/dist/commands/health/metrics.js +0 -329
  336. package/dist/commands/health/surfaces.js +0 -102
  337. package/dist/commands/improve/anti-collapse.js +0 -83
  338. package/dist/commands/improve/collapse-detector.js +0 -432
  339. package/dist/commands/improve/consolidate/eligibility.js +0 -48
  340. package/dist/commands/improve/consolidate/merge.js +0 -149
  341. package/dist/commands/improve/distill/promote-memory.js +0 -291
  342. package/dist/commands/improve/distill/quality-gate.js +0 -337
  343. package/dist/commands/improve/eval-cases.js +0 -52
  344. package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
  345. package/dist/commands/improve/proposal-envelope.js +0 -31
  346. package/dist/commands/improve/run-context.js +0 -123
  347. package/dist/commands/improve/shared.js +0 -31
  348. package/dist/commands/improve/source-identity.js +0 -28
  349. package/dist/commands/improve/triage.js +0 -96
  350. package/dist/commands/proposal/drain-policies.js +0 -151
  351. package/dist/commands/sources/update-transaction.js +0 -220
  352. package/dist/core/action-contributors.js +0 -28
  353. package/dist/core/config/config-version-shim.js +0 -101
  354. package/dist/core/fs-txn.js +0 -405
  355. package/dist/core/lexical-score.js +0 -25
  356. package/dist/core/maintenance-barrier.js +0 -167
  357. package/dist/execution/executable-identity.js +0 -105
  358. package/dist/execution/guarded-source.js +0 -427
  359. package/dist/indexer/db/graph-db.js +0 -444
  360. package/dist/indexer/graph/graph-boost.js +0 -427
  361. package/dist/indexer/graph/graph-dedup.js +0 -95
  362. package/dist/indexer/graph/graph-extraction.js +0 -1108
  363. package/dist/indexer/search/name-match.js +0 -35
  364. package/dist/indexer/search/ranking-contributors.js +0 -515
  365. package/dist/indexer/search/ranking-types.js +0 -4
  366. package/dist/indexer/walk/project-context.js +0 -192
  367. package/dist/integrations/agent/execution-cascade.js +0 -566
  368. package/dist/integrations/agent/execution-definitions.js +0 -202
  369. package/dist/integrations/agent/execution-lowering.js +0 -841
  370. package/dist/integrations/agent/execution-preparation.js +0 -98
  371. package/dist/integrations/agent/inline-execution.js +0 -74
  372. package/dist/llm/graph-extract.js +0 -728
  373. package/dist/llm/metadata-enhance.js +0 -96
  374. package/dist/registry/create-provider-registry.js +0 -29
  375. package/dist/registry/pinned-request-helper.js +0 -247
  376. package/dist/registry/pinned-transport.js +0 -717
  377. package/dist/sources/providers/index.js +0 -14
  378. package/dist/storage/engines/sqlite-migrations.js +0 -271
  379. package/dist/storage/repositories/canaries-repository.js +0 -107
  380. package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
  381. package/dist/storage/repositories/registry-cache.js +0 -113
  382. package/dist/tasks/scheduler-sync-preview.js +0 -52
  383. package/dist/tasks/source/task-to-v3.js +0 -507
  384. package/dist/workflows/freeze/resolve-steps.js +0 -86
  385. package/dist/workflows/freeze/source-freeze.js +0 -64
  386. package/dist/workflows/ir/compile.js +0 -321
  387. package/dist/workflows/ir/environment-v4.js +0 -330
  388. package/dist/workflows/ir/freeze-v4.js +0 -153
  389. package/dist/workflows/ir/schema-v4.js +0 -745
  390. package/dist/workflows/ir/schema.js +0 -354
  391. package/dist/workflows/program/schema.js +0 -77
  392. package/dist/workflows/runtime/checkin.js +0 -57
  393. package/dist/workflows/runtime/plan-classifier.js +0 -196
  394. package/dist/workflows/runtime/unit-checkin.js +0 -45
  395. package/dist/workflows/runtime/unit-phases.js +0 -20
  396. package/dist/workflows/schema.js +0 -4
  397. package/dist/workflows/source-ir/compile.js +0 -200
  398. package/dist/workflows/source-ir/program.js +0 -50
  399. package/dist/workflows/source-ir/result.js +0 -26
  400. package/dist/workflows/source-ir/schema.js +0 -786
  401. package/dist/workflows/source-ir/triggers.js +0 -79
  402. package/dist/workflows/source-ir/uses.js +0 -40
  403. package/dist/workflows/validator.js +0 -60
package/CHANGELOG.md CHANGED
@@ -6,6 +6,2107 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.9.17-alpha.10] - 2026-09-29
10
+
11
+ `akm info` now behaves like a help command. It always prints a report and
12
+ exits 0, naming whatever it could not read: a broken config, a missing
13
+ bundle directory, or an index that another akm process holds or that has a
14
+ newer layout. `akm health` again counts every improve run: 11 of the
15
+ owner's last 55 runs, recorded before `plan.processes` existed, had been
16
+ silently dropped from the health report. A hung test shard now fails within
17
+ 10 minutes and names itself, instead of stalling CI until the job times out.
18
+
19
+ ### Changed
20
+
21
+ - **`scripts/test-unit.sh`/`test-integration.sh` shards fail fast and name
22
+ themselves on a hang.** Each process shard now runs under its own process
23
+ group (`exec setsid`) with a 600s ceiling — well above the ~3-minute CI
24
+ norm for a full shard. A shard still alive past it is killed by process
25
+ group (so a child process it spawned dies too, not just `bun test`
26
+ itself), its log tail is printed so the last test file header shows where
27
+ it hung, and the script fails with a clear message. Previously a hung
28
+ shard blocked `wait` forever, so the only thing that ever stopped it was
29
+ the CI job's own timeout — which kills the whole job and keeps no logs,
30
+ as happened during the alpha.9 release.
31
+
32
+ ### Fixed
33
+
34
+ - **`akm health` silently excluded every improve run recorded before #947
35
+ added `plan.processes`.** `decodeImproveResult` called
36
+ `validateProcessRoutingRows` unconditionally, so a `plan` object with no
37
+ `processes` key — legitimately written by every release before
38
+ 2026-09-09T09:03:19Z — failed decode with "plan.processes must be an
39
+ array" and dropped the run from `--window-compare`, `--group-by run`, and
40
+ the HTML/MD reports. On a real owner `state.db`, 11 of 55 improve runs in
41
+ a 30-day window were affected; all 11 decode cleanly now. `plan.processes`
42
+ is validated only when present, the same guard already used for
43
+ `plan.proactive` and the `retrieval` gate (AGENTS.md "Reading persisted
44
+ data"). Also: a row `akm health` cannot decode — corrupt or otherwise —
45
+ now logs a warning naming the row id and the decode error, instead of
46
+ only incrementing `improve.resultRows.skipped.invalid` with no way to
47
+ tell why.
48
+ - **`akm info` now always reports and exits 0, like a help command.**
49
+ Whatever else is running, and whatever state the config and databases
50
+ are in, it prints its report in every format (and with `--quiet`) and
51
+ names what it could not read:
52
+ - an invalid or unreadable `config.json` is reported in a new
53
+ `configError` field. Every other command still refuses at startup
54
+ (exit 78) before any side effect;
55
+ - a missing or unresolvable bundle directory is reported in
56
+ `bundleDirError`, and a fresh install reports the default location;
57
+ - `index.db` is opened read-only with a bound of about 1.5 s. A
58
+ concurrent `akm index` or `akm improve` writer (which under the DELETE
59
+ journal mode could make `info` wait the full 30 s busy timeout), a
60
+ newer layout, or a corrupt index is reported in
61
+ `indexStats.unavailable` instead of as unexplained zeros. An older
62
+ layout is served as-is, never migrated;
63
+ - each path field degrades on its own, with a last-resort report for
64
+ anything unexpected;
65
+ - an unrecognized flag warns instead of exiting 2, as `akm help` does.
66
+
67
+ ## [0.9.17-alpha.9] - 2026-09-29
68
+
69
+ `akm improve` now forgets, reversibly and under review. A consolidation pair
70
+ pass compares each new or changed memory, flat knowledge file or lesson with
71
+ its nearest neighbours; where an LLM judge calls a pair duplicate, subsumed or
72
+ superseding, it mints a retire proposal that a person reviews (`akm proposal
73
+ list --generator consolidate-pair`). Accepting one archives the older or
74
+ contained copy, and `akm proposal revert` restores it exactly; an accepted
75
+ promotion now retires its source memory, so promotion no longer leaves a
76
+ duplicate. A continuity check flags a retirement whose survivor does not rank
77
+ where the retired asset did for its own past searches, and a flagged proposal
78
+ is never bulk-accepted. Archived files are purged 30 days after retirement,
79
+ only when git holds them unmodified. The per-run forgetting-safety lane, LLM
80
+ metadata enrichment and LLM entity-graph extraction are removed: each measured
81
+ no benefit. Decide pending retire proposals before downgrading to
82
+ 0.9.17-alpha.8.
83
+
84
+ ### Added
85
+
86
+ - **Consolidate pair pass: duplicate, subsumed and superseding memories are
87
+ now retired, review-gated.** A second pass inside `akmConsolidate`,
88
+ alongside the existing promote pass. It walks memory-tier assets (a
89
+ memory, base or `.derived`; a flat `knowledge/` asset; or a lesson) in the
90
+ retrieval scope that are new to the pass or whose body has changed since
91
+ their last full attempt — tracked by content hash, not a time window, so
92
+ an initiator the nightly cap or a pending-proposal collision leaves out
93
+ stays eligible rather than being marked settled, and the pass's own
94
+ ledger rows are never retrieval-scope evidence for the other improve
95
+ lanes — takes each one's nearest neighbours by stored vector (fetching 20,
96
+ keeping the first 5 that clear every filter; same bundle, memory tier
97
+ only — structured knowledge in subfolders is excluded), and judges every
98
+ pair at cosine >= `T_pair` (0.93) with one LLM call using the calibrated
99
+ relation prompt (`src/assets/prompts/consolidate-pair.md`, six labels:
100
+ `duplicate`, `subsumed`, `supersedes`, `contradicts`, `overlap`,
101
+ `unrelated`). An initiator with no prior attempt is held to a higher
102
+ `T_pair` >= 0.95, unless it is new material (git first-added within the
103
+ last 7 days), which judges at the ordinary 0.93. "Older"/"newer" for the
104
+ judge's own A/B labelling comes from one `git log` per run over the
105
+ bundle (first-add time, following renames so a moved or renamed file
106
+ keeps its original date), not frontmatter or file mtime — mtime is only
107
+ the fallback for a file git does not know, or a bundle with no `.git` at
108
+ all. At most 300 pairs are judged a night, admitted a whole initiator at
109
+ a time rather than by flat cosine rank: new-or-changed initiators first,
110
+ then the existing backlog by its own best cosine, each admitted only if
111
+ every one of its candidate pairs fits in what remains of the 300 — so an
112
+ initiator blocked on another pending decision never spends a slot doing
113
+ nothing, and a smaller initiator further down still fits when a larger
114
+ one ahead of it does not. `duplicate`, `subsumed` and `supersedes` mint a
115
+ reviewed `retire` proposal for the losing side (owner-calibrated
116
+ precision 20/22 = 0.91 [0.72, 0.97] against a second-rater baseline of
117
+ 0.17 for `supersedes` alone); `contradicts` is counted but stays a human
118
+ decision, and `overlap`/`unrelated` get no proposal. Guards: never a
119
+ `captureMode: hot` memory, never a `.derived` memory whose parent still
120
+ exists, never a pair where either side already has a pending retire
121
+ proposal (as the retired ref or its successor), and never retiring or
122
+ reusing as a successor an asset already spent earlier in the same run.
123
+ (`src/commands/improve/consolidate/pair-pass.ts`,
124
+ `src/commands/improve/retrieval-scope.ts`,
125
+ `src/storage/repositories/improve-ledger-repository.ts`,
126
+ `src/assets/prompts/consolidate-pair.md`)
127
+ - **Retire proposals.** A pair-pass retire proposal mints under its own
128
+ source, `consolidate-pair` — kept apart from the promote pass's
129
+ `consolidate` proposals, so a bulk `accept`/`reject --generator
130
+ consolidate` never sweeps a retirement, and the reverse; a bare `akm
131
+ proposal accept <ref>` never resolves to one either (it matches the
132
+ newest non-retire proposal for the ref, if any — a retire is reached by
133
+ its own proposal id, or the bulk `--generator consolidate-pair` form),
134
+ and retention expiry never drops a pending one for age alone. A pair the owner
135
+ rejected or reverted is not proposed again while both sides are unchanged. Its primary
136
+ change deletes its target instead of writing content. Accepting one first
137
+ confirms the decision is still fresh — the successor still exists, and
138
+ both sides' recorded body hashes still match their current files, not
139
+ just the retired side's, so a decision a later accept elsewhere made
140
+ stale (an A->B/B->C chain, or A->B/B->A both minted) is refused cleanly
141
+ rather than partially applied — then archives the asset (and its
142
+ `.derived` twin, if one exists) through a generalized
143
+ `archiveCleanupCandidate` (now usable on any memory, knowledge or lesson
144
+ file, not only `.derived` memories): the same
145
+ `.akm/memory-cleanup/archive/` encoding memory cleanup already used,
146
+ never the dead `.akm/archive/`. A `supersedes` judgement first writes the
147
+ `supersededBy` edge on the older asset, then archives it. Triage never
148
+ auto-accepts a retire proposal, whatever `applyMode` says — it waits for
149
+ a direct `akm proposal accept`, reviewed the same way as any other
150
+ proposal (`akm proposal list`, `show`, `diff`, bulk `accept --generator
151
+ consolidate-pair`; `--max-diff-lines` counts a retire by its target's own
152
+ line count). `accept` is crash-safe: it records its full intent —
153
+ `backupContent` and which file is about to move — durably before moving
154
+ anything, so a crash partway through, including between a primary and its
155
+ `.derived` twin, resumes and finishes from what was recorded rather than
156
+ leaving an asset stranded or the decision unrecorded. `revert` needs no
157
+ intent of its own — it resumes from what `accept` already recorded, and
158
+ refuses instead of overwriting a path that was reused by an unrelated file
159
+ since (its current content no longer matching what was retired). `revert`
160
+ restores the archived file(s) byte-for-byte from the bytes recorded at
161
+ accept, even a file that had no trailing newline — YAML comments, key
162
+ order and any human-written edge all survive the round trip. A ref to a retired asset keeps resolving to its tombstone
163
+ (`isArchivedRelPath`) — fixed along the way: that resolver assumed only
164
+ memories are ever archived, so an xref to a retired knowledge or lesson
165
+ asset was wrongly reported `missing-ref` by `akm lint` until now. Pending
166
+ retire proposals must be accepted or rejected before downgrading to
167
+ 0.9.17-alpha.8 or earlier — that release predates the retire shape
168
+ entirely and exits 70 on one in `show`/`diff`/`drain`, and drain's own
169
+ nightly pre-pass failing on the first one it meets stops that run's
170
+ auto-promotion for the whole stash. Downgrading also revives the
171
+ new-material starvation this same branch fixed forward-only: 0.9.17-alpha.8
172
+ counts a pair-pass ledger row as retrieval-scope evidence again, so its own
173
+ nightly attempts crowd new material back out of every other improve lane —
174
+ measured, two nights left only 341 of 2,183 new-only assets still in scope
175
+ once read under alpha.8
176
+ (`docs/architecture/persisted-data-compat.md`).
177
+ (`src/commands/proposal/repository.ts`,
178
+ `src/commands/improve/memory/memory-improve.ts`,
179
+ `src/commands/lint/base-linter.ts`)
180
+ - **A promotion retires its source memory (O1).** When `akm proposal accept`
181
+ promotes a consolidate `promote` proposal — by a person or by triage
182
+ auto-promotion — it now archives the source memory (and its `.derived`
183
+ twin) through the same retire-archive path, tombstoned `reason: promoted`,
184
+ provided the source's body still matches the hash recorded when the
185
+ promotion was minted; an edit since then leaves the source alone (a
186
+ proposal minted before this hash existed is never archived, for the same
187
+ reason). A promotion no longer leaves a memory/knowledge duplicate behind.
188
+ Best-effort: a failure to archive the source only warns; the promotion
189
+ itself is not undone. (`src/commands/improve/consolidate.ts`,
190
+ `src/commands/proposal/repository.ts`)
191
+ - **Retirement continuity check (rule R3).** Before the pair pass mints a
192
+ `retire` proposal, it replays up to five of the retired asset's own past
193
+ `search`/`curate` queries through akm's own search, in-process — the
194
+ ranking a user actually gets, no LLM. For every query where the retired
195
+ asset ranked in the top 10, the successor must too; compared directly,
196
+ since search itself returns at most the top 10 hits. A failing
197
+ query never blocks the mint — the
198
+ proposal's `retirement.continuityRisk` records the failing query count
199
+ and, per failing query, the retired asset's rank and the successor's
200
+ (`null` when the successor did not rank in the top 10 at all). A proposal
201
+ carrying `continuityRisk` is excluded from every bulk accept path (`accept
202
+ --generator …`, with or without `--yes`) but not from bulk reject —
203
+ declining a flagged proposal is always the safe direction; a person can
204
+ always accept one by id. An asset with no recorded queries is not checked
205
+ at all. A query that never ran (the search call threw) or that fell back
206
+ to keyword-only ranking (an unreachable embedding endpoint, most often)
207
+ is never silently trusted or silently dropped either: it counts as
208
+ "unverified" and, on its own, is enough to flag `continuityRisk` — an
209
+ endpoint outage reads as "risk unknown," never as "no risk found," for
210
+ every proposal checked while it stays down, not just the first. The
211
+ first fallback in a pair-pass run forces every later query in that same
212
+ run to skip the semantic attempt entirely, so a dead endpoint costs one
213
+ failed attempt total, not one per remaining query. Two fixes against false
214
+ flags, measured on a real night-1 admission (300 pairs, 6 flags, 3
215
+ spurious): replayed queries are the same cleaned set the retrieval
216
+ regression gate uses (`loadRetrievalQueries`) — stash-README boilerplate,
217
+ harness/tool envelopes, pastes over 2,000 characters, and near-duplicate
218
+ queries (equal once whitespace is collapsed) are dropped before replay,
219
+ not just capped at five raw entries; and the check does not run at all
220
+ when the retired and successor bodies are content-identical once
221
+ whitespace is collapsed — search's own content-dedupe already hides the
222
+ successor behind the retired asset for every such query, so a "successor
223
+ missing" finding would not be a real risk.
224
+ (`src/commands/improve/consolidate/continuity-check.ts`,
225
+ `src/commands/proposal/proposal-types.ts`,
226
+ `src/commands/proposal/proposal.ts`)
227
+ - **Continuity-risk visibility, and a `--generator` filter for `proposal
228
+ list`.** A retire proposal carrying `retirement.continuityRisk` now
229
+ shows `⚠ continuity-risk` inline in the default `akm proposal list`
230
+ output (and `--format text`), not just in `proposal show`. `proposal
231
+ show`'s text output now lists the actual failing query text and rank per
232
+ query, not just a count, and separately reports `unverifiedQueries` when
233
+ the risk is (also, or only) an unverified query rather than a rank
234
+ failure. `akm proposal accept --generator … --dry-run` (and a real bulk
235
+ run) now reports `skippedForContinuityRisk`, the count of otherwise
236
+ matching proposals excluded specifically for this reason, apart from an
237
+ ordinary `--max-diff-lines`/`--older-than` miss. `akm proposal list` gains
238
+ a `--generator <name>` filter, the same value `accept`/`reject
239
+ --generator` already take, so the (potentially large) backlog of one
240
+ generator's retire proposals can be reviewed as its own list.
241
+ (`src/commands/proposal/proposal.ts`, `src/commands/proposal/proposal-cli.ts`,
242
+ `src/output/text/proposal-format.ts`, `src/output/shapes/helpers.ts`)
243
+ - **Archive purge sweep.** Deterministic, no LLM, run once at the very start
244
+ of every `akm improve` invocation, ahead of index bootstrap and triage.
245
+ For a git-backed bundle, deletes the archived asset file(s) of a
246
+ retirement — never its `cleanup.md` tombstone — once `retiredAt` is more
247
+ than 30 days old (`RETIRE_GRACE_DAYS`) AND every file under that
248
+ retirement's archive directory is git-tracked, clean (`git ls-files` plus
249
+ `git status --porcelain -uall`), and verifiable (`git ls-files -v`: a
250
+ file marked `assume-unchanged` or `skip-worktree` hides its own edits
251
+ from `git status`, so it is never trusted as clean either) — each checked
252
+ once per sweep; git history keeps the bytes. `.git` presence alone is not
253
+ enough: `proposal accept` only commits for a `kind: "git"` write target,
254
+ and improve's own auto-sync stages only the paths its own run wrote, so a
255
+ filesystem-kind bundle can carry archived retirements that were never
256
+ committed — the tracked/clean/verifiable check is what keeps the sweep
257
+ from deleting the only surviving copy of those. A directory with even one
258
+ untracked, modified, or unverifiable file (tombstone included) is left
259
+ whole for a later sweep — and so is the ENTIRE archive for that sweep if
260
+ the underlying `git status` or `git ls-files` call itself fails (a broken
261
+ submodule, for instance, can fail `git status` while `git ls-files`
262
+ still succeeds): an empty result from a failed check is never treated as
263
+ "nothing to protect", and the sweep warns once rather than silently
264
+ purging nothing. A memory-cleanup family-prune archive carries no
265
+ `retiredAt`, so this sweep never touches that older archive class. Every
266
+ deleted file is journaled individually, so the end-of-run auto-sync
267
+ commits the removal the same way it commits the archive move itself.
268
+ (`src/commands/improve/memory/memory-improve.ts`,
269
+ `src/sources/providers/git-stash.ts`, `src/commands/improve/improve.ts`)
270
+ - **`akm health`'s `memory-cleanup-archive` advisory now covers every
271
+ bundle**, not just one with no `.git` at all. A bundle with no `.git` of
272
+ its own keeps every retirement's archived bytes forever (there is no
273
+ history to fall back on, so the purge sweep never runs there), and its
274
+ size and file count are reported as before. A git-backed bundle can ALSO
275
+ carry archived bytes the purge sweep will never remove — `.git` presence
276
+ alone never proved a retirement was committed — so this now runs the same
277
+ tracked/clean/verifiable check the purge sweep itself uses and reports
278
+ how many files and bytes of the archive cannot currently be purged
279
+ (untracked, modified, or unverifiable), alongside the total. Silent
280
+ whenever there is nothing to say: the archive is empty or absent, or (for
281
+ a git-backed bundle) every byte in it is purgeable once it ages out.
282
+ (`src/commands/health/archive-usage.ts`, `src/commands/health/data-dir-usage.ts`)
283
+
284
+ ### Removed
285
+
286
+ - **LLM metadata enrichment (`index.metadataEnhance`) is retired.** On 49
287
+ stratified queries, with every eligible candidate enriched (1,968
288
+ entries): search nDCG@10 moved −0.0092 [−0.0324, +0.0165], curate P@5
289
+ +0.000 [−0.037, +0.045], and long prompts lost −0.054 [−0.093, −0.012]. A
290
+ Doc2Query-- filter made it worse (P@5 −0.020 [−0.045, −0.004]). The pass
291
+ replaced authored descriptions on 89% of the entries it rewrote, and a
292
+ full pass costs about 27 B70-hours (RS-D, owner ruling 2026-09-28). It was
293
+ already off by default and off in the maintainer's config. The LLM call
294
+ (`src/llm/metadata-enhance.ts`), its `akm index` dispatch, and the
295
+ `metadata_enhance` feature-gate key are gone; the deterministic metadata
296
+ pass, `quality: "generated"`, and memory inference are unaffected. A
297
+ config that still sets `index.metadataEnhance` loads, named once by the
298
+ same unknown-config-key path every other retired key uses: kept in
299
+ memory, round-trips through ordinary writes, and is dropped only by
300
+ `akm migrate apply`. The pass's `llm_enrichment_cache` rows (the default
301
+ `cache_variant`; graph and memory inference use their own named variants)
302
+ are deleted on the next writable open of `index.db`. An index built while
303
+ enrichment was on keeps its entries' LLM-written descriptions on
304
+ incremental runs — nothing rewrites an unchanged row; run
305
+ `akm index --full` once to replace them with the deterministic ones.
306
+ (`src/indexer/indexer.ts`, `src/llm/feature-gate.ts`,
307
+ `src/core/config/config.ts`, `src/core/config/schema/index-config.ts`,
308
+ `src/storage/repositories/index-schema.ts`)
309
+ - **The LLM entity-graph extraction pass.** `akm improve`'s per-file
310
+ entity/relation extraction, its persisted tables (`graph_meta`,
311
+ `graph_files`, `graph_file_entities`, `graph_file_relations`), and `akm
312
+ show`'s `related` list are gone. On the navigation eval, vector kNN beat
313
+ the LLM `related` list by 0.157 P@5 [0.051, 0.260]; the ranking boost it
314
+ once fed was already removed in 0.9.17-alpha.4. Declared links (#935,
315
+ alpha.8) are the only navigation surface `akm show` has now, and curate's
316
+ support refs already came from them, not the graph. index.db is a
317
+ regenerable cache, so the graph tables are dropped unconditionally on the
318
+ next writable open — nothing migrates or backs them up.
319
+ - **The `graph-refresh` improve strategy** and the `akm-graph-refresh-weekly`
320
+ task template are deleted. Naming `graph-refresh` via `--strategy` or a task
321
+ now fails with a message naming the retirement, unconditionally — even when
322
+ `improve.strategies["graph-refresh"]` still has a leftover override block
323
+ from customizing the built-in (the message names it; `akm migrate apply`
324
+ drops it). `defaults.improveStrategy: "graph-refresh"` still loads config
325
+ successfully — the refusal happens lazily, when the strategy is actually
326
+ resolved, not at every command's config load.
327
+ - **Retired config keys:** `index.graph.*` and every strategy's
328
+ `processes.graphExtraction.*`. An old config that still sets them keeps
329
+ loading and the keys are unread, but `index.graph` is now also named once
330
+ by the unknown-config-key warning (it previously validated silently
331
+ against the generic per-pass catchall) and, like any other retired key,
332
+ is dropped only by `akm migrate apply` — not by an ordinary config write.
333
+ - **`akm health` drops every graph metric** — the KPI card, summary-table
334
+ rows, per-run duration/entity/relation columns, and the
335
+ `improve.graphExtraction.failures` window-compare delta. `--window-compare`
336
+ and `--group-by run` still count every run, including a pre-alpha.9 one:
337
+ its stored `graphExtraction`/`graphExtractionDurationMs` result fields and
338
+ its `graph-extraction` `plan.stages` / `graphExtraction` `plan.processes`
339
+ entries still decode, read-only, same as any other retired field (AGENTS.md
340
+ "Reading persisted data") — they are just no longer rendered. The
341
+ `improve_completed` event's `graphExtractionExtractedFiles`,
342
+ `graphExtractionDurationMs`, `graphCoverage`, `graphDensity`, and
343
+ `graphEntities` metadata fields are no longer emitted.
344
+ - **The per-run forgetting-safety lane.** `scoreSalience`'s stash-wide
345
+ salience-rank comparison, `applyForgettingSafety`, and the
346
+ `improve_salience_rank_change` event are gone. It was a one-time WS-1
347
+ cutover guard from the June 2026 ranking-formula change that had kept
348
+ running on every improve run since; the last 30 days of events
349
+ (2026-08-30 to 2026-09-29: 47 `improve_salience_rank_change` events, 5
350
+ refs flagged across 4 runs — 09-05, 09-08 x2, 09-19, 09-28) showed no
351
+ marginal pick over the signal-delta lane and the retrieval scope: 4 of
352
+ the 5 flagged refs were also picked that same run by signal-delta
353
+ (adjacent event ids/timestamps, 2–26 minutes after the rank-change
354
+ event), and the 5th (`workflows/create-github-issues-from-spec`, flagged
355
+ 09-05) has no `reflect_invoked` or `distill_invoked` event in the
356
+ retained history, but that run's `improve_runs.plannedRefs` shows it,
357
+ too, was planned under `signal-delta` — just not reflected (a
358
+ dispatch/budget limit that run, not a lane-exclusive pick). All 5
359
+ flagged refs were signal-delta picks; zero were forgetting-safety-only.
360
+ It also protected
361
+ `asset_salience.rank_score`, which only improve itself ever read — a rank
362
+ drop could not hide anything from search. `buildRankChangeReport` (its
363
+ comparator) is also gone: the new retirement continuity check (see
364
+ Added) compares ranks directly instead, and nothing else called it.
365
+ `forgetting-safety` stays a valid `eligibilitySource`/event-type
366
+ value so old proposals and events still decode, but nothing assigns or
367
+ emits it any more. (`src/commands/improve/preparation.ts`,
368
+ `src/commands/improve/salience.ts`, `src/core/events.ts`,
369
+ `src/storage/repositories/salience-repository.ts`)
370
+ - **`improve.strategies.<name>.processes.consolidate.incrementalSince` and
371
+ `.neighborsPerChanged`.** The consolidate pair pass is now the candidate
372
+ generator, narrowing per initiator through the improve ledger rather than
373
+ a global time window; neither key was set anywhere in the owner's config.
374
+ `narrowToIncrementalCandidates` goes with them, along with its
375
+ now-orphaned `parseSinceToIsoLenient` helper. A config that still sets
376
+ either key keeps loading under the retired-key contract: named once as
377
+ unknown, it survives an ordinary config write, and only `akm migrate
378
+ apply` drops it. (`src/core/config/schema/improve-processes.ts`,
379
+ `src/commands/improve/consolidate.ts`, `src/core/time.ts`,
380
+ `docs/reference/configuration.md`)
381
+
382
+ ### Fixed
383
+
384
+ - **Stale "advisory merge/delete/contradict" documentation.** Consolidation
385
+ stopped executing its merge/delete/contradict operations at `e82eec811`
386
+ (2026-07, #732; they had run in production until then, not "never
387
+ executed" as a couple of doc comments and `improve-workflow.md` claimed),
388
+ and 0.9.17-alpha.1 (`f4ebd763a`) dropped them from the prompt and schema,
389
+ which have offered `promote` only since. But `docs/architecture/
390
+ improvement.md`, `STABILITY.md` and `docs/architecture/internals/
391
+ improve-workflow.md` still described them as advisory planned output.
392
+ Corrected, and `improve-workflow.md` gains a section documenting the pair
393
+ pass, retire proposals and O1. Also corrected: the `default` strategy's
394
+ "advisory consolidation" description, two comments that still credited a
395
+ `beliefState` ranking boost alpha.4 removed (`memory-belief.ts`,
396
+ `knowledge.ts`), and D27's stale `archiveMemory` naming in the
397
+ architecture decision history. Deleted the unused
398
+ `src/assets/prompts/contradiction-judge.md` (no reader since
399
+ `e82eec811`).
400
+
401
+ ## [0.9.17-alpha.8] - 2026-09-28
402
+
403
+ `akm index` now records the links a bundle already declares (`xrefs`,
404
+ `supersededBy`, a `.derived` memory's parent, wiki sources, page links, and
405
+ workflow and task targets) as typed links, with no model. `akm show` lists
406
+ them, and curate's support refs come from them instead of the LLM entity
407
+ graph. The index moves to layout 26 in place on its first writable open;
408
+ 0.9.17-alpha.4 through alpha.7 cannot open it. `index.graph.enabled: false`
409
+ now stops graph extraction in `akm improve`, and a partly failed extraction is
410
+ retried. `akm migrate` converts a v2 or v3 task file to v4 in one pass, and
411
+ the launchers no longer lose a signal that arrives before their child starts.
412
+
413
+ ### Added
414
+
415
+ - **Declared links (#935).** The relations a bundle already declares are now
416
+ stored as typed links: `xrefs:` (`xref`), `supersededBy:`
417
+ (`superseded_by`), `contradictedBy:` (`contradicted_by`),
418
+ `currentBeliefRefs:` (`belief_peer`), a `.derived` memory's parent
419
+ (`derived_from`), wiki `sources:` that name an asset (`cites`), the page
420
+ links an llm-wiki or OKF bundle resolves (`links_to`), and a workflow step's
421
+ or a task's target (`uses`). `akm index` reads them from what it already
422
+ parses, with no model, so an install without an LLM engine gets them. They
423
+ live in their own table (`asset_links`), apart from the LLM entity graph:
424
+ each link belongs to the entry that declares it and is written, replaced
425
+ and deleted with it, including on incremental and write-path (`akm
426
+ remember`) indexing. A target resolves when it is indexed, not when its
427
+ citer was, so a note that cites something added later links to it without
428
+ being reindexed. Retired spellings convert in memory (`memory:<name>`,
429
+ `wiki:<wiki>/<page>`, a `.md` suffix), and, as lint already allows (#882),
430
+ a memory whose own file is gone resolves to its `.derived` child.
431
+ `akm show` lists an asset's links grouped by kind: `outgoing`, `incoming`
432
+ and the `unresolved` tokens it names, at most 10 per kind with a `total`.
433
+ `akm info` reports links per kind with how many are unresolved. Links do
434
+ not change search ranking, and `related` is unchanged: on the retrieval
435
+ suite, against 0.9.17-alpha.7 on the same index with two runs per arm,
436
+ search nDCG@10 moved +0.002 [−0.005, +0.009] (a rerun of alpha.7 alone
437
+ moved +0.006) and curate P@5 +0.000 [−0.001, +0.001].
438
+ On the retrieval snapshot of the maintainer's 21 bundles (23,979 entries)
439
+ there are 14,917 links: 7,393 `contradicted_by`, 4,401 `xref`, 2,577
440
+ `derived_from`, 540 `cites` and 6 `superseded_by`. 1,801 are unresolved;
441
+ 1,744 of those are `.derived` memories whose parent memory no longer
442
+ exists. (`src/indexer/links/declared-links.ts`,
443
+ `src/storage/repositories/index-links-repository.ts`,
444
+ `src/commands/read/show.ts`, `src/commands/sources/info.ts`)
445
+
446
+ ### Changed
447
+
448
+ - **`akm migrate` converts a v2 or v3 task file straight to v4 in one pass.**
449
+ The chain that read every file as v2, converted it to an intermediate v3
450
+ shape, then converted that to v4 (`src/tasks/source/task-to-v3.ts` ->
451
+ `task-to-v4.ts`, composed by `scripts/akm-migrate/migrate/task-files.ts`)
452
+ is now one planner: `task-to-v4.ts` reads a file once and, for v2, builds
453
+ the v3-shape record in memory — never written to disk or reported as its
454
+ own outcome — before hoisting it to v4 through the same code path a real
455
+ v3 file goes through. `akm migrate status`/`apply` output shapes, and
456
+ every blocked/changed reason code, are unchanged, checked fixture by
457
+ fixture against the prior two-hop chain's actual output (one exception:
458
+ a v3 document that fails only the typed pre-check's own "exactly one
459
+ scheduling source" rule — unreachable through the real chain, which
460
+ always ran that same pre-check first — now reports `invalid-v3-task`
461
+ instead of the raw hoist stage's own `ambiguous-scheduling-source`,
462
+ matching what `akm migrate apply` already returned end to end). The
463
+ `already-v3` and `pending-v2-to-v3-migration` intermediate states are
464
+ gone with the generation split that produced them.
465
+ `src/tasks/source/task-to-v3.ts` (500 lines) is deleted; its logic moved
466
+ into `task-to-v4.ts`, which also drops the duplicate raw-YAML reader and
467
+ outcome-base helpers the two files each carried their own copy of.
468
+ (`src/tasks/source/task-to-v4.ts`, `scripts/akm-migrate/migrate/task-files.ts`)
469
+ - **Curate's support refs come from declared links.** Each curated item's
470
+ support refs (at most two) are now assets its declared links name: what
471
+ it links to, then what links to it, in the order `akm show` lists them,
472
+ skipping assets curate already selected. They no longer come from the LLM
473
+ entity graph's `related` list. On the retrieval snapshot, `related` offered
474
+ support refs for 47 of 867 curate items (5.4%) and declared links for 257
475
+ (29.6%). The retrieval judge graded 157 of those items, asking whether each
476
+ support ref is worth opening next: 56% of declared support refs were useful
477
+ against 68% of `related`'s, so curate attaches about 24 useful support refs
478
+ per 100 items instead of 7. Curate's items are unchanged.
479
+ (`src/commands/read/curate.ts`)
480
+ - **Index layout 26.** The first writable open of an older index derives
481
+ every entry's links from its stored `document_json` in place, reading no
482
+ file: on the 23,979-entry snapshot index that takes 0.8 s and adds 3.5 MB.
483
+ Earlier layouts never stored a workflow's or a task's targets, so only the
484
+ directories holding workflows and tasks re-read on the next `akm index`.
485
+ The open leaves `index_meta.vacuumPending` like every layout migration.
486
+ 0.9.17-alpha.4 through alpha.7 refuse an index at layout 26
487
+ (`INDEX_SCHEMA_INCOMPATIBLE`, naming the upgrade), for writing as well as
488
+ reading, so going back to one of them needs a new index: move `index.db`
489
+ aside and run `akm index` under that release (the LLM graph and enrichment
490
+ cache it held are not rebuilt by `akm index`).
491
+ (`src/storage/repositories/index-schema.ts`,
492
+ `src/storage/repositories/index-entry-schema.ts`)
493
+
494
+ ### Fixed
495
+
496
+ - **`index.graph.enabled: false` stops graph extraction in `akm improve`.**
497
+ The switch was read only when improve had no strategy plan, which is never
498
+ the case in a real run, so the nightly and weekly graph tasks kept
499
+ extracting with it set. Improve now skips its graph extraction stage
500
+ whenever `index.graph.enabled` is `false`, whatever the strategy enables.
501
+ This also makes the graph ablation harness's "graph off" arm, which sets
502
+ exactly this key, turn extraction off. Improve still does not read
503
+ `index.defaults` when it picks the engine for graph extraction.
504
+ (`src/commands/improve/loop-stages.ts`)
505
+ - **Graph extraction never has more calls in flight than the run's
506
+ concurrency.** Batches run side by side up to the runner's concurrency, and
507
+ each batch also sent its per-file calls (long bodies, a non-array response)
508
+ up to that limit at once, so at a concurrency of 2 four calls could be in
509
+ flight. A batch now makes its per-file calls one at a time. At the default
510
+ concurrency of 1 nothing changes. (`src/llm/graph-extract.ts`)
511
+ - **A long document whose extraction partly failed is extracted again.** A
512
+ body over 1,600 characters is extracted in chunks. When some chunks failed
513
+ (a timeout, an error, an empty response) and others found entities, the
514
+ file was recorded and cached as extracted, so the failed chunks were never
515
+ retried. Such a file is now recorded as failed and not cached, and the next
516
+ run extracts it again, every chunk: partial failures are rare outside
517
+ provider outages, and keeping per-chunk results to skip the chunks that
518
+ succeeded would need a second cache. Until then the entities the other
519
+ chunks found are stored when the file had no graph rows yet; a file with
520
+ rows keeps them. (`src/llm/graph-extract.ts`)
521
+ - **`scripts/node-runtime/akm` could die from a raw signal instead of
522
+ forwarding it to its child.** The launcher registered its
523
+ SIGTERM/SIGINT/SIGHUP forwarding listeners only after spawning the child;
524
+ under load, a signal could arrive in that window and fall through to the
525
+ runtime's default (process-terminating) disposition, killing the launcher
526
+ before the child ever saw it. The listeners now go up before anything else
527
+ runs, with a small queue for a signal that arrives before the child exists.
528
+ - **`scripts/node-runtime/akm-migrate` carried the same pre-spawn signal
529
+ race** as `scripts/node-runtime/akm` above, for the same reason (listeners
530
+ registered only after `spawn()`), with no test covering it. Fixed the same
531
+ way, and added `tests/integration/akm-migrate-signal-forwarding.test.ts`
532
+ (modelled on `launcher-signal-forwarding.test.ts`) for its forwarding.
533
+ - **The launcher signal tests failed intermittently because of their own
534
+ fixture.** The fake child wrote its ready file before it registered its
535
+ signal handler, so a forwarded signal could reach it in between and kill it,
536
+ and the launcher then reported that signal. Each fixture now registers its
537
+ handler first. The launcher's pre-spawn window above could not cause this:
538
+ the tests signal only after the child is running.
539
+
540
+ ## [0.9.17-alpha.7] - 2026-09-28
541
+
542
+ A scheduled task is now just a command and a schedule. Each native row
543
+ carries its own `AKM_BUNDLE_DIR` instead of pointing at a descriptor file,
544
+ and the runtime reads only v4 task files; older files convert once with
545
+ `akm migrate apply`. The first `akm task sync` after upgrading rewrites each
546
+ row once, keeping every task and schedule. `akm improve` reworks only assets
547
+ that retrieval returned or that are new, and reflect refuses a rewrite that
548
+ grades worse on the asset's own searches. `--require-engines` no longer
549
+ skips a run because the LLM endpoint is busy.
550
+
551
+ ### Changed
552
+
553
+ - **akm reads only task source v4.** A `version: 2` or `version: 3` task
554
+ file, or a `version: 4` file that still carries 0.9.15's retired
555
+ `schedule[].enabled`, now fails on its own with a message naming
556
+ `akm migrate apply`, which converts it once, under a backup (`akm upgrade`
557
+ runs it after an install). Until now every read converted such a file in
558
+ memory. `akm task sync` reports each one as a failure, leaves its installed
559
+ row as it is, and keeps reconciling every other task; `akm task run`,
560
+ `akm lint` and `akm task validate` report it the same way, and
561
+ `akm task validate`'s `converts` outcome is gone (such a file is
562
+ `blocked`). `akm migrate apply` now also converts the tasks of the stash
563
+ `AKM_BUNDLE_DIR` selects when no configured bundle names it, since the
564
+ runtime reads those too, and a root whose top-level task files are all
565
+ v2/v3 is still detected as an `akm-task` bundle, so they are found and
566
+ converted. A host whose task files are all v4 (`akm migrate status`
567
+ reports `current`) sees no difference.
568
+ (`src/tasks/source/parse-task-source.ts`, `src/commands/tasks/validate.ts`,
569
+ `scripts/akm-migrate/task-migrate.ts`,
570
+ `src/core/adapter/adapters/akm-task-adapter.ts`)
571
+ - **A scheduled row is its command plus its schedule, and carries its own
572
+ context.** Rows no longer name a `--scheduler-context` descriptor file;
573
+ they set what it held themselves. Every row sets `AKM_BUNDLE_DIR` to the
574
+ working stash of the shell that ran `akm task sync`, plus any
575
+ `AKM_CONFIG_DIR`, `AKM_DATA_DIR`, `AKM_CACHE_DIR` or `AKM_STATE_DIR` that
576
+ shell set explicitly: a `VAR=value` prefix in the crontab, an
577
+ `EnvironmentVariables` entry in a launchd plist, a `$env:VAR='value';`
578
+ assignment ahead of the command in Task Scheduler (its action has no
579
+ environment of its own). Scheduled runs see the same environment as
580
+ before, and sync still tells installations sharing a crontab apart by
581
+ that path (#846).
582
+ **What hosts see:** the first `akm task sync` after upgrading rewrites
583
+ every akm row once. `akm task sync --dry-run` lists each one as an update,
584
+ never an add or a remove; each keeps its launcher and its schedule, and
585
+ the `--scheduler-context <file>` argument becomes an inline
586
+ `AKM_BUNDLE_DIR=<working stash>`. On a host whose working stash is
587
+ `/home/u/akm` a row changes from
588
+
589
+ ```text
590
+ 30 8 * * * /home/u/.bun/bin/bun /home/u/.bun/lib/node_modules/akm-cli/dist/akm --scheduler-context /home/u/.local/share/akm/tasks/context/e898….json task run capture --bundle akm --scheduled > /home/u/.cache/akm/tasks/logs/capture.log 2>&1
591
+ ```
592
+
593
+ to
594
+
595
+ ```text
596
+ 30 8 * * * AKM_BUNDLE_DIR=/home/u/akm /home/u/.bun/bin/bun /home/u/.bun/lib/node_modules/akm-cli/dist/akm task run capture --bundle akm --scheduled > /home/u/.cache/akm/tasks/logs/capture.log 2>&1
597
+ ```
598
+
599
+ Rows written by 0.9.0 through 0.9.17-alpha.6 keep firing until that sync:
600
+ the CLI still accepts `--scheduler-context <file>` and applies the file's
601
+ environment (PATH included, for a 0.9.16 row). The files under
602
+ `$DATA/tasks/context/` are no longer written, and the uid, mode, symlink
603
+ and content-hash checks made on every scheduled run are gone. A sync
604
+ leaves some rows as they are (one whose task file failed to load, one a
605
+ `--bundle` sync did not cover), and those still name their file: once
606
+ `akm task doctor` lists no binding with a `contextPath`, nothing reads
607
+ them and they can be deleted. `akm task prune` now
608
+ finds rows whose `AKM_BUNDLE_DIR` names a directory that is gone, and older
609
+ rows whose descriptor cannot be read; `akm task doctor` lists `contextPath`
610
+ only for an older row. (`src/tasks/scheduler-invocation.ts`,
611
+ `src/tasks/backends/cron.ts`, `src/tasks/backends/launchd.ts`,
612
+ `src/tasks/backends/schtasks.ts`, `src/tasks/scheduler-sync.ts`,
613
+ `src/commands/tasks/tasks.ts`)
614
+ - **`akm improve` reworks only what gets read (#986).** An asset with fresh
615
+ feedback, or one you name (`akm improve skills/x`), is handled as before.
616
+ Every other pick must now be in the retrieval scope. That covers the
617
+ proactive-maintenance, high-salience and forgetting-safety lanes, and the
618
+ memories consolidation judges. An asset is in scope if a user `search`,
619
+ `curate` or `show` returned it, or user `feedback` named it, in the last 90
620
+ days, which is the usage log's retention. A hit on a `.derived` memory counts
621
+ for its parent. New material that no improve stage has processed yet is also
622
+ in scope. There is no new config key.
623
+
624
+ Measured with `akm improve --dry-run` on a copy of the maintainer's bundle
625
+ (19,870 assets), against 0.9.17-alpha.6:
626
+ - The fallback lanes pick from 6,450 assets instead of 15,686, and 9,236
627
+ refs are left out. No lane setting can reach the unread tail any more. In
628
+ July the proactive lane rewrote 3,069 assets, and 3,059 of them had not
629
+ been retrieved since the usage log began on 1 July.
630
+ - Consolidation judges 59 memories instead of 69.
631
+ - The high-salience lane no longer admits distill outputs nobody has read (2
632
+ today).
633
+ - Under the scheduled caps, today's nightly work is unchanged. The default
634
+ strategy selects the same 50 feedback-driven refs, and weekly proactive
635
+ maintenance selects the same 25, because salience ranking already puts
636
+ retrieved assets first.
637
+
638
+ `akm improve --dry-run` and the run result report the left-out refs as a new
639
+ `retrieval` gate. Health reports them under the skip reason `not_retrieved`.
640
+ Improve results stored by earlier releases, which have no such gate, still
641
+ decode. (`src/commands/improve/retrieval-scope.ts`,
642
+ `src/commands/improve/preparation.ts`, `src/commands/improve/consolidate.ts`)
643
+
644
+ - **Reflect refuses a rewrite that makes an asset worse for its own searches
645
+ (#722).** Before reflect proposes a rewrite of an existing asset, it grades
646
+ the old and the new content on up to five of the queries that actually
647
+ retrieved the asset (user `search` and `curate`). It uses the retrieval
648
+ eval's relevance prompt, which agrees with human grades at kappa 0.83. When
649
+ the new content grades lower on average, the rewrite is refused the same way
650
+ a quality-judge rejection is: `quality_rejected`, with the 14-day reflect
651
+ window. An asset without retrieval queries is not graded.
652
+
653
+ This was measured before it was built. Of 60 accepted rewrites since July,
654
+ judged this way, 14 graded lower (23%, 95% CI 14–35%) and 12 graded higher.
655
+ The gate was built because the lower bound cleared the 10% threshold set
656
+ before any judging. It costs two judge calls per query on the engine that
657
+ already runs the quality judge. On the maintainer's 2026-09-28 nightly run,
658
+ whose 30 rewrites had 102 usable queries, that is 204 calls, about 6 more
659
+ minutes on a 73-minute run. (`src/commands/improve/retrieval-gate.ts`,
660
+ `src/commands/improve/reflect.ts`)
661
+
662
+ ### Fixed
663
+
664
+ - **A cron row too long for one line is seen by `akm task sync` again.** A
665
+ command over 1,000 bytes runs from a wrapper script, and sync could not
666
+ read which task such a row ran: every sync, and every `--dry-run`, showed
667
+ it as an add and wrote it again. Sync now reads the script, so the row is
668
+ unchanged or an update like any other. (`src/tasks/backends/cron.ts`)
669
+ - **A `$` or a backslash in a scheduled row's value is kept.** launchd and
670
+ Task Scheduler rows passed their values through a string replacement that
671
+ read `$'`, `$&` and `$$` as patterns, so a path such as a Windows admin
672
+ share (`\\nas\share\akm$`) came out corrupted; reading a crontab row
673
+ back dropped a backslash inside a single-quoted value.
674
+ (`src/tasks/backends/launchd.ts`, `src/tasks/backends/schtasks.ts`,
675
+ `src/tasks/backends/cron.ts`)
676
+ - **`akm improve --require-engines` no longer skips a run because the LLM
677
+ endpoint is busy.** Its reachability probe, one short completion, gave up
678
+ after 3 seconds, so a local server busy with another job looked
679
+ unreachable and the whole scheduled run failed (all four scheduled runs on
680
+ 2026-09-27). The probe now waits up to the engine's own `timeoutMs`, at
681
+ most two minutes, so a busy server can answer while a hung one still fails
682
+ fast. The error's hint now says to check the endpoint rather than to run
683
+ `akm setup`.
684
+ (`src/commands/improve/improve-cli.ts`)
685
+
686
+ ## [0.9.17-alpha.6] - 2026-09-27
687
+
688
+ Graph extraction stops losing and wasting work. A timed-out extraction is
689
+ retried instead of cached as empty. Long documents are extracted once, and
690
+ per-file calls respect the run's concurrency. `akm improve` honors
691
+ `index.graph`. `akm curate` returns nothing for harness and tool envelopes,
692
+ and search and curate show identical content once. Lazy graph extraction,
693
+ which never ran under Bun, is removed.
694
+
695
+ ### Changed
696
+
697
+ - **`akm curate` returns nothing, on purpose, for input that is not a task.**
698
+ A harness or tool envelope (input that starts with an XML-style tag and
699
+ contains a closing tag, such as `<task-notification>…</task-notification>`,
700
+ `<system-reminder>…` or `<cross-session-message …>…`) and the stash README
701
+ line each used to get `--limit` unrelated assets. Every caller of
702
+ `akm curate` (the CLI, the OpenCode plugin, other harnesses) now gets an
703
+ empty `items` list with a `summary` that starts with `Curate abstained` and
704
+ names the reason, and a `tip`. On the retrieval suite curate abstains on 57
705
+ of 60 recorded non-task inputs and on none of the 221 real queries (nor on
706
+ any of 5,725 mined task queries). Length is not a reason to abstain: the
707
+ other 3 are task prompts of 2,431–5,531 characters, and in a judged sample
708
+ of 30 inputs over 2,000 characters the top 5 held a relevant asset for 24
709
+ of them (P@5 0.42, against 0.46 for prompts of 400–2,000 characters).
710
+ (`src/commands/read/curate.ts`)
711
+ - **Search and curate return identical content once.** Of entries whose
712
+ indexed content is identical (the same body saved under another name, as
713
+ both a memory and a knowledge doc, or in another bundle), only the
714
+ highest-ranked is kept, and the next candidate takes the freed slot. On the
715
+ retrieval suite such copies filled 7.5% of curate's top 5. Unique
716
+ precision@5, where a copy of a higher-ranked result earns nothing, rises
717
+ from 0.467 to 0.514 (+0.046, 95% CI [+0.028, +0.067]), and the share of
718
+ top-5 slots that repeat a higher-ranked result falls from 0.131 to 0.055.
719
+ Plain P@5 (0.553 → 0.551) and nDCG@10 stay within noise: they counted each
720
+ copy as another relevant result. Latency is unchanged.
721
+ (`src/indexer/search/db-search.ts`)
722
+
723
+ ### Removed
724
+
725
+ - **The unused `utility_scores_scoped` index table is gone.** It shipped in
726
+ 0.9.17-alpha.5 for per-project scoped utility scores, but no code ever read
727
+ or wrote a row. An index database drops it on its next writable open, the
728
+ same way other retired derived tables are dropped, with no layout-version
729
+ change. (`src/storage/repositories/index-schema.ts`)
730
+ - **Lazy graph extraction in `akm show` and `akm curate`.** With
731
+ `index.graph.lazyGraphExtraction: true`, `show` extracted an asset's graph
732
+ after building its response, so only the next `show` saw it. `curate`
733
+ queued assets for a later pass, which drained only the working bundle's
734
+ queue, and extractions made this way wrote no cache entry. Under Bun neither
735
+ path ever ran: the "already has a graph" check read a missing row as
736
+ present. Graph extraction now runs only in `akm improve`. The
737
+ `graph_extraction_queue` table is dropped the next time the index is opened
738
+ for writing. A config that still sets the key loads, and the key is named
739
+ once as unknown. (`src/commands/read/show.ts`,
740
+ `src/commands/read/curate.ts`, `src/indexer/graph/graph-extraction.ts`,
741
+ `src/storage/repositories/index-schema.ts`)
742
+
743
+ ### Fixed
744
+
745
+ - **`index.metadataEnhance`'s default is no longer contradicted by dead
746
+ code.** Metadata enhancement has always defaulted to off
747
+ (`isLlmFeatureEnabled`); a second, unreachable code path in
748
+ `isProcessEnabled` claimed the opposite default and had no caller. Removed,
749
+ so one default remains. (`src/llm/feature-gate.ts`)
750
+ - **Eval tooling and docs catch up to the current config and index shape.**
751
+ `scripts/akm-eval/src/curate-bench.ts` wrote the retired `sources` config
752
+ key and called a nonexistent `akm index --dir`; it now seeds its sandbox
753
+ the same way the other akm-eval scripts and integration tests do, and
754
+ drops `--dir`. The graph A/B ablation harness
755
+ (`scripts/akm-eval/src/graph-ablation.ts`) planted its "graph off" config
756
+ where the sandboxed `akm` never read it, with config keys that didn't gate
757
+ anything (one of them a type error); it now writes
758
+ `index.graph.enabled: false` to the sandbox's actual `AKM_CONFIG_DIR`.
759
+ Updated `scripts/akm-eval/README.md` and `docs/maintainers/eval.md` to
760
+ match, and corrected stale `docs/architecture/architecture.md` references
761
+ to `db-backup`, `staleness-detect`, and `src/commands/graph/`.
762
+ - **Scheduled graph extraction reads `index.graph`.** `akm improve` passed
763
+ graph extraction a batch size of 4 and the `memory` and `knowledge` types
764
+ whenever the strategy's `processes.graphExtraction` did not set them, so
765
+ `index.graph.graphExtractionBatchSize` and `graphExtractionIncludeTypes`
766
+ never applied. It did not read `index.graph`'s `engine`, `model`,
767
+ `timeoutMs` or `llm` either, so a setting such as
768
+ `index.graph.llm.enableThinking: false` had no effect on improve runs. A
769
+ value in the strategy's `processes.graphExtraction` still wins. A setting it
770
+ leaves unset now comes from `index.graph`, then from the built-in default.
771
+ Where `index.graph` asks for something improve did not use before, the
772
+ extractor changes and cached extractions stop applying, so those files are
773
+ extracted again. (`src/commands/improve/loop-stages.ts`,
774
+ `src/commands/improve/execution.ts`,
775
+ `src/commands/improve/improve-strategies.ts`)
776
+ - **A graph extraction that times out is retried, not cached as empty.** A
777
+ call that ran past the engine's `timeoutMs` was recorded as "no entities"
778
+ and cached, so the file was never extracted again. It is now recorded as
779
+ failed, and the next run retries it; timeouts also count toward the run's
780
+ failure-rate abort. A batch that times out fails its files without then
781
+ calling the model once per file. An empty response is likewise recorded as
782
+ failed. (`src/llm/graph-extract.ts`)
783
+ - **Long bodies are extracted once after batching turns itself off.** Two
784
+ non-array batch responses turn batching off for the rest of a run. From
785
+ then on, a body over 1,600 characters was extracted on its own and then
786
+ again with the rest of its batch. Each body is now extracted once.
787
+ (`src/llm/graph-extract.ts`)
788
+ - **A batch's per-file calls respect the run's concurrency.** When a batch
789
+ fell back to one call per file (long bodies, a non-array response, batching
790
+ turned off), those calls all went out at once, up to the batch size. Local
791
+ endpoints serve one or two requests at a time. The calls now run within the
792
+ limit the run applies to its batches, one at a time by default.
793
+ (`src/llm/graph-extract.ts`)
794
+ - **`related` counts a shared entity once.** `akm show`'s `related` list,
795
+ and curate's support refs taken from it, ranked files by the number of
796
+ matching entity rows. A file holding two case forms of one entity, as rows
797
+ from older extractors can, counted it twice and could outrank a file that
798
+ shared two entities. `related` now counts distinct entities. Extraction also
799
+ keeps one form of each entity before writing. The stored key `related`
800
+ matches on is now the one extraction deduplicates on, which also drops
801
+ surrounding quotes and backticks.
802
+ (`src/indexer/graph/graph-related.ts`,
803
+ `src/indexer/graph/graph-extraction.ts`, `src/indexer/db/graph-db.ts`)
804
+ - **A config change that re-extracts the graph says so.** Cached graph
805
+ extractions are keyed by extractor: model, batch size, included asset types
806
+ and prompt version. Changing any of them made every cached file extract
807
+ again without a word. The first run after such a change now warns once,
808
+ naming the change and the number of cached files it will extract again, and
809
+ records the warning in the run's result.
810
+ (`src/indexer/graph/graph-extraction.ts`)
811
+ - **Graph extraction reports what its parser filtered.** A run's graph
812
+ telemetry, part of `akm improve`'s result, now carries
813
+ `filteredGenericEntities`, `filteredInvalidRelations`,
814
+ `filteredLowConfidenceRelations` and `contextBatchRetries`. The pass
815
+ computed them and dropped them, and did not count batch responses at all.
816
+ (`src/indexer/graph/graph-extraction.ts`, `src/llm/graph-extract.ts`)
817
+ - **An unknown key under `index.<pass>` is kept and named once.** It was
818
+ dropped from the loaded config and named twice. It is now handled like an
819
+ unknown key anywhere else in config. (`src/core/config/schema/index-config.ts`)
820
+
821
+ ## [0.9.17-alpha.5] - 2026-09-27
822
+
823
+ `akm show` works again for a memory that has a `.derived.md` child (835 of them
824
+ in one real bundle), and `akm bundle add --provider … --name` holds to the same
825
+ `--name` contract as every other add.
826
+
827
+ ### Fixed
828
+
829
+ - **`akm show` works for a memory that has a `.derived.md` child.** When
830
+ `memories/X.md` and `memories/X.derived.md` both existed, `akm show
831
+ memories/X`, with or without a `#fragment`, failed with
832
+ `RESOURCE_ALREADY_EXISTS` ("multiple physical owners"); `akm curate`
833
+ previewed such a memory from its description alone, and `akm curate --pack`
834
+ left it out. The index gives the derived child its own ref,
835
+ `memories/X.derived`, but the ref lookup also counted `X.derived.md` as a
836
+ file for `memories/X`. The lookup now follows the index: `memories/X` is
837
+ `X.md` and `memories/X.derived` is `X.derived.md`. A derived child whose
838
+ parent file is gone no longer answers for the parent's ref either, so it
839
+ cannot hide a real `X.md` in a lower-priority bundle. `akm lint` and
840
+ `--xref` / `--supersedes` validation still accept a ref to `memories/X`
841
+ when only `X.derived.md` remains. Broken since 0.9.7.
842
+ (`src/core/asset/asset-placement.ts`, `src/commands/lint/base-linter.ts`)
843
+ - **`akm bundle add --provider … --name` keeps the `--name` contract too.**
844
+ Since 0.9.17-alpha.4 an explicit `--name` that is not a legal bundle slug,
845
+ or is taken by another bundle, fails with exit 2, and re-adding a source
846
+ under a different name points at `akm bundle rename`. A declarative add
847
+ (`akm bundle add <target> --provider npm|git|website`) still replaced such
848
+ a name with a derived one and exited 0. It now fails the same way, before
849
+ any write. (`src/commands/sources/source-manage.ts`)
850
+
851
+ ## [0.9.17-alpha.4] - 2026-09-27
852
+
853
+ Search and curate are rebuilt on measured evidence. On a 221-query suite of real
854
+ akm queries judged for relevance, search nDCG@10 goes from 0.346 to 0.556 and
855
+ curate precision@5 from 0.350 to 0.551, with search p50 falling from 787 ms to
856
+ about 400 ms and a fresh index shrinking from 560 MB to 340 MB.
857
+
858
+ Upgrades stop breaking because the machinery that broke them is gone, not
859
+ because more was added: this release removes the scheduler-grant layer, the
860
+ filesystem transaction journals, the maintenance barrier and lock mutex, the
861
+ strict config schemas and their retired-key registry, and every per-key
862
+ config migration, and it lands with fewer lines in `src/` than 0.9.17-alpha.3.
863
+
864
+ ### Changed
865
+
866
+ - **Search ranks by reciprocal rank fusion of BM25 and document vectors.**
867
+ Two candidate lists, 100 each, are fused with equal weights (k = 60): BM25
868
+ over whole documents (`entries_fts`) matching any of the query's
869
+ non-stopword words (every word when the query has nothing else), and the
870
+ document vectors nearest to the query embedding. Equal scores are ordered by
871
+ ref, so the ranking depends only on the index and the query's embedding
872
+ (keyword-only runs of the suite reproduce exactly). Filters (`--type`,
873
+ `--from`, `--filter`, `--belief`, the default session exclusion, proposed
874
+ quality) and one-hit-per-file deduplication narrow the fused list without
875
+ reordering it. A hit's `score` is its fused score (at most 2/61 ≈ 0.033),
876
+ and `--detail full`'s `whyMatched` lists its rank in each list
877
+ (`lexical rank 3`, `vector rank 12`). On the retrieval suite (221 real
878
+ queries over a 23k-document snapshot, LLM-judged) nDCG@10 rises from 0.347
879
+ to 0.566 and P@5 from 0.347 to 0.558, level with the lab reference design;
880
+ every query class improves, questions (0.20 → 0.44) and long prompts
881
+ (0.24 → 0.52) most. End to end, process start included, search p50/p95
882
+ fell from 787/4130 ms to 341/830 ms. BM25 weighs the five columns
883
+ equally: the previous 10/5/3/2/1 weights measured 0.011 lower P@5.
884
+ (`src/indexer/search/db-search.ts`,
885
+ `src/indexer/search/ranking.ts`, `src/indexer/search/fts-query.ts`,
886
+ `src/storage/repositories/index-fts-repository.ts`.)
887
+ - **Queries are embedded the way the embedding model expects.** An embedding
888
+ profile picks query and document templates by model name: Qwen3-Embedding
889
+ gets its retrieval instruction on queries, nomic-embed `search_query: ` /
890
+ `search_document: `, the BGE English, mxbai and arctic models the
891
+ "Represent this sentence for searching relevant passages: " query prefix,
892
+ E5 `query: ` / `passage: `, and other models none; `embedding.queryTemplate`
893
+ and `embedding.documentTemplate` override the preset (`""` turns it off).
894
+ The document template is part of the embedding fingerprint, so a nomic or
895
+ E5 index re-embeds on the next `akm index`; Qwen3, BGE and the default local
896
+ model keep their vectors. Text reaches the embedder with its case: the query
897
+ is no longer lowercased, and an entry's embedded text keeps its case once
898
+ the entry is next re-indexed (`akm index --reembed` refreshes every vector
899
+ at once). The query embedding is requested before the keyword query runs,
900
+ and a search waits for it at most `embedding.queryTimeoutMs` (default 3000)
901
+ before serving keyword ranking alone with one warning — a hung endpoint
902
+ used to hold a search for up to 120 s. (`src/llm/embedders/profile.ts`,
903
+ `src/indexer/materialize-embeddings.ts`.)
904
+ - **Curate is one search.** `akm curate` takes the top `--limit` hits of the
905
+ fused search in order and enriches each with its preview, run details and
906
+ up to two graph-related support refs, so curate's items are search's top
907
+ hits (P@5 0.350 → 0.556 on the retrieval suite; p50/p95 1082/4148 ms →
908
+ 433/888 ms). The optional reranker (`search.curateRerank`, still off by
909
+ default) now reorders the top 30 fused candidates (`topN`, previously 8 but
910
+ applied only to the final `limit` items) and sends each as its name,
911
+ description and the start of its indexed content (2,000 characters in all)
912
+ instead of name and description. (`src/commands/read/curate.ts`.)
913
+ - **Vectors are stored once (index layout 25).** Each entry's vector lives
914
+ only in `embeddings`, and search scores every current-model row by cosine
915
+ similarity in JavaScript. The sqlite-vec mirror `entries_vec` is gone with
916
+ its repair pass, readiness flag and width bookkeeping: sqlite-vec cannot
917
+ load in the standalone binaries (`bun build --compile` does not bundle the
918
+ optional package), under Bun on macOS (the system SQLite refuses
919
+ extensions) or wherever the optional dependency is missing, so those
920
+ installs always searched the BLOB rows anyway, and a mirror that fell out
921
+ of step returned wrong neighbours without an error. The scan now reads
922
+ float32 views of the rows instead of copying each into an array: 85 ms per
923
+ query for 24k 1,024-dimension vectors in a fresh process, against 460 ms for
924
+ the old fallback and 41 ms for sqlite-vec. The first writable open drops
925
+ `entries_vec` (100 MB on that index) and the `embeddingDim` and
926
+ `vecFastPathReady` meta keys; dropping a vec0 table needs the extension, so
927
+ sqlite-vec stays an optional dependency for that alone, and an install
928
+ without it leaves the unread table in place. `semanticStatus` is `ready-js`
929
+ whenever every entry has a vector (`ready-vec` is gone), the `vecAvailable`
930
+ field leaves `akm index` and `akm info` output, setup no longer probes for
931
+ sqlite-vec, and `embedding.dimension` loses its 4,096 cap, which only the
932
+ vec0 column needed. (`src/storage/repositories/index-vec-repository.ts`,
933
+ `src/storage/repositories/index-schema.ts`,
934
+ `src/indexer/materialize-embeddings.ts`.)
935
+ - **The fragment full-text table is gone (index layout 25).** Nothing has
936
+ read `entry_fragments_fts` since fragments stopped competing as search
937
+ candidates, so an upsert no longer splits the body into fragment rows and
938
+ the first writable open drops the table (39 MB on a 24k-entry index).
939
+ `entry_fragments` stays: `akm show <ref>#akm-fragment-…` (#937) resolves the
940
+ selector from its stored safe Markdown.
941
+ (`src/storage/repositories/index-fts-repository.ts`,
942
+ `src/storage/repositories/index-schema.ts`.)
943
+ - **The embedding input is derived, not stored (index layout 25).**
944
+ `entries.search_text` held a third copy of every body (74 MB on a
945
+ 24k-entry index) only to feed the embedder and to notice when an entry's
946
+ vector went stale. The embedding pass now derives the text from
947
+ `document_json` when it embeds an entry, and `entries.embed_hash` keeps its
948
+ SHA-256: an upsert whose hash differs deletes the vector, exactly as a
949
+ changed `search_text` did. The first writable open hashes each stored
950
+ `search_text` before dropping the column, so every vector stays attached
951
+ until its entry's text really changes, and nothing is re-embedded by the
952
+ upgrade. (`src/storage/repositories/index-entries-repository.ts`,
953
+ `src/storage/repositories/index-vec-repository.ts`,
954
+ `src/storage/repositories/index-schema.ts`.)
955
+ - **`akm index` reclaims index.db's free pages.** Nothing ever VACUUMed
956
+ `index.db`, so every table an upgrade rebuilt or dropped stayed on disk as
957
+ free pages: a 24k-entry index measured 979 MB, 427 MB of it free, against
958
+ 560 MB for a fresh build of the same content. The run now ends with a
959
+ VACUUM when the writable open migrated the layout (it leaves
960
+ `index_meta.vacuumPending` for the next `akm index`, since the open may sit
961
+ inside a caller's transaction) and whenever more than half the pages are
962
+ free, the threshold and pass improve already apply to `state.db`
963
+ (`vacuumIfReclaimable`, formerly `vacuumStateDbIfReclaimable`). A busy
964
+ database skips the VACUUM instead of failing the run; each VACUUM prints
965
+ its page counts and appends an `index_db_vacuumed` event. With the three
966
+ layout-25 removals above, a fresh build of the 24k-entry retrieval snapshot
967
+ is 340 MB instead of 560 MB, and a copy of the 601 MB layout-24 build
968
+ migrates in 0.8 s with all 23,979 vectors kept byte for byte, then VACUUMs
969
+ to 377 MB. Retrieval is unchanged on the suite (search nDCG@10 0.5562 →
970
+ 0.5560, Δ −0.0002 [−0.0014, +0.0009]; P@5 0.5507 → 0.5517; curate P@5
971
+ identical), and search p50/p95 moved from 374/912 ms to 401/870 ms.
972
+ (`src/indexer/indexer.ts`, `src/storage/state-db-integrity.ts`.)
973
+ - **An index a newer akm wrote is refused, naming the upgrade.** Readers
974
+ used to serve a newer layout "as far as they could" and the writable open
975
+ continued at its own layout, setting the marker back so the two releases
976
+ alternated. A newer layout can lack a column an older reader selects —
977
+ layout 25 drops `entries.search_text` — so every opener now refuses it with
978
+ `INDEX_SCHEMA_INCOMPATIBLE` ("Upgrade akm to use this index.") and leaves
979
+ the file untouched; an older layout is still served as-is and migrated by
980
+ the next writable open, and `akm improve --dry-run` reports the refusal as
981
+ an incompatible snapshot. (`src/storage/repositories/index-connection.ts`,
982
+ `src/storage/repositories/index-schema.ts`.)
983
+ Downgrading to 0.9.17-alpha.3 or earlier is not supported for the index:
984
+ those releases select columns layout 25 dropped, so rebuild it with
985
+ `akm index --full` under the older release.
986
+ - **Scheduled rows no longer freeze the syncing shell's directories or PATH.**
987
+ A `--scheduler-context` descriptor now carries the resolved bundle path
988
+ (sync's ownership signal, #846) plus only the `AKM_CONFIG_DIR`,
989
+ `AKM_DATA_DIR`, `AKM_CACHE_DIR` and `AKM_STATE_DIR` values the process that
990
+ ran `task sync` had set explicitly; resolved defaults are left to resolve at
991
+ fire time, exactly as they do for an interactive command. It used to capture
992
+ every resolved directory and the whole PATH: one host's `task sync`, run from
993
+ inside a desktop app whose environment pointed `$STATE` at the app's own
994
+ config directory, froze that directory into eight cron rows on 2026-08-06,
995
+ every later sync preserved it, and the nightly improve run then held its
996
+ locks where no interactive command could see them. PATH moves into the
997
+ native artifact, where it is visible and editable: a `PATH=` line inside a
998
+ `# akm:env BEGIN`/`END` section written directly above the first akm task
999
+ block (cron applies it to the rows that follow it; akm rewrites the line on
1000
+ every crontab write and removes it with the last task block), and an
1001
+ `EnvironmentVariables` entry in each launchd plist. Task Scheduler runs a
1002
+ task with the account's own environment and carries no PATH. Plain
1003
+ `akm task sync` recomputes the descriptor on every run — an installed row
1004
+ whose descriptor no longer matches is updated while its launcher is kept as
1005
+ before (only `--rebind` moves that) — so one sync after upgrading rewrites
1006
+ every row written under the old policy. Descriptors an older release wrote
1007
+ still load, PATH included, until that sync. (`src/tasks/scheduler-invocation.ts`,
1008
+ `src/tasks/backends/cron.ts`, `src/tasks/backends/launchd.ts`,
1009
+ `src/tasks/scheduler-sync.ts`.)
1010
+ - **The write path keeps only what its callers use** (`src/core/write-source.ts`,
1011
+ 1,485 → 602 lines). The git transaction chain — publication identity capture,
1012
+ path, worktree and commit snapshot validation, base-HEAD assertions,
1013
+ transaction-commit discovery, a per-repo pending-mutation registry and a
1014
+ plan/begin/publish API — lost its last callers when the proposal
1015
+ transaction journals were removed and survived only because one test
1016
+ imported it; its 15 exports and their private helpers are gone. A write to a
1017
+ git-backed bundle now writes the file atomically inside the bundle root,
1018
+ records the exact path, and the boundary commits exactly those paths and
1019
+ pushes with `--force-with-lease`. A dirty or gitignored destination is no
1020
+ longer refused: an ignored path stays local with a warning instead of the
1021
+ command throwing after the file had already landed, and an upstream that
1022
+ cannot be inspected during preparation warns instead of aborting. Path
1023
+ containment, the symlink-escape refusal and the detached-HEAD refusal stay.
1024
+ - **Readers tolerate everything older releases wrote.** No config object is
1025
+ strict any more: a key this release does not know — retired, misspelled,
1026
+ or written by a newer release — is kept in memory and named once
1027
+ (`unknownConfigKeyPaths`, `src/core/config/config.ts`, found by walking
1028
+ the schema), round-trips through ordinary writes so a newer release's
1029
+ settings survive a downgrade, and is dropped only by `akm migrate apply`.
1030
+ The retired-keys registry, its read shim, and the schema-compatibility
1031
+ lint are removed; nothing needs registering for a key to be tolerated.
1032
+ - **`akm migrate apply` has one config step.** `configFile`
1033
+ (`normalizeConfigFile`) reads config.json through the same pipeline every
1034
+ load runs (`configVersion` read, legacy source shape, `extraParams` lift)
1035
+ and writes the current shape back under a backup, dropping unknown keys.
1036
+ It replaces the per-key `configLegacySourceShape`, `configExtraParams`,
1037
+ `configRetiredKeys` and `configSchedulerSourceIds` steps, the
1038
+ `schedulerActivation` and `staleTxns` steps, and the `--host-local` mode. A
1039
+ pending config lift is now `ready`, never a blocker for the other steps.
1040
+ The `deadResidue` step also removes the transaction-journal,
1041
+ maintenance-barrier, lock-mutex and version-stamp files older releases left
1042
+ under `$DATA`, `$STATE` and `$CONFIG`, and runs whether or not a bundle is
1043
+ configured.
1044
+ - **Scheduling is one list.** `scheduler.enabled` holds the fully-qualified
1045
+ refs this host schedules (`bundle//tasks/x`). It is still written in the
1046
+ `{kind, ref, sourceId}` shape 0.9.16 reads, so that release keeps working
1047
+ against a config this one wrote; either shape is read as the ref.
1048
+ A config with no list at all (every release before 0.9.17) means "keep
1049
+ what is installed": the first `akm task sync` (or `setup`, `task enable`,
1050
+ `task disable`, `task add`) takes the akm-written rows already in the
1051
+ native scheduler as the host's choice, writes the list, and says so;
1052
+ `task sync --dry-run` reports it without writing. An explicit list, empty
1053
+ or not, is never second-guessed. Grants, source identities, the
1054
+ carry-forward, the scheduler-activation and source-id migrations and the
1055
+ fire-time re-check are gone (`src/tasks/activation-config.ts`).
1056
+ - **Proposal accept and revert write directly.** The asset file is written
1057
+ (temp file + rename), committed through the ordinary write-target
1058
+ boundary, then the proposal row and its event are recorded in one
1059
+ state.db transaction and the file is indexed best-effort. A crash in
1060
+ between leaves a re-acceptable pending proposal, nothing corrupt. The
1061
+ filesystem transaction journals (`src/core/fs-txn.ts`) with their
1062
+ recovery, quarantine, deferral and fencing are removed, along with the
1063
+ `txn-quarantine`/`txn-awaiting-recovery` health advisories.
1064
+ - **`akm bundle update` publishes, records the lock entry, then reindexes —
1065
+ with no rollback transaction.** An update still fetches into a staging
1066
+ directory beside the cache and audits the staged bytes for dangerous env
1067
+ keys before anything goes live; a blocked or failed audit changes nothing.
1068
+ It then publishes with one rename (a fast-forward for a writable Git
1069
+ checkout), writes the lock entry, and reindexes. If the reindex fails, the
1070
+ new content and lock entry stay for the next `akm index`, and the previous
1071
+ install directory is kept. The config, staged-content, lockfile-byte and
1072
+ checkout-HEAD fences and the lockfile compare-and-swap restore are gone, so
1073
+ an update no longer fails with "changed concurrently" or "changed after its
1074
+ staged bytes were audited": it already runs under the asset-mutation lease,
1075
+ and Git refuses a fast-forward that would overwrite local work. A website
1076
+ source refreshes through its mirror's own snapshot staging, so a killed
1077
+ refresh still keeps the previous mirror.
1078
+ - **A lock file is one `O_EXCL` create** (`src/core/file-lock.ts`). The
1079
+ SQLite lock-operation mutex, the maintenance barrier (a lock guarding lock
1080
+ registration) and its per-open activity registry — the source of the
1081
+ lock-sidecar leak that grew `$STATE` by hundreds of megabytes — are
1082
+ removed. `MAINTENANCE_BARRIER_BUSY` no longer exists; contention is
1083
+ reported as `INDEX_DB_CONTENDED`, `STATE_DB_CONTENDED` or
1084
+ `IMPROVE_LOCK_HELD`, as before.
1085
+ - **Removed from 0.9.17-alpha:** startup version reconciliation
1086
+ (`version-reconcile.json`), the akm-install enumerator and `akm upgrade
1087
+ --version`/`--tag` with its other-install mover, the `version-reconcile`,
1088
+ `scheduler-grants`, `scheduled-startup-failures` and `akm-installs` health
1089
+ advisories, and the `akm info` `compat` manifest with its
1090
+ `PLUGIN_PROTOCOL_VERSION`. `akm upgrade` is what it was in 0.9.16.
1091
+ - **Documented the persisted-data compatibility contract.** Added
1092
+ `docs/architecture/persisted-data-compat.md`: the four-sentence contract a
1093
+ reader owes data an earlier release wrote, plus a per-format table (config,
1094
+ `state.db`, `index.db`, task source, workflow IR, native scheduler rows,
1095
+ proposal and task-history metadata, lock payloads, `.akm` residue) naming
1096
+ where each is written, its version marker, its older/newer-data behavior,
1097
+ and which gate covers it — with explicit `Gap:` notes where the code does
1098
+ not meet the contract yet. Registered in `docs/architecture/README.md`.
1099
+ `AGENTS.md`'s "Reading persisted data" section now points at this doc
1100
+ instead of a deleted file.
1101
+
1102
+ - **Scheduler writes hold one lock and apply row by row.** `akm task sync`,
1103
+ `add`, `enable`, `disable` and `prune --yes` hold one `O_EXCL` lock,
1104
+ `$STATE/locks/scheduler.lock` (`src/tasks/scheduler-lock.ts`), for the
1105
+ whole read–plan–write; a second scheduler command exits 75
1106
+ (`SCHEDULER_LOCK_HELD`), and a lock left by a dead process is reclaimed.
1107
+ Under it, sync reads the installed rows once and diffs by native id: a
1108
+ missing row is installed, a changed row rewritten, a row whose source is
1109
+ gone or no longer enabled removed. A row that fails to install or remove,
1110
+ or two sources claiming one native id, is reported in `failures` (exit 1)
1111
+ while every other row applies; the per-row compare-and-swap expectations
1112
+ and whole-set rollback are gone. A task whose source stops parsing keeps
1113
+ its installed row instead of being unscheduled by a YAML typo.
1114
+ (`src/tasks/scheduler-sync.ts`, `src/commands/tasks/tasks.ts`)
1115
+ - **`akm task add` is "write, enable, sync".** It validates the task and
1116
+ refuses an id already scheduled from another bundle before writing
1117
+ anything, then writes the source, adds the ref to `scheduler.enabled`
1118
+ (unless `--disabled`) and syncs the bundle. When the row cannot be
1119
+ installed, add fails naming the cause and the task stays written and
1120
+ enabled for the next `akm task sync` to retry; it no longer restores the
1121
+ prior source and rows byte-for-byte. `--force` with fewer schedules removes
1122
+ the dropped schedules' rows through the same sync, and `--rebind` means
1123
+ what it means for `task sync`.
1124
+ - **Improve records what it tried in one ledger** (`improve_ledger`,
1125
+ `src/storage/repositories/improve-ledger-repository.ts`). One row per
1126
+ stash, ref and stage holds the last attempt, its outcome and when the ref
1127
+ is next eligible, from one cadence table:
1128
+
1129
+ | Outcome | Next eligible | Lifted early by newer feedback? |
1130
+ | --- | --- | --- |
1131
+ | rejected, quality_rejected | 14 d reflect, 30 d distill, 7 d other stages | no |
1132
+ | expired | 1 d | no |
1133
+ | proposed, review_needed, unchanged, judged_no_action | 7 d | yes |
1134
+ | accepted, failed | immediately | — |
1135
+
1136
+ Every stage reads it before any LLM call. It replaces proposal
1137
+ fingerprints, the per-stage cooldowns, the distill reject files and the
1138
+ event-timestamp cursors, which disagreed with one another (quality
1139
+ rejections never reached the fingerprints; consolidate re-judged promoted
1140
+ memories). Distill and consolidate now key by their input refs, so each
1141
+ such input may be attempted once more after upgrading. Schema repair paces
1142
+ itself with the ledger too, replacing its private 7-day cooldown and
1143
+ 3-attempts-per-30-days cap. Every stage — reflect, distill, consolidate,
1144
+ extract, triage, memory inference, graph extraction — runs through one
1145
+ shared path (`src/commands/improve/stage.ts`): pick the runner, call the
1146
+ model, judge the output, mint the proposal, record the usage.
1147
+ - **`akm proposal drain` has one rule.** A proposal the quality judge passed
1148
+ (a `staged` gate decision whose content hash still matches) is accepted, an
1149
+ empty diff is rejected, and everything else goes to the judgment tier
1150
+ (`processes.triage.judgment`) or waits for review. Extract and consolidate
1151
+ proposals, which the `personal-stash` policy auto-accepted on size alone,
1152
+ carry no judge stamp, so they now go to the judgment tier — or wait for
1153
+ review when none is configured — instead of being accepted. The policies
1154
+ and their flags are retired (see Removed). `--dry-run` now predicts what a
1155
+ real drain does: a proposal whose target already holds its content (an
1156
+ accept that wrote the file but was interrupted before recording it) is
1157
+ reported as promoted, as the real drain finishes it, instead of as a
1158
+ stale-target rejection.
1159
+ - **State migration `028-improve-ledger` creates the ledger and drops six
1160
+ tables.** It backfills the ledger from each ref's latest proposal and drops
1161
+ `proposal_fingerprints`, `improve_gate_thresholds`, `proposal_fs_imports`,
1162
+ `consolidation_judged`, `improve_cycle_metrics` and `canary_queries`.
1163
+ Because it drops schema, the first open after upgrading copies the
1164
+ database to `state.db.pre-028-improve-ledger.bak` before it runs.
1165
+ - **Upgrading no longer rebuilds or re-embeds the search index (index layout
1166
+ 24).** The first writable open applies a layout change in place — added
1167
+ columns, and a one-time rebuild of the two full-text tables from the stored
1168
+ entries (about 2–3 s for 24k entries); embeddings, utility scores, the
1169
+ enrichment cache and the graph are never dropped, and only a corrupt file
1170
+ is rebuilt from scratch (#865). Both FTS5 tables are contentless, so
1171
+ indexed text is stored once (153 MB of a 980 MB index on a 23.9k-entry
1172
+ stash; a SQLite older than 3.43 keeps the previous layout). Each vector
1173
+ records its model (`embeddings.model`): a model change re-embeds only the
1174
+ entries missing a vector for the configured model, per batch and
1175
+ resumably, replacing the purge, the #955 re-embed canary and
1176
+ `embedding_salvage`. `akm index --full` keeps unchanged entries' vectors,
1177
+ and a one-file change in a large directory re-persists only that file.
1178
+ Readers serve an older layout as-is and say so once on stderr. An akm
1179
+ older than this release refuses a layout-24 index and asks to be upgraded.
1180
+ - **`--verbose` embedding output lists each document's size without a
1181
+ predicted batch number.** The per-batch lines already report every
1182
+ provider request's document and token counts, and skipped documents are
1183
+ listed at the end of the pass.
1184
+ - **Workflow runs are never refused for their plan's version or hash.**
1185
+ Markdown and the GitHub-shaped YAML subset compile straight to one plan
1186
+ type, and new runs record plan `irVersion` 6. A stored plan that decodes
1187
+ runs whatever release froze it — irVersion 4 and 5 plans are read
1188
+ tolerantly, and a key this release does not know is ignored instead of
1189
+ abandoning the run; one that does not decode is marked abandoned and `akm
1190
+ workflow run <ref>` starts afresh; only a plan a newer akm froze is
1191
+ refused, with "Upgrade akm" (`WORKFLOW_IR_VERSION_UNSUPPORTED` is gone).
1192
+ One driver per run is a lock file,
1193
+ `<data dir>/workflow-run-locks/<run id>.lock`: a second `akm workflow run`
1194
+ exits 75 (`RUN_LEASE_HELD`) naming the holder's pid, and a dead pid's lock
1195
+ is reclaimed at once — the database run lease, its heartbeat and the
1196
+ check-ins are gone. Resume reuses every completed unit whatever its
1197
+ recorded input hash, and warns once when the workflow file's sha256
1198
+ differs from the one recorded at freeze, then continues on the frozen
1199
+ plan. Executable identity (realpath, inode and hash captured at freeze,
1200
+ checked at dispatch) is gone, so upgrading `claude` mid-run no longer
1201
+ strands a run.
1202
+ - **Every execution goes through three plain functions:** `resolveExecution`
1203
+ → `buildExecution` → `runExecution` (`src/integrations/agent/execution.ts`,
1204
+ `runner-dispatch.ts`), replacing a 12-hop pipeline across 14 modules — the
1205
+ cascade planner, authorized-plan and provenance checks, lowerer registry
1206
+ and dispatch lease. Two behaviour changes: credentials are read at each
1207
+ dispatch, so a key rotated mid-run is used on the next call instead of a
1208
+ snapshot taken at the start; and an explicit `engine: null` in a task,
1209
+ workflow or command layer means "no preference here" and falls through to
1210
+ `defaults.engine` instead of forcing the `opencode-sdk` fallback.
1211
+ - **`state.db` opens on one connection.** The open creates the parent
1212
+ directory, opens the file, applies the pragmas, reads the migration ledger
1213
+ and runs every pending migration in one `BEGIN IMMEDIATE`; the read-only
1214
+ preflight connection, the `/proc/self/fd` alias and the refusal of an empty
1215
+ "unversioned" file are gone. Before a migration that drops schema runs on
1216
+ an existing database, it is copied to `state.db.pre-<id>.bak`. Since any
1217
+ open applies pending migrations, `akm health`'s `state-db-migrations` check
1218
+ now reports what its own open applied (`evidence.applied`,
1219
+ `evidence.backupPath`) and fails only when a migration could not be
1220
+ applied.
1221
+ - **`akm health` drops checks nothing acted on.** Removed: the
1222
+ `task-log-backing` hard check, the `pool-saturation` advisory, the six
1223
+ research advisories (`outcome-proxy-adequacy`, `outcome-proxy-dead`,
1224
+ `salience-uniformity-collapse`, `enrichment-lane-minting`,
1225
+ `improve-churn-ratio`, `collapse-churn-detector`) and the report's
1226
+ coverage, degradation and minting rollups. The HTML report's embedded
1227
+ `RUNS` data drops 11 per-run counters no chart or table read (scope mode,
1228
+ consolidation `processed`/`failedChunks`/`totalChunks`, memory-inference
1229
+ `considered`/`yieldRate`, graph-extraction `failures`, distill
1230
+ `skipped`/`queued`/`llmFailed`, `orphansPurged`); `--group-by run` and
1231
+ `--format md` are unchanged.
1232
+ - **`configVersion` is read, never gated on.** A missing field or `"0.9.0"`
1233
+ loads silently; any other value is named once and read as `0.9.0`.
1234
+ `UNSUPPORTED_CONFIG_VERSION` and `src/core/config/config-version-shim.ts`
1235
+ are gone.
1236
+ - **`akm migrate` converts a task file in one step, whatever its version.**
1237
+ One planner (`scripts/akm-migrate/migrate/task-files.ts`) takes a v2, v3 or
1238
+ v4 file still carrying `schedule[].enabled` to v4 in one pass, with one
1239
+ backup directory per run (`$DATA/backups/tasks/<ts>-<uuid>`); `akm migrate
1240
+ status` reports one `taskFiles` section instead of
1241
+ `taskV3Migration`/`taskV4Migration`. The per-generation steps, their
1242
+ convergence checks and backup pruning, and the writer-relocation step are
1243
+ gone.
1244
+ - **Registry requests use plain `fetch()`.** DNS pinning — a Node child
1245
+ process per request that resolved each registry host, rejected private
1246
+ addresses and pinned the connection — is removed: a registry URL is the
1247
+ built-in one or one an operator configured. `src/registry/network.ts`
1248
+ retries network failures, timeouts, 429 and 5xx with backoff, caps the
1249
+ body, and reports every failure as a classified error, never exit 70:
1250
+ `REGISTRY_NOT_FOUND` and `REGISTRY_RESPONSE_INVALID` exit 1,
1251
+ `REGISTRY_UNREACHABLE` exits 75, `REGISTRY_URL_INVALID` exits 78. A static
1252
+ index whose `version` is not 2 or 3 is read with one warning instead of
1253
+ refused.
1254
+
1255
+ ### Added
1256
+
1257
+ - **Upgrade rehearsal gate** (`tests/integration/upgrade-rehearsal/`,
1258
+ `AKM_UPGRADE_REHEARSAL=1`): installs the previous published `akm-cli`
1259
+ release as a real global npm package, drives it to build a realistic home
1260
+ (a filesystem, git, website, and npm bundle; scheduled and manual tasks; a
1261
+ synced fake crontab), then installs the candidate build OVER it in place —
1262
+ the same prefix a real `npm i -g`/`bun add -g` upgrade replaces — and runs
1263
+ the candidate against that home — `migrate status`/`apply`, `bundle list`
1264
+ with every bundle confirmed enabled, `search`, `show`, plain `task sync`
1265
+ (dry-run and real, no `--rebind`, as an upgrading user actually runs it),
1266
+ executing the generated cron command and confirming it ran the candidate,
1267
+ `health`, `improve --plan` — and finally installs a separate untouched copy
1268
+ of the previous release and runs it back against the candidate-written
1269
+ home. Wired into CI (`.github/workflows/ci.yml`'s new `upgrade-rehearsal`
1270
+ job) and `tests/release-check.sh` (right after packing the release
1271
+ candidate). `.github/workflows/ci.yml` also now runs on pushes to
1272
+ `release/*` branches, which previously had no CI coverage at all.
1273
+ It also proves the fix for the defect above (Fixed, below) two
1274
+ ways: a new first assertion in the "previous"-origin suite runs
1275
+ scheduled-a's generated cron command BEFORE any `migrate` call and
1276
+ confirms `akm-migrate status --host-local` then reports `current` with no
1277
+ manual step in between; and a second, dedicated origin,
1278
+ `KNOWN_UPGRADE_ORIGINS`' fixed `"0.9.15"` (the last release before
1279
+ source-bound scheduler grants), builds a minimal home whose crontab row
1280
+ carries no host-local grant at all — the exact 2026-09-24 shape — and
1281
+ confirms the candidate carries the grant forward and a plain `task sync`
1282
+ afterward does not remove it.
1283
+ - **`akm bundle rename <old> <new>`.** Renaming a bundle used to mean
1284
+ hand-editing the `bundles` key in `config.json`, which stranded every
1285
+ durable ref the tool had minted under the old id — the index and state
1286
+ databases kept the old `<old>//` prefix while config named the new one
1287
+ (the exact hand-rename signature `warnOnBundleRenameDrift` already
1288
+ detected and warned about, with "there is no rekey command in 0.9.0").
1289
+ `akm bundle rename` is that command: under the config lock it rewrites the
1290
+ `bundles` key, `defaultBundle`/`defaultWriteTarget` when they name the old
1291
+ id, and every `scheduler.enabled[].ref` with the old `//` prefix; then it
1292
+ renames the lockfile entry, re-keys every indexed entry's
1293
+ `bundle_id`/`item_ref` and the metadata-enrichment LLM cache's
1294
+ `asset_ref` (in the same `index.db` write, so a rename can't land between
1295
+ the two and strand the cache — the next `akm index` would otherwise treat
1296
+ every renamed asset as stale and re-enrich it through the LLM from
1297
+ scratch), and rewrites this tool's own state rows that name the old bundle
1298
+ (`proposals.ref`, a pending proposal's `proposedTarget.source`, and
1299
+ workflow `task_history.target_ref`). It then re-syncs native scheduler
1300
+ rows under the new name (`akmTasksSync`, run from the command handler and
1301
+ reported in the result's `taskSync` field, never thrown, since
1302
+ config/index/state are already renamed by then), so a scheduled task or
1303
+ workflow stops invoking `<old>//…` the moment the rename applies instead of
1304
+ waiting on a manual `akm task sync`. `taskSync.ok` is `false` both when
1305
+ the sync call itself fails and when it comes back with one or more
1306
+ `taskSync.result.failures` — a binding that failed to prepare has already
1307
+ lost its old native row and is not scheduled again until a retry, so
1308
+ `akm bundle rename` never reports a partial re-sync as a clean one. Refs
1309
+ inside the bundle's own CONTENT
1310
+ (cross-references, a task's `uses:`, `supersededBy`) are reported, never
1311
+ rewritten — the result's `contentRefs` lists the indexed files that still
1312
+ spell the old prefix. `--dry-run` shows the full plan (row counts,
1313
+ scheduler refs, content files, and the installed native scheduler rows a
1314
+ real run's sync would replace) without writing anything.
1315
+
1316
+ ### Removed
1317
+
1318
+ - **Every ranking signal besides the two fused lists.** Search no longer
1319
+ applies exact-name tiers, type, belief-state, tag, search-hint, alias,
1320
+ description, metadata, graph, capture-mode, lesson-strength, pinned-fact or
1321
+ project-context boosts, the utility multiplier, the relaxed-query score
1322
+ ceiling, or the cosine floor on vector-only hits, and it no longer loads
1323
+ the graph snapshot. On the retrieval suite plain whole-document BM25 alone
1324
+ beat the boosted pipeline by 0.156 nDCG@10, and applying the belief-state
1325
+ weights to the fused score lowered nDCG@10 by 0.010 [−0.020, −0.001], so
1326
+ `--belief current` is the way to leave out contradicted or superseded
1327
+ entries. Usage events and utility scores are still recorded (improve's
1328
+ salience and graph extraction read them), and the graph still backs
1329
+ `akm show`'s `related` list and curate's support refs.
1330
+ - **The require-every-word keyword ladder and prefix matching.** The strict
1331
+ AND query, its prefix-AND retry and the OR recovery behind them are gone
1332
+ (OR matching measured 0.108 nDCG@10 better), so a word fragment such as
1333
+ `dock` no longer matches `docker`.
1334
+ - **Fragment hits in search.** Markdown fragments no longer compete as search
1335
+ candidates (whole documents measured 0.059 nDCG@10 better), so search
1336
+ returns whole-document refs and its hits drop `selectedRef`, `parentRef`,
1337
+ `fragmentOrdinal`, `fragmentCount`, the fragment line and size fields and
1338
+ `matchStage`; `akm show <ref>#<fragment>` still selects a section.
1339
+ - **Curate's second-guessing of search:** the per-keyword fallback searches
1340
+ and their max-score merge, the intent and type nudges, skill-family
1341
+ collapse (and the family support refs it produced), and the close-score
1342
+ comparator.
1343
+ - **Retired options.** `akm search --no-project-context` now fails as an
1344
+ unknown flag (exit 2). The config keys `search.minScore`,
1345
+ `search.graphBoost.*` and `improve.utilityDecay.*` have no effect and are
1346
+ kept as unknown keys. Search hits no longer carry the `graph` field, and
1347
+ usage events no longer record `graphExtraction` attribution.
1348
+ - **Guarded source reads around workflow runs.** `akm workflow run` no
1349
+ longer records a read set of every source it touched or re-checks those
1350
+ sources before publishing the run, so editing a command, task, script or
1351
+ env file while a run is being created no longer fails creation; a source
1352
+ that resolves outside its bundle is still refused. `akm workflow plan` no
1353
+ longer prints a `read set:` block, and its JSON drops `sourceReadSet`. At
1354
+ dispatch an env file is re-read from its recorded path (a changed key set
1355
+ is still refused), so replacing or re-cloning the bundle directory no
1356
+ longer fails a unit with "environment owner root physical identity
1357
+ changed". The resume check that refused a run whose stored params row had
1358
+ been edited is gone.
1359
+ - **Drain policies.** `processes.triage.policy` and
1360
+ `processes.triage.maxDiffLines` (config) and `akm proposal drain --policy`
1361
+ / `--max-diff-lines` are retired with `drain-policies.ts`; the flags now
1362
+ fail as unknown (exit 2) and the keys are kept as unknown config keys.
1363
+ - **Improve machinery with no remaining reader:** the collapse detector with
1364
+ its canary set (`scripts/refresh-canary-set.ts`) and cycle metrics, replay
1365
+ selection, the outcome-proxy events, and the never-called anti-collapse
1366
+ merge guards. Retired config keys (kept as unknown keys):
1367
+ `processes.consolidate.antiCollapse.{maxGeneration, lexicalDiversityCheck,
1368
+ mergeInformationFloor, minSpecificityRetention}`,
1369
+ `processes.consolidate.contradictionDetection`,
1370
+ `improve.salience.replayBudget` and `improve.collapseDetector`. Retired
1371
+ events: `improve_salience_first_run`, `improve_replay_selected`,
1372
+ `collapse_detector_alert`, `improve_cycle_metrics_purged`,
1373
+ `outcome_proxy_dead` and `outcome_proxy_inverted`.
1374
+
1375
+ ### Fixed
1376
+
1377
+ - **Re-extracting an unchanged note now replaces its stored graph rows.**
1378
+ `replaceStoredGraph` refreshed only a file's status, reason and run id when
1379
+ its body hash was unchanged, so an extraction of the same body — after a
1380
+ model or prompt change, or after a failed first attempt — never reached
1381
+ `graph_file_entities` or `graph_file_relations`. One install had 1,389 files
1382
+ marked `extracted` with no entity rows while `llm_enrichment_cache` held
1383
+ their extractions. A file's rows are now rewritten whenever its entities or
1384
+ relations differ from the stored ones, so the next graph pass refills such
1385
+ files from the cache without a model call. (`src/indexer/db/graph-db.ts`)
1386
+ - **A graph pass that stops early no longer shrinks the stored graph.** A
1387
+ full scan wrote back only the files it reached, so a budget abort, a
1388
+ failure-rate abort or `processes.graphExtraction.topN` deleted the stored
1389
+ rows of every other file: the 2026-09-26 backfill hit its 4 h budget after
1390
+ 3,358 of 15,165 eligible files, and that prefix became the whole graph. The
1391
+ pass now keeps the rows of every eligible file it did not reach, and of a
1392
+ file whose extraction attempt failed. It drops rows only for a file that
1393
+ left the eligible set — deleted, emptied, now `inferred: true`, or of a type
1394
+ no longer included — which candidate-scoped runs never did; a scan that
1395
+ could not read part of the stash drops nothing. Because kept rows can come
1396
+ from an older extractor, the sweep no longer reuses a stored graph node as a
1397
+ cache hit: only `llm_enrichment_cache`, keyed by extractor, answers for the
1398
+ current one. (`src/indexer/graph/graph-extraction.ts`)
1399
+ - **`graph_meta` counts describe the stored rows.** The extraction pass
1400
+ wrote counts from its in-memory graph (22,304 entities reported against
1401
+ 15,833 stored on one install), and deleting entries overwrote them with raw
1402
+ row counts. Each write now derives them from the stored rows, one meaning
1403
+ each: stored files, files with entity rows, distinct case-folded entities and
1404
+ distinct case-folded relations. The pass result, and with it the
1405
+ `akm improve` summary, reports the same counts. The entries-delete recompute
1406
+ and the in-memory graph deduplicator (`src/indexer/graph/graph-dedup.ts`)
1407
+ are gone. (`src/indexer/db/graph-db.ts`,
1408
+ `src/indexer/graph/graph-extraction.ts`,
1409
+ `src/storage/repositories/index-entries-repository.ts`)
1410
+ - **`akm health` counts graph-extracted files per run.** Its
1411
+ `graphExtraction.extractedFiles` added the whole stored graph's file count
1412
+ once per improve run in the window; it now adds the files each run
1413
+ extracted, as `entities` and `relations` already did.
1414
+ (`src/commands/health/improve-metrics.ts`)
1415
+ - **`akm show`'s `related` refs no longer depend on index row order.** When
1416
+ two entries index the same file, the ref shown for it was whichever row
1417
+ SQLite returned last; the lowest concept id now wins, and shared entity
1418
+ names are read in a fixed order. The ranking itself (most shared entities,
1419
+ then path) was already deterministic. (`src/indexer/graph/graph-related.ts`)
1420
+ - **`akm search`/`akm curate` no longer store a pasted credential verbatim in
1421
+ `state.db`.** The Claude Code hook curates every user prompt, so a
1422
+ credential pasted into a prompt (`PASSWORD=…`, `TOKEN=…`, `SECRET=…`, a
1423
+ `Bearer` header, a JWT, a `ghp_…`/`xox…`/`AKIA…` token, a PEM private key, a
1424
+ `user:pass@` URL, …) flowed straight into the query text and was persisted
1425
+ as-is in both `usage_events.query` and the `events` table's
1426
+ `metadata_json` — a scan of mined queries found 22 credential-like values
1427
+ stored this way. `logSearchEvent`/`logCurateEvent` now redact the query
1428
+ with `redactCredentialPatterns` (extended with the shapes above, plus a
1429
+ `NAME=value`/`NAME: value` pass for names containing password, passwd,
1430
+ secret, token, auth, credential(s), or an api/private key — the value is
1431
+ replaced with `[REDACTED]`, the name is kept so queries stay useful for
1432
+ evaluation) before either write, so a `show`/`select` event tracing back to
1433
+ the search — which copies the search event's already-persisted `query`
1434
+ metadata — inherits the same redacted text. Existing rows already written
1435
+ are not rewritten. (`src/core/redaction.ts`, `src/commands/read/search.ts`,
1436
+ `src/commands/read/curate.ts`)
1437
+ - **`engines.<name>.supportsJsonSchema` on a `kind: "llm"` engine is a known
1438
+ key again.** `LlmConnectionConfigSchema` declares it and `llm/client.ts`
1439
+ reads it, but the named-engine object (`LlmEngineSchema`) never listed it,
1440
+ so this release's unknown-key walk named it on every load and `akm migrate
1441
+ apply` would have deleted a live setting from config.json.
1442
+ (`src/core/config/schema/engines.ts`)
1443
+ - **`akm migrate` finds the leaked activity registry where earlier releases
1444
+ actually wrote it.** The `deadResidue` step looked for
1445
+ `maintenance-activities/` under `$STATE`; the maintenance barrier created it
1446
+ next to its own lock under `$DATA`, so the directory that had grown to
1447
+ 229,943 four-kilobyte sidecars (927 MB) on one host was never reported or
1448
+ removed. Both roots are checked, the registry is reported as one entry rather
1449
+ than once per sidecar, and a directory already listed whole is not descended
1450
+ into by the sidecar scan. (`scripts/akm-migrate/migrate/dead-residue.ts`)
1451
+ - **`akm bundle add`'s `--name` is now a contract on every add path (local,
1452
+ website, registry), not a hint.** An explicit `--name` that is not a legal
1453
+ bundle slug, or that is already taken by a different bundle, used to fall
1454
+ back silently — `deriveBundleId` minted a derived name, or a `-<hash>`
1455
+ suffix — so `akm bundle add ... --name my.bundle` installed under a name
1456
+ the caller never asked for, without saying so. It now fails with a
1457
+ `UsageError` (exit 2) naming the rule, before any write (config, lock, or
1458
+ network sync). Re-adding an already-installed ref under a *different*
1459
+ `--name` than it already carries used to keep the existing key and say
1460
+ nothing; it now fails the same way, naming the existing key and
1461
+ `akm bundle rename <old> <new>`. A DERIVED name (no `--name` given) is
1462
+ unaffected and keeps `deriveBundleId`'s forgiving `-<hash>` uniqueness
1463
+ fallback. Every `akm bundle add` result (local, website, and registry) now
1464
+ also carries `bundleId` (the resolved bundle key), and a registry add's
1465
+ result always carries `registryId` (the registry install id) rather than
1466
+ only when it happens to differ from `bundleId`, so a caller no longer has
1467
+ to reconstruct the key from `sourceAdded`/`installed`.
1468
+ - **`akm bundle add <registry ref> --name <name>` now keys the bundle by
1469
+ `<name>`.** For npm, `github:` and Git refs, `--name` was accepted and then
1470
+ dropped before the bundle key was derived, so the bundle was keyed by the
1471
+ basename of its materialized cache directory instead — `extracted`, or
1472
+ `extracted-<hash>` once that was taken — and its assets were only
1473
+ addressable as `extracted//…`. The name now goes through the same
1474
+ slug-legality and uniqueness rules as a local or website add. The install's
1475
+ registry id is still recorded as `registryId`, so `akm bundle update` and
1476
+ `akm bundle remove` keep resolving the original ref. Re-adding a ref that is
1477
+ already installed keeps its existing key, as local and website re-adds do.
1478
+ - **A registry bundle added without `--name` is keyed by its package or repo
1479
+ name instead of `extracted`.** `akm bundle add npm:<pkg>` now creates bundle
1480
+ `<pkg>` (`npm:@scope/pkg` → `pkg`), and `github:owner/repo` or a Git URL
1481
+ ending in `/repo` creates `repo` — the mapping the bundle schema already
1482
+ documented for `registryId`. The key used to come from the basename of the
1483
+ cache directory the package was unpacked into, which is always `extracted`,
1484
+ so every registry bundle after the first was `extracted-<hash>`. A dotted
1485
+ or mixed-case name is slugged like a directory name (`Foo.js` → `foo-js`).
1486
+ Bundles that are already installed keep their current key, including
1487
+ `extracted`, because every recorded `extracted//…` ref depends on it.
1488
+ - **A one-file change in a large directory no longer costs `akm index` half
1489
+ an hour.** Both full-text tables keyed their per-entry deletes on
1490
+ `entry_id`, an unindexed FTS5 column, so every upsert scanned the whole
1491
+ full-text index, twice per entry per run. On a 23.9k-entry index, one
1492
+ touched file in a flat `knowledge/` directory of 13.7k entries took 26
1493
+ minutes (task run `2026-09-24T20-30-01-663Z`); on backup copies of that
1494
+ index the same rescan took 31 minutes before this change and takes 38 s
1495
+ after it. FTS rows are keyed by rowid, and the
1496
+ first writable open after upgrading realigns an existing index in place,
1497
+ about 10 s and ~1.1 GB peak memory at that size, with no index-generation
1498
+ bump, so an older binary keeps reading it. A writable open realigns again
1499
+ if an older binary sharing the generation has written rows since.
1500
+ - **One bad scheduler-sync item, or one bad migration step, no longer fails
1501
+ the whole operation.** `akm task sync` used to throw and abort the entire
1502
+ reconciliation over one binding it could not reconcile or one bundle whose
1503
+ sources failed to read; that binding or bundle is now reported in the sync
1504
+ result's `failures: [{path, ref?, reason}]` (documented in
1505
+ `docs/reference/cli.md`) while every other one still syncs (see Changed).
1506
+ `akm-migrate`'s
1507
+ `runMigration` (`scripts/akm-migrate/run-migrate.ts`) now runs every step
1508
+ under its own catch too: a step's own throw (or, under `apply`, its
1509
+ read-only fallback failing as well) is recorded in the plan's new
1510
+ `failedSteps: [{step, error}]` and forces `status: "blocked"` instead of
1511
+ ending the run with no plan at all — the remaining steps still run in
1512
+ order. `akm migrate status|apply` (`scripts/akm-migrate/main.ts`) already
1513
+ exits 1 for any blocked plan, so a poisoned step no longer exits the
1514
+ internal-error code 70 with nothing printed.
1515
+ - **The legacy `stashDir`/`sources[]`/`installed` config shape is persisted
1516
+ by `akm migrate apply`, and an empty one no longer fails every command**
1517
+ (#863). `migrateLegacySourceShape`
1518
+ (`src/core/config/legacy-source-shape-shim.ts`) has always converted a
1519
+ usable `stashDir`/`sources[]`/`installed` in memory on every load and told
1520
+ the user to run `akm migrate apply` to make that stick, but nothing on disk
1521
+ ever did; the migrator's `configFile` step now writes that current shape
1522
+ back once, under a backup. Separately, through 0.9.16 and 0.9.17-alpha.3 a
1523
+ config whose `sources` was `[]` (what 0.8.9's `akm source remove` writes
1524
+ after the last source is removed) or whose `stashDir` was empty or
1525
+ unusable failed every command with exit 78; it now loads, with the shim's
1526
+ one-time warning.
1527
+ - **A `version: 2` or `version: 3` task source reads and runs again instead
1528
+ of failing closed on upgrade.** `e413af024` deleted the in-memory
1529
+ v2/v3 -> v4 read shim on the argument that "untrusted source cannot carry
1530
+ obsolete activation semantics" — but activation had already moved to
1531
+ host-local `scheduler.enabled` in that same commit, so the shim never
1532
+ carried activation in the first place, and deleting it just reintroduced
1533
+ the exact upgrade break 0.9.4 originally shipped the shim to fix ("would
1534
+ have broken every pre-0.9.4 scheduled task headlessly on upgrade").
1535
+ `parseTaskSource` (`src/tasks/source/parse-task-source.ts`) once again
1536
+ routes `version: 2`/`version: 3` through the SAME pure planners
1537
+ `akm migrate apply` uses, entirely in memory, with a one-line stderr
1538
+ deprecation warning (once per file per process) and no disk write; the
1539
+ parsed document never carries a source-owned `enabled` field, since the
1540
+ v3->v4 planner already never hoists `akm.enabled` or a schedule entry's
1541
+ `enabled` key. Only a v2/v3 document the deterministic conversion itself
1542
+ cannot resolve still fails with `TASK_SCHEMA_VERSION_UNSUPPORTED`, naming
1543
+ the specific blocked reason. The same in-memory shim now also tolerates a
1544
+ declared `version: 4` document whose `schedule[]` still carries a
1545
+ per-entry `enabled` key — 0.9.15's v4 grammar accepted it (`akm task add
1546
+ --disabled` wrote it), this release's does not, and without this the
1547
+ upgrade break above recurs for every 0.9.15-authored scheduled task. The
1548
+ key is stripped without ever being read — `enabled: false` cannot
1549
+ suppress a granted task and `enabled: true` cannot schedule an ungranted
1550
+ one, since activation stays host-local `scheduler.enabled`. `akm task
1551
+ validate` reports such a file `converts` (`sourceVersion` still `4`)
1552
+ instead of `valid`, since it read through the shim rather than the direct
1553
+ v4 path.
1554
+ - **Lock contention exits 75, like `state.db` contention.** Another process
1555
+ holding `akm.lock`'s write sentinel (`LOCKFILE_CONTENDED`, was a config
1556
+ error, exit 78) or the asset-mutation writer lease past its wait
1557
+ (`ASSET_MUTATION_LEASE_HELD`, was an unclassified error, exit 70) is now a
1558
+ retry-shortly `TransientError`, exit 75.
1559
+ - **`akm health`'s `state.db` repair steps no longer corrupt the rebuilt
1560
+ file.** `state-db-integrity` used to suggest `.dump` into a new file with
1561
+ no writer stop; it now says to back up `state.db`, `.recover` it into
1562
+ `state.new.db`, confirm that passes `quick_check`, stop every akm process,
1563
+ delete `state.db-wal` and `state.db-shm`, then swap the new file in — a
1564
+ leftover WAL replays onto the new database and corrupts it.
1565
+ - **The package launcher (`dist/akm`) passes `--scheduler-context` through to
1566
+ the CLI** instead of re-validating the descriptor with a stale copy of its
1567
+ schema, which rejected every descriptor 0.9.17 writes.
1568
+ (`scripts/node-runtime/akm`)
1569
+ - **A `task_history` row with a malformed `engine` value decodes.** The
1570
+ decoder used to reject the whole row when `engine` was present but not a
1571
+ string or `null`; it now drops the bad value and decodes the rest, the
1572
+ tolerance it already applied to every other unrecognized field.
1573
+ (`src/storage/repositories/task-history-repository.ts`)
1574
+ - **The LLM enrichment budget warning prints for every index run.** When the
1575
+ metadata-enrichment pass ran out of its wall-clock budget during an index
1576
+ another command started (`akm bundle update`, `akm setup`, `akm bundle
1577
+ add`, improve's preflight, a read command's auto-index), it stopped
1578
+ silently; it now prints the same "LLM enrichment budget exceeded" warning
1579
+ `akm index` does.
1580
+
1581
+ ## [0.9.17-alpha.3] - 2026-09-24
1582
+
1583
+ ### Fixed
1584
+
1585
+ - **Unscoped `akm task sync` no longer aborts the whole host on the first
1586
+ bundle root that happens to contain any symlink.** `captureGuardedDirectoryManifest`
1587
+ threw for every symbolic directory entry it listed, even one the scheduler
1588
+ never reads (e.g. a third-party skill repo's `CLAUDE.md -> AGENTS.md`) —
1589
+ `SchedulerSourceCollector` manifests every scanned bundle's root, so one
1590
+ such bundle among many enabled ones failed sync entirely, dry-run included.
1591
+ A symlink that stays inside its bundle root is now recorded in the guarded
1592
+ directory manifest as its own `"symlink"` kind, identified without
1593
+ following it (its `readlink` text plus its no-follow `lstat` identity), so
1594
+ change detection still works; it is never read or descended into. A symlink
1595
+ sitting exactly where a task or workflow source lives (a `.yml` under
1596
+ `tasks/`, any `.yml` under an `akm-task` bundle, or a workflow-named file
1597
+ under `workflows/`) is reported as its own per-source failure — "is a
1598
+ symbolic source; guarded reads require a regular no-follow owner" — and its
1599
+ ref is not scheduled, even when a real sibling file shares that ref, while
1600
+ every other task and workflow still reconciles. A
1601
+ symlink that resolves outside the bundle root, or one that is broken and
1602
+ cannot be identified safely, is still refused, and a bundle root whose
1603
+ `tasks` or `workflows` entry is itself a symlink still refuses loudly,
1604
+ since that is a schedulable source location.
1605
+
1606
+ ## [0.9.17-alpha.2] - 2026-09-24
1607
+
1608
+ ### Fixed
1609
+
1610
+ - **A config carrying the retired `experimental.workflowEngine` key no longer
1611
+ fails to load.** `ExperimentalConfigSchema` moved from `.passthrough()` to
1612
+ `.strict()` in 0.9.16 (`cc6152e02`), after `workflowEngine` had already been
1613
+ removed from it in `e0655d13c`; a real config a 0.9.15 install wrote (whose
1614
+ passthrough still accepted the key) then failed every command with
1615
+ `Invalid config: experimental: Unrecognized key(s) in object: 'workflowEngine'`.
1616
+ The config loader now strips known-retired `experimental.*` keys in memory
1617
+ before validation, warning once and naming `akm migrate apply`; a genuinely
1618
+ unknown/misspelled key (e.g. `improveAutonomyy`) still fails closed.
1619
+ `akm migrate apply` removes the retired key from `config.json` on disk
1620
+ (with the usual backup), and `--dry-run` reports the pending removal.
1621
+ - **Unscoped `akm task sync` no longer crashes when an enabled website or npm
1622
+ bundle is configured.** The sync plan loop resolved every active source
1623
+ through the write-target resolver, which rejects any kind other than
1624
+ `filesystem`/`git` outright (writes, and therefore scheduler state, are
1625
+ undefined for those kinds — the same rejection `akm task enable` already
1626
+ hit). Unscoped sync now skips non-filesystem/git bundles when building
1627
+ install operations — they never carried schedulable tasks — while
1628
+ inactive-bundle removal/revocation still sees them. A scoped
1629
+ `akm task sync --bundle <website-or-npm-bundle>` now fails with a clear
1630
+ usage error instead of the write-target `ConfigError`.
1631
+
1632
+ ## [0.9.17-alpha.1] - 2026-09-24
1633
+
1634
+ ### Added
1635
+
1636
+ - **`akm improve --require-engines` now records its reachability probe on the
1637
+ run result (R17).** `assertRequiredEnginesReachable` only ever reported a
1638
+ failure (abort, exit 78); a probe that passed — including a slow or
1639
+ flapping gateway that still answered in time — left no trace once the run
1640
+ proceeded. It now returns one outcome per probed target (`process`,
1641
+ `engine`, `endpoint`, `reachable`, `latencyMs`), threaded through a new
1642
+ `AkmImproveOptions.engineProbe` and copied onto the persisted result as
1643
+ `AkmImproveResult.engineProbe`. Omitted entirely when `--require-engines`
1644
+ was not passed; a result persisted without it (every run before this
1645
+ change) still decodes. `--require-engines --dry-run` results carry it too.
1646
+ - **Reflect had no way to exclude raw wiki-ingest snapshots, which are the
1647
+ longest generations in the ledger (89.5s/161.8s observed).** `wikis/articles/raw/*.md`
1648
+ website snapshots index as `knowledge/wikis/articles/raw/<slug>`, and
1649
+ reflect's `allowedTypes` filter is type-only, so it can't exclude a subset
1650
+ of the `knowledge` type. `processes.reflect` now accepts an optional
1651
+ `excludeRefPrefixes: string[]` — conceptId prefixes, matched after
1652
+ stripping an optional `bundle//` from both the ref and each prefix.
1653
+ `shouldSkipRef` skips a matching ref with reason `exclude-filter`, for
1654
+ reflect only (distill and consolidate are memory-only and reject the key).
1655
+ A trailing `/` on a prefix is ignored, so
1656
+ `"knowledge/wikis/articles/raw/"` excludes the same refs as
1657
+ `"knowledge/wikis/articles/raw"`.
1658
+
1659
+ - **`akm health` now checks state.db's own SQLite integrity.** A new hard
1660
+ `state-db-integrity` check runs a read-only `PRAGMA quick_check` against
1661
+ `state.db` and fails, naming the returned diagnostic lines and the repair
1662
+ steps (back up, dump/restore via `sqlite3`, verify, swap in), when it
1663
+ reports anything other than `ok`. The same check reports state.db's
1664
+ freelist ratio (the fraction of pages `VACUUM` could reclaim) and warns
1665
+ above 50%. Previously nothing in `akm health` looked past a successful
1666
+ append/read round trip, which stays true on a database that is corrupt at
1667
+ the SQLite level.
1668
+ - **The retention purge (`akm improve`) now VACUUMs state.db when more than
1669
+ half its pages are free**, immediately after the events/improve_runs/
1670
+ cycle-metrics purge, recording a `state_db_vacuumed` event with pages
1671
+ before/after. Opportunistic: a locked/busy database is skipped, not
1672
+ raised, so it never fails the purge pass it follows.
1673
+
1674
+ ### Changed
1675
+
1676
+ - **The orphan-state GC pass no longer probes index.db once per pending
1677
+ row.** `runOrphanStateGcPass` used to call `getEntryByRef` (up to two
1678
+ statements each, via its bare-ref fallback) for every pending
1679
+ `asset_salience` / `asset_outcome` row — 2,101 pending rows cost 83–100s
1680
+ per run. It now builds one snapshot of every live `item_ref` in index.db up
1681
+ front and matches every pending row against it in memory: O(1) index.db
1682
+ queries per run instead of one probe per row, with the same live/orphan
1683
+ resolution (including the bundle-qualified-exact and bare-conceptId-suffix
1684
+ fallback) as before.
1685
+ - **Memory inference no longer forces a full reindex for the file(s) it
1686
+ writes.** The post-inference maintenance step used to call the full
1687
+ `reindexFn` (42–220s per run, typically for one written derived fact)
1688
+ whenever memory inference split a parent. `runMemoryInferencePass` now
1689
+ reports the exact paths it wrote or rewrote (`writtenPaths`, sourced from
1690
+ the run's write-provenance journal), and the maintenance pass indexes just
1691
+ those files with `indexWrittenAssets` instead — closing and reopening the
1692
+ shared index.db handle around the call with the same discipline the full
1693
+ reindex used (#584). The separate post-consolidation full reindex is
1694
+ removed outright rather than re-gated: it used to fire whenever
1695
+ `consolidation.processed > 0` (memories the LLM judged), but
1696
+ merge/delete/contradict ops are advisory and never auto-applied, and the
1697
+ one op that does execute — promote — writes a proposal to state.db, not to
1698
+ the stash. Consolidation therefore cannot change a file the index reads,
1699
+ so the reindex had no precondition it could ever satisfy.
1700
+ - **The improve loop's reflect dispatch now checks the proposal
1701
+ fingerprint/rejection-backoff guard *before* calling reflect, not just
1702
+ after.** `fingerprint_match` and `rejection_backoff` were evaluated only
1703
+ inside `createProposal`, which runs after reflect's full generation and
1704
+ quality-judge call — so a ref already guaranteed to be skipped still paid
1705
+ the LLM cost (measured: 2–16% of reflect LLM seconds spent on refs the
1706
+ guard then discarded). The guard's fingerprint is an input fingerprint
1707
+ (target ref, source, before-hash, model id), computable before dispatch, so
1708
+ `checkProposalGuard` (`src/commands/proposal/repository.ts`) exposes the
1709
+ identical check `createProposal` runs post-generation — the two share one
1710
+ implementation and can never disagree. `runLoopReflectPass`
1711
+ (`src/commands/improve/loop-stages.ts`) now calls it first; a hit skips
1712
+ `reflectFn` entirely and lands in the existing `reflect-cooldown` bucket
1713
+ with the same `reflect_invoked` event the signal-delta cursor
1714
+ (`buildLatestProposalTsMap`) reads, so cursor advancement and run-result
1715
+ classification are unchanged. `createProposal`'s post-generation check
1716
+ remains the authoritative gate.
1717
+ - **Consolidate's plan schema and prompt are promote-only.** The apply loop
1718
+ only ever executed `promote` — `merge`/`delete`/`contradict` were advisory
1719
+ by design and never applied — but the schema still asked for all four ops
1720
+ plus a free-text `warnings` array, and completion tokens rose from 7–8k to
1721
+ 21–30k per run after the 35B-A3B model switch with no change in
1722
+ promotions. `CONSOLIDATE_PLAN_JSON_SCHEMA` and `consolidate-system.md` now
1723
+ request only `promote` (with `reason` capped at 200 chars), and `isValidOp`
1724
+ rejects any other op shape — e.g. from a model that ignores the schema —
1725
+ with the existing "skipping invalid operation" warning instead of treating
1726
+ it as an actionable plan entry. `ConsolidateResult.merged` / `deleted` /
1727
+ `contradicted` and the `planned` op breakdown are unchanged in shape and
1728
+ stay zero.
1729
+ - **`improve-maintenance-passes.test.ts` moved under `tests/integration/`.**
1730
+ The suite opens a real `state.db` via `openStateDatabase`, which AGENTS.md's
1731
+ ORG-03..06 rule places under `tests/integration/`, not `tests/`; no content
1732
+ change. Also corrected
1733
+ `docs/architecture/specs/improve-collapse-churn-detector-design.md` §2.5,
1734
+ which described the post-loop collapse-detector gate as `consolidationRan
1735
+ OR recombination.processed > 0` — no `recombination` value is plumbed into
1736
+ `runImprovePostLoopStage` and no recombine pass exists in the codebase, so
1737
+ the spec now matches the shipped `consolidationRan`-only gate and notes
1738
+ that the recombine-triggered pass is not implemented.
1739
+ - **Graph-extraction relations are now compact `[from, type, to]` triples
1740
+ instead of `{"from","to","type"}` objects, and the batch graph-extraction
1741
+ call now sends a `responseSchema`.** The object-keyed form cost 10+ tokens
1742
+ per relation for no signal, and completion tokens cost far more than
1743
+ prompt tokens; a compact triple form measured −51% / −18% completion
1744
+ tokens on two chunks. `graph-extract.ts`'s single-asset and batch prompts
1745
+ and JSON schemas now ask for `["from", "type", "to"]` (`type` may be `""`);
1746
+ `parseGraphExtraction` accepts both the triple form and the legacy object
1747
+ form (a relation-level `confidence` is still read from a legacy object,
1748
+ though the schema no longer offers it — the prompt never asked for one).
1749
+ Separately, production runs graph extraction batched
1750
+ (`processes.graphExtraction.batchSize`), and `extractGraphFromBodies` sent
1751
+ no `responseSchema` at all, so the R12b output-bounding schema only ever
1752
+ reached the single-asset path. The batch call now sends the same
1753
+ `maxItems`-bounded schema (scoped to the batch's asset count) through the
1754
+ same `supportsJsonSchema`-gated `responseSchema` field the single-asset
1755
+ call uses. `GRAPH_EXTRACT_PROMPT_VERSION` bumps `v2` → `v3`, so every file
1756
+ re-extracts once on the next graph pass — entity semantics, caps, chunking
1757
+ and batch sizing are unchanged.
1758
+
1759
+ ### Removed
1760
+
1761
+ - **The write-only distill/proposal eval-cases path.** `writeEvalCase`
1762
+ (`src/commands/improve/eval-cases.ts`) wrote a Markdown file per rejection
1763
+ under `$STATE/improve/eval-cases/<stash>/` that nothing ever read back, and
1764
+ `countEvalCases` reported a cumulative on-disk file count as if it were a
1765
+ per-run number (surfaced as `evalCasesWritten` on the improve result and in
1766
+ `akm health`'s improve metrics). A rejected proposal row (see above) now
1767
+ carries the same information through a path something actually reads.
1768
+ Deleted `eval-cases.ts` and its two `loop-stages.ts` call sites, the
1769
+ `evalCasesWritten` field from `AkmImproveResult` and every health-metrics
1770
+ reader/aggregator, and the `improve_completed` event's `evalCasesWritten`
1771
+ field. `decodeImproveResult` still accepts (and ignores) `evalCasesWritten`
1772
+ on an envelope an older release wrote, and existing eval-case files on disk
1773
+ are untouched — `getEvalCasesDir` (`core/paths.ts`) stays, since
1774
+ `scripts/akm-migrate/migrate/writer-relocation.ts` still uses it to
1775
+ relocate them from the legacy `$STASH/.akm/eval-cases/` path.
1776
+
1777
+ ### Fixed
1778
+
1779
+ - **The lesson quality judge's ACTIONABILITY criterion carried no signal, and
1780
+ the judge's request/parser let a differently-spelled or extra key change
1781
+ the verdict (R16).** Splinter measured ACTIONABILITY at AUC 0.46 against
1782
+ accept/reject outcomes — no better than chance — and averaging it into the
1783
+ score pulled every verdict toward its 3.0 mode, i.e. the review band.
1784
+ `buildJudgePrompt` no longer asks for it;
1785
+ `LESSON_JUDGE_CRITERIA_KEYS` is now `novelty`/`nonRedundancy` only.
1786
+ Separately, `runQualityJudge`'s request sent no `responseSchema` while the
1787
+ prompt text spelled criteria as NON-REDUNDANCY / FEEDBACK ALIGNMENT, so a
1788
+ model that echoed a differently-cased or -spelled key turned the verdict
1789
+ into a parse failure routed to review; and `parseJudgeResponse` averaged
1790
+ over every key present in `scores`, so an unexpected extra key changed the
1791
+ score. `runQualityJudge` now sends a strict `responseSchema` — built from
1792
+ the judge's own expected criteria keys, `additionalProperties: false` at
1793
+ both levels — through the same `supportsJsonSchema`-gated
1794
+ `request.responseSchema` path `src/llm/graph-extract.ts` uses, a no-op for
1795
+ providers that don't opt in; and `parseJudgeResponse` now reads, validates,
1796
+ and averages only the expected keys, silently ignoring any other key
1797
+ instead of averaging or validating it. A missing expected key is still a
1798
+ parse failure, unchanged.
1799
+ - **The reflect quality-gate's "no judge configured" warning named a config
1800
+ key nothing reads.** It told users to set
1801
+ `improve.strategies.<name>.processes.reflect.qualityGate.engine`, but
1802
+ `qualityGate` is `{ enabled }` passthrough — `resolveReflectQualityJudgeRunner`
1803
+ always uses the generation runner when it is an LLM, or falls back to
1804
+ `defaults.llmEngine` via `resolveImproveLlmExecution` with no profile/process
1805
+ layer, so that key was never read. The warning now names only
1806
+ `defaults.llmEngine`.
1807
+ - **The distill/reflect LLM-as-judge quality gate inherited the generation
1808
+ runner's temperature, and its averaged score hid which criterion actually
1809
+ failed.** `runQualityJudge`'s request only pinned `enableThinking: false`,
1810
+ so the judge ran at whatever temperature generation used — measured at 0.3,
1811
+ the verdict flipped on 10/16 identical inputs, vs. 0/16 at temperature 0.
1812
+ The request now also pins `temperature: 0`, for both the distill and
1813
+ reflect judges that share this function, independent of the runner's
1814
+ configured temperature. Separately, both judge prompts asked for one
1815
+ averaged float, so a criterion carrying no signal was invisible in
1816
+ production. They now ask for per-criterion integer scores
1817
+ (`buildJudgePrompt`: novelty/actionability/nonRedundancy;
1818
+ `buildReflectJudgePrompt`: feedbackAlignment/preservation/quality), averaged
1819
+ in code to the same `score` the unchanged 3.5/2.5 thresholds gate on. The
1820
+ parser accepts this new `{"scores": {...}, "reason"}` shape and still
1821
+ accepts the old `{"score": <float>, "reason"}` shape a model may return;
1822
+ each criterion (or the bare score) must be a finite number in 1..5 or the
1823
+ response routes to review exactly as a parse failure does today. The
1824
+ per-criterion scores, when present, are now carried through
1825
+ `QualityJudgeResult.criteria` into the `distill_invoked` event metadata and
1826
+ rejection-envelope frontmatter `writeQualityRejection` writes, and into
1827
+ reflect's `reflect_completed` rejection event as `qualityCriteria`.
1828
+ - **The judge parser accepted a partial `scores` object and auto-passed it.**
1829
+ `parseJudgeResponse` validated only that whatever criterion keys arrived
1830
+ held finite 1-5 values, then averaged over those keys alone — so a
1831
+ truncated judge response like `{"scores": {"novelty": 5}, "reason": "…"}`
1832
+ parsed to `score: 5.0` and `pass: true`, promoting content the judge never
1833
+ finished evaluating on its other criteria. `runQualityJudge` now passes the
1834
+ criterion key set its prompt asked for (`buildJudgePrompt`:
1835
+ novelty/actionability/nonRedundancy; `buildReflectJudgePrompt`:
1836
+ feedbackAlignment/preservation/quality) down to `parseJudgeResponse`, which
1837
+ returns a parse failure — routed to review, exactly as a malformed response
1838
+ is today — when any expected key is missing from `scores`.
1839
+ - **The reflect pre-generation proposal-guard skip (R9) emitted `reflect_invoked` with no paired `reflect_completed`.** `runLoopReflectPass`'s guard-skip branch in `loop-stages.ts` appended a synthetic `reflect_invoked` event to advance the signal-delta cursor, but never called `reflectFn`, so `reflect.ts`'s own `reflect_completed` emission never ran either — a new, permanent source of unpaired `reflect_invoked` rows for every fingerprint/backoff hit, violating the invoke/complete pairing invariant `buildReflectEventEmitters` documents. The branch now also appends a matching `reflect_completed` (`ok:false`, `reason:"cooldown"`, `subreason:"pre_generation_guard"`), mirroring `emitFailed`'s shape.
1840
+ - **R9's pre-generation proposal guard covered reflect only — distill paid for a full generation + judge call before the same fingerprint/backoff guard could reject it.** `runLoopDistillPass` had no equivalent of `runLoopReflectPass`'s pre-check, even though `createProposal`'s post-generation guard (and every rejected row R10 now mints under `source: "distill"`) applies to distill just as much. `runLoopDistillPass` now calls `checkProposalGuard` against the derived lesson/knowledge ref (distill's real `createProposal` call never targets the input ref) before dispatching `distillFn`; a hit routes to the pass's existing `distill-skipped` bucket and emits `distill_invoked` with a `skipped` outcome so `buildLatestProposalTsMap`'s signal cursor still advances.
1841
+ - **The distill pre-generation proposal guard could suppress a legitimate dispatch by checking a ref distill would never target.** For a memory input, distill's real `createProposal` call targets one of two refs decided at dispatch time inside `planMemoryKnowledgePromotion` — the derived knowledge ref when the deterministic promotion heuristic fires, the derived lesson ref otherwise — but `runLoopDistillPass`'s pre-check checked both candidate refs and skipped on the FIRST guard hit, so a stale fingerprint/backoff hit on the ref distill would NOT have targeted silently suppressed dispatch until that ref's fingerprint happened to change. The pre-check now resolves the SAME target `planMemoryKnowledgePromotion` would via `wouldPromoteMemoryToKnowledge` (`distill/promote-memory.ts`) — a thin wrapper that delegates to `planMemoryKnowledgePromotion` itself so the classification can never drift from the real dispatch decision, with no LLM call — and checks only that ref; content and the classification's `durableInputRef` are read via `planned.ref` alone, matching `akmDistill`'s real dispatch, while `planned.itemRef ?? planned.ref` feeds only the feedback-events query.
1842
+ - **The `akm improve` triage pre-pass drain's judgment LLM calls were unattributed in the usage report.** `runTriagePrePass`'s `drainProposalsFn` call dispatched judgment calls with no `withLlmStage` wrapper, unlike the standalone `akm proposal drain` CLI path, so they landed in `byProcessEngineModel` as unattributed (5 calls, 24s per run) instead of under a `triage` stage. The pre-pass drain is now wrapped in `withLlmStage("triage", …, { engine, process: "triage.judgment" })`, mirroring the CLI path.
1843
+ - **The batch graph-extraction provider-storm guard only recognized one error
1844
+ code.** After a failed batch call, `extractGraphFromBodies` skipped the
1845
+ per-asset fallback retry only for `LlmCallError`s coded `provider_error` —
1846
+ but a dead endpoint more often raises `network_error` (a dropped
1847
+ connection) or `provider_html_error` (a provider serving an HTML error
1848
+ page), both of which still paid the full per-asset fallback storm the
1849
+ guard exists to prevent. The predicate is now `isTransportFailure`
1850
+ (`src/llm/client.ts`), shared with `chatCompletion`'s retry classifier so
1851
+ the two cannot drift apart, and covers `provider_error`, `network_error`,
1852
+ and `provider_html_error`.
1853
+ - **`akm health`'s `state-db-integrity` check no longer crashes when the freelist/page-count read fails.** `getStateDbFreelistInfo` had a `finally` but no `catch` around its read-only open and pragma reads, unlike its sibling `runStateDbQuickCheck` — a throw there (e.g. an unopenable state.db) escaped `akm health` as an unclassified exit 70 on exactly the damaged database the check exists to report. It now returns a zeroed `StateDbFreelistInfo` with an `error` field, and the check renders that as a failed check instead of throwing.
1854
+ - **The post-purge VACUUM's `state_db_vacuumed` event now honors the caller's `EventsContext`.** `vacuumStateDbIfReclaimable` appended its event with a direct `insertEvent` call, bypassing `EventsContext.readOnly` and the injectable clock its sibling purge events (`events_purged`, `improve_runs_purged`, `improve_cycle_metrics_purged`) use in the same `runRetentionPurgePass` callback. It now appends the event via `appendEvent` with the caller's `EventsContext` plumbed through.
1855
+ - **Consolidate's per-chunk prompt excerpt truncated the raw file (frontmatter
1856
+ + body) instead of the body.** `buildChunkPrompt` sliced `body.slice(0,
1857
+ bodyTruncation)` off the unstripped file; a memory whose frontmatter alone
1858
+ exceeded the excerpt length was judged on metadata only and never showed
1859
+ its own body text. The excerpt now truncates `stripFrontmatterBody(body)`;
1860
+ hot/queued detection is unchanged and still reads the raw body.
1861
+ - **Consolidate's chunk prompt carried an unused ~14k-char standards block and
1862
+ a header the model sometimes echoed back as a bogus `ref`.** Every chunk
1863
+ prompt resolved and injected a "Standards to follow" section
1864
+ (`resolveStandardsContext("memories/_consolidated", ...)`), but the chunk
1865
+ output is a promote-only op list that never reads it. Separately, the
1866
+ chunk header (`Chunk N of M, memories <first>–<last>:`) named the chunk's
1867
+ boundary memories with an en dash between two `memories/<name>` refs; on
1868
+ 2026-09-24 the judge model returned promote ops whose `ref` was exactly
1869
+ that `memories/<first>–memories/<last>` range, naming a memory that does
1870
+ not exist and losing the promotion. `buildChunkPrompt` no longer takes a
1871
+ `standardsContext` and the header is now
1872
+ `Chunk N of M (<count> memories):` — no refs in it.
1873
+ - **Consolidate re-judged memories that were already promoted verbatim into
1874
+ `knowledge/`.** That duplication was previously discovered only after the
1875
+ LLM (`shouldSkipPromotionBodyDuplicate`), so a pool where the large
1876
+ majority of memories were already-promoted duplicates still paid the full
1877
+ chunk/LLM cost on all of them before being skipped.
1878
+ `inspectConsolidationPool` now drops those memories before any chunking or
1879
+ LLM work, sharing one `loadExistingKnowledgeBodyHashes` call and the same
1880
+ `cacheHash` domain with the post-LLM check so the two cannot disagree. The
1881
+ dropped count is reported as `prefilteredAlreadyPromoted` on the
1882
+ consolidate result and in a warning line. The pre-filter also now runs
1883
+ *before* the `consolidate.limit` cap (previously after), so a run with a
1884
+ limit set selects its oldest-modified window from the pre-filtered pool
1885
+ instead of re-selecting and re-dropping the same permanently-undeletable
1886
+ duplicates every run while fresh memories past the cap went unreached; the
1887
+ preview/eligibility path (`preparation.ts`) computes and passes the same
1888
+ hash set so the reported candidate pool agrees with what the run will act
1889
+ on. A live (non-preview) `akm improve` run reuses that same hash set for
1890
+ the actual `akmConsolidate` call instead of recomputing it, so a run still
1891
+ walks `knowledge/` only once.
1892
+ - **`improve`'s start-of-run index rescan ran after triage dirtied the stash,
1893
+ not before it.** Proposal triage promotes accepted proposals straight into
1894
+ the flat `knowledge/` root, and the blocking `ensureIndex` call that is
1895
+ supposed to give the run a current index ran only afterward (inside
1896
+ `collectEligibleRefs`'s setup), so every triage promotion guaranteed the
1897
+ very full rescan it should have preceded — up to ~27 minutes, holding the
1898
+ index lock against co-scheduled writers. `ensureIndex` now runs before the
1899
+ triage pre-pass, and triage's own writes are indexed incrementally
1900
+ (`indexWrittenAssets`) so `collectEligibleRefs` still sees them without a
1901
+ second full walk. Because `indexWrittenAssets` upserts a file's
1902
+ `content_hash` without bumping `builtAt`, index staleness detection
1903
+ (`ensure-index.ts`) is now per-file: a file newer than the last build is
1904
+ only treated as stale when its current content actually differs from what
1905
+ is indexed, so incrementally-reindexed content stops re-triggering the
1906
+ same full rescan on every subsequent run. The implicit reindex's timing
1907
+ breakdown (walk/llm/embed/finalize), previously discarded, is now logged
1908
+ at verbose level and surfaced on the improve result as `ensureIndexDurationMs`.
1909
+ - **Distill quality rejections vanished instead of persisting, so backoff and
1910
+ Reflexion never saw them and the same ref was re-selected and re-rejected
1911
+ on every run** (two refs were rejected 11× and 10×). `writeQualityRejection`
1912
+ wrote only a `$STATE`-side file and an event, never a `proposals` row, so
1913
+ `rejection_backoff`/`fingerprint_match` (proposal/repository.ts) and the
1914
+ Reflexion "previously rejected" context had nothing to find; the distill
1915
+ signal-delta cursor (`buildLatestProposalTsMap`) also only advanced for
1916
+ `queued`/`skipped`/`validation_failed` outcomes, so a rejected ref stayed
1917
+ eligible forever. `writeQualityRejection` now mints a real proposal through
1918
+ the same `createProposal`/`archiveProposal` path every other proposal
1919
+ source uses: a `quality_rejected` outcome is minted pending then archived
1920
+ to `rejected` carrying the judge's reason; a `review_needed` outcome stays
1921
+ `pending` in the normal queue, where triage — a human, or the drain's
1922
+ judgment tier when one is configured — decides, the same path every other
1923
+ pending distill proposal (including quality-gate passes) already takes.
1924
+ The cursor now also advances on both outcomes (still excluding
1925
+ `llm_failed`, where no real attempt produced anything). A retry for the
1926
+ same target, source, and model is skipped by `fingerprint_match` (the
1927
+ input fingerprint recorded at mint, retained `archiveRetentionDays`,
1928
+ default 90 days); the 30-day `rejection_backoff` window only applies once
1929
+ the target's before-hash or the model differs. Because these machine
1930
+ rejections are now real `rejected` rows under `source: "distill"`, `akm
1931
+ health`'s distill accept rate (`computeAcceptRateBySource`,
1932
+ src/commands/health/accept-rate.ts) drops relative to earlier releases and
1933
+ no longer measures reviewer acceptance alone. Nothing gates on that
1934
+ metric.
1935
+ - **`writeQualityRejection` could throw instead of returning a rejection
1936
+ result.** Minting the proposal row above runs the mint-time canonical
1937
+ validator (`createProposal` → `rejectProposal`,
1938
+ `src/commands/proposal/repository.ts`), which throws `UsageError` for
1939
+ structurally-invalid content — e.g. a `lessons/` ref whose body lacks
1940
+ `description`/`when_to_use`. `writeQualityRejection` is the terminal,
1941
+ non-throwing rejection path and none of its callers handled a throw. The
1942
+ proposal row is bookkeeping for backoff/Reflexion, never the authoritative
1943
+ record of the rejection, so a validator throw now degrades to "no row
1944
+ minted" — the envelope file and `distill_invoked` event are still written,
1945
+ matching the existing fingerprint/backoff skip behavior.
1946
+ - **A `review_needed` quality-gate rejection could be auto-promoted by the
1947
+ triage drain's judgment tier with no human ever seeing it.**
1948
+ `writeQualityRejection` minted a `review_needed` outcome as an ordinary
1949
+ pending proposal under `source: "distill"` (knowledge promotions from
1950
+ `promote-memory.ts` take the same path); the `personal-stash` drain policy
1951
+ defers `distill` proposals to the judgment tier, which can auto-accept
1952
+ under `applyMode: promote` + `experimental.improveAutonomy` — so content
1953
+ the quality judge explicitly refused to auto-queue (the 2.5–3.5
1954
+ review-needed band) could be promoted without a human in the loop.
1955
+ `writeQualityRejection` now stamps a `review_needed` mint with a
1956
+ `{ outcome: "deferred", reason: "quality-review", gate: "quality-gate" }`
1957
+ gate decision (best-effort: a stamp failure warns and continues, like the
1958
+ existing mint/archive tolerance), and `classifyPendingProposals`
1959
+ (`proposal/drain.ts`) skips any pending row carrying it — leaving it
1960
+ pending and untouched, before the drain's own policy-deferred re-stamp
1961
+ loop would otherwise overwrite the stamp.
1962
+ - **Consolidate's post-LLM promote-dedup hash double-stripped frontmatter.**
1963
+ `shouldSkipPromotionBodyDuplicate`'s `bodyHash` was computed as
1964
+ `cacheHash(parseFrontmatter(memoryContent).content.trim())` — the body was
1965
+ already frontmatter-stripped before being handed to `cacheHash`, which
1966
+ strips it again internally — diverging from the single-strip
1967
+ `cacheHash(raw)` domain `loadExistingKnowledgeBodyHashes` and the pre-filter
1968
+ use for a source memory body that begins with its own `---` block. The
1969
+ check now hashes `cacheHash(memoryContent)` directly, so the two sides of
1970
+ the dedup comparison agree.
1971
+ - **Consolidate's per-chunk prompt still warned against proposing `delete`
1972
+ for `(captureMode: hot)` memories.** The consolidate op schema and system
1973
+ prompt dropped `delete` (along with `merge`/`contradict`), leaving
1974
+ `buildChunkPrompt`'s top-of-prompt hot-ref block as the only remaining
1975
+ mention of `delete` anywhere in the prompt — a retired op name that
1976
+ `isValidOp` now rejects if the model echoes it back, wasting tokens on
1977
+ "skipping invalid operation" warnings. The block and the `hotRefs`
1978
+ collection that fed it are removed; the inline `(captureMode: hot)`
1979
+ annotation on each memory line is unchanged.
1980
+ - **Graph extraction sent no `json_schema` and no per-asset chunk cap, so a
1981
+ long file could pay for dozens of LLM calls whose output was then sliced
1982
+ down to the same 32-entity/32-relation limit anyway** (one file spent 21 of
1983
+ 27 calls and 12.9k completion tokens this way). The single-asset extraction
1984
+ call (`extractGraphFromBody`) now sends a `responseSchema` (entities/
1985
+ relations capped at 32 each, `additionalProperties: false` otherwise), via
1986
+ the same `supportsJsonSchema`-gated request path memory-infer.ts uses — no
1987
+ `maxTokens` is sent; cost is bounded by the schema's `maxItems` caps alone,
1988
+ per AGENTS.md's "LLM Defaults" (a hardcoded cap risked silent truncation
1989
+ with zero headroom for JSON punctuation or reasoning tokens). A body
1990
+ chunked beyond the new
1991
+ `processes.graphExtraction.maxChunksPerAsset` (default 8) now stops after
1992
+ the first N chunks instead of processing every one; the skipped chunks are
1993
+ reported as `truncatedChunks` in the run's graph-extraction telemetry so
1994
+ the coverage loss is visible rather than silently absorbed. The `improve`
1995
+ loop's dispatch (`loop-stages.ts`) now also forwards a configured
1996
+ `maxChunksPerAsset` to the extraction call, mirroring the existing
1997
+ `topN`/`batchSize` wiring — without this the config key had no effect in a
1998
+ real `akm improve` run and the default of 8 always applied.
1999
+ - **The graph-extraction `responseSchema` forbade the `confidence` field the
2000
+ parser itself reads.** `additionalProperties: false` on both the root
2001
+ object and each relation item made `confidence` impossible on a
2002
+ `supportsJsonSchema` provider, even though `parseGraphExtraction` uses
2003
+ `rel.confidence` to drop relations below `MIN_RELATION_CONFIDENCE` and
2004
+ `item.confidence` to feed the merged extraction confidence — silently
2005
+ turning the confidence filter into dead code on exactly the providers the
2006
+ schema targets. `confidence: {"type": "number"}` is now allowed at both
2007
+ levels; `additionalProperties: false` still forbids anything else.
2008
+ - **A pending proposal went stale the moment akm's own bookkeeping touched
2009
+ its target, and promote refused it forever (R20).** `resolveProposalTargetInfo`
2010
+ captured the target's raw `beforeHash` at mint; the SAME nightly run's
2011
+ `writeSalienceToFrontmatter` (distill) and memory inference's
2012
+ `inferenceProcessed` stamp then rewrote the target's frontmatter before
2013
+ promote ran, so `promoteProposalWithLease`'s guard (`repository.ts` ~L2406)
2014
+ and `drain.ts`'s dry-run mirror (`assertProposalTargetFresh`) refused every
2015
+ affected proposal with "target changed after proposal was created" — the
2016
+ same 11+ reflect proposals, every day, on splinter. `resolveProposalTargetInfo`
2017
+ now also captures `beforeHashNormalized` (`core/asset/frontmatter.ts`'s new
2018
+ `computeNormalizedContentHash`, over the target with
2019
+ `BOOKKEEPING_FRONTMATTER_KEYS` — `salience`/`salienceInputs`/`inferenceProcessed`
2020
+ — stripped and the remaining frontmatter canonically re-serialized); the
2021
+ promote guard and its dry-run mirror both prefer it over the raw
2022
+ `beforeHash` when present, so a bookkeeping-only rewrite no longer stales a
2023
+ proposal out while a real content change still refuses. Promotion also now
2024
+ carries the live target's bookkeeping keys forward
2025
+ (`carryForwardBookkeepingFrontmatter`) when the proposal's own frontmatter
2026
+ doesn't set them, so accepting never drops `inferenceProcessed` and forces
2027
+ memory inference to reprocess the memory. A legacy proposal minted before
2028
+ this field existed keeps its exact original raw-hash check.
2029
+ `computeNormalizedContentHash` also normalizes the body boundary the same
2030
+ way `assembleAssetFromString` does (leading newlines stripped, exactly one
2031
+ trailing newline) before hashing, and treats an empty frontmatter block as
2032
+ `{}` instead of falling back to the raw hash — both `writeSalienceToFrontmatter`
2033
+ and the memory-inference `assembleAsset` rewrite shift where the body starts,
2034
+ which without this normalization still staled a proposal out unless the
2035
+ target's frontmatter was already in that exact on-disk shape.
2036
+ - **A stale-target promote failure was retried, and refused, identically
2037
+ every drain run forever (R20).** The drain already categorized a
2038
+ "target changed/was created after proposal" failure as `stale-target`
2039
+ (`categorizeDrainFailure`), but left the row pending either way — so the
2040
+ same proposals failed the same way on every subsequent `akm proposal
2041
+ drain` / triage pass. Both promote-failure sites (`drainProposals`'s
2042
+ deterministic loop and `runJudgmentTier`) now auto-reject a stale-target
2043
+ failure once, stamping `gateDecision: { outcome: "auto-rejected", reason:
2044
+ "stale-target" }` instead of leaving it to retry. This is not a merit
2045
+ rejection, so `checkFingerprintAndBackoff`'s rejection-backoff window
2046
+ (`repository.ts`) now excludes stale-target rows — the ref stays
2047
+ re-proposable against its current content — and the Reflexion
2048
+ "previously rejected" context (`reflect.ts`'s `readRejectedProposals`,
2049
+ `distill.ts`'s `buildDistillMessages`) and the accept-rate health metric
2050
+ (`health/accept-rate.ts`) now exclude stale-target rejections too, so a
2051
+ procedural refusal doesn't misrepresent content quality. `--dry-run` now
2052
+ predicts the same outcome: a stale-target promote failure it detects is
2053
+ reported under `rejected`, matching what a real run does, instead of under
2054
+ `failed`.
2055
+
2056
+ - **Reflect quality-gate rejections were mislabelled `parse_error` and fed
2057
+ back into later prompts as learned "avoid" patterns.** When the reflect
2058
+ quality judge rejected an otherwise well-parsed proposal, the result
2059
+ carried `reason: "parse_error"` — a real parse failure and a judge
2060
+ rejection were indistinguishable. The improve loop injects non-excluded
2061
+ reflect failures into the next reflect prompt's "Avoid These Patterns"
2062
+ block, so a single gate rejection could poison every subsequent candidate
2063
+ in the run. Judge rejections now carry a distinct `quality_rejected`
2064
+ reason, stay in the `reflect-failed` metrics bucket, and are excluded from
2065
+ that avoid-patterns injection like the existing deterministic skips.
2066
+ - **Legacy rejected proposals no longer throw before reflect/distill prompt
2067
+ dispatch.** `readRejectedProposals` (reflect.ts) and the equivalent mapper
2068
+ in distill.ts built their "previously rejected" context via
2069
+ `proposalContent(p)`, which throws when a proposal's `changes[0]?.after` is
2070
+ undefined — the shape `storedToChanges` deliberately returns for rows
2071
+ archived before the `changes` field existed (the large majority of
2072
+ real-world rejected-proposal history). The throw happened before the
2073
+ signal cursor advanced, so a ref with any such legacy rejection errored on
2074
+ every run instead of ever completing. Both call sites now read the preview
2075
+ from `payload.content`, which is populated for every row regardless of
2076
+ its `changes` shape.
2077
+ - **Failed graph extractions are no longer cached as permanent hits.** A
2078
+ provider outage upserted thousands of `{"entities":[],"status":"failed"}`
2079
+ results into `llm_enrichment_cache` and the persisted graph, and both
2080
+ cache-hit paths (the DB lookup and reuse from the previous graph) treated
2081
+ them as valid hits forever after — the affected files never retried.
2082
+ `status: "failed"` results are now treated as a miss and are never written
2083
+ to the cache; existing rows are left on disk and are overwritten naturally
2084
+ on the next successful extraction. `src/llm/graph-extract.ts` also no
2085
+ longer falls back to a per-asset retry for every body in a batch after a
2086
+ `provider_error` — the provider has already demonstrated it is failing, so
2087
+ each asset in that batch is recorded as failed directly. Graph extraction
2088
+ now aborts the rest of the run (returning the partial results already
2089
+ extracted) once the failure rate crosses 50% over at least 4 attempted
2090
+ extraction dispatches, mirroring consolidate's existing failure-rate guard.
2091
+ The abort counts one attempt per `extractGraphFromBodies` dispatch, not per
2092
+ file inside its batch — per-file counting let a single batched
2093
+ `provider_error` trip the guard after one HTTP failure whenever
2094
+ `graphExtractionBatchSize` was at its default of 4.
2095
+ - **Memory consolidation's cooldown could never engage.** `consolidate_completed`
2096
+ was only emitted when a run planned zero merge/delete/contradict operations —
2097
+ advisory ops the model plans daily and that are never auto-applied — so the
2098
+ event had, in practice, never fired and the pool-delta gate stayed
2099
+ permanently in its bootstrap "run every time" state. The event now fires
2100
+ whenever the LLM pass itself completes, recording the unapplied advisory op
2101
+ count (`advisoryOpsUnapplied`) instead of withholding the event. Separately,
2102
+ the memory-volume override (`memoryVolumeConsolidationThreshold`, forcing a
2103
+ run when the eligible pool exceeds the threshold) is now bootstrap-only: once
2104
+ a `consolidate_completed` event exists for the source, the pool-delta gate
2105
+ governs on its own, even when the pool is large. `akm improve --plan`'s
2106
+ `consolidation.gates.delta.reason` no longer reports "memory pool has work"
2107
+ for both a real pool delta and the bootstrap case (no `consolidate_completed`
2108
+ event yet, so no delta was evaluated) — bootstrap now reports its own reason.
2109
+
9
2110
  ## [0.9.16] - 2026-09-22
10
2111
 
11
2112
  ### Fixed