akm-cli 0.9.17-alpha.3 → 0.9.17-alpha.4

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 (343) hide show
  1. package/CHANGELOG.md +731 -0
  2. package/dist/akm +94 -196
  3. package/dist/cli/shared.js +6 -2
  4. package/dist/cli.js +22 -9
  5. package/dist/commands/agent/agent-dispatch.js +1 -1
  6. package/dist/commands/command/command-execution.js +24 -62
  7. package/dist/commands/feedback-cli.js +0 -1
  8. package/dist/commands/health/accept-rate.js +2 -2
  9. package/dist/commands/health/checks.js +30 -75
  10. package/dist/commands/health/config-skew.js +38 -0
  11. package/dist/commands/health/egress.js +54 -0
  12. package/dist/commands/health/html-report.js +0 -38
  13. package/dist/commands/health/improve-metrics.js +123 -562
  14. package/dist/commands/health/plugin-staleness.js +53 -3
  15. package/dist/commands/health/renderers.js +12 -4
  16. package/dist/commands/health/report-view-model.js +11 -106
  17. package/dist/commands/health/types-improve.js +4 -19
  18. package/dist/commands/health/windows.js +64 -73
  19. package/dist/commands/health.js +122 -143
  20. package/dist/commands/improve/consolidate/chunking.js +25 -100
  21. package/dist/commands/improve/consolidate/sanitize.js +54 -149
  22. package/dist/commands/improve/consolidate.js +538 -1075
  23. package/dist/commands/improve/content-hash.js +16 -24
  24. package/dist/commands/improve/distill/content-repair.js +18 -100
  25. package/dist/commands/improve/distill-guards.js +20 -81
  26. package/dist/commands/improve/distill-promotion-policy.js +23 -243
  27. package/dist/commands/improve/distill.js +608 -1075
  28. package/dist/commands/improve/eligibility.js +126 -400
  29. package/dist/commands/improve/execution.js +3 -5
  30. package/dist/commands/improve/extract.js +487 -1046
  31. package/dist/commands/improve/feedback-valence.js +0 -25
  32. package/dist/commands/improve/improve-cli.js +29 -166
  33. package/dist/commands/improve/improve-result-file.js +10 -66
  34. package/dist/commands/improve/improve-strategies.js +12 -7
  35. package/dist/commands/improve/improve-usage-report.js +18 -64
  36. package/dist/commands/improve/improve.js +443 -1063
  37. package/dist/commands/improve/ledger.js +114 -0
  38. package/dist/commands/improve/locks.js +2 -8
  39. package/dist/commands/improve/loop-stages.js +459 -1172
  40. package/dist/commands/improve/memory/derived-ref.js +12 -77
  41. package/dist/commands/improve/memory/memory-belief.js +14 -118
  42. package/dist/commands/improve/memory/memory-improve.js +4 -3
  43. package/dist/commands/improve/outcome-loop.js +28 -156
  44. package/dist/commands/improve/planner.js +5 -10
  45. package/dist/commands/improve/preparation.js +851 -2339
  46. package/dist/commands/improve/proactive-maintenance.js +34 -101
  47. package/dist/commands/improve/reflect-noise.js +104 -280
  48. package/dist/commands/improve/reflect.js +621 -1367
  49. package/dist/commands/improve/salience.js +46 -232
  50. package/dist/commands/improve/session-asset.js +19 -100
  51. package/dist/commands/improve/stage.js +323 -0
  52. package/dist/commands/proposal/drain.js +251 -644
  53. package/dist/commands/proposal/proposal-cli.js +3 -18
  54. package/dist/commands/proposal/proposal-types.js +20 -41
  55. package/dist/commands/proposal/proposal.js +1 -2
  56. package/dist/commands/proposal/propose.js +134 -160
  57. package/dist/commands/proposal/repository.js +502 -1487
  58. package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
  59. package/dist/commands/proposal/validators/proposal-validators.js +1 -1
  60. package/dist/commands/proposal/validators/proposals.js +13 -89
  61. package/dist/commands/read/curate.js +63 -413
  62. package/dist/commands/read/search-cli.js +16 -33
  63. package/dist/commands/read/search.js +17 -23
  64. package/dist/commands/read/show.js +2 -13
  65. package/dist/commands/sources/bundle-cli.js +25 -2
  66. package/dist/commands/sources/bundle-config-ops.js +7 -0
  67. package/dist/commands/sources/dangerous-env-audit.js +1 -2
  68. package/dist/commands/sources/info.js +2 -11
  69. package/dist/commands/sources/installed-stashes.js +197 -746
  70. package/dist/commands/sources/schema-repair.js +98 -129
  71. package/dist/commands/sources/source-add.js +62 -12
  72. package/dist/commands/sources/stash-cli.js +1 -1
  73. package/dist/commands/tasks/explain.js +10 -13
  74. package/dist/commands/tasks/tasks-cli.js +9 -8
  75. package/dist/commands/tasks/tasks.js +326 -930
  76. package/dist/commands/tasks/validate.js +42 -21
  77. package/dist/commands/workflow/plan.js +22 -29
  78. package/dist/commands/workflow-cli.js +4 -4
  79. package/dist/core/adapter/adapters/akm-adapter.js +0 -1
  80. package/dist/core/adapter/adapters/akm-lint.js +2 -3
  81. package/dist/core/adapter/adapters/akm-metadata.js +11 -12
  82. package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
  83. package/dist/core/adapter/execution-source.js +17 -29
  84. package/dist/core/asset/resolve-ref.js +1 -1
  85. package/dist/core/bundle-id.js +42 -5
  86. package/dist/core/bundle-rename.js +291 -0
  87. package/dist/core/config/config-io.js +1 -2
  88. package/dist/core/config/config-schema.js +1 -33
  89. package/dist/core/config/config-walker.js +1 -1
  90. package/dist/core/config/config.js +163 -68
  91. package/dist/core/config/legacy-source-shape-shim.js +38 -9
  92. package/dist/core/config/schema/embedding.js +20 -5
  93. package/dist/core/config/schema/engines.js +5 -0
  94. package/dist/core/config/schema/execution.js +1 -1
  95. package/dist/core/config/schema/experimental.js +1 -1
  96. package/dist/core/config/schema/improve-processes.js +21 -95
  97. package/dist/core/config/schema/improve.js +4 -42
  98. package/dist/core/config/schema/scheduler.js +12 -12
  99. package/dist/core/config/schema/search.js +6 -22
  100. package/dist/core/env-secret-ref.js +0 -1
  101. package/dist/core/errors.js +8 -9
  102. package/dist/core/file-lock.js +76 -173
  103. package/dist/core/logs-db.js +2 -2
  104. package/dist/core/paths.js +0 -27
  105. package/dist/core/redaction.js +109 -2
  106. package/dist/core/run-lock.js +2 -5
  107. package/dist/core/spawn-env.js +1 -1
  108. package/dist/core/state/migrations.js +108 -61
  109. package/dist/core/state-db-scope.js +2 -4
  110. package/dist/core/state-db.js +126 -692
  111. package/dist/core/type-presentation.js +1 -9
  112. package/dist/core/write-source.js +293 -1012
  113. package/dist/execution/input-contract.js +1 -1
  114. package/dist/execution/resolved-request.js +135 -689
  115. package/dist/execution/source.js +63 -257
  116. package/dist/execution/target-ref.js +1 -1
  117. package/dist/indexer/bundle-identity-guard.js +2 -2
  118. package/dist/indexer/db/graph-db.js +106 -46
  119. package/dist/indexer/ensure-index.js +44 -85
  120. package/dist/indexer/graph/graph-extraction.js +340 -562
  121. package/dist/indexer/graph/graph-related.js +130 -0
  122. package/dist/indexer/index-rebuild-lock.js +3 -11
  123. package/dist/indexer/index-writer-lock.js +8 -17
  124. package/dist/indexer/index-written-assets.js +139 -151
  125. package/dist/indexer/indexer.js +524 -846
  126. package/dist/indexer/materialize-embeddings.js +60 -397
  127. package/dist/indexer/passes/memory-inference.js +81 -90
  128. package/dist/indexer/passes/metadata.js +132 -200
  129. package/dist/indexer/read-preflight.js +0 -7
  130. package/dist/indexer/scan/doc-to-entry.js +1 -3
  131. package/dist/indexer/scan/drain-dir.js +1 -1
  132. package/dist/indexer/search/db-search.js +181 -590
  133. package/dist/indexer/search/fts-query.js +30 -41
  134. package/dist/indexer/search/ranking.js +28 -154
  135. package/dist/indexer/search/search-attribution.js +12 -32
  136. package/dist/indexer/search/search-fields.js +11 -15
  137. package/dist/indexer/search/search-hit-enrichers.js +54 -85
  138. package/dist/indexer/search/search-source.js +1 -4
  139. package/dist/indexer/usage/usage-events.js +2 -7
  140. package/dist/integrations/agent/engine-fallback.js +23 -40
  141. package/dist/integrations/agent/engine-resolution.js +93 -183
  142. package/dist/integrations/agent/execution.js +507 -0
  143. package/dist/integrations/agent/model-map.js +28 -156
  144. package/dist/integrations/agent/request-lowering.js +66 -141
  145. package/dist/integrations/agent/runner-dispatch.js +143 -321
  146. package/dist/integrations/agent/runner.js +54 -14
  147. package/dist/integrations/lockfile.js +53 -101
  148. package/dist/llm/embedders/deterministic.js +2 -3
  149. package/dist/llm/embedders/profile.js +71 -0
  150. package/dist/llm/embedders/remote.js +10 -15
  151. package/dist/llm/graph-extract.js +3 -12
  152. package/dist/llm/index-passes.js +3 -5
  153. package/dist/llm/memory-infer.js +1 -2
  154. package/dist/llm/metadata-enhance.js +1 -2
  155. package/dist/llm/structured-call.js +5 -24
  156. package/dist/output/generic-render.js +23 -11
  157. package/dist/output/html-render.js +13 -10
  158. package/dist/output/render-registry.js +3 -32
  159. package/dist/output/shapes/helpers.js +2 -34
  160. package/dist/output/shapes/passthrough.js +1 -9
  161. package/dist/{indexer/search/ranking-types.js → output/text/bundle-rename.js} +4 -1
  162. package/dist/output/text/command-format.js +60 -23
  163. package/dist/output/text/helpers.js +1 -1
  164. package/dist/output/text/migrate.js +5 -14
  165. package/dist/output/text/proposal-format.js +1 -2
  166. package/dist/output/text/workflow-format.js +0 -32
  167. package/dist/output/text.js +2 -0
  168. package/dist/registry/factory.js +4 -19
  169. package/dist/registry/network.js +66 -220
  170. package/dist/registry/providers/index.js +0 -2
  171. package/dist/registry/providers/skills-sh.js +3 -14
  172. package/dist/registry/providers/static-index.js +24 -26
  173. package/dist/registry/resolve.js +55 -131
  174. package/dist/scripts/akm-migrate-node.js +43937 -93313
  175. package/dist/scripts/akm-migrate.js +43697 -93071
  176. package/dist/setup/registry-stash-loader.js +4 -13
  177. package/dist/setup/semantic-assets.js +3 -44
  178. package/dist/setup/setup.js +1 -1
  179. package/dist/setup/steps/tasks.js +25 -15
  180. package/dist/sources/provider-factory.js +17 -18
  181. package/dist/sources/providers/filesystem.js +2 -3
  182. package/dist/sources/providers/git-install.js +7 -1
  183. package/dist/sources/providers/git-provider.js +0 -3
  184. package/dist/sources/providers/git-stash.js +0 -17
  185. package/dist/sources/providers/npm.js +2 -4
  186. package/dist/sources/providers/provider-utils.js +5 -10
  187. package/dist/sources/providers/website.js +0 -2
  188. package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
  189. package/dist/sources/website-url.js +2 -2
  190. package/dist/storage/database.js +9 -35
  191. package/dist/storage/repositories/improve-ledger-repository.js +168 -0
  192. package/dist/storage/repositories/index-connection.js +34 -70
  193. package/dist/storage/repositories/index-entries-repository.js +69 -111
  194. package/dist/storage/repositories/index-entry-mapper.js +1 -2
  195. package/dist/storage/repositories/index-entry-schema.js +83 -269
  196. package/dist/storage/repositories/index-fts-repository.js +86 -256
  197. package/dist/storage/repositories/index-llm-cache-repository.js +17 -0
  198. package/dist/storage/repositories/index-meta-repository.js +6 -4
  199. package/dist/storage/repositories/index-schema.js +192 -220
  200. package/dist/storage/repositories/index-utility-repository.js +8 -29
  201. package/dist/storage/repositories/index-vec-repository.js +133 -414
  202. package/dist/storage/repositories/outcome-repository.js +2 -1
  203. package/dist/storage/repositories/proposals-repository.js +35 -0
  204. package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
  205. package/dist/storage/repositories/task-history-repository.js +26 -4
  206. package/dist/storage/repositories/workflow-runs-repository.js +53 -244
  207. package/dist/storage/sqlite-migrations.js +136 -0
  208. package/dist/storage/sqlite-pragmas.js +11 -9
  209. package/dist/storage/sqlite-transaction.js +170 -0
  210. package/dist/storage/state-db-integrity.js +34 -27
  211. package/dist/tasks/activation-config.js +134 -62
  212. package/dist/tasks/backends/cron.js +129 -277
  213. package/dist/tasks/backends/exec-utils.js +2 -5
  214. package/dist/tasks/backends/launchd.js +125 -745
  215. package/dist/tasks/backends/schtasks.js +101 -620
  216. package/dist/tasks/prepare/prepare-support.js +5 -15
  217. package/dist/tasks/prepare/prepare.js +0 -2
  218. package/dist/tasks/resolve-akm-bin.js +20 -79
  219. package/dist/tasks/run/attempt-lifecycle.js +0 -1
  220. package/dist/tasks/scheduler-binding.js +18 -238
  221. package/dist/tasks/scheduler-invocation.js +52 -52
  222. package/dist/tasks/scheduler-lock.js +53 -0
  223. package/dist/tasks/scheduler-sync.js +361 -751
  224. package/dist/tasks/source/parse-task-source.js +160 -10
  225. package/dist/tasks/source/task-source-v3-frozen.js +3 -4
  226. package/dist/tasks/source/task-to-v4.js +2 -2
  227. package/dist/workflows/authoring/authoring.js +3 -12
  228. package/dist/workflows/compile.js +211 -0
  229. package/dist/workflows/concurrency-policy.js +13 -74
  230. package/dist/workflows/exec/child-invocation.js +3 -17
  231. package/dist/workflows/exec/child-workflow.js +32 -141
  232. package/dist/workflows/exec/dispatch-redaction.js +13 -53
  233. package/dist/workflows/exec/environment.js +98 -0
  234. package/dist/workflows/exec/exec-unit.js +33 -140
  235. package/dist/workflows/exec/frozen-judge.js +7 -59
  236. package/dist/workflows/exec/native-executor.js +82 -341
  237. package/dist/workflows/exec/param-secrets.js +29 -47
  238. package/dist/workflows/exec/run-workflow.js +154 -387
  239. package/dist/workflows/exec/scheduler.js +9 -36
  240. package/dist/workflows/exec/step-work.js +127 -430
  241. package/dist/workflows/exec/unit-dispatch.js +11 -63
  242. package/dist/workflows/exec/unit-writer.js +8 -52
  243. package/dist/workflows/exec/worktree.js +39 -273
  244. package/dist/workflows/freeze/child-output-references.js +4 -15
  245. package/dist/workflows/freeze/environment.js +99 -92
  246. package/dist/workflows/freeze/freeze.js +172 -0
  247. package/dist/workflows/freeze/step-values.js +19 -21
  248. package/dist/workflows/freeze/targets/child-workflow.js +23 -92
  249. package/dist/workflows/freeze/targets/command.js +10 -33
  250. package/dist/workflows/freeze/targets/script.js +5 -12
  251. package/dist/workflows/freeze/targets/shell.js +3 -6
  252. package/dist/workflows/freeze/targets/task.js +25 -80
  253. package/dist/workflows/freeze/task-bindings.js +20 -67
  254. package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
  255. package/dist/workflows/ir/params.js +6 -51
  256. package/dist/workflows/ir/plan-hash.js +2 -34
  257. package/dist/workflows/parser.js +140 -43
  258. package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
  259. package/dist/workflows/renderer.js +36 -69
  260. package/dist/workflows/resource-limits.js +12 -120
  261. package/dist/workflows/runtime/agent-identity.js +8 -40
  262. package/dist/workflows/runtime/run-outputs.js +3 -6
  263. package/dist/workflows/runtime/run-plan.js +316 -0
  264. package/dist/workflows/runtime/runs.js +48 -200
  265. package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
  266. package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
  267. package/dist/workflows/validate-summary.js +2 -7
  268. package/docs/integration/bundling-akm.md +49 -42
  269. package/docs/migration/README.md +1 -0
  270. package/docs/migration/release-notes/0.9.17.md +41 -0
  271. package/docs/migration/v0.9.1-to-v0.9.2.md +19 -7
  272. package/docs/reference/cli.md +182 -125
  273. package/docs/reference/configuration.md +49 -56
  274. package/docs/reference/data-and-telemetry.md +19 -20
  275. package/docs/reference/tasks.md +86 -38
  276. package/docs/reference/workflow-schema.md +14 -18
  277. package/docs/reference/workflows.md +6 -9
  278. package/package.json +1 -1
  279. package/schemas/akm-config.json +87 -406
  280. package/dist/commands/health/advisories.js +0 -150
  281. package/dist/commands/health/metrics.js +0 -329
  282. package/dist/commands/health/surfaces.js +0 -102
  283. package/dist/commands/improve/anti-collapse.js +0 -83
  284. package/dist/commands/improve/collapse-detector.js +0 -432
  285. package/dist/commands/improve/consolidate/eligibility.js +0 -48
  286. package/dist/commands/improve/consolidate/merge.js +0 -146
  287. package/dist/commands/improve/distill/promote-memory.js +0 -329
  288. package/dist/commands/improve/distill/quality-gate.js +0 -500
  289. package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
  290. package/dist/commands/improve/proposal-envelope.js +0 -31
  291. package/dist/commands/improve/run-context.js +0 -123
  292. package/dist/commands/improve/shared.js +0 -21
  293. package/dist/commands/improve/source-identity.js +0 -28
  294. package/dist/commands/improve/triage.js +0 -96
  295. package/dist/commands/proposal/drain-policies.js +0 -151
  296. package/dist/commands/sources/update-transaction.js +0 -220
  297. package/dist/core/action-contributors.js +0 -28
  298. package/dist/core/config/config-version-shim.js +0 -101
  299. package/dist/core/config/retired-experimental-keys-shim.js +0 -62
  300. package/dist/core/fs-txn.js +0 -405
  301. package/dist/core/lexical-score.js +0 -25
  302. package/dist/core/maintenance-barrier.js +0 -167
  303. package/dist/execution/executable-identity.js +0 -105
  304. package/dist/execution/guarded-source.js +0 -441
  305. package/dist/indexer/graph/graph-boost.js +0 -427
  306. package/dist/indexer/graph/graph-dedup.js +0 -95
  307. package/dist/indexer/search/name-match.js +0 -35
  308. package/dist/indexer/search/ranking-contributors.js +0 -515
  309. package/dist/indexer/walk/project-context.js +0 -192
  310. package/dist/integrations/agent/execution-cascade.js +0 -566
  311. package/dist/integrations/agent/execution-definitions.js +0 -202
  312. package/dist/integrations/agent/execution-lowering.js +0 -841
  313. package/dist/integrations/agent/execution-preparation.js +0 -98
  314. package/dist/integrations/agent/inline-execution.js +0 -74
  315. package/dist/registry/create-provider-registry.js +0 -29
  316. package/dist/registry/pinned-request-helper.js +0 -247
  317. package/dist/registry/pinned-transport.js +0 -717
  318. package/dist/sources/providers/index.js +0 -14
  319. package/dist/storage/engines/sqlite-migrations.js +0 -271
  320. package/dist/storage/repositories/canaries-repository.js +0 -107
  321. package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
  322. package/dist/storage/repositories/registry-cache.js +0 -113
  323. package/dist/tasks/scheduler-sync-preview.js +0 -52
  324. package/dist/workflows/freeze/resolve-steps.js +0 -86
  325. package/dist/workflows/freeze/source-freeze.js +0 -64
  326. package/dist/workflows/ir/compile.js +0 -321
  327. package/dist/workflows/ir/environment-v4.js +0 -330
  328. package/dist/workflows/ir/freeze-v4.js +0 -153
  329. package/dist/workflows/ir/schema-v4.js +0 -745
  330. package/dist/workflows/ir/schema.js +0 -354
  331. package/dist/workflows/program/schema.js +0 -78
  332. package/dist/workflows/runtime/checkin.js +0 -57
  333. package/dist/workflows/runtime/plan-classifier.js +0 -196
  334. package/dist/workflows/runtime/unit-checkin.js +0 -45
  335. package/dist/workflows/runtime/unit-phases.js +0 -20
  336. package/dist/workflows/schema.js +0 -4
  337. package/dist/workflows/source-ir/compile.js +0 -200
  338. package/dist/workflows/source-ir/program.js +0 -50
  339. package/dist/workflows/source-ir/result.js +0 -26
  340. package/dist/workflows/source-ir/schema.js +0 -786
  341. package/dist/workflows/source-ir/triggers.js +0 -79
  342. package/dist/workflows/source-ir/uses.js +0 -40
  343. package/dist/workflows/validator.js +0 -60
@@ -1,13 +1,13 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /** The refs an improve run may consider, read from the index, and the small predicates over them. */
4
5
  import fs from "node:fs";
5
6
  import path from "node:path";
6
7
  import { parseFrontmatter } from "../../core/asset/frontmatter.js";
7
- import { conceptIdFromTypeName, parseRefInput, resolveRef } from "../../core/asset/resolve-ref.js";
8
+ import { conceptIdFromTypeName, parseRefInput, resolveRef, typeNameFromConceptId } from "../../core/asset/resolve-ref.js";
8
9
  import { loadConfig } from "../../core/config/config.js";
9
10
  import { ConfigError, NotFoundError, rethrowIfTestIsolationError, UsageError } from "../../core/errors.js";
10
- import { readEvents } from "../../core/events.js";
11
11
  import { isPathAbsent } from "../../core/path-access.js";
12
12
  import { getDbPath } from "../../core/paths.js";
13
13
  import { deriveInstallations, deriveWritableBundleIds } from "../../indexer/installations.js";
@@ -15,70 +15,71 @@ import { resolveSourceEntries } from "../../indexer/search/search-source.js";
15
15
  import { resolveAssetPath } from "../../indexer/walk/path-resolver.js";
16
16
  import { closeDatabase, openExistingDatabase, openReadonlyExistingDatabase, } from "../../storage/repositories/index-connection.js";
17
17
  import { getAllEntries } from "../../storage/repositories/index-entries-repository.js";
18
+ import { hasCurrentEntriesTable } from "../../storage/repositories/index-entry-schema.js";
18
19
  import { getUtilityScoresByIds } from "../../storage/repositories/index-utility-repository.js";
19
20
  import { SqliteReadSnapshotUnavailableError } from "../../storage/sqlite-read-snapshot.js";
20
21
  import { isDistillRefusedInputType } from "./distill.js";
21
22
  import { isStrategyFilteredForAllPasses } from "./improve-strategies.js";
22
23
  import { parseMemoryRef } from "./memory/derived-ref.js";
23
- import { improveStateReadRefs } from "./source-identity.js";
24
- // Eligibility / candidate-selection predicates for improve. Free functions
25
- // (no akmImprove closure state) extracted from improve.ts to shrink the
26
- // orchestrator and make candidate selection independently testable.
24
+ /** The asset type of a short or bundle-qualified conceptId (`""` when unknown). */
25
+ export function assetTypeOf(ref) {
26
+ return typeNameFromConceptId(ref.includes("//") ? ref.slice(ref.indexOf("//") + 2) : ref)?.type ?? "";
27
+ }
27
28
  /**
28
- * Open the index for eligibility queries, or `undefined` when no index exists
29
- * yet. `openExistingDatabase` refuses to create a missing `index.db` (see
30
- * index-connection.ts) — for eligibility, "no index" simply means nothing is
31
- * eligible, exactly like the readOnly arm's `undefined`.
32
- *
33
- * `undefined` is reserved for a genuinely ABSENT index. The readOnly arm has
34
- * refused to conflate that with an unreadable one since #791; this arm used
35
- * `fs.existsSync`, so the same run reported "nothing eligible to improve" at
36
- * exit 0 depending only on which branch it took. Both arms now raise.
29
+ * Run `fn` against index.db — the live index, or a non-mutating snapshot for
30
+ * a dry run. `undefined` when there is none or the read fails (best-effort).
37
31
  */
38
- function openEligibilityDb(readOnly) {
39
- if (readOnly)
40
- return openReadonlyExistingDatabase(undefined, { isolatedSnapshot: true });
41
- return isPathAbsent(getDbPath()) ? undefined : openExistingDatabase();
42
- }
43
- function describeIndexSnapshot(readOnly, status) {
44
- if (status === "ready") {
45
- return {
46
- status,
47
- reason: readOnly ? "loaded a non-mutating point-in-time copy of the existing index" : "loaded the prepared index",
48
- };
32
+ export function withIndexDb(readOnly, fn) {
33
+ let db;
34
+ try {
35
+ db = readOnly ? openReadonlyExistingDatabase(undefined, { isolatedSnapshot: true }) : openExistingDatabase();
36
+ return db ? fn(db) : undefined;
37
+ }
38
+ catch (err) {
39
+ rethrowIfTestIsolationError(err);
40
+ return undefined;
41
+ }
42
+ finally {
43
+ if (db)
44
+ closeDatabase(db);
49
45
  }
50
- return {
51
- status,
52
- reason: readOnly
53
- ? "index.db is missing; dry-run uses an empty snapshot and does not create it"
54
- : "index.db is missing after index preparation; the selector uses an empty snapshot",
55
- };
56
- }
57
- function describeUnavailableSnapshot(error) {
58
- return {
59
- status: "incompatible",
60
- reason: `index.db cannot provide a stable non-mutating snapshot (${error.message}); dry-run uses an empty snapshot`,
61
- };
62
46
  }
63
47
  /**
64
- * A dry-run is explicitly a non-mutating planning operation, so it may report
65
- * an unusable derived index as an empty snapshot. This is deliberately a
66
- * typed boundary mapping: readers otherwise receive no incompatible handle,
67
- * and we must not turn an arbitrary SQLite error into a successful plan by
68
- * matching its text.
48
+ * The index for candidate selection, or `undefined` when it is absent. An
49
+ * index without a readable `entries` table needs a rebuild and says so.
69
50
  */
70
- function isIncompatibleIndexError(error) {
71
- return error instanceof ConfigError && error.code === "INDEX_SCHEMA_INCOMPATIBLE";
51
+ function openEligibilityDb(readOnly) {
52
+ const db = readOnly
53
+ ? openReadonlyExistingDatabase(undefined, { isolatedSnapshot: true })
54
+ : isPathAbsent(getDbPath())
55
+ ? undefined
56
+ : openExistingDatabase();
57
+ if (db && !hasCurrentEntriesTable(db)) {
58
+ closeDatabase(db);
59
+ throw new ConfigError("index.db has no entries table this akm can read, so nothing can be selected for improvement.", "INDEX_SCHEMA_INCOMPATIBLE", "Run `akm index` to rebuild the derived index from the currently materialized sources.");
60
+ }
61
+ return db;
72
62
  }
73
- function describeIncompatibleIndexSnapshot(error) {
74
- // `INDEX_SCHEMA_INCOMPATIBLE` establishes that this is the derived-index
75
- // boundary, and the error's hint preserves whether this binary should
76
- // rebuild an older/unknown generation or upgrade for a newer one.
77
- const action = error.hint() ?? error.message;
78
- return {
79
- status: "incompatible",
80
- reason: `index.db is incompatible; ${action} Dry-run uses an empty snapshot and does not migrate it.`,
81
- };
63
+ /**
64
+ * How a dry run describes an index it could not read. Only the typed
65
+ * failures map to an empty plan; any other error still throws.
66
+ */
67
+ function unreadableSnapshot(readOnly, error) {
68
+ if (!readOnly)
69
+ return undefined;
70
+ if (error instanceof SqliteReadSnapshotUnavailableError) {
71
+ return {
72
+ status: "incompatible",
73
+ reason: `index.db cannot provide a stable non-mutating snapshot (${error.message}); dry-run uses an empty snapshot`,
74
+ };
75
+ }
76
+ if (error instanceof ConfigError && error.code === "INDEX_SCHEMA_INCOMPATIBLE") {
77
+ return {
78
+ status: "incompatible",
79
+ reason: `index.db is incompatible; ${error.hint() ?? error.message} Dry-run uses an empty snapshot and does not migrate it.`,
80
+ };
81
+ }
82
+ return undefined;
82
83
  }
83
84
  export function resolveImproveScope(scope) {
84
85
  const trimmed = scope?.trim();
@@ -89,80 +90,68 @@ export function resolveImproveScope(scope) {
89
90
  return { mode: "ref", value: trimmed };
90
91
  }
91
92
  catch (err) {
92
- // A bare word with no ref separator is a `--scope <type>` filter attempt,
93
- // not a ref. Any such word is accepted, including foreign/unknown type strings,
94
- // which simply match zero entries downstream (a read-only query filter,
95
- // not a data-acceptance gate, so no deny-list is needed here: an
96
- // unrecognized type just matches nothing, since nothing is ever indexed
97
- // with it). No test pinned the old "Unknown asset type" rejection for
98
- // this case (chunk-1.5 anchors §A.5 — 0 hits).
99
- //
100
- // A colon-shaped value that still fails `parseRefInput` is a genuinely
101
- // malformed ref attempt (bad name, path traversal, or a deny-listed
102
- // deliberately-removed type like tool/vault, D1.5-6) — that must still
103
- // surface as a real error, not be silently absorbed into a type filter
104
- // that will never match anything.
105
- if (!/[/:#]/.test(trimmed)) {
93
+ // A bare word is a `--scope <type>` filter (an unknown type matches
94
+ // nothing); a ref-shaped value that fails to parse is a real error.
95
+ if (!/[/:#]/.test(trimmed))
106
96
  return { mode: "type", value: trimmed };
107
- }
108
97
  const message = err instanceof Error ? err.message : String(err);
109
98
  throw new UsageError(`Invalid --scope "${trimmed}": ${message}`, "INVALID_FLAG_VALUE");
110
99
  }
111
100
  }
112
- /**
113
- * Dedupe eligible refs by their durable item ref when available, preserving
114
- * the first candidate and its display ref. Used to
115
- * merge the eligibility sources (feedback-signal, Layer-2 proactive-maintenance,
116
- * high-salience) without admitting a ref into the loop twice.
117
- */
101
+ /** Dedupe by durable identity (item_ref, else ref), keeping the first. */
118
102
  export function dedupeRefs(refs) {
119
103
  const seen = new Set();
120
- const out = [];
121
- for (const r of refs) {
104
+ return refs.filter((r) => {
122
105
  const key = r.itemRef ?? r.ref;
123
106
  if (seen.has(key))
124
- continue;
107
+ return false;
125
108
  seen.add(key);
126
- out.push(r);
127
- }
128
- return out;
109
+ return true;
110
+ });
129
111
  }
130
112
  export async function collectEligibleRefs(scope, stashDir, improveProfile, config) {
131
113
  return collectEligibleRefsFromIndex(scope, stashDir, improveProfile, false, config);
132
114
  }
133
- /** Dry-run planner path: query an existing index without creating or mutating it. */
115
+ /** Dry-run selector: query an existing index without creating or mutating it. */
134
116
  export async function collectEligibleRefsReadOnly(scope, stashDir, improveProfile, config) {
135
117
  return collectEligibleRefsFromIndex(scope, stashDir, improveProfile, true, config);
136
118
  }
137
119
  async function collectEligibleRefsFromIndex(scope, stashDir, improveProfile, readOnly, configOverride) {
120
+ const empty = (indexSnapshot) => ({
121
+ plannedRefs: [],
122
+ memorySummary: { eligible: 0, derived: 0 },
123
+ strategyFilteredRefs: [],
124
+ ...(indexSnapshot ? { indexSnapshot } : {}),
125
+ });
138
126
  const config = configOverride ?? loadConfig();
139
127
  let sources;
140
128
  try {
141
129
  sources = resolveSourceEntries(stashDir, config);
142
130
  }
143
131
  catch {
144
- return { plannedRefs: [], memorySummary: { eligible: 0, derived: 0 }, strategyFilteredRefs: [] };
145
- }
146
- if (sources.length === 0) {
147
- return { plannedRefs: [], memorySummary: { eligible: 0, derived: 0 }, strategyFilteredRefs: [] };
132
+ return empty();
148
133
  }
134
+ if (sources.length === 0)
135
+ return empty();
149
136
  const installations = deriveInstallations(sources);
150
137
  const writableBundleIds = deriveWritableBundleIds(sources);
151
- if (scope.mode === "ref" && scope.value) {
152
- let db;
153
- try {
154
- db = openEligibilityDb(readOnly);
155
- if (!db) {
156
- return {
157
- plannedRefs: [],
158
- memorySummary: { eligible: 0, derived: 0 },
159
- strategyFilteredRefs: [],
160
- indexSnapshot: describeIndexSnapshot(readOnly, "missing"),
161
- };
162
- }
163
- const indexSnapshot = describeIndexSnapshot(readOnly, "ready");
164
- const entries = getAllEntries(db);
165
- const entriesByItemRef = new Map(entries.map((entry) => [entry.itemRef, entry]));
138
+ const ready = {
139
+ status: "ready",
140
+ reason: readOnly ? "loaded a non-mutating point-in-time copy of the existing index" : "loaded the prepared index",
141
+ };
142
+ const missing = {
143
+ status: "missing",
144
+ reason: readOnly
145
+ ? "index.db is missing; dry-run uses an empty snapshot and does not create it"
146
+ : "index.db is missing after index preparation; the selector uses an empty snapshot",
147
+ };
148
+ let db;
149
+ try {
150
+ db = openEligibilityDb(readOnly);
151
+ if (!db)
152
+ return empty(missing);
153
+ if (scope.mode === "ref" && scope.value) {
154
+ const entriesByItemRef = new Map(getAllEntries(db).map((entry) => [entry.itemRef, entry]));
166
155
  const resolved = resolveRef(scope.value, {
167
156
  defaultBundle: config.defaultBundle,
168
157
  bundles: installations.map((installation) => ({
@@ -172,77 +161,31 @@ async function collectEligibleRefsFromIndex(scope, stashDir, improveProfile, rea
172
161
  });
173
162
  const indexed = entriesByItemRef.get(`${resolved.bundle}//${resolved.conceptId}`);
174
163
  if (!indexed?.bundleId || !indexed.conceptId || !fs.existsSync(indexed.filePath)) {
175
- if (await findAssetFilePath(scope.value, stashDir)) {
176
- return {
177
- plannedRefs: [],
178
- memorySummary: { eligible: 0, derived: 0 },
179
- strategyFilteredRefs: [],
180
- indexSnapshot,
181
- };
182
- }
164
+ if (await findAssetFilePath(scope.value, stashDir))
165
+ return empty(ready);
183
166
  throw new NotFoundError(`Asset not found in the selected writable source: ${scope.value}`, "ASSET_NOT_FOUND");
184
167
  }
185
- if (!writableBundleIds.has(indexed.bundleId)) {
186
- return { plannedRefs: [], memorySummary: { eligible: 0, derived: 0 }, strategyFilteredRefs: [], indexSnapshot };
187
- }
168
+ if (!writableBundleIds.has(indexed.bundleId))
169
+ return empty(ready);
170
+ const isMemory = indexed.entry.type === "memory";
188
171
  return {
189
172
  plannedRefs: [
190
173
  { ref: indexed.conceptId, itemRef: indexed.itemRef, reason: "scope-ref", filePath: indexed.filePath },
191
174
  ],
192
175
  memorySummary: {
193
- eligible: indexed.entry.type === "memory" ? 1 : 0,
194
- derived: indexed.entry.type === "memory" && indexed.entry.name.endsWith(".derived") ? 1 : 0,
176
+ eligible: isMemory ? 1 : 0,
177
+ derived: isMemory && indexed.entry.name.endsWith(".derived") ? 1 : 0,
195
178
  },
196
179
  strategyFilteredRefs: [],
197
- indexSnapshot,
198
- };
199
- }
200
- catch (error) {
201
- if (readOnly && error instanceof SqliteReadSnapshotUnavailableError) {
202
- return {
203
- plannedRefs: [],
204
- memorySummary: { eligible: 0, derived: 0 },
205
- strategyFilteredRefs: [],
206
- indexSnapshot: describeUnavailableSnapshot(error),
207
- };
208
- }
209
- if (readOnly && isIncompatibleIndexError(error)) {
210
- return {
211
- plannedRefs: [],
212
- memorySummary: { eligible: 0, derived: 0 },
213
- strategyFilteredRefs: [],
214
- indexSnapshot: describeIncompatibleIndexSnapshot(error),
215
- };
216
- }
217
- throw error;
218
- }
219
- finally {
220
- if (db)
221
- closeDatabase(db);
222
- }
223
- }
224
- let db;
225
- try {
226
- db = openEligibilityDb(readOnly);
227
- if (!db) {
228
- return {
229
- plannedRefs: [],
230
- memorySummary: { eligible: 0, derived: 0 },
231
- strategyFilteredRefs: [],
232
- indexSnapshot: describeIndexSnapshot(readOnly, "missing"),
180
+ indexSnapshot: ready,
233
181
  };
234
182
  }
235
- const indexSnapshot = describeIndexSnapshot(readOnly, "ready");
236
183
  const entries = getAllEntries(db, scope.mode === "type" ? scope.value : undefined).filter((indexed) => writableBundleIds.has(indexed.bundleId));
237
184
  const planned = new Map();
238
185
  const strategyFiltered = new Map();
239
186
  let memoryEligible = 0;
240
187
  let memoryDerived = 0;
241
188
  for (const indexed of entries) {
242
- // Chunk-8 WI-8.5c: the candidate `ref` is the SHORT conceptId
243
- // (`<stash-subdir>/<name>`, D-R2) — it now matches the disk lookup,
244
- // xrefs, and `displayRef` output spelling. `.itemRef` below stays the
245
- // fully-qualified durable key.
246
189
  const ref = conceptIdFromTypeName(indexed.entry.type, indexed.entry.name);
247
190
  try {
248
191
  parseRefInput(ref);
@@ -252,40 +195,16 @@ async function collectEligibleRefsFromIndex(scope, stashDir, improveProfile, rea
252
195
  continue;
253
196
  throw error;
254
197
  }
255
- // Derive the durable item_ref from indexed provenance without another query.
256
- // A provenance-free row uses the short conceptId ref for improve state.
257
- const itemRef = indexed.itemRef;
258
198
  const isDerived = indexed.entry.name.endsWith(".derived");
259
- // `.derived` memories are LLM-inferred and intentionally skip reflect
260
- // (see the synthetic `derived-memory-reflect-skipped` branch in the
261
- // improve loop). Enqueueing them here just produced one synthetic skip
262
- // per derived memory per hour with no real work — pure churn observed
263
- // 2026-05-21: 11 derived refs re-planned every hour during idle periods.
264
- // The cleanup phase (analyzeMemoryCleanup) inspects derived memories
265
- // independently of `plannedRefs`, so dropping them here loses nothing.
199
+ // `.derived` memories skip reflect; cleanup inspects them on its own.
266
200
  if (!isDerived && !planned.has(ref) && !strategyFiltered.has(ref)) {
267
- // 2026-05-27: extend the .derived precedent to strategy-incompatible
268
- // refs. If every per-ref pass (reflect + distill) on the active
269
- // strategy would refuse this ref, drop it from `plannedRefs`. The
270
- // caller emits `improve_skipped { reason: strategy_filtered_all_passes }`
271
- // once `eventsCtx` is available so the audit trail is preserved in a
272
- // single event per ref instead of 2× synthetic actions per run.
273
- // Background: see /tmp/akm-health-investigations/planner-profile-metrics-deep-analysis.md
201
+ const candidate = { ref, filePath: indexed.filePath, itemRef: indexed.itemRef };
274
202
  if (improveProfile && isStrategyFilteredForAllPasses(ref, improveProfile)) {
275
- strategyFiltered.set(ref, {
276
- ref,
277
- reason: "strategy_filtered_all_passes",
278
- filePath: indexed.filePath,
279
- itemRef,
280
- });
203
+ strategyFiltered.set(ref, { ...candidate, reason: "strategy_filtered_all_passes" });
281
204
  }
282
205
  else {
283
- planned.set(ref, {
284
- ref,
285
- reason: scope.mode === "type" ? "scope-type" : indexed.entry.type === "memory" ? "memory-cleanup" : "scope-type",
286
- filePath: indexed.filePath,
287
- itemRef,
288
- });
206
+ const reason = scope.mode !== "type" && indexed.entry.type === "memory" ? "memory-cleanup" : "scope-type";
207
+ planned.set(ref, { ...candidate, reason });
289
208
  }
290
209
  }
291
210
  if (indexed.entry.type === "memory") {
@@ -298,28 +217,14 @@ async function collectEligibleRefsFromIndex(scope, stashDir, improveProfile, rea
298
217
  plannedRefs: [...planned.values()],
299
218
  memorySummary: { eligible: memoryEligible, derived: memoryDerived },
300
219
  strategyFilteredRefs: [...strategyFiltered.values()],
301
- indexSnapshot,
220
+ indexSnapshot: ready,
302
221
  };
303
222
  }
304
223
  catch (error) {
305
- // Empty-stash setup paths can open index.db before its schema exists.
306
224
  rethrowIfTestIsolationError(error);
307
- if (readOnly && error instanceof SqliteReadSnapshotUnavailableError) {
308
- return {
309
- plannedRefs: [],
310
- memorySummary: { eligible: 0, derived: 0 },
311
- strategyFilteredRefs: [],
312
- indexSnapshot: describeUnavailableSnapshot(error),
313
- };
314
- }
315
- if (readOnly && isIncompatibleIndexError(error)) {
316
- return {
317
- plannedRefs: [],
318
- memorySummary: { eligible: 0, derived: 0 },
319
- strategyFilteredRefs: [],
320
- indexSnapshot: describeIncompatibleIndexSnapshot(error),
321
- };
322
- }
225
+ const snapshot = unreadableSnapshot(readOnly, error);
226
+ if (snapshot)
227
+ return empty(snapshot);
323
228
  throw error;
324
229
  }
325
230
  finally {
@@ -327,30 +232,20 @@ async function collectEligibleRefsFromIndex(scope, stashDir, improveProfile, rea
327
232
  closeDatabase(db);
328
233
  }
329
234
  }
235
+ /** The parent memory a `--scope <memory ref>` cleanup is restricted to. */
330
236
  export function memoryCleanupParentRef(scope, stashDir) {
331
237
  if (scope.mode !== "ref" || !scope.value)
332
238
  return undefined;
333
239
  const parsed = parseRefInput(scope.value);
334
240
  if (parsed.type !== "memory")
335
241
  return undefined;
336
- // Non-derived parent scope: emit the canonical `memories/<name>` conceptId so
337
- // it matches `resolveParentRef`'s output in analyzeMemoryCleanup's parentRef
338
- // filter (Group-C item 2 — the reader and every comparison site flipped
339
- // together; emitting the raw scope value would re-open the mismatch the
340
- // chunk-8 history warns about).
341
242
  if (!parsed.name.endsWith(".derived"))
342
243
  return conceptIdFromTypeName(parsed.type, parsed.name);
343
- const sources = resolveSourceEntries(stashDir);
344
- for (const source of sources) {
244
+ for (const source of resolveSourceEntries(stashDir)) {
345
245
  const candidate = path.join(source.path, "memories", `${parsed.name}.md`);
346
246
  if (!fs.existsSync(candidate))
347
247
  continue;
348
- const raw = fs.readFileSync(candidate, "utf8");
349
- const fm = parseFrontmatter(raw).data;
350
- // The `source:` backref is normalized to the `memories/<name>` conceptId.
351
- // That is exactly what
352
- // analyzeMemoryCleanup's parentRef filter compares against — resolveParentRef
353
- // emits the same conceptId — so the two sites stay in lockstep.
248
+ const fm = parseFrontmatter(fs.readFileSync(candidate, "utf8")).data;
354
249
  const parent = parseMemoryRef(typeof fm.source === "string" ? fm.source : undefined);
355
250
  if (parent)
356
251
  return parent;
@@ -358,179 +253,29 @@ export function memoryCleanupParentRef(scope, stashDir) {
358
253
  return conceptIdFromTypeName("memory", parsed.name.slice(0, -".derived".length));
359
254
  }
360
255
  export function isLessonCandidate(ref) {
361
- // Only lesson assets need lesson-schema validation (description + when_to_use).
362
- // Memories have their own distill path via shouldDistillMemoryRef.
363
- // All other types go through reflect, not distill.
364
256
  return parseRefInput(ref).type === "lesson";
365
257
  }
366
- /**
367
- * Planner-side check: should this ref enter the distill queue?
368
- *
369
- * Distill produces lessons from non-lesson sources. Two cases are eligible:
370
- *
371
- * 1. Memory refs that pass {@link shouldDistillMemoryRef} (the existing
372
- * memory→lesson/knowledge promotion path).
373
- *
374
- * Refs whose `type` is in {@link DISTILL_REFUSED_INPUT_TYPES} (currently
375
- * `lesson:*`) are explicitly excluded — distill refuses them at runtime and
376
- * queuing them just produces a no-op `skipped` outcome per ref per hour. That
377
- * planner waste was the bug fixed in commit
378
- * fix(improve): drop distill-refused types from planner.
379
- *
380
- * Note: prior to this fix the gate used `isLessonCandidate(ref)` directly,
381
- * which was true *only* for `lesson:*` refs — exactly the set distill refuses.
382
- * The result: every hourly run re-queued the same lesson refs, the same skip
383
- * message returned, and no work was ever done. See
384
- * `tests/integration/commands/improve-distill-planner-skip-lessons.test.ts`.
385
- */
258
+ /** Should this ref enter the distill queue? Types distill refuses never do. */
386
259
  export function isDistillCandidateRef(ref, stashDir) {
387
- const parsed = parseRefInput(ref);
388
- if (isDistillRefusedInputType(parsed.type))
260
+ if (isDistillRefusedInputType(parseRefInput(ref).type))
389
261
  return false;
390
262
  return shouldDistillMemoryRef(ref, stashDir);
391
263
  }
264
+ /** A non-derived memory that is not marked `quality: proposed`. */
392
265
  export function shouldDistillMemoryRef(ref, stashDir) {
393
266
  const parsed = parseRefInput(ref);
394
267
  if (parsed.type !== "memory")
395
268
  return false;
396
- const sources = resolveSourceEntries(stashDir);
397
- for (const source of sources) {
269
+ for (const source of resolveSourceEntries(stashDir)) {
398
270
  const candidate = `${source.path}/memories/${parsed.name}.md`;
399
271
  if (!fs.existsSync(candidate))
400
272
  continue;
401
- const raw = fs.readFileSync(candidate, "utf8");
402
- const fm = parseFrontmatter(raw).data;
403
- const quality = typeof fm.quality === "string" ? fm.quality : undefined;
404
- if (quality === "proposed")
273
+ if (parseFrontmatter(fs.readFileSync(candidate, "utf8")).data.quality === "proposed")
405
274
  return false;
406
- return !parsed.name.endsWith(".derived");
275
+ break;
407
276
  }
408
277
  return !parsed.name.endsWith(".derived");
409
278
  }
410
- // ── Signal-delta eligibility helpers (0.8.0) ────────────────────────────────
411
- //
412
- // The 0.8.0 redesign replaced flat time-based cooldowns for reflect/distill
413
- // with a *signal-delta* gate: a ref is re-eligible iff new feedback has
414
- // landed since the last proposal was generated for it. These helpers build
415
- // the two timestamp maps the gate needs in bulk, so the planner avoids
416
- // N+1 queries across the full postCleanupRefs set.
417
- /**
418
- * Latest feedback event timestamp per ref in the active window. Reads all
419
- * `feedback` events newer than `sinceIso` in one query and indexes by ref,
420
- * keeping the maximum `ts` per ref.
421
- *
422
- * Only events with a meaningful payload count as "signal" — `metadata.signal`
423
- * (positive/negative) OR `metadata.note` (a free-form annotation). Empty
424
- * metadata events are ignored so a stray `akm feedback <ref>` invocation
425
- * without a flag doesn't trigger downstream re-processing.
426
- */
427
- export function buildLatestFeedbackTsMap(refs, sinceIso, itemRefByRef, eventsCtx) {
428
- const out = new Map();
429
- if (refs.length === 0)
430
- return out;
431
- // Correlate item_ref-keyed events and conceptId-keyed direct invocations.
432
- const refByDurableKey = new Map(refs.flatMap((ref) => improveStateReadRefs(ref, itemRefByRef?.get(ref)).map((key) => [key, ref])));
433
- const { events } = readEvents({ type: "feedback", since: sinceIso }, eventsCtx);
434
- for (const e of events) {
435
- const ref = e.ref ? refByDurableKey.get(e.ref) : undefined;
436
- if (!ref)
437
- continue;
438
- const meta = e.metadata;
439
- const hasSignal = meta !== undefined && (typeof meta.signal === "string" || typeof meta.note === "string");
440
- if (!hasSignal)
441
- continue;
442
- const ts = e.ts ?? "";
443
- if (ts > (out.get(ref) ?? ""))
444
- out.set(ref, ts);
445
- }
446
- return out;
447
- }
448
- /**
449
- * Latest proposal timestamp per input-ref, filtered by source ('reflect' or
450
- * 'distill'). Reads the corresponding `*_invoked` events from state.db —
451
- * these events are emitted at proposal creation time and carry the *input*
452
- * asset ref (`memories/foo`, `skills/bar`, etc.) directly. We use them rather than
453
- * `listProposals` because distill proposals are keyed by the derived
454
- * lesson/knowledge ref, not the source memory — joining back through the
455
- * payload would be fragile.
456
- */
457
- export function buildLatestProposalTsMap(refs, source, itemRefByRef, eventsCtx) {
458
- const out = new Map();
459
- if (refs.length === 0)
460
- return out;
461
- // Correlate item_ref-keyed events and conceptId-keyed direct invocations.
462
- const refByDurableKey = new Map(refs.flatMap((ref) => improveStateReadRefs(ref, itemRefByRef?.get(ref)).map((key) => [key, ref])));
463
- const eventType = source === "reflect" ? "reflect_invoked" : "distill_invoked";
464
- const { events } = readEvents({ type: eventType }, eventsCtx);
465
- for (const e of events) {
466
- const ref = e.ref ? refByDurableKey.get(e.ref) : undefined;
467
- if (!ref)
468
- continue;
469
- // For distill_invoked we only count attempts that produced (or attempted
470
- // to produce) a real proposal — config_disabled outcomes (no LLM work was
471
- // actually invoked) and llm_failed (transport/timeout — nothing to show
472
- // for the attempt) should not move the signal-delta cursor forward.
473
- // R10: quality_rejected and review_needed now also mint a real proposal
474
- // row (see quality-gate.ts's `writeQualityRejection`), so — like queued —
475
- // a real attempt was made and the cursor must advance, or the same
476
- // already-rejected ref is re-selected and re-rejected on every run.
477
- if (eventType === "distill_invoked") {
478
- const outcome = e.metadata?.outcome;
479
- if (outcome !== "queued" &&
480
- outcome !== "skipped" &&
481
- outcome !== "validation_failed" &&
482
- outcome !== "quality_rejected" &&
483
- outcome !== "review_needed") {
484
- continue;
485
- }
486
- }
487
- const ts = e.ts ?? "";
488
- if (ts > (out.get(ref) ?? ""))
489
- out.set(ref, ts);
490
- }
491
- return out;
492
- }
493
- /**
494
- * Signal-delta eligibility predicate.
495
- *
496
- * True iff `latestFeedback[ref]` is defined AND either no prior proposal
497
- * exists for this (ref, source) OR `latestFeedback[ref] > lastProposal[ref]`.
498
- *
499
- * Refs with no feedback signal at all are ineligible by definition — the
500
- * proactive-maintenance and high-salience fallback lanes (see
501
- * `noFeedbackCandidates` later in the planner) handle never-rated assets
502
- * separately.
503
- */
504
- export function isSignalDeltaEligible(ref, latestFeedback, lastProposal) {
505
- const fb = latestFeedback.get(ref);
506
- if (!fb)
507
- return false;
508
- const lp = lastProposal.get(ref);
509
- if (!lp)
510
- return true;
511
- return fb > lp;
512
- }
513
- /**
514
- * H7 (#566): cooperative budget watchdog with a captured, RAII-cleared hard-kill.
515
- *
516
- * When the wall-clock budget expires, `onExhausted` (normally an
517
- * `AbortController.abort`) signals cooperative cancellation so the run can drain
518
- * its in-flight log/`state.db` flush and unwind naturally. A second hard-kill
519
- * timer is then armed as a watchdog: it only `exit(0)`s if the drain itself
520
- * overruns `hardKillGraceMs`, preventing the process from outliving the task
521
- * timeout window (lock-cascade fix).
522
- *
523
- * Both timers are captured; the returned dispose() clears whichever is still
524
- * pending. Callers invoke it from a `finally`, so a *clean* drain reaches the
525
- * `finally` and cancels the pending hard-kill before it can fire — the previous
526
- * detached `setTimeout(() => process.exit(0), 5000)` always fired, truncating a
527
- * clean flush. The hard-kill timer is `unref()`-ed so it never keeps the event
528
- * loop alive on its own: once the run drains it exits with its own code, not the
529
- * forced 0.
530
- *
531
- * Dependencies are injectable purely so the concurrency-sensitive timing
532
- * contract can be exercised deterministically in unit tests.
533
- */
534
279
  export function shouldAnalyzeMemoryCleanup(scope, eligibleMemories, primaryStashDir) {
535
280
  if (!primaryStashDir || eligibleMemories === 0)
536
281
  return false;
@@ -538,47 +283,28 @@ export function shouldAnalyzeMemoryCleanup(scope, eligibleMemories, primaryStash
538
283
  return true;
539
284
  if (scope.mode === "type")
540
285
  return scope.value === "memory";
541
- if (!scope.value)
542
- return false;
543
- return parseRefInput(scope.value).type === "memory";
286
+ return scope.value ? parseRefInput(scope.value).type === "memory" : false;
544
287
  }
545
288
  export function buildUtilityMap(refs, readOnly = false) {
546
289
  const map = new Map();
547
290
  if (refs.length === 0)
548
291
  return map;
549
292
  const refSet = new Set(refs.map((r) => r.ref));
550
- let db;
551
- try {
552
- db = readOnly ? openReadonlyExistingDatabase(undefined, { isolatedSnapshot: true }) : openExistingDatabase();
553
- if (!db)
554
- return map;
555
- const allDbEntries = getAllEntries(db);
293
+ withIndexDb(readOnly, (db) => {
556
294
  const idToRef = new Map();
557
- for (const indexed of allDbEntries) {
558
- // Chunk-8 WI-8.5c: correlate on the SHORT conceptId to match the
559
- // `ImproveEligibleRef.ref` set built in collectEligibleRefsFromIndex.
295
+ for (const indexed of getAllEntries(db)) {
560
296
  const ref = conceptIdFromTypeName(indexed.entry.type, indexed.entry.name);
561
297
  if (refSet.has(ref))
562
298
  idToRef.set(indexed.id, ref);
563
299
  }
564
- const ids = [...idToRef.keys()];
565
- if (ids.length > 0) {
566
- const { global: scores } = getUtilityScoresByIds(db, ids);
567
- for (const [id, score] of scores) {
568
- const ref = idToRef.get(id);
569
- if (ref)
570
- map.set(ref, score.utility);
571
- }
300
+ if (idToRef.size === 0)
301
+ return;
302
+ for (const [id, score] of getUtilityScoresByIds(db, [...idToRef.keys()])) {
303
+ const ref = idToRef.get(id);
304
+ if (ref)
305
+ map.set(ref, score.utility);
572
306
  }
573
- }
574
- catch (err) {
575
- rethrowIfTestIsolationError(err);
576
- // best-effort: if DB unavailable, all utilities default to 0
577
- }
578
- finally {
579
- if (db)
580
- closeDatabase(db);
581
- }
307
+ });
582
308
  return map;
583
309
  }
584
310
  export async function findAssetFilePath(ref, stashDir, writableBundleIds) {