chainlesschain 0.163.5 → 0.163.6

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 (194) hide show
  1. package/completions/_cc +1 -1
  2. package/completions/cc.bash +1 -1
  3. package/completions/cc.fish +1 -1
  4. package/completions/cc.ps1 +1 -1
  5. package/package.json +1 -1
  6. package/src/assets/web-panel/assets/{AIOps-Cv2EE1Zy.js → AIOps-DDIUofA8.js} +1 -1
  7. package/src/assets/web-panel/assets/{ActionButton-CcpxMLuW.js → ActionButton-Bm-5xYRn.js} +1 -1
  8. package/src/assets/web-panel/assets/{Analytics-DE8mJoMI.js → Analytics-D5UH4ljI.js} +3 -3
  9. package/src/assets/web-panel/assets/{AppLayout-DKn9dk9L.js → AppLayout-D_ifjOOP.js} +4 -4
  10. package/src/assets/web-panel/assets/{Artifacts-Cp3mG3Py.js → Artifacts-BY1mXdC7.js} +1 -1
  11. package/src/assets/web-panel/assets/{Audit-zvsQfF2C.js → Audit-Bujr4Ml3.js} +1 -1
  12. package/src/assets/web-panel/assets/{BackgroundAgents-Bi85SsjN.js → BackgroundAgents-sioKSTcC.js} +1 -1
  13. package/src/assets/web-panel/assets/{Backup-Cw47Dj6s.js → Backup-DDOAK8RK.js} +1 -1
  14. package/src/assets/web-panel/assets/{BaseInput-C5Ip2afH.js → BaseInput-CtCsVVke.js} +1 -1
  15. package/src/assets/web-panel/assets/{Chat-C0iAFkJv.js → Chat-DpNj0vnd.js} +5 -5
  16. package/src/assets/web-panel/assets/{ChatBubbleRenderer-BV9Wv7wO.js → ChatBubbleRenderer-CZta3Ftr.js} +1 -1
  17. package/src/assets/web-panel/assets/{Checkbox-3KZZCiAF.js → Checkbox-DoHVNSXq.js} +1 -1
  18. package/src/assets/web-panel/assets/{Codegen-C8OMbBAz.js → Codegen-DlVl7Qmv.js} +1 -1
  19. package/src/assets/web-panel/assets/{Col-B-MB84LJ.js → Col-GIqoPYty.js} +1 -1
  20. package/src/assets/web-panel/assets/{Community-DpR7Dxao.js → Community-CIAPEVji.js} +1 -1
  21. package/src/assets/web-panel/assets/{Compact-CyDtz0OS.js → Compact-BN7JiI_h.js} +1 -1
  22. package/src/assets/web-panel/assets/{Compliance-5tiRa2An.js → Compliance-CqUWCM9B.js} +1 -1
  23. package/src/assets/web-panel/assets/{Cowork-DnQaKZ5x.js → Cowork-A4iPp7A4.js} +4 -4
  24. package/src/assets/web-panel/assets/{Cron-BaWfoEDf.js → Cron-B2zeXXWb.js} +2 -2
  25. package/src/assets/web-panel/assets/{Crosschain-Bu0HoHhb.js → Crosschain-BR2xmWr8.js} +1 -1
  26. package/src/assets/web-panel/assets/{DID-31zk8upX.js → DID-BzFDjrIv.js} +2 -2
  27. package/src/assets/web-panel/assets/{Dashboard-BmVne-e2.js → Dashboard-DKtDo3yA.js} +2 -2
  28. package/src/assets/web-panel/assets/{Dropdown-B4PkoOWd.js → Dropdown-RNx43_YG.js} +1 -1
  29. package/src/assets/web-panel/assets/{EmailListRenderer-BZ6tyFtI.js → EmailListRenderer-SwMt4Rcy.js} +1 -1
  30. package/src/assets/web-panel/assets/{FamilyGuardDashboard-Osozn4Ds.js → FamilyGuardDashboard-CsYebZ4X.js} +1 -1
  31. package/src/assets/web-panel/assets/{Federation-DaH8TOAP.js → Federation-BmRKvBTy.js} +1 -1
  32. package/src/assets/web-panel/assets/{FormItemContext-tbU0uwis.js → FormItemContext-CVxGTA-0.js} +1 -1
  33. package/src/assets/web-panel/assets/{GenericCardRenderer-Df8LQBrL.js → GenericCardRenderer-x2iXpyB7.js} +1 -1
  34. package/src/assets/web-panel/assets/{Git-DsMB6-14.js → Git-DfqbkiOP.js} +2 -2
  35. package/src/assets/web-panel/assets/{Governance-BnH0dZzd.js → Governance-RuHyyj5O.js} +1 -1
  36. package/src/assets/web-panel/assets/{Inference-DhnN1N_P.js → Inference-Bn7uZ7rI.js} +1 -1
  37. package/src/assets/web-panel/assets/{KnowledgeGraph-DnNshAgn.js → KnowledgeGraph-DlP7XefZ.js} +1 -1
  38. package/src/assets/web-panel/assets/{Logs-BmBFX3_N.js → Logs-BYNdOiCd.js} +2 -2
  39. package/src/assets/web-panel/assets/{MarkdownRenderer-DCslfQU3.js → MarkdownRenderer-aMtgN-R6.js} +1 -1
  40. package/src/assets/web-panel/assets/{Marketplace-CPEgWDc3.js → Marketplace-JQYmDqM8.js} +1 -1
  41. package/src/assets/web-panel/assets/{McpTools-C36qjgF9.js → McpTools-Cyx4yZ-9.js} +5 -5
  42. package/src/assets/web-panel/assets/{Memory-tNHvrgqN.js → Memory-VDuGZTN2.js} +2 -2
  43. package/src/assets/web-panel/assets/{MobileBridge-Xaf6cKjc.js → MobileBridge-_yKo97gi.js} +1 -1
  44. package/src/assets/web-panel/assets/{MobileProjects-D2E5TTeO.js → MobileProjects-CXRXW3q7.js} +1 -1
  45. package/src/assets/web-panel/assets/{Mtc-D4AJ2dwV.js → Mtc-acTGtfLH.js} +5 -5
  46. package/src/assets/web-panel/assets/{MtcAudit-Df-gtn2_.js → MtcAudit-ClpjXfkY.js} +4 -4
  47. package/src/assets/web-panel/assets/{Multisig-DTCHLWld.js → Multisig-_6Tn3o96.js} +3 -3
  48. package/src/assets/web-panel/assets/{NLProgramming-DruZ_1Jb.js → NLProgramming-CK7pbZzP.js} +1 -1
  49. package/src/assets/web-panel/assets/{Notes-Dx3pdGDs.js → Notes-Czy6V_P8.js} +4 -4
  50. package/src/assets/web-panel/assets/{NotificationSettings-Bi3L69oL.js → NotificationSettings-Cqx-UVv3.js} +1 -1
  51. package/src/assets/web-panel/assets/{OrderTableRenderer-BHY33m7C.js → OrderTableRenderer-C69WDxGy.js} +1 -1
  52. package/src/assets/web-panel/assets/{Organization-BG2ZVEV-.js → Organization-DFnA9vLQ.js} +4 -4
  53. package/src/assets/web-panel/assets/{Overflow-Viwtg4Mn.js → Overflow-ReZkk2yT.js} +1 -1
  54. package/src/assets/web-panel/assets/{P2P-Clm_yc1Z.js → P2P-DzjSI0aI.js} +2 -2
  55. package/src/assets/web-panel/assets/{PdhVaultBrowser-NKmmDiOw.js → PdhVaultBrowser-DKTf5xIP.js} +5 -5
  56. package/src/assets/web-panel/assets/{Permissions-DIL8u9Nk.js → Permissions-DhQ5klhR.js} +4 -4
  57. package/src/assets/web-panel/assets/{PersonalDataHub-B-jM4wt-.js → PersonalDataHub-CUL1iHrW.js} +4 -4
  58. package/src/assets/web-panel/assets/{Pipeline-627I__H5.js → Pipeline-DU88iu4N.js} +1 -1
  59. package/src/assets/web-panel/assets/{Privacy-2DN2qrkW.js → Privacy-Bw-sYfyK.js} +1 -1
  60. package/src/assets/web-panel/assets/{ProjectInit-CN4wC0sF.js → ProjectInit-Bq3Q5y0z.js} +2 -2
  61. package/src/assets/web-panel/assets/{ProjectSettings-K35vsh5H.js → ProjectSettings-C8spzpho.js} +2 -2
  62. package/src/assets/web-panel/assets/Projects-Dd-xuPIc.js +1 -0
  63. package/src/assets/web-panel/assets/{Providers-MckaQUhQ.js → Providers-Bf4eTP9W.js} +1 -1
  64. package/src/assets/web-panel/assets/{QrScannerModal-BS3mtA_O.js → QrScannerModal-Dme9BDU1.js} +1 -1
  65. package/src/assets/web-panel/assets/{QuickAsk-BHp_nofL.js → QuickAsk-CIXfv1rE.js} +1 -1
  66. package/src/assets/web-panel/assets/{Recommend-DlmnPuZj.js → Recommend-CIxvdeZ-.js} +1 -1
  67. package/src/assets/web-panel/assets/{RemoteSession-tsyFbvpy.js → RemoteSession-C6k7MExI.js} +2 -2
  68. package/src/assets/web-panel/assets/{Reputation-Ba1iWv2D.js → Reputation-CISDz2bl.js} +1 -1
  69. package/src/assets/web-panel/assets/{Row-NnlGC--f.js → Row-CdnDxXyf.js} +1 -1
  70. package/src/assets/web-panel/assets/{RssFeed-CjThSIRv.js → RssFeed-C-QpRHsl.js} +3 -3
  71. package/src/assets/web-panel/assets/{Search-CojUyC5x.js → Search-DBjaGzSn.js} +1 -1
  72. package/src/assets/web-panel/assets/{Security-c-sxeJEc.js → Security-CQKukOeE.js} +3 -3
  73. package/src/assets/web-panel/assets/{Services-CtcKEEmF.js → Services-DZz86jWb.js} +2 -2
  74. package/src/assets/web-panel/assets/{Skeleton-CIBBXflN.js → Skeleton-jb8RMK0d.js} +1 -1
  75. package/src/assets/web-panel/assets/{Skills-D1XOOK9n.js → Skills-CdY76fZG.js} +1 -1
  76. package/src/assets/web-panel/assets/{Sla-hRfEp5Sn.js → Sla-C_CSiZX9.js} +1 -1
  77. package/src/assets/web-panel/assets/SpeechSettings-C7tT9iad.js +1 -0
  78. package/src/assets/web-panel/assets/{SyncSettings-BJsSFGYy.js → SyncSettings-qvimGO0O.js} +2 -2
  79. package/src/assets/web-panel/assets/{Tasks-CfSZ8A1L.js → Tasks-Dng5LPJC.js} +1 -1
  80. package/src/assets/web-panel/assets/{Templates-idDf47W-.js → Templates-Bwqq5Wll.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tenant-C-UsX6w6.js → Tenant-DNWuwP7Z.js} +1 -1
  82. package/src/assets/web-panel/assets/{Terminal-BgJtX5n1.js → Terminal-B-6Othpp.js} +2 -2
  83. package/src/assets/web-panel/assets/{TimelineRenderer-DP0BLKTl.js → TimelineRenderer-DKpKZfzx.js} +1 -1
  84. package/src/assets/web-panel/assets/{Tokens-LwCzqETp.js → Tokens-DzfGMd2J.js} +1 -1
  85. package/src/assets/web-panel/assets/{Trigger-D_9tVch0.js → Trigger-BXwUi74J.js} +1 -1
  86. package/src/assets/web-panel/assets/{Trust-BH2UlEWh.js → Trust-BNzUq4ui.js} +1 -1
  87. package/src/assets/web-panel/assets/{UkeySign-CIpD0nYb.js → UkeySign-BYugxww4.js} +1 -1
  88. package/src/assets/web-panel/assets/{VideoEditing-9ShhYxK_.js → VideoEditing-DdauhtSJ.js} +1 -1
  89. package/src/assets/web-panel/assets/{Wallet-C7afGNIG.js → Wallet-CSC9bCe_.js} +4 -4
  90. package/src/assets/web-panel/assets/{WebAuthn-bUKn6i5R.js → WebAuthn-Q8AN3bTO.js} +5 -5
  91. package/src/assets/web-panel/assets/{WorkflowEditor-CxgQaGyp.js → WorkflowEditor-d-LBGCuA.js} +1 -1
  92. package/src/assets/web-panel/assets/{chat-DMVkmJwx.js → chat-BLVDCgsZ.js} +1 -1
  93. package/src/assets/web-panel/assets/{colors-CDzQh5nS.js → colors-BZlPSEHl.js} +1 -1
  94. package/src/assets/web-panel/assets/{compact-item-C51kEF4D.js → compact-item-IWseP6R6.js} +1 -1
  95. package/src/assets/web-panel/assets/{createContext-BTk9bXBn.js → createContext-DnVRV3Ao.js} +1 -1
  96. package/src/assets/web-panel/assets/devWarning-Jzu62CLC.js +1 -0
  97. package/src/assets/web-panel/assets/{hasIn-CMje33rb.js → hasIn-DLQZA_Wn.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-BBz9LIMS.js → index-1jeAudHd.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-hg_emBsi.js → index-BEu2lMBy.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-DRG-aqOs.js → index-BOZJBTWZ.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-CDLkNkLj.js → index-BSjilZ9L.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-CvSeVzqe.js → index-BZs5bBnk.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DpcrFxMI.js → index-B_N4-Gq9.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-D9rC-fGW.js → index-Bbj2NS_I.js} +3 -3
  105. package/src/assets/web-panel/assets/{index-BVePe_Rf.js → index-BhA3QEni.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-BzCkNylB.js → index-Bxp7uDD8.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-BAfY7m7i.js → index-C1ifZtXo.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-BTlsE8Df.js → index-C2FqfMvU.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-DqU9njjT.js → index-C5dgnilC.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-BtlEqHLP.js → index-CINJQEj6.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-BeEaAEIp.js → index-COev0W2g.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-CCLj9hcD.js → index-CT9vVN-X.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-h31MwrME.js → index-CZDekj7T.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-Ld_4mFmC.js → index-CarMTIyQ.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-CwGPwjUu.js → index-CbQNL0ue.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-C5IWTMVl.js → index-CpBaJ3TD.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BW4F5YDF.js → index-DB0MJgF_.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-CypsfcEq.js → index-DOoQpuQY.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-CfC-piWZ.js → index-DTbxp4UH.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-BCRvG9lq.js → index-DUq0c7gq.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-0Auljb10.js → index-DZpYeqrh.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-C4rQCS8T.js → index-D_LQodeJ.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-CtM8OT_7.js → index-DalA0TKC.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-C1oZQmAA.js → index-DjWnONdy.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-DRLQyp2A.js → index-Dkwsi1Kn.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-togc_uYn.js → index-DocEhinm.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-x53Vf7la.js → index-DpVrK6fr.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-DgivOKHA.js → index-Dy0R5ZED.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-B0-zMtKA.js → index-M7wxfAbm.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-BXQBPLyE.js → index-N_GoWZX4.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-C1oZReaY.js → index-YSw_gxlZ.js} +1 -1
  132. package/src/assets/web-panel/assets/index-Zb2MeDSy.js +1 -0
  133. package/src/assets/web-panel/assets/index-fikqml4k.js +1 -0
  134. package/src/assets/web-panel/assets/{index-Cd0hmPxD.js → index-gt8bNefW.js} +1 -1
  135. package/src/assets/web-panel/assets/{index-OS-vfbTA.js → index-hwVDeoC0.js} +1 -1
  136. package/src/assets/web-panel/assets/{index-DJfnCuMq.js → index-jWb70ZgZ.js} +1 -1
  137. package/src/assets/web-panel/assets/{initDefaultProps-vrA8K-ac.js → initDefaultProps-Co67Kqmb.js} +1 -1
  138. package/src/assets/web-panel/assets/{motion-ByEzwIAl.js → motion-BMOxQSF_.js} +1 -1
  139. package/src/assets/web-panel/assets/{move-Ck4AWjw-.js → move-C5fWDw-N.js} +1 -1
  140. package/src/assets/web-panel/assets/{mtc-parser-6p0l9SrB.js → mtc-parser-BJevpL0e.js} +1 -1
  141. package/src/assets/web-panel/assets/{omit-B1CUPQ6R.js → omit-BCzNU8s6.js} +1 -1
  142. package/src/assets/web-panel/assets/{pickAttrs-B1STg1iW.js → pickAttrs-CybWvDPk.js} +1 -1
  143. package/src/assets/web-panel/assets/{placementArrow-DTTllkBV.js → placementArrow-BMWS9HnW.js} +1 -1
  144. package/src/assets/web-panel/assets/{responsiveObserve-AIrrCfBb.js → responsiveObserve-BoL2UnGY.js} +1 -1
  145. package/src/assets/web-panel/assets/{slide-C_fPMjdR.js → slide-DWNfXraU.js} +1 -1
  146. package/src/assets/web-panel/assets/{statusUtils-D5ow2yIP.js → statusUtils-BquTM3tX.js} +1 -1
  147. package/src/assets/web-panel/assets/{styleChecker-Dy3hEA--.js → styleChecker-BEkCleim.js} +1 -1
  148. package/src/assets/web-panel/assets/{useFlexGapSupport-WfzcfSh4.js → useFlexGapSupport-Oecnq7EA.js} +1 -1
  149. package/src/assets/web-panel/assets/{useFs-CKSejfas.js → useFs-CkDffJ4F.js} +1 -1
  150. package/src/assets/web-panel/assets/{usePersonalDataHub-od3zpPBN.js → usePersonalDataHub-qbvKXesB.js} +1 -1
  151. package/src/assets/web-panel/assets/{vnode-DRD_2DFA.js → vnode-C6v8n6mA.js} +1 -1
  152. package/src/assets/web-panel/assets/{zoom-DmNrEtgf.js → zoom-4CTeFILV.js} +1 -1
  153. package/src/assets/web-panel/index.html +1 -1
  154. package/src/command-help-index.json +3 -3
  155. package/src/command-manifest.json +1 -1
  156. package/src/commands/agenda.js +76 -4
  157. package/src/commands/automation.js +462 -3
  158. package/src/commands/background-session.js +102 -1
  159. package/src/commands/cowork.js +6 -1
  160. package/src/commands/loop.js +167 -36
  161. package/src/commands/routine.js +11 -4
  162. package/src/commands/scheduler-daemon.js +450 -0
  163. package/src/data/changelog.json +15 -2
  164. package/src/lib/agent-schedule-store.js +323 -43
  165. package/src/lib/automation-center-routines.js +363 -0
  166. package/src/lib/automation-center.js +435 -0
  167. package/src/lib/automation-engine.js +74 -19
  168. package/src/lib/automation-execution-authority.js +736 -0
  169. package/src/lib/background-agent-supervisor.js +78 -0
  170. package/src/lib/background-needs-input-incident.js +221 -0
  171. package/src/lib/cowork-cron.js +316 -18
  172. package/src/lib/cowork-observe.js +5 -1
  173. package/src/lib/loop.js +37 -2
  174. package/src/lib/permission-engine.js +14 -7
  175. package/src/lib/public-ide-capability-manifest.js +2 -0
  176. package/src/lib/routine-store.js +166 -65
  177. package/src/lib/scheduler-kernel/agenda-adapter.js +297 -27
  178. package/src/lib/scheduler-kernel/authority-resolver.js +163 -0
  179. package/src/lib/scheduler-kernel/automation-adapter.js +592 -0
  180. package/src/lib/scheduler-kernel/automation-event-adapter.js +721 -0
  181. package/src/lib/scheduler-kernel/contract.js +11 -3
  182. package/src/lib/scheduler-kernel/cowork-cron-adapter.js +58 -12
  183. package/src/lib/scheduler-kernel/loop-adapter.js +378 -0
  184. package/src/lib/scheduler-kernel/routine-adapter.js +303 -8
  185. package/src/lib/scheduler-kernel/service.js +328 -0
  186. package/src/lib/scheduler-kernel/store.js +828 -96
  187. package/src/runtime/agent-core.js +2 -1
  188. package/src/runtime/coding-agent-contract-shared.cjs +5 -0
  189. package/src/workers/background-agent-worker.js +91 -28
  190. package/src/assets/web-panel/assets/Projects-B_wQadhX.js +0 -1
  191. package/src/assets/web-panel/assets/SpeechSettings-Dexb1f_6.js +0 -1
  192. package/src/assets/web-panel/assets/devWarning-CdwPwYj8.js +0 -1
  193. package/src/assets/web-panel/assets/index-BEeOdfEe.js +0 -1
  194. package/src/assets/web-panel/assets/index-BTe0TOu6.js +0 -1
@@ -6,7 +6,6 @@ import { getHomeDir } from "../paths.js";
6
6
  import { ensurePrivateDirectory, ensurePrivateFile } from "../secure-fs.js";
7
7
  import {
8
8
  OCCURRENCE_STATUS,
9
- SCHEDULER_SCHEMA_VERSION,
10
9
  SchedulerKernelError,
11
10
  canonicalJson,
12
11
  deriveOccurrenceIdentity,
@@ -22,18 +21,35 @@ import {
22
21
  const requireCjs = createRequire(import.meta.url);
23
22
 
24
23
  export const SCHEDULER_APPLICATION_ID = 0x4343534b; // "CCSK"
24
+ export const SCHEDULER_STORE_SCHEMA_VERSION = 2;
25
25
  export const DEFAULT_BUSY_TIMEOUT_MS = 5_000;
26
26
  export const MAX_LEASE_MS = 24 * 60 * 60 * 1_000;
27
-
28
- const MIGRATION_NAME = "scheduler-kernel-v1";
27
+ export const MIN_AUTHORITY_WINDOW_MS = 60_000;
28
+ export const MAX_AUTHORITY_WINDOW_MS = 31 * 24 * 60 * 60 * 1_000;
29
+ export const MAX_AUTHORITY_BUDGET = 1_000_000;
30
+ export const DEFAULT_AUTHORITY_WINDOW_MS = 24 * 60 * 60 * 1_000;
31
+ export const DEFAULT_AUTHORITY_MAX_RUNS = 100_000;
32
+ export const DEFAULT_AUTHORITY_MAX_UNITS = 100_000;
33
+
34
+ const MIGRATION_V1_NAME = "scheduler-kernel-v1";
35
+ const MIGRATION_V2_NAME = "scheduler-kernel-authority-v2";
36
+ const V1_USER_TABLES = Object.freeze([
37
+ "events",
38
+ "jobs",
39
+ "migrations",
40
+ "occurrences",
41
+ ]);
29
42
  const USER_TABLES = Object.freeze([
30
43
  "events",
31
44
  "jobs",
32
45
  "migrations",
33
46
  "occurrences",
47
+ "scheduler_authority_policies",
48
+ "scheduler_authority_reservations",
49
+ "scheduler_authority_usage",
34
50
  ]);
35
51
 
36
- const MIGRATION_V1_SQL = `
52
+ export const MIGRATION_V1_SQL = `
37
53
  CREATE TABLE migrations (
38
54
  version INTEGER PRIMARY KEY NOT NULL,
39
55
  name TEXT NOT NULL UNIQUE,
@@ -120,11 +136,86 @@ export const MIGRATION_V1_CHECKSUM = createHash("sha256")
120
136
  .update(MIGRATION_V1_SQL.trim().replace(/\r\n/g, "\n"), "utf8")
121
137
  .digest("hex");
122
138
 
139
+ export const MIGRATION_V2_SQL = `
140
+ CREATE TABLE scheduler_authority_policies (
141
+ principal_type TEXT NOT NULL,
142
+ principal_id TEXT NOT NULL,
143
+ revision INTEGER NOT NULL CHECK (revision >= 1),
144
+ enabled INTEGER NOT NULL CHECK (enabled IN (0, 1)),
145
+ capabilities_json TEXT NOT NULL CHECK (json_valid(capabilities_json)),
146
+ window_ms INTEGER NOT NULL CHECK (
147
+ window_ms BETWEEN 60000 AND 2678400000
148
+ ),
149
+ max_runs INTEGER NOT NULL CHECK (
150
+ max_runs BETWEEN 1 AND 1000000
151
+ ),
152
+ max_units INTEGER NOT NULL CHECK (
153
+ max_units BETWEEN 1 AND 1000000
154
+ ),
155
+ created_at INTEGER NOT NULL CHECK (created_at >= 0),
156
+ updated_at INTEGER NOT NULL CHECK (updated_at >= 0),
157
+ PRIMARY KEY (principal_type, principal_id)
158
+ );
159
+
160
+ CREATE TABLE scheduler_authority_usage (
161
+ principal_type TEXT NOT NULL,
162
+ principal_id TEXT NOT NULL,
163
+ policy_revision INTEGER NOT NULL CHECK (policy_revision >= 1),
164
+ window_started_at INTEGER NOT NULL CHECK (window_started_at >= 0),
165
+ runs INTEGER NOT NULL CHECK (runs BETWEEN 0 AND 1000000),
166
+ units INTEGER NOT NULL CHECK (units BETWEEN 0 AND 1000000),
167
+ updated_at INTEGER NOT NULL CHECK (updated_at >= 0),
168
+ PRIMARY KEY (
169
+ principal_type,
170
+ principal_id,
171
+ policy_revision,
172
+ window_started_at
173
+ )
174
+ );
175
+
176
+ CREATE TABLE scheduler_authority_reservations (
177
+ occurrence_id TEXT PRIMARY KEY NOT NULL
178
+ REFERENCES occurrences(occurrence_id) ON DELETE RESTRICT,
179
+ job_id TEXT NOT NULL REFERENCES jobs(job_id) ON DELETE RESTRICT,
180
+ principal_type TEXT NOT NULL,
181
+ principal_id TEXT NOT NULL,
182
+ policy_revision INTEGER NOT NULL CHECK (policy_revision >= 1),
183
+ window_started_at INTEGER NOT NULL CHECK (window_started_at >= 0),
184
+ units INTEGER NOT NULL CHECK (units BETWEEN 1 AND 1000000),
185
+ status TEXT NOT NULL CHECK (
186
+ status IN ('reserved', 'succeeded', 'failed')
187
+ ),
188
+ outcome_json TEXT CHECK (
189
+ outcome_json IS NULL OR json_valid(outcome_json)
190
+ ),
191
+ created_at INTEGER NOT NULL CHECK (created_at >= 0),
192
+ settled_at INTEGER,
193
+ CHECK (
194
+ (status = 'reserved' AND outcome_json IS NULL AND settled_at IS NULL)
195
+ OR
196
+ (status <> 'reserved' AND outcome_json IS NOT NULL AND settled_at IS NOT NULL)
197
+ )
198
+ );
199
+
200
+ CREATE INDEX scheduler_authority_reservations_principal
201
+ ON scheduler_authority_reservations
202
+ (principal_type, principal_id, policy_revision, window_started_at);
203
+ CREATE INDEX scheduler_authority_reservations_status
204
+ ON scheduler_authority_reservations(status, created_at);
205
+ `;
206
+
207
+ export const MIGRATION_V2_CHECKSUM = createHash("sha256")
208
+ .update(MIGRATION_V2_SQL.trim().replace(/\r\n/g, "\n"), "utf8")
209
+ .update("\0scheduler-authority-v2-backfill-v1", "utf8")
210
+ .digest("hex");
211
+
123
212
  // Fingerprint of the normalized sqlite_master catalog produced by the v1 DDL.
124
213
  // Unlike the migration-source checksum, this also detects added triggers/views
125
214
  // and constraint/foreign-key changes that preserve the visible column list.
126
215
  export const SCHEMA_V1_FINGERPRINT =
127
216
  "cf244f675ac7430683f67046b57524eb6c14baa45d6463ed3f704be898fec887";
217
+ export const SCHEMA_V2_FINGERPRINT =
218
+ "402c58d4b3b217699591528f9be6c1c7fe341650b843eb2d8d9d907779ff27b0";
128
219
 
129
220
  const EXPECTED_COLUMNS = Object.freeze({
130
221
  migrations: [
@@ -177,9 +268,45 @@ const EXPECTED_COLUMNS = Object.freeze({
177
268
  ["fence", "INTEGER", 0, 0],
178
269
  ["data_json", "TEXT", 1, 0],
179
270
  ],
271
+ scheduler_authority_policies: [
272
+ ["principal_type", "TEXT", 1, 1],
273
+ ["principal_id", "TEXT", 1, 2],
274
+ ["revision", "INTEGER", 1, 0],
275
+ ["enabled", "INTEGER", 1, 0],
276
+ ["capabilities_json", "TEXT", 1, 0],
277
+ ["window_ms", "INTEGER", 1, 0],
278
+ ["max_runs", "INTEGER", 1, 0],
279
+ ["max_units", "INTEGER", 1, 0],
280
+ ["created_at", "INTEGER", 1, 0],
281
+ ["updated_at", "INTEGER", 1, 0],
282
+ ],
283
+ scheduler_authority_usage: [
284
+ ["principal_type", "TEXT", 1, 1],
285
+ ["principal_id", "TEXT", 1, 2],
286
+ ["policy_revision", "INTEGER", 1, 3],
287
+ ["window_started_at", "INTEGER", 1, 4],
288
+ ["runs", "INTEGER", 1, 0],
289
+ ["units", "INTEGER", 1, 0],
290
+ ["updated_at", "INTEGER", 1, 0],
291
+ ],
292
+ scheduler_authority_reservations: [
293
+ ["occurrence_id", "TEXT", 1, 1],
294
+ ["job_id", "TEXT", 1, 0],
295
+ ["principal_type", "TEXT", 1, 0],
296
+ ["principal_id", "TEXT", 1, 0],
297
+ ["policy_revision", "INTEGER", 1, 0],
298
+ ["window_started_at", "INTEGER", 1, 0],
299
+ ["units", "INTEGER", 1, 0],
300
+ ["status", "TEXT", 1, 0],
301
+ ["outcome_json", "TEXT", 0, 0],
302
+ ["created_at", "INTEGER", 1, 0],
303
+ ["settled_at", "INTEGER", 0, 0],
304
+ ],
180
305
  });
181
306
 
182
307
  const EXPECTED_INDEXES = Object.freeze([
308
+ "scheduler_authority_reservations_principal",
309
+ "scheduler_authority_reservations_status",
183
310
  "scheduler_events_job",
184
311
  "scheduler_events_occurrence",
185
312
  "scheduler_occurrences_claim",
@@ -218,6 +345,84 @@ function normalizeBoolean(value, field) {
218
345
  return value;
219
346
  }
220
347
 
348
+ function normalizeAuthorityBudgetLimit(
349
+ value,
350
+ field,
351
+ { minimum = 1, maximum = MAX_AUTHORITY_BUDGET } = {},
352
+ ) {
353
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
354
+ throw invalidArgument(
355
+ `${field} must be an integer between ${minimum} and ${maximum}`,
356
+ );
357
+ }
358
+ return value;
359
+ }
360
+
361
+ function normalizeAuthorityPrincipal(principal) {
362
+ if (!principal || typeof principal !== "object" || Array.isArray(principal)) {
363
+ throw invalidArgument("authority principal must be an object");
364
+ }
365
+ return {
366
+ type: normalizeIdentifier(principal.type, "authority.principal.type", {
367
+ maxLength: 64,
368
+ }),
369
+ id: normalizeIdentifier(principal.id, "authority.principal.id"),
370
+ };
371
+ }
372
+
373
+ function normalizeCapabilityList(value, field = "capabilities") {
374
+ if (!Array.isArray(value) || value.length === 0 || value.length > 256) {
375
+ throw invalidArgument(`${field} must contain between 1 and 256 entries`);
376
+ }
377
+ const capabilities = [
378
+ ...new Set(
379
+ value.map((capability, index) =>
380
+ normalizeIdentifier(capability, `${field}[${index}]`),
381
+ ),
382
+ ),
383
+ ].sort();
384
+ if (capabilities.includes("*")) {
385
+ throw invalidArgument(`${field} must contain exact capabilities`);
386
+ }
387
+ return capabilities;
388
+ }
389
+
390
+ function mapAuthorityPolicy(row) {
391
+ if (!row) return null;
392
+ return {
393
+ principal: { type: row.principal_type, id: row.principal_id },
394
+ revision: row.revision,
395
+ enabled: row.enabled === 1,
396
+ capabilities: normalizeCapabilityList(
397
+ readStoredJson(row.capabilities_json, "scheduler authority capabilities"),
398
+ ),
399
+ windowMs: row.window_ms,
400
+ maxRuns: row.max_runs,
401
+ maxUnits: row.max_units,
402
+ createdAt: row.created_at,
403
+ updatedAt: row.updated_at,
404
+ };
405
+ }
406
+
407
+ function mapAuthorityReservation(row) {
408
+ if (!row) return null;
409
+ return {
410
+ occurrenceId: row.occurrence_id,
411
+ jobId: row.job_id,
412
+ principal: { type: row.principal_type, id: row.principal_id },
413
+ policyRevision: row.policy_revision,
414
+ windowStartedAt: row.window_started_at,
415
+ units: row.units,
416
+ status: row.status,
417
+ outcome:
418
+ row.outcome_json === null
419
+ ? null
420
+ : readStoredJson(row.outcome_json, "scheduler authority outcome"),
421
+ createdAt: row.created_at,
422
+ settledAt: row.settled_at,
423
+ };
424
+ }
425
+
221
426
  function normalizeJobInput(input) {
222
427
  if (!input || typeof input !== "object" || Array.isArray(input)) {
223
428
  throw invalidArgument("job must be an object");
@@ -428,7 +633,7 @@ function verifyTableShape(db, table) {
428
633
  if (JSON.stringify(actual) !== JSON.stringify(expected)) {
429
634
  throw schemaError(
430
635
  "SCHEDULER_SCHEMA_CORRUPT",
431
- `Scheduler table shape does not match v1: ${table}`,
636
+ `Scheduler table shape is invalid: ${table}`,
432
637
  undefined,
433
638
  { expected, actual },
434
639
  );
@@ -454,15 +659,16 @@ function schemaFingerprint(db) {
454
659
  .digest("hex");
455
660
  }
456
661
 
457
- function verifySchema(db) {
662
+ function verifySchema(db, version = SCHEDULER_STORE_SCHEMA_VERSION) {
458
663
  assertDatabaseIntegrity(db);
459
664
  const tables = listUserTables(db);
460
- if (JSON.stringify(tables) !== JSON.stringify(USER_TABLES)) {
665
+ const expectedTables = version === 1 ? V1_USER_TABLES : USER_TABLES;
666
+ if (JSON.stringify(tables) !== JSON.stringify(expectedTables)) {
461
667
  throw schemaError(
462
668
  "SCHEDULER_SCHEMA_UNKNOWN",
463
669
  "Scheduler database has unknown or missing tables",
464
670
  undefined,
465
- { expected: USER_TABLES, actual: tables },
671
+ { expected: expectedTables, actual: tables },
466
672
  );
467
673
  }
468
674
 
@@ -478,10 +684,7 @@ function verifySchema(db) {
478
684
  cause,
479
685
  );
480
686
  }
481
- if (
482
- applicationId !== SCHEDULER_APPLICATION_ID ||
483
- userVersion !== SCHEDULER_SCHEMA_VERSION
484
- ) {
687
+ if (applicationId !== SCHEDULER_APPLICATION_ID || userVersion !== version) {
485
688
  throw schemaError(
486
689
  "SCHEDULER_SCHEMA_UNKNOWN",
487
690
  "Scheduler schema version or application identity is unknown",
@@ -490,16 +693,18 @@ function verifySchema(db) {
490
693
  );
491
694
  }
492
695
 
493
- for (const table of USER_TABLES) verifyTableShape(db, table);
696
+ for (const table of expectedTables) verifyTableShape(db, table);
494
697
 
495
698
  const actualFingerprint = schemaFingerprint(db);
496
- if (actualFingerprint !== SCHEMA_V1_FINGERPRINT) {
699
+ const expectedFingerprint =
700
+ version === 1 ? SCHEMA_V1_FINGERPRINT : SCHEMA_V2_FINGERPRINT;
701
+ if (actualFingerprint !== expectedFingerprint) {
497
702
  throw schemaError(
498
703
  "SCHEDULER_SCHEMA_CORRUPT",
499
- "Scheduler sqlite_master catalog does not match v1",
704
+ `Scheduler sqlite_master catalog does not match v${version}`,
500
705
  undefined,
501
706
  {
502
- expected: SCHEMA_V1_FINGERPRINT,
707
+ expected: expectedFingerprint,
503
708
  actual: actualFingerprint,
504
709
  },
505
710
  );
@@ -520,10 +725,14 @@ function verifySchema(db) {
520
725
  );
521
726
  }
522
727
  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
728
+ migrations.length !== version ||
729
+ migrations[0].version !== 1 ||
730
+ migrations[0].name !== MIGRATION_V1_NAME ||
731
+ migrations[0].checksum !== MIGRATION_V1_CHECKSUM ||
732
+ (version === 2 &&
733
+ (migrations[1]?.version !== 2 ||
734
+ migrations[1]?.name !== MIGRATION_V2_NAME ||
735
+ migrations[1]?.checksum !== MIGRATION_V2_CHECKSUM))
527
736
  ) {
528
737
  throw schemaError(
529
738
  "SCHEDULER_SCHEMA_UNKNOWN",
@@ -539,12 +748,18 @@ function verifySchema(db) {
539
748
  )
540
749
  .all()
541
750
  .map((row) => row.name);
542
- if (JSON.stringify(indexes) !== JSON.stringify(EXPECTED_INDEXES)) {
751
+ const expectedIndexes =
752
+ version === 1
753
+ ? EXPECTED_INDEXES.filter(
754
+ (name) => !name.startsWith("scheduler_authority_"),
755
+ )
756
+ : EXPECTED_INDEXES;
757
+ if (JSON.stringify(indexes) !== JSON.stringify(expectedIndexes)) {
543
758
  throw schemaError(
544
759
  "SCHEDULER_SCHEMA_CORRUPT",
545
- "Scheduler indexes do not match v1",
760
+ `Scheduler indexes do not match v${version}`,
546
761
  undefined,
547
- { expected: EXPECTED_INDEXES, actual: indexes },
762
+ { expected: expectedIndexes, actual: indexes },
548
763
  );
549
764
  }
550
765
  const foreignKeyFailures = db.pragma("foreign_key_check");
@@ -558,22 +773,108 @@ function verifySchema(db) {
558
773
  }
559
774
  }
560
775
 
776
+ function migrateAuthorityV2(db, now) {
777
+ db.exec(MIGRATION_V2_SQL);
778
+ const policies = new Map();
779
+ const jobs = db
780
+ .prepare("SELECT job_id, authority_json FROM jobs ORDER BY job_id")
781
+ .all();
782
+ const occurrences = db
783
+ .prepare(
784
+ `SELECT occurrence_id, status, authority_json
785
+ FROM occurrences ORDER BY occurrence_id`,
786
+ )
787
+ .all();
788
+ const bind = (encoded, field) => {
789
+ const authority = readStoredAuthority(encoded, field);
790
+ const key = canonicalJson(authority.principal, `${field}.principal`);
791
+ const current = policies.get(key) ?? {
792
+ principal: authority.principal,
793
+ capabilities: new Set(),
794
+ };
795
+ for (const capability of authority.requestedCapabilities) {
796
+ current.capabilities.add(capability);
797
+ }
798
+ policies.set(key, current);
799
+ return {
800
+ ...authority,
801
+ authorizationRefs: {
802
+ ...authority.authorizationRefs,
803
+ schedulerPolicyRevision: "scheduler-authority:1",
804
+ },
805
+ };
806
+ };
807
+ const boundJobs = jobs.map((row) => ({
808
+ id: row.job_id,
809
+ authority: bind(row.authority_json, `job ${row.job_id} authority`),
810
+ }));
811
+ const boundOccurrences = occurrences.map((row) => {
812
+ const field = `occurrence ${row.occurrence_id} authority`;
813
+ const terminal =
814
+ row.status === OCCURRENCE_STATUS.SUCCEEDED ||
815
+ row.status === OCCURRENCE_STATUS.DEAD_LETTER;
816
+ return {
817
+ id: row.occurrence_id,
818
+ terminal,
819
+ // Validate terminal history too, but do not widen the active policy with
820
+ // capabilities that no current or replayable occurrence still needs.
821
+ authority: terminal
822
+ ? readStoredAuthority(row.authority_json, field)
823
+ : bind(row.authority_json, field),
824
+ };
825
+ });
826
+ const insertPolicy = db.prepare(
827
+ `INSERT INTO scheduler_authority_policies
828
+ (principal_type, principal_id, revision, enabled,
829
+ capabilities_json, window_ms, max_runs, max_units,
830
+ created_at, updated_at)
831
+ VALUES (?, ?, 1, 1, ?, ?, ?, ?, ?, ?)`,
832
+ );
833
+ for (const { principal, capabilities } of policies.values()) {
834
+ insertPolicy.run(
835
+ principal.type,
836
+ principal.id,
837
+ canonicalJson([...capabilities].sort(), "authorityPolicy.capabilities"),
838
+ DEFAULT_AUTHORITY_WINDOW_MS,
839
+ DEFAULT_AUTHORITY_MAX_RUNS,
840
+ DEFAULT_AUTHORITY_MAX_UNITS,
841
+ now,
842
+ now,
843
+ );
844
+ }
845
+ const updateJob = db.prepare(
846
+ "UPDATE jobs SET authority_json = ? WHERE job_id = ?",
847
+ );
848
+ for (const row of boundJobs) {
849
+ updateJob.run(canonicalJson(row.authority, "job.authority"), row.id);
850
+ }
851
+ const updateOccurrence = db.prepare(
852
+ "UPDATE occurrences SET authority_json = ? WHERE occurrence_id = ?",
853
+ );
854
+ for (const row of boundOccurrences) {
855
+ if (row.terminal) continue;
856
+ updateOccurrence.run(
857
+ canonicalJson(row.authority, "occurrence.authority"),
858
+ row.id,
859
+ );
860
+ }
861
+ }
862
+
561
863
  function initializeOrVerifySchema(db, now) {
562
864
  assertDatabaseIntegrity(db);
563
865
  const tables = listUserTables(db);
564
866
  if (tables.length === 0) {
565
867
  const migrate = db.transaction(() => {
566
868
  db.exec(MIGRATION_V1_SQL);
869
+ migrateAuthorityV2(db, now);
567
870
  db.prepare(
568
871
  "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
- );
872
+ ).run(1, MIGRATION_V1_NAME, MIGRATION_V1_CHECKSUM, now);
873
+ db.prepare(
874
+ "INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
875
+ ).run(2, MIGRATION_V2_NAME, MIGRATION_V2_CHECKSUM, now);
575
876
  db.pragma(`application_id = ${SCHEDULER_APPLICATION_ID}`);
576
- db.pragma(`user_version = ${SCHEDULER_SCHEMA_VERSION}`);
877
+ db.pragma(`user_version = ${SCHEDULER_STORE_SCHEMA_VERSION}`);
577
878
  });
578
879
  migrate.immediate();
579
880
  } else if (!tables.includes("migrations")) {
@@ -583,6 +884,16 @@ function initializeOrVerifySchema(db, now) {
583
884
  undefined,
584
885
  { tables },
585
886
  );
887
+ } else if (db.pragma("user_version", { simple: true }) === 1) {
888
+ verifySchema(db, 1);
889
+ const migrate = db.transaction(() => {
890
+ migrateAuthorityV2(db, now);
891
+ db.prepare(
892
+ "INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
893
+ ).run(2, MIGRATION_V2_NAME, MIGRATION_V2_CHECKSUM, now);
894
+ db.pragma(`user_version = ${SCHEDULER_STORE_SCHEMA_VERSION}`);
895
+ });
896
+ migrate.immediate();
586
897
  }
587
898
  verifySchema(db);
588
899
  }
@@ -683,6 +994,12 @@ export class SchedulerStore {
683
994
  getOccurrenceByKey: db.prepare(
684
995
  "SELECT * FROM occurrences WHERE idempotency_key = ?",
685
996
  ),
997
+ listOccurrencesByTrigger: db.prepare(
998
+ `SELECT * FROM occurrences
999
+ WHERE job_id = ? AND trigger_key = ?
1000
+ ORDER BY created_at ASC, occurrence_id ASC
1001
+ LIMIT ?`,
1002
+ ),
686
1003
  insertEvent: db.prepare(`
687
1004
  INSERT INTO events
688
1005
  (job_id, occurrence_id, event_type, occurred_at, owner_id, fence, data_json)
@@ -855,83 +1172,136 @@ export class SchedulerStore {
855
1172
  });
856
1173
  }
857
1174
 
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({
1175
+ _enqueueOccurrence({ id, scheduled, key, available, payload, now }) {
1176
+ const job = mapJob(this.statements.getJob.get(id));
1177
+ if (!job) {
1178
+ throw new SchedulerKernelError(
1179
+ "SCHEDULER_NOT_FOUND",
1180
+ `Scheduler job does not exist: ${id}`,
1181
+ );
1182
+ }
1183
+ if (!job.enabled) {
1184
+ throw new SchedulerKernelError(
1185
+ "SCHEDULER_JOB_DISABLED",
1186
+ `Scheduler job is disabled: ${id}`,
1187
+ );
1188
+ }
1189
+ const identity = deriveOccurrenceIdentity({
1190
+ jobId: id,
1191
+ jobRevision: job.revision,
1192
+ scheduledFor: scheduled,
1193
+ triggerKey: key,
1194
+ });
1195
+ const payloadJson = canonicalJson(
1196
+ payload === undefined ? job.payload : payload,
1197
+ "occurrence.payload",
1198
+ );
1199
+ const result = this.db
1200
+ .prepare(
1201
+ `
1202
+ INSERT INTO occurrences
1203
+ (occurrence_id, job_id, job_revision, idempotency_key, trigger_key,
1204
+ scheduled_for, available_at, status, attempt, max_attempts, fence,
1205
+ lease_owner, lease_expires_at, authority_json, payload_json,
1206
+ last_error_json, result_json, created_at, updated_at, settled_at)
1207
+ VALUES
1208
+ (@occurrenceId, @jobId, @jobRevision, @idempotencyKey, @triggerKey,
1209
+ @scheduledFor, @availableAt, 'queued', 0, @maxAttempts, 0,
1210
+ NULL, NULL, @authorityJson, @payloadJson,
1211
+ NULL, NULL, @now, @now, NULL)
1212
+ ON CONFLICT(idempotency_key) DO NOTHING
1213
+ `,
1214
+ )
1215
+ .run({
1216
+ ...identity,
879
1217
  jobId: id,
880
1218
  jobRevision: job.revision,
881
- scheduledFor: scheduled,
882
1219
  triggerKey: key,
1220
+ scheduledFor: scheduled,
1221
+ availableAt: available,
1222
+ maxAttempts: job.maxAttempts,
1223
+ authorityJson: canonicalJson(job.authority, "occurrence.authority"),
1224
+ payloadJson,
1225
+ now,
883
1226
  });
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,
1227
+ const row = this.statements.getOccurrenceByKey.get(identity.idempotencyKey);
1228
+ if (!row || row.occurrence_id !== identity.occurrenceId) {
1229
+ throw schemaError(
1230
+ "SCHEDULER_DATA_CORRUPT",
1231
+ "Occurrence idempotency identity does not match stored data",
1232
+ );
1233
+ }
1234
+ if (row.payload_json !== payloadJson) {
1235
+ throw new SchedulerKernelError(
1236
+ "SCHEDULER_IDEMPOTENCY_PAYLOAD_MISMATCH",
1237
+ "Occurrence idempotency identity was reused with a different payload",
1238
+ { jobId: id, triggerKey: key },
1239
+ );
1240
+ }
1241
+ if (result.changes === 1) {
1242
+ this._appendEvent({
1243
+ jobId: id,
1244
+ occurrenceId: identity.occurrenceId,
1245
+ type: "occurrence_enqueued",
1246
+ occurredAt: now,
1247
+ data: {
903
1248
  jobRevision: job.revision,
904
- triggerKey: key,
905
1249
  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) {
1250
+ triggerKey: key,
1251
+ },
1252
+ });
1253
+ }
1254
+ return { ...mapOccurrence(row), deduplicated: result.changes === 0 };
1255
+ }
1256
+
1257
+ enqueueOccurrence({
1258
+ jobId,
1259
+ scheduledFor,
1260
+ triggerKey,
1261
+ availableAt,
1262
+ payload,
1263
+ } = {}) {
1264
+ const id = normalizeIdentifier(jobId, "jobId");
1265
+ const scheduled = normalizeEpochMs(scheduledFor, "scheduledFor");
1266
+ const key = normalizeIdentifier(triggerKey, "triggerKey");
1267
+ const available = normalizeEpochMs(availableAt ?? scheduled, "availableAt");
1268
+ const now = this._now();
1269
+ return this._write(() =>
1270
+ this._enqueueOccurrence({ id, scheduled, key, available, payload, now }),
1271
+ );
1272
+ }
1273
+
1274
+ enqueueOccurrenceOncePerTrigger({
1275
+ jobId,
1276
+ scheduledFor,
1277
+ triggerKey,
1278
+ availableAt,
1279
+ payload,
1280
+ } = {}) {
1281
+ const id = normalizeIdentifier(jobId, "jobId");
1282
+ const scheduled = normalizeEpochMs(scheduledFor, "scheduledFor");
1283
+ const key = normalizeIdentifier(triggerKey, "triggerKey");
1284
+ const available = normalizeEpochMs(availableAt ?? scheduled, "availableAt");
1285
+ const now = this._now();
1286
+ return this._write(() => {
1287
+ const prior = this.statements.listOccurrencesByTrigger.all(id, key, 2);
1288
+ if (prior.length > 1) {
916
1289
  throw schemaError(
917
1290
  "SCHEDULER_DATA_CORRUPT",
918
- "Occurrence idempotency identity does not match stored data",
1291
+ "Multiple occurrences exist for a unique job trigger",
919
1292
  );
920
1293
  }
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
- });
1294
+ if (prior.length === 1) {
1295
+ return { ...mapOccurrence(prior[0]), deduplicated: true };
933
1296
  }
934
- return { ...mapOccurrence(row), deduplicated: result.changes === 0 };
1297
+ return this._enqueueOccurrence({
1298
+ id,
1299
+ scheduled,
1300
+ key,
1301
+ available,
1302
+ payload,
1303
+ now,
1304
+ });
935
1305
  });
936
1306
  }
937
1307
 
@@ -944,6 +1314,19 @@ export class SchedulerStore {
944
1314
  );
945
1315
  }
946
1316
 
1317
+ listOccurrencesByTrigger({ jobId, triggerKey, limit = 2 } = {}) {
1318
+ this._assertOpen();
1319
+ const id = normalizeIdentifier(jobId, "jobId");
1320
+ const key = normalizeIdentifier(triggerKey, "triggerKey");
1321
+ const boundedLimit = Math.min(
1322
+ 200,
1323
+ Math.max(1, Number.isSafeInteger(Number(limit)) ? Number(limit) : 2),
1324
+ );
1325
+ return this.statements.listOccurrencesByTrigger
1326
+ .all(id, key, boundedLimit)
1327
+ .map(mapOccurrence);
1328
+ }
1329
+
947
1330
  _deadLetterExpiredLeases(now) {
948
1331
  const expired = this.db
949
1332
  .prepare(
@@ -1353,6 +1736,42 @@ export class SchedulerStore {
1353
1736
  settledAt,
1354
1737
  });
1355
1738
  if (update.changes !== 1) throw leaseLost(id);
1739
+ const reservation = this.db
1740
+ .prepare(
1741
+ `SELECT * FROM scheduler_authority_reservations
1742
+ WHERE occurrence_id = ?`,
1743
+ )
1744
+ .get(id);
1745
+ if (
1746
+ reservation?.status === "reserved" &&
1747
+ (nextStatus === OCCURRENCE_STATUS.SUCCEEDED ||
1748
+ nextStatus === OCCURRENCE_STATUS.DEAD_LETTER)
1749
+ ) {
1750
+ const authorityOutcome =
1751
+ nextStatus === OCCURRENCE_STATUS.SUCCEEDED ? "succeeded" : "failed";
1752
+ const authorityResult =
1753
+ authorityOutcome === "succeeded"
1754
+ ? { status: nextStatus, result: normalizedResult }
1755
+ : { status: nextStatus, error: normalizedError };
1756
+ const authorityUpdate = this.db
1757
+ .prepare(
1758
+ `UPDATE scheduler_authority_reservations
1759
+ SET status = ?, outcome_json = ?, settled_at = ?
1760
+ WHERE occurrence_id = ? AND status = 'reserved'`,
1761
+ )
1762
+ .run(
1763
+ authorityOutcome,
1764
+ canonicalJson(authorityResult, "authoritySettlement"),
1765
+ now,
1766
+ id,
1767
+ );
1768
+ if (authorityUpdate.changes !== 1) {
1769
+ throw new SchedulerKernelError(
1770
+ "SCHEDULER_AUTHORITY_SETTLEMENT_CONFLICT",
1771
+ `Scheduler authority reservation could not be settled: ${id}`,
1772
+ );
1773
+ }
1774
+ }
1356
1775
  this._appendEvent({
1357
1776
  jobId: current.job_id,
1358
1777
  occurrenceId: id,
@@ -1363,6 +1782,12 @@ export class SchedulerStore {
1363
1782
  data: {
1364
1783
  attempt: current.attempt,
1365
1784
  maxAttempts: current.max_attempts,
1785
+ ...(reservation
1786
+ ? {
1787
+ authorityPolicyRevision: reservation.policy_revision,
1788
+ authorityUnits: reservation.units,
1789
+ }
1790
+ : {}),
1366
1791
  ...(nextStatus === OCCURRENCE_STATUS.RETRY_WAIT
1367
1792
  ? { retryAt: nextAvailableAt }
1368
1793
  : {}),
@@ -1418,6 +1843,313 @@ export class SchedulerStore {
1418
1843
  .map(mapOccurrence);
1419
1844
  }
1420
1845
 
1846
+ setAuthorityPolicy(
1847
+ principal,
1848
+ {
1849
+ capabilities,
1850
+ windowMs,
1851
+ maxRuns,
1852
+ maxUnits,
1853
+ enabled = true,
1854
+ expectedRevision,
1855
+ } = {},
1856
+ ) {
1857
+ const actor = normalizeAuthorityPrincipal(principal);
1858
+ const allowedCapabilities = normalizeCapabilityList(capabilities);
1859
+ const window = normalizeAuthorityBudgetLimit(windowMs, "windowMs", {
1860
+ minimum: MIN_AUTHORITY_WINDOW_MS,
1861
+ maximum: MAX_AUTHORITY_WINDOW_MS,
1862
+ });
1863
+ const runs = normalizeAuthorityBudgetLimit(maxRuns, "maxRuns");
1864
+ const units = normalizeAuthorityBudgetLimit(maxUnits, "maxUnits");
1865
+ const active = normalizeBoolean(enabled, "enabled");
1866
+ if (
1867
+ expectedRevision !== undefined &&
1868
+ (!Number.isSafeInteger(expectedRevision) || expectedRevision < 0)
1869
+ ) {
1870
+ throw invalidArgument("expectedRevision must be an integer >= 0");
1871
+ }
1872
+ const now = this._now();
1873
+ return this._write(() => {
1874
+ const current = this.db
1875
+ .prepare(
1876
+ `SELECT * FROM scheduler_authority_policies
1877
+ WHERE principal_type = ? AND principal_id = ?`,
1878
+ )
1879
+ .get(actor.type, actor.id);
1880
+ const currentRevision = current?.revision ?? 0;
1881
+ if (
1882
+ expectedRevision !== undefined &&
1883
+ expectedRevision !== currentRevision
1884
+ ) {
1885
+ throw new SchedulerKernelError(
1886
+ "SCHEDULER_AUTHORITY_POLICY_CONFLICT",
1887
+ "Scheduler authority policy revision does not match",
1888
+ { expectedRevision, actualRevision: currentRevision },
1889
+ );
1890
+ }
1891
+ const revision = currentRevision + 1;
1892
+ this.db
1893
+ .prepare(
1894
+ `INSERT INTO scheduler_authority_policies
1895
+ (principal_type, principal_id, revision, enabled,
1896
+ capabilities_json, window_ms, max_runs, max_units,
1897
+ created_at, updated_at)
1898
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1899
+ ON CONFLICT(principal_type, principal_id) DO UPDATE SET
1900
+ revision = excluded.revision,
1901
+ enabled = excluded.enabled,
1902
+ capabilities_json = excluded.capabilities_json,
1903
+ window_ms = excluded.window_ms,
1904
+ max_runs = excluded.max_runs,
1905
+ max_units = excluded.max_units,
1906
+ updated_at = excluded.updated_at`,
1907
+ )
1908
+ .run(
1909
+ actor.type,
1910
+ actor.id,
1911
+ revision,
1912
+ active ? 1 : 0,
1913
+ canonicalJson(allowedCapabilities, "authorityPolicy.capabilities"),
1914
+ window,
1915
+ runs,
1916
+ units,
1917
+ current?.created_at ?? now,
1918
+ now,
1919
+ );
1920
+ return this.getAuthorityPolicy(actor);
1921
+ });
1922
+ }
1923
+
1924
+ getAuthorityPolicy(principal) {
1925
+ this._assertOpen();
1926
+ const actor = normalizeAuthorityPrincipal(principal);
1927
+ return mapAuthorityPolicy(
1928
+ this.db
1929
+ .prepare(
1930
+ `SELECT * FROM scheduler_authority_policies
1931
+ WHERE principal_type = ? AND principal_id = ?`,
1932
+ )
1933
+ .get(actor.type, actor.id),
1934
+ );
1935
+ }
1936
+
1937
+ ensureAuthorityPolicy(
1938
+ authority,
1939
+ {
1940
+ windowMs = DEFAULT_AUTHORITY_WINDOW_MS,
1941
+ maxRuns = DEFAULT_AUTHORITY_MAX_RUNS,
1942
+ maxUnits = DEFAULT_AUTHORITY_MAX_UNITS,
1943
+ } = {},
1944
+ ) {
1945
+ const normalized = normalizeAuthorityEnvelope(authority);
1946
+ const current = this.getAuthorityPolicy(normalized.principal);
1947
+ if (current) return current;
1948
+ try {
1949
+ return this.setAuthorityPolicy(normalized.principal, {
1950
+ capabilities: normalized.requestedCapabilities,
1951
+ windowMs,
1952
+ maxRuns,
1953
+ maxUnits,
1954
+ expectedRevision: 0,
1955
+ });
1956
+ } catch (error) {
1957
+ if (error?.code !== "SCHEDULER_AUTHORITY_POLICY_CONFLICT") throw error;
1958
+ const raced = this.getAuthorityPolicy(normalized.principal);
1959
+ if (raced) return raced;
1960
+ throw error;
1961
+ }
1962
+ }
1963
+
1964
+ getAuthorityReservation(occurrenceId) {
1965
+ this._assertOpen();
1966
+ const id = normalizeIdentifier(occurrenceId, "occurrenceId");
1967
+ return mapAuthorityReservation(
1968
+ this.db
1969
+ .prepare(
1970
+ `SELECT * FROM scheduler_authority_reservations
1971
+ WHERE occurrence_id = ?`,
1972
+ )
1973
+ .get(id),
1974
+ );
1975
+ }
1976
+
1977
+ reserveAuthority({ occurrenceId, policyRevision, units = 1 } = {}) {
1978
+ const id = normalizeIdentifier(occurrenceId, "occurrenceId");
1979
+ const expectedPolicyRevision = normalizeAuthorityBudgetLimit(
1980
+ policyRevision,
1981
+ "policyRevision",
1982
+ { maximum: Number.MAX_SAFE_INTEGER },
1983
+ );
1984
+ const requestedUnits = normalizeAuthorityBudgetLimit(units, "units");
1985
+ const now = this._now();
1986
+ return this._write(() => {
1987
+ const occurrence = mapOccurrence(this.statements.getOccurrence.get(id));
1988
+ if (!occurrence) {
1989
+ throw new SchedulerKernelError(
1990
+ "SCHEDULER_NOT_FOUND",
1991
+ `Scheduler occurrence does not exist: ${id}`,
1992
+ );
1993
+ }
1994
+ if (occurrence.status !== OCCURRENCE_STATUS.RUNNING) {
1995
+ throw new SchedulerKernelError(
1996
+ "SCHEDULER_AUTHORITY_OCCURRENCE_NOT_RUNNING",
1997
+ `Scheduler authority may only reserve a running occurrence: ${id}`,
1998
+ );
1999
+ }
2000
+ const existing = this.db
2001
+ .prepare(
2002
+ `SELECT * FROM scheduler_authority_reservations
2003
+ WHERE occurrence_id = ?`,
2004
+ )
2005
+ .get(id);
2006
+ if (existing) {
2007
+ const reservation = mapAuthorityReservation(existing);
2008
+ if (
2009
+ reservation.jobId !== occurrence.jobId ||
2010
+ reservation.principal.type !== occurrence.authority.principal.type ||
2011
+ reservation.principal.id !== occurrence.authority.principal.id ||
2012
+ reservation.policyRevision !== expectedPolicyRevision ||
2013
+ reservation.units !== requestedUnits
2014
+ ) {
2015
+ throw new SchedulerKernelError(
2016
+ "SCHEDULER_AUTHORITY_RESERVATION_MISMATCH",
2017
+ `Scheduler authority reservation identity is mismatched: ${id}`,
2018
+ );
2019
+ }
2020
+ return { ...reservation, deduplicated: true };
2021
+ }
2022
+ const principal = occurrence.authority.principal;
2023
+ const policyRow = this.db
2024
+ .prepare(
2025
+ `SELECT * FROM scheduler_authority_policies
2026
+ WHERE principal_type = ? AND principal_id = ?`,
2027
+ )
2028
+ .get(principal.type, principal.id);
2029
+ const policy = mapAuthorityPolicy(policyRow);
2030
+ if (!policy || !policy.enabled) {
2031
+ throw new SchedulerKernelError(
2032
+ "SCHEDULER_AUTHORITY_POLICY_REQUIRED",
2033
+ `Scheduler authority policy is missing or disabled: ${principal.type}:${principal.id}`,
2034
+ );
2035
+ }
2036
+ if (policy.revision !== expectedPolicyRevision) {
2037
+ throw new SchedulerKernelError(
2038
+ "SCHEDULER_AUTHORITY_POLICY_STALE",
2039
+ `Scheduler authority policy changed before reservation: ${principal.type}:${principal.id}`,
2040
+ {
2041
+ expectedRevision: expectedPolicyRevision,
2042
+ actualRevision: policy.revision,
2043
+ },
2044
+ );
2045
+ }
2046
+ const denied = occurrence.authority.requestedCapabilities.filter(
2047
+ (capability) =>
2048
+ !policy.capabilities.includes("*") &&
2049
+ !policy.capabilities.includes(capability),
2050
+ );
2051
+ if (denied.length > 0) {
2052
+ throw new SchedulerKernelError(
2053
+ "SCHEDULER_AUTHORITY_PERMISSION_DENIED",
2054
+ `Scheduler authority capability was denied: ${principal.type}:${principal.id}`,
2055
+ { denied },
2056
+ );
2057
+ }
2058
+ const windowStartedAt =
2059
+ Math.floor(now / policy.windowMs) * policy.windowMs;
2060
+ const usage = this.db
2061
+ .prepare(
2062
+ `SELECT runs, units FROM scheduler_authority_usage
2063
+ WHERE principal_type = ? AND principal_id = ?
2064
+ AND policy_revision = ? AND window_started_at = ?`,
2065
+ )
2066
+ .get(
2067
+ principal.type,
2068
+ principal.id,
2069
+ policy.revision,
2070
+ windowStartedAt,
2071
+ ) ?? { runs: 0, units: 0 };
2072
+ const reservations = this.db
2073
+ .prepare(
2074
+ `SELECT COUNT(*) AS runs, COALESCE(SUM(units), 0) AS units
2075
+ FROM scheduler_authority_reservations
2076
+ WHERE principal_type = ? AND principal_id = ?
2077
+ AND policy_revision = ? AND window_started_at = ?`,
2078
+ )
2079
+ .get(principal.type, principal.id, policy.revision, windowStartedAt);
2080
+ if (
2081
+ reservations.runs !== usage.runs ||
2082
+ reservations.units !== usage.units
2083
+ ) {
2084
+ throw new SchedulerKernelError(
2085
+ "SCHEDULER_AUTHORITY_BUDGET_STATE_INVALID",
2086
+ `Scheduler authority usage does not match reservations: ${principal.type}:${principal.id}`,
2087
+ { usage, reservations },
2088
+ );
2089
+ }
2090
+ const nextRuns = usage.runs + 1;
2091
+ const nextUnits = usage.units + requestedUnits;
2092
+ if (nextRuns > policy.maxRuns || nextUnits > policy.maxUnits) {
2093
+ throw new SchedulerKernelError(
2094
+ "SCHEDULER_AUTHORITY_BUDGET_EXHAUSTED",
2095
+ `Scheduler authority budget is exhausted: ${principal.type}:${principal.id}`,
2096
+ {
2097
+ windowStartedAt,
2098
+ windowEndsAt: windowStartedAt + policy.windowMs,
2099
+ usedRuns: usage.runs,
2100
+ usedUnits: usage.units,
2101
+ requestedUnits,
2102
+ maxRuns: policy.maxRuns,
2103
+ maxUnits: policy.maxUnits,
2104
+ },
2105
+ );
2106
+ }
2107
+ this.db
2108
+ .prepare(
2109
+ `INSERT INTO scheduler_authority_usage
2110
+ (principal_type, principal_id, policy_revision,
2111
+ window_started_at, runs, units, updated_at)
2112
+ VALUES (?, ?, ?, ?, ?, ?, ?)
2113
+ ON CONFLICT(principal_type, principal_id, policy_revision,
2114
+ window_started_at) DO UPDATE SET
2115
+ runs = excluded.runs,
2116
+ units = excluded.units,
2117
+ updated_at = excluded.updated_at`,
2118
+ )
2119
+ .run(
2120
+ principal.type,
2121
+ principal.id,
2122
+ policy.revision,
2123
+ windowStartedAt,
2124
+ nextRuns,
2125
+ nextUnits,
2126
+ now,
2127
+ );
2128
+ this.db
2129
+ .prepare(
2130
+ `INSERT INTO scheduler_authority_reservations
2131
+ (occurrence_id, job_id, principal_type, principal_id,
2132
+ policy_revision, window_started_at, units, status,
2133
+ outcome_json, created_at, settled_at)
2134
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'reserved', NULL, ?, NULL)`,
2135
+ )
2136
+ .run(
2137
+ id,
2138
+ occurrence.jobId,
2139
+ principal.type,
2140
+ principal.id,
2141
+ policy.revision,
2142
+ windowStartedAt,
2143
+ requestedUnits,
2144
+ now,
2145
+ );
2146
+ return {
2147
+ ...this.getAuthorityReservation(id),
2148
+ deduplicated: false,
2149
+ };
2150
+ });
2151
+ }
2152
+
1421
2153
  schemaInfo() {
1422
2154
  this._assertOpen();
1423
2155
  return {