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.
- 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-DB5X7qqV.js → AIOps-DDIUofA8.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-DxHRtKLk.js → ActionButton-Bm-5xYRn.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DIH5QrxT.js → Analytics-D5UH4ljI.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-uu0oxVz7.js → AppLayout-D_ifjOOP.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-BNSMiW9Y.js → Artifacts-BY1mXdC7.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-DcAzB4_x.js → Audit-Bujr4Ml3.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-ClSsvjIc.js → BackgroundAgents-sioKSTcC.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-BMciY0js.js → Backup-DDOAK8RK.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-Hn3b57Sh.js → BaseInput-CtCsVVke.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CoSlC53V.js → Chat-DpNj0vnd.js} +4 -4
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-CFevJ3So.js → ChatBubbleRenderer-CZta3Ftr.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-B4L4Eu4w.js → Checkbox-DoHVNSXq.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-C77FgRxF.js → Codegen-DlVl7Qmv.js} +1 -1
- package/src/assets/web-panel/assets/{Col-BrLtJLH3.js → Col-GIqoPYty.js} +1 -1
- package/src/assets/web-panel/assets/{Community-Be7pXXhA.js → Community-CIAPEVji.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DuGPecjo.js → Compact-BN7JiI_h.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CzS66mjP.js → Compliance-CqUWCM9B.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-CEhjCoyK.js → Cowork-A4iPp7A4.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-D7ir8Vja.js → Cron-B2zeXXWb.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-BcXmuLxf.js → Crosschain-BR2xmWr8.js} +1 -1
- package/src/assets/web-panel/assets/{DID-Cto1dtBy.js → DID-BzFDjrIv.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-B7KjgwOX.js → Dashboard-DKtDo3yA.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-COMqxijg.js → Dropdown-RNx43_YG.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-BZrya-nf.js → EmailListRenderer-SwMt4Rcy.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-AmVeMq0J.js → FamilyGuardDashboard-CsYebZ4X.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-kZU8c-27.js → Federation-BmRKvBTy.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-sOV22v2-.js → FormItemContext-CVxGTA-0.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BUuLtGCz.js → GenericCardRenderer-x2iXpyB7.js} +1 -1
- package/src/assets/web-panel/assets/{Git-K-ZxsLsH.js → Git-DfqbkiOP.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-D3OEnawI.js → Governance-RuHyyj5O.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-Bll0gRmP.js → Inference-Bn7uZ7rI.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DBXwjr53.js → KnowledgeGraph-DlP7XefZ.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-CEzPwEfP.js → Logs-BYNdOiCd.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-aMtgN-R6.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-BYyRyOPz.js → Marketplace-JQYmDqM8.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DVu8w_rM.js → McpTools-Cyx4yZ-9.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-tenuZ_hO.js → Memory-VDuGZTN2.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-Bxgy4ozS.js → MobileBridge-_yKo97gi.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-Dgx1D6sc.js → MobileProjects-CXRXW3q7.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-CRfJ8r8j.js → Mtc-acTGtfLH.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-BBJq4Aiz.js → MtcAudit-ClpjXfkY.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-B3ii9lai.js → Multisig-_6Tn3o96.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-FIi-Cubv.js → NLProgramming-CK7pbZzP.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-DIq8MPf1.js → Notes-Czy6V_P8.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-BFOD9eGd.js → NotificationSettings-Cqx-UVv3.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer--41u7vre.js → OrderTableRenderer-C69WDxGy.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-B_0AWztW.js → Organization-DFnA9vLQ.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-B6I7TuXv.js → Overflow-ReZkk2yT.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-Cdf6qens.js → P2P-DzjSI0aI.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-B_0FcQJY.js → PdhVaultBrowser-DKTf5xIP.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-6nFBjsQp.js → Permissions-DhQ5klhR.js} +3 -3
- package/src/assets/web-panel/assets/{PersonalDataHub-nQYV7xpn.js → PersonalDataHub-CUL1iHrW.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-CBVod5jm.js → Pipeline-DU88iu4N.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-D52jW1Nt.js → Privacy-Bw-sYfyK.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-Bdx3BYhf.js → ProjectInit-Bq3Q5y0z.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-b7xr4DUa.js → ProjectSettings-C8spzpho.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-DhfifCKx.js → Projects-Dd-xuPIc.js} +1 -1
- package/src/assets/web-panel/assets/Providers-Bf4eTP9W.js +1 -0
- package/src/assets/web-panel/assets/{QrScannerModal-FQKSmc3e.js → QrScannerModal-Dme9BDU1.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-CSTrhzcF.js → QuickAsk-CIXfv1rE.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-CxMABOUh.js → Recommend-CIxvdeZ-.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-Cv2T8yRa.js → RemoteSession-C6k7MExI.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-CE1k4Vvu.js → Reputation-CISDz2bl.js} +1 -1
- package/src/assets/web-panel/assets/{Row-BNgNcAV_.js → Row-CdnDxXyf.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-5ZiBZ3ph.js → RssFeed-C-QpRHsl.js} +3 -3
- package/src/assets/web-panel/assets/{Search-mDOIF1UV.js → Search-DBjaGzSn.js} +1 -1
- package/src/assets/web-panel/assets/{Security-CtuCTC-e.js → Security-CQKukOeE.js} +3 -3
- package/src/assets/web-panel/assets/{Services-DxFO2Zg7.js → Services-DZz86jWb.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-CKmKkH9y.js → Skeleton-jb8RMK0d.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-Bmzl9qkk.js → Skills-CdY76fZG.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-9RqaWHOC.js → Sla-C_CSiZX9.js} +1 -1
- package/src/assets/web-panel/assets/SpeechSettings-C7tT9iad.js +1 -0
- package/src/assets/web-panel/assets/{SyncSettings-CvNuCeBT.js → SyncSettings-qvimGO0O.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-B_sLdcnw.js → Tasks-Dng5LPJC.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-D9_YijdK.js → Templates-Bwqq5Wll.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-Cuc7BUSP.js → Tenant-DNWuwP7Z.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-6VYRnJgd.js → Terminal-B-6Othpp.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-GtK8xd8Y.js → TimelineRenderer-DKpKZfzx.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-BZ94Lt5W.js → Tokens-DzfGMd2J.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DEcZgDaX.js → Trigger-BXwUi74J.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-CC3z9THe.js → Trust-BNzUq4ui.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-Cvtw8cV3.js → UkeySign-BYugxww4.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-CL4e6Bn3.js → VideoEditing-DdauhtSJ.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-ycEemP27.js → Wallet-CSC9bCe_.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-DgZ4X_aw.js → WebAuthn-Q8AN3bTO.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-D0qMhSv7.js → WorkflowEditor-d-LBGCuA.js} +1 -1
- package/src/assets/web-panel/assets/{chat-DsAg01Zn.js → chat-BLVDCgsZ.js} +1 -1
- package/src/assets/web-panel/assets/{colors-B0oqAhrQ.js → colors-BZlPSEHl.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-CmX0Rabz.js → compact-item-IWseP6R6.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-Be4cn1Ub.js → createContext-DnVRV3Ao.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-Jzu62CLC.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-D5v12kVq.js → hasIn-DLQZA_Wn.js} +1 -1
- package/src/assets/web-panel/assets/{index-BA4oulaP.js → index-1jeAudHd.js} +1 -1
- package/src/assets/web-panel/assets/{index-DM724NgU.js → index-BEu2lMBy.js} +1 -1
- package/src/assets/web-panel/assets/{index-5LGY0JbG.js → index-BOZJBTWZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DikJ0Qqe.js → index-BSjilZ9L.js} +1 -1
- package/src/assets/web-panel/assets/{index-BqltWojQ.js → index-BZs5bBnk.js} +1 -1
- package/src/assets/web-panel/assets/{index-7VKDn-t1.js → index-B_N4-Gq9.js} +1 -1
- package/src/assets/web-panel/assets/{index-BXGSbelg.js → index-Bbj2NS_I.js} +3 -3
- package/src/assets/web-panel/assets/{index-CGiIyTZw.js → index-BhA3QEni.js} +1 -1
- package/src/assets/web-panel/assets/{index-BKmsS2jj.js → index-Bxp7uDD8.js} +1 -1
- package/src/assets/web-panel/assets/{index-B3mUHuVc.js → index-C1ifZtXo.js} +1 -1
- package/src/assets/web-panel/assets/{index-BBvyHguy.js → index-C2FqfMvU.js} +1 -1
- package/src/assets/web-panel/assets/{index-q_fQ94SE.js → index-C5dgnilC.js} +1 -1
- package/src/assets/web-panel/assets/{index-D5YVopyr.js → index-CINJQEj6.js} +1 -1
- package/src/assets/web-panel/assets/{index-0eWLkQKT.js → index-COev0W2g.js} +1 -1
- package/src/assets/web-panel/assets/{index-BoAIAZ5C.js → index-CT9vVN-X.js} +1 -1
- package/src/assets/web-panel/assets/{index-CqoCZ5vR.js → index-CZDekj7T.js} +1 -1
- package/src/assets/web-panel/assets/{index-301ogenv.js → index-CarMTIyQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dn2dSdXx.js → index-CbQNL0ue.js} +1 -1
- package/src/assets/web-panel/assets/{index-DKUBj36_.js → index-CpBaJ3TD.js} +1 -1
- package/src/assets/web-panel/assets/{index-BBREnn1V.js → index-DB0MJgF_.js} +1 -1
- package/src/assets/web-panel/assets/{index-BRIqLy1m.js → index-DOoQpuQY.js} +1 -1
- package/src/assets/web-panel/assets/{index-jK3ax6Z6.js → index-DTbxp4UH.js} +1 -1
- package/src/assets/web-panel/assets/{index-B1YGjOWC.js → index-DUq0c7gq.js} +1 -1
- package/src/assets/web-panel/assets/{index-CyyGTMKd.js → index-DZpYeqrh.js} +1 -1
- package/src/assets/web-panel/assets/{index-_JjRLSD0.js → index-D_LQodeJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DyUaFWEk.js → index-DalA0TKC.js} +1 -1
- package/src/assets/web-panel/assets/{index-BaE0Ui07.js → index-DjWnONdy.js} +1 -1
- package/src/assets/web-panel/assets/{index-CyWBir7N.js → index-Dkwsi1Kn.js} +1 -1
- package/src/assets/web-panel/assets/{index-qMuZ-NRR.js → index-DocEhinm.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ckrk3pIV.js → index-DpVrK6fr.js} +1 -1
- package/src/assets/web-panel/assets/{index-DQRsQen0.js → index-Dy0R5ZED.js} +1 -1
- package/src/assets/web-panel/assets/{index-frSojpHj.js → index-M7wxfAbm.js} +1 -1
- package/src/assets/web-panel/assets/{index-D0BSkxYQ.js → index-N_GoWZX4.js} +1 -1
- package/src/assets/web-panel/assets/{index-8WvYGnON.js → index-YSw_gxlZ.js} +1 -1
- package/src/assets/web-panel/assets/index-Zb2MeDSy.js +1 -0
- package/src/assets/web-panel/assets/index-fikqml4k.js +1 -0
- package/src/assets/web-panel/assets/{index-Hfe_dEGx.js → index-gt8bNefW.js} +1 -1
- package/src/assets/web-panel/assets/{index-CZ2wUEgr.js → index-hwVDeoC0.js} +1 -1
- package/src/assets/web-panel/assets/{index-CZpfzcuy.js → index-jWb70ZgZ.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-D4VdhJ9_.js → initDefaultProps-Co67Kqmb.js} +1 -1
- package/src/assets/web-panel/assets/{motion-CZJA0MQW.js → motion-BMOxQSF_.js} +1 -1
- package/src/assets/web-panel/assets/{move-BqEs7Sb1.js → move-C5fWDw-N.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-BpHEtsu1.js → mtc-parser-BJevpL0e.js} +1 -1
- package/src/assets/web-panel/assets/{omit-BW94tY9W.js → omit-BCzNU8s6.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-BnBjPWP9.js → pickAttrs-CybWvDPk.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-uUkjQ_hu.js → placementArrow-BMWS9HnW.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-B-klUqXK.js → responsiveObserve-BoL2UnGY.js} +1 -1
- package/src/assets/web-panel/assets/{slide-CvY1cNAs.js → slide-DWNfXraU.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-Ix5sEDT2.js → statusUtils-BquTM3tX.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-C8qcXOne.js → styleChecker-BEkCleim.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-KNF6Kj8f.js → useFlexGapSupport-Oecnq7EA.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-DGJr345n.js → useFs-CkDffJ4F.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BboN99rI.js → usePersonalDataHub-qbvKXesB.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-Bgt8Vgte.js → vnode-C6v8n6mA.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-BhJtErgt.js → zoom-4CTeFILV.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +3 -3
- package/src/command-manifest.json +1 -1
- package/src/commands/agenda.js +125 -7
- package/src/commands/automation.js +462 -3
- package/src/commands/background-session.js +102 -1
- package/src/commands/cowork.js +30 -13
- package/src/commands/loop.js +167 -36
- package/src/commands/routine.js +50 -13
- package/src/commands/scheduler-daemon.js +450 -0
- package/src/data/changelog.json +27 -1
- package/src/lib/agent-schedule-store.js +509 -36
- package/src/lib/automation-center-routines.js +363 -0
- package/src/lib/automation-center.js +435 -0
- package/src/lib/automation-engine.js +74 -19
- package/src/lib/automation-execution-authority.js +736 -0
- package/src/lib/background-agent-supervisor.js +78 -0
- package/src/lib/background-needs-input-incident.js +221 -0
- package/src/lib/cowork-cron.js +555 -39
- package/src/lib/cowork-observe.js +5 -1
- package/src/lib/loop.js +37 -2
- package/src/lib/micro-compact.js +11 -5
- package/src/lib/permission-engine.js +14 -7
- package/src/lib/process-execution-broker/index.js +14 -3
- package/src/lib/public-ide-capability-manifest.js +2 -0
- package/src/lib/routine-store.js +242 -68
- package/src/lib/scheduler-kernel/agenda-adapter.js +812 -0
- package/src/lib/scheduler-kernel/authority-resolver.js +163 -0
- package/src/lib/scheduler-kernel/automation-adapter.js +592 -0
- package/src/lib/scheduler-kernel/automation-event-adapter.js +721 -0
- package/src/lib/scheduler-kernel/contract.js +11 -3
- package/src/lib/scheduler-kernel/cowork-cron-adapter.js +820 -0
- package/src/lib/scheduler-kernel/loop-adapter.js +378 -0
- package/src/lib/scheduler-kernel/routine-adapter.js +812 -0
- package/src/lib/scheduler-kernel/runtime.js +527 -0
- package/src/lib/scheduler-kernel/service.js +328 -0
- package/src/lib/scheduler-kernel/store.js +940 -98
- package/src/lib/session-message-provenance.js +4 -1
- package/src/repl/agent-repl.js +24 -5
- package/src/runtime/agent-core.js +38 -17
- package/src/runtime/coding-agent-contract-shared.cjs +5 -0
- package/src/workers/background-agent-worker.js +91 -28
- package/src/assets/web-panel/assets/MarkdownRenderer-CLmCbHx6.js +0 -1
- package/src/assets/web-panel/assets/Providers-Dy_PgDMr.js +0 -1
- package/src/assets/web-panel/assets/SpeechSettings-CGdzFIYw.js +0 -1
- package/src/assets/web-panel/assets/devWarning-CP2qzG6D.js +0 -1
- package/src/assets/web-panel/assets/index-CKB0vbJU.js +0 -1
- package/src/assets/web-panel/assets/index-DgoXmwqB.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 = {
|
|
@@ -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
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
const
|
|
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 {
|
|
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
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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-
|
|
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
|
-
}
|
|
650
|
-
|
|
651
|
-
|
|
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
|
|
1221
|
+
export function coworkCronMinuteKey(date) {
|
|
722
1222
|
return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}-${date.getHours()}-${date.getMinutes()}`;
|
|
723
1223
|
}
|
|
724
1224
|
|
|
725
|
-
function
|
|
726
|
-
return `${
|
|
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
|
// =====================================================================
|