charness 0.0.0-stage → 3.1.0

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 (1044) hide show
  1. package/.env.example +88 -0
  2. package/DEVELOPMENT.md +110 -0
  3. package/LICENSE +21 -0
  4. package/MIGRATION_1_TO_2.md +24 -0
  5. package/README.md +1196 -2
  6. package/dist/accessControl.d.ts +71 -0
  7. package/dist/accessControl.d.ts.map +1 -0
  8. package/dist/accessControl.js +108 -0
  9. package/dist/accessControl.js.map +1 -0
  10. package/dist/adapters/adapterEventRouting.d.ts +51 -0
  11. package/dist/adapters/adapterEventRouting.d.ts.map +1 -0
  12. package/dist/adapters/adapterEventRouting.js +74 -0
  13. package/dist/adapters/adapterEventRouting.js.map +1 -0
  14. package/dist/adapters/adapterNames.d.ts +19 -0
  15. package/dist/adapters/adapterNames.d.ts.map +1 -0
  16. package/dist/adapters/adapterNames.js +25 -0
  17. package/dist/adapters/adapterNames.js.map +1 -0
  18. package/dist/adapters/claudeCliAdapter.d.ts +1351 -0
  19. package/dist/adapters/claudeCliAdapter.d.ts.map +1 -0
  20. package/dist/adapters/claudeCliAdapter.js +4239 -0
  21. package/dist/adapters/claudeCliAdapter.js.map +1 -0
  22. package/dist/adapters/claudeConfigDir.d.ts +40 -0
  23. package/dist/adapters/claudeConfigDir.d.ts.map +1 -0
  24. package/dist/adapters/claudeConfigDir.js +3 -0
  25. package/dist/adapters/claudeConfigDir.js.map +1 -0
  26. package/dist/adapters/claudeJsonStreamAdapter.d.ts +580 -0
  27. package/dist/adapters/claudeJsonStreamAdapter.d.ts.map +1 -0
  28. package/dist/adapters/claudeJsonStreamAdapter.js +1891 -0
  29. package/dist/adapters/claudeJsonStreamAdapter.js.map +1 -0
  30. package/dist/adapters/claudePlatformFlags.d.ts +35 -0
  31. package/dist/adapters/claudePlatformFlags.d.ts.map +1 -0
  32. package/dist/adapters/claudePlatformFlags.js +46 -0
  33. package/dist/adapters/claudePlatformFlags.js.map +1 -0
  34. package/dist/adapters/createAdapter.d.ts +260 -0
  35. package/dist/adapters/createAdapter.d.ts.map +1 -0
  36. package/dist/adapters/createAdapter.js +512 -0
  37. package/dist/adapters/createAdapter.js.map +1 -0
  38. package/dist/adapters/openCodeAdapter.d.ts +1469 -0
  39. package/dist/adapters/openCodeAdapter.d.ts.map +1 -0
  40. package/dist/adapters/openCodeAdapter.js +4623 -0
  41. package/dist/adapters/openCodeAdapter.js.map +1 -0
  42. package/dist/adapters/sessionLaunchDefaults.d.ts +28 -0
  43. package/dist/adapters/sessionLaunchDefaults.d.ts.map +1 -0
  44. package/dist/adapters/sessionLaunchDefaults.js +15 -0
  45. package/dist/adapters/sessionLaunchDefaults.js.map +1 -0
  46. package/dist/adapters/terminalAdapter.d.ts +68 -0
  47. package/dist/adapters/terminalAdapter.d.ts.map +1 -0
  48. package/dist/adapters/terminalAdapter.js +371 -0
  49. package/dist/adapters/terminalAdapter.js.map +1 -0
  50. package/dist/agentLogin/agentLogin.d.ts +44 -0
  51. package/dist/agentLogin/agentLogin.d.ts.map +1 -0
  52. package/dist/agentLogin/agentLogin.js +530 -0
  53. package/dist/agentLogin/agentLogin.js.map +1 -0
  54. package/dist/agentTrigger.d.ts +28 -0
  55. package/dist/agentTrigger.d.ts.map +1 -0
  56. package/dist/agentTrigger.js +53 -0
  57. package/dist/agentTrigger.js.map +1 -0
  58. package/dist/apiErrorRetry.d.ts +214 -0
  59. package/dist/apiErrorRetry.d.ts.map +1 -0
  60. package/dist/apiErrorRetry.js +382 -0
  61. package/dist/apiErrorRetry.js.map +1 -0
  62. package/dist/apiRetryKick.d.ts +157 -0
  63. package/dist/apiRetryKick.d.ts.map +1 -0
  64. package/dist/apiRetryKick.js +174 -0
  65. package/dist/apiRetryKick.js.map +1 -0
  66. package/dist/bootClassifier.d.ts +46 -0
  67. package/dist/bootClassifier.d.ts.map +1 -0
  68. package/dist/bootClassifier.js +43 -0
  69. package/dist/bootClassifier.js.map +1 -0
  70. package/dist/bot.d.ts +207 -0
  71. package/dist/bot.d.ts.map +1 -0
  72. package/dist/bot.js +11226 -0
  73. package/dist/bot.js.map +1 -0
  74. package/dist/botFileStorage.d.ts +60 -0
  75. package/dist/botFileStorage.d.ts.map +1 -0
  76. package/dist/botFileStorage.js +205 -0
  77. package/dist/botFileStorage.js.map +1 -0
  78. package/dist/cli/applyDnsFix.d.ts +7 -0
  79. package/dist/cli/applyDnsFix.d.ts.map +1 -0
  80. package/dist/cli/applyDnsFix.js +46 -0
  81. package/dist/cli/applyDnsFix.js.map +1 -0
  82. package/dist/cli/bot.d.ts +27 -0
  83. package/dist/cli/bot.d.ts.map +1 -0
  84. package/dist/cli/bot.js +98 -0
  85. package/dist/cli/bot.js.map +1 -0
  86. package/dist/cli/botEntry.d.ts +3 -0
  87. package/dist/cli/botEntry.d.ts.map +1 -0
  88. package/dist/cli/botEntry.js +11 -0
  89. package/dist/cli/botEntry.js.map +1 -0
  90. package/dist/cli/claudeProfiles.d.ts +42 -0
  91. package/dist/cli/claudeProfiles.d.ts.map +1 -0
  92. package/dist/cli/claudeProfiles.js +106 -0
  93. package/dist/cli/claudeProfiles.js.map +1 -0
  94. package/dist/cli/connectorGuards.d.ts +31 -0
  95. package/dist/cli/connectorGuards.d.ts.map +1 -0
  96. package/dist/cli/connectorGuards.js +159 -0
  97. package/dist/cli/connectorGuards.js.map +1 -0
  98. package/dist/cli/envLoader.d.ts +34 -0
  99. package/dist/cli/envLoader.d.ts.map +1 -0
  100. package/dist/cli/envLoader.js +132 -0
  101. package/dist/cli/envLoader.js.map +1 -0
  102. package/dist/cli/hot.d.ts +82 -0
  103. package/dist/cli/hot.d.ts.map +1 -0
  104. package/dist/cli/hot.js +323 -0
  105. package/dist/cli/hot.js.map +1 -0
  106. package/dist/cli.d.ts +3 -0
  107. package/dist/cli.d.ts.map +1 -0
  108. package/dist/cli.js +140 -0
  109. package/dist/cli.js.map +1 -0
  110. package/dist/connectors/jira/adf.d.ts +74 -0
  111. package/dist/connectors/jira/adf.d.ts.map +1 -0
  112. package/dist/connectors/jira/adf.js +412 -0
  113. package/dist/connectors/jira/adf.js.map +1 -0
  114. package/dist/connectors/jira/agentBinaries.d.ts +14 -0
  115. package/dist/connectors/jira/agentBinaries.d.ts.map +1 -0
  116. package/dist/connectors/jira/agentBinaries.js +75 -0
  117. package/dist/connectors/jira/agentBinaries.js.map +1 -0
  118. package/dist/connectors/jira/answerSink.d.ts +59 -0
  119. package/dist/connectors/jira/answerSink.d.ts.map +1 -0
  120. package/dist/connectors/jira/answerSink.js +234 -0
  121. package/dist/connectors/jira/answerSink.js.map +1 -0
  122. package/dist/connectors/jira/attachmentDownload.d.ts +41 -0
  123. package/dist/connectors/jira/attachmentDownload.d.ts.map +1 -0
  124. package/dist/connectors/jira/attachmentDownload.js +92 -0
  125. package/dist/connectors/jira/attachmentDownload.js.map +1 -0
  126. package/dist/connectors/jira/attachmentTool.d.ts +50 -0
  127. package/dist/connectors/jira/attachmentTool.d.ts.map +1 -0
  128. package/dist/connectors/jira/attachmentTool.js +166 -0
  129. package/dist/connectors/jira/attachmentTool.js.map +1 -0
  130. package/dist/connectors/jira/client.d.ts +522 -0
  131. package/dist/connectors/jira/client.d.ts.map +1 -0
  132. package/dist/connectors/jira/client.js +491 -0
  133. package/dist/connectors/jira/client.js.map +1 -0
  134. package/dist/connectors/jira/config.d.ts +104 -0
  135. package/dist/connectors/jira/config.d.ts.map +1 -0
  136. package/dist/connectors/jira/config.js +323 -0
  137. package/dist/connectors/jira/config.js.map +1 -0
  138. package/dist/connectors/jira/configFile.d.ts +10 -0
  139. package/dist/connectors/jira/configFile.d.ts.map +1 -0
  140. package/dist/connectors/jira/configFile.js +50 -0
  141. package/dist/connectors/jira/configFile.js.map +1 -0
  142. package/dist/connectors/jira/connector.d.ts +71 -0
  143. package/dist/connectors/jira/connector.d.ts.map +1 -0
  144. package/dist/connectors/jira/connector.js +312 -0
  145. package/dist/connectors/jira/connector.js.map +1 -0
  146. package/dist/connectors/jira/contextLedger.d.ts +60 -0
  147. package/dist/connectors/jira/contextLedger.d.ts.map +1 -0
  148. package/dist/connectors/jira/contextLedger.js +196 -0
  149. package/dist/connectors/jira/contextLedger.js.map +1 -0
  150. package/dist/connectors/jira/contextPreamble.d.ts +18 -0
  151. package/dist/connectors/jira/contextPreamble.d.ts.map +1 -0
  152. package/dist/connectors/jira/contextPreamble.js +31 -0
  153. package/dist/connectors/jira/contextPreamble.js.map +1 -0
  154. package/dist/connectors/jira/inbound.d.ts +141 -0
  155. package/dist/connectors/jira/inbound.d.ts.map +1 -0
  156. package/dist/connectors/jira/inbound.js +331 -0
  157. package/dist/connectors/jira/inbound.js.map +1 -0
  158. package/dist/connectors/jira/issueBlocks.d.ts +74 -0
  159. package/dist/connectors/jira/issueBlocks.d.ts.map +1 -0
  160. package/dist/connectors/jira/issueBlocks.js +257 -0
  161. package/dist/connectors/jira/issueBlocks.js.map +1 -0
  162. package/dist/connectors/jira/issueContext.d.ts +31 -0
  163. package/dist/connectors/jira/issueContext.d.ts.map +1 -0
  164. package/dist/connectors/jira/issueContext.js +70 -0
  165. package/dist/connectors/jira/issueContext.js.map +1 -0
  166. package/dist/connectors/jira/issueDelta.d.ts +38 -0
  167. package/dist/connectors/jira/issueDelta.d.ts.map +1 -0
  168. package/dist/connectors/jira/issueDelta.js +59 -0
  169. package/dist/connectors/jira/issueDelta.js.map +1 -0
  170. package/dist/connectors/jira/mediaPlaceholders.d.ts +22 -0
  171. package/dist/connectors/jira/mediaPlaceholders.d.ts.map +1 -0
  172. package/dist/connectors/jira/mediaPlaceholders.js +105 -0
  173. package/dist/connectors/jira/mediaPlaceholders.js.map +1 -0
  174. package/dist/connectors/jira/outbound.d.ts +6 -0
  175. package/dist/connectors/jira/outbound.d.ts.map +1 -0
  176. package/dist/connectors/jira/outbound.js +38 -0
  177. package/dist/connectors/jira/outbound.js.map +1 -0
  178. package/dist/connectors/jira/prompt.d.ts +39 -0
  179. package/dist/connectors/jira/prompt.d.ts.map +1 -0
  180. package/dist/connectors/jira/prompt.js +102 -0
  181. package/dist/connectors/jira/prompt.js.map +1 -0
  182. package/dist/connectors/jira/promptSpill.d.ts +50 -0
  183. package/dist/connectors/jira/promptSpill.d.ts.map +1 -0
  184. package/dist/connectors/jira/promptSpill.js +157 -0
  185. package/dist/connectors/jira/promptSpill.js.map +1 -0
  186. package/dist/connectors/jira/promptText.d.ts +7 -0
  187. package/dist/connectors/jira/promptText.d.ts.map +1 -0
  188. package/dist/connectors/jira/promptText.js +26 -0
  189. package/dist/connectors/jira/promptText.js.map +1 -0
  190. package/dist/connectors/jira/sessionKeyCodec.d.ts +18 -0
  191. package/dist/connectors/jira/sessionKeyCodec.d.ts.map +1 -0
  192. package/dist/connectors/jira/sessionKeyCodec.js +106 -0
  193. package/dist/connectors/jira/sessionKeyCodec.js.map +1 -0
  194. package/dist/connectors/jira/trigger.d.ts +54 -0
  195. package/dist/connectors/jira/trigger.d.ts.map +1 -0
  196. package/dist/connectors/jira/trigger.js +101 -0
  197. package/dist/connectors/jira/trigger.js.map +1 -0
  198. package/dist/connectors/jira/triggerLog.d.ts +53 -0
  199. package/dist/connectors/jira/triggerLog.d.ts.map +1 -0
  200. package/dist/connectors/jira/triggerLog.js +78 -0
  201. package/dist/connectors/jira/triggerLog.js.map +1 -0
  202. package/dist/connectors/jira/trustedGroup.d.ts +54 -0
  203. package/dist/connectors/jira/trustedGroup.d.ts.map +1 -0
  204. package/dist/connectors/jira/trustedGroup.js +98 -0
  205. package/dist/connectors/jira/trustedGroup.js.map +1 -0
  206. package/dist/connectors/jira/unconfirmedPosts.d.ts +58 -0
  207. package/dist/connectors/jira/unconfirmedPosts.d.ts.map +1 -0
  208. package/dist/connectors/jira/unconfirmedPosts.js +105 -0
  209. package/dist/connectors/jira/unconfirmedPosts.js.map +1 -0
  210. package/dist/connectors/telegram/answerSink.d.ts +46 -0
  211. package/dist/connectors/telegram/answerSink.d.ts.map +1 -0
  212. package/dist/connectors/telegram/answerSink.js +71 -0
  213. package/dist/connectors/telegram/answerSink.js.map +1 -0
  214. package/dist/connectors/telegram/commands/autoContinueLimits.d.ts +34 -0
  215. package/dist/connectors/telegram/commands/autoContinueLimits.d.ts.map +1 -0
  216. package/dist/connectors/telegram/commands/autoContinueLimits.js +112 -0
  217. package/dist/connectors/telegram/commands/autoContinueLimits.js.map +1 -0
  218. package/dist/connectors/telegram/commands/botCore.d.ts +46 -0
  219. package/dist/connectors/telegram/commands/botCore.d.ts.map +1 -0
  220. package/dist/connectors/telegram/commands/botCore.js +3 -0
  221. package/dist/connectors/telegram/commands/botCore.js.map +1 -0
  222. package/dist/connectors/telegram/commands/callbackMessage.d.ts +12 -0
  223. package/dist/connectors/telegram/commands/callbackMessage.d.ts.map +1 -0
  224. package/dist/connectors/telegram/commands/callbackMessage.js +13 -0
  225. package/dist/connectors/telegram/commands/callbackMessage.js.map +1 -0
  226. package/dist/connectors/telegram/commands/claudeAccounts.d.ts +58 -0
  227. package/dist/connectors/telegram/commands/claudeAccounts.d.ts.map +1 -0
  228. package/dist/connectors/telegram/commands/claudeAccounts.js +591 -0
  229. package/dist/connectors/telegram/commands/claudeAccounts.js.map +1 -0
  230. package/dist/connectors/telegram/commands/compaction.d.ts +47 -0
  231. package/dist/connectors/telegram/commands/compaction.d.ts.map +1 -0
  232. package/dist/connectors/telegram/commands/compaction.js +868 -0
  233. package/dist/connectors/telegram/commands/compaction.js.map +1 -0
  234. package/dist/connectors/telegram/commands/displayModes.d.ts +13 -0
  235. package/dist/connectors/telegram/commands/displayModes.d.ts.map +1 -0
  236. package/dist/connectors/telegram/commands/displayModes.js +335 -0
  237. package/dist/connectors/telegram/commands/displayModes.js.map +1 -0
  238. package/dist/connectors/telegram/commands/fallbackChain.d.ts +49 -0
  239. package/dist/connectors/telegram/commands/fallbackChain.d.ts.map +1 -0
  240. package/dist/connectors/telegram/commands/fallbackChain.js +624 -0
  241. package/dist/connectors/telegram/commands/fallbackChain.js.map +1 -0
  242. package/dist/connectors/telegram/commands/modelProviders.d.ts +117 -0
  243. package/dist/connectors/telegram/commands/modelProviders.d.ts.map +1 -0
  244. package/dist/connectors/telegram/commands/modelProviders.js +966 -0
  245. package/dist/connectors/telegram/commands/modelProviders.js.map +1 -0
  246. package/dist/connectors/telegram/commands/reminders.d.ts +54 -0
  247. package/dist/connectors/telegram/commands/reminders.d.ts.map +1 -0
  248. package/dist/connectors/telegram/commands/reminders.js +624 -0
  249. package/dist/connectors/telegram/commands/reminders.js.map +1 -0
  250. package/dist/connectors/telegram/fileIntake.d.ts +166 -0
  251. package/dist/connectors/telegram/fileIntake.d.ts.map +1 -0
  252. package/dist/connectors/telegram/fileIntake.js +304 -0
  253. package/dist/connectors/telegram/fileIntake.js.map +1 -0
  254. package/dist/connectors/telegram/fileSendGateway.d.ts +57 -0
  255. package/dist/connectors/telegram/fileSendGateway.d.ts.map +1 -0
  256. package/dist/connectors/telegram/fileSendGateway.js +221 -0
  257. package/dist/connectors/telegram/fileSendGateway.js.map +1 -0
  258. package/dist/connectors/telegram/foreignKeyFallbacks.d.ts +52 -0
  259. package/dist/connectors/telegram/foreignKeyFallbacks.d.ts.map +1 -0
  260. package/dist/connectors/telegram/foreignKeyFallbacks.js +71 -0
  261. package/dist/connectors/telegram/foreignKeyFallbacks.js.map +1 -0
  262. package/dist/connectors/telegram/inbound.d.ts +131 -0
  263. package/dist/connectors/telegram/inbound.d.ts.map +1 -0
  264. package/dist/connectors/telegram/inbound.js +278 -0
  265. package/dist/connectors/telegram/inbound.js.map +1 -0
  266. package/dist/connectors/telegram/languagePicker.d.ts +32 -0
  267. package/dist/connectors/telegram/languagePicker.d.ts.map +1 -0
  268. package/dist/connectors/telegram/languagePicker.js +55 -0
  269. package/dist/connectors/telegram/languagePicker.js.map +1 -0
  270. package/dist/connectors/telegram/messageSplit.d.ts +62 -0
  271. package/dist/connectors/telegram/messageSplit.d.ts.map +1 -0
  272. package/dist/connectors/telegram/messageSplit.js +119 -0
  273. package/dist/connectors/telegram/messageSplit.js.map +1 -0
  274. package/dist/connectors/telegram/outbound.d.ts +62 -0
  275. package/dist/connectors/telegram/outbound.d.ts.map +1 -0
  276. package/dist/connectors/telegram/outbound.js +116 -0
  277. package/dist/connectors/telegram/outbound.js.map +1 -0
  278. package/dist/connectors/telegram/output/createOutputTransport.d.ts +34 -0
  279. package/dist/connectors/telegram/output/createOutputTransport.d.ts.map +1 -0
  280. package/dist/connectors/telegram/output/createOutputTransport.js +77 -0
  281. package/dist/connectors/telegram/output/createOutputTransport.js.map +1 -0
  282. package/dist/connectors/telegram/output/dmOutputTransport.d.ts +47 -0
  283. package/dist/connectors/telegram/output/dmOutputTransport.d.ts.map +1 -0
  284. package/dist/connectors/telegram/output/dmOutputTransport.js +412 -0
  285. package/dist/connectors/telegram/output/dmOutputTransport.js.map +1 -0
  286. package/dist/connectors/telegram/outputBacklogGlue.d.ts +38 -0
  287. package/dist/connectors/telegram/outputBacklogGlue.d.ts.map +1 -0
  288. package/dist/connectors/telegram/outputBacklogGlue.js +49 -0
  289. package/dist/connectors/telegram/outputBacklogGlue.js.map +1 -0
  290. package/dist/connectors/telegram/outputFlushPlan.d.ts +93 -0
  291. package/dist/connectors/telegram/outputFlushPlan.d.ts.map +1 -0
  292. package/dist/connectors/telegram/outputFlushPlan.js +120 -0
  293. package/dist/connectors/telegram/outputFlushPlan.js.map +1 -0
  294. package/dist/connectors/telegram/questionKeyboards.d.ts +30 -0
  295. package/dist/connectors/telegram/questionKeyboards.d.ts.map +1 -0
  296. package/dist/connectors/telegram/questionKeyboards.js +37 -0
  297. package/dist/connectors/telegram/questionKeyboards.js.map +1 -0
  298. package/dist/connectors/telegram/renderAgentHtml.d.ts +27 -0
  299. package/dist/connectors/telegram/renderAgentHtml.d.ts.map +1 -0
  300. package/dist/connectors/telegram/renderAgentHtml.js +97 -0
  301. package/dist/connectors/telegram/renderAgentHtml.js.map +1 -0
  302. package/dist/connectors/telegram/sessionKeyCodec.d.ts +28 -0
  303. package/dist/connectors/telegram/sessionKeyCodec.d.ts.map +1 -0
  304. package/dist/connectors/telegram/sessionKeyCodec.js +107 -0
  305. package/dist/connectors/telegram/sessionKeyCodec.js.map +1 -0
  306. package/dist/connectors/telegram/telegramCallGuard.d.ts +19 -0
  307. package/dist/connectors/telegram/telegramCallGuard.d.ts.map +1 -0
  308. package/dist/connectors/telegram/telegramCallGuard.js +35 -0
  309. package/dist/connectors/telegram/telegramCallGuard.js.map +1 -0
  310. package/dist/connectors/telegram/timezonePicker.d.ts +85 -0
  311. package/dist/connectors/telegram/timezonePicker.d.ts.map +1 -0
  312. package/dist/connectors/telegram/timezonePicker.js +142 -0
  313. package/dist/connectors/telegram/timezonePicker.js.map +1 -0
  314. package/dist/connectors/telegram/updateDispatcher.d.ts +85 -0
  315. package/dist/connectors/telegram/updateDispatcher.d.ts.map +1 -0
  316. package/dist/connectors/telegram/updateDispatcher.js +159 -0
  317. package/dist/connectors/telegram/updateDispatcher.js.map +1 -0
  318. package/dist/connectors/test/connector.d.ts +52 -0
  319. package/dist/connectors/test/connector.d.ts.map +1 -0
  320. package/dist/connectors/test/connector.js +143 -0
  321. package/dist/connectors/test/connector.js.map +1 -0
  322. package/dist/connectors/test/sessionKeyCodec.d.ts +25 -0
  323. package/dist/connectors/test/sessionKeyCodec.d.ts.map +1 -0
  324. package/dist/connectors/test/sessionKeyCodec.js +50 -0
  325. package/dist/connectors/test/sessionKeyCodec.js.map +1 -0
  326. package/dist/diagLog.d.ts +6 -0
  327. package/dist/diagLog.d.ts.map +1 -0
  328. package/dist/diagLog.js +58 -0
  329. package/dist/diagLog.js.map +1 -0
  330. package/dist/effortLevels.d.ts +68 -0
  331. package/dist/effortLevels.d.ts.map +1 -0
  332. package/dist/effortLevels.js +114 -0
  333. package/dist/effortLevels.js.map +1 -0
  334. package/dist/folderName.d.ts +42 -0
  335. package/dist/folderName.d.ts.map +1 -0
  336. package/dist/folderName.js +51 -0
  337. package/dist/folderName.js.map +1 -0
  338. package/dist/i18n/de.d.ts +2 -0
  339. package/dist/i18n/de.d.ts.map +1 -0
  340. package/dist/i18n/de.js +665 -0
  341. package/dist/i18n/de.js.map +1 -0
  342. package/dist/i18n/en.d.ts +7 -0
  343. package/dist/i18n/en.d.ts.map +1 -0
  344. package/dist/i18n/en.js +694 -0
  345. package/dist/i18n/en.js.map +1 -0
  346. package/dist/i18n/es.d.ts +2 -0
  347. package/dist/i18n/es.d.ts.map +1 -0
  348. package/dist/i18n/es.js +665 -0
  349. package/dist/i18n/es.js.map +1 -0
  350. package/dist/i18n/fr.d.ts +2 -0
  351. package/dist/i18n/fr.d.ts.map +1 -0
  352. package/dist/i18n/fr.js +665 -0
  353. package/dist/i18n/fr.js.map +1 -0
  354. package/dist/i18n/hi.d.ts +2 -0
  355. package/dist/i18n/hi.d.ts.map +1 -0
  356. package/dist/i18n/hi.js +665 -0
  357. package/dist/i18n/hi.js.map +1 -0
  358. package/dist/i18n/ja.d.ts +2 -0
  359. package/dist/i18n/ja.d.ts.map +1 -0
  360. package/dist/i18n/ja.js +665 -0
  361. package/dist/i18n/ja.js.map +1 -0
  362. package/dist/i18n/ka.d.ts +2 -0
  363. package/dist/i18n/ka.d.ts.map +1 -0
  364. package/dist/i18n/ka.js +665 -0
  365. package/dist/i18n/ka.js.map +1 -0
  366. package/dist/i18n/pt.d.ts +2 -0
  367. package/dist/i18n/pt.d.ts.map +1 -0
  368. package/dist/i18n/pt.js +665 -0
  369. package/dist/i18n/pt.js.map +1 -0
  370. package/dist/i18n/ru.d.ts +6 -0
  371. package/dist/i18n/ru.d.ts.map +1 -0
  372. package/dist/i18n/ru.js +668 -0
  373. package/dist/i18n/ru.js.map +1 -0
  374. package/dist/i18n/uk.d.ts +8 -0
  375. package/dist/i18n/uk.d.ts.map +1 -0
  376. package/dist/i18n/uk.js +670 -0
  377. package/dist/i18n/uk.js.map +1 -0
  378. package/dist/i18n/uz.d.ts +2 -0
  379. package/dist/i18n/uz.d.ts.map +1 -0
  380. package/dist/i18n/uz.js +665 -0
  381. package/dist/i18n/uz.js.map +1 -0
  382. package/dist/i18n/zh.d.ts +2 -0
  383. package/dist/i18n/zh.d.ts.map +1 -0
  384. package/dist/i18n/zh.js +665 -0
  385. package/dist/i18n/zh.js.map +1 -0
  386. package/dist/i18n.d.ts +115 -0
  387. package/dist/i18n.d.ts.map +1 -0
  388. package/dist/i18n.js +233 -0
  389. package/dist/i18n.js.map +1 -0
  390. package/dist/index.d.ts +3 -0
  391. package/dist/index.d.ts.map +1 -0
  392. package/dist/index.js +10 -0
  393. package/dist/index.js.map +1 -0
  394. package/dist/installManager.d.ts +99 -0
  395. package/dist/installManager.d.ts.map +1 -0
  396. package/dist/installManager.js +943 -0
  397. package/dist/installManager.js.map +1 -0
  398. package/dist/instanceLock.d.ts +161 -0
  399. package/dist/instanceLock.d.ts.map +1 -0
  400. package/dist/instanceLock.js +388 -0
  401. package/dist/instanceLock.js.map +1 -0
  402. package/dist/mcpConfig.d.ts +76 -0
  403. package/dist/mcpConfig.d.ts.map +1 -0
  404. package/dist/mcpConfig.js +270 -0
  405. package/dist/mcpConfig.js.map +1 -0
  406. package/dist/openCodeOpenQuestion.d.ts +44 -0
  407. package/dist/openCodeOpenQuestion.d.ts.map +1 -0
  408. package/dist/openCodeOpenQuestion.js +64 -0
  409. package/dist/openCodeOpenQuestion.js.map +1 -0
  410. package/dist/openCodeQuestionFlow.d.ts +105 -0
  411. package/dist/openCodeQuestionFlow.d.ts.map +1 -0
  412. package/dist/openCodeQuestionFlow.js +143 -0
  413. package/dist/openCodeQuestionFlow.js.map +1 -0
  414. package/dist/openCodeSessionRouting.d.ts +133 -0
  415. package/dist/openCodeSessionRouting.d.ts.map +1 -0
  416. package/dist/openCodeSessionRouting.js +253 -0
  417. package/dist/openCodeSessionRouting.js.map +1 -0
  418. package/dist/openCodeSessionTitle.d.ts +50 -0
  419. package/dist/openCodeSessionTitle.d.ts.map +1 -0
  420. package/dist/openCodeSessionTitle.js +84 -0
  421. package/dist/openCodeSessionTitle.js.map +1 -0
  422. package/dist/outputTrace.d.ts +108 -0
  423. package/dist/outputTrace.d.ts.map +1 -0
  424. package/dist/outputTrace.js +430 -0
  425. package/dist/outputTrace.js.map +1 -0
  426. package/dist/pendingQuestionRepost.d.ts +47 -0
  427. package/dist/pendingQuestionRepost.d.ts.map +1 -0
  428. package/dist/pendingQuestionRepost.js +29 -0
  429. package/dist/pendingQuestionRepost.js.map +1 -0
  430. package/dist/pinnedStatus.d.ts +52 -0
  431. package/dist/pinnedStatus.d.ts.map +1 -0
  432. package/dist/pinnedStatus.js +36 -0
  433. package/dist/pinnedStatus.js.map +1 -0
  434. package/dist/platform/answerSink.d.ts +93 -0
  435. package/dist/platform/answerSink.d.ts.map +1 -0
  436. package/dist/platform/answerSink.js +31 -0
  437. package/dist/platform/answerSink.js.map +1 -0
  438. package/dist/platform/capabilityFallback.d.ts +39 -0
  439. package/dist/platform/capabilityFallback.d.ts.map +1 -0
  440. package/dist/platform/capabilityFallback.js +55 -0
  441. package/dist/platform/capabilityFallback.js.map +1 -0
  442. package/dist/platform/commandRouter.d.ts +51 -0
  443. package/dist/platform/commandRouter.d.ts.map +1 -0
  444. package/dist/platform/commandRouter.js +53 -0
  445. package/dist/platform/commandRouter.js.map +1 -0
  446. package/dist/platform/connectorSet.d.ts +39 -0
  447. package/dist/platform/connectorSet.d.ts.map +1 -0
  448. package/dist/platform/connectorSet.js +55 -0
  449. package/dist/platform/connectorSet.js.map +1 -0
  450. package/dist/platform/inbound.d.ts +147 -0
  451. package/dist/platform/inbound.d.ts.map +1 -0
  452. package/dist/platform/inbound.js +18 -0
  453. package/dist/platform/inbound.js.map +1 -0
  454. package/dist/platform/outbound.d.ts +225 -0
  455. package/dist/platform/outbound.d.ts.map +1 -0
  456. package/dist/platform/outbound.js +30 -0
  457. package/dist/platform/outbound.js.map +1 -0
  458. package/dist/platform/unservedStateGuard.d.ts +27 -0
  459. package/dist/platform/unservedStateGuard.d.ts.map +1 -0
  460. package/dist/platform/unservedStateGuard.js +42 -0
  461. package/dist/platform/unservedStateGuard.js.map +1 -0
  462. package/dist/postToSession.d.ts +94 -0
  463. package/dist/postToSession.d.ts.map +1 -0
  464. package/dist/postToSession.js +56 -0
  465. package/dist/postToSession.js.map +1 -0
  466. package/dist/progressLine.d.ts +180 -0
  467. package/dist/progressLine.d.ts.map +1 -0
  468. package/dist/progressLine.js +293 -0
  469. package/dist/progressLine.js.map +1 -0
  470. package/dist/rateLimiter.d.ts +246 -0
  471. package/dist/rateLimiter.d.ts.map +1 -0
  472. package/dist/rateLimiter.js +555 -0
  473. package/dist/rateLimiter.js.map +1 -0
  474. package/dist/requests/answerRequest.d.ts +57 -0
  475. package/dist/requests/answerRequest.d.ts.map +1 -0
  476. package/dist/requests/answerRequest.js +96 -0
  477. package/dist/requests/answerRequest.js.map +1 -0
  478. package/dist/requests/answerTail.d.ts +83 -0
  479. package/dist/requests/answerTail.d.ts.map +1 -0
  480. package/dist/requests/answerTail.js +102 -0
  481. package/dist/requests/answerTail.js.map +1 -0
  482. package/dist/requests/limitWaitAnswer.d.ts +87 -0
  483. package/dist/requests/limitWaitAnswer.d.ts.map +1 -0
  484. package/dist/requests/limitWaitAnswer.js +83 -0
  485. package/dist/requests/limitWaitAnswer.js.map +1 -0
  486. package/dist/requests/requestGroup.d.ts +44 -0
  487. package/dist/requests/requestGroup.d.ts.map +1 -0
  488. package/dist/requests/requestGroup.js +63 -0
  489. package/dist/requests/requestGroup.js.map +1 -0
  490. package/dist/requests/requestHeader.d.ts +70 -0
  491. package/dist/requests/requestHeader.d.ts.map +1 -0
  492. package/dist/requests/requestHeader.js +83 -0
  493. package/dist/requests/requestHeader.js.map +1 -0
  494. package/dist/requests/requestLedger.d.ts +217 -0
  495. package/dist/requests/requestLedger.d.ts.map +1 -0
  496. package/dist/requests/requestLedger.js +468 -0
  497. package/dist/requests/requestLedger.js.map +1 -0
  498. package/dist/requests/sessionTurnProbe.d.ts +52 -0
  499. package/dist/requests/sessionTurnProbe.d.ts.map +1 -0
  500. package/dist/requests/sessionTurnProbe.js +29 -0
  501. package/dist/requests/sessionTurnProbe.js.map +1 -0
  502. package/dist/requests/types.d.ts +181 -0
  503. package/dist/requests/types.d.ts.map +1 -0
  504. package/dist/requests/types.js +9 -0
  505. package/dist/requests/types.js.map +1 -0
  506. package/dist/requests/wakeUpEngine.d.ts +160 -0
  507. package/dist/requests/wakeUpEngine.d.ts.map +1 -0
  508. package/dist/requests/wakeUpEngine.js +399 -0
  509. package/dist/requests/wakeUpEngine.js.map +1 -0
  510. package/dist/requests/wakeUpRules.d.ts +151 -0
  511. package/dist/requests/wakeUpRules.d.ts.map +1 -0
  512. package/dist/requests/wakeUpRules.js +167 -0
  513. package/dist/requests/wakeUpRules.js.map +1 -0
  514. package/dist/resumeContext.d.ts +87 -0
  515. package/dist/resumeContext.d.ts.map +1 -0
  516. package/dist/resumeContext.js +114 -0
  517. package/dist/resumeContext.js.map +1 -0
  518. package/dist/scheduler/checkRun.d.ts +95 -0
  519. package/dist/scheduler/checkRun.d.ts.map +1 -0
  520. package/dist/scheduler/checkRun.js +172 -0
  521. package/dist/scheduler/checkRun.js.map +1 -0
  522. package/dist/scheduler/delivery.d.ts +99 -0
  523. package/dist/scheduler/delivery.d.ts.map +1 -0
  524. package/dist/scheduler/delivery.js +212 -0
  525. package/dist/scheduler/delivery.js.map +1 -0
  526. package/dist/scheduler/deliveryKind.d.ts +29 -0
  527. package/dist/scheduler/deliveryKind.d.ts.map +1 -0
  528. package/dist/scheduler/deliveryKind.js +39 -0
  529. package/dist/scheduler/deliveryKind.js.map +1 -0
  530. package/dist/scheduler/directoryThreads.d.ts +19 -0
  531. package/dist/scheduler/directoryThreads.d.ts.map +1 -0
  532. package/dist/scheduler/directoryThreads.js +28 -0
  533. package/dist/scheduler/directoryThreads.js.map +1 -0
  534. package/dist/scheduler/engine.d.ts +77 -0
  535. package/dist/scheduler/engine.d.ts.map +1 -0
  536. package/dist/scheduler/engine.js +337 -0
  537. package/dist/scheduler/engine.js.map +1 -0
  538. package/dist/scheduler/injection.d.ts +102 -0
  539. package/dist/scheduler/injection.d.ts.map +1 -0
  540. package/dist/scheduler/injection.js +122 -0
  541. package/dist/scheduler/injection.js.map +1 -0
  542. package/dist/scheduler/mcpBoot.d.ts +65 -0
  543. package/dist/scheduler/mcpBoot.d.ts.map +1 -0
  544. package/dist/scheduler/mcpBoot.js +60 -0
  545. package/dist/scheduler/mcpBoot.js.map +1 -0
  546. package/dist/scheduler/mcpSurface.d.ts +363 -0
  547. package/dist/scheduler/mcpSurface.d.ts.map +1 -0
  548. package/dist/scheduler/mcpSurface.js +1297 -0
  549. package/dist/scheduler/mcpSurface.js.map +1 -0
  550. package/dist/scheduler/rebindResume.d.ts +27 -0
  551. package/dist/scheduler/rebindResume.d.ts.map +1 -0
  552. package/dist/scheduler/rebindResume.js +17 -0
  553. package/dist/scheduler/rebindResume.js.map +1 -0
  554. package/dist/scheduler/recurrence.d.ts +58 -0
  555. package/dist/scheduler/recurrence.d.ts.map +1 -0
  556. package/dist/scheduler/recurrence.js +210 -0
  557. package/dist/scheduler/recurrence.js.map +1 -0
  558. package/dist/scheduler/runLedger.d.ts +59 -0
  559. package/dist/scheduler/runLedger.d.ts.map +1 -0
  560. package/dist/scheduler/runLedger.js +54 -0
  561. package/dist/scheduler/runLedger.js.map +1 -0
  562. package/dist/scheduler/store.d.ts +97 -0
  563. package/dist/scheduler/store.d.ts.map +1 -0
  564. package/dist/scheduler/store.js +124 -0
  565. package/dist/scheduler/store.js.map +1 -0
  566. package/dist/scheduler/timezoneRecompute.d.ts +61 -0
  567. package/dist/scheduler/timezoneRecompute.d.ts.map +1 -0
  568. package/dist/scheduler/timezoneRecompute.js +46 -0
  569. package/dist/scheduler/timezoneRecompute.js.map +1 -0
  570. package/dist/scheduler/types.d.ts +139 -0
  571. package/dist/scheduler/types.d.ts.map +1 -0
  572. package/dist/scheduler/types.js +8 -0
  573. package/dist/scheduler/types.js.map +1 -0
  574. package/dist/sendErrorClassifier.d.ts +45 -0
  575. package/dist/sendErrorClassifier.d.ts.map +1 -0
  576. package/dist/sendErrorClassifier.js +65 -0
  577. package/dist/sendErrorClassifier.js.map +1 -0
  578. package/dist/sessionKey.d.ts +136 -0
  579. package/dist/sessionKey.d.ts.map +1 -0
  580. package/dist/sessionKey.js +153 -0
  581. package/dist/sessionKey.js.map +1 -0
  582. package/dist/sessionPick.d.ts +42 -0
  583. package/dist/sessionPick.d.ts.map +1 -0
  584. package/dist/sessionPick.js +39 -0
  585. package/dist/sessionPick.js.map +1 -0
  586. package/dist/shutdown.d.ts +126 -0
  587. package/dist/shutdown.d.ts.map +1 -0
  588. package/dist/shutdown.js +164 -0
  589. package/dist/shutdown.js.map +1 -0
  590. package/dist/startupPromptBuffer.d.ts +85 -0
  591. package/dist/startupPromptBuffer.d.ts.map +1 -0
  592. package/dist/startupPromptBuffer.js +123 -0
  593. package/dist/startupPromptBuffer.js.map +1 -0
  594. package/dist/state.d.ts +1416 -0
  595. package/dist/state.d.ts.map +1 -0
  596. package/dist/state.js +2128 -0
  597. package/dist/state.js.map +1 -0
  598. package/dist/threadContextPreamble.d.ts +89 -0
  599. package/dist/threadContextPreamble.d.ts.map +1 -0
  600. package/dist/threadContextPreamble.js +102 -0
  601. package/dist/threadContextPreamble.js.map +1 -0
  602. package/dist/threadRouting.d.ts +164 -0
  603. package/dist/threadRouting.d.ts.map +1 -0
  604. package/dist/threadRouting.js +182 -0
  605. package/dist/threadRouting.js.map +1 -0
  606. package/dist/types.d.ts +1022 -0
  607. package/dist/types.d.ts.map +1 -0
  608. package/dist/types.js +3 -0
  609. package/dist/types.js.map +1 -0
  610. package/dist/utils/abortableFifo.d.ts +10 -0
  611. package/dist/utils/abortableFifo.d.ts.map +1 -0
  612. package/dist/utils/abortableFifo.js +55 -0
  613. package/dist/utils/abortableFifo.js.map +1 -0
  614. package/dist/utils/adoptedToolList.d.ts +27 -0
  615. package/dist/utils/adoptedToolList.d.ts.map +1 -0
  616. package/dist/utils/adoptedToolList.js +22 -0
  617. package/dist/utils/adoptedToolList.js.map +1 -0
  618. package/dist/utils/agentEnvironment.d.ts +42 -0
  619. package/dist/utils/agentEnvironment.d.ts.map +1 -0
  620. package/dist/utils/agentEnvironment.js +122 -0
  621. package/dist/utils/agentEnvironment.js.map +1 -0
  622. package/dist/utils/ansiClean.d.ts +11 -0
  623. package/dist/utils/ansiClean.d.ts.map +1 -0
  624. package/dist/utils/ansiClean.js +139 -0
  625. package/dist/utils/ansiClean.js.map +1 -0
  626. package/dist/utils/autoContinueOnLimit.d.ts +56 -0
  627. package/dist/utils/autoContinueOnLimit.d.ts.map +1 -0
  628. package/dist/utils/autoContinueOnLimit.js +79 -0
  629. package/dist/utils/autoContinueOnLimit.js.map +1 -0
  630. package/dist/utils/canonicalPathContainment.d.ts +7 -0
  631. package/dist/utils/canonicalPathContainment.d.ts.map +1 -0
  632. package/dist/utils/canonicalPathContainment.js +48 -0
  633. package/dist/utils/canonicalPathContainment.js.map +1 -0
  634. package/dist/utils/claudeAccountPickerPlan.d.ts +85 -0
  635. package/dist/utils/claudeAccountPickerPlan.d.ts.map +1 -0
  636. package/dist/utils/claudeAccountPickerPlan.js +123 -0
  637. package/dist/utils/claudeAccountPickerPlan.js.map +1 -0
  638. package/dist/utils/claudeAuthLogin.d.ts +71 -0
  639. package/dist/utils/claudeAuthLogin.d.ts.map +1 -0
  640. package/dist/utils/claudeAuthLogin.js +93 -0
  641. package/dist/utils/claudeAuthLogin.js.map +1 -0
  642. package/dist/utils/claudeChunkClassifier.d.ts +97 -0
  643. package/dist/utils/claudeChunkClassifier.d.ts.map +1 -0
  644. package/dist/utils/claudeChunkClassifier.js +254 -0
  645. package/dist/utils/claudeChunkClassifier.js.map +1 -0
  646. package/dist/utils/claudeCodeVersion.d.ts +31 -0
  647. package/dist/utils/claudeCodeVersion.d.ts.map +1 -0
  648. package/dist/utils/claudeCodeVersion.js +62 -0
  649. package/dist/utils/claudeCodeVersion.js.map +1 -0
  650. package/dist/utils/claudeCompactHook.d.ts +64 -0
  651. package/dist/utils/claudeCompactHook.d.ts.map +1 -0
  652. package/dist/utils/claudeCompactHook.js +98 -0
  653. package/dist/utils/claudeCompactHook.js.map +1 -0
  654. package/dist/utils/claudeLimitSwitch.d.ts +40 -0
  655. package/dist/utils/claudeLimitSwitch.d.ts.map +1 -0
  656. package/dist/utils/claudeLimitSwitch.js +54 -0
  657. package/dist/utils/claudeLimitSwitch.js.map +1 -0
  658. package/dist/utils/claudeLivenessAction.d.ts +208 -0
  659. package/dist/utils/claudeLivenessAction.d.ts.map +1 -0
  660. package/dist/utils/claudeLivenessAction.js +135 -0
  661. package/dist/utils/claudeLivenessAction.js.map +1 -0
  662. package/dist/utils/claudeMcpHeal.d.ts +92 -0
  663. package/dist/utils/claudeMcpHeal.d.ts.map +1 -0
  664. package/dist/utils/claudeMcpHeal.js +91 -0
  665. package/dist/utils/claudeMcpHeal.js.map +1 -0
  666. package/dist/utils/claudeProfileUsageLines.d.ts +15 -0
  667. package/dist/utils/claudeProfileUsageLines.d.ts.map +1 -0
  668. package/dist/utils/claudeProfileUsageLines.js +42 -0
  669. package/dist/utils/claudeProfileUsageLines.js.map +1 -0
  670. package/dist/utils/claudeProfiles.d.ts +117 -0
  671. package/dist/utils/claudeProfiles.d.ts.map +1 -0
  672. package/dist/utils/claudeProfiles.js +285 -0
  673. package/dist/utils/claudeProfiles.js.map +1 -0
  674. package/dist/utils/claudeRelayRouting.d.ts +86 -0
  675. package/dist/utils/claudeRelayRouting.d.ts.map +1 -0
  676. package/dist/utils/claudeRelayRouting.js +159 -0
  677. package/dist/utils/claudeRelayRouting.js.map +1 -0
  678. package/dist/utils/claudeRuntimeInfo.d.ts +26 -0
  679. package/dist/utils/claudeRuntimeInfo.d.ts.map +1 -0
  680. package/dist/utils/claudeRuntimeInfo.js +151 -0
  681. package/dist/utils/claudeRuntimeInfo.js.map +1 -0
  682. package/dist/utils/claudeScrapeShapes.d.ts +256 -0
  683. package/dist/utils/claudeScrapeShapes.d.ts.map +1 -0
  684. package/dist/utils/claudeScrapeShapes.js +290 -0
  685. package/dist/utils/claudeScrapeShapes.js.map +1 -0
  686. package/dist/utils/claudeSessionTitle.d.ts +27 -0
  687. package/dist/utils/claudeSessionTitle.d.ts.map +1 -0
  688. package/dist/utils/claudeSessionTitle.js +90 -0
  689. package/dist/utils/claudeSessionTitle.js.map +1 -0
  690. package/dist/utils/claudeStreamJson.d.ts +214 -0
  691. package/dist/utils/claudeStreamJson.d.ts.map +1 -0
  692. package/dist/utils/claudeStreamJson.js +364 -0
  693. package/dist/utils/claudeStreamJson.js.map +1 -0
  694. package/dist/utils/claudeSubagentTail.d.ts +93 -0
  695. package/dist/utils/claudeSubagentTail.d.ts.map +1 -0
  696. package/dist/utils/claudeSubagentTail.js +136 -0
  697. package/dist/utils/claudeSubagentTail.js.map +1 -0
  698. package/dist/utils/claudeUsage.d.ts +209 -0
  699. package/dist/utils/claudeUsage.d.ts.map +1 -0
  700. package/dist/utils/claudeUsage.js +413 -0
  701. package/dist/utils/claudeUsage.js.map +1 -0
  702. package/dist/utils/claudeUsageRefresher.d.ts +44 -0
  703. package/dist/utils/claudeUsageRefresher.d.ts.map +1 -0
  704. package/dist/utils/claudeUsageRefresher.js +95 -0
  705. package/dist/utils/claudeUsageRefresher.js.map +1 -0
  706. package/dist/utils/clearThreadOutputQueues.d.ts +43 -0
  707. package/dist/utils/clearThreadOutputQueues.d.ts.map +1 -0
  708. package/dist/utils/clearThreadOutputQueues.js +39 -0
  709. package/dist/utils/clearThreadOutputQueues.js.map +1 -0
  710. package/dist/utils/compactCommandRoute.d.ts +45 -0
  711. package/dist/utils/compactCommandRoute.d.ts.map +1 -0
  712. package/dist/utils/compactCommandRoute.js +36 -0
  713. package/dist/utils/compactCommandRoute.js.map +1 -0
  714. package/dist/utils/compactOnIdle.d.ts +329 -0
  715. package/dist/utils/compactOnIdle.d.ts.map +1 -0
  716. package/dist/utils/compactOnIdle.js +379 -0
  717. package/dist/utils/compactOnIdle.js.map +1 -0
  718. package/dist/utils/consoleFileTap.d.ts +51 -0
  719. package/dist/utils/consoleFileTap.d.ts.map +1 -0
  720. package/dist/utils/consoleFileTap.js +86 -0
  721. package/dist/utils/consoleFileTap.js.map +1 -0
  722. package/dist/utils/displayVerbosity.d.ts +44 -0
  723. package/dist/utils/displayVerbosity.d.ts.map +1 -0
  724. package/dist/utils/displayVerbosity.js +75 -0
  725. package/dist/utils/displayVerbosity.js.map +1 -0
  726. package/dist/utils/download.d.ts +72 -0
  727. package/dist/utils/download.d.ts.map +1 -0
  728. package/dist/utils/download.js +216 -0
  729. package/dist/utils/download.js.map +1 -0
  730. package/dist/utils/draftFinalize.d.ts +110 -0
  731. package/dist/utils/draftFinalize.d.ts.map +1 -0
  732. package/dist/utils/draftFinalize.js +100 -0
  733. package/dist/utils/draftFinalize.js.map +1 -0
  734. package/dist/utils/draftId.d.ts +36 -0
  735. package/dist/utils/draftId.d.ts.map +1 -0
  736. package/dist/utils/draftId.js +46 -0
  737. package/dist/utils/draftId.js.map +1 -0
  738. package/dist/utils/draftPacer.d.ts +72 -0
  739. package/dist/utils/draftPacer.d.ts.map +1 -0
  740. package/dist/utils/draftPacer.js +53 -0
  741. package/dist/utils/draftPacer.js.map +1 -0
  742. package/dist/utils/effortStartupKeystroke.d.ts +20 -0
  743. package/dist/utils/effortStartupKeystroke.d.ts.map +1 -0
  744. package/dist/utils/effortStartupKeystroke.js +28 -0
  745. package/dist/utils/effortStartupKeystroke.js.map +1 -0
  746. package/dist/utils/ensureSessionPlan.d.ts +70 -0
  747. package/dist/utils/ensureSessionPlan.d.ts.map +1 -0
  748. package/dist/utils/ensureSessionPlan.js +54 -0
  749. package/dist/utils/ensureSessionPlan.js.map +1 -0
  750. package/dist/utils/fallbackBridge.d.ts +32 -0
  751. package/dist/utils/fallbackBridge.d.ts.map +1 -0
  752. package/dist/utils/fallbackBridge.js +62 -0
  753. package/dist/utils/fallbackBridge.js.map +1 -0
  754. package/dist/utils/fallbackChain.d.ts +299 -0
  755. package/dist/utils/fallbackChain.d.ts.map +1 -0
  756. package/dist/utils/fallbackChain.js +542 -0
  757. package/dist/utils/fallbackChain.js.map +1 -0
  758. package/dist/utils/fileSendPlan.d.ts +217 -0
  759. package/dist/utils/fileSendPlan.d.ts.map +1 -0
  760. package/dist/utils/fileSendPlan.js +291 -0
  761. package/dist/utils/fileSendPlan.js.map +1 -0
  762. package/dist/utils/fileSendService.d.ts +94 -0
  763. package/dist/utils/fileSendService.d.ts.map +1 -0
  764. package/dist/utils/fileSendService.js +387 -0
  765. package/dist/utils/fileSendService.js.map +1 -0
  766. package/dist/utils/groupFinalizePlan.d.ts +38 -0
  767. package/dist/utils/groupFinalizePlan.d.ts.map +1 -0
  768. package/dist/utils/groupFinalizePlan.js +35 -0
  769. package/dist/utils/groupFinalizePlan.js.map +1 -0
  770. package/dist/utils/identicalOutputGuard.d.ts +83 -0
  771. package/dist/utils/identicalOutputGuard.d.ts.map +1 -0
  772. package/dist/utils/identicalOutputGuard.js +99 -0
  773. package/dist/utils/identicalOutputGuard.js.map +1 -0
  774. package/dist/utils/isoTimestamp.d.ts +16 -0
  775. package/dist/utils/isoTimestamp.d.ts.map +1 -0
  776. package/dist/utils/isoTimestamp.js +34 -0
  777. package/dist/utils/isoTimestamp.js.map +1 -0
  778. package/dist/utils/jsonStreamBusyWatchdog.d.ts +130 -0
  779. package/dist/utils/jsonStreamBusyWatchdog.d.ts.map +1 -0
  780. package/dist/utils/jsonStreamBusyWatchdog.js +122 -0
  781. package/dist/utils/jsonStreamBusyWatchdog.js.map +1 -0
  782. package/dist/utils/jsonStreamHost.d.ts +238 -0
  783. package/dist/utils/jsonStreamHost.d.ts.map +1 -0
  784. package/dist/utils/jsonStreamHost.js +438 -0
  785. package/dist/utils/jsonStreamHost.js.map +1 -0
  786. package/dist/utils/keyedTransitionQueue.d.ts +18 -0
  787. package/dist/utils/keyedTransitionQueue.d.ts.map +1 -0
  788. package/dist/utils/keyedTransitionQueue.js +34 -0
  789. package/dist/utils/keyedTransitionQueue.js.map +1 -0
  790. package/dist/utils/limitEpisodeRecovery.d.ts +92 -0
  791. package/dist/utils/limitEpisodeRecovery.d.ts.map +1 -0
  792. package/dist/utils/limitEpisodeRecovery.js +133 -0
  793. package/dist/utils/limitEpisodeRecovery.js.map +1 -0
  794. package/dist/utils/limitHeldPromptQueue.d.ts +48 -0
  795. package/dist/utils/limitHeldPromptQueue.d.ts.map +1 -0
  796. package/dist/utils/limitHeldPromptQueue.js +59 -0
  797. package/dist/utils/limitHeldPromptQueue.js.map +1 -0
  798. package/dist/utils/limitHeldPrompts.d.ts +58 -0
  799. package/dist/utils/limitHeldPrompts.d.ts.map +1 -0
  800. package/dist/utils/limitHeldPrompts.js +65 -0
  801. package/dist/utils/limitHeldPrompts.js.map +1 -0
  802. package/dist/utils/linkPreviewSuppression.d.ts +18 -0
  803. package/dist/utils/linkPreviewSuppression.d.ts.map +1 -0
  804. package/dist/utils/linkPreviewSuppression.js +37 -0
  805. package/dist/utils/linkPreviewSuppression.js.map +1 -0
  806. package/dist/utils/localClock.d.ts +15 -0
  807. package/dist/utils/localClock.d.ts.map +1 -0
  808. package/dist/utils/localClock.js +29 -0
  809. package/dist/utils/localClock.js.map +1 -0
  810. package/dist/utils/markdownTableRender.d.ts +83 -0
  811. package/dist/utils/markdownTableRender.d.ts.map +1 -0
  812. package/dist/utils/markdownTableRender.js +298 -0
  813. package/dist/utils/markdownTableRender.js.map +1 -0
  814. package/dist/utils/mediaGroupCollector.d.ts +51 -0
  815. package/dist/utils/mediaGroupCollector.d.ts.map +1 -0
  816. package/dist/utils/mediaGroupCollector.js +90 -0
  817. package/dist/utils/mediaGroupCollector.js.map +1 -0
  818. package/dist/utils/messageSendService.d.ts +136 -0
  819. package/dist/utils/messageSendService.d.ts.map +1 -0
  820. package/dist/utils/messageSendService.js +165 -0
  821. package/dist/utils/messageSendService.js.map +1 -0
  822. package/dist/utils/minutesOverride.d.ts +7 -0
  823. package/dist/utils/minutesOverride.d.ts.map +1 -0
  824. package/dist/utils/minutesOverride.js +16 -0
  825. package/dist/utils/minutesOverride.js.map +1 -0
  826. package/dist/utils/modelPickerPlan.d.ts +119 -0
  827. package/dist/utils/modelPickerPlan.d.ts.map +1 -0
  828. package/dist/utils/modelPickerPlan.js +171 -0
  829. package/dist/utils/modelPickerPlan.js.map +1 -0
  830. package/dist/utils/modelRecentPlan.d.ts +56 -0
  831. package/dist/utils/modelRecentPlan.d.ts.map +1 -0
  832. package/dist/utils/modelRecentPlan.js +107 -0
  833. package/dist/utils/modelRecentPlan.js.map +1 -0
  834. package/dist/utils/modelSetReplyDecision.d.ts +67 -0
  835. package/dist/utils/modelSetReplyDecision.d.ts.map +1 -0
  836. package/dist/utils/modelSetReplyDecision.js +27 -0
  837. package/dist/utils/modelSetReplyDecision.js.map +1 -0
  838. package/dist/utils/openCodeAuthLogin.d.ts +181 -0
  839. package/dist/utils/openCodeAuthLogin.d.ts.map +1 -0
  840. package/dist/utils/openCodeAuthLogin.js +393 -0
  841. package/dist/utils/openCodeAuthLogin.js.map +1 -0
  842. package/dist/utils/openCodeCompactPlugin.d.ts +81 -0
  843. package/dist/utils/openCodeCompactPlugin.d.ts.map +1 -0
  844. package/dist/utils/openCodeCompactPlugin.js +146 -0
  845. package/dist/utils/openCodeCompactPlugin.js.map +1 -0
  846. package/dist/utils/openCodeTurnActivity.d.ts +67 -0
  847. package/dist/utils/openCodeTurnActivity.d.ts.map +1 -0
  848. package/dist/utils/openCodeTurnActivity.js +27 -0
  849. package/dist/utils/openCodeTurnActivity.js.map +1 -0
  850. package/dist/utils/outputDebounce.d.ts +24 -0
  851. package/dist/utils/outputDebounce.d.ts.map +1 -0
  852. package/dist/utils/outputDebounce.js +30 -0
  853. package/dist/utils/outputDebounce.js.map +1 -0
  854. package/dist/utils/outputFlushTiming.d.ts +54 -0
  855. package/dist/utils/outputFlushTiming.d.ts.map +1 -0
  856. package/dist/utils/outputFlushTiming.js +48 -0
  857. package/dist/utils/outputFlushTiming.js.map +1 -0
  858. package/dist/utils/paginateList.d.ts +25 -0
  859. package/dist/utils/paginateList.d.ts.map +1 -0
  860. package/dist/utils/paginateList.js +32 -0
  861. package/dist/utils/paginateList.js.map +1 -0
  862. package/dist/utils/paneDiff.d.ts +55 -0
  863. package/dist/utils/paneDiff.d.ts.map +1 -0
  864. package/dist/utils/paneDiff.js +88 -0
  865. package/dist/utils/paneDiff.js.map +1 -0
  866. package/dist/utils/paneResizeGuard.d.ts +69 -0
  867. package/dist/utils/paneResizeGuard.d.ts.map +1 -0
  868. package/dist/utils/paneResizeGuard.js +91 -0
  869. package/dist/utils/paneResizeGuard.js.map +1 -0
  870. package/dist/utils/persistAdapterSessionIds.d.ts +39 -0
  871. package/dist/utils/persistAdapterSessionIds.d.ts.map +1 -0
  872. package/dist/utils/persistAdapterSessionIds.js +28 -0
  873. package/dist/utils/persistAdapterSessionIds.js.map +1 -0
  874. package/dist/utils/pinnedBannerSkipDecision.d.ts +44 -0
  875. package/dist/utils/pinnedBannerSkipDecision.d.ts.map +1 -0
  876. package/dist/utils/pinnedBannerSkipDecision.js +20 -0
  877. package/dist/utils/pinnedBannerSkipDecision.js.map +1 -0
  878. package/dist/utils/pollBackoff.d.ts +45 -0
  879. package/dist/utils/pollBackoff.d.ts.map +1 -0
  880. package/dist/utils/pollBackoff.js +44 -0
  881. package/dist/utils/pollBackoff.js.map +1 -0
  882. package/dist/utils/promptDelivery.d.ts +46 -0
  883. package/dist/utils/promptDelivery.d.ts.map +1 -0
  884. package/dist/utils/promptDelivery.js +49 -0
  885. package/dist/utils/promptDelivery.js.map +1 -0
  886. package/dist/utils/providerDisconnectPlan.d.ts +82 -0
  887. package/dist/utils/providerDisconnectPlan.d.ts.map +1 -0
  888. package/dist/utils/providerDisconnectPlan.js +110 -0
  889. package/dist/utils/providerDisconnectPlan.js.map +1 -0
  890. package/dist/utils/rateLimitLog.d.ts +48 -0
  891. package/dist/utils/rateLimitLog.d.ts.map +1 -0
  892. package/dist/utils/rateLimitLog.js +43 -0
  893. package/dist/utils/rateLimitLog.js.map +1 -0
  894. package/dist/utils/recentRelayWindow.d.ts +165 -0
  895. package/dist/utils/recentRelayWindow.d.ts.map +1 -0
  896. package/dist/utils/recentRelayWindow.js +284 -0
  897. package/dist/utils/recentRelayWindow.js.map +1 -0
  898. package/dist/utils/recvPreviewRedaction.d.ts +23 -0
  899. package/dist/utils/recvPreviewRedaction.d.ts.map +1 -0
  900. package/dist/utils/recvPreviewRedaction.js +39 -0
  901. package/dist/utils/recvPreviewRedaction.js.map +1 -0
  902. package/dist/utils/reminderFlow.d.ts +148 -0
  903. package/dist/utils/reminderFlow.d.ts.map +1 -0
  904. package/dist/utils/reminderFlow.js +122 -0
  905. package/dist/utils/reminderFlow.js.map +1 -0
  906. package/dist/utils/reminderScheduleText.d.ts +87 -0
  907. package/dist/utils/reminderScheduleText.d.ts.map +1 -0
  908. package/dist/utils/reminderScheduleText.js +135 -0
  909. package/dist/utils/reminderScheduleText.js.map +1 -0
  910. package/dist/utils/reminderWizard.d.ts +710 -0
  911. package/dist/utils/reminderWizard.d.ts.map +1 -0
  912. package/dist/utils/reminderWizard.js +1164 -0
  913. package/dist/utils/reminderWizard.js.map +1 -0
  914. package/dist/utils/replyQuote.d.ts +111 -0
  915. package/dist/utils/replyQuote.d.ts.map +1 -0
  916. package/dist/utils/replyQuote.js +122 -0
  917. package/dist/utils/replyQuote.js.map +1 -0
  918. package/dist/utils/resolveBinary.d.ts +25 -0
  919. package/dist/utils/resolveBinary.d.ts.map +1 -0
  920. package/dist/utils/resolveBinary.js +99 -0
  921. package/dist/utils/resolveBinary.js.map +1 -0
  922. package/dist/utils/retryAfterHeader.d.ts +7 -0
  923. package/dist/utils/retryAfterHeader.d.ts.map +1 -0
  924. package/dist/utils/retryAfterHeader.js +17 -0
  925. package/dist/utils/retryAfterHeader.js.map +1 -0
  926. package/dist/utils/rotatingJsonlFile.d.ts +40 -0
  927. package/dist/utils/rotatingJsonlFile.d.ts.map +1 -0
  928. package/dist/utils/rotatingJsonlFile.js +106 -0
  929. package/dist/utils/rotatingJsonlFile.js.map +1 -0
  930. package/dist/utils/rotatingLogFile.d.ts +25 -0
  931. package/dist/utils/rotatingLogFile.d.ts.map +1 -0
  932. package/dist/utils/rotatingLogFile.js +74 -0
  933. package/dist/utils/rotatingLogFile.js.map +1 -0
  934. package/dist/utils/sendRateTracker.d.ts +59 -0
  935. package/dist/utils/sendRateTracker.d.ts.map +1 -0
  936. package/dist/utils/sendRateTracker.js +144 -0
  937. package/dist/utils/sendRateTracker.js.map +1 -0
  938. package/dist/utils/serialQueue.d.ts +5 -0
  939. package/dist/utils/serialQueue.d.ts.map +1 -0
  940. package/dist/utils/serialQueue.js +14 -0
  941. package/dist/utils/serialQueue.js.map +1 -0
  942. package/dist/utils/sseStreamLifecycle.d.ts +32 -0
  943. package/dist/utils/sseStreamLifecycle.d.ts.map +1 -0
  944. package/dist/utils/sseStreamLifecycle.js +41 -0
  945. package/dist/utils/sseStreamLifecycle.js.map +1 -0
  946. package/dist/utils/startupReadiness.d.ts +119 -0
  947. package/dist/utils/startupReadiness.d.ts.map +1 -0
  948. package/dist/utils/startupReadiness.js +148 -0
  949. package/dist/utils/startupReadiness.js.map +1 -0
  950. package/dist/utils/statusFlushDecision.d.ts +18 -0
  951. package/dist/utils/statusFlushDecision.d.ts.map +1 -0
  952. package/dist/utils/statusFlushDecision.js +53 -0
  953. package/dist/utils/statusFlushDecision.js.map +1 -0
  954. package/dist/utils/subagentRender.d.ts +52 -0
  955. package/dist/utils/subagentRender.d.ts.map +1 -0
  956. package/dist/utils/subagentRender.js +66 -0
  957. package/dist/utils/subagentRender.js.map +1 -0
  958. package/dist/utils/subagentStatusRender.d.ts +90 -0
  959. package/dist/utils/subagentStatusRender.d.ts.map +1 -0
  960. package/dist/utils/subagentStatusRender.js +124 -0
  961. package/dist/utils/subagentStatusRender.js.map +1 -0
  962. package/dist/utils/telegramError.d.ts +2 -0
  963. package/dist/utils/telegramError.d.ts.map +1 -0
  964. package/dist/utils/telegramError.js +16 -0
  965. package/dist/utils/telegramError.js.map +1 -0
  966. package/dist/utils/terminalEmitPlan.d.ts +74 -0
  967. package/dist/utils/terminalEmitPlan.d.ts.map +1 -0
  968. package/dist/utils/terminalEmitPlan.js +86 -0
  969. package/dist/utils/terminalEmitPlan.js.map +1 -0
  970. package/dist/utils/thinkingRender.d.ts +74 -0
  971. package/dist/utils/thinkingRender.d.ts.map +1 -0
  972. package/dist/utils/thinkingRender.js +85 -0
  973. package/dist/utils/thinkingRender.js.map +1 -0
  974. package/dist/utils/threadStatusReport.d.ts +79 -0
  975. package/dist/utils/threadStatusReport.d.ts.map +1 -0
  976. package/dist/utils/threadStatusReport.js +88 -0
  977. package/dist/utils/threadStatusReport.js.map +1 -0
  978. package/dist/utils/threadToggle.d.ts +29 -0
  979. package/dist/utils/threadToggle.d.ts.map +1 -0
  980. package/dist/utils/threadToggle.js +39 -0
  981. package/dist/utils/threadToggle.js.map +1 -0
  982. package/dist/utils/timezone.d.ts +125 -0
  983. package/dist/utils/timezone.d.ts.map +1 -0
  984. package/dist/utils/timezone.js +278 -0
  985. package/dist/utils/timezone.js.map +1 -0
  986. package/dist/utils/tmuxExec.d.ts +61 -0
  987. package/dist/utils/tmuxExec.d.ts.map +1 -0
  988. package/dist/utils/tmuxExec.js +109 -0
  989. package/dist/utils/tmuxExec.js.map +1 -0
  990. package/dist/utils/tmuxSessionName.d.ts +34 -0
  991. package/dist/utils/tmuxSessionName.d.ts.map +1 -0
  992. package/dist/utils/tmuxSessionName.js +51 -0
  993. package/dist/utils/tmuxSessionName.js.map +1 -0
  994. package/dist/utils/toolResultRender.d.ts +61 -0
  995. package/dist/utils/toolResultRender.d.ts.map +1 -0
  996. package/dist/utils/toolResultRender.js +62 -0
  997. package/dist/utils/toolResultRender.js.map +1 -0
  998. package/dist/utils/topicRequest.d.ts +61 -0
  999. package/dist/utils/topicRequest.d.ts.map +1 -0
  1000. package/dist/utils/topicRequest.js +68 -0
  1001. package/dist/utils/topicRequest.js.map +1 -0
  1002. package/dist/utils/topicView.d.ts +55 -0
  1003. package/dist/utils/topicView.d.ts.map +1 -0
  1004. package/dist/utils/topicView.js +56 -0
  1005. package/dist/utils/topicView.js.map +1 -0
  1006. package/dist/utils/transcribeAudio.d.ts +89 -0
  1007. package/dist/utils/transcribeAudio.d.ts.map +1 -0
  1008. package/dist/utils/transcribeAudio.js +249 -0
  1009. package/dist/utils/transcribeAudio.js.map +1 -0
  1010. package/dist/utils/transientFrames.d.ts +52 -0
  1011. package/dist/utils/transientFrames.d.ts.map +1 -0
  1012. package/dist/utils/transientFrames.js +41 -0
  1013. package/dist/utils/transientFrames.js.map +1 -0
  1014. package/dist/utils/typingActive.d.ts +35 -0
  1015. package/dist/utils/typingActive.d.ts.map +1 -0
  1016. package/dist/utils/typingActive.js +22 -0
  1017. package/dist/utils/typingActive.js.map +1 -0
  1018. package/dist/utils/typingLoopBackstop.d.ts +64 -0
  1019. package/dist/utils/typingLoopBackstop.d.ts.map +1 -0
  1020. package/dist/utils/typingLoopBackstop.js +68 -0
  1021. package/dist/utils/typingLoopBackstop.js.map +1 -0
  1022. package/dist/utils/verbosityRender.d.ts +18 -0
  1023. package/dist/utils/verbosityRender.d.ts.map +1 -0
  1024. package/dist/utils/verbosityRender.js +17 -0
  1025. package/dist/utils/verbosityRender.js.map +1 -0
  1026. package/dist/utils/wedgeRecovery.d.ts +57 -0
  1027. package/dist/utils/wedgeRecovery.d.ts.map +1 -0
  1028. package/dist/utils/wedgeRecovery.js +24 -0
  1029. package/dist/utils/wedgeRecovery.js.map +1 -0
  1030. package/dist/utils.d.ts +30 -0
  1031. package/dist/utils.d.ts.map +1 -0
  1032. package/dist/utils.js +61 -0
  1033. package/dist/utils.js.map +1 -0
  1034. package/dist/validation.d.ts +113 -0
  1035. package/dist/validation.d.ts.map +1 -0
  1036. package/dist/validation.js +226 -0
  1037. package/dist/validation.js.map +1 -0
  1038. package/dist/voiceQueue.d.ts +7 -0
  1039. package/dist/voiceQueue.d.ts.map +1 -0
  1040. package/dist/voiceQueue.js +33 -0
  1041. package/dist/voiceQueue.js.map +1 -0
  1042. package/examples/.env.example +17 -0
  1043. package/examples/docker-compose.yml +36 -0
  1044. package/package.json +83 -4
@@ -0,0 +1,4239 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ClaudeCliAdapter = exports.getNewPaneContent = exports.cleanOutput = exports.convertAnsiToMarkdown = exports.tmuxAsync = void 0;
37
+ exports.loadEffortPrefs = loadEffortPrefs;
38
+ exports.saveEffortPref = saveEffortPref;
39
+ exports.checkIsValidUuid = checkIsValidUuid;
40
+ exports.buildConfigDirEnvPrefix = buildConfigDirEnvPrefix;
41
+ exports.parseClaudeSessionIdFromCommand = parseClaudeSessionIdFromCommand;
42
+ exports.checkIsStatusOutput = checkIsStatusOutput;
43
+ exports.extractClaudeQuestion = extractClaudeQuestion;
44
+ exports.checkIsClaudeQuestionBlock = checkIsClaudeQuestionBlock;
45
+ exports.checkIsSelectorControlReply = checkIsSelectorControlReply;
46
+ exports.extractClaudeSurvey = extractClaudeSurvey;
47
+ exports.checkIsClaudeSurvey = checkIsClaudeSurvey;
48
+ exports.checkShouldDismissSurvey = checkShouldDismissSurvey;
49
+ exports.stripSurveyChromeLines = stripSurveyChromeLines;
50
+ exports.getClaudeSendKeysPlan = getClaudeSendKeysPlan;
51
+ exports.getClaudeReplyRoute = getClaudeReplyRoute;
52
+ exports.checkIsClaudeLoginPaste = checkIsClaudeLoginPaste;
53
+ exports.getLastSharpTableBlock = getLastSharpTableBlock;
54
+ exports.checkHasContentAfterLastSharpTable = checkHasContentAfterLastSharpTable;
55
+ exports.maskSharpTableLines = maskSharpTableLines;
56
+ exports.getTableStabilizationDecision = getTableStabilizationDecision;
57
+ exports.mergeAdjacentFences = mergeAdjacentFences;
58
+ exports.stripTuiElementsWithContext = stripTuiElementsWithContext;
59
+ exports.stripTuiElements = stripTuiElements;
60
+ exports.getClaudeProjectSlug = getClaudeProjectSlug;
61
+ exports.getClaudeProjectsRoot = getClaudeProjectsRoot;
62
+ exports.getClaudeTranscriptPath = getClaudeTranscriptPath;
63
+ exports.readRecentClaudeTurns = readRecentClaudeTurns;
64
+ exports.readClaudeReattachTranscript = readClaudeReattachTranscript;
65
+ exports.listClaudeSessionsForWorkDir = listClaudeSessionsForWorkDir;
66
+ exports.checkIsBareSlashCommand = checkIsBareSlashCommand;
67
+ exports.checkIsClaudeBusy = checkIsClaudeBusy;
68
+ exports.checkIsClaudeSessionBusy = checkIsClaudeSessionBusy;
69
+ exports.checkIsInputEchoFrame = checkIsInputEchoFrame;
70
+ exports.checkIsForwardedEcho = checkIsForwardedEcho;
71
+ exports.checkLooksUnsubmitted = checkLooksUnsubmitted;
72
+ exports.getClaudeAgentErrorLine = getClaudeAgentErrorLine;
73
+ exports.checkIsClaudePromptReady = checkIsClaudePromptReady;
74
+ exports.checkShouldAdvanceWatermark = checkShouldAdvanceWatermark;
75
+ exports.checkIsClaudeUninterruptible = checkIsClaudeUninterruptible;
76
+ exports.getResumeSeedDecision = getResumeSeedDecision;
77
+ const crypto_1 = require("crypto");
78
+ const events_1 = require("events");
79
+ const fs = __importStar(require("fs"));
80
+ const os = __importStar(require("os"));
81
+ const path = __importStar(require("path"));
82
+ const utils_1 = require("../utils");
83
+ const sessionKey_1 = require("../sessionKey");
84
+ const apiErrorRetry_1 = require("../apiErrorRetry");
85
+ const installManager_1 = require("../installManager");
86
+ const mcpConfig_1 = require("../mcpConfig");
87
+ const claudeCompactHook_1 = require("../utils/claudeCompactHook");
88
+ const claudePlatformFlags_1 = require("./claudePlatformFlags");
89
+ const state_1 = require("../state");
90
+ const threadContextPreamble_1 = require("../threadContextPreamble");
91
+ const claudeSessionTitle_1 = require("../utils/claudeSessionTitle");
92
+ const resolveBinary_1 = require("../utils/resolveBinary");
93
+ const claudeAuthLogin_1 = require("../utils/claudeAuthLogin");
94
+ const claudeProfiles_1 = require("../utils/claudeProfiles");
95
+ const claudeScrapeShapes_1 = require("../utils/claudeScrapeShapes");
96
+ const progressLine_1 = require("../progressLine");
97
+ const serialQueue_1 = require("../utils/serialQueue");
98
+ const i18n_1 = require("../i18n");
99
+ const resumeContext_1 = require("../resumeContext");
100
+ const effortLevels_1 = require("../effortLevels");
101
+ const pollBackoff_1 = require("../utils/pollBackoff");
102
+ const tmuxExec_1 = require("../utils/tmuxExec");
103
+ Object.defineProperty(exports, "tmuxAsync", { enumerable: true, get: function () { return tmuxExec_1.tmuxAsync; } });
104
+ const ansiClean_1 = require("../utils/ansiClean");
105
+ Object.defineProperty(exports, "convertAnsiToMarkdown", { enumerable: true, get: function () { return ansiClean_1.convertAnsiToMarkdown; } });
106
+ Object.defineProperty(exports, "cleanOutput", { enumerable: true, get: function () { return ansiClean_1.cleanOutput; } });
107
+ const paneDiff_1 = require("../utils/paneDiff");
108
+ Object.defineProperty(exports, "getNewPaneContent", { enumerable: true, get: function () { return paneDiff_1.getNewPaneContent; } });
109
+ const paneResizeGuard_1 = require("../utils/paneResizeGuard");
110
+ const tmuxSessionName_1 = require("../utils/tmuxSessionName");
111
+ const effortStartupKeystroke_1 = require("../utils/effortStartupKeystroke");
112
+ const claudeRuntimeInfo_1 = require("../utils/claudeRuntimeInfo");
113
+ const displayVerbosity_1 = require("../utils/displayVerbosity");
114
+ const claudeSubagentTail_1 = require("../utils/claudeSubagentTail");
115
+ const recentRelayWindow_1 = require("../utils/recentRelayWindow");
116
+ const claudeChunkClassifier_1 = require("../utils/claudeChunkClassifier");
117
+ const claudeRelayRouting_1 = require("../utils/claudeRelayRouting");
118
+ /**
119
+ * @description Hard cap on resume flood-suppression polls. The normal exit is
120
+ * "pane non-empty and unchanged across 2 consecutive polls" (the restored
121
+ * transcript finished painting), but if the paint stutters indefinitely we
122
+ * force-exit after this many polls (≈ {@link basePollIntervalMs} × this) so seeding
123
+ * can never wedge a session into permanent silence. See {@link getResumeSeedDecision}.
124
+ */
125
+ const resumeSeedMaxPolls = 40;
126
+ const claudePath = (0, resolveBinary_1.resolveClaudeBinary)();
127
+ /**
128
+ * @description Permission flags for EVERY bot-launched Claude session (start
129
+ * AND resume), forcing allow-all so the user never gets permission prompts.
130
+ *
131
+ * `--dangerously-skip-permissions` only sets bypass as the INITIAL mode and
132
+ * suppresses the "enter bypass?" confirm — it is NOT a runtime lock (shift+tab
133
+ * cycles bypass → auto → default). A live session ended up in a prompting,
134
+ * non-bypass mode despite the flag (live 2026-06-12); the bot never
135
+ * sends shift+tab itself, so the drift comes from Claude's own mode state, not
136
+ * the bot. `--permission-mode bypassPermissions` is the documented per-session
137
+ * override (verified present in v2.1.175 `--help`); passing it on every launch
138
+ * re-asserts bypass on each fresh start and resume — verified to land in
139
+ * "bypass permissions on" for both. Residual circuit-breakers (claude always
140
+ * asks for `rm -rf /` or `rm -rf ~`) are intentional and out of scope.
141
+ */
142
+ const claudePermissionArgs = [
143
+ '--dangerously-skip-permissions',
144
+ '--permission-mode',
145
+ 'bypassPermissions',
146
+ ];
147
+ /**
148
+ * @description When set (`CLAUDE_SCRAPE_DEBUG=1`), the poll loop logs the FULL
149
+ * RAW and FILTERED chunk bodies — a forensic dump useful when debugging the
150
+ * scrape pipeline. Default OFF: those two sync stdout writes per changed poll
151
+ * (up to ~600KB each under load) were starving the event loop in prod, so we
152
+ * log only one-line size summaries instead. Read once at module init like the
153
+ * other env flags in the codebase (see `outputTrace.ts`).
154
+ */
155
+ const isClaudeScrapeDebugEnabled = process.env.CLAUDE_SCRAPE_DEBUG === '1';
156
+ /**
157
+ * @description Max number of `.jsonl` transcripts to parse per folder.
158
+ *
159
+ * Parsing each transcript is the expensive step (stream the whole file to
160
+ * collect title/cwd/timestamp). We bound it to the most-recent N by mtime
161
+ * — the bot only ever shows the top {@link sessionsDisplayLimit}, so 30 is
162
+ * plenty of headroom even after the `recordedCwd === workDir` filter drops
163
+ * some entries.
164
+ */
165
+ const sessionsParseLimit = 30;
166
+ /** Cap on a session title's length so the list stays readable in Telegram. */
167
+ const sessionTitleMaxLength = 60;
168
+ /**
169
+ * @description Per-thread Claude `/effort` choice (plan 2026-05-30-effort-command, D6).
170
+ *
171
+ * Claude itself persists `effortLevel` GLOBALLY in its own `settings.json`,
172
+ * so two threads driven by the same Linux user would otherwise overwrite
173
+ * each other's choice across sessions. We mirror the per-thread model-prefs
174
+ * pattern (see `openCodeAdapter.ts:modelStateFile`) — a tiny JSON map
175
+ * `{ "<chatId>:<threadId>": "<level>" }` under DATA_DIR, used for the
176
+ * banner / picker UI **only**. Live apply still goes through the TUI
177
+ * keystroke path, and claude clamps unsupported levels for the current
178
+ * model (plan D2).
179
+ *
180
+ * The file is best-effort: a corrupt copy is archived and reset (same
181
+ * scheme as `loadStoredSessions`), and write failures only log — they
182
+ * never block the user-facing `/effort` reply.
183
+ */
184
+ const effortPrefsFile = path.join((0, state_1.resolveDataDir)(), '.claude-effort-prefs.json');
185
+ function loadEffortPrefs() {
186
+ try {
187
+ if (!fs.existsSync(effortPrefsFile))
188
+ return {};
189
+ const raw = fs.readFileSync(effortPrefsFile, 'utf-8');
190
+ const parsed = JSON.parse(raw);
191
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
192
+ ? parsed
193
+ : {};
194
+ }
195
+ catch (e) {
196
+ console.error(`[Claude] loadEffortPrefs failed:`, e instanceof Error ? e.message : e);
197
+ if (fs.existsSync(effortPrefsFile)) {
198
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
199
+ try {
200
+ fs.renameSync(effortPrefsFile, `${effortPrefsFile}.corrupted-${ts}`);
201
+ }
202
+ catch (re) {
203
+ console.warn(`[Claude] archive of corrupt effort prefs failed:`, re);
204
+ }
205
+ }
206
+ return {};
207
+ }
208
+ }
209
+ function saveEffortPref(key, level) {
210
+ try {
211
+ const data = loadEffortPrefs();
212
+ data[(0, sessionKey_1.keyToString)(key)] = level;
213
+ fs.writeFileSync(effortPrefsFile, JSON.stringify(data, null, 2));
214
+ }
215
+ catch (e) {
216
+ console.error(`[Claude] saveEffortPref failed:`, e instanceof Error ? e.message : e);
217
+ }
218
+ }
219
+ /**
220
+ * @description Tmux session-name prefix that namespaces Claude's sessions on
221
+ * the tmux server. The careful build/parse (negative chatId, strict per-half
222
+ * regex) is shared with other tmux-driven backends in `utils/tmuxSessionName`;
223
+ * these thin wrappers just bind the `'claude'` prefix.
224
+ */
225
+ const claudeTmuxPrefix = 'claude';
226
+ /**
227
+ * @description Tmux session name for a `SessionKey`.
228
+ *
229
+ * Format: `claude-<chatId>-<threadId>`. Negative chat ids (forum supergroups
230
+ * are negative) keep their minus sign — tmux session names accept it. The
231
+ * format is `parse`-able back to `SessionKey` via {@link parseTmuxSessionName}.
232
+ */
233
+ function buildTmuxSessionName(key) {
234
+ return (0, tmuxSessionName_1.buildTmuxSessionName)(claudeTmuxPrefix, key);
235
+ }
236
+ /**
237
+ * @description Inverse of {@link buildTmuxSessionName}. Returns `null` for
238
+ * names that don't match our format (e.g. unrelated tmux sessions a user
239
+ * started by hand).
240
+ *
241
+ * Carefully handles negative chat ids: `claude--1001234-42` is `chatId=-1001234, threadId=42`.
242
+ */
243
+ function parseTmuxSessionName(name) {
244
+ return (0, tmuxSessionName_1.parseTmuxSessionName)(claudeTmuxPrefix, name);
245
+ }
246
+ /**
247
+ * @description Validate a UUID-shaped session id before it ever reaches a
248
+ * tmux command line. Defence-in-depth (audit S1): even though we now route
249
+ * every tmux call through argv (no shell interpolation), the UUID is later
250
+ * concatenated into the `shell-command` we hand to `tmux new-session`,
251
+ * which tmux execs via `$SHELL -c`. If a non-UUID value ever sneaks in
252
+ * (corrupted state.json, future user-facing `/resume <id>`), it would land
253
+ * in that shell. Rejecting up front keeps the surface tight.
254
+ */
255
+ function checkIsValidUuid(s) {
256
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s);
257
+ }
258
+ /**
259
+ * @description The `env CLAUDE_CONFIG_DIR=<dir>` argv prefix of a pane whose
260
+ * conversation uses a non-default account profile; empty for the default
261
+ * (`utils/claudeProfiles.ts`). Quoted with the rest of the argv by the caller.
262
+ */
263
+ function buildConfigDirEnvPrefix(configDir) {
264
+ return configDir === null ? [] : ['env', `${claudeProfiles_1.claudeConfigDirVariableName}=${configDir}`];
265
+ }
266
+ /**
267
+ * @description Pure parser — pulls a `--session-id <uuid>` value out of a
268
+ * claude command line. Lives outside the adapter class so it can be
269
+ * unit-tested without booting tmux. Quoting in the input is flexible
270
+ * because `pane_start_command` can come back as `--session-id 'uuid'`,
271
+ * `--session-id "uuid"`, `--session-id=uuid`, or just `--session-id uuid`,
272
+ * depending on how the original shell-command was assembled. Returns the
273
+ * lowercase UUID, or `null` if no valid UUID is found.
274
+ */
275
+ function parseClaudeSessionIdFromCommand(cmd) {
276
+ if (!cmd)
277
+ return null;
278
+ // Lookahead refuses to truncate a 37+ char "UUID-like" garbage tail to
279
+ // the first 36 chars: trailing hex must be followed by end-of-string,
280
+ // whitespace, or a quote. Production input comes from our own
281
+ // `pane_start_command`, but the anchor closes a defence-in-depth gap
282
+ // against future call sites that may pass weirder strings.
283
+ const match = cmd.match(/--session-id[\s'"=]+([0-9a-fA-F-]{36})(?=$|[\s'"])/);
284
+ if (!match)
285
+ return null;
286
+ const uuid = match[1].toLowerCase();
287
+ return checkIsValidUuid(uuid) ? uuid : null;
288
+ }
289
+ function normalizeToolCallLine(line) {
290
+ const trimmed = line.trim();
291
+ const toolPattern = /^(Bash|Read|Write|Edit|Glob|Grep|Task|TodoWrite|WebFetch|WebSearch)\s*\(/i;
292
+ const bulletToolPattern = /^([●○⏺])\s*(Bash|Read|Write|Edit|Glob|Grep|Task|TodoWrite|WebFetch|WebSearch)\s*\(/i;
293
+ const bulletMatch = trimmed.match(bulletToolPattern);
294
+ if (bulletMatch) {
295
+ const bullet = bulletMatch[1];
296
+ const rest = trimmed.slice(bulletMatch[1].length).trimStart();
297
+ const icon = bullet === '●' || bullet === '⏺' ? '⏳' : '✓';
298
+ return `${icon} ${rest}`;
299
+ }
300
+ const toolMatch = trimmed.match(toolPattern);
301
+ if (toolMatch) {
302
+ return `✓ ${trimmed}`;
303
+ }
304
+ return line;
305
+ }
306
+ /**
307
+ * @description Check if output consists only of transient status lines (spinners, progress).
308
+ * Uses generic heuristics instead of hardcoded spinner chars/words,
309
+ * because Claude CLI can change its TUI symbols and wording at any time.
310
+ *
311
+ * Key insight: real Claude content is substantial (> 200 chars, multi-sentence).
312
+ * Status/progress is short, has few lines, and contains indicators like … or time/token stats.
313
+ */
314
+ function checkIsStatusOutput(text) {
315
+ // The thinking-`short` collapse line the router emits ("💭 thought for {N}s" /
316
+ // "💭 думал {N} с", S5) is a DELIBERATE permanent artifact. Its short,
317
+ // glyph-led, sentence-less shape otherwise trips the heuristics below (and the
318
+ // EN form the `thought for \d` rule), so a standalone collapse chunk would be
319
+ // folded into the ephemeral status frame and lost. The 💭 marker is the bot's
320
+ // own collapse glyph — treat it as real permanent content.
321
+ if (text.trimStart().startsWith('💭'))
322
+ return false;
323
+ // A lone file-diff `NN +` change gutter (`40 +`, `3 +delta`) is PERMANENT
324
+ // output, not a transient spinner — its short, glyph-less shape otherwise
325
+ // trips the heuristics below and folds it into the ephemeral status frame, so
326
+ // it is never RECORDED into the relay window and its later re-render leaks
327
+ // unguarded (live diff-line leak 2026-06-24). Treating it as real output sends
328
+ // it down the `record` path so the re-render dedup (recentRelayWindow's
329
+ // `NN +`-gutter bypass) can suppress the repaint. Anchored to the gutter shape
330
+ // — `+1 done`, `3 lines changed`, `done` all stay status-eligible.
331
+ const gutterLines = text.split('\n').filter(line => line.trim() !== '');
332
+ if (gutterLines.length > 0 && gutterLines.every(line => claudeScrapeShapes_1.DIFF_CHANGE_GUTTER_RE.test(line))) {
333
+ return false;
334
+ }
335
+ // Real content is always substantial
336
+ if (text.length > 200)
337
+ return false;
338
+ const lines = text.split('\n').filter(l => l.trim());
339
+ if (lines.length === 0)
340
+ return false;
341
+ // Many non-empty lines = real content
342
+ if (lines.length > 3)
343
+ return false;
344
+ return lines.every(line => {
345
+ const trimmed = line.trim();
346
+ // The ANSWER bullet `●`/`⏺` followed by text is real OUTPUT, never a spinner —
347
+ // it is Claude's assistant-output marker, NOT a liveness glyph (the liveness
348
+ // set is `✻✽✶✢·*○`). Lumping it in with the spinner glyphs made a fully short
349
+ // answer like `● DONE` read as a glyph-led status frame and vanish entirely
350
+ // (live bug 2026-06-24). A bare bullet with no text stays status-eligible.
351
+ if (/^[●⏺]\s+\S/.test(trimmed))
352
+ return false;
353
+ // Tree structure / subagent progress lines (├─, └─, │, ─)
354
+ if (/^[├└│─]/.test(trimmed))
355
+ return true;
356
+ // Contains unicode ellipsis — universal spinner/progress indicator ("Nesting…", "Reading…", "Simmering…")
357
+ if (/…/.test(trimmed))
358
+ return true;
359
+ // Contains progress stats: time patterns (3m 36s), token counts (↓ 12.2k tokens), thought duration
360
+ if (/\d+[smh]\b.*[·↓]|↓\s*[\d.]+k?\s*tokens|thought for \d/i.test(trimmed))
361
+ return true;
362
+ // A short answer fragment that ends a sentence ("Done.", "OK.", "Found 3
363
+ // bugs.") is real content, not a spinner — spinners always carry a `…`, a
364
+ // glyph, or token stats (caught above). Don't swallow it into a status.
365
+ const isShortSentence = /[а-яёa-z]{2,}/i.test(trimmed) && /[.!?]$/.test(trimmed);
366
+ if (isShortSentence)
367
+ return false;
368
+ // Two or more significant words (3+ letters each) ANYWHERE = real prose, not a
369
+ // spinner icon. The words need NOT be adjacent — a short filler word / digits /
370
+ // punctuation between them (e.g. «Итог ПО миксу», "Решение ЗА тобой") must not
371
+ // defeat the count (live bug 2026-06-24: the old adjacency requirement read a
372
+ // real 3-word heading as word-less → status → swallowed). The glyph/`…`/stats/
373
+ // tree STATUS checks above run FIRST, so a genuine `✻ Compacting conversation…`
374
+ // (2 real words but `…`-bearing AND glyph-led) still reads as status.
375
+ const significantWordCount = (trimmed.match(/[а-яёa-z]{3,}/gi) ?? []).length;
376
+ if (significantWordCount >= 2)
377
+ return false;
378
+ // A short line with no sentence-like structure and < 2 real words — a lone
379
+ // spinner/icon/stat fragment.
380
+ if (trimmed.length < 40)
381
+ return true;
382
+ return false;
383
+ });
384
+ }
385
+ /** Box-drawing chars Claude's TUI wraps option/question lines in. */
386
+ const QUESTION_BORDER_REGEX = /^[│┃]\s?|\s*[│┃]\s*$/g;
387
+ /** Numbered option line, optionally cursor-highlighted, optionally boxed. */
388
+ const QUESTION_OPTION_REGEX = /^(❯\s*)?(\d{1,2})[.)]\s+(\S.*?)\s*$/;
389
+ /** Lines that are pure chrome (borders / blanks / nav hints), never prose. */
390
+ const QUESTION_CHROME_REGEX = /^[╭╮╰╯┌┐└┘├┤┬┴┼─━│┃\s]*$|Enter to select|esc to|↑↓|↑\/↓|to cycle|shift\+tab|to confirm|to submit|use arrow/i;
391
+ /**
392
+ * The "Notes: press n to add notes" affordance the side-by-side
393
+ * AskUserQuestion layout renders under the preview pane. Terminal-only (a
394
+ * Telegram user cannot press `n`), so it is question chrome on the scrape
395
+ * side AND dropped on the output side. ANCHORED whole-line (live leak
396
+ * 2026-06-10: it reached Telegram as naked text when the selector
397
+ * frame fell through to the plain-output path) — prose merely mentioning the
398
+ * phrase mid-sentence must survive.
399
+ */
400
+ const QUESTION_NOTES_HINT_REGEX = /^\s*Notes: press n to add notes\s*$/i;
401
+ /**
402
+ * The UNNUMBERED "Chat about this" meta-row of the side-by-side
403
+ * AskUserQuestion layout (the single-column variant numbers it as a real
404
+ * option, e.g. `5. Chat about this`, which this regex deliberately does NOT
405
+ * match). A Telegram user gets the same affordance by simply typing free text
406
+ * (which breaks out of the selector as a fresh turn), so the row is chrome,
407
+ * not an option. ANCHORED whole-line, optional `❯` cursor — a prose sentence
408
+ * containing the phrase must survive.
409
+ */
410
+ const QUESTION_CHAT_ABOUT_REGEX = /^\s*(?:❯\s*)?Chat about this\s*$/i;
411
+ /** A positive "this is an interactive prompt" signal near the option group. */
412
+ const QUESTION_SELECT_HINT_REGEX = /Enter to select|to select|↑↓|↑\/↓|use arrow|esc to/i;
413
+ const QUESTION_MIN_OPTIONS = 2;
414
+ /**
415
+ * Consecutive poll diffs with NO selector detected before a pending question is
416
+ * treated as resolved (`questionGone`). 2 = ignore a single transient scrape
417
+ * miss while the selector is still repainting; a genuine resolve stays absent
418
+ * across successive diffs. See {@link ClaudeSession.questionAbsentPolls}.
419
+ */
420
+ const questionAbsentPollThreshold = 2;
421
+ /** How far below the last option to look for the "Enter to select" hint. */
422
+ const QUESTION_HINT_LOOKAHEAD = 4;
423
+ /**
424
+ * How far above a non-option line to look for another option when deciding
425
+ * whether the option group is still open. Real prompts interleave options
426
+ * with indented description sub-lines and full-width `────` separators (e.g.
427
+ * AskUserQuestion: option → description → option, or option → separator →
428
+ * option); a small look-back spans those gaps without merging an unrelated
429
+ * numbered list that sits further up the pane.
430
+ */
431
+ const QUESTION_OPTION_LOOKBACK = 4;
432
+ /**
433
+ * Box corner chars. The upward option-group walk stops at a box edge so a
434
+ * numbered list in prose sitting just above the box can't merge into the
435
+ * options — the box top border separates the prompt from preceding prose.
436
+ */
437
+ const QUESTION_BOX_EDGE_REGEX = /[╭╮╰╯]/;
438
+ /**
439
+ * Every box-drawing glyph a right-hand preview pane can put ON an option line
440
+ * in the side-by-side AskUserQuestion layout (options column left, per-option
441
+ * `preview` snippet boxed right):
442
+ *
443
+ * ❯ 1. winston ┌────────────────────────────┐
444
+ * 2. pino │ import winston from '…'; │
445
+ * 3. console │ │
446
+ * └────────────────────────────┘
447
+ *
448
+ * An option line is CUT at the first of these glyphs and only the left part
449
+ * is parsed — covers sharp AND rounded corner families plus double-line
450
+ * variants defensively (live bug #9, 2026-06-10: unparsed preview
451
+ * fragments polluted the labels / broke extraction and the question reached
452
+ * Telegram with ZERO options). Single-column frames carry none of these
453
+ * glyphs on option lines after {@link stripQuestionBoxBorder}, so the cut is
454
+ * a no-op there.
455
+ */
456
+ const PREVIEW_BOX_GLYPH_CLASS = '┌┐└┘├┤┬┴┼─━│┃╭╮╰╯╔╗╚╝║═';
457
+ const PREVIEW_BOX_GLYPH_REGEX = new RegExp(`[${PREVIEW_BOX_GLYPH_CLASS}]`);
458
+ /** A line whose first non-space char is a box glyph = right-column preview content, never an option/description. */
459
+ const PREVIEW_PANE_FRAGMENT_REGEX = new RegExp(`^[${PREVIEW_BOX_GLYPH_CLASS}]`);
460
+ /**
461
+ * The ANSI-bold artifact `cleanOutput` leaves on the ❯-highlighted option of
462
+ * the side-by-side layout: the TUI bolds ` winston` (space included), and the
463
+ * bold→`*…*` conversion trims the span, yielding `❯ 1.*winston*` — no space
464
+ * after the dot, so {@link QUESTION_OPTION_REGEX} missed the line, the cursor
465
+ * was "gone", and the whole frame fell through to the plain-output path (the
466
+ * live zero-options leak 2026-06-10). Normalised back to
467
+ * `❯ 1. winston`. ANCHORED start-to-end: the whole remainder after `N.` must
468
+ * be ONE `*…*` span, so prose like `1.5 *important* note` can never match.
469
+ */
470
+ const HIGHLIGHTED_OPTION_BOLD_ARTIFACT_REGEX = /^(❯\s*)?(\d{1,2})([.)])\s*\*([^*]+)\*$/;
471
+ /**
472
+ * How far below the last option to look for the strong `Enter to select`
473
+ * footer when the layout is POSITIVELY identified as side-by-side (an option
474
+ * line carried a preview-box fragment). The preview pane + the "Notes:" line
475
+ * + separator + "Chat about this" sit between the options and the footer, so
476
+ * the normal {@link QUESTION_HINT_LOOKAHEAD} cannot reach it. Only applied to
477
+ * side-by-side frames and only with the strong phrase — a prose numbered list
478
+ * has no box fragments on its lines, so this never widens the prose gate.
479
+ */
480
+ const SIDE_BY_SIDE_FOOTER_LOOKAHEAD = 20;
481
+ /** The strong footer anchor required within the extended side-by-side window. */
482
+ const QUESTION_FOOTER_STRONG_REGEX = /Enter to select/i;
483
+ /** Minimum leading-space width for a sub-line to count as an option's description. */
484
+ const QUESTION_DESCRIPTION_MIN_INDENT = 4;
485
+ /** How far below the LAST option to collect its trailing description sub-lines. */
486
+ const QUESTION_DESCRIPTION_TRAILING_LOOKAHEAD = 3;
487
+ /**
488
+ * Header walk caps. A permission prompt's action box (`Bash command` + the
489
+ * command + a one-line description, or `Edit file …` + a diff) sits above the
490
+ * question line; the header walk now climbs through it (spanning blank lines)
491
+ * up to the box boundary. SCAN_LIMIT bounds the upward climb itself (runaway
492
+ * guard on a malformed pane); MAX_LINES/MAX_CHARS bound the RELAYED text — over
493
+ * the cap, {@link truncateQuestionHeader} keeps the identifying TOP and the
494
+ * question line at the BOTTOM and drops the middle. A Bash command is well
495
+ * under the cap → never truncated.
496
+ */
497
+ const QUESTION_HEADER_SCAN_LIMIT = 200;
498
+ const QUESTION_HEADER_MAX_LINES = 26;
499
+ const QUESTION_HEADER_MAX_CHARS = 2000;
500
+ const QUESTION_HEADER_HEAD_LINES = 16;
501
+ const QUESTION_HEADER_TAIL_LINES = 6;
502
+ const QUESTION_HEADER_TRUNCATION_MARKER = '… (truncated — see terminal)';
503
+ /**
504
+ * AskUserQuestion category tab marker (`☐ Foo` / `☑ Bar`) — chrome that sits
505
+ * above the question line; a boundary for the header walk so it is never
506
+ * relayed (the side-by-side `☐` no-leak test). Anchored at line start.
507
+ */
508
+ const QUESTION_TAB_MARKER_REGEX = /^[☐☑]/;
509
+ function stripQuestionBoxBorder(line) {
510
+ return line.replace(QUESTION_BORDER_REGEX, '');
511
+ }
512
+ /**
513
+ * Bound an over-long captured header (an oversized Edit/Write diff box): keep
514
+ * the head (title + first lines — the most identifying part) and the tail (the
515
+ * question line sits at the bottom of the header), drop the middle behind one
516
+ * marker. A within-budget header passes through unchanged.
517
+ */
518
+ function truncateQuestionHeader(headerLines) {
519
+ const totalChars = headerLines.reduce((sum, line) => sum + line.length + 1, 0);
520
+ if (headerLines.length <= QUESTION_HEADER_MAX_LINES && totalChars <= QUESTION_HEADER_MAX_CHARS) {
521
+ return headerLines;
522
+ }
523
+ return [
524
+ ...headerLines.slice(0, QUESTION_HEADER_HEAD_LINES),
525
+ QUESTION_HEADER_TRUNCATION_MARKER,
526
+ ...headerLines.slice(-QUESTION_HEADER_TAIL_LINES),
527
+ ];
528
+ }
529
+ /**
530
+ * The left-column part of a side-by-side line: everything before the first
531
+ * preview-box glyph, trailing padding dropped. A line without box glyphs
532
+ * (single-column layout) passes through unchanged.
533
+ */
534
+ function getPreviewPaneCutLeftPart(line) {
535
+ const cutIndex = line.search(PREVIEW_BOX_GLYPH_REGEX);
536
+ return (cutIndex === -1 ? line : line.slice(0, cutIndex)).trimEnd();
537
+ }
538
+ /**
539
+ * Normalise a line for option matching: cut the right-hand preview-box
540
+ * fragment (side-by-side layout), then undo the highlighted-label bold
541
+ * artifact. A single-column line passes through unchanged.
542
+ */
543
+ function getOptionLineCandidate(line) {
544
+ return getPreviewPaneCutLeftPart(line).replace(HIGHLIGHTED_OPTION_BOLD_ARTIFACT_REGEX, '$1$2$3 $4');
545
+ }
546
+ function checkIsOptionLine(line) {
547
+ return QUESTION_OPTION_REGEX.test(getOptionLineCandidate(line));
548
+ }
549
+ function getLineIndentWidth(line) {
550
+ return line.length - line.trimStart().length;
551
+ }
552
+ /**
553
+ * Whether a (border-stripped, untrimmed) line is an option's indented
554
+ * description sub-line. Conservative on purpose: must be non-blank, not
555
+ * question chrome, not right-column preview content, not itself an option,
556
+ * and indented at least {@link QUESTION_DESCRIPTION_MIN_INDENT} — the TUI
557
+ * indents descriptions under their option label.
558
+ */
559
+ function checkIsOptionDescriptionLine(borderStrippedLine) {
560
+ const trimmed = borderStrippedLine.trim();
561
+ if (trimmed === '')
562
+ return false;
563
+ if (checkIsQuestionChrome(trimmed))
564
+ return false;
565
+ if (PREVIEW_PANE_FRAGMENT_REGEX.test(trimmed))
566
+ return false;
567
+ if (checkIsOptionLine(trimmed))
568
+ return false;
569
+ return getLineIndentWidth(borderStrippedLine) >= QUESTION_DESCRIPTION_MIN_INDENT;
570
+ }
571
+ /** Whether another option line sits within {@link QUESTION_OPTION_LOOKBACK} lines above `index`. */
572
+ function checkHasOptionAbove(lines, index) {
573
+ const top = Math.max(0, index - QUESTION_OPTION_LOOKBACK);
574
+ for (let i = index - 1; i >= top; i--) {
575
+ if (checkIsOptionLine(lines[i]))
576
+ return true;
577
+ }
578
+ return false;
579
+ }
580
+ function checkIsQuestionChrome(line) {
581
+ return (QUESTION_CHROME_REGEX.test(line) ||
582
+ QUESTION_NOTES_HINT_REGEX.test(line) ||
583
+ QUESTION_CHAT_ABOUT_REGEX.test(line));
584
+ }
585
+ /**
586
+ * @description Detect + extract the active interactive question/choice block
587
+ * from the FULL Claude pane, returning a durable rendering and a
588
+ * cursor-invariant signature, or `null` when the pane isn't confidently
589
+ * showing a question.
590
+ *
591
+ * Operates on the whole pane (not a poll diff) on purpose: moving the `❯`
592
+ * cursor repaints only the two changed option lines, so a signature taken
593
+ * from the diff would be a partial option set and wouldn't match the full
594
+ * box — the de-dup would fail and every keystroke would re-spam the thread.
595
+ * Reading the full option group makes the signature stable across cursor
596
+ * moves. We take the LAST option group (the active prompt sits at the bottom
597
+ * of the pane) and the header line(s) just above it.
598
+ *
599
+ * Conservative (plan §2026-05-30 / S2): requires a `❯` cursor on an option
600
+ * OR an "Enter to select"-style hint right below the group — a plain numbered
601
+ * list in prose has neither, so it returns `null` and falls through to the
602
+ * normal output path. The `❯`-highlighted option text is preserved (the old
603
+ * `stripTuiElements` path discarded every `^❯` line, losing the selection).
604
+ *
605
+ * Two layouts (plan §2026-06-09 question-ux / S4):
606
+ * - single-column — options with optional indented description sub-lines,
607
+ * which are ATTACHED to their option in the rendering (S1: previously
608
+ * discarded);
609
+ * - side-by-side (options WITH `preview` content) — each option line carries
610
+ * a right-hand preview-box fragment that is CUT off; the preview body is
611
+ * deliberately NOT relayed (Telegram gets labels + descriptions, the
612
+ * preview is a TUI-only nicety). See {@link PREVIEW_BOX_GLYPH_CLASS} and
613
+ * {@link HIGHLIGHTED_OPTION_BOLD_ARTIFACT_REGEX} for the live bug both
614
+ * rules close.
615
+ */
616
+ function extractClaudeQuestion(text) {
617
+ const borderStrippedLines = text.split('\n').map(line => stripQuestionBoxBorder(line));
618
+ const lines = borderStrippedLines.map(line => line.trim());
619
+ // Bottom-most option line anchors the active prompt (it sits at the pane
620
+ // bottom). The option run is NOT contiguous — descriptions / separators sit
621
+ // between options — so walk up spanning any non-option line while another
622
+ // option is still within reach above; stop once it isn't (that's the header).
623
+ let end = -1;
624
+ for (let i = lines.length - 1; i >= 0; i--) {
625
+ if (checkIsOptionLine(lines[i])) {
626
+ end = i;
627
+ break;
628
+ }
629
+ }
630
+ if (end === -1)
631
+ return null;
632
+ let start = end;
633
+ for (let i = end - 1; i >= 0; i--) {
634
+ if (checkIsOptionLine(lines[i])) {
635
+ start = i;
636
+ continue;
637
+ }
638
+ if (QUESTION_BOX_EDGE_REGEX.test(lines[i]))
639
+ break;
640
+ if (!checkHasOptionAbove(lines, i))
641
+ break;
642
+ }
643
+ // Collect options AND their indented description sub-lines (S1: the old
644
+ // flat `.filter(checkIsOptionLine)` discarded descriptions). A description
645
+ // attaches only while DIRECTLY under its option (or a previous description
646
+ // line of the same option) — chrome / separators / preview fragments end
647
+ // the run, so an indented stray further down can never mis-attach.
648
+ const options = [];
649
+ let isSideBySideLayout = false;
650
+ let isDirectlyUnderOption = false;
651
+ for (let i = start; i <= end; i++) {
652
+ const match = getOptionLineCandidate(lines[i]).match(QUESTION_OPTION_REGEX);
653
+ if (match) {
654
+ isSideBySideLayout = isSideBySideLayout || PREVIEW_BOX_GLYPH_REGEX.test(lines[i]);
655
+ options.push({
656
+ highlighted: Boolean(match[1]),
657
+ number: match[2],
658
+ label: match[3].trim(),
659
+ descriptionLines: [],
660
+ });
661
+ isDirectlyUnderOption = true;
662
+ continue;
663
+ }
664
+ if (isDirectlyUnderOption && checkIsOptionDescriptionLine(borderStrippedLines[i])) {
665
+ // The cut keeps a description clean if a right-column preview fragment
666
+ // rides the same row (side-by-side); single-column lines are unchanged.
667
+ options[options.length - 1].descriptionLines.push(getPreviewPaneCutLeftPart(lines[i]));
668
+ continue;
669
+ }
670
+ isDirectlyUnderOption = false;
671
+ }
672
+ if (options.length < QUESTION_MIN_OPTIONS)
673
+ return null;
674
+ // The LAST option's description sub-line(s) sit BELOW `end`; collect the
675
+ // directly-adjacent qualifying run (bounded, stops at the first blank /
676
+ // chrome / preview-fragment line).
677
+ const trailingLimit = Math.min(lines.length - 1, end + QUESTION_DESCRIPTION_TRAILING_LOOKAHEAD);
678
+ for (let i = end + 1; i <= trailingLimit; i++) {
679
+ if (!checkIsOptionDescriptionLine(borderStrippedLines[i]))
680
+ break;
681
+ options[options.length - 1].descriptionLines.push(getPreviewPaneCutLeftPart(lines[i]));
682
+ }
683
+ // Positive interactive signal, or it's not a real choice prompt.
684
+ const hasCursor = options.some(option => option.highlighted);
685
+ if (!hasCursor) {
686
+ const tail = lines.slice(end + 1, end + 1 + QUESTION_HINT_LOOKAHEAD).join('\n');
687
+ if (!QUESTION_SELECT_HINT_REGEX.test(tail)) {
688
+ // Side-by-side only: the preview pane pushes the footer beyond the
689
+ // normal lookahead (e.g. the cursor sits on the unnumbered "Chat about
690
+ // this" meta-row, so no option is highlighted). Demand the STRONG
691
+ // footer phrase within the extended window; never applied to frames
692
+ // without preview fragments, so the prose gate stays as tight as before.
693
+ if (!isSideBySideLayout)
694
+ return null;
695
+ const extendedTail = lines
696
+ .slice(end + 1, end + 1 + SIDE_BY_SIDE_FOOTER_LOOKAHEAD)
697
+ .join('\n');
698
+ if (!QUESTION_FOOTER_STRONG_REGEX.test(extendedTail))
699
+ return null;
700
+ }
701
+ }
702
+ // Header: skip the blank gap directly above the options, then climb to the
703
+ // box boundary collecting every prose line. A permission prompt's action box
704
+ // (`Bash command` + the command + a one-line description) sits ABOVE a blank
705
+ // line that separates it from `Do you want to proceed?`, so the walk must
706
+ // SPAN whitespace-only chrome (kept as a blank separator) and stop only at
707
+ // NON-blank chrome — the full-width `────` divider (current layout) or a
708
+ // `╭│` box border (Edit/older layout) — which bounds the box and keeps the
709
+ // transcript prose above it out (regression-guarded by the `codebase` /
710
+ // `alpha.ts` no-leak tests). Bounded by QUESTION_HEADER_MAX_LINES so a
711
+ // malformed pane can't climb the whole scrollback.
712
+ let h = start - 1;
713
+ while (h >= 0 && checkIsQuestionChrome(lines[h]))
714
+ h--;
715
+ const headerLines = [];
716
+ while (h >= 0 && !checkIsOptionLine(lines[h]) && headerLines.length < QUESTION_HEADER_SCAN_LIMIT) {
717
+ const line = lines[h];
718
+ // AskUserQuestion category tab (`☐ Foo`) is chrome above the question — a
719
+ // boundary, never relayed.
720
+ if (QUESTION_TAB_MARKER_REGEX.test(line))
721
+ break;
722
+ if (checkIsQuestionChrome(line)) {
723
+ // Non-blank chrome (divider / box border / nav hint) is the box edge.
724
+ if (line !== '')
725
+ break;
726
+ // Blank line inside the box — keep it as a separator and keep climbing.
727
+ headerLines.unshift(line);
728
+ h--;
729
+ continue;
730
+ }
731
+ headerLines.unshift(line);
732
+ h--;
733
+ }
734
+ const header = truncateQuestionHeader(headerLines).join('\n').trim();
735
+ // Descriptions render indented under their option label, mirroring the
736
+ // OpenCode question body (`buildQuestionBodyLines`).
737
+ const renderedOptions = options
738
+ .map(option => {
739
+ const optionLine = `${option.highlighted ? '❯' : ' '} ${option.number}. ${option.label}`;
740
+ const descriptionLines = option.descriptionLines.map(line => ` ${line}`);
741
+ return [optionLine, ...descriptionLines].join('\n');
742
+ })
743
+ .join('\n');
744
+ // Labels only — descriptions and the cursor stay out so the signature is
745
+ // stable across cursor moves and description repaints (de-dup holds).
746
+ const signature = options.map(option => `${option.number}.${option.label}`).join('|');
747
+ return {
748
+ text: header ? `${header}\n\n${renderedOptions}` : renderedOptions,
749
+ signature,
750
+ };
751
+ }
752
+ /** Whether `text` is confidently showing an interactive question/choice block. */
753
+ function checkIsClaudeQuestionBlock(text) {
754
+ return extractClaudeQuestion(text) !== null;
755
+ }
756
+ /**
757
+ * @description Classify a user reply sent WHILE a selector is on screen. A
758
+ * "control" reply is meant to drive the selector in place — a bare option
759
+ * number (1–2 digits) or a single `y`/`n` — so it must be typed straight into
760
+ * the TUI with no Escape. Anything else (a sentence, a new instruction) is a
761
+ * free-form message: the bot first sends Escape to cancel the selector, then
762
+ * forwards it as a fresh turn. Kept pure + exported so the routing decision is
763
+ * unit-testable without a live tmux session.
764
+ */
765
+ function checkIsSelectorControlReply(text) {
766
+ const trimmed = text.trim();
767
+ return /^\d{1,2}$/.test(trimmed) || /^[yYnN]$/.test(trimmed);
768
+ }
769
+ /**
770
+ * @description The KNOWN survey header texts — a CLOSED alternation (a future
771
+ * rewording becomes variant #3 here), matched ANCHORED start-to-end of a line
772
+ * (after an optional leading `●`/`⏺` assistant bullet, before an optional
773
+ * trailing ` (optional)`), NEVER as a substring. The matched header text is
774
+ * captured (group 1) and feeds the survey's `header` + `signature`, so two
775
+ * variants never share a signature.
776
+ *
777
+ * WHY anchored, not substring: an earlier substring matcher false-fired on a
778
+ * user message that merely QUOTED the survey text in prose — it spammed the
779
+ * live topic with bogus surveys + duplicates. The header MUST be the WHOLE
780
+ * line, so a header embedded mid-sentence can never match. Do NOT loosen this
781
+ * to an open-ended `How .* Claude` prose pattern.
782
+ *
783
+ * WHY an alternation: Claude Code ≥2.1.19x reworded the survey ("How well is
784
+ * Claude following the instructions…") — the old exact-header match never
785
+ * fired, the undetected survey sat on the pane and SWALLOWED the Enter of the
786
+ * next forwarded prompt: the text stranded unsubmitted in the TUI input box
787
+ * and the topic looked hung for hours (live 2026-07-02).
788
+ */
789
+ const CLAUDE_SURVEY_HEADER_REGEX = /^\s*[●⏺]?\s*(How is Claude doing this session\?|How well is Claude following the instructions you gave earlier in this conversation\?)(\s*\(optional\))?\s*$/;
790
+ /**
791
+ * The inline option row: `N: Label N: Label …` — every token is
792
+ * `digit: word`, ≥2 of them, and the WHOLE line is option tokens (anchored),
793
+ * so a prose line that merely contains `1: foo` can't pass.
794
+ */
795
+ const CLAUDE_SURVEY_OPTION_ROW_REGEX = /^\s*(\d+:\s*[A-Za-z]+\s*)+$/;
796
+ /** Pulls each `digit: label` pair out of a validated option row. */
797
+ const CLAUDE_SURVEY_OPTION_TOKEN_REGEX = /(\d+):\s*([A-Za-z]+)/g;
798
+ const CLAUDE_SURVEY_MIN_OPTIONS = 2;
799
+ /**
800
+ * @description AIRTIGHT detector for the Claude CLI bare-digit survey. Returns
801
+ * a survey ONLY when BOTH hold:
802
+ *
803
+ * 1. Some line, after stripping an optional leading `● ` bullet, matches
804
+ * {@link CLAUDE_SURVEY_HEADER_REGEX} ANCHORED start-to-end (the WHOLE
805
+ * line) — so a header quoted mid-sentence is rejected.
806
+ * 2. The IMMEDIATELY-FOLLOWING non-empty line is an inline option row
807
+ * ({@link CLAUDE_SURVEY_OPTION_ROW_REGEX}) parsing to ≥2 options.
808
+ *
809
+ * Kept fully SEPARATE from {@link extractClaudeQuestion} (the per-line
810
+ * AskUserQuestion selector): this is a lighter fixed-shape prompt. The
811
+ * signature is header + the option digits/labels only (no volatile chrome), so
812
+ * the same survey across polls de-dups to a single emit.
813
+ */
814
+ function extractClaudeSurvey(text) {
815
+ const lines = text.split('\n');
816
+ for (let i = 0; i < lines.length; i++) {
817
+ const headerMatch = CLAUDE_SURVEY_HEADER_REGEX.exec(lines[i]);
818
+ if (!headerMatch)
819
+ continue;
820
+ // The option row is the IMMEDIATELY-following non-empty line — skip only
821
+ // blank lines, never prose. If the next content line isn't an option row,
822
+ // this is not a survey (the header was probably quoted in prose).
823
+ let j = i + 1;
824
+ while (j < lines.length && lines[j].trim() === '')
825
+ j++;
826
+ if (j >= lines.length)
827
+ return null;
828
+ if (!CLAUDE_SURVEY_OPTION_ROW_REGEX.test(lines[j]))
829
+ return null;
830
+ const options = [];
831
+ for (const match of lines[j].matchAll(CLAUDE_SURVEY_OPTION_TOKEN_REGEX)) {
832
+ options.push({ digit: match[1], label: match[2] });
833
+ }
834
+ if (options.length < CLAUDE_SURVEY_MIN_OPTIONS)
835
+ return null;
836
+ const header = headerMatch[1];
837
+ const signature = `${header}::${options
838
+ .map(option => `${option.digit}:${option.label}`)
839
+ .join('|')}`;
840
+ return { header, options, signature };
841
+ }
842
+ return null;
843
+ }
844
+ /** Whether `text` is confidently showing a Claude CLI bare-digit survey. */
845
+ function checkIsClaudeSurvey(text) {
846
+ return extractClaudeSurvey(text) !== null;
847
+ }
848
+ /**
849
+ * @description Whether the poll loop should AUTO-DISMISS the survey on screen
850
+ * this poll (S4). True only when a survey is present AND its signature differs
851
+ * from the one we last dismissed — the survey repaints every poll, so the
852
+ * signature dedup makes us send the dismiss keystroke exactly ONCE per
853
+ * appearance, and re-dismiss a genuinely new survey later (the caller clears the
854
+ * stored signature when the survey leaves the pane). Pure + exported so the
855
+ * once-per-appearance rule is unit-testable without a live session.
856
+ */
857
+ function checkShouldDismissSurvey(surveySignature, lastDismissedSignature) {
858
+ return surveySignature !== null && surveySignature !== lastDismissedSignature;
859
+ }
860
+ /**
861
+ * @description Drop the survey's header + option-row lines from a relay chunk
862
+ * (S4) so the auto-dismissed survey's chrome never leaks into the topic — those
863
+ * lines match no tool/spinner/chrome rule and would otherwise classify as prose
864
+ * and post as a stray message. The CALLER gates this on a survey actually being
865
+ * detected on the pane this poll, so a legitimate "1: Yes 2: No" list in normal
866
+ * agent output is never stripped. Pure + exported for unit testing.
867
+ */
868
+ function stripSurveyChromeLines(text) {
869
+ return text
870
+ .split('\n')
871
+ .filter((line) => !CLAUDE_SURVEY_HEADER_REGEX.test(line) && !CLAUDE_SURVEY_OPTION_ROW_REGEX.test(line))
872
+ .join('\n');
873
+ }
874
+ /**
875
+ * @description Pure plan of the tmux send-keys steps for one `sendInput` call.
876
+ *
877
+ * Mirrors the three historical Enter branches (bare slash command → deferred
878
+ * Enter; short control reply → instant Enter; plain prompt → paste-race
879
+ * deferred Enter + verification). When `appendEnter === false` ONLY the literal
880
+ * keystrokes are sent — every Enter branch is skipped (for a keystroke that
881
+ * auto-submits on its own). `appendEnter` defaults to `true`, so every existing
882
+ * caller's plan is byte-for-byte unchanged.
883
+ */
884
+ function getClaudeSendKeysPlan(input, appendEnter) {
885
+ const steps = ['literal'];
886
+ if (!appendEnter)
887
+ return steps;
888
+ if (checkIsBareSlashCommand(input)) {
889
+ steps.push('slashEnter');
890
+ }
891
+ else if (input.length < CLAUDE_PASTE_RACE_MIN_LENGTH) {
892
+ steps.push('instantEnter');
893
+ }
894
+ else {
895
+ steps.push('verifiedEnter');
896
+ }
897
+ return steps;
898
+ }
899
+ /**
900
+ * @description Decide how to route a user TEXT reply for a Claude thread,
901
+ * given which interactive prompts are currently pending. Pure so the
902
+ * precedence is unit-testable.
903
+ *
904
+ * Precedence (a real AskUserQuestion always wins):
905
+ * 1. A selector (AskUserQuestion) pending + a bare control reply
906
+ * (digit / y / n) → `'selector'` (drive the menu in place).
907
+ * 2. Else the `/login` "Paste code here" box is on screen → `'loginPaste'`:
908
+ * the OAuth code is a long free-form string, so route it VERBATIM into
909
+ * the box (see {@link checkIsClaudeLoginPaste}). Without this it falls to
910
+ * `'prompt'`, whose Escape cancels the login flow and whose preamble
911
+ * corrupts the code (live bug: "login can't be done via the bot").
912
+ * 3. Else → `'prompt'` (a fresh turn; free-form prose breaks out of any
913
+ * pending prompt, mirroring the existing selector break-out behavior).
914
+ *
915
+ * Claude's native session SURVEY is NOT routed here: the adapter auto-dismisses
916
+ * it (Escape) and never surfaces it to the user, so a bare digit typed during a
917
+ * survey window falls through to `'prompt'` (or `'selector'` when a real
918
+ * question is also up) — never silently dropped.
919
+ */
920
+ function getClaudeReplyRoute(input) {
921
+ const isControlReply = checkIsSelectorControlReply(input.text);
922
+ if (input.isQuestionPending && isControlReply)
923
+ return 'selector';
924
+ if (input.isLoginPastePending)
925
+ return 'loginPaste';
926
+ return 'prompt';
927
+ }
928
+ /**
929
+ * @description Whether the captured pane shows Claude's `/login` OAuth
930
+ * "paste the code" step. After the user picks a login method Claude prints a
931
+ * sign-in URL and the input row `Paste code here if prompted >` (a plain `>`
932
+ * box, NOT the `❯` prompt or a numbered selector), then waits for the pasted
933
+ * `code#state` value. The marker string is stable (verified in the claude.exe
934
+ * string table, 2026-06-13). Used by {@link ClaudeCliAdapter.isLoginPastePending}
935
+ * so the bot routes the pasted code verbatim instead of as a fresh prompt.
936
+ * The marker regex is shared with the json-stream out-of-band `/login` flow
937
+ * ({@link claudeLoginPastePromptRe}) so the marker string has a single definition.
938
+ */
939
+ /**
940
+ * Lines from the pane BOTTOM that count as the live input region. The real
941
+ * `/login` "paste code" prompt is the active bottom input row; matching the
942
+ * marker anywhere in the full pane false-fired whenever this repo's OWN source
943
+ * or docs (which quote the marker) rendered in a Claude TUI working on
944
+ * Charness — the bot then ate the user's next message as a one-time login
945
+ * code (live 2026-06-25). Anchoring to the tail excludes scrollback
946
+ * mentions while still catching a genuine login row.
947
+ */
948
+ const loginPasteTailLineCount = 10;
949
+ function checkIsClaudeLoginPaste(paneText) {
950
+ const tail = paneText.split('\n').slice(-loginPasteTailLineCount).join('\n');
951
+ return claudeAuthLogin_1.claudeLoginPastePromptRe.test(tail);
952
+ }
953
+ /**
954
+ * A line-numbered diff row (` 88 + code`, ` 90 context`, bare ` 89`),
955
+ * recognisable WITHOUT its tool header — the fallback for a diff that arrived
956
+ * split from its header across two polls. Diff rows never occur in agent prose.
957
+ */
958
+ const DIFF_ROW_RE = /^\s+\d+(?:\s|$)/;
959
+ /** Min consecutive diff rows for the header-less fallback to fence them. */
960
+ const DIFF_FALLBACK_MIN_ROWS = 2;
961
+ /**
962
+ * @description Recognise the four line shapes of a markdown table that Claude's
963
+ * TUI renders as a SHARP-corner box-drawing frame.
964
+ *
965
+ * WHY a dedicated detector: Claude's own UI **chrome** panels use ROUNDED
966
+ * corners (`╭ ╮ ╰ ╯`) — exactly what the chrome-drop filters in
967
+ * {@link stripTuiElementsWithContext} target by dropping any `│`/`─` line wider
968
+ * than 50 chars. A rendered markdown **table** uses SHARP corners
969
+ * (`┌ ┬ ┐ ├ ┼ ┤ └ ┴ ┘`), so a WIDE table body row (a long `│ … │`) was caught
970
+ * by that same width filter and dropped — the "header survives, body rows lost"
971
+ * bug (#10). Detecting sharp corners targets tables specifically and lets the
972
+ * table-collecting branch run BEFORE the chrome filters, so table rows can
973
+ * never be dropped while chrome (rounded) handling stays untouched.
974
+ *
975
+ * An optional leading `●/⏺ ` assistant bullet + indent is tolerated on the TOP
976
+ * border (Claude prints the bullet on the table's first line).
977
+ */
978
+ const SHARP_TABLE_TOP_RE = /^\s*[●⏺]?\s*┌[─┬]+┐\s*$/;
979
+ const SHARP_TABLE_BOTTOM_RE = /^\s*└[─┴]+┘\s*$/;
980
+ const SHARP_TABLE_SEPARATOR_RE = /^\s*├[─┼]+┤\s*$/;
981
+ const SHARP_TABLE_CONTENT_RE = /^\s*│.*│\s*$/;
982
+ /** Strip a leading `●/⏺ ` assistant bullet so the collected box indent is uniform. */
983
+ const ASSISTANT_BULLET_PREFIX_RE = /^(\s*)[●⏺]\s/;
984
+ /** True iff `line` is the TOP border of a sharp-corner (markdown) table. */
985
+ function checkIsSharpTableTop(line) {
986
+ return SHARP_TABLE_TOP_RE.test(line);
987
+ }
988
+ /** True iff `line` is the BOTTOM border of a sharp-corner (markdown) table. */
989
+ function checkIsSharpTableBottom(line) {
990
+ return SHARP_TABLE_BOTTOM_RE.test(line);
991
+ }
992
+ /**
993
+ * True iff `line` is any line of a sharp-corner (markdown) table — top/bottom
994
+ * border, a `├─┼─┤` separator, or a `│ … │` content row.
995
+ */
996
+ function checkIsSharpTableLine(line) {
997
+ return (checkIsSharpTableTop(line) ||
998
+ checkIsSharpTableBottom(line) ||
999
+ SHARP_TABLE_SEPARATOR_RE.test(line) ||
1000
+ SHARP_TABLE_CONTENT_RE.test(line));
1001
+ }
1002
+ function getLeadingSpaceCount(line) {
1003
+ return line.match(/^(\s*)/)[1].length;
1004
+ }
1005
+ /** Strip the largest leading-whitespace prefix common to all non-blank lines. */
1006
+ function getDedented(lines) {
1007
+ const indents = lines.filter(line => line.trim()).map(getLeadingSpaceCount);
1008
+ if (indents.length === 0)
1009
+ return lines;
1010
+ const min = Math.min(...indents);
1011
+ return lines.map(line => line.slice(min));
1012
+ }
1013
+ /**
1014
+ * @description Wrap body lines in a ```` ``` ```` fence for `renderAgentHtml`
1015
+ * to turn into `<pre><code>`. Any run of 3+ backticks inside the body (e.g. a
1016
+ * diff of a markdown file with its own fences) is broken up with zero-width
1017
+ * spaces so it can't prematurely close our fence — the body is for reading,
1018
+ * not byte-exact copy.
1019
+ */
1020
+ function getFenced(bodyLines) {
1021
+ const safe = bodyLines.map(line => line.replace(/`{3,}/g, run => run.split('').join('​')));
1022
+ return ['```', ...safe, '```'];
1023
+ }
1024
+ /**
1025
+ * @description Hard cap on streaming-table hold polls. The normal exit is a
1026
+ * done-signal — real prose now follows the table, or the turn went idle (see
1027
+ * {@link getTableStabilizationDecision}); if a pane is stuck mid-paint and the
1028
+ * turn never resolves, force-emit the latest version after this many CONSECUTIVE
1029
+ * unchanged-block polls so the table can never be swallowed forever. At
1030
+ * {@link basePollIntervalMs} (300ms) this is ≈9s of an unchanging held block
1031
+ * before the safety fires — far longer than a real table takes to settle.
1032
+ */
1033
+ const maxTableHoldPolls = 30;
1034
+ /**
1035
+ * @description Locate the LAST contiguous sharp-corner (markdown) table block in
1036
+ * a pre-split pane snapshot: the block text plus the index of its LAST line, or
1037
+ * `null` when none is present. Mirrors the collection loop in
1038
+ * {@link stripTuiElementsWithContext} (a TOP border, then contiguous table lines
1039
+ * until the first non-table line, a BOTTOM border ending it inclusively). The
1040
+ * LAST block is taken because a streaming table sits at the bottom of the pane;
1041
+ * an already-finished earlier table was emitted on a prior poll. The end index
1042
+ * lets {@link checkHasContentAfterLastSharpTable} look at what follows the table
1043
+ * without re-scanning. Internal helper for the two callers below.
1044
+ */
1045
+ function getLastSharpTableRange(lines) {
1046
+ let range = null;
1047
+ for (let i = 0; i < lines.length; i++) {
1048
+ if (!checkIsSharpTableTop(lines[i]))
1049
+ continue;
1050
+ const tableLines = [lines[i]];
1051
+ let endIndex = i;
1052
+ for (let j = i + 1; j < lines.length; j++) {
1053
+ if (!checkIsSharpTableLine(lines[j]))
1054
+ break;
1055
+ tableLines.push(lines[j]);
1056
+ endIndex = j;
1057
+ if (checkIsSharpTableBottom(lines[j]))
1058
+ break;
1059
+ }
1060
+ range = { block: tableLines.join('\n'), endIndex };
1061
+ // Keep scanning: a later top border means a more recent table further down.
1062
+ i = endIndex;
1063
+ }
1064
+ return range;
1065
+ }
1066
+ /**
1067
+ * @description Find the LAST contiguous sharp-corner (markdown) table block in a
1068
+ * full pane snapshot, or `null` when none is present. Returns the raw block text
1069
+ * so the stabilizer can compare it across polls. Pure + exported for unit tests.
1070
+ */
1071
+ function getLastSharpTableBlock(content) {
1072
+ return getLastSharpTableRange(content.split('\n'))?.block ?? null;
1073
+ }
1074
+ /**
1075
+ * @description Whether any REAL prose line follows the last sharp-corner table in
1076
+ * the pane — the "the table is done, content moved past it" signal the stabilizer
1077
+ * uses to emit the held table BEFORE that trailing prose (preserving order).
1078
+ *
1079
+ * Deliberately CONSERVATIVE (errs toward `false`): a line after the table counts
1080
+ * as content only when it is none of the recognised non-content shapes —
1081
+ * blank/whitespace, another sharp-table line, TUI chrome
1082
+ * ({@link checkIsClaudeChromeLine} — covers the `❯ …` input box and its `───`
1083
+ * rules, box-drawing borders, the `⏵⏵ bypass permissions…` footer, nav hints),
1084
+ * any progress/spinner/sub-agent frame ({@link checkIsProgressChunk} per line),
1085
+ * a live spinner tick ({@link SPINNER_TICK_RE}), a `✻ … for Ns` post-thinking
1086
+ * trailer ({@link POST_THINKING_TRAILER_RE}), or a `Thinking for Ns…` header
1087
+ * ({@link THINKING_HEADER_RE}). Reuses the SAME predicates the relay already
1088
+ * trusts as chrome — it never invents a looser matcher — so it cannot
1089
+ * false-positive on the input box / spinner / trailers (the failure mode that
1090
+ * would emit a table prematurely). The turn-idle signal and the hold safety cap
1091
+ * are the backstops when this stays `false`. Pure + exported for unit tests.
1092
+ */
1093
+ function checkHasContentAfterLastSharpTable(content) {
1094
+ const lines = content.split('\n');
1095
+ const range = getLastSharpTableRange(lines);
1096
+ if (range === null)
1097
+ return false;
1098
+ for (let i = range.endIndex + 1; i < lines.length; i++) {
1099
+ const line = lines[i];
1100
+ if (line.trim() === '')
1101
+ continue;
1102
+ if (checkIsSharpTableLine(line))
1103
+ continue;
1104
+ // The `❯` input box ends the transcript region: everything BELOW it (the
1105
+ // bypass-permissions footer, right-aligned token/usage counters, and corner
1106
+ // shortcut hints like `/rc` in Claude v2.1.201) is TUI footer chrome, never
1107
+ // transcript content. A real prose line after the table appears ABOVE the
1108
+ // input box, so once the scan reaches it, stop — nothing below is content.
1109
+ // WHY this is the real fix (live 2026-07-04): the `/rc` corner
1110
+ // hint is not matched by any chrome predicate, so the scan ran PAST the input
1111
+ // box and counted it as "content after the table" → `hasContentAfterTable`
1112
+ // stayed true through the whole render → RULE 2 emitted every byte-stable
1113
+ // growth stage → the wide table flooded as ~7 messages.
1114
+ if (/^\s*❯/.test(line))
1115
+ return false;
1116
+ if ((0, claudeScrapeShapes_1.checkIsClaudeChromeLine)(line))
1117
+ continue;
1118
+ if ((0, progressLine_1.checkIsProgressChunk)(line))
1119
+ continue;
1120
+ if (claudeScrapeShapes_1.SPINNER_TICK_RE.test(line))
1121
+ continue;
1122
+ if (claudeScrapeShapes_1.POST_THINKING_TRAILER_RE.test(line.trim()))
1123
+ continue;
1124
+ if (claudeScrapeShapes_1.THINKING_HEADER_RE.test(line))
1125
+ continue;
1126
+ // Survived every chrome predicate → a genuine prose line follows the table.
1127
+ return true;
1128
+ }
1129
+ return false;
1130
+ }
1131
+ /**
1132
+ * @description Drop every sharp-corner table line from a poll's delta text so a
1133
+ * mid-layout table frame never ships while the stabilizer is holding the table.
1134
+ * Only table-shaped lines are removed — surrounding prose streams as usual.
1135
+ * Pure + exported for unit tests.
1136
+ */
1137
+ function maskSharpTableLines(deltaText) {
1138
+ return deltaText
1139
+ .split('\n')
1140
+ .filter(line => !checkIsSharpTableLine(line))
1141
+ .join('\n')
1142
+ .trim();
1143
+ }
1144
+ /**
1145
+ * @description Decide what the poll loop does with a re-flowing sharp-corner
1146
+ * table. A held table is emitted ONLY when the turn is genuinely DONE — never on
1147
+ * a mere "byte-stable for one poll", because Claude streams a table in stages and
1148
+ * briefly PAUSES at an intermediate row count (e.g. 4 rows), which is byte-stable
1149
+ * across a poll yet not the final table. The old "stable one poll → emit" rule
1150
+ * shipped that 4-row intermediate, then the table grew to 6 rows and shipped
1151
+ * again → the topic got two tables (live incident 2026-06-15). Pure + exported
1152
+ * for unit tests. Rules, in order:
1153
+ *
1154
+ * 1. no table in the pane: flush whatever was held, once (table scrolled away /
1155
+ * message moved on); else no-op.
1156
+ * 2. real PROSE now follows the table (`hasContentAfterTable`) AND the table's
1157
+ * width has settled (`sameBlock` — byte-stable since the previous poll) → the
1158
+ * table is done; emit it BEFORE that trailing prose (the poll loop withholds
1159
+ * the prose this tick), so ordering is table-then-prose. A table still
1160
+ * re-flowing wider while prose is already on the pane HOLDS instead — only the
1161
+ * settled width ships (live flood 2026-07-04).
1162
+ * 3. the turn is IDLE (`isTurnIdle`) → emit promptly (covers a table-only
1163
+ * answer, which never gets trailing prose).
1164
+ * 4. otherwise HOLD — update the held block and advance `heldPolls` only while
1165
+ * the block is unchanged (a changed/new block resets the counter); past
1166
+ * {@link maxTableHoldPolls} force-emit so a never-settling pane can't swallow
1167
+ * the table forever. A still-busy table is held even when byte-identical
1168
+ * across polls — that is exactly the mid-stream pause.
1169
+ */
1170
+ function getTableStabilizationDecision(input) {
1171
+ const { currentTable, streamingTable, hasContentAfterTable, isTurnIdle } = input;
1172
+ if (currentTable === null) {
1173
+ // RULE 1: the table left the pane — flush whatever was held, once.
1174
+ if (streamingTable) {
1175
+ return { kind: 'emit', block: streamingTable.block, nextStreamingTable: null };
1176
+ }
1177
+ return { kind: 'none', block: null, nextStreamingTable: null };
1178
+ }
1179
+ // Whether the table block is byte-stable since the previous poll (its WIDTH has
1180
+ // settled, not still re-flowing). Load-bearing for RULE 2 and the RULE 4 guard.
1181
+ const sameBlock = streamingTable !== null && currentTable === streamingTable.block;
1182
+ // RULE 2: real content follows the table → it is done; emit it BEFORE that
1183
+ // prose (the caller masks the prose out of this tick to keep ordering). BUT
1184
+ // only once the WIDTH has settled (`sameBlock`): a table still re-flowing wider
1185
+ // WHILE trailing prose is already on the pane must NOT ship an intermediate
1186
+ // width (live flood 2026-07-04 — each width shipped as its own
1187
+ // message). A changed (wider) block falls through to HOLD and re-checks next
1188
+ // poll; idle (RULE 3) and the safety cap still emit a final width.
1189
+ if (hasContentAfterTable && sameBlock) {
1190
+ return { kind: 'emit', block: currentTable, nextStreamingTable: null };
1191
+ }
1192
+ // RULE 3: the turn finished → emit promptly (a table-only answer never gets
1193
+ // trailing prose, so this is its done-signal).
1194
+ if (isTurnIdle) {
1195
+ return { kind: 'emit', block: currentTable, nextStreamingTable: null };
1196
+ }
1197
+ // RULE 4: still mid-turn (incl. a byte-stable mid-stream pause) → HOLD. Advance
1198
+ // the runaway guard only while the block is unchanged; a changed block restarts
1199
+ // the count (it is actively re-flowing, give it the full budget) — so a
1200
+ // continuously-widening table never hits the cap and can never force-emit a
1201
+ // partial width; only a FROZEN busy pane does.
1202
+ const heldPolls = (sameBlock ? streamingTable.heldPolls : 0) + 1;
1203
+ if (heldPolls > maxTableHoldPolls) {
1204
+ // SAFETY: a pane stuck mid-paint must never swallow the table — ship the
1205
+ // latest version we have and clear, rather than holding indefinitely.
1206
+ return { kind: 'emit', block: currentTable, nextStreamingTable: null };
1207
+ }
1208
+ return { kind: 'hold', block: null, nextStreamingTable: { block: currentTable, heldPolls } };
1209
+ }
1210
+ /**
1211
+ * @description A status / summary line the TUI paints inside a tool-result
1212
+ * body that is chrome, not command output, and so must NEVER be fenced.
1213
+ * Narrowly anchored to the three confirmed literal shapes — extending this to
1214
+ * an arbitrary `…`-ending line would swallow real one-line stdout.
1215
+ */
1216
+ function checkIsClaudeBodyStatusLine(line) {
1217
+ return (claudeScrapeShapes_1.TRANSIENT_TICK_RE.test(line) ||
1218
+ claudeScrapeShapes_1.COLLAPSE_MARKER_RE.test(line) ||
1219
+ claudeScrapeShapes_1.COMPLETION_SUMMARY_RE.test(line));
1220
+ }
1221
+ /**
1222
+ * @description Split a tool-result body into the lines that should be fenced
1223
+ * (genuine stdout / diff / file content) and the status/summary lines that
1224
+ * must stay PLAIN. A {@link TRANSIENT_TICK_RE} tick (`Running…`/`Waiting…`)
1225
+ * followed by ANY real output line in the SAME body is stale — superseded by
1226
+ * the output the bot captured one frame later (the live case) — so it is
1227
+ * DROPPED, not even kept plain. Other status/summary lines are kept and
1228
+ * returned in `plain` (emitted after the fence). Genuine content goes to
1229
+ * `fenced` untouched.
1230
+ */
1231
+ function splitStatusLinesFromBody(bodyLines) {
1232
+ const hasRealOutput = bodyLines.some(line => !checkIsClaudeBodyStatusLine(line));
1233
+ const fenced = [];
1234
+ const plain = [];
1235
+ for (const line of bodyLines) {
1236
+ if (!checkIsClaudeBodyStatusLine(line)) {
1237
+ fenced.push(line);
1238
+ continue;
1239
+ }
1240
+ // Stale transient tick superseded by real output → drop it entirely.
1241
+ if (claudeScrapeShapes_1.TRANSIENT_TICK_RE.test(line) && hasRealOutput)
1242
+ continue;
1243
+ plain.push(line);
1244
+ }
1245
+ return { fenced, plain };
1246
+ }
1247
+ /**
1248
+ * @description Fence a tool-result body while keeping its status/summary lines
1249
+ * (`Running…`, `… +N lines`, `Done (… tokens …)`) OUT of the fence as plain
1250
+ * text — see {@link splitStatusLinesFromBody}. Genuine content is dedented and
1251
+ * fenced; an empty-after-filtering body emits NO ```` ``` ```` fence. The
1252
+ * caller passes the already-dedented-and-prefixed body for the `output` kind
1253
+ * (the `⎿` line rides with it) so dedent is applied here over the full set.
1254
+ */
1255
+ function getFencedBodyWithStatus(bodyLines) {
1256
+ const { fenced, plain } = splitStatusLinesFromBody(bodyLines);
1257
+ const result = [];
1258
+ if (fenced.length > 0)
1259
+ result.push(...getFenced(getDedented(fenced)));
1260
+ result.push(...plain.map(line => line.trim()));
1261
+ return result;
1262
+ }
1263
+ /**
1264
+ * @description Wrap each code-producing tool's `⎿` result body in a code fence
1265
+ * (B2). Operates on the already echo-suppressed / chrome-filtered line array.
1266
+ * See {@link OUTPUT_TOOL_HEADER_RE} for the header→body classification.
1267
+ *
1268
+ * Stateful across the chunk AND across polls: a tool header sets the active
1269
+ * kind; a `⎿` result or its orphan indented continuation is fenced for that
1270
+ * kind; a solid non-tool line (prose, a `Thinking…` header) clears the kind so
1271
+ * thinking prose is never fenced. The kind is threaded across polls by the
1272
+ * caller — Claude's TUI redraws the whole pane each poll and the line-set diff
1273
+ * ({@link getNewPaneContent}) emits only NEW lines, so a slow command's output
1274
+ * (`yarn test`, a build) arrives in a later poll WITHOUT its `● Bash(…)` header
1275
+ * (suppressed as a duplicate). `incomingKind` carries the header poll's kind
1276
+ * forward; the returned `outgoingKind` is fed back next poll. This is tracked
1277
+ * via the clean emitted deltas, NOT by scanning the racy live pane (the spinner
1278
+ * repaints every tick, so a pane scan flickers between frames).
1279
+ *
1280
+ * An agent-authored ```` ``` ```` block is passed through untouched (and
1281
+ * suppresses the diff-row fallback inside it) so a real code block is never
1282
+ * double-wrapped.
1283
+ */
1284
+ function fenceToolResultBodies(lines, incomingKind = null) {
1285
+ const out = [];
1286
+ let inAgentFence = false;
1287
+ let currentKind = incomingKind;
1288
+ let i = 0;
1289
+ while (i < lines.length) {
1290
+ const line = lines[i];
1291
+ if (claudeScrapeShapes_1.CODE_FENCE_LINE_RE.test(line)) {
1292
+ inAgentFence = !inAgentFence;
1293
+ out.push(line);
1294
+ i++;
1295
+ continue;
1296
+ }
1297
+ if (inAgentFence) {
1298
+ out.push(line);
1299
+ i++;
1300
+ continue;
1301
+ }
1302
+ if (claudeScrapeShapes_1.OUTPUT_TOOL_HEADER_RE.test(line)) {
1303
+ currentKind = 'output';
1304
+ out.push(line);
1305
+ i++;
1306
+ continue;
1307
+ }
1308
+ if (claudeScrapeShapes_1.FILE_TOOL_HEADER_RE.test(line)) {
1309
+ currentKind = 'file';
1310
+ out.push(line);
1311
+ i++;
1312
+ continue;
1313
+ }
1314
+ // Sub-agent task / compaction-bar progress lines: the bot collapses these,
1315
+ // so they must pass through un-fenced and they end any open tool body.
1316
+ if (claudeScrapeShapes_1.PROGRESS_PASSTHROUGH_RE.test(line)) {
1317
+ currentKind = null;
1318
+ out.push(line);
1319
+ i++;
1320
+ continue;
1321
+ }
1322
+ const markerMatch = line.match(claudeScrapeShapes_1.TOOL_RESULT_MARKER_RE);
1323
+ if (markerMatch) {
1324
+ if (currentKind === null) {
1325
+ // Unknown owner (e.g. a thinking `⎿` body) — leave it as prose.
1326
+ out.push(line);
1327
+ i++;
1328
+ continue;
1329
+ }
1330
+ const markerIndent = markerMatch[1].length;
1331
+ const body = [];
1332
+ let j = i + 1;
1333
+ while (j < lines.length &&
1334
+ lines[j].trim() !== '' &&
1335
+ getLeadingSpaceCount(lines[j]) > markerIndent) {
1336
+ body.push(lines[j]);
1337
+ j++;
1338
+ }
1339
+ if (currentKind === 'output') {
1340
+ // `⎿`-content is stdout → drop the marker glyph (keep alignment), fence
1341
+ // it with the body, but keep any status/summary line plain (never fenced).
1342
+ out.push(...getFencedBodyWithStatus([line.replace('⎿', ' '), ...body]));
1343
+ }
1344
+ else {
1345
+ out.push(line); // summary line stays prose
1346
+ if (body.length > 0)
1347
+ out.push(...getFencedBodyWithStatus(body));
1348
+ }
1349
+ i = j;
1350
+ continue;
1351
+ }
1352
+ // Orphan indented continuation of a known tool body (header was in a prior
1353
+ // poll, or the `⎿` summary already streamed): fence the indented run.
1354
+ if (currentKind !== null && /^\s/.test(line) && line.trim() !== '') {
1355
+ const block = [];
1356
+ let j = i;
1357
+ while (j < lines.length &&
1358
+ lines[j].trim() !== '' &&
1359
+ /^\s/.test(lines[j]) &&
1360
+ !claudeScrapeShapes_1.TOOL_RESULT_MARKER_RE.test(lines[j]) &&
1361
+ !claudeScrapeShapes_1.CODE_FENCE_LINE_RE.test(lines[j])) {
1362
+ block.push(lines[j]);
1363
+ j++;
1364
+ }
1365
+ out.push(...getFencedBodyWithStatus(block));
1366
+ i = j;
1367
+ continue;
1368
+ }
1369
+ // Header-less diff-row run (cross-poll split with no known kind): fence on shape.
1370
+ if (currentKind === null && DIFF_ROW_RE.test(line)) {
1371
+ const block = [];
1372
+ let j = i;
1373
+ while (j < lines.length && DIFF_ROW_RE.test(lines[j])) {
1374
+ block.push(lines[j]);
1375
+ j++;
1376
+ }
1377
+ if (block.length >= DIFF_FALLBACK_MIN_ROWS) {
1378
+ out.push(...getFenced(getDedented(block)));
1379
+ }
1380
+ else {
1381
+ out.push(...block);
1382
+ }
1383
+ i = j;
1384
+ continue;
1385
+ }
1386
+ // Any solid prose line ends the tool context (blanks leave it intact).
1387
+ if (line.trim() !== '')
1388
+ currentKind = null;
1389
+ out.push(line);
1390
+ i++;
1391
+ }
1392
+ return { out, outgoingKind: currentKind };
1393
+ }
1394
+ /** The language tag of a fence-OPEN line (`` ```ts `` → `ts`, bare `` ``` `` → ''). */
1395
+ function getFenceLanguage(fenceLine) {
1396
+ return fenceLine.replace(/^\s*```/, '').trim();
1397
+ }
1398
+ /**
1399
+ * @description Merge two same-language fenced blocks separated by NOTHING but
1400
+ * blank line(s) into one continuous fence (#14). One logical tool output that
1401
+ * {@link fenceToolResultBodies} split into multiple `getFenced` runs renders as
1402
+ * two adjacent `<pre>` ({@link FENCE_REGEX} makes one per fence) — visually two
1403
+ * boxes for one output. A NON-blank line between the fences (a `● Bash(…)`
1404
+ * header, prose) BLOCKS the merge, so DIFFERENT tool calls stay separate; only
1405
+ * blank-separated same-language fences are joined, keeping one blank inside.
1406
+ * Languages must match (or both be empty) so a plain output block and a `ts`
1407
+ * block are never fused.
1408
+ */
1409
+ function mergeAdjacentFences(lines) {
1410
+ const out = [...lines];
1411
+ let i = 0;
1412
+ while (i < out.length) {
1413
+ if (!claudeScrapeShapes_1.CODE_FENCE_LINE_RE.test(out[i])) {
1414
+ i++;
1415
+ continue;
1416
+ }
1417
+ // `out[i]` opens a fence; find its closing delimiter.
1418
+ const closeIndex = findFenceClose(out, i);
1419
+ if (closeIndex === -1)
1420
+ break; // unbalanced — leave the tail untouched
1421
+ const nextOpenIndex = getNextOpenAfterBlanks(out, closeIndex + 1);
1422
+ if (nextOpenIndex !== -1 &&
1423
+ getFenceLanguage(out[nextOpenIndex]) === getFenceLanguage(out[i])) {
1424
+ // Drop the close + the re-open, keep a single blank line between bodies.
1425
+ out.splice(closeIndex, nextOpenIndex - closeIndex + 1, '');
1426
+ continue; // re-test from the SAME open fence (a third block may follow)
1427
+ }
1428
+ i = closeIndex + 1;
1429
+ }
1430
+ return out;
1431
+ }
1432
+ /** Index of the fence-CLOSE delimiter that matches the OPEN at `openIndex`. */
1433
+ function findFenceClose(lines, openIndex) {
1434
+ for (let j = openIndex + 1; j < lines.length; j++) {
1435
+ if (claudeScrapeShapes_1.CODE_FENCE_LINE_RE.test(lines[j]))
1436
+ return j;
1437
+ }
1438
+ return -1;
1439
+ }
1440
+ /**
1441
+ * Index of the next fence-OPEN delimiter reachable from `fromIndex` across ONLY
1442
+ * blank lines, or -1 if a non-blank line (a header / prose) intervenes first.
1443
+ */
1444
+ function getNextOpenAfterBlanks(lines, fromIndex) {
1445
+ for (let j = fromIndex; j < lines.length; j++) {
1446
+ if (lines[j].trim() === '')
1447
+ continue;
1448
+ return claudeScrapeShapes_1.CODE_FENCE_LINE_RE.test(lines[j]) ? j : -1;
1449
+ }
1450
+ return -1;
1451
+ }
1452
+ /**
1453
+ * @description Strip Claude TUI chrome from a pane diff and fence tool-result
1454
+ * bodies, returning the cleaned text AND the tool-result kind still open at the
1455
+ * end of the chunk. The caller (`pollOutput`) threads `toolKind` back in as the
1456
+ * next poll's `incomingKind` so a slow command's orphan output is fenced (B2).
1457
+ */
1458
+ function stripTuiElementsWithContext(text, incomingKind = null) {
1459
+ const lines = text.split('\n');
1460
+ const filtered = [];
1461
+ // A submitted multi-line prompt renders as a `❯ <first line>` user-turn
1462
+ // block followed by space-indented continuation lines. We only dropped the
1463
+ // `❯` line, so the continuation (incl. literal ``` fences) leaked out as a
1464
+ // phantom "agent message" duplicating the user's own prompt. Skip the whole
1465
+ // echo block: continuation lines stay suppressed until a non-indented line
1466
+ // (the spinner, a blank, or the agent's own `●` output) ends it.
1467
+ let inUserTurnEcho = false;
1468
+ for (let i = 0; i < lines.length; i++) {
1469
+ let line = lines[i];
1470
+ // B3: the `(ctrl+o …)` affordance is terminal-only — Telegram can't send
1471
+ // ctrl+o — so drop the parenthetical while keeping the rest of the line
1472
+ // (`… +2 lines`, `Thought for 4s`, `Read 50 lines`). Trailing-anchored, so
1473
+ // it never disturbs the leading-anchored echo / chrome detection below.
1474
+ line = line.replace(/\s*\(ctrl\+o[^)]*\)/gi, '');
1475
+ if (inUserTurnEcho) {
1476
+ if (/^\s+\S/.test(line))
1477
+ continue;
1478
+ inUserTurnEcho = false;
1479
+ }
1480
+ if (/^❯\s+\S/.test(line)) {
1481
+ inUserTurnEcho = true;
1482
+ continue;
1483
+ }
1484
+ // Markdown table (sharp-corner box). Collect the whole frame and emit it
1485
+ // FENCED before the chrome-drop filters below — those filters delete any
1486
+ // `│`/`─` line wider than 50 chars, which used to wipe a wide table's body
1487
+ // rows (bug #10). Chrome uses ROUNDED corners and is left to those filters;
1488
+ // only the sharp-corner shape is intercepted here. A table split across
1489
+ // scrape chunks (no bottom border yet) still keeps every row it has so far.
1490
+ if (checkIsSharpTableTop(line)) {
1491
+ const tableBlock = [line];
1492
+ let endIndex = i;
1493
+ // Collect contiguous table lines; a bottom border ends the block
1494
+ // (inclusive), as does the first non-table line (a table still painting
1495
+ // across scrape chunks keeps every row it has so far).
1496
+ for (let j = i + 1; j < lines.length; j++) {
1497
+ if (!checkIsSharpTableLine(lines[j]))
1498
+ break;
1499
+ tableBlock.push(lines[j]);
1500
+ endIndex = j;
1501
+ if (checkIsSharpTableBottom(lines[j]))
1502
+ break;
1503
+ }
1504
+ const unbulleted = tableBlock.map(tableLine => tableLine.replace(ASSISTANT_BULLET_PREFIX_RE, '$1 '));
1505
+ filtered.push(...getFenced(getDedented(unbulleted)));
1506
+ i = endIndex;
1507
+ continue;
1508
+ }
1509
+ if (/^[─━]+$/.test(line.trim()))
1510
+ continue;
1511
+ if (/⏵⏵\s*(bypass permissions|accept edits)\s*(on|off)/i.test(line))
1512
+ continue;
1513
+ if (/^❯/.test(line))
1514
+ continue;
1515
+ // Ephemeral UI hint Claude prints under a turn ("⎿ Tip: Use Plan Mode…").
1516
+ // Require the `⎿` marker so plain prose starting with "Tip:" is NOT eaten.
1517
+ if (/^\s*⎿\s*Tip:\s/i.test(line))
1518
+ continue;
1519
+ // Transient frame the TUI repaints right after an Escape interrupt
1520
+ // (" ⎿ Interrupted · What should Claude do instead?"). The pane-diff
1521
+ // pipeline relayed it as a scary "agent is waiting" message even though
1522
+ // the next prompt is already being typed. Anchored to the exact TUI shape
1523
+ // (line-start, optional ⎿/whitespace, the literal phrase to line-end) so
1524
+ // prose merely mentioning the word "Interrupted" is NOT stripped.
1525
+ if (/^\s*(?:⎿\s*)?Interrupted · What should Claude do instead\?\s*$/.test(line))
1526
+ continue;
1527
+ if (/\(shift\+tab to cycle\)/i.test(line))
1528
+ continue;
1529
+ // Bottom-right corner shortcut hint (v2.1.201 `/rc`): right-aligned, it
1530
+ // scrapes as a lone whitespace-padded token line. Same shape the chrome
1531
+ // classifier drops (`CORNER_HINT_RE` in claudeScrapeShapes — the deliberate
1532
+ // strip/classifier parallel, keep in sync).
1533
+ if (claudeScrapeShapes_1.CORNER_HINT_RE.test(line))
1534
+ continue;
1535
+ if (/^[\s·✽✢✶✻⏵❯─━↵]+$/.test(line))
1536
+ continue;
1537
+ // S3 (N1.a) / S4 (N1.b): per-line strip of mid-chunk spinner ticks
1538
+ // and post-thinking trailers. These shapes used to slip through
1539
+ // `checkIsStatusOutput` (adapter side) and `checkIsProgressChunk`
1540
+ // (bot side) when they appeared mixed with real output in a single
1541
+ // poll diff (the live debug session).
1542
+ if (claudeScrapeShapes_1.SPINNER_TICK_RE.test(line))
1543
+ continue;
1544
+ if (claudeScrapeShapes_1.POST_THINKING_TRAILER_RE.test(line.trim()))
1545
+ continue;
1546
+ // S2 (render-flood 2026-07-04): a leading star-burst activity title with NO
1547
+ // stats anchor (`✻ Herding cats`) is transient spinner chrome the
1548
+ // stats-requiring SPINNER_TICK_RE above misses — drop it so it never ships
1549
+ // as answer content. Both relay paths (fast + verbosity-routed) funnel
1550
+ // through here, so this one drop covers them.
1551
+ if ((0, claudeScrapeShapes_1.checkIsBareSpinnerActivityLine)(line))
1552
+ continue;
1553
+ const trimmedLine = line.trim();
1554
+ const isToolCall = /^[●○⏺]?\s*(Bash|Read|Write|Edit|Glob|Grep|Task|TodoWrite|WebFetch|WebSearch)\s*\(/i.test(trimmedLine);
1555
+ if (!isToolCall && /ctrl\+c.*to interrupt/i.test(line))
1556
+ continue;
1557
+ if (/claude code has switched|native installer|Run.*install.*or see/i.test(line))
1558
+ continue;
1559
+ if (/^install`?\s*(or see)?/i.test(trimmedLine))
1560
+ continue;
1561
+ if (/docs\.anthropic\.com/i.test(line))
1562
+ continue;
1563
+ if (/more options\.?\s*$/i.test(trimmedLine) && trimmedLine.length < 20)
1564
+ continue;
1565
+ if (/^[╭─╮│╰╯\s]+$/.test(trimmedLine))
1566
+ continue;
1567
+ if (/^[▐▛▜▌▝▘█▀▄░▒▓\s]+$/.test(trimmedLine))
1568
+ continue;
1569
+ // Interactive question UI: tab bar navigation (← ☐ ... →)
1570
+ if (/^←.*→\s*$/.test(trimmedLine))
1571
+ continue;
1572
+ // Interactive question UI: selection/navigation hints
1573
+ if (/Enter to select/i.test(line))
1574
+ continue;
1575
+ // Interactive question UI: side-by-side AskUserQuestion chrome — the
1576
+ // "Notes: press n…" affordance and the unnumbered "Chat about this"
1577
+ // meta-row leaked as naked text when a selector frame fell through to
1578
+ // this path (live 2026-06-10). Both regexes are anchored
1579
+ // whole-line, so prose containing the phrases survives.
1580
+ if (QUESTION_NOTES_HINT_REGEX.test(line))
1581
+ continue;
1582
+ if (QUESTION_CHAT_ABOUT_REGEX.test(line))
1583
+ continue;
1584
+ if (/Recent activity|What's new|\/resume for more/i.test(line))
1585
+ continue;
1586
+ if (/Welcome\s*back/i.test(line))
1587
+ continue;
1588
+ if (/[╭─╮│╰╯]/.test(line) && trimmedLine.length > 50)
1589
+ continue;
1590
+ if (/^\s*│.*\d+[smh]\s+ago\s+/i.test(line))
1591
+ continue;
1592
+ if (/^\s*│.*[─]+\s*│\s*$/.test(line))
1593
+ continue;
1594
+ if (isToolCall) {
1595
+ line = normalizeToolCallLine(line);
1596
+ }
1597
+ filtered.push(line);
1598
+ }
1599
+ const { out, outgoingKind } = fenceToolResultBodies(filtered, incomingKind);
1600
+ const merged = mergeAdjacentFences(out);
1601
+ let result = merged.join('\n');
1602
+ result = result.replace(/\n{3,}/g, '\n\n');
1603
+ return { text: result.trim(), toolKind: outgoingKind };
1604
+ }
1605
+ /**
1606
+ * @description Strip Claude TUI chrome and fence tool-result bodies (B2),
1607
+ * returning just the cleaned text. Thin wrapper over
1608
+ * {@link stripTuiElementsWithContext} for call sites (and tests) that don't
1609
+ * thread the cross-poll tool kind.
1610
+ */
1611
+ function stripTuiElements(text, incomingKind = null) {
1612
+ return stripTuiElementsWithContext(text, incomingKind).text;
1613
+ }
1614
+ /**
1615
+ * @description Turn an absolute workDir into Claude's transcript-folder slug.
1616
+ *
1617
+ * Claude stores per-project transcripts under
1618
+ * `~/.claude/projects/<slug>/<sessionUuid>.jsonl`, where the slug is the
1619
+ * absolute path with every non-alphanumeric character replaced by `-`
1620
+ * (e.g. `/home/user/src/telegramCode` → `-home-user-src-telegramCode`).
1621
+ *
1622
+ * The slug encoding is undocumented and could drift across Claude versions,
1623
+ * so it is used only to *locate* the candidate folder — the authoritative
1624
+ * correctness gate is the `recordedCwd === workDir` filter in
1625
+ * {@link listClaudeSessionsForWorkDir}.
1626
+ */
1627
+ function getClaudeProjectSlug(workDir) {
1628
+ return workDir.replace(/[^a-zA-Z0-9]/g, '-');
1629
+ }
1630
+ /**
1631
+ * @description Root of Claude's on-disk transcript store
1632
+ * (`~/.claude/projects`). Single source for every path built from it —
1633
+ * session listing, resume-context reads, and the sub-agent transcript scan.
1634
+ */
1635
+ function getClaudeProjectsRoot() {
1636
+ return path.join(os.homedir(), '.claude', 'projects');
1637
+ }
1638
+ /**
1639
+ * @description Absolute path of ONE session's transcript
1640
+ * (`<projectsRoot>/<slug>/<sessionId>.jsonl`).
1641
+ *
1642
+ * Single source for the layout: both Claude backends resolve it for the
1643
+ * watermark advance, the runtime-info read, the resume-context turns, and the
1644
+ * reattach recap — a hand-built copy in any of them would silently read nothing
1645
+ * if the layout ever drifts.
1646
+ */
1647
+ function getClaudeTranscriptPath(workDir, sessionId) {
1648
+ return path.join(getClaudeProjectsRoot(), getClaudeProjectSlug(workDir), `${sessionId}.jsonl`);
1649
+ }
1650
+ /**
1651
+ * @description Read the byte range `[startOffset..endOffset)` of a file as
1652
+ * UTF-8. The sub-agent transcript tail uses it to fetch only the bytes
1653
+ * appended since the previous poll instead of re-reading whole files. A short
1654
+ * read (file truncated between stat and read) returns just the bytes
1655
+ * available — the JSONL line parser skips any torn tail.
1656
+ */
1657
+ async function readFileSlice(filePath, startOffset, endOffset) {
1658
+ const fileHandle = await fs.promises.open(filePath, 'r');
1659
+ try {
1660
+ const length = endOffset - startOffset;
1661
+ const buffer = Buffer.alloc(length);
1662
+ const { bytesRead } = await fileHandle.read(buffer, 0, length, startOffset);
1663
+ return buffer.subarray(0, bytesRead).toString('utf-8');
1664
+ }
1665
+ finally {
1666
+ await fileHandle.close();
1667
+ }
1668
+ }
1669
+ /**
1670
+ * @description Is this fs error a plain "path does not exist" (`ENOENT`)?
1671
+ * Cast-free narrowing via the same record guard the JSONL parsers use.
1672
+ */
1673
+ function checkIsMissingPathError(error) {
1674
+ return checkIsRecord(error) && error.code === 'ENOENT';
1675
+ }
1676
+ /**
1677
+ * @description Narrow an untyped `JSON.parse` result to a plain object so its
1678
+ * fields can be read with `typeof` guards instead of casts (parse-boundary
1679
+ * pattern for untrusted on-disk data; mirrors `isRecord` in `state.ts`).
1680
+ */
1681
+ function checkIsRecord(value) {
1682
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
1683
+ }
1684
+ /**
1685
+ * @description Stream-parse a single Claude `.jsonl` transcript, pulling out
1686
+ * just the fields we need to render and gate a resumable session. Reads the
1687
+ * whole file synchronously (the caller already bounded the count to
1688
+ * {@link sessionsParseLimit}) and tolerates malformed lines.
1689
+ *
1690
+ * A `.jsonl` can carry many `summary` entries (one per branch ever summarised
1691
+ * in this project file). The one describing THIS conversation is the summary
1692
+ * whose `leafUuid` points at a message `uuid` that lives in this file, so we
1693
+ * collect message uuids + all (leafUuid → summary) pairs, then match.
1694
+ */
1695
+ function parseClaudeTranscript(filePath) {
1696
+ const result = {
1697
+ recordedCwd: null,
1698
+ summaryTitle: null,
1699
+ firstMeaningfulPrompt: null,
1700
+ firstTimestamp: null,
1701
+ };
1702
+ let raw;
1703
+ try {
1704
+ raw = fs.readFileSync(filePath, 'utf-8');
1705
+ }
1706
+ catch (e) {
1707
+ console.warn(`[Claude] failed to read transcript ${filePath}:`, e instanceof Error ? e.message : e);
1708
+ return result;
1709
+ }
1710
+ const messageUuids = new Set();
1711
+ const summaryByLeaf = new Map();
1712
+ for (const line of raw.split('\n')) {
1713
+ const trimmed = line.trim();
1714
+ if (!trimmed)
1715
+ continue;
1716
+ let entry;
1717
+ try {
1718
+ entry = JSON.parse(trimmed);
1719
+ }
1720
+ catch {
1721
+ continue; // tolerate a half-written trailing line
1722
+ }
1723
+ if (!checkIsRecord(entry))
1724
+ continue;
1725
+ if (typeof entry.uuid === 'string')
1726
+ messageUuids.add(entry.uuid);
1727
+ if (entry.type === 'summary' && typeof entry.summary === 'string' && typeof entry.leafUuid === 'string') {
1728
+ summaryByLeaf.set(entry.leafUuid, entry.summary);
1729
+ }
1730
+ if (result.recordedCwd === null && typeof entry.cwd === 'string') {
1731
+ result.recordedCwd = entry.cwd;
1732
+ }
1733
+ if (result.firstTimestamp === null && typeof entry.timestamp === 'string') {
1734
+ result.firstTimestamp = entry.timestamp;
1735
+ }
1736
+ if (entry.type === 'user' && result.firstMeaningfulPrompt === null) {
1737
+ const userText = extractUserText(entry.message);
1738
+ if (userText) {
1739
+ const candidate = (0, claudeSessionTitle_1.getSessionTitleCandidate)(userText);
1740
+ if (candidate)
1741
+ result.firstMeaningfulPrompt = candidate;
1742
+ }
1743
+ }
1744
+ }
1745
+ for (const [leafUuid, summary] of summaryByLeaf) {
1746
+ if (messageUuids.has(leafUuid)) {
1747
+ result.summaryTitle = summary;
1748
+ break;
1749
+ }
1750
+ }
1751
+ return result;
1752
+ }
1753
+ /**
1754
+ * @description Best-effort extraction of plain text from a transcript's
1755
+ * `user` message. Claude writes the message either as a bare string or as
1756
+ * an array of content blocks (`{ type: 'text', text }`). Returns the first
1757
+ * non-empty text found, or `null`.
1758
+ */
1759
+ function extractUserText(message) {
1760
+ if (typeof message === 'string')
1761
+ return message.trim() || null;
1762
+ if (!checkIsRecord(message))
1763
+ return null;
1764
+ const content = message.content;
1765
+ if (typeof content === 'string')
1766
+ return content.trim() || null;
1767
+ if (Array.isArray(content)) {
1768
+ for (const block of content) {
1769
+ if (checkIsRecord(block) && block.type === 'text' && typeof block.text === 'string' && block.text.trim()) {
1770
+ return block.text.trim();
1771
+ }
1772
+ }
1773
+ }
1774
+ return null;
1775
+ }
1776
+ /**
1777
+ * @description Best-effort extraction of plain text from a transcript's
1778
+ * `assistant` message. Unlike a user message, an assistant `message.content`
1779
+ * is always the block array; we concatenate every `{ type: 'text' }` block
1780
+ * (one assistant turn can interleave several) and IGNORE `tool_use` /
1781
+ * `thinking` / other block kinds so the resume context shows prose, not a
1782
+ * tool-call dump. Returns the joined text, or `null` if no renderable text.
1783
+ */
1784
+ function extractAssistantText(message) {
1785
+ if (!checkIsRecord(message))
1786
+ return null;
1787
+ const content = message.content;
1788
+ // Some assistant entries store a bare string (rare); accept it too.
1789
+ if (typeof content === 'string')
1790
+ return content.trim() || null;
1791
+ if (!Array.isArray(content))
1792
+ return null;
1793
+ const textBlocks = [];
1794
+ for (const block of content) {
1795
+ if (checkIsRecord(block) && block.type === 'text' && typeof block.text === 'string' && block.text.trim()) {
1796
+ textBlocks.push(block.text.trim());
1797
+ }
1798
+ }
1799
+ if (textBlocks.length === 0)
1800
+ return null;
1801
+ return textBlocks.join('\n\n');
1802
+ }
1803
+ /**
1804
+ * @description Parse ONE Claude `.jsonl` transcript line into a renderable
1805
+ * conversational turn, or `null` for a meta / tool-only / empty / malformed
1806
+ * line. Shared by {@link readRecentClaudeTurns} (the resume-context / recap turn
1807
+ * body) and {@link readClaudeReattachTranscript} (the missed-message count) so
1808
+ * both apply the EXACT same renderability rule: a `type:'user'` /
1809
+ * `type:'assistant'` entry whose message yields non-empty text (via
1810
+ * {@link extractUserText} / {@link extractAssistantText}); `summary` and other
1811
+ * meta lines, and assistant entries that are tool_use-only, yield `null`. Never
1812
+ * throws — a torn / half-written line (offset landing mid-record) fails
1813
+ * `JSON.parse` and yields `null`.
1814
+ */
1815
+ function parseClaudeTurnLine(line) {
1816
+ const trimmed = line.trim();
1817
+ if (!trimmed)
1818
+ return null;
1819
+ let entry;
1820
+ try {
1821
+ entry = JSON.parse(trimmed);
1822
+ }
1823
+ catch {
1824
+ return null; // tolerate a half-written / torn line
1825
+ }
1826
+ if (!checkIsRecord(entry))
1827
+ return null;
1828
+ if (entry.type === 'user') {
1829
+ const text = extractUserText(entry.message);
1830
+ return text ? { role: 'user', text } : null;
1831
+ }
1832
+ if (entry.type === 'assistant') {
1833
+ const text = extractAssistantText(entry.message);
1834
+ return text ? { role: 'assistant', text } : null;
1835
+ }
1836
+ return null;
1837
+ }
1838
+ /**
1839
+ * @description Read the last `limit` conversational turns (user/assistant
1840
+ * messages with renderable text, oldest→newest) from a Claude `.jsonl`
1841
+ * transcript, for the resume context block.
1842
+ *
1843
+ * Exported and pure (filesystem-only) so it is unit-testable against a temp
1844
+ * `.jsonl`. Streams the whole file via {@link parseClaudeTurnLine}, keeping only
1845
+ * the last `limit` in a rolling window so a huge transcript stays cheap.
1846
+ * Tolerates a half-written trailing line; a missing/unreadable file → `[]`.
1847
+ */
1848
+ function readRecentClaudeTurns(filePath, limit) {
1849
+ let raw;
1850
+ try {
1851
+ raw = fs.readFileSync(filePath, 'utf-8');
1852
+ }
1853
+ catch {
1854
+ return []; // transcript not on disk (unknown UUID / pruned) → no context
1855
+ }
1856
+ const turns = [];
1857
+ for (const line of raw.split('\n')) {
1858
+ const turn = parseClaudeTurnLine(line);
1859
+ if (turn)
1860
+ turns.push(turn);
1861
+ // Bound the window: drop the oldest once we exceed `limit` so a long
1862
+ // transcript never accumulates more than `limit` turns in memory.
1863
+ if (turns.length > limit)
1864
+ turns.shift();
1865
+ }
1866
+ return turns;
1867
+ }
1868
+ /**
1869
+ * @description Combined transcript read for the silent-reattach recap: ONE
1870
+ * `fs.readFileSync` yields BOTH the missed-message count and the recap turn
1871
+ * body, so the transcript is not read twice.
1872
+ *
1873
+ * - `missedCount` — renderable `type:'assistant'` turns appended AFTER byte
1874
+ * `offset` (the seen-watermark = transcript size at the last turn end), counted
1875
+ * over the `[offset, EOF)` byte tail. A torn leading line (offset mid-record)
1876
+ * fails to parse and is skipped. `offset >= size` → `0` (nothing appended /
1877
+ * transcript truncated or rewritten smaller).
1878
+ * - `turns` — the last `limit` renderable turns of the WHOLE session (NOT just
1879
+ * the missed region), identical to {@link readRecentClaudeTurns}.
1880
+ * - `headOffset` — the transcript's CURRENT byte size (`buffer.length`), i.e.
1881
+ * the watermark value meaning "everything currently on disk is accounted for".
1882
+ * The reattach recap advances the persisted watermark to it for idempotency.
1883
+ * Absent on an unreadable file (head unknown → the caller omits the watermark).
1884
+ *
1885
+ * Exported and pure (filesystem-only) so it is unit-testable. `fs.readFileSync`
1886
+ * reads the whole file in one call (it handles short reads internally), so a
1887
+ * large `[offset, EOF)` tail never loses its last lines to a partial `read` —
1888
+ * the silent undercount a manual `Buffer.alloc` + `fs.readSync` would hit on
1889
+ * exactly the long-downtime case this feature targets. A missing/unreadable
1890
+ * file → `{ missedCount: 0, turns: [] }` (no `headOffset`).
1891
+ */
1892
+ function readClaudeReattachTranscript(filePath, offset, limit) {
1893
+ let buffer;
1894
+ try {
1895
+ buffer = fs.readFileSync(filePath);
1896
+ }
1897
+ catch {
1898
+ return { missedCount: 0, turns: [] }; // transcript not on disk → nothing to recap
1899
+ }
1900
+ // Recap body: last `limit` renderable turns of the WHOLE session.
1901
+ const turns = [];
1902
+ for (const line of buffer.toString('utf-8').split('\n')) {
1903
+ const turn = parseClaudeTurnLine(line);
1904
+ if (turn)
1905
+ turns.push(turn);
1906
+ if (turns.length > limit)
1907
+ turns.shift();
1908
+ }
1909
+ // Missed count: renderable assistant turns in the [offset, EOF) byte tail.
1910
+ const start = Math.min(Math.max(0, offset), buffer.length);
1911
+ let missedCount = 0;
1912
+ for (const line of buffer.subarray(start).toString('utf-8').split('\n')) {
1913
+ const turn = parseClaudeTurnLine(line);
1914
+ if (turn?.role === 'assistant')
1915
+ missedCount += 1;
1916
+ }
1917
+ return { missedCount, turns, headOffset: buffer.length };
1918
+ }
1919
+ /**
1920
+ * @description List resumable Claude sessions for a workDir by reading the
1921
+ * real `~/.claude/projects/<slug>/*.jsonl` transcripts.
1922
+ *
1923
+ * Exported and pure (filesystem-only, no tmux/adapter boot) so it can be
1924
+ * unit-tested against a temp `projectsRoot`. Steps:
1925
+ *
1926
+ * 1. Resolve `<projectsRoot>/<slug>`; if missing → `[]`.
1927
+ * 2. Take the {@link sessionsParseLimit} most-recently-modified `.jsonl`.
1928
+ * 3. Parse each; KEEP only files whose recorded cwd === `workDir`
1929
+ * (the load-bearing correctness gate — slug collisions / version drift).
1930
+ * 4. id = filename stem, MUST pass {@link checkIsValidUuid} (else skip).
1931
+ * 5. title = summaryTitle ?? firstMeaningfulPrompt ?? id (trimmed, capped).
1932
+ * `summaryTitle` is legacy (current Claude no longer writes it); the
1933
+ * primary source is the first REAL prompt topic
1934
+ * ({@link getSessionTitleCandidate}) — preamble/marker stripped, tooling
1935
+ * noise skipped — NOT the last (often noise) prompt. A session with no
1936
+ * meaningful prompt (all turns were command echoes / noise) falls back to
1937
+ * the bare id. updatedAt = file mtime; createdAt = first timestamp ?? mtime.
1938
+ *
1939
+ * Result is newest-first by mtime.
1940
+ */
1941
+ function listClaudeSessionsForWorkDir(projectsRoot, workDir) {
1942
+ const slugDir = path.join(projectsRoot, getClaudeProjectSlug(workDir));
1943
+ let entries;
1944
+ try {
1945
+ entries = fs.readdirSync(slugDir);
1946
+ }
1947
+ catch {
1948
+ return []; // folder doesn't exist yet → no Claude session ran here
1949
+ }
1950
+ const candidates = entries
1951
+ .filter(name => name.endsWith('.jsonl'))
1952
+ .map(name => {
1953
+ const fullPath = path.join(slugDir, name);
1954
+ let mtimeMs;
1955
+ try {
1956
+ mtimeMs = fs.statSync(fullPath).mtimeMs;
1957
+ }
1958
+ catch {
1959
+ mtimeMs = 0;
1960
+ }
1961
+ return { name, fullPath, mtimeMs };
1962
+ })
1963
+ .sort((a, b) => b.mtimeMs - a.mtimeMs)
1964
+ .slice(0, sessionsParseLimit);
1965
+ const sessions = [];
1966
+ for (const candidate of candidates) {
1967
+ const id = candidate.name.slice(0, -'.jsonl'.length);
1968
+ if (!checkIsValidUuid(id))
1969
+ continue;
1970
+ const parsed = parseClaudeTranscript(candidate.fullPath);
1971
+ if (parsed.recordedCwd !== workDir)
1972
+ continue;
1973
+ const rawTitle = parsed.summaryTitle ?? parsed.firstMeaningfulPrompt ?? id;
1974
+ const title = rawTitle.trim().slice(0, sessionTitleMaxLength) || id;
1975
+ const updatedAt = new Date(candidate.mtimeMs);
1976
+ const createdAt = parsed.firstTimestamp ? new Date(parsed.firstTimestamp) : updatedAt;
1977
+ sessions.push({ id, title, createdAt, updatedAt });
1978
+ }
1979
+ return sessions;
1980
+ }
1981
+ // `shellSingleQuote` moved to `utils/tmuxExec` (shared with the terminal
1982
+ // backend); it was private here and is imported above.
1983
+ /**
1984
+ * @description A bare slash command (e.g. `/compact`, `/clear`, `/context`) —
1985
+ * a `/` followed only by letters/hyphens with NO argument. Typing one opens
1986
+ * Claude's command-autocomplete popup; an argument (a space) closes it. We
1987
+ * use this to decide whether the trailing `Enter` needs to wait for the popup
1988
+ * to settle (see {@link CLAUDE_SLASH_ENTER_DELAY_MS}).
1989
+ */
1990
+ const BARE_SLASH_COMMAND_RE = /^\/[a-zA-Z][a-zA-Z-]*$/;
1991
+ /**
1992
+ * @description Whether `input` is a bare slash command (matches
1993
+ * {@link BARE_SLASH_COMMAND_RE} after trimming). Exported so the
1994
+ * popup-settle decision is unit-testable without a live tmux session.
1995
+ */
1996
+ function checkIsBareSlashCommand(input) {
1997
+ return BARE_SLASH_COMMAND_RE.test(input.trim());
1998
+ }
1999
+ /**
2000
+ * @description Delay between typing a bare slash command and pressing Enter.
2001
+ *
2002
+ * Claude's TUI opens an autocomplete popup the instant `/` is typed. Sending
2003
+ * Enter the same tick the popup is still rendering accepts the highlighted
2004
+ * suggestion instead of RUNNING the command, so e.g. `/compact` silently
2005
+ * no-ops. Letting the popup settle first makes Enter execute the command.
2006
+ * Commands with arguments (`/model sonnet`) don't need this — the space
2007
+ * already dismissed the popup.
2008
+ */
2009
+ const CLAUDE_SLASH_ENTER_DELAY_MS = 250;
2010
+ /**
2011
+ * @description Delay between the literal-text burst and the Enter that submits
2012
+ * a plain (non-slash) prompt.
2013
+ *
2014
+ * Claude's TUI aggregates a fast burst of input as a paste; an Enter that lands
2015
+ * inside that aggregation window (observed: tens of ms after a 100+ char burst)
2016
+ * is absorbed as a paste newline instead of submitting, leaving the prompt
2017
+ * typed-but-unsubmitted in the input box (B5, live-reproduced ~1 in 4). Sending
2018
+ * the text and the Enter back-to-back on the serial queue is exactly that race.
2019
+ * Deferring the Enter past the window makes it register as a submit. Kept small
2020
+ * so the prompt still feels instant; the slash-command delay is larger because
2021
+ * it solves a different problem (popup settle), not this one.
2022
+ */
2023
+ const CLAUDE_TEXT_ENTER_DELAY_MS = 80;
2024
+ /**
2025
+ * @description Inputs shorter than this skip the deferred Enter + verification
2026
+ * entirely: a few characters (`y`, `n`, an option digit) cannot trigger the
2027
+ * TUI's paste aggregation, and these control replies were instant before B5 —
2028
+ * keep them instant.
2029
+ */
2030
+ const CLAUDE_PASTE_RACE_MIN_LENGTH = 8;
2031
+ /**
2032
+ * @description How long to wait after the submit-Enter before capturing the
2033
+ * pane to verify the prompt actually submitted (B5 post-Enter verification).
2034
+ * Must outlast the TUI repaint that follows a real submit (the input box clears
2035
+ * and Claude either goes busy or echoes the user turn). One capture only; if it
2036
+ * still looks unsubmitted we re-send Enter exactly once (no infinite retries).
2037
+ */
2038
+ const CLAUDE_ENTER_VERIFY_DELAY_MS = 600;
2039
+ /**
2040
+ * @description How many trailing pane lines form Claude's live footer/status
2041
+ * region. The `esc to interrupt` busy hint renders ONLY here — on the
2042
+ * `⏵⏵ bypass permissions…` footer line, or the spinner status line a row or two
2043
+ * above the input box. `checkIsClaudeBusy` reads `session.lastContent`, which is
2044
+ * the FULL `capture-pane -S -2000` scrollback; bounding the busy match to this
2045
+ * tail stops a transcript that merely TALKS about "esc to interrupt" from
2046
+ * pinning the session busy forever. Live 2026-06-29: the bot's own dev topic
2047
+ * (its conversation is full of the phrase) froze a "🔧 working" status frame
2048
+ * because the regex matched scrollback prose, not the live footer — the genuine
2049
+ * footer sat on the bottom line while the nearest prose hit was 17 lines above.
2050
+ */
2051
+ const CLAUDE_BUSY_FOOTER_TAIL_LINES = 8;
2052
+ /**
2053
+ * @description The busy hint as Claude's footer/spinner CHROME renders it:
2054
+ * bracketed by the `·` hint separator (`… · esc to interrupt · ← for agents`)
2055
+ * or the spinner-stats parens (`(12s · ↓ tokens · esc to interrupt)` /
2056
+ * `(esc to interrupt)`), never as bare prose. Requiring that bracketing keeps a
2057
+ * transcript line like `const RE = /esc to interrupt/i` or "the footer shows esc
2058
+ * to interrupt" — which can land in the inspected tail of an idle dev topic —
2059
+ * from reading as busy. Matched against CLEANED pane text (ANSI stripped) so the
2060
+ * marker is never split mid-word.
2061
+ */
2062
+ const CLAUDE_BUSY_FOOTER_RE = /[·(]\s*esc to interrupt|esc to interrupt\s*[·)]/i;
2063
+ /**
2064
+ * @description Whether Claude is mid-turn, judged from a captured pane. A
2065
+ * selector/permission prompt shows `Esc to cancel`, NOT `esc to interrupt`, so
2066
+ * it reads as idle here (correct — we only wait out a running turn). Only the
2067
+ * footer tail is inspected ({@link CLAUDE_BUSY_FOOTER_TAIL_LINES}) and the hint
2068
+ * must carry footer chrome ({@link CLAUDE_BUSY_FOOTER_RE}), so scrollback prose
2069
+ * mentioning the phrase can't false-positive. Exported so the busy detection is
2070
+ * unit-testable without a live tmux session.
2071
+ */
2072
+ function checkIsClaudeBusy(paneText) {
2073
+ const footer = paneText.replace(/\s+$/, '').split('\n').slice(-CLAUDE_BUSY_FOOTER_TAIL_LINES);
2074
+ return footer.some((line) => CLAUDE_BUSY_FOOTER_RE.test(line));
2075
+ }
2076
+ /**
2077
+ * @description Sync, in-memory "is this session mid-turn?" decision for the
2078
+ * scheduler's wait-for-idle loop ({@link AgentAdapter.checkIsBusy}). Reuses the
2079
+ * SAME pane predicate {@link interruptAndWaitIdle} polls
2080
+ * ({@link checkIsClaudeBusy} on the `esc to interrupt` footer), evaluated
2081
+ * against the session's last CLEANED pane content instead of a fresh `tmux`
2082
+ * call — so it stays synchronous and adds no tmux load. The cleaned text is
2083
+ * load-bearing: the raw `-e` capture carries ANSI/SGR runs that can split the
2084
+ * footer marker mid-word and make the regex miss (the predicate's other
2085
+ * callers read plain no-`-e` captures for the same reason). A dead/missing
2086
+ * session is never busy. Exported pure so the gate is unit-testable without a
2087
+ * live session.
2088
+ */
2089
+ function checkIsClaudeSessionBusy(args) {
2090
+ return args.isActive && checkIsClaudeBusy(args.lastContent);
2091
+ }
2092
+ /**
2093
+ * @description Markers that a frame carries real agent content (a tool header
2094
+ * `●`/`⏺`, a tool-result `⎿`, or a ``` ``` ``` code fence). Their presence vetoes
2095
+ * the input-echo classification in {@link checkIsInputEchoFrame}.
2096
+ */
2097
+ const CONTENT_MARKER_RE = /^(?:[●⏺]|⎿|```)/;
2098
+ /**
2099
+ * @description Whether a cleaned output frame is nothing but the TUI input box
2100
+ * echoing typed text (`❯ <draft>`). The poll can catch the input row between
2101
+ * the literal-text keystrokes and the deferred submit Enter (B5 window), and
2102
+ * that draft echo is pure noise in Telegram — the user already sees their own
2103
+ * message (B7).
2104
+ *
2105
+ * A frame is an echo when its FIRST non-empty line starts with `❯` AND no line
2106
+ * carries a content marker (`●`, `⎿`, or a ``` ``` ``` fence). The old
2107
+ * predicate required EVERY line to start with `❯`, but a long draft wraps in
2108
+ * the input box so only the first row carries `❯` and the continuation rows are
2109
+ * plain — those frames leaked into the topic (B7b). Anchoring on the first line
2110
+ * + a content-marker veto keeps a `❯` inside real tool output (e.g. a pane
2111
+ * capture printed by a Bash command) classified as content, not an echo.
2112
+ *
2113
+ * MAINTENANCE — Claude Code's TUI rendering drifts between versions, back and
2114
+ * forth (the prompt glyph, the echo indentation, the content markers — e.g. the
2115
+ * `●`→`⏺` bullet change noted in `utils/claudeScrapeShapes.ts`). This detector
2116
+ * must tolerate ALL known formatting variants, not assume the current one: when
2117
+ * an echo starts leaking into the topic, WIDEN the predicate to also cover the
2118
+ * new shape (keep the old variants), don't swap one shape for another.
2119
+ */
2120
+ function checkIsInputEchoFrame(frameText) {
2121
+ const contentLines = frameText
2122
+ .split('\n')
2123
+ .map((line) => line.trim())
2124
+ .filter(Boolean);
2125
+ if (contentLines.length === 0)
2126
+ return false;
2127
+ if (!contentLines[0].startsWith('❯'))
2128
+ return false;
2129
+ return !contentLines.some((line) => CONTENT_MARKER_RE.test(line));
2130
+ }
2131
+ /**
2132
+ * @description Min normalized chars a frame must share with the last forwarded
2133
+ * text before the content gate calls it an echo. Keeps a short reply that merely
2134
+ * coincides with a word of the prompt from being eaten — only a real echo of the
2135
+ * substance is suppressed. Below it, only the deterministic preamble signature
2136
+ * (which the agent never produces itself) can suppress.
2137
+ */
2138
+ const claudeEchoMinMatchChars = 16;
2139
+ /**
2140
+ * @description How long after a {@link ClaudeCliAdapter.sendInput} the content
2141
+ * echo gate stays armed. Claude echoes the typed text right at submit, so a short
2142
+ * window catches every echo variant while bounding false positives: a much later
2143
+ * real reply that happens to repeat prompt text is outside the window and kept.
2144
+ */
2145
+ const claudeInputEchoContentWindowMs = 30_000;
2146
+ /**
2147
+ * @description Collapse a frame / forwarded prompt to a whitespace-insensitive,
2148
+ * case-folded form for echo comparison. The TUI wraps a long input across rows
2149
+ * and pads differently between redraws, so runs of whitespace (incl. newlines)
2150
+ * flatten to a single space — the words survive, the layout noise does not.
2151
+ */
2152
+ function normalizeEchoText(text) {
2153
+ return text.replace(/\s+/g, ' ').trim().toLowerCase();
2154
+ }
2155
+ /**
2156
+ * @description CONTENT-based echo gate (S1), complementing the shape-only
2157
+ * {@link checkIsInputEchoFrame}. The bot types the user's prompt into the Claude
2158
+ * TUI and Claude echoes it back in the pane; when that echo carries NO `❯`
2159
+ * (a wrapped continuation row, the `[Telegram thread context]` preamble, or the
2160
+ * tail of a long voice transcript) the shape gate misses it and it leaks into
2161
+ * the topic as a ghost "bot message" (live 2026-06-28).
2162
+ *
2163
+ * A cleaned frame is the user's own echo when EITHER:
2164
+ * - it carries the deterministic thread-context preamble signature (the
2165
+ * `[Telegram thread context]` header — the agent never emits that itself), OR
2166
+ * - its normalized text is a contiguous slice of the normalized forwarded text
2167
+ * (a prefix, suffix, or interior run) of at least
2168
+ * {@link claudeEchoMinMatchChars} — i.e. the whole frame is nothing but a
2169
+ * chunk of what we just typed.
2170
+ *
2171
+ * The containment DIRECTION is load-bearing: the FRAME must be contained in the
2172
+ * forwarded text, never the reverse — so a genuine reply that QUOTES part of the
2173
+ * prompt and then adds an answer is NOT a substring of the prompt and is kept.
2174
+ * The caller additionally gates this on a short post-forward time window.
2175
+ *
2176
+ * WIDENED (live 2026-07-06): the poll can catch the just-submitted
2177
+ * echo's wrapped row GLUED to the new turn's first spinner tick in ONE frame
2178
+ * (`<echo row>\n\n* Twisting…`). The glued chrome is not part of the forwarded
2179
+ * text, so whole-frame containment failed and the row leaked as a permanent
2180
+ * message. When the whole frame misses, transient-shaped lines (per-line
2181
+ * {@link checkIsStatusOutput}) are dropped and the remaining content is
2182
+ * re-tested — only a frame whose real content is nothing but a slice of the
2183
+ * forwarded text is suppressed, so an answer glued to a spinner is still kept.
2184
+ *
2185
+ * Pure + exported so every observed echo variant is unit-testable. MAINTENANCE:
2186
+ * like {@link checkIsInputEchoFrame}, WIDEN (never swap) when a new echo shape
2187
+ * leaks — keep the old variants.
2188
+ */
2189
+ function checkIsForwardedEcho(frameText, forwardedText) {
2190
+ const frame = normalizeEchoText(frameText);
2191
+ if (!frame)
2192
+ return false;
2193
+ if (frame.includes(normalizeEchoText(threadContextPreamble_1.threadContextPreambleHeader)))
2194
+ return true;
2195
+ const forwarded = normalizeEchoText(forwardedText);
2196
+ if (!forwarded)
2197
+ return false;
2198
+ if (frame.length >= claudeEchoMinMatchChars && forwarded.includes(frame))
2199
+ return true;
2200
+ // Glued-chrome widen: drop transient/spinner-shaped lines, keep real content
2201
+ // lines, and re-run the SAME containment on the remainder. A frame with no
2202
+ // content lines left is pure status — the status flow owns it, not the gate.
2203
+ const contentLines = frameText
2204
+ .split('\n')
2205
+ .filter((line) => line.trim() && !checkIsStatusOutput(line));
2206
+ if (contentLines.length === 0)
2207
+ return false;
2208
+ const residual = normalizeEchoText(contentLines.join('\n'));
2209
+ if (residual === frame)
2210
+ return false; // nothing was dropped — already tested above
2211
+ return residual.length >= claudeEchoMinMatchChars && forwarded.includes(residual);
2212
+ }
2213
+ /**
2214
+ * @description Min chars of the typed prompt that must still sit in the live
2215
+ * input box for {@link checkLooksUnsubmitted} to call it unsubmitted. A short
2216
+ * prefix (vs the whole prompt) is enough — the input box shows only the first
2217
+ * wrapped line of a long prompt — while guarding against a one-glyph false
2218
+ * match. Below this length we compare the full trimmed text.
2219
+ */
2220
+ const CLAUDE_INPUT_MATCH_PREFIX_LEN = 12;
2221
+ /**
2222
+ * @description Whether a captured pane looks like the typed prompt is still
2223
+ * sitting UNSUBMITTED in Claude's live input box (the B5 paste-race symptom).
2224
+ *
2225
+ * Submission predicate (the load-bearing "did it submit?" check): a prompt has
2226
+ * submitted iff EITHER Claude went busy (`esc to interrupt` in the footer — the
2227
+ * turn started) OR the live input box at the bottom is empty (`❯ ` with no
2228
+ * text — Claude consumed the input and is idle again, e.g. a fast/no-op turn).
2229
+ * It looks UNSUBMITTED only when Claude is idle AND the last `❯`-led input row
2230
+ * still carries the typed text. We deliberately key on the LAST `❯` line: a
2231
+ * SUBMITTED prompt echoes as a `❯ <text>` user-turn block in scrollback, but
2232
+ * the live input box below it is the final `❯` line and is empty — so matching
2233
+ * the last one ignores the harmless scrollback echo.
2234
+ *
2235
+ * Tradeoff (locked in plan B5): if a single verification capture is ambiguous
2236
+ * we err toward reporting unsubmitted and let the caller retry Enter once — a
2237
+ * duplicate Enter on an already-submitted prompt is a harmless no-op in the
2238
+ * Claude TUI (submits an empty input = ignored), whereas a missed submit silently
2239
+ * loses the user's message.
2240
+ *
2241
+ * Exported so the predicate is unit-testable without a live tmux session.
2242
+ */
2243
+ function checkLooksUnsubmitted(paneText, typedText) {
2244
+ if (checkIsClaudeBusy(paneText))
2245
+ return false;
2246
+ const typed = typedText.trim();
2247
+ if (!typed)
2248
+ return false;
2249
+ // The input box renders the first visual line of the prompt; compare against
2250
+ // the prompt's first line so a multi-line paste doesn't fail to match.
2251
+ const firstLine = typed.split('\n', 1)[0].trim();
2252
+ if (!firstLine)
2253
+ return false;
2254
+ const lines = paneText.split('\n');
2255
+ let lastInputRow = null;
2256
+ for (const line of lines) {
2257
+ const match = line.match(/^\s*❯\s?(.*)$/);
2258
+ if (match)
2259
+ lastInputRow = match[1].trim();
2260
+ }
2261
+ // No input box at all (e.g. a selector/question is on screen) → not our case.
2262
+ if (lastInputRow === null)
2263
+ return false;
2264
+ if (lastInputRow === '')
2265
+ return false;
2266
+ const needle = firstLine.length <= CLAUDE_INPUT_MATCH_PREFIX_LEN
2267
+ ? firstLine
2268
+ : firstLine.slice(0, CLAUDE_INPUT_MATCH_PREFIX_LEN);
2269
+ return lastInputRow.startsWith(needle);
2270
+ }
2271
+ /**
2272
+ * @description Boot-time lifecycle gates Claude can paint before the real input
2273
+ * box is usable: the "Press Enter to continue" / login-success gate, and the
2274
+ * bypass-permissions warning. `handleAutoLifecycle` dismisses both; the S7
2275
+ * readiness predicate treats them as "not ready yet". Module-level so both the
2276
+ * auto-lifecycle handlers and {@link checkIsClaudePromptReady} share one source
2277
+ * of truth (no duplicated regexes).
2278
+ */
2279
+ const CLAUDE_AUTO_ENTER_PATTERNS = [
2280
+ /Press Enter to continue/i,
2281
+ /Login successful\. Press Enter/i,
2282
+ ];
2283
+ const CLAUDE_BYPASS_WARNING_RE = /WARNING.*Bypass|Bypass.*Permissions/i;
2284
+ const CLAUDE_BYPASS_ACCEPT_RE = /Yes,?\s*I\s*accept/i;
2285
+ /** The `API Error:`-at-line-start error row shape (detector case (a)). */
2286
+ const CLAUDE_API_ERROR_START_RE = /^\s*API Error:/;
2287
+ /** The `API Error:` substring, for a `⎿` result row that carries it (case (b)). */
2288
+ const CLAUDE_API_ERROR_SUBSTR_RE = /API Error:/;
2289
+ /**
2290
+ * @description A `⎿` result row whose content LEADS with an auth phrase — Claude's
2291
+ * canonical logged-out render (`⎿ Not logged in · Please run /login`,
2292
+ * `⎿ Please run /login · API Error: 401 …`). The phrase is anchored right after
2293
+ * the `⎿` marker (case (c)) — see the false-positive note on
2294
+ * {@link getClaudeAgentErrorLine}.
2295
+ */
2296
+ const CLAUDE_AUTH_ROW_RE = /^\s*⎿\s*(?:not logged in|please run \/login|invalid authentication credentials)\b/i;
2297
+ /**
2298
+ * @description A row whose CONTENT leads with a usage/session-limit phrase — the
2299
+ * render behind `You've hit your session limit · resets 10:50pm (UTC)`, which
2300
+ * carries no `API Error:` marker and so matched none of the shapes (a)–(c). The
2301
+ * optional leading glyph covers the `⎿` result marker and the `●`/`⏺` output
2302
+ * bullet; up to three punctuation-free words may precede the phrase because the
2303
+ * live wording leads with a subject ("You've hit …"). That bounded, punctuation-
2304
+ * free lead-in is the false-positive guard (case (d)): a QUOTED phrase inside a
2305
+ * tool result always carries a path / colon / bracket first (`⎿ log:224:[Claude]
2306
+ * … session limit`), so it can never reach the phrase. The phrase alternation
2307
+ * itself is shared with {@link classifyAgentApiError} via
2308
+ * {@link usageLimitPhraseSource} so the detector and the classifier cannot drift.
2309
+ */
2310
+ const CLAUDE_LIMIT_ROW_RE = new RegExp(String.raw `^\s*(?:[⎿●⏺]\s*)?(?:[A-Za-z][A-Za-z'’]*[ ]){0,3}(?:${apiErrorRetry_1.usageLimitPhraseSource})`, 'i');
2311
+ /**
2312
+ * @description SECOND required signal for case (d): the row must also READ like a
2313
+ * terminal limit error, not like the agent's own prose ABOUT limits. Case (d) is
2314
+ * the only shape with no glyph/`API Error:` anchor, so the lead-in alone let any
2315
+ * answer line that merely STARTS with a limit phrase fire a bogus episode
2316
+ * (`Session limits reset weekly for every plan.` / `Usage limits apply per 5-hour
2317
+ * window.` armed a 60-min wait and pushed a "continue" nudge into a healthy
2318
+ * topic — trivially reachable in a topic where the agent discusses limits, e.g.
2319
+ * this feature itself). Every real row carries one of these markers
2320
+ * (`… limit · resets 10:50pm (UTC)`, `… limit reached ∙ resets 3am`); the bare
2321
+ * verb prose uses ("limits reset weekly") is deliberately NOT in the list.
2322
+ */
2323
+ const CLAUDE_LIMIT_ROW_SHAPE_RE = /\b(?:reached|exceeded|resets|resetting|try again|too low)\b|[·∙]/i;
2324
+ /**
2325
+ * @description Find the ONE line in a scraped pane that is a terminal
2326
+ * provider / auth error, or `null` if none. `content` is `cleanOutput(raw)`,
2327
+ * which RETAINS the `⎿` result marker. Three accepted shapes:
2328
+ * a) `API Error: …` at line start — the classic provider row
2329
+ * (`API Error: … (not your usage limit) · Rate limited`).
2330
+ * b) a `⎿` result row that also contains `API Error:`
2331
+ * (`⎿ overloaded · API Error: 429` — the render the old start-anchored gate
2332
+ * MISSED, which is why a `⎿`-prefixed rate-limit never auto-retried).
2333
+ * c) a `⎿` result row whose content LEADS with an auth phrase
2334
+ * (`⎿ Not logged in · Please run /login` — the logged-out render).
2335
+ * d) a row whose content LEADS with a usage/session-limit phrase AND reads like
2336
+ * an error ({@link CLAUDE_LIMIT_ROW_SHAPE_RE}) — `You've hit your session
2337
+ * limit · resets 10:50pm (UTC)` carries no `API Error:` marker at all, so
2338
+ * (a)–(c) all missed it.
2339
+ * FALSE-POSITIVE GUARD: prose never starts a line with the `⎿` glyph nor with
2340
+ * `API Error:`. But TOOL results (Bash/Read/Grep output) ARE rendered by the TUI
2341
+ * under a `⎿` marker, so a result row that QUOTES an auth phrase deeper in the
2342
+ * line (the agent grepping the bot's own logs/source, or a `gh`/`npm` "not
2343
+ * logged in" line) would fire case (c) if we matched the phrase anywhere. So (c)
2344
+ * — and (d), for the same reason — anchor the phrase to the row START: a real
2345
+ * error row leads with it; a quote embeds it after other text (live 2026-07-03).
2346
+ * The returned line is fed VERBATIM to
2347
+ * {@link classifyAgentApiError}. Exported for unit testing without a live tmux
2348
+ * pane.
2349
+ */
2350
+ function getClaudeAgentErrorLine(content) {
2351
+ for (const line of content.split('\n')) {
2352
+ if (CLAUDE_API_ERROR_START_RE.test(line))
2353
+ return line;
2354
+ if (CLAUDE_AUTH_ROW_RE.test(line))
2355
+ return line;
2356
+ if (CLAUDE_LIMIT_ROW_RE.test(line) && CLAUDE_LIMIT_ROW_SHAPE_RE.test(line))
2357
+ return line;
2358
+ if (claudeScrapeShapes_1.TOOL_RESULT_MARKER_RE.test(line) && CLAUDE_API_ERROR_SUBSTR_RE.test(line))
2359
+ return line;
2360
+ }
2361
+ return null;
2362
+ }
2363
+ /**
2364
+ * @description Matches the TUI's live input box prompt row (`❯` led, optional
2365
+ * draft text after it). Its presence in a captured pane is our signal that the
2366
+ * Claude TUI has finished booting its banner and is ready to receive typed
2367
+ * input — before that, the pane shows only the boot banner with no `❯` row.
2368
+ */
2369
+ const CLAUDE_INPUT_BOX_RE = /^\s*❯/m;
2370
+ /**
2371
+ * @description Whether a captured pane shows the Claude TUI has finished
2372
+ * booting and is ready to accept a typed slash command (S7 readiness signal).
2373
+ *
2374
+ * Ready means: the live input box `❯` is on screen AND no boot-time lifecycle
2375
+ * gate is still up — neither the "Press Enter to continue" / login gate nor the
2376
+ * bypass-permissions warning, both of which `handleAutoLifecycle` dismisses
2377
+ * over the next polls. We do NOT require idle: a fresh spawn is idle, and
2378
+ * gating on "not busy" would only delay the re-apply past the box first
2379
+ * appearing. Keeping the predicate to "input box up, no lifecycle gate" matches
2380
+ * the single observable moment the box first accepts keys. Exported for unit
2381
+ * testing without a live tmux pane.
2382
+ */
2383
+ function checkIsClaudePromptReady(paneText) {
2384
+ if (!CLAUDE_INPUT_BOX_RE.test(paneText))
2385
+ return false;
2386
+ if (CLAUDE_AUTO_ENTER_PATTERNS.some((pattern) => pattern.test(paneText)))
2387
+ return false;
2388
+ const hasBypassWarning = CLAUDE_BYPASS_WARNING_RE.test(paneText);
2389
+ const hasBypassAccept = CLAUDE_BYPASS_ACCEPT_RE.test(paneText);
2390
+ if (hasBypassWarning && hasBypassAccept)
2391
+ return false;
2392
+ return true;
2393
+ }
2394
+ /**
2395
+ * @description Pure decision for the seen-watermark advance: persist the
2396
+ * transcript's current EOF as "relayed live up to here" whenever the input box
2397
+ * is ready AND the file grew past the last persisted offset — busy or idle.
2398
+ *
2399
+ * S7 dropped the old `!isBusy` gate. A live Claude adapter relays the pane on
2400
+ * EVERY poll, so EOF-on-growth already means "shown up to here" mid-turn, not
2401
+ * only at turn end; the old idle-only advance left the watermark at the previous
2402
+ * turn end all through a multi-message turn, so a mid-turn restart re-counted
2403
+ * every already-relayed message as a false "missed N" (live 2026-07-04). The
2404
+ * `isReady` gate stays (don't advance before the box is up / while a boot or
2405
+ * lifecycle gate is on screen — that isn't a settled relay), and the growth
2406
+ * guard (`eof > lastOffset`) stays so idle metadata rewrites cost no write and a
2407
+ * smaller rewrite never rewinds the monotonic anchor. `isBusy` is accepted so
2408
+ * the caller passes the poll's busy flag and the truth table documents that busy
2409
+ * no longer blocks; it is intentionally unread.
2410
+ *
2411
+ * Exported so the truth table is unit-testable without a live tmux;
2412
+ * {@link ClaudeCliAdapter.advanceClaudeWatermarkIfGrown} is the thin fs+writer
2413
+ * wrapper. Callers pass `lastOffset = session.lastWatermarkOffset ?? -1` so a
2414
+ * never-advanced session writes on its first ready poll.
2415
+ */
2416
+ function checkShouldAdvanceWatermark(args) {
2417
+ return args.isReady && args.eof > args.lastOffset;
2418
+ }
2419
+ /**
2420
+ * @description Markers of a live sub-agent (Task tool) on the pane. While a
2421
+ * sub-agent runs the TUI shows a task line led by `◯` (U+25EF LARGE CIRCLE —
2422
+ * NOT the spinner glyph `○` U+25CB used elsewhere in this file), e.g.
2423
+ * `◯ general-purpose <task> 7s`, and the footer gains a `↓ to manage` hint.
2424
+ * Either is enough.
2425
+ */
2426
+ const CLAUDE_SUBAGENT_TASK_RE = /^\s*◯\s+\S/m;
2427
+ const CLAUDE_SUBAGENT_MANAGE_RE = /↓ to manage/;
2428
+ /**
2429
+ * @description Phrase the TUI shows while compacting context (manual `/compact`
2430
+ * or auto), e.g. `✶ Compacting conversation… (57s · ↑ 3.1k tokens)`. A bare
2431
+ * substring match on the phrase is enough here: this only gates the
2432
+ * interrupt decision (don't Escape during compaction), so it deliberately
2433
+ * stays looser than the anchored {@link COMPACT_LINE_RE} in `progressLine.ts`
2434
+ * (which must be line-exact to safely collapse a redraw burst).
2435
+ */
2436
+ const CLAUDE_COMPACTING_RE = /Compacting conversation/i;
2437
+ /**
2438
+ * @description Whether Claude is doing work that must NOT be interrupted: a
2439
+ * running sub-agent (Escape would kill the child) or an in-flight compaction
2440
+ * (Escape would abort it and lose the summary). In these states the caller
2441
+ * forwards the prompt without Escape so it queues behind the current turn and
2442
+ * runs once the work finishes. Exported for unit testing without a live tmux.
2443
+ */
2444
+ function checkIsClaudeUninterruptible(paneText) {
2445
+ return (CLAUDE_COMPACTING_RE.test(paneText) ||
2446
+ CLAUDE_SUBAGENT_TASK_RE.test(paneText) ||
2447
+ CLAUDE_SUBAGENT_MANAGE_RE.test(paneText));
2448
+ }
2449
+ /**
2450
+ * @description Poll cadence while waiting for an interrupt to land. Each tick
2451
+ * is a cheap read-only `tmux capture-pane`.
2452
+ */
2453
+ const CLAUDE_INTERRUPT_POLL_MS = 100;
2454
+ /**
2455
+ * @description Upper bound on how long we wait for Claude to leave the busy
2456
+ * state after Escape before forwarding the prompt anyway. Escape reliably
2457
+ * interrupts a *thinking* turn within well under a second (observed
2458
+ * ~200 ms–1 s, vs the old 120 ms fixed guess that raced and let the prompt
2459
+ * queue). It does NOT interrupt an in-flight tool/subprocess — those run to
2460
+ * this timeout and then forward (the message queues behind the tool, which is
2461
+ * unavoidable, but is never dropped). So the bound is sized to cover
2462
+ * thinking-interrupt latency, not to wait out a running tool.
2463
+ */
2464
+ const CLAUDE_INTERRUPT_TIMEOUT_MS = 3000;
2465
+ /**
2466
+ * @description Decide whether to stay in resume flood-suppression mode or exit.
2467
+ *
2468
+ * While `keepSeeding` is true, `pollOutput` advances the baseline but emits no
2469
+ * conversation text, so the entire restored transcript Claude repaints on
2470
+ * `--resume` is swallowed instead of dumped into the topic. We exit (and let
2471
+ * the NEXT poll's diff be genuine new output) when either:
2472
+ *
2473
+ * - the pane is non-empty AND unchanged across two consecutive polls (the
2474
+ * restored transcript finished painting — the common case), or
2475
+ * - the hard cap {@link resumeSeedMaxPolls} is hit (paint stuttered forever;
2476
+ * force-exit so a session can never be wedged into permanent silence).
2477
+ *
2478
+ * Pure + exported so the swallow → exit-on-stable → emit-new transition is
2479
+ * unit-testable without a live tmux pane.
2480
+ */
2481
+ function getResumeSeedDecision(state) {
2482
+ const reachedCap = state.polls + 1 >= resumeSeedMaxPolls;
2483
+ const stableNonEmpty = state.content !== '' && state.content === state.prevContent;
2484
+ return { keepSeeding: !reachedCap && !stableNonEmpty };
2485
+ }
2486
+ class ClaudeCliAdapter extends events_1.EventEmitter {
2487
+ name = 'claude';
2488
+ label = 'Claude Code';
2489
+ outputsDeltas = true;
2490
+ // The TUI prints its own banner to the topic — the bot suppresses `agent.ready`
2491
+ // and keeps the typing loader up until that first output (see AgentAdapter).
2492
+ selfGreetsOnStart = true;
2493
+ /**
2494
+ * Map of serialised `SessionKey` (`"<chatId>:<threadId>"`) → live session.
2495
+ * Keyed by string rather than `SessionKey` object so map lookups work — JS
2496
+ * Map compares object identity, not structural equality.
2497
+ */
2498
+ sessions = new Map();
2499
+ /**
2500
+ * Per-thread display-prefs reader, injected by the bot at boot via
2501
+ * `createAdapter.registerDisplayPrefsReader` (S4; same idiom as the OpenCode
2502
+ * adapter). Consulted on the poll hot path: the relay branches each chunk's
2503
+ * tool / panel segments on `toolResults` (S4), and the transcript scan
2504
+ * fast-forwards (non-`full` `subagent`) or streams (`full`). `null` until
2505
+ * wired → reads fall back to all-fields-`minimal`.
2506
+ */
2507
+ displayPrefsReader = null;
2508
+ claudeConfigDirReader = null;
2509
+ setClaudeConfigDirReader(reader) {
2510
+ this.claudeConfigDirReader = reader;
2511
+ }
2512
+ /** The config directory the conversation's next pane must start with (its account profile). */
2513
+ resolveConfigDir(key) {
2514
+ return this.claudeConfigDirReader?.(key) ?? null;
2515
+ }
2516
+ /** The `CLAUDE_CONFIG_DIR` the live pane runs with (`null` = default profile); `undefined` without a live session. */
2517
+ getSessionClaudeConfigDir(key) {
2518
+ const session = this.sessions.get((0, sessionKey_1.keyToString)(key));
2519
+ return session?.isActive ? session.configDir : undefined;
2520
+ }
2521
+ /** @description Inject the per-thread display-prefs reader (see the field's JSDoc). */
2522
+ setDisplayPrefsReader(reader) {
2523
+ this.displayPrefsReader = reader;
2524
+ }
2525
+ /** Per-thread locale reader, injected at boot via `createAdapter.registerThreadLocaleReader`. */
2526
+ threadLocaleReader = null;
2527
+ /** @description Inject the per-thread locale reader (for adapter-side `t(...)` calls). */
2528
+ setThreadLocaleReader(reader) {
2529
+ this.threadLocaleReader = reader;
2530
+ }
2531
+ /** @description Run `fn` inside the thread's locale context so `t(...)` resolves correctly. */
2532
+ tl(key, fn) {
2533
+ return (0, i18n_1.runWithLocale)(this.threadLocaleReader?.(key) ?? i18n_1.defaultLocale, fn);
2534
+ }
2535
+ /**
2536
+ * Per-thread seen-watermark writer, injected by the bot at boot via
2537
+ * `createAdapter.registerSeenWatermarkWriter`. Called on every idle+ready poll
2538
+ * whose transcript EOF grew past the last persisted offset (via
2539
+ * {@link ClaudeCliAdapter.advanceClaudeWatermarkIfGrown}) to persist the new
2540
+ * EOF. `null` until wired → {@link advanceSeenWatermark} is a no-op.
2541
+ */
2542
+ seenWatermarkWriter = null;
2543
+ /** @description Inject the per-thread seen-watermark writer (see the field's JSDoc). */
2544
+ setSeenWatermarkWriter(writer) {
2545
+ this.seenWatermarkWriter = writer;
2546
+ }
2547
+ /** @description Advance the persisted seen-watermark for `key` (no-op until the
2548
+ * writer is wired). */
2549
+ advanceSeenWatermark(key, watermark) {
2550
+ this.seenWatermarkWriter?.(key, watermark);
2551
+ }
2552
+ /**
2553
+ * @description Advance the seen-watermark to the transcript's current EOF when
2554
+ * the input box is ready and the file grew since the last write (S7) — the
2555
+ * fs+writer wrapper around {@link checkShouldAdvanceWatermark}. Called every
2556
+ * non-seeding poll with the poll's `isBusy` / `isReady`; it early-returns only
2557
+ * on a NOT-ready poll BEFORE the `fs.stat` (a boot / lifecycle-gate frame isn't
2558
+ * a settled relay). S7 dropped the busy early-return so a BUSY-but-ready poll
2559
+ * mid-turn advances too — the pane is relayed every poll, so tracking EOF as it
2560
+ * grows keeps the watermark at "relayed up to here" instead of stranding it at
2561
+ * the previous turn end (the mid-turn false-"missed N" fix). It still writes
2562
+ * only when EOF actually grew — so idle metadata churn (Claude rewrites
2563
+ * `last-prompt`/`mode`/… for hours) costs no more than one bounded write per
2564
+ * real growth, and a smaller rewrite never rewinds the in-memory anchor
2565
+ * (monotonic). Best-effort — a transcript not yet on disk (UUID just born)
2566
+ * leaves the watermark where it was, the next poll re-tries. The path resolves
2567
+ * the same way {@link getRecentTurns} / {@link getReattachRecap} do.
2568
+ */
2569
+ advanceClaudeWatermarkIfGrown(key, session, isBusy, isReady) {
2570
+ if (!isReady)
2571
+ return; // skip the fs stat before the input box is up (not a relay)
2572
+ const transcriptPath = getClaudeTranscriptPath(session.workDir, session.claudeSessionId);
2573
+ let eof;
2574
+ try {
2575
+ eof = fs.statSync(transcriptPath).size;
2576
+ }
2577
+ catch {
2578
+ return;
2579
+ }
2580
+ const shouldAdvance = checkShouldAdvanceWatermark({
2581
+ isBusy,
2582
+ isReady,
2583
+ eof,
2584
+ lastOffset: session.lastWatermarkOffset ?? -1,
2585
+ });
2586
+ if (!shouldAdvance)
2587
+ return;
2588
+ this.advanceSeenWatermark(key, { sessionId: session.claudeSessionId, claudeTranscriptOffset: eof });
2589
+ session.lastWatermarkOffset = eof;
2590
+ }
2591
+ /** @description Resolve the thread's full display prefs, defaulting every
2592
+ * field to `minimal` for any read before the bot wires the reader at boot. */
2593
+ getDisplayPrefs(key) {
2594
+ return this.displayPrefsReader?.(key) ?? (0, displayVerbosity_1.getDefaultDisplayPrefs)();
2595
+ }
2596
+ /** @description Resolve the thread's `/subagent` mode (consulted by the
2597
+ * transcript-tail scan), via the full prefs reader. */
2598
+ getSubagentMode(key) {
2599
+ return this.getDisplayPrefs(key).subagent;
2600
+ }
2601
+ createSession(params) {
2602
+ return {
2603
+ ...params,
2604
+ queue: (0, serialQueue_1.createSerialQueue)(),
2605
+ pollTimer: null,
2606
+ lastContent: '',
2607
+ // One-shot auto-retry guard — every fresh session (start / resume /
2608
+ // adopt) begins un-armed; armed on the first retryable `API Error:` line.
2609
+ handledApiError: false,
2610
+ lastStatusText: '',
2611
+ lastForwardedText: '',
2612
+ lastForwardedTextAt: 0,
2613
+ lastQuestionSignature: '',
2614
+ questionAbsentPolls: 0,
2615
+ lastSurveySignature: '',
2616
+ autoEnterTimer: null,
2617
+ autoAcceptOuterTimer: null,
2618
+ autoAcceptInnerTimer: null,
2619
+ isPolling: false,
2620
+ openToolKind: null,
2621
+ // Seeding is armed explicitly by `resumeSession` after createSession;
2622
+ // a fresh `startSession` / reattach never floods, so it stays off here.
2623
+ resumeSeeding: false,
2624
+ resumeSeedPolls: 0,
2625
+ resumeSeedPrevContent: '',
2626
+ lastRawCapture: '',
2627
+ // S2: every fresh session (start / resume / adopt all flow through here)
2628
+ // begins at base cadence — this IS the "reset on session start/resume".
2629
+ // No timer is armed yet, so an explicit resetPollCadence would be a no-op.
2630
+ currentPollDelayMs: pollBackoff_1.basePollIntervalMs,
2631
+ unchangedPollStreak: 0,
2632
+ // Seed the pane-change clock to "now" so a freshly-created/adopted session
2633
+ // (its first poll hasn't run yet) never reads as 30s-static immediately.
2634
+ lastContentChangeAt: Date.now(),
2635
+ // S7: armed explicitly by `applyStoredEffortOnSpawn` after a fresh
2636
+ // start/resume spawn; adopt/reattach leaves it null (no re-apply).
2637
+ pendingEffortReapply: null,
2638
+ // S3 seen-watermark anchor: no advance yet → the first idle+ready poll
2639
+ // writes the current transcript EOF. Fresh per session object; cross-restart
2640
+ // correctness is protected by the recap's PRE-adopt snapshot, not by seeding.
2641
+ lastWatermarkOffset: undefined,
2642
+ // Every fresh session object (start / resume / adopt all flow through
2643
+ // here) begins with an EMPTY relay window — this IS the "reset on
2644
+ // session start"; stop deletes the session and the window with it.
2645
+ recentRelayWindow: (0, recentRelayWindow_1.createRecentRelayWindow)(),
2646
+ // S3: fresh tail state per session — its first scan seeds offsets to
2647
+ // EOF, so resume/adopt never replays an old sub-agent's transcript.
2648
+ subagentTail: (0, claudeSubagentTail_1.createSubagentTailState)(),
2649
+ // S4: fresh cross-poll classifier context per session — the all-closed
2650
+ // start, so a fresh session never inherits a prior session's open block.
2651
+ chunkContext: (0, claudeChunkClassifier_1.createInitialChunkContext)(),
2652
+ // Resize guard: the first successful size query of this session records
2653
+ // the baseline; only a CHANGE after that suppresses (never the first read).
2654
+ lastPaneSize: null,
2655
+ isResizeSettling: false,
2656
+ resizeSettlePolls: 0,
2657
+ // No table held at session start (start / resume / adopt all flow through
2658
+ // here) — this IS the "reset on session start"; stop deletes it with the
2659
+ // session. The resume-seed exit clears it again (see `pollOutput`).
2660
+ streamingTable: null,
2661
+ };
2662
+ }
2663
+ enqueueTmux(session, fn) {
2664
+ return session.queue.run(fn);
2665
+ }
2666
+ enqueueTmuxBestEffort(session, fn) {
2667
+ void this.enqueueTmux(session, fn).catch((e) => {
2668
+ console.warn(`[Claude] tmux operation failed:`, e instanceof Error ? e.message : e);
2669
+ });
2670
+ }
2671
+ async stopSessionInternal(key) {
2672
+ const k = (0, sessionKey_1.keyToString)(key);
2673
+ const session = this.sessions.get(k);
2674
+ if (!session)
2675
+ return;
2676
+ console.log(`[Claude] Stopping session for ${k}`);
2677
+ session.isActive = false;
2678
+ // pollTimer is now a `setTimeout` handle (not interval), but
2679
+ // clearTimeout safely handles either type.
2680
+ if (session.pollTimer) {
2681
+ clearTimeout(session.pollTimer);
2682
+ session.pollTimer = null;
2683
+ }
2684
+ // Audit S9 / #10: cancel pending auto-Enter / auto-Accept callbacks
2685
+ // so they don't land in a replacement session.
2686
+ if (session.autoEnterTimer) {
2687
+ clearTimeout(session.autoEnterTimer);
2688
+ session.autoEnterTimer = null;
2689
+ }
2690
+ if (session.autoAcceptOuterTimer) {
2691
+ clearTimeout(session.autoAcceptOuterTimer);
2692
+ session.autoAcceptOuterTimer = null;
2693
+ }
2694
+ if (session.autoAcceptInnerTimer) {
2695
+ clearTimeout(session.autoAcceptInnerTimer);
2696
+ session.autoAcceptInnerTimer = null;
2697
+ }
2698
+ const killPromise = this.enqueueTmux(session, () => (0, tmuxExec_1.tmuxAsync)('kill-session', '-t', (0, tmuxExec_1.getTmuxSessionTarget)(session.sessionName)));
2699
+ // Remove the tmp MCP files we wrote on startSession — claude inlines
2700
+ // their content into the session at boot, so once tmux is killed they
2701
+ // serve no purpose and would just leak secrets on disk (plan §13.18).
2702
+ (0, mcpConfig_1.cleanupMcpTempFiles)({ key, dataDir: (0, state_1.resolveDataDir)() });
2703
+ this.sessions.delete(k);
2704
+ this.emit('stopped', key);
2705
+ await killPromise;
2706
+ }
2707
+ /**
2708
+ * @description Schedule the next `pollOutput` for a session. Audit S9 /
2709
+ * #37: replaces `setInterval` so a slow `tmux capture-pane` cannot
2710
+ * cause overlapping invocations. The handle is stored back on
2711
+ * `session.pollTimer` so `stopSession` can cancel the next tick.
2712
+ */
2713
+ schedulePoll(key, session) {
2714
+ if (!session.isActive)
2715
+ return;
2716
+ session.pollTimer = setTimeout(() => {
2717
+ void (async () => {
2718
+ if (!session.isActive)
2719
+ return;
2720
+ if (session.isPolling) {
2721
+ // Previous poll still in-flight; skip this tick and reschedule.
2722
+ this.schedulePoll(key, session);
2723
+ return;
2724
+ }
2725
+ session.isPolling = true;
2726
+ try {
2727
+ await this.pollOutput(key);
2728
+ // S3: tail the session's on-disk sub-agent transcripts on the same
2729
+ // tick cadence. Self-guarding (catches everything internally), so a
2730
+ // scan problem can never break the pane polling.
2731
+ await this.scanSubagentTranscripts(key, session);
2732
+ }
2733
+ finally {
2734
+ session.isPolling = false;
2735
+ this.schedulePoll(key, session);
2736
+ }
2737
+ })();
2738
+ }, session.currentPollDelayMs);
2739
+ }
2740
+ /**
2741
+ * @description Per-poll-tick scan of the session's on-disk sub-agent
2742
+ * transcripts (`/subagent` on Claude, plan 2026-06-11 S3). Claude writes
2743
+ * each Task-tool child transcript to
2744
+ * `<projectsRoot>/<slug>/<claudeSessionId>/subagents/agent-<agentId>.jsonl`;
2745
+ * the pure `claudeSubagentTail` helpers own all decisions — first-scan EOF
2746
+ * seeding (no backlog replay on resume/adopt), compact-mode fast-forward
2747
+ * without reading, and assistant-text-block extraction (child thinking /
2748
+ * tool_use never rendered). In `full` mode every appended text block is
2749
+ * emitted as a marked output (`meta.isSubagent`) which the bot renders as a
2750
+ * standalone "🤖 ⤷" message outside the parent's continuation chain.
2751
+ *
2752
+ * A missing dir (`ENOENT` — no sub-agent yet, or an older claude version)
2753
+ * short-circuits cheaply but still runs the empty scan, so the first-scan
2754
+ * flag flips and a transcript created later streams from byte 0. Any other
2755
+ * error is logged and swallowed here — this method must never reject, or
2756
+ * the rejection would escape `schedulePoll`'s void'd poll chain.
2757
+ */
2758
+ async scanSubagentTranscripts(key, session) {
2759
+ try {
2760
+ const subagentsDir = path.join(getClaudeProjectsRoot(), getClaudeProjectSlug(session.workDir), session.claudeSessionId, 'subagents');
2761
+ let entries = [];
2762
+ try {
2763
+ entries = await fs.promises.readdir(subagentsDir);
2764
+ }
2765
+ catch (e) {
2766
+ if (!checkIsMissingPathError(e))
2767
+ throw e;
2768
+ }
2769
+ const scannedFiles = [];
2770
+ for (const fileName of entries.filter(claudeSubagentTail_1.checkIsSubagentTranscriptName).sort()) {
2771
+ try {
2772
+ const stats = await fs.promises.stat(path.join(subagentsDir, fileName));
2773
+ scannedFiles.push({ fileName, sizeBytes: stats.size });
2774
+ }
2775
+ catch {
2776
+ // File vanished between readdir and stat — picked up next tick.
2777
+ }
2778
+ }
2779
+ const reads = (0, claudeSubagentTail_1.getSubagentTailReads)(session.subagentTail, scannedFiles, this.getSubagentMode(key));
2780
+ for (const read of reads) {
2781
+ if (!session.isActive)
2782
+ return;
2783
+ // Per-read guard: one file's failed read (vanished mid-read) loses only
2784
+ // ITS already-advanced range (drop-over-resend), not the sibling files'
2785
+ // ranges the outer catch would otherwise abort with it.
2786
+ try {
2787
+ const appendedText = await readFileSlice(path.join(subagentsDir, read.fileName), read.startOffset, read.endOffset);
2788
+ for (const text of (0, claudeSubagentTail_1.extractAppendedSubagentTexts)(session.subagentTail, read.fileName, appendedText)) {
2789
+ const meta = { isSubagent: true };
2790
+ this.emit('output', key, text, meta);
2791
+ }
2792
+ }
2793
+ catch (e) {
2794
+ console.warn(`[Claude] sub-agent transcript read failed (${read.fileName}):`, e instanceof Error ? e.message : e);
2795
+ }
2796
+ }
2797
+ }
2798
+ catch (e) {
2799
+ console.warn(`[Claude] sub-agent transcript scan failed:`, e instanceof Error ? e.message : e);
2800
+ }
2801
+ }
2802
+ /**
2803
+ * @description Snap a session's poll cadence back to base (S2). Called on
2804
+ * every explicit write (keystroke / signal) and on session start/resume so a
2805
+ * fresh prompt never waits up to {@link maxPollIntervalMs} for the next
2806
+ * capture — user-visible latency after a write must stay at base cadence.
2807
+ *
2808
+ * If a poll timer is already pending we re-arm it at the base delay so the
2809
+ * backed-off idle timer can't keep the next capture far in the future. The
2810
+ * existing {@link ClaudeSession.isPolling} guard is respected: a poll already
2811
+ * in flight will reschedule itself from the (now reset) delay, so we never
2812
+ * double-poll.
2813
+ */
2814
+ resetPollCadence(key, session) {
2815
+ session.currentPollDelayMs = pollBackoff_1.basePollIntervalMs;
2816
+ session.unchangedPollStreak = 0;
2817
+ if (!session.isActive || session.isPolling)
2818
+ return;
2819
+ if (session.pollTimer) {
2820
+ clearTimeout(session.pollTimer);
2821
+ session.pollTimer = null;
2822
+ this.schedulePoll(key, session);
2823
+ }
2824
+ }
2825
+ async startSession(key, workDir, args, sessionId) {
2826
+ await this.stopSessionInternal(key);
2827
+ if (!(0, installManager_1.checkIsInstalled)('claude')) {
2828
+ this.emit('output', key, 'Installing Claude Code...');
2829
+ await (0, installManager_1.installTool)('claude');
2830
+ }
2831
+ const sessionName = buildTmuxSessionName(key);
2832
+ // If the bot didn't provide a UUID, mint one ourselves. The plan owns
2833
+ // generation in bot.ts so it can be persisted in state.json (D14), but
2834
+ // until §11 Этап 3 wires that up we mint here as a safe default.
2835
+ const claudeSessionId = sessionId || (0, crypto_1.randomUUID)();
2836
+ if (!checkIsValidUuid(claudeSessionId)) {
2837
+ // Caller-supplied UUID — refuse anything that doesn't look like one.
2838
+ // See `checkIsValidUuid` for the reasoning (audit S1).
2839
+ // Audit S10 / #16: throw instead of emit+return so callers can
2840
+ // distinguish "did not start" from "started, will fail async".
2841
+ throw new Error(`Invalid sessionId: ${claudeSessionId}`);
2842
+ }
2843
+ if (args && !(0, tmuxExec_1.checkArgsAreSafe)(args)) {
2844
+ throw new Error('Args contain control characters');
2845
+ }
2846
+ console.log(`[Claude] Starting tmux session ${sessionName} in ${workDir} ` +
2847
+ `(sessionId=${claudeSessionId})${args ? ` with args: ${args}` : ''}`);
2848
+ // Make sure no stale session with the same name is lingering.
2849
+ await (0, tmuxExec_1.tmuxAsync)('kill-session', '-t', (0, tmuxExec_1.getTmuxSessionTarget)(sessionName));
2850
+ // Build the claude command line as an argv list, then assemble the final
2851
+ // shell-command for tmux by single-quoting every element. tmux execs the
2852
+ // trailing `shell-command` argument via `$SHELL -c` (audit S1 / #1, #2):
2853
+ // there is no argv-only path for `new-session`, so the only defence is
2854
+ // to ensure no user-controlled string can break out of single quotes.
2855
+ // `shellSingleQuote` handles embedded single quotes via the standard
2856
+ // `'\\''` close-reopen idiom.
2857
+ //
2858
+ // --session-id <uuid> assigns the UUID to the NEW session so we can later
2859
+ // resume by UUID (plan §13.1). --dangerously-skip-permissions stays
2860
+ // hardcoded by D44 (symmetry with opencode auto-approve).
2861
+ //
2862
+ // MCP servers come from up to four sources (user/group/project/thread,
2863
+ // plan §19); user + project are auto-loaded by Claude from cwd, the
2864
+ // other two reach Claude through repeated `--mcp-config` flags. The flag
2865
+ // values point at tmp files because the bot expands `${VAR}` env-var
2866
+ // placeholders itself before handing the config off (plan §13.18, T2).
2867
+ // The `--settings` file carries the PreCompact hook that gives an
2868
+ // overflow-triggered compaction the bot's summary guidance.
2869
+ const mcpFlagsArr = await (0, mcpConfig_1.prepareMcpFlags)({ key, dataDir: (0, state_1.resolveDataDir)() });
2870
+ const configDir = this.resolveConfigDir(key);
2871
+ const claudeArgv = [
2872
+ // A non-default account profile: `env CLAUDE_CONFIG_DIR=<dir>` in front (the pane's shell
2873
+ // runs it, so a child claude the agent starts inherits the same account).
2874
+ ...buildConfigDirEnvPrefix(configDir),
2875
+ claudePath,
2876
+ // Followed by an option on purpose: `--disallowedTools` takes several values.
2877
+ ...(0, claudePlatformFlags_1.getClaudePlatformFlags)(key),
2878
+ ...claudePermissionArgs,
2879
+ '--session-id', claudeSessionId,
2880
+ ...mcpFlagsArr,
2881
+ ...(0, claudeCompactHook_1.prepareClaudeCompactHookFlags)((0, state_1.resolveDataDir)()),
2882
+ ...(args ? [args] : []),
2883
+ ];
2884
+ const claudeShellCmd = claudeArgv.map(tmuxExec_1.shellSingleQuote).join(' ');
2885
+ try {
2886
+ // `-c <workDir>` sets the new session's start directory, avoiding a
2887
+ // preceding `cd && …` chain (which would have to be shell-quoted too).
2888
+ await (0, tmuxExec_1.tmuxOrThrowAsync)('new-session', '-d', '-s', sessionName, '-x', '300', '-y', '50', '-c', workDir, claudeShellCmd);
2889
+ console.log(`[Claude] tmux session created`);
2890
+ }
2891
+ catch (e) {
2892
+ console.error(`[Claude] Failed to create tmux session:`, e);
2893
+ // Audit S9 / #14: even when the spawn fails, tmux can leave a
2894
+ // half-built session and we wrote MCP tmp files we don't want to
2895
+ // leak. Best-effort cleanup before bubbling up the error.
2896
+ await (0, tmuxExec_1.tmuxAsync)('kill-session', '-t', (0, tmuxExec_1.getTmuxSessionTarget)(sessionName));
2897
+ (0, mcpConfig_1.cleanupMcpTempFiles)({ key, dataDir: (0, state_1.resolveDataDir)() });
2898
+ // Audit S10 / #16: throw so the caller's `await startSession()`
2899
+ // sees the failure and skips registering the binding.
2900
+ throw new Error(`Failed to start Claude session: ${e instanceof Error ? e.message : String(e)}`);
2901
+ }
2902
+ const session = this.createSession({
2903
+ key,
2904
+ workDir,
2905
+ sessionName,
2906
+ claudeSessionId,
2907
+ configDir,
2908
+ isActive: true,
2909
+ handledAutoEnter: false,
2910
+ handledAutoAccept: false,
2911
+ });
2912
+ this.sessions.set((0, sessionKey_1.keyToString)(key), session);
2913
+ this.schedulePoll(key, session);
2914
+ // S7: ARM the thread's stored /effort for re-apply once the TUI input box
2915
+ // is actually ready (consumed in the poll loop) — NOT typed now, while the
2916
+ // pane is still painting its boot banner. Fresh spawn only — see
2917
+ // applyStoredEffortOnSpawn.
2918
+ this.applyStoredEffortOnSpawn(key);
2919
+ this.emit('started', key);
2920
+ }
2921
+ stopSession(key) {
2922
+ void this.stopSessionInternal(key).catch((e) => {
2923
+ console.warn(`[Claude] stopSession failed:`, e instanceof Error ? e.message : e);
2924
+ });
2925
+ }
2926
+ checkIsActive(key) {
2927
+ const session = this.sessions.get((0, sessionKey_1.keyToString)(key));
2928
+ return session?.isActive ?? false;
2929
+ }
2930
+ checkIsBusy(key) {
2931
+ const session = this.sessions.get((0, sessionKey_1.keyToString)(key));
2932
+ if (!session)
2933
+ return false;
2934
+ return checkIsClaudeSessionBusy({ isActive: session.isActive, lastContent: session.lastContent });
2935
+ }
2936
+ async getRuntimeInfo(key) {
2937
+ const session = this.sessions.get((0, sessionKey_1.keyToString)(key));
2938
+ if (!session?.isActive) {
2939
+ return { version: null, model: null, contextWindowTokens: null, contextUsedTokens: null };
2940
+ }
2941
+ const filePath = getClaudeTranscriptPath(session.workDir, session.claudeSessionId);
2942
+ try {
2943
+ return await (0, claudeRuntimeInfo_1.readClaudeRuntimeInfo)(filePath);
2944
+ }
2945
+ catch {
2946
+ return { version: null, model: null, contextWindowTokens: null, contextUsedTokens: null };
2947
+ }
2948
+ }
2949
+ /**
2950
+ * @description Ms since the TUI pane last actually changed (raw `capture-pane`
2951
+ * differed), or `null` when there is no live session for this thread. Feeds
2952
+ * the bot's S2 pane-static idle net: a genuinely working agent repaints the
2953
+ * pane every second, so a large value means the agent is idle even if the
2954
+ * footer busy signal is stuck. `null` is never treated as idle.
2955
+ */
2956
+ getMsSincePaneChange(key) {
2957
+ const session = this.sessions.get((0, sessionKey_1.keyToString)(key));
2958
+ if (!session?.isActive)
2959
+ return null;
2960
+ return Date.now() - session.lastContentChangeAt;
2961
+ }
2962
+ sendInput(key, input, options) {
2963
+ const session = this.sessions.get((0, sessionKey_1.keyToString)(key));
2964
+ if (!session?.isActive) {
2965
+ console.log(`[Claude] sendInput: no active session for ${(0, sessionKey_1.keyToString)(key)}`);
2966
+ return;
2967
+ }
2968
+ const appendEnter = options?.appendEnter ?? true;
2969
+ console.log(`[Claude] sendInput: "${input}"${appendEnter ? '' : ' (no Enter)'}`);
2970
+ // S1: remember what we just typed (every path — forward, `/c continue`,
2971
+ // buffered replay, control replies) so the content-based echo gate can
2972
+ // recognise Claude's pane echo of it, even when the echo carries no `❯`.
2973
+ session.lastForwardedText = input;
2974
+ session.lastForwardedTextAt = Date.now();
2975
+ this.resetPollCadence(key, session);
2976
+ // The ordered keystroke plan is pure (getClaudeSendKeysPlan) so the "no
2977
+ // Enter" decision is unit-testable; here we just execute each step.
2978
+ const steps = getClaudeSendKeysPlan(input, appendEnter);
2979
+ for (const step of steps) {
2980
+ if (step === 'literal') {
2981
+ // Argv-based send-keys: tmux never invokes a shell here, so user-typed
2982
+ // `$(...)` / backticks are delivered to claude's stdin as literal
2983
+ // bytes. The previous implementation used `execSync` with a shell
2984
+ // template; `JSON.stringify(input)` wraps the text in double quotes,
2985
+ // and `/bin/sh` happily expands `$(...)` inside double quotes BEFORE
2986
+ // tmux ever sees the keys — that was the RCE flagged by audit S1 / #1.
2987
+ //
2988
+ // `-l` tells tmux to treat the next argument as literal keys, not as
2989
+ // tmux special-key names (so the user typing the word "Enter" wouldn't
2990
+ // be rewritten to a newline). A separate call adds the actual Enter.
2991
+ this.enqueueTmuxBestEffort(session, async () => {
2992
+ if (!session.isActive)
2993
+ return '';
2994
+ return (0, tmuxExec_1.tmuxAsync)('send-keys', '-t', (0, tmuxExec_1.getTmuxPaneTarget)(session.sessionName), '-l', input);
2995
+ });
2996
+ }
2997
+ else if (step === 'slashEnter') {
2998
+ // Bare slash commands (`/compact`, `/clear`, …) open Claude's command
2999
+ // autocomplete popup; an Enter fired the same instant accepts the popup
3000
+ // highlight instead of running the command (so it silently no-ops). Defer
3001
+ // the Enter so the popup settles first. Re-check the session is still
3002
+ // alive when the timer fires — it may have been stopped meanwhile.
3003
+ const sessionName = session.sessionName;
3004
+ setTimeout(() => {
3005
+ const current = this.sessions.get((0, sessionKey_1.keyToString)(key));
3006
+ if (!current?.isActive || current.sessionName !== sessionName)
3007
+ return;
3008
+ this.enqueueTmuxBestEffort(current, async () => {
3009
+ if (!current.isActive)
3010
+ return '';
3011
+ return (0, tmuxExec_1.tmuxAsync)('send-keys', '-t', (0, tmuxExec_1.getTmuxPaneTarget)(sessionName), 'Enter');
3012
+ });
3013
+ }, CLAUDE_SLASH_ENTER_DELAY_MS);
3014
+ }
3015
+ else if (step === 'verifiedEnter') {
3016
+ // Plain prompt: defer the Enter past the TUI's paste-aggregation window
3017
+ // (see CLAUDE_TEXT_ENTER_DELAY_MS) so it submits instead of being absorbed
3018
+ // as a paste newline. The delay runs INSIDE the queued fn (not a bare
3019
+ // setTimeout): the per-session queue is the ordering guarantee, so any op
3020
+ // enqueued later — the next prompt's text, an interrupt Escape from
3021
+ // startup-prompt replay — physically cannot land between this text and
3022
+ // its submit Enter. An 80ms hold of THIS session's queue is harmless
3023
+ // (polls run every 300ms); other sessions' queues are independent.
3024
+ this.enqueueTmuxBestEffort(session, async () => {
3025
+ if (!session.isActive)
3026
+ return '';
3027
+ await (0, utils_1.sleep)(CLAUDE_TEXT_ENTER_DELAY_MS);
3028
+ if (!session.isActive)
3029
+ return '';
3030
+ return (0, tmuxExec_1.tmuxAsync)('send-keys', '-t', (0, tmuxExec_1.getTmuxPaneTarget)(session.sessionName), 'Enter');
3031
+ });
3032
+ this.scheduleEnterVerification(key, session.sessionName, input);
3033
+ }
3034
+ else {
3035
+ // Short control replies (y/n/option digits) can't trigger paste
3036
+ // aggregation — submit instantly, no verification capture.
3037
+ this.enqueueTmuxBestEffort(session, async () => {
3038
+ if (!session.isActive)
3039
+ return '';
3040
+ return (0, tmuxExec_1.tmuxAsync)('send-keys', '-t', (0, tmuxExec_1.getTmuxPaneTarget)(session.sessionName), 'Enter');
3041
+ });
3042
+ }
3043
+ }
3044
+ }
3045
+ /**
3046
+ * @description Post-Enter verification for a plain prompt (B5). After a short
3047
+ * settle, capture the pane through the session queue and — if the prompt still
3048
+ * looks unsubmitted (see {@link checkLooksUnsubmitted}) — re-send Enter exactly
3049
+ * ONCE. No further retries: a duplicate Enter on an already-submitted prompt is
3050
+ * a harmless no-op, but an unbounded retry loop on a genuinely-stuck pane would
3051
+ * spam keystrokes. Same staleness guards as the deferred Enter.
3052
+ */
3053
+ scheduleEnterVerification(key, sessionName, typedText) {
3054
+ setTimeout(() => {
3055
+ const current = this.sessions.get((0, sessionKey_1.keyToString)(key));
3056
+ if (!current?.isActive || current.sessionName !== sessionName)
3057
+ return;
3058
+ void this.enqueueTmux(current, () => (0, tmuxExec_1.tmuxAsync)('capture-pane', '-t', (0, tmuxExec_1.getTmuxPaneTarget)(sessionName), '-p'))
3059
+ .then((pane) => {
3060
+ const live = this.sessions.get((0, sessionKey_1.keyToString)(key));
3061
+ if (!live?.isActive || live.sessionName !== sessionName)
3062
+ return;
3063
+ if (!checkLooksUnsubmitted(pane, typedText))
3064
+ return;
3065
+ console.log(`[Claude] Enter retry (paste-race)`);
3066
+ this.enqueueTmuxBestEffort(live, async () => {
3067
+ if (!live.isActive)
3068
+ return '';
3069
+ return (0, tmuxExec_1.tmuxAsync)('send-keys', '-t', (0, tmuxExec_1.getTmuxPaneTarget)(sessionName), 'Enter');
3070
+ });
3071
+ })
3072
+ .catch(() => {
3073
+ /* capture failures are non-fatal: the next poll still drives the UI */
3074
+ });
3075
+ }, CLAUDE_ENTER_VERIFY_DELAY_MS);
3076
+ }
3077
+ sendSignal(key, signal) {
3078
+ const session = this.sessions.get((0, sessionKey_1.keyToString)(key));
3079
+ if (!session?.isActive)
3080
+ return;
3081
+ this.resetPollCadence(key, session);
3082
+ if (signal === 'SIGINT') {
3083
+ this.enqueueTmuxBestEffort(session, async () => {
3084
+ if (!session.isActive)
3085
+ return '';
3086
+ return (0, tmuxExec_1.tmuxAsync)('send-keys', '-t', (0, tmuxExec_1.getTmuxPaneTarget)(session.sessionName), 'C-c');
3087
+ });
3088
+ console.log(`[Claude] sent Ctrl+C`);
3089
+ }
3090
+ }
3091
+ sendEnter(key) {
3092
+ const session = this.sessions.get((0, sessionKey_1.keyToString)(key));
3093
+ if (!session?.isActive)
3094
+ return;
3095
+ this.resetPollCadence(key, session);
3096
+ console.log(`[Claude] sendEnter`);
3097
+ this.enqueueTmuxBestEffort(session, async () => {
3098
+ if (!session.isActive)
3099
+ return '';
3100
+ return (0, tmuxExec_1.tmuxAsync)('send-keys', '-t', (0, tmuxExec_1.getTmuxPaneTarget)(session.sessionName), 'Enter');
3101
+ });
3102
+ }
3103
+ sendArrow(key, direction) {
3104
+ const session = this.sessions.get((0, sessionKey_1.keyToString)(key));
3105
+ if (!session?.isActive)
3106
+ return;
3107
+ this.resetPollCadence(key, session);
3108
+ console.log(`[Claude] sendArrow: ${direction}`);
3109
+ this.enqueueTmuxBestEffort(session, async () => {
3110
+ if (!session.isActive)
3111
+ return '';
3112
+ return (0, tmuxExec_1.tmuxAsync)('send-keys', '-t', (0, tmuxExec_1.getTmuxPaneTarget)(session.sessionName), direction);
3113
+ });
3114
+ }
3115
+ sendTab(key) {
3116
+ const session = this.sessions.get((0, sessionKey_1.keyToString)(key));
3117
+ if (!session?.isActive)
3118
+ return;
3119
+ this.resetPollCadence(key, session);
3120
+ console.log(`[Claude] sendTab`);
3121
+ this.enqueueTmuxBestEffort(session, async () => {
3122
+ if (!session.isActive)
3123
+ return '';
3124
+ return (0, tmuxExec_1.tmuxAsync)('send-keys', '-t', (0, tmuxExec_1.getTmuxPaneTarget)(session.sessionName), 'Tab');
3125
+ });
3126
+ }
3127
+ /**
3128
+ * @description S4: auto-dismiss the native Claude session-feedback survey by
3129
+ * sending a single Escape into the pane (Escape clears the survey overlay the
3130
+ * same way it dismisses a selector). Fire-and-forget through the session queue,
3131
+ * like {@link sendEscape}; the caller's {@link checkShouldDismissSurvey}
3132
+ * signature dedup guarantees it fires once per appearance. Never surfaced to
3133
+ * the topic — the survey must not occupy input / disrupt the scrape.
3134
+ */
3135
+ dismissClaudeSurvey(session) {
3136
+ console.log('[Claude] auto-dismissing session survey (Escape)');
3137
+ this.enqueueTmuxBestEffort(session, async () => {
3138
+ if (!session.isActive)
3139
+ return '';
3140
+ return (0, tmuxExec_1.tmuxAsync)('send-keys', '-t', (0, tmuxExec_1.getTmuxPaneTarget)(session.sessionName), 'Escape');
3141
+ });
3142
+ }
3143
+ // Fire-and-forget single Escape keystroke (interrupt the current turn /
3144
+ // dismiss a selector). Deliberately distinct from `interruptAndWaitIdle`:
3145
+ // this is a raw one-shot key, NOT a wait-for-idle interrupt.
3146
+ sendEscape(key) {
3147
+ const session = this.sessions.get((0, sessionKey_1.keyToString)(key));
3148
+ if (!session?.isActive)
3149
+ return;
3150
+ this.resetPollCadence(key, session);
3151
+ console.log(`[Claude] sendEscape`);
3152
+ this.enqueueTmuxBestEffort(session, async () => {
3153
+ if (!session.isActive)
3154
+ return '';
3155
+ return (0, tmuxExec_1.tmuxAsync)('send-keys', '-t', (0, tmuxExec_1.getTmuxPaneTarget)(session.sessionName), 'Escape');
3156
+ });
3157
+ }
3158
+ /**
3159
+ * @description Interrupt the current turn, then wait until Claude is
3160
+ * actually idle before resolving, so the caller can type a fresh prompt
3161
+ * without it being queued behind the running turn.
3162
+ *
3163
+ * Sends a single Escape — which both cancels an on-screen selector AND
3164
+ * breaks Claude out of the "busy" state — then polls the pane footer until
3165
+ * the `esc to interrupt` hint clears. A fixed post-Escape delay was
3166
+ * unreliable: heavy extended-thinking turns take longer than the old 120 ms
3167
+ * to tear down, so the prompt landed while Claude was still busy and got
3168
+ * queued (the "voice message arrives before the interrupt, then the agent
3169
+ * waits" bug). On timeout we forward anyway rather than drop the message.
3170
+ *
3171
+ * Exception: if Claude is running a sub-agent or compacting context, we do
3172
+ * NOT interrupt — Escape would kill the child / abort the compaction. We
3173
+ * return without Escape so the caller's prompt queues behind the current
3174
+ * turn and runs once that work finishes.
3175
+ */
3176
+ async interruptAndWaitIdle(key) {
3177
+ const session = this.sessions.get((0, sessionKey_1.keyToString)(key));
3178
+ if (!session?.isActive)
3179
+ return;
3180
+ this.resetPollCadence(key, session);
3181
+ const paneBefore = await this.enqueueTmux(session, () => (0, tmuxExec_1.tmuxAsync)('capture-pane', '-t', (0, tmuxExec_1.getTmuxPaneTarget)(session.sessionName), '-p'));
3182
+ if (!session.isActive)
3183
+ return;
3184
+ if (checkIsClaudeUninterruptible(paneBefore)) {
3185
+ console.log(`[Claude] sub-agent/compaction in progress — queueing prompt, not interrupting`);
3186
+ return;
3187
+ }
3188
+ console.log(`[Claude] sendEscape (interrupt)`);
3189
+ await this.enqueueTmux(session, () => (0, tmuxExec_1.tmuxAsync)('send-keys', '-t', (0, tmuxExec_1.getTmuxPaneTarget)(session.sessionName), 'Escape'));
3190
+ if (!session.isActive)
3191
+ return;
3192
+ const deadline = Date.now() + CLAUDE_INTERRUPT_TIMEOUT_MS;
3193
+ while (Date.now() < deadline) {
3194
+ await new Promise((resolve) => setTimeout(resolve, CLAUDE_INTERRUPT_POLL_MS));
3195
+ const current = this.sessions.get((0, sessionKey_1.keyToString)(key));
3196
+ if (!current?.isActive)
3197
+ return;
3198
+ const pane = await this.enqueueTmux(current, () => (0, tmuxExec_1.tmuxAsync)('capture-pane', '-t', (0, tmuxExec_1.getTmuxPaneTarget)(current.sessionName), '-p'));
3199
+ if (!current.isActive)
3200
+ return;
3201
+ if (!checkIsClaudeBusy(pane)) {
3202
+ console.log(`[Claude] interrupt landed — idle, forwarding prompt`);
3203
+ return;
3204
+ }
3205
+ }
3206
+ console.log(`[Claude] interrupt: still busy after ${CLAUDE_INTERRUPT_TIMEOUT_MS}ms, forwarding anyway`);
3207
+ }
3208
+ /**
3209
+ * @description Whether a selector is currently on screen. Backed by the same
3210
+ * `lastQuestionSignature` the output pump sets when it scrapes a question
3211
+ * block (see the `extractClaudeQuestion` call in the poll loop) — non-empty
3212
+ * means a question is being shown and awaiting an answer.
3213
+ */
3214
+ isQuestionPending(key) {
3215
+ const session = this.sessions.get((0, sessionKey_1.keyToString)(key));
3216
+ return Boolean(session?.isActive && session.lastQuestionSignature);
3217
+ }
3218
+ /**
3219
+ * @description Whether Claude's `/login` OAuth "paste the code" box is on
3220
+ * screen right now, read live off the last captured pane ({@link checkIsClaudeLoginPaste}).
3221
+ * No signature/de-dup is needed — nothing is emitted; this only answers
3222
+ * "should the next text reply be typed verbatim into the paste box?" when a
3223
+ * message arrives, so the bot skips the prompt path (Escape + preamble) that
3224
+ * would otherwise cancel the login and corrupt the code.
3225
+ */
3226
+ isLoginPastePending(key) {
3227
+ const session = this.sessions.get((0, sessionKey_1.keyToString)(key));
3228
+ return Boolean(session?.isActive && checkIsClaudeLoginPaste(session.lastContent));
3229
+ }
3230
+ /**
3231
+ * @description For Claude CLI, model switching is done via the /model slash command.
3232
+ * Sends "/model <modelId>" as input to the tmux session. Returns `null`
3233
+ * on success (best-effort: claude doesn't ack the change synchronously).
3234
+ * Audit S10 / #39: unified signature with OpenCode adapter.
3235
+ *
3236
+ * Unlike OpenCode, Claude has NO model-pref persistence — model switching is
3237
+ * a TUI keystroke with no on-disk pref to replay at next start. So with no
3238
+ * active session there is nothing to do but refuse, otherwise `sendInput`
3239
+ * silently no-ops and the caller would report a false "model set" success
3240
+ * (reachable today via the ungated numeric-pick path).
3241
+ */
3242
+ async setModel(key, modelId) {
3243
+ const session = this.sessions.get((0, sessionKey_1.keyToString)(key));
3244
+ if (!session?.isActive)
3245
+ return (0, i18n_1.t)('model.start_agent_first');
3246
+ this.sendInput(key, `/model ${modelId}`);
3247
+ // No explicit /effort pref → (re)apply the bot's default for the newly
3248
+ // picked model (claude self-clamps unsupported levels). An explicit pick
3249
+ // is left alone — claude carries it across the model switch.
3250
+ if (this.getEffort(key) === null)
3251
+ this.sendInput(key, `/effort ${effortLevels_1.defaultEffortLevel}`);
3252
+ return null;
3253
+ }
3254
+ getCurrentModel(_key) {
3255
+ return null;
3256
+ }
3257
+ /**
3258
+ * @description Set the reasoning effort for this thread by typing claude's
3259
+ * native `/effort <level>` slash command into the running TUI.
3260
+ *
3261
+ * Plan 2026-05-30-effort-command / S3, D2/D6:
3262
+ *
3263
+ * - **Validation** is against the canonical Claude set (`getClaudeAvailableLevels`),
3264
+ * not per-model: the adapter can't read claude's live model after a
3265
+ * `/model` switch (`getCurrentModel` returns `null`), so we trust claude
3266
+ * to clamp an unsupported level for the actual model down to its nearest
3267
+ * supported one. Caller already filtered against the same canonical list
3268
+ * when building the picker.
3269
+ * - **Apply** is best-effort via {@link sendInput} (the keystroke path used
3270
+ * by `setModel`). Returns `null` immediately — claude's TUI doesn't
3271
+ * acknowledge the change synchronously and we don't poll-and-wait for it.
3272
+ * - **Persist** to the per-thread prefs file so the banner / `/effort`
3273
+ * picker survives a bot restart (claude's own settings.json is global).
3274
+ *
3275
+ * Returns a short notice instead of `null` when the session isn't running
3276
+ * (we still persist the choice so the next /claude picks it up in the
3277
+ * banner; live apply happens once the agent actually exists).
3278
+ */
3279
+ async setEffort(key, level) {
3280
+ if (!(0, effortLevels_1.checkIsClaudeEffortLevel)(level)) {
3281
+ return (0, i18n_1.t)('effort.invalid_level', {
3282
+ level,
3283
+ valid: (0, effortLevels_1.getClaudeAvailableLevels)().join(', '),
3284
+ });
3285
+ }
3286
+ // Persist first — the menu/banner must reflect the user's choice even
3287
+ // when there is no running session yet (D6).
3288
+ saveEffortPref(key, level);
3289
+ const session = this.sessions.get((0, sessionKey_1.keyToString)(key));
3290
+ if (!session?.isActive) {
3291
+ // Soft notice: choice is recorded, but live apply is deferred until a
3292
+ // session exists. Bot surfaces this distinctly from a hard error so
3293
+ // the user knows to start an agent.
3294
+ return (0, i18n_1.t)('effort.start_agent_first');
3295
+ }
3296
+ this.sendInput(key, `/effort ${level}`);
3297
+ return null;
3298
+ }
3299
+ /**
3300
+ * @description Last bot-set effort for this thread, or `null` if none.
3301
+ *
3302
+ * Reads from the on-disk prefs file (not an in-memory cache): the file is
3303
+ * tiny and the read happens only on menu / banner refresh paths, so a
3304
+ * cache would just be drift surface. Whatever claude itself stores in
3305
+ * its global `settings.json` is invisible to us by design (D2).
3306
+ */
3307
+ getEffort(key) {
3308
+ const prefs = loadEffortPrefs();
3309
+ return prefs[(0, sessionKey_1.keyToString)(key)] ?? null;
3310
+ }
3311
+ /**
3312
+ * @description ARM the thread's stored `/effort` choice for re-apply on a
3313
+ * FRESH TUI spawn (S7) — deferred, not typed immediately. claude persists
3314
+ * effort GLOBALLY in its own settings.json, so a fresh `startSession` /
3315
+ * `resumeSession` would otherwise inherit the last globally-set level —
3316
+ * possibly chosen in another topic.
3317
+ *
3318
+ * Timing is the whole point: a fresh tmux session returns the instant
3319
+ * `new-session` succeeds, while the claude TUI is still painting its boot
3320
+ * banner — typing into it then interleaves keystrokes with the paint and the
3321
+ * command sits unsubmitted (live bug 2026-06-05). So we do NOT type here; we
3322
+ * stash the level on `session.pendingEffortReapply` and let the poll loop
3323
+ * consume it the FIRST time the pane shows a ready input box
3324
+ * ({@link checkIsClaudePromptReady}). That readiness moment is strictly after
3325
+ * the banner paints and strictly before the box can echo a buffered prompt,
3326
+ * giving the required ordering (effort BEFORE buffered prompts) via the same
3327
+ * serial tmux queue.
3328
+ *
3329
+ * Called at the END of `startSession` / `resumeSession`. NOT called from
3330
+ * `adoptExistingTmuxSession` — that claude process survived the restart with
3331
+ * its in-TUI effort intact and may be mid-turn, so re-applying would be both
3332
+ * unnecessary and risk interleaving with a running stream.
3333
+ *
3334
+ * No stored pref → the bot's default (`defaultEffortLevel` = xhigh) is armed
3335
+ * instead of claude's own default, so a no-pref spawn types `/effort xhigh`
3336
+ * (claude clamps per model — same as a manual `/effort` or an explicit pref).
3337
+ * An explicit per-thread pref still wins and is armed verbatim.
3338
+ */
3339
+ applyStoredEffortOnSpawn(key) {
3340
+ const session = this.sessions.get((0, sessionKey_1.keyToString)(key));
3341
+ if (!session)
3342
+ return;
3343
+ const level = this.getEffort(key) ?? effortLevels_1.defaultEffortLevel;
3344
+ if (!(0, effortStartupKeystroke_1.getEffortStartupKeystroke)(level))
3345
+ return;
3346
+ session.pendingEffortReapply = level;
3347
+ }
3348
+ /**
3349
+ * @description S7 consumption point: once the poll loop sees the TUI input
3350
+ * box is up and no boot gate is left, type the armed `/effort <level>` ONCE
3351
+ * and clear the pending flag (one-shot, can't fire twice). Reuses the exact
3352
+ * keystroke path a manual `/effort` uses ({@link sendInput} with its
3353
+ * bare-slash deferred-Enter handling) so there is no duplicate keystroke
3354
+ * logic. Enqueuing onto the session's serial tmux queue at this ready moment
3355
+ * guarantees the keystroke lands in a usable input box (not the boot paint)
3356
+ * and ahead of any buffered prompt the bot replays onto the same queue.
3357
+ */
3358
+ consumePendingEffortReapply(session) {
3359
+ const keystroke = (0, effortStartupKeystroke_1.getEffortStartupKeystroke)(session.pendingEffortReapply);
3360
+ if (!keystroke)
3361
+ return;
3362
+ session.pendingEffortReapply = null;
3363
+ this.sendInput(session.key, keystroke);
3364
+ }
3365
+ /**
3366
+ * @description Effort levels offered by the `/effort` picker for Claude.
3367
+ *
3368
+ * Returns the canonical set unconditionally (plan D2). The `key` argument
3369
+ * is accepted for interface symmetry with OpenCode (whose set depends on
3370
+ * the per-thread current model) but is intentionally unused here.
3371
+ */
3372
+ async getAvailableEffortLevels(_key) {
3373
+ return (0, effortLevels_1.getClaudeAvailableLevels)();
3374
+ }
3375
+ getFullOutput(key, lines = 500) {
3376
+ const session = this.sessions.get((0, sessionKey_1.keyToString)(key));
3377
+ if (!session?.isActive)
3378
+ return null;
3379
+ return session.lastContent.split('\n').slice(-lines).join('\n') || null;
3380
+ }
3381
+ /**
3382
+ * @description Expose the Claude `--session-id` UUID for a live session.
3383
+ * The bot calls this right after `startSession()` so the UUID can be
3384
+ * persisted in state.json and reused on later resumes (plan §13.1, D14).
3385
+ */
3386
+ getClaudeSessionId(key) {
3387
+ return this.sessions.get((0, sessionKey_1.keyToString)(key))?.claudeSessionId ?? null;
3388
+ }
3389
+ // Lists REAL resumable sessions from `~/.claude/projects/<cwd>/*.jsonl`,
3390
+ // not a bot-private registry. This makes a conversation started by hand
3391
+ // on the laptop in `workDir` resumable from the bound Telegram thread.
3392
+ // `key` is unused: Claude scopes transcripts by folder, not by thread.
3393
+ async getSessions(_key, workDir) {
3394
+ return listClaudeSessionsForWorkDir(getClaudeProjectsRoot(), workDir);
3395
+ }
3396
+ /**
3397
+ * @description Read the last `limit` conversational turns of a resumable
3398
+ * Claude session for the resume context block. Resolves the same
3399
+ * `~/.claude/projects/<slug>/<sessionId>.jsonl` path the session listing uses
3400
+ * (`key` is unused — Claude scopes transcripts by folder, not by thread) and
3401
+ * delegates to the pure {@link readRecentClaudeTurns}. A missing transcript
3402
+ * (unknown / pruned UUID) yields `[]`, so the caller posts no context block.
3403
+ */
3404
+ async getRecentTurns(_key, workDir, sessionId, limit) {
3405
+ const filePath = getClaudeTranscriptPath(workDir, sessionId);
3406
+ return readRecentClaudeTurns(filePath, limit);
3407
+ }
3408
+ /**
3409
+ * @description Assemble the silent-reattach recap for Claude from the on-disk
3410
+ * transcript via ONE {@link readClaudeReattachTranscript} read: the
3411
+ * missed-message count is the assistant turns appended after the watermark
3412
+ * byte offset, the body is the last {@link resumeContextTurnLimit} turns, and
3413
+ * `isActive` is the best-effort in-memory busy flag (the just-adopted session
3414
+ * seeds `lastContent` from the current pane, so an `esc to interrupt` footer
3415
+ * reads as still-working). The offset is trusted only when the watermark's
3416
+ * `sessionId` matches the session being recapped — a stale watermark from a
3417
+ * different session (`/new`, `/sessions` resume keep the agent row) is treated
3418
+ * as unknown. A `null`/mismatched/offset-less watermark yields
3419
+ * `isWatermarkKnown: false` and `missedCount: 0` (the fallback recap), while
3420
+ * the body still shows the last turns.
3421
+ */
3422
+ async getReattachRecap(key, workDir, sessionId, watermark) {
3423
+ const filePath = getClaudeTranscriptPath(workDir, sessionId);
3424
+ const offset = watermark?.claudeTranscriptOffset;
3425
+ const isWatermarkKnown = typeof offset === 'number' && watermark?.sessionId === sessionId;
3426
+ // ONE read serves both the count and the body. When the watermark is unknown
3427
+ // we still want the turn body, so read from 0 and drop the count below.
3428
+ const { missedCount, turns, headOffset } = readClaudeReattachTranscript(filePath, offset ?? 0, resumeContext_1.resumeContextTurnLimit);
3429
+ const isActive = this.checkIsBusy(key);
3430
+ // Head watermark = the transcript's current size, scoped to THIS session.
3431
+ // Omitted when the file was unreadable (head unknown → no idempotent advance).
3432
+ const headWatermark = headOffset === undefined ? undefined : { sessionId, claudeTranscriptOffset: headOffset };
3433
+ return { missedCount: isWatermarkKnown ? missedCount : 0, turns, isWatermarkKnown, isActive, headWatermark };
3434
+ }
3435
+ /**
3436
+ * @description Resume a Claude session by UUID.
3437
+ *
3438
+ * Two fixes vs. the legacy implementation:
3439
+ *
3440
+ * 1. **Required `workDir`.** The old `resumeSession` fell back to
3441
+ * `process.env.WORK_DIR || '/workspace'`, which is wrong as soon as the
3442
+ * bot manages multiple folders. The bot now passes the correct workDir
3443
+ * from the thread binding.
3444
+ *
3445
+ * 2. **Use `--resume <uuid>` instead of `--resume` with no argument.** The
3446
+ * no-arg form opens an interactive picker that can't be driven from a
3447
+ * headless tmux pane, which manifested as a silent hang (see plan
3448
+ * §13.1, fix to claudeCliAdapter.ts:455). If Claude can't find the UUID
3449
+ * (history pruned, different machine), it just starts a new session;
3450
+ * we surface that to the user (T8 in plan §16.3).
3451
+ */
3452
+ async resumeSession(key, workDir, sessionId, options) {
3453
+ await this.stopSessionInternal(key);
3454
+ if (!(0, installManager_1.checkIsInstalled)('claude')) {
3455
+ this.emit('output', key, 'Installing Claude Code...');
3456
+ await (0, installManager_1.installTool)('claude');
3457
+ }
3458
+ if (!checkIsValidUuid(sessionId)) {
3459
+ throw new Error(`Invalid sessionId: ${sessionId}`);
3460
+ }
3461
+ const sessionName = buildTmuxSessionName(key);
3462
+ console.log(`[Claude] Resuming session ${sessionId} in ${workDir} for ${(0, sessionKey_1.keyToString)(key)}`);
3463
+ await (0, tmuxExec_1.tmuxAsync)('kill-session', '-t', (0, tmuxExec_1.getTmuxSessionTarget)(sessionName));
3464
+ // Pass the UUID explicitly. If it's unknown to claude, it'll just print a
3465
+ // notice and start fresh — better than hanging on a picker. MCP flags
3466
+ // are re-applied here so a resumed session sees the same servers as a
3467
+ // fresh one would (plan §19). Argv-style shell-quoting mirrors
3468
+ // `startSession` (audit S1).
3469
+ const mcpFlagsArr = await (0, mcpConfig_1.prepareMcpFlags)({ key, dataDir: (0, state_1.resolveDataDir)() });
3470
+ const configDir = this.resolveConfigDir(key);
3471
+ const claudeArgv = [
3472
+ ...buildConfigDirEnvPrefix(configDir),
3473
+ claudePath,
3474
+ // Followed by an option on purpose: `--disallowedTools` takes several values.
3475
+ ...(0, claudePlatformFlags_1.getClaudePlatformFlags)(key),
3476
+ ...claudePermissionArgs,
3477
+ '--resume', sessionId,
3478
+ ...mcpFlagsArr,
3479
+ ...(0, claudeCompactHook_1.prepareClaudeCompactHookFlags)((0, state_1.resolveDataDir)()),
3480
+ ];
3481
+ const claudeShellCmd = claudeArgv.map(tmuxExec_1.shellSingleQuote).join(' ');
3482
+ try {
3483
+ await (0, tmuxExec_1.tmuxOrThrowAsync)('new-session', '-d', '-s', sessionName, '-x', '300', '-y', '50', '-c', workDir, claudeShellCmd);
3484
+ }
3485
+ catch (e) {
3486
+ console.error(`[Claude] Failed to resume session:`, e);
3487
+ await (0, tmuxExec_1.tmuxAsync)('kill-session', '-t', (0, tmuxExec_1.getTmuxSessionTarget)(sessionName));
3488
+ (0, mcpConfig_1.cleanupMcpTempFiles)({ key, dataDir: (0, state_1.resolveDataDir)() });
3489
+ throw new Error(`Failed to resume Claude session: ${e instanceof Error ? e.message : String(e)}`);
3490
+ }
3491
+ const claudeSession = this.createSession({
3492
+ key,
3493
+ workDir,
3494
+ sessionName,
3495
+ claudeSessionId: sessionId,
3496
+ configDir,
3497
+ isActive: true,
3498
+ handledAutoEnter: false,
3499
+ handledAutoAccept: false,
3500
+ });
3501
+ // Arm flood-suppression BEFORE the first poll: Claude repaints the whole
3502
+ // restored transcript into the pane over the next polls, which would
3503
+ // otherwise be relayed as one giant dump. Seeding swallows that text while
3504
+ // still advancing the baseline (see `pollOutput` + `getResumeSeedDecision`).
3505
+ claudeSession.resumeSeeding = true;
3506
+ this.sessions.set((0, sessionKey_1.keyToString)(key), claudeSession);
3507
+ this.schedulePoll(key, claudeSession);
3508
+ // S7: ARM the stored /effort for re-apply on this fresh resume spawn too
3509
+ // (claude's global effort state could be from another topic). Deferred to
3510
+ // the poll-loop readiness point — same as startSession; the resume seeding
3511
+ // repaint must finish before the input box is ready, which the readiness
3512
+ // predicate already requires. See applyStoredEffortOnSpawn.
3513
+ this.applyStoredEffortOnSpawn(key);
3514
+ this.emit('started', key);
3515
+ // Post the short last-N-turn context block in place of the flood. ONLY on
3516
+ // the explicit user resume (`/sessions` pick) — a silent re-attach must
3517
+ // stay quiet (see ResumeSessionOptions). The `.jsonl` is independent of
3518
+ // the pane, so this can run immediately (it does not wait for seeding to
3519
+ // finish). Best-effort: a read failure or empty history simply posts no
3520
+ // extra block — the normal "resumed" reply stands.
3521
+ if (options?.isWithRecentContext) {
3522
+ try {
3523
+ const turns = await this.getRecentTurns(key, workDir, sessionId, resumeContext_1.resumeContextTurnLimit);
3524
+ const rendered = (0, resumeContext_1.formatResumeContext)(turns);
3525
+ // Complete one-shot block — post instantly, never via the dm draft
3526
+ // channel (its typing animation would "draw" this already-whole text).
3527
+ if (rendered)
3528
+ this.emit('output', key, rendered, { isComplete: true });
3529
+ }
3530
+ catch (e) {
3531
+ console.warn(`[Claude] resume context block failed:`, e instanceof Error ? e.message : e);
3532
+ }
3533
+ }
3534
+ }
3535
+ /**
3536
+ * @description Pick up tmux sessions that outlived the bot process.
3537
+ *
3538
+ * Called by `bot.ts` on startup, BEFORE `bot.launch()`. Scans
3539
+ * `tmux list-sessions` for names matching our `claude-<chatId>-<threadId>`
3540
+ * convention. For each match, returns the parsed key + tmux session name —
3541
+ * the bot then decides which ones to re-adopt (must have a live binding in
3542
+ * state.json) and which are orphans to garbage-collect (plan §10.2 / §13.19, E1).
3543
+ *
3544
+ * This method does NOT itself adopt sessions: it has no knowledge of state.json
3545
+ * or which sessions are still bound. The actual re-attach is done by the bot
3546
+ * calling {@link adoptExistingTmuxSession} for each key it wants to keep.
3547
+ *
3548
+ * Returns an empty array if tmux isn't installed or no matching sessions exist.
3549
+ */
3550
+ async listExistingTmuxSessions() {
3551
+ const raw = await (0, tmuxExec_1.tmuxAsync)('list-sessions', '-F', '#{session_name}');
3552
+ if (!raw)
3553
+ return [];
3554
+ const names = raw.split('\n').map(s => s.trim()).filter(Boolean);
3555
+ const result = [];
3556
+ for (const name of names) {
3557
+ const key = parseTmuxSessionName(name);
3558
+ if (key)
3559
+ result.push({ key, sessionName: name });
3560
+ }
3561
+ return result;
3562
+ }
3563
+ /**
3564
+ * @description Recover the `--session-id <uuid>` flag from a live tmux
3565
+ * session's start command.
3566
+ *
3567
+ * Used by the bot's reattach loop to reconcile state with reality when
3568
+ * `state.agents[key]` is missing a `claudeSessionId` (or names a
3569
+ * different adapter) but a `claude-<chatId>-<threadId>` tmux session is
3570
+ * still running. Previously such cases were treated as orphans and the
3571
+ * tmux session was killed, throwing away the user's live work.
3572
+ *
3573
+ * Implementation: read `pane_start_command` for the session's first
3574
+ * pane and parse the UUID out via {@link parseClaudeSessionIdFromCommand}.
3575
+ * `pane_start_command` survives across restarts because tmux keeps the
3576
+ * original command line for the pane.
3577
+ *
3578
+ * Returns `null` if no UUID can be recovered (caller falls back to
3579
+ * killing the session as an orphan).
3580
+ */
3581
+ async recoverSessionIdFromTmux(sessionName) {
3582
+ const sessions = await (0, tmuxExec_1.tmuxAsync)('list-sessions', '-F', '#{session_name}');
3583
+ if (!sessions.split('\n').includes(sessionName))
3584
+ return null;
3585
+ const cmd = await (0, tmuxExec_1.tmuxAsync)('display-message', '-p', '-t', (0, tmuxExec_1.getTmuxPaneTarget)(sessionName), '#{pane_start_command}');
3586
+ if (!cmd)
3587
+ return null;
3588
+ return parseClaudeSessionIdFromCommand(cmd);
3589
+ }
3590
+ /**
3591
+ * @description Adopt a tmux session that survived a bot restart.
3592
+ *
3593
+ * The bot calls this after `listExistingTmuxSessions()` for each
3594
+ * `(key, sessionName)` pair it wants to keep alive. Restores the in-memory
3595
+ * `ClaudeSession` and resumes polling so output flows back to Telegram.
3596
+ *
3597
+ * `workDir` and `claudeSessionId` come from state.json (the bot keeps a
3598
+ * binding `(key → subdir, claudeSessionId)`). If we ever lose them, the
3599
+ * caller should kill the tmux session as an orphan instead.
3600
+ *
3601
+ * Returns `true` on success, `false` if the tmux session disappeared between
3602
+ * the `list` call and now (race with manual `tmux kill-session`).
3603
+ */
3604
+ async adoptExistingTmuxSession(key, sessionName, workDir, claudeSessionId) {
3605
+ const sessions = await (0, tmuxExec_1.tmuxAsync)('list-sessions', '-F', '#{session_name}');
3606
+ if (!sessions.split('\n').includes(sessionName)) {
3607
+ console.log(`[Claude] adopt: tmux session ${sessionName} no longer exists`);
3608
+ return false;
3609
+ }
3610
+ // Audit S9 / #15: confirm the tmux session has a live child process,
3611
+ // not just an empty pane. With `remain-on-exit` semantics or a crashed
3612
+ // claude, a session can exist but produce no output forever; adopting
3613
+ // it would silently swallow further user input.
3614
+ const panesRaw = await (0, tmuxExec_1.tmuxAsync)('list-panes', '-t', (0, tmuxExec_1.getTmuxPaneTarget)(sessionName), '-F', '#{pane_pid}');
3615
+ const pids = panesRaw.split('\n').map(s => s.trim()).filter(s => /^\d+$/.test(s));
3616
+ const anyAlive = pids.some(pidStr => {
3617
+ try {
3618
+ process.kill(Number(pidStr), 0);
3619
+ return true;
3620
+ }
3621
+ catch {
3622
+ return false;
3623
+ }
3624
+ });
3625
+ if (!anyAlive) {
3626
+ console.log(`[Claude] adopt: ${sessionName} has no live child process, killing as zombie`);
3627
+ await (0, tmuxExec_1.tmuxAsync)('kill-session', '-t', (0, tmuxExec_1.getTmuxSessionTarget)(sessionName));
3628
+ return false;
3629
+ }
3630
+ const k = (0, sessionKey_1.keyToString)(key);
3631
+ // If we already have a tracked session for this key, leave it alone.
3632
+ if (this.sessions.has(k)) {
3633
+ console.log(`[Claude] adopt: already tracking ${k}, skipping`);
3634
+ return true;
3635
+ }
3636
+ // The account the surviving pane runs on is in its own start command; the bot compares it with the topic's pick.
3637
+ const startCommand = await (0, tmuxExec_1.tmuxAsync)('display-message', '-p', '-t', (0, tmuxExec_1.getTmuxPaneTarget)(sessionName), '#{pane_start_command}');
3638
+ const configDir = (0, claudeProfiles_1.parseClaudeConfigDirFromCommand)(startCommand ?? '');
3639
+ console.log(`[Claude] adopt: re-attaching to ${sessionName} in ${workDir} (configDir=${configDir ?? 'default'})`);
3640
+ const session = this.createSession({
3641
+ key,
3642
+ workDir,
3643
+ sessionName,
3644
+ claudeSessionId,
3645
+ configDir,
3646
+ isActive: true,
3647
+ handledAutoEnter: true, // don't try to auto-Enter on a session that's already past startup
3648
+ handledAutoAccept: true, // same — bypass-permissions was accepted on the original launch
3649
+ });
3650
+ // Seed `lastContent` with the current pane snapshot **before** the
3651
+ // first poll fires. Without this, the bot's first `pollOutput` after
3652
+ // re-adoption would diff a ~2000-line scrollback (hours of stale
3653
+ // conversation that survived the restart inside tmux) against `''`
3654
+ // — `getNewPaneContent('', x) === x` — and emit every line of it to
3655
+ // Telegram as if it were brand new output. Symptom: user restarts
3656
+ // the bot, types a fresh message in an existing thread, and the
3657
+ // thread gets flooded with answers from previous sessions before
3658
+ // the new answer arrives.
3659
+ //
3660
+ // The capture uses the SAME flags as `pollOutput` (`-e -S -2000`)
3661
+ // so the seed and the next poll's snapshot are produced by the
3662
+ // same code path; otherwise edge differences in ANSI handling or
3663
+ // scrollback depth would re-introduce phantom "new" lines on the
3664
+ // first diff. Best-effort — if `capture-pane` fails the seed stays
3665
+ // empty and we fall back to the pre-fix (noisy) behaviour, which
3666
+ // is still better than refusing to adopt.
3667
+ const initialRaw = await this.enqueueTmux(session, () => (0, tmuxExec_1.tmuxAsync)('capture-pane', '-t', (0, tmuxExec_1.getTmuxPaneTarget)(sessionName), '-p', '-e', '-S', '-2000'));
3668
+ if (initialRaw) {
3669
+ session.lastContent = (0, ansiClean_1.cleanOutput)(initialRaw);
3670
+ // S1: seed the raw baseline too so the first poll after adoption — which
3671
+ // captures the same idle pane — skips the redundant `cleanOutput`.
3672
+ session.lastRawCapture = initialRaw;
3673
+ // Seed the relay window too (live incident 2026-06-24): the
3674
+ // baseline above suppresses re-appearing PROSE, but a SETTLED table already
3675
+ // on the pane is deduped ONLY at the BLOCK level (`emitStabilizedTable`).
3676
+ // Without this seed the window is empty, so the first post-adopt poll
3677
+ // re-emits the whole table once per restart (~1100 chars). Recording the
3678
+ // pane + the settled table's own block signature makes that re-render hit
3679
+ // `checkBlockAlreadyRelayed === true` and be suppressed.
3680
+ (0, recentRelayWindow_1.seedRelayWindowFromPane)(session.recentRelayWindow, session.lastContent, getLastSharpTableBlock);
3681
+ }
3682
+ this.sessions.set(k, session);
3683
+ this.schedulePoll(key, session);
3684
+ this.emit('started', key);
3685
+ return true;
3686
+ }
3687
+ /**
3688
+ * @description Kill a tmux session by name without touching adapter state.
3689
+ *
3690
+ * The bot uses this on startup to garbage-collect orphan tmux sessions
3691
+ * (`claude-<chatId>-<threadId>` names with no corresponding binding in
3692
+ * state.json). See plan §10.2 / §13.19.
3693
+ */
3694
+ async killOrphanTmuxSession(sessionName) {
3695
+ console.log(`[Claude] kill orphan tmux session: ${sessionName}`);
3696
+ await (0, tmuxExec_1.tmuxAsync)('kill-session', '-t', (0, tmuxExec_1.getTmuxSessionTarget)(sessionName));
3697
+ }
3698
+ // Exposed for tests (see §11 Этап 7, R10): keeps the tmux-name parsing
3699
+ // logic unit-testable without instantiating the adapter (which would try to
3700
+ // auto-install claude on construction).
3701
+ static parseTmuxSessionName = parseTmuxSessionName;
3702
+ static buildTmuxSessionName = buildTmuxSessionName;
3703
+ async probeSessionAlive(session) {
3704
+ const sessions = await this.enqueueTmux(session, () => (0, tmuxExec_1.tmuxAsync)('list-sessions', '-F', '#{session_name}'));
3705
+ return sessions.split('\n').includes(session.sessionName);
3706
+ }
3707
+ async pollOutput(key) {
3708
+ const session = this.sessions.get((0, sessionKey_1.keyToString)(key));
3709
+ if (!session?.isActive)
3710
+ return;
3711
+ // Audit S9 / #38: a burst of more than 200 lines between two polls
3712
+ // would slide off the top of the capture window before the next poll
3713
+ // saw it, breaking the diff and causing duplicate "new" emissions.
3714
+ // 2000 lines comfortably covers a long Claude tool-call block; we
3715
+ // still diff against `session.lastContent` in memory so the larger
3716
+ // capture doesn't grow the output we send to Telegram.
3717
+ const raw = await this.enqueueTmux(session, () => (0, tmuxExec_1.tmuxAsync)('capture-pane', '-t', (0, tmuxExec_1.getTmuxPaneTarget)(session.sessionName), '-p', '-e', '-S', '-2000'));
3718
+ if (!session.isActive)
3719
+ return;
3720
+ if (!raw) {
3721
+ const alive = await this.probeSessionAlive(session);
3722
+ if (!session.isActive)
3723
+ return;
3724
+ if (!alive) {
3725
+ console.log(`[Claude] Session died, cleaning up`);
3726
+ await this.stopSessionInternal(key);
3727
+ this.emit('closed', key);
3728
+ }
3729
+ return;
3730
+ }
3731
+ // Pane-RESIZE guard (live incident 2026-07-02): an
3732
+ // interactive `tmux attach` resizes the window to the client terminal (and
3733
+ // detach restores the `-x/-y` default); tmux re-wraps the WHOLE scrollback
3734
+ // at the new width, so the line-SET diff below would see ~every line as
3735
+ // "new" and relay ragged fragments of OLD conversation (the relay window
3736
+ // can't match re-wrapped lines, and its short-line exemption lets clusters
3737
+ // of short old lines through on every flap). Query the size AFTER the
3738
+ // capture (same-poll order is what makes the race-free direction hold: a
3739
+ // resize landing between the two calls is detected on THIS poll, before
3740
+ // its giant diff ships) and, on a change, swallow the repaint — advance
3741
+ // the baseline, emit nothing — until the capture settles. Decision logic
3742
+ // is pure: `utils/paneResizeGuard.ts`.
3743
+ const paneSizeRaw = await this.enqueueTmux(session, () => (0, tmuxExec_1.tmuxAsync)('display-message', '-p', '-t', (0, tmuxExec_1.getTmuxPaneTarget)(session.sessionName), '#{pane_width}x#{pane_height}'));
3744
+ if (!session.isActive)
3745
+ return;
3746
+ const currentPaneSize = (0, paneResizeGuard_1.parsePaneSize)(paneSizeRaw);
3747
+ const resizeDecision = (0, paneResizeGuard_1.getPaneResizeGuardDecision)({
3748
+ lastSize: session.lastPaneSize,
3749
+ currentSize: currentPaneSize,
3750
+ isSettling: session.isResizeSettling,
3751
+ settlePolls: session.resizeSettlePolls,
3752
+ isRawChanged: raw !== session.lastRawCapture,
3753
+ });
3754
+ if (currentPaneSize !== null && session.lastPaneSize !== null && currentPaneSize !== session.lastPaneSize) {
3755
+ console.log(`[Claude] pane resized (${session.lastPaneSize} → ${currentPaneSize}) — suppressing scrape, reseeding baseline`);
3756
+ }
3757
+ if (currentPaneSize !== null)
3758
+ session.lastPaneSize = currentPaneSize;
3759
+ if (session.isResizeSettling && !resizeDecision.nextIsSettling) {
3760
+ console.log(`[Claude] pane resize settled after ${session.resizeSettlePolls} suppressed polls`);
3761
+ }
3762
+ session.isResizeSettling = resizeDecision.nextIsSettling;
3763
+ session.resizeSettlePolls = resizeDecision.nextSettlePolls;
3764
+ if (resizeDecision.action === 'suppress') {
3765
+ if (raw !== session.lastRawCapture) {
3766
+ session.lastRawCapture = raw;
3767
+ session.lastContent = (0, ansiClean_1.cleanOutput)(raw);
3768
+ // The pane genuinely changed — keep the 30s pane-static liveness net honest.
3769
+ session.lastContentChangeAt = Date.now();
3770
+ }
3771
+ return;
3772
+ }
3773
+ // S1: when the raw capture is byte-identical to the previous poll's, the
3774
+ // pane did not change, so the cleaned content is identical too — skip the
3775
+ // ~15-regex `cleanOutput` pass and reuse the cached result. Runs AFTER the
3776
+ // empty-raw death probe above (an empty capture short-circuits there and
3777
+ // never reaches the skip). All downstream branches behave bit-for-bit the
3778
+ // same: the resume-seeding "unchanged across 2 polls" exit still sees its
3779
+ // no-change signal, and the `content !== lastContent` compare still no-ops.
3780
+ const isRawChanged = raw !== session.lastRawCapture;
3781
+ let content;
3782
+ if (isRawChanged) {
3783
+ session.lastRawCapture = raw;
3784
+ // Pane changed → reset the idle clock the bot's 30s pane-static net reads.
3785
+ session.lastContentChangeAt = Date.now();
3786
+ content = (0, ansiClean_1.cleanOutput)(raw);
3787
+ }
3788
+ else {
3789
+ content = session.lastContent;
3790
+ }
3791
+ // S2: grow the poll delay while the pane stays idle, snap back to base on
3792
+ // any change. A change always resets the cadence so streaming latency is
3793
+ // unaffected; only no-activity panes slow down (toward maxPollIntervalMs).
3794
+ const nextCadence = (0, pollBackoff_1.getNextPollDelay)({
3795
+ isChanged: isRawChanged,
3796
+ currentDelayMs: session.currentPollDelayMs,
3797
+ unchangedStreak: session.unchangedPollStreak,
3798
+ });
3799
+ session.currentPollDelayMs = nextCadence.delayMs;
3800
+ session.unchangedPollStreak = nextCadence.unchangedStreak;
3801
+ // Resume flood-suppression: while seeding, Claude is repainting the whole
3802
+ // restored transcript into the pane. We advance the baseline every poll but
3803
+ // emit NO conversation text, so once the pane stops growing the baseline
3804
+ // equals the full restored transcript and later diffs are genuine new
3805
+ // output. This runs on EVERY poll (even when `content === lastContent`),
3806
+ // because "unchanged across 2 polls" — the exit signal — is exactly the
3807
+ // no-change case. Lifecycle auto-handling KEEPS running (suppress text, not
3808
+ // the "Press Enter" / bypass-permissions prompts a resume may re-show).
3809
+ if (session.resumeSeeding) {
3810
+ const decision = getResumeSeedDecision({
3811
+ content,
3812
+ prevContent: session.resumeSeedPrevContent,
3813
+ polls: session.resumeSeedPolls,
3814
+ });
3815
+ session.lastContent = content;
3816
+ session.resumeSeedPrevContent = content;
3817
+ session.resumeSeedPolls += 1;
3818
+ this.handleAutoLifecycle(session, content, (0, paneDiff_1.getNewPaneContent)('', content).text);
3819
+ if (!decision.keepSeeding) {
3820
+ session.resumeSeeding = false;
3821
+ // The relay window must start EMPTY here: seeding SWALLOWED the
3822
+ // restored transcript (it was never relayed this lifetime), and the
3823
+ // window only ever suppresses content it actually relayed. Seeding
3824
+ // emits no output so nothing was recorded — the reset keeps that
3825
+ // invariant explicit rather than incidental.
3826
+ session.recentRelayWindow.reset();
3827
+ // Same invariant for the table stabilizer: seeding swallowed any
3828
+ // restored table, so nothing should carry over into the live phase.
3829
+ session.streamingTable = null;
3830
+ console.log(`[Claude] resume seeding done after ${session.resumeSeedPolls} polls`);
3831
+ }
3832
+ return;
3833
+ }
3834
+ // S7: re-apply the thread's stored /effort the FIRST poll the TUI input box
3835
+ // is up (and no boot gate left). Runs every non-seeding poll, incl. unchanged
3836
+ // ones — the box can appear on a poll whose content didn't otherwise change.
3837
+ // Cheap early-out below when nothing is armed; `consumePendingEffortReapply`
3838
+ // clears the flag so it fires exactly once. Must precede any buffered-prompt
3839
+ // replay, which the bot only starts after the input box exists — they share
3840
+ // the serial tmux queue, and this enqueues first.
3841
+ if (session.pendingEffortReapply && checkIsClaudePromptReady(content)) {
3842
+ this.consumePendingEffortReapply(session);
3843
+ }
3844
+ // S7 seen-watermark advance: on EVERY ready poll whose transcript EOF grew
3845
+ // (busy OR idle) move the persisted watermark to the current EOF. Because a
3846
+ // live Claude adapter relays the pane on every poll, growth means "relayed
3847
+ // up to here" mid-turn — so the watermark tracks what the user already saw
3848
+ // instead of stranding at the previous turn end, which made a mid-turn
3849
+ // restart re-count the whole in-flight turn as a false "missed N" (live
3850
+ // 2026-07-04). Cheap: two regex tests every poll; the fs stat + write fire
3851
+ // only on a ready poll, and the write only when EOF actually grew. Lets a
3852
+ // later bot restart count only the assistant turns it genuinely missed
3853
+ // during downtime (the reattach recap).
3854
+ const isBusy = checkIsClaudeSessionBusy({ isActive: session.isActive, lastContent: content });
3855
+ this.advanceClaudeWatermarkIfGrown(key, session, isBusy, checkIsClaudePromptReady(content));
3856
+ // S1 idle-poll table flush: the change branch below never runs on a
3857
+ // byte-unchanged poll, so a table that is the LAST thing painted before the
3858
+ // pane freezes is flushed here instead (never silently drop it). The flush
3859
+ // is gated on the SAME done-signals as the change branch — real prose after
3860
+ // the table OR the turn going idle — so a mid-stream PAUSE (the pane briefly
3861
+ // frozen at an intermediate row count while still busy) is held, not emitted
3862
+ // (live incident 2026-06-15). A frozen-but-busy table rides the held-poll
3863
+ // safety cap; a frozen idle table emits via the idle signal.
3864
+ if (content === session.lastContent && session.streamingTable) {
3865
+ const idleDecision = getTableStabilizationDecision({
3866
+ currentTable: getLastSharpTableBlock(content),
3867
+ streamingTable: session.streamingTable,
3868
+ hasContentAfterTable: checkHasContentAfterLastSharpTable(content),
3869
+ isTurnIdle: !checkIsClaudeSessionBusy({ isActive: session.isActive, lastContent: content }),
3870
+ });
3871
+ session.streamingTable = idleDecision.nextStreamingTable;
3872
+ if (idleDecision.kind === 'emit' && idleDecision.block) {
3873
+ this.emitStabilizedTable(key, session, idleDecision.block);
3874
+ }
3875
+ }
3876
+ if (content !== session.lastContent) {
3877
+ const previousPane = session.lastContent;
3878
+ const diff = (0, paneDiff_1.getNewPaneContent)(previousPane, content);
3879
+ const startsNewParagraph = diff.startsNewParagraph;
3880
+ session.lastContent = content;
3881
+ // S1 streaming-table stabilizer (live incident 2026-06-11): a wide
3882
+ // markdown table re-flows its column widths as longer cells stream in, so
3883
+ // every layout is byte-distinct and the line-SET diff marks the whole
3884
+ // table "new" each poll — it would ship once per layout (empty → 1 row →
3885
+ // full), the final full frame often dropped under the coalescer's
3886
+ // debounce / a 429. Operate on the FULL live `content` (not the diff):
3887
+ // hold a still-laying-out table (masking its lines out of THIS poll's
3888
+ // delta so no intermediate frame ships), and emit the complete table only
3889
+ // when it is DONE — real prose now follows it, or the turn went idle, or
3890
+ // the safety cap fired (NOT on a mid-stream pause; live incident
3891
+ // 2026-06-15). Runs BEFORE the normal emit so a done table is its own
3892
+ // fenced block, ahead of any trailing prose in this same delta.
3893
+ const tableDecision = getTableStabilizationDecision({
3894
+ currentTable: getLastSharpTableBlock(content),
3895
+ streamingTable: session.streamingTable,
3896
+ hasContentAfterTable: checkHasContentAfterLastSharpTable(content),
3897
+ isTurnIdle: !checkIsClaudeSessionBusy({ isActive: session.isActive, lastContent: content }),
3898
+ });
3899
+ session.streamingTable = tableDecision.nextStreamingTable;
3900
+ // While a table is held OR being emitted separately, its lines must not
3901
+ // also ride in this poll's prose delta — mask them out either way.
3902
+ let newPart = diff.text;
3903
+ if (tableDecision.kind !== 'none') {
3904
+ newPart = maskSharpTableLines(newPart);
3905
+ }
3906
+ if (tableDecision.kind === 'emit' && tableDecision.block) {
3907
+ this.emitStabilizedTable(key, session, tableDecision.block);
3908
+ }
3909
+ if (newPart) {
3910
+ // S4: full body only when explicitly debugging the scrape pipeline;
3911
+ // default is a one-line size summary (the sync stdout write of up to
3912
+ // ~600KB per changed poll was starving the event loop in prod).
3913
+ console.log(isClaudeScrapeDebugEnabled
3914
+ ? `[Claude] RAW output (${newPart.length}):\n---\n${newPart}\n---`
3915
+ : `[Claude] RAW output (${newPart.length} chars)`);
3916
+ // Interactive question/choice prompts are detected on the FULL pane
3917
+ // (not this diff): moving the `❯` cursor repaints only the changed
3918
+ // option lines, so a signature from the diff would be a partial
3919
+ // option set and the de-dup would re-spam the thread on every
3920
+ // keystroke. Reading the whole option group keeps the signature
3921
+ // stable across cursor moves. Deliver once as durable output — not a
3922
+ // transient status frame that gets deleted — and suppress repaints.
3923
+ const question = extractClaudeQuestion(content);
3924
+ if (question) {
3925
+ // A real AskUserQuestion selector takes PRECEDENCE over a survey: a
3926
+ // digit reply must drive the selector, not be eaten by a stale
3927
+ // survey signal. Clear the survey de-dup so it can't shadow the
3928
+ // question's digit routing.
3929
+ session.lastSurveySignature = '';
3930
+ // Detected → reset the absence counter (any prior transient null was a
3931
+ // flicker, not a resolve).
3932
+ session.questionAbsentPolls = 0;
3933
+ if (question.signature !== session.lastQuestionSignature) {
3934
+ session.lastQuestionSignature = question.signature;
3935
+ session.lastStatusText = '';
3936
+ session.openToolKind = null; // a question ends any prior tool output
3937
+ // `isQuestion`: the bot sends this as its OWN pinnable message and
3938
+ // pins it so the muted topic fires a notification.
3939
+ this.emit('output', key, `${question.text}\n\n${this.tl(key, () => (0, i18n_1.t)('agent.question_hint'))}`, { isQuestion: true });
3940
+ }
3941
+ }
3942
+ else {
3943
+ // No selector THIS diff. While a question is pending, a single miss is
3944
+ // usually the selector mid-repaint, NOT a resolve — debounce so we
3945
+ // don't fire `questionGone` (premature unpin) then re-emit a duplicate
3946
+ // question + notification next poll once it re-appears. Only resolve
3947
+ // after it's been absent for `questionAbsentPollThreshold` consecutive
3948
+ // diffs; clearing the signature only THEN (so an identical question
3949
+ // asked again later is still delivered, not swallowed).
3950
+ if (session.lastQuestionSignature) {
3951
+ session.questionAbsentPolls += 1;
3952
+ if (session.questionAbsentPolls >= questionAbsentPollThreshold) {
3953
+ this.emit('questionGone', key);
3954
+ session.lastQuestionSignature = '';
3955
+ session.questionAbsentPolls = 0;
3956
+ }
3957
+ }
3958
+ else {
3959
+ session.questionAbsentPolls = 0;
3960
+ }
3961
+ // S4: AUTO-DISMISS the native Claude session-feedback survey (the
3962
+ // periodic bare-digit prompt — either known header wording, see
3963
+ // CLAUDE_SURVEY_HEADER_REGEX). It lands on the pane mid-turn,
3964
+ // occupies the input box and disrupts the scrape (it contributed to
3965
+ // the "status never appeared" hang, live 2026-06-29; an UNDETECTED
3966
+ // wording swallowed the next prompt's Enter and hung the topic, live
3967
+ // 2026-07-02). Never surface it to the topic — send a one-shot Escape
3968
+ // to clear it. The signature (header + option digits/labels, no
3969
+ // volatile chrome) is stable across the survey's per-poll repaints, so
3970
+ // we dismiss ONCE per appearance; cleared when it leaves the pane so a
3971
+ // genuinely new survey later is dismissed again. While a survey is on
3972
+ // the pane its header/option lines are ALSO stripped from the relay
3973
+ // chunk below (gated on this `survey`), so the dismissed survey's
3974
+ // chrome never leaks into the topic as a stray prose message.
3975
+ const survey = extractClaudeSurvey(content);
3976
+ if (survey) {
3977
+ if (checkShouldDismissSurvey(survey.signature, session.lastSurveySignature)) {
3978
+ session.lastSurveySignature = survey.signature;
3979
+ this.dismissClaudeSurvey(session);
3980
+ }
3981
+ }
3982
+ else {
3983
+ session.lastSurveySignature = '';
3984
+ }
3985
+ // Long-horizon re-render dedup (live incident 2026-06-10, plan
3986
+ // 2026-06-10-claude-stale-rescrape-dedup): the per-poll diff above
3987
+ // only knows the PREVIOUS capture, so when the TUI re-renders
3988
+ // hours-old scrollback (full repaint / scroll-through / resize) the
3989
+ // redrawn lines arrive here as "new" and would be re-relayed as a
3990
+ // duplicate flood. Drop every line already relayed to this topic;
3991
+ // when nothing survives, skip the poll's emit entirely.
3992
+ let relayablePart = (0, recentRelayWindow_1.getRelayDedupedChunk)(session.recentRelayWindow, newPart);
3993
+ if (relayablePart && survey) {
3994
+ // S4: a survey is on the pane this poll — drop its header/option
3995
+ // lines so the auto-dismissed survey's chrome never leaks (it would
3996
+ // otherwise classify as prose). Gated on a detected `survey` so a
3997
+ // legitimate "1: x 2: y" list in normal output is never touched.
3998
+ relayablePart = stripSurveyChromeLines(relayablePart);
3999
+ }
4000
+ if (!relayablePart) {
4001
+ console.log(`[Claude] already-relayed re-render suppressed (${newPart.length} chars)`);
4002
+ }
4003
+ else {
4004
+ // S4 verbosity relay: classify the chunk (pure, cheap), then route
4005
+ // its tool / panel segments per the thread's `/tool_results` pref.
4006
+ // The classifier threads fence/block context across polls (mirrors
4007
+ // `session.openToolKind` for orphan bodies whose header streamed in
4008
+ // an earlier poll). FAST PATH (regression anchor): all-`full`
4009
+ // tool+thinking prefs AND no panel-preview segment → the pre-S4
4010
+ // direct strip→emit runs BYTE-IDENTICALLY on the original chunk.
4011
+ const prefs = this.getDisplayPrefs(key);
4012
+ const classification = (0, claudeChunkClassifier_1.classifyClaudeChunk)(relayablePart, session.chunkContext);
4013
+ session.chunkContext = classification.outgoingContext;
4014
+ const isFastPath = (0, claudeRelayRouting_1.checkIsClaudeRelayFastPath)(classification.segments, prefs.toolResults, prefs.thinking);
4015
+ let stripInput;
4016
+ let activityLine = null;
4017
+ if (isFastPath) {
4018
+ // Pre-S4 path, unchanged: feed the ORIGINAL chunk to the stripper
4019
+ // (it is the chrome/fence backstop). Provably byte-identical.
4020
+ stripInput = relayablePart;
4021
+ }
4022
+ else {
4023
+ // QUIET PATH: keep prose + thinking verbatim, route tool bodies
4024
+ // (full keep / short truncate / minimal fold) and ALWAYS fold a
4025
+ // sub-agent panel preview into the rolling status frame.
4026
+ const routed = this.tl(key, () => (0, claudeRelayRouting_1.routeClaudeChunkSegments)(classification.segments, prefs.toolResults, prefs.thinking, (toolLabel) => (0, i18n_1.t)('toolResults.activity_status', { tool: toolLabel || (0, i18n_1.t)('toolResults.activity_fallback') }), () => (0, i18n_1.t)('subagent.panel_fold_status'), () => (0, i18n_1.t)('thinking.live'), (seconds) => (0, i18n_1.t)('thinking.thoughtForSeconds', { seconds }), (0, i18n_1.t)('toolResults.truncated_footer')));
4027
+ stripInput = routed.keptText;
4028
+ activityLine = routed.activityLine;
4029
+ }
4030
+ // Thread the tool-result kind across polls: the owning `● Bash(…)`
4031
+ // header of a slow command's output streamed in an earlier poll (the
4032
+ // line-set diff drops it as a duplicate), so `session.openToolKind`
4033
+ // carries it forward to fence the orphan `⎿` body (B2). Tracked via
4034
+ // the clean deltas, not a scan of the racy live pane.
4035
+ const stripped = stripTuiElementsWithContext(stripInput, session.openToolKind);
4036
+ session.openToolKind = stripped.toolKind;
4037
+ const cleanedOutput = stripped.text;
4038
+ // B7 shape gate (the `❯` input-box draft) FIRST; then the S1
4039
+ // content gate for the no-`❯` echo variants (preamble / wrapped
4040
+ // continuation / voice-transcript tail), armed only briefly after a
4041
+ // send so a later real reply that repeats prompt words is never eaten.
4042
+ const isShapeEcho = checkIsInputEchoFrame(cleanedOutput);
4043
+ const isContentEcho = !isShapeEcho &&
4044
+ Date.now() - session.lastForwardedTextAt < claudeInputEchoContentWindowMs &&
4045
+ checkIsForwardedEcho(cleanedOutput, session.lastForwardedText);
4046
+ if (cleanedOutput && (isShapeEcho || isContentEcho)) {
4047
+ // The frame is just Claude echoing the user's own typed input —
4048
+ // relaying it reads as a ghost message in the topic.
4049
+ console.log(`[Claude] input-echo frame filtered${isContentEcho ? ' (content match)' : ''}`);
4050
+ }
4051
+ else if (cleanedOutput) {
4052
+ // S4: gated full body (see the RAW dump above for the rationale).
4053
+ console.log(isClaudeScrapeDebugEnabled
4054
+ ? `[Claude] FILTERED output (${cleanedOutput.length}):\n---\n${cleanedOutput}\n---`
4055
+ : `[Claude] FILTERED output (${cleanedOutput.length} chars)`);
4056
+ if (checkIsStatusOutput(cleanedOutput)) {
4057
+ // S6 (c): when this same poll ALSO folded a verbosity activity
4058
+ // (🔧 tool / 🤖 sub-agent / ☁️ thinking), that routed line is the
4059
+ // INTENTIONAL minimal-mode indicator — prefer it over the
4060
+ // incidental transient so the rolling status reads cleanly
4061
+ // (the transient must not shadow the routed activity). Real
4062
+ // permanent output is the `else` below and still always wins.
4063
+ const statusText = activityLine ?? cleanedOutput;
4064
+ // Deduplicate spinner updates: normalize spinner character and compare
4065
+ const dedupKey = activityLine ?? cleanedOutput.replace(/^[✻✽✶✢·*●○⏺]\s*/gm, '');
4066
+ if (dedupKey !== session.lastStatusText) {
4067
+ session.lastStatusText = dedupKey;
4068
+ this.emit('status', key, statusText);
4069
+ }
4070
+ }
4071
+ else {
4072
+ session.lastStatusText = '';
4073
+ // Record in the PANE-line domain (pre-strip): a future
4074
+ // re-render arrives as pane lines, so the window must store
4075
+ // the same shape the filter above queries. Permanent output
4076
+ // only — status frames roll/are edited in place and never
4077
+ // flood as separate messages.
4078
+ session.recentRelayWindow.record(relayablePart);
4079
+ // Carry the dropped leading paragraph break out-of-band (S2): the
4080
+ // bot rebuilds the `\n\n` separator only when APPENDING this prose
4081
+ // chunk to the pending buffer / live draft — never at a message
4082
+ // start. Out-of-band so the pipeline's trims can't strip it.
4083
+ this.emit('output', key, cleanedOutput, { startsNewParagraph });
4084
+ }
4085
+ }
4086
+ else if (activityLine) {
4087
+ // Quiet path folded every routable segment into the status frame
4088
+ // (e.g. `minimal` tool calls / a sub-agent panel preview) — keep
4089
+ // the user informed with one rolling activity line, deduped like
4090
+ // the spinner-status path above so the frame doesn't churn.
4091
+ if (activityLine !== session.lastStatusText) {
4092
+ session.lastStatusText = activityLine;
4093
+ this.emit('status', key, activityLine);
4094
+ }
4095
+ }
4096
+ else {
4097
+ console.log(`[Claude] Output filtered out completely`);
4098
+ }
4099
+ }
4100
+ }
4101
+ }
4102
+ this.handleAutoLifecycle(session, content, newPart);
4103
+ }
4104
+ }
4105
+ /**
4106
+ * @description Emit a settled streaming table (S1) as ONE fenced `output`.
4107
+ *
4108
+ * Routes the held block through the SAME fence path as the inline table path
4109
+ * ({@link stripTuiElementsWithContext} fences a sharp-corner table) so the
4110
+ * `<pre>` rendering is byte-identical to today — only the TIMING/dedup change.
4111
+ * Records the raw block in {@link RecentRelayWindow} in the PANE-line domain
4112
+ * (pre-strip) so a later full repaint of the same table is suppressed by the
4113
+ * existing re-render dedup, exactly like the normal permanent-output path.
4114
+ * Emitted with `startsNewParagraph: true` — a table is its own visual block,
4115
+ * so it must not glue onto preceding prose in the append/draft join.
4116
+ */
4117
+ emitStabilizedTable(key, session, block) {
4118
+ // Root-cause dedup (flood 2026-06-16): the table block was RECORDED into the
4119
+ // relay window but never CHECKED before emit, and table lines are masked out
4120
+ // of the prose delta before `getRelayDedupedChunk` runs — so the long-horizon
4121
+ // re-render guard structurally never saw a re-printed/looped table. Each
4122
+ // re-render → one new message (a single topic flooded with ~500 byte-identical
4123
+ // copies). Check the window at the BLOCK level (border rows / tiny cells fall
4124
+ // under the per-line gate, so a per-line check would miss them) in the SAME
4125
+ // pre-strip pane-line domain `record` uses. An identical block → skip the
4126
+ // emit (suppressed exactly like a re-rendered prose chunk); a CHANGED table is
4127
+ // a different signature → still emits. Still record() to keep the window warm.
4128
+ const isAlreadyRelayed = session.recentRelayWindow.checkBlockAlreadyRelayed(block);
4129
+ const fenced = stripTuiElementsWithContext(block, session.openToolKind);
4130
+ session.openToolKind = fenced.toolKind;
4131
+ if (!fenced.text)
4132
+ return;
4133
+ session.recentRelayWindow.record(block);
4134
+ if (isAlreadyRelayed) {
4135
+ console.log(`[Claude] already-relayed table re-render suppressed (${block.length} chars)`);
4136
+ return;
4137
+ }
4138
+ session.lastStatusText = '';
4139
+ this.emit('output', key, fenced.text, { startsNewParagraph: true });
4140
+ }
4141
+ /**
4142
+ * @description Auto-handle the two lifecycle prompts Claude can show — the
4143
+ * "Press Enter to continue" gate and the bypass-permissions warning — by
4144
+ * issuing the matching keystrokes. Extracted from {@link pollOutput} so the
4145
+ * resume-seeding path (which suppresses conversation TEXT) can still drive
4146
+ * these prompts; suppression must hide chatter, not the lifecycle.
4147
+ *
4148
+ * Also the detection seam for a terminal provider error / logged-out row
4149
+ * ({@link getClaudeAgentErrorLine}: `API Error: …`, `⎿ … · API Error: …`, or
4150
+ * `⎿ … Please run /login`): when it first classifies, emit one `apiError`
4151
+ * event — the trigger for the auto-retry (transient / usageLimit) OR the
4152
+ * surfaced logged-out notice (auth). Detection scans the NEW pane delta only
4153
+ * (never the full pane / arbitrary conversation text), so a recognized error
4154
+ * row fires exactly once — when it first renders.
4155
+ *
4156
+ * A large new chunk (`newPart.length > 50`) means real conversation moved on,
4157
+ * so the one-shot `handled*` guards are reset to re-arm for a later prompt.
4158
+ */
4159
+ handleAutoLifecycle(session, content, newPart) {
4160
+ if (newPart.length > 50) {
4161
+ session.handledAutoEnter = false;
4162
+ session.handledAutoAccept = false;
4163
+ session.handledApiError = false;
4164
+ }
4165
+ if (!session.handledApiError) {
4166
+ // Scan the NEW delta, NOT the full pane: a logged-out `⎿ … /login` (or
4167
+ // `API Error:`) row lingers in the scrollback long after the user re-logs
4168
+ // in, and the guard above re-arms on any >50-char redraw during the login
4169
+ // flow — scanning `content` re-fired `apiError('auth')` on every poll, so
4170
+ // the recovery clear (first real output) and this re-fire oscillated:
4171
+ // re-pinning "logged out, run /login" AFTER a successful login and burying
4172
+ // the real answer (live 2026-07-03). The line-SET pane diff
4173
+ // (getNewPaneContent) puts the row in `newPart` only on its FIRST
4174
+ // appearance → one fire per genuine logout episode. The resume-seed caller
4175
+ // passes the full pane AS newPart, so an adopted logged-out session still
4176
+ // surfaces once.
4177
+ const apiErrorLine = getClaudeAgentErrorLine(newPart);
4178
+ if (apiErrorLine) {
4179
+ const apiError = (0, apiErrorRetry_1.classifyAgentApiError)(apiErrorLine, Date.now());
4180
+ if (apiError) {
4181
+ session.handledApiError = true;
4182
+ console.log(`[Claude] API error detected (${apiError.kind}): ${apiErrorLine.trim()}`);
4183
+ this.emit('apiError', session.key, apiError);
4184
+ }
4185
+ }
4186
+ }
4187
+ if (!session.handledAutoEnter && this.checkNeedsAutoEnter(content)) {
4188
+ session.handledAutoEnter = true;
4189
+ console.log(`[Claude] Auto-pressing Enter`);
4190
+ session.autoEnterTimer = setTimeout(() => {
4191
+ session.autoEnterTimer = null;
4192
+ if (!session.isActive)
4193
+ return;
4194
+ this.enqueueTmuxBestEffort(session, async () => {
4195
+ if (!session.isActive)
4196
+ return '';
4197
+ return (0, tmuxExec_1.tmuxAsync)('send-keys', '-t', (0, tmuxExec_1.getTmuxPaneTarget)(session.sessionName), 'Enter');
4198
+ });
4199
+ }, 300);
4200
+ }
4201
+ if (!session.handledAutoAccept && this.checkNeedsAutoAccept(content)) {
4202
+ session.handledAutoAccept = true;
4203
+ console.log(`[Claude] Auto-accepting bypass permissions`);
4204
+ session.autoAcceptOuterTimer = setTimeout(() => {
4205
+ session.autoAcceptOuterTimer = null;
4206
+ if (!session.isActive)
4207
+ return;
4208
+ this.enqueueTmuxBestEffort(session, async () => {
4209
+ if (!session.isActive)
4210
+ return '';
4211
+ return (0, tmuxExec_1.tmuxAsync)('send-keys', '-t', (0, tmuxExec_1.getTmuxPaneTarget)(session.sessionName), 'Down');
4212
+ });
4213
+ session.autoAcceptInnerTimer = setTimeout(() => {
4214
+ session.autoAcceptInnerTimer = null;
4215
+ if (!session.isActive)
4216
+ return;
4217
+ this.enqueueTmuxBestEffort(session, async () => {
4218
+ if (!session.isActive)
4219
+ return '';
4220
+ return (0, tmuxExec_1.tmuxAsync)('send-keys', '-t', (0, tmuxExec_1.getTmuxPaneTarget)(session.sessionName), 'Enter');
4221
+ });
4222
+ }, 100);
4223
+ }, 300);
4224
+ }
4225
+ }
4226
+ checkNeedsAutoEnter(content) {
4227
+ return CLAUDE_AUTO_ENTER_PATTERNS.some(pattern => pattern.test(content));
4228
+ }
4229
+ checkNeedsAutoAccept(content) {
4230
+ const hasWarning = CLAUDE_BYPASS_WARNING_RE.test(content);
4231
+ const hasAccept = CLAUDE_BYPASS_ACCEPT_RE.test(content);
4232
+ if (hasWarning || hasAccept) {
4233
+ console.log(`[Claude] checkNeedsAutoAccept: warning=${hasWarning}, accept=${hasAccept}`);
4234
+ }
4235
+ return hasWarning && hasAccept;
4236
+ }
4237
+ }
4238
+ exports.ClaudeCliAdapter = ClaudeCliAdapter;
4239
+ //# sourceMappingURL=claudeCliAdapter.js.map