very-happy-cli 0.2.159 → 0.2.160

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 (258) hide show
  1. package/dist/{AcpBackend-DUYFlyfN.cjs → AcpBackend-Bm2rdke8.cjs} +78 -76
  2. package/dist/{AcpBackend-VsfkZiVF.mjs → AcpBackend-mzv8Dhba.mjs} +4 -2
  3. package/dist/{AcpSessionManager-Bdv8LWXh.cjs → AcpSessionManager-BAY1Pggx.cjs} +9 -9
  4. package/dist/{AcpSessionManager-rYtVb0kN.mjs → AcpSessionManager-CoMWFVw3.mjs} +1 -1
  5. package/dist/BasePermissionHandler-CFHAxumV.cjs +157 -0
  6. package/dist/BasePermissionHandler-m3WVholL.mjs +155 -0
  7. package/dist/BaseReasoningProcessor-BFa9_rZA.cjs +200 -0
  8. package/dist/BaseReasoningProcessor-CYrKBXGm.mjs +198 -0
  9. package/dist/RemoteModeDisplay-D-HewyXg.mjs +150 -0
  10. package/dist/RemoteModeDisplay-NC1gPHtj.cjs +153 -0
  11. package/dist/agentAttachments-Dbw8iMCZ.cjs +20 -0
  12. package/dist/agentAttachments-M_B5qEF2.mjs +18 -0
  13. package/dist/agentGuidance-B5NATuFa.cjs +28 -0
  14. package/dist/agentGuidance-CQuNlNx8.mjs +20 -0
  15. package/dist/{agentHome-CHh4lmjM.cjs → agentHome-CboCeKWq.cjs} +14 -28
  16. package/dist/{agentHome-CvsJyeQB.mjs → agentHome-Dj1C43aJ.mjs} +8 -22
  17. package/dist/api-CLMUr-SX.mjs +3339 -0
  18. package/dist/api-CgundNNX.cjs +3346 -0
  19. package/dist/{types-Dh-s4Lqs.mjs → apiMachine-B9dLW_p5.mjs} +9116 -16205
  20. package/dist/{types-BbvK1Tr_.cjs → apiMachine-Bn8482VM.cjs} +9104 -16247
  21. package/dist/assistantTools-BTqPnlFl.cjs +380 -0
  22. package/dist/assistantTools-D6tuc6o4.mjs +376 -0
  23. package/dist/attachmentContent-DpiTJZb4.cjs +88 -0
  24. package/dist/attachmentContent-_kxRznNf.mjs +82 -0
  25. package/dist/attachmentLimits-CzVrqgLR.cjs +354 -0
  26. package/dist/attachmentLimits-yba1uHFO.mjs +348 -0
  27. package/dist/auth-BDOVh5h5.cjs +220 -0
  28. package/dist/auth-Cwutmdsa.mjs +225 -0
  29. package/dist/auth-PYHZQNPp.mjs +218 -0
  30. package/dist/auth-ba4WMYrN.cjs +227 -0
  31. package/dist/authRelay-BdYkC1Po.cjs +156 -0
  32. package/dist/authRelay-DSAvg1uv.mjs +143 -0
  33. package/dist/{auto-DKmEls8Z.cjs → auto-BOX_fgmR.cjs} +70 -79
  34. package/dist/{auto-nb8GhRRy.mjs → auto-BXu14yP_.mjs} +27 -36
  35. package/dist/backgroundTaskActivity-8PdPxrvy.mjs +149 -0
  36. package/dist/backgroundTaskActivity-CBvUdj39.cjs +157 -0
  37. package/dist/bootstrap-B5G1TijC.cjs +116 -0
  38. package/dist/bootstrap-hxXPCFMx.mjs +112 -0
  39. package/dist/browser-Y6Bl8L_O.cjs +22 -0
  40. package/dist/browser-jh2P0UVZ.mjs +20 -0
  41. package/dist/claudeCliPath-BqkaH2oR.mjs +8 -0
  42. package/dist/claudeCliPath-U4HHmV4r.cjs +10 -0
  43. package/dist/claudeLocal-BaY4fZKi.mjs +339 -0
  44. package/dist/claudeLocal-CRGG4Pea.cjs +343 -0
  45. package/dist/claudeLocalLauncher-D4T7MTqd.mjs +152 -0
  46. package/dist/claudeLocalLauncher-DyEbVzUN.cjs +154 -0
  47. package/dist/cliUpdate-DsKXPFYp.cjs +120 -0
  48. package/dist/cliUpdate-Y34VGe9Y.mjs +112 -0
  49. package/dist/client-3Ac-O4pw.cjs +104 -0
  50. package/dist/client-BPNbmtvO.cjs +119 -0
  51. package/dist/client-CeGKqqcJ.mjs +100 -0
  52. package/dist/client-ClR0K8zX.mjs +112 -0
  53. package/dist/codex/happyMcpStdioBridge.cjs +33 -14
  54. package/dist/codex/happyMcpStdioBridge.mjs +26 -7
  55. package/dist/codexAppServerClient-I3teiV-N.mjs +1088 -0
  56. package/dist/codexAppServerClient-q2FjnRcK.cjs +1090 -0
  57. package/dist/codexCommand-4zcccIoq.cjs +2088 -0
  58. package/dist/codexCommand-Cc9gcbdX.mjs +2086 -0
  59. package/dist/codexThreadFork-BH9IZPqD.cjs +350 -0
  60. package/dist/codexThreadFork-slzAseLx.mjs +338 -0
  61. package/dist/{config-ClLtM6F3.mjs → config-CTx_jb-C.mjs} +7 -40
  62. package/dist/{config-DLx-vW3I.cjs → config-Ci9Kpgwa.cjs} +26 -59
  63. package/dist/configuration-BSYpcPwE.cjs +181 -0
  64. package/dist/configuration-RfeNjFmd.mjs +170 -0
  65. package/dist/connect-BpOdzNbE.mjs +682 -0
  66. package/dist/connect-C2CiOUFs.cjs +684 -0
  67. package/dist/constants-BC4St0xd.mjs +11 -0
  68. package/dist/constants-VJ0N6WuG.cjs +17 -0
  69. package/dist/contentLogMetadata-BlWI0egu.mjs +51 -0
  70. package/dist/contentLogMetadata-ZSFk_NjQ.cjs +54 -0
  71. package/dist/context-DsDr6E2t.cjs +69 -0
  72. package/dist/context-H0Bcb9S_.mjs +65 -0
  73. package/dist/controlClient-BGbDcgWS.mjs +260 -0
  74. package/dist/controlClient-Bcwv3D21.cjs +279 -0
  75. package/dist/createSessionMetadata-BJxEnM-o.cjs +58 -0
  76. package/dist/createSessionMetadata-CVVkGjxV.mjs +54 -0
  77. package/dist/defaultPermissionMode-BnxJPVHT.mjs +4 -0
  78. package/dist/defaultPermissionMode-CxmTXUQR.cjs +7 -0
  79. package/dist/detectCLI-CSGMlV51.mjs +58 -0
  80. package/dist/detectCLI-DR9ZaN6B.cjs +61 -0
  81. package/dist/deterministicJson-C5CYTVpm.cjs +507 -0
  82. package/dist/deterministicJson-CzkFJWw1.mjs +502 -0
  83. package/dist/doctor-BPkYWSuq.mjs +75 -0
  84. package/dist/doctor-D2YJ-Yn2.cjs +395 -0
  85. package/dist/doctor-DOvByjbk.mjs +391 -0
  86. package/dist/doctor-u46qYYKX.cjs +79 -0
  87. package/dist/doctorReadiness-BCZGcN0a.cjs +151 -0
  88. package/dist/doctorReadiness-C208SMN9.mjs +142 -0
  89. package/dist/editPaths-BVfS_FFL.mjs +68 -0
  90. package/dist/editPaths-Bqp4OFPw.cjs +73 -0
  91. package/dist/ensureDaemonRunning-BCwNrMyv.cjs +64 -0
  92. package/dist/ensureDaemonRunning-Dqa2pOjY.mjs +61 -0
  93. package/dist/handleResumeCommand-BvXYf5El.mjs +240 -0
  94. package/dist/handleResumeCommand-C2cKchpX.cjs +245 -0
  95. package/dist/ids-B6ZC0HGt.cjs +13 -0
  96. package/dist/ids-DU6sLx4C.mjs +10 -0
  97. package/dist/index-B6UGRQ7h.mjs +102 -0
  98. package/dist/{index-BEOMECtE.cjs → index-B6pAdvJx.cjs} +121 -91
  99. package/dist/index-Bu5Zi_a9.cjs +182 -0
  100. package/dist/index-D3kI0hDR.cjs +107 -0
  101. package/dist/index-DA7yINuk.cjs +994 -0
  102. package/dist/index-DDeHXrIX.cjs +71 -0
  103. package/dist/index-DKLU_fD0.mjs +158 -0
  104. package/dist/{index-2mCaeKyf.mjs → index-DMZqWwsP.mjs} +68 -38
  105. package/dist/index-DVkMGPyS.mjs +68 -0
  106. package/dist/index-qdS1friN.mjs +949 -0
  107. package/dist/index.cjs +5 -46
  108. package/dist/index.mjs +5 -46
  109. package/dist/{install-BPhJppvd.mjs → install-BO_W7VhL.mjs} +37 -37
  110. package/dist/install-BgoC2e_2.cjs +88 -0
  111. package/dist/install-CFY3tYtJ.mjs +86 -0
  112. package/dist/{install-BOHrUaFM.cjs → install-Cs5qAPrB.cjs} +38 -38
  113. package/dist/{installPiTools-DuZ6jZvX.cjs → installPiTools-BKnXjrVu.cjs} +2 -3
  114. package/dist/{installPiTools-Cj7yU9Fc.mjs → installPiTools-B_CdM5_2.mjs} +1 -2
  115. package/dist/{installTerminalHooks-Cou3WUf3.cjs → installTerminalHooks-BQZbvxsZ.cjs} +5 -40
  116. package/dist/{installTerminalHooks-WeznjkuC.mjs → installTerminalHooks-By7gBMEF.mjs} +3 -38
  117. package/dist/lib.cjs +28 -19
  118. package/dist/lib.d.cts +12 -2
  119. package/dist/lib.d.mts +12 -2
  120. package/dist/lib.mjs +23 -14
  121. package/dist/lidState-BeDfxxEX.cjs +305 -0
  122. package/dist/lidState-CeEt442o.mjs +297 -0
  123. package/dist/limits-C-a7gr04.cjs +43 -0
  124. package/dist/limits-DIo7rEBR.mjs +36 -0
  125. package/dist/localHappyAgentAuth-C-HQyiv6.mjs +82 -0
  126. package/dist/localHappyAgentAuth-D6rFhaoc.cjs +86 -0
  127. package/dist/logger-6HYUEXNk.mjs +271 -0
  128. package/dist/logger-CS0mw3fa.cjs +275 -0
  129. package/dist/loginShellEnv-BkuYtExZ.cjs +182 -0
  130. package/dist/loginShellEnv-Dw8M7tkI.mjs +167 -0
  131. package/dist/machineMetadata-DyDbODYd.cjs +41 -0
  132. package/dist/machineMetadata-oOzBfDfs.mjs +38 -0
  133. package/dist/main-BkxJP6JQ.cjs +941 -0
  134. package/dist/main-BwNIAI1A.mjs +939 -0
  135. package/dist/manager-BQ4flvQZ.mjs +101 -0
  136. package/dist/manager-Cly8xVIR.cjs +105 -0
  137. package/dist/{mcp-Dy3f_K0h.mjs → mcp-B1A4I3TK.mjs} +27 -33
  138. package/dist/{mcp-CPYbtsYG.cjs → mcp-Hszd7qpS.cjs} +42 -48
  139. package/dist/messageBuffer-BWlGgR3A.mjs +71 -0
  140. package/dist/messageBuffer-fJMH6uY8.cjs +73 -0
  141. package/dist/mirrorProtocol-BNLtoWjO.mjs +170 -0
  142. package/dist/mirrorProtocol-BUtmg1ws.cjs +186 -0
  143. package/dist/oneShotPrompts-CL0dip9C.cjs +16 -0
  144. package/dist/oneShotPrompts-DT4p6RNd.mjs +12 -0
  145. package/dist/package-BhaKlnxz.mjs +171 -0
  146. package/dist/package-BwMrPwqH.cjs +173 -0
  147. package/dist/path-B7zW-WwE.cjs +18 -0
  148. package/dist/path-D2wdaNSk.mjs +15 -0
  149. package/dist/peerTools-BTqw97Gb.mjs +272 -0
  150. package/dist/peerTools-BvKtHXj8.cjs +283 -0
  151. package/dist/permissionMode-Cr5JCbB7.mjs +80 -0
  152. package/dist/permissionMode-CrV-XuMs.cjs +87 -0
  153. package/dist/persistence-HGW_ZVs9.mjs +372 -0
  154. package/dist/persistence-yKTH3-5X.cjs +414 -0
  155. package/dist/piExtension.mjs +2 -1
  156. package/dist/piRunnerArgs-BNloJCCg.cjs +66 -0
  157. package/dist/piRunnerArgs-BuX1UFWy.mjs +59 -0
  158. package/dist/{piRuntime-kLZNpAAl.mjs → piRuntime-CSGkwCKQ.mjs} +2 -2
  159. package/dist/{piRuntime-NtF0YqjH.cjs → piRuntime-C_H7MJCM.cjs} +4 -4
  160. package/dist/{piTerminal-B0fEOV_2.mjs → piTerminal-C8a1OrNQ.mjs} +32 -35
  161. package/dist/{piTerminal-9J0GAKzb.cjs → piTerminal-DNCZ6yTM.cjs} +32 -35
  162. package/dist/previewPath-B0rBdhy1.cjs +83 -0
  163. package/dist/previewPath-CXN4Vag0.mjs +81 -0
  164. package/dist/projectPath-BTBIMB57.mjs +10 -0
  165. package/dist/projectPath-ChLhdOyt.cjs +13 -0
  166. package/dist/registerCommonHandlers-Ccvsz7Ou.mjs +536 -0
  167. package/dist/registerCommonHandlers-DskirN-_.cjs +538 -0
  168. package/dist/registerKillSessionHandler-DZeLXWcB.cjs +361 -0
  169. package/dist/registerKillSessionHandler-GYvHdipX.mjs +358 -0
  170. package/dist/remoteSessionClient-B0LraSB9.cjs +634 -0
  171. package/dist/remoteSessionClient-CYnXRynE.mjs +612 -0
  172. package/dist/resources-BdFBQi8r.mjs +117 -0
  173. package/dist/resources-JDPugcDq.cjs +120 -0
  174. package/dist/run-CJzxoHVh.cjs +5762 -0
  175. package/dist/run-D3ifupU3.mjs +5739 -0
  176. package/dist/runClaude-Ba01qT-u.mjs +848 -0
  177. package/dist/runClaude-Da3RirzE.cjs +850 -0
  178. package/dist/runClaudeDeps-DOt79DjU.mjs +3624 -0
  179. package/dist/runClaudeDeps-DOzEBMls.cjs +3649 -0
  180. package/dist/{runGemini-DV6Ouadt.mjs → runGemini-BGeTz57m.mjs} +58 -31
  181. package/dist/{runGemini-F-mqx3_K.cjs → runGemini-DSr9LIxL.cjs} +202 -175
  182. package/dist/{runOpenClaw-Qqo9j63M.cjs → runOpenClaw-B1mTxN35.cjs} +64 -65
  183. package/dist/{runOpenClaw-D7MlPS5n.mjs → runOpenClaw-CogSedIc.mjs} +31 -32
  184. package/dist/sandbox-CG7IyDTU.cjs +187 -0
  185. package/dist/sandbox-DTe6d54R.mjs +180 -0
  186. package/dist/selfCheck-C9mVrQwf.mjs +84 -0
  187. package/dist/selfCheck-Dc-mNSLb.cjs +87 -0
  188. package/dist/{send-4qJTCAYW.cjs → send-B7J_eaG3.cjs} +16 -9
  189. package/dist/{send-CLhYXNgy.mjs → send-CBXJKc6O.mjs} +12 -5
  190. package/dist/server-CZTP_iOn.cjs +407 -0
  191. package/dist/server-DcVBv04l.mjs +404 -0
  192. package/dist/sessionLock-Bys1PNSo.cjs +205 -0
  193. package/dist/sessionLock-D-EG2CfG.mjs +197 -0
  194. package/dist/sessionModeFile-DMl3dw3r.mjs +32 -0
  195. package/dist/sessionModeFile-RSm0NQkH.cjs +36 -0
  196. package/dist/sessionOps-Dn8z85h_.cjs +783 -0
  197. package/dist/sessionOps-SuHW8Dfe.mjs +760 -0
  198. package/dist/sessionScanner-BOrGjMXL.cjs +209 -0
  199. package/dist/sessionScanner-CX3onqXh.mjs +206 -0
  200. package/dist/{sessions-CzIH2rGp.mjs → sessions-BEG_HTSi.mjs} +17 -8
  201. package/dist/{sessions-Bh1pyddi.cjs → sessions-D74aJbhL.cjs} +37 -28
  202. package/dist/setupOfflineReconnection-ByElY60m.cjs +74 -0
  203. package/dist/setupOfflineReconnection-DpvWTt69.mjs +72 -0
  204. package/dist/smallFastModel-BLNgMrwR.mjs +18 -0
  205. package/dist/smallFastModel-D_uxGMo3.cjs +21 -0
  206. package/dist/{spawn-CPIWlPOM.mjs → spawn-B7yro0b2.mjs} +26 -38
  207. package/dist/{spawn-CmFOSVdW.cjs → spawn-DgR0RKRs.cjs} +52 -64
  208. package/dist/spawnAgents-BGQiEF-w.cjs +52 -0
  209. package/dist/spawnAgents-DZzZ9jMI.mjs +45 -0
  210. package/dist/spawnHappyCLI-B2H5s4B_.mjs +64 -0
  211. package/dist/spawnHappyCLI-DBhwZT7O.cjs +66 -0
  212. package/dist/specialCommands-BSqhYg5X.mjs +52 -0
  213. package/dist/specialCommands-DSsTeRej.cjs +54 -0
  214. package/dist/startFileWatcher-B4VsLgjD.cjs +71 -0
  215. package/dist/startFileWatcher-DErVWZE4.mjs +69 -0
  216. package/dist/startupPlan-BLMbfQIH.cjs +12 -0
  217. package/dist/startupPlan-DFMyNgAs.mjs +10 -0
  218. package/dist/streamRelay-BiKw59Cc.cjs +233 -0
  219. package/dist/streamRelay-FPVbHpbM.mjs +231 -0
  220. package/dist/sync-C8gyQFMc.mjs +69 -0
  221. package/dist/sync-DccIKZ5_.cjs +71 -0
  222. package/dist/systemPrompt-BwTPWfMu.cjs +97 -0
  223. package/dist/systemPrompt-DX521JJz.mjs +94 -0
  224. package/dist/{teams-CEahj4cm.mjs → teams-C32F1hqd.mjs} +9 -40
  225. package/dist/{teams-CB1R9-Z9.cjs → teams-Dl8g8kTI.cjs} +14 -45
  226. package/dist/template-BaHAmH2K.cjs +129 -0
  227. package/dist/template-CiOPa2rd.mjs +121 -0
  228. package/dist/{terminalTitleSuggest-DKgX3osW.mjs → terminalTitleSuggest-CeKsX_V6.mjs} +1 -9
  229. package/dist/{terminalTitleSuggest-Dji8I0Jy.cjs → terminalTitleSuggest-DhvRFX3H.cjs} +0 -10
  230. package/dist/terminals-B_bfC9c2.cjs +250 -0
  231. package/dist/terminals-Dlg0x3nY.mjs +235 -0
  232. package/dist/time-D0XyHOid.mjs +38 -0
  233. package/dist/time-DCuTyook.cjs +42 -0
  234. package/dist/titleGenerator-CQ8CoGhg.mjs +166 -0
  235. package/dist/titleGenerator-ChzMJsyg.cjs +172 -0
  236. package/dist/tmuxSocket-B8Yx20sT.mjs +19 -0
  237. package/dist/tmuxSocket-D-Xt6Vn-.cjs +24 -0
  238. package/dist/{todo-DqyRI4wP.cjs → todo-BvUOTa8G.cjs} +15 -12
  239. package/dist/{todo-Cknp5D4U.mjs → todo-CZ6cFrrn.mjs} +11 -8
  240. package/dist/tools-BZtHF_qQ.mjs +177 -0
  241. package/dist/tools-Be3AoAwu.cjs +63 -0
  242. package/dist/tools-DQj0AeLk.cjs +180 -0
  243. package/dist/tools-DkluDcG4.mjs +60 -0
  244. package/dist/trimIdent-4gvk9Wk6.cjs +22 -0
  245. package/dist/trimIdent-BAjmv_TG.mjs +20 -0
  246. package/dist/types-CRS-srZK.cjs +66 -0
  247. package/dist/types-CkqAACXx.mjs +64 -0
  248. package/dist/uninstall-BkLzaJ6I.mjs +47 -0
  249. package/dist/uninstall-x-TUtf5H.cjs +49 -0
  250. package/package.json +7 -7
  251. package/dist/agentGuidance-BRDN1NoZ.cjs +0 -2217
  252. package/dist/agentGuidance-KgvYuF3R.mjs +0 -2145
  253. package/dist/index-BF6WfgV5.mjs +0 -19624
  254. package/dist/index-C-JazrMB.mjs +0 -52
  255. package/dist/index-blDSadUR.cjs +0 -58
  256. package/dist/index-rH3DrQ5B.cjs +0 -19688
  257. package/dist/limits-BH5CV5vR.cjs +0 -2683
  258. package/dist/limits-CjRj8IX5.mjs +0 -2558
@@ -0,0 +1,3339 @@
1
+ import axios from 'axios';
2
+ import { l as logger, r as redactLogValue } from './logger-6HYUEXNk.mjs';
3
+ import { EventEmitter } from 'node:events';
4
+ import { io } from 'socket.io-client';
5
+ import { A as AsyncLock, a as MAX_CHAT_ATTACHMENT_ENCRYPTED_BYTES, U as UserMessageSchema, F as FileEventMessageSchema } from './attachmentLimits-yba1uHFO.mjs';
6
+ import { h as decrypt, d as decodeBase64, i as decryptBlob, e as encodeBase64, g as encrypt, l as libsodiumEncryptForPublicKey, j as libsodiumPublicKeyFromSecretKey, k as getRandomBytes } from './authRelay-DSAvg1uv.mjs';
7
+ import { p as prepareClipboardText, d as prepareClipboardHistoryText } from './limits-DIo7rEBR.mjs';
8
+ import { b as backoff, d as delay, e as exponentialBackoffDelay } from './time-D0XyHOid.mjs';
9
+ import { R as RpcHandlerManager, a as answerSocketRequest, d as describeSocketAckDrop, s as safeAck, i as isRateQuotaCode, p as pauseForRateQuota, b as shouldReconnect } from './lidState-CeEt442o.mjs';
10
+ import { c as configuration } from './configuration-RfeNjFmd.mjs';
11
+ import { createHmac, randomUUID } from 'node:crypto';
12
+ import { e as createEnvelope, s as streamKeyOf, R as ReleaseDrainNoticeSchema, m as RelayAssignmentResponseSchema } from './index-qdS1friN.mjs';
13
+ import { r as registerCommonHandlers } from './registerCommonHandlers-Ccvsz7Ou.mjs';
14
+ import { createId } from '@paralleldrive/cuid2';
15
+ import { I as InvalidateSync } from './sync-C8gyQFMc.mjs';
16
+ import { B as BackgroundTaskReporter } from './backgroundTaskActivity-8PdPxrvy.mjs';
17
+ import { h as notifyDaemonTurnEvent, j as notifyDaemonBackgroundTasks } from './controlClient-BGbDcgWS.mjs';
18
+ import { c as contentLogMetadata } from './contentLogMetadata-BlWI0egu.mjs';
19
+ import chalk from 'chalk';
20
+
21
+ async function hmac_sha512(key, data) {
22
+ const hmac = createHmac("sha512", key);
23
+ hmac.update(data);
24
+ return new Uint8Array(hmac.digest());
25
+ }
26
+
27
+ async function deriveSecretKeyTreeRoot(seed, usage) {
28
+ const I = await hmac_sha512(new TextEncoder().encode(usage + " Master Seed"), seed);
29
+ return {
30
+ key: I.slice(0, 32),
31
+ chainCode: I.slice(32)
32
+ };
33
+ }
34
+ async function deriveSecretKeyTreeChild(chainCode, index) {
35
+ const data = new Uint8Array([0, ...new TextEncoder().encode(index)]);
36
+ const I = await hmac_sha512(chainCode, data);
37
+ return {
38
+ key: I.subarray(0, 32),
39
+ chainCode: I.subarray(32)
40
+ };
41
+ }
42
+ async function deriveKey(master, usage, path) {
43
+ let state = await deriveSecretKeyTreeRoot(master, usage);
44
+ let remaining = [...path];
45
+ while (remaining.length > 0) {
46
+ let index = remaining[0];
47
+ remaining = remaining.slice(1);
48
+ state = await deriveSecretKeyTreeChild(state.chainCode, index);
49
+ }
50
+ return state.key;
51
+ }
52
+
53
+ const PRICING = {
54
+ // --- Claude Fable / Mythos (same tier, same price) ---
55
+ "claude-fable-5-1": { input: 10, output: 50, cache_write: 12.5, cache_read: 0.25 },
56
+ "claude-fable-5": { input: 10, output: 50, cache_write: 12.5, cache_read: 1 },
57
+ "claude-mythos-5-1": { input: 10, output: 50, cache_write: 12.5, cache_read: 0.25 },
58
+ "claude-mythos-5": { input: 10, output: 50, cache_write: 12.5, cache_read: 1 },
59
+ // --- Claude 5 ---
60
+ "claude-opus-5-5": { input: 4, output: 20, cache_write: 5, cache_read: 0.2 },
61
+ "claude-opus-5": { input: 5, output: 25, cache_write: 6.25, cache_read: 0.5 },
62
+ "claude-sonnet-5": { input: 2, output: 10, cache_write: 2.5, cache_read: 0.2 },
63
+ // --- Claude 4.x ---
64
+ "claude-opus-4-8": { input: 5, output: 25, cache_write: 6.25, cache_read: 0.5 },
65
+ "claude-opus-4-7": { input: 5, output: 25, cache_write: 6.25, cache_read: 0.5 },
66
+ "claude-opus-4-6": { input: 5, output: 25, cache_write: 6.25, cache_read: 0.5 },
67
+ "claude-opus-4-5": { input: 5, output: 25, cache_write: 6.25, cache_read: 0.5 },
68
+ "claude-opus-4-1": { input: 15, output: 75, cache_write: 18.75, cache_read: 1.5 },
69
+ "claude-opus-4": { input: 15, output: 75, cache_write: 18.75, cache_read: 1.5 },
70
+ "claude-sonnet-4-6": { input: 3, output: 15, cache_write: 3.75, cache_read: 0.3 },
71
+ "claude-sonnet-4-5": { input: 3, output: 15, cache_write: 3.75, cache_read: 0.3 },
72
+ "claude-sonnet-4": { input: 3, output: 15, cache_write: 3.75, cache_read: 0.3 },
73
+ "claude-haiku-4-5": { input: 1, output: 5, cache_write: 1.25, cache_read: 0.1 },
74
+ // --- Legacy / Claude 3 ---
75
+ "claude-3-opus-20240229": { input: 15, output: 75, cache_write: 18.75, cache_read: 1.5 },
76
+ "claude-3-sonnet-20240229": { input: 3, output: 15, cache_write: 3.75, cache_read: 0.3 },
77
+ "claude-3-5-sonnet-20240620": { input: 3, output: 15, cache_write: 3.75, cache_read: 0.3 },
78
+ "claude-3-5-sonnet-20241022": { input: 3, output: 15, cache_write: 3.75, cache_read: 0.3 },
79
+ "claude-3-7-sonnet-20250219": { input: 3, output: 15, cache_write: 3.75, cache_read: 0.3 },
80
+ "claude-3-haiku-20240307": { input: 0.25, output: 1.25, cache_write: 0.3125, cache_read: 0.025 },
81
+ "claude-3-5-haiku-20241022": { input: 0.8, output: 4, cache_write: 1, cache_read: 0.08 }
82
+ };
83
+ const DEFAULT_MODEL = "claude-opus-5-5";
84
+ const FAMILY_DEFAULTS = {
85
+ fable: "claude-fable-5-1",
86
+ // Project Glasswing counterpart of Fable: same tier, same published rates.
87
+ mythos: "claude-mythos-5-1",
88
+ opus: "claude-opus-5-5",
89
+ sonnet: "claude-sonnet-5",
90
+ haiku: "claude-haiku-4-5"
91
+ };
92
+ function resolvePricingKey(modelId) {
93
+ if (!modelId) return DEFAULT_MODEL;
94
+ const raw = modelId.trim().toLowerCase();
95
+ if (raw in PRICING) return raw;
96
+ const id = raw.replace(/\[1m\]$/, "");
97
+ if (id in PRICING) return id;
98
+ const family = ["fable", "mythos", "opus", "sonnet", "haiku"].find((f) => id.includes(f));
99
+ if (!family) return DEFAULT_MODEL;
100
+ const version = id.match(/(?:^|[^0-9])(\d)(?:[-._](\d))?(?:[^0-9]|$)/);
101
+ const major = version ? version[1] : null;
102
+ const minor = version && version[2] ? version[2] : null;
103
+ const key = major ? `claude-${family}-${major}${minor ? `-${minor}` : ""}` : null;
104
+ if (key && key in PRICING) return key;
105
+ if (major === "3" && family !== "fable" && family !== "mythos") {
106
+ if (family === "opus") return "claude-3-opus-20240229";
107
+ if (family === "haiku") return minor === "5" ? "claude-3-5-haiku-20241022" : "claude-3-haiku-20240307";
108
+ if (minor === "7") return "claude-3-7-sonnet-20250219";
109
+ return minor === "5" ? "claude-3-5-sonnet-20241022" : "claude-3-sonnet-20240229";
110
+ }
111
+ return FAMILY_DEFAULTS[family];
112
+ }
113
+ function calculateCost(usage, modelId) {
114
+ const pricing = PRICING[resolvePricingKey(modelId)];
115
+ const inputCost = usage.input_tokens / 1e6 * pricing.input;
116
+ const outputCost = usage.output_tokens / 1e6 * pricing.output;
117
+ const cacheWriteCost = (usage.cache_creation_input_tokens || 0) / 1e6 * pricing.cache_write;
118
+ const cacheReadCost = (usage.cache_read_input_tokens || 0) / 1e6 * pricing.cache_read;
119
+ const totalInputCost = inputCost + cacheWriteCost + cacheReadCost;
120
+ return {
121
+ total: totalInputCost + outputCost,
122
+ input: totalInputCost,
123
+ output: outputCost
124
+ };
125
+ }
126
+
127
+ const SUBAGENT_RESULT_MAX_CHARS = 16384;
128
+ const SUBAGENT_PROGRESS_MIN_INTERVAL_MS = 5e3;
129
+ function capSubagentText(text, max = SUBAGENT_RESULT_MAX_CHARS) {
130
+ const trimmed = text.trim();
131
+ if (trimmed.length <= max) return { text: trimmed };
132
+ return { text: trimmed.slice(0, max), truncated: true };
133
+ }
134
+ function toolUseResultToText(toolUseResult) {
135
+ if (!toolUseResult || typeof toolUseResult !== "object") return null;
136
+ const content = toolUseResult.content;
137
+ if (typeof content === "string") return content;
138
+ if (!Array.isArray(content)) return null;
139
+ const parts = [];
140
+ for (const block of content) {
141
+ if (block && typeof block === "object" && block.type === "text" && typeof block.text === "string") {
142
+ parts.push(block.text);
143
+ }
144
+ }
145
+ return parts.length > 0 ? parts.join("\n") : null;
146
+ }
147
+ function toolUseResultStatus(toolUseResult) {
148
+ if (!toolUseResult || typeof toolUseResult !== "object") return "unknown";
149
+ const status = toolUseResult.status;
150
+ if (status === "async_launched") return "async_launched";
151
+ if (status === "completed") return "completed";
152
+ return "unknown";
153
+ }
154
+ function toolUseResultStats(toolUseResult) {
155
+ if (!toolUseResult || typeof toolUseResult !== "object") return void 0;
156
+ const r = toolUseResult;
157
+ const stats = {};
158
+ if (typeof r.totalToolUseCount === "number") stats.toolUses = r.totalToolUseCount;
159
+ if (typeof r.totalTokens === "number") stats.totalTokens = r.totalTokens;
160
+ if (typeof r.totalDurationMs === "number") stats.durationMs = r.totalDurationMs;
161
+ if (r.toolStats && typeof r.toolStats === "object") stats.toolStats = r.toolStats;
162
+ return Object.keys(stats).length > 0 ? stats : void 0;
163
+ }
164
+ function parseTaskNotificationText(text) {
165
+ if (!/<task-notification>/i.test(text)) return null;
166
+ const pick = (tag) => text.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`, "i"))?.[1] ?? null;
167
+ const rawStatus = pick("status")?.trim().toLowerCase() ?? null;
168
+ const status = rawStatus === "completed" || rawStatus === "failed" || rawStatus === "stopped" ? rawStatus : null;
169
+ let resultText = pick("result");
170
+ let result = null;
171
+ if (resultText !== null) {
172
+ resultText = resultText.replace(/^\s*\[harness:[^\]]*\]\s*/i, "").replace(/<usage>[\s\S]*?<\/usage>/gi, "").trim();
173
+ if (resultText.length > 0) result = capSubagentText(resultText);
174
+ }
175
+ return {
176
+ toolUseId: pick("tool-use-id")?.trim() || null,
177
+ taskId: pick("task-id")?.trim() || null,
178
+ status,
179
+ summary: pick("summary")?.trim() || null,
180
+ result
181
+ };
182
+ }
183
+ function shouldEmitProgress(previous, toolUses, now) {
184
+ if (!previous) return true;
185
+ if (previous.lastToolUses === toolUses) return false;
186
+ return now - previous.lastAt >= SUBAGENT_PROGRESS_MIN_INTERVAL_MS;
187
+ }
188
+
189
+ const SUBAGENT_BUFFER_MAX_MESSAGES = 100;
190
+ function isSubagentTool(name) {
191
+ return name === "Task" || name === "Agent";
192
+ }
193
+ function shouldHideParentToolCall(name) {
194
+ return name === "Task";
195
+ }
196
+ function pickProviderSubagent(message) {
197
+ const raw = message;
198
+ if (typeof raw.parent_tool_use_id === "string" && raw.parent_tool_use_id.length > 0) {
199
+ return raw.parent_tool_use_id;
200
+ }
201
+ if (typeof raw.parentToolUseId === "string" && raw.parentToolUseId.length > 0) {
202
+ return raw.parentToolUseId;
203
+ }
204
+ return void 0;
205
+ }
206
+ function getUuidToProviderSubagent(state) {
207
+ if (!state.uuidToProviderSubagent) {
208
+ state.uuidToProviderSubagent = /* @__PURE__ */ new Map();
209
+ }
210
+ return state.uuidToProviderSubagent;
211
+ }
212
+ function getTaskPromptToSubagents(state) {
213
+ if (!state.taskPromptToSubagents) {
214
+ state.taskPromptToSubagents = /* @__PURE__ */ new Map();
215
+ }
216
+ return state.taskPromptToSubagents;
217
+ }
218
+ function getProviderSubagentToSessionSubagent(state) {
219
+ if (!state.providerSubagentToSessionSubagent) {
220
+ state.providerSubagentToSessionSubagent = /* @__PURE__ */ new Map();
221
+ }
222
+ return state.providerSubagentToSessionSubagent;
223
+ }
224
+ function getSessionSubagentIdForProviderSubagent(state, providerSubagent) {
225
+ return getProviderSubagentToSessionSubagent(state).get(providerSubagent);
226
+ }
227
+ function ensureSessionSubagentIdForProviderSubagent(state, providerSubagent) {
228
+ const existing = getSessionSubagentIdForProviderSubagent(state, providerSubagent);
229
+ if (existing) {
230
+ return existing;
231
+ }
232
+ const created = createId();
233
+ getProviderSubagentToSessionSubagent(state).set(providerSubagent, created);
234
+ return created;
235
+ }
236
+ function getSubagentTitles(state) {
237
+ if (!state.subagentTitles) {
238
+ state.subagentTitles = /* @__PURE__ */ new Map();
239
+ }
240
+ return state.subagentTitles;
241
+ }
242
+ function getBufferedSubagentMessages(state) {
243
+ if (!state.bufferedSubagentMessages) {
244
+ state.bufferedSubagentMessages = /* @__PURE__ */ new Map();
245
+ }
246
+ return state.bufferedSubagentMessages;
247
+ }
248
+ function getHiddenParentToolCalls(state) {
249
+ if (!state.hiddenParentToolCalls) {
250
+ state.hiddenParentToolCalls = /* @__PURE__ */ new Set();
251
+ }
252
+ return state.hiddenParentToolCalls;
253
+ }
254
+ function bufferSubagentMessage(state, subagent, message) {
255
+ const buffer = getBufferedSubagentMessages(state);
256
+ const queue = buffer.get(subagent) ?? [];
257
+ queue.push(message);
258
+ while (queue.length > SUBAGENT_BUFFER_MAX_MESSAGES) queue.shift();
259
+ buffer.set(subagent, queue);
260
+ }
261
+ function consumeBufferedSubagentMessages(state, subagent) {
262
+ const buffer = getBufferedSubagentMessages(state);
263
+ const queue = buffer.get(subagent) ?? [];
264
+ buffer.delete(subagent);
265
+ return queue;
266
+ }
267
+ function getStartedSubagents(state) {
268
+ if (!state.startedSubagents) {
269
+ state.startedSubagents = /* @__PURE__ */ new Set();
270
+ }
271
+ return state.startedSubagents;
272
+ }
273
+ function getActiveSubagents(state) {
274
+ if (!state.activeSubagents) {
275
+ state.activeSubagents = /* @__PURE__ */ new Set();
276
+ }
277
+ return state.activeSubagents;
278
+ }
279
+ function getAgentToolCalls(state) {
280
+ if (!state.agentToolCalls) state.agentToolCalls = /* @__PURE__ */ new Set();
281
+ return state.agentToolCalls;
282
+ }
283
+ function getSubagentMeta(state) {
284
+ if (!state.subagentMeta) state.subagentMeta = /* @__PURE__ */ new Map();
285
+ return state.subagentMeta;
286
+ }
287
+ function getStubSubagents(state) {
288
+ if (!state.stubSubagents) state.stubSubagents = /* @__PURE__ */ new Set();
289
+ return state.stubSubagents;
290
+ }
291
+ function getProgressThrottle(state) {
292
+ if (!state.progressThrottle) state.progressThrottle = /* @__PURE__ */ new Map();
293
+ return state.progressThrottle;
294
+ }
295
+ function nowOf(state) {
296
+ return state.now ? state.now() : Date.now();
297
+ }
298
+ function rememberSubagentMeta(state, subagent, meta) {
299
+ const existing = getSubagentMeta(state).get(subagent) ?? {};
300
+ const description = typeof meta.description === "string" && meta.description.trim() ? meta.description.trim() : existing.description;
301
+ const subagentType = typeof meta.subagentType === "string" && meta.subagentType.trim() ? meta.subagentType.trim() : existing.subagentType;
302
+ getSubagentMeta(state).set(subagent, { ...description ? { description } : {}, ...subagentType ? { subagentType } : {} });
303
+ }
304
+ function pickAssistantUsage(message) {
305
+ if (message.type !== "assistant") return void 0;
306
+ const usage = message.message?.usage;
307
+ if (!usage || typeof usage.input_tokens !== "number" || typeof usage.output_tokens !== "number") return void 0;
308
+ return {
309
+ input_tokens: usage.input_tokens,
310
+ output_tokens: usage.output_tokens,
311
+ ...typeof usage.cache_creation_input_tokens === "number" ? { cache_creation_input_tokens: usage.cache_creation_input_tokens } : {},
312
+ ...typeof usage.cache_read_input_tokens === "number" ? { cache_read_input_tokens: usage.cache_read_input_tokens } : {}
313
+ };
314
+ }
315
+ function pickUuid(message) {
316
+ const raw = message;
317
+ if (typeof raw.uuid === "string" && raw.uuid.length > 0) {
318
+ return raw.uuid;
319
+ }
320
+ return void 0;
321
+ }
322
+ function pickParentUuid(message) {
323
+ const raw = message;
324
+ if (typeof raw.parentUuid === "string" && raw.parentUuid.length > 0) {
325
+ return raw.parentUuid;
326
+ }
327
+ if (typeof raw.parentUUID === "string" && raw.parentUUID.length > 0) {
328
+ return raw.parentUUID;
329
+ }
330
+ return void 0;
331
+ }
332
+ function isSidechainMessage(message) {
333
+ const raw = message;
334
+ return raw.isSidechain === true;
335
+ }
336
+ function normalizePrompt(prompt) {
337
+ return prompt.trim();
338
+ }
339
+ function queueTaskPromptSubagent(state, prompt, subagent) {
340
+ const normalized = normalizePrompt(prompt);
341
+ if (normalized.length === 0) {
342
+ return;
343
+ }
344
+ const promptMap = getTaskPromptToSubagents(state);
345
+ const queue = promptMap.get(normalized) ?? [];
346
+ if (!queue.includes(subagent)) {
347
+ queue.push(subagent);
348
+ }
349
+ promptMap.set(normalized, queue);
350
+ }
351
+ function consumeTaskPromptSubagent(state, prompt) {
352
+ const normalized = normalizePrompt(prompt);
353
+ if (normalized.length === 0) {
354
+ return void 0;
355
+ }
356
+ const promptMap = getTaskPromptToSubagents(state);
357
+ const queue = promptMap.get(normalized);
358
+ if (!queue || queue.length === 0) {
359
+ return void 0;
360
+ }
361
+ const subagent = queue.shift();
362
+ if (queue.length === 0) {
363
+ promptMap.delete(normalized);
364
+ }
365
+ return subagent;
366
+ }
367
+ function consumeSinglePendingTaskSubagent(state) {
368
+ const promptMap = getTaskPromptToSubagents(state);
369
+ let candidateKey = null;
370
+ let candidateSubagent = null;
371
+ for (const [prompt, queue2] of promptMap.entries()) {
372
+ if (queue2.length === 0) {
373
+ continue;
374
+ }
375
+ if (candidateKey !== null) {
376
+ return void 0;
377
+ }
378
+ candidateKey = prompt;
379
+ candidateSubagent = queue2[0] ?? null;
380
+ }
381
+ if (!candidateKey || !candidateSubagent) {
382
+ return void 0;
383
+ }
384
+ const queue = promptMap.get(candidateKey);
385
+ if (!queue || queue.length === 0) {
386
+ return void 0;
387
+ }
388
+ queue.shift();
389
+ if (queue.length === 0) {
390
+ promptMap.delete(candidateKey);
391
+ }
392
+ return candidateSubagent;
393
+ }
394
+ function pickSidechainRootPrompt(message) {
395
+ if (message.type !== "user") {
396
+ return void 0;
397
+ }
398
+ if (typeof message.message?.content === "string") {
399
+ const normalized = normalizePrompt(message.message.content);
400
+ return normalized.length > 0 ? normalized : void 0;
401
+ }
402
+ return void 0;
403
+ }
404
+ function resolveProviderSubagent(message, state) {
405
+ const explicitSubagent = pickProviderSubagent(message);
406
+ if (explicitSubagent) {
407
+ return explicitSubagent;
408
+ }
409
+ const parentUuid = pickParentUuid(message);
410
+ if (parentUuid) {
411
+ const inheritedSubagent = getUuidToProviderSubagent(state).get(parentUuid);
412
+ if (inheritedSubagent) {
413
+ return inheritedSubagent;
414
+ }
415
+ }
416
+ if (!isSidechainMessage(message)) {
417
+ return void 0;
418
+ }
419
+ const prompt = pickSidechainRootPrompt(message);
420
+ if (prompt) {
421
+ const matchedSubagent = consumeTaskPromptSubagent(state, prompt);
422
+ if (matchedSubagent) {
423
+ return matchedSubagent;
424
+ }
425
+ }
426
+ if (!parentUuid) {
427
+ return consumeSinglePendingTaskSubagent(state);
428
+ }
429
+ return void 0;
430
+ }
431
+ function rememberSubagentForMessage(message, state, providerSubagent) {
432
+ if (!providerSubagent) {
433
+ return;
434
+ }
435
+ const uuid = pickUuid(message);
436
+ if (!uuid) {
437
+ return;
438
+ }
439
+ getUuidToProviderSubagent(state).set(uuid, providerSubagent);
440
+ }
441
+ function pickTaskPrompt(input) {
442
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
443
+ return void 0;
444
+ }
445
+ const prompt = input.prompt;
446
+ if (typeof prompt !== "string") {
447
+ return void 0;
448
+ }
449
+ const normalized = normalizePrompt(prompt);
450
+ return normalized.length > 0 ? normalized : void 0;
451
+ }
452
+ function pickTaskTitle(input) {
453
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
454
+ return void 0;
455
+ }
456
+ const candidateKeys = ["description", "title", "subagent_type"];
457
+ for (const key of candidateKeys) {
458
+ const value = input[key];
459
+ if (typeof value === "string" && value.trim().length > 0) {
460
+ return value.trim();
461
+ }
462
+ }
463
+ return void 0;
464
+ }
465
+ function setSubagentTitle(state, subagent, title) {
466
+ if (!title || title.trim().length === 0) {
467
+ return;
468
+ }
469
+ getSubagentTitles(state).set(subagent, title.trim());
470
+ }
471
+ function maybeEmitSubagentStart(state, turn, subagent, envelopes) {
472
+ if (!subagent) {
473
+ return;
474
+ }
475
+ const started = getStartedSubagents(state);
476
+ if (started.has(subagent)) {
477
+ return;
478
+ }
479
+ const title = getSubagentTitles(state).get(subagent);
480
+ const meta = getSubagentMeta(state).get(subagent);
481
+ envelopes.push(createEnvelope("agent", {
482
+ t: "start",
483
+ ...title ? { title } : {},
484
+ ...meta?.description ? { description: meta.description } : {},
485
+ ...meta?.subagentType ? { subagentType: meta.subagentType } : {}
486
+ }, { turn, subagent }));
487
+ started.add(subagent);
488
+ getActiveSubagents(state).add(subagent);
489
+ }
490
+ function emitSubagentStopWithStatus(state, turn, subagent, envelopes, payload) {
491
+ envelopes.push(createEnvelope("agent", {
492
+ t: "stop",
493
+ ...payload.status ? { status: payload.status } : {},
494
+ ...payload.result ? { result: payload.result } : {},
495
+ ...payload.usage ? { usage: payload.usage } : {}
496
+ }, { ...turn ? { turn } : {}, subagent }));
497
+ getActiveSubagents(state).delete(subagent);
498
+ getStartedSubagents(state).delete(subagent);
499
+ }
500
+ function maybeEmitSubagentStop(state, turn, subagent, envelopes) {
501
+ const active = getActiveSubagents(state);
502
+ if (!active.has(subagent)) {
503
+ return;
504
+ }
505
+ envelopes.push(createEnvelope("agent", { t: "stop" }, { turn, subagent }));
506
+ active.delete(subagent);
507
+ }
508
+ function clearSubagentTracking(state) {
509
+ getUuidToProviderSubagent(state).clear();
510
+ getTaskPromptToSubagents(state).clear();
511
+ getBufferedSubagentMessages(state).clear();
512
+ getHiddenParentToolCalls(state).clear();
513
+ getStartedSubagents(state).clear();
514
+ getActiveSubagents(state).clear();
515
+ }
516
+ function ensureTurn(state, envelopes) {
517
+ if (state.currentTurnId) {
518
+ return state.currentTurnId;
519
+ }
520
+ const turnId = createId();
521
+ envelopes.push(createEnvelope("agent", { t: "turn-start" }, { turn: turnId }));
522
+ state.currentTurnId = turnId;
523
+ return turnId;
524
+ }
525
+ function closeTurn(state, status, envelopes, meta) {
526
+ if (!state.currentTurnId) {
527
+ return;
528
+ }
529
+ envelopes.push(createEnvelope("agent", {
530
+ t: "turn-end",
531
+ status,
532
+ ...meta?.error ? { error: meta.error } : {},
533
+ ...typeof meta?.costUsd === "number" ? { costUsd: meta.costUsd } : {},
534
+ ...typeof meta?.durationMs === "number" ? { durationMs: meta.durationMs } : {},
535
+ ...typeof meta?.numTurns === "number" ? { numTurns: meta.numTurns } : {},
536
+ ...meta?.usage ? { usage: meta.usage } : {}
537
+ }, { turn: state.currentTurnId }));
538
+ state.currentTurnId = null;
539
+ state.pendingAssistantError = void 0;
540
+ clearSubagentTracking(state);
541
+ }
542
+ function toolTitle(name, input) {
543
+ if (input && typeof input === "object") {
544
+ const description = input.description;
545
+ if (typeof description === "string" && description.trim().length > 0) {
546
+ return description.length > 80 ? `${description.slice(0, 77)}...` : description;
547
+ }
548
+ }
549
+ return `${name} call`;
550
+ }
551
+ function toToolArgs(input) {
552
+ if (input && typeof input === "object" && !Array.isArray(input)) {
553
+ return input;
554
+ }
555
+ if (input === void 0) {
556
+ return {};
557
+ }
558
+ return { input };
559
+ }
560
+ function closeClaudeTurnWithStatus(state, status, meta) {
561
+ const envelopes = [];
562
+ closeTurn(state, status, envelopes, meta);
563
+ return {
564
+ currentTurnId: state.currentTurnId,
565
+ envelopes
566
+ };
567
+ }
568
+ function mapClaudeLogMessageToSessionEnvelopes(message, state) {
569
+ return mapClaudeLogMessageToSessionEnvelopesInternal(message, state);
570
+ }
571
+ function mapTaskLifecycleSystemMessage(message, state, envelopes) {
572
+ const raw = message;
573
+ const subtype = typeof raw.subtype === "string" ? raw.subtype : "";
574
+ if (!subtype.startsWith("task_")) return;
575
+ if (raw.skip_transcript === true) return;
576
+ const toolUseId = typeof raw.tool_use_id === "string" && raw.tool_use_id.length > 0 ? raw.tool_use_id : null;
577
+ if (!toolUseId) return;
578
+ if (subtype === "task_started" && raw.task_type === "local_bash" && state.bashToolCalls?.has(toolUseId)) {
579
+ (state.backgroundBashCalls ??= /* @__PURE__ */ new Set()).add(toolUseId);
580
+ }
581
+ const backgroundBash = state.backgroundBashCalls?.has(toolUseId) === true;
582
+ if (!getAgentToolCalls(state).has(toolUseId) && !backgroundBash) return;
583
+ const subagent = ensureSessionSubagentIdForProviderSubagent(state, toolUseId);
584
+ const usage = raw.usage && typeof raw.usage === "object" ? {
585
+ ...typeof raw.usage.tool_uses === "number" ? { toolUses: raw.usage.tool_uses } : {},
586
+ ...typeof raw.usage.total_tokens === "number" ? { totalTokens: raw.usage.total_tokens } : {},
587
+ ...typeof raw.usage.duration_ms === "number" ? { durationMs: raw.usage.duration_ms } : {}
588
+ } : void 0;
589
+ if (subtype === "task_started") {
590
+ rememberSubagentMeta(state, subagent, { description: raw.description, subagentType: backgroundBash ? "background-command" : raw.subagent_type });
591
+ const turn = ensureTurn(state, envelopes);
592
+ getStartedSubagents(state).delete(subagent);
593
+ maybeEmitSubagentStart(state, turn, subagent, envelopes);
594
+ return;
595
+ }
596
+ if (subtype === "task_progress") {
597
+ const toolUses = typeof raw.usage?.tool_uses === "number" ? raw.usage.tool_uses : 0;
598
+ const throttle = getProgressThrottle(state);
599
+ const now = nowOf(state);
600
+ if (!shouldEmitProgress(throttle.get(subagent), toolUses, now)) return;
601
+ throttle.set(subagent, { lastAt: now, lastToolUses: toolUses });
602
+ const turn = ensureTurn(state, envelopes);
603
+ maybeEmitSubagentStart(state, turn, subagent, envelopes);
604
+ envelopes.push(createEnvelope("agent", {
605
+ t: "progress",
606
+ toolUses,
607
+ ...typeof raw.last_tool_name === "string" && raw.last_tool_name ? { lastTool: raw.last_tool_name } : {},
608
+ ...usage?.totalTokens !== void 0 ? { totalTokens: usage.totalTokens } : {},
609
+ ...usage?.durationMs !== void 0 ? { durationMs: usage.durationMs } : {},
610
+ ...typeof raw.summary === "string" && raw.summary.trim() ? { summary: raw.summary.trim() } : {}
611
+ }, { turn, subagent }));
612
+ return;
613
+ }
614
+ if (subtype === "task_notification") {
615
+ const status = raw.status === "completed" || raw.status === "failed" || raw.status === "stopped" ? raw.status : "completed";
616
+ emitSubagentStopWithStatus(state, state.currentTurnId ?? void 0, subagent, envelopes, {
617
+ status,
618
+ ...usage ? { usage } : {}
619
+ });
620
+ getStubSubagents(state).delete(subagent);
621
+ getProgressThrottle(state).delete(subagent);
622
+ return;
623
+ }
624
+ if (subtype === "task_updated" && raw.patch?.is_backgrounded === true) {
625
+ getStubSubagents(state).add(subagent);
626
+ }
627
+ }
628
+ function mapClaudeLogMessageToSessionEnvelopesInternal(message, state) {
629
+ const envelopes = [];
630
+ const claudeUuid = pickUuid(message);
631
+ const providerSubagent = resolveProviderSubagent(message, state);
632
+ const subagent = providerSubagent ? getSessionSubagentIdForProviderSubagent(state, providerSubagent) : void 0;
633
+ rememberSubagentForMessage(message, state, providerSubagent);
634
+ if (providerSubagent && !subagent) {
635
+ bufferSubagentMessage(state, providerSubagent, message);
636
+ return {
637
+ currentTurnId: state.currentTurnId,
638
+ envelopes: []
639
+ };
640
+ }
641
+ if (message.type === "summary") {
642
+ return {
643
+ currentTurnId: state.currentTurnId,
644
+ envelopes
645
+ };
646
+ }
647
+ if (message.type === "system") {
648
+ mapTaskLifecycleSystemMessage(message, state, envelopes);
649
+ return {
650
+ currentTurnId: state.currentTurnId,
651
+ envelopes
652
+ };
653
+ }
654
+ if (message.isCompactSummary) {
655
+ return {
656
+ currentTurnId: state.currentTurnId,
657
+ envelopes
658
+ };
659
+ }
660
+ if (message.type === "assistant") {
661
+ const assistantError = message.error;
662
+ if (!message.isSidechain && typeof assistantError === "string" && assistantError.trim()) {
663
+ state.pendingAssistantError = assistantError.trim();
664
+ }
665
+ const turnId = ensureTurn(state, envelopes);
666
+ maybeEmitSubagentStart(state, turnId, subagent, envelopes);
667
+ const usage = pickAssistantUsage(message);
668
+ const blocks = Array.isArray(message.message?.content) ? message.message.content : [];
669
+ const apiMessageId = !isSidechainMessage(message) && typeof message.message?.id === "string" && message.message.id.length > 0 ? message.message.id : void 0;
670
+ let blockCursor = 0;
671
+ if (apiMessageId) {
672
+ if (state.streamBlockMessageId !== apiMessageId) {
673
+ state.streamBlockMessageId = apiMessageId;
674
+ state.streamBlockCursor = 0;
675
+ }
676
+ blockCursor = state.streamBlockCursor ?? 0;
677
+ state.streamBlockCursor = blockCursor + blocks.length;
678
+ }
679
+ const actualModel = subagent && typeof message.message?.model === "string" && message.message.model.trim() ? { actualModel: message.message.model.trim() } : {};
680
+ const draftKey = (index) => apiMessageId ? streamKeyOf(apiMessageId, blockCursor + index) : void 0;
681
+ for (const [blockIndex, block] of blocks.entries()) {
682
+ if (block.type === "text" && typeof block.text === "string" && block.text.trim().length > 0) {
683
+ envelopes.push(createEnvelope("agent", { t: "text", text: block.text, ...actualModel }, { turn: turnId, subagent, claudeUuid, usage, streamKey: draftKey(blockIndex) }));
684
+ continue;
685
+ }
686
+ if (block.type === "thinking" && typeof block.thinking === "string" && block.thinking.trim().length > 0) {
687
+ envelopes.push(createEnvelope("agent", { t: "text", text: block.thinking, thinking: true, ...actualModel }, { turn: turnId, subagent, claudeUuid, usage, streamKey: draftKey(blockIndex) }));
688
+ continue;
689
+ }
690
+ if (block.type === "tool_use") {
691
+ const call = typeof block.id === "string" && block.id.length > 0 ? block.id : createId();
692
+ const name = typeof block.name === "string" && block.name.length > 0 ? block.name : "unknown";
693
+ const baseArgs = toToolArgs(block.input);
694
+ const title = toolTitle(name, block.input);
695
+ const sessionSubagentForCall = ensureSessionSubagentIdForProviderSubagent(state, call);
696
+ if (isSubagentTool(name)) {
697
+ const prompt = pickTaskPrompt(block.input);
698
+ if (prompt) {
699
+ queueTaskPromptSubagent(state, prompt, call);
700
+ }
701
+ setSubagentTitle(state, sessionSubagentForCall, pickTaskTitle(block.input) ?? prompt);
702
+ getAgentToolCalls(state).add(call);
703
+ const input = block.input ?? {};
704
+ rememberSubagentMeta(state, sessionSubagentForCall, { description: input.description, subagentType: input.subagent_type });
705
+ }
706
+ if (shouldHideParentToolCall(name)) {
707
+ getHiddenParentToolCalls(state).add(call);
708
+ const buffered2 = consumeBufferedSubagentMessages(state, call);
709
+ for (const bufferedMessage of buffered2) {
710
+ const replay = mapClaudeLogMessageToSessionEnvelopesInternal(bufferedMessage, state);
711
+ envelopes.push(...replay.envelopes);
712
+ }
713
+ continue;
714
+ }
715
+ if (name === "Bash") (state.bashToolCalls ??= /* @__PURE__ */ new Set()).add(call);
716
+ const args = isSubagentTool(name) || name === "Bash" ? { ...baseArgs, sessionSubagent: sessionSubagentForCall } : baseArgs;
717
+ envelopes.push(createEnvelope("agent", {
718
+ t: "tool-call-start",
719
+ call,
720
+ name,
721
+ title,
722
+ description: title,
723
+ args
724
+ }, { turn: turnId, subagent, usage }));
725
+ const buffered = consumeBufferedSubagentMessages(state, call);
726
+ for (const bufferedMessage of buffered) {
727
+ const replay = mapClaudeLogMessageToSessionEnvelopesInternal(bufferedMessage, state);
728
+ envelopes.push(...replay.envelopes);
729
+ }
730
+ }
731
+ }
732
+ return {
733
+ currentTurnId: state.currentTurnId,
734
+ envelopes
735
+ };
736
+ }
737
+ if (message.type === "user") {
738
+ if (message.isMeta) {
739
+ return {
740
+ currentTurnId: state.currentTurnId,
741
+ envelopes
742
+ };
743
+ }
744
+ if (typeof message.message.content === "string") {
745
+ if (message.isSidechain) {
746
+ const turnId2 = ensureTurn(state, envelopes);
747
+ maybeEmitSubagentStart(state, turnId2, subagent, envelopes);
748
+ envelopes.push(createEnvelope("agent", { t: "text", text: message.message.content }, { turn: turnId2, subagent, claudeUuid }));
749
+ } else {
750
+ const origin = message.origin;
751
+ if (origin?.kind === "task-notification" || /<task-notification>/i.test(message.message.content)) {
752
+ const fields = parseTaskNotificationText(message.message.content);
753
+ const providerId = fields?.toolUseId ?? null;
754
+ const subagentForNotification = providerId && getAgentToolCalls(state).has(providerId) ? getSessionSubagentIdForProviderSubagent(state, providerId) : void 0;
755
+ if (fields && subagentForNotification && (fields.result || fields.status)) {
756
+ emitSubagentStopWithStatus(state, state.currentTurnId ?? void 0, subagentForNotification, envelopes, {
757
+ ...fields.status ? { status: fields.status } : {},
758
+ ...fields.result ? { result: fields.result } : {}
759
+ });
760
+ }
761
+ }
762
+ closeTurn(state, "completed", envelopes);
763
+ envelopes.push(createEnvelope("user", { t: "text", text: message.message.content }, { claudeUuid }));
764
+ }
765
+ return {
766
+ currentTurnId: state.currentTurnId,
767
+ envelopes
768
+ };
769
+ }
770
+ const blocks = Array.isArray(message.message.content) ? message.message.content : [];
771
+ if (blocks.length === 0) {
772
+ return {
773
+ currentTurnId: state.currentTurnId,
774
+ envelopes
775
+ };
776
+ }
777
+ const turnId = ensureTurn(state, envelopes);
778
+ if (message.isSidechain) {
779
+ maybeEmitSubagentStart(state, turnId, subagent, envelopes);
780
+ }
781
+ for (const block of blocks) {
782
+ if (block.type === "tool_result" && typeof block.tool_use_id === "string" && block.tool_use_id.length > 0) {
783
+ const sessionSubagentForToolResult = getSessionSubagentIdForProviderSubagent(state, block.tool_use_id);
784
+ if (!message.isSidechain) {
785
+ if (getHiddenParentToolCalls(state).has(block.tool_use_id)) {
786
+ if (sessionSubagentForToolResult) {
787
+ maybeEmitSubagentStop(state, turnId, sessionSubagentForToolResult, envelopes);
788
+ }
789
+ getHiddenParentToolCalls(state).delete(block.tool_use_id);
790
+ continue;
791
+ }
792
+ if (sessionSubagentForToolResult && getAgentToolCalls(state).has(block.tool_use_id)) {
793
+ const toolUseResult = message.tool_use_result;
794
+ const resultStatus = toolUseResultStatus(toolUseResult);
795
+ if (resultStatus === "async_launched") {
796
+ getStubSubagents(state).add(sessionSubagentForToolResult);
797
+ envelopes.push(createEnvelope("agent", { t: "tool-call-end", call: block.tool_use_id }, { turn: turnId, subagent }));
798
+ continue;
799
+ }
800
+ if (resultStatus === "completed") {
801
+ const text = toolUseResultToText(toolUseResult);
802
+ const stats = toolUseResultStats(toolUseResult);
803
+ const result = text ? capSubagentText(text) : void 0;
804
+ emitSubagentStopWithStatus(state, turnId, sessionSubagentForToolResult, envelopes, {
805
+ status: "completed",
806
+ ...result ? { result } : {},
807
+ ...stats ? { usage: { toolUses: stats.toolUses, totalTokens: stats.totalTokens, durationMs: stats.durationMs } } : {}
808
+ });
809
+ envelopes.push(createEnvelope("agent", {
810
+ t: "tool-call-end",
811
+ call: block.tool_use_id,
812
+ ...result ? { result: { ...result, ...stats ? { stats } : {} } } : {}
813
+ }, { turn: turnId, subagent }));
814
+ continue;
815
+ }
816
+ }
817
+ if (sessionSubagentForToolResult && !state.backgroundBashCalls?.has(block.tool_use_id)) {
818
+ maybeEmitSubagentStop(state, turnId, sessionSubagentForToolResult, envelopes);
819
+ }
820
+ }
821
+ envelopes.push(createEnvelope("agent", {
822
+ t: "tool-call-end",
823
+ call: block.tool_use_id
824
+ }, { turn: turnId, subagent }));
825
+ continue;
826
+ }
827
+ if (block.type === "text" && typeof block.text === "string" && block.text.trim().length > 0) {
828
+ envelopes.push(createEnvelope("agent", { t: "text", text: block.text }, { turn: turnId, subagent, claudeUuid }));
829
+ }
830
+ }
831
+ return {
832
+ currentTurnId: state.currentTurnId,
833
+ envelopes
834
+ };
835
+ }
836
+ if (message.type === "result") {
837
+ const raw = message;
838
+ const status = raw.interrupted === true ? "cancelled" : raw.is_error === true ? "failed" : "completed";
839
+ const error = status === "failed" ? raw.errors?.filter((item) => typeof item === "string" && item.trim()).join("\n") || state.pendingAssistantError || raw.subtype || "Claude turn failed" : void 0;
840
+ if (status === "failed" && !state.currentTurnId) {
841
+ ensureTurn(state, envelopes);
842
+ }
843
+ const usage = raw.usage && typeof raw.usage.input_tokens === "number" && typeof raw.usage.output_tokens === "number" ? {
844
+ input_tokens: raw.usage.input_tokens,
845
+ output_tokens: raw.usage.output_tokens,
846
+ ...typeof raw.usage.cache_creation_input_tokens === "number" ? { cache_creation_input_tokens: raw.usage.cache_creation_input_tokens } : {},
847
+ ...typeof raw.usage.cache_read_input_tokens === "number" ? { cache_read_input_tokens: raw.usage.cache_read_input_tokens } : {}
848
+ } : void 0;
849
+ closeTurn(state, status, envelopes, {
850
+ error,
851
+ costUsd: typeof raw.total_cost_usd === "number" ? raw.total_cost_usd : void 0,
852
+ durationMs: typeof raw.duration_ms === "number" ? raw.duration_ms : void 0,
853
+ numTurns: typeof raw.num_turns === "number" ? raw.num_turns : void 0,
854
+ usage
855
+ });
856
+ return {
857
+ currentTurnId: state.currentTurnId,
858
+ envelopes
859
+ };
860
+ }
861
+ return {
862
+ currentTurnId: state.currentTurnId,
863
+ envelopes
864
+ };
865
+ }
866
+
867
+ function sessionEnvelopeOf(body) {
868
+ if (!body || typeof body !== "object") return null;
869
+ const record = body;
870
+ if (record.role !== "session" || !record.content || typeof record.content !== "object") return null;
871
+ const content = record.content;
872
+ if (content.type === "session" && content.data && typeof content.data === "object") {
873
+ return content.data;
874
+ }
875
+ if (content.ev !== void 0) return content;
876
+ return null;
877
+ }
878
+ function isQueuedUserText(body) {
879
+ if (!body || typeof body !== "object") return false;
880
+ const record = body;
881
+ return record.role === "user" && record.content?.type === "text" && typeof record.meta?.queuedAt === "number";
882
+ }
883
+ function scanUndeliveredQueuedInputs(records, prior) {
884
+ const canceled = prior?.canceled ?? /* @__PURE__ */ new Set();
885
+ const undelivered = [];
886
+ let reachedBoundary = false;
887
+ for (const record of records) {
888
+ const envelope = sessionEnvelopeOf(record.body);
889
+ if (envelope) {
890
+ const t = envelope.ev?.t;
891
+ if (t === "turn-end") {
892
+ reachedBoundary = true;
893
+ break;
894
+ }
895
+ if (t === "queue-cancel" && Array.isArray(envelope.ev?.targetLocalKeys)) {
896
+ for (const key of envelope.ev.targetLocalKeys) {
897
+ if (typeof key === "string") canceled.add(key);
898
+ }
899
+ }
900
+ continue;
901
+ }
902
+ if (isQueuedUserText(record.body) && record.localId && !canceled.has(record.localId)) {
903
+ undelivered.push(record.localId);
904
+ }
905
+ }
906
+ return { undeliveredLocalKeys: undelivered, reachedBoundary, canceled };
907
+ }
908
+
909
+ function record(value) {
910
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
911
+ }
912
+ function finiteNonNegative(value) {
913
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
914
+ }
915
+ function firstNumber(source, keys) {
916
+ for (const key of keys) {
917
+ const value = finiteNonNegative(source[key]);
918
+ if (value !== void 0) return value;
919
+ }
920
+ return void 0;
921
+ }
922
+ function normalizeAgentUsage(raw) {
923
+ const outer = record(raw);
924
+ if (!outer) return null;
925
+ const wrapped = record(outer.totalTokenUsage) ?? record(outer.total_token_usage) ?? record(outer.total) ?? record(outer.usage) ?? outer;
926
+ const input = firstNumber(wrapped, ["inputTokens", "input_tokens", "input"]);
927
+ const output = firstNumber(wrapped, ["outputTokens", "output_tokens", "output"]);
928
+ const cacheRead = firstNumber(wrapped, ["cachedInputTokens", "cached_input_tokens", "cachedReadTokens", "cacheReadInputTokens", "cache_read_input_tokens", "cache_read"]);
929
+ const cacheCreation = firstNumber(wrapped, ["cacheCreationInputTokens", "cache_creation_input_tokens", "cachedWriteTokens", "cacheWriteInputTokens", "cache_write_input_tokens", "cache_creation"]);
930
+ const reasoning = firstNumber(wrapped, ["reasoningOutputTokens", "reasoning_output_tokens", "thoughtTokens", "reasoning"]);
931
+ const explicitTotal = firstNumber(wrapped, ["totalTokens", "total_tokens", "total"]);
932
+ const fallbackTotal = input !== void 0 || output !== void 0 ? (input ?? 0) + (output ?? 0) : void 0;
933
+ const total = explicitTotal ?? fallbackTotal;
934
+ if (total === void 0) return null;
935
+ return {
936
+ total,
937
+ ...input !== void 0 ? { input } : {},
938
+ ...output !== void 0 ? { output } : {},
939
+ ...cacheCreation !== void 0 ? { cache_creation: cacheCreation } : {},
940
+ ...cacheRead !== void 0 ? { cache_read: cacheRead } : {},
941
+ ...reasoning !== void 0 ? { reasoning } : {}
942
+ };
943
+ }
944
+ function usageAgentKey(agent) {
945
+ return agent.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
946
+ }
947
+
948
+ const TURN_LEASE_MS = 6e4;
949
+ const TURN_BUSY_TTL_MS = 15e4;
950
+ class TurnActivityTracker {
951
+ constructor(ttlMs = TURN_BUSY_TTL_MS) {
952
+ this.ttlMs = ttlMs;
953
+ }
954
+ busy = /* @__PURE__ */ new Map();
955
+ apply(sessionId, event, now = Date.now()) {
956
+ if (event === "turn_started") this.busy.set(sessionId, now);
957
+ else this.busy.delete(sessionId);
958
+ }
959
+ /** The wrapper is gone: whatever it reported no longer holds. */
960
+ forget(sessionId) {
961
+ this.busy.delete(sessionId);
962
+ }
963
+ /** Sessions whose last renewal is still inside the TTL. */
964
+ activeSessions(now = Date.now()) {
965
+ const out = [];
966
+ for (const [id, at] of this.busy) {
967
+ if (now - at <= this.ttlMs) out.push(id);
968
+ else this.busy.delete(id);
969
+ }
970
+ return out;
971
+ }
972
+ hasActiveTurn(now = Date.now()) {
973
+ return this.activeSessions(now).length > 0;
974
+ }
975
+ }
976
+ class TurnReporter {
977
+ constructor(send, leaseMs = TURN_LEASE_MS) {
978
+ this.send = send;
979
+ this.leaseMs = leaseMs;
980
+ }
981
+ thinking = false;
982
+ lastStartedAt = 0;
983
+ /** Called on every keepAlive tick; emits only on an edge or a due renewal. */
984
+ observe(thinking, now = Date.now()) {
985
+ if (thinking) {
986
+ const due = !this.thinking || now - this.lastStartedAt >= this.leaseMs;
987
+ this.thinking = true;
988
+ if (!due) return null;
989
+ this.lastStartedAt = now;
990
+ this.send("turn_started");
991
+ return "turn_started";
992
+ }
993
+ if (!this.thinking) return null;
994
+ this.thinking = false;
995
+ this.lastStartedAt = 0;
996
+ this.send("turn_ended");
997
+ return "turn_ended";
998
+ }
999
+ }
1000
+
1001
+ const PROMPT_QUEUE_INFLIGHT_TIMEOUT_MS = 2e4;
1002
+ const PROMPT_QUEUE_RETRY_MIN_MS = 1e3;
1003
+ const PROMPT_QUEUE_RETRY_MAX_MS = 3e4;
1004
+ class PromptQueueDrain {
1005
+ constructor(deps) {
1006
+ this.deps = deps;
1007
+ this.now = deps.now ?? (() => Date.now());
1008
+ this.log = deps.log ?? (() => {
1009
+ });
1010
+ this.schedule = deps.schedule ?? ((fn, delayMs) => {
1011
+ const timer = setTimeout(fn, delayMs);
1012
+ timer.unref?.();
1013
+ });
1014
+ this.inflightTimeoutMs = deps.inflightTimeoutMs ?? PROMPT_QUEUE_INFLIGHT_TIMEOUT_MS;
1015
+ }
1016
+ inflight = null;
1017
+ /**
1018
+ * localIds routed while a dispatch call was still in flight. The server's
1019
+ * `new-message` echo regularly beats the HTTP response (both come off the
1020
+ * same transaction), so `onInbound` can precede the `dispatched` outcome;
1021
+ * without this the guard armed AFTER the message had arrived and sat
1022
+ * until the timeout — observed in the first isolated e2e run.
1023
+ */
1024
+ arrivedDuringDispatch = /* @__PURE__ */ new Set();
1025
+ /** Items the server still holds, from the last snapshot / dispatch response. */
1026
+ pending = 0;
1027
+ disabled = false;
1028
+ closed = false;
1029
+ busy = false;
1030
+ failures = 0;
1031
+ retryAt = 0;
1032
+ retryScheduled = false;
1033
+ now;
1034
+ log;
1035
+ schedule;
1036
+ inflightTimeoutMs;
1037
+ /** The runner is blocked on an empty input queue. */
1038
+ onIdle() {
1039
+ void this.maybeDispatch("idle");
1040
+ }
1041
+ /** A `prompt-queue` snapshot arrived; only a non-empty queue is worth a call. */
1042
+ onQueueChanged(count) {
1043
+ this.pending = Math.max(0, count);
1044
+ if (count > 0) void this.maybeDispatch("queue-changed");
1045
+ }
1046
+ /**
1047
+ * True while the server still holds queued prompts for this session. The
1048
+ * runners fold it into their "idle, waiting for the user" judgement so a
1049
+ * turn that ends with more queue behind it raises no reply_done /
1050
+ * input_needed notification and no daemon `completed` report per item.
1051
+ */
1052
+ hasPending() {
1053
+ return !this.disabled && !this.closed && this.pending > 0;
1054
+ }
1055
+ /** A user message with this localId was routed into the runner's queue. */
1056
+ onInbound(localId) {
1057
+ if (this.inflight?.localId === localId) {
1058
+ this.inflight = null;
1059
+ return;
1060
+ }
1061
+ if (this.busy) {
1062
+ this.arrivedDuringDispatch.add(localId);
1063
+ if (this.arrivedDuringDispatch.size > 64) {
1064
+ const oldest = this.arrivedDuringDispatch.values().next().value;
1065
+ if (oldest !== void 0) this.arrivedDuringDispatch.delete(oldest);
1066
+ }
1067
+ }
1068
+ }
1069
+ close() {
1070
+ this.closed = true;
1071
+ }
1072
+ state() {
1073
+ return { inflight: this.inflight, disabled: this.disabled, busy: this.busy, failures: this.failures, retryAt: this.retryAt, pending: this.pending };
1074
+ }
1075
+ async maybeDispatch(reason) {
1076
+ if (this.closed || this.disabled || this.busy) return false;
1077
+ const now = this.now();
1078
+ if (this.inflight) {
1079
+ if (now - this.inflight.since < this.inflightTimeoutMs) return false;
1080
+ this.log(`[prompt-queue] inflight ${this.inflight.localId} not routed within ${this.inflightTimeoutMs}ms; releasing`);
1081
+ this.inflight = null;
1082
+ }
1083
+ if (now < this.retryAt) {
1084
+ this.scheduleRetry(this.retryAt - now);
1085
+ return false;
1086
+ }
1087
+ if (!this.deps.isIdle()) return false;
1088
+ this.busy = true;
1089
+ this.arrivedDuringDispatch.clear();
1090
+ try {
1091
+ const outcome = await this.deps.dispatch();
1092
+ switch (outcome.kind) {
1093
+ case "dispatched": {
1094
+ this.failures = 0;
1095
+ if (typeof outcome.remaining === "number") this.pending = Math.max(0, outcome.remaining);
1096
+ const alreadyRouted = this.arrivedDuringDispatch.delete(outcome.localId) || outcome.created === false;
1097
+ if (!alreadyRouted) {
1098
+ this.inflight = { localId: outcome.localId, since: this.now() };
1099
+ this.schedule(() => {
1100
+ void this.maybeDispatch("inflight-timeout");
1101
+ }, this.inflightTimeoutMs + 1);
1102
+ }
1103
+ this.log(`[prompt-queue] dispatched ${outcome.localId} (${reason}${alreadyRouted ? ", already routed" : ""})`);
1104
+ return true;
1105
+ }
1106
+ case "empty":
1107
+ this.failures = 0;
1108
+ this.pending = 0;
1109
+ return false;
1110
+ case "unsupported":
1111
+ this.disabled = true;
1112
+ this.log("[prompt-queue] server has no prompt queue; drain disabled for this process");
1113
+ return false;
1114
+ case "error": {
1115
+ this.failures += 1;
1116
+ const delay = Math.min(PROMPT_QUEUE_RETRY_MAX_MS, PROMPT_QUEUE_RETRY_MIN_MS * 2 ** (this.failures - 1));
1117
+ this.retryAt = this.now() + delay;
1118
+ this.log(`[prompt-queue] dispatch failed (${outcome.message}); retry in ${delay}ms`);
1119
+ this.scheduleRetry(delay);
1120
+ return false;
1121
+ }
1122
+ }
1123
+ } finally {
1124
+ this.busy = false;
1125
+ this.arrivedDuringDispatch.clear();
1126
+ }
1127
+ }
1128
+ scheduleRetry(delayMs) {
1129
+ if (this.retryScheduled || this.closed) return;
1130
+ this.retryScheduled = true;
1131
+ this.schedule(() => {
1132
+ this.retryScheduled = false;
1133
+ void this.maybeDispatch("retry");
1134
+ }, delayMs);
1135
+ }
1136
+ }
1137
+
1138
+ class ApiSessionClient extends EventEmitter {
1139
+ /**
1140
+ * B-512: the session socket now connects before the runner has attached its
1141
+ * `archived` listener (registerKillSessionHandler). An archive signal that
1142
+ * fires in that window must not be lost: remember it and replay it to any
1143
+ * listener attached later.
1144
+ */
1145
+ archiveSignalled = false;
1146
+ emit(event, ...args) {
1147
+ if (event === "archived") this.archiveSignalled = true;
1148
+ return super.emit(event, ...args);
1149
+ }
1150
+ on(event, listener) {
1151
+ super.on(event, listener);
1152
+ if (event === "archived" && this.archiveSignalled) queueMicrotask(() => listener());
1153
+ return this;
1154
+ }
1155
+ token;
1156
+ sessionId;
1157
+ metadata;
1158
+ metadataVersion;
1159
+ agentState;
1160
+ agentStateVersion;
1161
+ socket;
1162
+ relaySocket = null;
1163
+ pendingMessages = [];
1164
+ pendingMessageCallback = null;
1165
+ pendingFileEvents = [];
1166
+ pendingFileEventCallback = null;
1167
+ blobKey = null;
1168
+ /**
1169
+ * In-flight attachment download promises that belong to the *current*
1170
+ * (not-yet-drained) batch. Each promise resolves to the decoded blob (or
1171
+ * null on failure), so per-message ownership is intrinsic — there is no
1172
+ * shared push-array between batches that a late download could leak into.
1173
+ */
1174
+ pendingDownloads = [];
1175
+ rpcHandlerManager;
1176
+ agentStateLock = new AsyncLock();
1177
+ metadataLock = new AsyncLock();
1178
+ encryptionKey;
1179
+ encryptionVariant;
1180
+ reconnectInterval = null;
1181
+ ignoreArchiveSignal = false;
1182
+ skipInitialMessages = false;
1183
+ claudeSessionProtocolState = {
1184
+ currentTurnId: null,
1185
+ uuidToProviderSubagent: /* @__PURE__ */ new Map(),
1186
+ taskPromptToSubagents: /* @__PURE__ */ new Map(),
1187
+ providerSubagentToSessionSubagent: /* @__PURE__ */ new Map(),
1188
+ subagentTitles: /* @__PURE__ */ new Map(),
1189
+ bufferedSubagentMessages: /* @__PURE__ */ new Map(),
1190
+ hiddenParentToolCalls: /* @__PURE__ */ new Set(),
1191
+ startedSubagents: /* @__PURE__ */ new Set(),
1192
+ activeSubagents: /* @__PURE__ */ new Set()
1193
+ };
1194
+ seenClaudeUsageEvents = /* @__PURE__ */ new Set();
1195
+ claudeUsageTotals = {
1196
+ total: 0,
1197
+ input: 0,
1198
+ output: 0,
1199
+ cache_creation: 0,
1200
+ cache_read: 0,
1201
+ costTotal: 0,
1202
+ costInput: 0,
1203
+ costOutput: 0
1204
+ };
1205
+ lastSeq = 0;
1206
+ pendingOutbox = [];
1207
+ sendSync;
1208
+ receiveSync;
1209
+ directInboundLocalIds = /* @__PURE__ */ new Set();
1210
+ routedInboundLocalIds = /* @__PURE__ */ new Set();
1211
+ handoverInFlight = null;
1212
+ /** B-265 reconnect: the message cursor was seeded from the server, so
1213
+ * nothing before it is history to replay — but socket fast-path routing
1214
+ * must wait for the first fetch, otherwise a message that lands between
1215
+ * the seed and the first fetch is routed twice (socket + fetch). */
1216
+ awaitingInitialFetch = false;
1217
+ /** B-509: server-side prompt queue drain, attached by the runner. */
1218
+ promptQueueDrain = null;
1219
+ constructor(token, session, opts) {
1220
+ super();
1221
+ this.token = token;
1222
+ this.sessionId = session.id;
1223
+ if (opts?.initialSeq !== void 0 && opts.initialSeq >= 0) {
1224
+ this.lastSeq = opts.initialSeq;
1225
+ this.awaitingInitialFetch = true;
1226
+ }
1227
+ this.metadata = session.metadata;
1228
+ this.metadataVersion = session.metadataVersion;
1229
+ this.agentState = session.agentState;
1230
+ this.agentStateVersion = session.agentStateVersion;
1231
+ this.encryptionKey = session.encryptionKey;
1232
+ this.encryptionVariant = session.encryptionVariant;
1233
+ this.sendSync = new InvalidateSync(() => this.flushOutbox());
1234
+ this.receiveSync = new InvalidateSync(() => this.fetchMessages());
1235
+ this.rpcHandlerManager = new RpcHandlerManager({
1236
+ scopePrefix: this.sessionId,
1237
+ encryptionKey: this.encryptionKey,
1238
+ encryptionVariant: this.encryptionVariant,
1239
+ logger: (msg, data) => logger.debug(msg, data)
1240
+ });
1241
+ registerCommonHandlers(this.rpcHandlerManager, this.metadata.path);
1242
+ this.socket = this.createControlSocket();
1243
+ const controlSocket = this.socket;
1244
+ this.socket.on("connect", () => {
1245
+ logger.debug("Socket connected successfully");
1246
+ if (this.reconnectInterval) {
1247
+ clearInterval(this.reconnectInterval);
1248
+ this.reconnectInterval = null;
1249
+ }
1250
+ this.rpcHandlerManager.onSocketConnect(controlSocket);
1251
+ this.receiveSync.invalidate();
1252
+ void this.connectSessionRelay();
1253
+ void this.promptQueueDrain?.maybeDispatch("connect");
1254
+ });
1255
+ this.socket.on("rpc-request", async (data, callback) => {
1256
+ await answerSocketRequest(
1257
+ callback,
1258
+ () => this.rpcHandlerManager.handleRequest(data),
1259
+ (drop) => logger.debug(`[API] RPC ${data?.method} unanswered \u2014 ${describeSocketAckDrop(drop)}`)
1260
+ );
1261
+ });
1262
+ this.socket.on("disconnect", (reason) => {
1263
+ logger.debug(`[API] Socket disconnected: ${reason}`);
1264
+ this.rpcHandlerManager.onSocketDisconnect(controlSocket);
1265
+ this.startSmartReconnect();
1266
+ });
1267
+ this.socket.on("connect_error", (error) => {
1268
+ logger.debug("[API] Socket connection error:", error);
1269
+ this.rpcHandlerManager.onSocketDisconnect(controlSocket);
1270
+ if (error.message === "Session archived") {
1271
+ logger.debug("[API] Archived session rejected during reconnect; exiting");
1272
+ this.emit("archived");
1273
+ return;
1274
+ }
1275
+ this.startSmartReconnect();
1276
+ });
1277
+ this.socket.on("session-archive", ({ sessionId }) => {
1278
+ if (sessionId !== this.sessionId) return;
1279
+ logger.debug("[SOCKET] Server archived this session; exiting...");
1280
+ this.emit("archived");
1281
+ });
1282
+ this.socket.on("update", (data) => {
1283
+ try {
1284
+ logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", data);
1285
+ if (!data.body) {
1286
+ logger.debug("[SOCKET] [UPDATE] [ERROR] No body in update!");
1287
+ return;
1288
+ }
1289
+ if (data.body.t === "prompt-queue") {
1290
+ const items = data.body.items;
1291
+ this.promptQueueDrain?.onQueueChanged(Array.isArray(items) ? items.length : 0);
1292
+ return;
1293
+ }
1294
+ if (data.body.t === "new-message") {
1295
+ const messageSeq = data.body.message?.seq;
1296
+ if (this.lastSeq === 0 || this.awaitingInitialFetch) {
1297
+ this.receiveSync.invalidate();
1298
+ return;
1299
+ }
1300
+ if (typeof messageSeq !== "number" || messageSeq !== this.lastSeq + 1 || data.body.message.content.t !== "encrypted") {
1301
+ this.receiveSync.invalidate();
1302
+ return;
1303
+ }
1304
+ const localId = data.body.message.localId;
1305
+ const relayAccepted = !!localId && this.directInboundLocalIds.delete(localId);
1306
+ if (localId && this.routedInboundLocalIds.has(localId)) {
1307
+ this.lastSeq = messageSeq;
1308
+ return;
1309
+ }
1310
+ if (relayAccepted) {
1311
+ logger.debug(`[API] Routing relay-accepted message ${localId} from the central echo (own persist still pending or failed)`);
1312
+ }
1313
+ const body = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(data.body.message.content.c));
1314
+ logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", body);
1315
+ this.routeIncomingMessage(body, localId);
1316
+ if (localId) this.rememberRoutedInbound(localId);
1317
+ this.lastSeq = messageSeq;
1318
+ } else if (data.body.t === "update-session") {
1319
+ if (data.body.metadata && data.body.metadata.version > this.metadataVersion) {
1320
+ this.metadata = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(data.body.metadata.value));
1321
+ this.metadataVersion = data.body.metadata.version;
1322
+ const meta = this.metadata;
1323
+ if (meta?.lifecycleState === "archiveRequested" || meta?.lifecycleState === "archived") {
1324
+ if (this.ignoreArchiveSignal) {
1325
+ logger.debug(`[SOCKET] Session archived (${meta.lifecycleState}) but suppressed for reconnect`);
1326
+ this.ignoreArchiveSignal = false;
1327
+ } else {
1328
+ logger.debug(`[SOCKET] Session archived (${meta.lifecycleState}), exiting...`);
1329
+ this.emit("archived");
1330
+ }
1331
+ }
1332
+ }
1333
+ if (data.body.agentState && data.body.agentState.version > this.agentStateVersion) {
1334
+ this.agentState = data.body.agentState.value ? decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(data.body.agentState.value)) : null;
1335
+ this.agentStateVersion = data.body.agentState.version;
1336
+ }
1337
+ } else if (data.body.t === "update-machine") {
1338
+ logger.debug(`[SOCKET] WARNING: Session client received unexpected machine update - ignoring`);
1339
+ } else {
1340
+ this.emit("message", data.body);
1341
+ }
1342
+ } catch (error) {
1343
+ logger.debug("[SOCKET] [UPDATE] [ERROR] Error handling update", { error });
1344
+ }
1345
+ });
1346
+ this.socket.on("error", (error) => {
1347
+ logger.debug("[API] Socket error:", error);
1348
+ });
1349
+ this.socket.on("server-draining", (data) => {
1350
+ const parsed = ReleaseDrainNoticeSchema.safeParse(data);
1351
+ if (!parsed.success || parsed.data.deadline <= Date.now()) return;
1352
+ void this.startReleaseHandover(parsed.data);
1353
+ });
1354
+ this.socket.connect();
1355
+ }
1356
+ createControlSocket(handover) {
1357
+ return io(configuration.serverUrl, {
1358
+ auth: {
1359
+ token: this.token,
1360
+ clientType: "session-scoped",
1361
+ sessionId: this.sessionId,
1362
+ happyClient: `cli-coding-session/${configuration.currentCliVersion}`,
1363
+ ...handover ? { handoverEpoch: handover.epoch } : {}
1364
+ },
1365
+ ...handover ? { query: { vh_slot: handover.candidateSlot }, forceNew: true } : {},
1366
+ path: "/v1/updates",
1367
+ reconnection: false,
1368
+ transports: ["websocket"],
1369
+ withCredentials: true,
1370
+ autoConnect: false
1371
+ });
1372
+ }
1373
+ startReleaseHandover(notice) {
1374
+ if (this.handoverInFlight) return this.handoverInFlight;
1375
+ this.handoverInFlight = this.releaseHandover(notice).finally(() => {
1376
+ this.handoverInFlight = null;
1377
+ });
1378
+ return this.handoverInFlight;
1379
+ }
1380
+ async releaseHandover(notice) {
1381
+ const startedAt = Date.now();
1382
+ const previous = this.socket;
1383
+ const candidate = this.createControlSocket(notice);
1384
+ const transferredListeners = /* @__PURE__ */ new Map();
1385
+ for (const listener of previous.listeners("rpc-request")) candidate.on("rpc-request", listener);
1386
+ const timeoutMs = Math.max(1, Math.min(1e4, notice.deadline - Date.now()));
1387
+ try {
1388
+ await new Promise((resolve, reject) => {
1389
+ const timer = setTimeout(() => reject(new Error("release handover timeout")), timeoutMs);
1390
+ candidate.once("connect", () => {
1391
+ clearTimeout(timer);
1392
+ resolve();
1393
+ });
1394
+ candidate.once("connect_error", (error) => {
1395
+ clearTimeout(timer);
1396
+ reject(error);
1397
+ });
1398
+ candidate.connect();
1399
+ });
1400
+ await this.rpcHandlerManager.onSocketConnectAndWait(candidate, timeoutMs);
1401
+ if (this.socket !== previous) {
1402
+ this.rpcHandlerManager.onSocketDisconnect(candidate);
1403
+ candidate.close();
1404
+ return;
1405
+ }
1406
+ for (const event of ["session-archive", "update", "error", "server-draining"]) {
1407
+ const listeners = previous.listeners(event);
1408
+ previous.removeAllListeners(event);
1409
+ for (const listener of listeners) candidate.on(event, listener);
1410
+ transferredListeners.set(event, listeners);
1411
+ }
1412
+ this.socket = candidate;
1413
+ candidate.on("connect", () => {
1414
+ if (this.reconnectInterval) {
1415
+ clearInterval(this.reconnectInterval);
1416
+ this.reconnectInterval = null;
1417
+ }
1418
+ this.rpcHandlerManager.onSocketConnect(candidate);
1419
+ this.receiveSync.invalidate();
1420
+ });
1421
+ candidate.on("disconnect", () => {
1422
+ this.rpcHandlerManager.onSocketDisconnect(candidate);
1423
+ this.startSmartReconnect();
1424
+ });
1425
+ candidate.on("connect_error", () => {
1426
+ this.rpcHandlerManager.onSocketDisconnect(candidate);
1427
+ this.startSmartReconnect();
1428
+ });
1429
+ this.receiveSync.invalidate();
1430
+ candidate.emit("release-handover-result", { result: "success", durationMs: Date.now() - startedAt });
1431
+ this.rpcHandlerManager.onSocketDisconnect(previous);
1432
+ previous.removeAllListeners("disconnect");
1433
+ previous.close();
1434
+ } catch (error) {
1435
+ if (this.socket === candidate) this.socket = previous;
1436
+ for (const [event, listeners] of transferredListeners) {
1437
+ candidate.removeAllListeners(event);
1438
+ for (const listener of listeners) previous.on(event, listener);
1439
+ }
1440
+ this.rpcHandlerManager.onSocketDisconnect(candidate);
1441
+ candidate.close();
1442
+ if (previous.connected) previous.emit("release-handover-result", { result: "failed", durationMs: Date.now() - startedAt });
1443
+ logger.debug(`[API] Release handover failed: ${error instanceof Error ? error.message : error}`);
1444
+ }
1445
+ }
1446
+ onUserMessage(callback) {
1447
+ this.pendingMessageCallback = callback;
1448
+ while (this.pendingMessages.length > 0) {
1449
+ callback(this.pendingMessages.shift());
1450
+ }
1451
+ }
1452
+ onFileEvent(callback) {
1453
+ this.pendingFileEventCallback = callback;
1454
+ while (this.pendingFileEvents.length > 0) {
1455
+ callback(this.pendingFileEvents.shift());
1456
+ }
1457
+ }
1458
+ /**
1459
+ * Derive (and cache) the blob decryption key for this session.
1460
+ * Legacy sessions use deriveKey(masterSecret, 'Happy Blobs', ['master']).
1461
+ * DataKey sessions use deriveKey(dataKey, 'Happy Blobs', ['session']).
1462
+ */
1463
+ async getBlobKey() {
1464
+ if (!this.blobKey) {
1465
+ const path = this.encryptionVariant === "dataKey" ? ["session"] : ["master"];
1466
+ this.blobKey = await deriveKey(this.encryptionKey, "Happy Blobs", path);
1467
+ }
1468
+ return this.blobKey;
1469
+ }
1470
+ /**
1471
+ * Download an encrypted attachment blob via the request-download flow:
1472
+ * POST /request-download → { downloadUrl } → GET downloadUrl. Local mode
1473
+ * downloadUrl points back at our server (Bearer required); S3 mode is a
1474
+ * presigned URL that does not accept extra headers.
1475
+ */
1476
+ async downloadAttachment(ref) {
1477
+ const requestUrl = `${configuration.serverUrl}/v1/sessions/${this.sessionId}/attachments/request-download`;
1478
+ const requestRes = await axios.post(
1479
+ requestUrl,
1480
+ { ref },
1481
+ {
1482
+ headers: { "Authorization": `Bearer ${this.token}`, "Content-Type": "application/json" },
1483
+ timeout: 3e4
1484
+ }
1485
+ );
1486
+ const downloadUrl = requestRes.data?.downloadUrl;
1487
+ if (typeof downloadUrl !== "string") {
1488
+ throw new Error("request-download returned no downloadUrl");
1489
+ }
1490
+ const isServerUrl = downloadUrl.startsWith(configuration.serverUrl);
1491
+ const headers = {};
1492
+ if (isServerUrl) {
1493
+ headers["Authorization"] = `Bearer ${this.token}`;
1494
+ }
1495
+ const response = await axios.get(downloadUrl, {
1496
+ headers,
1497
+ responseType: "arraybuffer",
1498
+ timeout: 6e4,
1499
+ maxRedirects: 5,
1500
+ maxContentLength: MAX_CHAT_ATTACHMENT_ENCRYPTED_BYTES,
1501
+ maxBodyLength: MAX_CHAT_ATTACHMENT_ENCRYPTED_BYTES
1502
+ });
1503
+ return new Uint8Array(response.data);
1504
+ }
1505
+ /**
1506
+ * Download and decrypt an attachment blob.
1507
+ * Returns the decrypted binary data or null if decryption fails.
1508
+ */
1509
+ async downloadAndDecryptAttachment(ref) {
1510
+ const encrypted = await this.downloadAttachment(ref);
1511
+ const key = await this.getBlobKey();
1512
+ const decrypted = decryptBlob(encrypted, key);
1513
+ return decrypted;
1514
+ }
1515
+ /**
1516
+ * Track an attachment download whose promise resolves to the decoded blob
1517
+ * (or null on failure). The download stays in the current batch until the
1518
+ * next drainAttachmentsForUserMessage call swaps the bucket out — file
1519
+ * events that arrive after the swap go into a fresh bucket bound to the
1520
+ * next user-text message.
1521
+ */
1522
+ trackAttachmentDownload(promise) {
1523
+ this.pendingDownloads.push(promise);
1524
+ }
1525
+ /**
1526
+ * Atomically claim every download started before this call, wait for them
1527
+ * to resolve, and return the successful ones. The swap-then-await order
1528
+ * guarantees that a late-arriving file event cannot leak into this batch.
1529
+ */
1530
+ async drainAttachmentsForUserMessage() {
1531
+ const downloads = this.pendingDownloads;
1532
+ this.pendingDownloads = [];
1533
+ if (downloads.length === 0) return [];
1534
+ const results = await Promise.all(downloads);
1535
+ return results.filter((x) => x !== null);
1536
+ }
1537
+ authHeaders() {
1538
+ return {
1539
+ "Authorization": `Bearer ${this.token}`,
1540
+ "Content-Type": "application/json",
1541
+ "X-Happy-Client": `cli-coding-session/${configuration.currentCliVersion}`
1542
+ };
1543
+ }
1544
+ /**
1545
+ * B-509: attach the server-side prompt queue drain. `isIdle` is the
1546
+ * runner's own notion (input queue empty + blocked waiting); dispatching is
1547
+ * `POST /v1/sessions/:id/prompt-queue/dispatch`, whose message then comes
1548
+ * back through the ordinary socket / catch-up path and is routed below.
1549
+ */
1550
+ attachPromptQueueDrain(opts) {
1551
+ this.promptQueueDrain?.close();
1552
+ const drain = new PromptQueueDrain({
1553
+ isIdle: opts.isIdle,
1554
+ dispatch: () => this.dispatchPromptQueueHead(),
1555
+ log: (message) => logger.debug(`[API] ${message}`)
1556
+ });
1557
+ this.promptQueueDrain = drain;
1558
+ return drain;
1559
+ }
1560
+ /** B-509: the server still holds queued prompts for this session (see PromptQueueDrain.hasPending). */
1561
+ promptQueueHasPending() {
1562
+ return this.promptQueueDrain?.hasPending() === true;
1563
+ }
1564
+ /** B-509: stop draining before the process winds down (cleanup's first line). */
1565
+ closePromptQueueDrain() {
1566
+ this.promptQueueDrain?.close();
1567
+ }
1568
+ async dispatchPromptQueueHead() {
1569
+ try {
1570
+ const response = await axios.post(
1571
+ `${configuration.serverUrl}/v1/sessions/${encodeURIComponent(this.sessionId)}/prompt-queue/dispatch`,
1572
+ {},
1573
+ { headers: this.authHeaders(), timeout: 3e4, validateStatus: () => true }
1574
+ );
1575
+ if (response.status === 404) return { kind: "unsupported" };
1576
+ if (response.status !== 200) return { kind: "error", message: `HTTP ${response.status}` };
1577
+ const dispatched = response.data?.dispatched;
1578
+ const remaining = typeof response.data?.remaining === "number" ? response.data.remaining : void 0;
1579
+ if (dispatched && typeof dispatched.localId === "string") {
1580
+ return { kind: "dispatched", localId: dispatched.localId, remaining, created: dispatched.created !== false };
1581
+ }
1582
+ return { kind: "empty" };
1583
+ } catch (error) {
1584
+ return { kind: "error", message: error instanceof Error ? error.message : String(error) };
1585
+ }
1586
+ }
1587
+ routeIncomingMessage(message, sourceLocalId) {
1588
+ if (sourceLocalId) this.promptQueueDrain?.onInbound(sourceLocalId);
1589
+ const messageWithSource = sourceLocalId && typeof message === "object" && message !== null ? { ...message, localKey: message.localKey ?? sourceLocalId } : message;
1590
+ const userResult = UserMessageSchema.safeParse(messageWithSource);
1591
+ if (userResult.success) {
1592
+ if (this.pendingMessageCallback) {
1593
+ this.pendingMessageCallback(userResult.data);
1594
+ } else {
1595
+ this.pendingMessages.push(userResult.data);
1596
+ }
1597
+ return;
1598
+ }
1599
+ const fileResult = FileEventMessageSchema.safeParse(message);
1600
+ if (fileResult.success) {
1601
+ logger.debug(`[API] Received file event: ${fileResult.data.content.data.ev.name} (ref: ${fileResult.data.content.data.ev.ref})`);
1602
+ if (this.pendingFileEventCallback) {
1603
+ this.pendingFileEventCallback(fileResult.data);
1604
+ } else {
1605
+ this.pendingFileEvents.push(fileResult.data);
1606
+ }
1607
+ return;
1608
+ }
1609
+ const role = typeof message === "object" && message !== null ? message.role : void 0;
1610
+ if (role === "user") {
1611
+ logger.debug("[API] Inbound user-role message did not match UserMessageSchema; forwarding as generic message", {
1612
+ sourceLocalId: sourceLocalId ?? null,
1613
+ issues: userResult.error.issues.slice(0, 5).map((issue) => `${issue.path.join(".")}: ${issue.message}`)
1614
+ });
1615
+ }
1616
+ this.emit("message", message);
1617
+ }
1618
+ async fetchMessages() {
1619
+ const skipRouting = this.skipInitialMessages;
1620
+ if (skipRouting) {
1621
+ this.skipInitialMessages = false;
1622
+ logger.debug("[API] Reconnect mode: skipping existing messages, advancing lastSeq");
1623
+ }
1624
+ let afterSeq = this.lastSeq;
1625
+ while (true) {
1626
+ const response = await axios.get(
1627
+ `${configuration.serverUrl}/v3/sessions/${encodeURIComponent(this.sessionId)}/messages`,
1628
+ {
1629
+ params: {
1630
+ after_seq: afterSeq,
1631
+ limit: 100
1632
+ },
1633
+ headers: this.authHeaders(),
1634
+ timeout: 6e4
1635
+ }
1636
+ );
1637
+ const messages = Array.isArray(response.data.messages) ? response.data.messages : [];
1638
+ let maxSeq = afterSeq;
1639
+ for (const message of messages) {
1640
+ if (message.seq > maxSeq) {
1641
+ maxSeq = message.seq;
1642
+ }
1643
+ if (skipRouting) continue;
1644
+ const relayAccepted = !!message.localId && this.directInboundLocalIds.delete(message.localId);
1645
+ if (message.localId && this.routedInboundLocalIds.has(message.localId)) {
1646
+ continue;
1647
+ }
1648
+ if (relayAccepted) {
1649
+ logger.debug(`[API] Routing relay-accepted message ${message.localId} from catch-up fetch (own persist still pending or failed)`);
1650
+ }
1651
+ if (message.content?.t !== "encrypted") {
1652
+ continue;
1653
+ }
1654
+ try {
1655
+ const body = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(message.content.c));
1656
+ this.routeIncomingMessage(body, message.localId);
1657
+ if (message.localId) this.rememberRoutedInbound(message.localId);
1658
+ } catch (error) {
1659
+ logger.debug("[API] Failed to decrypt fetched message", {
1660
+ sessionId: this.sessionId,
1661
+ seq: message.seq,
1662
+ error
1663
+ });
1664
+ }
1665
+ }
1666
+ this.lastSeq = Math.max(this.lastSeq, maxSeq);
1667
+ const hasMore = !!response.data.hasMore;
1668
+ if (hasMore && maxSeq === afterSeq) {
1669
+ logger.debug("[API] fetchMessages pagination stalled, stopping to avoid infinite loop", {
1670
+ sessionId: this.sessionId,
1671
+ afterSeq
1672
+ });
1673
+ break;
1674
+ }
1675
+ afterSeq = maxSeq;
1676
+ if (!hasMore) {
1677
+ break;
1678
+ }
1679
+ }
1680
+ this.awaitingInitialFetch = false;
1681
+ }
1682
+ static MAX_OUTBOX_BATCH_SIZE = 50;
1683
+ async flushOutbox() {
1684
+ while (this.pendingOutbox.length > 0) {
1685
+ const batchSize = Math.min(this.pendingOutbox.length, ApiSessionClient.MAX_OUTBOX_BATCH_SIZE);
1686
+ const batch = this.pendingOutbox.slice(0, batchSize);
1687
+ const response = await axios.post(
1688
+ `${configuration.serverUrl}/v3/sessions/${encodeURIComponent(this.sessionId)}/messages`,
1689
+ {
1690
+ messages: batch
1691
+ },
1692
+ {
1693
+ headers: this.authHeaders(),
1694
+ timeout: 6e4
1695
+ }
1696
+ );
1697
+ const messages = Array.isArray(response.data.messages) ? response.data.messages : [];
1698
+ const maxSeq = messages.reduce((acc, message) => message.seq > acc ? message.seq : acc, this.lastSeq);
1699
+ this.lastSeq = maxSeq;
1700
+ this.pushCommittedMessagesToRelay(batch, messages);
1701
+ this.pendingOutbox.splice(0, batch.length);
1702
+ }
1703
+ }
1704
+ rememberRoutedInbound(localId) {
1705
+ this.routedInboundLocalIds.add(localId);
1706
+ if (this.routedInboundLocalIds.size > 2e3) {
1707
+ const oldest = this.routedInboundLocalIds.values().next().value;
1708
+ if (oldest) this.routedInboundLocalIds.delete(oldest);
1709
+ }
1710
+ }
1711
+ rememberDirectInbound(localId) {
1712
+ this.directInboundLocalIds.add(localId);
1713
+ if (this.directInboundLocalIds.size > 2e3) {
1714
+ const oldest = this.directInboundLocalIds.values().next().value;
1715
+ if (oldest) this.directInboundLocalIds.delete(oldest);
1716
+ }
1717
+ }
1718
+ async connectSessionRelay() {
1719
+ const machineId = this.metadata?.machineId;
1720
+ if (!machineId || this.relaySocket?.connected) return;
1721
+ try {
1722
+ const response = await fetch(
1723
+ `${configuration.serverUrl}/v1/relays/sessions/${encodeURIComponent(this.sessionId)}/machines/${encodeURIComponent(machineId)}`,
1724
+ { headers: this.authHeaders(), cache: "no-store" }
1725
+ );
1726
+ if (!response.ok) return;
1727
+ const assignment = RelayAssignmentResponseSchema.parse(await response.json()).assignment;
1728
+ if (!assignment) return;
1729
+ const relaySocket = io(assignment.url, {
1730
+ path: "/v1/relay",
1731
+ auth: { token: assignment.token },
1732
+ transports: ["websocket"],
1733
+ reconnection: true,
1734
+ reconnectionDelay: 500,
1735
+ reconnectionDelayMax: 5e3
1736
+ });
1737
+ this.relaySocket?.close();
1738
+ this.relaySocket = relaySocket;
1739
+ relaySocket.on("connect", () => {
1740
+ if (this.relaySocket !== relaySocket) return;
1741
+ this.rpcHandlerManager.onSocketConnect(relaySocket);
1742
+ logger.debug(`[API] Session connected to regional relay ${assignment.relayId}`);
1743
+ });
1744
+ relaySocket.on("rpc-request", async (data, callback) => {
1745
+ await answerSocketRequest(
1746
+ callback,
1747
+ () => this.rpcHandlerManager.handleRequest(data),
1748
+ (drop) => logger.debug(`[API] Relay RPC ${data?.method} unanswered \u2014 ${describeSocketAckDrop(drop)}`)
1749
+ );
1750
+ });
1751
+ relaySocket.on("session-message-deliver", async (data, rawCallback) => {
1752
+ const callback = safeAck(rawCallback, (drop) => logger.debug(`[API] session-message-deliver unanswered \u2014 ${describeSocketAckDrop(drop)}`));
1753
+ if (data?.sessionId !== this.sessionId || !Array.isArray(data.messages) || data.messages.length === 0 || data.messages.length > 50) {
1754
+ callback({ ok: false, error: "Invalid session message request" });
1755
+ return;
1756
+ }
1757
+ const unique = /* @__PURE__ */ new Map();
1758
+ for (const item of data.messages) {
1759
+ if (typeof item?.localId !== "string" || !item.localId || typeof item.content !== "string") {
1760
+ callback({ ok: false, error: "Invalid session message request" });
1761
+ return;
1762
+ }
1763
+ if (!unique.has(item.localId)) unique.set(item.localId, { localId: item.localId, content: item.content });
1764
+ }
1765
+ const batch = Array.from(unique.values());
1766
+ for (const item of batch) this.rememberDirectInbound(item.localId);
1767
+ try {
1768
+ const persisted = await axios.post(
1769
+ `${configuration.serverUrl}/v3/sessions/${encodeURIComponent(this.sessionId)}/messages`,
1770
+ { messages: batch },
1771
+ { headers: { ...this.authHeaders(), "X-Happy-Message-Origin": "relay-client" }, timeout: 6e4 }
1772
+ );
1773
+ const stored = Array.isArray(persisted.data.messages) ? persisted.data.messages : [];
1774
+ const storedLocalIds = new Set(stored.map((message) => message.localId));
1775
+ if (batch.some((item) => !storedLocalIds.has(item.localId))) {
1776
+ throw new Error("Central persistence response omitted a session message");
1777
+ }
1778
+ this.lastSeq = stored.reduce((max, message) => Math.max(max, message.seq), this.lastSeq);
1779
+ for (const item of batch) {
1780
+ if (this.routedInboundLocalIds.has(item.localId)) continue;
1781
+ const body = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(item.content));
1782
+ this.routeIncomingMessage(body, item.localId);
1783
+ this.rememberRoutedInbound(item.localId);
1784
+ }
1785
+ callback({ ok: true, messages: stored });
1786
+ } catch (error) {
1787
+ const routed = batch.filter((item) => this.routedInboundLocalIds.has(item.localId)).length;
1788
+ logger.debug(`[API] Relay-delivered persist failed for ${batch.length} message(s) (${routed} already routed via central echo): ${error instanceof Error ? error.message : error}`);
1789
+ for (const item of batch) this.directInboundLocalIds.delete(item.localId);
1790
+ callback({ ok: false, error: error instanceof Error ? error.message : "Failed to persist session messages" });
1791
+ }
1792
+ });
1793
+ relaySocket.on("disconnect", () => {
1794
+ if (this.relaySocket === relaySocket) this.rpcHandlerManager.onSocketDisconnect(relaySocket);
1795
+ });
1796
+ } catch (error) {
1797
+ logger.debug(`[API] Session regional relay unavailable: ${error instanceof Error ? error.message : error}`);
1798
+ }
1799
+ }
1800
+ pushCommittedMessagesToRelay(batch, messages) {
1801
+ if (!this.relaySocket?.connected || messages.length === 0) return;
1802
+ const contentByLocalId = new Map(batch.map((item) => [item.localId, item.content]));
1803
+ const committed = messages.flatMap((message) => {
1804
+ if (!message.localId) return [];
1805
+ const content = contentByLocalId.get(message.localId);
1806
+ if (!content) return [];
1807
+ return [{ ...message, content }];
1808
+ });
1809
+ if (committed.length > 0) {
1810
+ this.relaySocket.emit("session-message-committed", {
1811
+ sessionId: this.sessionId,
1812
+ messages: committed
1813
+ });
1814
+ }
1815
+ }
1816
+ enqueueMessage(content, invalidate = true, localId) {
1817
+ const encrypted = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, content));
1818
+ this.pendingOutbox.push({
1819
+ content: encrypted,
1820
+ // Deterministic localIds (terminal mirror, B-105) ride the server's
1821
+ // @@unique([sessionId, localId]) for replay idempotency; everything
1822
+ // else keeps the fire-once random id.
1823
+ localId: localId ?? randomUUID()
1824
+ });
1825
+ if (invalidate) {
1826
+ this.sendSync.invalidate();
1827
+ }
1828
+ }
1829
+ /** B-332: emit a queue-cancel tombstone, mirroring the web's own signal. */
1830
+ sendQueueCancelReason(localKeys, reason) {
1831
+ if (localKeys.length === 0) return;
1832
+ const envelope = createEnvelope("user", { t: "queue-cancel", targetLocalKeys: localKeys, reason });
1833
+ this.sendSessionProtocolMessage(envelope);
1834
+ }
1835
+ /**
1836
+ * B-332 site ③: find queued user input this wrapper destroyed (it was still
1837
+ * in the previous wrapper's in-memory queue when it died) and tell the web.
1838
+ *
1839
+ * Walks NEWEST → OLDEST via `before_seq`, decrypting each page, and stops at
1840
+ * the first turn-end. The runner calls this on EVERY reconnect (runClaude /
1841
+ * runCodex), seeded or not: a cursor seeded at the server's latest seq skips
1842
+ * the undelivered tail just as thoroughly as skipExistingMessages does —
1843
+ * and the daemon's resume/restart always seeds, so gating on the skip path
1844
+ * would miss the common case. Best-effort: a failed scan must not take a
1845
+ * session down. Needs no socket; POSTs through the same session-message
1846
+ * channel so the web receives it identically to a cancel-button tombstone.
1847
+ */
1848
+ async cancelUndeliveredQueuedInputs() {
1849
+ const MAX_SCAN_PAGES = 10;
1850
+ const canceled = /* @__PURE__ */ new Set();
1851
+ const undelivered = [];
1852
+ let beforeSeq = 2147483647;
1853
+ let reachedBoundary = false;
1854
+ let pages = 0;
1855
+ try {
1856
+ while (!reachedBoundary && pages < MAX_SCAN_PAGES) {
1857
+ pages += 1;
1858
+ const response = await axios.get(
1859
+ `${configuration.serverUrl}/v3/sessions/${encodeURIComponent(this.sessionId)}/messages`,
1860
+ {
1861
+ params: { before_seq: beforeSeq, limit: 100 },
1862
+ headers: this.authHeaders(),
1863
+ timeout: 6e4
1864
+ }
1865
+ );
1866
+ const messages = Array.isArray(response.data.messages) ? response.data.messages : [];
1867
+ if (messages.length === 0) break;
1868
+ const records = messages.map((message) => {
1869
+ let body = null;
1870
+ try {
1871
+ body = message.content?.t === "encrypted" ? decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(message.content.c)) : null;
1872
+ } catch (error) {
1873
+ logger.debug("[API] Failed to decrypt during undelivered scan:", error);
1874
+ }
1875
+ return { seq: message.seq, localId: message.localId, body };
1876
+ });
1877
+ const scan = scanUndeliveredQueuedInputs(records, { canceled });
1878
+ for (const key of scan.canceled) canceled.add(key);
1879
+ undelivered.push(...scan.undeliveredLocalKeys);
1880
+ reachedBoundary = scan.reachedBoundary;
1881
+ const oldest = records.at(-1)?.seq;
1882
+ if (oldest === void 0 || messages.length < 100) break;
1883
+ beforeSeq = oldest;
1884
+ }
1885
+ } catch (error) {
1886
+ logger.debug("[API] cancelUndeliveredQueuedInputs scan failed:", error);
1887
+ return;
1888
+ }
1889
+ if (!reachedBoundary) {
1890
+ logger.debug("[API] cancelUndeliveredQueuedInputs found no turn boundary; leaving messages as-is");
1891
+ return;
1892
+ }
1893
+ undelivered.reverse();
1894
+ this.sendQueueCancelReason(undelivered, "restarted");
1895
+ }
1896
+ /**
1897
+ * Send a message to session
1898
+ * @param body - Message body (can be MessageContent or raw content for agent messages)
1899
+ */
1900
+ sendClaudeSessionMessage(body, opts) {
1901
+ this.emit("claude-session-message", body);
1902
+ const mapped = mapClaudeLogMessageToSessionEnvelopes(body, this.claudeSessionProtocolState);
1903
+ this.claudeSessionProtocolState.currentTurnId = mapped.currentTurnId;
1904
+ for (const [envelopeIndex, envelope] of mapped.envelopes.entries()) {
1905
+ this.sendSessionProtocolMessage(envelope, opts?.localIdFor?.(envelopeIndex));
1906
+ }
1907
+ if (body.type === "assistant" && body.message?.usage) {
1908
+ try {
1909
+ const eventId = typeof body.uuid === "string" ? body.uuid : typeof body.message.id === "string" ? body.message.id : void 0;
1910
+ this.sendUsageData(body.message.usage, body.message.model, eventId);
1911
+ } catch (error) {
1912
+ logger.debug("[SOCKET] Failed to send usage data:", error);
1913
+ }
1914
+ }
1915
+ if (body.type === "summary" && "summary" in body && "leafUuid" in body) {
1916
+ this.updateMetadata((metadata) => ({
1917
+ ...metadata,
1918
+ summary: {
1919
+ text: body.summary,
1920
+ updatedAt: Date.now()
1921
+ }
1922
+ }));
1923
+ }
1924
+ }
1925
+ closeClaudeSessionTurn(status = "completed", meta) {
1926
+ this.emit("turn-ended", status);
1927
+ const mapped = closeClaudeTurnWithStatus(this.claudeSessionProtocolState, status, meta);
1928
+ this.claudeSessionProtocolState.currentTurnId = mapped.currentTurnId;
1929
+ for (const envelope of mapped.envelopes) {
1930
+ this.sendSessionProtocolMessage(envelope);
1931
+ }
1932
+ }
1933
+ sendCodexMessage(body) {
1934
+ let content = {
1935
+ role: "agent",
1936
+ content: {
1937
+ type: "codex",
1938
+ data: body
1939
+ // This wraps the entire Claude message
1940
+ },
1941
+ meta: {
1942
+ sentFrom: "cli"
1943
+ }
1944
+ };
1945
+ this.enqueueMessage(content);
1946
+ }
1947
+ enqueueSessionProtocolEnvelope(envelope, invalidate = true, localId) {
1948
+ const content = {
1949
+ role: "session",
1950
+ content: envelope,
1951
+ meta: {
1952
+ sentFrom: "cli"
1953
+ }
1954
+ };
1955
+ this.enqueueMessage(content, invalidate, localId);
1956
+ }
1957
+ sendSessionProtocolMessage(envelope, localId) {
1958
+ this.enqueueSessionProtocolEnvelope(envelope, true, localId);
1959
+ }
1960
+ /**
1961
+ * Send a generic agent message to the session using ACP (Agent Communication Protocol) format.
1962
+ * Works for any agent type (Gemini, Codex, Claude, etc.) - CLI normalizes to unified ACP format.
1963
+ *
1964
+ * @param provider - The agent provider sending the message (e.g., 'gemini', 'codex', 'claude')
1965
+ * @param body - The message payload (type: 'message' | 'reasoning' | 'tool-call' | 'tool-result')
1966
+ */
1967
+ sendAgentMessage(provider, body) {
1968
+ let content = {
1969
+ role: "agent",
1970
+ content: {
1971
+ type: "acp",
1972
+ provider,
1973
+ data: body
1974
+ },
1975
+ meta: {
1976
+ sentFrom: "cli"
1977
+ }
1978
+ };
1979
+ logger.debug(`[SOCKET] Sending ACP message from ${provider}:`, { type: body.type, hasMessage: "message" in body });
1980
+ this.enqueueMessage(content);
1981
+ }
1982
+ sendSessionEvent(event, id) {
1983
+ let content = {
1984
+ role: "agent",
1985
+ content: {
1986
+ id: id ?? randomUUID(),
1987
+ type: "event",
1988
+ data: event
1989
+ }
1990
+ };
1991
+ this.enqueueMessage(content);
1992
+ }
1993
+ /**
1994
+ * Send a ping message to keep the connection alive
1995
+ */
1996
+ /** B-466: turn edges (and a periodic lease) reported to the local daemon so
1997
+ * auto-update waits for "no turn in flight" instead of "no sessions". */
1998
+ turnReporter = new TurnReporter((event) => void notifyDaemonTurnEvent(this.sessionId, event).catch((error) => logger.debug("[API] Failed to report turn event to daemon:", error)));
1999
+ /** B-507: background tasks in flight, published on change and renewed every
2000
+ * 60s to the daemon (`/session-event background_tasks`) and into agentState
2001
+ * (`backgroundTasks: { updatedAt, tasks }`) — the two readers that outlive a
2002
+ * single heartbeat. The heartbeat itself carries only the count. */
2003
+ backgroundTaskReporter = new BackgroundTaskReporter((tasks, now) => {
2004
+ void notifyDaemonBackgroundTasks(this.sessionId, tasks).catch((error) => logger.debug("[API] Failed to report background tasks to daemon:", error));
2005
+ this.updateAgentState((state) => ({
2006
+ ...state,
2007
+ backgroundTasks: tasks.length > 0 ? { updatedAt: now, tasks } : void 0
2008
+ }));
2009
+ });
2010
+ keepAlive(thinking, mode, backgroundTasks) {
2011
+ if (process.env.DEBUG) {
2012
+ logger.debug(`[API] Sending keep alive message: ${thinking}`);
2013
+ }
2014
+ this.turnReporter.observe(thinking);
2015
+ if (backgroundTasks) this.backgroundTaskReporter.observe(backgroundTasks);
2016
+ this.socket.volatile.emit("session-alive", {
2017
+ sid: this.sessionId,
2018
+ time: Date.now(),
2019
+ thinking,
2020
+ mode,
2021
+ ...backgroundTasks ? { backgroundTasks: backgroundTasks.length } : {}
2022
+ });
2023
+ }
2024
+ /**
2025
+ * B-309: push one live stream frame (thinking/text delta or progress) to
2026
+ * this account's web clients.
2027
+ *
2028
+ * Encrypted with the SESSION key and relayed without being stored —
2029
+ * clipboard-push's shape, for clipboard-push's reasons: the relay must not
2030
+ * be able to read thinking text, and the session id is stamped by the
2031
+ * server from the authenticated connection rather than trusted from here.
2032
+ *
2033
+ * `volatile` on purpose. A draft that misses the wire while disconnected
2034
+ * is worth nothing — the persisted message is still coming, and replaying
2035
+ * stale deltas after a reconnect would paint text that has already landed.
2036
+ */
2037
+ sendStreamFrame(frame) {
2038
+ if (!this.socket.connected) return;
2039
+ try {
2040
+ this.socket.volatile.emit("session-stream", {
2041
+ // The frame object, NOT a JSON string: `encrypt` serializes
2042
+ // internally, so pre-stringifying would double-encode and
2043
+ // escape every quote in the streamed text.
2044
+ payload: encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, frame)),
2045
+ enc: true
2046
+ });
2047
+ } catch (error) {
2048
+ logger.debug(`[API] Failed to relay stream frame: ${error}`);
2049
+ }
2050
+ }
2051
+ /**
2052
+ * Send session death message
2053
+ */
2054
+ sendSessionDeath() {
2055
+ this.socket.emit("session-end", { sid: this.sessionId, time: Date.now() });
2056
+ }
2057
+ /**
2058
+ * Push text to the clipboard of every web client the user has open.
2059
+ * Encrypted with the SESSION key (same primitive the message stream uses,
2060
+ * proven to interop with the web's SessionEncryption.decryptRaw); the
2061
+ * server relays it to the user's web clients without reading it.
2062
+ * Returns delivery info for the MCP tool to report back to the model.
2063
+ */
2064
+ pushClipboard(text) {
2065
+ const prepared = prepareClipboardText(text);
2066
+ const history = prepareClipboardHistoryText(text);
2067
+ if (!this.socket.connected) {
2068
+ return { delivered: false, truncated: prepared.truncated, totalBytes: prepared.totalBytes };
2069
+ }
2070
+ this.socket.emit("clipboard-push", {
2071
+ payload: encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, prepared.text)),
2072
+ enc: true,
2073
+ historyPayload: encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, history.text)),
2074
+ historyTruncated: history.truncated,
2075
+ truncated: prepared.truncated,
2076
+ totalBytes: prepared.totalBytes
2077
+ });
2078
+ return { delivered: true, truncated: prepared.truncated, totalBytes: prepared.totalBytes };
2079
+ }
2080
+ /**
2081
+ * B-131: ask every web client the user has open to preview a file.
2082
+ *
2083
+ * Only the PATH travels — encrypted with the session key exactly like
2084
+ * clipboard-push. The web client then reads the file over the existing
2085
+ * machine-level fs-read RPC. Two reasons: it adds no new file access
2086
+ * (fs-read is already exposed) and it keeps the relay payload tiny, so
2087
+ * large files / images / PDFs ride the existing chunked read instead of
2088
+ * the 1MB relay cap.
2089
+ *
2090
+ * The path is encrypted rather than sent in the clear because every other
2091
+ * payload on this channel is (clipboard, fs RPC) — and a path leaks
2092
+ * project/client names and directory structure.
2093
+ */
2094
+ pushFilePreview(path, mode = "file") {
2095
+ if (!this.socket.connected) {
2096
+ return { delivered: false, error: "not connected to the server" };
2097
+ }
2098
+ this.socket.emit("file-preview-push", {
2099
+ payload: encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, path)),
2100
+ enc: true,
2101
+ mode
2102
+ });
2103
+ return { delivered: true };
2104
+ }
2105
+ /**
2106
+ * Send usage data to the server
2107
+ */
2108
+ sendUsageData(usage, model, eventId) {
2109
+ if (eventId && this.seenClaudeUsageEvents.has(eventId)) return;
2110
+ if (eventId) this.seenClaudeUsageEvents.add(eventId);
2111
+ const totalTokens = usage.input_tokens + usage.output_tokens + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0);
2112
+ const costs = calculateCost(usage, model);
2113
+ this.claudeUsageTotals.total += totalTokens;
2114
+ this.claudeUsageTotals.input += usage.input_tokens;
2115
+ this.claudeUsageTotals.output += usage.output_tokens;
2116
+ this.claudeUsageTotals.cache_creation += usage.cache_creation_input_tokens || 0;
2117
+ this.claudeUsageTotals.cache_read += usage.cache_read_input_tokens || 0;
2118
+ this.claudeUsageTotals.costTotal += costs.total;
2119
+ this.claudeUsageTotals.costInput += costs.input;
2120
+ this.claudeUsageTotals.costOutput += costs.output;
2121
+ const usageReport = {
2122
+ // Keep the legacy key so upgrading replaces the old last-call row
2123
+ // instead of making old + new clients double-count one session.
2124
+ key: "claude-session",
2125
+ sessionId: this.sessionId,
2126
+ tokens: {
2127
+ total: this.claudeUsageTotals.total,
2128
+ input: this.claudeUsageTotals.input,
2129
+ output: this.claudeUsageTotals.output,
2130
+ cache_creation: this.claudeUsageTotals.cache_creation,
2131
+ cache_read: this.claudeUsageTotals.cache_read
2132
+ },
2133
+ cost: {
2134
+ total: this.claudeUsageTotals.costTotal,
2135
+ input: this.claudeUsageTotals.costInput,
2136
+ output: this.claudeUsageTotals.costOutput
2137
+ }
2138
+ };
2139
+ logger.debugLargeJson("[SOCKET] Sending usage data:", usageReport);
2140
+ this.socket.emit("usage-report", usageReport);
2141
+ }
2142
+ /** Save a provider's cumulative session snapshot under one upsert key. */
2143
+ sendAgentUsageSnapshot(agent, rawUsage) {
2144
+ const tokens = normalizeAgentUsage(rawUsage);
2145
+ if (!tokens) return false;
2146
+ const usageReport = {
2147
+ key: `usage:${usageAgentKey(agent)}:session`,
2148
+ sessionId: this.sessionId,
2149
+ tokens,
2150
+ // Provider token-count events do not expose reliable billed cost.
2151
+ // The Web treats this sentinel as unknown, not as real $0.00.
2152
+ cost: { total: 0 }
2153
+ };
2154
+ logger.debugLargeJson("[SOCKET] Sending agent usage snapshot:", usageReport);
2155
+ this.socket.emit("usage-report", usageReport);
2156
+ return true;
2157
+ }
2158
+ /**
2159
+ * Returns the latest session metadata known to the client.
2160
+ */
2161
+ getMetadata() {
2162
+ return this.metadata;
2163
+ }
2164
+ /**
2165
+ * Returns the latest agent state known to the client (requests,
2166
+ * controlledByUser, etc.). Used by the notification producer to classify
2167
+ * turn-end events.
2168
+ */
2169
+ getAgentState() {
2170
+ return this.agentState;
2171
+ }
2172
+ /**
2173
+ * Update session metadata
2174
+ * @param handler - Handler function that returns the updated metadata
2175
+ */
2176
+ suppressNextArchiveSignal() {
2177
+ this.ignoreArchiveSignal = true;
2178
+ }
2179
+ skipExistingMessages() {
2180
+ this.skipInitialMessages = true;
2181
+ }
2182
+ updateMetadata(handler) {
2183
+ this.metadataLock.inLock(async () => {
2184
+ let rateRefusals = 0;
2185
+ await backoff(async () => {
2186
+ let updated = handler(this.metadata);
2187
+ const answer = await this.socket.emitWithAck("update-metadata", { sid: this.sessionId, expectedVersion: this.metadataVersion, metadata: encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, updated)) });
2188
+ if (answer.result === "success") {
2189
+ this.metadata = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(answer.metadata));
2190
+ this.metadataVersion = answer.version;
2191
+ rateRefusals = 0;
2192
+ } else if (answer.result === "version-mismatch") {
2193
+ if (answer.version > this.metadataVersion) {
2194
+ this.metadataVersion = answer.version;
2195
+ this.metadata = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(answer.metadata));
2196
+ }
2197
+ throw new Error("Metadata version mismatch");
2198
+ } else if (answer.result === "error") {
2199
+ if (isRateQuotaCode(answer.error)) {
2200
+ rateRefusals += 1;
2201
+ await pauseForRateQuota({
2202
+ label: "session metadata",
2203
+ code: answer.error,
2204
+ attempt: rateRefusals,
2205
+ sleep: delay,
2206
+ log: (message) => logger.debug(message)
2207
+ });
2208
+ }
2209
+ throw new Error("Metadata update failed");
2210
+ }
2211
+ });
2212
+ });
2213
+ }
2214
+ /**
2215
+ * Update session agent state
2216
+ * @param handler - Handler function that returns the updated agent state
2217
+ */
2218
+ updateAgentState(handler) {
2219
+ logger.debugLargeJson("Updating agent state", this.agentState);
2220
+ this.agentStateLock.inLock(async () => {
2221
+ let rateRefusals = 0;
2222
+ await backoff(async () => {
2223
+ let updated = handler(this.agentState || {});
2224
+ const answer = await this.socket.emitWithAck("update-state", { sid: this.sessionId, expectedVersion: this.agentStateVersion, agentState: updated ? encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, updated)) : null });
2225
+ if (answer.result === "success") {
2226
+ this.agentState = answer.agentState ? decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(answer.agentState)) : null;
2227
+ this.agentStateVersion = answer.version;
2228
+ rateRefusals = 0;
2229
+ logger.debug("Agent state updated", this.agentState);
2230
+ } else if (answer.result === "version-mismatch") {
2231
+ if (answer.version > this.agentStateVersion) {
2232
+ this.agentStateVersion = answer.version;
2233
+ this.agentState = answer.agentState ? decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(answer.agentState)) : null;
2234
+ }
2235
+ throw new Error("Agent state version mismatch");
2236
+ } else if (answer.result === "error") {
2237
+ if (isRateQuotaCode(answer.error)) {
2238
+ rateRefusals += 1;
2239
+ await pauseForRateQuota({
2240
+ label: "session agent state",
2241
+ code: answer.error,
2242
+ attempt: rateRefusals,
2243
+ sleep: delay,
2244
+ log: (message) => logger.debug(message)
2245
+ });
2246
+ }
2247
+ throw new Error("Agent state update failed");
2248
+ }
2249
+ });
2250
+ });
2251
+ }
2252
+ /**
2253
+ * Wait for socket buffer to flush
2254
+ */
2255
+ async flush() {
2256
+ await Promise.race([
2257
+ this.sendSync.invalidateAndAwait(),
2258
+ delay(1e4)
2259
+ ]);
2260
+ if (!this.socket.connected) {
2261
+ return;
2262
+ }
2263
+ return new Promise((resolve) => {
2264
+ this.socket.emit("ping", () => {
2265
+ resolve();
2266
+ });
2267
+ setTimeout(() => {
2268
+ resolve();
2269
+ }, 1e4);
2270
+ });
2271
+ }
2272
+ async close() {
2273
+ logger.debug("[API] socket.close() called");
2274
+ this.promptQueueDrain?.close();
2275
+ this.sendSync.stop();
2276
+ this.receiveSync.stop();
2277
+ if (this.reconnectInterval) {
2278
+ clearInterval(this.reconnectInterval);
2279
+ this.reconnectInterval = null;
2280
+ }
2281
+ this.socket.close();
2282
+ this.relaySocket?.close();
2283
+ this.relaySocket = null;
2284
+ }
2285
+ startSmartReconnect() {
2286
+ if (this.reconnectInterval) return;
2287
+ this.reconnectInterval = setInterval(() => {
2288
+ if (this.socket.connected) {
2289
+ clearInterval(this.reconnectInterval);
2290
+ this.reconnectInterval = null;
2291
+ return;
2292
+ }
2293
+ if (!shouldReconnect()) {
2294
+ logger.debug("[API] Still not ready to reconnect");
2295
+ return;
2296
+ }
2297
+ logger.debug("[API] Attempting reconnect");
2298
+ this.socket.connect();
2299
+ }, 3e3);
2300
+ if (shouldReconnect()) {
2301
+ logger.debug("[API] Network up + lid open \u2014 reconnecting in 1s");
2302
+ setTimeout(() => {
2303
+ if (!this.socket.connected) this.socket.connect();
2304
+ }, 1e3);
2305
+ }
2306
+ }
2307
+ }
2308
+
2309
+ function getSessionTitle(metadata) {
2310
+ const summaryText = metadata?.summary?.text?.trim();
2311
+ if (summaryText) {
2312
+ return summaryText;
2313
+ }
2314
+ const path = metadata?.path?.trim();
2315
+ if (!path) {
2316
+ return "Session";
2317
+ }
2318
+ const segments = path.split(/[\\/]/).filter(Boolean);
2319
+ return segments[segments.length - 1] || "Session";
2320
+ }
2321
+ function getSessionNotificationUrl(data) {
2322
+ const sessionId = data?.sessionId;
2323
+ if (typeof sessionId !== "string") {
2324
+ return null;
2325
+ }
2326
+ const trimmedSessionId = sessionId.trim();
2327
+ if (!trimmedSessionId) {
2328
+ return null;
2329
+ }
2330
+ return `/session/${encodeURIComponent(trimmedSessionId)}`;
2331
+ }
2332
+ function getSessionNotificationTitle(kind) {
2333
+ switch (kind) {
2334
+ case "done":
2335
+ return "It's ready!";
2336
+ case "permission":
2337
+ return "Permission request";
2338
+ case "question":
2339
+ return "Clarification needed";
2340
+ }
2341
+ }
2342
+ function getSessionNotificationBody(metadata) {
2343
+ return getSessionTitle(metadata);
2344
+ }
2345
+ function getSessionNotificationCopy(kind, metadata) {
2346
+ return {
2347
+ title: getSessionNotificationTitle(kind),
2348
+ body: getSessionNotificationBody(metadata)
2349
+ };
2350
+ }
2351
+ class PushNotificationClient {
2352
+ token;
2353
+ baseUrl;
2354
+ expoLoad = null;
2355
+ constructor(token, baseUrl = "https://api.cluster-fluster.com") {
2356
+ this.token = token;
2357
+ this.baseUrl = baseUrl;
2358
+ }
2359
+ loadExpo() {
2360
+ if (!this.expoLoad) {
2361
+ const load = import('expo-server-sdk').then(({ Expo }) => ({ Expo, expo: new Expo() }));
2362
+ load.catch(() => {
2363
+ if (this.expoLoad === load) this.expoLoad = null;
2364
+ });
2365
+ this.expoLoad = load;
2366
+ }
2367
+ return this.expoLoad;
2368
+ }
2369
+ /**
2370
+ * Fetch all push tokens for the authenticated user.
2371
+ * Retries up to 3 times with exponential backoff on transient errors.
2372
+ */
2373
+ async fetchPushTokens() {
2374
+ const maxAttempts = 3;
2375
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
2376
+ try {
2377
+ const response = await axios.get(
2378
+ `${this.baseUrl}/v1/push-tokens`,
2379
+ {
2380
+ headers: {
2381
+ "Authorization": `Bearer ${this.token}`,
2382
+ "Content-Type": "application/json",
2383
+ "X-Happy-Client": `cli-daemon/${configuration.currentCliVersion}`
2384
+ }
2385
+ }
2386
+ );
2387
+ logger.debug(`Fetched ${response.data.tokens.length} push tokens`);
2388
+ response.data.tokens.forEach((token, index) => {
2389
+ logger.debug(`[PUSH] Token ${index + 1}: id=${token.id}, created=${new Date(token.createdAt).toISOString()}, updated=${new Date(token.updatedAt).toISOString()}`);
2390
+ });
2391
+ return response.data.tokens;
2392
+ } catch (error) {
2393
+ logger.debug(`[PUSH] [ERROR] Failed to fetch push tokens (attempt ${attempt}/${maxAttempts}):`, error);
2394
+ if (attempt < maxAttempts) {
2395
+ const delay = 1e3 * Math.pow(2, attempt - 1);
2396
+ await new Promise((resolve) => setTimeout(resolve, delay));
2397
+ }
2398
+ }
2399
+ }
2400
+ logger.debug("[PUSH] [ERROR] All push token fetch attempts failed");
2401
+ return [];
2402
+ }
2403
+ /**
2404
+ * Send push notification via Expo Push API with retry
2405
+ * @param messages - Array of push messages to send
2406
+ */
2407
+ async sendPushNotifications(messages) {
2408
+ logger.debug(`Sending ${messages.length} push notifications`);
2409
+ const { Expo, expo } = await this.loadExpo();
2410
+ const validMessages = messages.filter((message) => {
2411
+ if (Array.isArray(message.to)) {
2412
+ return message.to.every((token) => Expo.isExpoPushToken(token));
2413
+ }
2414
+ return Expo.isExpoPushToken(message.to);
2415
+ });
2416
+ if (validMessages.length === 0) {
2417
+ logger.debug("No valid Expo push tokens found");
2418
+ return;
2419
+ }
2420
+ const chunks = expo.chunkPushNotifications(validMessages);
2421
+ for (const chunk of chunks) {
2422
+ const startTime = Date.now();
2423
+ const timeout = 3e5;
2424
+ let attempt = 0;
2425
+ while (true) {
2426
+ try {
2427
+ const ticketChunk = await expo.sendPushNotificationsAsync(chunk);
2428
+ const errors = ticketChunk.filter((ticket) => ticket.status === "error");
2429
+ if (errors.length > 0) {
2430
+ const errorDetails = errors.map((e) => ({ message: e.message, details: e.details }));
2431
+ logger.debug("[PUSH] Some notifications failed:", errorDetails);
2432
+ }
2433
+ if (errors.length === ticketChunk.length) {
2434
+ throw new Error("All push notifications in chunk failed");
2435
+ }
2436
+ break;
2437
+ } catch (error) {
2438
+ const elapsed = Date.now() - startTime;
2439
+ if (elapsed >= timeout) {
2440
+ logger.debug("[PUSH] Timeout reached after 5 minutes, giving up on chunk");
2441
+ break;
2442
+ }
2443
+ attempt++;
2444
+ const delay = Math.min(1e3 * Math.pow(2, attempt), 3e4);
2445
+ const remainingTime = timeout - elapsed;
2446
+ const waitTime = Math.min(delay, remainingTime);
2447
+ if (waitTime > 0) {
2448
+ logger.debug(`[PUSH] Retrying in ${waitTime}ms (attempt ${attempt})`);
2449
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
2450
+ }
2451
+ }
2452
+ }
2453
+ }
2454
+ logger.debug(`Push notifications sent successfully`);
2455
+ }
2456
+ /**
2457
+ * Send a push notification to all registered devices for the user
2458
+ * @param title - Notification title
2459
+ * @param body - Notification body
2460
+ * @param data - Additional data to send with the notification
2461
+ */
2462
+ sendToAllDevices(title, body, data) {
2463
+ logger.debug("[PUSH] sendToAllDevices called:", {
2464
+ title: contentLogMetadata(title),
2465
+ body: contentLogMetadata(body),
2466
+ dataKeyCount: data ? Object.keys(data).length : 0
2467
+ });
2468
+ (async () => {
2469
+ try {
2470
+ logger.debug("[PUSH] Fetching push tokens...");
2471
+ const tokens = await this.fetchPushTokens();
2472
+ logger.debug(`[PUSH] Fetched ${tokens.length} push tokens`);
2473
+ tokens.forEach((token, index) => {
2474
+ logger.debug(`[PUSH] Using token ${index + 1}: id=${token.id}`);
2475
+ });
2476
+ if (tokens.length === 0) {
2477
+ logger.debug("No push tokens found for user");
2478
+ return;
2479
+ }
2480
+ const messages = tokens.map((token, index) => {
2481
+ logger.debug(`[PUSH] Creating message ${index + 1} for token`);
2482
+ return {
2483
+ to: token.token,
2484
+ title,
2485
+ body: body && body.length > 0 ? body : void 0,
2486
+ data,
2487
+ // TODO: For brutalist session artwork, attach rich media via a public HTTPS image URL.
2488
+ // Bundled app asset paths / require(...) / local file paths will not work in push payloads.
2489
+ // iOS also needs a Notification Service Extension to render richContent.image reliably.
2490
+ sound: "default",
2491
+ priority: "high"
2492
+ };
2493
+ });
2494
+ logger.debug(`[PUSH] Sending ${messages.length} push notifications...`);
2495
+ await this.sendPushNotifications(messages);
2496
+ logger.debug("[PUSH] Push notifications sent successfully");
2497
+ } catch (error) {
2498
+ logger.debug("[PUSH] Error sending to all devices:", error);
2499
+ }
2500
+ })();
2501
+ }
2502
+ /**
2503
+ * Routes session-event pushes through the server so it can apply
2504
+ * presence-based suppression (active desktop/web, mobile foreground).
2505
+ * Falls back to direct Expo send only when sessionId is missing — that
2506
+ * shouldn't happen for session notifications but guards against regressions.
2507
+ */
2508
+ sendSessionNotification(params) {
2509
+ const { title, body } = getSessionNotificationCopy(params.kind, params.metadata);
2510
+ const sessionTitle = getSessionNotificationBody(params.metadata);
2511
+ const url = getSessionNotificationUrl(params.data);
2512
+ const payloadData = {
2513
+ ...params.data,
2514
+ kind: params.kind,
2515
+ sessionTitle,
2516
+ ...url ? { url } : {}
2517
+ };
2518
+ const sessionId = typeof params.data?.sessionId === "string" ? params.data.sessionId : null;
2519
+ if (!sessionId) {
2520
+ logger.debug("[PUSH] sendSessionNotification: missing sessionId, falling back to direct send");
2521
+ this.sendToAllDevices(title, body, payloadData);
2522
+ return;
2523
+ }
2524
+ void (async () => {
2525
+ try {
2526
+ await axios.post(
2527
+ `${this.baseUrl}/v1/sessions/${encodeURIComponent(sessionId)}/push-event`,
2528
+ {
2529
+ kind: params.kind,
2530
+ title,
2531
+ body,
2532
+ data: payloadData
2533
+ },
2534
+ {
2535
+ headers: {
2536
+ "Authorization": `Bearer ${this.token}`,
2537
+ "Content-Type": "application/json",
2538
+ "X-Happy-Client": `cli-daemon/${configuration.currentCliVersion}`
2539
+ },
2540
+ timeout: 15e3
2541
+ }
2542
+ );
2543
+ logger.debug(`[PUSH] sendSessionNotification dispatched via server (kind=${params.kind})`);
2544
+ } catch (error) {
2545
+ logger.debug("[PUSH] sendSessionNotification failed:", error);
2546
+ }
2547
+ })();
2548
+ }
2549
+ }
2550
+
2551
+ function startOfflineReconnection(config) {
2552
+ let reconnected = false;
2553
+ let session = null;
2554
+ let timeoutId = null;
2555
+ let failureCount = 0;
2556
+ let cancelled = false;
2557
+ const defaultHealthCheck = async () => {
2558
+ await axios.get(`${config.serverUrl}/v1/sessions`, {
2559
+ timeout: 5e3,
2560
+ validateStatus: (status) => status < 500,
2561
+ // 4xx = server is up, 5xx = server error
2562
+ headers: {
2563
+ "X-Happy-Client": `cli-daemon/${configuration.currentCliVersion}`
2564
+ }
2565
+ });
2566
+ };
2567
+ const healthCheck = config.healthCheck ?? defaultHealthCheck;
2568
+ const initialDelayMs = config.initialDelayMs ?? 5e3;
2569
+ const retryDelayMs = config.retryDelayMs ?? ((count) => exponentialBackoffDelay(count, 5e3, 6e4, 10));
2570
+ const attemptReconnect = async () => {
2571
+ if (reconnected || cancelled) return;
2572
+ try {
2573
+ await healthCheck();
2574
+ if (cancelled) return;
2575
+ session = await config.onReconnected();
2576
+ if (cancelled) return;
2577
+ reconnected = true;
2578
+ config.onNotify("\u2705 Reconnected! Session syncing in background.");
2579
+ logger.debug("[OfflineReconnection] Successfully reconnected");
2580
+ } catch (e) {
2581
+ if (axios.isAxiosError(e) && e.response?.status === 401) {
2582
+ logger.debug("[OfflineReconnection] Authentication error, stopping retries");
2583
+ config.onNotify("\u274C Authentication failed. Please re-authenticate with `very-happy auth`.");
2584
+ return;
2585
+ }
2586
+ failureCount++;
2587
+ const delay = retryDelayMs(failureCount);
2588
+ logger.debug(`[OfflineReconnection] Attempt ${failureCount} failed, retrying in ${delay}ms`);
2589
+ if (!cancelled) {
2590
+ timeoutId = setTimeout(attemptReconnect, delay);
2591
+ }
2592
+ }
2593
+ };
2594
+ timeoutId = setTimeout(attemptReconnect, initialDelayMs);
2595
+ return {
2596
+ cancel: () => {
2597
+ cancelled = true;
2598
+ if (timeoutId) {
2599
+ clearTimeout(timeoutId);
2600
+ timeoutId = null;
2601
+ }
2602
+ config.onCleanup?.();
2603
+ },
2604
+ getSession: () => session,
2605
+ isReconnected: () => reconnected
2606
+ };
2607
+ }
2608
+ const NETWORK_ERROR_CODES = [
2609
+ "ECONNREFUSED",
2610
+ "ENOTFOUND",
2611
+ "ETIMEDOUT",
2612
+ "ECONNRESET",
2613
+ "EHOSTUNREACH",
2614
+ "ENETUNREACH"
2615
+ ];
2616
+ function isNetworkError(code) {
2617
+ return code !== void 0 && NETWORK_ERROR_CODES.includes(code);
2618
+ }
2619
+ const ERROR_DESCRIPTIONS = {
2620
+ // Network errors (Node.js)
2621
+ ECONNREFUSED: "server not accepting connections",
2622
+ ENOTFOUND: "server hostname not found",
2623
+ ETIMEDOUT: "connection timed out",
2624
+ ECONNRESET: "connection reset by server",
2625
+ EHOSTUNREACH: "server host unreachable",
2626
+ ENETUNREACH: "network unreachable",
2627
+ // HTTP errors
2628
+ "401": "authentication failed - run `very-happy auth`",
2629
+ "403": "access forbidden",
2630
+ "404": "endpoint not found, check server deployment",
2631
+ "500": "server internal error",
2632
+ "502": "bad gateway",
2633
+ "503": "service unavailable"
2634
+ };
2635
+ class OfflineState {
2636
+ state = "online";
2637
+ failures = /* @__PURE__ */ new Map();
2638
+ // Dedupe by operation
2639
+ backend = "Claude";
2640
+ /** Report failure - accumulates context, prints once on first offline transition */
2641
+ fail(failure) {
2642
+ this.failures.set(failure.operation, failure);
2643
+ if (this.state === "online") {
2644
+ this.state = "offline";
2645
+ this.print();
2646
+ }
2647
+ }
2648
+ /** Reset on reconnection */
2649
+ recover() {
2650
+ this.state = "online";
2651
+ this.failures.clear();
2652
+ }
2653
+ /** Set backend name before API calls */
2654
+ setBackend(name) {
2655
+ this.backend = name;
2656
+ }
2657
+ /** Check current state */
2658
+ isOffline() {
2659
+ return this.state === "offline";
2660
+ }
2661
+ /** Reset for testing - clears all state */
2662
+ reset() {
2663
+ this.state = "online";
2664
+ this.failures.clear();
2665
+ this.backend = "Claude";
2666
+ }
2667
+ print() {
2668
+ const summary = [...this.failures.values()].map((f) => {
2669
+ const desc = f.errorCode ? `${f.errorCode} - ${ERROR_DESCRIPTIONS[f.errorCode] || "unknown error"}` : "unknown error";
2670
+ const url = f.url ? ` at ${f.url}` : "";
2671
+ return `${f.operation} failed: ${desc}${url}`;
2672
+ }).join("; ");
2673
+ console.log(`\u26A0\uFE0F Happy server unreachable, offline mode with auto-reconnect enabled - error details: ${summary}`);
2674
+ const allDetails = [...this.failures.values()].flatMap((f) => f.details || []);
2675
+ allDetails.forEach((line) => console.log(chalk.yellow(` \u2192 ${line}`)));
2676
+ }
2677
+ }
2678
+ const connectionState = new OfflineState();
2679
+
2680
+ function accountBoxPublicKeyFromCredentials(credential) {
2681
+ if (credential.encryption.type === "legacy") {
2682
+ return libsodiumPublicKeyFromSecretKey(credential.encryption.secret);
2683
+ }
2684
+ return credential.encryption.publicKey;
2685
+ }
2686
+ class NotificationProducer {
2687
+ token;
2688
+ accountBoxPubKey;
2689
+ sessionId;
2690
+ getMetadata;
2691
+ /** Last repeatKey we successfully (or attempted to) emit, used for local dedup. */
2692
+ lastEmittedAt = /* @__PURE__ */ new Map();
2693
+ constructor(opts) {
2694
+ this.token = opts.credential.token;
2695
+ this.accountBoxPubKey = accountBoxPublicKeyFromCredentials(opts.credential);
2696
+ this.sessionId = opts.sessionId;
2697
+ this.getMetadata = opts.getMetadata;
2698
+ }
2699
+ /**
2700
+ * A new non-empty permission request was added to agentState.requests.
2701
+ */
2702
+ permissionRequest(toolName) {
2703
+ const project = this.projectLabel();
2704
+ this.emit("permission_request", {
2705
+ title: project ? `Permission needed \xB7 ${project}` : "Permission needed",
2706
+ snippet: toolName ? `Claude wants to use ${toolName}` : "Claude is requesting permission"
2707
+ });
2708
+ }
2709
+ /**
2710
+ * A turn finished and Claude produced a reply (had assistant output during
2711
+ * the turn). thinking went true → false with new assistant message.
2712
+ */
2713
+ replyDone(snippet) {
2714
+ const project = this.projectLabel();
2715
+ this.emit("reply_done", {
2716
+ title: project ? `Claude replied \xB7 ${project}` : "Claude replied",
2717
+ snippet: this.clip(snippet) || "Claude finished responding"
2718
+ });
2719
+ }
2720
+ /**
2721
+ * A turn ended with the session idle and waiting for user input:
2722
+ * no pending permission requests, not controlled by user, not thinking.
2723
+ */
2724
+ inputNeeded() {
2725
+ const project = this.projectLabel();
2726
+ this.emit("input_needed", {
2727
+ title: project ? `Waiting for you \xB7 ${project}` : "Waiting for you",
2728
+ snippet: "Claude is idle and waiting for your input"
2729
+ });
2730
+ }
2731
+ /**
2732
+ * The session errored.
2733
+ */
2734
+ error(message) {
2735
+ const project = this.projectLabel();
2736
+ this.emit("error", {
2737
+ title: project ? `Session error \xB7 ${project}` : "Session error",
2738
+ snippet: this.clip(message) || "The session encountered an error"
2739
+ });
2740
+ }
2741
+ /**
2742
+ * Build, encrypt, dedup and POST a notification. Best-effort, fire-and-forget.
2743
+ */
2744
+ emit(notifType, parts) {
2745
+ const repeatKey = `${this.sessionId}:${notifType}`;
2746
+ const now = Date.now();
2747
+ const last = this.lastEmittedAt.get(repeatKey);
2748
+ if (last !== void 0 && now - last < DEDUP_WINDOW_MS) {
2749
+ logger.debug(`[notif] Suppressing duplicate ${notifType} for ${this.sessionId}`);
2750
+ return;
2751
+ }
2752
+ this.lastEmittedAt.set(repeatKey, now);
2753
+ const body = {
2754
+ title: parts.title,
2755
+ snippet: parts.snippet,
2756
+ sessionId: this.sessionId,
2757
+ ts: now
2758
+ };
2759
+ let enc;
2760
+ try {
2761
+ const plaintext = new TextEncoder().encode(JSON.stringify(body));
2762
+ const bundle = libsodiumEncryptForPublicKey(plaintext, this.accountBoxPubKey);
2763
+ enc = encodeBase64(bundle);
2764
+ } catch (err) {
2765
+ logger.debug("[notif] Failed to encrypt notification body:", err);
2766
+ return;
2767
+ }
2768
+ void this.post(notifType, enc, repeatKey);
2769
+ }
2770
+ async post(notifType, enc, repeatKey) {
2771
+ try {
2772
+ await axios.post(
2773
+ `${configuration.serverUrl}/v1/feed`,
2774
+ {
2775
+ notifType,
2776
+ sessionId: this.sessionId,
2777
+ enc,
2778
+ repeatKey
2779
+ },
2780
+ {
2781
+ headers: {
2782
+ "Authorization": `Bearer ${this.token}`,
2783
+ "Content-Type": "application/json",
2784
+ "X-Happy-Client": `cli-coding-session/${configuration.currentCliVersion}`
2785
+ },
2786
+ timeout: 5e3
2787
+ }
2788
+ );
2789
+ logger.debug(`[notif] Posted ${notifType} for session ${this.sessionId}`);
2790
+ } catch (err) {
2791
+ logger.debug(`[notif] Failed to post ${notifType}:`, err);
2792
+ }
2793
+ }
2794
+ /** Short, human-friendly label for the project/session for use in titles. */
2795
+ projectLabel() {
2796
+ const meta = this.getMetadata();
2797
+ if (!meta) return void 0;
2798
+ if (meta.name) return meta.name;
2799
+ if (meta.path) {
2800
+ const parts = meta.path.split("/").filter(Boolean);
2801
+ return parts.length ? parts[parts.length - 1] : meta.path;
2802
+ }
2803
+ return void 0;
2804
+ }
2805
+ clip(text, max = 140) {
2806
+ if (!text) return void 0;
2807
+ const trimmed = text.replace(/\s+/g, " ").trim();
2808
+ if (!trimmed) return void 0;
2809
+ return trimmed.length > max ? trimmed.slice(0, max - 1) + "\u2026" : trimmed;
2810
+ }
2811
+ }
2812
+ const DEDUP_WINDOW_MS = 3e3;
2813
+
2814
+ function describeSessionCreateError(error) {
2815
+ const base = error instanceof Error ? error.message : "Unknown error";
2816
+ if (!axios.isAxiosError(error) || !error.response) return base;
2817
+ const data = error.response.data;
2818
+ const code = data && typeof data === "object" && typeof data.error === "string" ? data.error : null;
2819
+ return code ? `${base} (${code})` : base;
2820
+ }
2821
+ class ApiClient {
2822
+ static async create(credential) {
2823
+ return new ApiClient(credential);
2824
+ }
2825
+ credential;
2826
+ pushClient;
2827
+ constructor(credential) {
2828
+ this.credential = credential;
2829
+ this.pushClient = new PushNotificationClient(credential.token, configuration.serverUrl);
2830
+ }
2831
+ /**
2832
+ * Create a new session or load existing one with the given tag
2833
+ */
2834
+ async getOrCreateSession(opts) {
2835
+ let dataEncryptionKey = null;
2836
+ let encryptionKey;
2837
+ let encryptionVariant;
2838
+ if (this.credential.encryption.type === "dataKey") {
2839
+ encryptionKey = getRandomBytes(32);
2840
+ encryptionVariant = "dataKey";
2841
+ let encryptedDataKey = libsodiumEncryptForPublicKey(encryptionKey, this.credential.encryption.publicKey);
2842
+ dataEncryptionKey = new Uint8Array(encryptedDataKey.length + 1);
2843
+ dataEncryptionKey.set([0], 0);
2844
+ dataEncryptionKey.set(encryptedDataKey, 1);
2845
+ } else {
2846
+ encryptionKey = this.credential.encryption.secret;
2847
+ encryptionVariant = "legacy";
2848
+ }
2849
+ try {
2850
+ const response = await axios.post(
2851
+ `${configuration.serverUrl}/v1/sessions`,
2852
+ {
2853
+ tag: opts.tag,
2854
+ metadata: encodeBase64(encrypt(encryptionKey, encryptionVariant, opts.metadata)),
2855
+ agentState: opts.state ? encodeBase64(encrypt(encryptionKey, encryptionVariant, opts.state)) : null,
2856
+ dataEncryptionKey: dataEncryptionKey ? encodeBase64(dataEncryptionKey) : null
2857
+ },
2858
+ {
2859
+ headers: {
2860
+ "Authorization": `Bearer ${this.credential.token}`,
2861
+ "Content-Type": "application/json",
2862
+ "X-Happy-Client": `cli-coding-session/${configuration.currentCliVersion}`
2863
+ },
2864
+ timeout: 6e4
2865
+ // 1 minute timeout for very bad network connections
2866
+ }
2867
+ );
2868
+ logger.debug(`Session created/loaded: ${response.data.session.id} (tag: ${opts.tag})`);
2869
+ let raw = response.data.session;
2870
+ let session = {
2871
+ id: raw.id,
2872
+ seq: raw.seq,
2873
+ metadata: decrypt(encryptionKey, encryptionVariant, decodeBase64(raw.metadata)),
2874
+ metadataVersion: raw.metadataVersion,
2875
+ agentState: raw.agentState ? decrypt(encryptionKey, encryptionVariant, decodeBase64(raw.agentState)) : null,
2876
+ agentStateVersion: raw.agentStateVersion,
2877
+ encryptionKey,
2878
+ encryptionVariant
2879
+ };
2880
+ return session;
2881
+ } catch (error) {
2882
+ logger.debug("[API] [ERROR] Failed to get or create session:", error);
2883
+ if (error && typeof error === "object" && "code" in error) {
2884
+ const errorCode = error.code;
2885
+ if (isNetworkError(errorCode)) {
2886
+ connectionState.fail({
2887
+ operation: "Session creation",
2888
+ caller: "api.getOrCreateSession",
2889
+ errorCode,
2890
+ url: `${configuration.serverUrl}/v1/sessions`
2891
+ });
2892
+ return null;
2893
+ }
2894
+ }
2895
+ const is404Error = axios.isAxiosError(error) && error.response?.status === 404 || error && typeof error === "object" && "response" in error && error.response?.status === 404;
2896
+ if (is404Error) {
2897
+ connectionState.fail({
2898
+ operation: "Session creation",
2899
+ errorCode: "404",
2900
+ url: `${configuration.serverUrl}/v1/sessions`
2901
+ });
2902
+ return null;
2903
+ }
2904
+ if (axios.isAxiosError(error) && error.response?.status) {
2905
+ const status = error.response.status;
2906
+ if (status >= 500) {
2907
+ connectionState.fail({
2908
+ operation: "Session creation",
2909
+ errorCode: String(status),
2910
+ url: `${configuration.serverUrl}/v1/sessions`,
2911
+ details: ["Server encountered an error, will retry automatically"]
2912
+ });
2913
+ return null;
2914
+ }
2915
+ }
2916
+ throw new Error(`Failed to get or create session: ${describeSessionCreateError(error)}`);
2917
+ }
2918
+ }
2919
+ /**
2920
+ * Register or update machine with the server
2921
+ * Returns the current machine state from the server with decrypted metadata and daemonState
2922
+ */
2923
+ async getOrCreateMachine(opts) {
2924
+ let dataEncryptionKey = null;
2925
+ let encryptionKey;
2926
+ let encryptionVariant;
2927
+ if (this.credential.encryption.type === "dataKey") {
2928
+ encryptionVariant = "dataKey";
2929
+ encryptionKey = this.credential.encryption.machineKey;
2930
+ let encryptedDataKey = libsodiumEncryptForPublicKey(this.credential.encryption.machineKey, this.credential.encryption.publicKey);
2931
+ dataEncryptionKey = new Uint8Array(encryptedDataKey.length + 1);
2932
+ dataEncryptionKey.set([0], 0);
2933
+ dataEncryptionKey.set(encryptedDataKey, 1);
2934
+ } else {
2935
+ encryptionKey = this.credential.encryption.secret;
2936
+ encryptionVariant = "legacy";
2937
+ }
2938
+ const createMinimalMachine = () => ({
2939
+ id: opts.machineId,
2940
+ encryptionKey,
2941
+ encryptionVariant,
2942
+ metadata: opts.metadata,
2943
+ metadataVersion: 0,
2944
+ daemonState: opts.daemonState || null,
2945
+ daemonStateVersion: 0
2946
+ });
2947
+ try {
2948
+ const response = await axios.post(
2949
+ `${configuration.serverUrl}/v1/machines`,
2950
+ {
2951
+ id: opts.machineId,
2952
+ metadata: encodeBase64(encrypt(encryptionKey, encryptionVariant, opts.metadata)),
2953
+ daemonState: opts.daemonState ? encodeBase64(encrypt(encryptionKey, encryptionVariant, opts.daemonState)) : void 0,
2954
+ dataEncryptionKey: dataEncryptionKey ? encodeBase64(dataEncryptionKey) : void 0
2955
+ },
2956
+ {
2957
+ headers: {
2958
+ "Authorization": `Bearer ${this.credential.token}`,
2959
+ "Content-Type": "application/json",
2960
+ "X-Happy-Client": `cli-coding-session/${configuration.currentCliVersion}`
2961
+ },
2962
+ timeout: 6e4
2963
+ // 1 minute timeout for very bad network connections
2964
+ }
2965
+ );
2966
+ const raw = response.data.machine;
2967
+ logger.debug(`[API] Machine ${opts.machineId} registered/updated with server`);
2968
+ const machine = {
2969
+ id: raw.id,
2970
+ encryptionKey,
2971
+ encryptionVariant,
2972
+ metadata: raw.metadata ? decrypt(encryptionKey, encryptionVariant, decodeBase64(raw.metadata)) : null,
2973
+ metadataVersion: raw.metadataVersion || 0,
2974
+ daemonState: raw.daemonState ? decrypt(encryptionKey, encryptionVariant, decodeBase64(raw.daemonState)) : null,
2975
+ daemonStateVersion: raw.daemonStateVersion || 0
2976
+ };
2977
+ return machine;
2978
+ } catch (error) {
2979
+ if (axios.isAxiosError(error) && error.code && isNetworkError(error.code)) {
2980
+ connectionState.fail({
2981
+ operation: "Machine registration",
2982
+ caller: "api.getOrCreateMachine",
2983
+ errorCode: error.code,
2984
+ url: `${configuration.serverUrl}/v1/machines`
2985
+ });
2986
+ return createMinimalMachine();
2987
+ }
2988
+ if (axios.isAxiosError(error) && error.response?.status) {
2989
+ const status = error.response.status;
2990
+ if (status === 403 || status === 409) {
2991
+ console.log(chalk.yellow(
2992
+ `\u26A0\uFE0F Machine registration rejected by the server with status ${status}`
2993
+ ));
2994
+ console.log(chalk.yellow(
2995
+ ` \u2192 This machine ID is already registered to another account on the server`
2996
+ ));
2997
+ console.log(chalk.yellow(
2998
+ ` \u2192 This usually happens after re-authenticating with a different account`
2999
+ ));
3000
+ console.log(chalk.yellow(
3001
+ // B-297: `doctor clean` only kills processes — it has never reset local
3002
+ // state or minted a machine id. `auth login --force` is the command
3003
+ // that does (it regenerates machineId, src/ui/auth.ts).
3004
+ ` \u2192 Run 'very-happy auth login --force' to re-authenticate and generate a new machine ID`
3005
+ ));
3006
+ console.log(chalk.yellow(
3007
+ ` \u2192 Open a GitHub issue if this problem persists`
3008
+ ));
3009
+ return createMinimalMachine();
3010
+ }
3011
+ if (status === 429 && error.response?.data?.error === "machine_state_rate_quota_exceeded") {
3012
+ connectionState.fail({
3013
+ operation: "Machine registration",
3014
+ errorCode: "429",
3015
+ url: `${configuration.serverUrl}/v1/machines`,
3016
+ details: ["Relay write quota is cooling down; realtime connection will retry automatically"]
3017
+ });
3018
+ return createMinimalMachine();
3019
+ }
3020
+ if (status >= 500) {
3021
+ connectionState.fail({
3022
+ operation: "Machine registration",
3023
+ errorCode: String(status),
3024
+ url: `${configuration.serverUrl}/v1/machines`,
3025
+ details: ["Server encountered an error, will retry automatically"]
3026
+ });
3027
+ return createMinimalMachine();
3028
+ }
3029
+ if (status === 404) {
3030
+ connectionState.fail({
3031
+ operation: "Machine registration",
3032
+ errorCode: "404",
3033
+ url: `${configuration.serverUrl}/v1/machines`
3034
+ });
3035
+ return createMinimalMachine();
3036
+ }
3037
+ }
3038
+ throw error;
3039
+ }
3040
+ }
3041
+ sessionSyncClient(session, opts) {
3042
+ return new ApiSessionClient(this.credential.token, session, opts);
3043
+ }
3044
+ async machineSyncClient(machine) {
3045
+ const { ApiMachineClient } = await import('./apiMachine-B9dLW_p5.mjs');
3046
+ return new ApiMachineClient(this.credential.token, machine);
3047
+ }
3048
+ push() {
3049
+ return this.pushClient;
3050
+ }
3051
+ /**
3052
+ * Create an account-encrypted notification producer bound to a session.
3053
+ * The producer derives the account box public key from the stored
3054
+ * credentials (no raw secret seed needed for the dataKey credential shape)
3055
+ * and posts encrypted notifications to the server feed. Best-effort:
3056
+ * failures never propagate into the session loop.
3057
+ */
3058
+ notificationProducer(sessionId, getMetadata) {
3059
+ return new NotificationProducer({
3060
+ credential: this.credential,
3061
+ sessionId,
3062
+ getMetadata
3063
+ });
3064
+ }
3065
+ /**
3066
+ * Read a single value from the account KV store (same HTTP endpoint the
3067
+ * web app uses). Returns null when the key does not exist or on ANY
3068
+ * failure — callers (boardAnalyzer) treat KV as best-effort context.
3069
+ */
3070
+ async kvGet(key) {
3071
+ try {
3072
+ const response = await axios.get(
3073
+ `${configuration.serverUrl}/v1/kv/${encodeURIComponent(key)}`,
3074
+ {
3075
+ headers: {
3076
+ "Authorization": `Bearer ${this.credential.token}`,
3077
+ "X-Happy-Client": `cli-coding-session/${configuration.currentCliVersion}`
3078
+ },
3079
+ timeout: 5e3,
3080
+ validateStatus: (s) => s === 200 || s === 404
3081
+ }
3082
+ );
3083
+ if (response.status === 404) return null;
3084
+ return response.data;
3085
+ } catch (error) {
3086
+ logger.debug(`[API] kvGet(${key}) failed:`, error);
3087
+ return null;
3088
+ }
3089
+ }
3090
+ /**
3091
+ * Register a vendor API token with the server
3092
+ * The token is sent as a JSON string - server handles encryption
3093
+ */
3094
+ async registerVendorToken(vendor, apiKey) {
3095
+ try {
3096
+ const response = await axios.post(
3097
+ `${configuration.serverUrl}/v1/connect/${vendor}/register`,
3098
+ {
3099
+ token: JSON.stringify(apiKey)
3100
+ },
3101
+ {
3102
+ headers: {
3103
+ "Authorization": `Bearer ${this.credential.token}`,
3104
+ "Content-Type": "application/json",
3105
+ "X-Happy-Client": `cli-coding-session/${configuration.currentCliVersion}`
3106
+ },
3107
+ timeout: 5e3
3108
+ }
3109
+ );
3110
+ if (response.status !== 200 && response.status !== 201) {
3111
+ throw new Error(`Server returned status ${response.status}`);
3112
+ }
3113
+ logger.debug(`[API] Vendor token for ${vendor} registered successfully`);
3114
+ } catch (error) {
3115
+ logger.debug(`[API] [ERROR] Failed to register vendor token:`, redactLogValue(error));
3116
+ throw new Error(`Failed to register vendor token: ${error instanceof Error ? error.message : "Unknown error"}`);
3117
+ }
3118
+ }
3119
+ /**
3120
+ * Get vendor API token from the server
3121
+ * Returns the token if it exists, null otherwise
3122
+ */
3123
+ async getVendorToken(vendor) {
3124
+ try {
3125
+ const response = await axios.get(
3126
+ `${configuration.serverUrl}/v1/connect/${vendor}/token`,
3127
+ {
3128
+ headers: {
3129
+ "Authorization": `Bearer ${this.credential.token}`,
3130
+ "Content-Type": "application/json",
3131
+ "X-Happy-Client": `cli-coding-session/${configuration.currentCliVersion}`
3132
+ },
3133
+ timeout: 5e3
3134
+ }
3135
+ );
3136
+ if (response.status === 404) {
3137
+ logger.debug(`[API] No vendor token found for ${vendor}`);
3138
+ return null;
3139
+ }
3140
+ if (response.status !== 200) {
3141
+ throw new Error(`Server returned status ${response.status}`);
3142
+ }
3143
+ logger.debug(`[API] Raw vendor token response:`, {
3144
+ status: response.status,
3145
+ dataKeys: Object.keys(response.data || {}),
3146
+ hasToken: "token" in (response.data || {}),
3147
+ tokenType: typeof response.data?.token
3148
+ });
3149
+ let tokenData = null;
3150
+ if (response.data?.token) {
3151
+ if (typeof response.data.token === "string") {
3152
+ try {
3153
+ tokenData = JSON.parse(response.data.token);
3154
+ } catch (parseError) {
3155
+ logger.debug(`[API] Failed to parse token as JSON, using as string:`, parseError);
3156
+ tokenData = response.data.token;
3157
+ }
3158
+ } else if (response.data.token !== null) {
3159
+ tokenData = response.data.token;
3160
+ } else {
3161
+ logger.debug(`[API] Token is null for ${vendor}, treating as not found`);
3162
+ return null;
3163
+ }
3164
+ } else if (response.data && typeof response.data === "object") {
3165
+ if (response.data.token === null && Object.keys(response.data).length === 1) {
3166
+ logger.debug(`[API] Response contains only null token for ${vendor}, treating as not found`);
3167
+ return null;
3168
+ }
3169
+ tokenData = response.data;
3170
+ }
3171
+ if (tokenData === null || tokenData && typeof tokenData === "object" && tokenData.token === null && Object.keys(tokenData).length === 1) {
3172
+ logger.debug(`[API] Token data is null for ${vendor}`);
3173
+ return null;
3174
+ }
3175
+ logger.debug(`[API] Vendor token for ${vendor} retrieved successfully`, {
3176
+ tokenDataType: typeof tokenData,
3177
+ tokenDataKeys: tokenData && typeof tokenData === "object" ? Object.keys(tokenData) : "not an object"
3178
+ });
3179
+ return tokenData;
3180
+ } catch (error) {
3181
+ if (error.response?.status === 404) {
3182
+ logger.debug(`[API] No vendor token found for ${vendor}`);
3183
+ return null;
3184
+ }
3185
+ logger.debug(`[API] [ERROR] Failed to get vendor token:`, redactLogValue(error));
3186
+ return null;
3187
+ }
3188
+ }
3189
+ /**
3190
+ * B-505: mark a session OFFLINE on the server (active=false, `archivedAt`
3191
+ * untouched) — the same row state a missed keepalive produces, so the web
3192
+ * shows it offline and resumable instead of archived.
3193
+ *
3194
+ * Used when the wrapper exits for an infrastructure reason (SIGTERM /
3195
+ * SIGINT: daemon stop, supervisor restart, machine shutdown) as a
3196
+ * synchronous fallback for the socket `session-end` signal: even if the
3197
+ * socket emit doesn't drain before the process exits, the HTTP response
3198
+ * confirms the row went inactive.
3199
+ *
3200
+ * This MUST NOT hit `/archive`: since B-265 that route writes the
3201
+ * server-owned `archivedAt` tombstone (the only thing that distinguishes
3202
+ * "archived" from "offline"), and the server answers it with a
3203
+ * `session-archive` command that makes the wrapper archive itself. That is
3204
+ * exactly how every dev-sg upgrade archived the sessions mid-turn on
3205
+ * 2026-09-25. A server without `/deactivate` returns 404, which is a
3206
+ * compatible no-op: `session-end` already sets active=false there.
3207
+ */
3208
+ async deactivateSession(sessionId) {
3209
+ try {
3210
+ const response = await axios.post(
3211
+ `${configuration.serverUrl}/v1/sessions/${sessionId}/deactivate`,
3212
+ {},
3213
+ {
3214
+ headers: {
3215
+ "Authorization": `Bearer ${this.credential.token}`,
3216
+ "X-Happy-Client": `cli-coding-session/${configuration.currentCliVersion}`
3217
+ },
3218
+ timeout: 3e3,
3219
+ validateStatus: (status) => status >= 200 && status < 300 || status === 404
3220
+ }
3221
+ );
3222
+ if (response.status === 404) {
3223
+ logger.debug("[API] deactivateSession: server has no /deactivate route; relying on session-end");
3224
+ return false;
3225
+ }
3226
+ return true;
3227
+ } catch (error) {
3228
+ logger.debug("[API] deactivateSession failed:", error);
3229
+ return false;
3230
+ }
3231
+ }
3232
+ /**
3233
+ * Archive a session on the server: writes the `archivedAt` tombstone and
3234
+ * makes the server tell every wrapper of this session to exit. Only for an
3235
+ * intentional end of life — the user's Archive / kill, a crash the wrapper
3236
+ * cannot recover from, a terminal mirror whose pane went back to a shell.
3237
+ * Never call this from a signal handler; see `deactivateSession`.
3238
+ */
3239
+ async archiveSession(sessionId) {
3240
+ try {
3241
+ const response = await axios.post(
3242
+ `${configuration.serverUrl}/v1/sessions/${sessionId}/archive`,
3243
+ {},
3244
+ {
3245
+ headers: {
3246
+ "Authorization": `Bearer ${this.credential.token}`,
3247
+ "X-Happy-Client": `cli-coding-session/${configuration.currentCliVersion}`
3248
+ },
3249
+ timeout: 3e3
3250
+ }
3251
+ );
3252
+ return response.status >= 200 && response.status < 300;
3253
+ } catch (error) {
3254
+ logger.debug("[API] archiveSession failed:", error);
3255
+ return false;
3256
+ }
3257
+ }
3258
+ /**
3259
+ * B-265: one session by id — the message cursor (`seq`), metadata and
3260
+ * agent state with their versions, decrypted with the session key the
3261
+ * reconnecting process already holds. `unsupported` = the server has no
3262
+ * such route (or the row is gone): callers fall back to the legacy
3263
+ * skip-all-history reconnect. Transient failures are retried a few times
3264
+ * first, because falling back silently discards the very message a restore
3265
+ * is meant to deliver.
3266
+ */
3267
+ async getSession(sessionId, encryptionKey, encryptionVariant, opts) {
3268
+ const attempts = opts?.attempts ?? 3;
3269
+ const delays = opts?.delaysMs ?? [500, 1500];
3270
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
3271
+ try {
3272
+ const response = await axios.get(
3273
+ `${configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`,
3274
+ {
3275
+ headers: {
3276
+ "Authorization": `Bearer ${this.credential.token}`,
3277
+ "X-Happy-Client": `cli-coding-session/${configuration.currentCliVersion}`
3278
+ },
3279
+ timeout: opts?.timeoutMs ?? 4e3,
3280
+ validateStatus: () => true
3281
+ }
3282
+ );
3283
+ if (response.status === 404 || response.status === 401 || response.status === 403) {
3284
+ return { ok: false, reason: "unsupported" };
3285
+ }
3286
+ if (response.status >= 200 && response.status < 300) {
3287
+ const row = response.data.session;
3288
+ if (!row || typeof row.seq !== "number" || typeof row.metadata !== "string") {
3289
+ return { ok: false, reason: "unsupported" };
3290
+ }
3291
+ const metadata = decrypt(encryptionKey, encryptionVariant, decodeBase64(row.metadata));
3292
+ if (!metadata) return { ok: false, reason: "unavailable" };
3293
+ const agentState = row.agentState ? decrypt(encryptionKey, encryptionVariant, decodeBase64(row.agentState)) : null;
3294
+ return {
3295
+ ok: true,
3296
+ session: { seq: row.seq, metadata, metadataVersion: row.metadataVersion, agentState, agentStateVersion: row.agentStateVersion }
3297
+ };
3298
+ }
3299
+ logger.debug(`[API] getSession ${sessionId}: status ${response.status} (attempt ${attempt + 1}/${attempts})`);
3300
+ } catch (error) {
3301
+ logger.debug(`[API] getSession ${sessionId} failed (attempt ${attempt + 1}/${attempts}):`, error);
3302
+ }
3303
+ const delayMs = delays[Math.min(attempt, delays.length - 1)];
3304
+ if (attempt < attempts - 1 && delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
3305
+ }
3306
+ return { ok: false, reason: "unavailable" };
3307
+ }
3308
+ /** Clear the server-owned archive tombstone before an intentional resume.
3309
+ * Older servers return 404 because the endpoint does not exist; that is a
3310
+ * compatible no-op because those servers also have no durable tombstone. */
3311
+ async reactivateSession(sessionId) {
3312
+ try {
3313
+ const response = await axios.post(
3314
+ `${configuration.serverUrl}/v1/sessions/${sessionId}/unarchive`,
3315
+ {},
3316
+ {
3317
+ headers: {
3318
+ "Authorization": `Bearer ${this.credential.token}`,
3319
+ "X-Happy-Client": `cli-coding-session/${configuration.currentCliVersion}`
3320
+ },
3321
+ timeout: 3e3,
3322
+ validateStatus: (status) => status >= 200 && status < 300 || status === 404
3323
+ }
3324
+ );
3325
+ return response.status >= 200 && response.status < 300 || response.status === 404;
3326
+ } catch (error) {
3327
+ logger.debug("[API] reactivateSession failed:", error);
3328
+ return false;
3329
+ }
3330
+ }
3331
+ }
3332
+
3333
+ var api = /*#__PURE__*/Object.freeze({
3334
+ __proto__: null,
3335
+ ApiClient: ApiClient,
3336
+ describeSessionCreateError: describeSessionCreateError
3337
+ });
3338
+
3339
+ export { ApiClient as A, TurnActivityTracker as T, ApiSessionClient as a, api as b, connectionState as c, startOfflineReconnection as s };