akm-cli 0.8.3 → 0.9.0-beta.1

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 (316) hide show
  1. package/CHANGELOG.md +209 -0
  2. package/dist/assets/help/help-proposals.md +1 -2
  3. package/dist/assets/hints/cli-hints-full.md +34 -19
  4. package/dist/assets/hints/cli-hints-short.md +1 -1
  5. package/dist/assets/profiles/catchup.json +13 -0
  6. package/dist/assets/profiles/consolidate.json +13 -0
  7. package/dist/assets/profiles/frequent.json +13 -0
  8. package/dist/assets/tasks/core/backup.yml +4 -0
  9. package/dist/assets/tasks/core/extract.yml +4 -0
  10. package/dist/assets/tasks/core/improve.yml +4 -0
  11. package/dist/assets/tasks/core/index-refresh.yml +4 -0
  12. package/dist/assets/tasks/core/sync.yml +4 -0
  13. package/dist/assets/tasks/core/update-stashes.yml +4 -0
  14. package/dist/assets/tasks/core/version-check.yml +4 -0
  15. package/dist/cli/config-migrate.js +6 -6
  16. package/dist/cli/config-validate.js +4 -4
  17. package/dist/cli/confirm.js +3 -3
  18. package/dist/cli/parse-args.js +1 -1
  19. package/dist/cli/shared.js +51 -14
  20. package/dist/cli-node.mjs +26 -0
  21. package/dist/cli.js +171 -3862
  22. package/dist/commands/{agent-dispatch.js → agent/agent-dispatch.js} +6 -6
  23. package/dist/commands/{agent-support.js → agent/agent-support.js} +2 -2
  24. package/dist/commands/agent/contribute-cli.js +200 -0
  25. package/dist/commands/completions.js +1 -1
  26. package/dist/commands/config-cli.js +240 -3
  27. package/dist/commands/config-edit.js +344 -0
  28. package/dist/commands/db-cli.js +2 -2
  29. package/dist/commands/env/env-cli.js +529 -0
  30. package/dist/commands/env/env.js +410 -0
  31. package/dist/commands/env/secret-cli.js +259 -0
  32. package/dist/commands/{secret.js → env/secret.js} +6 -47
  33. package/dist/commands/events.js +4 -4
  34. package/dist/commands/feedback-cli.js +18 -34
  35. package/dist/commands/graph/graph-cli.js +132 -0
  36. package/dist/commands/{graph.js → graph/graph.js} +22 -16
  37. package/dist/commands/health/checks.js +279 -0
  38. package/dist/commands/health.js +94 -262
  39. package/dist/commands/{consolidate.js → improve/consolidate.js} +48 -36
  40. package/dist/commands/{distill-promotion-policy.js → improve/distill-promotion-policy.js} +3 -3
  41. package/dist/commands/{distill.js → improve/distill.js} +39 -18
  42. package/dist/commands/{eval-cases.js → improve/eval-cases.js} +1 -1
  43. package/dist/commands/{extract-cli.js → improve/extract-cli.js} +4 -4
  44. package/dist/commands/{extract-prompt.js → improve/extract-prompt.js} +2 -2
  45. package/dist/commands/{extract.js → improve/extract.js} +185 -26
  46. package/dist/commands/{improve-auto-accept.js → improve/improve-auto-accept.js} +4 -4
  47. package/dist/commands/{improve-cli.js → improve/improve-cli.js} +44 -22
  48. package/dist/commands/{improve-profiles.js → improve/improve-profiles.js} +13 -7
  49. package/dist/commands/{improve-result-file.js → improve/improve-result-file.js} +1 -1
  50. package/dist/commands/{improve.js → improve/improve.js} +517 -253
  51. package/dist/{core → commands/improve/memory}/memory-belief.js +2 -2
  52. package/dist/{core → commands/improve/memory}/memory-contradiction-detect.js +5 -5
  53. package/dist/{core → commands/improve/memory}/memory-improve.js +4 -4
  54. package/dist/commands/{reflect.js → improve/reflect.js} +33 -28
  55. package/dist/commands/improve/session-asset.js +248 -0
  56. package/dist/commands/lint/agent-linter.js +1 -1
  57. package/dist/commands/lint/base-linter.js +55 -37
  58. package/dist/commands/lint/command-linter.js +1 -1
  59. package/dist/commands/lint/default-linter.js +1 -1
  60. package/dist/commands/lint/env-key-rules.js +1 -1
  61. package/dist/commands/lint/index.js +19 -25
  62. package/dist/commands/lint/knowledge-linter.js +1 -1
  63. package/dist/commands/lint/memory-linter.js +1 -1
  64. package/dist/commands/lint/registry.js +8 -8
  65. package/dist/commands/lint/skill-linter.js +1 -1
  66. package/dist/commands/lint/task-linter.js +1 -1
  67. package/dist/commands/lint/workflow-linter.js +1 -1
  68. package/dist/commands/lint.js +1 -1
  69. package/dist/commands/observability-cli.js +244 -0
  70. package/dist/commands/proposal/drain-policies.js +3 -3
  71. package/dist/commands/proposal/drain.js +15 -10
  72. package/dist/commands/proposal/proposal-cli.js +478 -0
  73. package/dist/commands/{proposal.js → proposal/proposal.js} +5 -5
  74. package/dist/commands/{propose.js → proposal/propose.js} +11 -11
  75. package/dist/{core → commands/proposal/validators}/proposal-quality-validators.js +8 -3
  76. package/dist/{core → commands/proposal/validators}/proposal-validators.js +5 -5
  77. package/dist/{core → commands/proposal/validators}/proposals.js +13 -7
  78. package/dist/commands/{curate.js → read/curate.js} +7 -7
  79. package/dist/commands/{knowledge.js → read/knowledge.js} +22 -9
  80. package/dist/commands/{registry-search.js → read/registry-search.js} +5 -5
  81. package/dist/commands/{remember-cli.js → read/remember-cli.js} +15 -7
  82. package/dist/commands/read/search-cli.js +207 -0
  83. package/dist/commands/{search.js → read/search.js} +22 -27
  84. package/dist/commands/{show.js → read/show.js} +31 -45
  85. package/dist/commands/registry-cli.js +8 -8
  86. package/dist/commands/remember.js +8 -8
  87. package/dist/commands/sources/add-cli.js +293 -0
  88. package/dist/commands/{history.js → sources/history.js} +27 -25
  89. package/dist/commands/{info.js → sources/info.js} +6 -6
  90. package/dist/commands/{init.js → sources/init.js} +6 -6
  91. package/dist/commands/{installed-stashes.js → sources/installed-stashes.js} +12 -12
  92. package/dist/commands/{migration-help.js → sources/migration-help.js} +3 -2
  93. package/dist/commands/{schema-repair.js → sources/schema-repair.js} +8 -8
  94. package/dist/commands/{self-update.js → sources/self-update.js} +10 -9
  95. package/dist/commands/{source-add.js → sources/source-add.js} +10 -10
  96. package/dist/commands/{source-clone.js → sources/source-clone.js} +7 -7
  97. package/dist/commands/{source-manage.js → sources/source-manage.js} +4 -4
  98. package/dist/commands/sources/sources-cli.js +305 -0
  99. package/dist/commands/sources/stash-cli.js +219 -0
  100. package/dist/commands/{stash-skeleton.js → sources/stash-skeleton.js} +2 -1
  101. package/dist/commands/tasks/default-tasks.js +173 -0
  102. package/dist/commands/tasks/tasks-cli.js +210 -0
  103. package/dist/commands/{tasks.js → tasks/tasks.js} +14 -14
  104. package/dist/commands/wiki-cli.js +307 -0
  105. package/dist/commands/workflow-cli.js +329 -0
  106. package/dist/core/action-contributors.js +1 -1
  107. package/dist/core/assert.js +40 -0
  108. package/dist/core/asset/asset-create.js +54 -0
  109. package/dist/core/{asset-ref.js → asset/asset-ref.js} +21 -4
  110. package/dist/core/{asset-registry.js → asset/asset-registry.js} +3 -3
  111. package/dist/core/{asset-spec.js → asset/asset-spec.js} +17 -31
  112. package/dist/core/{markdown.js → asset/markdown.js} +1 -1
  113. package/dist/core/{stash-meta.js → asset/stash-meta.js} +1 -1
  114. package/dist/core/best-effort.js +64 -0
  115. package/dist/core/common.js +32 -18
  116. package/dist/core/{config-io.js → config/config-io.js} +29 -19
  117. package/dist/core/{config-migration.js → config/config-migration.js} +11 -9
  118. package/dist/core/{config-schema.js → config/config-schema.js} +45 -1
  119. package/dist/core/config/config-types.js +16 -0
  120. package/dist/core/{config-walker.js → config/config-walker.js} +2 -2
  121. package/dist/core/{config.js → config/config.js} +10 -8
  122. package/dist/core/env-secret-ref.js +90 -0
  123. package/dist/core/errors.js +13 -3
  124. package/dist/core/events.js +27 -4
  125. package/dist/core/file-lock.js +1 -1
  126. package/dist/core/improve-types.js +48 -0
  127. package/dist/core/lesson-lint.js +2 -2
  128. package/dist/core/paths.js +2 -2
  129. package/dist/core/ripgrep/install.js +2 -2
  130. package/dist/core/ripgrep/resolve.js +2 -2
  131. package/dist/core/state-db.js +88 -46
  132. package/dist/core/text-truncation.js +148 -0
  133. package/dist/core/time.js +1 -1
  134. package/dist/core/write-source.js +98 -85
  135. package/dist/indexer/{db-backup.js → db/db-backup.js} +9 -24
  136. package/dist/indexer/{db.js → db/db.js} +126 -116
  137. package/dist/indexer/{graph-db.js → db/graph-db.js} +9 -4
  138. package/dist/indexer/{llm-cache.js → db/llm-cache.js} +15 -12
  139. package/dist/indexer/ensure-index.js +4 -4
  140. package/dist/indexer/{graph-boost.js → graph/graph-boost.js} +1 -1
  141. package/dist/indexer/{graph-extraction.js → graph/graph-extraction.js} +55 -13
  142. package/dist/indexer/indexer.js +37 -30
  143. package/dist/indexer/init.js +54 -0
  144. package/dist/indexer/manifest.js +10 -10
  145. package/dist/indexer/{memory-inference.js → passes/memory-inference.js} +92 -23
  146. package/dist/indexer/{metadata-contributors.js → passes/metadata-contributors.js} +10 -8
  147. package/dist/indexer/{metadata.js → passes/metadata.js} +15 -19
  148. package/dist/indexer/{staleness-detect.js → passes/staleness-detect.js} +53 -12
  149. package/dist/indexer/{db-search.js → search/db-search.js} +28 -16
  150. package/dist/indexer/{ranking-contributors.js → search/ranking-contributors.js} +1 -1
  151. package/dist/indexer/{ranking.js → search/ranking.js} +2 -2
  152. package/dist/indexer/{search-hit-enrichers.js → search/search-hit-enrichers.js} +3 -3
  153. package/dist/indexer/{search-source.js → search/search-source.js} +8 -8
  154. package/dist/indexer/{semantic-status.js → search/semantic-status.js} +3 -3
  155. package/dist/indexer/usage/unmigrated-vaults-guard.js +94 -0
  156. package/dist/indexer/{usage-events.js → usage/usage-events.js} +32 -0
  157. package/dist/indexer/{file-context.js → walk/file-context.js} +10 -15
  158. package/dist/indexer/{matchers.js → walk/matchers.js} +13 -9
  159. package/dist/indexer/{path-resolver.js → walk/path-resolver.js} +6 -6
  160. package/dist/indexer/{project-context.js → walk/project-context.js} +1 -1
  161. package/dist/indexer/{walker.js → walk/walker.js} +4 -3
  162. package/dist/integrations/agent/builder-shared.js +39 -0
  163. package/dist/integrations/agent/builders.js +14 -81
  164. package/dist/integrations/agent/config.js +6 -4
  165. package/dist/integrations/agent/detect.js +1 -1
  166. package/dist/integrations/agent/index.js +23 -8
  167. package/dist/integrations/agent/prompts.js +2 -3
  168. package/dist/integrations/agent/runner.js +22 -3
  169. package/dist/integrations/agent/spawn.js +9 -10
  170. package/dist/integrations/harnesses/claude/agent-builder.js +48 -0
  171. package/dist/integrations/harnesses/claude/config-import.js +70 -0
  172. package/dist/integrations/harnesses/claude/index.js +64 -0
  173. package/dist/integrations/{session-logs/providers/claude-code.js → harnesses/claude/session-log.js} +16 -1
  174. package/dist/integrations/harnesses/index.js +144 -0
  175. package/dist/integrations/harnesses/opencode/agent-builder.js +43 -0
  176. package/dist/integrations/harnesses/opencode/config-import.js +82 -0
  177. package/dist/integrations/harnesses/opencode/index.js +59 -0
  178. package/dist/integrations/{session-logs/providers/opencode.js → harnesses/opencode/session-log.js} +1 -1
  179. package/dist/integrations/harnesses/opencode-sdk/index.js +49 -0
  180. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +234 -0
  181. package/dist/integrations/harnesses/types.js +43 -0
  182. package/dist/integrations/lockfile.js +7 -16
  183. package/dist/integrations/session-logs/index.js +82 -9
  184. package/dist/llm/call-ai.js +4 -4
  185. package/dist/llm/client.js +131 -6
  186. package/dist/llm/embedder.js +6 -6
  187. package/dist/llm/embedders/local.js +9 -22
  188. package/dist/llm/embedders/remote.js +2 -2
  189. package/dist/llm/embedders/types.js +1 -1
  190. package/dist/llm/graph-extract.js +31 -12
  191. package/dist/llm/index-passes.js +1 -1
  192. package/dist/llm/memory-infer.js +12 -5
  193. package/dist/llm/metadata-enhance.js +2 -2
  194. package/dist/output/context.js +6 -44
  195. package/dist/output/renderers.js +88 -58
  196. package/dist/output/shapes/curate.js +7 -3
  197. package/dist/output/shapes/distill.js +7 -3
  198. package/dist/output/shapes/env-list.js +18 -16
  199. package/dist/output/shapes/events.js +5 -4
  200. package/dist/output/shapes/helpers.js +2 -4
  201. package/dist/output/shapes/history.js +7 -3
  202. package/dist/output/shapes/passthrough.js +8 -11
  203. package/dist/output/shapes/{proposal-accept.js → proposal/accept.js} +7 -3
  204. package/dist/output/shapes/{proposal-diff.js → proposal/diff.js} +7 -3
  205. package/dist/output/shapes/{proposal-list.js → proposal/list.js} +7 -3
  206. package/dist/output/shapes/{proposal-producer.js → proposal/producer.js} +5 -4
  207. package/dist/output/shapes/{proposal-reject.js → proposal/reject.js} +7 -3
  208. package/dist/output/shapes/{proposal-show.js → proposal/show.js} +7 -3
  209. package/dist/output/shapes/registry-search.js +7 -3
  210. package/dist/output/shapes/registry.js +12 -0
  211. package/dist/output/shapes/search.js +7 -3
  212. package/dist/output/shapes/secret-list.js +18 -16
  213. package/dist/output/shapes/show.js +7 -3
  214. package/dist/output/shapes.js +55 -30
  215. package/dist/output/text/add.js +2 -3
  216. package/dist/output/text/clone.js +2 -3
  217. package/dist/output/text/config.js +2 -3
  218. package/dist/output/text/curate.js +4 -3
  219. package/dist/output/text/distill.js +2 -3
  220. package/dist/output/text/enable-disable.js +5 -4
  221. package/dist/output/text/env.js +13 -0
  222. package/dist/output/text/events.js +5 -4
  223. package/dist/output/text/feedback.js +4 -3
  224. package/dist/output/text/helpers.js +54 -39
  225. package/dist/output/text/history.js +2 -3
  226. package/dist/output/text/import.js +2 -3
  227. package/dist/output/text/index.js +2 -3
  228. package/dist/output/text/info.js +2 -3
  229. package/dist/output/text/init.js +2 -3
  230. package/dist/output/text/list.js +2 -3
  231. package/dist/output/text/proposal/producer.js +9 -0
  232. package/dist/output/text/proposal/proposal.js +13 -0
  233. package/dist/output/text/registry-commands.js +8 -7
  234. package/dist/output/text/registry.js +12 -0
  235. package/dist/output/text/remember.js +4 -3
  236. package/dist/output/text/remove.js +2 -3
  237. package/dist/output/text/save.js +2 -3
  238. package/dist/output/text/search.js +4 -3
  239. package/dist/output/text/show.js +4 -3
  240. package/dist/output/text/update.js +2 -3
  241. package/dist/output/text/upgrade.js +2 -3
  242. package/dist/output/text/wiki.js +12 -11
  243. package/dist/output/text/workflow.js +12 -10
  244. package/dist/output/text.js +66 -32
  245. package/dist/registry/build-index.js +11 -10
  246. package/dist/registry/factory.js +1 -1
  247. package/dist/registry/origin-resolve.js +1 -1
  248. package/dist/registry/providers/index.js +2 -2
  249. package/dist/registry/providers/skills-sh.js +91 -72
  250. package/dist/registry/providers/static-index.js +75 -52
  251. package/dist/registry/resolve.js +3 -3
  252. package/dist/runtime.js +242 -0
  253. package/dist/scripts/migrate-storage.js +1594 -673
  254. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +240 -166
  255. package/dist/setup/detect.js +311 -9
  256. package/dist/setup/harness-config-import.js +6 -120
  257. package/dist/setup/setup.js +454 -43
  258. package/dist/sources/include.js +1 -1
  259. package/dist/sources/provider-factory.js +2 -2
  260. package/dist/sources/providers/filesystem.js +3 -3
  261. package/dist/sources/providers/git.js +9 -9
  262. package/dist/sources/providers/index.js +4 -4
  263. package/dist/sources/providers/npm.js +6 -6
  264. package/dist/sources/providers/provider-utils.js +13 -20
  265. package/dist/sources/providers/sync-from-ref.js +5 -5
  266. package/dist/sources/providers/tar-utils.js +2 -2
  267. package/dist/sources/providers/website.js +2 -2
  268. package/dist/sources/resolve.js +5 -5
  269. package/dist/sources/website-ingest.js +5 -5
  270. package/dist/storage/database.js +102 -0
  271. package/dist/storage/engines/sqlite-migrations.js +42 -0
  272. package/dist/storage/locations.js +25 -0
  273. package/dist/storage/repositories/index-db.js +43 -0
  274. package/dist/storage/repositories/workflow-runs-repository.js +141 -0
  275. package/dist/tasks/backends/cron.js +4 -4
  276. package/dist/tasks/backends/exec-utils.js +32 -0
  277. package/dist/tasks/backends/index.js +3 -3
  278. package/dist/tasks/backends/launchd.js +7 -14
  279. package/dist/tasks/backends/schtasks.js +7 -16
  280. package/dist/tasks/embedded.js +71 -0
  281. package/dist/tasks/parser.js +2 -2
  282. package/dist/tasks/resolveAkmBin.js +1 -1
  283. package/dist/tasks/runner.js +28 -15
  284. package/dist/tasks/schedule.js +1 -1
  285. package/dist/tasks/validator.js +7 -7
  286. package/dist/text-import-hook.mjs +51 -0
  287. package/dist/version.js +2 -1
  288. package/dist/wiki/wiki.js +7 -7
  289. package/dist/workflows/{authoring.js → authoring/authoring.js} +6 -6
  290. package/dist/workflows/{scope-key.js → authoring/scope-key.js} +1 -1
  291. package/dist/workflows/cli.js +1 -1
  292. package/dist/workflows/db.js +50 -32
  293. package/dist/workflows/parser.js +4 -4
  294. package/dist/workflows/renderer.js +5 -5
  295. package/dist/workflows/runtime/agent-identity.js +56 -0
  296. package/dist/workflows/runtime/checkin.js +57 -0
  297. package/dist/workflows/{runs.js → runtime/runs.js} +197 -101
  298. package/dist/workflows/validate-summary.js +82 -0
  299. package/docs/README.md +1 -1
  300. package/docs/data-and-telemetry.md +6 -6
  301. package/package.json +16 -8
  302. package/dist/commands/add-cli.js +0 -279
  303. package/dist/commands/env.js +0 -213
  304. package/dist/integrations/agent/sdk-runner.js +0 -126
  305. package/dist/output/shapes/vault-list.js +0 -19
  306. package/dist/output/text/proposal-producer.js +0 -8
  307. package/dist/output/text/proposal.js +0 -12
  308. package/dist/output/text/vault.js +0 -16
  309. /package/dist/core/{asset-serialize.js → asset/asset-serialize.js} +0 -0
  310. /package/dist/core/{frontmatter.js → asset/frontmatter.js} +0 -0
  311. /package/dist/core/{config-sources.js → config/config-sources.js} +0 -0
  312. /package/dist/indexer/{graph-dedup.js → graph/graph-dedup.js} +0 -0
  313. /package/dist/{core/config-types.js → indexer/passes/pass-context.js} +0 -0
  314. /package/dist/indexer/{search-fields.js → search/search-fields.js} +0 -0
  315. /package/dist/indexer/{index-context.js → walk/index-context.js} +0 -0
  316. /package/dist/workflows/{document-cache.js → runtime/document-cache.js} +0 -0
@@ -3,44 +3,47 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
- import { makeAssetRef, parseAssetRef } from "../core/asset-ref";
7
- import { daysToMs, isAssetType } from "../core/common";
8
- import { getDefaultLlmConfig, loadConfig } from "../core/config";
9
- import { ConfigError, NotFoundError, rethrowIfTestIsolationError, UsageError } from "../core/errors";
10
- import { appendEvent, readEvents } from "../core/events";
11
- import { probeLock, releaseLock, releaseLockIfOwned, tryAcquireLockSync } from "../core/file-lock";
12
- import { parseFrontmatter } from "../core/frontmatter";
13
- import { detectAndWriteContradictions } from "../core/memory-contradiction-detect";
14
- import { analyzeMemoryCleanup, applyMemoryCleanup, } from "../core/memory-improve";
15
- import { getDbPath } from "../core/paths";
16
- import { createProposal, expireStaleProposals, getProposal, isProposalSkipped, listProposals, purgeOrphanProposals, } from "../core/proposals";
17
- import { openStateDatabase, purgeOldEvents, purgeOldImproveRuns } from "../core/state-db";
18
- import { info, warn } from "../core/warn";
19
- import { closeDatabase, getAllEntries, getEntryCount, getRetrievalCounts, getUtilityScoresByIds, getZeroResultSearches, openDatabase, openExistingDatabase, } from "../indexer/db";
20
- import { ensureIndex } from "../indexer/ensure-index";
21
- import { runGraphExtractionPass } from "../indexer/graph-extraction";
22
- import { akmIndex } from "../indexer/indexer";
23
- import { runMemoryInferencePass, } from "../indexer/memory-inference";
24
- import { resolveAssetPath } from "../indexer/path-resolver";
25
- import { getWritableStashDirs, resolveSourceEntries } from "../indexer/search-source";
26
- import { runStalenessDetectionPass } from "../indexer/staleness-detect";
27
- import { resolveImproveProcessRunnerFromProfile, resolveTriageJudgmentRunner } from "../integrations/agent/runner";
28
- import { getAvailableHarnesses } from "../integrations/session-logs";
29
- import { isLlmFeatureEnabled, isProcessEnabled } from "../llm/feature-gate";
30
- import { isGitBackedStash, resolveWritableOverride, saveGitStash } from "../sources/providers/git";
31
- import { akmConsolidate } from "./consolidate";
32
- import { akmDistill, deriveLessonRef, isDistillRefusedInputType } from "./distill";
33
- import { deriveKnowledgeRef } from "./distill-promotion-policy";
34
- import { countEvalCases, writeEvalCase } from "./eval-cases";
35
- import { akmExtract } from "./extract";
36
- import { makeGateConfig, resolveExtractConfidence, runAutoAcceptGate } from "./improve-auto-accept";
37
- import { isProfileFilteredForAllPasses, resolveImproveProfile, resolveProcessEnabled, shouldSkipRef, } from "./improve-profiles";
38
- import { akmLint } from "./lint/index";
39
- import { drainProposals } from "./proposal/drain";
40
- import { resolveDrainPolicy } from "./proposal/drain-policies";
41
- import { akmReflect } from "./reflect";
42
- import { runSchemaRepairPass } from "./schema-repair";
43
- import { checkDeadUrls } from "./url-checker";
6
+ import { assertNever } from "../../core/assert.js";
7
+ import { makeAssetRef, parseAssetRef } from "../../core/asset/asset-ref.js";
8
+ import { parseFrontmatter } from "../../core/asset/frontmatter.js";
9
+ import { daysToMs, isAssetType } from "../../core/common.js";
10
+ import { getDefaultLlmConfig, loadConfig } from "../../core/config/config.js";
11
+ import { ConfigError, NotFoundError, rethrowIfTestIsolationError, UsageError } from "../../core/errors.js";
12
+ import { appendEvent, readEvents } from "../../core/events.js";
13
+ import { probeLock, releaseLock, releaseLockIfOwned, tryAcquireLockSync } from "../../core/file-lock.js";
14
+ import { classifyImproveAction } from "../../core/improve-types.js";
15
+ import { getDbPath, getStateDbPathInDataDir } from "../../core/paths.js";
16
+ import { openStateDatabase, purgeOldEvents, purgeOldImproveRuns } from "../../core/state-db.js";
17
+ import { info, warn } from "../../core/warn.js";
18
+ import { closeDatabase, getAllEntries, getEntryCount, getRetrievalCounts, getUtilityScoresByIds, getZeroResultSearches, openDatabase, openExistingDatabase, } from "../../indexer/db/db.js";
19
+ import { ensureIndex } from "../../indexer/ensure-index.js";
20
+ import { runGraphExtractionPass } from "../../indexer/graph/graph-extraction.js";
21
+ import { akmIndex } from "../../indexer/indexer.js";
22
+ import { runMemoryInferencePass } from "../../indexer/passes/memory-inference.js";
23
+ import { runStalenessDetectionPass } from "../../indexer/passes/staleness-detect.js";
24
+ import { getWritableStashDirs, resolveSourceEntries } from "../../indexer/search/search-source.js";
25
+ import { countUsageEventsByType } from "../../indexer/usage/usage-events.js";
26
+ import { resolveAssetPath } from "../../indexer/walk/path-resolver.js";
27
+ import { resolveImproveProcessRunnerFromProfile, resolveTriageJudgmentRunner } from "../../integrations/agent/runner.js";
28
+ import { getAvailableHarnesses } from "../../integrations/session-logs/index.js";
29
+ import { isLlmFeatureEnabled, isProcessEnabled } from "../../llm/feature-gate.js";
30
+ import { isGitBackedStash, resolveWritableOverride, saveGitStash } from "../../sources/providers/git.js";
31
+ import { akmLint } from "../lint/index.js";
32
+ import { drainProposals } from "../proposal/drain.js";
33
+ import { resolveDrainPolicy } from "../proposal/drain-policies.js";
34
+ import { createProposal, expireStaleProposals, getProposal, isProposalSkipped, listProposals, purgeOrphanProposals, } from "../proposal/validators/proposals.js";
35
+ import { runSchemaRepairPass } from "../sources/schema-repair.js";
36
+ import { checkDeadUrls } from "../url-checker.js";
37
+ import { akmConsolidate } from "./consolidate.js";
38
+ import { akmDistill, deriveLessonRef, isDistillRefusedInputType } from "./distill.js";
39
+ import { deriveKnowledgeRef } from "./distill-promotion-policy.js";
40
+ import { countEvalCases, writeEvalCase } from "./eval-cases.js";
41
+ import { akmExtract, countNewExtractCandidates } from "./extract.js";
42
+ import { makeGateConfig, resolveExtractConfidence, runAutoAcceptGate } from "./improve-auto-accept.js";
43
+ import { isProfileFilteredForAllPasses, resolveImproveProfile, resolveProcessEnabled, shouldSkipRef, } from "./improve-profiles.js";
44
+ import { detectAndWriteContradictions } from "./memory/memory-contradiction-detect.js";
45
+ import { analyzeMemoryCleanup, applyMemoryCleanup } from "./memory/memory-improve.js";
46
+ import { akmReflect } from "./reflect.js";
44
47
  function resolveImproveScope(scope) {
45
48
  const trimmed = scope?.trim();
46
49
  if (!trimmed)
@@ -51,7 +54,7 @@ function resolveImproveScope(scope) {
51
54
  }
52
55
  catch {
53
56
  if (!isAssetType(trimmed)) {
54
- throw new UsageError(`Unknown asset type: "${trimmed}". Valid types: memory, knowledge, skill, lesson, workflow, agent, command, script, wiki, env, vault, task.\n` +
57
+ throw new UsageError(`Unknown asset type: "${trimmed}". Valid types: memory, knowledge, skill, lesson, workflow, agent, command, script, wiki, env, secret, task.\n` +
55
58
  `If you passed --format to akm improve, that flag is not supported — use it with akm search or akm show instead.`, "INVALID_FLAG_VALUE");
56
59
  }
57
60
  return { mode: "type", value: trimmed };
@@ -70,6 +73,9 @@ function resolveImproveScope(scope) {
70
73
  * {scope} scope value (e.g. a ref/type) or the scope mode (`all`)
71
74
  * {refs} number of planned refs this run processed
72
75
  * {accepted} number of proposals auto-accepted by the confidence gate
76
+ * {triage_promoted} proposals promoted by the triage pre-pass (0 if triage did not run)
77
+ * {triage_rejected} proposals rejected by the triage pre-pass (0 if triage did not run)
78
+ * {runId} this run's id (empty string when absent)
73
79
  *
74
80
  * The result is still passed through `sanitizeCommitMessage` downstream in
75
81
  * `saveGitStash`, so token values never widen the commit-message attack surface
@@ -87,6 +93,9 @@ export function renderSyncCommitMessage(template, result, nowMs) {
87
93
  scope: result.scope.value ?? result.scope.mode,
88
94
  refs: String(result.plannedRefs.length),
89
95
  accepted: String(result.gateAutoAcceptedCount ?? 0),
96
+ triage_promoted: String(result.triage?.promoted ?? 0),
97
+ triage_rejected: String(result.triage?.rejected ?? 0),
98
+ runId: result.runId ?? "",
90
99
  };
91
100
  return template.replace(/\{(\w+)\}/g, (match, key) => (Object.hasOwn(tokens, key) ? tokens[key] : match));
92
101
  }
@@ -395,6 +404,51 @@ function isSignalDeltaEligible(ref, latestFeedback, lastProposal) {
395
404
  return true;
396
405
  return fb > lp;
397
406
  }
407
+ /**
408
+ * H7 (#566): cooperative budget watchdog with a captured, RAII-cleared hard-kill.
409
+ *
410
+ * When the wall-clock budget expires, `onExhausted` (normally an
411
+ * `AbortController.abort`) signals cooperative cancellation so the run can drain
412
+ * its in-flight log/`state.db` flush and unwind naturally. A second hard-kill
413
+ * timer is then armed as a watchdog: it only `exit(0)`s if the drain itself
414
+ * overruns `hardKillGraceMs`, preventing the process from outliving the task
415
+ * timeout window (lock-cascade fix).
416
+ *
417
+ * Both timers are captured; the returned dispose() clears whichever is still
418
+ * pending. Callers invoke it from a `finally`, so a *clean* drain reaches the
419
+ * `finally` and cancels the pending hard-kill before it can fire — the previous
420
+ * detached `setTimeout(() => process.exit(0), 5000)` always fired, truncating a
421
+ * clean flush. The hard-kill timer is `unref()`-ed so it never keeps the event
422
+ * loop alive on its own: once the run drains it exits with its own code, not the
423
+ * forced 0.
424
+ *
425
+ * Dependencies are injectable purely so the concurrency-sensitive timing
426
+ * contract can be exercised deterministically in unit tests.
427
+ */
428
+ export function armBudgetWatchdog(budgetMs, controller, deps) {
429
+ const setTimeoutFn = deps?.setTimeoutFn ?? setTimeout;
430
+ const clearTimeoutFn = deps?.clearTimeoutFn ?? clearTimeout;
431
+ const exitFn = deps?.exitFn ?? ((code) => process.exit(code));
432
+ const hardKillGraceMs = deps?.hardKillGraceMs ?? 5_000;
433
+ let hardKillTimer;
434
+ const budgetTimer = setTimeoutFn(() => {
435
+ // Cooperative cancellation first: let the run drain.
436
+ controller.abort("improve budget exhausted");
437
+ // Watchdog: only force-exit if the drain itself overruns the grace period.
438
+ // Exit 0: budget exhaustion is a normal scheduled-task condition, not an error.
439
+ hardKillTimer = setTimeoutFn(() => exitFn(0), hardKillGraceMs);
440
+ // Never keep the event loop alive solely for the watchdog.
441
+ hardKillTimer.unref?.();
442
+ }, budgetMs);
443
+ // RAII dispose: clears whichever timer is still pending. Idempotent.
444
+ return () => {
445
+ clearTimeoutFn(budgetTimer);
446
+ if (hardKillTimer !== undefined) {
447
+ clearTimeoutFn(hardKillTimer);
448
+ hardKillTimer = undefined;
449
+ }
450
+ };
451
+ }
398
452
  export async function akmImprove(options = {}) {
399
453
  const scope = resolveImproveScope(options.scope);
400
454
  const reflectFn = options.reflectFn ?? akmReflect;
@@ -421,6 +475,15 @@ export async function akmImprove(options = {}) {
421
475
  catch {
422
476
  primaryStashDir = undefined;
423
477
  }
478
+ // C2 (#553/#554/#499): resolve the state.db path ONCE, synchronously, at the
479
+ // command boundary — before the first `await` below. Every state.db open in
480
+ // this run (`openStateDatabase`, every default-path `appendEvent`) is pinned
481
+ // to this snapshot via `eventsCtx.dbPath`, so a parallel test file mutating
482
+ // `process.env.XDG_DATA_HOME` across an await boundary can never redirect this
483
+ // run's DB opens to a wrong/just-deleted tmpdir mid-flight (the parallel-load
484
+ // timeout root cause). Because beforeEach runs synchronously, env is still the
485
+ // calling test's own at this point; we capture it before yielding the loop.
486
+ const resolvedStateDbPath = getStateDbPathInDataDir();
424
487
  // Phase 4 lock hoist (§7): the `improve.lock` setup is hoisted ABOVE
425
488
  // ensureIndex/collectEligibleRefs so the triage pre-pass (and improve's own
426
489
  // queue writes) run fully serialized under the lock. The dry-run early-return
@@ -497,14 +560,14 @@ export async function akmImprove(options = {}) {
497
560
  }
498
561
  lockAcquired = false;
499
562
  };
500
- // Signal-safe lock release (0.8.3 hotfix). The SIGTERM/SIGINT/SIGHUP handler
501
- // in improve-cli.ts calls `process.exit()`, which does NOT run the `finally`
502
- // below that owns lock release — so a cron-timeout SIGTERM leaked
503
- // `improve.lock` every run. `process.exit()` DOES fire `'exit'` listeners,
504
- // so we release the lock from one. `releaseLockIfOwned` only unlinks a lock
505
- // still owned by this PID, so it is safe even if a later run re-acquired it.
506
- // The listener is removed in the `finally` so the normal path stays single-release
507
- // and repeated in-process `akmImprove` calls (tests) do not accumulate listeners.
563
+ // Signal-safe lock release. The SIGTERM/SIGINT/SIGHUP handler in improve-cli.ts
564
+ // calls `process.exit()`, which does NOT run the `finally` below that owns lock
565
+ // release — so a cron-timeout SIGTERM leaked `improve.lock` every run.
566
+ // `process.exit()` DOES fire `'exit'` listeners, so we release the lock from
567
+ // one. `releaseLockIfOwned` only unlinks a lock still owned by this PID, so it
568
+ // is safe even if a later run re-acquired it. The listener is removed in the
569
+ // `finally` so the normal path stays single-release and repeated in-process
570
+ // `akmImprove` calls (tests) do not accumulate listeners.
508
571
  const releaseLockOnExit = () => {
509
572
  releaseLockIfOwned(resolvedLockPath, process.pid);
510
573
  };
@@ -514,6 +577,7 @@ export async function akmImprove(options = {}) {
514
577
  let profileFilteredRefs;
515
578
  let memoryCleanupPlan;
516
579
  let guidance;
580
+ let triageDrain;
517
581
  try {
518
582
  // Acquire the lock and run the triage pre-pass for non-dry-run executions.
519
583
  // The dry-run branch below produces plannedRefs/memorySummary WITHOUT the lock
@@ -545,7 +609,7 @@ export async function akmImprove(options = {}) {
545
609
  const judgment = triageConfig?.judgment
546
610
  ? resolveTriageJudgmentRunner(triageConfig.judgment, _earlyConfig)
547
611
  : null;
548
- await drainProposalsFn({
612
+ triageDrain = await drainProposalsFn({
549
613
  stashDir: primaryStashDir,
550
614
  policy,
551
615
  applyMode,
@@ -686,25 +750,27 @@ export async function akmImprove(options = {}) {
686
750
  let eventsDb;
687
751
  // `eventsCtx` is read by the main catch (improve_failed) and finally, so it
688
752
  // lives in the outer scope. It is always assigned at the top of the try.
689
- let eventsCtx = {};
753
+ // Pinned to the boundary snapshot so the fallback per-call `appendEvent`
754
+ // opens (when the long-lived handle below fails to open) never re-read env.
755
+ let eventsCtx = { dbPath: resolvedStateDbPath };
690
756
  try {
691
- const budgetTimer = setTimeout(() => {
692
- budgetAbortController.abort("improve budget exhausted");
693
- // Grace period: let finally run to release improve.lock, then hard-exit
694
- // to prevent the process outliving the task timeout window (lock-cascade fix).
695
- // Exit 0: budget exhaustion is a normal scheduled-task condition, not an error.
696
- setTimeout(() => process.exit(0), 5_000);
697
- }, budgetMs);
698
- // Clear the timer when the run ends to avoid keeping the event loop alive.
699
- clearBudgetTimer = () => clearTimeout(budgetTimer);
757
+ // H7 (#566): arm the budget watchdog. `armBudgetWatchdog` captures both the
758
+ // budget timer and the hard-kill timer it schedules on exhaustion, returning
759
+ // a single dispose() that clears whichever are still pending. The `finally`
760
+ // calls dispose() via `clearBudgetTimer` (RAII), so a clean cooperative
761
+ // drain cancels the pending hard-kill before it can fire the process then
762
+ // exits naturally instead of being force-`exit(0)`-ed mid-flush, which could
763
+ // truncate an in-flight log or `state.db` transaction.
764
+ clearBudgetTimer = armBudgetWatchdog(budgetMs, budgetAbortController);
700
765
  try {
701
- eventsDb = openStateDatabase();
766
+ eventsDb = openStateDatabase(resolvedStateDbPath);
702
767
  eventsCtx = { db: eventsDb };
703
768
  }
704
769
  catch (err) {
705
770
  rethrowIfTestIsolationError(err);
706
- // If we cannot open state.db up-front, fall back to per-call opens.
707
- eventsCtx = {};
771
+ // If we cannot open state.db up-front, fall back to per-call opens — but
772
+ // still pinned to the boundary-resolved path, never a live env re-read.
773
+ eventsCtx = { dbPath: resolvedStateDbPath };
708
774
  }
709
775
  // 2026-05-27: emit `improve_skipped` audit events for refs the planner
710
776
  // pre-filtered (reflect AND distill both refuse them under the active
@@ -763,20 +829,23 @@ export async function akmImprove(options = {}) {
763
829
  eventsCtx,
764
830
  improveProfile,
765
831
  });
766
- const { allWarnings, consolidation, deadUrls, memoryInference, graphExtraction, stalenessDetection, maintenanceActions, memoryInferenceDurationMs, graphExtractionDurationMs, orphansPurged, proposalsExpired, gateAutoAcceptedCount: postLoopGateCount, gateAutoAcceptFailedCount: postLoopGateFailedCount, } = await runImprovePostLoopStage({
832
+ // #551: consolidation now runs in the preparation stage (before extract);
833
+ // its result and run-flag are read from `preparation`, not the post-loop.
834
+ const consolidation = preparation.consolidation;
835
+ const { allWarnings, deadUrls, memoryInference, graphExtraction, stalenessDetection, maintenanceActions, memoryInferenceDurationMs, graphExtractionDurationMs, orphansPurged, proposalsExpired, gateAutoAcceptedCount: postLoopGateCount, gateAutoAcceptFailedCount: postLoopGateFailedCount, } = await runImprovePostLoopStage({
767
836
  scope,
768
837
  options,
769
838
  primaryStashDir,
770
839
  actionableRefs: preparation.actionableRefs,
771
840
  appliedCleanup: preparation.appliedCleanup,
772
841
  cleanupWarnings: preparation.cleanupWarnings,
773
- memorySummary,
774
842
  memoryRefsForInference,
775
843
  reindexFn,
776
844
  eventsCtx,
777
845
  // O-1 (#364): propagate wall-clock budget signal to post-loop maintenance.
778
846
  budgetSignal: budgetAbortController.signal,
779
847
  improveProfile,
848
+ consolidationRan: preparation.consolidationRan,
780
849
  });
781
850
  const finalActions = maintenanceActions && maintenanceActions.length > 0
782
851
  ? [...preparation.actions, ...maintenanceActions]
@@ -852,6 +921,17 @@ export async function akmImprove(options = {}) {
852
921
  const f = preparation.gateAutoAcceptFailedCount + loopGateFailedCount + postLoopGateFailedCount;
853
922
  return f > 0 ? { gateAutoAcceptFailedCount: f } : {};
854
923
  })(),
924
+ ...(triageDrain
925
+ ? {
926
+ triage: {
927
+ promoted: triageDrain.promoted.length,
928
+ rejected: triageDrain.rejected.length,
929
+ deferred: triageDrain.deferred.length,
930
+ skippedByCap: triageDrain.skippedByCap.length,
931
+ },
932
+ }
933
+ : {}),
934
+ ...(options.runId !== undefined ? { runId: options.runId } : {}),
855
935
  };
856
936
  if (!result.dryRun)
857
937
  emitImproveCompletedEvent(result, {
@@ -954,6 +1034,7 @@ function emitImproveCompletedEvent(result, durations, eventsCtx) {
954
1034
  reflectFailed: 0,
955
1035
  reflectCooldown: 0,
956
1036
  reflectSkipped: 0,
1037
+ reflectGuardRejected: 0,
957
1038
  distill: 0,
958
1039
  distillSkipped: 0,
959
1040
  memoryPrune: 0,
@@ -961,7 +1042,16 @@ function emitImproveCompletedEvent(result, durations, eventsCtx) {
961
1042
  graphExtraction: 0,
962
1043
  error: 0,
963
1044
  };
1045
+ // Coarse audit buckets, derived from the SAME classifyImproveAction the
1046
+ // persisted metrics_json uses (state-db.ts#computeImproveRunMetrics) so the
1047
+ // emitted event and the stored row can never disagree.
1048
+ const classCounts = { accepted: 0, rejected: 0, error: 0, noop: 0 };
964
1049
  for (const action of result.actions ?? []) {
1050
+ classCounts[classifyImproveAction(action.mode)] += 1;
1051
+ // Per-variant counters for the event metadata. The default arm makes any
1052
+ // new ImproveActionMode variant a compile error so a future variant cannot
1053
+ // be silently dropped from the improve_completed event (the `reflect-guard-
1054
+ // rejected` case below was previously missing here entirely).
965
1055
  switch (action.mode) {
966
1056
  case "reflect":
967
1057
  actionCounts.reflect += 1;
@@ -975,6 +1065,9 @@ function emitImproveCompletedEvent(result, durations, eventsCtx) {
975
1065
  case "reflect-skipped":
976
1066
  actionCounts.reflectSkipped += 1;
977
1067
  break;
1068
+ case "reflect-guard-rejected":
1069
+ actionCounts.reflectGuardRejected += 1;
1070
+ break;
978
1071
  case "distill":
979
1072
  actionCounts.distill += 1;
980
1073
  break;
@@ -993,6 +1086,8 @@ function emitImproveCompletedEvent(result, durations, eventsCtx) {
993
1086
  case "error":
994
1087
  actionCounts.error += 1;
995
1088
  break;
1089
+ default:
1090
+ assertNever(action.mode);
996
1091
  }
997
1092
  }
998
1093
  appendEvent({
@@ -1012,6 +1107,12 @@ function emitImproveCompletedEvent(result, durations, eventsCtx) {
1012
1107
  reflectFailedActions: actionCounts.reflectFailed,
1013
1108
  reflectCooldownActions: actionCounts.reflectCooldown,
1014
1109
  reflectSkippedActions: actionCounts.reflectSkipped,
1110
+ // Previously dropped from the event entirely; now emitted so the guard
1111
+ // rejections are visible in improve_completed telemetry.
1112
+ reflectGuardRejectedActions: actionCounts.reflectGuardRejected,
1113
+ acceptedActions: classCounts.accepted,
1114
+ rejectedActions: classCounts.rejected,
1115
+ noopActions: classCounts.noop,
1015
1116
  reflectsWithErrorContext: result.reflectsWithErrorContext ?? 0,
1016
1117
  coverageGapCount: result.coverageGaps?.length ?? 0,
1017
1118
  evalCasesWritten: result.evalCasesWritten ?? 0,
@@ -1044,12 +1145,261 @@ function emitImproveCompletedEvent(result, durations, eventsCtx) {
1044
1145
  },
1045
1146
  }, eventsCtx);
1046
1147
  }
1148
+ /**
1149
+ * Run (or gate-skip) the memory consolidation pass.
1150
+ *
1151
+ * #551 — two coordinated changes live here:
1152
+ *
1153
+ * 1. STRUCTURAL: this runs before extract in the improve pipeline (see
1154
+ * `runImprovePreparationStage`). Consolidation therefore only ever judges
1155
+ * PRIOR-run memories; current-run extract promotions are invisible to it.
1156
+ *
1157
+ * 2. SMARTER POOL-DELTA GATE: even among on-disk files, a memory whose only
1158
+ * post-`lastConsolidateTs` mtime bump came from its OWN auto-accept
1159
+ * promotion (i.e. it was just promoted by extract in the immediately
1160
+ * preceding run and has not had a full improve cycle to settle) does NOT
1161
+ * count as "work to do". We exclude those paths from the pool-delta check
1162
+ * using the `promoted` events already emitted with each promotion's
1163
+ * `assetPath`. A genuinely-settled prior memory — one edited by feedback,
1164
+ * reflect, manual edit, or simply older than the last consolidate — still
1165
+ * triggers the run. This is gate-option (a) from the issue (same-run /
1166
+ * adjacent-run promotion exclusion), chosen over option (b) because there
1167
+ * is no `extract_completed` event in the data model to gate against;
1168
+ * `promoted` events with `assetPath` already carry exactly the signal we
1169
+ * need, so the fix is non-invasive and provably correct.
1170
+ */
1171
+ async function runConsolidationPass(args) {
1172
+ const { options, primaryStashDir, memorySummary, improveProfile, eventsCtx } = args;
1173
+ const baseConfig = options.config ?? loadConfig();
1174
+ const MEMORY_VOLUME_THRESHOLD = options.memoryVolumeConsolidationThreshold ?? 100;
1175
+ const hasLlm = !!(baseConfig.defaults?.llm || baseConfig.defaults?.agent);
1176
+ const volumeTriggered = typeof memorySummary.eligible === "number" && memorySummary.eligible > MEMORY_VOLUME_THRESHOLD && hasLlm;
1177
+ // When volume triggers a consolidation pass, force-enable the consolidate
1178
+ // process on the default improve profile so the gate accepts the run even
1179
+ // if the user's config disabled it. We synthesise a new profile override
1180
+ // rather than mutating connection settings.
1181
+ const consolidationConfig = volumeTriggered
1182
+ ? {
1183
+ ...baseConfig,
1184
+ profiles: {
1185
+ ...(baseConfig.profiles ?? {}),
1186
+ improve: {
1187
+ ...(baseConfig.profiles?.improve ?? {}),
1188
+ default: {
1189
+ ...(baseConfig.profiles?.improve?.default ?? {}),
1190
+ processes: {
1191
+ ...(baseConfig.profiles?.improve?.default?.processes ?? {}),
1192
+ consolidate: {
1193
+ ...(baseConfig.profiles?.improve?.default?.processes?.consolidate ?? {}),
1194
+ enabled: true,
1195
+ },
1196
+ },
1197
+ },
1198
+ },
1199
+ },
1200
+ }
1201
+ : baseConfig;
1202
+ // 0.8.0 pool-delta gate for consolidate: re-eligible iff at least one
1203
+ // memory file has been updated since the most recent successful
1204
+ // consolidate_completed event. Time-based cooldowns produced the same
1205
+ // synchronised-wave failure mode the reflect/distill cooldowns did; the
1206
+ // pool-delta gate ties consolidation to actual work-to-do.
1207
+ const recentConsolidations = readEvents({ type: "consolidate_completed" });
1208
+ const lastConsolidation = recentConsolidations.events
1209
+ .filter((e) => e.metadata?.processed && Number(e.metadata.processed) > 0)
1210
+ .sort((a, b) => new Date(b.ts ?? 0).getTime() - new Date(a.ts ?? 0).getTime())[0];
1211
+ const lastConsolidateTs = lastConsolidation?.ts;
1212
+ // #551 smarter gate: build the set of memory asset paths whose only delta
1213
+ // since the last consolidate is their OWN auto-accept promotion. Those files
1214
+ // have not had a full improve cycle to settle, so they offer no merge /
1215
+ // contradiction candidates yet — excluding them stops the gate firing on
1216
+ // freshly-promoted single-source memories. We read `promoted` events emitted
1217
+ // after the last consolidate; each carries the written `assetPath`.
1218
+ const promotedSinceConsolidate = (() => {
1219
+ const paths = new Set();
1220
+ try {
1221
+ const promoted = readEvents({
1222
+ type: "promoted",
1223
+ ...(lastConsolidateTs ? { since: lastConsolidateTs } : {}),
1224
+ }).events;
1225
+ for (const e of promoted) {
1226
+ const ap = e.metadata?.assetPath;
1227
+ if (typeof ap === "string" && ap.length > 0)
1228
+ paths.add(path.resolve(ap));
1229
+ }
1230
+ }
1231
+ catch {
1232
+ // best-effort: if the events query fails, fall back to no exclusions
1233
+ // (preserves pre-#551 behaviour rather than over-skipping).
1234
+ }
1235
+ return paths;
1236
+ })();
1237
+ // Pool-delta: any memory file with mtime > lastConsolidateTs flags work to do,
1238
+ // EXCEPT files whose only post-consolidate change was their own promotion.
1239
+ // Using file mtime keeps this query DB-free and matches what the indexer
1240
+ // already uses as the canonical `memory.updated_at` proxy.
1241
+ //
1242
+ // Bootstrap: when no successful consolidate_completed event has ever been
1243
+ // recorded, we cannot evaluate the pool-delta — treat as eligible so a
1244
+ // fresh stash runs consolidate once before the steady-state gate kicks in.
1245
+ const memoryUpdatedAfterLastConsolidate = (() => {
1246
+ if (volumeTriggered)
1247
+ return true; // volume override forces the run regardless.
1248
+ if (!lastConsolidateTs)
1249
+ return true; // bootstrap path: never consolidated.
1250
+ if (!primaryStashDir)
1251
+ return false;
1252
+ const memoriesDir = path.join(primaryStashDir, "memories");
1253
+ if (!fs.existsSync(memoriesDir))
1254
+ return false;
1255
+ try {
1256
+ return fs.readdirSync(memoriesDir).some((f) => {
1257
+ if (!f.endsWith(".md"))
1258
+ return false;
1259
+ const filePath = path.join(memoriesDir, f);
1260
+ // #551: skip files that were only touched by their own promotion this
1261
+ // cohort — they have no settled merge/contradiction candidates yet.
1262
+ if (promotedSinceConsolidate.has(path.resolve(filePath)))
1263
+ return false;
1264
+ try {
1265
+ return fs.statSync(filePath).mtime.toISOString() > lastConsolidateTs;
1266
+ }
1267
+ catch {
1268
+ return false;
1269
+ }
1270
+ });
1271
+ }
1272
+ catch {
1273
+ return false;
1274
+ }
1275
+ })();
1276
+ const consolidationOnCooldown = !volumeTriggered && !memoryUpdatedAfterLastConsolidate;
1277
+ // Profile gate: if profile explicitly disables consolidate, skip the entire pass.
1278
+ const consolidateDisabledByProfile = improveProfile?.processes?.consolidate?.enabled === false;
1279
+ // #553 minPoolSize guard: skip consolidation when the eligible memory pool is
1280
+ // below a minimum size, rather than spending an LLM pass on a handful of
1281
+ // memories. This is an INDEPENDENT skip condition from #551's mtime pool-delta
1282
+ // gate — either can skip. Default 500; `minPoolSize: 0` disables the guard.
1283
+ // Evaluated against the eligible-pool count BEFORE entering the LLM loop so a
1284
+ // skip costs ZERO LLM calls.
1285
+ const CONSOLIDATE_DEFAULT_MIN_POOL_SIZE = 500;
1286
+ const configuredMinPoolSize = improveProfile?.processes?.consolidate?.minPoolSize;
1287
+ const minPoolSize = typeof configuredMinPoolSize === "number" ? configuredMinPoolSize : CONSOLIDATE_DEFAULT_MIN_POOL_SIZE;
1288
+ const eligiblePoolSize = typeof memorySummary.eligible === "number" ? memorySummary.eligible : 0;
1289
+ // volumeTriggered means the pool already exceeds the volume threshold (100),
1290
+ // so a force-triggered run never trips the pool-size guard. The guard only
1291
+ // engages when minPoolSize > 0 and the eligible pool is strictly below it.
1292
+ const poolBelowMinSize = !volumeTriggered && minPoolSize > 0 && eligiblePoolSize < minPoolSize;
1293
+ let consolidation = {
1294
+ schemaVersion: 1,
1295
+ ok: true,
1296
+ shape: "consolidate-result",
1297
+ dryRun: false,
1298
+ previewOnly: false,
1299
+ target: "",
1300
+ processed: 0,
1301
+ merged: 0,
1302
+ deleted: 0,
1303
+ promoted: [],
1304
+ contradicted: 0,
1305
+ warnings: [],
1306
+ durationMs: 0,
1307
+ };
1308
+ let gateAutoAcceptedCount = 0;
1309
+ let gateAutoAcceptFailedCount = 0;
1310
+ const consolidateGateCfg = makeGateConfig("consolidate", {
1311
+ globalThreshold: options.autoAccept,
1312
+ dryRun: options.dryRun ?? false,
1313
+ stashDir: primaryStashDir,
1314
+ config: consolidationConfig,
1315
+ eventsCtx,
1316
+ }, { minimumThreshold: 95 });
1317
+ if (consolidateDisabledByProfile) {
1318
+ info("[improve] consolidation skipped (disabled by improve profile)");
1319
+ }
1320
+ else if (poolBelowMinSize) {
1321
+ // #553: eligible pool below the configured minimum — skip with zero LLM
1322
+ // calls. Reuse the #551 `improve_skipped` emission path so health surfaces
1323
+ // it via the dynamic skipReasons aggregation under `pool_below_min_size`.
1324
+ appendEvent({
1325
+ eventType: "improve_skipped",
1326
+ ref: "memory:_consolidation",
1327
+ metadata: {
1328
+ reason: "pool_below_min_size",
1329
+ poolSize: eligiblePoolSize,
1330
+ minPoolSize,
1331
+ },
1332
+ }, eventsCtx);
1333
+ info(`[improve] consolidation skipped (pool ${eligiblePoolSize} < minPoolSize ${minPoolSize})`);
1334
+ }
1335
+ else if (!consolidationOnCooldown) {
1336
+ consolidation = await akmConsolidate({
1337
+ ...options.consolidateOptions,
1338
+ config: consolidationConfig,
1339
+ stashDir: options.stashDir,
1340
+ autoTriggered: volumeTriggered,
1341
+ // Tie consolidate proposals back to this improve invocation so
1342
+ // accept-rate-per-run aggregation works. Mirrors reflect/propose/extract.
1343
+ sourceRun: `consolidate-${Date.now()}`,
1344
+ // Incremental consolidation: pass the last-consolidation timestamp so
1345
+ // akmConsolidate skips chunks with no memory changed since then. Converts
1346
+ // consolidation cost from O(pool) to O(changed clusters) — the fix for
1347
+ // the rising p95 tail where full-pool re-judging produced 5–10 min runs
1348
+ // that promoted ~0. undefined → full pass on first-ever run (bootstrap).
1349
+ // volumeTriggered correctly forces the run past cooldown but must NOT
1350
+ // override incrementalSince — the stash has ~1400 eligible memories so
1351
+ // volumeTriggered=true on every run, permanently forcing full 12-chunk
1352
+ // scans (~264s) instead of the intended 1-2 chunk incremental path (~44s).
1353
+ incrementalSince: lastConsolidateTs,
1354
+ maxChunkSize: improveProfile?.processes?.consolidate?.maxChunkSize,
1355
+ // Honor profile.autoAccept (already merged into options.autoAccept at the
1356
+ // top of akmImprove). The CLI parser always supplies 90 when --auto-accept
1357
+ // is absent, so ?? 90 is not needed here and would prevent --auto-accept=false
1358
+ // (which maps to undefined) from disabling consolidation auto-accept.
1359
+ // options.consolidateOptions.autoAccept (if explicitly provided by caller)
1360
+ // still wins because the spread above runs first.
1361
+ autoAccept: options.consolidateOptions?.autoAccept ?? options.autoAccept,
1362
+ });
1363
+ {
1364
+ const consolidateGr = await runAutoAcceptGate(consolidation.promoted.map((proposalId) => {
1365
+ try {
1366
+ if (!primaryStashDir)
1367
+ return { proposalId, confidence: undefined };
1368
+ const proposal = getProposal(primaryStashDir, proposalId);
1369
+ return { proposalId, confidence: proposal.confidence };
1370
+ }
1371
+ catch {
1372
+ return { proposalId, confidence: undefined };
1373
+ }
1374
+ }), consolidateGateCfg);
1375
+ gateAutoAcceptedCount += consolidateGr.promoted.length;
1376
+ gateAutoAcceptFailedCount += consolidateGr.failed.length;
1377
+ }
1378
+ if (consolidation.processed > 0) {
1379
+ appendEvent({
1380
+ eventType: "consolidate_completed",
1381
+ ref: "memory:_consolidation",
1382
+ metadata: { processed: consolidation.processed, merged: consolidation.merged },
1383
+ }, eventsCtx);
1384
+ }
1385
+ }
1386
+ else {
1387
+ appendEvent({
1388
+ eventType: "improve_skipped",
1389
+ ref: "memory:_consolidation",
1390
+ metadata: {
1391
+ reason: "consolidation_no_memory_updates",
1392
+ lastEventTs: lastConsolidation?.ts ?? null,
1393
+ },
1394
+ }, eventsCtx);
1395
+ info("[improve] consolidation skipped (no memory updates since last run)");
1396
+ }
1397
+ // D9: track whether consolidation wrote any data so graph extraction can reindex if needed
1398
+ const consolidationRan = !consolidateDisabledByProfile && !poolBelowMinSize && !consolidationOnCooldown && consolidation.processed > 0;
1399
+ return { consolidation, consolidationRan, gateAutoAcceptedCount, gateAutoAcceptFailedCount };
1400
+ }
1047
1401
  async function runImprovePreparationStage(args) {
1048
- const { scope, options, plannedRefs, memoryCleanupPlan, primaryStashDir, reindexFn, startMs, budgetMs, eventsCtx, initialCleanupWarnings,
1049
- // improveProfile is part of the preparation-stage signature for future use
1050
- // (per-process gating moved into the in-loop stage). Kept here so the
1051
- // signature does not drift away from the rest of the planner stack.
1052
- improveProfile: _improveProfile, } = args;
1402
+ const { scope, options, plannedRefs, memoryCleanupPlan, primaryStashDir, memorySummary, reindexFn, startMs, budgetMs, eventsCtx, initialCleanupWarnings, improveProfile, } = args;
1053
1403
  const actions = [];
1054
1404
  const cleanupWarnings = initialCleanupWarnings ? [...initialCleanupWarnings] : [];
1055
1405
  // Phase 0 — MEMORY.md budget check (200-line cap; warn at 180)
@@ -1070,6 +1420,23 @@ async function runImprovePreparationStage(args) {
1070
1420
  }
1071
1421
  }
1072
1422
  }
1423
+ // Phase 0.3 — memory consolidation pass (#551).
1424
+ //
1425
+ // Consolidation runs BEFORE the session-extract pass. This is the structural
1426
+ // half of the #551 fix: extract auto-accept writes brand-new memory .md files
1427
+ // on every run, which previously made the consolidation pool-delta gate fire
1428
+ // unconditionally (any new file => "memory updated since last consolidate").
1429
+ // By running consolidation first, the gate and akmConsolidate only ever see
1430
+ // memories that existed at the start of the run — current-run extract
1431
+ // promotions are not on disk yet. The complementary smarter-gate logic
1432
+ // (excluding adjacent-run promotions) lives in `runConsolidationPass`.
1433
+ const consolidationPass = await runConsolidationPass({
1434
+ options,
1435
+ primaryStashDir,
1436
+ memorySummary,
1437
+ improveProfile,
1438
+ eventsCtx,
1439
+ });
1073
1440
  // Phase 0.4 — session-extract pass.
1074
1441
  //
1075
1442
  // Reads native session files (claude-code JSONL, opencode storage tree)
@@ -1087,8 +1454,11 @@ async function runImprovePreparationStage(args) {
1087
1454
  // Failures are non-fatal — one harness throwing doesn't abort improve.
1088
1455
  // The extract envelope's own `warnings` field surfaces what went wrong.
1089
1456
  let extractResults;
1090
- let gateAutoAcceptedCount = 0;
1091
- let gateAutoAcceptFailedCount = 0;
1457
+ // Seed the preparation-stage gate counters with consolidation's auto-accept
1458
+ // gate results (#551: consolidation now runs in this stage), then accumulate
1459
+ // extract's gate results on top.
1460
+ let gateAutoAcceptedCount = consolidationPass.gateAutoAcceptedCount;
1461
+ let gateAutoAcceptFailedCount = consolidationPass.gateAutoAcceptFailedCount;
1092
1462
  const extractConfig = options.config ?? loadConfig();
1093
1463
  const extractGateCfg = makeGateConfig("extract", {
1094
1464
  globalThreshold: options.autoAccept,
@@ -1097,9 +1467,47 @@ async function runImprovePreparationStage(args) {
1097
1467
  config: extractConfig,
1098
1468
  eventsCtx,
1099
1469
  });
1470
+ // #554 minNewSessions gate: skip the entire extract pass (ensureIndex was
1471
+ // already done upstream; here we elide every akmExtract/processSession call)
1472
+ // when the NEW (unseen, in-window) candidate-session pool is below a minimum.
1473
+ // 22% of improve runs produce zero memory-inference writes because extract
1474
+ // finds no new sessions, yet still burns the full extract pipeline. Default 0
1475
+ // (disabled) preserves existing always-run behaviour; only opted-in profiles
1476
+ // (e.g. `frequent`) set it. Evaluated BEFORE any LLM call so a skip costs zero
1477
+ // LLM work AND writes nothing — which also means no extract auto-accept bumps
1478
+ // memory mtimes, so a skipped extract never flags work for the NEXT run's
1479
+ // consolidation mtime-gate (the downstream trigger #554 asks us to suppress).
1480
+ const EXTRACT_DEFAULT_MIN_NEW_SESSIONS = 0;
1481
+ const configuredMinNewSessions = extractConfig.profiles?.improve?.default?.processes?.extract?.minNewSessions;
1482
+ const minNewSessions = typeof configuredMinNewSessions === "number" ? configuredMinNewSessions : EXTRACT_DEFAULT_MIN_NEW_SESSIONS;
1100
1483
  if (isLlmFeatureEnabled(extractConfig, "session_extraction")) {
1101
- const availableHarnesses = getAvailableHarnesses();
1102
- if (availableHarnesses.length > 0) {
1484
+ const availableHarnesses = options.extractHarnesses ?? getAvailableHarnesses();
1485
+ // The guard engages only when minNewSessions > 0; 0 disables it entirely.
1486
+ let belowMinNewSessions = false;
1487
+ if (minNewSessions > 0 && availableHarnesses.length > 0) {
1488
+ const countFn = options.extractCandidateCountFn ?? countNewExtractCandidates;
1489
+ const newCandidateCount = countFn(extractConfig, {
1490
+ ...(options.extractHarnesses ? { harnesses: options.extractHarnesses } : {}),
1491
+ // C2: pin the candidate-count state.db open to the boundary-resolved path.
1492
+ ...(eventsCtx?.dbPath ? { stateDbPath: eventsCtx.dbPath } : {}),
1493
+ });
1494
+ if (newCandidateCount < minNewSessions) {
1495
+ belowMinNewSessions = true;
1496
+ // Reuse the #551/#553 `improve_skipped` emission path so health's dynamic
1497
+ // skipReasons aggregation surfaces this under `below_min_new_sessions`.
1498
+ appendEvent({
1499
+ eventType: "improve_skipped",
1500
+ ref: "memory:_extract",
1501
+ metadata: {
1502
+ reason: "below_min_new_sessions",
1503
+ newSessions: newCandidateCount,
1504
+ minNewSessions,
1505
+ },
1506
+ }, eventsCtx);
1507
+ info(`[improve] extract skipped (new sessions ${newCandidateCount} < minNewSessions ${minNewSessions})`);
1508
+ }
1509
+ }
1510
+ if (!belowMinNewSessions && availableHarnesses.length > 0) {
1103
1511
  extractResults = [];
1104
1512
  for (const h of availableHarnesses) {
1105
1513
  try {
@@ -1108,6 +1516,9 @@ async function runImprovePreparationStage(args) {
1108
1516
  ...(primaryStashDir !== undefined ? { stashDir: primaryStashDir } : {}),
1109
1517
  config: extractConfig,
1110
1518
  dryRun: options.dryRun ?? false,
1519
+ ...(options.extractHarnesses ? { harnesses: options.extractHarnesses } : {}),
1520
+ // C2: pin extract's skip-tracking state.db open to the boundary path.
1521
+ ...(eventsCtx?.dbPath ? { stateDbPath: eventsCtx.dbPath } : {}),
1111
1522
  });
1112
1523
  extractResults.push(result);
1113
1524
  {
@@ -1420,7 +1831,7 @@ async function runImprovePreparationStage(args) {
1420
1831
  let dbForRetrieval;
1421
1832
  try {
1422
1833
  dbForRetrieval = openExistingDatabase();
1423
- const showEventCount = dbForRetrieval.prepare("SELECT COUNT(*) AS cnt FROM usage_events WHERE event_type = 'show'").get().cnt;
1834
+ const showEventCount = countUsageEventsByType(dbForRetrieval, "show");
1424
1835
  if (showEventCount === 0) {
1425
1836
  warn("Warning: show events not yet in usage_events — zero-feedback fallback will match only search-retrieved assets.");
1426
1837
  }
@@ -1574,9 +1985,10 @@ async function runImprovePreparationStage(args) {
1574
1985
  utilityMap,
1575
1986
  gateAutoAcceptedCount,
1576
1987
  gateAutoAcceptFailedCount,
1988
+ consolidation: consolidationPass.consolidation,
1989
+ consolidationRan: consolidationPass.consolidationRan,
1577
1990
  };
1578
1991
  }
1579
- // TODO(refactor): 13 args including `actions`/`recentErrors` mutation channels. Restructure into immutable plan + mutable context objects — deferred to dedicated refactor with isolated testing.
1580
1992
  async function runImproveLoopStage(args) {
1581
1993
  const { scope, options, primaryStashDir, reflectFn, distillFn, loopRefs, actions, signalBearingSet, distillCooledRefs, distillOnlyRefs, recentErrors, rejectedProposalsByRef, utilityMap, startMs, budgetMs, eventsCtx, improveProfile, } = args;
1582
1994
  // O-1 (#364): compute remaining budget at call time so each sub-call
@@ -1710,7 +2122,7 @@ async function runImproveLoopStage(args) {
1710
2122
  // path is also a no-op for them — we just avoid unnecessary agent spawns.
1711
2123
  // D2: distillOnlyRefs also skip the reflect call (reflect-cooled, distill path only).
1712
2124
  if (!isDistillOnly && !planned.ref.endsWith(".derived")) {
1713
- // Type guard: skip reflect for unsupported types (script, vault, task, etc.)
2125
+ // Type guard: skip reflect for unsupported types (script, env, task, etc.)
1714
2126
  // and raw wiki directories, driven by the active improve profile.
1715
2127
  const reflectSkip = shouldSkipRef(planned.ref, "reflect", improveProfile);
1716
2128
  if (reflectSkip.skip) {
@@ -1792,7 +2204,7 @@ async function runImproveLoopStage(args) {
1792
2204
  // true LLM failures. See
1793
2205
  // `/tmp/akm-health-investigations/metrics-taxonomy-review.md` §1a.
1794
2206
  const isGuardReject = !reflectResult.ok && reflectResult.reason === "content_policy_reject";
1795
- // Type-guard rejection (reflect refused a script/vault/task ref) is
2207
+ // Type-guard rejection (reflect refused a script/env/task ref) is
1796
2208
  // also NOT an LLM failure — the LLM is never invoked. Route to the
1797
2209
  // existing `reflect-skipped` bucket so it does not inflate the
1798
2210
  // failure-rate numerator. ~9% of `reflect-failed` events in the
@@ -2025,174 +2437,8 @@ async function runImproveLoopStage(args) {
2025
2437
  return { reflectsWithErrorContext, memoryRefsForInference, gateAutoAcceptedCount, gateAutoAcceptFailedCount };
2026
2438
  }
2027
2439
  async function runImprovePostLoopStage(args) {
2028
- const { scope, options, primaryStashDir, actionableRefs, appliedCleanup, cleanupWarnings, memorySummary, memoryRefsForInference, reindexFn, eventsCtx, budgetSignal, improveProfile, } = args;
2440
+ const { scope, options, primaryStashDir, actionableRefs, appliedCleanup, cleanupWarnings, memoryRefsForInference, reindexFn, eventsCtx, budgetSignal, improveProfile, consolidationRan, } = args;
2029
2441
  const allWarnings = [...cleanupWarnings, ...(appliedCleanup?.warnings ?? [])];
2030
- const baseConfig = options.config ?? loadConfig();
2031
- const MEMORY_VOLUME_THRESHOLD = options.memoryVolumeConsolidationThreshold ?? 100;
2032
- const hasLlm = !!(baseConfig.defaults?.llm || baseConfig.defaults?.agent);
2033
- const volumeTriggered = typeof memorySummary.eligible === "number" && memorySummary.eligible > MEMORY_VOLUME_THRESHOLD && hasLlm;
2034
- // When volume triggers a consolidation pass, force-enable the consolidate
2035
- // process on the default improve profile so the gate accepts the run even
2036
- // if the user's config disabled it. We synthesise a new profile override
2037
- // rather than mutating connection settings.
2038
- const consolidationConfig = volumeTriggered
2039
- ? {
2040
- ...baseConfig,
2041
- profiles: {
2042
- ...(baseConfig.profiles ?? {}),
2043
- improve: {
2044
- ...(baseConfig.profiles?.improve ?? {}),
2045
- default: {
2046
- ...(baseConfig.profiles?.improve?.default ?? {}),
2047
- processes: {
2048
- ...(baseConfig.profiles?.improve?.default?.processes ?? {}),
2049
- consolidate: {
2050
- ...(baseConfig.profiles?.improve?.default?.processes?.consolidate ?? {}),
2051
- enabled: true,
2052
- },
2053
- },
2054
- },
2055
- },
2056
- },
2057
- }
2058
- : baseConfig;
2059
- // 0.8.0 pool-delta gate for consolidate: re-eligible iff at least one
2060
- // memory file has been updated since the most recent successful
2061
- // consolidate_completed event. Time-based cooldowns produced the same
2062
- // synchronised-wave failure mode the reflect/distill cooldowns did; the
2063
- // pool-delta gate ties consolidation to actual work-to-do.
2064
- const recentConsolidations = readEvents({ type: "consolidate_completed" });
2065
- const lastConsolidation = recentConsolidations.events
2066
- .filter((e) => e.metadata?.processed && Number(e.metadata.processed) > 0)
2067
- .sort((a, b) => new Date(b.ts ?? 0).getTime() - new Date(a.ts ?? 0).getTime())[0];
2068
- const lastConsolidateTs = lastConsolidation?.ts;
2069
- // Pool-delta: any memory file with mtime > lastConsolidateTs flags work to do.
2070
- // Using file mtime keeps this query DB-free and matches what the indexer
2071
- // already uses as the canonical `memory.updated_at` proxy.
2072
- //
2073
- // Bootstrap: when no successful consolidate_completed event has ever been
2074
- // recorded, we cannot evaluate the pool-delta — treat as eligible so a
2075
- // fresh stash runs consolidate once before the steady-state gate kicks in.
2076
- const memoryUpdatedAfterLastConsolidate = (() => {
2077
- if (volumeTriggered)
2078
- return true; // volume override forces the run regardless.
2079
- if (!lastConsolidateTs)
2080
- return true; // bootstrap path: never consolidated.
2081
- if (!primaryStashDir)
2082
- return false;
2083
- const memoriesDir = path.join(primaryStashDir, "memories");
2084
- if (!fs.existsSync(memoriesDir))
2085
- return false;
2086
- try {
2087
- return fs.readdirSync(memoriesDir).some((f) => {
2088
- if (!f.endsWith(".md"))
2089
- return false;
2090
- try {
2091
- return fs.statSync(path.join(memoriesDir, f)).mtime.toISOString() > lastConsolidateTs;
2092
- }
2093
- catch {
2094
- return false;
2095
- }
2096
- });
2097
- }
2098
- catch {
2099
- return false;
2100
- }
2101
- })();
2102
- const consolidationOnCooldown = !volumeTriggered && !memoryUpdatedAfterLastConsolidate;
2103
- // Profile gate: if profile explicitly disables consolidate, skip the entire pass.
2104
- const consolidateDisabledByProfile = improveProfile?.processes?.consolidate?.enabled === false;
2105
- let consolidation = {
2106
- schemaVersion: 1,
2107
- ok: true,
2108
- shape: "consolidate-result",
2109
- dryRun: false,
2110
- previewOnly: false,
2111
- target: "",
2112
- processed: 0,
2113
- merged: 0,
2114
- deleted: 0,
2115
- promoted: [],
2116
- contradicted: 0,
2117
- warnings: [],
2118
- durationMs: 0,
2119
- };
2120
- let gateAutoAcceptedCount = 0;
2121
- let gateAutoAcceptFailedCount = 0;
2122
- const consolidateGateCfg = makeGateConfig("consolidate", {
2123
- globalThreshold: options.autoAccept,
2124
- dryRun: options.dryRun ?? false,
2125
- stashDir: primaryStashDir,
2126
- config: consolidationConfig,
2127
- eventsCtx,
2128
- }, { minimumThreshold: 95 });
2129
- if (consolidateDisabledByProfile) {
2130
- info("[improve] consolidation skipped (disabled by improve profile)");
2131
- }
2132
- else if (!consolidationOnCooldown) {
2133
- consolidation = await akmConsolidate({
2134
- ...options.consolidateOptions,
2135
- config: consolidationConfig,
2136
- stashDir: options.stashDir,
2137
- autoTriggered: volumeTriggered,
2138
- // Tie consolidate proposals back to this improve invocation so
2139
- // accept-rate-per-run aggregation works. Mirrors reflect/propose/extract.
2140
- sourceRun: `consolidate-${Date.now()}`,
2141
- // Incremental consolidation: pass the last-consolidation timestamp so
2142
- // akmConsolidate skips chunks with no memory changed since then. Converts
2143
- // consolidation cost from O(pool) to O(changed clusters) — the fix for
2144
- // the rising p95 tail where full-pool re-judging produced 5–10 min runs
2145
- // that promoted ~0. undefined → full pass on first-ever run (bootstrap).
2146
- // volumeTriggered correctly forces the run past cooldown but must NOT
2147
- // override incrementalSince — the stash has ~1400 eligible memories so
2148
- // volumeTriggered=true on every run, permanently forcing full 12-chunk
2149
- // scans (~264s) instead of the intended 1-2 chunk incremental path (~44s).
2150
- incrementalSince: lastConsolidateTs,
2151
- maxChunkSize: improveProfile?.processes?.consolidate?.maxChunkSize,
2152
- // Honor profile.autoAccept (already merged into options.autoAccept at the
2153
- // top of akmImprove). The CLI parser always supplies 90 when --auto-accept
2154
- // is absent, so ?? 90 is not needed here and would prevent --auto-accept=false
2155
- // (which maps to undefined) from disabling consolidation auto-accept.
2156
- // options.consolidateOptions.autoAccept (if explicitly provided by caller)
2157
- // still wins because the spread above runs first.
2158
- autoAccept: options.consolidateOptions?.autoAccept ?? options.autoAccept,
2159
- });
2160
- {
2161
- const consolidateGr = await runAutoAcceptGate(consolidation.promoted.map((proposalId) => {
2162
- try {
2163
- if (!primaryStashDir)
2164
- return { proposalId, confidence: undefined };
2165
- const proposal = getProposal(primaryStashDir, proposalId);
2166
- return { proposalId, confidence: proposal.confidence };
2167
- }
2168
- catch {
2169
- return { proposalId, confidence: undefined };
2170
- }
2171
- }), consolidateGateCfg);
2172
- gateAutoAcceptedCount += consolidateGr.promoted.length;
2173
- gateAutoAcceptFailedCount += consolidateGr.failed.length;
2174
- }
2175
- if (consolidation.processed > 0) {
2176
- appendEvent({
2177
- eventType: "consolidate_completed",
2178
- ref: "memory:_consolidation",
2179
- metadata: { processed: consolidation.processed, merged: consolidation.merged },
2180
- }, eventsCtx);
2181
- }
2182
- }
2183
- else {
2184
- appendEvent({
2185
- eventType: "improve_skipped",
2186
- ref: "memory:_consolidation",
2187
- metadata: {
2188
- reason: "consolidation_no_memory_updates",
2189
- lastEventTs: lastConsolidation?.ts ?? null,
2190
- },
2191
- }, eventsCtx);
2192
- info("[improve] consolidation skipped (no memory updates since last run)");
2193
- }
2194
- // D9: track whether consolidation wrote any data so graph extraction can reindex if needed
2195
- const consolidationRan = !consolidateDisabledByProfile && !consolidationOnCooldown && consolidation.processed > 0;
2196
2442
  info("[improve] post-loop maintenance starting");
2197
2443
  const maintenanceResult = await runImproveMaintenancePasses({
2198
2444
  options,
@@ -2233,7 +2479,6 @@ async function runImprovePostLoopStage(args) {
2233
2479
  }
2234
2480
  return {
2235
2481
  allWarnings,
2236
- consolidation,
2237
2482
  deadUrls,
2238
2483
  ...(maintenanceResult.memoryInference ? { memoryInference: maintenanceResult.memoryInference } : {}),
2239
2484
  ...(maintenanceResult.graphExtraction ? { graphExtraction: maintenanceResult.graphExtraction } : {}),
@@ -2245,8 +2490,10 @@ async function runImprovePostLoopStage(args) {
2245
2490
  graphExtractionDurationMs: maintenanceResult.graphExtractionDurationMs,
2246
2491
  orphansPurged: maintenanceResult.orphansPurged,
2247
2492
  proposalsExpired: maintenanceResult.proposalsExpired,
2248
- gateAutoAcceptedCount,
2249
- gateAutoAcceptFailedCount,
2493
+ // Consolidation's auto-accept gate counts now accrue in the preparation
2494
+ // stage (#551); post-loop no longer runs an auto-accept gate of its own.
2495
+ gateAutoAcceptedCount: 0,
2496
+ gateAutoAcceptFailedCount: 0,
2250
2497
  };
2251
2498
  }
2252
2499
  // TODO(refactor): mutates the passed-in `allWarnings` array as a hidden side channel. Return warnings in ImproveMaintenanceResult and merge in caller — invasive signature change deferred to next refactor pass.
@@ -2296,9 +2543,16 @@ async function runImproveMaintenancePasses(args) {
2296
2543
  const inferenceStart = Date.now();
2297
2544
  try {
2298
2545
  // O-1 (#364): pass budget signal so a hung inference call is cancelled.
2299
- memoryInference = await memoryInferenceFn(config, sources, budgetSignal, db, false, (event) => {
2300
- const current = event.currentRef ? ` ${event.currentRef}` : "";
2301
- info(`[improve] memory inference ${event.processed}/${event.total}${current} (written ${event.writtenFacts}, skipped ${event.skippedNoFacts})`);
2546
+ memoryInference = await memoryInferenceFn({
2547
+ config,
2548
+ sources,
2549
+ signal: budgetSignal,
2550
+ db,
2551
+ reEnrich: false,
2552
+ onProgress: (event) => {
2553
+ const current = event.currentRef ? ` ${event.currentRef}` : "";
2554
+ info(`[improve] memory inference ${event.processed}/${event.total}${current} (written ${event.writtenFacts}, skipped ${event.skippedNoFacts})`);
2555
+ },
2302
2556
  });
2303
2557
  memoryInferenceDurationMs = Date.now() - inferenceStart;
2304
2558
  actions.push({ ref: "memory:_inference", mode: "memory-inference", result: memoryInference });
@@ -2379,8 +2633,14 @@ async function runImproveMaintenancePasses(args) {
2379
2633
  info(`[improve] graph extraction ${event.processed}/${event.total}${current} (extracted ${event.extracted}, entities ${event.totalEntities}, relations ${event.totalRelations})`);
2380
2634
  };
2381
2635
  // O-1 (#364): pass budget signal so a hung graph extraction call is cancelled.
2382
- graphExtraction = await graphExtractionFn(config, sources, budgetSignal, db, false, progressHandler, {
2383
- candidatePaths,
2636
+ graphExtraction = await graphExtractionFn({
2637
+ config,
2638
+ sources,
2639
+ signal: budgetSignal,
2640
+ db,
2641
+ reEnrich: false,
2642
+ onProgress: progressHandler,
2643
+ options: { candidatePaths },
2384
2644
  });
2385
2645
  graphExtractionDurationMs = Date.now() - extractionStart;
2386
2646
  actions.push({ ref: "graph:_artifact", mode: "graph-extraction", result: graphExtraction });
@@ -2459,7 +2719,11 @@ async function runImproveMaintenancePasses(args) {
2459
2719
  if (retentionDays > 0) {
2460
2720
  let stateDb;
2461
2721
  try {
2462
- stateDb = openStateDatabase();
2722
+ // C2: reuse the boundary-pinned state.db path carried on eventsCtx so
2723
+ // this purge open never re-reads `process.env` live mid-run. The path
2724
+ // is always set by akmImprove; openStateDatabase() falls back to the
2725
+ // env-derived default only if a caller omitted it entirely.
2726
+ stateDb = openStateDatabase(eventsCtx?.dbPath);
2463
2727
  const purgedCount = purgeOldEvents(stateDb, retentionDays);
2464
2728
  if (purgedCount > 0) {
2465
2729
  info(`[improve] events purge: ${purgedCount} event(s) older than ${retentionDays}d removed from state.db`);
@@ -2504,7 +2768,7 @@ async function runImproveMaintenancePasses(args) {
2504
2768
  // and before the URL check (which lives in the outer caller).
2505
2769
  if (sources.length > 0) {
2506
2770
  try {
2507
- stalenessDetection = await stalenessDetectionFn(config, sources, budgetSignal, db);
2771
+ stalenessDetection = await stalenessDetectionFn({ config, sources, signal: budgetSignal, db });
2508
2772
  if (stalenessDetection.considered > 0) {
2509
2773
  info(`[improve] staleness detection complete (considered ${stalenessDetection.considered}, ` +
2510
2774
  `deprecated ${stalenessDetection.deprecated}, confirmed ${stalenessDetection.confirmed}, ` +