very-happy-cli 0.2.159 → 0.2.160

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