akm-cli 0.9.16 → 0.9.17-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (403) hide show
  1. package/CHANGELOG.md +2101 -0
  2. package/STABILITY.md +11 -10
  3. package/dist/akm +124 -193
  4. package/dist/akm-migrate +38 -19
  5. package/dist/assets/hints/cli-hints-full.md +6 -7
  6. package/dist/assets/improve-strategies/catchup.json +0 -3
  7. package/dist/assets/improve-strategies/consolidate.json +0 -1
  8. package/dist/assets/improve-strategies/default.json +1 -2
  9. package/dist/assets/improve-strategies/proactive-maintenance.json +1 -2
  10. package/dist/assets/improve-strategies/quick.json +1 -2
  11. package/dist/assets/improve-strategies/reflect-distill.json +1 -2
  12. package/dist/assets/improve-strategies/thorough.json +0 -3
  13. package/dist/assets/prompts/consolidate-pair.md +20 -0
  14. package/dist/assets/prompts/consolidate-system.md +4 -11
  15. package/dist/assets/prompts/retrieval-relevance-judge.md +6 -0
  16. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +20 -20
  17. package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -2
  18. package/dist/assets/templates/html/health.html +3 -5
  19. package/dist/cli/retired-commands.js +1 -1
  20. package/dist/cli/shared.js +6 -2
  21. package/dist/cli/unknown-flags.js +24 -1
  22. package/dist/cli.js +68 -10
  23. package/dist/commands/agent/agent-dispatch.js +1 -1
  24. package/dist/commands/command/command-execution.js +24 -62
  25. package/dist/commands/feedback-cli.js +0 -1
  26. package/dist/commands/health/accept-rate.js +6 -0
  27. package/dist/commands/health/archive-usage.js +92 -0
  28. package/dist/commands/health/checks.js +83 -74
  29. package/dist/commands/health/config-skew.js +38 -0
  30. package/dist/commands/health/data-dir-usage.js +25 -13
  31. package/dist/commands/health/egress.js +54 -0
  32. package/dist/commands/health/html-report.js +1 -42
  33. package/dist/commands/health/improve-metrics.js +136 -591
  34. package/dist/commands/health/md-report.js +1 -6
  35. package/dist/commands/health/plugin-staleness.js +53 -3
  36. package/dist/commands/health/renderers.js +12 -4
  37. package/dist/commands/health/report-view-model.js +14 -120
  38. package/dist/commands/health/types-improve.js +4 -19
  39. package/dist/commands/health/windows.js +64 -74
  40. package/dist/commands/health.js +145 -143
  41. package/dist/commands/improve/consolidate/chunking.js +26 -117
  42. package/dist/commands/improve/consolidate/continuity-check.js +137 -0
  43. package/dist/commands/improve/consolidate/pair-pass.js +791 -0
  44. package/dist/commands/improve/consolidate/sanitize.js +54 -149
  45. package/dist/commands/improve/consolidate.js +589 -1127
  46. package/dist/commands/improve/content-hash.js +16 -24
  47. package/dist/commands/improve/distill/content-repair.js +18 -100
  48. package/dist/commands/improve/distill-guards.js +20 -81
  49. package/dist/commands/improve/distill-promotion-policy.js +23 -243
  50. package/dist/commands/improve/distill.js +608 -1041
  51. package/dist/commands/improve/eligibility.js +126 -390
  52. package/dist/commands/improve/execution.js +8 -10
  53. package/dist/commands/improve/extract-prompt.js +1 -2
  54. package/dist/commands/improve/extract.js +487 -1046
  55. package/dist/commands/improve/feedback-valence.js +0 -25
  56. package/dist/commands/improve/improve-cli.js +75 -169
  57. package/dist/commands/improve/improve-result-file.js +10 -66
  58. package/dist/commands/improve/improve-strategies.js +52 -4
  59. package/dist/commands/improve/improve-usage-report.js +18 -64
  60. package/dist/commands/improve/improve.js +480 -1074
  61. package/dist/commands/improve/ledger.js +119 -0
  62. package/dist/commands/improve/locks.js +2 -8
  63. package/dist/commands/improve/loop-stages.js +415 -1073
  64. package/dist/commands/improve/memory/derived-ref.js +12 -77
  65. package/dist/commands/improve/memory/memory-belief.js +16 -118
  66. package/dist/commands/improve/memory/memory-improve.js +266 -14
  67. package/dist/commands/improve/outcome-loop.js +28 -156
  68. package/dist/commands/improve/planner.js +5 -15
  69. package/dist/commands/improve/preparation.js +779 -2319
  70. package/dist/commands/improve/proactive-maintenance.js +34 -101
  71. package/dist/commands/improve/reflect-noise.js +104 -280
  72. package/dist/commands/improve/reflect.js +642 -1353
  73. package/dist/commands/improve/retrieval-gate.js +127 -0
  74. package/dist/commands/improve/retrieval-scope.js +92 -0
  75. package/dist/commands/improve/salience.js +41 -240
  76. package/dist/commands/improve/session-asset.js +19 -100
  77. package/dist/commands/improve/stage.js +322 -0
  78. package/dist/commands/lint/base-linter.js +37 -15
  79. package/dist/commands/proposal/drain.js +261 -578
  80. package/dist/commands/proposal/proposal-cli.js +19 -20
  81. package/dist/commands/proposal/proposal-types.js +31 -24
  82. package/dist/commands/proposal/proposal.js +38 -8
  83. package/dist/commands/proposal/propose.js +134 -160
  84. package/dist/commands/proposal/repository.js +1097 -1394
  85. package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
  86. package/dist/commands/proposal/validators/proposal-validators.js +1 -1
  87. package/dist/commands/proposal/validators/proposals.js +22 -89
  88. package/dist/commands/read/curate.js +105 -462
  89. package/dist/commands/read/knowledge.js +3 -2
  90. package/dist/commands/read/search-cli.js +16 -33
  91. package/dist/commands/read/search.js +17 -23
  92. package/dist/commands/read/show.js +57 -108
  93. package/dist/commands/sources/bundle-cli.js +25 -2
  94. package/dist/commands/sources/bundle-config-ops.js +4 -0
  95. package/dist/commands/sources/dangerous-env-audit.js +1 -2
  96. package/dist/commands/sources/info.js +127 -29
  97. package/dist/commands/sources/installed-stashes.js +197 -746
  98. package/dist/commands/sources/schema-repair.js +98 -129
  99. package/dist/commands/sources/source-add.js +62 -12
  100. package/dist/commands/sources/source-manage.js +9 -2
  101. package/dist/commands/sources/stash-cli.js +24 -4
  102. package/dist/commands/tasks/explain.js +10 -13
  103. package/dist/commands/tasks/tasks-cli.js +12 -13
  104. package/dist/commands/tasks/tasks.js +350 -936
  105. package/dist/commands/tasks/validate.js +26 -24
  106. package/dist/commands/workflow/plan.js +22 -29
  107. package/dist/commands/workflow-cli.js +4 -4
  108. package/dist/core/adapter/adapters/akm-adapter.js +2 -1
  109. package/dist/core/adapter/adapters/akm-lint.js +2 -3
  110. package/dist/core/adapter/adapters/akm-metadata.js +42 -12
  111. package/dist/core/adapter/adapters/akm-task-adapter.js +29 -8
  112. package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
  113. package/dist/core/adapter/execution-source.js +17 -29
  114. package/dist/core/asset/asset-placement.js +4 -13
  115. package/dist/core/asset/frontmatter.js +106 -1
  116. package/dist/core/asset/resolve-ref.js +1 -1
  117. package/dist/core/bundle-id.js +42 -5
  118. package/dist/core/bundle-rename.js +285 -0
  119. package/dist/core/config/config-io.js +1 -2
  120. package/dist/core/config/config-schema.js +9 -34
  121. package/dist/core/config/config-walker.js +1 -1
  122. package/dist/core/config/config.js +184 -111
  123. package/dist/core/config/engine-semantics.js +0 -2
  124. package/dist/core/config/legacy-source-shape-shim.js +38 -9
  125. package/dist/core/config/schema/embedding.js +20 -5
  126. package/dist/core/config/schema/engines.js +5 -0
  127. package/dist/core/config/schema/execution.js +1 -1
  128. package/dist/core/config/schema/experimental.js +1 -1
  129. package/dist/core/config/schema/improve-processes.js +54 -125
  130. package/dist/core/config/schema/improve.js +4 -42
  131. package/dist/core/config/schema/index-config.js +9 -48
  132. package/dist/core/config/schema/scheduler.js +12 -12
  133. package/dist/core/config/schema/search.js +6 -22
  134. package/dist/core/env-secret-ref.js +0 -1
  135. package/dist/core/errors.js +8 -9
  136. package/dist/core/file-change.js +13 -5
  137. package/dist/core/file-lock.js +76 -173
  138. package/dist/core/improve-result.js +35 -7
  139. package/dist/core/improve-types.js +0 -1
  140. package/dist/core/logs-db.js +2 -2
  141. package/dist/core/loopback.js +7 -12
  142. package/dist/core/non-task-input.js +20 -0
  143. package/dist/core/parse.js +13 -16
  144. package/dist/core/paths.js +0 -24
  145. package/dist/core/redaction.js +109 -2
  146. package/dist/core/run-lock.js +2 -5
  147. package/dist/core/spawn-env.js +1 -1
  148. package/dist/core/state/migrations.js +123 -61
  149. package/dist/core/state-db-scope.js +2 -4
  150. package/dist/core/state-db.js +126 -692
  151. package/dist/core/time.js +0 -20
  152. package/dist/core/type-presentation.js +1 -9
  153. package/dist/core/write-source.js +294 -1005
  154. package/dist/execution/input-contract.js +1 -1
  155. package/dist/execution/resolved-request.js +135 -689
  156. package/dist/execution/source.js +63 -257
  157. package/dist/execution/target-ref.js +1 -1
  158. package/dist/indexer/bundle-identity-guard.js +2 -2
  159. package/dist/indexer/db/llm-cache.js +2 -2
  160. package/dist/indexer/ensure-index.js +77 -73
  161. package/dist/indexer/index-rebuild-lock.js +3 -11
  162. package/dist/indexer/index-writer-lock.js +8 -17
  163. package/dist/indexer/index-written-assets.js +141 -154
  164. package/dist/indexer/indexer.js +400 -1124
  165. package/dist/indexer/links/declared-links.js +90 -0
  166. package/dist/indexer/materialize-embeddings.js +60 -397
  167. package/dist/indexer/passes/memory-inference.js +96 -90
  168. package/dist/indexer/passes/metadata.js +132 -219
  169. package/dist/indexer/read-preflight.js +0 -7
  170. package/dist/indexer/scan/doc-to-entry.js +2 -3
  171. package/dist/indexer/scan/drain-dir.js +1 -1
  172. package/dist/indexer/search/db-search.js +190 -590
  173. package/dist/indexer/search/fts-query.js +30 -41
  174. package/dist/indexer/search/ranking.js +28 -154
  175. package/dist/indexer/search/search-attribution.js +12 -32
  176. package/dist/indexer/search/search-fields.js +11 -15
  177. package/dist/indexer/search/search-hit-enrichers.js +54 -85
  178. package/dist/indexer/search/search-source.js +1 -4
  179. package/dist/indexer/usage/usage-events.js +36 -7
  180. package/dist/indexer/walk/walker.js +3 -4
  181. package/dist/integrations/agent/engine-fallback.js +23 -40
  182. package/dist/integrations/agent/engine-resolution.js +93 -183
  183. package/dist/integrations/agent/execution.js +507 -0
  184. package/dist/integrations/agent/model-map.js +28 -156
  185. package/dist/integrations/agent/request-lowering.js +66 -141
  186. package/dist/integrations/agent/runner-dispatch.js +143 -321
  187. package/dist/integrations/agent/runner.js +54 -14
  188. package/dist/integrations/lockfile.js +53 -101
  189. package/dist/llm/client.js +18 -6
  190. package/dist/llm/embedders/deterministic.js +2 -3
  191. package/dist/llm/embedders/profile.js +71 -0
  192. package/dist/llm/embedders/remote.js +11 -17
  193. package/dist/llm/feature-gate.js +0 -8
  194. package/dist/llm/index-passes.js +3 -5
  195. package/dist/llm/memory-infer.js +1 -2
  196. package/dist/llm/structured-call.js +5 -24
  197. package/dist/output/generic-render.js +23 -11
  198. package/dist/output/html-render.js +13 -10
  199. package/dist/output/render-registry.js +3 -32
  200. package/dist/output/shapes/helpers.js +25 -38
  201. package/dist/output/shapes/passthrough.js +1 -9
  202. package/dist/{indexer/graph/graph-types.js → output/text/bundle-rename.js} +4 -1
  203. package/dist/output/text/command-format.js +69 -31
  204. package/dist/output/text/helpers.js +1 -1
  205. package/dist/output/text/migrate.js +5 -14
  206. package/dist/output/text/proposal-format.js +48 -3
  207. package/dist/output/text/show-format.js +13 -17
  208. package/dist/output/text/workflow-format.js +0 -32
  209. package/dist/output/text.js +2 -0
  210. package/dist/registry/factory.js +4 -19
  211. package/dist/registry/network.js +66 -220
  212. package/dist/registry/providers/index.js +0 -2
  213. package/dist/registry/providers/skills-sh.js +3 -14
  214. package/dist/registry/providers/static-index.js +24 -26
  215. package/dist/registry/resolve.js +55 -131
  216. package/dist/scripts/akm-migrate-node.js +42948 -92369
  217. package/dist/scripts/akm-migrate.js +42935 -92354
  218. package/dist/setup/registry-stash-loader.js +4 -13
  219. package/dist/setup/semantic-assets.js +3 -44
  220. package/dist/setup/setup.js +1 -1
  221. package/dist/setup/steps/connection.js +5 -6
  222. package/dist/setup/steps/platforms.js +2 -2
  223. package/dist/setup/steps/tasks.js +25 -15
  224. package/dist/sources/provider-factory.js +17 -18
  225. package/dist/sources/providers/filesystem.js +2 -3
  226. package/dist/sources/providers/git-install.js +7 -1
  227. package/dist/sources/providers/git-provider.js +0 -3
  228. package/dist/sources/providers/git-stash.js +83 -21
  229. package/dist/sources/providers/npm.js +2 -4
  230. package/dist/sources/providers/provider-utils.js +5 -10
  231. package/dist/sources/providers/website.js +0 -2
  232. package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
  233. package/dist/sources/website-url.js +2 -2
  234. package/dist/storage/database.js +9 -35
  235. package/dist/storage/repositories/improve-ledger-repository.js +209 -0
  236. package/dist/storage/repositories/index-connection.js +39 -72
  237. package/dist/storage/repositories/index-entries-repository.js +131 -129
  238. package/dist/storage/repositories/index-entry-mapper.js +1 -2
  239. package/dist/storage/repositories/index-entry-schema.js +101 -268
  240. package/dist/storage/repositories/index-fts-repository.js +86 -256
  241. package/dist/storage/repositories/index-links-repository.js +143 -0
  242. package/dist/storage/repositories/index-llm-cache-repository.js +7 -9
  243. package/dist/storage/repositories/index-meta-repository.js +6 -4
  244. package/dist/storage/repositories/index-schema.js +257 -325
  245. package/dist/storage/repositories/index-utility-repository.js +8 -29
  246. package/dist/storage/repositories/index-vec-repository.js +133 -414
  247. package/dist/storage/repositories/outcome-repository.js +2 -1
  248. package/dist/storage/repositories/proposals-repository.js +104 -1
  249. package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
  250. package/dist/storage/repositories/salience-repository.js +1 -19
  251. package/dist/storage/repositories/task-history-repository.js +26 -4
  252. package/dist/storage/repositories/workflow-runs-repository.js +53 -244
  253. package/dist/storage/sqlite-migrations.js +136 -0
  254. package/dist/storage/sqlite-pragmas.js +11 -9
  255. package/dist/storage/sqlite-transaction.js +170 -0
  256. package/dist/storage/state-db-integrity.js +130 -0
  257. package/dist/tasks/activation-config.js +134 -62
  258. package/dist/tasks/backends/cron.js +191 -302
  259. package/dist/tasks/backends/exec-utils.js +2 -5
  260. package/dist/tasks/backends/launchd.js +141 -748
  261. package/dist/tasks/backends/schtasks.js +119 -623
  262. package/dist/tasks/prepare/prepare-support.js +5 -15
  263. package/dist/tasks/prepare/prepare.js +0 -2
  264. package/dist/tasks/resolve-akm-bin.js +20 -79
  265. package/dist/tasks/run/attempt-lifecycle.js +0 -1
  266. package/dist/tasks/run/load-task.js +1 -1
  267. package/dist/tasks/scheduler-binding.js +20 -238
  268. package/dist/tasks/scheduler-invocation.js +136 -244
  269. package/dist/tasks/scheduler-lock.js +53 -0
  270. package/dist/tasks/scheduler-sync.js +368 -679
  271. package/dist/tasks/source/parse-task-source.js +55 -9
  272. package/dist/tasks/source/task-source-v3-frozen.js +3 -4
  273. package/dist/tasks/source/task-to-v4.js +464 -88
  274. package/dist/workflows/authoring/authoring.js +3 -12
  275. package/dist/workflows/compile.js +211 -0
  276. package/dist/workflows/concurrency-policy.js +13 -74
  277. package/dist/workflows/exec/child-invocation.js +3 -17
  278. package/dist/workflows/exec/child-workflow.js +32 -141
  279. package/dist/workflows/exec/dispatch-redaction.js +13 -53
  280. package/dist/workflows/exec/environment.js +98 -0
  281. package/dist/workflows/exec/exec-unit.js +33 -140
  282. package/dist/workflows/exec/frozen-judge.js +7 -59
  283. package/dist/workflows/exec/native-executor.js +82 -341
  284. package/dist/workflows/exec/param-secrets.js +29 -47
  285. package/dist/workflows/exec/run-workflow.js +154 -387
  286. package/dist/workflows/exec/scheduler.js +9 -36
  287. package/dist/workflows/exec/step-work.js +127 -430
  288. package/dist/workflows/exec/unit-dispatch.js +11 -63
  289. package/dist/workflows/exec/unit-writer.js +8 -52
  290. package/dist/workflows/exec/worktree.js +39 -273
  291. package/dist/workflows/freeze/child-output-references.js +4 -15
  292. package/dist/workflows/freeze/environment.js +99 -92
  293. package/dist/workflows/freeze/freeze.js +172 -0
  294. package/dist/workflows/freeze/step-values.js +19 -21
  295. package/dist/workflows/freeze/targets/child-workflow.js +23 -92
  296. package/dist/workflows/freeze/targets/command.js +10 -33
  297. package/dist/workflows/freeze/targets/script.js +5 -12
  298. package/dist/workflows/freeze/targets/shell.js +3 -6
  299. package/dist/workflows/freeze/targets/task.js +25 -80
  300. package/dist/workflows/freeze/task-bindings.js +20 -67
  301. package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
  302. package/dist/workflows/ir/params.js +6 -51
  303. package/dist/workflows/ir/plan-hash.js +2 -34
  304. package/dist/workflows/parser.js +140 -43
  305. package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
  306. package/dist/workflows/renderer.js +36 -69
  307. package/dist/workflows/resource-limits.js +12 -120
  308. package/dist/workflows/runtime/agent-identity.js +8 -40
  309. package/dist/workflows/runtime/run-outputs.js +3 -6
  310. package/dist/workflows/runtime/run-plan.js +316 -0
  311. package/dist/workflows/runtime/runs.js +48 -200
  312. package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
  313. package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
  314. package/dist/workflows/validate-summary.js +2 -7
  315. package/docs/integration/bundling-akm.md +49 -42
  316. package/docs/migration/README.md +1 -0
  317. package/docs/migration/release-notes/0.9.17.md +43 -0
  318. package/docs/migration/v0.9.1-to-v0.9.2.md +23 -7
  319. package/docs/reference/cli.md +232 -135
  320. package/docs/reference/configuration.md +71 -57
  321. package/docs/reference/data-and-telemetry.md +20 -21
  322. package/docs/reference/tasks.md +105 -39
  323. package/docs/reference/workflow-schema.md +14 -18
  324. package/docs/reference/workflows.md +6 -9
  325. package/package.json +1 -1
  326. package/schemas/akm-config.json +115 -738
  327. package/schemas/akm-workflow.json +1 -0
  328. package/dist/assets/improve-strategies/graph-refresh.json +0 -15
  329. package/dist/assets/prompts/contradiction-judge.md +0 -33
  330. package/dist/assets/prompts/graph-extract-system.md +0 -1
  331. package/dist/assets/prompts/graph-extract-user-prompt.md +0 -35
  332. package/dist/assets/prompts/metadata-enhance-system.md +0 -1
  333. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +0 -4
  334. package/dist/commands/health/advisories.js +0 -150
  335. package/dist/commands/health/metrics.js +0 -329
  336. package/dist/commands/health/surfaces.js +0 -102
  337. package/dist/commands/improve/anti-collapse.js +0 -83
  338. package/dist/commands/improve/collapse-detector.js +0 -432
  339. package/dist/commands/improve/consolidate/eligibility.js +0 -48
  340. package/dist/commands/improve/consolidate/merge.js +0 -149
  341. package/dist/commands/improve/distill/promote-memory.js +0 -291
  342. package/dist/commands/improve/distill/quality-gate.js +0 -337
  343. package/dist/commands/improve/eval-cases.js +0 -52
  344. package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
  345. package/dist/commands/improve/proposal-envelope.js +0 -31
  346. package/dist/commands/improve/run-context.js +0 -123
  347. package/dist/commands/improve/shared.js +0 -31
  348. package/dist/commands/improve/source-identity.js +0 -28
  349. package/dist/commands/improve/triage.js +0 -96
  350. package/dist/commands/proposal/drain-policies.js +0 -151
  351. package/dist/commands/sources/update-transaction.js +0 -220
  352. package/dist/core/action-contributors.js +0 -28
  353. package/dist/core/config/config-version-shim.js +0 -101
  354. package/dist/core/fs-txn.js +0 -405
  355. package/dist/core/lexical-score.js +0 -25
  356. package/dist/core/maintenance-barrier.js +0 -167
  357. package/dist/execution/executable-identity.js +0 -105
  358. package/dist/execution/guarded-source.js +0 -427
  359. package/dist/indexer/db/graph-db.js +0 -444
  360. package/dist/indexer/graph/graph-boost.js +0 -427
  361. package/dist/indexer/graph/graph-dedup.js +0 -95
  362. package/dist/indexer/graph/graph-extraction.js +0 -1108
  363. package/dist/indexer/search/name-match.js +0 -35
  364. package/dist/indexer/search/ranking-contributors.js +0 -515
  365. package/dist/indexer/search/ranking-types.js +0 -4
  366. package/dist/indexer/walk/project-context.js +0 -192
  367. package/dist/integrations/agent/execution-cascade.js +0 -566
  368. package/dist/integrations/agent/execution-definitions.js +0 -202
  369. package/dist/integrations/agent/execution-lowering.js +0 -841
  370. package/dist/integrations/agent/execution-preparation.js +0 -98
  371. package/dist/integrations/agent/inline-execution.js +0 -74
  372. package/dist/llm/graph-extract.js +0 -728
  373. package/dist/llm/metadata-enhance.js +0 -96
  374. package/dist/registry/create-provider-registry.js +0 -29
  375. package/dist/registry/pinned-request-helper.js +0 -247
  376. package/dist/registry/pinned-transport.js +0 -717
  377. package/dist/sources/providers/index.js +0 -14
  378. package/dist/storage/engines/sqlite-migrations.js +0 -271
  379. package/dist/storage/repositories/canaries-repository.js +0 -107
  380. package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
  381. package/dist/storage/repositories/registry-cache.js +0 -113
  382. package/dist/tasks/scheduler-sync-preview.js +0 -52
  383. package/dist/tasks/source/task-to-v3.js +0 -507
  384. package/dist/workflows/freeze/resolve-steps.js +0 -86
  385. package/dist/workflows/freeze/source-freeze.js +0 -64
  386. package/dist/workflows/ir/compile.js +0 -321
  387. package/dist/workflows/ir/environment-v4.js +0 -330
  388. package/dist/workflows/ir/freeze-v4.js +0 -153
  389. package/dist/workflows/ir/schema-v4.js +0 -745
  390. package/dist/workflows/ir/schema.js +0 -354
  391. package/dist/workflows/program/schema.js +0 -77
  392. package/dist/workflows/runtime/checkin.js +0 -57
  393. package/dist/workflows/runtime/plan-classifier.js +0 -196
  394. package/dist/workflows/runtime/unit-checkin.js +0 -45
  395. package/dist/workflows/runtime/unit-phases.js +0 -20
  396. package/dist/workflows/schema.js +0 -4
  397. package/dist/workflows/source-ir/compile.js +0 -200
  398. package/dist/workflows/source-ir/program.js +0 -50
  399. package/dist/workflows/source-ir/result.js +0 -26
  400. package/dist/workflows/source-ir/schema.js +0 -786
  401. package/dist/workflows/source-ir/triggers.js +0 -79
  402. package/dist/workflows/source-ir/uses.js +0 -40
  403. package/dist/workflows/validator.js +0 -60
@@ -2,69 +2,16 @@
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
  import { randomUUID } from "node:crypto";
5
- import { NotFoundError, TransientError, UsageError } from "../../core/errors.js";
6
- import { isSqliteContentionError, openStateDatabase, withImmediateTransaction } from "../../core/state-db.js";
5
+ import { NotFoundError, UsageError } from "../../core/errors.js";
6
+ import { openStateDatabase, withImmediateTransaction } from "../../core/state-db.js";
7
7
  import { borrowScopedStateDb, withStateDbScope } from "../../core/state-db-scope.js";
8
- import { sleepSync } from "../../runtime.js";
8
+ import { WORKFLOW_PLAN_VERSION } from "../../workflows/plan.js";
9
9
  import { escapeLikePattern } from "../like-pattern.js";
10
10
  import { resolveStorageLocations } from "../locations.js";
11
11
  import { insertEventOnce, insertEventStrict } from "./events-repository.js";
12
- function assertAttemptReservationLease(input, run) {
13
- if (input.leaseMode === "direct") {
14
- if (run.engine_lease_holder === null)
15
- return;
16
- throw new UsageError(`Workflow run ${input.runId} is leased by another engine; direct durable dispatch reservation is forbidden.`, "RESOURCE_ALREADY_EXISTS");
17
- }
18
- if (run.engine_lease_holder !== input.claimHolder) {
19
- throw new UsageError(`Workflow run ${input.runId} lease holder changed; refusing a stale durable dispatch reservation.`, "RESOURCE_ALREADY_EXISTS");
20
- }
21
- if (run.engine_lease_until === null || run.engine_lease_until < input.now) {
22
- throw new UsageError(`Workflow run ${input.runId} engine lease expired before durable dispatch reservation.`, "RESOURCE_ALREADY_EXISTS");
23
- }
24
- }
25
- /**
26
- * Whether `error` is one of the specific SQLite conditions a run-lease
27
- * statement can throw under real cross-process contention on the same row:
28
- * the shared {@link isSqliteContentionError} classifier (SQLITE_BUSY/LOCKED,
29
- * "database is locked", "database table is locked", the phantom-BEGIN
30
- * marker) plus two corruption-shaped message texts a transient contention
31
- * blip has been observed producing on this specific race, "disk I/O error"
32
- * and "database disk image is malformed". Matching on this set alone is
33
- * never sufficient to call something lease contention — see
34
- * {@link WorkflowRunsRepository.acquireEngineLease}, which additionally
35
- * requires a fresh read confirming a live lease before substituting the
36
- * lease-held message for the original error.
37
- */
38
- function isLeaseContentionSqliteError(error) {
39
- if (isSqliteContentionError(error))
40
- return true;
41
- const message = error instanceof Error ? error.message : String(error);
42
- return message.includes("disk I/O error") || message.includes("database disk image is malformed");
43
- }
44
- const LEASE_RETRY_ATTEMPTS = 4;
45
- const LEASE_RETRY_BASE_DELAY_MS = 15;
46
- /**
47
- * Retry a single lease statement across a short, bounded set of attempts when
48
- * it throws one of {@link isLeaseContentionSqliteError}'s conditions —
49
- * absorbing a blip that a fresh attempt on the same connection clears on its
50
- * own. Any other error, or the same error surviving every attempt, propagates
51
- * unchanged; this never converts a persistent failure into a false success.
52
- */
53
- function runLeaseStatementWithRetry(fn) {
54
- let lastError;
55
- for (let attempt = 0; attempt < LEASE_RETRY_ATTEMPTS; attempt += 1) {
56
- try {
57
- return fn();
58
- }
59
- catch (error) {
60
- if (!isLeaseContentionSqliteError(error))
61
- throw error;
62
- lastError = error;
63
- if (attempt < LEASE_RETRY_ATTEMPTS - 1)
64
- sleepSync(LEASE_RETRY_BASE_DELAY_MS * 2 ** attempt);
65
- }
66
- }
67
- throw lastError;
12
+ /** Informational `claim_holder` value stamped on attempt rows: the dispatching process. */
13
+ function dispatchingProcess() {
14
+ return `pid:${process.pid}`;
68
15
  }
69
16
  /**
70
17
  * Repository owning every raw SQL statement against `workflow_runs` and
@@ -245,7 +192,7 @@ export class WorkflowRunsRepository {
245
192
  // ── writes ─────────────────────────────────────────────────────────────────
246
193
  insertRun(input) {
247
194
  // R-R3 (P3a Review log; docs/plans/specs/p4-deletions-closeout.md §8):
248
- // this 13-column list is hand-duplicated by publishChildWorkflowRun's own
195
+ // this 12-column list is hand-duplicated by publishChildWorkflowRun's own
249
196
  // INSERT below, which extends it with parent_run_id/parent_unit_id/
250
197
  // invocation_key. A signature refactor to share one INSERT builder was
251
198
  // considered and deliberately deferred — see
@@ -254,9 +201,9 @@ export class WorkflowRunsRepository {
254
201
  this.db
255
202
  .prepare(`INSERT INTO workflow_runs (
256
203
  id, workflow_ref, scope_key, workflow_entry_id, workflow_title, status, params_json, current_step_id, created_at, updated_at,
257
- agent_harness, agent_session_id, checkin_armed_at
258
- ) VALUES (?, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?)`)
259
- .run(input.id, input.workflowRef, input.scopeKey, input.workflowEntryId, input.workflowTitle, input.paramsJson, input.currentStepId, input.createdAt, input.updatedAt, input.agentHarness, input.agentSessionId, input.checkinArmedAt);
204
+ agent_harness, agent_session_id
205
+ ) VALUES (?, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?, ?)`)
206
+ .run(input.id, input.workflowRef, input.scopeKey, input.workflowEntryId, input.workflowTitle, input.paramsJson, input.currentStepId, input.createdAt, input.updatedAt, input.agentHarness, input.agentSessionId);
260
207
  }
261
208
  insertSteps(steps) {
262
209
  const insertStep = this.db.prepare(`INSERT INTO workflow_run_steps (
@@ -274,9 +221,7 @@ export class WorkflowRunsRepository {
274
221
  .run(runId, currentStepId);
275
222
  }
276
223
  markRunActive(runId, updatedAt) {
277
- this.db
278
- .prepare("UPDATE workflow_runs SET status = 'active', updated_at = ?, engine_lease_holder = NULL, engine_lease_until = NULL WHERE id = ?")
279
- .run(updatedAt, runId);
224
+ this.db.prepare("UPDATE workflow_runs SET status = 'active', updated_at = ? WHERE id = ?").run(updatedAt, runId);
280
225
  }
281
226
  updateStepCompletion(input) {
282
227
  this.db
@@ -288,29 +233,25 @@ export class WorkflowRunsRepository {
288
233
  updateRunState(input) {
289
234
  this.db
290
235
  .prepare(`UPDATE workflow_runs
291
- SET status = ?, current_step_id = ?, updated_at = ?, completed_at = ?, checkin_armed_at = ?
236
+ SET status = ?, current_step_id = ?, updated_at = ?, completed_at = ?
292
237
  WHERE id = ?`)
293
- .run(input.status, input.currentStepId, input.updatedAt, input.completedAt, input.checkinArmedAt, input.runId);
238
+ .run(input.status, input.currentStepId, input.updatedAt, input.completedAt, input.runId);
294
239
  }
295
240
  markRunAbandoned(runId, updatedAt) {
296
241
  const result = this.db
297
242
  .prepare(`UPDATE workflow_runs
298
- SET status = 'failed', updated_at = ?, completed_at = ?, checkin_armed_at = ?
243
+ SET status = 'failed', updated_at = ?, completed_at = ?
299
244
  WHERE id = ? AND status IN ('active', 'blocked')`)
300
- .run(updatedAt, updatedAt, updatedAt, runId);
245
+ .run(updatedAt, updatedAt, runId);
301
246
  return Number(result.changes) === 1;
302
247
  }
303
- rearmCheckin(runId, checkinArmedAt) {
304
- this.db.prepare("UPDATE workflow_runs SET checkin_armed_at = ? WHERE id = ?").run(checkinArmedAt, runId);
305
- }
306
248
  /**
307
- * Atomically publish the entire durable-v4 run spine after the final source
308
- * CAS. No run row, partial spine, plan attachment, or started event can
309
- * escape independently across a crash or statement failure.
249
+ * Atomically publish the entire run spine. No run row, partial spine, plan
250
+ * attachment, or started event can escape independently across a crash or
251
+ * statement failure.
310
252
  */
311
253
  publishWorkflowRunV4(input) {
312
254
  this.immediateTransaction((db) => {
313
- input.revalidateSources();
314
255
  if (!input.force) {
315
256
  // The uniqueness guard must never silently skip its scope predicate
316
257
  // (#942) — every real caller (`startWorkflowRun`) stamps a concrete
@@ -329,7 +270,7 @@ export class WorkflowRunsRepository {
329
270
  }
330
271
  this.insertRun(input.run);
331
272
  this.insertSteps(input.steps);
332
- db.prepare("UPDATE workflow_runs SET plan_json = ?, plan_hash = ?, plan_ir_version = 5 WHERE id = ?").run(input.planJson, input.planHash, input.run.id);
273
+ db.prepare("UPDATE workflow_runs SET plan_json = ?, plan_hash = ?, plan_ir_version = ? WHERE id = ?").run(input.planJson, input.planHash, WORKFLOW_PLAN_VERSION, input.run.id);
333
274
  insertEventOnce(db, {
334
275
  eventType: "workflow_started",
335
276
  ts: input.run.createdAt,
@@ -350,15 +291,12 @@ export class WorkflowRunsRepository {
350
291
  * `(parent_run_id, invocation_key)` and return the existing child if
351
292
  * present; otherwise INSERT the child run row (parentage columns +
352
293
  * `invocation_key`), its step rows, attach the embedded frozen child plan
353
- * (`plan_ir_version = 5`), and append its `workflow_started` event — then
354
- * return the freshly-inserted row.
294
+ * (`plan_ir_version` = the current plan version), and append its
295
+ * `workflow_started` event — then return the freshly-inserted row.
355
296
  *
356
- * Deliberately does NOT: call {@link findActiveRunForScope} or raise
297
+ * Deliberately does NOT call {@link findActiveRunForScope} or raise
357
298
  * `RESOURCE_ALREADY_EXISTS` (top-level scope-conflict rules do not apply to
358
- * a child, C-10); call `revalidateSources` or read the filesystem in any
359
- * way (the child plan was frozen and CAS'd into the PARENT's read set at
360
- * parent freeze, C-11); or touch {@link publishWorkflowRunV4} /
361
- * `startWorkflowRun` (untouched, C-12).
299
+ * a child), and reads no source: the child plan was frozen into the parent's.
362
300
  *
363
301
  * This method's serialization guarantee holds only when it is the
364
302
  * OUTERMOST transaction on the connection (spec Review log R10,
@@ -394,16 +332,16 @@ export class WorkflowRunsRepository {
394
332
  if (existing)
395
333
  return existing;
396
334
  // R-R3 (P3a Review log; docs/plans/specs/p4-deletions-closeout.md §8):
397
- // the first 13 columns here must stay byte-identical to insertRun's own
335
+ // the first 12 columns here must stay byte-identical to insertRun's own
398
336
  // column list above (hand-duplicated, not shared, by deliberate choice
399
337
  // — see docs/architecture/decisions/0009-child-run-publication-column-parity.md).
400
338
  // Keep both column lists in sync by hand.
401
339
  db.prepare(`INSERT INTO workflow_runs (
402
340
  id, workflow_ref, scope_key, workflow_entry_id, workflow_title, status, params_json, current_step_id, created_at, updated_at,
403
- agent_harness, agent_session_id, checkin_armed_at, parent_run_id, parent_unit_id, invocation_key
404
- ) VALUES (?, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(input.run.id, input.run.workflowRef, input.run.scopeKey, input.run.workflowEntryId, input.run.workflowTitle, input.run.paramsJson, input.run.currentStepId, input.run.createdAt, input.run.updatedAt, input.run.agentHarness, input.run.agentSessionId, input.run.checkinArmedAt, input.parentRunId, input.spawnedByUnitId, input.invocationKey);
341
+ agent_harness, agent_session_id, parent_run_id, parent_unit_id, invocation_key
342
+ ) VALUES (?, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(input.run.id, input.run.workflowRef, input.run.scopeKey, input.run.workflowEntryId, input.run.workflowTitle, input.run.paramsJson, input.run.currentStepId, input.run.createdAt, input.run.updatedAt, input.run.agentHarness, input.run.agentSessionId, input.parentRunId, input.spawnedByUnitId, input.invocationKey);
405
343
  this.insertSteps(input.steps);
406
- db.prepare("UPDATE workflow_runs SET plan_json = ?, plan_hash = ?, plan_ir_version = 5 WHERE id = ?").run(input.planJson, input.planHash, input.run.id);
344
+ db.prepare("UPDATE workflow_runs SET plan_json = ?, plan_hash = ?, plan_ir_version = ? WHERE id = ?").run(input.planJson, input.planHash, WORKFLOW_PLAN_VERSION, input.run.id);
407
345
  insertEventOnce(db, {
408
346
  eventType: "workflow_started",
409
347
  ts: input.run.createdAt,
@@ -436,111 +374,6 @@ export class WorkflowRunsRepository {
436
374
  .prepare("SELECT * FROM workflow_runs WHERE parent_run_id = ? AND invocation_key = ?")
437
375
  .get(parentRunId, key) ?? undefined);
438
376
  }
439
- // ── engine run lease (migration 006 columns, R2 enforcement) ──────────────
440
- //
441
- // Single-driver invariant: at most one `akm workflow run` invocation drives
442
- // a run at a time. The lease is (holder id, expiry); all timestamps are
443
- // ISO-8601 UTC strings, which compare correctly with SQL `<` (lexicographic
444
- // order matches chronological order for a fixed-format UTC ISO string).
445
- /**
446
- * Atomically claim the run lease: succeeds when the run is unleased OR the
447
- * existing lease has expired (`engine_lease_until < now` — crash recovery).
448
- * A live lease held by anyone (including a stale copy of the same holder)
449
- * is NOT reclaimable through this method; the single UPDATE is the whole
450
- * claim, so two racing invocations cannot both win.
451
- *
452
- * The UPDATE can throw instead of cleanly returning `changes: 0` under real
453
- * cross-process contention on this row: a `SQLITE_BUSY`/`SQLITE_LOCKED`
454
- * from two engines racing the same statement, occasionally surfacing as
455
- * "database is locked" or even "database disk image is malformed" text that
456
- * reads as corruption but is not. `runLeaseStatementWithRetry` absorbs a
457
- * blip that a fresh attempt clears on its own. If it is still failing after
458
- * every retry, the row is read fresh (a plain SELECT, far less likely to
459
- * trip whatever the write hit) to get independent evidence of what is
460
- * actually going on: a live lease there means this really was contention,
461
- * so the caller gets the same lease-held message `akm workflow run` already
462
- * shows for the clean (non-throwing) case, now with `RUN_LEASE_HELD`. No
463
- * live lease — or the verifying read itself fails — means the error was
464
- * never actually about the lease, so it is rethrown exactly as raised.
465
- * Nothing here invents a diagnosis from error text alone or suppresses a
466
- * genuine SQLite failure.
467
- */
468
- acquireEngineLease(runId, holder, until, now) {
469
- try {
470
- const result = runLeaseStatementWithRetry(() => this.db
471
- .prepare(`UPDATE workflow_runs
472
- SET engine_lease_holder = ?, engine_lease_until = ?
473
- WHERE id = ? AND status = 'active'
474
- AND (engine_lease_holder IS NULL OR engine_lease_until IS NULL OR engine_lease_until < ?)`)
475
- .run(holder, until, runId, now));
476
- return Number(result.changes) > 0;
477
- }
478
- catch (error) {
479
- if (!isLeaseContentionSqliteError(error))
480
- throw error;
481
- const row = this.tryReadLeaseColumns(runId);
482
- if (row?.engine_lease_holder && row.engine_lease_until && row.engine_lease_until >= now) {
483
- throw new TransientError(`Workflow run ${runId} is already being driven by engine ${row.engine_lease_holder} ` +
484
- `(run lease expires ${row.engine_lease_until}). A second \`akm workflow run\` would race it — ` +
485
- `wait for that invocation to finish or for the lease to expire.`, "RUN_LEASE_HELD");
486
- }
487
- throw error;
488
- }
489
- }
490
- /**
491
- * Extend the lease expiry — only while `holder` still owns it. Returns
492
- * false when the lease was lost (expired and claimed by another engine),
493
- * so the caller can stop driving instead of racing the new owner. Wrapped
494
- * in the same transient-error retry as {@link acquireEngineLease}; a
495
- * renewal that still fails after retries is rethrown as-is (no confirmed
496
- * "lost lease" diagnosis to substitute, unlike the acquire case above).
497
- */
498
- renewEngineLease(runId, holder, until) {
499
- const result = runLeaseStatementWithRetry(() => this.db
500
- .prepare("UPDATE workflow_runs SET engine_lease_until = ? WHERE id = ? AND engine_lease_holder = ? AND status = 'active'")
501
- .run(until, runId, holder));
502
- return Number(result.changes) > 0;
503
- }
504
- /**
505
- * Clear the lease only while `holder` still owns a non-failed run. Failed
506
- * runs retain the final holder/expiry for forensics until explicit resume.
507
- * Releasing an already-lost lease is a harmless no-op.
508
- */
509
- releaseEngineLease(runId, holder) {
510
- this.db
511
- .prepare("UPDATE workflow_runs SET engine_lease_holder = NULL, engine_lease_until = NULL WHERE id = ? AND engine_lease_holder = ? AND status <> 'failed'")
512
- .run(runId, holder);
513
- }
514
- /**
515
- * Self-heal an engine lease its holder crashed without releasing: once
516
- * `engine_lease_until` has passed, clear it so a read (`workflow status`,
517
- * `workflow list`) stops reporting a run as engine-driven when the engine is
518
- * long gone — mirroring the maintenance barrier's self-reclaim of a wedged
519
- * sentinel (`tryAcquireMaintenanceBarrier`) rather than a bespoke mechanism.
520
- * The WHERE clause repeats the exact (holder, until) snapshot the caller
521
- * read, so a lease renewed or re-acquired in between never gets clobbered —
522
- * same compare-and-swap shape as the claim above. Never touches a lease
523
- * that is still live.
524
- */
525
- reclaimExpiredEngineLease(runId, holder, until, now) {
526
- if (until >= now)
527
- return false;
528
- const result = this.db
529
- .prepare(`UPDATE workflow_runs
530
- SET engine_lease_holder = NULL, engine_lease_until = NULL
531
- WHERE id = ? AND engine_lease_holder = ? AND engine_lease_until = ? AND engine_lease_until < ?`)
532
- .run(runId, holder, until, now);
533
- return Number(result.changes) > 0;
534
- }
535
- /** Best-effort lease-column read used only to confirm genuine contention after {@link acquireEngineLease} exhausts its retries. `undefined` on any failure — never a diagnosis, just "couldn't confirm". */
536
- tryReadLeaseColumns(runId) {
537
- try {
538
- return (this.db.prepare("SELECT engine_lease_holder, engine_lease_until FROM workflow_runs WHERE id = ?").get(runId) ?? undefined);
539
- }
540
- catch {
541
- return undefined;
542
- }
543
- }
544
377
  // ── durable v4 append-only dispatch attempts (migration 022) ─────────────
545
378
  getUnitAttempts(runId, unitId) {
546
379
  return this.db
@@ -571,61 +404,37 @@ export class WorkflowRunsRepository {
571
404
  };
572
405
  }
573
406
  /**
574
- * Reserve or reclaim one v4 external dispatch. The attempt row, legacy
575
- * projection, and directly-paired started event share one IMMEDIATE
576
- * transaction. Reclaim keeps the stable dispatch id and emits no duplicate
577
- * start event: the external effect remains explicitly at-least-once.
407
+ * Reserve or reclaim one dispatch attempt. The attempt row, the unit
408
+ * projection, and the directly-paired started event share one IMMEDIATE
409
+ * transaction. A latest attempt still `running` is reclaimed in place —
410
+ * same attempt number, same stable dispatch id, no duplicate start event —
411
+ * so a re-dispatch after a crash stays explicitly at-least-once with an
412
+ * idempotency key a downstream can dedupe on. One process drives a run at a
413
+ * time (the per-run lock file in `workflows/exec/run-workflow.ts`), so a
414
+ * `running` attempt found here is never another live driver's.
578
415
  */
579
416
  reserveUnitAttempt(input) {
417
+ const holder = dispatchingProcess();
580
418
  return this.immediateTransaction((db) => {
581
- const run = db
582
- .prepare("SELECT workflow_ref, status, engine_lease_holder, engine_lease_until FROM workflow_runs WHERE id = ?")
583
- .get(input.runId);
419
+ const run = db.prepare("SELECT workflow_ref, status FROM workflow_runs WHERE id = ?").get(input.runId);
584
420
  if (!run || run.status !== "active") {
585
421
  throw new UsageError(`Workflow run ${input.runId} is not active; refusing to reserve a durable dispatch attempt.`, "RESOURCE_ALREADY_EXISTS");
586
422
  }
587
- assertAttemptReservationLease(input, run);
588
423
  const latest = db
589
424
  .prepare(`SELECT * FROM workflow_run_unit_attempts
590
425
  WHERE run_id = ? AND unit_id = ?
591
426
  ORDER BY attempt DESC
592
427
  LIMIT 1`)
593
428
  .get(input.runId, input.unitId);
594
- if (latest?.status === "running") {
595
- if (latest.claim_holder === input.claimHolder) {
596
- return { kind: "existing", attempt: latest };
597
- }
598
- const currentRunLeaseDisplacedClaim = run.engine_lease_holder === input.claimHolder;
599
- const expired = latest.claim_expires_at < input.now;
600
- if (!expired && !currentRunLeaseDisplacedClaim) {
601
- return { kind: "busy", attempt: latest };
602
- }
603
- const reclaimed = db
604
- .prepare(`UPDATE workflow_run_unit_attempts
605
- SET claim_holder = ?, claim_expires_at = ?
606
- WHERE run_id = ? AND unit_id = ? AND attempt = ?
607
- AND status = 'running' AND dispatch_id = ? AND claim_holder = ?
608
- AND claim_expires_at = ?`)
609
- .run(input.claimHolder, input.claimExpiresAt, input.runId, input.unitId, latest.attempt, latest.dispatch_id, latest.claim_holder, latest.claim_expires_at);
610
- if (Number(reclaimed.changes) !== 1) {
611
- throw new Error(`Durable attempt ${input.unitId} changed while its displaced claim was reclaimed.`);
612
- }
613
- db.prepare(`UPDATE workflow_run_units
614
- SET claim_holder = ?, claim_expires_at = ?, last_checkin_at = ?
615
- WHERE run_id = ? AND unit_id = ? AND status = 'running'`).run(input.claimHolder, input.claimExpiresAt, input.now, input.runId, input.unitId);
616
- const attempt = db
617
- .prepare(`SELECT * FROM workflow_run_unit_attempts
618
- WHERE run_id = ? AND unit_id = ? AND attempt = ?`)
619
- .get(input.runId, input.unitId, latest.attempt);
620
- return { kind: "reclaimed", attempt };
621
- }
429
+ if (latest?.status === "running")
430
+ return { kind: "reclaimed", attempt: latest };
622
431
  const attemptNumber = (latest?.attempt ?? 0) + 1;
623
432
  const dispatchId = randomUUID();
624
433
  db.prepare(`INSERT INTO workflow_run_unit_attempts (
625
434
  run_id, unit_id, attempt, dispatch_id, step_id, node_id, phase,
626
435
  runner, engine, model, input_hash, status, worktree_path, started_at,
627
436
  claim_holder, claim_expires_at
628
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', ?, ?, ?, ?)`).run(input.runId, input.unitId, attemptNumber, dispatchId, input.stepId, input.nodeId, input.phase, input.runner, input.engine, input.model, input.inputHash, input.worktreePath ?? null, input.now, input.claimHolder, input.claimExpiresAt);
437
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', ?, ?, ?, ?)`).run(input.runId, input.unitId, attemptNumber, dispatchId, input.stepId, input.nodeId, input.phase, input.runner, input.engine, input.model, input.inputHash, input.worktreePath ?? null, input.now, holder, input.now);
629
438
  db.prepare(`INSERT INTO workflow_run_units (
630
439
  run_id, unit_id, step_id, node_id, parent_unit_id, phase, runner, engine, model,
631
440
  status, input_hash, worktree_path, started_at, claim_holder, claim_expires_at, attempts
@@ -649,8 +458,7 @@ export class WorkflowRunsRepository {
649
458
  failure_reason = NULL,
650
459
  session_id = NULL,
651
460
  finished_at = NULL,
652
- last_checkin_at = NULL,
653
- attempts = excluded.attempts`).run(input.runId, input.unitId, input.stepId, input.nodeId, input.parentUnitId ?? null, input.phase, input.runner, input.engine, input.model, input.inputHash, input.worktreePath ?? null, input.now, input.claimHolder, input.claimExpiresAt, attemptNumber);
461
+ attempts = excluded.attempts`).run(input.runId, input.unitId, input.stepId, input.nodeId, input.parentUnitId ?? null, input.phase, input.runner, input.engine, input.model, input.inputHash, input.worktreePath ?? null, input.now, holder, input.now, attemptNumber);
654
462
  insertEventStrict(db, {
655
463
  eventType: "workflow_unit_started",
656
464
  ts: input.now,
@@ -672,16 +480,19 @@ export class WorkflowRunsRepository {
672
480
  return { kind: "reserved", attempt };
673
481
  });
674
482
  }
675
- /** Commit one CAS-valid v4 terminal result, known usage, and finish event. */
483
+ /**
484
+ * Commit one terminal result, known usage, and finish event. Returns false
485
+ * when the attempt is no longer `running` (already finished) — a duplicate
486
+ * terminal callback never adds usage or a second event.
487
+ */
676
488
  finishUnitAttempt(input) {
677
489
  return this.immediateTransaction((db) => {
678
490
  const changed = db
679
491
  .prepare(`UPDATE workflow_run_unit_attempts
680
492
  SET status = ?, result_json = ?, tokens = ?, failure_reason = ?,
681
493
  session_id = ?, finished_at = ?
682
- WHERE run_id = ? AND unit_id = ? AND attempt = ? AND dispatch_id = ?
683
- AND claim_holder = ? AND status = 'running'`)
684
- .run(input.status, input.resultJson, input.tokens, input.failureReason, input.sessionId ?? null, input.finishedAt, input.runId, input.unitId, input.attempt, input.dispatchId, input.claimHolder);
494
+ WHERE run_id = ? AND unit_id = ? AND attempt = ? AND dispatch_id = ? AND status = 'running'`)
495
+ .run(input.status, input.resultJson, input.tokens, input.failureReason, input.sessionId ?? null, input.finishedAt, input.runId, input.unitId, input.attempt, input.dispatchId);
685
496
  if (Number(changed.changes) !== 1)
686
497
  return false;
687
498
  const attempt = db
@@ -692,9 +503,8 @@ export class WorkflowRunsRepository {
692
503
  .prepare(`UPDATE workflow_run_units
693
504
  SET status = ?, result_json = ?, tokens = ?, failure_reason = ?,
694
505
  session_id = ?, finished_at = ?
695
- WHERE run_id = ? AND unit_id = ? AND status = 'running'
696
- AND attempts = ? AND claim_holder = ?`)
697
- .run(input.status, input.resultJson, input.tokens, input.failureReason, input.sessionId ?? null, input.finishedAt, input.runId, input.unitId, input.attempt, input.claimHolder);
506
+ WHERE run_id = ? AND unit_id = ? AND status = 'running' AND attempts = ?`)
507
+ .run(input.status, input.resultJson, input.tokens, input.failureReason, input.sessionId ?? null, input.finishedAt, input.runId, input.unitId, input.attempt);
698
508
  if (Number(projection.changes) !== 1) {
699
509
  throw new Error(`Durable attempt ${input.unitId} has no matching live workflow_run_units projection.`);
700
510
  }
@@ -754,9 +564,8 @@ export class WorkflowRunsRepository {
754
564
  * - Inside a {@link withWorkflowRunsConnection} scope, the ambient handle is
755
565
  * BORROWED and left open for the rest of the scope. A wide `map` fan-out
756
566
  * therefore opens ONE connection for the whole step instead of two per unit
757
- * (insert + finish) — `openStateDatabase` registers a maintenance activity
758
- * lockfile and opens a read-only ledger-preflight handle on every call, so
759
- * the per-call cost is milliseconds, not microseconds.
567
+ * (insert + finish) — `openStateDatabase` opens a read-only ledger-preflight
568
+ * handle on every call, so the per-call cost is milliseconds, not microseconds.
760
569
  * - Outside a scope the behaviour is unchanged: open a fresh connection, run
761
570
  * `fn`, close it in a `finally`.
762
571
  *
@@ -789,7 +598,7 @@ export async function withWorkflowRunsRepo(fn) {
789
598
  * logically concurrent units cannot interleave statements on the shared handle
790
599
  * in a single-threaded event loop — sharing REMOVES in-process writer
791
600
  * contention instead of creating it. Cross-process arbitration (WAL,
792
- * `busy_timeout`, the run lease) is untouched. See `core/state-db-scope.ts` for
601
+ * `busy_timeout`, the per-run lock file) is untouched. See `core/state-db-scope.ts` for
793
602
  * the escaped-async-work guard.
794
603
  */
795
604
  export function withWorkflowRunsConnection(fn) {
@@ -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)