chainlesschain 0.163.7 → 0.164.0
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/package.json +5 -1
- package/scripts/macos-mcp-launcher-build.mjs +317 -0
- package/scripts/macos-mcp-launcher-postinstall.sh +54 -0
- package/scripts/macos-mcp-launcher-preinstall.sh +61 -0
- package/src/assets/web-panel/assets/{AIOps-axqQfwv8.js → AIOps-D2MEn7fI.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-DV-Ro0By.js → ActionButton-lCW_4Gf0.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-Bt72WEVu.js → Analytics-Bs7i-nsS.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-BtepG55E.js → AppLayout-CK7B6S_j.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-B7KaJ7S_.js → Artifacts-C8ILjpyo.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-FNFaUg7A.js → Audit-Bb9cNaWc.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-DFb6J2wQ.js → BackgroundAgents-sC9ooXIf.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-CC7YBhyj.js → Backup-D-DrsplT.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-RkgTNVtj.js → BaseInput-uCNZpwSK.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-hHEmGZy-.js → Chat-CsDK1uE9.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-CAP9YhHv.js → ChatBubbleRenderer-OqSDt50w.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-z5wXOQP1.js → Checkbox-JhTXM1-K.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-DVaDJnxy.js → Codegen-BbzZLnyD.js} +1 -1
- package/src/assets/web-panel/assets/{Col-CJmaRDR5.js → Col-D4VW4m4j.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DF29_2bI.js → Community-BXrYawxV.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DtkQkBzn.js → Compact-DCihGy1u.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-xoHDxp7I.js → Compliance-DlyZ-lk8.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BUm-R7ht.js → Cowork-DvGZrhbO.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-Cgl14ZkB.js → Cron-DUncZi8h.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-BNcbmoqh.js → Crosschain-C_hLuUcm.js} +1 -1
- package/src/assets/web-panel/assets/{DID-Bwja0Zo9.js → DID-b6YSUqxr.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-Bk5XjEdJ.js → Dashboard-uvM15Cfb.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-Ci36Oitq.js → Dropdown-D1-Nd4qY.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer--U04ZN4q.js → EmailListRenderer-8ApvL6Zc.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-B2gNPR2s.js → FamilyGuardDashboard-LYJt8811.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-C4KghYn7.js → Federation-BbI3HGpv.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-QL7x_Q_0.js → FormItemContext-DSoEKuFu.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-F3Ryltwm.js → GenericCardRenderer-B0S9OWd5.js} +1 -1
- package/src/assets/web-panel/assets/{Git-2HassK5k.js → Git-DIs9Zhf3.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-DoBIp7oe.js → Governance-jzZXrxcl.js} +1 -1
- package/src/assets/web-panel/assets/{Inference--W01QhZ-.js → Inference-JUPjf2ZI.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DmvwqGpT.js → KnowledgeGraph-DxnfJvlP.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-BBJ4yAel.js → Logs-iXiz8BEE.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-BLCZMzhJ.js → MarkdownRenderer-GmB6DOr6.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-D5lNngSI.js → Marketplace-Y3Xch312.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-CkEVOMKH.js → McpTools-D7kGGbO7.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-B1Ow1xVE.js → Memory-Do_hVxzh.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-SgYbUfb6.js → MobileBridge-CtrR0Tas.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-BL0Czcoj.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-DcTXSSma.js → Mtc-qo5QCTGd.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-Dt3WG4oj.js → MtcAudit-cq5mnjYj.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-B9B38UZx.js → Multisig-Bq0MAQHw.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-B0S4GGvG.js → NLProgramming-DdWE6Vi2.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-DZ5U58Ng.js → Notes-BxMBFcIp.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-PjL5ZW34.js → NotificationSettings-RTrAcbUe.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-DrAX2ESt.js → OrderTableRenderer-DJbpGqzT.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-Cv1GYs-c.js → Organization-SQzIPdqC.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-Bbkp_PL3.js → Overflow-02yECh22.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BD8QUgoZ.js → P2P-BNg4pNCM.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-BO-2PeaB.js → PdhVaultBrowser-BxPJZsD3.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-D_OPVa02.js → Permissions-CLnDnPAz.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-mh-pEmaB.js → PersonalDataHub-C5m0AjGf.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-6JC2EDdb.js → Pipeline-VwOf5Dhv.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-CUshzUAG.js → Privacy-CC0W2hZd.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-Cs2Aalri.js → ProjectInit-Dz893Bob.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-1ak49tcj.js → ProjectSettings-CxQkvl0k.js} +2 -2
- package/src/assets/web-panel/assets/Projects-DDAjkUkT.js +1 -0
- package/src/assets/web-panel/assets/{Providers-CXXxvafH.js → Providers-DeB9IkxK.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-PTcDsF58.js → QrScannerModal-OCeoaswn.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-CDwucrJw.js → QuickAsk-DenotaAj.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-CJlLVRXR.js → Recommend-Cies_jjI.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-DlN-ETsO.js → RemoteSession-Dxzl5n2T.js} +3 -3
- package/src/assets/web-panel/assets/{Reputation-DI23LR6m.js → Reputation-CW0mvxgZ.js} +1 -1
- package/src/assets/web-panel/assets/{Row-Cl7UQMrM.js → Row-SEd32qZU.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-CexHNfh8.js → RssFeed-BSwnJ3Lm.js} +3 -3
- package/src/assets/web-panel/assets/{Search-BneBWvRN.js → Search-xe2m8bkC.js} +1 -1
- package/src/assets/web-panel/assets/{Security-DX7Phgrp.js → Security-Mq2F9Nro.js} +4 -4
- package/src/assets/web-panel/assets/{Services-BWXO8Mte.js → Services-BKGw2V3P.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DCyOjKPJ.js → Skeleton-DW95-Rxf.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CXqERsTB.js → Skills-CHgChCv_.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-bhbrbz8E.js → Sla-tKh6k3R7.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-3IPU0HBZ.js → SpeechSettings-D6UL3dA5.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-DBNTQLbM.js → SyncSettings-D0Fw8fVL.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-quP0bkN-.js → Tasks-20x1GB3D.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-BouWmgho.js → Templates-DJ-lts-a.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-qv0D9s7z.js → Tenant-BFdGpbs7.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-BxvbE8BB.js → Terminal-Xpx61vHL.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-D-IeLnJ2.js → TimelineRenderer-C6UIvyZQ.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-CFMKKyVG.js → Tokens-Dellq08f.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-L8as3UuJ.js → Trigger-VKw8d0J-.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-MxmA4VZG.js → Trust-DNftdKT5.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-C0VvJYRi.js → UkeySign-DX9VAKJI.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-Bf5QZj8N.js → VideoEditing-B2h3jO8q.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-BIJ9riRz.js → Wallet-CjGkof7f.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-B8p9Mhkw.js → WebAuthn-2Zfcu55P.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-BVsuaPen.js → WorkflowEditor-B9eZ8UxZ.js} +1 -1
- package/src/assets/web-panel/assets/{chat-BfkjoKpV.js → chat-CQNfD-YO.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DeAlsWK8.js → colors-D4usyFT2.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-Cs97bz5j.js → compact-item-s4TpjvjX.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BqbG60LH.js → createContext-BRCSvYxj.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DKJjSWRn.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-i_RY0b1X.js → hasIn-AdkUbkV-.js} +1 -1
- package/src/assets/web-panel/assets/{index-BOVkf1K3.js → index--00U6vXZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-B-3tEpTc.js → index-B2V-eKfK.js} +1 -1
- package/src/assets/web-panel/assets/{index-D2B4VPyP.js → index-B4JJCfQ2.js} +1 -1
- package/src/assets/web-panel/assets/{index-BkBi5TmC.js → index-BIoG5yYf.js} +1 -1
- package/src/assets/web-panel/assets/{index-79V_zv1s.js → index-BMcimU-2.js} +1 -1
- package/src/assets/web-panel/assets/{index-U4f5izHz.js → index-BPhkFZ1I.js} +1 -1
- package/src/assets/web-panel/assets/{index-DiSkx4UB.js → index-BQ3DXBtD.js} +3 -3
- package/src/assets/web-panel/assets/{index-dIHr4B_u.js → index-Bad4VNvz.js} +1 -1
- package/src/assets/web-panel/assets/{index-CIG8fQ2_.js → index-BgiXR_-_.js} +1 -1
- package/src/assets/web-panel/assets/{index-DqBgBpgp.js → index-BtobShro.js} +1 -1
- package/src/assets/web-panel/assets/{index-ZxO_-E8a.js → index-Bw2MEnUk.js} +1 -1
- package/src/assets/web-panel/assets/{index-DUUrv141.js → index-C1mJtE6q.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bm7lLR4c.js → index-C484EJiS.js} +1 -1
- package/src/assets/web-panel/assets/{index-COdGA-8n.js → index-C5KAQFPQ.js} +1 -1
- package/src/assets/web-panel/assets/index-C61cnl3K.js +1 -0
- package/src/assets/web-panel/assets/{index-CED2PL_y.js → index-C7Y8Aluc.js} +1 -1
- package/src/assets/web-panel/assets/{index-YXgLng0K.js → index-CCsM_Ybx.js} +1 -1
- package/src/assets/web-panel/assets/{index-0-agYlWL.js → index-CJCFe2Qf.js} +1 -1
- package/src/assets/web-panel/assets/{index-B5UkyA3c.js → index-CJVEOa4i.js} +1 -1
- package/src/assets/web-panel/assets/{index-B1CbOH1i.js → index-CJdQrZth.js} +1 -1
- package/src/assets/web-panel/assets/{index-s1opAL-M.js → index-CKC1t3Ic.js} +1 -1
- package/src/assets/web-panel/assets/{index-yObj1O4n.js → index-CQhAVU5t.js} +1 -1
- package/src/assets/web-panel/assets/{index-CgsqdnRV.js → index-CYdoQsa1.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dk7-v4hP.js → index-Cduu2Hhr.js} +1 -1
- package/src/assets/web-panel/assets/{index-B-JVTrpr.js → index-CkwiSLhU.js} +1 -1
- package/src/assets/web-panel/assets/{index-CeWoNHau.js → index-Cvyt-zhm.js} +1 -1
- package/src/assets/web-panel/assets/{index-CE2VtTlp.js → index-D2GjI2oe.js} +1 -1
- package/src/assets/web-panel/assets/index-D4XFHxBr.js +1 -0
- package/src/assets/web-panel/assets/{index-DmbfhJUP.js → index-DARsAyUp.js} +1 -1
- package/src/assets/web-panel/assets/{index-BESLFt4q.js → index-DVKfQ0Ux.js} +1 -1
- package/src/assets/web-panel/assets/{index-PT_5sBMA.js → index-DZT36ZYq.js} +1 -1
- package/src/assets/web-panel/assets/{index-gyFpvL7l.js → index-D_XBHopw.js} +1 -1
- package/src/assets/web-panel/assets/{index-BYUVuwKt.js → index-DdCB70SS.js} +1 -1
- package/src/assets/web-panel/assets/{index-BcUXAB2s.js → index-DzRjHxEv.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dk2qouAz.js → index-MMN9V6JF.js} +1 -1
- package/src/assets/web-panel/assets/{index-D9evtvg2.js → index-RfMr79MW.js} +1 -1
- package/src/assets/web-panel/assets/{index-BuLQobzW.js → index-_IRVsV9X.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cf3WCLmy.js → index-nXyk8GZ9.js} +1 -1
- package/src/assets/web-panel/assets/{index-CG3PRyAL.js → index-s_buFMEG.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-C9h9yKL3.js → initDefaultProps-Blmpk7HL.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BvmLsPdu.js → motion-DPBJMv1n.js} +1 -1
- package/src/assets/web-panel/assets/{move-CoL4ekU1.js → move-ButdzCM7.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-C4pMy6xV.js → mtc-parser-cyRC_QCe.js} +1 -1
- package/src/assets/web-panel/assets/{omit-DPuJfNxx.js → omit-1epSV7hz.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-D0KEeaK2.js → pickAttrs-DuPY8xJs.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BpkyINxt.js → placementArrow-6XZqarhe.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DQI_wOq8.js → responsiveObserve-B40pXBWM.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DYfdDiRg.js → slide-jbZ9Wu4n.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-B5dtvb0D.js → statusUtils-BKQQcVy4.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-CF310mSj.js → styleChecker-NiKfWjUT.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-D1Qp4BLp.js → useFlexGapSupport-bQUCvPeb.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-C7_iyvJK.js → useFs-CUGji7ph.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-DZUt_fIm.js → usePersonalDataHub-4veyN2N2.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-D3dsi0lZ.js → vnode-1-nLug-Z.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-WdbOlvdQ.js → zoom-dhCeRDMo.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +6 -6
- package/src/commands/automation.js +103 -1
- package/src/commands/cowork.js +174 -19
- package/src/commands/permissions.js +240 -16
- package/src/commands/plugin.js +1293 -56
- package/src/commands/session-location.js +276 -0
- package/src/commands/session.js +2 -0
- package/src/commands/team-merge-review.js +937 -0
- package/src/commands/team.js +2 -0
- package/src/data/changelog.json +27 -1
- package/src/gateways/ws/action-protocol.js +35 -10
- package/src/gateways/ws/remote-session-protocol.js +24 -1
- package/src/gateways/ws/ws-agent-handler.js +12 -0
- package/src/gateways/ws/ws-server.js +48 -12
- package/src/harness/jsonl-session-store.js +341 -24
- package/src/harness/mcp-client.js +8 -1
- package/src/harness/remote-session-audit.js +1 -0
- package/src/harness/remote-session-crypto.js +210 -2
- package/src/harness/remote-session-registry.js +159 -6
- package/src/harness/session-list-index.js +233 -14
- package/src/lib/agent-authority.js +6 -0
- package/src/lib/agent-schedule-store.js +139 -28
- package/src/lib/agent-team/team-merge-review-store.js +765 -0
- package/src/lib/agent-team/team-merge-review-transaction.js +1223 -0
- package/src/lib/agent-team/team-merge-review.js +1847 -0
- package/src/lib/automation-center-incidents.js +314 -0
- package/src/lib/automation-center-runtime.js +448 -0
- package/src/lib/automation-center.js +182 -38
- package/src/lib/automation-engine.js +41 -0
- package/src/lib/automation-execution-authority.js +241 -12
- package/src/lib/automation-execution-incident.js +737 -0
- package/src/lib/background-agent-keeper-client.js +248 -0
- package/src/lib/background-agent-keeper-protocol.js +147 -0
- package/src/lib/background-agent-supervisor.js +520 -40
- package/src/lib/background-interaction-resolver.js +12 -0
- package/src/lib/background-turn-bootstrap-protocol.js +126 -0
- package/src/lib/cowork-cron.js +85 -14
- package/src/lib/cowork-workflow.js +336 -16
- package/src/lib/dynamic-workflow-facade.js +648 -0
- package/src/lib/execution-location-contract.js +551 -0
- package/src/lib/execution-location-runtime.js +130 -0
- package/src/lib/ide-context.js +94 -6
- package/src/lib/mcp-stdio-package-materialization.js +166 -14
- package/src/lib/packer/pkg-config-generator.js +2 -0
- package/src/lib/permission-authority.js +77 -0
- package/src/lib/permission-side-effect-center.js +495 -0
- package/src/lib/plugin-runtime/install.js +514 -13
- package/src/lib/plugin-runtime/marketplace-artifact-readback.js +900 -0
- package/src/lib/plugin-runtime/marketplace-catalog.js +1181 -0
- package/src/lib/plugin-runtime/marketplace-impact.js +408 -0
- package/src/lib/plugin-runtime/marketplace-remote-artifacts.js +1567 -0
- package/src/lib/plugin-security.js +60 -3
- package/src/lib/process-execution-broker/index.js +1095 -57
- package/src/lib/process-execution-broker/linux-bwrap-descriptor-launch.js +375 -0
- package/src/lib/process-execution-broker/linux-generic-bwrap-runtime.js +90 -16
- package/src/lib/process-execution-broker/linux-generic-bwrap.js +159 -18
- package/src/lib/process-execution-broker/macos-mcp-launcher-contract.js +278 -0
- package/src/lib/process-execution-broker/macos-mcp-launcher-protocol.json +23 -0
- package/src/lib/process-execution-broker/macos-mcp-launcher.c +1497 -0
- package/src/lib/process-execution-broker/mcp-fd-entry-bootstrap.js +37 -0
- package/src/lib/process-execution-broker/platform-sandbox.js +2330 -279
- package/src/lib/process-execution-broker/windows-sandbox-helper.dll +0 -0
- package/src/lib/process-execution-broker/windows-sandbox-helper.exe +0 -0
- package/src/lib/process-execution-broker/windows-sandbox.cs +50 -4
- package/src/lib/public-ide-capability-manifest.js +3 -0
- package/src/lib/remote-approval-bridge.js +123 -17
- package/src/lib/remote-membership-authority.js +632 -0
- package/src/lib/remote-membership-coordinator.js +2409 -0
- package/src/lib/remote-membership-host-store.js +1750 -0
- package/src/lib/scheduler-kernel/agenda-adapter.js +17 -7
- package/src/lib/scheduler-kernel/automation-adapter.js +105 -11
- package/src/lib/scheduler-kernel/automation-event-adapter.js +87 -11
- package/src/lib/scheduler-kernel/contract.js +91 -0
- package/src/lib/scheduler-kernel/cowork-cron-adapter.js +12 -1
- package/src/lib/scheduler-kernel/loop-adapter.js +7 -1
- package/src/lib/scheduler-kernel/routine-adapter.js +7 -1
- package/src/lib/scheduler-kernel/runtime-control-capabilities.js +31 -0
- package/src/lib/scheduler-kernel/runtime.js +176 -3
- package/src/lib/scheduler-kernel/store.js +1253 -45
- package/src/lib/scoped-permission-store.js +369 -0
- package/src/lib/session-anti-rollback-anchor.js +262 -5
- package/src/lib/sub-agent-context.js +4 -0
- package/src/lib/workflow-definition-contract.js +149 -0
- package/src/repl/agent-repl.js +41 -7
- package/src/repl/chat-repl.js +2 -0
- package/src/repl/clipboard-image.js +735 -16
- package/src/repl/ide-status.js +1 -1
- package/src/repl/prompt-interactions.js +59 -6
- package/src/runtime/agent-core.js +93 -3
- package/src/runtime/headless-runner.js +30 -25
- package/src/runtime/headless-stream.js +32 -25
- package/src/runtime/mcp-config.js +81 -10
- package/src/workers/background-agent-keeper.js +498 -0
- package/src/workers/background-agent-worker.js +358 -13
- package/src/workers/background-turn-bootstrap.js +143 -0
- package/src/assets/web-panel/assets/MobileProjects-BWdOvQAj.js +0 -1
- package/src/assets/web-panel/assets/Projects-BV4oApsm.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DPty0mGN.js +0 -1
- package/src/assets/web-panel/assets/index-Bt7g0Vq_.js +0 -1
- package/src/assets/web-panel/assets/index-ChVT0p9u.js +0 -1
|
@@ -6,6 +6,8 @@ import { getHomeDir } from "../paths.js";
|
|
|
6
6
|
import { ensurePrivateDirectory, ensurePrivateFile } from "../secure-fs.js";
|
|
7
7
|
import {
|
|
8
8
|
OCCURRENCE_STATUS,
|
|
9
|
+
RUNTIME_CONTROL_SAFE_POINTS,
|
|
10
|
+
RUNTIME_PAUSE_RESUME,
|
|
9
11
|
SchedulerKernelError,
|
|
10
12
|
canonicalJson,
|
|
11
13
|
deriveOccurrenceIdentity,
|
|
@@ -16,21 +18,51 @@ import {
|
|
|
16
18
|
normalizeIdentifier,
|
|
17
19
|
normalizeJson,
|
|
18
20
|
normalizeMaxAttempts,
|
|
21
|
+
normalizeRuntimeControlCapability,
|
|
19
22
|
} from "./contract.js";
|
|
20
23
|
import { isCanonicalSchedulerSourcePath } from "./source-locator-path.js";
|
|
21
24
|
|
|
22
25
|
const requireCjs = createRequire(import.meta.url);
|
|
23
26
|
|
|
27
|
+
const STORAGE_FAILURE_CODES = Object.freeze([
|
|
28
|
+
"ENOSPC",
|
|
29
|
+
"EROFS",
|
|
30
|
+
"EIO",
|
|
31
|
+
"SQLITE_FULL",
|
|
32
|
+
"SQLITE_IOERR",
|
|
33
|
+
"SQLITE_READONLY",
|
|
34
|
+
]);
|
|
35
|
+
const STORAGE_FAILURE_CODE_SET = new Set(STORAGE_FAILURE_CODES);
|
|
36
|
+
|
|
24
37
|
export const SCHEDULER_APPLICATION_ID = 0x4343534b; // "CCSK"
|
|
25
|
-
export const SCHEDULER_STORE_SCHEMA_VERSION =
|
|
38
|
+
export const SCHEDULER_STORE_SCHEMA_VERSION = 6;
|
|
26
39
|
export const DEFAULT_BUSY_TIMEOUT_MS = 5_000;
|
|
27
40
|
export const MAX_LEASE_MS = 24 * 60 * 60 * 1_000;
|
|
28
41
|
export const MIN_AUTHORITY_WINDOW_MS = 60_000;
|
|
29
42
|
export const MAX_AUTHORITY_WINDOW_MS = 31 * 24 * 60 * 60 * 1_000;
|
|
30
43
|
export const MAX_AUTHORITY_BUDGET = 1_000_000;
|
|
44
|
+
export const MAX_RUNTIME_CHECKPOINT_BYTES = 64 * 1024;
|
|
31
45
|
export const DEFAULT_AUTHORITY_WINDOW_MS = 24 * 60 * 60 * 1_000;
|
|
32
46
|
export const DEFAULT_AUTHORITY_MAX_RUNS = 100_000;
|
|
33
47
|
export const DEFAULT_AUTHORITY_MAX_UNITS = 100_000;
|
|
48
|
+
export const MAX_RUNTIME_CONTROL_OCCURRENCES = 200;
|
|
49
|
+
export const RUNTIME_CONTROL_OCCURRENCE_STATUSES = Object.freeze([
|
|
50
|
+
"running",
|
|
51
|
+
"pause_requested",
|
|
52
|
+
"paused",
|
|
53
|
+
]);
|
|
54
|
+
export const RUNTIME_CONTROL_JOB_KINDS = Object.freeze([
|
|
55
|
+
"agenda",
|
|
56
|
+
"automation",
|
|
57
|
+
"automation-event",
|
|
58
|
+
"cowork-cron",
|
|
59
|
+
"loop-iteration",
|
|
60
|
+
"routine",
|
|
61
|
+
]);
|
|
62
|
+
const RUNTIME_CONTROL_OCCURRENCE_STATUS_SET = new Set(
|
|
63
|
+
RUNTIME_CONTROL_OCCURRENCE_STATUSES,
|
|
64
|
+
);
|
|
65
|
+
const RUNTIME_CONTROL_JOB_KIND_SET = new Set(RUNTIME_CONTROL_JOB_KINDS);
|
|
34
66
|
export const SCHEDULER_ADJUDICATION_AUTHORITY = "local-operator";
|
|
35
67
|
export const SCHEDULER_ADJUDICATION_DECISIONS = Object.freeze({
|
|
36
68
|
CONFIRMED_APPLIED: "confirmed_applied",
|
|
@@ -58,6 +90,7 @@ const MIGRATION_V2_NAME = "scheduler-kernel-authority-v2";
|
|
|
58
90
|
const MIGRATION_V3_NAME = "scheduler-kernel-adjudication-v3";
|
|
59
91
|
const MIGRATION_V4_NAME = "scheduler-kernel-domain-migration-v4";
|
|
60
92
|
const MIGRATION_V5_NAME = "scheduler-kernel-source-locator-v5";
|
|
93
|
+
const MIGRATION_V6_NAME = "scheduler-kernel-runtime-control-v6";
|
|
61
94
|
const V1_USER_TABLES = Object.freeze([
|
|
62
95
|
"events",
|
|
63
96
|
"jobs",
|
|
@@ -77,12 +110,17 @@ const V3_USER_TABLES = Object.freeze([
|
|
|
77
110
|
...V2_USER_TABLES,
|
|
78
111
|
"scheduler_occurrence_adjudications",
|
|
79
112
|
]);
|
|
80
|
-
const
|
|
113
|
+
const V4_V5_USER_TABLES = Object.freeze([
|
|
81
114
|
...V2_USER_TABLES,
|
|
82
115
|
"scheduler_domain_migration_entries",
|
|
83
116
|
"scheduler_domain_migrations",
|
|
84
117
|
"scheduler_occurrence_adjudications",
|
|
85
118
|
]);
|
|
119
|
+
const USER_TABLES = Object.freeze([
|
|
120
|
+
...V4_V5_USER_TABLES,
|
|
121
|
+
"scheduler_occurrence_controls",
|
|
122
|
+
"scheduler_occurrence_retries",
|
|
123
|
+
]);
|
|
86
124
|
|
|
87
125
|
export const MIGRATION_V1_SQL = `
|
|
88
126
|
CREATE TABLE migrations (
|
|
@@ -415,6 +453,68 @@ export const MIGRATION_V5_CHECKSUM = createHash("sha256")
|
|
|
415
453
|
.update(MIGRATION_V5_SQL.trim().replace(/\r\n/g, "\n"), "utf8")
|
|
416
454
|
.digest("hex");
|
|
417
455
|
|
|
456
|
+
export const MIGRATION_V6_SQL = `
|
|
457
|
+
CREATE TABLE scheduler_occurrence_controls (
|
|
458
|
+
occurrence_id TEXT PRIMARY KEY NOT NULL
|
|
459
|
+
REFERENCES occurrences(occurrence_id) ON DELETE RESTRICT,
|
|
460
|
+
capability_json TEXT NOT NULL CHECK (json_valid(capability_json)),
|
|
461
|
+
capability_digest TEXT NOT NULL CHECK (
|
|
462
|
+
capability_digest GLOB 'sha256:[0-9a-f]*'
|
|
463
|
+
AND length(capability_digest) = 71
|
|
464
|
+
),
|
|
465
|
+
state TEXT NOT NULL CHECK (
|
|
466
|
+
state IN ('pause_requested', 'paused', 'resumed', 'terminal')
|
|
467
|
+
),
|
|
468
|
+
revision INTEGER NOT NULL CHECK (revision >= 1),
|
|
469
|
+
pause_request_id TEXT NOT NULL,
|
|
470
|
+
resume_request_id TEXT UNIQUE,
|
|
471
|
+
expected_fence INTEGER NOT NULL CHECK (expected_fence >= 1),
|
|
472
|
+
checkpoint_json TEXT CHECK (
|
|
473
|
+
checkpoint_json IS NULL OR json_valid(checkpoint_json)
|
|
474
|
+
),
|
|
475
|
+
requested_at INTEGER NOT NULL CHECK (requested_at >= 0),
|
|
476
|
+
paused_at INTEGER CHECK (paused_at >= 0),
|
|
477
|
+
resumed_at INTEGER CHECK (resumed_at >= 0),
|
|
478
|
+
terminal_at INTEGER CHECK (terminal_at >= 0),
|
|
479
|
+
updated_at INTEGER NOT NULL CHECK (updated_at >= 0),
|
|
480
|
+
CHECK (
|
|
481
|
+
(state = 'pause_requested' AND paused_at IS NULL
|
|
482
|
+
AND resumed_at IS NULL AND terminal_at IS NULL)
|
|
483
|
+
OR
|
|
484
|
+
(state = 'paused' AND paused_at IS NOT NULL
|
|
485
|
+
AND resumed_at IS NULL AND terminal_at IS NULL)
|
|
486
|
+
OR
|
|
487
|
+
(state = 'resumed' AND paused_at IS NOT NULL
|
|
488
|
+
AND resumed_at IS NOT NULL AND terminal_at IS NULL)
|
|
489
|
+
OR
|
|
490
|
+
(state = 'terminal' AND terminal_at IS NOT NULL)
|
|
491
|
+
)
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
CREATE UNIQUE INDEX scheduler_occurrence_controls_pause_request
|
|
495
|
+
ON scheduler_occurrence_controls(pause_request_id);
|
|
496
|
+
CREATE INDEX scheduler_occurrence_controls_state
|
|
497
|
+
ON scheduler_occurrence_controls(state, updated_at, occurrence_id);
|
|
498
|
+
|
|
499
|
+
CREATE TABLE scheduler_occurrence_retries (
|
|
500
|
+
occurrence_id TEXT NOT NULL
|
|
501
|
+
REFERENCES occurrences(occurrence_id) ON DELETE RESTRICT,
|
|
502
|
+
request_id TEXT NOT NULL UNIQUE,
|
|
503
|
+
expected_fence INTEGER NOT NULL CHECK (expected_fence >= 1),
|
|
504
|
+
expected_error_code TEXT NOT NULL,
|
|
505
|
+
created_at INTEGER NOT NULL CHECK (created_at >= 0),
|
|
506
|
+
claimed_at INTEGER CHECK (claimed_at >= 0),
|
|
507
|
+
PRIMARY KEY (occurrence_id, request_id)
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
CREATE INDEX scheduler_occurrence_retries_pending
|
|
511
|
+
ON scheduler_occurrence_retries(occurrence_id, claimed_at, expected_fence);
|
|
512
|
+
`;
|
|
513
|
+
|
|
514
|
+
export const MIGRATION_V6_CHECKSUM = createHash("sha256")
|
|
515
|
+
.update(MIGRATION_V6_SQL.trim().replace(/\r\n/g, "\n"), "utf8")
|
|
516
|
+
.digest("hex");
|
|
517
|
+
|
|
418
518
|
// Fingerprint of the normalized sqlite_master catalog produced by the v1 DDL.
|
|
419
519
|
// Unlike the migration-source checksum, this also detects added triggers/views
|
|
420
520
|
// and constraint/foreign-key changes that preserve the visible column list.
|
|
@@ -428,6 +528,8 @@ export const SCHEMA_V4_FINGERPRINT =
|
|
|
428
528
|
"59762b9d5da862b857edb76021ed51d05a256133dce543a3fd7d7b0403c9c081";
|
|
429
529
|
export const SCHEMA_V5_FINGERPRINT =
|
|
430
530
|
"9619647628488fc4fdc79ad36b4b6632d8257f40c23245822a18941870c187ac";
|
|
531
|
+
export const SCHEMA_V6_FINGERPRINT =
|
|
532
|
+
"6abe5595ba3f772c9643440b27d18d5d40057c85523ecb3b4986b195d67787c2";
|
|
431
533
|
|
|
432
534
|
const EXPECTED_COLUMNS = Object.freeze({
|
|
433
535
|
migrations: [
|
|
@@ -531,6 +633,30 @@ const EXPECTED_COLUMNS = Object.freeze({
|
|
|
531
633
|
["retry_settled_at", "INTEGER", 0, 0],
|
|
532
634
|
["retry_outcome_json", "TEXT", 0, 0],
|
|
533
635
|
],
|
|
636
|
+
scheduler_occurrence_controls: [
|
|
637
|
+
["occurrence_id", "TEXT", 1, 1],
|
|
638
|
+
["capability_json", "TEXT", 1, 0],
|
|
639
|
+
["capability_digest", "TEXT", 1, 0],
|
|
640
|
+
["state", "TEXT", 1, 0],
|
|
641
|
+
["revision", "INTEGER", 1, 0],
|
|
642
|
+
["pause_request_id", "TEXT", 1, 0],
|
|
643
|
+
["resume_request_id", "TEXT", 0, 0],
|
|
644
|
+
["expected_fence", "INTEGER", 1, 0],
|
|
645
|
+
["checkpoint_json", "TEXT", 0, 0],
|
|
646
|
+
["requested_at", "INTEGER", 1, 0],
|
|
647
|
+
["paused_at", "INTEGER", 0, 0],
|
|
648
|
+
["resumed_at", "INTEGER", 0, 0],
|
|
649
|
+
["terminal_at", "INTEGER", 0, 0],
|
|
650
|
+
["updated_at", "INTEGER", 1, 0],
|
|
651
|
+
],
|
|
652
|
+
scheduler_occurrence_retries: [
|
|
653
|
+
["occurrence_id", "TEXT", 1, 1],
|
|
654
|
+
["request_id", "TEXT", 1, 2],
|
|
655
|
+
["expected_fence", "INTEGER", 1, 0],
|
|
656
|
+
["expected_error_code", "TEXT", 1, 0],
|
|
657
|
+
["created_at", "INTEGER", 1, 0],
|
|
658
|
+
["claimed_at", "INTEGER", 0, 0],
|
|
659
|
+
],
|
|
534
660
|
scheduler_domain_migrations: [
|
|
535
661
|
["migration_id", "TEXT", 1, 1],
|
|
536
662
|
["manifest_digest", "TEXT", 1, 0],
|
|
@@ -579,6 +705,9 @@ const EXPECTED_INDEXES = Object.freeze([
|
|
|
579
705
|
"scheduler_domain_migrations_state",
|
|
580
706
|
"scheduler_events_job",
|
|
581
707
|
"scheduler_events_occurrence",
|
|
708
|
+
"scheduler_occurrence_controls_pause_request",
|
|
709
|
+
"scheduler_occurrence_controls_state",
|
|
710
|
+
"scheduler_occurrence_retries_pending",
|
|
582
711
|
"scheduler_occurrences_claim",
|
|
583
712
|
"scheduler_occurrences_idempotency",
|
|
584
713
|
"scheduler_occurrences_job",
|
|
@@ -593,6 +722,45 @@ function schemaError(code, message, cause = undefined, details = undefined) {
|
|
|
593
722
|
);
|
|
594
723
|
}
|
|
595
724
|
|
|
725
|
+
function normalizedStorageFailureCode(value) {
|
|
726
|
+
if (typeof value !== "string") return null;
|
|
727
|
+
const candidate = value.trim().toUpperCase();
|
|
728
|
+
if (STORAGE_FAILURE_CODE_SET.has(candidate)) return candidate;
|
|
729
|
+
return (
|
|
730
|
+
STORAGE_FAILURE_CODES.find((code) => candidate.startsWith(`${code}_`)) ||
|
|
731
|
+
null
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function findStorageFailureCode(error) {
|
|
736
|
+
const visited = new Set();
|
|
737
|
+
let current = error;
|
|
738
|
+
while (
|
|
739
|
+
current !== null &&
|
|
740
|
+
(typeof current === "object" || typeof current === "function") &&
|
|
741
|
+
!visited.has(current)
|
|
742
|
+
) {
|
|
743
|
+
visited.add(current);
|
|
744
|
+
const code = normalizedStorageFailureCode(current.code);
|
|
745
|
+
if (code) return code;
|
|
746
|
+
current = current.cause;
|
|
747
|
+
}
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function storageUnavailableError({ phase, storageCode, commitState }) {
|
|
752
|
+
return new SchedulerKernelError(
|
|
753
|
+
"SCHEDULER_STORAGE_UNAVAILABLE",
|
|
754
|
+
"Scheduler storage is unavailable",
|
|
755
|
+
{
|
|
756
|
+
phase,
|
|
757
|
+
storageCode,
|
|
758
|
+
commitState,
|
|
759
|
+
retryable: false,
|
|
760
|
+
},
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
|
|
596
764
|
function assertPositiveSafeInteger(value, field) {
|
|
597
765
|
if (!Number.isSafeInteger(value) || value < 1) {
|
|
598
766
|
throw invalidArgument(`${field} must be a positive integer`);
|
|
@@ -843,6 +1011,109 @@ function mapOccurrence(row) {
|
|
|
843
1011
|
};
|
|
844
1012
|
}
|
|
845
1013
|
|
|
1014
|
+
function mapRuntimeControlOccurrence(row) {
|
|
1015
|
+
if (!row) return null;
|
|
1016
|
+
return {
|
|
1017
|
+
id: row.occurrence_id,
|
|
1018
|
+
jobId: row.job_id,
|
|
1019
|
+
jobKind: row.job_kind,
|
|
1020
|
+
runtimeStatus: row.runtime_status,
|
|
1021
|
+
occurrenceStatus: row.occurrence_status,
|
|
1022
|
+
scheduledFor: row.scheduled_for,
|
|
1023
|
+
attempt: row.attempt,
|
|
1024
|
+
maxAttempts: row.max_attempts,
|
|
1025
|
+
fence: row.fence,
|
|
1026
|
+
createdAt: row.created_at,
|
|
1027
|
+
updatedAt: row.updated_at,
|
|
1028
|
+
control:
|
|
1029
|
+
row.control_state === null
|
|
1030
|
+
? null
|
|
1031
|
+
: {
|
|
1032
|
+
state: row.control_state,
|
|
1033
|
+
revision: row.control_revision,
|
|
1034
|
+
expectedFence: row.control_expected_fence,
|
|
1035
|
+
capabilityDigest: row.control_capability_digest,
|
|
1036
|
+
requestedAt: row.control_requested_at,
|
|
1037
|
+
pausedAt: row.control_paused_at,
|
|
1038
|
+
updatedAt: row.control_updated_at,
|
|
1039
|
+
},
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function normalizeRuntimeControlEnumeration(values, allowed, field) {
|
|
1044
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
1045
|
+
throw invalidArgument(`${field} must be a non-empty array`);
|
|
1046
|
+
}
|
|
1047
|
+
const normalized = [
|
|
1048
|
+
...new Set(
|
|
1049
|
+
values.map((value, index) =>
|
|
1050
|
+
normalizeIdentifier(value, `${field}[${index}]`, { maxLength: 128 }),
|
|
1051
|
+
),
|
|
1052
|
+
),
|
|
1053
|
+
];
|
|
1054
|
+
const unsupported = normalized.filter((value) => !allowed.has(value));
|
|
1055
|
+
if (unsupported.length > 0) {
|
|
1056
|
+
throw invalidArgument(`${field} contains unsupported values`, {
|
|
1057
|
+
values: unsupported.sort(),
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
return normalized.sort();
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
export function schedulerRuntimeControlCapabilityDigest(capability) {
|
|
1064
|
+
return sha256PayloadDigest(
|
|
1065
|
+
normalizeRuntimeControlCapability(capability),
|
|
1066
|
+
"runtimeControl.capability",
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function mapOccurrenceControl(row) {
|
|
1071
|
+
if (!row) return null;
|
|
1072
|
+
const capability = normalizeRuntimeControlCapability(
|
|
1073
|
+
readStoredJson(row.capability_json, "scheduler runtime control capability"),
|
|
1074
|
+
);
|
|
1075
|
+
const capabilityDigest = schedulerRuntimeControlCapabilityDigest(capability);
|
|
1076
|
+
if (capabilityDigest !== row.capability_digest) {
|
|
1077
|
+
throw schemaError(
|
|
1078
|
+
"SCHEDULER_DATA_CORRUPT",
|
|
1079
|
+
`Scheduler runtime-control capability digest is invalid: ${row.occurrence_id}`,
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
return {
|
|
1083
|
+
occurrenceId: row.occurrence_id,
|
|
1084
|
+
capability,
|
|
1085
|
+
capabilityDigest,
|
|
1086
|
+
state: row.state,
|
|
1087
|
+
revision: row.revision,
|
|
1088
|
+
pauseRequestId: row.pause_request_id,
|
|
1089
|
+
resumeRequestId: row.resume_request_id,
|
|
1090
|
+
expectedFence: row.expected_fence,
|
|
1091
|
+
checkpoint:
|
|
1092
|
+
row.checkpoint_json === null
|
|
1093
|
+
? null
|
|
1094
|
+
: readStoredJson(
|
|
1095
|
+
row.checkpoint_json,
|
|
1096
|
+
"scheduler runtime control checkpoint",
|
|
1097
|
+
),
|
|
1098
|
+
requestedAt: row.requested_at,
|
|
1099
|
+
pausedAt: row.paused_at,
|
|
1100
|
+
resumedAt: row.resumed_at,
|
|
1101
|
+
terminalAt: row.terminal_at,
|
|
1102
|
+
updatedAt: row.updated_at,
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
function normalizeRuntimeCheckpoint(value) {
|
|
1107
|
+
const checkpoint = normalizeJson(value ?? null, "runtimeControl.checkpoint");
|
|
1108
|
+
const encoded = canonicalJson(checkpoint, "runtimeControl.checkpoint");
|
|
1109
|
+
if (Buffer.byteLength(encoded, "utf8") > MAX_RUNTIME_CHECKPOINT_BYTES) {
|
|
1110
|
+
throw invalidArgument(
|
|
1111
|
+
`runtimeControl.checkpoint exceeds ${MAX_RUNTIME_CHECKPOINT_BYTES} encoded bytes`,
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
return { checkpoint, encoded };
|
|
1115
|
+
}
|
|
1116
|
+
|
|
846
1117
|
function mapEvent(row) {
|
|
847
1118
|
return {
|
|
848
1119
|
eventId: row.event_id,
|
|
@@ -1483,7 +1754,9 @@ function verifySchema(db, version = SCHEDULER_STORE_SCHEMA_VERSION) {
|
|
|
1483
1754
|
? V2_USER_TABLES
|
|
1484
1755
|
: version === 3
|
|
1485
1756
|
? V3_USER_TABLES
|
|
1486
|
-
:
|
|
1757
|
+
: version <= 5
|
|
1758
|
+
? V4_V5_USER_TABLES
|
|
1759
|
+
: USER_TABLES;
|
|
1487
1760
|
if (JSON.stringify(tables) !== JSON.stringify(expectedTables)) {
|
|
1488
1761
|
throw schemaError(
|
|
1489
1762
|
"SCHEDULER_SCHEMA_UNKNOWN",
|
|
@@ -1526,7 +1799,9 @@ function verifySchema(db, version = SCHEDULER_STORE_SCHEMA_VERSION) {
|
|
|
1526
1799
|
? SCHEMA_V3_FINGERPRINT
|
|
1527
1800
|
: version === 4
|
|
1528
1801
|
? SCHEMA_V4_FINGERPRINT
|
|
1529
|
-
:
|
|
1802
|
+
: version === 5
|
|
1803
|
+
? SCHEMA_V5_FINGERPRINT
|
|
1804
|
+
: SCHEMA_V6_FINGERPRINT;
|
|
1530
1805
|
if (actualFingerprint !== expectedFingerprint) {
|
|
1531
1806
|
throw schemaError(
|
|
1532
1807
|
"SCHEDULER_SCHEMA_CORRUPT",
|
|
@@ -1573,7 +1848,11 @@ function verifySchema(db, version = SCHEDULER_STORE_SCHEMA_VERSION) {
|
|
|
1573
1848
|
(version >= 5 &&
|
|
1574
1849
|
(migrations[4]?.version !== 5 ||
|
|
1575
1850
|
migrations[4]?.name !== MIGRATION_V5_NAME ||
|
|
1576
|
-
migrations[4]?.checksum !== MIGRATION_V5_CHECKSUM))
|
|
1851
|
+
migrations[4]?.checksum !== MIGRATION_V5_CHECKSUM)) ||
|
|
1852
|
+
(version >= 6 &&
|
|
1853
|
+
(migrations[5]?.version !== 6 ||
|
|
1854
|
+
migrations[5]?.name !== MIGRATION_V6_NAME ||
|
|
1855
|
+
migrations[5]?.checksum !== MIGRATION_V6_CHECKSUM))
|
|
1577
1856
|
) {
|
|
1578
1857
|
throw schemaError(
|
|
1579
1858
|
"SCHEDULER_SCHEMA_UNKNOWN",
|
|
@@ -1593,7 +1872,9 @@ function verifySchema(db, version = SCHEDULER_STORE_SCHEMA_VERSION) {
|
|
|
1593
1872
|
(name) =>
|
|
1594
1873
|
(version >= 2 || !name.startsWith("scheduler_authority_")) &&
|
|
1595
1874
|
(version >= 3 || !name.startsWith("scheduler_adjudications_")) &&
|
|
1596
|
-
(version >= 4 || !name.startsWith("scheduler_domain_migration"))
|
|
1875
|
+
(version >= 4 || !name.startsWith("scheduler_domain_migration")) &&
|
|
1876
|
+
(version >= 6 || !name.startsWith("scheduler_occurrence_controls_")) &&
|
|
1877
|
+
(version >= 6 || !name.startsWith("scheduler_occurrence_retries_")),
|
|
1597
1878
|
);
|
|
1598
1879
|
if (JSON.stringify(indexes) !== JSON.stringify(expectedIndexes)) {
|
|
1599
1880
|
throw schemaError(
|
|
@@ -1713,6 +1994,10 @@ function migrateSourceLocatorV5(db) {
|
|
|
1713
1994
|
db.exec(MIGRATION_V5_SQL);
|
|
1714
1995
|
}
|
|
1715
1996
|
|
|
1997
|
+
function migrateRuntimeControlV6(db) {
|
|
1998
|
+
db.exec(MIGRATION_V6_SQL);
|
|
1999
|
+
}
|
|
2000
|
+
|
|
1716
2001
|
function initializeOrVerifySchema(db, now) {
|
|
1717
2002
|
assertDatabaseIntegrity(db);
|
|
1718
2003
|
const tables = listUserTables(db);
|
|
@@ -1723,6 +2008,7 @@ function initializeOrVerifySchema(db, now) {
|
|
|
1723
2008
|
migrateAdjudicationV3(db);
|
|
1724
2009
|
migrateDomainMigrationV4(db);
|
|
1725
2010
|
migrateSourceLocatorV5(db);
|
|
2011
|
+
migrateRuntimeControlV6(db);
|
|
1726
2012
|
db.prepare(
|
|
1727
2013
|
"INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
|
|
1728
2014
|
).run(1, MIGRATION_V1_NAME, MIGRATION_V1_CHECKSUM, now);
|
|
@@ -1738,6 +2024,9 @@ function initializeOrVerifySchema(db, now) {
|
|
|
1738
2024
|
db.prepare(
|
|
1739
2025
|
"INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
|
|
1740
2026
|
).run(5, MIGRATION_V5_NAME, MIGRATION_V5_CHECKSUM, now);
|
|
2027
|
+
db.prepare(
|
|
2028
|
+
"INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
|
|
2029
|
+
).run(6, MIGRATION_V6_NAME, MIGRATION_V6_CHECKSUM, now);
|
|
1741
2030
|
db.pragma(`application_id = ${SCHEDULER_APPLICATION_ID}`);
|
|
1742
2031
|
db.pragma(`user_version = ${SCHEDULER_STORE_SCHEMA_VERSION}`);
|
|
1743
2032
|
});
|
|
@@ -1756,6 +2045,7 @@ function initializeOrVerifySchema(db, now) {
|
|
|
1756
2045
|
migrateAdjudicationV3(db);
|
|
1757
2046
|
migrateDomainMigrationV4(db);
|
|
1758
2047
|
migrateSourceLocatorV5(db);
|
|
2048
|
+
migrateRuntimeControlV6(db);
|
|
1759
2049
|
db.prepare(
|
|
1760
2050
|
"INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
|
|
1761
2051
|
).run(2, MIGRATION_V2_NAME, MIGRATION_V2_CHECKSUM, now);
|
|
@@ -1768,6 +2058,9 @@ function initializeOrVerifySchema(db, now) {
|
|
|
1768
2058
|
db.prepare(
|
|
1769
2059
|
"INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
|
|
1770
2060
|
).run(5, MIGRATION_V5_NAME, MIGRATION_V5_CHECKSUM, now);
|
|
2061
|
+
db.prepare(
|
|
2062
|
+
"INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
|
|
2063
|
+
).run(6, MIGRATION_V6_NAME, MIGRATION_V6_CHECKSUM, now);
|
|
1771
2064
|
db.pragma(`user_version = ${SCHEDULER_STORE_SCHEMA_VERSION}`);
|
|
1772
2065
|
});
|
|
1773
2066
|
migrate.immediate();
|
|
@@ -1777,6 +2070,7 @@ function initializeOrVerifySchema(db, now) {
|
|
|
1777
2070
|
migrateAdjudicationV3(db);
|
|
1778
2071
|
migrateDomainMigrationV4(db);
|
|
1779
2072
|
migrateSourceLocatorV5(db);
|
|
2073
|
+
migrateRuntimeControlV6(db);
|
|
1780
2074
|
db.prepare(
|
|
1781
2075
|
"INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
|
|
1782
2076
|
).run(3, MIGRATION_V3_NAME, MIGRATION_V3_CHECKSUM, now);
|
|
@@ -1786,6 +2080,9 @@ function initializeOrVerifySchema(db, now) {
|
|
|
1786
2080
|
db.prepare(
|
|
1787
2081
|
"INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
|
|
1788
2082
|
).run(5, MIGRATION_V5_NAME, MIGRATION_V5_CHECKSUM, now);
|
|
2083
|
+
db.prepare(
|
|
2084
|
+
"INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
|
|
2085
|
+
).run(6, MIGRATION_V6_NAME, MIGRATION_V6_CHECKSUM, now);
|
|
1789
2086
|
db.pragma(`user_version = ${SCHEDULER_STORE_SCHEMA_VERSION}`);
|
|
1790
2087
|
});
|
|
1791
2088
|
migrate.immediate();
|
|
@@ -1794,12 +2091,16 @@ function initializeOrVerifySchema(db, now) {
|
|
|
1794
2091
|
const migrate = db.transaction(() => {
|
|
1795
2092
|
migrateDomainMigrationV4(db);
|
|
1796
2093
|
migrateSourceLocatorV5(db);
|
|
2094
|
+
migrateRuntimeControlV6(db);
|
|
1797
2095
|
db.prepare(
|
|
1798
2096
|
"INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
|
|
1799
2097
|
).run(4, MIGRATION_V4_NAME, MIGRATION_V4_CHECKSUM, now);
|
|
1800
2098
|
db.prepare(
|
|
1801
2099
|
"INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
|
|
1802
2100
|
).run(5, MIGRATION_V5_NAME, MIGRATION_V5_CHECKSUM, now);
|
|
2101
|
+
db.prepare(
|
|
2102
|
+
"INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
|
|
2103
|
+
).run(6, MIGRATION_V6_NAME, MIGRATION_V6_CHECKSUM, now);
|
|
1803
2104
|
db.pragma(`user_version = ${SCHEDULER_STORE_SCHEMA_VERSION}`);
|
|
1804
2105
|
});
|
|
1805
2106
|
migrate.immediate();
|
|
@@ -1807,9 +2108,23 @@ function initializeOrVerifySchema(db, now) {
|
|
|
1807
2108
|
verifySchema(db, 4);
|
|
1808
2109
|
const migrate = db.transaction(() => {
|
|
1809
2110
|
migrateSourceLocatorV5(db);
|
|
2111
|
+
migrateRuntimeControlV6(db);
|
|
1810
2112
|
db.prepare(
|
|
1811
2113
|
"INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
|
|
1812
2114
|
).run(5, MIGRATION_V5_NAME, MIGRATION_V5_CHECKSUM, now);
|
|
2115
|
+
db.prepare(
|
|
2116
|
+
"INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
|
|
2117
|
+
).run(6, MIGRATION_V6_NAME, MIGRATION_V6_CHECKSUM, now);
|
|
2118
|
+
db.pragma(`user_version = ${SCHEDULER_STORE_SCHEMA_VERSION}`);
|
|
2119
|
+
});
|
|
2120
|
+
migrate.immediate();
|
|
2121
|
+
} else if (db.pragma("user_version", { simple: true }) === 5) {
|
|
2122
|
+
verifySchema(db, 5);
|
|
2123
|
+
const migrate = db.transaction(() => {
|
|
2124
|
+
migrateRuntimeControlV6(db);
|
|
2125
|
+
db.prepare(
|
|
2126
|
+
"INSERT INTO migrations (version, name, checksum, applied_at) VALUES (?, ?, ?, ?)",
|
|
2127
|
+
).run(6, MIGRATION_V6_NAME, MIGRATION_V6_CHECKSUM, now);
|
|
1813
2128
|
db.pragma(`user_version = ${SCHEDULER_STORE_SCHEMA_VERSION}`);
|
|
1814
2129
|
});
|
|
1815
2130
|
migrate.immediate();
|
|
@@ -1848,11 +2163,21 @@ function openDatabase({ file, Database, busyTimeoutMs, clock }) {
|
|
|
1848
2163
|
}
|
|
1849
2164
|
const now = normalizeEpochMs(clock(), "clock result");
|
|
1850
2165
|
if (target !== ":memory:") {
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
2166
|
+
try {
|
|
2167
|
+
ensurePrivateDirectory(dirname(target));
|
|
2168
|
+
// Existing WAL/SHM files participate in the next SQLite open, so validate
|
|
2169
|
+
// them before the native driver consumes either sidecar.
|
|
2170
|
+
for (const candidate of sqliteStorageFiles(target)) {
|
|
2171
|
+
if (existsSync(candidate)) ensurePrivateFile(candidate);
|
|
2172
|
+
}
|
|
2173
|
+
} catch (cause) {
|
|
2174
|
+
const storageCode = findStorageFailureCode(cause);
|
|
2175
|
+
if (!storageCode) throw cause;
|
|
2176
|
+
throw storageUnavailableError({
|
|
2177
|
+
phase: "open",
|
|
2178
|
+
storageCode,
|
|
2179
|
+
commitState: "unknown",
|
|
2180
|
+
});
|
|
1856
2181
|
}
|
|
1857
2182
|
}
|
|
1858
2183
|
|
|
@@ -1873,6 +2198,14 @@ function openDatabase({ file, Database, busyTimeoutMs, clock }) {
|
|
|
1873
2198
|
// Preserve the authoritative open/schema failure.
|
|
1874
2199
|
}
|
|
1875
2200
|
if (cause instanceof SchedulerKernelError) throw cause;
|
|
2201
|
+
const storageCode = findStorageFailureCode(cause);
|
|
2202
|
+
if (storageCode) {
|
|
2203
|
+
throw storageUnavailableError({
|
|
2204
|
+
phase: "open",
|
|
2205
|
+
storageCode,
|
|
2206
|
+
commitState: "unknown",
|
|
2207
|
+
});
|
|
2208
|
+
}
|
|
1876
2209
|
throw schemaError(
|
|
1877
2210
|
"SCHEDULER_SCHEMA_CORRUPT",
|
|
1878
2211
|
"Scheduler database could not be opened or verified",
|
|
@@ -1881,12 +2214,27 @@ function openDatabase({ file, Database, busyTimeoutMs, clock }) {
|
|
|
1881
2214
|
}
|
|
1882
2215
|
|
|
1883
2216
|
if (target !== ":memory:") {
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
2217
|
+
try {
|
|
2218
|
+
// A quiet WAL database may not have materialized sidecars yet. On Windows,
|
|
2219
|
+
// asking secure-fs to classify a missing path deliberately falls back to a
|
|
2220
|
+
// native ACL preflight. Only inspect files that SQLite actually created;
|
|
2221
|
+
// later opens repeat this check for any surviving sidecars.
|
|
2222
|
+
for (const candidate of sqliteStorageFiles(target)) {
|
|
2223
|
+
if (existsSync(candidate)) ensurePrivateFile(candidate);
|
|
2224
|
+
}
|
|
2225
|
+
} catch (cause) {
|
|
2226
|
+
try {
|
|
2227
|
+
db.close();
|
|
2228
|
+
} catch {
|
|
2229
|
+
// Preserve the authoritative storage or security failure.
|
|
2230
|
+
}
|
|
2231
|
+
const storageCode = findStorageFailureCode(cause);
|
|
2232
|
+
if (!storageCode) throw cause;
|
|
2233
|
+
throw storageUnavailableError({
|
|
2234
|
+
phase: "open",
|
|
2235
|
+
storageCode,
|
|
2236
|
+
commitState: "unknown",
|
|
2237
|
+
});
|
|
1890
2238
|
}
|
|
1891
2239
|
}
|
|
1892
2240
|
return { db, target };
|
|
@@ -1913,6 +2261,18 @@ export class SchedulerStore {
|
|
|
1913
2261
|
getOccurrenceByKey: db.prepare(
|
|
1914
2262
|
"SELECT * FROM occurrences WHERE idempotency_key = ?",
|
|
1915
2263
|
),
|
|
2264
|
+
getOccurrenceControl: db.prepare(
|
|
2265
|
+
"SELECT * FROM scheduler_occurrence_controls WHERE occurrence_id = ?",
|
|
2266
|
+
),
|
|
2267
|
+
getOccurrenceControlByPauseRequest: db.prepare(
|
|
2268
|
+
"SELECT * FROM scheduler_occurrence_controls WHERE pause_request_id = ?",
|
|
2269
|
+
),
|
|
2270
|
+
getOccurrenceControlByResumeRequest: db.prepare(
|
|
2271
|
+
"SELECT * FROM scheduler_occurrence_controls WHERE resume_request_id = ?",
|
|
2272
|
+
),
|
|
2273
|
+
getOccurrenceRetryByRequest: db.prepare(
|
|
2274
|
+
"SELECT * FROM scheduler_occurrence_retries WHERE request_id = ?",
|
|
2275
|
+
),
|
|
1916
2276
|
listOccurrencesByTrigger: db.prepare(
|
|
1917
2277
|
`SELECT * FROM occurrences
|
|
1918
2278
|
WHERE job_id = ? AND trigger_key = ?
|
|
@@ -1954,8 +2314,46 @@ export class SchedulerStore {
|
|
|
1954
2314
|
|
|
1955
2315
|
_write(callback) {
|
|
1956
2316
|
this._assertOpen();
|
|
1957
|
-
|
|
1958
|
-
|
|
2317
|
+
let bodyState = "not_started";
|
|
2318
|
+
let bodyObservedTransaction = false;
|
|
2319
|
+
try {
|
|
2320
|
+
const transaction = this.db.transaction((...args) => {
|
|
2321
|
+
bodyState = "running";
|
|
2322
|
+
try {
|
|
2323
|
+
bodyObservedTransaction = this.db.inTransaction === true;
|
|
2324
|
+
} catch {
|
|
2325
|
+
bodyObservedTransaction = false;
|
|
2326
|
+
}
|
|
2327
|
+
const result = callback(...args);
|
|
2328
|
+
bodyState = "returned";
|
|
2329
|
+
return result;
|
|
2330
|
+
});
|
|
2331
|
+
return transaction.immediate();
|
|
2332
|
+
} catch (cause) {
|
|
2333
|
+
if (cause instanceof SchedulerKernelError) throw cause;
|
|
2334
|
+
const storageCode = findStorageFailureCode(cause);
|
|
2335
|
+
if (!storageCode) throw cause;
|
|
2336
|
+
let transactionClosed = false;
|
|
2337
|
+
try {
|
|
2338
|
+
transactionClosed = this.db.inTransaction === false;
|
|
2339
|
+
} catch {
|
|
2340
|
+
transactionClosed = false;
|
|
2341
|
+
}
|
|
2342
|
+
// Once the body returns, the wrapper may already be in COMMIT and an I/O
|
|
2343
|
+
// failure cannot distinguish an applied commit from an aborted one. A
|
|
2344
|
+
// body failure is only safe to call not-committed when we observed the
|
|
2345
|
+
// native transaction and can also observe that its rollback closed it.
|
|
2346
|
+
throw storageUnavailableError({
|
|
2347
|
+
phase: "write",
|
|
2348
|
+
storageCode,
|
|
2349
|
+
commitState:
|
|
2350
|
+
bodyState === "running" &&
|
|
2351
|
+
bodyObservedTransaction &&
|
|
2352
|
+
transactionClosed
|
|
2353
|
+
? "not_committed"
|
|
2354
|
+
: "unknown",
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
1959
2357
|
}
|
|
1960
2358
|
|
|
1961
2359
|
_appendEvent({
|
|
@@ -3468,6 +3866,576 @@ export class SchedulerStore {
|
|
|
3468
3866
|
);
|
|
3469
3867
|
}
|
|
3470
3868
|
|
|
3869
|
+
getOccurrenceControl(occurrenceId) {
|
|
3870
|
+
this._assertOpen();
|
|
3871
|
+
return mapOccurrenceControl(
|
|
3872
|
+
this.statements.getOccurrenceControl.get(
|
|
3873
|
+
normalizeIdentifier(occurrenceId, "occurrenceId"),
|
|
3874
|
+
),
|
|
3875
|
+
);
|
|
3876
|
+
}
|
|
3877
|
+
|
|
3878
|
+
/**
|
|
3879
|
+
* Enumerate the small, public runtime-control surface used by Automation
|
|
3880
|
+
* Center. This intentionally does not reuse mapOccurrence(): payload,
|
|
3881
|
+
* authority, lease ownership, checkpoints, results and error bodies never
|
|
3882
|
+
* cross this read boundary.
|
|
3883
|
+
*/
|
|
3884
|
+
listRuntimeControlOccurrences({
|
|
3885
|
+
statuses = RUNTIME_CONTROL_OCCURRENCE_STATUSES,
|
|
3886
|
+
jobKinds = RUNTIME_CONTROL_JOB_KINDS,
|
|
3887
|
+
limit = 50,
|
|
3888
|
+
} = {}) {
|
|
3889
|
+
this._assertOpen();
|
|
3890
|
+
const selectedStatuses = normalizeRuntimeControlEnumeration(
|
|
3891
|
+
statuses,
|
|
3892
|
+
RUNTIME_CONTROL_OCCURRENCE_STATUS_SET,
|
|
3893
|
+
"statuses",
|
|
3894
|
+
);
|
|
3895
|
+
const selectedJobKinds = normalizeRuntimeControlEnumeration(
|
|
3896
|
+
jobKinds,
|
|
3897
|
+
RUNTIME_CONTROL_JOB_KIND_SET,
|
|
3898
|
+
"jobKinds",
|
|
3899
|
+
);
|
|
3900
|
+
if (!Number.isSafeInteger(limit) || limit < 1) {
|
|
3901
|
+
throw invalidArgument("limit must be a positive integer");
|
|
3902
|
+
}
|
|
3903
|
+
const boundedLimit = Math.min(limit, MAX_RUNTIME_CONTROL_OCCURRENCES);
|
|
3904
|
+
const statusParameters = selectedStatuses.map(() => "?").join(", ");
|
|
3905
|
+
const kindParameters = selectedJobKinds.map(() => "?").join(", ");
|
|
3906
|
+
const rows = this.db
|
|
3907
|
+
.prepare(
|
|
3908
|
+
`SELECT o.occurrence_id, o.job_id, j.kind AS job_kind,
|
|
3909
|
+
CASE
|
|
3910
|
+
WHEN c.state = 'pause_requested' THEN 'pause_requested'
|
|
3911
|
+
WHEN c.state = 'paused' THEN 'paused'
|
|
3912
|
+
ELSE 'running'
|
|
3913
|
+
END AS runtime_status,
|
|
3914
|
+
o.status AS occurrence_status, o.scheduled_for, o.attempt,
|
|
3915
|
+
o.max_attempts, o.fence, o.created_at, o.updated_at,
|
|
3916
|
+
c.state AS control_state, c.revision AS control_revision,
|
|
3917
|
+
c.expected_fence AS control_expected_fence,
|
|
3918
|
+
c.capability_digest AS control_capability_digest,
|
|
3919
|
+
c.requested_at AS control_requested_at,
|
|
3920
|
+
c.paused_at AS control_paused_at,
|
|
3921
|
+
c.updated_at AS control_updated_at
|
|
3922
|
+
FROM occurrences o
|
|
3923
|
+
JOIN jobs j ON j.job_id = o.job_id
|
|
3924
|
+
LEFT JOIN scheduler_occurrence_controls c
|
|
3925
|
+
ON c.occurrence_id = o.occurrence_id
|
|
3926
|
+
WHERE j.kind IN (${kindParameters})
|
|
3927
|
+
AND (
|
|
3928
|
+
(o.status = 'running' AND c.occurrence_id IS NULL)
|
|
3929
|
+
OR (o.status = 'running' AND c.state = 'pause_requested')
|
|
3930
|
+
OR (o.status = 'running' AND c.state = 'resumed')
|
|
3931
|
+
OR (o.status = 'retry_wait' AND c.state = 'paused')
|
|
3932
|
+
)
|
|
3933
|
+
AND CASE
|
|
3934
|
+
WHEN c.state = 'pause_requested' THEN 'pause_requested'
|
|
3935
|
+
WHEN c.state = 'paused' THEN 'paused'
|
|
3936
|
+
ELSE 'running'
|
|
3937
|
+
END IN (${statusParameters})
|
|
3938
|
+
ORDER BY o.updated_at DESC, o.occurrence_id ASC
|
|
3939
|
+
LIMIT ?`,
|
|
3940
|
+
)
|
|
3941
|
+
.all(...selectedJobKinds, ...selectedStatuses, boundedLimit);
|
|
3942
|
+
return rows.map(mapRuntimeControlOccurrence);
|
|
3943
|
+
}
|
|
3944
|
+
|
|
3945
|
+
requestOccurrencePause({
|
|
3946
|
+
occurrenceId,
|
|
3947
|
+
expectedFence,
|
|
3948
|
+
expectedRevision,
|
|
3949
|
+
requestId,
|
|
3950
|
+
capability,
|
|
3951
|
+
} = {}) {
|
|
3952
|
+
const id = normalizeIdentifier(occurrenceId, "occurrenceId");
|
|
3953
|
+
const fence = assertPositiveSafeInteger(expectedFence, "expectedFence");
|
|
3954
|
+
const request = normalizeIdentifier(requestId, "requestId", {
|
|
3955
|
+
maxLength: 128,
|
|
3956
|
+
});
|
|
3957
|
+
const revision =
|
|
3958
|
+
expectedRevision === undefined
|
|
3959
|
+
? null
|
|
3960
|
+
: (() => {
|
|
3961
|
+
if (
|
|
3962
|
+
!Number.isSafeInteger(expectedRevision) ||
|
|
3963
|
+
expectedRevision < 0
|
|
3964
|
+
) {
|
|
3965
|
+
throw invalidArgument(
|
|
3966
|
+
"expectedRevision must be a non-negative integer",
|
|
3967
|
+
);
|
|
3968
|
+
}
|
|
3969
|
+
return expectedRevision;
|
|
3970
|
+
})();
|
|
3971
|
+
const normalizedCapability = normalizeRuntimeControlCapability(capability);
|
|
3972
|
+
if (
|
|
3973
|
+
normalizedCapability.pauseResume !== RUNTIME_PAUSE_RESUME.CHECKPOINT_V1
|
|
3974
|
+
) {
|
|
3975
|
+
throw new SchedulerKernelError(
|
|
3976
|
+
"SCHEDULER_PAUSE_UNSUPPORTED",
|
|
3977
|
+
"Scheduler adapter does not declare durable checkpoint pause/resume",
|
|
3978
|
+
);
|
|
3979
|
+
}
|
|
3980
|
+
const capabilityJson = canonicalJson(
|
|
3981
|
+
normalizedCapability,
|
|
3982
|
+
"runtimeControl.capability",
|
|
3983
|
+
);
|
|
3984
|
+
const capabilityDigest =
|
|
3985
|
+
schedulerRuntimeControlCapabilityDigest(normalizedCapability);
|
|
3986
|
+
const now = this._now();
|
|
3987
|
+
return this._write(() => {
|
|
3988
|
+
const occurrence = this.statements.getOccurrence.get(id);
|
|
3989
|
+
if (!occurrence) {
|
|
3990
|
+
throw new SchedulerKernelError(
|
|
3991
|
+
"SCHEDULER_NOT_FOUND",
|
|
3992
|
+
`Scheduler occurrence does not exist: ${id}`,
|
|
3993
|
+
);
|
|
3994
|
+
}
|
|
3995
|
+
const requestOwner =
|
|
3996
|
+
this.statements.getOccurrenceControlByPauseRequest.get(request);
|
|
3997
|
+
if (requestOwner && requestOwner.occurrence_id !== id) {
|
|
3998
|
+
throw new SchedulerKernelError(
|
|
3999
|
+
"SCHEDULER_OCCURRENCE_CONTROL_CONFLICT",
|
|
4000
|
+
"Scheduler pause request is already bound to another occurrence",
|
|
4001
|
+
);
|
|
4002
|
+
}
|
|
4003
|
+
const current = this.statements.getOccurrenceControl.get(id);
|
|
4004
|
+
if (current?.pause_request_id === request) {
|
|
4005
|
+
if (
|
|
4006
|
+
current.expected_fence !== fence ||
|
|
4007
|
+
current.capability_digest !== capabilityDigest
|
|
4008
|
+
) {
|
|
4009
|
+
throw new SchedulerKernelError(
|
|
4010
|
+
"SCHEDULER_OCCURRENCE_CONTROL_CONFLICT",
|
|
4011
|
+
`Scheduler pause request evidence changed: ${id}`,
|
|
4012
|
+
);
|
|
4013
|
+
}
|
|
4014
|
+
return { ...mapOccurrenceControl(current), deduplicated: true };
|
|
4015
|
+
}
|
|
4016
|
+
const actualRevision = current?.revision ?? 0;
|
|
4017
|
+
if (revision !== null && actualRevision !== revision) {
|
|
4018
|
+
throw new SchedulerKernelError(
|
|
4019
|
+
"SCHEDULER_REVISION_CONFLICT",
|
|
4020
|
+
`Scheduler occurrence control revision changed: ${id}`,
|
|
4021
|
+
{ expectedRevision: revision, actualRevision },
|
|
4022
|
+
);
|
|
4023
|
+
}
|
|
4024
|
+
if (current && current.state !== "terminal") {
|
|
4025
|
+
throw new SchedulerKernelError(
|
|
4026
|
+
"SCHEDULER_OCCURRENCE_CONTROL_CONFLICT",
|
|
4027
|
+
`Scheduler occurrence already has an active runtime control: ${id}`,
|
|
4028
|
+
{ state: current.state, revision: current.revision },
|
|
4029
|
+
);
|
|
4030
|
+
}
|
|
4031
|
+
if (occurrence.status !== OCCURRENCE_STATUS.RUNNING) {
|
|
4032
|
+
throw new SchedulerKernelError(
|
|
4033
|
+
"SCHEDULER_OCCURRENCE_NOT_RUNNING",
|
|
4034
|
+
`Scheduler occurrence is not running: ${id}`,
|
|
4035
|
+
{ status: occurrence.status },
|
|
4036
|
+
);
|
|
4037
|
+
}
|
|
4038
|
+
if (occurrence.fence !== fence) {
|
|
4039
|
+
throw new SchedulerKernelError(
|
|
4040
|
+
"SCHEDULER_FENCE_CONFLICT",
|
|
4041
|
+
`Scheduler occurrence fence changed: ${id}`,
|
|
4042
|
+
{ expectedFence: fence, actualFence: occurrence.fence },
|
|
4043
|
+
);
|
|
4044
|
+
}
|
|
4045
|
+
if (occurrence.lease_expires_at <= now) throw leaseLost(id);
|
|
4046
|
+
if (current) {
|
|
4047
|
+
this.db
|
|
4048
|
+
.prepare(
|
|
4049
|
+
`UPDATE scheduler_occurrence_controls
|
|
4050
|
+
SET capability_json = @capabilityJson,
|
|
4051
|
+
capability_digest = @capabilityDigest,
|
|
4052
|
+
state = 'pause_requested', revision = revision + 1,
|
|
4053
|
+
pause_request_id = @requestId, resume_request_id = NULL,
|
|
4054
|
+
expected_fence = @expectedFence, checkpoint_json = NULL,
|
|
4055
|
+
requested_at = @now, paused_at = NULL, resumed_at = NULL,
|
|
4056
|
+
terminal_at = NULL, updated_at = @now
|
|
4057
|
+
WHERE occurrence_id = @occurrenceId AND state = 'terminal'`,
|
|
4058
|
+
)
|
|
4059
|
+
.run({
|
|
4060
|
+
occurrenceId: id,
|
|
4061
|
+
capabilityJson,
|
|
4062
|
+
capabilityDigest,
|
|
4063
|
+
requestId: request,
|
|
4064
|
+
expectedFence: fence,
|
|
4065
|
+
now,
|
|
4066
|
+
});
|
|
4067
|
+
} else {
|
|
4068
|
+
this.db
|
|
4069
|
+
.prepare(
|
|
4070
|
+
`INSERT INTO scheduler_occurrence_controls
|
|
4071
|
+
(occurrence_id, capability_json, capability_digest, state,
|
|
4072
|
+
revision, pause_request_id, resume_request_id, expected_fence,
|
|
4073
|
+
checkpoint_json, requested_at, paused_at, resumed_at,
|
|
4074
|
+
terminal_at, updated_at)
|
|
4075
|
+
VALUES (@occurrenceId, @capabilityJson, @capabilityDigest,
|
|
4076
|
+
'pause_requested', 1, @requestId, NULL, @expectedFence,
|
|
4077
|
+
NULL, @now, NULL, NULL, NULL, @now)`,
|
|
4078
|
+
)
|
|
4079
|
+
.run({
|
|
4080
|
+
occurrenceId: id,
|
|
4081
|
+
capabilityJson,
|
|
4082
|
+
capabilityDigest,
|
|
4083
|
+
requestId: request,
|
|
4084
|
+
expectedFence: fence,
|
|
4085
|
+
now,
|
|
4086
|
+
});
|
|
4087
|
+
}
|
|
4088
|
+
const control = this.statements.getOccurrenceControl.get(id);
|
|
4089
|
+
this._appendEvent({
|
|
4090
|
+
jobId: occurrence.job_id,
|
|
4091
|
+
occurrenceId: id,
|
|
4092
|
+
type: "occurrence_pause_requested",
|
|
4093
|
+
occurredAt: now,
|
|
4094
|
+
fence,
|
|
4095
|
+
data: {
|
|
4096
|
+
requestId: request,
|
|
4097
|
+
controlRevision: control.revision,
|
|
4098
|
+
capabilityDigest,
|
|
4099
|
+
},
|
|
4100
|
+
});
|
|
4101
|
+
return { ...mapOccurrenceControl(control), deduplicated: false };
|
|
4102
|
+
});
|
|
4103
|
+
}
|
|
4104
|
+
|
|
4105
|
+
ackOccurrencePause({
|
|
4106
|
+
occurrenceId,
|
|
4107
|
+
ownerId,
|
|
4108
|
+
fence,
|
|
4109
|
+
requestId,
|
|
4110
|
+
expectedRevision,
|
|
4111
|
+
safePoint,
|
|
4112
|
+
checkpoint,
|
|
4113
|
+
} = {}) {
|
|
4114
|
+
const id = normalizeIdentifier(occurrenceId, "occurrenceId");
|
|
4115
|
+
const owner = normalizeIdentifier(ownerId, "ownerId");
|
|
4116
|
+
const token = assertPositiveSafeInteger(fence, "fence");
|
|
4117
|
+
const request = normalizeIdentifier(requestId, "requestId", {
|
|
4118
|
+
maxLength: 128,
|
|
4119
|
+
});
|
|
4120
|
+
const revision = assertPositiveSafeInteger(
|
|
4121
|
+
expectedRevision,
|
|
4122
|
+
"expectedRevision",
|
|
4123
|
+
);
|
|
4124
|
+
const point = normalizeIdentifier(safePoint, "safePoint", {
|
|
4125
|
+
maxLength: 64,
|
|
4126
|
+
});
|
|
4127
|
+
if (!Object.values(RUNTIME_CONTROL_SAFE_POINTS).includes(point)) {
|
|
4128
|
+
throw invalidArgument("safePoint is not a scheduler runtime safe point");
|
|
4129
|
+
}
|
|
4130
|
+
const normalizedCheckpoint = normalizeRuntimeCheckpoint({
|
|
4131
|
+
schemaVersion: 1,
|
|
4132
|
+
safePoint: point,
|
|
4133
|
+
data: checkpoint ?? null,
|
|
4134
|
+
});
|
|
4135
|
+
const now = this._now();
|
|
4136
|
+
return this._write(() => {
|
|
4137
|
+
const occurrence = this.statements.getOccurrence.get(id);
|
|
4138
|
+
const controlRow = this.statements.getOccurrenceControl.get(id);
|
|
4139
|
+
if (!occurrence || !controlRow) {
|
|
4140
|
+
throw new SchedulerKernelError(
|
|
4141
|
+
"SCHEDULER_NOT_FOUND",
|
|
4142
|
+
`Scheduler occurrence control does not exist: ${id}`,
|
|
4143
|
+
);
|
|
4144
|
+
}
|
|
4145
|
+
const control = mapOccurrenceControl(controlRow);
|
|
4146
|
+
if (
|
|
4147
|
+
control.state !== "pause_requested" ||
|
|
4148
|
+
control.pauseRequestId !== request
|
|
4149
|
+
) {
|
|
4150
|
+
throw new SchedulerKernelError(
|
|
4151
|
+
"SCHEDULER_OCCURRENCE_CONTROL_CONFLICT",
|
|
4152
|
+
`Scheduler pause request is no longer pending: ${id}`,
|
|
4153
|
+
{ state: control.state, revision: control.revision },
|
|
4154
|
+
);
|
|
4155
|
+
}
|
|
4156
|
+
if (control.revision !== revision) {
|
|
4157
|
+
throw new SchedulerKernelError(
|
|
4158
|
+
"SCHEDULER_REVISION_CONFLICT",
|
|
4159
|
+
`Scheduler occurrence control revision changed: ${id}`,
|
|
4160
|
+
{ expectedRevision: revision, actualRevision: control.revision },
|
|
4161
|
+
);
|
|
4162
|
+
}
|
|
4163
|
+
if (
|
|
4164
|
+
control.expectedFence !== token ||
|
|
4165
|
+
occurrence.status !== OCCURRENCE_STATUS.RUNNING ||
|
|
4166
|
+
occurrence.fence !== token ||
|
|
4167
|
+
occurrence.lease_owner !== owner ||
|
|
4168
|
+
occurrence.lease_expires_at <= now
|
|
4169
|
+
) {
|
|
4170
|
+
throw leaseLost(id);
|
|
4171
|
+
}
|
|
4172
|
+
if (!control.capability.safePoints.includes(point)) {
|
|
4173
|
+
throw new SchedulerKernelError(
|
|
4174
|
+
"SCHEDULER_PAUSE_UNSUPPORTED_SAFE_POINT",
|
|
4175
|
+
`Scheduler adapter did not declare pause safety at ${point}`,
|
|
4176
|
+
);
|
|
4177
|
+
}
|
|
4178
|
+
const occurrenceUpdate = this.db
|
|
4179
|
+
.prepare(
|
|
4180
|
+
`UPDATE occurrences
|
|
4181
|
+
SET status = 'retry_wait', available_at = @now,
|
|
4182
|
+
lease_owner = NULL, lease_expires_at = NULL,
|
|
4183
|
+
updated_at = @now, settled_at = NULL
|
|
4184
|
+
WHERE occurrence_id = @occurrenceId AND status = 'running'
|
|
4185
|
+
AND lease_owner = @ownerId AND fence = @fence
|
|
4186
|
+
AND lease_expires_at > @now`,
|
|
4187
|
+
)
|
|
4188
|
+
.run({ occurrenceId: id, ownerId: owner, fence: token, now });
|
|
4189
|
+
if (occurrenceUpdate.changes !== 1) throw leaseLost(id);
|
|
4190
|
+
const controlUpdate = this.db
|
|
4191
|
+
.prepare(
|
|
4192
|
+
`UPDATE scheduler_occurrence_controls
|
|
4193
|
+
SET state = 'paused', revision = revision + 1,
|
|
4194
|
+
checkpoint_json = @checkpointJson,
|
|
4195
|
+
paused_at = @now, updated_at = @now
|
|
4196
|
+
WHERE occurrence_id = @occurrenceId
|
|
4197
|
+
AND state = 'pause_requested' AND revision = @revision
|
|
4198
|
+
AND pause_request_id = @requestId AND expected_fence = @fence`,
|
|
4199
|
+
)
|
|
4200
|
+
.run({
|
|
4201
|
+
occurrenceId: id,
|
|
4202
|
+
checkpointJson: normalizedCheckpoint.encoded,
|
|
4203
|
+
now,
|
|
4204
|
+
revision,
|
|
4205
|
+
requestId: request,
|
|
4206
|
+
fence: token,
|
|
4207
|
+
});
|
|
4208
|
+
if (controlUpdate.changes !== 1) {
|
|
4209
|
+
throw new SchedulerKernelError(
|
|
4210
|
+
"SCHEDULER_OCCURRENCE_CONTROL_CONFLICT",
|
|
4211
|
+
`Scheduler pause acknowledgement lost its CAS: ${id}`,
|
|
4212
|
+
);
|
|
4213
|
+
}
|
|
4214
|
+
this._appendEvent({
|
|
4215
|
+
jobId: occurrence.job_id,
|
|
4216
|
+
occurrenceId: id,
|
|
4217
|
+
type: "occurrence_paused",
|
|
4218
|
+
occurredAt: now,
|
|
4219
|
+
fence: token,
|
|
4220
|
+
data: {
|
|
4221
|
+
requestId: request,
|
|
4222
|
+
safePoint: point,
|
|
4223
|
+
checkpointDigest: sha256PayloadDigest(
|
|
4224
|
+
normalizedCheckpoint.checkpoint,
|
|
4225
|
+
"runtimeControl.checkpoint",
|
|
4226
|
+
),
|
|
4227
|
+
},
|
|
4228
|
+
});
|
|
4229
|
+
return {
|
|
4230
|
+
control: mapOccurrenceControl(
|
|
4231
|
+
this.statements.getOccurrenceControl.get(id),
|
|
4232
|
+
),
|
|
4233
|
+
occurrence: mapOccurrence(this.statements.getOccurrence.get(id)),
|
|
4234
|
+
};
|
|
4235
|
+
});
|
|
4236
|
+
}
|
|
4237
|
+
|
|
4238
|
+
resumeOccurrence({ occurrenceId, expectedRevision, requestId } = {}) {
|
|
4239
|
+
const id = normalizeIdentifier(occurrenceId, "occurrenceId");
|
|
4240
|
+
const revision = assertPositiveSafeInteger(
|
|
4241
|
+
expectedRevision,
|
|
4242
|
+
"expectedRevision",
|
|
4243
|
+
);
|
|
4244
|
+
const request = normalizeIdentifier(requestId, "requestId", {
|
|
4245
|
+
maxLength: 128,
|
|
4246
|
+
});
|
|
4247
|
+
const now = this._now();
|
|
4248
|
+
return this._write(() => {
|
|
4249
|
+
const occurrence = this.statements.getOccurrence.get(id);
|
|
4250
|
+
const controlRow = this.statements.getOccurrenceControl.get(id);
|
|
4251
|
+
if (!occurrence || !controlRow) {
|
|
4252
|
+
throw new SchedulerKernelError(
|
|
4253
|
+
"SCHEDULER_NOT_FOUND",
|
|
4254
|
+
`Scheduler occurrence control does not exist: ${id}`,
|
|
4255
|
+
);
|
|
4256
|
+
}
|
|
4257
|
+
const requestOwner =
|
|
4258
|
+
this.statements.getOccurrenceControlByResumeRequest.get(request);
|
|
4259
|
+
if (requestOwner && requestOwner.occurrence_id !== id) {
|
|
4260
|
+
throw new SchedulerKernelError(
|
|
4261
|
+
"SCHEDULER_OCCURRENCE_CONTROL_CONFLICT",
|
|
4262
|
+
"Scheduler resume request is already bound to another occurrence",
|
|
4263
|
+
);
|
|
4264
|
+
}
|
|
4265
|
+
const control = mapOccurrenceControl(controlRow);
|
|
4266
|
+
if (control.resumeRequestId === request) {
|
|
4267
|
+
return {
|
|
4268
|
+
control: { ...control, deduplicated: true },
|
|
4269
|
+
occurrence: mapOccurrence(occurrence),
|
|
4270
|
+
};
|
|
4271
|
+
}
|
|
4272
|
+
if (control.state !== "paused") {
|
|
4273
|
+
throw new SchedulerKernelError(
|
|
4274
|
+
"SCHEDULER_OCCURRENCE_CONTROL_CONFLICT",
|
|
4275
|
+
`Scheduler occurrence is not paused: ${id}`,
|
|
4276
|
+
{ state: control.state, revision: control.revision },
|
|
4277
|
+
);
|
|
4278
|
+
}
|
|
4279
|
+
if (control.revision !== revision) {
|
|
4280
|
+
throw new SchedulerKernelError(
|
|
4281
|
+
"SCHEDULER_REVISION_CONFLICT",
|
|
4282
|
+
`Scheduler occurrence control revision changed: ${id}`,
|
|
4283
|
+
{ expectedRevision: revision, actualRevision: control.revision },
|
|
4284
|
+
);
|
|
4285
|
+
}
|
|
4286
|
+
if (
|
|
4287
|
+
occurrence.status !== OCCURRENCE_STATUS.RETRY_WAIT ||
|
|
4288
|
+
occurrence.lease_owner !== null ||
|
|
4289
|
+
occurrence.lease_expires_at !== null
|
|
4290
|
+
) {
|
|
4291
|
+
throw new SchedulerKernelError(
|
|
4292
|
+
"SCHEDULER_OCCURRENCE_CONTROL_CONFLICT",
|
|
4293
|
+
`Paused occurrence state is inconsistent: ${id}`,
|
|
4294
|
+
);
|
|
4295
|
+
}
|
|
4296
|
+
const update = this.db
|
|
4297
|
+
.prepare(
|
|
4298
|
+
`UPDATE scheduler_occurrence_controls
|
|
4299
|
+
SET state = 'resumed', revision = revision + 1,
|
|
4300
|
+
resume_request_id = @requestId, resumed_at = @now,
|
|
4301
|
+
updated_at = @now
|
|
4302
|
+
WHERE occurrence_id = @occurrenceId AND state = 'paused'
|
|
4303
|
+
AND revision = @revision`,
|
|
4304
|
+
)
|
|
4305
|
+
.run({ occurrenceId: id, requestId: request, now, revision });
|
|
4306
|
+
if (update.changes !== 1) {
|
|
4307
|
+
throw new SchedulerKernelError(
|
|
4308
|
+
"SCHEDULER_OCCURRENCE_CONTROL_CONFLICT",
|
|
4309
|
+
`Scheduler resume request lost its CAS: ${id}`,
|
|
4310
|
+
);
|
|
4311
|
+
}
|
|
4312
|
+
this.db
|
|
4313
|
+
.prepare(
|
|
4314
|
+
`UPDATE occurrences SET available_at = ?, updated_at = ?
|
|
4315
|
+
WHERE occurrence_id = ? AND status = 'retry_wait'`,
|
|
4316
|
+
)
|
|
4317
|
+
.run(now, now, id);
|
|
4318
|
+
this._appendEvent({
|
|
4319
|
+
jobId: occurrence.job_id,
|
|
4320
|
+
occurrenceId: id,
|
|
4321
|
+
type: "occurrence_resume_requested",
|
|
4322
|
+
occurredAt: now,
|
|
4323
|
+
fence: occurrence.fence,
|
|
4324
|
+
data: { requestId: request, controlRevision: revision + 1 },
|
|
4325
|
+
});
|
|
4326
|
+
return {
|
|
4327
|
+
control: mapOccurrenceControl(
|
|
4328
|
+
this.statements.getOccurrenceControl.get(id),
|
|
4329
|
+
),
|
|
4330
|
+
occurrence: mapOccurrence(this.statements.getOccurrence.get(id)),
|
|
4331
|
+
};
|
|
4332
|
+
});
|
|
4333
|
+
}
|
|
4334
|
+
|
|
4335
|
+
requeueDeadLetter({
|
|
4336
|
+
occurrenceId,
|
|
4337
|
+
expectedFence,
|
|
4338
|
+
expectedErrorCode,
|
|
4339
|
+
requestId,
|
|
4340
|
+
} = {}) {
|
|
4341
|
+
const id = normalizeIdentifier(occurrenceId, "occurrenceId");
|
|
4342
|
+
const fence = assertPositiveSafeInteger(expectedFence, "expectedFence");
|
|
4343
|
+
const errorCode = normalizeIdentifier(
|
|
4344
|
+
expectedErrorCode,
|
|
4345
|
+
"expectedErrorCode",
|
|
4346
|
+
{
|
|
4347
|
+
maxLength: 128,
|
|
4348
|
+
},
|
|
4349
|
+
);
|
|
4350
|
+
const request = normalizeIdentifier(requestId, "requestId", {
|
|
4351
|
+
maxLength: 128,
|
|
4352
|
+
});
|
|
4353
|
+
const now = this._now();
|
|
4354
|
+
return this._write(() => {
|
|
4355
|
+
const existing = this.statements.getOccurrenceRetryByRequest.get(request);
|
|
4356
|
+
if (existing) {
|
|
4357
|
+
if (
|
|
4358
|
+
existing.occurrence_id !== id ||
|
|
4359
|
+
existing.expected_fence !== fence ||
|
|
4360
|
+
existing.expected_error_code !== errorCode
|
|
4361
|
+
) {
|
|
4362
|
+
throw new SchedulerKernelError(
|
|
4363
|
+
"SCHEDULER_REQUEUE_CONFLICT",
|
|
4364
|
+
"Scheduler retry request evidence changed",
|
|
4365
|
+
);
|
|
4366
|
+
}
|
|
4367
|
+
return {
|
|
4368
|
+
occurrence: mapOccurrence(this.statements.getOccurrence.get(id)),
|
|
4369
|
+
requestId: request,
|
|
4370
|
+
deduplicated: true,
|
|
4371
|
+
};
|
|
4372
|
+
}
|
|
4373
|
+
const occurrence = this.statements.getOccurrence.get(id);
|
|
4374
|
+
if (!occurrence) {
|
|
4375
|
+
throw new SchedulerKernelError(
|
|
4376
|
+
"SCHEDULER_NOT_FOUND",
|
|
4377
|
+
`Scheduler occurrence does not exist: ${id}`,
|
|
4378
|
+
);
|
|
4379
|
+
}
|
|
4380
|
+
const lastError =
|
|
4381
|
+
occurrence.last_error_json === null
|
|
4382
|
+
? null
|
|
4383
|
+
: readStoredJson(occurrence.last_error_json, "occurrence error");
|
|
4384
|
+
if (
|
|
4385
|
+
occurrence.status !== OCCURRENCE_STATUS.DEAD_LETTER ||
|
|
4386
|
+
occurrence.fence !== fence ||
|
|
4387
|
+
lastError?.code !== errorCode
|
|
4388
|
+
) {
|
|
4389
|
+
throw new SchedulerKernelError(
|
|
4390
|
+
"SCHEDULER_REQUEUE_EVIDENCE_CONFLICT",
|
|
4391
|
+
`Scheduler dead-letter evidence changed: ${id}`,
|
|
4392
|
+
{
|
|
4393
|
+
status: occurrence.status,
|
|
4394
|
+
expectedFence: fence,
|
|
4395
|
+
actualFence: occurrence.fence,
|
|
4396
|
+
expectedErrorCode: errorCode,
|
|
4397
|
+
actualErrorCode: lastError?.code ?? null,
|
|
4398
|
+
},
|
|
4399
|
+
);
|
|
4400
|
+
}
|
|
4401
|
+
this.db
|
|
4402
|
+
.prepare(
|
|
4403
|
+
`INSERT INTO scheduler_occurrence_retries
|
|
4404
|
+
(occurrence_id, request_id, expected_fence,
|
|
4405
|
+
expected_error_code, created_at, claimed_at)
|
|
4406
|
+
VALUES (?, ?, ?, ?, ?, NULL)`,
|
|
4407
|
+
)
|
|
4408
|
+
.run(id, request, fence, errorCode, now);
|
|
4409
|
+
const update = this.db
|
|
4410
|
+
.prepare(
|
|
4411
|
+
`UPDATE occurrences
|
|
4412
|
+
SET status = 'retry_wait', available_at = ?, updated_at = ?,
|
|
4413
|
+
settled_at = NULL
|
|
4414
|
+
WHERE occurrence_id = ? AND status = 'dead_letter' AND fence = ?`,
|
|
4415
|
+
)
|
|
4416
|
+
.run(now, now, id, fence);
|
|
4417
|
+
if (update.changes !== 1) {
|
|
4418
|
+
throw new SchedulerKernelError(
|
|
4419
|
+
"SCHEDULER_REQUEUE_CONFLICT",
|
|
4420
|
+
`Scheduler dead letter changed during requeue: ${id}`,
|
|
4421
|
+
);
|
|
4422
|
+
}
|
|
4423
|
+
this._appendEvent({
|
|
4424
|
+
jobId: occurrence.job_id,
|
|
4425
|
+
occurrenceId: id,
|
|
4426
|
+
type: "occurrence_requeued",
|
|
4427
|
+
occurredAt: now,
|
|
4428
|
+
fence,
|
|
4429
|
+
data: { requestId: request, expectedErrorCode: errorCode },
|
|
4430
|
+
});
|
|
4431
|
+
return {
|
|
4432
|
+
occurrence: mapOccurrence(this.statements.getOccurrence.get(id)),
|
|
4433
|
+
requestId: request,
|
|
4434
|
+
deduplicated: false,
|
|
4435
|
+
};
|
|
4436
|
+
});
|
|
4437
|
+
}
|
|
4438
|
+
|
|
3471
4439
|
listOccurrencesByTrigger({ jobId, triggerKey, limit = 2 } = {}) {
|
|
3472
4440
|
this._assertOpen();
|
|
3473
4441
|
const id = normalizeIdentifier(jobId, "jobId");
|
|
@@ -3490,6 +4458,11 @@ export class SchedulerStore {
|
|
|
3490
4458
|
WHERE status = 'running'
|
|
3491
4459
|
AND lease_expires_at <= ?
|
|
3492
4460
|
AND attempt >= max_attempts
|
|
4461
|
+
AND NOT EXISTS (
|
|
4462
|
+
SELECT 1 FROM scheduler_occurrence_controls c
|
|
4463
|
+
WHERE c.occurrence_id = occurrences.occurrence_id
|
|
4464
|
+
AND c.state IN ('pause_requested', 'resumed')
|
|
4465
|
+
)
|
|
3493
4466
|
ORDER BY lease_expires_at, occurrence_id
|
|
3494
4467
|
LIMIT 100
|
|
3495
4468
|
`,
|
|
@@ -3509,6 +4482,11 @@ export class SchedulerStore {
|
|
|
3509
4482
|
AND fence = @fence
|
|
3510
4483
|
AND lease_expires_at <= @now
|
|
3511
4484
|
AND attempt >= max_attempts
|
|
4485
|
+
AND NOT EXISTS (
|
|
4486
|
+
SELECT 1 FROM scheduler_occurrence_controls c
|
|
4487
|
+
WHERE c.occurrence_id = occurrences.occurrence_id
|
|
4488
|
+
AND c.state IN ('pause_requested', 'resumed')
|
|
4489
|
+
)
|
|
3512
4490
|
`);
|
|
3513
4491
|
for (const row of expired) {
|
|
3514
4492
|
const error = {
|
|
@@ -3590,26 +4568,43 @@ export class SchedulerStore {
|
|
|
3590
4568
|
workspaceId === undefined
|
|
3591
4569
|
? null
|
|
3592
4570
|
: normalizeIdentifier(workspaceId, "workspaceId", { maxLength: 256 });
|
|
3593
|
-
const now = this._now();
|
|
3594
|
-
const leaseExpiresAt = now + lease;
|
|
3595
|
-
if (!Number.isSafeInteger(leaseExpiresAt)) {
|
|
3596
|
-
throw invalidArgument("lease expiry exceeds the safe integer range");
|
|
3597
|
-
}
|
|
3598
4571
|
return this._write(() => {
|
|
4572
|
+
const now = this._now();
|
|
4573
|
+
const leaseExpiresAt = now + lease;
|
|
4574
|
+
if (!Number.isSafeInteger(leaseExpiresAt)) {
|
|
4575
|
+
throw invalidArgument("lease expiry exceeds the safe integer range");
|
|
4576
|
+
}
|
|
3599
4577
|
this._deadLetterExpiredLeases(now);
|
|
3600
4578
|
const candidate = this.db
|
|
3601
4579
|
.prepare(
|
|
3602
4580
|
`
|
|
3603
|
-
SELECT o
|
|
4581
|
+
SELECT o.*, c.state AS control_state,
|
|
4582
|
+
c.expected_fence AS control_expected_fence,
|
|
4583
|
+
r.request_id AS retry_request_id
|
|
3604
4584
|
FROM occurrences o
|
|
3605
4585
|
JOIN jobs j ON j.job_id = o.job_id
|
|
4586
|
+
LEFT JOIN scheduler_occurrence_controls c
|
|
4587
|
+
ON c.occurrence_id = o.occurrence_id
|
|
4588
|
+
LEFT JOIN scheduler_occurrence_retries r
|
|
4589
|
+
ON r.occurrence_id = o.occurrence_id
|
|
4590
|
+
AND r.claimed_at IS NULL
|
|
4591
|
+
AND r.expected_fence = o.fence
|
|
3606
4592
|
WHERE j.enabled = 1
|
|
3607
4593
|
AND (@jobKind IS NULL OR j.kind = @jobKind)
|
|
3608
4594
|
AND (
|
|
3609
4595
|
@workspaceId IS NULL
|
|
3610
4596
|
OR json_extract(j.authority_json, '$.workspaceId') = @workspaceId
|
|
3611
4597
|
)
|
|
3612
|
-
AND
|
|
4598
|
+
AND (
|
|
4599
|
+
o.attempt < o.max_attempts
|
|
4600
|
+
OR c.state IN ('pause_requested', 'resumed')
|
|
4601
|
+
OR r.request_id IS NOT NULL
|
|
4602
|
+
)
|
|
4603
|
+
AND (c.state IS NULL OR c.state <> 'paused')
|
|
4604
|
+
AND (
|
|
4605
|
+
c.state IS NULL OR c.state <> 'pause_requested'
|
|
4606
|
+
OR o.status = 'running'
|
|
4607
|
+
)
|
|
3613
4608
|
AND (
|
|
3614
4609
|
(o.status IN ('queued', 'retry_wait') AND o.available_at <= @now)
|
|
3615
4610
|
OR
|
|
@@ -3631,7 +4626,25 @@ export class SchedulerStore {
|
|
|
3631
4626
|
`
|
|
3632
4627
|
UPDATE occurrences
|
|
3633
4628
|
SET status = 'running',
|
|
3634
|
-
attempt = attempt +
|
|
4629
|
+
attempt = attempt + CASE
|
|
4630
|
+
WHEN EXISTS (
|
|
4631
|
+
SELECT 1 FROM scheduler_occurrence_controls c
|
|
4632
|
+
WHERE c.occurrence_id = occurrences.occurrence_id
|
|
4633
|
+
AND c.state = 'pause_requested'
|
|
4634
|
+
) THEN 0
|
|
4635
|
+
WHEN EXISTS (
|
|
4636
|
+
SELECT 1 FROM scheduler_occurrence_controls c
|
|
4637
|
+
WHERE c.occurrence_id = occurrences.occurrence_id
|
|
4638
|
+
AND c.state = 'resumed'
|
|
4639
|
+
AND c.expected_fence = occurrences.fence
|
|
4640
|
+
) THEN 0
|
|
4641
|
+
WHEN EXISTS (
|
|
4642
|
+
SELECT 1 FROM scheduler_occurrence_retries r
|
|
4643
|
+
WHERE r.occurrence_id = occurrences.occurrence_id
|
|
4644
|
+
AND r.claimed_at IS NULL
|
|
4645
|
+
AND r.expected_fence = occurrences.fence
|
|
4646
|
+
) THEN 0
|
|
4647
|
+
ELSE 1 END,
|
|
3635
4648
|
fence = fence + 1,
|
|
3636
4649
|
lease_owner = @ownerId,
|
|
3637
4650
|
lease_expires_at = @leaseExpiresAt,
|
|
@@ -3639,7 +4652,25 @@ export class SchedulerStore {
|
|
|
3639
4652
|
settled_at = NULL
|
|
3640
4653
|
WHERE occurrence_id = @occurrenceId
|
|
3641
4654
|
AND fence = @expectedFence
|
|
3642
|
-
AND
|
|
4655
|
+
AND (
|
|
4656
|
+
attempt < max_attempts
|
|
4657
|
+
OR EXISTS (
|
|
4658
|
+
SELECT 1 FROM scheduler_occurrence_controls c
|
|
4659
|
+
WHERE c.occurrence_id = occurrences.occurrence_id
|
|
4660
|
+
AND c.state IN ('pause_requested', 'resumed')
|
|
4661
|
+
)
|
|
4662
|
+
OR EXISTS (
|
|
4663
|
+
SELECT 1 FROM scheduler_occurrence_retries r
|
|
4664
|
+
WHERE r.occurrence_id = occurrences.occurrence_id
|
|
4665
|
+
AND r.claimed_at IS NULL
|
|
4666
|
+
AND r.expected_fence = occurrences.fence
|
|
4667
|
+
)
|
|
4668
|
+
)
|
|
4669
|
+
AND NOT EXISTS (
|
|
4670
|
+
SELECT 1 FROM scheduler_occurrence_controls c
|
|
4671
|
+
WHERE c.occurrence_id = occurrences.occurrence_id
|
|
4672
|
+
AND c.state = 'paused'
|
|
4673
|
+
)
|
|
3643
4674
|
AND (
|
|
3644
4675
|
(status IN ('queued', 'retry_wait') AND available_at <= @now)
|
|
3645
4676
|
OR
|
|
@@ -3663,6 +4694,49 @@ export class SchedulerStore {
|
|
|
3663
4694
|
const claimed = this.statements.getOccurrence.get(
|
|
3664
4695
|
candidate.occurrence_id,
|
|
3665
4696
|
);
|
|
4697
|
+
if (candidate.control_state === "pause_requested") {
|
|
4698
|
+
this.db
|
|
4699
|
+
.prepare(
|
|
4700
|
+
`UPDATE scheduler_occurrence_controls
|
|
4701
|
+
SET expected_fence = ?, revision = revision + 1, updated_at = ?
|
|
4702
|
+
WHERE occurrence_id = ? AND state = 'pause_requested'
|
|
4703
|
+
AND expected_fence = ?`,
|
|
4704
|
+
)
|
|
4705
|
+
.run(
|
|
4706
|
+
claimed.fence,
|
|
4707
|
+
now,
|
|
4708
|
+
candidate.occurrence_id,
|
|
4709
|
+
candidate.control_expected_fence,
|
|
4710
|
+
);
|
|
4711
|
+
} else if (candidate.control_state === "resumed") {
|
|
4712
|
+
const rebound = this.db
|
|
4713
|
+
.prepare(
|
|
4714
|
+
`UPDATE scheduler_occurrence_controls
|
|
4715
|
+
SET expected_fence = ?, revision = revision + 1, updated_at = ?
|
|
4716
|
+
WHERE occurrence_id = ? AND state = 'resumed'
|
|
4717
|
+
AND expected_fence = ?`,
|
|
4718
|
+
)
|
|
4719
|
+
.run(
|
|
4720
|
+
claimed.fence,
|
|
4721
|
+
now,
|
|
4722
|
+
candidate.occurrence_id,
|
|
4723
|
+
candidate.control_expected_fence,
|
|
4724
|
+
);
|
|
4725
|
+
if (rebound.changes !== 1) {
|
|
4726
|
+
throw new SchedulerKernelError(
|
|
4727
|
+
"SCHEDULER_OCCURRENCE_CONTROL_CONFLICT",
|
|
4728
|
+
`Scheduler resume checkpoint lost its claim fence: ${candidate.occurrence_id}`,
|
|
4729
|
+
);
|
|
4730
|
+
}
|
|
4731
|
+
}
|
|
4732
|
+
if (candidate.retry_request_id !== null) {
|
|
4733
|
+
this.db
|
|
4734
|
+
.prepare(
|
|
4735
|
+
`UPDATE scheduler_occurrence_retries SET claimed_at = ?
|
|
4736
|
+
WHERE occurrence_id = ? AND request_id = ? AND claimed_at IS NULL`,
|
|
4737
|
+
)
|
|
4738
|
+
.run(now, candidate.occurrence_id, candidate.retry_request_id);
|
|
4739
|
+
}
|
|
3666
4740
|
this._appendEvent({
|
|
3667
4741
|
jobId: candidate.job_id,
|
|
3668
4742
|
occurrenceId: candidate.occurrence_id,
|
|
@@ -3677,6 +4751,15 @@ export class SchedulerStore {
|
|
|
3677
4751
|
attempt: claimed.attempt,
|
|
3678
4752
|
previousOwner,
|
|
3679
4753
|
previousStatus,
|
|
4754
|
+
...(candidate.control_state === "pause_requested"
|
|
4755
|
+
? { pauseRequestRebound: true }
|
|
4756
|
+
: {}),
|
|
4757
|
+
...(candidate.control_state === "resumed"
|
|
4758
|
+
? { resumedWithoutAttempt: true }
|
|
4759
|
+
: {}),
|
|
4760
|
+
...(candidate.retry_request_id === null
|
|
4761
|
+
? {}
|
|
4762
|
+
: { retryRequestId: candidate.retry_request_id }),
|
|
3680
4763
|
},
|
|
3681
4764
|
});
|
|
3682
4765
|
return mapOccurrence(claimed);
|
|
@@ -3696,12 +4779,12 @@ export class SchedulerStore {
|
|
|
3696
4779
|
const id = normalizeIdentifier(occurrenceId, "occurrenceId");
|
|
3697
4780
|
const owner = normalizeIdentifier(ownerId, "ownerId");
|
|
3698
4781
|
const lease = normalizeLeaseMs(leaseMs);
|
|
3699
|
-
const now = this._now();
|
|
3700
|
-
const leaseExpiresAt = now + lease;
|
|
3701
|
-
if (!Number.isSafeInteger(leaseExpiresAt)) {
|
|
3702
|
-
throw invalidArgument("lease expiry exceeds the safe integer range");
|
|
3703
|
-
}
|
|
3704
4782
|
return this._write(() => {
|
|
4783
|
+
const now = this._now();
|
|
4784
|
+
const leaseExpiresAt = now + lease;
|
|
4785
|
+
if (!Number.isSafeInteger(leaseExpiresAt)) {
|
|
4786
|
+
throw invalidArgument("lease expiry exceeds the safe integer range");
|
|
4787
|
+
}
|
|
3705
4788
|
this._deadLetterExpiredLeases(now);
|
|
3706
4789
|
const candidate = this.statements.getOccurrence.get(id);
|
|
3707
4790
|
if (!candidate) {
|
|
@@ -3711,9 +4794,24 @@ export class SchedulerStore {
|
|
|
3711
4794
|
);
|
|
3712
4795
|
}
|
|
3713
4796
|
const job = this.statements.getJob.get(candidate.job_id);
|
|
4797
|
+
const controlRow = this.statements.getOccurrenceControl.get(id);
|
|
4798
|
+
const retryRow = this.db
|
|
4799
|
+
.prepare(
|
|
4800
|
+
`SELECT * FROM scheduler_occurrence_retries
|
|
4801
|
+
WHERE occurrence_id = ? AND claimed_at IS NULL
|
|
4802
|
+
AND expected_fence = ? ORDER BY created_at, request_id LIMIT 1`,
|
|
4803
|
+
)
|
|
4804
|
+
.get(id, candidate.fence);
|
|
3714
4805
|
const eligible =
|
|
3715
4806
|
job?.enabled === 1 &&
|
|
3716
|
-
candidate.attempt < candidate.max_attempts
|
|
4807
|
+
(candidate.attempt < candidate.max_attempts ||
|
|
4808
|
+
["pause_requested", "resumed"].includes(controlRow?.state) ||
|
|
4809
|
+
retryRow !== undefined) &&
|
|
4810
|
+
controlRow?.state !== "paused" &&
|
|
4811
|
+
!(
|
|
4812
|
+
controlRow?.state === "pause_requested" &&
|
|
4813
|
+
candidate.status !== OCCURRENCE_STATUS.RUNNING
|
|
4814
|
+
) &&
|
|
3717
4815
|
(([OCCURRENCE_STATUS.QUEUED, OCCURRENCE_STATUS.RETRY_WAIT].includes(
|
|
3718
4816
|
candidate.status,
|
|
3719
4817
|
) &&
|
|
@@ -3729,7 +4827,25 @@ export class SchedulerStore {
|
|
|
3729
4827
|
`
|
|
3730
4828
|
UPDATE occurrences
|
|
3731
4829
|
SET status = 'running',
|
|
3732
|
-
attempt = attempt +
|
|
4830
|
+
attempt = attempt + CASE
|
|
4831
|
+
WHEN EXISTS (
|
|
4832
|
+
SELECT 1 FROM scheduler_occurrence_controls c
|
|
4833
|
+
WHERE c.occurrence_id = occurrences.occurrence_id
|
|
4834
|
+
AND c.state = 'pause_requested'
|
|
4835
|
+
) THEN 0
|
|
4836
|
+
WHEN EXISTS (
|
|
4837
|
+
SELECT 1 FROM scheduler_occurrence_controls c
|
|
4838
|
+
WHERE c.occurrence_id = occurrences.occurrence_id
|
|
4839
|
+
AND c.state = 'resumed'
|
|
4840
|
+
AND c.expected_fence = occurrences.fence
|
|
4841
|
+
) THEN 0
|
|
4842
|
+
WHEN EXISTS (
|
|
4843
|
+
SELECT 1 FROM scheduler_occurrence_retries r
|
|
4844
|
+
WHERE r.occurrence_id = occurrences.occurrence_id
|
|
4845
|
+
AND r.claimed_at IS NULL
|
|
4846
|
+
AND r.expected_fence = occurrences.fence
|
|
4847
|
+
) THEN 0
|
|
4848
|
+
ELSE 1 END,
|
|
3733
4849
|
fence = fence + 1,
|
|
3734
4850
|
lease_owner = @ownerId,
|
|
3735
4851
|
lease_expires_at = @leaseExpiresAt,
|
|
@@ -3737,7 +4853,25 @@ export class SchedulerStore {
|
|
|
3737
4853
|
settled_at = NULL
|
|
3738
4854
|
WHERE occurrence_id = @occurrenceId
|
|
3739
4855
|
AND fence = @expectedFence
|
|
3740
|
-
AND
|
|
4856
|
+
AND (
|
|
4857
|
+
attempt < max_attempts
|
|
4858
|
+
OR EXISTS (
|
|
4859
|
+
SELECT 1 FROM scheduler_occurrence_controls c
|
|
4860
|
+
WHERE c.occurrence_id = occurrences.occurrence_id
|
|
4861
|
+
AND c.state IN ('pause_requested', 'resumed')
|
|
4862
|
+
)
|
|
4863
|
+
OR EXISTS (
|
|
4864
|
+
SELECT 1 FROM scheduler_occurrence_retries r
|
|
4865
|
+
WHERE r.occurrence_id = occurrences.occurrence_id
|
|
4866
|
+
AND r.claimed_at IS NULL
|
|
4867
|
+
AND r.expected_fence = occurrences.fence
|
|
4868
|
+
)
|
|
4869
|
+
)
|
|
4870
|
+
AND NOT EXISTS (
|
|
4871
|
+
SELECT 1 FROM scheduler_occurrence_controls c
|
|
4872
|
+
WHERE c.occurrence_id = occurrences.occurrence_id
|
|
4873
|
+
AND c.state = 'paused'
|
|
4874
|
+
)
|
|
3741
4875
|
AND (
|
|
3742
4876
|
(status IN ('queued', 'retry_wait') AND available_at <= @now)
|
|
3743
4877
|
OR
|
|
@@ -3759,6 +4893,39 @@ export class SchedulerStore {
|
|
|
3759
4893
|
);
|
|
3760
4894
|
}
|
|
3761
4895
|
const claimed = this.statements.getOccurrence.get(id);
|
|
4896
|
+
if (controlRow?.state === "pause_requested") {
|
|
4897
|
+
this.db
|
|
4898
|
+
.prepare(
|
|
4899
|
+
`UPDATE scheduler_occurrence_controls
|
|
4900
|
+
SET expected_fence = ?, revision = revision + 1, updated_at = ?
|
|
4901
|
+
WHERE occurrence_id = ? AND state = 'pause_requested'
|
|
4902
|
+
AND expected_fence = ?`,
|
|
4903
|
+
)
|
|
4904
|
+
.run(claimed.fence, now, id, controlRow.expected_fence);
|
|
4905
|
+
} else if (controlRow?.state === "resumed") {
|
|
4906
|
+
const rebound = this.db
|
|
4907
|
+
.prepare(
|
|
4908
|
+
`UPDATE scheduler_occurrence_controls
|
|
4909
|
+
SET expected_fence = ?, revision = revision + 1, updated_at = ?
|
|
4910
|
+
WHERE occurrence_id = ? AND state = 'resumed'
|
|
4911
|
+
AND expected_fence = ?`,
|
|
4912
|
+
)
|
|
4913
|
+
.run(claimed.fence, now, id, controlRow.expected_fence);
|
|
4914
|
+
if (rebound.changes !== 1) {
|
|
4915
|
+
throw new SchedulerKernelError(
|
|
4916
|
+
"SCHEDULER_OCCURRENCE_CONTROL_CONFLICT",
|
|
4917
|
+
`Scheduler resume checkpoint lost its claim fence: ${id}`,
|
|
4918
|
+
);
|
|
4919
|
+
}
|
|
4920
|
+
}
|
|
4921
|
+
if (retryRow) {
|
|
4922
|
+
this.db
|
|
4923
|
+
.prepare(
|
|
4924
|
+
`UPDATE scheduler_occurrence_retries SET claimed_at = ?
|
|
4925
|
+
WHERE occurrence_id = ? AND request_id = ? AND claimed_at IS NULL`,
|
|
4926
|
+
)
|
|
4927
|
+
.run(now, id, retryRow.request_id);
|
|
4928
|
+
}
|
|
3762
4929
|
this._appendEvent({
|
|
3763
4930
|
jobId: candidate.job_id,
|
|
3764
4931
|
occurrenceId: id,
|
|
@@ -3774,6 +4941,13 @@ export class SchedulerStore {
|
|
|
3774
4941
|
previousOwner,
|
|
3775
4942
|
previousStatus,
|
|
3776
4943
|
targeted: true,
|
|
4944
|
+
...(controlRow?.state === "pause_requested"
|
|
4945
|
+
? { pauseRequestRebound: true }
|
|
4946
|
+
: {}),
|
|
4947
|
+
...(controlRow?.state === "resumed"
|
|
4948
|
+
? { resumedWithoutAttempt: true }
|
|
4949
|
+
: {}),
|
|
4950
|
+
...(retryRow ? { retryRequestId: retryRow.request_id } : {}),
|
|
3777
4951
|
},
|
|
3778
4952
|
});
|
|
3779
4953
|
return mapOccurrence(claimed);
|
|
@@ -3785,12 +4959,12 @@ export class SchedulerStore {
|
|
|
3785
4959
|
const owner = normalizeIdentifier(ownerId, "ownerId");
|
|
3786
4960
|
const token = assertPositiveSafeInteger(fence, "fence");
|
|
3787
4961
|
const lease = normalizeLeaseMs(leaseMs);
|
|
3788
|
-
const now = this._now();
|
|
3789
|
-
const leaseExpiresAt = now + lease;
|
|
3790
|
-
if (!Number.isSafeInteger(leaseExpiresAt)) {
|
|
3791
|
-
throw invalidArgument("lease expiry exceeds the safe integer range");
|
|
3792
|
-
}
|
|
3793
4962
|
return this._write(() => {
|
|
4963
|
+
const now = this._now();
|
|
4964
|
+
const leaseExpiresAt = now + lease;
|
|
4965
|
+
if (!Number.isSafeInteger(leaseExpiresAt)) {
|
|
4966
|
+
throw invalidArgument("lease expiry exceeds the safe integer range");
|
|
4967
|
+
}
|
|
3794
4968
|
const result = this.db
|
|
3795
4969
|
.prepare(
|
|
3796
4970
|
`
|
|
@@ -3858,11 +5032,10 @@ export class SchedulerStore {
|
|
|
3858
5032
|
"error",
|
|
3859
5033
|
)
|
|
3860
5034
|
: null;
|
|
3861
|
-
const
|
|
3862
|
-
const availableAt =
|
|
5035
|
+
const normalizedRetryAt =
|
|
3863
5036
|
outcome !== "failed" || retryAt === undefined
|
|
3864
|
-
?
|
|
3865
|
-
:
|
|
5037
|
+
? null
|
|
5038
|
+
: normalizeEpochMs(retryAt, "retryAt");
|
|
3866
5039
|
const adjudicationRequest =
|
|
3867
5040
|
adjudicationRequestId === undefined
|
|
3868
5041
|
? null
|
|
@@ -3871,6 +5044,9 @@ export class SchedulerStore {
|
|
|
3871
5044
|
});
|
|
3872
5045
|
|
|
3873
5046
|
return this._write(() => {
|
|
5047
|
+
const now = this._now();
|
|
5048
|
+
const availableAt =
|
|
5049
|
+
normalizedRetryAt === null ? now : Math.max(now, normalizedRetryAt);
|
|
3874
5050
|
const current = this.statements.getOccurrence.get(id);
|
|
3875
5051
|
if (
|
|
3876
5052
|
!current ||
|
|
@@ -3936,6 +5112,35 @@ export class SchedulerStore {
|
|
|
3936
5112
|
settledAt,
|
|
3937
5113
|
});
|
|
3938
5114
|
if (update.changes !== 1) throw leaseLost(id);
|
|
5115
|
+
if (nextStatus === OCCURRENCE_STATUS.RETRY_WAIT) {
|
|
5116
|
+
this.db
|
|
5117
|
+
.prepare(
|
|
5118
|
+
`UPDATE scheduler_occurrence_controls
|
|
5119
|
+
SET expected_fence = @nextFence, revision = revision + 1,
|
|
5120
|
+
updated_at = @now
|
|
5121
|
+
WHERE occurrence_id = @occurrenceId
|
|
5122
|
+
AND state = 'resumed'
|
|
5123
|
+
AND expected_fence = @fence`,
|
|
5124
|
+
)
|
|
5125
|
+
.run({
|
|
5126
|
+
occurrenceId: id,
|
|
5127
|
+
fence: token,
|
|
5128
|
+
nextFence: token + 1,
|
|
5129
|
+
now,
|
|
5130
|
+
});
|
|
5131
|
+
}
|
|
5132
|
+
const controlTerminalized = this.db
|
|
5133
|
+
.prepare(
|
|
5134
|
+
`UPDATE scheduler_occurrence_controls
|
|
5135
|
+
SET state = 'terminal', revision = revision + 1,
|
|
5136
|
+
terminal_at = @now, updated_at = @now
|
|
5137
|
+
WHERE occurrence_id = @occurrenceId
|
|
5138
|
+
AND (
|
|
5139
|
+
state = 'pause_requested'
|
|
5140
|
+
OR (state = 'resumed' AND @status <> 'retry_wait')
|
|
5141
|
+
)`,
|
|
5142
|
+
)
|
|
5143
|
+
.run({ occurrenceId: id, status: nextStatus, now }).changes;
|
|
3939
5144
|
const reservation = this.db
|
|
3940
5145
|
.prepare(
|
|
3941
5146
|
`SELECT * FROM scheduler_authority_reservations
|
|
@@ -4039,6 +5244,9 @@ export class SchedulerStore {
|
|
|
4039
5244
|
...(adjudication?.status === "pending"
|
|
4040
5245
|
? { adjudicationRequestId: adjudication.request_id }
|
|
4041
5246
|
: {}),
|
|
5247
|
+
...(controlTerminalized === 1
|
|
5248
|
+
? { runtimeControlTerminalized: true }
|
|
5249
|
+
: {}),
|
|
4042
5250
|
},
|
|
4043
5251
|
});
|
|
4044
5252
|
return mapOccurrence(this.statements.getOccurrence.get(id));
|