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
|
@@ -55,6 +55,8 @@ import {
|
|
|
55
55
|
BACKGROUND_AGENT_KEEPER_TASKKILL_TIMEOUT_MS,
|
|
56
56
|
BACKGROUND_AGENT_KEEPER_WMIC_TIMEOUT_MS,
|
|
57
57
|
backgroundAgentKeeperPipePath,
|
|
58
|
+
cleanupBackgroundAgentKeeperPipeDirectory,
|
|
59
|
+
prepareBackgroundAgentKeeperPipePath,
|
|
58
60
|
} from "./background-agent-keeper-protocol.js";
|
|
59
61
|
|
|
60
62
|
export const DEFAULT_HEARTBEAT_INTERVAL_MS = 5000;
|
|
@@ -74,8 +76,88 @@ export const DEFAULT_HEARTBEAT_STALE_MS = 120000;
|
|
|
74
76
|
*/
|
|
75
77
|
export const PID_IDENTITY_TOLERANCE_MS = 60000;
|
|
76
78
|
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
+
export const PROCESS_START_TIME_CACHE_TTL_MS = 10_000;
|
|
80
|
+
export const PROCESS_START_TIME_CACHE_MAX_ENTRIES = 4_096;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A bounded FIFO/TTL cache for process creation-time probes.
|
|
84
|
+
*
|
|
85
|
+
* Probe timestamps use a monotonic clock. Deleting and re-inserting an
|
|
86
|
+
* existing pid refreshes its FIFO position without increasing the number of
|
|
87
|
+
* retained keys. Expired entries are removed from the oldest edge, making
|
|
88
|
+
* cleanup amortized O(1) because insertion timestamps are monotonic.
|
|
89
|
+
*/
|
|
90
|
+
export class ProcessStartTimeCache {
|
|
91
|
+
constructor({
|
|
92
|
+
ttlMs = PROCESS_START_TIME_CACHE_TTL_MS,
|
|
93
|
+
maxEntries = PROCESS_START_TIME_CACHE_MAX_ENTRIES,
|
|
94
|
+
now = () => performance.now(),
|
|
95
|
+
} = {}) {
|
|
96
|
+
if (!Number.isFinite(Number(ttlMs)) || Number(ttlMs) <= 0) {
|
|
97
|
+
throw new TypeError("process start-time cache ttlMs must be positive");
|
|
98
|
+
}
|
|
99
|
+
if (!Number.isSafeInteger(Number(maxEntries)) || Number(maxEntries) <= 0) {
|
|
100
|
+
throw new TypeError(
|
|
101
|
+
"process start-time cache maxEntries must be a positive integer",
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
if (typeof now !== "function") {
|
|
105
|
+
throw new TypeError("process start-time cache now must be a function");
|
|
106
|
+
}
|
|
107
|
+
this.ttlMs = Number(ttlMs);
|
|
108
|
+
this.maxEntries = Number(maxEntries);
|
|
109
|
+
this._now = now;
|
|
110
|
+
this._entries = new Map();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
_at(value) {
|
|
114
|
+
const at = value === undefined ? Number(this._now()) : Number(value);
|
|
115
|
+
if (!Number.isFinite(at)) {
|
|
116
|
+
throw new TypeError("process start-time cache time must be finite");
|
|
117
|
+
}
|
|
118
|
+
return at;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
_evictExpired(at) {
|
|
122
|
+
for (const [key, entry] of this._entries) {
|
|
123
|
+
if (at - entry.at < this.ttlMs) break;
|
|
124
|
+
this._entries.delete(key);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
read(key, atValue) {
|
|
129
|
+
const at = this._at(atValue);
|
|
130
|
+
this._evictExpired(at);
|
|
131
|
+
const entry = this._entries.get(key);
|
|
132
|
+
return entry ? { hit: true, value: entry.value } : { hit: false };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
set(key, value, atValue) {
|
|
136
|
+
const at = this._at(atValue);
|
|
137
|
+
this._evictExpired(at);
|
|
138
|
+
// Map preserves insertion order. Refresh the same pid at the newest edge
|
|
139
|
+
// so capacity eviction cannot discard a just-probed identity.
|
|
140
|
+
this._entries.delete(key);
|
|
141
|
+
while (this._entries.size >= this.maxEntries) {
|
|
142
|
+
const oldest = this._entries.keys().next().value;
|
|
143
|
+
this._entries.delete(oldest);
|
|
144
|
+
}
|
|
145
|
+
this._entries.set(key, { at, value });
|
|
146
|
+
return value;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
get size() {
|
|
150
|
+
return this._entries.size;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
clear() {
|
|
154
|
+
this._entries.clear();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const _startTimeCache = new ProcessStartTimeCache();
|
|
159
|
+
const PROCESS_START_TIME_ABSENT = Symbol("process-start-time-absent");
|
|
160
|
+
const WINDOWS_PROCESS_ABSENT_MARKER = "__CC_PROCESS_ABSENT__";
|
|
79
161
|
|
|
80
162
|
function runSupervisorCommand(file, args, options, origin) {
|
|
81
163
|
return _deps.spawnSync(file, args, {
|
|
@@ -108,7 +190,8 @@ export function parseCimDateToMs(raw) {
|
|
|
108
190
|
}
|
|
109
191
|
|
|
110
192
|
/**
|
|
111
|
-
* Read a process's creation time (epoch ms) with ONE synchronous probe,
|
|
193
|
+
* Read a process's creation time (epoch ms) with ONE synchronous probe,
|
|
194
|
+
* `{status:"absent"}` when the OS authoritatively reports no such process, or
|
|
112
195
|
* null when it cannot be determined (missing tools, permissions, platform
|
|
113
196
|
* quirks). Callers must treat null as "unknown" and FAIL OPEN to the legacy
|
|
114
197
|
* kill(pid, 0) answer — a broken probe must never declare a live worker dead
|
|
@@ -140,9 +223,13 @@ function defaultReadProcessStartTimeMs(pid) {
|
|
|
140
223
|
"background-agent:process-start-time",
|
|
141
224
|
);
|
|
142
225
|
if (!r.error && r.status === 0) {
|
|
143
|
-
const
|
|
226
|
+
const stdout = String(r.stdout || "");
|
|
227
|
+
const m = /CreationDate=([^\r\n]+)/.exec(stdout);
|
|
144
228
|
const ms = m ? parseCimDateToMs(m[1]) : null;
|
|
145
229
|
if (ms !== null) return ms;
|
|
230
|
+
if (/No Instance\(s\) Available/u.test(stdout)) {
|
|
231
|
+
return { status: "absent" };
|
|
232
|
+
}
|
|
146
233
|
}
|
|
147
234
|
} catch {
|
|
148
235
|
/* fall through to PowerShell */
|
|
@@ -150,7 +237,9 @@ function defaultReadProcessStartTimeMs(pid) {
|
|
|
150
237
|
try {
|
|
151
238
|
const script =
|
|
152
239
|
`$p = Get-CimInstance Win32_Process -Filter 'ProcessId=${target}' -ErrorAction SilentlyContinue; ` +
|
|
153
|
-
|
|
240
|
+
`if (-not $p) { '${WINDOWS_PROCESS_ABSENT_MARKER}' } ` +
|
|
241
|
+
"elseif ($p.CreationDate) { [DateTimeOffset]::new($p.CreationDate).ToUnixTimeMilliseconds() } " +
|
|
242
|
+
"else { exit 2 }";
|
|
154
243
|
const r = runSupervisorCommand(
|
|
155
244
|
"powershell",
|
|
156
245
|
["-NoProfile", "-NonInteractive", "-Command", script],
|
|
@@ -162,7 +251,11 @@ function defaultReadProcessStartTimeMs(pid) {
|
|
|
162
251
|
"background-agent:process-start-time",
|
|
163
252
|
);
|
|
164
253
|
if (!r.error && r.status === 0) {
|
|
165
|
-
const
|
|
254
|
+
const output = String(r.stdout || "").trim();
|
|
255
|
+
if (output === WINDOWS_PROCESS_ABSENT_MARKER) {
|
|
256
|
+
return { status: "absent" };
|
|
257
|
+
}
|
|
258
|
+
const n = Number(output);
|
|
166
259
|
if (Number.isFinite(n) && n > 0) return n;
|
|
167
260
|
}
|
|
168
261
|
} catch {
|
|
@@ -322,6 +415,8 @@ export const _deps = {
|
|
|
322
415
|
readProcessState: defaultReadProcessState,
|
|
323
416
|
readProcessGroupStates: defaultReadProcessGroupStates,
|
|
324
417
|
processExitWaitDeadlineMs: 2_000,
|
|
418
|
+
posixPermissionRetryNowMs: () => performance.now(),
|
|
419
|
+
posixPermissionRetrySleep: sleepSyncMs,
|
|
325
420
|
killProcessTree: defaultKillProcessTree,
|
|
326
421
|
terminateOwnedProcessTree,
|
|
327
422
|
withSessionHostRecoveryLease,
|
|
@@ -334,7 +429,14 @@ export const _deps = {
|
|
|
334
429
|
*/
|
|
335
430
|
export function stopBackgroundAgentChildTree(
|
|
336
431
|
pid,
|
|
337
|
-
{
|
|
432
|
+
{
|
|
433
|
+
platform = process.platform,
|
|
434
|
+
signal = "SIGTERM",
|
|
435
|
+
expectedStartedAt,
|
|
436
|
+
processGroup = true,
|
|
437
|
+
strictIdentity = false,
|
|
438
|
+
allowDirectFallback = true,
|
|
439
|
+
} = {},
|
|
338
440
|
) {
|
|
339
441
|
const target = Number(pid);
|
|
340
442
|
if (!Number.isInteger(target) || target <= 0 || target === process.pid) {
|
|
@@ -342,6 +444,15 @@ export function stopBackgroundAgentChildTree(
|
|
|
342
444
|
error.code = "ERR_BACKGROUND_AGENT_CHILD_PID_INVALID";
|
|
343
445
|
throw error;
|
|
344
446
|
}
|
|
447
|
+
if (strictIdentity === true) {
|
|
448
|
+
signalExactBackgroundProcessTree(target, signal, {
|
|
449
|
+
platform,
|
|
450
|
+
expectedStartedAt,
|
|
451
|
+
processGroup,
|
|
452
|
+
allowDirectFallback,
|
|
453
|
+
});
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
345
456
|
if (platform === "win32") {
|
|
346
457
|
const result = runSupervisorCommand(
|
|
347
458
|
"taskkill",
|
|
@@ -366,32 +477,38 @@ export function stopBackgroundAgentChildTree(
|
|
|
366
477
|
}
|
|
367
478
|
return true;
|
|
368
479
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
480
|
+
// Preserve the legacy caller contract while sharing the safe POSIX tree
|
|
481
|
+
// primitive: permission/invalid-signal failures remain fatal, and only a
|
|
482
|
+
// genuinely absent process group may use the historical direct fallback.
|
|
483
|
+
signalBackgroundProcessTree(target, signal, {
|
|
484
|
+
platform,
|
|
485
|
+
processGroup,
|
|
486
|
+
allowDirectFallback,
|
|
487
|
+
});
|
|
374
488
|
return true;
|
|
375
489
|
}
|
|
376
490
|
|
|
377
491
|
/** Cached probe (default impl only — injected probes run uncached for tests). */
|
|
378
492
|
function processStartTimeMs(pid, options = {}) {
|
|
493
|
+
const normalize = (raw) => {
|
|
494
|
+
if (raw?.status === "absent") return PROCESS_START_TIME_ABSENT;
|
|
495
|
+
const value = Number(raw);
|
|
496
|
+
return Number.isFinite(value) && value > 0 ? value : null;
|
|
497
|
+
};
|
|
379
498
|
if (_deps.readProcessStartTimeMs !== defaultReadProcessStartTimeMs) {
|
|
380
|
-
|
|
381
|
-
const n = Number(raw);
|
|
382
|
-
return Number.isFinite(n) && n > 0 ? n : null;
|
|
499
|
+
return normalize(_deps.readProcessStartTimeMs(pid));
|
|
383
500
|
}
|
|
384
501
|
const key = Number(pid);
|
|
385
|
-
const at =
|
|
502
|
+
const at = performance.now();
|
|
386
503
|
if (options.fresh === true) {
|
|
387
|
-
const value = defaultReadProcessStartTimeMs(key);
|
|
388
|
-
_startTimeCache.set(key,
|
|
504
|
+
const value = normalize(defaultReadProcessStartTimeMs(key));
|
|
505
|
+
_startTimeCache.set(key, value, at);
|
|
389
506
|
return value;
|
|
390
507
|
}
|
|
391
|
-
const hit = _startTimeCache.
|
|
392
|
-
if (hit
|
|
393
|
-
const value = defaultReadProcessStartTimeMs(key);
|
|
394
|
-
_startTimeCache.set(key,
|
|
508
|
+
const hit = _startTimeCache.read(key, at);
|
|
509
|
+
if (hit.hit) return hit.value;
|
|
510
|
+
const value = normalize(defaultReadProcessStartTimeMs(key));
|
|
511
|
+
_startTimeCache.set(key, value, at);
|
|
395
512
|
return value;
|
|
396
513
|
}
|
|
397
514
|
|
|
@@ -441,11 +558,70 @@ const TERMINAL_STATUSES = new Set(["completed", "failed", "stopped", "lost"]);
|
|
|
441
558
|
const KEEPER_TERMINAL_STATUSES = new Set([
|
|
442
559
|
"closed",
|
|
443
560
|
"worker-disconnected",
|
|
561
|
+
"cleanup-unconfirmed",
|
|
444
562
|
"startup-timeout",
|
|
445
563
|
"spawn-error",
|
|
446
564
|
"exited",
|
|
447
565
|
]);
|
|
448
566
|
|
|
567
|
+
export function hasValidBackgroundAgentStopCleanupProof(state) {
|
|
568
|
+
const requestedAt = Number(state?.stopRequestedAt);
|
|
569
|
+
const confirmedAt = Number(state?.stopCleanupConfirmedAt);
|
|
570
|
+
return Boolean(
|
|
571
|
+
Number.isFinite(requestedAt) &&
|
|
572
|
+
requestedAt > 0 &&
|
|
573
|
+
Number.isFinite(confirmedAt) &&
|
|
574
|
+
confirmedAt >= requestedAt,
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function hasBackgroundAgentStopFenceWithoutCleanupProof(state) {
|
|
579
|
+
return Boolean(
|
|
580
|
+
Number.isFinite(Number(state?.stopRequestedAt)) &&
|
|
581
|
+
Number(state.stopRequestedAt) > 0 &&
|
|
582
|
+
!hasValidBackgroundAgentStopCleanupProof(state),
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function isBackgroundAgentStopCleanupUnconfirmed(state) {
|
|
587
|
+
return Boolean(
|
|
588
|
+
state?.status === "stopped" &&
|
|
589
|
+
hasBackgroundAgentStopFenceWithoutCleanupProof(state),
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function selectBackgroundAgentStopCleanupProof(current, requested) {
|
|
594
|
+
if (hasValidBackgroundAgentStopCleanupProof(current)) {
|
|
595
|
+
return current.stopCleanupConfirmedAt;
|
|
596
|
+
}
|
|
597
|
+
const requestedAt = Number(
|
|
598
|
+
Number.isFinite(Number(current?.stopRequestedAt)) &&
|
|
599
|
+
Number(current.stopRequestedAt) > 0
|
|
600
|
+
? current.stopRequestedAt
|
|
601
|
+
: requested?.stopRequestedAt,
|
|
602
|
+
);
|
|
603
|
+
const requestedProof = Number(requested?.stopCleanupConfirmedAt);
|
|
604
|
+
if (
|
|
605
|
+
Number.isFinite(requestedAt) &&
|
|
606
|
+
requestedAt > 0 &&
|
|
607
|
+
Number.isFinite(requestedProof) &&
|
|
608
|
+
requestedProof >= requestedAt
|
|
609
|
+
) {
|
|
610
|
+
return requestedProof;
|
|
611
|
+
}
|
|
612
|
+
return current?.stopCleanupConfirmedAt;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function backgroundAgentStopCleanupConfirmationTimestamp(
|
|
616
|
+
state,
|
|
617
|
+
now = Date.now(),
|
|
618
|
+
) {
|
|
619
|
+
const requestedAt = Number(state?.stopRequestedAt);
|
|
620
|
+
return Number.isFinite(requestedAt) && requestedAt > 0
|
|
621
|
+
? Math.max(now, requestedAt)
|
|
622
|
+
: now;
|
|
623
|
+
}
|
|
624
|
+
|
|
449
625
|
const TURN_PROCESS_PROJECTION_FIELDS = [
|
|
450
626
|
"agentPid",
|
|
451
627
|
"agentStartedAt",
|
|
@@ -606,10 +782,17 @@ function mergeBackgroundAgentState(current, requested) {
|
|
|
606
782
|
current.stopRequestedAt &&
|
|
607
783
|
requested.stopRequestedAt === current.stopRequestedAt;
|
|
608
784
|
if (TERMINAL_STATUSES.has(current.status) && !explicitHeartbeatStop) {
|
|
785
|
+
const stopCleanupConfirmedAt = selectBackgroundAgentStopCleanupProof(
|
|
786
|
+
current,
|
|
787
|
+
requested,
|
|
788
|
+
);
|
|
609
789
|
next = {
|
|
610
790
|
...next,
|
|
611
791
|
status: current.status,
|
|
612
792
|
endedAt: current.endedAt ?? next.endedAt ?? null,
|
|
793
|
+
...(stopCleanupConfirmedAt !== undefined
|
|
794
|
+
? { stopCleanupConfirmedAt }
|
|
795
|
+
: {}),
|
|
613
796
|
exitCode: current.exitCode ?? next.exitCode ?? null,
|
|
614
797
|
...(current.signal !== undefined ? { signal: current.signal } : {}),
|
|
615
798
|
...(current.error !== undefined ? { error: current.error } : {}),
|
|
@@ -920,6 +1103,26 @@ function mergeBackgroundAgentState(current, requested) {
|
|
|
920
1103
|
transport: null,
|
|
921
1104
|
};
|
|
922
1105
|
}
|
|
1106
|
+
if (
|
|
1107
|
+
current.status === "running" &&
|
|
1108
|
+
next.status === "stopped" &&
|
|
1109
|
+
!hasValidBackgroundAgentStopCleanupProof(next)
|
|
1110
|
+
) {
|
|
1111
|
+
// A worker or read-side reconciliation can publish a terminal outcome
|
|
1112
|
+
// after an explicit stop fence wins but before the stopper has observed
|
|
1113
|
+
// every exact-owned process retire. Keep that record durably fenced
|
|
1114
|
+
// and retryable; only stopBackgroundAgent may publish `stopped`
|
|
1115
|
+
// together with cleanup proof.
|
|
1116
|
+
next = {
|
|
1117
|
+
...next,
|
|
1118
|
+
status: "running",
|
|
1119
|
+
stoppedByUser: false,
|
|
1120
|
+
phase: "stop_waiting_for_exit",
|
|
1121
|
+
transport: null,
|
|
1122
|
+
stopPending: true,
|
|
1123
|
+
stopPendingReason: "process-exit",
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
923
1126
|
}
|
|
924
1127
|
}
|
|
925
1128
|
return next;
|
|
@@ -952,6 +1155,20 @@ function persistBackgroundAgentState(target, next) {
|
|
|
952
1155
|
return next;
|
|
953
1156
|
}
|
|
954
1157
|
|
|
1158
|
+
function isBackgroundAgentStateLockOwnerAlive(owner) {
|
|
1159
|
+
const target = Number(owner?.pid);
|
|
1160
|
+
if (!isProcessExecutionAlive(target)) return false;
|
|
1161
|
+
const acquiredAt = Number(owner?.startedAt);
|
|
1162
|
+
if (!Number.isFinite(acquiredAt) || acquiredAt <= 0) return true;
|
|
1163
|
+
const actualStartedAt = processStartTimeMs(target, { fresh: true });
|
|
1164
|
+
if (actualStartedAt === PROCESS_START_TIME_ABSENT) return false;
|
|
1165
|
+
if (!Number.isFinite(actualStartedAt)) return true;
|
|
1166
|
+
// owner.startedAt is written only after this process acquired the lock. A
|
|
1167
|
+
// live process created later therefore proves PID reuse; retaining that
|
|
1168
|
+
// stale lock would permanently fence the state after high-churn workers.
|
|
1169
|
+
return actualStartedAt <= acquiredAt;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
955
1172
|
/**
|
|
956
1173
|
* Strict cross-process read/mutate/write transaction for one supervisor state.
|
|
957
1174
|
* Returning null from `updater` leaves the record untouched. Missing and
|
|
@@ -981,7 +1198,25 @@ export function mutateBackgroundAgentState(id, updater, options = {}) {
|
|
|
981
1198
|
},
|
|
982
1199
|
{
|
|
983
1200
|
failIfUnavailable: true,
|
|
984
|
-
|
|
1201
|
+
// Startup has three independent writers (launcher, worker and keeper),
|
|
1202
|
+
// while stop/turn settlement adds another contender. Hosted Windows can
|
|
1203
|
+
// otherwise starve a first heartbeat for the old five-second budget when
|
|
1204
|
+
// the full strict matrix is saturating the runner. Keep the lock strict
|
|
1205
|
+
// and bounded, but give default callers one full ten-second contention
|
|
1206
|
+
// window; keeper cleanup keeps its explicit protocol-bound five seconds.
|
|
1207
|
+
timeoutMs: options.timeoutMs ?? 10_000,
|
|
1208
|
+
// Release before yielding so an already-waiting process gets a scheduling
|
|
1209
|
+
// opportunity instead of letting a heartbeat loop immediately reacquire.
|
|
1210
|
+
yieldAfterReleaseMs: 2,
|
|
1211
|
+
// A stopped POSIX worker can remain as a zombie long enough for
|
|
1212
|
+
// kill(pid, 0) to report it as alive. Such a process cannot still own
|
|
1213
|
+
// the critical section, so use the supervisor's execution-state probe
|
|
1214
|
+
// (which treats only Z/X as dead and keeps unknown states fail-closed).
|
|
1215
|
+
_isProcessAlive: isProcessExecutionAlive,
|
|
1216
|
+
// A live numeric pid can belong to a later process after the lock owner
|
|
1217
|
+
// was killed. Use the lock acquisition timestamp as a one-sided identity
|
|
1218
|
+
// fence so only an owner which existed before acquisition still blocks.
|
|
1219
|
+
_isOwnerAlive: isBackgroundAgentStateLockOwnerAlive,
|
|
985
1220
|
},
|
|
986
1221
|
);
|
|
987
1222
|
}
|
|
@@ -1208,6 +1443,7 @@ export function isSameProcess(pid, expectedStartedAtMs, options = {}) {
|
|
|
1208
1443
|
const expected = Number(expectedStartedAtMs);
|
|
1209
1444
|
if (!Number.isFinite(expected) || expected <= 0) return true;
|
|
1210
1445
|
const actual = processStartTimeMs(pid);
|
|
1446
|
+
if (actual === PROCESS_START_TIME_ABSENT) return false;
|
|
1211
1447
|
if (actual === null) return true;
|
|
1212
1448
|
const tolerance = Number.isFinite(Number(options.toleranceMs))
|
|
1213
1449
|
? Number(options.toleranceMs)
|
|
@@ -1242,7 +1478,7 @@ function inspectDestructiveProcessIdentity(
|
|
|
1242
1478
|
const leaderCanExecute =
|
|
1243
1479
|
leaderAlive && leaderState !== "Z" && leaderState !== "X";
|
|
1244
1480
|
if (!leaderCanExecute) {
|
|
1245
|
-
if (process.platform === "win32") {
|
|
1481
|
+
if (process.platform === "win32" || options.processGroup === false) {
|
|
1246
1482
|
return { status: "dead", reason: "not-executing", pid: target };
|
|
1247
1483
|
}
|
|
1248
1484
|
const group = inspectProcessGroupExecution(target);
|
|
@@ -1280,6 +1516,13 @@ function inspectDestructiveProcessIdentity(
|
|
|
1280
1516
|
};
|
|
1281
1517
|
}
|
|
1282
1518
|
const actual = processStartTimeMs(target, { fresh: true });
|
|
1519
|
+
if (actual === PROCESS_START_TIME_ABSENT) {
|
|
1520
|
+
return {
|
|
1521
|
+
status: "dead",
|
|
1522
|
+
reason: "process-not-observed",
|
|
1523
|
+
pid: target,
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1283
1526
|
if (actual === null) {
|
|
1284
1527
|
return {
|
|
1285
1528
|
status: "unverifiable",
|
|
@@ -1292,7 +1535,7 @@ function inspectDestructiveProcessIdentity(
|
|
|
1292
1535
|
? Number(options.toleranceMs)
|
|
1293
1536
|
: PID_IDENTITY_TOLERANCE_MS;
|
|
1294
1537
|
if (Math.abs(actual - expected) > tolerance) {
|
|
1295
|
-
if (process.platform !== "win32") {
|
|
1538
|
+
if (process.platform !== "win32" && options.processGroup !== false) {
|
|
1296
1539
|
const group = inspectProcessGroupExecution(target);
|
|
1297
1540
|
// The leader PID can be reused while descendants from its detached
|
|
1298
1541
|
// process group still execute under the old PGID. We can prove the new
|
|
@@ -1578,6 +1821,16 @@ export function effectiveBackgroundAgentState(state, options = {}) {
|
|
|
1578
1821
|
}
|
|
1579
1822
|
return state;
|
|
1580
1823
|
}
|
|
1824
|
+
if (
|
|
1825
|
+
Number.isFinite(Number(state.stopRequestedAt)) &&
|
|
1826
|
+
Number(state.stopRequestedAt) > 0 &&
|
|
1827
|
+
!hasValidBackgroundAgentStopCleanupProof(state)
|
|
1828
|
+
) {
|
|
1829
|
+
// A durable stop fence transfers terminal reconciliation to the explicit
|
|
1830
|
+
// stopper. A concurrent list/view must not persist `lost` before that path
|
|
1831
|
+
// has verified every exact-owned process and published cleanup proof.
|
|
1832
|
+
return state;
|
|
1833
|
+
}
|
|
1581
1834
|
// OS identity probes can take seconds on Windows. Perform them outside the
|
|
1582
1835
|
// state lock, then use the exact observed identity/heartbeat as a CAS fence
|
|
1583
1836
|
// inside the short persistence transaction.
|
|
@@ -1952,6 +2205,13 @@ export function resumeBackgroundAgent(id, prompt, options = {}) {
|
|
|
1952
2205
|
heartbeatStaleMs: options.heartbeatStaleMs,
|
|
1953
2206
|
});
|
|
1954
2207
|
if (!state) throw new Error(`Background agent not found: ${id}`);
|
|
2208
|
+
if (hasBackgroundAgentStopFenceWithoutCleanupProof(state)) {
|
|
2209
|
+
const error = new Error(
|
|
2210
|
+
`Background agent ${id} stop cleanup is still pending; retry stop and wait for durable cleanup confirmation before resuming`,
|
|
2211
|
+
);
|
|
2212
|
+
error.code = "BACKGROUND_AGENT_STOP_CLEANUP_PENDING";
|
|
2213
|
+
throw error;
|
|
2214
|
+
}
|
|
1955
2215
|
if (state.status === "running") {
|
|
1956
2216
|
throw new Error(
|
|
1957
2217
|
`Background agent ${id} is still running — use cc attach ${id} to send follow-up prompts instead`,
|
|
@@ -2270,6 +2530,12 @@ export function launchBackgroundAgent({
|
|
|
2270
2530
|
omitted: [...launchProfileState.omitted, "apiKey"],
|
|
2271
2531
|
});
|
|
2272
2532
|
}
|
|
2533
|
+
const launchCwd = launchProfileState.workspace.cwd;
|
|
2534
|
+
if (canonicalPath(cwd) !== canonicalPath(launchCwd)) {
|
|
2535
|
+
throw new Error(
|
|
2536
|
+
"Cannot launch background agent: launch profile cwd does not match the requested cwd",
|
|
2537
|
+
);
|
|
2538
|
+
}
|
|
2273
2539
|
const configFingerprint =
|
|
2274
2540
|
fingerprintBackgroundLaunchProfile(launchProfileState);
|
|
2275
2541
|
const dir = backgroundAgentsDir();
|
|
@@ -2277,6 +2543,7 @@ export function launchBackgroundAgent({
|
|
|
2277
2543
|
const keeperGeneration = randomBytes(16).toString("hex");
|
|
2278
2544
|
const keeperToken = randomBytes(32).toString("hex");
|
|
2279
2545
|
const keeperPipePath = backgroundAgentKeeperPipePath(id, dir);
|
|
2546
|
+
prepareBackgroundAgentKeeperPipePath(keeperPipePath);
|
|
2280
2547
|
const jobFile = join(dir, `${id}.job.${process.pid}.json`);
|
|
2281
2548
|
const keeperJobFile = join(dir, `${id}.keeper.${process.pid}.json`);
|
|
2282
2549
|
const worker = fileURLToPath(
|
|
@@ -2289,7 +2556,7 @@ export function launchBackgroundAgent({
|
|
|
2289
2556
|
id,
|
|
2290
2557
|
workerGeneration,
|
|
2291
2558
|
argv: initialSecrets.argv,
|
|
2292
|
-
cwd,
|
|
2559
|
+
cwd: launchCwd,
|
|
2293
2560
|
sessionId,
|
|
2294
2561
|
sessionBootstrapExpected,
|
|
2295
2562
|
title: title || "Background agent",
|
|
@@ -2324,6 +2591,7 @@ export function launchBackgroundAgent({
|
|
|
2324
2591
|
} catch (error) {
|
|
2325
2592
|
rmSync(jobFile, { force: true });
|
|
2326
2593
|
rmSync(keeperJobFile, { force: true });
|
|
2594
|
+
cleanupBackgroundAgentKeeperPipeDirectory(keeperPipePath);
|
|
2327
2595
|
throw error;
|
|
2328
2596
|
}
|
|
2329
2597
|
// Write the initial state BEFORE spawning so the worker's own merges (the
|
|
@@ -2335,7 +2603,7 @@ export function launchBackgroundAgent({
|
|
|
2335
2603
|
sessionId,
|
|
2336
2604
|
sessionBootstrapExpected,
|
|
2337
2605
|
title: job.title,
|
|
2338
|
-
cwd,
|
|
2606
|
+
cwd: launchCwd,
|
|
2339
2607
|
pid: null,
|
|
2340
2608
|
workerPid: null,
|
|
2341
2609
|
workerClaimedPid: null,
|
|
@@ -2372,6 +2640,7 @@ export function launchBackgroundAgent({
|
|
|
2372
2640
|
rmSync(jobFile, { force: true });
|
|
2373
2641
|
rmSync(keeperJobFile, { force: true });
|
|
2374
2642
|
rmSync(statePath(id), { force: true });
|
|
2643
|
+
cleanupBackgroundAgentKeeperPipeDirectory(keeperPipePath);
|
|
2375
2644
|
throw error;
|
|
2376
2645
|
}
|
|
2377
2646
|
let child;
|
|
@@ -2386,7 +2655,7 @@ export function launchBackgroundAgent({
|
|
|
2386
2655
|
// hosted process' possibly absent standard handles.
|
|
2387
2656
|
workerLogFd = openSync(job.logFile, "a");
|
|
2388
2657
|
child = _deps.spawn(process.execPath, [worker, jobFile], {
|
|
2389
|
-
cwd,
|
|
2658
|
+
cwd: launchCwd,
|
|
2390
2659
|
detached: true,
|
|
2391
2660
|
stdio: ["ignore", workerLogFd, workerLogFd],
|
|
2392
2661
|
windowsHide: true,
|
|
@@ -2400,7 +2669,7 @@ export function launchBackgroundAgent({
|
|
|
2400
2669
|
});
|
|
2401
2670
|
keeperStartedAt = Date.now();
|
|
2402
2671
|
keeperChild = _deps.spawn(process.execPath, [keeper, keeperJobFile], {
|
|
2403
|
-
cwd,
|
|
2672
|
+
cwd: launchCwd,
|
|
2404
2673
|
detached: true,
|
|
2405
2674
|
stdio: ["ignore", workerLogFd, workerLogFd],
|
|
2406
2675
|
windowsHide: true,
|
|
@@ -2451,6 +2720,7 @@ export function launchBackgroundAgent({
|
|
|
2451
2720
|
rmSync(jobFile, { force: true });
|
|
2452
2721
|
rmSync(keeperJobFile, { force: true });
|
|
2453
2722
|
rmSync(statePath(id), { force: true });
|
|
2723
|
+
cleanupBackgroundAgentKeeperPipeDirectory(keeperPipePath);
|
|
2454
2724
|
throw error;
|
|
2455
2725
|
} finally {
|
|
2456
2726
|
if (Number.isInteger(workerLogFd)) {
|
|
@@ -2635,33 +2905,50 @@ export function readBackgroundAgentLog(id, options = {}) {
|
|
|
2635
2905
|
}
|
|
2636
2906
|
|
|
2637
2907
|
function recordedBackgroundProcessIdentities(state) {
|
|
2908
|
+
const hasRecordedAgentTree =
|
|
2909
|
+
Number(state?.agentPid) > 0 || Number(state?.agentRuntimePid) > 0;
|
|
2638
2910
|
const records = [
|
|
2911
|
+
{
|
|
2912
|
+
role: "agent",
|
|
2913
|
+
pid: state?.agentPid,
|
|
2914
|
+
startedAt: state?.agentStartedAt,
|
|
2915
|
+
signalAsGroup: true,
|
|
2916
|
+
},
|
|
2639
2917
|
{
|
|
2640
2918
|
role: "agent-runtime",
|
|
2641
2919
|
pid: state?.agentRuntimePid,
|
|
2642
2920
|
startedAt: state?.agentRuntimeStartedAt,
|
|
2921
|
+
signalAsGroup: false,
|
|
2922
|
+
},
|
|
2923
|
+
{
|
|
2924
|
+
role: "worker",
|
|
2925
|
+
pid: state?.pid,
|
|
2926
|
+
startedAt: state?.startedAt,
|
|
2927
|
+
signalAsGroup: !hasRecordedAgentTree,
|
|
2643
2928
|
},
|
|
2644
|
-
{ role: "agent", pid: state?.agentPid, startedAt: state?.agentStartedAt },
|
|
2645
|
-
{ role: "worker", pid: state?.pid, startedAt: state?.startedAt },
|
|
2646
2929
|
{
|
|
2647
2930
|
role: "worker-published",
|
|
2648
2931
|
pid: state?.workerPid,
|
|
2649
2932
|
startedAt: state?.startedAt,
|
|
2933
|
+
signalAsGroup: !hasRecordedAgentTree,
|
|
2650
2934
|
},
|
|
2651
2935
|
{
|
|
2652
2936
|
role: "worker-wrapper",
|
|
2653
2937
|
pid: state?.workerWrapperPid,
|
|
2654
2938
|
startedAt: state?.startedAt,
|
|
2939
|
+
signalAsGroup: true,
|
|
2655
2940
|
},
|
|
2656
2941
|
{
|
|
2657
2942
|
role: "keeper",
|
|
2658
2943
|
pid: state?.keeperPid,
|
|
2659
2944
|
startedAt: state?.keeperStartedAt,
|
|
2945
|
+
signalAsGroup: false,
|
|
2660
2946
|
},
|
|
2661
2947
|
{
|
|
2662
2948
|
role: "keeper-wrapper",
|
|
2663
2949
|
pid: state?.keeperWrapperPid,
|
|
2664
2950
|
startedAt: state?.keeperStartedAt,
|
|
2951
|
+
signalAsGroup: true,
|
|
2665
2952
|
},
|
|
2666
2953
|
];
|
|
2667
2954
|
const seen = new Set();
|
|
@@ -2677,7 +2964,9 @@ function recordedBackgroundProcessIdentities(state) {
|
|
|
2677
2964
|
function inspectRecordedBackgroundProcesses(state) {
|
|
2678
2965
|
return recordedBackgroundProcessIdentities(state).map((record) => ({
|
|
2679
2966
|
...record,
|
|
2680
|
-
identity: inspectDestructiveProcessIdentity(record.pid, record.startedAt
|
|
2967
|
+
identity: inspectDestructiveProcessIdentity(record.pid, record.startedAt, {
|
|
2968
|
+
processGroup: record.signalAsGroup,
|
|
2969
|
+
}),
|
|
2681
2970
|
}));
|
|
2682
2971
|
}
|
|
2683
2972
|
|
|
@@ -2698,7 +2987,11 @@ export function isBackgroundProcessTreeExecutionAlive(pid, startedAt) {
|
|
|
2698
2987
|
export function signalBackgroundProcessTree(
|
|
2699
2988
|
pid,
|
|
2700
2989
|
signal = "SIGTERM",
|
|
2701
|
-
{
|
|
2990
|
+
{
|
|
2991
|
+
platform = process.platform,
|
|
2992
|
+
processGroup = true,
|
|
2993
|
+
allowDirectFallback = true,
|
|
2994
|
+
} = {},
|
|
2702
2995
|
) {
|
|
2703
2996
|
const target = Number(pid);
|
|
2704
2997
|
if (platform === "win32") {
|
|
@@ -2721,6 +3014,15 @@ export function signalBackgroundProcessTree(
|
|
|
2721
3014
|
}
|
|
2722
3015
|
return "process-tree";
|
|
2723
3016
|
}
|
|
3017
|
+
if (processGroup !== true) {
|
|
3018
|
+
try {
|
|
3019
|
+
_deps.kill(target, signal);
|
|
3020
|
+
return "process";
|
|
3021
|
+
} catch (error) {
|
|
3022
|
+
if (error?.code === "ESRCH") return "already-exited";
|
|
3023
|
+
throw error;
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
2724
3026
|
try {
|
|
2725
3027
|
_deps.kill(-target, signal);
|
|
2726
3028
|
return "process-group";
|
|
@@ -2728,7 +3030,9 @@ export function signalBackgroundProcessTree(
|
|
|
2728
3030
|
// A denied/invalid group signal can leave descendants executing even if a
|
|
2729
3031
|
// direct leader signal would succeed. Only ESRCH proves that there is no
|
|
2730
3032
|
// process group to terminate and permits the single-process fallback.
|
|
2731
|
-
if (groupError?.code !== "ESRCH")
|
|
3033
|
+
if (groupError?.code !== "ESRCH" || allowDirectFallback !== true) {
|
|
3034
|
+
throw groupError;
|
|
3035
|
+
}
|
|
2732
3036
|
try {
|
|
2733
3037
|
_deps.kill(target, signal);
|
|
2734
3038
|
return "process";
|
|
@@ -2743,6 +3047,346 @@ export function signalBackgroundProcessTree(
|
|
|
2743
3047
|
}
|
|
2744
3048
|
}
|
|
2745
3049
|
|
|
3050
|
+
function isDestructiveIdentityRetired(identity) {
|
|
3051
|
+
return ["dead", "reused"].includes(identity?.status);
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
function isRecoverableDestructiveIdentityProbe(identity) {
|
|
3055
|
+
// These outcomes come only from read-only OS observations. Retrying them
|
|
3056
|
+
// cannot address a process, and the caller still requires two consecutive
|
|
3057
|
+
// fresh exact matches before it may issue or reissue a signal.
|
|
3058
|
+
return (
|
|
3059
|
+
identity?.status === "unverifiable" &&
|
|
3060
|
+
["creation-time-probe-failed", "process-group-state-probe-failed"].includes(
|
|
3061
|
+
identity?.reason,
|
|
3062
|
+
)
|
|
3063
|
+
);
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
function createPosixProbeRetryBudget(deadline) {
|
|
3067
|
+
const configuredRetryWindow = Math.max(
|
|
3068
|
+
0,
|
|
3069
|
+
Number(_deps.processExitWaitDeadlineMs) || 0,
|
|
3070
|
+
);
|
|
3071
|
+
let remainingSleepBudget = Math.min(
|
|
3072
|
+
configuredRetryWindow,
|
|
3073
|
+
Math.max(0, deadline - _deps.posixPermissionRetryNowMs()),
|
|
3074
|
+
);
|
|
3075
|
+
let retryWaits = 0;
|
|
3076
|
+
|
|
3077
|
+
return {
|
|
3078
|
+
wait() {
|
|
3079
|
+
const remaining = Math.min(
|
|
3080
|
+
remainingSleepBudget,
|
|
3081
|
+
deadline - _deps.posixPermissionRetryNowMs(),
|
|
3082
|
+
);
|
|
3083
|
+
if (!(remaining > 0)) return false;
|
|
3084
|
+
const retryDelayMs = Math.min(
|
|
3085
|
+
remaining,
|
|
3086
|
+
10 * 2 ** Math.min(retryWaits, 4),
|
|
3087
|
+
);
|
|
3088
|
+
_deps.posixPermissionRetrySleep(retryDelayMs);
|
|
3089
|
+
remainingSleepBudget -= retryDelayMs;
|
|
3090
|
+
retryWaits += 1;
|
|
3091
|
+
return true;
|
|
3092
|
+
},
|
|
3093
|
+
hasRemaining() {
|
|
3094
|
+
return (
|
|
3095
|
+
remainingSleepBudget > 0 && _deps.posixPermissionRetryNowMs() < deadline
|
|
3096
|
+
);
|
|
3097
|
+
},
|
|
3098
|
+
};
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3101
|
+
/**
|
|
3102
|
+
* Retry an observational failure before the first POSIX signal. Unknown
|
|
3103
|
+
* identity never authorizes a signal: only two consecutive delayed fresh
|
|
3104
|
+
* matches do. A dead/reused target converges without signalling, while every
|
|
3105
|
+
* non-observational identity failure remains immediately fail-closed.
|
|
3106
|
+
*/
|
|
3107
|
+
function retryRecoverableInitialPosixIdentity({
|
|
3108
|
+
inspectIdentity,
|
|
3109
|
+
initialIdentity,
|
|
3110
|
+
deadline,
|
|
3111
|
+
}) {
|
|
3112
|
+
let lastIdentity = initialIdentity;
|
|
3113
|
+
let identityProbeRetries = 0;
|
|
3114
|
+
const retryBudget = createPosixProbeRetryBudget(deadline);
|
|
3115
|
+
const outcome = (status, retryStoppedBy) => ({
|
|
3116
|
+
status,
|
|
3117
|
+
lastIdentity,
|
|
3118
|
+
identityProbeRetries,
|
|
3119
|
+
retryStoppedBy,
|
|
3120
|
+
});
|
|
3121
|
+
|
|
3122
|
+
for (;;) {
|
|
3123
|
+
if (isDestructiveIdentityRetired(lastIdentity)) {
|
|
3124
|
+
return outcome("retired", "retired");
|
|
3125
|
+
}
|
|
3126
|
+
if (
|
|
3127
|
+
lastIdentity?.status !== "match" &&
|
|
3128
|
+
!isRecoverableDestructiveIdentityProbe(lastIdentity)
|
|
3129
|
+
) {
|
|
3130
|
+
return outcome("failed", "preflight-identity");
|
|
3131
|
+
}
|
|
3132
|
+
if (!retryBudget.wait()) {
|
|
3133
|
+
return outcome("failed", "initial-identity-probe-deadline");
|
|
3134
|
+
}
|
|
3135
|
+
|
|
3136
|
+
const identityBeforeDelay = lastIdentity;
|
|
3137
|
+
identityProbeRetries += 1;
|
|
3138
|
+
lastIdentity = inspectIdentity();
|
|
3139
|
+
|
|
3140
|
+
if (isDestructiveIdentityRetired(lastIdentity)) {
|
|
3141
|
+
return outcome("retired", "retired");
|
|
3142
|
+
}
|
|
3143
|
+
if (
|
|
3144
|
+
lastIdentity?.status !== "match" &&
|
|
3145
|
+
!isRecoverableDestructiveIdentityProbe(lastIdentity)
|
|
3146
|
+
) {
|
|
3147
|
+
return outcome("failed", "preflight-identity");
|
|
3148
|
+
}
|
|
3149
|
+
if (
|
|
3150
|
+
identityBeforeDelay?.status === "match" &&
|
|
3151
|
+
lastIdentity?.status === "match"
|
|
3152
|
+
) {
|
|
3153
|
+
if (!retryBudget.hasRemaining()) {
|
|
3154
|
+
return outcome("failed", "initial-identity-probe-deadline");
|
|
3155
|
+
}
|
|
3156
|
+
return outcome("matched", "consecutive-match");
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
/**
|
|
3162
|
+
* Recover from a transient POSIX EPERM without ever signalling from unknown
|
|
3163
|
+
* identity evidence. Probe-only waits may bridge a transient creation-time or
|
|
3164
|
+
* process-group-state lookup failure, but a resend requires two consecutive
|
|
3165
|
+
* fresh exact matches.
|
|
3166
|
+
*/
|
|
3167
|
+
function retryExactPosixSignalAfterPermissionError({
|
|
3168
|
+
inspectIdentity,
|
|
3169
|
+
sendSignal,
|
|
3170
|
+
deadline,
|
|
3171
|
+
initialCause,
|
|
3172
|
+
}) {
|
|
3173
|
+
let cause = initialCause;
|
|
3174
|
+
let lastIdentity = inspectIdentity();
|
|
3175
|
+
let signalResends = 0;
|
|
3176
|
+
let identityProbeRetries = 0;
|
|
3177
|
+
const retryBudget = createPosixProbeRetryBudget(deadline);
|
|
3178
|
+
|
|
3179
|
+
const outcome = (status, retryStoppedBy, extra = {}) => ({
|
|
3180
|
+
status,
|
|
3181
|
+
cause,
|
|
3182
|
+
lastIdentity,
|
|
3183
|
+
signalResends,
|
|
3184
|
+
identityProbeRetries,
|
|
3185
|
+
retryStoppedBy,
|
|
3186
|
+
...extra,
|
|
3187
|
+
});
|
|
3188
|
+
|
|
3189
|
+
for (;;) {
|
|
3190
|
+
if (isDestructiveIdentityRetired(lastIdentity)) {
|
|
3191
|
+
return outcome("retired", "retired");
|
|
3192
|
+
}
|
|
3193
|
+
if (
|
|
3194
|
+
lastIdentity?.status !== "match" &&
|
|
3195
|
+
!isRecoverableDestructiveIdentityProbe(lastIdentity)
|
|
3196
|
+
) {
|
|
3197
|
+
return outcome("failed", "identity");
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
if (!retryBudget.wait()) {
|
|
3201
|
+
return outcome(
|
|
3202
|
+
"failed",
|
|
3203
|
+
lastIdentity?.status === "match"
|
|
3204
|
+
? "signal-retry-deadline"
|
|
3205
|
+
: "identity-probe-deadline",
|
|
3206
|
+
);
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
const identityBeforeDelay = lastIdentity;
|
|
3210
|
+
identityProbeRetries += 1;
|
|
3211
|
+
lastIdentity = inspectIdentity();
|
|
3212
|
+
|
|
3213
|
+
if (isDestructiveIdentityRetired(lastIdentity)) {
|
|
3214
|
+
return outcome("retired", "retired");
|
|
3215
|
+
}
|
|
3216
|
+
if (
|
|
3217
|
+
lastIdentity?.status !== "match" &&
|
|
3218
|
+
!isRecoverableDestructiveIdentityProbe(lastIdentity)
|
|
3219
|
+
) {
|
|
3220
|
+
return outcome("failed", "identity");
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
// A target that moved from unknown to match gets one more probe-only
|
|
3224
|
+
// interval. Only match -> fresh match authorizes the next signal syscall.
|
|
3225
|
+
if (
|
|
3226
|
+
identityBeforeDelay?.status !== "match" ||
|
|
3227
|
+
lastIdentity?.status !== "match"
|
|
3228
|
+
) {
|
|
3229
|
+
continue;
|
|
3230
|
+
}
|
|
3231
|
+
if (!retryBudget.hasRemaining()) {
|
|
3232
|
+
return outcome("failed", "signal-retry-deadline");
|
|
3233
|
+
}
|
|
3234
|
+
signalResends += 1;
|
|
3235
|
+
try {
|
|
3236
|
+
const signalResult = sendSignal();
|
|
3237
|
+
return outcome(
|
|
3238
|
+
signalResult === "already-exited" ? "retired" : "signalled",
|
|
3239
|
+
"completed",
|
|
3240
|
+
{ signalResult },
|
|
3241
|
+
);
|
|
3242
|
+
} catch (error) {
|
|
3243
|
+
cause = error;
|
|
3244
|
+
if (error?.code !== "EPERM") {
|
|
3245
|
+
return outcome("failed", "signal");
|
|
3246
|
+
}
|
|
3247
|
+
// The failed resend has no authority value. Start the next decision from
|
|
3248
|
+
// a fresh probe, then require another delayed fresh match before signalling.
|
|
3249
|
+
lastIdentity = inspectIdentity();
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
|
|
3254
|
+
function signalExactBackgroundProcessTree(
|
|
3255
|
+
pid,
|
|
3256
|
+
signal,
|
|
3257
|
+
{
|
|
3258
|
+
platform = process.platform,
|
|
3259
|
+
expectedStartedAt,
|
|
3260
|
+
processGroup = true,
|
|
3261
|
+
allowDirectFallback = false,
|
|
3262
|
+
} = {},
|
|
3263
|
+
) {
|
|
3264
|
+
const target = Number(pid);
|
|
3265
|
+
const mode = processGroup === true ? "process-group" : "direct";
|
|
3266
|
+
const posixRetryDeadline =
|
|
3267
|
+
_deps.posixPermissionRetryNowMs() +
|
|
3268
|
+
Math.max(0, Number(_deps.processExitWaitDeadlineMs) || 0);
|
|
3269
|
+
let initialSignalAttempts = 0;
|
|
3270
|
+
let signalResends = 0;
|
|
3271
|
+
let identityProbeRetries = 0;
|
|
3272
|
+
let lastIdentity = null;
|
|
3273
|
+
let retryStoppedBy = "preflight";
|
|
3274
|
+
const inspectIdentity = () =>
|
|
3275
|
+
inspectDestructiveProcessIdentity(target, expectedStartedAt, {
|
|
3276
|
+
processGroup,
|
|
3277
|
+
});
|
|
3278
|
+
const failure = (cause) => {
|
|
3279
|
+
const reason = cause?.message || String(cause);
|
|
3280
|
+
const error = new Error(
|
|
3281
|
+
`background_agent_child_tree_stop_failed: ${reason} (${mode} pid ${target}, initial signal attempts ${initialSignalAttempts}, signal resends ${signalResends}, identity probe retries ${identityProbeRetries}, last identity ${lastIdentity?.status || "not-probed"}/${lastIdentity?.reason || "no-reason"}, retry stopped by ${retryStoppedBy})`,
|
|
3282
|
+
{ cause },
|
|
3283
|
+
);
|
|
3284
|
+
error.code = "ERR_BACKGROUND_AGENT_CHILD_TREE_STOP_FAILED";
|
|
3285
|
+
return error;
|
|
3286
|
+
};
|
|
3287
|
+
|
|
3288
|
+
// Unlike the legacy tree helper, the keeper's destructive path owns a
|
|
3289
|
+
// persisted creation-time anchor. Re-authorize the initial signal from a
|
|
3290
|
+
// fresh exact snapshot so a stale/reused PID can never address a stranger.
|
|
3291
|
+
lastIdentity = inspectIdentity();
|
|
3292
|
+
if (isDestructiveIdentityRetired(lastIdentity)) {
|
|
3293
|
+
return { alreadyRetired: true, signalledAsGroup: processGroup === true };
|
|
3294
|
+
}
|
|
3295
|
+
if (lastIdentity.status !== "match") {
|
|
3296
|
+
if (
|
|
3297
|
+
platform === "win32" ||
|
|
3298
|
+
!isRecoverableDestructiveIdentityProbe(lastIdentity)
|
|
3299
|
+
) {
|
|
3300
|
+
retryStoppedBy = "preflight-identity";
|
|
3301
|
+
throw failure(
|
|
3302
|
+
new Error(
|
|
3303
|
+
`destructive process identity is ${lastIdentity.status}: ${lastIdentity.reason || "unknown"}`,
|
|
3304
|
+
),
|
|
3305
|
+
);
|
|
3306
|
+
}
|
|
3307
|
+
const retry = retryRecoverableInitialPosixIdentity({
|
|
3308
|
+
inspectIdentity,
|
|
3309
|
+
initialIdentity: lastIdentity,
|
|
3310
|
+
deadline: posixRetryDeadline,
|
|
3311
|
+
});
|
|
3312
|
+
identityProbeRetries += retry.identityProbeRetries;
|
|
3313
|
+
lastIdentity = retry.lastIdentity;
|
|
3314
|
+
retryStoppedBy = retry.retryStoppedBy;
|
|
3315
|
+
if (retry.status === "retired") {
|
|
3316
|
+
return { alreadyRetired: true, signalledAsGroup: processGroup === true };
|
|
3317
|
+
}
|
|
3318
|
+
if (retry.status !== "matched") {
|
|
3319
|
+
throw failure(
|
|
3320
|
+
new Error(
|
|
3321
|
+
`destructive process identity is ${lastIdentity.status}: ${lastIdentity.reason || "unknown"}`,
|
|
3322
|
+
),
|
|
3323
|
+
);
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
const sendSignal = () =>
|
|
3328
|
+
signalBackgroundProcessTree(target, signal, {
|
|
3329
|
+
platform,
|
|
3330
|
+
processGroup,
|
|
3331
|
+
allowDirectFallback,
|
|
3332
|
+
});
|
|
3333
|
+
|
|
3334
|
+
retryStoppedBy = "initial-signal";
|
|
3335
|
+
try {
|
|
3336
|
+
initialSignalAttempts += 1;
|
|
3337
|
+
const result = sendSignal();
|
|
3338
|
+
return {
|
|
3339
|
+
alreadyRetired: result === "already-exited",
|
|
3340
|
+
signalledAsGroup: result === "process-group",
|
|
3341
|
+
};
|
|
3342
|
+
} catch (initialCause) {
|
|
3343
|
+
let cause = initialCause;
|
|
3344
|
+
if (platform !== "win32" && cause?.code === "EPERM") {
|
|
3345
|
+
const retry = retryExactPosixSignalAfterPermissionError({
|
|
3346
|
+
inspectIdentity,
|
|
3347
|
+
sendSignal,
|
|
3348
|
+
deadline: posixRetryDeadline,
|
|
3349
|
+
initialCause: cause,
|
|
3350
|
+
});
|
|
3351
|
+
signalResends = retry.signalResends;
|
|
3352
|
+
identityProbeRetries += retry.identityProbeRetries;
|
|
3353
|
+
lastIdentity = retry.lastIdentity;
|
|
3354
|
+
retryStoppedBy = retry.retryStoppedBy;
|
|
3355
|
+
cause = retry.cause;
|
|
3356
|
+
if (retry.status === "retired") {
|
|
3357
|
+
return {
|
|
3358
|
+
alreadyRetired: true,
|
|
3359
|
+
signalledAsGroup: processGroup === true,
|
|
3360
|
+
};
|
|
3361
|
+
}
|
|
3362
|
+
if (retry.status === "signalled") {
|
|
3363
|
+
return {
|
|
3364
|
+
alreadyRetired: retry.signalResult === "already-exited",
|
|
3365
|
+
signalledAsGroup: retry.signalResult === "process-group",
|
|
3366
|
+
};
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
if (
|
|
3370
|
+
processGroup === true &&
|
|
3371
|
+
allowDirectFallback !== true &&
|
|
3372
|
+
cause?.code === "ESRCH"
|
|
3373
|
+
) {
|
|
3374
|
+
// A group may disappear between the exact preflight and signal. It is
|
|
3375
|
+
// safe to accept only a fresh dead/reused result; a still-live leader
|
|
3376
|
+
// is never converted to a direct kill on the keeper path.
|
|
3377
|
+
lastIdentity = inspectIdentity();
|
|
3378
|
+
if (isDestructiveIdentityRetired(lastIdentity)) {
|
|
3379
|
+
return {
|
|
3380
|
+
alreadyRetired: true,
|
|
3381
|
+
signalledAsGroup: true,
|
|
3382
|
+
};
|
|
3383
|
+
}
|
|
3384
|
+
retryStoppedBy = "identity";
|
|
3385
|
+
}
|
|
3386
|
+
throw failure(cause);
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
|
|
2746
3390
|
function persistStopPending(state, reason, details = {}) {
|
|
2747
3391
|
const at = Date.now();
|
|
2748
3392
|
const mutation = mutateBackgroundAgentState(state.id, (current) =>
|
|
@@ -2804,6 +3448,7 @@ function waitForSignalledProcessesToExit(
|
|
|
2804
3448
|
const identity = inspectDestructiveProcessIdentity(
|
|
2805
3449
|
target.pid,
|
|
2806
3450
|
target.startedAt,
|
|
3451
|
+
{ processGroup: target.signalledAsGroup },
|
|
2807
3452
|
);
|
|
2808
3453
|
return ["match", "unverifiable", "self"].includes(identity.status);
|
|
2809
3454
|
});
|
|
@@ -2839,7 +3484,13 @@ export function stopBackgroundAgent(id) {
|
|
|
2839
3484
|
if (!state) throw new Error(`Background agent not found: ${id}`);
|
|
2840
3485
|
const staleHeartbeatOwner =
|
|
2841
3486
|
state.status === "lost" && state.lostReason === "heartbeat-stale";
|
|
2842
|
-
|
|
3487
|
+
const preexistingStopCleanupRepair =
|
|
3488
|
+
hasBackgroundAgentStopFenceWithoutCleanupProof(state);
|
|
3489
|
+
if (
|
|
3490
|
+
state.status !== "running" &&
|
|
3491
|
+
!staleHeartbeatOwner &&
|
|
3492
|
+
!preexistingStopCleanupRepair
|
|
3493
|
+
) {
|
|
2843
3494
|
// Gap 2: stopping an already-lost session still reaps a leaked agent
|
|
2844
3495
|
// child recorded before the worker died (identity-guarded inside).
|
|
2845
3496
|
if (state.status === "lost") {
|
|
@@ -2864,7 +3515,8 @@ export function stopBackgroundAgent(id) {
|
|
|
2864
3515
|
(current.status !== "running" &&
|
|
2865
3516
|
!(
|
|
2866
3517
|
current.status === "lost" && current.lostReason === "heartbeat-stale"
|
|
2867
|
-
)
|
|
3518
|
+
) &&
|
|
3519
|
+
!hasBackgroundAgentStopFenceWithoutCleanupProof(current))
|
|
2868
3520
|
) {
|
|
2869
3521
|
return null;
|
|
2870
3522
|
}
|
|
@@ -2883,6 +3535,10 @@ export function stopBackgroundAgent(id) {
|
|
|
2883
3535
|
return { ...fence.state, stopped: false };
|
|
2884
3536
|
}
|
|
2885
3537
|
state = fence.state;
|
|
3538
|
+
const repairingUnconfirmedStop =
|
|
3539
|
+
preexistingStopCleanupRepair ||
|
|
3540
|
+
hasBackgroundAgentStopFenceWithoutCleanupProof(fence.previous) ||
|
|
3541
|
+
isBackgroundAgentStopCleanupUnconfirmed(state);
|
|
2886
3542
|
if (state.launchFinalizationUncertain === true || state.turnLaunchIntent) {
|
|
2887
3543
|
// The worker has durably announced that native spawn is prepared or that
|
|
2888
3544
|
// post-spawn pid persistence is unresolved. Killing the worker from this
|
|
@@ -2921,6 +3577,7 @@ export function stopBackgroundAgent(id) {
|
|
|
2921
3577
|
let postKill = state;
|
|
2922
3578
|
let workerPidReused = false;
|
|
2923
3579
|
const signalledPids = new Set();
|
|
3580
|
+
let activeTerminationContext = null;
|
|
2924
3581
|
// A worker may publish a just-spawned child identity while the stop fence is
|
|
2925
3582
|
// being observed. Re-read once after the first termination round so that a
|
|
2926
3583
|
// newly durable, exact-owned agent tree is also terminated before recovery.
|
|
@@ -2944,7 +3601,17 @@ export function stopBackgroundAgent(id) {
|
|
|
2944
3601
|
inspected.find(({ role }) => role === "worker") ||
|
|
2945
3602
|
inspected.find(({ role }) => role === "worker-published") ||
|
|
2946
3603
|
inspected.find(({ role }) => role === "worker-wrapper");
|
|
2947
|
-
if (
|
|
3604
|
+
if (
|
|
3605
|
+
primaryWorker?.identity.status === "reused" &&
|
|
3606
|
+
!signalledPids.has(primaryWorker.pid)
|
|
3607
|
+
) {
|
|
3608
|
+
// A fresh stop must refuse a worker pid that was already reused at the
|
|
3609
|
+
// first strict probe. If the exact worker matched and was signalled in an
|
|
3610
|
+
// earlier round, however, observing that numeric pid reused afterward is
|
|
3611
|
+
// evidence that the owned process retired, not grounds to discard the
|
|
3612
|
+
// durable cleanup proof.
|
|
3613
|
+
workerPidReused = true;
|
|
3614
|
+
}
|
|
2948
3615
|
const targets = inspected.filter(
|
|
2949
3616
|
({ pid, identity }) =>
|
|
2950
3617
|
identity.status === "match" && !signalledPids.has(pid),
|
|
@@ -2952,14 +3619,163 @@ export function stopBackgroundAgent(id) {
|
|
|
2952
3619
|
if (targets.length === 0) break;
|
|
2953
3620
|
try {
|
|
2954
3621
|
// The child is detached into its own group and therefore gets its own
|
|
2955
|
-
// exact-identity signal before the worker tree is terminated.
|
|
3622
|
+
// exact-identity signal before the worker tree is terminated. The
|
|
3623
|
+
// turn's detached agent root owns its tool descendants; the worker and
|
|
3624
|
+
// independent keeper do not. Signal those single processes directly so
|
|
3625
|
+
// a rapidly recycled pid cannot address an unrelated POSIX group.
|
|
2956
3626
|
for (const target of targets) {
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
3627
|
+
let permissionRetryDeadline = 0;
|
|
3628
|
+
const inspectIdentity = () =>
|
|
3629
|
+
inspectDestructiveProcessIdentity(target.pid, target.startedAt, {
|
|
3630
|
+
processGroup: target.signalAsGroup,
|
|
3631
|
+
});
|
|
3632
|
+
const sendInitialSignal = () =>
|
|
3633
|
+
signalBackgroundProcessTree(target.pid, "SIGTERM", {
|
|
3634
|
+
processGroup: target.signalAsGroup,
|
|
3635
|
+
// Preserve the pre-existing ESRCH behavior. EPERM itself never
|
|
3636
|
+
// enters this fallback because the tree primitive rethrows it.
|
|
3637
|
+
allowDirectFallback: true,
|
|
3638
|
+
});
|
|
3639
|
+
const resendSignal = () =>
|
|
3640
|
+
signalBackgroundProcessTree(target.pid, "SIGTERM", {
|
|
3641
|
+
processGroup: target.signalAsGroup,
|
|
3642
|
+
// After EPERM, every resend is a newly authorized action. A group
|
|
3643
|
+
// ESRCH must be resolved by fresh identity evidence, never by
|
|
3644
|
+
// changing that action into a direct leader signal.
|
|
3645
|
+
allowDirectFallback: false,
|
|
3646
|
+
});
|
|
3647
|
+
activeTerminationContext = {
|
|
3648
|
+
role: target.role,
|
|
3649
|
+
pid: target.pid,
|
|
3650
|
+
mode: target.signalAsGroup ? "process-group" : "direct",
|
|
3651
|
+
signalResends: 0,
|
|
3652
|
+
identityProbeRetries: 0,
|
|
3653
|
+
};
|
|
3654
|
+
try {
|
|
3655
|
+
let preflightIdentity = inspectIdentity();
|
|
3656
|
+
activeTerminationContext.lastIdentityStatus =
|
|
3657
|
+
preflightIdentity.status;
|
|
3658
|
+
activeTerminationContext.lastIdentityReason =
|
|
3659
|
+
preflightIdentity.reason || null;
|
|
3660
|
+
if (isDestructiveIdentityRetired(preflightIdentity)) {
|
|
3661
|
+
if (
|
|
3662
|
+
preflightIdentity.status === "reused" &&
|
|
3663
|
+
primaryWorker?.pid === target.pid &&
|
|
3664
|
+
!signalledPids.has(target.pid)
|
|
3665
|
+
) {
|
|
3666
|
+
workerPidReused = true;
|
|
3667
|
+
}
|
|
3668
|
+
target.retiredBeforeWait = true;
|
|
3669
|
+
signalledPids.add(target.pid);
|
|
3670
|
+
continue;
|
|
3671
|
+
}
|
|
3672
|
+
if (preflightIdentity.status !== "match") {
|
|
3673
|
+
if (
|
|
3674
|
+
process.platform === "win32" ||
|
|
3675
|
+
!isRecoverableDestructiveIdentityProbe(preflightIdentity)
|
|
3676
|
+
) {
|
|
3677
|
+
activeTerminationContext.retryStoppedBy = "preflight-identity";
|
|
3678
|
+
throw new Error(
|
|
3679
|
+
`destructive process identity is ${preflightIdentity.status}: ${preflightIdentity.reason || "unknown"}`,
|
|
3680
|
+
);
|
|
3681
|
+
}
|
|
3682
|
+
permissionRetryDeadline =
|
|
3683
|
+
_deps.posixPermissionRetryNowMs() +
|
|
3684
|
+
Math.max(0, Number(_deps.processExitWaitDeadlineMs) || 0);
|
|
3685
|
+
const retry = retryRecoverableInitialPosixIdentity({
|
|
3686
|
+
inspectIdentity,
|
|
3687
|
+
initialIdentity: preflightIdentity,
|
|
3688
|
+
deadline: permissionRetryDeadline,
|
|
3689
|
+
});
|
|
3690
|
+
activeTerminationContext.identityProbeRetries +=
|
|
3691
|
+
retry.identityProbeRetries;
|
|
3692
|
+
preflightIdentity = retry.lastIdentity;
|
|
3693
|
+
activeTerminationContext.lastIdentityStatus =
|
|
3694
|
+
preflightIdentity?.status;
|
|
3695
|
+
activeTerminationContext.lastIdentityReason =
|
|
3696
|
+
preflightIdentity?.reason || null;
|
|
3697
|
+
activeTerminationContext.retryStoppedBy = retry.retryStoppedBy;
|
|
3698
|
+
if (retry.status === "retired") {
|
|
3699
|
+
if (
|
|
3700
|
+
preflightIdentity?.status === "reused" &&
|
|
3701
|
+
primaryWorker?.pid === target.pid &&
|
|
3702
|
+
!signalledPids.has(target.pid)
|
|
3703
|
+
) {
|
|
3704
|
+
workerPidReused = true;
|
|
3705
|
+
}
|
|
3706
|
+
target.retiredBeforeWait = true;
|
|
3707
|
+
signalledPids.add(target.pid);
|
|
3708
|
+
continue;
|
|
3709
|
+
}
|
|
3710
|
+
if (retry.status !== "matched") {
|
|
3711
|
+
throw new Error(
|
|
3712
|
+
`destructive process identity is ${preflightIdentity?.status || "unknown"}: ${preflightIdentity?.reason || "unknown"}`,
|
|
3713
|
+
);
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
if (!(permissionRetryDeadline > 0)) {
|
|
3717
|
+
permissionRetryDeadline =
|
|
3718
|
+
_deps.posixPermissionRetryNowMs() +
|
|
3719
|
+
Math.max(0, Number(_deps.processExitWaitDeadlineMs) || 0);
|
|
3720
|
+
}
|
|
3721
|
+
activeTerminationContext.retryStoppedBy = "initial-signal";
|
|
3722
|
+
target.signalledAsGroup = sendInitialSignal() === "process-group";
|
|
3723
|
+
signalledPids.add(target.pid);
|
|
3724
|
+
} catch (error) {
|
|
3725
|
+
if (process.platform !== "win32" && error?.code === "EPERM") {
|
|
3726
|
+
const retry = retryExactPosixSignalAfterPermissionError({
|
|
3727
|
+
inspectIdentity,
|
|
3728
|
+
sendSignal: resendSignal,
|
|
3729
|
+
deadline: permissionRetryDeadline,
|
|
3730
|
+
initialCause: error,
|
|
3731
|
+
});
|
|
3732
|
+
activeTerminationContext.signalResends = retry.signalResends;
|
|
3733
|
+
activeTerminationContext.identityProbeRetries =
|
|
3734
|
+
retry.identityProbeRetries;
|
|
3735
|
+
activeTerminationContext.lastIdentityStatus =
|
|
3736
|
+
retry.lastIdentity?.status;
|
|
3737
|
+
activeTerminationContext.lastIdentityReason =
|
|
3738
|
+
retry.lastIdentity?.reason || null;
|
|
3739
|
+
activeTerminationContext.retryStoppedBy = retry.retryStoppedBy;
|
|
3740
|
+
if (retry.status === "retired") {
|
|
3741
|
+
target.signalledAsGroup = retry.signalResult
|
|
3742
|
+
? retry.signalResult === "process-group"
|
|
3743
|
+
: target.signalAsGroup === true;
|
|
3744
|
+
signalledPids.add(target.pid);
|
|
3745
|
+
continue;
|
|
3746
|
+
}
|
|
3747
|
+
if (retry.status === "signalled") {
|
|
3748
|
+
target.signalledAsGroup = retry.signalResult === "process-group";
|
|
3749
|
+
signalledPids.add(target.pid);
|
|
3750
|
+
continue;
|
|
3751
|
+
}
|
|
3752
|
+
if (
|
|
3753
|
+
target.signalAsGroup === true &&
|
|
3754
|
+
retry.cause?.code === "ESRCH"
|
|
3755
|
+
) {
|
|
3756
|
+
const refreshedIdentity = inspectIdentity();
|
|
3757
|
+
activeTerminationContext.lastIdentityStatus =
|
|
3758
|
+
refreshedIdentity.status;
|
|
3759
|
+
activeTerminationContext.lastIdentityReason =
|
|
3760
|
+
refreshedIdentity.reason || null;
|
|
3761
|
+
activeTerminationContext.retryStoppedBy = "identity";
|
|
3762
|
+
if (isDestructiveIdentityRetired(refreshedIdentity)) {
|
|
3763
|
+
target.signalledAsGroup = true;
|
|
3764
|
+
signalledPids.add(target.pid);
|
|
3765
|
+
continue;
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
throw retry.cause;
|
|
3769
|
+
}
|
|
3770
|
+
throw error;
|
|
3771
|
+
}
|
|
2961
3772
|
}
|
|
2962
3773
|
} catch (error) {
|
|
3774
|
+
const failureMessage = `Failed to stop background agent ${id}${
|
|
3775
|
+
activeTerminationContext
|
|
3776
|
+
? ` (${activeTerminationContext.role} pid ${activeTerminationContext.pid}, ${activeTerminationContext.mode}, signal resends ${activeTerminationContext.signalResends}, identity probe retries ${activeTerminationContext.identityProbeRetries}, last identity ${activeTerminationContext.lastIdentityStatus || "not-probed"}/${activeTerminationContext.lastIdentityReason || "no-reason"}, retry stopped by ${activeTerminationContext.retryStoppedBy || "non-permission-error"})`
|
|
3777
|
+
: ""
|
|
3778
|
+
}: ${error.message}`;
|
|
2963
3779
|
try {
|
|
2964
3780
|
mutateBackgroundAgentState(id, (current) =>
|
|
2965
3781
|
current
|
|
@@ -2968,18 +3784,18 @@ export function stopBackgroundAgent(id) {
|
|
|
2968
3784
|
phase: "stop_failed",
|
|
2969
3785
|
stopPending: true,
|
|
2970
3786
|
stopPendingReason: "process-termination",
|
|
2971
|
-
stopError:
|
|
3787
|
+
stopError: failureMessage,
|
|
2972
3788
|
}
|
|
2973
3789
|
: null,
|
|
2974
3790
|
);
|
|
2975
3791
|
} catch {
|
|
2976
3792
|
/* the durable stopRequestedAt fence already blocks new turns */
|
|
2977
3793
|
}
|
|
2978
|
-
throw new Error(
|
|
2979
|
-
`Failed to stop background agent ${id}: ${error.message}`,
|
|
2980
|
-
);
|
|
3794
|
+
throw new Error(failureMessage);
|
|
2981
3795
|
}
|
|
2982
|
-
const stillAlive = waitForSignalledProcessesToExit(
|
|
3796
|
+
const stillAlive = waitForSignalledProcessesToExit(
|
|
3797
|
+
targets.filter((target) => target.retiredBeforeWait !== true),
|
|
3798
|
+
);
|
|
2983
3799
|
postKill = readBackgroundAgentState(id) || postKill;
|
|
2984
3800
|
if (stillAlive.length > 0) {
|
|
2985
3801
|
return persistStopPending(postKill, "process-exit", {
|
|
@@ -2994,7 +3810,7 @@ export function stopBackgroundAgent(id) {
|
|
|
2994
3810
|
return persistStopPending(postKill, "process-exit");
|
|
2995
3811
|
}
|
|
2996
3812
|
|
|
2997
|
-
if (workerPidReused) {
|
|
3813
|
+
if (workerPidReused && !repairingUnconfirmedStop) {
|
|
2998
3814
|
const lostMutation = mutateBackgroundAgentState(id, (current) =>
|
|
2999
3815
|
current
|
|
3000
3816
|
? {
|
|
@@ -3052,6 +3868,12 @@ export function stopBackgroundAgent(id) {
|
|
|
3052
3868
|
...current,
|
|
3053
3869
|
status: "stopped",
|
|
3054
3870
|
endedAt: current.endedAt || Date.now(),
|
|
3871
|
+
// `endedAt` belongs to the worker/session lifecycle and may predate
|
|
3872
|
+
// retirement of a detached child. Publish a distinct durable proof
|
|
3873
|
+
// only after every exact-owned process identity has been observed
|
|
3874
|
+
// non-executable above.
|
|
3875
|
+
stopCleanupConfirmedAt:
|
|
3876
|
+
backgroundAgentStopCleanupConfirmationTimestamp(current),
|
|
3055
3877
|
stoppedByUser: true,
|
|
3056
3878
|
lostReason: null,
|
|
3057
3879
|
phase: null,
|