@cleocode/core 2026.6.11 → 2026.6.13

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 (414) hide show
  1. package/dist/conduit/local-transport.d.ts +12 -0
  2. package/dist/conduit/local-transport.d.ts.map +1 -1
  3. package/dist/conduit/local-transport.js +53 -23
  4. package/dist/conduit/local-transport.js.map +1 -1
  5. package/dist/crypto/credentials.d.ts +37 -0
  6. package/dist/crypto/credentials.d.ts.map +1 -1
  7. package/dist/crypto/credentials.js +110 -0
  8. package/dist/crypto/credentials.js.map +1 -1
  9. package/dist/dispatch/contracts/derive-output-contract.d.ts +86 -0
  10. package/dist/dispatch/contracts/derive-output-contract.d.ts.map +1 -0
  11. package/dist/dispatch/contracts/derive-output-contract.js +347 -0
  12. package/dist/dispatch/contracts/derive-output-contract.js.map +1 -0
  13. package/dist/dispatch/contracts/ensures-schema.d.ts +77 -0
  14. package/dist/dispatch/contracts/ensures-schema.d.ts.map +1 -0
  15. package/dist/dispatch/contracts/ensures-schema.js +91 -0
  16. package/dist/dispatch/contracts/ensures-schema.js.map +1 -0
  17. package/dist/dispatch/contracts/output-contracts.d.ts +27 -5
  18. package/dist/dispatch/contracts/output-contracts.d.ts.map +1 -1
  19. package/dist/dispatch/contracts/output-contracts.js +29 -6
  20. package/dist/dispatch/contracts/output-contracts.js.map +1 -1
  21. package/dist/docs/export-document.js +2309 -1262
  22. package/dist/docs/export-document.js.map +3 -3
  23. package/dist/go/driver.d.ts +132 -7
  24. package/dist/go/driver.d.ts.map +1 -1
  25. package/dist/go/driver.js +135 -10
  26. package/dist/go/driver.js.map +1 -1
  27. package/dist/go/index.d.ts +1 -1
  28. package/dist/go/index.d.ts.map +1 -1
  29. package/dist/go/index.js +1 -1
  30. package/dist/go/index.js.map +1 -1
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +5 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/internal.d.ts +13 -10
  36. package/dist/internal.d.ts.map +1 -1
  37. package/dist/internal.js +11 -11
  38. package/dist/internal.js.map +1 -1
  39. package/dist/lifecycle/engine-ops.d.ts.map +1 -1
  40. package/dist/lifecycle/engine-ops.js +7 -4
  41. package/dist/lifecycle/engine-ops.js.map +1 -1
  42. package/dist/lifecycle/ivtr-loop.d.ts +108 -183
  43. package/dist/lifecycle/ivtr-loop.d.ts.map +1 -1
  44. package/dist/lifecycle/ivtr-loop.js +207 -522
  45. package/dist/lifecycle/ivtr-loop.js.map +1 -1
  46. package/dist/llm/auxiliary-fallback.js +1 -1
  47. package/dist/llm/auxiliary-fallback.js.map +1 -1
  48. package/dist/llm/cli-ops.d.ts +14 -1
  49. package/dist/llm/cli-ops.d.ts.map +1 -1
  50. package/dist/llm/cli-ops.js +41 -12
  51. package/dist/llm/cli-ops.js.map +1 -1
  52. package/dist/llm/concrete-session.d.ts.map +1 -1
  53. package/dist/llm/concrete-session.js +4 -0
  54. package/dist/llm/concrete-session.js.map +1 -1
  55. package/dist/llm/credentials.d.ts +35 -0
  56. package/dist/llm/credentials.d.ts.map +1 -1
  57. package/dist/llm/credentials.js +49 -0
  58. package/dist/llm/credentials.js.map +1 -1
  59. package/dist/llm/index.d.ts +4 -0
  60. package/dist/llm/index.d.ts.map +1 -1
  61. package/dist/llm/index.js +4 -0
  62. package/dist/llm/index.js.map +1 -1
  63. package/dist/llm/pi/gondolin-loader.d.ts +241 -0
  64. package/dist/llm/pi/gondolin-loader.d.ts.map +1 -0
  65. package/dist/llm/pi/gondolin-loader.js +195 -0
  66. package/dist/llm/pi/gondolin-loader.js.map +1 -0
  67. package/dist/llm/pi/index.d.ts +28 -0
  68. package/dist/llm/pi/index.d.ts.map +1 -0
  69. package/dist/llm/pi/index.js +27 -0
  70. package/dist/llm/pi/index.js.map +1 -0
  71. package/dist/llm/pi/pi-agent-adapter.d.ts +135 -0
  72. package/dist/llm/pi/pi-agent-adapter.d.ts.map +1 -0
  73. package/dist/llm/pi/pi-agent-adapter.js +329 -0
  74. package/dist/llm/pi/pi-agent-adapter.js.map +1 -0
  75. package/dist/llm/pi/pi-errors.d.ts +161 -0
  76. package/dist/llm/pi/pi-errors.d.ts.map +1 -0
  77. package/dist/llm/pi/pi-errors.js +296 -0
  78. package/dist/llm/pi/pi-errors.js.map +1 -0
  79. package/dist/llm/pi/pi-execution-env.d.ts +286 -0
  80. package/dist/llm/pi/pi-execution-env.d.ts.map +1 -0
  81. package/dist/llm/pi/pi-execution-env.js +406 -0
  82. package/dist/llm/pi/pi-execution-env.js.map +1 -0
  83. package/dist/llm/pi/pi-gondolin-env.d.ts +227 -0
  84. package/dist/llm/pi/pi-gondolin-env.d.ts.map +1 -0
  85. package/dist/llm/pi/pi-gondolin-env.js +541 -0
  86. package/dist/llm/pi/pi-gondolin-env.js.map +1 -0
  87. package/dist/llm/pi/pi-session-storage.d.ts +154 -0
  88. package/dist/llm/pi/pi-session-storage.d.ts.map +1 -0
  89. package/dist/llm/pi/pi-session-storage.js +314 -0
  90. package/dist/llm/pi/pi-session-storage.js.map +1 -0
  91. package/dist/llm/pi/pi-stream-fn.d.ts +124 -0
  92. package/dist/llm/pi/pi-stream-fn.d.ts.map +1 -0
  93. package/dist/llm/pi/pi-stream-fn.js +504 -0
  94. package/dist/llm/pi/pi-stream-fn.js.map +1 -0
  95. package/dist/llm/pi/pi-tool-bridge.d.ts +99 -0
  96. package/dist/llm/pi/pi-tool-bridge.d.ts.map +1 -0
  97. package/dist/llm/pi/pi-tool-bridge.js +117 -0
  98. package/dist/llm/pi/pi-tool-bridge.js.map +1 -0
  99. package/dist/llm/pi/pi-types.d.ts +56 -0
  100. package/dist/llm/pi/pi-types.d.ts.map +1 -0
  101. package/dist/llm/pi/pi-types.js +16 -0
  102. package/dist/llm/pi/pi-types.js.map +1 -0
  103. package/dist/llm/pi/resolve-execution-env.d.ts +194 -0
  104. package/dist/llm/pi/resolve-execution-env.d.ts.map +1 -0
  105. package/dist/llm/pi/resolve-execution-env.js +132 -0
  106. package/dist/llm/pi/resolve-execution-env.js.map +1 -0
  107. package/dist/llm/plugin-facade.js +2253 -1151
  108. package/dist/llm/plugin-facade.js.map +3 -3
  109. package/dist/llm/role-executor.d.ts.map +1 -1
  110. package/dist/llm/role-executor.js +8 -5
  111. package/dist/llm/role-executor.js.map +1 -1
  112. package/dist/llm/role-resolver.d.ts +46 -14
  113. package/dist/llm/role-resolver.d.ts.map +1 -1
  114. package/dist/llm/role-resolver.js +178 -20
  115. package/dist/llm/role-resolver.js.map +1 -1
  116. package/dist/llm/sealed-credential.d.ts +125 -0
  117. package/dist/llm/sealed-credential.d.ts.map +1 -0
  118. package/dist/llm/sealed-credential.js +133 -0
  119. package/dist/llm/sealed-credential.js.map +1 -0
  120. package/dist/llm/session-factory.d.ts.map +1 -1
  121. package/dist/llm/session-factory.js +6 -2
  122. package/dist/llm/session-factory.js.map +1 -1
  123. package/dist/llm/system-key.d.ts +67 -0
  124. package/dist/llm/system-key.d.ts.map +1 -0
  125. package/dist/llm/system-key.js +87 -0
  126. package/dist/llm/system-key.js.map +1 -0
  127. package/dist/llm/system-of-use-registry.d.ts +119 -0
  128. package/dist/llm/system-of-use-registry.d.ts.map +1 -0
  129. package/dist/llm/system-of-use-registry.js +200 -0
  130. package/dist/llm/system-of-use-registry.js.map +1 -0
  131. package/dist/llm/system-resolver.d.ts +32 -10
  132. package/dist/llm/system-resolver.d.ts.map +1 -1
  133. package/dist/llm/system-resolver.js +70 -18
  134. package/dist/llm/system-resolver.js.map +1 -1
  135. package/dist/memory/brain-writer-thread.d.ts.map +1 -1
  136. package/dist/memory/brain-writer-thread.js +116 -11
  137. package/dist/memory/brain-writer-thread.js.map +1 -1
  138. package/dist/memory/claude-mem-migration.d.ts.map +1 -1
  139. package/dist/memory/claude-mem-migration.js +21 -0
  140. package/dist/memory/claude-mem-migration.js.map +1 -1
  141. package/dist/memory/graph-memory-bridge.d.ts.map +1 -1
  142. package/dist/memory/graph-memory-bridge.js +32 -25
  143. package/dist/memory/graph-memory-bridge.js.map +1 -1
  144. package/dist/memory/llm-backend-resolver.js +23 -5
  145. package/dist/memory/llm-backend-resolver.js.map +1 -1
  146. package/dist/nexus/migrations/M-split-graph.d.ts.map +1 -1
  147. package/dist/nexus/migrations/M-split-graph.js +60 -53
  148. package/dist/nexus/migrations/M-split-graph.js.map +1 -1
  149. package/dist/orchestrate/pivot.d.ts.map +1 -1
  150. package/dist/orchestrate/pivot.js +20 -2
  151. package/dist/orchestrate/pivot.js.map +1 -1
  152. package/dist/release/engine-ops.js +2 -2
  153. package/dist/release/engine-ops.js.map +1 -1
  154. package/dist/release/plan.d.ts +4 -6
  155. package/dist/release/plan.d.ts.map +1 -1
  156. package/dist/release/plan.js +10 -23
  157. package/dist/release/plan.js.map +1 -1
  158. package/dist/release/reconcile.d.ts +0 -2
  159. package/dist/release/reconcile.d.ts.map +1 -1
  160. package/dist/release/reconcile.js +43 -50
  161. package/dist/release/reconcile.js.map +1 -1
  162. package/dist/release/release-manifest.d.ts.map +1 -1
  163. package/dist/release/release-manifest.js +4 -1
  164. package/dist/release/release-manifest.js.map +1 -1
  165. package/dist/release/verify-provenance.js +20 -20
  166. package/dist/release/verify-provenance.js.map +1 -1
  167. package/dist/routing/capability-matrix.d.ts.map +1 -1
  168. package/dist/routing/capability-matrix.js +0 -57
  169. package/dist/routing/capability-matrix.js.map +1 -1
  170. package/dist/selfimprove/budget.d.ts +206 -0
  171. package/dist/selfimprove/budget.d.ts.map +1 -0
  172. package/dist/selfimprove/budget.js +203 -0
  173. package/dist/selfimprove/budget.js.map +1 -0
  174. package/dist/selfimprove/dhq-adapter.d.ts +105 -0
  175. package/dist/selfimprove/dhq-adapter.d.ts.map +1 -0
  176. package/dist/selfimprove/dhq-adapter.js +95 -0
  177. package/dist/selfimprove/dhq-adapter.js.map +1 -0
  178. package/dist/selfimprove/draft-pr.d.ts +129 -0
  179. package/dist/selfimprove/draft-pr.d.ts.map +1 -0
  180. package/dist/selfimprove/draft-pr.js +158 -0
  181. package/dist/selfimprove/draft-pr.js.map +1 -0
  182. package/dist/selfimprove/envelope-diff.d.ts +162 -0
  183. package/dist/selfimprove/envelope-diff.d.ts.map +1 -0
  184. package/dist/selfimprove/envelope-diff.js +321 -0
  185. package/dist/selfimprove/envelope-diff.js.map +1 -0
  186. package/dist/selfimprove/replay.d.ts +121 -0
  187. package/dist/selfimprove/replay.d.ts.map +1 -0
  188. package/dist/selfimprove/replay.js +109 -0
  189. package/dist/selfimprove/replay.js.map +1 -0
  190. package/dist/selfimprove/run-loop.d.ts +148 -0
  191. package/dist/selfimprove/run-loop.d.ts.map +1 -0
  192. package/dist/selfimprove/run-loop.js +240 -0
  193. package/dist/selfimprove/run-loop.js.map +1 -0
  194. package/dist/selfimprove/scenario.d.ts +145 -0
  195. package/dist/selfimprove/scenario.d.ts.map +1 -0
  196. package/dist/selfimprove/scenario.js +243 -0
  197. package/dist/selfimprove/scenario.js.map +1 -0
  198. package/dist/sentient/daemon-entry.js +5 -0
  199. package/dist/sentient/daemon-entry.js.map +1 -1
  200. package/dist/sentient/daemon.d.ts.map +1 -1
  201. package/dist/sentient/daemon.js +19 -0
  202. package/dist/sentient/daemon.js.map +1 -1
  203. package/dist/sentient/hygiene-scan.d.ts.map +1 -1
  204. package/dist/sentient/hygiene-scan.js +14 -6
  205. package/dist/sentient/hygiene-scan.js.map +1 -1
  206. package/dist/sessions/briefing.d.ts.map +1 -1
  207. package/dist/sessions/briefing.js +13 -9
  208. package/dist/sessions/briefing.js.map +1 -1
  209. package/dist/sessions/connection-session-handle.d.ts +161 -0
  210. package/dist/sessions/connection-session-handle.d.ts.map +1 -0
  211. package/dist/sessions/connection-session-handle.js +168 -0
  212. package/dist/sessions/connection-session-handle.js.map +1 -0
  213. package/dist/sessions/handoff.d.ts.map +1 -1
  214. package/dist/sessions/handoff.js +9 -0
  215. package/dist/sessions/handoff.js.map +1 -1
  216. package/dist/sessions/index.d.ts +4 -1
  217. package/dist/sessions/index.d.ts.map +1 -1
  218. package/dist/sessions/index.js +31 -1
  219. package/dist/sessions/index.js.map +1 -1
  220. package/dist/sessions/session-id.d.ts +34 -0
  221. package/dist/sessions/session-id.d.ts.map +1 -1
  222. package/dist/sessions/session-id.js +37 -0
  223. package/dist/sessions/session-id.js.map +1 -1
  224. package/dist/sessions/session-manifest-mirror.d.ts +115 -0
  225. package/dist/sessions/session-manifest-mirror.d.ts.map +1 -0
  226. package/dist/sessions/session-manifest-mirror.js +250 -0
  227. package/dist/sessions/session-manifest-mirror.js.map +1 -0
  228. package/dist/sessions/session-switch.d.ts.map +1 -1
  229. package/dist/sessions/session-switch.js +5 -2
  230. package/dist/sessions/session-switch.js.map +1 -1
  231. package/dist/store/converters.d.ts.map +1 -1
  232. package/dist/store/converters.js +2 -0
  233. package/dist/store/converters.js.map +1 -1
  234. package/dist/store/db-helpers.d.ts.map +1 -1
  235. package/dist/store/db-helpers.js +2 -0
  236. package/dist/store/db-helpers.js.map +1 -1
  237. package/dist/store/dual-scope-db.d.ts.map +1 -1
  238. package/dist/store/dual-scope-db.js +87 -47
  239. package/dist/store/dual-scope-db.js.map +1 -1
  240. package/dist/store/pi-session-store.d.ts +151 -0
  241. package/dist/store/pi-session-store.d.ts.map +1 -0
  242. package/dist/store/pi-session-store.js +166 -0
  243. package/dist/store/pi-session-store.js.map +1 -0
  244. package/dist/store/safety-data-accessor.d.ts +1 -0
  245. package/dist/store/safety-data-accessor.d.ts.map +1 -1
  246. package/dist/store/safety-data-accessor.js +5 -0
  247. package/dist/store/safety-data-accessor.js.map +1 -1
  248. package/dist/store/schema/cleo-global/accounts.d.ts +357 -0
  249. package/dist/store/schema/cleo-global/accounts.d.ts.map +1 -0
  250. package/dist/store/schema/cleo-global/accounts.js +136 -0
  251. package/dist/store/schema/cleo-global/accounts.js.map +1 -0
  252. package/dist/store/schema/cleo-global/index.d.ts +2 -0
  253. package/dist/store/schema/cleo-global/index.d.ts.map +1 -1
  254. package/dist/store/schema/cleo-global/index.js +2 -0
  255. package/dist/store/schema/cleo-global/index.js.map +1 -1
  256. package/dist/store/schema/cleo-global/session-manifest.d.ts +217 -0
  257. package/dist/store/schema/cleo-global/session-manifest.d.ts.map +1 -0
  258. package/dist/store/schema/cleo-global/session-manifest.js +99 -0
  259. package/dist/store/schema/cleo-global/session-manifest.js.map +1 -0
  260. package/dist/store/schema/cleo-project/tasks-core.d.ts +16 -0
  261. package/dist/store/schema/cleo-project/tasks-core.d.ts.map +1 -1
  262. package/dist/store/schema/cleo-project/tasks-core.js +14 -0
  263. package/dist/store/schema/cleo-project/tasks-core.js.map +1 -1
  264. package/dist/store/schema/tasks.d.ts +16 -0
  265. package/dist/store/schema/tasks.d.ts.map +1 -1
  266. package/dist/store/schema/tasks.js +10 -0
  267. package/dist/store/schema/tasks.js.map +1 -1
  268. package/dist/store/selfimprove-dhq-schema.d.ts +284 -0
  269. package/dist/store/selfimprove-dhq-schema.d.ts.map +1 -0
  270. package/dist/store/selfimprove-dhq-schema.js +128 -0
  271. package/dist/store/selfimprove-dhq-schema.js.map +1 -0
  272. package/dist/store/selfimprove-dhq-store.d.ts +175 -0
  273. package/dist/store/selfimprove-dhq-store.d.ts.map +1 -0
  274. package/dist/store/selfimprove-dhq-store.js +182 -0
  275. package/dist/store/selfimprove-dhq-store.js.map +1 -0
  276. package/dist/store/session-store.d.ts +52 -13
  277. package/dist/store/session-store.d.ts.map +1 -1
  278. package/dist/store/session-store.js +71 -13
  279. package/dist/store/session-store.js.map +1 -1
  280. package/dist/store/sqlite-data-accessor.d.ts.map +1 -1
  281. package/dist/store/sqlite-data-accessor.js +7 -0
  282. package/dist/store/sqlite-data-accessor.js.map +1 -1
  283. package/dist/store/tasks-schema.d.ts +4 -0
  284. package/dist/store/tasks-schema.d.ts.map +1 -1
  285. package/dist/store/tasks-schema.js +14 -0
  286. package/dist/store/tasks-schema.js.map +1 -1
  287. package/dist/store/umbrella-data-accessor.d.ts +1 -0
  288. package/dist/store/umbrella-data-accessor.d.ts.map +1 -1
  289. package/dist/store/umbrella-data-accessor.js +5 -0
  290. package/dist/store/umbrella-data-accessor.js.map +1 -1
  291. package/dist/store/validation-schemas.d.ts +94 -62
  292. package/dist/store/validation-schemas.d.ts.map +1 -1
  293. package/dist/store/writer-lease-schema.d.ts +392 -0
  294. package/dist/store/writer-lease-schema.d.ts.map +1 -0
  295. package/dist/store/writer-lease-schema.js +142 -0
  296. package/dist/store/writer-lease-schema.js.map +1 -0
  297. package/dist/store/writer-lease.d.ts +368 -0
  298. package/dist/store/writer-lease.d.ts.map +1 -0
  299. package/dist/store/writer-lease.js +1002 -0
  300. package/dist/store/writer-lease.js.map +1 -0
  301. package/dist/task-tools/describe-schema.d.ts +4 -2
  302. package/dist/task-tools/describe-schema.d.ts.map +1 -1
  303. package/dist/task-tools/describe-schema.js +9 -3
  304. package/dist/task-tools/describe-schema.js.map +1 -1
  305. package/dist/tasks/backfill-child-projections.d.ts.map +1 -1
  306. package/dist/tasks/backfill-child-projections.js +14 -9
  307. package/dist/tasks/backfill-child-projections.js.map +1 -1
  308. package/dist/tasks/complete.d.ts.map +1 -1
  309. package/dist/tasks/complete.js +7 -10
  310. package/dist/tasks/complete.js.map +1 -1
  311. package/dist/tasks/duplicate-detector.d.ts.map +1 -1
  312. package/dist/tasks/duplicate-detector.js +14 -7
  313. package/dist/tasks/duplicate-detector.js.map +1 -1
  314. package/dist/telemetry/index.d.ts.map +1 -1
  315. package/dist/telemetry/index.js +10 -4
  316. package/dist/telemetry/index.js.map +1 -1
  317. package/dist/tools/agent-registry.d.ts +250 -0
  318. package/dist/tools/agent-registry.d.ts.map +1 -0
  319. package/dist/tools/agent-registry.js +292 -0
  320. package/dist/tools/agent-registry.js.map +1 -0
  321. package/dist/tools/agent-tool-families.d.ts +107 -0
  322. package/dist/tools/agent-tool-families.d.ts.map +1 -0
  323. package/dist/tools/agent-tool-families.js +527 -0
  324. package/dist/tools/agent-tool-families.js.map +1 -0
  325. package/dist/tools/browser-driver.d.ts +183 -0
  326. package/dist/tools/browser-driver.d.ts.map +1 -0
  327. package/dist/tools/browser-driver.js +242 -0
  328. package/dist/tools/browser-driver.js.map +1 -0
  329. package/dist/tools/builtin-agent-tools.d.ts +36 -0
  330. package/dist/tools/builtin-agent-tools.d.ts.map +1 -0
  331. package/dist/tools/builtin-agent-tools.js +150 -0
  332. package/dist/tools/builtin-agent-tools.js.map +1 -0
  333. package/dist/tools/dispatch.d.ts +370 -0
  334. package/dist/tools/dispatch.d.ts.map +1 -0
  335. package/dist/tools/dispatch.js +505 -0
  336. package/dist/tools/dispatch.js.map +1 -0
  337. package/dist/tools/env-scrub.d.ts +93 -0
  338. package/dist/tools/env-scrub.d.ts.map +1 -0
  339. package/dist/tools/env-scrub.js +176 -0
  340. package/dist/tools/env-scrub.js.map +1 -0
  341. package/dist/tools/fs.d.ts +30 -0
  342. package/dist/tools/fs.d.ts.map +1 -1
  343. package/dist/tools/fs.js +55 -2
  344. package/dist/tools/fs.js.map +1 -1
  345. package/dist/tools/guard.d.ts +15 -0
  346. package/dist/tools/guard.d.ts.map +1 -1
  347. package/dist/tools/guard.js +49 -12
  348. package/dist/tools/guard.js.map +1 -1
  349. package/dist/tools/index.d.ts +10 -0
  350. package/dist/tools/index.d.ts.map +1 -1
  351. package/dist/tools/index.js +37 -0
  352. package/dist/tools/index.js.map +1 -1
  353. package/dist/tools/pty.d.ts +45 -0
  354. package/dist/tools/pty.d.ts.map +1 -0
  355. package/dist/tools/pty.js +166 -0
  356. package/dist/tools/pty.js.map +1 -0
  357. package/dist/tools/schema-gen.d.ts +57 -0
  358. package/dist/tools/schema-gen.d.ts.map +1 -0
  359. package/dist/tools/schema-gen.js +52 -0
  360. package/dist/tools/schema-gen.js.map +1 -0
  361. package/dist/tools/shell.d.ts +12 -0
  362. package/dist/tools/shell.d.ts.map +1 -1
  363. package/dist/tools/shell.js +14 -1
  364. package/dist/tools/shell.js.map +1 -1
  365. package/dist/tools/web-agent-tools.d.ts +72 -0
  366. package/dist/tools/web-agent-tools.d.ts.map +1 -0
  367. package/dist/tools/web-agent-tools.js +393 -0
  368. package/dist/tools/web-agent-tools.js.map +1 -0
  369. package/dist/tools/web-search-backends.d.ts +156 -0
  370. package/dist/tools/web-search-backends.d.ts.map +1 -0
  371. package/dist/tools/web-search-backends.js +290 -0
  372. package/dist/tools/web-search-backends.js.map +1 -0
  373. package/dist/validation/ops.d.ts +1 -15
  374. package/dist/validation/ops.d.ts.map +1 -1
  375. package/dist/validation/ops.js +0 -20
  376. package/dist/validation/ops.js.map +1 -1
  377. package/migrations/drizzle-cleo-global/20260607000000_t11891-writer-leases/migration.sql +55 -0
  378. package/migrations/drizzle-cleo-global/20260607010000_t11639-session-manifest/migration.sql +43 -0
  379. package/migrations/drizzle-cleo-global/20260608000000_t11899-pi-session-tree/migration.sql +52 -0
  380. package/migrations/drizzle-cleo-global/20260608010000_t11709-accounts-credential-pool/migration.sql +72 -0
  381. package/migrations/drizzle-cleo-global/20260608020000_t11889-selfimprove-dhq/migration.sql +64 -0
  382. package/migrations/drizzle-cleo-project/20260606000000_t11884-restore-prefixed-task-triggers/migration.sql +272 -0
  383. package/migrations/drizzle-cleo-project/20260606000000_t11884-restore-prefixed-task-triggers/revert.sql +29 -0
  384. package/migrations/drizzle-cleo-project/20260606000100_t11883-tasks-releases-version-unique/migration.sql +21 -0
  385. package/migrations/drizzle-cleo-project/20260606000100_t11883-tasks-releases-version-unique/revert.sql +4 -0
  386. package/migrations/drizzle-cleo-project/20260607000000_t11891-writer-leases/migration.sql +55 -0
  387. package/migrations/drizzle-cleo-project/20260607010000_t11639-session-parent/migration.sql +22 -0
  388. package/migrations/drizzle-cleo-project/20260608000000_t11899-pi-session-tree/migration.sql +52 -0
  389. package/migrations/drizzle-cleo-project/20260608020000_t11889-selfimprove-dhq/migration.sql +64 -0
  390. package/package.json +15 -13
  391. package/dist/lifecycle/chain-composition.d.ts +0 -30
  392. package/dist/lifecycle/chain-composition.d.ts.map +0 -1
  393. package/dist/lifecycle/chain-composition.js +0 -152
  394. package/dist/lifecycle/chain-composition.js.map +0 -1
  395. package/dist/lifecycle/chain-store.d.ts +0 -70
  396. package/dist/lifecycle/chain-store.d.ts.map +0 -1
  397. package/dist/lifecycle/chain-store.js +0 -246
  398. package/dist/lifecycle/chain-store.js.map +0 -1
  399. package/dist/lifecycle/default-chain.d.ts +0 -39
  400. package/dist/lifecycle/default-chain.d.ts.map +0 -1
  401. package/dist/lifecycle/default-chain.js +0 -176
  402. package/dist/lifecycle/default-chain.js.map +0 -1
  403. package/dist/lifecycle/tessera-engine.d.ts +0 -48
  404. package/dist/lifecycle/tessera-engine.d.ts.map +0 -1
  405. package/dist/lifecycle/tessera-engine.js +0 -249
  406. package/dist/lifecycle/tessera-engine.js.map +0 -1
  407. package/dist/release/provenance-fk.d.ts +0 -74
  408. package/dist/release/provenance-fk.d.ts.map +0 -1
  409. package/dist/release/provenance-fk.js +0 -122
  410. package/dist/release/provenance-fk.js.map +0 -1
  411. package/dist/validation/chain-validation.d.ts +0 -43
  412. package/dist/validation/chain-validation.d.ts.map +0 -1
  413. package/dist/validation/chain-validation.js +0 -146
  414. package/dist/validation/chain-validation.js.map +0 -1
@@ -0,0 +1,1002 @@
1
+ /**
2
+ * **DbWriterLease** — single in-process writer-lease arbitration over a persisted
3
+ * SQLite row (T11627 ST-2 · local-mode engine).
4
+ *
5
+ * Heals the T5158 multi-writer corruption (`E_NOT_INITIALIZED` / `E_INTERNAL` on
6
+ * the consolidated `cleo.db`) **while the supervisor daemon stays DISABLED in
7
+ * production** by serializing writers through a `BEGIN IMMEDIATE` claim transaction
8
+ * against a persisted `_writer_leases` row, fenced by an epoch-CAS. The supervisor
9
+ * (ST-5, daemon-on) and Node (this module, daemon-off) share the SAME arbitration
10
+ * primitive and row format — IPC is a coordination optimization over the persisted
11
+ * source-of-truth, never a second source.
12
+ *
13
+ * ## Modes ({@link resolveLeaseMode})
14
+ *
15
+ * `CLEO_WRITER_LEASE_MODE` ∈ `{ supervisor | local | off | require }`, default
16
+ * `local`:
17
+ *
18
+ * - `local` — Node arbitrates the row directly. No IPC. **Heals T5158 with the
19
+ * daemon off** (the shipping config). DEFAULT.
20
+ * - `supervisor` — would prefer IPC to the supervisor; in ST-2 (no IPC client
21
+ * wired) it transparently behaves as `local`, logging the demotion once.
22
+ * - `off` — pure pass-through: {@link withWriterLease} just runs `fn` under the
23
+ * existing `busy_timeout=30000`. Byte-identical to pre-lease behaviour. Rollback
24
+ * escape hatch.
25
+ * - `require` — strict: an acquire that cannot take the row throws
26
+ * `E_LEASE_UNAVAILABLE`. No fallback.
27
+ *
28
+ * `busy_timeout=30000` (SSoT in `specs/sqlite-pragmas.json`) backstops every
29
+ * `BEGIN IMMEDIATE`, so a contended claim degrades to today's bounded wait rather
30
+ * than a hang in any mode.
31
+ *
32
+ * ## Re-entrancy
33
+ *
34
+ * A process-local grant memo (`Map<\`${scope}::${lane}\`, { handle, refcount }>`)
35
+ * memoizes the active grant so a nested same-lane write in the SAME process shares
36
+ * one lease (`refcount++` + the row's `reentrancy_depth++`) instead of re-running
37
+ * the claim txn — no second `BEGIN IMMEDIATE`, no IPC. `release()` decrements; the
38
+ * row is freed (`active = 0`) at depth 0.
39
+ *
40
+ * ## DB Open Guard (Gate 3)
41
+ *
42
+ * This module lives under `packages/core/src/store/**`, which is inside the Gate-3
43
+ * raw-open allowlist (the DB chokepoint). It obtains the native handle via
44
+ * {@link openDualScopeDb} (no new raw `new DatabaseSync(`) and runs raw
45
+ * `BEGIN IMMEDIATE` claim/reclaim/release transactions on it.
46
+ *
47
+ * ## Cold-open seam (T11627 ST-3 · Seam 0 — the T5158 heal)
48
+ *
49
+ * {@link withColdOpenLease} leases the `dual-scope-db.ts` cold-open critical
50
+ * section (`reconcileJournal` + `migrateWithRetry` + the exodus-on-open hook)
51
+ * against the SAME native handle the open just created — it does NOT route through
52
+ * the default resolver (which would re-enter `openDualScopeDb` and recurse). It
53
+ * idempotently bootstraps the lease tables on that handle FIRST (the full
54
+ * migration that creates them runs INSIDE the leased section), so exactly one
55
+ * process per scope runs the migrate/reconcile write-txn while peers
56
+ * `BEGIN IMMEDIATE`-queue and then observe a ready DB. This is the heal that ships
57
+ * with the supervisor daemon disabled.
58
+ *
59
+ * @module
60
+ * @task T11627
61
+ * @epic T11625
62
+ * @see ./writer-lease-schema.ts — the drizzle table decls + bootstrap index assertion
63
+ * @see ./dual-scope-db.ts — the open chokepoint this engine routes through (Seams 0 & 1)
64
+ */
65
+ import { randomUUID } from 'node:crypto';
66
+ import { getLogger } from '../logger.js';
67
+ import { openDualScopeDb, openDualScopeDbAtPath, resolveDualScopeDbPath } from './dual-scope-db.js';
68
+ import { assertWriterLeaseActiveIndexPresent, WRITER_LEASES_ACTIVE_INDEX, WRITER_LEASES_TABLE, WRITER_QUEUE_TABLE, } from './writer-lease-schema.js';
69
+ /** Default lease TTL — aligned to `busy_timeout=30000` (specs/sqlite-pragmas.json). */
70
+ export const DEFAULT_TTL_MS = 30_000;
71
+ /** Default advisory priority (lower acquires sooner). */
72
+ const DEFAULT_PRIORITY = 100;
73
+ /** The cold-open schema-bootstrap priority (highest). */
74
+ export const SCHEMA_BOOTSTRAP_PRIORITY = 0;
75
+ /** Max wall-clock a single acquire will spin before giving up (ms). */
76
+ const ACQUIRE_DEADLINE_MS = 30_000;
77
+ /** Backoff between contended claim retries (ms). */
78
+ const CLAIM_RETRY_DELAY_MS = 25;
79
+ /**
80
+ * Lazily-memoized module logger.
81
+ *
82
+ * Constructed on first use rather than at import time. A top-level
83
+ * `getLogger(...)` call executes the `'../logger.js'` factory during module
84
+ * initialization, which — when this module is pulled into a test's mocked
85
+ * import graph via `dual-scope-db.ts` → `writer-lease.ts` — reaches a
86
+ * `vi.mock('../../logger.js')` factory before its module-scoped spy `const` has
87
+ * been initialized, throwing a TDZ `ReferenceError`. Deferring the call to the
88
+ * first log site keeps import-time side-effect-free and matches the
89
+ * call-inside-function pattern used by `dual-scope-db.ts` and
90
+ * `brain-writer-thread.ts`.
91
+ *
92
+ * @task T11627
93
+ */
94
+ let _log = null;
95
+ function log() {
96
+ if (_log === null)
97
+ _log = getLogger('writer-lease');
98
+ return _log;
99
+ }
100
+ // ── Mode resolution ───────────────────────────────────────────────────────────
101
+ let _cachedMode = null;
102
+ let _supervisorDemotionLogged = false;
103
+ /**
104
+ * Resolve the writer-lease mode from `CLEO_WRITER_LEASE_MODE`, once per process.
105
+ *
106
+ * Unknown / unset values resolve to `'local'` — the production-safe default while
107
+ * the supervisor daemon is disabled.
108
+ *
109
+ * @returns The resolved {@link LeaseMode}.
110
+ *
111
+ * @task T11627
112
+ */
113
+ export function resolveLeaseMode() {
114
+ if (_cachedMode !== null)
115
+ return _cachedMode;
116
+ const raw = process.env.CLEO_WRITER_LEASE_MODE;
117
+ switch (raw) {
118
+ case 'supervisor':
119
+ case 'local':
120
+ case 'off':
121
+ case 'require':
122
+ _cachedMode = raw;
123
+ break;
124
+ default:
125
+ _cachedMode = 'local';
126
+ break;
127
+ }
128
+ return _cachedMode;
129
+ }
130
+ /**
131
+ * The mode actually used for arbitration. `supervisor` demotes to `local` in ST-2
132
+ * because the IPC client is not wired yet — a dead/absent arbiter must never
133
+ * deadlock a write. Logged once.
134
+ */
135
+ function effectiveMode() {
136
+ const mode = resolveLeaseMode();
137
+ if (mode === 'supervisor') {
138
+ if (!_supervisorDemotionLogged) {
139
+ _supervisorDemotionLogged = true;
140
+ log().info('CLEO_WRITER_LEASE_MODE=supervisor but no IPC client is wired (ST-2); ' +
141
+ 'demoting to local-mode arbitration for the process lifetime.');
142
+ }
143
+ return 'local';
144
+ }
145
+ return mode;
146
+ }
147
+ /**
148
+ * Reset cached process-global state (mode + demotion flag + grant memo). Tests
149
+ * only — production resolves these once and never resets.
150
+ *
151
+ * @internal
152
+ */
153
+ export function _resetWriterLeaseStateForTest() {
154
+ _cachedMode = null;
155
+ _supervisorDemotionLogged = false;
156
+ _grantMemo.clear();
157
+ _inflightAcquire.clear();
158
+ _nativeDbResolver = defaultNativeDbResolver;
159
+ _dbPathResolver = resolveDualScopeDbPath;
160
+ _activeScope = null;
161
+ }
162
+ /**
163
+ * Default resolver: route through the dual-scope chokepoint so the lease
164
+ * migration is applied (tables + raw partial-unique index), then extract the
165
+ * native handle drizzle holds on `$client` together with the handle's resolved
166
+ * `dbPath` (the lease-scope discriminator). When an explicit `dbPath` is supplied
167
+ * the cached path-aware opener is used so the lease row lands in THAT file (the
168
+ * multi-project-in-one-process case — Finding 1).
169
+ */
170
+ const defaultNativeDbResolver = async (scope, dbPath) => {
171
+ let handle;
172
+ if (dbPath !== undefined && dbPath !== resolveDualScopeDbPath(scope)) {
173
+ // Explicit non-canonical path → open (cached) at that exact file.
174
+ handle =
175
+ scope === 'project'
176
+ ? await openDualScopeDbAtPath('project', dbPath)
177
+ : await openDualScopeDbAtPath('global', dbPath);
178
+ }
179
+ else {
180
+ handle =
181
+ scope === 'project' ? await openDualScopeDb('project') : await openDualScopeDb('global');
182
+ }
183
+ const native = handle.db.$client;
184
+ // `handle.dbPath` is the path this open resolved; fall back to the scope→path
185
+ // resolver defensively (the handle always carries it in practice).
186
+ const resolvedPath = handle.dbPath ?? dbPath ?? resolveDualScopeDbPath(scope);
187
+ return { native, dbPath: resolvedPath };
188
+ };
189
+ let _nativeDbResolver = defaultNativeDbResolver;
190
+ /**
191
+ * Override how the engine obtains a scope's native `cleo.db` handle + path. Tests
192
+ * inject a temp-dir handle here so arbitration runs against an isolated fixture
193
+ * with no supervisor and no canonical-path side effects.
194
+ *
195
+ * For backward-compatibility a resolver that returns a bare `DatabaseSync` is
196
+ * accepted and adapted to a {@link LeaseTarget} whose `dbPath` is a stable
197
+ * synthetic per-scope token (`test://<scope>`) — sufficient for single-file test
198
+ * fixtures, which key one file per scope.
199
+ *
200
+ * @param resolver - The resolver to use, or `undefined` to restore the default.
201
+ * @internal
202
+ */
203
+ export function _setNativeDbResolverForTest(resolver) {
204
+ if (resolver === undefined) {
205
+ _nativeDbResolver = defaultNativeDbResolver;
206
+ return;
207
+ }
208
+ _nativeDbResolver = async (scope, dbPath) => {
209
+ const out = await resolver(scope, dbPath);
210
+ // Adapt a bare native handle to a LeaseTarget with a stable synthetic path.
211
+ return 'native' in out ? out : { native: out, dbPath: dbPath ?? scopePathToken(scope) };
212
+ };
213
+ // Keep the (I/O-free) path resolver consistent with the injected native one so
214
+ // the memo key derived before the resolver await matches the resolved target.
215
+ _dbPathResolver = (scope) => scopePathToken(scope);
216
+ }
217
+ /** Stable synthetic dbPath token for a scope under a test resolver injection. */
218
+ function scopePathToken(scope) {
219
+ return `test://${scope}`;
220
+ }
221
+ /**
222
+ * Resolve the absolute dbPath for a scope WITHOUT opening the handle. Used to
223
+ * build the memo key on the re-entrant fast path (before the native resolver
224
+ * await). Defaults to the canonical scope→path resolver (deterministic, no I/O);
225
+ * a test native-resolver injection swaps in the matching synthetic token so the
226
+ * key built here equals the {@link LeaseTarget.dbPath} the resolver returns.
227
+ */
228
+ let _dbPathResolver = resolveDualScopeDbPath;
229
+ /**
230
+ * `Map<\`${scope}::${dbPath}::${lane}\`, GrantEntry>` — the active grant per
231
+ * (scope, dbPath, lane). Keyed on the EXISTING `cacheKey(scope, dbPath)` composite
232
+ * (spec §6 Seam 0) so two DIFFERENT project files in one process are distinct
233
+ * lease scopes — the in-process grant is NEVER mis-shared across projects.
234
+ */
235
+ const _grantMemo = new Map();
236
+ /**
237
+ * In-flight FIRST-acquisition promises per memo key (Finding 2 — single-flight).
238
+ *
239
+ * The re-entrant fast path memoizes the active grant, but the FIRST acquisition
240
+ * resolves the native handle (`await _nativeDbResolver`) BEFORE writing the memo
241
+ * entry. Two callers racing the first acquire would both observe an empty memo
242
+ * across that await and both run the full claim path — the loser then spins the
243
+ * whole acquire window and degrades to a lease-less write. This map makes the
244
+ * first acquisition single-flight: concurrent callers await the SAME in-flight
245
+ * acquire and share the resulting grant (refcount++), exactly like a nested
246
+ * same-frame re-entry.
247
+ */
248
+ const _inflightAcquire = new Map();
249
+ /**
250
+ * Build the lease-scope memo key. Keyed on `${scope}::${dbPath}::${lane}` — the
251
+ * `${scope}::${dbPath}` prefix is byte-equal to `cacheKey(scope, dbPath)` in
252
+ * `dual-scope-db.ts` so the lease scope and the handle cache agree on identity.
253
+ */
254
+ function memoKey(scope, dbPath, lane) {
255
+ return `${scope}::${dbPath}::${lane}`;
256
+ }
257
+ // ── Active-scope registry (ST-3 · Seam 1 — process-local, no signature change) ──
258
+ /**
259
+ * The scope + resolved dbPath of the most-recent canonical cold-open in this
260
+ * process.
261
+ *
262
+ * The chokepoint write primitives ({@link insertIdempotent} /
263
+ * {@link upsertIdempotent} in `dual-scope-db.ts`) receive only a drizzle handle —
264
+ * NOT the scope or a {@link LeaseHandle}. To gate them through the writer lease
265
+ * (Seam 1) WITHOUT a signature change, the cold-open path records its scope AND
266
+ * dbPath here (mirroring the `getRecordedExodusAbort` registry pattern already
267
+ * used by those primitives) and the primitive reads them back via
268
+ * {@link activeScope} / {@link activeScopeDbPath}.
269
+ *
270
+ * Recording the dbPath (not just the abstract scope LABEL) means the chokepoint
271
+ * leases against the lease ROW in the SAME file the open targeted — so two
272
+ * different projects opened in one process lease in their OWN cleo.db, never the
273
+ * cwd-default file (Finding 1).
274
+ *
275
+ * `null` until the first canonical cold-open records a scope.
276
+ */
277
+ let _activeScope = null;
278
+ let _activeScopeDbPath = null;
279
+ /**
280
+ * Record the scope (and optionally the resolved dbPath) of the cold-open
281
+ * currently in progress / most recently opened. Called by `dual-scope-db.ts` at
282
+ * the head of its cold-open critical section (Seam 0). Idempotent — last writer
283
+ * wins; a project open after a global open makes `'project'` the active scope,
284
+ * which is correct because the chokepoint write primitives only ever write the
285
+ * project-tier `tasks_*` tables.
286
+ *
287
+ * @param scope - The scope of the in-progress cold-open.
288
+ * @param dbPath - The resolved on-disk path of that scope's cleo.db. When
289
+ * omitted, the active dbPath is cleared so {@link activeScopeDbPath} falls back
290
+ * to the canonical scope→path resolver.
291
+ * @internal
292
+ * @task T11627
293
+ */
294
+ export function setActiveScope(scope, dbPath) {
295
+ _activeScope = scope;
296
+ _activeScopeDbPath = dbPath ?? null;
297
+ }
298
+ /**
299
+ * The scope the chokepoint write primitives should lease against. Defaults to
300
+ * `'project'` (the tasks chokepoint is project-tier) when no cold-open has
301
+ * recorded a scope yet — a write before any open is degenerate, but `'project'`
302
+ * is the only correct lease scope for `tasks_*` mutations.
303
+ *
304
+ * @returns The active {@link LeaseScope}.
305
+ * @internal
306
+ * @task T11627
307
+ */
308
+ export function activeScope() {
309
+ return _activeScope ?? 'project';
310
+ }
311
+ /**
312
+ * The dbPath the chokepoint write primitives should lease their row in. Returns
313
+ * the path recorded by the most-recent cold-open, or — defensively — the
314
+ * canonical scope→path resolution for {@link activeScope} when none was recorded.
315
+ *
316
+ * @returns The active cleo.db on-disk path.
317
+ * @internal
318
+ * @task T11627
319
+ */
320
+ export function activeScopeDbPath() {
321
+ return _activeScopeDbPath ?? _dbPathResolver(activeScope());
322
+ }
323
+ /**
324
+ * Clear the recorded active scope + dbPath. Tests only — production records once
325
+ * per cold-open and never clears (the scope of the last canonical open remains
326
+ * the lease target for subsequent writes).
327
+ *
328
+ * @internal
329
+ */
330
+ export function _clearActiveScopeForTest() {
331
+ _activeScope = null;
332
+ _activeScopeDbPath = null;
333
+ }
334
+ // ── Small helpers ─────────────────────────────────────────────────────────────
335
+ /**
336
+ * Asynchronously wait `ms` between contended claim retries. MUST be async (not
337
+ * `Atomics.wait`): the claim transaction itself is synchronous, but the BETWEEN-
338
+ * retry gap has to yield the event loop so a concurrent holder in THIS process can
339
+ * run its `release()` (which frees the row this waiter is spinning for). A
340
+ * synchronous spin would deadlock single-process re-entrant contention.
341
+ */
342
+ function sleepAsync(ms) {
343
+ return new Promise((resolve) => setTimeout(resolve, Math.max(0, Math.round(ms))));
344
+ }
345
+ /** No-throw pid-liveness probe (`process.kill(pid, 0)`), mirrors gc/daemon.ts. */
346
+ function isPidAlive(pid) {
347
+ try {
348
+ process.kill(pid, 0);
349
+ return true;
350
+ }
351
+ catch (err) {
352
+ // ESRCH => no such process (dead). EPERM => process exists, not ours (alive).
353
+ return err.code === 'EPERM';
354
+ }
355
+ }
356
+ /** This process's stable holder identity for a lane. */
357
+ function makeHolderId(lane) {
358
+ return `pid-${process.pid}:${lane}:${randomUUID().slice(0, 8)}`;
359
+ }
360
+ // ── Internal handle ───────────────────────────────────────────────────────────
361
+ /**
362
+ * Concrete {@link LeaseHandle} bound to a native handle + holder identity. Owns
363
+ * the heartbeat timer and the epoch-guarded release/heartbeat SQL.
364
+ */
365
+ class InternalLeaseHandle {
366
+ scope;
367
+ lane;
368
+ epoch;
369
+ holderId;
370
+ /** The resolved on-disk path of the cleo.db this lease's row lives in. */
371
+ dbPath;
372
+ nativeDb;
373
+ ttlMs;
374
+ heartbeatTimer = null;
375
+ released = false;
376
+ constructor(args) {
377
+ this.scope = args.scope;
378
+ this.lane = args.lane;
379
+ this.epoch = args.epoch;
380
+ this.holderId = args.holderId;
381
+ this.dbPath = args.dbPath;
382
+ this.nativeDb = args.nativeDb;
383
+ this.ttlMs = args.ttlMs;
384
+ this.startHeartbeat();
385
+ }
386
+ /** Start the heartbeat timer at `ttl/3` (unref'd so it never holds the event loop open). */
387
+ startHeartbeat() {
388
+ const interval = Math.max(1, Math.floor(this.ttlMs / 3));
389
+ this.heartbeatTimer = setInterval(() => this.heartbeat(), interval);
390
+ // Never keep the process alive solely for a heartbeat.
391
+ this.heartbeatTimer.unref?.();
392
+ }
393
+ heartbeat() {
394
+ if (this.released)
395
+ return;
396
+ // The heartbeat fires from an unref'd timer; if the underlying native handle
397
+ // was closed out from under a still-held lease (cache eviction / shutdown /
398
+ // test teardown), `prepare()` throws `database is not open` INSIDE the timer
399
+ // callback — an UNCAUGHT exception that would crash Node. Tolerate a closed
400
+ // handle the same way `tryClaimOnce` tolerates a rolled-back txn: stop the
401
+ // timer, mark released, and no-op. A reclaimed holder (new epoch) updates 0
402
+ // rows → no-op via the epoch guard.
403
+ try {
404
+ this.nativeDb
405
+ .prepare(`UPDATE ${WRITER_LEASES_TABLE} SET heartbeat_at = ? ` +
406
+ `WHERE scope = ? AND lane = ? AND holder_id = ? AND epoch = ? AND active = 1`)
407
+ .run(Date.now(), this.scope, this.lane, this.holderId, this.epoch);
408
+ }
409
+ catch (err) {
410
+ // Closed handle (or any heartbeat-write failure): stop heartbeating rather
411
+ // than throw on every subsequent tick. The row is freed by whoever closed
412
+ // the DB (close drops the file lock) or reclaimed via TTL by a peer.
413
+ this.released = true;
414
+ if (this.heartbeatTimer) {
415
+ clearInterval(this.heartbeatTimer);
416
+ this.heartbeatTimer = null;
417
+ }
418
+ log().debug({ scope: this.scope, lane: this.lane, err: err instanceof Error ? err.message : err }, 'writer-lease heartbeat failed (native handle likely closed); stopping heartbeat');
419
+ }
420
+ }
421
+ /** Free the row (`active = 0`) under the epoch guard, stop the heartbeat. */
422
+ releaseRow() {
423
+ if (this.released)
424
+ return;
425
+ this.released = true;
426
+ if (this.heartbeatTimer) {
427
+ clearInterval(this.heartbeatTimer);
428
+ this.heartbeatTimer = null;
429
+ }
430
+ // Tolerate a closed native handle on release (same race as the heartbeat):
431
+ // the lease row is freed implicitly when the DB closes / dies, so a failed
432
+ // free-row UPDATE must not throw out of `release()`.
433
+ try {
434
+ this.nativeDb
435
+ .prepare(`UPDATE ${WRITER_LEASES_TABLE} SET active = 0, reentrancy_depth = 0 ` +
436
+ `WHERE scope = ? AND lane = ? AND holder_id = ? AND epoch = ? AND active = 1`)
437
+ .run(this.scope, this.lane, this.holderId, this.epoch);
438
+ }
439
+ catch (err) {
440
+ log().debug({ scope: this.scope, lane: this.lane, err: err instanceof Error ? err.message : err }, 'writer-lease releaseRow failed (native handle likely closed); row freed by close/TTL');
441
+ }
442
+ }
443
+ async release() {
444
+ await releaseGrant(this.scope, this.dbPath, this.lane);
445
+ }
446
+ }
447
+ // ── Error ─────────────────────────────────────────────────────────────────────
448
+ /**
449
+ * Thrown when `require` mode cannot acquire the lease, or (future) when a
450
+ * supervisor explicitly denies. Carries a stable `codeName` for envelope mapping.
451
+ *
452
+ * @public
453
+ */
454
+ export class LeaseUnavailableError extends Error {
455
+ /** Stable string error code for envelope `codeName` / log correlation. */
456
+ codeName = 'E_LEASE_UNAVAILABLE';
457
+ constructor(scope, lane, reason) {
458
+ super(`E_LEASE_UNAVAILABLE: could not acquire ${scope}/${lane} writer lease ` +
459
+ `(mode=require): ${reason}`);
460
+ this.name = 'LeaseUnavailableError';
461
+ }
462
+ }
463
+ /**
464
+ * Thrown when a write primitive opens the WRITE handle without a held lease
465
+ * (T11627 ST-4 · Seam 2 · AC4). The brain worker (and any other dedicated-handle
466
+ * writer outside the chokepoint) MUST hold its lane's grant before it writes — a
467
+ * lease-less write is exactly the multi-writer race the lease exists to kill.
468
+ *
469
+ * Enforced (not merely advised): the brain writer bootstrap calls
470
+ * {@link assertWriterLeaseHeld} before draining, and a lease-less drain throws
471
+ * this. `off` mode (the rollback escape hatch) is exempt — there is no lease to
472
+ * hold and the underlying `busy_timeout` serializes writes as before.
473
+ *
474
+ * @public
475
+ * @task T11627
476
+ */
477
+ export class WriterLeaseRequiredError extends Error {
478
+ /** Stable string error code for envelope `codeName` / log correlation. */
479
+ codeName = 'E_WRITER_LEASE_REQUIRED';
480
+ constructor(scope, lane) {
481
+ super(`E_WRITER_LEASE_REQUIRED: cannot open the ${scope}/${lane} writer handle ` +
482
+ 'without a held writer lease (AC4) — wrap the write in withWriterLease/' +
483
+ 'acquireWriterLease, or set CLEO_WRITER_LEASE_MODE=off to bypass.');
484
+ this.name = 'WriterLeaseRequiredError';
485
+ }
486
+ }
487
+ /**
488
+ * Whether THIS process currently holds an active grant for `(scope, lane)` in the
489
+ * process-local grant memo. Used by dedicated-handle writers (the brain worker) to
490
+ * assert AC4 without re-running the claim txn.
491
+ *
492
+ * @param scope - The cleo.db scope.
493
+ * @param lane - The write lane.
494
+ * @returns `true` iff a memoized grant with `refcount > 0` exists for the key.
495
+ *
496
+ * @task T11627
497
+ */
498
+ export function hasActiveGrant(scope, lane) {
499
+ const entry = _grantMemo.get(memoKey(scope, _dbPathResolver(scope), lane));
500
+ return entry !== undefined && entry.refcount > 0;
501
+ }
502
+ /**
503
+ * AC4 guard. Assert this process holds the `(scope, lane)` writer lease before it
504
+ * opens a dedicated WRITE handle. In `off` mode the assertion is a no-op (there is
505
+ * no lease; `busy_timeout` serializes). In every other mode a missing grant throws
506
+ * {@link WriterLeaseRequiredError}.
507
+ *
508
+ * @param scope - The cleo.db scope the handle writes to.
509
+ * @param lane - The write lane that must be held.
510
+ * @throws {WriterLeaseRequiredError} when no grant is held and mode is not `off`.
511
+ *
512
+ * @task T11627
513
+ */
514
+ export function assertWriterLeaseHeld(scope, lane) {
515
+ if (effectiveMode() === 'off')
516
+ return;
517
+ if (!hasActiveGrant(scope, lane)) {
518
+ throw new WriterLeaseRequiredError(scope, lane);
519
+ }
520
+ }
521
+ // ── Claim transaction (shared primitive — local mode) ─────────────────────────
522
+ /**
523
+ * Attempt ONE `BEGIN IMMEDIATE` claim against the active row. Returns the granted
524
+ * epoch on success, or `null` if a live holder owns the row (caller backs off and
525
+ * retries; the queue row was enqueued on the first miss).
526
+ */
527
+ function tryClaimOnce(nativeDb, scope, lane, holderId, ttlMs) {
528
+ const now = Date.now();
529
+ nativeDb.exec('BEGIN IMMEDIATE TRANSACTION');
530
+ try {
531
+ const active = nativeDb
532
+ .prepare(`SELECT id, holder_id, holder_pid, epoch, heartbeat_at, ttl_ms, reentrancy_depth ` +
533
+ `FROM ${WRITER_LEASES_TABLE} WHERE scope = ? AND lane = ? AND active = 1`)
534
+ .get(scope, lane);
535
+ if (active === undefined) {
536
+ // No active holder — take a fresh row with the next epoch.
537
+ const nextEpoch = nativeDb
538
+ .prepare(`SELECT COALESCE(MAX(epoch), 0) + 1 AS e FROM ${WRITER_LEASES_TABLE} WHERE scope = ? AND lane = ?`)
539
+ .get(scope, lane)?.e ?? 1;
540
+ nativeDb
541
+ .prepare(`INSERT INTO ${WRITER_LEASES_TABLE} ` +
542
+ `(scope, lane, holder_id, holder_pid, epoch, acquired_at, heartbeat_at, ttl_ms, reentrancy_depth, active) ` +
543
+ `VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, 1)`)
544
+ .run(scope, lane, holderId, process.pid, nextEpoch, now, now, ttlMs);
545
+ nativeDb.exec('COMMIT');
546
+ return nextEpoch;
547
+ }
548
+ if (active.holder_id === holderId) {
549
+ // Same holder (defensive — same-process re-entrancy is handled by the memo
550
+ // before reaching here). Bump the durable depth and re-assert the epoch.
551
+ nativeDb
552
+ .prepare(`UPDATE ${WRITER_LEASES_TABLE} SET reentrancy_depth = reentrancy_depth + 1, heartbeat_at = ? ` +
553
+ `WHERE id = ?`)
554
+ .run(now, active.id);
555
+ nativeDb.exec('COMMIT');
556
+ return active.epoch;
557
+ }
558
+ // A different holder owns the row. Reclaim IFF it is stale (TTL expired AND
559
+ // its pid is dead) — SQLite serializes this inside BEGIN IMMEDIATE so two
560
+ // reclaimers cannot both win; the loser sees the new epoch and re-queues.
561
+ const stale = now - active.heartbeat_at > active.ttl_ms && !isPidAlive(active.holder_pid);
562
+ if (stale) {
563
+ const reclaimedEpoch = active.epoch + 1;
564
+ nativeDb
565
+ .prepare(`UPDATE ${WRITER_LEASES_TABLE} ` +
566
+ `SET holder_id = ?, holder_pid = ?, epoch = ?, acquired_at = ?, heartbeat_at = ?, ttl_ms = ?, reentrancy_depth = 1 ` +
567
+ `WHERE id = ? AND epoch = ?`)
568
+ .run(holderId, process.pid, reclaimedEpoch, now, now, ttlMs, active.id, active.epoch);
569
+ nativeDb.exec('COMMIT');
570
+ return reclaimedEpoch;
571
+ }
572
+ // Live holder — give up this attempt.
573
+ nativeDb.exec('ROLLBACK');
574
+ return null;
575
+ }
576
+ catch (err) {
577
+ try {
578
+ nativeDb.exec('ROLLBACK');
579
+ }
580
+ catch {
581
+ // already rolled back / no active txn — ignore
582
+ }
583
+ throw err;
584
+ }
585
+ }
586
+ /** Enqueue a waiter row (idempotent per holder) for FIFO+priority ordering / aging. */
587
+ function enqueueWaiter(nativeDb, scope, lane, holderId, priority, ttlMs) {
588
+ const now = Date.now();
589
+ const existing = nativeDb
590
+ .prepare(`SELECT ticket FROM ${WRITER_QUEUE_TABLE} WHERE scope = ? AND lane = ? AND holder_id = ?`)
591
+ .get(scope, lane, holderId);
592
+ if (existing)
593
+ return;
594
+ nativeDb
595
+ .prepare(`INSERT INTO ${WRITER_QUEUE_TABLE} (scope, lane, holder_id, priority, enqueued_at, deadline_at) ` +
596
+ `VALUES (?, ?, ?, ?, ?, ?)`)
597
+ .run(scope, lane, holderId, priority, now, now + ttlMs);
598
+ }
599
+ /** Remove this holder's waiter row once granted (or on give-up). */
600
+ function dequeueWaiter(nativeDb, scope, lane, holderId) {
601
+ nativeDb
602
+ .prepare(`DELETE FROM ${WRITER_QUEUE_TABLE} WHERE scope = ? AND lane = ? AND holder_id = ?`)
603
+ .run(scope, lane, holderId);
604
+ }
605
+ // ── Acquire / release surface ─────────────────────────────────────────────────
606
+ /**
607
+ * Acquire (or re-enter) the writer lease for `(scope, lane)`. Caller MUST call
608
+ * `release()` on the returned handle (use {@link withWriterLease} to do so
609
+ * automatically).
610
+ *
611
+ * - `off` mode → returns a no-op handle (pass-through; no row written).
612
+ * - `local`/`supervisor` mode → arbitrates over the persisted row via
613
+ * `BEGIN IMMEDIATE` (+ epoch-CAS). A nested same-(scope,lane) acquire in this
614
+ * process re-enters the memoized grant (refcount++).
615
+ * - `require` mode → throws {@link LeaseUnavailableError} if the row cannot be
616
+ * taken within the acquire window (`min(ACQUIRE_DEADLINE_MS, ttlMs)`).
617
+ *
618
+ * @param scope - The cleo.db scope.
619
+ * @param lane - The write lane within the scope.
620
+ * @param opts - Priority / TTL / re-entrancy options.
621
+ * @returns A held {@link LeaseHandle}.
622
+ *
623
+ * @task T11627
624
+ */
625
+ export async function acquireWriterLease(scope, lane, opts) {
626
+ const mode = effectiveMode();
627
+ const reentrant = opts?.reentrant ?? true;
628
+ // `off` mode — pure pass-through. No row written, no memo entry; busy_timeout on
629
+ // the underlying connection serializes writes exactly as before the lease.
630
+ if (mode === 'off') {
631
+ return makeNoopHandle(scope, lane);
632
+ }
633
+ // Build the lease-scope key up front from the explicit pinned dbPath (when the
634
+ // chokepoint pins a non-cwd-default file) or the I/O-free path resolver, so the
635
+ // re-entrant fast path AND the single-flight guard are decided SYNCHRONOUSLY,
636
+ // before any `await` yields the event loop. Keying on `${scope}::${dbPath}::
637
+ // ${lane}` means two different project files in one process are distinct lease
638
+ // scopes (Finding 1) and that the same (scope,dbPath,lane) never double-claims.
639
+ const dbPath = opts?.dbPath ?? _dbPathResolver(scope);
640
+ const key = memoKey(scope, dbPath, lane);
641
+ // Re-entrant fast path: an existing same-(scope,dbPath,lane) grant is shared
642
+ // (refcount++ + durable depth++) without a second claim txn.
643
+ if (reentrant) {
644
+ const existing = _grantMemo.get(key);
645
+ if (existing) {
646
+ existing.refcount += 1;
647
+ // Reflect the re-entry in the durable row depth (best-effort under epoch guard).
648
+ const { native } = await _nativeDbResolver(scope, dbPath);
649
+ native
650
+ .prepare(`UPDATE ${WRITER_LEASES_TABLE} SET reentrancy_depth = reentrancy_depth + 1 ` +
651
+ `WHERE scope = ? AND lane = ? AND holder_id = ? AND epoch = ? AND active = 1`)
652
+ .run(scope, lane, existing.handle.holderId, existing.handle.epoch);
653
+ return existing.handle;
654
+ }
655
+ // Single-flight FIRST acquisition (Finding 2): if another caller is already
656
+ // mid-acquire for this key, await its in-flight promise and share the grant
657
+ // (refcount++) instead of racing a second full claim that would spin the whole
658
+ // acquire window and degrade to a lease-less write. Decided synchronously here
659
+ // — no `await` has run since the memo check above, so the two checks are atomic.
660
+ const inflight = _inflightAcquire.get(key);
661
+ if (inflight) {
662
+ const shared = await inflight;
663
+ // Re-check the memo: the in-flight acquire may have already released (e.g.
664
+ // immediate withWriterLease) or degraded to a no-op (not memoized).
665
+ const entry = _grantMemo.get(key);
666
+ if (entry && entry.handle === shared) {
667
+ entry.refcount += 1;
668
+ // `entry.handle` is the concrete InternalLeaseHandle (holderId/epoch).
669
+ const { native } = await _nativeDbResolver(scope, dbPath);
670
+ native
671
+ .prepare(`UPDATE ${WRITER_LEASES_TABLE} SET reentrancy_depth = reentrancy_depth + 1 ` +
672
+ `WHERE scope = ? AND lane = ? AND holder_id = ? AND epoch = ? AND active = 1`)
673
+ .run(scope, lane, entry.handle.holderId, entry.handle.epoch);
674
+ return entry.handle;
675
+ }
676
+ // The shared acquire is no longer active — fall through to a fresh acquire.
677
+ }
678
+ }
679
+ // First acquisition for this key. Register the in-flight promise BEFORE the
680
+ // first `await` so a concurrent reentrant caller (above) can single-flight onto
681
+ // it. Only the reentrant path participates; reentrant:false callers (test cross-
682
+ // holder contention) deliberately bypass the memo + single-flight.
683
+ const acquirePromise = performFirstAcquire(scope, dbPath, lane, mode, key, opts);
684
+ if (reentrant) {
685
+ _inflightAcquire.set(key, acquirePromise);
686
+ }
687
+ try {
688
+ return await acquirePromise;
689
+ }
690
+ finally {
691
+ // Always clear our in-flight entry. A degraded acquire resolves to a no-op
692
+ // handle (never memoized) and `require` mode rejects — in both cases the
693
+ // single-flight followers re-check the memo, find nothing, and acquire freshly,
694
+ // so a stale in-flight entry must never linger.
695
+ if (reentrant && _inflightAcquire.get(key) === acquirePromise) {
696
+ _inflightAcquire.delete(key);
697
+ }
698
+ }
699
+ }
700
+ /**
701
+ * Run ONE full first-acquisition for `(scope, dbPath, lane)` against the persisted
702
+ * row: resolve the native handle, claim/queue/backoff under the bounded acquire
703
+ * window, and on success memoize the grant. Returns a degraded no-op handle on
704
+ * deadline (local) or throws {@link LeaseUnavailableError} (`require`).
705
+ *
706
+ * Factored out of {@link acquireWriterLease} so the single-flight guard can wrap a
707
+ * single in-flight promise per key — the resolved handle (a memoized
708
+ * {@link InternalLeaseHandle}, or a no-op handle on degrade) is what concurrent
709
+ * reentrant followers re-check the memo against before sharing.
710
+ */
711
+ async function performFirstAcquire(scope, dbPath, lane, mode, key, opts) {
712
+ const { native } = await _nativeDbResolver(scope, dbPath);
713
+ // Defensive bootstrap assert: the partial-unique active index MUST exist or AC1
714
+ // is unenforced. Cheap single-row sqlite_master lookup, runs on first acquire.
715
+ assertWriterLeaseActiveIndexPresent(native);
716
+ const holderId = makeHolderId(lane);
717
+ const ttlMs = opts?.ttlMs ?? DEFAULT_TTL_MS;
718
+ const priority = opts?.priority ?? DEFAULT_PRIORITY;
719
+ // Wait at most the lease TTL (but never longer than the hard cap): a caller must
720
+ // not block for a grant longer than the lease itself would live. busy_timeout on
721
+ // the IMMEDIATE lock still backstops every individual claim attempt.
722
+ const acquireWindowMs = Math.min(ACQUIRE_DEADLINE_MS, Math.max(1, ttlMs));
723
+ const deadline = Date.now() + acquireWindowMs;
724
+ let enqueued = false;
725
+ for (;;) {
726
+ const epoch = tryClaimOnce(native, scope, lane, holderId, ttlMs);
727
+ if (epoch !== null) {
728
+ if (enqueued)
729
+ dequeueWaiter(native, scope, lane, holderId);
730
+ const handle = new InternalLeaseHandle({
731
+ scope,
732
+ lane,
733
+ epoch,
734
+ holderId,
735
+ dbPath,
736
+ nativeDb: native,
737
+ ttlMs,
738
+ });
739
+ _grantMemo.set(key, { handle, refcount: 1 });
740
+ return handle;
741
+ }
742
+ // Contended by a live holder. Enqueue once for ordering/aging, then back off.
743
+ if (!enqueued) {
744
+ enqueueWaiter(native, scope, lane, holderId, priority, ttlMs);
745
+ enqueued = true;
746
+ }
747
+ if (Date.now() >= deadline) {
748
+ if (enqueued)
749
+ dequeueWaiter(native, scope, lane, holderId);
750
+ if (mode === 'require') {
751
+ throw new LeaseUnavailableError(scope, lane, 'live holder did not release within deadline');
752
+ }
753
+ // local: degrade to today's behaviour — proceed without a lease. busy_timeout
754
+ // on the connection still serializes the actual write. A no-op handle writes
755
+ // no row, starts no heartbeat, and is never memoized — so single-flight
756
+ // followers find no memo entry and acquire freshly.
757
+ log().warn({ scope, lane }, 'writer-lease acquire deadline exceeded; proceeding under busy_timeout fallback (degraded)');
758
+ return makeNoopHandle(scope, lane);
759
+ }
760
+ await sleepAsync(CLAIM_RETRY_DELAY_MS);
761
+ }
762
+ }
763
+ /** Decrement the memoized grant; free the row at depth 0. */
764
+ async function releaseGrant(scope, dbPath, lane) {
765
+ const key = memoKey(scope, dbPath, lane);
766
+ const entry = _grantMemo.get(key);
767
+ if (!entry)
768
+ return; // off-mode / no-op handle / already released
769
+ entry.refcount -= 1;
770
+ if (entry.refcount > 0) {
771
+ // Still re-entered above this frame — decrement the durable depth only. Pin the
772
+ // resolver at the grant's own dbPath so the depth-write lands in the SAME file
773
+ // the lease row lives in (matches the memo key, not the cwd-default).
774
+ const { native } = await _nativeDbResolver(scope, dbPath);
775
+ native
776
+ .prepare(`UPDATE ${WRITER_LEASES_TABLE} SET reentrancy_depth = reentrancy_depth - 1 ` +
777
+ `WHERE scope = ? AND lane = ? AND holder_id = ? AND epoch = ? AND active = 1`)
778
+ .run(scope, lane, entry.handle.holderId, entry.handle.epoch);
779
+ return;
780
+ }
781
+ // Depth 0 — free the row and evict the memo.
782
+ _grantMemo.delete(key);
783
+ entry.handle.releaseRow();
784
+ }
785
+ /**
786
+ * A no-op handle for `off` mode and degraded-fallback returns: it holds no row,
787
+ * starts no heartbeat, and `release()` is a no-op. `epoch` is `0` (sentinel).
788
+ */
789
+ function makeNoopHandle(scope, lane) {
790
+ return {
791
+ scope,
792
+ lane,
793
+ epoch: 0,
794
+ async release() {
795
+ /* pass-through */
796
+ },
797
+ heartbeat() {
798
+ /* pass-through */
799
+ },
800
+ };
801
+ }
802
+ /**
803
+ * Primary surface: acquire → run `fn` → release (always, even on throw).
804
+ *
805
+ * Refcounted + re-entrant by `(scope, lane)`: a nested same-lane write in the same
806
+ * process re-enters the memoized grant rather than re-running the claim txn.
807
+ *
808
+ * - `off` mode → pass-through (runs `fn` under today's busy_timeout, no row).
809
+ * - `require` mode → an unacquirable lease throws {@link LeaseUnavailableError}
810
+ * before `fn` runs.
811
+ *
812
+ * @param scope - The cleo.db scope.
813
+ * @param lane - The write lane.
814
+ * @param fn - The work to run while holding the lease; receives the handle.
815
+ * @param opts - Priority / TTL / re-entrancy options.
816
+ * @returns The resolved value of `fn`.
817
+ *
818
+ * @task T11627
819
+ */
820
+ export async function withWriterLease(scope, lane, fn, opts) {
821
+ const handle = await acquireWriterLease(scope, lane, opts);
822
+ try {
823
+ return await fn(handle);
824
+ }
825
+ finally {
826
+ await handle.release();
827
+ }
828
+ }
829
+ // ── Cold-open lease (ST-3 · Seam 0 — the T5158 heal) ──────────────────────────
830
+ /**
831
+ * The lease-table bootstrap DDL, applied idempotently to a native handle BEFORE
832
+ * the cold-open claim txn. It is byte-equivalent (modulo `IF NOT EXISTS`) to the
833
+ * `_t11891-writer-leases` migration so the claim can run before the full migration
834
+ * (which is what creates these tables) executes inside the leased section. The
835
+ * partial-unique index is identical to the migration's — AC1 holds whether the
836
+ * tables came from here or from the migration.
837
+ */
838
+ const COLD_OPEN_LEASE_BOOTSTRAP_DDL = [
839
+ // Reserve rootpage 2 for drizzle's migration journal — the FIRST table drizzle
840
+ // `migrate()` creates on a fresh DB — BEFORE the lease tables claim it.
841
+ //
842
+ // SQLite assigns b-tree rootpages sequentially: the first `CREATE TABLE` on an
843
+ // empty file takes page 2. Bootstrapping the lease tables here (a prerequisite of
844
+ // the cold-open claim txn, which runs BEFORE the data migrations inside `fn`)
845
+ // would otherwise plant the EMPTY `_writer_leases` table at page 2. After the
846
+ // cold-open lease releases (`active = 0`), that page is an empty leaf — so a DB
847
+ // whose page-2 bytes are later scribbled is NOT flagged by `PRAGMA
848
+ // integrity_check` (no cells to traverse), silently masking real corruption that
849
+ // `cleo doctor` / project-health probes must detect. Pre-creating the journal
850
+ // table (identical DDL to migration-manager's reconcile path) keeps page 2 owned
851
+ // by the always-populated `__drizzle_migrations` table exactly as on a lease-free
852
+ // open — the corruption-detection contract is preserved and the heal is unchanged.
853
+ // `IF NOT EXISTS` → a no-op re-running over an already-migrated DB.
854
+ //
855
+ // @task T11627
856
+ `CREATE TABLE IF NOT EXISTS "__drizzle_migrations" (
857
+ id INTEGER PRIMARY KEY,
858
+ hash text NOT NULL,
859
+ created_at numeric,
860
+ name text,
861
+ applied_at TEXT
862
+ )`,
863
+ `CREATE TABLE IF NOT EXISTS ${WRITER_LEASES_TABLE} (
864
+ id INTEGER PRIMARY KEY,
865
+ scope TEXT NOT NULL,
866
+ lane TEXT NOT NULL,
867
+ holder_id TEXT NOT NULL,
868
+ holder_pid INTEGER NOT NULL,
869
+ epoch INTEGER NOT NULL,
870
+ acquired_at INTEGER NOT NULL,
871
+ heartbeat_at INTEGER NOT NULL,
872
+ ttl_ms INTEGER NOT NULL,
873
+ reentrancy_depth INTEGER NOT NULL DEFAULT 1,
874
+ active INTEGER NOT NULL DEFAULT 1
875
+ )`,
876
+ `CREATE UNIQUE INDEX IF NOT EXISTS ${WRITER_LEASES_ACTIVE_INDEX} ON ${WRITER_LEASES_TABLE} (scope, lane) WHERE active = 1`,
877
+ `CREATE TABLE IF NOT EXISTS ${WRITER_QUEUE_TABLE} (
878
+ ticket INTEGER PRIMARY KEY AUTOINCREMENT,
879
+ scope TEXT NOT NULL,
880
+ lane TEXT NOT NULL,
881
+ holder_id TEXT NOT NULL,
882
+ priority INTEGER NOT NULL DEFAULT 100,
883
+ enqueued_at INTEGER NOT NULL,
884
+ deadline_at INTEGER NOT NULL
885
+ )`,
886
+ `CREATE INDEX IF NOT EXISTS ix_writer_queue_order ON ${WRITER_QUEUE_TABLE} (scope, lane, priority ASC, ticket ASC)`,
887
+ ];
888
+ /**
889
+ * Idempotently create the lease tables + the raw partial-unique active index on
890
+ * `nativeDb`. Safe to call before any migration has run (the migration that
891
+ * normally creates these tables executes INSIDE the leased cold-open section, so
892
+ * the claim txn needs the tables present first). Each statement is `IF NOT EXISTS`
893
+ * so a re-open over an already-migrated DB is a no-op.
894
+ */
895
+ function ensureColdOpenLeaseTables(nativeDb) {
896
+ for (const stmt of COLD_OPEN_LEASE_BOOTSTRAP_DDL) {
897
+ nativeDb.exec(stmt);
898
+ }
899
+ }
900
+ /**
901
+ * Lease the dual-scope-db COLD-OPEN critical section (Seam 0 — the T5158 heal).
902
+ *
903
+ * This is the high-value gate: it serializes the cold-open `reconcileJournal` +
904
+ * `migrateWithRetry` write-txn — the precise span that races in T5158 — so EXACTLY
905
+ * ONE process per scope runs it while peers `BEGIN IMMEDIATE`-queue and then observe
906
+ * a ready DB. It heals the T5158 `E_NOT_INITIALIZED` / `E_INTERNAL` corruption
907
+ * **with the supervisor daemon disabled** (`local` mode default). The caller
908
+ * (`dual-scope-db.ts`) runs the exodus-on-open hook AFTER this lease releases — the
909
+ * exodus engine owns its own single-flight lock + dedicated connections and
910
+ * closes/re-opens handles, so it must not run under this row lease.
911
+ *
912
+ * Unlike {@link withWriterLease} it operates DIRECTLY on the already-opened native
913
+ * handle — it never routes through the default resolver (which would re-enter
914
+ * `openDualScopeDb` and recurse) and it bootstraps the lease tables first (the full
915
+ * migration that creates them runs inside `fn`). It also records the scope in the
916
+ * Seam-1 active-scope registry ({@link setActiveScope}) so chokepoint write
917
+ * primitives lease against the right scope.
918
+ *
919
+ * Mode semantics mirror {@link withWriterLease}:
920
+ * - `off` → pass-through: runs `fn` under today's `busy_timeout=30000`, byte-
921
+ * identical to pre-lease cold-open behaviour.
922
+ * - `local` / `supervisor` (demoted) → claim the row via `BEGIN IMMEDIATE`; on a
923
+ * contended live holder, spin under the busy-timeout-backstopped deadline, then
924
+ * degrade to running `fn` (busy_timeout still serializes the migrate write-txn).
925
+ * - `require` → throw {@link LeaseUnavailableError} if the row cannot be taken.
926
+ *
927
+ * @param scope - The cleo.db scope being cold-opened.
928
+ * @param nativeDb - The native handle the cold-open just created (pragmas applied).
929
+ * @param fn - The cold-open body to run while holding the lease.
930
+ * @param opts - Priority / TTL options + the resolved `dbPath` of this cold-open.
931
+ * `dbPath` is recorded in the Seam-1 active-scope registry so the chokepoint
932
+ * write primitives lease their row in the SAME file this open targeted (correct
933
+ * when more than one project is open in one process — Finding 1). Cold-open
934
+ * defaults to highest priority ({@link SCHEMA_BOOTSTRAP_PRIORITY}) and a 60s TTL
935
+ * (schema bootstrap can be slow).
936
+ * @returns The resolved value of `fn`.
937
+ *
938
+ * @task T11627
939
+ */
940
+ export async function withColdOpenLease(scope, nativeDb, fn, opts) {
941
+ // Seam 1 wiring: record the scope + resolved dbPath of this cold-open so
942
+ // chokepoint write primitives (insertIdempotent/upsertIdempotent) lease against
943
+ // the right scope AND the right file.
944
+ setActiveScope(scope, opts?.dbPath);
945
+ const mode = effectiveMode();
946
+ // `off` mode — pure pass-through. busy_timeout=30000 on the connection still
947
+ // serializes the migrate/reconcile write-txn exactly as before the lease.
948
+ if (mode === 'off') {
949
+ return fn();
950
+ }
951
+ // The lease tables MUST exist before the claim txn — the migration that creates
952
+ // them runs inside `fn`, so bootstrap them idempotently here first.
953
+ ensureColdOpenLeaseTables(nativeDb);
954
+ // Defensive: the partial-unique active index MUST be present or AC1 is unenforced.
955
+ assertWriterLeaseActiveIndexPresent(nativeDb);
956
+ const lane = 'tasks';
957
+ const holderId = makeHolderId(lane);
958
+ const ttlMs = opts?.ttlMs ?? 60_000;
959
+ const priority = opts?.priority ?? SCHEMA_BOOTSTRAP_PRIORITY;
960
+ // Bounded acquire window — never wait longer than the lease TTL. busy_timeout on
961
+ // the IMMEDIATE lock backstops every individual claim attempt.
962
+ const acquireWindowMs = Math.min(ACQUIRE_DEADLINE_MS, Math.max(1, ttlMs));
963
+ const deadline = Date.now() + acquireWindowMs;
964
+ let enqueued = false;
965
+ let epoch = null;
966
+ for (;;) {
967
+ epoch = tryClaimOnce(nativeDb, scope, lane, holderId, ttlMs);
968
+ if (epoch !== null) {
969
+ if (enqueued)
970
+ dequeueWaiter(nativeDb, scope, lane, holderId);
971
+ break;
972
+ }
973
+ if (!enqueued) {
974
+ enqueueWaiter(nativeDb, scope, lane, holderId, priority, ttlMs);
975
+ enqueued = true;
976
+ }
977
+ if (Date.now() >= deadline) {
978
+ if (enqueued)
979
+ dequeueWaiter(nativeDb, scope, lane, holderId);
980
+ if (mode === 'require') {
981
+ throw new LeaseUnavailableError(scope, lane, 'cold-open: live holder did not release within deadline');
982
+ }
983
+ // local/supervisor: degrade to today's behaviour — run the cold-open under
984
+ // busy_timeout, which still serializes the migrate write-txn.
985
+ log().warn({ scope }, 'cold-open writer-lease acquire deadline exceeded; proceeding under ' +
986
+ 'busy_timeout fallback (degraded)');
987
+ return fn();
988
+ }
989
+ await sleepAsync(CLAIM_RETRY_DELAY_MS);
990
+ }
991
+ // Held — run the cold-open body, then free the row (epoch-guarded) on the way out.
992
+ try {
993
+ return await fn();
994
+ }
995
+ finally {
996
+ nativeDb
997
+ .prepare(`UPDATE ${WRITER_LEASES_TABLE} SET active = 0, reentrancy_depth = 0 ` +
998
+ `WHERE scope = ? AND lane = ? AND holder_id = ? AND epoch = ? AND active = 1`)
999
+ .run(scope, lane, holderId, epoch);
1000
+ }
1001
+ }
1002
+ //# sourceMappingURL=writer-lease.js.map