akm-cli 0.9.1 → 0.9.2-alpha.2

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 (350) hide show
  1. package/CHANGELOG.md +103 -28
  2. package/README.md +3 -1
  3. package/SECURITY.md +1 -1
  4. package/STABILITY.md +1 -1
  5. package/dist/akm +2 -2
  6. package/dist/akm-migrate +2 -2
  7. package/dist/assets/hints/cli-hints-full.md +14 -9
  8. package/dist/assets/improve-strategies/proactive-maintenance.json +1 -1
  9. package/dist/assets/improve-strategies/reflect-distill.json +1 -1
  10. package/dist/assets/models.json +35 -0
  11. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +3 -4
  12. package/dist/assets/stash-skeleton/facts/conventions/organization.md +1 -3
  13. package/dist/assets/tasks/core/extract.yml +6 -5
  14. package/dist/assets/tasks/core/improve.yml +6 -5
  15. package/dist/assets/tasks/core/index-refresh.yml +6 -5
  16. package/dist/assets/tasks/core/sync.yml +6 -5
  17. package/dist/assets/tasks/core/version-check.yml +6 -5
  18. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +6 -5
  19. package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -5
  20. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +6 -5
  21. package/dist/assets/tasks/improve/akm-improve-frequent.yml +6 -5
  22. package/dist/assets/tasks/improve/akm-improve-nightly.yml +6 -5
  23. package/dist/cli/confirm.js +2 -2
  24. package/dist/cli/parse-args.js +3 -24
  25. package/dist/cli/retired-commands.js +1 -1
  26. package/dist/cli/shared.js +2 -2
  27. package/dist/cli.js +11 -9
  28. package/dist/commands/agent/agent-dispatch.js +55 -89
  29. package/dist/commands/agent/contribute-cli.js +12 -45
  30. package/dist/commands/command/builtin-action.js +32 -0
  31. package/dist/commands/command/command-cli.js +99 -0
  32. package/dist/commands/command/command-execution.js +308 -0
  33. package/dist/commands/command/execution-source-loader.js +176 -0
  34. package/dist/commands/command/portable-template.js +60 -0
  35. package/dist/commands/config-cli.js +10 -4
  36. package/dist/commands/env/env.js +4 -2
  37. package/dist/commands/feedback-cli.js +1 -1
  38. package/dist/commands/health/checks.js +241 -29
  39. package/dist/commands/health/html-report.js +0 -14
  40. package/dist/commands/health/report-view-model.js +0 -1
  41. package/dist/commands/health/surfaces.js +6 -7
  42. package/dist/commands/health/types.js +0 -2
  43. package/dist/commands/health.js +63 -18
  44. package/dist/commands/improve/collapse-detector.js +5 -6
  45. package/dist/commands/improve/consolidate.js +251 -214
  46. package/dist/commands/improve/distill/promote-memory.js +71 -34
  47. package/dist/commands/improve/distill/quality-gate.js +17 -5
  48. package/dist/commands/improve/distill.js +232 -155
  49. package/dist/commands/improve/eligibility.js +112 -79
  50. package/dist/commands/improve/execution.js +57 -0
  51. package/dist/commands/improve/extract-cli.js +5 -5
  52. package/dist/commands/improve/extract-prompt.js +64 -22
  53. package/dist/commands/improve/extract.js +608 -360
  54. package/dist/commands/improve/improve-strategies.js +43 -14
  55. package/dist/commands/improve/improve.js +249 -29
  56. package/dist/commands/improve/loop-stages.js +11 -17
  57. package/dist/commands/improve/memory/memory-contradiction-detect.js +90 -66
  58. package/dist/commands/improve/outcome-loop.js +22 -38
  59. package/dist/commands/improve/planner.js +134 -0
  60. package/dist/commands/improve/preparation.js +730 -409
  61. package/dist/commands/improve/reflect.js +386 -223
  62. package/dist/commands/improve/run-context.js +3 -4
  63. package/dist/commands/improve/salience.js +6 -58
  64. package/dist/commands/improve/session-asset.js +12 -12
  65. package/dist/commands/lint/index.js +101 -29
  66. package/dist/commands/migrate-cli.js +11 -69
  67. package/dist/commands/migration-tool.js +6 -9
  68. package/dist/commands/models-cli.js +27 -0
  69. package/dist/commands/proposal/drain.js +258 -186
  70. package/dist/commands/proposal/proposal-cli.js +32 -10
  71. package/dist/commands/proposal/proposal.js +2 -5
  72. package/dist/commands/proposal/propose.js +192 -172
  73. package/dist/commands/proposal/repository.js +54 -91
  74. package/dist/commands/proposal/validators/proposal-validators.js +9 -7
  75. package/dist/commands/read/curate.js +53 -22
  76. package/dist/commands/read/registry-search.js +25 -9
  77. package/dist/commands/read/remember-cli.js +14 -2
  78. package/dist/commands/read/search.js +10 -4
  79. package/dist/commands/read/show.js +139 -153
  80. package/dist/commands/registry-cli.js +16 -7
  81. package/dist/commands/remember.js +33 -18
  82. package/dist/commands/sources/add-cli.js +19 -178
  83. package/dist/commands/sources/bundle-cli.js +15 -3
  84. package/dist/commands/sources/dangerous-env-audit.js +135 -0
  85. package/dist/commands/sources/info.js +2 -1
  86. package/dist/commands/sources/installed-stashes.js +901 -177
  87. package/dist/commands/sources/schema-repair.js +174 -95
  88. package/dist/commands/sources/self-update.js +30 -74
  89. package/dist/commands/sources/source-add.js +3 -5
  90. package/dist/commands/sources/sources-cli.js +2 -15
  91. package/dist/commands/sources/update-transaction.js +220 -0
  92. package/dist/commands/tasks/tasks-cli.js +3 -3
  93. package/dist/commands/tasks/tasks.js +736 -317
  94. package/dist/commands/workflow-cli.js +2 -2
  95. package/dist/core/adapter/adapters/agent-skills-adapter.js +3 -0
  96. package/dist/core/adapter/adapters/akm-adapter.js +85 -35
  97. package/dist/core/adapter/adapters/akm-lint.js +54 -39
  98. package/dist/core/adapter/adapters/akm-metadata.js +45 -45
  99. package/dist/core/adapter/adapters/akm-task-adapter.js +32 -49
  100. package/dist/core/adapter/adapters/akm-workflow-adapter.js +38 -23
  101. package/dist/core/adapter/adapters/dotenv-adapter.js +30 -1
  102. package/dist/core/adapter/adapters/generic-files-adapter.js +11 -0
  103. package/dist/core/adapter/adapters/index.js +0 -9
  104. package/dist/core/adapter/adapters/llm-wiki-adapter.js +4 -0
  105. package/dist/core/adapter/adapters/okf-adapter.js +4 -0
  106. package/dist/core/adapter/adapters/opencode-adapter.js +5 -8
  107. package/dist/core/adapter/adapters/tool-dir-shared.js +63 -6
  108. package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -0
  109. package/dist/core/adapter/execution-source.js +308 -0
  110. package/dist/core/adapter/recognize-match.js +36 -13
  111. package/dist/core/adapter/registry.js +0 -9
  112. package/dist/core/asset/stash-meta.js +94 -4
  113. package/dist/core/common.js +6 -11
  114. package/dist/core/config/config-io.js +3 -3
  115. package/dist/core/config/config-schema.js +18 -40
  116. package/dist/core/config/config-sources.js +11 -21
  117. package/dist/core/config/config-walker.js +31 -13
  118. package/dist/core/config/config.js +23 -26
  119. package/dist/core/config/schema/engines.js +8 -7
  120. package/dist/core/config/schema/improve-processes.js +29 -5
  121. package/dist/core/config/schema/index-config.js +0 -27
  122. package/dist/core/config/schema/primitives.js +1 -23
  123. package/dist/core/config/schema/sources-bundles.js +13 -16
  124. package/dist/core/errors.js +2 -0
  125. package/dist/core/events.js +68 -32
  126. package/dist/core/extra-params.js +1 -0
  127. package/dist/core/improve-result.js +315 -0
  128. package/dist/core/lesson-lint.js +0 -6
  129. package/dist/core/maintenance-barrier.js +4 -4
  130. package/dist/core/network-policy.js +152 -0
  131. package/dist/core/paths.js +1 -1
  132. package/dist/core/recognition-util.js +4 -4
  133. package/dist/core/registry-url.js +456 -0
  134. package/dist/core/state/migrations.js +161 -47
  135. package/dist/core/state-db.js +453 -80
  136. package/dist/core/system-error.js +32 -0
  137. package/dist/core/time.js +2 -12
  138. package/dist/core/write-source.js +0 -18
  139. package/dist/execution/directory-identity.js +52 -0
  140. package/dist/execution/executable-identity.js +107 -0
  141. package/dist/execution/guarded-source.js +398 -0
  142. package/dist/execution/json.js +95 -0
  143. package/dist/{commands/health/types-session-log.js → execution/limits.js} +2 -1
  144. package/dist/execution/record.js +55 -0
  145. package/dist/execution/resolved-request.js +730 -0
  146. package/dist/execution/source.js +320 -0
  147. package/dist/indexer/bundle-identity-guard.js +5 -4
  148. package/dist/indexer/db/graph-db.js +33 -0
  149. package/dist/indexer/graph/graph-boost.js +3 -4
  150. package/dist/indexer/graph/graph-extraction.js +562 -373
  151. package/dist/indexer/index-written-assets.js +78 -39
  152. package/dist/indexer/indexer.js +471 -432
  153. package/dist/indexer/installations.js +6 -0
  154. package/dist/indexer/lookup/adapter-concept-owner.js +283 -0
  155. package/dist/indexer/materialize-embeddings.js +155 -0
  156. package/dist/indexer/passes/memory-inference.js +227 -174
  157. package/dist/indexer/passes/metadata.js +263 -118
  158. package/dist/indexer/scan/doc-to-entry.js +7 -10
  159. package/dist/indexer/scan/drain-dir.js +51 -23
  160. package/dist/indexer/search/db-search.js +156 -50
  161. package/dist/indexer/search/fts-query.js +40 -40
  162. package/dist/indexer/search/ranking.js +36 -1
  163. package/dist/indexer/search/search-attribution.js +3 -1
  164. package/dist/indexer/search/search-fields.js +23 -14
  165. package/dist/indexer/search/search-hit-enrichers.js +1 -1
  166. package/dist/indexer/search/search-source.js +7 -16
  167. package/dist/indexer/search/semantic-status.js +10 -1
  168. package/dist/indexer/usage/show-usage.js +105 -0
  169. package/dist/indexer/usage/usage-events.js +7 -2
  170. package/dist/indexer/walk/matchers.js +40 -10
  171. package/dist/indexer/walk/path-resolver.js +5 -2
  172. package/dist/indexer/walk/walker.js +20 -2
  173. package/dist/integrations/agent/builder-shared.js +3 -6
  174. package/dist/integrations/agent/conversation-fallback.js +16 -0
  175. package/dist/integrations/agent/engine-resolution.js +87 -87
  176. package/dist/integrations/agent/execution-cascade.js +566 -0
  177. package/dist/integrations/agent/execution-definitions.js +211 -0
  178. package/dist/integrations/agent/execution-lowering.js +811 -0
  179. package/dist/integrations/agent/execution-preparation.js +67 -0
  180. package/dist/integrations/agent/index.js +0 -2
  181. package/dist/integrations/agent/inline-execution.js +74 -0
  182. package/dist/integrations/agent/model-map.js +515 -0
  183. package/dist/integrations/agent/persona-fallback.js +30 -0
  184. package/dist/integrations/agent/request-lowering.js +186 -0
  185. package/dist/integrations/agent/runner-dispatch.js +230 -37
  186. package/dist/integrations/agent/runner.js +12 -83
  187. package/dist/integrations/harnesses/aider/agent-builder.js +8 -0
  188. package/dist/integrations/harnesses/aider/index.js +0 -1
  189. package/dist/integrations/harnesses/amazonq/agent-builder.js +8 -0
  190. package/dist/integrations/harnesses/amazonq/index.js +0 -1
  191. package/dist/integrations/harnesses/claude/agent-builder.js +14 -1
  192. package/dist/integrations/harnesses/claude/index.js +1 -5
  193. package/dist/integrations/harnesses/claude/session-log.js +3 -33
  194. package/dist/integrations/harnesses/codex/agent-builder.js +8 -0
  195. package/dist/integrations/harnesses/codex/index.js +0 -1
  196. package/dist/integrations/harnesses/copilot/agent-builder.js +8 -0
  197. package/dist/integrations/harnesses/copilot/index.js +0 -1
  198. package/dist/integrations/harnesses/gemini/agent-builder.js +8 -0
  199. package/dist/integrations/harnesses/gemini/index.js +0 -1
  200. package/dist/integrations/harnesses/index.js +4 -44
  201. package/dist/integrations/harnesses/opencode/agent-builder.js +16 -9
  202. package/dist/integrations/harnesses/opencode/index.js +0 -2
  203. package/dist/integrations/harnesses/opencode/session-log.js +14 -204
  204. package/dist/integrations/harnesses/opencode-sdk/harness.js +12 -1
  205. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +40 -42
  206. package/dist/integrations/harnesses/openhands/agent-builder.js +8 -0
  207. package/dist/integrations/harnesses/openhands/index.js +0 -1
  208. package/dist/integrations/harnesses/pi/agent-builder.js +8 -0
  209. package/dist/integrations/harnesses/pi/index.js +0 -1
  210. package/dist/integrations/harnesses/shared.js +0 -1
  211. package/dist/integrations/harnesses/types.js +1 -3
  212. package/dist/integrations/lockfile.js +82 -79
  213. package/dist/integrations/session-logs/index.js +6 -17
  214. package/dist/integrations/session-logs/provider-base.js +1 -29
  215. package/dist/llm/client.js +10 -5
  216. package/dist/llm/embedder.js +6 -7
  217. package/dist/llm/embedders/local.js +37 -88
  218. package/dist/llm/embedders/types.js +1 -1
  219. package/dist/llm/graph-extract.js +75 -50
  220. package/dist/llm/index-passes.js +43 -5
  221. package/dist/llm/memory-infer.js +8 -6
  222. package/dist/llm/metadata-enhance.js +5 -3
  223. package/dist/llm/structured-call.js +122 -25
  224. package/dist/output/format-exempt.js +1 -1
  225. package/dist/output/render-registry.js +0 -16
  226. package/dist/output/renderers.js +12 -7
  227. package/dist/output/shapes/curate.js +1 -0
  228. package/dist/output/shapes/helpers.js +10 -2
  229. package/dist/output/shapes/passthrough.js +2 -0
  230. package/dist/output/text/command-format.js +31 -33
  231. package/dist/output/text/health-format.js +1 -29
  232. package/dist/output/text/migrate.js +6 -56
  233. package/dist/output/text/proposal-format.js +16 -1
  234. package/dist/output/text/workflow-format.js +16 -0
  235. package/dist/registry/network.js +279 -0
  236. package/dist/registry/pinned-request-helper.js +247 -0
  237. package/dist/registry/pinned-transport.js +717 -0
  238. package/dist/registry/providers/skills-sh.js +18 -6
  239. package/dist/registry/providers/static-index.js +20 -7
  240. package/dist/registry/resolve.js +53 -28
  241. package/dist/scripts/akm-migrate-node.js +19334 -52269
  242. package/dist/scripts/akm-migrate.js +19270 -51612
  243. package/dist/setup/registry-stash-loader.js +64 -20
  244. package/dist/setup/semantic-assets.js +9 -34
  245. package/dist/setup/setup.js +12 -30
  246. package/dist/setup/source-identity.js +17 -0
  247. package/dist/setup/steps/sources.js +36 -15
  248. package/dist/setup/steps/tasks.js +39 -11
  249. package/dist/sources/providers/git-provider.js +3 -3
  250. package/dist/sources/providers/npm.js +2 -2
  251. package/dist/sources/providers/provider-utils.js +4 -3
  252. package/dist/sources/providers/website.js +11 -7
  253. package/dist/sources/snapshot-fetchers/host-guard.js +9 -136
  254. package/dist/sources/snapshot-fetchers/website-ingest.js +25 -109
  255. package/dist/sources/website-url.js +73 -0
  256. package/dist/storage/engines/sqlite-migrations.js +81 -26
  257. package/dist/storage/managed-db.js +27 -24
  258. package/dist/storage/repositories/events-repository.js +3 -0
  259. package/dist/storage/repositories/index-connection.js +42 -10
  260. package/dist/storage/repositories/index-entries-repository.js +203 -229
  261. package/dist/storage/repositories/index-entry-mapper.js +8 -12
  262. package/dist/storage/repositories/index-entry-schema.js +255 -0
  263. package/dist/storage/repositories/index-fts-repository.js +64 -71
  264. package/dist/storage/repositories/index-llm-cache-repository.js +8 -13
  265. package/dist/storage/repositories/index-meta-repository.js +0 -11
  266. package/dist/storage/repositories/index-schema.js +74 -350
  267. package/dist/storage/repositories/index-utility-repository.js +12 -17
  268. package/dist/storage/repositories/index-vec-repository.js +56 -7
  269. package/dist/storage/repositories/proposals-repository.js +4 -127
  270. package/dist/storage/repositories/registry-cache.js +2 -1
  271. package/dist/storage/repositories/task-history-repository.js +20 -40
  272. package/dist/storage/repositories/workflow-runs-repository.js +228 -129
  273. package/dist/storage/sqlite-read-snapshot.js +148 -0
  274. package/dist/tasks/backends/cron.js +170 -42
  275. package/dist/tasks/backends/index.js +1 -1
  276. package/dist/tasks/backends/launchd.js +787 -202
  277. package/dist/tasks/backends/schtasks.js +282 -83
  278. package/dist/tasks/embedded.js +7 -7
  279. package/dist/tasks/frozen-script.js +50 -0
  280. package/dist/tasks/resolve-akm-bin.js +5 -1
  281. package/dist/tasks/runner.js +239 -251
  282. package/dist/tasks/runtime-v3.js +281 -0
  283. package/dist/tasks/scheduler-binding.js +272 -0
  284. package/dist/tasks/scheduler-invocation.js +57 -43
  285. package/dist/tasks/scheduler-sync.js +654 -0
  286. package/dist/tasks/source-v3.js +752 -0
  287. package/dist/tasks/standalone-script-entry.js +5 -0
  288. package/dist/tasks/task-id.js +29 -0
  289. package/dist/workflows/authoring/authoring.js +15 -32
  290. package/dist/workflows/exec/dispatch-redaction.js +14 -8
  291. package/dist/workflows/exec/exec-unit.js +7 -28
  292. package/dist/workflows/exec/frozen-judge.js +57 -89
  293. package/dist/workflows/exec/lowering-notices.js +23 -0
  294. package/dist/workflows/exec/native-executor.js +301 -458
  295. package/dist/workflows/exec/param-secrets.js +4 -3
  296. package/dist/workflows/exec/run-workflow.js +26 -32
  297. package/dist/workflows/exec/step-work.js +105 -109
  298. package/dist/workflows/exec/unit-dispatch.js +103 -27
  299. package/dist/workflows/exec/unit-writer.js +3 -3
  300. package/dist/workflows/exec/worktree.js +2 -2
  301. package/dist/workflows/ir/compile.js +86 -72
  302. package/dist/workflows/ir/environment-v4.js +328 -0
  303. package/dist/workflows/ir/freeze-v4.js +122 -0
  304. package/dist/workflows/ir/plan-hash.js +13 -7
  305. package/dist/workflows/ir/schema-v4.js +525 -0
  306. package/dist/workflows/ir/schema.js +25 -284
  307. package/dist/workflows/ir/source-freeze-v4.js +506 -0
  308. package/dist/workflows/parser.js +27 -24
  309. package/dist/workflows/program/schema.js +1 -2
  310. package/dist/workflows/renderer.js +42 -29
  311. package/dist/workflows/resource-limits.js +4 -5
  312. package/dist/workflows/runtime/agent-identity.js +11 -13
  313. package/dist/workflows/runtime/plan-classifier.js +8 -8
  314. package/dist/workflows/runtime/runs.js +27 -43
  315. package/dist/workflows/runtime/workflow-asset-loader.js +45 -205
  316. package/dist/workflows/source-files.js +373 -0
  317. package/dist/workflows/source-ir/compile.js +196 -0
  318. package/dist/workflows/source-ir/github-yaml.js +577 -0
  319. package/dist/workflows/source-ir/ordering.js +38 -0
  320. package/dist/workflows/source-ir/program.js +50 -0
  321. package/dist/workflows/source-ir/result.js +26 -0
  322. package/dist/workflows/source-ir/schema.js +772 -0
  323. package/dist/workflows/source-ir/semantics.js +242 -0
  324. package/dist/workflows/source-ir/uses.js +14 -0
  325. package/docs/README.md +2 -0
  326. package/docs/migration/README.md +3 -1
  327. package/docs/migration/release-notes/0.9.2.md +55 -0
  328. package/docs/migration/release-notes/README.md +5 -0
  329. package/docs/migration/v0.8-to-v0.9.md +76 -1077
  330. package/docs/migration/v0.9.0-troubleshooting.md +104 -516
  331. package/docs/migration/v0.9.1-to-v0.9.2.md +150 -0
  332. package/docs/reference/README.md +1 -0
  333. package/docs/reference/cli.md +230 -98
  334. package/docs/reference/configuration.md +159 -36
  335. package/docs/reference/data-and-telemetry.md +19 -1
  336. package/docs/reference/supported-formats.md +23 -3
  337. package/docs/reference/tasks.md +182 -0
  338. package/docs/reference/workflow-schema.md +91 -40
  339. package/docs/reference/workflows.md +33 -6
  340. package/package.json +10 -6
  341. package/schemas/akm-config.json +372 -224
  342. package/schemas/akm-task.json +324 -80
  343. package/schemas/akm-workflow.json +6 -9
  344. package/dist/core/migration-operation.js +0 -75
  345. package/dist/integrations/agent/model-aliases.js +0 -74
  346. package/dist/tasks/parser.js +0 -380
  347. package/dist/tasks/schema.js +0 -123
  348. package/dist/tasks/validator.js +0 -80
  349. package/dist/workflows/ir/freeze.js +0 -320
  350. package/dist/workflows/runtime/document-cache.js +0 -13
@@ -16,17 +16,19 @@ import { openStateDatabase } from "../../core/state-db.js";
16
16
  import { parseSinceToIsoLenient } from "../../core/time.js";
17
17
  import { warn, warnVerbose } from "../../core/warn.js";
18
18
  import { resolveWriteTarget } from "../../core/write-source.js";
19
- import { getDefaultLlmConfig } from "../../integrations/agent/engine-resolution.js";
20
- import { materializeLlmRunnerConnection, resolveImproveProcessRunner } from "../../integrations/agent/runner.js";
19
+ import { deriveInstallations } from "../../indexer/installations.js";
20
+ import { resolveSourceEntries } from "../../indexer/search/search-source.js";
21
+ import { disposeLoweredExecutionDispatchLease, } from "../../integrations/agent/execution-lowering.js";
21
22
  import { cosineSimilarity, embedBatch, resolveEmbeddingModelId } from "../../llm/embedder.js";
22
- import { callStructured } from "../../llm/structured-call.js";
23
+ import { callStructured, preflightStructuredLlmRunner } from "../../llm/structured-call.js";
23
24
  import { getBodyEmbeddings, upsertBodyEmbeddings } from "../../storage/repositories/embeddings-repository.js";
24
- import { closeDatabase, openExistingDatabase } from "../../storage/repositories/index-connection.js";
25
+ import { closeDatabase, openExistingDatabase, openReadonlyExistingDatabase, } from "../../storage/repositories/index-connection.js";
25
26
  import { findEntryIdByRef, getAllEntries, getEntryById } from "../../storage/repositories/index-entries-repository.js";
26
27
  import { getNeighborsByEntryId } from "../../storage/repositories/index-vec-repository.js";
27
- import { isProposalSkipped, listProposals, proposalContent } from "../proposal/repository.js";
28
+ import { isProposalSkipped, listProposals, listProposalsReadOnly, proposalContent, } from "../proposal/repository.js";
28
29
  import { hasSupersededStatus, validateProposalFrontmatter } from "../proposal/validators/proposal-quality-validators.js";
29
30
  import { cacheHash } from "./content-hash.js";
31
+ import { resolveImproveLlmExecution } from "./execution.js";
30
32
  import { resolveImproveStrategy, resolveProcessEnabled } from "./improve-strategies.js";
31
33
  import { emitProposal } from "./proposal-envelope.js";
32
34
  import { createRunContext } from "./run-context.js";
@@ -271,7 +273,7 @@ async function clusterMemoriesBySimilarity(memories, config, stateDb, signal) {
271
273
  function loadPendingConsolidateProposalHashes(stashDir) {
272
274
  const hashes = new Set();
273
275
  try {
274
- const pending = listProposals(stashDir, { status: "pending" }).filter((p) => p.source === "consolidate");
276
+ const pending = listProposalsReadOnly(stashDir, { status: "pending" }).filter((proposal) => proposal.source === "consolidate");
275
277
  for (const p of pending) {
276
278
  try {
277
279
  hashes.add(cacheHash(proposalContent(p)));
@@ -350,30 +352,36 @@ function promoteProvenanceXrefs(existing, sourceRef) {
350
352
  }
351
353
  // ── LLM resolution ──────────────────────────────────────────────────────────
352
354
  /**
353
- * Resolve the LLM connection for the consolidate pass.
355
+ * Resolve the symbolic LLM runner for the consolidate pass.
354
356
  *
355
357
  * Priority order (mirrors extract / reflect / distill — see
356
358
  * `resolveExtractRunConfig` in `src/commands/improve/extract.ts` and the
357
- * canonical `resolveImproveProcessRunner` pattern):
359
+ * canonical improve execution-cascade pattern):
358
360
  *
359
361
  * 1. `improve.strategies.<name>.processes.consolidate.engine`
360
- * via {@link resolveImproveProcessRunner}. Lets the user pin
362
+ * via the common execution planner. Lets the user pin
361
363
  * a dedicated model (e.g. `ministral-3b`) for consolidation instead of
362
364
  * whatever `defaults.llmEngine` happens to be.
363
- * 2. `getDefaultLlmConfig(config)` — the baseline default LLM engine.
365
+ * 2. the baseline default LLM engine.
364
366
  *
365
- * Regression guard (2026-05-26): before this resolver, `akmConsolidate`
366
- * called `getDefaultLlmConfig` directly and silently ignored a configured
367
- * `processes.consolidate.profile`, sending every chunk to the default LLM
368
- * (often a long-context model loaded with a smaller runtime `n_ctx`, causing
369
- * silent 400s from LM Studio). The investigation lives at
370
- * `/tmp/akm-health-investigations/consolidation-no-op.md`.
367
+ * All consolidate execution crosses the same improve engine-resolution
368
+ * boundary as extract, reflect, and distill.
371
369
  */
372
- function resolveConsolidateLlmConfig(config, activeProfile) {
373
- const runnerSpec = resolveImproveProcessRunner(activeProfile, "consolidate", config);
374
- if (runnerSpec)
375
- return materializeLlmRunnerConnection(runnerSpec);
376
- return getDefaultLlmConfig(config);
370
+ function resolveConsolidateLlmRunner(config, activeProfile) {
371
+ return resolveImproveLlmExecution({
372
+ config,
373
+ profile: activeProfile,
374
+ process: getImproveProcessConfig("consolidate", activeProfile),
375
+ processName: "consolidate",
376
+ });
377
+ }
378
+ function consolidateRunnerFromOptions(opts, config) {
379
+ if (Object.hasOwn(opts, "llmRunner"))
380
+ return opts.llmRunner ?? undefined;
381
+ const resolved = resolveConsolidateLlmRunner(config, opts.improveProfile);
382
+ if (resolved)
383
+ opts.onNotices?.(resolved.notices);
384
+ return resolved?.runner;
377
385
  }
378
386
  /**
379
387
  * Build a {@link ConsolidateResult} from partial overrides, filling the envelope
@@ -434,8 +442,24 @@ export async function akmConsolidate(opts = {}) {
434
442
  const config = opts.config ?? loadConfig();
435
443
  const writeTarget = resolveConsolidationWriteTarget(opts, config);
436
444
  opts = { ...opts, target: writeTarget.source.name, writeTarget };
437
- opts = { ...opts, improveProfile: opts.improveProfile ?? resolveImproveStrategy(undefined, config).config };
445
+ const activeProfile = opts.improveProfile ?? resolveImproveStrategy(undefined, config).config;
446
+ opts = { ...opts, improveProfile: activeProfile };
438
447
  const stashDir = writeTarget.source.path;
448
+ const executionNotices = new Map();
449
+ const externalOnNotices = opts.onNotices;
450
+ const collectNotices = (notices) => {
451
+ for (const notice of notices)
452
+ executionNotices.set(JSON.stringify(notice), notice);
453
+ externalOnNotices?.(notices);
454
+ };
455
+ opts = { ...opts, onNotices: collectNotices };
456
+ const consolidateEnabled = resolveProcessEnabled("consolidate", activeProfile);
457
+ const frozenLlmRunner = consolidateEnabled ? consolidateRunnerFromOptions(opts, config) : undefined;
458
+ // Own the field even when no runner exists. Every downstream reader now
459
+ // observes this one symbolic snapshot instead of re-running config/model-map
460
+ // selection during the same invocation.
461
+ opts = { ...opts, llmRunner: frozenLlmRunner ?? null };
462
+ const withNotices = (result) => executionNotices.size > 0 ? { ...result, notices: Object.freeze([...executionNotices.values()]) } : result;
439
463
  // WI-9.10: construct this run's RunContext from values already resolved
440
464
  // above (sourceRun, config, stashDir) — no second config load, no new db
441
465
  // handle. consolidate.ts has no `eventsCtx`/proposals-`ctx` option at all
@@ -443,31 +467,21 @@ export async function akmConsolidate(opts = {}) {
443
467
  // called with the default, seam-less ProposalsContext — see
444
468
  // emitPromotionProposal below), so both get the safe empty-object default,
445
469
  // behaviorally identical to `undefined` (EventsContext/ProposalsContext
446
- // fields are all optional-chained by their consumers). `getLlmConfig`
447
- // mirrors `planConsolidation`'s own resolution (`resolveConsolidateLlmConfig`)
448
- // verbatim but lazily and independently — nothing calls `ctx.getLlmConfig`
449
- // yet this stage, so this never duplicates real work, only the (pure,
450
- // side-effect-free) resolution logic if invoked. consolidate has no `chat`
451
- // seam (it drives the LLM directly via the HTTP client path, never through
452
- // `chatCompletion`), so that field is left to its default.
470
+ // fields are all optional-chained by their consumers). LLM work uses the
471
+ // already-frozen symbolic runner through the shared dispatch seam.
453
472
  const runContext = createRunContext({
454
473
  stashDir,
455
474
  config,
456
475
  eventsCtx: {},
457
476
  proposalsCtx: {},
458
- getLlmConfig: () => {
459
- const resolved = Object.hasOwn(opts, "llmConfig")
460
- ? (opts.llmConfig ?? undefined)
461
- : resolveConsolidateLlmConfig(config, opts.improveProfile);
462
- return resolved ?? null;
463
- },
477
+ getLlmRunner: () => opts.llmRunner ?? null,
464
478
  sourceRun,
465
479
  dryRun: opts.dryRun ?? false,
466
480
  signal: opts.signal,
467
481
  });
468
482
  const warnings = [];
469
- if (!resolveProcessEnabled("consolidate", opts.improveProfile ?? resolveImproveStrategy(undefined, config).config)) {
470
- return makeConsolidateResult({
483
+ if (!consolidateEnabled) {
484
+ return withNotices(makeConsolidateResult({
471
485
  // Sourced from runContext (identical value to `opts.dryRun ?? false`)
472
486
  // so the constructed RunContext has a genuine downstream reference —
473
487
  // consolidate's own content-read sites are out of this stage's stated
@@ -476,21 +490,23 @@ export async function akmConsolidate(opts = {}) {
476
490
  target: opts.target ?? stashDir,
477
491
  durationMs: Date.now() - startMs,
478
492
  warnings,
479
- });
493
+ }));
480
494
  }
481
495
  // WS-3a: open one state.db handle shared by the body-embedding cache (dedup
482
496
  // + cluster) and the judged-state cache. All callers in the function body
483
497
  // receive this handle; it is closed in the `finally` block below.
484
498
  // Fail-open: any open error leaves it `undefined` and all cache paths skip.
485
499
  let sharedStateDb;
486
- try {
487
- sharedStateDb = openStateDatabase();
488
- }
489
- catch {
490
- // State DB unavailable → skip the embedding cache for this run.
500
+ if (config.embedding) {
501
+ try {
502
+ sharedStateDb = openStateDatabase();
503
+ }
504
+ catch {
505
+ // State DB unavailable → skip the embedding cache for this run.
506
+ }
491
507
  }
492
508
  try {
493
- return await akmConsolidateInner(opts, config, stashDir, startMs, warnings, sharedStateDb);
509
+ return withNotices(await akmConsolidateInner(opts, config, stashDir, startMs, warnings, sharedStateDb));
494
510
  }
495
511
  finally {
496
512
  sharedStateDb?.close();
@@ -530,6 +546,65 @@ function createConsolidateAccounting() {
530
546
  };
531
547
  return acc;
532
548
  }
549
+ function resolveConsolidationSourceOwner(opts, stashDir) {
550
+ const targetRoot = path.resolve(opts.writeTarget?.source.path ?? stashDir);
551
+ try {
552
+ const sources = resolveSourceEntries(stashDir, opts.config);
553
+ const installations = deriveInstallations(sources);
554
+ const targetIndex = sources.findIndex((source) => path.resolve(source.path) === targetRoot);
555
+ const target = installations[targetIndex];
556
+ if (!target)
557
+ return undefined;
558
+ return {
559
+ bundleId: target.id,
560
+ sourceRoot: targetRoot,
561
+ excludedSourceRoots: new Set(sources
562
+ .filter((_, index) => index !== targetIndex)
563
+ .map((source) => path.resolve(source.path))
564
+ .filter((sourceRoot) => sourceRoot.startsWith(`${targetRoot}${path.sep}`))),
565
+ };
566
+ }
567
+ catch {
568
+ return undefined;
569
+ }
570
+ }
571
+ /**
572
+ * Read and narrow the exact pool the live pass consumes, without embedding,
573
+ * LLM, proposal, event, or asset writes. Used by both preview and execution.
574
+ */
575
+ export function inspectConsolidationPool(opts, stashDir, warnings, access) {
576
+ const readOnly = access?.readOnly === true;
577
+ const sourceOwner = resolveConsolidationSourceOwner(opts, stashDir);
578
+ let memories = loadMemoriesForSource(sourceOwner, warnings, readOnly);
579
+ const staleCount = memories.filter((memory) => !fs.existsSync(memory.filePath)).length;
580
+ if (staleCount > 0) {
581
+ warnings.push(`Pre-flight: filtered ${staleCount} stale DB entr${staleCount === 1 ? "y" : "ies"} (file absent on disk) from memory pool before chunking.`);
582
+ }
583
+ memories = memories.filter((memory) => fs.existsSync(memory.filePath));
584
+ const poolSize = memories.length;
585
+ if (opts.incrementalSince && memories.length > 0) {
586
+ memories = narrowToIncrementalCandidates(memories, opts.incrementalSince, warnings, opts.neighborsPerChanged, readOnly);
587
+ }
588
+ const dedupPoolSize = memories.length;
589
+ if (opts.limit === undefined && memories.length > 150) {
590
+ warnings.push(`Consolidation: pool has ${memories.length} memories and no limit is set. Consider adding a limit to your consolidate config to prevent timeouts on slow LLM endpoints.`);
591
+ }
592
+ if (opts.limit !== undefined && memories.length > opts.limit) {
593
+ const mtimeOf = (memory) => {
594
+ try {
595
+ return fs.statSync(memory.filePath).mtimeMs;
596
+ }
597
+ catch {
598
+ return 0;
599
+ }
600
+ };
601
+ const mtimeCache = new Map(memories.map((memory) => [memory.filePath, mtimeOf(memory)]));
602
+ memories = [...memories].sort((a, b) => (mtimeCache.get(a.filePath) ?? 0) - (mtimeCache.get(b.filePath) ?? 0));
603
+ warnings.push(`Consolidation: pool capped at ${opts.limit} of ${memories.length} memories (limit option, oldest-modified first).`);
604
+ memories = memories.slice(0, opts.limit);
605
+ }
606
+ return { poolSize, candidatePoolSize: memories.length, dedupPoolSize, memories };
607
+ }
533
608
  /**
534
609
  * Pass 1 — narrow the memory pool before any LLM work: drop stale DB entries,
535
610
  * apply incremental-since narrowing, and cap to `opts.limit` (oldest-modified
@@ -538,17 +613,8 @@ function createConsolidateAccounting() {
538
613
  * consume. Behavior-identical to the former inlined narrowing block.
539
614
  */
540
615
  async function narrowConsolidationPool(opts, stashDir, startMs, warnings) {
541
- let memories = loadMemoriesForSource(opts.writeTarget?.source.path, stashDir, warnings);
542
- // Pre-flight: filter out stale DB entries whose files no longer exist on
543
- // disk. Without this, memories deleted by a prior run (but not yet
544
- // reindexed) appear in chunk prompts, causing the LLM to generate plans
545
- // against ghost refs and wasting tokens. Filtering here ensures the chunk
546
- // pool and memoryByRef are authoritative against the actual filesystem state.
547
- const staleCount = memories.filter((m) => !fs.existsSync(m.filePath)).length;
548
- if (staleCount > 0) {
549
- warnings.push(`Pre-flight: filtered ${staleCount} stale DB entr${staleCount === 1 ? "y" : "ies"} (file absent on disk) from memory pool before chunking.`);
550
- }
551
- memories = memories.filter((m) => fs.existsSync(m.filePath));
616
+ const snapshot = inspectConsolidationPool(opts, stashDir, warnings);
617
+ const memories = snapshot.memories;
552
618
  // (The former WS-3b Step 0a homeostatic demotion pass was removed — R4:
553
619
  // it was default-off and self-undoing (the next salience recompute
554
620
  // unconditionally overwrote the demoted values). Continuous decay now lives
@@ -564,50 +630,7 @@ async function narrowConsolidationPool(opts, stashDir, startMs, warnings) {
564
630
  }),
565
631
  };
566
632
  }
567
- if (opts.incrementalSince) {
568
- memories = narrowToIncrementalCandidates(memories, opts.incrementalSince, warnings, opts.neighborsPerChanged);
569
- if (memories.length === 0) {
570
- return {
571
- done: true,
572
- result: makeConsolidateResult({
573
- dryRun: opts.dryRun ?? false,
574
- target: opts.target ?? stashDir,
575
- warnings,
576
- durationMs: Date.now() - startMs,
577
- }),
578
- };
579
- }
580
- }
581
- // WS-5 perf telemetry: `dedupPoolSize` = memories entering the LLM pool
582
- // (after incremental narrowing, before the limit cap). `llmPoolSize` =
583
- // memories actually sent to the LLM. `embedMs/cacheHits/cacheMisses` =
584
- // accumulated from clusterMemoriesBySimilarity.
585
- const dedupPoolSize = memories.length;
586
- if (opts.limit === undefined && memories.length > 150) {
587
- warnings.push(`Consolidation: pool has ${memories.length} memories and no limit is set. Consider adding a limit to your consolidate config to prevent timeouts on slow LLM endpoints.`);
588
- }
589
- if (opts.limit !== undefined && memories.length > opts.limit) {
590
- // Order oldest-modified-first before capping so the limit selects the
591
- // stalest memories rather than a fixed head of the (rowid-ordered) DB
592
- // query. Consolidation rewrites surviving files, bumping their mtime, so
593
- // processed memories drift to the back of the queue and the cap rotates
594
- // across the whole corpus over successive runs instead of revisiting the
595
- // same slice every time. Fail-open to 0 (front of queue) when a file can
596
- // no longer be stat'd.
597
- const mtimeOf = (m) => {
598
- try {
599
- return fs.statSync(m.filePath).mtimeMs;
600
- }
601
- catch {
602
- return 0;
603
- }
604
- };
605
- const mtimeCache = new Map(memories.map((m) => [m.filePath, mtimeOf(m)]));
606
- memories = [...memories].sort((a, b) => (mtimeCache.get(a.filePath) ?? 0) - (mtimeCache.get(b.filePath) ?? 0));
607
- warnings.push(`Consolidation: pool capped at ${opts.limit} of ${memories.length} memories (limit option, oldest-modified first).`);
608
- memories = memories.slice(0, opts.limit);
609
- }
610
- return { done: false, memories, dedupPoolSize };
633
+ return { done: false, memories, dedupPoolSize: snapshot.dedupPoolSize };
611
634
  }
612
635
  /**
613
636
  * Pass 2 — turn the narrowed pool into an executable plan. Sizes chunks to the
@@ -656,7 +679,7 @@ function recordChunkJudgedNoAction(chunk, ops, accounting) {
656
679
  * are byte-identical, and every counter-increment point is unmoved.
657
680
  */
658
681
  async function judgeConsolidationChunks(args) {
659
- const { chunks, opts, config, llmConfig, sourceName, bodyTruncation, pendingProposalBodyHashes, standardsContext, warnings, accounting, } = args;
682
+ const { chunks, opts, config, llmRunner, lease, sourceName, bodyTruncation, pendingProposalBodyHashes, standardsContext, warnings, accounting, } = args;
660
683
  const chunkOpsArrays = [];
661
684
  // judgedNoAction tracks memories the LLM saw inside a chunk but proposed
662
685
  // no op for. Computed per chunk as `chunk.length − unique(targetRefs in ops)`.
@@ -733,13 +756,14 @@ async function judgeConsolidationChunks(args) {
733
756
  const callChunkLlm = async (fallbackError) => {
734
757
  // The gate runs with enabled:true (always open), so this guard is
735
758
  // exactly the envelope the gated fn used to return first thing.
736
- if (!llmConfig)
759
+ if (!llmRunner)
737
760
  return { ok: false, error: "No LLM configured for consolidation" };
738
761
  return callStructured({
739
762
  feature: "memory_consolidation",
740
763
  akmConfig: config,
741
764
  enabled: true,
742
- config: llmConfig,
765
+ runner: llmRunner,
766
+ ...(lease ? { lease } : {}),
743
767
  messages: [
744
768
  { role: "system", content: CONSOLIDATE_SYSTEM_PROMPT },
745
769
  { role: "user", content: userPrompt },
@@ -747,7 +771,7 @@ async function judgeConsolidationChunks(args) {
747
771
  request: {
748
772
  responseSchema: CONSOLIDATE_PLAN_JSON_SCHEMA,
749
773
  enableThinking: false,
750
- timeoutMs: llmConfig.timeoutMs,
774
+ timeoutMs: llmRunner.timeoutMs,
751
775
  signal: opts.signal,
752
776
  },
753
777
  parse: (raw) => ({ ok: true, content: raw ?? "" }),
@@ -756,6 +780,7 @@ async function judgeConsolidationChunks(args) {
756
780
  // reproduces that. The fallback fires only on wrapper timeout.
757
781
  onError: (_cls, e) => ({ ok: false, error: String(e) }),
758
782
  fallback: { ok: false, error: fallbackError },
783
+ ...(opts.onNotices ? { onNotices: opts.onNotices } : {}),
759
784
  });
760
785
  };
761
786
  let raw = await callChunkLlm(`chunk ${chunkIdx + 1} failed`);
@@ -824,11 +849,8 @@ async function planConsolidation(opts, config, stashDir, _startMs, memories, war
824
849
  // CLI. The agent CLI is for interactive agent sessions (reflect, propose);
825
850
  // structured JSON generation works better and faster via HTTP.
826
851
  //
827
- // Improve supplies a frozen connection; standalone consolidate resolves its
828
- // selected strategy/default engine here.
829
- const llmConfig = Object.hasOwn(opts, "llmConfig")
830
- ? (opts.llmConfig ?? undefined)
831
- : resolveConsolidateLlmConfig(config, opts.improveProfile);
852
+ // The outer invocation freezes standalone and improve-owned selection once.
853
+ const llmRunner = opts.llmRunner ?? undefined;
832
854
  // Chunk sizing: derive a safe chunk size from the configured model context
833
855
  // window so that the full prompt (system prompt + chunk user prompt) never
834
856
  // exceeds the model's n_ctx limit. When no context length is configured we
@@ -840,7 +862,7 @@ async function planConsolidation(opts, config, stashDir, _startMs, memories, war
840
862
  // keep it fixed and let computeSafeChunkSize vary the number of memories
841
863
  // per chunk instead.
842
864
  const bodyTruncation = 500;
843
- const modelContextLength = llmConfig?.contextLength ?? DEFAULT_CONTEXT_LENGTH_TOKENS;
865
+ const modelContextLength = llmRunner?.connection.contextLength ?? DEFAULT_CONTEXT_LENGTH_TOKENS;
844
866
  const chunkSize = computeSafeChunkSize(modelContextLength, bodyTruncation, opts.maxChunkSize);
845
867
  // -- Phase A: plan generation -----------------------------------------------
846
868
  const sourceName = opts.target ?? stashDir;
@@ -874,102 +896,117 @@ async function planConsolidation(opts, config, stashDir, _startMs, memories, war
874
896
  }
875
897
  // WS-5: capture llmPoolSize after every pre-LLM cap.
876
898
  const llmPoolSize = budgetedMemories.length;
877
- // C-1 / #380: Pre-cluster memories by embedding similarity before chunking.
878
- // This ensures that semantically similar memories land in the same LLM
879
- // context window, allowing the model to detect and merge duplicates that
880
- // would otherwise be split across chunks and survive indefinitely.
881
- // mem0 arXiv:2504.19413, A-MEM arXiv:2502.12110.
882
- // Fails open: if embeddings are unavailable or fail, original order is used.
883
- const { ordered: clusteredMemories, embedTelemetry } = await clusterMemoriesBySimilarity(budgetedMemories, config, sharedStateDb, opts.signal);
884
- // WS-3b Anti-collapse step 8c: inject random (non-similar) clusters.
885
- // A small fraction (default 5%) of the pool is shuffled into random positions
886
- // so the pipeline isn't PURELY similarity-driven. This prevents rich-get-richer
887
- // entrenchment where only the most-retrieved assets ever get consolidated.
888
- // DEFAULT ON since R5 opt out via antiCollapse.enabled: false.
889
- let finalClusteredMemories = clusteredMemories;
890
- {
891
- const antiCollapseForCluster = getImproveProcessConfig("consolidate", opts.improveProfile)?.antiCollapse ??
892
- {};
893
- if (antiCollapseForCluster.enabled !== false && clusteredMemories.length > 2) {
894
- const fraction = antiCollapseForCluster.randomClusterFraction ?? 0.05;
895
- const randomCount = Math.max(1, Math.floor(clusteredMemories.length * fraction));
896
- // Pick `randomCount` positions to inject random (un-clustered) members.
897
- // Use a seeded-ish shuffle: sort by hash of the name so it's deterministic
898
- // per run but not strictly similarity-driven.
899
- const shuffled = [...clusteredMemories].sort((a, b) => {
900
- // Deterministic shuffle: compare sha256-ish (use name hash as proxy).
901
- const ha = a.name.split("").reduce((acc, c) => ((acc << 5) - acc + c.charCodeAt(0)) | 0, 0);
902
- const hb = b.name.split("").reduce((acc, c) => ((acc << 5) - acc + c.charCodeAt(0)) | 0, 0);
903
- return ha - hb;
904
- });
905
- const randomSlice = shuffled.slice(0, randomCount);
906
- const randomSet = new Set(randomSlice.map((m) => m.name));
907
- // Insert random members at intervals through the clustered sequence.
908
- const withRandom = [];
909
- const interval = Math.max(2, Math.floor(clusteredMemories.length / randomCount));
910
- let randomIdx = 0;
911
- for (let i = 0; i < clusteredMemories.length; i++) {
912
- const m = clusteredMemories[i];
913
- if (m && !randomSet.has(m.name))
914
- withRandom.push(m);
915
- if (i > 0 && i % interval === 0 && randomIdx < randomSlice.length) {
899
+ const dispatchingChunks = [];
900
+ for (let i = 0; i < budgetedMemories.length; i += chunkSize) {
901
+ const chunk = budgetedMemories.slice(i, i + chunkSize);
902
+ if (chunk.length > 0 && !chunk.every((memory) => isHotCapturedMemory(memory.filePath))) {
903
+ dispatchingChunks.push(chunk);
904
+ }
905
+ }
906
+ const dispatchLease = llmRunner && dispatchingChunks.length > 0 ? await preflightStructuredLlmRunner(llmRunner) : undefined;
907
+ try {
908
+ // C-1 / #380: Pre-cluster memories by embedding similarity before chunking.
909
+ // This ensures that semantically similar memories land in the same LLM
910
+ // context window, allowing the model to detect and merge duplicates that
911
+ // would otherwise be split across chunks and survive indefinitely.
912
+ // mem0 arXiv:2504.19413, A-MEM arXiv:2502.12110.
913
+ // Fails open: if embeddings are unavailable or fail, original order is used.
914
+ const { ordered: clusteredMemories, embedTelemetry } = await clusterMemoriesBySimilarity(budgetedMemories, config, sharedStateDb, opts.signal);
915
+ // WS-3b Anti-collapse step 8c: inject random (non-similar) clusters.
916
+ // A small fraction (default 5%) of the pool is shuffled into random positions
917
+ // so the pipeline isn't PURELY similarity-driven. This prevents rich-get-richer
918
+ // entrenchment where only the most-retrieved assets ever get consolidated.
919
+ // DEFAULT ON since R5 opt out via antiCollapse.enabled: false.
920
+ let finalClusteredMemories = clusteredMemories;
921
+ {
922
+ const antiCollapseForCluster = getImproveProcessConfig("consolidate", opts.improveProfile)?.antiCollapse ??
923
+ {};
924
+ if (antiCollapseForCluster.enabled !== false && clusteredMemories.length > 2) {
925
+ const fraction = antiCollapseForCluster.randomClusterFraction ?? 0.05;
926
+ const randomCount = Math.max(1, Math.floor(clusteredMemories.length * fraction));
927
+ // Pick `randomCount` positions to inject random (un-clustered) members.
928
+ // Use a seeded-ish shuffle: sort by hash of the name so it's deterministic
929
+ // per run but not strictly similarity-driven.
930
+ const shuffled = [...clusteredMemories].sort((a, b) => {
931
+ // Deterministic shuffle: compare sha256-ish (use name hash as proxy).
932
+ const ha = a.name.split("").reduce((acc, c) => ((acc << 5) - acc + c.charCodeAt(0)) | 0, 0);
933
+ const hb = b.name.split("").reduce((acc, c) => ((acc << 5) - acc + c.charCodeAt(0)) | 0, 0);
934
+ return ha - hb;
935
+ });
936
+ const randomSlice = shuffled.slice(0, randomCount);
937
+ const randomSet = new Set(randomSlice.map((m) => m.name));
938
+ // Insert random members at intervals through the clustered sequence.
939
+ const withRandom = [];
940
+ const interval = Math.max(2, Math.floor(clusteredMemories.length / randomCount));
941
+ let randomIdx = 0;
942
+ for (let i = 0; i < clusteredMemories.length; i++) {
943
+ const m = clusteredMemories[i];
944
+ if (m && !randomSet.has(m.name))
945
+ withRandom.push(m);
946
+ if (i > 0 && i % interval === 0 && randomIdx < randomSlice.length) {
947
+ const r = randomSlice[randomIdx++];
948
+ if (r)
949
+ withRandom.push(r);
950
+ }
951
+ }
952
+ // Append any remaining random members not yet inserted.
953
+ while (randomIdx < randomSlice.length) {
916
954
  const r = randomSlice[randomIdx++];
917
955
  if (r)
918
956
  withRandom.push(r);
919
957
  }
958
+ finalClusteredMemories = withRandom;
959
+ warnings.push(`Anti-collapse: injected ${randomCount} random (non-similarity-driven) cluster member(s) into consolidation pool (fraction=${fraction}).`);
920
960
  }
921
- // Append any remaining random members not yet inserted.
922
- while (randomIdx < randomSlice.length) {
923
- const r = randomSlice[randomIdx++];
924
- if (r)
925
- withRandom.push(r);
926
- }
927
- finalClusteredMemories = withRandom;
928
- warnings.push(`Anti-collapse: injected ${randomCount} random (non-similarity-driven) cluster member(s) into consolidation pool (fraction=${fraction}).`);
929
961
  }
962
+ const chunks = [];
963
+ for (let i = 0; i < finalClusteredMemories.length; i += chunkSize) {
964
+ chunks.push(finalClusteredMemories.slice(i, i + chunkSize));
965
+ }
966
+ // 2026-05-27 prompt-context fix: precompute body-hashes of pending
967
+ // consolidate proposals once, so the per-chunk prompt can annotate
968
+ // memories whose body would just produce a deterministic
969
+ // `dedup_pending_proposal` skip. Cuts ~110 wasted LLM proposals per
970
+ // 4h on this user's stack. See
971
+ // /tmp/akm-health-investigations/tuning-reasons-investigation.md §Q3.
972
+ const pendingProposalBodyHashes = loadPendingConsolidateProposalHashes(stashDir);
973
+ warn(`[consolidate] ${budgetedMemories.length} memories / ${chunks.length} chunk(s) / chunk_size=${chunkSize}` +
974
+ ` / pending-proposal hashes: ${pendingProposalBodyHashes.size}`);
975
+ // Consolidate output merges memories (non-wiki) → stash authoring standards.
976
+ // Resolved ONCE per run and passed to each chunk prompt (facts not re-read
977
+ // per chunk).
978
+ const standardsContext = resolveStandardsContext("memories/_consolidated", stashDir);
979
+ const chunkOpsArrays = await judgeConsolidationChunks({
980
+ chunks,
981
+ opts,
982
+ config,
983
+ llmRunner,
984
+ lease: dispatchLease,
985
+ sourceName,
986
+ bodyTruncation,
987
+ pendingProposalBodyHashes,
988
+ standardsContext,
989
+ warnings,
990
+ accounting,
991
+ });
992
+ // Build the known-refs set from the already-filtered memory pool so
993
+ // mergePlans() can reject LLM-hallucinated primary refs before execution.
994
+ const knownRefs = new Set(budgetedMemories.map((m) => conceptIdFromTypeName("memory", m.name)));
995
+ const { ops: allOps, warnings: mergeWarnings } = mergePlans(chunkOpsArrays, knownRefs);
996
+ warnings.push(...mergeWarnings);
997
+ return {
998
+ allOps,
999
+ totalChunks: chunks.length,
1000
+ llmPoolSize,
1001
+ deferredMemories: memories.length - budgetedMemories.length,
1002
+ embedTelemetry,
1003
+ sourceName,
1004
+ };
930
1005
  }
931
- const chunks = [];
932
- for (let i = 0; i < finalClusteredMemories.length; i += chunkSize) {
933
- chunks.push(finalClusteredMemories.slice(i, i + chunkSize));
1006
+ finally {
1007
+ if (dispatchLease)
1008
+ disposeLoweredExecutionDispatchLease(dispatchLease);
934
1009
  }
935
- // 2026-05-27 prompt-context fix: precompute body-hashes of pending
936
- // consolidate proposals once, so the per-chunk prompt can annotate
937
- // memories whose body would just produce a deterministic
938
- // `dedup_pending_proposal` skip. Cuts ~110 wasted LLM proposals per
939
- // 4h on this user's stack. See
940
- // /tmp/akm-health-investigations/tuning-reasons-investigation.md §Q3.
941
- const pendingProposalBodyHashes = loadPendingConsolidateProposalHashes(stashDir);
942
- warn(`[consolidate] ${budgetedMemories.length} memories / ${chunks.length} chunk(s) / chunk_size=${chunkSize}` +
943
- ` / pending-proposal hashes: ${pendingProposalBodyHashes.size}`);
944
- // Consolidate output merges memories (non-wiki) → stash authoring standards.
945
- // Resolved ONCE per run and passed to each chunk prompt (facts not re-read
946
- // per chunk).
947
- const standardsContext = resolveStandardsContext("memories/_consolidated", stashDir);
948
- const chunkOpsArrays = await judgeConsolidationChunks({
949
- chunks,
950
- opts,
951
- config,
952
- llmConfig,
953
- sourceName,
954
- bodyTruncation,
955
- pendingProposalBodyHashes,
956
- standardsContext,
957
- warnings,
958
- accounting,
959
- });
960
- // Build the known-refs set from the already-filtered memory pool so
961
- // mergePlans() can reject LLM-hallucinated primary refs before execution.
962
- const knownRefs = new Set(budgetedMemories.map((m) => conceptIdFromTypeName("memory", m.name)));
963
- const { ops: allOps, warnings: mergeWarnings } = mergePlans(chunkOpsArrays, knownRefs);
964
- warnings.push(...mergeWarnings);
965
- return {
966
- allOps,
967
- totalChunks: chunks.length,
968
- llmPoolSize,
969
- deferredMemories: memories.length - budgetedMemories.length,
970
- embedTelemetry,
971
- sourceName,
972
- };
973
1010
  }
974
1011
  async function akmConsolidateInner(opts, config, stashDir, startMs, warnings, sharedStateDb) {
975
1012
  // -- Pass 1: narrow the memory pool (may early-return an envelope) ----------
@@ -1020,9 +1057,7 @@ async function akmConsolidateInner(opts, config, stashDir, startMs, warnings, sh
1020
1057
  promotionFailures,
1021
1058
  warnings,
1022
1059
  pushSkipReason: accounting.pushSkipReason,
1023
- llmConfig: Object.hasOwn(opts, "llmConfig")
1024
- ? (opts.llmConfig ?? null)
1025
- : (resolveConsolidateLlmConfig(config, opts.improveProfile) ?? null),
1060
+ llmRunner: opts.llmRunner ?? null,
1026
1061
  };
1027
1062
  for (const op of allOps) {
1028
1063
  if (op.op === "promote")
@@ -1228,7 +1263,7 @@ export async function emitPromotionProposal(op, ctx) {
1228
1263
  source: "consolidate",
1229
1264
  sourceRun,
1230
1265
  // §23.6 fingerprint model-id term (WI-6.4).
1231
- ...(ctx.llmConfig?.model ? { modelId: ctx.llmConfig.model } : {}),
1266
+ ...(ctx.llmRunner?.connection.model ? { modelId: ctx.llmRunner.connection.model } : {}),
1232
1267
  payload: {
1233
1268
  content: promotedAssetContent,
1234
1269
  frontmatter: { description, xrefs: [canonicalXref(op.ref)] },
@@ -1332,7 +1367,7 @@ async function checkPreEmitDedup(opts) {
1332
1367
  * everything changed or the index can't answer (fail-open to preserve merge
1333
1368
  * correctness). `since` is an ISO timestamp.
1334
1369
  */
1335
- export function narrowToIncrementalCandidates(memories, since, warnings, neighborsPerChanged = 5) {
1370
+ export function narrowToIncrementalCandidates(memories, since, warnings, neighborsPerChanged = 5, readOnly = false) {
1336
1371
  // Lenient by design: garbage `since` passes through unchanged and the ISO
1337
1372
  // string comparison below then selects nothing (see core/time.ts doc).
1338
1373
  const sinceIso = parseSinceToIsoLenient(since);
@@ -1353,7 +1388,9 @@ export function narrowToIncrementalCandidates(memories, since, warnings, neighbo
1353
1388
  const keep = new Set(changed.map((m) => m.name));
1354
1389
  let db;
1355
1390
  try {
1356
- db = openExistingDatabase();
1391
+ db = readOnly ? openReadonlyExistingDatabase(undefined, { isolatedSnapshot: true }) : openExistingDatabase();
1392
+ if (!db)
1393
+ return memories;
1357
1394
  for (const m of changed) {
1358
1395
  const id = findEntryIdByRef(db, conceptIdFromTypeName("memory", m.name));
1359
1396
  if (id === undefined)
@@ -1382,19 +1419,17 @@ export function narrowToIncrementalCandidates(memories, since, warnings, neighbo
1382
1419
  warnings.push(`Incremental consolidation: ${changed.length} changed + neighbours → ${candidates.length}/${memories.length} memories considered (since ${since}${sinceIso !== since ? ` = ${sinceIso}` : ""}).`);
1383
1420
  return candidates;
1384
1421
  }
1385
- function loadMemoriesForSource(source, stashDir, warnings) {
1422
+ function loadMemoriesForSource(source, warnings, readOnly) {
1386
1423
  // Load from DB first
1387
1424
  let memories = [];
1388
1425
  let db;
1389
1426
  try {
1390
- db = openExistingDatabase();
1427
+ db = readOnly ? openReadonlyExistingDatabase(undefined, { isolatedSnapshot: true }) : openExistingDatabase();
1428
+ if (!db)
1429
+ throw new Error("index unavailable");
1391
1430
  const entries = getAllEntries(db, "memory");
1392
1431
  memories = entries
1393
- .filter((e) => {
1394
- if (!source)
1395
- return true;
1396
- return path.resolve(e.stashDir) === path.resolve(source);
1397
- })
1432
+ .filter((entry) => source !== undefined && entry.bundleId === source.bundleId)
1398
1433
  .filter((e) => isConsolidationEligibleMemoryName(e.entry.name))
1399
1434
  // Skip stale DB entries whose file was deleted by a prior run but not yet
1400
1435
  // re-indexed. Without this guard the deleted file's ref appears in chunks
@@ -1407,7 +1442,7 @@ function loadMemoriesForSource(source, stashDir, warnings) {
1407
1442
  filePath: e.filePath,
1408
1443
  description: e.entry.description ?? "",
1409
1444
  tags: e.entry.tags ?? [],
1410
- stashDir: e.stashDir,
1445
+ stashDir: source?.sourceRoot ?? "",
1411
1446
  }));
1412
1447
  }
1413
1448
  catch {
@@ -1417,10 +1452,10 @@ function loadMemoriesForSource(source, stashDir, warnings) {
1417
1452
  if (db)
1418
1453
  closeDatabase(db);
1419
1454
  }
1420
- if (memories.length === 0) {
1455
+ if (memories.length === 0 && source) {
1421
1456
  // DB fallback: walk filesystem
1422
- const memoriesDir = path.join(source ?? stashDir, "memories");
1423
- const fsStashDir = source ?? stashDir;
1457
+ const memoriesDir = path.join(source.sourceRoot, "memories");
1458
+ const fsStashDir = source.sourceRoot;
1424
1459
  if (fs.existsSync(memoriesDir)) {
1425
1460
  const pending = [memoriesDir];
1426
1461
  while (pending.length > 0) {
@@ -1428,6 +1463,8 @@ function loadMemoriesForSource(source, stashDir, warnings) {
1428
1463
  for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
1429
1464
  const filePath = path.join(current, entry.name);
1430
1465
  if (entry.isDirectory()) {
1466
+ if (source.excludedSourceRoots.has(path.resolve(filePath)))
1467
+ continue;
1431
1468
  pending.push(filePath);
1432
1469
  continue;
1433
1470
  }