chainlesschain 0.162.158 → 0.162.159

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 (201) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-BwpTJTN_.js → AIOps-ByIiF5bT.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-BtQOoo3l.js → ActionButton-NlgSFZq5.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-Duf_rXZa.js → Analytics-D17cOUcb.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-XQK3wjvB.js → AppLayout-B4mEZcbl.js} +4 -4
  6. package/src/assets/web-panel/assets/Artifacts-CASsmyQw.js +1 -0
  7. package/src/assets/web-panel/assets/{Audit-DNjOGQLh.js → Audit-DYAia2e2.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-FH9RAfB5.js → BackgroundAgents-C1lvYUSu.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-ZIbrR4-U.js → Backup-B6agK6bA.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-DSt3NaND.js → BaseInput-DM43R2vy.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-CY-wvbjv.js → Chat-Dtdp3eXp.js} +5 -5
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-BAV9V-t-.js → ChatBubbleRenderer-BL5c_OQ1.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-8JVadE_B.js → Checkbox-D2xf7uZt.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-CN83nPYI.js → Codegen-D3j45jJL.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-xPSvqdN8.js → Col-voPJ9fUZ.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-D78cnIjG.js → Community-DEsyTESF.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-CSw6Csbp.js → Compact-OHV_QXLD.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-E_HhjcYR.js → Compliance-eDTJWC-J.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-DybeQjmm.js → Cowork-BBvy-6d1.js} +3 -3
  20. package/src/assets/web-panel/assets/{Cron-DS0BJeLY.js → Cron-Dw2eoJxu.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-BOwVgkbs.js → Crosschain-DQHms_Bh.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-DRUr9YuX.js → DID-Cyu7Id1c.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-Cht-yUKW.js → Dashboard-sIDzkG92.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-BLr_EJ9X.js → Dropdown-DUW3T2ey.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-Drexuh5W.js → EmailListRenderer-CECELXW8.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-D9Q2Kq63.js → FamilyGuardDashboard-BIZR37ov.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-C-Z1xI-T.js → Federation-BEZIQX84.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-DLHPMkQF.js → FormItemContext-BcdUMznL.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-DxxEdn2r.js → GenericCardRenderer-DsN_fr5H.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-syATcaBt.js → Git-63LhjBx2.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-CYe_r8Zf.js → Governance-CYtQtIcH.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-GWxErB7k.js → Inference-BwR6UUox.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-qQvVCZoi.js → KnowledgeGraph-D9lVNTUA.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-BenAU2W8.js → Logs-C4q5h7Ut.js} +2 -2
  35. package/src/assets/web-panel/assets/{MarkdownRenderer-7agE3Or1.js → MarkdownRenderer-DexLlXfN.js} +1 -1
  36. package/src/assets/web-panel/assets/{Marketplace-D4Jn5fpD.js → Marketplace-Plksliuw.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-DtAwoqiQ.js → McpTools-DZUFfeuG.js} +4 -4
  38. package/src/assets/web-panel/assets/{Memory-BI_O50I8.js → Memory-NoDYt29V.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-CQWE2lqx.js → MobileBridge-CEXGTkdh.js} +1 -1
  40. package/src/assets/web-panel/assets/{MobileProjects-CVtTqgtg.js → MobileProjects-DnbJkbZo.js} +1 -1
  41. package/src/assets/web-panel/assets/{Mtc-Dnr16qsn.js → Mtc-DTAa0cwy.js} +5 -5
  42. package/src/assets/web-panel/assets/{MtcAudit-CQ5Xe4-a.js → MtcAudit-CVV7LnNS.js} +2 -2
  43. package/src/assets/web-panel/assets/{Multisig-Bjr8O-wy.js → Multisig-BqqGrjt_.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-CEe-KirU.js → NLProgramming-C4WP8DRp.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-spKJIKF9.js → Notes-Zo4DJUVI.js} +4 -4
  46. package/src/assets/web-panel/assets/{NotificationSettings-DRp83LFG.js → NotificationSettings-DBcjRaz2.js} +1 -1
  47. package/src/assets/web-panel/assets/OrderTableRenderer-BCQel9bD.js +1 -0
  48. package/src/assets/web-panel/assets/{Organization-BlgB0IYg.js → Organization-BT9WPBOX.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-a_35HqZI.js → Overflow-D6b4bZ3q.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-CSNHCNPC.js → P2P-Cw3OsYx-.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-C8M1JH-1.js → PdhVaultBrowser-C0nyiK2b.js} +5 -5
  52. package/src/assets/web-panel/assets/{Permissions-DmjJi9Ms.js → Permissions-zVBZEB_C.js} +3 -3
  53. package/src/assets/web-panel/assets/{PersonalDataHub-DfuwRyf4.js → PersonalDataHub-BOspsFew.js} +2 -2
  54. package/src/assets/web-panel/assets/{Pipeline-B2-S4ekE.js → Pipeline-CQWfZ4XH.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-B1dOiBLC.js → Privacy-C2oGJ_WL.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-_pNmc-DX.js → ProjectInit-BZMR4qY1.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-DmwG2Ii1.js → ProjectSettings-DAujoP63.js} +2 -2
  58. package/src/assets/web-panel/assets/{Projects-COATmHxF.js → Projects-DvH_P2Uq.js} +1 -1
  59. package/src/assets/web-panel/assets/{Providers-C6HSjg1d.js → Providers-D_RGHM-v.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-CrDmS7l1.js → QrScannerModal-Mf6KfL2J.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-CfKvWt-x.js → QuickAsk-5D8URoI_.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-DMa6Kuzv.js → Recommend-Df4kC8Gy.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-DrktUo5x.js → RemoteSession-DGi3f3vO.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-BTPQ1vjL.js → Reputation-jmkGFNWb.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-B6Q7QkMG.js → Row-DP6Pcu3m.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-B1IJhAmX.js → RssFeed-D60hSuPO.js} +3 -3
  67. package/src/assets/web-panel/assets/{Search-xbea1kGD.js → Search-0mf1BQUK.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-7rs0cO3F.js → Security-CfZtLFYV.js} +4 -4
  69. package/src/assets/web-panel/assets/{Services-0fjixK0S.js → Services-D9zpZLWz.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-B_rVq9Cu.js → Skeleton-D1_44eMD.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-C5uM90b9.js → Skills-cVFa8zkz.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-U4pYCIZH.js → Sla-Cinpzc3L.js} +1 -1
  73. package/src/assets/web-panel/assets/{SpeechSettings-Cdz4yTWx.js → SpeechSettings-bqdiJE_P.js} +1 -1
  74. package/src/assets/web-panel/assets/{SyncSettings-Me3aetHv.js → SyncSettings-Dgl_SLgj.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-Cf6aNkL_.js → Tasks-DawWAEhy.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-CYatk4tU.js → Templates-Ca0nY651.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-DIMKLw7o.js → Tenant-CgaGEhLm.js} +1 -1
  78. package/src/assets/web-panel/assets/{Terminal-C_cp-uSJ.js → Terminal-D-9EnpRx.js} +2 -2
  79. package/src/assets/web-panel/assets/{TimelineRenderer-cUPVNADp.js → TimelineRenderer-CSOxDHZ6.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-CiaEIXVf.js → Tokens-BpwLZpL3.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-DB1WU5lQ.js → Trigger-DgowwBK5.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-Cfht9Ijy.js → Trust-BZutapjE.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-K3P4Vt9S.js → UkeySign-QZPt8_Sp.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-DWjhjXRh.js → VideoEditing-DlQx5UDJ.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-D9-Q1Kan.js → Wallet-ZfdTgtWA.js} +4 -4
  86. package/src/assets/web-panel/assets/{WebAuthn-BDszy-oV.js → WebAuthn-CAp0y_Cr.js} +5 -5
  87. package/src/assets/web-panel/assets/{WorkflowEditor-DIhij4rA.js → WorkflowEditor-BVITZEmr.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-D7VCiKbh.js → chat-CC2tPaNg.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-BvOd9pVn.js → colors-31brc9CA.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-DoqXBiAO.js → compact-item-DLMkC9C-.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-CrKRzUtp.js → createContext-BMtveI6F.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-CB8pPOtd.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-BWAev4WC.js → hasIn-DbD6WHW2.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-B30NvR21.js → index-3oBsXZmF.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-Cx7M7PSF.js → index-ALvoNTRF.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-BHOcVYrx.js → index-AaIKG60c.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-BXyl8O23.js → index-BFRLmCR3.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-YGDy7nNq.js → index-BKAB6-6V.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-BoYrS_L2.js → index-BOM-VFlv.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-Bq_ctRpz.js → index-BPpS-lGb.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-CDGLdn12.js → index-Bbc2JeQG.js} +1 -1
  102. package/src/assets/web-panel/assets/index-BhXiaiev.js +1 -0
  103. package/src/assets/web-panel/assets/{index-ISvPZ05r.js → index-BiRQUoyE.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-B0vGEbJT.js → index-BnFLzejT.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-Bj7EgmOp.js → index-BoO4WEJB.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-BZGk8AbZ.js → index-C-cGlCDW.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-BkgnGX7v.js → index-C1PfeI88.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-HwHdTusY.js → index-C21pdBqs.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-Bf9YlbDn.js → index-C7LHNw_N.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-oY45XrkG.js → index-CEXa-4zd.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-KR1-Em9C.js → index-CEwjBA9q.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-C5kRmj3P.js → index-CFXKbOek.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-BN9m4Ux2.js → index-CJhREOlG.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-PDxGjKRu.js → index-COHHQ9cU.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-D23L1V76.js → index-CUmLMFsy.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-zH-tuuGZ.js → index-CWgfipcC.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-CmJ6xKBO.js → index-C_eUKGYP.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-CEPHMSih.js → index-CcVszn2b.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DV-Rfmfx.js → index-Cod97Urs.js} +3 -3
  120. package/src/assets/web-panel/assets/{index-BWWsaimP.js → index-Cwbqh0KZ.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-8g5TJCZ3.js → index-D67kIagl.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-CEXfKSiI.js → index-D7cV0tu0.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-CoD31Ndm.js → index-DJvxFyIz.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-CSb-tnIo.js → index-DZl-UXB6.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-DvDOpsJB.js → index-Dm0byjm1.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-DlGu7As6.js → index-DndNnYc0.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-gmxqu5Kk.js → index-WQUO2vvK.js} +1 -1
  128. package/src/assets/web-panel/assets/index-WpaYaE3D.js +1 -0
  129. package/src/assets/web-panel/assets/{index-ww1QtLnX.js → index-bdaJwTXJ.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-JzUmJMvn.js → index-ep6JchNG.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-BCyP071m.js → index-jG6h22Qg.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-1poMpMU0.js → index-ntVQqSML.js} +1 -1
  133. package/src/assets/web-panel/assets/{initDefaultProps-XW-OR3mL.js → initDefaultProps-D5UoBRhN.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-CeuEHh20.js → motion-DltOw0CR.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-Dva79Keg.js → move-_oPkk5zj.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-C0B6bD2j.js → mtc-parser-EES8ROCS.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-AkZvO-a2.js → omit-B-_39x_M.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-B4r55qGc.js → pickAttrs-l6D0yJHz.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-BLH-z7YE.js → placementArrow-DIeUK31L.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-ksEGZ2Wf.js → responsiveObserve-dG4EzUQo.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-Cth4OD1Z.js → slide-DWFzYGdH.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-9S83ezxw.js → statusUtils-CECdTA5H.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-7yQmQGZ4.js → styleChecker-D6kP-mMK.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-DpGnMmPc.js → useFlexGapSupport-BWfna_rd.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-C4zDZIXH.js → useFs-Bb-j6rzo.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-CoLICdUZ.js → usePersonalDataHub-D5OIsYM4.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-CKyH-35X.js → vnode-DNpwzFTM.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-DRDGit17.js → zoom-vNLpFQBW.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/command-manifest.json +17 -3
  151. package/src/commands/agent.js +7 -0
  152. package/src/commands/background-session.js +143 -3
  153. package/src/commands/browse.js +86 -0
  154. package/src/commands/cloud.js +232 -0
  155. package/src/commands/doctor.js +61 -6
  156. package/src/commands/mcp.js +61 -0
  157. package/src/commands/plugin.js +63 -1
  158. package/src/commands/routine.js +367 -0
  159. package/src/commands/session.js +326 -0
  160. package/src/data/changelog.json +7 -2
  161. package/src/harness/jsonl-session-store.js +82 -2
  162. package/src/harness/session-index.js +295 -0
  163. package/src/harness/session-mirror.js +142 -0
  164. package/src/harness/transcript-integrity.js +182 -0
  165. package/src/index.js +8 -0
  166. package/src/lib/background-agent-supervisor.js +126 -2
  167. package/src/lib/channels/channel-manager.js +126 -0
  168. package/src/lib/channels/telegram-channel.js +112 -0
  169. package/src/lib/channels/webhook-channel.js +140 -0
  170. package/src/lib/chrome-connector.js +386 -0
  171. package/src/lib/cloud/bundle.js +110 -0
  172. package/src/lib/cloud/cloud-client.js +152 -0
  173. package/src/lib/computer-use/authorization.js +173 -0
  174. package/src/lib/computer-use/computer-use-server.js +276 -0
  175. package/src/lib/computer-use/control-backend.js +150 -0
  176. package/src/lib/doctor-checkup.js +652 -0
  177. package/src/lib/jsonl-session-store.js +4 -0
  178. package/src/lib/lsp/lsp-server-registry.js +48 -21
  179. package/src/lib/plugin-runtime/governance.js +174 -0
  180. package/src/lib/pr-link-ledger.js +221 -0
  181. package/src/lib/routine-store.js +352 -0
  182. package/src/lib/session-store-guard.js +40 -0
  183. package/src/lib/session-usage.js +318 -2
  184. package/src/lib/sub-agent-context.js +23 -0
  185. package/src/repl/agent-repl.js +361 -3
  186. package/src/repl/bg-dashboard.js +355 -0
  187. package/src/repl/fast-mode.js +103 -0
  188. package/src/repl/fullscreen-tui.js +197 -0
  189. package/src/repl/slash-command-registry.js +11 -3
  190. package/src/repl/voice-dictation.js +132 -0
  191. package/src/runtime/agent-core.js +242 -1
  192. package/src/runtime/coding-agent-contract-shared.cjs +88 -0
  193. package/src/runtime/coding-agent-policy.cjs +17 -0
  194. package/src/runtime/headless-runner.js +71 -7
  195. package/src/runtime/headless-stream.js +14 -8
  196. package/src/runtime/policies/agent-policy.js +3 -0
  197. package/src/assets/web-panel/assets/Artifacts-DQvJoQ6E.js +0 -1
  198. package/src/assets/web-panel/assets/OrderTableRenderer-h1A0XB2a.js +0 -1
  199. package/src/assets/web-panel/assets/devWarning-nDB--2xr.js +0 -1
  200. package/src/assets/web-panel/assets/index-B1_pRTtB.js +0 -1
  201. package/src/assets/web-panel/assets/index-TMM9FuW-.js +0 -1
@@ -8,6 +8,7 @@ import {
8
8
  readdirSync,
9
9
  renameSync,
10
10
  rmSync,
11
+ statSync,
11
12
  writeFileSync,
12
13
  } from "node:fs";
13
14
  import { randomBytes } from "node:crypto";
@@ -61,12 +62,57 @@ export function logPath(id) {
61
62
  return join(backgroundAgentsDir(), `${safeId(id)}.log`);
62
63
  }
63
64
 
65
+ const TERMINAL_STATUSES = new Set(["completed", "failed", "stopped", "lost"]);
66
+
64
67
  export function writeBackgroundAgentState(state) {
65
68
  const target = statePath(state.id);
69
+ // Field-aware merge against the freshest on-disk state. The state file has
70
+ // multiple concurrent read-modify-write writers (launcher, worker heartbeat/
71
+ // turn/finalize, rename/pin/stop from other processes) with last-writer-wins
72
+ // semantics — a writer holding a stale snapshot used to clobber a terminal
73
+ // status back to "running" (phantom session, real exit code lost) or roll a
74
+ // fresh rename back to the old title. Two invariants restore convergence:
75
+ // 1. a terminal status wins over a racing "running" snapshot — there is no
76
+ // legitimate same-id terminal→running transition (resume mints a NEW
77
+ // id, and the worker's own writeHeartbeat already refuses to resurrect);
78
+ // 2. the newest rename/pin (by renamedAt/pinnedAt) wins regardless of
79
+ // which writer's snapshot carries it.
80
+ // The read happens immediately before the atomic rename, shrinking the
81
+ // clobber window from "any caller's RMW span" to microseconds; rename/pin
82
+ // additionally verify-and-retry on top of this.
83
+ let next = state;
84
+ const current = readBackgroundAgentState(state.id);
85
+ if (current) {
86
+ if (TERMINAL_STATUSES.has(current.status) && next.status === "running") {
87
+ next = {
88
+ ...next,
89
+ status: current.status,
90
+ endedAt: current.endedAt ?? next.endedAt ?? null,
91
+ exitCode: current.exitCode ?? next.exitCode ?? null,
92
+ ...(current.signal !== undefined ? { signal: current.signal } : {}),
93
+ ...(current.error !== undefined ? { error: current.error } : {}),
94
+ ...(current.lostReason !== undefined
95
+ ? { lostReason: current.lostReason }
96
+ : {}),
97
+ ...(current.stoppedByUser !== undefined
98
+ ? { stoppedByUser: current.stoppedByUser }
99
+ : {}),
100
+ // terminal sessions have no live phase or transport endpoint
101
+ phase: null,
102
+ transport: null,
103
+ };
104
+ }
105
+ if (Number(current.renamedAt || 0) > Number(next.renamedAt || 0)) {
106
+ next = { ...next, title: current.title, renamedAt: current.renamedAt };
107
+ }
108
+ if (Number(current.pinnedAt || 0) > Number(next.pinnedAt || 0)) {
109
+ next = { ...next, pinned: current.pinned, pinnedAt: current.pinnedAt };
110
+ }
111
+ }
66
112
  const tmp = `${target}.${process.pid}.tmp`;
67
- writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 0o600 });
113
+ writeFileSync(tmp, JSON.stringify(next, null, 2), { mode: 0o600 });
68
114
  renameSync(tmp, target);
69
- return state;
115
+ return next;
70
116
  }
71
117
 
72
118
  export function readBackgroundAgentState(id) {
@@ -177,6 +223,34 @@ export function renameBackgroundAgent(id, title, options = {}) {
177
223
  return next;
178
224
  }
179
225
 
226
+ /**
227
+ * Pin/unpin a session for the dashboard (`cc daemon view`) — pinned sessions
228
+ * sort first inside their group. Same read-modify-write + verify-retry dance
229
+ * as rename: the worker's periodic state merges can clobber a write that
230
+ * lands inside its read→write window.
231
+ */
232
+ export function setBackgroundAgentPinned(id, pinned, options = {}) {
233
+ const state = effectiveBackgroundAgentState(readBackgroundAgentState(id), {
234
+ now: options.now,
235
+ heartbeatStaleMs: options.heartbeatStaleMs,
236
+ });
237
+ if (!state) throw new Error(`Background agent not found: ${id}`);
238
+ const value = pinned === true;
239
+ const pinnedAt = typeof options.now === "number" ? options.now : Date.now();
240
+ let next = { ...state, pinned: value, pinnedAt };
241
+ for (let attempt = 0; attempt < 4; attempt++) {
242
+ writeBackgroundAgentState(next);
243
+ sleepSyncMs(15);
244
+ const current = readBackgroundAgentState(id) || next;
245
+ if (current.pinned === value) {
246
+ return { ...current, pinned: value, pinnedAt };
247
+ }
248
+ next = { ...current, pinned: value, pinnedAt };
249
+ }
250
+ writeBackgroundAgentState(next);
251
+ return next;
252
+ }
253
+
180
254
  /**
181
255
  * Build the follow-up argv template for interactive attach turns: the launch
182
256
  * argv minus every token that carried the FIRST turn's prompt (positional
@@ -258,6 +332,33 @@ export function resumeBackgroundAgent(id, prompt, options = {}) {
258
332
  });
259
333
  }
260
334
 
335
+ /**
336
+ * Fail fast on an unusable cwd. Without this, spawn() surfaces a bad cwd as
337
+ * an ASYNC 'error' event on the detached child — which nothing listened to
338
+ * (uncaught exception with no context) — and the pre-written state/job files
339
+ * stayed behind as a phantom "running" session. Deleted, file-replaced, and
340
+ * unmounted paths all land here with one clear message.
341
+ */
342
+ function assertUsableCwd(cwd) {
343
+ let reason = null;
344
+ if (!cwd) {
345
+ reason = "no working directory given";
346
+ } else if (!existsSync(cwd)) {
347
+ reason = "directory does not exist (deleted or unmounted?)";
348
+ } else {
349
+ try {
350
+ if (!statSync(cwd).isDirectory()) reason = "path is not a directory";
351
+ } catch (err) {
352
+ reason = `directory is not accessible (${err.code || err.message})`;
353
+ }
354
+ }
355
+ if (reason) {
356
+ throw new Error(
357
+ `Cannot launch background agent: cwd "${cwd ?? ""}" — ${reason}`,
358
+ );
359
+ }
360
+ }
361
+
261
362
  export function launchBackgroundAgent({
262
363
  argv,
263
364
  cwd,
@@ -266,6 +367,7 @@ export function launchBackgroundAgent({
266
367
  cliEntry,
267
368
  followUpArgv,
268
369
  }) {
370
+ assertUsableCwd(cwd);
269
371
  const id = createBackgroundAgentId();
270
372
  const dir = backgroundAgentsDir();
271
373
  const jobFile = join(dir, `${id}.job.${process.pid}.json`);
@@ -317,6 +419,28 @@ export function launchBackgroundAgent({
317
419
  rmSync(statePath(id), { force: true });
318
420
  throw error;
319
421
  }
422
+ // Async spawn failures (EPERM, cwd raced away between the check and the
423
+ // spawn, …) arrive as an 'error' event on the detached child. Reap them
424
+ // into the state file instead of leaving a phantom "running" session and
425
+ // an uncaught exception.
426
+ if (typeof child.on === "function") {
427
+ child.on("error", (error) => {
428
+ try {
429
+ rmSync(jobFile, { force: true });
430
+ const current = readBackgroundAgentState(id);
431
+ if (current && current.status === "running") {
432
+ writeBackgroundAgentState({
433
+ ...current,
434
+ status: "failed",
435
+ endedAt: Date.now(),
436
+ lostReason: `spawn-error: ${error.code || error.message}`,
437
+ });
438
+ }
439
+ } catch {
440
+ /* best-effort */
441
+ }
442
+ });
443
+ }
320
444
  child.unref();
321
445
  const current = readBackgroundAgentState(id) || state;
322
446
  const withPid = {
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Inbound channels — external events entering a running agent session
3
+ * (gap-2026-07-11 P0#5; Claude-Code "channels" parity, self-hosted).
4
+ *
5
+ * `cc agent --channels webhook,telegram` starts channel listeners whose
6
+ * events become user turns in the interactive session (queued behind any
7
+ * in-flight turn by the REPL's pending-line queue). Every channel enforces a
8
+ * trust boundary: webhook = bearer token + loopback-only by default;
9
+ * telegram = explicit chat-id allowlist (deny-all when unset).
10
+ *
11
+ * Outbound replies reuse the existing notifier stack (`notify` agent tool /
12
+ * NotificationManager) — the inbound event's metadata tells the model which
13
+ * channel and sender to address.
14
+ */
15
+
16
+ import { logger } from "../logger.js";
17
+
18
+ export const CHANNEL_KINDS = ["webhook", "telegram"];
19
+
20
+ /**
21
+ * Parse `--channels` input: "webhook,telegram", "webhook:18901", or an array.
22
+ * @returns {Array<{kind:string, arg:string|null}>}
23
+ */
24
+ export function parseChannelSpecs(input) {
25
+ const items = Array.isArray(input)
26
+ ? input
27
+ : String(input || "")
28
+ .split(",")
29
+ .map((s) => s.trim())
30
+ .filter(Boolean);
31
+ return items.map((item) => {
32
+ const idx = item.indexOf(":");
33
+ const kind = (idx === -1 ? item : item.slice(0, idx)).toLowerCase();
34
+ const arg = idx === -1 ? null : item.slice(idx + 1);
35
+ if (!CHANNEL_KINDS.includes(kind)) {
36
+ throw new Error(
37
+ `Unknown channel "${kind}" — supported: ${CHANNEL_KINDS.join(", ")}`,
38
+ );
39
+ }
40
+ return { kind, arg };
41
+ });
42
+ }
43
+
44
+ /**
45
+ * Render a channel event as the user-turn text. The channel/sender prefix
46
+ * keeps provenance visible to the model AND guarantees the line can never be
47
+ * mistaken for a REPL slash command or `!` shell escape, no matter what the
48
+ * external sender wrote.
49
+ */
50
+ export function formatChannelEvent(event) {
51
+ const channel = event?.channel || "channel";
52
+ const sender = event?.sender ? ` from ${event.sender}` : "";
53
+ const text = String(event?.text ?? "").trim();
54
+ return `[channel:${channel}${sender}] ${text}`;
55
+ }
56
+
57
+ /**
58
+ * Start every requested channel. Returns a handle with the started channel
59
+ * descriptions and a single stop().
60
+ *
61
+ * @param {string|string[]} specsInput --channels value
62
+ * @param {object} opts
63
+ * @param {(event) => void} opts.onEvent called for each inbound event
64
+ * @param {object} [opts.config] loadConfig().channels subtree
65
+ * @param {object} [opts.deps] channel starters (tests inject)
66
+ */
67
+ export async function startChannels(specsInput, opts = {}) {
68
+ const specs = parseChannelSpecs(specsInput);
69
+ if (specs.length === 0) {
70
+ throw new Error("--channels given but no channel names parsed");
71
+ }
72
+ const config = opts.config || {};
73
+ const log = opts.log || ((msg) => logger.info(msg));
74
+ const deps = opts.deps || {};
75
+
76
+ const started = [];
77
+ const stopAll = () => {
78
+ for (const c of started) {
79
+ try {
80
+ c.stop?.();
81
+ } catch {
82
+ /* best-effort */
83
+ }
84
+ }
85
+ };
86
+
87
+ try {
88
+ for (const spec of specs) {
89
+ if (spec.kind === "webhook") {
90
+ const { startWebhookChannel } =
91
+ deps.webhook || (await import("./webhook-channel.js"));
92
+ const cfg = config.webhook || {};
93
+ const handle = await (
94
+ deps.webhook?.startWebhookChannel || startWebhookChannel
95
+ )({
96
+ ...cfg,
97
+ port: spec.arg ? Number(spec.arg) : cfg.port,
98
+ onEvent: opts.onEvent,
99
+ log,
100
+ });
101
+ started.push({ kind: "webhook", describe: handle.describe, ...handle });
102
+ } else if (spec.kind === "telegram") {
103
+ const { startTelegramChannel } =
104
+ deps.telegram || (await import("./telegram-channel.js"));
105
+ const cfg = config.telegram || {};
106
+ const handle = await (
107
+ deps.telegram?.startTelegramChannel || startTelegramChannel
108
+ )({
109
+ ...cfg,
110
+ onEvent: opts.onEvent,
111
+ log,
112
+ });
113
+ started.push({
114
+ kind: "telegram",
115
+ describe: handle.describe,
116
+ ...handle,
117
+ });
118
+ }
119
+ }
120
+ } catch (err) {
121
+ stopAll(); // never leave half the channels listening after a failure
122
+ throw err;
123
+ }
124
+
125
+ return { channels: started, stop: stopAll };
126
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Telegram inbound channel — long-polls the Bot API (`getUpdates`) so no
3
+ * public endpoint is needed, and turns allowed chats' messages into session
4
+ * events.
5
+ *
6
+ * Trust boundary: `allowFrom` (array of chat ids) is REQUIRED — an inbound
7
+ * channel with no allowlist would let anyone who finds the bot drive the
8
+ * agent, so an empty/missing allowlist denies every update (fail closed).
9
+ *
10
+ * Replies: `handle.reply(event, text)` posts a sendMessage back to the
11
+ * event's chat. fetch is injectable for tests.
12
+ */
13
+
14
+ const POLL_TIMEOUT_S = 25;
15
+ const ERROR_BACKOFF_MS = 5000;
16
+
17
+ export async function startTelegramChannel(options = {}) {
18
+ const {
19
+ botToken = process.env.TELEGRAM_BOT_TOKEN,
20
+ allowFrom,
21
+ onEvent,
22
+ log = () => {},
23
+ fetchImpl = globalThis.fetch,
24
+ pollTimeoutS = POLL_TIMEOUT_S,
25
+ // Real macrotask pause between polls. The server-side long-poll dominates
26
+ // in production; this exists so a fast-resolving fetch (tests, HTTP-level
27
+ // failures) can never turn the loop into a microtask chain that starves
28
+ // the event loop (timers, stop()) entirely.
29
+ pollPauseMs = 50,
30
+ } = options;
31
+
32
+ if (!botToken) {
33
+ throw new Error(
34
+ "telegram channel needs a bot token (channels.telegram.botToken or TELEGRAM_BOT_TOKEN)",
35
+ );
36
+ }
37
+ const allowed = new Set((allowFrom || []).map((id) => String(id)));
38
+ if (allowed.size === 0) {
39
+ throw new Error(
40
+ "telegram channel needs channels.telegram.allowFrom (chat-id allowlist) — refusing to listen to everyone",
41
+ );
42
+ }
43
+
44
+ const api = (method) => `https://api.telegram.org/bot${botToken}/${method}`;
45
+ let stopped = false;
46
+ let offset = 0;
47
+
48
+ async function poll() {
49
+ while (!stopped) {
50
+ try {
51
+ const res = await fetchImpl(api("getUpdates"), {
52
+ method: "POST",
53
+ headers: { "Content-Type": "application/json" },
54
+ body: JSON.stringify({ timeout: pollTimeoutS, offset }),
55
+ });
56
+ const data = await res.json();
57
+ if (!data?.ok || !Array.isArray(data.result)) {
58
+ throw new Error(data?.description || `getUpdates HTTP ${res.status}`);
59
+ }
60
+ for (const update of data.result) {
61
+ offset = Math.max(offset, Number(update.update_id) + 1);
62
+ const msg = update.message || update.edited_message;
63
+ const chatId = msg?.chat?.id;
64
+ const text = typeof msg?.text === "string" ? msg.text.trim() : "";
65
+ if (!chatId || !text) continue;
66
+ if (!allowed.has(String(chatId))) continue; // fail closed
67
+ try {
68
+ onEvent?.({
69
+ channel: "telegram",
70
+ sender: String(chatId),
71
+ text,
72
+ meta: { updateId: update.update_id },
73
+ });
74
+ } catch {
75
+ /* downstream owns its failures */
76
+ }
77
+ }
78
+ } catch (err) {
79
+ if (stopped) return;
80
+ log(`channel telegram poll error: ${err.message} — backing off`);
81
+ await new Promise((r) => setTimeout(r, ERROR_BACKOFF_MS));
82
+ }
83
+ if (!stopped) {
84
+ await new Promise((r) => setTimeout(r, pollPauseMs));
85
+ }
86
+ }
87
+ }
88
+ const pollPromise = poll();
89
+
90
+ log(`channel telegram polling (allowlist: ${[...allowed].join(", ")})`);
91
+
92
+ return {
93
+ kind: "telegram",
94
+ describe: `telegram (${allowed.size} allowed chat${allowed.size === 1 ? "" : "s"})`,
95
+ async reply(event, text) {
96
+ const res = await fetchImpl(api("sendMessage"), {
97
+ method: "POST",
98
+ headers: { "Content-Type": "application/json" },
99
+ body: JSON.stringify({ chat_id: event.sender, text }),
100
+ });
101
+ const data = await res.json();
102
+ if (!data?.ok) {
103
+ throw new Error(data?.description || `sendMessage HTTP ${res.status}`);
104
+ }
105
+ return true;
106
+ },
107
+ stop: () => {
108
+ stopped = true;
109
+ return pollPromise.catch(() => {});
110
+ },
111
+ };
112
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Webhook inbound channel — a tiny loopback HTTP listener that turns
3
+ * authenticated POSTs into session events.
4
+ *
5
+ * POST /v1/event
6
+ * Authorization: Bearer <token>
7
+ * { "text": "deploy finished", "sender": "ci", "meta": {...} }
8
+ *
9
+ * Trust boundary:
10
+ * - bearer token REQUIRED on every request; without a configured token a
11
+ * random one is generated and printed once at startup (pairing secret).
12
+ * - binds 127.0.0.1 unless `allowExternal: true` is explicitly configured
13
+ * (same LAN/loopback posture as remote-control direct pairing).
14
+ * - optional `allowlist` of sender ids — configured ⇒ only those senders.
15
+ * - body capped at 64 KB; JSON only.
16
+ */
17
+
18
+ import { createServer } from "node:http";
19
+ import { randomBytes, timingSafeEqual } from "node:crypto";
20
+
21
+ const MAX_BODY_BYTES = 64 * 1024;
22
+
23
+ function tokenMatches(expected, got) {
24
+ const a = Buffer.from(String(expected));
25
+ const b = Buffer.from(String(got || ""));
26
+ return a.length === b.length && timingSafeEqual(a, b);
27
+ }
28
+
29
+ export async function startWebhookChannel(options = {}) {
30
+ const {
31
+ port = 18901,
32
+ host = "127.0.0.1",
33
+ allowExternal = false,
34
+ allowlist,
35
+ onEvent,
36
+ log = () => {},
37
+ } = options;
38
+
39
+ const isLoopback =
40
+ host === "127.0.0.1" || host === "::1" || host === "localhost";
41
+ if (!isLoopback && allowExternal !== true) {
42
+ throw new Error(
43
+ `webhook channel refuses to bind non-loopback host "${host}" without channels.webhook.allowExternal=true`,
44
+ );
45
+ }
46
+
47
+ const token = options.token || randomBytes(24).toString("base64url");
48
+ const generated = !options.token;
49
+
50
+ const server = createServer((req, res) => {
51
+ const respond = (status, body) => {
52
+ res.writeHead(status, { "Content-Type": "application/json" });
53
+ res.end(JSON.stringify(body));
54
+ };
55
+
56
+ if (req.method !== "POST" || req.url !== "/v1/event") {
57
+ return respond(404, { error: "not found" });
58
+ }
59
+ const auth = String(req.headers.authorization || "");
60
+ const got = auth.startsWith("Bearer ") ? auth.slice(7) : "";
61
+ if (!tokenMatches(token, got)) {
62
+ return respond(401, { error: "unauthorized" });
63
+ }
64
+
65
+ let size = 0;
66
+ const chunks = [];
67
+ let aborted = false;
68
+ req.on("data", (chunk) => {
69
+ size += chunk.length;
70
+ if (size > MAX_BODY_BYTES) {
71
+ aborted = true;
72
+ respond(413, { error: "body too large" });
73
+ req.destroy();
74
+ return;
75
+ }
76
+ chunks.push(chunk);
77
+ });
78
+ req.on("end", () => {
79
+ if (aborted) return;
80
+ let payload;
81
+ try {
82
+ payload = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
83
+ } catch {
84
+ return respond(400, { error: "invalid JSON" });
85
+ }
86
+ const text = typeof payload?.text === "string" ? payload.text.trim() : "";
87
+ if (!text) return respond(400, { error: "missing text" });
88
+ const sender =
89
+ typeof payload.sender === "string" ? payload.sender : "webhook";
90
+ if (Array.isArray(allowlist) && allowlist.length > 0) {
91
+ if (!allowlist.includes(sender)) {
92
+ return respond(403, { error: "sender not in allowlist" });
93
+ }
94
+ }
95
+ try {
96
+ onEvent?.({
97
+ channel: "webhook",
98
+ sender,
99
+ text,
100
+ meta:
101
+ payload.meta && typeof payload.meta === "object"
102
+ ? payload.meta
103
+ : undefined,
104
+ });
105
+ } catch {
106
+ /* the session queue owns downstream failures */
107
+ }
108
+ respond(202, { ok: true });
109
+ });
110
+ });
111
+
112
+ await new Promise((resolve, reject) => {
113
+ server.once("error", reject);
114
+ server.listen(port, host, resolve);
115
+ });
116
+ server.unref?.();
117
+
118
+ const actualPort = server.address()?.port ?? port;
119
+ const url = `http://${host}:${actualPort}/v1/event`;
120
+ log(
121
+ `channel webhook listening on ${url}` +
122
+ (generated ? ` (generated token: ${token})` : ""),
123
+ );
124
+
125
+ return {
126
+ kind: "webhook",
127
+ url,
128
+ port: actualPort,
129
+ token,
130
+ describe: `webhook ${url}`,
131
+ stop: () =>
132
+ new Promise((resolve) => {
133
+ try {
134
+ server.close(() => resolve());
135
+ } catch {
136
+ resolve();
137
+ }
138
+ }),
139
+ };
140
+ }