very-happy-cli 0.2.159 → 0.2.161

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 (259) hide show
  1. package/dist/{AcpBackend-VsfkZiVF.mjs → AcpBackend-6U6pQgx1.mjs} +4 -2
  2. package/dist/{AcpBackend-DUYFlyfN.cjs → AcpBackend-Ba98QDif.cjs} +78 -76
  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-2R0uH2CI.cjs +157 -0
  6. package/dist/BasePermissionHandler-BvvhBTja.mjs +155 -0
  7. package/dist/BaseReasoningProcessor-DyhO1uoU.mjs +198 -0
  8. package/dist/BaseReasoningProcessor-fsVNmCEF.cjs +200 -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-CvsJyeQB.mjs → agentHome-Ck1i1TQB.mjs} +8 -22
  16. package/dist/{agentHome-CHh4lmjM.cjs → agentHome-DvNnGphb.cjs} +14 -28
  17. package/dist/api-B2vLMS-z.cjs +3346 -0
  18. package/dist/api-SffjRQ1b.mjs +3339 -0
  19. package/dist/{types-Dh-s4Lqs.mjs → apiMachine-BpW9t8MO.mjs} +9116 -16205
  20. package/dist/{types-BbvK1Tr_.cjs → apiMachine-gFA54X7y.cjs} +9104 -16247
  21. package/dist/assistantTools-BfA0R3_m.cjs +380 -0
  22. package/dist/assistantTools-CgiTbnWb.mjs +376 -0
  23. package/dist/attachmentContent-CSGxr71v.mjs +82 -0
  24. package/dist/attachmentContent-DMMZzevR.cjs +88 -0
  25. package/dist/attachmentLimits-CzVrqgLR.cjs +354 -0
  26. package/dist/attachmentLimits-yba1uHFO.mjs +348 -0
  27. package/dist/auth-Bt4ceBBy.cjs +227 -0
  28. package/dist/auth-CYHjQ8oQ.mjs +225 -0
  29. package/dist/auth-DdyVZSo6.mjs +218 -0
  30. package/dist/auth-x4CEoysH.cjs +220 -0
  31. package/dist/authRelay-BdYkC1Po.cjs +156 -0
  32. package/dist/authRelay-DSAvg1uv.mjs +143 -0
  33. package/dist/{auto-DKmEls8Z.cjs → auto-BDXDUnjB.cjs} +70 -79
  34. package/dist/{auto-nb8GhRRy.mjs → auto-CXLlRi3a.mjs} +27 -36
  35. package/dist/backgroundTaskActivity-8PdPxrvy.mjs +149 -0
  36. package/dist/backgroundTaskActivity-CBvUdj39.cjs +157 -0
  37. package/dist/bootstrap-BNQq05jR.mjs +112 -0
  38. package/dist/bootstrap-CkgIq8N3.cjs +116 -0
  39. package/dist/browser-9pyhQr1R.mjs +20 -0
  40. package/dist/browser-BDOlKWYe.cjs +22 -0
  41. package/dist/claudeCliPath-BqkaH2oR.mjs +8 -0
  42. package/dist/claudeCliPath-U4HHmV4r.cjs +10 -0
  43. package/dist/claudeLocal-BDuGFnjJ.cjs +343 -0
  44. package/dist/claudeLocal-CQa8qnwq.mjs +339 -0
  45. package/dist/claudeLocalLauncher-Bd5I2-j6.cjs +154 -0
  46. package/dist/claudeLocalLauncher-CDqQI9ak.mjs +152 -0
  47. package/dist/cliUpdate-DsKXPFYp.cjs +120 -0
  48. package/dist/cliUpdate-Y34VGe9Y.mjs +112 -0
  49. package/dist/client-Brn-UwVk.mjs +100 -0
  50. package/dist/client-CXb1VJui.mjs +112 -0
  51. package/dist/client-C_axwRry.cjs +104 -0
  52. package/dist/client-DOBWJL3u.cjs +119 -0
  53. package/dist/codex/happyMcpStdioBridge.cjs +33 -14
  54. package/dist/codex/happyMcpStdioBridge.mjs +26 -7
  55. package/dist/codexAppServerClient-ZzhUTWPo.cjs +1090 -0
  56. package/dist/codexAppServerClient-oeQKD6Qf.mjs +1088 -0
  57. package/dist/codexCommand-C7H0NpBe.cjs +2088 -0
  58. package/dist/codexCommand-CJ82bl3l.mjs +2086 -0
  59. package/dist/codexThreadFork-BCaEMBiM.mjs +338 -0
  60. package/dist/codexThreadFork-BZqF-9bX.cjs +350 -0
  61. package/dist/{config-ClLtM6F3.mjs → config-BglnktIo.mjs} +7 -40
  62. package/dist/{config-DLx-vW3I.cjs → config-CYpL1Ahd.cjs} +26 -59
  63. package/dist/configuration-DoWFJUPm.mjs +170 -0
  64. package/dist/configuration-v9f8LM9T.cjs +181 -0
  65. package/dist/connect-hgcNjyAB.mjs +682 -0
  66. package/dist/connect-yEvjdRvx.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-B45vMtSZ.cjs +69 -0
  72. package/dist/context-C9GHPLEm.mjs +65 -0
  73. package/dist/controlClient-DjoKTtgK.mjs +260 -0
  74. package/dist/controlClient-fEHE85bH.cjs +279 -0
  75. package/dist/createSessionMetadata-BRLB9CNO.cjs +58 -0
  76. package/dist/createSessionMetadata-mIeehclp.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-DKDP_mM-.cjs +507 -0
  82. package/dist/deterministicJson-p835AdAU.mjs +502 -0
  83. package/dist/doctor-BPkYWSuq.mjs +75 -0
  84. package/dist/doctor-BeUuQXFs.mjs +391 -0
  85. package/dist/doctor-hVNnSZJn.cjs +395 -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-CO7d1gOC.mjs +61 -0
  92. package/dist/ensureDaemonRunning-Dg9aZfjs.cjs +64 -0
  93. package/dist/handleResumeCommand-BiTPK0qd.mjs +240 -0
  94. package/dist/handleResumeCommand-DnVmy7a3.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-2mCaeKyf.mjs → index-CAFwetLk.mjs} +68 -38
  99. package/dist/index-Cqx-Raq6.mjs +68 -0
  100. package/dist/index-D3kI0hDR.cjs +107 -0
  101. package/dist/index-DA7yINuk.cjs +994 -0
  102. package/dist/{index-BEOMECtE.cjs → index-LIn6nCcq.cjs} +121 -91
  103. package/dist/index-Pf2cV9cn.cjs +182 -0
  104. package/dist/index-SPGPenK5.mjs +158 -0
  105. package/dist/index-gNTWtSKN.cjs +71 -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-Bu85vlHD.cjs +88 -0
  111. package/dist/install-ClUUl3yp.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-C2dI0FAN.mjs +297 -0
  122. package/dist/lidState-KHHu1015.cjs +305 -0
  123. package/dist/limits-C-a7gr04.cjs +43 -0
  124. package/dist/limits-DIo7rEBR.mjs +36 -0
  125. package/dist/localHappyAgentAuth-BXiHPXdC.cjs +86 -0
  126. package/dist/localHappyAgentAuth-DCsetiyG.mjs +82 -0
  127. package/dist/logger-Bc8-u1w0.cjs +275 -0
  128. package/dist/logger-fXoNzNuB.mjs +271 -0
  129. package/dist/loginShellEnv-BkuYtExZ.cjs +182 -0
  130. package/dist/loginShellEnv-Dw8M7tkI.mjs +167 -0
  131. package/dist/machineMetadata-BHjehxJY.mjs +38 -0
  132. package/dist/machineMetadata-BibDMFVx.cjs +41 -0
  133. package/dist/main-BktM4A1K.mjs +939 -0
  134. package/dist/main-CLaX-t-b.cjs +941 -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-CeUd9KDr.mjs} +27 -33
  138. package/dist/{mcp-CPYbtsYG.cjs → mcp-GBfE70nk.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-Cl5BEP5x.cjs +173 -0
  146. package/dist/package-uOg9SCKi.mjs +171 -0
  147. package/dist/path-B7zW-WwE.cjs +18 -0
  148. package/dist/path-D2wdaNSk.mjs +15 -0
  149. package/dist/peerTools-CIeiy9wn.cjs +283 -0
  150. package/dist/peerTools-Dz3_7Bkk.mjs +272 -0
  151. package/dist/permissionMode-CH4FoZfU.mjs +80 -0
  152. package/dist/permissionMode-CrV-XuMs.cjs +87 -0
  153. package/dist/persistence-CBNMzSa8.cjs +414 -0
  154. package/dist/persistence-oET5n61f.mjs +372 -0
  155. package/dist/piExtension.mjs +2 -1
  156. package/dist/piRunnerArgs-Bf7RUF__.cjs +66 -0
  157. package/dist/piRunnerArgs-Dk1ncwHF.mjs +59 -0
  158. package/dist/{piRuntime-NtF0YqjH.cjs → piRuntime-B_6TE9Eb.cjs} +4 -4
  159. package/dist/{piRuntime-kLZNpAAl.mjs → piRuntime-BwRdhK_N.mjs} +2 -2
  160. package/dist/{piTerminal-B0fEOV_2.mjs → piTerminal-Bap3R8Re.mjs} +32 -35
  161. package/dist/{piTerminal-9J0GAKzb.cjs → piTerminal-CagmEoez.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-BjSV2Skz.cjs +538 -0
  167. package/dist/registerCommonHandlers-CrgvJsOy.mjs +536 -0
  168. package/dist/registerKillSessionHandler-6TC2S3lf.cjs +366 -0
  169. package/dist/registerKillSessionHandler-Cbc8Rgtd.mjs +363 -0
  170. package/dist/remoteSessionClient-BNx78NLb.cjs +634 -0
  171. package/dist/remoteSessionClient-CO7qOjR0.mjs +612 -0
  172. package/dist/resources-BdFBQi8r.mjs +117 -0
  173. package/dist/resources-JDPugcDq.cjs +120 -0
  174. package/dist/run-2l5vSt2h.mjs +5743 -0
  175. package/dist/run-D5xAEWSB.cjs +5766 -0
  176. package/dist/runClaude-BX6ixjtW.cjs +894 -0
  177. package/dist/runClaude-BgN6xo05.mjs +892 -0
  178. package/dist/runClaudeDeps-Bp20MScE.cjs +4168 -0
  179. package/dist/runClaudeDeps-COBBhD9o.mjs +4129 -0
  180. package/dist/{runGemini-F-mqx3_K.cjs → runGemini-CSMPkt0p.cjs} +202 -175
  181. package/dist/{runGemini-DV6Ouadt.mjs → runGemini-Cc2jq-uh.mjs} +58 -31
  182. package/dist/{runOpenClaw-D7MlPS5n.mjs → runOpenClaw-CIlCwdW2.mjs} +31 -32
  183. package/dist/{runOpenClaw-Qqo9j63M.cjs → runOpenClaw-YBwua4Rf.cjs} +64 -65
  184. package/dist/sandbox-C4gXLP1p.cjs +187 -0
  185. package/dist/sandbox-CNkwOtHI.mjs +180 -0
  186. package/dist/selfCheck-CU8xSDsl.cjs +87 -0
  187. package/dist/selfCheck-hJ6PCh7v.mjs +84 -0
  188. package/dist/{send-CLhYXNgy.mjs → send-CGHcKZ-w.mjs} +12 -5
  189. package/dist/{send-4qJTCAYW.cjs → send-DY9Nr-x4.cjs} +16 -9
  190. package/dist/server-II51Mimf.cjs +407 -0
  191. package/dist/server-m2b5pamZ.mjs +404 -0
  192. package/dist/sessionLock-7ePAYDOg.cjs +205 -0
  193. package/dist/sessionLock-Cw6FmbMv.mjs +197 -0
  194. package/dist/sessionModeFile-BVCV5f7E.cjs +36 -0
  195. package/dist/sessionModeFile-CDmDyRkD.mjs +32 -0
  196. package/dist/sessionOps-C_f1wKOr.mjs +760 -0
  197. package/dist/sessionOps-DJdExmBy.cjs +783 -0
  198. package/dist/sessionScanner-CaAHLyNV.mjs +206 -0
  199. package/dist/sessionScanner-D1goKdd3.cjs +209 -0
  200. package/dist/{sessions-Bh1pyddi.cjs → sessions-DGs6ALNo.cjs} +37 -28
  201. package/dist/{sessions-CzIH2rGp.mjs → sessions-DnvmWUel.mjs} +17 -8
  202. package/dist/setupOfflineReconnection-CVViqZft.mjs +72 -0
  203. package/dist/setupOfflineReconnection-R7XuR_sj.cjs +74 -0
  204. package/dist/smallFastModel-BLNgMrwR.mjs +18 -0
  205. package/dist/smallFastModel-D_uxGMo3.cjs +21 -0
  206. package/dist/{spawn-CmFOSVdW.cjs → spawn-CXTH7qg1.cjs} +61 -65
  207. package/dist/{spawn-CPIWlPOM.mjs → spawn-DNYSvFtC.mjs} +35 -40
  208. package/dist/spawnAgents-BGQiEF-w.cjs +52 -0
  209. package/dist/spawnAgents-DZzZ9jMI.mjs +45 -0
  210. package/dist/spawnHappyCLI-BhYj84rv.cjs +66 -0
  211. package/dist/spawnHappyCLI-cqWQbwsb.mjs +64 -0
  212. package/dist/specialCommands-BSqhYg5X.mjs +52 -0
  213. package/dist/specialCommands-DSsTeRej.cjs +54 -0
  214. package/dist/startFileWatcher-CKKx-8ZO.cjs +71 -0
  215. package/dist/startFileWatcher-CdegMbJj.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-CVk4QERO.cjs +71 -0
  221. package/dist/sync-CxD-w_40.mjs +69 -0
  222. package/dist/systemPrompt-BygJcmbi.cjs +97 -0
  223. package/dist/systemPrompt-DmtypUc7.mjs +94 -0
  224. package/dist/{teams-CEahj4cm.mjs → teams-BOC6nIPL.mjs} +9 -40
  225. package/dist/{teams-CB1R9-Z9.cjs → teams-BsvpOBfn.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-BZNdNiIV.mjs +38 -0
  233. package/dist/time-CNRE4ChD.cjs +42 -0
  234. package/dist/titleGenerator-CFRhU-xq.cjs +172 -0
  235. package/dist/titleGenerator-ox6ptCsp.mjs +166 -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-B-KB7oAB.cjs} +15 -12
  239. package/dist/{todo-Cknp5D4U.mjs → todo-C5KBVbUw.mjs} +11 -8
  240. package/dist/tools-BZabNIid.mjs +177 -0
  241. package/dist/tools-BmQQUATY.mjs +60 -0
  242. package/dist/tools-CsSovyNp.cjs +180 -0
  243. package/dist/tools-lMDra4Pv.cjs +63 -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-B1WLtC1d.mjs +47 -0
  249. package/dist/uninstall-fvA5aWpU.cjs +49 -0
  250. package/package.json +7 -7
  251. package/scripts/session_hook_forwarder.cjs +9 -2
  252. package/dist/agentGuidance-BRDN1NoZ.cjs +0 -2217
  253. package/dist/agentGuidance-KgvYuF3R.mjs +0 -2145
  254. package/dist/index-BF6WfgV5.mjs +0 -19624
  255. package/dist/index-C-JazrMB.mjs +0 -52
  256. package/dist/index-blDSadUR.cjs +0 -58
  257. package/dist/index-rH3DrQ5B.cjs +0 -19688
  258. package/dist/limits-BH5CV5vR.cjs +0 -2683
  259. package/dist/limits-CjRj8IX5.mjs +0 -2558
@@ -0,0 +1,4168 @@
1
+ 'use strict';
2
+
3
+ var logger = require('./logger-Bc8-u1w0.cjs');
4
+ var controlClient = require('./controlClient-fEHE85bH.cjs');
5
+ var backgroundTaskActivity = require('./backgroundTaskActivity-CBvUdj39.cjs');
6
+ var node_crypto = require('node:crypto');
7
+ var messageBuffer = require('./messageBuffer-fJMH6uY8.cjs');
8
+ var index = require('./index-D3kI0hDR.cjs');
9
+ var claudeAgentSdk = require('@anthropic-ai/claude-agent-sdk');
10
+ var permissionMode = require('./permissionMode-CrV-XuMs.cjs');
11
+ var systemPrompt = require('./systemPrompt-BygJcmbi.cjs');
12
+ var path$1 = require('node:path');
13
+ var specialCommands = require('./specialCommands-DSsTeRej.cjs');
14
+ require('axios');
15
+ require('node:events');
16
+ require('socket.io-client');
17
+ var attachmentLimits = require('./attachmentLimits-CzVrqgLR.cjs');
18
+ require('tweetnacl');
19
+ var configuration = require('./configuration-v9f8LM9T.cjs');
20
+ require('./index-DA7yINuk.cjs');
21
+ require('./registerCommonHandlers-BjSV2Skz.cjs');
22
+ require('os');
23
+ require('child_process');
24
+ require('@paralleldrive/cuid2');
25
+ require('./persistence-CBNMzSa8.cjs');
26
+ var contentLogMetadata = require('./contentLogMetadata-ZSFk_NjQ.cjs');
27
+ require('chalk');
28
+ var types = require('./types-CRS-srZK.cjs');
29
+ var path = require('./path-B7zW-WwE.cjs');
30
+ var fs = require('fs/promises');
31
+ var time = require('./time-CNRE4ChD.cjs');
32
+ var node_fs = require('node:fs');
33
+ var deterministicJson = require('./deterministicJson-DKDP_mM-.cjs');
34
+ var sessionScanner = require('./sessionScanner-D1goKdd3.cjs');
35
+ var node_child_process = require('node:child_process');
36
+ var streamRelay = require('./streamRelay-BiKw59Cc.cjs');
37
+ var attachmentContent = require('./attachmentContent-DMMZzevR.cjs');
38
+ var registerKillSessionHandler = require('./registerKillSessionHandler-6TC2S3lf.cjs');
39
+ var doctor = require('./doctor-hVNnSZJn.cjs');
40
+ var node_http = require('node:http');
41
+ var editPaths = require('./editPaths-Bqp4OFPw.cjs');
42
+ var projectPath = require('./projectPath-ChLhdOyt.cjs');
43
+ var titleGenerator = require('./titleGenerator-CFRhU-xq.cjs');
44
+ var oneShotPrompts = require('./oneShotPrompts-CL0dip9C.cjs');
45
+ require('node:util');
46
+ require('./smallFastModel-D_uxGMo3.cjs');
47
+ require('./trimIdent-4gvk9Wk6.cjs');
48
+ require('node:os');
49
+ require('./agentGuidance-B5NATuFa.cjs');
50
+ require('zod');
51
+ require('./package-Cl5BEP5x.cjs');
52
+ require('node:fs/promises');
53
+ require('util');
54
+ require('crypto');
55
+ require('path');
56
+ require('cross-spawn');
57
+ require('./authRelay-BdYkC1Po.cjs');
58
+ require('./client-C_axwRry.cjs');
59
+ require('./context-B45vMtSZ.cjs');
60
+ require('./sessionOps-DJdExmBy.cjs');
61
+ require('./resources-JDPugcDq.cjs');
62
+ require('./tools-lMDra4Pv.cjs');
63
+ require('./tools-CsSovyNp.cjs');
64
+ require('./client-DOBWJL3u.cjs');
65
+ require('./peerTools-CIeiy9wn.cjs');
66
+ require('./ids-B6ZC0HGt.cjs');
67
+ require('./remoteSessionClient-BNx78NLb.cjs');
68
+ require('@modelcontextprotocol/sdk/server/mcp.js');
69
+ require('@modelcontextprotocol/sdk/server/streamableHttp.js');
70
+ require('./limits-C-a7gr04.cjs');
71
+ require('./assistantTools-BfA0R3_m.cjs');
72
+ require('./terminals-B_bfC9c2.cjs');
73
+ require('./terminalTitleSuggest-DhvRFX3H.cjs');
74
+ require('./tmuxSocket-D-Xt6Vn-.cjs');
75
+ require('./bootstrap-CkgIq8N3.cjs');
76
+ require('./previewPath-B0rBdhy1.cjs');
77
+ require('./sync-CVk4QERO.cjs');
78
+ require('./startFileWatcher-CKKx-8ZO.cjs');
79
+ require('./doctor-u46qYYKX.cjs');
80
+ require('ps-list');
81
+ require('./doctorReadiness-BCZGcN0a.cjs');
82
+ require('./detectCLI-DR9ZaN6B.cjs');
83
+ require('fs');
84
+ require('./cliUpdate-DsKXPFYp.cjs');
85
+ require('url');
86
+ require('node:module');
87
+
88
+ class Session {
89
+ path;
90
+ logPath;
91
+ api;
92
+ client;
93
+ queue;
94
+ claudeEnvVars;
95
+ claudeArgs;
96
+ // Made mutable to allow filtering
97
+ mcpServers;
98
+ allowedTools;
99
+ sandboxConfig;
100
+ _onModeChange;
101
+ _onAbort;
102
+ /** Path to temporary settings file with SessionStart hook (required for session tracking) */
103
+ hookSettingsPath;
104
+ /** JavaScript runtime to use for spawning Claude Code (default: 'node') */
105
+ jsRuntime;
106
+ sessionId;
107
+ mode = "local";
108
+ thinking = false;
109
+ /** B-507: background tasks still in flight (async sub-agents, backgrounded
110
+ * commands, monitors) — published with every keepAlive. Always an array
111
+ * for the Claude runner: an empty one says "nothing running", which is a
112
+ * different statement from a runner that has no such notion (undefined). */
113
+ backgroundTasks = [];
114
+ /**
115
+ * Window after a user-initiated abort during which stray promise rejections
116
+ * from tearing down the SDK query (MCP transports, in-flight tool calls,
117
+ * the aborted streaming generator) are benign teardown noise — NOT a real
118
+ * crash. The process-level unhandledRejection/uncaughtException handlers
119
+ * must not treat them as fatal and force-archive the session (the reported
120
+ * "点终止就 archive" bug). Self-expiring so a genuine later crash still
121
+ * archives normally.
122
+ */
123
+ abortingUntil = 0;
124
+ markAborting = (windowMs = 8e3) => {
125
+ this.abortingUntil = Date.now() + windowMs;
126
+ };
127
+ isAborting = () => Date.now() < this.abortingUntil;
128
+ /**
129
+ * Account-encrypted notification producer. Set lazily once we have a
130
+ * server session id (notifications are addressed per session). May be
131
+ * null in offline/degraded paths — all call sites must null-check.
132
+ */
133
+ notificationProducer = null;
134
+ /**
135
+ * Whether Claude produced assistant text output during the current turn.
136
+ * Used at turn end to distinguish `reply_done` (Claude replied) from
137
+ * `input_needed` (turn ended idle without a reply, e.g. after an abort).
138
+ */
139
+ hadAssistantOutputThisTurn = false;
140
+ /** Last assistant text snippet seen this turn, used as the reply_done body. */
141
+ lastAssistantSnippet;
142
+ /** Live remote-query input sink. Absent in local mode and between SDK queries. */
143
+ steerHandler = null;
144
+ /** B-482: the live remote Query's in-process side-question entry; null between Queries and in local mode. */
145
+ sideQuestionLive = null;
146
+ /** B-515: consumed by the first remote launch; later launches never prewarm. */
147
+ claudePrewarm = null;
148
+ /** Hand the prewarm lease to the first remote launch — once per process. */
149
+ takeClaudePrewarm = () => {
150
+ const factory = this.claudePrewarm;
151
+ this.claudePrewarm = null;
152
+ if (!factory || this.sessionId) return null;
153
+ try {
154
+ return factory();
155
+ } catch (error) {
156
+ logger.logger.debug("[Session] Claude prewarm setup failed:", error);
157
+ return null;
158
+ }
159
+ };
160
+ /** Callbacks to be notified when session ID is found/changed */
161
+ sessionFoundCallbacks = [];
162
+ /** Keep alive interval reference for cleanup */
163
+ keepAliveInterval;
164
+ constructor(opts) {
165
+ this.path = opts.path;
166
+ this.api = opts.api;
167
+ this.client = opts.client;
168
+ this.logPath = opts.logPath;
169
+ this.sessionId = opts.sessionId;
170
+ this.queue = opts.messageQueue;
171
+ this.claudeEnvVars = opts.claudeEnvVars;
172
+ this.claudeArgs = opts.claudeArgs;
173
+ this.mcpServers = opts.mcpServers;
174
+ this.allowedTools = opts.allowedTools;
175
+ this.sandboxConfig = opts.sandboxConfig;
176
+ this._onModeChange = opts.onModeChange;
177
+ this._onAbort = opts.onAbort;
178
+ this.hookSettingsPath = opts.hookSettingsPath;
179
+ this.jsRuntime = opts.jsRuntime ?? "node";
180
+ this.claudePrewarm = opts.claudePrewarm ?? null;
181
+ try {
182
+ this.notificationProducer = opts.api.notificationProducer(
183
+ opts.client.sessionId,
184
+ () => opts.client.getMetadata()
185
+ );
186
+ } catch (err) {
187
+ logger.logger.debug("[Session] Failed to create notification producer:", err);
188
+ this.notificationProducer = null;
189
+ }
190
+ this.client.keepAlive(this.thinking, this.mode, this.backgroundTasks);
191
+ this.keepAliveInterval = setInterval(() => {
192
+ this.client.keepAlive(this.thinking, this.mode, this.backgroundTasks);
193
+ }, 2e3);
194
+ }
195
+ /**
196
+ * B-507: the tracker's current set (`claude/backgroundTasks.ts`). A change
197
+ * beats at once so the daemon, agentState and the web see the edge without
198
+ * waiting for the 2s tick; an unchanged set is left to the interval.
199
+ */
200
+ setBackgroundTasks = (tasks) => {
201
+ if (backgroundTaskActivity.sameBackgroundTasks(this.backgroundTasks, tasks)) return;
202
+ this.backgroundTasks = tasks;
203
+ this.client.keepAlive(this.thinking, this.mode, this.backgroundTasks);
204
+ };
205
+ /**
206
+ * Cleanup resources (call when session is no longer needed)
207
+ */
208
+ cleanup = () => {
209
+ clearInterval(this.keepAliveInterval);
210
+ this.sessionFoundCallbacks = [];
211
+ this.steerHandler = null;
212
+ this.sideQuestionLive = null;
213
+ logger.logger.debug("[Session] Cleaned up resources");
214
+ };
215
+ /** Register the current remote query's in-process side-question entry (B-482); null removes it. */
216
+ setSideQuestionLive = (live) => {
217
+ this.sideQuestionLive = live;
218
+ };
219
+ getSideQuestionLive = () => this.sideQuestionLive;
220
+ /** Register the current remote query's Steer sink; null removes it. */
221
+ setSteerHandler = (handler) => {
222
+ this.steerHandler = handler;
223
+ };
224
+ /**
225
+ * Attempt same-turn delivery. A stale Web "working" snapshot must never
226
+ * lose a message: once the turn is no longer thinking, return false and
227
+ * let runClaude fall back to the ordinary next-turn queue.
228
+ */
229
+ trySteer = async (input) => {
230
+ if (!this.thinking || !this.steerHandler) return false;
231
+ return this.steerHandler(input);
232
+ };
233
+ onThinkingChange = (thinking) => {
234
+ if (thinking && !this.thinking) {
235
+ this.hadAssistantOutputThisTurn = false;
236
+ this.lastAssistantSnippet = void 0;
237
+ }
238
+ this.thinking = thinking;
239
+ this.client.keepAlive(thinking, this.mode, this.backgroundTasks);
240
+ };
241
+ /**
242
+ * Record that Claude emitted an assistant text reply during this turn.
243
+ * Called from the message stream so turn-end can fire `reply_done` with a
244
+ * meaningful snippet.
245
+ */
246
+ noteAssistantOutput = (snippet) => {
247
+ this.hadAssistantOutputThisTurn = true;
248
+ if (snippet) {
249
+ this.lastAssistantSnippet = snippet;
250
+ }
251
+ };
252
+ /**
253
+ * A turn has ended (the agent yielded control back, e.g. SDK `result`).
254
+ * Produces the appropriate notification:
255
+ * - `reply_done` when Claude produced output this turn,
256
+ * - `input_needed` when the session is now idle awaiting user input
257
+ * (not controlled by user, no pending permission requests, not thinking).
258
+ *
259
+ * `idle` is true when there are no pending/queued messages — the producer
260
+ * caller (launcher) knows this. The pending-requests / controlledByUser
261
+ * checks are made here against the latest agent state.
262
+ */
263
+ onTurnEnd = (idle) => {
264
+ if (idle) {
265
+ const state2 = this.client.getAgentState();
266
+ const hasPendingRequests2 = !!state2?.requests && Object.keys(state2.requests).length > 0;
267
+ if (!hasPendingRequests2 && state2?.controlledByUser !== true && !this.thinking) {
268
+ this.reportEventToDaemon("completed");
269
+ }
270
+ }
271
+ const producer = this.notificationProducer;
272
+ if (!producer) return;
273
+ if (this.hadAssistantOutputThisTurn) {
274
+ producer.replyDone(this.lastAssistantSnippet);
275
+ this.hadAssistantOutputThisTurn = false;
276
+ this.lastAssistantSnippet = void 0;
277
+ return;
278
+ }
279
+ if (!idle) {
280
+ return;
281
+ }
282
+ const state = this.client.getAgentState();
283
+ const hasPendingRequests = !!state?.requests && Object.keys(state.requests).length > 0;
284
+ const controlledByUser = state?.controlledByUser === true;
285
+ if (!hasPendingRequests && !controlledByUser && !this.thinking) {
286
+ producer.inputNeeded();
287
+ }
288
+ };
289
+ /**
290
+ * B-069: best-effort state-transition report to the local daemon
291
+ * (/session-event). Only assistant-dispatched sessions carry
292
+ * HAPPY_SPAWNED_BY, so ordinary sessions never POST. Fire-and-forget —
293
+ * a missing/old daemon must never disturb the session loop.
294
+ */
295
+ reportEventToDaemon = (event) => {
296
+ const spawnedBy = process.env.HAPPY_SPAWNED_BY;
297
+ if (!spawnedBy) return;
298
+ void controlClient.notifyDaemonSessionEvent(this.client.sessionId, event, spawnedBy).catch((error) => logger.logger.debug("[Session] Failed to report session event to daemon:", error));
299
+ };
300
+ /** Produce an `error` notification for an unexpected session failure. */
301
+ onSessionError = (message) => {
302
+ this.notificationProducer?.error(message);
303
+ };
304
+ onModeChange = (mode) => {
305
+ this.mode = mode;
306
+ this.client.keepAlive(this.thinking, mode, this.backgroundTasks);
307
+ this._onModeChange(mode);
308
+ };
309
+ onAbort = () => {
310
+ this._onAbort?.();
311
+ };
312
+ /**
313
+ * Called when Claude session ID is discovered or changed.
314
+ *
315
+ * This is triggered by the SessionStart hook when:
316
+ * - Claude starts a new session (fresh start)
317
+ * - Claude resumes a session (--continue, --resume flags)
318
+ * - Claude forks a session (/compact, double-escape fork)
319
+ *
320
+ * Updates internal state, syncs to API metadata, and notifies
321
+ * all registered callbacks (e.g., SessionScanner) about the change.
322
+ */
323
+ onSessionFound = (sessionId) => {
324
+ this.sessionId = sessionId;
325
+ this.client.updateMetadata((metadata) => ({
326
+ ...metadata,
327
+ claudeSessionId: sessionId
328
+ }));
329
+ logger.logger.debug(`[Session] Claude Code session ID ${sessionId} added to metadata`);
330
+ for (const callback of this.sessionFoundCallbacks) {
331
+ callback(sessionId);
332
+ }
333
+ };
334
+ /**
335
+ * Register a callback to be notified when session ID is found/changed
336
+ */
337
+ addSessionFoundCallback = (callback) => {
338
+ this.sessionFoundCallbacks.push(callback);
339
+ };
340
+ /**
341
+ * Remove a session found callback
342
+ */
343
+ removeSessionFoundCallback = (callback) => {
344
+ const index = this.sessionFoundCallbacks.indexOf(callback);
345
+ if (index !== -1) {
346
+ this.sessionFoundCallbacks.splice(index, 1);
347
+ }
348
+ };
349
+ /**
350
+ * Clear the current session ID (used by /clear command)
351
+ */
352
+ clearSessionId = () => {
353
+ this.sessionId = null;
354
+ logger.logger.debug("[Session] Session ID cleared");
355
+ };
356
+ /**
357
+ * Consume one-time Claude flags from claudeArgs after Claude spawn
358
+ * Handles: --resume (with or without session ID), --continue
359
+ */
360
+ consumeOneTimeFlags = () => {
361
+ if (!this.claudeArgs) return;
362
+ const filteredArgs = [];
363
+ for (let i = 0; i < this.claudeArgs.length; i++) {
364
+ const arg = this.claudeArgs[i];
365
+ if (arg === "--continue") {
366
+ logger.logger.debug("[Session] Consumed --continue flag");
367
+ continue;
368
+ }
369
+ if (arg === "--resume") {
370
+ if (i + 1 < this.claudeArgs.length) {
371
+ const nextArg = this.claudeArgs[i + 1];
372
+ if (!nextArg.startsWith("-") && nextArg.includes("-")) {
373
+ i++;
374
+ logger.logger.debug(`[Session] Consumed --resume flag with session ID: ${nextArg}`);
375
+ } else {
376
+ logger.logger.debug("[Session] Consumed --resume flag (no session ID)");
377
+ }
378
+ } else {
379
+ logger.logger.debug("[Session] Consumed --resume flag (no session ID)");
380
+ }
381
+ continue;
382
+ }
383
+ filteredArgs.push(arg);
384
+ }
385
+ this.claudeArgs = filteredArgs.length > 0 ? filteredArgs : void 0;
386
+ logger.logger.debug(`[Session] Consumed one-time flags, remaining args:`, this.claudeArgs);
387
+ };
388
+ }
389
+
390
+ const text = (value) => typeof value === "string" && value.length > 0 && value.length <= 1024 && !value.includes("\0") ? value : null;
391
+ const publicMcpStatus = (servers) => servers.map(({ name, status, serverInfo, scope, tools }) => ({ name, status, serverInfo, scope, tools: tools?.map(({ name: name2 }) => ({ name: name2 })) }));
392
+ const fingerprint = (result) => JSON.stringify([result.canRewind, [...result.filesChanged ?? []].sort(), result.insertions, result.deletions]);
393
+ function isRuntimeCheckpointReplay(message) {
394
+ const m = message;
395
+ return m.type === "user" && m.isReplay === true && !m.parent_tool_use_id && !(Array.isArray(m.message?.content) && m.message.content.some((block) => block.type === "tool_result"));
396
+ }
397
+ function createRuntimeControls(isThinking) {
398
+ let query = null, generation = node_crypto.randomInt(0, 2 ** 40), activityRevision = 0;
399
+ const operations = /* @__PURE__ */ new Map();
400
+ const confirmations = /* @__PURE__ */ new Map();
401
+ const tasks = /* @__PURE__ */ new Map();
402
+ const checkpoints = /* @__PURE__ */ new Map();
403
+ let pending = false;
404
+ const setQuery = (next) => {
405
+ for (const operation of operations.values()) if (operation.status === "running") {
406
+ operation.status = "failed";
407
+ operation.error = "Claude query ended before this operation completed";
408
+ }
409
+ query = next;
410
+ generation++;
411
+ pending = false;
412
+ confirmations.clear();
413
+ tasks.clear();
414
+ checkpoints.clear();
415
+ };
416
+ function observe(message) {
417
+ const m = message;
418
+ if (m.type === "user" || m.type === "assistant") {
419
+ activityRevision++;
420
+ confirmations.clear();
421
+ }
422
+ const content = typeof m.message?.content === "string" ? m.message.content : Array.isArray(m.message?.content) ? m.message.content.filter((block) => block.type === "text").map((block) => block.text).join(" ") : "";
423
+ if (isRuntimeCheckpointReplay(message) && text(m.uuid) && content) {
424
+ checkpoints.set(m.uuid, { id: m.uuid, title: content.slice(0, 120) });
425
+ if (checkpoints.size > 100) checkpoints.delete(checkpoints.keys().next().value);
426
+ }
427
+ if (m.type === "system" && text(m.task_id) && ["task_started", "task_updated", "task_progress", "task_notification"].includes(m.subtype)) {
428
+ const previous = tasks.get(m.task_id);
429
+ if (m.subtype !== "task_notification" && ["completed", "failed", "stopped", "cancelled"].includes(String(previous?.status))) return;
430
+ tasks.set(m.task_id, {
431
+ ...previous,
432
+ id: m.task_id,
433
+ ...text(m.tool_use_id) ? { toolUseId: m.tool_use_id } : {},
434
+ ...text(m.description) ? { description: m.description } : {},
435
+ ...text(m.task_type) ? { type: m.task_type } : {},
436
+ status: m.subtype === "task_notification" ? m.status ?? "completed" : "running"
437
+ });
438
+ if (tasks.size > 100) tasks.delete(tasks.keys().next().value);
439
+ }
440
+ }
441
+ function request(input) {
442
+ const r = input && typeof input === "object" ? input : {};
443
+ if (r.action === "status") return { queryGeneration: generation, active: !!query, isRunning: isThinking(), canRewind: !!query && !isThinking() && !pending && query.canControl(), busy: pending || !!query && !query.canControl(), tasks: [...tasks.values()], checkpoints: [...checkpoints.values()], operations: [...operations.values()].filter((op2) => op2.queryGeneration === generation) };
444
+ if (r.action === "operation") {
445
+ const op2 = operations.get(String(r.operationId));
446
+ return op2 ? { ...op2 } : { error: "Operation not found" };
447
+ }
448
+ const q = query;
449
+ if (!q) return { error: "No active Claude SDK query" };
450
+ if (pending || !q.canControl()) return { error: "Claude is waiting for an interaction or another control operation; retry after it finishes" };
451
+ const action = String(r.action), server = text(r.serverName), task = text(r.taskId), target = text(r.userMessageId);
452
+ const runGeneration = generation, runRevision = activityRevision;
453
+ let execute;
454
+ switch (action) {
455
+ case "reload-skills":
456
+ execute = () => q.reloadSkills();
457
+ break;
458
+ case "reload-plugins":
459
+ execute = async () => {
460
+ const result = await q.reloadPlugins();
461
+ return { ...result, mcpServers: publicMcpStatus(result.mcpServers ?? []) };
462
+ };
463
+ break;
464
+ case "mcp-status":
465
+ execute = async () => publicMcpStatus(await q.mcpServerStatus());
466
+ break;
467
+ case "mcp-reconnect":
468
+ if (!server) return { error: "Invalid server name" };
469
+ execute = () => q.reconnectMcpServer(server);
470
+ break;
471
+ case "mcp-toggle":
472
+ if (!server || typeof r.enabled !== "boolean") return { error: "Invalid server or enabled flag" };
473
+ execute = () => q.toggleMcpServer(server, r.enabled);
474
+ break;
475
+ case "stop-task":
476
+ if (!task || !tasks.has(task)) return { error: "Unknown task in this query" };
477
+ execute = () => q.stopTask(task);
478
+ break;
479
+ case "background-tasks":
480
+ if (r.toolUseId !== void 0 && !text(r.toolUseId)) return { error: "Invalid tool use ID" };
481
+ execute = async () => ({ backgrounded: await q.backgroundTasks(r.toolUseId) });
482
+ break;
483
+ case "rewind-preview":
484
+ if (isThinking() || !target || !checkpoints.has(target)) return { error: "Rewind requires an idle query and a known user checkpoint" };
485
+ execute = async () => {
486
+ const result = await q.rewindFiles(target, { dryRun: true });
487
+ if (!result.canRewind) return result;
488
+ if (query !== q || generation !== runGeneration || activityRevision !== runRevision || isThinking() || !q.canControl()) throw Error("Query changed; preview again");
489
+ const token = node_crypto.randomUUID();
490
+ confirmations.set(token, { generation, revision: activityRevision, userMessageId: target, fingerprint: fingerprint(result), expires: Date.now() + 3e5 });
491
+ return { ...result, confirmationToken: token };
492
+ };
493
+ break;
494
+ case "rewind-apply": {
495
+ const confirmation = confirmations.get(String(r.confirmationToken));
496
+ if (isThinking() || !confirmation || confirmation.generation !== generation || confirmation.expires < Date.now()) return { error: "Preview expired or query is running; preview again" };
497
+ confirmations.delete(String(r.confirmationToken));
498
+ execute = async () => {
499
+ const fresh = await q.rewindFiles(confirmation.userMessageId, { dryRun: true });
500
+ if (query !== q || generation !== runGeneration || activityRevision !== confirmation.revision || isThinking() || !q.canControl() || fingerprint(fresh) !== confirmation.fingerprint) throw Error("Files or query changed; preview again");
501
+ return q.rewindFiles(confirmation.userMessageId, { dryRun: false });
502
+ };
503
+ break;
504
+ }
505
+ default:
506
+ return { error: "Unknown Claude control action" };
507
+ }
508
+ pending = true;
509
+ const op = { id: node_crypto.randomUUID(), queryGeneration: runGeneration, action, status: "running" };
510
+ operations.set(op.id, op);
511
+ if (operations.size > 50) for (const [id, old] of operations) {
512
+ if (old.status !== "running") {
513
+ operations.delete(id);
514
+ break;
515
+ }
516
+ }
517
+ void Promise.resolve().then(() => {
518
+ if (query !== q || generation !== runGeneration || !q.canControl()) throw Error("Claude query changed or is waiting for an interaction");
519
+ return execute();
520
+ }).then((result) => {
521
+ if (generation !== runGeneration || op.status !== "running") return;
522
+ op.status = "completed";
523
+ op.result = result ?? null;
524
+ }, (error) => {
525
+ if (generation !== runGeneration || op.status !== "running") return;
526
+ op.status = "failed";
527
+ op.error = error instanceof Error ? error.message : "Claude control failed";
528
+ }).finally(() => {
529
+ if (generation === runGeneration) pending = false;
530
+ });
531
+ return { operationId: op.id };
532
+ }
533
+ async function stopRunningTasks() {
534
+ const q = query, runGeneration = generation;
535
+ if (!q || !q.canControl()) return [];
536
+ const running = [...tasks.values()].filter((task) => task.status === "running" && text(task.id)).map((task) => String(task.id)).slice(0, 20);
537
+ const stopped = [];
538
+ await Promise.all(running.map(async (id) => {
539
+ try {
540
+ await q.stopTask(id);
541
+ if (query === q && generation === runGeneration) stopped.push(id);
542
+ } catch {
543
+ }
544
+ }));
545
+ return stopped;
546
+ }
547
+ return { setQuery, observe, request, stopRunningTasks };
548
+ }
549
+
550
+ const AUTH_FAILURE_RESULT_TEXT = /Failed to authenticate|OAuth session expired/i;
551
+ const QUERY_RECYCLE_NOTICE = {
552
+ authentication_failed: "\u26A0\uFE0F Claude Code could not refresh its OAuth session. This agent process was ended so your next message starts a fresh one that re-reads the credentials on this machine; if it fails again, run `claude` on the machine to log in again."
553
+ };
554
+ function queryRecycleReason(result, lastAssistantError) {
555
+ if (lastAssistantError === "authentication_failed") {
556
+ return "authentication_failed";
557
+ }
558
+ if (result.is_error && typeof result.result === "string" && AUTH_FAILURE_RESULT_TEXT.test(result.result)) {
559
+ return "authentication_failed";
560
+ }
561
+ return null;
562
+ }
563
+
564
+ class PushableAsyncIterable {
565
+ queue = [];
566
+ waiters = [];
567
+ isDone = false;
568
+ error = null;
569
+ started = false;
570
+ constructor() {
571
+ }
572
+ /**
573
+ * Push a value to the iterable
574
+ */
575
+ push(value) {
576
+ if (this.isDone) {
577
+ throw new Error("Cannot push to completed iterable");
578
+ }
579
+ if (this.error) {
580
+ throw this.error;
581
+ }
582
+ const waiter = this.waiters.shift();
583
+ if (waiter) {
584
+ waiter.resolve({ done: false, value });
585
+ } else {
586
+ this.queue.push(value);
587
+ }
588
+ }
589
+ /**
590
+ * Mark the iterable as complete
591
+ */
592
+ end() {
593
+ if (this.isDone) {
594
+ return;
595
+ }
596
+ this.isDone = true;
597
+ this.cleanup();
598
+ }
599
+ /**
600
+ * Set an error on the iterable
601
+ */
602
+ setError(err) {
603
+ if (this.isDone) {
604
+ return;
605
+ }
606
+ this.error = err;
607
+ this.isDone = true;
608
+ this.cleanup();
609
+ }
610
+ /**
611
+ * Cleanup waiting consumers
612
+ */
613
+ cleanup() {
614
+ while (this.waiters.length > 0) {
615
+ const waiter = this.waiters.shift();
616
+ if (this.error) {
617
+ waiter.reject(this.error);
618
+ } else {
619
+ waiter.resolve({ done: true, value: void 0 });
620
+ }
621
+ }
622
+ }
623
+ /**
624
+ * AsyncIterableIterator implementation
625
+ */
626
+ async next() {
627
+ if (this.queue.length > 0) {
628
+ return { done: false, value: this.queue.shift() };
629
+ }
630
+ if (this.isDone) {
631
+ if (this.error) {
632
+ throw this.error;
633
+ }
634
+ return { done: true, value: void 0 };
635
+ }
636
+ return new Promise((resolve, reject) => {
637
+ this.waiters.push({ resolve, reject });
638
+ });
639
+ }
640
+ /**
641
+ * AsyncIterableIterator return implementation
642
+ */
643
+ async return(_value) {
644
+ this.end();
645
+ return { done: true, value: void 0 };
646
+ }
647
+ /**
648
+ * AsyncIterableIterator throw implementation
649
+ */
650
+ async throw(e) {
651
+ this.setError(e instanceof Error ? e : new Error(String(e)));
652
+ throw this.error;
653
+ }
654
+ /**
655
+ * Make this iterable
656
+ */
657
+ [Symbol.asyncIterator]() {
658
+ if (this.started) {
659
+ throw new Error("PushableAsyncIterable can only be iterated once");
660
+ }
661
+ this.started = true;
662
+ return this;
663
+ }
664
+ /**
665
+ * Check if the iterable is done
666
+ */
667
+ get done() {
668
+ return this.isDone;
669
+ }
670
+ /**
671
+ * Check if the iterable has an error
672
+ */
673
+ get hasError() {
674
+ return this.error !== null;
675
+ }
676
+ /**
677
+ * Get the current queue size
678
+ */
679
+ get queueSize() {
680
+ return this.queue.length;
681
+ }
682
+ /**
683
+ * Get the number of waiting consumers
684
+ */
685
+ get waiterCount() {
686
+ return this.waiters.length;
687
+ }
688
+ }
689
+
690
+ async function awaitFileExist(file, timeout = 1e4) {
691
+ const startTime = Date.now();
692
+ while (Date.now() - startTime < timeout) {
693
+ try {
694
+ await fs.access(file);
695
+ return true;
696
+ } catch (e) {
697
+ await time.delay(1e3);
698
+ }
699
+ }
700
+ return false;
701
+ }
702
+
703
+ function modelTarget(model) {
704
+ return model == null || model === "" ? void 0 : model;
705
+ }
706
+ function needsModelSwitch(current, next) {
707
+ return modelTarget(current) !== modelTarget(next);
708
+ }
709
+ function modelSwitchFailureNotice(model, error) {
710
+ const name = modelTarget(model) ?? "default";
711
+ const detail = error instanceof Error ? error.message : String(error);
712
+ return `\u26A0\uFE0F Could not switch to model "${name}" \u2014 continuing with the previous model. ${detail}`;
713
+ }
714
+
715
+ const SDK_TIMING_FIELDS = ["ttft_ms", "time_to_request_ms", "time_to_request_from_spawn_ms"];
716
+ function delta(from, to) {
717
+ if (typeof from !== "number" || typeof to !== "number" || to < from) return "-";
718
+ return `${Math.round(to - from)}ms`;
719
+ }
720
+ function sdkField(result, key) {
721
+ if (!result || typeof result !== "object") return "-";
722
+ const value = result[key];
723
+ return typeof value === "number" && Number.isFinite(value) ? `${Math.round(value)}` : "-";
724
+ }
725
+ function formatClaudeTimingLine(input) {
726
+ const m = input.marks;
727
+ const sdk = SDK_TIMING_FIELDS.map((key) => `${key}=${sdkField(input.result, key)}`).join(",");
728
+ return [
729
+ "[CLAUDE TIMING]",
730
+ `turn=${input.turn}`,
731
+ `firstTurnOfProcess=${input.firstTurnOfProcess}`,
732
+ `pushed\u2192spawn=${delta(m.pushedAt, m.spawnAt)}`,
733
+ `spawn\u2192handshake=${delta(m.spawnAt, m.handshakeAt)}`,
734
+ `handshake\u2192init=${delta(m.handshakeAt, m.initAt)}`,
735
+ `init\u2192firstAssistant=${delta(m.initAt, m.firstAssistantAt)}`,
736
+ `pushed\u2192firstAssistant=${delta(m.pushedAt, m.firstAssistantAt)}`,
737
+ `sdk{${sdk}}`,
738
+ ...input.prewarm ? [`prewarm=${input.prewarm}`] : []
739
+ ].join(" ");
740
+ }
741
+
742
+ function claudeModeHash(mode) {
743
+ return deterministicJson.hashObject({
744
+ isPlan: mode.permissionMode === "plan",
745
+ fallbackModel: mode.fallbackModel,
746
+ customSystemPrompt: mode.customSystemPrompt,
747
+ appendSystemPrompt: mode.appendSystemPrompt,
748
+ allowedTools: mode.allowedTools,
749
+ disallowedTools: mode.disallowedTools,
750
+ effort: mode.effort
751
+ });
752
+ }
753
+
754
+ const CLAUDE_PREWARM_IDLE_MS = 15 * 60 * 1e3;
755
+ const CLAUDE_PREWARM_MAX_CONCURRENT = 2;
756
+ const CLAUDE_PREWARM_ADOPT_TIMEOUT_MS = 5e3;
757
+ const PREWARM_HOOK_SOURCE_PREFIX = "prewarm-";
758
+ const BAD_AUTH_STATUSES = /* @__PURE__ */ new Set(["not-logged-in", "error", "claude-missing"]);
759
+ const RESUME_ARGS = /* @__PURE__ */ new Set(["--resume", "-r", "--continue", "-c", "--fork-session", "--session-id"]);
760
+ const CLAUDE_PREWARM_MAX_CONSECUTIVE_MISSES = 5;
761
+ function claudePrewarmSessionEligibility(input) {
762
+ const { env } = input;
763
+ const no = (reason) => ({ eligible: false, reason });
764
+ if (env.HAPPY_CLAUDE_PREWARM === "0") return no("env-off");
765
+ if (input.setting === "off") return no("setting-off");
766
+ if (input.startedBy !== "daemon") return no("not-daemon");
767
+ if (input.startingMode !== "remote") return no("not-remote");
768
+ if (env.HAPPY_RECONNECT_SESSION_ID) return no("reconnect");
769
+ if (env.HAPPY_FORK_CLAUDE_SESSION_ID || env.HAPPY_FORKED_FROM_SESSION_ID) return no("fork");
770
+ if (env.HAPPY_IMPORTED_FROM_CLAUDE_SESSION_ID) return no("import");
771
+ if (env.HAPPY_SESSION_VARIANT === "assistant") return no("assistant");
772
+ if (env.VH_TEAM_OPERATION_ID) return no("teams");
773
+ if (env.VH_AUTOMATION_RUN_ID) return no("automation");
774
+ if (env.HAPPY_SPAWNED_BY) return no("spawned-by");
775
+ if (env.HAPPY_FIRST_MESSAGE_FROM_CLI === "1") return no("cli-first-message");
776
+ if (input.claudeArgs?.some((arg) => RESUME_ARGS.has(arg))) return no("resume-args");
777
+ if (env.HAPPY_CLAUDE_AUTH_STATUS && BAD_AUTH_STATUSES.has(env.HAPPY_CLAUDE_AUTH_STATUS)) return no("claude-auth");
778
+ return { eligible: true };
779
+ }
780
+ function claudePrewarmEligibility(input) {
781
+ const session = claudePrewarmSessionEligibility(input);
782
+ if (!session.eligible) return session;
783
+ if (!input.cache?.appendSystemPrompt) return { eligible: false, reason: "no-cached-system-prompt" };
784
+ if (input.cache.consecutiveMisses >= CLAUDE_PREWARM_MAX_CONSECUTIVE_MISSES) return { eligible: false, reason: "recent-misses" };
785
+ return { eligible: true };
786
+ }
787
+ function prewarmCacheFile(happyHomeDir) {
788
+ return path$1.join(happyHomeDir, "claude-prewarm.json");
789
+ }
790
+ function readPrewarmCache(file) {
791
+ try {
792
+ const raw = JSON.parse(node_fs.readFileSync(file, "utf8"));
793
+ if (typeof raw?.appendSystemPrompt !== "string" || raw.appendSystemPrompt.length === 0) return null;
794
+ const misses = typeof raw.consecutiveMisses === "number" && Number.isFinite(raw.consecutiveMisses) && raw.consecutiveMisses > 0 ? Math.floor(raw.consecutiveMisses) : 0;
795
+ return {
796
+ appendSystemPrompt: raw.appendSystemPrompt,
797
+ ...typeof raw.effort === "string" || raw.effort === null ? { effort: raw.effort } : {},
798
+ consecutiveMisses: misses
799
+ };
800
+ } catch {
801
+ return null;
802
+ }
803
+ }
804
+ function writePrewarmCache(file, cache) {
805
+ try {
806
+ const tmp = `${file}.${process.pid}.tmp`;
807
+ node_fs.writeFileSync(tmp, JSON.stringify({ ...cache, updatedAt: Date.now() }), { mode: 384 });
808
+ node_fs.renameSync(tmp, file);
809
+ return true;
810
+ } catch {
811
+ return false;
812
+ }
813
+ }
814
+ function prewarmPredictionMeta(cache) {
815
+ return {
816
+ appendSystemPrompt: cache.appendSystemPrompt,
817
+ ...cache.effort !== void 0 ? { effort: cache.effort } : {}
818
+ };
819
+ }
820
+ function evaluatePrewarmPrediction(input) {
821
+ const outcome = input.predicted === null ? "no-prediction" : claudeModeHash(input.predicted) === claudeModeHash(input.actual) ? "hit" : "miss";
822
+ const append = input.meta?.appendSystemPrompt;
823
+ const misses = outcome === "miss" ? (input.previous?.consecutiveMisses ?? 0) + 1 : 0;
824
+ if (typeof append !== "string" || append.length === 0) {
825
+ return { outcome, next: input.previous ? { ...input.previous, consecutiveMisses: misses } : null };
826
+ }
827
+ const hasEffort = !!input.meta && Object.prototype.hasOwnProperty.call(input.meta, "effort");
828
+ return {
829
+ outcome,
830
+ next: {
831
+ appendSystemPrompt: append,
832
+ ...hasEffort ? { effort: input.meta.effort ?? null } : {},
833
+ consecutiveMisses: misses
834
+ }
835
+ };
836
+ }
837
+ function prewarmSlotDir(happyHomeDir) {
838
+ return path$1.join(happyHomeDir, "tmp", "claude-prewarm");
839
+ }
840
+ function acquirePrewarmSlot(dir, opts = {}) {
841
+ const max = opts.max ?? CLAUDE_PREWARM_MAX_CONCURRENT;
842
+ const pid = opts.pid ?? process.pid;
843
+ const isAlive = opts.isAlive ?? pidAlive;
844
+ const now = opts.now ?? Date.now();
845
+ const staleMs = opts.staleMs ?? CLAUDE_PREWARM_IDLE_MS + 6e4;
846
+ try {
847
+ node_fs.mkdirSync(dir, { recursive: true, mode: 448 });
848
+ } catch {
849
+ return null;
850
+ }
851
+ for (let i = 0; i < max; i++) {
852
+ const path = path$1.join(dir, `slot-${i}.lock`);
853
+ for (let attempt = 0; attempt < 2; attempt++) {
854
+ try {
855
+ node_fs.writeFileSync(path, String(pid), { flag: "wx", mode: 384 });
856
+ let released = false;
857
+ return {
858
+ path,
859
+ release: () => {
860
+ if (released) return;
861
+ released = true;
862
+ try {
863
+ if (node_fs.readFileSync(path, "utf8").trim() === String(pid)) node_fs.unlinkSync(path);
864
+ } catch {
865
+ }
866
+ }
867
+ };
868
+ } catch (error) {
869
+ if (error?.code !== "EEXIST") return null;
870
+ if (!slotIsStale(path, { isAlive, now, staleMs }) || attempt > 0) break;
871
+ try {
872
+ node_fs.unlinkSync(path);
873
+ } catch {
874
+ }
875
+ }
876
+ }
877
+ }
878
+ return null;
879
+ }
880
+ function slotIsStale(path, opts) {
881
+ try {
882
+ const owner = Number(node_fs.readFileSync(path, "utf8").trim());
883
+ if (!Number.isInteger(owner) || owner <= 0 || !opts.isAlive(owner)) return true;
884
+ return opts.now - node_fs.statSync(path).mtimeMs > opts.staleMs;
885
+ } catch {
886
+ return !node_fs.existsSync(path);
887
+ }
888
+ }
889
+ function pidAlive(pid) {
890
+ try {
891
+ process.kill(pid, 0);
892
+ return true;
893
+ } catch (error) {
894
+ return error?.code === "EPERM";
895
+ }
896
+ }
897
+ class PrewarmHookGate {
898
+ next = 0;
899
+ adopted = /* @__PURE__ */ new Set();
900
+ newTag() {
901
+ return `${PREWARM_HOOK_SOURCE_PREFIX}${process.pid}-${this.next++}`;
902
+ }
903
+ adopt(tag) {
904
+ this.adopted.add(tag);
905
+ }
906
+ /** Untagged hooks (the ordinary per-process settings file) always pass. */
907
+ accepts(source) {
908
+ if (!source || !source.startsWith(PREWARM_HOOK_SOURCE_PREFIX)) return true;
909
+ return this.adopted.has(source);
910
+ }
911
+ }
912
+ function decideWarmAdoption(input) {
913
+ if (input.specialCommand === "clear") return { adopt: false, reason: "clear" };
914
+ if (input.specialCommand === "compact") return { adopt: false, reason: "compact" };
915
+ if (claudeModeHash(input.predicted) !== claudeModeHash(input.actual)) return { adopt: false, reason: "mode-mismatch" };
916
+ const warmPermission = permissionMode.mapToClaudeMode(input.predicted.permissionMode);
917
+ const wantedPermission = permissionMode.mapToClaudeMode(input.actual.permissionMode);
918
+ return {
919
+ adopt: true,
920
+ ...warmPermission !== wantedPermission ? { setPermissionMode: wantedPermission } : {},
921
+ switchModel: needsModelSwitch(input.predicted.model, input.actual.model)
922
+ };
923
+ }
924
+ function formatPrewarmLine(event, detail) {
925
+ if (event === "discarded" || event === "skipped") {
926
+ return `[CLAUDE PREWARM] ${event}(${typeof detail === "string" ? detail : "unknown"})`;
927
+ }
928
+ const fields = detail && typeof detail === "object" ? Object.entries(detail).filter(([, v]) => v !== void 0).map(([k, v]) => ` ${k}=${v}`).join("") : "";
929
+ return `[CLAUDE PREWARM] ${event}${fields}`;
930
+ }
931
+ const liveLeases = /* @__PURE__ */ new Set();
932
+ let exitHookInstalled = false;
933
+ function disposeAllClaudePrewarms(reason) {
934
+ for (const lease of [...liveLeases]) lease.discard(reason);
935
+ }
936
+ function createClaudePrewarmLease(input) {
937
+ let state = "warm";
938
+ let teardown = null;
939
+ const lease = {
940
+ mode: input.mode,
941
+ hookSettingsPath: input.hookSettingsPath,
942
+ tag: input.tag,
943
+ idleMs: input.idleMs ?? CLAUDE_PREWARM_IDLE_MS,
944
+ adoptTimeoutMs: input.adoptTimeoutMs ?? CLAUDE_PREWARM_ADOPT_TIMEOUT_MS,
945
+ adopt: () => {
946
+ if (state !== "warm") return;
947
+ state = "adopted";
948
+ teardown = null;
949
+ input.gate.adopt(input.tag);
950
+ input.slot.release();
951
+ liveLeases.delete(lease);
952
+ },
953
+ discard: (reason) => {
954
+ if (state !== "warm") return;
955
+ state = "closed";
956
+ const t = teardown;
957
+ teardown = null;
958
+ liveLeases.delete(lease);
959
+ try {
960
+ t?.(reason);
961
+ } catch {
962
+ }
963
+ input.slot.release();
964
+ input.cleanupHookSettings();
965
+ input.log(formatPrewarmLine("discarded", reason));
966
+ },
967
+ setTeardown: (next) => {
968
+ if (state === "warm") teardown = next;
969
+ }
970
+ };
971
+ liveLeases.add(lease);
972
+ if (!exitHookInstalled) {
973
+ exitHookInstalled = true;
974
+ process.once("exit", () => disposeAllClaudePrewarms("exit"));
975
+ }
976
+ return lease;
977
+ }
978
+
979
+ function withTimeout(promise, ms) {
980
+ promise.catch(() => {
981
+ });
982
+ let timer;
983
+ const timeout = new Promise((_, reject) => {
984
+ timer = setTimeout(() => reject(new Error(`timed out after ${ms}ms`)), ms);
985
+ });
986
+ return Promise.race([promise, timeout]).finally(() => {
987
+ if (timer) clearTimeout(timer);
988
+ });
989
+ }
990
+ async function claudeRemote(opts) {
991
+ let startFrom = opts.sessionId;
992
+ if (opts.sessionId && !systemPrompt.claudeCheckSession(opts.sessionId, opts.path)) {
993
+ startFrom = null;
994
+ }
995
+ if (!startFrom && opts.claudeArgs) {
996
+ for (let i = 0; i < opts.claudeArgs.length; i++) {
997
+ if (opts.claudeArgs[i] === "--resume") {
998
+ if (i + 1 < opts.claudeArgs.length) {
999
+ const nextArg = opts.claudeArgs[i + 1];
1000
+ if (!nextArg.startsWith("-") && nextArg.includes("-")) {
1001
+ startFrom = nextArg;
1002
+ logger.logger.debug(`[claudeRemote] Found --resume with session ID: ${startFrom}`);
1003
+ break;
1004
+ } else {
1005
+ logger.logger.debug("[claudeRemote] Found --resume without session ID - not supported in remote mode");
1006
+ break;
1007
+ }
1008
+ } else {
1009
+ logger.logger.debug("[claudeRemote] Found --resume without session ID - not supported in remote mode");
1010
+ break;
1011
+ }
1012
+ }
1013
+ }
1014
+ }
1015
+ if (opts.claudeEnvVars) {
1016
+ Object.entries(opts.claudeEnvVars).forEach(([key, value]) => {
1017
+ process.env[key] = value;
1018
+ });
1019
+ }
1020
+ let thinking = false;
1021
+ const updateThinking = (newThinking) => {
1022
+ if (thinking !== newThinking) {
1023
+ thinking = newThinking;
1024
+ logger.logger.debug(`[claudeRemote] Thinking state changed to: ${thinking}`);
1025
+ if (opts.onThinkingChange) {
1026
+ opts.onThinkingChange(thinking);
1027
+ }
1028
+ }
1029
+ };
1030
+ let callbackDepth = 0;
1031
+ const guardCallback = (callback) => (async (...args) => {
1032
+ callbackDepth++;
1033
+ try {
1034
+ return await callback(...args);
1035
+ } finally {
1036
+ setImmediate(() => {
1037
+ callbackDepth--;
1038
+ });
1039
+ }
1040
+ });
1041
+ let mode = opts.prewarm?.mode ?? { permissionMode: "default" };
1042
+ const buildSdkOptions = (creationMode, io) => {
1043
+ const sdkOptions = {
1044
+ cwd: opts.path,
1045
+ resume: startFrom ?? void 0,
1046
+ mcpServers: opts.mcpServers,
1047
+ permissionMode: permissionMode.mapToClaudeMode(creationMode.permissionMode),
1048
+ // This is only the SDK safety opt-in; permissionMode/canUseTool still
1049
+ // enforce the selected policy. It must be enabled at Query creation so
1050
+ // a later explicit live switch to bypassPermissions can succeed.
1051
+ allowDangerouslySkipPermissions: true,
1052
+ model: creationMode.model,
1053
+ fallbackModel: creationMode.fallbackModel,
1054
+ customSystemPrompt: creationMode.customSystemPrompt ? creationMode.customSystemPrompt + "\n\n" + systemPrompt.systemPrompt : void 0,
1055
+ appendSystemPrompt: creationMode.appendSystemPrompt ? creationMode.appendSystemPrompt + "\n\n" + systemPrompt.systemPrompt : systemPrompt.systemPrompt,
1056
+ allowedTools: creationMode.allowedTools ? creationMode.allowedTools.concat(opts.allowedTools) : opts.allowedTools,
1057
+ additionalDirectories: opts.additionalDirectories,
1058
+ disallowedTools: creationMode.disallowedTools,
1059
+ effort: creationMode.effort,
1060
+ canCallTool: (toolName, input, options) => opts.canCallTool(toolName, input, mode, options),
1061
+ onElicitation: opts.onElicitation,
1062
+ onUserDialog: opts.onUserDialog,
1063
+ supportedDialogKinds: opts.onUserDialog ? ["refusal_fallback_prompt"] : void 0,
1064
+ abort: io.abort,
1065
+ settingsPath: io.settingsPath,
1066
+ // B-309: token-level partials. Without this the SDK emits nothing
1067
+ // between "turn started" and "entire assistant message", which is
1068
+ // exactly why the web used to show a spinner where the terminal shows
1069
+ // the model thinking out loud. The partials do NOT enter the
1070
+ // transcript; they are split off below and relayed on a side channel.
1071
+ // Only pay for them when someone is listening.
1072
+ includePartialMessages: opts.onStreamFrame ? true : void 0
1073
+ };
1074
+ if (sdkOptions.canCallTool) sdkOptions.canCallTool = guardCallback(sdkOptions.canCallTool);
1075
+ if (sdkOptions.onElicitation) sdkOptions.onElicitation = guardCallback(sdkOptions.onElicitation);
1076
+ if (sdkOptions.onUserDialog) sdkOptions.onUserDialog = guardCallback(sdkOptions.onUserDialog);
1077
+ return sdkOptions;
1078
+ };
1079
+ const discoverModels = async (q) => {
1080
+ if (typeof q.supportedModels !== "function") return void 0;
1081
+ return (await q.supportedModels()).map((model) => ({
1082
+ code: model.value,
1083
+ value: model.displayName,
1084
+ description: model.description,
1085
+ resolvedModel: model.resolvedModel,
1086
+ ...model.supportsEffort === false ? { reasoningEfforts: [] } : model.supportedEffortLevels ? { reasoningEfforts: model.supportedEffortLevels } : {}
1087
+ }));
1088
+ };
1089
+ let warm = null;
1090
+ if (opts.prewarm && startFrom) {
1091
+ opts.prewarm.discard("resume");
1092
+ } else if (opts.prewarm && opts.signal?.aborted) {
1093
+ opts.prewarm.discard("aborted");
1094
+ } else if (opts.prewarm) {
1095
+ const lease = opts.prewarm;
1096
+ const abort = new AbortController();
1097
+ const onOuterAbort = () => abort.abort();
1098
+ opts.signal?.addEventListener("abort", onOuterAbort, { once: true });
1099
+ const messages2 = new PushableAsyncIterable();
1100
+ const startedAt = Date.now();
1101
+ try {
1102
+ const response2 = index.query({
1103
+ prompt: messages2,
1104
+ options: buildSdkOptions(lease.mode, { settingsPath: lease.hookSettingsPath, abort: abort.signal })
1105
+ });
1106
+ const handshake = discoverModels(response2).then(
1107
+ (models2) => ({ ok: true, models: models2, at: Date.now() }),
1108
+ () => ({ ok: false, at: Date.now() })
1109
+ );
1110
+ const w = { response: response2, messages: messages2, lease, startedAt, handshake };
1111
+ lease.setTeardown(() => {
1112
+ if (w.idleTimer) clearTimeout(w.idleTimer);
1113
+ try {
1114
+ w.offExit?.();
1115
+ } catch {
1116
+ }
1117
+ opts.signal?.removeEventListener("abort", onOuterAbort);
1118
+ if (warm === w) warm = null;
1119
+ messages2.end();
1120
+ abort.abort();
1121
+ });
1122
+ w.idleTimer = setTimeout(() => lease.discard("idle"), lease.idleMs);
1123
+ w.idleTimer.unref?.();
1124
+ void handshake.then((result) => {
1125
+ if (!result.ok) {
1126
+ lease.discard("handshake-failed");
1127
+ return;
1128
+ }
1129
+ if (warm !== w) return;
1130
+ try {
1131
+ const transport = response2.transport;
1132
+ const off = transport?.onExit?.(() => {
1133
+ if (warm === w) lease.discard("exited");
1134
+ });
1135
+ if (typeof off === "function") w.offExit = off;
1136
+ } catch {
1137
+ }
1138
+ });
1139
+ warm = w;
1140
+ logger.logger.debug(formatPrewarmLine("started", { tag: lease.tag }));
1141
+ } catch (error) {
1142
+ opts.signal?.removeEventListener("abort", onOuterAbort);
1143
+ logger.logger.debug(`[claudeRemote] prewarm query() threw: ${error instanceof Error ? error.message : String(error)}`);
1144
+ lease.discard("spawn-failed");
1145
+ }
1146
+ }
1147
+ const discardWarm = (reason) => {
1148
+ warm?.lease.discard(reason);
1149
+ };
1150
+ let initial;
1151
+ try {
1152
+ initial = await opts.nextMessage();
1153
+ } catch (error) {
1154
+ discardWarm("error");
1155
+ throw error;
1156
+ }
1157
+ if (!initial) {
1158
+ discardWarm("exit");
1159
+ return;
1160
+ }
1161
+ let timingTurn = 1;
1162
+ let timingMarks = { pushedAt: initial.enqueuedAt ?? Date.now() };
1163
+ const initialText = typeof initial.message === "string" ? initial.message : initial.message.find((b) => b.type === "text")?.text ?? "";
1164
+ const specialCommand = specialCommands.parseSpecialCommand(initialText);
1165
+ if (specialCommand.type === "clear") {
1166
+ discardWarm("clear");
1167
+ if (opts.onCompletionEvent) {
1168
+ opts.onCompletionEvent("Context was reset");
1169
+ }
1170
+ if (opts.onSessionReset) {
1171
+ opts.onSessionReset();
1172
+ }
1173
+ opts.onReady();
1174
+ return;
1175
+ }
1176
+ let isCompactCommand = false;
1177
+ if (specialCommand.type === "compact") {
1178
+ logger.logger.debug("[claudeRemote] /compact command detected - will process as normal but with compaction behavior");
1179
+ isCompactCommand = true;
1180
+ if (opts.onCompletionEvent) {
1181
+ opts.onCompletionEvent("Compaction started");
1182
+ }
1183
+ }
1184
+ let adopted = null;
1185
+ if (warm) {
1186
+ const w = warm;
1187
+ const adoptStart = Date.now();
1188
+ const decision = decideWarmAdoption({ predicted: w.lease.mode, actual: initial.mode, specialCommand: specialCommand.type });
1189
+ let reason = decision.adopt ? null : decision.reason;
1190
+ const hs = reason ? null : await w.handshake;
1191
+ if (!reason) {
1192
+ if (warm !== w) {
1193
+ reason = "closed";
1194
+ } else if (!hs?.ok) {
1195
+ reason = "handshake-failed";
1196
+ } else if (decision.adopt) {
1197
+ try {
1198
+ if (decision.setPermissionMode) {
1199
+ await withTimeout(w.response.setPermissionMode(decision.setPermissionMode), w.lease.adoptTimeoutMs);
1200
+ } else {
1201
+ await withTimeout(w.response.mcpServerStatus(), w.lease.adoptTimeoutMs);
1202
+ }
1203
+ } catch {
1204
+ reason = decision.setPermissionMode ? "set-permission-failed" : "not-alive";
1205
+ }
1206
+ if (!reason && decision.switchModel) {
1207
+ try {
1208
+ await withTimeout(w.response.setModel(modelTarget(initial.mode.model)), w.lease.adoptTimeoutMs);
1209
+ } catch {
1210
+ reason = "set-model-failed";
1211
+ }
1212
+ }
1213
+ if (!reason && warm !== w) reason = "closed";
1214
+ }
1215
+ }
1216
+ if (reason || !hs?.ok) {
1217
+ w.lease.discard(reason ?? "handshake-failed");
1218
+ warm = null;
1219
+ } else {
1220
+ if (w.idleTimer) clearTimeout(w.idleTimer);
1221
+ try {
1222
+ w.offExit?.();
1223
+ } catch {
1224
+ }
1225
+ warm = null;
1226
+ w.lease.adopt();
1227
+ adopted = { response: w.response, messages: w.messages, models: hs.models };
1228
+ logger.logger.debug(formatPrewarmLine("adopted", {
1229
+ warmForMs: adoptStart - w.startedAt,
1230
+ handshakeWaitMs: Math.max(0, hs.at - adoptStart),
1231
+ adoptMs: Date.now() - adoptStart,
1232
+ setPermissionMode: decision.adopt ? decision.setPermissionMode : void 0,
1233
+ setModel: decision.adopt && decision.switchModel ? true : void 0
1234
+ }));
1235
+ }
1236
+ }
1237
+ mode = initial.mode;
1238
+ const initialUserMessage = {
1239
+ type: "user",
1240
+ parent_tool_use_id: null,
1241
+ origin: { kind: "human" },
1242
+ message: {
1243
+ role: "user",
1244
+ content: initial.message
1245
+ }
1246
+ };
1247
+ let messages;
1248
+ let response;
1249
+ let models;
1250
+ if (adopted) {
1251
+ messages = adopted.messages;
1252
+ response = adopted.response;
1253
+ models = adopted.models;
1254
+ timingMarks.handshakeAt = Date.now();
1255
+ messages.push(initialUserMessage);
1256
+ } else {
1257
+ messages = new PushableAsyncIterable();
1258
+ messages.push(initialUserMessage);
1259
+ timingMarks.spawnAt = Date.now();
1260
+ response = index.query({
1261
+ prompt: messages,
1262
+ options: buildSdkOptions(initial.mode, { settingsPath: opts.hookSettingsPath, abort: opts.signal })
1263
+ });
1264
+ try {
1265
+ models = await discoverModels(response);
1266
+ } catch (error) {
1267
+ logger.logger.debug("[claudeRemote] Model capability discovery unavailable");
1268
+ }
1269
+ timingMarks.handshakeAt = Date.now();
1270
+ }
1271
+ const prewarmTiming = adopted ? "adopted" : void 0;
1272
+ if (opts.onQueryReady) {
1273
+ opts.onQueryReady({
1274
+ canControl: () => callbackDepth === 0,
1275
+ reloadSkills: () => response.reloadSkills(),
1276
+ reloadPlugins: () => response.reloadPlugins(),
1277
+ mcpServerStatus: () => response.mcpServerStatus(),
1278
+ reconnectMcpServer: (name) => response.reconnectMcpServer(name),
1279
+ toggleMcpServer: (name, enabled) => response.toggleMcpServer(name, enabled),
1280
+ stopTask: (id) => response.stopTask(id),
1281
+ backgroundTasks: (id) => response.backgroundTasks(id),
1282
+ rewindFiles: (id, options) => response.rewindFiles(id, options),
1283
+ setPermissionMode: (mode2) => response.setPermissionMode(mode2),
1284
+ setModel: (model) => response.setModel(modelTarget(model)),
1285
+ interrupt: async () => {
1286
+ await response.interrupt();
1287
+ },
1288
+ // `request` is the SDK's generic control-request sender (every typed
1289
+ // method wraps it); the public types omit `side_question`, the CLI
1290
+ // handles it. An abort on `signal` becomes control_cancel_request.
1291
+ sideQuestion: async (request, signal) => {
1292
+ const raw = await response.request({ subtype: "side_question", ...request }, { signal });
1293
+ const body = raw?.response;
1294
+ return body && typeof body === "object" ? body : { response: null };
1295
+ },
1296
+ steer: (message, nextMode) => {
1297
+ mode = { ...nextMode, model: mode.model };
1298
+ messages.push({
1299
+ type: "user",
1300
+ parent_tool_use_id: null,
1301
+ priority: "now",
1302
+ origin: { kind: "human" },
1303
+ message: { role: "user", content: message }
1304
+ });
1305
+ }
1306
+ });
1307
+ }
1308
+ let lastAssistantError;
1309
+ updateThinking(true);
1310
+ try {
1311
+ logger.logger.debug(`[claudeRemote] Starting to iterate over response`);
1312
+ for await (const message of response) {
1313
+ if (message.type === "stream_event") {
1314
+ if (opts.onStreamFrame && !message.parent_tool_use_id) {
1315
+ opts.onStreamFrame({ type: "stream_event", event: message.event });
1316
+ }
1317
+ continue;
1318
+ }
1319
+ logger.logger.debug(`[claudeRemote] Message ${message.type}`, contentLogMetadata.contentLogMetadata(message));
1320
+ if (message.type === "assistant" && message.error) {
1321
+ lastAssistantError = message.error;
1322
+ }
1323
+ if (message.type === "assistant" && timingMarks.firstAssistantAt === void 0) {
1324
+ timingMarks.firstAssistantAt = Date.now();
1325
+ } else if (message.type === "system" && message.subtype === "init" && timingMarks.initAt === void 0) {
1326
+ timingMarks.initAt = Date.now();
1327
+ }
1328
+ if (opts.onStreamFrame && message.type === "system") {
1329
+ const subtype = message.subtype;
1330
+ if (subtype === "thinking_tokens") {
1331
+ const tokens = message;
1332
+ opts.onStreamFrame({ type: "system", subtype: "thinking_tokens", estimated_tokens: tokens.estimated_tokens });
1333
+ } else if (subtype === "status") {
1334
+ const status = message;
1335
+ opts.onStreamFrame({ type: "system", subtype: "status", status: status.status });
1336
+ }
1337
+ }
1338
+ const outboundMessage = isCompactCommand && message.type === "assistant" ? { ...message, isCompactSummary: true } : message;
1339
+ opts.onMessage(outboundMessage);
1340
+ if (message.type === "system" && message.subtype === "init") {
1341
+ updateThinking(true);
1342
+ const systemInit = message;
1343
+ if (opts.onSDKMetadata) {
1344
+ opts.onSDKMetadata({
1345
+ models,
1346
+ tools: systemInit.tools,
1347
+ slashCommands: systemInit.slash_commands,
1348
+ mcpServers: systemInit.mcp_servers?.map((s) => ({ name: s.name, status: s.status })),
1349
+ skills: systemInit.skills,
1350
+ model: systemInit.model,
1351
+ // `mode`, not `initial.mode`: the model can move mid-Query
1352
+ // via setModel, and init is re-emitted every turn with the
1353
+ // model actually in force.
1354
+ modelIsDefault: modelTarget(mode.model) === void 0,
1355
+ // The SDK's own verdict on the mode it will enforce —
1356
+ // settings (permissions.deny/ask/defaultMode/
1357
+ // disableBypassPermissionsMode) can override what we
1358
+ // asked for. runClaude publishes THIS, not our intent.
1359
+ permissionMode: systemInit.permissionMode
1360
+ });
1361
+ }
1362
+ if (systemInit.session_id) {
1363
+ logger.logger.debug(`[claudeRemote] Waiting for session file to be written to disk: ${systemInit.session_id}`);
1364
+ const projectDir = path.getProjectPath(opts.path);
1365
+ const found = await awaitFileExist(path$1.join(projectDir, `${systemInit.session_id}.jsonl`), 3e4);
1366
+ logger.logger.debug(`[claudeRemote] Session file found: ${systemInit.session_id} ${found}`);
1367
+ if (!found) {
1368
+ logger.logger.debug(`[claudeRemote] WARNING: session transcript ${systemInit.session_id} never appeared after 30s`);
1369
+ opts.onCompletionEvent?.("\u26A0\uFE0F Claude session did not produce a transcript \u2014 the agent may be unresponsive. Try sending your message again.");
1370
+ }
1371
+ opts.onSessionFound(systemInit.session_id);
1372
+ }
1373
+ }
1374
+ if (message.type === "result") {
1375
+ updateThinking(false);
1376
+ logger.logger.debug("[claudeRemote] Result received");
1377
+ logger.logger.debug(formatClaudeTimingLine({
1378
+ turn: timingTurn,
1379
+ firstTurnOfProcess: timingTurn === 1,
1380
+ marks: timingMarks,
1381
+ result: message,
1382
+ prewarm: timingTurn === 1 ? prewarmTiming : void 0
1383
+ }));
1384
+ timingTurn++;
1385
+ timingMarks = {};
1386
+ const denials = message.permission_denials;
1387
+ if (denials && denials.length > 0) {
1388
+ logger.logger.warn(`[claudeRemote] ${denials.length} tool call(s) auto-denied by Claude Code policy: ${denials.map((d) => d.tool_name ?? "?").join(", ")}`);
1389
+ }
1390
+ if (isCompactCommand) {
1391
+ const compactSucceeded = message.subtype === "success";
1392
+ logger.logger.debug(`[claudeRemote] Compaction ${compactSucceeded ? "completed" : "failed"}`);
1393
+ if (opts.onCompletionEvent) {
1394
+ opts.onCompletionEvent(message.subtype === "success" ? "Compaction completed" : `Compaction failed: ${message.errors?.join("\n") || message.subtype}`);
1395
+ }
1396
+ isCompactCommand = false;
1397
+ }
1398
+ opts.onReady(message);
1399
+ const recycleReason = queryRecycleReason(message, lastAssistantError);
1400
+ lastAssistantError = void 0;
1401
+ if (recycleReason) {
1402
+ logger.logger.warn(`[claudeRemote] Ending SDK query after ${recycleReason}; the next message starts a fresh Claude Code process`);
1403
+ opts.onCompletionEvent?.(QUERY_RECYCLE_NOTICE[recycleReason]);
1404
+ opts.onAuthFailure?.(recycleReason);
1405
+ messages.end();
1406
+ continue;
1407
+ }
1408
+ opts.nextMessage().then(async (next) => {
1409
+ if (!next) {
1410
+ messages.end();
1411
+ return;
1412
+ }
1413
+ if (needsModelSwitch(mode.model, next.mode.model)) {
1414
+ try {
1415
+ await response.setModel(modelTarget(next.mode.model));
1416
+ logger.logger.debug(`[claudeRemote] Model switched to ${modelTarget(next.mode.model) ?? "default"}`);
1417
+ } catch (e) {
1418
+ logger.logger.debug(`[claudeRemote] setModel failed: ${e instanceof Error ? e.message : String(e)}`);
1419
+ opts.onCompletionEvent?.(modelSwitchFailureNotice(next.mode.model, e));
1420
+ next = { ...next, mode: { ...next.mode, model: mode.model } };
1421
+ }
1422
+ }
1423
+ mode = next.mode;
1424
+ updateThinking(true);
1425
+ timingMarks = { pushedAt: next.enqueuedAt ?? Date.now() };
1426
+ messages.push({
1427
+ type: "user",
1428
+ parent_tool_use_id: null,
1429
+ origin: { kind: "human" },
1430
+ message: { role: "user", content: next.message }
1431
+ });
1432
+ }).catch(() => {
1433
+ messages.end();
1434
+ });
1435
+ }
1436
+ if (message.type === "user") {
1437
+ const msg = message;
1438
+ if (msg.message.role === "user" && Array.isArray(msg.message.content)) {
1439
+ for (let c of msg.message.content) {
1440
+ if (c.type === "tool_result" && c.tool_use_id && opts.isAborted(c.tool_use_id)) {
1441
+ logger.logger.debug("[claudeRemote] Tool aborted, exiting claudeRemote");
1442
+ return;
1443
+ }
1444
+ }
1445
+ }
1446
+ }
1447
+ }
1448
+ } catch (e) {
1449
+ if (e instanceof claudeAgentSdk.AbortError) {
1450
+ logger.logger.debug(`[claudeRemote] Aborted`);
1451
+ } else {
1452
+ throw e;
1453
+ }
1454
+ } finally {
1455
+ updateThinking(false);
1456
+ }
1457
+ }
1458
+
1459
+ function getToolDescriptor(toolName) {
1460
+ if (toolName === "exit_plan_mode" || toolName === "ExitPlanMode") {
1461
+ return { edit: false, exitPlan: true, dangerous: false };
1462
+ }
1463
+ if (toolName === "Edit" || toolName === "MultiEdit" || toolName === "Write" || toolName === "NotebookEdit") {
1464
+ return { edit: true, exitPlan: false, dangerous: true };
1465
+ }
1466
+ if (toolName === "Bash") {
1467
+ return { edit: false, exitPlan: false, dangerous: true };
1468
+ }
1469
+ return { edit: false, exitPlan: false, dangerous: false };
1470
+ }
1471
+
1472
+ function elicitationContent(input) {
1473
+ if (!input) return void 0;
1474
+ const content = {};
1475
+ for (const [key, value] of Object.entries(input)) {
1476
+ if (typeof value === "string" || typeof value === "number" && Number.isFinite(value) || typeof value === "boolean" || Array.isArray(value) && value.every((item) => typeof item === "string")) {
1477
+ content[key] = value;
1478
+ }
1479
+ }
1480
+ return content;
1481
+ }
1482
+ class PermissionHandler {
1483
+ responses = /* @__PURE__ */ new Map();
1484
+ pendingRequests = /* @__PURE__ */ new Map();
1485
+ session;
1486
+ allowedTools = /* @__PURE__ */ new Set();
1487
+ allowedBashLiterals = /* @__PURE__ */ new Set();
1488
+ allowedBashPrefixes = /* @__PURE__ */ new Set();
1489
+ permissionMode = "default";
1490
+ onPermissionRequestCallback;
1491
+ /** Callback to change permission mode on the active query (set by claudeRemote) */
1492
+ setPermissionModeCallback;
1493
+ modeChangeQueue = Promise.resolve();
1494
+ deferredPermissionMode;
1495
+ /** Fired when an approval response (plan approval / approve-with-mode) moved the mode. */
1496
+ onModeChangedCallback;
1497
+ constructor(session) {
1498
+ this.session = session;
1499
+ this.setupClientHandler();
1500
+ }
1501
+ /**
1502
+ * Set callback to trigger when permission request is made
1503
+ */
1504
+ setOnPermissionRequest(callback) {
1505
+ this.onPermissionRequestCallback = callback;
1506
+ }
1507
+ handleModeChange(mode) {
1508
+ this.permissionMode = permissionMode.mapToClaudeMode(mode);
1509
+ }
1510
+ /** Current locally enforced mode (what canUseTool applies right now). */
1511
+ getPermissionMode() {
1512
+ return this.permissionMode;
1513
+ }
1514
+ /**
1515
+ * Observe mode transitions that originate inside the approval flow
1516
+ * (ExitPlanMode approval, approve-with-mode). Live/idle RPC changes and
1517
+ * queue-driven changes are owned by the launcher and do not fire this.
1518
+ */
1519
+ setOnModeChanged(callback) {
1520
+ this.onModeChangedCallback = callback;
1521
+ }
1522
+ /**
1523
+ * Set callback to dynamically change permission mode on the active query.
1524
+ * Called by claudeRemote after the Query object is created.
1525
+ */
1526
+ setPermissionModeUpdater(callback) {
1527
+ this.setPermissionModeCallback = callback;
1528
+ }
1529
+ /**
1530
+ * Apply a user-requested mode to the active SDK Query. Mode changes are
1531
+ * serialized so rapid menu selections cannot finish out of order.
1532
+ *
1533
+ * A live Query can be blocked inside canUseTool. When the requested mode
1534
+ * would have auto-allowed that ordinary tool, finish the callback first;
1535
+ * interactive prompts and ExitPlanMode deliberately remain pending.
1536
+ */
1537
+ setLivePermissionMode(mode) {
1538
+ const task = this.modeChangeQueue.then(() => this.applyLivePermissionMode(mode));
1539
+ this.modeChangeQueue = task.catch(() => void 0);
1540
+ return task;
1541
+ }
1542
+ async applyLivePermissionMode(mode) {
1543
+ if (!this.setPermissionModeCallback) {
1544
+ throw new Error("active Claude query is not ready");
1545
+ }
1546
+ const previousMode = this.permissionMode;
1547
+ this.permissionMode = mode;
1548
+ this.deferredPermissionMode = void 0;
1549
+ try {
1550
+ const autoApproved = [...this.pendingRequests.entries()].filter(([, pending]) => pending.kind === "tool" && this.modeAutoAllowsTool(mode, pending.toolName));
1551
+ for (const [id, pending] of autoApproved) {
1552
+ if (pending.kind !== "tool" || !this.pendingRequests.delete(id)) continue;
1553
+ const response = {
1554
+ id,
1555
+ approved: true,
1556
+ decision: "approved",
1557
+ reason: `Approved by live permission mode ${mode}`
1558
+ };
1559
+ const effectiveResponse = await this.handlePermissionResponse(response, pending);
1560
+ this.responses.set(id, { ...effectiveResponse, receivedAt: Date.now() });
1561
+ this.recordCompletedRequest(id, effectiveResponse);
1562
+ }
1563
+ if (this.pendingRequests.size > 0) {
1564
+ this.deferredPermissionMode = mode;
1565
+ return;
1566
+ }
1567
+ await new Promise((resolve) => setImmediate(resolve));
1568
+ await this.setPermissionModeCallback(mode);
1569
+ } catch (error) {
1570
+ this.permissionMode = previousMode;
1571
+ if (this.deferredPermissionMode === mode) this.deferredPermissionMode = void 0;
1572
+ throw error;
1573
+ }
1574
+ }
1575
+ scheduleDeferredPermissionModeUpdate() {
1576
+ const mode = this.deferredPermissionMode;
1577
+ if (!mode || this.pendingRequests.size > 0) return;
1578
+ this.deferredPermissionMode = void 0;
1579
+ setImmediate(() => {
1580
+ const task = this.modeChangeQueue.then(async () => {
1581
+ if (!this.setPermissionModeCallback || this.permissionMode !== mode) return;
1582
+ await this.setPermissionModeCallback(mode);
1583
+ });
1584
+ this.modeChangeQueue = task.catch((error) => {
1585
+ logger.logger.debug("[PermissionHandler] Deferred SDK permission mode update failed", error);
1586
+ });
1587
+ });
1588
+ }
1589
+ modeAutoAllowsTool(mode, toolName) {
1590
+ if (toolName === "AskUserQuestion") return false;
1591
+ const descriptor = getToolDescriptor(toolName);
1592
+ if (descriptor.exitPlan) return false;
1593
+ if (mode === "bypassPermissions") return true;
1594
+ if (mode === "acceptEdits") return descriptor.edit;
1595
+ return mode === "plan" && !descriptor.dangerous;
1596
+ }
1597
+ /**
1598
+ * Handler response
1599
+ */
1600
+ async handlePermissionResponse(response, pending) {
1601
+ if (pending.toolName === "exit_plan_mode" || pending.toolName === "ExitPlanMode") {
1602
+ logger.logger.debug("Plan mode result received", {
1603
+ approved: response.approved,
1604
+ mode: response.mode,
1605
+ reason: contentLogMetadata.contentLogMetadata(response.reason),
1606
+ allowToolCount: response.allowTools?.length ?? 0,
1607
+ updatedInput: contentLogMetadata.contentLogMetadata(response.updatedInput)
1608
+ });
1609
+ if (response.approved) {
1610
+ const newMode = response.mode && ["default", "acceptEdits", "bypassPermissions"].includes(response.mode) ? response.mode : "bypassPermissions";
1611
+ logger.logger.debug(`Plan approved - allowing ExitPlanMode with ${newMode} as the local follow-up mode`);
1612
+ this.permissionMode = newMode;
1613
+ this.onModeChangedCallback?.(newMode);
1614
+ pending.resolve({ behavior: "allow", updatedInput: pending.input || {} });
1615
+ } else {
1616
+ pending.resolve({ behavior: "deny", message: response.reason || "Plan rejected" });
1617
+ }
1618
+ } else {
1619
+ if (response.approved && response.mode) {
1620
+ this.permissionMode = response.mode;
1621
+ this.deferredPermissionMode = response.mode;
1622
+ this.onModeChangedCallback?.(response.mode);
1623
+ }
1624
+ if (response.approved && !pending.suggestions?.length && response.allowTools?.length) {
1625
+ response.allowTools.forEach((tool) => {
1626
+ if (tool.startsWith("Bash(") || tool === "Bash") {
1627
+ this.parseBashPermission(tool);
1628
+ } else {
1629
+ this.allowedTools.add(tool);
1630
+ }
1631
+ });
1632
+ }
1633
+ const originalInput = pending.input || {};
1634
+ const updatedInput = response.updatedInput ? { ...originalInput, ...response.updatedInput } : originalInput;
1635
+ const result = response.approved ? {
1636
+ behavior: "allow",
1637
+ updatedInput,
1638
+ ...response.decision === "approved_for_session" && pending.suggestions?.length ? { updatedPermissions: pending.suggestions } : {}
1639
+ } : { behavior: "deny", message: response.reason || `The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.` };
1640
+ pending.resolve(result);
1641
+ }
1642
+ return response;
1643
+ }
1644
+ /**
1645
+ * Creates the canCallTool callback for the SDK.
1646
+ * Uses toolUseID from official SDK callback options directly.
1647
+ */
1648
+ handleToolCall = async (toolName, input, mode, options) => {
1649
+ const toolCallId = options.toolUseID;
1650
+ if (toolName === "AskUserQuestion") {
1651
+ return this.handlePermissionRequest(toolCallId, toolName, input, options.signal, options.suggestions);
1652
+ }
1653
+ if (toolName === "Bash") {
1654
+ const inputObj = input;
1655
+ if (inputObj?.command) {
1656
+ if (this.allowedBashLiterals.has(inputObj.command)) {
1657
+ return { behavior: "allow", updatedInput: input };
1658
+ }
1659
+ for (const prefix of this.allowedBashPrefixes) {
1660
+ if (inputObj.command.startsWith(prefix)) {
1661
+ return { behavior: "allow", updatedInput: input };
1662
+ }
1663
+ }
1664
+ }
1665
+ } else if (this.allowedTools.has(toolName)) {
1666
+ return { behavior: "allow", updatedInput: input };
1667
+ }
1668
+ const descriptor = getToolDescriptor(toolName);
1669
+ if (descriptor.exitPlan) {
1670
+ return this.handlePermissionRequest(toolCallId, toolName, input, options.signal, options.suggestions);
1671
+ }
1672
+ if (this.permissionMode === "bypassPermissions") {
1673
+ return { behavior: "allow", updatedInput: input };
1674
+ }
1675
+ if (this.permissionMode === "acceptEdits" && descriptor.edit) {
1676
+ return { behavior: "allow", updatedInput: input };
1677
+ }
1678
+ if (this.permissionMode === "plan" && !descriptor.dangerous) {
1679
+ return { behavior: "allow", updatedInput: input };
1680
+ }
1681
+ return this.handlePermissionRequest(toolCallId, toolName, input, options.signal, options.suggestions);
1682
+ };
1683
+ /**
1684
+ * Handles individual permission requests
1685
+ */
1686
+ async handlePermissionRequest(id, toolName, input, signal, suggestions) {
1687
+ return new Promise((resolve, reject) => {
1688
+ if (signal.aborted) {
1689
+ reject(new Error("Permission request aborted", { cause: signal.reason }));
1690
+ return;
1691
+ }
1692
+ const abortHandler = () => {
1693
+ this.pendingRequests.delete(id);
1694
+ this.cancelRequestInAgentState(id, "Permission request aborted");
1695
+ reject(new Error("Permission request aborted"));
1696
+ };
1697
+ signal.addEventListener("abort", abortHandler, { once: true });
1698
+ this.pendingRequests.set(id, {
1699
+ kind: "tool",
1700
+ resolve: (result) => {
1701
+ signal.removeEventListener("abort", abortHandler);
1702
+ resolve(result);
1703
+ },
1704
+ reject: (error) => {
1705
+ signal.removeEventListener("abort", abortHandler);
1706
+ reject(error);
1707
+ },
1708
+ toolName,
1709
+ input,
1710
+ suggestions
1711
+ });
1712
+ if (this.onPermissionRequestCallback) {
1713
+ this.onPermissionRequestCallback(id);
1714
+ }
1715
+ this.session.api.push().sendSessionNotification({
1716
+ kind: "permission",
1717
+ metadata: this.session.client.getMetadata(),
1718
+ data: {
1719
+ sessionId: this.session.client.sessionId,
1720
+ requestId: id,
1721
+ tool: toolName,
1722
+ type: "permission_request",
1723
+ provider: "claude"
1724
+ }
1725
+ });
1726
+ this.session.client.updateAgentState((currentState) => ({
1727
+ ...currentState,
1728
+ requests: {
1729
+ ...currentState.requests,
1730
+ [id]: {
1731
+ tool: toolName,
1732
+ arguments: input,
1733
+ createdAt: Date.now(),
1734
+ kind: "tool",
1735
+ ...suggestions?.length ? { permissionSuggestions: suggestions } : {}
1736
+ }
1737
+ }
1738
+ }));
1739
+ this.session.notificationProducer?.permissionRequest(toolName);
1740
+ this.session.reportEventToDaemon("needs_input");
1741
+ logger.logger.debug(`Permission request sent for tool call ${id}: ${toolName}`);
1742
+ });
1743
+ }
1744
+ handleElicitation = async (request, options) => {
1745
+ return this.handleInteractionRequest(options.requestId, "elicitation", request, options.signal);
1746
+ };
1747
+ handleUserDialog = async (request, options) => {
1748
+ if (request.dialogKind !== "refusal_fallback_prompt") {
1749
+ return { behavior: "cancelled" };
1750
+ }
1751
+ return this.handleInteractionRequest(options.requestId, "user_dialog", request, options.signal);
1752
+ };
1753
+ handleInteractionRequest(id, kind, request, signal) {
1754
+ return new Promise((resolve, reject) => {
1755
+ if (signal.aborted) {
1756
+ reject(new Error("Interaction request aborted", { cause: signal.reason }));
1757
+ return;
1758
+ }
1759
+ const abortHandler = () => {
1760
+ this.pendingRequests.delete(id);
1761
+ this.cancelRequestInAgentState(id, "Interaction request aborted");
1762
+ reject(new Error("Interaction request aborted", { cause: signal.reason }));
1763
+ };
1764
+ signal.addEventListener("abort", abortHandler, { once: true });
1765
+ const finishResolve = (value) => {
1766
+ signal.removeEventListener("abort", abortHandler);
1767
+ resolve(value);
1768
+ };
1769
+ const finishReject = (error) => {
1770
+ signal.removeEventListener("abort", abortHandler);
1771
+ reject(error);
1772
+ };
1773
+ if (kind === "elicitation") {
1774
+ this.pendingRequests.set(id, {
1775
+ kind,
1776
+ request,
1777
+ resolve: finishResolve,
1778
+ reject: finishReject
1779
+ });
1780
+ } else {
1781
+ this.pendingRequests.set(id, {
1782
+ kind,
1783
+ request,
1784
+ resolve: finishResolve,
1785
+ reject: finishReject
1786
+ });
1787
+ }
1788
+ const tool = "AskUserQuestion";
1789
+ this.publishRequest(id, tool, request, kind);
1790
+ });
1791
+ }
1792
+ publishRequest(id, tool, args, kind) {
1793
+ this.session.api.push().sendSessionNotification({
1794
+ kind: "permission",
1795
+ metadata: this.session.client.getMetadata(),
1796
+ data: {
1797
+ sessionId: this.session.client.sessionId,
1798
+ requestId: id,
1799
+ tool,
1800
+ type: "permission_request",
1801
+ provider: "claude"
1802
+ }
1803
+ });
1804
+ this.session.client.updateAgentState((currentState) => ({
1805
+ ...currentState,
1806
+ requests: {
1807
+ ...currentState.requests,
1808
+ [id]: { tool, arguments: args, createdAt: Date.now(), kind }
1809
+ }
1810
+ }));
1811
+ this.session.notificationProducer?.permissionRequest(tool);
1812
+ this.session.reportEventToDaemon("needs_input");
1813
+ logger.logger.debug(`${kind} request sent: ${id}`);
1814
+ }
1815
+ cancelRequestInAgentState(id, reason) {
1816
+ this.session.client.updateAgentState((currentState) => {
1817
+ const request = currentState.requests?.[id];
1818
+ if (!request) return currentState;
1819
+ const requests = { ...currentState.requests };
1820
+ delete requests[id];
1821
+ return {
1822
+ ...currentState,
1823
+ requests,
1824
+ completedRequests: {
1825
+ ...currentState.completedRequests,
1826
+ [id]: {
1827
+ ...request,
1828
+ completedAt: Date.now(),
1829
+ status: "canceled",
1830
+ reason
1831
+ }
1832
+ }
1833
+ };
1834
+ });
1835
+ }
1836
+ recordCompletedRequest(id, response) {
1837
+ this.session.client.updateAgentState((currentState) => {
1838
+ const request = currentState.requests?.[id];
1839
+ if (!request) return currentState;
1840
+ const requests = { ...currentState.requests };
1841
+ delete requests[id];
1842
+ return {
1843
+ ...currentState,
1844
+ requests,
1845
+ completedRequests: {
1846
+ ...currentState.completedRequests,
1847
+ [id]: {
1848
+ ...request,
1849
+ completedAt: Date.now(),
1850
+ status: response.approved ? "approved" : "denied",
1851
+ reason: response.reason,
1852
+ mode: response.mode,
1853
+ decision: response.decision,
1854
+ allowedTools: response.allowTools
1855
+ }
1856
+ }
1857
+ };
1858
+ });
1859
+ }
1860
+ /**
1861
+ * Parses Bash permission strings into literal and prefix sets
1862
+ */
1863
+ parseBashPermission(permission) {
1864
+ if (permission === "Bash") {
1865
+ return;
1866
+ }
1867
+ const bashPattern = /^Bash\((.+?)\)$/;
1868
+ const match = permission.match(bashPattern);
1869
+ if (!match) {
1870
+ return;
1871
+ }
1872
+ const command = match[1];
1873
+ if (command.endsWith(":*")) {
1874
+ const prefix = command.slice(0, -2);
1875
+ this.allowedBashPrefixes.add(prefix);
1876
+ } else {
1877
+ this.allowedBashLiterals.add(command);
1878
+ }
1879
+ }
1880
+ /**
1881
+ * Checks if a tool call is rejected
1882
+ */
1883
+ isAborted(toolCallId) {
1884
+ if (this.responses.get(toolCallId)?.approved === false) {
1885
+ return true;
1886
+ }
1887
+ return false;
1888
+ }
1889
+ /**
1890
+ * Resets all state for new sessions
1891
+ */
1892
+ reset(reason = "Session switched to local mode") {
1893
+ this.responses.clear();
1894
+ this.allowedTools.clear();
1895
+ this.allowedBashLiterals.clear();
1896
+ this.allowedBashPrefixes.clear();
1897
+ this.permissionMode = "default";
1898
+ this.deferredPermissionMode = void 0;
1899
+ for (const [, pending] of this.pendingRequests.entries()) {
1900
+ pending.reject(new Error("Session reset"));
1901
+ }
1902
+ this.pendingRequests.clear();
1903
+ this.session.client.updateAgentState((currentState) => {
1904
+ const pendingRequests = currentState.requests || {};
1905
+ const completedRequests = { ...currentState.completedRequests };
1906
+ for (const [id, request] of Object.entries(pendingRequests)) {
1907
+ completedRequests[id] = {
1908
+ ...request,
1909
+ completedAt: Date.now(),
1910
+ status: "canceled",
1911
+ reason
1912
+ };
1913
+ }
1914
+ return {
1915
+ ...currentState,
1916
+ requests: {},
1917
+ // Clear all pending requests
1918
+ completedRequests
1919
+ };
1920
+ });
1921
+ }
1922
+ /**
1923
+ * Sets up the client handler for permission responses
1924
+ */
1925
+ setupClientHandler() {
1926
+ this.session.client.rpcHandlerManager.registerHandler("permission", async (message) => {
1927
+ logger.logger.debug("Permission response received:", {
1928
+ id: message.id,
1929
+ approved: message.approved,
1930
+ mode: message.mode,
1931
+ reason: contentLogMetadata.contentLogMetadata(message.reason),
1932
+ allowToolCount: message.allowTools?.length ?? 0,
1933
+ updatedInput: contentLogMetadata.contentLogMetadata(message.updatedInput)
1934
+ });
1935
+ const id = message.id;
1936
+ const pending = this.pendingRequests.get(id);
1937
+ if (!pending) {
1938
+ logger.logger.debug("Permission request not found or already resolved");
1939
+ return;
1940
+ }
1941
+ this.pendingRequests.delete(id);
1942
+ let effectiveResponse = message;
1943
+ if (pending.kind === "tool") {
1944
+ effectiveResponse = await this.handlePermissionResponse(message, pending);
1945
+ this.responses.set(id, { ...effectiveResponse, receivedAt: Date.now() });
1946
+ } else if (pending.kind === "elicitation") {
1947
+ pending.resolve(message.approved ? { action: "accept", content: elicitationContent(message.updatedInput) } : { action: message.decision === "abort" ? "cancel" : "decline" });
1948
+ } else {
1949
+ pending.resolve(message.approved && pending.request.dialogKind === "refusal_fallback_prompt" && (message.updatedInput?.result === void 0 || message.updatedInput.result === "retry_fallback") ? { behavior: "completed", result: "retry_fallback" } : { behavior: "cancelled" });
1950
+ }
1951
+ this.recordCompletedRequest(id, effectiveResponse);
1952
+ this.scheduleDeferredPermissionModeUpdate();
1953
+ });
1954
+ }
1955
+ /**
1956
+ * Gets the responses map (for compatibility with existing code)
1957
+ */
1958
+ getResponses() {
1959
+ return this.responses;
1960
+ }
1961
+ }
1962
+
1963
+ function rewriteQueuedPermissionMode(queue, hasher, permissionMode) {
1964
+ let rewritten = 0;
1965
+ for (const item of queue) {
1966
+ if (item.mode.permissionMode === permissionMode) continue;
1967
+ item.mode = { ...item.mode, permissionMode };
1968
+ item.modeHash = hasher(item.mode);
1969
+ rewritten += 1;
1970
+ }
1971
+ return rewritten;
1972
+ }
1973
+
1974
+ function formatClaudeMessageForInk(message, messageBuffer, onAssistantResult) {
1975
+ logger.logger.debugLargeJson("[CLAUDE INK] Message from remote mode:", message);
1976
+ switch (message.type) {
1977
+ case "system": {
1978
+ const sysMsg = message;
1979
+ if (sysMsg.subtype === "init") {
1980
+ messageBuffer.addMessage("\u2500".repeat(40), "status");
1981
+ messageBuffer.addMessage(`\u{1F680} Session initialized: ${sysMsg.session_id}`, "system");
1982
+ messageBuffer.addMessage(` Model: ${sysMsg.model}`, "status");
1983
+ messageBuffer.addMessage(` CWD: ${sysMsg.cwd}`, "status");
1984
+ if (sysMsg.tools && sysMsg.tools.length > 0) {
1985
+ messageBuffer.addMessage(` Tools: ${sysMsg.tools.join(", ")}`, "status");
1986
+ }
1987
+ messageBuffer.addMessage("\u2500".repeat(40), "status");
1988
+ }
1989
+ break;
1990
+ }
1991
+ case "user": {
1992
+ const userMsg = message;
1993
+ if (userMsg.message && typeof userMsg.message === "object" && "content" in userMsg.message) {
1994
+ const content = userMsg.message.content;
1995
+ if (typeof content === "string") {
1996
+ messageBuffer.addMessage(`\u{1F464} User: ${content}`, "user");
1997
+ } else if (Array.isArray(content)) {
1998
+ for (const block of content) {
1999
+ if (block.type === "text") {
2000
+ messageBuffer.addMessage(`\u{1F464} User: ${block.text}`, "user");
2001
+ } else if (block.type === "tool_result") {
2002
+ messageBuffer.addMessage(`\u2705 Tool Result (ID: ${block.tool_use_id})`, "result");
2003
+ if (block.content) {
2004
+ const outputStr = typeof block.content === "string" ? block.content : JSON.stringify(block.content, null, 2);
2005
+ const maxLength = 200;
2006
+ if (outputStr.length > maxLength) {
2007
+ messageBuffer.addMessage(outputStr.substring(0, maxLength) + "... (truncated)", "result");
2008
+ } else {
2009
+ messageBuffer.addMessage(outputStr, "result");
2010
+ }
2011
+ }
2012
+ }
2013
+ }
2014
+ } else {
2015
+ messageBuffer.addMessage(`\u{1F464} User: ${JSON.stringify(content, null, 2)}`, "user");
2016
+ }
2017
+ }
2018
+ break;
2019
+ }
2020
+ case "assistant": {
2021
+ const assistantMsg = message;
2022
+ if (assistantMsg.message && assistantMsg.message.content) {
2023
+ messageBuffer.addMessage("\u{1F916} Assistant:", "assistant");
2024
+ for (const block of assistantMsg.message.content) {
2025
+ if (block.type === "text") {
2026
+ messageBuffer.addMessage(block.text || "", "assistant");
2027
+ } else if (block.type === "tool_use") {
2028
+ messageBuffer.addMessage(`\u{1F527} Tool: ${block.name}`, "tool");
2029
+ if (block.input) {
2030
+ const inputStr = JSON.stringify(block.input, null, 2);
2031
+ const maxLength = 500;
2032
+ if (inputStr.length > maxLength) {
2033
+ messageBuffer.addMessage(`Input: ${inputStr.substring(0, maxLength)}... (truncated)`, "tool");
2034
+ } else {
2035
+ messageBuffer.addMessage(`Input: ${inputStr}`, "tool");
2036
+ }
2037
+ }
2038
+ }
2039
+ }
2040
+ }
2041
+ break;
2042
+ }
2043
+ case "result": {
2044
+ const resultMsg = message;
2045
+ if (resultMsg.subtype === "success") {
2046
+ if ("result" in resultMsg && resultMsg.result) {
2047
+ messageBuffer.addMessage("\u2728 Summary:", "result");
2048
+ messageBuffer.addMessage(resultMsg.result || "", "result");
2049
+ }
2050
+ if (resultMsg.usage) {
2051
+ messageBuffer.addMessage("\u{1F4CA} Session Stats:", "status");
2052
+ messageBuffer.addMessage(` \u2022 Turns: ${resultMsg.num_turns}`, "status");
2053
+ messageBuffer.addMessage(` \u2022 Input tokens: ${resultMsg.usage.input_tokens}`, "status");
2054
+ messageBuffer.addMessage(` \u2022 Output tokens: ${resultMsg.usage.output_tokens}`, "status");
2055
+ if (resultMsg.usage.cache_read_input_tokens) {
2056
+ messageBuffer.addMessage(` \u2022 Cache read tokens: ${resultMsg.usage.cache_read_input_tokens}`, "status");
2057
+ }
2058
+ if (resultMsg.usage.cache_creation_input_tokens) {
2059
+ messageBuffer.addMessage(` \u2022 Cache creation tokens: ${resultMsg.usage.cache_creation_input_tokens}`, "status");
2060
+ }
2061
+ messageBuffer.addMessage(` \u2022 Cost: $${resultMsg.total_cost_usd.toFixed(4)}`, "status");
2062
+ messageBuffer.addMessage(` \u2022 Duration: ${resultMsg.duration_ms}ms`, "status");
2063
+ }
2064
+ } else if (resultMsg.subtype === "error_max_turns") {
2065
+ messageBuffer.addMessage("\u274C Error: Maximum turns reached", "result");
2066
+ messageBuffer.addMessage(`Completed ${resultMsg.num_turns} turns`, "status");
2067
+ } else if (resultMsg.subtype === "error_during_execution") {
2068
+ messageBuffer.addMessage("\u274C Error during execution", "result");
2069
+ messageBuffer.addMessage(`Completed ${resultMsg.num_turns} turns before error`, "status");
2070
+ logger.logger.debugLargeJson("[RESULT] Error during execution", resultMsg);
2071
+ }
2072
+ break;
2073
+ }
2074
+ default: {
2075
+ if (process.env.DEBUG) {
2076
+ messageBuffer.addMessage(`[Unknown message type: ${message.type}]`, "status");
2077
+ }
2078
+ }
2079
+ }
2080
+ }
2081
+
2082
+ const INTERRUPT_SENTINEL = /^\[Request interrupted by user(?: for tool use)?\]$/i;
2083
+ const EDE_DIAGNOSTIC_PREFIX = "[ede_diagnostic]";
2084
+ const SDK_ERROR_PREFIX = "Claude Code returned an error result:";
2085
+ function errorEntries(value) {
2086
+ return value.split(/[;\n]+/).map((entry) => entry.trim()).filter(Boolean);
2087
+ }
2088
+ function textBlocks(content) {
2089
+ if (!Array.isArray(content) || content.length === 0) return null;
2090
+ const texts = [];
2091
+ for (const block of content) {
2092
+ if (!block || typeof block !== "object" || block.type !== "text") {
2093
+ return null;
2094
+ }
2095
+ const text = block.text;
2096
+ if (typeof text !== "string") return null;
2097
+ texts.push(text.trim());
2098
+ }
2099
+ return texts;
2100
+ }
2101
+ function isClaudeInterruptSentinelContent(content) {
2102
+ if (typeof content === "string") {
2103
+ return INTERRUPT_SENTINEL.test(content.trim());
2104
+ }
2105
+ const texts = textBlocks(content);
2106
+ return texts !== null && texts.length > 0 && texts.every((text) => INTERRUPT_SENTINEL.test(text));
2107
+ }
2108
+ function resultErrors(value) {
2109
+ if (!value || typeof value !== "object") return [];
2110
+ const errors = value.errors;
2111
+ if (!Array.isArray(errors) || errors.length === 0 || errors.some((error) => typeof error !== "string")) {
2112
+ return [];
2113
+ }
2114
+ return errors.map((error) => error.trim()).filter(Boolean);
2115
+ }
2116
+ function isClaudeEdeOnlyResult(value) {
2117
+ const errors = resultErrors(value);
2118
+ return errors.length > 0 && errors.every((error) => error.startsWith(EDE_DIAGNOSTIC_PREFIX));
2119
+ }
2120
+ function stripClaudeEdeDiagnosticText(value) {
2121
+ if (typeof value !== "string") return void 0;
2122
+ const trimmed = value.trim();
2123
+ if (!trimmed) return void 0;
2124
+ if (!trimmed.includes(EDE_DIAGNOSTIC_PREFIX)) return trimmed;
2125
+ const visible = errorEntries(trimmed).filter((entry) => !entry.startsWith(EDE_DIAGNOSTIC_PREFIX));
2126
+ return visible.length > 0 ? visible.join("\n") : void 0;
2127
+ }
2128
+ function stripClaudeEdeDiagnosticErrors(value) {
2129
+ return resultErrors({ errors: value }).filter((error) => !error.startsWith(EDE_DIAGNOSTIC_PREFIX));
2130
+ }
2131
+ function isClaudeEdeOnlySdkError(error) {
2132
+ if (!(error instanceof Error)) return false;
2133
+ const message = error.message.trim();
2134
+ if (!message.startsWith(SDK_ERROR_PREFIX)) return false;
2135
+ const payload = message.slice(SDK_ERROR_PREFIX.length).trim();
2136
+ const entries = errorEntries(payload);
2137
+ return entries.length > 0 && entries.every((entry) => entry.startsWith(EDE_DIAGNOSTIC_PREFIX));
2138
+ }
2139
+
2140
+ function getGitBranch(cwd) {
2141
+ try {
2142
+ const branch = node_child_process.execSync("git rev-parse --abbrev-ref HEAD", {
2143
+ cwd,
2144
+ encoding: "utf8",
2145
+ stdio: ["ignore", "pipe", "ignore"],
2146
+ windowsHide: true
2147
+ }).trim();
2148
+ return branch || void 0;
2149
+ } catch {
2150
+ return void 0;
2151
+ }
2152
+ }
2153
+ class SDKToLogConverter {
2154
+ lastUuid = null;
2155
+ context;
2156
+ responses;
2157
+ sidechainLastUUID = /* @__PURE__ */ new Map();
2158
+ sawInterruptSentinel = false;
2159
+ sawEdeOnlyAssistantError = false;
2160
+ sawVisibleAssistantError = false;
2161
+ constructor(context, responses) {
2162
+ this.context = {
2163
+ ...context,
2164
+ gitBranch: context.gitBranch ?? getGitBranch(context.cwd),
2165
+ version: context.version ?? process.env.npm_package_version ?? "0.0.0",
2166
+ parentUuid: null
2167
+ };
2168
+ this.responses = responses;
2169
+ }
2170
+ /**
2171
+ * Update session ID (for when session changes during resume)
2172
+ */
2173
+ updateSessionId(sessionId) {
2174
+ this.context.sessionId = sessionId;
2175
+ }
2176
+ /**
2177
+ * Reset parent chain (useful when starting new conversation)
2178
+ */
2179
+ resetParentChain() {
2180
+ this.lastUuid = null;
2181
+ this.context.parentUuid = null;
2182
+ }
2183
+ /** Drop per-query markers that must never leak into the next SDK query. */
2184
+ resetTransientState() {
2185
+ this.sawInterruptSentinel = false;
2186
+ this.sawEdeOnlyAssistantError = false;
2187
+ this.sawVisibleAssistantError = false;
2188
+ }
2189
+ /**
2190
+ * Convert SDK message to log format
2191
+ */
2192
+ convert(sdkMessage) {
2193
+ const uuid = node_crypto.randomUUID();
2194
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2195
+ let parentUuid = this.lastUuid;
2196
+ let isSidechain = false;
2197
+ const parentToolUseId = sdkMessage.parent_tool_use_id;
2198
+ if (parentToolUseId) {
2199
+ isSidechain = true;
2200
+ parentUuid = this.sidechainLastUUID.get(parentToolUseId) ?? null;
2201
+ this.sidechainLastUUID.set(parentToolUseId, uuid);
2202
+ }
2203
+ const baseFields = {
2204
+ parentUuid,
2205
+ isSidechain,
2206
+ userType: "external",
2207
+ cwd: this.context.cwd,
2208
+ sessionId: this.context.sessionId,
2209
+ version: this.context.version,
2210
+ gitBranch: this.context.gitBranch,
2211
+ uuid,
2212
+ timestamp
2213
+ };
2214
+ let logMessage = null;
2215
+ switch (sdkMessage.type) {
2216
+ case "user": {
2217
+ const userMsg = sdkMessage;
2218
+ if (isClaudeInterruptSentinelContent(userMsg.message.content)) {
2219
+ this.sawInterruptSentinel = true;
2220
+ return null;
2221
+ }
2222
+ const meta = userMsg.isSynthetic === true || userMsg.isMeta === true;
2223
+ logMessage = {
2224
+ ...baseFields,
2225
+ type: "user",
2226
+ message: userMsg.message,
2227
+ ...userMsg.parent_tool_use_id ? { parent_tool_use_id: userMsg.parent_tool_use_id } : {},
2228
+ ...meta ? { isMeta: true } : {},
2229
+ // B-260-P2: structured tool output (Agent/Task final report +
2230
+ // run totals, or the `async_launched` stub marker) and the
2231
+ // message origin (task-notification). Both are consumed by
2232
+ // sessionProtocolMapper; the raw-line schema is passthrough.
2233
+ ...userMsg.tool_use_result !== void 0 ? { tool_use_result: userMsg.tool_use_result } : {},
2234
+ ...userMsg.origin ? { origin: userMsg.origin } : {}
2235
+ };
2236
+ if (Array.isArray(userMsg.message.content)) {
2237
+ for (const content of userMsg.message.content) {
2238
+ if (content.type === "tool_result" && content.tool_use_id && this.responses?.has(content.tool_use_id)) {
2239
+ const response = this.responses.get(content.tool_use_id);
2240
+ if (response?.mode) {
2241
+ logMessage.mode = response.mode;
2242
+ }
2243
+ }
2244
+ }
2245
+ } else if (typeof userMsg.message.content === "string") ;
2246
+ break;
2247
+ }
2248
+ case "assistant": {
2249
+ const assistantMsg = sdkMessage;
2250
+ const assistantError = stripClaudeEdeDiagnosticText(assistantMsg.error);
2251
+ if (typeof assistantMsg.error === "string" && assistantMsg.error.trim() && !assistantError) {
2252
+ this.sawEdeOnlyAssistantError = true;
2253
+ } else if (assistantError) {
2254
+ this.sawVisibleAssistantError = true;
2255
+ }
2256
+ logMessage = {
2257
+ ...baseFields,
2258
+ type: "assistant",
2259
+ message: assistantMsg.message,
2260
+ // Assistant messages often have additional fields
2261
+ requestId: assistantMsg.requestId,
2262
+ ...assistantMsg.isCompactSummary ? { isCompactSummary: true } : {},
2263
+ ...assistantMsg.parent_tool_use_id ? { parent_tool_use_id: assistantMsg.parent_tool_use_id } : {},
2264
+ ...assistantError ? { error: assistantError } : {}
2265
+ };
2266
+ break;
2267
+ }
2268
+ case "system": {
2269
+ const systemMsg = sdkMessage;
2270
+ if (systemMsg.subtype === "init" && systemMsg.session_id) {
2271
+ this.updateSessionId(systemMsg.session_id);
2272
+ }
2273
+ logMessage = {
2274
+ ...baseFields,
2275
+ type: "system",
2276
+ subtype: systemMsg.subtype,
2277
+ model: systemMsg.model,
2278
+ tools: systemMsg.tools,
2279
+ // Include all other fields
2280
+ ...systemMsg
2281
+ };
2282
+ break;
2283
+ }
2284
+ case "result": {
2285
+ const resultMsg = sdkMessage;
2286
+ const visibleErrors = stripClaudeEdeDiagnosticErrors(
2287
+ "errors" in resultMsg ? resultMsg.errors : void 0
2288
+ );
2289
+ const interruptionSignal = this.sawInterruptSentinel || this.sawEdeOnlyAssistantError || isClaudeEdeOnlyResult(resultMsg);
2290
+ const interrupted = interruptionSignal && !this.sawVisibleAssistantError && visibleErrors.length === 0;
2291
+ this.sawInterruptSentinel = false;
2292
+ this.sawEdeOnlyAssistantError = false;
2293
+ this.sawVisibleAssistantError = false;
2294
+ logMessage = {
2295
+ ...baseFields,
2296
+ type: "result",
2297
+ subtype: resultMsg.subtype,
2298
+ is_error: resultMsg.is_error,
2299
+ ..."result" in resultMsg && typeof resultMsg.result === "string" ? { result: resultMsg.result } : {},
2300
+ total_cost_usd: resultMsg.total_cost_usd,
2301
+ duration_ms: resultMsg.duration_ms,
2302
+ num_turns: resultMsg.num_turns,
2303
+ usage: resultMsg.usage,
2304
+ ...visibleErrors.length > 0 ? { errors: visibleErrors } : {},
2305
+ ..."terminal_reason" in resultMsg && resultMsg.terminal_reason ? { terminal_reason: resultMsg.terminal_reason } : {},
2306
+ ...interrupted ? { interrupted: true } : {}
2307
+ };
2308
+ break;
2309
+ }
2310
+ // These SDK lifecycle/advisory frames are intentionally not part
2311
+ // of the persisted Claude transcript. Enumerating them keeps SDK
2312
+ // upgrades type-visible instead of silently passing new frames
2313
+ // through as conversation messages.
2314
+ case "stream_event":
2315
+ case "tool_progress":
2316
+ case "auth_status":
2317
+ case "tool_use_summary":
2318
+ case "rate_limit_event":
2319
+ case "prompt_suggestion":
2320
+ case "conversation_reset":
2321
+ return null;
2322
+ default: {
2323
+ const exhaustive = sdkMessage;
2324
+ return exhaustive;
2325
+ }
2326
+ }
2327
+ if (logMessage && logMessage.type !== "summary" && logMessage.type !== "result") {
2328
+ this.lastUuid = uuid;
2329
+ }
2330
+ return logMessage;
2331
+ }
2332
+ /**
2333
+ * Convert multiple SDK messages to log format
2334
+ */
2335
+ convertMany(sdkMessages) {
2336
+ return sdkMessages.map((msg) => this.convert(msg)).filter((msg) => msg !== null);
2337
+ }
2338
+ /**
2339
+ * Convert a simple string content to a sidechain user message
2340
+ * Used for Task tool sub-agent prompts
2341
+ */
2342
+ convertSidechainUserMessage(toolUseId, content) {
2343
+ const uuid = node_crypto.randomUUID();
2344
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2345
+ this.sidechainLastUUID.set(toolUseId, uuid);
2346
+ return {
2347
+ parentUuid: null,
2348
+ isSidechain: true,
2349
+ parent_tool_use_id: toolUseId,
2350
+ userType: "external",
2351
+ cwd: this.context.cwd,
2352
+ sessionId: this.context.sessionId,
2353
+ version: this.context.version,
2354
+ gitBranch: this.context.gitBranch,
2355
+ type: "user",
2356
+ message: {
2357
+ role: "user",
2358
+ content
2359
+ },
2360
+ uuid,
2361
+ timestamp
2362
+ };
2363
+ }
2364
+ /**
2365
+ * Generate an interrupted tool result message
2366
+ * Used when a tool call is interrupted by the user
2367
+ * @param toolUseId - The ID of the tool that was interrupted
2368
+ * @param parentToolUseId - Optional parent tool ID if this is a sidechain tool
2369
+ */
2370
+ generateInterruptedToolResult(toolUseId, parentToolUseId) {
2371
+ const uuid = node_crypto.randomUUID();
2372
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2373
+ const errorMessage = "[Request interrupted by user for tool use]";
2374
+ let isSidechain = false;
2375
+ let parentUuid = this.lastUuid;
2376
+ if (parentToolUseId) {
2377
+ isSidechain = true;
2378
+ parentUuid = this.sidechainLastUUID.get(parentToolUseId) ?? null;
2379
+ this.sidechainLastUUID.set(parentToolUseId, uuid);
2380
+ }
2381
+ const logMessage = {
2382
+ type: "user",
2383
+ isSidechain,
2384
+ ...parentToolUseId ? { parent_tool_use_id: parentToolUseId } : {},
2385
+ uuid,
2386
+ message: {
2387
+ role: "user",
2388
+ content: [
2389
+ {
2390
+ type: "tool_result",
2391
+ content: errorMessage,
2392
+ is_error: true,
2393
+ tool_use_id: toolUseId
2394
+ }
2395
+ ]
2396
+ },
2397
+ parentUuid,
2398
+ userType: "external",
2399
+ cwd: this.context.cwd,
2400
+ sessionId: this.context.sessionId,
2401
+ version: this.context.version,
2402
+ gitBranch: this.context.gitBranch,
2403
+ timestamp,
2404
+ toolUseResult: `Error: ${errorMessage}`
2405
+ };
2406
+ this.lastUuid = uuid;
2407
+ return logMessage;
2408
+ }
2409
+ }
2410
+
2411
+ const transcriptSystemSubtypes = /* @__PURE__ */ new Set(["task_started", "task_updated", "task_progress", "task_notification"]);
2412
+ class OutgoingMessageQueue {
2413
+ constructor(sendFunction) {
2414
+ this.sendFunction = sendFunction;
2415
+ }
2416
+ queue = [];
2417
+ nextId = 1;
2418
+ lock = new attachmentLimits.AsyncLock();
2419
+ processTimer;
2420
+ delayTimers = /* @__PURE__ */ new Map();
2421
+ /**
2422
+ * Add message to queue
2423
+ */
2424
+ enqueue(logMessage, options) {
2425
+ this.lock.inLock(async () => {
2426
+ const item = {
2427
+ id: this.nextId++,
2428
+ logMessage,
2429
+ delayed: !!options?.delay,
2430
+ delayMs: options?.delay || 0,
2431
+ toolCallIds: options?.toolCallIds,
2432
+ released: !options?.delay,
2433
+ // Not delayed = already released
2434
+ sent: false
2435
+ };
2436
+ this.queue.push(item);
2437
+ if (item.delayed) {
2438
+ const timer = setTimeout(() => {
2439
+ this.releaseItem(item.id);
2440
+ }, item.delayMs);
2441
+ this.delayTimers.set(item.id, timer);
2442
+ }
2443
+ });
2444
+ this.scheduleProcessing();
2445
+ }
2446
+ /**
2447
+ * Release specific item by ID
2448
+ */
2449
+ async releaseItem(itemId) {
2450
+ await this.lock.inLock(async () => {
2451
+ const item = this.queue.find((i) => i.id === itemId);
2452
+ if (item && !item.released) {
2453
+ item.released = true;
2454
+ const timer = this.delayTimers.get(itemId);
2455
+ if (timer) {
2456
+ clearTimeout(timer);
2457
+ this.delayTimers.delete(itemId);
2458
+ }
2459
+ }
2460
+ });
2461
+ this.scheduleProcessing();
2462
+ }
2463
+ /**
2464
+ * Release all messages with specific tool call ID
2465
+ */
2466
+ async releaseToolCall(toolCallId) {
2467
+ await this.lock.inLock(async () => {
2468
+ for (const item of this.queue) {
2469
+ if (item.toolCallIds?.includes(toolCallId) && !item.released) {
2470
+ item.released = true;
2471
+ const timer = this.delayTimers.get(item.id);
2472
+ if (timer) {
2473
+ clearTimeout(timer);
2474
+ this.delayTimers.delete(item.id);
2475
+ }
2476
+ }
2477
+ }
2478
+ });
2479
+ this.scheduleProcessing();
2480
+ }
2481
+ /**
2482
+ * Process queue - send messages in ID order that are released
2483
+ * (Internal implementation without lock)
2484
+ */
2485
+ processQueueInternal() {
2486
+ this.queue.sort((a, b) => a.id - b.id);
2487
+ while (this.queue.length > 0) {
2488
+ const item = this.queue[0];
2489
+ if (!item.released) {
2490
+ break;
2491
+ }
2492
+ if (!item.sent) {
2493
+ if (item.logMessage.type !== "system" || transcriptSystemSubtypes.has(item.logMessage.subtype)) {
2494
+ this.sendFunction(item.logMessage);
2495
+ }
2496
+ item.sent = true;
2497
+ }
2498
+ this.queue.shift();
2499
+ }
2500
+ }
2501
+ /**
2502
+ * Process queue - send messages in ID order that are released
2503
+ */
2504
+ async processQueue() {
2505
+ await this.lock.inLock(async () => {
2506
+ this.processQueueInternal();
2507
+ });
2508
+ }
2509
+ /**
2510
+ * Flush all messages immediately (for cleanup)
2511
+ */
2512
+ async flush() {
2513
+ await this.lock.inLock(async () => {
2514
+ for (const timer of this.delayTimers.values()) {
2515
+ clearTimeout(timer);
2516
+ }
2517
+ this.delayTimers.clear();
2518
+ for (const item of this.queue) {
2519
+ item.released = true;
2520
+ }
2521
+ this.processQueueInternal();
2522
+ });
2523
+ }
2524
+ /**
2525
+ * Schedule processing on next tick
2526
+ */
2527
+ scheduleProcessing() {
2528
+ if (this.processTimer) {
2529
+ clearTimeout(this.processTimer);
2530
+ }
2531
+ this.processTimer = setTimeout(() => {
2532
+ this.processQueue();
2533
+ }, 0);
2534
+ }
2535
+ /**
2536
+ * Cleanup timers and resources
2537
+ */
2538
+ destroy() {
2539
+ if (this.processTimer) {
2540
+ clearTimeout(this.processTimer);
2541
+ }
2542
+ for (const timer of this.delayTimers.values()) {
2543
+ clearTimeout(timer);
2544
+ }
2545
+ this.delayTimers.clear();
2546
+ }
2547
+ }
2548
+
2549
+ function getAskUserQuestionToolCallIds(message) {
2550
+ if (message.type !== "assistant") {
2551
+ return [];
2552
+ }
2553
+ const assistantMessage = message;
2554
+ const content = assistantMessage.message.content;
2555
+ if (!Array.isArray(content)) {
2556
+ return [];
2557
+ }
2558
+ const ids = [];
2559
+ for (const block of content) {
2560
+ if (block.type === "tool_use" && block.name === "AskUserQuestion" && typeof block.id === "string" && block.id.length > 0) {
2561
+ ids.push(block.id);
2562
+ }
2563
+ }
2564
+ return ids;
2565
+ }
2566
+
2567
+ async function cleanupStdinAfterInk(opts) {
2568
+ const stdin = opts.stdin;
2569
+ if (!stdin.isTTY) return;
2570
+ const leaveRawMode = opts.leaveRawMode ?? true;
2571
+ try {
2572
+ stdin.setRawMode?.(true);
2573
+ } catch {
2574
+ }
2575
+ const drainMs = Math.max(0, opts.drainMs);
2576
+ if (drainMs === 0) {
2577
+ try {
2578
+ stdin.pause();
2579
+ } catch {
2580
+ }
2581
+ if (!leaveRawMode) {
2582
+ try {
2583
+ stdin.setRawMode?.(false);
2584
+ } catch {
2585
+ }
2586
+ }
2587
+ return;
2588
+ }
2589
+ let bytes = 0;
2590
+ let chunks = 0;
2591
+ const drainListener = (chunk) => {
2592
+ chunks++;
2593
+ if (typeof chunk === "string") {
2594
+ bytes += Buffer.byteLength(chunk);
2595
+ } else if (chunk && typeof chunk.length === "number") {
2596
+ bytes += chunk.length;
2597
+ }
2598
+ };
2599
+ try {
2600
+ stdin.on("data", drainListener);
2601
+ stdin.resume();
2602
+ await new Promise((resolve) => setTimeout(resolve, drainMs));
2603
+ } finally {
2604
+ try {
2605
+ stdin.off("data", drainListener);
2606
+ } catch {
2607
+ }
2608
+ try {
2609
+ stdin.pause();
2610
+ } catch {
2611
+ }
2612
+ if (!leaveRawMode) {
2613
+ try {
2614
+ stdin.setRawMode?.(false);
2615
+ } catch {
2616
+ }
2617
+ }
2618
+ opts.onDebug?.({ kind: "drain-byte-count", bytes, chunks });
2619
+ }
2620
+ }
2621
+
2622
+ function applyClaudeResultLifecycle(result, callbacks) {
2623
+ if (result && (result.subtype !== "success" || result.is_error === true)) {
2624
+ const frame = result;
2625
+ const error = frame.errors?.filter(Boolean).join("\n").trim() || (typeof frame.result === "string" ? frame.result.trim() : "") || frame.subtype;
2626
+ callbacks.closeFailed(error);
2627
+ callbacks.onFailed(error);
2628
+ return;
2629
+ }
2630
+ callbacks.closeCompleted();
2631
+ callbacks.onCompleted();
2632
+ }
2633
+
2634
+ function applyClaudeSdkMetadata(current, update) {
2635
+ return {
2636
+ ...current,
2637
+ ...update.models ? { models: update.models } : {},
2638
+ tools: update.tools,
2639
+ slashCommands: update.slashCommands,
2640
+ mcpServers: update.mcpServers,
2641
+ skills: update.skills,
2642
+ attachmentKinds: [...attachmentContent.CLAUDE_ATTACHMENT_KINDS],
2643
+ queueCancellation: true,
2644
+ // The model Claude Code reports it is ACTUALLY running. system/init is
2645
+ // re-emitted at every turn boundary (verified against the pinned SDK,
2646
+ // 2026-09-03), including after a live setModel, so this is the web's
2647
+ // only ground truth for "did my switch take effect" — the picker
2648
+ // otherwise renders nothing but the client's own optimistic intent.
2649
+ ...update.model ? { currentModelCode: update.model } : {},
2650
+ ...update.modelIsDefault && update.model ? { defaultModelCode: update.model } : {}
2651
+ };
2652
+ }
2653
+
2654
+ function createTurnSteeringController() {
2655
+ let interrupt = null;
2656
+ let steering = false;
2657
+ let interrupting = false;
2658
+ return {
2659
+ setInterrupt(next) {
2660
+ interrupt = next;
2661
+ },
2662
+ async steer() {
2663
+ if (!interrupt) return false;
2664
+ steering = true;
2665
+ try {
2666
+ await interrupt();
2667
+ return true;
2668
+ } catch (error) {
2669
+ steering = false;
2670
+ throw error;
2671
+ }
2672
+ },
2673
+ /**
2674
+ * Interrupt the current turn WITHOUT injecting a follow-up message —
2675
+ * used by the stop button. Like steer it keeps the streaming query
2676
+ * alive (same process, same claudeSessionId); unlike steer it does not
2677
+ * set the steering flag, so the turn simply ends and the query waits
2678
+ * for the next user message. Returns false when no query is attached.
2679
+ */
2680
+ async interruptTurn() {
2681
+ if (!interrupt) return false;
2682
+ interrupting = true;
2683
+ try {
2684
+ await interrupt();
2685
+ return true;
2686
+ } catch (error) {
2687
+ interrupting = false;
2688
+ throw error;
2689
+ }
2690
+ },
2691
+ consumeReady() {
2692
+ if (!steering) return false;
2693
+ steering = false;
2694
+ return true;
2695
+ },
2696
+ /** The next `result` belongs to a stop-button interrupt: close the
2697
+ * turn as cancelled and say "Aborted by user", not "failed". */
2698
+ consumeInterrupted() {
2699
+ if (!interrupting) return false;
2700
+ interrupting = false;
2701
+ return true;
2702
+ },
2703
+ reset() {
2704
+ interrupt = null;
2705
+ steering = false;
2706
+ interrupting = false;
2707
+ }
2708
+ };
2709
+ }
2710
+
2711
+ class LaunchModeGate {
2712
+ constructor(hasher) {
2713
+ this.hasher = hasher;
2714
+ }
2715
+ currentMode = null;
2716
+ currentHash = null;
2717
+ parked = null;
2718
+ /** The mode this launch is running, or null before the first message. */
2719
+ get mode() {
2720
+ return this.currentMode;
2721
+ }
2722
+ /** Hash of {@link mode}; null means "not armed yet — accept anything". */
2723
+ get hash() {
2724
+ return this.currentHash;
2725
+ }
2726
+ /** Whether a message has been adopted into this launch yet. */
2727
+ get armed() {
2728
+ return this.currentHash !== null;
2729
+ }
2730
+ /** Bind the launch to `mode`. Idempotent; safe to call with the same mode. */
2731
+ adopt(mode) {
2732
+ this.currentMode = mode;
2733
+ this.currentHash = this.hasher(mode);
2734
+ }
2735
+ /**
2736
+ * Re-adopt the current mode with one field replaced. No-op before the first
2737
+ * adopt: there is nothing to derive from, and arming here would make the
2738
+ * first real message look like a mode change.
2739
+ */
2740
+ amend(patch) {
2741
+ if (this.currentMode === null) return null;
2742
+ this.adopt({ ...this.currentMode, ...patch });
2743
+ return this.currentMode;
2744
+ }
2745
+ /**
2746
+ * True when `incomingHash` cannot run on this launch's Query and must be
2747
+ * parked for a fresh one. An un-armed gate accepts anything — that is how a
2748
+ * new launch takes its first message, including a replayed parked one.
2749
+ */
2750
+ requiresRelaunch(incomingHash, isolate) {
2751
+ if (isolate) return true;
2752
+ return this.armed && incomingHash !== this.currentHash;
2753
+ }
2754
+ /** True when `mode` is exactly what this launch adopted (Steer's precondition). */
2755
+ matches(mode) {
2756
+ return this.armed && this.hasher(mode) === this.currentHash;
2757
+ }
2758
+ /**
2759
+ * Hold a message that cannot run on this launch until its successor picks
2760
+ * it up. Survives {@link reset} — the parked message is precisely what the
2761
+ * next launch exists to run.
2762
+ */
2763
+ park(message) {
2764
+ this.parked = message;
2765
+ }
2766
+ /** Whether a parked message is waiting for the next launch. */
2767
+ get hasParked() {
2768
+ return this.parked !== null;
2769
+ }
2770
+ /**
2771
+ * Take the parked message AND adopt its mode in one step.
2772
+ *
2773
+ * This pairing is the whole point of the class. When parking and adopting
2774
+ * were separate — a `pending` variable in the launcher next to a `modeHash`
2775
+ * one — the replay path took the message and forgot to adopt, so the gate
2776
+ * stayed un-armed and swallowed the NEXT mode change into a Query built for
2777
+ * the previous one. Nothing structural tied the two, and no unit test could
2778
+ * reach the wiring. Here they cannot come apart.
2779
+ */
2780
+ takeParked() {
2781
+ const message = this.parked;
2782
+ if (!message) return null;
2783
+ this.parked = null;
2784
+ this.adopt(message.mode);
2785
+ return message;
2786
+ }
2787
+ /**
2788
+ * Re-adopt the parked message with a patched mode. Used when an explicit
2789
+ * mode switch lands while a message is parked: the switch is newer than the
2790
+ * snapshot the message carried when it was queued.
2791
+ */
2792
+ amendParked(patch) {
2793
+ if (!this.parked) return;
2794
+ this.parked = { ...this.parked, mode: { ...this.parked.mode, ...patch } };
2795
+ }
2796
+ /**
2797
+ * Drop back to un-armed. Called when a launch ends. Deliberately does NOT
2798
+ * clear a parked message: parking happens in the launch that is ending, and
2799
+ * the message belongs to the one about to start.
2800
+ */
2801
+ reset() {
2802
+ this.currentMode = null;
2803
+ this.currentHash = null;
2804
+ }
2805
+ }
2806
+
2807
+ const ABORT_INTERRUPT_GRACE_MS = 4e3;
2808
+ async function claudeRemoteLauncher(session, onPermissionModeChange, onEffectivePermissionMode, onPromptFinalized) {
2809
+ logger.logger.debug("[claudeRemoteLauncher] Starting remote launcher");
2810
+ const hasTTY = process.stdout.isTTY && process.stdin.isTTY;
2811
+ logger.logger.debug(`[claudeRemoteLauncher] TTY available: ${hasTTY}`);
2812
+ let messageBuffer$1 = new messageBuffer.MessageBuffer();
2813
+ let inkInstance = null;
2814
+ if (hasTTY) {
2815
+ const [{ render }, { default: React }, { RemoteModeDisplay }] = await Promise.all([
2816
+ import('ink'),
2817
+ import('react'),
2818
+ Promise.resolve().then(function () { return require('./RemoteModeDisplay-NC1gPHtj.cjs'); })
2819
+ ]);
2820
+ console.clear();
2821
+ inkInstance = render(React.createElement(RemoteModeDisplay, {
2822
+ messageBuffer: messageBuffer$1,
2823
+ logPath: process.env.DEBUG ? session.logPath : void 0,
2824
+ onExit: async () => {
2825
+ logger.logger.debug("[remote]: Exiting client via Ctrl-C");
2826
+ if (!exitReason) {
2827
+ exitReason = "exit";
2828
+ }
2829
+ await abort();
2830
+ },
2831
+ onSwitchToLocal: () => {
2832
+ logger.logger.debug("[remote]: Switching to local mode via double space");
2833
+ doSwitch();
2834
+ }
2835
+ }), {
2836
+ exitOnCtrlC: false,
2837
+ patchConsole: false
2838
+ });
2839
+ }
2840
+ if (hasTTY) {
2841
+ process.stdin.resume();
2842
+ if (process.stdin.isTTY) {
2843
+ process.stdin.setRawMode(true);
2844
+ }
2845
+ process.stdin.setEncoding("utf8");
2846
+ }
2847
+ let exitReason = null;
2848
+ let abortController = null;
2849
+ let abortFuture = null;
2850
+ const turnSteering = createTurnSteeringController();
2851
+ const runtimeControls = createRuntimeControls(() => session.thinking);
2852
+ session.client.rpcHandlerManager.registerHandler("claude-runtime-control", async (input) => runtimeControls.request(input));
2853
+ const backgroundTasks = backgroundTaskActivity.createBackgroundTaskTracker();
2854
+ async function abort() {
2855
+ if (abortController && !abortController.signal.aborted) {
2856
+ abortController.abort();
2857
+ }
2858
+ await abortFuture?.promise;
2859
+ }
2860
+ async function doAbort() {
2861
+ logger.logger.debug("[remote]: doAbort");
2862
+ session.markAborting();
2863
+ session.onAbort();
2864
+ void runtimeControls.stopRunningTasks().then((ids) => {
2865
+ if (ids.length > 0) logger.logger.debug(`[remote]: stopped ${ids.length} background task(s) on abort: ${ids.join(", ")}`);
2866
+ }).catch((error) => logger.logger.debug("[remote]: stopping background tasks on abort failed", error));
2867
+ if (session.thinking) {
2868
+ const interrupted = await turnSteering.interruptTurn().catch((e) => {
2869
+ logger.logger.debug("[remote]: interruptTurn threw, falling back to hard abort", e);
2870
+ return false;
2871
+ });
2872
+ if (interrupted && await waitForThinkingToStop(ABORT_INTERRUPT_GRACE_MS)) {
2873
+ logger.logger.debug("[remote]: interrupt stopped the turn gracefully; query kept alive");
2874
+ return;
2875
+ }
2876
+ logger.logger.debug("[remote]: interrupt did not stop the turn in time; escalating to hard abort");
2877
+ turnSteering.consumeInterrupted();
2878
+ }
2879
+ await abort();
2880
+ }
2881
+ async function waitForThinkingToStop(graceMs) {
2882
+ const deadline = Date.now() + graceMs;
2883
+ while (Date.now() < deadline) {
2884
+ if (!session.thinking) return true;
2885
+ await new Promise((resolve) => setTimeout(resolve, 100));
2886
+ }
2887
+ return !session.thinking;
2888
+ }
2889
+ async function doSwitch() {
2890
+ logger.logger.debug("[remote]: doSwitch");
2891
+ if (!exitReason) {
2892
+ exitReason = "switch";
2893
+ }
2894
+ await abort();
2895
+ }
2896
+ session.client.rpcHandlerManager.registerHandler("abort", doAbort);
2897
+ session.client.rpcHandlerManager.registerHandler("steer", async () => {
2898
+ await turnSteering.steer();
2899
+ });
2900
+ session.client.rpcHandlerManager.registerHandler("switch", doSwitch);
2901
+ let livePermissionModeHandler = null;
2902
+ let idlePermissionModeHandler = null;
2903
+ session.client.rpcHandlerManager.registerHandler(
2904
+ "set-permission-mode",
2905
+ async (request) => {
2906
+ const requestedMode = permissionMode.parseClaudePermissionMode(request?.mode);
2907
+ if (!requestedMode) throw new Error("Invalid Claude permission mode");
2908
+ if (livePermissionModeHandler) return { mode: await livePermissionModeHandler(requestedMode) };
2909
+ if (idlePermissionModeHandler) return { mode: idlePermissionModeHandler(requestedMode) };
2910
+ throw new Error("No active Claude query");
2911
+ }
2912
+ );
2913
+ const permissionHandler = new PermissionHandler(session);
2914
+ idlePermissionModeHandler = (nextMode) => {
2915
+ permissionHandler.handleModeChange(nextMode);
2916
+ onPermissionModeChange?.(nextMode);
2917
+ return nextMode;
2918
+ };
2919
+ permissionHandler.reset("Previous CLI process exited before responding");
2920
+ const streamingEnabled = process.env.HAPPY_SESSION_STREAM_DISABLED !== "1";
2921
+ const streamRelay$1 = new streamRelay.StreamRelay({
2922
+ send: (frame) => session.client.sendStreamFrame(frame)
2923
+ });
2924
+ const messageQueue = new OutgoingMessageQueue(
2925
+ (logMessage) => session.client.sendClaudeSessionMessage(logMessage)
2926
+ );
2927
+ permissionHandler.setOnPermissionRequest((toolCallId) => {
2928
+ messageQueue.releaseToolCall(toolCallId);
2929
+ });
2930
+ const sdkToLogConverter = new SDKToLogConverter({
2931
+ sessionId: session.sessionId || "unknown",
2932
+ cwd: session.path,
2933
+ version: process.env.npm_package_version
2934
+ }, permissionHandler.getResponses());
2935
+ let ongoingToolCalls = /* @__PURE__ */ new Map();
2936
+ let notifiedQuestionToolCalls = /* @__PURE__ */ new Set();
2937
+ function onMessage(message) {
2938
+ runtimeControls.observe(message);
2939
+ backgroundTasks.observe(message);
2940
+ session.setBackgroundTasks(backgroundTasks.list());
2941
+ if (isRuntimeCheckpointReplay(message)) return;
2942
+ if (message.type === "user" && isClaudeInterruptSentinelContent(message.message.content)) {
2943
+ sdkToLogConverter.convert(message);
2944
+ return;
2945
+ }
2946
+ formatClaudeMessageForInk(message, messageBuffer$1);
2947
+ if (message.type === "assistant") {
2948
+ let umessage = message;
2949
+ if (umessage.message.content && Array.isArray(umessage.message.content)) {
2950
+ for (let c of umessage.message.content) {
2951
+ if (c.type === "tool_use") {
2952
+ logger.logger.debug("[remote]: detected tool use " + c.id + " parent: " + umessage.parent_tool_use_id);
2953
+ ongoingToolCalls.set(c.id, { parentToolCallId: umessage.parent_tool_use_id ?? null });
2954
+ }
2955
+ if (c.type === "text" && (umessage.parent_tool_use_id ?? null) === null) {
2956
+ session.noteAssistantOutput(typeof c.text === "string" ? c.text : void 0);
2957
+ }
2958
+ }
2959
+ }
2960
+ }
2961
+ for (const toolCallId of getAskUserQuestionToolCallIds(message)) {
2962
+ if (notifiedQuestionToolCalls.has(toolCallId)) {
2963
+ continue;
2964
+ }
2965
+ notifiedQuestionToolCalls.add(toolCallId);
2966
+ session.api.push().sendSessionNotification({
2967
+ kind: "question",
2968
+ metadata: session.client.getMetadata(),
2969
+ data: {
2970
+ sessionId: session.client.sessionId,
2971
+ tool: "AskUserQuestion",
2972
+ toolCallId,
2973
+ type: "question_request",
2974
+ provider: "claude"
2975
+ }
2976
+ });
2977
+ }
2978
+ if (message.type === "user") {
2979
+ let umessage = message;
2980
+ if (umessage.message.content && Array.isArray(umessage.message.content)) {
2981
+ for (let c of umessage.message.content) {
2982
+ if (c.type === "tool_result" && c.tool_use_id) {
2983
+ ongoingToolCalls.delete(c.tool_use_id);
2984
+ messageQueue.releaseToolCall(c.tool_use_id);
2985
+ }
2986
+ }
2987
+ }
2988
+ }
2989
+ const logMessage = sdkToLogConverter.convert(message);
2990
+ if (logMessage) {
2991
+ if (logMessage.type === "user" && logMessage.message?.content) {
2992
+ const content = Array.isArray(logMessage.message.content) ? logMessage.message.content : [];
2993
+ for (let i = 0; i < content.length; i++) {
2994
+ const c = content[i];
2995
+ if (c.type === "tool_result" && c.tool_use_id) {
2996
+ const responses = permissionHandler.getResponses();
2997
+ const response = responses.get(c.tool_use_id);
2998
+ if (response) {
2999
+ const permissions = {
3000
+ date: response.receivedAt || Date.now(),
3001
+ result: response.approved ? "approved" : "denied"
3002
+ };
3003
+ if (response.mode) {
3004
+ permissions.mode = response.mode;
3005
+ }
3006
+ if (response.allowTools && response.allowTools.length > 0) {
3007
+ permissions.allowedTools = response.allowTools;
3008
+ }
3009
+ content[i] = {
3010
+ ...c,
3011
+ permissions
3012
+ };
3013
+ }
3014
+ }
3015
+ }
3016
+ }
3017
+ if (logMessage.type === "assistant" && message.type === "assistant") {
3018
+ const assistantMsg = message;
3019
+ const toolCallIds = [];
3020
+ if (assistantMsg.message.content && Array.isArray(assistantMsg.message.content)) {
3021
+ for (const block of assistantMsg.message.content) {
3022
+ if (block.type === "tool_use" && block.id) {
3023
+ toolCallIds.push(block.id);
3024
+ }
3025
+ }
3026
+ }
3027
+ if (toolCallIds.length > 0) {
3028
+ const isSidechain = assistantMsg.parent_tool_use_id !== void 0;
3029
+ if (!isSidechain) {
3030
+ messageQueue.enqueue(logMessage, {
3031
+ delay: 250,
3032
+ toolCallIds
3033
+ });
3034
+ return;
3035
+ }
3036
+ }
3037
+ }
3038
+ messageQueue.enqueue(logMessage);
3039
+ }
3040
+ if (message.type === "assistant") {
3041
+ let umessage = message;
3042
+ if (umessage.message.content && Array.isArray(umessage.message.content)) {
3043
+ for (let c of umessage.message.content) {
3044
+ if (c.type === "tool_use" && c.name === "Task" && c.input && typeof c.input.prompt === "string") {
3045
+ const logMessage2 = sdkToLogConverter.convertSidechainUserMessage(c.id, c.input.prompt);
3046
+ if (logMessage2) {
3047
+ messageQueue.enqueue(logMessage2);
3048
+ }
3049
+ }
3050
+ }
3051
+ }
3052
+ }
3053
+ }
3054
+ try {
3055
+ const modeGate = new LaunchModeGate(session.queue.modeHasher);
3056
+ let previousSessionId = null;
3057
+ while (!exitReason) {
3058
+ logger.logger.debug("[remote]: launch");
3059
+ messageBuffer$1.addMessage("\u2550".repeat(40), "status");
3060
+ const isNewSession = session.sessionId !== previousSessionId;
3061
+ if (isNewSession) {
3062
+ messageBuffer$1.addMessage("Starting new Claude session...", "status");
3063
+ permissionHandler.reset();
3064
+ sdkToLogConverter.resetParentChain();
3065
+ logger.logger.debug(`[remote]: New session detected (previous: ${previousSessionId}, current: ${session.sessionId})`);
3066
+ } else {
3067
+ messageBuffer$1.addMessage("Continuing Claude session...", "status");
3068
+ logger.logger.debug(`[remote]: Continuing existing session: ${session.sessionId}`);
3069
+ }
3070
+ previousSessionId = session.sessionId;
3071
+ const controller = new AbortController();
3072
+ abortController = controller;
3073
+ abortFuture = new sessionScanner.Future();
3074
+ const commitPermissionMode = (nextMode) => {
3075
+ permissionHandler.handleModeChange(nextMode);
3076
+ modeGate.amend({ permissionMode: nextMode });
3077
+ rewriteQueuedPermissionMode(session.queue.queue, session.queue.modeHasher, nextMode);
3078
+ modeGate.amendParked({ permissionMode: nextMode });
3079
+ onPermissionModeChange?.(nextMode);
3080
+ return nextMode;
3081
+ };
3082
+ idlePermissionModeHandler = commitPermissionMode;
3083
+ permissionHandler.setOnModeChanged((nextMode) => {
3084
+ commitPermissionMode(nextMode);
3085
+ });
3086
+ let launchPrewarm = null;
3087
+ try {
3088
+ const attachmentDirectory = attachmentContent.chatAttachmentDirectory(configuration.configuration.happyHomeDir, session.client.sessionId);
3089
+ await configuration.ensurePrivateDirectory(attachmentDirectory);
3090
+ launchPrewarm = session.takeClaudePrewarm();
3091
+ const remoteResult = await claudeRemote({
3092
+ prewarm: launchPrewarm ?? void 0,
3093
+ sessionId: session.sessionId,
3094
+ path: session.path,
3095
+ additionalDirectories: [attachmentDirectory],
3096
+ allowedTools: session.allowedTools ?? [],
3097
+ mcpServers: session.mcpServers,
3098
+ hookSettingsPath: session.hookSettingsPath,
3099
+ jsRuntime: session.jsRuntime,
3100
+ canCallTool: permissionHandler.handleToolCall,
3101
+ onElicitation: permissionHandler.handleElicitation,
3102
+ onUserDialog: permissionHandler.handleUserDialog,
3103
+ isAborted: (toolCallId) => {
3104
+ return permissionHandler.isAborted(toolCallId);
3105
+ },
3106
+ nextMessage: async () => {
3107
+ const parked = modeGate.takeParked();
3108
+ if (parked) {
3109
+ permissionHandler.handleModeChange(parked.mode.permissionMode);
3110
+ if (typeof parked.message === "string") onPromptFinalized?.(parked.message);
3111
+ return parked;
3112
+ }
3113
+ let msg = await session.queue.waitForMessagesAndGetAsString(controller.signal);
3114
+ if (msg) {
3115
+ if (modeGate.requiresRelaunch(msg.hash, msg.isolate)) {
3116
+ logger.logger.debug("[remote]: mode has changed, pending message");
3117
+ const parkedAttachments = msg.attachments ?? [];
3118
+ const parkedStaged = parkedAttachments.length > 0 ? await attachmentContent.stageClaudeAttachments(parkedAttachments, {
3119
+ happyHomeDir: configuration.configuration.happyHomeDir,
3120
+ sessionId: session.client.sessionId
3121
+ }) : [];
3122
+ modeGate.park({
3123
+ message: parkedStaged.length > 0 ? attachmentContent.appendStagedAttachmentsToPrompt(msg.message, parkedStaged) : msg.message,
3124
+ mode: msg.mode
3125
+ });
3126
+ return null;
3127
+ }
3128
+ modeGate.adopt(msg.mode);
3129
+ permissionHandler.handleModeChange(msg.mode.permissionMode);
3130
+ const attachments = msg.attachments ?? [];
3131
+ if (attachments.length > 0) {
3132
+ const staged = await attachmentContent.stageClaudeAttachments(attachments, {
3133
+ happyHomeDir: configuration.configuration.happyHomeDir,
3134
+ sessionId: session.client.sessionId
3135
+ });
3136
+ logger.logger.debug(`[remote] Staged ${staged.length} attachment(s) for the coding agent`);
3137
+ const withAttachments = attachmentContent.appendStagedAttachmentsToPrompt(msg.message, staged);
3138
+ onPromptFinalized?.(withAttachments);
3139
+ return {
3140
+ message: withAttachments,
3141
+ mode: msg.mode,
3142
+ enqueuedAt: msg.enqueuedAt
3143
+ };
3144
+ }
3145
+ onPromptFinalized?.(msg.message);
3146
+ return {
3147
+ message: msg.message,
3148
+ mode: msg.mode,
3149
+ enqueuedAt: msg.enqueuedAt
3150
+ };
3151
+ }
3152
+ return null;
3153
+ },
3154
+ onSessionFound: (sessionId) => {
3155
+ sdkToLogConverter.updateSessionId(sessionId);
3156
+ session.onSessionFound(sessionId);
3157
+ },
3158
+ onSDKMetadata: (metadata) => {
3159
+ logger.logger.debug("[remote] SDK metadata received, updating session:", {
3160
+ toolCount: metadata.tools?.length ?? 0,
3161
+ slashCommandCount: metadata.slashCommands?.length ?? 0,
3162
+ mcpServerCount: metadata.mcpServers?.length ?? 0,
3163
+ skillCount: metadata.skills?.length ?? 0
3164
+ });
3165
+ session.client.updateMetadata((currentMetadata) => applyClaudeSdkMetadata(currentMetadata, metadata));
3166
+ if (metadata.permissionMode) onEffectivePermissionMode?.(metadata.permissionMode);
3167
+ },
3168
+ onQueryReady: (q) => {
3169
+ runtimeControls.setQuery(q);
3170
+ backgroundTasks.reset();
3171
+ session.setBackgroundTasks([]);
3172
+ turnSteering.setInterrupt(q.interrupt);
3173
+ session.setSideQuestionLive({ ask: q.sideQuestion, canControl: q.canControl });
3174
+ session.setSteerHandler(async (input) => {
3175
+ if (!modeGate.matches(input.mode)) return false;
3176
+ let message = input.message;
3177
+ if (input.attachments?.length) {
3178
+ const staged = await attachmentContent.stageClaudeAttachments(input.attachments, {
3179
+ happyHomeDir: configuration.configuration.happyHomeDir,
3180
+ sessionId: session.client.sessionId
3181
+ });
3182
+ message = attachmentContent.appendStagedAttachmentsToPrompt(input.message, staged);
3183
+ }
3184
+ if (!session.thinking || !modeGate.matches(input.mode)) return false;
3185
+ if (typeof message === "string") onPromptFinalized?.(message);
3186
+ q.steer(message, input.mode);
3187
+ return true;
3188
+ });
3189
+ permissionHandler.setPermissionModeUpdater(async (mode) => {
3190
+ await q.setPermissionMode(mode);
3191
+ });
3192
+ livePermissionModeHandler = async (nextMode) => {
3193
+ await permissionHandler.setLivePermissionMode(nextMode);
3194
+ modeGate.amend({ permissionMode: nextMode });
3195
+ onPermissionModeChange?.(nextMode);
3196
+ return nextMode;
3197
+ };
3198
+ },
3199
+ onThinkingChange: session.onThinkingChange,
3200
+ onStreamFrame: streamingEnabled ? (frame) => streamRelay$1.ingest(frame) : void 0,
3201
+ claudeEnvVars: session.claudeEnvVars,
3202
+ claudeArgs: session.claudeArgs,
3203
+ onMessage,
3204
+ onCompletionEvent: (message) => {
3205
+ logger.logger.debug("[remote]: Completion event received:", contentLogMetadata.contentLogMetadata(message));
3206
+ session.client.sendSessionEvent({ type: "message", message });
3207
+ },
3208
+ onAuthFailure: (reason) => {
3209
+ session.client.sendSessionEvent({ type: "message", message: `Claude Code auth: ${reason}`, kind: "claude-auth-failed" });
3210
+ void controlClient.notifyDaemonClaudeAuthFailed(session.client.sessionId).catch((error) => logger.logger.debug("[remote]: auth_failed report to daemon failed:", error));
3211
+ },
3212
+ onSessionReset: () => {
3213
+ logger.logger.debug("[remote]: Session reset");
3214
+ session.clearSessionId();
3215
+ },
3216
+ onReady: (result) => {
3217
+ if (turnSteering.consumeReady()) {
3218
+ session.client.closeClaudeSessionTurn("cancelled");
3219
+ return;
3220
+ }
3221
+ if (turnSteering.consumeInterrupted()) {
3222
+ session.client.closeClaudeSessionTurn("cancelled");
3223
+ session.client.sendSessionEvent({ type: "message", message: "Aborted by user" });
3224
+ return;
3225
+ }
3226
+ applyClaudeResultLifecycle(result, {
3227
+ closeCompleted: () => session.client.closeClaudeSessionTurn("completed"),
3228
+ closeFailed: (error) => session.client.closeClaudeSessionTurn("failed", { error }),
3229
+ onFailed: (error) => session.onSessionError(error),
3230
+ onCompleted: () => {
3231
+ const idle = !modeGate.hasParked && session.queue.size() === 0 && !session.client.promptQueueHasPending();
3232
+ session.onTurnEnd(idle);
3233
+ if (idle) {
3234
+ session.api.push().sendSessionNotification({
3235
+ kind: "done",
3236
+ metadata: session.client.getMetadata(),
3237
+ data: {
3238
+ sessionId: session.client.sessionId,
3239
+ type: "ready",
3240
+ provider: "claude"
3241
+ }
3242
+ });
3243
+ }
3244
+ }
3245
+ });
3246
+ },
3247
+ signal: abortController.signal
3248
+ });
3249
+ session.consumeOneTimeFlags();
3250
+ if (!exitReason && abortController.signal.aborted) {
3251
+ session.client.closeClaudeSessionTurn("cancelled");
3252
+ session.client.sendSessionEvent({ type: "message", message: "Aborted by user" });
3253
+ }
3254
+ } catch (e) {
3255
+ logger.logger.debug("[remote]: launch error", e);
3256
+ if (!exitReason) {
3257
+ if (isClaudeEdeOnlySdkError(e)) {
3258
+ session.client.closeClaudeSessionTurn("cancelled");
3259
+ continue;
3260
+ }
3261
+ session.client.closeClaudeSessionTurn("failed");
3262
+ session.client.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
3263
+ session.onSessionError(e instanceof Error ? e.message : "Process exited unexpectedly");
3264
+ continue;
3265
+ }
3266
+ } finally {
3267
+ launchPrewarm?.discard("launch-ended");
3268
+ runtimeControls.setQuery(null);
3269
+ backgroundTasks.reset();
3270
+ session.setBackgroundTasks([]);
3271
+ turnSteering.reset();
3272
+ session.setSteerHandler(null);
3273
+ session.setSideQuestionLive(null);
3274
+ livePermissionModeHandler = null;
3275
+ permissionHandler.setPermissionModeUpdater(void 0);
3276
+ sdkToLogConverter.resetTransientState();
3277
+ logger.logger.debug("[remote]: launch finally");
3278
+ for (let [toolCallId, { parentToolCallId }] of ongoingToolCalls) {
3279
+ const converted = sdkToLogConverter.generateInterruptedToolResult(toolCallId, parentToolCallId);
3280
+ if (converted) {
3281
+ logger.logger.debug("[remote]: terminating tool call " + toolCallId + " parent: " + parentToolCallId);
3282
+ session.client.sendClaudeSessionMessage(converted);
3283
+ }
3284
+ }
3285
+ ongoingToolCalls.clear();
3286
+ logger.logger.debug("[remote]: flushing message queue");
3287
+ await messageQueue.flush();
3288
+ messageQueue.destroy();
3289
+ logger.logger.debug("[remote]: message queue flushed");
3290
+ streamRelay$1.endTurn();
3291
+ streamRelay$1.dispose();
3292
+ abortController = null;
3293
+ abortFuture?.resolve(void 0);
3294
+ abortFuture = null;
3295
+ logger.logger.debug("[remote]: launch done");
3296
+ permissionHandler.reset();
3297
+ modeGate.reset();
3298
+ }
3299
+ }
3300
+ } finally {
3301
+ permissionHandler.reset();
3302
+ const t0 = Date.now();
3303
+ logger.logger.debug(`[remote]: cleanup begin exitReason=${exitReason} hasInk=${!!inkInstance} rawMode=${process.stdin.isRaw}`);
3304
+ if (inkInstance) {
3305
+ inkInstance.unmount();
3306
+ }
3307
+ logger.logger.debug(`[remote]: ink.unmount() done +${Date.now() - t0}ms rawMode=${process.stdin.isRaw}`);
3308
+ await cleanupStdinAfterInk({
3309
+ stdin: process.stdin,
3310
+ drainMs: 150,
3311
+ onDebug: (event) => {
3312
+ logger.logger.debug(`[remote]: stdin drain ${event.bytes}B / ${event.chunks} chunk(s) +${Date.now() - t0}ms`);
3313
+ }
3314
+ });
3315
+ logger.logger.debug(`[remote]: cleanup done +${Date.now() - t0}ms rawMode=${process.stdin.isRaw}`);
3316
+ messageBuffer$1.clear();
3317
+ if (abortFuture) {
3318
+ abortFuture.resolve(void 0);
3319
+ }
3320
+ }
3321
+ return exitReason || "exit";
3322
+ }
3323
+
3324
+ async function loop(opts) {
3325
+ const logPath = logger.logger.logFilePath;
3326
+ let session = new Session({
3327
+ api: opts.api,
3328
+ client: opts.session,
3329
+ path: opts.path,
3330
+ sessionId: null,
3331
+ claudeEnvVars: opts.claudeEnvVars,
3332
+ claudeArgs: opts.claudeArgs,
3333
+ mcpServers: opts.mcpServers,
3334
+ logPath,
3335
+ messageQueue: opts.messageQueue,
3336
+ allowedTools: opts.allowedTools,
3337
+ sandboxConfig: opts.sandboxConfig,
3338
+ onModeChange: opts.onModeChange,
3339
+ onAbort: opts.onAbort,
3340
+ hookSettingsPath: opts.hookSettingsPath,
3341
+ jsRuntime: opts.jsRuntime,
3342
+ claudePrewarm: opts.claudePrewarm
3343
+ });
3344
+ opts.onSessionReady?.(session);
3345
+ let mode = opts.startingMode ?? "local";
3346
+ while (true) {
3347
+ logger.logger.debug(`[loop] Iteration with mode: ${mode}`);
3348
+ switch (mode) {
3349
+ case "local": {
3350
+ const { claudeLocalLauncher } = await Promise.resolve().then(function () { return require('./claudeLocalLauncher-Bd5I2-j6.cjs'); });
3351
+ const result = await claudeLocalLauncher(session);
3352
+ switch (result.type) {
3353
+ case "switch":
3354
+ mode = "remote";
3355
+ opts.onModeChange?.(mode);
3356
+ break;
3357
+ case "exit":
3358
+ return result.code;
3359
+ }
3360
+ break;
3361
+ }
3362
+ case "remote": {
3363
+ const reason = await claudeRemoteLauncher(session, opts.onPermissionModeChange, opts.onEffectivePermissionMode, opts.onPromptFinalized);
3364
+ switch (reason) {
3365
+ case "exit":
3366
+ return 0;
3367
+ case "switch":
3368
+ mode = "local";
3369
+ opts.onModeChange?.(mode);
3370
+ break;
3371
+ }
3372
+ break;
3373
+ }
3374
+ }
3375
+ }
3376
+ }
3377
+
3378
+ function parseSessionHookTarget(url) {
3379
+ if (!url) return null;
3380
+ let parsed;
3381
+ try {
3382
+ parsed = new URL(url, "http://127.0.0.1");
3383
+ } catch {
3384
+ return null;
3385
+ }
3386
+ if (parsed.pathname !== "/hook/session-start") return null;
3387
+ const source = parsed.searchParams.get("source");
3388
+ return source && /^[A-Za-z0-9_.-]{1,64}$/.test(source) ? { source } : {};
3389
+ }
3390
+ function hookRequestLogMetadata(body, data) {
3391
+ return {
3392
+ bodyBytes: Buffer.byteLength(body, "utf8"),
3393
+ parsed: data !== void 0,
3394
+ hasSessionId: Boolean(data?.session_id || data?.sessionId)
3395
+ };
3396
+ }
3397
+ function hookErrorLogMetadata(error) {
3398
+ if (!error || typeof error !== "object") return { errorType: typeof error };
3399
+ const value = error;
3400
+ const errorType = typeof value.name === "string" && /^[a-z][a-z0-9_.-]{0,63}$/i.test(value.name) ? value.name : "Error";
3401
+ const safeCode = typeof value.code === "string" && /^[a-z0-9_.-]{1,64}$/i.test(value.code) ? value.code : typeof value.code === "number" ? value.code : void 0;
3402
+ return {
3403
+ errorType,
3404
+ ...safeCode === void 0 ? {} : { code: safeCode }
3405
+ };
3406
+ }
3407
+ async function startHookServer(options) {
3408
+ const { onSessionHook } = options;
3409
+ return new Promise((resolve, reject) => {
3410
+ const server = node_http.createServer(async (req, res) => {
3411
+ const hookMeta = req.method === "POST" ? parseSessionHookTarget(req.url) : null;
3412
+ if (hookMeta) {
3413
+ const timeout = setTimeout(() => {
3414
+ if (!res.headersSent) {
3415
+ logger.logger.debug("[hookServer] Request timeout");
3416
+ res.writeHead(408).end("timeout");
3417
+ }
3418
+ }, 5e3);
3419
+ try {
3420
+ const chunks = [];
3421
+ for await (const chunk of req) {
3422
+ chunks.push(chunk);
3423
+ }
3424
+ clearTimeout(timeout);
3425
+ const body = Buffer.concat(chunks).toString("utf-8");
3426
+ let data;
3427
+ try {
3428
+ data = JSON.parse(body);
3429
+ logger.logger.debug("[hookServer] Received session hook metadata", hookRequestLogMetadata(body, data));
3430
+ } catch {
3431
+ logger.logger.debug("[hookServer] Failed to parse session hook", hookRequestLogMetadata(body));
3432
+ }
3433
+ const sessionId = data?.session_id || data?.sessionId;
3434
+ if (sessionId && data) {
3435
+ logger.logger.debug(`[hookServer] Session hook received session ID: ${sessionId}`);
3436
+ onSessionHook(sessionId, data, hookMeta);
3437
+ } else {
3438
+ logger.logger.debug("[hookServer] Session hook received but no session_id found in data");
3439
+ }
3440
+ res.writeHead(200, { "Content-Type": "text/plain" }).end("ok");
3441
+ } catch (error) {
3442
+ clearTimeout(timeout);
3443
+ logger.logger.debug("[hookServer] Error handling session hook", hookErrorLogMetadata(error));
3444
+ if (!res.headersSent) {
3445
+ res.writeHead(500).end("error");
3446
+ }
3447
+ }
3448
+ return;
3449
+ }
3450
+ res.writeHead(404).end("not found");
3451
+ });
3452
+ server.listen(0, "127.0.0.1", () => {
3453
+ const address = server.address();
3454
+ if (!address || typeof address === "string") {
3455
+ reject(new Error("Failed to get server address"));
3456
+ return;
3457
+ }
3458
+ const port = address.port;
3459
+ logger.logger.debug(`[hookServer] Started on port ${port}`);
3460
+ resolve({
3461
+ port,
3462
+ stop: () => {
3463
+ server.close();
3464
+ logger.logger.debug("[hookServer] Stopped");
3465
+ }
3466
+ });
3467
+ });
3468
+ server.on("error", (err) => {
3469
+ logger.logger.debug("[hookServer] Server error", hookErrorLogMetadata(err));
3470
+ reject(err);
3471
+ });
3472
+ });
3473
+ }
3474
+
3475
+ function generateHookSettingsFile(port, source) {
3476
+ const hooksDir = path$1.join(configuration.configuration.happyHomeDir, "tmp", "hooks");
3477
+ node_fs.mkdirSync(hooksDir, { recursive: true });
3478
+ const filename = source ? `session-hook-${process.pid}-${source}.json` : `session-hook-${process.pid}.json`;
3479
+ const filepath = path$1.join(hooksDir, filename);
3480
+ const forwarderScript = path$1.resolve(projectPath.projectPath(), "scripts", "session_hook_forwarder.cjs");
3481
+ if (source !== void 0 && !/^[A-Za-z0-9_.-]{1,64}$/.test(source)) {
3482
+ throw new Error("invalid hook source tag");
3483
+ }
3484
+ const hookCommand = source ? `node "${forwarderScript}" ${port} ${source}` : `node "${forwarderScript}" ${port}`;
3485
+ const settings = {
3486
+ hooks: {
3487
+ SessionStart: [
3488
+ {
3489
+ matcher: "*",
3490
+ hooks: [
3491
+ {
3492
+ type: "command",
3493
+ command: hookCommand
3494
+ }
3495
+ ]
3496
+ }
3497
+ ]
3498
+ }
3499
+ };
3500
+ node_fs.writeFileSync(filepath, JSON.stringify(settings, null, 2));
3501
+ logger.logger.debug(`[generateHookSettings] Created hook settings file: ${filepath}`);
3502
+ return filepath;
3503
+ }
3504
+ function cleanupHookSettingsFile(filepath) {
3505
+ try {
3506
+ if (node_fs.existsSync(filepath)) {
3507
+ node_fs.unlinkSync(filepath);
3508
+ logger.logger.debug(`[generateHookSettings] Cleaned up hook settings file: ${filepath}`);
3509
+ }
3510
+ } catch (error) {
3511
+ logger.logger.debug(`[generateHookSettings] Failed to cleanup hook settings file: ${error}`);
3512
+ }
3513
+ }
3514
+
3515
+ async function settleSessionOnExit(sessionId, intent, deps) {
3516
+ if (intent === "archive") {
3517
+ deps.updateMetadata((currentMetadata) => ({
3518
+ ...currentMetadata,
3519
+ lifecycleState: "archived",
3520
+ lifecycleStateSince: Date.now(),
3521
+ archivedBy: "cli",
3522
+ archiveReason: "User terminated"
3523
+ }));
3524
+ }
3525
+ deps.sendSessionDeath();
3526
+ try {
3527
+ if (intent === "archive") await deps.archiveSession(sessionId);
3528
+ else await deps.deactivateSession(sessionId);
3529
+ } catch (error) {
3530
+ deps.log?.(`[START] ${intent === "archive" ? "archiveSession" : "deactivateSession"} during cleanup failed:`, error);
3531
+ }
3532
+ }
3533
+ class SessionExitGate {
3534
+ decided = null;
3535
+ /** Returns the intent to act on, or null when cleanup already started. */
3536
+ claim(intent) {
3537
+ if (this.decided !== null) return null;
3538
+ this.decided = intent;
3539
+ return intent;
3540
+ }
3541
+ get current() {
3542
+ return this.decided;
3543
+ }
3544
+ }
3545
+ function exitIntentFromArchiveFlag(archive) {
3546
+ return archive ?? true ? "archive" : "offline";
3547
+ }
3548
+
3549
+ function isUnsupportedSideQuestionError(error) {
3550
+ const message = error instanceof Error ? error.message : String(error);
3551
+ return /unknown|unsupported|not supported|unrecognized|invalid.*subtype/i.test(message);
3552
+ }
3553
+ async function runSideQuestionLive(ask, input) {
3554
+ const history = (input.history ?? []).filter((h) => h.question.trim() && h.answer.trim()).slice(-12).map((h) => ({ question: clip(h.question.trim(), MAX_HISTORY_CHARS), response: clip(h.answer.trim(), MAX_HISTORY_CHARS) }));
3555
+ const result = await ask({ question: input.question.trim(), ...history.length ? { history } : {} }, input.signal);
3556
+ if (input.signal?.aborted) throw new Error("Side question cancelled");
3557
+ const text = typeof result?.response === "string" ? result.response : "";
3558
+ if (!text.trim()) throw new Error("Side question ended without a result");
3559
+ const fallback = result.refusal_fallback?.content;
3560
+ const answer = fallback ? `\u26A0 ${fallback}
3561
+
3562
+ ${text}` : text;
3563
+ input.onText?.(answer);
3564
+ return { answer, hadContext: true, mode: "live" };
3565
+ }
3566
+ const SIDE_QUESTION_SYSTEM_PROMPT = [
3567
+ "A user message wrapped in a side-question system-reminder is a SIDE QUESTION: answer it",
3568
+ "directly in one response from the conversation context, with no tools and no action on the main task."
3569
+ ].join(" ");
3570
+ const SIDE_QUESTION_REMINDER = [
3571
+ "<system-reminder>This is a side question from the user. You must answer this question directly in a single response.",
3572
+ "",
3573
+ "IMPORTANT CONTEXT:",
3574
+ "- You are a separate, lightweight agent spawned to answer this one question",
3575
+ "- The main agent is NOT interrupted - it continues working independently in the background",
3576
+ "- You share the conversation context but are a completely separate instance",
3577
+ '- Do NOT reference being interrupted or what you were "previously doing" - that framing is incorrect',
3578
+ "",
3579
+ "CRITICAL CONSTRAINTS:",
3580
+ "- You have NO tools available - you cannot read files, run commands, search, or take any actions",
3581
+ "- This is a one-off response - there will be no follow-up turns",
3582
+ "- You can ONLY provide information based on what you already know from the conversation context",
3583
+ `- NEVER say things like "Let me try...", "I'll now...", "Let me check...", or promise to take any action`,
3584
+ "- If you don't know the answer, say so - do not offer to look it up or investigate",
3585
+ "",
3586
+ "Simply answer the question with the information you have.</system-reminder>"
3587
+ ].join("\n");
3588
+ const MAX_HISTORY_CHARS = 2e3;
3589
+ function clip(text, max) {
3590
+ return text.length > max ? `${text.slice(0, max)}\u2026` : text;
3591
+ }
3592
+ function buildSideQuestionPrompt(question, history = []) {
3593
+ const trimmed = question.trim();
3594
+ const prior = history.filter((h) => h.question.trim() && h.answer.trim()).slice(-12);
3595
+ const body = `${SIDE_QUESTION_REMINDER}
3596
+
3597
+ ${trimmed}`;
3598
+ if (prior.length === 0) return body;
3599
+ const lines = prior.map((h) => `Q: ${clip(h.question.trim(), MAX_HISTORY_CHARS)}
3600
+ A: ${clip(h.answer.trim(), MAX_HISTORY_CHARS)}`);
3601
+ return `<earlier-side-questions>
3602
+ ${lines.join("\n\n")}
3603
+ </earlier-side-questions>
3604
+
3605
+ ${body}`;
3606
+ }
3607
+ function sideQuestionQueryOptions(input) {
3608
+ return {
3609
+ cwd: input.cwd,
3610
+ ...input.resumeSessionId ? { resume: input.resumeSessionId, forkSession: true } : {},
3611
+ persistSession: false,
3612
+ tools: [],
3613
+ mcpServers: {},
3614
+ strictMcpConfig: true,
3615
+ maxTurns: 1,
3616
+ includePartialMessages: true,
3617
+ permissionMode: "default",
3618
+ model: input.model,
3619
+ env: input.env,
3620
+ settingsPath: input.settingsPath,
3621
+ appendSystemPrompt: SIDE_QUESTION_SYSTEM_PROMPT,
3622
+ canCallTool: async () => ({ behavior: "deny", message: "Side questions cannot use tools" }),
3623
+ abort: input.signal
3624
+ };
3625
+ }
3626
+ function assistantBlocks(message) {
3627
+ if (message.type !== "assistant") return [];
3628
+ const content = message.message?.content;
3629
+ return Array.isArray(content) ? content.filter((b) => Boolean(b) && typeof b === "object") : [];
3630
+ }
3631
+ function textFromAssistant(message) {
3632
+ return assistantBlocks(message).map((block) => block.type === "text" ? String(block.text ?? "") : "").join("");
3633
+ }
3634
+ function toolUseFromAssistant(message) {
3635
+ const block = assistantBlocks(message).find((b) => b.type === "tool_use");
3636
+ return block ? String(block.name ?? "a tool") : null;
3637
+ }
3638
+ function toolAttemptNotice(toolName) {
3639
+ return `(The model tried to call ${toolName} instead of answering directly. Try rephrasing or ask in the main conversation.)`;
3640
+ }
3641
+ function apiErrorFromSystem(message) {
3642
+ if (message.type !== "system") return null;
3643
+ const m = message;
3644
+ if (m.subtype !== "api_error") return null;
3645
+ if (typeof m.error === "string") return m.error;
3646
+ const detail = m.error?.formatted ?? m.error?.message;
3647
+ return typeof detail === "string" && detail ? detail : "API error";
3648
+ }
3649
+ async function runSideQuestion(query, input) {
3650
+ const options = sideQuestionQueryOptions(input);
3651
+ const stream = query({ prompt: buildSideQuestionPrompt(input.question, input.history), options });
3652
+ let streamed = "";
3653
+ let final = "";
3654
+ let toolAttempt = null;
3655
+ let lastApiError = null;
3656
+ let resultSeen = false;
3657
+ for await (const message of stream) {
3658
+ if (message.type === "stream_event") {
3659
+ const event = message.event;
3660
+ if (event?.type === "content_block_delta" && event.delta?.type === "text_delta" && event.delta.text) {
3661
+ streamed += event.delta.text;
3662
+ input.onText?.(streamed);
3663
+ }
3664
+ continue;
3665
+ }
3666
+ if (message.type === "assistant") {
3667
+ const text = textFromAssistant(message);
3668
+ if (text) {
3669
+ final = final ? `${final}
3670
+
3671
+ ${text}` : text;
3672
+ input.onText?.(final);
3673
+ }
3674
+ toolAttempt ??= toolUseFromAssistant(message);
3675
+ continue;
3676
+ }
3677
+ if (message.type === "system") {
3678
+ lastApiError = apiErrorFromSystem(message) ?? lastApiError;
3679
+ continue;
3680
+ }
3681
+ if (message.type === "result") {
3682
+ resultSeen = true;
3683
+ const result = message;
3684
+ if (result.subtype !== "success") {
3685
+ if (toolAttempt && result.subtype === "error_max_turns") break;
3686
+ const detail = Array.isArray(result.errors) && result.errors.length > 0 ? String(result.errors[0]) : typeof result.result === "string" && result.result ? result.result : lastApiError ?? result.subtype ?? "unknown";
3687
+ throw new Error(`Side question failed: ${detail}`);
3688
+ }
3689
+ if (!final && typeof result.result === "string") final = result.result;
3690
+ }
3691
+ }
3692
+ if (input.signal?.aborted) throw new Error("Side question cancelled");
3693
+ if (toolAttempt && !final.trim() && !streamed.trim()) {
3694
+ const notice = toolAttemptNotice(toolAttempt);
3695
+ input.onText?.(notice);
3696
+ return { answer: notice, hadContext: Boolean(input.resumeSessionId), mode: "fork" };
3697
+ }
3698
+ if (!resultSeen && !final && !streamed) throw new Error(`Side question ended without a result${lastApiError ? `: ${lastApiError}` : ""}`);
3699
+ return { answer: final || streamed, hadContext: Boolean(input.resumeSessionId), mode: "fork" };
3700
+ }
3701
+
3702
+ const SIDE_QUESTION_RETAIN_MS = 5 * 6e4;
3703
+ const SIDE_QUESTION_MAX_CHARS = 8e3;
3704
+ const SIDE_QUESTION_MAX_RUN_MS = 10 * 6e4;
3705
+ function writeSideQuestionSettingsFile(homeDir) {
3706
+ const dir = path$1.join(homeDir, "tmp", "hooks");
3707
+ node_fs.mkdirSync(dir, { recursive: true });
3708
+ const file = path$1.join(dir, `side-question-${process.pid}.json`);
3709
+ node_fs.writeFileSync(file, JSON.stringify({ disableAllHooks: true }));
3710
+ return file;
3711
+ }
3712
+ function parseHistory(raw) {
3713
+ if (!Array.isArray(raw)) return [];
3714
+ return raw.flatMap((item) => {
3715
+ if (!item || typeof item !== "object") return [];
3716
+ const { question, answer } = item;
3717
+ return typeof question === "string" && typeof answer === "string" ? [{ question, answer }] : [];
3718
+ });
3719
+ }
3720
+ function registerSideQuestionHandler(rpc, deps) {
3721
+ const now = deps.now ?? (() => Date.now());
3722
+ const retainMs = deps.retainMs ?? SIDE_QUESTION_RETAIN_MS;
3723
+ const maxRunMs = deps.maxRunMs ?? SIDE_QUESTION_MAX_RUN_MS;
3724
+ const fork = deps.run ?? (async (input) => {
3725
+ const { query } = await Promise.resolve().then(function () { return require('./index-D3kI0hDR.cjs'); });
3726
+ return runSideQuestion(query, input);
3727
+ });
3728
+ const run = async (input, live) => {
3729
+ if (!live) return fork(input);
3730
+ try {
3731
+ return await runSideQuestionLive(live.ask, input);
3732
+ } catch (error) {
3733
+ if (input.signal?.aborted || !isUnsupportedSideQuestionError(error)) throw error;
3734
+ logger.logger.debug(`[btw] live side question unsupported by this CLI, forking instead: ${error instanceof Error ? error.message : String(error)}`);
3735
+ return fork(input);
3736
+ }
3737
+ };
3738
+ const slots = /* @__PURE__ */ new Map();
3739
+ let active = null;
3740
+ const prune = () => {
3741
+ const t = now();
3742
+ for (const [id, slot] of slots) {
3743
+ if (slot.status !== "running" && slot.finishedAt !== void 0 && t - slot.finishedAt > retainMs) slots.delete(id);
3744
+ }
3745
+ };
3746
+ const finish = (slot, status, error) => {
3747
+ if (slot.status !== "running") return;
3748
+ slot.status = status;
3749
+ slot.error = error;
3750
+ slot.finishedAt = now();
3751
+ if (active === slot) active = null;
3752
+ };
3753
+ rpc.registerHandler("btw-ask", async (request) => {
3754
+ const question = typeof request?.question === "string" ? request.question.trim() : "";
3755
+ if (!question) throw new Error("Side question is empty");
3756
+ if (question.length > SIDE_QUESTION_MAX_CHARS) throw new Error("Side question is too long");
3757
+ prune();
3758
+ if (active) {
3759
+ const stale = active;
3760
+ stale.abort.abort("superseded");
3761
+ finish(stale, "cancelled");
3762
+ logger.logger.debug(`[btw] side question ${stale.requestId} superseded by a new ask`);
3763
+ }
3764
+ const candidate = deps.getLiveQuery?.() ?? null;
3765
+ const live = candidate && candidate.canControl() ? candidate : null;
3766
+ const resumeSessionId = deps.getClaudeSessionId() ?? null;
3767
+ const slot = {
3768
+ requestId: node_crypto.randomUUID(),
3769
+ status: "running",
3770
+ text: "",
3771
+ startedAt: now(),
3772
+ abort: new AbortController()
3773
+ };
3774
+ slots.set(slot.requestId, slot);
3775
+ active = slot;
3776
+ logger.logger.debug(`[btw] side question ${slot.requestId} start (${live ? "live" : resumeSessionId ? "fork" : "no-context"})`);
3777
+ const deadline = setTimeout(() => {
3778
+ if (slot.status !== "running") return;
3779
+ slot.abort.abort("timeout");
3780
+ finish(slot, "error", "Side question timed out");
3781
+ logger.logger.debug(`[btw] side question ${slot.requestId} timed out after ${maxRunMs}ms`);
3782
+ }, maxRunMs);
3783
+ deadline.unref?.();
3784
+ void run({
3785
+ question,
3786
+ history: parseHistory(request.history),
3787
+ resumeSessionId,
3788
+ cwd: deps.cwd,
3789
+ model: deps.getModel(),
3790
+ env: deps.getEnv?.(),
3791
+ settingsPath: deps.settingsPath,
3792
+ signal: slot.abort.signal,
3793
+ onText: (text) => {
3794
+ if (slot.status === "running") slot.text = text;
3795
+ }
3796
+ }, live).then((result) => {
3797
+ slot.text = result.answer;
3798
+ finish(slot, "done");
3799
+ logger.logger.debug(`[btw] side question ${slot.requestId} done (${result.mode ?? "fork"}, ${result.answer.length} chars)`);
3800
+ }).catch((error) => {
3801
+ const message = error instanceof Error ? error.message : String(error);
3802
+ if (slot.abort.signal.aborted) {
3803
+ finish(slot, "cancelled");
3804
+ } else {
3805
+ finish(slot, "error", message);
3806
+ logger.logger.debug(`[btw] side question ${slot.requestId} failed: ${message}`);
3807
+ }
3808
+ }).finally(() => clearTimeout(deadline));
3809
+ return { requestId: slot.requestId, hadContext: live ? true : Boolean(resumeSessionId), mode: live ? "live" : "fork" };
3810
+ });
3811
+ rpc.registerHandler("btw-poll", async (request) => {
3812
+ prune();
3813
+ const id = typeof request?.requestId === "string" ? request.requestId : "";
3814
+ const slot = slots.get(id);
3815
+ if (!slot) throw new Error("Unknown side question");
3816
+ return {
3817
+ requestId: slot.requestId,
3818
+ status: slot.status,
3819
+ text: slot.text,
3820
+ error: slot.error,
3821
+ startedAt: slot.startedAt,
3822
+ finishedAt: slot.finishedAt
3823
+ };
3824
+ });
3825
+ rpc.registerHandler("btw-cancel", async (request) => {
3826
+ const id = typeof request?.requestId === "string" ? request.requestId : "";
3827
+ const slot = slots.get(id);
3828
+ if (!slot || slot.status !== "running") return { cancelled: false };
3829
+ slot.abort.abort("cancelled by user");
3830
+ finish(slot, "cancelled");
3831
+ return { cancelled: true };
3832
+ });
3833
+ return {
3834
+ /** Test/introspection hook. */
3835
+ _slots: slots
3836
+ };
3837
+ }
3838
+
3839
+ const ANALYZE_MIN_INTERVAL_MS = 5 * 60 * 1e3;
3840
+ const HOURLY_LIMIT = 30;
3841
+ const HOURLY_WINDOW_MS = 60 * 60 * 1e3;
3842
+ const USER_MESSAGE_MAX_CHARS = 1e3;
3843
+ const ASSISTANT_TAIL_MAX_CHARS = 1e3;
3844
+ const PROGRESS_MAX_CHARS = 200;
3845
+ const MAX_TASKS_IN_PROMPT = 30;
3846
+ function computeInputHash(input) {
3847
+ const h = node_crypto.createHash("sha256");
3848
+ h.update(input.lastUserMessage);
3849
+ h.update("\0");
3850
+ h.update(input.assistantTail);
3851
+ h.update("\0");
3852
+ h.update(input.todos.join("\n"));
3853
+ h.update("\0");
3854
+ h.update(input.tasks.map((t) => t.id).join(","));
3855
+ return h.digest("hex");
3856
+ }
3857
+ function shouldAnalyze(state, hash, now) {
3858
+ if (state.lastHash !== null && state.lastHash === hash) return false;
3859
+ if (now - state.lastRunAt < ANALYZE_MIN_INTERVAL_MS) return false;
3860
+ return true;
3861
+ }
3862
+ function pruneRateWindow(timestamps, now, limit = HOURLY_LIMIT, windowMs = HOURLY_WINDOW_MS) {
3863
+ const window = timestamps.filter((t) => typeof t === "number" && t > now - windowMs && t <= now);
3864
+ return { window, allowed: window.length < limit };
3865
+ }
3866
+ function parseBoardAnalysis(raw, knownTaskIds) {
3867
+ let text = raw.trim();
3868
+ if (!text) return null;
3869
+ const fence = text.match(/```(?:json)?\s*([\s\S]*?)```/);
3870
+ if (fence) text = fence[1].trim();
3871
+ if (!text.startsWith("{")) {
3872
+ const start = text.indexOf("{");
3873
+ const end = text.lastIndexOf("}");
3874
+ if (start === -1 || end <= start) return null;
3875
+ text = text.slice(start, end + 1);
3876
+ }
3877
+ let parsed;
3878
+ try {
3879
+ parsed = JSON.parse(text);
3880
+ } catch {
3881
+ return null;
3882
+ }
3883
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
3884
+ const obj = parsed;
3885
+ const attention = obj.attention;
3886
+ if (attention !== "none" && attention !== "review" && attention !== "blocked") return null;
3887
+ if (typeof obj.progress !== "string") return null;
3888
+ const progress = obj.progress.trim().slice(0, PROGRESS_MAX_CHARS);
3889
+ if (!progress) return null;
3890
+ let taskId = null;
3891
+ if (typeof obj.taskId === "string" && knownTaskIds.has(obj.taskId)) {
3892
+ taskId = obj.taskId;
3893
+ }
3894
+ return { taskId, attention, progress };
3895
+ }
3896
+ function buildBoardPrompt(input) {
3897
+ const tasks = input.tasks.slice(0, MAX_TASKS_IN_PROMPT);
3898
+ const taskLines = tasks.length > 0 ? tasks.map((t) => `- ${t.id}: ${t.title}`).join("\n") : "(no tasks defined)";
3899
+ const todoBlock = input.todos.length > 0 ? input.todos.join("\n") : "(none)";
3900
+ return [
3901
+ `${oneShotPrompts.BOARD_ANALYZER_PROMPT_PREFIX} Analyze this session snapshot and reply with ONLY a strict JSON object, no markdown, no explanations:`,
3902
+ '{"taskId": string|null, "attention": "none"|"review"|"blocked", "progress": string}',
3903
+ "",
3904
+ "Rules:",
3905
+ `- taskId: the id of the ONE task below this session is working on, or null if none clearly matches. Never invent an id.`,
3906
+ '- attention: "blocked" if the agent is stuck/waiting on the user; "review" if it finished something that needs the user to check; otherwise "none".',
3907
+ "- progress: one short sentence in Chinese (\u7B80\u4F53\u4E2D\u6587) describing current progress. No quotes inside.",
3908
+ "",
3909
+ `Task list:
3910
+ ${taskLines}`,
3911
+ "",
3912
+ `Latest user message:
3913
+ ${input.lastUserMessage.slice(0, USER_MESSAGE_MAX_CHARS)}`,
3914
+ "",
3915
+ `Latest assistant output (tail):
3916
+ ${input.assistantTail.slice(-ASSISTANT_TAIL_MAX_CHARS)}`,
3917
+ "",
3918
+ `Current todos:
3919
+ ${todoBlock}`
3920
+ ].join("\n");
3921
+ }
3922
+ function extractFromClaudeMessage(body) {
3923
+ if (body.type !== "assistant") return {};
3924
+ const message = body.message;
3925
+ const content = message?.content;
3926
+ if (!Array.isArray(content)) return {};
3927
+ let assistantText;
3928
+ let todos;
3929
+ for (const block of content) {
3930
+ if (!block || typeof block !== "object") continue;
3931
+ const b = block;
3932
+ if (b.type === "text" && typeof b.text === "string" && b.text.trim()) {
3933
+ assistantText = b.text;
3934
+ } else if (b.type === "tool_use" && b.name === "TodoWrite" && Array.isArray(b.input?.todos)) {
3935
+ todos = b.input.todos.filter((t) => t && typeof t.content === "string").map((t) => `[${t.status === "completed" ? "x" : t.status === "in_progress" ? "~" : " "}] ${t.content}`);
3936
+ }
3937
+ }
3938
+ return { assistantText, todos };
3939
+ }
3940
+ class FileRateLimiter {
3941
+ constructor(filePath, limit = HOURLY_LIMIT, windowMs = HOURLY_WINDOW_MS) {
3942
+ this.filePath = filePath;
3943
+ this.limit = limit;
3944
+ this.windowMs = windowMs;
3945
+ }
3946
+ /** Try to record one run right now. False → over the hourly cap. */
3947
+ tryAcquire(now = Date.now()) {
3948
+ let timestamps = [];
3949
+ try {
3950
+ const raw = node_fs.readFileSync(this.filePath, "utf8");
3951
+ const parsed = JSON.parse(raw);
3952
+ if (Array.isArray(parsed.runs)) timestamps = parsed.runs;
3953
+ } catch {
3954
+ }
3955
+ const { window, allowed } = pruneRateWindow(timestamps, now, this.limit, this.windowMs);
3956
+ if (!allowed) return false;
3957
+ window.push(now);
3958
+ try {
3959
+ node_fs.mkdirSync(path$1.dirname(this.filePath), { recursive: true });
3960
+ node_fs.writeFileSync(this.filePath, JSON.stringify({ runs: window }));
3961
+ } catch (error) {
3962
+ logger.logger.debug("[boardAnalyzer] rate file write failed", { error: String(error) });
3963
+ }
3964
+ return true;
3965
+ }
3966
+ }
3967
+ class BoardAnalyzer {
3968
+ constructor(session, options) {
3969
+ this.session = session;
3970
+ this.options = options;
3971
+ }
3972
+ lastUserMessage = "";
3973
+ assistantTail = "";
3974
+ todos = [];
3975
+ throttle = { lastRunAt: 0, lastHash: null };
3976
+ running = false;
3977
+ sawUserMessage = false;
3978
+ /** Feed every observed user prompt (both the app channel and the JSONL
3979
+ * scanner — same call sites as titleGenerator.maybeGenerate). The FIRST
3980
+ * one also triggers an immediate analysis attempt. */
3981
+ noteUserMessage(text) {
3982
+ if (!this.options.enabled) return;
3983
+ if (!text || text.trim().length === 0) return;
3984
+ this.lastUserMessage = text.slice(0, USER_MESSAGE_MAX_CHARS * 2);
3985
+ if (!this.sawUserMessage) {
3986
+ this.sawUserMessage = true;
3987
+ this.maybeAnalyze("first-message");
3988
+ }
3989
+ }
3990
+ /** Feed every raw Claude JSONL line flowing to the server (assistant text
3991
+ * tail + TodoWrite snapshots). Cheap; called from a socket 'claude-session-message' listener. */
3992
+ noteClaudeMessage(body) {
3993
+ if (!this.options.enabled) return;
3994
+ try {
3995
+ const { assistantText, todos } = extractFromClaudeMessage(body);
3996
+ if (assistantText) this.assistantTail = assistantText.slice(-ASSISTANT_TAIL_MAX_CHARS);
3997
+ if (todos) this.todos = todos;
3998
+ } catch {
3999
+ }
4000
+ }
4001
+ /** Turn ended (any status) — throttled analysis attempt. */
4002
+ onTurnEnd() {
4003
+ if (!this.options.enabled) return;
4004
+ this.maybeAnalyze("turn-end");
4005
+ }
4006
+ maybeAnalyze(reason) {
4007
+ if (this.running) return;
4008
+ if (!this.lastUserMessage) return;
4009
+ this.running = true;
4010
+ void this.analyze(reason).catch((error) => {
4011
+ logger.logger.debug("[boardAnalyzer] analysis rejected", { error: String(error) });
4012
+ }).finally(() => {
4013
+ this.running = false;
4014
+ });
4015
+ }
4016
+ async analyze(reason) {
4017
+ let tasks = [];
4018
+ try {
4019
+ tasks = await this.options.fetchTasks() ?? [];
4020
+ } catch {
4021
+ tasks = [];
4022
+ }
4023
+ const input = {
4024
+ lastUserMessage: this.lastUserMessage,
4025
+ assistantTail: this.assistantTail,
4026
+ todos: this.todos,
4027
+ tasks
4028
+ };
4029
+ const hash = computeInputHash(input);
4030
+ const now = Date.now();
4031
+ if (this.options.selfReportState && deterministicJson.isSelfReportFresh(this.options.selfReportState, now)) {
4032
+ logger.logger.debug("[boardAnalyzer] recent self-report; skipping LLM analysis");
4033
+ return;
4034
+ }
4035
+ if (!shouldAnalyze(this.throttle, hash, now)) {
4036
+ logger.logger.debug(`[boardAnalyzer] throttled (${reason})`);
4037
+ return;
4038
+ }
4039
+ if (!this.options.rateLimiter.tryAcquire(now)) {
4040
+ logger.logger.debug("[boardAnalyzer] machine hourly cap reached; skipping");
4041
+ return;
4042
+ }
4043
+ this.throttle = { lastRunAt: now, lastHash: hash };
4044
+ const binary = titleGenerator.resolveClaudeBinary();
4045
+ if (!binary) {
4046
+ logger.logger.debug("[boardAnalyzer] no claude binary; skipping");
4047
+ return;
4048
+ }
4049
+ const stdout = await titleGenerator.runClaudeOneShot(binary, buildBoardPrompt(input));
4050
+ if (stdout == null) return;
4051
+ const verdict = parseBoardAnalysis(stdout, new Set(tasks.map((t) => t.id)));
4052
+ if (!verdict) {
4053
+ logger.logger.debug("[boardAnalyzer] unusable analysis output; dropped");
4054
+ return;
4055
+ }
4056
+ logger.logger.debug(`[boardAnalyzer] verdict: task=${verdict.taskId} attention=${verdict.attention}`);
4057
+ this.session.updateMetadata((metadata) => ({
4058
+ ...metadata,
4059
+ board: {
4060
+ ...verdict.taskId ? { taskId: verdict.taskId } : {},
4061
+ attention: verdict.attention,
4062
+ progress: verdict.progress,
4063
+ analyzedAt: Date.now()
4064
+ }
4065
+ }));
4066
+ }
4067
+ }
4068
+
4069
+ const ASSISTANT_DISALLOWED_TOOLS = [
4070
+ "Bash",
4071
+ "Edit",
4072
+ "Write",
4073
+ "MultiEdit",
4074
+ "NotebookEdit"
4075
+ ];
4076
+ function withAssistantDenylist(current, isAssistant) {
4077
+ if (!isAssistant) return current;
4078
+ const merged = /* @__PURE__ */ new Set([...current ?? [], ...ASSISTANT_DISALLOWED_TOOLS]);
4079
+ return [...merged];
4080
+ }
4081
+
4082
+ const VALID_EFFORTS = /* @__PURE__ */ new Set(["low", "medium", "high", "xhigh", "max"]);
4083
+ const has = (meta, key) => !!meta && Object.prototype.hasOwnProperty.call(meta, key);
4084
+ function resolveMessageMode(current, meta, ctx) {
4085
+ const next = { ...current };
4086
+ const result = {};
4087
+ let permissionMode$1 = current.permissionMode;
4088
+ if (!ctx.staleModeSnapshot && meta?.permissionMode) {
4089
+ permissionMode$1 = permissionMode.resolveRemoteClaudePermissionMode(current.permissionMode, meta.permissionMode, ctx.sandboxEnabled);
4090
+ next.permissionMode = permissionMode$1;
4091
+ result.publishPermissionMode = permissionMode$1;
4092
+ }
4093
+ if (has(meta, "model")) next.model = meta.model || void 0;
4094
+ if (has(meta, "customSystemPrompt")) next.customSystemPrompt = meta.customSystemPrompt || void 0;
4095
+ if (has(meta, "fallbackModel")) next.fallbackModel = meta.fallbackModel || void 0;
4096
+ if (has(meta, "appendSystemPrompt")) next.appendSystemPrompt = meta.appendSystemPrompt || void 0;
4097
+ if (has(meta, "allowedTools")) next.allowedTools = meta.allowedTools || void 0;
4098
+ if (has(meta, "disallowedTools")) {
4099
+ next.disallowedTools = withAssistantDenylist(meta.disallowedTools || void 0, ctx.isAssistantVariant);
4100
+ }
4101
+ if (has(meta, "effort")) {
4102
+ const incoming = meta.effort;
4103
+ if (incoming === null || incoming === void 0) {
4104
+ next.effort = void 0;
4105
+ } else if (typeof incoming === "string" && VALID_EFFORTS.has(incoming)) {
4106
+ next.effort = incoming;
4107
+ } else {
4108
+ result.ignoredEffort = String(incoming);
4109
+ }
4110
+ }
4111
+ return {
4112
+ ...result,
4113
+ next,
4114
+ mode: {
4115
+ permissionMode: permissionMode$1 || "default",
4116
+ model: next.model,
4117
+ fallbackModel: next.fallbackModel,
4118
+ customSystemPrompt: next.customSystemPrompt,
4119
+ appendSystemPrompt: next.appendSystemPrompt,
4120
+ allowedTools: next.allowedTools,
4121
+ disallowedTools: next.disallowedTools,
4122
+ effort: next.effort
4123
+ }
4124
+ };
4125
+ }
4126
+
4127
+ exports.reportSessionEditToDaemon = controlClient.reportSessionEditToDaemon;
4128
+ exports.claudeCheckSession = systemPrompt.claudeCheckSession;
4129
+ exports.parseSpecialCommand = specialCommands.parseSpecialCommand;
4130
+ exports.contentLogMetadata = contentLogMetadata.contentLogMetadata;
4131
+ exports.RawJSONLinesSchema = types.RawJSONLinesSchema;
4132
+ exports.getProjectPath = path.getProjectPath;
4133
+ exports.createSelfReportState = deterministicJson.createSelfReportState;
4134
+ exports.startHappyServer = deterministicJson.startHappyServer;
4135
+ exports.createSessionScanner = sessionScanner.createSessionScanner;
4136
+ exports.MessageQueue2 = registerKillSessionHandler.MessageQueue2;
4137
+ exports.registerKillSessionHandler = registerKillSessionHandler.registerKillSessionHandler;
4138
+ exports.getEnvironmentInfo = doctor.getEnvironmentInfo;
4139
+ exports.EditReportThrottle = editPaths.EditReportThrottle;
4140
+ exports.extractClaudeEditPaths = editPaths.extractClaudeEditPaths;
4141
+ exports.TitleGenerator = titleGenerator.TitleGenerator;
4142
+ exports.BoardAnalyzer = BoardAnalyzer;
4143
+ exports.FileRateLimiter = FileRateLimiter;
4144
+ exports.PrewarmHookGate = PrewarmHookGate;
4145
+ exports.SessionExitGate = SessionExitGate;
4146
+ exports.acquirePrewarmSlot = acquirePrewarmSlot;
4147
+ exports.claudeModeHash = claudeModeHash;
4148
+ exports.claudePrewarmEligibility = claudePrewarmEligibility;
4149
+ exports.claudePrewarmSessionEligibility = claudePrewarmSessionEligibility;
4150
+ exports.cleanupHookSettingsFile = cleanupHookSettingsFile;
4151
+ exports.createClaudePrewarmLease = createClaudePrewarmLease;
4152
+ exports.disposeAllClaudePrewarms = disposeAllClaudePrewarms;
4153
+ exports.evaluatePrewarmPrediction = evaluatePrewarmPrediction;
4154
+ exports.exitIntentFromArchiveFlag = exitIntentFromArchiveFlag;
4155
+ exports.formatPrewarmLine = formatPrewarmLine;
4156
+ exports.generateHookSettingsFile = generateHookSettingsFile;
4157
+ exports.loop = loop;
4158
+ exports.prewarmCacheFile = prewarmCacheFile;
4159
+ exports.prewarmPredictionMeta = prewarmPredictionMeta;
4160
+ exports.prewarmSlotDir = prewarmSlotDir;
4161
+ exports.readPrewarmCache = readPrewarmCache;
4162
+ exports.registerSideQuestionHandler = registerSideQuestionHandler;
4163
+ exports.resolveMessageMode = resolveMessageMode;
4164
+ exports.settleSessionOnExit = settleSessionOnExit;
4165
+ exports.startHookServer = startHookServer;
4166
+ exports.withAssistantDenylist = withAssistantDenylist;
4167
+ exports.writePrewarmCache = writePrewarmCache;
4168
+ exports.writeSideQuestionSettingsFile = writeSideQuestionSettingsFile;