chainlesschain 0.163.2 → 0.163.4

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 (222) hide show
  1. package/package.json +3 -2
  2. package/scripts/command-lifecycle-report.mjs +330 -38
  3. package/src/assets/web-panel/assets/{AIOps-DnGsmsqJ.js → AIOps-DB5X7qqV.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-CrO6fcQ7.js → ActionButton-DxHRtKLk.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-DsBdOaP2.js → Analytics-DIH5QrxT.js} +3 -3
  6. package/src/assets/web-panel/assets/{AppLayout-DmQslKUB.js → AppLayout-uu0oxVz7.js} +5 -5
  7. package/src/assets/web-panel/assets/{Artifacts-BvyrT_QQ.js → Artifacts-BNSMiW9Y.js} +1 -1
  8. package/src/assets/web-panel/assets/{Audit-prndoLiC.js → Audit-DcAzB4_x.js} +1 -1
  9. package/src/assets/web-panel/assets/{BackgroundAgents-DOTFyA55.js → BackgroundAgents-ClSsvjIc.js} +1 -1
  10. package/src/assets/web-panel/assets/{Backup-9zOnPp_8.js → Backup-BMciY0js.js} +1 -1
  11. package/src/assets/web-panel/assets/{BaseInput-C4PC6sAG.js → BaseInput-Hn3b57Sh.js} +1 -1
  12. package/src/assets/web-panel/assets/{Chat-BBAF-W9v.js → Chat-CoSlC53V.js} +6 -6
  13. package/src/assets/web-panel/assets/{ChatBubbleRenderer-CWhvvloO.js → ChatBubbleRenderer-CFevJ3So.js} +1 -1
  14. package/src/assets/web-panel/assets/{Checkbox-BI9iculd.js → Checkbox-B4L4Eu4w.js} +1 -1
  15. package/src/assets/web-panel/assets/{Codegen-3DP9BCKb.js → Codegen-C77FgRxF.js} +1 -1
  16. package/src/assets/web-panel/assets/{Col-df-2TnJX.js → Col-BrLtJLH3.js} +1 -1
  17. package/src/assets/web-panel/assets/{Community-CHs8gG_A.js → Community-Be7pXXhA.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compact-CDfF6q7V.js → Compact-DuGPecjo.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compliance-CthrDnO7.js → Compliance-CzS66mjP.js} +1 -1
  20. package/src/assets/web-panel/assets/{Cowork-HjIqsWJw.js → Cowork-CEhjCoyK.js} +2 -2
  21. package/src/assets/web-panel/assets/{Cron-BKc196fb.js → Cron-D7ir8Vja.js} +2 -2
  22. package/src/assets/web-panel/assets/{Crosschain-CJnjdc8W.js → Crosschain-BcXmuLxf.js} +1 -1
  23. package/src/assets/web-panel/assets/{DID-BHUy-8Q2.js → DID-Cto1dtBy.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dashboard-QDB7sKeY.js → Dashboard-B7KjgwOX.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dropdown-DStcVK11.js → Dropdown-COMqxijg.js} +1 -1
  26. package/src/assets/web-panel/assets/{EmailListRenderer-BwSUskMJ.js → EmailListRenderer-BZrya-nf.js} +1 -1
  27. package/src/assets/web-panel/assets/{FamilyGuardDashboard-DgF_kurT.js → FamilyGuardDashboard-AmVeMq0J.js} +1 -1
  28. package/src/assets/web-panel/assets/{Federation-k8wvv-wN.js → Federation-kZU8c-27.js} +1 -1
  29. package/src/assets/web-panel/assets/{FormItemContext-C11qoq2r.js → FormItemContext-sOV22v2-.js} +1 -1
  30. package/src/assets/web-panel/assets/{GenericCardRenderer-BIIpr9kg.js → GenericCardRenderer-BUuLtGCz.js} +1 -1
  31. package/src/assets/web-panel/assets/{Git-BlA1exTe.js → Git-K-ZxsLsH.js} +2 -2
  32. package/src/assets/web-panel/assets/{Governance-CYa1ZYM4.js → Governance-D3OEnawI.js} +1 -1
  33. package/src/assets/web-panel/assets/{Inference-6_kKVIt6.js → Inference-Bll0gRmP.js} +1 -1
  34. package/src/assets/web-panel/assets/{KnowledgeGraph-CB4AwxYy.js → KnowledgeGraph-DBXwjr53.js} +1 -1
  35. package/src/assets/web-panel/assets/{Logs-xf7nHFAe.js → Logs-CEzPwEfP.js} +2 -2
  36. package/src/assets/web-panel/assets/{MarkdownRenderer-Bf3DEqP6.js → MarkdownRenderer-CLmCbHx6.js} +1 -1
  37. package/src/assets/web-panel/assets/{Marketplace-aPg8e4Jt.js → Marketplace-BYyRyOPz.js} +1 -1
  38. package/src/assets/web-panel/assets/{McpTools-C-TNlVUf.js → McpTools-DVu8w_rM.js} +5 -5
  39. package/src/assets/web-panel/assets/{Memory-DMTPIpGa.js → Memory-tenuZ_hO.js} +2 -2
  40. package/src/assets/web-panel/assets/{MobileBridge-BxyvveRq.js → MobileBridge-Bxgy4ozS.js} +1 -1
  41. package/src/assets/web-panel/assets/{MobileProjects-DyPCrAyQ.js → MobileProjects-Dgx1D6sc.js} +1 -1
  42. package/src/assets/web-panel/assets/{Mtc-BdhqNXp6.js → Mtc-CRfJ8r8j.js} +5 -5
  43. package/src/assets/web-panel/assets/{MtcAudit-Cz-HFxzW.js → MtcAudit-BBJq4Aiz.js} +5 -5
  44. package/src/assets/web-panel/assets/{Multisig-CIUaFNsx.js → Multisig-B3ii9lai.js} +3 -3
  45. package/src/assets/web-panel/assets/{NLProgramming-CWe6-J17.js → NLProgramming-FIi-Cubv.js} +1 -1
  46. package/src/assets/web-panel/assets/{Notes-vYAKeviU.js → Notes-DIq8MPf1.js} +3 -3
  47. package/src/assets/web-panel/assets/{NotificationSettings-DQL9-2d7.js → NotificationSettings-BFOD9eGd.js} +1 -1
  48. package/src/assets/web-panel/assets/{OrderTableRenderer-DnXj1jBn.js → OrderTableRenderer--41u7vre.js} +1 -1
  49. package/src/assets/web-panel/assets/{Organization-B3spU8AN.js → Organization-B_0AWztW.js} +2 -2
  50. package/src/assets/web-panel/assets/{Overflow-DgEwo7vm.js → Overflow-B6I7TuXv.js} +1 -1
  51. package/src/assets/web-panel/assets/{P2P-hujMRH8X.js → P2P-Cdf6qens.js} +2 -2
  52. package/src/assets/web-panel/assets/{PdhVaultBrowser-C_qhdg9h.js → PdhVaultBrowser-B_0FcQJY.js} +3 -3
  53. package/src/assets/web-panel/assets/{Permissions-vp7LMcSf.js → Permissions-6nFBjsQp.js} +4 -4
  54. package/src/assets/web-panel/assets/{PersonalDataHub-Dxr5Hgnq.js → PersonalDataHub-nQYV7xpn.js} +3 -3
  55. package/src/assets/web-panel/assets/{Pipeline-DEPZqqH4.js → Pipeline-CBVod5jm.js} +1 -1
  56. package/src/assets/web-panel/assets/{Privacy-DTpDiKPe.js → Privacy-D52jW1Nt.js} +1 -1
  57. package/src/assets/web-panel/assets/{ProjectInit-jDJsRWmF.js → ProjectInit-Bdx3BYhf.js} +2 -2
  58. package/src/assets/web-panel/assets/{ProjectSettings-Dhor5RWH.js → ProjectSettings-b7xr4DUa.js} +2 -2
  59. package/src/assets/web-panel/assets/Projects-DhfifCKx.js +1 -0
  60. package/src/assets/web-panel/assets/Providers-Dy_PgDMr.js +1 -0
  61. package/src/assets/web-panel/assets/{QrScannerModal-CjIw-wIb.js → QrScannerModal-FQKSmc3e.js} +1 -1
  62. package/src/assets/web-panel/assets/{QuickAsk-DX6jcqoi.js → QuickAsk-CSTrhzcF.js} +1 -1
  63. package/src/assets/web-panel/assets/{Recommend-DopXkhar.js → Recommend-CxMABOUh.js} +1 -1
  64. package/src/assets/web-panel/assets/{RemoteSession-BLvqJE9c.js → RemoteSession-Cv2T8yRa.js} +2 -2
  65. package/src/assets/web-panel/assets/{Reputation-DhRj2Me7.js → Reputation-CE1k4Vvu.js} +1 -1
  66. package/src/assets/web-panel/assets/{Row-DQ53vuPq.js → Row-BNgNcAV_.js} +1 -1
  67. package/src/assets/web-panel/assets/{RssFeed-BslvwiBD.js → RssFeed-5ZiBZ3ph.js} +3 -3
  68. package/src/assets/web-panel/assets/{Search-DJKBDNKZ.js → Search-mDOIF1UV.js} +1 -1
  69. package/src/assets/web-panel/assets/{Security-COhpxHER.js → Security-CtuCTC-e.js} +3 -3
  70. package/src/assets/web-panel/assets/{Services-dbulD6cI.js → Services-DxFO2Zg7.js} +2 -2
  71. package/src/assets/web-panel/assets/{Skeleton-ciIVNfhC.js → Skeleton-CKmKkH9y.js} +1 -1
  72. package/src/assets/web-panel/assets/{Skills-AiRTkTCk.js → Skills-Bmzl9qkk.js} +1 -1
  73. package/src/assets/web-panel/assets/{Sla-DDMcWYBc.js → Sla-9RqaWHOC.js} +1 -1
  74. package/src/assets/web-panel/assets/{SpeechSettings-BPxWbpXO.js → SpeechSettings-CGdzFIYw.js} +1 -1
  75. package/src/assets/web-panel/assets/{SyncSettings-BoEOXCyg.js → SyncSettings-CvNuCeBT.js} +2 -2
  76. package/src/assets/web-panel/assets/Tasks-B_sLdcnw.js +1 -0
  77. package/src/assets/web-panel/assets/{Templates-UowY8Ixe.js → Templates-D9_YijdK.js} +1 -1
  78. package/src/assets/web-panel/assets/{Tenant-DauSLQXN.js → Tenant-Cuc7BUSP.js} +1 -1
  79. package/src/assets/web-panel/assets/{Terminal-DBj7Jneq.js → Terminal-6VYRnJgd.js} +2 -2
  80. package/src/assets/web-panel/assets/{TimelineRenderer-UMrPwoZC.js → TimelineRenderer-GtK8xd8Y.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tokens-4ea96-bn.js → Tokens-BZ94Lt5W.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trigger--ISCqt4b.js → Trigger-DEcZgDaX.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trust-D4GVimRJ.js → Trust-CC3z9THe.js} +1 -1
  84. package/src/assets/web-panel/assets/{UkeySign-Ci-nXRQc.js → UkeySign-Cvtw8cV3.js} +1 -1
  85. package/src/assets/web-panel/assets/{VideoEditing-Cjk7mKPw.js → VideoEditing-CL4e6Bn3.js} +1 -1
  86. package/src/assets/web-panel/assets/{Wallet-BgOLOO3Z.js → Wallet-ycEemP27.js} +2 -2
  87. package/src/assets/web-panel/assets/{WebAuthn-CCX0K29i.js → WebAuthn-DgZ4X_aw.js} +5 -5
  88. package/src/assets/web-panel/assets/{WorkflowEditor-Ch3zgIsE.js → WorkflowEditor-D0qMhSv7.js} +1 -1
  89. package/src/assets/web-panel/assets/{chat-DKOU_2Vb.js → chat-DsAg01Zn.js} +1 -1
  90. package/src/assets/web-panel/assets/{colors-6OVo4u3C.js → colors-B0oqAhrQ.js} +1 -1
  91. package/src/assets/web-panel/assets/{compact-item-BH78Hlsq.js → compact-item-CmX0Rabz.js} +1 -1
  92. package/src/assets/web-panel/assets/{createContext-CFRpSbbC.js → createContext-Be4cn1Ub.js} +1 -1
  93. package/src/assets/web-panel/assets/devWarning-CP2qzG6D.js +1 -0
  94. package/src/assets/web-panel/assets/{hasIn-DNQhgcVi.js → hasIn-D5v12kVq.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-BkN3iTTp.js → index-0eWLkQKT.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-CkNEMANR.js → index-301ogenv.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-D3RtTT4K.js → index-5LGY0JbG.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-dDkoL4Pe.js → index-7VKDn-t1.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-BHa4WyTZ.js → index-8WvYGnON.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-CaYqY35Z.js → index-B1YGjOWC.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-bKm3TUDp.js → index-B3mUHuVc.js} +1 -1
  102. package/src/assets/web-panel/assets/{index--rq90rL9.js → index-BA4oulaP.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DaDgdJYj.js → index-BBREnn1V.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-B_Z-YoxX.js → index-BBvyHguy.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-CkqHgG-b.js → index-BKmsS2jj.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-CrcLHN2F.js → index-BRIqLy1m.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-BaVE6E8y.js → index-BXGSbelg.js} +3 -3
  108. package/src/assets/web-panel/assets/{index-BYrnm5Ob.js → index-BaE0Ui07.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-BDSR7ecX.js → index-BoAIAZ5C.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-CNuCHHQT.js → index-BqltWojQ.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-Eag8e_mA.js → index-CGiIyTZw.js} +1 -1
  112. package/src/assets/web-panel/assets/index-CKB0vbJU.js +1 -0
  113. package/src/assets/web-panel/assets/{index-BepBTpa8.js → index-CZ2wUEgr.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-HNGGVF6o.js → index-CZpfzcuy.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-AuBhLqOm.js → index-Ckrk3pIV.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-7TC3S8SN.js → index-CqoCZ5vR.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-CFPJ8yzF.js → index-CyWBir7N.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-Bb6kDrUx.js → index-CyyGTMKd.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DKN2noIF.js → index-D0BSkxYQ.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-Du0rnksC.js → index-D5YVopyr.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-BxRy5LPR.js → index-DKUBj36_.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-sm7W8AXy.js → index-DM724NgU.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-BujsOZfc.js → index-DQRsQen0.js} +1 -1
  124. package/src/assets/web-panel/assets/index-DgoXmwqB.js +1 -0
  125. package/src/assets/web-panel/assets/{index-Y9MJv28e.js → index-DikJ0Qqe.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-caye7AOE.js → index-Dn2dSdXx.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-cdf7fd7d.js → index-DyUaFWEk.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-BnnXQot_.js → index-Hfe_dEGx.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-DvHGtNnF.js → index-_JjRLSD0.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-nbfHl1oV.js → index-frSojpHj.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-Cu3L4r5l.js → index-jK3ax6Z6.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-D84id2Y4.js → index-qMuZ-NRR.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-hPY0fFrd.js → index-q_fQ94SE.js} +1 -1
  134. package/src/assets/web-panel/assets/{initDefaultProps-BZ9w6jPL.js → initDefaultProps-D4VdhJ9_.js} +1 -1
  135. package/src/assets/web-panel/assets/{motion-BnwUiOho.js → motion-CZJA0MQW.js} +1 -1
  136. package/src/assets/web-panel/assets/{move-ByUKJnNm.js → move-BqEs7Sb1.js} +1 -1
  137. package/src/assets/web-panel/assets/{mtc-parser-V7yK1KEl.js → mtc-parser-BpHEtsu1.js} +1 -1
  138. package/src/assets/web-panel/assets/{omit-BEXBBt4j.js → omit-BW94tY9W.js} +1 -1
  139. package/src/assets/web-panel/assets/{pickAttrs-qDm_46Gi.js → pickAttrs-BnBjPWP9.js} +1 -1
  140. package/src/assets/web-panel/assets/{placementArrow-BIbhEo_0.js → placementArrow-uUkjQ_hu.js} +1 -1
  141. package/src/assets/web-panel/assets/{responsiveObserve-NbPbDgA_.js → responsiveObserve-B-klUqXK.js} +1 -1
  142. package/src/assets/web-panel/assets/{slide-BnUWglZ0.js → slide-CvY1cNAs.js} +1 -1
  143. package/src/assets/web-panel/assets/{statusUtils-BX8_wZI5.js → statusUtils-Ix5sEDT2.js} +1 -1
  144. package/src/assets/web-panel/assets/{styleChecker-BV4OycQX.js → styleChecker-C8qcXOne.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFlexGapSupport-Od_H5llY.js → useFlexGapSupport-KNF6Kj8f.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFs-BWI-PyiX.js → useFs-DGJr345n.js} +1 -1
  147. package/src/assets/web-panel/assets/{usePersonalDataHub-BqMuhG3g.js → usePersonalDataHub-BboN99rI.js} +1 -1
  148. package/src/assets/web-panel/assets/{vnode-DNd23z6f.js → vnode-Bgt8Vgte.js} +1 -1
  149. package/src/assets/web-panel/assets/{zoom-Cjaxnm7l.js → zoom-BhJtErgt.js} +1 -1
  150. package/src/assets/web-panel/index.html +1 -1
  151. package/src/command-help-index.json +2 -2
  152. package/src/commands/agenda.js +6 -5
  153. package/src/commands/agent.js +237 -75
  154. package/src/commands/agents.js +6 -0
  155. package/src/commands/artifacts.js +98 -0
  156. package/src/commands/background-session.js +11 -1
  157. package/src/commands/mcp.js +218 -31
  158. package/src/commands/review.js +221 -22
  159. package/src/commands/session-mcp-recovery.js +5 -5
  160. package/src/data/changelog.json +27 -1
  161. package/src/gateways/ws/ws-agent-handler.js +492 -14
  162. package/src/harness/jsonl-session-store.js +39 -1
  163. package/src/harness/mcp-client.js +414 -71
  164. package/src/harness/prompt-compressor.js +24 -0
  165. package/src/harness/provider-backed-compaction.js +205 -0
  166. package/src/harness/worktree-isolator.js +26 -3
  167. package/src/lib/agent-worktree.js +41 -2
  168. package/src/lib/app-preview.js +3 -0
  169. package/src/lib/background-agent-supervisor.js +790 -186
  170. package/src/lib/background-command-argv.js +318 -0
  171. package/src/lib/background-launch-profile.js +1 -0
  172. package/src/lib/background-phase-reporter.js +22 -2
  173. package/src/lib/background-worktree-policy.js +109 -0
  174. package/src/lib/command-lifecycle-report.js +1340 -145
  175. package/src/lib/cowork-mcp-tools.js +51 -6
  176. package/src/lib/cowork-task-runner.js +1 -0
  177. package/src/lib/delivery-coordinator.js +30 -5
  178. package/src/lib/delivery-production-adapter.js +1777 -0
  179. package/src/lib/delivery-production-runner.js +240 -0
  180. package/src/lib/durable-security-store.js +29 -17
  181. package/src/lib/execution-trace/trace-context.cjs +179 -0
  182. package/src/lib/execution-trace/trace-context.js +8 -175
  183. package/src/lib/mcp-call-ledger-store.js +58 -6
  184. package/src/lib/mcp-headers-helper.js +76 -8
  185. package/src/lib/mcp-host-recovery-runtime.js +2 -0
  186. package/src/lib/mcp-ledger-recovery-admission.js +39 -3
  187. package/src/lib/mcp-recovery-adjudication.js +256 -19
  188. package/src/lib/mcp-sandbox-policy.js +184 -0
  189. package/src/lib/mcp-stdio-capsule-builder-worker.cjs +495 -0
  190. package/src/lib/mcp-stdio-executable-identity.js +33 -2
  191. package/src/lib/mcp-stdio-execution-authority.js +163 -0
  192. package/src/lib/mcp-stdio-immutable-vfs-resolver.cjs +830 -0
  193. package/src/lib/mcp-stdio-package-materialization.js +721 -333
  194. package/src/lib/mcp-stdio-workspace-authority.js +252 -0
  195. package/src/lib/observability/index.js +2 -0
  196. package/src/lib/observability/otlp-exporter.js +2 -2
  197. package/src/lib/packer/pack-update-applier.js +8 -1
  198. package/src/lib/packer/pkg-config-generator.js +34 -0
  199. package/src/lib/plugin-runtime/sandbox-policy.js +42 -2
  200. package/src/lib/process-execution-broker/index.js +213 -109
  201. package/src/lib/process-execution-broker/platform-sandbox.js +1024 -169
  202. package/src/lib/process-tree-termination.js +17 -1
  203. package/src/lib/scheduler-kernel/contract.js +315 -0
  204. package/src/lib/scheduler-kernel/store.js +1345 -0
  205. package/src/lib/session-host-lease.js +403 -30
  206. package/src/lib/session-persistence-failure.js +4 -1
  207. package/src/lib/skill-mcp.js +65 -4
  208. package/src/lib/sub-agent-context.js +8 -0
  209. package/src/lib/web-fetch.js +4 -4
  210. package/src/repl/agent-repl.js +285 -38
  211. package/src/runtime/agent-core.js +1044 -90
  212. package/src/runtime/agent-runtime.js +61 -8
  213. package/src/runtime/headless-runner.js +260 -166
  214. package/src/runtime/headless-stream.js +254 -25
  215. package/src/runtime/mcp-config.js +235 -28
  216. package/src/workers/background-agent-worker.js +492 -71
  217. package/src/assets/web-panel/assets/Projects-08xUDZGt.js +0 -1
  218. package/src/assets/web-panel/assets/Providers-BGs47s1a.js +0 -1
  219. package/src/assets/web-panel/assets/Tasks-CEYFJwmr.js +0 -1
  220. package/src/assets/web-panel/assets/devWarning-DEMblAIm.js +0 -1
  221. package/src/assets/web-panel/assets/index-BTvq-36D.js +0 -1
  222. package/src/assets/web-panel/assets/index-hUpvk6Jl.js +0 -1
@@ -15,6 +15,7 @@ import { randomBytes } from "node:crypto";
15
15
  import { join, resolve } from "node:path";
16
16
  import { fileURLToPath } from "node:url";
17
17
  import { getHomeDir } from "./paths.js";
18
+ import { withFileLock } from "./with-file-lock.js";
18
19
  import { deriveSessionState } from "./session-lifecycle.js";
19
20
  import {
20
21
  finishAgentWorktree,
@@ -22,6 +23,11 @@ import {
22
23
  } from "./agent-worktree.js";
23
24
  import { rejectPendingBackgroundInteractions } from "./background-interaction-journal.js";
24
25
  import executionBroker from "./process-execution-broker/index.js";
26
+ import {
27
+ agentPrintArgument,
28
+ stripFirstTurnPromptArgv,
29
+ } from "./background-command-argv.js";
30
+ import { terminateOwnedProcessTree } from "./process-tree-termination.js";
25
31
  import {
26
32
  assessBackgroundLaunchProfileCompatibility,
27
33
  buildArgvFromBackgroundLaunchProfile,
@@ -209,6 +215,7 @@ export const _deps = {
209
215
  kill: (pid, signal) => process.kill(pid, signal),
210
216
  readProcessStartTimeMs: defaultReadProcessStartTimeMs,
211
217
  killProcessTree: defaultKillProcessTree,
218
+ terminateOwnedProcessTree,
212
219
  };
213
220
 
214
221
  /**
@@ -316,8 +323,27 @@ export function logPath(id) {
316
323
 
317
324
  const TERMINAL_STATUSES = new Set(["completed", "failed", "stopped", "lost"]);
318
325
 
319
- export function writeBackgroundAgentState(state) {
320
- const target = statePath(state.id);
326
+ function readBackgroundAgentStateResult(id) {
327
+ const target = statePath(id);
328
+ if (!existsSync(target)) return { state: null, missing: true, error: null };
329
+ try {
330
+ const state = JSON.parse(readFileSync(target, "utf8"));
331
+ if (!state || typeof state !== "object" || state.id !== id) {
332
+ throw new Error(`State record does not belong to ${id}`);
333
+ }
334
+ return { state, missing: false, error: null };
335
+ } catch (cause) {
336
+ if (cause?.code === "ENOENT") {
337
+ return { state: null, missing: true, error: null };
338
+ }
339
+ const error = new Error(`Corrupt background agent state: ${id}`, { cause });
340
+ error.code = "BACKGROUND_AGENT_STATE_CORRUPT";
341
+ return { state: null, missing: false, error };
342
+ }
343
+ }
344
+
345
+ function mergeBackgroundAgentState(current, requested) {
346
+ let next = requested;
321
347
  // Field-aware merge against the freshest on-disk state. The state file has
322
348
  // multiple concurrent read-modify-write writers (launcher, worker heartbeat/
323
349
  // turn/finalize, rename/pin/stop from other processes) with last-writer-wins
@@ -332,10 +358,8 @@ export function writeBackgroundAgentState(state) {
332
358
  // The read happens immediately before the atomic rename, shrinking the
333
359
  // clobber window from "any caller's RMW span" to microseconds; rename/pin
334
360
  // additionally verify-and-retry on top of this.
335
- let next = state;
336
- const current = readBackgroundAgentState(state.id);
337
361
  if (current) {
338
- if (TERMINAL_STATUSES.has(current.status) && next.status === "running") {
362
+ if (TERMINAL_STATUSES.has(current.status)) {
339
363
  next = {
340
364
  ...next,
341
365
  status: current.status,
@@ -384,7 +408,163 @@ export function writeBackgroundAgentState(state) {
384
408
  configFingerprint: current.configFingerprint,
385
409
  };
386
410
  }
411
+ // Core same-id identity is immutable even for non-worktree sessions.
412
+ // Stale launcher/phase/finalize writers may update lifecycle fields, but
413
+ // never retarget a conversation, cwd or log record.
414
+ next = {
415
+ ...next,
416
+ ...(current.sessionId ? { sessionId: current.sessionId } : {}),
417
+ ...(current.cwd ? { cwd: current.cwd } : {}),
418
+ ...(current.startedAt !== undefined
419
+ ? { startedAt: current.startedAt }
420
+ : {}),
421
+ ...(current.logFile ? { logFile: current.logFile } : {}),
422
+ };
423
+ // The launch generation is immutable same-id identity. It lets the real
424
+ // worker replace a platform wrapper pid without allowing a stale/different
425
+ // worker to claim this record.
426
+ if (current.workerGeneration) {
427
+ next = { ...next, workerGeneration: current.workerGeneration };
428
+ }
429
+ const claimedWorkerPid = Number(current.workerClaimedPid);
430
+ if (
431
+ current.workerClaimedPid != null &&
432
+ Number.isInteger(claimedWorkerPid) &&
433
+ claimedWorkerPid > 0
434
+ ) {
435
+ next = {
436
+ ...next,
437
+ workerClaimedPid: claimedWorkerPid,
438
+ workerClaimedAt: current.workerClaimedAt,
439
+ };
440
+ }
441
+ // A turn launch uses a durable prepare/resolve record. The prepare must
442
+ // reach disk before native spawn so stop/remove can distinguish "no turn"
443
+ // from "a child may exist but its pid commit failed". Only the owner that
444
+ // knows the matching token/attempt may resolve it; stale snapshots cannot
445
+ // drop an unresolved intent or resurrect an already-resolved one.
446
+ const currentTurnAttempt = Math.max(
447
+ 0,
448
+ Number.isInteger(Number(current.turnLaunchAttempt))
449
+ ? Number(current.turnLaunchAttempt)
450
+ : 0,
451
+ );
452
+ const requestedTurnAttempt = Number(next.turnLaunchAttempt);
453
+ const currentTurnIntent = current.turnLaunchIntent;
454
+ const requestedTurnIntent = next.turnLaunchIntent;
455
+ const requestedTurnResolution = next.turnLaunchResolution;
456
+ const resolvesCurrentTurnIntent =
457
+ currentTurnIntent &&
458
+ requestedTurnIntent === null &&
459
+ requestedTurnResolution?.token === currentTurnIntent.token &&
460
+ Number(requestedTurnResolution?.attempt) ===
461
+ Number(currentTurnIntent.attempt) &&
462
+ ["spawned", "not-spawned", "terminated"].includes(
463
+ requestedTurnResolution?.outcome,
464
+ );
465
+ const currentTurnResolution = current.turnLaunchResolution;
466
+ const advancesSpawnedTurnResolution =
467
+ !currentTurnIntent &&
468
+ currentTurnResolution?.outcome === "spawned" &&
469
+ requestedTurnResolution?.outcome === "terminated" &&
470
+ requestedTurnResolution.token === currentTurnResolution.token &&
471
+ Number(requestedTurnResolution.attempt) ===
472
+ Number(currentTurnResolution.attempt);
473
+ const startsNewTurnIntent =
474
+ !currentTurnIntent &&
475
+ requestedTurnIntent &&
476
+ Number.isInteger(Number(requestedTurnIntent.attempt)) &&
477
+ Number(requestedTurnIntent.attempt) > currentTurnAttempt &&
478
+ Number(requestedTurnAttempt) === Number(requestedTurnIntent.attempt);
479
+ if (currentTurnIntent && !resolvesCurrentTurnIntent) {
480
+ next = {
481
+ ...next,
482
+ turnLaunchAttempt: currentTurnAttempt,
483
+ turnLaunchIntent: currentTurnIntent,
484
+ ...(current.turnLaunchResolution !== undefined
485
+ ? { turnLaunchResolution: current.turnLaunchResolution }
486
+ : {}),
487
+ };
488
+ } else if (
489
+ !currentTurnIntent &&
490
+ !startsNewTurnIntent &&
491
+ !advancesSpawnedTurnResolution
492
+ ) {
493
+ next = {
494
+ ...next,
495
+ turnLaunchAttempt: currentTurnAttempt,
496
+ turnLaunchIntent: null,
497
+ ...(current.turnLaunchResolution !== undefined
498
+ ? { turnLaunchResolution: current.turnLaunchResolution }
499
+ : {}),
500
+ };
501
+ } else if (advancesSpawnedTurnResolution) {
502
+ next = {
503
+ ...next,
504
+ turnLaunchAttempt: currentTurnAttempt,
505
+ turnLaunchIntent: null,
506
+ };
507
+ }
508
+ const turnTerminationConfirmed =
509
+ next.turnLaunchFinalizationUncertain === false &&
510
+ next.turnLaunchTermination?.confirmed === true;
511
+ const currentTurnTerminationConfirmed =
512
+ current.turnLaunchFinalizationUncertain === false &&
513
+ current.turnLaunchTermination?.confirmed === true;
514
+ if (currentTurnTerminationConfirmed) {
515
+ // Confirmation is absorbing for this terminal same-id run. A stopper or
516
+ // heartbeat carrying an older snapshot must never restore uncertainty
517
+ // after the owned process tree has been proven gone.
518
+ next = {
519
+ ...next,
520
+ turnLaunchFinalizationUncertain: false,
521
+ turnLaunchIntent: null,
522
+ turnLaunchToken: null,
523
+ turnLaunchError: null,
524
+ turnLaunchTermination: current.turnLaunchTermination,
525
+ ...(current.turnLaunchResolution !== undefined
526
+ ? { turnLaunchResolution: current.turnLaunchResolution }
527
+ : {}),
528
+ };
529
+ } else if (
530
+ current.turnLaunchFinalizationUncertain === true &&
531
+ !turnTerminationConfirmed
532
+ ) {
533
+ next = {
534
+ ...next,
535
+ agentPid: current.agentPid,
536
+ agentStartedAt: current.agentStartedAt,
537
+ turnLaunchFinalizationUncertain: true,
538
+ turnLaunchError: current.turnLaunchError,
539
+ ...(current.turnLaunchToken
540
+ ? { turnLaunchToken: current.turnLaunchToken }
541
+ : {}),
542
+ };
543
+ }
544
+ if (current.stopRequestedAt) {
545
+ next = {
546
+ ...next,
547
+ stopRequestedAt: current.stopRequestedAt,
548
+ ...(current.stopRequestedBy !== undefined
549
+ ? { stopRequestedBy: current.stopRequestedBy }
550
+ : {}),
551
+ };
552
+ if (next.status === "completed" || next.status === "failed") {
553
+ next = {
554
+ ...next,
555
+ status: "stopped",
556
+ stoppedByUser: true,
557
+ endedAt: next.endedAt || Date.now(),
558
+ phase: null,
559
+ transport: null,
560
+ };
561
+ }
562
+ }
387
563
  }
564
+ return next;
565
+ }
566
+
567
+ function persistBackgroundAgentState(target, next) {
388
568
  // Multiple async state writers can run in one process; a pid-only temporary
389
569
  // name lets them overwrite each other's staging file. A unique suffix keeps
390
570
  // each atomic replacement independent. Windows scanners/readers can also
@@ -411,14 +591,130 @@ export function writeBackgroundAgentState(state) {
411
591
  return next;
412
592
  }
413
593
 
414
- export function readBackgroundAgentState(id) {
415
- const file = statePath(id);
416
- if (!existsSync(file)) return null;
417
- try {
418
- return JSON.parse(readFileSync(file, "utf8"));
419
- } catch {
420
- return null;
594
+ /**
595
+ * Strict cross-process read/mutate/write transaction for one supervisor state.
596
+ * Returning null from `updater` leaves the record untouched. Missing and
597
+ * corrupt records fail closed unless an explicit creator opts into missing.
598
+ */
599
+ export function mutateBackgroundAgentState(id, updater, options = {}) {
600
+ const safe = safeId(id);
601
+ const target = statePath(safe);
602
+ return withFileLock(
603
+ target,
604
+ () => {
605
+ const read = readBackgroundAgentStateResult(safe);
606
+ if (read.error) throw read.error;
607
+ if (read.missing && options.createIfMissing !== true) {
608
+ return { applied: false, state: null, previous: null };
609
+ }
610
+ const requested = updater(read.state);
611
+ if (!requested) {
612
+ return { applied: false, state: read.state, previous: read.state };
613
+ }
614
+ const next = mergeBackgroundAgentState(read.state, {
615
+ ...requested,
616
+ id: safe,
617
+ });
618
+ persistBackgroundAgentState(target, next);
619
+ return { applied: true, state: next, previous: read.state };
620
+ },
621
+ {
622
+ failIfUnavailable: true,
623
+ timeoutMs: options.timeoutMs ?? 5_000,
624
+ },
625
+ );
626
+ }
627
+
628
+ export function writeBackgroundAgentState(state, options = {}) {
629
+ return mutateBackgroundAgentState(state.id, () => state, {
630
+ createIfMissing: options.createIfMissing === true,
631
+ }).state;
632
+ }
633
+
634
+ /**
635
+ * Atomically claim/refresh a worker heartbeat. The returned `applied` bit is
636
+ * the only authority to start or continue a turn: a terminal, deleted,
637
+ * corrupt, differently-generated or already-owned record is never revived.
638
+ */
639
+ export function claimBackgroundAgentHeartbeat(id, heartbeat = {}) {
640
+ const workerPid = Number(heartbeat.workerPid ?? heartbeat.pid);
641
+ const workerGeneration = heartbeat.workerGeneration;
642
+ if (!Number.isInteger(workerPid) || workerPid <= 0) {
643
+ throw new Error(`Invalid background worker pid: ${heartbeat.workerPid}`);
421
644
  }
645
+ return mutateBackgroundAgentState(id, (current) => {
646
+ if (!current || current.status !== "running") return null;
647
+ if (current.stopRequestedAt) return null;
648
+ if (
649
+ current.workerGeneration &&
650
+ current.workerGeneration !== workerGeneration
651
+ ) {
652
+ return null;
653
+ }
654
+ const claimedWorkerPid = Number(current.workerClaimedPid);
655
+ if (
656
+ current.workerClaimedPid != null &&
657
+ Number.isInteger(claimedWorkerPid) &&
658
+ claimedWorkerPid > 0 &&
659
+ claimedWorkerPid !== workerPid
660
+ ) {
661
+ return null;
662
+ }
663
+ const currentWorkerPid = Number(current.workerPid ?? current.pid);
664
+ if (
665
+ !current.workerGeneration &&
666
+ Number.isInteger(currentWorkerPid) &&
667
+ currentWorkerPid > 0 &&
668
+ currentWorkerPid !== workerPid &&
669
+ current.launchFinalizationUncertain !== true
670
+ ) {
671
+ return null;
672
+ }
673
+ return {
674
+ ...current,
675
+ ...heartbeat,
676
+ id,
677
+ pid: workerPid,
678
+ workerPid,
679
+ workerClaimedPid:
680
+ current.workerClaimedPid != null &&
681
+ Number.isInteger(claimedWorkerPid) &&
682
+ claimedWorkerPid > 0
683
+ ? claimedWorkerPid
684
+ : workerPid,
685
+ workerClaimedAt: current.workerClaimedAt || Date.now(),
686
+ status: "running",
687
+ heartbeatAt: heartbeat.heartbeatAt ?? Date.now(),
688
+ };
689
+ });
690
+ }
691
+
692
+ function deleteBackgroundAgentState(id, predicate = () => true) {
693
+ const safe = safeId(id);
694
+ const target = statePath(safe);
695
+ return withFileLock(
696
+ target,
697
+ () => {
698
+ const read = readBackgroundAgentStateResult(safe);
699
+ if (read.error) throw read.error;
700
+ if (!read.state) return null;
701
+ if (!predicate(read.state)) {
702
+ const error = new Error(
703
+ `Background agent state changed before removal: ${safe}`,
704
+ );
705
+ error.code = "BACKGROUND_AGENT_REMOVE_CONFLICT";
706
+ throw error;
707
+ }
708
+ rmSync(target, { force: true });
709
+ return read.state;
710
+ },
711
+ { failIfUnavailable: true, timeoutMs: 5_000 },
712
+ );
713
+ }
714
+
715
+ export function readBackgroundAgentState(id) {
716
+ const result = readBackgroundAgentStateResult(safeId(id));
717
+ return result.error ? null : result.state;
422
718
  }
423
719
 
424
720
  export function isProcessAlive(pid) {
@@ -507,21 +803,30 @@ function withLifecycleState(state) {
507
803
  return { ...state, lifecycleState: deriveSessionState(state) };
508
804
  }
509
805
 
510
- export function effectiveBackgroundAgentState(state, options = {}) {
806
+ function deriveEffectiveBackgroundAgentState(state, options = {}) {
511
807
  if (!state) return null;
512
808
  if (state.status !== "running") return state;
513
809
  const t = nowMs(options);
810
+ const staleAfterMs = heartbeatStaleMs(options);
811
+ const heartbeatAt = Number(state.heartbeatAt);
812
+ const launchBootstrapPending =
813
+ state.launchFinalizationUncertain === true &&
814
+ Number.isFinite(heartbeatAt) &&
815
+ t - heartbeatAt <= staleAfterMs;
514
816
  let next = state;
515
- if (
516
- Number.isFinite(Number(state.heartbeatAt)) &&
517
- t - Number(state.heartbeatAt) > heartbeatStaleMs(options)
518
- ) {
817
+ if (Number.isFinite(heartbeatAt) && t - heartbeatAt > staleAfterMs) {
519
818
  next = {
520
819
  ...state,
521
820
  status: "lost",
522
821
  endedAt: state.endedAt || t,
523
822
  lostReason: "heartbeat-stale",
524
823
  };
824
+ } else if (launchBootstrapPending) {
825
+ // The initial state is intentionally visible before spawn. Until the
826
+ // launcher or worker publishes a concrete pid, a concurrent list/view
827
+ // must not convert that bounded bootstrap window into a terminal `lost`
828
+ // record which the worker can no longer claim. The ordinary heartbeat
829
+ // stale deadline still closes the window above.
525
830
  } else if (!isProcessAlive(state.pid)) {
526
831
  next = {
527
832
  ...state,
@@ -539,42 +844,81 @@ export function effectiveBackgroundAgentState(state, options = {}) {
539
844
  lostReason: "pid-reused",
540
845
  };
541
846
  }
542
- if (next !== state) {
543
- // Gap 2: the worker is gone (dead pid or pid-reused stranger) — reap the
544
- // recorded agent grandchild so a crashed worker never leaves it running
545
- // unsupervised. A merely-stale heartbeat with a live, verified worker
546
- // still owns its child, so isSameProcess gates the reclaim.
547
- if (!isSameProcess(state.pid, state.startedAt)) {
548
- try {
549
- reclaimOrphanAgentProcess(state);
550
- } catch {
551
- /* best-effort */
552
- }
553
- }
847
+ return next;
848
+ }
849
+
850
+ export function effectiveBackgroundAgentState(state, options = {}) {
851
+ if (!state) return null;
852
+ if (state.status !== "running") return state;
853
+ // OS identity probes can take seconds on Windows. Perform them outside the
854
+ // state lock, then use the exact observed identity/heartbeat as a CAS fence
855
+ // inside the short persistence transaction.
856
+ const derived = deriveEffectiveBackgroundAgentState(state, options);
857
+ if (options.persist === false || derived === state) return derived;
858
+
859
+ const mutation = mutateBackgroundAgentState(state.id, (current) => {
554
860
  if (
555
- options.persist !== false &&
556
- state.sessionId &&
557
- !isSameProcess(state.pid, state.startedAt)
861
+ !current ||
862
+ current.status !== "running" ||
863
+ current.heartbeatAt !== state.heartbeatAt ||
864
+ current.pid !== state.pid ||
865
+ current.workerPid !== state.workerPid ||
866
+ current.startedAt !== state.startedAt ||
867
+ current.workerGeneration !== state.workerGeneration ||
868
+ current.launchFinalizationUncertain !==
869
+ state.launchFinalizationUncertain ||
870
+ current.turnLaunchAttempt !== state.turnLaunchAttempt ||
871
+ current.turnLaunchIntent?.token !== state.turnLaunchIntent?.token ||
872
+ current.turnLaunchFinalizationUncertain !==
873
+ state.turnLaunchFinalizationUncertain ||
874
+ current.stopRequestedAt !== state.stopRequestedAt
558
875
  ) {
559
- const pending = state.pendingQuestion;
876
+ return null;
877
+ }
878
+ return {
879
+ ...current,
880
+ status: derived.status,
881
+ endedAt: derived.endedAt,
882
+ lostReason: derived.lostReason,
883
+ };
884
+ });
885
+ if (!mutation.applied) return mutation.state;
886
+
887
+ const previous = mutation.previous;
888
+ let next = mutation.state;
889
+ // Gap 2: the worker is gone (dead pid or pid-reused stranger) — reap the
890
+ // recorded agent grandchild so a crashed worker never leaves it running
891
+ // unsupervised. A merely-stale heartbeat with a live, verified worker still
892
+ // owns its child, so isSameProcess gates the reclaim.
893
+ const workerGone = !isSameProcess(previous?.pid, previous?.startedAt);
894
+ if (workerGone) {
895
+ try {
896
+ reclaimOrphanAgentProcess(previous);
897
+ } catch {
898
+ /* best-effort */
899
+ }
900
+ if (previous?.sessionId) {
901
+ const t = nowMs(options);
902
+ const stateBeforeRecovery = next;
903
+ const pending = previous.pendingQuestion;
560
904
  const requestId = pending?.requestId || pending?.intId;
561
905
  try {
562
906
  const recovery = rejectPendingBackgroundInteractions(
563
- state.sessionId,
564
- state.id,
907
+ previous.sessionId,
908
+ previous.id,
565
909
  {
566
910
  fallbackRequest: requestId
567
911
  ? {
568
912
  requestId,
569
- binding: pending.binding,
913
+ binding: pending?.binding,
570
914
  payload: {
571
915
  kind: "question",
572
- question: pending.question || pending.prompt || "",
573
- options: pending.options || null,
574
- multiSelect: pending.multiSelect === true,
575
- timeoutMs: pending.timeoutMs,
916
+ question: pending?.question || pending?.prompt || "",
917
+ options: pending?.options || null,
918
+ multiSelect: pending?.multiSelect === true,
919
+ timeoutMs: pending?.timeoutMs,
576
920
  },
577
- createdAt: pending.askedAt,
921
+ createdAt: pending?.askedAt,
578
922
  }
579
923
  : null,
580
924
  code: "INTERACTION_WORKER_LOST",
@@ -611,12 +955,30 @@ export function effectiveBackgroundAgentState(state, options = {}) {
611
955
  },
612
956
  };
613
957
  }
614
- }
615
- if (options.persist !== false) {
616
- try {
617
- writeBackgroundAgentState(next);
618
- } catch {
619
- /* best-effort; callers still get the corrected state */
958
+ if (next !== stateBeforeRecovery) {
959
+ try {
960
+ const recoveryMutation = mutateBackgroundAgentState(
961
+ previous.id,
962
+ (current) => {
963
+ if (
964
+ !current ||
965
+ current.status !== stateBeforeRecovery.status ||
966
+ current.lostReason !== stateBeforeRecovery.lostReason
967
+ ) {
968
+ return null;
969
+ }
970
+ return {
971
+ ...current,
972
+ phase: next.phase,
973
+ pendingQuestion: next.pendingQuestion,
974
+ interactionRecovery: next.interactionRecovery,
975
+ };
976
+ },
977
+ );
978
+ next = recoveryMutation.state || next;
979
+ } catch {
980
+ /* best-effort; the terminal transition itself is already durable */
981
+ }
620
982
  }
621
983
  }
622
984
  }
@@ -658,23 +1020,11 @@ export function renameBackgroundAgent(id, title, options = {}) {
658
1020
  if (!state) throw new Error(`Background agent not found: ${id}`);
659
1021
  const normalized = normalizeBackgroundAgentTitle(title);
660
1022
  const renamedAt = typeof options.now === "number" ? options.now : Date.now();
661
- // The worker persists state via read-modify-write (heartbeat / transport /
662
- // turn merges). A rename that lands inside one of those read→write windows
663
- // gets clobbered by the worker's stale snapshot — so verify after a short
664
- // beat and re-apply onto the freshest state (bounded retries; the window is
665
- // milliseconds wide, worst during worker startup's write burst).
666
- let next = { ...state, title: normalized, renamedAt };
667
- for (let attempt = 0; attempt < 4; attempt++) {
668
- writeBackgroundAgentState(next);
669
- sleepSyncMs(15);
670
- const current = readBackgroundAgentState(id) || next;
671
- if (current.title === normalized) {
672
- return { ...current, title: normalized, renamedAt };
673
- }
674
- next = { ...current, title: normalized, renamedAt };
675
- }
676
- writeBackgroundAgentState(next);
677
- return next;
1023
+ const mutation = mutateBackgroundAgentState(id, (current) =>
1024
+ current ? { ...current, title: normalized, renamedAt } : null,
1025
+ );
1026
+ if (!mutation.applied) throw new Error(`Background agent not found: ${id}`);
1027
+ return mutation.state;
678
1028
  }
679
1029
 
680
1030
  /**
@@ -710,7 +1060,16 @@ export function removeBackgroundAgent(id, options = {}) {
710
1060
  const worktree = cleanupBackgroundAgentWorktree(state, {
711
1061
  keepWorktree: options.keepWorktree === true,
712
1062
  });
713
- rmSync(statePath(id), { force: true });
1063
+ const deleted = deleteBackgroundAgentState(
1064
+ id,
1065
+ (current) =>
1066
+ current.status !== "running" &&
1067
+ current.launchFinalizationUncertain !== true &&
1068
+ !current.turnLaunchIntent &&
1069
+ current.turnLaunchFinalizationUncertain !== true &&
1070
+ activeWorktreeProcesses(current).length === 0,
1071
+ );
1072
+ if (!deleted) throw new Error(`Background agent not found: ${id}`);
714
1073
  if (options.keepLog !== true) {
715
1074
  rmSync(logPath(id), { force: true });
716
1075
  }
@@ -736,18 +1095,11 @@ export function setBackgroundAgentPinned(id, pinned, options = {}) {
736
1095
  if (!state) throw new Error(`Background agent not found: ${id}`);
737
1096
  const value = pinned === true;
738
1097
  const pinnedAt = typeof options.now === "number" ? options.now : Date.now();
739
- let next = { ...state, pinned: value, pinnedAt };
740
- for (let attempt = 0; attempt < 4; attempt++) {
741
- writeBackgroundAgentState(next);
742
- sleepSyncMs(15);
743
- const current = readBackgroundAgentState(id) || next;
744
- if (current.pinned === value) {
745
- return { ...current, pinned: value, pinnedAt };
746
- }
747
- next = { ...current, pinned: value, pinnedAt };
748
- }
749
- writeBackgroundAgentState(next);
750
- return next;
1098
+ const mutation = mutateBackgroundAgentState(id, (current) =>
1099
+ current ? { ...current, pinned: value, pinnedAt } : null,
1100
+ );
1101
+ if (!mutation.applied) throw new Error(`Background agent not found: ${id}`);
1102
+ return mutation.state;
751
1103
  }
752
1104
 
753
1105
  /**
@@ -763,8 +1115,26 @@ export function setBackgroundAgentPinned(id, pinned, options = {}) {
763
1115
  * @param {string[]} [opts.positionalTokens] raw positional prompt tokens
764
1116
  * @param {string|null} [opts.printValue] inline `-p <value>` text, when the
765
1117
  * prompt came from --print
1118
+ * @param {Array<object>} [opts.optionSpecs] captured Commander option grammar
1119
+ * @param {string[]} [opts.commandNames] command name and aliases
766
1120
  */
1121
+ export function insertArgumentsBeforeOptionTerminator(argv, additions) {
1122
+ const out = [...(argv || [])];
1123
+ const index = out.indexOf("--");
1124
+ out.splice(index === -1 ? out.length : index, 0, ...(additions || []));
1125
+ return out;
1126
+ }
1127
+
767
1128
  export function buildFollowUpArgv(argv, opts = {}) {
1129
+ if (Array.isArray(opts.optionSpecs) && opts.optionSpecs.length > 0) {
1130
+ return stripFirstTurnPromptArgv(argv, {
1131
+ optionSpecs: opts.optionSpecs,
1132
+ commandNames: opts.commandNames,
1133
+ });
1134
+ }
1135
+ // Compatibility fallback for stored/direct callers created before option
1136
+ // grammar was persisted by the agent command. New launches always supply
1137
+ // optionSpecs and therefore cannot confuse option values with operands.
768
1138
  const positionalLeft = Array.isArray(opts.positionalTokens)
769
1139
  ? [...opts.positionalTokens]
770
1140
  : [];
@@ -1024,7 +1394,9 @@ export function resumeBackgroundAgent(id, prompt, options = {}) {
1024
1394
  state.sessionId,
1025
1395
  ]
1026
1396
  : ["agent", "--session", state.sessionId];
1027
- const argv = [...followUpArgv, "-p", text];
1397
+ const argv = insertArgumentsBeforeOptionTerminator(followUpArgv, [
1398
+ agentPrintArgument(text),
1399
+ ]);
1028
1400
  return launchBackgroundAgent({
1029
1401
  argv,
1030
1402
  // Worktree sessions fail closed: never fall back to process.cwd(), which
@@ -1076,6 +1448,90 @@ function assertUsableCwd(cwd) {
1076
1448
  }
1077
1449
  }
1078
1450
 
1451
+ /**
1452
+ * A detached worker exists, so the caller must transfer/retain any worktree
1453
+ * even though launcher finalization failed. `instanceof` keeps this ownership
1454
+ * signal typed instead of trusting a forgeable string/code from user input.
1455
+ */
1456
+ export class BackgroundWorkerStartedError extends Error {
1457
+ constructor(error, { id, workerPid } = {}) {
1458
+ super(
1459
+ `Background worker ${id || "unknown"} started, but launcher finalization failed: ${error?.message || String(error)}`,
1460
+ { cause: error },
1461
+ );
1462
+ this.name = "BackgroundWorkerStartedError";
1463
+ this.code = "ERR_BACKGROUND_WORKER_STARTED";
1464
+ this.backgroundAgentId = id || null;
1465
+ this.workerPid = Number.isInteger(Number(workerPid))
1466
+ ? Number(workerPid)
1467
+ : null;
1468
+ }
1469
+ }
1470
+
1471
+ export function isBackgroundWorkerStartedError(error) {
1472
+ return error instanceof BackgroundWorkerStartedError;
1473
+ }
1474
+
1475
+ async function settleUncertainBackgroundLaunch({
1476
+ id,
1477
+ jobFile,
1478
+ spawnedProcess,
1479
+ error,
1480
+ }) {
1481
+ let termination;
1482
+ try {
1483
+ termination = await _deps.terminateOwnedProcessTree(spawnedProcess, {
1484
+ treeMode: process.platform === "win32" ? "windows-tree" : "posix-group",
1485
+ });
1486
+ } catch {
1487
+ return;
1488
+ }
1489
+ if (termination?.confirmed !== true) return;
1490
+ try {
1491
+ const workerPid = Number(spawnedProcess?.pid);
1492
+ const current = readBackgroundAgentState(id);
1493
+ if (!current || current.launchFinalizationUncertain !== true) return;
1494
+ if (
1495
+ Number.isInteger(workerPid) &&
1496
+ workerPid > 0 &&
1497
+ current.workerPid != null &&
1498
+ Number(current.workerPid) !== workerPid
1499
+ ) {
1500
+ return;
1501
+ }
1502
+ const terminal = current.status === "running" ? "failed" : current.status;
1503
+ writeBackgroundAgentState({
1504
+ ...current,
1505
+ status: terminal,
1506
+ endedAt: current.endedAt || Date.now(),
1507
+ exitCode: current.exitCode ?? 1,
1508
+ ...(current.status === "running"
1509
+ ? {
1510
+ error: `background launch failed after spawn: ${
1511
+ error?.message || "unknown post-spawn failure"
1512
+ }`,
1513
+ }
1514
+ : {}),
1515
+ phase: null,
1516
+ transport: null,
1517
+ launchFinalizationUncertain: false,
1518
+ launchTermination: {
1519
+ confirmed: true,
1520
+ treeMode: termination.treeMode || null,
1521
+ closed: termination.closed === true,
1522
+ treeTerminated: termination.treeTerminated === true,
1523
+ settledAt: Date.now(),
1524
+ },
1525
+ });
1526
+ // State is the durable cleanup authority; remove the one-shot job only
1527
+ // after the terminal/ownership evidence is safely persisted.
1528
+ rmSync(jobFile, { force: true });
1529
+ } catch {
1530
+ // Persistence failure must retain the original uncertainty fence and must
1531
+ // never become an unhandled rejection in the launching CLI process.
1532
+ }
1533
+ }
1534
+
1079
1535
  export function launchBackgroundAgent({
1080
1536
  argv,
1081
1537
  cwd,
@@ -1118,12 +1574,14 @@ export function launchBackgroundAgent({
1118
1574
  const configFingerprint =
1119
1575
  fingerprintBackgroundLaunchProfile(launchProfileState);
1120
1576
  const dir = backgroundAgentsDir();
1577
+ const workerGeneration = randomBytes(16).toString("hex");
1121
1578
  const jobFile = join(dir, `${id}.job.${process.pid}.json`);
1122
1579
  const worker = fileURLToPath(
1123
1580
  new URL("../workers/background-agent-worker.js", import.meta.url),
1124
1581
  );
1125
1582
  const job = {
1126
1583
  id,
1584
+ workerGeneration,
1127
1585
  argv: initialSecrets.argv,
1128
1586
  cwd,
1129
1587
  sessionId,
@@ -1144,11 +1602,17 @@ export function launchBackgroundAgent({
1144
1602
  // by a late launcher write racing the worker.
1145
1603
  const state = {
1146
1604
  id,
1605
+ workerGeneration,
1147
1606
  sessionId,
1148
1607
  title: job.title,
1149
1608
  cwd,
1150
1609
  pid: null,
1151
1610
  workerPid: null,
1611
+ workerClaimedPid: null,
1612
+ workerClaimedAt: null,
1613
+ turnLaunchAttempt: 0,
1614
+ turnLaunchIntent: null,
1615
+ turnLaunchResolution: null,
1152
1616
  agentPid: null,
1153
1617
  agentStartedAt: null,
1154
1618
  status: "running",
@@ -1161,10 +1625,20 @@ export function launchBackgroundAgent({
1161
1625
  governance: governanceState,
1162
1626
  launchProfile: launchProfileState,
1163
1627
  configFingerprint,
1628
+ // Cleared only after the launcher (or worker heartbeat) has published a
1629
+ // concrete worker identity. Cleanup treats true as an ownership fence.
1630
+ launchFinalizationUncertain: true,
1164
1631
  };
1165
- writeBackgroundAgentState(state);
1632
+ try {
1633
+ writeBackgroundAgentState(state, { createIfMissing: true });
1634
+ } catch (error) {
1635
+ rmSync(jobFile, { force: true });
1636
+ rmSync(statePath(id), { force: true });
1637
+ throw error;
1638
+ }
1166
1639
  let child;
1167
1640
  let workerLogFd;
1641
+ let postSpawnError = null;
1168
1642
  try {
1169
1643
  // Preserve loader/bootstrap failures that happen before the worker can
1170
1644
  // open its own append handle. This also gives detached Windows targets
@@ -1185,73 +1659,123 @@ export function launchBackgroundAgent({
1185
1659
  shell: false,
1186
1660
  });
1187
1661
  } catch (error) {
1662
+ if (error?.spawnedProcess) {
1663
+ // The broker can fail a post-spawn sandbox handshake only after native
1664
+ // spawn returned. It requests termination, but this synchronous caller
1665
+ // cannot prove the close fence has settled; retain state/job/worktree so
1666
+ // cleanup never races a process that may still hold cwd.
1667
+ const workerPid = Number(error.spawnedProcess.pid);
1668
+ try {
1669
+ const current = readBackgroundAgentState(id) || state;
1670
+ writeBackgroundAgentState({
1671
+ ...current,
1672
+ pid: Number.isInteger(workerPid) && workerPid > 0 ? workerPid : null,
1673
+ workerPid:
1674
+ Number.isInteger(workerPid) && workerPid > 0 ? workerPid : null,
1675
+ launchFinalizationUncertain: true,
1676
+ });
1677
+ } catch {
1678
+ // The initial state already carries the uncertainty fence. Failure to
1679
+ // add a pid must retain that fail-closed marker.
1680
+ }
1681
+ void settleUncertainBackgroundLaunch({
1682
+ id,
1683
+ jobFile,
1684
+ spawnedProcess: error.spawnedProcess,
1685
+ error,
1686
+ });
1687
+ throw new BackgroundWorkerStartedError(error, {
1688
+ id,
1689
+ workerPid,
1690
+ });
1691
+ }
1188
1692
  rmSync(jobFile, { force: true });
1189
1693
  rmSync(statePath(id), { force: true });
1190
1694
  throw error;
1191
1695
  } finally {
1192
1696
  if (Number.isInteger(workerLogFd)) {
1193
- closeSync(workerLogFd);
1697
+ try {
1698
+ closeSync(workerLogFd);
1699
+ } catch (error) {
1700
+ // Once spawn returned a child, even fd-finalization errors are
1701
+ // post-transfer: the worker may already be using the worktree.
1702
+ if (child) postSpawnError = error;
1703
+ }
1194
1704
  }
1195
1705
  }
1196
- // Async spawn failures (EPERM, cwd raced away between the check and the
1197
- // spawn, …) arrive as an 'error' event on the detached child. Reap them
1198
- // into the state file instead of leaving a phantom "running" session and
1199
- // an uncaught exception.
1200
- if (typeof child.on === "function") {
1201
- child.on("error", (error) => {
1202
- try {
1203
- rmSync(jobFile, { force: true });
1204
- const current = readBackgroundAgentState(id);
1205
- if (current && current.status === "running") {
1206
- writeBackgroundAgentState({
1207
- ...current,
1208
- status: "failed",
1209
- endedAt: Date.now(),
1210
- lostReason: `spawn-error: ${error.code || error.message}`,
1211
- });
1706
+ let withPid = null;
1707
+ try {
1708
+ // Async spawn failures (EPERM, cwd raced away between the check and the
1709
+ // spawn, …) arrive as an 'error' event on the detached child. Reap them
1710
+ // into the state file instead of leaving a phantom "running" session and
1711
+ // an uncaught exception.
1712
+ if (typeof child.on === "function") {
1713
+ child.on("error", (error) => {
1714
+ try {
1715
+ rmSync(jobFile, { force: true });
1716
+ const current = readBackgroundAgentState(id);
1717
+ if (current && current.status === "running") {
1718
+ writeBackgroundAgentState({
1719
+ ...current,
1720
+ status: "failed",
1721
+ endedAt: Date.now(),
1722
+ lostReason: `spawn-error: ${error.code || error.message}`,
1723
+ launchFinalizationUncertain: false,
1724
+ });
1725
+ }
1726
+ } catch {
1727
+ /* best-effort */
1212
1728
  }
1213
- } catch {
1214
- /* best-effort */
1215
- }
1216
- });
1217
- child.on("exit", (code, signal) => {
1218
- try {
1219
- rmSync(jobFile, { force: true });
1220
- const current = readBackgroundAgentState(id);
1221
- // A healthy worker persists completed/failed before exiting. If the
1222
- // process (or its platform wrapper) exits while the state still says
1223
- // running, bootstrap or finalization failed and the session must not
1224
- // remain a phantom live task until heartbeat reconciliation.
1225
- if (current && current.status === "running") {
1226
- writeBackgroundAgentState({
1227
- ...current,
1228
- status: "failed",
1229
- endedAt: Date.now(),
1230
- exitCode: Number.isInteger(code) ? code : null,
1231
- signal: signal || null,
1232
- lostReason: `worker-exited-before-finalize: exit=${
1233
- Number.isInteger(code) ? code : "null"
1234
- } signal=${signal || "none"}`,
1235
- });
1729
+ });
1730
+ child.on("exit", (code, signal) => {
1731
+ try {
1732
+ rmSync(jobFile, { force: true });
1733
+ const current = readBackgroundAgentState(id);
1734
+ // A healthy worker persists completed/failed before exiting. If the
1735
+ // process (or its platform wrapper) exits while the state still says
1736
+ // running, bootstrap or finalization failed and the session must not
1737
+ // remain a phantom live task until heartbeat reconciliation.
1738
+ if (current && current.status === "running") {
1739
+ writeBackgroundAgentState({
1740
+ ...current,
1741
+ status: "failed",
1742
+ endedAt: Date.now(),
1743
+ exitCode: Number.isInteger(code) ? code : null,
1744
+ signal: signal || null,
1745
+ lostReason: `worker-exited-before-finalize: exit=${
1746
+ Number.isInteger(code) ? code : "null"
1747
+ } signal=${signal || "none"}`,
1748
+ launchFinalizationUncertain: false,
1749
+ });
1750
+ }
1751
+ } catch {
1752
+ /* best-effort */
1236
1753
  }
1237
- } catch {
1238
- /* best-effort */
1239
- }
1754
+ });
1755
+ }
1756
+ const current = readBackgroundAgentState(id) || state;
1757
+ withPid = {
1758
+ ...current,
1759
+ // A platform wrapper (notably the Windows Job Object adapter) can return
1760
+ // from spawn after the real worker has already claimed the state file.
1761
+ // Preserve that authoritative process.pid instead of racing it back to
1762
+ // the wrapper pid. With an ordinary direct spawn both fields are still
1763
+ // null here and are initialized from child.pid as before.
1764
+ pid: current.pid ?? child.pid,
1765
+ workerPid: current.workerPid ?? child.pid,
1766
+ launchFinalizationUncertain: false,
1767
+ };
1768
+ writeBackgroundAgentState(withPid);
1769
+ child.unref();
1770
+ } catch (error) {
1771
+ postSpawnError ||= error;
1772
+ }
1773
+ if (postSpawnError) {
1774
+ throw new BackgroundWorkerStartedError(postSpawnError, {
1775
+ id,
1776
+ workerPid: child?.pid,
1240
1777
  });
1241
1778
  }
1242
- child.unref();
1243
- const current = readBackgroundAgentState(id) || state;
1244
- const withPid = {
1245
- ...current,
1246
- // A platform wrapper (notably the Windows Job Object adapter) can return
1247
- // from spawn after the real worker has already claimed the state file.
1248
- // Preserve that authoritative process.pid instead of racing it back to
1249
- // the wrapper pid. With an ordinary direct spawn both fields are still
1250
- // null here and are initialized from child.pid as before.
1251
- pid: current.pid ?? child.pid,
1252
- workerPid: current.workerPid ?? child.pid,
1253
- };
1254
- writeBackgroundAgentState(withPid);
1255
1779
  return withPid;
1256
1780
  }
1257
1781
 
@@ -1265,7 +1789,7 @@ export function readBackgroundAgentLog(id, options = {}) {
1265
1789
  }
1266
1790
 
1267
1791
  export function stopBackgroundAgent(id) {
1268
- const state = effectiveBackgroundAgentState(readBackgroundAgentState(id));
1792
+ let state = effectiveBackgroundAgentState(readBackgroundAgentState(id));
1269
1793
  if (!state) throw new Error(`Background agent not found: ${id}`);
1270
1794
  if (state.status !== "running") {
1271
1795
  // Gap 2: stopping an already-lost session still reaps a leaked agent
@@ -1279,6 +1803,40 @@ export function stopBackgroundAgent(id) {
1279
1803
  }
1280
1804
  return { ...state, stopped: false };
1281
1805
  }
1806
+ const stopRequestedAt = Date.now();
1807
+ const fence = mutateBackgroundAgentState(id, (current) => {
1808
+ if (!current || current.status !== "running") return null;
1809
+ return {
1810
+ ...current,
1811
+ stopRequestedAt: current.stopRequestedAt || stopRequestedAt,
1812
+ stopRequestedBy: "user",
1813
+ phase: "stopping",
1814
+ transport: null,
1815
+ };
1816
+ });
1817
+ if (!fence.applied) {
1818
+ if (!fence.state) throw new Error(`Background agent not found: ${id}`);
1819
+ return { ...fence.state, stopped: false };
1820
+ }
1821
+ state = fence.state;
1822
+ if (
1823
+ state.turnLaunchIntent ||
1824
+ state.turnLaunchFinalizationUncertain === true
1825
+ ) {
1826
+ // The worker has durably announced that native spawn is prepared or that
1827
+ // post-spawn pid persistence is unresolved. Killing the worker from this
1828
+ // snapshot could strand its detached child before ownership is published.
1829
+ // The stop fence prevents any new spawn; the live owner must now resolve
1830
+ // no-spawn or terminate the exact owned tree and persist confirmation.
1831
+ return {
1832
+ ...state,
1833
+ stopped: false,
1834
+ stopPending: true,
1835
+ stopPendingReason: state.turnLaunchIntent
1836
+ ? "turn-launch-intent"
1837
+ : "turn-launch-finalization",
1838
+ };
1839
+ }
1282
1840
  // Self-pid guard: a worker record whose pid is the CURRENT process is
1283
1841
  // impossible in legitimate operation (the stopper is never the worker) —
1284
1842
  // it means a corrupt/hand-edited state file. Signalling it would SIGTERM /
@@ -1291,8 +1849,8 @@ export function stopBackgroundAgent(id) {
1291
1849
  endedAt: Date.now(),
1292
1850
  lostReason: "self-pid-corrupt-record",
1293
1851
  };
1294
- writeBackgroundAgentState(lost);
1295
- return { ...lost, stopped: false };
1852
+ const written = writeBackgroundAgentState(lost) || lost;
1853
+ return { ...written, stopped: false };
1296
1854
  }
1297
1855
  // Gap 1: last-instant identity re-check before the kill — never
1298
1856
  // taskkill/SIGTERM a pid the OS has re-assigned to an unrelated process.
@@ -1308,64 +1866,101 @@ export function stopBackgroundAgent(id) {
1308
1866
  endedAt: Date.now(),
1309
1867
  lostReason: "pid-reused",
1310
1868
  };
1311
- writeBackgroundAgentState(lost);
1312
- return { ...lost, stopped: false };
1869
+ const written = writeBackgroundAgentState(lost) || lost;
1870
+ return { ...written, stopped: false };
1313
1871
  }
1314
- if (process.platform === "win32") {
1315
- const killed = runSupervisorCommand(
1316
- "taskkill",
1317
- ["/PID", String(state.pid), "/T", "/F"],
1318
- {
1319
- windowsHide: true,
1320
- encoding: "utf8",
1321
- },
1322
- "background-agent:stop-tree",
1323
- );
1324
- if (killed.error || (killed.status !== 0 && isProcessAlive(state.pid))) {
1325
- throw new Error(
1326
- `Failed to stop background agent ${id}: ${killed.error?.message || killed.stderr || `taskkill exited ${killed.status}`}`,
1872
+ try {
1873
+ if (process.platform === "win32") {
1874
+ const killed = runSupervisorCommand(
1875
+ "taskkill",
1876
+ ["/PID", String(state.pid), "/T", "/F"],
1877
+ {
1878
+ windowsHide: true,
1879
+ encoding: "utf8",
1880
+ },
1881
+ "background-agent:stop-tree",
1327
1882
  );
1328
- }
1329
- } else {
1330
- try {
1331
- _deps.kill(-Number(state.pid), "SIGTERM");
1332
- } catch {
1333
- _deps.kill(Number(state.pid), "SIGTERM");
1334
- }
1335
- // The worker detaches the agent child into its OWN process group (so a
1336
- // crashed worker's child can be group-killed) — which also means the
1337
- // worker-group SIGTERM above no longer reaches it. Kill the agent group
1338
- // explicitly, identity-guarded via its agentStartedAt anchor.
1339
- const agentPid = Number(state.agentPid);
1340
- const anchor = Number(state.agentStartedAt);
1341
- if (
1342
- Number.isInteger(agentPid) &&
1343
- agentPid > 0 &&
1344
- agentPid !== Number(state.pid) &&
1345
- agentPid !== process.pid &&
1346
- Number.isFinite(anchor) &&
1347
- anchor > 0 &&
1348
- isSameProcess(agentPid, anchor)
1349
- ) {
1883
+ if (killed.error || (killed.status !== 0 && isProcessAlive(state.pid))) {
1884
+ throw new Error(
1885
+ `Failed to stop background agent ${id}: ${killed.error?.message || killed.stderr || `taskkill exited ${killed.status}`}`,
1886
+ );
1887
+ }
1888
+ } else {
1350
1889
  try {
1351
- _deps.kill(-agentPid, "SIGTERM");
1890
+ _deps.kill(-Number(state.pid), "SIGTERM");
1352
1891
  } catch {
1892
+ _deps.kill(Number(state.pid), "SIGTERM");
1893
+ }
1894
+ // The worker detaches the agent child into its OWN process group (so a
1895
+ // crashed worker's child can be group-killed) — which also means the
1896
+ // worker-group SIGTERM above no longer reaches it. Kill the agent group
1897
+ // explicitly, identity-guarded via its agentStartedAt anchor.
1898
+ const agentPid = Number(state.agentPid);
1899
+ const anchor = Number(state.agentStartedAt);
1900
+ if (
1901
+ Number.isInteger(agentPid) &&
1902
+ agentPid > 0 &&
1903
+ agentPid !== Number(state.pid) &&
1904
+ agentPid !== process.pid &&
1905
+ Number.isFinite(anchor) &&
1906
+ anchor > 0 &&
1907
+ isSameProcess(agentPid, anchor)
1908
+ ) {
1353
1909
  try {
1354
- _deps.kill(agentPid, "SIGTERM");
1910
+ _deps.kill(-agentPid, "SIGTERM");
1355
1911
  } catch {
1356
- /* already gone */
1912
+ try {
1913
+ _deps.kill(agentPid, "SIGTERM");
1914
+ } catch {
1915
+ /* already gone */
1916
+ }
1357
1917
  }
1358
1918
  }
1359
1919
  }
1920
+ } catch (error) {
1921
+ try {
1922
+ mutateBackgroundAgentState(id, (current) =>
1923
+ current
1924
+ ? {
1925
+ ...current,
1926
+ phase: "stop_failed",
1927
+ stopError: error?.message || String(error),
1928
+ }
1929
+ : null,
1930
+ );
1931
+ } catch {
1932
+ /* the durable stopRequestedAt fence already blocks new turns */
1933
+ }
1934
+ throw error;
1360
1935
  }
1361
- const next = {
1362
- ...state,
1363
- status: "stopped",
1364
- endedAt: Date.now(),
1365
- stoppedByUser: true,
1366
- };
1367
- writeBackgroundAgentState(next);
1368
- return { ...next, stopped: true };
1936
+ // Build the terminal write from the freshest record under the lock. In
1937
+ // particular, never spread the pre-kill fence snapshot over a worker's
1938
+ // concurrently persisted process-tree termination confirmation.
1939
+ const terminal = mutateBackgroundAgentState(id, (current) =>
1940
+ current
1941
+ ? {
1942
+ ...current,
1943
+ status: "stopped",
1944
+ endedAt: current.endedAt || Date.now(),
1945
+ stoppedByUser: true,
1946
+ phase: null,
1947
+ transport: null,
1948
+ stopError: null,
1949
+ }
1950
+ : null,
1951
+ );
1952
+ const written = terminal.state || state;
1953
+ // A turn-spawn persistence failure may publish its agent pid after the
1954
+ // stopper took the first snapshot. Re-read/merge above preserves that
1955
+ // uncertainty identity; make one final identity-guarded tree reap attempt.
1956
+ if (written.agentPid) {
1957
+ try {
1958
+ reclaimOrphanAgentProcess(written);
1959
+ } catch {
1960
+ /* retained agentPid + uncertainty fence block destructive cleanup */
1961
+ }
1962
+ }
1963
+ return { ...written, stopped: written.status === "stopped" };
1369
1964
  }
1370
1965
 
1371
1966
  function activeWorktreeProcesses(state) {
@@ -1405,6 +2000,15 @@ export function cleanupBackgroundAgentWorktree(state, options = {}) {
1405
2000
  }
1406
2001
 
1407
2002
  const blockers = [];
2003
+ if (state?.launchFinalizationUncertain === true) {
2004
+ blockers.push("background launch finalization is uncertain");
2005
+ }
2006
+ if (state?.turnLaunchFinalizationUncertain === true) {
2007
+ blockers.push("background turn launch finalization is uncertain");
2008
+ }
2009
+ if (state?.turnLaunchIntent) {
2010
+ blockers.push("background turn launch intent is unresolved");
2011
+ }
1408
2012
  const active = activeWorktreeProcesses(state);
1409
2013
  if (active.length) blockers.push(`active ${active.join("+")} process`);
1410
2014
  if (Number(state?.pendingApprovals || 0) > 0) {