chainlesschain 0.162.160 → 0.162.162

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 (199) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-CkRHjW8e.js → AIOps-ClI2ALgv.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-BP3MeNi2.js → ActionButton-BDjRcJaK.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-CjbjQsOr.js → Analytics-DikSRsaT.js} +2 -2
  5. package/src/assets/web-panel/assets/{AppLayout-i0adH1M8.js → AppLayout-9mLhMFmz.js} +5 -5
  6. package/src/assets/web-panel/assets/{Artifacts-CCCG2GOU.js → Artifacts-BSr88BrH.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-DrsgvwHC.js → Audit-CdvyimOF.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-DkXNkbZM.js → BackgroundAgents-D3_hCedh.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-DarTWCex.js → Backup-Cyvv9RHf.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-DaDzhcto.js → BaseInput-BZM14kt8.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-C4EzGw6V.js → Chat-DrQSokVz.js} +5 -5
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-lkavIWMS.js → ChatBubbleRenderer-HwJR-C6B.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-gddNgaQi.js → Checkbox-bFLYeWMV.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-BKldFWf9.js → Codegen-7mN7xJXT.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-ChR-brDs.js → Col-DAB5ZWm7.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-DlHtZX86.js → Community-B_1TymaK.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-BAhfcBpC.js → Compact-Bq1uSWLJ.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-gIrKkm6m.js → Compliance-CyiE1jEe.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-BYnzLBO3.js → Cowork-Cpk0D7vm.js} +2 -2
  20. package/src/assets/web-panel/assets/{Cron-ByUDIljI.js → Cron-d_Lv3rdB.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-DOdNbHFp.js → Crosschain-DMqhHjMR.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-BJHaNWD2.js → DID-Br-oPuBw.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-Drm_tClz.js → Dashboard-CfLW_RaP.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-CA1WLzGN.js → Dropdown-DZnTOExV.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-Di1SKjNO.js → EmailListRenderer-DHIUHDv1.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-44Ve7ZFe.js → FamilyGuardDashboard-Bw3t3myu.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-CubPwv_N.js → Federation-D9cpQPAY.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-CsUFe5bU.js → FormItemContext-Jdczr9Mr.js} +1 -1
  29. package/src/assets/web-panel/assets/GenericCardRenderer-BT8ZMpC5.js +1 -0
  30. package/src/assets/web-panel/assets/{Git-D-Qx9dhO.js → Git-DjARip2b.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-_YMYaU4n.js → Governance-BgWbSRcj.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-J7Yzh-K_.js → Inference-CfouJciM.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-D37iI_Mo.js → KnowledgeGraph-50l7mrAr.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-oMTSYfGY.js → Logs-rq50H84B.js} +2 -2
  35. package/src/assets/web-panel/assets/{MarkdownRenderer-BqaPmC0W.js → MarkdownRenderer-DUPIEdQH.js} +1 -1
  36. package/src/assets/web-panel/assets/{Marketplace-DKUqYDaF.js → Marketplace-BYgPDmHU.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-BEv3Gx61.js → McpTools-5DVgcFdx.js} +3 -3
  38. package/src/assets/web-panel/assets/{Memory-CtA8XD2y.js → Memory-B70LPk7f.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-ug7c1mMj.js → MobileBridge-De8ssA5Y.js} +1 -1
  40. package/src/assets/web-panel/assets/MobileProjects-D10VISps.js +1 -0
  41. package/src/assets/web-panel/assets/{Mtc-WJaMs4i2.js → Mtc-RJsOTsIA.js} +2 -2
  42. package/src/assets/web-panel/assets/{MtcAudit-DXnK_-ru.js → MtcAudit-Crh-6BEK.js} +5 -5
  43. package/src/assets/web-panel/assets/{Multisig-C4Ev7726.js → Multisig-wuoR5l3x.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-BMFb9_zk.js → NLProgramming-U2Mji4fZ.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-Bl93nvBX.js → Notes-CdTZAyDH.js} +3 -3
  46. package/src/assets/web-panel/assets/{NotificationSettings-BN2W5cl6.js → NotificationSettings-C-FucWE7.js} +1 -1
  47. package/src/assets/web-panel/assets/{OrderTableRenderer-Dxc9oN4o.js → OrderTableRenderer-DCYFTeaI.js} +1 -1
  48. package/src/assets/web-panel/assets/{Organization-CAs48637.js → Organization-an3OF85L.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-Bhq27Zoc.js → Overflow-C-1vmYFe.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-D61-2542.js → P2P-Cwh47xKg.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-C1j4iUxz.js → PdhVaultBrowser-Dm38GT0k.js} +3 -3
  52. package/src/assets/web-panel/assets/{Permissions-rJ2-fScM.js → Permissions-DS2Y8awD.js} +4 -4
  53. package/src/assets/web-panel/assets/{PersonalDataHub-DFIlTok6.js → PersonalDataHub-BQNziA0I.js} +3 -3
  54. package/src/assets/web-panel/assets/{Pipeline-BjIgEWoZ.js → Pipeline-DF85L1ue.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-BQoH5wcX.js → Privacy-BuLornF-.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-CYR_-_Ub.js → ProjectInit-C3Y31IwX.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-nkatCq-9.js → ProjectSettings-BCPiI8aD.js} +2 -2
  58. package/src/assets/web-panel/assets/Projects-BZ6s4k6g.js +1 -0
  59. package/src/assets/web-panel/assets/{Providers-BnKM9fsF.js → Providers-D3Wp022X.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-Cfe82dbz.js → QrScannerModal-CUJfQkH4.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-r1RbPufV.js → QuickAsk-UAc0EO8l.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-CjCbM2WE.js → Recommend-BaxKupcs.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-BuMGPH6V.js → RemoteSession-C75NNnXZ.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-Bmj3NY1F.js → Reputation-BVXynZAk.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-BIsNcE_1.js → Row-C5MRZ0bi.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-BObmojD2.js → RssFeed-DAKkEYka.js} +3 -3
  67. package/src/assets/web-panel/assets/{Search-BRmbkQNS.js → Search-DvWv9YOA.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-_rF3DRpY.js → Security-B5peafW1.js} +4 -4
  69. package/src/assets/web-panel/assets/{Services-D0If9ZV4.js → Services-UQmvPEaG.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-o7zIksUg.js → Skeleton-C3bhWpuF.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-oUR7zi9u.js → Skills-DfkKf9_H.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-fqKyGi03.js → Sla-DqMSjlBM.js} +1 -1
  73. package/src/assets/web-panel/assets/{SpeechSettings-B1wZ_gxx.js → SpeechSettings-C-WbxWUX.js} +1 -1
  74. package/src/assets/web-panel/assets/{SyncSettings-B3qJWYXq.js → SyncSettings-CYuHZ4N2.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-BH5_dUfr.js → Tasks-CyeAyB9d.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-CCIVpjRP.js → Templates-BTCeQnpq.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-2GZtTGnB.js → Tenant-BifQuA9Y.js} +1 -1
  78. package/src/assets/web-panel/assets/{Terminal-BlMkfs_u.js → Terminal-Bx_9Bocf.js} +2 -2
  79. package/src/assets/web-panel/assets/{TimelineRenderer-U6hi5OMc.js → TimelineRenderer-BX0RFCOm.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-C-_BerGJ.js → Tokens-CGHrCmUS.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-DaKzBgN3.js → Trigger-DlqaY4SO.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-CLvcsco3.js → Trust-DfOmj-ay.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-BFn_Y1zZ.js → UkeySign-B72jH5fE.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-B0V18dLT.js → VideoEditing-BT-63fqv.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-BbmP7Imu.js → Wallet-Dsul3sHr.js} +3 -3
  86. package/src/assets/web-panel/assets/{WebAuthn-Dewj_0kt.js → WebAuthn-Izkwwdgy.js} +4 -4
  87. package/src/assets/web-panel/assets/{WorkflowEditor-DOxz3Lwa.js → WorkflowEditor-D3FHSiH1.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-B1OIpZnh.js → chat-CmVz5z3U.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-CSGNVdri.js → colors-CuLgyJBp.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-8S0vcg0u.js → compact-item-Ml1YkXpw.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-vZyc27TN.js → createContext-BvjJ48bs.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-DhiGrbsh.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-DWw3-UhA.js → hasIn-DVC0157C.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-DLAvUtOH.js → index-4SBta3d7.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-BZnauCqN.js → index-4wcgvPoF.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-t-q3rkVm.js → index-B0S8NzO6.js} +1 -1
  97. package/src/assets/web-panel/assets/index-B7a1TrU6.js +1 -0
  98. package/src/assets/web-panel/assets/{index-BwKrTVV0.js → index-BUt8kTIB.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-BQssf0EQ.js → index-B_EvG_GW.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-xx5M5Rbj.js → index-BbPtzB8M.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-DKwl1AkQ.js → index-BbepULSc.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-B4pP2ycr.js → index-BfuKP4qe.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-D1i_8oop.js → index-BiC6dQZG.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-Dc40pX3F.js → index-BjHBpRYZ.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-DeCS12Mh.js → index-BpFJHiTv.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-BjC-TrOb.js → index-BydjdZnl.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-DcfLXAW7.js → index-BzG-x9p4.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-CfEvacz7.js → index-Bzr0rB5T.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-BoIPHP4R.js → index-C229ABfy.js} +1 -1
  110. package/src/assets/web-panel/assets/index-C2B1KRja.js +1 -0
  111. package/src/assets/web-panel/assets/{index-BdyokEau.js → index-CFYVQMvu.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-CLPr2vC_.js → index-CXkJH-lK.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-DtHWHJnj.js → index-C_H_S-DY.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-Q6oyLnKC.js → index-C_XuRgGn.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-BeBv896m.js → index-C_vRgTl9.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-DLgkAhEE.js → index-Ck_52dz_.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-tr1x96W9.js → index-CmFNvNLZ.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-CB2MOS_8.js → index-CudmwNBV.js} +3 -3
  119. package/src/assets/web-panel/assets/{index-CFQWZvak.js → index-Cw0Rjy87.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-BuB5P0TR.js → index-D3lD5qap.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-D8qXVfoV.js → index-DC_OI9H2.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-C6r27A-o.js → index-DDGaOWBV.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-CbZqw7pg.js → index-DJyN28Et.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-CJgKP2cm.js → index-DYXHFr4r.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-CI3slBKI.js → index-Dt5KMjwJ.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-CwXBblm-.js → index-DuGszaWY.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-BKEmQQvR.js → index-DukDVXCv.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-Dg0Izipr.js → index-KMqhcF7l.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BkWd6YL7.js → index-KjgKdZe-.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-DPgY5GLy.js → index-QXzlT9KD.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-BDHyovnK.js → index-vG4XSJ1d.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-dxocDVD_.js → index-wvb_FtaV.js} +1 -1
  133. package/src/assets/web-panel/assets/{initDefaultProps-Di5YFId0.js → initDefaultProps-BDgd2YZ8.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-BVSN0aeL.js → motion-ErtrXdYe.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-DHNiaP2l.js → move-_GPPNCgs.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-xJPf3gQD.js → mtc-parser-DRLL-8V2.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-C7qzUdkN.js → omit-p86sl023.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-pSpjg3Le.js → pickAttrs-BTu7cMLH.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-cs_jL_UC.js → placementArrow-BEKz6Isr.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-DkS_Zs4r.js → responsiveObserve-Ck42c9MP.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-B5O1Kpxo.js → slide-1CEvuPsB.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-BX02GnZp.js → statusUtils-BDfdx1GJ.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-EmBYRftd.js → styleChecker-B4K46m5w.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-ZGzN1cU8.js → useFlexGapSupport-DoIVpJNq.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-CfP7M1m7.js → useFs-DdAWi-ln.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-CbIPHgNh.js → usePersonalDataHub-iUu6k8dx.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-7xh0If_O.js → vnode-DZz4R6Tw.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-DmitvTR2.js → zoom-DXlf9sA3.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/commands/agenda.js +15 -1
  151. package/src/commands/agent.js +7 -0
  152. package/src/commands/background-session.js +16 -2
  153. package/src/commands/review.js +6 -1
  154. package/src/data/changelog.json +12 -2
  155. package/src/gateways/ws/background-agent-protocol.js +88 -14
  156. package/src/lib/agent-authority.js +251 -0
  157. package/src/lib/agents.js +19 -0
  158. package/src/lib/background-agent-phase.js +100 -0
  159. package/src/lib/background-agent-supervisor.js +322 -15
  160. package/src/lib/backpressure-policy.js +105 -0
  161. package/src/lib/capability-negotiation.js +212 -0
  162. package/src/lib/credential-guard.js +11 -0
  163. package/src/lib/credential-proxy.js +182 -0
  164. package/src/lib/docs-drift.js +166 -0
  165. package/src/lib/doctor-checkup.js +76 -0
  166. package/src/lib/event-seq-replay.js +167 -0
  167. package/src/lib/goal-condition-engine.js +369 -0
  168. package/src/lib/headless-manifest.js +13 -0
  169. package/src/lib/hook-event-bus.cjs +0 -0
  170. package/src/lib/ide-context.js +53 -14
  171. package/src/lib/json-schema-output.js +30 -1
  172. package/src/lib/json-schema-validate.js +691 -0
  173. package/src/lib/lsp/__tests__/diagnostics-scheduler.test.js +93 -0
  174. package/src/lib/lsp/diagnostics-scheduler.js +127 -0
  175. package/src/lib/operation-fingerprint.js +95 -0
  176. package/src/lib/plugin-runtime/capabilities.js +371 -0
  177. package/src/lib/plugin-runtime/manifest.js +30 -0
  178. package/src/lib/project-instructions.js +98 -1
  179. package/src/lib/remote-approval-bridge.js +16 -0
  180. package/src/lib/remote-path-mapping.js +229 -0
  181. package/src/lib/review-pipeline.js +227 -0
  182. package/src/lib/runtime-checkup.js +318 -0
  183. package/src/lib/schedule-planner.js +136 -0
  184. package/src/lib/session-lifecycle.js +305 -0
  185. package/src/lib/settings-hook-events.cjs +39 -13
  186. package/src/lib/subagent-contract.js +383 -0
  187. package/src/lib/telemetry-ids.js +190 -0
  188. package/src/lib/turn-binding.js +397 -0
  189. package/src/repl/bg-dashboard.js +13 -12
  190. package/src/runtime/agent-core.js +24 -10
  191. package/src/runtime/headless-stream.js +195 -11
  192. package/src/runtime/system-prompt.js +3 -0
  193. package/src/workers/background-agent-worker.js +37 -2
  194. package/src/assets/web-panel/assets/GenericCardRenderer-D7xRjPjZ.js +0 -1
  195. package/src/assets/web-panel/assets/MobileProjects-BpX5g9It.js +0 -1
  196. package/src/assets/web-panel/assets/Projects-SS9uZKCl.js +0 -1
  197. package/src/assets/web-panel/assets/devWarning-DYEZcY55.js +0 -1
  198. package/src/assets/web-panel/assets/index-DmVFfcNR.js +0 -1
  199. package/src/assets/web-panel/assets/index-x7YyhMUa.js +0 -1
@@ -15,12 +15,169 @@ import { randomBytes } from "node:crypto";
15
15
  import { join } from "node:path";
16
16
  import { fileURLToPath } from "node:url";
17
17
  import { getHomeDir } from "./paths.js";
18
-
19
- export const _deps = { spawn, spawnSync };
18
+ import { deriveSessionState } from "./session-lifecycle.js";
20
19
 
21
20
  export const DEFAULT_HEARTBEAT_INTERVAL_MS = 5000;
22
21
  export const DEFAULT_HEARTBEAT_STALE_MS = 120000;
23
22
 
23
+ /**
24
+ * Pid identity tolerance (Gap 1: OS pid reuse). A pid alone does not identify
25
+ * a process — after the worker dies the OS can hand the same pid to an
26
+ * unrelated process, making `kill(pid, 0)` lie: the session shows "running"
27
+ * until the heartbeat goes stale and, far worse, `stopBackgroundAgent` would
28
+ * `taskkill /T /F` an innocent process tree. The state file records
29
+ * `startedAt` (launcher clock, written just before the spawn), so we compare
30
+ * it against the pid's REAL creation time. Reuse is one-sided: a process
31
+ * that took over the pid can only have been created AFTER the original
32
+ * worker died — i.e. noticeably later than startedAt. Creation at/before
33
+ * startedAt (± tolerance for clock skew and spawn latency) is our process.
34
+ */
35
+ export const PID_IDENTITY_TOLERANCE_MS = 60000;
36
+
37
+ const START_TIME_CACHE_TTL_MS = 10000;
38
+ const _startTimeCache = new Map();
39
+
40
+ /** CIM_DATETIME (`20260711120000.500000+480`) → epoch ms, null when unparseable. */
41
+ export function parseCimDateToMs(raw) {
42
+ const m =
43
+ /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\.(\d{6})([+-])(\d{3})$/.exec(
44
+ String(raw || "").trim(),
45
+ );
46
+ if (!m) return null;
47
+ const localAsUtc = Date.UTC(
48
+ Number(m[1]),
49
+ Number(m[2]) - 1,
50
+ Number(m[3]),
51
+ Number(m[4]),
52
+ Number(m[5]),
53
+ Number(m[6]),
54
+ Math.floor(Number(m[7]) / 1000),
55
+ );
56
+ const offsetMinutes = (m[8] === "-" ? -1 : 1) * Number(m[9]);
57
+ return localAsUtc - offsetMinutes * 60000;
58
+ }
59
+
60
+ /**
61
+ * Read a process's creation time (epoch ms) with ONE synchronous probe, or
62
+ * null when it cannot be determined (missing tools, permissions, platform
63
+ * quirks). Callers must treat null as "unknown" and FAIL OPEN to the legacy
64
+ * kill(pid, 0) answer — a broken probe must never declare a live worker dead
65
+ * (nor green-light killing a process we could not identify as ours; see the
66
+ * per-call-site policy).
67
+ */
68
+ function defaultReadProcessStartTimeMs(pid) {
69
+ const target = Number(pid);
70
+ if (!Number.isInteger(target) || target <= 0) return null;
71
+ if (process.platform === "win32") {
72
+ // wmic first (fast, ~100-300ms); PowerShell CIM as fallback (wmic is
73
+ // removed from recent Windows 11 builds).
74
+ try {
75
+ const r = spawnSync(
76
+ "wmic",
77
+ [
78
+ "process",
79
+ "where",
80
+ `ProcessId=${target}`,
81
+ "get",
82
+ "CreationDate",
83
+ "/value",
84
+ ],
85
+ { windowsHide: true, encoding: "utf8", timeout: 5000 },
86
+ );
87
+ if (!r.error && r.status === 0) {
88
+ const m = /CreationDate=([^\r\n]+)/.exec(r.stdout || "");
89
+ const ms = m ? parseCimDateToMs(m[1]) : null;
90
+ if (ms !== null) return ms;
91
+ }
92
+ } catch {
93
+ /* fall through to PowerShell */
94
+ }
95
+ try {
96
+ const script =
97
+ `$p = Get-CimInstance Win32_Process -Filter 'ProcessId=${target}' -ErrorAction SilentlyContinue; ` +
98
+ "if ($p -and $p.CreationDate) { [DateTimeOffset]::new($p.CreationDate).ToUnixTimeMilliseconds() }";
99
+ const r = spawnSync(
100
+ "powershell",
101
+ ["-NoProfile", "-NonInteractive", "-Command", script],
102
+ { windowsHide: true, encoding: "utf8", timeout: 10000 },
103
+ );
104
+ if (!r.error && r.status === 0) {
105
+ const n = Number(String(r.stdout || "").trim());
106
+ if (Number.isFinite(n) && n > 0) return n;
107
+ }
108
+ } catch {
109
+ /* fail open below */
110
+ }
111
+ return null;
112
+ }
113
+ // POSIX: `ps -o lstart=` gives an absolute start timestamp on Linux + macOS
114
+ // (procps and BSD ps both support it; busybox ps does not → null → open).
115
+ try {
116
+ const r = spawnSync("ps", ["-o", "lstart=", "-p", String(target)], {
117
+ encoding: "utf8",
118
+ timeout: 5000,
119
+ });
120
+ if (!r.error && r.status === 0) {
121
+ const t = Date.parse(String(r.stdout || "").trim());
122
+ if (Number.isFinite(t) && t > 0) return t;
123
+ }
124
+ } catch {
125
+ /* fail open below */
126
+ }
127
+ return null;
128
+ }
129
+
130
+ /**
131
+ * Kill a whole process tree (Gap 2 orphan reclaim). Windows: `taskkill /T`.
132
+ * POSIX: negative-pid group signal (the worker spawns the agent child
133
+ * detached → its own group), falling back to a direct kill.
134
+ */
135
+ function defaultKillProcessTree(pid, signal = "SIGKILL") {
136
+ const target = Number(pid);
137
+ if (!Number.isInteger(target) || target <= 0) return false;
138
+ try {
139
+ if (process.platform === "win32") {
140
+ const r = spawnSync("taskkill", ["/PID", String(target), "/T", "/F"], {
141
+ windowsHide: true,
142
+ encoding: "utf8",
143
+ });
144
+ return !r.error && r.status === 0;
145
+ }
146
+ try {
147
+ process.kill(-target, signal);
148
+ return true;
149
+ } catch {
150
+ process.kill(target, signal);
151
+ return true;
152
+ }
153
+ } catch {
154
+ return false;
155
+ }
156
+ }
157
+
158
+ export const _deps = {
159
+ spawn,
160
+ spawnSync,
161
+ readProcessStartTimeMs: defaultReadProcessStartTimeMs,
162
+ killProcessTree: defaultKillProcessTree,
163
+ };
164
+
165
+ /** Cached probe (default impl only — injected probes run uncached for tests). */
166
+ function processStartTimeMs(pid) {
167
+ if (_deps.readProcessStartTimeMs !== defaultReadProcessStartTimeMs) {
168
+ const raw = _deps.readProcessStartTimeMs(pid);
169
+ const n = Number(raw);
170
+ return Number.isFinite(n) && n > 0 ? n : null;
171
+ }
172
+ const key = Number(pid);
173
+ const at = Date.now();
174
+ const hit = _startTimeCache.get(key);
175
+ if (hit && at - hit.at < START_TIME_CACHE_TTL_MS) return hit.value;
176
+ const value = defaultReadProcessStartTimeMs(key);
177
+ _startTimeCache.set(key, { at, value });
178
+ return value;
179
+ }
180
+
24
181
  function nowMs(options = {}) {
25
182
  return typeof options.now === "number" ? options.now : Date.now();
26
183
  }
@@ -136,12 +293,81 @@ export function isProcessAlive(pid) {
136
293
  }
137
294
  }
138
295
 
296
+ /**
297
+ * Is the live process at `pid` the SAME process the state file recorded at
298
+ * `expectedStartedAtMs` — or a pid-reusing stranger? (Gap 1.)
299
+ *
300
+ * - dead pid → false (not the same process; it is no process at all)
301
+ * - no anchor (legacy state without startedAt) → true (legacy semantics)
302
+ * - probe failed → true (FAIL OPEN: never declare a live worker dead — and
303
+ * never kill — on the strength of a broken probe; callers fall back to the
304
+ * plain kill(pid,0) behavior this system always had)
305
+ * - creation time > startedAt + tolerance → false (a reuser can only be
306
+ * born after the original died)
307
+ */
308
+ export function isSameProcess(pid, expectedStartedAtMs, options = {}) {
309
+ if (!isProcessAlive(pid)) return false;
310
+ const expected = Number(expectedStartedAtMs);
311
+ if (!Number.isFinite(expected) || expected <= 0) return true;
312
+ const actual = processStartTimeMs(pid);
313
+ if (actual === null) return true;
314
+ const tolerance = Number.isFinite(Number(options.toleranceMs))
315
+ ? Number(options.toleranceMs)
316
+ : PID_IDENTITY_TOLERANCE_MS;
317
+ return actual <= expected + tolerance;
318
+ }
319
+
320
+ /**
321
+ * Reap a lost worker's recorded agent child (Gap 2). The worker persists
322
+ * `agentPid` + `agentStartedAt` per turn; when the WORKER dies (crash /
323
+ * pid-reuse detection) nothing used to kill that grandchild — it kept
324
+ * running unsupervised. Kill it, tree-wide, IF we can prove identity:
325
+ *
326
+ * - `agentStartedAt` anchor is REQUIRED — unlike the read-only liveness
327
+ * probe, this path kills, so a legacy state without the anchor fails
328
+ * CLOSED (no kill) rather than risking a pid-reused stranger;
329
+ * - with the anchor present, a failed creation-time probe fails open to the
330
+ * kill (the orphan leak is the common case, reuse the rare one — and the
331
+ * anchor already bounds the window to this session's lifetime).
332
+ *
333
+ * @returns {boolean} true when a kill was actually issued
334
+ */
335
+ export function reclaimOrphanAgentProcess(state) {
336
+ const agentPid = Number(state?.agentPid);
337
+ if (!Number.isInteger(agentPid) || agentPid <= 0) return false;
338
+ if (agentPid === Number(state?.pid)) return false; // never the worker itself
339
+ const anchor = Number(state?.agentStartedAt);
340
+ if (!Number.isFinite(anchor) || anchor <= 0) return false; // fail closed
341
+ if (!isSameProcess(agentPid, anchor)) return false; // reused / already gone
342
+ return _deps.killProcessTree(agentPid, "SIGKILL") === true;
343
+ }
344
+
139
345
  export function normalizeBackgroundAgentTitle(title) {
140
346
  const value = String(title || "").trim();
141
347
  if (!value) throw new Error("Background agent title cannot be empty");
142
348
  return value.slice(0, 160);
143
349
  }
144
350
 
351
+ /**
352
+ * The canonical unified lifecycle state (session-lifecycle.js) for a session —
353
+ * folds the supervisor `status` + worker `phase` + `pendingApprovals` into one
354
+ * of the 10 canonical states so every consumer (dashboard, `cc daemon list
355
+ * --json`, an IDE bridge) reasons about a single vocabulary. Pure; call it at a
356
+ * DISPLAY boundary only. It is deliberately NOT baked into
357
+ * effectiveBackgroundAgentState because that function's output is spread back
358
+ * into writeBackgroundAgentState by the rename/pin/stop read-modify-write
359
+ * paths — a derived field there would leak into the on-disk schema.
360
+ */
361
+ export function sessionLifecycleState(state) {
362
+ return deriveSessionState(state);
363
+ }
364
+
365
+ /** A display copy of `state` with the derived `lifecycleState` attached. */
366
+ function withLifecycleState(state) {
367
+ if (!state) return state;
368
+ return { ...state, lifecycleState: deriveSessionState(state) };
369
+ }
370
+
145
371
  export function effectiveBackgroundAgentState(state, options = {}) {
146
372
  if (!state) return null;
147
373
  if (state.status !== "running") return state;
@@ -164,25 +390,53 @@ export function effectiveBackgroundAgentState(state, options = {}) {
164
390
  endedAt: state.endedAt || t,
165
391
  lostReason: "process-exited",
166
392
  };
393
+ } else if (!isSameProcess(state.pid, state.startedAt)) {
394
+ // Gap 1: the pid is alive but belongs to a process created well after
395
+ // this session started — the OS reused the worker's pid.
396
+ next = {
397
+ ...state,
398
+ status: "lost",
399
+ endedAt: state.endedAt || t,
400
+ lostReason: "pid-reused",
401
+ };
167
402
  }
168
- if (next !== state && options.persist !== false) {
169
- try {
170
- writeBackgroundAgentState(next);
171
- } catch {
172
- /* best-effort; callers still get the corrected state */
403
+ if (next !== state) {
404
+ // Gap 2: the worker is gone (dead pid or pid-reused stranger) — reap the
405
+ // recorded agent grandchild so a crashed worker never leaves it running
406
+ // unsupervised. A merely-stale heartbeat with a live, verified worker
407
+ // still owns its child, so isSameProcess gates the reclaim.
408
+ if (!isSameProcess(state.pid, state.startedAt)) {
409
+ try {
410
+ reclaimOrphanAgentProcess(state);
411
+ } catch {
412
+ /* best-effort */
413
+ }
414
+ }
415
+ if (options.persist !== false) {
416
+ try {
417
+ writeBackgroundAgentState(next);
418
+ } catch {
419
+ /* best-effort; callers still get the corrected state */
420
+ }
173
421
  }
174
422
  }
175
423
  return next;
176
424
  }
177
425
 
178
426
  export function listBackgroundAgents(options = {}) {
179
- return readdirSync(backgroundAgentsDir())
180
- .filter((name) => name.endsWith(".json") && !name.includes(".job."))
181
- .map((name) => readBackgroundAgentState(name.slice(0, -5)))
182
- .filter(Boolean)
183
- .map((state) => effectiveBackgroundAgentState(state, options))
184
- .filter((state) => options.all || state.status === "running")
185
- .sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0));
427
+ return (
428
+ readdirSync(backgroundAgentsDir())
429
+ .filter((name) => name.endsWith(".json") && !name.includes(".job."))
430
+ .map((name) => readBackgroundAgentState(name.slice(0, -5)))
431
+ .filter(Boolean)
432
+ .map((state) => effectiveBackgroundAgentState(state, options))
433
+ .filter((state) => options.all || state.status === "running")
434
+ .sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0))
435
+ // Display-only enrichment: attach the canonical unified lifecycle state.
436
+ // This is a list feed (never written back to disk), so the derived field
437
+ // cannot leak into the on-disk schema the way it would in the mutate paths.
438
+ .map(withLifecycleState)
439
+ );
186
440
  }
187
441
 
188
442
  function sleepSyncMs(ms) {
@@ -430,6 +684,7 @@ export function launchBackgroundAgent({
430
684
  pid: null,
431
685
  workerPid: null,
432
686
  agentPid: null,
687
+ agentStartedAt: null,
433
688
  status: "running",
434
689
  startedAt: Date.now(),
435
690
  heartbeatAt: Date.now(),
@@ -496,7 +751,35 @@ export function readBackgroundAgentLog(id, options = {}) {
496
751
  export function stopBackgroundAgent(id) {
497
752
  const state = effectiveBackgroundAgentState(readBackgroundAgentState(id));
498
753
  if (!state) throw new Error(`Background agent not found: ${id}`);
499
- if (state.status !== "running") return { ...state, stopped: false };
754
+ if (state.status !== "running") {
755
+ // Gap 2: stopping an already-lost session still reaps a leaked agent
756
+ // child recorded before the worker died (identity-guarded inside).
757
+ if (state.status === "lost") {
758
+ try {
759
+ reclaimOrphanAgentProcess(state);
760
+ } catch {
761
+ /* best-effort */
762
+ }
763
+ }
764
+ return { ...state, stopped: false };
765
+ }
766
+ // Gap 1: last-instant identity re-check before the kill — never
767
+ // taskkill/SIGTERM a pid the OS has re-assigned to an unrelated process.
768
+ if (!isSameProcess(state.pid, state.startedAt)) {
769
+ try {
770
+ reclaimOrphanAgentProcess(state);
771
+ } catch {
772
+ /* best-effort */
773
+ }
774
+ const lost = {
775
+ ...state,
776
+ status: "lost",
777
+ endedAt: Date.now(),
778
+ lostReason: "pid-reused",
779
+ };
780
+ writeBackgroundAgentState(lost);
781
+ return { ...lost, stopped: false };
782
+ }
500
783
  if (process.platform === "win32") {
501
784
  const killed = _deps.spawnSync(
502
785
  "taskkill",
@@ -517,6 +800,30 @@ export function stopBackgroundAgent(id) {
517
800
  } catch {
518
801
  process.kill(Number(state.pid), "SIGTERM");
519
802
  }
803
+ // The worker detaches the agent child into its OWN process group (so a
804
+ // crashed worker's child can be group-killed) — which also means the
805
+ // worker-group SIGTERM above no longer reaches it. Kill the agent group
806
+ // explicitly, identity-guarded via its agentStartedAt anchor.
807
+ const agentPid = Number(state.agentPid);
808
+ const anchor = Number(state.agentStartedAt);
809
+ if (
810
+ Number.isInteger(agentPid) &&
811
+ agentPid > 0 &&
812
+ agentPid !== Number(state.pid) &&
813
+ Number.isFinite(anchor) &&
814
+ anchor > 0 &&
815
+ isSameProcess(agentPid, anchor)
816
+ ) {
817
+ try {
818
+ process.kill(-agentPid, "SIGTERM");
819
+ } catch {
820
+ try {
821
+ process.kill(agentPid, "SIGTERM");
822
+ } catch {
823
+ /* already gone */
824
+ }
825
+ }
826
+ }
520
827
  }
521
828
  const next = {
522
829
  ...state,
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Outbound backpressure policy — bounds server→client buffering when a
3
+ * consumer drains too slowly, so a stuck WS client can't grow the sender's
4
+ * memory without limit (the classic slow-consumer blowup) and the UI degrades
5
+ * explicitly instead of freezing.
6
+ *
7
+ * Pure + deterministic (no transport, no timers): the caller feeds the
8
+ * transport's current queued-byte count (e.g. `ws.bufferedAmount`) and a frame
9
+ * class; the policy decides send-vs-drop with hysteresis and reports the
10
+ * lag-episode transitions the caller turns into a notice.
11
+ *
12
+ * Designed to compose with EventReplayBuffer (event-seq-replay.js): a dropped
13
+ * *droppable* frame should still be RECORDED (it gets a seq and stays in the
14
+ * bounded replay buffer), just not sent. The seq gap on the next *sent* frame
15
+ * then trips the consumer's SeqGapTracker, which requests replaySince — so the
16
+ * dropped frames are recovered once the consumer catches up (or a truncated
17
+ * replay tells it to full-resync). Critical frames are never dropped.
18
+ */
19
+
20
+ const DEFAULT_HIGH_WATER_BYTES = 4 << 20; // 4 MiB queued → consumer is behind
21
+ const DEFAULT_LOW_WATER_BYTES = 1 << 20; // 1 MiB → considered caught up again
22
+
23
+ /** A frame that must always be delivered (lifecycle, errors, acks). */
24
+ export const CRITICAL = "critical";
25
+ /** A frame safe to drop under pressure and recover via replay/resync. */
26
+ export const DROPPABLE = "droppable";
27
+
28
+ export class BackpressurePolicy {
29
+ constructor({
30
+ highWaterBytes = DEFAULT_HIGH_WATER_BYTES,
31
+ lowWaterBytes = DEFAULT_LOW_WATER_BYTES,
32
+ } = {}) {
33
+ const high = Math.max(
34
+ 1,
35
+ Math.floor(highWaterBytes) || DEFAULT_HIGH_WATER_BYTES,
36
+ );
37
+ // Low water must sit below high water or hysteresis collapses to a flapping
38
+ // single threshold; clamp it to at most high-1.
39
+ const low = Math.min(Math.max(0, Math.floor(lowWaterBytes) || 0), high - 1);
40
+ this._high = high;
41
+ this._low = low;
42
+ this._lagging = false;
43
+ this._dropped = 0; // dropped since the CURRENT lag episode started
44
+ this._droppedTotal = 0; // dropped across the whole connection
45
+ }
46
+
47
+ /** True while the consumer is behind (buffered ≥ high, until it drains ≤ low). */
48
+ get lagging() {
49
+ return this._lagging;
50
+ }
51
+
52
+ /** Frames dropped across the whole connection. */
53
+ get droppedTotal() {
54
+ return this._droppedTotal;
55
+ }
56
+
57
+ /**
58
+ * Decide what to do with one outbound frame.
59
+ * @param {number} bufferedBytes transport's currently-queued bytes
60
+ * (`ws.bufferedAmount`); non-finite → treated as 0 (never lags).
61
+ * @param {"critical"|"droppable"} frameClass
62
+ * @returns {{send:boolean, drop:boolean, lagStarted:boolean, lagEnded:boolean,
63
+ * dropped:number}} `dropped` = count dropped in the episode that just
64
+ * started/ended (for the caller's notice); `send:false` only ever for a
65
+ * droppable frame while lagging.
66
+ */
67
+ evaluate(bufferedBytes, frameClass) {
68
+ const b = Number.isFinite(bufferedBytes) ? bufferedBytes : 0;
69
+ let lagStarted = false;
70
+ let lagEnded = false;
71
+
72
+ if (!this._lagging && b >= this._high) {
73
+ this._lagging = true;
74
+ this._dropped = 0;
75
+ lagStarted = true;
76
+ } else if (this._lagging && b <= this._low) {
77
+ this._lagging = false;
78
+ lagEnded = true;
79
+ }
80
+
81
+ // While lagging, shed droppable frames to let the buffer drain; critical
82
+ // frames (and everything while not lagging) always go out.
83
+ if (this._lagging && frameClass === DROPPABLE) {
84
+ this._dropped += 1;
85
+ this._droppedTotal += 1;
86
+ return {
87
+ send: false,
88
+ drop: true,
89
+ lagStarted,
90
+ lagEnded,
91
+ dropped: this._dropped,
92
+ };
93
+ }
94
+
95
+ // `dropped` carries the episode's running count so a lag-end notice can
96
+ // report how many droppable frames were shed.
97
+ return {
98
+ send: true,
99
+ drop: false,
100
+ lagStarted,
101
+ lagEnded,
102
+ dropped: this._dropped,
103
+ };
104
+ }
105
+ }