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
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import crypto from "crypto";
|
|
14
|
+
import { nextCronTime } from "./agent-schedule-store.js";
|
|
14
15
|
|
|
15
16
|
/* ── Constants ─────────────────────────────────────────────── */
|
|
16
17
|
|
|
@@ -42,6 +43,7 @@ export const NODE_TYPE = Object.freeze({
|
|
|
42
43
|
PARALLEL: "parallel",
|
|
43
44
|
LOOP: "loop",
|
|
44
45
|
});
|
|
46
|
+
export const AUTOMATION_SCHEDULER_MIGRATION_SCHEMA_VERSION = 1;
|
|
45
47
|
|
|
46
48
|
/* ── Built-in connector catalog ────────────────────────────── */
|
|
47
49
|
|
|
@@ -309,6 +311,33 @@ export function ensureAutomationTables(db) {
|
|
|
309
311
|
created_at TEXT DEFAULT (datetime('now'))
|
|
310
312
|
)
|
|
311
313
|
`);
|
|
314
|
+
db.exec(`
|
|
315
|
+
CREATE TABLE IF NOT EXISTS auto_scheduler_migrations (
|
|
316
|
+
flow_id TEXT PRIMARY KEY NOT NULL,
|
|
317
|
+
schema_version INTEGER NOT NULL CHECK (schema_version = 1),
|
|
318
|
+
state TEXT NOT NULL CHECK (state IN ('prepared', 'retired')),
|
|
319
|
+
migration_id TEXT NOT NULL UNIQUE,
|
|
320
|
+
source_digest TEXT NOT NULL,
|
|
321
|
+
target_job_id TEXT NOT NULL UNIQUE,
|
|
322
|
+
retirement_token TEXT UNIQUE,
|
|
323
|
+
prior_status TEXT NOT NULL CHECK (prior_status = 'active'),
|
|
324
|
+
prior_updated_at TEXT NOT NULL,
|
|
325
|
+
source_snapshot TEXT,
|
|
326
|
+
prepared_at TEXT NOT NULL,
|
|
327
|
+
retired_at TEXT
|
|
328
|
+
)
|
|
329
|
+
`);
|
|
330
|
+
const migrationColumns = new Set(
|
|
331
|
+
db
|
|
332
|
+
.prepare("PRAGMA table_info(auto_scheduler_migrations)")
|
|
333
|
+
.all()
|
|
334
|
+
.map((column) => column.name),
|
|
335
|
+
);
|
|
336
|
+
if (!migrationColumns.has("source_snapshot")) {
|
|
337
|
+
db.exec(
|
|
338
|
+
"ALTER TABLE auto_scheduler_migrations ADD COLUMN source_snapshot TEXT",
|
|
339
|
+
);
|
|
340
|
+
}
|
|
312
341
|
}
|
|
313
342
|
|
|
314
343
|
/* ── Helpers ───────────────────────────────────────────────── */
|
|
@@ -321,6 +350,89 @@ function _now() {
|
|
|
321
350
|
return new Date().toISOString();
|
|
322
351
|
}
|
|
323
352
|
|
|
353
|
+
function _stableJsonValue(value) {
|
|
354
|
+
if (Array.isArray(value)) return value.map(_stableJsonValue);
|
|
355
|
+
if (value && typeof value === "object") {
|
|
356
|
+
return Object.fromEntries(
|
|
357
|
+
Object.keys(value)
|
|
358
|
+
.sort()
|
|
359
|
+
.map((key) => [key, _stableJsonValue(value[key])]),
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
return value;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function _automationMigrationField(value, field) {
|
|
366
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
367
|
+
const error = new Error(
|
|
368
|
+
`automation scheduler migration ${field} is required`,
|
|
369
|
+
);
|
|
370
|
+
error.code = "AUTOMATION_SCHEDULER_MIGRATION_INVALID";
|
|
371
|
+
throw error;
|
|
372
|
+
}
|
|
373
|
+
return value.trim();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function _automationTransaction(db, callback) {
|
|
377
|
+
if (typeof db.transaction !== "function") return callback();
|
|
378
|
+
const transaction = db.transaction(callback);
|
|
379
|
+
return typeof transaction.immediate === "function"
|
|
380
|
+
? transaction.immediate()
|
|
381
|
+
: transaction();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function _rowToSchedulerMigration(row) {
|
|
385
|
+
if (!row) return null;
|
|
386
|
+
return {
|
|
387
|
+
flowId: row.flow_id,
|
|
388
|
+
schemaVersion: row.schema_version,
|
|
389
|
+
state: row.state,
|
|
390
|
+
migrationId: row.migration_id,
|
|
391
|
+
sourceDigest: row.source_digest,
|
|
392
|
+
targetJobId: row.target_job_id,
|
|
393
|
+
retirementToken: row.retirement_token || null,
|
|
394
|
+
priorStatus: row.prior_status,
|
|
395
|
+
priorUpdatedAt: row.prior_updated_at,
|
|
396
|
+
sourceSnapshot: _parseJSON(row.source_snapshot, null),
|
|
397
|
+
preparedAt: row.prepared_at,
|
|
398
|
+
retiredAt: row.retired_at || null,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function _automationMigrationOptionalField(value, field) {
|
|
403
|
+
if (value == null) return null;
|
|
404
|
+
return _automationMigrationField(value, field);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function _automationMigrationSourceDefinition(migration) {
|
|
408
|
+
const source = migration?.sourceSnapshot;
|
|
409
|
+
if (
|
|
410
|
+
!source ||
|
|
411
|
+
typeof source !== "object" ||
|
|
412
|
+
source.id !== migration.flowId ||
|
|
413
|
+
source.status !== migration.priorStatus ||
|
|
414
|
+
typeof source.schedule !== "string" ||
|
|
415
|
+
source.schedule.length === 0
|
|
416
|
+
) {
|
|
417
|
+
const error = new Error(
|
|
418
|
+
`Automation migration source definition is invalid: ${migration?.flowId || "unknown"}`,
|
|
419
|
+
);
|
|
420
|
+
error.code = "AUTOMATION_SCHEDULER_MIGRATION_CONFLICT";
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
423
|
+
return source;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function _assertAutomationSchedulerMutationSafe(db, flowId) {
|
|
427
|
+
const migration = getAutomationSchedulerMigration(db, flowId);
|
|
428
|
+
if (!migration) return;
|
|
429
|
+
const error = new Error(
|
|
430
|
+
`Automation flow scheduler authority is owned by migration ${migration.migrationId}: ${flowId}`,
|
|
431
|
+
);
|
|
432
|
+
error.code = "AUTOMATION_SCHEDULER_MIGRATION_ACTIVE";
|
|
433
|
+
throw error;
|
|
434
|
+
}
|
|
435
|
+
|
|
324
436
|
function _parseJSON(value, fallback) {
|
|
325
437
|
if (value == null) return fallback;
|
|
326
438
|
if (typeof value !== "string") return value;
|
|
@@ -468,6 +580,292 @@ export function getFlow(db, flowId) {
|
|
|
468
580
|
);
|
|
469
581
|
}
|
|
470
582
|
|
|
583
|
+
export function automationMigrationSourceSnapshot(flow) {
|
|
584
|
+
const source = { ...flow };
|
|
585
|
+
delete source.updatedAt;
|
|
586
|
+
delete source.schedulerMigration;
|
|
587
|
+
return _stableJsonValue(source);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
export function automationMigrationSourceDigest(flow) {
|
|
591
|
+
return crypto
|
|
592
|
+
.createHash("sha256")
|
|
593
|
+
.update("chainlesschain.automation.scheduler-migration-source.v1\0", "utf8")
|
|
594
|
+
.update(JSON.stringify(automationMigrationSourceSnapshot(flow)), "utf8")
|
|
595
|
+
.digest("hex");
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export function getAutomationSchedulerMigration(db, flowId) {
|
|
599
|
+
return _rowToSchedulerMigration(
|
|
600
|
+
db
|
|
601
|
+
.prepare("SELECT * FROM auto_scheduler_migrations WHERE flow_id = ?")
|
|
602
|
+
.get(flowId),
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
export function automationEffectiveSchedulerFlow(db, flow) {
|
|
607
|
+
if (!flow) return flow;
|
|
608
|
+
const migration = getAutomationSchedulerMigration(db, flow.id);
|
|
609
|
+
if (
|
|
610
|
+
migration?.schemaVersion ===
|
|
611
|
+
AUTOMATION_SCHEDULER_MIGRATION_SCHEMA_VERSION &&
|
|
612
|
+
migration.state === "retired" &&
|
|
613
|
+
migration.priorStatus === FLOW_STATUS.ACTIVE &&
|
|
614
|
+
flow.schedule === null &&
|
|
615
|
+
[FLOW_STATUS.PAUSED, FLOW_STATUS.ACTIVE].includes(flow.status)
|
|
616
|
+
) {
|
|
617
|
+
const source = _automationMigrationSourceDefinition(migration);
|
|
618
|
+
return {
|
|
619
|
+
...flow,
|
|
620
|
+
status: source.status,
|
|
621
|
+
schedule: source.schedule,
|
|
622
|
+
updatedAt: migration.priorUpdatedAt,
|
|
623
|
+
schedulerMigration: migration,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
return flow;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
export function listAutomationSchedulerFlows(db, { limit = 10_000 } = {}) {
|
|
630
|
+
return db
|
|
631
|
+
.prepare(
|
|
632
|
+
`SELECT f.* FROM auto_flows f
|
|
633
|
+
JOIN auto_scheduler_migrations m ON m.flow_id = f.id
|
|
634
|
+
WHERE m.state = 'retired' AND f.schedule IS NULL
|
|
635
|
+
AND f.status IN ('paused', 'active')
|
|
636
|
+
ORDER BY f.created_at DESC LIMIT ?`,
|
|
637
|
+
)
|
|
638
|
+
.all(limit)
|
|
639
|
+
.map(_rowToFlow)
|
|
640
|
+
.map((flow) => automationEffectiveSchedulerFlow(db, flow));
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
export function prepareAutomationSchedulerMigration(
|
|
644
|
+
db,
|
|
645
|
+
flowId,
|
|
646
|
+
{ migrationId, sourceDigest, targetJobId } = {},
|
|
647
|
+
) {
|
|
648
|
+
const migration = _automationMigrationField(migrationId, "migrationId");
|
|
649
|
+
const digest = _automationMigrationField(sourceDigest, "sourceDigest");
|
|
650
|
+
const target = _automationMigrationField(targetJobId, "targetJobId");
|
|
651
|
+
return _automationTransaction(db, () => {
|
|
652
|
+
const flow = getFlow(db, flowId);
|
|
653
|
+
if (!flow) throw new Error(`Flow not found: ${flowId}`);
|
|
654
|
+
if (flow.status !== FLOW_STATUS.ACTIVE) {
|
|
655
|
+
const error = new Error(`Automation flow is not active: ${flowId}`);
|
|
656
|
+
error.code = "AUTOMATION_SCHEDULER_FLOW_NOT_ACTIVE";
|
|
657
|
+
throw error;
|
|
658
|
+
}
|
|
659
|
+
if (automationMigrationSourceDigest(flow) !== digest) {
|
|
660
|
+
const error = new Error(
|
|
661
|
+
`Automation flow changed before migration: ${flowId}`,
|
|
662
|
+
);
|
|
663
|
+
error.code = "AUTOMATION_SCHEDULER_MIGRATION_SOURCE_CHANGED";
|
|
664
|
+
throw error;
|
|
665
|
+
}
|
|
666
|
+
const prior = getAutomationSchedulerMigration(db, flowId);
|
|
667
|
+
if (prior) {
|
|
668
|
+
if (
|
|
669
|
+
prior.migrationId !== migration ||
|
|
670
|
+
prior.sourceDigest !== digest ||
|
|
671
|
+
prior.targetJobId !== target ||
|
|
672
|
+
prior.state !== "prepared"
|
|
673
|
+
) {
|
|
674
|
+
const error = new Error(
|
|
675
|
+
`Automation flow already belongs to another migration: ${flowId}`,
|
|
676
|
+
);
|
|
677
|
+
error.code = "AUTOMATION_SCHEDULER_MIGRATION_CONFLICT";
|
|
678
|
+
throw error;
|
|
679
|
+
}
|
|
680
|
+
return prior;
|
|
681
|
+
}
|
|
682
|
+
const now = _now();
|
|
683
|
+
db.prepare(
|
|
684
|
+
`INSERT INTO auto_scheduler_migrations
|
|
685
|
+
(flow_id, schema_version, state, migration_id, source_digest,
|
|
686
|
+
target_job_id, retirement_token, prior_status, prior_updated_at,
|
|
687
|
+
source_snapshot, prepared_at, retired_at)
|
|
688
|
+
VALUES (?, 1, 'prepared', ?, ?, ?, NULL, 'active', ?, ?, ?, NULL)`,
|
|
689
|
+
).run(
|
|
690
|
+
flowId,
|
|
691
|
+
migration,
|
|
692
|
+
digest,
|
|
693
|
+
target,
|
|
694
|
+
flow.updatedAt,
|
|
695
|
+
JSON.stringify(automationMigrationSourceSnapshot(flow)),
|
|
696
|
+
now,
|
|
697
|
+
);
|
|
698
|
+
return getAutomationSchedulerMigration(db, flowId);
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
export function retireAutomationSchedulerMigration(
|
|
703
|
+
db,
|
|
704
|
+
flowId,
|
|
705
|
+
{ migrationId, sourceDigest, targetJobId, retirementToken } = {},
|
|
706
|
+
) {
|
|
707
|
+
const migration = _automationMigrationField(migrationId, "migrationId");
|
|
708
|
+
const digest = _automationMigrationField(sourceDigest, "sourceDigest");
|
|
709
|
+
const target = _automationMigrationField(targetJobId, "targetJobId");
|
|
710
|
+
const token = _automationMigrationField(retirementToken, "retirementToken");
|
|
711
|
+
return _automationTransaction(db, () => {
|
|
712
|
+
const marker = getAutomationSchedulerMigration(db, flowId);
|
|
713
|
+
const flow = getFlow(db, flowId);
|
|
714
|
+
if (
|
|
715
|
+
!marker ||
|
|
716
|
+
marker.migrationId !== migration ||
|
|
717
|
+
marker.sourceDigest !== digest ||
|
|
718
|
+
marker.targetJobId !== target ||
|
|
719
|
+
!["prepared", "retired"].includes(marker.state) ||
|
|
720
|
+
!flow
|
|
721
|
+
) {
|
|
722
|
+
const error = new Error(
|
|
723
|
+
`Automation migration changed before retirement: ${flowId}`,
|
|
724
|
+
);
|
|
725
|
+
error.code = "AUTOMATION_SCHEDULER_MIGRATION_CONFLICT";
|
|
726
|
+
throw error;
|
|
727
|
+
}
|
|
728
|
+
if (marker.state === "retired") {
|
|
729
|
+
if (
|
|
730
|
+
marker.retirementToken !== token ||
|
|
731
|
+
flow.schedule !== null ||
|
|
732
|
+
![FLOW_STATUS.PAUSED, FLOW_STATUS.ACTIVE].includes(flow.status)
|
|
733
|
+
) {
|
|
734
|
+
const error = new Error(
|
|
735
|
+
`Automation migration fence changed: ${flowId}`,
|
|
736
|
+
);
|
|
737
|
+
error.code = "AUTOMATION_SCHEDULER_MIGRATION_CONFLICT";
|
|
738
|
+
throw error;
|
|
739
|
+
}
|
|
740
|
+
if (flow.status === FLOW_STATUS.ACTIVE) {
|
|
741
|
+
db.prepare(
|
|
742
|
+
`UPDATE auto_flows SET status = 'paused' WHERE id = ? AND schedule IS NULL`,
|
|
743
|
+
).run(flowId);
|
|
744
|
+
}
|
|
745
|
+
return automationEffectiveSchedulerFlow(db, flow);
|
|
746
|
+
}
|
|
747
|
+
if (automationMigrationSourceDigest(flow) !== digest) {
|
|
748
|
+
const error = new Error(
|
|
749
|
+
`Automation flow changed before retirement: ${flowId}`,
|
|
750
|
+
);
|
|
751
|
+
error.code = "AUTOMATION_SCHEDULER_MIGRATION_SOURCE_CHANGED";
|
|
752
|
+
throw error;
|
|
753
|
+
}
|
|
754
|
+
const now = _now();
|
|
755
|
+
db.prepare(
|
|
756
|
+
`UPDATE auto_flows SET status = 'paused', schedule = NULL, updated_at = ?
|
|
757
|
+
WHERE id = ? AND status = 'active' AND updated_at = ?`,
|
|
758
|
+
).run(now, flowId, marker.priorUpdatedAt);
|
|
759
|
+
const fenced = getFlow(db, flowId);
|
|
760
|
+
if (fenced?.status !== FLOW_STATUS.PAUSED || fenced.schedule !== null) {
|
|
761
|
+
const error = new Error(
|
|
762
|
+
`Automation flow changed during retirement: ${flowId}`,
|
|
763
|
+
);
|
|
764
|
+
error.code = "AUTOMATION_SCHEDULER_MIGRATION_CONFLICT";
|
|
765
|
+
throw error;
|
|
766
|
+
}
|
|
767
|
+
const changed = db
|
|
768
|
+
.prepare(
|
|
769
|
+
`UPDATE auto_scheduler_migrations
|
|
770
|
+
SET state = 'retired', retirement_token = ?, retired_at = ?
|
|
771
|
+
WHERE flow_id = ? AND state = 'prepared'`,
|
|
772
|
+
)
|
|
773
|
+
.run(token, now, flowId);
|
|
774
|
+
if (changed.changes !== 1) {
|
|
775
|
+
const error = new Error(
|
|
776
|
+
`Automation migration changed during retirement: ${flowId}`,
|
|
777
|
+
);
|
|
778
|
+
error.code = "AUTOMATION_SCHEDULER_MIGRATION_CONFLICT";
|
|
779
|
+
throw error;
|
|
780
|
+
}
|
|
781
|
+
return automationEffectiveSchedulerFlow(db, getFlow(db, flowId));
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
export function restoreAutomationSchedulerMigration(
|
|
786
|
+
db,
|
|
787
|
+
flowId,
|
|
788
|
+
{ migrationId, sourceDigest, targetJobId, retirementToken } = {},
|
|
789
|
+
) {
|
|
790
|
+
const migration = _automationMigrationField(migrationId, "migrationId");
|
|
791
|
+
const digest = _automationMigrationOptionalField(
|
|
792
|
+
sourceDigest,
|
|
793
|
+
"sourceDigest",
|
|
794
|
+
);
|
|
795
|
+
const target = _automationMigrationField(targetJobId, "targetJobId");
|
|
796
|
+
const token = _automationMigrationOptionalField(
|
|
797
|
+
retirementToken,
|
|
798
|
+
"retirementToken",
|
|
799
|
+
);
|
|
800
|
+
return _automationTransaction(db, () => {
|
|
801
|
+
const marker = getAutomationSchedulerMigration(db, flowId);
|
|
802
|
+
const flow = getFlow(db, flowId);
|
|
803
|
+
if (!marker) {
|
|
804
|
+
if (flow && digest && automationMigrationSourceDigest(flow) === digest) {
|
|
805
|
+
return flow;
|
|
806
|
+
}
|
|
807
|
+
const error = new Error(
|
|
808
|
+
`Automation migration fence changed before rollback: ${flowId}`,
|
|
809
|
+
);
|
|
810
|
+
error.code = "AUTOMATION_SCHEDULER_MIGRATION_CONFLICT";
|
|
811
|
+
throw error;
|
|
812
|
+
}
|
|
813
|
+
const source = _automationMigrationSourceDefinition(marker);
|
|
814
|
+
if (
|
|
815
|
+
marker.migrationId !== migration ||
|
|
816
|
+
marker.targetJobId !== target ||
|
|
817
|
+
(digest !== null && marker.sourceDigest !== digest) ||
|
|
818
|
+
marker.priorStatus !== FLOW_STATUS.ACTIVE ||
|
|
819
|
+
!flow
|
|
820
|
+
) {
|
|
821
|
+
const error = new Error(
|
|
822
|
+
`Automation migration fence changed before rollback: ${flowId}`,
|
|
823
|
+
);
|
|
824
|
+
error.code = "AUTOMATION_SCHEDULER_MIGRATION_CONFLICT";
|
|
825
|
+
throw error;
|
|
826
|
+
}
|
|
827
|
+
if (marker.state === "prepared") {
|
|
828
|
+
if (
|
|
829
|
+
token !== null ||
|
|
830
|
+
marker.retirementToken !== null ||
|
|
831
|
+
automationMigrationSourceDigest(flow) !== marker.sourceDigest
|
|
832
|
+
) {
|
|
833
|
+
const error = new Error(
|
|
834
|
+
`Automation migration fence changed before rollback: ${flowId}`,
|
|
835
|
+
);
|
|
836
|
+
error.code = "AUTOMATION_SCHEDULER_MIGRATION_CONFLICT";
|
|
837
|
+
throw error;
|
|
838
|
+
}
|
|
839
|
+
db.prepare("DELETE FROM auto_scheduler_migrations WHERE flow_id = ?").run(
|
|
840
|
+
flowId,
|
|
841
|
+
);
|
|
842
|
+
return flow;
|
|
843
|
+
}
|
|
844
|
+
if (
|
|
845
|
+
marker.state !== "retired" ||
|
|
846
|
+
marker.retirementToken !== token ||
|
|
847
|
+
flow.schedule !== null ||
|
|
848
|
+
![FLOW_STATUS.PAUSED, FLOW_STATUS.ACTIVE].includes(flow.status) ||
|
|
849
|
+
automationMigrationSourceDigest(
|
|
850
|
+
automationEffectiveSchedulerFlow(db, flow),
|
|
851
|
+
) !== marker.sourceDigest
|
|
852
|
+
) {
|
|
853
|
+
const error = new Error(
|
|
854
|
+
`Automation migration fence changed before rollback: ${flowId}`,
|
|
855
|
+
);
|
|
856
|
+
error.code = "AUTOMATION_SCHEDULER_MIGRATION_CONFLICT";
|
|
857
|
+
throw error;
|
|
858
|
+
}
|
|
859
|
+
db.prepare(
|
|
860
|
+
`UPDATE auto_flows SET status = ?, schedule = ?, updated_at = ? WHERE id = ?`,
|
|
861
|
+
).run(source.status, source.schedule, marker.priorUpdatedAt, flowId);
|
|
862
|
+
db.prepare("DELETE FROM auto_scheduler_migrations WHERE flow_id = ?").run(
|
|
863
|
+
flowId,
|
|
864
|
+
);
|
|
865
|
+
return getFlow(db, flowId);
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
|
|
471
869
|
export function listFlows(db, filters = {}) {
|
|
472
870
|
const { status, limit = 50 } = filters;
|
|
473
871
|
const all = db.data?.get("auto_flows") || [];
|
|
@@ -499,6 +897,7 @@ export function updateFlowStatus(db, flowId, status) {
|
|
|
499
897
|
throw new Error(`Invalid flow status: ${status}`);
|
|
500
898
|
}
|
|
501
899
|
_requireFlow(db, flowId);
|
|
900
|
+
_assertAutomationSchedulerMutationSafe(db, flowId);
|
|
502
901
|
db.prepare(
|
|
503
902
|
`UPDATE auto_flows SET status = ?, updated_at = ? WHERE id = ?`,
|
|
504
903
|
).run(status, _now(), flowId);
|
|
@@ -509,6 +908,7 @@ export function updateFlowStatus(db, flowId, status) {
|
|
|
509
908
|
|
|
510
909
|
export function deleteFlow(db, flowId) {
|
|
511
910
|
_requireFlow(db, flowId);
|
|
911
|
+
_assertAutomationSchedulerMutationSafe(db, flowId);
|
|
512
912
|
db.prepare(`DELETE FROM auto_triggers WHERE flow_id = ?`).run(flowId);
|
|
513
913
|
db.prepare(`DELETE FROM auto_executions WHERE flow_id = ?`).run(flowId);
|
|
514
914
|
db.prepare(`DELETE FROM auto_flows WHERE id = ?`).run(flowId);
|
|
@@ -519,7 +919,14 @@ export function scheduleFlow(db, flowId, cron) {
|
|
|
519
919
|
if (!cron || typeof cron !== "string") {
|
|
520
920
|
throw new Error("cron expression required");
|
|
521
921
|
}
|
|
922
|
+
// Validate at write time so an invalid schedule cannot poison a resident
|
|
923
|
+
// scheduler loop later. nextCronTime() parses the existing five-field cron
|
|
924
|
+
// dialect and returns the first matching minute.
|
|
925
|
+
if (nextCronTime(cron, Date.now()) === null) {
|
|
926
|
+
throw new Error("cron expression has no occurrence in the next 366 days");
|
|
927
|
+
}
|
|
522
928
|
_requireFlow(db, flowId);
|
|
929
|
+
_assertAutomationSchedulerMutationSafe(db, flowId);
|
|
523
930
|
db.prepare(
|
|
524
931
|
`UPDATE auto_flows SET schedule = ?, updated_at = ? WHERE id = ?`,
|
|
525
932
|
).run(cron, _now(), flowId);
|
|
@@ -732,29 +1139,45 @@ export function executeFlow(db, flowId, options = {}) {
|
|
|
732
1139
|
inputData = {},
|
|
733
1140
|
triggerType = TRIGGER_TYPE.MANUAL,
|
|
734
1141
|
testMode = false,
|
|
1142
|
+
executionId,
|
|
735
1143
|
} = options;
|
|
736
|
-
const execId =
|
|
1144
|
+
const execId =
|
|
1145
|
+
executionId === undefined
|
|
1146
|
+
? _genId("exec")
|
|
1147
|
+
: _normalizeExecutionId(executionId);
|
|
1148
|
+
const existing = getExecution(db, execId);
|
|
1149
|
+
if (existing) {
|
|
1150
|
+
return _resolveExistingExecution(existing, { flowId, triggerType });
|
|
1151
|
+
}
|
|
737
1152
|
const startedAt = _now();
|
|
738
1153
|
const startMs = Date.now();
|
|
739
1154
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
1155
|
+
try {
|
|
1156
|
+
db.prepare(
|
|
1157
|
+
`INSERT INTO auto_executions
|
|
1158
|
+
(id, flow_id, trigger_type, input_data, output_data, status, steps_log, duration_ms, error, test_mode, started_at, completed_at)
|
|
1159
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1160
|
+
).run(
|
|
1161
|
+
execId,
|
|
1162
|
+
flowId,
|
|
1163
|
+
triggerType,
|
|
1164
|
+
JSON.stringify(inputData),
|
|
1165
|
+
null,
|
|
1166
|
+
EXECUTION_STATUS.RUNNING,
|
|
1167
|
+
JSON.stringify([]),
|
|
1168
|
+
0,
|
|
1169
|
+
null,
|
|
1170
|
+
testMode ? 1 : 0,
|
|
1171
|
+
startedAt,
|
|
1172
|
+
null,
|
|
1173
|
+
);
|
|
1174
|
+
} catch (error) {
|
|
1175
|
+
// A deterministic scheduler execution id may race another process. Resolve
|
|
1176
|
+
// the winner from durable evidence instead of running the flow twice.
|
|
1177
|
+
const concurrent = getExecution(db, execId);
|
|
1178
|
+
if (!concurrent) throw error;
|
|
1179
|
+
return _resolveExistingExecution(concurrent, { flowId, triggerType });
|
|
1180
|
+
}
|
|
758
1181
|
|
|
759
1182
|
const stepsLog = [];
|
|
760
1183
|
let finalStatus = EXECUTION_STATUS.SUCCESS;
|
|
@@ -817,6 +1240,132 @@ export function executeFlow(db, flowId, options = {}) {
|
|
|
817
1240
|
);
|
|
818
1241
|
}
|
|
819
1242
|
|
|
1243
|
+
export function adjudicateAutomationExecution(
|
|
1244
|
+
db,
|
|
1245
|
+
executionId,
|
|
1246
|
+
{ flowId, triggerType, decision, requestId } = {},
|
|
1247
|
+
) {
|
|
1248
|
+
const execId = _normalizeExecutionId(executionId);
|
|
1249
|
+
if (!["confirmed_applied", "confirmed_not_applied"].includes(decision)) {
|
|
1250
|
+
const error = new Error("automation adjudication decision is invalid");
|
|
1251
|
+
error.code = "AUTOMATION_ADJUDICATION_INVALID";
|
|
1252
|
+
throw error;
|
|
1253
|
+
}
|
|
1254
|
+
const marker = JSON.stringify({
|
|
1255
|
+
authority: "scheduler-adjudication",
|
|
1256
|
+
requestId,
|
|
1257
|
+
decision,
|
|
1258
|
+
});
|
|
1259
|
+
const archivedId = `${execId}:adjudication:${String(requestId).slice(-24)}`;
|
|
1260
|
+
const existing = getExecution(db, execId);
|
|
1261
|
+
const archived = getExecution(db, archivedId);
|
|
1262
|
+
if (decision === "confirmed_not_applied" && archived) {
|
|
1263
|
+
if (
|
|
1264
|
+
archived.flowId !== flowId ||
|
|
1265
|
+
archived.triggerType !== triggerType ||
|
|
1266
|
+
archived.error !== marker
|
|
1267
|
+
) {
|
|
1268
|
+
const error = new Error(
|
|
1269
|
+
`Automation adjudication archive mismatch: ${execId}`,
|
|
1270
|
+
);
|
|
1271
|
+
error.code = "AUTOMATION_ADJUDICATION_EVIDENCE_MISMATCH";
|
|
1272
|
+
throw error;
|
|
1273
|
+
}
|
|
1274
|
+
return archived;
|
|
1275
|
+
}
|
|
1276
|
+
if (!existing) {
|
|
1277
|
+
const error = new Error(`Automation execution not found: ${execId}`);
|
|
1278
|
+
error.code = "AUTOMATION_ADJUDICATION_EVIDENCE_MISMATCH";
|
|
1279
|
+
throw error;
|
|
1280
|
+
}
|
|
1281
|
+
if (existing.flowId !== flowId || existing.triggerType !== triggerType) {
|
|
1282
|
+
const error = new Error(`Automation execution binding mismatch: ${execId}`);
|
|
1283
|
+
error.code = "AUTOMATION_ADJUDICATION_EVIDENCE_MISMATCH";
|
|
1284
|
+
throw error;
|
|
1285
|
+
}
|
|
1286
|
+
if (
|
|
1287
|
+
existing.status !== EXECUTION_STATUS.RUNNING &&
|
|
1288
|
+
existing.error === marker
|
|
1289
|
+
) {
|
|
1290
|
+
return existing;
|
|
1291
|
+
}
|
|
1292
|
+
if (existing.status !== EXECUTION_STATUS.RUNNING) {
|
|
1293
|
+
const error = new Error(
|
|
1294
|
+
`Automation execution is not outcome-unknown: ${execId}`,
|
|
1295
|
+
);
|
|
1296
|
+
error.code = "AUTOMATION_ADJUDICATION_EVIDENCE_MISMATCH";
|
|
1297
|
+
throw error;
|
|
1298
|
+
}
|
|
1299
|
+
if (decision === "confirmed_not_applied") {
|
|
1300
|
+
const result = db
|
|
1301
|
+
.prepare(
|
|
1302
|
+
`UPDATE auto_executions
|
|
1303
|
+
SET id = ?, status = ?, error = ?, completed_at = ?
|
|
1304
|
+
WHERE id = ? AND status = ?`,
|
|
1305
|
+
)
|
|
1306
|
+
.run(
|
|
1307
|
+
archivedId,
|
|
1308
|
+
EXECUTION_STATUS.CANCELLED,
|
|
1309
|
+
marker,
|
|
1310
|
+
_now(),
|
|
1311
|
+
execId,
|
|
1312
|
+
EXECUTION_STATUS.RUNNING,
|
|
1313
|
+
);
|
|
1314
|
+
if (result.changes !== 1) {
|
|
1315
|
+
const error = new Error(
|
|
1316
|
+
`Automation adjudication changed concurrently: ${execId}`,
|
|
1317
|
+
);
|
|
1318
|
+
error.code = "AUTOMATION_ADJUDICATION_EVIDENCE_MISMATCH";
|
|
1319
|
+
throw error;
|
|
1320
|
+
}
|
|
1321
|
+
return getExecution(db, archivedId);
|
|
1322
|
+
}
|
|
1323
|
+
db.prepare(
|
|
1324
|
+
`UPDATE auto_executions
|
|
1325
|
+
SET status = ?, error = ?, completed_at = ?
|
|
1326
|
+
WHERE id = ? AND status = ?`,
|
|
1327
|
+
).run(
|
|
1328
|
+
EXECUTION_STATUS.SUCCESS,
|
|
1329
|
+
marker,
|
|
1330
|
+
_now(),
|
|
1331
|
+
execId,
|
|
1332
|
+
EXECUTION_STATUS.RUNNING,
|
|
1333
|
+
);
|
|
1334
|
+
return getExecution(db, execId);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
function _normalizeExecutionId(value) {
|
|
1338
|
+
if (
|
|
1339
|
+
typeof value !== "string" ||
|
|
1340
|
+
value.length === 0 ||
|
|
1341
|
+
value.length > 256 ||
|
|
1342
|
+
!/^[A-Za-z0-9._:-]+$/.test(value)
|
|
1343
|
+
) {
|
|
1344
|
+
const error = new Error("executionId must be a safe non-empty identifier");
|
|
1345
|
+
error.code = "AUTOMATION_EXECUTION_ID_INVALID";
|
|
1346
|
+
throw error;
|
|
1347
|
+
}
|
|
1348
|
+
return value;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
function _resolveExistingExecution(existing, { flowId, triggerType }) {
|
|
1352
|
+
if (existing.flowId !== flowId || existing.triggerType !== triggerType) {
|
|
1353
|
+
const error = new Error(
|
|
1354
|
+
`Execution id is already bound to another automation request: ${existing.id}`,
|
|
1355
|
+
);
|
|
1356
|
+
error.code = "AUTOMATION_EXECUTION_BINDING_MISMATCH";
|
|
1357
|
+
throw error;
|
|
1358
|
+
}
|
|
1359
|
+
if (existing.status === EXECUTION_STATUS.RUNNING) {
|
|
1360
|
+
const error = new Error(
|
|
1361
|
+
`Automation execution outcome is unknown; refusing duplicate execution: ${existing.id}`,
|
|
1362
|
+
);
|
|
1363
|
+
error.code = "AUTOMATION_EXECUTION_OUTCOME_UNKNOWN";
|
|
1364
|
+
throw error;
|
|
1365
|
+
}
|
|
1366
|
+
return existing;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
820
1369
|
export function fireTrigger(db, triggerId, inputData = {}) {
|
|
821
1370
|
const trig = getTrigger(db, triggerId);
|
|
822
1371
|
if (!trig) throw new Error(`Trigger not found: ${triggerId}`);
|