akm-cli 0.9.17-alpha.2 → 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 +756 -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 +363 -679
  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 -427
  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
@@ -0,0 +1,136 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ import { withImmediateTransaction } from "./sqlite-transaction.js";
5
+ /**
6
+ * Reject a `MIGRATIONS` array containing a duplicate `id`.
7
+ *
8
+ * A compiled-in registry can't diverge at runtime, so this is a dev-time
9
+ * invariant, not a per-open guard: each consumer (state.db, logs.db) calls it
10
+ * once on its own array at module load, and `tests/storage/sqlite-migrations.test.ts`
11
+ * pins the duplicate-detection behavior directly. {@link inspectMigrationLedger}
12
+ * does NOT call this — re-scanning the same compiled-in array on every DB open
13
+ * added no safety over the module-load check, only repeated O(n) cost.
14
+ */
15
+ export function assertMigrationRegistry(migrations) {
16
+ const seen = new Set();
17
+ for (const migration of migrations) {
18
+ if (seen.has(migration.id))
19
+ throw new Error(`Migration registry contains duplicate ID ${migration.id}.`);
20
+ seen.add(migration.id);
21
+ }
22
+ }
23
+ export function migrationLedgerExists(db) {
24
+ return !!db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'schema_migrations'").get();
25
+ }
26
+ /** Inspect the database's applied IDs against the exact ordered registry prefix. */
27
+ function inspectLedgerAgainst(db, registryIds) {
28
+ if (!migrationLedgerExists(db))
29
+ return { status: registryIds.length === 0 ? "current" : "old", migrationIds: [] };
30
+ const rows = db.prepare("SELECT id FROM schema_migrations ORDER BY rowid").all();
31
+ const migrationIds = rows.map((row) => row.id);
32
+ for (const [index, row] of rows.entries()) {
33
+ const expectedId = registryIds[index];
34
+ // Every id this binary knows matched in order and the ledger carries more:
35
+ // the database was migrated by a newer akm. Nothing here is applicable —
36
+ // this binary's whole registry is already applied — so this is version
37
+ // skew, not divergence.
38
+ if (!expectedId) {
39
+ return {
40
+ status: "newer",
41
+ migrationIds,
42
+ detail: `applied migration ID${rows.length - registryIds.length === 1 ? "" : "s"} ${migrationIds
43
+ .slice(registryIds.length)
44
+ .join(", ")} unknown to this akm`,
45
+ };
46
+ }
47
+ // A mismatch at a position this binary has a migration for is divergence,
48
+ // whether or not the id is one this binary knows later: this binary's
49
+ // migration at `index` was never applied, and something else was.
50
+ if (row.id !== expectedId) {
51
+ return {
52
+ status: "inconsistent",
53
+ migrationIds,
54
+ detail: `migration ledger is not an exact ordered prefix at position ${index + 1} (found '${row.id}', expected '${expectedId}'). ` +
55
+ `Applied, in order: [${migrationIds.join(", ")}]. This akm's expected order: [${registryIds.join(", ")}].`,
56
+ };
57
+ }
58
+ }
59
+ return {
60
+ status: rows.length === registryIds.length ? "current" : "old",
61
+ migrationIds,
62
+ };
63
+ }
64
+ export function inspectMigrationLedger(db, migrations) {
65
+ return inspectLedgerAgainst(db, migrations.map((migration) => migration.id));
66
+ }
67
+ /**
68
+ * Reject only a ledger this binary cannot reason about at all.
69
+ *
70
+ * A `newer` ledger — an exact ordered prefix of this binary's registry plus
71
+ * migrations a later akm added — is NOT rejected. Two akm versions sharing one
72
+ * data directory is a supported deployment (a bundled CLI alongside a newer
73
+ * global install), and refusing the open bricked the older one for every
74
+ * command while protecting nothing: its entire registry is already applied, so
75
+ * it has no pending migration to run. Callers that want to tell an operator
76
+ * about the skew read {@link MigrationLedgerState.status}.
77
+ *
78
+ * An `inconsistent` ledger is different: this binary has a migration that was
79
+ * never applied and something else was applied in its place, so running the
80
+ * pending set could conflict with schema it cannot see. That still refuses.
81
+ */
82
+ export function assertMigrationLedger(db, migrations) {
83
+ const state = inspectMigrationLedger(db, migrations);
84
+ if (state.status === "inconsistent") {
85
+ throw new Error(`Refusing a database whose migrations are not an exact ordered prefix: ${state.detail} ` +
86
+ "Applying this binary's missing migration now could run it against a schema a later migration already " +
87
+ "changed underneath it, which is a real risk of producing a wrong schema — not something akm can guess " +
88
+ "its way out of safely. This usually means the database was migrated by an incompatible akm build or " +
89
+ "fork, or schema_migrations was edited by hand. Restore this file from a backup taken before the " +
90
+ "divergence, or — if there is no backup and the data is not needed — delete it and let akm rebuild it " +
91
+ "from scratch (a derived index.db regenerates from your sources on the next 'akm index'; state.db loses " +
92
+ "durable history such as improve/proposal state and must be treated as a last resort).");
93
+ }
94
+ return state;
95
+ }
96
+ /** Create the migrations ledger table if it does not exist. */
97
+ export function ensureMigrationsTable(db) {
98
+ db.exec(`
99
+ CREATE TABLE IF NOT EXISTS schema_migrations (
100
+ id TEXT PRIMARY KEY,
101
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
102
+ );
103
+ `);
104
+ }
105
+ /** The registry entries not yet recorded in the ledger, in order. Throws on a divergent ledger. */
106
+ export function pendingMigrations(db, migrations) {
107
+ return migrations.slice(assertMigrationLedger(db, migrations).migrationIds.length);
108
+ }
109
+ /**
110
+ * Apply every pending migration in one `BEGIN IMMEDIATE` transaction.
111
+ *
112
+ * A database with nothing pending is only read, never write-locked. Otherwise
113
+ * the write lock is taken up front — a second process bootstrapping the same
114
+ * database WAITS for the first to commit instead of racing it — and the
115
+ * pending set is re-read under that lock, so the process that lost the race
116
+ * finds nothing left to do rather than re-running DDL. Each migration's ledger
117
+ * row is inserted right after its SQL inside the same transaction: a failing
118
+ * migration rolls back every migration this call applied, and their ledger
119
+ * rows with them. Contention that outlasts every BEGIN retry surfaces as
120
+ * `TransientError("STATE_DB_CONTENDED")` (`../sqlite-transaction`). Returns the
121
+ * IDs this call applied, in order — empty when another process got there first.
122
+ */
123
+ export function runMigrations(db, migrations) {
124
+ if (pendingMigrations(db, migrations).length === 0)
125
+ return [];
126
+ return withImmediateTransaction(db, () => {
127
+ ensureMigrationsTable(db);
128
+ const applied = [];
129
+ for (const migration of pendingMigrations(db, migrations)) {
130
+ db.exec(migration.up);
131
+ db.prepare("INSERT INTO schema_migrations (id) VALUES (?)").run(migration.id);
132
+ applied.push(migration.id);
133
+ }
134
+ return applied;
135
+ });
136
+ }
@@ -95,17 +95,19 @@ export function isNetworkFilesystem(fsType) {
95
95
  return false;
96
96
  return NETWORK_FS_MAGICS.has(fsType);
97
97
  }
98
- /** Options for {@link applyStandardPragmas}. */
98
+ /** How long a statement waits for a lock before failing with SQLITE_BUSY. */
99
+ export const SQLITE_BUSY_TIMEOUT_MS = 30_000;
99
100
  /**
100
- * How long a statement waits for a lock before failing with SQLITE_BUSY.
101
- *
102
- * Exported so read-only openers can apply it too. They cannot run the rest of
103
- * the standard set (journal_mode and foreign_keys are write operations), but
104
- * the default of 0 makes reads fail INSTANTLY under writer contention — which
105
- * matters in the DELETE/TRUNCATE journal modes 0.9.1's network-filesystem
106
- * fallback and `AKM_SQLITE_JOURNAL_MODE` can select, where readers do block.
101
+ * The standard set for a READ-ONLY handle: just `busy_timeout`. A read-only
102
+ * connection cannot run the rest (journal_mode and foreign_keys are write
103
+ * operations), but SQLite's default timeout of 0 makes reads fail INSTANTLY
104
+ * under writer contention — which matters in the DELETE/TRUNCATE journal modes
105
+ * the network-filesystem fallback and `AKM_SQLITE_JOURNAL_MODE` can select,
106
+ * where readers do block.
107
107
  */
108
- export const SQLITE_BUSY_TIMEOUT_MS = 30_000;
108
+ export function applyReadonlyPragmas(db) {
109
+ db.exec(`PRAGMA busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS}`);
110
+ }
109
111
  /**
110
112
  * Apply AKM's standard opening PRAGMAs to `db`, in order:
111
113
  * 1. `journal_mode` = the configured mode (with WAL→DELETE network-FS fallback)
@@ -0,0 +1,170 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * `BEGIN IMMEDIATE` transactions and the one SQLite contention classifier.
6
+ *
7
+ * `db.transaction()` is DEFERRED by default on both Bun and better-sqlite3,
8
+ * which means two writers can both perform stale preflight reads and only race
9
+ * when they finally attempt the write. Proposal creation, queue mutation and
10
+ * schema migration need the write lock BEFORE those reads so concurrent
11
+ * processes serialize on the live state rather than clobbering each other.
12
+ *
13
+ * Every open already carries a 30 s `busy_timeout` (`sqlite-pragmas.ts`), so a
14
+ * single blocked statement waits before failing. {@link beginImmediateTransaction}
15
+ * additionally retries `BEGIN IMMEDIATE` itself ({@link WITH_IMMEDIATE_TX_MAX_ATTEMPTS}
16
+ * attempts) for the rarer case of two writers racing the BEGIN statement
17
+ * back-to-back. If every attempt is still contention-shaped
18
+ * ({@link isSqliteContentionError} — SQLITE_BUSY/LOCKED, the matching message
19
+ * text, or the phantom-BEGIN marker), the exhaustion throw is reclassified
20
+ * into `TransientError("STATE_DB_CONTENDED")` (exit 75, #948 addendum) instead
21
+ * of surfacing the raw driver text: another akm process (an unrelated
22
+ * `improve`, `workflow run`, or task run — not necessarily contending for the
23
+ * same row) is writing the database right now. A genuinely unrelated error
24
+ * (real corruption, a body-thrown failure) is never reclassified and rethrows
25
+ * exactly as raised.
26
+ *
27
+ * This is the single place SQLITE_BUSY/LOCKED becomes exit 75 for state.db:
28
+ * `openStateDatabase`'s migration transaction, every repository write and the
29
+ * shared migration runner (`engines/sqlite-migrations.ts`) all go through it.
30
+ * `core/state-db.ts` re-exports these helpers for its callers.
31
+ *
32
+ * @module storage/sqlite-transaction
33
+ */
34
+ import { TransientError } from "../core/errors.js";
35
+ import { sleepSync } from "../runtime.js";
36
+ /**
37
+ * Whether `err` is one of the SQLite conditions a concurrent-writer race can
38
+ * throw that are transient — the statement did NOT corrupt anything, another
39
+ * writer just holds the lock right now — and therefore safe to retry or
40
+ * reclassify as ordinary contention rather than a genuine failure:
41
+ * - `SQLITE_BUSY` / `SQLITE_LOCKED` (either driver's `.code`).
42
+ * - "database is locked" / "database table is locked" message text.
43
+ * - the phantom-BEGIN marker synthesized below when `BEGIN IMMEDIATE`
44
+ * returns without actually opening a transaction.
45
+ *
46
+ * This is the single shared classifier for "is this ordinary contention"
47
+ * (#948): `reclassifyIndexDbContention` (indexer) and the workflow-runs
48
+ * repository's lease-contention classifier (which additionally matches a
49
+ * couple of corruption-shaped texts specific to its own narrower cross-process
50
+ * race and keeps its own live-lease confirmation before reclassifying) both
51
+ * delegate to it. Matching this set alone is never sufficient to declare
52
+ * something DEFINITELY contention when a caller needs corroborating evidence
53
+ * (see the lease path); for `beginImmediateTransaction`'s own exhaustion case
54
+ * there is no such evidence available, so the five 30 s `busy_timeout` waits
55
+ * already spent stand as the evidence instead.
56
+ */
57
+ export function isSqliteContentionError(err) {
58
+ const code = err?.code;
59
+ if (code === "SQLITE_BUSY" || code === "SQLITE_LOCKED")
60
+ return true;
61
+ const msg = (err instanceof Error ? err.message : String(err)).toLowerCase();
62
+ return (msg.includes("database is locked") ||
63
+ msg.includes("database table is locked") ||
64
+ // Phantom BEGIN (see below) — synthesized when BEGIN IMMEDIATE returns
65
+ // without opening a transaction. Safe to retry: fn() has not run.
66
+ msg.includes("did not open a transaction"));
67
+ }
68
+ const WITH_IMMEDIATE_TX_MAX_ATTEMPTS = 5;
69
+ /**
70
+ * Reclassify an exhausted-retry BEGIN failure that is still contention-shaped
71
+ * (#948) into a `TransientError("STATE_DB_CONTENDED")`, the same class as a
72
+ * held workflow run lock (`RUN_LEASE_HELD`): the driver text is accurate but unhelpful (`{"ok":false,"error":"database is
73
+ * locked"}`, exit 70/INTERNAL) — this instead reads as a retryable-shortly
74
+ * signal (exit 75, sysexits EX_TEMPFAIL) with the original error preserved as
75
+ * `cause` for `--verbose`/debugging. A genuinely unrelated error (not
76
+ * contention-shaped) is rethrown exactly as raised, never reclassified.
77
+ */
78
+ function throwBeginFailure(err) {
79
+ if (isSqliteContentionError(err)) {
80
+ const contended = new TransientError("akm's state database is busy (another akm process is writing it); retry shortly.", "STATE_DB_CONTENDED");
81
+ contended.cause = err;
82
+ throw contended;
83
+ }
84
+ throw err;
85
+ }
86
+ /**
87
+ * Open, but deliberately do not finish, an immediate transaction.
88
+ *
89
+ * This is the split-phase counterpart to {@link withImmediateTransaction} for
90
+ * the source-update coordinator: index finalization must mutate state.db in a
91
+ * transaction that remains pending until content, lockfile, and index
92
+ * publication have all succeeded. The caller that asked for this split phase
93
+ * owns the matching COMMIT/ROLLBACK.
94
+ *
95
+ * Only a contention-shaped BEGIN failure is retried. "cannot start a
96
+ * transaction within a transaction" is deliberately NOT: it means a
97
+ * transaction is already open on this connection (a re-entrant call — handled
98
+ * by the entry guard in {@link withImmediateTransaction}), and "retrying" it
99
+ * with a ROLLBACK would destroy the caller's transaction (issue #686).
100
+ */
101
+ export function beginImmediateTransaction(db) {
102
+ if (db.inTransaction) {
103
+ throw new Error("beginImmediateTransaction requires a connection with no active transaction");
104
+ }
105
+ let lastBeginErr;
106
+ for (let attempt = 1; attempt <= WITH_IMMEDIATE_TX_MAX_ATTEMPTS; attempt++) {
107
+ try {
108
+ db.exec("BEGIN IMMEDIATE");
109
+ if (!db.inTransaction) {
110
+ throw new Error("BEGIN IMMEDIATE did not open a transaction (phantom contention state)");
111
+ }
112
+ return;
113
+ }
114
+ catch (err) {
115
+ lastBeginErr = err;
116
+ if (isSqliteContentionError(err) && attempt < WITH_IMMEDIATE_TX_MAX_ATTEMPTS) {
117
+ if (db.inTransaction) {
118
+ try {
119
+ db.exec("ROLLBACK");
120
+ }
121
+ catch {
122
+ // Transaction already gone — safe to retry BEGIN.
123
+ }
124
+ }
125
+ sleepSync(2 ** (attempt - 1));
126
+ continue;
127
+ }
128
+ throwBeginFailure(err);
129
+ }
130
+ }
131
+ throwBeginFailure(lastBeginErr);
132
+ }
133
+ /** Run `fn` inside a `BEGIN IMMEDIATE` transaction, joining one that is already open on `db`. */
134
+ export function withImmediateTransaction(db, fn) {
135
+ // Re-entrancy guard (issue #686): if a transaction is already open on this
136
+ // connection (e.g. a nested withImmediateTransaction call inside an outer
137
+ // frame's fn), join it — run fn directly with no BEGIN/COMMIT/ROLLBACK of
138
+ // our own. Without this, the nested BEGIN throws "cannot start a transaction
139
+ // within a transaction", which the old retry path answered with an
140
+ // unconditional ROLLBACK — destroying the OUTER transaction and leaving its
141
+ // COMMIT to fail with "cannot commit - no transaction is active".
142
+ if (db.inTransaction) {
143
+ return fn();
144
+ }
145
+ beginImmediateTransaction(db);
146
+ try {
147
+ const result = fn();
148
+ if (!db.inTransaction) {
149
+ // The transaction we opened vanished while fn() ran (e.g. an
150
+ // auto-rollback or a stray ROLLBACK inside fn). fn's writes may have
151
+ // escaped serialization, so retrying is unsafe — fail loudly instead of
152
+ // letting COMMIT throw the opaque "cannot commit - no transaction is
153
+ // active" SQLiteError.
154
+ throw new Error("withImmediateTransaction invariant violated: transaction opened by BEGIN IMMEDIATE was no longer active after the transaction body ran; refusing to COMMIT (writes may have escaped serialization)");
155
+ }
156
+ db.exec("COMMIT");
157
+ return result;
158
+ }
159
+ catch (err) {
160
+ if (db.inTransaction) {
161
+ try {
162
+ db.exec("ROLLBACK");
163
+ }
164
+ catch {
165
+ // Ignore rollback failures so the original error is preserved.
166
+ }
167
+ }
168
+ throw err;
169
+ }
170
+ }
@@ -2,42 +2,49 @@
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
4
  /**
5
- * state.db integrity + reclaimable-space probes (R0, tier0-0917).
5
+ * state.db integrity + reclaimable-space probes (R0), and the VACUUM pass
6
+ * state.db and index.db share.
6
7
  *
7
8
  * `akm health`'s `state-db-integrity` check (src/commands/health/checks.ts)
8
9
  * is a pure projection like every other check, so the actual IO lives here:
9
10
  * a read-only `PRAGMA quick_check` and a read-only freelist/page-count read.
10
- * Both open their own short-lived connection via the plain {@link openDatabase}
11
- * opener — deliberately bypassing `openStateDatabase`'s managed-open/migration
12
- * machinery (src/core/state-db.ts), since a corrupt database must not need a
13
- * clean migration-ledger read just to report itself as corrupt.
11
+ * Both open their own short-lived read-only connection via the plain
12
+ * {@link openDatabase} opener rather than `openStateDatabase`
13
+ * (src/core/state-db.ts), since a corrupt database must not need a clean
14
+ * migration-ledger read, let alone a migration, just to report itself as
15
+ * corrupt.
14
16
  *
15
- * {@link vacuumStateDbIfReclaimable} is the post-purge VACUUM step: given an
16
- * already-open read-write connection (VACUUM cannot run inside a transaction,
17
- * and a read-only handle cannot run it at all) and a freelist reading, it
18
- * VACUUMs only when the freelist ratio crosses {@link STATE_DB_FREELIST_WARN_RATIO}
19
- * and never throws — a locked/busy database is reported, not raised.
17
+ * {@link vacuumIfReclaimable} is the VACUUM step: given an already-open
18
+ * read-write connection (VACUUM cannot run inside a transaction, and a
19
+ * read-only handle cannot run it at all) and a freelist reading, it VACUUMs
20
+ * when asked to or when the freelist ratio crosses
21
+ * {@link STATE_DB_FREELIST_WARN_RATIO}, and never throws — a locked/busy
22
+ * database is reported, not raised. improve's post-purge pass runs it on
23
+ * state.db, and `akm index` on index.db.
20
24
  *
21
25
  * @module storage/state-db-integrity
22
26
  */
23
27
  import { appendEvent } from "../core/events.js";
24
28
  import { openDatabase } from "./database.js";
25
- import { SQLITE_BUSY_TIMEOUT_MS } from "./sqlite-pragmas.js";
29
+ import { applyReadonlyPragmas } from "./sqlite-pragmas.js";
26
30
  /** How many corruption errors `PRAGMA quick_check` collects before it stops scanning and returns. */
27
31
  const QUICK_CHECK_ERROR_LIMIT = 10;
28
- /** Above this fraction of free pages, `state-db-integrity` warns and a post-purge pass VACUUMs. */
32
+ /**
33
+ * Above this fraction of free pages, `state-db-integrity` warns, and
34
+ * {@link vacuumIfReclaimable} compacts state.db (after improve's retention
35
+ * purge) or index.db (at the end of `akm index`).
36
+ */
29
37
  export const STATE_DB_FREELIST_WARN_RATIO = 0.5;
30
- /** Event appended by {@link vacuumStateDbIfReclaimable} after a successful VACUUM. */
38
+ /** Event appended by {@link vacuumIfReclaimable} after a successful VACUUM of state.db. */
31
39
  export const STATE_DB_VACUUMED_EVENT = "state_db_vacuumed";
40
+ /** Event appended by {@link vacuumIfReclaimable} after a successful VACUUM of index.db. */
41
+ export const INDEX_DB_VACUUMED_EVENT = "index_db_vacuumed";
32
42
  function firstColumn(row) {
33
43
  return row === undefined ? undefined : Object.values(row)[0];
34
44
  }
35
45
  function openReadonlyStateDb(dbPath) {
36
46
  const db = openDatabase(dbPath, { readonly: true, create: false });
37
- // Read-only handles cannot run journal_mode/foreign_keys (write operations),
38
- // but busy_timeout is legal — see openReadonlyExistingDatabase's identical
39
- // rationale in src/storage/repositories/index-connection.ts.
40
- db.exec(`PRAGMA busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS}`);
47
+ applyReadonlyPragmas(db);
41
48
  return db;
42
49
  }
43
50
  /**
@@ -89,21 +96,21 @@ export function getStateDbFreelistInfo(dbPath) {
89
96
  }
90
97
  }
91
98
  /**
92
- * VACUUM `db` when `freelist.ratio` exceeds {@link STATE_DB_FREELIST_WARN_RATIO},
93
- * appending a {@link STATE_DB_VACUUMED_EVENT} recording pages before/after.
94
- * Intended to run immediately after the retention purge, on the same
95
- * read-write connection the purge just used. Never throws: a locked/busy
96
- * database (another writer holds the file right now) is reported via
97
- * `reason: "busy"` rather than raised, since this is opportunistic
98
- * maintenance and must not fail the purge pass it follows.
99
+ * VACUUM `db` when `vacuum.force` is set or `freelist.ratio` exceeds
100
+ * {@link STATE_DB_FREELIST_WARN_RATIO}, appending `vacuum.eventType` with
101
+ * the pages before/after. Runs on the caller's read-write connection — for
102
+ * state.db right after the retention purge, for index.db at the end of an
103
+ * index run. Never throws: a locked/busy database (another writer holds the
104
+ * file right now) is reported via `reason: "busy"` rather than raised, since
105
+ * this is opportunistic maintenance and must not fail the pass it follows.
99
106
  *
100
107
  * The event is appended via `appendEvent` (not a direct `insertEvent` on
101
108
  * `db`) so it honors the caller's `EventsContext` — `readOnly` suppresses
102
109
  * the write and an injected `now` is used for `ts` — the same as every
103
110
  * other event `runRetentionPurgePass` appends in this callback.
104
111
  */
105
- export function vacuumStateDbIfReclaimable(db, freelist, eventsCtx) {
106
- if (freelist.ratio <= STATE_DB_FREELIST_WARN_RATIO) {
112
+ export function vacuumIfReclaimable(db, freelist, vacuum, eventsCtx) {
113
+ if (!vacuum.force && freelist.ratio <= STATE_DB_FREELIST_WARN_RATIO) {
107
114
  return { ran: false, reason: "below-threshold", pagesBefore: freelist.pageCount };
108
115
  }
109
116
  try {
@@ -116,7 +123,7 @@ export function vacuumStateDbIfReclaimable(db, freelist, eventsCtx) {
116
123
  }
117
124
  const pagesAfter = Number(firstColumn(db.prepare("PRAGMA page_count").get()) ?? 0);
118
125
  appendEvent({
119
- eventType: STATE_DB_VACUUMED_EVENT,
126
+ eventType: vacuum.eventType,
120
127
  metadata: { pagesBefore: freelist.pageCount, pagesAfter, freelistRatioBefore: freelist.ratio },
121
128
  }, eventsCtx);
122
129
  return { ran: true, pagesBefore: freelist.pageCount, pagesAfter };