@wjarka/cezarion 0.15.1 → 0.16.0

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 (462) hide show
  1. package/README.md +167 -46
  2. package/dist/agent-config/catalog.d.ts +6 -2
  3. package/dist/agent-config/catalog.js +78 -3
  4. package/dist/agent-config/catalog.js.map +1 -1
  5. package/dist/agent-config/model-settings/shared.js +6 -3
  6. package/dist/agent-config/model-settings/shared.js.map +1 -1
  7. package/dist/agent-config/models.js +4 -0
  8. package/dist/agent-config/models.js.map +1 -1
  9. package/dist/agent-config/validate.js +6 -0
  10. package/dist/agent-config/validate.js.map +1 -1
  11. package/dist/artifacts/cli.js +10 -2
  12. package/dist/artifacts/cli.js.map +1 -1
  13. package/dist/automations/coordinator.d.ts +7 -0
  14. package/dist/automations/coordinator.js +22 -2
  15. package/dist/automations/coordinator.js.map +1 -1
  16. package/dist/automations/github-poller.d.ts +4 -4
  17. package/dist/automations/github-poller.js.map +1 -1
  18. package/dist/automations/schedule-runner.d.ts +130 -0
  19. package/dist/automations/schedule-runner.js +326 -0
  20. package/dist/automations/schedule-runner.js.map +1 -0
  21. package/dist/automations/scheduler.d.ts +38 -7
  22. package/dist/automations/scheduler.js +90 -22
  23. package/dist/automations/scheduler.js.map +1 -1
  24. package/dist/automations/store.d.ts +80 -3
  25. package/dist/automations/store.js +208 -16
  26. package/dist/automations/store.js.map +1 -1
  27. package/dist/automations/task-template.d.ts +42 -6
  28. package/dist/automations/task-template.js +152 -24
  29. package/dist/automations/task-template.js.map +1 -1
  30. package/dist/automations/types.d.ts +142 -6
  31. package/dist/automations/types.js +64 -6
  32. package/dist/automations/types.js.map +1 -1
  33. package/dist/autosave-holders.js +8 -12
  34. package/dist/autosave-holders.js.map +1 -1
  35. package/dist/ci-wait/client.d.ts +8 -1
  36. package/dist/ci-wait/client.js +43 -1
  37. package/dist/ci-wait/client.js.map +1 -1
  38. package/dist/ci-wait/controller.d.ts +83 -4
  39. package/dist/ci-wait/controller.js +76 -11
  40. package/dist/ci-wait/controller.js.map +1 -1
  41. package/dist/ci-wait/errors.d.ts +7 -1
  42. package/dist/ci-wait/errors.js +36 -0
  43. package/dist/ci-wait/errors.js.map +1 -1
  44. package/dist/ci-wait/github.d.ts +2 -0
  45. package/dist/ci-wait/github.js +3 -1
  46. package/dist/ci-wait/github.js.map +1 -1
  47. package/dist/ci-wait/mcp.d.ts +2 -98
  48. package/dist/ci-wait/mcp.js +11 -25
  49. package/dist/ci-wait/mcp.js.map +1 -1
  50. package/dist/ci-wait/supervisor.d.ts +2 -1
  51. package/dist/ci-wait/supervisor.js +2 -1
  52. package/dist/ci-wait/supervisor.js.map +1 -1
  53. package/dist/ci-wait/tools.d.ts +307 -0
  54. package/dist/ci-wait/tools.js +63 -0
  55. package/dist/ci-wait/tools.js.map +1 -0
  56. package/dist/config.d.ts +3 -0
  57. package/dist/config.js +7 -0
  58. package/dist/config.js.map +1 -1
  59. package/dist/contract/agent-config.d.ts +5 -0
  60. package/dist/contract/agent-profiles.d.ts +15 -0
  61. package/dist/contract/automation-schedule.d.ts +60 -0
  62. package/dist/contract/automations.d.ts +208 -36
  63. package/dist/contract/delegation.d.ts +96 -0
  64. package/dist/contract/discovery.d.ts +7 -0
  65. package/dist/contract/github.d.ts +1 -0
  66. package/dist/contract/health.d.ts +6 -0
  67. package/dist/contract/index.d.ts +3 -0
  68. package/dist/contract/index.js +1445 -745
  69. package/dist/contract/preview.d.ts +508 -0
  70. package/dist/contract/runs.d.ts +716 -15
  71. package/dist/contract/skills.d.ts +39 -0
  72. package/dist/contract/workflows.d.ts +6 -0
  73. package/dist/contract/workspace.d.ts +65 -0
  74. package/dist/contract/zoned-time.d.ts +35 -0
  75. package/dist/core/agent-env.d.ts +11 -0
  76. package/dist/core/agent-env.js +27 -5
  77. package/dist/core/agent-env.js.map +1 -1
  78. package/dist/core/agent-profiles.js +7 -0
  79. package/dist/core/agent-profiles.js.map +1 -1
  80. package/dist/core/agent-runner.d.ts +49 -1
  81. package/dist/core/agent-runner.js +2 -1
  82. package/dist/core/agent-runner.js.map +1 -1
  83. package/dist/core/backend-detect.d.ts +3 -2
  84. package/dist/core/backend-detect.js +27 -1
  85. package/dist/core/backend-detect.js.map +1 -1
  86. package/dist/core/claude-cli-runner.d.ts +1 -0
  87. package/dist/core/claude-cli-runner.js +69 -19
  88. package/dist/core/claude-cli-runner.js.map +1 -1
  89. package/dist/core/codex-app-server-runner.d.ts +1 -0
  90. package/dist/core/codex-app-server-runner.js +200 -25
  91. package/dist/core/codex-app-server-runner.js.map +1 -1
  92. package/dist/core/codex-app-server-transport.d.ts +7 -3
  93. package/dist/core/codex-app-server-transport.js +11 -9
  94. package/dist/core/codex-app-server-transport.js.map +1 -1
  95. package/dist/core/cursor-acp-runner.d.ts +1 -0
  96. package/dist/core/cursor-acp-runner.js +24 -9
  97. package/dist/core/cursor-acp-runner.js.map +1 -1
  98. package/dist/core/host-model-catalog.js +2 -0
  99. package/dist/core/host-model-catalog.js.map +1 -1
  100. package/dist/core/managed-session.js +2 -0
  101. package/dist/core/managed-session.js.map +1 -1
  102. package/dist/core/model-identity.js +2 -0
  103. package/dist/core/model-identity.js.map +1 -1
  104. package/dist/core/model-presets.js +3 -0
  105. package/dist/core/model-presets.js.map +1 -1
  106. package/dist/core/omp-model-catalog.d.ts +43 -0
  107. package/dist/core/omp-model-catalog.js +205 -0
  108. package/dist/core/omp-model-catalog.js.map +1 -0
  109. package/dist/core/omp-runner.d.ts +73 -0
  110. package/dist/core/omp-runner.js +990 -0
  111. package/dist/core/omp-runner.js.map +1 -0
  112. package/dist/core/omp-ui-mapper.d.ts +106 -0
  113. package/dist/core/omp-ui-mapper.js +882 -0
  114. package/dist/core/omp-ui-mapper.js.map +1 -0
  115. package/dist/core/opencode-server-runner.d.ts +18 -4
  116. package/dist/core/opencode-server-runner.js +337 -96
  117. package/dist/core/opencode-server-runner.js.map +1 -1
  118. package/dist/core/opencode-session-error.d.ts +11 -0
  119. package/dist/core/opencode-session-error.js +26 -0
  120. package/dist/core/opencode-session-error.js.map +1 -0
  121. package/dist/core/opencode-ui-mapper.js +4 -36
  122. package/dist/core/opencode-ui-mapper.js.map +1 -1
  123. package/dist/core/pi-runner.d.ts +1 -0
  124. package/dist/core/pi-runner.js +75 -12
  125. package/dist/core/pi-runner.js.map +1 -1
  126. package/dist/core/provider-auth.d.ts +9 -2
  127. package/dist/core/provider-auth.js +43 -6
  128. package/dist/core/provider-auth.js.map +1 -1
  129. package/dist/core/runner-factory.js +3 -0
  130. package/dist/core/runner-factory.js.map +1 -1
  131. package/dist/core/runner-model-catalog.js +1 -0
  132. package/dist/core/runner-model-catalog.js.map +1 -1
  133. package/dist/core/runner-stderr.d.ts +2 -0
  134. package/dist/core/runner-stderr.js +20 -0
  135. package/dist/core/runner-stderr.js.map +1 -0
  136. package/dist/core/session-process.d.ts +22 -0
  137. package/dist/core/session-process.js +202 -0
  138. package/dist/core/session-process.js.map +1 -0
  139. package/dist/core/ui-events.d.ts +1 -1
  140. package/dist/core/usage-limit.js +3 -60
  141. package/dist/core/usage-limit.js.map +1 -1
  142. package/dist/data-gitignore.d.ts +6 -0
  143. package/dist/data-gitignore.js +57 -0
  144. package/dist/data-gitignore.js.map +1 -0
  145. package/dist/delegation/capacity.d.ts +19 -0
  146. package/dist/delegation/capacity.js +34 -0
  147. package/dist/delegation/capacity.js.map +1 -0
  148. package/dist/delegation/cli.js +13 -4
  149. package/dist/delegation/cli.js.map +1 -1
  150. package/dist/delegation/conversations.d.ts +4 -2
  151. package/dist/delegation/conversations.js +8 -12
  152. package/dist/delegation/conversations.js.map +1 -1
  153. package/dist/delegation/credentials.d.ts +2 -0
  154. package/dist/delegation/credentials.js +10 -0
  155. package/dist/delegation/credentials.js.map +1 -1
  156. package/dist/delegation/destroy-observation.d.ts +42 -0
  157. package/dist/delegation/destroy-observation.js +115 -0
  158. package/dist/delegation/destroy-observation.js.map +1 -0
  159. package/dist/delegation/execution-identity.d.ts +6 -0
  160. package/dist/delegation/execution-identity.js +1 -1
  161. package/dist/delegation/execution-identity.js.map +1 -1
  162. package/dist/delegation/policy.d.ts +3 -2
  163. package/dist/delegation/policy.js +7 -5
  164. package/dist/delegation/policy.js.map +1 -1
  165. package/dist/delegation/process-liveness.d.ts +57 -20
  166. package/dist/delegation/process-liveness.js +114 -92
  167. package/dist/delegation/process-liveness.js.map +1 -1
  168. package/dist/delegation/provision.js +6 -6
  169. package/dist/delegation/provision.js.map +1 -1
  170. package/dist/delegation/results.js +4 -2
  171. package/dist/delegation/results.js.map +1 -1
  172. package/dist/delegation/retry-backoff.d.ts +23 -0
  173. package/dist/delegation/retry-backoff.js +18 -0
  174. package/dist/delegation/retry-backoff.js.map +1 -0
  175. package/dist/delegation/routes.d.ts +64 -58
  176. package/dist/delegation/scratch-cleanup.d.ts +29 -0
  177. package/dist/delegation/scratch-cleanup.js +143 -0
  178. package/dist/delegation/scratch-cleanup.js.map +1 -0
  179. package/dist/delegation/service.d.ts +20 -1
  180. package/dist/delegation/service.js +233 -70
  181. package/dist/delegation/service.js.map +1 -1
  182. package/dist/delegation/transport.d.ts +12 -6
  183. package/dist/delegation/workspace.d.ts +23 -4
  184. package/dist/delegation/workspace.js +147 -31
  185. package/dist/delegation/workspace.js.map +1 -1
  186. package/dist/git-worktree-lock-helper.js +8 -3
  187. package/dist/git-worktree-lock-helper.js.map +1 -1
  188. package/dist/git-worktree-lock.d.ts +6 -1
  189. package/dist/git-worktree-lock.js +10 -4
  190. package/dist/git-worktree-lock.js.map +1 -1
  191. package/dist/git-worktree-release.d.ts +20 -0
  192. package/dist/git-worktree-release.js +27 -0
  193. package/dist/git-worktree-release.js.map +1 -0
  194. package/dist/git-worktree.d.ts +8 -2
  195. package/dist/git-worktree.js +19 -2
  196. package/dist/git-worktree.js.map +1 -1
  197. package/dist/handoff.d.ts +2 -2
  198. package/dist/handoff.js +1 -1
  199. package/dist/index.js +109 -76
  200. package/dist/index.js.map +1 -1
  201. package/dist/paths.d.ts +1 -1
  202. package/dist/paths.js +5 -1
  203. package/dist/paths.js.map +1 -1
  204. package/dist/preview/cdp.d.ts +13 -0
  205. package/dist/preview/cdp.js +69 -0
  206. package/dist/preview/cdp.js.map +1 -0
  207. package/dist/preview/chromium.d.ts +75 -0
  208. package/dist/preview/chromium.js +394 -0
  209. package/dist/preview/chromium.js.map +1 -0
  210. package/dist/preview/dev-server.d.ts +68 -0
  211. package/dist/preview/dev-server.js +341 -0
  212. package/dist/preview/dev-server.js.map +1 -0
  213. package/dist/preview/host.d.ts +154 -0
  214. package/dist/preview/host.js +681 -0
  215. package/dist/preview/host.js.map +1 -0
  216. package/dist/preview/registration.d.ts +63 -0
  217. package/dist/preview/registration.js +109 -0
  218. package/dist/preview/registration.js.map +1 -0
  219. package/dist/preview/session.d.ts +73 -0
  220. package/dist/preview/session.js +347 -0
  221. package/dist/preview/session.js.map +1 -0
  222. package/dist/preview/url.d.ts +6 -0
  223. package/dist/preview/url.js +17 -0
  224. package/dist/preview/url.js.map +1 -0
  225. package/dist/runs/agent-tmpdir-removal.d.ts +15 -0
  226. package/dist/runs/agent-tmpdir-removal.js +60 -0
  227. package/dist/runs/agent-tmpdir-removal.js.map +1 -0
  228. package/dist/runs/agent-tmpdir.d.ts +12 -1
  229. package/dist/runs/agent-tmpdir.js +123 -34
  230. package/dist/runs/agent-tmpdir.js.map +1 -1
  231. package/dist/runs/delegation-state.d.ts +5 -1
  232. package/dist/runs/delegation-state.js +9 -0
  233. package/dist/runs/delegation-state.js.map +1 -1
  234. package/dist/runs/event-history.d.ts +4 -4
  235. package/dist/runs/event-history.js +97 -117
  236. package/dist/runs/event-history.js.map +1 -1
  237. package/dist/runs/history-compressor.d.ts +29 -0
  238. package/dist/runs/history-compressor.js +116 -0
  239. package/dist/runs/history-compressor.js.map +1 -0
  240. package/dist/runs/history-file.d.ts +36 -0
  241. package/dist/runs/history-file.js +463 -0
  242. package/dist/runs/history-file.js.map +1 -0
  243. package/dist/runs/human-ask-summary.js +18 -13
  244. package/dist/runs/human-ask-summary.js.map +1 -1
  245. package/dist/runs/legacy-index.d.ts +62 -0
  246. package/dist/runs/legacy-index.js +168 -0
  247. package/dist/runs/legacy-index.js.map +1 -0
  248. package/dist/runs/raw-record.d.ts +52 -0
  249. package/dist/runs/raw-record.js +145 -0
  250. package/dist/runs/raw-record.js.map +1 -0
  251. package/dist/runs/retention.d.ts +13 -6
  252. package/dist/runs/retention.js +42 -13
  253. package/dist/runs/retention.js.map +1 -1
  254. package/dist/runs/run-claims.d.ts +23 -0
  255. package/dist/runs/run-claims.js +24 -0
  256. package/dist/runs/run-claims.js.map +1 -0
  257. package/dist/runs/run-database.d.ts +385 -0
  258. package/dist/runs/run-database.js +930 -0
  259. package/dist/runs/run-database.js.map +1 -0
  260. package/dist/runs/run-index.d.ts +35 -9
  261. package/dist/runs/run-index.js +200 -25
  262. package/dist/runs/run-index.js.map +1 -1
  263. package/dist/runs/run-row.d.ts +44 -0
  264. package/dist/runs/run-row.js +84 -0
  265. package/dist/runs/run-row.js.map +1 -0
  266. package/dist/runs/run-search.d.ts +29 -0
  267. package/dist/runs/run-search.js +63 -0
  268. package/dist/runs/run-search.js.map +1 -0
  269. package/dist/runs/store-open-error.d.ts +24 -0
  270. package/dist/runs/store-open-error.js +61 -0
  271. package/dist/runs/store-open-error.js.map +1 -0
  272. package/dist/runs/store.d.ts +667 -32
  273. package/dist/runs/store.js +2153 -324
  274. package/dist/runs/store.js.map +1 -1
  275. package/dist/runs/transcript-facts.d.ts +77 -0
  276. package/dist/runs/transcript-facts.js +302 -0
  277. package/dist/runs/transcript-facts.js.map +1 -0
  278. package/dist/runs/webhook.js +5 -4
  279. package/dist/runs/webhook.js.map +1 -1
  280. package/dist/runs/worker-execution.d.ts +22 -0
  281. package/dist/runs/worker-execution.js +30 -0
  282. package/dist/runs/worker-execution.js.map +1 -0
  283. package/dist/server/capabilities.d.ts +1 -0
  284. package/dist/server/capabilities.js +2 -0
  285. package/dist/server/capabilities.js.map +1 -1
  286. package/dist/server/cockpit-ownership.d.ts +8 -0
  287. package/dist/server/cockpit-ownership.js +19 -0
  288. package/dist/server/cockpit-ownership.js.map +1 -1
  289. package/dist/server/forge/github.d.ts +1 -4
  290. package/dist/server/forge/github.js +47 -23
  291. package/dist/server/forge/github.js.map +1 -1
  292. package/dist/server/git.js +1 -1
  293. package/dist/server/git.js.map +1 -1
  294. package/dist/server/open-in-app.js +1 -0
  295. package/dist/server/open-in-app.js.map +1 -1
  296. package/dist/server/preview-socket.d.ts +36 -0
  297. package/dist/server/preview-socket.js +126 -0
  298. package/dist/server/preview-socket.js.map +1 -0
  299. package/dist/server/project-context.d.ts +31 -4
  300. package/dist/server/project-context.js +73 -9
  301. package/dist/server/project-context.js.map +1 -1
  302. package/dist/server/provider-action-gate.js +1 -0
  303. package/dist/server/provider-action-gate.js.map +1 -1
  304. package/dist/server/repo-branches.d.ts +4 -3
  305. package/dist/server/repo-branches.js +5 -4
  306. package/dist/server/repo-branches.js.map +1 -1
  307. package/dist/server/server.d.ts +4500 -2727
  308. package/dist/server/server.js +651 -202
  309. package/dist/server/server.js.map +1 -1
  310. package/dist/server/upgrade-router.d.ts +23 -0
  311. package/dist/server/upgrade-router.js +53 -0
  312. package/dist/server/upgrade-router.js.map +1 -0
  313. package/dist/server/validators.d.ts +6 -1
  314. package/dist/server/validators.js +4 -4
  315. package/dist/server/validators.js.map +1 -1
  316. package/dist/server/ws.d.ts +16 -16
  317. package/dist/server/ws.js +56 -54
  318. package/dist/server/ws.js.map +1 -1
  319. package/dist/skills-remote.d.ts +14 -0
  320. package/dist/skills-remote.js +91 -15
  321. package/dist/skills-remote.js.map +1 -1
  322. package/dist/task-cli/cli.js +40 -16
  323. package/dist/task-cli/cli.js.map +1 -1
  324. package/dist/task-cli/discovery.d.ts +4 -3
  325. package/dist/task-cli/discovery.js +4 -3
  326. package/dist/task-cli/discovery.js.map +1 -1
  327. package/dist/task-cli/http.d.ts +2 -0
  328. package/dist/task-cli/http.js +10 -4
  329. package/dist/task-cli/http.js.map +1 -1
  330. package/dist/task-cli/projections.d.ts +5 -2
  331. package/dist/task-cli/projections.js +13 -0
  332. package/dist/task-cli/projections.js.map +1 -1
  333. package/dist/task-cli/watch.d.ts +15 -2
  334. package/dist/task-cli/watch.js +31 -9
  335. package/dist/task-cli/watch.js.map +1 -1
  336. package/dist/ui-state.d.ts +4 -3
  337. package/dist/ui-state.js +8 -3
  338. package/dist/ui-state.js.map +1 -1
  339. package/dist/workflows/grouped-command.d.ts +31 -0
  340. package/dist/workflows/grouped-command.js +143 -0
  341. package/dist/workflows/grouped-command.js.map +1 -0
  342. package/dist/workflows/run.d.ts +223 -42
  343. package/dist/workflows/run.js +1316 -398
  344. package/dist/workflows/run.js.map +1 -1
  345. package/dist/workflows/types.d.ts +12 -0
  346. package/dist/workflows/types.js +20 -0
  347. package/dist/workflows/types.js.map +1 -1
  348. package/dist/workspace/agent-accounts.d.ts +5 -1
  349. package/dist/workspace/agent-accounts.js +1 -0
  350. package/dist/workspace/agent-accounts.js.map +1 -1
  351. package/dist/workspace/agent-profiles.js +1 -1
  352. package/dist/workspace/agent-profiles.js.map +1 -1
  353. package/dist/workspace/config.d.ts +3 -1
  354. package/dist/workspace/config.js +1 -0
  355. package/dist/workspace/config.js.map +1 -1
  356. package/dist/worktree-setup.d.ts +85 -0
  357. package/dist/worktree-setup.js +164 -0
  358. package/dist/worktree-setup.js.map +1 -0
  359. package/package.json +7 -5
  360. package/scripts/benchmark-run-store-fixture.ts +428 -0
  361. package/scripts/benchmark-run-store.ts +743 -0
  362. package/scripts/benchmark-stuck-destroys.ts +143 -0
  363. package/scripts/mock-autonomous.mjs +11 -0
  364. package/scripts/mock-ci-tool.mjs +20 -5
  365. package/scripts/mock-claude.mjs +110 -1
  366. package/scripts/mock-codex-app-server.mjs +117 -2
  367. package/scripts/mock-codex-coalesce-steer.mjs +59 -0
  368. package/scripts/mock-codex-reverse-steer.mjs +130 -0
  369. package/scripts/mock-cursor-acp.mjs +75 -1
  370. package/scripts/mock-omp-rpc.mjs +605 -0
  371. package/scripts/mock-opencode-serve.mjs +312 -22
  372. package/scripts/mock-pi-rpc.mjs +109 -3
  373. package/scripts/mock-runner-crash.mjs +42 -0
  374. package/scripts/mock-silent-tail.mjs +78 -0
  375. package/scripts/mock-turn-messages.mjs +5 -0
  376. package/scripts/omp-ci-wait.mjs +29 -0
  377. package/scripts/omp-restrict-delegation.yml +4 -0
  378. package/scripts/pi-ci-wait.mjs +12 -9
  379. package/scripts/seed-archive-profile.ts +3 -3
  380. package/web/dist/assets/agent-accounts-CFOBMVHc.js +1 -0
  381. package/web/dist/assets/alert-dialog-BrpNUwMt.js +1 -0
  382. package/web/dist/assets/app-window-CSHwDX9N.js +1 -0
  383. package/web/dist/assets/button-C_rd7CPp.js +43 -0
  384. package/web/dist/assets/compare-variants-D6WXhvXA.js +1 -0
  385. package/web/dist/assets/diff-DrEARXTV.js +3 -0
  386. package/web/dist/assets/diff-controls-B1-j7nFg.js +1 -0
  387. package/web/dist/assets/{diff-view-DPn6bQ5W.js → diff-view-DvGLjdTk.js} +3 -3
  388. package/web/dist/assets/dropdown-menu-BegOZFrj.js +1 -0
  389. package/web/dist/assets/format-DpNe3S0A.js +1 -0
  390. package/web/dist/assets/git-toolbar-Cw4ljmhB.js +1 -0
  391. package/web/dist/assets/github-CfV5sqL0.js +1 -0
  392. package/web/dist/assets/{image-preview-DJ8cgb1Z.js → image-preview-FOEfqpZP.js} +1 -1
  393. package/web/dist/assets/index-DgWWlOLy.js +13 -0
  394. package/web/dist/assets/index-fBFnuvxu.css +1 -0
  395. package/web/dist/assets/{nav-row-styles-3j64kH5s.js → nav-row-styles-vp2JbhuJ.js} +1 -1
  396. package/web/dist/assets/play-CmVAj_fu.js +1 -0
  397. package/web/dist/assets/popover-BohP6WTx.js +1 -0
  398. package/web/dist/assets/preview-pane-connected-qTA5KNoJ.js +3 -0
  399. package/web/dist/assets/preview-state-vuMviq6q.js +1 -0
  400. package/web/dist/assets/prompt-templates-Cubdut_C.js +15 -0
  401. package/web/dist/assets/repo-git-Cf4t5PD0.js +1 -0
  402. package/web/dist/assets/{run-diff-5-RRdcVz.js → run-diff-BpkUW37Z.js} +3 -3
  403. package/web/dist/assets/run-header-68WHf9wV.js +1 -0
  404. package/web/dist/assets/skills-DiVIgKeU.js +1 -0
  405. package/web/dist/assets/square-terminal-B6RYYieR.js +1 -0
  406. package/web/dist/assets/status-dot-CqTJHSP9.js +1 -0
  407. package/web/dist/assets/{tab-link-K0hjzEXR.js → tab-link-B07Hu4QX.js} +1 -1
  408. package/web/dist/assets/task-changes--lQF6EYf.js +1 -0
  409. package/web/dist/assets/task-commits-Cx0qy4DN.js +1 -0
  410. package/web/dist/assets/task-files-D50s-gKW.js +2 -0
  411. package/web/dist/assets/task-github-item-DaqmSwYT.js +1 -0
  412. package/web/dist/assets/task-thread-BPQ64lui.js +7 -0
  413. package/web/dist/assets/{task-thread-Bw_nQuTx.css → task-thread-zNE2Q9E-.css} +1 -1
  414. package/web/dist/assets/textarea-D20O4Pw0.js +1 -0
  415. package/web/dist/assets/thread-state-DWcFMrvb.js +4 -0
  416. package/web/dist/assets/use-hand-to-agent-state-BLIGmwbd.css +1 -0
  417. package/web/dist/assets/use-hand-to-agent-state-Cy9eyyfL.js +1 -0
  418. package/web/dist/assets/{utils-fzNfbPzA.js → utils-JsL3ZsJf.js} +1 -1
  419. package/web/dist/assets/workflows-jwG7uxqi.js +11 -0
  420. package/web/dist/assets/{zoomable-image-CDmZ_Rtl.js → zoomable-image-BJJ64KGc.js} +1 -1
  421. package/web/dist/cezarion-lockup-dark.svg +1 -13
  422. package/web/dist/cezarion-lockup-light.svg +1 -13
  423. package/web/dist/cezarion-mark-dark.svg +1 -12
  424. package/web/dist/cezarion-mark-light.svg +1 -12
  425. package/web/dist/index.html +16 -21
  426. package/web/dist/assets/agent-accounts-CU2ZDv24.js +0 -1
  427. package/web/dist/assets/alert-dialog-C-xYE_dg.js +0 -43
  428. package/web/dist/assets/chevron-right-BENZ-1ZN.js +0 -1
  429. package/web/dist/assets/collapsible-DD9J91Iv.js +0 -1
  430. package/web/dist/assets/compare-variants-5FmqpFSN.js +0 -1
  431. package/web/dist/assets/diff-controls-Bj0YrioE.js +0 -1
  432. package/web/dist/assets/diff-rE8rm75J2.js +0 -3
  433. package/web/dist/assets/diff-stat-BKDCSVfJ.js +0 -1
  434. package/web/dist/assets/dropdown-menu-Ch5BYQh_.js +0 -1
  435. package/web/dist/assets/format-dBiNDNVe.js +0 -1
  436. package/web/dist/assets/git-toolbar-ClVvf5iF.js +0 -1
  437. package/web/dist/assets/github-BkNOyour.css +0 -1
  438. package/web/dist/assets/github-DeZMeMwJ.js +0 -1
  439. package/web/dist/assets/github-item-detail-W5F6GVjF.js +0 -1
  440. package/web/dist/assets/github-sidebar-model-DcMu-IK2.js +0 -1
  441. package/web/dist/assets/highlighter-CdHqIwFr.js +0 -3
  442. package/web/dist/assets/index-CatemBEL.js +0 -8
  443. package/web/dist/assets/index-MvfD8bZJ.css +0 -1
  444. package/web/dist/assets/markdown-zdKeLIYA.js +0 -2
  445. package/web/dist/assets/mobile-run-bar-BCfmFi8V.js +0 -1
  446. package/web/dist/assets/prompt-templates-CBPp39IU.js +0 -15
  447. package/web/dist/assets/reference-chip-D6owncJT.js +0 -1
  448. package/web/dist/assets/repo-git-CtdkZFey.js +0 -1
  449. package/web/dist/assets/run-header-BcjoDi0W.js +0 -1
  450. package/web/dist/assets/sheet-ChnYVrp1.js +0 -1
  451. package/web/dist/assets/skills-C8YKp2vr.js +0 -1
  452. package/web/dist/assets/task-changes-BW8YdWG8.js +0 -1
  453. package/web/dist/assets/task-commits-q2kB2y6L.js +0 -1
  454. package/web/dist/assets/task-files-pPL6_3Cr.js +0 -2
  455. package/web/dist/assets/task-github-item-BXF5R52U.js +0 -1
  456. package/web/dist/assets/task-thread-DtkFL3EC.js +0 -10
  457. package/web/dist/assets/textarea-DxlkE-Yd.js +0 -1
  458. package/web/dist/assets/token-metrics-DIAjrPlM.js +0 -1
  459. package/web/dist/assets/use-desktop-CrDny2uG.js +0 -1
  460. package/web/dist/assets/use-submit-shortcut-KWJsC906.js +0 -1
  461. package/web/dist/assets/workflows-TmcPEj2K.js +0 -11
  462. /package/web/dist/assets/{lib-BQXq3kEf2.js → lib-BQXq3kEf.js} +0 -0
@@ -0,0 +1,930 @@
1
+ import { accessSync, closeSync, constants, existsSync, openSync, readSync, statSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import { DatabaseSync } from 'node:sqlite';
4
+ /**
5
+ * The transactional run store underneath `RunStore` (#779, plan step 2).
6
+ *
7
+ * One SQLite file per project, `<dataDir>/runs.db`, holding every run as a complete JSON record
8
+ * plus the handful of columns the store has to query without decoding that record. The module is
9
+ * deliberately schema-agnostic: it never imports `RunRecord`, never parses `data` or `summary`,
10
+ * and never decides what a column's value should be. The caller (RunStore's codec) computes them
11
+ * and hands over strings, so the record schema can change without this file noticing.
12
+ *
13
+ * Everything is synchronous, like the store it serves. Writes go through ONE entry point,
14
+ * `transaction()`, which commits a whole change set or nothing — the property `runs.json`'s
15
+ * debounced whole-file rewrite only approximated.
16
+ *
17
+ * Failure policy: every SQLite error leaves as a typed `RunDatabaseError` (`toRunDatabaseError`),
18
+ * and nothing here ever deletes, truncates or "repairs" a file. A corrupt database, WAL or SHM
19
+ * stays exactly where it is for the user (or a later recovery step) to inspect.
20
+ */
21
+ export const RUNS_DB_FILE = 'runs.db';
22
+ /** `meta` key whose presence means the legacy `runs.json` import committed (or there was nothing
23
+ * to import): from then on this database is authoritative and `runs.json` is history. Written in
24
+ * the same transaction as the imported rows, so it can never describe a half-finished import. */
25
+ export const RUNS_IMPORT_COMPLETE_KEY = 'import-complete';
26
+ /** `PRAGMA user_version` of a database this build created. A higher number means a newer cezar
27
+ * wrote it; that is refused rather than read, because a newer schema may store what this one
28
+ * would silently drop on its next write. */
29
+ export const RUN_DATABASE_SCHEMA_VERSION = 2;
30
+ /**
31
+ * The oldest `user_version` the READ-ONLY reader can read as it stands (`openReadOnly`). It never
32
+ * migrates, so a project this build has not opened yet is still at the version an earlier build
33
+ * left; while every migration since only adds what readers can do without (indexes), that
34
+ * database stays readable rather than reading as empty. Raise it with any migration that changes
35
+ * a table or column a reader queries.
36
+ */
37
+ export const RUN_DATABASE_READABLE_FROM = 1;
38
+ /** How long a statement waits on another connection's lock before failing as busy. Short on
39
+ * purpose: the store runs on the event loop, and a blocked write is a blocked cockpit. */
40
+ const BUSY_TIMEOUT_MS = 50;
41
+ /**
42
+ * Forward-only migrations: entry `i` takes a database from `user_version` i to i + 1. Never edit
43
+ * a shipped entry; append a new one and raise `RUN_DATABASE_SCHEMA_VERSION`.
44
+ *
45
+ * Changing `isLiveRecord` (run-row.ts) needs an entry that recomputes the `live` column for every
46
+ * row. That predicate is TypeScript over the decoded record, so such an entry cannot be SQL alone.
47
+ */
48
+ const MIGRATIONS = [
49
+ `
50
+ -- Every column beyond id/data/summary/revision exists for a query the spec names. They are
51
+ -- copies of record fields (or of one predicate over the record), computed by the caller in the
52
+ -- same upsert as data and summary, so a row can never disagree with its own columns.
53
+ CREATE TABLE runs (
54
+ -- Insertion order, the rowid itself: given by a row's first insert (runs.json order on import,
55
+ -- then the order runs are first written) and kept by every upsert. Runs created in the same
56
+ -- millisecond list in this order, as they did when every list was a stable sort of the
57
+ -- in-memory map: recovery and the queue start tied runs in it.
58
+ seq INTEGER PRIMARY KEY,
59
+ id TEXT NOT NULL UNIQUE,
60
+ -- Newest-first order: GET /run-summaries, /workspace/runs-index, the cold newest-200 read,
61
+ -- and count-based history retention (the runs past the newest N).
62
+ created_at TEXT NOT NULL,
63
+ -- Worktree retention: most recently finished first (falls back to created_at).
64
+ finished_at TEXT,
65
+ -- The mark-all-read and archive-finished sweeps select finished statuses.
66
+ status TEXT NOT NULL,
67
+ -- History retention, mark-all-read and archive-finished skip or select archived runs (0/1).
68
+ archived INTEGER NOT NULL,
69
+ -- isLiveRecord() (run-row.ts): the rows RunStore.open loads into memory and recovers. Every
70
+ -- other row stays here until something reads it. Written with the row, never recomputed
71
+ -- (see the note above MIGRATIONS).
72
+ live INTEGER NOT NULL,
73
+ -- The workers of a parent (delegation.parentRunId): delegation families, archive cascades and
74
+ -- the "every worker" recovery passes, replacing whole-map scans.
75
+ parent_run_id TEXT,
76
+ -- The run an idempotent start already created (#504).
77
+ client_request_id TEXT,
78
+ -- The members of a parallel-variant group (spec 010).
79
+ group_id TEXT,
80
+ -- A materialized worktree directory (the path, unless retention reclaimed it): worktree
81
+ -- retention, the worktrees panel and the owned-worker resource collision check.
82
+ worktree_path TEXT,
83
+ -- The branch the run owns: branch cleanup, git-log attribution and the collision check.
84
+ branch TEXT,
85
+ -- Where that branch forked, which branch cleanup reads with it.
86
+ base_branch TEXT,
87
+ -- The commit that last wrote the row: one database-wide sequence (meta 'commit-seq'), so a
88
+ -- revision is never reused, not even by a row deleted and created again under the same id.
89
+ -- A writer compares it with the revision it last read to tell whether the row changed under it.
90
+ revision INTEGER NOT NULL,
91
+ -- The complete record JSON, opaque here.
92
+ data TEXT NOT NULL,
93
+ -- The record's toRunSummary() JSON, so list routes never decode data.
94
+ summary TEXT NOT NULL
95
+ ) STRICT;
96
+ -- Newest first, ties in insertion order: the order every newest-first read asks for, so SQLite
97
+ -- walks the index instead of sorting the tie groups.
98
+ CREATE INDEX runs_created_at ON runs (created_at DESC, seq);
99
+ CREATE INDEX runs_status ON runs (status);
100
+ -- Partial indexes: each query asks for the rows that HAVE the value, and most rows do not.
101
+ CREATE INDEX runs_live ON runs (live) WHERE live = 1;
102
+ CREATE INDEX runs_parent_run_id ON runs (parent_run_id) WHERE parent_run_id IS NOT NULL;
103
+ CREATE INDEX runs_client_request_id ON runs (client_request_id) WHERE client_request_id IS NOT NULL;
104
+ CREATE INDEX runs_group_id ON runs (group_id) WHERE group_id IS NOT NULL;
105
+ CREATE INDEX runs_worktree_path ON runs (worktree_path) WHERE worktree_path IS NOT NULL;
106
+ CREATE INDEX runs_branch ON runs (branch) WHERE branch IS NOT NULL;
107
+ -- Private key/value metadata: import completion and the two sequences below.
108
+ CREATE TABLE meta (
109
+ key TEXT PRIMARY KEY,
110
+ value TEXT NOT NULL
111
+ ) STRICT;
112
+ -- Cross-process ownership (#779, plan step 3): which RunStore may write a delegation family —
113
+ -- a root and its direct workers, or an ordinary run on its own. Only the holder writes the
114
+ -- family's rows, its NDJSON and its cleanup; everyone else reads them.
115
+ CREATE TABLE run_claims (
116
+ -- The family's root id: parent_run_id for a worker, the run's own id otherwise.
117
+ family TEXT PRIMARY KEY,
118
+ -- The RunStore holding it: a random id per open, so two stores in one process differ.
119
+ session TEXT NOT NULL,
120
+ -- That store's process and the process's start identity, so a reused pid is never taken for
121
+ -- the owner. start_token is NULL only where the platform cannot read one.
122
+ pid INTEGER NOT NULL,
123
+ start_token TEXT,
124
+ -- From meta 'claim-generation', bumped by every acquisition. Every write checks that its
125
+ -- family still carries this store's session and generation.
126
+ generation INTEGER NOT NULL
127
+ ) STRICT;
128
+ CREATE INDEX run_claims_session ON run_claims (session);
129
+ -- Private recovery evidence: a write that found its row changed, created or deleted under it,
130
+ -- or its family's claim gone. Never served; kept for a person to inspect.
131
+ CREATE TABLE run_conflicts (
132
+ seq INTEGER PRIMARY KEY,
133
+ run_id TEXT NOT NULL,
134
+ detected_at TEXT NOT NULL,
135
+ session TEXT NOT NULL,
136
+ -- changed | created | deleted | claim-lost
137
+ reason TEXT NOT NULL,
138
+ -- The row as the writer last read or wrote it (the original); NULL for a row it created.
139
+ base_revision INTEGER,
140
+ base_data TEXT,
141
+ -- What the writer meant to store; NULL with local_deleted = 1 when it meant to delete it.
142
+ local_data TEXT,
143
+ local_deleted INTEGER NOT NULL,
144
+ -- The row as it stands; NULL when another writer deleted it.
145
+ current_revision INTEGER,
146
+ current_data TEXT
147
+ ) STRICT;
148
+ `,
149
+ `
150
+ -- The run lists' window (#864): every unarchived run, plus the newest archived roots. Without
151
+ -- these, each half scans the whole table — about 30 ms at 20,000 runs, on every list read.
152
+ -- Indexes only: nothing a v1 reader reads changes, which is why RUN_DATABASE_READABLE_FROM stays 1.
153
+ CREATE INDEX runs_unarchived ON runs (created_at DESC, seq) WHERE archived = 0;
154
+ CREATE INDEX runs_archived_roots ON runs (archived, created_at DESC, seq) WHERE parent_run_id IS NULL;
155
+ `,
156
+ ];
157
+ /** Every failure this module reports. `kind` is the discriminant; the subclasses exist so a
158
+ * caller can also `instanceof` the one case it handles. */
159
+ export class RunDatabaseError extends Error {
160
+ kind;
161
+ /** SQLite's primary result code, when the failure came from SQLite. */
162
+ sqliteCode;
163
+ constructor(kind, message, options = {}) {
164
+ super(message, { cause: options.cause });
165
+ this.name = new.target.name;
166
+ this.kind = kind;
167
+ this.sqliteCode = options.sqliteCode;
168
+ }
169
+ }
170
+ /** Another connection holds the lock past the busy timeout. Transient: retry later. */
171
+ export class RunDatabaseBusyError extends RunDatabaseError {
172
+ }
173
+ /** The file or its directory cannot be opened or written by this user. */
174
+ export class RunDatabasePermissionError extends RunDatabaseError {
175
+ }
176
+ /** The disk (or a page-count limit) has no room for the write. */
177
+ export class RunDatabaseDiskFullError extends RunDatabaseError {
178
+ }
179
+ /** The file is not a database, or a page is malformed. Never reset by this module. */
180
+ export class RunDatabaseCorruptError extends RunDatabaseError {
181
+ }
182
+ /** The database was written by a newer cezar whose schema this build does not know. */
183
+ export class RunDatabaseUnsupportedSchemaError extends RunDatabaseError {
184
+ found;
185
+ supported;
186
+ constructor(found, supported) {
187
+ super('unsupported-schema', `runs database schema ${found} is newer than this cezar supports (${supported})`);
188
+ this.found = found;
189
+ this.supported = supported;
190
+ }
191
+ }
192
+ /** A fenced write found rows changed, created or deleted under it, or its claim gone. Nothing was
193
+ * written. `conflicts` names every such row of the change set, each with its current row. */
194
+ export class RunConflictError extends RunDatabaseError {
195
+ conflicts;
196
+ constructor(conflicts) {
197
+ super('conflict', `runs database: ${conflicts.map((c) => `${c.id} (${c.reason})`).join(', ')} changed under this writer`);
198
+ this.conflicts = conflicts;
199
+ }
200
+ }
201
+ // SQLite primary result codes (https://sqlite.org/rescode.html). node:sqlite reports the
202
+ // EXTENDED code in `errcode`; its low byte is the primary code.
203
+ const SQLITE_PERM = 3;
204
+ const SQLITE_BUSY = 5;
205
+ const SQLITE_LOCKED = 6;
206
+ const SQLITE_READONLY = 8;
207
+ const SQLITE_CORRUPT = 11;
208
+ const SQLITE_FULL = 13;
209
+ const SQLITE_CANTOPEN = 14;
210
+ const SQLITE_AUTH = 23;
211
+ const SQLITE_NOTADB = 26;
212
+ /**
213
+ * Map a node:sqlite error (`code: 'ERR_SQLITE_ERROR'`, numeric `errcode`) to its typed class.
214
+ * Anything that is not a SQLite error — a closed connection, a bad argument — passes through
215
+ * unchanged, because wrapping it would claim a database failure that did not happen.
216
+ *
217
+ * `path`, when given, lets SQLITE_CANTOPEN be told apart: it is a permission problem only when
218
+ * the directory, or the file itself, refuses this user. A missing directory, or a directory where
219
+ * the file should be, is not — and "check your permissions" would send the user the wrong way.
220
+ */
221
+ export function toRunDatabaseError(error, path) {
222
+ if (error instanceof RunDatabaseError)
223
+ return error;
224
+ if (!(error instanceof Error) || error.code !== 'ERR_SQLITE_ERROR')
225
+ return error;
226
+ const errcode = error.errcode;
227
+ if (typeof errcode !== 'number')
228
+ return error;
229
+ const primary = errcode & 0xff;
230
+ const options = { sqliteCode: primary, cause: error };
231
+ const message = `runs database: ${error.message}`;
232
+ switch (primary) {
233
+ case SQLITE_BUSY:
234
+ case SQLITE_LOCKED:
235
+ return new RunDatabaseBusyError('busy', message, options);
236
+ case SQLITE_PERM:
237
+ case SQLITE_READONLY:
238
+ case SQLITE_AUTH:
239
+ return new RunDatabasePermissionError('permission', message, options);
240
+ case SQLITE_CANTOPEN:
241
+ return path !== undefined && accessDenied(path)
242
+ ? new RunDatabasePermissionError('permission', message, options)
243
+ : new RunDatabaseError('other', message, options);
244
+ case SQLITE_FULL:
245
+ return new RunDatabaseDiskFullError('disk-full', message, options);
246
+ case SQLITE_CORRUPT:
247
+ case SQLITE_NOTADB:
248
+ return new RunDatabaseCorruptError('corrupt', message, options);
249
+ default:
250
+ return new RunDatabaseError('other', message, options);
251
+ }
252
+ }
253
+ /** Whether this user is refused read-write access to the database file, or (when the file does
254
+ * not exist yet) to the directory that would hold it. */
255
+ function accessDenied(path) {
256
+ const target = existsSync(path) ? path : dirname(path);
257
+ if (!existsSync(target))
258
+ return false;
259
+ try {
260
+ accessSync(target, constants.R_OK | constants.W_OK);
261
+ return false;
262
+ }
263
+ catch {
264
+ return true;
265
+ }
266
+ }
267
+ /** The 16 bytes every SQLite database file starts with (https://sqlite.org/fileformat.html). */
268
+ const SQLITE_HEADER = Buffer.from('SQLite format 3\0', 'latin1');
269
+ /**
270
+ * Refuse a non-empty file that does not start with the SQLite header before SQLite sees it.
271
+ * SQLite would also report it as not a database, but closing that connection deletes the `-wal`
272
+ * and `-shm` files beside it — and those may be the only intact copy of recent commits. An empty
273
+ * file is a valid new database. Anything that is not a regular file is left to SQLite to refuse.
274
+ */
275
+ function assertDatabaseHeader(path) {
276
+ if (!statSync(path, { throwIfNoEntry: false })?.isFile())
277
+ return;
278
+ const fd = openSync(path, 'r');
279
+ let header;
280
+ try {
281
+ header = Buffer.alloc(SQLITE_HEADER.length);
282
+ header = header.subarray(0, readSync(fd, header, 0, header.length, 0));
283
+ }
284
+ finally {
285
+ closeSync(fd);
286
+ }
287
+ if (header.length === 0 || header.equals(SQLITE_HEADER))
288
+ return;
289
+ throw new RunDatabaseCorruptError('corrupt', `runs database: ${path} is not a database`, { sqliteCode: SQLITE_NOTADB });
290
+ }
291
+ const ROW_COLUMNS = 'seq, id, created_at, finished_at, status, archived, live, parent_run_id, client_request_id, group_id, worktree_path, branch, base_branch, revision, data, summary';
292
+ /** Newest first, runs created in the same millisecond in insertion order (`seq`): the one order
293
+ * every list read uses. */
294
+ const NEWEST_FIRST = 'ORDER BY created_at DESC, seq';
295
+ /** The columns a list row reads, without the record. */
296
+ const SUMMARY_SELECT = 'SELECT seq, id, created_at, revision, summary FROM runs';
297
+ /** An archived run that is not an owned worker: what the run lists' archived window counts (#864). */
298
+ export const ARCHIVED_ROOT = 'archived = 1 AND parent_run_id IS NULL';
299
+ function toSummaryRow(row) {
300
+ return {
301
+ seq: row.seq,
302
+ id: row.id,
303
+ createdAt: row.created_at,
304
+ revision: row.revision,
305
+ summary: row.summary,
306
+ };
307
+ }
308
+ function toRunRow(row) {
309
+ return {
310
+ seq: row.seq,
311
+ id: row.id,
312
+ createdAt: row.created_at,
313
+ finishedAt: row.finished_at,
314
+ status: row.status,
315
+ archived: row.archived === 1,
316
+ live: row.live === 1,
317
+ parentRunId: row.parent_run_id,
318
+ clientRequestId: row.client_request_id,
319
+ groupId: row.group_id,
320
+ worktreePath: row.worktree_path,
321
+ branch: row.branch,
322
+ baseBranch: row.base_branch,
323
+ revision: row.revision,
324
+ data: row.data,
325
+ summary: row.summary,
326
+ };
327
+ }
328
+ function validateChanges(changes) {
329
+ const upserted = new Set();
330
+ for (const row of changes.upserts) {
331
+ if (upserted.has(row.id))
332
+ throw new TypeError(`run ${row.id} is upserted twice in one transaction`);
333
+ upserted.add(row.id);
334
+ }
335
+ for (const id of changes.deletes) {
336
+ if (upserted.has(id))
337
+ throw new TypeError(`run ${id} is both upserted and deleted in one transaction`);
338
+ }
339
+ }
340
+ /** `meta` key of the database-wide commit sequence every upsert stamps as its row's revision. */
341
+ const COMMIT_SEQ_KEY = 'commit-seq';
342
+ /** `meta` key of the sequence every claim acquisition takes its generation from. */
343
+ const CLAIM_GENERATION_KEY = 'claim-generation';
344
+ const CLAIM_COLUMNS = 'family, session, pid, start_token, generation';
345
+ function toRunClaim(row) {
346
+ return {
347
+ family: row.family,
348
+ session: row.session,
349
+ pid: row.pid,
350
+ startToken: row.start_token,
351
+ generation: row.generation,
352
+ };
353
+ }
354
+ export class RunDatabase {
355
+ db;
356
+ path;
357
+ /** Set by the first statement that reports the file damaged (SQLITE_CORRUPT or NOTADB). */
358
+ damaged = false;
359
+ statements;
360
+ constructor(db, path) {
361
+ this.db = db;
362
+ this.path = path;
363
+ this.statements = {
364
+ get: db.prepare(`SELECT ${ROW_COLUMNS} FROM runs WHERE id = ?`),
365
+ getMany: db.prepare(`SELECT ${ROW_COLUMNS} FROM runs WHERE id IN (SELECT value FROM json_each(?))`),
366
+ listSummaries: db.prepare(`SELECT seq, id, created_at, revision, summary FROM runs ${NEWEST_FIRST} LIMIT ?`),
367
+ getSummaries: db.prepare(`${SUMMARY_SELECT} WHERE id IN (SELECT value FROM json_each(?))`),
368
+ listWindowSummaries: db.prepare(`${SUMMARY_SELECT} WHERE archived = 0 UNION ALL SELECT * FROM (${SUMMARY_SELECT} WHERE ${ARCHIVED_ROOT} ${NEWEST_FIRST} LIMIT ?) ${NEWEST_FIRST}`),
369
+ listRootWindowSummaries: db.prepare(`${SUMMARY_SELECT} WHERE archived = 0 AND parent_run_id IS NULL UNION ALL SELECT * FROM (${SUMMARY_SELECT} WHERE ${ARCHIVED_ROOT} ${NEWEST_FIRST} LIMIT ?) ${NEWEST_FIRST}`),
370
+ listAll: db.prepare(`SELECT ${ROW_COLUMNS} FROM runs ${NEWEST_FIRST}`),
371
+ listLive: db.prepare(`SELECT ${ROW_COLUMNS} FROM runs WHERE live = 1 ORDER BY seq`),
372
+ listByParent: db.prepare(`SELECT ${ROW_COLUMNS} FROM runs WHERE parent_run_id = ? ${NEWEST_FIRST}`),
373
+ listWorkerIds: db.prepare('SELECT id FROM runs WHERE parent_run_id IS NOT NULL'),
374
+ listIdsByParent: db.prepare('SELECT id FROM runs WHERE parent_run_id = ?'),
375
+ findByClientRequestId: db.prepare(`SELECT ${ROW_COLUMNS} FROM runs WHERE client_request_id = ? LIMIT 1`),
376
+ listByGroup: db.prepare(`SELECT ${ROW_COLUMNS} FROM runs WHERE group_id = ?`),
377
+ listWithWorktree: db.prepare(`SELECT ${ROW_COLUMNS} FROM runs WHERE worktree_path IS NOT NULL ORDER BY coalesce(finished_at, created_at) DESC, created_at DESC, seq`),
378
+ findResourceHolders: db.prepare('SELECT id FROM runs WHERE branch = :branch UNION SELECT id FROM runs WHERE worktree_path = :worktreePath'),
379
+ listIds: db.prepare('SELECT id FROM runs'),
380
+ has: db.prepare('SELECT 1 FROM runs WHERE id = ?'),
381
+ listRevisions: db.prepare('SELECT id, revision FROM runs ORDER BY id'),
382
+ getMeta: db.prepare('SELECT value FROM meta WHERE key = ?'),
383
+ upsert: db.prepare(`
384
+ INSERT INTO runs (id, created_at, finished_at, status, archived, live, parent_run_id, client_request_id, group_id,
385
+ worktree_path, branch, base_branch, revision, data, summary)
386
+ VALUES (:id, :createdAt, :finishedAt, :status, :archived, :live, :parentRunId, :clientRequestId, :groupId,
387
+ :worktreePath, :branch, :baseBranch, :revision, :data, :summary)
388
+ ON CONFLICT (id) DO UPDATE SET
389
+ created_at = excluded.created_at, finished_at = excluded.finished_at, status = excluded.status,
390
+ archived = excluded.archived, live = excluded.live, parent_run_id = excluded.parent_run_id,
391
+ client_request_id = excluded.client_request_id, group_id = excluded.group_id,
392
+ worktree_path = excluded.worktree_path, branch = excluded.branch, base_branch = excluded.base_branch,
393
+ revision = excluded.revision, data = excluded.data, summary = excluded.summary
394
+ RETURNING seq`),
395
+ delete: db.prepare('DELETE FROM runs WHERE id = ?'),
396
+ setMeta: db.prepare('INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT (key) DO UPDATE SET value = excluded.value'),
397
+ deleteMeta: db.prepare('DELETE FROM meta WHERE key = ?'),
398
+ nextSequence: db.prepare(`INSERT INTO meta (key, value) VALUES (?, '1')
399
+ ON CONFLICT (key) DO UPDATE SET value = CAST(CAST(value AS INTEGER) + 1 AS TEXT) RETURNING value`),
400
+ familyOf: db.prepare('SELECT coalesce(parent_run_id, id) AS family FROM runs WHERE id = ?'),
401
+ familyHasLive: db.prepare('SELECT 1 FROM runs WHERE live = 1 AND (id = :family OR parent_run_id = :family) LIMIT 1'),
402
+ getClaim: db.prepare(`SELECT ${CLAIM_COLUMNS} FROM run_claims WHERE family = ?`),
403
+ getClaims: db.prepare(`SELECT ${CLAIM_COLUMNS} FROM run_claims WHERE family IN (SELECT value FROM json_each(?))`),
404
+ listClaims: db.prepare(`SELECT ${CLAIM_COLUMNS} FROM run_claims`),
405
+ putClaim: db.prepare(`INSERT INTO run_claims (family, session, pid, start_token, generation)
406
+ VALUES (:family, :session, :pid, :startToken, :generation)
407
+ ON CONFLICT (family) DO UPDATE SET session = excluded.session, pid = excluded.pid,
408
+ start_token = excluded.start_token, generation = excluded.generation`),
409
+ releaseClaims: db.prepare('DELETE FROM run_claims WHERE session = ? AND family IN (SELECT value FROM json_each(?))'),
410
+ releaseSessionClaims: db.prepare('DELETE FROM run_claims WHERE session = ?'),
411
+ listForeignClaimedIds: db.prepare(`SELECT runs.id AS id, run_claims.family AS family FROM run_claims JOIN runs
412
+ ON runs.id = run_claims.family OR runs.parent_run_id = run_claims.family WHERE run_claims.session <> ?`),
413
+ insertConflict: db.prepare(`INSERT INTO run_conflicts (run_id, detected_at, session, reason, base_revision, base_data,
414
+ local_data, local_deleted, current_revision, current_data)
415
+ VALUES (:runId, :detectedAt, :session, :reason, :baseRevision, :baseData, :localData, :localDeleted,
416
+ :currentRevision, :currentData) RETURNING seq`),
417
+ listConflicts: db.prepare('SELECT * FROM run_conflicts ORDER BY seq'),
418
+ };
419
+ }
420
+ /**
421
+ * Open (creating if absent) the database at `path` and bring its schema up to date. The
422
+ * directory must already exist; creating it is the caller's decision. On any failure the
423
+ * connection is closed and no existing data is deleted or rewritten — but a fresh path may be
424
+ * left holding an empty database (and its WAL), which the next open migrates. A reader that must
425
+ * create nothing uses `openReadOnly`.
426
+ */
427
+ static open(path) {
428
+ let db;
429
+ try {
430
+ assertDatabaseHeader(path);
431
+ db = new DatabaseSync(path);
432
+ // busy_timeout first, so every later statement here (including the migration) waits on a
433
+ // concurrent opener instead of failing at once.
434
+ db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`);
435
+ db.exec('PRAGMA journal_mode = WAL');
436
+ db.exec('PRAGMA synchronous = NORMAL');
437
+ migrate(db);
438
+ return new RunDatabase(db, path);
439
+ }
440
+ catch (error) {
441
+ const typed = toRunDatabaseError(error, path);
442
+ if (db && typed instanceof RunDatabaseCorruptError)
443
+ closeLeavingFiles(db, path);
444
+ else if (db?.isOpen)
445
+ db.close();
446
+ throw typed;
447
+ }
448
+ }
449
+ /**
450
+ * Open an existing database for reading only: never creates the file, never migrates it, never
451
+ * changes journal mode. Answers `null` when there is nothing this build can read yet — no file,
452
+ * or a file below `RUN_DATABASE_READABLE_FROM` (schema 0 is what a failed first open leaves) —
453
+ * so the caller can fall back to whatever it read before the database existed. A database from
454
+ * `RUN_DATABASE_READABLE_FROM` up to this build's schema is read as it stands. A newer schema is
455
+ * still refused.
456
+ *
457
+ * SQLite may create the `-wal`/`-shm` coordination files beside the database; that is the price
458
+ * of seeing a live writer's commits. In a read-only directory with neither file present it
459
+ * cannot, and the open fails as permission denied — deliberately not worked around with
460
+ * `immutable=1`, which would read a stale snapshot without saying so.
461
+ */
462
+ static openReadOnly(path) {
463
+ if (!existsSync(path))
464
+ return null;
465
+ let db;
466
+ try {
467
+ assertDatabaseHeader(path);
468
+ db = new DatabaseSync(path, { readOnly: true });
469
+ db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`);
470
+ const found = readSchemaVersion(db);
471
+ if (found > RUN_DATABASE_SCHEMA_VERSION)
472
+ throw new RunDatabaseUnsupportedSchemaError(found, RUN_DATABASE_SCHEMA_VERSION);
473
+ if (found < RUN_DATABASE_READABLE_FROM) {
474
+ db.close();
475
+ return null;
476
+ }
477
+ return new RunDatabase(db, path);
478
+ }
479
+ catch (error) {
480
+ if (db?.isOpen)
481
+ db.close();
482
+ throw toRunDatabaseError(error, path);
483
+ }
484
+ }
485
+ get(id) {
486
+ const row = this.run(() => this.statements.get.get(id));
487
+ return row ? toRunRow(row) : undefined;
488
+ }
489
+ /** The rows for `ids` that exist, in no particular order. */
490
+ getMany(ids) {
491
+ if (ids.length === 0)
492
+ return [];
493
+ return this.run(() => this.statements.getMany.all(JSON.stringify(ids))).map(toRunRow);
494
+ }
495
+ /** Summaries newest first (`created_at` descending, then insertion order). Archived runs are included,
496
+ * as every list route includes them. Without a limit, every row. */
497
+ listSummaries(options = {}) {
498
+ const limit = options.limit ?? -1;
499
+ return this.run(() => this.statements.listSummaries.all(limit)).map(toSummaryRow);
500
+ }
501
+ /**
502
+ * The run lists' window (#864), newest first: every unarchived row, plus the newest
503
+ * `archivedLimit` archived roots. Archived workers are never in it — no list renders them, and
504
+ * counting them is what let them push an older live root out of a newest-N window.
505
+ */
506
+ listWindowSummaries(archivedLimit, options = {}) {
507
+ const statement = options.roots ? this.statements.listRootWindowSummaries : this.statements.listWindowSummaries;
508
+ return this.run(() => statement.all(archivedLimit)).map(toSummaryRow);
509
+ }
510
+ /** The summary rows of `ids` that exist, in no particular order. */
511
+ getSummaries(ids) {
512
+ if (ids.length === 0)
513
+ return [];
514
+ return this.run(() => this.statements.getSummaries.all(JSON.stringify(ids))).map(toSummaryRow);
515
+ }
516
+ /** Every root row (archived or not) whose stored summary contains each `prefilter` token,
517
+ * lowercased, newest first: the candidates of a run search (#864), which the caller still
518
+ * checks with `matchesRunQuery`. No tokens, every root. */
519
+ searchRootSummaries(prefilter) {
520
+ const rows = [];
521
+ this.visitRootSummaries(prefilter, (row) => { rows.push(row); return true; });
522
+ return rows;
523
+ }
524
+ /** The rows `searchRootSummaries` answers, one at a time, until `visit` answers false: a search
525
+ * that needs only its first matches stops reading there. */
526
+ visitRootSummaries(prefilter, visit) {
527
+ const where = ['parent_run_id IS NULL', ...prefilter.map(() => 'instr(lower(summary), ?) > 0')].join(' AND ');
528
+ this.run(() => {
529
+ for (const row of this.db.prepare(`${SUMMARY_SELECT} WHERE ${where} ${NEWEST_FIRST}`).iterate(...prefilter)) {
530
+ if (!visit(toSummaryRow(row)))
531
+ break;
532
+ }
533
+ });
534
+ }
535
+ /** Every row, newest first (`created_at` descending, then insertion order). */
536
+ listAll() {
537
+ return this.run(() => this.statements.listAll.all()).map(toRunRow);
538
+ }
539
+ /** The rows `RunStore.open` holds in memory: those whose `live` column is set, in insertion order. */
540
+ listLive() {
541
+ return this.run(() => this.statements.listLive.all()).map(toRunRow);
542
+ }
543
+ /** The workers of one parent, newest first. */
544
+ listByParent(parentId) {
545
+ return this.run(() => this.statements.listByParent.all(parentId)).map(toRunRow);
546
+ }
547
+ /** The ids of one parent's workers, in no particular order, without reading their records. */
548
+ listIdsByParent(parentId) {
549
+ return this.run(() => this.statements.listIdsByParent.all(parentId)).map((row) => row.id);
550
+ }
551
+ /** The id of every worker row, in no particular order. */
552
+ listWorkerIds() {
553
+ return this.run(() => this.statements.listWorkerIds.all()).map((row) => row.id);
554
+ }
555
+ findByClientRequestId(clientRequestId) {
556
+ const row = this.run(() => this.statements.findByClientRequestId.get(clientRequestId));
557
+ return row ? toRunRow(row) : undefined;
558
+ }
559
+ /** The members of one variant group, in no particular order. */
560
+ listByGroup(groupId) {
561
+ return this.run(() => this.statements.listByGroup.all(groupId)).map(toRunRow);
562
+ }
563
+ /** Rows with a materialized worktree, most recently finished (else created) first, then newest first. */
564
+ listWithWorktree() {
565
+ return this.run(() => this.statements.listWithWorktree.all()).map(toRunRow);
566
+ }
567
+ /** The rows matching `where` (by default, every row that owns a branch) as branch owners,
568
+ * with their summary, in no particular order. Same contract for `where` as `listWhere`. */
569
+ listBranchOwners(where = 'branch IS NOT NULL') {
570
+ const sql = `SELECT id, created_at, status, archived, branch, base_branch, summary FROM runs WHERE ${where}`;
571
+ return this.run(() => this.db.prepare(sql).all()).map((row) => ({
572
+ id: row.id,
573
+ createdAt: row.created_at,
574
+ status: row.status,
575
+ archived: row.archived === 1,
576
+ branch: row.branch,
577
+ baseBranch: row.base_branch,
578
+ summary: row.summary,
579
+ }));
580
+ }
581
+ /** The ids of the rows that own `branch` or hold a worktree at `worktreePath`. */
582
+ findResourceHolders(resource) {
583
+ return this.run(() => this.statements.findResourceHolders.all(resource)).map((row) => row.id);
584
+ }
585
+ /** Every id, in no particular order. */
586
+ listIds() {
587
+ return this.run(() => this.statements.listIds.all()).map((row) => row.id);
588
+ }
589
+ has(id) {
590
+ return this.run(() => this.statements.has.get(id)) !== undefined;
591
+ }
592
+ /**
593
+ * Rows matching `where`, newest first — the store's candidate filters (mark all read, archive
594
+ * finished, retention, the reference heal), which combine these columns with `json_extract`
595
+ * over `summary`. `where` is the caller's constant SQL; values go in `params`, never into it.
596
+ * `offset` skips the newest rows first (retention keeps the newest N).
597
+ */
598
+ listWhere(where, params = [], options = {}) {
599
+ const sql = `SELECT ${ROW_COLUMNS} FROM runs WHERE ${where} ${NEWEST_FIRST} LIMIT -1 OFFSET ?`;
600
+ return this.run(() => this.db.prepare(sql).all(...params, options.offset ?? 0)).map(toRunRow);
601
+ }
602
+ /** The id and creation time of each row matching `where`, newest first, read off the
603
+ * `created_at` index without touching `data` or `summary` (history retention's ranking).
604
+ * Same contract for `where` as `listWhere`. */
605
+ listKeysWhere(where, params = []) {
606
+ const sql = `SELECT seq, id, created_at FROM runs WHERE ${where} ${NEWEST_FIRST}`;
607
+ return this.run(() => this.db.prepare(sql).all(...params))
608
+ .map((row) => ({ id: row.id, createdAt: row.created_at, seq: row.seq }));
609
+ }
610
+ /** Every id with its revision, ordered by id. */
611
+ listRevisions() {
612
+ return this.run(() => this.statements.listRevisions.all()).map((row) => ({
613
+ id: row.id,
614
+ revision: row.revision,
615
+ }));
616
+ }
617
+ getMeta(key) {
618
+ const row = this.run(() => this.statements.getMeta.get(key));
619
+ return row ? row.value : undefined;
620
+ }
621
+ /**
622
+ * The only write of run rows. Deletes, upserts and metadata commit together or not at all: on
623
+ * any failure the transaction is rolled back and the error is rethrown typed, with the database
624
+ * exactly as it was. Every upserted row gets this commit's revision from the database-wide
625
+ * sequence. With a `fence`, nothing is written unless every claim and row is still as the writer
626
+ * last saw it (`RunConflictError` otherwise).
627
+ */
628
+ transaction(changes) {
629
+ validateChanges(changes);
630
+ const revisions = new Map();
631
+ const seqs = new Map();
632
+ let claims = new Map();
633
+ let skipped = false;
634
+ this.run(() => {
635
+ this.db.exec('BEGIN IMMEDIATE');
636
+ try {
637
+ if (changes.onlyIfMetaAbsent !== undefined && this.statements.getMeta.get(changes.onlyIfMetaAbsent) !== undefined) {
638
+ skipped = true;
639
+ this.db.exec('ROLLBACK');
640
+ return;
641
+ }
642
+ if (changes.fence)
643
+ claims = this.checkFence(changes.fence);
644
+ for (const id of changes.deletes)
645
+ this.statements.delete.run(id);
646
+ const revision = changes.upserts.length > 0 ? this.nextSequence(COMMIT_SEQ_KEY) : 0;
647
+ for (const row of changes.upserts) {
648
+ const inserted = this.statements.upsert.get({
649
+ id: row.id,
650
+ createdAt: row.createdAt,
651
+ finishedAt: row.finishedAt ?? null,
652
+ status: row.status,
653
+ archived: row.archived ? 1 : 0,
654
+ live: row.live ? 1 : 0,
655
+ parentRunId: row.parentRunId ?? null,
656
+ clientRequestId: row.clientRequestId ?? null,
657
+ groupId: row.groupId ?? null,
658
+ worktreePath: row.worktreePath ?? null,
659
+ branch: row.branch ?? null,
660
+ baseBranch: row.baseBranch ?? null,
661
+ revision,
662
+ data: row.data,
663
+ summary: row.summary,
664
+ });
665
+ revisions.set(row.id, revision);
666
+ seqs.set(row.id, inserted.seq);
667
+ }
668
+ for (const [key, value] of Object.entries(changes.meta ?? {})) {
669
+ if (value === null)
670
+ this.statements.deleteMeta.run(key);
671
+ else
672
+ this.statements.setMeta.run(key, value);
673
+ }
674
+ for (const [key, value] of Object.entries(changes.beforeCommit?.() ?? {}))
675
+ this.statements.setMeta.run(key, value);
676
+ this.db.exec('COMMIT');
677
+ }
678
+ catch (error) {
679
+ rollback(this.db);
680
+ throw error;
681
+ }
682
+ });
683
+ return { revisions, seqs, claims, skipped };
684
+ }
685
+ /** Inside a write transaction: take the claims the fence asks for, then throw `RunConflictError`
686
+ * naming every row of the fence that is not as its writer last saw it, before anything is
687
+ * written (the claims taken roll back with it). Returns the claims taken. */
688
+ checkFence(fence) {
689
+ const lost = new Set();
690
+ const taken = new Map();
691
+ for (const [family, expected] of fence.claims) {
692
+ const claim = this.statements.getClaim.get(family);
693
+ if ('generation' in expected) {
694
+ if (!claim || claim.session !== fence.owner.session || claim.generation !== expected.generation)
695
+ lost.add(family);
696
+ continue;
697
+ }
698
+ const unchanged = expected.take === null ? !claim
699
+ : claim?.session === expected.take.session && claim.generation === expected.take.generation;
700
+ if (!unchanged) {
701
+ lost.add(family);
702
+ continue;
703
+ }
704
+ const generation = this.nextSequence(CLAIM_GENERATION_KEY);
705
+ this.statements.putClaim.run({ family, ...fence.owner, generation });
706
+ taken.set(family, generation);
707
+ }
708
+ const conflicts = [];
709
+ for (const [id, expected] of fence.rows) {
710
+ const found = this.statements.get.get(id);
711
+ const current = found ? toRunRow(found) : undefined;
712
+ const reason = lost.has(expected.family) ? 'claim-lost'
713
+ : expected.revision === null ? (current ? 'created' : undefined)
714
+ : !current ? 'deleted' : current.revision !== expected.revision ? 'changed' : undefined;
715
+ if (reason)
716
+ conflicts.push({ id, reason, current });
717
+ }
718
+ if (conflicts.length > 0)
719
+ throw new RunConflictError(conflicts);
720
+ return taken;
721
+ }
722
+ /** Inside a write transaction: the next value of a database-wide sequence kept in `meta`. */
723
+ nextSequence(key) {
724
+ return Number(this.statements.nextSequence.get(key).value);
725
+ }
726
+ /** The family a stored run belongs to (`parent_run_id`, else its own id), or undefined. */
727
+ familyOf(id) {
728
+ const row = this.run(() => this.statements.familyOf.get(id));
729
+ return row ? row.family : undefined;
730
+ }
731
+ /** Whether any row of `family` (its root or a direct worker) is live. */
732
+ familyHasLive(family) {
733
+ return this.run(() => this.statements.familyHasLive.get({ family })) !== undefined;
734
+ }
735
+ getClaim(family) {
736
+ const row = this.run(() => this.statements.getClaim.get(family));
737
+ return row ? toRunClaim(row) : undefined;
738
+ }
739
+ /** The claims on `families` that exist, by family. */
740
+ getClaims(families) {
741
+ if (families.length === 0)
742
+ return new Map();
743
+ return new Map(this.run(() => this.statements.getClaims.all(JSON.stringify(families))).map((row) => [row.family, toRunClaim(row)]));
744
+ }
745
+ listClaims() {
746
+ return this.run(() => this.statements.listClaims.all()).map(toRunClaim);
747
+ }
748
+ /**
749
+ * Take the claims in `take` for `owner`, in one transaction. Each is taken only when its claim
750
+ * is still exactly what the caller judged it to be: absent (`expect: null`) or held by the dead
751
+ * owner it read (same session and generation) — so two takers racing for one family cannot both
752
+ * win. Returns the generation of each claim taken.
753
+ */
754
+ takeClaims(owner, take) {
755
+ const taken = new Map();
756
+ if (take.length === 0)
757
+ return taken;
758
+ this.run(() => {
759
+ this.db.exec('BEGIN IMMEDIATE');
760
+ try {
761
+ for (const { family, expect } of take) {
762
+ const current = this.statements.getClaim.get(family);
763
+ const unchanged = expect === null ? !current
764
+ : current?.session === expect.session && current.generation === expect.generation;
765
+ if (!unchanged)
766
+ continue;
767
+ const generation = this.nextSequence(CLAIM_GENERATION_KEY);
768
+ this.statements.putClaim.run({ family, ...owner, generation });
769
+ taken.set(family, generation);
770
+ }
771
+ this.db.exec('COMMIT');
772
+ }
773
+ catch (error) {
774
+ rollback(this.db);
775
+ throw error;
776
+ }
777
+ });
778
+ return taken;
779
+ }
780
+ /** Drop `session`'s claims on `families`, or on everything it holds when omitted. */
781
+ releaseClaims(session, families) {
782
+ this.run(() => {
783
+ if (families === undefined)
784
+ this.statements.releaseSessionClaims.run(session);
785
+ else if (families.length > 0)
786
+ this.statements.releaseClaims.run(session, JSON.stringify(families));
787
+ });
788
+ }
789
+ /** Every row whose family a session other than `session` claims, live or not, with that family. */
790
+ listForeignClaimedIds(session) {
791
+ return this.run(() => this.statements.listForeignClaimedIds.all(session)).map((row) => ({ id: row.id, family: row.family }));
792
+ }
793
+ /** Store conflict evidence in one transaction; returns each entry's `seq`. */
794
+ recordConflicts(session, entries) {
795
+ const detectedAt = new Date().toISOString();
796
+ const seqs = [];
797
+ this.run(() => {
798
+ this.db.exec('BEGIN IMMEDIATE');
799
+ try {
800
+ for (const entry of entries) {
801
+ seqs.push(this.statements.insertConflict.get({ ...entry, session, detectedAt, localDeleted: entry.localDeleted ? 1 : 0 }).seq);
802
+ }
803
+ this.db.exec('COMMIT');
804
+ }
805
+ catch (error) {
806
+ rollback(this.db);
807
+ throw error;
808
+ }
809
+ });
810
+ return seqs;
811
+ }
812
+ /** Every stored conflict, oldest first (diagnostics and tests). */
813
+ listConflicts() {
814
+ return this.run(() => this.statements.listConflicts.all()).map((row) => ({
815
+ seq: row.seq,
816
+ runId: row.run_id,
817
+ detectedAt: row.detected_at,
818
+ session: row.session,
819
+ reason: row.reason,
820
+ baseRevision: row.base_revision,
821
+ baseData: row.base_data,
822
+ localData: row.local_data,
823
+ localDeleted: row.local_deleted === 1,
824
+ currentRevision: row.current_revision,
825
+ currentData: row.current_data,
826
+ }));
827
+ }
828
+ /** The connection's effective settings, for diagnostics. */
829
+ pragmas() {
830
+ return this.run(() => ({
831
+ journalMode: this.db.prepare('PRAGMA journal_mode').get().journal_mode,
832
+ synchronous: this.db.prepare('PRAGMA synchronous').get().synchronous,
833
+ busyTimeoutMs: this.db.prepare('PRAGMA busy_timeout').get().timeout,
834
+ }));
835
+ }
836
+ /** Idempotent: closing a closed database does nothing. Once any statement found the file
837
+ * damaged, the close leaves every file exactly as it is (`closeLeavingFiles`). */
838
+ close() {
839
+ if (this.damaged)
840
+ closeLeavingFiles(this.db, this.path);
841
+ else if (this.db.isOpen)
842
+ this.db.close();
843
+ }
844
+ run(fn) {
845
+ try {
846
+ return fn();
847
+ }
848
+ catch (error) {
849
+ const typed = toRunDatabaseError(error, this.path);
850
+ if (typed instanceof RunDatabaseCorruptError)
851
+ this.damaged = true;
852
+ throw typed;
853
+ }
854
+ }
855
+ }
856
+ /** Connections to a damaged database that could not be closed safely (`closeLeavingFiles`). */
857
+ const leftOpen = [];
858
+ /**
859
+ * Close `db` without letting SQLite touch the files of a damaged database. The last connection to
860
+ * close checkpoints the WAL into the database file and then deletes `-wal` and `-shm`: over a
861
+ * malformed page that rewrites the damaged file and removes what may be the only intact copy of
862
+ * the latest commits. A read-only connection opened first keeps a shared lock on the file, so the
863
+ * closing one is not the last and does neither; the read-only one cannot write, so its own close
864
+ * does neither either. Any failure on the way still closes `db` the same guarded way or not at all.
865
+ */
866
+ function closeLeavingFiles(db, path) {
867
+ if (!db.isOpen)
868
+ return;
869
+ let guard;
870
+ try {
871
+ guard = new DatabaseSync(path, { readOnly: true });
872
+ // A read takes the shared lock; a WAL database keeps it for as long as the connection is open.
873
+ guard.prepare('PRAGMA user_version').get();
874
+ }
875
+ catch {
876
+ // Without a guard, leaving the connection open is the only close that changes nothing. Held
877
+ // here so garbage collection cannot close it either; the process lets it go when it exits.
878
+ guard?.close();
879
+ leftOpen.push(db);
880
+ return;
881
+ }
882
+ try {
883
+ db.close();
884
+ }
885
+ finally {
886
+ guard.close();
887
+ }
888
+ }
889
+ function readSchemaVersion(db) {
890
+ return db.prepare('PRAGMA user_version').get().user_version;
891
+ }
892
+ /**
893
+ * Bring `db` to `RUN_DATABASE_SCHEMA_VERSION`, or refuse a newer one. A current database is only
894
+ * read, never written. The version is re-read inside the write transaction, so two processes
895
+ * opening a fresh file at once cannot both run the same migration.
896
+ */
897
+ function migrate(db) {
898
+ const found = readSchemaVersion(db);
899
+ if (found > RUN_DATABASE_SCHEMA_VERSION)
900
+ throw new RunDatabaseUnsupportedSchemaError(found, RUN_DATABASE_SCHEMA_VERSION);
901
+ if (found === RUN_DATABASE_SCHEMA_VERSION)
902
+ return;
903
+ db.exec('BEGIN IMMEDIATE');
904
+ try {
905
+ let version = readSchemaVersion(db);
906
+ if (version > RUN_DATABASE_SCHEMA_VERSION)
907
+ throw new RunDatabaseUnsupportedSchemaError(version, RUN_DATABASE_SCHEMA_VERSION);
908
+ for (; version < RUN_DATABASE_SCHEMA_VERSION; version++)
909
+ db.exec(MIGRATIONS[version]);
910
+ db.exec(`PRAGMA user_version = ${RUN_DATABASE_SCHEMA_VERSION}`);
911
+ db.exec('COMMIT');
912
+ }
913
+ catch (error) {
914
+ rollback(db);
915
+ throw error;
916
+ }
917
+ }
918
+ /** Undo an open transaction, if there is one. Never throws: the caller is already handling the
919
+ * error that got it here, and a failed rollback must not replace that error with its own. SQLite
920
+ * rolls back by itself on FULL, IOERR and BUSY, which is why "no transaction" is not a failure. */
921
+ function rollback(db) {
922
+ try {
923
+ if (db.isTransaction)
924
+ db.exec('ROLLBACK');
925
+ }
926
+ catch {
927
+ // The connection is closed or broken; whatever the transaction held is gone with it.
928
+ }
929
+ }
930
+ //# sourceMappingURL=run-database.js.map