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
@@ -1,6 +1,10 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ import { rethrowIfTestIsolationError } from "../../core/errors.js";
5
+ import { formatRegistryError } from "../../core/registry-url.js";
6
+ import { warn } from "../../core/warn.js";
7
+ import { closeDatabase, openIndexDatabase } from "./index-connection.js";
4
8
  /**
5
9
  * Upsert a registry index cache entry in index.db.
6
10
  *
@@ -44,3 +48,99 @@ export function getRegistryIndexCache(db, registryUrl, maxAgeMs = 3_600_000 /* 1
44
48
  return undefined;
45
49
  return { indexJson: row.index_json, etag: row.etag, lastModified: row.last_modified };
46
50
  }
51
+ /**
52
+ * Open the cache DB (a failed open yields `db = undefined`; the bun-test
53
+ * isolation guard is re-thrown), run `fn`, and close the DB only after `fn`
54
+ * has fully settled — the callbacks are async, and closing earlier would tear
55
+ * the DB down mid-write.
56
+ */
57
+ export async function withRegistryCacheDb(fn) {
58
+ let db;
59
+ try {
60
+ db = openIndexDatabase();
61
+ }
62
+ catch (err) {
63
+ rethrowIfTestIsolationError(err);
64
+ db = undefined;
65
+ }
66
+ try {
67
+ return await fn(db);
68
+ }
69
+ finally {
70
+ if (db) {
71
+ try {
72
+ closeDatabase(db);
73
+ }
74
+ catch {
75
+ /* ignore */
76
+ }
77
+ }
78
+ }
79
+ }
80
+ /**
81
+ * Returns a fresh cache hit when present, otherwise fetches live (writing the
82
+ * result back best-effort), and falls back to a stale cache row when the
83
+ * fetch fails.
84
+ */
85
+ export async function fetchCachedJson(opts) {
86
+ const { cacheKey, ttlMs, parseCache, fetchFresh } = opts;
87
+ return withRegistryCacheDb(async (db) => {
88
+ let dbCacheResult;
89
+ try {
90
+ if (db) {
91
+ dbCacheResult = getRegistryIndexCache(db, cacheKey, ttlMs);
92
+ }
93
+ }
94
+ catch (err) {
95
+ // Never mask the bun-test isolation guard as "DB unavailable" — see
96
+ // rethrowIfTestIsolationError in src/core/errors.ts.
97
+ rethrowIfTestIsolationError(err);
98
+ // index.db read failed (pre-migration install or test env) — fall through
99
+ }
100
+ if (dbCacheResult) {
101
+ const cached = parseCache(dbCacheResult.indexJson, { stale: false });
102
+ if (cached !== undefined) {
103
+ return cached;
104
+ }
105
+ }
106
+ try {
107
+ const { value, cacheJson } = await fetchFresh();
108
+ if (db) {
109
+ try {
110
+ upsertRegistryIndexCache(db, cacheKey, cacheJson);
111
+ }
112
+ catch {
113
+ /* best-effort */
114
+ }
115
+ }
116
+ return value;
117
+ }
118
+ catch (err) {
119
+ // Fetch failed — use stale DB cache if available.
120
+ if (dbCacheResult) {
121
+ const stale = parseCache(dbCacheResult.indexJson, { stale: true });
122
+ if (stale !== undefined)
123
+ return stale;
124
+ }
125
+ // No in-TTL row — consult the cache PAST its TTL before giving up: a
126
+ // briefly unreachable registry should degrade to the last-known index,
127
+ // loudly, not hard-fail the command.
128
+ try {
129
+ const expiredRow = db ? getRegistryIndexCache(db, cacheKey, Number.POSITIVE_INFINITY) : undefined;
130
+ if (expiredRow) {
131
+ const stale = parseCache(expiredRow.indexJson, { stale: true });
132
+ if (stale !== undefined) {
133
+ warn(`Registry fetch failed (${formatRegistryError(err)}); ` +
134
+ "serving the last cached index, which is past its refresh interval.");
135
+ return stale;
136
+ }
137
+ }
138
+ }
139
+ catch (cacheErr) {
140
+ rethrowIfTestIsolationError(cacheErr);
141
+ // cache read failed — fall through to the original fetch error
142
+ }
143
+ throw err;
144
+ }
145
+ });
146
+ }
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import { isRecord } from "../../core/common.js";
13
13
  import { warnOnce } from "../../core/warn.js";
14
+ import { escapeLikePattern } from "../like-pattern.js";
14
15
  function metadataError(message) {
15
16
  throw new Error(`invalid task_history metadata_json: ${message}`);
16
17
  }
@@ -63,9 +64,9 @@ export function decodeTaskHistoryMetadata(input) {
63
64
  if (typeof parsed.durationMs !== "number")
64
65
  metadataError("durationMs must be a number");
65
66
  const detail = "detail" in parsed ? parsed.detail : null;
66
- if (parsed.engine !== undefined && parsed.engine !== null && typeof parsed.engine !== "string") {
67
- metadataError("engine must be a string or null");
68
- }
67
+ // A malformed `engine` (wrong type) is dropped like any other unrecognized
68
+ // field rather than rejecting the whole row over a non-essential value.
69
+ const engine = parsed.engine === null || typeof parsed.engine === "string" ? parsed.engine : undefined;
69
70
  if (parsed.targetVocab !== undefined && parsed.targetVocab !== 2) {
70
71
  warnOnce(`task-history-target-vocab:${String(parsed.targetVocab)}`, `task_history row has targetVocab ${String(parsed.targetVocab)}, newer than this akm's 2 — falling back to ` +
71
72
  "the legacy target_kind mapping rather than rejecting the row.");
@@ -83,7 +84,7 @@ export function decodeTaskHistoryMetadata(input) {
83
84
  metadataVersion: 2,
84
85
  durationMs: parsed.durationMs,
85
86
  detail: cleanDetail,
86
- ...(parsed.engine !== undefined ? { engine: parsed.engine } : {}),
87
+ ...(engine !== undefined ? { engine } : {}),
87
88
  ...(parsed.targetVocab === 2 ? { targetVocab: 2 } : {}),
88
89
  };
89
90
  }
@@ -207,3 +208,24 @@ export function queryCompletedTaskIntervals(db, since, until) {
207
208
  : "SELECT started_at, completed_at FROM task_history WHERE task_id = 'akm-improve' AND started_at >= ? AND completed_at IS NOT NULL ORDER BY started_at";
208
209
  return (until ? db.prepare(sql).all(since, until) : db.prepare(sql).all(since));
209
210
  }
211
+ /**
212
+ * Preview counterpart of {@link renameTaskHistoryTargetRefs} for `akm bundle
213
+ * rename --dry-run`: the same `WHERE` predicate, read-only.
214
+ */
215
+ export function countTaskHistoryTargetRefs(db, oldBundleId) {
216
+ const prefix = `${escapeLikePattern(oldBundleId)}//`;
217
+ return db.prepare(`SELECT COUNT(*) AS n FROM task_history WHERE target_ref LIKE ? ESCAPE '\\'`).get(`${prefix}%`).n;
218
+ }
219
+ /**
220
+ * Rewrite every `task_history.target_ref` naming `oldBundleId` (a workflow
221
+ * run's fully-qualified `<bundle>//conceptId` target) to `newBundleId`
222
+ * (`akm bundle rename`, D6). A row whose `target_ref` is unset or not
223
+ * bundle-qualified is not matched by the `LIKE` prefix and is left alone.
224
+ * Returns the number of rows rewritten.
225
+ */
226
+ export function renameTaskHistoryTargetRefs(db, oldBundleId, newBundleId) {
227
+ const prefix = `${escapeLikePattern(oldBundleId)}//`;
228
+ return Number(db
229
+ .prepare(`UPDATE task_history SET target_ref = ? || substr(target_ref, ?) WHERE target_ref LIKE ? ESCAPE '\\'`)
230
+ .run(newBundleId, oldBundleId.length + 1, `${prefix}%`).changes);
231
+ }
@@ -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) {