chainlesschain 0.163.5 → 0.163.7
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.
- package/completions/_cc +1 -1
- package/completions/cc.bash +1 -1
- package/completions/cc.fish +1 -1
- package/completions/cc.ps1 +1 -1
- package/package.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-Cv2EE1Zy.js → AIOps-axqQfwv8.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-CcpxMLuW.js → ActionButton-DV-Ro0By.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DE8mJoMI.js → Analytics-Bt72WEVu.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-DKn9dk9L.js → AppLayout-BtepG55E.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-Cp3mG3Py.js → Artifacts-B7KaJ7S_.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-zvsQfF2C.js → Audit-FNFaUg7A.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-Bi85SsjN.js → BackgroundAgents-DFb6J2wQ.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-Cw47Dj6s.js → Backup-CC7YBhyj.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-C5Ip2afH.js → BaseInput-RkgTNVtj.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-C0iAFkJv.js → Chat-hHEmGZy-.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-BV9Wv7wO.js → ChatBubbleRenderer-CAP9YhHv.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-3KZZCiAF.js → Checkbox-z5wXOQP1.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-C8OMbBAz.js → Codegen-DVaDJnxy.js} +1 -1
- package/src/assets/web-panel/assets/{Col-B-MB84LJ.js → Col-CJmaRDR5.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DpR7Dxao.js → Community-DF29_2bI.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-CyDtz0OS.js → Compact-DtkQkBzn.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-5tiRa2An.js → Compliance-xoHDxp7I.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-DnQaKZ5x.js → Cowork-BUm-R7ht.js} +4 -4
- package/src/assets/web-panel/assets/{Cron-BaWfoEDf.js → Cron-Cgl14ZkB.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-Bu0HoHhb.js → Crosschain-BNcbmoqh.js} +1 -1
- package/src/assets/web-panel/assets/{DID-31zk8upX.js → DID-Bwja0Zo9.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-BmVne-e2.js → Dashboard-Bk5XjEdJ.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-B4PkoOWd.js → Dropdown-Ci36Oitq.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-BZ6tyFtI.js → EmailListRenderer--U04ZN4q.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-Osozn4Ds.js → FamilyGuardDashboard-B2gNPR2s.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-DaH8TOAP.js → Federation-C4KghYn7.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-tbU0uwis.js → FormItemContext-QL7x_Q_0.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-Df8LQBrL.js → GenericCardRenderer-F3Ryltwm.js} +1 -1
- package/src/assets/web-panel/assets/{Git-DsMB6-14.js → Git-2HassK5k.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BnH0dZzd.js → Governance-DoBIp7oe.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-DhnN1N_P.js → Inference--W01QhZ-.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DnNshAgn.js → KnowledgeGraph-DmvwqGpT.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-BmBFX3_N.js → Logs-BBJ4yAel.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-DCslfQU3.js → MarkdownRenderer-BLCZMzhJ.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-CPEgWDc3.js → Marketplace-D5lNngSI.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-C36qjgF9.js → McpTools-CkEVOMKH.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-tNHvrgqN.js → Memory-B1Ow1xVE.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-Xaf6cKjc.js → MobileBridge-SgYbUfb6.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-D2E5TTeO.js → MobileProjects-BWdOvQAj.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-D4AJ2dwV.js → Mtc-DcTXSSma.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-Df-gtn2_.js → MtcAudit-Dt3WG4oj.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-DTCHLWld.js → Multisig-B9B38UZx.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-DruZ_1Jb.js → NLProgramming-B0S4GGvG.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-Dx3pdGDs.js → Notes-DZ5U58Ng.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-Bi3L69oL.js → NotificationSettings-PjL5ZW34.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-BHY33m7C.js → OrderTableRenderer-DrAX2ESt.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-BG2ZVEV-.js → Organization-Cv1GYs-c.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-Viwtg4Mn.js → Overflow-Bbkp_PL3.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-Clm_yc1Z.js → P2P-BD8QUgoZ.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-NKmmDiOw.js → PdhVaultBrowser-BO-2PeaB.js} +5 -5
- package/src/assets/web-panel/assets/{Permissions-DIL8u9Nk.js → Permissions-D_OPVa02.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-B-jM4wt-.js → PersonalDataHub-mh-pEmaB.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-627I__H5.js → Pipeline-6JC2EDdb.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-2DN2qrkW.js → Privacy-CUshzUAG.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-CN4wC0sF.js → ProjectInit-Cs2Aalri.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-K35vsh5H.js → ProjectSettings-1ak49tcj.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-B_wQadhX.js → Projects-BV4oApsm.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-MckaQUhQ.js → Providers-CXXxvafH.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-BS3mtA_O.js → QrScannerModal-PTcDsF58.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-BHp_nofL.js → QuickAsk-CDwucrJw.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-DlmnPuZj.js → Recommend-CJlLVRXR.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-tsyFbvpy.js → RemoteSession-DlN-ETsO.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-Ba1iWv2D.js → Reputation-DI23LR6m.js} +1 -1
- package/src/assets/web-panel/assets/{Row-NnlGC--f.js → Row-Cl7UQMrM.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-CjThSIRv.js → RssFeed-CexHNfh8.js} +3 -3
- package/src/assets/web-panel/assets/{Search-CojUyC5x.js → Search-BneBWvRN.js} +1 -1
- package/src/assets/web-panel/assets/{Security-c-sxeJEc.js → Security-DX7Phgrp.js} +4 -4
- package/src/assets/web-panel/assets/{Services-CtcKEEmF.js → Services-BWXO8Mte.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-CIBBXflN.js → Skeleton-DCyOjKPJ.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-D1XOOK9n.js → Skills-CXqERsTB.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-hRfEp5Sn.js → Sla-bhbrbz8E.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-Dexb1f_6.js → SpeechSettings-3IPU0HBZ.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-BJsSFGYy.js → SyncSettings-DBNTQLbM.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-CfSZ8A1L.js → Tasks-quP0bkN-.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-idDf47W-.js → Templates-BouWmgho.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-C-UsX6w6.js → Tenant-qv0D9s7z.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-BgJtX5n1.js → Terminal-BxvbE8BB.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-DP0BLKTl.js → TimelineRenderer-D-IeLnJ2.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-LwCzqETp.js → Tokens-CFMKKyVG.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-D_9tVch0.js → Trigger-L8as3UuJ.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-BH2UlEWh.js → Trust-MxmA4VZG.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-CIpD0nYb.js → UkeySign-C0VvJYRi.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-9ShhYxK_.js → VideoEditing-Bf5QZj8N.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-C7afGNIG.js → Wallet-BIJ9riRz.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-bUKn6i5R.js → WebAuthn-B8p9Mhkw.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-CxgQaGyp.js → WorkflowEditor-BVsuaPen.js} +1 -1
- package/src/assets/web-panel/assets/{chat-DMVkmJwx.js → chat-BfkjoKpV.js} +1 -1
- package/src/assets/web-panel/assets/{colors-CDzQh5nS.js → colors-DeAlsWK8.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-C51kEF4D.js → compact-item-Cs97bz5j.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BTk9bXBn.js → createContext-BqbG60LH.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DPty0mGN.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-CMje33rb.js → hasIn-i_RY0b1X.js} +1 -1
- package/src/assets/web-panel/assets/{index-DpcrFxMI.js → index-0-agYlWL.js} +1 -1
- package/src/assets/web-panel/assets/{index-BW4F5YDF.js → index-79V_zv1s.js} +1 -1
- package/src/assets/web-panel/assets/{index-DRG-aqOs.js → index-B-3tEpTc.js} +1 -1
- package/src/assets/web-panel/assets/{index-BCRvG9lq.js → index-B-JVTrpr.js} +1 -1
- package/src/assets/web-panel/assets/{index-DgivOKHA.js → index-B1CbOH1i.js} +1 -1
- package/src/assets/web-panel/assets/{index-BzCkNylB.js → index-B5UkyA3c.js} +1 -1
- package/src/assets/web-panel/assets/{index-x53Vf7la.js → index-BESLFt4q.js} +1 -1
- package/src/assets/web-panel/assets/{index-CCLj9hcD.js → index-BOVkf1K3.js} +1 -1
- package/src/assets/web-panel/assets/{index-CypsfcEq.js → index-BYUVuwKt.js} +1 -1
- package/src/assets/web-panel/assets/{index-BtlEqHLP.js → index-BcUXAB2s.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ld_4mFmC.js → index-BkBi5TmC.js} +1 -1
- package/src/assets/web-panel/assets/{index-DqU9njjT.js → index-Bm7lLR4c.js} +1 -1
- package/src/assets/web-panel/assets/index-Bt7g0Vq_.js +1 -0
- package/src/assets/web-panel/assets/{index-CwGPwjUu.js → index-BuLQobzW.js} +1 -1
- package/src/assets/web-panel/assets/{index-C5IWTMVl.js → index-CE2VtTlp.js} +1 -1
- package/src/assets/web-panel/assets/{index-DJfnCuMq.js → index-CED2PL_y.js} +1 -1
- package/src/assets/web-panel/assets/{index-hg_emBsi.js → index-CG3PRyAL.js} +1 -1
- package/src/assets/web-panel/assets/{index-togc_uYn.js → index-CIG8fQ2_.js} +1 -1
- package/src/assets/web-panel/assets/{index-C4rQCS8T.js → index-COdGA-8n.js} +1 -1
- package/src/assets/web-panel/assets/{index-0Auljb10.js → index-CeWoNHau.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cd0hmPxD.js → index-Cf3WCLmy.js} +1 -1
- package/src/assets/web-panel/assets/{index-BXQBPLyE.js → index-CgsqdnRV.js} +1 -1
- package/src/assets/web-panel/assets/index-ChVT0p9u.js +1 -0
- package/src/assets/web-panel/assets/{index-h31MwrME.js → index-D2B4VPyP.js} +1 -1
- package/src/assets/web-panel/assets/{index-C1oZReaY.js → index-D9evtvg2.js} +1 -1
- package/src/assets/web-panel/assets/{index-CfC-piWZ.js → index-DUUrv141.js} +1 -1
- package/src/assets/web-panel/assets/{index-D9rC-fGW.js → index-DiSkx4UB.js} +3 -3
- package/src/assets/web-panel/assets/{index-CvSeVzqe.js → index-Dk2qouAz.js} +1 -1
- package/src/assets/web-panel/assets/{index-CtM8OT_7.js → index-Dk7-v4hP.js} +1 -1
- package/src/assets/web-panel/assets/{index-BBz9LIMS.js → index-DmbfhJUP.js} +1 -1
- package/src/assets/web-panel/assets/{index-BAfY7m7i.js → index-DqBgBpgp.js} +1 -1
- package/src/assets/web-panel/assets/{index-BVePe_Rf.js → index-PT_5sBMA.js} +1 -1
- package/src/assets/web-panel/assets/{index-CDLkNkLj.js → index-U4f5izHz.js} +1 -1
- package/src/assets/web-panel/assets/{index-BTlsE8Df.js → index-YXgLng0K.js} +1 -1
- package/src/assets/web-panel/assets/{index-BeEaAEIp.js → index-ZxO_-E8a.js} +1 -1
- package/src/assets/web-panel/assets/{index-B0-zMtKA.js → index-dIHr4B_u.js} +1 -1
- package/src/assets/web-panel/assets/{index-C1oZQmAA.js → index-gyFpvL7l.js} +1 -1
- package/src/assets/web-panel/assets/{index-DRLQyp2A.js → index-s1opAL-M.js} +1 -1
- package/src/assets/web-panel/assets/{index-OS-vfbTA.js → index-yObj1O4n.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-vrA8K-ac.js → initDefaultProps-C9h9yKL3.js} +1 -1
- package/src/assets/web-panel/assets/{motion-ByEzwIAl.js → motion-BvmLsPdu.js} +1 -1
- package/src/assets/web-panel/assets/{move-Ck4AWjw-.js → move-CoL4ekU1.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-6p0l9SrB.js → mtc-parser-C4pMy6xV.js} +1 -1
- package/src/assets/web-panel/assets/{omit-B1CUPQ6R.js → omit-DPuJfNxx.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-B1STg1iW.js → pickAttrs-D0KEeaK2.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-DTTllkBV.js → placementArrow-BpkyINxt.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-AIrrCfBb.js → responsiveObserve-DQI_wOq8.js} +1 -1
- package/src/assets/web-panel/assets/{slide-C_fPMjdR.js → slide-DYfdDiRg.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-D5ow2yIP.js → statusUtils-B5dtvb0D.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-Dy3hEA--.js → styleChecker-CF310mSj.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-WfzcfSh4.js → useFlexGapSupport-D1Qp4BLp.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-CKSejfas.js → useFs-C7_iyvJK.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-od3zpPBN.js → usePersonalDataHub-DZUt_fIm.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-DRD_2DFA.js → vnode-D3dsi0lZ.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DmNrEtgf.js → zoom-WdbOlvdQ.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +5 -5
- package/src/command-manifest.json +1 -1
- package/src/commands/agenda.js +76 -4
- package/src/commands/agent.js +90 -1
- package/src/commands/automation.js +462 -3
- package/src/commands/background-session.js +102 -1
- package/src/commands/chat.js +11 -2
- package/src/commands/compact.js +106 -23
- package/src/commands/cowork.js +6 -1
- package/src/commands/loop.js +187 -38
- package/src/commands/routine.js +12 -5
- package/src/commands/scheduler-daemon.js +807 -0
- package/src/commands/session-observability.js +305 -0
- package/src/commands/session.js +38 -0
- package/src/data/changelog.json +28 -2
- package/src/gateways/ws/ws-agent-handler.js +439 -5
- package/src/harness/jsonl-session-store.js +463 -125
- package/src/harness/prompt-compressor.js +86 -13
- package/src/harness/provider-backed-compaction.js +100 -27
- package/src/lib/advisor-runtime.js +3 -1
- package/src/lib/agent-schedule-store.js +737 -47
- package/src/lib/agent-team/team-adjudication.js +40 -1
- package/src/lib/automation-center-routines.js +363 -0
- package/src/lib/automation-center.js +435 -0
- package/src/lib/automation-engine.js +568 -19
- package/src/lib/automation-execution-authority.js +736 -0
- package/src/lib/background-agent-supervisor.js +871 -215
- package/src/lib/background-command-argv.js +24 -0
- package/src/lib/background-interaction-journal.js +361 -52
- package/src/lib/background-interaction-resolver.js +32 -0
- package/src/lib/background-launch-profile.js +2 -0
- package/src/lib/background-needs-input-incident.js +221 -0
- package/src/lib/causal-observability.js +2661 -0
- package/src/lib/chat-session-admission.js +76 -0
- package/src/lib/cost-budget.js +5 -7
- package/src/lib/cowork/ab-comparator-cli.js +11 -0
- package/src/lib/cowork/debate-review-cli.js +12 -0
- package/src/lib/cowork-adapter.js +159 -38
- package/src/lib/cowork-cron.js +673 -19
- package/src/lib/cowork-observe.js +5 -1
- package/src/lib/cowork-task-runner.js +112 -17
- package/src/lib/delivery-coordinator.js +142 -0
- package/src/lib/direct-model-usage.js +121 -0
- package/src/lib/file-lines.js +183 -47
- package/src/lib/llm-pricing.js +12 -17
- package/src/lib/loop.js +62 -3
- package/src/lib/permission-engine.js +14 -7
- package/src/lib/public-ide-capability-manifest.js +2 -0
- package/src/lib/routine-store.js +546 -86
- package/src/lib/runtime-usage-ledger.js +182 -0
- package/src/lib/scheduler-kernel/agenda-adapter.js +604 -29
- package/src/lib/scheduler-kernel/authority-resolver.js +163 -0
- package/src/lib/scheduler-kernel/automation-adapter.js +994 -0
- package/src/lib/scheduler-kernel/automation-event-adapter.js +757 -0
- package/src/lib/scheduler-kernel/contract.js +11 -3
- package/src/lib/scheduler-kernel/cowork-cron-adapter.js +343 -15
- package/src/lib/scheduler-kernel/loop-adapter.js +913 -0
- package/src/lib/scheduler-kernel/migration-admin.js +346 -0
- package/src/lib/scheduler-kernel/migration-rollback-admin.js +932 -0
- package/src/lib/scheduler-kernel/routine-adapter.js +691 -22
- package/src/lib/scheduler-kernel/runtime.js +63 -1
- package/src/lib/scheduler-kernel/service.js +328 -0
- package/src/lib/scheduler-kernel/source-locator-path.js +142 -0
- package/src/lib/scheduler-kernel/store.js +3439 -206
- package/src/lib/session-host-lease.js +171 -2
- package/src/lib/session-host-snapshot.js +14 -3
- package/src/lib/session-persistence-failure.js +1 -0
- package/src/lib/session-tail.js +179 -34
- package/src/lib/session-transcript-structure.js +96 -0
- package/src/lib/sub-agent-context.js +328 -22
- package/src/lib/ws-agent-handler.js +4 -1
- package/src/repl/agent-repl.js +732 -91
- package/src/repl/chat-repl.js +5 -1
- package/src/runtime/agent-core.js +1274 -208
- package/src/runtime/coding-agent-contract-shared.cjs +5 -0
- package/src/runtime/fallback-model.js +5 -0
- package/src/runtime/headless-runner.js +324 -82
- package/src/runtime/headless-stream.js +232 -26
- package/src/runtime/policies/agent-policy.js +4 -0
- package/src/workers/background-agent-worker.js +468 -98
- package/src/assets/web-panel/assets/devWarning-CdwPwYj8.js +0 -1
- package/src/assets/web-panel/assets/index-BEeOdfEe.js +0 -1
- package/src/assets/web-panel/assets/index-BTe0TOu6.js +0 -1
package/src/lib/cowork-cron.js
CHANGED
|
@@ -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 = {
|
|
@@ -55,6 +60,7 @@ export const _deps = {
|
|
|
55
60
|
|
|
56
61
|
export const COWORK_SCHEDULER_UNKNOWN_LEASE_EXPIRES_AT =
|
|
57
62
|
"9999-12-31T23:59:59.999Z";
|
|
63
|
+
export const COWORK_SCHEDULER_MIGRATION_SCHEMA_VERSION = 1;
|
|
58
64
|
|
|
59
65
|
function coworkSchedulerError(
|
|
60
66
|
code,
|
|
@@ -80,6 +86,81 @@ function normalizedDate(value, label) {
|
|
|
80
86
|
return date;
|
|
81
87
|
}
|
|
82
88
|
|
|
89
|
+
function stableJsonValue(value) {
|
|
90
|
+
if (Array.isArray(value)) return value.map(stableJsonValue);
|
|
91
|
+
if (value && typeof value === "object") {
|
|
92
|
+
return Object.fromEntries(
|
|
93
|
+
Object.keys(value)
|
|
94
|
+
.sort()
|
|
95
|
+
.map((key) => [key, stableJsonValue(value[key])]),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function coworkCronMigrationSourceSnapshot(schedule) {
|
|
102
|
+
const source = { ...schedule };
|
|
103
|
+
delete source.activeDelivery;
|
|
104
|
+
delete source.deliveryFence;
|
|
105
|
+
delete source.schedulerExecution;
|
|
106
|
+
delete source.schedulerMigration;
|
|
107
|
+
delete source.lastDeliveryId;
|
|
108
|
+
delete source.lastRunAt;
|
|
109
|
+
delete source.lastStatus;
|
|
110
|
+
delete source.nextAt;
|
|
111
|
+
delete source.missedRunPolicy;
|
|
112
|
+
return stableJsonValue(source);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function coworkCronMigrationSourceDigest(schedule) {
|
|
116
|
+
return crypto
|
|
117
|
+
.createHash("sha256")
|
|
118
|
+
.update(
|
|
119
|
+
"chainlesschain.cowork-cron.scheduler-migration-source.v1\0",
|
|
120
|
+
"utf8",
|
|
121
|
+
)
|
|
122
|
+
.update(JSON.stringify(coworkCronMigrationSourceSnapshot(schedule)), "utf8")
|
|
123
|
+
.digest("hex");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function migrationField(value, field) {
|
|
127
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
128
|
+
throw coworkSchedulerError(
|
|
129
|
+
"COWORK_SCHEDULER_MIGRATION_INVALID",
|
|
130
|
+
`Cowork scheduler migration ${field} is required`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return value.trim();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function coworkMigrationFence(schedule, nowMs) {
|
|
137
|
+
const migration = schedule.schedulerMigration;
|
|
138
|
+
const expiresAt = Date.parse(schedule.activeDelivery?.leaseExpiresAt || "");
|
|
139
|
+
return (
|
|
140
|
+
migration?.schemaVersion === COWORK_SCHEDULER_MIGRATION_SCHEMA_VERSION &&
|
|
141
|
+
typeof migration.retirementToken === "string" &&
|
|
142
|
+
schedule.activeDelivery?.ownerId ===
|
|
143
|
+
`scheduler-migration:${migration.retirementToken}` &&
|
|
144
|
+
Number.isFinite(expiresAt) &&
|
|
145
|
+
expiresAt > nowMs
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function applyCoworkMigrationFence(schedule, now) {
|
|
150
|
+
const token = schedule.schedulerMigration?.retirementToken;
|
|
151
|
+
if (typeof token !== "string" || token.length === 0) return false;
|
|
152
|
+
const fence = Math.max(Number(schedule.deliveryFence) || 0, 0) + 1;
|
|
153
|
+
schedule.deliveryFence = fence;
|
|
154
|
+
schedule.activeDelivery = {
|
|
155
|
+
deliveryId: `scheduler-migration:${token}`,
|
|
156
|
+
ownerId: `scheduler-migration:${token}`,
|
|
157
|
+
fence,
|
|
158
|
+
claimedAt: now.toISOString(),
|
|
159
|
+
leaseExpiresAt: COWORK_SCHEDULER_UNKNOWN_LEASE_EXPIRES_AT,
|
|
160
|
+
};
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
83
164
|
// ─── Cron parser ─────────────────────────────────────────────────────────────
|
|
84
165
|
|
|
85
166
|
const FIELD_RANGES = [
|
|
@@ -91,6 +172,56 @@ const FIELD_RANGES = [
|
|
|
91
172
|
];
|
|
92
173
|
|
|
93
174
|
const SECOND_RANGE = [0, 59];
|
|
175
|
+
const CRON_TIME_ZONE_FORMATTERS = new Map();
|
|
176
|
+
|
|
177
|
+
function timeZoneFormatter(timeZone) {
|
|
178
|
+
let formatter = CRON_TIME_ZONE_FORMATTERS.get(timeZone);
|
|
179
|
+
if (!formatter) {
|
|
180
|
+
formatter = new Intl.DateTimeFormat("en-US-u-ca-gregory-nu-latn", {
|
|
181
|
+
timeZone,
|
|
182
|
+
year: "numeric",
|
|
183
|
+
month: "2-digit",
|
|
184
|
+
day: "2-digit",
|
|
185
|
+
hour: "2-digit",
|
|
186
|
+
minute: "2-digit",
|
|
187
|
+
second: "2-digit",
|
|
188
|
+
hourCycle: "h23",
|
|
189
|
+
});
|
|
190
|
+
CRON_TIME_ZONE_FORMATTERS.set(timeZone, formatter);
|
|
191
|
+
}
|
|
192
|
+
return formatter;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function timeZoneParts(epochMs, timeZone) {
|
|
196
|
+
const values = {};
|
|
197
|
+
for (const part of timeZoneFormatter(timeZone).formatToParts(epochMs)) {
|
|
198
|
+
if (part.type !== "literal") values[part.type] = Number(part.value);
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
year: values.year,
|
|
202
|
+
month: values.month,
|
|
203
|
+
day: values.day,
|
|
204
|
+
hour: values.hour,
|
|
205
|
+
minute: values.minute,
|
|
206
|
+
second: values.second,
|
|
207
|
+
dayOfWeek: new Date(
|
|
208
|
+
Date.UTC(values.year, values.month - 1, values.day),
|
|
209
|
+
).getUTCDay(),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function cronDateParts(date, timeZone) {
|
|
214
|
+
if (timeZone) return timeZoneParts(date.getTime(), timeZone);
|
|
215
|
+
return {
|
|
216
|
+
year: date.getFullYear(),
|
|
217
|
+
month: date.getMonth() + 1,
|
|
218
|
+
day: date.getDate(),
|
|
219
|
+
hour: date.getHours(),
|
|
220
|
+
minute: date.getMinutes(),
|
|
221
|
+
second: date.getSeconds(),
|
|
222
|
+
dayOfWeek: date.getDay(),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
94
225
|
|
|
95
226
|
/**
|
|
96
227
|
* Non-standard alias → 5-field expression. Aliases never carry seconds.
|
|
@@ -182,7 +313,7 @@ export function parseCronField(field, [min, max]) {
|
|
|
182
313
|
* @param {string} expr
|
|
183
314
|
* @returns {((date: Date) => boolean) & { hasSeconds: boolean }}
|
|
184
315
|
*/
|
|
185
|
-
export function parseCron(expr) {
|
|
316
|
+
export function parseCron(expr, options = {}) {
|
|
186
317
|
if (typeof expr !== "string") {
|
|
187
318
|
throw new Error("cron expression must be a string");
|
|
188
319
|
}
|
|
@@ -194,6 +325,9 @@ export function parseCron(expr) {
|
|
|
194
325
|
);
|
|
195
326
|
}
|
|
196
327
|
const hasSeconds = parts.length === 6;
|
|
328
|
+
const timeZone = normalizeTimeZone(
|
|
329
|
+
typeof options === "string" ? options : options?.timeZone,
|
|
330
|
+
);
|
|
197
331
|
let second = null;
|
|
198
332
|
let minute, hour, dom, month, dow;
|
|
199
333
|
if (hasSeconds) {
|
|
@@ -214,12 +348,15 @@ export function parseCron(expr) {
|
|
|
214
348
|
const dowRestricted = dowField !== "*";
|
|
215
349
|
|
|
216
350
|
function matches(date) {
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
const
|
|
221
|
-
const
|
|
222
|
-
const
|
|
351
|
+
const value = date instanceof Date ? date : new Date(date);
|
|
352
|
+
if (!Number.isFinite(value.getTime())) return false;
|
|
353
|
+
const civil = cronDateParts(value, timeZone);
|
|
354
|
+
const s = civil.second;
|
|
355
|
+
const m = civil.minute;
|
|
356
|
+
const h = civil.hour;
|
|
357
|
+
const D = civil.day;
|
|
358
|
+
const M = civil.month;
|
|
359
|
+
const W = civil.dayOfWeek;
|
|
223
360
|
if (hasSeconds && !second.has(s)) return false;
|
|
224
361
|
if (!minute.has(m)) return false;
|
|
225
362
|
if (!hour.has(h)) return false;
|
|
@@ -233,6 +370,10 @@ export function parseCron(expr) {
|
|
|
233
370
|
return true;
|
|
234
371
|
}
|
|
235
372
|
matches.hasSeconds = hasSeconds;
|
|
373
|
+
matches.timeZone = timeZone;
|
|
374
|
+
matches.fields = { second, minute, hour, dom, month, dow };
|
|
375
|
+
matches.domRestricted = domRestricted;
|
|
376
|
+
matches.dowRestricted = dowRestricted;
|
|
236
377
|
return matches;
|
|
237
378
|
}
|
|
238
379
|
|
|
@@ -246,15 +387,153 @@ export function hasSecondsResolution(expr) {
|
|
|
246
387
|
}
|
|
247
388
|
|
|
248
389
|
/** Validate a cron expression — returns null if valid, error string otherwise. */
|
|
249
|
-
export function validateCron(expr) {
|
|
390
|
+
export function validateCron(expr, options = {}) {
|
|
250
391
|
try {
|
|
251
|
-
parseCron(expr);
|
|
392
|
+
parseCron(expr, options);
|
|
252
393
|
return null;
|
|
253
394
|
} catch (err) {
|
|
254
395
|
return err.message;
|
|
255
396
|
}
|
|
256
397
|
}
|
|
257
398
|
|
|
399
|
+
/** Return the next real instant for a Cowork 5/6-field cron. */
|
|
400
|
+
export function nextCoworkCronTime(expr, fromMs, options = {}) {
|
|
401
|
+
const matcher = parseCron(expr, options);
|
|
402
|
+
const from = Number(fromMs);
|
|
403
|
+
if (!Number.isFinite(from)) throw new Error("cron start time is invalid");
|
|
404
|
+
const expanded = _expandExpr(expr);
|
|
405
|
+
const parts = expanded.split(/\s+/);
|
|
406
|
+
const minuteFields = matcher.hasSeconds ? parts.slice(1) : parts;
|
|
407
|
+
const minuteExpressions =
|
|
408
|
+
matcher.domRestricted && matcher.dowRestricted
|
|
409
|
+
? [
|
|
410
|
+
[
|
|
411
|
+
minuteFields[0],
|
|
412
|
+
minuteFields[1],
|
|
413
|
+
minuteFields[2],
|
|
414
|
+
minuteFields[3],
|
|
415
|
+
"*",
|
|
416
|
+
].join(" "),
|
|
417
|
+
[
|
|
418
|
+
minuteFields[0],
|
|
419
|
+
minuteFields[1],
|
|
420
|
+
"*",
|
|
421
|
+
minuteFields[3],
|
|
422
|
+
minuteFields[4],
|
|
423
|
+
].join(" "),
|
|
424
|
+
]
|
|
425
|
+
: [minuteFields.join(" ")];
|
|
426
|
+
const nextMinute = (after) => {
|
|
427
|
+
const candidates = minuteExpressions
|
|
428
|
+
.map((candidate) =>
|
|
429
|
+
nextAgentCronTime(candidate, after, { timeZone: matcher.timeZone }),
|
|
430
|
+
)
|
|
431
|
+
.filter((candidate) => candidate !== null);
|
|
432
|
+
return candidates.length > 0 ? Math.min(...candidates) : null;
|
|
433
|
+
};
|
|
434
|
+
if (!matcher.hasSeconds) return nextMinute(from);
|
|
435
|
+
|
|
436
|
+
const seconds = [...matcher.fields.second].sort((a, b) => a - b);
|
|
437
|
+
let minute = nextMinute(Math.floor(from / 60_000) * 60_000 - 60_000);
|
|
438
|
+
while (minute !== null) {
|
|
439
|
+
for (const second of seconds) {
|
|
440
|
+
const candidate = minute + second * 1_000;
|
|
441
|
+
if (candidate > from) return candidate;
|
|
442
|
+
}
|
|
443
|
+
minute = nextMinute(minute);
|
|
444
|
+
}
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Select the latest due instant in `[firstMs, throughMs]`. Six-field schedules
|
|
450
|
+
* advance by matching civil minutes, not every elapsed second, so a long
|
|
451
|
+
* outage cannot turn an every-second schedule into millions of iterations.
|
|
452
|
+
*/
|
|
453
|
+
export function latestCoworkCronTime(expr, firstMs, throughMs, options = {}) {
|
|
454
|
+
const first = Number(firstMs);
|
|
455
|
+
const through = Number(throughMs);
|
|
456
|
+
if (!Number.isSafeInteger(first) || !Number.isSafeInteger(through)) {
|
|
457
|
+
throw new Error("cron catch-up bounds are invalid");
|
|
458
|
+
}
|
|
459
|
+
if (first > through) return null;
|
|
460
|
+
const matcher = parseCron(expr, options);
|
|
461
|
+
const seconds = matcher.hasSeconds
|
|
462
|
+
? [...matcher.fields.second].sort((a, b) => b - a)
|
|
463
|
+
: [0];
|
|
464
|
+
const expanded = _expandExpr(expr);
|
|
465
|
+
const fields = expanded.split(/\s+/);
|
|
466
|
+
const minuteFields = matcher.hasSeconds ? fields.slice(1) : fields;
|
|
467
|
+
const minuteExpressions =
|
|
468
|
+
matcher.domRestricted && matcher.dowRestricted
|
|
469
|
+
? [
|
|
470
|
+
[
|
|
471
|
+
minuteFields[0],
|
|
472
|
+
minuteFields[1],
|
|
473
|
+
minuteFields[2],
|
|
474
|
+
minuteFields[3],
|
|
475
|
+
"*",
|
|
476
|
+
].join(" "),
|
|
477
|
+
[
|
|
478
|
+
minuteFields[0],
|
|
479
|
+
minuteFields[1],
|
|
480
|
+
"*",
|
|
481
|
+
minuteFields[3],
|
|
482
|
+
minuteFields[4],
|
|
483
|
+
].join(" "),
|
|
484
|
+
]
|
|
485
|
+
: [minuteFields.join(" ")];
|
|
486
|
+
|
|
487
|
+
const throughMinute = Math.floor(through / 60_000) * 60_000;
|
|
488
|
+
for (const second of seconds) {
|
|
489
|
+
const candidate = throughMinute + second * 1_000;
|
|
490
|
+
if (
|
|
491
|
+
candidate >= first &&
|
|
492
|
+
candidate <= through &&
|
|
493
|
+
matcher(new Date(candidate))
|
|
494
|
+
) {
|
|
495
|
+
return candidate;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const previousMinute = Math.max(
|
|
500
|
+
...minuteExpressions
|
|
501
|
+
.map((candidate) =>
|
|
502
|
+
previousAgentCronTime(candidate, throughMinute, {
|
|
503
|
+
timeZone: matcher.timeZone,
|
|
504
|
+
}),
|
|
505
|
+
)
|
|
506
|
+
.filter((candidate) => candidate !== null),
|
|
507
|
+
);
|
|
508
|
+
if (Number.isFinite(previousMinute)) {
|
|
509
|
+
for (const second of seconds) {
|
|
510
|
+
const candidate = previousMinute + second * 1_000;
|
|
511
|
+
if (candidate >= first && candidate <= through) return candidate;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return first;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function currentCoworkCronTime(schedule, at) {
|
|
518
|
+
const date = normalizedDate(at, "cowork cron observation time");
|
|
519
|
+
const matcher = parseCron(schedule.cron, { timeZone: schedule.timeZone });
|
|
520
|
+
if (!matcher(date)) return null;
|
|
521
|
+
const resolution = matcher.hasSeconds ? 1_000 : 60_000;
|
|
522
|
+
return Math.floor(date.getTime() / resolution) * resolution;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function initialCoworkCronNextAt(schedule, at) {
|
|
526
|
+
const current = currentCoworkCronTime(schedule, at);
|
|
527
|
+
if (current !== null) return current;
|
|
528
|
+
return nextCoworkCronTime(
|
|
529
|
+
schedule.cron,
|
|
530
|
+
normalizedDate(at, "now").getTime(),
|
|
531
|
+
{
|
|
532
|
+
timeZone: schedule.timeZone,
|
|
533
|
+
},
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
258
537
|
// ─── Persistence ─────────────────────────────────────────────────────────────
|
|
259
538
|
|
|
260
539
|
function _scheduleFile(cwd) {
|
|
@@ -326,12 +605,20 @@ export function saveSchedules(cwd, schedules) {
|
|
|
326
605
|
// ─── CRUD ────────────────────────────────────────────────────────────────────
|
|
327
606
|
|
|
328
607
|
export function addSchedule(cwd, input) {
|
|
329
|
-
const {
|
|
608
|
+
const {
|
|
609
|
+
cron,
|
|
610
|
+
timeZone = null,
|
|
611
|
+
templateId = null,
|
|
612
|
+
userMessage,
|
|
613
|
+
files = [],
|
|
614
|
+
} = input || {};
|
|
330
615
|
if (!userMessage || typeof userMessage !== "string") {
|
|
331
616
|
throw new Error("userMessage is required");
|
|
332
617
|
}
|
|
333
|
-
const
|
|
618
|
+
const normalizedTimeZone = normalizeTimeZone(timeZone);
|
|
619
|
+
const err = validateCron(cron, { timeZone: normalizedTimeZone });
|
|
334
620
|
if (err) throw new Error(`invalid cron: ${err}`);
|
|
621
|
+
const createdAt = _deps.now();
|
|
335
622
|
|
|
336
623
|
// Serialize the read-modify-write across processes (cc+cc / cc+desktop share
|
|
337
624
|
// the cowork dir) so concurrently-added schedules don't clobber each other.
|
|
@@ -340,11 +627,17 @@ export function addSchedule(cwd, input) {
|
|
|
340
627
|
const entry = {
|
|
341
628
|
id: `sch-${crypto.randomUUID().slice(0, 12)}`,
|
|
342
629
|
cron: cron.trim(),
|
|
630
|
+
...(normalizedTimeZone ? { timeZone: normalizedTimeZone } : {}),
|
|
343
631
|
templateId,
|
|
344
632
|
userMessage,
|
|
345
633
|
files: Array.isArray(files) ? files : [],
|
|
346
634
|
enabled: true,
|
|
347
|
-
createdAt:
|
|
635
|
+
createdAt: createdAt.toISOString(),
|
|
636
|
+
nextAt: initialCoworkCronNextAt(
|
|
637
|
+
{ cron: cron.trim(), timeZone: normalizedTimeZone },
|
|
638
|
+
createdAt,
|
|
639
|
+
),
|
|
640
|
+
missedRunPolicy: "collapse",
|
|
348
641
|
lastRunAt: null,
|
|
349
642
|
lastStatus: null,
|
|
350
643
|
};
|
|
@@ -354,6 +647,73 @@ export function addSchedule(cwd, input) {
|
|
|
354
647
|
});
|
|
355
648
|
}
|
|
356
649
|
|
|
650
|
+
/**
|
|
651
|
+
* Upgrade a legacy schedule to durable next-fire state. Upgrade never replays
|
|
652
|
+
* pre-upgrade history: a matching current window is retained, otherwise the
|
|
653
|
+
* first future occurrence becomes authoritative.
|
|
654
|
+
*/
|
|
655
|
+
export function ensureScheduleNextAt(cwd, id, at = _deps.now()) {
|
|
656
|
+
return _withScheduleLock(cwd, () => {
|
|
657
|
+
const schedules = loadSchedules(cwd, { failOnMalformed: true });
|
|
658
|
+
const schedule = schedules.find((entry) => entry.id === id);
|
|
659
|
+
if (!schedule) return null;
|
|
660
|
+
if (Object.hasOwn(schedule, "nextAt")) {
|
|
661
|
+
if (
|
|
662
|
+
schedule.nextAt === null ||
|
|
663
|
+
(typeof schedule.nextAt === "number" &&
|
|
664
|
+
Number.isSafeInteger(schedule.nextAt) &&
|
|
665
|
+
schedule.nextAt >= 0)
|
|
666
|
+
) {
|
|
667
|
+
return schedule;
|
|
668
|
+
}
|
|
669
|
+
throw coworkSchedulerError(
|
|
670
|
+
"COWORK_SCHEDULER_CURSOR_INVALID",
|
|
671
|
+
`Cowork schedule nextAt is invalid: ${id}`,
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
const lastRunAt = Date.parse(schedule.lastRunAt || "");
|
|
675
|
+
const observedAt = normalizedDate(at, "cowork cron migration time");
|
|
676
|
+
schedule.nextAt = Number.isSafeInteger(lastRunAt)
|
|
677
|
+
? nextCoworkCronTime(schedule.cron, lastRunAt, {
|
|
678
|
+
timeZone: schedule.timeZone,
|
|
679
|
+
})
|
|
680
|
+
: initialCoworkCronNextAt(schedule, observedAt);
|
|
681
|
+
schedule.missedRunPolicy = "collapse";
|
|
682
|
+
_saveSchedulesUnlocked(cwd, schedules);
|
|
683
|
+
return schedule;
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* CAS-advance one due schedule beyond `from`, collapsing any number of missed
|
|
689
|
+
* periods to the single occurrence that was just durably enqueued.
|
|
690
|
+
*/
|
|
691
|
+
export function advanceScheduleNextAt(
|
|
692
|
+
cwd,
|
|
693
|
+
id,
|
|
694
|
+
{ expectedNextAt, from = _deps.now() } = {},
|
|
695
|
+
) {
|
|
696
|
+
return _withScheduleLock(cwd, () => {
|
|
697
|
+
const schedules = loadSchedules(cwd, { failOnMalformed: true });
|
|
698
|
+
const schedule = schedules.find((entry) => entry.id === id);
|
|
699
|
+
if (!schedule) return null;
|
|
700
|
+
if (
|
|
701
|
+
typeof schedule.nextAt !== "number" ||
|
|
702
|
+
schedule.nextAt !== Number(expectedNextAt)
|
|
703
|
+
) {
|
|
704
|
+
return schedule;
|
|
705
|
+
}
|
|
706
|
+
schedule.nextAt = nextCoworkCronTime(
|
|
707
|
+
schedule.cron,
|
|
708
|
+
normalizedDate(from, "cowork cron advance time").getTime(),
|
|
709
|
+
{ timeZone: schedule.timeZone },
|
|
710
|
+
);
|
|
711
|
+
schedule.missedRunPolicy = "collapse";
|
|
712
|
+
_saveSchedulesUnlocked(cwd, schedules);
|
|
713
|
+
return schedule;
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
|
|
357
717
|
export function removeSchedule(cwd, id) {
|
|
358
718
|
return _withScheduleLock(cwd, () => {
|
|
359
719
|
const schedules = loadSchedules(cwd, { failOnMalformed: true });
|
|
@@ -370,7 +730,12 @@ export function setScheduleEnabled(cwd, id, enabled) {
|
|
|
370
730
|
const schedules = loadSchedules(cwd, { failOnMalformed: true });
|
|
371
731
|
const s = schedules.find((x) => x.id === id);
|
|
372
732
|
if (!s) return false;
|
|
373
|
-
|
|
733
|
+
const nextEnabled = !!enabled;
|
|
734
|
+
if (nextEnabled && !s.enabled) {
|
|
735
|
+
s.nextAt = initialCoworkCronNextAt(s, _deps.now());
|
|
736
|
+
s.missedRunPolicy = "collapse";
|
|
737
|
+
}
|
|
738
|
+
s.enabled = nextEnabled;
|
|
374
739
|
_saveSchedulesUnlocked(cwd, schedules);
|
|
375
740
|
return true;
|
|
376
741
|
});
|
|
@@ -398,6 +763,205 @@ export function getSchedule(cwd, id) {
|
|
|
398
763
|
);
|
|
399
764
|
}
|
|
400
765
|
|
|
766
|
+
export function prepareCoworkSchedulerMigration(
|
|
767
|
+
cwd,
|
|
768
|
+
id,
|
|
769
|
+
{ migrationId, sourceDigest, targetJobId } = {},
|
|
770
|
+
) {
|
|
771
|
+
const migration = migrationField(migrationId, "migrationId");
|
|
772
|
+
const digest = migrationField(sourceDigest, "sourceDigest");
|
|
773
|
+
const target = migrationField(targetJobId, "targetJobId");
|
|
774
|
+
return _withScheduleLock(cwd, () => {
|
|
775
|
+
const schedules = loadSchedules(cwd, { failOnMalformed: true });
|
|
776
|
+
const schedule = schedules.find((entry) => entry.id === id);
|
|
777
|
+
if (!schedule) {
|
|
778
|
+
throw coworkSchedulerError(
|
|
779
|
+
"COWORK_SCHEDULER_SCHEDULE_NOT_FOUND",
|
|
780
|
+
`Cowork schedule does not exist: ${id}`,
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
if (schedule.enabled !== true) {
|
|
784
|
+
throw coworkSchedulerError(
|
|
785
|
+
"COWORK_SCHEDULER_SCHEDULE_DISABLED",
|
|
786
|
+
`Cowork schedule is disabled: ${id}`,
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
if (coworkCronMigrationSourceDigest(schedule) !== digest) {
|
|
790
|
+
throw coworkSchedulerError(
|
|
791
|
+
"COWORK_SCHEDULER_MIGRATION_SOURCE_CHANGED",
|
|
792
|
+
`Cowork schedule changed before migration: ${id}`,
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
const prior = schedule.schedulerMigration;
|
|
796
|
+
if (prior) {
|
|
797
|
+
if (
|
|
798
|
+
prior.schemaVersion !== COWORK_SCHEDULER_MIGRATION_SCHEMA_VERSION ||
|
|
799
|
+
prior.migrationId !== migration ||
|
|
800
|
+
prior.sourceDigest !== digest ||
|
|
801
|
+
prior.targetJobId !== target ||
|
|
802
|
+
prior.state !== "prepared"
|
|
803
|
+
) {
|
|
804
|
+
throw coworkSchedulerError(
|
|
805
|
+
"COWORK_SCHEDULER_MIGRATION_CONFLICT",
|
|
806
|
+
`Cowork schedule already belongs to another migration: ${id}`,
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
return schedule;
|
|
810
|
+
}
|
|
811
|
+
schedule.schedulerMigration = {
|
|
812
|
+
schemaVersion: COWORK_SCHEDULER_MIGRATION_SCHEMA_VERSION,
|
|
813
|
+
state: "prepared",
|
|
814
|
+
migrationId: migration,
|
|
815
|
+
sourceDigest: digest,
|
|
816
|
+
targetJobId: target,
|
|
817
|
+
preparedAt: _deps.now().toISOString(),
|
|
818
|
+
};
|
|
819
|
+
_saveSchedulesUnlocked(cwd, schedules);
|
|
820
|
+
return schedule;
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
export function retireCoworkSchedulerMigration(
|
|
825
|
+
cwd,
|
|
826
|
+
id,
|
|
827
|
+
{
|
|
828
|
+
migrationId,
|
|
829
|
+
sourceDigest,
|
|
830
|
+
targetJobId,
|
|
831
|
+
retirementToken,
|
|
832
|
+
at = _deps.now(),
|
|
833
|
+
} = {},
|
|
834
|
+
) {
|
|
835
|
+
const migration = migrationField(migrationId, "migrationId");
|
|
836
|
+
const digest = migrationField(sourceDigest, "sourceDigest");
|
|
837
|
+
const target = migrationField(targetJobId, "targetJobId");
|
|
838
|
+
const token = migrationField(retirementToken, "retirementToken");
|
|
839
|
+
const now = normalizedDate(at, "cowork scheduler migration retirement");
|
|
840
|
+
return _withScheduleLock(cwd, () => {
|
|
841
|
+
const schedules = loadSchedules(cwd, { failOnMalformed: true });
|
|
842
|
+
const schedule = schedules.find((entry) => entry.id === id);
|
|
843
|
+
if (!schedule) {
|
|
844
|
+
throw coworkSchedulerError(
|
|
845
|
+
"COWORK_SCHEDULER_SCHEDULE_NOT_FOUND",
|
|
846
|
+
`Cowork schedule does not exist: ${id}`,
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
const prior = schedule.schedulerMigration;
|
|
850
|
+
if (
|
|
851
|
+
prior?.schemaVersion !== COWORK_SCHEDULER_MIGRATION_SCHEMA_VERSION ||
|
|
852
|
+
prior.migrationId !== migration ||
|
|
853
|
+
prior.sourceDigest !== digest ||
|
|
854
|
+
prior.targetJobId !== target ||
|
|
855
|
+
!["prepared", "retired"].includes(prior.state)
|
|
856
|
+
) {
|
|
857
|
+
throw coworkSchedulerError(
|
|
858
|
+
"COWORK_SCHEDULER_MIGRATION_CONFLICT",
|
|
859
|
+
`Cowork migration preparation changed before retirement: ${id}`,
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
if (prior.state === "retired") {
|
|
863
|
+
if (
|
|
864
|
+
prior.retirementToken !== token ||
|
|
865
|
+
!coworkMigrationFence(schedule, now.getTime())
|
|
866
|
+
) {
|
|
867
|
+
throw coworkSchedulerError(
|
|
868
|
+
"COWORK_SCHEDULER_MIGRATION_CONFLICT",
|
|
869
|
+
`Cowork migration retirement fence changed: ${id}`,
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
return schedule;
|
|
873
|
+
}
|
|
874
|
+
if (coworkCronMigrationSourceDigest(schedule) !== digest) {
|
|
875
|
+
throw coworkSchedulerError(
|
|
876
|
+
"COWORK_SCHEDULER_MIGRATION_SOURCE_CHANGED",
|
|
877
|
+
`Cowork schedule changed before retirement: ${id}`,
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
const activeExpiresAt = Date.parse(
|
|
881
|
+
schedule.activeDelivery?.leaseExpiresAt || "",
|
|
882
|
+
);
|
|
883
|
+
if (
|
|
884
|
+
schedule.activeDelivery &&
|
|
885
|
+
Number.isFinite(activeExpiresAt) &&
|
|
886
|
+
activeExpiresAt > now.getTime()
|
|
887
|
+
) {
|
|
888
|
+
throw coworkSchedulerError(
|
|
889
|
+
"COWORK_SCHEDULER_LEGACY_CLAIM_ACTIVE",
|
|
890
|
+
`Cowork schedule is already claimed: ${id}`,
|
|
891
|
+
{ retryable: true, retryAt: activeExpiresAt },
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
schedule.schedulerMigration = {
|
|
895
|
+
...prior,
|
|
896
|
+
state: "retired",
|
|
897
|
+
retirementToken: token,
|
|
898
|
+
retiredAt: now.toISOString(),
|
|
899
|
+
};
|
|
900
|
+
applyCoworkMigrationFence(schedule, now);
|
|
901
|
+
_saveSchedulesUnlocked(cwd, schedules);
|
|
902
|
+
return schedule;
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
export function restoreCoworkSchedulerMigration(
|
|
907
|
+
cwd,
|
|
908
|
+
id,
|
|
909
|
+
{ migrationId, sourceDigest, targetJobId, retirementToken } = {},
|
|
910
|
+
) {
|
|
911
|
+
const migration = migrationField(migrationId, "migrationId");
|
|
912
|
+
const expectedDigest = migrationField(sourceDigest, "sourceDigest");
|
|
913
|
+
const target = migrationField(targetJobId, "targetJobId");
|
|
914
|
+
const token =
|
|
915
|
+
retirementToken === null || retirementToken === undefined
|
|
916
|
+
? null
|
|
917
|
+
: migrationField(retirementToken, "retirementToken");
|
|
918
|
+
return _withScheduleLock(cwd, () => {
|
|
919
|
+
const schedules = loadSchedules(cwd, { failOnMalformed: true });
|
|
920
|
+
const schedule = schedules.find((entry) => entry.id === id);
|
|
921
|
+
if (!schedule) {
|
|
922
|
+
throw coworkSchedulerError(
|
|
923
|
+
"COWORK_SCHEDULER_SCHEDULE_NOT_FOUND",
|
|
924
|
+
`Cowork schedule does not exist: ${id}`,
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
const current = schedule.schedulerMigration;
|
|
928
|
+
if (!current) {
|
|
929
|
+
if (
|
|
930
|
+
coworkCronMigrationSourceDigest(schedule) !== expectedDigest ||
|
|
931
|
+
String(schedule.activeDelivery?.ownerId || "").startsWith(
|
|
932
|
+
"scheduler-migration:",
|
|
933
|
+
)
|
|
934
|
+
) {
|
|
935
|
+
throw coworkSchedulerError(
|
|
936
|
+
"COWORK_SCHEDULER_MIGRATION_CONFLICT",
|
|
937
|
+
`Cowork source changed after rollback restoration: ${id}`,
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
return schedule;
|
|
941
|
+
}
|
|
942
|
+
if (
|
|
943
|
+
current?.schemaVersion !== COWORK_SCHEDULER_MIGRATION_SCHEMA_VERSION ||
|
|
944
|
+
current.migrationId !== migration ||
|
|
945
|
+
current.sourceDigest !== expectedDigest ||
|
|
946
|
+
current.targetJobId !== target ||
|
|
947
|
+
!["prepared", "retired"].includes(current.state) ||
|
|
948
|
+
coworkCronMigrationSourceDigest(schedule) !== expectedDigest ||
|
|
949
|
+
(current.state === "retired" &&
|
|
950
|
+
(current.retirementToken !== token ||
|
|
951
|
+
schedule.activeDelivery?.ownerId !== `scheduler-migration:${token}`))
|
|
952
|
+
) {
|
|
953
|
+
throw coworkSchedulerError(
|
|
954
|
+
"COWORK_SCHEDULER_MIGRATION_CONFLICT",
|
|
955
|
+
`Cowork migration fence changed before rollback: ${id}`,
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
delete schedule.schedulerMigration;
|
|
959
|
+
if (current.state === "retired") schedule.activeDelivery = null;
|
|
960
|
+
_saveSchedulesUnlocked(cwd, schedules);
|
|
961
|
+
return schedule;
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
|
|
401
965
|
/**
|
|
402
966
|
* Bind a scheduler-kernel occurrence before starting the Cowork task.
|
|
403
967
|
* The same JSONL lock used by the legacy driver closes both old-first and
|
|
@@ -456,7 +1020,13 @@ export function bindSchedulerScheduleFire(
|
|
|
456
1020
|
|
|
457
1021
|
const active = schedule.activeDelivery;
|
|
458
1022
|
const activeExpiresAt = Date.parse(active?.leaseExpiresAt || "");
|
|
459
|
-
|
|
1023
|
+
const migrationFence = coworkMigrationFence(schedule, nowMs);
|
|
1024
|
+
if (
|
|
1025
|
+
active &&
|
|
1026
|
+
Number.isFinite(activeExpiresAt) &&
|
|
1027
|
+
activeExpiresAt > nowMs &&
|
|
1028
|
+
!migrationFence
|
|
1029
|
+
) {
|
|
460
1030
|
throw coworkSchedulerError(
|
|
461
1031
|
"COWORK_SCHEDULER_LEGACY_CLAIM_ACTIVE",
|
|
462
1032
|
`Cowork schedule is already claimed: ${id}`,
|
|
@@ -568,6 +1138,81 @@ export function completeSchedulerScheduleFire(
|
|
|
568
1138
|
} else {
|
|
569
1139
|
schedule.activeDelivery = null;
|
|
570
1140
|
}
|
|
1141
|
+
if (schedule.enabled === true) applyCoworkMigrationFence(schedule, now);
|
|
1142
|
+
_saveSchedulesUnlocked(cwd, schedules);
|
|
1143
|
+
return schedule;
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
export function adjudicateSchedulerScheduleFire(
|
|
1148
|
+
cwd,
|
|
1149
|
+
id,
|
|
1150
|
+
{
|
|
1151
|
+
deliveryId,
|
|
1152
|
+
occurrenceId,
|
|
1153
|
+
snapshotDigest,
|
|
1154
|
+
attempt,
|
|
1155
|
+
decision,
|
|
1156
|
+
requestId,
|
|
1157
|
+
at = _deps.now(),
|
|
1158
|
+
} = {},
|
|
1159
|
+
) {
|
|
1160
|
+
if (!["confirmed_applied", "confirmed_not_applied"].includes(decision)) {
|
|
1161
|
+
throw coworkSchedulerError(
|
|
1162
|
+
"COWORK_SCHEDULER_ADJUDICATION_INVALID",
|
|
1163
|
+
"Cowork scheduler adjudication decision is invalid",
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
const now = normalizedDate(at, "cowork scheduler adjudication time");
|
|
1167
|
+
return _withScheduleLock(cwd, () => {
|
|
1168
|
+
const schedules = loadSchedules(cwd, { failOnMalformed: true });
|
|
1169
|
+
const schedule = schedules.find((entry) => entry.id === id);
|
|
1170
|
+
if (!schedule) {
|
|
1171
|
+
throw coworkSchedulerError(
|
|
1172
|
+
"COWORK_SCHEDULER_SCHEDULE_NOT_FOUND",
|
|
1173
|
+
`Cowork schedule does not exist: ${id}`,
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
const evidence = schedule.schedulerExecution;
|
|
1177
|
+
const targetStatus =
|
|
1178
|
+
decision === "confirmed_applied"
|
|
1179
|
+
? "adjudicated_applied"
|
|
1180
|
+
: "adjudicated_not_applied";
|
|
1181
|
+
if (
|
|
1182
|
+
evidence?.status === targetStatus &&
|
|
1183
|
+
evidence.adjudication?.requestId === requestId &&
|
|
1184
|
+
evidence.adjudication.decision === decision
|
|
1185
|
+
) {
|
|
1186
|
+
return schedule;
|
|
1187
|
+
}
|
|
1188
|
+
if (
|
|
1189
|
+
!evidence ||
|
|
1190
|
+
evidence.deliveryId !== deliveryId ||
|
|
1191
|
+
evidence.occurrenceId !== occurrenceId ||
|
|
1192
|
+
evidence.snapshotDigest !== snapshotDigest ||
|
|
1193
|
+
!Number.isSafeInteger(evidence.attempt) ||
|
|
1194
|
+
evidence.attempt < 1 ||
|
|
1195
|
+
evidence.attempt > attempt ||
|
|
1196
|
+
evidence.status !== "running"
|
|
1197
|
+
) {
|
|
1198
|
+
throw coworkSchedulerError(
|
|
1199
|
+
"COWORK_SCHEDULER_ADJUDICATION_EVIDENCE_MISMATCH",
|
|
1200
|
+
`Cowork scheduler adjudication evidence does not match: ${id}`,
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
schedule.schedulerExecution = {
|
|
1204
|
+
...evidence,
|
|
1205
|
+
status: targetStatus,
|
|
1206
|
+
endedAt: now.toISOString(),
|
|
1207
|
+
adjudication: { requestId, decision },
|
|
1208
|
+
};
|
|
1209
|
+
if (decision === "confirmed_applied") {
|
|
1210
|
+
schedule.lastDeliveryId = deliveryId;
|
|
1211
|
+
schedule.lastRunAt = now.toISOString();
|
|
1212
|
+
schedule.lastStatus = "adjudicated-applied";
|
|
1213
|
+
}
|
|
1214
|
+
schedule.activeDelivery = null;
|
|
1215
|
+
if (schedule.enabled === true) applyCoworkMigrationFence(schedule, now);
|
|
571
1216
|
_saveSchedulesUnlocked(cwd, schedules);
|
|
572
1217
|
return schedule;
|
|
573
1218
|
});
|
|
@@ -769,7 +1414,7 @@ export class CoworkCronScheduler {
|
|
|
769
1414
|
if (!s.enabled) continue;
|
|
770
1415
|
let matcher;
|
|
771
1416
|
try {
|
|
772
|
-
matcher = parseCron(s.cron);
|
|
1417
|
+
matcher = parseCron(s.cron, { timeZone: s.timeZone });
|
|
773
1418
|
} catch (err) {
|
|
774
1419
|
this._emit({ type: "invalid-cron", id: s.id, error: err.message });
|
|
775
1420
|
continue;
|
|
@@ -938,10 +1583,19 @@ export function coworkCronSecondKey(date) {
|
|
|
938
1583
|
}
|
|
939
1584
|
|
|
940
1585
|
export function coworkCronFireKey(schedule, date) {
|
|
941
|
-
const matcher = parseCron(schedule.cron);
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1586
|
+
const matcher = parseCron(schedule.cron, { timeZone: schedule.timeZone });
|
|
1587
|
+
// Keep the old host-local key while old and new CLIs may share this store.
|
|
1588
|
+
// Zoned schedules need epoch identity so repeated DST wall times stay distinct.
|
|
1589
|
+
if (!matcher.timeZone) {
|
|
1590
|
+
const wallKey = matcher.hasSeconds
|
|
1591
|
+
? coworkCronSecondKey(date)
|
|
1592
|
+
: coworkCronMinuteKey(date);
|
|
1593
|
+
return `${schedule.id}:${wallKey}`;
|
|
1594
|
+
}
|
|
1595
|
+
const epoch = matcher.hasSeconds
|
|
1596
|
+
? Math.floor(date.getTime() / 1_000) * 1_000
|
|
1597
|
+
: Math.floor(date.getTime() / 60_000) * 60_000;
|
|
1598
|
+
return `${schedule.id}:${epoch}`;
|
|
945
1599
|
}
|
|
946
1600
|
|
|
947
1601
|
// =====================================================================
|