sliccy 6.137.0 → 6.138.0

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 (238) hide show
  1. package/dist/node-server/cdp-proxy/chrome-reconnect.d.ts +141 -0
  2. package/dist/node-server/cdp-proxy/chrome-reconnect.js +206 -0
  3. package/dist/node-server/cdp-proxy/client-frame-buffer.d.ts +118 -0
  4. package/dist/node-server/cdp-proxy/client-frame-buffer.js +141 -0
  5. package/dist/node-server/cdp-proxy/close-codes.d.ts +28 -0
  6. package/dist/node-server/cdp-proxy/close-codes.js +28 -0
  7. package/dist/node-server/index.js +128 -28
  8. package/dist/ui/.vite/manifest.json +359 -624
  9. package/dist/ui/assets/SKILL-Cj8ElTxg.js +1 -0
  10. package/dist/ui/assets/{SKILL-AGJ6NFW_.js → SKILL-DfOG7jCj.js} +22 -19
  11. package/dist/ui/assets/account-store-CEzlUstH.js +63 -0
  12. package/dist/ui/assets/account-store-DD8DCyBK.js +1 -0
  13. package/dist/ui/assets/{adobe-jqcr8RHN.js → adobe-Cdd_E6rB.js} +1 -1
  14. package/dist/ui/assets/adobe-DhfJgCHq.js +2 -0
  15. package/dist/ui/assets/{agent-bridge-S8j2812Y.js → agent-bridge-D0DMll5k.js} +10 -9
  16. package/dist/ui/assets/agent-panels-EBOP0pAa.js +1 -0
  17. package/dist/ui/assets/{apply-layout-D3gOL_38.js → apply-layout-B2EQevMq.js} +1 -1
  18. package/dist/ui/assets/{apps-CSQRDxWq.js → apps-BR3cRjE_.js} +2 -2
  19. package/dist/ui/assets/{azure-openai-CRJdUlny.js → azure-openai-CarvB7O9.js} +1 -1
  20. package/dist/ui/assets/{azure-openai-CcxSOX33.js → azure-openai-x_uz-FS4.js} +2 -2
  21. package/dist/ui/assets/bedrock-camp-C8BwH8Mj.js +1 -0
  22. package/dist/ui/assets/bedrock-camp-D1dak7rV.js +1 -0
  23. package/dist/ui/assets/bedrock-camp-compat-99DjWBHV.js +1 -0
  24. package/dist/ui/assets/bedrock-camp-compat-BeCSBUrZ.js +1 -0
  25. package/dist/ui/assets/capability-CTDMHYYU.js +2 -0
  26. package/dist/ui/assets/cdp-DseOo5rP.js +881 -0
  27. package/dist/ui/assets/{cerebras-CWb9p9Rf.js → cerebras-B1AF6wK5.js} +1 -1
  28. package/dist/ui/assets/{cerebras-DO9w7qJ8.js → cerebras-fR81CWcL.js} +1 -1
  29. package/dist/ui/assets/{claude-model-version-BJLrDjmH.js → claude-model-version-CSATLzrT.js} +1 -1
  30. package/dist/ui/assets/claude-model-version-m5AAX9Oe.js +1 -0
  31. package/dist/ui/assets/composer-speech-B9BMS4Az.js +1 -0
  32. package/dist/ui/assets/{connect-surface-IBJGJKXd.js → connect-surface-DP45y859.js} +1 -1
  33. package/dist/ui/assets/{directed-approval-B2nSvGGo.js → directed-approval-CuXuJ8VY.js} +1 -1
  34. package/dist/ui/assets/dist-CMD3ZuiV.js +1 -0
  35. package/dist/ui/assets/dist-CWHNqkcj.js +1 -0
  36. package/dist/ui/assets/dist-CrVgSn_b.js +6 -0
  37. package/dist/ui/assets/dist-DkG8wgj1.js +23 -0
  38. package/dist/ui/assets/esp8266-1y0C3qJ1.js +1 -0
  39. package/dist/ui/assets/esptool-operations-CiccBbBW.js +12 -0
  40. package/dist/ui/assets/extension-bridge-transport-zMxrmuc2.js +1 -0
  41. package/dist/ui/assets/{follower-navigate-watcher-JQ0b_cW8.js → follower-navigate-watcher-dqOOG5CZ.js} +1 -1
  42. package/dist/ui/assets/{fs-BoEnJw4s.js → fs-B8U5EC4S.js} +1 -1
  43. package/dist/ui/assets/fs-DZiSOeiV.js +2 -0
  44. package/dist/ui/assets/{github-C0ynjd_v.js → github-BKPC-6V7.js} +1 -1
  45. package/dist/ui/assets/github-Rph4kR8u.js +2 -0
  46. package/dist/ui/assets/github-copilot-B45fsbJE.js +2 -0
  47. package/dist/ui/assets/{github-copilot-Dun7obqf.js → github-copilot-m7FAkLKL.js} +1 -1
  48. package/dist/ui/assets/{github-install-CCj8_2v5.js → github-install-DEyHFxj7.js} +1 -1
  49. package/dist/ui/assets/{github-zip-Dy9qcuNt.js → github-zip-D4owU6oz.js} +1 -1
  50. package/dist/ui/assets/{hear-CNQSuR1y.js → hear-CvLWI56h.js} +1 -1
  51. package/dist/ui/assets/{help-1e8yOlr-.js → help-C4EiNzBg.js} +1 -1
  52. package/dist/ui/assets/icons-BXcp4CBT.js +1 -0
  53. package/dist/ui/assets/{js-realm-worker-rSnHM5iE.js → js-realm-worker-t0tgCFRF.js} +1 -1
  54. package/dist/ui/assets/{kernel-worker-DkiXdKIm.js → kernel-worker-HXzODg96.js} +602 -601
  55. package/dist/ui/assets/legacy-api-aliases-bAJ0Klzq.js +1 -0
  56. package/dist/ui/assets/{lick-ws-bridge-Bo5sH7lf.js → lick-ws-bridge-Bb2eVADZ.js} +1 -1
  57. package/dist/ui/assets/{local-llm-BG6FnzbS.js → local-llm-BRXVAmOc.js} +1 -1
  58. package/dist/ui/assets/{local-llm-CHUrcvmR.js → local-llm-CGxqNPHi.js} +1 -1
  59. package/dist/ui/assets/main-CRJcIlg2.js +22 -0
  60. package/dist/ui/assets/{main-cherry-Ibu56rin.js → main-cherry-Cz3S2BJy.js} +1 -1
  61. package/dist/ui/assets/{mount-rNqMCM6y.js → mount-DSBwGVEx.js} +3 -3
  62. package/dist/ui/assets/{mount-DxsGYJn5.js → mount-jaKXBhl1.js} +1 -1
  63. package/dist/ui/assets/{mount-recovery-BMIs8Fmy.js → mount-recovery-DL3Skeu-.js} +1 -1
  64. package/dist/ui/assets/navigation-watcher-COUKtxwQ.js +1 -0
  65. package/dist/ui/assets/new-session-s3Xs25kA.js +2 -0
  66. package/dist/ui/assets/oauth-bootstrap-CuNwuZBb.js +2 -0
  67. package/dist/ui/assets/onboarding-orchestrator-BmeBv4Gp.js +1 -0
  68. package/dist/ui/assets/{openai-codex-XqP20i_Q.js → openai-codex-C3MfGQX9.js} +1 -1
  69. package/dist/ui/assets/openai-codex-cu3w-X1S.js +1 -0
  70. package/dist/ui/assets/openrouter-D4XcSujz.js +1 -0
  71. package/dist/ui/assets/{openrouter-CeWQMDvQ.js → openrouter-PY89nOyM.js} +1 -1
  72. package/dist/ui/assets/{openrouter-oauth-BrfPX9xc.js → openrouter-oauth-Bf6qJKa8.js} +1 -1
  73. package/dist/ui/assets/openrouter-oauth-D26-a5--.js +1 -0
  74. package/dist/ui/assets/panel-rpc-handlers-DZBKznj3.js +2 -0
  75. package/dist/ui/assets/panelize-shell-DrwwO2I-.js +4 -0
  76. package/dist/ui/assets/{provider-BrN0oYcu.js → provider-BiqZhM5h.js} +1 -1
  77. package/dist/ui/assets/provider-QB1F4Xys.js +2 -0
  78. package/dist/ui/assets/provider-settings-DaR5wS_z.js +1 -0
  79. package/dist/ui/assets/provider-store-access-BDFJqulW.js +1 -0
  80. package/dist/ui/assets/provider-store-access-CYkQdTfa.js +1 -0
  81. package/dist/ui/assets/{providers-uyqwOc6n.js → providers-DpiQagGT.js} +1 -1
  82. package/dist/ui/assets/providers-sxOhGKYB.js +2 -0
  83. package/dist/ui/assets/{quick-llm-KuQledjU.js → quick-llm-CKHfcFuQ.js} +1 -1
  84. package/dist/ui/assets/{recovery-screen-CEWIaXnj.js → recovery-screen-DmT0rFlT.js} +1 -1
  85. package/dist/ui/assets/{rest-ops-CLpS_ilI.js → rest-ops-B8ZocTVM.js} +1 -1
  86. package/dist/ui/assets/{run-t78pCdsT.js → run-BcAa2ols.js} +1 -1
  87. package/dist/ui/assets/{run-Dt5cxT0F.js → run-BrA8TEwf.js} +1 -1
  88. package/dist/ui/assets/{run-CIYc2Qb2.js → run-C9g_NP0v.js} +2 -2
  89. package/dist/ui/assets/{setup-feature-flags-remote-bNwKlj1P.js → setup-feature-flags-remote-CLGA0pxR.js} +1 -1
  90. package/dist/ui/assets/sidecar-repair-Csokye05.js +1 -0
  91. package/dist/ui/assets/silent-renew-backoff-CTLMurGM.js +1 -0
  92. package/dist/ui/assets/{skills-BOdEsYE9.js → skills-CVnzD-KG.js} +1 -1
  93. package/dist/ui/assets/slicc-editor-D-eO_pjd.js +23 -0
  94. package/dist/ui/assets/slicc-editor-Sdfe-_x_.js +1 -0
  95. package/dist/ui/assets/slicc-version-BNbx6DEv.js +1 -0
  96. package/dist/ui/assets/{snapshot-g-snbDyA.js → snapshot-Cc4QPfvM.js} +1 -1
  97. package/dist/ui/assets/{snapshot-features-BTV-hsWa.js → snapshot-features-B86A9a5T.js} +1 -1
  98. package/dist/ui/assets/soundscape-Bq_V_6Yw.js +1 -0
  99. package/dist/ui/assets/{speak-BxNdNjKi.js → speak-CqbRi6pA.js} +1 -1
  100. package/dist/ui/assets/sprinkle-manager-CqK5Gzty.js +1 -0
  101. package/dist/ui/assets/sprinkle-renderer-BGbM2VW8.js +286 -0
  102. package/dist/ui/assets/{state-Bz2C8x64.js → state-CBPU3yix.js} +1 -1
  103. package/dist/ui/assets/{store-DB0WokKu.js → store-CfgG6MCL.js} +1 -1
  104. package/dist/ui/assets/{store-BHve8pmc.js → store-Wcm463vo.js} +1 -1
  105. package/dist/ui/assets/{sudo-B8TRUg6E.js → sudo-BGnIjc_Q.js} +1 -1
  106. package/dist/ui/assets/sync-dialog-Cd8n4IiV.js +1 -0
  107. package/dist/ui/assets/telemetry-BWAgv6Hm.js +3 -0
  108. package/dist/ui/assets/temperature-support-B2rDQKe1.js +1 -0
  109. package/dist/ui/assets/temperature-support-LHBt3FMX.js +1 -0
  110. package/dist/ui/assets/transformers-env-NnCJIbOS.js +1 -0
  111. package/dist/ui/assets/tray-leader-DaLUK8LB.js +1 -0
  112. package/dist/ui/assets/tray-sidecar-iqsBxl7p.js +2 -0
  113. package/dist/ui/assets/{upgrade-detection-BpAx3I8x.js → upgrade-detection-BHqNxLM0.js} +1 -1
  114. package/dist/ui/assets/voice-reply-wpoJjCii.js +1 -0
  115. package/dist/ui/assets/wc-attach-BRK3Sw08.js +13 -0
  116. package/dist/ui/assets/wc-browser-CuZwpwEQ.js +1 -0
  117. package/dist/ui/assets/wc-extension-DBJold4K.js +2 -0
  118. package/dist/ui/assets/wc-follower-DGKQnqCu.js +17 -0
  119. package/dist/ui/assets/wc-follower-oauth-D5QCyPfS.js +5 -0
  120. package/dist/ui/assets/wc-live-CyqSJGYW.js +400 -0
  121. package/dist/ui/assets/wc-nav-DhQemEAd.js +2 -0
  122. package/dist/ui/assets/wc-onboarding-CmGOAIm8.js +2 -0
  123. package/dist/ui/assets/wc-placeholder-B6IIK-nS.js +4 -0
  124. package/dist/ui/assets/{wc-settings-C7_YdY0L.js → wc-settings-BvUwaUq0.js} +3 -3
  125. package/dist/ui/assets/wc-shell-maFgJD5l.js +125 -0
  126. package/dist/ui/assets/{wc-shortcut-config-2ywqohA1.js → wc-shortcut-config-BJkRa19F.js} +1 -1
  127. package/dist/ui/assets/{src-hh5Jc_tK.js → wc-shortcuts-BvdDd3KG.js} +236 -186
  128. package/dist/ui/assets/wc-sprinkles-BzEC5lo2.js +2 -0
  129. package/dist/ui/assets/wc-transcript-export-D3kvMKNz.js +2 -0
  130. package/dist/ui/assets/wc-tray-DEXQTMgX.js +5 -0
  131. package/dist/ui/assets/welcome-detection-CQm-JCYe.js +1 -0
  132. package/dist/ui/assets/whisper-session-BZBk770W.js +2 -0
  133. package/dist/ui/assets/xai-grok-BO8zcLws.js +1 -0
  134. package/dist/ui/assets/{xai-grok-DlChQnzy.js → xai-grok-Kgw4mVWn.js} +1 -1
  135. package/dist/ui/index.html +7 -12
  136. package/dist/ui/packages/webapp/index.html +7 -12
  137. package/dist/ui/slicc-editor.js +1 -1
  138. package/package.json +1 -1
  139. package/dist/ui/assets/SKILL--AFE5RE3.js +0 -1
  140. package/dist/ui/assets/account-store-BGNETLsm.js +0 -63
  141. package/dist/ui/assets/account-store-CM0gf4zY.js +0 -1
  142. package/dist/ui/assets/adobe-DyaLZq-8.js +0 -2
  143. package/dist/ui/assets/agent-handle-CNeA3sd0.js +0 -1
  144. package/dist/ui/assets/agent-message-to-chat-Df4an5Ei.js +0 -1
  145. package/dist/ui/assets/agent-panels-B92KQapO.js +0 -1
  146. package/dist/ui/assets/bedrock-camp-Djf-aXdl.js +0 -1
  147. package/dist/ui/assets/bedrock-camp-H3Byazn0.js +0 -1
  148. package/dist/ui/assets/bedrock-camp-compat-CYnC9BQI.js +0 -1
  149. package/dist/ui/assets/bedrock-camp-compat-CgBMwna8.js +0 -1
  150. package/dist/ui/assets/capability-Bxl0SE1s.js +0 -2
  151. package/dist/ui/assets/cdp-DKhmYptD.js +0 -881
  152. package/dist/ui/assets/chat-session-store-pWb1-jNp.js +0 -1
  153. package/dist/ui/assets/claude-model-version-CJiITCDG.js +0 -1
  154. package/dist/ui/assets/composer-speech-CElQJ4q_.js +0 -1
  155. package/dist/ui/assets/db-cKsBsMMN.js +0 -1
  156. package/dist/ui/assets/dictation-priming-CNllGgNq.js +0 -1
  157. package/dist/ui/assets/dip-DySyh98z.js +0 -233
  158. package/dist/ui/assets/dist-BamC4SL9.js +0 -1
  159. package/dist/ui/assets/dist-Bwf-JITi.js +0 -12
  160. package/dist/ui/assets/dist-CJQsHfm3.js +0 -6
  161. package/dist/ui/assets/dist-Cre853-J.js +0 -23
  162. package/dist/ui/assets/dist-D9gC7MoR.js +0 -1
  163. package/dist/ui/assets/esptool-operations-DEqDNikQ.js +0 -12
  164. package/dist/ui/assets/export-provider-CqI0pk50.js +0 -1
  165. package/dist/ui/assets/extension-bridge-transport-DfRNjx8c.js +0 -1
  166. package/dist/ui/assets/follower-presentation-DqBw0YiV.js +0 -1
  167. package/dist/ui/assets/fs-Bm2LRyUH.js +0 -2
  168. package/dist/ui/assets/github-ZxrIXroz.js +0 -2
  169. package/dist/ui/assets/github-copilot-DK9BvwFj.js +0 -2
  170. package/dist/ui/assets/host-command-29ain4NA.js +0 -1
  171. package/dist/ui/assets/icons-D4SDtJW0.js +0 -1
  172. package/dist/ui/assets/image-markers-BFDmhPy4.js +0 -1
  173. package/dist/ui/assets/layout-schema-m9_VAwOw.js +0 -1
  174. package/dist/ui/assets/legacy-api-aliases-45zvHmpJ.js +0 -1
  175. package/dist/ui/assets/main-L2EM-k_T.js +0 -22
  176. package/dist/ui/assets/navigation-watcher-C8xRyQBd.js +0 -1
  177. package/dist/ui/assets/new-session-CZy_BIUd.js +0 -2
  178. package/dist/ui/assets/oauth-bootstrap-D8Q37TT-.js +0 -2
  179. package/dist/ui/assets/onboarding-orchestrator-CpVkmcoM.js +0 -1
  180. package/dist/ui/assets/openai-codex-BoGyqGTb.js +0 -1
  181. package/dist/ui/assets/openrouter-d0ZBwe7-.js +0 -1
  182. package/dist/ui/assets/openrouter-oauth-DFBYXCcd.js +0 -1
  183. package/dist/ui/assets/panel-registry-CJdOibpf.js +0 -1
  184. package/dist/ui/assets/panel-rpc-handlers-CsyJxXeR.js +0 -2
  185. package/dist/ui/assets/panelize-shell-C4WoeBi-.js +0 -4
  186. package/dist/ui/assets/preview-url-r19H6zH1.js +0 -1
  187. package/dist/ui/assets/provider-bztYEW41.js +0 -2
  188. package/dist/ui/assets/provider-settings-3-JiPamh.js +0 -1
  189. package/dist/ui/assets/provider-settings-DT-N1OD6.js +0 -2
  190. package/dist/ui/assets/provider-store-access-Bch5GC1b.js +0 -2
  191. package/dist/ui/assets/provider-store-access-KBqWyi3A.js +0 -1
  192. package/dist/ui/assets/providers-Y6vDOS61.js +0 -2
  193. package/dist/ui/assets/rest-paths-DJCfupdz.js +0 -1
  194. package/dist/ui/assets/session-freezer-DWq5xKa-.js +0 -163
  195. package/dist/ui/assets/setup-standalone-prelude-DU7F98aW.js +0 -2
  196. package/dist/ui/assets/setup-sudo-kBAwNpgU.js +0 -2
  197. package/dist/ui/assets/sidecar-repair-431vRT2V.js +0 -1
  198. package/dist/ui/assets/silent-renew-backoff-YUr3mR2G.js +0 -1
  199. package/dist/ui/assets/slicc-editor-Bhg4cWBq.js +0 -12
  200. package/dist/ui/assets/slicc-editor-CL_D1iJB.js +0 -1
  201. package/dist/ui/assets/slicc-version-BH39IvgC.js +0 -1
  202. package/dist/ui/assets/soundscape-TvriK11t.js +0 -1
  203. package/dist/ui/assets/soundscape-cues-5_sT5Un6.js +0 -1
  204. package/dist/ui/assets/speech-C5U8h4In.js +0 -1
  205. package/dist/ui/assets/sprinkle-bridge-CeRz-DjR.js +0 -2
  206. package/dist/ui/assets/sprinkle-manager-jHNM2oxy.js +0 -1
  207. package/dist/ui/assets/sprinkle-renderer-Ce6YjEek.js +0 -281
  208. package/dist/ui/assets/surface-fullscreen-4YjALjE7.js +0 -1
  209. package/dist/ui/assets/sync-dialog-BBo1RfX_.js +0 -1
  210. package/dist/ui/assets/telemetry-MJ77Fp1a.js +0 -3
  211. package/dist/ui/assets/temperature-support-BCoY8YuX.js +0 -1
  212. package/dist/ui/assets/temperature-support-C046iRqM.js +0 -1
  213. package/dist/ui/assets/timestamp-preference-sKwBl5I8.js +0 -2
  214. package/dist/ui/assets/transformers-env-BmBEKck6.js +0 -1
  215. package/dist/ui/assets/tray-leader-DabyaEs-.js +0 -1
  216. package/dist/ui/assets/tray-sidecar-Zx9F34-1.js +0 -2
  217. package/dist/ui/assets/tray-sync-protocol-jKTx-dCF.js +0 -1
  218. package/dist/ui/assets/tray-webrtc-uW6sTyIA.js +0 -1
  219. package/dist/ui/assets/trusted-layer-ghPA1ENf.js +0 -1
  220. package/dist/ui/assets/voice-reply-B43Nr1Xr.js +0 -1
  221. package/dist/ui/assets/wc-attach-Cr1o6HzV.js +0 -13
  222. package/dist/ui/assets/wc-browser-Ctsf5Y23.js +0 -1
  223. package/dist/ui/assets/wc-extension-BjK-tKBf.js +0 -2
  224. package/dist/ui/assets/wc-float-label-BkfYPqdF.js +0 -1
  225. package/dist/ui/assets/wc-follower-CH9Ey9IG.js +0 -17
  226. package/dist/ui/assets/wc-follower-oauth-DCz20htV.js +0 -5
  227. package/dist/ui/assets/wc-live-D_Z9cUh5.js +0 -8
  228. package/dist/ui/assets/wc-nav-CPhi1Chn.js +0 -2
  229. package/dist/ui/assets/wc-onboarding-DVK6dIOf.js +0 -2
  230. package/dist/ui/assets/wc-placeholder-DcsoOquv.js +0 -4
  231. package/dist/ui/assets/wc-shell-BtcgWp4y.js +0 -125
  232. package/dist/ui/assets/wc-shortcuts-DESTsoW8.js +0 -41
  233. package/dist/ui/assets/wc-sprinkles-CDQGgIIW.js +0 -2
  234. package/dist/ui/assets/wc-transcript-export-DdEuCfi6.js +0 -2
  235. package/dist/ui/assets/wc-tray-gcTDn_z9.js +0 -5
  236. package/dist/ui/assets/welcome-detection-Ddf03FcG.js +0 -1
  237. package/dist/ui/assets/whisper-session-DW7HkDu2.js +0 -2
  238. package/dist/ui/assets/xai-grok-xFpjw8yf.js +0 -1
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Chrome-leg reconnect supervisor for the node-server `/cdp` proxy.
3
+ *
4
+ * Chrome's browser-level debugger socket drops on its own (`messageTooLarge`,
5
+ * an inbound-queue overflow, a devtools window stealing the single slot). When
6
+ * it does, Chrome discards EVERY CDP session behind it. Until now node-server
7
+ * just cleared `chromeWs` and dropped client frames on the floor until a NEW
8
+ * `/cdp` client connected — i.e. until the SLICC tab was reloaded by hand
9
+ * (issue #2417, `DIAGNOSIS.md` §2.6). swift-server already reconnects
10
+ * (`CDPProxy.scheduleChromeReconnect` / `runChromeReconnectLoop`); this brings
11
+ * node-server to parity and adds the missing half both floats needed: the
12
+ * active client is closed with `CDP_UPSTREAM_RESET_CLOSE_CODE` so the page
13
+ * re-dials and resets its session state instead of issuing commands against
14
+ * sessions Chrome has forgotten.
15
+ *
16
+ * ## Reconnect policy (identical in node-server and swift-server)
17
+ *
18
+ * Retry indefinitely with a 1 s delay between attempts, until shutdown — there
19
+ * is no attempt cap. Close the active client with 4002 `upstream-reset` after
20
+ * the 3rd consecutive failure, so it does not hang on a proxy whose Chrome leg
21
+ * is gone. After a successful reconnect, reset the active client ONLY if it is
22
+ * the same client that held the slot when the Chrome leg went down — it is the
23
+ * one whose sessions Chrome discarded. A client that connected during the
24
+ * outage never had sessions on the dead leg and its buffered frames were just
25
+ * flushed onto the replacement connection, so closing it with 4002 would make
26
+ * the page retry commands that already ran (a sessionless `Target.createTarget`
27
+ * opens a duplicate tab). Never leave a clientless buffer around: buffered
28
+ * frames are dropped whenever the client that wrote them loses the slot.
29
+ */
30
+ import { type ClientFrameBuffer } from './client-frame-buffer.js';
31
+ /** Delay before each reconnect attempt. Mirrors swift `defaultReconnectDelayNanoseconds`. */
32
+ export declare const CHROME_RECONNECT_DELAY_MS = 1000;
33
+ /**
34
+ * Consecutive failed attempts after which the active client is cut loose with
35
+ * 4002 rather than left hanging on a proxy whose Chrome leg is gone. The loop
36
+ * keeps retrying afterwards. Mirrors swift `upstreamResetFailureThreshold`.
37
+ */
38
+ export declare const CHROME_RECONNECT_FAILURE_THRESHOLD = 3;
39
+ export interface ChromeReconnectDeps {
40
+ /** Re-discover Chrome's browser-level ws URL (via `/json/version`). */
41
+ discoverChromeWsUrl: () => Promise<string>;
42
+ /** Connect the Chrome leg to `url`; rejects when the socket never opens. */
43
+ connectChrome: (url: string) => Promise<void>;
44
+ /** Close the active `/cdp` client with the upstream-reset code, if any. */
45
+ resetClient: (reason: string) => void;
46
+ /**
47
+ * Id of the client holding the single `/cdp` slot right now, or null when it
48
+ * is empty. Sampled on every leg drop and compared again after a successful
49
+ * reconnect: only the holder that saw the dead leg carries stale sessions.
50
+ */
51
+ activeClientId: () => number | null;
52
+ /** True once graceful shutdown started — no reconnect, no client reset. */
53
+ isShuttingDown: () => boolean;
54
+ /**
55
+ * True when the Chrome leg is already open again — e.g. a NEW `/cdp` client
56
+ * connected during the delay and `handleCdpClient` rebuilt it. That client
57
+ * has no stale sessions, so the loop must exit WITHOUT evicting it.
58
+ */
59
+ isChromeLegHealthy?: () => boolean;
60
+ /** Log one `[cdp-proxy]`-prefixed line (caller applies `CliLogDedup`). */
61
+ log: (line: string) => void;
62
+ sleep?: (ms: number) => Promise<void>;
63
+ delayMs?: number;
64
+ failureThreshold?: number;
65
+ }
66
+ /** The proxy state a Chrome-leg drop mutates (a slice of `ServerState`). */
67
+ export interface ChromeLegState<Socket = unknown> {
68
+ chromeWs: Socket | null;
69
+ /** Id of the Chrome connection `chromeWs` refers to (0 = never connected). */
70
+ chromeConnectionId: number;
71
+ /** Id of the client holding the single `/cdp` slot, or null for none. */
72
+ activeClientId: number | null;
73
+ /** Non-null = buffering Client→Chrome frames instead of forwarding them. */
74
+ messageBuffer: ClientFrameBuffer | null;
75
+ shuttingDown: boolean;
76
+ }
77
+ /**
78
+ * Record that the Chrome leg went away: clear the socket and start buffering
79
+ * client frames so they survive the gap (node-server used to drop them until a
80
+ * NEW `/cdp` client connected). Returns false — meaning "do not schedule a
81
+ * reconnect" — when the event came from a socket a newer connect already
82
+ * replaced, or when the server is shutting down.
83
+ *
84
+ * The buffer opened here is tagged with the connection that just died, so the
85
+ * flush onto the REPLACEMENT connection discards it: those frames name sessions
86
+ * Chrome dropped with the old socket, and the client is being reset anyway.
87
+ */
88
+ export declare function markChromeLegDown<Socket>(state: ChromeLegState<Socket>, droppedWs: Socket): boolean;
89
+ /** Minimal `ws` surface needed to evict the active `/cdp` client. */
90
+ export interface ResettableClientSocket {
91
+ readyState: number;
92
+ close(code?: number, reason?: string): void;
93
+ }
94
+ /**
95
+ * Close the active `/cdp` client with the upstream-reset code so the page
96
+ * re-dials and rebuilds its (now dead) CDP sessions. Returns true when a close
97
+ * was actually issued.
98
+ */
99
+ export declare function closeClientForUpstreamReset(client: ResettableClientSocket | null | undefined, reason: string, log: (line: string) => void): boolean;
100
+ /**
101
+ * Owns at most one in-flight reconnect loop. `schedule()` is idempotent — a
102
+ * Chrome socket that fires both `error` and `close` schedules once.
103
+ */
104
+ export declare class ChromeReconnectController {
105
+ private readonly deps;
106
+ private task;
107
+ private cancelled;
108
+ /** Slot holder at the most recent Chrome-leg drop — the one with stale sessions. */
109
+ private slotHolderAtDrop;
110
+ constructor(deps: ChromeReconnectDeps);
111
+ /** True while a reconnect loop is running. */
112
+ get reconnecting(): boolean;
113
+ /** Start the reconnect loop unless one is already running or we're shutting down. */
114
+ schedule(reason: string): void;
115
+ /** Stop the loop (graceful shutdown). Sticky — the controller is not reused. */
116
+ cancel(): void;
117
+ /** Resolves when the current reconnect loop (if any) has finished. For tests. */
118
+ settled(): Promise<void>;
119
+ /**
120
+ * Retry until the leg is back or shutdown stops us — no attempt cap, so a
121
+ * Chrome that comes back after a long outage is picked up without needing a
122
+ * fresh client to drive discovery (swift parity).
123
+ */
124
+ private run;
125
+ /** One discover + connect round. Returns true when the leg is back. */
126
+ private attempt;
127
+ /**
128
+ * Close the client that held the slot when the leg dropped: Chrome discarded
129
+ * every session with the old socket, so that page's cached sessionIds are
130
+ * dead and it must re-dial with a clean slate.
131
+ *
132
+ * A client that connected DURING the outage is left alone. It never had
133
+ * sessions on the dead leg, and the frames it buffered were just flushed onto
134
+ * the replacement connection — closing it with 4002 would make the page retry
135
+ * commands that already ran, and a sessionless `Target.createTarget` among
136
+ * them opens a duplicate tab (issue #2417). The third-failure reset clears
137
+ * the slot, so the comparison below naturally fails for any replacement.
138
+ */
139
+ private resetStaleSlotHolder;
140
+ private stopped;
141
+ }
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Chrome-leg reconnect supervisor for the node-server `/cdp` proxy.
3
+ *
4
+ * Chrome's browser-level debugger socket drops on its own (`messageTooLarge`,
5
+ * an inbound-queue overflow, a devtools window stealing the single slot). When
6
+ * it does, Chrome discards EVERY CDP session behind it. Until now node-server
7
+ * just cleared `chromeWs` and dropped client frames on the floor until a NEW
8
+ * `/cdp` client connected — i.e. until the SLICC tab was reloaded by hand
9
+ * (issue #2417, `DIAGNOSIS.md` §2.6). swift-server already reconnects
10
+ * (`CDPProxy.scheduleChromeReconnect` / `runChromeReconnectLoop`); this brings
11
+ * node-server to parity and adds the missing half both floats needed: the
12
+ * active client is closed with `CDP_UPSTREAM_RESET_CLOSE_CODE` so the page
13
+ * re-dials and resets its session state instead of issuing commands against
14
+ * sessions Chrome has forgotten.
15
+ *
16
+ * ## Reconnect policy (identical in node-server and swift-server)
17
+ *
18
+ * Retry indefinitely with a 1 s delay between attempts, until shutdown — there
19
+ * is no attempt cap. Close the active client with 4002 `upstream-reset` after
20
+ * the 3rd consecutive failure, so it does not hang on a proxy whose Chrome leg
21
+ * is gone. After a successful reconnect, reset the active client ONLY if it is
22
+ * the same client that held the slot when the Chrome leg went down — it is the
23
+ * one whose sessions Chrome discarded. A client that connected during the
24
+ * outage never had sessions on the dead leg and its buffered frames were just
25
+ * flushed onto the replacement connection, so closing it with 4002 would make
26
+ * the page retry commands that already ran (a sessionless `Target.createTarget`
27
+ * opens a duplicate tab). Never leave a clientless buffer around: buffered
28
+ * frames are dropped whenever the client that wrote them loses the slot.
29
+ */
30
+ import { createClientFrameBuffer } from './client-frame-buffer.js';
31
+ import { CDP_UPSTREAM_RESET_CLOSE_CODE, CDP_UPSTREAM_RESET_CLOSE_REASON } from './close-codes.js';
32
+ /** Delay before each reconnect attempt. Mirrors swift `defaultReconnectDelayNanoseconds`. */
33
+ export const CHROME_RECONNECT_DELAY_MS = 1000;
34
+ /**
35
+ * Consecutive failed attempts after which the active client is cut loose with
36
+ * 4002 rather than left hanging on a proxy whose Chrome leg is gone. The loop
37
+ * keeps retrying afterwards. Mirrors swift `upstreamResetFailureThreshold`.
38
+ */
39
+ export const CHROME_RECONNECT_FAILURE_THRESHOLD = 3;
40
+ /**
41
+ * Record that the Chrome leg went away: clear the socket and start buffering
42
+ * client frames so they survive the gap (node-server used to drop them until a
43
+ * NEW `/cdp` client connected). Returns false — meaning "do not schedule a
44
+ * reconnect" — when the event came from a socket a newer connect already
45
+ * replaced, or when the server is shutting down.
46
+ *
47
+ * The buffer opened here is tagged with the connection that just died, so the
48
+ * flush onto the REPLACEMENT connection discards it: those frames name sessions
49
+ * Chrome dropped with the old socket, and the client is being reset anyway.
50
+ */
51
+ export function markChromeLegDown(state, droppedWs) {
52
+ // swift-server guards the same way with `chromeConnectionID`: a late close
53
+ // from the previous socket must not tear down the live one.
54
+ if (state.chromeWs !== null && state.chromeWs !== droppedWs)
55
+ return false;
56
+ state.chromeWs = null;
57
+ if (state.shuttingDown)
58
+ return false;
59
+ state.messageBuffer ??= createClientFrameBuffer({
60
+ chromeConnectionId: state.chromeConnectionId,
61
+ clientId: state.activeClientId,
62
+ });
63
+ return true;
64
+ }
65
+ /** `WebSocket.OPEN` — kept local so this module stays free of the `ws` import. */
66
+ const WS_READY_STATE_OPEN = 1;
67
+ /**
68
+ * Close the active `/cdp` client with the upstream-reset code so the page
69
+ * re-dials and rebuilds its (now dead) CDP sessions. Returns true when a close
70
+ * was actually issued.
71
+ */
72
+ export function closeClientForUpstreamReset(client, reason, log) {
73
+ if (!client || client.readyState !== WS_READY_STATE_OPEN)
74
+ return false;
75
+ log(`[cdp-proxy] Closing client after Chrome-leg reset (${reason})`);
76
+ client.close(CDP_UPSTREAM_RESET_CLOSE_CODE, CDP_UPSTREAM_RESET_CLOSE_REASON);
77
+ return true;
78
+ }
79
+ const defaultSleep = (ms) => new Promise((resolve) => {
80
+ setTimeout(resolve, ms);
81
+ });
82
+ /**
83
+ * Owns at most one in-flight reconnect loop. `schedule()` is idempotent — a
84
+ * Chrome socket that fires both `error` and `close` schedules once.
85
+ */
86
+ export class ChromeReconnectController {
87
+ deps;
88
+ task = null;
89
+ cancelled = false;
90
+ /** Slot holder at the most recent Chrome-leg drop — the one with stale sessions. */
91
+ slotHolderAtDrop = null;
92
+ constructor(deps) {
93
+ this.deps = deps;
94
+ }
95
+ /** True while a reconnect loop is running. */
96
+ get reconnecting() {
97
+ return this.task !== null;
98
+ }
99
+ /** Start the reconnect loop unless one is already running or we're shutting down. */
100
+ schedule(reason) {
101
+ if (this.cancelled || this.deps.isShuttingDown()) {
102
+ this.deps.log(`[cdp-proxy] Chrome WS dropped during shutdown — not reconnecting (${reason})`);
103
+ return;
104
+ }
105
+ // Sampled on EVERY drop, including one that lands while a loop is already
106
+ // running (a client that connected during the outage rebuilt the leg and
107
+ // then lost it too): the stale-session client is whoever held the slot at
108
+ // the most recent drop.
109
+ this.slotHolderAtDrop = this.deps.activeClientId();
110
+ if (this.task !== null)
111
+ return;
112
+ const delayMs = this.deps.delayMs ?? CHROME_RECONNECT_DELAY_MS;
113
+ this.deps.log(`[cdp-proxy] Scheduling Chrome WS reconnect in ${delayMs}ms (${reason})`);
114
+ const task = this.run(delayMs);
115
+ this.task = task;
116
+ void task.then(() => {
117
+ if (this.task === task)
118
+ this.task = null;
119
+ });
120
+ }
121
+ /** Stop the loop (graceful shutdown). Sticky — the controller is not reused. */
122
+ cancel() {
123
+ this.cancelled = true;
124
+ }
125
+ /** Resolves when the current reconnect loop (if any) has finished. For tests. */
126
+ async settled() {
127
+ await this.task;
128
+ }
129
+ /**
130
+ * Retry until the leg is back or shutdown stops us — no attempt cap, so a
131
+ * Chrome that comes back after a long outage is picked up without needing a
132
+ * fresh client to drive discovery (swift parity).
133
+ */
134
+ async run(delayMs) {
135
+ const threshold = this.deps.failureThreshold ?? CHROME_RECONNECT_FAILURE_THRESHOLD;
136
+ const sleep = this.deps.sleep ?? defaultSleep;
137
+ let consecutiveFailures = 0;
138
+ let didSignalFailure = false;
139
+ for (;;) {
140
+ await sleep(delayMs);
141
+ if (this.stopped())
142
+ return;
143
+ if (await this.attempt(consecutiveFailures + 1))
144
+ return;
145
+ consecutiveFailures++;
146
+ if (didSignalFailure || consecutiveFailures < threshold)
147
+ continue;
148
+ // Cut the client loose ONCE so it stops waiting on a dead proxy; the loop
149
+ // keeps going and resets whoever holds the slot when Chrome returns.
150
+ didSignalFailure = true;
151
+ this.deps.log(`[cdp-proxy] Chrome WS reconnect failed ${consecutiveFailures}x — resetting client (still retrying)`);
152
+ this.deps.resetClient('reconnect-failed');
153
+ }
154
+ }
155
+ /** One discover + connect round. Returns true when the leg is back. */
156
+ async attempt(attempt) {
157
+ if (this.deps.isChromeLegHealthy?.() === true) {
158
+ this.deps.log('[cdp-proxy] Chrome WS already re-established — no client reset needed');
159
+ return true;
160
+ }
161
+ try {
162
+ const url = await this.deps.discoverChromeWsUrl();
163
+ if (this.stopped())
164
+ return true;
165
+ await this.deps.connectChrome(url);
166
+ if (this.stopped())
167
+ return true;
168
+ this.deps.log('[cdp-proxy] Chrome WS auto-reconnected');
169
+ this.resetStaleSlotHolder();
170
+ return true;
171
+ }
172
+ catch (err) {
173
+ this.deps.log(`[cdp-proxy] Auto-reconnect attempt ${attempt} failed: ${String(err)}`);
174
+ return false;
175
+ }
176
+ }
177
+ /**
178
+ * Close the client that held the slot when the leg dropped: Chrome discarded
179
+ * every session with the old socket, so that page's cached sessionIds are
180
+ * dead and it must re-dial with a clean slate.
181
+ *
182
+ * A client that connected DURING the outage is left alone. It never had
183
+ * sessions on the dead leg, and the frames it buffered were just flushed onto
184
+ * the replacement connection — closing it with 4002 would make the page retry
185
+ * commands that already ran, and a sessionless `Target.createTarget` among
186
+ * them opens a duplicate tab (issue #2417). The third-failure reset clears
187
+ * the slot, so the comparison below naturally fails for any replacement.
188
+ */
189
+ resetStaleSlotHolder() {
190
+ const holder = this.slotHolderAtDrop;
191
+ const current = this.deps.activeClientId();
192
+ if (holder !== null && current === holder) {
193
+ this.deps.resetClient('reconnected');
194
+ return;
195
+ }
196
+ if (current !== null) {
197
+ this.deps.log('[cdp-proxy] Client connected during the outage — no stale sessions, not resetting it');
198
+ }
199
+ }
200
+ stopped() {
201
+ if (!this.cancelled && !this.deps.isShuttingDown())
202
+ return false;
203
+ this.deps.log('[cdp-proxy] Chrome WS reconnect cancelled (shutting down)');
204
+ return true;
205
+ }
206
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Bounded, generation-tagged Client→Chrome frame buffer for the `/cdp` proxy.
3
+ *
4
+ * Frames a client sends while the Chrome leg is unavailable are held here. The
5
+ * buffer is bounded and drops the OLDEST frame on overflow — same policy and
6
+ * limit as swift-server's `CDPProxy.appendBufferedMessage` (`maxBufferSize =
7
+ * 1_000`). Unbounded growth would turn a Chrome leg that never comes back into
8
+ * a heap leak.
9
+ *
10
+ * The generation tag decides whether the buffer may be flushed at all. A
11
+ * buffered frame is only meaningful on the exact `{chromeConnection, client}`
12
+ * pair it was written for:
13
+ *
14
+ * - Chrome discards EVERY CDP session when its browser-level socket closes,
15
+ * so frames buffered after a leg drop name sessions the replacement
16
+ * connection has never heard of. Worse, a browser-level `Target.createTarget`
17
+ * would still execute — after its caller was already rejected with 4002 —
18
+ * and the caller's retry opens a duplicate tab.
19
+ * - Frames buffered by a client that has since been superseded or closed must
20
+ * not run under whoever holds the slot now.
21
+ *
22
+ * So a buffer is flushed only when both halves of its generation still match at
23
+ * flush time, and dropped (the caller logs the count and the reason) otherwise.
24
+ * The one case that still flushes is the original initial-connect buffering — a
25
+ * client that connected before Chrome was ready lost no sessions, so its
26
+ * `chromeConnectionId` is `null` and any connection will do.
27
+ *
28
+ * The state transitions live here rather than in `index.ts` so the whole policy
29
+ * is unit-testable without booting a server. Mirrors `CDPProxy`'s
30
+ * `clientFrameBufferDropReason` / `discardBufferedMessages` in swift-server.
31
+ */
32
+ /** Max frames held while the Chrome leg is down. Mirrors swift `maxBufferSize`. */
33
+ export declare const CDP_CLIENT_FRAME_BUFFER_LIMIT = 1000;
34
+ /** The `{chromeConnection, client}` pair a buffer's frames were written for. */
35
+ export interface CdpBufferGeneration {
36
+ /**
37
+ * Id of the Chrome connection that was live when buffering started, or
38
+ * `null` when no leg was live at all (initial connect / a leg that never
39
+ * opened). `null` flushes onto any connection; a concrete id only flushes
40
+ * onto that same connection, which a post-drop replacement never is.
41
+ */
42
+ chromeConnectionId: number | null;
43
+ /** Id of the client that held the single `/cdp` slot, or `null` for none. */
44
+ clientId: number | null;
45
+ }
46
+ /** Buffered Client→Chrome frames plus the generation they belong to. */
47
+ export interface ClientFrameBuffer {
48
+ readonly generation: CdpBufferGeneration;
49
+ readonly frames: unknown[];
50
+ }
51
+ /** Why a buffer was dropped rather than flushed. Used verbatim in log lines. */
52
+ export type ClientFrameBufferDropReason = 'chrome-leg-reset' | 'client-superseded' | 'client-disconnected' | 'upstream-reset' | 'no-client';
53
+ /** How many frames were thrown away, and why. `null` when nothing was lost. */
54
+ export interface DroppedClientFrames {
55
+ count: number;
56
+ reason: ClientFrameBufferDropReason;
57
+ }
58
+ export interface ClientFrameBufferHost {
59
+ /**
60
+ * The Chrome leg, or null while it is down. A socket that is still
61
+ * CONNECTING (a reconnect attempt in flight) counts as "no live leg": a
62
+ * client joining then has lost nothing and its frames must stay flushable.
63
+ */
64
+ chromeWs: {
65
+ readyState: number;
66
+ } | null;
67
+ /** Monotonic id of `chromeWs`, assigned when the socket is created. */
68
+ chromeConnectionId: number;
69
+ /** Monotonic id of the client holding the single slot; null when empty. */
70
+ activeClientId: number | null;
71
+ messageBuffer: ClientFrameBuffer | null;
72
+ }
73
+ export declare function createClientFrameBuffer(generation: CdpBufferGeneration): ClientFrameBuffer;
74
+ /**
75
+ * Append `frame` to `frames`, dropping the oldest entry when the limit is
76
+ * reached. Returns true when a frame was dropped, so the caller can log it.
77
+ */
78
+ export declare function appendBufferedClientFrame(frames: unknown[], frame: unknown, limit?: number): boolean;
79
+ /** The generation a buffer opened right now belongs to. */
80
+ export declare function currentBufferGeneration(state: ClientFrameBufferHost): CdpBufferGeneration;
81
+ /**
82
+ * Decide whether `buffer` may be flushed onto the connection identified by
83
+ * `current`. Returns `null` when the flush is safe, otherwise the drop reason.
84
+ */
85
+ export declare function clientFrameBufferDropReason(buffer: ClientFrameBuffer, current: CdpBufferGeneration): ClientFrameBufferDropReason | null;
86
+ /** Frames to send now, plus what was thrown away instead of being sent. */
87
+ export interface ClientFrameFlush {
88
+ frames: unknown[];
89
+ dropped: DroppedClientFrames | null;
90
+ }
91
+ /**
92
+ * Clear the buffer and report what may be delivered onto `targetConnectionId`.
93
+ * Always leaves `state.messageBuffer` null: either the frames go out now, or
94
+ * they are gone.
95
+ */
96
+ export declare function takeClientFrameBuffer(state: ClientFrameBufferHost, targetConnectionId: number): ClientFrameFlush;
97
+ /**
98
+ * Whether `clientId` still holds the single `/cdp` slot. A frame from a
99
+ * client that was superseded, closed, or reset with 4002 must not reach
100
+ * Chrome even if it arrives before that client's socket has finished
101
+ * closing — swift-server's `receive` guard, mirrored here.
102
+ */
103
+ export declare function clientHoldsSlot(state: ClientFrameBufferHost, clientId: number): boolean;
104
+ /**
105
+ * Give the single `/cdp` slot to `clientId`. Anything the previous holder
106
+ * buffered belongs to IT — running those frames under the new client is the
107
+ * duplicate-tab bug from issue #2417 — so it is dropped, and a fresh buffer is
108
+ * opened so frames arriving during the Chrome handshake are still captured.
109
+ */
110
+ export declare function adoptClientSlot(state: ClientFrameBufferHost, clientId: number): DroppedClientFrames | null;
111
+ /**
112
+ * Empty the slot. Whatever the departing client buffered dies with it — a
113
+ * clientless buffer must never reach a Chrome leg, and after a 4002 reset the
114
+ * page re-issues whatever it still needs.
115
+ */
116
+ export declare function releaseClientSlot(state: ClientFrameBufferHost, reason: Extract<ClientFrameBufferDropReason, 'client-disconnected' | 'upstream-reset'>): DroppedClientFrames | null;
117
+ /** The `[cdp-proxy]` line emitted whenever buffered frames are thrown away. */
118
+ export declare function describeDroppedClientFrames(dropped: DroppedClientFrames): string;
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Bounded, generation-tagged Client→Chrome frame buffer for the `/cdp` proxy.
3
+ *
4
+ * Frames a client sends while the Chrome leg is unavailable are held here. The
5
+ * buffer is bounded and drops the OLDEST frame on overflow — same policy and
6
+ * limit as swift-server's `CDPProxy.appendBufferedMessage` (`maxBufferSize =
7
+ * 1_000`). Unbounded growth would turn a Chrome leg that never comes back into
8
+ * a heap leak.
9
+ *
10
+ * The generation tag decides whether the buffer may be flushed at all. A
11
+ * buffered frame is only meaningful on the exact `{chromeConnection, client}`
12
+ * pair it was written for:
13
+ *
14
+ * - Chrome discards EVERY CDP session when its browser-level socket closes,
15
+ * so frames buffered after a leg drop name sessions the replacement
16
+ * connection has never heard of. Worse, a browser-level `Target.createTarget`
17
+ * would still execute — after its caller was already rejected with 4002 —
18
+ * and the caller's retry opens a duplicate tab.
19
+ * - Frames buffered by a client that has since been superseded or closed must
20
+ * not run under whoever holds the slot now.
21
+ *
22
+ * So a buffer is flushed only when both halves of its generation still match at
23
+ * flush time, and dropped (the caller logs the count and the reason) otherwise.
24
+ * The one case that still flushes is the original initial-connect buffering — a
25
+ * client that connected before Chrome was ready lost no sessions, so its
26
+ * `chromeConnectionId` is `null` and any connection will do.
27
+ *
28
+ * The state transitions live here rather than in `index.ts` so the whole policy
29
+ * is unit-testable without booting a server. Mirrors `CDPProxy`'s
30
+ * `clientFrameBufferDropReason` / `discardBufferedMessages` in swift-server.
31
+ */
32
+ /** Max frames held while the Chrome leg is down. Mirrors swift `maxBufferSize`. */
33
+ export const CDP_CLIENT_FRAME_BUFFER_LIMIT = 1000;
34
+ /** The slice of `ServerState` the buffer lifecycle reads and mutates. */
35
+ /** ws `WebSocket.OPEN`; the only readyState under which frames can actually be delivered. */
36
+ const SOCKET_OPEN = 1;
37
+ export function createClientFrameBuffer(generation) {
38
+ return { generation, frames: [] };
39
+ }
40
+ /**
41
+ * Append `frame` to `frames`, dropping the oldest entry when the limit is
42
+ * reached. Returns true when a frame was dropped, so the caller can log it.
43
+ */
44
+ export function appendBufferedClientFrame(frames, frame, limit = CDP_CLIENT_FRAME_BUFFER_LIMIT) {
45
+ let dropped = false;
46
+ while (frames.length >= limit) {
47
+ frames.shift();
48
+ dropped = true;
49
+ }
50
+ frames.push(frame);
51
+ return dropped;
52
+ }
53
+ /** The generation a buffer opened right now belongs to. */
54
+ export function currentBufferGeneration(state) {
55
+ return {
56
+ // No live leg = nothing was lost, so the frames may flush onto whatever
57
+ // connection comes up next (the original initial-connect buffering).
58
+ chromeConnectionId: state.chromeWs?.readyState === SOCKET_OPEN ? state.chromeConnectionId : null,
59
+ clientId: state.activeClientId,
60
+ };
61
+ }
62
+ /**
63
+ * Decide whether `buffer` may be flushed onto the connection identified by
64
+ * `current`. Returns `null` when the flush is safe, otherwise the drop reason.
65
+ */
66
+ export function clientFrameBufferDropReason(buffer, current) {
67
+ const { chromeConnectionId, clientId } = buffer.generation;
68
+ if (chromeConnectionId !== null && chromeConnectionId !== current.chromeConnectionId) {
69
+ return 'chrome-leg-reset';
70
+ }
71
+ if (current.clientId === null)
72
+ return 'no-client';
73
+ if (clientId !== current.clientId)
74
+ return 'client-superseded';
75
+ return null;
76
+ }
77
+ /**
78
+ * Clear the buffer and report what may be delivered onto `targetConnectionId`.
79
+ * Always leaves `state.messageBuffer` null: either the frames go out now, or
80
+ * they are gone.
81
+ */
82
+ export function takeClientFrameBuffer(state, targetConnectionId) {
83
+ const buffer = state.messageBuffer;
84
+ state.messageBuffer = null;
85
+ if (!buffer)
86
+ return { frames: [], dropped: null };
87
+ const reason = clientFrameBufferDropReason(buffer, {
88
+ chromeConnectionId: targetConnectionId,
89
+ clientId: state.activeClientId,
90
+ });
91
+ if (!reason)
92
+ return { frames: buffer.frames, dropped: null };
93
+ return {
94
+ frames: [],
95
+ dropped: buffer.frames.length > 0 ? { count: buffer.frames.length, reason } : null,
96
+ };
97
+ }
98
+ /**
99
+ * Whether `clientId` still holds the single `/cdp` slot. A frame from a
100
+ * client that was superseded, closed, or reset with 4002 must not reach
101
+ * Chrome even if it arrives before that client's socket has finished
102
+ * closing — swift-server's `receive` guard, mirrored here.
103
+ */
104
+ export function clientHoldsSlot(state, clientId) {
105
+ return state.activeClientId === clientId;
106
+ }
107
+ /**
108
+ * Give the single `/cdp` slot to `clientId`. Anything the previous holder
109
+ * buffered belongs to IT — running those frames under the new client is the
110
+ * duplicate-tab bug from issue #2417 — so it is dropped, and a fresh buffer is
111
+ * opened so frames arriving during the Chrome handshake are still captured.
112
+ */
113
+ export function adoptClientSlot(state, clientId) {
114
+ state.activeClientId = clientId;
115
+ let dropped = null;
116
+ if (state.messageBuffer && state.messageBuffer.generation.clientId !== clientId) {
117
+ dropped = releaseClientFrameBuffer(state, 'client-superseded');
118
+ }
119
+ state.messageBuffer ??= createClientFrameBuffer(currentBufferGeneration(state));
120
+ return dropped;
121
+ }
122
+ /**
123
+ * Empty the slot. Whatever the departing client buffered dies with it — a
124
+ * clientless buffer must never reach a Chrome leg, and after a 4002 reset the
125
+ * page re-issues whatever it still needs.
126
+ */
127
+ export function releaseClientSlot(state, reason) {
128
+ state.activeClientId = null;
129
+ return releaseClientFrameBuffer(state, reason);
130
+ }
131
+ function releaseClientFrameBuffer(state, reason) {
132
+ const buffer = state.messageBuffer;
133
+ state.messageBuffer = null;
134
+ if (!buffer || buffer.frames.length === 0)
135
+ return null;
136
+ return { count: buffer.frames.length, reason };
137
+ }
138
+ /** The `[cdp-proxy]` line emitted whenever buffered frames are thrown away. */
139
+ export function describeDroppedClientFrames(dropped) {
140
+ return `[cdp-proxy] Dropped ${dropped.count} buffered client frame(s) — ${dropped.reason}`;
141
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Application-range (4000–4999) WebSocket close codes the `/cdp` proxy sends
3
+ * to its single client. Both MUST stay in sync with the same-named constants
4
+ * in `packages/webapp/src/cdp/cdp-client.ts` (and their Swift twins in
5
+ * `packages/swift-server/Sources/WebSocket/CDPProxy.swift`), because the
6
+ * page-side `CDPClient` branches on the exact numeric code.
7
+ */
8
+ /**
9
+ * Sent to a CDP client that is evicted because a newer client took the single
10
+ * proxy slot. The page-side `CDPClient` recognises it and stops
11
+ * auto-reconnecting (otherwise two webapp tabs on one instance evict each
12
+ * other forever). MUST stay in sync with `CDP_SUPERSEDED_CLOSE_CODE` in
13
+ * `packages/webapp/src/cdp/cdp-client.ts`.
14
+ */
15
+ export declare const CDP_SUPERSEDED_CLOSE_CODE = 4001;
16
+ /**
17
+ * Sent to the active CDP client after the proxy's Chrome-leg WebSocket dropped
18
+ * and was re-established (or definitively failed to come back). Chrome
19
+ * discards every CDP session when that socket closes, so the page's cached
20
+ * `sessionId`s are dead — but nothing else tells it. The page-side `CDPClient`
21
+ * treats this code as "reconnect and reset session state" (unlike
22
+ * {@link CDP_SUPERSEDED_CLOSE_CODE}, it does NOT latch superseded). MUST stay
23
+ * in sync with `CDP_UPSTREAM_RESET_CLOSE_CODE` in
24
+ * `packages/webapp/src/cdp/cdp-client.ts`.
25
+ */
26
+ export declare const CDP_UPSTREAM_RESET_CLOSE_CODE = 4002;
27
+ /** Close reason paired with {@link CDP_UPSTREAM_RESET_CLOSE_CODE}. */
28
+ export declare const CDP_UPSTREAM_RESET_CLOSE_REASON = "upstream-reset";
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Application-range (4000–4999) WebSocket close codes the `/cdp` proxy sends
3
+ * to its single client. Both MUST stay in sync with the same-named constants
4
+ * in `packages/webapp/src/cdp/cdp-client.ts` (and their Swift twins in
5
+ * `packages/swift-server/Sources/WebSocket/CDPProxy.swift`), because the
6
+ * page-side `CDPClient` branches on the exact numeric code.
7
+ */
8
+ /**
9
+ * Sent to a CDP client that is evicted because a newer client took the single
10
+ * proxy slot. The page-side `CDPClient` recognises it and stops
11
+ * auto-reconnecting (otherwise two webapp tabs on one instance evict each
12
+ * other forever). MUST stay in sync with `CDP_SUPERSEDED_CLOSE_CODE` in
13
+ * `packages/webapp/src/cdp/cdp-client.ts`.
14
+ */
15
+ export const CDP_SUPERSEDED_CLOSE_CODE = 4001;
16
+ /**
17
+ * Sent to the active CDP client after the proxy's Chrome-leg WebSocket dropped
18
+ * and was re-established (or definitively failed to come back). Chrome
19
+ * discards every CDP session when that socket closes, so the page's cached
20
+ * `sessionId`s are dead — but nothing else tells it. The page-side `CDPClient`
21
+ * treats this code as "reconnect and reset session state" (unlike
22
+ * {@link CDP_SUPERSEDED_CLOSE_CODE}, it does NOT latch superseded). MUST stay
23
+ * in sync with `CDP_UPSTREAM_RESET_CLOSE_CODE` in
24
+ * `packages/webapp/src/cdp/cdp-client.ts`.
25
+ */
26
+ export const CDP_UPSTREAM_RESET_CLOSE_CODE = 4002;
27
+ /** Close reason paired with {@link CDP_UPSTREAM_RESET_CLOSE_CODE}. */
28
+ export const CDP_UPSTREAM_RESET_CLOSE_REASON = 'upstream-reset';