chainlesschain 0.164.0 → 0.165.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/README.md +248 -2
  2. package/package.json +7 -4
  3. package/src/assets/web-panel/.build-hash +1 -1
  4. package/src/assets/web-panel/assets/{AIOps-D2MEn7fI.js → AIOps-lS1R0Cza.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-lCW_4Gf0.js → ActionButton-BmlgBPsj.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-Bs7i-nsS.js → Analytics-CNKczv1S.js} +3 -3
  7. package/src/assets/web-panel/assets/{AppLayout-CK7B6S_j.js → AppLayout-BuZGJARQ.js} +5 -5
  8. package/src/assets/web-panel/assets/{Artifacts-C8ILjpyo.js → Artifacts-Dm73Sl-O.js} +1 -1
  9. package/src/assets/web-panel/assets/{Audit-Bb9cNaWc.js → Audit-BGMW7A5e.js} +1 -1
  10. package/src/assets/web-panel/assets/{BackgroundAgents-sC9ooXIf.js → BackgroundAgents-BQ6M54JR.js} +1 -1
  11. package/src/assets/web-panel/assets/{Backup-D-DrsplT.js → Backup-DBzGP0bR.js} +1 -1
  12. package/src/assets/web-panel/assets/{BaseInput-uCNZpwSK.js → BaseInput-D1mLCoOV.js} +1 -1
  13. package/src/assets/web-panel/assets/{Chat-CsDK1uE9.js → Chat-BUfcN7p8.js} +6 -6
  14. package/src/assets/web-panel/assets/{ChatBubbleRenderer-OqSDt50w.js → ChatBubbleRenderer-CY5m8ZHg.js} +1 -1
  15. package/src/assets/web-panel/assets/{Checkbox-JhTXM1-K.js → Checkbox-Du0Yg5Pi.js} +1 -1
  16. package/src/assets/web-panel/assets/{Codegen-BbzZLnyD.js → Codegen-B2RXziHW.js} +1 -1
  17. package/src/assets/web-panel/assets/{Col-D4VW4m4j.js → Col-DWhekOrR.js} +1 -1
  18. package/src/assets/web-panel/assets/{Community-BXrYawxV.js → Community-HQhrkon4.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compact-DCihGy1u.js → Compact-izHNb4uz.js} +1 -1
  20. package/src/assets/web-panel/assets/{Compliance-DlyZ-lk8.js → Compliance-BDwLy41-.js} +1 -1
  21. package/src/assets/web-panel/assets/{Cowork-DvGZrhbO.js → Cowork-DFJhLa7K.js} +3 -3
  22. package/src/assets/web-panel/assets/{Cron-DUncZi8h.js → Cron-DYY9RG7-.js} +2 -2
  23. package/src/assets/web-panel/assets/{Crosschain-C_hLuUcm.js → Crosschain-rr7tJTtv.js} +1 -1
  24. package/src/assets/web-panel/assets/{DID-b6YSUqxr.js → DID-D2sfHRLi.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dashboard-uvM15Cfb.js → Dashboard-q_eGdhha.js} +2 -2
  26. package/src/assets/web-panel/assets/{Dropdown-D1-Nd4qY.js → Dropdown-Cufrh2Bz.js} +1 -1
  27. package/src/assets/web-panel/assets/{EmailListRenderer-8ApvL6Zc.js → EmailListRenderer-B-kdv10f.js} +1 -1
  28. package/src/assets/web-panel/assets/{FamilyGuardDashboard-LYJt8811.js → FamilyGuardDashboard-D21XjgfG.js} +1 -1
  29. package/src/assets/web-panel/assets/{Federation-BbI3HGpv.js → Federation-CAwRTLZH.js} +1 -1
  30. package/src/assets/web-panel/assets/{FormItemContext-DSoEKuFu.js → FormItemContext-vx3daNDF.js} +1 -1
  31. package/src/assets/web-panel/assets/{GenericCardRenderer-B0S9OWd5.js → GenericCardRenderer-CRWY2wU4.js} +1 -1
  32. package/src/assets/web-panel/assets/{Git-DIs9Zhf3.js → Git-Q6SwauyD.js} +2 -2
  33. package/src/assets/web-panel/assets/{Governance-jzZXrxcl.js → Governance-BJ1q4IQZ.js} +1 -1
  34. package/src/assets/web-panel/assets/{Inference-JUPjf2ZI.js → Inference-CSWjROgD.js} +1 -1
  35. package/src/assets/web-panel/assets/{KnowledgeGraph-DxnfJvlP.js → KnowledgeGraph-fzuQsWuY.js} +1 -1
  36. package/src/assets/web-panel/assets/{Logs-iXiz8BEE.js → Logs-DVF8Mnns.js} +2 -2
  37. package/src/assets/web-panel/assets/MarkdownRenderer-BLif4QSg.js +1 -0
  38. package/src/assets/web-panel/assets/{Marketplace-Y3Xch312.js → Marketplace-BQP7LOA4.js} +1 -1
  39. package/src/assets/web-panel/assets/{McpTools-D7kGGbO7.js → McpTools-BNlJCmNc.js} +4 -4
  40. package/src/assets/web-panel/assets/{Memory-Do_hVxzh.js → Memory-DLLg5WQ4.js} +2 -2
  41. package/src/assets/web-panel/assets/{MobileBridge-CtrR0Tas.js → MobileBridge-CR2skNAF.js} +1 -1
  42. package/src/assets/web-panel/assets/MobileProjects-BxVavObl.js +1 -0
  43. package/src/assets/web-panel/assets/{Mtc-qo5QCTGd.js → Mtc-B17pgqMI.js} +4 -4
  44. package/src/assets/web-panel/assets/{MtcAudit-cq5mnjYj.js → MtcAudit-4UmV5cIL.js} +2 -2
  45. package/src/assets/web-panel/assets/{Multisig-Bq0MAQHw.js → Multisig-DoMEXEcU.js} +3 -3
  46. package/src/assets/web-panel/assets/{NLProgramming-DdWE6Vi2.js → NLProgramming-h9DMXFX4.js} +1 -1
  47. package/src/assets/web-panel/assets/{Notes-BxMBFcIp.js → Notes-Cihn-xX8.js} +3 -3
  48. package/src/assets/web-panel/assets/{NotificationSettings-RTrAcbUe.js → NotificationSettings-BE6ZGa3R.js} +1 -1
  49. package/src/assets/web-panel/assets/{OrderTableRenderer-DJbpGqzT.js → OrderTableRenderer-CnwojoSI.js} +1 -1
  50. package/src/assets/web-panel/assets/{Organization-SQzIPdqC.js → Organization-B2vpwmjb.js} +4 -4
  51. package/src/assets/web-panel/assets/{Overflow-02yECh22.js → Overflow-C4z28R0-.js} +1 -1
  52. package/src/assets/web-panel/assets/{P2P-BNg4pNCM.js → P2P-BVlFkwhW.js} +2 -2
  53. package/src/assets/web-panel/assets/{PdhVaultBrowser-BxPJZsD3.js → PdhVaultBrowser-BXbEdL5B.js} +3 -3
  54. package/src/assets/web-panel/assets/{Permissions-CLnDnPAz.js → Permissions-C28xs4lp.js} +4 -4
  55. package/src/assets/web-panel/assets/{PersonalDataHub-C5m0AjGf.js → PersonalDataHub-Dgl_rBjM.js} +4 -4
  56. package/src/assets/web-panel/assets/{Pipeline-VwOf5Dhv.js → Pipeline-3QUIhrKI.js} +1 -1
  57. package/src/assets/web-panel/assets/{Privacy-CC0W2hZd.js → Privacy-8Gh33OZ5.js} +1 -1
  58. package/src/assets/web-panel/assets/{ProjectInit-Dz893Bob.js → ProjectInit-BNmg4tZi.js} +2 -2
  59. package/src/assets/web-panel/assets/{ProjectSettings-CxQkvl0k.js → ProjectSettings-vgMfeCf9.js} +2 -2
  60. package/src/assets/web-panel/assets/Projects-BhtAVilc.js +1 -0
  61. package/src/assets/web-panel/assets/{Providers-DeB9IkxK.js → Providers-CXoOa8eX.js} +1 -1
  62. package/src/assets/web-panel/assets/{QrScannerModal-OCeoaswn.js → QrScannerModal-BhfTF0oS.js} +1 -1
  63. package/src/assets/web-panel/assets/{QuickAsk-DenotaAj.js → QuickAsk-C746HEAj.js} +1 -1
  64. package/src/assets/web-panel/assets/{Recommend-Cies_jjI.js → Recommend-BSej8Eo0.js} +1 -1
  65. package/src/assets/web-panel/assets/RemoteSession-f3gmnlG3.js +5 -0
  66. package/src/assets/web-panel/assets/{Reputation-CW0mvxgZ.js → Reputation-CyvybpkN.js} +1 -1
  67. package/src/assets/web-panel/assets/{Row-SEd32qZU.js → Row-DwV2PeYn.js} +1 -1
  68. package/src/assets/web-panel/assets/{RssFeed-BSwnJ3Lm.js → RssFeed-11GrzjZy.js} +3 -3
  69. package/src/assets/web-panel/assets/{Search-xe2m8bkC.js → Search-BjKXkrAY.js} +1 -1
  70. package/src/assets/web-panel/assets/{Security-Mq2F9Nro.js → Security-eWiFb9Yk.js} +4 -4
  71. package/src/assets/web-panel/assets/{Services-BKGw2V3P.js → Services-CdCsMAtt.js} +2 -2
  72. package/src/assets/web-panel/assets/{Skeleton-DW95-Rxf.js → Skeleton-4Q2UTKQ5.js} +1 -1
  73. package/src/assets/web-panel/assets/{Skills-CHgChCv_.js → Skills-v4rtYrRj.js} +1 -1
  74. package/src/assets/web-panel/assets/{Sla-tKh6k3R7.js → Sla-CRhsFvd_.js} +1 -1
  75. package/src/assets/web-panel/assets/SpeechSettings-DLjKsDVu.js +1 -0
  76. package/src/assets/web-panel/assets/{SyncSettings-D0Fw8fVL.js → SyncSettings-D16zonl7.js} +2 -2
  77. package/src/assets/web-panel/assets/{Tasks-20x1GB3D.js → Tasks-Xxozz0i_.js} +1 -1
  78. package/src/assets/web-panel/assets/{Templates-DJ-lts-a.js → Templates-CXJpPtCT.js} +1 -1
  79. package/src/assets/web-panel/assets/{Tenant-BFdGpbs7.js → Tenant-XRV0L1ZF.js} +1 -1
  80. package/src/assets/web-panel/assets/{Terminal-Xpx61vHL.js → Terminal-CYuO1i_w.js} +2 -2
  81. package/src/assets/web-panel/assets/{TimelineRenderer-C6UIvyZQ.js → TimelineRenderer-BXqitWsu.js} +1 -1
  82. package/src/assets/web-panel/assets/{Tokens-Dellq08f.js → Tokens-BvPrDHIy.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trigger-VKw8d0J-.js → Trigger-DhCrSTf5.js} +1 -1
  84. package/src/assets/web-panel/assets/{Trust-DNftdKT5.js → Trust-DmsuZD6K.js} +1 -1
  85. package/src/assets/web-panel/assets/{UkeySign-DX9VAKJI.js → UkeySign-D_yrts5C.js} +1 -1
  86. package/src/assets/web-panel/assets/{VideoEditing-B2h3jO8q.js → VideoEditing-B01ihPrm.js} +1 -1
  87. package/src/assets/web-panel/assets/{Wallet-CjGkof7f.js → Wallet-DpUjKa-N.js} +4 -4
  88. package/src/assets/web-panel/assets/{WebAuthn-2Zfcu55P.js → WebAuthn-CL07yON6.js} +4 -4
  89. package/src/assets/web-panel/assets/{WorkflowEditor-B9eZ8UxZ.js → WorkflowEditor-CLMflnRr.js} +1 -1
  90. package/src/assets/web-panel/assets/{chat-CQNfD-YO.js → chat-DtUqe8h_.js} +1 -1
  91. package/src/assets/web-panel/assets/{colors-D4usyFT2.js → colors-DPKrgVfy.js} +1 -1
  92. package/src/assets/web-panel/assets/{compact-item-s4TpjvjX.js → compact-item-BTt2GtOm.js} +1 -1
  93. package/src/assets/web-panel/assets/{createContext-BRCSvYxj.js → createContext-D_VeUc1J.js} +1 -1
  94. package/src/assets/web-panel/assets/devWarning-Bj5nUW2v.js +1 -0
  95. package/src/assets/web-panel/assets/{hasIn-AdkUbkV-.js → hasIn-1kndiylj.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-BIoG5yYf.js → index-2eyAXE3d.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-nXyk8GZ9.js → index-4bna2_SR.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-B2V-eKfK.js → index-8nz_fmlj.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-D_XBHopw.js → index-B9KvcS7R.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-s_buFMEG.js → index-BAXfRKfG.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-DZT36ZYq.js → index-BHpBMWCh.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-BMcimU-2.js → index-BaZAbw9A.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DzRjHxEv.js → index-BepLZwgB.js} +1 -1
  104. package/src/assets/web-panel/assets/{index--00U6vXZ.js → index-Bhudc_Ez.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-C7Y8Aluc.js → index-BlDNVgBt.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-BtobShro.js → index-By5n8up9.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-Cvyt-zhm.js → index-CHG-KcS4.js} +1 -1
  108. package/src/assets/web-panel/assets/index-CPcM-eqo.js +1 -0
  109. package/src/assets/web-panel/assets/{index-MMN9V6JF.js → index-CX81qSM4.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-Bad4VNvz.js → index-CauoBQkH.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-CCsM_Ybx.js → index-CcO04eDn.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-DARsAyUp.js → index-CtF7EN49.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-C5KAQFPQ.js → index-CzjkKUmR.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-RfMr79MW.js → index-D95OFht7.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-DdCB70SS.js → index-DFIZ_1Sf.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-CYdoQsa1.js → index-DG2MZQ3f.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BPhkFZ1I.js → index-DJ8vXGOa.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-_IRVsV9X.js → index-Dbbymxpj.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DVKfQ0Ux.js → index-DdkplL9R.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-CkwiSLhU.js → index-DfGl8CqG.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CJVEOa4i.js → index-DfKBRdSS.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-C1mJtE6q.js → index-DsJCDUJG.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-BgiXR_-_.js → index-Dwqmtk65.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-CJCFe2Qf.js → index-DxdTdBix.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-C484EJiS.js → index-FzdwcSy2.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-B4JJCfQ2.js → index-GsoUQ2M7.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-CQhAVU5t.js → index-brCInrzq.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-CKC1t3Ic.js → index-dVc-CXY4.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BQ3DXBtD.js → index-fk2Rjacw.js} +3 -3
  130. package/src/assets/web-panel/assets/{index-Cduu2Hhr.js → index-l1BETrDb.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-Bw2MEnUk.js → index-m31Ka8hZ.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-D2GjI2oe.js → index-ocf06EVQ.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-CJdQrZth.js → index-sDoMHTNA.js} +1 -1
  134. package/src/assets/web-panel/assets/index-xeSyVoRe.js +1 -0
  135. package/src/assets/web-panel/assets/{initDefaultProps-Blmpk7HL.js → initDefaultProps-CvyYJd7z.js} +1 -1
  136. package/src/assets/web-panel/assets/{motion-DPBJMv1n.js → motion-DYZkhUdz.js} +1 -1
  137. package/src/assets/web-panel/assets/{move-ButdzCM7.js → move-slnJRVT1.js} +1 -1
  138. package/src/assets/web-panel/assets/{mtc-parser-cyRC_QCe.js → mtc-parser-DVc0CKmy.js} +1 -1
  139. package/src/assets/web-panel/assets/{omit-1epSV7hz.js → omit-BCXAtHCq.js} +1 -1
  140. package/src/assets/web-panel/assets/{pickAttrs-DuPY8xJs.js → pickAttrs-DBsemz_K.js} +1 -1
  141. package/src/assets/web-panel/assets/{placementArrow-6XZqarhe.js → placementArrow-BRtkJ-nv.js} +1 -1
  142. package/src/assets/web-panel/assets/{responsiveObserve-B40pXBWM.js → responsiveObserve-Cebq7EZn.js} +1 -1
  143. package/src/assets/web-panel/assets/{slide-jbZ9Wu4n.js → slide-DT7qvId2.js} +1 -1
  144. package/src/assets/web-panel/assets/{statusUtils-BKQQcVy4.js → statusUtils-pQiITLkG.js} +1 -1
  145. package/src/assets/web-panel/assets/{styleChecker-NiKfWjUT.js → styleChecker-c-19BVKQ.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFlexGapSupport-bQUCvPeb.js → useFlexGapSupport-DsS5-0bE.js} +1 -1
  147. package/src/assets/web-panel/assets/{useFs-CUGji7ph.js → useFs-CxGiMe5n.js} +1 -1
  148. package/src/assets/web-panel/assets/{usePersonalDataHub-4veyN2N2.js → usePersonalDataHub-D9btVID5.js} +1 -1
  149. package/src/assets/web-panel/assets/{vnode-1-nLug-Z.js → vnode-CUYFsFEN.js} +1 -1
  150. package/src/assets/web-panel/assets/{zoom-dhCeRDMo.js → zoom-CXmn_35j.js} +1 -1
  151. package/src/assets/web-panel/index.html +1 -1
  152. package/src/command-help-index.json +1 -1
  153. package/src/commands/cowork.js +559 -62
  154. package/src/commands/plugin.js +819 -64
  155. package/src/commands/session-location.js +318 -7
  156. package/src/constants.js +2 -2
  157. package/src/data/changelog.json +28 -2
  158. package/src/gateways/ws/action-protocol.js +293 -19
  159. package/src/gateways/ws/message-dispatcher.js +30 -0
  160. package/src/gateways/ws/remote-session-protocol.js +542 -35
  161. package/src/gateways/ws/ws-approval-gate.js +5 -0
  162. package/src/gateways/ws/ws-server.js +462 -38
  163. package/src/harness/jsonl-session-store.js +638 -2
  164. package/src/harness/prompt-compressor.js +4 -0
  165. package/src/harness/remote-session-audit.js +1 -0
  166. package/src/harness/remote-session-crypto.js +750 -50
  167. package/src/harness/remote-session-registry.js +795 -27
  168. package/src/lib/auto-mode-config.js +98 -3
  169. package/src/lib/background-agent-keeper-client.js +29 -0
  170. package/src/lib/background-agent-keeper-protocol.js +158 -10
  171. package/src/lib/background-agent-supervisor.js +870 -48
  172. package/src/lib/background-session-transport.js +23 -6
  173. package/src/lib/background-turn-bootstrap-protocol.js +64 -0
  174. package/src/lib/cowork-task-runner.js +70 -0
  175. package/src/lib/cowork-workflow.js +887 -62
  176. package/src/lib/delivery-production-adapter.js +38 -2
  177. package/src/lib/doctor-checkup.js +34 -10
  178. package/src/lib/dynamic-workflow-draft.js +535 -0
  179. package/src/lib/dynamic-workflow-facade.js +1105 -17
  180. package/src/lib/dynamic-workflow-runtime.js +2974 -0
  181. package/src/lib/execution-location-contract.js +111 -7
  182. package/src/lib/execution-location-runtime.js +3 -1
  183. package/src/lib/execution-location-target.js +910 -0
  184. package/src/lib/llm-providers.js +1 -0
  185. package/src/lib/mcp-call-ledger-store.js +35 -0
  186. package/src/lib/mcp-call-ledger.js +30 -0
  187. package/src/lib/mcp-stdio-package-materialization.js +1 -1
  188. package/src/lib/plugin-runtime/__tests__/remote-source.test.js +34 -16
  189. package/src/lib/plugin-runtime/install.js +4956 -657
  190. package/src/lib/plugin-runtime/manifest.js +3 -1
  191. package/src/lib/plugin-runtime/marketplace-artifact-readback.js +671 -31
  192. package/src/lib/plugin-runtime/marketplace-catalog.js +68 -2
  193. package/src/lib/plugin-runtime/marketplace-impact.js +208 -15
  194. package/src/lib/plugin-runtime/marketplace-network.js +317 -0
  195. package/src/lib/plugin-runtime/marketplace-pac-worker.js +15 -0
  196. package/src/lib/plugin-runtime/marketplace-remote-artifacts.js +25 -2
  197. package/src/lib/plugin-runtime/marketplace-source-cache.js +359 -0
  198. package/src/lib/plugin-runtime/policy.js +21 -6
  199. package/src/lib/plugin-runtime/publisher-trust.js +332 -0
  200. package/src/lib/plugin-runtime/remote-source.js +323 -27
  201. package/src/lib/plugin-runtime/scopes.js +317 -41
  202. package/src/lib/plugin-runtime/transaction-journal.js +806 -0
  203. package/src/lib/public-ide-capability-manifest.js +1 -1
  204. package/src/lib/remote-approval-bridge.js +745 -108
  205. package/src/lib/remote-control.js +2 -0
  206. package/src/lib/remote-membership-coordinator.js +612 -46
  207. package/src/lib/remote-membership-host-store.js +59 -10
  208. package/src/lib/runtime-provenance-ledger.js +171 -46
  209. package/src/lib/shell-approval.js +169 -6
  210. package/src/lib/sub-agent-context.js +268 -2
  211. package/src/lib/task-model-selector.js +1 -1
  212. package/src/lib/with-file-lock.js +167 -42
  213. package/src/repl/agent-repl.js +70 -29
  214. package/src/repl/remote-approval.js +29 -7
  215. package/src/runtime/agent-core.js +1695 -194
  216. package/src/runtime/headless-runner.js +22 -5
  217. package/src/runtime/headless-stream.js +19 -3
  218. package/src/workers/background-agent-keeper.js +279 -58
  219. package/src/workers/background-agent-worker.js +7 -0
  220. package/src/workers/background-turn-bootstrap.js +29 -2
  221. package/src/assets/web-panel/assets/MarkdownRenderer-GmB6DOr6.js +0 -1
  222. package/src/assets/web-panel/assets/MobileProjects-BL0Czcoj.js +0 -1
  223. package/src/assets/web-panel/assets/Projects-DDAjkUkT.js +0 -1
  224. package/src/assets/web-panel/assets/RemoteSession-Dxzl5n2T.js +0 -5
  225. package/src/assets/web-panel/assets/SpeechSettings-D6UL3dA5.js +0 -1
  226. package/src/assets/web-panel/assets/devWarning-DKJjSWRn.js +0 -1
  227. package/src/assets/web-panel/assets/index-C61cnl3K.js +0 -1
  228. package/src/assets/web-panel/assets/index-D4XFHxBr.js +0 -1
@@ -55,6 +55,8 @@ import {
55
55
  BACKGROUND_AGENT_KEEPER_TASKKILL_TIMEOUT_MS,
56
56
  BACKGROUND_AGENT_KEEPER_WMIC_TIMEOUT_MS,
57
57
  backgroundAgentKeeperPipePath,
58
+ cleanupBackgroundAgentKeeperPipeDirectory,
59
+ prepareBackgroundAgentKeeperPipePath,
58
60
  } from "./background-agent-keeper-protocol.js";
59
61
 
60
62
  export const DEFAULT_HEARTBEAT_INTERVAL_MS = 5000;
@@ -74,8 +76,88 @@ export const DEFAULT_HEARTBEAT_STALE_MS = 120000;
74
76
  */
75
77
  export const PID_IDENTITY_TOLERANCE_MS = 60000;
76
78
 
77
- const START_TIME_CACHE_TTL_MS = 10000;
78
- const _startTimeCache = new Map();
79
+ export const PROCESS_START_TIME_CACHE_TTL_MS = 10_000;
80
+ export const PROCESS_START_TIME_CACHE_MAX_ENTRIES = 4_096;
81
+
82
+ /**
83
+ * A bounded FIFO/TTL cache for process creation-time probes.
84
+ *
85
+ * Probe timestamps use a monotonic clock. Deleting and re-inserting an
86
+ * existing pid refreshes its FIFO position without increasing the number of
87
+ * retained keys. Expired entries are removed from the oldest edge, making
88
+ * cleanup amortized O(1) because insertion timestamps are monotonic.
89
+ */
90
+ export class ProcessStartTimeCache {
91
+ constructor({
92
+ ttlMs = PROCESS_START_TIME_CACHE_TTL_MS,
93
+ maxEntries = PROCESS_START_TIME_CACHE_MAX_ENTRIES,
94
+ now = () => performance.now(),
95
+ } = {}) {
96
+ if (!Number.isFinite(Number(ttlMs)) || Number(ttlMs) <= 0) {
97
+ throw new TypeError("process start-time cache ttlMs must be positive");
98
+ }
99
+ if (!Number.isSafeInteger(Number(maxEntries)) || Number(maxEntries) <= 0) {
100
+ throw new TypeError(
101
+ "process start-time cache maxEntries must be a positive integer",
102
+ );
103
+ }
104
+ if (typeof now !== "function") {
105
+ throw new TypeError("process start-time cache now must be a function");
106
+ }
107
+ this.ttlMs = Number(ttlMs);
108
+ this.maxEntries = Number(maxEntries);
109
+ this._now = now;
110
+ this._entries = new Map();
111
+ }
112
+
113
+ _at(value) {
114
+ const at = value === undefined ? Number(this._now()) : Number(value);
115
+ if (!Number.isFinite(at)) {
116
+ throw new TypeError("process start-time cache time must be finite");
117
+ }
118
+ return at;
119
+ }
120
+
121
+ _evictExpired(at) {
122
+ for (const [key, entry] of this._entries) {
123
+ if (at - entry.at < this.ttlMs) break;
124
+ this._entries.delete(key);
125
+ }
126
+ }
127
+
128
+ read(key, atValue) {
129
+ const at = this._at(atValue);
130
+ this._evictExpired(at);
131
+ const entry = this._entries.get(key);
132
+ return entry ? { hit: true, value: entry.value } : { hit: false };
133
+ }
134
+
135
+ set(key, value, atValue) {
136
+ const at = this._at(atValue);
137
+ this._evictExpired(at);
138
+ // Map preserves insertion order. Refresh the same pid at the newest edge
139
+ // so capacity eviction cannot discard a just-probed identity.
140
+ this._entries.delete(key);
141
+ while (this._entries.size >= this.maxEntries) {
142
+ const oldest = this._entries.keys().next().value;
143
+ this._entries.delete(oldest);
144
+ }
145
+ this._entries.set(key, { at, value });
146
+ return value;
147
+ }
148
+
149
+ get size() {
150
+ return this._entries.size;
151
+ }
152
+
153
+ clear() {
154
+ this._entries.clear();
155
+ }
156
+ }
157
+
158
+ const _startTimeCache = new ProcessStartTimeCache();
159
+ const PROCESS_START_TIME_ABSENT = Symbol("process-start-time-absent");
160
+ const WINDOWS_PROCESS_ABSENT_MARKER = "__CC_PROCESS_ABSENT__";
79
161
 
80
162
  function runSupervisorCommand(file, args, options, origin) {
81
163
  return _deps.spawnSync(file, args, {
@@ -108,7 +190,8 @@ export function parseCimDateToMs(raw) {
108
190
  }
109
191
 
110
192
  /**
111
- * Read a process's creation time (epoch ms) with ONE synchronous probe, or
193
+ * Read a process's creation time (epoch ms) with ONE synchronous probe,
194
+ * `{status:"absent"}` when the OS authoritatively reports no such process, or
112
195
  * null when it cannot be determined (missing tools, permissions, platform
113
196
  * quirks). Callers must treat null as "unknown" and FAIL OPEN to the legacy
114
197
  * kill(pid, 0) answer — a broken probe must never declare a live worker dead
@@ -140,9 +223,13 @@ function defaultReadProcessStartTimeMs(pid) {
140
223
  "background-agent:process-start-time",
141
224
  );
142
225
  if (!r.error && r.status === 0) {
143
- const m = /CreationDate=([^\r\n]+)/.exec(r.stdout || "");
226
+ const stdout = String(r.stdout || "");
227
+ const m = /CreationDate=([^\r\n]+)/.exec(stdout);
144
228
  const ms = m ? parseCimDateToMs(m[1]) : null;
145
229
  if (ms !== null) return ms;
230
+ if (/No Instance\(s\) Available/u.test(stdout)) {
231
+ return { status: "absent" };
232
+ }
146
233
  }
147
234
  } catch {
148
235
  /* fall through to PowerShell */
@@ -150,7 +237,9 @@ function defaultReadProcessStartTimeMs(pid) {
150
237
  try {
151
238
  const script =
152
239
  `$p = Get-CimInstance Win32_Process -Filter 'ProcessId=${target}' -ErrorAction SilentlyContinue; ` +
153
- "if ($p -and $p.CreationDate) { [DateTimeOffset]::new($p.CreationDate).ToUnixTimeMilliseconds() }";
240
+ `if (-not $p) { '${WINDOWS_PROCESS_ABSENT_MARKER}' } ` +
241
+ "elseif ($p.CreationDate) { [DateTimeOffset]::new($p.CreationDate).ToUnixTimeMilliseconds() } " +
242
+ "else { exit 2 }";
154
243
  const r = runSupervisorCommand(
155
244
  "powershell",
156
245
  ["-NoProfile", "-NonInteractive", "-Command", script],
@@ -162,7 +251,11 @@ function defaultReadProcessStartTimeMs(pid) {
162
251
  "background-agent:process-start-time",
163
252
  );
164
253
  if (!r.error && r.status === 0) {
165
- const n = Number(String(r.stdout || "").trim());
254
+ const output = String(r.stdout || "").trim();
255
+ if (output === WINDOWS_PROCESS_ABSENT_MARKER) {
256
+ return { status: "absent" };
257
+ }
258
+ const n = Number(output);
166
259
  if (Number.isFinite(n) && n > 0) return n;
167
260
  }
168
261
  } catch {
@@ -322,6 +415,8 @@ export const _deps = {
322
415
  readProcessState: defaultReadProcessState,
323
416
  readProcessGroupStates: defaultReadProcessGroupStates,
324
417
  processExitWaitDeadlineMs: 2_000,
418
+ posixPermissionRetryNowMs: () => performance.now(),
419
+ posixPermissionRetrySleep: sleepSyncMs,
325
420
  killProcessTree: defaultKillProcessTree,
326
421
  terminateOwnedProcessTree,
327
422
  withSessionHostRecoveryLease,
@@ -334,7 +429,14 @@ export const _deps = {
334
429
  */
335
430
  export function stopBackgroundAgentChildTree(
336
431
  pid,
337
- { platform = process.platform, signal = "SIGTERM" } = {},
432
+ {
433
+ platform = process.platform,
434
+ signal = "SIGTERM",
435
+ expectedStartedAt,
436
+ processGroup = true,
437
+ strictIdentity = false,
438
+ allowDirectFallback = true,
439
+ } = {},
338
440
  ) {
339
441
  const target = Number(pid);
340
442
  if (!Number.isInteger(target) || target <= 0 || target === process.pid) {
@@ -342,6 +444,15 @@ export function stopBackgroundAgentChildTree(
342
444
  error.code = "ERR_BACKGROUND_AGENT_CHILD_PID_INVALID";
343
445
  throw error;
344
446
  }
447
+ if (strictIdentity === true) {
448
+ signalExactBackgroundProcessTree(target, signal, {
449
+ platform,
450
+ expectedStartedAt,
451
+ processGroup,
452
+ allowDirectFallback,
453
+ });
454
+ return true;
455
+ }
345
456
  if (platform === "win32") {
346
457
  const result = runSupervisorCommand(
347
458
  "taskkill",
@@ -366,32 +477,38 @@ export function stopBackgroundAgentChildTree(
366
477
  }
367
478
  return true;
368
479
  }
369
- try {
370
- _deps.kill(-target, signal);
371
- } catch {
372
- _deps.kill(target, signal);
373
- }
480
+ // Preserve the legacy caller contract while sharing the safe POSIX tree
481
+ // primitive: permission/invalid-signal failures remain fatal, and only a
482
+ // genuinely absent process group may use the historical direct fallback.
483
+ signalBackgroundProcessTree(target, signal, {
484
+ platform,
485
+ processGroup,
486
+ allowDirectFallback,
487
+ });
374
488
  return true;
375
489
  }
376
490
 
377
491
  /** Cached probe (default impl only — injected probes run uncached for tests). */
378
492
  function processStartTimeMs(pid, options = {}) {
493
+ const normalize = (raw) => {
494
+ if (raw?.status === "absent") return PROCESS_START_TIME_ABSENT;
495
+ const value = Number(raw);
496
+ return Number.isFinite(value) && value > 0 ? value : null;
497
+ };
379
498
  if (_deps.readProcessStartTimeMs !== defaultReadProcessStartTimeMs) {
380
- const raw = _deps.readProcessStartTimeMs(pid);
381
- const n = Number(raw);
382
- return Number.isFinite(n) && n > 0 ? n : null;
499
+ return normalize(_deps.readProcessStartTimeMs(pid));
383
500
  }
384
501
  const key = Number(pid);
385
- const at = Date.now();
502
+ const at = performance.now();
386
503
  if (options.fresh === true) {
387
- const value = defaultReadProcessStartTimeMs(key);
388
- _startTimeCache.set(key, { at, value });
504
+ const value = normalize(defaultReadProcessStartTimeMs(key));
505
+ _startTimeCache.set(key, value, at);
389
506
  return value;
390
507
  }
391
- const hit = _startTimeCache.get(key);
392
- if (hit && at - hit.at < START_TIME_CACHE_TTL_MS) return hit.value;
393
- const value = defaultReadProcessStartTimeMs(key);
394
- _startTimeCache.set(key, { at, value });
508
+ const hit = _startTimeCache.read(key, at);
509
+ if (hit.hit) return hit.value;
510
+ const value = normalize(defaultReadProcessStartTimeMs(key));
511
+ _startTimeCache.set(key, value, at);
395
512
  return value;
396
513
  }
397
514
 
@@ -441,11 +558,70 @@ const TERMINAL_STATUSES = new Set(["completed", "failed", "stopped", "lost"]);
441
558
  const KEEPER_TERMINAL_STATUSES = new Set([
442
559
  "closed",
443
560
  "worker-disconnected",
561
+ "cleanup-unconfirmed",
444
562
  "startup-timeout",
445
563
  "spawn-error",
446
564
  "exited",
447
565
  ]);
448
566
 
567
+ export function hasValidBackgroundAgentStopCleanupProof(state) {
568
+ const requestedAt = Number(state?.stopRequestedAt);
569
+ const confirmedAt = Number(state?.stopCleanupConfirmedAt);
570
+ return Boolean(
571
+ Number.isFinite(requestedAt) &&
572
+ requestedAt > 0 &&
573
+ Number.isFinite(confirmedAt) &&
574
+ confirmedAt >= requestedAt,
575
+ );
576
+ }
577
+
578
+ function hasBackgroundAgentStopFenceWithoutCleanupProof(state) {
579
+ return Boolean(
580
+ Number.isFinite(Number(state?.stopRequestedAt)) &&
581
+ Number(state.stopRequestedAt) > 0 &&
582
+ !hasValidBackgroundAgentStopCleanupProof(state),
583
+ );
584
+ }
585
+
586
+ function isBackgroundAgentStopCleanupUnconfirmed(state) {
587
+ return Boolean(
588
+ state?.status === "stopped" &&
589
+ hasBackgroundAgentStopFenceWithoutCleanupProof(state),
590
+ );
591
+ }
592
+
593
+ function selectBackgroundAgentStopCleanupProof(current, requested) {
594
+ if (hasValidBackgroundAgentStopCleanupProof(current)) {
595
+ return current.stopCleanupConfirmedAt;
596
+ }
597
+ const requestedAt = Number(
598
+ Number.isFinite(Number(current?.stopRequestedAt)) &&
599
+ Number(current.stopRequestedAt) > 0
600
+ ? current.stopRequestedAt
601
+ : requested?.stopRequestedAt,
602
+ );
603
+ const requestedProof = Number(requested?.stopCleanupConfirmedAt);
604
+ if (
605
+ Number.isFinite(requestedAt) &&
606
+ requestedAt > 0 &&
607
+ Number.isFinite(requestedProof) &&
608
+ requestedProof >= requestedAt
609
+ ) {
610
+ return requestedProof;
611
+ }
612
+ return current?.stopCleanupConfirmedAt;
613
+ }
614
+
615
+ function backgroundAgentStopCleanupConfirmationTimestamp(
616
+ state,
617
+ now = Date.now(),
618
+ ) {
619
+ const requestedAt = Number(state?.stopRequestedAt);
620
+ return Number.isFinite(requestedAt) && requestedAt > 0
621
+ ? Math.max(now, requestedAt)
622
+ : now;
623
+ }
624
+
449
625
  const TURN_PROCESS_PROJECTION_FIELDS = [
450
626
  "agentPid",
451
627
  "agentStartedAt",
@@ -606,10 +782,17 @@ function mergeBackgroundAgentState(current, requested) {
606
782
  current.stopRequestedAt &&
607
783
  requested.stopRequestedAt === current.stopRequestedAt;
608
784
  if (TERMINAL_STATUSES.has(current.status) && !explicitHeartbeatStop) {
785
+ const stopCleanupConfirmedAt = selectBackgroundAgentStopCleanupProof(
786
+ current,
787
+ requested,
788
+ );
609
789
  next = {
610
790
  ...next,
611
791
  status: current.status,
612
792
  endedAt: current.endedAt ?? next.endedAt ?? null,
793
+ ...(stopCleanupConfirmedAt !== undefined
794
+ ? { stopCleanupConfirmedAt }
795
+ : {}),
613
796
  exitCode: current.exitCode ?? next.exitCode ?? null,
614
797
  ...(current.signal !== undefined ? { signal: current.signal } : {}),
615
798
  ...(current.error !== undefined ? { error: current.error } : {}),
@@ -920,6 +1103,26 @@ function mergeBackgroundAgentState(current, requested) {
920
1103
  transport: null,
921
1104
  };
922
1105
  }
1106
+ if (
1107
+ current.status === "running" &&
1108
+ next.status === "stopped" &&
1109
+ !hasValidBackgroundAgentStopCleanupProof(next)
1110
+ ) {
1111
+ // A worker or read-side reconciliation can publish a terminal outcome
1112
+ // after an explicit stop fence wins but before the stopper has observed
1113
+ // every exact-owned process retire. Keep that record durably fenced
1114
+ // and retryable; only stopBackgroundAgent may publish `stopped`
1115
+ // together with cleanup proof.
1116
+ next = {
1117
+ ...next,
1118
+ status: "running",
1119
+ stoppedByUser: false,
1120
+ phase: "stop_waiting_for_exit",
1121
+ transport: null,
1122
+ stopPending: true,
1123
+ stopPendingReason: "process-exit",
1124
+ };
1125
+ }
923
1126
  }
924
1127
  }
925
1128
  return next;
@@ -952,6 +1155,20 @@ function persistBackgroundAgentState(target, next) {
952
1155
  return next;
953
1156
  }
954
1157
 
1158
+ function isBackgroundAgentStateLockOwnerAlive(owner) {
1159
+ const target = Number(owner?.pid);
1160
+ if (!isProcessExecutionAlive(target)) return false;
1161
+ const acquiredAt = Number(owner?.startedAt);
1162
+ if (!Number.isFinite(acquiredAt) || acquiredAt <= 0) return true;
1163
+ const actualStartedAt = processStartTimeMs(target, { fresh: true });
1164
+ if (actualStartedAt === PROCESS_START_TIME_ABSENT) return false;
1165
+ if (!Number.isFinite(actualStartedAt)) return true;
1166
+ // owner.startedAt is written only after this process acquired the lock. A
1167
+ // live process created later therefore proves PID reuse; retaining that
1168
+ // stale lock would permanently fence the state after high-churn workers.
1169
+ return actualStartedAt <= acquiredAt;
1170
+ }
1171
+
955
1172
  /**
956
1173
  * Strict cross-process read/mutate/write transaction for one supervisor state.
957
1174
  * Returning null from `updater` leaves the record untouched. Missing and
@@ -981,7 +1198,25 @@ export function mutateBackgroundAgentState(id, updater, options = {}) {
981
1198
  },
982
1199
  {
983
1200
  failIfUnavailable: true,
984
- timeoutMs: options.timeoutMs ?? 5_000,
1201
+ // Startup has three independent writers (launcher, worker and keeper),
1202
+ // while stop/turn settlement adds another contender. Hosted Windows can
1203
+ // otherwise starve a first heartbeat for the old five-second budget when
1204
+ // the full strict matrix is saturating the runner. Keep the lock strict
1205
+ // and bounded, but give default callers one full ten-second contention
1206
+ // window; keeper cleanup keeps its explicit protocol-bound five seconds.
1207
+ timeoutMs: options.timeoutMs ?? 10_000,
1208
+ // Release before yielding so an already-waiting process gets a scheduling
1209
+ // opportunity instead of letting a heartbeat loop immediately reacquire.
1210
+ yieldAfterReleaseMs: 2,
1211
+ // A stopped POSIX worker can remain as a zombie long enough for
1212
+ // kill(pid, 0) to report it as alive. Such a process cannot still own
1213
+ // the critical section, so use the supervisor's execution-state probe
1214
+ // (which treats only Z/X as dead and keeps unknown states fail-closed).
1215
+ _isProcessAlive: isProcessExecutionAlive,
1216
+ // A live numeric pid can belong to a later process after the lock owner
1217
+ // was killed. Use the lock acquisition timestamp as a one-sided identity
1218
+ // fence so only an owner which existed before acquisition still blocks.
1219
+ _isOwnerAlive: isBackgroundAgentStateLockOwnerAlive,
985
1220
  },
986
1221
  );
987
1222
  }
@@ -1208,6 +1443,7 @@ export function isSameProcess(pid, expectedStartedAtMs, options = {}) {
1208
1443
  const expected = Number(expectedStartedAtMs);
1209
1444
  if (!Number.isFinite(expected) || expected <= 0) return true;
1210
1445
  const actual = processStartTimeMs(pid);
1446
+ if (actual === PROCESS_START_TIME_ABSENT) return false;
1211
1447
  if (actual === null) return true;
1212
1448
  const tolerance = Number.isFinite(Number(options.toleranceMs))
1213
1449
  ? Number(options.toleranceMs)
@@ -1242,7 +1478,7 @@ function inspectDestructiveProcessIdentity(
1242
1478
  const leaderCanExecute =
1243
1479
  leaderAlive && leaderState !== "Z" && leaderState !== "X";
1244
1480
  if (!leaderCanExecute) {
1245
- if (process.platform === "win32") {
1481
+ if (process.platform === "win32" || options.processGroup === false) {
1246
1482
  return { status: "dead", reason: "not-executing", pid: target };
1247
1483
  }
1248
1484
  const group = inspectProcessGroupExecution(target);
@@ -1280,6 +1516,13 @@ function inspectDestructiveProcessIdentity(
1280
1516
  };
1281
1517
  }
1282
1518
  const actual = processStartTimeMs(target, { fresh: true });
1519
+ if (actual === PROCESS_START_TIME_ABSENT) {
1520
+ return {
1521
+ status: "dead",
1522
+ reason: "process-not-observed",
1523
+ pid: target,
1524
+ };
1525
+ }
1283
1526
  if (actual === null) {
1284
1527
  return {
1285
1528
  status: "unverifiable",
@@ -1292,7 +1535,7 @@ function inspectDestructiveProcessIdentity(
1292
1535
  ? Number(options.toleranceMs)
1293
1536
  : PID_IDENTITY_TOLERANCE_MS;
1294
1537
  if (Math.abs(actual - expected) > tolerance) {
1295
- if (process.platform !== "win32") {
1538
+ if (process.platform !== "win32" && options.processGroup !== false) {
1296
1539
  const group = inspectProcessGroupExecution(target);
1297
1540
  // The leader PID can be reused while descendants from its detached
1298
1541
  // process group still execute under the old PGID. We can prove the new
@@ -1578,6 +1821,16 @@ export function effectiveBackgroundAgentState(state, options = {}) {
1578
1821
  }
1579
1822
  return state;
1580
1823
  }
1824
+ if (
1825
+ Number.isFinite(Number(state.stopRequestedAt)) &&
1826
+ Number(state.stopRequestedAt) > 0 &&
1827
+ !hasValidBackgroundAgentStopCleanupProof(state)
1828
+ ) {
1829
+ // A durable stop fence transfers terminal reconciliation to the explicit
1830
+ // stopper. A concurrent list/view must not persist `lost` before that path
1831
+ // has verified every exact-owned process and published cleanup proof.
1832
+ return state;
1833
+ }
1581
1834
  // OS identity probes can take seconds on Windows. Perform them outside the
1582
1835
  // state lock, then use the exact observed identity/heartbeat as a CAS fence
1583
1836
  // inside the short persistence transaction.
@@ -1952,6 +2205,13 @@ export function resumeBackgroundAgent(id, prompt, options = {}) {
1952
2205
  heartbeatStaleMs: options.heartbeatStaleMs,
1953
2206
  });
1954
2207
  if (!state) throw new Error(`Background agent not found: ${id}`);
2208
+ if (hasBackgroundAgentStopFenceWithoutCleanupProof(state)) {
2209
+ const error = new Error(
2210
+ `Background agent ${id} stop cleanup is still pending; retry stop and wait for durable cleanup confirmation before resuming`,
2211
+ );
2212
+ error.code = "BACKGROUND_AGENT_STOP_CLEANUP_PENDING";
2213
+ throw error;
2214
+ }
1955
2215
  if (state.status === "running") {
1956
2216
  throw new Error(
1957
2217
  `Background agent ${id} is still running — use cc attach ${id} to send follow-up prompts instead`,
@@ -2270,6 +2530,12 @@ export function launchBackgroundAgent({
2270
2530
  omitted: [...launchProfileState.omitted, "apiKey"],
2271
2531
  });
2272
2532
  }
2533
+ const launchCwd = launchProfileState.workspace.cwd;
2534
+ if (canonicalPath(cwd) !== canonicalPath(launchCwd)) {
2535
+ throw new Error(
2536
+ "Cannot launch background agent: launch profile cwd does not match the requested cwd",
2537
+ );
2538
+ }
2273
2539
  const configFingerprint =
2274
2540
  fingerprintBackgroundLaunchProfile(launchProfileState);
2275
2541
  const dir = backgroundAgentsDir();
@@ -2277,6 +2543,7 @@ export function launchBackgroundAgent({
2277
2543
  const keeperGeneration = randomBytes(16).toString("hex");
2278
2544
  const keeperToken = randomBytes(32).toString("hex");
2279
2545
  const keeperPipePath = backgroundAgentKeeperPipePath(id, dir);
2546
+ prepareBackgroundAgentKeeperPipePath(keeperPipePath);
2280
2547
  const jobFile = join(dir, `${id}.job.${process.pid}.json`);
2281
2548
  const keeperJobFile = join(dir, `${id}.keeper.${process.pid}.json`);
2282
2549
  const worker = fileURLToPath(
@@ -2289,7 +2556,7 @@ export function launchBackgroundAgent({
2289
2556
  id,
2290
2557
  workerGeneration,
2291
2558
  argv: initialSecrets.argv,
2292
- cwd,
2559
+ cwd: launchCwd,
2293
2560
  sessionId,
2294
2561
  sessionBootstrapExpected,
2295
2562
  title: title || "Background agent",
@@ -2324,6 +2591,7 @@ export function launchBackgroundAgent({
2324
2591
  } catch (error) {
2325
2592
  rmSync(jobFile, { force: true });
2326
2593
  rmSync(keeperJobFile, { force: true });
2594
+ cleanupBackgroundAgentKeeperPipeDirectory(keeperPipePath);
2327
2595
  throw error;
2328
2596
  }
2329
2597
  // Write the initial state BEFORE spawning so the worker's own merges (the
@@ -2335,7 +2603,7 @@ export function launchBackgroundAgent({
2335
2603
  sessionId,
2336
2604
  sessionBootstrapExpected,
2337
2605
  title: job.title,
2338
- cwd,
2606
+ cwd: launchCwd,
2339
2607
  pid: null,
2340
2608
  workerPid: null,
2341
2609
  workerClaimedPid: null,
@@ -2372,6 +2640,7 @@ export function launchBackgroundAgent({
2372
2640
  rmSync(jobFile, { force: true });
2373
2641
  rmSync(keeperJobFile, { force: true });
2374
2642
  rmSync(statePath(id), { force: true });
2643
+ cleanupBackgroundAgentKeeperPipeDirectory(keeperPipePath);
2375
2644
  throw error;
2376
2645
  }
2377
2646
  let child;
@@ -2386,7 +2655,7 @@ export function launchBackgroundAgent({
2386
2655
  // hosted process' possibly absent standard handles.
2387
2656
  workerLogFd = openSync(job.logFile, "a");
2388
2657
  child = _deps.spawn(process.execPath, [worker, jobFile], {
2389
- cwd,
2658
+ cwd: launchCwd,
2390
2659
  detached: true,
2391
2660
  stdio: ["ignore", workerLogFd, workerLogFd],
2392
2661
  windowsHide: true,
@@ -2400,7 +2669,7 @@ export function launchBackgroundAgent({
2400
2669
  });
2401
2670
  keeperStartedAt = Date.now();
2402
2671
  keeperChild = _deps.spawn(process.execPath, [keeper, keeperJobFile], {
2403
- cwd,
2672
+ cwd: launchCwd,
2404
2673
  detached: true,
2405
2674
  stdio: ["ignore", workerLogFd, workerLogFd],
2406
2675
  windowsHide: true,
@@ -2451,6 +2720,7 @@ export function launchBackgroundAgent({
2451
2720
  rmSync(jobFile, { force: true });
2452
2721
  rmSync(keeperJobFile, { force: true });
2453
2722
  rmSync(statePath(id), { force: true });
2723
+ cleanupBackgroundAgentKeeperPipeDirectory(keeperPipePath);
2454
2724
  throw error;
2455
2725
  } finally {
2456
2726
  if (Number.isInteger(workerLogFd)) {
@@ -2635,33 +2905,50 @@ export function readBackgroundAgentLog(id, options = {}) {
2635
2905
  }
2636
2906
 
2637
2907
  function recordedBackgroundProcessIdentities(state) {
2908
+ const hasRecordedAgentTree =
2909
+ Number(state?.agentPid) > 0 || Number(state?.agentRuntimePid) > 0;
2638
2910
  const records = [
2911
+ {
2912
+ role: "agent",
2913
+ pid: state?.agentPid,
2914
+ startedAt: state?.agentStartedAt,
2915
+ signalAsGroup: true,
2916
+ },
2639
2917
  {
2640
2918
  role: "agent-runtime",
2641
2919
  pid: state?.agentRuntimePid,
2642
2920
  startedAt: state?.agentRuntimeStartedAt,
2921
+ signalAsGroup: false,
2922
+ },
2923
+ {
2924
+ role: "worker",
2925
+ pid: state?.pid,
2926
+ startedAt: state?.startedAt,
2927
+ signalAsGroup: !hasRecordedAgentTree,
2643
2928
  },
2644
- { role: "agent", pid: state?.agentPid, startedAt: state?.agentStartedAt },
2645
- { role: "worker", pid: state?.pid, startedAt: state?.startedAt },
2646
2929
  {
2647
2930
  role: "worker-published",
2648
2931
  pid: state?.workerPid,
2649
2932
  startedAt: state?.startedAt,
2933
+ signalAsGroup: !hasRecordedAgentTree,
2650
2934
  },
2651
2935
  {
2652
2936
  role: "worker-wrapper",
2653
2937
  pid: state?.workerWrapperPid,
2654
2938
  startedAt: state?.startedAt,
2939
+ signalAsGroup: true,
2655
2940
  },
2656
2941
  {
2657
2942
  role: "keeper",
2658
2943
  pid: state?.keeperPid,
2659
2944
  startedAt: state?.keeperStartedAt,
2945
+ signalAsGroup: false,
2660
2946
  },
2661
2947
  {
2662
2948
  role: "keeper-wrapper",
2663
2949
  pid: state?.keeperWrapperPid,
2664
2950
  startedAt: state?.keeperStartedAt,
2951
+ signalAsGroup: true,
2665
2952
  },
2666
2953
  ];
2667
2954
  const seen = new Set();
@@ -2677,7 +2964,9 @@ function recordedBackgroundProcessIdentities(state) {
2677
2964
  function inspectRecordedBackgroundProcesses(state) {
2678
2965
  return recordedBackgroundProcessIdentities(state).map((record) => ({
2679
2966
  ...record,
2680
- identity: inspectDestructiveProcessIdentity(record.pid, record.startedAt),
2967
+ identity: inspectDestructiveProcessIdentity(record.pid, record.startedAt, {
2968
+ processGroup: record.signalAsGroup,
2969
+ }),
2681
2970
  }));
2682
2971
  }
2683
2972
 
@@ -2698,7 +2987,11 @@ export function isBackgroundProcessTreeExecutionAlive(pid, startedAt) {
2698
2987
  export function signalBackgroundProcessTree(
2699
2988
  pid,
2700
2989
  signal = "SIGTERM",
2701
- { platform = process.platform } = {},
2990
+ {
2991
+ platform = process.platform,
2992
+ processGroup = true,
2993
+ allowDirectFallback = true,
2994
+ } = {},
2702
2995
  ) {
2703
2996
  const target = Number(pid);
2704
2997
  if (platform === "win32") {
@@ -2721,6 +3014,15 @@ export function signalBackgroundProcessTree(
2721
3014
  }
2722
3015
  return "process-tree";
2723
3016
  }
3017
+ if (processGroup !== true) {
3018
+ try {
3019
+ _deps.kill(target, signal);
3020
+ return "process";
3021
+ } catch (error) {
3022
+ if (error?.code === "ESRCH") return "already-exited";
3023
+ throw error;
3024
+ }
3025
+ }
2724
3026
  try {
2725
3027
  _deps.kill(-target, signal);
2726
3028
  return "process-group";
@@ -2728,7 +3030,9 @@ export function signalBackgroundProcessTree(
2728
3030
  // A denied/invalid group signal can leave descendants executing even if a
2729
3031
  // direct leader signal would succeed. Only ESRCH proves that there is no
2730
3032
  // process group to terminate and permits the single-process fallback.
2731
- if (groupError?.code !== "ESRCH") throw groupError;
3033
+ if (groupError?.code !== "ESRCH" || allowDirectFallback !== true) {
3034
+ throw groupError;
3035
+ }
2732
3036
  try {
2733
3037
  _deps.kill(target, signal);
2734
3038
  return "process";
@@ -2743,6 +3047,346 @@ export function signalBackgroundProcessTree(
2743
3047
  }
2744
3048
  }
2745
3049
 
3050
+ function isDestructiveIdentityRetired(identity) {
3051
+ return ["dead", "reused"].includes(identity?.status);
3052
+ }
3053
+
3054
+ function isRecoverableDestructiveIdentityProbe(identity) {
3055
+ // These outcomes come only from read-only OS observations. Retrying them
3056
+ // cannot address a process, and the caller still requires two consecutive
3057
+ // fresh exact matches before it may issue or reissue a signal.
3058
+ return (
3059
+ identity?.status === "unverifiable" &&
3060
+ ["creation-time-probe-failed", "process-group-state-probe-failed"].includes(
3061
+ identity?.reason,
3062
+ )
3063
+ );
3064
+ }
3065
+
3066
+ function createPosixProbeRetryBudget(deadline) {
3067
+ const configuredRetryWindow = Math.max(
3068
+ 0,
3069
+ Number(_deps.processExitWaitDeadlineMs) || 0,
3070
+ );
3071
+ let remainingSleepBudget = Math.min(
3072
+ configuredRetryWindow,
3073
+ Math.max(0, deadline - _deps.posixPermissionRetryNowMs()),
3074
+ );
3075
+ let retryWaits = 0;
3076
+
3077
+ return {
3078
+ wait() {
3079
+ const remaining = Math.min(
3080
+ remainingSleepBudget,
3081
+ deadline - _deps.posixPermissionRetryNowMs(),
3082
+ );
3083
+ if (!(remaining > 0)) return false;
3084
+ const retryDelayMs = Math.min(
3085
+ remaining,
3086
+ 10 * 2 ** Math.min(retryWaits, 4),
3087
+ );
3088
+ _deps.posixPermissionRetrySleep(retryDelayMs);
3089
+ remainingSleepBudget -= retryDelayMs;
3090
+ retryWaits += 1;
3091
+ return true;
3092
+ },
3093
+ hasRemaining() {
3094
+ return (
3095
+ remainingSleepBudget > 0 && _deps.posixPermissionRetryNowMs() < deadline
3096
+ );
3097
+ },
3098
+ };
3099
+ }
3100
+
3101
+ /**
3102
+ * Retry an observational failure before the first POSIX signal. Unknown
3103
+ * identity never authorizes a signal: only two consecutive delayed fresh
3104
+ * matches do. A dead/reused target converges without signalling, while every
3105
+ * non-observational identity failure remains immediately fail-closed.
3106
+ */
3107
+ function retryRecoverableInitialPosixIdentity({
3108
+ inspectIdentity,
3109
+ initialIdentity,
3110
+ deadline,
3111
+ }) {
3112
+ let lastIdentity = initialIdentity;
3113
+ let identityProbeRetries = 0;
3114
+ const retryBudget = createPosixProbeRetryBudget(deadline);
3115
+ const outcome = (status, retryStoppedBy) => ({
3116
+ status,
3117
+ lastIdentity,
3118
+ identityProbeRetries,
3119
+ retryStoppedBy,
3120
+ });
3121
+
3122
+ for (;;) {
3123
+ if (isDestructiveIdentityRetired(lastIdentity)) {
3124
+ return outcome("retired", "retired");
3125
+ }
3126
+ if (
3127
+ lastIdentity?.status !== "match" &&
3128
+ !isRecoverableDestructiveIdentityProbe(lastIdentity)
3129
+ ) {
3130
+ return outcome("failed", "preflight-identity");
3131
+ }
3132
+ if (!retryBudget.wait()) {
3133
+ return outcome("failed", "initial-identity-probe-deadline");
3134
+ }
3135
+
3136
+ const identityBeforeDelay = lastIdentity;
3137
+ identityProbeRetries += 1;
3138
+ lastIdentity = inspectIdentity();
3139
+
3140
+ if (isDestructiveIdentityRetired(lastIdentity)) {
3141
+ return outcome("retired", "retired");
3142
+ }
3143
+ if (
3144
+ lastIdentity?.status !== "match" &&
3145
+ !isRecoverableDestructiveIdentityProbe(lastIdentity)
3146
+ ) {
3147
+ return outcome("failed", "preflight-identity");
3148
+ }
3149
+ if (
3150
+ identityBeforeDelay?.status === "match" &&
3151
+ lastIdentity?.status === "match"
3152
+ ) {
3153
+ if (!retryBudget.hasRemaining()) {
3154
+ return outcome("failed", "initial-identity-probe-deadline");
3155
+ }
3156
+ return outcome("matched", "consecutive-match");
3157
+ }
3158
+ }
3159
+ }
3160
+
3161
+ /**
3162
+ * Recover from a transient POSIX EPERM without ever signalling from unknown
3163
+ * identity evidence. Probe-only waits may bridge a transient creation-time or
3164
+ * process-group-state lookup failure, but a resend requires two consecutive
3165
+ * fresh exact matches.
3166
+ */
3167
+ function retryExactPosixSignalAfterPermissionError({
3168
+ inspectIdentity,
3169
+ sendSignal,
3170
+ deadline,
3171
+ initialCause,
3172
+ }) {
3173
+ let cause = initialCause;
3174
+ let lastIdentity = inspectIdentity();
3175
+ let signalResends = 0;
3176
+ let identityProbeRetries = 0;
3177
+ const retryBudget = createPosixProbeRetryBudget(deadline);
3178
+
3179
+ const outcome = (status, retryStoppedBy, extra = {}) => ({
3180
+ status,
3181
+ cause,
3182
+ lastIdentity,
3183
+ signalResends,
3184
+ identityProbeRetries,
3185
+ retryStoppedBy,
3186
+ ...extra,
3187
+ });
3188
+
3189
+ for (;;) {
3190
+ if (isDestructiveIdentityRetired(lastIdentity)) {
3191
+ return outcome("retired", "retired");
3192
+ }
3193
+ if (
3194
+ lastIdentity?.status !== "match" &&
3195
+ !isRecoverableDestructiveIdentityProbe(lastIdentity)
3196
+ ) {
3197
+ return outcome("failed", "identity");
3198
+ }
3199
+
3200
+ if (!retryBudget.wait()) {
3201
+ return outcome(
3202
+ "failed",
3203
+ lastIdentity?.status === "match"
3204
+ ? "signal-retry-deadline"
3205
+ : "identity-probe-deadline",
3206
+ );
3207
+ }
3208
+
3209
+ const identityBeforeDelay = lastIdentity;
3210
+ identityProbeRetries += 1;
3211
+ lastIdentity = inspectIdentity();
3212
+
3213
+ if (isDestructiveIdentityRetired(lastIdentity)) {
3214
+ return outcome("retired", "retired");
3215
+ }
3216
+ if (
3217
+ lastIdentity?.status !== "match" &&
3218
+ !isRecoverableDestructiveIdentityProbe(lastIdentity)
3219
+ ) {
3220
+ return outcome("failed", "identity");
3221
+ }
3222
+
3223
+ // A target that moved from unknown to match gets one more probe-only
3224
+ // interval. Only match -> fresh match authorizes the next signal syscall.
3225
+ if (
3226
+ identityBeforeDelay?.status !== "match" ||
3227
+ lastIdentity?.status !== "match"
3228
+ ) {
3229
+ continue;
3230
+ }
3231
+ if (!retryBudget.hasRemaining()) {
3232
+ return outcome("failed", "signal-retry-deadline");
3233
+ }
3234
+ signalResends += 1;
3235
+ try {
3236
+ const signalResult = sendSignal();
3237
+ return outcome(
3238
+ signalResult === "already-exited" ? "retired" : "signalled",
3239
+ "completed",
3240
+ { signalResult },
3241
+ );
3242
+ } catch (error) {
3243
+ cause = error;
3244
+ if (error?.code !== "EPERM") {
3245
+ return outcome("failed", "signal");
3246
+ }
3247
+ // The failed resend has no authority value. Start the next decision from
3248
+ // a fresh probe, then require another delayed fresh match before signalling.
3249
+ lastIdentity = inspectIdentity();
3250
+ }
3251
+ }
3252
+ }
3253
+
3254
+ function signalExactBackgroundProcessTree(
3255
+ pid,
3256
+ signal,
3257
+ {
3258
+ platform = process.platform,
3259
+ expectedStartedAt,
3260
+ processGroup = true,
3261
+ allowDirectFallback = false,
3262
+ } = {},
3263
+ ) {
3264
+ const target = Number(pid);
3265
+ const mode = processGroup === true ? "process-group" : "direct";
3266
+ const posixRetryDeadline =
3267
+ _deps.posixPermissionRetryNowMs() +
3268
+ Math.max(0, Number(_deps.processExitWaitDeadlineMs) || 0);
3269
+ let initialSignalAttempts = 0;
3270
+ let signalResends = 0;
3271
+ let identityProbeRetries = 0;
3272
+ let lastIdentity = null;
3273
+ let retryStoppedBy = "preflight";
3274
+ const inspectIdentity = () =>
3275
+ inspectDestructiveProcessIdentity(target, expectedStartedAt, {
3276
+ processGroup,
3277
+ });
3278
+ const failure = (cause) => {
3279
+ const reason = cause?.message || String(cause);
3280
+ const error = new Error(
3281
+ `background_agent_child_tree_stop_failed: ${reason} (${mode} pid ${target}, initial signal attempts ${initialSignalAttempts}, signal resends ${signalResends}, identity probe retries ${identityProbeRetries}, last identity ${lastIdentity?.status || "not-probed"}/${lastIdentity?.reason || "no-reason"}, retry stopped by ${retryStoppedBy})`,
3282
+ { cause },
3283
+ );
3284
+ error.code = "ERR_BACKGROUND_AGENT_CHILD_TREE_STOP_FAILED";
3285
+ return error;
3286
+ };
3287
+
3288
+ // Unlike the legacy tree helper, the keeper's destructive path owns a
3289
+ // persisted creation-time anchor. Re-authorize the initial signal from a
3290
+ // fresh exact snapshot so a stale/reused PID can never address a stranger.
3291
+ lastIdentity = inspectIdentity();
3292
+ if (isDestructiveIdentityRetired(lastIdentity)) {
3293
+ return { alreadyRetired: true, signalledAsGroup: processGroup === true };
3294
+ }
3295
+ if (lastIdentity.status !== "match") {
3296
+ if (
3297
+ platform === "win32" ||
3298
+ !isRecoverableDestructiveIdentityProbe(lastIdentity)
3299
+ ) {
3300
+ retryStoppedBy = "preflight-identity";
3301
+ throw failure(
3302
+ new Error(
3303
+ `destructive process identity is ${lastIdentity.status}: ${lastIdentity.reason || "unknown"}`,
3304
+ ),
3305
+ );
3306
+ }
3307
+ const retry = retryRecoverableInitialPosixIdentity({
3308
+ inspectIdentity,
3309
+ initialIdentity: lastIdentity,
3310
+ deadline: posixRetryDeadline,
3311
+ });
3312
+ identityProbeRetries += retry.identityProbeRetries;
3313
+ lastIdentity = retry.lastIdentity;
3314
+ retryStoppedBy = retry.retryStoppedBy;
3315
+ if (retry.status === "retired") {
3316
+ return { alreadyRetired: true, signalledAsGroup: processGroup === true };
3317
+ }
3318
+ if (retry.status !== "matched") {
3319
+ throw failure(
3320
+ new Error(
3321
+ `destructive process identity is ${lastIdentity.status}: ${lastIdentity.reason || "unknown"}`,
3322
+ ),
3323
+ );
3324
+ }
3325
+ }
3326
+
3327
+ const sendSignal = () =>
3328
+ signalBackgroundProcessTree(target, signal, {
3329
+ platform,
3330
+ processGroup,
3331
+ allowDirectFallback,
3332
+ });
3333
+
3334
+ retryStoppedBy = "initial-signal";
3335
+ try {
3336
+ initialSignalAttempts += 1;
3337
+ const result = sendSignal();
3338
+ return {
3339
+ alreadyRetired: result === "already-exited",
3340
+ signalledAsGroup: result === "process-group",
3341
+ };
3342
+ } catch (initialCause) {
3343
+ let cause = initialCause;
3344
+ if (platform !== "win32" && cause?.code === "EPERM") {
3345
+ const retry = retryExactPosixSignalAfterPermissionError({
3346
+ inspectIdentity,
3347
+ sendSignal,
3348
+ deadline: posixRetryDeadline,
3349
+ initialCause: cause,
3350
+ });
3351
+ signalResends = retry.signalResends;
3352
+ identityProbeRetries += retry.identityProbeRetries;
3353
+ lastIdentity = retry.lastIdentity;
3354
+ retryStoppedBy = retry.retryStoppedBy;
3355
+ cause = retry.cause;
3356
+ if (retry.status === "retired") {
3357
+ return {
3358
+ alreadyRetired: true,
3359
+ signalledAsGroup: processGroup === true,
3360
+ };
3361
+ }
3362
+ if (retry.status === "signalled") {
3363
+ return {
3364
+ alreadyRetired: retry.signalResult === "already-exited",
3365
+ signalledAsGroup: retry.signalResult === "process-group",
3366
+ };
3367
+ }
3368
+ }
3369
+ if (
3370
+ processGroup === true &&
3371
+ allowDirectFallback !== true &&
3372
+ cause?.code === "ESRCH"
3373
+ ) {
3374
+ // A group may disappear between the exact preflight and signal. It is
3375
+ // safe to accept only a fresh dead/reused result; a still-live leader
3376
+ // is never converted to a direct kill on the keeper path.
3377
+ lastIdentity = inspectIdentity();
3378
+ if (isDestructiveIdentityRetired(lastIdentity)) {
3379
+ return {
3380
+ alreadyRetired: true,
3381
+ signalledAsGroup: true,
3382
+ };
3383
+ }
3384
+ retryStoppedBy = "identity";
3385
+ }
3386
+ throw failure(cause);
3387
+ }
3388
+ }
3389
+
2746
3390
  function persistStopPending(state, reason, details = {}) {
2747
3391
  const at = Date.now();
2748
3392
  const mutation = mutateBackgroundAgentState(state.id, (current) =>
@@ -2804,6 +3448,7 @@ function waitForSignalledProcessesToExit(
2804
3448
  const identity = inspectDestructiveProcessIdentity(
2805
3449
  target.pid,
2806
3450
  target.startedAt,
3451
+ { processGroup: target.signalledAsGroup },
2807
3452
  );
2808
3453
  return ["match", "unverifiable", "self"].includes(identity.status);
2809
3454
  });
@@ -2839,7 +3484,13 @@ export function stopBackgroundAgent(id) {
2839
3484
  if (!state) throw new Error(`Background agent not found: ${id}`);
2840
3485
  const staleHeartbeatOwner =
2841
3486
  state.status === "lost" && state.lostReason === "heartbeat-stale";
2842
- if (state.status !== "running" && !staleHeartbeatOwner) {
3487
+ const preexistingStopCleanupRepair =
3488
+ hasBackgroundAgentStopFenceWithoutCleanupProof(state);
3489
+ if (
3490
+ state.status !== "running" &&
3491
+ !staleHeartbeatOwner &&
3492
+ !preexistingStopCleanupRepair
3493
+ ) {
2843
3494
  // Gap 2: stopping an already-lost session still reaps a leaked agent
2844
3495
  // child recorded before the worker died (identity-guarded inside).
2845
3496
  if (state.status === "lost") {
@@ -2864,7 +3515,8 @@ export function stopBackgroundAgent(id) {
2864
3515
  (current.status !== "running" &&
2865
3516
  !(
2866
3517
  current.status === "lost" && current.lostReason === "heartbeat-stale"
2867
- ))
3518
+ ) &&
3519
+ !hasBackgroundAgentStopFenceWithoutCleanupProof(current))
2868
3520
  ) {
2869
3521
  return null;
2870
3522
  }
@@ -2883,6 +3535,10 @@ export function stopBackgroundAgent(id) {
2883
3535
  return { ...fence.state, stopped: false };
2884
3536
  }
2885
3537
  state = fence.state;
3538
+ const repairingUnconfirmedStop =
3539
+ preexistingStopCleanupRepair ||
3540
+ hasBackgroundAgentStopFenceWithoutCleanupProof(fence.previous) ||
3541
+ isBackgroundAgentStopCleanupUnconfirmed(state);
2886
3542
  if (state.launchFinalizationUncertain === true || state.turnLaunchIntent) {
2887
3543
  // The worker has durably announced that native spawn is prepared or that
2888
3544
  // post-spawn pid persistence is unresolved. Killing the worker from this
@@ -2921,6 +3577,7 @@ export function stopBackgroundAgent(id) {
2921
3577
  let postKill = state;
2922
3578
  let workerPidReused = false;
2923
3579
  const signalledPids = new Set();
3580
+ let activeTerminationContext = null;
2924
3581
  // A worker may publish a just-spawned child identity while the stop fence is
2925
3582
  // being observed. Re-read once after the first termination round so that a
2926
3583
  // newly durable, exact-owned agent tree is also terminated before recovery.
@@ -2944,7 +3601,17 @@ export function stopBackgroundAgent(id) {
2944
3601
  inspected.find(({ role }) => role === "worker") ||
2945
3602
  inspected.find(({ role }) => role === "worker-published") ||
2946
3603
  inspected.find(({ role }) => role === "worker-wrapper");
2947
- if (primaryWorker?.identity.status === "reused") workerPidReused = true;
3604
+ if (
3605
+ primaryWorker?.identity.status === "reused" &&
3606
+ !signalledPids.has(primaryWorker.pid)
3607
+ ) {
3608
+ // A fresh stop must refuse a worker pid that was already reused at the
3609
+ // first strict probe. If the exact worker matched and was signalled in an
3610
+ // earlier round, however, observing that numeric pid reused afterward is
3611
+ // evidence that the owned process retired, not grounds to discard the
3612
+ // durable cleanup proof.
3613
+ workerPidReused = true;
3614
+ }
2948
3615
  const targets = inspected.filter(
2949
3616
  ({ pid, identity }) =>
2950
3617
  identity.status === "match" && !signalledPids.has(pid),
@@ -2952,14 +3619,163 @@ export function stopBackgroundAgent(id) {
2952
3619
  if (targets.length === 0) break;
2953
3620
  try {
2954
3621
  // The child is detached into its own group and therefore gets its own
2955
- // exact-identity signal before the worker tree is terminated.
3622
+ // exact-identity signal before the worker tree is terminated. The
3623
+ // turn's detached agent root owns its tool descendants; the worker and
3624
+ // independent keeper do not. Signal those single processes directly so
3625
+ // a rapidly recycled pid cannot address an unrelated POSIX group.
2956
3626
  for (const target of targets) {
2957
- target.signalledAsGroup =
2958
- signalBackgroundProcessTree(target.pid, "SIGTERM") ===
2959
- "process-group";
2960
- signalledPids.add(target.pid);
3627
+ let permissionRetryDeadline = 0;
3628
+ const inspectIdentity = () =>
3629
+ inspectDestructiveProcessIdentity(target.pid, target.startedAt, {
3630
+ processGroup: target.signalAsGroup,
3631
+ });
3632
+ const sendInitialSignal = () =>
3633
+ signalBackgroundProcessTree(target.pid, "SIGTERM", {
3634
+ processGroup: target.signalAsGroup,
3635
+ // Preserve the pre-existing ESRCH behavior. EPERM itself never
3636
+ // enters this fallback because the tree primitive rethrows it.
3637
+ allowDirectFallback: true,
3638
+ });
3639
+ const resendSignal = () =>
3640
+ signalBackgroundProcessTree(target.pid, "SIGTERM", {
3641
+ processGroup: target.signalAsGroup,
3642
+ // After EPERM, every resend is a newly authorized action. A group
3643
+ // ESRCH must be resolved by fresh identity evidence, never by
3644
+ // changing that action into a direct leader signal.
3645
+ allowDirectFallback: false,
3646
+ });
3647
+ activeTerminationContext = {
3648
+ role: target.role,
3649
+ pid: target.pid,
3650
+ mode: target.signalAsGroup ? "process-group" : "direct",
3651
+ signalResends: 0,
3652
+ identityProbeRetries: 0,
3653
+ };
3654
+ try {
3655
+ let preflightIdentity = inspectIdentity();
3656
+ activeTerminationContext.lastIdentityStatus =
3657
+ preflightIdentity.status;
3658
+ activeTerminationContext.lastIdentityReason =
3659
+ preflightIdentity.reason || null;
3660
+ if (isDestructiveIdentityRetired(preflightIdentity)) {
3661
+ if (
3662
+ preflightIdentity.status === "reused" &&
3663
+ primaryWorker?.pid === target.pid &&
3664
+ !signalledPids.has(target.pid)
3665
+ ) {
3666
+ workerPidReused = true;
3667
+ }
3668
+ target.retiredBeforeWait = true;
3669
+ signalledPids.add(target.pid);
3670
+ continue;
3671
+ }
3672
+ if (preflightIdentity.status !== "match") {
3673
+ if (
3674
+ process.platform === "win32" ||
3675
+ !isRecoverableDestructiveIdentityProbe(preflightIdentity)
3676
+ ) {
3677
+ activeTerminationContext.retryStoppedBy = "preflight-identity";
3678
+ throw new Error(
3679
+ `destructive process identity is ${preflightIdentity.status}: ${preflightIdentity.reason || "unknown"}`,
3680
+ );
3681
+ }
3682
+ permissionRetryDeadline =
3683
+ _deps.posixPermissionRetryNowMs() +
3684
+ Math.max(0, Number(_deps.processExitWaitDeadlineMs) || 0);
3685
+ const retry = retryRecoverableInitialPosixIdentity({
3686
+ inspectIdentity,
3687
+ initialIdentity: preflightIdentity,
3688
+ deadline: permissionRetryDeadline,
3689
+ });
3690
+ activeTerminationContext.identityProbeRetries +=
3691
+ retry.identityProbeRetries;
3692
+ preflightIdentity = retry.lastIdentity;
3693
+ activeTerminationContext.lastIdentityStatus =
3694
+ preflightIdentity?.status;
3695
+ activeTerminationContext.lastIdentityReason =
3696
+ preflightIdentity?.reason || null;
3697
+ activeTerminationContext.retryStoppedBy = retry.retryStoppedBy;
3698
+ if (retry.status === "retired") {
3699
+ if (
3700
+ preflightIdentity?.status === "reused" &&
3701
+ primaryWorker?.pid === target.pid &&
3702
+ !signalledPids.has(target.pid)
3703
+ ) {
3704
+ workerPidReused = true;
3705
+ }
3706
+ target.retiredBeforeWait = true;
3707
+ signalledPids.add(target.pid);
3708
+ continue;
3709
+ }
3710
+ if (retry.status !== "matched") {
3711
+ throw new Error(
3712
+ `destructive process identity is ${preflightIdentity?.status || "unknown"}: ${preflightIdentity?.reason || "unknown"}`,
3713
+ );
3714
+ }
3715
+ }
3716
+ if (!(permissionRetryDeadline > 0)) {
3717
+ permissionRetryDeadline =
3718
+ _deps.posixPermissionRetryNowMs() +
3719
+ Math.max(0, Number(_deps.processExitWaitDeadlineMs) || 0);
3720
+ }
3721
+ activeTerminationContext.retryStoppedBy = "initial-signal";
3722
+ target.signalledAsGroup = sendInitialSignal() === "process-group";
3723
+ signalledPids.add(target.pid);
3724
+ } catch (error) {
3725
+ if (process.platform !== "win32" && error?.code === "EPERM") {
3726
+ const retry = retryExactPosixSignalAfterPermissionError({
3727
+ inspectIdentity,
3728
+ sendSignal: resendSignal,
3729
+ deadline: permissionRetryDeadline,
3730
+ initialCause: error,
3731
+ });
3732
+ activeTerminationContext.signalResends = retry.signalResends;
3733
+ activeTerminationContext.identityProbeRetries =
3734
+ retry.identityProbeRetries;
3735
+ activeTerminationContext.lastIdentityStatus =
3736
+ retry.lastIdentity?.status;
3737
+ activeTerminationContext.lastIdentityReason =
3738
+ retry.lastIdentity?.reason || null;
3739
+ activeTerminationContext.retryStoppedBy = retry.retryStoppedBy;
3740
+ if (retry.status === "retired") {
3741
+ target.signalledAsGroup = retry.signalResult
3742
+ ? retry.signalResult === "process-group"
3743
+ : target.signalAsGroup === true;
3744
+ signalledPids.add(target.pid);
3745
+ continue;
3746
+ }
3747
+ if (retry.status === "signalled") {
3748
+ target.signalledAsGroup = retry.signalResult === "process-group";
3749
+ signalledPids.add(target.pid);
3750
+ continue;
3751
+ }
3752
+ if (
3753
+ target.signalAsGroup === true &&
3754
+ retry.cause?.code === "ESRCH"
3755
+ ) {
3756
+ const refreshedIdentity = inspectIdentity();
3757
+ activeTerminationContext.lastIdentityStatus =
3758
+ refreshedIdentity.status;
3759
+ activeTerminationContext.lastIdentityReason =
3760
+ refreshedIdentity.reason || null;
3761
+ activeTerminationContext.retryStoppedBy = "identity";
3762
+ if (isDestructiveIdentityRetired(refreshedIdentity)) {
3763
+ target.signalledAsGroup = true;
3764
+ signalledPids.add(target.pid);
3765
+ continue;
3766
+ }
3767
+ }
3768
+ throw retry.cause;
3769
+ }
3770
+ throw error;
3771
+ }
2961
3772
  }
2962
3773
  } catch (error) {
3774
+ const failureMessage = `Failed to stop background agent ${id}${
3775
+ activeTerminationContext
3776
+ ? ` (${activeTerminationContext.role} pid ${activeTerminationContext.pid}, ${activeTerminationContext.mode}, signal resends ${activeTerminationContext.signalResends}, identity probe retries ${activeTerminationContext.identityProbeRetries}, last identity ${activeTerminationContext.lastIdentityStatus || "not-probed"}/${activeTerminationContext.lastIdentityReason || "no-reason"}, retry stopped by ${activeTerminationContext.retryStoppedBy || "non-permission-error"})`
3777
+ : ""
3778
+ }: ${error.message}`;
2963
3779
  try {
2964
3780
  mutateBackgroundAgentState(id, (current) =>
2965
3781
  current
@@ -2968,18 +3784,18 @@ export function stopBackgroundAgent(id) {
2968
3784
  phase: "stop_failed",
2969
3785
  stopPending: true,
2970
3786
  stopPendingReason: "process-termination",
2971
- stopError: error?.message || String(error),
3787
+ stopError: failureMessage,
2972
3788
  }
2973
3789
  : null,
2974
3790
  );
2975
3791
  } catch {
2976
3792
  /* the durable stopRequestedAt fence already blocks new turns */
2977
3793
  }
2978
- throw new Error(
2979
- `Failed to stop background agent ${id}: ${error.message}`,
2980
- );
3794
+ throw new Error(failureMessage);
2981
3795
  }
2982
- const stillAlive = waitForSignalledProcessesToExit(targets);
3796
+ const stillAlive = waitForSignalledProcessesToExit(
3797
+ targets.filter((target) => target.retiredBeforeWait !== true),
3798
+ );
2983
3799
  postKill = readBackgroundAgentState(id) || postKill;
2984
3800
  if (stillAlive.length > 0) {
2985
3801
  return persistStopPending(postKill, "process-exit", {
@@ -2994,7 +3810,7 @@ export function stopBackgroundAgent(id) {
2994
3810
  return persistStopPending(postKill, "process-exit");
2995
3811
  }
2996
3812
 
2997
- if (workerPidReused) {
3813
+ if (workerPidReused && !repairingUnconfirmedStop) {
2998
3814
  const lostMutation = mutateBackgroundAgentState(id, (current) =>
2999
3815
  current
3000
3816
  ? {
@@ -3052,6 +3868,12 @@ export function stopBackgroundAgent(id) {
3052
3868
  ...current,
3053
3869
  status: "stopped",
3054
3870
  endedAt: current.endedAt || Date.now(),
3871
+ // `endedAt` belongs to the worker/session lifecycle and may predate
3872
+ // retirement of a detached child. Publish a distinct durable proof
3873
+ // only after every exact-owned process identity has been observed
3874
+ // non-executable above.
3875
+ stopCleanupConfirmedAt:
3876
+ backgroundAgentStopCleanupConfirmationTimestamp(current),
3055
3877
  stoppedByUser: true,
3056
3878
  lostReason: null,
3057
3879
  phase: null,