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
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
import fs from "fs";
|
|
23
23
|
import os from "os";
|
|
24
24
|
import path from "path";
|
|
25
|
-
import { randomUUID } from "crypto";
|
|
25
|
+
import { createHash, randomUUID } from "crypto";
|
|
26
26
|
import { isEntryExpired, effectiveFireAt } from "./schedule-planner.js";
|
|
27
27
|
import { SUBAGENT_PERMISSION_MODES } from "./subagent-contract.js";
|
|
28
28
|
import { parseGoalCondition } from "./goal-condition-engine.js";
|
|
@@ -30,6 +30,7 @@ import { normalizeActionClass } from "./unattended-action-policy.js";
|
|
|
30
30
|
import { withFileLock } from "./with-file-lock.js";
|
|
31
31
|
|
|
32
32
|
export const SCHEDULE_KINDS = Object.freeze(["wakeup", "cron", "monitor"]);
|
|
33
|
+
export const AGENDA_SCHEDULER_MIGRATION_SCHEMA_VERSION = 1;
|
|
33
34
|
|
|
34
35
|
/** A valid absolute-epoch-ms expiry, or null (never expires). */
|
|
35
36
|
function normalizeExpiresAt(v) {
|
|
@@ -168,6 +169,87 @@ function hasLiveExecutionLease(entry, now) {
|
|
|
168
169
|
return Number(entry.executionLease?.expiresAt) > Number(now);
|
|
169
170
|
}
|
|
170
171
|
|
|
172
|
+
function stableJsonValue(value) {
|
|
173
|
+
if (Array.isArray(value)) return value.map(stableJsonValue);
|
|
174
|
+
if (value && typeof value === "object") {
|
|
175
|
+
return Object.fromEntries(
|
|
176
|
+
Object.keys(value)
|
|
177
|
+
.sort()
|
|
178
|
+
.map((key) => [key, stableJsonValue(value[key])]),
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
return value;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function agendaMigrationSource(entry) {
|
|
185
|
+
const source = { ...entry };
|
|
186
|
+
delete source.executionLease;
|
|
187
|
+
delete source.schedulerExecution;
|
|
188
|
+
delete source.schedulerMigration;
|
|
189
|
+
delete source.firedAt;
|
|
190
|
+
delete source.lastRunAt;
|
|
191
|
+
delete source.runs;
|
|
192
|
+
delete source.nextAt;
|
|
193
|
+
delete source.lastCheckAt;
|
|
194
|
+
delete source.checks;
|
|
195
|
+
delete source.lastMtimeMs;
|
|
196
|
+
delete source.lastEventId;
|
|
197
|
+
delete source.lastAuthority;
|
|
198
|
+
delete source.status;
|
|
199
|
+
return source;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function agendaMigrationSourceDigest(entry) {
|
|
203
|
+
return createHash("sha256")
|
|
204
|
+
.update("chainlesschain.agenda.scheduler-migration-source.v1\0", "utf8")
|
|
205
|
+
.update(
|
|
206
|
+
JSON.stringify(stableJsonValue(agendaMigrationSource(entry))),
|
|
207
|
+
"utf8",
|
|
208
|
+
)
|
|
209
|
+
.digest("hex");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function agendaMigrationSourceSnapshot(entry) {
|
|
213
|
+
return stableJsonValue(agendaMigrationSource(entry));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function schedulerMigrationFence(entry, now) {
|
|
217
|
+
const migration = entry.schedulerMigration;
|
|
218
|
+
if (
|
|
219
|
+
migration?.schemaVersion !== AGENDA_SCHEDULER_MIGRATION_SCHEMA_VERSION ||
|
|
220
|
+
typeof migration.retirementToken !== "string" ||
|
|
221
|
+
migration.retirementToken.length === 0
|
|
222
|
+
) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
return (
|
|
226
|
+
hasLiveExecutionLease(entry, now) &&
|
|
227
|
+
entry.executionLease?.owner ===
|
|
228
|
+
`scheduler-migration:${migration.retirementToken}`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function applySchedulerMigrationFence(entry, now) {
|
|
233
|
+
const token = entry.schedulerMigration?.retirementToken;
|
|
234
|
+
if (typeof token !== "string" || token.length === 0) return false;
|
|
235
|
+
entry.executionLease = {
|
|
236
|
+
owner: `scheduler-migration:${token}`,
|
|
237
|
+
claimedAt: now,
|
|
238
|
+
expiresAt: Number.MAX_SAFE_INTEGER,
|
|
239
|
+
};
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function normalizeMigrationField(value, field) {
|
|
244
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
245
|
+
throw agendaStoreError(
|
|
246
|
+
"AGENDA_SCHEDULER_MIGRATION_INVALID",
|
|
247
|
+
`Agenda scheduler migration ${field} is required`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return value.trim();
|
|
251
|
+
}
|
|
252
|
+
|
|
171
253
|
function agendaStoreError(code, message, { retryable = false, retryAt } = {}) {
|
|
172
254
|
const error = new Error(message);
|
|
173
255
|
error.code = code;
|
|
@@ -185,7 +267,9 @@ function applyScheduledAgentSuccess(entry, atMs) {
|
|
|
185
267
|
if (entry.kind === "cron") {
|
|
186
268
|
entry.lastRunAt = atMs;
|
|
187
269
|
entry.runs = (entry.runs || 0) + 1;
|
|
188
|
-
const next = nextCronTime(entry.cron, atMs
|
|
270
|
+
const next = nextCronTime(entry.cron, atMs, {
|
|
271
|
+
timeZone: entry.timeZone || null,
|
|
272
|
+
});
|
|
189
273
|
if (next == null) entry.status = "exhausted";
|
|
190
274
|
else entry.nextAt = next;
|
|
191
275
|
return;
|
|
@@ -196,6 +280,51 @@ function applyScheduledAgentSuccess(entry, atMs) {
|
|
|
196
280
|
);
|
|
197
281
|
}
|
|
198
282
|
|
|
283
|
+
function applyScheduledMonitorSuccess(entry, monitorCheck, atMs) {
|
|
284
|
+
if (
|
|
285
|
+
entry.kind !== "monitor" ||
|
|
286
|
+
!monitorCheck ||
|
|
287
|
+
typeof monitorCheck !== "object" ||
|
|
288
|
+
Array.isArray(monitorCheck) ||
|
|
289
|
+
typeof monitorCheck.matched !== "boolean"
|
|
290
|
+
) {
|
|
291
|
+
throw agendaStoreError(
|
|
292
|
+
"AGENDA_SCHEDULER_MONITOR_RESULT_INVALID",
|
|
293
|
+
`Scheduler monitor result is invalid: ${entry.id}`,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
const mtimeMs =
|
|
297
|
+
monitorCheck.mtimeMs === null || monitorCheck.mtimeMs === undefined
|
|
298
|
+
? null
|
|
299
|
+
: Number(monitorCheck.mtimeMs);
|
|
300
|
+
if (mtimeMs !== null && (!Number.isFinite(mtimeMs) || mtimeMs < 0)) {
|
|
301
|
+
throw agendaStoreError(
|
|
302
|
+
"AGENDA_SCHEDULER_MONITOR_RESULT_INVALID",
|
|
303
|
+
`Scheduler monitor mtime is invalid: ${entry.id}`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
entry.checks = (entry.checks || 0) + 1;
|
|
307
|
+
entry.lastCheckAt = atMs;
|
|
308
|
+
if (mtimeMs !== null && entry.lastMtimeMs == null) {
|
|
309
|
+
entry.lastMtimeMs = mtimeMs;
|
|
310
|
+
}
|
|
311
|
+
if (monitorCheck.matched) {
|
|
312
|
+
entry.status = "matched";
|
|
313
|
+
entry.matchedAt = atMs;
|
|
314
|
+
if (monitorCheck.eventId != null) {
|
|
315
|
+
entry.lastEventId = String(monitorCheck.eventId);
|
|
316
|
+
entry.lastEventAt = atMs;
|
|
317
|
+
}
|
|
318
|
+
if (monitorCheck.authority != null) {
|
|
319
|
+
entry.lastAuthority = String(monitorCheck.authority);
|
|
320
|
+
}
|
|
321
|
+
} else if (entry.maxChecks != null && entry.checks >= entry.maxChecks) {
|
|
322
|
+
entry.status = "exhausted";
|
|
323
|
+
} else {
|
|
324
|
+
entry.nextAt = atMs + entry.intervalMs;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
199
328
|
/** Statuses an entry can never leave — safe to prune. */
|
|
200
329
|
export const TERMINAL_STATUSES = Object.freeze([
|
|
201
330
|
"fired",
|
|
@@ -232,6 +361,61 @@ const CRON_RANGES = [
|
|
|
232
361
|
[0, 6], // day of week (0/7 = Sunday)
|
|
233
362
|
];
|
|
234
363
|
|
|
364
|
+
const CRON_TIME_ZONE_FORMATTERS = new Map();
|
|
365
|
+
|
|
366
|
+
export function normalizeTimeZone(value) {
|
|
367
|
+
if (value == null || value === "") return null;
|
|
368
|
+
if (typeof value !== "string") {
|
|
369
|
+
throw new Error("cron timeZone must be an IANA time zone string");
|
|
370
|
+
}
|
|
371
|
+
const requested = value.trim();
|
|
372
|
+
if (!requested) return null;
|
|
373
|
+
if (requested.length > 128 || /\p{Cc}/u.test(requested)) {
|
|
374
|
+
throw new Error("invalid IANA time zone");
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
return new Intl.DateTimeFormat("en-US", {
|
|
378
|
+
timeZone: requested,
|
|
379
|
+
}).resolvedOptions().timeZone;
|
|
380
|
+
} catch {
|
|
381
|
+
throw new Error(`invalid IANA time zone: ${requested}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function timeZoneFormatter(timeZone) {
|
|
386
|
+
let formatter = CRON_TIME_ZONE_FORMATTERS.get(timeZone);
|
|
387
|
+
if (!formatter) {
|
|
388
|
+
formatter = new Intl.DateTimeFormat("en-US-u-ca-gregory-nu-latn", {
|
|
389
|
+
timeZone,
|
|
390
|
+
year: "numeric",
|
|
391
|
+
month: "2-digit",
|
|
392
|
+
day: "2-digit",
|
|
393
|
+
hour: "2-digit",
|
|
394
|
+
minute: "2-digit",
|
|
395
|
+
hourCycle: "h23",
|
|
396
|
+
});
|
|
397
|
+
CRON_TIME_ZONE_FORMATTERS.set(timeZone, formatter);
|
|
398
|
+
}
|
|
399
|
+
return formatter;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function timeZoneParts(epochMs, timeZone) {
|
|
403
|
+
const values = {};
|
|
404
|
+
for (const part of timeZoneFormatter(timeZone).formatToParts(epochMs)) {
|
|
405
|
+
if (part.type !== "literal") values[part.type] = Number(part.value);
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
year: values.year,
|
|
409
|
+
minute: values.minute,
|
|
410
|
+
hour: values.hour,
|
|
411
|
+
day: values.day,
|
|
412
|
+
month: values.month,
|
|
413
|
+
dayOfWeek: new Date(
|
|
414
|
+
Date.UTC(values.year, values.month - 1, values.day),
|
|
415
|
+
).getUTCDay(),
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
235
419
|
function parseCronField(field, [min, max]) {
|
|
236
420
|
const values = new Set();
|
|
237
421
|
for (const part of String(field).split(",")) {
|
|
@@ -280,13 +464,175 @@ function cronMatches(sets, date) {
|
|
|
280
464
|
);
|
|
281
465
|
}
|
|
282
466
|
|
|
467
|
+
function timeZoneOffsetAt(epochMs, timeZone) {
|
|
468
|
+
const minuteEpoch = Math.floor(epochMs / 60_000) * 60_000;
|
|
469
|
+
const parts = timeZoneParts(minuteEpoch, timeZone);
|
|
470
|
+
return (
|
|
471
|
+
Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute) -
|
|
472
|
+
minuteEpoch
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function localDayOffsets(year, month, day, timeZone) {
|
|
477
|
+
const reference = Date.UTC(year, month - 1, day, 12, 0, 0);
|
|
478
|
+
return [
|
|
479
|
+
...new Set(
|
|
480
|
+
[-36, -12, 0, 12, 36].map((hours) =>
|
|
481
|
+
timeZoneOffsetAt(reference + hours * 3_600_000, timeZone),
|
|
482
|
+
),
|
|
483
|
+
),
|
|
484
|
+
];
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function epochCandidatesForCivilMinute(
|
|
488
|
+
{ year, month, day, hour, minute },
|
|
489
|
+
timeZone,
|
|
490
|
+
offsets,
|
|
491
|
+
) {
|
|
492
|
+
const civilEpoch = Date.UTC(year, month - 1, day, hour, minute, 0);
|
|
493
|
+
const matches = [];
|
|
494
|
+
for (const offset of offsets) {
|
|
495
|
+
const epoch = civilEpoch - offset;
|
|
496
|
+
const actual = timeZoneParts(epoch, timeZone);
|
|
497
|
+
if (
|
|
498
|
+
actual.year === year &&
|
|
499
|
+
actual.month === month &&
|
|
500
|
+
actual.day === day &&
|
|
501
|
+
actual.hour === hour &&
|
|
502
|
+
actual.minute === minute
|
|
503
|
+
) {
|
|
504
|
+
matches.push(epoch);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return [...new Set(matches)].sort((a, b) => a - b);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function nextCronTimeInTimeZone(sets, fromMs, timeZone) {
|
|
511
|
+
const from = Number(fromMs);
|
|
512
|
+
const firstLocal = timeZoneParts(from, timeZone);
|
|
513
|
+
const firstLocalDay = Date.UTC(
|
|
514
|
+
firstLocal.year,
|
|
515
|
+
firstLocal.month - 1,
|
|
516
|
+
firstLocal.day,
|
|
517
|
+
);
|
|
518
|
+
const minuteLimit = 366 * 24 * 60;
|
|
519
|
+
const lastEpoch = Math.floor(from / 60_000) * 60_000 + minuteLimit * 60_000;
|
|
520
|
+
const hours = [...sets[1]].sort((a, b) => a - b);
|
|
521
|
+
const minutes = [...sets[0]].sort((a, b) => a - b);
|
|
522
|
+
let earliest = null;
|
|
523
|
+
|
|
524
|
+
// Iterate civil dates instead of formatting every real minute. At most two
|
|
525
|
+
// UTC offsets can map a civil minute around a DST transition; verifying each
|
|
526
|
+
// reconstructed candidate preserves missing and repeated wall-time behavior.
|
|
527
|
+
// Start one civil day earlier because a rare midnight rollback can make a
|
|
528
|
+
// future real instant display as the previous local date. Once a later civil
|
|
529
|
+
// date is more than 36 hours beyond the best epoch, no supported IANA offset
|
|
530
|
+
// can map that date back before the current best candidate.
|
|
531
|
+
for (let dayOffset = -1; dayOffset <= 367; dayOffset += 1) {
|
|
532
|
+
const civilDay = firstLocalDay + dayOffset * 86_400_000;
|
|
533
|
+
if (earliest != null && civilDay - 36 * 3_600_000 > earliest) break;
|
|
534
|
+
const date = new Date(civilDay);
|
|
535
|
+
const year = date.getUTCFullYear();
|
|
536
|
+
const month = date.getUTCMonth() + 1;
|
|
537
|
+
const day = date.getUTCDate();
|
|
538
|
+
const dayOfWeek = date.getUTCDay();
|
|
539
|
+
if (
|
|
540
|
+
!sets[2].has(day) ||
|
|
541
|
+
!sets[3].has(month) ||
|
|
542
|
+
(!sets[4].has(dayOfWeek) && !(sets[4].has(7) && dayOfWeek === 0))
|
|
543
|
+
) {
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
const offsets = localDayOffsets(year, month, day, timeZone);
|
|
547
|
+
for (const hour of hours) {
|
|
548
|
+
for (const minute of minutes) {
|
|
549
|
+
for (const epoch of epochCandidatesForCivilMinute(
|
|
550
|
+
{ year, month, day, hour, minute },
|
|
551
|
+
timeZone,
|
|
552
|
+
offsets,
|
|
553
|
+
)) {
|
|
554
|
+
if (
|
|
555
|
+
epoch > from &&
|
|
556
|
+
epoch <= lastEpoch &&
|
|
557
|
+
(earliest == null || epoch < earliest)
|
|
558
|
+
) {
|
|
559
|
+
earliest = epoch;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return earliest;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function previousCronTimeInTimeZone(sets, fromMs, timeZone) {
|
|
569
|
+
const from = Number(fromMs);
|
|
570
|
+
const firstLocal = timeZoneParts(from, timeZone);
|
|
571
|
+
const firstLocalDay = Date.UTC(
|
|
572
|
+
firstLocal.year,
|
|
573
|
+
firstLocal.month - 1,
|
|
574
|
+
firstLocal.day,
|
|
575
|
+
);
|
|
576
|
+
const minuteLimit = 366 * 24 * 60;
|
|
577
|
+
const firstEpoch = Math.floor(from / 60_000) * 60_000 - minuteLimit * 60_000;
|
|
578
|
+
const hours = [...sets[1]].sort((a, b) => b - a);
|
|
579
|
+
const minutes = [...sets[0]].sort((a, b) => b - a);
|
|
580
|
+
let latest = null;
|
|
581
|
+
|
|
582
|
+
// Start one civil day ahead because a rare midnight transition can make a
|
|
583
|
+
// prior real instant display as the following local date.
|
|
584
|
+
for (let dayOffset = 1; dayOffset >= -367; dayOffset -= 1) {
|
|
585
|
+
const civilDay = firstLocalDay + dayOffset * 86_400_000;
|
|
586
|
+
if (latest != null && civilDay + 36 * 3_600_000 < latest) break;
|
|
587
|
+
const date = new Date(civilDay);
|
|
588
|
+
const year = date.getUTCFullYear();
|
|
589
|
+
const month = date.getUTCMonth() + 1;
|
|
590
|
+
const day = date.getUTCDate();
|
|
591
|
+
const dayOfWeek = date.getUTCDay();
|
|
592
|
+
if (
|
|
593
|
+
!sets[2].has(day) ||
|
|
594
|
+
!sets[3].has(month) ||
|
|
595
|
+
(!sets[4].has(dayOfWeek) && !(sets[4].has(7) && dayOfWeek === 0))
|
|
596
|
+
) {
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
const offsets = localDayOffsets(year, month, day, timeZone);
|
|
600
|
+
for (const hour of hours) {
|
|
601
|
+
for (const minute of minutes) {
|
|
602
|
+
for (const epoch of epochCandidatesForCivilMinute(
|
|
603
|
+
{ year, month, day, hour, minute },
|
|
604
|
+
timeZone,
|
|
605
|
+
offsets,
|
|
606
|
+
)) {
|
|
607
|
+
if (
|
|
608
|
+
epoch < from &&
|
|
609
|
+
epoch >= firstEpoch &&
|
|
610
|
+
(latest == null || epoch > latest)
|
|
611
|
+
) {
|
|
612
|
+
latest = epoch;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return latest;
|
|
619
|
+
}
|
|
620
|
+
|
|
283
621
|
/**
|
|
284
|
-
* Next epoch-ms at which `expr` fires strictly after `fromMs
|
|
285
|
-
*
|
|
286
|
-
*
|
|
622
|
+
* Next epoch-ms at which `expr` fires strictly after `fromMs`, at minute
|
|
623
|
+
* granularity. By default this preserves host-local cron behavior; an explicit
|
|
624
|
+
* IANA `timeZone` evaluates the same epoch minutes against that civil clock.
|
|
625
|
+
* A repeated fall-back wall minute therefore maps to both real instants, while
|
|
626
|
+
* a skipped spring-forward wall minute does not exist. Scans up to ~366 days
|
|
627
|
+
* then gives up (returns null) so a never-matching expression can't loop
|
|
628
|
+
* forever.
|
|
287
629
|
*/
|
|
288
|
-
export function nextCronTime(expr, fromMs) {
|
|
630
|
+
export function nextCronTime(expr, fromMs, options = {}) {
|
|
289
631
|
const sets = parseCron(expr);
|
|
632
|
+
const timeZone = normalizeTimeZone(
|
|
633
|
+
typeof options === "string" ? options : options?.timeZone,
|
|
634
|
+
);
|
|
635
|
+
if (timeZone) return nextCronTimeInTimeZone(sets, fromMs, timeZone);
|
|
290
636
|
const cursor = new Date(fromMs);
|
|
291
637
|
cursor.setSeconds(0, 0);
|
|
292
638
|
cursor.setMinutes(cursor.getMinutes() + 1);
|
|
@@ -298,6 +644,25 @@ export function nextCronTime(expr, fromMs) {
|
|
|
298
644
|
return null;
|
|
299
645
|
}
|
|
300
646
|
|
|
647
|
+
/** Previous cron occurrence strictly before `fromMs`, bounded to one year. */
|
|
648
|
+
export function previousCronTime(expr, fromMs, options = {}) {
|
|
649
|
+
const sets = parseCron(expr);
|
|
650
|
+
const timeZone = normalizeTimeZone(
|
|
651
|
+
typeof options === "string" ? options : options?.timeZone,
|
|
652
|
+
);
|
|
653
|
+
if (timeZone) return previousCronTimeInTimeZone(sets, fromMs, timeZone);
|
|
654
|
+
const cursor = new Date(fromMs);
|
|
655
|
+
cursor.setSeconds(0, 0);
|
|
656
|
+
if (cursor.getTime() >= Number(fromMs))
|
|
657
|
+
cursor.setMinutes(cursor.getMinutes() - 1);
|
|
658
|
+
const limit = 366 * 24 * 60;
|
|
659
|
+
for (let i = 0; i < limit; i += 1) {
|
|
660
|
+
if (cronMatches(sets, cursor)) return cursor.getTime();
|
|
661
|
+
cursor.setMinutes(cursor.getMinutes() - 1);
|
|
662
|
+
}
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
|
|
301
666
|
// ─── store ───────────────────────────────────────────────────────────────
|
|
302
667
|
|
|
303
668
|
export class AgentScheduleStore {
|
|
@@ -390,12 +755,16 @@ export class AgentScheduleStore {
|
|
|
390
755
|
expiresAt = null,
|
|
391
756
|
expiresInMs = null,
|
|
392
757
|
jitterMs = 0,
|
|
758
|
+
timeZone = null,
|
|
393
759
|
} = options;
|
|
394
760
|
if (!prompt || typeof prompt !== "string") {
|
|
395
761
|
throw new Error("cron requires a prompt");
|
|
396
762
|
}
|
|
397
763
|
const now = this._now();
|
|
398
|
-
const
|
|
764
|
+
const normalizedTimeZone = normalizeTimeZone(timeZone);
|
|
765
|
+
const nextAt = nextCronTime(cron, now, {
|
|
766
|
+
timeZone: normalizedTimeZone,
|
|
767
|
+
}); // validates the expression and optional IANA time zone
|
|
399
768
|
if (nextAt == null) {
|
|
400
769
|
throw new Error(`cron "${cron}" has no upcoming match`);
|
|
401
770
|
}
|
|
@@ -406,6 +775,7 @@ export class AgentScheduleStore {
|
|
|
406
775
|
kind: "cron",
|
|
407
776
|
prompt,
|
|
408
777
|
cron,
|
|
778
|
+
...(normalizedTimeZone ? { timeZone: normalizedTimeZone } : {}),
|
|
409
779
|
label: label || null,
|
|
410
780
|
nextAt,
|
|
411
781
|
expiresAt: resolveExpiresAt(expiresAt, expiresInMs, now),
|
|
@@ -532,15 +902,246 @@ export class AgentScheduleStore {
|
|
|
532
902
|
return this.list().find((entry) => entry.id === id) || null;
|
|
533
903
|
}
|
|
534
904
|
|
|
905
|
+
prepareSchedulerMigration(
|
|
906
|
+
id,
|
|
907
|
+
{ migrationId, sourceDigest, targetJobId } = {},
|
|
908
|
+
) {
|
|
909
|
+
const migration = normalizeMigrationField(migrationId, "migrationId");
|
|
910
|
+
const expectedDigest = normalizeMigrationField(
|
|
911
|
+
sourceDigest,
|
|
912
|
+
"sourceDigest",
|
|
913
|
+
);
|
|
914
|
+
const target = normalizeMigrationField(targetJobId, "targetJobId");
|
|
915
|
+
this._ensureDir();
|
|
916
|
+
return withFileLock(
|
|
917
|
+
path.join(this.dir, ".agenda-claims"),
|
|
918
|
+
() => {
|
|
919
|
+
for (const kind of SCHEDULE_KINDS) {
|
|
920
|
+
const entries = this._readAll(kind);
|
|
921
|
+
const entry = entries.find((candidate) => candidate.id === id);
|
|
922
|
+
if (!entry) continue;
|
|
923
|
+
if (!isSchedulableStatus(entry)) {
|
|
924
|
+
throw agendaStoreError(
|
|
925
|
+
"AGENDA_SCHEDULER_ENTRY_TERMINAL",
|
|
926
|
+
`Agenda entry is not schedulable: ${id}`,
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
const actualDigest = agendaMigrationSourceDigest(entry);
|
|
930
|
+
if (actualDigest !== expectedDigest) {
|
|
931
|
+
throw agendaStoreError(
|
|
932
|
+
"AGENDA_SCHEDULER_MIGRATION_SOURCE_CHANGED",
|
|
933
|
+
`Agenda entry changed before migration: ${id}`,
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
const prior = entry.schedulerMigration;
|
|
937
|
+
if (prior) {
|
|
938
|
+
if (
|
|
939
|
+
prior.schemaVersion !==
|
|
940
|
+
AGENDA_SCHEDULER_MIGRATION_SCHEMA_VERSION ||
|
|
941
|
+
prior.sourceDigest !== expectedDigest ||
|
|
942
|
+
prior.targetJobId !== target ||
|
|
943
|
+
prior.migrationId !== migration ||
|
|
944
|
+
prior.state !== "prepared"
|
|
945
|
+
) {
|
|
946
|
+
throw agendaStoreError(
|
|
947
|
+
"AGENDA_SCHEDULER_MIGRATION_CONFLICT",
|
|
948
|
+
`Agenda entry already belongs to another scheduler migration: ${id}`,
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
return entry;
|
|
952
|
+
}
|
|
953
|
+
entry.schedulerMigration = {
|
|
954
|
+
schemaVersion: AGENDA_SCHEDULER_MIGRATION_SCHEMA_VERSION,
|
|
955
|
+
state: "prepared",
|
|
956
|
+
migrationId: migration,
|
|
957
|
+
sourceDigest: expectedDigest,
|
|
958
|
+
targetJobId: target,
|
|
959
|
+
preparedAt: this._now(),
|
|
960
|
+
};
|
|
961
|
+
this._writeAll(kind, entries);
|
|
962
|
+
return entry;
|
|
963
|
+
}
|
|
964
|
+
throw agendaStoreError(
|
|
965
|
+
"AGENDA_SCHEDULER_ENTRY_NOT_FOUND",
|
|
966
|
+
`Agenda entry does not exist: ${id}`,
|
|
967
|
+
);
|
|
968
|
+
},
|
|
969
|
+
{ failIfUnavailable: true },
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
retireForSchedulerMigration(
|
|
974
|
+
id,
|
|
975
|
+
{
|
|
976
|
+
migrationId,
|
|
977
|
+
sourceDigest,
|
|
978
|
+
targetJobId,
|
|
979
|
+
retirementToken,
|
|
980
|
+
atMs = null,
|
|
981
|
+
} = {},
|
|
982
|
+
) {
|
|
983
|
+
const migration = normalizeMigrationField(migrationId, "migrationId");
|
|
984
|
+
const expectedDigest = normalizeMigrationField(
|
|
985
|
+
sourceDigest,
|
|
986
|
+
"sourceDigest",
|
|
987
|
+
);
|
|
988
|
+
const target = normalizeMigrationField(targetJobId, "targetJobId");
|
|
989
|
+
const token = normalizeMigrationField(retirementToken, "retirementToken");
|
|
990
|
+
const now = atMs != null ? Number(atMs) : this._now();
|
|
991
|
+
this._ensureDir();
|
|
992
|
+
return withFileLock(
|
|
993
|
+
path.join(this.dir, ".agenda-claims"),
|
|
994
|
+
() => {
|
|
995
|
+
for (const kind of SCHEDULE_KINDS) {
|
|
996
|
+
const entries = this._readAll(kind);
|
|
997
|
+
const entry = entries.find((candidate) => candidate.id === id);
|
|
998
|
+
if (!entry) continue;
|
|
999
|
+
const prior = entry.schedulerMigration;
|
|
1000
|
+
if (
|
|
1001
|
+
prior?.schemaVersion !==
|
|
1002
|
+
AGENDA_SCHEDULER_MIGRATION_SCHEMA_VERSION ||
|
|
1003
|
+
prior.sourceDigest !== expectedDigest ||
|
|
1004
|
+
prior.targetJobId !== target ||
|
|
1005
|
+
prior.migrationId !== migration ||
|
|
1006
|
+
!["prepared", "retired"].includes(prior.state)
|
|
1007
|
+
) {
|
|
1008
|
+
throw agendaStoreError(
|
|
1009
|
+
"AGENDA_SCHEDULER_MIGRATION_CONFLICT",
|
|
1010
|
+
`Agenda migration preparation changed before retirement: ${id}`,
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
if (prior.state === "retired") {
|
|
1014
|
+
if (
|
|
1015
|
+
prior.retirementToken !== token ||
|
|
1016
|
+
!schedulerMigrationFence(entry, now)
|
|
1017
|
+
) {
|
|
1018
|
+
throw agendaStoreError(
|
|
1019
|
+
"AGENDA_SCHEDULER_MIGRATION_CONFLICT",
|
|
1020
|
+
`Agenda migration retirement fence changed: ${id}`,
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
return entry;
|
|
1024
|
+
}
|
|
1025
|
+
if (agendaMigrationSourceDigest(entry) !== expectedDigest) {
|
|
1026
|
+
throw agendaStoreError(
|
|
1027
|
+
"AGENDA_SCHEDULER_MIGRATION_SOURCE_CHANGED",
|
|
1028
|
+
`Agenda entry changed before retirement: ${id}`,
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
if (hasLiveExecutionLease(entry, now)) {
|
|
1032
|
+
throw agendaStoreError(
|
|
1033
|
+
"AGENDA_SCHEDULER_LEGACY_CLAIM_ACTIVE",
|
|
1034
|
+
`Agenda entry is already claimed by a legacy runner: ${id}`,
|
|
1035
|
+
{
|
|
1036
|
+
retryable: true,
|
|
1037
|
+
retryAt: Number(entry.executionLease.expiresAt),
|
|
1038
|
+
},
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
entry.schedulerMigration = {
|
|
1042
|
+
...prior,
|
|
1043
|
+
state: "retired",
|
|
1044
|
+
retirementToken: token,
|
|
1045
|
+
retiredAt: now,
|
|
1046
|
+
};
|
|
1047
|
+
applySchedulerMigrationFence(entry, now);
|
|
1048
|
+
this._writeAll(kind, entries);
|
|
1049
|
+
return entry;
|
|
1050
|
+
}
|
|
1051
|
+
throw agendaStoreError(
|
|
1052
|
+
"AGENDA_SCHEDULER_ENTRY_NOT_FOUND",
|
|
1053
|
+
`Agenda entry does not exist: ${id}`,
|
|
1054
|
+
);
|
|
1055
|
+
},
|
|
1056
|
+
{ failIfUnavailable: true },
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
restoreFromSchedulerMigration(
|
|
1061
|
+
id,
|
|
1062
|
+
{ migrationId, sourceDigest, targetJobId, retirementToken } = {},
|
|
1063
|
+
) {
|
|
1064
|
+
const migrationIdValue = normalizeMigrationField(
|
|
1065
|
+
migrationId,
|
|
1066
|
+
"migrationId",
|
|
1067
|
+
);
|
|
1068
|
+
const expectedDigest = normalizeMigrationField(
|
|
1069
|
+
sourceDigest,
|
|
1070
|
+
"sourceDigest",
|
|
1071
|
+
);
|
|
1072
|
+
const target = normalizeMigrationField(targetJobId, "targetJobId");
|
|
1073
|
+
const token =
|
|
1074
|
+
retirementToken === null || retirementToken === undefined
|
|
1075
|
+
? null
|
|
1076
|
+
: normalizeMigrationField(retirementToken, "retirementToken");
|
|
1077
|
+
this._ensureDir();
|
|
1078
|
+
return withFileLock(
|
|
1079
|
+
path.join(this.dir, ".agenda-claims"),
|
|
1080
|
+
() => {
|
|
1081
|
+
for (const kind of SCHEDULE_KINDS) {
|
|
1082
|
+
const entries = this._readAll(kind);
|
|
1083
|
+
const entry = entries.find((candidate) => candidate.id === id);
|
|
1084
|
+
if (!entry) continue;
|
|
1085
|
+
const migration = entry.schedulerMigration;
|
|
1086
|
+
if (!migration) {
|
|
1087
|
+
if (
|
|
1088
|
+
agendaMigrationSourceDigest(entry) !== expectedDigest ||
|
|
1089
|
+
String(entry.executionLease?.owner || "").startsWith(
|
|
1090
|
+
"scheduler-migration:",
|
|
1091
|
+
)
|
|
1092
|
+
) {
|
|
1093
|
+
throw agendaStoreError(
|
|
1094
|
+
"AGENDA_SCHEDULER_MIGRATION_CONFLICT",
|
|
1095
|
+
`Agenda source changed after rollback restoration: ${id}`,
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
return entry;
|
|
1099
|
+
}
|
|
1100
|
+
if (
|
|
1101
|
+
migration?.schemaVersion !==
|
|
1102
|
+
AGENDA_SCHEDULER_MIGRATION_SCHEMA_VERSION ||
|
|
1103
|
+
migration.sourceDigest !== expectedDigest ||
|
|
1104
|
+
agendaMigrationSourceDigest(entry) !== expectedDigest ||
|
|
1105
|
+
migration.targetJobId !== target ||
|
|
1106
|
+
migration.migrationId !== migrationIdValue ||
|
|
1107
|
+
!["prepared", "retired"].includes(migration.state) ||
|
|
1108
|
+
(migration.state === "retired" &&
|
|
1109
|
+
(migration.retirementToken !== token ||
|
|
1110
|
+
entry.executionLease?.owner !== `scheduler-migration:${token}`))
|
|
1111
|
+
) {
|
|
1112
|
+
throw agendaStoreError(
|
|
1113
|
+
"AGENDA_SCHEDULER_MIGRATION_CONFLICT",
|
|
1114
|
+
`Agenda migration fence changed before rollback: ${id}`,
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
delete entry.schedulerMigration;
|
|
1118
|
+
if (migration.state === "retired") delete entry.executionLease;
|
|
1119
|
+
this._writeAll(kind, entries);
|
|
1120
|
+
return entry;
|
|
1121
|
+
}
|
|
1122
|
+
throw agendaStoreError(
|
|
1123
|
+
"AGENDA_SCHEDULER_ENTRY_NOT_FOUND",
|
|
1124
|
+
`Agenda entry does not exist: ${id}`,
|
|
1125
|
+
);
|
|
1126
|
+
},
|
|
1127
|
+
{ failIfUnavailable: true },
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
535
1131
|
/** Entries whose time has come (pending wakeup / active cron|monitor). */
|
|
536
|
-
due(kind = null, atMs = null) {
|
|
1132
|
+
due(kind = null, atMs = null, { schedulerMigration = false } = {}) {
|
|
537
1133
|
const now = atMs != null ? atMs : this._now();
|
|
538
1134
|
return this.list(kind).filter((entry) => {
|
|
539
1135
|
// An entry past its expiry never fires — even if it is not yet retired
|
|
540
1136
|
// (defense-in-depth: retireExpired need not have run first).
|
|
541
1137
|
if (isEntryExpired(entry, now)) return false;
|
|
542
1138
|
if (!isSchedulableStatus(entry)) return false;
|
|
543
|
-
if (
|
|
1139
|
+
if (
|
|
1140
|
+
hasLiveExecutionLease(entry, now) &&
|
|
1141
|
+
!(schedulerMigration && schedulerMigrationFence(entry, now))
|
|
1142
|
+
) {
|
|
1143
|
+
return false;
|
|
1144
|
+
}
|
|
544
1145
|
// The effective fire time applies the entry's OWN deterministic jitter
|
|
545
1146
|
// (`entry.jitterMs`, default 0 → base fire time, byte-identical to the
|
|
546
1147
|
// prior `dueAt`/`nextAt` comparison). A jittered entry fires slightly
|
|
@@ -609,7 +1210,7 @@ export class AgentScheduleStore {
|
|
|
609
1210
|
}
|
|
610
1211
|
|
|
611
1212
|
/**
|
|
612
|
-
* Bind a scheduler claim to the legacy Agenda entry before
|
|
1213
|
+
* Bind a scheduler claim to the legacy Agenda entry before execution.
|
|
613
1214
|
* The same lock used by legacy `claimDue()` prevents an old/new driver from
|
|
614
1215
|
* starting the entry concurrently during migration.
|
|
615
1216
|
*/
|
|
@@ -640,7 +1241,7 @@ export class AgentScheduleStore {
|
|
|
640
1241
|
return withFileLock(
|
|
641
1242
|
path.join(this.dir, ".agenda-claims"),
|
|
642
1243
|
() => {
|
|
643
|
-
for (const kind of
|
|
1244
|
+
for (const kind of SCHEDULE_KINDS) {
|
|
644
1245
|
const entries = this._readAll(kind);
|
|
645
1246
|
const entry = entries.find((candidate) => candidate.id === id);
|
|
646
1247
|
if (!entry) continue;
|
|
@@ -658,7 +1259,8 @@ export class AgentScheduleStore {
|
|
|
658
1259
|
) {
|
|
659
1260
|
return entry;
|
|
660
1261
|
}
|
|
661
|
-
|
|
1262
|
+
const migrationFence = schedulerMigrationFence(entry, now);
|
|
1263
|
+
if (hasLiveExecutionLease(entry, now) && !migrationFence) {
|
|
662
1264
|
throw agendaStoreError(
|
|
663
1265
|
"AGENDA_SCHEDULER_LEGACY_CLAIM_ACTIVE",
|
|
664
1266
|
`Agenda entry is already claimed by a legacy runner: ${id}`,
|
|
@@ -678,7 +1280,8 @@ export class AgentScheduleStore {
|
|
|
678
1280
|
// Fence an older CLI that only understands the legacy claim field.
|
|
679
1281
|
// Terminal scheduler evidence clears this lease; an outcome-unknown
|
|
680
1282
|
// execution intentionally keeps it non-expiring until adjudicated so
|
|
681
|
-
// an old runner cannot duplicate an uncertain
|
|
1283
|
+
// an old runner cannot duplicate an uncertain external observation or
|
|
1284
|
+
// agent side effect.
|
|
682
1285
|
entry.executionLease = {
|
|
683
1286
|
owner: `scheduler:${occurrenceId}:${attempt}`,
|
|
684
1287
|
claimedAt: now,
|
|
@@ -697,10 +1300,10 @@ export class AgentScheduleStore {
|
|
|
697
1300
|
}
|
|
698
1301
|
|
|
699
1302
|
/**
|
|
700
|
-
* Atomically publish scheduler evidence and the legacy
|
|
701
|
-
*
|
|
702
|
-
*
|
|
703
|
-
*
|
|
1303
|
+
* Atomically publish scheduler evidence and the legacy Agenda state. A
|
|
1304
|
+
* successful cron advances and a successful monitor records/re-arms exactly
|
|
1305
|
+
* once in the same JSONL replacement that records terminal evidence;
|
|
1306
|
+
* failures keep the entry schedulable for the kernel's bounded retry policy.
|
|
704
1307
|
*/
|
|
705
1308
|
completeSchedulerExecution(
|
|
706
1309
|
id,
|
|
@@ -711,6 +1314,7 @@ export class AgentScheduleStore {
|
|
|
711
1314
|
outcome,
|
|
712
1315
|
result = null,
|
|
713
1316
|
error = null,
|
|
1317
|
+
monitorCheck = null,
|
|
714
1318
|
atMs = null,
|
|
715
1319
|
} = {},
|
|
716
1320
|
) {
|
|
@@ -725,7 +1329,7 @@ export class AgentScheduleStore {
|
|
|
725
1329
|
return withFileLock(
|
|
726
1330
|
path.join(this.dir, ".agenda-claims"),
|
|
727
1331
|
() => {
|
|
728
|
-
for (const kind of
|
|
1332
|
+
for (const kind of SCHEDULE_KINDS) {
|
|
729
1333
|
const entries = this._readAll(kind);
|
|
730
1334
|
const entry = entries.find((candidate) => candidate.id === id);
|
|
731
1335
|
if (!entry) continue;
|
|
@@ -742,16 +1346,121 @@ export class AgentScheduleStore {
|
|
|
742
1346
|
`Agenda scheduler evidence does not match the active claim: ${id}`,
|
|
743
1347
|
);
|
|
744
1348
|
}
|
|
1349
|
+
let storedResult = result;
|
|
745
1350
|
if (outcome === "succeeded") {
|
|
746
|
-
|
|
1351
|
+
if (entry.kind === "monitor") {
|
|
1352
|
+
applyScheduledMonitorSuccess(entry, monitorCheck, now);
|
|
1353
|
+
storedResult =
|
|
1354
|
+
result && typeof result === "object" && !Array.isArray(result)
|
|
1355
|
+
? { ...result, status: entry.status }
|
|
1356
|
+
: result;
|
|
1357
|
+
} else {
|
|
1358
|
+
applyScheduledAgentSuccess(entry, now);
|
|
1359
|
+
}
|
|
747
1360
|
}
|
|
748
1361
|
entry.schedulerExecution = {
|
|
749
1362
|
...evidence,
|
|
750
1363
|
status: outcome,
|
|
751
1364
|
endedAt: now,
|
|
752
|
-
...(outcome === "succeeded" ? { result } : { error }),
|
|
1365
|
+
...(outcome === "succeeded" ? { result: storedResult } : { error }),
|
|
1366
|
+
};
|
|
1367
|
+
delete entry.executionLease;
|
|
1368
|
+
if (isSchedulableStatus(entry))
|
|
1369
|
+
applySchedulerMigrationFence(entry, now);
|
|
1370
|
+
this._writeAll(kind, entries);
|
|
1371
|
+
return entry;
|
|
1372
|
+
}
|
|
1373
|
+
throw agendaStoreError(
|
|
1374
|
+
"AGENDA_SCHEDULER_ENTRY_NOT_FOUND",
|
|
1375
|
+
`Agenda entry does not exist: ${id}`,
|
|
1376
|
+
);
|
|
1377
|
+
},
|
|
1378
|
+
{ failIfUnavailable: true },
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
/**
|
|
1383
|
+
* Resolve start-only scheduler evidence after an operator has verified the
|
|
1384
|
+
* external outcome. This is intentionally separate from normal completion:
|
|
1385
|
+
* it preserves the adjudication identity and can release the non-expiring
|
|
1386
|
+
* legacy fence without pretending that a machine observed a terminal result.
|
|
1387
|
+
*/
|
|
1388
|
+
adjudicateSchedulerExecution(
|
|
1389
|
+
id,
|
|
1390
|
+
{
|
|
1391
|
+
occurrenceId,
|
|
1392
|
+
snapshotDigest,
|
|
1393
|
+
attempt,
|
|
1394
|
+
decision,
|
|
1395
|
+
requestId,
|
|
1396
|
+
atMs = null,
|
|
1397
|
+
} = {},
|
|
1398
|
+
) {
|
|
1399
|
+
if (!["confirmed_applied", "confirmed_not_applied"].includes(decision)) {
|
|
1400
|
+
throw agendaStoreError(
|
|
1401
|
+
"AGENDA_SCHEDULER_ADJUDICATION_INVALID",
|
|
1402
|
+
"Agenda scheduler adjudication decision is invalid",
|
|
1403
|
+
);
|
|
1404
|
+
}
|
|
1405
|
+
const now = atMs != null ? Number(atMs) : this._now();
|
|
1406
|
+
this._ensureDir();
|
|
1407
|
+
return withFileLock(
|
|
1408
|
+
path.join(this.dir, ".agenda-claims"),
|
|
1409
|
+
() => {
|
|
1410
|
+
for (const kind of SCHEDULE_KINDS) {
|
|
1411
|
+
const entries = this._readAll(kind);
|
|
1412
|
+
const entry = entries.find((candidate) => candidate.id === id);
|
|
1413
|
+
if (!entry) continue;
|
|
1414
|
+
const evidence = entry.schedulerExecution;
|
|
1415
|
+
if (
|
|
1416
|
+
evidence?.occurrenceId === occurrenceId &&
|
|
1417
|
+
evidence.snapshotDigest === snapshotDigest &&
|
|
1418
|
+
evidence.adjudication?.requestId === requestId &&
|
|
1419
|
+
evidence.adjudication.decision === decision &&
|
|
1420
|
+
evidence.status ===
|
|
1421
|
+
(decision === "confirmed_applied"
|
|
1422
|
+
? "adjudicated_applied"
|
|
1423
|
+
: "adjudicated_not_applied")
|
|
1424
|
+
) {
|
|
1425
|
+
return entry;
|
|
1426
|
+
}
|
|
1427
|
+
if (
|
|
1428
|
+
!evidence ||
|
|
1429
|
+
evidence.occurrenceId !== occurrenceId ||
|
|
1430
|
+
evidence.snapshotDigest !== snapshotDigest ||
|
|
1431
|
+
!Number.isSafeInteger(evidence.attempt) ||
|
|
1432
|
+
evidence.attempt < 1 ||
|
|
1433
|
+
evidence.attempt > attempt ||
|
|
1434
|
+
evidence.status !== "running"
|
|
1435
|
+
) {
|
|
1436
|
+
throw agendaStoreError(
|
|
1437
|
+
"AGENDA_SCHEDULER_ADJUDICATION_EVIDENCE_MISMATCH",
|
|
1438
|
+
`Agenda scheduler adjudication evidence does not match: ${id}`,
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
if (decision === "confirmed_applied") {
|
|
1442
|
+
if (entry.kind === "monitor") {
|
|
1443
|
+
// A generic operator decision proves the prior check ran, but
|
|
1444
|
+
// not whether its observation matched. Pause the monitor rather
|
|
1445
|
+
// than inventing a domain result or rearming a duplicate check.
|
|
1446
|
+
entry.status = "adjudicated";
|
|
1447
|
+
entry.lastCheckAt = now;
|
|
1448
|
+
} else {
|
|
1449
|
+
applyScheduledAgentSuccess(entry, now);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
entry.schedulerExecution = {
|
|
1453
|
+
...evidence,
|
|
1454
|
+
status:
|
|
1455
|
+
decision === "confirmed_applied"
|
|
1456
|
+
? "adjudicated_applied"
|
|
1457
|
+
: "adjudicated_not_applied",
|
|
1458
|
+
endedAt: now,
|
|
1459
|
+
adjudication: { requestId, decision },
|
|
753
1460
|
};
|
|
754
1461
|
delete entry.executionLease;
|
|
1462
|
+
if (isSchedulableStatus(entry))
|
|
1463
|
+
applySchedulerMigrationFence(entry, now);
|
|
755
1464
|
this._writeAll(kind, entries);
|
|
756
1465
|
return entry;
|
|
757
1466
|
}
|
|
@@ -841,7 +1550,9 @@ export class AgentScheduleStore {
|
|
|
841
1550
|
return this._mutate("cron", id, (entry) => {
|
|
842
1551
|
entry.lastRunAt = now;
|
|
843
1552
|
entry.runs = (entry.runs || 0) + 1;
|
|
844
|
-
const next = nextCronTime(entry.cron, now
|
|
1553
|
+
const next = nextCronTime(entry.cron, now, {
|
|
1554
|
+
timeZone: entry.timeZone || null,
|
|
1555
|
+
});
|
|
845
1556
|
if (next == null) {
|
|
846
1557
|
entry.status = "exhausted";
|
|
847
1558
|
} else {
|
|
@@ -866,32 +1577,11 @@ export class AgentScheduleStore {
|
|
|
866
1577
|
) {
|
|
867
1578
|
const now = atMs != null ? atMs : this._now();
|
|
868
1579
|
return this._mutate("monitor", id, (entry) => {
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
if (mtimeMs != null && entry.lastMtimeMs == null) {
|
|
875
|
-
entry.lastMtimeMs = mtimeMs;
|
|
876
|
-
}
|
|
877
|
-
if (matched) {
|
|
878
|
-
entry.status = "matched";
|
|
879
|
-
entry.matchedAt = now;
|
|
880
|
-
// Audit record of the firing: the deterministic monitor-event id + the
|
|
881
|
-
// (always SYSTEM/steer) authority the event was delivered under. Durable
|
|
882
|
-
// on the entry so `cc agenda list --json` and a future resident daemon
|
|
883
|
-
// can recognise / dedup the exact observation. Only stamped when the
|
|
884
|
-
// caller supplies them, so legacy callers stay byte-identical.
|
|
885
|
-
if (eventId != null) {
|
|
886
|
-
entry.lastEventId = String(eventId);
|
|
887
|
-
entry.lastEventAt = now;
|
|
888
|
-
}
|
|
889
|
-
if (authority != null) entry.lastAuthority = String(authority);
|
|
890
|
-
} else if (entry.maxChecks != null && entry.checks >= entry.maxChecks) {
|
|
891
|
-
entry.status = "exhausted";
|
|
892
|
-
} else {
|
|
893
|
-
entry.nextAt = now + entry.intervalMs;
|
|
894
|
-
}
|
|
1580
|
+
applyScheduledMonitorSuccess(
|
|
1581
|
+
entry,
|
|
1582
|
+
{ matched: Boolean(matched), mtimeMs, eventId, authority },
|
|
1583
|
+
now,
|
|
1584
|
+
);
|
|
895
1585
|
});
|
|
896
1586
|
}
|
|
897
1587
|
|