chainlesschain 0.163.4 → 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 (200) 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-DB5X7qqV.js → AIOps-DDIUofA8.js} +1 -1
  7. package/src/assets/web-panel/assets/{ActionButton-DxHRtKLk.js → ActionButton-Bm-5xYRn.js} +1 -1
  8. package/src/assets/web-panel/assets/{Analytics-DIH5QrxT.js → Analytics-D5UH4ljI.js} +3 -3
  9. package/src/assets/web-panel/assets/{AppLayout-uu0oxVz7.js → AppLayout-D_ifjOOP.js} +5 -5
  10. package/src/assets/web-panel/assets/{Artifacts-BNSMiW9Y.js → Artifacts-BY1mXdC7.js} +1 -1
  11. package/src/assets/web-panel/assets/{Audit-DcAzB4_x.js → Audit-Bujr4Ml3.js} +1 -1
  12. package/src/assets/web-panel/assets/{BackgroundAgents-ClSsvjIc.js → BackgroundAgents-sioKSTcC.js} +1 -1
  13. package/src/assets/web-panel/assets/{Backup-BMciY0js.js → Backup-DDOAK8RK.js} +1 -1
  14. package/src/assets/web-panel/assets/{BaseInput-Hn3b57Sh.js → BaseInput-CtCsVVke.js} +1 -1
  15. package/src/assets/web-panel/assets/{Chat-CoSlC53V.js → Chat-DpNj0vnd.js} +4 -4
  16. package/src/assets/web-panel/assets/{ChatBubbleRenderer-CFevJ3So.js → ChatBubbleRenderer-CZta3Ftr.js} +1 -1
  17. package/src/assets/web-panel/assets/{Checkbox-B4L4Eu4w.js → Checkbox-DoHVNSXq.js} +1 -1
  18. package/src/assets/web-panel/assets/{Codegen-C77FgRxF.js → Codegen-DlVl7Qmv.js} +1 -1
  19. package/src/assets/web-panel/assets/{Col-BrLtJLH3.js → Col-GIqoPYty.js} +1 -1
  20. package/src/assets/web-panel/assets/{Community-Be7pXXhA.js → Community-CIAPEVji.js} +1 -1
  21. package/src/assets/web-panel/assets/{Compact-DuGPecjo.js → Compact-BN7JiI_h.js} +1 -1
  22. package/src/assets/web-panel/assets/{Compliance-CzS66mjP.js → Compliance-CqUWCM9B.js} +1 -1
  23. package/src/assets/web-panel/assets/{Cowork-CEhjCoyK.js → Cowork-A4iPp7A4.js} +3 -3
  24. package/src/assets/web-panel/assets/{Cron-D7ir8Vja.js → Cron-B2zeXXWb.js} +2 -2
  25. package/src/assets/web-panel/assets/{Crosschain-BcXmuLxf.js → Crosschain-BR2xmWr8.js} +1 -1
  26. package/src/assets/web-panel/assets/{DID-Cto1dtBy.js → DID-BzFDjrIv.js} +2 -2
  27. package/src/assets/web-panel/assets/{Dashboard-B7KjgwOX.js → Dashboard-DKtDo3yA.js} +2 -2
  28. package/src/assets/web-panel/assets/{Dropdown-COMqxijg.js → Dropdown-RNx43_YG.js} +1 -1
  29. package/src/assets/web-panel/assets/{EmailListRenderer-BZrya-nf.js → EmailListRenderer-SwMt4Rcy.js} +1 -1
  30. package/src/assets/web-panel/assets/{FamilyGuardDashboard-AmVeMq0J.js → FamilyGuardDashboard-CsYebZ4X.js} +1 -1
  31. package/src/assets/web-panel/assets/{Federation-kZU8c-27.js → Federation-BmRKvBTy.js} +1 -1
  32. package/src/assets/web-panel/assets/{FormItemContext-sOV22v2-.js → FormItemContext-CVxGTA-0.js} +1 -1
  33. package/src/assets/web-panel/assets/{GenericCardRenderer-BUuLtGCz.js → GenericCardRenderer-x2iXpyB7.js} +1 -1
  34. package/src/assets/web-panel/assets/{Git-K-ZxsLsH.js → Git-DfqbkiOP.js} +2 -2
  35. package/src/assets/web-panel/assets/{Governance-D3OEnawI.js → Governance-RuHyyj5O.js} +1 -1
  36. package/src/assets/web-panel/assets/{Inference-Bll0gRmP.js → Inference-Bn7uZ7rI.js} +1 -1
  37. package/src/assets/web-panel/assets/{KnowledgeGraph-DBXwjr53.js → KnowledgeGraph-DlP7XefZ.js} +1 -1
  38. package/src/assets/web-panel/assets/{Logs-CEzPwEfP.js → Logs-BYNdOiCd.js} +2 -2
  39. package/src/assets/web-panel/assets/MarkdownRenderer-aMtgN-R6.js +1 -0
  40. package/src/assets/web-panel/assets/{Marketplace-BYyRyOPz.js → Marketplace-JQYmDqM8.js} +1 -1
  41. package/src/assets/web-panel/assets/{McpTools-DVu8w_rM.js → McpTools-Cyx4yZ-9.js} +4 -4
  42. package/src/assets/web-panel/assets/{Memory-tenuZ_hO.js → Memory-VDuGZTN2.js} +2 -2
  43. package/src/assets/web-panel/assets/{MobileBridge-Bxgy4ozS.js → MobileBridge-_yKo97gi.js} +1 -1
  44. package/src/assets/web-panel/assets/{MobileProjects-Dgx1D6sc.js → MobileProjects-CXRXW3q7.js} +1 -1
  45. package/src/assets/web-panel/assets/{Mtc-CRfJ8r8j.js → Mtc-acTGtfLH.js} +5 -5
  46. package/src/assets/web-panel/assets/{MtcAudit-BBJq4Aiz.js → MtcAudit-ClpjXfkY.js} +4 -4
  47. package/src/assets/web-panel/assets/{Multisig-B3ii9lai.js → Multisig-_6Tn3o96.js} +3 -3
  48. package/src/assets/web-panel/assets/{NLProgramming-FIi-Cubv.js → NLProgramming-CK7pbZzP.js} +1 -1
  49. package/src/assets/web-panel/assets/{Notes-DIq8MPf1.js → Notes-Czy6V_P8.js} +3 -3
  50. package/src/assets/web-panel/assets/{NotificationSettings-BFOD9eGd.js → NotificationSettings-Cqx-UVv3.js} +1 -1
  51. package/src/assets/web-panel/assets/{OrderTableRenderer--41u7vre.js → OrderTableRenderer-C69WDxGy.js} +1 -1
  52. package/src/assets/web-panel/assets/{Organization-B_0AWztW.js → Organization-DFnA9vLQ.js} +4 -4
  53. package/src/assets/web-panel/assets/{Overflow-B6I7TuXv.js → Overflow-ReZkk2yT.js} +1 -1
  54. package/src/assets/web-panel/assets/{P2P-Cdf6qens.js → P2P-DzjSI0aI.js} +2 -2
  55. package/src/assets/web-panel/assets/{PdhVaultBrowser-B_0FcQJY.js → PdhVaultBrowser-DKTf5xIP.js} +3 -3
  56. package/src/assets/web-panel/assets/{Permissions-6nFBjsQp.js → Permissions-DhQ5klhR.js} +3 -3
  57. package/src/assets/web-panel/assets/{PersonalDataHub-nQYV7xpn.js → PersonalDataHub-CUL1iHrW.js} +3 -3
  58. package/src/assets/web-panel/assets/{Pipeline-CBVod5jm.js → Pipeline-DU88iu4N.js} +1 -1
  59. package/src/assets/web-panel/assets/{Privacy-D52jW1Nt.js → Privacy-Bw-sYfyK.js} +1 -1
  60. package/src/assets/web-panel/assets/{ProjectInit-Bdx3BYhf.js → ProjectInit-Bq3Q5y0z.js} +2 -2
  61. package/src/assets/web-panel/assets/{ProjectSettings-b7xr4DUa.js → ProjectSettings-C8spzpho.js} +2 -2
  62. package/src/assets/web-panel/assets/{Projects-DhfifCKx.js → Projects-Dd-xuPIc.js} +1 -1
  63. package/src/assets/web-panel/assets/Providers-Bf4eTP9W.js +1 -0
  64. package/src/assets/web-panel/assets/{QrScannerModal-FQKSmc3e.js → QrScannerModal-Dme9BDU1.js} +1 -1
  65. package/src/assets/web-panel/assets/{QuickAsk-CSTrhzcF.js → QuickAsk-CIXfv1rE.js} +1 -1
  66. package/src/assets/web-panel/assets/{Recommend-CxMABOUh.js → Recommend-CIxvdeZ-.js} +1 -1
  67. package/src/assets/web-panel/assets/{RemoteSession-Cv2T8yRa.js → RemoteSession-C6k7MExI.js} +2 -2
  68. package/src/assets/web-panel/assets/{Reputation-CE1k4Vvu.js → Reputation-CISDz2bl.js} +1 -1
  69. package/src/assets/web-panel/assets/{Row-BNgNcAV_.js → Row-CdnDxXyf.js} +1 -1
  70. package/src/assets/web-panel/assets/{RssFeed-5ZiBZ3ph.js → RssFeed-C-QpRHsl.js} +3 -3
  71. package/src/assets/web-panel/assets/{Search-mDOIF1UV.js → Search-DBjaGzSn.js} +1 -1
  72. package/src/assets/web-panel/assets/{Security-CtuCTC-e.js → Security-CQKukOeE.js} +3 -3
  73. package/src/assets/web-panel/assets/{Services-DxFO2Zg7.js → Services-DZz86jWb.js} +2 -2
  74. package/src/assets/web-panel/assets/{Skeleton-CKmKkH9y.js → Skeleton-jb8RMK0d.js} +1 -1
  75. package/src/assets/web-panel/assets/{Skills-Bmzl9qkk.js → Skills-CdY76fZG.js} +1 -1
  76. package/src/assets/web-panel/assets/{Sla-9RqaWHOC.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-CvNuCeBT.js → SyncSettings-qvimGO0O.js} +2 -2
  79. package/src/assets/web-panel/assets/{Tasks-B_sLdcnw.js → Tasks-Dng5LPJC.js} +1 -1
  80. package/src/assets/web-panel/assets/{Templates-D9_YijdK.js → Templates-Bwqq5Wll.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tenant-Cuc7BUSP.js → Tenant-DNWuwP7Z.js} +1 -1
  82. package/src/assets/web-panel/assets/{Terminal-6VYRnJgd.js → Terminal-B-6Othpp.js} +2 -2
  83. package/src/assets/web-panel/assets/{TimelineRenderer-GtK8xd8Y.js → TimelineRenderer-DKpKZfzx.js} +1 -1
  84. package/src/assets/web-panel/assets/{Tokens-BZ94Lt5W.js → Tokens-DzfGMd2J.js} +1 -1
  85. package/src/assets/web-panel/assets/{Trigger-DEcZgDaX.js → Trigger-BXwUi74J.js} +1 -1
  86. package/src/assets/web-panel/assets/{Trust-CC3z9THe.js → Trust-BNzUq4ui.js} +1 -1
  87. package/src/assets/web-panel/assets/{UkeySign-Cvtw8cV3.js → UkeySign-BYugxww4.js} +1 -1
  88. package/src/assets/web-panel/assets/{VideoEditing-CL4e6Bn3.js → VideoEditing-DdauhtSJ.js} +1 -1
  89. package/src/assets/web-panel/assets/{Wallet-ycEemP27.js → Wallet-CSC9bCe_.js} +4 -4
  90. package/src/assets/web-panel/assets/{WebAuthn-DgZ4X_aw.js → WebAuthn-Q8AN3bTO.js} +5 -5
  91. package/src/assets/web-panel/assets/{WorkflowEditor-D0qMhSv7.js → WorkflowEditor-d-LBGCuA.js} +1 -1
  92. package/src/assets/web-panel/assets/{chat-DsAg01Zn.js → chat-BLVDCgsZ.js} +1 -1
  93. package/src/assets/web-panel/assets/{colors-B0oqAhrQ.js → colors-BZlPSEHl.js} +1 -1
  94. package/src/assets/web-panel/assets/{compact-item-CmX0Rabz.js → compact-item-IWseP6R6.js} +1 -1
  95. package/src/assets/web-panel/assets/{createContext-Be4cn1Ub.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-D5v12kVq.js → hasIn-DLQZA_Wn.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-BA4oulaP.js → index-1jeAudHd.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-DM724NgU.js → index-BEu2lMBy.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-5LGY0JbG.js → index-BOZJBTWZ.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-DikJ0Qqe.js → index-BSjilZ9L.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-BqltWojQ.js → index-BZs5bBnk.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-7VKDn-t1.js → index-B_N4-Gq9.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-BXGSbelg.js → index-Bbj2NS_I.js} +3 -3
  105. package/src/assets/web-panel/assets/{index-CGiIyTZw.js → index-BhA3QEni.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-BKmsS2jj.js → index-Bxp7uDD8.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-B3mUHuVc.js → index-C1ifZtXo.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-BBvyHguy.js → index-C2FqfMvU.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-q_fQ94SE.js → index-C5dgnilC.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-D5YVopyr.js → index-CINJQEj6.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-0eWLkQKT.js → index-COev0W2g.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-BoAIAZ5C.js → index-CT9vVN-X.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-CqoCZ5vR.js → index-CZDekj7T.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-301ogenv.js → index-CarMTIyQ.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-Dn2dSdXx.js → index-CbQNL0ue.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-DKUBj36_.js → index-CpBaJ3TD.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BBREnn1V.js → index-DB0MJgF_.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-BRIqLy1m.js → index-DOoQpuQY.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-jK3ax6Z6.js → index-DTbxp4UH.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-B1YGjOWC.js → index-DUq0c7gq.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CyyGTMKd.js → index-DZpYeqrh.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-_JjRLSD0.js → index-D_LQodeJ.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-DyUaFWEk.js → index-DalA0TKC.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-BaE0Ui07.js → index-DjWnONdy.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-CyWBir7N.js → index-Dkwsi1Kn.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-qMuZ-NRR.js → index-DocEhinm.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-Ckrk3pIV.js → index-DpVrK6fr.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-DQRsQen0.js → index-Dy0R5ZED.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-frSojpHj.js → index-M7wxfAbm.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-D0BSkxYQ.js → index-N_GoWZX4.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-8WvYGnON.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-Hfe_dEGx.js → index-gt8bNefW.js} +1 -1
  135. package/src/assets/web-panel/assets/{index-CZ2wUEgr.js → index-hwVDeoC0.js} +1 -1
  136. package/src/assets/web-panel/assets/{index-CZpfzcuy.js → index-jWb70ZgZ.js} +1 -1
  137. package/src/assets/web-panel/assets/{initDefaultProps-D4VdhJ9_.js → initDefaultProps-Co67Kqmb.js} +1 -1
  138. package/src/assets/web-panel/assets/{motion-CZJA0MQW.js → motion-BMOxQSF_.js} +1 -1
  139. package/src/assets/web-panel/assets/{move-BqEs7Sb1.js → move-C5fWDw-N.js} +1 -1
  140. package/src/assets/web-panel/assets/{mtc-parser-BpHEtsu1.js → mtc-parser-BJevpL0e.js} +1 -1
  141. package/src/assets/web-panel/assets/{omit-BW94tY9W.js → omit-BCzNU8s6.js} +1 -1
  142. package/src/assets/web-panel/assets/{pickAttrs-BnBjPWP9.js → pickAttrs-CybWvDPk.js} +1 -1
  143. package/src/assets/web-panel/assets/{placementArrow-uUkjQ_hu.js → placementArrow-BMWS9HnW.js} +1 -1
  144. package/src/assets/web-panel/assets/{responsiveObserve-B-klUqXK.js → responsiveObserve-BoL2UnGY.js} +1 -1
  145. package/src/assets/web-panel/assets/{slide-CvY1cNAs.js → slide-DWNfXraU.js} +1 -1
  146. package/src/assets/web-panel/assets/{statusUtils-Ix5sEDT2.js → statusUtils-BquTM3tX.js} +1 -1
  147. package/src/assets/web-panel/assets/{styleChecker-C8qcXOne.js → styleChecker-BEkCleim.js} +1 -1
  148. package/src/assets/web-panel/assets/{useFlexGapSupport-KNF6Kj8f.js → useFlexGapSupport-Oecnq7EA.js} +1 -1
  149. package/src/assets/web-panel/assets/{useFs-DGJr345n.js → useFs-CkDffJ4F.js} +1 -1
  150. package/src/assets/web-panel/assets/{usePersonalDataHub-BboN99rI.js → usePersonalDataHub-qbvKXesB.js} +1 -1
  151. package/src/assets/web-panel/assets/{vnode-Bgt8Vgte.js → vnode-C6v8n6mA.js} +1 -1
  152. package/src/assets/web-panel/assets/{zoom-BhJtErgt.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 +125 -7
  157. package/src/commands/automation.js +462 -3
  158. package/src/commands/background-session.js +102 -1
  159. package/src/commands/cowork.js +30 -13
  160. package/src/commands/loop.js +167 -36
  161. package/src/commands/routine.js +50 -13
  162. package/src/commands/scheduler-daemon.js +450 -0
  163. package/src/data/changelog.json +27 -1
  164. package/src/lib/agent-schedule-store.js +509 -36
  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 +555 -39
  172. package/src/lib/cowork-observe.js +5 -1
  173. package/src/lib/loop.js +37 -2
  174. package/src/lib/micro-compact.js +11 -5
  175. package/src/lib/permission-engine.js +14 -7
  176. package/src/lib/process-execution-broker/index.js +14 -3
  177. package/src/lib/public-ide-capability-manifest.js +2 -0
  178. package/src/lib/routine-store.js +242 -68
  179. package/src/lib/scheduler-kernel/agenda-adapter.js +812 -0
  180. package/src/lib/scheduler-kernel/authority-resolver.js +163 -0
  181. package/src/lib/scheduler-kernel/automation-adapter.js +592 -0
  182. package/src/lib/scheduler-kernel/automation-event-adapter.js +721 -0
  183. package/src/lib/scheduler-kernel/contract.js +11 -3
  184. package/src/lib/scheduler-kernel/cowork-cron-adapter.js +820 -0
  185. package/src/lib/scheduler-kernel/loop-adapter.js +378 -0
  186. package/src/lib/scheduler-kernel/routine-adapter.js +812 -0
  187. package/src/lib/scheduler-kernel/runtime.js +527 -0
  188. package/src/lib/scheduler-kernel/service.js +328 -0
  189. package/src/lib/scheduler-kernel/store.js +940 -98
  190. package/src/lib/session-message-provenance.js +4 -1
  191. package/src/repl/agent-repl.js +24 -5
  192. package/src/runtime/agent-core.js +38 -17
  193. package/src/runtime/coding-agent-contract-shared.cjs +5 -0
  194. package/src/workers/background-agent-worker.js +91 -28
  195. package/src/assets/web-panel/assets/MarkdownRenderer-CLmCbHx6.js +0 -1
  196. package/src/assets/web-panel/assets/Providers-Dy_PgDMr.js +0 -1
  197. package/src/assets/web-panel/assets/SpeechSettings-CGdzFIYw.js +0 -1
  198. package/src/assets/web-panel/assets/devWarning-CP2qzG6D.js +0 -1
  199. package/src/assets/web-panel/assets/index-CKB0vbJU.js +0 -1
  200. package/src/assets/web-panel/assets/index-DgoXmwqB.js +0 -1
@@ -39,6 +39,11 @@ import {
39
39
  writeFileSync,
40
40
  } from "node:fs";
41
41
  import { join } from "node:path";
42
+ import {
43
+ nextCronTime as nextAgentCronTime,
44
+ normalizeTimeZone,
45
+ previousCronTime as previousAgentCronTime,
46
+ } from "./agent-schedule-store.js";
42
47
  import { withFileLock } from "./with-file-lock.js";
43
48
 
44
49
  export const _deps = {
@@ -53,6 +58,33 @@ export const _deps = {
53
58
  runTask: null, // injected at runtime to avoid circular import
54
59
  };
55
60
 
61
+ export const COWORK_SCHEDULER_UNKNOWN_LEASE_EXPIRES_AT =
62
+ "9999-12-31T23:59:59.999Z";
63
+
64
+ function coworkSchedulerError(
65
+ code,
66
+ message,
67
+ { retryable = false, retryAt } = {},
68
+ ) {
69
+ const error = new Error(message);
70
+ error.code = code;
71
+ error.retryable = retryable;
72
+ if (retryAt !== undefined) error.retryAt = retryAt;
73
+ return error;
74
+ }
75
+
76
+ function normalizedDate(value, label) {
77
+ const date =
78
+ value instanceof Date ? new Date(value.getTime()) : new Date(value);
79
+ if (!Number.isFinite(date.getTime())) {
80
+ throw coworkSchedulerError(
81
+ "COWORK_SCHEDULER_TIME_INVALID",
82
+ `${label} must be a valid date`,
83
+ );
84
+ }
85
+ return date;
86
+ }
87
+
56
88
  // ─── Cron parser ─────────────────────────────────────────────────────────────
57
89
 
58
90
  const FIELD_RANGES = [
@@ -64,6 +96,56 @@ const FIELD_RANGES = [
64
96
  ];
65
97
 
66
98
  const SECOND_RANGE = [0, 59];
99
+ const CRON_TIME_ZONE_FORMATTERS = new Map();
100
+
101
+ function timeZoneFormatter(timeZone) {
102
+ let formatter = CRON_TIME_ZONE_FORMATTERS.get(timeZone);
103
+ if (!formatter) {
104
+ formatter = new Intl.DateTimeFormat("en-US-u-ca-gregory-nu-latn", {
105
+ timeZone,
106
+ year: "numeric",
107
+ month: "2-digit",
108
+ day: "2-digit",
109
+ hour: "2-digit",
110
+ minute: "2-digit",
111
+ second: "2-digit",
112
+ hourCycle: "h23",
113
+ });
114
+ CRON_TIME_ZONE_FORMATTERS.set(timeZone, formatter);
115
+ }
116
+ return formatter;
117
+ }
118
+
119
+ function timeZoneParts(epochMs, timeZone) {
120
+ const values = {};
121
+ for (const part of timeZoneFormatter(timeZone).formatToParts(epochMs)) {
122
+ if (part.type !== "literal") values[part.type] = Number(part.value);
123
+ }
124
+ return {
125
+ year: values.year,
126
+ month: values.month,
127
+ day: values.day,
128
+ hour: values.hour,
129
+ minute: values.minute,
130
+ second: values.second,
131
+ dayOfWeek: new Date(
132
+ Date.UTC(values.year, values.month - 1, values.day),
133
+ ).getUTCDay(),
134
+ };
135
+ }
136
+
137
+ function cronDateParts(date, timeZone) {
138
+ if (timeZone) return timeZoneParts(date.getTime(), timeZone);
139
+ return {
140
+ year: date.getFullYear(),
141
+ month: date.getMonth() + 1,
142
+ day: date.getDate(),
143
+ hour: date.getHours(),
144
+ minute: date.getMinutes(),
145
+ second: date.getSeconds(),
146
+ dayOfWeek: date.getDay(),
147
+ };
148
+ }
67
149
 
68
150
  /**
69
151
  * Non-standard alias → 5-field expression. Aliases never carry seconds.
@@ -155,7 +237,7 @@ export function parseCronField(field, [min, max]) {
155
237
  * @param {string} expr
156
238
  * @returns {((date: Date) => boolean) & { hasSeconds: boolean }}
157
239
  */
158
- export function parseCron(expr) {
240
+ export function parseCron(expr, options = {}) {
159
241
  if (typeof expr !== "string") {
160
242
  throw new Error("cron expression must be a string");
161
243
  }
@@ -167,6 +249,9 @@ export function parseCron(expr) {
167
249
  );
168
250
  }
169
251
  const hasSeconds = parts.length === 6;
252
+ const timeZone = normalizeTimeZone(
253
+ typeof options === "string" ? options : options?.timeZone,
254
+ );
170
255
  let second = null;
171
256
  let minute, hour, dom, month, dow;
172
257
  if (hasSeconds) {
@@ -187,12 +272,15 @@ export function parseCron(expr) {
187
272
  const dowRestricted = dowField !== "*";
188
273
 
189
274
  function matches(date) {
190
- const s = date.getSeconds();
191
- const m = date.getMinutes();
192
- const h = date.getHours();
193
- const D = date.getDate();
194
- const M = date.getMonth() + 1; // JS month is 0-based
195
- const W = date.getDay();
275
+ const value = date instanceof Date ? date : new Date(date);
276
+ if (!Number.isFinite(value.getTime())) return false;
277
+ const civil = cronDateParts(value, timeZone);
278
+ const s = civil.second;
279
+ const m = civil.minute;
280
+ const h = civil.hour;
281
+ const D = civil.day;
282
+ const M = civil.month;
283
+ const W = civil.dayOfWeek;
196
284
  if (hasSeconds && !second.has(s)) return false;
197
285
  if (!minute.has(m)) return false;
198
286
  if (!hour.has(h)) return false;
@@ -206,6 +294,10 @@ export function parseCron(expr) {
206
294
  return true;
207
295
  }
208
296
  matches.hasSeconds = hasSeconds;
297
+ matches.timeZone = timeZone;
298
+ matches.fields = { second, minute, hour, dom, month, dow };
299
+ matches.domRestricted = domRestricted;
300
+ matches.dowRestricted = dowRestricted;
209
301
  return matches;
210
302
  }
211
303
 
@@ -219,15 +311,153 @@ export function hasSecondsResolution(expr) {
219
311
  }
220
312
 
221
313
  /** Validate a cron expression — returns null if valid, error string otherwise. */
222
- export function validateCron(expr) {
314
+ export function validateCron(expr, options = {}) {
223
315
  try {
224
- parseCron(expr);
316
+ parseCron(expr, options);
225
317
  return null;
226
318
  } catch (err) {
227
319
  return err.message;
228
320
  }
229
321
  }
230
322
 
323
+ /** Return the next real instant for a Cowork 5/6-field cron. */
324
+ export function nextCoworkCronTime(expr, fromMs, options = {}) {
325
+ const matcher = parseCron(expr, options);
326
+ const from = Number(fromMs);
327
+ if (!Number.isFinite(from)) throw new Error("cron start time is invalid");
328
+ const expanded = _expandExpr(expr);
329
+ const parts = expanded.split(/\s+/);
330
+ const minuteFields = matcher.hasSeconds ? parts.slice(1) : parts;
331
+ const minuteExpressions =
332
+ matcher.domRestricted && matcher.dowRestricted
333
+ ? [
334
+ [
335
+ minuteFields[0],
336
+ minuteFields[1],
337
+ minuteFields[2],
338
+ minuteFields[3],
339
+ "*",
340
+ ].join(" "),
341
+ [
342
+ minuteFields[0],
343
+ minuteFields[1],
344
+ "*",
345
+ minuteFields[3],
346
+ minuteFields[4],
347
+ ].join(" "),
348
+ ]
349
+ : [minuteFields.join(" ")];
350
+ const nextMinute = (after) => {
351
+ const candidates = minuteExpressions
352
+ .map((candidate) =>
353
+ nextAgentCronTime(candidate, after, { timeZone: matcher.timeZone }),
354
+ )
355
+ .filter((candidate) => candidate !== null);
356
+ return candidates.length > 0 ? Math.min(...candidates) : null;
357
+ };
358
+ if (!matcher.hasSeconds) return nextMinute(from);
359
+
360
+ const seconds = [...matcher.fields.second].sort((a, b) => a - b);
361
+ let minute = nextMinute(Math.floor(from / 60_000) * 60_000 - 60_000);
362
+ while (minute !== null) {
363
+ for (const second of seconds) {
364
+ const candidate = minute + second * 1_000;
365
+ if (candidate > from) return candidate;
366
+ }
367
+ minute = nextMinute(minute);
368
+ }
369
+ return null;
370
+ }
371
+
372
+ /**
373
+ * Select the latest due instant in `[firstMs, throughMs]`. Six-field schedules
374
+ * advance by matching civil minutes, not every elapsed second, so a long
375
+ * outage cannot turn an every-second schedule into millions of iterations.
376
+ */
377
+ export function latestCoworkCronTime(expr, firstMs, throughMs, options = {}) {
378
+ const first = Number(firstMs);
379
+ const through = Number(throughMs);
380
+ if (!Number.isSafeInteger(first) || !Number.isSafeInteger(through)) {
381
+ throw new Error("cron catch-up bounds are invalid");
382
+ }
383
+ if (first > through) return null;
384
+ const matcher = parseCron(expr, options);
385
+ const seconds = matcher.hasSeconds
386
+ ? [...matcher.fields.second].sort((a, b) => b - a)
387
+ : [0];
388
+ const expanded = _expandExpr(expr);
389
+ const fields = expanded.split(/\s+/);
390
+ const minuteFields = matcher.hasSeconds ? fields.slice(1) : fields;
391
+ const minuteExpressions =
392
+ matcher.domRestricted && matcher.dowRestricted
393
+ ? [
394
+ [
395
+ minuteFields[0],
396
+ minuteFields[1],
397
+ minuteFields[2],
398
+ minuteFields[3],
399
+ "*",
400
+ ].join(" "),
401
+ [
402
+ minuteFields[0],
403
+ minuteFields[1],
404
+ "*",
405
+ minuteFields[3],
406
+ minuteFields[4],
407
+ ].join(" "),
408
+ ]
409
+ : [minuteFields.join(" ")];
410
+
411
+ const throughMinute = Math.floor(through / 60_000) * 60_000;
412
+ for (const second of seconds) {
413
+ const candidate = throughMinute + second * 1_000;
414
+ if (
415
+ candidate >= first &&
416
+ candidate <= through &&
417
+ matcher(new Date(candidate))
418
+ ) {
419
+ return candidate;
420
+ }
421
+ }
422
+
423
+ const previousMinute = Math.max(
424
+ ...minuteExpressions
425
+ .map((candidate) =>
426
+ previousAgentCronTime(candidate, throughMinute, {
427
+ timeZone: matcher.timeZone,
428
+ }),
429
+ )
430
+ .filter((candidate) => candidate !== null),
431
+ );
432
+ if (Number.isFinite(previousMinute)) {
433
+ for (const second of seconds) {
434
+ const candidate = previousMinute + second * 1_000;
435
+ if (candidate >= first && candidate <= through) return candidate;
436
+ }
437
+ }
438
+ return first;
439
+ }
440
+
441
+ function currentCoworkCronTime(schedule, at) {
442
+ const date = normalizedDate(at, "cowork cron observation time");
443
+ const matcher = parseCron(schedule.cron, { timeZone: schedule.timeZone });
444
+ if (!matcher(date)) return null;
445
+ const resolution = matcher.hasSeconds ? 1_000 : 60_000;
446
+ return Math.floor(date.getTime() / resolution) * resolution;
447
+ }
448
+
449
+ function initialCoworkCronNextAt(schedule, at) {
450
+ const current = currentCoworkCronTime(schedule, at);
451
+ if (current !== null) return current;
452
+ return nextCoworkCronTime(
453
+ schedule.cron,
454
+ normalizedDate(at, "now").getTime(),
455
+ {
456
+ timeZone: schedule.timeZone,
457
+ },
458
+ );
459
+ }
460
+
231
461
  // ─── Persistence ─────────────────────────────────────────────────────────────
232
462
 
233
463
  function _scheduleFile(cwd) {
@@ -293,20 +523,26 @@ function _withScheduleLock(cwd, body) {
293
523
 
294
524
  /** Atomically replace the full schedule list under a fail-closed lock. */
295
525
  export function saveSchedules(cwd, schedules) {
296
- return _withScheduleLock(cwd, () =>
297
- _saveSchedulesUnlocked(cwd, schedules),
298
- );
526
+ return _withScheduleLock(cwd, () => _saveSchedulesUnlocked(cwd, schedules));
299
527
  }
300
528
 
301
529
  // ─── CRUD ────────────────────────────────────────────────────────────────────
302
530
 
303
531
  export function addSchedule(cwd, input) {
304
- const { cron, templateId = null, userMessage, files = [] } = input || {};
532
+ const {
533
+ cron,
534
+ timeZone = null,
535
+ templateId = null,
536
+ userMessage,
537
+ files = [],
538
+ } = input || {};
305
539
  if (!userMessage || typeof userMessage !== "string") {
306
540
  throw new Error("userMessage is required");
307
541
  }
308
- const err = validateCron(cron);
542
+ const normalizedTimeZone = normalizeTimeZone(timeZone);
543
+ const err = validateCron(cron, { timeZone: normalizedTimeZone });
309
544
  if (err) throw new Error(`invalid cron: ${err}`);
545
+ const createdAt = _deps.now();
310
546
 
311
547
  // Serialize the read-modify-write across processes (cc+cc / cc+desktop share
312
548
  // the cowork dir) so concurrently-added schedules don't clobber each other.
@@ -315,11 +551,17 @@ export function addSchedule(cwd, input) {
315
551
  const entry = {
316
552
  id: `sch-${crypto.randomUUID().slice(0, 12)}`,
317
553
  cron: cron.trim(),
554
+ ...(normalizedTimeZone ? { timeZone: normalizedTimeZone } : {}),
318
555
  templateId,
319
556
  userMessage,
320
557
  files: Array.isArray(files) ? files : [],
321
558
  enabled: true,
322
- createdAt: _deps.now().toISOString(),
559
+ createdAt: createdAt.toISOString(),
560
+ nextAt: initialCoworkCronNextAt(
561
+ { cron: cron.trim(), timeZone: normalizedTimeZone },
562
+ createdAt,
563
+ ),
564
+ missedRunPolicy: "collapse",
323
565
  lastRunAt: null,
324
566
  lastStatus: null,
325
567
  };
@@ -329,6 +571,73 @@ export function addSchedule(cwd, input) {
329
571
  });
330
572
  }
331
573
 
574
+ /**
575
+ * Upgrade a legacy schedule to durable next-fire state. Upgrade never replays
576
+ * pre-upgrade history: a matching current window is retained, otherwise the
577
+ * first future occurrence becomes authoritative.
578
+ */
579
+ export function ensureScheduleNextAt(cwd, id, at = _deps.now()) {
580
+ return _withScheduleLock(cwd, () => {
581
+ const schedules = loadSchedules(cwd, { failOnMalformed: true });
582
+ const schedule = schedules.find((entry) => entry.id === id);
583
+ if (!schedule) return null;
584
+ if (Object.hasOwn(schedule, "nextAt")) {
585
+ if (
586
+ schedule.nextAt === null ||
587
+ (typeof schedule.nextAt === "number" &&
588
+ Number.isSafeInteger(schedule.nextAt) &&
589
+ schedule.nextAt >= 0)
590
+ ) {
591
+ return schedule;
592
+ }
593
+ throw coworkSchedulerError(
594
+ "COWORK_SCHEDULER_CURSOR_INVALID",
595
+ `Cowork schedule nextAt is invalid: ${id}`,
596
+ );
597
+ }
598
+ const lastRunAt = Date.parse(schedule.lastRunAt || "");
599
+ const observedAt = normalizedDate(at, "cowork cron migration time");
600
+ schedule.nextAt = Number.isSafeInteger(lastRunAt)
601
+ ? nextCoworkCronTime(schedule.cron, lastRunAt, {
602
+ timeZone: schedule.timeZone,
603
+ })
604
+ : initialCoworkCronNextAt(schedule, observedAt);
605
+ schedule.missedRunPolicy = "collapse";
606
+ _saveSchedulesUnlocked(cwd, schedules);
607
+ return schedule;
608
+ });
609
+ }
610
+
611
+ /**
612
+ * CAS-advance one due schedule beyond `from`, collapsing any number of missed
613
+ * periods to the single occurrence that was just durably enqueued.
614
+ */
615
+ export function advanceScheduleNextAt(
616
+ cwd,
617
+ id,
618
+ { expectedNextAt, from = _deps.now() } = {},
619
+ ) {
620
+ return _withScheduleLock(cwd, () => {
621
+ const schedules = loadSchedules(cwd, { failOnMalformed: true });
622
+ const schedule = schedules.find((entry) => entry.id === id);
623
+ if (!schedule) return null;
624
+ if (
625
+ typeof schedule.nextAt !== "number" ||
626
+ schedule.nextAt !== Number(expectedNextAt)
627
+ ) {
628
+ return schedule;
629
+ }
630
+ schedule.nextAt = nextCoworkCronTime(
631
+ schedule.cron,
632
+ normalizedDate(from, "cowork cron advance time").getTime(),
633
+ { timeZone: schedule.timeZone },
634
+ );
635
+ schedule.missedRunPolicy = "collapse";
636
+ _saveSchedulesUnlocked(cwd, schedules);
637
+ return schedule;
638
+ });
639
+ }
640
+
332
641
  export function removeSchedule(cwd, id) {
333
642
  return _withScheduleLock(cwd, () => {
334
643
  const schedules = loadSchedules(cwd, { failOnMalformed: true });
@@ -345,7 +654,12 @@ export function setScheduleEnabled(cwd, id, enabled) {
345
654
  const schedules = loadSchedules(cwd, { failOnMalformed: true });
346
655
  const s = schedules.find((x) => x.id === id);
347
656
  if (!s) return false;
348
- s.enabled = !!enabled;
657
+ const nextEnabled = !!enabled;
658
+ if (nextEnabled && !s.enabled) {
659
+ s.nextAt = initialCoworkCronNextAt(s, _deps.now());
660
+ s.missedRunPolicy = "collapse";
661
+ }
662
+ s.enabled = nextEnabled;
349
663
  _saveSchedulesUnlocked(cwd, schedules);
350
664
  return true;
351
665
  });
@@ -363,6 +677,191 @@ export function updateScheduleRunState(cwd, id, { lastRunAt, lastStatus }) {
363
677
  });
364
678
  }
365
679
 
680
+ /** Return one schedule by id, or null when it does not exist. */
681
+ export function getSchedule(cwd, id) {
682
+ if (typeof id !== "string" || id.length === 0) return null;
683
+ return (
684
+ loadSchedules(cwd, { failOnMalformed: true }).find(
685
+ (schedule) => schedule.id === id,
686
+ ) || null
687
+ );
688
+ }
689
+
690
+ /**
691
+ * Bind a scheduler-kernel occurrence before starting the Cowork task.
692
+ * The same JSONL lock used by the legacy driver closes both old-first and
693
+ * new-first claim races during migration.
694
+ */
695
+ export function bindSchedulerScheduleFire(
696
+ cwd,
697
+ id,
698
+ { deliveryId, occurrenceId, snapshotDigest, attempt, at = _deps.now() } = {},
699
+ ) {
700
+ if (
701
+ typeof deliveryId !== "string" ||
702
+ typeof occurrenceId !== "string" ||
703
+ typeof snapshotDigest !== "string" ||
704
+ deliveryId.length === 0 ||
705
+ occurrenceId.length === 0 ||
706
+ snapshotDigest.length === 0 ||
707
+ !Number.isSafeInteger(attempt) ||
708
+ attempt < 1
709
+ ) {
710
+ throw coworkSchedulerError(
711
+ "COWORK_SCHEDULER_BINDING_INVALID",
712
+ "Cowork scheduler binding requires deliveryId, occurrenceId, snapshotDigest, and a positive attempt",
713
+ );
714
+ }
715
+ const now = normalizedDate(at, "cowork scheduler binding time");
716
+ const nowMs = now.getTime();
717
+ return _withScheduleLock(cwd, () => {
718
+ const schedules = loadSchedules(cwd, { failOnMalformed: true });
719
+ const schedule = schedules.find((entry) => entry.id === id);
720
+ if (!schedule) {
721
+ throw coworkSchedulerError(
722
+ "COWORK_SCHEDULER_SCHEDULE_NOT_FOUND",
723
+ `Cowork schedule does not exist: ${id}`,
724
+ );
725
+ }
726
+ const prior = schedule.schedulerExecution;
727
+ if (prior?.status === "running") {
728
+ throw coworkSchedulerError(
729
+ "COWORK_SCHEDULER_OUTCOME_UNKNOWN",
730
+ `Cowork schedule outcome is unknown: ${id}`,
731
+ );
732
+ }
733
+ if (
734
+ prior?.status === "succeeded" &&
735
+ prior.occurrenceId === occurrenceId &&
736
+ prior.snapshotDigest === snapshotDigest
737
+ ) {
738
+ return schedule;
739
+ }
740
+ // A legacy runner may have completed after the kernel enqueued this fire
741
+ // but before it acquired the JSONL lock. Its durable delivery id is
742
+ // terminal evidence for the same logical fire, so never overwrite it with
743
+ // a new scheduler claim.
744
+ if (schedule.lastDeliveryId === deliveryId) return schedule;
745
+
746
+ const active = schedule.activeDelivery;
747
+ const activeExpiresAt = Date.parse(active?.leaseExpiresAt || "");
748
+ if (active && Number.isFinite(activeExpiresAt) && activeExpiresAt > nowMs) {
749
+ throw coworkSchedulerError(
750
+ "COWORK_SCHEDULER_LEGACY_CLAIM_ACTIVE",
751
+ `Cowork schedule is already claimed: ${id}`,
752
+ { retryable: true, retryAt: activeExpiresAt },
753
+ );
754
+ }
755
+
756
+ const fence =
757
+ Math.max(
758
+ Number(schedule.deliveryFence) || 0,
759
+ Number(active?.fence) || 0,
760
+ ) + 1;
761
+ schedule.deliveryFence = fence;
762
+ schedule.activeDelivery = {
763
+ deliveryId,
764
+ ownerId: `scheduler:${occurrenceId}:${attempt}`,
765
+ fence,
766
+ claimedAt: now.toISOString(),
767
+ leaseExpiresAt: COWORK_SCHEDULER_UNKNOWN_LEASE_EXPIRES_AT,
768
+ };
769
+ schedule.schedulerExecution = {
770
+ deliveryId,
771
+ occurrenceId,
772
+ snapshotDigest,
773
+ attempt,
774
+ status: "running",
775
+ startedAt: now.toISOString(),
776
+ };
777
+ _saveSchedulesUnlocked(cwd, schedules);
778
+ return schedule;
779
+ });
780
+ }
781
+
782
+ /**
783
+ * Atomically settle scheduler evidence and the legacy Cowork delivery state.
784
+ * A known failure may keep a short legacy-visible fence until retryAt; an
785
+ * unknown outcome never calls this function and therefore remains fenced.
786
+ */
787
+ export function completeSchedulerScheduleFire(
788
+ cwd,
789
+ id,
790
+ {
791
+ deliveryId,
792
+ occurrenceId,
793
+ snapshotDigest,
794
+ attempt,
795
+ outcome,
796
+ result = null,
797
+ error = null,
798
+ retryAt,
799
+ at = _deps.now(),
800
+ } = {},
801
+ ) {
802
+ if (!["succeeded", "failed"].includes(outcome)) {
803
+ throw coworkSchedulerError(
804
+ "COWORK_SCHEDULER_OUTCOME_INVALID",
805
+ "Cowork scheduler outcome must be succeeded or failed",
806
+ );
807
+ }
808
+ const now = normalizedDate(at, "cowork scheduler completion time");
809
+ const retryAtMs =
810
+ retryAt === undefined || retryAt === null ? null : Number(retryAt);
811
+ if (retryAtMs !== null && !Number.isSafeInteger(retryAtMs)) {
812
+ throw coworkSchedulerError(
813
+ "COWORK_SCHEDULER_RETRY_TIME_INVALID",
814
+ "Cowork scheduler retryAt must be a safe epoch millisecond",
815
+ );
816
+ }
817
+ return _withScheduleLock(cwd, () => {
818
+ const schedules = loadSchedules(cwd, { failOnMalformed: true });
819
+ const schedule = schedules.find((entry) => entry.id === id);
820
+ if (!schedule) {
821
+ throw coworkSchedulerError(
822
+ "COWORK_SCHEDULER_SCHEDULE_NOT_FOUND",
823
+ `Cowork schedule does not exist: ${id}`,
824
+ );
825
+ }
826
+ const evidence = schedule.schedulerExecution;
827
+ if (
828
+ !evidence ||
829
+ evidence.deliveryId !== deliveryId ||
830
+ evidence.occurrenceId !== occurrenceId ||
831
+ evidence.snapshotDigest !== snapshotDigest ||
832
+ evidence.attempt !== attempt ||
833
+ evidence.status !== "running"
834
+ ) {
835
+ throw coworkSchedulerError(
836
+ "COWORK_SCHEDULER_BINDING_MISMATCH",
837
+ `Cowork scheduler evidence does not match the active claim: ${id}`,
838
+ );
839
+ }
840
+
841
+ schedule.schedulerExecution = {
842
+ ...evidence,
843
+ status: outcome,
844
+ endedAt: now.toISOString(),
845
+ ...(outcome === "succeeded" ? { result } : { error }),
846
+ };
847
+ if (outcome === "succeeded") {
848
+ schedule.lastDeliveryId = deliveryId;
849
+ schedule.lastRunAt = now.toISOString();
850
+ schedule.lastStatus = result?.status || "completed";
851
+ schedule.activeDelivery = null;
852
+ } else if (retryAtMs !== null && retryAtMs > now.getTime()) {
853
+ schedule.activeDelivery = {
854
+ ...schedule.activeDelivery,
855
+ leaseExpiresAt: new Date(retryAtMs).toISOString(),
856
+ };
857
+ } else {
858
+ schedule.activeDelivery = null;
859
+ }
860
+ _saveSchedulesUnlocked(cwd, schedules);
861
+ return schedule;
862
+ });
863
+ }
864
+
366
865
  /**
367
866
  * Claim one concrete cron fire before launching its side effect. The persisted
368
867
  * delivery id prevents two scheduler processes from running the same fire; an
@@ -559,14 +1058,12 @@ export class CoworkCronScheduler {
559
1058
  if (!s.enabled) continue;
560
1059
  let matcher;
561
1060
  try {
562
- matcher = parseCron(s.cron);
1061
+ matcher = parseCron(s.cron, { timeZone: s.timeZone });
563
1062
  } catch (err) {
564
1063
  this._emit({ type: "invalid-cron", id: s.id, error: err.message });
565
1064
  continue;
566
1065
  }
567
- const fireKey = `${s.id}:${
568
- matcher.hasSeconds ? _secondKey(now) : _minuteKey(now)
569
- }`;
1066
+ const fireKey = coworkCronFireKey(s, now);
570
1067
  if (this._firedKeys.has(fireKey)) continue;
571
1068
  if (this._running.has(s.id)) continue;
572
1069
  const isDue = matcher(now);
@@ -634,27 +1131,30 @@ export class CoworkCronScheduler {
634
1131
  deliveryId: claim.deliveryId,
635
1132
  });
636
1133
 
637
- const renewal = setInterval(() => {
638
- try {
639
- const renewed = renewScheduleFire(this.cwd, schedule.id, claim, {
640
- leaseMs: this.leaseMs,
641
- });
642
- if (!renewed) {
1134
+ const renewal = setInterval(
1135
+ () => {
1136
+ try {
1137
+ const renewed = renewScheduleFire(this.cwd, schedule.id, claim, {
1138
+ leaseMs: this.leaseMs,
1139
+ });
1140
+ if (!renewed) {
1141
+ this._emit({
1142
+ type: "schedule-lease-lost",
1143
+ id: schedule.id,
1144
+ deliveryId: claim.deliveryId,
1145
+ });
1146
+ }
1147
+ } catch (err) {
643
1148
  this._emit({
644
- type: "schedule-lease-lost",
1149
+ type: "schedule-lease-renewal-failed",
645
1150
  id: schedule.id,
646
1151
  deliveryId: claim.deliveryId,
1152
+ error: err.message,
647
1153
  });
648
1154
  }
649
- } catch (err) {
650
- this._emit({
651
- type: "schedule-lease-renewal-failed",
652
- id: schedule.id,
653
- deliveryId: claim.deliveryId,
654
- error: err.message,
655
- });
656
- }
657
- }, Math.max(1000, Math.floor(this.leaseMs / 3)));
1155
+ },
1156
+ Math.max(1000, Math.floor(this.leaseMs / 3)),
1157
+ );
658
1158
  renewal.unref?.();
659
1159
 
660
1160
  let result = null;
@@ -718,12 +1218,28 @@ export class CoworkCronScheduler {
718
1218
  }
719
1219
  }
720
1220
 
721
- function _minuteKey(date) {
1221
+ export function coworkCronMinuteKey(date) {
722
1222
  return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}-${date.getHours()}-${date.getMinutes()}`;
723
1223
  }
724
1224
 
725
- function _secondKey(date) {
726
- return `${_minuteKey(date)}-${date.getSeconds()}`;
1225
+ export function coworkCronSecondKey(date) {
1226
+ return `${coworkCronMinuteKey(date)}-${date.getSeconds()}`;
1227
+ }
1228
+
1229
+ export function coworkCronFireKey(schedule, date) {
1230
+ const matcher = parseCron(schedule.cron, { timeZone: schedule.timeZone });
1231
+ // Keep the old host-local key while old and new CLIs may share this store.
1232
+ // Zoned schedules need epoch identity so repeated DST wall times stay distinct.
1233
+ if (!matcher.timeZone) {
1234
+ const wallKey = matcher.hasSeconds
1235
+ ? coworkCronSecondKey(date)
1236
+ : coworkCronMinuteKey(date);
1237
+ return `${schedule.id}:${wallKey}`;
1238
+ }
1239
+ const epoch = matcher.hasSeconds
1240
+ ? Math.floor(date.getTime() / 1_000) * 1_000
1241
+ : Math.floor(date.getTime() / 60_000) * 60_000;
1242
+ return `${schedule.id}:${epoch}`;
727
1243
  }
728
1244
 
729
1245
  // =====================================================================