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
|
@@ -168,6 +168,81 @@ function hasLiveExecutionLease(entry, now) {
|
|
|
168
168
|
return Number(entry.executionLease?.expiresAt) > Number(now);
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
function agendaStoreError(code, message, { retryable = false, retryAt } = {}) {
|
|
172
|
+
const error = new Error(message);
|
|
173
|
+
error.code = code;
|
|
174
|
+
error.retryable = retryable;
|
|
175
|
+
if (retryAt !== undefined) error.retryAt = retryAt;
|
|
176
|
+
return error;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function applyScheduledAgentSuccess(entry, atMs) {
|
|
180
|
+
if (entry.kind === "wakeup") {
|
|
181
|
+
entry.status = "fired";
|
|
182
|
+
entry.firedAt = atMs;
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (entry.kind === "cron") {
|
|
186
|
+
entry.lastRunAt = atMs;
|
|
187
|
+
entry.runs = (entry.runs || 0) + 1;
|
|
188
|
+
const next = nextCronTime(entry.cron, atMs, {
|
|
189
|
+
timeZone: entry.timeZone || null,
|
|
190
|
+
});
|
|
191
|
+
if (next == null) entry.status = "exhausted";
|
|
192
|
+
else entry.nextAt = next;
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
throw agendaStoreError(
|
|
196
|
+
"AGENDA_SCHEDULER_KIND_UNSUPPORTED",
|
|
197
|
+
`Scheduler execution is not supported for agenda kind: ${entry.kind}`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function applyScheduledMonitorSuccess(entry, monitorCheck, atMs) {
|
|
202
|
+
if (
|
|
203
|
+
entry.kind !== "monitor" ||
|
|
204
|
+
!monitorCheck ||
|
|
205
|
+
typeof monitorCheck !== "object" ||
|
|
206
|
+
Array.isArray(monitorCheck) ||
|
|
207
|
+
typeof monitorCheck.matched !== "boolean"
|
|
208
|
+
) {
|
|
209
|
+
throw agendaStoreError(
|
|
210
|
+
"AGENDA_SCHEDULER_MONITOR_RESULT_INVALID",
|
|
211
|
+
`Scheduler monitor result is invalid: ${entry.id}`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
const mtimeMs =
|
|
215
|
+
monitorCheck.mtimeMs === null || monitorCheck.mtimeMs === undefined
|
|
216
|
+
? null
|
|
217
|
+
: Number(monitorCheck.mtimeMs);
|
|
218
|
+
if (mtimeMs !== null && (!Number.isFinite(mtimeMs) || mtimeMs < 0)) {
|
|
219
|
+
throw agendaStoreError(
|
|
220
|
+
"AGENDA_SCHEDULER_MONITOR_RESULT_INVALID",
|
|
221
|
+
`Scheduler monitor mtime is invalid: ${entry.id}`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
entry.checks = (entry.checks || 0) + 1;
|
|
225
|
+
entry.lastCheckAt = atMs;
|
|
226
|
+
if (mtimeMs !== null && entry.lastMtimeMs == null) {
|
|
227
|
+
entry.lastMtimeMs = mtimeMs;
|
|
228
|
+
}
|
|
229
|
+
if (monitorCheck.matched) {
|
|
230
|
+
entry.status = "matched";
|
|
231
|
+
entry.matchedAt = atMs;
|
|
232
|
+
if (monitorCheck.eventId != null) {
|
|
233
|
+
entry.lastEventId = String(monitorCheck.eventId);
|
|
234
|
+
entry.lastEventAt = atMs;
|
|
235
|
+
}
|
|
236
|
+
if (monitorCheck.authority != null) {
|
|
237
|
+
entry.lastAuthority = String(monitorCheck.authority);
|
|
238
|
+
}
|
|
239
|
+
} else if (entry.maxChecks != null && entry.checks >= entry.maxChecks) {
|
|
240
|
+
entry.status = "exhausted";
|
|
241
|
+
} else {
|
|
242
|
+
entry.nextAt = atMs + entry.intervalMs;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
171
246
|
/** Statuses an entry can never leave — safe to prune. */
|
|
172
247
|
export const TERMINAL_STATUSES = Object.freeze([
|
|
173
248
|
"fired",
|
|
@@ -204,6 +279,61 @@ const CRON_RANGES = [
|
|
|
204
279
|
[0, 6], // day of week (0/7 = Sunday)
|
|
205
280
|
];
|
|
206
281
|
|
|
282
|
+
const CRON_TIME_ZONE_FORMATTERS = new Map();
|
|
283
|
+
|
|
284
|
+
export function normalizeTimeZone(value) {
|
|
285
|
+
if (value == null || value === "") return null;
|
|
286
|
+
if (typeof value !== "string") {
|
|
287
|
+
throw new Error("cron timeZone must be an IANA time zone string");
|
|
288
|
+
}
|
|
289
|
+
const requested = value.trim();
|
|
290
|
+
if (!requested) return null;
|
|
291
|
+
if (requested.length > 128 || /\p{Cc}/u.test(requested)) {
|
|
292
|
+
throw new Error("invalid IANA time zone");
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
return new Intl.DateTimeFormat("en-US", {
|
|
296
|
+
timeZone: requested,
|
|
297
|
+
}).resolvedOptions().timeZone;
|
|
298
|
+
} catch {
|
|
299
|
+
throw new Error(`invalid IANA time zone: ${requested}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function timeZoneFormatter(timeZone) {
|
|
304
|
+
let formatter = CRON_TIME_ZONE_FORMATTERS.get(timeZone);
|
|
305
|
+
if (!formatter) {
|
|
306
|
+
formatter = new Intl.DateTimeFormat("en-US-u-ca-gregory-nu-latn", {
|
|
307
|
+
timeZone,
|
|
308
|
+
year: "numeric",
|
|
309
|
+
month: "2-digit",
|
|
310
|
+
day: "2-digit",
|
|
311
|
+
hour: "2-digit",
|
|
312
|
+
minute: "2-digit",
|
|
313
|
+
hourCycle: "h23",
|
|
314
|
+
});
|
|
315
|
+
CRON_TIME_ZONE_FORMATTERS.set(timeZone, formatter);
|
|
316
|
+
}
|
|
317
|
+
return formatter;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function timeZoneParts(epochMs, timeZone) {
|
|
321
|
+
const values = {};
|
|
322
|
+
for (const part of timeZoneFormatter(timeZone).formatToParts(epochMs)) {
|
|
323
|
+
if (part.type !== "literal") values[part.type] = Number(part.value);
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
year: values.year,
|
|
327
|
+
minute: values.minute,
|
|
328
|
+
hour: values.hour,
|
|
329
|
+
day: values.day,
|
|
330
|
+
month: values.month,
|
|
331
|
+
dayOfWeek: new Date(
|
|
332
|
+
Date.UTC(values.year, values.month - 1, values.day),
|
|
333
|
+
).getUTCDay(),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
207
337
|
function parseCronField(field, [min, max]) {
|
|
208
338
|
const values = new Set();
|
|
209
339
|
for (const part of String(field).split(",")) {
|
|
@@ -252,13 +382,175 @@ function cronMatches(sets, date) {
|
|
|
252
382
|
);
|
|
253
383
|
}
|
|
254
384
|
|
|
385
|
+
function timeZoneOffsetAt(epochMs, timeZone) {
|
|
386
|
+
const minuteEpoch = Math.floor(epochMs / 60_000) * 60_000;
|
|
387
|
+
const parts = timeZoneParts(minuteEpoch, timeZone);
|
|
388
|
+
return (
|
|
389
|
+
Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute) -
|
|
390
|
+
minuteEpoch
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function localDayOffsets(year, month, day, timeZone) {
|
|
395
|
+
const reference = Date.UTC(year, month - 1, day, 12, 0, 0);
|
|
396
|
+
return [
|
|
397
|
+
...new Set(
|
|
398
|
+
[-36, -12, 0, 12, 36].map((hours) =>
|
|
399
|
+
timeZoneOffsetAt(reference + hours * 3_600_000, timeZone),
|
|
400
|
+
),
|
|
401
|
+
),
|
|
402
|
+
];
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function epochCandidatesForCivilMinute(
|
|
406
|
+
{ year, month, day, hour, minute },
|
|
407
|
+
timeZone,
|
|
408
|
+
offsets,
|
|
409
|
+
) {
|
|
410
|
+
const civilEpoch = Date.UTC(year, month - 1, day, hour, minute, 0);
|
|
411
|
+
const matches = [];
|
|
412
|
+
for (const offset of offsets) {
|
|
413
|
+
const epoch = civilEpoch - offset;
|
|
414
|
+
const actual = timeZoneParts(epoch, timeZone);
|
|
415
|
+
if (
|
|
416
|
+
actual.year === year &&
|
|
417
|
+
actual.month === month &&
|
|
418
|
+
actual.day === day &&
|
|
419
|
+
actual.hour === hour &&
|
|
420
|
+
actual.minute === minute
|
|
421
|
+
) {
|
|
422
|
+
matches.push(epoch);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return [...new Set(matches)].sort((a, b) => a - b);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function nextCronTimeInTimeZone(sets, fromMs, timeZone) {
|
|
429
|
+
const from = Number(fromMs);
|
|
430
|
+
const firstLocal = timeZoneParts(from, timeZone);
|
|
431
|
+
const firstLocalDay = Date.UTC(
|
|
432
|
+
firstLocal.year,
|
|
433
|
+
firstLocal.month - 1,
|
|
434
|
+
firstLocal.day,
|
|
435
|
+
);
|
|
436
|
+
const minuteLimit = 366 * 24 * 60;
|
|
437
|
+
const lastEpoch = Math.floor(from / 60_000) * 60_000 + minuteLimit * 60_000;
|
|
438
|
+
const hours = [...sets[1]].sort((a, b) => a - b);
|
|
439
|
+
const minutes = [...sets[0]].sort((a, b) => a - b);
|
|
440
|
+
let earliest = null;
|
|
441
|
+
|
|
442
|
+
// Iterate civil dates instead of formatting every real minute. At most two
|
|
443
|
+
// UTC offsets can map a civil minute around a DST transition; verifying each
|
|
444
|
+
// reconstructed candidate preserves missing and repeated wall-time behavior.
|
|
445
|
+
// Start one civil day earlier because a rare midnight rollback can make a
|
|
446
|
+
// future real instant display as the previous local date. Once a later civil
|
|
447
|
+
// date is more than 36 hours beyond the best epoch, no supported IANA offset
|
|
448
|
+
// can map that date back before the current best candidate.
|
|
449
|
+
for (let dayOffset = -1; dayOffset <= 367; dayOffset += 1) {
|
|
450
|
+
const civilDay = firstLocalDay + dayOffset * 86_400_000;
|
|
451
|
+
if (earliest != null && civilDay - 36 * 3_600_000 > earliest) break;
|
|
452
|
+
const date = new Date(civilDay);
|
|
453
|
+
const year = date.getUTCFullYear();
|
|
454
|
+
const month = date.getUTCMonth() + 1;
|
|
455
|
+
const day = date.getUTCDate();
|
|
456
|
+
const dayOfWeek = date.getUTCDay();
|
|
457
|
+
if (
|
|
458
|
+
!sets[2].has(day) ||
|
|
459
|
+
!sets[3].has(month) ||
|
|
460
|
+
(!sets[4].has(dayOfWeek) && !(sets[4].has(7) && dayOfWeek === 0))
|
|
461
|
+
) {
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
const offsets = localDayOffsets(year, month, day, timeZone);
|
|
465
|
+
for (const hour of hours) {
|
|
466
|
+
for (const minute of minutes) {
|
|
467
|
+
for (const epoch of epochCandidatesForCivilMinute(
|
|
468
|
+
{ year, month, day, hour, minute },
|
|
469
|
+
timeZone,
|
|
470
|
+
offsets,
|
|
471
|
+
)) {
|
|
472
|
+
if (
|
|
473
|
+
epoch > from &&
|
|
474
|
+
epoch <= lastEpoch &&
|
|
475
|
+
(earliest == null || epoch < earliest)
|
|
476
|
+
) {
|
|
477
|
+
earliest = epoch;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return earliest;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function previousCronTimeInTimeZone(sets, fromMs, timeZone) {
|
|
487
|
+
const from = Number(fromMs);
|
|
488
|
+
const firstLocal = timeZoneParts(from, timeZone);
|
|
489
|
+
const firstLocalDay = Date.UTC(
|
|
490
|
+
firstLocal.year,
|
|
491
|
+
firstLocal.month - 1,
|
|
492
|
+
firstLocal.day,
|
|
493
|
+
);
|
|
494
|
+
const minuteLimit = 366 * 24 * 60;
|
|
495
|
+
const firstEpoch = Math.floor(from / 60_000) * 60_000 - minuteLimit * 60_000;
|
|
496
|
+
const hours = [...sets[1]].sort((a, b) => b - a);
|
|
497
|
+
const minutes = [...sets[0]].sort((a, b) => b - a);
|
|
498
|
+
let latest = null;
|
|
499
|
+
|
|
500
|
+
// Start one civil day ahead because a rare midnight transition can make a
|
|
501
|
+
// prior real instant display as the following local date.
|
|
502
|
+
for (let dayOffset = 1; dayOffset >= -367; dayOffset -= 1) {
|
|
503
|
+
const civilDay = firstLocalDay + dayOffset * 86_400_000;
|
|
504
|
+
if (latest != null && civilDay + 36 * 3_600_000 < latest) break;
|
|
505
|
+
const date = new Date(civilDay);
|
|
506
|
+
const year = date.getUTCFullYear();
|
|
507
|
+
const month = date.getUTCMonth() + 1;
|
|
508
|
+
const day = date.getUTCDate();
|
|
509
|
+
const dayOfWeek = date.getUTCDay();
|
|
510
|
+
if (
|
|
511
|
+
!sets[2].has(day) ||
|
|
512
|
+
!sets[3].has(month) ||
|
|
513
|
+
(!sets[4].has(dayOfWeek) && !(sets[4].has(7) && dayOfWeek === 0))
|
|
514
|
+
) {
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
const offsets = localDayOffsets(year, month, day, timeZone);
|
|
518
|
+
for (const hour of hours) {
|
|
519
|
+
for (const minute of minutes) {
|
|
520
|
+
for (const epoch of epochCandidatesForCivilMinute(
|
|
521
|
+
{ year, month, day, hour, minute },
|
|
522
|
+
timeZone,
|
|
523
|
+
offsets,
|
|
524
|
+
)) {
|
|
525
|
+
if (
|
|
526
|
+
epoch < from &&
|
|
527
|
+
epoch >= firstEpoch &&
|
|
528
|
+
(latest == null || epoch > latest)
|
|
529
|
+
) {
|
|
530
|
+
latest = epoch;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return latest;
|
|
537
|
+
}
|
|
538
|
+
|
|
255
539
|
/**
|
|
256
|
-
* Next epoch-ms at which `expr` fires strictly after `fromMs
|
|
257
|
-
*
|
|
258
|
-
*
|
|
540
|
+
* Next epoch-ms at which `expr` fires strictly after `fromMs`, at minute
|
|
541
|
+
* granularity. By default this preserves host-local cron behavior; an explicit
|
|
542
|
+
* IANA `timeZone` evaluates the same epoch minutes against that civil clock.
|
|
543
|
+
* A repeated fall-back wall minute therefore maps to both real instants, while
|
|
544
|
+
* a skipped spring-forward wall minute does not exist. Scans up to ~366 days
|
|
545
|
+
* then gives up (returns null) so a never-matching expression can't loop
|
|
546
|
+
* forever.
|
|
259
547
|
*/
|
|
260
|
-
export function nextCronTime(expr, fromMs) {
|
|
548
|
+
export function nextCronTime(expr, fromMs, options = {}) {
|
|
261
549
|
const sets = parseCron(expr);
|
|
550
|
+
const timeZone = normalizeTimeZone(
|
|
551
|
+
typeof options === "string" ? options : options?.timeZone,
|
|
552
|
+
);
|
|
553
|
+
if (timeZone) return nextCronTimeInTimeZone(sets, fromMs, timeZone);
|
|
262
554
|
const cursor = new Date(fromMs);
|
|
263
555
|
cursor.setSeconds(0, 0);
|
|
264
556
|
cursor.setMinutes(cursor.getMinutes() + 1);
|
|
@@ -270,6 +562,25 @@ export function nextCronTime(expr, fromMs) {
|
|
|
270
562
|
return null;
|
|
271
563
|
}
|
|
272
564
|
|
|
565
|
+
/** Previous cron occurrence strictly before `fromMs`, bounded to one year. */
|
|
566
|
+
export function previousCronTime(expr, fromMs, options = {}) {
|
|
567
|
+
const sets = parseCron(expr);
|
|
568
|
+
const timeZone = normalizeTimeZone(
|
|
569
|
+
typeof options === "string" ? options : options?.timeZone,
|
|
570
|
+
);
|
|
571
|
+
if (timeZone) return previousCronTimeInTimeZone(sets, fromMs, timeZone);
|
|
572
|
+
const cursor = new Date(fromMs);
|
|
573
|
+
cursor.setSeconds(0, 0);
|
|
574
|
+
if (cursor.getTime() >= Number(fromMs))
|
|
575
|
+
cursor.setMinutes(cursor.getMinutes() - 1);
|
|
576
|
+
const limit = 366 * 24 * 60;
|
|
577
|
+
for (let i = 0; i < limit; i += 1) {
|
|
578
|
+
if (cronMatches(sets, cursor)) return cursor.getTime();
|
|
579
|
+
cursor.setMinutes(cursor.getMinutes() - 1);
|
|
580
|
+
}
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
|
|
273
584
|
// ─── store ───────────────────────────────────────────────────────────────
|
|
274
585
|
|
|
275
586
|
export class AgentScheduleStore {
|
|
@@ -362,12 +673,16 @@ export class AgentScheduleStore {
|
|
|
362
673
|
expiresAt = null,
|
|
363
674
|
expiresInMs = null,
|
|
364
675
|
jitterMs = 0,
|
|
676
|
+
timeZone = null,
|
|
365
677
|
} = options;
|
|
366
678
|
if (!prompt || typeof prompt !== "string") {
|
|
367
679
|
throw new Error("cron requires a prompt");
|
|
368
680
|
}
|
|
369
681
|
const now = this._now();
|
|
370
|
-
const
|
|
682
|
+
const normalizedTimeZone = normalizeTimeZone(timeZone);
|
|
683
|
+
const nextAt = nextCronTime(cron, now, {
|
|
684
|
+
timeZone: normalizedTimeZone,
|
|
685
|
+
}); // validates the expression and optional IANA time zone
|
|
371
686
|
if (nextAt == null) {
|
|
372
687
|
throw new Error(`cron "${cron}" has no upcoming match`);
|
|
373
688
|
}
|
|
@@ -378,6 +693,7 @@ export class AgentScheduleStore {
|
|
|
378
693
|
kind: "cron",
|
|
379
694
|
prompt,
|
|
380
695
|
cron,
|
|
696
|
+
...(normalizedTimeZone ? { timeZone: normalizedTimeZone } : {}),
|
|
381
697
|
label: label || null,
|
|
382
698
|
nextAt,
|
|
383
699
|
expiresAt: resolveExpiresAt(expiresAt, expiresInMs, now),
|
|
@@ -499,6 +815,11 @@ export class AgentScheduleStore {
|
|
|
499
815
|
return out;
|
|
500
816
|
}
|
|
501
817
|
|
|
818
|
+
get(id) {
|
|
819
|
+
if (typeof id !== "string" || id.length === 0) return null;
|
|
820
|
+
return this.list().find((entry) => entry.id === id) || null;
|
|
821
|
+
}
|
|
822
|
+
|
|
502
823
|
/** Entries whose time has come (pending wakeup / active cron|monitor). */
|
|
503
824
|
due(kind = null, atMs = null) {
|
|
504
825
|
const now = atMs != null ? atMs : this._now();
|
|
@@ -524,16 +845,20 @@ export class AgentScheduleStore {
|
|
|
524
845
|
* lease expires. This is deliberately separate from `due()` for backwards
|
|
525
846
|
* compatibility with read-only callers and injected test stores.
|
|
526
847
|
*/
|
|
527
|
-
claimDue(atMs = null, leaseMs = 120000) {
|
|
848
|
+
claimDue(atMs = null, leaseMs = 120000, kind = null) {
|
|
528
849
|
const now = atMs != null ? Number(atMs) : this._now();
|
|
529
850
|
const ttl = Math.max(1000, Number(leaseMs) || 120000);
|
|
851
|
+
const kinds = kind == null ? SCHEDULE_KINDS : [kind];
|
|
852
|
+
if (kinds.some((value) => !SCHEDULE_KINDS.includes(value))) {
|
|
853
|
+
throw new Error(`unknown schedule kind: ${kind}`);
|
|
854
|
+
}
|
|
530
855
|
this._ensureDir();
|
|
531
856
|
return withFileLock(
|
|
532
857
|
path.join(this.dir, ".agenda-claims"),
|
|
533
858
|
() => {
|
|
534
859
|
const claimed = [];
|
|
535
|
-
for (const
|
|
536
|
-
const entries = this._readAll(
|
|
860
|
+
for (const scheduleKind of kinds) {
|
|
861
|
+
const entries = this._readAll(scheduleKind);
|
|
537
862
|
let changed = false;
|
|
538
863
|
for (const entry of entries) {
|
|
539
864
|
if (!isSchedulableStatus(entry) || isEntryExpired(entry, now))
|
|
@@ -552,7 +877,7 @@ export class AgentScheduleStore {
|
|
|
552
877
|
});
|
|
553
878
|
changed = true;
|
|
554
879
|
}
|
|
555
|
-
if (changed) this._writeAll(
|
|
880
|
+
if (changed) this._writeAll(scheduleKind, entries);
|
|
556
881
|
}
|
|
557
882
|
return claimed;
|
|
558
883
|
},
|
|
@@ -571,6 +896,173 @@ export class AgentScheduleStore {
|
|
|
571
896
|
return null;
|
|
572
897
|
}
|
|
573
898
|
|
|
899
|
+
/**
|
|
900
|
+
* Bind a scheduler claim to the legacy Agenda entry before execution.
|
|
901
|
+
* The same lock used by legacy `claimDue()` prevents an old/new driver from
|
|
902
|
+
* starting the entry concurrently during migration.
|
|
903
|
+
*/
|
|
904
|
+
bindSchedulerExecution(
|
|
905
|
+
id,
|
|
906
|
+
{ occurrenceId, snapshotDigest, attempt, atMs = null } = {},
|
|
907
|
+
) {
|
|
908
|
+
const now = atMs != null ? Number(atMs) : this._now();
|
|
909
|
+
if (typeof occurrenceId !== "string" || occurrenceId.length === 0) {
|
|
910
|
+
throw agendaStoreError(
|
|
911
|
+
"AGENDA_SCHEDULER_BINDING_INVALID",
|
|
912
|
+
"Agenda scheduler occurrenceId is required",
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
if (typeof snapshotDigest !== "string" || snapshotDigest.length === 0) {
|
|
916
|
+
throw agendaStoreError(
|
|
917
|
+
"AGENDA_SCHEDULER_BINDING_INVALID",
|
|
918
|
+
"Agenda scheduler snapshotDigest is required",
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
if (!Number.isSafeInteger(attempt) || attempt < 1) {
|
|
922
|
+
throw agendaStoreError(
|
|
923
|
+
"AGENDA_SCHEDULER_BINDING_INVALID",
|
|
924
|
+
"Agenda scheduler attempt must be a positive integer",
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
this._ensureDir();
|
|
928
|
+
return withFileLock(
|
|
929
|
+
path.join(this.dir, ".agenda-claims"),
|
|
930
|
+
() => {
|
|
931
|
+
for (const kind of SCHEDULE_KINDS) {
|
|
932
|
+
const entries = this._readAll(kind);
|
|
933
|
+
const entry = entries.find((candidate) => candidate.id === id);
|
|
934
|
+
if (!entry) continue;
|
|
935
|
+
const prior = entry.schedulerExecution;
|
|
936
|
+
if (prior?.status === "running") {
|
|
937
|
+
throw agendaStoreError(
|
|
938
|
+
"AGENDA_SCHEDULER_OUTCOME_UNKNOWN",
|
|
939
|
+
`Agenda scheduler execution outcome is unknown: ${id}`,
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
if (
|
|
943
|
+
prior?.status === "succeeded" &&
|
|
944
|
+
prior.occurrenceId === occurrenceId &&
|
|
945
|
+
prior.snapshotDigest === snapshotDigest
|
|
946
|
+
) {
|
|
947
|
+
return entry;
|
|
948
|
+
}
|
|
949
|
+
if (hasLiveExecutionLease(entry, now)) {
|
|
950
|
+
throw agendaStoreError(
|
|
951
|
+
"AGENDA_SCHEDULER_LEGACY_CLAIM_ACTIVE",
|
|
952
|
+
`Agenda entry is already claimed by a legacy runner: ${id}`,
|
|
953
|
+
{
|
|
954
|
+
retryable: true,
|
|
955
|
+
retryAt: Number(entry.executionLease.expiresAt),
|
|
956
|
+
},
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
entry.schedulerExecution = {
|
|
960
|
+
occurrenceId,
|
|
961
|
+
snapshotDigest,
|
|
962
|
+
attempt,
|
|
963
|
+
status: "running",
|
|
964
|
+
startedAt: now,
|
|
965
|
+
};
|
|
966
|
+
// Fence an older CLI that only understands the legacy claim field.
|
|
967
|
+
// Terminal scheduler evidence clears this lease; an outcome-unknown
|
|
968
|
+
// execution intentionally keeps it non-expiring until adjudicated so
|
|
969
|
+
// an old runner cannot duplicate an uncertain external observation or
|
|
970
|
+
// agent side effect.
|
|
971
|
+
entry.executionLease = {
|
|
972
|
+
owner: `scheduler:${occurrenceId}:${attempt}`,
|
|
973
|
+
claimedAt: now,
|
|
974
|
+
expiresAt: Number.MAX_SAFE_INTEGER,
|
|
975
|
+
};
|
|
976
|
+
this._writeAll(kind, entries);
|
|
977
|
+
return entry;
|
|
978
|
+
}
|
|
979
|
+
throw agendaStoreError(
|
|
980
|
+
"AGENDA_SCHEDULER_ENTRY_NOT_FOUND",
|
|
981
|
+
`Agenda entry does not exist: ${id}`,
|
|
982
|
+
);
|
|
983
|
+
},
|
|
984
|
+
{ failIfUnavailable: true },
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* Atomically publish scheduler evidence and the legacy Agenda state. A
|
|
990
|
+
* successful cron advances and a successful monitor records/re-arms exactly
|
|
991
|
+
* once in the same JSONL replacement that records terminal evidence;
|
|
992
|
+
* failures keep the entry schedulable for the kernel's bounded retry policy.
|
|
993
|
+
*/
|
|
994
|
+
completeSchedulerExecution(
|
|
995
|
+
id,
|
|
996
|
+
{
|
|
997
|
+
occurrenceId,
|
|
998
|
+
snapshotDigest,
|
|
999
|
+
attempt,
|
|
1000
|
+
outcome,
|
|
1001
|
+
result = null,
|
|
1002
|
+
error = null,
|
|
1003
|
+
monitorCheck = null,
|
|
1004
|
+
atMs = null,
|
|
1005
|
+
} = {},
|
|
1006
|
+
) {
|
|
1007
|
+
if (!["succeeded", "failed"].includes(outcome)) {
|
|
1008
|
+
throw agendaStoreError(
|
|
1009
|
+
"AGENDA_SCHEDULER_OUTCOME_INVALID",
|
|
1010
|
+
"Agenda scheduler outcome must be succeeded or failed",
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
const now = atMs != null ? Number(atMs) : this._now();
|
|
1014
|
+
this._ensureDir();
|
|
1015
|
+
return withFileLock(
|
|
1016
|
+
path.join(this.dir, ".agenda-claims"),
|
|
1017
|
+
() => {
|
|
1018
|
+
for (const kind of SCHEDULE_KINDS) {
|
|
1019
|
+
const entries = this._readAll(kind);
|
|
1020
|
+
const entry = entries.find((candidate) => candidate.id === id);
|
|
1021
|
+
if (!entry) continue;
|
|
1022
|
+
const evidence = entry.schedulerExecution;
|
|
1023
|
+
if (
|
|
1024
|
+
!evidence ||
|
|
1025
|
+
evidence.occurrenceId !== occurrenceId ||
|
|
1026
|
+
evidence.snapshotDigest !== snapshotDigest ||
|
|
1027
|
+
evidence.attempt !== attempt ||
|
|
1028
|
+
evidence.status !== "running"
|
|
1029
|
+
) {
|
|
1030
|
+
throw agendaStoreError(
|
|
1031
|
+
"AGENDA_SCHEDULER_BINDING_MISMATCH",
|
|
1032
|
+
`Agenda scheduler evidence does not match the active claim: ${id}`,
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
let storedResult = result;
|
|
1036
|
+
if (outcome === "succeeded") {
|
|
1037
|
+
if (entry.kind === "monitor") {
|
|
1038
|
+
applyScheduledMonitorSuccess(entry, monitorCheck, now);
|
|
1039
|
+
storedResult =
|
|
1040
|
+
result && typeof result === "object" && !Array.isArray(result)
|
|
1041
|
+
? { ...result, status: entry.status }
|
|
1042
|
+
: result;
|
|
1043
|
+
} else {
|
|
1044
|
+
applyScheduledAgentSuccess(entry, now);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
entry.schedulerExecution = {
|
|
1048
|
+
...evidence,
|
|
1049
|
+
status: outcome,
|
|
1050
|
+
endedAt: now,
|
|
1051
|
+
...(outcome === "succeeded" ? { result: storedResult } : { error }),
|
|
1052
|
+
};
|
|
1053
|
+
delete entry.executionLease;
|
|
1054
|
+
this._writeAll(kind, entries);
|
|
1055
|
+
return entry;
|
|
1056
|
+
}
|
|
1057
|
+
throw agendaStoreError(
|
|
1058
|
+
"AGENDA_SCHEDULER_ENTRY_NOT_FOUND",
|
|
1059
|
+
`Agenda entry does not exist: ${id}`,
|
|
1060
|
+
);
|
|
1061
|
+
},
|
|
1062
|
+
{ failIfUnavailable: true },
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
574
1066
|
/**
|
|
575
1067
|
* Retire every schedulable entry whose `expiresAt` has passed (status →
|
|
576
1068
|
* "expired", stamping `expiredAt`), across all kinds, so it never fires again.
|
|
@@ -648,7 +1140,9 @@ export class AgentScheduleStore {
|
|
|
648
1140
|
return this._mutate("cron", id, (entry) => {
|
|
649
1141
|
entry.lastRunAt = now;
|
|
650
1142
|
entry.runs = (entry.runs || 0) + 1;
|
|
651
|
-
const next = nextCronTime(entry.cron, now
|
|
1143
|
+
const next = nextCronTime(entry.cron, now, {
|
|
1144
|
+
timeZone: entry.timeZone || null,
|
|
1145
|
+
});
|
|
652
1146
|
if (next == null) {
|
|
653
1147
|
entry.status = "exhausted";
|
|
654
1148
|
} else {
|
|
@@ -673,32 +1167,11 @@ export class AgentScheduleStore {
|
|
|
673
1167
|
) {
|
|
674
1168
|
const now = atMs != null ? atMs : this._now();
|
|
675
1169
|
return this._mutate("monitor", id, (entry) => {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
if (mtimeMs != null && entry.lastMtimeMs == null) {
|
|
682
|
-
entry.lastMtimeMs = mtimeMs;
|
|
683
|
-
}
|
|
684
|
-
if (matched) {
|
|
685
|
-
entry.status = "matched";
|
|
686
|
-
entry.matchedAt = now;
|
|
687
|
-
// Audit record of the firing: the deterministic monitor-event id + the
|
|
688
|
-
// (always SYSTEM/steer) authority the event was delivered under. Durable
|
|
689
|
-
// on the entry so `cc agenda list --json` and a future resident daemon
|
|
690
|
-
// can recognise / dedup the exact observation. Only stamped when the
|
|
691
|
-
// caller supplies them, so legacy callers stay byte-identical.
|
|
692
|
-
if (eventId != null) {
|
|
693
|
-
entry.lastEventId = String(eventId);
|
|
694
|
-
entry.lastEventAt = now;
|
|
695
|
-
}
|
|
696
|
-
if (authority != null) entry.lastAuthority = String(authority);
|
|
697
|
-
} else if (entry.maxChecks != null && entry.checks >= entry.maxChecks) {
|
|
698
|
-
entry.status = "exhausted";
|
|
699
|
-
} else {
|
|
700
|
-
entry.nextAt = now + entry.intervalMs;
|
|
701
|
-
}
|
|
1170
|
+
applyScheduledMonitorSuccess(
|
|
1171
|
+
entry,
|
|
1172
|
+
{ matched: Boolean(matched), mtimeMs, eventId, authority },
|
|
1173
|
+
now,
|
|
1174
|
+
);
|
|
702
1175
|
});
|
|
703
1176
|
}
|
|
704
1177
|
|