chainlesschain 0.165.5 → 0.165.8

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 (256) hide show
  1. package/package.json +2 -1
  2. package/src/assets/web-panel/assets/{AIOps-CEeYTeDB.js → AIOps-B3gWmXjo.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-BeRUQ3V2.js → ActionButton-G9pdT-0e.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-DrV718XF.js → Analytics-Ca30J7m2.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-Cd2Frt7Q.js → AppLayout-BtVTm8PU.js} +5 -5
  6. package/src/assets/web-panel/assets/{Artifacts-CUJMlORU.js → Artifacts-BhViNa6u.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-CXJcfT6p.js → Audit-DhKq-FnY.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-DOv6Z7H9.js → BackgroundAgents-BL0SVbnE.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-BwdDyBI0.js → Backup-BWkblnFi.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-BmLn4kYH.js → BaseInput-ClaRNrF8.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-COg8pAW2.js → Chat-sZE2Z77o.js} +4 -4
  12. package/src/assets/web-panel/assets/ChatBubbleRenderer-BAfKOnRe.js +1 -0
  13. package/src/assets/web-panel/assets/{Checkbox-BCP4uoU8.js → Checkbox-Cvhay5WM.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-pRt6Y9JI.js → Codegen-BpejL-4N.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-DLaprKhS.js → Col-g2Ki6g_1.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-DyATsz5f.js → Community-D2xKbNo9.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-BXAaupZK.js → Compact-DbL1Pds3.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-DpcJBELr.js → Compliance-DGtZCV4c.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-DG4EFWyd.js → Cowork-4kQpoNsI.js} +3 -3
  20. package/src/assets/web-panel/assets/{Cron-F6Oy5n2P.js → Cron-BxTYXXAN.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-BOdgtalX.js → Crosschain-k0J0LT2N.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-DT1dSkjH.js → DID-BaqhoPgN.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-NBUIqWzC.js → Dashboard-T4RVPIM1.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-CSpHeMYB.js → Dropdown-DszOToWq.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-CEG0xISr.js → EmailListRenderer-C9KJU7HF.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-BTZ0BZna.js → FamilyGuardDashboard-Cmr0JnsA.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-BuuCTn2V.js → Federation-BI-_2xZX.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-KAVceevk.js → FormItemContext-DsnDEPsl.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-DnHpcQvW.js → GenericCardRenderer-BLncAKKK.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-C_B_0HwB.js → Git-BPQhGXDn.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-DPSZuzuu.js → Governance-CyElbUQW.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-BV-06V7q.js → Inference-CcSntBQD.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-wpDuupn6.js → KnowledgeGraph-p6e7HZ2r.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-Bseca1_I.js → Logs-CNEtr-wz.js} +2 -2
  35. package/src/assets/web-panel/assets/MarkdownRenderer-Bj9xI0YH.js +1 -0
  36. package/src/assets/web-panel/assets/{Marketplace-d3kM4DDL.js → Marketplace-Dd2notAJ.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-DQ_xZ23i.js → McpTools-BBsN2CSW.js} +4 -4
  38. package/src/assets/web-panel/assets/{Memory-CRIeYO4L.js → Memory-Dg2uTvwx.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-BsllIa9l.js → MobileBridge-r1YMRiJV.js} +1 -1
  40. package/src/assets/web-panel/assets/{MobileProjects-BUuQy30X.js → MobileProjects-C-89kSm4.js} +1 -1
  41. package/src/assets/web-panel/assets/{Mtc-BOFDIzrC.js → Mtc-CrPZcVgf.js} +5 -5
  42. package/src/assets/web-panel/assets/{MtcAudit-BQg-vyvG.js → MtcAudit-sxi7hqJb.js} +5 -5
  43. package/src/assets/web-panel/assets/{Multisig-Bo1jrPdN.js → Multisig-CAgOMfF1.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-Bmod8alw.js → NLProgramming-DafaPts3.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-CGGFL8w-.js → Notes-CJM-xUW_.js} +3 -3
  46. package/src/assets/web-panel/assets/{NotificationSettings-BsZvCJxC.js → NotificationSettings-L0ZjfAh_.js} +1 -1
  47. package/src/assets/web-panel/assets/{OrderTableRenderer-C1-Quo_h.js → OrderTableRenderer-BB1CvCK0.js} +1 -1
  48. package/src/assets/web-panel/assets/{Organization-BjOeDYOi.js → Organization-DeLtgAD8.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-DwVV3l5A.js → Overflow-CKC2b10z.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-Cu3DQT9W.js → P2P-eyf2rzY1.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-Cg0gR7dH.js → PdhVaultBrowser-5upNIOW8.js} +3 -3
  52. package/src/assets/web-panel/assets/{Permissions-CsAXxRN5.js → Permissions-B8p3W6JK.js} +4 -4
  53. package/src/assets/web-panel/assets/{PersonalDataHub-C-nNmnYp.js → PersonalDataHub-BnRMxTq3.js} +5 -5
  54. package/src/assets/web-panel/assets/{Pipeline-BOnW7nNc.js → Pipeline-DjCNI1li.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-ClLSyGKt.js → Privacy-u6gXLbWT.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-DuLvjjq7.js → ProjectInit-BZAw4tkM.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-DQYYbDD9.js → ProjectSettings-B1Tr72q7.js} +2 -2
  58. package/src/assets/web-panel/assets/{Projects-Drw8MBGK.js → Projects-DO7HFuBQ.js} +1 -1
  59. package/src/assets/web-panel/assets/{Providers-ckqHQe6K.js → Providers-BjCztyhj.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-DW5BXk-G.js → QrScannerModal-ZWyCUbR1.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-DDLnhNTp.js → QuickAsk-DhDjeW4G.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-PJQ2C91U.js → Recommend-R_CN2MuU.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-s6wmda34.js → RemoteSession-uxFcujeu.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-DC2sb-I3.js → Reputation-DX8D7rks.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-hPGk1eGj.js → Row-BYahQILD.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-RIvfZql0.js → RssFeed-CUSQ2tm-.js} +3 -3
  67. package/src/assets/web-panel/assets/{Search-DFg1G1eR.js → Search-CnN_S8Cs.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-D6H5p8Dp.js → Security-DcZlh-O4.js} +4 -4
  69. package/src/assets/web-panel/assets/{Services-nxYiFV_w.js → Services-ZhBOjzIX.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-BmgFNAdB.js → Skeleton-D5Qnq_lI.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-CsaaD2av.js → Skills-BC9cZ3Be.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-yM5y9yvK.js → Sla-DauJ3E8_.js} +1 -1
  73. package/src/assets/web-panel/assets/SpeechSettings-DbNeZ_fh.js +1 -0
  74. package/src/assets/web-panel/assets/{SyncSettings-CFVhEyyr.js → SyncSettings-DUvJbSct.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-DdO02Inn.js → Tasks-BXa-5noQ.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-Eo2d8Llu.js → Templates-JxaiUt_3.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-ChESOXgy.js → Tenant-BW4ERQN8.js} +1 -1
  78. package/src/assets/web-panel/assets/{Terminal-BJ7oDxTI.js → Terminal-DltUbdtv.js} +2 -2
  79. package/src/assets/web-panel/assets/{TimelineRenderer-hWX0fUvz.js → TimelineRenderer-CMoZz0Tc.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-DVBZQnd-.js → Tokens-D_VEV60K.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-D89cQfVq.js → Trigger-CCXTCs0R.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-BIVSb4-V.js → Trust-lkGRIPcG.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-B58gWopl.js → UkeySign-BfGeQ9HX.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-DhHI7bmG.js → VideoEditing-BEC7pYvc.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-C4FheWS_.js → Wallet-BZdG24o0.js} +4 -4
  86. package/src/assets/web-panel/assets/{WebAuthn-DnPRq-Bx.js → WebAuthn-yoEddedp.js} +3 -3
  87. package/src/assets/web-panel/assets/{WorkflowEditor-BerRPjpJ.js → WorkflowEditor-BXZEprOW.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-BzZ52iKr.js → chat-gg0bzZal.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-BI_E4BAn.js → colors-DjRVRuin.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-d50mr3yk.js → compact-item-su-dieAc.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-C6FsQ1IH.js → createContext-Ci3S8oKF.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-CxbFX9uH.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-DLZEMgLP.js → hasIn-D8F10heH.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-D-aBumtY.js → index-11wtYQiF.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-CjuKZBqJ.js → index-1IgxgALJ.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-9H_M2jju.js → index-1agckWSq.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-DE0V6cp-.js → index-5JqhujnR.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-Y2-q7QLm.js → index-6Tniqk1s.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-DoFtCVZ7.js → index-8Z-8BIlN.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-KNGgO4Wm.js → index-BCI5v0yB.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-DY4j5zkF.js → index-BDJntYmJ.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-BZjjdQ62.js → index-BGzixOGZ.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-BTpIOTfR.js → index-BMMOVoLD.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-BB9SqVSt.js → index-BTFpdBtt.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-AHr7ryWf.js → index-Ba_HyPnE.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-Pvf30m8Q.js → index-BbF1IHof.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-CRN5TRTQ.js → index-BgT9su_e.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-CUaEHfQ0.js → index-BzPB6kGZ.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-CdKt9fOd.js → index-C6ygzplr.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-Dec0Kf_Z.js → index-CC3ZDjzY.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-IPkwSAW_.js → index-CEmXRhAE.js} +1 -1
  112. package/src/assets/web-panel/assets/index-CJGOlduY.js +1 -0
  113. package/src/assets/web-panel/assets/{index-C3k8kevG.js → index-CMPqd5Cv.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-A5CDWP7y.js → index-COWd8uGx.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-CO8DAF7U.js → index-CPIYSp4H.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-CVzuawCX.js → index-CR0h6seT.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-Co7s9LsB.js → index-CUmDhC4j.js} +1 -1
  118. package/src/assets/web-panel/assets/index-CkBCN5yH.js +1 -0
  119. package/src/assets/web-panel/assets/{index-CLoTQr0W.js → index-CusgZPtc.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-B81mo1Et.js → index-DBAETXft.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-BcSUl9fA.js → index-DIJ2qFOC.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-Q5Tn735s.js → index-Dbq4NMET.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-GXE0D8Nx.js → index-DkMEF5VT.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-TWG-_D1M.js → index-DkfQR3hM.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-BAE3hwev.js → index-DnLucOn9.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-CgkoH3_g.js → index-DugB_aKa.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-BRGx3w6T.js → index-DyLUarhZ.js} +3 -3
  128. package/src/assets/web-panel/assets/{index-BeM4hfht.js → index-T6_6ygJZ.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-DsscwxPE.js → index-Tu8eWqh-.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-Bmr_vxXD.js → index-W7XUgdVL.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-BoNZlzhQ.js → index-dHZinfko.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-DqkbHHg8.js → index-jyNgp5FI.js} +1 -1
  133. package/src/assets/web-panel/assets/{initDefaultProps-D-4Hzgej.js → initDefaultProps-Cv4p8e4w.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-D4Wl4xU7.js → motion-B8EwmMOw.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-Cd5eQvL0.js → move-B8R9F8TK.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-B4ilslCW.js → mtc-parser-DoFYz5Bp.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-Ckarx-tf.js → omit-CHWYLBjI.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-QJ0bpCmc.js → pickAttrs-CXuPDO77.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-CKHm-tq8.js → placementArrow-BaAtUUon.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-CV6a_w_Y.js → responsiveObserve-DCc4zcDD.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-mHpeQFgk.js → slide-B_lx89oZ.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-DIaQNQWA.js → statusUtils-DE8DPmR0.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-PJpzavV8.js → styleChecker-1ktXOsL_.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-BH7zTw8S.js → useFlexGapSupport-xH89-wjh.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-BDNG3JDd.js → useFs-DscvfW5p.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-CY2VWdUg.js → usePersonalDataHub-RdMKQta7.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-D0kw9jhz.js → vnode-Z3wTni2T.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-B5g7TN7r.js → zoom-B0iChCsm.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/command-help-index.json +3 -3
  151. package/src/commands/agent.js +105 -62
  152. package/src/commands/background-session.js +100 -10
  153. package/src/commands/git.js +39 -0
  154. package/src/commands/mcp.js +5 -0
  155. package/src/commands/plugin.js +160 -29
  156. package/src/commands/remote-control.js +295 -110
  157. package/src/commands/session-group.js +131 -0
  158. package/src/commands/session-location.js +48 -0
  159. package/src/commands/session-message.js +171 -0
  160. package/src/commands/session.js +22 -0
  161. package/src/commands/sync-providers.js +4 -1
  162. package/src/commands/update.js +1 -0
  163. package/src/data/changelog.json +41 -2
  164. package/src/gateways/ws/ws-agent-handler.js +20 -12
  165. package/src/harness/jsonl-session-store.js +279 -37
  166. package/src/harness/mcp-client.js +832 -91
  167. package/src/harness/session-index.js +15 -4
  168. package/src/lib/agent-worktree.js +8 -1
  169. package/src/lib/auto-mode-safety-classifier.js +3 -17
  170. package/src/lib/background-agent-keeper-protocol.js +26 -6
  171. package/src/lib/background-agent-supervisor.js +17 -1
  172. package/src/lib/background-session-transport.js +153 -27
  173. package/src/lib/browser-evidence.js +1405 -0
  174. package/src/lib/chrome-connector.js +465 -79
  175. package/src/lib/claude-project-auto-memory.js +481 -0
  176. package/src/lib/claude-project-storage-layout.js +104 -0
  177. package/src/lib/cloud/cloud-client.js +40 -0
  178. package/src/lib/execution-location-local-supervisor.mjs +226 -0
  179. package/src/lib/execution-location-runner-lifecycle.js +991 -0
  180. package/src/lib/execution-location-target-preflight.js +333 -0
  181. package/src/lib/execution-location-target.js +783 -13
  182. package/src/lib/file-lines.js +6 -1
  183. package/src/lib/gateway-client-resilience.js +571 -0
  184. package/src/lib/gitlab-mr-worktree.js +176 -0
  185. package/src/lib/hooks-v2-producers.js +119 -15
  186. package/src/lib/hooks-v2-workspace-context.js +44 -1
  187. package/src/lib/host-resource-budget.js +295 -0
  188. package/src/lib/ide-context.js +15 -2
  189. package/src/lib/llm-config-defaults.js +43 -0
  190. package/src/lib/mcp-client.js +2 -0
  191. package/src/lib/mcp-headers-helper.js +47 -5
  192. package/src/lib/mcp-lifecycle-authority.js +887 -0
  193. package/src/lib/mcp-oauth.js +156 -26
  194. package/src/lib/mcp-tls.js +330 -0
  195. package/src/lib/output-styles.js +12 -0
  196. package/src/lib/paths.js +260 -32
  197. package/src/lib/permission-request.js +48 -0
  198. package/src/lib/plugin-runtime/bin.js +3 -1
  199. package/src/lib/plugin-runtime/hooks.js +3 -1
  200. package/src/lib/plugin-runtime/install.js +730 -71
  201. package/src/lib/plugin-runtime/lsp.js +4 -4
  202. package/src/lib/plugin-runtime/marketplace-archive-source.js +366 -0
  203. package/src/lib/plugin-runtime/marketplace-catalog.js +63 -6
  204. package/src/lib/plugin-runtime/marketplace-command-descriptor.js +343 -0
  205. package/src/lib/plugin-runtime/marketplace-command-source.js +580 -0
  206. package/src/lib/plugin-runtime/marketplace-remote-artifacts.js +1 -0
  207. package/src/lib/plugin-runtime/marketplace-source-adapter.js +188 -0
  208. package/src/lib/plugin-runtime/marketplace-source-cache.js +11 -1
  209. package/src/lib/plugin-runtime/mcp.js +3 -1
  210. package/src/lib/plugin-runtime/monitors.js +3 -1
  211. package/src/lib/plugin-runtime/remote-source.js +583 -58
  212. package/src/lib/plugin-runtime/settings.js +3 -1
  213. package/src/lib/plugin-runtime/trust.js +274 -13
  214. package/src/lib/plugin-security.js +393 -41
  215. package/src/lib/plugin-source-identity.js +850 -0
  216. package/src/lib/process-execution-broker/credential-agent.js +210 -41
  217. package/src/lib/process-execution-broker/index.js +15 -1
  218. package/src/lib/process-execution-broker/linux-cgroup-v2.js +391 -0
  219. package/src/lib/process-execution-broker/platform-sandbox.js +50 -28
  220. package/src/lib/project-mcp-trust.js +165 -10
  221. package/src/lib/remote-approval-bridge.js +136 -46
  222. package/src/lib/remote-control.js +217 -31
  223. package/src/lib/resume-session.js +9 -6
  224. package/src/lib/session-core-singletons.js +26 -11
  225. package/src/lib/session-host-lease.js +52 -16
  226. package/src/lib/session-lifecycle-scope.js +153 -0
  227. package/src/lib/session-message-fabric.js +1252 -0
  228. package/src/lib/session-projection.js +286 -3
  229. package/src/lib/session-runtime-retention.js +155 -0
  230. package/src/lib/session-store-guard.js +3 -1
  231. package/src/lib/session-workbench-store.js +466 -0
  232. package/src/lib/settings-loader.cjs +29 -0
  233. package/src/lib/spellcheck.js +211 -0
  234. package/src/lib/sub-agent-context.js +9 -9
  235. package/src/lib/sync-cli-db.js +485 -129
  236. package/src/lib/sync-credentials.js +31 -20
  237. package/src/lib/web-fetch.js +112 -40
  238. package/src/lib/web-search.js +127 -61
  239. package/src/lib/workspace-trust.js +793 -0
  240. package/src/repl/agent-repl.js +175 -19
  241. package/src/repl/permission-prompt.js +29 -8
  242. package/src/repl/repl-keybindings.js +55 -0
  243. package/src/runtime/agent-core.js +362 -20
  244. package/src/runtime/bootstrap.js +37 -13
  245. package/src/runtime/coding-agent-contract-shared.cjs +11 -4
  246. package/src/runtime/headless-runner.js +281 -102
  247. package/src/runtime/headless-stream.js +199 -30
  248. package/src/runtime/mcp-config.js +298 -6
  249. package/src/runtime/policies/agent-policy.js +10 -0
  250. package/src/runtime/status-diagnostics-lite.js +9 -1
  251. package/src/assets/web-panel/assets/ChatBubbleRenderer-4gGkdGgA.js +0 -1
  252. package/src/assets/web-panel/assets/MarkdownRenderer-B4RLp2zs.js +0 -1
  253. package/src/assets/web-panel/assets/SpeechSettings-DPy1XMYh.js +0 -1
  254. package/src/assets/web-panel/assets/devWarning-CagEcIex.js +0 -1
  255. package/src/assets/web-panel/assets/index-pzT9PpOU.js +0 -1
  256. package/src/assets/web-panel/assets/index-z1kFadaX.js +0 -1
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { executionBroker } from "../lib/process-execution-broker/index.js";
12
12
  import { AsyncLocalStorage } from "node:async_hooks";
13
+ import { randomUUID } from "node:crypto";
13
14
  import { EventEmitter } from "events";
14
15
  import { pathToFileURL } from "url";
15
16
  import { isProxy } from "node:util/types";
@@ -45,6 +46,17 @@ import {
45
46
  prepareMcpStdioExecutableIdentity,
46
47
  } from "../lib/mcp-stdio-executable-identity.js";
47
48
  import { resolveMcpStdioSandboxContext } from "../lib/mcp-stdio-workspace-authority.js";
49
+ import {
50
+ assertManagedMcpTlsConfig,
51
+ closeMcpTlsDispatcher,
52
+ createMcpTlsDispatcher,
53
+ loadMcpTlsMaterial,
54
+ } from "../lib/mcp-tls.js";
55
+ import {
56
+ MCP_LIFECYCLE_ERROR_CODES,
57
+ MCP_LIFECYCLE_PHASES,
58
+ McpLifecycleAuthority,
59
+ } from "../lib/mcp-lifecycle-authority.js";
48
60
 
49
61
  /**
50
62
  * Injectable dependencies — overridable from tests.
@@ -63,6 +75,9 @@ export const _deps = {
63
75
  verifyMcpStdioApprovedWorkingDirectory,
64
76
  prepareMcpStdioExecutableIdentity,
65
77
  resolveMcpStdioSandboxContext,
78
+ loadMcpTlsMaterial,
79
+ createMcpTlsDispatcher,
80
+ closeMcpTlsDispatcher,
66
81
  // Backoff sleep seam (tests override with a no-op so retries don't wait).
67
82
  sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
68
83
  };
@@ -229,7 +244,124 @@ const MCP_HTTP_RESPONSE_MAX_BYTES = 16 * 1024 * 1024;
229
244
  * hard 30s regardless of its config — an inconsistency with HTTP servers.
230
245
  */
231
246
  const STDIO_REQUEST_TIMEOUT_MS = 30000;
247
+ // Long-running MCP tools are allowed to exceed the ordinary request deadline,
248
+ // but may not retain an agent turn forever without any protocol progress. A
249
+ // server-specific value can only tighten this host ceiling; zero/invalid values
250
+ // never disable it.
251
+ const MCP_TOOL_IDLE_TIMEOUT_MS = 5 * 60 * 1000;
252
+ const MCP_DISCOVERY_CACHE_TTL_MS = 60 * 1000;
253
+ const MCP_DISCOVERY_CACHE_MAX_STALE_MS = 10 * 60 * 1000;
254
+ const MCP_DISCOVERY_CACHE_MAX_STRIKES = 2;
232
255
  const MCP_PROTOCOL_VERSION = "2025-11-25";
256
+ export const SUPPORTED_MCP_PROTOCOL_VERSIONS = Object.freeze([
257
+ "2024-11-05",
258
+ "2025-03-26",
259
+ "2025-06-18",
260
+ MCP_PROTOCOL_VERSION,
261
+ ]);
262
+ const supportedMcpProtocolVersions = new Set(SUPPORTED_MCP_PROTOCOL_VERSIONS);
263
+
264
+ function mcpToolIdleTimeoutMs(config, options = {}) {
265
+ const candidate = Number.isFinite(options.idleTimeoutMs)
266
+ ? options.idleTimeoutMs
267
+ : config?.toolIdleTimeoutMs;
268
+ if (Number.isFinite(candidate) && candidate > 0) {
269
+ return Math.min(MCP_TOOL_IDLE_TIMEOUT_MS, Math.floor(candidate));
270
+ }
271
+ return MCP_TOOL_IDLE_TIMEOUT_MS;
272
+ }
273
+
274
+ function mcpToolIdleTimeoutError(serverName, toolName, timeoutMs) {
275
+ const error = new Error(
276
+ `MCP tool "${toolName}" on server "${serverName}" made no progress for ${timeoutMs}ms`,
277
+ );
278
+ error.code = "CC_MCP_TOOL_IDLE_TIMEOUT";
279
+ error.timeoutMs = timeoutMs;
280
+ return error;
281
+ }
282
+
283
+ function boundedPositiveInt(value, fallback, maximum) {
284
+ if (!Number.isFinite(value) || value <= 0) return fallback;
285
+ return Math.min(maximum, Math.floor(value));
286
+ }
287
+
288
+ function discoveryCachePolicy(config = {}) {
289
+ const configured = config?.discoveryCache;
290
+ if (configured !== true && (!configured || typeof configured !== "object")) {
291
+ return { enabled: false };
292
+ }
293
+ if (configured?.enabled === false) return { enabled: false };
294
+ const fields = configured === true ? {} : configured;
295
+ const ttlMs = boundedPositiveInt(
296
+ fields.ttlMs,
297
+ MCP_DISCOVERY_CACHE_TTL_MS,
298
+ MCP_DISCOVERY_CACHE_MAX_STALE_MS,
299
+ );
300
+ const maxStaleMs = boundedPositiveInt(
301
+ fields.maxStaleMs,
302
+ MCP_DISCOVERY_CACHE_MAX_STALE_MS,
303
+ 24 * 60 * 60 * 1000,
304
+ );
305
+ return {
306
+ enabled: true,
307
+ ttlMs,
308
+ maxStaleMs: Math.max(ttlMs, maxStaleMs),
309
+ maxStrikes: boundedPositiveInt(
310
+ fields.maxStrikes,
311
+ MCP_DISCOVERY_CACHE_MAX_STRIKES,
312
+ 10,
313
+ ),
314
+ };
315
+ }
316
+
317
+ function discoveryCapabilitiesSignature(capabilities) {
318
+ const value =
319
+ capabilities && typeof capabilities === "object" ? capabilities : {};
320
+ return JSON.stringify({
321
+ tools: value.tools !== undefined,
322
+ resources: value.resources !== undefined,
323
+ prompts: value.prompts !== undefined,
324
+ });
325
+ }
326
+
327
+ function discoveryCacheKey(name, entry) {
328
+ const config = entry?.config || {};
329
+ // This key never leaves process memory. Bind to an endpoint/launcher so a
330
+ // same-named server pointed at a different target cannot inherit tools.
331
+ return JSON.stringify([
332
+ String(name),
333
+ entry?.transportKind || inferTransport(config),
334
+ entry?.httpUrl || entry?.socketUrl || config.url || null,
335
+ config.command || null,
336
+ Array.isArray(config.args) ? config.args : [],
337
+ ]);
338
+ }
339
+
340
+ function cloneDiscoveryList(value) {
341
+ return Array.isArray(value)
342
+ ? value.map((item) =>
343
+ item && typeof item === "object" ? { ...item } : item,
344
+ )
345
+ : [];
346
+ }
347
+
348
+ function applyDiscoveryCache(entry, cached) {
349
+ entry.resources = cloneDiscoveryList(cached.resources);
350
+ entry.resourceTemplates = cloneDiscoveryList(cached.resourceTemplates);
351
+ entry.prompts = cloneDiscoveryList(cached.prompts);
352
+ entry.toolsError = null;
353
+ }
354
+
355
+ export function assertSupportedMcpProtocolVersion(value) {
356
+ if (typeof value !== "string" || !supportedMcpProtocolVersions.has(value)) {
357
+ const error = new Error(
358
+ "MCP server selected an unsupported protocol version",
359
+ );
360
+ error.code = "CC_MCP_PROTOCOL_VERSION_UNSUPPORTED";
361
+ throw error;
362
+ }
363
+ return value;
364
+ }
233
365
  const URL_ELICITATION_REQUIRED = -32042;
234
366
  const MCP_RPC_ERROR_CODE = "CC_MCP_RPC_ERROR";
235
367
  const mcpRpcControlData = new WeakMap();
@@ -885,6 +1017,10 @@ function isMcpNonRetryableProtocolError(error) {
885
1017
  );
886
1018
  }
887
1019
 
1020
+ function isMcpFatalDiscoveryError(error) {
1021
+ return isMcpNonRetryableProtocolError(error) || isMcpJsonBudgetError(error);
1022
+ }
1023
+
888
1024
  /**
889
1025
  * Heuristic: does this error look like the server went away (vs. the tool
890
1026
  * itself failing)? Used to gate reconnect-and-retry for servers that have a
@@ -1287,8 +1423,27 @@ export class MCPClient extends EventEmitter {
1287
1423
  this._nextId = 1;
1288
1424
  this._reconnectors = new Map(); // name → async () => config|null
1289
1425
  this._reconnecting = new Map(); // name → in-flight reconnect promise
1426
+ // progressToken → reset-idle callback for in-flight tools/call requests.
1427
+ // Tokens are generated by this client and are removed on every terminal
1428
+ // path, so a peer notification cannot retain timers or request state.
1429
+ this._toolProgress = new Map();
1430
+ // Capability metadata stays process-local by default. A server must opt in
1431
+ // through `discoveryCache`; each reconnect still performs initialize and
1432
+ // all policy/transport admission before a cache entry is considered.
1433
+ this._discoveryCache =
1434
+ options?.discoveryCache instanceof Map
1435
+ ? options.discoveryCache
1436
+ : new Map();
1437
+ // Desired resource subscriptions outlive a single transport entry. Keeping
1438
+ // them on the client lets a later reconnect retry restoration even when a
1439
+ // previous partial reconnect was torn down fail-closed.
1440
+ this._resourceSubscriptions = new Map(); // name → Set<uri>
1441
+ this._lifecycleAttempts = new Map(); // name → latest durable generation token
1290
1442
  this._sessionId =
1291
1443
  options && options.sessionId != null ? String(options.sessionId) : null;
1444
+ this._lifecycleSessionId = this._sessionId || "default";
1445
+ this._lifecycleAuthority =
1446
+ options?.lifecycleAuthority || new McpLifecycleAuthority();
1292
1447
  this._workspaceBinding =
1293
1448
  options?.workspaceBinding || currentHostHooksV2WorkspaceBinding();
1294
1449
  // MCP roots capability (Claude-Code 2.1.203 parity): servers may ask the
@@ -1848,6 +2003,90 @@ export class MCPClient extends EventEmitter {
1848
2003
  */
1849
2004
  setSessionId(id) {
1850
2005
  this._sessionId = id != null && id !== "" ? String(id) : null;
2006
+ if (this.servers.size === 0) {
2007
+ this._lifecycleSessionId = this._sessionId || "default";
2008
+ }
2009
+ }
2010
+
2011
+ lifecycleSnapshot(name) {
2012
+ return this._lifecycleAuthority.snapshot({
2013
+ name,
2014
+ sessionId: this._lifecycleSessionId,
2015
+ });
2016
+ }
2017
+
2018
+ _transitionLifecycle(
2019
+ entry,
2020
+ phase,
2021
+ details = {},
2022
+ { tolerateFence = false } = {},
2023
+ ) {
2024
+ if (!entry?._lifecycleAttempt) return null;
2025
+ try {
2026
+ return this._lifecycleAuthority.transition(
2027
+ entry._lifecycleAttempt,
2028
+ phase,
2029
+ details,
2030
+ );
2031
+ } catch (error) {
2032
+ if (tolerateFence && error?.code === MCP_LIFECYCLE_ERROR_CODES.FENCED) {
2033
+ return null;
2034
+ }
2035
+ throw error;
2036
+ }
2037
+ }
2038
+
2039
+ _registerLifecycleRpc(entry, requestId, method) {
2040
+ if (!entry?._lifecycleAttempt) return null;
2041
+ return this._lifecycleAuthority.registerRpc(
2042
+ entry._lifecycleAttempt,
2043
+ requestId,
2044
+ method,
2045
+ );
2046
+ }
2047
+
2048
+ _settleLifecycleRpc(entry, requestId, outcome) {
2049
+ if (!entry?._lifecycleAttempt) return true;
2050
+ return this._lifecycleAuthority.settleRpc(
2051
+ entry._lifecycleAttempt,
2052
+ requestId,
2053
+ outcome,
2054
+ );
2055
+ }
2056
+
2057
+ _staleLifecycleCallbackError() {
2058
+ const error = new Error(
2059
+ "MCP callback belongs to a stale lifecycle generation",
2060
+ );
2061
+ error.code = MCP_LIFECYCLE_ERROR_CODES.FENCED;
2062
+ return error;
2063
+ }
2064
+
2065
+ _rejectUnexpectedLifecycleRpc(entry, requestId, reason) {
2066
+ if (!entry?._lifecycleAttempt) return false;
2067
+ return this._lifecycleAuthority.rejectUnexpectedRpc(
2068
+ entry._lifecycleAttempt,
2069
+ requestId,
2070
+ reason,
2071
+ );
2072
+ }
2073
+
2074
+ _failLifecycle(entry, error) {
2075
+ try {
2076
+ return this._transitionLifecycle(
2077
+ entry,
2078
+ MCP_LIFECYCLE_PHASES.FAILED,
2079
+ { reasonCode: error?.code || "CC_MCP_TRANSPORT_FAILED" },
2080
+ { tolerateFence: true },
2081
+ );
2082
+ } catch (transitionError) {
2083
+ if (
2084
+ transitionError?.code === MCP_LIFECYCLE_ERROR_CODES.INVALID_TRANSITION
2085
+ ) {
2086
+ return null;
2087
+ }
2088
+ return null;
2089
+ }
1851
2090
  }
1852
2091
 
1853
2092
  /**
@@ -1990,11 +2229,27 @@ export class MCPClient extends EventEmitter {
1990
2229
  * @param {string} name - Server name
1991
2230
  * @param {object} config - { command?, args?, env?, url?, transport? }
1992
2231
  */
1993
- async connect(name, config, _authRetryUsed = false) {
2232
+ async connect(name, config, internalOptions = false) {
1994
2233
  if (this.servers.has(name)) {
1995
2234
  throw new Error(`Server "${name}" already connected`);
1996
2235
  }
1997
2236
 
2237
+ const internal =
2238
+ internalOptions && typeof internalOptions === "object"
2239
+ ? internalOptions
2240
+ : { authRetryUsed: internalOptions === true };
2241
+ const authRetryUsed = internal.authRetryUsed === true;
2242
+ if (config?.disabled === true || config?.enabled === false) {
2243
+ this._lifecycleAuthority.markDisabled({
2244
+ name,
2245
+ sessionId: this._lifecycleSessionId,
2246
+ config,
2247
+ });
2248
+ const error = new Error(`MCP server "${name}" is disabled`);
2249
+ error.code = MCP_LIFECYCLE_ERROR_CODES.DISABLED;
2250
+ throw error;
2251
+ }
2252
+
1998
2253
  const transportKind = inferTransport(config);
1999
2254
  let stdioExecutionApproval = null;
2000
2255
  let approvedStdioInvocation = null;
@@ -2082,11 +2337,46 @@ export class MCPClient extends EventEmitter {
2082
2337
  };
2083
2338
  }
2084
2339
  }
2085
- const connectionHeaders = await this._connectionHeaders(
2086
- name,
2087
- sourceConfig,
2088
- transportKind,
2089
- );
2340
+ const lifecycleAttempt =
2341
+ internal.lifecycleAttempt ||
2342
+ this._lifecycleAuthority.beginConnection({
2343
+ name,
2344
+ sessionId: this._lifecycleSessionId,
2345
+ config: sourceConfig,
2346
+ reconnect: internal.reconnect === true,
2347
+ explicitEnable: true,
2348
+ });
2349
+ this._lifecycleAttempts.set(name, lifecycleAttempt);
2350
+ let tlsMaterial = null;
2351
+ let connectionHeaders;
2352
+ try {
2353
+ if (sourceConfig.tls != null) {
2354
+ // Source provenance is enforced before header helpers, dispatcher
2355
+ // creation, or any transport work. `configScope` alone is not trusted:
2356
+ // the TLS object must carry the opaque managed-loader provenance too.
2357
+ assertManagedMcpTlsConfig(sourceConfig.tls, {
2358
+ configScope: sourceConfig.configScope,
2359
+ });
2360
+ if (transportKind !== "https" && transportKind !== "wss") {
2361
+ throw mcpTransportError(
2362
+ "CC_MCP_TLS_TRANSPORT_REQUIRED",
2363
+ `MCP TLS configuration requires https or wss (server "${name}")`,
2364
+ { transport: transportKind, url: sourceConfig.url },
2365
+ );
2366
+ }
2367
+ tlsMaterial = _deps.loadMcpTlsMaterial(sourceConfig.tls, {
2368
+ configScope: sourceConfig.configScope,
2369
+ });
2370
+ }
2371
+ connectionHeaders = await this._connectionHeaders(
2372
+ name,
2373
+ sourceConfig,
2374
+ transportKind,
2375
+ );
2376
+ } catch (error) {
2377
+ this._failLifecycle({ _lifecycleAttempt: lifecycleAttempt }, error);
2378
+ throw error;
2379
+ }
2090
2380
  config = { ...runtimeConfig, headers: connectionHeaders };
2091
2381
  const entry = {
2092
2382
  // Keep the source config (static headers + helper command) so every
@@ -2104,6 +2394,8 @@ export class MCPClient extends EventEmitter {
2104
2394
  _httpRequestControllers: new Set(),
2105
2395
  _httpDiscardControllers: new Set(),
2106
2396
  _httpDiscardStopping: false,
2397
+ _httpDispatcher: null,
2398
+ _tlsIdentityDigest: tlsMaterial?.identityDigest || null,
2107
2399
  _disconnectPromise: null,
2108
2400
  _webSocketPayloadError: null,
2109
2401
  _webSocketInboundError: null,
@@ -2111,6 +2403,7 @@ export class MCPClient extends EventEmitter {
2111
2403
  _toolMetadataBytes: 0,
2112
2404
  _stdioExecutionApproval: stdioExecutionApproval,
2113
2405
  _stdioExecutableIdentity: stdioExecutableIdentity,
2406
+ _lifecycleAttempt: lifecycleAttempt,
2114
2407
  tools: [],
2115
2408
  resources: [],
2116
2409
  resourceTemplates: [],
@@ -2157,6 +2450,9 @@ export class MCPClient extends EventEmitter {
2157
2450
  }
2158
2451
  entry.httpUrl = config.url;
2159
2452
  entry.httpHeaders = connectionHeaders;
2453
+ entry._httpDispatcher = tlsMaterial
2454
+ ? _deps.createMcpTlsDispatcher(tlsMaterial)
2455
+ : null;
2160
2456
  } else if (isWebSocketTransport(transportKind)) {
2161
2457
  if (!config.url) {
2162
2458
  throw mcpTransportError(
@@ -2203,6 +2499,7 @@ export class MCPClient extends EventEmitter {
2203
2499
  ...(transportKind === "wss" && config.rejectUnauthorized === false
2204
2500
  ? { rejectUnauthorized: false }
2205
2501
  : {}),
2502
+ ...(tlsMaterial?.connectOptions || {}),
2206
2503
  });
2207
2504
  entry.socket = socket;
2208
2505
 
@@ -2218,6 +2515,7 @@ export class MCPClient extends EventEmitter {
2218
2515
  });
2219
2516
  entry._webSocketPayloadError = error;
2220
2517
  entry.state = ServerState.ERROR;
2518
+ this._failLifecycle(entry, error);
2221
2519
  rejectPending(error);
2222
2520
  this._cancelServerElicitations(name);
2223
2521
  this.emit("server-error", {
@@ -2242,6 +2540,7 @@ export class MCPClient extends EventEmitter {
2242
2540
  }
2243
2541
  entry._webSocketInboundError = error;
2244
2542
  entry.state = ServerState.ERROR;
2543
+ this._failLifecycle(entry, error);
2245
2544
  rejectPending(error);
2246
2545
  this._cancelServerElicitations(name);
2247
2546
  this.emit("server-error", {
@@ -2319,6 +2618,7 @@ export class MCPClient extends EventEmitter {
2319
2618
  { transport: transportKind, url: config.url },
2320
2619
  );
2321
2620
  entry.state = ServerState.ERROR;
2621
+ this._failLifecycle(entry, error);
2322
2622
  rejectPending(error);
2323
2623
  this._cancelServerElicitations(name);
2324
2624
  this.emit("server-error", {
@@ -2353,7 +2653,7 @@ export class MCPClient extends EventEmitter {
2353
2653
  ) {
2354
2654
  throw new Error("expected one JSON-RPC object");
2355
2655
  }
2356
- this._handleMessage(name, message);
2656
+ this._handleMessage(name, message, entry);
2357
2657
  } catch (cause) {
2358
2658
  if (isMcpJsonBudgetError(cause)) {
2359
2659
  recordInboundFailure(cause);
@@ -2365,6 +2665,7 @@ export class MCPClient extends EventEmitter {
2365
2665
  { transport: transportKind, url: config.url },
2366
2666
  );
2367
2667
  entry.state = ServerState.ERROR;
2668
+ this._failLifecycle(entry, error);
2368
2669
  rejectPending(error);
2369
2670
  this._cancelServerElicitations(name);
2370
2671
  this.emit("server-error", {
@@ -2386,6 +2687,7 @@ export class MCPClient extends EventEmitter {
2386
2687
  const inboundError = entry._webSocketInboundError;
2387
2688
  if (inboundError) {
2388
2689
  entry.state = ServerState.ERROR;
2690
+ this._failLifecycle(entry, inboundError);
2389
2691
  rejectPending(inboundError);
2390
2692
  this._cancelServerElicitations(name);
2391
2693
  this.emit("server-disconnected", {
@@ -2413,6 +2715,7 @@ export class MCPClient extends EventEmitter {
2413
2715
  }
2414
2716
  if (payloadError) {
2415
2717
  entry.state = ServerState.ERROR;
2718
+ this._failLifecycle(entry, payloadError);
2416
2719
  rejectPending(payloadError);
2417
2720
  this._cancelServerElicitations(name);
2418
2721
  this.emit("server-disconnected", {
@@ -2437,6 +2740,7 @@ export class MCPClient extends EventEmitter {
2437
2740
  outcomeUnknown: dispatched,
2438
2741
  },
2439
2742
  );
2743
+ this._failLifecycle(entry, error);
2440
2744
  rejectPending(error);
2441
2745
  this._cancelServerElicitations(name);
2442
2746
  this.emit("server-disconnected", {
@@ -2464,6 +2768,7 @@ export class MCPClient extends EventEmitter {
2464
2768
  outcomeUnknown: dispatched,
2465
2769
  },
2466
2770
  );
2771
+ this._failLifecycle(entry, error);
2467
2772
  rejectPending(error);
2468
2773
  this._cancelServerElicitations(name);
2469
2774
  this.emit("server-error", {
@@ -2579,6 +2884,7 @@ export class MCPClient extends EventEmitter {
2579
2884
  typeof data === "string"
2580
2885
  ? data
2581
2886
  : entry._decoder.decode(data, { stream: true }),
2887
+ entry,
2582
2888
  );
2583
2889
  });
2584
2890
 
@@ -2608,6 +2914,7 @@ export class MCPClient extends EventEmitter {
2608
2914
  if (!entry._stdioFrameError) {
2609
2915
  entry.state = ServerState.DISCONNECTED;
2610
2916
  }
2917
+ this._failLifecycle(entry, { code: "CC_MCP_STDIO_EXIT" });
2611
2918
  failPending(
2612
2919
  `MCP server "${name}" process exited (code ${code}) before responding`,
2613
2920
  );
@@ -2622,6 +2929,7 @@ export class MCPClient extends EventEmitter {
2622
2929
  if (cleanup?.verifiable && !cleanup.confirmed) {
2623
2930
  entry.state = ServerState.ERROR;
2624
2931
  const cleanupError = mcpStdioCleanupError(name, cleanup);
2932
+ this._failLifecycle(entry, cleanupError);
2625
2933
  this.emit("server-error", {
2626
2934
  name,
2627
2935
  error: cleanupError.message,
@@ -2638,6 +2946,7 @@ export class MCPClient extends EventEmitter {
2638
2946
 
2639
2947
  proc.on("error", (err) => {
2640
2948
  entry.state = ServerState.ERROR;
2949
+ this._failLifecycle(entry, err);
2641
2950
  failPending(`MCP server "${name}" process error: ${err.message}`);
2642
2951
  this._cancelServerElicitations(name);
2643
2952
  this.emit("server-error", { name, error: err.message });
@@ -2652,6 +2961,7 @@ export class MCPClient extends EventEmitter {
2652
2961
  if (proc.stdin && typeof proc.stdin.on === "function") {
2653
2962
  proc.stdin.on("error", (err) => {
2654
2963
  entry.state = ServerState.ERROR;
2964
+ this._failLifecycle(entry, err);
2655
2965
  failPending(`MCP server "${name}" stdin error: ${err.message}`);
2656
2966
  this._cancelServerElicitations(name);
2657
2967
  this.emit("server-error", { name, error: err.message });
@@ -2664,6 +2974,9 @@ export class MCPClient extends EventEmitter {
2664
2974
  }
2665
2975
 
2666
2976
  // Initialize MCP protocol (retried on transient network errors).
2977
+ this._transitionLifecycle(entry, MCP_LIFECYCLE_PHASES.INITIALIZING, {
2978
+ tlsIdentityDigest: entry._tlsIdentityDigest,
2979
+ });
2667
2980
  const initResult = await this._requestWithRetry(name, "initialize", {
2668
2981
  protocolVersion: this._protocolVersion,
2669
2982
  capabilities: {
@@ -2680,9 +2993,12 @@ export class MCPClient extends EventEmitter {
2680
2993
  clientInfo: { name: "chainlesschain-cli", version: "0.37.9" },
2681
2994
  });
2682
2995
 
2683
- entry.protocolVersion = String(
2684
- initResult?.protocolVersion || this._protocolVersion,
2996
+ entry.protocolVersion = assertSupportedMcpProtocolVersion(
2997
+ initResult?.protocolVersion,
2685
2998
  );
2999
+ this._transitionLifecycle(entry, MCP_LIFECYCLE_PHASES.DISCOVERING, {
3000
+ tlsIdentityDigest: entry._tlsIdentityDigest,
3001
+ });
2686
3002
  // Send initialized notification
2687
3003
  this._sendNotification(name, "notifications/initialized", {});
2688
3004
 
@@ -2697,62 +3013,150 @@ export class MCPClient extends EventEmitter {
2697
3013
  ? initResult.instructions
2698
3014
  : null;
2699
3015
 
2700
- // Fetch available tools. Per MCP a server advertises a `tools` capability
2701
- // in its initialize response; if it does and tools/list then fails, that
2702
- // is a genuine fetch failure we must surface (Claude-Code 2.1.181 — show
2703
- // "Connected · tools fetch failed" rather than a misleading "Tools: 0").
2704
- // A server that did not advertise tools simply has none, so a failure
2705
- // there is expected and stays quiet.
2706
- entry.tools = [];
2707
- entry.toolsError = null;
2708
- const advertisesTools =
2709
- entry.capabilities && entry.capabilities.tools !== undefined;
2710
- try {
2711
- const toolsResult = await this._requestWithRetry(
3016
+ const cachePolicy = discoveryCachePolicy(entry.config);
3017
+ const cacheKey = discoveryCacheKey(name, entry);
3018
+ const cached = cachePolicy.enabled
3019
+ ? this._discoveryCache.get(cacheKey) || null
3020
+ : null;
3021
+ const now = Date.now();
3022
+ const capabilitiesSignature = discoveryCapabilitiesSignature(
3023
+ entry.capabilities,
3024
+ );
3025
+ const cacheCompatible =
3026
+ cached?.capabilitiesSignature === capabilitiesSignature;
3027
+ const cacheAgeMs = cacheCompatible
3028
+ ? Math.max(0, now - cached.savedAt)
3029
+ : 0;
3030
+ entry._discoveryCacheKey = cachePolicy.enabled ? cacheKey : null;
3031
+
3032
+ if (cacheCompatible && cacheAgeMs <= cachePolicy.ttlMs) {
3033
+ this._replaceToolInventory(name, entry, cached.tools);
3034
+ applyDiscoveryCache(entry, cached);
3035
+ entry.discoveryCache = { state: "fresh", ageMs: cacheAgeMs };
3036
+ this.emit("server-discovery-cache", {
2712
3037
  name,
2713
- "tools/list",
2714
- {},
2715
- );
2716
- this._replaceToolInventory(name, entry, toolsResult?.tools || []);
2717
- } catch (err) {
2718
- if (advertisesTools) {
2719
- entry.toolsError = err?.message || String(err);
3038
+ state: "fresh",
3039
+ ageMs: cacheAgeMs,
3040
+ });
3041
+ } else {
3042
+ // Fetch available tools. Per MCP a server advertises a `tools`
3043
+ // capability in its initialize response; if it does and tools/list
3044
+ // fails, that is a genuine fetch failure we must surface. A server that
3045
+ // did not advertise tools simply has none, so a failure stays quiet.
3046
+ entry.tools = [];
3047
+ entry.toolsError = null;
3048
+ const advertisesTools =
3049
+ entry.capabilities && entry.capabilities.tools !== undefined;
3050
+ let toolsDiscoveryFailed = false;
3051
+ try {
3052
+ const toolsResult = await this._requestWithRetry(
3053
+ name,
3054
+ "tools/list",
3055
+ {},
3056
+ );
3057
+ this._replaceToolInventory(name, entry, toolsResult?.tools || []);
3058
+ } catch (err) {
3059
+ if (isMcpFatalDiscoveryError(err)) throw err;
3060
+ if (advertisesTools) {
3061
+ toolsDiscoveryFailed = true;
3062
+ entry.toolsError = err?.message || String(err);
3063
+ }
3064
+ // else: server did not advertise tools — legitimately none.
2720
3065
  }
2721
- // else: server did not advertise tools — legitimately none.
2722
- }
2723
3066
 
2724
- // Fetch available resources
2725
- try {
2726
- const resourcesResult = await this._sendRequest(
2727
- name,
2728
- "resources/list",
2729
- {},
2730
- );
2731
- entry.resources = resourcesResult?.resources || [];
2732
- } catch {
2733
- // Server may not support resources
2734
- }
3067
+ // Fetch available resources
3068
+ try {
3069
+ const resourcesResult = await this._sendRequest(
3070
+ name,
3071
+ "resources/list",
3072
+ {},
3073
+ );
3074
+ entry.resources = resourcesResult?.resources || [];
3075
+ } catch (error) {
3076
+ if (isMcpFatalDiscoveryError(error)) throw error;
3077
+ // Server may not support resources
3078
+ }
2735
3079
 
2736
- // Resource templates are optional even when resources are supported.
2737
- try {
2738
- const templatesResult = await this._sendRequest(
2739
- name,
2740
- "resources/templates/list",
2741
- {},
2742
- );
2743
- entry.resourceTemplates = templatesResult?.resourceTemplates || [];
2744
- } catch {
2745
- // Server may not support resource templates.
3080
+ // Resource templates are optional even when resources are supported.
3081
+ try {
3082
+ const templatesResult = await this._sendRequest(
3083
+ name,
3084
+ "resources/templates/list",
3085
+ {},
3086
+ );
3087
+ entry.resourceTemplates = templatesResult?.resourceTemplates || [];
3088
+ } catch (error) {
3089
+ if (isMcpFatalDiscoveryError(error)) throw error;
3090
+ // Server may not support resource templates.
3091
+ }
3092
+
3093
+ // Fetch available prompts (server-provided slash commands)
3094
+ try {
3095
+ const promptsResult = await this._sendRequest(
3096
+ name,
3097
+ "prompts/list",
3098
+ {},
3099
+ );
3100
+ entry.prompts = promptsResult?.prompts || [];
3101
+ } catch (error) {
3102
+ if (isMcpFatalDiscoveryError(error)) throw error;
3103
+ // Server may not support prompts
3104
+ }
3105
+
3106
+ const staleUsable =
3107
+ cacheCompatible &&
3108
+ cacheAgeMs <= cachePolicy.maxStaleMs &&
3109
+ cached.strikes < cachePolicy.maxStrikes;
3110
+ if (toolsDiscoveryFailed && staleUsable) {
3111
+ cached.strikes += 1;
3112
+ cached.lastFailureAt = now;
3113
+ this._replaceToolInventory(name, entry, cached.tools);
3114
+ applyDiscoveryCache(entry, cached);
3115
+ entry.discoveryCache = {
3116
+ state: "stale",
3117
+ ageMs: cacheAgeMs,
3118
+ strikes: cached.strikes,
3119
+ };
3120
+ this.emit("server-discovery-cache", {
3121
+ name,
3122
+ state: "stale",
3123
+ ageMs: cacheAgeMs,
3124
+ strikes: cached.strikes,
3125
+ });
3126
+ } else if (cachePolicy.enabled && !toolsDiscoveryFailed) {
3127
+ this._discoveryCache.set(cacheKey, {
3128
+ savedAt: now,
3129
+ strikes: 0,
3130
+ capabilitiesSignature,
3131
+ tools: cloneDiscoveryList(entry.tools),
3132
+ resources: cloneDiscoveryList(entry.resources),
3133
+ resourceTemplates: cloneDiscoveryList(entry.resourceTemplates),
3134
+ prompts: cloneDiscoveryList(entry.prompts),
3135
+ });
3136
+ entry.discoveryCache = { state: "miss", ageMs: null };
3137
+ }
2746
3138
  }
2747
3139
 
2748
- // Fetch available prompts (server-provided slash commands)
2749
- try {
2750
- const promptsResult = await this._sendRequest(name, "prompts/list", {});
2751
- entry.prompts = promptsResult?.prompts || [];
2752
- } catch {
2753
- // Server may not support prompts
3140
+ // Desired v2 subscriptions belong to the logical authority, not a
3141
+ // transport object. Restore them after initialize + capability discovery
3142
+ // so both hot reconnect and process restart preserve live updates.
3143
+ for (const uri of this._lifecycleAuthority.desiredSubscriptions(
3144
+ lifecycleAttempt,
3145
+ )) {
3146
+ await this._sendRequest(name, "resources/subscribe", { uri });
3147
+ entry.resourceSubscriptions.add(uri);
3148
+ let desired = this._resourceSubscriptions.get(name);
3149
+ if (!desired) {
3150
+ desired = new Set();
3151
+ this._resourceSubscriptions.set(name, desired);
3152
+ }
3153
+ desired.add(uri);
2754
3154
  }
2755
3155
 
3156
+ this._transitionLifecycle(entry, MCP_LIFECYCLE_PHASES.READY, {
3157
+ tlsIdentityDigest: entry._tlsIdentityDigest,
3158
+ });
3159
+
2756
3160
  this.emit("server-connected", {
2757
3161
  name,
2758
3162
  tools: entry.tools.length,
@@ -2777,6 +3181,16 @@ export class MCPClient extends EventEmitter {
2777
3181
  };
2778
3182
  } catch (err) {
2779
3183
  entry.state = ServerState.ERROR;
3184
+ try {
3185
+ this._transitionLifecycle(
3186
+ entry,
3187
+ MCP_LIFECYCLE_PHASES.FAILED,
3188
+ { reasonCode: err?.code || "CC_MCP_CONNECTION_FAILED" },
3189
+ { tolerateFence: true },
3190
+ );
3191
+ } catch {
3192
+ // The original transport/protocol failure remains authoritative.
3193
+ }
2780
3194
  this._cancelServerElicitations(name);
2781
3195
  // The initialize notification is fire-and-forget. If later capability
2782
3196
  // discovery fails, connect() deletes this entry without going through
@@ -2831,17 +3245,26 @@ export class MCPClient extends EventEmitter {
2831
3245
  // teardown is best-effort
2832
3246
  }
2833
3247
  }
3248
+ try {
3249
+ await _deps.closeMcpTlsDispatcher(entry._httpDispatcher);
3250
+ } catch {
3251
+ // Preserve the connection failure; dispatcher cleanup is best-effort.
3252
+ }
3253
+ entry._httpDispatcher = null;
2834
3254
  this._releaseToolInventory(entry);
2835
3255
  this.servers.delete(name);
2836
3256
  if (
2837
- !_authRetryUsed &&
3257
+ !authRetryUsed &&
2838
3258
  sourceConfig.headersHelper &&
2839
3259
  isMcpAuthenticationError(err)
2840
3260
  ) {
2841
3261
  // A helper is intentionally uncached. One authentication rejection may
2842
3262
  // mean its short-lived token expired between generation and initialize;
2843
3263
  // execute it once more and retry the connection exactly once.
2844
- return this.connect(name, sourceConfig, true);
3264
+ return this.connect(name, sourceConfig, {
3265
+ authRetryUsed: true,
3266
+ reconnect: true,
3267
+ });
2845
3268
  }
2846
3269
  throw err;
2847
3270
  }
@@ -2850,18 +3273,78 @@ export class MCPClient extends EventEmitter {
2850
3273
  /**
2851
3274
  * Disconnect from an MCP server.
2852
3275
  */
2853
- disconnect(name) {
3276
+ disconnect(name, options = {}) {
3277
+ if (options.preserveResourceSubscriptions !== true) {
3278
+ this._resourceSubscriptions.delete(name);
3279
+ }
2854
3280
  const entry = this.servers.get(name);
3281
+ const lifecycleAttempt =
3282
+ entry?._lifecycleAttempt || this._lifecycleAttempts.get(name) || null;
2855
3283
  if (!entry) {
2856
3284
  this._cancelServerElicitations(name);
3285
+ if (options.preserveResourceSubscriptions !== true && lifecycleAttempt) {
3286
+ try {
3287
+ this._lifecycleAuthority.clearSubscriptions(lifecycleAttempt);
3288
+ const snapshot = this._lifecycleAuthority.snapshot({
3289
+ name,
3290
+ sessionId: this._lifecycleSessionId,
3291
+ });
3292
+ if (
3293
+ snapshot &&
3294
+ snapshot.phase !== MCP_LIFECYCLE_PHASES.IDLE &&
3295
+ snapshot.phase !== MCP_LIFECYCLE_PHASES.DISABLED
3296
+ ) {
3297
+ this._lifecycleAuthority.transition(
3298
+ lifecycleAttempt,
3299
+ MCP_LIFECYCLE_PHASES.DISCONNECTING,
3300
+ );
3301
+ this._lifecycleAuthority.transition(
3302
+ lifecycleAttempt,
3303
+ MCP_LIFECYCLE_PHASES.IDLE,
3304
+ );
3305
+ }
3306
+ } catch (error) {
3307
+ if (error?.code !== MCP_LIFECYCLE_ERROR_CODES.FENCED) {
3308
+ return Promise.reject(error);
3309
+ }
3310
+ } finally {
3311
+ if (this._lifecycleAttempts.get(name) === lifecycleAttempt) {
3312
+ this._lifecycleAttempts.delete(name);
3313
+ }
3314
+ }
3315
+ }
2857
3316
  return Promise.resolve(false);
2858
3317
  }
2859
3318
  if (entry._disconnectPromise) return entry._disconnectPromise;
2860
3319
 
3320
+ let subscriptionClearError = null;
3321
+ if (options.preserveResourceSubscriptions !== true && lifecycleAttempt) {
3322
+ try {
3323
+ this._lifecycleAuthority.clearSubscriptions(lifecycleAttempt);
3324
+ } catch (error) {
3325
+ if (error?.code !== MCP_LIFECYCLE_ERROR_CODES.FENCED) {
3326
+ subscriptionClearError = error;
3327
+ }
3328
+ }
3329
+ }
3330
+
2861
3331
  // Make disconnect visible before any teardown awaits. In particular, an
2862
3332
  // HTTP session DELETE may wait for response headers; no new tool/resource
2863
3333
  // request may enter the transport during that window or trigger a hot
2864
3334
  // reconnect that resurrects a server the caller is intentionally closing.
3335
+ if (options.preserveLifecycleAuthority !== true) {
3336
+ try {
3337
+ this._transitionLifecycle(
3338
+ entry,
3339
+ MCP_LIFECYCLE_PHASES.DISCONNECTING,
3340
+ {},
3341
+ { tolerateFence: true },
3342
+ );
3343
+ } catch {
3344
+ // Local transport teardown must still run if durable state is
3345
+ // temporarily unavailable. No further request is admitted below.
3346
+ }
3347
+ }
2865
3348
  entry.state = ServerState.DISCONNECTED;
2866
3349
  entry._httpDiscardStopping = true;
2867
3350
  this._cancelServerElicitations(name);
@@ -2904,14 +3387,40 @@ export class MCPClient extends EventEmitter {
2904
3387
  // ignore — disconnect is best-effort
2905
3388
  }
2906
3389
  }
2907
- return true;
2908
3390
  } finally {
3391
+ try {
3392
+ await _deps.closeMcpTlsDispatcher(entry._httpDispatcher);
3393
+ } catch {
3394
+ // Transport teardown remains best-effort.
3395
+ }
3396
+ entry._httpDispatcher = null;
2909
3397
  entry.state = ServerState.DISCONNECTED;
3398
+ if (options.preserveLifecycleAuthority !== true) {
3399
+ try {
3400
+ this._transitionLifecycle(
3401
+ entry,
3402
+ MCP_LIFECYCLE_PHASES.IDLE,
3403
+ {},
3404
+ { tolerateFence: true },
3405
+ );
3406
+ } catch {
3407
+ // The transport is already closed; retain the durable failure for
3408
+ // the next generation's restart recovery.
3409
+ }
3410
+ }
2910
3411
  this._releaseToolInventory(entry);
2911
3412
  // A future connection may reuse the same name. Never let a delayed
2912
3413
  // teardown delete that replacement entry.
2913
3414
  if (this.servers.get(name) === entry) this.servers.delete(name);
3415
+ if (
3416
+ options.preserveResourceSubscriptions !== true &&
3417
+ this._lifecycleAttempts.get(name) === lifecycleAttempt
3418
+ ) {
3419
+ this._lifecycleAttempts.delete(name);
3420
+ }
2914
3421
  }
3422
+ if (subscriptionClearError) throw subscriptionClearError;
3423
+ return true;
2915
3424
  });
2916
3425
  entry._disconnectPromise = disconnecting;
2917
3426
 
@@ -2927,9 +3436,19 @@ export class MCPClient extends EventEmitter {
2927
3436
  * Disconnect from all servers.
2928
3437
  */
2929
3438
  async disconnectAll() {
2930
- const names = [...this.servers.keys()];
2931
- for (const name of names) {
2932
- await this.disconnect(name);
3439
+ const names = new Set([
3440
+ ...this.servers.keys(),
3441
+ ...this._lifecycleAttempts.keys(),
3442
+ ]);
3443
+ try {
3444
+ for (const name of names) {
3445
+ await this.disconnect(name);
3446
+ }
3447
+ } finally {
3448
+ // A failed reconnect may have already removed its transport entry while
3449
+ // retaining desired subscriptions for another retry. disconnectAll()
3450
+ // is the explicit terminal teardown and must clear those tombstones too.
3451
+ this._resourceSubscriptions.clear();
2933
3452
  }
2934
3453
  }
2935
3454
 
@@ -3146,15 +3665,58 @@ export class MCPClient extends EventEmitter {
3146
3665
  throw new Error(`Server "${serverName}" is not connected`);
3147
3666
  }
3148
3667
 
3149
- const result = await this._sendRequest(
3150
- serverName,
3151
- "tools/call",
3152
- {
3153
- name: toolName,
3154
- arguments: args,
3155
- },
3156
- options,
3157
- );
3668
+ const timeoutMs = mcpToolIdleTimeoutMs(entry.config, options);
3669
+ const progressToken = `cc:${randomUUID()}`;
3670
+ const controller = new AbortController();
3671
+ let idleError = null;
3672
+ let idleTimer = null;
3673
+ let removeCallerAbort = null;
3674
+ const resetIdleTimer = () => {
3675
+ if (idleTimer) clearTimeout(idleTimer);
3676
+ idleTimer = setTimeout(() => {
3677
+ idleError = mcpToolIdleTimeoutError(serverName, toolName, timeoutMs);
3678
+ controller.abort();
3679
+ }, timeoutMs);
3680
+ idleTimer.unref?.();
3681
+ };
3682
+ const callerSignal = options?.signal || null;
3683
+ if (callerSignal?.addEventListener) {
3684
+ const abortFromCaller = () => controller.abort();
3685
+ callerSignal.addEventListener("abort", abortFromCaller, { once: true });
3686
+ removeCallerAbort = () =>
3687
+ callerSignal.removeEventListener?.("abort", abortFromCaller);
3688
+ if (callerSignal.aborted) controller.abort();
3689
+ }
3690
+ this._toolProgress.set(progressToken, resetIdleTimer);
3691
+ resetIdleTimer();
3692
+
3693
+ let result;
3694
+ try {
3695
+ result = await this._sendRequest(
3696
+ serverName,
3697
+ "tools/call",
3698
+ {
3699
+ name: toolName,
3700
+ arguments: args,
3701
+ _meta: { progressToken },
3702
+ },
3703
+ { ...options, signal: controller.signal },
3704
+ );
3705
+ } catch (error) {
3706
+ // The inner transport supplies cancellation and JSON-RPC
3707
+ // notifications/cancelled. Preserve the idle classification at the host
3708
+ // boundary instead of leaking its generic AbortError/cancelled detail.
3709
+ if (idleError) throw idleError;
3710
+ throw error;
3711
+ } finally {
3712
+ if (idleTimer) clearTimeout(idleTimer);
3713
+ try {
3714
+ removeCallerAbort?.();
3715
+ } catch {
3716
+ // Best-effort cleanup for non-native AbortSignal adapters.
3717
+ }
3718
+ this._toolProgress.delete(progressToken);
3719
+ }
3158
3720
 
3159
3721
  return admitMcpToolResult(serverName, result, entry.config).result;
3160
3722
  }
@@ -3173,11 +3735,32 @@ export class MCPClient extends EventEmitter {
3173
3735
  try {
3174
3736
  const currentEntry = this.servers.get(name) || null;
3175
3737
  const currentConfig = currentEntry?.config || null;
3738
+ // Resource subscriptions belong to the logical client/server
3739
+ // relationship, not to one transport connection. Snapshot them before
3740
+ // disconnect() removes the old entry so a hot reconnect cannot
3741
+ // silently stop delivering resource update notifications.
3742
+ const resourceSubscriptions = new Set([
3743
+ ...(this._resourceSubscriptions.get(name) || []),
3744
+ ...(currentEntry?.resourceSubscriptions || []),
3745
+ ]);
3746
+ if (resourceSubscriptions.size > 0) {
3747
+ this._resourceSubscriptions.set(name, new Set(resourceSubscriptions));
3748
+ }
3176
3749
  const resolver = this._reconnectors.get(name);
3177
3750
  const fresh = resolver ? await resolver() : currentConfig;
3178
3751
  if (!fresh) return false;
3752
+ const lifecycleAttempt = this._lifecycleAuthority.beginConnection({
3753
+ name,
3754
+ sessionId: this._lifecycleSessionId,
3755
+ config: fresh,
3756
+ reconnect: true,
3757
+ explicitEnable: true,
3758
+ });
3179
3759
  try {
3180
- await this.disconnect(name);
3760
+ await this.disconnect(name, {
3761
+ preserveResourceSubscriptions: true,
3762
+ preserveLifecycleAuthority: true,
3763
+ });
3181
3764
  } catch {
3182
3765
  // entry may already be gone — connect() below is what matters
3183
3766
  }
@@ -3190,11 +3773,32 @@ export class MCPClient extends EventEmitter {
3190
3773
  { serverName: name, config: reconnectConfig },
3191
3774
  );
3192
3775
  }
3193
- await this.connect(
3194
- name,
3195
- reconnectConfig,
3196
- options.connectAuthRetryUsed === true,
3197
- );
3776
+ await this.connect(name, reconnectConfig, {
3777
+ authRetryUsed: options.connectAuthRetryUsed === true,
3778
+ lifecycleAttempt,
3779
+ reconnect: true,
3780
+ });
3781
+ try {
3782
+ for (const uri of resourceSubscriptions) {
3783
+ if (!this.servers.get(name)?.resourceSubscriptions?.has(uri)) {
3784
+ await this.subscribeResource(name, uri);
3785
+ }
3786
+ }
3787
+ } catch {
3788
+ // A partially restored connection is unsafe: callers would believe
3789
+ // the reconnect recovered their live subscriptions while some were
3790
+ // already lost. Tear it down and let the original operation surface
3791
+ // its connection error.
3792
+ try {
3793
+ await this.disconnect(name, {
3794
+ preserveResourceSubscriptions: true,
3795
+ });
3796
+ } catch {
3797
+ // The outer reconnect result remains fail-closed even if cleanup
3798
+ // itself reports an error.
3799
+ }
3800
+ return false;
3801
+ }
3198
3802
  this.emit("server-reconnected", { name, url: fresh.url || null });
3199
3803
  return true;
3200
3804
  } catch {
@@ -3269,9 +3873,30 @@ export class MCPClient extends EventEmitter {
3269
3873
  async subscribeResource(serverName, uri) {
3270
3874
  const entry = this.servers.get(serverName);
3271
3875
  if (!entry) throw new Error(`Server "${serverName}" not found`);
3272
- await this._sendRequest(serverName, "resources/subscribe", { uri });
3876
+ if (typeof uri !== "string" || !uri.trim()) {
3877
+ throw new TypeError(
3878
+ "MCP resource subscription URI must be a non-empty string",
3879
+ );
3880
+ }
3881
+ const normalizedUri = uri;
3882
+ await this._sendRequest(serverName, "resources/subscribe", {
3883
+ uri: normalizedUri,
3884
+ });
3273
3885
  entry.resourceSubscriptions ||= new Set();
3274
- entry.resourceSubscriptions.add(String(uri));
3886
+ entry.resourceSubscriptions.add(normalizedUri);
3887
+ let desired = this._resourceSubscriptions.get(serverName);
3888
+ if (!desired) {
3889
+ desired = new Set();
3890
+ this._resourceSubscriptions.set(serverName, desired);
3891
+ }
3892
+ desired.add(normalizedUri);
3893
+ if (entry._lifecycleAttempt) {
3894
+ this._lifecycleAuthority.setSubscription(
3895
+ entry._lifecycleAttempt,
3896
+ normalizedUri,
3897
+ true,
3898
+ );
3899
+ }
3275
3900
  return true;
3276
3901
  }
3277
3902
 
@@ -3279,8 +3904,26 @@ export class MCPClient extends EventEmitter {
3279
3904
  async unsubscribeResource(serverName, uri) {
3280
3905
  const entry = this.servers.get(serverName);
3281
3906
  if (!entry) throw new Error(`Server "${serverName}" not found`);
3282
- await this._sendRequest(serverName, "resources/unsubscribe", { uri });
3283
- entry.resourceSubscriptions?.delete(String(uri));
3907
+ if (typeof uri !== "string" || !uri.trim()) {
3908
+ throw new TypeError(
3909
+ "MCP resource subscription URI must be a non-empty string",
3910
+ );
3911
+ }
3912
+ const normalizedUri = uri;
3913
+ await this._sendRequest(serverName, "resources/unsubscribe", {
3914
+ uri: normalizedUri,
3915
+ });
3916
+ entry.resourceSubscriptions?.delete(normalizedUri);
3917
+ const desired = this._resourceSubscriptions.get(serverName);
3918
+ desired?.delete(normalizedUri);
3919
+ if (desired?.size === 0) this._resourceSubscriptions.delete(serverName);
3920
+ if (entry._lifecycleAttempt) {
3921
+ this._lifecycleAuthority.setSubscription(
3922
+ entry._lifecycleAttempt,
3923
+ normalizedUri,
3924
+ false,
3925
+ );
3926
+ }
3284
3927
  return true;
3285
3928
  }
3286
3929
 
@@ -3413,6 +4056,7 @@ export class MCPClient extends EventEmitter {
3413
4056
  method,
3414
4057
  params,
3415
4058
  });
4059
+ this._registerLifecycleRpc(entry, id, method);
3416
4060
 
3417
4061
  let timeout = null;
3418
4062
  let removeCallerAbort = null;
@@ -3431,6 +4075,20 @@ export class MCPClient extends EventEmitter {
3431
4075
  if (settled) return false;
3432
4076
  settled = true;
3433
4077
  cleanup();
4078
+ try {
4079
+ const admitted = this._settleLifecycleRpc(
4080
+ entry,
4081
+ id,
4082
+ settler === resolve ? "completed" : "failed-closed",
4083
+ );
4084
+ if (!admitted && settler === resolve) {
4085
+ reject(this._staleLifecycleCallbackError());
4086
+ return true;
4087
+ }
4088
+ } catch (authorityError) {
4089
+ reject(authorityError);
4090
+ return true;
4091
+ }
3434
4092
  settler(value);
3435
4093
  return true;
3436
4094
  };
@@ -3534,6 +4192,7 @@ export class MCPClient extends EventEmitter {
3534
4192
  return new Promise((resolve, reject) => {
3535
4193
  const id = this._nextId++;
3536
4194
  const message = JSON.stringify({ jsonrpc: "2.0", id, method, params });
4195
+ this._registerLifecycleRpc(entry, id, method);
3537
4196
  const longRunning = Boolean(entry.config?.longRunning);
3538
4197
  const timeoutMs = Number.isFinite(entry.config?.requestTimeoutMs)
3539
4198
  ? entry.config.requestTimeoutMs
@@ -3555,6 +4214,20 @@ export class MCPClient extends EventEmitter {
3555
4214
  if (settled) return false;
3556
4215
  settled = true;
3557
4216
  cleanup();
4217
+ try {
4218
+ const admitted = this._settleLifecycleRpc(
4219
+ entry,
4220
+ id,
4221
+ settler === resolve ? "completed" : "failed-closed",
4222
+ );
4223
+ if (!admitted && settler === resolve) {
4224
+ reject(this._staleLifecycleCallbackError());
4225
+ return true;
4226
+ }
4227
+ } catch (authorityError) {
4228
+ reject(authorityError);
4229
+ return true;
4230
+ }
3558
4231
  settler(value);
3559
4232
  return true;
3560
4233
  };
@@ -3752,6 +4425,9 @@ export class MCPClient extends EventEmitter {
3752
4425
  ...(stream.controller
3753
4426
  ? { signal: stream.controller.signal }
3754
4427
  : {}),
4428
+ ...(entry._httpDispatcher
4429
+ ? { dispatcher: entry._httpDispatcher }
4430
+ : {}),
3755
4431
  }),
3756
4432
  stream.controller?.signal,
3757
4433
  );
@@ -3796,7 +4472,8 @@ export class MCPClient extends EventEmitter {
3796
4472
  if (error) throw error;
3797
4473
  },
3798
4474
  parseJson: (text) => parseMcpJson(text, serverName, entry),
3799
- onMessage: (message) => this._handleMessage(serverName, message),
4475
+ onMessage: (message) =>
4476
+ this._handleMessage(serverName, message, entry),
3800
4477
  });
3801
4478
  } catch (error) {
3802
4479
  if (stream.stopped) return;
@@ -3879,6 +4556,14 @@ export class MCPClient extends EventEmitter {
3879
4556
 
3880
4557
  const id = this._nextId++;
3881
4558
  const body = JSON.stringify({ jsonrpc: "2.0", id, method, params });
4559
+ this._registerLifecycleRpc(entry, id, method);
4560
+ let lifecycleSettled = false;
4561
+ const settleLifecycle = (outcome) => {
4562
+ if (lifecycleSettled) return true;
4563
+ const admitted = this._settleLifecycleRpc(entry, id, outcome);
4564
+ lifecycleSettled = true;
4565
+ return admitted;
4566
+ };
3882
4567
  const headers = {
3883
4568
  "Content-Type": "application/json",
3884
4569
  Accept: "application/json, text/event-stream",
@@ -3997,6 +4682,9 @@ export class MCPClient extends EventEmitter {
3997
4682
  headers,
3998
4683
  body,
3999
4684
  ...(controller ? { signal: controller.signal } : {}),
4685
+ ...(entry._httpDispatcher
4686
+ ? { dispatcher: entry._httpDispatcher }
4687
+ : {}),
4000
4688
  });
4001
4689
  },
4002
4690
  );
@@ -4080,7 +4768,7 @@ export class MCPClient extends EventEmitter {
4080
4768
  response,
4081
4769
  id,
4082
4770
  cap,
4083
- (message) => this._handleMessage(serverName, message),
4771
+ (message) => this._handleMessage(serverName, message, entry),
4084
4772
  controller?.signal,
4085
4773
  (text) => parseMcpJson(text, serverName, entry),
4086
4774
  );
@@ -4113,9 +4801,18 @@ export class MCPClient extends EventEmitter {
4113
4801
  throw mcpRpcError(wire.error);
4114
4802
  }
4115
4803
  throwIfAborted();
4804
+ if (!settleLifecycle("completed")) {
4805
+ throw this._staleLifecycleCallbackError();
4806
+ }
4116
4807
  return wire.result;
4117
4808
  } catch (err) {
4118
- throwIfAborted();
4809
+ try {
4810
+ throwIfAborted();
4811
+ } catch (abortError) {
4812
+ settleLifecycle("failed-closed");
4813
+ throw abortError;
4814
+ }
4815
+ settleLifecycle("failed-closed");
4119
4816
  throw err;
4120
4817
  } finally {
4121
4818
  if (timer) clearTimeout(timer);
@@ -4156,8 +4853,9 @@ export class MCPClient extends EventEmitter {
4156
4853
  });
4157
4854
  }
4158
4855
 
4159
- _handleData(serverName, data) {
4160
- const entry = this.servers.get(serverName);
4856
+ _handleData(serverName, data, sourceEntry = null) {
4857
+ const entry = sourceEntry || this.servers.get(serverName);
4858
+ if (sourceEntry && this.servers.get(serverName) !== sourceEntry) return;
4161
4859
  if (!entry || entry._stdioFrameError) return;
4162
4860
 
4163
4861
  const cap = stdioFrameByteLimit(entry.config?.maxBufferChars);
@@ -4216,7 +4914,7 @@ export class MCPClient extends EventEmitter {
4216
4914
  }
4217
4915
  continue;
4218
4916
  }
4219
- this._handleMessage(serverName, msg);
4917
+ this._handleMessage(serverName, msg, entry);
4220
4918
  }
4221
4919
  }
4222
4920
 
@@ -4312,6 +5010,7 @@ export class MCPClient extends EventEmitter {
4312
5010
  entry._buffer = "";
4313
5011
  entry._bufferBytes = 0;
4314
5012
  entry.state = ServerState.ERROR;
5013
+ this._failLifecycle(entry, error);
4315
5014
  if (entry._pending instanceof Map) {
4316
5015
  for (const pending of entry._pending.values()) {
4317
5016
  if (pending.timeout) clearTimeout(pending.timeout);
@@ -4345,12 +5044,21 @@ export class MCPClient extends EventEmitter {
4345
5044
  return error;
4346
5045
  }
4347
5046
 
4348
- _handleMessage(serverName, msg) {
4349
- const entry = this.servers.get(serverName);
5047
+ _handleMessage(serverName, msg, sourceEntry = null) {
5048
+ const currentEntry = this.servers.get(serverName);
5049
+ const entry = sourceEntry || currentEntry;
4350
5050
  if (!entry) return;
5051
+ if (sourceEntry && currentEntry !== sourceEntry) {
5052
+ const wire = snapshotRpcMessage(msg);
5053
+ if (wire?.hasId && !wire.hasMethod) {
5054
+ this._rejectUnexpectedLifecycleRpc(sourceEntry, wire.id, "stale");
5055
+ }
5056
+ return;
5057
+ }
4351
5058
 
4352
5059
  const failMessage = (error) => {
4353
5060
  entry.state = ServerState.ERROR;
5061
+ this._failLifecycle(entry, error);
4354
5062
  if (entry._pending instanceof Map) {
4355
5063
  for (const pending of entry._pending.values()) {
4356
5064
  clearTimeout(pending.timeout);
@@ -4419,6 +5127,13 @@ export class MCPClient extends EventEmitter {
4419
5127
  // one of ours). Previously these fell into the notification branch and
4420
5128
  // never got a response, so a server calling e.g. roots/list hung until its
4421
5129
  // own timeout. Answer the ones our advertised capabilities invite.
5130
+ // A response without a live pending request is never delivered. The
5131
+ // durable authority records whether it was duplicate or uncorrelated.
5132
+ if (wire.hasId && !wire.hasMethod) {
5133
+ this._rejectUnexpectedLifecycleRpc(entry, wire.id, "unmatched");
5134
+ return;
5135
+ }
5136
+
4422
5137
  if (wire.hasId && typeof wire.method === "string" && wire.method) {
4423
5138
  if (
4424
5139
  wire.hasParams &&
@@ -4451,10 +5166,35 @@ export class MCPClient extends EventEmitter {
4451
5166
  // tool-search's prompt-cache stability depends on an append-only,
4452
5167
  // stable-prefix tool list.)
4453
5168
  if (wire.method === "notifications/tools/list_changed") {
5169
+ if (entry._discoveryCacheKey) {
5170
+ this._discoveryCache.delete(entry._discoveryCacheKey);
5171
+ }
4454
5172
  this._refreshServerList(serverName, "tools");
4455
5173
  } else if (wire.method === "notifications/resources/list_changed") {
5174
+ if (entry._discoveryCacheKey) {
5175
+ this._discoveryCache.delete(entry._discoveryCacheKey);
5176
+ }
4456
5177
  this._refreshServerList(serverName, "resources");
4457
5178
  this._refreshServerList(serverName, "resourceTemplates");
5179
+ } else if (wire.method === "notifications/progress") {
5180
+ const progressToken = safeRpcProperty(wire.params, "progressToken");
5181
+ const resetIdle =
5182
+ typeof progressToken === "string"
5183
+ ? this._toolProgress.get(progressToken)
5184
+ : null;
5185
+ if (resetIdle) resetIdle();
5186
+ // Project only numeric progress. Server-controlled status text belongs
5187
+ // to the tool result, not host diagnostics/events.
5188
+ this.emit("tool-progress", {
5189
+ server: serverName,
5190
+ ...(typeof progressToken === "string" ? { progressToken } : {}),
5191
+ ...(Number.isFinite(safeRpcProperty(wire.params, "progress"))
5192
+ ? { progress: safeRpcProperty(wire.params, "progress") }
5193
+ : {}),
5194
+ ...(Number.isFinite(safeRpcProperty(wire.params, "total"))
5195
+ ? { total: safeRpcProperty(wire.params, "total") }
5196
+ : {}),
5197
+ });
4458
5198
  } else if (wire.method === "notifications/resources/updated") {
4459
5199
  this.emit("resource-updated", {
4460
5200
  server: serverName,
@@ -4729,6 +5469,7 @@ async function _fetchAndDiscardHttpResponse(entry, options) {
4729
5469
  const response = await _deps.fetch(entry.httpUrl, {
4730
5470
  ...options,
4731
5471
  ...(controller ? { signal: controller.signal } : {}),
5472
+ ...(entry._httpDispatcher ? { dispatcher: entry._httpDispatcher } : {}),
4732
5473
  });
4733
5474
  _cancelHttpResponseBody(response);
4734
5475
  if (timedOut) {