visionclaw 0.1.195-beta.0 → 0.1.195-dev.feat-e2e-test-system.1

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 (345) hide show
  1. package/dist/agent/command-handlers.d.ts.map +1 -1
  2. package/dist/agent/command-handlers.js +17 -0
  3. package/dist/agent/command-handlers.js.map +1 -1
  4. package/dist/builtin-skills/catalog/equity-research/SKILL.md +256 -0
  5. package/dist/builtin-skills/catalog/financial-modeling/SKILL.md +186 -0
  6. package/dist/builtin-skills/catalog/investment-banking/SKILL.md +213 -0
  7. package/dist/builtin-skills/catalog/private-equity/SKILL.md +282 -0
  8. package/dist/builtin-skills/catalog/wealth-management/SKILL.md +252 -0
  9. package/dist/channels/interface.d.ts +9 -0
  10. package/dist/channels/interface.d.ts.map +1 -1
  11. package/dist/channels/manager.d.ts.map +1 -1
  12. package/dist/channels/manager.js +3 -0
  13. package/dist/channels/manager.js.map +1 -1
  14. package/dist/channels/telegram.d.ts.map +1 -1
  15. package/dist/channels/telegram.js +7 -1
  16. package/dist/channels/telegram.js.map +1 -1
  17. package/dist/config/types.d.ts +6 -0
  18. package/dist/config/types.d.ts.map +1 -1
  19. package/dist/config/types.js +10 -0
  20. package/dist/config/types.js.map +1 -1
  21. package/dist/e2e/artifacts.d.ts +8 -0
  22. package/dist/e2e/artifacts.d.ts.map +1 -0
  23. package/dist/e2e/artifacts.js +35 -0
  24. package/dist/e2e/artifacts.js.map +1 -0
  25. package/dist/e2e/cleanup.d.ts +8 -0
  26. package/dist/e2e/cleanup.d.ts.map +1 -0
  27. package/dist/e2e/cleanup.js +108 -0
  28. package/dist/e2e/cleanup.js.map +1 -0
  29. package/dist/e2e/cli.d.ts +4 -0
  30. package/dist/e2e/cli.d.ts.map +1 -0
  31. package/dist/e2e/cli.js +16 -0
  32. package/dist/e2e/cli.js.map +1 -0
  33. package/dist/e2e/index.d.ts +5 -0
  34. package/dist/e2e/index.d.ts.map +1 -0
  35. package/dist/e2e/index.js +4 -0
  36. package/dist/e2e/index.js.map +1 -0
  37. package/dist/e2e/local-test-server.d.ts +7 -0
  38. package/dist/e2e/local-test-server.d.ts.map +1 -0
  39. package/dist/e2e/local-test-server.js +75 -0
  40. package/dist/e2e/local-test-server.js.map +1 -0
  41. package/dist/e2e/oauth-setup-store.d.ts +28 -0
  42. package/dist/e2e/oauth-setup-store.d.ts.map +1 -0
  43. package/dist/e2e/oauth-setup-store.js +56 -0
  44. package/dist/e2e/oauth-setup-store.js.map +1 -0
  45. package/dist/e2e/parser.d.ts +4 -0
  46. package/dist/e2e/parser.d.ts.map +1 -0
  47. package/dist/e2e/parser.js +52 -0
  48. package/dist/e2e/parser.js.map +1 -0
  49. package/dist/e2e/registry.d.ts +3 -0
  50. package/dist/e2e/registry.d.ts.map +1 -0
  51. package/dist/e2e/registry.js +44 -0
  52. package/dist/e2e/registry.js.map +1 -0
  53. package/dist/e2e/reporter.d.ts +6 -0
  54. package/dist/e2e/reporter.d.ts.map +1 -0
  55. package/dist/e2e/reporter.js +56 -0
  56. package/dist/e2e/reporter.js.map +1 -0
  57. package/dist/e2e/runner.d.ts +4 -0
  58. package/dist/e2e/runner.d.ts.map +1 -0
  59. package/dist/e2e/runner.js +116 -0
  60. package/dist/e2e/runner.js.map +1 -0
  61. package/dist/e2e/setup-google-guest.d.ts +19 -0
  62. package/dist/e2e/setup-google-guest.d.ts.map +1 -0
  63. package/dist/e2e/setup-google-guest.js +205 -0
  64. package/dist/e2e/setup-google-guest.js.map +1 -0
  65. package/dist/e2e/suite-utils.d.ts +19 -0
  66. package/dist/e2e/suite-utils.d.ts.map +1 -0
  67. package/dist/e2e/suite-utils.js +60 -0
  68. package/dist/e2e/suite-utils.js.map +1 -0
  69. package/dist/e2e/suites/agent.d.ts +3 -0
  70. package/dist/e2e/suites/agent.d.ts.map +1 -0
  71. package/dist/e2e/suites/agent.js +33 -0
  72. package/dist/e2e/suites/agent.js.map +1 -0
  73. package/dist/e2e/suites/browser.d.ts +3 -0
  74. package/dist/e2e/suites/browser.d.ts.map +1 -0
  75. package/dist/e2e/suites/browser.js +58 -0
  76. package/dist/e2e/suites/browser.js.map +1 -0
  77. package/dist/e2e/suites/cua.d.ts +3 -0
  78. package/dist/e2e/suites/cua.d.ts.map +1 -0
  79. package/dist/e2e/suites/cua.js +68 -0
  80. package/dist/e2e/suites/cua.js.map +1 -0
  81. package/dist/e2e/suites/google.d.ts +3 -0
  82. package/dist/e2e/suites/google.d.ts.map +1 -0
  83. package/dist/e2e/suites/google.js +145 -0
  84. package/dist/e2e/suites/google.js.map +1 -0
  85. package/dist/e2e/suites/memory.d.ts +3 -0
  86. package/dist/e2e/suites/memory.d.ts.map +1 -0
  87. package/dist/e2e/suites/memory.js +50 -0
  88. package/dist/e2e/suites/memory.js.map +1 -0
  89. package/dist/e2e/suites/obs.d.ts +3 -0
  90. package/dist/e2e/suites/obs.d.ts.map +1 -0
  91. package/dist/e2e/suites/obs.js +29 -0
  92. package/dist/e2e/suites/obs.js.map +1 -0
  93. package/dist/e2e/suites/self.d.ts +3 -0
  94. package/dist/e2e/suites/self.d.ts.map +1 -0
  95. package/dist/e2e/suites/self.js +65 -0
  96. package/dist/e2e/suites/self.js.map +1 -0
  97. package/dist/e2e/suites/upgrade.d.ts +3 -0
  98. package/dist/e2e/suites/upgrade.d.ts.map +1 -0
  99. package/dist/e2e/suites/upgrade.js +31 -0
  100. package/dist/e2e/suites/upgrade.js.map +1 -0
  101. package/dist/e2e/types.d.ts +91 -0
  102. package/dist/e2e/types.d.ts.map +1 -0
  103. package/dist/e2e/types.js +2 -0
  104. package/dist/e2e/types.js.map +1 -0
  105. package/dist/index.js.map +1 -1
  106. package/dist/service/daemon.d.ts +1 -0
  107. package/dist/service/daemon.d.ts.map +1 -1
  108. package/dist/service/daemon.js +110 -15
  109. package/dist/service/daemon.js.map +1 -1
  110. package/dist/tools/upgrade.d.ts +8 -0
  111. package/dist/tools/upgrade.d.ts.map +1 -1
  112. package/dist/tools/upgrade.js +64 -8
  113. package/dist/tools/upgrade.js.map +1 -1
  114. package/dist-agent/bundle.cjs +32037 -30064
  115. package/package.json +1 -1
  116. package/dist/agent/applied-credential-signature.d.ts +0 -53
  117. package/dist/agent/applied-credential-signature.d.ts.map +0 -1
  118. package/dist/agent/applied-credential-signature.js +0 -137
  119. package/dist/agent/applied-credential-signature.js.map +0 -1
  120. package/dist/agent/engines/claude/cli-resolver.d.ts +0 -16
  121. package/dist/agent/engines/claude/cli-resolver.d.ts.map +0 -1
  122. package/dist/agent/engines/claude/cli-resolver.js +0 -83
  123. package/dist/agent/engines/claude/cli-resolver.js.map +0 -1
  124. package/dist/agent/engines/claude/session-browser-policy.d.ts +0 -9
  125. package/dist/agent/engines/claude/session-browser-policy.d.ts.map +0 -1
  126. package/dist/agent/engines/claude/session-browser-policy.js +0 -49
  127. package/dist/agent/engines/claude/session-browser-policy.js.map +0 -1
  128. package/dist/agent/engines/claude/session.d.ts +0 -304
  129. package/dist/agent/engines/claude/session.d.ts.map +0 -1
  130. package/dist/agent/engines/claude/session.js +0 -1233
  131. package/dist/agent/engines/claude/session.js.map +0 -1
  132. package/dist/agent/engines/client-factory.d.ts +0 -63
  133. package/dist/agent/engines/client-factory.d.ts.map +0 -1
  134. package/dist/agent/engines/client-factory.js +0 -382
  135. package/dist/agent/engines/client-factory.js.map +0 -1
  136. package/dist/agent/engines/engine-factory.d.ts +0 -5
  137. package/dist/agent/engines/engine-factory.d.ts.map +0 -1
  138. package/dist/agent/engines/engine-factory.js +0 -7
  139. package/dist/agent/engines/engine-factory.js.map +0 -1
  140. package/dist/agent/engines/engine.d.ts +0 -8
  141. package/dist/agent/engines/engine.d.ts.map +0 -1
  142. package/dist/agent/engines/engine.js +0 -15
  143. package/dist/agent/engines/engine.js.map +0 -1
  144. package/dist/agent/engines/openai/file-session.d.ts +0 -19
  145. package/dist/agent/engines/openai/file-session.d.ts.map +0 -1
  146. package/dist/agent/engines/openai/file-session.js +0 -78
  147. package/dist/agent/engines/openai/file-session.js.map +0 -1
  148. package/dist/agent/engines/openai/file-tools.d.ts +0 -35
  149. package/dist/agent/engines/openai/file-tools.d.ts.map +0 -1
  150. package/dist/agent/engines/openai/file-tools.js +0 -194
  151. package/dist/agent/engines/openai/file-tools.js.map +0 -1
  152. package/dist/agent/engines/openai/session.d.ts +0 -55
  153. package/dist/agent/engines/openai/session.d.ts.map +0 -1
  154. package/dist/agent/engines/openai/session.js +0 -447
  155. package/dist/agent/engines/openai/session.js.map +0 -1
  156. package/dist/agent/engines/openai/tools.d.ts +0 -15
  157. package/dist/agent/engines/openai/tools.d.ts.map +0 -1
  158. package/dist/agent/engines/openai/tools.js +0 -221
  159. package/dist/agent/engines/openai/tools.js.map +0 -1
  160. package/dist/agent/engines/pi/session.d.ts +0 -54
  161. package/dist/agent/engines/pi/session.d.ts.map +0 -1
  162. package/dist/agent/engines/pi/session.js +0 -397
  163. package/dist/agent/engines/pi/session.js.map +0 -1
  164. package/dist/agent/engines/pi/tools.d.ts +0 -19
  165. package/dist/agent/engines/pi/tools.d.ts.map +0 -1
  166. package/dist/agent/engines/pi/tools.js +0 -127
  167. package/dist/agent/engines/pi/tools.js.map +0 -1
  168. package/dist/agent/engines/session-types.d.ts +0 -153
  169. package/dist/agent/engines/session-types.d.ts.map +0 -1
  170. package/dist/agent/engines/session-types.js +0 -2
  171. package/dist/agent/engines/session-types.js.map +0 -1
  172. package/dist/agent/engines/system-prompt-log.d.ts +0 -9
  173. package/dist/agent/engines/system-prompt-log.d.ts.map +0 -1
  174. package/dist/agent/engines/system-prompt-log.js +0 -46
  175. package/dist/agent/engines/system-prompt-log.js.map +0 -1
  176. package/dist/agent/model-provider.d.ts +0 -103
  177. package/dist/agent/model-provider.d.ts.map +0 -1
  178. package/dist/agent/model-provider.js +0 -540
  179. package/dist/agent/model-provider.js.map +0 -1
  180. package/dist/agent/transcript/transcript-backfill.d.ts +0 -54
  181. package/dist/agent/transcript/transcript-backfill.d.ts.map +0 -1
  182. package/dist/agent/transcript/transcript-backfill.js +0 -604
  183. package/dist/agent/transcript/transcript-backfill.js.map +0 -1
  184. package/dist/agent/transcript/transcript-indexer.d.ts +0 -273
  185. package/dist/agent/transcript/transcript-indexer.d.ts.map +0 -1
  186. package/dist/agent/transcript/transcript-indexer.js +0 -1217
  187. package/dist/agent/transcript/transcript-indexer.js.map +0 -1
  188. package/dist/agent/transcript/transcript-memory-migrations.d.ts +0 -25
  189. package/dist/agent/transcript/transcript-memory-migrations.d.ts.map +0 -1
  190. package/dist/agent/transcript/transcript-memory-migrations.js +0 -87
  191. package/dist/agent/transcript/transcript-memory-migrations.js.map +0 -1
  192. package/dist/agent/transcript-memory-migrations.d.ts +0 -25
  193. package/dist/agent/transcript-memory-migrations.d.ts.map +0 -1
  194. package/dist/agent/transcript-memory-migrations.js +0 -87
  195. package/dist/agent/transcript-memory-migrations.js.map +0 -1
  196. package/dist/agent/tunnel-credential-handler.d.ts +0 -90
  197. package/dist/agent/tunnel-credential-handler.d.ts.map +0 -1
  198. package/dist/agent/tunnel-credential-handler.js +0 -162
  199. package/dist/agent/tunnel-credential-handler.js.map +0 -1
  200. package/dist/agent/usage/usage-backfill-handler.d.ts +0 -18
  201. package/dist/agent/usage/usage-backfill-handler.d.ts.map +0 -1
  202. package/dist/agent/usage/usage-backfill-handler.js +0 -69
  203. package/dist/agent/usage/usage-backfill-handler.js.map +0 -1
  204. package/dist/agent/usage/usage-gate.d.ts +0 -25
  205. package/dist/agent/usage/usage-gate.d.ts.map +0 -1
  206. package/dist/agent/usage/usage-gate.js +0 -83
  207. package/dist/agent/usage/usage-gate.js.map +0 -1
  208. package/dist/agent/usage/usage-handler.d.ts +0 -7
  209. package/dist/agent/usage/usage-handler.d.ts.map +0 -1
  210. package/dist/agent/usage/usage-handler.js +0 -28
  211. package/dist/agent/usage/usage-handler.js.map +0 -1
  212. package/dist/agent/usage/usage-report-builder.d.ts +0 -26
  213. package/dist/agent/usage/usage-report-builder.d.ts.map +0 -1
  214. package/dist/agent/usage/usage-report-builder.js +0 -80
  215. package/dist/agent/usage/usage-report-builder.js.map +0 -1
  216. package/dist/agent/usage/usage-report-queue.d.ts +0 -26
  217. package/dist/agent/usage/usage-report-queue.d.ts.map +0 -1
  218. package/dist/agent/usage/usage-report-queue.js +0 -199
  219. package/dist/agent/usage/usage-report-queue.js.map +0 -1
  220. package/dist/agent/usage/usage-report-types.d.ts +0 -41
  221. package/dist/agent/usage/usage-report-types.d.ts.map +0 -1
  222. package/dist/agent/usage/usage-report-types.js +0 -2
  223. package/dist/agent/usage/usage-report-types.js.map +0 -1
  224. package/dist/agent/usage/usage-reporter.d.ts +0 -31
  225. package/dist/agent/usage/usage-reporter.d.ts.map +0 -1
  226. package/dist/agent/usage/usage-reporter.js +0 -102
  227. package/dist/agent/usage/usage-reporter.js.map +0 -1
  228. package/dist/agent/usage-backfill-handler.d.ts +0 -18
  229. package/dist/agent/usage-backfill-handler.d.ts.map +0 -1
  230. package/dist/agent/usage-backfill-handler.js +0 -69
  231. package/dist/agent/usage-backfill-handler.js.map +0 -1
  232. package/dist/agent/usage-gate.d.ts +0 -25
  233. package/dist/agent/usage-gate.d.ts.map +0 -1
  234. package/dist/agent/usage-gate.js +0 -83
  235. package/dist/agent/usage-gate.js.map +0 -1
  236. package/dist/agent/usage-report-builder.d.ts +0 -26
  237. package/dist/agent/usage-report-builder.d.ts.map +0 -1
  238. package/dist/agent/usage-report-builder.js +0 -80
  239. package/dist/agent/usage-report-builder.js.map +0 -1
  240. package/dist/agent/usage-report-queue.d.ts +0 -26
  241. package/dist/agent/usage-report-queue.d.ts.map +0 -1
  242. package/dist/agent/usage-report-queue.js +0 -199
  243. package/dist/agent/usage-report-queue.js.map +0 -1
  244. package/dist/agent/usage-report-types.d.ts +0 -41
  245. package/dist/agent/usage-report-types.d.ts.map +0 -1
  246. package/dist/agent/usage-report-types.js +0 -2
  247. package/dist/agent/usage-report-types.js.map +0 -1
  248. package/dist/agent/usage-reporter.d.ts +0 -31
  249. package/dist/agent/usage-reporter.d.ts.map +0 -1
  250. package/dist/agent/usage-reporter.js +0 -102
  251. package/dist/agent/usage-reporter.js.map +0 -1
  252. package/dist/agent/wake-cycle-tool-tracker.d.ts +0 -39
  253. package/dist/agent/wake-cycle-tool-tracker.d.ts.map +0 -1
  254. package/dist/agent/wake-cycle-tool-tracker.js +0 -72
  255. package/dist/agent/wake-cycle-tool-tracker.js.map +0 -1
  256. package/dist/billing/payg-handler.d.ts +0 -29
  257. package/dist/billing/payg-handler.d.ts.map +0 -1
  258. package/dist/billing/payg-handler.js +0 -92
  259. package/dist/billing/payg-handler.js.map +0 -1
  260. package/dist/billing/payment-handler.d.ts +0 -24
  261. package/dist/billing/payment-handler.d.ts.map +0 -1
  262. package/dist/billing/payment-handler.js +0 -101
  263. package/dist/billing/payment-handler.js.map +0 -1
  264. package/dist/builtin-skills/catalog/phone-adb-automation/SKILL.md +0 -412
  265. package/dist/builtin-skills/catalog/phone-adb-automation/phone_input.sh +0 -132
  266. package/dist/builtin-skills/catalog/phone-adb-automation/phone_launch.sh +0 -166
  267. package/dist/builtin-skills/catalog/phone-adb-automation/phone_screenshot.sh +0 -87
  268. package/dist/builtin-skills/catalog/phone-adb-automation/phone_security_kbd.py +0 -174
  269. package/dist/builtin-skills/catalog/phone-adb-automation/phone_setup.sh +0 -274
  270. package/dist/builtin-skills/catalog/phone-adb-automation/phone_swipe.sh +0 -111
  271. package/dist/builtin-skills/catalog/phone-adb-automation/phone_tap.sh +0 -87
  272. package/dist/builtin-skills/catalog/phone-adb-automation/phone_ui_parse.py +0 -176
  273. package/dist/builtin-skills/catalog/phone-adb-automation/phone_wake_unlock.sh +0 -67
  274. package/dist/builtin-skills/transcribe-audio/SKILL.md +0 -122
  275. package/dist/data-processing/convert-demo-cli.d.ts +0 -7
  276. package/dist/data-processing/convert-demo-cli.d.ts.map +0 -1
  277. package/dist/data-processing/convert-demo-cli.js +0 -30
  278. package/dist/data-processing/convert-demo-cli.js.map +0 -1
  279. package/dist/data-processing/convert-demo.d.ts +0 -26
  280. package/dist/data-processing/convert-demo.d.ts.map +0 -1
  281. package/dist/data-processing/convert-demo.js +0 -233
  282. package/dist/data-processing/convert-demo.js.map +0 -1
  283. package/dist/obs/rdp/icons/icons/app_windows.svg +0 -4
  284. package/dist/obs/rdp/icons/icons/clip_get.svg +0 -4
  285. package/dist/obs/rdp/icons/icons/clip_send.svg +0 -4
  286. package/dist/obs/rdp/icons/icons/clip_shared.svg +0 -4
  287. package/dist/obs/rdp/icons/icons/clipboard.svg +0 -4
  288. package/dist/obs/rdp/icons/icons/clipboard_shared.svg +0 -4
  289. package/dist/obs/rdp/icons/icons/control.svg +0 -4
  290. package/dist/obs/rdp/icons/icons/desktop.svg +0 -4
  291. package/dist/obs/rdp/icons/icons/display.svg +0 -4
  292. package/dist/obs/rdp/icons/icons/launchpad.svg +0 -4
  293. package/dist/obs/rdp/icons/icons/mission_control.svg +0 -4
  294. package/dist/obs/rdp/icons/icons/screenshot.svg +0 -4
  295. package/dist/obs/rdp/icons/icons/zoom_actual.svg +0 -4
  296. package/dist/obs/rdp/icons/icons/zoom_fit.svg +0 -4
  297. package/dist/obs/rdp/icons/icons/zoom_in.svg +0 -4
  298. package/dist/obs/rdp/icons/icons/zoom_out.svg +0 -4
  299. package/dist/obs/tunnel-telemetry.d.ts +0 -46
  300. package/dist/obs/tunnel-telemetry.d.ts.map +0 -1
  301. package/dist/obs/tunnel-telemetry.js +0 -70
  302. package/dist/obs/tunnel-telemetry.js.map +0 -1
  303. package/dist/onboarding/cloudflared-cert.d.ts +0 -15
  304. package/dist/onboarding/cloudflared-cert.d.ts.map +0 -1
  305. package/dist/onboarding/cloudflared-cert.js +0 -57
  306. package/dist/onboarding/cloudflared-cert.js.map +0 -1
  307. package/dist/onboarding/playwriter-extension.d.ts +0 -19
  308. package/dist/onboarding/playwriter-extension.d.ts.map +0 -1
  309. package/dist/onboarding/playwriter-extension.js +0 -246
  310. package/dist/onboarding/playwriter-extension.js.map +0 -1
  311. package/dist/realtime/websocket.d.ts +0 -7
  312. package/dist/realtime/websocket.d.ts.map +0 -1
  313. package/dist/realtime/websocket.js +0 -65
  314. package/dist/realtime/websocket.js.map +0 -1
  315. package/dist/service/gbox-tun.d.ts +0 -14
  316. package/dist/service/gbox-tun.d.ts.map +0 -1
  317. package/dist/service/gbox-tun.js +0 -315
  318. package/dist/service/gbox-tun.js.map +0 -1
  319. package/dist/skills/installed.d.ts +0 -11
  320. package/dist/skills/installed.d.ts.map +0 -1
  321. package/dist/skills/installed.js +0 -35
  322. package/dist/skills/installed.js.map +0 -1
  323. package/dist/tools/coordinate-resolver.d.ts +0 -30
  324. package/dist/tools/coordinate-resolver.d.ts.map +0 -1
  325. package/dist/tools/coordinate-resolver.js +0 -104
  326. package/dist/tools/coordinate-resolver.js.map +0 -1
  327. package/dist/utils/playwriter-relay.d.ts +0 -9
  328. package/dist/utils/playwriter-relay.d.ts.map +0 -1
  329. package/dist/utils/playwriter-relay.js +0 -77
  330. package/dist/utils/playwriter-relay.js.map +0 -1
  331. package/dist/utils/wechat-monitor.d.ts +0 -21
  332. package/dist/utils/wechat-monitor.d.ts.map +0 -1
  333. package/dist/utils/wechat-monitor.js +0 -88
  334. package/dist/utils/wechat-monitor.js.map +0 -1
  335. package/dist-agent/realtime/assets/index.html +0 -1058
  336. package/dist-agent/realtime/assets/samples/alloy.mp3 +0 -0
  337. package/dist-agent/realtime/assets/samples/ash.mp3 +0 -0
  338. package/dist-agent/realtime/assets/samples/ballad.mp3 +0 -0
  339. package/dist-agent/realtime/assets/samples/cedar.mp3 +0 -0
  340. package/dist-agent/realtime/assets/samples/coral.mp3 +0 -0
  341. package/dist-agent/realtime/assets/samples/echo.mp3 +0 -0
  342. package/dist-agent/realtime/assets/samples/marin.mp3 +0 -0
  343. package/dist-agent/realtime/assets/samples/sage.mp3 +0 -0
  344. package/dist-agent/realtime/assets/samples/shimmer.mp3 +0 -0
  345. package/dist-agent/realtime/assets/samples/verse.mp3 +0 -0
@@ -1,1233 +0,0 @@
1
- import * as claudeAgentSdk from "@anthropic-ai/claude-agent-sdk";
2
- import { query, } from "@anthropic-ai/claude-agent-sdk";
3
- import { loadSessionId, saveSessionId, loadUsageSnapshot, saveUsageSnapshot, getConfigDir } from "../../../config/index.js";
4
- import { seedClaudeAutoMemorySetting } from "../../../config/claude-settings.js";
5
- import { logger } from "../../../logger.js";
6
- import { createToolServer } from "../../../tools/index.js";
7
- import { sanitizeJsonString, sanitizeJsonValueInPlace } from "../../../utils/json-sanitize.js";
8
- import { buildAgentEnv, getModelId } from "../../model-provider.js";
9
- import { logSystemPrompt } from "../system-prompt-log.js";
10
- import { getAgentState } from "../../state.js";
11
- import { resolveClaudeCliExecutable } from "./cli-resolver.js";
12
- import { ensurePlaywriterChrome } from "./session-browser-policy.js";
13
- import path from "path";
14
- import { getClaudeTranscriptDirForProfileDir } from "../../../utils/claude-transcripts.js";
15
- import { ensureBrowser } from "../../browser-launcher.js";
16
- import * as imagePrunerUnTyped from "../../image-pruner.js";
17
- import * as sessionTrimmerUnTyped from "../../session-trimmer.js";
18
- const imagePruner = imagePrunerUnTyped;
19
- const sessionTrimmer = sessionTrimmerUnTyped;
20
- /**
21
- * Used-token threshold (fraction of context window) at which we trigger
22
- * compaction for **large** context windows (>= {@link COMPACT_MAX_CONTEXT_WINDOW}).
23
- *
24
- * Smaller windows scale up to {@link COMPACT_SMALL_WINDOW_USED_PCT_THRESHOLD}
25
- * via {@link getCompactUsedPctThreshold}.
26
- */
27
- export const COMPACT_LARGE_WINDOW_USED_PCT_THRESHOLD = 0.7;
28
- const COMPACT_MIN_CONTEXT_WINDOW = 200_000;
29
- const COMPACT_MAX_CONTEXT_WINDOW = 1_000_000;
30
- /**
31
- * Used-token threshold (fraction of context window) at which we trigger
32
- * compaction for **small** context windows (<= {@link COMPACT_MIN_CONTEXT_WINDOW}).
33
- * Set above 1.0 to effectively disable token-based compaction for tiny
34
- * windows where the SDK already manages context internally.
35
- */
36
- const COMPACT_SMALL_WINDOW_USED_PCT_THRESHOLD = 5.0;
37
- export const MAX_BASE64_IMAGE_BLOCKS = 10;
38
- /**
39
- * Safety-net threshold: trigger compaction when remaining type:"image" blocks
40
- * exceed this count, to stay under Claude API's 100-image hard limit.
41
- *
42
- * Since #328, pruned images become text blocks ("[Screenshot: archived=...]")
43
- * and no longer count toward the API image limit. This threshold now primarily
44
- * guards against edge cases: legacy placeholder PNGs from older transcripts,
45
- * sessions with many unpruned screenshots accumulating before the pruner runs,
46
- * or rapid screenshot bursts between prune cycles.
47
- */
48
- export const IMAGE_COMPACT_THRESHOLD = 80;
49
- //const CONTEXT_1M_BETAS = ["context-1m-2025-08-07"] as const;
50
- /**
51
- * Cap the SDK's `startup()` initialization wait. The SDK's default is 60s,
52
- * which can effectively block an entire wake cycle if the CLI hangs on
53
- * startup. We prefer to fall back to a normal cold `query()` quickly and
54
- * let the live path surface the real error.
55
- */
56
- const WARM_INITIALIZE_TIMEOUT_MS = 15_000;
57
- /**
58
- * How long to keep a pre-warmed subprocess alive before proactively
59
- * refreshing it. Bounded staleness protects us from silent degradation:
60
- * - Anthropic API sessions have server-side idle limits; a long-idle
61
- * warmed handle may get GC'd on the server, causing the first real
62
- * turn to fail and forcing a cold recovery.
63
- * - Long-lived Node subprocesses can accumulate state (fds, memory,
64
- * half-closed sockets). Periodic replacement keeps each consumed
65
- * handle young.
66
- *
67
- * Ten minutes is a comfortable trade-off: frequent enough to avoid
68
- * most server-side reaping thresholds, infrequent enough that the
69
- * extra cold-starts during refresh are negligible (~6/hour).
70
- */
71
- const WARM_REFRESH_TTL_MS = 10 * 60_000;
72
- /**
73
- * When a proactive refresh fails (startup errored), retry at this
74
- * shorter cadence instead of waiting a full TTL. This keeps us from
75
- * sitting cold for 10 minutes if the failure was transient.
76
- */
77
- const WARM_REFRESH_RETRY_MS = 30_000;
78
- const sdkStartup = claudeAgentSdk.startup;
79
- /**
80
- * Compute the used-pct compaction threshold for a given context window.
81
- *
82
- * Behavior:
83
- * - `contextWindow <= 0` (unknown): return `fallbackThreshold` (caller's choice).
84
- * - `contextWindow <= 200K`: {@link COMPACT_SMALL_WINDOW_USED_PCT_THRESHOLD} (effectively disabled).
85
- * - `contextWindow >= 1M`: {@link COMPACT_LARGE_WINDOW_USED_PCT_THRESHOLD} (default 50%).
86
- * - In between: linear interpolation between the two.
87
- *
88
- * Tiny windows accumulate tokens slowly and the SDK manages context internally,
89
- * so we want a much higher trigger; very large windows benefit from compacting
90
- * earlier to keep latency / cache hit-rate reasonable.
91
- */
92
- function getCompactUsedPctThreshold(contextWindow, fallbackThreshold) {
93
- if (contextWindow <= 0)
94
- return fallbackThreshold;
95
- if (contextWindow <= COMPACT_MIN_CONTEXT_WINDOW)
96
- return COMPACT_SMALL_WINDOW_USED_PCT_THRESHOLD;
97
- if (contextWindow >= COMPACT_MAX_CONTEXT_WINDOW)
98
- return COMPACT_LARGE_WINDOW_USED_PCT_THRESHOLD;
99
- const progress = (contextWindow - COMPACT_MIN_CONTEXT_WINDOW) / (COMPACT_MAX_CONTEXT_WINDOW - COMPACT_MIN_CONTEXT_WINDOW);
100
- return COMPACT_SMALL_WINDOW_USED_PCT_THRESHOLD + progress * (COMPACT_LARGE_WINDOW_USED_PCT_THRESHOLD - COMPACT_SMALL_WINDOW_USED_PCT_THRESHOLD);
101
- }
102
- /**
103
- * Wraps the V1 query() API with a long-lived async generator for streaming
104
- * input. This allows injecting additional user messages while the agent is
105
- * processing (interrupt messages), instead of waiting for the query to
106
- * finish before sending the next message.
107
- */
108
- export class ClaudeAgentSession {
109
- config;
110
- buildSystemPrompt;
111
- runtimeSurface;
112
- getDualSessionEnabled;
113
- getAndroidUseEnabled;
114
- getCardAvailable;
115
- /** Working directory for the SDK subprocess. Defaults to profile config dir. */
116
- sessionCwd;
117
- nativeTools;
118
- /** Resolved external MCP servers (playwright, serpapi, etc.) for this runtime. */
119
- externalMcpServers;
120
- /** Dynamic MCP servers added at runtime via manage_mcp_servers tool. */
121
- dynamicMcpServers;
122
- currentQuery = null;
123
- warmQuery = null;
124
- warmUpPromise = null;
125
- warmReadyAtMs = null;
126
- warmGeneration = 0;
127
- sessionId;
128
- transcriptPath = null;
129
- mode;
130
- /**
131
- * Pending message queue for the long-lived generator.
132
- * When a message is injected, it's pushed here and the waiting
133
- * generator is woken up via the resolver.
134
- */
135
- pendingMessages = [];
136
- messageResolver = null;
137
- generatorClosed = true;
138
- _orphanedInjections = 0;
139
- lastCompactRequestAtMs = 0;
140
- compactInFlight = false;
141
- stopRequested = false;
142
- /**
143
- * Tracks whether we've seeded `<profile>/.claude/settings.local.json`
144
- * with our auto-memory directory override this process-lifetime. The seed
145
- * helper is itself idempotent, but this flag keeps hot paths (every
146
- * `consume()` / `warmUp()`) from hitting the filesystem repeatedly.
147
- */
148
- autoMemorySeedDone = false;
149
- /** Optional: auto-injects screenshots after Playwright tool calls. */
150
- _screenshotInjector = null;
151
- /** AbortController passed to the SDK; aborting it cleanly stops the running query. */
152
- abortController = null;
153
- warmAbortController = null;
154
- /**
155
- * Timer that proactively replaces the pre-warmed subprocess at
156
- * `WARM_REFRESH_TTL_MS` intervals. Cleared when the warm is consumed,
157
- * on stop, or before arming a new one.
158
- */
159
- warmRefreshTimer = null;
160
- lastUsageSnapshot = null;
161
- playwriterConnectionsResetThisTurn = false;
162
- constructor({ config, buildSystemPrompt, runtimeSurface, sessionContext, }) {
163
- this.config = config;
164
- this.buildSystemPrompt = buildSystemPrompt;
165
- this.runtimeSurface = runtimeSurface;
166
- this.getDualSessionEnabled = sessionContext.getDualSessionEnabled;
167
- this.getAndroidUseEnabled = sessionContext.getAndroidUseEnabled;
168
- this.getCardAvailable = sessionContext.getCardAvailable;
169
- // Always use the config dir as cwd so the SDK stores transcripts in a
170
- // consistent directory regardless of which project the coding session
171
- // is working on.
172
- this.sessionCwd = getConfigDir();
173
- this.nativeTools = runtimeSurface.nativeTools;
174
- this.externalMcpServers = runtimeSurface.externalMcpServers;
175
- this.dynamicMcpServers = {};
176
- this.mode = sessionContext.mode;
177
- this.sessionId = loadSessionId(this.mode);
178
- const persisted = loadUsageSnapshot(this.mode);
179
- if (persisted && this.sessionId) {
180
- this.lastUsageSnapshot = { ...persisted, capturedAtMs: Date.now() };
181
- }
182
- }
183
- set screenshotInjector(injector) {
184
- this._screenshotInjector = injector;
185
- }
186
- /**
187
- * Seed `<profile>/.claude/settings.local.json` once per process so the
188
- * CLI's auto-memory lands inside the active profile instead of the
189
- * default `~/.claude/projects/...` tree. Called from every CLI-spawn
190
- * path (warm + cold); the underlying helper is itself idempotent.
191
- *
192
- * Gated on the `autoMemorySeedDone` flag so the happy path doesn't
193
- * re-read/re-write the file every turn. The flag is set **only on
194
- * success** — if the first attempt fails (permissions, disk full, etc.)
195
- * the next warm/cold spawn will retry until it lands, instead of
196
- * silently leaving the CLI on its default memory path for the rest of
197
- * the session.
198
- */
199
- ensureAutoMemorySeeded() {
200
- if (this.autoMemorySeedDone)
201
- return;
202
- try {
203
- this.autoMemorySeedDone = seedClaudeAutoMemorySetting();
204
- }
205
- catch (err) {
206
- // Never let a settings-seeding failure block the agent. Worst case
207
- // the CLI uses its default memory path and /obs reports that path
208
- // correctly via the system/init event — degraded but functional.
209
- // We deliberately do NOT set autoMemorySeedDone here so the next
210
- // spawn retries.
211
- logger.warn(`[auto-memory] Seeding failed; proceeding with CLI defaults: ${err instanceof Error ? err.message : String(err)}`);
212
- }
213
- }
214
- /**
215
- * Build a fresh mcpServers record with a new visionclaw tool server instance.
216
- * This avoids the "Already connected to a transport" error when the same
217
- * McpServer instance is reused across concurrent/sequential query() calls.
218
- */
219
- buildMcpServers() {
220
- if (this.runtimeSurface.visionClawToolTransport.transport !== "claude-mcp") {
221
- throw new Error("Claude session requires claude-mcp VisionClaw tool transport");
222
- }
223
- return {
224
- ...this.externalMcpServers,
225
- ...this.dynamicMcpServers,
226
- visionclaw: createToolServer({
227
- dualSession: this.getDualSessionEnabled()
228
- && this.runtimeSurface.visionClawToolTransport.supportsSwitchSessionTool,
229
- androidUse: this.getAndroidUseEnabled(),
230
- cardAvailable: this.getCardAvailable(),
231
- }),
232
- };
233
- }
234
- buildNativeToolOptions() {
235
- return {
236
- tools: { type: "preset", preset: "claude_code" },
237
- };
238
- }
239
- buildHooks() {
240
- return {
241
- UserPromptSubmit: [
242
- {
243
- hooks: [
244
- (async (input, _toolUseID, { signal }) => {
245
- const up = input;
246
- try {
247
- this.captureTranscriptPath(up.transcript_path);
248
- if (signal.aborted)
249
- return {};
250
- const res = await imagePruner.pruneSessionImages({
251
- transcriptPath: up.transcript_path,
252
- keepLastNBase64Images: MAX_BASE64_IMAGE_BLOCKS,
253
- });
254
- if (this.lastUsageSnapshot) {
255
- this.captureUsageSnapshot({
256
- ...this.lastUsageSnapshot,
257
- allImageBlocks: res.allImageBlocks,
258
- });
259
- }
260
- }
261
- catch (err) {
262
- logger.warn(`UserPromptSubmit image pruning failed: ${err instanceof Error ? err.message : String(err)}`);
263
- }
264
- return {};
265
- }),
266
- ],
267
- timeout: 120,
268
- },
269
- ],
270
- PreCompact: [
271
- {
272
- hooks: [
273
- (async (input, _toolUseID, { signal }) => {
274
- const pre = input;
275
- try {
276
- this.captureTranscriptPath(pre.transcript_path);
277
- if (signal.aborted)
278
- return {};
279
- const res = await imagePruner.pruneSessionImages({
280
- transcriptPath: pre.transcript_path,
281
- keepLastNBase64Images: MAX_BASE64_IMAGE_BLOCKS,
282
- });
283
- if (res.fileChanged) {
284
- logger.system(`Pruned transcript images before compaction: pruned=${res.prunedImageBlocks} resized=${res.resizedImageBlocks} kept=${res.keptImageBlocks} total=${res.totalImageBlocks}`);
285
- }
286
- }
287
- catch (err) {
288
- logger.warn(`PreCompact image pruning failed: ${err instanceof Error ? err.message : String(err)}`);
289
- }
290
- return {};
291
- }),
292
- ],
293
- timeout: 120,
294
- },
295
- ],
296
- PreToolUse: [
297
- {
298
- matcher: this.config.browserBackend === "playwriter"
299
- ? "mcp__playwriter__.*"
300
- : "mcp__playwright__.*",
301
- hooks: [
302
- (async (_input) => {
303
- const pre = _input;
304
- logger.debug(`[PreToolUse] ensuring browser for ${pre.tool_name}`);
305
- if (this.config.browserBackend === "playwriter") {
306
- const didReset = ensurePlaywriterChrome(this.playwriterConnectionsResetThisTurn);
307
- if (didReset) {
308
- this.playwriterConnectionsResetThisTurn = true;
309
- }
310
- }
311
- else {
312
- await ensureBrowser();
313
- }
314
- return {};
315
- }),
316
- ],
317
- timeout: 30,
318
- },
319
- ],
320
- PostToolUse: [
321
- {
322
- hooks: [
323
- (async (input, _toolUseID) => {
324
- const post = input;
325
- // Track the (possibly mutated) tool response separately from
326
- // post.tool_response so the top-level-string case can swap in
327
- // a sanitized replacement without aliasing the original.
328
- let toolResponse = post.tool_response;
329
- let mutated = false;
330
- try {
331
- const { resizedCount } = await imagePruner.resizeOversizedImagesInToolOutput(toolResponse);
332
- if (resizedCount > 0) {
333
- logger.debug(`[PostToolUse] resized ${resizedCount} oversized image(s) in ${post.tool_name} output`);
334
- mutated = true;
335
- }
336
- }
337
- catch (err) {
338
- logger.warn(`PostToolUse image resize failed: ${err instanceof Error ? err.message : String(err)}`);
339
- }
340
- // Strip lone UTF-16 surrogates from the tool response so they
341
- // never reach the model API's JSON validator (which rejects
342
- // them, unlike Node's permissive JSON.stringify).
343
- //
344
- // MCP tools return objects (CallToolResult), but
345
- // tool_response is typed as `unknown` and non-MCP tools may
346
- // surface raw strings. Handle both shapes:
347
- // - string: sanitize and replace `toolResponse`
348
- // - object/array: sanitize in place
349
- try {
350
- if (typeof toolResponse === "string") {
351
- const sanitized = sanitizeJsonString(toolResponse);
352
- if (sanitized !== toolResponse) {
353
- toolResponse = sanitized;
354
- logger.debug(`[PostToolUse] sanitized lone surrogates in ${post.tool_name} string output`);
355
- mutated = true;
356
- }
357
- }
358
- else if (sanitizeJsonValueInPlace(toolResponse)) {
359
- logger.debug(`[PostToolUse] sanitized lone surrogates in ${post.tool_name} output`);
360
- mutated = true;
361
- }
362
- }
363
- catch (err) {
364
- logger.warn(`PostToolUse surrogate sanitize failed: ${err instanceof Error ? err.message : String(err)}`);
365
- }
366
- if (this._screenshotInjector) {
367
- try {
368
- await this._screenshotInjector.onToolComplete(post.tool_name, this);
369
- }
370
- catch (err) {
371
- logger.warn(`PostToolUse screenshot injection failed: ${err instanceof Error ? err.message : String(err)}`);
372
- }
373
- }
374
- if (mutated) {
375
- return {
376
- hookSpecificOutput: {
377
- hookEventName: "PostToolUse",
378
- updatedMCPToolOutput: toolResponse,
379
- },
380
- };
381
- }
382
- return {};
383
- }),
384
- ],
385
- timeout: 120,
386
- },
387
- ],
388
- };
389
- }
390
- /**
391
- * Build the shared Claude query options used for both pre-warm and live runs.
392
- */
393
- buildQueryOptions(systemPrompt, savedSessionId, abortController, effort) {
394
- const cliResolution = resolveClaudeCliExecutable();
395
- const options = {
396
- model: getModelId(this.config),
397
- ...(cliResolution.kind !== "sdk-default"
398
- ? { pathToClaudeCodeExecutable: cliResolution.path }
399
- : {}),
400
- systemPrompt,
401
- ...this.buildNativeToolOptions(),
402
- disallowedTools: this.runtimeSurface.disallowedTools,
403
- permissionMode: "bypassPermissions",
404
- allowDangerouslySkipPermissions: true,
405
- cwd: this.sessionCwd,
406
- settingSources: ["project"],
407
- mcpServers: this.buildMcpServers(),
408
- hooks: this.buildHooks(),
409
- env: buildAgentEnv(this.config),
410
- ...(savedSessionId ? { resume: savedSessionId } : {}),
411
- ...(abortController ? { abortController } : {}),
412
- includePartialMessages: true,
413
- //betas: [...CONTEXT_1M_BETAS],
414
- };
415
- options.effort = effort ?? (this.mode === "coding" ? "high" : "medium");
416
- return options;
417
- }
418
- /**
419
- * Cancel any pending proactive-refresh timer. Idempotent.
420
- */
421
- clearWarmRefreshTimer() {
422
- if (this.warmRefreshTimer) {
423
- clearTimeout(this.warmRefreshTimer);
424
- this.warmRefreshTimer = null;
425
- }
426
- }
427
- /**
428
- * Arm the proactive-refresh timer. Caller must ensure any previous timer
429
- * is cleared. Uses `.unref()` so the process can exit while a warm
430
- * subprocess is idle — the refresh is purely a background optimization
431
- * and should never hold the event loop alive on its own.
432
- */
433
- scheduleWarmRefresh(delayMs) {
434
- this.clearWarmRefreshTimer();
435
- const timer = setTimeout(() => {
436
- this.warmRefreshTimer = null;
437
- void this.performWarmRefresh();
438
- }, delayMs);
439
- if (typeof timer.unref === "function") {
440
- timer.unref();
441
- }
442
- this.warmRefreshTimer = timer;
443
- }
444
- /**
445
- * Handler for the refresh timer. Covers two cases:
446
- * 1. Normal TTL refresh: `warmQuery` is non-null → close the stale
447
- * subprocess and start a fresh one.
448
- * 2. Retry after a previous failure: `warmQuery` is null → just
449
- * attempt warmUp again (no teardown needed).
450
- *
451
- * If the warm was already consumed between timer arming and firing,
452
- * the consume path would have cleared the timer, so we shouldn't
453
- * reach here in that case.
454
- */
455
- async performWarmRefresh() {
456
- if (this.stopRequested) {
457
- // stopRequested is briefly true between requestStop() and the next
458
- // loop iteration's clearStop(). If we just return here without
459
- // rescheduling, the refresh loop silently dies until someone
460
- // explicitly re-arms it. Defer to a shorter retry so the refresh
461
- // resumes once the stop window passes.
462
- logger.debug(`Claude warm refresh deferred: stop requested; retrying in ${WARM_REFRESH_RETRY_MS}ms`);
463
- this.scheduleWarmRefresh(WARM_REFRESH_RETRY_MS);
464
- return;
465
- }
466
- if (this.warmQuery) {
467
- const oldWarm = this.warmQuery;
468
- const oldAbort = this.warmAbortController;
469
- const oldAgeMs = this.warmReadyAtMs === null ? null : Date.now() - this.warmReadyAtMs;
470
- this.warmQuery = null;
471
- this.warmAbortController = null;
472
- this.warmReadyAtMs = null;
473
- // Tear down the stale warm subprocess before starting a fresh one.
474
- // We abort first so any in-flight SDK I/O short-circuits, then close
475
- // to release the child process and open transports.
476
- try {
477
- oldAbort?.abort();
478
- }
479
- catch { /* best-effort */ }
480
- try {
481
- oldWarm.close();
482
- }
483
- catch { /* best-effort */ }
484
- logger.debug(`Claude warm refresh: replacing ${oldAgeMs === null ? "warm" : `${oldAgeMs}ms-old warm`} subprocess`);
485
- }
486
- else {
487
- // Retry path after a prior failure — nothing to tear down.
488
- logger.debug("Claude warm refresh: retrying after previous failure");
489
- }
490
- // `warmUp()` swallows its own errors (to keep its contract simple for
491
- // fire-and-forget callers), so we detect failure by inspecting warm
492
- // state after it resolves rather than relying on a thrown error. We
493
- // route the post-warmUp checks through a helper so TypeScript doesn't
494
- // narrow `this.stopRequested`/`this.warmQuery` based on the synchronous
495
- // assignments above.
496
- await this.warmUp();
497
- this.handleWarmRefreshOutcome();
498
- }
499
- /**
500
- * Post-warmUp follow-up for performWarmRefresh. Kept in its own method so
501
- * TS's control-flow narrowing in performWarmRefresh doesn't treat these
502
- * fields as having their pre-await values.
503
- */
504
- handleWarmRefreshOutcome() {
505
- if (this.stopRequested) {
506
- // Stop arrived while we were refreshing — requestStop already cleaned
507
- // up; don't re-arm a refresh timer.
508
- return;
509
- }
510
- if (!this.warmQuery) {
511
- // warmUp() failed (or was a no-op because startup is unavailable).
512
- // Schedule a shorter retry so we don't sit cold for a full TTL after
513
- // a transient failure. If startup is permanently unavailable, these
514
- // retries are cheap no-ops.
515
- logger.debug(`Claude warm refresh did not produce a new warm; retrying in ${WARM_REFRESH_RETRY_MS}ms`);
516
- this.scheduleWarmRefresh(WARM_REFRESH_RETRY_MS);
517
- }
518
- // On success, warmUp() already scheduled the next refresh at TTL.
519
- }
520
- /**
521
- * Send the initial user message and return the query generator to stream
522
- * responses. The underlying async generator stays open so that additional
523
- * messages can be injected via injectMessage().
524
- */
525
- async warmUp() {
526
- if (!sdkStartup) {
527
- return;
528
- }
529
- // Seed `<profile>/.claude/settings.local.json` once per process. The
530
- // CLI reads `autoMemoryDirectory` from this file at subprocess
531
- // startup, so we must write it before spawning the warm child.
532
- this.ensureAutoMemorySeeded();
533
- // Already warm — nothing to do, but defensively re-arm the refresh
534
- // timer if it happens to be missing. This can occur if a TTL refresh
535
- // fired during a brief stopRequested window and chose to defer rather
536
- // than close the warm.
537
- if (this.warmQuery) {
538
- if (!this.warmRefreshTimer && !this.stopRequested) {
539
- this.scheduleWarmRefresh(WARM_REFRESH_TTL_MS);
540
- }
541
- return;
542
- }
543
- // A warm-up is already in flight — await that one instead of racing.
544
- if (this.warmUpPromise) {
545
- await this.warmUpPromise;
546
- return;
547
- }
548
- // Honor an in-progress stop request — don't spin up a subprocess we're
549
- // just going to abort on the next line.
550
- if (this.stopRequested) {
551
- return;
552
- }
553
- const savedSessionId = this.sessionId;
554
- const systemPrompt = this.buildSystemPrompt();
555
- const warmAbortController = new AbortController();
556
- const warmStartMs = Date.now();
557
- const generation = this.warmGeneration;
558
- const promise = (async () => {
559
- try {
560
- const warmed = await sdkStartup({
561
- options: this.buildQueryOptions(systemPrompt, savedSessionId, warmAbortController),
562
- initializeTimeoutMs: WARM_INITIALIZE_TIMEOUT_MS,
563
- });
564
- if (generation !== this.warmGeneration) {
565
- try {
566
- warmed.close();
567
- }
568
- catch { /* best-effort */ }
569
- logger.debug("Claude query pre-warm discarded after session reset");
570
- return;
571
- }
572
- // Note: we intentionally do NOT discard the freshly-ready warm when
573
- // `this.stopRequested` is true. requestStop() is typically a "stop
574
- // the current task" signal, and the user usually starts a new task
575
- // shortly after. Throwing the warm away here would force the next
576
- // task to pay the cold-start cost we just paid. On actual process
577
- // shutdown, the warm child process is reaped with the parent.
578
- this.warmQuery = warmed;
579
- this.warmAbortController = warmAbortController;
580
- this.warmReadyAtMs = Date.now();
581
- logger.debug(`Claude query pre-warm ready in ${this.warmReadyAtMs - warmStartMs}ms (next refresh in ${WARM_REFRESH_TTL_MS}ms)`);
582
- // Arm the proactive-refresh timer. When it fires we'll close this
583
- // subprocess and spin up a fresh one, keeping staleness bounded.
584
- this.scheduleWarmRefresh(WARM_REFRESH_TTL_MS);
585
- }
586
- catch (err) {
587
- logger.debug(`Claude query pre-warm skipped after ${Date.now() - warmStartMs}ms: ${err instanceof Error ? err.message : String(err)}`);
588
- if (generation === this.warmGeneration) {
589
- this.warmQuery = null;
590
- this.warmAbortController = null;
591
- this.warmReadyAtMs = null;
592
- }
593
- }
594
- finally {
595
- if (generation === this.warmGeneration) {
596
- this.warmUpPromise = null;
597
- }
598
- }
599
- })();
600
- this.warmUpPromise = promise;
601
- await promise;
602
- }
603
- /**
604
- * Tear down any idle pre-warmed subprocess and cancel its refresh timer.
605
- *
606
- * Called when something in `config` that was baked into the warm options
607
- * changed (provider, env, model). The warm subprocess inlines provider
608
- * env via `buildAgentEnv(config)` at warm time and `sendAndStream()`
609
- * deliberately reuses those baked options on the warm path — so without
610
- * this, a provider switch can be silently undone by consuming a stale
611
- * warm child for one more turn.
612
- *
613
- * Idempotent and best-effort: safe to call when no warm exists, and
614
- * does not throw on close failures (the child will be reaped on
615
- * process exit if abort/close misfires).
616
- */
617
- discardWarmQuery() {
618
- this.clearWarmRefreshTimer();
619
- const oldWarm = this.warmQuery;
620
- const oldAbort = this.warmAbortController;
621
- if (!oldWarm && !oldAbort)
622
- return;
623
- this.warmQuery = null;
624
- this.warmAbortController = null;
625
- this.warmReadyAtMs = null;
626
- try {
627
- oldAbort?.abort();
628
- }
629
- catch { /* best-effort */ }
630
- try {
631
- oldWarm?.close();
632
- }
633
- catch { /* best-effort */ }
634
- logger.debug("Claude warm subprocess discarded (provider/env changed)");
635
- }
636
- sendAndStream(content, options) {
637
- this.playwriterConnectionsResetThisTurn = false;
638
- const savedSessionId = this.sessionId;
639
- const effort = options?.effort;
640
- // Resolve the Claude Code executable — prefers the native binary shipped
641
- // by @anthropic-ai/claude-agent-sdk >= 0.2.113, falls back to the legacy
642
- // cli.js, and finally lets the SDK resolve it internally.
643
- const cliResolution = resolveClaudeCliExecutable();
644
- logger.debug(`Claude CLI resolution: kind=${cliResolution.kind}` +
645
- (cliResolution.kind === "sdk-default" ? "" : ` path=${cliResolution.path}`));
646
- const systemPrompt = this.buildSystemPrompt();
647
- // Log the resolved system prompt for training data capture. The console
648
- // gets a compact head/tail preview; the full prompt goes into `data.prompt`
649
- // of the JSONL entry so the log file remains complete.
650
- logSystemPrompt(systemPrompt);
651
- // If a reset summary is pending, prepend it to the content as priority context.
652
- const resetSummary = this.consumeResetSummary();
653
- let finalContent = content;
654
- if (resetSummary && !savedSessionId) {
655
- const summaryBlock = {
656
- type: "text",
657
- text: `This is a fresh session created after a context reset. Here is a summary of your previous session state:\n\n${resetSummary}\n\nReview this summary and continue where you left off. Check your memory and todo list for any additional context.`,
658
- };
659
- if (Array.isArray(finalContent)) {
660
- finalContent = [summaryBlock, ...finalContent];
661
- }
662
- else if (typeof finalContent === "string") {
663
- finalContent = [summaryBlock, { type: "text", text: finalContent }];
664
- }
665
- }
666
- // Seed the generator with the initial message
667
- const initialMessage = {
668
- type: "user",
669
- session_id: savedSessionId ?? "",
670
- message: { role: "user", content: finalContent },
671
- parent_tool_use_id: null,
672
- };
673
- this.pendingMessages = [initialMessage];
674
- this.generatorClosed = false;
675
- this._orphanedInjections = 0;
676
- // Create a long-lived async generator that yields the initial message
677
- // and then waits for additional messages injected via injectMessage().
678
- // We capture references to the session's pending queue and resolver
679
- // so the generator can read from them without aliasing `this`.
680
- const pending = this.pendingMessages;
681
- const setResolver = (r) => {
682
- this.messageResolver = r;
683
- };
684
- const isClosed = () => this.generatorClosed;
685
- const shiftPending = () => pending.shift();
686
- async function* messageStream() {
687
- while (!isClosed()) {
688
- // Yield all pending messages
689
- let next = shiftPending();
690
- while (next) {
691
- yield next;
692
- next = shiftPending();
693
- }
694
- // If generator is still open, wait for the next message
695
- if (!isClosed()) {
696
- await new Promise((resolve) => {
697
- setResolver(resolve);
698
- });
699
- }
700
- }
701
- }
702
- if (savedSessionId) {
703
- logger.system(`Resuming session: ${savedSessionId}`);
704
- }
705
- else {
706
- logger.system("Creating new session...");
707
- }
708
- const prompt = messageStream();
709
- if (this.warmQuery) {
710
- // Warm path: the subprocess was already initialized with a full set of
711
- // query options (system prompt, MCP servers, hooks, CLI path, betas,
712
- // env, resume, etc.) by warmUp(). We deliberately do NOT call
713
- // buildQueryOptions() here — doing so would spin up a throwaway
714
- // McpServer instance and allocate a fresh hook closure set that get
715
- // immediately discarded, defeating the whole point of pre-warming.
716
- //
717
- // Cancel the proactive-refresh timer first: we're about to consume
718
- // this warm, and we don't want the timer to fire mid-turn and try
719
- // to close a subprocess that's now actively serving a query.
720
- this.clearWarmRefreshTimer();
721
- const warmQuery = this.warmQuery;
722
- this.warmQuery = null;
723
- // Adopt the warm abort controller so requestStop() cleanly cancels
724
- // the running query via the same AbortSignal the SDK is watching.
725
- this.abortController = this.warmAbortController;
726
- this.warmAbortController = null;
727
- const warmAgeMs = this.warmReadyAtMs === null ? null : Date.now() - this.warmReadyAtMs;
728
- if (effort) {
729
- logger.debug(`Claude query using pre-warmed subprocess${warmAgeMs === null ? "" : ` (ready ${warmAgeMs}ms ago)`} (effort=${effort} ignored — warm subprocess uses pre-built options)`);
730
- }
731
- else {
732
- logger.debug(`Claude query using pre-warmed subprocess${warmAgeMs === null ? "" : ` (ready ${warmAgeMs}ms ago)`}`);
733
- }
734
- this.warmReadyAtMs = null;
735
- this.currentQuery = warmQuery.query(prompt);
736
- }
737
- else {
738
- // Cold path: build full options and let query() spawn a fresh CLI.
739
- // Same rationale as warmUp() — seed before the subprocess starts so
740
- // it picks up the overridden `autoMemoryDirectory`.
741
- this.ensureAutoMemorySeeded();
742
- const abortController = new AbortController();
743
- this.abortController = abortController;
744
- const options = this.buildQueryOptions(systemPrompt, savedSessionId, abortController, effort);
745
- if (effort) {
746
- logger.debug(`Claude query using cold start (effort=${effort})`);
747
- }
748
- else {
749
- logger.debug("Claude query using cold start");
750
- }
751
- this.currentQuery = query({ prompt, options });
752
- }
753
- return this.currentQuery;
754
- }
755
- /**
756
- * Inject a new user message into the running query stream.
757
- * The agent will see this as a follow-up user message in the conversation.
758
- * Only works while a query is active (between sendAndStream and closeInput).
759
- *
760
- * @returns true if the message was injected, false if the generator is closed.
761
- */
762
- injectMessage(content, options) {
763
- const msg = {
764
- type: "user",
765
- session_id: this.sessionId ?? "",
766
- message: { role: "user", content },
767
- parent_tool_use_id: null,
768
- ...(options?.shouldQuery === false ? { shouldQuery: false } : {}),
769
- };
770
- if (this.generatorClosed) {
771
- logger.warn("Cannot inject message: generator is closed");
772
- return false;
773
- }
774
- this.pendingMessages.push(msg);
775
- logger.info("Injected interrupt message into active session");
776
- // Wake the generator if it's waiting
777
- if (this.messageResolver) {
778
- this.messageResolver();
779
- this.messageResolver = null;
780
- }
781
- return true;
782
- }
783
- /**
784
- * Signal the input generator to close.
785
- * Any injected messages still in the pending queue are counted as
786
- * orphaned — they were accepted by injectMessage but never delivered
787
- * to the SDK.
788
- */
789
- closeInput() {
790
- this._orphanedInjections = this.pendingMessages.length;
791
- if (this._orphanedInjections > 0) {
792
- logger.warn(`closeInput: ${this._orphanedInjections} injected message(s) orphaned`);
793
- }
794
- this.pendingMessages = [];
795
- this.generatorClosed = true;
796
- if (this.messageResolver) {
797
- this.messageResolver();
798
- this.messageResolver = null;
799
- }
800
- }
801
- /**
802
- * Returns true if injected messages were lost when the generator closed.
803
- */
804
- get hasOrphanedInjections() {
805
- return this._orphanedInjections > 0;
806
- }
807
- get isInputClosed() {
808
- return this.generatorClosed;
809
- }
810
- /**
811
- * Request that the current agent turn be stopped.
812
- * Uses the SDK's AbortController to cleanly signal the running query to
813
- * stop, then closes the input stream so the loop returns to idle.
814
- */
815
- requestStop() {
816
- this.stopRequested = true;
817
- if (this.abortController) {
818
- this.abortController.abort();
819
- logger.system("Stop requested — aborting query via AbortController");
820
- }
821
- // IMPORTANT: Do NOT tear down the idle pre-warmed subprocess here.
822
- // requestStop() is typically called to end the *current* turn (user
823
- // /stop, watchdog timeout, stream silence, obs UI button); the user
824
- // will usually start a new task shortly after. Preserving the idle
825
- // warm lets that next task skip cold-start (~2-3s).
826
- //
827
- // The warm is a fully separate subprocess with its own AbortController
828
- // — aborting `this.abortController` above does not touch it.
829
- //
830
- // For graceful shutdown: the warm is a child process of ours and will
831
- // be reaped when we exit; the refresh timer uses `.unref()` so it
832
- // doesn't block process exit.
833
- this.closeInput();
834
- }
835
- isStopRequested() {
836
- return this.stopRequested;
837
- }
838
- clearStop() {
839
- this.stopRequested = false;
840
- // Only null the *live query's* abort controller. The warm subprocess
841
- // has its own abort controller that must survive the stop→clearStop
842
- // cycle — sendAndStream() adopts it when consuming the warm.
843
- this.abortController = null;
844
- }
845
- /**
846
- * Update the persisted session ID.
847
- *
848
- * Only saves on first capture (null → id). Once set, a different ID from
849
- * the SDK is ignored — it means the resume failed and the SDK created a
850
- * throwaway conversation. Persisting it would corrupt session.json and
851
- * orphan the real transcript.
852
- */
853
- captureSessionId(id) {
854
- if (!id || this.sessionId)
855
- return;
856
- this.sessionId = id;
857
- saveSessionId(id, this.mode);
858
- }
859
- captureTranscriptPath(p) {
860
- if (p && typeof p === "string") {
861
- this.transcriptPath = p;
862
- }
863
- }
864
- getSessionId() {
865
- return this.sessionId;
866
- }
867
- pendingResetSummary = null;
868
- resetForNewSession(summary) {
869
- this.warmGeneration++;
870
- this.warmUpPromise = null;
871
- this.sessionId = null;
872
- this.transcriptPath = null;
873
- this.lastUsageSnapshot = null;
874
- this.pendingResetSummary = summary ?? null;
875
- // Discard warm query since it's bound to the old session
876
- this.discardWarmQuery();
877
- logger.system(`[session] reset for new session (mode=${this.mode})`);
878
- }
879
- /**
880
- * If a reset summary is pending, consume and return it (clears the pending state).
881
- */
882
- consumeResetSummary() {
883
- const s = this.pendingResetSummary;
884
- this.pendingResetSummary = null;
885
- return s;
886
- }
887
- /**
888
- * Proactively request compaction by running the /compact slash command as a
889
- * separate one-turn query resumed from the current session.
890
- *
891
- * This is more reliable than injecting "/compact" into the streaming prompt
892
- * because the running query may treat it as ordinary text.
893
- */
894
- async requestCompaction() {
895
- if (this.compactInFlight)
896
- return;
897
- this.compactInFlight = true;
898
- try {
899
- getAgentState().compacting = true;
900
- }
901
- catch {
902
- // Agent state not yet initialized
903
- }
904
- // TODO: maybe should use reply map.
905
- const sendCompactNotice = (text) => {
906
- try {
907
- const state = getAgentState();
908
- const { ownerConfig, channelManager } = state;
909
- const channel = ownerConfig.telegramChatId ? "telegram" : ownerConfig.ownerEmail ? "gmail" : undefined;
910
- const recipient = ownerConfig.telegramChatId ? String(ownerConfig.telegramChatId) : ownerConfig.ownerEmail;
911
- if (channel && recipient) {
912
- channelManager
913
- .sendMessage(channel, recipient, text)
914
- .catch((err) => {
915
- logger.warn(`Failed to send compaction notification: ${err instanceof Error ? err.message : String(err)}`);
916
- });
917
- }
918
- }
919
- catch {
920
- // Agent state not yet initialized — skip notification
921
- }
922
- };
923
- try {
924
- const savedSessionId = this.sessionId;
925
- if (!savedSessionId) {
926
- logger.warn("Cannot request /compact: session_id not available yet");
927
- return;
928
- }
929
- // Resolve the Claude Code executable — same lookup as sendAndStream().
930
- const cliResolution = resolveClaudeCliExecutable();
931
- const options = {
932
- model: getModelId(this.config),
933
- ...(cliResolution.kind !== "sdk-default"
934
- ? { pathToClaudeCodeExecutable: cliResolution.path }
935
- : {}),
936
- systemPrompt: this.buildSystemPrompt(),
937
- permissionMode: "bypassPermissions",
938
- allowDangerouslySkipPermissions: true,
939
- cwd: this.sessionCwd,
940
- settingSources: ["project"],
941
- mcpServers: this.buildMcpServers(),
942
- env: buildAgentEnv(this.config),
943
- resume: savedSessionId,
944
- maxTurns: 1,
945
- };
946
- logger.system("Requesting /compact...");
947
- const isSilent = getAgentState().ownerConfig.silentCompaction;
948
- if (!isSilent) {
949
- sendCompactNotice("My brain is getting full... Let me tidy up my thoughts. This may take a minute — I might forget some older details.");
950
- }
951
- const cliLabel = cliResolution.kind === "sdk-default"
952
- ? "sdk-default"
953
- : `${cliResolution.kind}:${cliResolution.path}`;
954
- logger.debug(`Compaction query options: cli=${cliLabel} cwd=${this.sessionCwd} resume=${savedSessionId}`);
955
- // Sanitize the transcript before compaction — the compact subprocess
956
- // loads the file on startup, so any corrupt entries must be fixed first.
957
- const tp = this.transcriptPath ?? (savedSessionId ? this.deriveTranscriptPath(savedSessionId) : null);
958
- if (tp) {
959
- try {
960
- await imagePruner.sanitizeTranscript(tp);
961
- }
962
- catch (sanitizeErr) {
963
- logger.warn(`Pre-compact sanitization failed: ${sanitizeErr instanceof Error ? sanitizeErr.message : String(sanitizeErr)}`);
964
- }
965
- }
966
- // Guard against compaction hanging forever — abort after 5 minutes.
967
- const COMPACTION_TIMEOUT_MS = 5 * 60_000;
968
- const compactAbort = new AbortController();
969
- const compactTimer = setTimeout(() => {
970
- logger.warn(`[compact] Compaction timed out after ${COMPACTION_TIMEOUT_MS / 1000}s — aborting`);
971
- compactAbort.abort();
972
- }, COMPACTION_TIMEOUT_MS);
973
- try {
974
- for await (const msg of query({
975
- prompt: "/compact",
976
- options: { ...options, abortController: compactAbort },
977
- })) {
978
- if (msg.type === "system" && msg.subtype === "compact_boundary") {
979
- // Capture session id (should remain the same) and log compaction metadata.
980
- if ("session_id" in msg && msg.session_id) {
981
- this.captureSessionId(msg.session_id);
982
- }
983
- const meta = msg.compact_metadata;
984
- logger.system(`Compaction completed (trigger=${meta?.trigger ?? "unknown"}, pre_tokens=${meta?.pre_tokens ?? "?"})`);
985
- if (typeof meta?.pre_tokens === "number") {
986
- this.capturePostCompactionSnapshot(meta.pre_tokens);
987
- }
988
- else if (this.lastUsageSnapshot) {
989
- // No token data available, but still reset the image count
990
- // so maybeCompact() doesn't re-trigger on the stale value.
991
- this.captureUsageSnapshot({
992
- ...this.lastUsageSnapshot,
993
- allImageBlocks: 0,
994
- });
995
- }
996
- if (!isSilent) {
997
- sendCompactNotice("Brain cleanup done! Ready to continue.");
998
- }
999
- // Trim the JSONL transcript to remove pre-compaction content.
1000
- // This keeps the live file small for faster resumes and pruning.
1001
- const tp = this.transcriptPath;
1002
- if (tp) {
1003
- try {
1004
- const archivePath = sessionTrimmer.deriveArchivePath(tp);
1005
- await sessionTrimmer.trimTranscriptAfterCompaction({
1006
- transcriptPath: tp,
1007
- archivePath,
1008
- });
1009
- }
1010
- catch (trimErr) {
1011
- logger.warn(`Post-compaction JSONL trim failed: ${trimErr instanceof Error ? trimErr.message : String(trimErr)}`);
1012
- }
1013
- }
1014
- }
1015
- }
1016
- }
1017
- finally {
1018
- clearTimeout(compactTimer);
1019
- }
1020
- }
1021
- catch (err) {
1022
- logger.warn(`Request /compact failed: ${err instanceof Error ? err.message : String(err)}`);
1023
- }
1024
- finally {
1025
- this.compactInFlight = false;
1026
- try {
1027
- getAgentState().compacting = false;
1028
- }
1029
- catch {
1030
- // Agent state not yet initialized
1031
- }
1032
- }
1033
- }
1034
- captureUsageSnapshot(snapshot) {
1035
- // Merge with previous snapshot so callers that only update token data
1036
- // don't clobber allImageBlocks, and vice-versa.
1037
- this.lastUsageSnapshot = {
1038
- ...this.lastUsageSnapshot,
1039
- ...snapshot,
1040
- capturedAtMs: snapshot.capturedAtMs ?? Date.now(),
1041
- };
1042
- saveUsageSnapshot(this.lastUsageSnapshot, this.mode);
1043
- }
1044
- /**
1045
- * Update the usage snapshot after the SDK performs mid-query auto-compaction.
1046
- * Uses `pre_tokens` (the post-compaction token count) and the last known
1047
- * context window to compute an accurate `usedPct`, preventing the post-query
1048
- * `maybeCompact` from triggering a redundant compaction.
1049
- */
1050
- capturePostCompactionSnapshot(postCompactionTokens) {
1051
- const contextWindow = this.lastUsageSnapshot?.contextWindow ?? 0;
1052
- if (contextWindow <= 0 || postCompactionTokens < 0) {
1053
- this.lastUsageSnapshot = null;
1054
- return;
1055
- }
1056
- this.captureUsageSnapshot({
1057
- usedInputTokens: postCompactionTokens,
1058
- contextWindow,
1059
- usedPct: postCompactionTokens / contextWindow,
1060
- allImageBlocks: 0,
1061
- });
1062
- }
1063
- /**
1064
- * Unified between-turn compaction gate. Triggers compaction when **either**:
1065
- * 1. Token usage exceeds the context-window threshold, OR
1066
- * 2. Total image blocks (real + placeholder) in the transcript reach
1067
- * `imageThreshold`, approaching Claude API's 100-image limit.
1068
- *
1069
- * Both triggers share the same cooldown and in-flight guard so we never
1070
- * run two back-to-back compactions in the same between-turn pass.
1071
- */
1072
- async maybeCompact(options) {
1073
- const explicitUsedPctThreshold = options?.usedPctThreshold;
1074
- const cooldownMs = options?.cooldownMs ?? 10 * 60 * 1000;
1075
- const imageThreshold = options?.imageThreshold ?? IMAGE_COMPACT_THRESHOLD;
1076
- // --- guard: in-flight ---
1077
- if (this.compactInFlight) {
1078
- logger.debug("[compact] skip: compaction already in flight");
1079
- return;
1080
- }
1081
- // --- guard: cooldown ---
1082
- const nowMs = Date.now();
1083
- const sinceLastCompactMs = nowMs - this.lastCompactRequestAtMs;
1084
- if (sinceLastCompactMs <= cooldownMs) {
1085
- logger.debug(`[compact] skip: cooldown active sinceLast=${sinceLastCompactMs}ms cooldown=${cooldownMs}ms`);
1086
- return;
1087
- }
1088
- // --- trigger 1: token usage ---
1089
- const snap = this.lastUsageSnapshot;
1090
- // Caller-supplied override always wins. Otherwise auto-scale by
1091
- // context window, falling back to the large-window default when no
1092
- // snapshot is available yet.
1093
- const effectiveUsedPctThreshold = explicitUsedPctThreshold ??
1094
- (snap
1095
- ? getCompactUsedPctThreshold(snap.contextWindow, COMPACT_LARGE_WINDOW_USED_PCT_THRESHOLD)
1096
- : COMPACT_LARGE_WINDOW_USED_PCT_THRESHOLD);
1097
- let tokenTrigger = false;
1098
- if (snap) {
1099
- const snapshotAgeMs = nowMs - (snap.capturedAtMs ?? 0);
1100
- logger.debug(`[compact] check: usedPct=${(snap.usedPct * 100).toFixed(1)}% threshold=${(effectiveUsedPctThreshold * 100).toFixed(1)}% used=${snap.usedInputTokens} window=${snap.contextWindow} snapshotAge=${snapshotAgeMs}ms`);
1101
- tokenTrigger = snap.contextWindow > 0 && snap.usedPct >= effectiveUsedPctThreshold;
1102
- }
1103
- // --- trigger 2: image count ---
1104
- const allImages = snap?.allImageBlocks ?? 0;
1105
- const imageTrigger = allImages >= imageThreshold;
1106
- if (!tokenTrigger && !imageTrigger) {
1107
- if (snap) {
1108
- logger.debug(`[compact] skip: under threshold (usedPct=${(snap.usedPct * 100).toFixed(1)}% threshold=${(effectiveUsedPctThreshold * 100).toFixed(1)}%, images=${allImages}/${imageThreshold})`);
1109
- }
1110
- else {
1111
- logger.debug(`[compact] skip: no usage snapshot yet (images=${allImages}/${imageThreshold})`);
1112
- }
1113
- return;
1114
- }
1115
- const reason = [tokenTrigger && "tokens", imageTrigger && `images(${allImages}>=${imageThreshold})`]
1116
- .filter(Boolean)
1117
- .join("+");
1118
- logger.system(`[compact] requesting compaction (reason=${reason})`);
1119
- this.lastCompactRequestAtMs = nowMs;
1120
- await this.requestCompaction();
1121
- }
1122
- getTranscriptPath() {
1123
- return this.transcriptPath;
1124
- }
1125
- /**
1126
- * Derive the transcript path from the session ID and config dir, even before
1127
- * the SDK subprocess has started and reported it via hooks.
1128
- */
1129
- deriveTranscriptPath(sessionId) {
1130
- const dir = getClaudeTranscriptDirForProfileDir(getConfigDir());
1131
- return path.join(dir, `${sessionId}.jsonl`);
1132
- }
1133
- getUsageSnapshot() {
1134
- return this.lastUsageSnapshot ? { ...this.lastUsageSnapshot } : null;
1135
- }
1136
- /** Returns the live Query object, or null if no query is active. */
1137
- getCurrentQuery() {
1138
- return this.currentQuery;
1139
- }
1140
- /**
1141
- * Validation adapter around `Query.getContextUsage()` (SDK 0.2.114+).
1142
- *
1143
- * The SDK's `Query` interface exposes `getContextUsage()` as a control
1144
- * request over the streaming bridge. It returns a rich breakdown:
1145
- *
1146
- * ```ts
1147
- * {
1148
- * categories: { name: string; tokens: number; color: string; isDeferred?: boolean }[];
1149
- * totalTokens: number;
1150
- * maxTokens: number; // context window for the *active* model
1151
- * rawMaxTokens: number; // nominal max before any reductions
1152
- * percentage: number; // UI percentage (0..100)
1153
- * gridRows: ...[]; // TUI-only
1154
- * model: string;
1155
- * memoryFiles: { path: string; type: string; tokens: number }[];
1156
- * mcpTools: { name: string; serverName: string; tokens: number }[];
1157
- * }
1158
- * ```
1159
- *
1160
- * We map it onto our existing {@link UsageSnapshot} shape:
1161
- * - `usedInputTokens` ← `totalTokens`
1162
- * - `contextWindow` ← `maxTokens` (post-reduction; matches what the
1163
- * model actually has to work with right now)
1164
- * - `usedPct` ← computed from the two above, NOT from the
1165
- * SDK's `percentage` field. The SDK's value is
1166
- * UI-scale (0..100); our consumers expect 0..1.
1167
- * Computing locally keeps both fields internally
1168
- * consistent.
1169
- * - `allImageBlocks` — intentionally preserved from any existing
1170
- * snapshot; `getContextUsage` doesn't know about
1171
- * raw image-block counts in the transcript, so
1172
- * we don't clobber the value that
1173
- * {@link captureUsageSnapshot} maintains.
1174
- *
1175
- * This is purely a read adapter — no callers in compaction or the
1176
- * wake loop should depend on it yet. It exists to let us sanity-check
1177
- * the live SDK response before wiring any behavior changes around it.
1178
- * Returns null when no live query is active, when the method is absent
1179
- * on the active Query instance (e.g. an older CLI), or when the call
1180
- * errors transiently.
1181
- */
1182
- async getLiveContextUsage() {
1183
- const q = this.currentQuery;
1184
- if (!q) {
1185
- logger.debug("[getLiveContextUsage] no active query");
1186
- return null;
1187
- }
1188
- // The typed interface declares the method, but runtime code may
1189
- // predate it if the user pins an older CLI via
1190
- // `pathToClaudeCodeExecutable`. Probe defensively.
1191
- const candidate = q.getContextUsage;
1192
- if (typeof candidate !== "function") {
1193
- logger.debug("[getLiveContextUsage] query.getContextUsage unavailable");
1194
- return null;
1195
- }
1196
- try {
1197
- const raw = (await candidate.call(q));
1198
- // logger.debug(
1199
- // `[getContextUsage] raw: totalTokens=${String(raw.totalTokens)} maxTokens=${String(raw.maxTokens)}`,
1200
- // );
1201
- const totalTokens = typeof raw.totalTokens === "number" ? raw.totalTokens : NaN;
1202
- const maxTokens = typeof raw.maxTokens === "number" ? raw.maxTokens : NaN;
1203
- if (!Number.isFinite(totalTokens) || !Number.isFinite(maxTokens) || maxTokens <= 0) {
1204
- logger.debug(`[getLiveContextUsage] unexpected shape: totalTokens=${String(raw.totalTokens)} maxTokens=${String(raw.maxTokens)}`);
1205
- return null;
1206
- }
1207
- const usage = {
1208
- usedInputTokens: totalTokens,
1209
- contextWindow: maxTokens,
1210
- usedPct: totalTokens / maxTokens,
1211
- allImageBlocks: this.lastUsageSnapshot?.allImageBlocks,
1212
- capturedAtMs: Date.now(),
1213
- };
1214
- // logger.debug(
1215
- // `[getLiveContextUsage] snapshot: usedPct=${(usage.usedPct * 100).toFixed(1)}% used=${usage.usedInputTokens} window=${usage.contextWindow}`,
1216
- // );
1217
- return usage;
1218
- }
1219
- catch (err) {
1220
- logger.debug(`[getLiveContextUsage] call failed: ${err instanceof Error ? err.message : String(err)}`);
1221
- return null;
1222
- }
1223
- }
1224
- /** Replace the full set of dynamic MCP servers (used when loading from disk). */
1225
- setDynamicMcpServers(servers) {
1226
- this.dynamicMcpServers = { ...servers };
1227
- }
1228
- /** Get the current dynamic MCP servers record. */
1229
- getDynamicMcpServers() {
1230
- return { ...this.dynamicMcpServers };
1231
- }
1232
- }
1233
- //# sourceMappingURL=session.js.map