chainlesschain 0.163.2 → 0.163.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/package.json +3 -2
  2. package/scripts/command-lifecycle-report.mjs +330 -38
  3. package/src/assets/web-panel/assets/{AIOps-DnGsmsqJ.js → AIOps-DB5X7qqV.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-CrO6fcQ7.js → ActionButton-DxHRtKLk.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-DsBdOaP2.js → Analytics-DIH5QrxT.js} +3 -3
  6. package/src/assets/web-panel/assets/{AppLayout-DmQslKUB.js → AppLayout-uu0oxVz7.js} +5 -5
  7. package/src/assets/web-panel/assets/{Artifacts-BvyrT_QQ.js → Artifacts-BNSMiW9Y.js} +1 -1
  8. package/src/assets/web-panel/assets/{Audit-prndoLiC.js → Audit-DcAzB4_x.js} +1 -1
  9. package/src/assets/web-panel/assets/{BackgroundAgents-DOTFyA55.js → BackgroundAgents-ClSsvjIc.js} +1 -1
  10. package/src/assets/web-panel/assets/{Backup-9zOnPp_8.js → Backup-BMciY0js.js} +1 -1
  11. package/src/assets/web-panel/assets/{BaseInput-C4PC6sAG.js → BaseInput-Hn3b57Sh.js} +1 -1
  12. package/src/assets/web-panel/assets/{Chat-BBAF-W9v.js → Chat-CoSlC53V.js} +6 -6
  13. package/src/assets/web-panel/assets/{ChatBubbleRenderer-CWhvvloO.js → ChatBubbleRenderer-CFevJ3So.js} +1 -1
  14. package/src/assets/web-panel/assets/{Checkbox-BI9iculd.js → Checkbox-B4L4Eu4w.js} +1 -1
  15. package/src/assets/web-panel/assets/{Codegen-3DP9BCKb.js → Codegen-C77FgRxF.js} +1 -1
  16. package/src/assets/web-panel/assets/{Col-df-2TnJX.js → Col-BrLtJLH3.js} +1 -1
  17. package/src/assets/web-panel/assets/{Community-CHs8gG_A.js → Community-Be7pXXhA.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compact-CDfF6q7V.js → Compact-DuGPecjo.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compliance-CthrDnO7.js → Compliance-CzS66mjP.js} +1 -1
  20. package/src/assets/web-panel/assets/{Cowork-HjIqsWJw.js → Cowork-CEhjCoyK.js} +2 -2
  21. package/src/assets/web-panel/assets/{Cron-BKc196fb.js → Cron-D7ir8Vja.js} +2 -2
  22. package/src/assets/web-panel/assets/{Crosschain-CJnjdc8W.js → Crosschain-BcXmuLxf.js} +1 -1
  23. package/src/assets/web-panel/assets/{DID-BHUy-8Q2.js → DID-Cto1dtBy.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dashboard-QDB7sKeY.js → Dashboard-B7KjgwOX.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dropdown-DStcVK11.js → Dropdown-COMqxijg.js} +1 -1
  26. package/src/assets/web-panel/assets/{EmailListRenderer-BwSUskMJ.js → EmailListRenderer-BZrya-nf.js} +1 -1
  27. package/src/assets/web-panel/assets/{FamilyGuardDashboard-DgF_kurT.js → FamilyGuardDashboard-AmVeMq0J.js} +1 -1
  28. package/src/assets/web-panel/assets/{Federation-k8wvv-wN.js → Federation-kZU8c-27.js} +1 -1
  29. package/src/assets/web-panel/assets/{FormItemContext-C11qoq2r.js → FormItemContext-sOV22v2-.js} +1 -1
  30. package/src/assets/web-panel/assets/{GenericCardRenderer-BIIpr9kg.js → GenericCardRenderer-BUuLtGCz.js} +1 -1
  31. package/src/assets/web-panel/assets/{Git-BlA1exTe.js → Git-K-ZxsLsH.js} +2 -2
  32. package/src/assets/web-panel/assets/{Governance-CYa1ZYM4.js → Governance-D3OEnawI.js} +1 -1
  33. package/src/assets/web-panel/assets/{Inference-6_kKVIt6.js → Inference-Bll0gRmP.js} +1 -1
  34. package/src/assets/web-panel/assets/{KnowledgeGraph-CB4AwxYy.js → KnowledgeGraph-DBXwjr53.js} +1 -1
  35. package/src/assets/web-panel/assets/{Logs-xf7nHFAe.js → Logs-CEzPwEfP.js} +2 -2
  36. package/src/assets/web-panel/assets/{MarkdownRenderer-Bf3DEqP6.js → MarkdownRenderer-CLmCbHx6.js} +1 -1
  37. package/src/assets/web-panel/assets/{Marketplace-aPg8e4Jt.js → Marketplace-BYyRyOPz.js} +1 -1
  38. package/src/assets/web-panel/assets/{McpTools-C-TNlVUf.js → McpTools-DVu8w_rM.js} +5 -5
  39. package/src/assets/web-panel/assets/{Memory-DMTPIpGa.js → Memory-tenuZ_hO.js} +2 -2
  40. package/src/assets/web-panel/assets/{MobileBridge-BxyvveRq.js → MobileBridge-Bxgy4ozS.js} +1 -1
  41. package/src/assets/web-panel/assets/{MobileProjects-DyPCrAyQ.js → MobileProjects-Dgx1D6sc.js} +1 -1
  42. package/src/assets/web-panel/assets/{Mtc-BdhqNXp6.js → Mtc-CRfJ8r8j.js} +5 -5
  43. package/src/assets/web-panel/assets/{MtcAudit-Cz-HFxzW.js → MtcAudit-BBJq4Aiz.js} +5 -5
  44. package/src/assets/web-panel/assets/{Multisig-CIUaFNsx.js → Multisig-B3ii9lai.js} +3 -3
  45. package/src/assets/web-panel/assets/{NLProgramming-CWe6-J17.js → NLProgramming-FIi-Cubv.js} +1 -1
  46. package/src/assets/web-panel/assets/{Notes-vYAKeviU.js → Notes-DIq8MPf1.js} +3 -3
  47. package/src/assets/web-panel/assets/{NotificationSettings-DQL9-2d7.js → NotificationSettings-BFOD9eGd.js} +1 -1
  48. package/src/assets/web-panel/assets/{OrderTableRenderer-DnXj1jBn.js → OrderTableRenderer--41u7vre.js} +1 -1
  49. package/src/assets/web-panel/assets/{Organization-B3spU8AN.js → Organization-B_0AWztW.js} +2 -2
  50. package/src/assets/web-panel/assets/{Overflow-DgEwo7vm.js → Overflow-B6I7TuXv.js} +1 -1
  51. package/src/assets/web-panel/assets/{P2P-hujMRH8X.js → P2P-Cdf6qens.js} +2 -2
  52. package/src/assets/web-panel/assets/{PdhVaultBrowser-C_qhdg9h.js → PdhVaultBrowser-B_0FcQJY.js} +3 -3
  53. package/src/assets/web-panel/assets/{Permissions-vp7LMcSf.js → Permissions-6nFBjsQp.js} +4 -4
  54. package/src/assets/web-panel/assets/{PersonalDataHub-Dxr5Hgnq.js → PersonalDataHub-nQYV7xpn.js} +3 -3
  55. package/src/assets/web-panel/assets/{Pipeline-DEPZqqH4.js → Pipeline-CBVod5jm.js} +1 -1
  56. package/src/assets/web-panel/assets/{Privacy-DTpDiKPe.js → Privacy-D52jW1Nt.js} +1 -1
  57. package/src/assets/web-panel/assets/{ProjectInit-jDJsRWmF.js → ProjectInit-Bdx3BYhf.js} +2 -2
  58. package/src/assets/web-panel/assets/{ProjectSettings-Dhor5RWH.js → ProjectSettings-b7xr4DUa.js} +2 -2
  59. package/src/assets/web-panel/assets/Projects-DhfifCKx.js +1 -0
  60. package/src/assets/web-panel/assets/Providers-Dy_PgDMr.js +1 -0
  61. package/src/assets/web-panel/assets/{QrScannerModal-CjIw-wIb.js → QrScannerModal-FQKSmc3e.js} +1 -1
  62. package/src/assets/web-panel/assets/{QuickAsk-DX6jcqoi.js → QuickAsk-CSTrhzcF.js} +1 -1
  63. package/src/assets/web-panel/assets/{Recommend-DopXkhar.js → Recommend-CxMABOUh.js} +1 -1
  64. package/src/assets/web-panel/assets/{RemoteSession-BLvqJE9c.js → RemoteSession-Cv2T8yRa.js} +2 -2
  65. package/src/assets/web-panel/assets/{Reputation-DhRj2Me7.js → Reputation-CE1k4Vvu.js} +1 -1
  66. package/src/assets/web-panel/assets/{Row-DQ53vuPq.js → Row-BNgNcAV_.js} +1 -1
  67. package/src/assets/web-panel/assets/{RssFeed-BslvwiBD.js → RssFeed-5ZiBZ3ph.js} +3 -3
  68. package/src/assets/web-panel/assets/{Search-DJKBDNKZ.js → Search-mDOIF1UV.js} +1 -1
  69. package/src/assets/web-panel/assets/{Security-COhpxHER.js → Security-CtuCTC-e.js} +3 -3
  70. package/src/assets/web-panel/assets/{Services-dbulD6cI.js → Services-DxFO2Zg7.js} +2 -2
  71. package/src/assets/web-panel/assets/{Skeleton-ciIVNfhC.js → Skeleton-CKmKkH9y.js} +1 -1
  72. package/src/assets/web-panel/assets/{Skills-AiRTkTCk.js → Skills-Bmzl9qkk.js} +1 -1
  73. package/src/assets/web-panel/assets/{Sla-DDMcWYBc.js → Sla-9RqaWHOC.js} +1 -1
  74. package/src/assets/web-panel/assets/{SpeechSettings-BPxWbpXO.js → SpeechSettings-CGdzFIYw.js} +1 -1
  75. package/src/assets/web-panel/assets/{SyncSettings-BoEOXCyg.js → SyncSettings-CvNuCeBT.js} +2 -2
  76. package/src/assets/web-panel/assets/Tasks-B_sLdcnw.js +1 -0
  77. package/src/assets/web-panel/assets/{Templates-UowY8Ixe.js → Templates-D9_YijdK.js} +1 -1
  78. package/src/assets/web-panel/assets/{Tenant-DauSLQXN.js → Tenant-Cuc7BUSP.js} +1 -1
  79. package/src/assets/web-panel/assets/{Terminal-DBj7Jneq.js → Terminal-6VYRnJgd.js} +2 -2
  80. package/src/assets/web-panel/assets/{TimelineRenderer-UMrPwoZC.js → TimelineRenderer-GtK8xd8Y.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tokens-4ea96-bn.js → Tokens-BZ94Lt5W.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trigger--ISCqt4b.js → Trigger-DEcZgDaX.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trust-D4GVimRJ.js → Trust-CC3z9THe.js} +1 -1
  84. package/src/assets/web-panel/assets/{UkeySign-Ci-nXRQc.js → UkeySign-Cvtw8cV3.js} +1 -1
  85. package/src/assets/web-panel/assets/{VideoEditing-Cjk7mKPw.js → VideoEditing-CL4e6Bn3.js} +1 -1
  86. package/src/assets/web-panel/assets/{Wallet-BgOLOO3Z.js → Wallet-ycEemP27.js} +2 -2
  87. package/src/assets/web-panel/assets/{WebAuthn-CCX0K29i.js → WebAuthn-DgZ4X_aw.js} +5 -5
  88. package/src/assets/web-panel/assets/{WorkflowEditor-Ch3zgIsE.js → WorkflowEditor-D0qMhSv7.js} +1 -1
  89. package/src/assets/web-panel/assets/{chat-DKOU_2Vb.js → chat-DsAg01Zn.js} +1 -1
  90. package/src/assets/web-panel/assets/{colors-6OVo4u3C.js → colors-B0oqAhrQ.js} +1 -1
  91. package/src/assets/web-panel/assets/{compact-item-BH78Hlsq.js → compact-item-CmX0Rabz.js} +1 -1
  92. package/src/assets/web-panel/assets/{createContext-CFRpSbbC.js → createContext-Be4cn1Ub.js} +1 -1
  93. package/src/assets/web-panel/assets/devWarning-CP2qzG6D.js +1 -0
  94. package/src/assets/web-panel/assets/{hasIn-DNQhgcVi.js → hasIn-D5v12kVq.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-BkN3iTTp.js → index-0eWLkQKT.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-CkNEMANR.js → index-301ogenv.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-D3RtTT4K.js → index-5LGY0JbG.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-dDkoL4Pe.js → index-7VKDn-t1.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-BHa4WyTZ.js → index-8WvYGnON.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-CaYqY35Z.js → index-B1YGjOWC.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-bKm3TUDp.js → index-B3mUHuVc.js} +1 -1
  102. package/src/assets/web-panel/assets/{index--rq90rL9.js → index-BA4oulaP.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DaDgdJYj.js → index-BBREnn1V.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-B_Z-YoxX.js → index-BBvyHguy.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-CkqHgG-b.js → index-BKmsS2jj.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-CrcLHN2F.js → index-BRIqLy1m.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-BaVE6E8y.js → index-BXGSbelg.js} +3 -3
  108. package/src/assets/web-panel/assets/{index-BYrnm5Ob.js → index-BaE0Ui07.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-BDSR7ecX.js → index-BoAIAZ5C.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-CNuCHHQT.js → index-BqltWojQ.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-Eag8e_mA.js → index-CGiIyTZw.js} +1 -1
  112. package/src/assets/web-panel/assets/index-CKB0vbJU.js +1 -0
  113. package/src/assets/web-panel/assets/{index-BepBTpa8.js → index-CZ2wUEgr.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-HNGGVF6o.js → index-CZpfzcuy.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-AuBhLqOm.js → index-Ckrk3pIV.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-7TC3S8SN.js → index-CqoCZ5vR.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-CFPJ8yzF.js → index-CyWBir7N.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-Bb6kDrUx.js → index-CyyGTMKd.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DKN2noIF.js → index-D0BSkxYQ.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-Du0rnksC.js → index-D5YVopyr.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-BxRy5LPR.js → index-DKUBj36_.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-sm7W8AXy.js → index-DM724NgU.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-BujsOZfc.js → index-DQRsQen0.js} +1 -1
  124. package/src/assets/web-panel/assets/index-DgoXmwqB.js +1 -0
  125. package/src/assets/web-panel/assets/{index-Y9MJv28e.js → index-DikJ0Qqe.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-caye7AOE.js → index-Dn2dSdXx.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-cdf7fd7d.js → index-DyUaFWEk.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-BnnXQot_.js → index-Hfe_dEGx.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-DvHGtNnF.js → index-_JjRLSD0.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-nbfHl1oV.js → index-frSojpHj.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-Cu3L4r5l.js → index-jK3ax6Z6.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-D84id2Y4.js → index-qMuZ-NRR.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-hPY0fFrd.js → index-q_fQ94SE.js} +1 -1
  134. package/src/assets/web-panel/assets/{initDefaultProps-BZ9w6jPL.js → initDefaultProps-D4VdhJ9_.js} +1 -1
  135. package/src/assets/web-panel/assets/{motion-BnwUiOho.js → motion-CZJA0MQW.js} +1 -1
  136. package/src/assets/web-panel/assets/{move-ByUKJnNm.js → move-BqEs7Sb1.js} +1 -1
  137. package/src/assets/web-panel/assets/{mtc-parser-V7yK1KEl.js → mtc-parser-BpHEtsu1.js} +1 -1
  138. package/src/assets/web-panel/assets/{omit-BEXBBt4j.js → omit-BW94tY9W.js} +1 -1
  139. package/src/assets/web-panel/assets/{pickAttrs-qDm_46Gi.js → pickAttrs-BnBjPWP9.js} +1 -1
  140. package/src/assets/web-panel/assets/{placementArrow-BIbhEo_0.js → placementArrow-uUkjQ_hu.js} +1 -1
  141. package/src/assets/web-panel/assets/{responsiveObserve-NbPbDgA_.js → responsiveObserve-B-klUqXK.js} +1 -1
  142. package/src/assets/web-panel/assets/{slide-BnUWglZ0.js → slide-CvY1cNAs.js} +1 -1
  143. package/src/assets/web-panel/assets/{statusUtils-BX8_wZI5.js → statusUtils-Ix5sEDT2.js} +1 -1
  144. package/src/assets/web-panel/assets/{styleChecker-BV4OycQX.js → styleChecker-C8qcXOne.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFlexGapSupport-Od_H5llY.js → useFlexGapSupport-KNF6Kj8f.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFs-BWI-PyiX.js → useFs-DGJr345n.js} +1 -1
  147. package/src/assets/web-panel/assets/{usePersonalDataHub-BqMuhG3g.js → usePersonalDataHub-BboN99rI.js} +1 -1
  148. package/src/assets/web-panel/assets/{vnode-DNd23z6f.js → vnode-Bgt8Vgte.js} +1 -1
  149. package/src/assets/web-panel/assets/{zoom-Cjaxnm7l.js → zoom-BhJtErgt.js} +1 -1
  150. package/src/assets/web-panel/index.html +1 -1
  151. package/src/command-help-index.json +2 -2
  152. package/src/commands/agenda.js +6 -5
  153. package/src/commands/agent.js +237 -75
  154. package/src/commands/agents.js +6 -0
  155. package/src/commands/artifacts.js +98 -0
  156. package/src/commands/background-session.js +11 -1
  157. package/src/commands/mcp.js +218 -31
  158. package/src/commands/review.js +221 -22
  159. package/src/commands/session-mcp-recovery.js +5 -5
  160. package/src/data/changelog.json +27 -1
  161. package/src/gateways/ws/ws-agent-handler.js +492 -14
  162. package/src/harness/jsonl-session-store.js +39 -1
  163. package/src/harness/mcp-client.js +414 -71
  164. package/src/harness/prompt-compressor.js +24 -0
  165. package/src/harness/provider-backed-compaction.js +205 -0
  166. package/src/harness/worktree-isolator.js +26 -3
  167. package/src/lib/agent-worktree.js +41 -2
  168. package/src/lib/app-preview.js +3 -0
  169. package/src/lib/background-agent-supervisor.js +790 -186
  170. package/src/lib/background-command-argv.js +318 -0
  171. package/src/lib/background-launch-profile.js +1 -0
  172. package/src/lib/background-phase-reporter.js +22 -2
  173. package/src/lib/background-worktree-policy.js +109 -0
  174. package/src/lib/command-lifecycle-report.js +1340 -145
  175. package/src/lib/cowork-mcp-tools.js +51 -6
  176. package/src/lib/cowork-task-runner.js +1 -0
  177. package/src/lib/delivery-coordinator.js +30 -5
  178. package/src/lib/delivery-production-adapter.js +1777 -0
  179. package/src/lib/delivery-production-runner.js +240 -0
  180. package/src/lib/durable-security-store.js +29 -17
  181. package/src/lib/execution-trace/trace-context.cjs +179 -0
  182. package/src/lib/execution-trace/trace-context.js +8 -175
  183. package/src/lib/mcp-call-ledger-store.js +58 -6
  184. package/src/lib/mcp-headers-helper.js +76 -8
  185. package/src/lib/mcp-host-recovery-runtime.js +2 -0
  186. package/src/lib/mcp-ledger-recovery-admission.js +39 -3
  187. package/src/lib/mcp-recovery-adjudication.js +256 -19
  188. package/src/lib/mcp-sandbox-policy.js +184 -0
  189. package/src/lib/mcp-stdio-capsule-builder-worker.cjs +495 -0
  190. package/src/lib/mcp-stdio-executable-identity.js +33 -2
  191. package/src/lib/mcp-stdio-execution-authority.js +163 -0
  192. package/src/lib/mcp-stdio-immutable-vfs-resolver.cjs +830 -0
  193. package/src/lib/mcp-stdio-package-materialization.js +721 -333
  194. package/src/lib/mcp-stdio-workspace-authority.js +252 -0
  195. package/src/lib/observability/index.js +2 -0
  196. package/src/lib/observability/otlp-exporter.js +2 -2
  197. package/src/lib/packer/pack-update-applier.js +8 -1
  198. package/src/lib/packer/pkg-config-generator.js +34 -0
  199. package/src/lib/plugin-runtime/sandbox-policy.js +42 -2
  200. package/src/lib/process-execution-broker/index.js +213 -109
  201. package/src/lib/process-execution-broker/platform-sandbox.js +1024 -169
  202. package/src/lib/process-tree-termination.js +17 -1
  203. package/src/lib/scheduler-kernel/contract.js +315 -0
  204. package/src/lib/scheduler-kernel/store.js +1345 -0
  205. package/src/lib/session-host-lease.js +403 -30
  206. package/src/lib/session-persistence-failure.js +4 -1
  207. package/src/lib/skill-mcp.js +65 -4
  208. package/src/lib/sub-agent-context.js +8 -0
  209. package/src/lib/web-fetch.js +4 -4
  210. package/src/repl/agent-repl.js +285 -38
  211. package/src/runtime/agent-core.js +1044 -90
  212. package/src/runtime/agent-runtime.js +61 -8
  213. package/src/runtime/headless-runner.js +260 -166
  214. package/src/runtime/headless-stream.js +254 -25
  215. package/src/runtime/mcp-config.js +235 -28
  216. package/src/workers/background-agent-worker.js +492 -71
  217. package/src/assets/web-panel/assets/Projects-08xUDZGt.js +0 -1
  218. package/src/assets/web-panel/assets/Providers-BGs47s1a.js +0 -1
  219. package/src/assets/web-panel/assets/Tasks-CEYFJwmr.js +0 -1
  220. package/src/assets/web-panel/assets/devWarning-DEMblAIm.js +0 -1
  221. package/src/assets/web-panel/assets/index-BTvq-36D.js +0 -1
  222. package/src/assets/web-panel/assets/index-hUpvk6Jl.js +0 -1
@@ -0,0 +1,1345 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync } from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { getHomeDir } from "../paths.js";
6
+ import { ensurePrivateDirectory, ensurePrivateFile } from "../secure-fs.js";
7
+ import {
8
+ OCCURRENCE_STATUS,
9
+ SCHEDULER_SCHEMA_VERSION,
10
+ SchedulerKernelError,
11
+ canonicalJson,
12
+ deriveOccurrenceIdentity,
13
+ invalidArgument,
14
+ normalizeAuthorityEnvelope,
15
+ normalizeEpochMs,
16
+ normalizeHistoryLimit,
17
+ normalizeIdentifier,
18
+ normalizeJson,
19
+ normalizeMaxAttempts,
20
+ } from "./contract.js";
21
+
22
+ const requireCjs = createRequire(import.meta.url);
23
+
24
+ export const SCHEDULER_APPLICATION_ID = 0x4343534b; // "CCSK"
25
+ export const DEFAULT_BUSY_TIMEOUT_MS = 5_000;
26
+ export const MAX_LEASE_MS = 24 * 60 * 60 * 1_000;
27
+
28
+ const MIGRATION_NAME = "scheduler-kernel-v1";
29
+ const USER_TABLES = Object.freeze([
30
+ "events",
31
+ "jobs",
32
+ "migrations",
33
+ "occurrences",
34
+ ]);
35
+
36
+ const MIGRATION_V1_SQL = `
37
+ CREATE TABLE migrations (
38
+ version INTEGER PRIMARY KEY NOT NULL,
39
+ name TEXT NOT NULL UNIQUE,
40
+ checksum TEXT NOT NULL,
41
+ applied_at INTEGER NOT NULL CHECK (applied_at >= 0)
42
+ );
43
+
44
+ CREATE TABLE jobs (
45
+ job_id TEXT PRIMARY KEY NOT NULL,
46
+ kind TEXT NOT NULL,
47
+ trigger_json TEXT NOT NULL CHECK (json_valid(trigger_json)),
48
+ payload_json TEXT NOT NULL CHECK (json_valid(payload_json)),
49
+ authority_json TEXT NOT NULL CHECK (json_valid(authority_json)),
50
+ enabled INTEGER NOT NULL CHECK (enabled IN (0, 1)),
51
+ revision INTEGER NOT NULL CHECK (revision >= 1),
52
+ max_attempts INTEGER NOT NULL CHECK (max_attempts BETWEEN 1 AND 32),
53
+ created_at INTEGER NOT NULL CHECK (created_at >= 0),
54
+ updated_at INTEGER NOT NULL CHECK (updated_at >= 0)
55
+ );
56
+
57
+ CREATE TABLE occurrences (
58
+ occurrence_id TEXT PRIMARY KEY NOT NULL,
59
+ job_id TEXT NOT NULL REFERENCES jobs(job_id) ON DELETE RESTRICT,
60
+ job_revision INTEGER NOT NULL CHECK (job_revision >= 1),
61
+ idempotency_key TEXT NOT NULL,
62
+ trigger_key TEXT NOT NULL,
63
+ scheduled_for INTEGER NOT NULL CHECK (scheduled_for >= 0),
64
+ available_at INTEGER NOT NULL CHECK (available_at >= 0),
65
+ status TEXT NOT NULL CHECK (
66
+ status IN ('queued', 'running', 'retry_wait', 'succeeded', 'dead_letter')
67
+ ),
68
+ attempt INTEGER NOT NULL DEFAULT 0 CHECK (attempt >= 0),
69
+ max_attempts INTEGER NOT NULL CHECK (max_attempts BETWEEN 1 AND 32),
70
+ fence INTEGER NOT NULL DEFAULT 0 CHECK (fence >= 0),
71
+ lease_owner TEXT,
72
+ lease_expires_at INTEGER,
73
+ authority_json TEXT NOT NULL CHECK (json_valid(authority_json)),
74
+ payload_json TEXT NOT NULL CHECK (json_valid(payload_json)),
75
+ last_error_json TEXT CHECK (
76
+ last_error_json IS NULL OR json_valid(last_error_json)
77
+ ),
78
+ result_json TEXT CHECK (result_json IS NULL OR json_valid(result_json)),
79
+ created_at INTEGER NOT NULL CHECK (created_at >= 0),
80
+ updated_at INTEGER NOT NULL CHECK (updated_at >= 0),
81
+ settled_at INTEGER,
82
+ CHECK (attempt <= max_attempts),
83
+ CHECK (
84
+ (status = 'running' AND lease_owner IS NOT NULL AND lease_expires_at IS NOT NULL)
85
+ OR
86
+ (status <> 'running' AND lease_owner IS NULL AND lease_expires_at IS NULL)
87
+ ),
88
+ CHECK (
89
+ (status IN ('succeeded', 'dead_letter') AND settled_at IS NOT NULL)
90
+ OR
91
+ (status NOT IN ('succeeded', 'dead_letter') AND settled_at IS NULL)
92
+ )
93
+ );
94
+
95
+ CREATE UNIQUE INDEX scheduler_occurrences_idempotency
96
+ ON occurrences(idempotency_key);
97
+ CREATE INDEX scheduler_occurrences_claim
98
+ ON occurrences(status, available_at, lease_expires_at, scheduled_for, occurrence_id);
99
+ CREATE INDEX scheduler_occurrences_job
100
+ ON occurrences(job_id, created_at, occurrence_id);
101
+
102
+ CREATE TABLE events (
103
+ event_id INTEGER PRIMARY KEY AUTOINCREMENT,
104
+ job_id TEXT NOT NULL REFERENCES jobs(job_id) ON DELETE RESTRICT,
105
+ occurrence_id TEXT REFERENCES occurrences(occurrence_id) ON DELETE RESTRICT,
106
+ event_type TEXT NOT NULL,
107
+ occurred_at INTEGER NOT NULL CHECK (occurred_at >= 0),
108
+ owner_id TEXT,
109
+ fence INTEGER,
110
+ data_json TEXT NOT NULL CHECK (json_valid(data_json))
111
+ );
112
+
113
+ CREATE INDEX scheduler_events_occurrence
114
+ ON events(occurrence_id, event_id DESC);
115
+ CREATE INDEX scheduler_events_job
116
+ ON events(job_id, event_id DESC);
117
+ `;
118
+
119
+ export const MIGRATION_V1_CHECKSUM = createHash("sha256")
120
+ .update(MIGRATION_V1_SQL.trim().replace(/\r\n/g, "\n"), "utf8")
121
+ .digest("hex");
122
+
123
+ // Fingerprint of the normalized sqlite_master catalog produced by the v1 DDL.
124
+ // Unlike the migration-source checksum, this also detects added triggers/views
125
+ // and constraint/foreign-key changes that preserve the visible column list.
126
+ export const SCHEMA_V1_FINGERPRINT =
127
+ "cf244f675ac7430683f67046b57524eb6c14baa45d6463ed3f704be898fec887";
128
+
129
+ const EXPECTED_COLUMNS = Object.freeze({
130
+ migrations: [
131
+ ["version", "INTEGER", 1, 1],
132
+ ["name", "TEXT", 1, 0],
133
+ ["checksum", "TEXT", 1, 0],
134
+ ["applied_at", "INTEGER", 1, 0],
135
+ ],
136
+ jobs: [
137
+ ["job_id", "TEXT", 1, 1],
138
+ ["kind", "TEXT", 1, 0],
139
+ ["trigger_json", "TEXT", 1, 0],
140
+ ["payload_json", "TEXT", 1, 0],
141
+ ["authority_json", "TEXT", 1, 0],
142
+ ["enabled", "INTEGER", 1, 0],
143
+ ["revision", "INTEGER", 1, 0],
144
+ ["max_attempts", "INTEGER", 1, 0],
145
+ ["created_at", "INTEGER", 1, 0],
146
+ ["updated_at", "INTEGER", 1, 0],
147
+ ],
148
+ occurrences: [
149
+ ["occurrence_id", "TEXT", 1, 1],
150
+ ["job_id", "TEXT", 1, 0],
151
+ ["job_revision", "INTEGER", 1, 0],
152
+ ["idempotency_key", "TEXT", 1, 0],
153
+ ["trigger_key", "TEXT", 1, 0],
154
+ ["scheduled_for", "INTEGER", 1, 0],
155
+ ["available_at", "INTEGER", 1, 0],
156
+ ["status", "TEXT", 1, 0],
157
+ ["attempt", "INTEGER", 1, 0],
158
+ ["max_attempts", "INTEGER", 1, 0],
159
+ ["fence", "INTEGER", 1, 0],
160
+ ["lease_owner", "TEXT", 0, 0],
161
+ ["lease_expires_at", "INTEGER", 0, 0],
162
+ ["authority_json", "TEXT", 1, 0],
163
+ ["payload_json", "TEXT", 1, 0],
164
+ ["last_error_json", "TEXT", 0, 0],
165
+ ["result_json", "TEXT", 0, 0],
166
+ ["created_at", "INTEGER", 1, 0],
167
+ ["updated_at", "INTEGER", 1, 0],
168
+ ["settled_at", "INTEGER", 0, 0],
169
+ ],
170
+ events: [
171
+ ["event_id", "INTEGER", 0, 1],
172
+ ["job_id", "TEXT", 1, 0],
173
+ ["occurrence_id", "TEXT", 0, 0],
174
+ ["event_type", "TEXT", 1, 0],
175
+ ["occurred_at", "INTEGER", 1, 0],
176
+ ["owner_id", "TEXT", 0, 0],
177
+ ["fence", "INTEGER", 0, 0],
178
+ ["data_json", "TEXT", 1, 0],
179
+ ],
180
+ });
181
+
182
+ const EXPECTED_INDEXES = Object.freeze([
183
+ "scheduler_events_job",
184
+ "scheduler_events_occurrence",
185
+ "scheduler_occurrences_claim",
186
+ "scheduler_occurrences_idempotency",
187
+ "scheduler_occurrences_job",
188
+ ]);
189
+
190
+ function schemaError(code, message, cause = undefined, details = undefined) {
191
+ return new SchedulerKernelError(
192
+ code,
193
+ message,
194
+ details,
195
+ cause ? { cause } : undefined,
196
+ );
197
+ }
198
+
199
+ function assertPositiveSafeInteger(value, field) {
200
+ if (!Number.isSafeInteger(value) || value < 1) {
201
+ throw invalidArgument(`${field} must be a positive integer`);
202
+ }
203
+ return value;
204
+ }
205
+
206
+ function normalizeLeaseMs(value) {
207
+ const leaseMs = assertPositiveSafeInteger(value, "leaseMs");
208
+ if (leaseMs > MAX_LEASE_MS) {
209
+ throw invalidArgument(`leaseMs must not exceed ${MAX_LEASE_MS}`);
210
+ }
211
+ return leaseMs;
212
+ }
213
+
214
+ function normalizeBoolean(value, field) {
215
+ if (typeof value !== "boolean") {
216
+ throw invalidArgument(`${field} must be a boolean`);
217
+ }
218
+ return value;
219
+ }
220
+
221
+ function normalizeJobInput(input) {
222
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
223
+ throw invalidArgument("job must be an object");
224
+ }
225
+ const allowed = new Set([
226
+ "id",
227
+ "kind",
228
+ "trigger",
229
+ "payload",
230
+ "authority",
231
+ "enabled",
232
+ "maxAttempts",
233
+ ]);
234
+ const unknown = Object.keys(input).filter((key) => !allowed.has(key));
235
+ if (unknown.length > 0) {
236
+ throw invalidArgument("job contains unknown fields", {
237
+ fields: unknown.sort(),
238
+ });
239
+ }
240
+ return {
241
+ id: normalizeIdentifier(input.id, "job.id"),
242
+ kind: normalizeIdentifier(input.kind, "job.kind", { maxLength: 128 }),
243
+ trigger: normalizeJson(input.trigger ?? {}, "job.trigger"),
244
+ payload: normalizeJson(input.payload ?? null, "job.payload"),
245
+ authority: normalizeAuthorityEnvelope(input.authority),
246
+ enabled:
247
+ input.enabled === undefined
248
+ ? true
249
+ : normalizeBoolean(input.enabled, "job.enabled"),
250
+ maxAttempts: normalizeMaxAttempts(input.maxAttempts),
251
+ };
252
+ }
253
+
254
+ function normalizeJobPatch(patch) {
255
+ if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
256
+ throw invalidArgument("job patch must be an object");
257
+ }
258
+ const allowed = new Set([
259
+ "kind",
260
+ "trigger",
261
+ "payload",
262
+ "authority",
263
+ "enabled",
264
+ "maxAttempts",
265
+ ]);
266
+ const keys = Object.keys(patch);
267
+ const unknown = keys.filter((key) => !allowed.has(key));
268
+ if (unknown.length > 0) {
269
+ throw invalidArgument("job patch contains unknown fields", {
270
+ fields: unknown.sort(),
271
+ });
272
+ }
273
+ if (keys.length === 0) throw invalidArgument("job patch must not be empty");
274
+ const normalized = {};
275
+ if (Object.hasOwn(patch, "kind")) {
276
+ normalized.kind = normalizeIdentifier(patch.kind, "job.kind", {
277
+ maxLength: 128,
278
+ });
279
+ }
280
+ if (Object.hasOwn(patch, "trigger")) {
281
+ normalized.trigger = normalizeJson(patch.trigger, "job.trigger");
282
+ }
283
+ if (Object.hasOwn(patch, "payload")) {
284
+ normalized.payload = normalizeJson(patch.payload, "job.payload");
285
+ }
286
+ if (Object.hasOwn(patch, "authority")) {
287
+ normalized.authority = normalizeAuthorityEnvelope(patch.authority);
288
+ }
289
+ if (Object.hasOwn(patch, "enabled")) {
290
+ normalized.enabled = normalizeBoolean(patch.enabled, "job.enabled");
291
+ }
292
+ if (Object.hasOwn(patch, "maxAttempts")) {
293
+ normalized.maxAttempts = normalizeMaxAttempts(patch.maxAttempts);
294
+ }
295
+ return { normalized, changedFields: keys.sort() };
296
+ }
297
+
298
+ function readStoredJson(encoded, field) {
299
+ try {
300
+ return JSON.parse(encoded);
301
+ } catch (cause) {
302
+ throw schemaError(
303
+ "SCHEDULER_DATA_CORRUPT",
304
+ `Stored ${field} is not valid JSON`,
305
+ cause,
306
+ );
307
+ }
308
+ }
309
+
310
+ function readStoredAuthority(encoded, field) {
311
+ try {
312
+ return normalizeAuthorityEnvelope(readStoredJson(encoded, field));
313
+ } catch (cause) {
314
+ if (cause?.code === "SCHEDULER_DATA_CORRUPT") throw cause;
315
+ throw schemaError(
316
+ "SCHEDULER_DATA_CORRUPT",
317
+ `Stored ${field} does not match authority envelope v1`,
318
+ cause,
319
+ );
320
+ }
321
+ }
322
+
323
+ function mapJob(row) {
324
+ if (!row) return null;
325
+ return {
326
+ id: row.job_id,
327
+ kind: row.kind,
328
+ trigger: readStoredJson(row.trigger_json, "job trigger"),
329
+ payload: readStoredJson(row.payload_json, "job payload"),
330
+ authority: readStoredAuthority(row.authority_json, "job authority"),
331
+ enabled: row.enabled === 1,
332
+ revision: row.revision,
333
+ maxAttempts: row.max_attempts,
334
+ createdAt: row.created_at,
335
+ updatedAt: row.updated_at,
336
+ };
337
+ }
338
+
339
+ function mapOccurrence(row) {
340
+ if (!row) return null;
341
+ return {
342
+ id: row.occurrence_id,
343
+ jobId: row.job_id,
344
+ jobRevision: row.job_revision,
345
+ idempotencyKey: row.idempotency_key,
346
+ triggerKey: row.trigger_key,
347
+ scheduledFor: row.scheduled_for,
348
+ availableAt: row.available_at,
349
+ status: row.status,
350
+ attempt: row.attempt,
351
+ maxAttempts: row.max_attempts,
352
+ fence: row.fence,
353
+ leaseOwner: row.lease_owner,
354
+ leaseExpiresAt: row.lease_expires_at,
355
+ authority: readStoredAuthority(row.authority_json, "occurrence authority"),
356
+ payload: readStoredJson(row.payload_json, "occurrence payload"),
357
+ lastError:
358
+ row.last_error_json === null
359
+ ? null
360
+ : readStoredJson(row.last_error_json, "occurrence error"),
361
+ result:
362
+ row.result_json === null
363
+ ? null
364
+ : readStoredJson(row.result_json, "occurrence result"),
365
+ createdAt: row.created_at,
366
+ updatedAt: row.updated_at,
367
+ settledAt: row.settled_at,
368
+ };
369
+ }
370
+
371
+ function mapEvent(row) {
372
+ return {
373
+ eventId: row.event_id,
374
+ jobId: row.job_id,
375
+ occurrenceId: row.occurrence_id,
376
+ type: row.event_type,
377
+ occurredAt: row.occurred_at,
378
+ ownerId: row.owner_id,
379
+ fence: row.fence,
380
+ data: readStoredJson(row.data_json, "event data"),
381
+ };
382
+ }
383
+
384
+ function listUserTables(db) {
385
+ return db
386
+ .prepare(
387
+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name",
388
+ )
389
+ .all()
390
+ .map((row) => row.name);
391
+ }
392
+
393
+ function assertDatabaseIntegrity(db) {
394
+ let rows;
395
+ try {
396
+ rows = db.pragma("quick_check(1)");
397
+ } catch (cause) {
398
+ throw schemaError(
399
+ "SCHEDULER_SCHEMA_CORRUPT",
400
+ "Scheduler database integrity check could not run",
401
+ cause,
402
+ );
403
+ }
404
+ if (
405
+ !Array.isArray(rows) ||
406
+ rows.length !== 1 ||
407
+ rows[0]?.quick_check !== "ok"
408
+ ) {
409
+ throw schemaError(
410
+ "SCHEDULER_SCHEMA_CORRUPT",
411
+ "Scheduler database failed SQLite quick_check",
412
+ undefined,
413
+ { result: rows },
414
+ );
415
+ }
416
+ }
417
+
418
+ function verifyTableShape(db, table) {
419
+ const actual = db
420
+ .pragma(`table_info(${JSON.stringify(table)})`)
421
+ .map((column) => [
422
+ column.name,
423
+ String(column.type || "").toUpperCase(),
424
+ column.notnull,
425
+ column.pk,
426
+ ]);
427
+ const expected = EXPECTED_COLUMNS[table];
428
+ if (JSON.stringify(actual) !== JSON.stringify(expected)) {
429
+ throw schemaError(
430
+ "SCHEDULER_SCHEMA_CORRUPT",
431
+ `Scheduler table shape does not match v1: ${table}`,
432
+ undefined,
433
+ { expected, actual },
434
+ );
435
+ }
436
+ }
437
+
438
+ function schemaFingerprint(db) {
439
+ const catalog = db
440
+ .prepare(
441
+ "SELECT type, name, tbl_name, sql FROM sqlite_master WHERE name NOT LIKE 'sqlite_%' ORDER BY type, name",
442
+ )
443
+ .all()
444
+ .map((row) => ({
445
+ type: row.type,
446
+ name: row.name,
447
+ tbl_name: row.tbl_name,
448
+ sql: String(row.sql || "")
449
+ .replace(/\s+/g, " ")
450
+ .trim(),
451
+ }));
452
+ return createHash("sha256")
453
+ .update(JSON.stringify(catalog), "utf8")
454
+ .digest("hex");
455
+ }
456
+
457
+ function verifySchema(db) {
458
+ assertDatabaseIntegrity(db);
459
+ const tables = listUserTables(db);
460
+ if (JSON.stringify(tables) !== JSON.stringify(USER_TABLES)) {
461
+ throw schemaError(
462
+ "SCHEDULER_SCHEMA_UNKNOWN",
463
+ "Scheduler database has unknown or missing tables",
464
+ undefined,
465
+ { expected: USER_TABLES, actual: tables },
466
+ );
467
+ }
468
+
469
+ let applicationId;
470
+ let userVersion;
471
+ try {
472
+ applicationId = db.pragma("application_id", { simple: true });
473
+ userVersion = db.pragma("user_version", { simple: true });
474
+ } catch (cause) {
475
+ throw schemaError(
476
+ "SCHEDULER_SCHEMA_CORRUPT",
477
+ "Scheduler schema metadata could not be read",
478
+ cause,
479
+ );
480
+ }
481
+ if (
482
+ applicationId !== SCHEDULER_APPLICATION_ID ||
483
+ userVersion !== SCHEDULER_SCHEMA_VERSION
484
+ ) {
485
+ throw schemaError(
486
+ "SCHEDULER_SCHEMA_UNKNOWN",
487
+ "Scheduler schema version or application identity is unknown",
488
+ undefined,
489
+ { applicationId, userVersion },
490
+ );
491
+ }
492
+
493
+ for (const table of USER_TABLES) verifyTableShape(db, table);
494
+
495
+ const actualFingerprint = schemaFingerprint(db);
496
+ if (actualFingerprint !== SCHEMA_V1_FINGERPRINT) {
497
+ throw schemaError(
498
+ "SCHEDULER_SCHEMA_CORRUPT",
499
+ "Scheduler sqlite_master catalog does not match v1",
500
+ undefined,
501
+ {
502
+ expected: SCHEMA_V1_FINGERPRINT,
503
+ actual: actualFingerprint,
504
+ },
505
+ );
506
+ }
507
+
508
+ let migrations;
509
+ try {
510
+ migrations = db
511
+ .prepare(
512
+ "SELECT version, name, checksum FROM migrations ORDER BY version",
513
+ )
514
+ .all();
515
+ } catch (cause) {
516
+ throw schemaError(
517
+ "SCHEDULER_SCHEMA_CORRUPT",
518
+ "Scheduler migration history could not be read",
519
+ cause,
520
+ );
521
+ }
522
+ if (
523
+ migrations.length !== 1 ||
524
+ migrations[0].version !== SCHEDULER_SCHEMA_VERSION ||
525
+ migrations[0].name !== MIGRATION_NAME ||
526
+ migrations[0].checksum !== MIGRATION_V1_CHECKSUM
527
+ ) {
528
+ throw schemaError(
529
+ "SCHEDULER_SCHEMA_UNKNOWN",
530
+ "Scheduler migration history is unknown",
531
+ undefined,
532
+ { migrations },
533
+ );
534
+ }
535
+
536
+ const indexes = db
537
+ .prepare(
538
+ "SELECT name FROM sqlite_master WHERE type = 'index' AND name LIKE 'scheduler_%' ORDER BY name",
539
+ )
540
+ .all()
541
+ .map((row) => row.name);
542
+ if (JSON.stringify(indexes) !== JSON.stringify(EXPECTED_INDEXES)) {
543
+ throw schemaError(
544
+ "SCHEDULER_SCHEMA_CORRUPT",
545
+ "Scheduler indexes do not match v1",
546
+ undefined,
547
+ { expected: EXPECTED_INDEXES, actual: indexes },
548
+ );
549
+ }
550
+ const foreignKeyFailures = db.pragma("foreign_key_check");
551
+ if (foreignKeyFailures.length > 0) {
552
+ throw schemaError(
553
+ "SCHEDULER_SCHEMA_CORRUPT",
554
+ "Scheduler database contains broken foreign keys",
555
+ undefined,
556
+ { count: foreignKeyFailures.length },
557
+ );
558
+ }
559
+ }
560
+
561
+ function initializeOrVerifySchema(db, now) {
562
+ assertDatabaseIntegrity(db);
563
+ const tables = listUserTables(db);
564
+ if (tables.length === 0) {
565
+ const migrate = db.transaction(() => {
566
+ db.exec(MIGRATION_V1_SQL);
567
+ db.prepare(
568
+ "INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
569
+ ).run(
570
+ SCHEDULER_SCHEMA_VERSION,
571
+ MIGRATION_NAME,
572
+ MIGRATION_V1_CHECKSUM,
573
+ now,
574
+ );
575
+ db.pragma(`application_id = ${SCHEDULER_APPLICATION_ID}`);
576
+ db.pragma(`user_version = ${SCHEDULER_SCHEMA_VERSION}`);
577
+ });
578
+ migrate.immediate();
579
+ } else if (!tables.includes("migrations")) {
580
+ throw schemaError(
581
+ "SCHEDULER_SCHEMA_UNKNOWN",
582
+ "Refusing a non-empty database without scheduler migration history",
583
+ undefined,
584
+ { tables },
585
+ );
586
+ }
587
+ verifySchema(db);
588
+ }
589
+
590
+ function defaultDatabasePath() {
591
+ return join(getHomeDir(), "scheduler", "kernel-v1.sqlite");
592
+ }
593
+
594
+ function loadDatabaseConstructor() {
595
+ try {
596
+ return requireCjs("better-sqlite3");
597
+ } catch (cause) {
598
+ throw schemaError(
599
+ "SCHEDULER_SQLITE_UNAVAILABLE",
600
+ "Scheduler kernel requires the existing optional better-sqlite3 dependency",
601
+ cause,
602
+ );
603
+ }
604
+ }
605
+
606
+ function sqliteStorageFiles(target) {
607
+ return [target, `${target}-journal`, `${target}-wal`, `${target}-shm`];
608
+ }
609
+
610
+ function openDatabase({ file, Database, busyTimeoutMs, clock }) {
611
+ const target =
612
+ file === ":memory:" ? file : resolve(file || defaultDatabasePath());
613
+ const timeout = Number(busyTimeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS);
614
+ if (!Number.isSafeInteger(timeout) || timeout < 0 || timeout > 60_000) {
615
+ throw invalidArgument(
616
+ "busyTimeoutMs must be an integer between 0 and 60000",
617
+ );
618
+ }
619
+ const now = normalizeEpochMs(clock(), "clock result");
620
+ if (target !== ":memory:") {
621
+ ensurePrivateDirectory(dirname(target));
622
+ // Existing WAL/SHM files participate in the next SQLite open, so validate
623
+ // them before the native driver consumes either sidecar.
624
+ for (const candidate of sqliteStorageFiles(target)) {
625
+ if (existsSync(candidate)) ensurePrivateFile(candidate);
626
+ }
627
+ }
628
+
629
+ const DatabaseConstructor = Database || loadDatabaseConstructor();
630
+ let db;
631
+ try {
632
+ db = new DatabaseConstructor(target, { timeout });
633
+ db.pragma("foreign_keys = ON");
634
+ db.pragma("trusted_schema = OFF");
635
+ db.pragma(`busy_timeout = ${timeout}`);
636
+ initializeOrVerifySchema(db, now);
637
+ if (target !== ":memory:") db.pragma("journal_mode = WAL");
638
+ db.pragma("synchronous = FULL");
639
+ } catch (cause) {
640
+ try {
641
+ db?.close();
642
+ } catch {
643
+ // Preserve the authoritative open/schema failure.
644
+ }
645
+ if (cause instanceof SchedulerKernelError) throw cause;
646
+ throw schemaError(
647
+ "SCHEDULER_SCHEMA_CORRUPT",
648
+ "Scheduler database could not be opened or verified",
649
+ cause,
650
+ );
651
+ }
652
+
653
+ if (target !== ":memory:") {
654
+ // A quiet WAL database may not have materialized sidecars yet. On Windows,
655
+ // asking secure-fs to classify a missing path deliberately falls back to a
656
+ // native ACL preflight. Only inspect files that SQLite actually created;
657
+ // later opens repeat this check for any surviving sidecars.
658
+ for (const candidate of sqliteStorageFiles(target)) {
659
+ if (existsSync(candidate)) ensurePrivateFile(candidate);
660
+ }
661
+ }
662
+ return { db, target };
663
+ }
664
+
665
+ function leaseLost(occurrenceId) {
666
+ return new SchedulerKernelError(
667
+ "SCHEDULER_LEASE_LOST",
668
+ `Occurrence lease is absent, expired, or owned by another fence: ${occurrenceId}`,
669
+ );
670
+ }
671
+
672
+ export class SchedulerStore {
673
+ constructor(db, { file, clock }) {
674
+ this.db = db;
675
+ this.file = file;
676
+ this.clock = clock;
677
+ this.closed = false;
678
+ this.statements = {
679
+ getJob: db.prepare("SELECT * FROM jobs WHERE job_id = ?"),
680
+ getOccurrence: db.prepare(
681
+ "SELECT * FROM occurrences WHERE occurrence_id = ?",
682
+ ),
683
+ getOccurrenceByKey: db.prepare(
684
+ "SELECT * FROM occurrences WHERE idempotency_key = ?",
685
+ ),
686
+ insertEvent: db.prepare(`
687
+ INSERT INTO events
688
+ (job_id, occurrence_id, event_type, occurred_at, owner_id, fence, data_json)
689
+ VALUES
690
+ (@jobId, @occurrenceId, @type, @occurredAt, @ownerId, @fence, @dataJson)
691
+ `),
692
+ };
693
+ }
694
+
695
+ _assertOpen() {
696
+ if (this.closed) {
697
+ throw new SchedulerKernelError(
698
+ "SCHEDULER_STORE_CLOSED",
699
+ "Scheduler store is closed",
700
+ );
701
+ }
702
+ }
703
+
704
+ _now() {
705
+ return normalizeEpochMs(this.clock(), "clock result");
706
+ }
707
+
708
+ _write(callback) {
709
+ this._assertOpen();
710
+ const transaction = this.db.transaction(callback);
711
+ return transaction.immediate();
712
+ }
713
+
714
+ _appendEvent({
715
+ jobId,
716
+ occurrenceId = null,
717
+ type,
718
+ occurredAt,
719
+ ownerId = null,
720
+ fence = null,
721
+ data = {},
722
+ }) {
723
+ this.statements.insertEvent.run({
724
+ jobId,
725
+ occurrenceId,
726
+ type,
727
+ occurredAt,
728
+ ownerId,
729
+ fence,
730
+ dataJson: canonicalJson(data, "event.data"),
731
+ });
732
+ }
733
+
734
+ createJob(input) {
735
+ const job = normalizeJobInput(input);
736
+ const now = this._now();
737
+ return this._write(() => {
738
+ if (this.statements.getJob.get(job.id)) {
739
+ throw new SchedulerKernelError(
740
+ "SCHEDULER_CONFLICT",
741
+ `Scheduler job already exists: ${job.id}`,
742
+ );
743
+ }
744
+ this.db
745
+ .prepare(
746
+ `
747
+ INSERT INTO jobs
748
+ (job_id, kind, trigger_json, payload_json, authority_json,
749
+ enabled, revision, max_attempts, created_at, updated_at)
750
+ VALUES
751
+ (@id, @kind, @triggerJson, @payloadJson, @authorityJson,
752
+ @enabled, 1, @maxAttempts, @now, @now)
753
+ `,
754
+ )
755
+ .run({
756
+ id: job.id,
757
+ kind: job.kind,
758
+ triggerJson: canonicalJson(job.trigger, "job.trigger"),
759
+ payloadJson: canonicalJson(job.payload, "job.payload"),
760
+ authorityJson: canonicalJson(job.authority, "job.authority"),
761
+ enabled: job.enabled ? 1 : 0,
762
+ maxAttempts: job.maxAttempts,
763
+ now,
764
+ });
765
+ this._appendEvent({
766
+ jobId: job.id,
767
+ type: "job_created",
768
+ occurredAt: now,
769
+ data: { revision: 1 },
770
+ });
771
+ return mapJob(this.statements.getJob.get(job.id));
772
+ });
773
+ }
774
+
775
+ getJob(jobId) {
776
+ this._assertOpen();
777
+ return mapJob(
778
+ this.statements.getJob.get(normalizeIdentifier(jobId, "jobId")),
779
+ );
780
+ }
781
+
782
+ updateJob(jobId, expectedRevision, patch) {
783
+ const id = normalizeIdentifier(jobId, "jobId");
784
+ const revision = assertPositiveSafeInteger(
785
+ expectedRevision,
786
+ "expectedRevision",
787
+ );
788
+ const { normalized, changedFields } = normalizeJobPatch(patch);
789
+ const now = this._now();
790
+ return this._write(() => {
791
+ const current = mapJob(this.statements.getJob.get(id));
792
+ if (!current) {
793
+ throw new SchedulerKernelError(
794
+ "SCHEDULER_NOT_FOUND",
795
+ `Scheduler job does not exist: ${id}`,
796
+ );
797
+ }
798
+ if (current.revision !== revision) {
799
+ throw new SchedulerKernelError(
800
+ "SCHEDULER_REVISION_CONFLICT",
801
+ `Scheduler job revision changed: ${id}`,
802
+ { expectedRevision: revision, actualRevision: current.revision },
803
+ );
804
+ }
805
+ const next = {
806
+ ...current,
807
+ ...normalized,
808
+ revision: current.revision + 1,
809
+ updatedAt: now,
810
+ };
811
+ const result = this.db
812
+ .prepare(
813
+ `
814
+ UPDATE jobs
815
+ SET kind = @kind,
816
+ trigger_json = @triggerJson,
817
+ payload_json = @payloadJson,
818
+ authority_json = @authorityJson,
819
+ enabled = @enabled,
820
+ revision = @nextRevision,
821
+ max_attempts = @maxAttempts,
822
+ updated_at = @now
823
+ WHERE job_id = @id AND revision = @expectedRevision
824
+ `,
825
+ )
826
+ .run({
827
+ id,
828
+ kind: next.kind,
829
+ triggerJson: canonicalJson(next.trigger, "job.trigger"),
830
+ payloadJson: canonicalJson(next.payload, "job.payload"),
831
+ authorityJson: canonicalJson(next.authority, "job.authority"),
832
+ enabled: next.enabled ? 1 : 0,
833
+ nextRevision: next.revision,
834
+ maxAttempts: next.maxAttempts,
835
+ now,
836
+ expectedRevision: revision,
837
+ });
838
+ if (result.changes !== 1) {
839
+ throw new SchedulerKernelError(
840
+ "SCHEDULER_REVISION_CONFLICT",
841
+ `Scheduler job revision changed during update: ${id}`,
842
+ );
843
+ }
844
+ this._appendEvent({
845
+ jobId: id,
846
+ type: "job_updated",
847
+ occurredAt: now,
848
+ data: {
849
+ fromRevision: revision,
850
+ toRevision: next.revision,
851
+ changedFields,
852
+ },
853
+ });
854
+ return mapJob(this.statements.getJob.get(id));
855
+ });
856
+ }
857
+
858
+ enqueueOccurrence({ jobId, scheduledFor, triggerKey, availableAt } = {}) {
859
+ const id = normalizeIdentifier(jobId, "jobId");
860
+ const scheduled = normalizeEpochMs(scheduledFor, "scheduledFor");
861
+ const key = normalizeIdentifier(triggerKey, "triggerKey");
862
+ const available = normalizeEpochMs(availableAt ?? scheduled, "availableAt");
863
+ const now = this._now();
864
+ return this._write(() => {
865
+ const job = mapJob(this.statements.getJob.get(id));
866
+ if (!job) {
867
+ throw new SchedulerKernelError(
868
+ "SCHEDULER_NOT_FOUND",
869
+ `Scheduler job does not exist: ${id}`,
870
+ );
871
+ }
872
+ if (!job.enabled) {
873
+ throw new SchedulerKernelError(
874
+ "SCHEDULER_JOB_DISABLED",
875
+ `Scheduler job is disabled: ${id}`,
876
+ );
877
+ }
878
+ const identity = deriveOccurrenceIdentity({
879
+ jobId: id,
880
+ jobRevision: job.revision,
881
+ scheduledFor: scheduled,
882
+ triggerKey: key,
883
+ });
884
+ const result = this.db
885
+ .prepare(
886
+ `
887
+ INSERT INTO occurrences
888
+ (occurrence_id, job_id, job_revision, idempotency_key, trigger_key,
889
+ scheduled_for, available_at, status, attempt, max_attempts, fence,
890
+ lease_owner, lease_expires_at, authority_json, payload_json,
891
+ last_error_json, result_json, created_at, updated_at, settled_at)
892
+ VALUES
893
+ (@occurrenceId, @jobId, @jobRevision, @idempotencyKey, @triggerKey,
894
+ @scheduledFor, @availableAt, 'queued', 0, @maxAttempts, 0,
895
+ NULL, NULL, @authorityJson, @payloadJson,
896
+ NULL, NULL, @now, @now, NULL)
897
+ ON CONFLICT(idempotency_key) DO NOTHING
898
+ `,
899
+ )
900
+ .run({
901
+ ...identity,
902
+ jobId: id,
903
+ jobRevision: job.revision,
904
+ triggerKey: key,
905
+ scheduledFor: scheduled,
906
+ availableAt: available,
907
+ maxAttempts: job.maxAttempts,
908
+ authorityJson: canonicalJson(job.authority, "occurrence.authority"),
909
+ payloadJson: canonicalJson(job.payload, "occurrence.payload"),
910
+ now,
911
+ });
912
+ const row = this.statements.getOccurrenceByKey.get(
913
+ identity.idempotencyKey,
914
+ );
915
+ if (!row || row.occurrence_id !== identity.occurrenceId) {
916
+ throw schemaError(
917
+ "SCHEDULER_DATA_CORRUPT",
918
+ "Occurrence idempotency identity does not match stored data",
919
+ );
920
+ }
921
+ if (result.changes === 1) {
922
+ this._appendEvent({
923
+ jobId: id,
924
+ occurrenceId: identity.occurrenceId,
925
+ type: "occurrence_enqueued",
926
+ occurredAt: now,
927
+ data: {
928
+ jobRevision: job.revision,
929
+ scheduledFor: scheduled,
930
+ triggerKey: key,
931
+ },
932
+ });
933
+ }
934
+ return { ...mapOccurrence(row), deduplicated: result.changes === 0 };
935
+ });
936
+ }
937
+
938
+ getOccurrence(occurrenceId) {
939
+ this._assertOpen();
940
+ return mapOccurrence(
941
+ this.statements.getOccurrence.get(
942
+ normalizeIdentifier(occurrenceId, "occurrenceId"),
943
+ ),
944
+ );
945
+ }
946
+
947
+ _deadLetterExpiredLeases(now) {
948
+ const expired = this.db
949
+ .prepare(
950
+ `
951
+ SELECT occurrence_id, job_id, lease_owner, fence, attempt, max_attempts
952
+ FROM occurrences
953
+ WHERE status = 'running'
954
+ AND lease_expires_at <= ?
955
+ AND attempt >= max_attempts
956
+ ORDER BY lease_expires_at, occurrence_id
957
+ LIMIT 100
958
+ `,
959
+ )
960
+ .all(now);
961
+ const update = this.db.prepare(`
962
+ UPDATE occurrences
963
+ SET status = 'dead_letter',
964
+ lease_owner = NULL,
965
+ lease_expires_at = NULL,
966
+ last_error_json = @errorJson,
967
+ updated_at = @now,
968
+ settled_at = @now
969
+ WHERE occurrence_id = @occurrenceId
970
+ AND status = 'running'
971
+ AND lease_owner = @ownerId
972
+ AND fence = @fence
973
+ AND lease_expires_at <= @now
974
+ AND attempt >= max_attempts
975
+ `);
976
+ for (const row of expired) {
977
+ const error = {
978
+ code: "lease_expired",
979
+ message: "Execution lease expired after the final allowed attempt",
980
+ };
981
+ const result = update.run({
982
+ occurrenceId: row.occurrence_id,
983
+ ownerId: row.lease_owner,
984
+ fence: row.fence,
985
+ errorJson: canonicalJson(error, "occurrence.error"),
986
+ now,
987
+ });
988
+ if (result.changes === 1) {
989
+ this._appendEvent({
990
+ jobId: row.job_id,
991
+ occurrenceId: row.occurrence_id,
992
+ type: "occurrence_dead_lettered",
993
+ occurredAt: now,
994
+ ownerId: row.lease_owner,
995
+ fence: row.fence,
996
+ data: {
997
+ attempt: row.attempt,
998
+ maxAttempts: row.max_attempts,
999
+ reason: "lease_expired",
1000
+ },
1001
+ });
1002
+ }
1003
+ }
1004
+ }
1005
+
1006
+ claimNext({ ownerId, leaseMs } = {}) {
1007
+ const owner = normalizeIdentifier(ownerId, "ownerId");
1008
+ const lease = normalizeLeaseMs(leaseMs);
1009
+ const now = this._now();
1010
+ const leaseExpiresAt = now + lease;
1011
+ if (!Number.isSafeInteger(leaseExpiresAt)) {
1012
+ throw invalidArgument("lease expiry exceeds the safe integer range");
1013
+ }
1014
+ return this._write(() => {
1015
+ this._deadLetterExpiredLeases(now);
1016
+ const candidate = this.db
1017
+ .prepare(
1018
+ `
1019
+ SELECT o.*
1020
+ FROM occurrences o
1021
+ JOIN jobs j ON j.job_id = o.job_id
1022
+ WHERE j.enabled = 1
1023
+ AND o.attempt < o.max_attempts
1024
+ AND (
1025
+ (o.status IN ('queued', 'retry_wait') AND o.available_at <= @now)
1026
+ OR
1027
+ (o.status = 'running' AND o.lease_expires_at <= @now)
1028
+ )
1029
+ ORDER BY
1030
+ CASE WHEN o.status = 'running' THEN o.lease_expires_at ELSE o.available_at END,
1031
+ o.scheduled_for,
1032
+ o.occurrence_id
1033
+ LIMIT 1
1034
+ `,
1035
+ )
1036
+ .get({ now });
1037
+ if (!candidate) return null;
1038
+ const previousStatus = candidate.status;
1039
+ const previousOwner = candidate.lease_owner;
1040
+ const result = this.db
1041
+ .prepare(
1042
+ `
1043
+ UPDATE occurrences
1044
+ SET status = 'running',
1045
+ attempt = attempt + 1,
1046
+ fence = fence + 1,
1047
+ lease_owner = @ownerId,
1048
+ lease_expires_at = @leaseExpiresAt,
1049
+ updated_at = @now,
1050
+ settled_at = NULL
1051
+ WHERE occurrence_id = @occurrenceId
1052
+ AND fence = @expectedFence
1053
+ AND attempt < max_attempts
1054
+ AND (
1055
+ (status IN ('queued', 'retry_wait') AND available_at <= @now)
1056
+ OR
1057
+ (status = 'running' AND lease_expires_at <= @now)
1058
+ )
1059
+ `,
1060
+ )
1061
+ .run({
1062
+ occurrenceId: candidate.occurrence_id,
1063
+ expectedFence: candidate.fence,
1064
+ ownerId: owner,
1065
+ leaseExpiresAt,
1066
+ now,
1067
+ });
1068
+ if (result.changes !== 1) {
1069
+ throw new SchedulerKernelError(
1070
+ "SCHEDULER_CLAIM_CONFLICT",
1071
+ "Occurrence eligibility changed during claim",
1072
+ );
1073
+ }
1074
+ const claimed = this.statements.getOccurrence.get(
1075
+ candidate.occurrence_id,
1076
+ );
1077
+ this._appendEvent({
1078
+ jobId: candidate.job_id,
1079
+ occurrenceId: candidate.occurrence_id,
1080
+ type:
1081
+ previousStatus === OCCURRENCE_STATUS.RUNNING
1082
+ ? "occurrence_reclaimed"
1083
+ : "occurrence_claimed",
1084
+ occurredAt: now,
1085
+ ownerId: owner,
1086
+ fence: claimed.fence,
1087
+ data: {
1088
+ attempt: claimed.attempt,
1089
+ previousOwner,
1090
+ previousStatus,
1091
+ },
1092
+ });
1093
+ return mapOccurrence(claimed);
1094
+ });
1095
+ }
1096
+
1097
+ renew({ occurrenceId, ownerId, fence, leaseMs } = {}) {
1098
+ const id = normalizeIdentifier(occurrenceId, "occurrenceId");
1099
+ const owner = normalizeIdentifier(ownerId, "ownerId");
1100
+ const token = assertPositiveSafeInteger(fence, "fence");
1101
+ const lease = normalizeLeaseMs(leaseMs);
1102
+ const now = this._now();
1103
+ const leaseExpiresAt = now + lease;
1104
+ if (!Number.isSafeInteger(leaseExpiresAt)) {
1105
+ throw invalidArgument("lease expiry exceeds the safe integer range");
1106
+ }
1107
+ return this._write(() => {
1108
+ const result = this.db
1109
+ .prepare(
1110
+ `
1111
+ UPDATE occurrences
1112
+ SET lease_expires_at = MAX(lease_expires_at, @leaseExpiresAt),
1113
+ updated_at = @now
1114
+ WHERE occurrence_id = @occurrenceId
1115
+ AND status = 'running'
1116
+ AND lease_owner = @ownerId
1117
+ AND fence = @fence
1118
+ AND lease_expires_at > @now
1119
+ `,
1120
+ )
1121
+ .run({
1122
+ occurrenceId: id,
1123
+ ownerId: owner,
1124
+ fence: token,
1125
+ leaseExpiresAt,
1126
+ now,
1127
+ });
1128
+ if (result.changes !== 1) throw leaseLost(id);
1129
+ const row = this.statements.getOccurrence.get(id);
1130
+ this._appendEvent({
1131
+ jobId: row.job_id,
1132
+ occurrenceId: id,
1133
+ type: "occurrence_renewed",
1134
+ occurredAt: now,
1135
+ ownerId: owner,
1136
+ fence: token,
1137
+ data: { leaseExpiresAt: row.lease_expires_at },
1138
+ });
1139
+ return mapOccurrence(row);
1140
+ });
1141
+ }
1142
+
1143
+ settle({
1144
+ occurrenceId,
1145
+ ownerId,
1146
+ fence,
1147
+ outcome,
1148
+ result,
1149
+ error,
1150
+ retryable = true,
1151
+ retryAt,
1152
+ } = {}) {
1153
+ const id = normalizeIdentifier(occurrenceId, "occurrenceId");
1154
+ const owner = normalizeIdentifier(ownerId, "ownerId");
1155
+ const token = assertPositiveSafeInteger(fence, "fence");
1156
+ if (!["succeeded", "failed"].includes(outcome)) {
1157
+ throw invalidArgument("outcome must be succeeded or failed");
1158
+ }
1159
+ if (typeof retryable !== "boolean") {
1160
+ throw invalidArgument("retryable must be a boolean");
1161
+ }
1162
+ const normalizedResult =
1163
+ outcome === "succeeded" ? normalizeJson(result ?? null, "result") : null;
1164
+ const normalizedError =
1165
+ outcome === "failed"
1166
+ ? normalizeJson(
1167
+ error ?? {
1168
+ code: "execution_failed",
1169
+ message: "Execution failed",
1170
+ },
1171
+ "error",
1172
+ )
1173
+ : null;
1174
+ const now = this._now();
1175
+ const availableAt =
1176
+ outcome !== "failed" || retryAt === undefined
1177
+ ? now
1178
+ : Math.max(now, normalizeEpochMs(retryAt, "retryAt"));
1179
+
1180
+ return this._write(() => {
1181
+ const current = this.statements.getOccurrence.get(id);
1182
+ if (
1183
+ !current ||
1184
+ current.status !== OCCURRENCE_STATUS.RUNNING ||
1185
+ current.lease_owner !== owner ||
1186
+ current.fence !== token ||
1187
+ current.lease_expires_at <= now
1188
+ ) {
1189
+ throw leaseLost(id);
1190
+ }
1191
+
1192
+ let nextStatus;
1193
+ let eventType;
1194
+ let settledAt = null;
1195
+ let nextAvailableAt = current.available_at;
1196
+ let lastErrorJson = null;
1197
+ let resultJson = null;
1198
+ if (outcome === "succeeded") {
1199
+ nextStatus = OCCURRENCE_STATUS.SUCCEEDED;
1200
+ eventType = "occurrence_succeeded";
1201
+ settledAt = now;
1202
+ resultJson = canonicalJson(normalizedResult, "result");
1203
+ } else if (retryable && current.attempt < current.max_attempts) {
1204
+ nextStatus = OCCURRENCE_STATUS.RETRY_WAIT;
1205
+ eventType = "occurrence_retry_scheduled";
1206
+ nextAvailableAt = availableAt;
1207
+ lastErrorJson = canonicalJson(normalizedError, "error");
1208
+ } else {
1209
+ nextStatus = OCCURRENCE_STATUS.DEAD_LETTER;
1210
+ eventType = "occurrence_dead_lettered";
1211
+ settledAt = now;
1212
+ lastErrorJson = canonicalJson(normalizedError, "error");
1213
+ }
1214
+
1215
+ const update = this.db
1216
+ .prepare(
1217
+ `
1218
+ UPDATE occurrences
1219
+ SET status = @status,
1220
+ available_at = @availableAt,
1221
+ lease_owner = NULL,
1222
+ lease_expires_at = NULL,
1223
+ last_error_json = @lastErrorJson,
1224
+ result_json = @resultJson,
1225
+ updated_at = @now,
1226
+ settled_at = @settledAt
1227
+ WHERE occurrence_id = @occurrenceId
1228
+ AND status = 'running'
1229
+ AND lease_owner = @ownerId
1230
+ AND fence = @fence
1231
+ AND lease_expires_at > @now
1232
+ `,
1233
+ )
1234
+ .run({
1235
+ occurrenceId: id,
1236
+ ownerId: owner,
1237
+ fence: token,
1238
+ status: nextStatus,
1239
+ availableAt: nextAvailableAt,
1240
+ lastErrorJson,
1241
+ resultJson,
1242
+ now,
1243
+ settledAt,
1244
+ });
1245
+ if (update.changes !== 1) throw leaseLost(id);
1246
+ this._appendEvent({
1247
+ jobId: current.job_id,
1248
+ occurrenceId: id,
1249
+ type: eventType,
1250
+ occurredAt: now,
1251
+ ownerId: owner,
1252
+ fence: token,
1253
+ data: {
1254
+ attempt: current.attempt,
1255
+ maxAttempts: current.max_attempts,
1256
+ ...(nextStatus === OCCURRENCE_STATUS.RETRY_WAIT
1257
+ ? { retryAt: nextAvailableAt }
1258
+ : {}),
1259
+ },
1260
+ });
1261
+ return mapOccurrence(this.statements.getOccurrence.get(id));
1262
+ });
1263
+ }
1264
+
1265
+ history({ occurrenceId, jobId, limit } = {}) {
1266
+ this._assertOpen();
1267
+ const boundedLimit = normalizeHistoryLimit(limit);
1268
+ const occurrence =
1269
+ occurrenceId === undefined
1270
+ ? null
1271
+ : normalizeIdentifier(occurrenceId, "occurrenceId");
1272
+ const job =
1273
+ jobId === undefined ? null : normalizeIdentifier(jobId, "jobId");
1274
+ let sql = "SELECT * FROM events";
1275
+ const parameters = { limit: boundedLimit };
1276
+ if (occurrence !== null && job !== null) {
1277
+ sql += " WHERE occurrence_id = @occurrenceId AND job_id = @jobId";
1278
+ parameters.occurrenceId = occurrence;
1279
+ parameters.jobId = job;
1280
+ } else if (occurrence !== null) {
1281
+ sql += " WHERE occurrence_id = @occurrenceId";
1282
+ parameters.occurrenceId = occurrence;
1283
+ } else if (job !== null) {
1284
+ sql += " WHERE job_id = @jobId";
1285
+ parameters.jobId = job;
1286
+ }
1287
+ sql += " ORDER BY event_id DESC LIMIT @limit";
1288
+ return this.db.prepare(sql).all(parameters).map(mapEvent);
1289
+ }
1290
+
1291
+ listDeadLetters({ jobId, limit } = {}) {
1292
+ this._assertOpen();
1293
+ const boundedLimit = normalizeHistoryLimit(limit);
1294
+ if (jobId === undefined) {
1295
+ return this.db
1296
+ .prepare(
1297
+ "SELECT * FROM occurrences WHERE status = 'dead_letter' ORDER BY settled_at DESC, occurrence_id LIMIT ?",
1298
+ )
1299
+ .all(boundedLimit)
1300
+ .map(mapOccurrence);
1301
+ }
1302
+ const id = normalizeIdentifier(jobId, "jobId");
1303
+ return this.db
1304
+ .prepare(
1305
+ "SELECT * FROM occurrences WHERE status = 'dead_letter' AND job_id = ? ORDER BY settled_at DESC, occurrence_id LIMIT ?",
1306
+ )
1307
+ .all(id, boundedLimit)
1308
+ .map(mapOccurrence);
1309
+ }
1310
+
1311
+ schemaInfo() {
1312
+ this._assertOpen();
1313
+ return {
1314
+ applicationId: this.db.pragma("application_id", { simple: true }),
1315
+ schemaVersion: this.db.pragma("user_version", { simple: true }),
1316
+ migration: this.db
1317
+ .prepare(
1318
+ "SELECT version, name, checksum, applied_at AS appliedAt FROM migrations ORDER BY version DESC LIMIT 1",
1319
+ )
1320
+ .get(),
1321
+ };
1322
+ }
1323
+
1324
+ close() {
1325
+ if (this.closed) return;
1326
+ this.closed = true;
1327
+ this.db.close();
1328
+ }
1329
+ }
1330
+
1331
+ export function openSchedulerStore({
1332
+ file,
1333
+ Database,
1334
+ clock = Date.now,
1335
+ busyTimeoutMs = DEFAULT_BUSY_TIMEOUT_MS,
1336
+ } = {}) {
1337
+ if (typeof clock !== "function") {
1338
+ throw invalidArgument("clock must be a function");
1339
+ }
1340
+ const opened = openDatabase({ file, Database, busyTimeoutMs, clock });
1341
+ return new SchedulerStore(opened.db, {
1342
+ file: opened.target,
1343
+ clock,
1344
+ });
1345
+ }