chainlesschain 0.162.154 → 0.162.155

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 (198) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/assets/web-panel/.build-hash +1 -1
  4. package/src/assets/web-panel/assets/{AIOps-CIpoXyVI.js → AIOps-Cth_zZeA.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-Ch_QXIPk.js → ActionButton-zfwA3Oxn.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-CvaBYShv.js → Analytics-FeKc4aU2.js} +2 -2
  7. package/src/assets/web-panel/assets/AppLayout-C6WIrDZk.js +9 -0
  8. package/src/assets/web-panel/assets/{Audit-Bng8eHCW.js → Audit-C2gRq5Qi.js} +1 -1
  9. package/src/assets/web-panel/assets/BackgroundAgents-Dq2340Kl.css +1 -0
  10. package/src/assets/web-panel/assets/BackgroundAgents-TeTIFiW-.js +1 -0
  11. package/src/assets/web-panel/assets/{Backup-DvBs8-lm.js → Backup-CLJk3k6i.js} +1 -1
  12. package/src/assets/web-panel/assets/{BaseInput-DKwajm0g.js → BaseInput-5ms537NY.js} +1 -1
  13. package/src/assets/web-panel/assets/{Chat-BoGpZ3uO.js → Chat-4Nr5U9PJ.js} +5 -5
  14. package/src/assets/web-panel/assets/ChatBubbleRenderer-BFEnA8LN.js +1 -0
  15. package/src/assets/web-panel/assets/{Checkbox-BMJEmxFQ.js → Checkbox-B2Bca5ev.js} +1 -1
  16. package/src/assets/web-panel/assets/{Codegen-Bsoag5CV.js → Codegen-D6Uysdbv.js} +1 -1
  17. package/src/assets/web-panel/assets/{Col-CDjH9-sp.js → Col-B3XQ8xc7.js} +1 -1
  18. package/src/assets/web-panel/assets/{Community-Cg2QHPQ6.js → Community-CqwKtoPz.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compact-BU2BaqP-.js → Compact-BZZINTus.js} +1 -1
  20. package/src/assets/web-panel/assets/{Compliance-D6ZYMCqZ.js → Compliance-Dr3BXkWA.js} +1 -1
  21. package/src/assets/web-panel/assets/{Cowork-DaZ8VW-I.js → Cowork-rx_x9Wqc.js} +2 -2
  22. package/src/assets/web-panel/assets/{Cron-DQY1RH9v.js → Cron-BpZr7rHc.js} +2 -2
  23. package/src/assets/web-panel/assets/{Crosschain-BVR28vk9.js → Crosschain-CeV5EYVg.js} +1 -1
  24. package/src/assets/web-panel/assets/{DID-N44KvTP-.js → DID-fe2Qaowz.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dashboard-Bjhlx6wS.js → Dashboard-Fh_EdWVa.js} +2 -2
  26. package/src/assets/web-panel/assets/{Dropdown-CpLYxpy1.js → Dropdown-N1PIwWYf.js} +1 -1
  27. package/src/assets/web-panel/assets/{EmailListRenderer-srEITSPI.js → EmailListRenderer-B6vK93Ed.js} +1 -1
  28. package/src/assets/web-panel/assets/{FamilyGuardDashboard-juOC_fX2.js → FamilyGuardDashboard-e3iUZhAg.js} +1 -1
  29. package/src/assets/web-panel/assets/{Federation-nRaQtaXW.js → Federation-uInc9YcN.js} +1 -1
  30. package/src/assets/web-panel/assets/{FormItemContext-DflhTj09.js → FormItemContext-DmQZSX0z.js} +1 -1
  31. package/src/assets/web-panel/assets/{GenericCardRenderer-DulI_3NI.js → GenericCardRenderer-CV_Z8OAo.js} +1 -1
  32. package/src/assets/web-panel/assets/{Git-BxOPefXn.js → Git-jXAhNN8d.js} +2 -2
  33. package/src/assets/web-panel/assets/{Governance-C6G_Wnwi.js → Governance-Blw1esBB.js} +1 -1
  34. package/src/assets/web-panel/assets/{Inference-BnFYa2BF.js → Inference-CoK8Ia0Z.js} +1 -1
  35. package/src/assets/web-panel/assets/{KnowledgeGraph-TqQLEabm.js → KnowledgeGraph-5xrglMTK.js} +1 -1
  36. package/src/assets/web-panel/assets/{Logs-BH4AVh7s.js → Logs-BGIG9798.js} +2 -2
  37. package/src/assets/web-panel/assets/{Marketplace-DoWLTaEE.js → Marketplace-DFqhJMuY.js} +1 -1
  38. package/src/assets/web-panel/assets/{McpTools-D6yC7OG_.js → McpTools-7U8tEhwK.js} +5 -5
  39. package/src/assets/web-panel/assets/{Memory-DLCeHj21.js → Memory-BrvboPLE.js} +2 -2
  40. package/src/assets/web-panel/assets/{MobileBridge-BZBE0GRl.js → MobileBridge-nzcd7d1H.js} +1 -1
  41. package/src/assets/web-panel/assets/{MobileProjects-DJA15G1J.js → MobileProjects-DETVNKa7.js} +1 -1
  42. package/src/assets/web-panel/assets/{Mtc-CyTKJDHn.js → Mtc-SZcCYec3.js} +4 -4
  43. package/src/assets/web-panel/assets/{MtcAudit-DZKjgEVc.js → MtcAudit-Cx_d3N17.js} +5 -5
  44. package/src/assets/web-panel/assets/{Multisig-2rMPNhSO.js → Multisig-DtcmSUo-.js} +3 -3
  45. package/src/assets/web-panel/assets/{NLProgramming-VeMk4Fx-.js → NLProgramming-Dgx2QHl-.js} +1 -1
  46. package/src/assets/web-panel/assets/{Notes-jr2GHMhh.js → Notes-oHTGEHoJ.js} +3 -3
  47. package/src/assets/web-panel/assets/{NotificationSettings-WN9VZyFb.js → NotificationSettings-qKD-IlOl.js} +1 -1
  48. package/src/assets/web-panel/assets/{OrderTableRenderer-CjNO-e9T.js → OrderTableRenderer-CiU4Sy1J.js} +1 -1
  49. package/src/assets/web-panel/assets/{Organization-DdjVHuzT.js → Organization-BBNXnltP.js} +4 -4
  50. package/src/assets/web-panel/assets/{Overflow-l8vBsGGq.js → Overflow-Df2fDbvT.js} +1 -1
  51. package/src/assets/web-panel/assets/{OverrideContext-BlgL9440.js → OverrideContext-C0w0P4A3.js} +1 -1
  52. package/src/assets/web-panel/assets/{P2P-CVQDHHwq.js → P2P-DUHj6zjO.js} +2 -2
  53. package/src/assets/web-panel/assets/{PdhVaultBrowser-CTsNh8qE.js → PdhVaultBrowser-B2nWgdoV.js} +4 -4
  54. package/src/assets/web-panel/assets/{Permissions-DLW87uyK.js → Permissions-CiW0afny.js} +3 -3
  55. package/src/assets/web-panel/assets/{PersonalDataHub-N8OvASTx.js → PersonalDataHub-cc1mHU_D.js} +2 -2
  56. package/src/assets/web-panel/assets/{Pipeline-BPZmdlz2.js → Pipeline-B93zTapU.js} +1 -1
  57. package/src/assets/web-panel/assets/{Privacy-MFZw_GNh.js → Privacy-Db1jXWfd.js} +1 -1
  58. package/src/assets/web-panel/assets/{ProjectInit-zW0pEsfU.js → ProjectInit-DFoZ1h9U.js} +2 -2
  59. package/src/assets/web-panel/assets/{ProjectSettings-CppVHr_o.js → ProjectSettings-BkkUnmYs.js} +2 -2
  60. package/src/assets/web-panel/assets/{Projects-BTglkJO2.js → Projects-Xa779XLC.js} +1 -1
  61. package/src/assets/web-panel/assets/{Providers-CPHZ6JKT.js → Providers-DOF01IEN.js} +1 -1
  62. package/src/assets/web-panel/assets/{QrScannerModal-DO4klNZ2.js → QrScannerModal-TohRsKd0.js} +1 -1
  63. package/src/assets/web-panel/assets/{QuickAsk-B37TTqUa.js → QuickAsk-DjLcr1ZU.js} +1 -1
  64. package/src/assets/web-panel/assets/{Recommend-n7KIfK7y.js → Recommend--Ism6bMx.js} +1 -1
  65. package/src/assets/web-panel/assets/{RemoteSession-SjVoS9mm.js → RemoteSession-B8CL3YHM.js} +2 -2
  66. package/src/assets/web-panel/assets/{Reputation-Dt6RTeHi.js → Reputation-CaRxB3Ep.js} +1 -1
  67. package/src/assets/web-panel/assets/{Row-Dty6jz5V.js → Row-BioxZBBH.js} +1 -1
  68. package/src/assets/web-panel/assets/{RssFeed-DDLhqw5j.js → RssFeed-B2LipwH3.js} +3 -3
  69. package/src/assets/web-panel/assets/{Search-DrYbXi-W.js → Search-BpjLBV8u.js} +1 -1
  70. package/src/assets/web-panel/assets/{Security-Fc9Z6fbB.js → Security-TibHuJWP.js} +3 -3
  71. package/src/assets/web-panel/assets/{Services-BAhPgjSM.js → Services-DtkuWseW.js} +2 -2
  72. package/src/assets/web-panel/assets/{Skeleton-8JF7EPfE.js → Skeleton-BIcPQae6.js} +1 -1
  73. package/src/assets/web-panel/assets/Skills-DoTWIQOX.js +1 -0
  74. package/src/assets/web-panel/assets/{Sla-CSvjcATc.js → Sla-uUSU185m.js} +1 -1
  75. package/src/assets/web-panel/assets/{SpeechSettings-Demx7Gnx.js → SpeechSettings-B7Bgc9mk.js} +1 -1
  76. package/src/assets/web-panel/assets/{SyncSettings-CKLnRWJr.js → SyncSettings-K-XrJ2s9.js} +2 -2
  77. package/src/assets/web-panel/assets/{Tasks-mhITRxjv.js → Tasks-B3hEXNcI.js} +1 -1
  78. package/src/assets/web-panel/assets/{Templates-AjQI5rxD.js → Templates-CaFnyGDK.js} +1 -1
  79. package/src/assets/web-panel/assets/{Tenant-dbtEh1yR.js → Tenant-C2X0PtEC.js} +1 -1
  80. package/src/assets/web-panel/assets/{Terminal-DX-9jjfk.js → Terminal-CKU5yVsT.js} +2 -2
  81. package/src/assets/web-panel/assets/{TimelineRenderer-C11EDZRt.js → TimelineRenderer-8trZeYDC.js} +1 -1
  82. package/src/assets/web-panel/assets/{Tokens-GiA9fiZn.js → Tokens-BUEEe9US.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trigger-BPcnclJn.js → Trigger-COU97I7Y.js} +1 -1
  84. package/src/assets/web-panel/assets/{Trust-C-DVY8nq.js → Trust-DNWQChaP.js} +1 -1
  85. package/src/assets/web-panel/assets/{UkeySign-DxodSwME.js → UkeySign-DWX_VtCp.js} +1 -1
  86. package/src/assets/web-panel/assets/{VideoEditing-D_WV0O8O.js → VideoEditing-D9eIDhsp.js} +1 -1
  87. package/src/assets/web-panel/assets/{Wallet-DSeNlVSU.js → Wallet-BOnCCgIL.js} +4 -4
  88. package/src/assets/web-panel/assets/{WebAuthn-Bwuuvpz1.js → WebAuthn-DN4dD-W8.js} +4 -4
  89. package/src/assets/web-panel/assets/{WorkflowEditor-8M96EU1N.js → WorkflowEditor-ClvDb_jX.js} +1 -1
  90. package/src/assets/web-panel/assets/{chat-D5S6SmLm.js → chat-DIxQ_Eeb.js} +1 -1
  91. package/src/assets/web-panel/assets/{collapseMotion-DVfhbsdP.js → collapseMotion-CqM1QOwM.js} +1 -1
  92. package/src/assets/web-panel/assets/{colors-5fbnGkAP.js → colors-BOX3WKcP.js} +1 -1
  93. package/src/assets/web-panel/assets/{compact-item-DaSuuMlW.js → compact-item-Bny82VDi.js} +1 -1
  94. package/src/assets/web-panel/assets/{createContext-BNOHcXAF.js → createContext-Cnoy3enS.js} +1 -1
  95. package/src/assets/web-panel/assets/devWarning-Dr9dzlQ5.js +1 -0
  96. package/src/assets/web-panel/assets/{echarts-CL0F2SnX.js → echarts-C61Vn3mP.js} +1 -1
  97. package/src/assets/web-panel/assets/{hasIn-DONwZUra.js → hasIn-BJ7fkx3F.js} +1 -1
  98. package/src/assets/web-panel/assets/{icons-BsDmGpcT.js → icons-DEmcnxBf.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-B_yDD9Lm.js → index--CcGiwAI.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-5_fj5gVd.js → index--Xb2lq6E.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-uR5KTNZA.js → index-2N9Y1cLx.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-MOgO5Yvx.js → index-7GLXvG8p.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-CLXf-8ek.js → index-B1-h2QrC.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-D7gs_3Ck.js → index-B6WoW8DJ.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-CztESwB4.js → index-B7Fah_BX.js} +1 -1
  106. package/src/assets/web-panel/assets/index-B92M3t6B.js +1 -0
  107. package/src/assets/web-panel/assets/{index-BEO5f7Vf.js → index-BDkN6YOI.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-_6S41ikV.js → index-BJ8-oLHI.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-CrkCFohi.js → index-BM0mQhVv.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-BrAJSu-v.js → index-BP7HFsAK.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-BcqOK7Lv.js → index-BWPey0Uf.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-BLzMuz38.js → index-BbZynivw.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-DbdQBn4n.js → index-BcmXbIwv.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-KwV9g_cf.js → index-Bk5yST66.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-ClGqO-2X.js → index-Bvheiep2.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-Dd7xwGcD.js → index-C--PPqwi.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-DP2-3A_j.js → index-C1nHEOFE.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-CAYmc5Nh.js → index-CEPTiBWy.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-LRIt-AFl.js → index-CEq5Vh0i.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-Dm5A-MN5.js → index-CN6emwbq.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-Cf2OCa8q.js → index-CQ-xLCVb.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-DHx2lhHW.js → index-CW6n-y1L.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-DM3Ug61e.js → index-CZ6CHiCQ.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-BJp4kRjw.js → index-CZCTan8Q.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-PsMErAB1.js → index-CnjtAKdw.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-CEzd5m5a.js → index-D2qNdrNW.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-B4L3gsNA.js → index-D9QEc7Tq.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-BcWMZqb4.js → index-DBhkJqVF.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-CSfYJqXt.js → index-DSRIjAWO.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-CmUOjyL6.js → index-DxX8hhmL.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-RkInlwV0.js → index-Kio1ekYg.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-CTZeOnzO.js → index-OaNFXV8N.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-BK2N5yVW.js → index-Ty4w18mR.js} +4 -4
  134. package/src/assets/web-panel/assets/{index-Cn5LtyIH.js → index-ceH9DsAi.js} +1 -1
  135. package/src/assets/web-panel/assets/index-d2UcGpK-.js +1 -0
  136. package/src/assets/web-panel/assets/{index-xo2f4DZR.js → index-qOBKrw-g.js} +1 -1
  137. package/src/assets/web-panel/assets/{index-vYtprlNs.js → index-rn6xd12s.js} +1 -1
  138. package/src/assets/web-panel/assets/{initDefaultProps-CxwLynp9.js → initDefaultProps-BmxMBuUZ.js} +1 -1
  139. package/src/assets/web-panel/assets/{motion-XbPvGJbW.js → motion-DnXNwo2k.js} +1 -1
  140. package/src/assets/web-panel/assets/{move-D1u5pcgS.js → move-WnIJwmZN.js} +1 -1
  141. package/src/assets/web-panel/assets/{mtc-parser-BDxhTBMH.js → mtc-parser-DAto-c2w.js} +1 -1
  142. package/src/assets/web-panel/assets/{omit-EtIFGg5k.js → omit-BtvW9W0H.js} +1 -1
  143. package/src/assets/web-panel/assets/{pickAttrs-D33sWb0l.js → pickAttrs-DFqI9-_C.js} +1 -1
  144. package/src/assets/web-panel/assets/{placementArrow-BwFRzaGo.js → placementArrow-Dax6wOK3.js} +1 -1
  145. package/src/assets/web-panel/assets/{responsiveObserve-CBk2WqvQ.js → responsiveObserve-BUFWWC83.js} +1 -1
  146. package/src/assets/web-panel/assets/{slide-B52HtrKn.js → slide-DyARyuSz.js} +1 -1
  147. package/src/assets/web-panel/assets/{statusUtils-D4qxqWnH.js → statusUtils-BZSDugpi.js} +1 -1
  148. package/src/assets/web-panel/assets/{styleChecker-BxaCFlOL.js → styleChecker-B7o_K-4R.js} +1 -1
  149. package/src/assets/web-panel/assets/useFlexGapSupport-Dg7Bg8VG.js +1 -0
  150. package/src/assets/web-panel/assets/{useFs-C6VAU1_6.js → useFs-DQYXo5Rk.js} +1 -1
  151. package/src/assets/web-panel/assets/{useMergedState-Dc9sO2d7.js → useMergedState-DXbJa_zE.js} +1 -1
  152. package/src/assets/web-panel/assets/{usePersonalDataHub-rEG6oXG8.js → usePersonalDataHub-E8NBAnkq.js} +1 -1
  153. package/src/assets/web-panel/assets/{useRefs-CAeFRzPf.js → useRefs-BrCfMLMc.js} +1 -1
  154. package/src/assets/web-panel/assets/{useState-LwExUYDO.js → useState-CnxtbLoC.js} +1 -1
  155. package/src/assets/web-panel/assets/{vendor-BMxUs6UX.js → vendor-Bn1JzzAn.js} +1 -1
  156. package/src/assets/web-panel/assets/{vnode-DOTafkEi.js → vnode-DD71_w1b.js} +1 -1
  157. package/src/assets/web-panel/assets/{zoom-BuwwN5K2.js → zoom-zc9EMd3q.js} +1 -1
  158. package/src/assets/web-panel/index.html +3 -3
  159. package/src/command-manifest.json +36 -1
  160. package/src/commands/agent.js +23 -2
  161. package/src/commands/agents.js +62 -1
  162. package/src/commands/auto-mode.js +87 -0
  163. package/src/commands/background-session.js +499 -0
  164. package/src/commands/command.js +4 -1
  165. package/src/commands/permissions.js +49 -0
  166. package/src/commands/remote-control.js +407 -0
  167. package/src/data/changelog.json +7 -2
  168. package/src/gateways/ws/background-agent-protocol.js +337 -0
  169. package/src/gateways/ws/message-dispatcher.js +10 -0
  170. package/src/gateways/ws/remote-session-protocol.js +102 -1
  171. package/src/gateways/ws/ws-server.js +61 -0
  172. package/src/harness/mcp-client.js +8 -0
  173. package/src/index.js +10 -0
  174. package/src/lib/auto-mode-config.js +396 -0
  175. package/src/lib/background-agent-supervisor.js +202 -17
  176. package/src/lib/background-session-transport.js +315 -0
  177. package/src/lib/permission-denial-store.js +159 -0
  178. package/src/lib/remote-approval-bridge.js +360 -0
  179. package/src/lib/remote-control.js +270 -0
  180. package/src/lib/repl-denials.js +26 -0
  181. package/src/lib/shell-approval.js +4 -0
  182. package/src/lib/ws-rpc-client.js +150 -0
  183. package/src/repl/agent-repl.js +208 -16
  184. package/src/repl/permission-tier.js +31 -0
  185. package/src/runtime/agent-core.js +57 -0
  186. package/src/runtime/headless-runner.js +126 -8
  187. package/src/runtime/headless-stream.js +87 -1
  188. package/src/runtime/mcp-config.js +11 -0
  189. package/src/runtime/mcp-tool-search.js +0 -0
  190. package/src/runtime/policies/agent-policy.js +17 -0
  191. package/src/workers/background-agent-worker.js +218 -37
  192. package/src/assets/web-panel/assets/AppLayout-B6Q6olcr.js +0 -9
  193. package/src/assets/web-panel/assets/ChatBubbleRenderer-mDQl_PXO.js +0 -1
  194. package/src/assets/web-panel/assets/Skills-DL39dY0t.js +0 -1
  195. package/src/assets/web-panel/assets/devWarning-K_woVElh.js +0 -1
  196. package/src/assets/web-panel/assets/index-3ILvaBpo.js +0 -1
  197. package/src/assets/web-panel/assets/index-JpB-FoZq.js +0 -1
  198. package/src/assets/web-panel/assets/useFlexGapSupport-B1lWlqXN.js +0 -1
@@ -0,0 +1,396 @@
1
+ import settingsLoader from "./settings-loader.cjs";
2
+
3
+ const { loadManagedSettings, readSettingsFile, settingsPaths } = settingsLoader;
4
+
5
+ export const AUTO_MODE_SCHEMA = "chainlesschain.auto-mode/v1";
6
+
7
+ export const AUTO_MODE_DEFAULTS = Object.freeze({
8
+ schema: AUTO_MODE_SCHEMA,
9
+ mode: "auto",
10
+ sessionPolicy: "trusted",
11
+ nonInteractiveConfirm: "deny",
12
+ interactiveConfirm: "ask",
13
+ precedence: Object.freeze([
14
+ "managed-settings",
15
+ "permission-rules.deny",
16
+ "permission-rules.ask",
17
+ "permission-rules.allow",
18
+ "shell-policy",
19
+ "approval-gate",
20
+ "hooks",
21
+ ]),
22
+ decisions: Object.freeze([
23
+ Object.freeze({
24
+ match: Object.freeze({ riskLevel: "low" }),
25
+ decision: "allow",
26
+ reason: "read-only and low-risk tools do not require approval",
27
+ }),
28
+ Object.freeze({
29
+ match: Object.freeze({ riskLevel: "medium" }),
30
+ decision: "allow",
31
+ reason: "auto maps to the trusted ApprovalGate policy",
32
+ }),
33
+ Object.freeze({
34
+ match: Object.freeze({ riskLevel: "high" }),
35
+ decision: "ask",
36
+ nonInteractiveDecision: "deny",
37
+ reason: "dangerous execution still needs approval",
38
+ }),
39
+ ]),
40
+ settings: Object.freeze({
41
+ classifyAllShell: false,
42
+ }),
43
+ });
44
+
45
+ function clone(value) {
46
+ return JSON.parse(JSON.stringify(value));
47
+ }
48
+
49
+ function isPlainObject(value) {
50
+ return value && typeof value === "object" && !Array.isArray(value);
51
+ }
52
+
53
+ function autoModeFromSettings(data) {
54
+ if (!isPlainObject(data?.autoMode)) return null;
55
+ return clone(data.autoMode);
56
+ }
57
+
58
+ /**
59
+ * Layered merge for `autoMode` settings. NOT mergeSandboxSettings: that
60
+ * helper's array branch stringifies elements (`map(String)`) for sandbox
61
+ * allowlists, which destroys array-form `decisions` rule objects into
62
+ * "[object Object]". Here `decisions` arrays are an ORDERED RULE LIST — a
63
+ * closer layer replaces the whole list; everything else keeps the familiar
64
+ * closer-scalar-wins / deep-object-merge semantics.
65
+ */
66
+ function mergeAutoModeSettings(base, overlay) {
67
+ const out = isPlainObject(base) ? clone(base) : {};
68
+ for (const [key, value] of Object.entries(overlay || {})) {
69
+ if (Array.isArray(value)) {
70
+ out[key] = clone(value);
71
+ } else if (isPlainObject(value)) {
72
+ out[key] = mergeAutoModeSettings(out[key], value);
73
+ } else if (["string", "boolean", "number"].includes(typeof value)) {
74
+ out[key] = value;
75
+ }
76
+ }
77
+ return out;
78
+ }
79
+
80
+ export function loadAutoModeConfig(opts = {}) {
81
+ const cwd = opts.cwd || process.cwd();
82
+ let effective = clone(AUTO_MODE_DEFAULTS.settings);
83
+ const files = [];
84
+
85
+ for (const file of settingsPaths(cwd, opts.settingsFile)) {
86
+ const data = readSettingsFile(file, { onWarn: opts.onWarn });
87
+ const autoMode = autoModeFromSettings(data);
88
+ if (!autoMode) continue;
89
+ effective = mergeAutoModeSettings(effective, autoMode);
90
+ files.push(file);
91
+ }
92
+
93
+ const managed = loadManagedSettings(opts);
94
+ let managedFile = null;
95
+ if (managed.settings) {
96
+ const autoMode = autoModeFromSettings(managed.settings);
97
+ if (autoMode) {
98
+ effective = mergeAutoModeSettings(effective, autoMode);
99
+ managedFile = managed.file;
100
+ files.push(managed.file);
101
+ }
102
+ }
103
+
104
+ return {
105
+ schema: AUTO_MODE_SCHEMA,
106
+ defaults: clone(AUTO_MODE_DEFAULTS.settings),
107
+ effective,
108
+ files,
109
+ managedFile,
110
+ };
111
+ }
112
+
113
+ export function autoModeDefaultsDocument() {
114
+ return clone(AUTO_MODE_DEFAULTS);
115
+ }
116
+
117
+ const RISK_LEVELS = Object.freeze(["low", "medium", "high"]);
118
+ const DECISION_VALUES = Object.freeze(["allow", "ask", "deny"]);
119
+
120
+ function normalizeDecisionValue(value) {
121
+ const v = typeof value === "string" ? value.toLowerCase() : null;
122
+ if (v === "confirm") return "ask";
123
+ return DECISION_VALUES.includes(v) ? v : null;
124
+ }
125
+
126
+ function defaultDecisionMap() {
127
+ const map = {};
128
+ for (const rule of AUTO_MODE_DEFAULTS.decisions) {
129
+ map[rule.match.riskLevel] = {
130
+ decision: rule.decision,
131
+ reason: rule.reason,
132
+ source: "default",
133
+ };
134
+ }
135
+ return map;
136
+ }
137
+
138
+ /**
139
+ * Compile a shell-style glob (`*` wildcard only) into an anchored RegExp.
140
+ * Case-sensitive on purpose: match patterns are user-vetted capabilities and
141
+ * a looser match could over-authorize.
142
+ */
143
+ function compileGlob(pattern) {
144
+ const escaped = String(pattern)
145
+ .replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
146
+ .replace(/\\\*/g, ".*");
147
+ return new RegExp(`^${escaped}$`);
148
+ }
149
+
150
+ /**
151
+ * Resolve the effective decision rules for `--permission-mode auto`.
152
+ *
153
+ * Accepts `autoMode.decisions` in two shapes:
154
+ * - object: `{ "medium": "ask", "high": { "decision": "deny", "reason": "…" } }`
155
+ * (riskLevel-level overrides only)
156
+ * - array (same shape as the defaults document):
157
+ * `[{ "match": { "riskLevel": "medium" }, "decision": "ask", "reason": "…" }]`
158
+ * Array rules may additionally match on `tool` (exact name) and/or
159
+ * `commandPattern` (a `*` glob against the shell command). Rules with a
160
+ * tool/commandPattern are FINE-GRAINED: they are tried in declaration
161
+ * order before the riskLevel map, first full match wins.
162
+ *
163
+ * Invalid risk levels / decision values / non-string patterns are ignored
164
+ * (fail to defaults) so a typo in settings.json can never loosen NOR silently
165
+ * break the gate.
166
+ *
167
+ * @param {object} [effectiveSettings] merged `autoMode` settings
168
+ * @returns {{
169
+ * map: Record<string,{decision:string,reason:string,source:string}>,
170
+ * rules: Array<{match:object,decision:string,reason:string,_test:(ctx:object)=>boolean}>,
171
+ * customized: boolean,
172
+ * }}
173
+ */
174
+ export function resolveAutoModeDecisions(effectiveSettings = {}) {
175
+ const map = defaultDecisionMap();
176
+ const rules = [];
177
+ let customized = false;
178
+
179
+ const raw = effectiveSettings?.decisions;
180
+ const entries = [];
181
+ if (Array.isArray(raw)) {
182
+ for (const rule of raw) {
183
+ if (!isPlainObject(rule)) continue;
184
+ const match = isPlainObject(rule.match) ? rule.match : {};
185
+ const decision = normalizeDecisionValue(rule.decision);
186
+ if (!decision) continue;
187
+ const hasTool = typeof match.tool === "string" && match.tool.trim();
188
+ const hasPattern =
189
+ typeof match.commandPattern === "string" && match.commandPattern.trim();
190
+ if (hasTool || hasPattern) {
191
+ // Fine-grained rule: tried in order before the riskLevel map.
192
+ const riskLevel =
193
+ typeof match.riskLevel === "string" &&
194
+ RISK_LEVELS.includes(match.riskLevel.toLowerCase())
195
+ ? match.riskLevel.toLowerCase()
196
+ : null;
197
+ const tool = hasTool ? match.tool.trim() : null;
198
+ const pattern = hasPattern ? match.commandPattern.trim() : null;
199
+ const regex = pattern ? compileGlob(pattern) : null;
200
+ const reason =
201
+ typeof rule.reason === "string" && rule.reason.trim()
202
+ ? rule.reason.trim()
203
+ : `autoMode.decisions rule matches ${[
204
+ tool ? `tool=${tool}` : null,
205
+ pattern ? `command=${pattern}` : null,
206
+ riskLevel ? `risk=${riskLevel}` : null,
207
+ ]
208
+ .filter(Boolean)
209
+ .join(" ")} → ${decision}`;
210
+ rules.push({
211
+ match: {
212
+ ...(riskLevel ? { riskLevel } : {}),
213
+ ...(tool ? { tool } : {}),
214
+ ...(pattern ? { commandPattern: pattern } : {}),
215
+ },
216
+ decision,
217
+ reason,
218
+ _test: (ctx) => {
219
+ if (riskLevel) {
220
+ const r = RISK_LEVELS.includes(ctx.riskLevel)
221
+ ? ctx.riskLevel
222
+ : "low";
223
+ if (r !== riskLevel) return false;
224
+ }
225
+ if (tool && ctx.tool !== tool) return false;
226
+ if (regex && !regex.test(String(ctx.args?.command ?? ""))) {
227
+ return false;
228
+ }
229
+ return true;
230
+ },
231
+ });
232
+ customized = true;
233
+ continue;
234
+ }
235
+ entries.push([match.riskLevel, rule.decision, rule.reason]);
236
+ }
237
+ } else if (isPlainObject(raw)) {
238
+ for (const [riskLevel, value] of Object.entries(raw)) {
239
+ if (isPlainObject(value)) {
240
+ entries.push([riskLevel, value.decision, value.reason]);
241
+ } else {
242
+ entries.push([riskLevel, value, undefined]);
243
+ }
244
+ }
245
+ }
246
+
247
+ for (const [riskLevelRaw, decisionRaw, reasonRaw] of entries) {
248
+ const riskLevel =
249
+ typeof riskLevelRaw === "string" ? riskLevelRaw.toLowerCase() : null;
250
+ if (!RISK_LEVELS.includes(riskLevel)) continue;
251
+ const decision = normalizeDecisionValue(decisionRaw);
252
+ if (!decision) continue;
253
+ const reason =
254
+ typeof reasonRaw === "string" && reasonRaw.trim()
255
+ ? reasonRaw.trim()
256
+ : `autoMode.decisions maps ${riskLevel} risk to ${decision}`;
257
+ if (map[riskLevel].decision !== decision) customized = true;
258
+ map[riskLevel] = { decision, reason, source: "settings" };
259
+ }
260
+
261
+ return { map, rules, customized };
262
+ }
263
+
264
+ /**
265
+ * Wrap a session-core ApprovalGate so `--permission-mode auto` consults the
266
+ * user-configured riskLevel → decision map instead of the fixed trusted-policy
267
+ * tier. Only wired when `resolveAutoModeDecisions` reports `customized` — the
268
+ * unconfigured path keeps the byte-identical trusted mapping.
269
+ *
270
+ * The wrapper cannot up-authorize hard shell-policy denies: those return from
271
+ * `evaluateShellCommandWithApproval` before the gate is ever consulted.
272
+ *
273
+ * @param {object} inner session-core ApprovalGate (or compatible)
274
+ * @param {ReturnType<typeof resolveAutoModeDecisions>} resolved
275
+ * @param {{ isActive?: () => boolean }} [opts] `isActive` lets a host that can
276
+ * switch permission modes mid-session (the REPL) leave the wrapper
277
+ * installed permanently and toggle it: when it returns false, decide()
278
+ * delegates untouched to the inner gate. Omitted → always active
279
+ * (headless runs pick the mode once per process).
280
+ */
281
+ export function createAutoModeApprovalGate(inner, resolved, opts = {}) {
282
+ const map = resolved?.map || defaultDecisionMap();
283
+ const rules = Array.isArray(resolved?.rules) ? resolved.rules : [];
284
+ const isActive =
285
+ typeof opts.isActive === "function" ? opts.isActive : () => true;
286
+ let confirm = null;
287
+
288
+ return {
289
+ isAutoModeGate: true,
290
+ setSessionPolicy(sessionId, policy) {
291
+ return inner?.setSessionPolicy?.(sessionId, policy);
292
+ },
293
+ getSessionPolicy(sessionId) {
294
+ return inner?.getSessionPolicy?.(sessionId);
295
+ },
296
+ clearSessionPolicy(sessionId) {
297
+ return inner?.clearSessionPolicy?.(sessionId);
298
+ },
299
+ setConfirmer(fn) {
300
+ confirm = typeof fn === "function" ? fn : null;
301
+ inner?.setConfirmer?.(fn);
302
+ },
303
+ hasConfirmer() {
304
+ return typeof confirm === "function";
305
+ },
306
+ async decide(ctx = {}) {
307
+ if (!isActive()) return inner.decide(ctx);
308
+ const riskLevel = RISK_LEVELS.includes(ctx.riskLevel)
309
+ ? ctx.riskLevel
310
+ : "low";
311
+ // Fine-grained rules (tool / commandPattern) run in declaration order
312
+ // before the riskLevel map — first full match wins.
313
+ let rule = null;
314
+ for (const candidate of rules) {
315
+ let matched = false;
316
+ try {
317
+ matched = candidate._test({ ...ctx, riskLevel });
318
+ } catch {
319
+ matched = false; // a broken matcher never decides anything
320
+ }
321
+ if (matched) {
322
+ rule = {
323
+ decision: candidate.decision,
324
+ reason: candidate.reason,
325
+ source: "settings",
326
+ match: candidate.match,
327
+ };
328
+ break;
329
+ }
330
+ }
331
+ if (!rule) {
332
+ const fromMap = map[riskLevel];
333
+ rule = {
334
+ decision: fromMap.decision,
335
+ reason: fromMap.reason,
336
+ source: fromMap.source,
337
+ };
338
+ }
339
+ const common = {
340
+ policy: "auto-mode",
341
+ riskLevel,
342
+ reason: rule.reason,
343
+ rule: {
344
+ riskLevel,
345
+ decision: rule.decision,
346
+ source: rule.source,
347
+ ...(rule.match ? { match: rule.match } : {}),
348
+ },
349
+ };
350
+ if (rule.decision === "allow") {
351
+ return {
352
+ decision: "allow",
353
+ via: "auto-mode-config",
354
+ base: "allow",
355
+ ...common,
356
+ };
357
+ }
358
+ if (rule.decision === "deny") {
359
+ return {
360
+ decision: "deny",
361
+ via: "auto-mode-config",
362
+ base: "deny",
363
+ ...common,
364
+ };
365
+ }
366
+ // ask — same confirm semantics as the session-core gate: no confirmer
367
+ // means fail-closed deny (headless installs a deny-confirmer anyway).
368
+ if (typeof confirm !== "function") {
369
+ return {
370
+ decision: "deny",
371
+ via: "no-confirmer",
372
+ base: "confirm",
373
+ ...common,
374
+ };
375
+ }
376
+ let ok = false;
377
+ try {
378
+ ok = await confirm(ctx);
379
+ } catch (error) {
380
+ return {
381
+ decision: "deny",
382
+ via: "confirm-error",
383
+ base: "confirm",
384
+ error,
385
+ ...common,
386
+ };
387
+ }
388
+ return {
389
+ decision: ok ? "allow" : "deny",
390
+ via: ok ? "user-confirm" : "user-deny",
391
+ base: "confirm",
392
+ ...common,
393
+ };
394
+ },
395
+ };
396
+ }
@@ -17,6 +17,23 @@ import { getHomeDir } from "./paths.js";
17
17
 
18
18
  export const _deps = { spawn, spawnSync };
19
19
 
20
+ export const DEFAULT_HEARTBEAT_INTERVAL_MS = 5000;
21
+ export const DEFAULT_HEARTBEAT_STALE_MS = 120000;
22
+
23
+ function nowMs(options = {}) {
24
+ return typeof options.now === "number" ? options.now : Date.now();
25
+ }
26
+
27
+ function heartbeatStaleMs(options = {}) {
28
+ const configured =
29
+ options.heartbeatStaleMs ||
30
+ process.env.CC_BACKGROUND_AGENT_HEARTBEAT_STALE_MS;
31
+ const n = Number(configured);
32
+ return Number.isFinite(n) && n > 0
33
+ ? Math.floor(n)
34
+ : DEFAULT_HEARTBEAT_STALE_MS;
35
+ }
36
+
20
37
  export function backgroundAgentsDir() {
21
38
  const dir =
22
39
  process.env.CC_BACKGROUND_AGENTS_DIR ||
@@ -73,12 +90,43 @@ export function isProcessAlive(pid) {
73
90
  }
74
91
  }
75
92
 
76
- export function effectiveBackgroundAgentState(state) {
93
+ export function normalizeBackgroundAgentTitle(title) {
94
+ const value = String(title || "").trim();
95
+ if (!value) throw new Error("Background agent title cannot be empty");
96
+ return value.slice(0, 160);
97
+ }
98
+
99
+ export function effectiveBackgroundAgentState(state, options = {}) {
77
100
  if (!state) return null;
78
- if (state.status === "running" && !isProcessAlive(state.pid)) {
79
- return { ...state, status: "lost", endedAt: state.endedAt || Date.now() };
101
+ if (state.status !== "running") return state;
102
+ const t = nowMs(options);
103
+ let next = state;
104
+ if (
105
+ Number.isFinite(Number(state.heartbeatAt)) &&
106
+ t - Number(state.heartbeatAt) > heartbeatStaleMs(options)
107
+ ) {
108
+ next = {
109
+ ...state,
110
+ status: "lost",
111
+ endedAt: state.endedAt || t,
112
+ lostReason: "heartbeat-stale",
113
+ };
114
+ } else if (!isProcessAlive(state.pid)) {
115
+ next = {
116
+ ...state,
117
+ status: "lost",
118
+ endedAt: state.endedAt || t,
119
+ lostReason: "process-exited",
120
+ };
80
121
  }
81
- return state;
122
+ if (next !== state && options.persist !== false) {
123
+ try {
124
+ writeBackgroundAgentState(next);
125
+ } catch {
126
+ /* best-effort; callers still get the corrected state */
127
+ }
128
+ }
129
+ return next;
82
130
  }
83
131
 
84
132
  export function listBackgroundAgents(options = {}) {
@@ -86,17 +134,137 @@ export function listBackgroundAgents(options = {}) {
86
134
  .filter((name) => name.endsWith(".json") && !name.includes(".job."))
87
135
  .map((name) => readBackgroundAgentState(name.slice(0, -5)))
88
136
  .filter(Boolean)
89
- .map(effectiveBackgroundAgentState)
137
+ .map((state) => effectiveBackgroundAgentState(state, options))
90
138
  .filter((state) => options.all || state.status === "running")
91
139
  .sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0));
92
140
  }
93
141
 
142
+ function sleepSyncMs(ms) {
143
+ try {
144
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
145
+ } catch {
146
+ const until = Date.now() + ms;
147
+ while (Date.now() < until) {
148
+ /* fallback busy-wait — only a few ms */
149
+ }
150
+ }
151
+ }
152
+
153
+ export function renameBackgroundAgent(id, title, options = {}) {
154
+ const state = effectiveBackgroundAgentState(readBackgroundAgentState(id), {
155
+ now: options.now,
156
+ heartbeatStaleMs: options.heartbeatStaleMs,
157
+ });
158
+ if (!state) throw new Error(`Background agent not found: ${id}`);
159
+ const normalized = normalizeBackgroundAgentTitle(title);
160
+ const renamedAt = typeof options.now === "number" ? options.now : Date.now();
161
+ // The worker persists state via read-modify-write (heartbeat / transport /
162
+ // turn merges). A rename that lands inside one of those read→write windows
163
+ // gets clobbered by the worker's stale snapshot — so verify after a short
164
+ // beat and re-apply onto the freshest state (bounded retries; the window is
165
+ // milliseconds wide, worst during worker startup's write burst).
166
+ let next = { ...state, title: normalized, renamedAt };
167
+ for (let attempt = 0; attempt < 4; attempt++) {
168
+ writeBackgroundAgentState(next);
169
+ sleepSyncMs(15);
170
+ const current = readBackgroundAgentState(id) || next;
171
+ if (current.title === normalized) {
172
+ return { ...current, title: normalized, renamedAt };
173
+ }
174
+ next = { ...current, title: normalized, renamedAt };
175
+ }
176
+ writeBackgroundAgentState(next);
177
+ return next;
178
+ }
179
+
180
+ /**
181
+ * Build the follow-up argv template for interactive attach turns: the launch
182
+ * argv minus every token that carried the FIRST turn's prompt (positional
183
+ * task words, `-p/--print` and its inline value). The worker appends
184
+ * `["-p", "<follow-up text>"]` per turn, so all remaining flags (model,
185
+ * permission-mode, session id, …) keep applying to later turns.
186
+ *
187
+ * @param {string[]} argv the background child argv (before any piped-prompt
188
+ * token is appended)
189
+ * @param {object} [opts]
190
+ * @param {string[]} [opts.positionalTokens] raw positional prompt tokens
191
+ * @param {string|null} [opts.printValue] inline `-p <value>` text, when the
192
+ * prompt came from --print
193
+ */
194
+ export function buildFollowUpArgv(argv, opts = {}) {
195
+ const positionalLeft = Array.isArray(opts.positionalTokens)
196
+ ? [...opts.positionalTokens]
197
+ : [];
198
+ const printValue =
199
+ typeof opts.printValue === "string" && opts.printValue.trim()
200
+ ? opts.printValue
201
+ : null;
202
+ const out = [];
203
+ for (let i = 0; i < argv.length; i++) {
204
+ const arg = argv[i];
205
+ if (arg === "-p" || arg === "--print") {
206
+ if (printValue !== null && argv[i + 1] === printValue) i++;
207
+ continue;
208
+ }
209
+ // equals-form: --print=<value> carries the prompt in the same token
210
+ if (
211
+ printValue !== null &&
212
+ arg.startsWith("--print=") &&
213
+ arg.slice("--print=".length) === printValue
214
+ ) {
215
+ continue;
216
+ }
217
+ if (positionalLeft.length && arg === positionalLeft[0]) {
218
+ positionalLeft.shift();
219
+ continue;
220
+ }
221
+ out.push(arg);
222
+ }
223
+ return out;
224
+ }
225
+
226
+ /**
227
+ * Continue a finished/crashed background session as a NEW background session
228
+ * on the same conversation (`cc daemon resume <id> <prompt>`). The original
229
+ * launch argv is intentionally NOT persisted (it may carry secrets), so the
230
+ * resume runs a minimal `agent --session <sid> -p <prompt>` — model/provider
231
+ * come from config defaults, and the headless runner replays the JSONL
232
+ * transcript for the session id.
233
+ */
234
+ export function resumeBackgroundAgent(id, prompt, options = {}) {
235
+ const state = effectiveBackgroundAgentState(readBackgroundAgentState(id), {
236
+ now: options.now,
237
+ heartbeatStaleMs: options.heartbeatStaleMs,
238
+ });
239
+ if (!state) throw new Error(`Background agent not found: ${id}`);
240
+ if (state.status === "running") {
241
+ throw new Error(
242
+ `Background agent ${id} is still running — use cc attach ${id} to send follow-up prompts instead`,
243
+ );
244
+ }
245
+ if (!state.sessionId) {
246
+ throw new Error(`Background agent ${id} has no session id to resume from`);
247
+ }
248
+ const text = String(prompt || "").trim();
249
+ if (!text) throw new Error("resume requires a prompt");
250
+ const argv = ["agent", "--session", state.sessionId, "-p", text];
251
+ return launchBackgroundAgent({
252
+ argv,
253
+ cwd: options.cwd || state.cwd || process.cwd(),
254
+ sessionId: state.sessionId,
255
+ title: options.title || state.title || text.slice(0, 100),
256
+ cliEntry: options.cliEntry,
257
+ followUpArgv: ["agent", "--session", state.sessionId],
258
+ });
259
+ }
260
+
94
261
  export function launchBackgroundAgent({
95
262
  argv,
96
263
  cwd,
97
264
  sessionId,
98
265
  title,
99
266
  cliEntry,
267
+ followUpArgv,
100
268
  }) {
101
269
  const id = createBackgroundAgentId();
102
270
  const dir = backgroundAgentsDir();
@@ -112,8 +280,30 @@ export function launchBackgroundAgent({
112
280
  title: title || "Background agent",
113
281
  cliEntry: cliEntry || process.argv[1],
114
282
  logFile: logPath(id),
283
+ // Present = interactive attach can start follow-up turns; absent = the
284
+ // transport rejects prompts (log-only session).
285
+ ...(Array.isArray(followUpArgv) ? { followUpArgv } : {}),
115
286
  };
116
287
  writeFileSync(jobFile, JSON.stringify(job), { mode: 0o600 });
288
+ // Write the initial state BEFORE spawning so the worker's own merges (the
289
+ // transport endpoint lands within its first ~100ms) can never be clobbered
290
+ // by a late launcher write racing the worker.
291
+ const state = {
292
+ id,
293
+ sessionId,
294
+ title: job.title,
295
+ cwd,
296
+ pid: null,
297
+ workerPid: null,
298
+ agentPid: null,
299
+ status: "running",
300
+ startedAt: Date.now(),
301
+ heartbeatAt: Date.now(),
302
+ endedAt: null,
303
+ exitCode: null,
304
+ logFile: job.logFile,
305
+ };
306
+ writeBackgroundAgentState(state);
117
307
  let child;
118
308
  try {
119
309
  child = _deps.spawn(process.execPath, [worker, jobFile], {
@@ -124,23 +314,18 @@ export function launchBackgroundAgent({
124
314
  });
125
315
  } catch (error) {
126
316
  rmSync(jobFile, { force: true });
317
+ rmSync(statePath(id), { force: true });
127
318
  throw error;
128
319
  }
129
320
  child.unref();
130
- const state = {
131
- id,
132
- sessionId,
133
- title: job.title,
134
- cwd,
321
+ const current = readBackgroundAgentState(id) || state;
322
+ const withPid = {
323
+ ...current,
135
324
  pid: child.pid,
136
- status: "running",
137
- startedAt: Date.now(),
138
- endedAt: null,
139
- exitCode: null,
140
- logFile: job.logFile,
325
+ workerPid: child.pid,
141
326
  };
142
- writeBackgroundAgentState(state);
143
- return state;
327
+ writeBackgroundAgentState(withPid);
328
+ return withPid;
144
329
  }
145
330
 
146
331
  export function readBackgroundAgentLog(id, options = {}) {