chainlesschain 0.163.8 → 0.165.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -4
- package/scripts/macos-mcp-launcher-build.mjs +317 -0
- package/scripts/macos-mcp-launcher-postinstall.sh +54 -0
- package/scripts/macos-mcp-launcher-preinstall.sh +61 -0
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-CavM7W3k.js → AIOps-CYxFDrcm.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-CUvZGabQ.js → ActionButton-CPBtdiU2.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DeBnWnbe.js → Analytics-Dayq7Dp7.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-DbI5kxvd.js → AppLayout-6U7bPb2s.js} +4 -4
- package/src/assets/web-panel/assets/{Artifacts-D93bigKm.js → Artifacts-D2Iqg_3D.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CBNvfUZa.js → Audit-CdoOB7WV.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-CJLyh_YU.js → BackgroundAgents-DrnAVB8w.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-D_0yoehD.js → Backup-CesMi5cy.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-TJxmbObW.js → BaseInput-DaX9ptax.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CE0dto1q.js → Chat-OrwOPxDS.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-CZ5-yYiD.js → ChatBubbleRenderer-CvPZFRxM.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-Ck1RVIVo.js → Checkbox-DiHbcysC.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-qpoyf6DK.js → Codegen-CbgCkaBJ.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DUyPWjIE.js → Col-Du5hgrh4.js} +1 -1
- package/src/assets/web-panel/assets/{Community-D_S1BemS.js → Community-DsPOBW-p.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BjxeK-Gc.js → Compact-BwHIFkNC.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-Dd3CEJsu.js → Compliance-BvrDQSgd.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-B4QG7-ad.js → Cowork-gwAE6arx.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-BtZnids5.js → Cron-DW-0l4uF.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-CHPfCBFD.js → Crosschain-j-NsLesB.js} +1 -1
- package/src/assets/web-panel/assets/{DID-DT8DCEOK.js → DID-CibVEBOI.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-COQab5_U.js → Dashboard-W4z9vSfe.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BY1PsQuU.js → Dropdown-DAcrkKxG.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-CsuAuaq3.js → EmailListRenderer-6GXah-89.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-BqpinD-j.js → FamilyGuardDashboard-TKDCq8Ku.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-D0gkfQFv.js → Federation-BXBLlcNm.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-BqgE-axK.js → FormItemContext-BoJJBkG3.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-D0hJ2KvC.js → GenericCardRenderer-okjsl46v.js} +1 -1
- package/src/assets/web-panel/assets/{Git-CmMB0uPY.js → Git-Rvs7i6nn.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BnVEI0Dk.js → Governance-CHSUxXXS.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-YTAMihRP.js → Inference-V3BlfxsB.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-0ax7VHop.js → KnowledgeGraph-C_NlD4aR.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-B9TRcokL.js → Logs-Nn4LNxhA.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-SVAgNnkt.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-B6CSuD_H.js → Marketplace-DEmjFcg-.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-CjMXvOVU.js → McpTools-DlopXipI.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-CXRF71U3.js → Memory-BG3ynX60.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-DQgNp_oS.js → MobileBridge-DeRzWOfT.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-DlPwcCbq.js → MobileProjects-Tyd3o2A0.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-6hDTRPe1.js → Mtc-BoPbOv1C.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-BlVHU1DS.js → MtcAudit-un-rTHp7.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-DP9pzEbV.js → Multisig-Ds0oveEB.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-Djorh8EV.js → NLProgramming-D09JI-yY.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-oLZ-JkUg.js → Notes-DTRhiKJn.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-Cg288VH3.js → NotificationSettings-BAnnNCim.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-Yr-1fSYi.js +1 -0
- package/src/assets/web-panel/assets/{Organization-CBdo-bHs.js → Organization-S9EAolpj.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-qZdnVumx.js → Overflow-CqcLKqJ8.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-D4kNfGNQ.js → P2P-Dw0F9EcU.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-DnlMemM7.js → PdhVaultBrowser-BDRwUevN.js} +5 -5
- package/src/assets/web-panel/assets/{Permissions-DHucHFBJ.js → Permissions-D3_c3nKB.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-BovlOIPR.js → PersonalDataHub-CxdMV98x.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-Deru3CQY.js → Pipeline-CgjNQFLI.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-Bq5oTTbU.js → Privacy-BHghIIht.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-BfzxVWAz.js → ProjectInit-De6pqaKM.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-CKC6XBtH.js → ProjectSettings-Bg4ZkNYQ.js} +2 -2
- package/src/assets/web-panel/assets/Projects-CP8vPvzs.js +1 -0
- package/src/assets/web-panel/assets/{Providers-C_jx4TWX.js → Providers-CUnHbNf-.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-BdIHeVGC.js → QrScannerModal-DA2f3Wts.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-BwZwAcPD.js → QuickAsk-C1FeHDqp.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BNTt0L-m.js → Recommend-B5guaUMN.js} +1 -1
- package/src/assets/web-panel/assets/RemoteSession-PDADSds3.js +5 -0
- package/src/assets/web-panel/assets/{Reputation-D6VpG32B.js → Reputation-eTlRhgvK.js} +1 -1
- package/src/assets/web-panel/assets/{Row-D_74OvQQ.js → Row-D7AqSGY0.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BTr0a-BQ.js → RssFeed-JiuLm4Ja.js} +3 -3
- package/src/assets/web-panel/assets/{Search-C3MCFoNU.js → Search-CwruahW3.js} +1 -1
- package/src/assets/web-panel/assets/{Security-CqW9aJ6s.js → Security-CdhqaR0b.js} +4 -4
- package/src/assets/web-panel/assets/{Services-Cw5GNaHF.js → Services-CU0ZIlcT.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DZps5qeY.js → Skeleton-CtduPfAb.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-C6NQ1znT.js → Skills-CROXm6AY.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DFP8G2Sh.js → Sla-DiZiABFD.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-ESt6MB6v.js → SpeechSettings-B5pLcXUw.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-BHnN98aM.js → SyncSettings-DBX6J1Oe.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-NiSCaOf-.js → Tasks-BLyRJd3-.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-DsKFhW2C.js → Templates-BKa5qzmk.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-CQ0E0ze5.js → Tenant-DnD3a4M6.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-D_rBu_by.js → Terminal-DEitQeTb.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-D3Vm4Po4.js → TimelineRenderer-CjHggTDq.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-DNTxRH6q.js → Tokens-CsGT7sfl.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-Dgprz1ms.js → Trigger-yr4rFDrW.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-B2rUnVBD.js → Trust-CLvu1Sjp.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DEGQNc6_.js → UkeySign-B0m7yUP8.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-jlsAx3Eh.js → VideoEditing-C0VvP4lc.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-8jQih1ij.js → Wallet-kllBAG1M.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-XLeO6xIB.js → WebAuthn-C54UQNLx.js} +3 -3
- package/src/assets/web-panel/assets/{WorkflowEditor-De_dgDAG.js → WorkflowEditor-B7y8qSvU.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CI19R_Sk.js → chat-DRzTh8CJ.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DsYTOYN3.js → colors-BSMwSnCx.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-CM3-t_Qr.js → compact-item-BTg602H5.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BjxlBRZv.js → createContext-B6EGsdvp.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-Dol8jLEP.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-RDaWX_4i.js → hasIn-DDKqJ8RD.js} +1 -1
- package/src/assets/web-panel/assets/{index-nAH87mQP.js → index-323Hhxxt.js} +1 -1
- package/src/assets/web-panel/assets/{index-DIUnNOjG.js → index-BICn3Qh1.js} +1 -1
- package/src/assets/web-panel/assets/{index-DyIWssPC.js → index-BMuGd2--.js} +1 -1
- package/src/assets/web-panel/assets/{index-DX7tuWVJ.js → index-BRqt4fO-.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cd3euP_4.js → index-BVTAl5N2.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cpqm1qfU.js → index-BsR7Y2NG.js} +1 -1
- package/src/assets/web-panel/assets/{index-DcPFB8qJ.js → index-Bx044qDj.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cd9OJhHG.js → index-C1GFfuqN.js} +1 -1
- package/src/assets/web-panel/assets/index-C3QX5tOl.js +1 -0
- package/src/assets/web-panel/assets/{index-B-Z8eFUJ.js → index-C66NpWy5.js} +1 -1
- package/src/assets/web-panel/assets/{index-BlT2VAEK.js → index-CDTvsX8t.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dsij_KsN.js → index-CGNklbP6.js} +1 -1
- package/src/assets/web-panel/assets/{index-C4gkzxkw.js → index-CHfLYwcW.js} +1 -1
- package/src/assets/web-panel/assets/{index-D6sm5-Xq.js → index-CNxg61YP.js} +1 -1
- package/src/assets/web-panel/assets/{index-C0yx-jtQ.js → index-CQlHFN_g.js} +1 -1
- package/src/assets/web-panel/assets/{index-CjTrRt13.js → index-CUEU48o1.js} +1 -1
- package/src/assets/web-panel/assets/{index-B7WBt_4T.js → index-CWGax2QW.js} +1 -1
- package/src/assets/web-panel/assets/{index-IeiL9RKc.js → index-C_JzIXUq.js} +1 -1
- package/src/assets/web-panel/assets/{index-CnHH7bnD.js → index-Cf23wVMs.js} +1 -1
- package/src/assets/web-panel/assets/{index-BKJr-TZp.js → index-CmAInUkX.js} +1 -1
- package/src/assets/web-panel/assets/{index-CiECJi-Q.js → index-CmAubjDi.js} +1 -1
- package/src/assets/web-panel/assets/{index-BCnZyIMA.js → index-Co7oM1qr.js} +1 -1
- package/src/assets/web-panel/assets/{index-Drlnwr8F.js → index-CwPUZbMI.js} +1 -1
- package/src/assets/web-panel/assets/{index-9R6BxAr0.js → index-D1naNj4U.js} +1 -1
- package/src/assets/web-panel/assets/{index-BGqqJfL0.js → index-D5CsBShd.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bi46Xu4u.js → index-DBT3YeQ9.js} +1 -1
- package/src/assets/web-panel/assets/{index-DXXtGMUJ.js → index-DLdrHhML.js} +1 -1
- package/src/assets/web-panel/assets/{index-B0PMlbMs.js → index-Dc77VR3H.js} +1 -1
- package/src/assets/web-panel/assets/{index-D0zcy54x.js → index-Dfv2lKu3.js} +1 -1
- package/src/assets/web-panel/assets/{index-BN6UPD2E.js → index-DiW0sDkz.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dxge-xWl.js → index-DpTHIRfZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CyOGN6a5.js → index-DqFXGzzC.js} +1 -1
- package/src/assets/web-panel/assets/{index-B9O9YyWE.js → index-XFe-1twF.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHNYPO0b.js → index-p2HYn89m.js} +3 -3
- package/src/assets/web-panel/assets/{index-CVQMeiwH.js → index-pqfux8vZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-pPL66J85.js → index-rjzk9z1Q.js} +1 -1
- package/src/assets/web-panel/assets/{index-DoQA0-52.js → index-th2ihDc0.js} +1 -1
- package/src/assets/web-panel/assets/{index-B5REnKt8.js → index-u2YVld9W.js} +1 -1
- package/src/assets/web-panel/assets/index-xYybvEic.js +1 -0
- package/src/assets/web-panel/assets/{initDefaultProps-BTL88of4.js → initDefaultProps-C3Djh9fa.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BUB2pXwB.js → motion-BzqFmbSe.js} +1 -1
- package/src/assets/web-panel/assets/{move-egCkkOMm.js → move-DJo_FFaW.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-DAWhZC8c.js → mtc-parser-_tlTiBvk.js} +1 -1
- package/src/assets/web-panel/assets/{omit-CvOxe_6d.js → omit-CB2ZyxUg.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DZUSLZ5Z.js → pickAttrs-DZwIFD4u.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BERxGNMY.js → placementArrow-DxZY0dgk.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DZuDfZ9n.js → responsiveObserve-CzzGt7GL.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DO756sgI.js → slide-BaIK52yz.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BRQlIz28.js → statusUtils-3-FHu9_M.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-DCSfdybl.js → styleChecker-BkRT68fK.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-LD8dWVve.js → useFlexGapSupport-DHYwKS-s.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-BX9uW2LK.js → useFs-DVVUhV8p.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BPSlPuHU.js → usePersonalDataHub-CkrFKAzQ.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-BmOILC9V.js → vnode-BuBLFz0c.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-1iKowKGg.js → zoom-kpm6LtXU.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +3 -3
- package/src/commands/cowork.js +418 -41
- package/src/commands/permissions.js +240 -16
- package/src/commands/plugin.js +1293 -56
- package/src/commands/session-location.js +276 -0
- package/src/commands/session.js +2 -0
- package/src/data/changelog.json +28 -2
- package/src/gateways/ws/action-protocol.js +316 -17
- package/src/gateways/ws/message-dispatcher.js +30 -0
- package/src/gateways/ws/remote-session-protocol.js +565 -35
- package/src/gateways/ws/ws-agent-handler.js +12 -0
- package/src/gateways/ws/ws-approval-gate.js +5 -0
- package/src/gateways/ws/ws-server.js +510 -50
- package/src/harness/jsonl-session-store.js +341 -24
- package/src/harness/mcp-client.js +8 -1
- package/src/harness/remote-session-audit.js +2 -0
- package/src/harness/remote-session-crypto.js +929 -21
- package/src/harness/remote-session-registry.js +945 -24
- package/src/harness/session-list-index.js +233 -14
- package/src/lib/agent-authority.js +6 -0
- package/src/lib/auto-mode-config.js +98 -3
- package/src/lib/background-agent-keeper-client.js +248 -0
- package/src/lib/background-agent-keeper-protocol.js +291 -0
- package/src/lib/background-agent-supervisor.js +1357 -85
- package/src/lib/background-interaction-resolver.js +12 -0
- package/src/lib/background-session-transport.js +23 -6
- package/src/lib/background-turn-bootstrap-protocol.js +190 -0
- package/src/lib/cowork-workflow.js +1188 -76
- package/src/lib/delivery-production-adapter.js +38 -2
- package/src/lib/dynamic-workflow-facade.js +1736 -0
- package/src/lib/execution-location-contract.js +551 -0
- package/src/lib/execution-location-runtime.js +130 -0
- package/src/lib/ide-context.js +94 -6
- package/src/lib/mcp-stdio-package-materialization.js +166 -14
- package/src/lib/packer/pkg-config-generator.js +2 -0
- package/src/lib/permission-authority.js +77 -0
- package/src/lib/permission-side-effect-center.js +495 -0
- package/src/lib/plugin-runtime/install.js +514 -13
- package/src/lib/plugin-runtime/marketplace-artifact-readback.js +900 -0
- package/src/lib/plugin-runtime/marketplace-catalog.js +1181 -0
- package/src/lib/plugin-runtime/marketplace-impact.js +408 -0
- package/src/lib/plugin-runtime/marketplace-remote-artifacts.js +1567 -0
- package/src/lib/plugin-security.js +60 -3
- package/src/lib/process-execution-broker/index.js +1095 -57
- package/src/lib/process-execution-broker/linux-bwrap-descriptor-launch.js +375 -0
- package/src/lib/process-execution-broker/linux-generic-bwrap-runtime.js +90 -16
- package/src/lib/process-execution-broker/linux-generic-bwrap.js +159 -18
- package/src/lib/process-execution-broker/macos-mcp-launcher-contract.js +278 -0
- package/src/lib/process-execution-broker/macos-mcp-launcher-protocol.json +23 -0
- package/src/lib/process-execution-broker/macos-mcp-launcher.c +1497 -0
- package/src/lib/process-execution-broker/mcp-fd-entry-bootstrap.js +37 -0
- package/src/lib/process-execution-broker/platform-sandbox.js +2330 -279
- package/src/lib/process-execution-broker/windows-sandbox-helper.dll +0 -0
- package/src/lib/process-execution-broker/windows-sandbox-helper.exe +0 -0
- package/src/lib/process-execution-broker/windows-sandbox.cs +50 -4
- package/src/lib/public-ide-capability-manifest.js +3 -0
- package/src/lib/remote-approval-bridge.js +777 -34
- package/src/lib/remote-control.js +2 -0
- package/src/lib/remote-membership-authority.js +632 -0
- package/src/lib/remote-membership-coordinator.js +2973 -0
- package/src/lib/remote-membership-host-store.js +1799 -0
- package/src/lib/runtime-provenance-ledger.js +171 -46
- package/src/lib/scoped-permission-store.js +369 -0
- package/src/lib/session-anti-rollback-anchor.js +262 -5
- package/src/lib/shell-approval.js +169 -6
- package/src/lib/sub-agent-context.js +4 -0
- package/src/lib/with-file-lock.js +73 -38
- package/src/lib/workflow-definition-contract.js +149 -0
- package/src/repl/agent-repl.js +111 -36
- package/src/repl/chat-repl.js +2 -0
- package/src/repl/clipboard-image.js +735 -16
- package/src/repl/ide-status.js +1 -1
- package/src/repl/prompt-interactions.js +59 -6
- package/src/repl/remote-approval.js +29 -7
- package/src/runtime/agent-core.js +1182 -155
- package/src/runtime/headless-runner.js +52 -30
- package/src/runtime/headless-stream.js +51 -28
- package/src/workers/background-agent-keeper.js +689 -0
- package/src/workers/background-agent-worker.js +365 -13
- package/src/workers/background-turn-bootstrap.js +170 -0
- package/src/assets/web-panel/assets/MarkdownRenderer-yySN5YNM.js +0 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-Bb1O3Ofr.js +0 -1
- package/src/assets/web-panel/assets/Projects-CMolGhae.js +0 -1
- package/src/assets/web-panel/assets/RemoteSession-Biyn2-Ro.js +0 -5
- package/src/assets/web-panel/assets/devWarning-S1ld2LwU.js +0 -1
- package/src/assets/web-panel/assets/index-Drz6yq0m.js +0 -1
- package/src/assets/web-panel/assets/index-uWiUs7S_.js +0 -1
|
@@ -50,6 +50,14 @@ import {
|
|
|
50
50
|
stripBackgroundLaunchSecrets,
|
|
51
51
|
verifyBackgroundLaunchProfileSources,
|
|
52
52
|
} from "./background-launch-profile.js";
|
|
53
|
+
import {
|
|
54
|
+
BACKGROUND_AGENT_KEEPER_POWERSHELL_TIMEOUT_MS,
|
|
55
|
+
BACKGROUND_AGENT_KEEPER_TASKKILL_TIMEOUT_MS,
|
|
56
|
+
BACKGROUND_AGENT_KEEPER_WMIC_TIMEOUT_MS,
|
|
57
|
+
backgroundAgentKeeperPipePath,
|
|
58
|
+
cleanupBackgroundAgentKeeperPipeDirectory,
|
|
59
|
+
prepareBackgroundAgentKeeperPipePath,
|
|
60
|
+
} from "./background-agent-keeper-protocol.js";
|
|
53
61
|
|
|
54
62
|
export const DEFAULT_HEARTBEAT_INTERVAL_MS = 5000;
|
|
55
63
|
export const DEFAULT_HEARTBEAT_STALE_MS = 120000;
|
|
@@ -68,8 +76,88 @@ export const DEFAULT_HEARTBEAT_STALE_MS = 120000;
|
|
|
68
76
|
*/
|
|
69
77
|
export const PID_IDENTITY_TOLERANCE_MS = 60000;
|
|
70
78
|
|
|
71
|
-
const
|
|
72
|
-
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__";
|
|
73
161
|
|
|
74
162
|
function runSupervisorCommand(file, args, options, origin) {
|
|
75
163
|
return _deps.spawnSync(file, args, {
|
|
@@ -102,7 +190,8 @@ export function parseCimDateToMs(raw) {
|
|
|
102
190
|
}
|
|
103
191
|
|
|
104
192
|
/**
|
|
105
|
-
* 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
|
|
106
195
|
* null when it cannot be determined (missing tools, permissions, platform
|
|
107
196
|
* quirks). Callers must treat null as "unknown" and FAIL OPEN to the legacy
|
|
108
197
|
* kill(pid, 0) answer — a broken probe must never declare a live worker dead
|
|
@@ -126,13 +215,21 @@ function defaultReadProcessStartTimeMs(pid) {
|
|
|
126
215
|
"CreationDate",
|
|
127
216
|
"/value",
|
|
128
217
|
],
|
|
129
|
-
{
|
|
218
|
+
{
|
|
219
|
+
windowsHide: true,
|
|
220
|
+
encoding: "utf8",
|
|
221
|
+
timeout: BACKGROUND_AGENT_KEEPER_WMIC_TIMEOUT_MS,
|
|
222
|
+
},
|
|
130
223
|
"background-agent:process-start-time",
|
|
131
224
|
);
|
|
132
225
|
if (!r.error && r.status === 0) {
|
|
133
|
-
const
|
|
226
|
+
const stdout = String(r.stdout || "");
|
|
227
|
+
const m = /CreationDate=([^\r\n]+)/.exec(stdout);
|
|
134
228
|
const ms = m ? parseCimDateToMs(m[1]) : null;
|
|
135
229
|
if (ms !== null) return ms;
|
|
230
|
+
if (/No Instance\(s\) Available/u.test(stdout)) {
|
|
231
|
+
return { status: "absent" };
|
|
232
|
+
}
|
|
136
233
|
}
|
|
137
234
|
} catch {
|
|
138
235
|
/* fall through to PowerShell */
|
|
@@ -140,15 +237,25 @@ function defaultReadProcessStartTimeMs(pid) {
|
|
|
140
237
|
try {
|
|
141
238
|
const script =
|
|
142
239
|
`$p = Get-CimInstance Win32_Process -Filter 'ProcessId=${target}' -ErrorAction SilentlyContinue; ` +
|
|
143
|
-
|
|
240
|
+
`if (-not $p) { '${WINDOWS_PROCESS_ABSENT_MARKER}' } ` +
|
|
241
|
+
"elseif ($p.CreationDate) { [DateTimeOffset]::new($p.CreationDate).ToUnixTimeMilliseconds() } " +
|
|
242
|
+
"else { exit 2 }";
|
|
144
243
|
const r = runSupervisorCommand(
|
|
145
244
|
"powershell",
|
|
146
245
|
["-NoProfile", "-NonInteractive", "-Command", script],
|
|
147
|
-
{
|
|
246
|
+
{
|
|
247
|
+
windowsHide: true,
|
|
248
|
+
encoding: "utf8",
|
|
249
|
+
timeout: BACKGROUND_AGENT_KEEPER_POWERSHELL_TIMEOUT_MS,
|
|
250
|
+
},
|
|
148
251
|
"background-agent:process-start-time",
|
|
149
252
|
);
|
|
150
253
|
if (!r.error && r.status === 0) {
|
|
151
|
-
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);
|
|
152
259
|
if (Number.isFinite(n) && n > 0) return n;
|
|
153
260
|
}
|
|
154
261
|
} catch {
|
|
@@ -276,6 +383,7 @@ function defaultKillProcessTree(pid, signal = "SIGKILL") {
|
|
|
276
383
|
{
|
|
277
384
|
windowsHide: true,
|
|
278
385
|
encoding: "utf8",
|
|
386
|
+
timeout: BACKGROUND_AGENT_KEEPER_TASKKILL_TIMEOUT_MS,
|
|
279
387
|
},
|
|
280
388
|
"background-agent:process-tree-kill",
|
|
281
389
|
);
|
|
@@ -307,8 +415,11 @@ export const _deps = {
|
|
|
307
415
|
readProcessState: defaultReadProcessState,
|
|
308
416
|
readProcessGroupStates: defaultReadProcessGroupStates,
|
|
309
417
|
processExitWaitDeadlineMs: 2_000,
|
|
418
|
+
posixPermissionRetryNowMs: () => performance.now(),
|
|
419
|
+
posixPermissionRetrySleep: sleepSyncMs,
|
|
310
420
|
killProcessTree: defaultKillProcessTree,
|
|
311
421
|
terminateOwnedProcessTree,
|
|
422
|
+
withSessionHostRecoveryLease,
|
|
312
423
|
};
|
|
313
424
|
|
|
314
425
|
/**
|
|
@@ -318,7 +429,14 @@ export const _deps = {
|
|
|
318
429
|
*/
|
|
319
430
|
export function stopBackgroundAgentChildTree(
|
|
320
431
|
pid,
|
|
321
|
-
{
|
|
432
|
+
{
|
|
433
|
+
platform = process.platform,
|
|
434
|
+
signal = "SIGTERM",
|
|
435
|
+
expectedStartedAt,
|
|
436
|
+
processGroup = true,
|
|
437
|
+
strictIdentity = false,
|
|
438
|
+
allowDirectFallback = true,
|
|
439
|
+
} = {},
|
|
322
440
|
) {
|
|
323
441
|
const target = Number(pid);
|
|
324
442
|
if (!Number.isInteger(target) || target <= 0 || target === process.pid) {
|
|
@@ -326,6 +444,15 @@ export function stopBackgroundAgentChildTree(
|
|
|
326
444
|
error.code = "ERR_BACKGROUND_AGENT_CHILD_PID_INVALID";
|
|
327
445
|
throw error;
|
|
328
446
|
}
|
|
447
|
+
if (strictIdentity === true) {
|
|
448
|
+
signalExactBackgroundProcessTree(target, signal, {
|
|
449
|
+
platform,
|
|
450
|
+
expectedStartedAt,
|
|
451
|
+
processGroup,
|
|
452
|
+
allowDirectFallback,
|
|
453
|
+
});
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
329
456
|
if (platform === "win32") {
|
|
330
457
|
const result = runSupervisorCommand(
|
|
331
458
|
"taskkill",
|
|
@@ -333,6 +460,7 @@ export function stopBackgroundAgentChildTree(
|
|
|
333
460
|
{
|
|
334
461
|
windowsHide: true,
|
|
335
462
|
encoding: "utf8",
|
|
463
|
+
timeout: BACKGROUND_AGENT_KEEPER_TASKKILL_TIMEOUT_MS,
|
|
336
464
|
},
|
|
337
465
|
"background-agent:session-stop-tree",
|
|
338
466
|
);
|
|
@@ -349,32 +477,38 @@ export function stopBackgroundAgentChildTree(
|
|
|
349
477
|
}
|
|
350
478
|
return true;
|
|
351
479
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
+
});
|
|
357
488
|
return true;
|
|
358
489
|
}
|
|
359
490
|
|
|
360
491
|
/** Cached probe (default impl only — injected probes run uncached for tests). */
|
|
361
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
|
+
};
|
|
362
498
|
if (_deps.readProcessStartTimeMs !== defaultReadProcessStartTimeMs) {
|
|
363
|
-
|
|
364
|
-
const n = Number(raw);
|
|
365
|
-
return Number.isFinite(n) && n > 0 ? n : null;
|
|
499
|
+
return normalize(_deps.readProcessStartTimeMs(pid));
|
|
366
500
|
}
|
|
367
501
|
const key = Number(pid);
|
|
368
|
-
const at =
|
|
502
|
+
const at = performance.now();
|
|
369
503
|
if (options.fresh === true) {
|
|
370
|
-
const value = defaultReadProcessStartTimeMs(key);
|
|
371
|
-
_startTimeCache.set(key,
|
|
504
|
+
const value = normalize(defaultReadProcessStartTimeMs(key));
|
|
505
|
+
_startTimeCache.set(key, value, at);
|
|
372
506
|
return value;
|
|
373
507
|
}
|
|
374
|
-
const hit = _startTimeCache.
|
|
375
|
-
if (hit
|
|
376
|
-
const value = defaultReadProcessStartTimeMs(key);
|
|
377
|
-
_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);
|
|
378
512
|
return value;
|
|
379
513
|
}
|
|
380
514
|
|
|
@@ -421,6 +555,190 @@ export function logPath(id) {
|
|
|
421
555
|
|
|
422
556
|
const TERMINAL_STATUSES = new Set(["completed", "failed", "stopped", "lost"]);
|
|
423
557
|
|
|
558
|
+
const KEEPER_TERMINAL_STATUSES = new Set([
|
|
559
|
+
"closed",
|
|
560
|
+
"worker-disconnected",
|
|
561
|
+
"cleanup-unconfirmed",
|
|
562
|
+
"startup-timeout",
|
|
563
|
+
"spawn-error",
|
|
564
|
+
"exited",
|
|
565
|
+
]);
|
|
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
|
+
|
|
625
|
+
const TURN_PROCESS_PROJECTION_FIELDS = [
|
|
626
|
+
"agentPid",
|
|
627
|
+
"agentStartedAt",
|
|
628
|
+
"agentRuntimePid",
|
|
629
|
+
"agentRuntimeStartedAt",
|
|
630
|
+
"turnBootstrapStatus",
|
|
631
|
+
"turnBootstrapCommittedAt",
|
|
632
|
+
"turnKeeperStatus",
|
|
633
|
+
"turnKeeperPid",
|
|
634
|
+
"turnKeeperArmedAt",
|
|
635
|
+
"turnKeeperCleanupReason",
|
|
636
|
+
"turnKeeperCleanupRequestedAt",
|
|
637
|
+
"turnKeeperCleanupConfirmedAt",
|
|
638
|
+
"turnKeeperCleanupError",
|
|
639
|
+
];
|
|
640
|
+
|
|
641
|
+
function keeperLifecycleStage(state) {
|
|
642
|
+
if (KEEPER_TERMINAL_STATUSES.has(state?.keeperStatus)) return 4;
|
|
643
|
+
if (state?.keeperStatus === "ready") return 3;
|
|
644
|
+
if (state?.keeperStatus === "listening") return 2;
|
|
645
|
+
if (state?.keeperStatus === "starting") return 1;
|
|
646
|
+
return 0;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function keeperLifecycleClock(state) {
|
|
650
|
+
return Math.max(
|
|
651
|
+
0,
|
|
652
|
+
Number(state?.keeperStartedAt || 0),
|
|
653
|
+
Number(state?.keeperHeartbeatAt || 0),
|
|
654
|
+
Number(state?.keeperReadyAt || 0),
|
|
655
|
+
Number(state?.keeperEndedAt || 0),
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function turnProcessProjectionStage(state) {
|
|
660
|
+
if (
|
|
661
|
+
["terminated", "not-spawned"].includes(state?.turnLaunchResolution?.outcome)
|
|
662
|
+
) {
|
|
663
|
+
return 100;
|
|
664
|
+
}
|
|
665
|
+
if (
|
|
666
|
+
state?.turnLaunchResolution?.outcome === "spawned" &&
|
|
667
|
+
state?.agentPid == null &&
|
|
668
|
+
state?.agentRuntimePid == null &&
|
|
669
|
+
state?.turnBootstrapStatus == null &&
|
|
670
|
+
state?.turnKeeperStatus == null &&
|
|
671
|
+
["idle", "needs_input"].includes(state?.phase)
|
|
672
|
+
) {
|
|
673
|
+
return 90;
|
|
674
|
+
}
|
|
675
|
+
if (state?.turnKeeperStatus === "cleanup-unconfirmed") return 85;
|
|
676
|
+
if (state?.turnKeeperStatus === "retired") return 80;
|
|
677
|
+
if (state?.turnKeeperStatus === "cleanup-requested") return 70;
|
|
678
|
+
if (state?.turnBootstrapStatus === "released") return 60;
|
|
679
|
+
if (state?.turnKeeperStatus === "armed") return 50;
|
|
680
|
+
if (
|
|
681
|
+
state?.turnKeeperStatus === "arming" ||
|
|
682
|
+
state?.turnBootstrapStatus === "awaiting-keeper"
|
|
683
|
+
) {
|
|
684
|
+
return 40;
|
|
685
|
+
}
|
|
686
|
+
if (Number(state?.agentRuntimePid) > 0) return 30;
|
|
687
|
+
if (
|
|
688
|
+
state?.turnBootstrapStatus === "awaiting-ready" ||
|
|
689
|
+
state?.turnKeeperStatus === "waiting-for-runtime"
|
|
690
|
+
) {
|
|
691
|
+
return 20;
|
|
692
|
+
}
|
|
693
|
+
if (Number(state?.agentPid) > 0) return 10;
|
|
694
|
+
return 0;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function turnProcessProjectionClock(state) {
|
|
698
|
+
return Math.max(
|
|
699
|
+
0,
|
|
700
|
+
Number(state?.agentStartedAt || 0),
|
|
701
|
+
Number(state?.agentRuntimeStartedAt || 0),
|
|
702
|
+
Number(state?.turnBootstrapCommittedAt || 0),
|
|
703
|
+
Number(state?.turnKeeperArmedAt || 0),
|
|
704
|
+
Number(state?.turnKeeperCleanupRequestedAt || 0),
|
|
705
|
+
Number(state?.turnKeeperCleanupConfirmedAt || 0),
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function turnProjectionIdentity(state) {
|
|
710
|
+
const resolvedRecord = state?.turnLaunchResolution;
|
|
711
|
+
if (
|
|
712
|
+
turnProcessProjectionStage(state) > 0 &&
|
|
713
|
+
Number.isInteger(Number(resolvedRecord?.attempt))
|
|
714
|
+
) {
|
|
715
|
+
return {
|
|
716
|
+
attempt: Number(resolvedRecord.attempt),
|
|
717
|
+
token: resolvedRecord.token || null,
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
const records = [state?.turnLaunchIntent, state?.turnLaunchResolution].filter(
|
|
721
|
+
(record) => Number.isInteger(Number(record?.attempt)),
|
|
722
|
+
);
|
|
723
|
+
const record = records.sort(
|
|
724
|
+
(left, right) => Number(right.attempt) - Number(left.attempt),
|
|
725
|
+
)[0];
|
|
726
|
+
const attempt = Number(record?.attempt ?? state?.turnLaunchAttempt);
|
|
727
|
+
if (!Number.isInteger(attempt) || attempt <= 0) return null;
|
|
728
|
+
return { attempt, token: record?.token || null };
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function sameTurnProjectionIdentity(leftIdentity, rightIdentity) {
|
|
732
|
+
return Boolean(
|
|
733
|
+
leftIdentity &&
|
|
734
|
+
rightIdentity &&
|
|
735
|
+
leftIdentity.attempt === rightIdentity.attempt &&
|
|
736
|
+
(!leftIdentity.token ||
|
|
737
|
+
!rightIdentity.token ||
|
|
738
|
+
leftIdentity.token === rightIdentity.token),
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
|
|
424
742
|
function readBackgroundAgentStateResult(id) {
|
|
425
743
|
const target = statePath(id);
|
|
426
744
|
if (!existsSync(target)) return { state: null, missing: true, error: null };
|
|
@@ -464,10 +782,17 @@ function mergeBackgroundAgentState(current, requested) {
|
|
|
464
782
|
current.stopRequestedAt &&
|
|
465
783
|
requested.stopRequestedAt === current.stopRequestedAt;
|
|
466
784
|
if (TERMINAL_STATUSES.has(current.status) && !explicitHeartbeatStop) {
|
|
785
|
+
const stopCleanupConfirmedAt = selectBackgroundAgentStopCleanupProof(
|
|
786
|
+
current,
|
|
787
|
+
requested,
|
|
788
|
+
);
|
|
467
789
|
next = {
|
|
468
790
|
...next,
|
|
469
791
|
status: current.status,
|
|
470
792
|
endedAt: current.endedAt ?? next.endedAt ?? null,
|
|
793
|
+
...(stopCleanupConfirmedAt !== undefined
|
|
794
|
+
? { stopCleanupConfirmedAt }
|
|
795
|
+
: {}),
|
|
471
796
|
exitCode: current.exitCode ?? next.exitCode ?? null,
|
|
472
797
|
...(current.signal !== undefined ? { signal: current.signal } : {}),
|
|
473
798
|
...(current.error !== undefined ? { error: current.error } : {}),
|
|
@@ -533,6 +858,9 @@ function mergeBackgroundAgentState(current, requested) {
|
|
|
533
858
|
if (current.workerGeneration) {
|
|
534
859
|
next = { ...next, workerGeneration: current.workerGeneration };
|
|
535
860
|
}
|
|
861
|
+
if (current.keeperGeneration) {
|
|
862
|
+
next = { ...next, keeperGeneration: current.keeperGeneration };
|
|
863
|
+
}
|
|
536
864
|
const claimedWorkerPid = Number(current.workerClaimedPid);
|
|
537
865
|
if (
|
|
538
866
|
current.workerClaimedPid != null &&
|
|
@@ -545,6 +873,43 @@ function mergeBackgroundAgentState(current, requested) {
|
|
|
545
873
|
workerClaimedAt: current.workerClaimedAt,
|
|
546
874
|
};
|
|
547
875
|
}
|
|
876
|
+
// Wrapper and keeper identities are write-once ownership evidence for this
|
|
877
|
+
// generation. In particular, a title/pin writer holding the pre-claim
|
|
878
|
+
// launch snapshot must not drop the independent process that owns cleanup.
|
|
879
|
+
if (Number(current.workerWrapperPid) > 0) {
|
|
880
|
+
next = { ...next, workerWrapperPid: Number(current.workerWrapperPid) };
|
|
881
|
+
}
|
|
882
|
+
if (Number(current.keeperPid) > 0) {
|
|
883
|
+
next = {
|
|
884
|
+
...next,
|
|
885
|
+
keeperPid: Number(current.keeperPid),
|
|
886
|
+
keeperStartedAt: current.keeperStartedAt,
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
if (Number(current.keeperWrapperPid) > 0) {
|
|
890
|
+
next = {
|
|
891
|
+
...next,
|
|
892
|
+
keeperWrapperPid: Number(current.keeperWrapperPid),
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
const currentKeeperStage = keeperLifecycleStage(current);
|
|
896
|
+
const requestedKeeperStage = keeperLifecycleStage(next);
|
|
897
|
+
if (
|
|
898
|
+
currentKeeperStage > requestedKeeperStage ||
|
|
899
|
+
(currentKeeperStage === requestedKeeperStage &&
|
|
900
|
+
keeperLifecycleClock(current) > keeperLifecycleClock(next))
|
|
901
|
+
) {
|
|
902
|
+
next = {
|
|
903
|
+
...next,
|
|
904
|
+
keeperStatus: current.keeperStatus,
|
|
905
|
+
keeperHeartbeatAt: current.keeperHeartbeatAt,
|
|
906
|
+
keeperReadyAt: current.keeperReadyAt,
|
|
907
|
+
keeperEndedAt: current.keeperEndedAt,
|
|
908
|
+
keeperExitCode: current.keeperExitCode,
|
|
909
|
+
keeperSignal: current.keeperSignal,
|
|
910
|
+
keeperError: current.keeperError,
|
|
911
|
+
};
|
|
912
|
+
}
|
|
548
913
|
// A turn launch uses a durable prepare/resolve record. The prepare must
|
|
549
914
|
// reach disk before native spawn so stop/remove can distinguish "no turn"
|
|
550
915
|
// from "a child may exist but its pid commit failed". Only the owner that
|
|
@@ -557,9 +922,25 @@ function mergeBackgroundAgentState(current, requested) {
|
|
|
557
922
|
: 0,
|
|
558
923
|
);
|
|
559
924
|
const requestedTurnAttempt = Number(next.turnLaunchAttempt);
|
|
925
|
+
// Capture each writer's process projection identity before the durable
|
|
926
|
+
// launch record below is reconciled. Otherwise replacing a stale
|
|
927
|
+
// resolution with the current token can accidentally relabel old PIDs as
|
|
928
|
+
// belonging to the newest turn.
|
|
929
|
+
const currentProcessProjectionIdentity = turnProjectionIdentity(current);
|
|
930
|
+
const requestedProcessProjectionIdentity = turnProjectionIdentity(next);
|
|
931
|
+
const currentProcessProjectionStage = turnProcessProjectionStage(current);
|
|
932
|
+
const requestedProcessProjectionStage = turnProcessProjectionStage(next);
|
|
933
|
+
const currentProcessProjectionClock = turnProcessProjectionClock(current);
|
|
934
|
+
const requestedProcessProjectionClock = turnProcessProjectionClock(next);
|
|
560
935
|
const currentTurnIntent = current.turnLaunchIntent;
|
|
561
936
|
const requestedTurnIntent = next.turnLaunchIntent;
|
|
562
937
|
const requestedTurnResolution = next.turnLaunchResolution;
|
|
938
|
+
const requestedCarriesCurrentTurnIntent = Boolean(
|
|
939
|
+
currentTurnIntent &&
|
|
940
|
+
requestedTurnIntent?.token === currentTurnIntent.token &&
|
|
941
|
+
Number(requestedTurnIntent?.attempt) ===
|
|
942
|
+
Number(currentTurnIntent.attempt),
|
|
943
|
+
);
|
|
563
944
|
const resolvesCurrentTurnIntent =
|
|
564
945
|
currentTurnIntent &&
|
|
565
946
|
requestedTurnIntent === null &&
|
|
@@ -591,6 +972,9 @@ function mergeBackgroundAgentState(current, requested) {
|
|
|
591
972
|
...(current.turnLaunchResolution !== undefined
|
|
592
973
|
? { turnLaunchResolution: current.turnLaunchResolution }
|
|
593
974
|
: {}),
|
|
975
|
+
...(!requestedCarriesCurrentTurnIntent && current.phase !== undefined
|
|
976
|
+
? { phase: current.phase }
|
|
977
|
+
: {}),
|
|
594
978
|
};
|
|
595
979
|
} else if (
|
|
596
980
|
!currentTurnIntent &&
|
|
@@ -612,6 +996,59 @@ function mergeBackgroundAgentState(current, requested) {
|
|
|
612
996
|
turnLaunchIntent: null,
|
|
613
997
|
};
|
|
614
998
|
}
|
|
999
|
+
// The target cannot execute Agent main until these fields reach `released`,
|
|
1000
|
+
// and the keeper cannot relinquish ownership until cleanup is confirmed.
|
|
1001
|
+
// Keep that same-turn projection monotonic across stale generic writers.
|
|
1002
|
+
// `retired` -> idle is the only intentional projection clear; once idle,
|
|
1003
|
+
// an old active snapshot may not resurrect already-reaped process ids.
|
|
1004
|
+
const sameProcessProjection = sameTurnProjectionIdentity(
|
|
1005
|
+
currentProcessProjectionIdentity,
|
|
1006
|
+
requestedProcessProjectionIdentity,
|
|
1007
|
+
);
|
|
1008
|
+
const currentProjectionIsNewer = Boolean(
|
|
1009
|
+
currentProcessProjectionIdentity &&
|
|
1010
|
+
(!requestedProcessProjectionIdentity ||
|
|
1011
|
+
currentProcessProjectionIdentity.attempt >
|
|
1012
|
+
requestedProcessProjectionIdentity.attempt ||
|
|
1013
|
+
(currentProcessProjectionIdentity.attempt ===
|
|
1014
|
+
requestedProcessProjectionIdentity.attempt &&
|
|
1015
|
+
currentProcessProjectionIdentity.token &&
|
|
1016
|
+
requestedProcessProjectionIdentity.token &&
|
|
1017
|
+
currentProcessProjectionIdentity.token !==
|
|
1018
|
+
requestedProcessProjectionIdentity.token)),
|
|
1019
|
+
);
|
|
1020
|
+
if (sameProcessProjection || currentProjectionIsNewer) {
|
|
1021
|
+
const intentionalIdleClear =
|
|
1022
|
+
sameProcessProjection &&
|
|
1023
|
+
requestedProcessProjectionStage === 90 &&
|
|
1024
|
+
currentProcessProjectionStage === 80 &&
|
|
1025
|
+
current.turnKeeperStatus === "retired" &&
|
|
1026
|
+
Number(current.turnKeeperCleanupConfirmedAt) > 0;
|
|
1027
|
+
const preserveCurrentProjection =
|
|
1028
|
+
currentProjectionIsNewer ||
|
|
1029
|
+
(!intentionalIdleClear &&
|
|
1030
|
+
(currentProcessProjectionStage > requestedProcessProjectionStage ||
|
|
1031
|
+
(currentProcessProjectionStage ===
|
|
1032
|
+
requestedProcessProjectionStage &&
|
|
1033
|
+
currentProcessProjectionClock >
|
|
1034
|
+
requestedProcessProjectionClock)));
|
|
1035
|
+
if (preserveCurrentProjection) {
|
|
1036
|
+
next = {
|
|
1037
|
+
...next,
|
|
1038
|
+
...((currentProjectionIsNewer ||
|
|
1039
|
+
currentProcessProjectionStage === 90) &&
|
|
1040
|
+
next.status === "running"
|
|
1041
|
+
? { phase: current.phase }
|
|
1042
|
+
: {}),
|
|
1043
|
+
...Object.fromEntries(
|
|
1044
|
+
TURN_PROCESS_PROJECTION_FIELDS.map((field) => [
|
|
1045
|
+
field,
|
|
1046
|
+
current[field],
|
|
1047
|
+
]),
|
|
1048
|
+
),
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
615
1052
|
const turnTerminationConfirmed =
|
|
616
1053
|
next.turnLaunchFinalizationUncertain === false &&
|
|
617
1054
|
next.turnLaunchTermination?.confirmed === true;
|
|
@@ -666,6 +1103,26 @@ function mergeBackgroundAgentState(current, requested) {
|
|
|
666
1103
|
transport: null,
|
|
667
1104
|
};
|
|
668
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
|
+
}
|
|
669
1126
|
}
|
|
670
1127
|
}
|
|
671
1128
|
return next;
|
|
@@ -698,6 +1155,20 @@ function persistBackgroundAgentState(target, next) {
|
|
|
698
1155
|
return next;
|
|
699
1156
|
}
|
|
700
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
|
+
|
|
701
1172
|
/**
|
|
702
1173
|
* Strict cross-process read/mutate/write transaction for one supervisor state.
|
|
703
1174
|
* Returning null from `updater` leaves the record untouched. Missing and
|
|
@@ -727,7 +1198,25 @@ export function mutateBackgroundAgentState(id, updater, options = {}) {
|
|
|
727
1198
|
},
|
|
728
1199
|
{
|
|
729
1200
|
failIfUnavailable: true,
|
|
730
|
-
|
|
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,
|
|
731
1220
|
},
|
|
732
1221
|
);
|
|
733
1222
|
}
|
|
@@ -954,6 +1443,7 @@ export function isSameProcess(pid, expectedStartedAtMs, options = {}) {
|
|
|
954
1443
|
const expected = Number(expectedStartedAtMs);
|
|
955
1444
|
if (!Number.isFinite(expected) || expected <= 0) return true;
|
|
956
1445
|
const actual = processStartTimeMs(pid);
|
|
1446
|
+
if (actual === PROCESS_START_TIME_ABSENT) return false;
|
|
957
1447
|
if (actual === null) return true;
|
|
958
1448
|
const tolerance = Number.isFinite(Number(options.toleranceMs))
|
|
959
1449
|
? Number(options.toleranceMs)
|
|
@@ -988,7 +1478,7 @@ function inspectDestructiveProcessIdentity(
|
|
|
988
1478
|
const leaderCanExecute =
|
|
989
1479
|
leaderAlive && leaderState !== "Z" && leaderState !== "X";
|
|
990
1480
|
if (!leaderCanExecute) {
|
|
991
|
-
if (process.platform === "win32") {
|
|
1481
|
+
if (process.platform === "win32" || options.processGroup === false) {
|
|
992
1482
|
return { status: "dead", reason: "not-executing", pid: target };
|
|
993
1483
|
}
|
|
994
1484
|
const group = inspectProcessGroupExecution(target);
|
|
@@ -1026,6 +1516,13 @@ function inspectDestructiveProcessIdentity(
|
|
|
1026
1516
|
};
|
|
1027
1517
|
}
|
|
1028
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
|
+
}
|
|
1029
1526
|
if (actual === null) {
|
|
1030
1527
|
return {
|
|
1031
1528
|
status: "unverifiable",
|
|
@@ -1038,7 +1535,7 @@ function inspectDestructiveProcessIdentity(
|
|
|
1038
1535
|
? Number(options.toleranceMs)
|
|
1039
1536
|
: PID_IDENTITY_TOLERANCE_MS;
|
|
1040
1537
|
if (Math.abs(actual - expected) > tolerance) {
|
|
1041
|
-
if (process.platform !== "win32") {
|
|
1538
|
+
if (process.platform !== "win32" && options.processGroup !== false) {
|
|
1042
1539
|
const group = inspectProcessGroupExecution(target);
|
|
1043
1540
|
// The leader PID can be reused while descendants from its detached
|
|
1044
1541
|
// process group still execute under the old PGID. We can prove the new
|
|
@@ -1087,15 +1584,36 @@ function inspectDestructiveProcessIdentity(
|
|
|
1087
1584
|
* @returns {boolean} true when a kill was actually issued
|
|
1088
1585
|
*/
|
|
1089
1586
|
export function reclaimOrphanAgentProcess(state) {
|
|
1090
|
-
const
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1587
|
+
const targets = [
|
|
1588
|
+
{
|
|
1589
|
+
pid: Number(state?.agentRuntimePid),
|
|
1590
|
+
startedAt: Number(state?.agentRuntimeStartedAt),
|
|
1591
|
+
},
|
|
1592
|
+
{
|
|
1593
|
+
pid: Number(state?.agentPid),
|
|
1594
|
+
startedAt: Number(state?.agentStartedAt),
|
|
1595
|
+
},
|
|
1596
|
+
].filter(
|
|
1597
|
+
(target, index, values) =>
|
|
1598
|
+
Number.isInteger(target.pid) &&
|
|
1599
|
+
target.pid > 0 &&
|
|
1600
|
+
target.pid !== Number(state?.pid) &&
|
|
1601
|
+
values.findIndex((candidate) => candidate.pid === target.pid) === index,
|
|
1602
|
+
);
|
|
1603
|
+
let killed = false;
|
|
1604
|
+
for (const target of targets) {
|
|
1605
|
+
// Destructive recovery never falls back to a naked PID. Every target must
|
|
1606
|
+
// retain a positive creation-time anchor and pass the fresh OS probe.
|
|
1607
|
+
if (!Number.isFinite(target.startedAt) || target.startedAt <= 0) continue;
|
|
1608
|
+
if (
|
|
1609
|
+
inspectDestructiveProcessIdentity(target.pid, target.startedAt).status !==
|
|
1610
|
+
"match"
|
|
1611
|
+
) {
|
|
1612
|
+
continue;
|
|
1613
|
+
}
|
|
1614
|
+
killed = _deps.killProcessTree(target.pid, "SIGKILL") === true || killed;
|
|
1097
1615
|
}
|
|
1098
|
-
return
|
|
1616
|
+
return killed;
|
|
1099
1617
|
}
|
|
1100
1618
|
|
|
1101
1619
|
export function normalizeBackgroundAgentTitle(title) {
|
|
@@ -1259,7 +1777,7 @@ function recoverTerminalBackgroundInteractions(
|
|
|
1259
1777
|
|
|
1260
1778
|
const fallbackRequest = backgroundInteractionFallback(state);
|
|
1261
1779
|
try {
|
|
1262
|
-
const recovery = withSessionHostRecoveryLease(state.sessionId, () =>
|
|
1780
|
+
const recovery = _deps.withSessionHostRecoveryLease(state.sessionId, () =>
|
|
1263
1781
|
rejectPendingBackgroundInteractions(state.sessionId, state.id, {
|
|
1264
1782
|
fallbackRequest,
|
|
1265
1783
|
code,
|
|
@@ -1303,6 +1821,16 @@ export function effectiveBackgroundAgentState(state, options = {}) {
|
|
|
1303
1821
|
}
|
|
1304
1822
|
return state;
|
|
1305
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
|
+
}
|
|
1306
1834
|
// OS identity probes can take seconds on Windows. Perform them outside the
|
|
1307
1835
|
// state lock, then use the exact observed identity/heartbeat as a CAS fence
|
|
1308
1836
|
// inside the short persistence transaction.
|
|
@@ -1677,6 +2205,13 @@ export function resumeBackgroundAgent(id, prompt, options = {}) {
|
|
|
1677
2205
|
heartbeatStaleMs: options.heartbeatStaleMs,
|
|
1678
2206
|
});
|
|
1679
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
|
+
}
|
|
1680
2215
|
if (state.status === "running") {
|
|
1681
2216
|
throw new Error(
|
|
1682
2217
|
`Background agent ${id} is still running — use cc attach ${id} to send follow-up prompts instead`,
|
|
@@ -1859,20 +2394,33 @@ export function isBackgroundWorkerStartedError(error) {
|
|
|
1859
2394
|
async function settleUncertainBackgroundLaunch({
|
|
1860
2395
|
id,
|
|
1861
2396
|
jobFile,
|
|
2397
|
+
jobFiles,
|
|
1862
2398
|
spawnedProcess,
|
|
2399
|
+
spawnedProcesses,
|
|
1863
2400
|
error,
|
|
1864
2401
|
}) {
|
|
1865
|
-
|
|
2402
|
+
const owned = (spawnedProcesses || [spawnedProcess]).filter(Boolean);
|
|
2403
|
+
let terminations;
|
|
1866
2404
|
try {
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
2405
|
+
terminations = await Promise.all(
|
|
2406
|
+
owned.map((processHandle) =>
|
|
2407
|
+
_deps.terminateOwnedProcessTree(processHandle, {
|
|
2408
|
+
treeMode:
|
|
2409
|
+
process.platform === "win32" ? "windows-tree" : "posix-group",
|
|
2410
|
+
}),
|
|
2411
|
+
),
|
|
2412
|
+
);
|
|
1870
2413
|
} catch {
|
|
1871
2414
|
return;
|
|
1872
2415
|
}
|
|
1873
|
-
if (
|
|
2416
|
+
if (
|
|
2417
|
+
terminations.length !== owned.length ||
|
|
2418
|
+
terminations.some((termination) => termination?.confirmed !== true)
|
|
2419
|
+
) {
|
|
2420
|
+
return;
|
|
2421
|
+
}
|
|
1874
2422
|
try {
|
|
1875
|
-
const workerPid = Number(
|
|
2423
|
+
const workerPid = Number(owned[0]?.pid);
|
|
1876
2424
|
const current = readBackgroundAgentState(id);
|
|
1877
2425
|
if (!current || current.launchFinalizationUncertain !== true) return;
|
|
1878
2426
|
if (
|
|
@@ -1901,15 +2449,25 @@ async function settleUncertainBackgroundLaunch({
|
|
|
1901
2449
|
launchFinalizationUncertain: false,
|
|
1902
2450
|
launchTermination: {
|
|
1903
2451
|
confirmed: true,
|
|
1904
|
-
treeMode:
|
|
1905
|
-
|
|
1906
|
-
|
|
2452
|
+
treeMode:
|
|
2453
|
+
terminations.length === 1
|
|
2454
|
+
? terminations[0].treeMode || null
|
|
2455
|
+
: "multiple-owned-trees",
|
|
2456
|
+
closed: terminations.every(
|
|
2457
|
+
(termination) => termination.closed === true,
|
|
2458
|
+
),
|
|
2459
|
+
treeTerminated: terminations.every(
|
|
2460
|
+
(termination) => termination.treeTerminated === true,
|
|
2461
|
+
),
|
|
2462
|
+
ownedTreeCount: terminations.length,
|
|
1907
2463
|
settledAt: Date.now(),
|
|
1908
2464
|
},
|
|
1909
2465
|
});
|
|
1910
2466
|
// State is the durable cleanup authority; remove the one-shot job only
|
|
1911
2467
|
// after the terminal/ownership evidence is safely persisted.
|
|
1912
|
-
|
|
2468
|
+
for (const file of jobFiles || [jobFile]) {
|
|
2469
|
+
if (file) rmSync(file, { force: true });
|
|
2470
|
+
}
|
|
1913
2471
|
} catch {
|
|
1914
2472
|
// Persistence failure must retain the original uncertainty fence and must
|
|
1915
2473
|
// never become an unhandled rejection in the launching CLI process.
|
|
@@ -1972,24 +2530,43 @@ export function launchBackgroundAgent({
|
|
|
1972
2530
|
omitted: [...launchProfileState.omitted, "apiKey"],
|
|
1973
2531
|
});
|
|
1974
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
|
+
}
|
|
1975
2539
|
const configFingerprint =
|
|
1976
2540
|
fingerprintBackgroundLaunchProfile(launchProfileState);
|
|
1977
2541
|
const dir = backgroundAgentsDir();
|
|
1978
2542
|
const workerGeneration = randomBytes(16).toString("hex");
|
|
2543
|
+
const keeperGeneration = randomBytes(16).toString("hex");
|
|
2544
|
+
const keeperToken = randomBytes(32).toString("hex");
|
|
2545
|
+
const keeperPipePath = backgroundAgentKeeperPipePath(id, dir);
|
|
2546
|
+
prepareBackgroundAgentKeeperPipePath(keeperPipePath);
|
|
1979
2547
|
const jobFile = join(dir, `${id}.job.${process.pid}.json`);
|
|
2548
|
+
const keeperJobFile = join(dir, `${id}.keeper.${process.pid}.json`);
|
|
1980
2549
|
const worker = fileURLToPath(
|
|
1981
2550
|
new URL("../workers/background-agent-worker.js", import.meta.url),
|
|
1982
2551
|
);
|
|
2552
|
+
const keeper = fileURLToPath(
|
|
2553
|
+
new URL("../workers/background-agent-keeper.js", import.meta.url),
|
|
2554
|
+
);
|
|
1983
2555
|
const job = {
|
|
1984
2556
|
id,
|
|
1985
2557
|
workerGeneration,
|
|
1986
2558
|
argv: initialSecrets.argv,
|
|
1987
|
-
cwd,
|
|
2559
|
+
cwd: launchCwd,
|
|
1988
2560
|
sessionId,
|
|
1989
2561
|
sessionBootstrapExpected,
|
|
1990
2562
|
title: title || "Background agent",
|
|
1991
2563
|
cliEntry: cliEntry || process.argv[1],
|
|
1992
2564
|
logFile: logPath(id),
|
|
2565
|
+
keeper: {
|
|
2566
|
+
generation: keeperGeneration,
|
|
2567
|
+
pipePath: keeperPipePath,
|
|
2568
|
+
token: keeperToken,
|
|
2569
|
+
},
|
|
1993
2570
|
...(worktreeState || {}),
|
|
1994
2571
|
governance: governanceState,
|
|
1995
2572
|
// Present = interactive attach can start follow-up turns; absent = the
|
|
@@ -1998,7 +2575,25 @@ export function launchBackgroundAgent({
|
|
|
1998
2575
|
? { followUpArgv: followUpSecrets.argv }
|
|
1999
2576
|
: {}),
|
|
2000
2577
|
};
|
|
2001
|
-
|
|
2578
|
+
try {
|
|
2579
|
+
writeFileSync(jobFile, JSON.stringify(job), { mode: 0o600 });
|
|
2580
|
+
writeFileSync(
|
|
2581
|
+
keeperJobFile,
|
|
2582
|
+
JSON.stringify({
|
|
2583
|
+
id,
|
|
2584
|
+
workerGeneration,
|
|
2585
|
+
keeperGeneration,
|
|
2586
|
+
pipePath: keeperPipePath,
|
|
2587
|
+
token: keeperToken,
|
|
2588
|
+
}),
|
|
2589
|
+
{ mode: 0o600 },
|
|
2590
|
+
);
|
|
2591
|
+
} catch (error) {
|
|
2592
|
+
rmSync(jobFile, { force: true });
|
|
2593
|
+
rmSync(keeperJobFile, { force: true });
|
|
2594
|
+
cleanupBackgroundAgentKeeperPipeDirectory(keeperPipePath);
|
|
2595
|
+
throw error;
|
|
2596
|
+
}
|
|
2002
2597
|
// Write the initial state BEFORE spawning so the worker's own merges (the
|
|
2003
2598
|
// transport endpoint lands within its first ~100ms) can never be clobbered
|
|
2004
2599
|
// by a late launcher write racing the worker.
|
|
@@ -2008,11 +2603,18 @@ export function launchBackgroundAgent({
|
|
|
2008
2603
|
sessionId,
|
|
2009
2604
|
sessionBootstrapExpected,
|
|
2010
2605
|
title: job.title,
|
|
2011
|
-
cwd,
|
|
2606
|
+
cwd: launchCwd,
|
|
2012
2607
|
pid: null,
|
|
2013
2608
|
workerPid: null,
|
|
2014
2609
|
workerClaimedPid: null,
|
|
2015
2610
|
workerClaimedAt: null,
|
|
2611
|
+
workerWrapperPid: null,
|
|
2612
|
+
keeperGeneration,
|
|
2613
|
+
keeperPid: null,
|
|
2614
|
+
keeperStartedAt: null,
|
|
2615
|
+
keeperWrapperPid: null,
|
|
2616
|
+
keeperStatus: "launching",
|
|
2617
|
+
keeperHeartbeatAt: null,
|
|
2016
2618
|
turnLaunchAttempt: 0,
|
|
2017
2619
|
turnLaunchIntent: null,
|
|
2018
2620
|
turnLaunchResolution: null,
|
|
@@ -2036,10 +2638,14 @@ export function launchBackgroundAgent({
|
|
|
2036
2638
|
writeBackgroundAgentState(state, { createIfMissing: true });
|
|
2037
2639
|
} catch (error) {
|
|
2038
2640
|
rmSync(jobFile, { force: true });
|
|
2641
|
+
rmSync(keeperJobFile, { force: true });
|
|
2039
2642
|
rmSync(statePath(id), { force: true });
|
|
2643
|
+
cleanupBackgroundAgentKeeperPipeDirectory(keeperPipePath);
|
|
2040
2644
|
throw error;
|
|
2041
2645
|
}
|
|
2042
2646
|
let child;
|
|
2647
|
+
let keeperChild;
|
|
2648
|
+
let keeperStartedAt = null;
|
|
2043
2649
|
let workerLogFd;
|
|
2044
2650
|
let postSpawnError = null;
|
|
2045
2651
|
try {
|
|
@@ -2049,7 +2655,7 @@ export function launchBackgroundAgent({
|
|
|
2049
2655
|
// hosted process' possibly absent standard handles.
|
|
2050
2656
|
workerLogFd = openSync(job.logFile, "a");
|
|
2051
2657
|
child = _deps.spawn(process.execPath, [worker, jobFile], {
|
|
2052
|
-
cwd,
|
|
2658
|
+
cwd: launchCwd,
|
|
2053
2659
|
detached: true,
|
|
2054
2660
|
stdio: ["ignore", workerLogFd, workerLogFd],
|
|
2055
2661
|
windowsHide: true,
|
|
@@ -2061,13 +2667,29 @@ export function launchBackgroundAgent({
|
|
|
2061
2667
|
scope: "background-agent",
|
|
2062
2668
|
shell: false,
|
|
2063
2669
|
});
|
|
2670
|
+
keeperStartedAt = Date.now();
|
|
2671
|
+
keeperChild = _deps.spawn(process.execPath, [keeper, keeperJobFile], {
|
|
2672
|
+
cwd: launchCwd,
|
|
2673
|
+
detached: true,
|
|
2674
|
+
stdio: ["ignore", workerLogFd, workerLogFd],
|
|
2675
|
+
windowsHide: true,
|
|
2676
|
+
origin: "background-agent:keeper",
|
|
2677
|
+
policy: "allow",
|
|
2678
|
+
scope: "background-agent",
|
|
2679
|
+
shell: false,
|
|
2680
|
+
});
|
|
2064
2681
|
} catch (error) {
|
|
2065
|
-
|
|
2682
|
+
const ownedProcesses = [child, keeperChild, error?.spawnedProcess].filter(
|
|
2683
|
+
(processHandle, index, values) =>
|
|
2684
|
+
processHandle && values.indexOf(processHandle) === index,
|
|
2685
|
+
);
|
|
2686
|
+
if (ownedProcesses.length > 0) {
|
|
2066
2687
|
// The broker can fail a post-spawn sandbox handshake only after native
|
|
2067
2688
|
// spawn returned. It requests termination, but this synchronous caller
|
|
2068
|
-
// cannot prove
|
|
2069
|
-
// cleanup never races a process that may still hold cwd.
|
|
2070
|
-
const workerPid = Number(error
|
|
2689
|
+
// cannot prove every close fence has settled; retain state/jobs/worktree
|
|
2690
|
+
// so cleanup never races a process that may still hold cwd.
|
|
2691
|
+
const workerPid = Number(child?.pid || error?.spawnedProcess?.pid);
|
|
2692
|
+
const keeperPid = Number(keeperChild?.pid);
|
|
2071
2693
|
try {
|
|
2072
2694
|
const current = readBackgroundAgentState(id) || state;
|
|
2073
2695
|
writeBackgroundAgentState({
|
|
@@ -2075,6 +2697,9 @@ export function launchBackgroundAgent({
|
|
|
2075
2697
|
pid: Number.isInteger(workerPid) && workerPid > 0 ? workerPid : null,
|
|
2076
2698
|
workerPid:
|
|
2077
2699
|
Number.isInteger(workerPid) && workerPid > 0 ? workerPid : null,
|
|
2700
|
+
keeperPid:
|
|
2701
|
+
Number.isInteger(keeperPid) && keeperPid > 0 ? keeperPid : null,
|
|
2702
|
+
keeperStartedAt,
|
|
2078
2703
|
launchFinalizationUncertain: true,
|
|
2079
2704
|
});
|
|
2080
2705
|
} catch {
|
|
@@ -2083,8 +2708,8 @@ export function launchBackgroundAgent({
|
|
|
2083
2708
|
}
|
|
2084
2709
|
void settleUncertainBackgroundLaunch({
|
|
2085
2710
|
id,
|
|
2086
|
-
jobFile,
|
|
2087
|
-
|
|
2711
|
+
jobFiles: [jobFile, keeperJobFile],
|
|
2712
|
+
spawnedProcesses: ownedProcesses,
|
|
2088
2713
|
error,
|
|
2089
2714
|
});
|
|
2090
2715
|
throw new BackgroundWorkerStartedError(error, {
|
|
@@ -2093,7 +2718,9 @@ export function launchBackgroundAgent({
|
|
|
2093
2718
|
});
|
|
2094
2719
|
}
|
|
2095
2720
|
rmSync(jobFile, { force: true });
|
|
2721
|
+
rmSync(keeperJobFile, { force: true });
|
|
2096
2722
|
rmSync(statePath(id), { force: true });
|
|
2723
|
+
cleanupBackgroundAgentKeeperPipeDirectory(keeperPipePath);
|
|
2097
2724
|
throw error;
|
|
2098
2725
|
} finally {
|
|
2099
2726
|
if (Number.isInteger(workerLogFd)) {
|
|
@@ -2102,7 +2729,7 @@ export function launchBackgroundAgent({
|
|
|
2102
2729
|
} catch (error) {
|
|
2103
2730
|
// Once spawn returned a child, even fd-finalization errors are
|
|
2104
2731
|
// post-transfer: the worker may already be using the worktree.
|
|
2105
|
-
if (child) postSpawnError = error;
|
|
2732
|
+
if (child || keeperChild) postSpawnError = error;
|
|
2106
2733
|
}
|
|
2107
2734
|
}
|
|
2108
2735
|
}
|
|
@@ -2173,6 +2800,44 @@ export function launchBackgroundAgent({
|
|
|
2173
2800
|
}
|
|
2174
2801
|
});
|
|
2175
2802
|
}
|
|
2803
|
+
if (typeof keeperChild.on === "function") {
|
|
2804
|
+
keeperChild.on("error", (error) => {
|
|
2805
|
+
try {
|
|
2806
|
+
mutateBackgroundAgentState(id, (current) =>
|
|
2807
|
+
current?.status === "running" &&
|
|
2808
|
+
current.keeperGeneration === keeperGeneration
|
|
2809
|
+
? {
|
|
2810
|
+
...current,
|
|
2811
|
+
keeperStatus: "spawn-error",
|
|
2812
|
+
keeperError: error?.message || String(error),
|
|
2813
|
+
keeperEndedAt: Date.now(),
|
|
2814
|
+
}
|
|
2815
|
+
: null,
|
|
2816
|
+
);
|
|
2817
|
+
} catch {
|
|
2818
|
+
/* worker-side channel failure remains authoritative */
|
|
2819
|
+
}
|
|
2820
|
+
});
|
|
2821
|
+
keeperChild.on("exit", (code, signal) => {
|
|
2822
|
+
try {
|
|
2823
|
+
mutateBackgroundAgentState(id, (current) =>
|
|
2824
|
+
current?.status === "running" &&
|
|
2825
|
+
current.keeperGeneration === keeperGeneration
|
|
2826
|
+
? {
|
|
2827
|
+
...current,
|
|
2828
|
+
keeperStatus:
|
|
2829
|
+
current.keeperStatus === "closed" ? "closed" : "exited",
|
|
2830
|
+
keeperExitCode: Number.isInteger(code) ? code : null,
|
|
2831
|
+
keeperSignal: signal || null,
|
|
2832
|
+
keeperEndedAt: Date.now(),
|
|
2833
|
+
}
|
|
2834
|
+
: null,
|
|
2835
|
+
);
|
|
2836
|
+
} catch {
|
|
2837
|
+
/* worker-side channel failure remains authoritative */
|
|
2838
|
+
}
|
|
2839
|
+
});
|
|
2840
|
+
}
|
|
2176
2841
|
// Read child.pid before entering the state transaction. A platform wrapper
|
|
2177
2842
|
// (notably the Windows Job Object adapter) can return from spawn just as the
|
|
2178
2843
|
// real worker claims the state record. Finalize the wrapper pid against the
|
|
@@ -2180,6 +2845,7 @@ export function launchBackgroundAgent({
|
|
|
2180
2845
|
// worker already published (agentPid, pendingQuestion, recovery marker,
|
|
2181
2846
|
// turnCount, ...).
|
|
2182
2847
|
const spawnedWorkerPid = child.pid;
|
|
2848
|
+
const spawnedKeeperPid = keeperChild.pid;
|
|
2183
2849
|
const pidFinalization = mutateBackgroundAgentState(id, (current) => {
|
|
2184
2850
|
if (
|
|
2185
2851
|
!current ||
|
|
@@ -2195,6 +2861,17 @@ export function launchBackgroundAgent({
|
|
|
2195
2861
|
// platform child returned by spawn.
|
|
2196
2862
|
pid: current.pid ?? spawnedWorkerPid,
|
|
2197
2863
|
workerPid: current.workerPid ?? spawnedWorkerPid,
|
|
2864
|
+
workerWrapperPid:
|
|
2865
|
+
current.workerWrapperPid ?? child.sandboxWrapperPid ?? null,
|
|
2866
|
+
keeperPid: current.keeperPid ?? spawnedKeeperPid,
|
|
2867
|
+
keeperStartedAt: current.keeperStartedAt ?? keeperStartedAt,
|
|
2868
|
+
keeperWrapperPid:
|
|
2869
|
+
current.keeperWrapperPid ?? keeperChild.sandboxWrapperPid ?? null,
|
|
2870
|
+
keeperStatus:
|
|
2871
|
+
current.keeperStatus === "listening" ||
|
|
2872
|
+
current.keeperStatus === "ready"
|
|
2873
|
+
? current.keeperStatus
|
|
2874
|
+
: "starting",
|
|
2198
2875
|
launchFinalizationUncertain: false,
|
|
2199
2876
|
};
|
|
2200
2877
|
});
|
|
@@ -2205,6 +2882,7 @@ export function launchBackgroundAgent({
|
|
|
2205
2882
|
}
|
|
2206
2883
|
withPid = pidFinalization.state;
|
|
2207
2884
|
child.unref();
|
|
2885
|
+
keeperChild.unref();
|
|
2208
2886
|
} catch (error) {
|
|
2209
2887
|
postSpawnError ||= error;
|
|
2210
2888
|
}
|
|
@@ -2227,13 +2905,50 @@ export function readBackgroundAgentLog(id, options = {}) {
|
|
|
2227
2905
|
}
|
|
2228
2906
|
|
|
2229
2907
|
function recordedBackgroundProcessIdentities(state) {
|
|
2908
|
+
const hasRecordedAgentTree =
|
|
2909
|
+
Number(state?.agentPid) > 0 || Number(state?.agentRuntimePid) > 0;
|
|
2230
2910
|
const records = [
|
|
2231
|
-
{ role: "agent", pid: state?.agentPid, startedAt: state?.agentStartedAt },
|
|
2232
|
-
{ role: "worker", pid: state?.pid, startedAt: state?.startedAt },
|
|
2233
2911
|
{
|
|
2234
|
-
role: "
|
|
2912
|
+
role: "agent",
|
|
2913
|
+
pid: state?.agentPid,
|
|
2914
|
+
startedAt: state?.agentStartedAt,
|
|
2915
|
+
signalAsGroup: true,
|
|
2916
|
+
},
|
|
2917
|
+
{
|
|
2918
|
+
role: "agent-runtime",
|
|
2919
|
+
pid: state?.agentRuntimePid,
|
|
2920
|
+
startedAt: state?.agentRuntimeStartedAt,
|
|
2921
|
+
signalAsGroup: false,
|
|
2922
|
+
},
|
|
2923
|
+
{
|
|
2924
|
+
role: "worker",
|
|
2925
|
+
pid: state?.pid,
|
|
2926
|
+
startedAt: state?.startedAt,
|
|
2927
|
+
signalAsGroup: !hasRecordedAgentTree,
|
|
2928
|
+
},
|
|
2929
|
+
{
|
|
2930
|
+
role: "worker-published",
|
|
2235
2931
|
pid: state?.workerPid,
|
|
2236
2932
|
startedAt: state?.startedAt,
|
|
2933
|
+
signalAsGroup: !hasRecordedAgentTree,
|
|
2934
|
+
},
|
|
2935
|
+
{
|
|
2936
|
+
role: "worker-wrapper",
|
|
2937
|
+
pid: state?.workerWrapperPid,
|
|
2938
|
+
startedAt: state?.startedAt,
|
|
2939
|
+
signalAsGroup: true,
|
|
2940
|
+
},
|
|
2941
|
+
{
|
|
2942
|
+
role: "keeper",
|
|
2943
|
+
pid: state?.keeperPid,
|
|
2944
|
+
startedAt: state?.keeperStartedAt,
|
|
2945
|
+
signalAsGroup: false,
|
|
2946
|
+
},
|
|
2947
|
+
{
|
|
2948
|
+
role: "keeper-wrapper",
|
|
2949
|
+
pid: state?.keeperWrapperPid,
|
|
2950
|
+
startedAt: state?.keeperStartedAt,
|
|
2951
|
+
signalAsGroup: true,
|
|
2237
2952
|
},
|
|
2238
2953
|
];
|
|
2239
2954
|
const seen = new Set();
|
|
@@ -2249,7 +2964,9 @@ function recordedBackgroundProcessIdentities(state) {
|
|
|
2249
2964
|
function inspectRecordedBackgroundProcesses(state) {
|
|
2250
2965
|
return recordedBackgroundProcessIdentities(state).map((record) => ({
|
|
2251
2966
|
...record,
|
|
2252
|
-
identity: inspectDestructiveProcessIdentity(record.pid, record.startedAt
|
|
2967
|
+
identity: inspectDestructiveProcessIdentity(record.pid, record.startedAt, {
|
|
2968
|
+
processGroup: record.signalAsGroup,
|
|
2969
|
+
}),
|
|
2253
2970
|
}));
|
|
2254
2971
|
}
|
|
2255
2972
|
|
|
@@ -2259,7 +2976,7 @@ function recordedBackgroundProcessesAlive(state) {
|
|
|
2259
2976
|
);
|
|
2260
2977
|
}
|
|
2261
2978
|
|
|
2262
|
-
function
|
|
2979
|
+
export function isBackgroundProcessTreeExecutionAlive(pid, startedAt) {
|
|
2263
2980
|
const identity = inspectDestructiveProcessIdentity(pid, startedAt);
|
|
2264
2981
|
// `unverifiable` includes a vanished/zombie POSIX leader whose process group
|
|
2265
2982
|
// is still executable or whose group snapshot failed. Both must block
|
|
@@ -2267,13 +2984,25 @@ function isRecordedProcessTreeExecutionAlive(pid, startedAt) {
|
|
|
2267
2984
|
return ["match", "unverifiable", "self"].includes(identity.status);
|
|
2268
2985
|
}
|
|
2269
2986
|
|
|
2270
|
-
function signalBackgroundProcessTree(
|
|
2987
|
+
export function signalBackgroundProcessTree(
|
|
2988
|
+
pid,
|
|
2989
|
+
signal = "SIGTERM",
|
|
2990
|
+
{
|
|
2991
|
+
platform = process.platform,
|
|
2992
|
+
processGroup = true,
|
|
2993
|
+
allowDirectFallback = true,
|
|
2994
|
+
} = {},
|
|
2995
|
+
) {
|
|
2271
2996
|
const target = Number(pid);
|
|
2272
|
-
if (
|
|
2997
|
+
if (platform === "win32") {
|
|
2273
2998
|
const killed = runSupervisorCommand(
|
|
2274
2999
|
"taskkill",
|
|
2275
3000
|
["/PID", String(target), "/T", "/F"],
|
|
2276
|
-
{
|
|
3001
|
+
{
|
|
3002
|
+
windowsHide: true,
|
|
3003
|
+
encoding: "utf8",
|
|
3004
|
+
timeout: BACKGROUND_AGENT_KEEPER_TASKKILL_TIMEOUT_MS,
|
|
3005
|
+
},
|
|
2277
3006
|
"background-agent:stop-tree",
|
|
2278
3007
|
);
|
|
2279
3008
|
if (killed.error || (killed.status !== 0 && isProcessAlive(target))) {
|
|
@@ -2285,12 +3014,387 @@ function signalBackgroundProcessTree(pid, signal = "SIGTERM") {
|
|
|
2285
3014
|
}
|
|
2286
3015
|
return "process-tree";
|
|
2287
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
|
+
}
|
|
2288
3026
|
try {
|
|
2289
3027
|
_deps.kill(-target, signal);
|
|
2290
3028
|
return "process-group";
|
|
2291
|
-
} catch {
|
|
2292
|
-
|
|
2293
|
-
|
|
3029
|
+
} catch (groupError) {
|
|
3030
|
+
// A denied/invalid group signal can leave descendants executing even if a
|
|
3031
|
+
// direct leader signal would succeed. Only ESRCH proves that there is no
|
|
3032
|
+
// process group to terminate and permits the single-process fallback.
|
|
3033
|
+
if (groupError?.code !== "ESRCH" || allowDirectFallback !== true) {
|
|
3034
|
+
throw groupError;
|
|
3035
|
+
}
|
|
3036
|
+
try {
|
|
3037
|
+
_deps.kill(target, signal);
|
|
3038
|
+
return "process";
|
|
3039
|
+
} catch (error) {
|
|
3040
|
+
// The exact-identity probe and signal are necessarily separate syscalls.
|
|
3041
|
+
// A previously signalled group (or a naturally exiting child) can reap
|
|
3042
|
+
// this target between them. ESRCH therefore proves there is no process
|
|
3043
|
+
// left to terminate; permission and all other failures remain fatal.
|
|
3044
|
+
if (error?.code === "ESRCH") return "already-exited";
|
|
3045
|
+
throw error;
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
|
|
3050
|
+
function isDestructiveIdentityRetired(identity) {
|
|
3051
|
+
return ["dead", "reused"].includes(identity?.status);
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
function isRecoverableCreationTimeIdentityProbe(identity) {
|
|
3055
|
+
// A one-shot `ps -o lstart=` failure is observational: it says nothing about
|
|
3056
|
+
// whether the exact target is still present. Other unverifiable outcomes can
|
|
3057
|
+
// mean a missing durable anchor, a vanished group owner, PID reuse with a
|
|
3058
|
+
// surviving group, or an untrusted group snapshot; those remain immediately
|
|
3059
|
+
// fail-closed rather than being promoted into the resend path.
|
|
3060
|
+
return (
|
|
3061
|
+
identity?.status === "unverifiable" &&
|
|
3062
|
+
identity?.reason === "creation-time-probe-failed"
|
|
3063
|
+
);
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
function isRecoverableInitialDestructiveIdentityProbe(identity) {
|
|
3067
|
+
// These outcomes come only from read-only OS observations. Retrying them
|
|
3068
|
+
// cannot address a process, and the caller still requires two consecutive
|
|
3069
|
+
// fresh exact matches before it may issue the first signal.
|
|
3070
|
+
return (
|
|
3071
|
+
identity?.status === "unverifiable" &&
|
|
3072
|
+
["creation-time-probe-failed", "process-group-state-probe-failed"].includes(
|
|
3073
|
+
identity?.reason,
|
|
3074
|
+
)
|
|
3075
|
+
);
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
function createPosixProbeRetryBudget(deadline) {
|
|
3079
|
+
const configuredRetryWindow = Math.max(
|
|
3080
|
+
0,
|
|
3081
|
+
Number(_deps.processExitWaitDeadlineMs) || 0,
|
|
3082
|
+
);
|
|
3083
|
+
let remainingSleepBudget = Math.min(
|
|
3084
|
+
configuredRetryWindow,
|
|
3085
|
+
Math.max(0, deadline - _deps.posixPermissionRetryNowMs()),
|
|
3086
|
+
);
|
|
3087
|
+
let retryWaits = 0;
|
|
3088
|
+
|
|
3089
|
+
return {
|
|
3090
|
+
wait() {
|
|
3091
|
+
const remaining = Math.min(
|
|
3092
|
+
remainingSleepBudget,
|
|
3093
|
+
deadline - _deps.posixPermissionRetryNowMs(),
|
|
3094
|
+
);
|
|
3095
|
+
if (!(remaining > 0)) return false;
|
|
3096
|
+
const retryDelayMs = Math.min(
|
|
3097
|
+
remaining,
|
|
3098
|
+
10 * 2 ** Math.min(retryWaits, 4),
|
|
3099
|
+
);
|
|
3100
|
+
_deps.posixPermissionRetrySleep(retryDelayMs);
|
|
3101
|
+
remainingSleepBudget -= retryDelayMs;
|
|
3102
|
+
retryWaits += 1;
|
|
3103
|
+
return true;
|
|
3104
|
+
},
|
|
3105
|
+
hasRemaining() {
|
|
3106
|
+
return (
|
|
3107
|
+
remainingSleepBudget > 0 && _deps.posixPermissionRetryNowMs() < deadline
|
|
3108
|
+
);
|
|
3109
|
+
},
|
|
3110
|
+
};
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
/**
|
|
3114
|
+
* Retry an observational failure before the first POSIX signal. Unknown
|
|
3115
|
+
* identity never authorizes a signal: only two consecutive delayed fresh
|
|
3116
|
+
* matches do. A dead/reused target converges without signalling, while every
|
|
3117
|
+
* non-observational identity failure remains immediately fail-closed.
|
|
3118
|
+
*/
|
|
3119
|
+
function retryRecoverableInitialPosixIdentity({
|
|
3120
|
+
inspectIdentity,
|
|
3121
|
+
initialIdentity,
|
|
3122
|
+
deadline,
|
|
3123
|
+
}) {
|
|
3124
|
+
let lastIdentity = initialIdentity;
|
|
3125
|
+
let identityProbeRetries = 0;
|
|
3126
|
+
const retryBudget = createPosixProbeRetryBudget(deadline);
|
|
3127
|
+
const outcome = (status, retryStoppedBy) => ({
|
|
3128
|
+
status,
|
|
3129
|
+
lastIdentity,
|
|
3130
|
+
identityProbeRetries,
|
|
3131
|
+
retryStoppedBy,
|
|
3132
|
+
});
|
|
3133
|
+
|
|
3134
|
+
for (;;) {
|
|
3135
|
+
if (isDestructiveIdentityRetired(lastIdentity)) {
|
|
3136
|
+
return outcome("retired", "retired");
|
|
3137
|
+
}
|
|
3138
|
+
if (
|
|
3139
|
+
lastIdentity?.status !== "match" &&
|
|
3140
|
+
!isRecoverableInitialDestructiveIdentityProbe(lastIdentity)
|
|
3141
|
+
) {
|
|
3142
|
+
return outcome("failed", "preflight-identity");
|
|
3143
|
+
}
|
|
3144
|
+
if (!retryBudget.wait()) {
|
|
3145
|
+
return outcome("failed", "initial-identity-probe-deadline");
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
const identityBeforeDelay = lastIdentity;
|
|
3149
|
+
identityProbeRetries += 1;
|
|
3150
|
+
lastIdentity = inspectIdentity();
|
|
3151
|
+
|
|
3152
|
+
if (isDestructiveIdentityRetired(lastIdentity)) {
|
|
3153
|
+
return outcome("retired", "retired");
|
|
3154
|
+
}
|
|
3155
|
+
if (
|
|
3156
|
+
lastIdentity?.status !== "match" &&
|
|
3157
|
+
!isRecoverableInitialDestructiveIdentityProbe(lastIdentity)
|
|
3158
|
+
) {
|
|
3159
|
+
return outcome("failed", "preflight-identity");
|
|
3160
|
+
}
|
|
3161
|
+
if (
|
|
3162
|
+
identityBeforeDelay?.status === "match" &&
|
|
3163
|
+
lastIdentity?.status === "match"
|
|
3164
|
+
) {
|
|
3165
|
+
if (!retryBudget.hasRemaining()) {
|
|
3166
|
+
return outcome("failed", "initial-identity-probe-deadline");
|
|
3167
|
+
}
|
|
3168
|
+
return outcome("matched", "consecutive-match");
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3173
|
+
/**
|
|
3174
|
+
* Recover from a transient POSIX EPERM without ever signalling from unknown
|
|
3175
|
+
* identity evidence. Probe-only waits may bridge a transient creation-time
|
|
3176
|
+
* lookup failure, but a resend requires two consecutive fresh exact matches.
|
|
3177
|
+
*/
|
|
3178
|
+
function retryExactPosixSignalAfterPermissionError({
|
|
3179
|
+
inspectIdentity,
|
|
3180
|
+
sendSignal,
|
|
3181
|
+
deadline,
|
|
3182
|
+
initialCause,
|
|
3183
|
+
}) {
|
|
3184
|
+
let cause = initialCause;
|
|
3185
|
+
let lastIdentity = inspectIdentity();
|
|
3186
|
+
let signalResends = 0;
|
|
3187
|
+
let identityProbeRetries = 0;
|
|
3188
|
+
const retryBudget = createPosixProbeRetryBudget(deadline);
|
|
3189
|
+
|
|
3190
|
+
const outcome = (status, retryStoppedBy, extra = {}) => ({
|
|
3191
|
+
status,
|
|
3192
|
+
cause,
|
|
3193
|
+
lastIdentity,
|
|
3194
|
+
signalResends,
|
|
3195
|
+
identityProbeRetries,
|
|
3196
|
+
retryStoppedBy,
|
|
3197
|
+
...extra,
|
|
3198
|
+
});
|
|
3199
|
+
|
|
3200
|
+
for (;;) {
|
|
3201
|
+
if (isDestructiveIdentityRetired(lastIdentity)) {
|
|
3202
|
+
return outcome("retired", "retired");
|
|
3203
|
+
}
|
|
3204
|
+
if (
|
|
3205
|
+
lastIdentity?.status !== "match" &&
|
|
3206
|
+
!isRecoverableCreationTimeIdentityProbe(lastIdentity)
|
|
3207
|
+
) {
|
|
3208
|
+
return outcome("failed", "identity");
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3211
|
+
if (!retryBudget.wait()) {
|
|
3212
|
+
return outcome(
|
|
3213
|
+
"failed",
|
|
3214
|
+
lastIdentity?.status === "match"
|
|
3215
|
+
? "signal-retry-deadline"
|
|
3216
|
+
: "identity-probe-deadline",
|
|
3217
|
+
);
|
|
3218
|
+
}
|
|
3219
|
+
|
|
3220
|
+
const identityBeforeDelay = lastIdentity;
|
|
3221
|
+
identityProbeRetries += 1;
|
|
3222
|
+
lastIdentity = inspectIdentity();
|
|
3223
|
+
|
|
3224
|
+
if (isDestructiveIdentityRetired(lastIdentity)) {
|
|
3225
|
+
return outcome("retired", "retired");
|
|
3226
|
+
}
|
|
3227
|
+
if (
|
|
3228
|
+
lastIdentity?.status !== "match" &&
|
|
3229
|
+
!isRecoverableCreationTimeIdentityProbe(lastIdentity)
|
|
3230
|
+
) {
|
|
3231
|
+
return outcome("failed", "identity");
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
// A target that moved from unknown to match gets one more probe-only
|
|
3235
|
+
// interval. Only match -> fresh match authorizes the next signal syscall.
|
|
3236
|
+
if (
|
|
3237
|
+
identityBeforeDelay?.status !== "match" ||
|
|
3238
|
+
lastIdentity?.status !== "match"
|
|
3239
|
+
) {
|
|
3240
|
+
continue;
|
|
3241
|
+
}
|
|
3242
|
+
if (!retryBudget.hasRemaining()) {
|
|
3243
|
+
return outcome("failed", "signal-retry-deadline");
|
|
3244
|
+
}
|
|
3245
|
+
signalResends += 1;
|
|
3246
|
+
try {
|
|
3247
|
+
const signalResult = sendSignal();
|
|
3248
|
+
return outcome(
|
|
3249
|
+
signalResult === "already-exited" ? "retired" : "signalled",
|
|
3250
|
+
"completed",
|
|
3251
|
+
{ signalResult },
|
|
3252
|
+
);
|
|
3253
|
+
} catch (error) {
|
|
3254
|
+
cause = error;
|
|
3255
|
+
if (error?.code !== "EPERM") {
|
|
3256
|
+
return outcome("failed", "signal");
|
|
3257
|
+
}
|
|
3258
|
+
// The failed resend has no authority value. Start the next decision from
|
|
3259
|
+
// a fresh probe, then require another delayed fresh match before signalling.
|
|
3260
|
+
lastIdentity = inspectIdentity();
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
|
|
3265
|
+
function signalExactBackgroundProcessTree(
|
|
3266
|
+
pid,
|
|
3267
|
+
signal,
|
|
3268
|
+
{
|
|
3269
|
+
platform = process.platform,
|
|
3270
|
+
expectedStartedAt,
|
|
3271
|
+
processGroup = true,
|
|
3272
|
+
allowDirectFallback = false,
|
|
3273
|
+
} = {},
|
|
3274
|
+
) {
|
|
3275
|
+
const target = Number(pid);
|
|
3276
|
+
const mode = processGroup === true ? "process-group" : "direct";
|
|
3277
|
+
const posixRetryDeadline =
|
|
3278
|
+
_deps.posixPermissionRetryNowMs() +
|
|
3279
|
+
Math.max(0, Number(_deps.processExitWaitDeadlineMs) || 0);
|
|
3280
|
+
let initialSignalAttempts = 0;
|
|
3281
|
+
let signalResends = 0;
|
|
3282
|
+
let identityProbeRetries = 0;
|
|
3283
|
+
let lastIdentity = null;
|
|
3284
|
+
let retryStoppedBy = "preflight";
|
|
3285
|
+
const inspectIdentity = () =>
|
|
3286
|
+
inspectDestructiveProcessIdentity(target, expectedStartedAt, {
|
|
3287
|
+
processGroup,
|
|
3288
|
+
});
|
|
3289
|
+
const failure = (cause) => {
|
|
3290
|
+
const reason = cause?.message || String(cause);
|
|
3291
|
+
const error = new Error(
|
|
3292
|
+
`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})`,
|
|
3293
|
+
{ cause },
|
|
3294
|
+
);
|
|
3295
|
+
error.code = "ERR_BACKGROUND_AGENT_CHILD_TREE_STOP_FAILED";
|
|
3296
|
+
return error;
|
|
3297
|
+
};
|
|
3298
|
+
|
|
3299
|
+
// Unlike the legacy tree helper, the keeper's destructive path owns a
|
|
3300
|
+
// persisted creation-time anchor. Re-authorize the initial signal from a
|
|
3301
|
+
// fresh exact snapshot so a stale/reused PID can never address a stranger.
|
|
3302
|
+
lastIdentity = inspectIdentity();
|
|
3303
|
+
if (isDestructiveIdentityRetired(lastIdentity)) {
|
|
3304
|
+
return { alreadyRetired: true, signalledAsGroup: processGroup === true };
|
|
3305
|
+
}
|
|
3306
|
+
if (lastIdentity.status !== "match") {
|
|
3307
|
+
if (
|
|
3308
|
+
platform === "win32" ||
|
|
3309
|
+
!isRecoverableInitialDestructiveIdentityProbe(lastIdentity)
|
|
3310
|
+
) {
|
|
3311
|
+
retryStoppedBy = "preflight-identity";
|
|
3312
|
+
throw failure(
|
|
3313
|
+
new Error(
|
|
3314
|
+
`destructive process identity is ${lastIdentity.status}: ${lastIdentity.reason || "unknown"}`,
|
|
3315
|
+
),
|
|
3316
|
+
);
|
|
3317
|
+
}
|
|
3318
|
+
const retry = retryRecoverableInitialPosixIdentity({
|
|
3319
|
+
inspectIdentity,
|
|
3320
|
+
initialIdentity: lastIdentity,
|
|
3321
|
+
deadline: posixRetryDeadline,
|
|
3322
|
+
});
|
|
3323
|
+
identityProbeRetries += retry.identityProbeRetries;
|
|
3324
|
+
lastIdentity = retry.lastIdentity;
|
|
3325
|
+
retryStoppedBy = retry.retryStoppedBy;
|
|
3326
|
+
if (retry.status === "retired") {
|
|
3327
|
+
return { alreadyRetired: true, signalledAsGroup: processGroup === true };
|
|
3328
|
+
}
|
|
3329
|
+
if (retry.status !== "matched") {
|
|
3330
|
+
throw failure(
|
|
3331
|
+
new Error(
|
|
3332
|
+
`destructive process identity is ${lastIdentity.status}: ${lastIdentity.reason || "unknown"}`,
|
|
3333
|
+
),
|
|
3334
|
+
);
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
|
|
3338
|
+
const sendSignal = () =>
|
|
3339
|
+
signalBackgroundProcessTree(target, signal, {
|
|
3340
|
+
platform,
|
|
3341
|
+
processGroup,
|
|
3342
|
+
allowDirectFallback,
|
|
3343
|
+
});
|
|
3344
|
+
|
|
3345
|
+
retryStoppedBy = "initial-signal";
|
|
3346
|
+
try {
|
|
3347
|
+
initialSignalAttempts += 1;
|
|
3348
|
+
const result = sendSignal();
|
|
3349
|
+
return {
|
|
3350
|
+
alreadyRetired: result === "already-exited",
|
|
3351
|
+
signalledAsGroup: result === "process-group",
|
|
3352
|
+
};
|
|
3353
|
+
} catch (initialCause) {
|
|
3354
|
+
let cause = initialCause;
|
|
3355
|
+
if (platform !== "win32" && cause?.code === "EPERM") {
|
|
3356
|
+
const retry = retryExactPosixSignalAfterPermissionError({
|
|
3357
|
+
inspectIdentity,
|
|
3358
|
+
sendSignal,
|
|
3359
|
+
deadline: posixRetryDeadline,
|
|
3360
|
+
initialCause: cause,
|
|
3361
|
+
});
|
|
3362
|
+
signalResends = retry.signalResends;
|
|
3363
|
+
identityProbeRetries += retry.identityProbeRetries;
|
|
3364
|
+
lastIdentity = retry.lastIdentity;
|
|
3365
|
+
retryStoppedBy = retry.retryStoppedBy;
|
|
3366
|
+
cause = retry.cause;
|
|
3367
|
+
if (retry.status === "retired") {
|
|
3368
|
+
return {
|
|
3369
|
+
alreadyRetired: true,
|
|
3370
|
+
signalledAsGroup: processGroup === true,
|
|
3371
|
+
};
|
|
3372
|
+
}
|
|
3373
|
+
if (retry.status === "signalled") {
|
|
3374
|
+
return {
|
|
3375
|
+
alreadyRetired: retry.signalResult === "already-exited",
|
|
3376
|
+
signalledAsGroup: retry.signalResult === "process-group",
|
|
3377
|
+
};
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
if (
|
|
3381
|
+
processGroup === true &&
|
|
3382
|
+
allowDirectFallback !== true &&
|
|
3383
|
+
cause?.code === "ESRCH"
|
|
3384
|
+
) {
|
|
3385
|
+
// A group may disappear between the exact preflight and signal. It is
|
|
3386
|
+
// safe to accept only a fresh dead/reused result; a still-live leader
|
|
3387
|
+
// is never converted to a direct kill on the keeper path.
|
|
3388
|
+
lastIdentity = inspectIdentity();
|
|
3389
|
+
if (isDestructiveIdentityRetired(lastIdentity)) {
|
|
3390
|
+
return {
|
|
3391
|
+
alreadyRetired: true,
|
|
3392
|
+
signalledAsGroup: true,
|
|
3393
|
+
};
|
|
3394
|
+
}
|
|
3395
|
+
retryStoppedBy = "identity";
|
|
3396
|
+
}
|
|
3397
|
+
throw failure(cause);
|
|
2294
3398
|
}
|
|
2295
3399
|
}
|
|
2296
3400
|
|
|
@@ -2355,6 +3459,7 @@ function waitForSignalledProcessesToExit(
|
|
|
2355
3459
|
const identity = inspectDestructiveProcessIdentity(
|
|
2356
3460
|
target.pid,
|
|
2357
3461
|
target.startedAt,
|
|
3462
|
+
{ processGroup: target.signalledAsGroup },
|
|
2358
3463
|
);
|
|
2359
3464
|
return ["match", "unverifiable", "self"].includes(identity.status);
|
|
2360
3465
|
});
|
|
@@ -2362,7 +3467,7 @@ function waitForSignalledProcessesToExit(
|
|
|
2362
3467
|
|
|
2363
3468
|
function recoverStoppedInteractions(state) {
|
|
2364
3469
|
if (!state?.sessionId) return { state, recovery: null };
|
|
2365
|
-
const recovery = withSessionHostRecoveryLease(state.sessionId, () =>
|
|
3470
|
+
const recovery = _deps.withSessionHostRecoveryLease(state.sessionId, () =>
|
|
2366
3471
|
rejectPendingBackgroundInteractions(state.sessionId, state.id, {
|
|
2367
3472
|
fallbackRequest: backgroundInteractionFallback(state),
|
|
2368
3473
|
code: "INTERACTION_STOPPED",
|
|
@@ -2390,7 +3495,13 @@ export function stopBackgroundAgent(id) {
|
|
|
2390
3495
|
if (!state) throw new Error(`Background agent not found: ${id}`);
|
|
2391
3496
|
const staleHeartbeatOwner =
|
|
2392
3497
|
state.status === "lost" && state.lostReason === "heartbeat-stale";
|
|
2393
|
-
|
|
3498
|
+
const preexistingStopCleanupRepair =
|
|
3499
|
+
hasBackgroundAgentStopFenceWithoutCleanupProof(state);
|
|
3500
|
+
if (
|
|
3501
|
+
state.status !== "running" &&
|
|
3502
|
+
!staleHeartbeatOwner &&
|
|
3503
|
+
!preexistingStopCleanupRepair
|
|
3504
|
+
) {
|
|
2394
3505
|
// Gap 2: stopping an already-lost session still reaps a leaked agent
|
|
2395
3506
|
// child recorded before the worker died (identity-guarded inside).
|
|
2396
3507
|
if (state.status === "lost") {
|
|
@@ -2415,7 +3526,8 @@ export function stopBackgroundAgent(id) {
|
|
|
2415
3526
|
(current.status !== "running" &&
|
|
2416
3527
|
!(
|
|
2417
3528
|
current.status === "lost" && current.lostReason === "heartbeat-stale"
|
|
2418
|
-
)
|
|
3529
|
+
) &&
|
|
3530
|
+
!hasBackgroundAgentStopFenceWithoutCleanupProof(current))
|
|
2419
3531
|
) {
|
|
2420
3532
|
return null;
|
|
2421
3533
|
}
|
|
@@ -2434,6 +3546,10 @@ export function stopBackgroundAgent(id) {
|
|
|
2434
3546
|
return { ...fence.state, stopped: false };
|
|
2435
3547
|
}
|
|
2436
3548
|
state = fence.state;
|
|
3549
|
+
const repairingUnconfirmedStop =
|
|
3550
|
+
preexistingStopCleanupRepair ||
|
|
3551
|
+
hasBackgroundAgentStopFenceWithoutCleanupProof(fence.previous) ||
|
|
3552
|
+
isBackgroundAgentStopCleanupUnconfirmed(state);
|
|
2437
3553
|
if (state.launchFinalizationUncertain === true || state.turnLaunchIntent) {
|
|
2438
3554
|
// The worker has durably announced that native spawn is prepared or that
|
|
2439
3555
|
// post-spawn pid persistence is unresolved. Killing the worker from this
|
|
@@ -2472,6 +3588,7 @@ export function stopBackgroundAgent(id) {
|
|
|
2472
3588
|
let postKill = state;
|
|
2473
3589
|
let workerPidReused = false;
|
|
2474
3590
|
const signalledPids = new Set();
|
|
3591
|
+
let activeTerminationContext = null;
|
|
2475
3592
|
// A worker may publish a just-spawned child identity while the stop fence is
|
|
2476
3593
|
// being observed. Re-read once after the first termination round so that a
|
|
2477
3594
|
// newly durable, exact-owned agent tree is also terminated before recovery.
|
|
@@ -2493,8 +3610,19 @@ export function stopBackgroundAgent(id) {
|
|
|
2493
3610
|
}
|
|
2494
3611
|
const primaryWorker =
|
|
2495
3612
|
inspected.find(({ role }) => role === "worker") ||
|
|
3613
|
+
inspected.find(({ role }) => role === "worker-published") ||
|
|
2496
3614
|
inspected.find(({ role }) => role === "worker-wrapper");
|
|
2497
|
-
if (
|
|
3615
|
+
if (
|
|
3616
|
+
primaryWorker?.identity.status === "reused" &&
|
|
3617
|
+
!signalledPids.has(primaryWorker.pid)
|
|
3618
|
+
) {
|
|
3619
|
+
// A fresh stop must refuse a worker pid that was already reused at the
|
|
3620
|
+
// first strict probe. If the exact worker matched and was signalled in an
|
|
3621
|
+
// earlier round, however, observing that numeric pid reused afterward is
|
|
3622
|
+
// evidence that the owned process retired, not grounds to discard the
|
|
3623
|
+
// durable cleanup proof.
|
|
3624
|
+
workerPidReused = true;
|
|
3625
|
+
}
|
|
2498
3626
|
const targets = inspected.filter(
|
|
2499
3627
|
({ pid, identity }) =>
|
|
2500
3628
|
identity.status === "match" && !signalledPids.has(pid),
|
|
@@ -2502,14 +3630,122 @@ export function stopBackgroundAgent(id) {
|
|
|
2502
3630
|
if (targets.length === 0) break;
|
|
2503
3631
|
try {
|
|
2504
3632
|
// The child is detached into its own group and therefore gets its own
|
|
2505
|
-
// exact-identity signal before the worker tree is terminated.
|
|
3633
|
+
// exact-identity signal before the worker tree is terminated. The
|
|
3634
|
+
// turn's detached agent root owns its tool descendants; the worker and
|
|
3635
|
+
// independent keeper do not. Signal those single processes directly so
|
|
3636
|
+
// a rapidly recycled pid cannot address an unrelated POSIX group.
|
|
2506
3637
|
for (const target of targets) {
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
3638
|
+
let permissionRetryDeadline = 0;
|
|
3639
|
+
const inspectIdentity = () =>
|
|
3640
|
+
inspectDestructiveProcessIdentity(target.pid, target.startedAt, {
|
|
3641
|
+
processGroup: target.signalAsGroup,
|
|
3642
|
+
});
|
|
3643
|
+
const sendInitialSignal = () =>
|
|
3644
|
+
signalBackgroundProcessTree(target.pid, "SIGTERM", {
|
|
3645
|
+
processGroup: target.signalAsGroup,
|
|
3646
|
+
// Preserve the pre-existing ESRCH behavior. EPERM itself never
|
|
3647
|
+
// enters this fallback because the tree primitive rethrows it.
|
|
3648
|
+
allowDirectFallback: true,
|
|
3649
|
+
});
|
|
3650
|
+
const resendSignal = () =>
|
|
3651
|
+
signalBackgroundProcessTree(target.pid, "SIGTERM", {
|
|
3652
|
+
processGroup: target.signalAsGroup,
|
|
3653
|
+
// After EPERM, every resend is a newly authorized action. A group
|
|
3654
|
+
// ESRCH must be resolved by fresh identity evidence, never by
|
|
3655
|
+
// changing that action into a direct leader signal.
|
|
3656
|
+
allowDirectFallback: false,
|
|
3657
|
+
});
|
|
3658
|
+
activeTerminationContext = {
|
|
3659
|
+
role: target.role,
|
|
3660
|
+
pid: target.pid,
|
|
3661
|
+
mode: target.signalAsGroup ? "process-group" : "direct",
|
|
3662
|
+
signalResends: 0,
|
|
3663
|
+
identityProbeRetries: 0,
|
|
3664
|
+
};
|
|
3665
|
+
try {
|
|
3666
|
+
const preflightIdentity = inspectIdentity();
|
|
3667
|
+
activeTerminationContext.lastIdentityStatus =
|
|
3668
|
+
preflightIdentity.status;
|
|
3669
|
+
activeTerminationContext.lastIdentityReason =
|
|
3670
|
+
preflightIdentity.reason || null;
|
|
3671
|
+
if (isDestructiveIdentityRetired(preflightIdentity)) {
|
|
3672
|
+
if (
|
|
3673
|
+
preflightIdentity.status === "reused" &&
|
|
3674
|
+
primaryWorker?.pid === target.pid &&
|
|
3675
|
+
!signalledPids.has(target.pid)
|
|
3676
|
+
) {
|
|
3677
|
+
workerPidReused = true;
|
|
3678
|
+
}
|
|
3679
|
+
target.retiredBeforeWait = true;
|
|
3680
|
+
signalledPids.add(target.pid);
|
|
3681
|
+
continue;
|
|
3682
|
+
}
|
|
3683
|
+
if (preflightIdentity.status !== "match") {
|
|
3684
|
+
activeTerminationContext.retryStoppedBy = "preflight-identity";
|
|
3685
|
+
throw new Error(
|
|
3686
|
+
`destructive process identity is ${preflightIdentity.status}: ${preflightIdentity.reason || "unknown"}`,
|
|
3687
|
+
);
|
|
3688
|
+
}
|
|
3689
|
+
permissionRetryDeadline =
|
|
3690
|
+
_deps.posixPermissionRetryNowMs() +
|
|
3691
|
+
Math.max(0, Number(_deps.processExitWaitDeadlineMs) || 0);
|
|
3692
|
+
target.signalledAsGroup = sendInitialSignal() === "process-group";
|
|
3693
|
+
signalledPids.add(target.pid);
|
|
3694
|
+
} catch (error) {
|
|
3695
|
+
if (process.platform !== "win32" && error?.code === "EPERM") {
|
|
3696
|
+
const retry = retryExactPosixSignalAfterPermissionError({
|
|
3697
|
+
inspectIdentity,
|
|
3698
|
+
sendSignal: resendSignal,
|
|
3699
|
+
deadline: permissionRetryDeadline,
|
|
3700
|
+
initialCause: error,
|
|
3701
|
+
});
|
|
3702
|
+
activeTerminationContext.signalResends = retry.signalResends;
|
|
3703
|
+
activeTerminationContext.identityProbeRetries =
|
|
3704
|
+
retry.identityProbeRetries;
|
|
3705
|
+
activeTerminationContext.lastIdentityStatus =
|
|
3706
|
+
retry.lastIdentity?.status;
|
|
3707
|
+
activeTerminationContext.lastIdentityReason =
|
|
3708
|
+
retry.lastIdentity?.reason || null;
|
|
3709
|
+
activeTerminationContext.retryStoppedBy = retry.retryStoppedBy;
|
|
3710
|
+
if (retry.status === "retired") {
|
|
3711
|
+
target.signalledAsGroup = retry.signalResult
|
|
3712
|
+
? retry.signalResult === "process-group"
|
|
3713
|
+
: target.signalAsGroup === true;
|
|
3714
|
+
signalledPids.add(target.pid);
|
|
3715
|
+
continue;
|
|
3716
|
+
}
|
|
3717
|
+
if (retry.status === "signalled") {
|
|
3718
|
+
target.signalledAsGroup = retry.signalResult === "process-group";
|
|
3719
|
+
signalledPids.add(target.pid);
|
|
3720
|
+
continue;
|
|
3721
|
+
}
|
|
3722
|
+
if (
|
|
3723
|
+
target.signalAsGroup === true &&
|
|
3724
|
+
retry.cause?.code === "ESRCH"
|
|
3725
|
+
) {
|
|
3726
|
+
const refreshedIdentity = inspectIdentity();
|
|
3727
|
+
activeTerminationContext.lastIdentityStatus =
|
|
3728
|
+
refreshedIdentity.status;
|
|
3729
|
+
activeTerminationContext.lastIdentityReason =
|
|
3730
|
+
refreshedIdentity.reason || null;
|
|
3731
|
+
activeTerminationContext.retryStoppedBy = "identity";
|
|
3732
|
+
if (isDestructiveIdentityRetired(refreshedIdentity)) {
|
|
3733
|
+
target.signalledAsGroup = true;
|
|
3734
|
+
signalledPids.add(target.pid);
|
|
3735
|
+
continue;
|
|
3736
|
+
}
|
|
3737
|
+
}
|
|
3738
|
+
throw retry.cause;
|
|
3739
|
+
}
|
|
3740
|
+
throw error;
|
|
3741
|
+
}
|
|
2511
3742
|
}
|
|
2512
3743
|
} catch (error) {
|
|
3744
|
+
const failureMessage = `Failed to stop background agent ${id}${
|
|
3745
|
+
activeTerminationContext
|
|
3746
|
+
? ` (${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"})`
|
|
3747
|
+
: ""
|
|
3748
|
+
}: ${error.message}`;
|
|
2513
3749
|
try {
|
|
2514
3750
|
mutateBackgroundAgentState(id, (current) =>
|
|
2515
3751
|
current
|
|
@@ -2518,18 +3754,18 @@ export function stopBackgroundAgent(id) {
|
|
|
2518
3754
|
phase: "stop_failed",
|
|
2519
3755
|
stopPending: true,
|
|
2520
3756
|
stopPendingReason: "process-termination",
|
|
2521
|
-
stopError:
|
|
3757
|
+
stopError: failureMessage,
|
|
2522
3758
|
}
|
|
2523
3759
|
: null,
|
|
2524
3760
|
);
|
|
2525
3761
|
} catch {
|
|
2526
3762
|
/* the durable stopRequestedAt fence already blocks new turns */
|
|
2527
3763
|
}
|
|
2528
|
-
throw new Error(
|
|
2529
|
-
`Failed to stop background agent ${id}: ${error.message}`,
|
|
2530
|
-
);
|
|
3764
|
+
throw new Error(failureMessage);
|
|
2531
3765
|
}
|
|
2532
|
-
const stillAlive = waitForSignalledProcessesToExit(
|
|
3766
|
+
const stillAlive = waitForSignalledProcessesToExit(
|
|
3767
|
+
targets.filter((target) => target.retiredBeforeWait !== true),
|
|
3768
|
+
);
|
|
2533
3769
|
postKill = readBackgroundAgentState(id) || postKill;
|
|
2534
3770
|
if (stillAlive.length > 0) {
|
|
2535
3771
|
return persistStopPending(postKill, "process-exit", {
|
|
@@ -2544,7 +3780,7 @@ export function stopBackgroundAgent(id) {
|
|
|
2544
3780
|
return persistStopPending(postKill, "process-exit");
|
|
2545
3781
|
}
|
|
2546
3782
|
|
|
2547
|
-
if (workerPidReused) {
|
|
3783
|
+
if (workerPidReused && !repairingUnconfirmedStop) {
|
|
2548
3784
|
const lostMutation = mutateBackgroundAgentState(id, (current) =>
|
|
2549
3785
|
current
|
|
2550
3786
|
? {
|
|
@@ -2602,6 +3838,12 @@ export function stopBackgroundAgent(id) {
|
|
|
2602
3838
|
...current,
|
|
2603
3839
|
status: "stopped",
|
|
2604
3840
|
endedAt: current.endedAt || Date.now(),
|
|
3841
|
+
// `endedAt` belongs to the worker/session lifecycle and may predate
|
|
3842
|
+
// retirement of a detached child. Publish a distinct durable proof
|
|
3843
|
+
// only after every exact-owned process identity has been observed
|
|
3844
|
+
// non-executable above.
|
|
3845
|
+
stopCleanupConfirmedAt:
|
|
3846
|
+
backgroundAgentStopCleanupConfirmationTimestamp(current),
|
|
2605
3847
|
stoppedByUser: true,
|
|
2606
3848
|
lostReason: null,
|
|
2607
3849
|
phase: null,
|
|
@@ -2624,17 +3866,47 @@ function activeWorktreeProcesses(state) {
|
|
|
2624
3866
|
if (
|
|
2625
3867
|
Number.isInteger(Number(state?.pid)) &&
|
|
2626
3868
|
Number(state.pid) > 0 &&
|
|
2627
|
-
|
|
3869
|
+
isBackgroundProcessTreeExecutionAlive(state.pid, state.startedAt)
|
|
2628
3870
|
) {
|
|
2629
3871
|
active.push("worker");
|
|
2630
3872
|
}
|
|
2631
3873
|
if (
|
|
2632
3874
|
Number.isInteger(Number(state?.agentPid)) &&
|
|
2633
3875
|
Number(state.agentPid) > 0 &&
|
|
2634
|
-
|
|
3876
|
+
isBackgroundProcessTreeExecutionAlive(state.agentPid, state.agentStartedAt)
|
|
2635
3877
|
) {
|
|
2636
3878
|
active.push("agent");
|
|
2637
3879
|
}
|
|
3880
|
+
if (
|
|
3881
|
+
Number.isInteger(Number(state?.keeperPid)) &&
|
|
3882
|
+
Number(state.keeperPid) > 0 &&
|
|
3883
|
+
isBackgroundProcessTreeExecutionAlive(
|
|
3884
|
+
state.keeperPid,
|
|
3885
|
+
state.keeperStartedAt,
|
|
3886
|
+
)
|
|
3887
|
+
) {
|
|
3888
|
+
active.push("keeper");
|
|
3889
|
+
}
|
|
3890
|
+
if (
|
|
3891
|
+
Number.isInteger(Number(state?.workerWrapperPid)) &&
|
|
3892
|
+
Number(state.workerWrapperPid) > 0 &&
|
|
3893
|
+
isBackgroundProcessTreeExecutionAlive(
|
|
3894
|
+
state.workerWrapperPid,
|
|
3895
|
+
state.startedAt,
|
|
3896
|
+
)
|
|
3897
|
+
) {
|
|
3898
|
+
active.push("worker-wrapper");
|
|
3899
|
+
}
|
|
3900
|
+
if (
|
|
3901
|
+
Number.isInteger(Number(state?.keeperWrapperPid)) &&
|
|
3902
|
+
Number(state.keeperWrapperPid) > 0 &&
|
|
3903
|
+
isBackgroundProcessTreeExecutionAlive(
|
|
3904
|
+
state.keeperWrapperPid,
|
|
3905
|
+
state.keeperStartedAt,
|
|
3906
|
+
)
|
|
3907
|
+
) {
|
|
3908
|
+
active.push("keeper-wrapper");
|
|
3909
|
+
}
|
|
2638
3910
|
return active;
|
|
2639
3911
|
}
|
|
2640
3912
|
|