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