chainlesschain 0.164.0 → 0.165.2
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/README.md +248 -2
- package/package.json +7 -4
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-D2MEn7fI.js → AIOps-lS1R0Cza.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-lCW_4Gf0.js → ActionButton-BmlgBPsj.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-Bs7i-nsS.js → Analytics-CNKczv1S.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-CK7B6S_j.js → AppLayout-BuZGJARQ.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-C8ILjpyo.js → Artifacts-Dm73Sl-O.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-Bb9cNaWc.js → Audit-BGMW7A5e.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-sC9ooXIf.js → BackgroundAgents-BQ6M54JR.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-D-DrsplT.js → Backup-DBzGP0bR.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-uCNZpwSK.js → BaseInput-D1mLCoOV.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CsDK1uE9.js → Chat-BUfcN7p8.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-OqSDt50w.js → ChatBubbleRenderer-CY5m8ZHg.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-JhTXM1-K.js → Checkbox-Du0Yg5Pi.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-BbzZLnyD.js → Codegen-B2RXziHW.js} +1 -1
- package/src/assets/web-panel/assets/{Col-D4VW4m4j.js → Col-DWhekOrR.js} +1 -1
- package/src/assets/web-panel/assets/{Community-BXrYawxV.js → Community-HQhrkon4.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DCihGy1u.js → Compact-izHNb4uz.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-DlyZ-lk8.js → Compliance-BDwLy41-.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-DvGZrhbO.js → Cowork-DFJhLa7K.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-DUncZi8h.js → Cron-DYY9RG7-.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-C_hLuUcm.js → Crosschain-rr7tJTtv.js} +1 -1
- package/src/assets/web-panel/assets/{DID-b6YSUqxr.js → DID-D2sfHRLi.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-uvM15Cfb.js → Dashboard-q_eGdhha.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-D1-Nd4qY.js → Dropdown-Cufrh2Bz.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-8ApvL6Zc.js → EmailListRenderer-B-kdv10f.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-LYJt8811.js → FamilyGuardDashboard-D21XjgfG.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-BbI3HGpv.js → Federation-CAwRTLZH.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-DSoEKuFu.js → FormItemContext-vx3daNDF.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-B0S9OWd5.js → GenericCardRenderer-CRWY2wU4.js} +1 -1
- package/src/assets/web-panel/assets/{Git-DIs9Zhf3.js → Git-Q6SwauyD.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-jzZXrxcl.js → Governance-BJ1q4IQZ.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-JUPjf2ZI.js → Inference-CSWjROgD.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DxnfJvlP.js → KnowledgeGraph-fzuQsWuY.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-iXiz8BEE.js → Logs-DVF8Mnns.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-BLif4QSg.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-Y3Xch312.js → Marketplace-BQP7LOA4.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-D7kGGbO7.js → McpTools-BNlJCmNc.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-Do_hVxzh.js → Memory-DLLg5WQ4.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-CtrR0Tas.js → MobileBridge-CR2skNAF.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-BxVavObl.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-qo5QCTGd.js → Mtc-B17pgqMI.js} +4 -4
- package/src/assets/web-panel/assets/{MtcAudit-cq5mnjYj.js → MtcAudit-4UmV5cIL.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-Bq0MAQHw.js → Multisig-DoMEXEcU.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-DdWE6Vi2.js → NLProgramming-h9DMXFX4.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-BxMBFcIp.js → Notes-Cihn-xX8.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-RTrAcbUe.js → NotificationSettings-BE6ZGa3R.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-DJbpGqzT.js → OrderTableRenderer-CnwojoSI.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-SQzIPdqC.js → Organization-B2vpwmjb.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-02yECh22.js → Overflow-C4z28R0-.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BNg4pNCM.js → P2P-BVlFkwhW.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-BxPJZsD3.js → PdhVaultBrowser-BXbEdL5B.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-CLnDnPAz.js → Permissions-C28xs4lp.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-C5m0AjGf.js → PersonalDataHub-Dgl_rBjM.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-VwOf5Dhv.js → Pipeline-3QUIhrKI.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-CC0W2hZd.js → Privacy-8Gh33OZ5.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-Dz893Bob.js → ProjectInit-BNmg4tZi.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-CxQkvl0k.js → ProjectSettings-vgMfeCf9.js} +2 -2
- package/src/assets/web-panel/assets/Projects-BhtAVilc.js +1 -0
- package/src/assets/web-panel/assets/{Providers-DeB9IkxK.js → Providers-CXoOa8eX.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-OCeoaswn.js → QrScannerModal-BhfTF0oS.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-DenotaAj.js → QuickAsk-C746HEAj.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-Cies_jjI.js → Recommend-BSej8Eo0.js} +1 -1
- package/src/assets/web-panel/assets/RemoteSession-f3gmnlG3.js +5 -0
- package/src/assets/web-panel/assets/{Reputation-CW0mvxgZ.js → Reputation-CyvybpkN.js} +1 -1
- package/src/assets/web-panel/assets/{Row-SEd32qZU.js → Row-DwV2PeYn.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BSwnJ3Lm.js → RssFeed-11GrzjZy.js} +3 -3
- package/src/assets/web-panel/assets/{Search-xe2m8bkC.js → Search-BjKXkrAY.js} +1 -1
- package/src/assets/web-panel/assets/{Security-Mq2F9Nro.js → Security-eWiFb9Yk.js} +4 -4
- package/src/assets/web-panel/assets/{Services-BKGw2V3P.js → Services-CdCsMAtt.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DW95-Rxf.js → Skeleton-4Q2UTKQ5.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CHgChCv_.js → Skills-v4rtYrRj.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-tKh6k3R7.js → Sla-CRhsFvd_.js} +1 -1
- package/src/assets/web-panel/assets/SpeechSettings-DLjKsDVu.js +1 -0
- package/src/assets/web-panel/assets/{SyncSettings-D0Fw8fVL.js → SyncSettings-D16zonl7.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-20x1GB3D.js → Tasks-Xxozz0i_.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-DJ-lts-a.js → Templates-CXJpPtCT.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BFdGpbs7.js → Tenant-XRV0L1ZF.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-Xpx61vHL.js → Terminal-CYuO1i_w.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-C6UIvyZQ.js → TimelineRenderer-BXqitWsu.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-Dellq08f.js → Tokens-BvPrDHIy.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-VKw8d0J-.js → Trigger-DhCrSTf5.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DNftdKT5.js → Trust-DmsuZD6K.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DX9VAKJI.js → UkeySign-D_yrts5C.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-B2h3jO8q.js → VideoEditing-B01ihPrm.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-CjGkof7f.js → Wallet-DpUjKa-N.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-2Zfcu55P.js → WebAuthn-CL07yON6.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-B9eZ8UxZ.js → WorkflowEditor-CLMflnRr.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CQNfD-YO.js → chat-DtUqe8h_.js} +1 -1
- package/src/assets/web-panel/assets/{colors-D4usyFT2.js → colors-DPKrgVfy.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-s4TpjvjX.js → compact-item-BTt2GtOm.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BRCSvYxj.js → createContext-D_VeUc1J.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-Bj5nUW2v.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-AdkUbkV-.js → hasIn-1kndiylj.js} +1 -1
- package/src/assets/web-panel/assets/{index-BIoG5yYf.js → index-2eyAXE3d.js} +1 -1
- package/src/assets/web-panel/assets/{index-nXyk8GZ9.js → index-4bna2_SR.js} +1 -1
- package/src/assets/web-panel/assets/{index-B2V-eKfK.js → index-8nz_fmlj.js} +1 -1
- package/src/assets/web-panel/assets/{index-D_XBHopw.js → index-B9KvcS7R.js} +1 -1
- package/src/assets/web-panel/assets/{index-s_buFMEG.js → index-BAXfRKfG.js} +1 -1
- package/src/assets/web-panel/assets/{index-DZT36ZYq.js → index-BHpBMWCh.js} +1 -1
- package/src/assets/web-panel/assets/{index-BMcimU-2.js → index-BaZAbw9A.js} +1 -1
- package/src/assets/web-panel/assets/{index-DzRjHxEv.js → index-BepLZwgB.js} +1 -1
- package/src/assets/web-panel/assets/{index--00U6vXZ.js → index-Bhudc_Ez.js} +1 -1
- package/src/assets/web-panel/assets/{index-C7Y8Aluc.js → index-BlDNVgBt.js} +1 -1
- package/src/assets/web-panel/assets/{index-BtobShro.js → index-By5n8up9.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cvyt-zhm.js → index-CHG-KcS4.js} +1 -1
- package/src/assets/web-panel/assets/index-CPcM-eqo.js +1 -0
- package/src/assets/web-panel/assets/{index-MMN9V6JF.js → index-CX81qSM4.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bad4VNvz.js → index-CauoBQkH.js} +1 -1
- package/src/assets/web-panel/assets/{index-CCsM_Ybx.js → index-CcO04eDn.js} +1 -1
- package/src/assets/web-panel/assets/{index-DARsAyUp.js → index-CtF7EN49.js} +1 -1
- package/src/assets/web-panel/assets/{index-C5KAQFPQ.js → index-CzjkKUmR.js} +1 -1
- package/src/assets/web-panel/assets/{index-RfMr79MW.js → index-D95OFht7.js} +1 -1
- package/src/assets/web-panel/assets/{index-DdCB70SS.js → index-DFIZ_1Sf.js} +1 -1
- package/src/assets/web-panel/assets/{index-CYdoQsa1.js → index-DG2MZQ3f.js} +1 -1
- package/src/assets/web-panel/assets/{index-BPhkFZ1I.js → index-DJ8vXGOa.js} +1 -1
- package/src/assets/web-panel/assets/{index-_IRVsV9X.js → index-Dbbymxpj.js} +1 -1
- package/src/assets/web-panel/assets/{index-DVKfQ0Ux.js → index-DdkplL9R.js} +1 -1
- package/src/assets/web-panel/assets/{index-CkwiSLhU.js → index-DfGl8CqG.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJVEOa4i.js → index-DfKBRdSS.js} +1 -1
- package/src/assets/web-panel/assets/{index-C1mJtE6q.js → index-DsJCDUJG.js} +1 -1
- package/src/assets/web-panel/assets/{index-BgiXR_-_.js → index-Dwqmtk65.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJCFe2Qf.js → index-DxdTdBix.js} +1 -1
- package/src/assets/web-panel/assets/{index-C484EJiS.js → index-FzdwcSy2.js} +1 -1
- package/src/assets/web-panel/assets/{index-B4JJCfQ2.js → index-GsoUQ2M7.js} +1 -1
- package/src/assets/web-panel/assets/{index-CQhAVU5t.js → index-brCInrzq.js} +1 -1
- package/src/assets/web-panel/assets/{index-CKC1t3Ic.js → index-dVc-CXY4.js} +1 -1
- package/src/assets/web-panel/assets/{index-BQ3DXBtD.js → index-fk2Rjacw.js} +3 -3
- package/src/assets/web-panel/assets/{index-Cduu2Hhr.js → index-l1BETrDb.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bw2MEnUk.js → index-m31Ka8hZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-D2GjI2oe.js → index-ocf06EVQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJdQrZth.js → index-sDoMHTNA.js} +1 -1
- package/src/assets/web-panel/assets/index-xeSyVoRe.js +1 -0
- package/src/assets/web-panel/assets/{initDefaultProps-Blmpk7HL.js → initDefaultProps-CvyYJd7z.js} +1 -1
- package/src/assets/web-panel/assets/{motion-DPBJMv1n.js → motion-DYZkhUdz.js} +1 -1
- package/src/assets/web-panel/assets/{move-ButdzCM7.js → move-slnJRVT1.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-cyRC_QCe.js → mtc-parser-DVc0CKmy.js} +1 -1
- package/src/assets/web-panel/assets/{omit-1epSV7hz.js → omit-BCXAtHCq.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DuPY8xJs.js → pickAttrs-DBsemz_K.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-6XZqarhe.js → placementArrow-BRtkJ-nv.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-B40pXBWM.js → responsiveObserve-Cebq7EZn.js} +1 -1
- package/src/assets/web-panel/assets/{slide-jbZ9Wu4n.js → slide-DT7qvId2.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BKQQcVy4.js → statusUtils-pQiITLkG.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-NiKfWjUT.js → styleChecker-c-19BVKQ.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-bQUCvPeb.js → useFlexGapSupport-DsS5-0bE.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-CUGji7ph.js → useFs-CxGiMe5n.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-4veyN2N2.js → usePersonalDataHub-D9btVID5.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-1-nLug-Z.js → vnode-CUYFsFEN.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-dhCeRDMo.js → zoom-CXmn_35j.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +1 -1
- package/src/commands/cowork.js +559 -62
- package/src/commands/plugin.js +819 -64
- package/src/commands/session-location.js +318 -7
- package/src/constants.js +2 -2
- package/src/data/changelog.json +28 -2
- package/src/gateways/ws/action-protocol.js +293 -19
- package/src/gateways/ws/message-dispatcher.js +30 -0
- package/src/gateways/ws/remote-session-protocol.js +542 -35
- package/src/gateways/ws/ws-approval-gate.js +5 -0
- package/src/gateways/ws/ws-server.js +462 -38
- package/src/harness/jsonl-session-store.js +638 -2
- package/src/harness/prompt-compressor.js +4 -0
- package/src/harness/remote-session-audit.js +1 -0
- package/src/harness/remote-session-crypto.js +750 -50
- package/src/harness/remote-session-registry.js +795 -27
- package/src/lib/auto-mode-config.js +98 -3
- package/src/lib/background-agent-keeper-client.js +29 -0
- package/src/lib/background-agent-keeper-protocol.js +158 -10
- package/src/lib/background-agent-supervisor.js +870 -48
- package/src/lib/background-session-transport.js +23 -6
- package/src/lib/background-turn-bootstrap-protocol.js +64 -0
- package/src/lib/cowork-task-runner.js +70 -0
- package/src/lib/cowork-workflow.js +887 -62
- package/src/lib/delivery-production-adapter.js +38 -2
- package/src/lib/doctor-checkup.js +34 -10
- package/src/lib/dynamic-workflow-draft.js +535 -0
- package/src/lib/dynamic-workflow-facade.js +1105 -17
- package/src/lib/dynamic-workflow-runtime.js +2974 -0
- package/src/lib/execution-location-contract.js +111 -7
- package/src/lib/execution-location-runtime.js +3 -1
- package/src/lib/execution-location-target.js +910 -0
- package/src/lib/llm-providers.js +1 -0
- package/src/lib/mcp-call-ledger-store.js +35 -0
- package/src/lib/mcp-call-ledger.js +30 -0
- package/src/lib/mcp-stdio-package-materialization.js +1 -1
- package/src/lib/plugin-runtime/__tests__/remote-source.test.js +34 -16
- package/src/lib/plugin-runtime/install.js +4956 -657
- package/src/lib/plugin-runtime/manifest.js +3 -1
- package/src/lib/plugin-runtime/marketplace-artifact-readback.js +671 -31
- package/src/lib/plugin-runtime/marketplace-catalog.js +68 -2
- package/src/lib/plugin-runtime/marketplace-impact.js +208 -15
- package/src/lib/plugin-runtime/marketplace-network.js +317 -0
- package/src/lib/plugin-runtime/marketplace-pac-worker.js +15 -0
- package/src/lib/plugin-runtime/marketplace-remote-artifacts.js +25 -2
- package/src/lib/plugin-runtime/marketplace-source-cache.js +359 -0
- package/src/lib/plugin-runtime/policy.js +21 -6
- package/src/lib/plugin-runtime/publisher-trust.js +332 -0
- package/src/lib/plugin-runtime/remote-source.js +323 -27
- package/src/lib/plugin-runtime/scopes.js +317 -41
- package/src/lib/plugin-runtime/transaction-journal.js +806 -0
- package/src/lib/public-ide-capability-manifest.js +1 -1
- package/src/lib/remote-approval-bridge.js +745 -108
- package/src/lib/remote-control.js +2 -0
- package/src/lib/remote-membership-coordinator.js +612 -46
- package/src/lib/remote-membership-host-store.js +59 -10
- package/src/lib/runtime-provenance-ledger.js +171 -46
- package/src/lib/shell-approval.js +169 -6
- package/src/lib/sub-agent-context.js +268 -2
- package/src/lib/task-model-selector.js +1 -1
- package/src/lib/with-file-lock.js +167 -42
- package/src/repl/agent-repl.js +70 -29
- package/src/repl/remote-approval.js +29 -7
- package/src/runtime/agent-core.js +1695 -194
- package/src/runtime/headless-runner.js +22 -5
- package/src/runtime/headless-stream.js +19 -3
- package/src/workers/background-agent-keeper.js +279 -58
- package/src/workers/background-agent-worker.js +7 -0
- package/src/workers/background-turn-bootstrap.js +29 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-GmB6DOr6.js +0 -1
- package/src/assets/web-panel/assets/MobileProjects-BL0Czcoj.js +0 -1
- package/src/assets/web-panel/assets/Projects-DDAjkUkT.js +0 -1
- package/src/assets/web-panel/assets/RemoteSession-Dxzl5n2T.js +0 -5
- package/src/assets/web-panel/assets/SpeechSettings-D6UL3dA5.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DKJjSWRn.js +0 -1
- package/src/assets/web-panel/assets/index-C61cnl3K.js +0 -1
- package/src/assets/web-panel/assets/index-D4XFHxBr.js +0 -1
|
@@ -0,0 +1,2974 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { types as utilTypes } from "node:util";
|
|
5
|
+
import {
|
|
6
|
+
COWORK_WORKFLOW_CONTROL_SIGNAL_CODE,
|
|
7
|
+
executeWorkflow as executeCoworkWorkflow,
|
|
8
|
+
} from "./cowork-workflow.js";
|
|
9
|
+
import { writeSecurityStore } from "./durable-security-store.js";
|
|
10
|
+
import { containsSecret } from "./secret-scan.js";
|
|
11
|
+
import {
|
|
12
|
+
sameFileStatIdentity,
|
|
13
|
+
samePathHandleFileIdentity,
|
|
14
|
+
withTrustedFileParentSync,
|
|
15
|
+
} from "./secure-file-identity.js";
|
|
16
|
+
import { canonicalJson } from "./scheduler-kernel/contract.js";
|
|
17
|
+
import { withFileLock } from "./with-file-lock.js";
|
|
18
|
+
|
|
19
|
+
export const DYNAMIC_WORKFLOW_RUNTIME_SCHEMA = "cc-dynamic-workflow-runtime/v1";
|
|
20
|
+
export const DYNAMIC_WORKFLOW_RUNTIME_VERSION = 1;
|
|
21
|
+
export const DYNAMIC_WORKFLOW_EFFECT_SCHEMA = "cc-dynamic-workflow-effect/v1";
|
|
22
|
+
export const DYNAMIC_WORKFLOW_CALL_SCHEMA = "cc-dynamic-workflow-call/v1";
|
|
23
|
+
export const DYNAMIC_WORKFLOW_OBSERVABILITY_SCHEMA =
|
|
24
|
+
"cc-dynamic-workflow-observability/v1";
|
|
25
|
+
export const DYNAMIC_WORKFLOW_RUNTIME_CONTROL_CODE =
|
|
26
|
+
COWORK_WORKFLOW_CONTROL_SIGNAL_CODE;
|
|
27
|
+
|
|
28
|
+
const STATE_STATUSES = new Set([
|
|
29
|
+
"ready",
|
|
30
|
+
"running",
|
|
31
|
+
"pause_requested",
|
|
32
|
+
"paused",
|
|
33
|
+
"blocked",
|
|
34
|
+
"failed",
|
|
35
|
+
"stopped",
|
|
36
|
+
"completed",
|
|
37
|
+
]);
|
|
38
|
+
const TERMINAL_STATUSES = new Set(["stopped", "completed"]);
|
|
39
|
+
const SHA256_RE = /^sha256:[a-f0-9]{64}$/u;
|
|
40
|
+
const MAX_STATE_BYTES = 4 * 1024 * 1024;
|
|
41
|
+
const MAX_EFFECTS = 64;
|
|
42
|
+
const MAX_EFFECT_BATCH_SIZE = 64;
|
|
43
|
+
const MAX_LINEAGE_EVENTS = 4096;
|
|
44
|
+
const MAX_CALLS_PER_EFFECT = 128;
|
|
45
|
+
const MAX_PROJECTED_ARTIFACTS_PER_EFFECT = 256;
|
|
46
|
+
const PROVIDER_CLIENT_REQUEST_ID_RE = /^ccwf_[a-f0-9]{64}$/u;
|
|
47
|
+
const PROVIDER_RECEIPT_ID_RE = /^[A-Za-z0-9._:-]{1,256}$/u;
|
|
48
|
+
const PROVIDER_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/u;
|
|
49
|
+
const WORKFLOW_TOOL_CALL_ID_RE = /^[\x21-\x7e]{1,512}$/u;
|
|
50
|
+
const WORKFLOW_TOOL_NAME_RE = /^[A-Za-z0-9._:-]{1,256}$/u;
|
|
51
|
+
const WORKFLOW_CHILD_EFFECT_PROTOCOL = "cc-workflow-child-effect/v1";
|
|
52
|
+
const WORKFLOW_PROVIDER_ATTEMPT_PROTOCOL = "cc-provider-request-attempt/v1";
|
|
53
|
+
const WORKFLOW_PROVIDER_CALL_ID_RE = /^[A-Za-z0-9._:-]{1,256}$/u;
|
|
54
|
+
const WORKFLOW_PROVIDER_REQUEST_ID_RE = /^ccwf_[a-f0-9]{64}$/u;
|
|
55
|
+
const DESCENDANT_OWNER_TOOL_NAMES = new Set(["run_skill", "spawn_sub_agent"]);
|
|
56
|
+
const WORKFLOW_CALL_STATUSES = new Set([
|
|
57
|
+
"started",
|
|
58
|
+
"completed",
|
|
59
|
+
"failed",
|
|
60
|
+
"outcome_unknown",
|
|
61
|
+
"operator_reconciled",
|
|
62
|
+
]);
|
|
63
|
+
const SETTLEMENT_AUTHORITIES = new Set([
|
|
64
|
+
"provider-return",
|
|
65
|
+
"operator-reconciled",
|
|
66
|
+
"runtime-not-dispatched",
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
function digest(domain, value) {
|
|
70
|
+
return `sha256:${createHash("sha256")
|
|
71
|
+
.update(domain, "utf8")
|
|
72
|
+
.update(canonicalJson(value, "dynamicWorkflowRuntime"), "utf8")
|
|
73
|
+
.digest("hex")}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isoNow(now) {
|
|
77
|
+
const raw = typeof now === "function" ? now() : now;
|
|
78
|
+
const date = raw == null ? new Date() : new Date(raw);
|
|
79
|
+
if (!Number.isFinite(date.getTime())) {
|
|
80
|
+
throw new TypeError("dynamic workflow runtime time is invalid");
|
|
81
|
+
}
|
|
82
|
+
return date.toISOString();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function safeId(value, field, max = 256) {
|
|
86
|
+
if (
|
|
87
|
+
typeof value !== "string" ||
|
|
88
|
+
!new RegExp(`^[A-Za-z0-9][A-Za-z0-9._-]{0,${max - 1}}$`, "u").test(value) ||
|
|
89
|
+
value.includes("..")
|
|
90
|
+
) {
|
|
91
|
+
throw new TypeError(`${field} is invalid`);
|
|
92
|
+
}
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function snapshotJson(value, field, maxBytes = MAX_STATE_BYTES) {
|
|
97
|
+
let encoded;
|
|
98
|
+
try {
|
|
99
|
+
encoded = canonicalJson(value, field);
|
|
100
|
+
} catch {
|
|
101
|
+
throw new TypeError(`${field} must contain canonical JSON`);
|
|
102
|
+
}
|
|
103
|
+
if (Buffer.byteLength(encoded, "utf8") > maxBytes) {
|
|
104
|
+
throw new TypeError(`${field} exceeds its byte limit`);
|
|
105
|
+
}
|
|
106
|
+
return JSON.parse(encoded);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function nonNegativeSafeInteger(value) {
|
|
110
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function effectResultDigest(result) {
|
|
114
|
+
return digest("chainlesschain.dynamic-workflow.effect-result.v1\0", result);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function effectBatchId(runId, effects) {
|
|
118
|
+
return digest("chainlesschain.dynamic-workflow.effect-batch.v1\0", {
|
|
119
|
+
runId,
|
|
120
|
+
effects: effects.map((effect) => ({
|
|
121
|
+
key: effect.key,
|
|
122
|
+
payloadDigest: effect.payloadDigest,
|
|
123
|
+
})),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function hasProviderDispatchMetadata(effect) {
|
|
128
|
+
return [
|
|
129
|
+
effect?.providerDispatchedAt,
|
|
130
|
+
effect?.timeoutMs,
|
|
131
|
+
effect?.timeoutObservedAt,
|
|
132
|
+
].some((field) => field !== undefined);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isKnownUndispatched(effect) {
|
|
136
|
+
return (
|
|
137
|
+
hasProviderDispatchMetadata(effect) && effect?.providerDispatchedAt === null
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function stateMaterial(state) {
|
|
142
|
+
const material = { ...state };
|
|
143
|
+
delete material.stateDigest;
|
|
144
|
+
return material;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function finalizeState(state) {
|
|
148
|
+
const material = snapshotJson(stateMaterial(state), "workflow runtime state");
|
|
149
|
+
return Object.freeze({
|
|
150
|
+
...material,
|
|
151
|
+
stateDigest: digest(
|
|
152
|
+
"chainlesschain.dynamic-workflow.runtime-state.v1\0",
|
|
153
|
+
material,
|
|
154
|
+
),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function appendLineage(state, type, details, now) {
|
|
159
|
+
if (state.lineage.length >= MAX_LINEAGE_EVENTS) {
|
|
160
|
+
throw new Error("dynamic workflow runtime lineage limit exceeded");
|
|
161
|
+
}
|
|
162
|
+
const at = isoNow(now);
|
|
163
|
+
const previousDigest = state.lineage.at(-1)?.eventDigest || null;
|
|
164
|
+
state.revision += 1;
|
|
165
|
+
state.updatedAt = at;
|
|
166
|
+
const material = {
|
|
167
|
+
sequence: state.lineage.length + 1,
|
|
168
|
+
revision: state.revision,
|
|
169
|
+
at,
|
|
170
|
+
type,
|
|
171
|
+
details: snapshotJson(details || {}, "workflow runtime event", 64 * 1024),
|
|
172
|
+
previousDigest,
|
|
173
|
+
};
|
|
174
|
+
state.lineage.push({
|
|
175
|
+
...material,
|
|
176
|
+
eventDigest: digest(
|
|
177
|
+
"chainlesschain.dynamic-workflow.runtime-event.v1\0",
|
|
178
|
+
material,
|
|
179
|
+
),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function transition(snapshot, type, details, mutate, now) {
|
|
184
|
+
const state = snapshotJson(
|
|
185
|
+
stateMaterial(snapshot),
|
|
186
|
+
"workflow runtime transition",
|
|
187
|
+
);
|
|
188
|
+
mutate(state);
|
|
189
|
+
appendLineage(state, type, details, now);
|
|
190
|
+
return finalizeState(state);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function verifyLineage(state, issues) {
|
|
194
|
+
if (
|
|
195
|
+
!Array.isArray(state.lineage) ||
|
|
196
|
+
state.lineage.length > MAX_LINEAGE_EVENTS
|
|
197
|
+
) {
|
|
198
|
+
issues.push("lineage-invalid");
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
let previousDigest = null;
|
|
202
|
+
for (const [index, event] of state.lineage.entries()) {
|
|
203
|
+
const material = {
|
|
204
|
+
sequence: event?.sequence,
|
|
205
|
+
revision: event?.revision,
|
|
206
|
+
at: event?.at,
|
|
207
|
+
type: event?.type,
|
|
208
|
+
details: event?.details,
|
|
209
|
+
previousDigest: event?.previousDigest,
|
|
210
|
+
};
|
|
211
|
+
const expected = digest(
|
|
212
|
+
"chainlesschain.dynamic-workflow.runtime-event.v1\0",
|
|
213
|
+
material,
|
|
214
|
+
);
|
|
215
|
+
if (
|
|
216
|
+
event?.sequence !== index + 1 ||
|
|
217
|
+
event?.revision !== index + 1 ||
|
|
218
|
+
event?.previousDigest !== previousDigest ||
|
|
219
|
+
event?.eventDigest !== expected ||
|
|
220
|
+
isoNow(event?.at) !== event.at
|
|
221
|
+
) {
|
|
222
|
+
issues.push(`lineage-${index}-invalid`);
|
|
223
|
+
}
|
|
224
|
+
previousDigest = event?.eventDigest || null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function workflowCallId(
|
|
229
|
+
effectId,
|
|
230
|
+
kind,
|
|
231
|
+
callId,
|
|
232
|
+
childEffectId = null,
|
|
233
|
+
ownerEffectId = null,
|
|
234
|
+
) {
|
|
235
|
+
const material = {
|
|
236
|
+
effectId,
|
|
237
|
+
kind,
|
|
238
|
+
callId,
|
|
239
|
+
childEffectId,
|
|
240
|
+
};
|
|
241
|
+
if (ownerEffectId !== null) material.ownerEffectId = ownerEffectId;
|
|
242
|
+
return digest("chainlesschain.dynamic-workflow.call-id.v1\0", material);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function expectedWorkflowProviderRequestId(effectId, source, sequence) {
|
|
246
|
+
return `ccwf_${createHash("sha256")
|
|
247
|
+
.update(`${effectId}\0${source}\0${String(sequence)}`, "utf8")
|
|
248
|
+
.digest("hex")}`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function verifyEffectCalls(effect, effectIndex, issues) {
|
|
252
|
+
const calls = effect?.calls === undefined ? [] : effect.calls;
|
|
253
|
+
if (!Array.isArray(calls) || calls.length > MAX_CALLS_PER_EFFECT) {
|
|
254
|
+
issues.push(`effect-${effectIndex}-calls-invalid`);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const ids = new Set();
|
|
258
|
+
const identities = new Set();
|
|
259
|
+
const authorizedOwnerEffectIds = new Set([effect.id]);
|
|
260
|
+
for (const [callIndex, call] of calls.entries()) {
|
|
261
|
+
const legacyOwner = call?.ownerEffectId === undefined;
|
|
262
|
+
const ownerEffectId = legacyOwner ? effect.id : call?.ownerEffectId;
|
|
263
|
+
const requestSource =
|
|
264
|
+
call?.requestSource === undefined
|
|
265
|
+
? call?.kind === "provider"
|
|
266
|
+
? call?.source
|
|
267
|
+
: null
|
|
268
|
+
: call.requestSource;
|
|
269
|
+
const terminal = call?.status !== "started";
|
|
270
|
+
const identityKey = `${call?.kind || ""}\0${
|
|
271
|
+
call?.kind === "tool" ? call?.childEffectId : call?.callId
|
|
272
|
+
}`;
|
|
273
|
+
let timestampsValid = false;
|
|
274
|
+
try {
|
|
275
|
+
timestampsValid =
|
|
276
|
+
isoNow(call?.startedAt) === call.startedAt &&
|
|
277
|
+
Date.parse(call.startedAt) >= Date.parse(effect.requestedAt) &&
|
|
278
|
+
(terminal
|
|
279
|
+
? isoNow(call?.settledAt) === call.settledAt &&
|
|
280
|
+
Date.parse(call.settledAt) >= Date.parse(call.startedAt) &&
|
|
281
|
+
(effect.settledAt === null ||
|
|
282
|
+
Date.parse(call.settledAt) <= Date.parse(effect.settledAt))
|
|
283
|
+
: call?.settledAt === null);
|
|
284
|
+
} catch {
|
|
285
|
+
timestampsValid = false;
|
|
286
|
+
}
|
|
287
|
+
const commonValid =
|
|
288
|
+
call?.schema === DYNAMIC_WORKFLOW_CALL_SCHEMA &&
|
|
289
|
+
call.effectId === effect.id &&
|
|
290
|
+
SHA256_RE.test(call.id || "") &&
|
|
291
|
+
call.id ===
|
|
292
|
+
workflowCallId(
|
|
293
|
+
effect.id,
|
|
294
|
+
call.kind,
|
|
295
|
+
call.callId,
|
|
296
|
+
call.childEffectId,
|
|
297
|
+
legacyOwner ? null : ownerEffectId,
|
|
298
|
+
) &&
|
|
299
|
+
SHA256_RE.test(ownerEffectId || "") &&
|
|
300
|
+
authorizedOwnerEffectIds.has(ownerEffectId) &&
|
|
301
|
+
["provider", "tool"].includes(call.kind) &&
|
|
302
|
+
WORKFLOW_CALL_STATUSES.has(call.status) &&
|
|
303
|
+
Number.isSafeInteger(call.sequence) &&
|
|
304
|
+
call.sequence >= 1 &&
|
|
305
|
+
typeof call.name === "string" &&
|
|
306
|
+
call.name.length >= 1 &&
|
|
307
|
+
call.name.length <= 256 &&
|
|
308
|
+
typeof call.source === "string" &&
|
|
309
|
+
call.source.length >= 1 &&
|
|
310
|
+
call.source.length <= 64 &&
|
|
311
|
+
typeof call.identitySemantics === "string" &&
|
|
312
|
+
call.identitySemantics.length >= 1 &&
|
|
313
|
+
call.identitySemantics.length <= 64 &&
|
|
314
|
+
(call.settlementCode === null ||
|
|
315
|
+
(typeof call.settlementCode === "string" &&
|
|
316
|
+
call.settlementCode.length >= 1 &&
|
|
317
|
+
call.settlementCode.length <= 128)) &&
|
|
318
|
+
typeof call.outcomeUnknown === "boolean" &&
|
|
319
|
+
call.outcomeUnknown === (call.status === "outcome_unknown") &&
|
|
320
|
+
typeof call.providerReceiptPersisted === "boolean" &&
|
|
321
|
+
(call.providerReceiptRequestId === null ||
|
|
322
|
+
(typeof call.providerReceiptRequestId === "string" &&
|
|
323
|
+
PROVIDER_RECEIPT_ID_RE.test(call.providerReceiptRequestId))) &&
|
|
324
|
+
(call.providerReceiptResponseId === null ||
|
|
325
|
+
(typeof call.providerReceiptResponseId === "string" &&
|
|
326
|
+
PROVIDER_RECEIPT_ID_RE.test(call.providerReceiptResponseId))) &&
|
|
327
|
+
call.providerReceiptPersisted ===
|
|
328
|
+
Boolean(
|
|
329
|
+
call.providerReceiptRequestId || call.providerReceiptResponseId,
|
|
330
|
+
) &&
|
|
331
|
+
(call.mcpLedgerId === null ||
|
|
332
|
+
PROVIDER_RECEIPT_ID_RE.test(call.mcpLedgerId || "")) &&
|
|
333
|
+
typeof call.mcpLedgerPrewritePersisted === "boolean" &&
|
|
334
|
+
typeof call.mcpLedgerSettlementPersisted === "boolean" &&
|
|
335
|
+
(!call.mcpLedgerSettlementPersisted || call.mcpLedgerPrewritePersisted) &&
|
|
336
|
+
timestampsValid &&
|
|
337
|
+
!ids.has(call.id) &&
|
|
338
|
+
!identities.has(identityKey);
|
|
339
|
+
const providerValid =
|
|
340
|
+
call?.kind !== "provider" ||
|
|
341
|
+
(call.protocol === WORKFLOW_PROVIDER_ATTEMPT_PROTOCOL &&
|
|
342
|
+
WORKFLOW_PROVIDER_CALL_ID_RE.test(call.callId || "") &&
|
|
343
|
+
call.childEffectId === null &&
|
|
344
|
+
PROVIDER_NAME_RE.test(call.name || "") &&
|
|
345
|
+
["model", "semantic-compaction", "subagent"].includes(call.source) &&
|
|
346
|
+
["model", "semantic-compaction"].includes(requestSource) &&
|
|
347
|
+
(call.source === "subagent"
|
|
348
|
+
? requestSource === "model"
|
|
349
|
+
: requestSource === call.source) &&
|
|
350
|
+
WORKFLOW_PROVIDER_REQUEST_ID_RE.test(call.clientRequestId || "") &&
|
|
351
|
+
call.clientRequestId ===
|
|
352
|
+
expectedWorkflowProviderRequestId(
|
|
353
|
+
ownerEffectId,
|
|
354
|
+
requestSource,
|
|
355
|
+
call.sequence,
|
|
356
|
+
) &&
|
|
357
|
+
call.identitySemantics === "trace-only" &&
|
|
358
|
+
(call.providerReceiptPersisted === false ||
|
|
359
|
+
["completed", "outcome_unknown"].includes(call.status)) &&
|
|
360
|
+
call.mcpLedgerId === null &&
|
|
361
|
+
call.mcpLedgerPrewritePersisted === false &&
|
|
362
|
+
call.mcpLedgerSettlementPersisted === false);
|
|
363
|
+
const toolValid =
|
|
364
|
+
call?.kind !== "tool" ||
|
|
365
|
+
(call.protocol === WORKFLOW_CHILD_EFFECT_PROTOCOL &&
|
|
366
|
+
WORKFLOW_TOOL_CALL_ID_RE.test(call.callId || "") &&
|
|
367
|
+
SHA256_RE.test(call.childEffectId || "") &&
|
|
368
|
+
call.childEffectId ===
|
|
369
|
+
expectedNestedToolEffectId(
|
|
370
|
+
ownerEffectId,
|
|
371
|
+
call.sequence,
|
|
372
|
+
call.callId,
|
|
373
|
+
call.name,
|
|
374
|
+
) &&
|
|
375
|
+
WORKFLOW_TOOL_NAME_RE.test(call.name || "") &&
|
|
376
|
+
call.source === "tool" &&
|
|
377
|
+
requestSource === null &&
|
|
378
|
+
call.clientRequestId === null &&
|
|
379
|
+
call.providerReceiptPersisted === false &&
|
|
380
|
+
call.providerReceiptRequestId === null &&
|
|
381
|
+
call.providerReceiptResponseId === null &&
|
|
382
|
+
call.identitySemantics === "runtime-derived");
|
|
383
|
+
const settlementValid = terminal
|
|
384
|
+
? call.settlementCode !== null || call.status === "completed"
|
|
385
|
+
: call.settlementCode === null &&
|
|
386
|
+
call.outcomeUnknown === false &&
|
|
387
|
+
call.providerReceiptPersisted === false &&
|
|
388
|
+
call.providerReceiptRequestId === null &&
|
|
389
|
+
call.providerReceiptResponseId === null &&
|
|
390
|
+
call.mcpLedgerId === null &&
|
|
391
|
+
call.mcpLedgerPrewritePersisted === false &&
|
|
392
|
+
call.mcpLedgerSettlementPersisted === false;
|
|
393
|
+
const callValid =
|
|
394
|
+
commonValid && providerValid && toolValid && settlementValid;
|
|
395
|
+
if (!callValid) {
|
|
396
|
+
issues.push(`effect-${effectIndex}-call-${callIndex}-invalid`);
|
|
397
|
+
}
|
|
398
|
+
if (
|
|
399
|
+
callValid &&
|
|
400
|
+
call.kind === "tool" &&
|
|
401
|
+
DESCENDANT_OWNER_TOOL_NAMES.has(call.name)
|
|
402
|
+
) {
|
|
403
|
+
authorizedOwnerEffectIds.add(call.childEffectId);
|
|
404
|
+
}
|
|
405
|
+
ids.add(call?.id);
|
|
406
|
+
identities.add(identityKey);
|
|
407
|
+
}
|
|
408
|
+
if (
|
|
409
|
+
effect?.status === "settled" &&
|
|
410
|
+
calls.some((call) => call?.status === "started")
|
|
411
|
+
) {
|
|
412
|
+
issues.push(`effect-${effectIndex}-calls-unsettled`);
|
|
413
|
+
}
|
|
414
|
+
if (calls.length > 0 && typeof effect?.providerDispatchedAt !== "string") {
|
|
415
|
+
issues.push(`effect-${effectIndex}-calls-without-dispatch`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function verifyDynamicWorkflowRuntimeState(value) {
|
|
420
|
+
const state = snapshotJson(value, "workflow runtime state");
|
|
421
|
+
const issues = [];
|
|
422
|
+
if (
|
|
423
|
+
state.schema !== DYNAMIC_WORKFLOW_RUNTIME_SCHEMA ||
|
|
424
|
+
state.version !== DYNAMIC_WORKFLOW_RUNTIME_VERSION ||
|
|
425
|
+
!STATE_STATUSES.has(state.status)
|
|
426
|
+
) {
|
|
427
|
+
issues.push("identity-invalid");
|
|
428
|
+
}
|
|
429
|
+
try {
|
|
430
|
+
safeId(state.runId, "runId");
|
|
431
|
+
safeId(state.workflowId, "workflowId", 128);
|
|
432
|
+
} catch {
|
|
433
|
+
issues.push("run-binding-invalid");
|
|
434
|
+
}
|
|
435
|
+
if (
|
|
436
|
+
!SHA256_RE.test(state.definitionDigest || "") ||
|
|
437
|
+
!SHA256_RE.test(state.admissionDigest || "") ||
|
|
438
|
+
typeof state.executionAuthoritySessionId !== "string" ||
|
|
439
|
+
state.executionAuthoritySessionId.length === 0 ||
|
|
440
|
+
state.executionAuthoritySessionId.length > 256
|
|
441
|
+
) {
|
|
442
|
+
issues.push("authority-binding-invalid");
|
|
443
|
+
}
|
|
444
|
+
if (
|
|
445
|
+
!Number.isSafeInteger(state.revision) ||
|
|
446
|
+
state.revision < 1 ||
|
|
447
|
+
state.revision !== state.lineage?.length
|
|
448
|
+
) {
|
|
449
|
+
issues.push("revision-invalid");
|
|
450
|
+
}
|
|
451
|
+
verifyLineage(state, issues);
|
|
452
|
+
if (!Array.isArray(state.effects) || state.effects.length > MAX_EFFECTS) {
|
|
453
|
+
issues.push("effects-invalid");
|
|
454
|
+
} else {
|
|
455
|
+
const ids = new Set();
|
|
456
|
+
const keys = new Set();
|
|
457
|
+
const batches = new Map();
|
|
458
|
+
for (const [index, effect] of state.effects.entries()) {
|
|
459
|
+
let expectedKey = null;
|
|
460
|
+
let expectedId = null;
|
|
461
|
+
try {
|
|
462
|
+
expectedKey = effectKey(effect);
|
|
463
|
+
expectedId = digest("chainlesschain.dynamic-workflow.effect-id.v1\0", {
|
|
464
|
+
runId: state.runId,
|
|
465
|
+
key: expectedKey,
|
|
466
|
+
payloadDigest: effect?.payloadDigest,
|
|
467
|
+
});
|
|
468
|
+
} catch {
|
|
469
|
+
// The common invalid-effect branch below records the stable issue.
|
|
470
|
+
}
|
|
471
|
+
const resultDigest =
|
|
472
|
+
effect?.result == null ? null : effectResultDigest(effect.result);
|
|
473
|
+
const batchFields = [
|
|
474
|
+
effect?.batchId,
|
|
475
|
+
effect?.batchIndex,
|
|
476
|
+
effect?.batchSize,
|
|
477
|
+
];
|
|
478
|
+
const hasBatchMetadata = batchFields.some((field) => field !== undefined);
|
|
479
|
+
const batchMetadataValid =
|
|
480
|
+
!hasBatchMetadata ||
|
|
481
|
+
(batchFields.every((field) => field !== undefined) &&
|
|
482
|
+
SHA256_RE.test(effect.batchId || "") &&
|
|
483
|
+
Number.isSafeInteger(effect.batchIndex) &&
|
|
484
|
+
effect.batchIndex >= 0 &&
|
|
485
|
+
Number.isSafeInteger(effect.batchSize) &&
|
|
486
|
+
effect.batchSize >= 1 &&
|
|
487
|
+
effect.batchSize <= MAX_EFFECT_BATCH_SIZE &&
|
|
488
|
+
effect.batchIndex < effect.batchSize);
|
|
489
|
+
const hasDispatchMetadata = hasProviderDispatchMetadata(effect);
|
|
490
|
+
const dispatchFields = [
|
|
491
|
+
effect?.providerDispatchedAt,
|
|
492
|
+
effect?.timeoutMs,
|
|
493
|
+
effect?.timeoutObservedAt,
|
|
494
|
+
];
|
|
495
|
+
const providerDispatchedAtValid =
|
|
496
|
+
effect?.providerDispatchedAt === null ||
|
|
497
|
+
(typeof effect?.providerDispatchedAt === "string" &&
|
|
498
|
+
isoNow(effect.providerDispatchedAt) === effect.providerDispatchedAt &&
|
|
499
|
+
Date.parse(effect.providerDispatchedAt) >=
|
|
500
|
+
Date.parse(effect.requestedAt));
|
|
501
|
+
const timeoutObservedAtValid =
|
|
502
|
+
effect?.timeoutObservedAt === null ||
|
|
503
|
+
(typeof effect?.timeoutObservedAt === "string" &&
|
|
504
|
+
Number(effect?.timeoutMs) > 0 &&
|
|
505
|
+
isoNow(effect.timeoutObservedAt) === effect.timeoutObservedAt &&
|
|
506
|
+
Date.parse(effect.timeoutObservedAt) >=
|
|
507
|
+
Date.parse(effect.requestedAt) &&
|
|
508
|
+
(effect.providerDispatchedAt === null ||
|
|
509
|
+
Date.parse(effect.timeoutObservedAt) >=
|
|
510
|
+
Date.parse(effect.providerDispatchedAt)));
|
|
511
|
+
const dispatchMetadataValid =
|
|
512
|
+
!hasDispatchMetadata ||
|
|
513
|
+
(dispatchFields.every((field) => field !== undefined) &&
|
|
514
|
+
Number.isFinite(effect.timeoutMs) &&
|
|
515
|
+
effect.timeoutMs >= 0 &&
|
|
516
|
+
providerDispatchedAtValid &&
|
|
517
|
+
timeoutObservedAtValid);
|
|
518
|
+
if (
|
|
519
|
+
effect?.schema !== DYNAMIC_WORKFLOW_EFFECT_SCHEMA ||
|
|
520
|
+
!SHA256_RE.test(effect.id || "") ||
|
|
521
|
+
!SHA256_RE.test(effect.payloadDigest || "") ||
|
|
522
|
+
!["pending", "settled"].includes(effect.status) ||
|
|
523
|
+
typeof effect.key !== "string" ||
|
|
524
|
+
effect.key.length === 0 ||
|
|
525
|
+
effect.key.length > 1024 ||
|
|
526
|
+
effect.key !== expectedKey ||
|
|
527
|
+
effect.id !== expectedId ||
|
|
528
|
+
isoNow(effect.requestedAt) !== effect.requestedAt ||
|
|
529
|
+
ids.has(effect.id) ||
|
|
530
|
+
keys.has(effect.key) ||
|
|
531
|
+
!batchMetadataValid ||
|
|
532
|
+
!dispatchMetadataValid ||
|
|
533
|
+
(effect.status === "pending" &&
|
|
534
|
+
(effect.result !== null ||
|
|
535
|
+
effect.resultDigest !== null ||
|
|
536
|
+
effect.settlementAuthority !== null ||
|
|
537
|
+
effect.settledAt !== null)) ||
|
|
538
|
+
(effect.status === "settled" &&
|
|
539
|
+
(effect.result == null ||
|
|
540
|
+
effect.resultDigest !== resultDigest ||
|
|
541
|
+
!SETTLEMENT_AUTHORITIES.has(effect.settlementAuthority) ||
|
|
542
|
+
(hasDispatchMetadata &&
|
|
543
|
+
effect.settlementAuthority === "runtime-not-dispatched" &&
|
|
544
|
+
(effect.providerDispatchedAt !== null ||
|
|
545
|
+
effect.result?.status !== "failed" ||
|
|
546
|
+
effect.result?.result?.providerDispatched !== false ||
|
|
547
|
+
![
|
|
548
|
+
"timeout-before-dispatch",
|
|
549
|
+
"stopped-before-dispatch",
|
|
550
|
+
].includes(effect.result?.result?.reason) ||
|
|
551
|
+
(effect.result?.result?.reason ===
|
|
552
|
+
"timeout-before-dispatch") !==
|
|
553
|
+
(typeof effect.timeoutObservedAt === "string"))) ||
|
|
554
|
+
(hasDispatchMetadata &&
|
|
555
|
+
effect.settlementAuthority !== "runtime-not-dispatched" &&
|
|
556
|
+
typeof effect.providerDispatchedAt !== "string") ||
|
|
557
|
+
(hasDispatchMetadata &&
|
|
558
|
+
typeof effect.providerDispatchedAt === "string" &&
|
|
559
|
+
Date.parse(effect.providerDispatchedAt) >
|
|
560
|
+
Date.parse(effect.settledAt)) ||
|
|
561
|
+
(hasDispatchMetadata &&
|
|
562
|
+
typeof effect.timeoutObservedAt === "string" &&
|
|
563
|
+
Date.parse(effect.timeoutObservedAt) >
|
|
564
|
+
Date.parse(effect.settledAt)) ||
|
|
565
|
+
Date.parse(effect.settledAt) < Date.parse(effect.requestedAt) ||
|
|
566
|
+
isoNow(effect.settledAt) !== effect.settledAt))
|
|
567
|
+
) {
|
|
568
|
+
issues.push(`effect-${index}-invalid`);
|
|
569
|
+
}
|
|
570
|
+
ids.add(effect?.id);
|
|
571
|
+
keys.add(effect?.key);
|
|
572
|
+
verifyEffectCalls(effect, index, issues);
|
|
573
|
+
if (batchMetadataValid && hasBatchMetadata) {
|
|
574
|
+
if (!batches.has(effect.batchId)) batches.set(effect.batchId, []);
|
|
575
|
+
batches.get(effect.batchId).push(effect);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
let batchIndex = 0;
|
|
579
|
+
for (const [batchId, entries] of batches) {
|
|
580
|
+
entries.sort((left, right) => left.batchIndex - right.batchIndex);
|
|
581
|
+
const expectedSize = entries[0]?.batchSize;
|
|
582
|
+
const complete =
|
|
583
|
+
entries.length === expectedSize &&
|
|
584
|
+
entries.every(
|
|
585
|
+
(entry, index) =>
|
|
586
|
+
entry.batchSize === expectedSize &&
|
|
587
|
+
entry.batchIndex === index &&
|
|
588
|
+
entry.requestedAt === entries[0].requestedAt,
|
|
589
|
+
);
|
|
590
|
+
const expectedBatchId = complete
|
|
591
|
+
? effectBatchId(state.runId, entries)
|
|
592
|
+
: null;
|
|
593
|
+
if (!complete || batchId !== expectedBatchId) {
|
|
594
|
+
issues.push(`effect-batch-${batchIndex}-invalid`);
|
|
595
|
+
}
|
|
596
|
+
batchIndex += 1;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (
|
|
600
|
+
(state.status === "completed") !== (state.finalRecord != null) ||
|
|
601
|
+
(state.status === "stopped" && state.finalRecord != null)
|
|
602
|
+
) {
|
|
603
|
+
issues.push("terminal-record-invalid");
|
|
604
|
+
}
|
|
605
|
+
if (
|
|
606
|
+
isoNow(state.createdAt) !== state.createdAt ||
|
|
607
|
+
isoNow(state.updatedAt) !== state.updatedAt
|
|
608
|
+
) {
|
|
609
|
+
issues.push("timestamp-invalid");
|
|
610
|
+
}
|
|
611
|
+
const expectedStateDigest = digest(
|
|
612
|
+
"chainlesschain.dynamic-workflow.runtime-state.v1\0",
|
|
613
|
+
stateMaterial(state),
|
|
614
|
+
);
|
|
615
|
+
if (state.stateDigest !== expectedStateDigest)
|
|
616
|
+
issues.push("state-digest-invalid");
|
|
617
|
+
if (issues.length > 0) {
|
|
618
|
+
const error = new Error(
|
|
619
|
+
`dynamic workflow runtime state is invalid: ${issues.join(", ")}`,
|
|
620
|
+
);
|
|
621
|
+
error.code = "CC_DYNAMIC_WORKFLOW_RUNTIME_STATE_INVALID";
|
|
622
|
+
error.issues = issues;
|
|
623
|
+
throw error;
|
|
624
|
+
}
|
|
625
|
+
return Object.freeze(state);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function parseStateBytes(raw) {
|
|
629
|
+
const text =
|
|
630
|
+
typeof raw === "string" ? raw : Buffer.from(raw).toString("utf8");
|
|
631
|
+
const bytes = Buffer.byteLength(text, "utf8");
|
|
632
|
+
if (bytes <= 0 || bytes > MAX_STATE_BYTES) {
|
|
633
|
+
throw new Error("dynamic workflow runtime state exceeds its byte limit");
|
|
634
|
+
}
|
|
635
|
+
let value;
|
|
636
|
+
try {
|
|
637
|
+
value = JSON.parse(text);
|
|
638
|
+
} catch {
|
|
639
|
+
throw new Error("dynamic workflow runtime state is corrupt");
|
|
640
|
+
}
|
|
641
|
+
return verifyDynamicWorkflowRuntimeState(value);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function readStateFile(statePath) {
|
|
645
|
+
try {
|
|
646
|
+
fs.lstatSync(statePath);
|
|
647
|
+
} catch (error) {
|
|
648
|
+
if (error?.code === "ENOENT") return null;
|
|
649
|
+
throw error;
|
|
650
|
+
}
|
|
651
|
+
return withTrustedFileParentSync(
|
|
652
|
+
fs,
|
|
653
|
+
statePath,
|
|
654
|
+
({ canonicalPath, parentDevice }) => {
|
|
655
|
+
const before = fs.lstatSync(canonicalPath, { bigint: true });
|
|
656
|
+
if (
|
|
657
|
+
!before.isFile() ||
|
|
658
|
+
before.isSymbolicLink() ||
|
|
659
|
+
Number(before.nlink) !== 1 ||
|
|
660
|
+
Number(before.size) <= 0 ||
|
|
661
|
+
Number(before.size) > MAX_STATE_BYTES
|
|
662
|
+
) {
|
|
663
|
+
throw new Error(
|
|
664
|
+
"dynamic workflow runtime state must be a bounded, regular, single-link file",
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
let descriptor;
|
|
668
|
+
try {
|
|
669
|
+
let flags = Number(fs.constants.O_RDONLY || 0);
|
|
670
|
+
if (typeof fs.constants.O_NOFOLLOW === "number") {
|
|
671
|
+
flags |= fs.constants.O_NOFOLLOW;
|
|
672
|
+
}
|
|
673
|
+
descriptor = fs.openSync(canonicalPath, flags);
|
|
674
|
+
const opened = fs.fstatSync(descriptor, { bigint: true });
|
|
675
|
+
if (
|
|
676
|
+
!opened.isFile() ||
|
|
677
|
+
Number(opened.nlink) !== 1 ||
|
|
678
|
+
!samePathHandleFileIdentity(before, opened, parentDevice)
|
|
679
|
+
) {
|
|
680
|
+
throw new Error(
|
|
681
|
+
"dynamic workflow runtime state identity changed while opening",
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
const state = parseStateBytes(fs.readFileSync(descriptor, "utf8"));
|
|
685
|
+
const after = fs.fstatSync(descriptor, { bigint: true });
|
|
686
|
+
if (!sameFileStatIdentity(opened, after)) {
|
|
687
|
+
throw new Error(
|
|
688
|
+
"dynamic workflow runtime state changed while reading",
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
return state;
|
|
692
|
+
} finally {
|
|
693
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
694
|
+
}
|
|
695
|
+
},
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function replaceObject(target, value) {
|
|
700
|
+
for (const key of Object.keys(target)) delete target[key];
|
|
701
|
+
Object.assign(target, value);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function withStateMutation(statePath, mutator) {
|
|
705
|
+
fs.mkdirSync(path.dirname(statePath), { recursive: true, mode: 0o700 });
|
|
706
|
+
return withFileLock(
|
|
707
|
+
statePath,
|
|
708
|
+
() => {
|
|
709
|
+
const current = readStateFile(statePath);
|
|
710
|
+
const result = mutator(current);
|
|
711
|
+
if (result?.state) {
|
|
712
|
+
const verified = verifyDynamicWorkflowRuntimeState(result.state);
|
|
713
|
+
writeSecurityStore(statePath, "dynamic workflow runtime", verified);
|
|
714
|
+
}
|
|
715
|
+
return result?.value;
|
|
716
|
+
},
|
|
717
|
+
{ failIfUnavailable: true, timeoutMs: 5000, staleMs: 30000 },
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
export function dynamicWorkflowRunStatePath(cwd, runId) {
|
|
722
|
+
const safeRunId = safeId(runId, "runId");
|
|
723
|
+
return path.join(
|
|
724
|
+
path.resolve(cwd),
|
|
725
|
+
".chainlesschain",
|
|
726
|
+
"cowork",
|
|
727
|
+
"workflow-runs",
|
|
728
|
+
`${safeRunId}.json`,
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
export function readDynamicWorkflowRuntimeState(statePath) {
|
|
733
|
+
const state = readStateFile(path.resolve(statePath));
|
|
734
|
+
if (!state) {
|
|
735
|
+
const error = new Error("dynamic workflow runtime state was not found");
|
|
736
|
+
error.code = "CC_DYNAMIC_WORKFLOW_RUNTIME_NOT_FOUND";
|
|
737
|
+
throw error;
|
|
738
|
+
}
|
|
739
|
+
return state;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
export function readDynamicWorkflowEffectResultFile(filePath) {
|
|
743
|
+
const resolved = path.resolve(filePath);
|
|
744
|
+
return withTrustedFileParentSync(
|
|
745
|
+
fs,
|
|
746
|
+
resolved,
|
|
747
|
+
({ canonicalPath, parentDevice }) => {
|
|
748
|
+
const before = fs.lstatSync(canonicalPath, { bigint: true });
|
|
749
|
+
if (
|
|
750
|
+
!before.isFile() ||
|
|
751
|
+
before.isSymbolicLink() ||
|
|
752
|
+
Number(before.nlink) !== 1 ||
|
|
753
|
+
Number(before.size) <= 0 ||
|
|
754
|
+
Number(before.size) > 1024 * 1024
|
|
755
|
+
) {
|
|
756
|
+
throw new Error(
|
|
757
|
+
"workflow effect result must be a bounded, regular, single-link file",
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
let descriptor;
|
|
761
|
+
try {
|
|
762
|
+
let flags = Number(fs.constants.O_RDONLY || 0);
|
|
763
|
+
if (typeof fs.constants.O_NOFOLLOW === "number") {
|
|
764
|
+
flags |= fs.constants.O_NOFOLLOW;
|
|
765
|
+
}
|
|
766
|
+
descriptor = fs.openSync(canonicalPath, flags);
|
|
767
|
+
const opened = fs.fstatSync(descriptor, { bigint: true });
|
|
768
|
+
if (
|
|
769
|
+
!opened.isFile() ||
|
|
770
|
+
Number(opened.nlink) !== 1 ||
|
|
771
|
+
!samePathHandleFileIdentity(before, opened, parentDevice)
|
|
772
|
+
) {
|
|
773
|
+
throw new Error(
|
|
774
|
+
"workflow effect result identity changed while opening",
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
let parsed;
|
|
778
|
+
try {
|
|
779
|
+
parsed = JSON.parse(fs.readFileSync(descriptor, "utf8"));
|
|
780
|
+
} catch {
|
|
781
|
+
throw new Error("workflow effect result is not valid JSON");
|
|
782
|
+
}
|
|
783
|
+
const result = snapshotJson(
|
|
784
|
+
parsed,
|
|
785
|
+
"workflow reconciled effect result",
|
|
786
|
+
1024 * 1024,
|
|
787
|
+
);
|
|
788
|
+
if (containsSecret(JSON.stringify(result))) {
|
|
789
|
+
throw new Error("workflow effect result contains secret-shaped data");
|
|
790
|
+
}
|
|
791
|
+
const after = fs.fstatSync(descriptor, { bigint: true });
|
|
792
|
+
if (!sameFileStatIdentity(opened, after)) {
|
|
793
|
+
throw new Error("workflow effect result changed while being read");
|
|
794
|
+
}
|
|
795
|
+
return result;
|
|
796
|
+
} finally {
|
|
797
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
798
|
+
}
|
|
799
|
+
},
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function stateBindings(execution, runId) {
|
|
804
|
+
const admission = execution.runAdmission;
|
|
805
|
+
if (!admission || typeof admission !== "object") {
|
|
806
|
+
throw new TypeError("durable workflow execution requires run admission");
|
|
807
|
+
}
|
|
808
|
+
if (
|
|
809
|
+
!Number.isSafeInteger(admission.maxParallel) ||
|
|
810
|
+
admission.maxParallel < 1 ||
|
|
811
|
+
admission.maxParallel > MAX_EFFECT_BATCH_SIZE
|
|
812
|
+
) {
|
|
813
|
+
throw new TypeError("durable workflow maxParallel admission is invalid");
|
|
814
|
+
}
|
|
815
|
+
const generation = execution.workflow?.facade?.generation;
|
|
816
|
+
const review = execution.workflow?.facade?.review;
|
|
817
|
+
if (generation != null || review != null) {
|
|
818
|
+
if (
|
|
819
|
+
generation?.schema !== "cc-dynamic-workflow-generation/v1" ||
|
|
820
|
+
review?.schema !== "cc-dynamic-workflow-review/v1" ||
|
|
821
|
+
review?.decision !== "accepted" ||
|
|
822
|
+
!SHA256_RE.test(review?.draftDigest || "") ||
|
|
823
|
+
!SHA256_RE.test(review?.sourceDefinitionDigest || "") ||
|
|
824
|
+
typeof review?.reviewer !== "string" ||
|
|
825
|
+
review.reviewer.length === 0
|
|
826
|
+
) {
|
|
827
|
+
const error = new Error(
|
|
828
|
+
"model-generated durable workflow requires accepted review authority",
|
|
829
|
+
);
|
|
830
|
+
error.code = "CC_DYNAMIC_WORKFLOW_REVIEW_AUTHORITY_REQUIRED";
|
|
831
|
+
throw error;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return {
|
|
835
|
+
runId: safeId(runId, "runId"),
|
|
836
|
+
workflowId: safeId(execution.workflow?.id, "workflowId", 128),
|
|
837
|
+
definitionDigest: execution.definitionDigest,
|
|
838
|
+
admissionDigest: admission.admissionDigest,
|
|
839
|
+
executionAuthoritySessionId:
|
|
840
|
+
admission.executionLocation?.session?.sessionId,
|
|
841
|
+
executionPolicy: snapshotJson(
|
|
842
|
+
admission.executionPolicy,
|
|
843
|
+
"workflow execution policy",
|
|
844
|
+
64 * 1024,
|
|
845
|
+
),
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function bindingsMatch(state, bindings) {
|
|
850
|
+
return (
|
|
851
|
+
[
|
|
852
|
+
"runId",
|
|
853
|
+
"workflowId",
|
|
854
|
+
"definitionDigest",
|
|
855
|
+
"admissionDigest",
|
|
856
|
+
"executionAuthoritySessionId",
|
|
857
|
+
].every((field) => state[field] === bindings[field]) &&
|
|
858
|
+
canonicalJson(state.executionPolicy) ===
|
|
859
|
+
canonicalJson(bindings.executionPolicy)
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function createState(bindings, now) {
|
|
864
|
+
const at = isoNow(now);
|
|
865
|
+
const state = {
|
|
866
|
+
schema: DYNAMIC_WORKFLOW_RUNTIME_SCHEMA,
|
|
867
|
+
version: DYNAMIC_WORKFLOW_RUNTIME_VERSION,
|
|
868
|
+
...bindings,
|
|
869
|
+
revision: 0,
|
|
870
|
+
status: "ready",
|
|
871
|
+
effects: [],
|
|
872
|
+
finalRecord: null,
|
|
873
|
+
createdAt: at,
|
|
874
|
+
updatedAt: at,
|
|
875
|
+
lineage: [],
|
|
876
|
+
};
|
|
877
|
+
appendLineage(
|
|
878
|
+
state,
|
|
879
|
+
"run-created",
|
|
880
|
+
{ definitionDigest: bindings.definitionDigest },
|
|
881
|
+
at,
|
|
882
|
+
);
|
|
883
|
+
return finalizeState(state);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function controlError(message, reason, state, effect = null, cause = null) {
|
|
887
|
+
const error = new Error(message, cause ? { cause } : undefined);
|
|
888
|
+
error.code = DYNAMIC_WORKFLOW_RUNTIME_CONTROL_CODE;
|
|
889
|
+
error.reason = reason;
|
|
890
|
+
error.runtimeState = state;
|
|
891
|
+
if (effect) error.pendingEffect = effect;
|
|
892
|
+
return error;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function startRun(statePath, bindings, now) {
|
|
896
|
+
return withStateMutation(statePath, (current) => {
|
|
897
|
+
let state = current || createState(bindings, now);
|
|
898
|
+
if (!bindingsMatch(state, bindings)) {
|
|
899
|
+
throw new Error(
|
|
900
|
+
"durable workflow run authority does not match stored state",
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
if (state.status === "completed") return { value: state };
|
|
904
|
+
if (state.status !== "ready") {
|
|
905
|
+
throw new Error(
|
|
906
|
+
`durable workflow run is ${state.status}; use explicit control or reconciliation`,
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
state = transition(
|
|
910
|
+
state,
|
|
911
|
+
"run-started",
|
|
912
|
+
{},
|
|
913
|
+
(draft) => {
|
|
914
|
+
draft.status = "running";
|
|
915
|
+
},
|
|
916
|
+
now,
|
|
917
|
+
);
|
|
918
|
+
return { state, value: state };
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function effectKey(effect) {
|
|
923
|
+
const stepId = String(effect?.stepId || "");
|
|
924
|
+
const iteration = Number(effect?.iteration);
|
|
925
|
+
const attempt = Number(effect?.attempt);
|
|
926
|
+
if (
|
|
927
|
+
!stepId ||
|
|
928
|
+
stepId.length > 512 ||
|
|
929
|
+
!Number.isSafeInteger(iteration) ||
|
|
930
|
+
iteration < 1 ||
|
|
931
|
+
!Number.isSafeInteger(attempt) ||
|
|
932
|
+
attempt < 1
|
|
933
|
+
) {
|
|
934
|
+
throw new TypeError("workflow effect context is invalid");
|
|
935
|
+
}
|
|
936
|
+
return `${stepId}\u0000${iteration}\u0000${attempt}`;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function effectPayload(args) {
|
|
940
|
+
const { signal: _signal, workflowEffect: _workflowEffect, ...payload } = args;
|
|
941
|
+
return snapshotJson(payload, "workflow effect payload", 1024 * 1024);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function prepareEffectRequest(args) {
|
|
945
|
+
const key = effectKey(args.workflowEffect);
|
|
946
|
+
const stepId = String(args.workflowEffect.stepId);
|
|
947
|
+
const iteration = Number(args.workflowEffect.iteration);
|
|
948
|
+
const attempt = Number(args.workflowEffect.attempt);
|
|
949
|
+
const timeoutMs = Number(args.workflowEffect.timeoutMs || 0);
|
|
950
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
|
|
951
|
+
throw new TypeError("workflow effect timeout context is invalid");
|
|
952
|
+
}
|
|
953
|
+
const payload = effectPayload(args);
|
|
954
|
+
const payloadDigest = digest(
|
|
955
|
+
"chainlesschain.dynamic-workflow.effect-payload.v1\0",
|
|
956
|
+
payload,
|
|
957
|
+
);
|
|
958
|
+
return { key, stepId, iteration, attempt, timeoutMs, payloadDigest };
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function requestEffectBatch(statePath, runId, argsList, now) {
|
|
962
|
+
if (!Array.isArray(argsList) || argsList.length === 0) {
|
|
963
|
+
throw new TypeError("workflow effect request batch is empty");
|
|
964
|
+
}
|
|
965
|
+
if (argsList.length > MAX_EFFECT_BATCH_SIZE) {
|
|
966
|
+
throw new Error("workflow effect request batch exceeds its hard limit");
|
|
967
|
+
}
|
|
968
|
+
const requests = argsList.map((args) => ({
|
|
969
|
+
...prepareEffectRequest(args),
|
|
970
|
+
}));
|
|
971
|
+
const batchKeys = new Set(requests.map((request) => request.key));
|
|
972
|
+
if (batchKeys.size !== requests.length) {
|
|
973
|
+
throw new Error("workflow effect request batch contains duplicate keys");
|
|
974
|
+
}
|
|
975
|
+
return withStateMutation(statePath, (current) => {
|
|
976
|
+
if (!current || current.runId !== runId) {
|
|
977
|
+
throw new Error(
|
|
978
|
+
"durable workflow run disappeared before effect batch request",
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
if (current.status === "pause_requested") {
|
|
982
|
+
return {
|
|
983
|
+
value: { control: "paused", state: current, results: null },
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
if (current.status === "stopped") {
|
|
987
|
+
return {
|
|
988
|
+
value: { control: "stopped", state: current, results: null },
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
if (current.status !== "running") {
|
|
992
|
+
throw new Error(
|
|
993
|
+
`durable workflow run cannot request an effect batch while ${current.status}`,
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
const existingByKey = new Map(
|
|
997
|
+
current.effects.map((effect) => [effect.key, effect]),
|
|
998
|
+
);
|
|
999
|
+
for (const request of requests) {
|
|
1000
|
+
const existing = existingByKey.get(request.key);
|
|
1001
|
+
if (existing && existing.payloadDigest !== request.payloadDigest) {
|
|
1002
|
+
throw new Error("workflow effect payload drifted during replay");
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
const existingPending = requests
|
|
1006
|
+
.map((request) => existingByKey.get(request.key))
|
|
1007
|
+
.find(
|
|
1008
|
+
(effect) =>
|
|
1009
|
+
effect?.status === "pending" && !isKnownUndispatched(effect),
|
|
1010
|
+
);
|
|
1011
|
+
if (existingPending) {
|
|
1012
|
+
const state = transition(
|
|
1013
|
+
current,
|
|
1014
|
+
"effect-batch-reconciliation-required",
|
|
1015
|
+
{ effectId: existingPending.id },
|
|
1016
|
+
(draft) => {
|
|
1017
|
+
draft.status = "blocked";
|
|
1018
|
+
},
|
|
1019
|
+
now,
|
|
1020
|
+
);
|
|
1021
|
+
return {
|
|
1022
|
+
state,
|
|
1023
|
+
value: {
|
|
1024
|
+
state,
|
|
1025
|
+
results: requests.map(() => ({
|
|
1026
|
+
pending: true,
|
|
1027
|
+
effect: existingPending,
|
|
1028
|
+
state,
|
|
1029
|
+
})),
|
|
1030
|
+
},
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
const newRequests = [];
|
|
1034
|
+
const resultsByKey = new Map();
|
|
1035
|
+
for (const request of requests) {
|
|
1036
|
+
const existing = existingByKey.get(request.key);
|
|
1037
|
+
if (!existing) {
|
|
1038
|
+
newRequests.push(request);
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
if (existing.status === "pending" && isKnownUndispatched(existing)) {
|
|
1042
|
+
resultsByKey.set(request.key, {
|
|
1043
|
+
cached: false,
|
|
1044
|
+
effect: existing,
|
|
1045
|
+
});
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
1048
|
+
resultsByKey.set(request.key, {
|
|
1049
|
+
cached: true,
|
|
1050
|
+
effect: existing,
|
|
1051
|
+
result: existing.result,
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
if (current.effects.length + newRequests.length > MAX_EFFECTS) {
|
|
1055
|
+
throw new Error("durable workflow effect limit exceeded");
|
|
1056
|
+
}
|
|
1057
|
+
if (newRequests.length === 0) {
|
|
1058
|
+
return {
|
|
1059
|
+
value: {
|
|
1060
|
+
state: current,
|
|
1061
|
+
results: requests.map((request) => resultsByKey.get(request.key)),
|
|
1062
|
+
},
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
const ordered = [...newRequests].sort((left, right) =>
|
|
1066
|
+
left.key.localeCompare(right.key),
|
|
1067
|
+
);
|
|
1068
|
+
const batchId = effectBatchId(runId, ordered);
|
|
1069
|
+
const requestedAt = isoNow(now);
|
|
1070
|
+
const effects = ordered.map((request, batchIndex) => {
|
|
1071
|
+
const id = digest("chainlesschain.dynamic-workflow.effect-id.v1\0", {
|
|
1072
|
+
runId,
|
|
1073
|
+
key: request.key,
|
|
1074
|
+
payloadDigest: request.payloadDigest,
|
|
1075
|
+
});
|
|
1076
|
+
return {
|
|
1077
|
+
schema: DYNAMIC_WORKFLOW_EFFECT_SCHEMA,
|
|
1078
|
+
id,
|
|
1079
|
+
key: request.key,
|
|
1080
|
+
stepId: request.stepId,
|
|
1081
|
+
iteration: request.iteration,
|
|
1082
|
+
attempt: request.attempt,
|
|
1083
|
+
payloadDigest: request.payloadDigest,
|
|
1084
|
+
batchId,
|
|
1085
|
+
batchIndex,
|
|
1086
|
+
batchSize: ordered.length,
|
|
1087
|
+
timeoutMs: request.timeoutMs,
|
|
1088
|
+
timeoutObservedAt: null,
|
|
1089
|
+
providerDispatchedAt: null,
|
|
1090
|
+
calls: [],
|
|
1091
|
+
status: "pending",
|
|
1092
|
+
requestedAt,
|
|
1093
|
+
settledAt: null,
|
|
1094
|
+
settlementAuthority: null,
|
|
1095
|
+
resultDigest: null,
|
|
1096
|
+
result: null,
|
|
1097
|
+
};
|
|
1098
|
+
});
|
|
1099
|
+
for (const effect of effects) {
|
|
1100
|
+
resultsByKey.set(effect.key, { cached: false, effect });
|
|
1101
|
+
}
|
|
1102
|
+
const state = transition(
|
|
1103
|
+
current,
|
|
1104
|
+
"effect-batch-requested",
|
|
1105
|
+
{
|
|
1106
|
+
batchId,
|
|
1107
|
+
effectIds: effects.map((effect) => effect.id),
|
|
1108
|
+
effectCount: effects.length,
|
|
1109
|
+
},
|
|
1110
|
+
(draft) => {
|
|
1111
|
+
draft.effects.push(...effects);
|
|
1112
|
+
},
|
|
1113
|
+
now,
|
|
1114
|
+
);
|
|
1115
|
+
return {
|
|
1116
|
+
state,
|
|
1117
|
+
value: {
|
|
1118
|
+
state,
|
|
1119
|
+
results: requests.map((request) => resultsByKey.get(request.key)),
|
|
1120
|
+
},
|
|
1121
|
+
};
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function createEffectRequestBatcher(statePath, runId, now) {
|
|
1126
|
+
let queue = [];
|
|
1127
|
+
let scheduled = false;
|
|
1128
|
+
const flush = () => {
|
|
1129
|
+
scheduled = false;
|
|
1130
|
+
const batch = queue;
|
|
1131
|
+
queue = [];
|
|
1132
|
+
try {
|
|
1133
|
+
const requested = requestEffectBatch(
|
|
1134
|
+
statePath,
|
|
1135
|
+
runId,
|
|
1136
|
+
batch.map((entry) => entry.args),
|
|
1137
|
+
now,
|
|
1138
|
+
);
|
|
1139
|
+
for (let index = 0; index < batch.length; index += 1) {
|
|
1140
|
+
if (requested.control) batch[index].resolve(requested);
|
|
1141
|
+
else batch[index].resolve(requested.results[index]);
|
|
1142
|
+
}
|
|
1143
|
+
} catch (error) {
|
|
1144
|
+
for (const entry of batch) entry.reject(error);
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
return (args) =>
|
|
1148
|
+
new Promise((resolve, reject) => {
|
|
1149
|
+
queue.push({ args, resolve, reject });
|
|
1150
|
+
if (scheduled) return;
|
|
1151
|
+
scheduled = true;
|
|
1152
|
+
queueMicrotask(flush);
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function markEffectProviderDispatched(statePath, runId, effectId, now) {
|
|
1157
|
+
return withStateMutation(statePath, (current) => {
|
|
1158
|
+
if (!current || current.runId !== runId) {
|
|
1159
|
+
throw new Error("durable workflow run disappeared before dispatch");
|
|
1160
|
+
}
|
|
1161
|
+
const index = current.effects.findIndex((effect) => effect.id === effectId);
|
|
1162
|
+
const effect = current.effects[index];
|
|
1163
|
+
if (!effect || effect.status !== "pending") {
|
|
1164
|
+
throw new Error("workflow effect is not pending before dispatch");
|
|
1165
|
+
}
|
|
1166
|
+
if (!isKnownUndispatched(effect)) {
|
|
1167
|
+
throw new Error("workflow effect dispatch authority is unavailable");
|
|
1168
|
+
}
|
|
1169
|
+
if (current.status === "stopped") {
|
|
1170
|
+
return { value: { control: "stopped", state: current, effect } };
|
|
1171
|
+
}
|
|
1172
|
+
if (!["running", "pause_requested", "blocked"].includes(current.status)) {
|
|
1173
|
+
throw new Error(
|
|
1174
|
+
`workflow effect cannot be dispatched while ${current.status}`,
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
const providerDispatchedAt = isoNow(now);
|
|
1178
|
+
const state = transition(
|
|
1179
|
+
current,
|
|
1180
|
+
"effect-provider-dispatched",
|
|
1181
|
+
{ effectId, providerDispatchedAt },
|
|
1182
|
+
(draft) => {
|
|
1183
|
+
draft.effects[index] = {
|
|
1184
|
+
...draft.effects[index],
|
|
1185
|
+
providerDispatchedAt,
|
|
1186
|
+
};
|
|
1187
|
+
},
|
|
1188
|
+
now,
|
|
1189
|
+
);
|
|
1190
|
+
return {
|
|
1191
|
+
state,
|
|
1192
|
+
value: {
|
|
1193
|
+
effect: state.effects[index],
|
|
1194
|
+
state,
|
|
1195
|
+
},
|
|
1196
|
+
};
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function observeEffectTimeout(statePath, runId, effectId, now) {
|
|
1201
|
+
return withStateMutation(statePath, (current) => {
|
|
1202
|
+
if (!current || current.runId !== runId) return { value: current };
|
|
1203
|
+
const index = current.effects.findIndex((effect) => effect.id === effectId);
|
|
1204
|
+
const effect = current.effects[index];
|
|
1205
|
+
if (
|
|
1206
|
+
!effect ||
|
|
1207
|
+
effect.status !== "pending" ||
|
|
1208
|
+
!hasProviderDispatchMetadata(effect) ||
|
|
1209
|
+
typeof effect.providerDispatchedAt !== "string" ||
|
|
1210
|
+
!(effect.timeoutMs > 0) ||
|
|
1211
|
+
effect.timeoutObservedAt !== null
|
|
1212
|
+
) {
|
|
1213
|
+
return { value: current };
|
|
1214
|
+
}
|
|
1215
|
+
const timeoutObservedAt = isoNow(now);
|
|
1216
|
+
const state = transition(
|
|
1217
|
+
current,
|
|
1218
|
+
"effect-timeout-observed",
|
|
1219
|
+
{ effectId, timeoutMs: effect.timeoutMs, timeoutObservedAt },
|
|
1220
|
+
(draft) => {
|
|
1221
|
+
draft.effects[index] = {
|
|
1222
|
+
...draft.effects[index],
|
|
1223
|
+
timeoutObservedAt,
|
|
1224
|
+
};
|
|
1225
|
+
},
|
|
1226
|
+
now,
|
|
1227
|
+
);
|
|
1228
|
+
return { state, value: state };
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
function ownDataValue(value, property) {
|
|
1233
|
+
if (!value || (typeof value !== "object" && typeof value !== "function")) {
|
|
1234
|
+
return undefined;
|
|
1235
|
+
}
|
|
1236
|
+
try {
|
|
1237
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, property);
|
|
1238
|
+
return descriptor && Object.hasOwn(descriptor, "value")
|
|
1239
|
+
? descriptor.value
|
|
1240
|
+
: undefined;
|
|
1241
|
+
} catch {
|
|
1242
|
+
return undefined;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
function workflowCallAttempt(effectId, kind, event, now) {
|
|
1247
|
+
const startedAt = isoNow(now);
|
|
1248
|
+
const ownerEffectId = event?.workflowEffectId;
|
|
1249
|
+
if (kind === "provider") {
|
|
1250
|
+
const requestSource =
|
|
1251
|
+
event?.workflowRequestSource ||
|
|
1252
|
+
(event?.source === "subagent" ? "model" : event?.source);
|
|
1253
|
+
if (
|
|
1254
|
+
event?.type !== "model-usage-started" ||
|
|
1255
|
+
!SHA256_RE.test(ownerEffectId || "") ||
|
|
1256
|
+
!WORKFLOW_PROVIDER_CALL_ID_RE.test(event.callId || "") ||
|
|
1257
|
+
!PROVIDER_NAME_RE.test(event.provider || "") ||
|
|
1258
|
+
!["model", "semantic-compaction", "subagent"].includes(event.source) ||
|
|
1259
|
+
!["model", "semantic-compaction"].includes(requestSource) ||
|
|
1260
|
+
(event.source === "subagent"
|
|
1261
|
+
? requestSource !== "model"
|
|
1262
|
+
: requestSource !== event.source) ||
|
|
1263
|
+
!Number.isSafeInteger(event.callSequence) ||
|
|
1264
|
+
event.callSequence < 1 ||
|
|
1265
|
+
!WORKFLOW_PROVIDER_REQUEST_ID_RE.test(event.providerRequestId || "") ||
|
|
1266
|
+
event.providerRequestId !==
|
|
1267
|
+
expectedWorkflowProviderRequestId(
|
|
1268
|
+
ownerEffectId,
|
|
1269
|
+
requestSource,
|
|
1270
|
+
event.callSequence,
|
|
1271
|
+
) ||
|
|
1272
|
+
event.requestIdentitySemantics !== "trace-only"
|
|
1273
|
+
) {
|
|
1274
|
+
throw new TypeError("workflow provider call boundary is malformed");
|
|
1275
|
+
}
|
|
1276
|
+
return {
|
|
1277
|
+
schema: DYNAMIC_WORKFLOW_CALL_SCHEMA,
|
|
1278
|
+
id: workflowCallId(effectId, kind, event.callId, null, ownerEffectId),
|
|
1279
|
+
effectId,
|
|
1280
|
+
ownerEffectId,
|
|
1281
|
+
kind,
|
|
1282
|
+
protocol: WORKFLOW_PROVIDER_ATTEMPT_PROTOCOL,
|
|
1283
|
+
callId: event.callId,
|
|
1284
|
+
childEffectId: null,
|
|
1285
|
+
sequence: event.callSequence,
|
|
1286
|
+
name: event.provider,
|
|
1287
|
+
source: event.source,
|
|
1288
|
+
requestSource,
|
|
1289
|
+
clientRequestId: event.providerRequestId,
|
|
1290
|
+
identitySemantics: "trace-only",
|
|
1291
|
+
status: "started",
|
|
1292
|
+
startedAt,
|
|
1293
|
+
settledAt: null,
|
|
1294
|
+
outcomeUnknown: false,
|
|
1295
|
+
settlementCode: null,
|
|
1296
|
+
providerReceiptPersisted: false,
|
|
1297
|
+
providerReceiptRequestId: null,
|
|
1298
|
+
providerReceiptResponseId: null,
|
|
1299
|
+
mcpLedgerId: null,
|
|
1300
|
+
mcpLedgerPrewritePersisted: false,
|
|
1301
|
+
mcpLedgerSettlementPersisted: false,
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
if (
|
|
1305
|
+
kind !== "tool" ||
|
|
1306
|
+
event?.type !== "tool-executing" ||
|
|
1307
|
+
!SHA256_RE.test(ownerEffectId || "") ||
|
|
1308
|
+
event.workflowEffectProtocol !== WORKFLOW_CHILD_EFFECT_PROTOCOL ||
|
|
1309
|
+
!SHA256_RE.test(event.workflowChildEffectId || "") ||
|
|
1310
|
+
!Number.isSafeInteger(event.workflowChildSequence) ||
|
|
1311
|
+
event.workflowChildSequence < 1 ||
|
|
1312
|
+
!WORKFLOW_TOOL_CALL_ID_RE.test(event.tool_use_id || "") ||
|
|
1313
|
+
!WORKFLOW_TOOL_NAME_RE.test(event.tool || "") ||
|
|
1314
|
+
event.workflowChildEffectId !==
|
|
1315
|
+
expectedNestedToolEffectId(
|
|
1316
|
+
ownerEffectId,
|
|
1317
|
+
event.workflowChildSequence,
|
|
1318
|
+
event.tool_use_id,
|
|
1319
|
+
event.tool,
|
|
1320
|
+
)
|
|
1321
|
+
) {
|
|
1322
|
+
throw new TypeError("workflow tool call boundary is malformed");
|
|
1323
|
+
}
|
|
1324
|
+
return {
|
|
1325
|
+
schema: DYNAMIC_WORKFLOW_CALL_SCHEMA,
|
|
1326
|
+
id: workflowCallId(
|
|
1327
|
+
effectId,
|
|
1328
|
+
kind,
|
|
1329
|
+
event.tool_use_id,
|
|
1330
|
+
event.workflowChildEffectId,
|
|
1331
|
+
ownerEffectId,
|
|
1332
|
+
),
|
|
1333
|
+
effectId,
|
|
1334
|
+
ownerEffectId,
|
|
1335
|
+
kind,
|
|
1336
|
+
protocol: WORKFLOW_CHILD_EFFECT_PROTOCOL,
|
|
1337
|
+
callId: event.tool_use_id,
|
|
1338
|
+
childEffectId: event.workflowChildEffectId,
|
|
1339
|
+
sequence: event.workflowChildSequence,
|
|
1340
|
+
name: event.tool,
|
|
1341
|
+
source: "tool",
|
|
1342
|
+
requestSource: null,
|
|
1343
|
+
clientRequestId: null,
|
|
1344
|
+
identitySemantics: "runtime-derived",
|
|
1345
|
+
status: "started",
|
|
1346
|
+
startedAt,
|
|
1347
|
+
settledAt: null,
|
|
1348
|
+
outcomeUnknown: false,
|
|
1349
|
+
settlementCode: null,
|
|
1350
|
+
providerReceiptPersisted: false,
|
|
1351
|
+
providerReceiptRequestId: null,
|
|
1352
|
+
providerReceiptResponseId: null,
|
|
1353
|
+
mcpLedgerId: null,
|
|
1354
|
+
mcpLedgerPrewritePersisted: false,
|
|
1355
|
+
mcpLedgerSettlementPersisted: false,
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
function beginEffectCall(statePath, runId, effectId, kind, event, now) {
|
|
1360
|
+
const attempt = workflowCallAttempt(effectId, kind, event, now);
|
|
1361
|
+
return withStateMutation(statePath, (current) => {
|
|
1362
|
+
if (!current || current.runId !== runId) {
|
|
1363
|
+
throw new Error("durable workflow run disappeared before call boundary");
|
|
1364
|
+
}
|
|
1365
|
+
const index = current.effects.findIndex((effect) => effect.id === effectId);
|
|
1366
|
+
const effect = current.effects[index];
|
|
1367
|
+
const calls = effect?.calls || [];
|
|
1368
|
+
if (
|
|
1369
|
+
!effect ||
|
|
1370
|
+
effect.status !== "pending" ||
|
|
1371
|
+
typeof effect.providerDispatchedAt !== "string"
|
|
1372
|
+
) {
|
|
1373
|
+
throw new Error("workflow effect cannot begin a durable child call");
|
|
1374
|
+
}
|
|
1375
|
+
if (calls.length >= MAX_CALLS_PER_EFFECT) {
|
|
1376
|
+
throw new Error("workflow effect child-call limit exceeded");
|
|
1377
|
+
}
|
|
1378
|
+
if (
|
|
1379
|
+
attempt.ownerEffectId !== effectId &&
|
|
1380
|
+
!calls.some(
|
|
1381
|
+
(call) =>
|
|
1382
|
+
call.kind === "tool" &&
|
|
1383
|
+
call.childEffectId === attempt.ownerEffectId &&
|
|
1384
|
+
DESCENDANT_OWNER_TOOL_NAMES.has(call.name),
|
|
1385
|
+
)
|
|
1386
|
+
) {
|
|
1387
|
+
throw new Error("workflow descendant call owner is not authorized");
|
|
1388
|
+
}
|
|
1389
|
+
if (
|
|
1390
|
+
calls.some(
|
|
1391
|
+
(call) =>
|
|
1392
|
+
call.id === attempt.id ||
|
|
1393
|
+
(call.kind === attempt.kind &&
|
|
1394
|
+
(attempt.kind === "tool"
|
|
1395
|
+
? call.childEffectId === attempt.childEffectId
|
|
1396
|
+
: call.callId === attempt.callId)),
|
|
1397
|
+
)
|
|
1398
|
+
) {
|
|
1399
|
+
throw new Error("workflow child-call boundary is duplicated");
|
|
1400
|
+
}
|
|
1401
|
+
const state = transition(
|
|
1402
|
+
current,
|
|
1403
|
+
"effect-call-started",
|
|
1404
|
+
{
|
|
1405
|
+
effectId,
|
|
1406
|
+
ownerEffectId: attempt.ownerEffectId,
|
|
1407
|
+
callRecordId: attempt.id,
|
|
1408
|
+
kind,
|
|
1409
|
+
},
|
|
1410
|
+
(draft) => {
|
|
1411
|
+
draft.effects[index] = {
|
|
1412
|
+
...draft.effects[index],
|
|
1413
|
+
calls: [...(draft.effects[index].calls || []), attempt],
|
|
1414
|
+
};
|
|
1415
|
+
},
|
|
1416
|
+
now,
|
|
1417
|
+
);
|
|
1418
|
+
return { state, value: state.effects[index].calls.at(-1) };
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
function providerReceiptSettlement(call, event) {
|
|
1423
|
+
const receipt = ownDataValue(event, "providerReceipt");
|
|
1424
|
+
if (receipt === null || receipt === undefined) {
|
|
1425
|
+
return {
|
|
1426
|
+
persisted: false,
|
|
1427
|
+
requestId: null,
|
|
1428
|
+
responseId: null,
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
let descriptors;
|
|
1432
|
+
try {
|
|
1433
|
+
if (
|
|
1434
|
+
typeof receipt !== "object" ||
|
|
1435
|
+
Array.isArray(receipt) ||
|
|
1436
|
+
utilTypes.isProxy(receipt)
|
|
1437
|
+
) {
|
|
1438
|
+
throw new TypeError();
|
|
1439
|
+
}
|
|
1440
|
+
descriptors = Object.getOwnPropertyDescriptors(receipt);
|
|
1441
|
+
} catch {
|
|
1442
|
+
throw new TypeError("workflow provider receipt settlement is malformed");
|
|
1443
|
+
}
|
|
1444
|
+
const expectedFields = [
|
|
1445
|
+
"callId",
|
|
1446
|
+
"callSequence",
|
|
1447
|
+
"clientRequestId",
|
|
1448
|
+
"independentlyReadable",
|
|
1449
|
+
"protocol",
|
|
1450
|
+
"provider",
|
|
1451
|
+
"requestId",
|
|
1452
|
+
"requestIdentitySemantics",
|
|
1453
|
+
"responseId",
|
|
1454
|
+
"source",
|
|
1455
|
+
"workflowEffectId",
|
|
1456
|
+
];
|
|
1457
|
+
const ownFields = Reflect.ownKeys(descriptors);
|
|
1458
|
+
const fields = ownFields.filter((field) => typeof field === "string").sort();
|
|
1459
|
+
const hasOnlyDataFields =
|
|
1460
|
+
ownFields.length === expectedFields.length &&
|
|
1461
|
+
fields.length === expectedFields.length &&
|
|
1462
|
+
fields.every(
|
|
1463
|
+
(field, index) =>
|
|
1464
|
+
field === expectedFields[index] &&
|
|
1465
|
+
descriptors[field].enumerable === true &&
|
|
1466
|
+
Object.hasOwn(descriptors[field], "value"),
|
|
1467
|
+
);
|
|
1468
|
+
const value = (field) => descriptors?.[field]?.value;
|
|
1469
|
+
const requestId = value("requestId");
|
|
1470
|
+
const responseId = value("responseId");
|
|
1471
|
+
if (
|
|
1472
|
+
!hasOnlyDataFields ||
|
|
1473
|
+
value("protocol") !== "cc-provider-request-receipt/v1" ||
|
|
1474
|
+
value("provider") !== call.name ||
|
|
1475
|
+
value("workflowEffectId") !== (call.ownerEffectId ?? call.effectId) ||
|
|
1476
|
+
value("callId") !== call.callId ||
|
|
1477
|
+
value("callSequence") !== call.sequence ||
|
|
1478
|
+
value("source") !== call.requestSource ||
|
|
1479
|
+
value("clientRequestId") !== call.clientRequestId ||
|
|
1480
|
+
value("requestIdentitySemantics") !== "trace-only" ||
|
|
1481
|
+
value("independentlyReadable") !== false ||
|
|
1482
|
+
(requestId !== null &&
|
|
1483
|
+
(typeof requestId !== "string" ||
|
|
1484
|
+
!PROVIDER_RECEIPT_ID_RE.test(requestId))) ||
|
|
1485
|
+
(responseId !== null &&
|
|
1486
|
+
(typeof responseId !== "string" ||
|
|
1487
|
+
!PROVIDER_RECEIPT_ID_RE.test(responseId))) ||
|
|
1488
|
+
(!requestId && !responseId)
|
|
1489
|
+
) {
|
|
1490
|
+
throw new TypeError("workflow provider receipt settlement is malformed");
|
|
1491
|
+
}
|
|
1492
|
+
return { persisted: true, requestId, responseId };
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
function effectCallSettlement(call, event, now) {
|
|
1496
|
+
let status;
|
|
1497
|
+
let settlementCode = null;
|
|
1498
|
+
let providerReceiptPersisted = false;
|
|
1499
|
+
let providerReceiptRequestId = null;
|
|
1500
|
+
let providerReceiptResponseId = null;
|
|
1501
|
+
let mcpLedgerId = null;
|
|
1502
|
+
let mcpLedgerPrewritePersisted = false;
|
|
1503
|
+
let mcpLedgerSettlementPersisted = false;
|
|
1504
|
+
if (call.kind === "provider") {
|
|
1505
|
+
if (
|
|
1506
|
+
!["token-usage", "model-usage-unknown"].includes(event?.type) ||
|
|
1507
|
+
event.callId !== call.callId ||
|
|
1508
|
+
(event.provider && event.provider !== call.name) ||
|
|
1509
|
+
(event.source && event.source !== call.source)
|
|
1510
|
+
) {
|
|
1511
|
+
throw new TypeError("workflow provider call settlement is malformed");
|
|
1512
|
+
}
|
|
1513
|
+
status = event.type === "token-usage" ? "completed" : "outcome_unknown";
|
|
1514
|
+
settlementCode =
|
|
1515
|
+
status === "outcome_unknown"
|
|
1516
|
+
? String(event.code || "provider_outcome_unknown")
|
|
1517
|
+
: null;
|
|
1518
|
+
const providerReceipt = providerReceiptSettlement(call, event);
|
|
1519
|
+
providerReceiptPersisted = providerReceipt.persisted;
|
|
1520
|
+
providerReceiptRequestId = providerReceipt.requestId;
|
|
1521
|
+
providerReceiptResponseId = providerReceipt.responseId;
|
|
1522
|
+
} else {
|
|
1523
|
+
if (
|
|
1524
|
+
event?.type !== "tool-result" ||
|
|
1525
|
+
event.workflowEffectId !== (call.ownerEffectId ?? call.effectId) ||
|
|
1526
|
+
event.workflowEffectProtocol !== WORKFLOW_CHILD_EFFECT_PROTOCOL ||
|
|
1527
|
+
event.workflowChildEffectId !== call.childEffectId ||
|
|
1528
|
+
event.workflowChildSequence !== call.sequence ||
|
|
1529
|
+
event.tool_use_id !== call.callId ||
|
|
1530
|
+
event.tool !== call.name
|
|
1531
|
+
) {
|
|
1532
|
+
throw new TypeError("workflow tool call settlement is malformed");
|
|
1533
|
+
}
|
|
1534
|
+
const outcomeUnknown =
|
|
1535
|
+
ownDataValue(event.result, "outcomeUnknown") === true;
|
|
1536
|
+
const failed = Boolean(event.error || ownDataValue(event.result, "error"));
|
|
1537
|
+
status = outcomeUnknown
|
|
1538
|
+
? "outcome_unknown"
|
|
1539
|
+
: failed
|
|
1540
|
+
? "failed"
|
|
1541
|
+
: "completed";
|
|
1542
|
+
settlementCode = outcomeUnknown
|
|
1543
|
+
? "nested_tool_outcome_unknown"
|
|
1544
|
+
: failed
|
|
1545
|
+
? "tool_failed"
|
|
1546
|
+
: null;
|
|
1547
|
+
mcpLedgerId = ownDataValue(event.result, "mcpLedgerId") ?? null;
|
|
1548
|
+
mcpLedgerPrewritePersisted =
|
|
1549
|
+
ownDataValue(event.result, "mcpLedgerPrewritePersisted") === true;
|
|
1550
|
+
mcpLedgerSettlementPersisted =
|
|
1551
|
+
ownDataValue(event.result, "mcpLedgerSettlementPersisted") === true;
|
|
1552
|
+
if (
|
|
1553
|
+
(mcpLedgerId !== null &&
|
|
1554
|
+
!PROVIDER_RECEIPT_ID_RE.test(String(mcpLedgerId))) ||
|
|
1555
|
+
(mcpLedgerSettlementPersisted && !mcpLedgerPrewritePersisted)
|
|
1556
|
+
) {
|
|
1557
|
+
throw new TypeError("workflow MCP call settlement is malformed");
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
if (
|
|
1561
|
+
settlementCode !== null &&
|
|
1562
|
+
(!/^[A-Za-z0-9._:-]{1,128}$/u.test(settlementCode) ||
|
|
1563
|
+
settlementCode.includes(".."))
|
|
1564
|
+
) {
|
|
1565
|
+
throw new TypeError("workflow call settlement code is malformed");
|
|
1566
|
+
}
|
|
1567
|
+
return {
|
|
1568
|
+
...call,
|
|
1569
|
+
status,
|
|
1570
|
+
settledAt: isoNow(now),
|
|
1571
|
+
outcomeUnknown: status === "outcome_unknown",
|
|
1572
|
+
settlementCode,
|
|
1573
|
+
providerReceiptPersisted,
|
|
1574
|
+
providerReceiptRequestId,
|
|
1575
|
+
providerReceiptResponseId,
|
|
1576
|
+
mcpLedgerId,
|
|
1577
|
+
mcpLedgerPrewritePersisted,
|
|
1578
|
+
mcpLedgerSettlementPersisted,
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
function settleEffectCall(statePath, runId, effectId, kind, event, now) {
|
|
1583
|
+
return withStateMutation(statePath, (current) => {
|
|
1584
|
+
if (!current || current.runId !== runId) {
|
|
1585
|
+
throw new Error(
|
|
1586
|
+
"durable workflow run disappeared before call settlement",
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
const effectIndex = current.effects.findIndex(
|
|
1590
|
+
(effect) => effect.id === effectId,
|
|
1591
|
+
);
|
|
1592
|
+
const effect = current.effects[effectIndex];
|
|
1593
|
+
const callIndex = (effect?.calls || []).findIndex((call) =>
|
|
1594
|
+
kind === "tool"
|
|
1595
|
+
? call.kind === kind &&
|
|
1596
|
+
call.childEffectId === event?.workflowChildEffectId
|
|
1597
|
+
: call.kind === kind && call.callId === event?.callId,
|
|
1598
|
+
);
|
|
1599
|
+
const call = effect?.calls?.[callIndex];
|
|
1600
|
+
if (
|
|
1601
|
+
!effect ||
|
|
1602
|
+
effect.status !== "pending" ||
|
|
1603
|
+
!call ||
|
|
1604
|
+
call.status !== "started"
|
|
1605
|
+
) {
|
|
1606
|
+
throw new Error("workflow child call is not started at settlement");
|
|
1607
|
+
}
|
|
1608
|
+
const settlement = effectCallSettlement(call, event, now);
|
|
1609
|
+
const state = transition(
|
|
1610
|
+
current,
|
|
1611
|
+
"effect-call-settled",
|
|
1612
|
+
{
|
|
1613
|
+
effectId,
|
|
1614
|
+
callRecordId: call.id,
|
|
1615
|
+
kind,
|
|
1616
|
+
status: settlement.status,
|
|
1617
|
+
},
|
|
1618
|
+
(draft) => {
|
|
1619
|
+
const calls = [...(draft.effects[effectIndex].calls || [])];
|
|
1620
|
+
calls[callIndex] = settlement;
|
|
1621
|
+
draft.effects[effectIndex] = {
|
|
1622
|
+
...draft.effects[effectIndex],
|
|
1623
|
+
calls,
|
|
1624
|
+
};
|
|
1625
|
+
},
|
|
1626
|
+
now,
|
|
1627
|
+
);
|
|
1628
|
+
return { state, value: state.effects[effectIndex].calls[callIndex] };
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
function createDurableCallObservers(statePath, runId, effectId, now) {
|
|
1633
|
+
return Object.freeze({
|
|
1634
|
+
strictUsageTelemetry: true,
|
|
1635
|
+
onUsageBoundary(event) {
|
|
1636
|
+
beginEffectCall(statePath, runId, effectId, "provider", event, now);
|
|
1637
|
+
},
|
|
1638
|
+
onUsageSettlement(event) {
|
|
1639
|
+
settleEffectCall(statePath, runId, effectId, "provider", event, now);
|
|
1640
|
+
},
|
|
1641
|
+
onToolCallBoundary(event) {
|
|
1642
|
+
beginEffectCall(statePath, runId, effectId, "tool", event, now);
|
|
1643
|
+
},
|
|
1644
|
+
onToolCallSettlement(event) {
|
|
1645
|
+
settleEffectCall(statePath, runId, effectId, "tool", event, now);
|
|
1646
|
+
},
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
function runtimeNotDispatchedResult(effect, reason) {
|
|
1651
|
+
if (reason !== "timeout" && reason !== "stopped") {
|
|
1652
|
+
throw new TypeError("undispatched workflow effect reason is invalid");
|
|
1653
|
+
}
|
|
1654
|
+
const timeout = reason === "timeout";
|
|
1655
|
+
return {
|
|
1656
|
+
taskId: `runtime-${effect.id.slice("sha256:".length, 39)}`,
|
|
1657
|
+
status: "failed",
|
|
1658
|
+
result: {
|
|
1659
|
+
summary: timeout
|
|
1660
|
+
? `step timed out after ${effect.timeoutMs}ms before provider dispatch`
|
|
1661
|
+
: "workflow stopped before provider dispatch",
|
|
1662
|
+
providerDispatched: false,
|
|
1663
|
+
reason: timeout ? "timeout-before-dispatch" : "stopped-before-dispatch",
|
|
1664
|
+
},
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
function settleUndispatchedEffect(statePath, runId, effectId, reason, now) {
|
|
1669
|
+
return withStateMutation(statePath, (current) => {
|
|
1670
|
+
if (!current || current.runId !== runId) {
|
|
1671
|
+
throw new Error(
|
|
1672
|
+
"durable workflow run disappeared before undispatched settlement",
|
|
1673
|
+
);
|
|
1674
|
+
}
|
|
1675
|
+
const index = current.effects.findIndex((effect) => effect.id === effectId);
|
|
1676
|
+
const effect = current.effects[index];
|
|
1677
|
+
if (
|
|
1678
|
+
!effect ||
|
|
1679
|
+
effect.status !== "pending" ||
|
|
1680
|
+
!isKnownUndispatched(effect) ||
|
|
1681
|
+
(reason === "timeout" && !(effect.timeoutMs > 0)) ||
|
|
1682
|
+
(reason === "stopped" && current.status !== "stopped")
|
|
1683
|
+
) {
|
|
1684
|
+
throw new Error("workflow effect is not safely undispatched");
|
|
1685
|
+
}
|
|
1686
|
+
const result = runtimeNotDispatchedResult(effect, reason);
|
|
1687
|
+
const resultDigest = effectResultDigest(result);
|
|
1688
|
+
const timeoutObservedAt =
|
|
1689
|
+
reason === "timeout" && effect.timeoutObservedAt === null
|
|
1690
|
+
? isoNow(now)
|
|
1691
|
+
: effect.timeoutObservedAt;
|
|
1692
|
+
const settledAt = isoNow(now);
|
|
1693
|
+
const state = transition(
|
|
1694
|
+
current,
|
|
1695
|
+
"effect-not-dispatched",
|
|
1696
|
+
{
|
|
1697
|
+
effectId,
|
|
1698
|
+
reason,
|
|
1699
|
+
resultDigest,
|
|
1700
|
+
settlementAuthority: "runtime-not-dispatched",
|
|
1701
|
+
},
|
|
1702
|
+
(draft) => {
|
|
1703
|
+
draft.effects[index] = {
|
|
1704
|
+
...draft.effects[index],
|
|
1705
|
+
status: "settled",
|
|
1706
|
+
timeoutObservedAt,
|
|
1707
|
+
settledAt,
|
|
1708
|
+
settlementAuthority: "runtime-not-dispatched",
|
|
1709
|
+
resultDigest,
|
|
1710
|
+
result,
|
|
1711
|
+
};
|
|
1712
|
+
},
|
|
1713
|
+
now,
|
|
1714
|
+
);
|
|
1715
|
+
return { state, value: result };
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
function settleEffect(statePath, runId, effectId, rawResult, now) {
|
|
1720
|
+
const result = snapshotJson(rawResult, "workflow effect result", 1024 * 1024);
|
|
1721
|
+
const resultDigest = effectResultDigest(result);
|
|
1722
|
+
if (containsSecret(JSON.stringify(result))) {
|
|
1723
|
+
throw new Error("workflow effect result contains secret-shaped data");
|
|
1724
|
+
}
|
|
1725
|
+
return withStateMutation(statePath, (current) => {
|
|
1726
|
+
if (!current || current.runId !== runId) {
|
|
1727
|
+
throw new Error("durable workflow run disappeared before settlement");
|
|
1728
|
+
}
|
|
1729
|
+
const index = current.effects.findIndex((effect) => effect.id === effectId);
|
|
1730
|
+
const effect = current.effects[index];
|
|
1731
|
+
if (!effect || effect.status !== "pending") {
|
|
1732
|
+
throw new Error("workflow effect is not pending at settlement");
|
|
1733
|
+
}
|
|
1734
|
+
if (
|
|
1735
|
+
hasProviderDispatchMetadata(effect) &&
|
|
1736
|
+
typeof effect.providerDispatchedAt !== "string"
|
|
1737
|
+
) {
|
|
1738
|
+
throw new Error("workflow effect provider dispatch was not persisted");
|
|
1739
|
+
}
|
|
1740
|
+
if (
|
|
1741
|
+
(effect.calls || []).some((call) =>
|
|
1742
|
+
["started", "outcome_unknown"].includes(call.status),
|
|
1743
|
+
)
|
|
1744
|
+
) {
|
|
1745
|
+
throw new Error(
|
|
1746
|
+
"workflow effect has an unsettled or outcome-unknown durable child call",
|
|
1747
|
+
);
|
|
1748
|
+
}
|
|
1749
|
+
const settledAt = isoNow(now);
|
|
1750
|
+
const state = transition(
|
|
1751
|
+
current,
|
|
1752
|
+
"effect-settled",
|
|
1753
|
+
{ effectId, resultDigest, settlementAuthority: "provider-return" },
|
|
1754
|
+
(draft) => {
|
|
1755
|
+
draft.effects[index] = {
|
|
1756
|
+
...draft.effects[index],
|
|
1757
|
+
status: "settled",
|
|
1758
|
+
settledAt,
|
|
1759
|
+
settlementAuthority: "provider-return",
|
|
1760
|
+
resultDigest,
|
|
1761
|
+
result,
|
|
1762
|
+
};
|
|
1763
|
+
},
|
|
1764
|
+
now,
|
|
1765
|
+
);
|
|
1766
|
+
return { state, value: result };
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
function blockPendingEffect(statePath, runId, effectId, now) {
|
|
1771
|
+
return withStateMutation(statePath, (current) => {
|
|
1772
|
+
if (!current || current.runId !== runId) return { value: current };
|
|
1773
|
+
const effect = current.effects.find((entry) => entry.id === effectId);
|
|
1774
|
+
if (
|
|
1775
|
+
!effect ||
|
|
1776
|
+
effect.status !== "pending" ||
|
|
1777
|
+
current.status === "stopped"
|
|
1778
|
+
) {
|
|
1779
|
+
return { value: current };
|
|
1780
|
+
}
|
|
1781
|
+
if (current.status === "blocked") return { value: current };
|
|
1782
|
+
const state = transition(
|
|
1783
|
+
current,
|
|
1784
|
+
"effect-reconciliation-required",
|
|
1785
|
+
{ effectId },
|
|
1786
|
+
(draft) => {
|
|
1787
|
+
draft.status = "blocked";
|
|
1788
|
+
},
|
|
1789
|
+
now,
|
|
1790
|
+
);
|
|
1791
|
+
return { state, value: state };
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
function finalizePauseAfterSettlementBarrier(statePath, runId, now) {
|
|
1796
|
+
return withStateMutation(statePath, (current) => {
|
|
1797
|
+
if (!current || current.runId !== runId) return { value: current };
|
|
1798
|
+
if (current.status !== "pause_requested") return { value: current };
|
|
1799
|
+
if (current.effects.some((effect) => effect.status === "pending")) {
|
|
1800
|
+
return { value: current };
|
|
1801
|
+
}
|
|
1802
|
+
const state = transition(
|
|
1803
|
+
current,
|
|
1804
|
+
"run-paused",
|
|
1805
|
+
{ barrier: "all-requested-effects-settled" },
|
|
1806
|
+
(draft) => {
|
|
1807
|
+
draft.status = "paused";
|
|
1808
|
+
},
|
|
1809
|
+
now,
|
|
1810
|
+
);
|
|
1811
|
+
return { state, value: state };
|
|
1812
|
+
});
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
function completeRun(statePath, runId, record, now) {
|
|
1816
|
+
const finalRecord = snapshotJson(
|
|
1817
|
+
record,
|
|
1818
|
+
"workflow final record",
|
|
1819
|
+
2 * 1024 * 1024,
|
|
1820
|
+
);
|
|
1821
|
+
return withStateMutation(statePath, (current) => {
|
|
1822
|
+
if (!current || current.runId !== runId) {
|
|
1823
|
+
throw new Error("durable workflow run disappeared before completion");
|
|
1824
|
+
}
|
|
1825
|
+
if (current.status === "pause_requested") {
|
|
1826
|
+
const state = transition(
|
|
1827
|
+
current,
|
|
1828
|
+
"run-paused",
|
|
1829
|
+
{},
|
|
1830
|
+
(draft) => {
|
|
1831
|
+
draft.status = "paused";
|
|
1832
|
+
},
|
|
1833
|
+
now,
|
|
1834
|
+
);
|
|
1835
|
+
return { state, value: { control: "paused", state } };
|
|
1836
|
+
}
|
|
1837
|
+
if (current.status === "stopped") {
|
|
1838
|
+
return { value: { control: "stopped", state: current } };
|
|
1839
|
+
}
|
|
1840
|
+
if (current.status !== "running") {
|
|
1841
|
+
throw new Error(
|
|
1842
|
+
`durable workflow run cannot complete while ${current.status}`,
|
|
1843
|
+
);
|
|
1844
|
+
}
|
|
1845
|
+
const state = transition(
|
|
1846
|
+
current,
|
|
1847
|
+
"run-completed",
|
|
1848
|
+
{ recordStatus: finalRecord.status },
|
|
1849
|
+
(draft) => {
|
|
1850
|
+
draft.status = "completed";
|
|
1851
|
+
draft.finalRecord = finalRecord;
|
|
1852
|
+
},
|
|
1853
|
+
now,
|
|
1854
|
+
);
|
|
1855
|
+
return { state, value: { record: finalRecord, state } };
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
function markFailed(statePath, runId, error, now) {
|
|
1860
|
+
return withStateMutation(statePath, (current) => {
|
|
1861
|
+
if (
|
|
1862
|
+
!current ||
|
|
1863
|
+
current.runId !== runId ||
|
|
1864
|
+
TERMINAL_STATUSES.has(current.status) ||
|
|
1865
|
+
["paused", "blocked", "pause_requested"].includes(current.status)
|
|
1866
|
+
) {
|
|
1867
|
+
return { value: current };
|
|
1868
|
+
}
|
|
1869
|
+
const state = transition(
|
|
1870
|
+
current,
|
|
1871
|
+
"run-failed",
|
|
1872
|
+
{ code: String(error?.code || "WORKFLOW_RUN_FAILED").slice(0, 256) },
|
|
1873
|
+
(draft) => {
|
|
1874
|
+
draft.status = "failed";
|
|
1875
|
+
},
|
|
1876
|
+
now,
|
|
1877
|
+
);
|
|
1878
|
+
return { state, value: state };
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
export async function executeDurableDynamicWorkflow(options = {}, deps = {}) {
|
|
1883
|
+
const statePath = path.resolve(options.statePath);
|
|
1884
|
+
const runId = safeId(options.runId, "runId");
|
|
1885
|
+
const execution = options.execution;
|
|
1886
|
+
const bindings = stateBindings(execution, runId);
|
|
1887
|
+
if (typeof deps.runTask !== "function") {
|
|
1888
|
+
throw new TypeError("durable workflow task provider is required");
|
|
1889
|
+
}
|
|
1890
|
+
const initial = startRun(statePath, bindings, deps.now);
|
|
1891
|
+
if (initial.status === "completed") return initial.finalRecord;
|
|
1892
|
+
const requestEffectForDispatch = createEffectRequestBatcher(
|
|
1893
|
+
statePath,
|
|
1894
|
+
runId,
|
|
1895
|
+
deps.now,
|
|
1896
|
+
);
|
|
1897
|
+
const durableRunTask = async (args) => {
|
|
1898
|
+
const requested = await requestEffectForDispatch(args);
|
|
1899
|
+
if (requested.control) {
|
|
1900
|
+
throw controlError(
|
|
1901
|
+
`dynamic workflow run ${requested.control}`,
|
|
1902
|
+
requested.control,
|
|
1903
|
+
requested.state,
|
|
1904
|
+
);
|
|
1905
|
+
}
|
|
1906
|
+
if (requested.pending) {
|
|
1907
|
+
throw controlError(
|
|
1908
|
+
`workflow effect ${requested.effect.id} requires reconciliation`,
|
|
1909
|
+
"reconciliation-required",
|
|
1910
|
+
requested.state,
|
|
1911
|
+
requested.effect,
|
|
1912
|
+
);
|
|
1913
|
+
}
|
|
1914
|
+
if (requested.cached) return requested.result;
|
|
1915
|
+
if (typeof deps.beforeProviderDispatch === "function") {
|
|
1916
|
+
try {
|
|
1917
|
+
await deps.beforeProviderDispatch(requested.effect, args);
|
|
1918
|
+
} catch (cause) {
|
|
1919
|
+
const state = readDynamicWorkflowRuntimeState(statePath);
|
|
1920
|
+
throw controlError(
|
|
1921
|
+
`workflow effect ${requested.effect.id} was persisted but not dispatched`,
|
|
1922
|
+
"undispatched-recovery-required",
|
|
1923
|
+
state,
|
|
1924
|
+
requested.effect,
|
|
1925
|
+
cause,
|
|
1926
|
+
);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
if (
|
|
1930
|
+
isKnownUndispatched(requested.effect) &&
|
|
1931
|
+
(requested.effect.timeoutObservedAt !== null || args.signal?.aborted)
|
|
1932
|
+
) {
|
|
1933
|
+
return settleUndispatchedEffect(
|
|
1934
|
+
statePath,
|
|
1935
|
+
runId,
|
|
1936
|
+
requested.effect.id,
|
|
1937
|
+
"timeout",
|
|
1938
|
+
deps.now,
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
const dispatched = markEffectProviderDispatched(
|
|
1942
|
+
statePath,
|
|
1943
|
+
runId,
|
|
1944
|
+
requested.effect.id,
|
|
1945
|
+
deps.now,
|
|
1946
|
+
);
|
|
1947
|
+
if (dispatched.control === "stopped") {
|
|
1948
|
+
settleUndispatchedEffect(
|
|
1949
|
+
statePath,
|
|
1950
|
+
runId,
|
|
1951
|
+
requested.effect.id,
|
|
1952
|
+
"stopped",
|
|
1953
|
+
deps.now,
|
|
1954
|
+
);
|
|
1955
|
+
const state = readDynamicWorkflowRuntimeState(statePath);
|
|
1956
|
+
throw controlError("dynamic workflow run stopped", "stopped", state);
|
|
1957
|
+
}
|
|
1958
|
+
const effect = dispatched.effect;
|
|
1959
|
+
let timeoutObservationError = null;
|
|
1960
|
+
const observeTimeout = () => {
|
|
1961
|
+
try {
|
|
1962
|
+
observeEffectTimeout(statePath, runId, effect.id, deps.now);
|
|
1963
|
+
} catch (error) {
|
|
1964
|
+
timeoutObservationError ||= error;
|
|
1965
|
+
}
|
|
1966
|
+
};
|
|
1967
|
+
if (args.signal) {
|
|
1968
|
+
args.signal.addEventListener("abort", observeTimeout, { once: true });
|
|
1969
|
+
if (args.signal.aborted) observeTimeout();
|
|
1970
|
+
}
|
|
1971
|
+
let result;
|
|
1972
|
+
try {
|
|
1973
|
+
const durableCallObservers = createDurableCallObservers(
|
|
1974
|
+
statePath,
|
|
1975
|
+
runId,
|
|
1976
|
+
effect.id,
|
|
1977
|
+
deps.now,
|
|
1978
|
+
);
|
|
1979
|
+
result = await deps.runTask({
|
|
1980
|
+
...args,
|
|
1981
|
+
workflowEffectId: effect.id,
|
|
1982
|
+
...durableCallObservers,
|
|
1983
|
+
});
|
|
1984
|
+
if (typeof deps.afterProvider === "function") {
|
|
1985
|
+
await deps.afterProvider(effect, result);
|
|
1986
|
+
}
|
|
1987
|
+
if (timeoutObservationError) throw timeoutObservationError;
|
|
1988
|
+
} catch (cause) {
|
|
1989
|
+
const state = blockPendingEffect(statePath, runId, effect.id, deps.now);
|
|
1990
|
+
throw controlError(
|
|
1991
|
+
`workflow effect ${effect.id} has an unknown outcome and requires reconciliation`,
|
|
1992
|
+
"reconciliation-required",
|
|
1993
|
+
state,
|
|
1994
|
+
effect,
|
|
1995
|
+
cause,
|
|
1996
|
+
);
|
|
1997
|
+
} finally {
|
|
1998
|
+
args.signal?.removeEventListener("abort", observeTimeout);
|
|
1999
|
+
}
|
|
2000
|
+
try {
|
|
2001
|
+
return settleEffect(statePath, runId, effect.id, result, deps.now);
|
|
2002
|
+
} catch (cause) {
|
|
2003
|
+
const state = blockPendingEffect(statePath, runId, effect.id, deps.now);
|
|
2004
|
+
throw controlError(
|
|
2005
|
+
`workflow effect ${effect.id} settlement is unknown and requires reconciliation`,
|
|
2006
|
+
"reconciliation-required",
|
|
2007
|
+
state,
|
|
2008
|
+
effect,
|
|
2009
|
+
cause,
|
|
2010
|
+
);
|
|
2011
|
+
}
|
|
2012
|
+
};
|
|
2013
|
+
try {
|
|
2014
|
+
const record = await (deps.executeWorkflow || executeCoworkWorkflow)({
|
|
2015
|
+
...execution,
|
|
2016
|
+
maxParallel: execution.runAdmission.maxParallel,
|
|
2017
|
+
runTask: durableRunTask,
|
|
2018
|
+
});
|
|
2019
|
+
const completed = completeRun(statePath, runId, record, deps.now);
|
|
2020
|
+
if (completed.control) {
|
|
2021
|
+
throw controlError(
|
|
2022
|
+
`dynamic workflow run ${completed.control}`,
|
|
2023
|
+
completed.control,
|
|
2024
|
+
completed.state,
|
|
2025
|
+
);
|
|
2026
|
+
}
|
|
2027
|
+
return completed.record;
|
|
2028
|
+
} catch (error) {
|
|
2029
|
+
if (
|
|
2030
|
+
error?.code === DYNAMIC_WORKFLOW_RUNTIME_CONTROL_CODE &&
|
|
2031
|
+
error.reason === "paused"
|
|
2032
|
+
) {
|
|
2033
|
+
const state = finalizePauseAfterSettlementBarrier(
|
|
2034
|
+
statePath,
|
|
2035
|
+
runId,
|
|
2036
|
+
deps.now,
|
|
2037
|
+
);
|
|
2038
|
+
const pending = state?.effects?.find(
|
|
2039
|
+
(effect) => effect.status === "pending",
|
|
2040
|
+
);
|
|
2041
|
+
if (pending) {
|
|
2042
|
+
throw controlError(
|
|
2043
|
+
`workflow effect ${pending.id} requires reconciliation before pause can settle`,
|
|
2044
|
+
"reconciliation-required",
|
|
2045
|
+
state,
|
|
2046
|
+
pending,
|
|
2047
|
+
error,
|
|
2048
|
+
);
|
|
2049
|
+
}
|
|
2050
|
+
if (state?.status === "stopped") {
|
|
2051
|
+
throw controlError(
|
|
2052
|
+
"dynamic workflow run stopped while its pause barrier was settling",
|
|
2053
|
+
"stopped",
|
|
2054
|
+
state,
|
|
2055
|
+
null,
|
|
2056
|
+
error,
|
|
2057
|
+
);
|
|
2058
|
+
}
|
|
2059
|
+
throw controlError(
|
|
2060
|
+
"dynamic workflow run paused after its settlement barrier",
|
|
2061
|
+
"paused",
|
|
2062
|
+
state,
|
|
2063
|
+
null,
|
|
2064
|
+
error,
|
|
2065
|
+
);
|
|
2066
|
+
}
|
|
2067
|
+
if (error?.code !== DYNAMIC_WORKFLOW_RUNTIME_CONTROL_CODE) {
|
|
2068
|
+
markFailed(statePath, runId, error, deps.now);
|
|
2069
|
+
}
|
|
2070
|
+
throw error;
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
function requireRevision(state, expectedRevision) {
|
|
2075
|
+
const expected = Number(expectedRevision);
|
|
2076
|
+
if (!Number.isSafeInteger(expected) || expected < 1) {
|
|
2077
|
+
throw new TypeError("expected runtime revision is required");
|
|
2078
|
+
}
|
|
2079
|
+
if (state.revision !== expected) {
|
|
2080
|
+
throw new Error(
|
|
2081
|
+
`stale dynamic workflow runtime revision: expected ${expected}, found ${state.revision}`,
|
|
2082
|
+
);
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
export function requestDurableWorkflowPause(
|
|
2087
|
+
statePath,
|
|
2088
|
+
expectedRevision,
|
|
2089
|
+
deps = {},
|
|
2090
|
+
) {
|
|
2091
|
+
return withStateMutation(path.resolve(statePath), (current) => {
|
|
2092
|
+
if (!current)
|
|
2093
|
+
throw new Error("dynamic workflow runtime state was not found");
|
|
2094
|
+
requireRevision(current, expectedRevision);
|
|
2095
|
+
if (TERMINAL_STATUSES.has(current.status)) {
|
|
2096
|
+
throw new Error(`cannot pause a ${current.status} workflow run`);
|
|
2097
|
+
}
|
|
2098
|
+
const immediate = ["ready", "paused", "failed"].includes(current.status);
|
|
2099
|
+
const state = transition(
|
|
2100
|
+
current,
|
|
2101
|
+
immediate ? "run-paused" : "pause-requested",
|
|
2102
|
+
{},
|
|
2103
|
+
(draft) => {
|
|
2104
|
+
draft.status = immediate ? "paused" : "pause_requested";
|
|
2105
|
+
},
|
|
2106
|
+
deps.now,
|
|
2107
|
+
);
|
|
2108
|
+
return { state, value: state };
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
export function requestDurableWorkflowStop(
|
|
2113
|
+
statePath,
|
|
2114
|
+
expectedRevision,
|
|
2115
|
+
deps = {},
|
|
2116
|
+
) {
|
|
2117
|
+
return withStateMutation(path.resolve(statePath), (current) => {
|
|
2118
|
+
if (!current)
|
|
2119
|
+
throw new Error("dynamic workflow runtime state was not found");
|
|
2120
|
+
requireRevision(current, expectedRevision);
|
|
2121
|
+
if (current.status === "completed") {
|
|
2122
|
+
throw new Error("cannot stop a completed workflow run");
|
|
2123
|
+
}
|
|
2124
|
+
if (current.status === "stopped") return { value: current };
|
|
2125
|
+
const state = transition(
|
|
2126
|
+
current,
|
|
2127
|
+
"run-stopped",
|
|
2128
|
+
{},
|
|
2129
|
+
(draft) => {
|
|
2130
|
+
draft.status = "stopped";
|
|
2131
|
+
draft.finalRecord = null;
|
|
2132
|
+
},
|
|
2133
|
+
deps.now,
|
|
2134
|
+
);
|
|
2135
|
+
return { state, value: state };
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
export function prepareDurableWorkflowResume(
|
|
2140
|
+
statePath,
|
|
2141
|
+
expectedRevision,
|
|
2142
|
+
deps = {},
|
|
2143
|
+
) {
|
|
2144
|
+
return withStateMutation(path.resolve(statePath), (current) => {
|
|
2145
|
+
if (!current)
|
|
2146
|
+
throw new Error("dynamic workflow runtime state was not found");
|
|
2147
|
+
requireRevision(current, expectedRevision);
|
|
2148
|
+
if (TERMINAL_STATUSES.has(current.status)) {
|
|
2149
|
+
throw new Error(`cannot resume a ${current.status} workflow run`);
|
|
2150
|
+
}
|
|
2151
|
+
const pending = current.effects.find(
|
|
2152
|
+
(effect) => effect.status === "pending" && !isKnownUndispatched(effect),
|
|
2153
|
+
);
|
|
2154
|
+
if (pending) {
|
|
2155
|
+
const error = new Error(
|
|
2156
|
+
`workflow effect ${pending.id} must be reconciled before resume`,
|
|
2157
|
+
);
|
|
2158
|
+
error.code = "CC_DYNAMIC_WORKFLOW_EFFECT_RECONCILIATION_REQUIRED";
|
|
2159
|
+
error.pendingEffect = pending;
|
|
2160
|
+
throw error;
|
|
2161
|
+
}
|
|
2162
|
+
const state = transition(
|
|
2163
|
+
current,
|
|
2164
|
+
"run-resume-authorized",
|
|
2165
|
+
{},
|
|
2166
|
+
(draft) => {
|
|
2167
|
+
draft.status = "ready";
|
|
2168
|
+
},
|
|
2169
|
+
deps.now,
|
|
2170
|
+
);
|
|
2171
|
+
return { state, value: state };
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
export function reconcileDurableWorkflowEffect(
|
|
2176
|
+
statePath,
|
|
2177
|
+
input = {},
|
|
2178
|
+
deps = {},
|
|
2179
|
+
) {
|
|
2180
|
+
const effectId = String(input.effectId || "")
|
|
2181
|
+
.trim()
|
|
2182
|
+
.toLowerCase();
|
|
2183
|
+
if (!SHA256_RE.test(effectId)) throw new TypeError("effectId is invalid");
|
|
2184
|
+
const result = snapshotJson(
|
|
2185
|
+
input.result,
|
|
2186
|
+
"workflow reconciled effect result",
|
|
2187
|
+
1024 * 1024,
|
|
2188
|
+
);
|
|
2189
|
+
if (containsSecret(JSON.stringify(result))) {
|
|
2190
|
+
throw new Error("workflow reconciled result contains secret-shaped data");
|
|
2191
|
+
}
|
|
2192
|
+
const resultDigest = effectResultDigest(result);
|
|
2193
|
+
return withStateMutation(path.resolve(statePath), (current) => {
|
|
2194
|
+
if (!current)
|
|
2195
|
+
throw new Error("dynamic workflow runtime state was not found");
|
|
2196
|
+
requireRevision(current, input.expectedRevision);
|
|
2197
|
+
const index = current.effects.findIndex((effect) => effect.id === effectId);
|
|
2198
|
+
const effect = current.effects[index];
|
|
2199
|
+
if (!effect || effect.status !== "pending") {
|
|
2200
|
+
throw new Error("workflow effect is not pending reconciliation");
|
|
2201
|
+
}
|
|
2202
|
+
if (isKnownUndispatched(effect)) {
|
|
2203
|
+
throw new Error(
|
|
2204
|
+
"workflow effect was not dispatched and must be resumed instead of reconciled",
|
|
2205
|
+
);
|
|
2206
|
+
}
|
|
2207
|
+
const firstPending = current.effects.find(
|
|
2208
|
+
(candidate) =>
|
|
2209
|
+
candidate.status === "pending" && !isKnownUndispatched(candidate),
|
|
2210
|
+
);
|
|
2211
|
+
if (firstPending?.id !== effectId) {
|
|
2212
|
+
const error = new Error(
|
|
2213
|
+
`workflow effect ${firstPending.id} must be reconciled before ${effectId}`,
|
|
2214
|
+
);
|
|
2215
|
+
error.code = "CC_DYNAMIC_WORKFLOW_EFFECT_RECONCILIATION_OUT_OF_ORDER";
|
|
2216
|
+
error.pendingEffect = firstPending;
|
|
2217
|
+
throw error;
|
|
2218
|
+
}
|
|
2219
|
+
const settledAt = isoNow(deps.now);
|
|
2220
|
+
const state = transition(
|
|
2221
|
+
current,
|
|
2222
|
+
"effect-reconciled",
|
|
2223
|
+
{ effectId, resultDigest, settlementAuthority: "operator-reconciled" },
|
|
2224
|
+
(draft) => {
|
|
2225
|
+
const calls = (draft.effects[index].calls || []).map((call) =>
|
|
2226
|
+
call.status === "started"
|
|
2227
|
+
? {
|
|
2228
|
+
...call,
|
|
2229
|
+
status: "operator_reconciled",
|
|
2230
|
+
settledAt,
|
|
2231
|
+
outcomeUnknown: false,
|
|
2232
|
+
settlementCode: "operator_reconciled",
|
|
2233
|
+
}
|
|
2234
|
+
: call,
|
|
2235
|
+
);
|
|
2236
|
+
draft.effects[index] = {
|
|
2237
|
+
...draft.effects[index],
|
|
2238
|
+
calls,
|
|
2239
|
+
status: "settled",
|
|
2240
|
+
settledAt,
|
|
2241
|
+
settlementAuthority: "operator-reconciled",
|
|
2242
|
+
resultDigest,
|
|
2243
|
+
result,
|
|
2244
|
+
};
|
|
2245
|
+
const unknownPendingRemain = draft.effects.some(
|
|
2246
|
+
(candidate) =>
|
|
2247
|
+
candidate.status === "pending" && !isKnownUndispatched(candidate),
|
|
2248
|
+
);
|
|
2249
|
+
if (draft.status === "blocked" && !unknownPendingRemain) {
|
|
2250
|
+
draft.status = "ready";
|
|
2251
|
+
}
|
|
2252
|
+
},
|
|
2253
|
+
deps.now,
|
|
2254
|
+
);
|
|
2255
|
+
return { state, value: state };
|
|
2256
|
+
});
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
function reportedContentDigest(value) {
|
|
2260
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
2261
|
+
for (const candidate of [value.sha256, value.digest, value.recordDigest]) {
|
|
2262
|
+
if (typeof candidate !== "string") continue;
|
|
2263
|
+
const normalized = candidate.startsWith("sha256:")
|
|
2264
|
+
? candidate.toLowerCase()
|
|
2265
|
+
: `sha256:${candidate.toLowerCase()}`;
|
|
2266
|
+
if (SHA256_RE.test(normalized)) return normalized;
|
|
2267
|
+
}
|
|
2268
|
+
return null;
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
function projectResultValues(effect, field, domain) {
|
|
2272
|
+
const values = effect.result?.result?.[field];
|
|
2273
|
+
if (!Array.isArray(values)) {
|
|
2274
|
+
return { count: 0, truncated: false, lineage: [] };
|
|
2275
|
+
}
|
|
2276
|
+
const visible = values.slice(0, MAX_PROJECTED_ARTIFACTS_PER_EFFECT);
|
|
2277
|
+
return {
|
|
2278
|
+
count: values.length,
|
|
2279
|
+
truncated: values.length > visible.length,
|
|
2280
|
+
lineage: visible.map((value, ordinal) => ({
|
|
2281
|
+
effectId: effect.id,
|
|
2282
|
+
ordinal,
|
|
2283
|
+
valueDigest: digest(domain, value),
|
|
2284
|
+
reportedContentDigest: reportedContentDigest(value),
|
|
2285
|
+
})),
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
function projectProviderRequestReceipts(effect) {
|
|
2290
|
+
const attemptValues = Array.isArray(effect.result?.providerRequestAttempts)
|
|
2291
|
+
? effect.result.providerRequestAttempts
|
|
2292
|
+
: [];
|
|
2293
|
+
const receiptValues = Array.isArray(effect.result?.providerRequestReceipts)
|
|
2294
|
+
? effect.result.providerRequestReceipts
|
|
2295
|
+
: [];
|
|
2296
|
+
const visibleAttempts = attemptValues.slice(
|
|
2297
|
+
0,
|
|
2298
|
+
MAX_PROJECTED_ARTIFACTS_PER_EFFECT,
|
|
2299
|
+
);
|
|
2300
|
+
const visibleReceipts = receiptValues.slice(
|
|
2301
|
+
0,
|
|
2302
|
+
MAX_PROJECTED_ARTIFACTS_PER_EFFECT,
|
|
2303
|
+
);
|
|
2304
|
+
const attemptLineage = [];
|
|
2305
|
+
const receiptLineage = [];
|
|
2306
|
+
const attemptsByCallId = new Map();
|
|
2307
|
+
const matchedAttemptCallIds = new Set();
|
|
2308
|
+
let invalidAttempts = 0;
|
|
2309
|
+
let invalidReceipts = 0;
|
|
2310
|
+
|
|
2311
|
+
for (let ordinal = 0; ordinal < visibleAttempts.length; ordinal += 1) {
|
|
2312
|
+
const attempt = visibleAttempts[ordinal];
|
|
2313
|
+
if (
|
|
2314
|
+
effect.settlementAuthority !== "provider-return" ||
|
|
2315
|
+
effect.result?.workflowEffectId !== effect.id ||
|
|
2316
|
+
!attempt ||
|
|
2317
|
+
typeof attempt !== "object" ||
|
|
2318
|
+
Array.isArray(attempt) ||
|
|
2319
|
+
attempt.protocol !== "cc-provider-request-attempt/v1" ||
|
|
2320
|
+
attempt.workflowEffectId !== effect.id ||
|
|
2321
|
+
!PROVIDER_NAME_RE.test(attempt.provider || "") ||
|
|
2322
|
+
!PROVIDER_RECEIPT_ID_RE.test(attempt.callId || "") ||
|
|
2323
|
+
!Number.isSafeInteger(attempt.callSequence) ||
|
|
2324
|
+
attempt.callSequence < 1 ||
|
|
2325
|
+
!["model", "semantic-compaction"].includes(attempt.source) ||
|
|
2326
|
+
!PROVIDER_CLIENT_REQUEST_ID_RE.test(attempt.clientRequestId || "") ||
|
|
2327
|
+
attempt.requestIdentitySemantics !== "trace-only" ||
|
|
2328
|
+
attemptsByCallId.has(attempt.callId)
|
|
2329
|
+
) {
|
|
2330
|
+
invalidAttempts += 1;
|
|
2331
|
+
continue;
|
|
2332
|
+
}
|
|
2333
|
+
const projected = {
|
|
2334
|
+
effectId: effect.id,
|
|
2335
|
+
ordinal,
|
|
2336
|
+
provider: attempt.provider,
|
|
2337
|
+
callId: attempt.callId,
|
|
2338
|
+
callSequence: attempt.callSequence,
|
|
2339
|
+
source: attempt.source,
|
|
2340
|
+
clientRequestId: attempt.clientRequestId,
|
|
2341
|
+
requestIdentitySemantics: "trace-only",
|
|
2342
|
+
};
|
|
2343
|
+
attemptsByCallId.set(attempt.callId, projected);
|
|
2344
|
+
attemptLineage.push(projected);
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
for (let ordinal = 0; ordinal < visibleReceipts.length; ordinal += 1) {
|
|
2348
|
+
const receipt = visibleReceipts[ordinal];
|
|
2349
|
+
const requestId = receipt?.requestId ?? null;
|
|
2350
|
+
const responseId = receipt?.responseId ?? null;
|
|
2351
|
+
const attempt = attemptsByCallId.get(receipt?.callId);
|
|
2352
|
+
if (
|
|
2353
|
+
effect.settlementAuthority !== "provider-return" ||
|
|
2354
|
+
effect.result?.workflowEffectId !== effect.id ||
|
|
2355
|
+
!receipt ||
|
|
2356
|
+
typeof receipt !== "object" ||
|
|
2357
|
+
Array.isArray(receipt) ||
|
|
2358
|
+
receipt.protocol !== "cc-provider-request-receipt/v1" ||
|
|
2359
|
+
receipt.workflowEffectId !== effect.id ||
|
|
2360
|
+
!PROVIDER_NAME_RE.test(receipt.provider || "") ||
|
|
2361
|
+
typeof receipt.callId !== "string" ||
|
|
2362
|
+
!PROVIDER_RECEIPT_ID_RE.test(receipt.callId) ||
|
|
2363
|
+
!Number.isSafeInteger(receipt.callSequence) ||
|
|
2364
|
+
receipt.callSequence < 1 ||
|
|
2365
|
+
!["model", "semantic-compaction"].includes(receipt.source) ||
|
|
2366
|
+
!PROVIDER_CLIENT_REQUEST_ID_RE.test(receipt.clientRequestId || "") ||
|
|
2367
|
+
(requestId !== null && !PROVIDER_RECEIPT_ID_RE.test(requestId)) ||
|
|
2368
|
+
(responseId !== null && !PROVIDER_RECEIPT_ID_RE.test(responseId)) ||
|
|
2369
|
+
(!requestId && !responseId) ||
|
|
2370
|
+
receipt.requestIdentitySemantics !== "trace-only" ||
|
|
2371
|
+
receipt.independentlyReadable !== false ||
|
|
2372
|
+
!attempt ||
|
|
2373
|
+
attempt.provider !== receipt.provider ||
|
|
2374
|
+
attempt.callSequence !== receipt.callSequence ||
|
|
2375
|
+
attempt.source !== receipt.source ||
|
|
2376
|
+
attempt.clientRequestId !== receipt.clientRequestId ||
|
|
2377
|
+
matchedAttemptCallIds.has(receipt.callId)
|
|
2378
|
+
) {
|
|
2379
|
+
invalidReceipts += 1;
|
|
2380
|
+
continue;
|
|
2381
|
+
}
|
|
2382
|
+
matchedAttemptCallIds.add(receipt.callId);
|
|
2383
|
+
receiptLineage.push({
|
|
2384
|
+
effectId: effect.id,
|
|
2385
|
+
ordinal,
|
|
2386
|
+
provider: receipt.provider,
|
|
2387
|
+
callId: receipt.callId,
|
|
2388
|
+
callSequence: receipt.callSequence,
|
|
2389
|
+
source: receipt.source,
|
|
2390
|
+
clientRequestId: receipt.clientRequestId,
|
|
2391
|
+
requestId,
|
|
2392
|
+
responseId,
|
|
2393
|
+
requestIdentitySemantics: "trace-only",
|
|
2394
|
+
independentlyReadable: false,
|
|
2395
|
+
});
|
|
2396
|
+
}
|
|
2397
|
+
return {
|
|
2398
|
+
attemptCount: attemptValues.length,
|
|
2399
|
+
receiptCount: receiptValues.length,
|
|
2400
|
+
invalidAttempts,
|
|
2401
|
+
invalidReceipts,
|
|
2402
|
+
missingReceipts: attemptLineage.length - matchedAttemptCallIds.size,
|
|
2403
|
+
truncated:
|
|
2404
|
+
attemptValues.length > visibleAttempts.length ||
|
|
2405
|
+
receiptValues.length > visibleReceipts.length,
|
|
2406
|
+
attemptLineage,
|
|
2407
|
+
receiptLineage,
|
|
2408
|
+
};
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
function expectedNestedToolEffectId(effectId, sequence, toolUseId, tool) {
|
|
2412
|
+
return `sha256:${createHash("sha256")
|
|
2413
|
+
.update(
|
|
2414
|
+
`${effectId}\0tool\0${String(sequence)}\0${toolUseId}\0${tool}`,
|
|
2415
|
+
"utf8",
|
|
2416
|
+
)
|
|
2417
|
+
.digest("hex")}`;
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
function projectNestedToolEffects(effect) {
|
|
2421
|
+
const attemptValues = Array.isArray(effect.result?.nestedEffectAttempts)
|
|
2422
|
+
? effect.result.nestedEffectAttempts
|
|
2423
|
+
: [];
|
|
2424
|
+
const settlementValues = Array.isArray(effect.result?.nestedEffectSettlements)
|
|
2425
|
+
? effect.result.nestedEffectSettlements
|
|
2426
|
+
: [];
|
|
2427
|
+
const visibleAttempts = attemptValues.slice(
|
|
2428
|
+
0,
|
|
2429
|
+
MAX_PROJECTED_ARTIFACTS_PER_EFFECT,
|
|
2430
|
+
);
|
|
2431
|
+
const visibleSettlements = settlementValues.slice(
|
|
2432
|
+
0,
|
|
2433
|
+
MAX_PROJECTED_ARTIFACTS_PER_EFFECT,
|
|
2434
|
+
);
|
|
2435
|
+
const attemptsById = new Map();
|
|
2436
|
+
const settledIds = new Set();
|
|
2437
|
+
const attemptLineage = [];
|
|
2438
|
+
const settlementLineage = [];
|
|
2439
|
+
let invalidAttempts = 0;
|
|
2440
|
+
let invalidSettlements = 0;
|
|
2441
|
+
let durableMcpSettlements = 0;
|
|
2442
|
+
|
|
2443
|
+
for (let ordinal = 0; ordinal < visibleAttempts.length; ordinal += 1) {
|
|
2444
|
+
const attempt = visibleAttempts[ordinal];
|
|
2445
|
+
const expectedId =
|
|
2446
|
+
attempt &&
|
|
2447
|
+
expectedNestedToolEffectId(
|
|
2448
|
+
effect.id,
|
|
2449
|
+
attempt.childSequence,
|
|
2450
|
+
attempt.toolUseId,
|
|
2451
|
+
attempt.tool,
|
|
2452
|
+
);
|
|
2453
|
+
if (
|
|
2454
|
+
effect.settlementAuthority !== "provider-return" ||
|
|
2455
|
+
effect.result?.workflowEffectId !== effect.id ||
|
|
2456
|
+
!attempt ||
|
|
2457
|
+
typeof attempt !== "object" ||
|
|
2458
|
+
Array.isArray(attempt) ||
|
|
2459
|
+
attempt.protocol !== WORKFLOW_CHILD_EFFECT_PROTOCOL ||
|
|
2460
|
+
attempt.workflowEffectId !== effect.id ||
|
|
2461
|
+
!SHA256_RE.test(attempt.childEffectId || "") ||
|
|
2462
|
+
attempt.childEffectId !== expectedId ||
|
|
2463
|
+
!Number.isSafeInteger(attempt.childSequence) ||
|
|
2464
|
+
attempt.childSequence < 1 ||
|
|
2465
|
+
attempt.kind !== "tool" ||
|
|
2466
|
+
!WORKFLOW_TOOL_NAME_RE.test(attempt.tool || "") ||
|
|
2467
|
+
!WORKFLOW_TOOL_CALL_ID_RE.test(attempt.toolUseId || "") ||
|
|
2468
|
+
attempt.identitySemantics !== "runtime-derived" ||
|
|
2469
|
+
attemptsById.has(attempt.childEffectId) ||
|
|
2470
|
+
[...attemptsById.values()].some(
|
|
2471
|
+
(existing) => existing.childSequence === attempt.childSequence,
|
|
2472
|
+
)
|
|
2473
|
+
) {
|
|
2474
|
+
invalidAttempts += 1;
|
|
2475
|
+
continue;
|
|
2476
|
+
}
|
|
2477
|
+
const projected = {
|
|
2478
|
+
effectId: effect.id,
|
|
2479
|
+
ordinal,
|
|
2480
|
+
childEffectId: attempt.childEffectId,
|
|
2481
|
+
childSequence: attempt.childSequence,
|
|
2482
|
+
kind: "tool",
|
|
2483
|
+
tool: attempt.tool,
|
|
2484
|
+
toolUseId: attempt.toolUseId,
|
|
2485
|
+
identitySemantics: "runtime-derived",
|
|
2486
|
+
};
|
|
2487
|
+
attemptsById.set(attempt.childEffectId, projected);
|
|
2488
|
+
attemptLineage.push(projected);
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
for (let ordinal = 0; ordinal < visibleSettlements.length; ordinal += 1) {
|
|
2492
|
+
const settlement = visibleSettlements[ordinal];
|
|
2493
|
+
const attempt = attemptsById.get(settlement?.childEffectId);
|
|
2494
|
+
const mcpLedgerId = settlement?.mcpLedgerId ?? null;
|
|
2495
|
+
const validMcpLedgerId =
|
|
2496
|
+
mcpLedgerId === null || PROVIDER_RECEIPT_ID_RE.test(mcpLedgerId);
|
|
2497
|
+
const mcpPersistenceValid =
|
|
2498
|
+
mcpLedgerId === null
|
|
2499
|
+
? settlement?.mcpLedgerPrewritePersisted === false &&
|
|
2500
|
+
settlement?.mcpLedgerSettlementPersisted === false
|
|
2501
|
+
: typeof settlement?.mcpLedgerPrewritePersisted === "boolean" &&
|
|
2502
|
+
typeof settlement?.mcpLedgerSettlementPersisted === "boolean" &&
|
|
2503
|
+
(!settlement.mcpLedgerSettlementPersisted ||
|
|
2504
|
+
settlement.mcpLedgerPrewritePersisted);
|
|
2505
|
+
if (
|
|
2506
|
+
effect.settlementAuthority !== "provider-return" ||
|
|
2507
|
+
effect.result?.workflowEffectId !== effect.id ||
|
|
2508
|
+
!settlement ||
|
|
2509
|
+
typeof settlement !== "object" ||
|
|
2510
|
+
Array.isArray(settlement) ||
|
|
2511
|
+
settlement.protocol !== WORKFLOW_CHILD_EFFECT_PROTOCOL ||
|
|
2512
|
+
settlement.workflowEffectId !== effect.id ||
|
|
2513
|
+
!attempt ||
|
|
2514
|
+
settlement.childSequence !== attempt.childSequence ||
|
|
2515
|
+
settlement.kind !== "tool" ||
|
|
2516
|
+
settlement.tool !== attempt.tool ||
|
|
2517
|
+
settlement.toolUseId !== attempt.toolUseId ||
|
|
2518
|
+
!["completed", "failed"].includes(settlement.status) ||
|
|
2519
|
+
settlement.outcomeUnknown !== false ||
|
|
2520
|
+
!validMcpLedgerId ||
|
|
2521
|
+
!mcpPersistenceValid ||
|
|
2522
|
+
settledIds.has(settlement.childEffectId)
|
|
2523
|
+
) {
|
|
2524
|
+
invalidSettlements += 1;
|
|
2525
|
+
continue;
|
|
2526
|
+
}
|
|
2527
|
+
settledIds.add(settlement.childEffectId);
|
|
2528
|
+
if (
|
|
2529
|
+
mcpLedgerId &&
|
|
2530
|
+
settlement.mcpLedgerPrewritePersisted &&
|
|
2531
|
+
settlement.mcpLedgerSettlementPersisted
|
|
2532
|
+
) {
|
|
2533
|
+
durableMcpSettlements += 1;
|
|
2534
|
+
}
|
|
2535
|
+
settlementLineage.push({
|
|
2536
|
+
effectId: effect.id,
|
|
2537
|
+
ordinal,
|
|
2538
|
+
childEffectId: settlement.childEffectId,
|
|
2539
|
+
childSequence: settlement.childSequence,
|
|
2540
|
+
kind: "tool",
|
|
2541
|
+
tool: settlement.tool,
|
|
2542
|
+
toolUseId: settlement.toolUseId,
|
|
2543
|
+
status: settlement.status,
|
|
2544
|
+
outcomeUnknown: false,
|
|
2545
|
+
mcpLedgerId,
|
|
2546
|
+
mcpLedgerPrewritePersisted:
|
|
2547
|
+
settlement.mcpLedgerPrewritePersisted === true,
|
|
2548
|
+
mcpLedgerSettlementPersisted:
|
|
2549
|
+
settlement.mcpLedgerSettlementPersisted === true,
|
|
2550
|
+
});
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
return {
|
|
2554
|
+
attemptCount: attemptValues.length,
|
|
2555
|
+
settlementCount: settlementValues.length,
|
|
2556
|
+
invalidAttempts,
|
|
2557
|
+
invalidSettlements,
|
|
2558
|
+
missingSettlements: attemptLineage.length - settledIds.size,
|
|
2559
|
+
durableMcpSettlements,
|
|
2560
|
+
truncated:
|
|
2561
|
+
attemptValues.length > visibleAttempts.length ||
|
|
2562
|
+
settlementValues.length > visibleSettlements.length,
|
|
2563
|
+
attemptLineage,
|
|
2564
|
+
settlementLineage,
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
function projectDurableWorkflowCalls(state) {
|
|
2569
|
+
const lineage = state.effects.flatMap((effect) =>
|
|
2570
|
+
(effect.calls || []).map((call) => ({
|
|
2571
|
+
effectId: effect.id,
|
|
2572
|
+
callRecordId: call.id,
|
|
2573
|
+
kind: call.kind,
|
|
2574
|
+
protocol: call.protocol,
|
|
2575
|
+
ownerEffectId: call.ownerEffectId ?? effect.id,
|
|
2576
|
+
childEffectId: call.childEffectId,
|
|
2577
|
+
sequence: call.sequence,
|
|
2578
|
+
name: call.name,
|
|
2579
|
+
source: call.source,
|
|
2580
|
+
requestSource:
|
|
2581
|
+
call.requestSource ?? (call.kind === "provider" ? call.source : null),
|
|
2582
|
+
descendant: (call.ownerEffectId ?? effect.id) !== effect.id,
|
|
2583
|
+
status: call.status,
|
|
2584
|
+
startedAt: call.startedAt,
|
|
2585
|
+
settledAt: call.settledAt,
|
|
2586
|
+
outcomeUnknown: call.outcomeUnknown,
|
|
2587
|
+
settlementCode: call.settlementCode,
|
|
2588
|
+
providerReceipt: call.providerReceiptPersisted
|
|
2589
|
+
? {
|
|
2590
|
+
protocol: "cc-provider-request-receipt/v1",
|
|
2591
|
+
requestId: call.providerReceiptRequestId,
|
|
2592
|
+
responseId: call.providerReceiptResponseId,
|
|
2593
|
+
requestIdentitySemantics: "trace-only",
|
|
2594
|
+
independentlyReadable: false,
|
|
2595
|
+
}
|
|
2596
|
+
: null,
|
|
2597
|
+
mcpLedgerId: call.mcpLedgerId,
|
|
2598
|
+
mcpLedgerPrewritePersisted: call.mcpLedgerPrewritePersisted,
|
|
2599
|
+
mcpLedgerSettlementPersisted: call.mcpLedgerSettlementPersisted,
|
|
2600
|
+
})),
|
|
2601
|
+
);
|
|
2602
|
+
return {
|
|
2603
|
+
authority: "runtime-state-hash-chain-fsync",
|
|
2604
|
+
count: lineage.length,
|
|
2605
|
+
started: lineage.filter((call) => call.status === "started").length,
|
|
2606
|
+
completed: lineage.filter((call) => call.status === "completed").length,
|
|
2607
|
+
failed: lineage.filter((call) => call.status === "failed").length,
|
|
2608
|
+
outcomeUnknown: lineage.filter((call) => call.status === "outcome_unknown")
|
|
2609
|
+
.length,
|
|
2610
|
+
operatorReconciled: lineage.filter(
|
|
2611
|
+
(call) => call.status === "operator_reconciled",
|
|
2612
|
+
).length,
|
|
2613
|
+
descendants: lineage.filter((call) => call.descendant).length,
|
|
2614
|
+
providerReceipts: lineage.filter((call) => call.providerReceipt !== null)
|
|
2615
|
+
.length,
|
|
2616
|
+
providerNativeIdempotencyProven: false,
|
|
2617
|
+
providerReceiptsIndependentlyReadable: false,
|
|
2618
|
+
lineageDigest: digest(
|
|
2619
|
+
"chainlesschain.dynamic-workflow.durable-call-lineage.v1\0",
|
|
2620
|
+
lineage,
|
|
2621
|
+
),
|
|
2622
|
+
lineage,
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
function projectDynamicWorkflowObservability(state) {
|
|
2627
|
+
const settled = state.effects.filter((effect) => effect.status === "settled");
|
|
2628
|
+
const effectLineage = [];
|
|
2629
|
+
const artifactLineage = [];
|
|
2630
|
+
const checkpointLineage = [];
|
|
2631
|
+
const providerRequestAttemptLineage = [];
|
|
2632
|
+
const providerReceiptLineage = [];
|
|
2633
|
+
const nestedEffectAttemptLineage = [];
|
|
2634
|
+
const nestedEffectSettlementLineage = [];
|
|
2635
|
+
const durableCalls = projectDurableWorkflowCalls(state);
|
|
2636
|
+
let requestToSettlementMs = 0;
|
|
2637
|
+
let maxRequestToSettlementMs = 0;
|
|
2638
|
+
let estimatedTokens = 0;
|
|
2639
|
+
let tokenEstimateEffects = 0;
|
|
2640
|
+
let completedTaskEffects = 0;
|
|
2641
|
+
let failedTaskEffects = 0;
|
|
2642
|
+
let invalidTaskResultEffects = 0;
|
|
2643
|
+
let artifactCount = 0;
|
|
2644
|
+
let checkpointCount = 0;
|
|
2645
|
+
let artifactTruncatedEffects = 0;
|
|
2646
|
+
let checkpointTruncatedEffects = 0;
|
|
2647
|
+
let providerRequestAttemptCount = 0;
|
|
2648
|
+
let providerRequestAttemptEffects = 0;
|
|
2649
|
+
let providerReceiptCount = 0;
|
|
2650
|
+
let providerReceiptEffects = 0;
|
|
2651
|
+
let invalidProviderRequestAttempts = 0;
|
|
2652
|
+
let invalidProviderReceipts = 0;
|
|
2653
|
+
let missingProviderRequestReceipts = 0;
|
|
2654
|
+
let providerReceiptTruncatedEffects = 0;
|
|
2655
|
+
let nestedEffectAttemptCount = 0;
|
|
2656
|
+
let nestedEffectSettlementCount = 0;
|
|
2657
|
+
let nestedEffectDurableMcpSettlements = 0;
|
|
2658
|
+
let invalidNestedEffectAttempts = 0;
|
|
2659
|
+
let invalidNestedEffectSettlements = 0;
|
|
2660
|
+
let missingNestedEffectSettlements = 0;
|
|
2661
|
+
let nestedEffectTruncatedEffects = 0;
|
|
2662
|
+
|
|
2663
|
+
for (const effect of settled) {
|
|
2664
|
+
const elapsed = Math.max(
|
|
2665
|
+
0,
|
|
2666
|
+
Date.parse(effect.settledAt) - Date.parse(effect.requestedAt),
|
|
2667
|
+
);
|
|
2668
|
+
requestToSettlementMs += elapsed;
|
|
2669
|
+
maxRequestToSettlementMs = Math.max(maxRequestToSettlementMs, elapsed);
|
|
2670
|
+
|
|
2671
|
+
const taskStatus = effect.result?.status;
|
|
2672
|
+
if (taskStatus === "completed") completedTaskEffects += 1;
|
|
2673
|
+
else if (taskStatus === "failed") failedTaskEffects += 1;
|
|
2674
|
+
else invalidTaskResultEffects += 1;
|
|
2675
|
+
|
|
2676
|
+
const tokenCount = nonNegativeSafeInteger(
|
|
2677
|
+
effect.result?.result?.tokenCount,
|
|
2678
|
+
);
|
|
2679
|
+
if (tokenCount !== null) {
|
|
2680
|
+
estimatedTokens += tokenCount;
|
|
2681
|
+
tokenEstimateEffects += 1;
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
const artifacts = projectResultValues(
|
|
2685
|
+
effect,
|
|
2686
|
+
"artifacts",
|
|
2687
|
+
"chainlesschain.dynamic-workflow.artifact-observation.v1\0",
|
|
2688
|
+
);
|
|
2689
|
+
artifactCount += artifacts.count;
|
|
2690
|
+
artifactLineage.push(...artifacts.lineage);
|
|
2691
|
+
if (artifacts.truncated) artifactTruncatedEffects += 1;
|
|
2692
|
+
|
|
2693
|
+
const checkpoints = projectResultValues(
|
|
2694
|
+
effect,
|
|
2695
|
+
"checkpoints",
|
|
2696
|
+
"chainlesschain.dynamic-workflow.checkpoint-observation.v1\0",
|
|
2697
|
+
);
|
|
2698
|
+
checkpointCount += checkpoints.count;
|
|
2699
|
+
checkpointLineage.push(...checkpoints.lineage);
|
|
2700
|
+
if (checkpoints.truncated) checkpointTruncatedEffects += 1;
|
|
2701
|
+
|
|
2702
|
+
const providerReceipts = projectProviderRequestReceipts(effect);
|
|
2703
|
+
providerRequestAttemptCount += providerReceipts.attemptCount;
|
|
2704
|
+
providerReceiptCount += providerReceipts.receiptCount;
|
|
2705
|
+
providerRequestAttemptLineage.push(...providerReceipts.attemptLineage);
|
|
2706
|
+
providerReceiptLineage.push(...providerReceipts.receiptLineage);
|
|
2707
|
+
invalidProviderRequestAttempts += providerReceipts.invalidAttempts;
|
|
2708
|
+
invalidProviderReceipts += providerReceipts.invalidReceipts;
|
|
2709
|
+
missingProviderRequestReceipts += providerReceipts.missingReceipts;
|
|
2710
|
+
if (providerReceipts.attemptLineage.length > 0) {
|
|
2711
|
+
providerRequestAttemptEffects += 1;
|
|
2712
|
+
}
|
|
2713
|
+
if (providerReceipts.receiptLineage.length > 0) {
|
|
2714
|
+
providerReceiptEffects += 1;
|
|
2715
|
+
}
|
|
2716
|
+
if (providerReceipts.truncated) providerReceiptTruncatedEffects += 1;
|
|
2717
|
+
|
|
2718
|
+
const nestedEffects = projectNestedToolEffects(effect);
|
|
2719
|
+
nestedEffectAttemptCount += nestedEffects.attemptCount;
|
|
2720
|
+
nestedEffectSettlementCount += nestedEffects.settlementCount;
|
|
2721
|
+
nestedEffectDurableMcpSettlements += nestedEffects.durableMcpSettlements;
|
|
2722
|
+
invalidNestedEffectAttempts += nestedEffects.invalidAttempts;
|
|
2723
|
+
invalidNestedEffectSettlements += nestedEffects.invalidSettlements;
|
|
2724
|
+
missingNestedEffectSettlements += nestedEffects.missingSettlements;
|
|
2725
|
+
nestedEffectAttemptLineage.push(...nestedEffects.attemptLineage);
|
|
2726
|
+
nestedEffectSettlementLineage.push(...nestedEffects.settlementLineage);
|
|
2727
|
+
if (nestedEffects.truncated) nestedEffectTruncatedEffects += 1;
|
|
2728
|
+
|
|
2729
|
+
effectLineage.push({
|
|
2730
|
+
effectId: effect.id,
|
|
2731
|
+
stepId: effect.stepId,
|
|
2732
|
+
iteration: effect.iteration,
|
|
2733
|
+
attempt: effect.attempt,
|
|
2734
|
+
status: effect.status,
|
|
2735
|
+
batchId: effect.batchId || null,
|
|
2736
|
+
batchIndex: effect.batchIndex ?? null,
|
|
2737
|
+
batchSize: effect.batchSize ?? null,
|
|
2738
|
+
timeoutMs: effect.timeoutMs ?? null,
|
|
2739
|
+
timeoutObservedAt: effect.timeoutObservedAt ?? null,
|
|
2740
|
+
providerDispatchedAt: effect.providerDispatchedAt ?? null,
|
|
2741
|
+
taskStatus:
|
|
2742
|
+
taskStatus === "completed" || taskStatus === "failed"
|
|
2743
|
+
? taskStatus
|
|
2744
|
+
: "invalid",
|
|
2745
|
+
payloadDigest: effect.payloadDigest,
|
|
2746
|
+
resultDigest: effect.resultDigest,
|
|
2747
|
+
settlementAuthority: effect.settlementAuthority,
|
|
2748
|
+
requestedAt: effect.requestedAt,
|
|
2749
|
+
settledAt: effect.settledAt,
|
|
2750
|
+
requestToSettlementMs: elapsed,
|
|
2751
|
+
});
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
const gaps = [
|
|
2755
|
+
"provider-token-usage-unavailable",
|
|
2756
|
+
"provider-cost-usd-unavailable",
|
|
2757
|
+
"provider-native-idempotency-unavailable",
|
|
2758
|
+
"provider-receipt-independent-readback-unavailable",
|
|
2759
|
+
"checkpoint-provider-readback-unavailable",
|
|
2760
|
+
"artifact-store-readback-unavailable",
|
|
2761
|
+
"nested-tool-independent-ledger-incomplete",
|
|
2762
|
+
];
|
|
2763
|
+
if (tokenEstimateEffects !== settled.length) {
|
|
2764
|
+
gaps.push("cowork-token-estimate-incomplete");
|
|
2765
|
+
}
|
|
2766
|
+
if (artifactTruncatedEffects > 0) {
|
|
2767
|
+
gaps.push("artifact-lineage-projection-truncated");
|
|
2768
|
+
}
|
|
2769
|
+
if (checkpointTruncatedEffects > 0) {
|
|
2770
|
+
gaps.push("checkpoint-lineage-projection-truncated");
|
|
2771
|
+
}
|
|
2772
|
+
const providerReturnedEffects = settled.filter(
|
|
2773
|
+
(effect) => effect.settlementAuthority === "provider-return",
|
|
2774
|
+
).length;
|
|
2775
|
+
if (
|
|
2776
|
+
providerReceiptEffects !== providerReturnedEffects ||
|
|
2777
|
+
missingProviderRequestReceipts > 0
|
|
2778
|
+
) {
|
|
2779
|
+
gaps.push("provider-request-receipt-incomplete");
|
|
2780
|
+
}
|
|
2781
|
+
if (invalidProviderRequestAttempts > 0) {
|
|
2782
|
+
gaps.push("provider-request-attempt-invalid");
|
|
2783
|
+
}
|
|
2784
|
+
if (invalidProviderReceipts > 0) {
|
|
2785
|
+
gaps.push("provider-request-receipt-invalid");
|
|
2786
|
+
}
|
|
2787
|
+
if (providerReceiptTruncatedEffects > 0) {
|
|
2788
|
+
gaps.push("provider-request-receipt-projection-truncated");
|
|
2789
|
+
}
|
|
2790
|
+
if (invalidNestedEffectAttempts > 0) {
|
|
2791
|
+
gaps.push("nested-tool-effect-attempt-invalid");
|
|
2792
|
+
}
|
|
2793
|
+
if (invalidNestedEffectSettlements > 0) {
|
|
2794
|
+
gaps.push("nested-tool-effect-settlement-invalid");
|
|
2795
|
+
}
|
|
2796
|
+
if (missingNestedEffectSettlements > 0) {
|
|
2797
|
+
gaps.push("nested-tool-effect-settlement-incomplete");
|
|
2798
|
+
}
|
|
2799
|
+
if (nestedEffectTruncatedEffects > 0) {
|
|
2800
|
+
gaps.push("nested-tool-effect-projection-truncated");
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
return snapshotJson(
|
|
2804
|
+
{
|
|
2805
|
+
schema: DYNAMIC_WORKFLOW_OBSERVABILITY_SCHEMA,
|
|
2806
|
+
complete: false,
|
|
2807
|
+
effects: {
|
|
2808
|
+
requested: state.effects.length,
|
|
2809
|
+
settled: settled.length,
|
|
2810
|
+
pending: state.effects.length - settled.length,
|
|
2811
|
+
providerReturned: settled.filter(
|
|
2812
|
+
(effect) => effect.settlementAuthority === "provider-return",
|
|
2813
|
+
).length,
|
|
2814
|
+
operatorReconciled: settled.filter(
|
|
2815
|
+
(effect) => effect.settlementAuthority === "operator-reconciled",
|
|
2816
|
+
).length,
|
|
2817
|
+
runtimeNotDispatched: settled.filter(
|
|
2818
|
+
(effect) => effect.settlementAuthority === "runtime-not-dispatched",
|
|
2819
|
+
).length,
|
|
2820
|
+
providerDispatched: state.effects.filter(
|
|
2821
|
+
(effect) => typeof effect.providerDispatchedAt === "string",
|
|
2822
|
+
).length,
|
|
2823
|
+
timeoutObserved: state.effects.filter(
|
|
2824
|
+
(effect) => typeof effect.timeoutObservedAt === "string",
|
|
2825
|
+
).length,
|
|
2826
|
+
completedTasks: completedTaskEffects,
|
|
2827
|
+
failedTasks: failedTaskEffects,
|
|
2828
|
+
invalidTaskResults: invalidTaskResultEffects,
|
|
2829
|
+
lineageDigest: digest(
|
|
2830
|
+
"chainlesschain.dynamic-workflow.effect-observability.v1\0",
|
|
2831
|
+
effectLineage,
|
|
2832
|
+
),
|
|
2833
|
+
lineage: effectLineage,
|
|
2834
|
+
},
|
|
2835
|
+
duration: {
|
|
2836
|
+
authority: "request-to-settlement-wall-clock",
|
|
2837
|
+
observedEffects: settled.length,
|
|
2838
|
+
totalMs: requestToSettlementMs,
|
|
2839
|
+
maxMs: maxRequestToSettlementMs,
|
|
2840
|
+
},
|
|
2841
|
+
tokens: {
|
|
2842
|
+
authority: "cowork-result-heuristic",
|
|
2843
|
+
estimated: estimatedTokens,
|
|
2844
|
+
observedEffects: tokenEstimateEffects,
|
|
2845
|
+
missingEffects: settled.length - tokenEstimateEffects,
|
|
2846
|
+
providerReported: null,
|
|
2847
|
+
},
|
|
2848
|
+
cost: {
|
|
2849
|
+
authority: "unavailable",
|
|
2850
|
+
reportedUsd: null,
|
|
2851
|
+
observedEffects: 0,
|
|
2852
|
+
},
|
|
2853
|
+
providerReceipts: {
|
|
2854
|
+
authority: "provider-returned-trace-only",
|
|
2855
|
+
count: providerReceiptCount,
|
|
2856
|
+
projectedRecords: providerReceiptLineage.length,
|
|
2857
|
+
requestAttempts: providerRequestAttemptCount,
|
|
2858
|
+
projectedRequestAttempts: providerRequestAttemptLineage.length,
|
|
2859
|
+
requestAttemptEffects: providerRequestAttemptEffects,
|
|
2860
|
+
observedEffects: providerReceiptEffects,
|
|
2861
|
+
missingProviderReturnedEffects:
|
|
2862
|
+
providerReturnedEffects - providerReceiptEffects,
|
|
2863
|
+
missingRequestReceipts: missingProviderRequestReceipts,
|
|
2864
|
+
invalidRequestAttempts: invalidProviderRequestAttempts,
|
|
2865
|
+
invalidRecords: invalidProviderReceipts,
|
|
2866
|
+
truncatedEffects: providerReceiptTruncatedEffects,
|
|
2867
|
+
nativeIdempotencyProven: false,
|
|
2868
|
+
independentlyReadable: false,
|
|
2869
|
+
requestAttemptLineageDigest: digest(
|
|
2870
|
+
"chainlesschain.dynamic-workflow.provider-request-attempt-lineage.v1\0",
|
|
2871
|
+
providerRequestAttemptLineage,
|
|
2872
|
+
),
|
|
2873
|
+
lineageDigest: digest(
|
|
2874
|
+
"chainlesschain.dynamic-workflow.provider-request-receipt-lineage.v1\0",
|
|
2875
|
+
providerReceiptLineage,
|
|
2876
|
+
),
|
|
2877
|
+
requestAttemptLineage: providerRequestAttemptLineage,
|
|
2878
|
+
lineage: providerReceiptLineage,
|
|
2879
|
+
},
|
|
2880
|
+
durableCalls,
|
|
2881
|
+
nestedEffects: {
|
|
2882
|
+
authority: "task-result-bound-with-mcp-session-ledger-flags",
|
|
2883
|
+
attempts: nestedEffectAttemptCount,
|
|
2884
|
+
settlements: nestedEffectSettlementCount,
|
|
2885
|
+
projectedAttempts: nestedEffectAttemptLineage.length,
|
|
2886
|
+
projectedSettlements: nestedEffectSettlementLineage.length,
|
|
2887
|
+
durableMcpSettlements: nestedEffectDurableMcpSettlements,
|
|
2888
|
+
missingSettlements: missingNestedEffectSettlements,
|
|
2889
|
+
invalidAttempts: invalidNestedEffectAttempts,
|
|
2890
|
+
invalidSettlements: invalidNestedEffectSettlements,
|
|
2891
|
+
truncatedEffects: nestedEffectTruncatedEffects,
|
|
2892
|
+
allEffectsIndependentlyDurable: false,
|
|
2893
|
+
attemptLineageDigest: digest(
|
|
2894
|
+
"chainlesschain.dynamic-workflow.nested-effect-attempt-lineage.v1\0",
|
|
2895
|
+
nestedEffectAttemptLineage,
|
|
2896
|
+
),
|
|
2897
|
+
settlementLineageDigest: digest(
|
|
2898
|
+
"chainlesschain.dynamic-workflow.nested-effect-settlement-lineage.v1\0",
|
|
2899
|
+
nestedEffectSettlementLineage,
|
|
2900
|
+
),
|
|
2901
|
+
attemptLineage: nestedEffectAttemptLineage,
|
|
2902
|
+
settlementLineage: nestedEffectSettlementLineage,
|
|
2903
|
+
},
|
|
2904
|
+
artifacts: {
|
|
2905
|
+
authority: "task-result-digest-only",
|
|
2906
|
+
count: artifactCount,
|
|
2907
|
+
projectedRecords: artifactLineage.length,
|
|
2908
|
+
truncatedEffects: artifactTruncatedEffects,
|
|
2909
|
+
lineageDigest: digest(
|
|
2910
|
+
"chainlesschain.dynamic-workflow.artifact-lineage.v1\0",
|
|
2911
|
+
artifactLineage,
|
|
2912
|
+
),
|
|
2913
|
+
lineage: artifactLineage,
|
|
2914
|
+
},
|
|
2915
|
+
checkpoints: {
|
|
2916
|
+
authority: "task-result-digest-only",
|
|
2917
|
+
count: checkpointCount,
|
|
2918
|
+
projectedRecords: checkpointLineage.length,
|
|
2919
|
+
truncatedEffects: checkpointTruncatedEffects,
|
|
2920
|
+
lineageDigest: digest(
|
|
2921
|
+
"chainlesschain.dynamic-workflow.checkpoint-lineage.v1\0",
|
|
2922
|
+
checkpointLineage,
|
|
2923
|
+
),
|
|
2924
|
+
lineage: checkpointLineage,
|
|
2925
|
+
},
|
|
2926
|
+
gaps,
|
|
2927
|
+
},
|
|
2928
|
+
"dynamic workflow observability projection",
|
|
2929
|
+
2 * 1024 * 1024,
|
|
2930
|
+
);
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
export function projectDynamicWorkflowRuntime(stateOrPath) {
|
|
2934
|
+
const state =
|
|
2935
|
+
typeof stateOrPath === "string"
|
|
2936
|
+
? readDynamicWorkflowRuntimeState(stateOrPath)
|
|
2937
|
+
: verifyDynamicWorkflowRuntimeState(stateOrPath);
|
|
2938
|
+
return Object.freeze({
|
|
2939
|
+
schema: DYNAMIC_WORKFLOW_RUNTIME_SCHEMA,
|
|
2940
|
+
runId: state.runId,
|
|
2941
|
+
workflowId: state.workflowId,
|
|
2942
|
+
revision: state.revision,
|
|
2943
|
+
status: state.status,
|
|
2944
|
+
definitionDigest: state.definitionDigest,
|
|
2945
|
+
admissionDigest: state.admissionDigest,
|
|
2946
|
+
executionAuthoritySessionId: state.executionAuthoritySessionId,
|
|
2947
|
+
effectCount: state.effects.length,
|
|
2948
|
+
settledEffectCount: state.effects.filter(
|
|
2949
|
+
(effect) => effect.status === "settled",
|
|
2950
|
+
).length,
|
|
2951
|
+
pendingEffects: Object.freeze(
|
|
2952
|
+
state.effects
|
|
2953
|
+
.filter((effect) => effect.status === "pending")
|
|
2954
|
+
.map((effect) =>
|
|
2955
|
+
Object.freeze({
|
|
2956
|
+
id: effect.id,
|
|
2957
|
+
key: effect.key,
|
|
2958
|
+
payloadDigest: effect.payloadDigest,
|
|
2959
|
+
batchId: effect.batchId || null,
|
|
2960
|
+
batchIndex: effect.batchIndex ?? null,
|
|
2961
|
+
batchSize: effect.batchSize ?? null,
|
|
2962
|
+
timeoutMs: effect.timeoutMs ?? null,
|
|
2963
|
+
timeoutObservedAt: effect.timeoutObservedAt ?? null,
|
|
2964
|
+
providerDispatchedAt: effect.providerDispatchedAt ?? null,
|
|
2965
|
+
requestedAt: effect.requestedAt,
|
|
2966
|
+
}),
|
|
2967
|
+
),
|
|
2968
|
+
),
|
|
2969
|
+
finalRecordStatus: state.finalRecord?.status || null,
|
|
2970
|
+
observability: projectDynamicWorkflowObservability(state),
|
|
2971
|
+
updatedAt: state.updatedAt,
|
|
2972
|
+
stateDigest: state.stateDigest,
|
|
2973
|
+
});
|
|
2974
|
+
}
|