chainlesschain 0.163.7 → 0.164.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 (252) hide show
  1. package/package.json +5 -1
  2. package/scripts/macos-mcp-launcher-build.mjs +317 -0
  3. package/scripts/macos-mcp-launcher-postinstall.sh +54 -0
  4. package/scripts/macos-mcp-launcher-preinstall.sh +61 -0
  5. package/src/assets/web-panel/assets/{AIOps-axqQfwv8.js → AIOps-D2MEn7fI.js} +1 -1
  6. package/src/assets/web-panel/assets/{ActionButton-DV-Ro0By.js → ActionButton-lCW_4Gf0.js} +1 -1
  7. package/src/assets/web-panel/assets/{Analytics-Bt72WEVu.js → Analytics-Bs7i-nsS.js} +3 -3
  8. package/src/assets/web-panel/assets/{AppLayout-BtepG55E.js → AppLayout-CK7B6S_j.js} +5 -5
  9. package/src/assets/web-panel/assets/{Artifacts-B7KaJ7S_.js → Artifacts-C8ILjpyo.js} +1 -1
  10. package/src/assets/web-panel/assets/{Audit-FNFaUg7A.js → Audit-Bb9cNaWc.js} +1 -1
  11. package/src/assets/web-panel/assets/{BackgroundAgents-DFb6J2wQ.js → BackgroundAgents-sC9ooXIf.js} +1 -1
  12. package/src/assets/web-panel/assets/{Backup-CC7YBhyj.js → Backup-D-DrsplT.js} +1 -1
  13. package/src/assets/web-panel/assets/{BaseInput-RkgTNVtj.js → BaseInput-uCNZpwSK.js} +1 -1
  14. package/src/assets/web-panel/assets/{Chat-hHEmGZy-.js → Chat-CsDK1uE9.js} +6 -6
  15. package/src/assets/web-panel/assets/{ChatBubbleRenderer-CAP9YhHv.js → ChatBubbleRenderer-OqSDt50w.js} +1 -1
  16. package/src/assets/web-panel/assets/{Checkbox-z5wXOQP1.js → Checkbox-JhTXM1-K.js} +1 -1
  17. package/src/assets/web-panel/assets/{Codegen-DVaDJnxy.js → Codegen-BbzZLnyD.js} +1 -1
  18. package/src/assets/web-panel/assets/{Col-CJmaRDR5.js → Col-D4VW4m4j.js} +1 -1
  19. package/src/assets/web-panel/assets/{Community-DF29_2bI.js → Community-BXrYawxV.js} +1 -1
  20. package/src/assets/web-panel/assets/{Compact-DtkQkBzn.js → Compact-DCihGy1u.js} +1 -1
  21. package/src/assets/web-panel/assets/{Compliance-xoHDxp7I.js → Compliance-DlyZ-lk8.js} +1 -1
  22. package/src/assets/web-panel/assets/{Cowork-BUm-R7ht.js → Cowork-DvGZrhbO.js} +2 -2
  23. package/src/assets/web-panel/assets/{Cron-Cgl14ZkB.js → Cron-DUncZi8h.js} +2 -2
  24. package/src/assets/web-panel/assets/{Crosschain-BNcbmoqh.js → Crosschain-C_hLuUcm.js} +1 -1
  25. package/src/assets/web-panel/assets/{DID-Bwja0Zo9.js → DID-b6YSUqxr.js} +2 -2
  26. package/src/assets/web-panel/assets/{Dashboard-Bk5XjEdJ.js → Dashboard-uvM15Cfb.js} +2 -2
  27. package/src/assets/web-panel/assets/{Dropdown-Ci36Oitq.js → Dropdown-D1-Nd4qY.js} +1 -1
  28. package/src/assets/web-panel/assets/{EmailListRenderer--U04ZN4q.js → EmailListRenderer-8ApvL6Zc.js} +1 -1
  29. package/src/assets/web-panel/assets/{FamilyGuardDashboard-B2gNPR2s.js → FamilyGuardDashboard-LYJt8811.js} +1 -1
  30. package/src/assets/web-panel/assets/{Federation-C4KghYn7.js → Federation-BbI3HGpv.js} +1 -1
  31. package/src/assets/web-panel/assets/{FormItemContext-QL7x_Q_0.js → FormItemContext-DSoEKuFu.js} +1 -1
  32. package/src/assets/web-panel/assets/{GenericCardRenderer-F3Ryltwm.js → GenericCardRenderer-B0S9OWd5.js} +1 -1
  33. package/src/assets/web-panel/assets/{Git-2HassK5k.js → Git-DIs9Zhf3.js} +2 -2
  34. package/src/assets/web-panel/assets/{Governance-DoBIp7oe.js → Governance-jzZXrxcl.js} +1 -1
  35. package/src/assets/web-panel/assets/{Inference--W01QhZ-.js → Inference-JUPjf2ZI.js} +1 -1
  36. package/src/assets/web-panel/assets/{KnowledgeGraph-DmvwqGpT.js → KnowledgeGraph-DxnfJvlP.js} +1 -1
  37. package/src/assets/web-panel/assets/{Logs-BBJ4yAel.js → Logs-iXiz8BEE.js} +2 -2
  38. package/src/assets/web-panel/assets/{MarkdownRenderer-BLCZMzhJ.js → MarkdownRenderer-GmB6DOr6.js} +1 -1
  39. package/src/assets/web-panel/assets/{Marketplace-D5lNngSI.js → Marketplace-Y3Xch312.js} +1 -1
  40. package/src/assets/web-panel/assets/{McpTools-CkEVOMKH.js → McpTools-D7kGGbO7.js} +4 -4
  41. package/src/assets/web-panel/assets/{Memory-B1Ow1xVE.js → Memory-Do_hVxzh.js} +2 -2
  42. package/src/assets/web-panel/assets/{MobileBridge-SgYbUfb6.js → MobileBridge-CtrR0Tas.js} +1 -1
  43. package/src/assets/web-panel/assets/MobileProjects-BL0Czcoj.js +1 -0
  44. package/src/assets/web-panel/assets/{Mtc-DcTXSSma.js → Mtc-qo5QCTGd.js} +2 -2
  45. package/src/assets/web-panel/assets/{MtcAudit-Dt3WG4oj.js → MtcAudit-cq5mnjYj.js} +2 -2
  46. package/src/assets/web-panel/assets/{Multisig-B9B38UZx.js → Multisig-Bq0MAQHw.js} +3 -3
  47. package/src/assets/web-panel/assets/{NLProgramming-B0S4GGvG.js → NLProgramming-DdWE6Vi2.js} +1 -1
  48. package/src/assets/web-panel/assets/{Notes-DZ5U58Ng.js → Notes-BxMBFcIp.js} +3 -3
  49. package/src/assets/web-panel/assets/{NotificationSettings-PjL5ZW34.js → NotificationSettings-RTrAcbUe.js} +1 -1
  50. package/src/assets/web-panel/assets/{OrderTableRenderer-DrAX2ESt.js → OrderTableRenderer-DJbpGqzT.js} +1 -1
  51. package/src/assets/web-panel/assets/{Organization-Cv1GYs-c.js → Organization-SQzIPdqC.js} +4 -4
  52. package/src/assets/web-panel/assets/{Overflow-Bbkp_PL3.js → Overflow-02yECh22.js} +1 -1
  53. package/src/assets/web-panel/assets/{P2P-BD8QUgoZ.js → P2P-BNg4pNCM.js} +2 -2
  54. package/src/assets/web-panel/assets/{PdhVaultBrowser-BO-2PeaB.js → PdhVaultBrowser-BxPJZsD3.js} +3 -3
  55. package/src/assets/web-panel/assets/{Permissions-D_OPVa02.js → Permissions-CLnDnPAz.js} +4 -4
  56. package/src/assets/web-panel/assets/{PersonalDataHub-mh-pEmaB.js → PersonalDataHub-C5m0AjGf.js} +4 -4
  57. package/src/assets/web-panel/assets/{Pipeline-6JC2EDdb.js → Pipeline-VwOf5Dhv.js} +1 -1
  58. package/src/assets/web-panel/assets/{Privacy-CUshzUAG.js → Privacy-CC0W2hZd.js} +1 -1
  59. package/src/assets/web-panel/assets/{ProjectInit-Cs2Aalri.js → ProjectInit-Dz893Bob.js} +2 -2
  60. package/src/assets/web-panel/assets/{ProjectSettings-1ak49tcj.js → ProjectSettings-CxQkvl0k.js} +2 -2
  61. package/src/assets/web-panel/assets/Projects-DDAjkUkT.js +1 -0
  62. package/src/assets/web-panel/assets/{Providers-CXXxvafH.js → Providers-DeB9IkxK.js} +1 -1
  63. package/src/assets/web-panel/assets/{QrScannerModal-PTcDsF58.js → QrScannerModal-OCeoaswn.js} +1 -1
  64. package/src/assets/web-panel/assets/{QuickAsk-CDwucrJw.js → QuickAsk-DenotaAj.js} +1 -1
  65. package/src/assets/web-panel/assets/{Recommend-CJlLVRXR.js → Recommend-Cies_jjI.js} +1 -1
  66. package/src/assets/web-panel/assets/{RemoteSession-DlN-ETsO.js → RemoteSession-Dxzl5n2T.js} +3 -3
  67. package/src/assets/web-panel/assets/{Reputation-DI23LR6m.js → Reputation-CW0mvxgZ.js} +1 -1
  68. package/src/assets/web-panel/assets/{Row-Cl7UQMrM.js → Row-SEd32qZU.js} +1 -1
  69. package/src/assets/web-panel/assets/{RssFeed-CexHNfh8.js → RssFeed-BSwnJ3Lm.js} +3 -3
  70. package/src/assets/web-panel/assets/{Search-BneBWvRN.js → Search-xe2m8bkC.js} +1 -1
  71. package/src/assets/web-panel/assets/{Security-DX7Phgrp.js → Security-Mq2F9Nro.js} +4 -4
  72. package/src/assets/web-panel/assets/{Services-BWXO8Mte.js → Services-BKGw2V3P.js} +2 -2
  73. package/src/assets/web-panel/assets/{Skeleton-DCyOjKPJ.js → Skeleton-DW95-Rxf.js} +1 -1
  74. package/src/assets/web-panel/assets/{Skills-CXqERsTB.js → Skills-CHgChCv_.js} +1 -1
  75. package/src/assets/web-panel/assets/{Sla-bhbrbz8E.js → Sla-tKh6k3R7.js} +1 -1
  76. package/src/assets/web-panel/assets/{SpeechSettings-3IPU0HBZ.js → SpeechSettings-D6UL3dA5.js} +1 -1
  77. package/src/assets/web-panel/assets/{SyncSettings-DBNTQLbM.js → SyncSettings-D0Fw8fVL.js} +2 -2
  78. package/src/assets/web-panel/assets/{Tasks-quP0bkN-.js → Tasks-20x1GB3D.js} +1 -1
  79. package/src/assets/web-panel/assets/{Templates-BouWmgho.js → Templates-DJ-lts-a.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tenant-qv0D9s7z.js → Tenant-BFdGpbs7.js} +1 -1
  81. package/src/assets/web-panel/assets/{Terminal-BxvbE8BB.js → Terminal-Xpx61vHL.js} +2 -2
  82. package/src/assets/web-panel/assets/{TimelineRenderer-D-IeLnJ2.js → TimelineRenderer-C6UIvyZQ.js} +1 -1
  83. package/src/assets/web-panel/assets/{Tokens-CFMKKyVG.js → Tokens-Dellq08f.js} +1 -1
  84. package/src/assets/web-panel/assets/{Trigger-L8as3UuJ.js → Trigger-VKw8d0J-.js} +1 -1
  85. package/src/assets/web-panel/assets/{Trust-MxmA4VZG.js → Trust-DNftdKT5.js} +1 -1
  86. package/src/assets/web-panel/assets/{UkeySign-C0VvJYRi.js → UkeySign-DX9VAKJI.js} +1 -1
  87. package/src/assets/web-panel/assets/{VideoEditing-Bf5QZj8N.js → VideoEditing-B2h3jO8q.js} +1 -1
  88. package/src/assets/web-panel/assets/{Wallet-BIJ9riRz.js → Wallet-CjGkof7f.js} +4 -4
  89. package/src/assets/web-panel/assets/{WebAuthn-B8p9Mhkw.js → WebAuthn-2Zfcu55P.js} +4 -4
  90. package/src/assets/web-panel/assets/{WorkflowEditor-BVsuaPen.js → WorkflowEditor-B9eZ8UxZ.js} +1 -1
  91. package/src/assets/web-panel/assets/{chat-BfkjoKpV.js → chat-CQNfD-YO.js} +1 -1
  92. package/src/assets/web-panel/assets/{colors-DeAlsWK8.js → colors-D4usyFT2.js} +1 -1
  93. package/src/assets/web-panel/assets/{compact-item-Cs97bz5j.js → compact-item-s4TpjvjX.js} +1 -1
  94. package/src/assets/web-panel/assets/{createContext-BqbG60LH.js → createContext-BRCSvYxj.js} +1 -1
  95. package/src/assets/web-panel/assets/devWarning-DKJjSWRn.js +1 -0
  96. package/src/assets/web-panel/assets/{hasIn-i_RY0b1X.js → hasIn-AdkUbkV-.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-BOVkf1K3.js → index--00U6vXZ.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-B-3tEpTc.js → index-B2V-eKfK.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-D2B4VPyP.js → index-B4JJCfQ2.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-BkBi5TmC.js → index-BIoG5yYf.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-79V_zv1s.js → index-BMcimU-2.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-U4f5izHz.js → index-BPhkFZ1I.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DiSkx4UB.js → index-BQ3DXBtD.js} +3 -3
  104. package/src/assets/web-panel/assets/{index-dIHr4B_u.js → index-Bad4VNvz.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-CIG8fQ2_.js → index-BgiXR_-_.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-DqBgBpgp.js → index-BtobShro.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-ZxO_-E8a.js → index-Bw2MEnUk.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-DUUrv141.js → index-C1mJtE6q.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-Bm7lLR4c.js → index-C484EJiS.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-COdGA-8n.js → index-C5KAQFPQ.js} +1 -1
  111. package/src/assets/web-panel/assets/index-C61cnl3K.js +1 -0
  112. package/src/assets/web-panel/assets/{index-CED2PL_y.js → index-C7Y8Aluc.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-YXgLng0K.js → index-CCsM_Ybx.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-0-agYlWL.js → index-CJCFe2Qf.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-B5UkyA3c.js → index-CJVEOa4i.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-B1CbOH1i.js → index-CJdQrZth.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-s1opAL-M.js → index-CKC1t3Ic.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-yObj1O4n.js → index-CQhAVU5t.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-CgsqdnRV.js → index-CYdoQsa1.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-Dk7-v4hP.js → index-Cduu2Hhr.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-B-JVTrpr.js → index-CkwiSLhU.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-CeWoNHau.js → index-Cvyt-zhm.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-CE2VtTlp.js → index-D2GjI2oe.js} +1 -1
  124. package/src/assets/web-panel/assets/index-D4XFHxBr.js +1 -0
  125. package/src/assets/web-panel/assets/{index-DmbfhJUP.js → index-DARsAyUp.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-BESLFt4q.js → index-DVKfQ0Ux.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-PT_5sBMA.js → index-DZT36ZYq.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-gyFpvL7l.js → index-D_XBHopw.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BYUVuwKt.js → index-DdCB70SS.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-BcUXAB2s.js → index-DzRjHxEv.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-Dk2qouAz.js → index-MMN9V6JF.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-D9evtvg2.js → index-RfMr79MW.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-BuLQobzW.js → index-_IRVsV9X.js} +1 -1
  134. package/src/assets/web-panel/assets/{index-Cf3WCLmy.js → index-nXyk8GZ9.js} +1 -1
  135. package/src/assets/web-panel/assets/{index-CG3PRyAL.js → index-s_buFMEG.js} +1 -1
  136. package/src/assets/web-panel/assets/{initDefaultProps-C9h9yKL3.js → initDefaultProps-Blmpk7HL.js} +1 -1
  137. package/src/assets/web-panel/assets/{motion-BvmLsPdu.js → motion-DPBJMv1n.js} +1 -1
  138. package/src/assets/web-panel/assets/{move-CoL4ekU1.js → move-ButdzCM7.js} +1 -1
  139. package/src/assets/web-panel/assets/{mtc-parser-C4pMy6xV.js → mtc-parser-cyRC_QCe.js} +1 -1
  140. package/src/assets/web-panel/assets/{omit-DPuJfNxx.js → omit-1epSV7hz.js} +1 -1
  141. package/src/assets/web-panel/assets/{pickAttrs-D0KEeaK2.js → pickAttrs-DuPY8xJs.js} +1 -1
  142. package/src/assets/web-panel/assets/{placementArrow-BpkyINxt.js → placementArrow-6XZqarhe.js} +1 -1
  143. package/src/assets/web-panel/assets/{responsiveObserve-DQI_wOq8.js → responsiveObserve-B40pXBWM.js} +1 -1
  144. package/src/assets/web-panel/assets/{slide-DYfdDiRg.js → slide-jbZ9Wu4n.js} +1 -1
  145. package/src/assets/web-panel/assets/{statusUtils-B5dtvb0D.js → statusUtils-BKQQcVy4.js} +1 -1
  146. package/src/assets/web-panel/assets/{styleChecker-CF310mSj.js → styleChecker-NiKfWjUT.js} +1 -1
  147. package/src/assets/web-panel/assets/{useFlexGapSupport-D1Qp4BLp.js → useFlexGapSupport-bQUCvPeb.js} +1 -1
  148. package/src/assets/web-panel/assets/{useFs-C7_iyvJK.js → useFs-CUGji7ph.js} +1 -1
  149. package/src/assets/web-panel/assets/{usePersonalDataHub-DZUt_fIm.js → usePersonalDataHub-4veyN2N2.js} +1 -1
  150. package/src/assets/web-panel/assets/{vnode-D3dsi0lZ.js → vnode-1-nLug-Z.js} +1 -1
  151. package/src/assets/web-panel/assets/{zoom-WdbOlvdQ.js → zoom-dhCeRDMo.js} +1 -1
  152. package/src/assets/web-panel/index.html +1 -1
  153. package/src/command-help-index.json +6 -6
  154. package/src/commands/automation.js +103 -1
  155. package/src/commands/cowork.js +174 -19
  156. package/src/commands/permissions.js +240 -16
  157. package/src/commands/plugin.js +1293 -56
  158. package/src/commands/session-location.js +276 -0
  159. package/src/commands/session.js +2 -0
  160. package/src/commands/team-merge-review.js +937 -0
  161. package/src/commands/team.js +2 -0
  162. package/src/data/changelog.json +27 -1
  163. package/src/gateways/ws/action-protocol.js +35 -10
  164. package/src/gateways/ws/remote-session-protocol.js +24 -1
  165. package/src/gateways/ws/ws-agent-handler.js +12 -0
  166. package/src/gateways/ws/ws-server.js +48 -12
  167. package/src/harness/jsonl-session-store.js +341 -24
  168. package/src/harness/mcp-client.js +8 -1
  169. package/src/harness/remote-session-audit.js +1 -0
  170. package/src/harness/remote-session-crypto.js +210 -2
  171. package/src/harness/remote-session-registry.js +159 -6
  172. package/src/harness/session-list-index.js +233 -14
  173. package/src/lib/agent-authority.js +6 -0
  174. package/src/lib/agent-schedule-store.js +139 -28
  175. package/src/lib/agent-team/team-merge-review-store.js +765 -0
  176. package/src/lib/agent-team/team-merge-review-transaction.js +1223 -0
  177. package/src/lib/agent-team/team-merge-review.js +1847 -0
  178. package/src/lib/automation-center-incidents.js +314 -0
  179. package/src/lib/automation-center-runtime.js +448 -0
  180. package/src/lib/automation-center.js +182 -38
  181. package/src/lib/automation-engine.js +41 -0
  182. package/src/lib/automation-execution-authority.js +241 -12
  183. package/src/lib/automation-execution-incident.js +737 -0
  184. package/src/lib/background-agent-keeper-client.js +248 -0
  185. package/src/lib/background-agent-keeper-protocol.js +147 -0
  186. package/src/lib/background-agent-supervisor.js +520 -40
  187. package/src/lib/background-interaction-resolver.js +12 -0
  188. package/src/lib/background-turn-bootstrap-protocol.js +126 -0
  189. package/src/lib/cowork-cron.js +85 -14
  190. package/src/lib/cowork-workflow.js +336 -16
  191. package/src/lib/dynamic-workflow-facade.js +648 -0
  192. package/src/lib/execution-location-contract.js +551 -0
  193. package/src/lib/execution-location-runtime.js +130 -0
  194. package/src/lib/ide-context.js +94 -6
  195. package/src/lib/mcp-stdio-package-materialization.js +166 -14
  196. package/src/lib/packer/pkg-config-generator.js +2 -0
  197. package/src/lib/permission-authority.js +77 -0
  198. package/src/lib/permission-side-effect-center.js +495 -0
  199. package/src/lib/plugin-runtime/install.js +514 -13
  200. package/src/lib/plugin-runtime/marketplace-artifact-readback.js +900 -0
  201. package/src/lib/plugin-runtime/marketplace-catalog.js +1181 -0
  202. package/src/lib/plugin-runtime/marketplace-impact.js +408 -0
  203. package/src/lib/plugin-runtime/marketplace-remote-artifacts.js +1567 -0
  204. package/src/lib/plugin-security.js +60 -3
  205. package/src/lib/process-execution-broker/index.js +1095 -57
  206. package/src/lib/process-execution-broker/linux-bwrap-descriptor-launch.js +375 -0
  207. package/src/lib/process-execution-broker/linux-generic-bwrap-runtime.js +90 -16
  208. package/src/lib/process-execution-broker/linux-generic-bwrap.js +159 -18
  209. package/src/lib/process-execution-broker/macos-mcp-launcher-contract.js +278 -0
  210. package/src/lib/process-execution-broker/macos-mcp-launcher-protocol.json +23 -0
  211. package/src/lib/process-execution-broker/macos-mcp-launcher.c +1497 -0
  212. package/src/lib/process-execution-broker/mcp-fd-entry-bootstrap.js +37 -0
  213. package/src/lib/process-execution-broker/platform-sandbox.js +2330 -279
  214. package/src/lib/process-execution-broker/windows-sandbox-helper.dll +0 -0
  215. package/src/lib/process-execution-broker/windows-sandbox-helper.exe +0 -0
  216. package/src/lib/process-execution-broker/windows-sandbox.cs +50 -4
  217. package/src/lib/public-ide-capability-manifest.js +3 -0
  218. package/src/lib/remote-approval-bridge.js +123 -17
  219. package/src/lib/remote-membership-authority.js +632 -0
  220. package/src/lib/remote-membership-coordinator.js +2409 -0
  221. package/src/lib/remote-membership-host-store.js +1750 -0
  222. package/src/lib/scheduler-kernel/agenda-adapter.js +17 -7
  223. package/src/lib/scheduler-kernel/automation-adapter.js +105 -11
  224. package/src/lib/scheduler-kernel/automation-event-adapter.js +87 -11
  225. package/src/lib/scheduler-kernel/contract.js +91 -0
  226. package/src/lib/scheduler-kernel/cowork-cron-adapter.js +12 -1
  227. package/src/lib/scheduler-kernel/loop-adapter.js +7 -1
  228. package/src/lib/scheduler-kernel/routine-adapter.js +7 -1
  229. package/src/lib/scheduler-kernel/runtime-control-capabilities.js +31 -0
  230. package/src/lib/scheduler-kernel/runtime.js +176 -3
  231. package/src/lib/scheduler-kernel/store.js +1253 -45
  232. package/src/lib/scoped-permission-store.js +369 -0
  233. package/src/lib/session-anti-rollback-anchor.js +262 -5
  234. package/src/lib/sub-agent-context.js +4 -0
  235. package/src/lib/workflow-definition-contract.js +149 -0
  236. package/src/repl/agent-repl.js +41 -7
  237. package/src/repl/chat-repl.js +2 -0
  238. package/src/repl/clipboard-image.js +735 -16
  239. package/src/repl/ide-status.js +1 -1
  240. package/src/repl/prompt-interactions.js +59 -6
  241. package/src/runtime/agent-core.js +93 -3
  242. package/src/runtime/headless-runner.js +30 -25
  243. package/src/runtime/headless-stream.js +32 -25
  244. package/src/runtime/mcp-config.js +81 -10
  245. package/src/workers/background-agent-keeper.js +498 -0
  246. package/src/workers/background-agent-worker.js +358 -13
  247. package/src/workers/background-turn-bootstrap.js +143 -0
  248. package/src/assets/web-panel/assets/MobileProjects-BWdOvQAj.js +0 -1
  249. package/src/assets/web-panel/assets/Projects-BV4oApsm.js +0 -1
  250. package/src/assets/web-panel/assets/devWarning-DPty0mGN.js +0 -1
  251. package/src/assets/web-panel/assets/index-Bt7g0Vq_.js +0 -1
  252. package/src/assets/web-panel/assets/index-ChVT0p9u.js +0 -1
@@ -6,6 +6,8 @@ import { getHomeDir } from "../paths.js";
6
6
  import { ensurePrivateDirectory, ensurePrivateFile } from "../secure-fs.js";
7
7
  import {
8
8
  OCCURRENCE_STATUS,
9
+ RUNTIME_CONTROL_SAFE_POINTS,
10
+ RUNTIME_PAUSE_RESUME,
9
11
  SchedulerKernelError,
10
12
  canonicalJson,
11
13
  deriveOccurrenceIdentity,
@@ -16,21 +18,51 @@ import {
16
18
  normalizeIdentifier,
17
19
  normalizeJson,
18
20
  normalizeMaxAttempts,
21
+ normalizeRuntimeControlCapability,
19
22
  } from "./contract.js";
20
23
  import { isCanonicalSchedulerSourcePath } from "./source-locator-path.js";
21
24
 
22
25
  const requireCjs = createRequire(import.meta.url);
23
26
 
27
+ const STORAGE_FAILURE_CODES = Object.freeze([
28
+ "ENOSPC",
29
+ "EROFS",
30
+ "EIO",
31
+ "SQLITE_FULL",
32
+ "SQLITE_IOERR",
33
+ "SQLITE_READONLY",
34
+ ]);
35
+ const STORAGE_FAILURE_CODE_SET = new Set(STORAGE_FAILURE_CODES);
36
+
24
37
  export const SCHEDULER_APPLICATION_ID = 0x4343534b; // "CCSK"
25
- export const SCHEDULER_STORE_SCHEMA_VERSION = 5;
38
+ export const SCHEDULER_STORE_SCHEMA_VERSION = 6;
26
39
  export const DEFAULT_BUSY_TIMEOUT_MS = 5_000;
27
40
  export const MAX_LEASE_MS = 24 * 60 * 60 * 1_000;
28
41
  export const MIN_AUTHORITY_WINDOW_MS = 60_000;
29
42
  export const MAX_AUTHORITY_WINDOW_MS = 31 * 24 * 60 * 60 * 1_000;
30
43
  export const MAX_AUTHORITY_BUDGET = 1_000_000;
44
+ export const MAX_RUNTIME_CHECKPOINT_BYTES = 64 * 1024;
31
45
  export const DEFAULT_AUTHORITY_WINDOW_MS = 24 * 60 * 60 * 1_000;
32
46
  export const DEFAULT_AUTHORITY_MAX_RUNS = 100_000;
33
47
  export const DEFAULT_AUTHORITY_MAX_UNITS = 100_000;
48
+ export const MAX_RUNTIME_CONTROL_OCCURRENCES = 200;
49
+ export const RUNTIME_CONTROL_OCCURRENCE_STATUSES = Object.freeze([
50
+ "running",
51
+ "pause_requested",
52
+ "paused",
53
+ ]);
54
+ export const RUNTIME_CONTROL_JOB_KINDS = Object.freeze([
55
+ "agenda",
56
+ "automation",
57
+ "automation-event",
58
+ "cowork-cron",
59
+ "loop-iteration",
60
+ "routine",
61
+ ]);
62
+ const RUNTIME_CONTROL_OCCURRENCE_STATUS_SET = new Set(
63
+ RUNTIME_CONTROL_OCCURRENCE_STATUSES,
64
+ );
65
+ const RUNTIME_CONTROL_JOB_KIND_SET = new Set(RUNTIME_CONTROL_JOB_KINDS);
34
66
  export const SCHEDULER_ADJUDICATION_AUTHORITY = "local-operator";
35
67
  export const SCHEDULER_ADJUDICATION_DECISIONS = Object.freeze({
36
68
  CONFIRMED_APPLIED: "confirmed_applied",
@@ -58,6 +90,7 @@ const MIGRATION_V2_NAME = "scheduler-kernel-authority-v2";
58
90
  const MIGRATION_V3_NAME = "scheduler-kernel-adjudication-v3";
59
91
  const MIGRATION_V4_NAME = "scheduler-kernel-domain-migration-v4";
60
92
  const MIGRATION_V5_NAME = "scheduler-kernel-source-locator-v5";
93
+ const MIGRATION_V6_NAME = "scheduler-kernel-runtime-control-v6";
61
94
  const V1_USER_TABLES = Object.freeze([
62
95
  "events",
63
96
  "jobs",
@@ -77,12 +110,17 @@ const V3_USER_TABLES = Object.freeze([
77
110
  ...V2_USER_TABLES,
78
111
  "scheduler_occurrence_adjudications",
79
112
  ]);
80
- const USER_TABLES = Object.freeze([
113
+ const V4_V5_USER_TABLES = Object.freeze([
81
114
  ...V2_USER_TABLES,
82
115
  "scheduler_domain_migration_entries",
83
116
  "scheduler_domain_migrations",
84
117
  "scheduler_occurrence_adjudications",
85
118
  ]);
119
+ const USER_TABLES = Object.freeze([
120
+ ...V4_V5_USER_TABLES,
121
+ "scheduler_occurrence_controls",
122
+ "scheduler_occurrence_retries",
123
+ ]);
86
124
 
87
125
  export const MIGRATION_V1_SQL = `
88
126
  CREATE TABLE migrations (
@@ -415,6 +453,68 @@ export const MIGRATION_V5_CHECKSUM = createHash("sha256")
415
453
  .update(MIGRATION_V5_SQL.trim().replace(/\r\n/g, "\n"), "utf8")
416
454
  .digest("hex");
417
455
 
456
+ export const MIGRATION_V6_SQL = `
457
+ CREATE TABLE scheduler_occurrence_controls (
458
+ occurrence_id TEXT PRIMARY KEY NOT NULL
459
+ REFERENCES occurrences(occurrence_id) ON DELETE RESTRICT,
460
+ capability_json TEXT NOT NULL CHECK (json_valid(capability_json)),
461
+ capability_digest TEXT NOT NULL CHECK (
462
+ capability_digest GLOB 'sha256:[0-9a-f]*'
463
+ AND length(capability_digest) = 71
464
+ ),
465
+ state TEXT NOT NULL CHECK (
466
+ state IN ('pause_requested', 'paused', 'resumed', 'terminal')
467
+ ),
468
+ revision INTEGER NOT NULL CHECK (revision >= 1),
469
+ pause_request_id TEXT NOT NULL,
470
+ resume_request_id TEXT UNIQUE,
471
+ expected_fence INTEGER NOT NULL CHECK (expected_fence >= 1),
472
+ checkpoint_json TEXT CHECK (
473
+ checkpoint_json IS NULL OR json_valid(checkpoint_json)
474
+ ),
475
+ requested_at INTEGER NOT NULL CHECK (requested_at >= 0),
476
+ paused_at INTEGER CHECK (paused_at >= 0),
477
+ resumed_at INTEGER CHECK (resumed_at >= 0),
478
+ terminal_at INTEGER CHECK (terminal_at >= 0),
479
+ updated_at INTEGER NOT NULL CHECK (updated_at >= 0),
480
+ CHECK (
481
+ (state = 'pause_requested' AND paused_at IS NULL
482
+ AND resumed_at IS NULL AND terminal_at IS NULL)
483
+ OR
484
+ (state = 'paused' AND paused_at IS NOT NULL
485
+ AND resumed_at IS NULL AND terminal_at IS NULL)
486
+ OR
487
+ (state = 'resumed' AND paused_at IS NOT NULL
488
+ AND resumed_at IS NOT NULL AND terminal_at IS NULL)
489
+ OR
490
+ (state = 'terminal' AND terminal_at IS NOT NULL)
491
+ )
492
+ );
493
+
494
+ CREATE UNIQUE INDEX scheduler_occurrence_controls_pause_request
495
+ ON scheduler_occurrence_controls(pause_request_id);
496
+ CREATE INDEX scheduler_occurrence_controls_state
497
+ ON scheduler_occurrence_controls(state, updated_at, occurrence_id);
498
+
499
+ CREATE TABLE scheduler_occurrence_retries (
500
+ occurrence_id TEXT NOT NULL
501
+ REFERENCES occurrences(occurrence_id) ON DELETE RESTRICT,
502
+ request_id TEXT NOT NULL UNIQUE,
503
+ expected_fence INTEGER NOT NULL CHECK (expected_fence >= 1),
504
+ expected_error_code TEXT NOT NULL,
505
+ created_at INTEGER NOT NULL CHECK (created_at >= 0),
506
+ claimed_at INTEGER CHECK (claimed_at >= 0),
507
+ PRIMARY KEY (occurrence_id, request_id)
508
+ );
509
+
510
+ CREATE INDEX scheduler_occurrence_retries_pending
511
+ ON scheduler_occurrence_retries(occurrence_id, claimed_at, expected_fence);
512
+ `;
513
+
514
+ export const MIGRATION_V6_CHECKSUM = createHash("sha256")
515
+ .update(MIGRATION_V6_SQL.trim().replace(/\r\n/g, "\n"), "utf8")
516
+ .digest("hex");
517
+
418
518
  // Fingerprint of the normalized sqlite_master catalog produced by the v1 DDL.
419
519
  // Unlike the migration-source checksum, this also detects added triggers/views
420
520
  // and constraint/foreign-key changes that preserve the visible column list.
@@ -428,6 +528,8 @@ export const SCHEMA_V4_FINGERPRINT =
428
528
  "59762b9d5da862b857edb76021ed51d05a256133dce543a3fd7d7b0403c9c081";
429
529
  export const SCHEMA_V5_FINGERPRINT =
430
530
  "9619647628488fc4fdc79ad36b4b6632d8257f40c23245822a18941870c187ac";
531
+ export const SCHEMA_V6_FINGERPRINT =
532
+ "6abe5595ba3f772c9643440b27d18d5d40057c85523ecb3b4986b195d67787c2";
431
533
 
432
534
  const EXPECTED_COLUMNS = Object.freeze({
433
535
  migrations: [
@@ -531,6 +633,30 @@ const EXPECTED_COLUMNS = Object.freeze({
531
633
  ["retry_settled_at", "INTEGER", 0, 0],
532
634
  ["retry_outcome_json", "TEXT", 0, 0],
533
635
  ],
636
+ scheduler_occurrence_controls: [
637
+ ["occurrence_id", "TEXT", 1, 1],
638
+ ["capability_json", "TEXT", 1, 0],
639
+ ["capability_digest", "TEXT", 1, 0],
640
+ ["state", "TEXT", 1, 0],
641
+ ["revision", "INTEGER", 1, 0],
642
+ ["pause_request_id", "TEXT", 1, 0],
643
+ ["resume_request_id", "TEXT", 0, 0],
644
+ ["expected_fence", "INTEGER", 1, 0],
645
+ ["checkpoint_json", "TEXT", 0, 0],
646
+ ["requested_at", "INTEGER", 1, 0],
647
+ ["paused_at", "INTEGER", 0, 0],
648
+ ["resumed_at", "INTEGER", 0, 0],
649
+ ["terminal_at", "INTEGER", 0, 0],
650
+ ["updated_at", "INTEGER", 1, 0],
651
+ ],
652
+ scheduler_occurrence_retries: [
653
+ ["occurrence_id", "TEXT", 1, 1],
654
+ ["request_id", "TEXT", 1, 2],
655
+ ["expected_fence", "INTEGER", 1, 0],
656
+ ["expected_error_code", "TEXT", 1, 0],
657
+ ["created_at", "INTEGER", 1, 0],
658
+ ["claimed_at", "INTEGER", 0, 0],
659
+ ],
534
660
  scheduler_domain_migrations: [
535
661
  ["migration_id", "TEXT", 1, 1],
536
662
  ["manifest_digest", "TEXT", 1, 0],
@@ -579,6 +705,9 @@ const EXPECTED_INDEXES = Object.freeze([
579
705
  "scheduler_domain_migrations_state",
580
706
  "scheduler_events_job",
581
707
  "scheduler_events_occurrence",
708
+ "scheduler_occurrence_controls_pause_request",
709
+ "scheduler_occurrence_controls_state",
710
+ "scheduler_occurrence_retries_pending",
582
711
  "scheduler_occurrences_claim",
583
712
  "scheduler_occurrences_idempotency",
584
713
  "scheduler_occurrences_job",
@@ -593,6 +722,45 @@ function schemaError(code, message, cause = undefined, details = undefined) {
593
722
  );
594
723
  }
595
724
 
725
+ function normalizedStorageFailureCode(value) {
726
+ if (typeof value !== "string") return null;
727
+ const candidate = value.trim().toUpperCase();
728
+ if (STORAGE_FAILURE_CODE_SET.has(candidate)) return candidate;
729
+ return (
730
+ STORAGE_FAILURE_CODES.find((code) => candidate.startsWith(`${code}_`)) ||
731
+ null
732
+ );
733
+ }
734
+
735
+ function findStorageFailureCode(error) {
736
+ const visited = new Set();
737
+ let current = error;
738
+ while (
739
+ current !== null &&
740
+ (typeof current === "object" || typeof current === "function") &&
741
+ !visited.has(current)
742
+ ) {
743
+ visited.add(current);
744
+ const code = normalizedStorageFailureCode(current.code);
745
+ if (code) return code;
746
+ current = current.cause;
747
+ }
748
+ return null;
749
+ }
750
+
751
+ function storageUnavailableError({ phase, storageCode, commitState }) {
752
+ return new SchedulerKernelError(
753
+ "SCHEDULER_STORAGE_UNAVAILABLE",
754
+ "Scheduler storage is unavailable",
755
+ {
756
+ phase,
757
+ storageCode,
758
+ commitState,
759
+ retryable: false,
760
+ },
761
+ );
762
+ }
763
+
596
764
  function assertPositiveSafeInteger(value, field) {
597
765
  if (!Number.isSafeInteger(value) || value < 1) {
598
766
  throw invalidArgument(`${field} must be a positive integer`);
@@ -843,6 +1011,109 @@ function mapOccurrence(row) {
843
1011
  };
844
1012
  }
845
1013
 
1014
+ function mapRuntimeControlOccurrence(row) {
1015
+ if (!row) return null;
1016
+ return {
1017
+ id: row.occurrence_id,
1018
+ jobId: row.job_id,
1019
+ jobKind: row.job_kind,
1020
+ runtimeStatus: row.runtime_status,
1021
+ occurrenceStatus: row.occurrence_status,
1022
+ scheduledFor: row.scheduled_for,
1023
+ attempt: row.attempt,
1024
+ maxAttempts: row.max_attempts,
1025
+ fence: row.fence,
1026
+ createdAt: row.created_at,
1027
+ updatedAt: row.updated_at,
1028
+ control:
1029
+ row.control_state === null
1030
+ ? null
1031
+ : {
1032
+ state: row.control_state,
1033
+ revision: row.control_revision,
1034
+ expectedFence: row.control_expected_fence,
1035
+ capabilityDigest: row.control_capability_digest,
1036
+ requestedAt: row.control_requested_at,
1037
+ pausedAt: row.control_paused_at,
1038
+ updatedAt: row.control_updated_at,
1039
+ },
1040
+ };
1041
+ }
1042
+
1043
+ function normalizeRuntimeControlEnumeration(values, allowed, field) {
1044
+ if (!Array.isArray(values) || values.length === 0) {
1045
+ throw invalidArgument(`${field} must be a non-empty array`);
1046
+ }
1047
+ const normalized = [
1048
+ ...new Set(
1049
+ values.map((value, index) =>
1050
+ normalizeIdentifier(value, `${field}[${index}]`, { maxLength: 128 }),
1051
+ ),
1052
+ ),
1053
+ ];
1054
+ const unsupported = normalized.filter((value) => !allowed.has(value));
1055
+ if (unsupported.length > 0) {
1056
+ throw invalidArgument(`${field} contains unsupported values`, {
1057
+ values: unsupported.sort(),
1058
+ });
1059
+ }
1060
+ return normalized.sort();
1061
+ }
1062
+
1063
+ export function schedulerRuntimeControlCapabilityDigest(capability) {
1064
+ return sha256PayloadDigest(
1065
+ normalizeRuntimeControlCapability(capability),
1066
+ "runtimeControl.capability",
1067
+ );
1068
+ }
1069
+
1070
+ function mapOccurrenceControl(row) {
1071
+ if (!row) return null;
1072
+ const capability = normalizeRuntimeControlCapability(
1073
+ readStoredJson(row.capability_json, "scheduler runtime control capability"),
1074
+ );
1075
+ const capabilityDigest = schedulerRuntimeControlCapabilityDigest(capability);
1076
+ if (capabilityDigest !== row.capability_digest) {
1077
+ throw schemaError(
1078
+ "SCHEDULER_DATA_CORRUPT",
1079
+ `Scheduler runtime-control capability digest is invalid: ${row.occurrence_id}`,
1080
+ );
1081
+ }
1082
+ return {
1083
+ occurrenceId: row.occurrence_id,
1084
+ capability,
1085
+ capabilityDigest,
1086
+ state: row.state,
1087
+ revision: row.revision,
1088
+ pauseRequestId: row.pause_request_id,
1089
+ resumeRequestId: row.resume_request_id,
1090
+ expectedFence: row.expected_fence,
1091
+ checkpoint:
1092
+ row.checkpoint_json === null
1093
+ ? null
1094
+ : readStoredJson(
1095
+ row.checkpoint_json,
1096
+ "scheduler runtime control checkpoint",
1097
+ ),
1098
+ requestedAt: row.requested_at,
1099
+ pausedAt: row.paused_at,
1100
+ resumedAt: row.resumed_at,
1101
+ terminalAt: row.terminal_at,
1102
+ updatedAt: row.updated_at,
1103
+ };
1104
+ }
1105
+
1106
+ function normalizeRuntimeCheckpoint(value) {
1107
+ const checkpoint = normalizeJson(value ?? null, "runtimeControl.checkpoint");
1108
+ const encoded = canonicalJson(checkpoint, "runtimeControl.checkpoint");
1109
+ if (Buffer.byteLength(encoded, "utf8") > MAX_RUNTIME_CHECKPOINT_BYTES) {
1110
+ throw invalidArgument(
1111
+ `runtimeControl.checkpoint exceeds ${MAX_RUNTIME_CHECKPOINT_BYTES} encoded bytes`,
1112
+ );
1113
+ }
1114
+ return { checkpoint, encoded };
1115
+ }
1116
+
846
1117
  function mapEvent(row) {
847
1118
  return {
848
1119
  eventId: row.event_id,
@@ -1483,7 +1754,9 @@ function verifySchema(db, version = SCHEDULER_STORE_SCHEMA_VERSION) {
1483
1754
  ? V2_USER_TABLES
1484
1755
  : version === 3
1485
1756
  ? V3_USER_TABLES
1486
- : USER_TABLES;
1757
+ : version <= 5
1758
+ ? V4_V5_USER_TABLES
1759
+ : USER_TABLES;
1487
1760
  if (JSON.stringify(tables) !== JSON.stringify(expectedTables)) {
1488
1761
  throw schemaError(
1489
1762
  "SCHEDULER_SCHEMA_UNKNOWN",
@@ -1526,7 +1799,9 @@ function verifySchema(db, version = SCHEDULER_STORE_SCHEMA_VERSION) {
1526
1799
  ? SCHEMA_V3_FINGERPRINT
1527
1800
  : version === 4
1528
1801
  ? SCHEMA_V4_FINGERPRINT
1529
- : SCHEMA_V5_FINGERPRINT;
1802
+ : version === 5
1803
+ ? SCHEMA_V5_FINGERPRINT
1804
+ : SCHEMA_V6_FINGERPRINT;
1530
1805
  if (actualFingerprint !== expectedFingerprint) {
1531
1806
  throw schemaError(
1532
1807
  "SCHEDULER_SCHEMA_CORRUPT",
@@ -1573,7 +1848,11 @@ function verifySchema(db, version = SCHEDULER_STORE_SCHEMA_VERSION) {
1573
1848
  (version >= 5 &&
1574
1849
  (migrations[4]?.version !== 5 ||
1575
1850
  migrations[4]?.name !== MIGRATION_V5_NAME ||
1576
- migrations[4]?.checksum !== MIGRATION_V5_CHECKSUM))
1851
+ migrations[4]?.checksum !== MIGRATION_V5_CHECKSUM)) ||
1852
+ (version >= 6 &&
1853
+ (migrations[5]?.version !== 6 ||
1854
+ migrations[5]?.name !== MIGRATION_V6_NAME ||
1855
+ migrations[5]?.checksum !== MIGRATION_V6_CHECKSUM))
1577
1856
  ) {
1578
1857
  throw schemaError(
1579
1858
  "SCHEDULER_SCHEMA_UNKNOWN",
@@ -1593,7 +1872,9 @@ function verifySchema(db, version = SCHEDULER_STORE_SCHEMA_VERSION) {
1593
1872
  (name) =>
1594
1873
  (version >= 2 || !name.startsWith("scheduler_authority_")) &&
1595
1874
  (version >= 3 || !name.startsWith("scheduler_adjudications_")) &&
1596
- (version >= 4 || !name.startsWith("scheduler_domain_migration")),
1875
+ (version >= 4 || !name.startsWith("scheduler_domain_migration")) &&
1876
+ (version >= 6 || !name.startsWith("scheduler_occurrence_controls_")) &&
1877
+ (version >= 6 || !name.startsWith("scheduler_occurrence_retries_")),
1597
1878
  );
1598
1879
  if (JSON.stringify(indexes) !== JSON.stringify(expectedIndexes)) {
1599
1880
  throw schemaError(
@@ -1713,6 +1994,10 @@ function migrateSourceLocatorV5(db) {
1713
1994
  db.exec(MIGRATION_V5_SQL);
1714
1995
  }
1715
1996
 
1997
+ function migrateRuntimeControlV6(db) {
1998
+ db.exec(MIGRATION_V6_SQL);
1999
+ }
2000
+
1716
2001
  function initializeOrVerifySchema(db, now) {
1717
2002
  assertDatabaseIntegrity(db);
1718
2003
  const tables = listUserTables(db);
@@ -1723,6 +2008,7 @@ function initializeOrVerifySchema(db, now) {
1723
2008
  migrateAdjudicationV3(db);
1724
2009
  migrateDomainMigrationV4(db);
1725
2010
  migrateSourceLocatorV5(db);
2011
+ migrateRuntimeControlV6(db);
1726
2012
  db.prepare(
1727
2013
  "INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
1728
2014
  ).run(1, MIGRATION_V1_NAME, MIGRATION_V1_CHECKSUM, now);
@@ -1738,6 +2024,9 @@ function initializeOrVerifySchema(db, now) {
1738
2024
  db.prepare(
1739
2025
  "INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
1740
2026
  ).run(5, MIGRATION_V5_NAME, MIGRATION_V5_CHECKSUM, now);
2027
+ db.prepare(
2028
+ "INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
2029
+ ).run(6, MIGRATION_V6_NAME, MIGRATION_V6_CHECKSUM, now);
1741
2030
  db.pragma(`application_id = ${SCHEDULER_APPLICATION_ID}`);
1742
2031
  db.pragma(`user_version = ${SCHEDULER_STORE_SCHEMA_VERSION}`);
1743
2032
  });
@@ -1756,6 +2045,7 @@ function initializeOrVerifySchema(db, now) {
1756
2045
  migrateAdjudicationV3(db);
1757
2046
  migrateDomainMigrationV4(db);
1758
2047
  migrateSourceLocatorV5(db);
2048
+ migrateRuntimeControlV6(db);
1759
2049
  db.prepare(
1760
2050
  "INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
1761
2051
  ).run(2, MIGRATION_V2_NAME, MIGRATION_V2_CHECKSUM, now);
@@ -1768,6 +2058,9 @@ function initializeOrVerifySchema(db, now) {
1768
2058
  db.prepare(
1769
2059
  "INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
1770
2060
  ).run(5, MIGRATION_V5_NAME, MIGRATION_V5_CHECKSUM, now);
2061
+ db.prepare(
2062
+ "INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
2063
+ ).run(6, MIGRATION_V6_NAME, MIGRATION_V6_CHECKSUM, now);
1771
2064
  db.pragma(`user_version = ${SCHEDULER_STORE_SCHEMA_VERSION}`);
1772
2065
  });
1773
2066
  migrate.immediate();
@@ -1777,6 +2070,7 @@ function initializeOrVerifySchema(db, now) {
1777
2070
  migrateAdjudicationV3(db);
1778
2071
  migrateDomainMigrationV4(db);
1779
2072
  migrateSourceLocatorV5(db);
2073
+ migrateRuntimeControlV6(db);
1780
2074
  db.prepare(
1781
2075
  "INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
1782
2076
  ).run(3, MIGRATION_V3_NAME, MIGRATION_V3_CHECKSUM, now);
@@ -1786,6 +2080,9 @@ function initializeOrVerifySchema(db, now) {
1786
2080
  db.prepare(
1787
2081
  "INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
1788
2082
  ).run(5, MIGRATION_V5_NAME, MIGRATION_V5_CHECKSUM, now);
2083
+ db.prepare(
2084
+ "INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
2085
+ ).run(6, MIGRATION_V6_NAME, MIGRATION_V6_CHECKSUM, now);
1789
2086
  db.pragma(`user_version = ${SCHEDULER_STORE_SCHEMA_VERSION}`);
1790
2087
  });
1791
2088
  migrate.immediate();
@@ -1794,12 +2091,16 @@ function initializeOrVerifySchema(db, now) {
1794
2091
  const migrate = db.transaction(() => {
1795
2092
  migrateDomainMigrationV4(db);
1796
2093
  migrateSourceLocatorV5(db);
2094
+ migrateRuntimeControlV6(db);
1797
2095
  db.prepare(
1798
2096
  "INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
1799
2097
  ).run(4, MIGRATION_V4_NAME, MIGRATION_V4_CHECKSUM, now);
1800
2098
  db.prepare(
1801
2099
  "INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
1802
2100
  ).run(5, MIGRATION_V5_NAME, MIGRATION_V5_CHECKSUM, now);
2101
+ db.prepare(
2102
+ "INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
2103
+ ).run(6, MIGRATION_V6_NAME, MIGRATION_V6_CHECKSUM, now);
1803
2104
  db.pragma(`user_version = ${SCHEDULER_STORE_SCHEMA_VERSION}`);
1804
2105
  });
1805
2106
  migrate.immediate();
@@ -1807,9 +2108,23 @@ function initializeOrVerifySchema(db, now) {
1807
2108
  verifySchema(db, 4);
1808
2109
  const migrate = db.transaction(() => {
1809
2110
  migrateSourceLocatorV5(db);
2111
+ migrateRuntimeControlV6(db);
1810
2112
  db.prepare(
1811
2113
  "INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
1812
2114
  ).run(5, MIGRATION_V5_NAME, MIGRATION_V5_CHECKSUM, now);
2115
+ db.prepare(
2116
+ "INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
2117
+ ).run(6, MIGRATION_V6_NAME, MIGRATION_V6_CHECKSUM, now);
2118
+ db.pragma(`user_version = ${SCHEDULER_STORE_SCHEMA_VERSION}`);
2119
+ });
2120
+ migrate.immediate();
2121
+ } else if (db.pragma("user_version", { simple: true }) === 5) {
2122
+ verifySchema(db, 5);
2123
+ const migrate = db.transaction(() => {
2124
+ migrateRuntimeControlV6(db);
2125
+ db.prepare(
2126
+ "INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
2127
+ ).run(6, MIGRATION_V6_NAME, MIGRATION_V6_CHECKSUM, now);
1813
2128
  db.pragma(`user_version = ${SCHEDULER_STORE_SCHEMA_VERSION}`);
1814
2129
  });
1815
2130
  migrate.immediate();
@@ -1848,11 +2163,21 @@ function openDatabase({ file, Database, busyTimeoutMs, clock }) {
1848
2163
  }
1849
2164
  const now = normalizeEpochMs(clock(), "clock result");
1850
2165
  if (target !== ":memory:") {
1851
- ensurePrivateDirectory(dirname(target));
1852
- // Existing WAL/SHM files participate in the next SQLite open, so validate
1853
- // them before the native driver consumes either sidecar.
1854
- for (const candidate of sqliteStorageFiles(target)) {
1855
- if (existsSync(candidate)) ensurePrivateFile(candidate);
2166
+ try {
2167
+ ensurePrivateDirectory(dirname(target));
2168
+ // Existing WAL/SHM files participate in the next SQLite open, so validate
2169
+ // them before the native driver consumes either sidecar.
2170
+ for (const candidate of sqliteStorageFiles(target)) {
2171
+ if (existsSync(candidate)) ensurePrivateFile(candidate);
2172
+ }
2173
+ } catch (cause) {
2174
+ const storageCode = findStorageFailureCode(cause);
2175
+ if (!storageCode) throw cause;
2176
+ throw storageUnavailableError({
2177
+ phase: "open",
2178
+ storageCode,
2179
+ commitState: "unknown",
2180
+ });
1856
2181
  }
1857
2182
  }
1858
2183
 
@@ -1873,6 +2198,14 @@ function openDatabase({ file, Database, busyTimeoutMs, clock }) {
1873
2198
  // Preserve the authoritative open/schema failure.
1874
2199
  }
1875
2200
  if (cause instanceof SchedulerKernelError) throw cause;
2201
+ const storageCode = findStorageFailureCode(cause);
2202
+ if (storageCode) {
2203
+ throw storageUnavailableError({
2204
+ phase: "open",
2205
+ storageCode,
2206
+ commitState: "unknown",
2207
+ });
2208
+ }
1876
2209
  throw schemaError(
1877
2210
  "SCHEDULER_SCHEMA_CORRUPT",
1878
2211
  "Scheduler database could not be opened or verified",
@@ -1881,12 +2214,27 @@ function openDatabase({ file, Database, busyTimeoutMs, clock }) {
1881
2214
  }
1882
2215
 
1883
2216
  if (target !== ":memory:") {
1884
- // A quiet WAL database may not have materialized sidecars yet. On Windows,
1885
- // asking secure-fs to classify a missing path deliberately falls back to a
1886
- // native ACL preflight. Only inspect files that SQLite actually created;
1887
- // later opens repeat this check for any surviving sidecars.
1888
- for (const candidate of sqliteStorageFiles(target)) {
1889
- if (existsSync(candidate)) ensurePrivateFile(candidate);
2217
+ try {
2218
+ // A quiet WAL database may not have materialized sidecars yet. On Windows,
2219
+ // asking secure-fs to classify a missing path deliberately falls back to a
2220
+ // native ACL preflight. Only inspect files that SQLite actually created;
2221
+ // later opens repeat this check for any surviving sidecars.
2222
+ for (const candidate of sqliteStorageFiles(target)) {
2223
+ if (existsSync(candidate)) ensurePrivateFile(candidate);
2224
+ }
2225
+ } catch (cause) {
2226
+ try {
2227
+ db.close();
2228
+ } catch {
2229
+ // Preserve the authoritative storage or security failure.
2230
+ }
2231
+ const storageCode = findStorageFailureCode(cause);
2232
+ if (!storageCode) throw cause;
2233
+ throw storageUnavailableError({
2234
+ phase: "open",
2235
+ storageCode,
2236
+ commitState: "unknown",
2237
+ });
1890
2238
  }
1891
2239
  }
1892
2240
  return { db, target };
@@ -1913,6 +2261,18 @@ export class SchedulerStore {
1913
2261
  getOccurrenceByKey: db.prepare(
1914
2262
  "SELECT * FROM occurrences WHERE idempotency_key = ?",
1915
2263
  ),
2264
+ getOccurrenceControl: db.prepare(
2265
+ "SELECT * FROM scheduler_occurrence_controls WHERE occurrence_id = ?",
2266
+ ),
2267
+ getOccurrenceControlByPauseRequest: db.prepare(
2268
+ "SELECT * FROM scheduler_occurrence_controls WHERE pause_request_id = ?",
2269
+ ),
2270
+ getOccurrenceControlByResumeRequest: db.prepare(
2271
+ "SELECT * FROM scheduler_occurrence_controls WHERE resume_request_id = ?",
2272
+ ),
2273
+ getOccurrenceRetryByRequest: db.prepare(
2274
+ "SELECT * FROM scheduler_occurrence_retries WHERE request_id = ?",
2275
+ ),
1916
2276
  listOccurrencesByTrigger: db.prepare(
1917
2277
  `SELECT * FROM occurrences
1918
2278
  WHERE job_id = ? AND trigger_key = ?
@@ -1954,8 +2314,46 @@ export class SchedulerStore {
1954
2314
 
1955
2315
  _write(callback) {
1956
2316
  this._assertOpen();
1957
- const transaction = this.db.transaction(callback);
1958
- return transaction.immediate();
2317
+ let bodyState = "not_started";
2318
+ let bodyObservedTransaction = false;
2319
+ try {
2320
+ const transaction = this.db.transaction((...args) => {
2321
+ bodyState = "running";
2322
+ try {
2323
+ bodyObservedTransaction = this.db.inTransaction === true;
2324
+ } catch {
2325
+ bodyObservedTransaction = false;
2326
+ }
2327
+ const result = callback(...args);
2328
+ bodyState = "returned";
2329
+ return result;
2330
+ });
2331
+ return transaction.immediate();
2332
+ } catch (cause) {
2333
+ if (cause instanceof SchedulerKernelError) throw cause;
2334
+ const storageCode = findStorageFailureCode(cause);
2335
+ if (!storageCode) throw cause;
2336
+ let transactionClosed = false;
2337
+ try {
2338
+ transactionClosed = this.db.inTransaction === false;
2339
+ } catch {
2340
+ transactionClosed = false;
2341
+ }
2342
+ // Once the body returns, the wrapper may already be in COMMIT and an I/O
2343
+ // failure cannot distinguish an applied commit from an aborted one. A
2344
+ // body failure is only safe to call not-committed when we observed the
2345
+ // native transaction and can also observe that its rollback closed it.
2346
+ throw storageUnavailableError({
2347
+ phase: "write",
2348
+ storageCode,
2349
+ commitState:
2350
+ bodyState === "running" &&
2351
+ bodyObservedTransaction &&
2352
+ transactionClosed
2353
+ ? "not_committed"
2354
+ : "unknown",
2355
+ });
2356
+ }
1959
2357
  }
1960
2358
 
1961
2359
  _appendEvent({
@@ -3468,6 +3866,576 @@ export class SchedulerStore {
3468
3866
  );
3469
3867
  }
3470
3868
 
3869
+ getOccurrenceControl(occurrenceId) {
3870
+ this._assertOpen();
3871
+ return mapOccurrenceControl(
3872
+ this.statements.getOccurrenceControl.get(
3873
+ normalizeIdentifier(occurrenceId, "occurrenceId"),
3874
+ ),
3875
+ );
3876
+ }
3877
+
3878
+ /**
3879
+ * Enumerate the small, public runtime-control surface used by Automation
3880
+ * Center. This intentionally does not reuse mapOccurrence(): payload,
3881
+ * authority, lease ownership, checkpoints, results and error bodies never
3882
+ * cross this read boundary.
3883
+ */
3884
+ listRuntimeControlOccurrences({
3885
+ statuses = RUNTIME_CONTROL_OCCURRENCE_STATUSES,
3886
+ jobKinds = RUNTIME_CONTROL_JOB_KINDS,
3887
+ limit = 50,
3888
+ } = {}) {
3889
+ this._assertOpen();
3890
+ const selectedStatuses = normalizeRuntimeControlEnumeration(
3891
+ statuses,
3892
+ RUNTIME_CONTROL_OCCURRENCE_STATUS_SET,
3893
+ "statuses",
3894
+ );
3895
+ const selectedJobKinds = normalizeRuntimeControlEnumeration(
3896
+ jobKinds,
3897
+ RUNTIME_CONTROL_JOB_KIND_SET,
3898
+ "jobKinds",
3899
+ );
3900
+ if (!Number.isSafeInteger(limit) || limit < 1) {
3901
+ throw invalidArgument("limit must be a positive integer");
3902
+ }
3903
+ const boundedLimit = Math.min(limit, MAX_RUNTIME_CONTROL_OCCURRENCES);
3904
+ const statusParameters = selectedStatuses.map(() => "?").join(", ");
3905
+ const kindParameters = selectedJobKinds.map(() => "?").join(", ");
3906
+ const rows = this.db
3907
+ .prepare(
3908
+ `SELECT o.occurrence_id, o.job_id, j.kind AS job_kind,
3909
+ CASE
3910
+ WHEN c.state = 'pause_requested' THEN 'pause_requested'
3911
+ WHEN c.state = 'paused' THEN 'paused'
3912
+ ELSE 'running'
3913
+ END AS runtime_status,
3914
+ o.status AS occurrence_status, o.scheduled_for, o.attempt,
3915
+ o.max_attempts, o.fence, o.created_at, o.updated_at,
3916
+ c.state AS control_state, c.revision AS control_revision,
3917
+ c.expected_fence AS control_expected_fence,
3918
+ c.capability_digest AS control_capability_digest,
3919
+ c.requested_at AS control_requested_at,
3920
+ c.paused_at AS control_paused_at,
3921
+ c.updated_at AS control_updated_at
3922
+ FROM occurrences o
3923
+ JOIN jobs j ON j.job_id = o.job_id
3924
+ LEFT JOIN scheduler_occurrence_controls c
3925
+ ON c.occurrence_id = o.occurrence_id
3926
+ WHERE j.kind IN (${kindParameters})
3927
+ AND (
3928
+ (o.status = 'running' AND c.occurrence_id IS NULL)
3929
+ OR (o.status = 'running' AND c.state = 'pause_requested')
3930
+ OR (o.status = 'running' AND c.state = 'resumed')
3931
+ OR (o.status = 'retry_wait' AND c.state = 'paused')
3932
+ )
3933
+ AND CASE
3934
+ WHEN c.state = 'pause_requested' THEN 'pause_requested'
3935
+ WHEN c.state = 'paused' THEN 'paused'
3936
+ ELSE 'running'
3937
+ END IN (${statusParameters})
3938
+ ORDER BY o.updated_at DESC, o.occurrence_id ASC
3939
+ LIMIT ?`,
3940
+ )
3941
+ .all(...selectedJobKinds, ...selectedStatuses, boundedLimit);
3942
+ return rows.map(mapRuntimeControlOccurrence);
3943
+ }
3944
+
3945
+ requestOccurrencePause({
3946
+ occurrenceId,
3947
+ expectedFence,
3948
+ expectedRevision,
3949
+ requestId,
3950
+ capability,
3951
+ } = {}) {
3952
+ const id = normalizeIdentifier(occurrenceId, "occurrenceId");
3953
+ const fence = assertPositiveSafeInteger(expectedFence, "expectedFence");
3954
+ const request = normalizeIdentifier(requestId, "requestId", {
3955
+ maxLength: 128,
3956
+ });
3957
+ const revision =
3958
+ expectedRevision === undefined
3959
+ ? null
3960
+ : (() => {
3961
+ if (
3962
+ !Number.isSafeInteger(expectedRevision) ||
3963
+ expectedRevision < 0
3964
+ ) {
3965
+ throw invalidArgument(
3966
+ "expectedRevision must be a non-negative integer",
3967
+ );
3968
+ }
3969
+ return expectedRevision;
3970
+ })();
3971
+ const normalizedCapability = normalizeRuntimeControlCapability(capability);
3972
+ if (
3973
+ normalizedCapability.pauseResume !== RUNTIME_PAUSE_RESUME.CHECKPOINT_V1
3974
+ ) {
3975
+ throw new SchedulerKernelError(
3976
+ "SCHEDULER_PAUSE_UNSUPPORTED",
3977
+ "Scheduler adapter does not declare durable checkpoint pause/resume",
3978
+ );
3979
+ }
3980
+ const capabilityJson = canonicalJson(
3981
+ normalizedCapability,
3982
+ "runtimeControl.capability",
3983
+ );
3984
+ const capabilityDigest =
3985
+ schedulerRuntimeControlCapabilityDigest(normalizedCapability);
3986
+ const now = this._now();
3987
+ return this._write(() => {
3988
+ const occurrence = this.statements.getOccurrence.get(id);
3989
+ if (!occurrence) {
3990
+ throw new SchedulerKernelError(
3991
+ "SCHEDULER_NOT_FOUND",
3992
+ `Scheduler occurrence does not exist: ${id}`,
3993
+ );
3994
+ }
3995
+ const requestOwner =
3996
+ this.statements.getOccurrenceControlByPauseRequest.get(request);
3997
+ if (requestOwner && requestOwner.occurrence_id !== id) {
3998
+ throw new SchedulerKernelError(
3999
+ "SCHEDULER_OCCURRENCE_CONTROL_CONFLICT",
4000
+ "Scheduler pause request is already bound to another occurrence",
4001
+ );
4002
+ }
4003
+ const current = this.statements.getOccurrenceControl.get(id);
4004
+ if (current?.pause_request_id === request) {
4005
+ if (
4006
+ current.expected_fence !== fence ||
4007
+ current.capability_digest !== capabilityDigest
4008
+ ) {
4009
+ throw new SchedulerKernelError(
4010
+ "SCHEDULER_OCCURRENCE_CONTROL_CONFLICT",
4011
+ `Scheduler pause request evidence changed: ${id}`,
4012
+ );
4013
+ }
4014
+ return { ...mapOccurrenceControl(current), deduplicated: true };
4015
+ }
4016
+ const actualRevision = current?.revision ?? 0;
4017
+ if (revision !== null && actualRevision !== revision) {
4018
+ throw new SchedulerKernelError(
4019
+ "SCHEDULER_REVISION_CONFLICT",
4020
+ `Scheduler occurrence control revision changed: ${id}`,
4021
+ { expectedRevision: revision, actualRevision },
4022
+ );
4023
+ }
4024
+ if (current && current.state !== "terminal") {
4025
+ throw new SchedulerKernelError(
4026
+ "SCHEDULER_OCCURRENCE_CONTROL_CONFLICT",
4027
+ `Scheduler occurrence already has an active runtime control: ${id}`,
4028
+ { state: current.state, revision: current.revision },
4029
+ );
4030
+ }
4031
+ if (occurrence.status !== OCCURRENCE_STATUS.RUNNING) {
4032
+ throw new SchedulerKernelError(
4033
+ "SCHEDULER_OCCURRENCE_NOT_RUNNING",
4034
+ `Scheduler occurrence is not running: ${id}`,
4035
+ { status: occurrence.status },
4036
+ );
4037
+ }
4038
+ if (occurrence.fence !== fence) {
4039
+ throw new SchedulerKernelError(
4040
+ "SCHEDULER_FENCE_CONFLICT",
4041
+ `Scheduler occurrence fence changed: ${id}`,
4042
+ { expectedFence: fence, actualFence: occurrence.fence },
4043
+ );
4044
+ }
4045
+ if (occurrence.lease_expires_at <= now) throw leaseLost(id);
4046
+ if (current) {
4047
+ this.db
4048
+ .prepare(
4049
+ `UPDATE scheduler_occurrence_controls
4050
+ SET capability_json = @capabilityJson,
4051
+ capability_digest = @capabilityDigest,
4052
+ state = 'pause_requested', revision = revision + 1,
4053
+ pause_request_id = @requestId, resume_request_id = NULL,
4054
+ expected_fence = @expectedFence, checkpoint_json = NULL,
4055
+ requested_at = @now, paused_at = NULL, resumed_at = NULL,
4056
+ terminal_at = NULL, updated_at = @now
4057
+ WHERE occurrence_id = @occurrenceId AND state = 'terminal'`,
4058
+ )
4059
+ .run({
4060
+ occurrenceId: id,
4061
+ capabilityJson,
4062
+ capabilityDigest,
4063
+ requestId: request,
4064
+ expectedFence: fence,
4065
+ now,
4066
+ });
4067
+ } else {
4068
+ this.db
4069
+ .prepare(
4070
+ `INSERT INTO scheduler_occurrence_controls
4071
+ (occurrence_id, capability_json, capability_digest, state,
4072
+ revision, pause_request_id, resume_request_id, expected_fence,
4073
+ checkpoint_json, requested_at, paused_at, resumed_at,
4074
+ terminal_at, updated_at)
4075
+ VALUES (@occurrenceId, @capabilityJson, @capabilityDigest,
4076
+ 'pause_requested', 1, @requestId, NULL, @expectedFence,
4077
+ NULL, @now, NULL, NULL, NULL, @now)`,
4078
+ )
4079
+ .run({
4080
+ occurrenceId: id,
4081
+ capabilityJson,
4082
+ capabilityDigest,
4083
+ requestId: request,
4084
+ expectedFence: fence,
4085
+ now,
4086
+ });
4087
+ }
4088
+ const control = this.statements.getOccurrenceControl.get(id);
4089
+ this._appendEvent({
4090
+ jobId: occurrence.job_id,
4091
+ occurrenceId: id,
4092
+ type: "occurrence_pause_requested",
4093
+ occurredAt: now,
4094
+ fence,
4095
+ data: {
4096
+ requestId: request,
4097
+ controlRevision: control.revision,
4098
+ capabilityDigest,
4099
+ },
4100
+ });
4101
+ return { ...mapOccurrenceControl(control), deduplicated: false };
4102
+ });
4103
+ }
4104
+
4105
+ ackOccurrencePause({
4106
+ occurrenceId,
4107
+ ownerId,
4108
+ fence,
4109
+ requestId,
4110
+ expectedRevision,
4111
+ safePoint,
4112
+ checkpoint,
4113
+ } = {}) {
4114
+ const id = normalizeIdentifier(occurrenceId, "occurrenceId");
4115
+ const owner = normalizeIdentifier(ownerId, "ownerId");
4116
+ const token = assertPositiveSafeInteger(fence, "fence");
4117
+ const request = normalizeIdentifier(requestId, "requestId", {
4118
+ maxLength: 128,
4119
+ });
4120
+ const revision = assertPositiveSafeInteger(
4121
+ expectedRevision,
4122
+ "expectedRevision",
4123
+ );
4124
+ const point = normalizeIdentifier(safePoint, "safePoint", {
4125
+ maxLength: 64,
4126
+ });
4127
+ if (!Object.values(RUNTIME_CONTROL_SAFE_POINTS).includes(point)) {
4128
+ throw invalidArgument("safePoint is not a scheduler runtime safe point");
4129
+ }
4130
+ const normalizedCheckpoint = normalizeRuntimeCheckpoint({
4131
+ schemaVersion: 1,
4132
+ safePoint: point,
4133
+ data: checkpoint ?? null,
4134
+ });
4135
+ const now = this._now();
4136
+ return this._write(() => {
4137
+ const occurrence = this.statements.getOccurrence.get(id);
4138
+ const controlRow = this.statements.getOccurrenceControl.get(id);
4139
+ if (!occurrence || !controlRow) {
4140
+ throw new SchedulerKernelError(
4141
+ "SCHEDULER_NOT_FOUND",
4142
+ `Scheduler occurrence control does not exist: ${id}`,
4143
+ );
4144
+ }
4145
+ const control = mapOccurrenceControl(controlRow);
4146
+ if (
4147
+ control.state !== "pause_requested" ||
4148
+ control.pauseRequestId !== request
4149
+ ) {
4150
+ throw new SchedulerKernelError(
4151
+ "SCHEDULER_OCCURRENCE_CONTROL_CONFLICT",
4152
+ `Scheduler pause request is no longer pending: ${id}`,
4153
+ { state: control.state, revision: control.revision },
4154
+ );
4155
+ }
4156
+ if (control.revision !== revision) {
4157
+ throw new SchedulerKernelError(
4158
+ "SCHEDULER_REVISION_CONFLICT",
4159
+ `Scheduler occurrence control revision changed: ${id}`,
4160
+ { expectedRevision: revision, actualRevision: control.revision },
4161
+ );
4162
+ }
4163
+ if (
4164
+ control.expectedFence !== token ||
4165
+ occurrence.status !== OCCURRENCE_STATUS.RUNNING ||
4166
+ occurrence.fence !== token ||
4167
+ occurrence.lease_owner !== owner ||
4168
+ occurrence.lease_expires_at <= now
4169
+ ) {
4170
+ throw leaseLost(id);
4171
+ }
4172
+ if (!control.capability.safePoints.includes(point)) {
4173
+ throw new SchedulerKernelError(
4174
+ "SCHEDULER_PAUSE_UNSUPPORTED_SAFE_POINT",
4175
+ `Scheduler adapter did not declare pause safety at ${point}`,
4176
+ );
4177
+ }
4178
+ const occurrenceUpdate = this.db
4179
+ .prepare(
4180
+ `UPDATE occurrences
4181
+ SET status = 'retry_wait', available_at = @now,
4182
+ lease_owner = NULL, lease_expires_at = NULL,
4183
+ updated_at = @now, settled_at = NULL
4184
+ WHERE occurrence_id = @occurrenceId AND status = 'running'
4185
+ AND lease_owner = @ownerId AND fence = @fence
4186
+ AND lease_expires_at > @now`,
4187
+ )
4188
+ .run({ occurrenceId: id, ownerId: owner, fence: token, now });
4189
+ if (occurrenceUpdate.changes !== 1) throw leaseLost(id);
4190
+ const controlUpdate = this.db
4191
+ .prepare(
4192
+ `UPDATE scheduler_occurrence_controls
4193
+ SET state = 'paused', revision = revision + 1,
4194
+ checkpoint_json = @checkpointJson,
4195
+ paused_at = @now, updated_at = @now
4196
+ WHERE occurrence_id = @occurrenceId
4197
+ AND state = 'pause_requested' AND revision = @revision
4198
+ AND pause_request_id = @requestId AND expected_fence = @fence`,
4199
+ )
4200
+ .run({
4201
+ occurrenceId: id,
4202
+ checkpointJson: normalizedCheckpoint.encoded,
4203
+ now,
4204
+ revision,
4205
+ requestId: request,
4206
+ fence: token,
4207
+ });
4208
+ if (controlUpdate.changes !== 1) {
4209
+ throw new SchedulerKernelError(
4210
+ "SCHEDULER_OCCURRENCE_CONTROL_CONFLICT",
4211
+ `Scheduler pause acknowledgement lost its CAS: ${id}`,
4212
+ );
4213
+ }
4214
+ this._appendEvent({
4215
+ jobId: occurrence.job_id,
4216
+ occurrenceId: id,
4217
+ type: "occurrence_paused",
4218
+ occurredAt: now,
4219
+ fence: token,
4220
+ data: {
4221
+ requestId: request,
4222
+ safePoint: point,
4223
+ checkpointDigest: sha256PayloadDigest(
4224
+ normalizedCheckpoint.checkpoint,
4225
+ "runtimeControl.checkpoint",
4226
+ ),
4227
+ },
4228
+ });
4229
+ return {
4230
+ control: mapOccurrenceControl(
4231
+ this.statements.getOccurrenceControl.get(id),
4232
+ ),
4233
+ occurrence: mapOccurrence(this.statements.getOccurrence.get(id)),
4234
+ };
4235
+ });
4236
+ }
4237
+
4238
+ resumeOccurrence({ occurrenceId, expectedRevision, requestId } = {}) {
4239
+ const id = normalizeIdentifier(occurrenceId, "occurrenceId");
4240
+ const revision = assertPositiveSafeInteger(
4241
+ expectedRevision,
4242
+ "expectedRevision",
4243
+ );
4244
+ const request = normalizeIdentifier(requestId, "requestId", {
4245
+ maxLength: 128,
4246
+ });
4247
+ const now = this._now();
4248
+ return this._write(() => {
4249
+ const occurrence = this.statements.getOccurrence.get(id);
4250
+ const controlRow = this.statements.getOccurrenceControl.get(id);
4251
+ if (!occurrence || !controlRow) {
4252
+ throw new SchedulerKernelError(
4253
+ "SCHEDULER_NOT_FOUND",
4254
+ `Scheduler occurrence control does not exist: ${id}`,
4255
+ );
4256
+ }
4257
+ const requestOwner =
4258
+ this.statements.getOccurrenceControlByResumeRequest.get(request);
4259
+ if (requestOwner && requestOwner.occurrence_id !== id) {
4260
+ throw new SchedulerKernelError(
4261
+ "SCHEDULER_OCCURRENCE_CONTROL_CONFLICT",
4262
+ "Scheduler resume request is already bound to another occurrence",
4263
+ );
4264
+ }
4265
+ const control = mapOccurrenceControl(controlRow);
4266
+ if (control.resumeRequestId === request) {
4267
+ return {
4268
+ control: { ...control, deduplicated: true },
4269
+ occurrence: mapOccurrence(occurrence),
4270
+ };
4271
+ }
4272
+ if (control.state !== "paused") {
4273
+ throw new SchedulerKernelError(
4274
+ "SCHEDULER_OCCURRENCE_CONTROL_CONFLICT",
4275
+ `Scheduler occurrence is not paused: ${id}`,
4276
+ { state: control.state, revision: control.revision },
4277
+ );
4278
+ }
4279
+ if (control.revision !== revision) {
4280
+ throw new SchedulerKernelError(
4281
+ "SCHEDULER_REVISION_CONFLICT",
4282
+ `Scheduler occurrence control revision changed: ${id}`,
4283
+ { expectedRevision: revision, actualRevision: control.revision },
4284
+ );
4285
+ }
4286
+ if (
4287
+ occurrence.status !== OCCURRENCE_STATUS.RETRY_WAIT ||
4288
+ occurrence.lease_owner !== null ||
4289
+ occurrence.lease_expires_at !== null
4290
+ ) {
4291
+ throw new SchedulerKernelError(
4292
+ "SCHEDULER_OCCURRENCE_CONTROL_CONFLICT",
4293
+ `Paused occurrence state is inconsistent: ${id}`,
4294
+ );
4295
+ }
4296
+ const update = this.db
4297
+ .prepare(
4298
+ `UPDATE scheduler_occurrence_controls
4299
+ SET state = 'resumed', revision = revision + 1,
4300
+ resume_request_id = @requestId, resumed_at = @now,
4301
+ updated_at = @now
4302
+ WHERE occurrence_id = @occurrenceId AND state = 'paused'
4303
+ AND revision = @revision`,
4304
+ )
4305
+ .run({ occurrenceId: id, requestId: request, now, revision });
4306
+ if (update.changes !== 1) {
4307
+ throw new SchedulerKernelError(
4308
+ "SCHEDULER_OCCURRENCE_CONTROL_CONFLICT",
4309
+ `Scheduler resume request lost its CAS: ${id}`,
4310
+ );
4311
+ }
4312
+ this.db
4313
+ .prepare(
4314
+ `UPDATE occurrences SET available_at = ?, updated_at = ?
4315
+ WHERE occurrence_id = ? AND status = 'retry_wait'`,
4316
+ )
4317
+ .run(now, now, id);
4318
+ this._appendEvent({
4319
+ jobId: occurrence.job_id,
4320
+ occurrenceId: id,
4321
+ type: "occurrence_resume_requested",
4322
+ occurredAt: now,
4323
+ fence: occurrence.fence,
4324
+ data: { requestId: request, controlRevision: revision + 1 },
4325
+ });
4326
+ return {
4327
+ control: mapOccurrenceControl(
4328
+ this.statements.getOccurrenceControl.get(id),
4329
+ ),
4330
+ occurrence: mapOccurrence(this.statements.getOccurrence.get(id)),
4331
+ };
4332
+ });
4333
+ }
4334
+
4335
+ requeueDeadLetter({
4336
+ occurrenceId,
4337
+ expectedFence,
4338
+ expectedErrorCode,
4339
+ requestId,
4340
+ } = {}) {
4341
+ const id = normalizeIdentifier(occurrenceId, "occurrenceId");
4342
+ const fence = assertPositiveSafeInteger(expectedFence, "expectedFence");
4343
+ const errorCode = normalizeIdentifier(
4344
+ expectedErrorCode,
4345
+ "expectedErrorCode",
4346
+ {
4347
+ maxLength: 128,
4348
+ },
4349
+ );
4350
+ const request = normalizeIdentifier(requestId, "requestId", {
4351
+ maxLength: 128,
4352
+ });
4353
+ const now = this._now();
4354
+ return this._write(() => {
4355
+ const existing = this.statements.getOccurrenceRetryByRequest.get(request);
4356
+ if (existing) {
4357
+ if (
4358
+ existing.occurrence_id !== id ||
4359
+ existing.expected_fence !== fence ||
4360
+ existing.expected_error_code !== errorCode
4361
+ ) {
4362
+ throw new SchedulerKernelError(
4363
+ "SCHEDULER_REQUEUE_CONFLICT",
4364
+ "Scheduler retry request evidence changed",
4365
+ );
4366
+ }
4367
+ return {
4368
+ occurrence: mapOccurrence(this.statements.getOccurrence.get(id)),
4369
+ requestId: request,
4370
+ deduplicated: true,
4371
+ };
4372
+ }
4373
+ const occurrence = this.statements.getOccurrence.get(id);
4374
+ if (!occurrence) {
4375
+ throw new SchedulerKernelError(
4376
+ "SCHEDULER_NOT_FOUND",
4377
+ `Scheduler occurrence does not exist: ${id}`,
4378
+ );
4379
+ }
4380
+ const lastError =
4381
+ occurrence.last_error_json === null
4382
+ ? null
4383
+ : readStoredJson(occurrence.last_error_json, "occurrence error");
4384
+ if (
4385
+ occurrence.status !== OCCURRENCE_STATUS.DEAD_LETTER ||
4386
+ occurrence.fence !== fence ||
4387
+ lastError?.code !== errorCode
4388
+ ) {
4389
+ throw new SchedulerKernelError(
4390
+ "SCHEDULER_REQUEUE_EVIDENCE_CONFLICT",
4391
+ `Scheduler dead-letter evidence changed: ${id}`,
4392
+ {
4393
+ status: occurrence.status,
4394
+ expectedFence: fence,
4395
+ actualFence: occurrence.fence,
4396
+ expectedErrorCode: errorCode,
4397
+ actualErrorCode: lastError?.code ?? null,
4398
+ },
4399
+ );
4400
+ }
4401
+ this.db
4402
+ .prepare(
4403
+ `INSERT INTO scheduler_occurrence_retries
4404
+ (occurrence_id, request_id, expected_fence,
4405
+ expected_error_code, created_at, claimed_at)
4406
+ VALUES (?, ?, ?, ?, ?, NULL)`,
4407
+ )
4408
+ .run(id, request, fence, errorCode, now);
4409
+ const update = this.db
4410
+ .prepare(
4411
+ `UPDATE occurrences
4412
+ SET status = 'retry_wait', available_at = ?, updated_at = ?,
4413
+ settled_at = NULL
4414
+ WHERE occurrence_id = ? AND status = 'dead_letter' AND fence = ?`,
4415
+ )
4416
+ .run(now, now, id, fence);
4417
+ if (update.changes !== 1) {
4418
+ throw new SchedulerKernelError(
4419
+ "SCHEDULER_REQUEUE_CONFLICT",
4420
+ `Scheduler dead letter changed during requeue: ${id}`,
4421
+ );
4422
+ }
4423
+ this._appendEvent({
4424
+ jobId: occurrence.job_id,
4425
+ occurrenceId: id,
4426
+ type: "occurrence_requeued",
4427
+ occurredAt: now,
4428
+ fence,
4429
+ data: { requestId: request, expectedErrorCode: errorCode },
4430
+ });
4431
+ return {
4432
+ occurrence: mapOccurrence(this.statements.getOccurrence.get(id)),
4433
+ requestId: request,
4434
+ deduplicated: false,
4435
+ };
4436
+ });
4437
+ }
4438
+
3471
4439
  listOccurrencesByTrigger({ jobId, triggerKey, limit = 2 } = {}) {
3472
4440
  this._assertOpen();
3473
4441
  const id = normalizeIdentifier(jobId, "jobId");
@@ -3490,6 +4458,11 @@ export class SchedulerStore {
3490
4458
  WHERE status = 'running'
3491
4459
  AND lease_expires_at <= ?
3492
4460
  AND attempt >= max_attempts
4461
+ AND NOT EXISTS (
4462
+ SELECT 1 FROM scheduler_occurrence_controls c
4463
+ WHERE c.occurrence_id = occurrences.occurrence_id
4464
+ AND c.state IN ('pause_requested', 'resumed')
4465
+ )
3493
4466
  ORDER BY lease_expires_at, occurrence_id
3494
4467
  LIMIT 100
3495
4468
  `,
@@ -3509,6 +4482,11 @@ export class SchedulerStore {
3509
4482
  AND fence = @fence
3510
4483
  AND lease_expires_at <= @now
3511
4484
  AND attempt >= max_attempts
4485
+ AND NOT EXISTS (
4486
+ SELECT 1 FROM scheduler_occurrence_controls c
4487
+ WHERE c.occurrence_id = occurrences.occurrence_id
4488
+ AND c.state IN ('pause_requested', 'resumed')
4489
+ )
3512
4490
  `);
3513
4491
  for (const row of expired) {
3514
4492
  const error = {
@@ -3590,26 +4568,43 @@ export class SchedulerStore {
3590
4568
  workspaceId === undefined
3591
4569
  ? null
3592
4570
  : normalizeIdentifier(workspaceId, "workspaceId", { maxLength: 256 });
3593
- const now = this._now();
3594
- const leaseExpiresAt = now + lease;
3595
- if (!Number.isSafeInteger(leaseExpiresAt)) {
3596
- throw invalidArgument("lease expiry exceeds the safe integer range");
3597
- }
3598
4571
  return this._write(() => {
4572
+ const now = this._now();
4573
+ const leaseExpiresAt = now + lease;
4574
+ if (!Number.isSafeInteger(leaseExpiresAt)) {
4575
+ throw invalidArgument("lease expiry exceeds the safe integer range");
4576
+ }
3599
4577
  this._deadLetterExpiredLeases(now);
3600
4578
  const candidate = this.db
3601
4579
  .prepare(
3602
4580
  `
3603
- SELECT o.*
4581
+ SELECT o.*, c.state AS control_state,
4582
+ c.expected_fence AS control_expected_fence,
4583
+ r.request_id AS retry_request_id
3604
4584
  FROM occurrences o
3605
4585
  JOIN jobs j ON j.job_id = o.job_id
4586
+ LEFT JOIN scheduler_occurrence_controls c
4587
+ ON c.occurrence_id = o.occurrence_id
4588
+ LEFT JOIN scheduler_occurrence_retries r
4589
+ ON r.occurrence_id = o.occurrence_id
4590
+ AND r.claimed_at IS NULL
4591
+ AND r.expected_fence = o.fence
3606
4592
  WHERE j.enabled = 1
3607
4593
  AND (@jobKind IS NULL OR j.kind = @jobKind)
3608
4594
  AND (
3609
4595
  @workspaceId IS NULL
3610
4596
  OR json_extract(j.authority_json, '$.workspaceId') = @workspaceId
3611
4597
  )
3612
- AND o.attempt < o.max_attempts
4598
+ AND (
4599
+ o.attempt < o.max_attempts
4600
+ OR c.state IN ('pause_requested', 'resumed')
4601
+ OR r.request_id IS NOT NULL
4602
+ )
4603
+ AND (c.state IS NULL OR c.state <> 'paused')
4604
+ AND (
4605
+ c.state IS NULL OR c.state <> 'pause_requested'
4606
+ OR o.status = 'running'
4607
+ )
3613
4608
  AND (
3614
4609
  (o.status IN ('queued', 'retry_wait') AND o.available_at <= @now)
3615
4610
  OR
@@ -3631,7 +4626,25 @@ export class SchedulerStore {
3631
4626
  `
3632
4627
  UPDATE occurrences
3633
4628
  SET status = 'running',
3634
- attempt = attempt + 1,
4629
+ attempt = attempt + CASE
4630
+ WHEN EXISTS (
4631
+ SELECT 1 FROM scheduler_occurrence_controls c
4632
+ WHERE c.occurrence_id = occurrences.occurrence_id
4633
+ AND c.state = 'pause_requested'
4634
+ ) THEN 0
4635
+ WHEN EXISTS (
4636
+ SELECT 1 FROM scheduler_occurrence_controls c
4637
+ WHERE c.occurrence_id = occurrences.occurrence_id
4638
+ AND c.state = 'resumed'
4639
+ AND c.expected_fence = occurrences.fence
4640
+ ) THEN 0
4641
+ WHEN EXISTS (
4642
+ SELECT 1 FROM scheduler_occurrence_retries r
4643
+ WHERE r.occurrence_id = occurrences.occurrence_id
4644
+ AND r.claimed_at IS NULL
4645
+ AND r.expected_fence = occurrences.fence
4646
+ ) THEN 0
4647
+ ELSE 1 END,
3635
4648
  fence = fence + 1,
3636
4649
  lease_owner = @ownerId,
3637
4650
  lease_expires_at = @leaseExpiresAt,
@@ -3639,7 +4652,25 @@ export class SchedulerStore {
3639
4652
  settled_at = NULL
3640
4653
  WHERE occurrence_id = @occurrenceId
3641
4654
  AND fence = @expectedFence
3642
- AND attempt < max_attempts
4655
+ AND (
4656
+ attempt < max_attempts
4657
+ OR EXISTS (
4658
+ SELECT 1 FROM scheduler_occurrence_controls c
4659
+ WHERE c.occurrence_id = occurrences.occurrence_id
4660
+ AND c.state IN ('pause_requested', 'resumed')
4661
+ )
4662
+ OR EXISTS (
4663
+ SELECT 1 FROM scheduler_occurrence_retries r
4664
+ WHERE r.occurrence_id = occurrences.occurrence_id
4665
+ AND r.claimed_at IS NULL
4666
+ AND r.expected_fence = occurrences.fence
4667
+ )
4668
+ )
4669
+ AND NOT EXISTS (
4670
+ SELECT 1 FROM scheduler_occurrence_controls c
4671
+ WHERE c.occurrence_id = occurrences.occurrence_id
4672
+ AND c.state = 'paused'
4673
+ )
3643
4674
  AND (
3644
4675
  (status IN ('queued', 'retry_wait') AND available_at <= @now)
3645
4676
  OR
@@ -3663,6 +4694,49 @@ export class SchedulerStore {
3663
4694
  const claimed = this.statements.getOccurrence.get(
3664
4695
  candidate.occurrence_id,
3665
4696
  );
4697
+ if (candidate.control_state === "pause_requested") {
4698
+ this.db
4699
+ .prepare(
4700
+ `UPDATE scheduler_occurrence_controls
4701
+ SET expected_fence = ?, revision = revision + 1, updated_at = ?
4702
+ WHERE occurrence_id = ? AND state = 'pause_requested'
4703
+ AND expected_fence = ?`,
4704
+ )
4705
+ .run(
4706
+ claimed.fence,
4707
+ now,
4708
+ candidate.occurrence_id,
4709
+ candidate.control_expected_fence,
4710
+ );
4711
+ } else if (candidate.control_state === "resumed") {
4712
+ const rebound = this.db
4713
+ .prepare(
4714
+ `UPDATE scheduler_occurrence_controls
4715
+ SET expected_fence = ?, revision = revision + 1, updated_at = ?
4716
+ WHERE occurrence_id = ? AND state = 'resumed'
4717
+ AND expected_fence = ?`,
4718
+ )
4719
+ .run(
4720
+ claimed.fence,
4721
+ now,
4722
+ candidate.occurrence_id,
4723
+ candidate.control_expected_fence,
4724
+ );
4725
+ if (rebound.changes !== 1) {
4726
+ throw new SchedulerKernelError(
4727
+ "SCHEDULER_OCCURRENCE_CONTROL_CONFLICT",
4728
+ `Scheduler resume checkpoint lost its claim fence: ${candidate.occurrence_id}`,
4729
+ );
4730
+ }
4731
+ }
4732
+ if (candidate.retry_request_id !== null) {
4733
+ this.db
4734
+ .prepare(
4735
+ `UPDATE scheduler_occurrence_retries SET claimed_at = ?
4736
+ WHERE occurrence_id = ? AND request_id = ? AND claimed_at IS NULL`,
4737
+ )
4738
+ .run(now, candidate.occurrence_id, candidate.retry_request_id);
4739
+ }
3666
4740
  this._appendEvent({
3667
4741
  jobId: candidate.job_id,
3668
4742
  occurrenceId: candidate.occurrence_id,
@@ -3677,6 +4751,15 @@ export class SchedulerStore {
3677
4751
  attempt: claimed.attempt,
3678
4752
  previousOwner,
3679
4753
  previousStatus,
4754
+ ...(candidate.control_state === "pause_requested"
4755
+ ? { pauseRequestRebound: true }
4756
+ : {}),
4757
+ ...(candidate.control_state === "resumed"
4758
+ ? { resumedWithoutAttempt: true }
4759
+ : {}),
4760
+ ...(candidate.retry_request_id === null
4761
+ ? {}
4762
+ : { retryRequestId: candidate.retry_request_id }),
3680
4763
  },
3681
4764
  });
3682
4765
  return mapOccurrence(claimed);
@@ -3696,12 +4779,12 @@ export class SchedulerStore {
3696
4779
  const id = normalizeIdentifier(occurrenceId, "occurrenceId");
3697
4780
  const owner = normalizeIdentifier(ownerId, "ownerId");
3698
4781
  const lease = normalizeLeaseMs(leaseMs);
3699
- const now = this._now();
3700
- const leaseExpiresAt = now + lease;
3701
- if (!Number.isSafeInteger(leaseExpiresAt)) {
3702
- throw invalidArgument("lease expiry exceeds the safe integer range");
3703
- }
3704
4782
  return this._write(() => {
4783
+ const now = this._now();
4784
+ const leaseExpiresAt = now + lease;
4785
+ if (!Number.isSafeInteger(leaseExpiresAt)) {
4786
+ throw invalidArgument("lease expiry exceeds the safe integer range");
4787
+ }
3705
4788
  this._deadLetterExpiredLeases(now);
3706
4789
  const candidate = this.statements.getOccurrence.get(id);
3707
4790
  if (!candidate) {
@@ -3711,9 +4794,24 @@ export class SchedulerStore {
3711
4794
  );
3712
4795
  }
3713
4796
  const job = this.statements.getJob.get(candidate.job_id);
4797
+ const controlRow = this.statements.getOccurrenceControl.get(id);
4798
+ const retryRow = this.db
4799
+ .prepare(
4800
+ `SELECT * FROM scheduler_occurrence_retries
4801
+ WHERE occurrence_id = ? AND claimed_at IS NULL
4802
+ AND expected_fence = ? ORDER BY created_at, request_id LIMIT 1`,
4803
+ )
4804
+ .get(id, candidate.fence);
3714
4805
  const eligible =
3715
4806
  job?.enabled === 1 &&
3716
- candidate.attempt < candidate.max_attempts &&
4807
+ (candidate.attempt < candidate.max_attempts ||
4808
+ ["pause_requested", "resumed"].includes(controlRow?.state) ||
4809
+ retryRow !== undefined) &&
4810
+ controlRow?.state !== "paused" &&
4811
+ !(
4812
+ controlRow?.state === "pause_requested" &&
4813
+ candidate.status !== OCCURRENCE_STATUS.RUNNING
4814
+ ) &&
3717
4815
  (([OCCURRENCE_STATUS.QUEUED, OCCURRENCE_STATUS.RETRY_WAIT].includes(
3718
4816
  candidate.status,
3719
4817
  ) &&
@@ -3729,7 +4827,25 @@ export class SchedulerStore {
3729
4827
  `
3730
4828
  UPDATE occurrences
3731
4829
  SET status = 'running',
3732
- attempt = attempt + 1,
4830
+ attempt = attempt + CASE
4831
+ WHEN EXISTS (
4832
+ SELECT 1 FROM scheduler_occurrence_controls c
4833
+ WHERE c.occurrence_id = occurrences.occurrence_id
4834
+ AND c.state = 'pause_requested'
4835
+ ) THEN 0
4836
+ WHEN EXISTS (
4837
+ SELECT 1 FROM scheduler_occurrence_controls c
4838
+ WHERE c.occurrence_id = occurrences.occurrence_id
4839
+ AND c.state = 'resumed'
4840
+ AND c.expected_fence = occurrences.fence
4841
+ ) THEN 0
4842
+ WHEN EXISTS (
4843
+ SELECT 1 FROM scheduler_occurrence_retries r
4844
+ WHERE r.occurrence_id = occurrences.occurrence_id
4845
+ AND r.claimed_at IS NULL
4846
+ AND r.expected_fence = occurrences.fence
4847
+ ) THEN 0
4848
+ ELSE 1 END,
3733
4849
  fence = fence + 1,
3734
4850
  lease_owner = @ownerId,
3735
4851
  lease_expires_at = @leaseExpiresAt,
@@ -3737,7 +4853,25 @@ export class SchedulerStore {
3737
4853
  settled_at = NULL
3738
4854
  WHERE occurrence_id = @occurrenceId
3739
4855
  AND fence = @expectedFence
3740
- AND attempt < max_attempts
4856
+ AND (
4857
+ attempt < max_attempts
4858
+ OR EXISTS (
4859
+ SELECT 1 FROM scheduler_occurrence_controls c
4860
+ WHERE c.occurrence_id = occurrences.occurrence_id
4861
+ AND c.state IN ('pause_requested', 'resumed')
4862
+ )
4863
+ OR EXISTS (
4864
+ SELECT 1 FROM scheduler_occurrence_retries r
4865
+ WHERE r.occurrence_id = occurrences.occurrence_id
4866
+ AND r.claimed_at IS NULL
4867
+ AND r.expected_fence = occurrences.fence
4868
+ )
4869
+ )
4870
+ AND NOT EXISTS (
4871
+ SELECT 1 FROM scheduler_occurrence_controls c
4872
+ WHERE c.occurrence_id = occurrences.occurrence_id
4873
+ AND c.state = 'paused'
4874
+ )
3741
4875
  AND (
3742
4876
  (status IN ('queued', 'retry_wait') AND available_at <= @now)
3743
4877
  OR
@@ -3759,6 +4893,39 @@ export class SchedulerStore {
3759
4893
  );
3760
4894
  }
3761
4895
  const claimed = this.statements.getOccurrence.get(id);
4896
+ if (controlRow?.state === "pause_requested") {
4897
+ this.db
4898
+ .prepare(
4899
+ `UPDATE scheduler_occurrence_controls
4900
+ SET expected_fence = ?, revision = revision + 1, updated_at = ?
4901
+ WHERE occurrence_id = ? AND state = 'pause_requested'
4902
+ AND expected_fence = ?`,
4903
+ )
4904
+ .run(claimed.fence, now, id, controlRow.expected_fence);
4905
+ } else if (controlRow?.state === "resumed") {
4906
+ const rebound = this.db
4907
+ .prepare(
4908
+ `UPDATE scheduler_occurrence_controls
4909
+ SET expected_fence = ?, revision = revision + 1, updated_at = ?
4910
+ WHERE occurrence_id = ? AND state = 'resumed'
4911
+ AND expected_fence = ?`,
4912
+ )
4913
+ .run(claimed.fence, now, id, controlRow.expected_fence);
4914
+ if (rebound.changes !== 1) {
4915
+ throw new SchedulerKernelError(
4916
+ "SCHEDULER_OCCURRENCE_CONTROL_CONFLICT",
4917
+ `Scheduler resume checkpoint lost its claim fence: ${id}`,
4918
+ );
4919
+ }
4920
+ }
4921
+ if (retryRow) {
4922
+ this.db
4923
+ .prepare(
4924
+ `UPDATE scheduler_occurrence_retries SET claimed_at = ?
4925
+ WHERE occurrence_id = ? AND request_id = ? AND claimed_at IS NULL`,
4926
+ )
4927
+ .run(now, id, retryRow.request_id);
4928
+ }
3762
4929
  this._appendEvent({
3763
4930
  jobId: candidate.job_id,
3764
4931
  occurrenceId: id,
@@ -3774,6 +4941,13 @@ export class SchedulerStore {
3774
4941
  previousOwner,
3775
4942
  previousStatus,
3776
4943
  targeted: true,
4944
+ ...(controlRow?.state === "pause_requested"
4945
+ ? { pauseRequestRebound: true }
4946
+ : {}),
4947
+ ...(controlRow?.state === "resumed"
4948
+ ? { resumedWithoutAttempt: true }
4949
+ : {}),
4950
+ ...(retryRow ? { retryRequestId: retryRow.request_id } : {}),
3777
4951
  },
3778
4952
  });
3779
4953
  return mapOccurrence(claimed);
@@ -3785,12 +4959,12 @@ export class SchedulerStore {
3785
4959
  const owner = normalizeIdentifier(ownerId, "ownerId");
3786
4960
  const token = assertPositiveSafeInteger(fence, "fence");
3787
4961
  const lease = normalizeLeaseMs(leaseMs);
3788
- const now = this._now();
3789
- const leaseExpiresAt = now + lease;
3790
- if (!Number.isSafeInteger(leaseExpiresAt)) {
3791
- throw invalidArgument("lease expiry exceeds the safe integer range");
3792
- }
3793
4962
  return this._write(() => {
4963
+ const now = this._now();
4964
+ const leaseExpiresAt = now + lease;
4965
+ if (!Number.isSafeInteger(leaseExpiresAt)) {
4966
+ throw invalidArgument("lease expiry exceeds the safe integer range");
4967
+ }
3794
4968
  const result = this.db
3795
4969
  .prepare(
3796
4970
  `
@@ -3858,11 +5032,10 @@ export class SchedulerStore {
3858
5032
  "error",
3859
5033
  )
3860
5034
  : null;
3861
- const now = this._now();
3862
- const availableAt =
5035
+ const normalizedRetryAt =
3863
5036
  outcome !== "failed" || retryAt === undefined
3864
- ? now
3865
- : Math.max(now, normalizeEpochMs(retryAt, "retryAt"));
5037
+ ? null
5038
+ : normalizeEpochMs(retryAt, "retryAt");
3866
5039
  const adjudicationRequest =
3867
5040
  adjudicationRequestId === undefined
3868
5041
  ? null
@@ -3871,6 +5044,9 @@ export class SchedulerStore {
3871
5044
  });
3872
5045
 
3873
5046
  return this._write(() => {
5047
+ const now = this._now();
5048
+ const availableAt =
5049
+ normalizedRetryAt === null ? now : Math.max(now, normalizedRetryAt);
3874
5050
  const current = this.statements.getOccurrence.get(id);
3875
5051
  if (
3876
5052
  !current ||
@@ -3936,6 +5112,35 @@ export class SchedulerStore {
3936
5112
  settledAt,
3937
5113
  });
3938
5114
  if (update.changes !== 1) throw leaseLost(id);
5115
+ if (nextStatus === OCCURRENCE_STATUS.RETRY_WAIT) {
5116
+ this.db
5117
+ .prepare(
5118
+ `UPDATE scheduler_occurrence_controls
5119
+ SET expected_fence = @nextFence, revision = revision + 1,
5120
+ updated_at = @now
5121
+ WHERE occurrence_id = @occurrenceId
5122
+ AND state = 'resumed'
5123
+ AND expected_fence = @fence`,
5124
+ )
5125
+ .run({
5126
+ occurrenceId: id,
5127
+ fence: token,
5128
+ nextFence: token + 1,
5129
+ now,
5130
+ });
5131
+ }
5132
+ const controlTerminalized = this.db
5133
+ .prepare(
5134
+ `UPDATE scheduler_occurrence_controls
5135
+ SET state = 'terminal', revision = revision + 1,
5136
+ terminal_at = @now, updated_at = @now
5137
+ WHERE occurrence_id = @occurrenceId
5138
+ AND (
5139
+ state = 'pause_requested'
5140
+ OR (state = 'resumed' AND @status <> 'retry_wait')
5141
+ )`,
5142
+ )
5143
+ .run({ occurrenceId: id, status: nextStatus, now }).changes;
3939
5144
  const reservation = this.db
3940
5145
  .prepare(
3941
5146
  `SELECT * FROM scheduler_authority_reservations
@@ -4039,6 +5244,9 @@ export class SchedulerStore {
4039
5244
  ...(adjudication?.status === "pending"
4040
5245
  ? { adjudicationRequestId: adjudication.request_id }
4041
5246
  : {}),
5247
+ ...(controlTerminalized === 1
5248
+ ? { runtimeControlTerminalized: true }
5249
+ : {}),
4042
5250
  },
4043
5251
  });
4044
5252
  return mapOccurrence(this.statements.getOccurrence.get(id));