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
|
@@ -31,6 +31,27 @@ const SUMMARY_DIRECT_THRESHOLD = 500; // chars — below this, use result as-is
|
|
|
31
31
|
const SUMMARY_SECTION_PATTERN =
|
|
32
32
|
/^##\s*(Summary|Result|Output|Conclusion|Answer)/im;
|
|
33
33
|
const TRUNCATE_LENGTH = 500;
|
|
34
|
+
const PROVIDER_CLIENT_REQUEST_ID_RE = /^ccwf_[a-f0-9]{64}$/;
|
|
35
|
+
const PROVIDER_CALL_ID_RE = /^[A-Za-z0-9._:-]{1,256}$/;
|
|
36
|
+
const PROVIDER_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
37
|
+
const WORKFLOW_EFFECT_ID_RE = /^sha256:[a-f0-9]{64}$/;
|
|
38
|
+
const WORKFLOW_CHILD_EFFECT_PROTOCOL = "cc-workflow-child-effect/v1";
|
|
39
|
+
const WORKFLOW_TOOL_CALL_ID_RE = /^[\x21-\x7e]{1,512}$/;
|
|
40
|
+
const WORKFLOW_TOOL_NAME_RE = /^[A-Za-z0-9._:-]{1,256}$/;
|
|
41
|
+
|
|
42
|
+
function ownDataValue(value, property) {
|
|
43
|
+
if (!value || (typeof value !== "object" && typeof value !== "function")) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, property);
|
|
48
|
+
return descriptor && Object.hasOwn(descriptor, "value")
|
|
49
|
+
? descriptor.value
|
|
50
|
+
: undefined;
|
|
51
|
+
} catch {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
34
55
|
|
|
35
56
|
function runtimeUsageObserverFailure(error, code, message) {
|
|
36
57
|
const failure =
|
|
@@ -132,6 +153,8 @@ export class SubAgentContext {
|
|
|
132
153
|
* @param {boolean} [options.memoryEnabled=true] - When false, the child's
|
|
133
154
|
* context engine suppresses hierarchical-memory recall (contract memory:false)
|
|
134
155
|
* @param {object} [options.llmOptions] - LLM provider/model/key options
|
|
156
|
+
* @param {string} [options.workflowEffectId] - Durable workflow effect bound
|
|
157
|
+
* to provider request identities for this child run
|
|
135
158
|
* @param {string} [options.cwd] - Working directory
|
|
136
159
|
* @param {boolean} [options.useWorktree] - Force worktree isolation (overrides flag)
|
|
137
160
|
* @param {function} [options.onUsageBoundary] - Synchronous observer for
|
|
@@ -207,6 +230,11 @@ export class SubAgentContext {
|
|
|
207
230
|
this._runId = null;
|
|
208
231
|
this._checkpointIds = [];
|
|
209
232
|
this._toolUseIds = [];
|
|
233
|
+
this._workflowEffectId = options.workflowEffectId ?? null;
|
|
234
|
+
this._providerRequestAttempts = [];
|
|
235
|
+
this._providerRequestReceipts = [];
|
|
236
|
+
this._nestedEffectAttempts = [];
|
|
237
|
+
this._nestedEffectSettlements = [];
|
|
210
238
|
|
|
211
239
|
// ── Isolated state ──────────────────────────────────────────────
|
|
212
240
|
// Independent message history — never shared with parent
|
|
@@ -547,7 +575,9 @@ export class SubAgentContext {
|
|
|
547
575
|
}
|
|
548
576
|
return result;
|
|
549
577
|
} catch (err) {
|
|
550
|
-
if (err?.runtimeLedgerPersistence === true)
|
|
578
|
+
if (err?.runtimeLedgerPersistence === true || this._workflowEffectId) {
|
|
579
|
+
throw err;
|
|
580
|
+
}
|
|
551
581
|
// If worktree creation fails (e.g. not a git repo), fall back to direct
|
|
552
582
|
this.status = "failed";
|
|
553
583
|
this.completedAt = new Date().toISOString();
|
|
@@ -615,6 +645,17 @@ export class SubAgentContext {
|
|
|
615
645
|
}
|
|
616
646
|
options.toolAdmission = this.toolAdmission;
|
|
617
647
|
options.skillAllowlist = this.skillAllowlist;
|
|
648
|
+
// A caller cannot replace the durable outer effect through loopOptions.
|
|
649
|
+
// agentLoop validates the canonical sha256 form before any provider call.
|
|
650
|
+
if (this._workflowEffectId) {
|
|
651
|
+
options.workflowEffectId = this._workflowEffectId;
|
|
652
|
+
options.onProviderRequestBoundary = (event) => {
|
|
653
|
+
this._recordProviderRequestAttempt(event);
|
|
654
|
+
};
|
|
655
|
+
} else {
|
|
656
|
+
delete options.workflowEffectId;
|
|
657
|
+
delete options.onProviderRequestBoundary;
|
|
658
|
+
}
|
|
618
659
|
const authority = this._parentAuthority;
|
|
619
660
|
if (authority.permissionRules) {
|
|
620
661
|
options.permissionRules = authority.permissionRules;
|
|
@@ -651,6 +692,22 @@ export class SubAgentContext {
|
|
|
651
692
|
let observeUsageSettlement = null;
|
|
652
693
|
let observeToolBoundary = null;
|
|
653
694
|
let observeToolSettlement = null;
|
|
695
|
+
const providerReceiptForSettlement = (event) => {
|
|
696
|
+
for (
|
|
697
|
+
let index = this._providerRequestReceipts.length - 1;
|
|
698
|
+
index >= 0;
|
|
699
|
+
index -= 1
|
|
700
|
+
) {
|
|
701
|
+
const receipt = this._providerRequestReceipts[index];
|
|
702
|
+
if (
|
|
703
|
+
receipt.callId === event?.callId &&
|
|
704
|
+
(!event?.provider || receipt.provider === event.provider)
|
|
705
|
+
) {
|
|
706
|
+
return { ...receipt };
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return null;
|
|
710
|
+
};
|
|
654
711
|
if (strictUsageTelemetry) {
|
|
655
712
|
// Presence is checked without invoking either observer. In particular,
|
|
656
713
|
// do not manufacture a null "validation event" that a durable writer
|
|
@@ -805,11 +862,29 @@ export class SubAgentContext {
|
|
|
805
862
|
}
|
|
806
863
|
}
|
|
807
864
|
if (event.type === "tool-executing" && event.tool_use_id) {
|
|
865
|
+
this._recordNestedEffectAttempt(event);
|
|
808
866
|
const toolUseId = String(event.tool_use_id);
|
|
809
867
|
if (!this._toolUseIds.includes(toolUseId)) {
|
|
810
868
|
this._toolUseIds.push(toolUseId);
|
|
811
869
|
}
|
|
812
870
|
}
|
|
871
|
+
if (event.type === "provider-request-receipt") {
|
|
872
|
+
this._providerRequestReceipts.push(
|
|
873
|
+
Object.freeze({
|
|
874
|
+
protocol: event.protocol,
|
|
875
|
+
provider: event.provider,
|
|
876
|
+
workflowEffectId: event.workflowEffectId,
|
|
877
|
+
callId: event.callId,
|
|
878
|
+
callSequence: event.callSequence,
|
|
879
|
+
source: event.source,
|
|
880
|
+
clientRequestId: event.clientRequestId,
|
|
881
|
+
requestId: event.requestId || null,
|
|
882
|
+
responseId: event.responseId || null,
|
|
883
|
+
requestIdentitySemantics: event.requestIdentitySemantics,
|
|
884
|
+
independentlyReadable: event.independentlyReadable,
|
|
885
|
+
}),
|
|
886
|
+
);
|
|
887
|
+
}
|
|
813
888
|
|
|
814
889
|
if (event.type === "token-usage") {
|
|
815
890
|
const forwardedUsage = {
|
|
@@ -819,6 +894,7 @@ export class SubAgentContext {
|
|
|
819
894
|
model: event.model || null,
|
|
820
895
|
usage: event.usage || null,
|
|
821
896
|
...(event.source ? { source: event.source } : {}),
|
|
897
|
+
providerReceipt: providerReceiptForSettlement(event),
|
|
822
898
|
attribution: event.attribution || null,
|
|
823
899
|
...(event.boundaryNotified === true ||
|
|
824
900
|
event.ledgerPersisted === true
|
|
@@ -865,6 +941,9 @@ export class SubAgentContext {
|
|
|
865
941
|
}
|
|
866
942
|
|
|
867
943
|
if (event.type === "model-usage-started") {
|
|
944
|
+
if (event.providerRequestId) {
|
|
945
|
+
this._recordProviderRequestAttempt(event);
|
|
946
|
+
}
|
|
868
947
|
if (strictUsageTelemetry) {
|
|
869
948
|
if (
|
|
870
949
|
event.boundaryNotified === true ||
|
|
@@ -896,6 +975,7 @@ export class SubAgentContext {
|
|
|
896
975
|
? "semantic-compaction"
|
|
897
976
|
: "model"),
|
|
898
977
|
code: event.code || "provider_transport_outcome_unknown",
|
|
978
|
+
providerReceipt: providerReceiptForSettlement(event),
|
|
899
979
|
attribution: event.attribution || null,
|
|
900
980
|
...(event.boundaryNotified === true ||
|
|
901
981
|
event.ledgerPersisted === true
|
|
@@ -932,9 +1012,26 @@ export class SubAgentContext {
|
|
|
932
1012
|
}
|
|
933
1013
|
|
|
934
1014
|
if (event.type === "tool-result") {
|
|
1015
|
+
const nestedEffectSettlement =
|
|
1016
|
+
this._recordNestedEffectSettlement(event);
|
|
935
1017
|
if (strictUsageTelemetry && event.tool_use_id) {
|
|
936
1018
|
observeToolSettlement(event);
|
|
937
1019
|
}
|
|
1020
|
+
// A consumer may stop the async generator after this yielded result
|
|
1021
|
+
// because of cancellation or a local token budget. Fence the unknown
|
|
1022
|
+
// here, after the strict settlement observer closes its boundary, so
|
|
1023
|
+
// generator.return() cannot suppress the outer reconciliation signal.
|
|
1024
|
+
if (nestedEffectSettlement?.outcomeUnknown === true) {
|
|
1025
|
+
const error = new Error(
|
|
1026
|
+
`Workflow-bound nested tool ${nestedEffectSettlement.tool} has an unknown outcome and requires reconciliation`,
|
|
1027
|
+
);
|
|
1028
|
+
error.code = "CC_WORKFLOW_NESTED_TOOL_OUTCOME_UNKNOWN";
|
|
1029
|
+
error.workflowEffectOutcomeUnknown = true;
|
|
1030
|
+
error.workflowEffectId = nestedEffectSettlement.workflowEffectId;
|
|
1031
|
+
error.workflowChildEffectId =
|
|
1032
|
+
nestedEffectSettlement.childEffectId;
|
|
1033
|
+
throw error;
|
|
1034
|
+
}
|
|
938
1035
|
// Store large tool results as artifacts
|
|
939
1036
|
const resultStr = JSON.stringify(event.result);
|
|
940
1037
|
// Estimate token count from tool result (~4 chars per token)
|
|
@@ -1004,8 +1101,22 @@ export class SubAgentContext {
|
|
|
1004
1101
|
"Strict child execution ended with an unsettled provider or tool call",
|
|
1005
1102
|
);
|
|
1006
1103
|
}
|
|
1104
|
+
if (
|
|
1105
|
+
this._workflowEffectId &&
|
|
1106
|
+
this._nestedEffectAttempts.length !==
|
|
1107
|
+
this._nestedEffectSettlements.length
|
|
1108
|
+
) {
|
|
1109
|
+
const error = new Error(
|
|
1110
|
+
"Workflow-bound child execution ended with an unsettled nested effect",
|
|
1111
|
+
);
|
|
1112
|
+
error.code = "CC_WORKFLOW_CHILD_EFFECT_SETTLEMENT_INCOMPLETE";
|
|
1113
|
+
error.workflowEffectOutcomeUnknown = true;
|
|
1114
|
+
throw error;
|
|
1115
|
+
}
|
|
1007
1116
|
} catch (err) {
|
|
1008
|
-
if (err?.runtimeLedgerPersistence === true)
|
|
1117
|
+
if (err?.runtimeLedgerPersistence === true || this._workflowEffectId) {
|
|
1118
|
+
throw err;
|
|
1119
|
+
}
|
|
1009
1120
|
if (this.isAborted()) {
|
|
1010
1121
|
this.forceComplete(this._cancelReason || "cancelled", {
|
|
1011
1122
|
partialContent: lastContent,
|
|
@@ -1159,11 +1270,166 @@ export class SubAgentContext {
|
|
|
1159
1270
|
parentTraceId: this._hookParentTraceId || null,
|
|
1160
1271
|
checkpointIds: [...this._checkpointIds],
|
|
1161
1272
|
toolUseIds: [...this._toolUseIds],
|
|
1273
|
+
...(this._workflowEffectId
|
|
1274
|
+
? {
|
|
1275
|
+
providerRequestAttempts: this.providerRequestAttempts(),
|
|
1276
|
+
providerRequestReceipts: this.providerRequestReceipts(),
|
|
1277
|
+
nestedEffectAttempts: this.nestedEffectAttempts(),
|
|
1278
|
+
nestedEffectSettlements: this.nestedEffectSettlements(),
|
|
1279
|
+
}
|
|
1280
|
+
: {}),
|
|
1162
1281
|
worktreeId: worktree?.branch || this._worktreeBranch || null,
|
|
1163
1282
|
worktreePath: worktree?.path || this._worktreePath || null,
|
|
1164
1283
|
};
|
|
1165
1284
|
}
|
|
1166
1285
|
|
|
1286
|
+
/**
|
|
1287
|
+
* Effect-bound provider calls admitted before transport dispatch.
|
|
1288
|
+
*/
|
|
1289
|
+
providerRequestAttempts() {
|
|
1290
|
+
return this._providerRequestAttempts.map((attempt) => ({ ...attempt }));
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
/**
|
|
1294
|
+
* Provider-returned request identifiers observed during this child run.
|
|
1295
|
+
* Values are copied so callers cannot mutate recovery evidence in place.
|
|
1296
|
+
*/
|
|
1297
|
+
providerRequestReceipts() {
|
|
1298
|
+
return this._providerRequestReceipts.map((receipt) => ({ ...receipt }));
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
nestedEffectAttempts() {
|
|
1302
|
+
return this._nestedEffectAttempts.map((attempt) => ({ ...attempt }));
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
nestedEffectSettlements() {
|
|
1306
|
+
return this._nestedEffectSettlements.map((settlement) => ({
|
|
1307
|
+
...settlement,
|
|
1308
|
+
}));
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
_recordNestedEffectAttempt(event) {
|
|
1312
|
+
if (!this._workflowEffectId) return;
|
|
1313
|
+
const attempt = {
|
|
1314
|
+
protocol: event?.workflowEffectProtocol,
|
|
1315
|
+
workflowEffectId: event?.workflowEffectId,
|
|
1316
|
+
childEffectId: event?.workflowChildEffectId,
|
|
1317
|
+
childSequence: event?.workflowChildSequence,
|
|
1318
|
+
kind: "tool",
|
|
1319
|
+
tool: event?.tool,
|
|
1320
|
+
toolUseId: event?.tool_use_id,
|
|
1321
|
+
identitySemantics: "runtime-derived",
|
|
1322
|
+
};
|
|
1323
|
+
if (
|
|
1324
|
+
attempt.protocol !== WORKFLOW_CHILD_EFFECT_PROTOCOL ||
|
|
1325
|
+
attempt.workflowEffectId !== this._workflowEffectId ||
|
|
1326
|
+
!WORKFLOW_EFFECT_ID_RE.test(attempt.childEffectId || "") ||
|
|
1327
|
+
!Number.isSafeInteger(attempt.childSequence) ||
|
|
1328
|
+
attempt.childSequence < 1 ||
|
|
1329
|
+
!WORKFLOW_TOOL_NAME_RE.test(attempt.tool || "") ||
|
|
1330
|
+
!WORKFLOW_TOOL_CALL_ID_RE.test(attempt.toolUseId || "") ||
|
|
1331
|
+
this._nestedEffectAttempts.some(
|
|
1332
|
+
(existing) =>
|
|
1333
|
+
existing.childEffectId === attempt.childEffectId ||
|
|
1334
|
+
existing.childSequence === attempt.childSequence,
|
|
1335
|
+
)
|
|
1336
|
+
) {
|
|
1337
|
+
const error = new TypeError(
|
|
1338
|
+
"workflow nested-effect boundary is malformed or duplicated",
|
|
1339
|
+
);
|
|
1340
|
+
error.code = "CC_WORKFLOW_CHILD_EFFECT_BOUNDARY_INVALID";
|
|
1341
|
+
throw error;
|
|
1342
|
+
}
|
|
1343
|
+
this._nestedEffectAttempts.push(Object.freeze(attempt));
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
_recordNestedEffectSettlement(event) {
|
|
1347
|
+
if (!this._workflowEffectId || !event?.workflowChildEffectId) return null;
|
|
1348
|
+
const attempt = this._nestedEffectAttempts.find(
|
|
1349
|
+
(candidate) =>
|
|
1350
|
+
candidate.childEffectId === event.workflowChildEffectId,
|
|
1351
|
+
);
|
|
1352
|
+
const outcomeUnknown = ownDataValue(event.result, "outcomeUnknown") === true;
|
|
1353
|
+
const resultError = ownDataValue(event.result, "error");
|
|
1354
|
+
const mcpLedgerId = ownDataValue(event.result, "mcpLedgerId") ?? null;
|
|
1355
|
+
const settlement = {
|
|
1356
|
+
protocol: event?.workflowEffectProtocol,
|
|
1357
|
+
workflowEffectId: event?.workflowEffectId,
|
|
1358
|
+
childEffectId: event?.workflowChildEffectId,
|
|
1359
|
+
childSequence: event?.workflowChildSequence,
|
|
1360
|
+
kind: "tool",
|
|
1361
|
+
tool: event?.tool,
|
|
1362
|
+
toolUseId: event?.tool_use_id,
|
|
1363
|
+
status: outcomeUnknown
|
|
1364
|
+
? "outcome_unknown"
|
|
1365
|
+
: event?.error || resultError
|
|
1366
|
+
? "failed"
|
|
1367
|
+
: "completed",
|
|
1368
|
+
outcomeUnknown,
|
|
1369
|
+
mcpLedgerId,
|
|
1370
|
+
mcpLedgerPrewritePersisted:
|
|
1371
|
+
ownDataValue(event.result, "mcpLedgerPrewritePersisted") === true,
|
|
1372
|
+
mcpLedgerSettlementPersisted:
|
|
1373
|
+
ownDataValue(event.result, "mcpLedgerSettlementPersisted") === true,
|
|
1374
|
+
};
|
|
1375
|
+
if (
|
|
1376
|
+
!attempt ||
|
|
1377
|
+
settlement.protocol !== WORKFLOW_CHILD_EFFECT_PROTOCOL ||
|
|
1378
|
+
settlement.workflowEffectId !== this._workflowEffectId ||
|
|
1379
|
+
settlement.childSequence !== attempt.childSequence ||
|
|
1380
|
+
settlement.tool !== attempt.tool ||
|
|
1381
|
+
settlement.toolUseId !== attempt.toolUseId ||
|
|
1382
|
+
(mcpLedgerId !== null &&
|
|
1383
|
+
!PROVIDER_CALL_ID_RE.test(String(mcpLedgerId))) ||
|
|
1384
|
+
this._nestedEffectSettlements.some(
|
|
1385
|
+
(existing) => existing.childEffectId === settlement.childEffectId,
|
|
1386
|
+
)
|
|
1387
|
+
) {
|
|
1388
|
+
const error = new TypeError(
|
|
1389
|
+
"workflow nested-effect settlement is malformed or duplicated",
|
|
1390
|
+
);
|
|
1391
|
+
error.code = "CC_WORKFLOW_CHILD_EFFECT_SETTLEMENT_INVALID";
|
|
1392
|
+
error.workflowEffectOutcomeUnknown = true;
|
|
1393
|
+
throw error;
|
|
1394
|
+
}
|
|
1395
|
+
this._nestedEffectSettlements.push(Object.freeze(settlement));
|
|
1396
|
+
return settlement;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
_recordProviderRequestAttempt(event) {
|
|
1400
|
+
if (!this._workflowEffectId) return;
|
|
1401
|
+
const attempt = {
|
|
1402
|
+
protocol: "cc-provider-request-attempt/v1",
|
|
1403
|
+
provider: event?.provider,
|
|
1404
|
+
workflowEffectId: event?.workflowEffectId,
|
|
1405
|
+
callId: event?.callId,
|
|
1406
|
+
callSequence: event?.callSequence,
|
|
1407
|
+
source: event?.source,
|
|
1408
|
+
clientRequestId: event?.providerRequestId,
|
|
1409
|
+
requestIdentitySemantics: event?.requestIdentitySemantics,
|
|
1410
|
+
};
|
|
1411
|
+
if (
|
|
1412
|
+
!PROVIDER_NAME_RE.test(attempt.provider || "") ||
|
|
1413
|
+
attempt.workflowEffectId !== this._workflowEffectId ||
|
|
1414
|
+
!PROVIDER_CALL_ID_RE.test(attempt.callId || "") ||
|
|
1415
|
+
!Number.isSafeInteger(attempt.callSequence) ||
|
|
1416
|
+
attempt.callSequence < 1 ||
|
|
1417
|
+
!["model", "semantic-compaction"].includes(attempt.source) ||
|
|
1418
|
+
!PROVIDER_CLIENT_REQUEST_ID_RE.test(attempt.clientRequestId || "") ||
|
|
1419
|
+
attempt.requestIdentitySemantics !== "trace-only" ||
|
|
1420
|
+
this._providerRequestAttempts.some(
|
|
1421
|
+
(existing) => existing.callId === attempt.callId,
|
|
1422
|
+
)
|
|
1423
|
+
) {
|
|
1424
|
+
const error = new TypeError(
|
|
1425
|
+
"workflow provider request boundary is malformed or duplicated",
|
|
1426
|
+
);
|
|
1427
|
+
error.code = "CC_WORKFLOW_PROVIDER_REQUEST_BOUNDARY_INVALID";
|
|
1428
|
+
throw error;
|
|
1429
|
+
}
|
|
1430
|
+
this._providerRequestAttempts.push(Object.freeze(attempt));
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1167
1433
|
/**
|
|
1168
1434
|
* True when this sub-agent has been asked to stop — either via an external
|
|
1169
1435
|
* abort signal or via this context's own `abort()` (precise single-agent
|
|
@@ -24,7 +24,7 @@ export const TaskType = {
|
|
|
24
24
|
*/
|
|
25
25
|
const TASK_MODEL_MAP = {
|
|
26
26
|
[TaskType.CHAT]: {
|
|
27
|
-
volcengine: "
|
|
27
|
+
volcengine: "deepseek-v4-flash-260425",
|
|
28
28
|
openai: "gpt-4o-mini",
|
|
29
29
|
anthropic: "claude-sonnet-4-6",
|
|
30
30
|
deepseek: "deepseek-chat",
|
|
@@ -12,10 +12,13 @@
|
|
|
12
12
|
* `timeoutMs`, it proceeds unlocked. Callers that set `failIfUnavailable` get a
|
|
13
13
|
* strict lock: every owner has a PID + unguessable token, a live owner is never
|
|
14
14
|
* reclaimed merely because the directory is old, and corrupt ownership fails
|
|
15
|
-
* closed.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* closed. The default filesystem publishes a fully populated, uniquely staged
|
|
16
|
+
* directory in one rename so a process killed during acquisition cannot expose
|
|
17
|
+
* an ownerless lock. Confirmed-dead owners are reclaimed with an exact-token
|
|
18
|
+
* marker inside the lock directory so a delayed contender cannot delete a
|
|
19
|
+
* replacement lock. Strict callers may use bounded jittered retries and an
|
|
20
|
+
* after-release yield to avoid Windows sharing transients and fixed-interval
|
|
21
|
+
* lock convoys.
|
|
19
22
|
*
|
|
20
23
|
* @param {string} targetPath the file being guarded (lock is `${targetPath}.lock`)
|
|
21
24
|
* @param {(ctx:{locked:boolean})=>T} fn critical section; receives whether the lock was held
|
|
@@ -37,6 +40,10 @@ export function withFileLock(targetPath, fn, opts = {}) {
|
|
|
37
40
|
_ownerToken = () => randomUUID(),
|
|
38
41
|
failIfUnavailable = false,
|
|
39
42
|
} = opts;
|
|
43
|
+
const isOwnerAlive =
|
|
44
|
+
typeof opts._isOwnerAlive === "function"
|
|
45
|
+
? opts._isOwnerAlive
|
|
46
|
+
: (candidate) => _isProcessAlive(candidate.pid);
|
|
40
47
|
|
|
41
48
|
const lockDir = `${targetPath}.lock`;
|
|
42
49
|
const ownerPath = path.join(lockDir, "owner.json");
|
|
@@ -52,36 +59,9 @@ export function withFileLock(targetPath, fn, opts = {}) {
|
|
|
52
59
|
|
|
53
60
|
for (;;) {
|
|
54
61
|
try {
|
|
55
|
-
_fs
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
encoding: "utf8",
|
|
59
|
-
mode: 0o600,
|
|
60
|
-
flag: "wx",
|
|
61
|
-
});
|
|
62
|
-
held = true;
|
|
63
|
-
break;
|
|
64
|
-
} catch (error) {
|
|
65
|
-
_fs.rmSync(lockDir, { recursive: true, force: true });
|
|
66
|
-
acquisitionError = error;
|
|
67
|
-
if (
|
|
68
|
-
failIfUnavailable &&
|
|
69
|
-
isTransientLockError(error) &&
|
|
70
|
-
waitForRetry({
|
|
71
|
-
_now,
|
|
72
|
-
_sleep,
|
|
73
|
-
_random,
|
|
74
|
-
deadline,
|
|
75
|
-
retryAttempt: retryAttempt++,
|
|
76
|
-
retryMs,
|
|
77
|
-
maxRetryMs,
|
|
78
|
-
retryJitterMs,
|
|
79
|
-
})
|
|
80
|
-
) {
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
break;
|
|
84
|
-
}
|
|
62
|
+
acquireOwnedDirectory(_fs, lockDir, owner);
|
|
63
|
+
held = true;
|
|
64
|
+
break;
|
|
85
65
|
} catch (error) {
|
|
86
66
|
if (!error || error.code !== "EEXIST") {
|
|
87
67
|
acquisitionError = error;
|
|
@@ -108,8 +88,23 @@ export function withFileLock(targetPath, fn, opts = {}) {
|
|
|
108
88
|
const incumbent = ownerRead.owner;
|
|
109
89
|
acquisitionError = ownerRead.error;
|
|
110
90
|
if (incumbent) {
|
|
111
|
-
|
|
112
|
-
|
|
91
|
+
// The owner writes this exact-token marker only after its critical
|
|
92
|
+
// section has finished. A Windows sharing violation can then prevent
|
|
93
|
+
// the final recursive removal while the owner process remains alive,
|
|
94
|
+
// which would otherwise strand every later strict caller behind a
|
|
95
|
+
// lock that no longer protects any work. Complete only the release
|
|
96
|
+
// explicitly published by the still-current owner; a missing,
|
|
97
|
+
// corrupt or mismatched marker remains fail-closed.
|
|
98
|
+
const publishedRelease = completePublishedRelease(
|
|
99
|
+
_fs,
|
|
100
|
+
lockDir,
|
|
101
|
+
incumbent,
|
|
102
|
+
owner,
|
|
103
|
+
isOwnerAlive,
|
|
104
|
+
);
|
|
105
|
+
if (publishedRelease.completed) continue;
|
|
106
|
+
if (!publishedRelease.published && !isOwnerAlive(incumbent)) {
|
|
107
|
+
if (reclaimOwnedDirectory(_fs, lockDir, incumbent, isOwnerAlive)) {
|
|
113
108
|
continue;
|
|
114
109
|
}
|
|
115
110
|
}
|
|
@@ -166,7 +161,7 @@ export function withFileLock(targetPath, fn, opts = {}) {
|
|
|
166
161
|
let releaseError = null;
|
|
167
162
|
if (held) {
|
|
168
163
|
try {
|
|
169
|
-
released = releaseOwnedDirectory(_fs, lockDir, owner);
|
|
164
|
+
released = releaseOwnedDirectory(_fs, lockDir, owner, isOwnerAlive);
|
|
170
165
|
} catch (error) {
|
|
171
166
|
releaseError = error;
|
|
172
167
|
}
|
|
@@ -180,8 +175,9 @@ export function withFileLock(targetPath, fn, opts = {}) {
|
|
|
180
175
|
throw error;
|
|
181
176
|
}
|
|
182
177
|
if (held && released && yieldAfterReleaseMs > 0) {
|
|
183
|
-
// The lock is
|
|
184
|
-
//
|
|
178
|
+
// The lock is gone or its exact release marker has transferred cleanup to
|
|
179
|
+
// contenders. Let an existing waiter run before a process performing many
|
|
180
|
+
// tiny transactions can immediately reacquire it.
|
|
185
181
|
_sleep(yieldAfterReleaseMs);
|
|
186
182
|
}
|
|
187
183
|
return result;
|
|
@@ -200,6 +196,61 @@ const defaultFs = {
|
|
|
200
196
|
rmSync: (p, o) => fs.rmSync(p, o),
|
|
201
197
|
};
|
|
202
198
|
|
|
199
|
+
function acquireOwnedDirectory(_fs, lockDir, owner) {
|
|
200
|
+
const serializedOwner = JSON.stringify(owner);
|
|
201
|
+
// Injected legacy filesystems used by a few consumers may not implement
|
|
202
|
+
// rename. Keep their old acquire path, while the real Node filesystem always
|
|
203
|
+
// uses the crash-safe staged publication below.
|
|
204
|
+
if (typeof _fs.renameSync !== "function") {
|
|
205
|
+
_fs.mkdirSync(lockDir);
|
|
206
|
+
try {
|
|
207
|
+
_fs.writeFileSync(path.join(lockDir, "owner.json"), serializedOwner, {
|
|
208
|
+
encoding: "utf8",
|
|
209
|
+
mode: 0o600,
|
|
210
|
+
flag: "wx",
|
|
211
|
+
});
|
|
212
|
+
return;
|
|
213
|
+
} catch (error) {
|
|
214
|
+
_fs.rmSync(lockDir, { recursive: true, force: true });
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const candidateDir = `${lockDir}.acquire-${owner.token}`;
|
|
220
|
+
try {
|
|
221
|
+
_fs.mkdirSync(candidateDir);
|
|
222
|
+
_fs.writeFileSync(path.join(candidateDir, "owner.json"), serializedOwner, {
|
|
223
|
+
encoding: "utf8",
|
|
224
|
+
mode: 0o600,
|
|
225
|
+
flag: "wx",
|
|
226
|
+
});
|
|
227
|
+
_fs.renameSync(candidateDir, lockDir);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
try {
|
|
230
|
+
_fs.rmSync(candidateDir, { recursive: true, force: true });
|
|
231
|
+
} catch {
|
|
232
|
+
/* uniquely-tokened acquisition debris cannot block the lock path */
|
|
233
|
+
}
|
|
234
|
+
if (directoryExists(_fs, lockDir)) {
|
|
235
|
+
const contention = new Error("State lock already exists", {
|
|
236
|
+
cause: error,
|
|
237
|
+
});
|
|
238
|
+
contention.code = "EEXIST";
|
|
239
|
+
throw contention;
|
|
240
|
+
}
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function directoryExists(_fs, target) {
|
|
246
|
+
try {
|
|
247
|
+
_fs.statSync(target);
|
|
248
|
+
return true;
|
|
249
|
+
} catch {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
203
254
|
function isProcessAlive(pid) {
|
|
204
255
|
try {
|
|
205
256
|
process.kill(pid, 0);
|
|
@@ -310,7 +361,7 @@ function writeOwnerMarker(_fs, markerPath, owner) {
|
|
|
310
361
|
});
|
|
311
362
|
}
|
|
312
363
|
|
|
313
|
-
function reclaimOwnedDirectory(_fs, lockDir, owner,
|
|
364
|
+
function reclaimOwnedDirectory(_fs, lockDir, owner, ownerAlive) {
|
|
314
365
|
const markerPath = path.join(lockDir, `.reclaim-${owner.token}`);
|
|
315
366
|
try {
|
|
316
367
|
writeOwnerMarker(_fs, markerPath, owner);
|
|
@@ -319,7 +370,7 @@ function reclaimOwnedDirectory(_fs, lockDir, owner, processAlive) {
|
|
|
319
370
|
throw error;
|
|
320
371
|
}
|
|
321
372
|
const current = readOwner(_fs, path.join(lockDir, "owner.json"));
|
|
322
|
-
if (!sameOwner(current, owner) ||
|
|
373
|
+
if (!sameOwner(current, owner) || ownerAlive(owner)) {
|
|
323
374
|
removeOwnMarker(_fs, markerPath, owner);
|
|
324
375
|
return false;
|
|
325
376
|
}
|
|
@@ -348,7 +399,69 @@ function reclaimLegacyDirectory(_fs, lockDir, stat) {
|
|
|
348
399
|
return true;
|
|
349
400
|
}
|
|
350
401
|
|
|
351
|
-
function
|
|
402
|
+
function completePublishedRelease(_fs, lockDir, owner, claimant, ownerAlive) {
|
|
403
|
+
const markerPath = path.join(lockDir, `.release-${owner.token}`);
|
|
404
|
+
if (!sameOwner(readOwner(_fs, markerPath), owner)) {
|
|
405
|
+
return { published: false, completed: false };
|
|
406
|
+
}
|
|
407
|
+
const claimPath = path.join(lockDir, `.release-claim-${owner.token}`);
|
|
408
|
+
for (;;) {
|
|
409
|
+
const existingClaim = readOwner(_fs, claimPath);
|
|
410
|
+
if (existingClaim) {
|
|
411
|
+
if (sameOwner(existingClaim, claimant)) break;
|
|
412
|
+
if (!ownerAlive(existingClaim)) {
|
|
413
|
+
if (removeOwnMarker(_fs, claimPath, existingClaim)) continue;
|
|
414
|
+
}
|
|
415
|
+
return { published: true, completed: false };
|
|
416
|
+
}
|
|
417
|
+
try {
|
|
418
|
+
writeOwnerMarker(_fs, claimPath, claimant);
|
|
419
|
+
break;
|
|
420
|
+
} catch (error) {
|
|
421
|
+
if (error?.code === "ENOENT") {
|
|
422
|
+
return { published: true, completed: !directoryExists(_fs, lockDir) };
|
|
423
|
+
}
|
|
424
|
+
if (error?.code === "EEXIST" || isTransientLockError(error)) {
|
|
425
|
+
return { published: true, completed: false };
|
|
426
|
+
}
|
|
427
|
+
throw error;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (!sameOwner(readOwner(_fs, path.join(lockDir, "owner.json")), owner)) {
|
|
431
|
+
removeOwnMarker(_fs, claimPath, claimant);
|
|
432
|
+
return { published: true, completed: false };
|
|
433
|
+
}
|
|
434
|
+
const cleanupDir = `${lockDir}.released-${owner.token}-${claimant.token}`;
|
|
435
|
+
try {
|
|
436
|
+
// Detach the exact released directory from the acquisition path before
|
|
437
|
+
// cleaning it. Recursive removal may span several Windows filesystem
|
|
438
|
+
// operations; deleting `lockDir` in place lets a new atomic acquirer
|
|
439
|
+
// install a replacement between those operations and risks deleting that
|
|
440
|
+
// replacement. The claimant token makes delayed cleanup private to this
|
|
441
|
+
// handoff, just like the owner's normal rename-first release path.
|
|
442
|
+
_fs.renameSync(lockDir, cleanupDir);
|
|
443
|
+
try {
|
|
444
|
+
_fs.rmSync(cleanupDir, { recursive: true, force: true });
|
|
445
|
+
} catch {
|
|
446
|
+
// The acquisition path is already free. A uniquely named released
|
|
447
|
+
// directory is harmless bounded debris if Windows still holds a handle.
|
|
448
|
+
}
|
|
449
|
+
return { published: true, completed: true };
|
|
450
|
+
} catch (error) {
|
|
451
|
+
removeOwnMarker(_fs, claimPath, claimant);
|
|
452
|
+
if (error?.code === "ENOENT") {
|
|
453
|
+
return { published: true, completed: !directoryExists(_fs, lockDir) };
|
|
454
|
+
}
|
|
455
|
+
// A Windows sharing transient leaves the exact release marker in place so
|
|
456
|
+
// this or another contender can retry within its normal deadline.
|
|
457
|
+
if (isTransientLockError(error)) {
|
|
458
|
+
return { published: true, completed: false };
|
|
459
|
+
}
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function releaseOwnedDirectory(_fs, lockDir, owner, ownerAlive) {
|
|
352
465
|
if (!sameOwner(readOwner(_fs, path.join(lockDir, "owner.json")), owner)) {
|
|
353
466
|
return false;
|
|
354
467
|
}
|
|
@@ -384,6 +497,18 @@ function releaseOwnedDirectory(_fs, lockDir, owner) {
|
|
|
384
497
|
removeOwnMarker(_fs, markerPath, owner);
|
|
385
498
|
return false;
|
|
386
499
|
}
|
|
500
|
+
if (typeof _fs.renameSync === "function") {
|
|
501
|
+
// After publishing the marker, use the same exclusive claim protocol as a
|
|
502
|
+
// later contender. The releasing process must not perform an unclaimed,
|
|
503
|
+
// delayed delete: another process could complete the release and install a
|
|
504
|
+
// replacement lock before that delete resumes.
|
|
505
|
+
// Publishing the exact release marker is the logical handoff point. A
|
|
506
|
+
// contender may win the cleanup claim before this owner does; that is a
|
|
507
|
+
// successful release, not lost ownership, because every acquirer must
|
|
508
|
+
// complete or wait behind the published marker before entering its body.
|
|
509
|
+
return completePublishedRelease(_fs, lockDir, owner, owner, ownerAlive)
|
|
510
|
+
.published;
|
|
511
|
+
}
|
|
387
512
|
_fs.rmSync(lockDir, { recursive: true, force: true });
|
|
388
513
|
return true;
|
|
389
514
|
}
|