@qwen-code/qwen-code 0.19.3 → 0.19.4-nightly.20260702.46814e4f1

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 (308) hide show
  1. package/bundled/loop/SKILL.md +22 -3
  2. package/bundled/loop/SKILL.test.ts +20 -0
  3. package/bundled/loop/autonomous-loop.test.ts +59 -0
  4. package/bundled/loop/autonomous-loop.ts +102 -0
  5. package/bundled/loop/loop-task-file.test.ts +1064 -0
  6. package/bundled/loop/loop-task-file.ts +404 -0
  7. package/bundled/loop/loop-tick-resolver.test.ts +906 -0
  8. package/bundled/loop/loop-tick-resolver.ts +416 -0
  9. package/bundled/qc-helper/SKILL.md +12 -5
  10. package/bundled/qc-helper/docs/configuration/auth.md +1 -1
  11. package/bundled/qc-helper/docs/configuration/settings.md +62 -32
  12. package/bundled/qc-helper/docs/features/approval-mode.md +3 -0
  13. package/bundled/qc-helper/docs/features/auto-mode.md +40 -0
  14. package/bundled/qc-helper/docs/features/channels/overview.md +81 -18
  15. package/bundled/qc-helper/docs/features/channels/plugins.md +1 -1
  16. package/bundled/qc-helper/docs/features/code-review.md +17 -61
  17. package/bundled/qc-helper/docs/features/commands.md +42 -40
  18. package/bundled/qc-helper/docs/features/headless.md +19 -18
  19. package/bundled/qc-helper/docs/features/hooks.md +6 -1
  20. package/bundled/qc-helper/docs/features/mcp.md +3 -1
  21. package/bundled/qc-helper/docs/features/memory.md +1 -1
  22. package/bundled/qc-helper/docs/features/scheduled-tasks.md +10 -0
  23. package/bundled/qc-helper/docs/features/status-line.md +3 -3
  24. package/bundled/qc-helper/docs/ide-integration/ide-integration.md +4 -4
  25. package/bundled/qc-helper/docs/qwen-serve.md +53 -0
  26. package/bundled/qc-helper/docs/support/troubleshooting.md +11 -0
  27. package/bundled/review/DESIGN.md +21 -26
  28. package/bundled/review/SKILL.md +45 -129
  29. package/chunks/MaxSizedBox-GHGRQ2OS.js +69 -0
  30. package/chunks/{StandaloneSessionPicker-Z52JTYST.js → StandaloneSessionPicker-G7YTTM27.js} +49 -42
  31. package/chunks/{acpAgent-7RZUOWOM.js → acpAgent-SDOILBSU.js} +1325 -365
  32. package/chunks/{agent-4FG7WTHJ.js → agent-NTSXGV4L.js} +31 -25
  33. package/chunks/agent-headless-WYURMSAB.js +62 -0
  34. package/chunks/{anthropicContentGenerator-PFITVL7Y.js → anthropicContentGenerator-WL3LJOUB.js} +110 -31
  35. package/chunks/{artifact-tool-D2X7G3C3.js → artifact-tool-W76WC2JX.js} +3 -3
  36. package/chunks/{askUserQuestion-UGETICJQ.js → askUserQuestion-J3P6TH3V.js} +2 -2
  37. package/chunks/bridge-AMOL4W4P.js +73 -0
  38. package/chunks/{ca-IUFWESZL.js → ca-HCMB2I54.js} +10 -9
  39. package/chunks/channel-worker-supervisor-C3IB63V7.js +659 -0
  40. package/chunks/{chunk-STUWFKGD.js → chunk-26JIREA5.js} +466 -543
  41. package/chunks/{chunk-SLNKLLEO.js → chunk-2MRV62AU.js} +37 -8
  42. package/chunks/{chunk-ODLIHNOY.js → chunk-2U4HUMOL.js} +1 -1
  43. package/chunks/{chunk-CKQA6AWF.js → chunk-4D73HUOP.js} +1 -1
  44. package/chunks/{chunk-M2U5NM24.js → chunk-54YZOP7P.js} +309 -72
  45. package/chunks/{chunk-J7FA27RO.js → chunk-5LKL3PAU.js} +3 -3
  46. package/chunks/chunk-5P5XGNYH.js +93 -0
  47. package/chunks/{chunk-UFLGERMB.js → chunk-6JE32BRB.js} +178 -124
  48. package/chunks/{chunk-FKAVULLM.js → chunk-75AVTQJ2.js} +74 -10
  49. package/chunks/{chunk-I2LYBGNI.js → chunk-7IOMIYOE.js} +1 -1
  50. package/chunks/{chunk-4LUZAKMK.js → chunk-7KXAGQJ2.js} +1 -1
  51. package/chunks/{chunk-BXSHL2SF.js → chunk-7LUOQJ3D.js} +1 -1
  52. package/chunks/{chunk-YAINVQ3Q.js → chunk-A5OPLHFM.js} +18693 -17055
  53. package/chunks/chunk-AHNXYU4O.js +24 -0
  54. package/chunks/{chunk-UX4SGCGN.js → chunk-AVQDCKNF.js} +96 -22
  55. package/chunks/{chunk-HYCC3XM3.js → chunk-B236XPGF.js} +1 -1
  56. package/chunks/chunk-B6SZLQAZ.js +3523 -0
  57. package/chunks/{chunk-5DWXLGOM.js → chunk-BJBWRCSK.js} +1 -1
  58. package/chunks/chunk-BPFJQGY7.js +193 -0
  59. package/chunks/{chunk-SKQZZVAV.js → chunk-BZHLWC6T.js} +2 -2
  60. package/chunks/{chunk-U3KSMM7G.js → chunk-D5KISTLW.js} +339 -383
  61. package/chunks/{chunk-GT2FGICD.js → chunk-DE7YXUR7.js} +1 -1
  62. package/chunks/{chunk-BUUQ5HPX.js → chunk-DG573EOO.js} +1 -1
  63. package/chunks/{chunk-VVLOGDB6.js → chunk-DX3TKXNT.js} +2 -2
  64. package/chunks/chunk-E5A7LHNN.js +51 -0
  65. package/chunks/{chunk-3PJXIDKI.js → chunk-E6US47KI.js} +3 -27
  66. package/chunks/{chunk-QBZ22PQX.js → chunk-EWALXUAD.js} +3 -1
  67. package/chunks/{chunk-PDE7CSDS.js → chunk-FXKI7OW6.js} +2 -2
  68. package/chunks/{chunk-F3ZYSEY6.js → chunk-FY4TT4L4.js} +3 -3
  69. package/chunks/{chunk-QLDPRILK.js → chunk-G7OQ3RFJ.js} +3 -3
  70. package/chunks/{chunk-R2BXK7SX.js → chunk-GHOTR7HL.js} +6 -1
  71. package/chunks/{chunk-KMGMKW5Q.js → chunk-GLPV75ES.js} +9 -3
  72. package/chunks/{chunk-LD2XBG6Z.js → chunk-GO6LNQXT.js} +1 -39
  73. package/chunks/{chunk-CNLEDYE3.js → chunk-H6TNLOLM.js} +5 -5
  74. package/chunks/{chunk-JVHEVCAU.js → chunk-HLW4EGKR.js} +31 -6
  75. package/chunks/{chunk-I4H7PRWQ.js → chunk-HTWHAN44.js} +1 -1
  76. package/chunks/{chunk-QE6KJYLT.js → chunk-IPYE5BIS.js} +123 -25
  77. package/chunks/{chunk-EJYJYJZ2.js → chunk-IRJWHZWU.js} +3 -70
  78. package/chunks/{chunk-EDMZZFDU.js → chunk-J3YPTTNL.js} +930 -798
  79. package/chunks/{chunk-GGBIN5K7.js → chunk-JGAJMIJZ.js} +1 -1
  80. package/chunks/{chunk-N4EJFBN5.js → chunk-JVLP5US5.js} +427 -263
  81. package/chunks/{chunk-R5OP2ATH.js → chunk-JZG7VB6C.js} +11 -11
  82. package/chunks/{chunk-QI7GOGJR.js → chunk-KKQWC6WS.js} +1 -1
  83. package/chunks/{chunk-ADCIHOQZ.js → chunk-KX5QZRW2.js} +142 -30
  84. package/chunks/{chunk-SBBN4BWR.js → chunk-L7OJ3ZO3.js} +1 -1
  85. package/chunks/{chunk-NDOC6M5S.js → chunk-L7QPM2YD.js} +2 -2
  86. package/chunks/{chunk-BVSVIN45.js → chunk-LKWAVAB5.js} +3 -3
  87. package/chunks/{chunk-T4T2FNV4.js → chunk-LOW52WYZ.js} +2 -2
  88. package/chunks/{chunk-4QK3O43S.js → chunk-LU7BIVWC.js} +4 -4
  89. package/chunks/{chunk-ZP6UKOPB.js → chunk-LWGLBVWA.js} +1 -1
  90. package/chunks/{chunk-L7OKCI7T.js → chunk-M7BLWURA.js} +15 -1
  91. package/chunks/{chunk-PR32FRT6.js → chunk-MIM62IXS.js} +3 -3
  92. package/chunks/{chunk-RD6YJJJ6.js → chunk-MOUXUCXQ.js} +1 -1
  93. package/chunks/{chunk-VBZ452JT.js → chunk-MSNGFH6L.js} +494 -114
  94. package/chunks/{chunk-BX5YO7UW.js → chunk-MTNTVOJR.js} +542 -370
  95. package/chunks/chunk-MX2YXRER.js +44 -0
  96. package/chunks/chunk-NIOPRZNN.js +27 -0
  97. package/chunks/{chunk-SMYQZVZ3.js → chunk-NOGSPTLJ.js} +1 -1
  98. package/chunks/{chunk-DDMOU62Q.js → chunk-NPQWQYU6.js} +1 -1
  99. package/chunks/{chunk-MNF6S5PR.js → chunk-P5THQV6A.js} +4 -4
  100. package/chunks/{chunk-6V4NZFUS.js → chunk-PCYOT4PK.js} +5 -5
  101. package/chunks/{chunk-CF6BB3LT.js → chunk-PPFWEV7G.js} +10 -1
  102. package/chunks/{chunk-FXKVLFFY.js → chunk-PSFDB5FZ.js} +3 -3
  103. package/chunks/{chunk-XJYUPHRC.js → chunk-QOMAHFYI.js} +3 -3
  104. package/chunks/{chunk-J37FGIOA.js → chunk-QXPI7FEC.js} +38 -4
  105. package/chunks/{chunk-RRFZXMYP.js → chunk-QZDBHNMV.js} +691 -164
  106. package/chunks/{chunk-S7AKE7HL.js → chunk-R6RI2HIC.js} +63 -9
  107. package/chunks/{chunk-HZT4UCFB.js → chunk-RB7S52NJ.js} +4 -4
  108. package/chunks/{chunk-J33ZR6OJ.js → chunk-RK22ACPP.js} +16 -16
  109. package/chunks/chunk-RM3PGSGL.js +66 -0
  110. package/chunks/chunk-RNBYOUGV.js +42 -0
  111. package/chunks/chunk-S6RLAIUR.js +23 -0
  112. package/chunks/{chunk-3TV3HUWZ.js → chunk-SCYK2YLI.js} +81 -30
  113. package/chunks/{chunk-HPZTOP4I.js → chunk-SEKRT4SD.js} +133 -23
  114. package/chunks/chunk-SEZC2725.js +36 -0
  115. package/chunks/chunk-SIUQ3YYX.js +78 -0
  116. package/chunks/{chunk-O2VBQ3Y2.js → chunk-SJBIFVD2.js} +2 -2
  117. package/chunks/chunk-SYCJMSIJ.js +82 -0
  118. package/chunks/{chunk-JE5FVRPC.js → chunk-T22H4V4R.js} +48 -23
  119. package/chunks/{chunk-3227DZO4.js → chunk-T42ACHFI.js} +1 -1
  120. package/chunks/{chunk-UO5GVXQE.js → chunk-TENOUWY2.js} +3 -3
  121. package/chunks/{chunk-JOLIREYR.js → chunk-TOAUDIS7.js} +1 -1
  122. package/chunks/{chunk-QNKBEJO2.js → chunk-UA6XBZT7.js} +2776 -1289
  123. package/chunks/{chunk-O75BHFQW.js → chunk-UQTYK5SS.js} +2 -2
  124. package/chunks/{chunk-Y6YMRS3U.js → chunk-UY2TCIZS.js} +17 -3
  125. package/chunks/{chunk-FNJ7BDPH.js → chunk-UY7ZAYTY.js} +10 -7
  126. package/chunks/{chunk-UYT3MWEB.js → chunk-UYHWM24C.js} +30 -2
  127. package/chunks/{chunk-SMXULRAN.js → chunk-VXGPYF7U.js} +1 -1
  128. package/chunks/{chunk-BTLJ2MF5.js → chunk-W7CG5HPC.js} +3 -3
  129. package/chunks/{chunk-JJXWTVSK.js → chunk-XHZKDN7M.js} +34 -1
  130. package/chunks/{chunk-K5XU6E4T.js → chunk-XKSCTXPK.js} +2 -2
  131. package/chunks/chunk-Y4LITCRB.js +367 -0
  132. package/chunks/chunk-YHTNWGRR.js +239 -0
  133. package/chunks/{chunk-BKXBARJZ.js → chunk-YPCPZSZB.js} +1 -1
  134. package/chunks/{chunk-QWFNTRH4.js → chunk-YSM24SCE.js} +1 -1
  135. package/chunks/chunk-YTFBURQD.js +313 -0
  136. package/chunks/{chunk-NL4C3H6C.js → chunk-YXK4GPYO.js} +1 -1
  137. package/chunks/{chunk-RYLJU2DY.js → chunk-ZN5T4BHI.js} +1 -1
  138. package/chunks/{computer-use-JGB36FDW.js → computer-use-MTSVPLPY.js} +34 -28
  139. package/chunks/contextCommand-VGJ3BPTK.js +67 -0
  140. package/chunks/{cron-create-SKEY55RG.js → cron-create-6YTCOX6P.js} +4 -4
  141. package/chunks/{cron-delete-I3NAT32Z.js → cron-delete-E3NIFSUK.js} +4 -4
  142. package/chunks/{cron-list-6XRRL6TH.js → cron-list-Q64MNQNE.js} +4 -4
  143. package/chunks/daemon-EEB2RYOL.js +8453 -0
  144. package/chunks/daemon-status-provider-H5SNMK42.js +71 -0
  145. package/chunks/{de-EOW5PVHF.js → de-PLLFDERI.js} +10 -9
  146. package/chunks/{dist-NHEYYB2B.js → dist-ADMIQGHI.js} +2 -2
  147. package/chunks/{dist-J37VXPLD.js → dist-BCQ7INM7.js} +40 -15
  148. package/chunks/{dist-663NZ6MI.js → dist-KNZFCY5F.js} +116 -11
  149. package/chunks/{dist-LB6OGPVM.js → dist-LHUWC3K6.js} +26 -5
  150. package/chunks/{dist-PQPZ5Q4N.js → dist-WBBZ76JZ.js} +20 -9
  151. package/chunks/{earlyInputCapture-44RISVVJ.js → earlyInputCapture-U4ZIQSQU.js} +32 -26
  152. package/chunks/{edit-NN6GM5DT.js → edit-SYIQ2KBI.js} +41 -38
  153. package/chunks/{en-HPWEJBTR.js → en-M4EBUBNU.js} +32 -10
  154. package/chunks/{enter-worktree-PB3O2C7D.js → enter-worktree-HPPOO244.js} +31 -25
  155. package/chunks/{enterPlanMode-YQFNNSUA.js → enterPlanMode-56YYO52U.js} +41 -25
  156. package/chunks/{errors-LS7XU5DZ.js → errors-EO4SXCFP.js} +33 -27
  157. package/chunks/{exit-worktree-E2FTNSBK.js → exit-worktree-UDM7E6XH.js} +31 -25
  158. package/chunks/{exitPlanMode-GLYK2BUB.js → exitPlanMode-M6P3AWLJ.js} +149 -33
  159. package/chunks/{fast-path-WQ6M7LI7.js → fast-path-42P4QYWI.js} +33 -14
  160. package/chunks/{fast-path-settings-OAGU3X5H.js → fast-path-settings-UQLRFDQP.js} +2 -2
  161. package/chunks/{fr-BN5NKSAC.js → fr-MBMNQSQ6.js} +10 -9
  162. package/chunks/{gemini-2YFLVA2M.js → gemini-NRLXS5J7.js} +84 -70
  163. package/chunks/{geminiContentGenerator-XV6SV6CD.js → geminiContentGenerator-DLJPYYPA.js} +41 -5
  164. package/chunks/{glob-2H6CDDMK.js → glob-JPY3F74Y.js} +31 -25
  165. package/chunks/{grep-VXIUXM3C.js → grep-HW23YES5.js} +31 -25
  166. package/chunks/initializer-HWOZTC4A.js +67 -0
  167. package/chunks/{ja-7DEUB2TS.js → ja-HFV5QCHR.js} +10 -9
  168. package/chunks/{keychain-token-storage-YLMDLKXT.js → keychain-token-storage-GPAKRDKQ.js} +2 -2
  169. package/chunks/list-OZGAGRDG.js +71 -0
  170. package/chunks/loadedSettingsAdapter-RUTZVVIA.js +66 -0
  171. package/chunks/{loop-wakeup-CITGGQHA.js → loop-wakeup-G2EHBRGR.js} +5 -5
  172. package/chunks/{ls-R5WP4BGZ.js → ls-IDHUVSAG.js} +4 -4
  173. package/chunks/{lsp-GSICJMMW.js → lsp-FSRYXGFT.js} +2 -2
  174. package/chunks/{monitor-HKLRS2HQ.js → monitor-QVWEIXU3.js} +33 -26
  175. package/chunks/nonInteractiveCli-RV2WYPML.js +109 -0
  176. package/chunks/{notebook-edit-Y6SKSGTT.js → notebook-edit-B26W3FFH.js} +37 -34
  177. package/chunks/{openaiContentGenerator-J7G2FYJT.js → openaiContentGenerator-6TH7AZGQ.js} +15 -13
  178. package/chunks/pidfile-KW2QQJZI.js +73 -0
  179. package/chunks/{pt-TNZRNLQB.js → pt-2THT3YHA.js} +10 -9
  180. package/chunks/{qwenContentGenerator-5XIEM5GV.js → qwenContentGenerator-5IQAC7RN.js} +33 -27
  181. package/chunks/{qwenOAuth2-AG3PFFLZ.js → qwenOAuth2-LWLA6MQG.js} +5 -4
  182. package/chunks/{read-file-J7D552ZJ.js → read-file-CWFAHOI2.js} +11 -10
  183. package/chunks/{read-mcp-resource-DQQW7H7T.js → read-mcp-resource-PIJF24RO.js} +2 -2
  184. package/chunks/ripGrep-FTZGDKY4.js +60 -0
  185. package/chunks/{ru-NIAUBKCN.js → ru-Z67YJHBG.js} +10 -9
  186. package/chunks/{run-qwen-serve-SGEEVMXD.js → run-qwen-serve-FN2SW5ZI.js} +545 -97
  187. package/chunks/{scheduler-7CQI3S4Q.js → scheduler-BMS23O3I.js} +31 -25
  188. package/chunks/{send-message-EE4TWKVU.js → send-message-33PYFMCT.js} +20 -4
  189. package/chunks/serve-PXFYKL65.js +71 -0
  190. package/chunks/{server-4QMUWXPW.js → server-BRKALJ4F.js} +4132 -1175
  191. package/chunks/{session-NRO7UFOY.js → session-AQMISBP4.js} +205 -64
  192. package/chunks/{settings-SGWRDQSC.js → settings-JCPVUZMB.js} +35 -29
  193. package/chunks/{shell-62ANGIHL.js → shell-POHCRVN2.js} +31 -25
  194. package/chunks/{skill-VWAQAWW6.js → skill-QPEVHS6H.js} +13 -12
  195. package/chunks/spawnChannel-V3QT62TE.js +69 -0
  196. package/chunks/{src-KUV5J5Q2.js → src-IZSP7L6Y.js} +127 -32
  197. package/chunks/{startInteractiveUI-6EYJN3JV.js → startInteractiveUI-63KATISO.js} +16762 -16075
  198. package/chunks/{syntheticOutput-T5YHQ6S2.js → syntheticOutput-YOSWIHDJ.js} +3 -3
  199. package/chunks/{task-create-BLY7RHXX.js → task-create-NHS2NGB3.js} +9 -7
  200. package/chunks/{task-list-3QUYNVYR.js → task-list-5UIB564R.js} +7 -6
  201. package/chunks/{task-stop-U4RATK2V.js → task-stop-HGXZ2VZ3.js} +2 -2
  202. package/chunks/{task-update-U54O7KJX.js → task-update-IYYWUILG.js} +40 -7
  203. package/chunks/{team-create-LPOGCPM7.js → team-create-E5O2F3CA.js} +31 -25
  204. package/chunks/{team-delete-LKBYPASD.js → team-delete-FEKJCU4P.js} +7 -6
  205. package/chunks/team-plan-approval-2TU4YROB.js +202 -0
  206. package/chunks/theme-manager-37KOGPJL.js +64 -0
  207. package/chunks/{todoWrite-OYXKTVYN.js → todoWrite-TKMVZ2HT.js} +4 -4
  208. package/chunks/{tool-search-YPUFJ3ZQ.js → tool-search-4ZTGNSFI.js} +38 -11
  209. package/chunks/{trustedFolders-5B24TMEA.js → trustedFolders-RJ5WXJ6L.js} +32 -26
  210. package/chunks/{validateNonInterActiveAuth-BTDTYOVT.js → validateNonInterActiveAuth-ZWMHZAOK.js} +66 -55
  211. package/chunks/{web-fetch-EEB5LZYU.js → web-fetch-J4PIW36Q.js} +5 -4
  212. package/chunks/{workflow-LAWIW5LI.js → workflow-6PG2PC5M.js} +68 -39
  213. package/chunks/workspace-providers-status-4QIZN2W4.js +68 -0
  214. package/chunks/workspace-service-CYMCOEIO.js +78 -0
  215. package/chunks/{write-file-PCV226DJ.js → write-file-R354LGQF.js} +41 -40
  216. package/chunks/{zh-5EUYXASN.js → zh-SLZNSSZB.js} +32 -10
  217. package/chunks/{zh-TW-TR3XYGE5.js → zh-TW-P53C2SL5.js} +32 -10
  218. package/cli.js +6 -6
  219. package/locales/ca.js +15 -13
  220. package/locales/de.js +15 -13
  221. package/locales/en.js +52 -15
  222. package/locales/fr.js +15 -14
  223. package/locales/ja.js +15 -14
  224. package/locales/pt.js +15 -13
  225. package/locales/ru.js +15 -13
  226. package/locales/zh-TW.js +50 -14
  227. package/locales/zh.js +50 -14
  228. package/node_modules/@qwen-code/audio-capture/package.json +1 -1
  229. package/node_modules/@qwen-code/audio-capture/prebuilds/darwin-arm64/@qwen-code+audio-capture.node +0 -0
  230. package/node_modules/@qwen-code/audio-capture/prebuilds/darwin-x64/@qwen-code+audio-capture.node +0 -0
  231. package/node_modules/@qwen-code/audio-capture/prebuilds/win32-x64/@qwen-code+audio-capture.node +0 -0
  232. package/package.json +15 -6
  233. package/patches/chrome-devtools-mcp+1.4.0.patch +31 -0
  234. package/postinstall.js +50 -0
  235. package/web-shell/assets/{arc-DA7r8Ueo.js → arc-Bd3048nX.js} +1 -1
  236. package/web-shell/assets/{architectureDiagram-3BPJPVTR-ZDUxPtZ2.js → architectureDiagram-3BPJPVTR-DyzbzABx.js} +1 -1
  237. package/web-shell/assets/{blockDiagram-GPEHLZMM-vrtOf1kJ.js → blockDiagram-GPEHLZMM-CTjxm9Sy.js} +1 -1
  238. package/web-shell/assets/{c4Diagram-AAUBKEIU-BHFjzIi4.js → c4Diagram-AAUBKEIU-DSOFWNU6.js} +1 -1
  239. package/web-shell/assets/channel-oVoup_Ci.js +1 -0
  240. package/web-shell/assets/{chunk-2J33WTMH-mdeQVz59.js → chunk-2J33WTMH-D_PFdjo2.js} +1 -1
  241. package/web-shell/assets/{chunk-4BX2VUAB-BkKMlwBX.js → chunk-4BX2VUAB-52eG6wOT.js} +1 -1
  242. package/web-shell/assets/{chunk-55IACEB6-B7bnXNfu.js → chunk-55IACEB6-CGTZfKkC.js} +1 -1
  243. package/web-shell/assets/{chunk-727SXJPM-DGz8Mm6i.js → chunk-727SXJPM-CZ-qfRUm.js} +1 -1
  244. package/web-shell/assets/{chunk-AQP2D5EJ-BQAydUNk.js → chunk-AQP2D5EJ-B2dSDcqM.js} +1 -1
  245. package/web-shell/assets/{chunk-FMBD7UC4-GHr2LN_p.js → chunk-FMBD7UC4-Cfy3tyA0.js} +1 -1
  246. package/web-shell/assets/{chunk-ND2GUHAM-7R91lnUx.js → chunk-ND2GUHAM-CApNUpUq.js} +1 -1
  247. package/web-shell/assets/{chunk-QZHKN3VN-aTJD3lSQ.js → chunk-QZHKN3VN-CVBG8kgg.js} +1 -1
  248. package/web-shell/assets/classDiagram-4FO5ZUOK-B8WsaT2G.js +1 -0
  249. package/web-shell/assets/classDiagram-v2-Q7XG4LA2-B8WsaT2G.js +1 -0
  250. package/web-shell/assets/{cose-bilkent-S5V4N54A-BTf8t1RL.js → cose-bilkent-S5V4N54A-URd77-_-.js} +1 -1
  251. package/web-shell/assets/{dagre-BM42HDAG-_sgp3WuA.js → dagre-BM42HDAG-C07H4G9-.js} +1 -1
  252. package/web-shell/assets/{diagram-2AECGRRQ-CROtRy1w.js → diagram-2AECGRRQ-CYbpXrSn.js} +1 -1
  253. package/web-shell/assets/{diagram-5GNKFQAL-BqGRzwSc.js → diagram-5GNKFQAL-SI8s6fjX.js} +1 -1
  254. package/web-shell/assets/{diagram-KO2AKTUF-CMrcEtA7.js → diagram-KO2AKTUF-mBkC4RTW.js} +1 -1
  255. package/web-shell/assets/{diagram-LMA3HP47-BRydGNW5.js → diagram-LMA3HP47-CyBPDzWJ.js} +1 -1
  256. package/web-shell/assets/{diagram-OG6HWLK6-D0ilgNjV.js → diagram-OG6HWLK6-DwK7kBCu.js} +1 -1
  257. package/web-shell/assets/{erDiagram-TEJ5UH35-2lnQeJUM.js → erDiagram-TEJ5UH35-Ds1StEUd.js} +1 -1
  258. package/web-shell/assets/{flowDiagram-I6XJVG4X-C_ZMdQiC.js → flowDiagram-I6XJVG4X-BDC4-jHO.js} +1 -1
  259. package/web-shell/assets/{ganttDiagram-6RSMTGT7-Dw9V_Ny0.js → ganttDiagram-6RSMTGT7-CV2aHQqf.js} +1 -1
  260. package/web-shell/assets/{gitGraphDiagram-PVQCEYII-HDXI-F1B.js → gitGraphDiagram-PVQCEYII-B8RcuV_L.js} +1 -1
  261. package/web-shell/assets/index-B5WZ0qP1.js +717 -0
  262. package/web-shell/assets/index-CYE1IMd1.css +5 -0
  263. package/web-shell/assets/{infoDiagram-5YYISTIA-CuB8Ebua.js → infoDiagram-5YYISTIA-DUPoTf50.js} +1 -1
  264. package/web-shell/assets/{ishikawaDiagram-YF4QCWOH-D_tyhVwh.js → ishikawaDiagram-YF4QCWOH-FDYOx9rf.js} +1 -1
  265. package/web-shell/assets/{journeyDiagram-JHISSGLW-CgCwJQNU.js → journeyDiagram-JHISSGLW-C5_ohMRp.js} +1 -1
  266. package/web-shell/assets/{kanban-definition-UN3LZRKU-PZUtcbPx.js → kanban-definition-UN3LZRKU-Bn4UnOMG.js} +1 -1
  267. package/web-shell/assets/{linear-CZqRdJIU.js → linear-Bl0pKQtY.js} +1 -1
  268. package/web-shell/assets/{mermaid.core-CPW_4UEF.js → mermaid.core-sevQFoji.js} +72 -72
  269. package/web-shell/assets/{mindmap-definition-RKZ34NQL-D_Y75ID7.js → mindmap-definition-RKZ34NQL-DSH4JZF9.js} +1 -1
  270. package/web-shell/assets/{pieDiagram-4H26LBE5-Q4dTyLeF.js → pieDiagram-4H26LBE5-acWvdQTD.js} +1 -1
  271. package/web-shell/assets/{quadrantDiagram-W4KKPZXB-ckw329Ft.js → quadrantDiagram-W4KKPZXB-DlggX4Sm.js} +1 -1
  272. package/web-shell/assets/{requirementDiagram-4Y6WPE33-DbVDm4It.js → requirementDiagram-4Y6WPE33--mMqwmV8.js} +1 -1
  273. package/web-shell/assets/{sankeyDiagram-5OEKKPKP-CHuZ_ecH.js → sankeyDiagram-5OEKKPKP-BtV9tZ77.js} +1 -1
  274. package/web-shell/assets/{sequenceDiagram-3UESZ5HK-DU2YSpq-.js → sequenceDiagram-3UESZ5HK-iDQzPFlj.js} +1 -1
  275. package/web-shell/assets/{stateDiagram-AJRCARHV-CA9PEBUo.js → stateDiagram-AJRCARHV-CsCyQLZ3.js} +1 -1
  276. package/web-shell/assets/stateDiagram-v2-BHNVJYJU-CPapECG9.js +1 -0
  277. package/web-shell/assets/{timeline-definition-PNZ67QCA-Bhlo4WVc.js → timeline-definition-PNZ67QCA-Dl0rnKvr.js} +1 -1
  278. package/web-shell/assets/{vennDiagram-CIIHVFJN-6xtPLsyl.js → vennDiagram-CIIHVFJN-qF7nTZzA.js} +1 -1
  279. package/web-shell/assets/{wardley-L42UT6IY-3eozmXPf.js → wardley-L42UT6IY-fWoK9i7F.js} +1 -1
  280. package/web-shell/assets/{wardleyDiagram-YWT4CUSO-BOn6hKss.js → wardleyDiagram-YWT4CUSO-pHGfkMnc.js} +1 -1
  281. package/web-shell/assets/{xychartDiagram-2RQKCTM6-BsP4-I_y.js → xychartDiagram-2RQKCTM6-3Doc4Trz.js} +1 -1
  282. package/web-shell/index.html +18 -2
  283. package/chunks/MaxSizedBox-QNF3EIGU.js +0 -63
  284. package/chunks/agent-headless-RQ5BTX3G.js +0 -56
  285. package/chunks/bridge-JL2PMCIW.js +0 -66
  286. package/chunks/chunk-5GOVVXEV.js +0 -73
  287. package/chunks/chunk-BC3C4F3Q.js +0 -40
  288. package/chunks/chunk-BW4KCQFD.js +0 -1103
  289. package/chunks/chunk-RKIZXJMB.js +0 -13
  290. package/chunks/contextCommand-WYU6OQ2F.js +0 -61
  291. package/chunks/daemon-status-provider-HH2EC5FI.js +0 -65
  292. package/chunks/headlessSafetyWarnings-TBE35GJ4.js +0 -55
  293. package/chunks/initializer-2Y7SLOKV.js +0 -61
  294. package/chunks/list-ZAFHZ7OS.js +0 -65
  295. package/chunks/loadedSettingsAdapter-6KJXD6M3.js +0 -60
  296. package/chunks/nonInteractiveCli-5W4GIZQM.js +0 -96
  297. package/chunks/ripGrep-IEIFIWP4.js +0 -54
  298. package/chunks/serve-V6GVZR6U.js +0 -65
  299. package/chunks/spawnChannel-3VHW5EPY.js +0 -63
  300. package/chunks/theme-manager-LYLGAERF.js +0 -58
  301. package/chunks/workspace-providers-status-PIAFVYHA.js +0 -62
  302. package/chunks/workspace-service-YAIOD4HG.js +0 -71
  303. package/web-shell/assets/channel-iRo68q4Q.js +0 -1
  304. package/web-shell/assets/classDiagram-4FO5ZUOK-iHF24a8X.js +0 -1
  305. package/web-shell/assets/classDiagram-v2-Q7XG4LA2-iHF24a8X.js +0 -1
  306. package/web-shell/assets/index-CHmzAWys.css +0 -5
  307. package/web-shell/assets/index-w6GTOHh1.js +0 -713
  308. package/web-shell/assets/stateDiagram-v2-BHNVJYJU-t1LRI0oY.js +0 -1
@@ -0,0 +1,3523 @@
1
+ // Force strict mode and setup for ESM
2
+ "use strict";
3
+ import {
4
+ ClientSideConnection,
5
+ PROTOCOL_VERSION,
6
+ ndJsonStream
7
+ } from "./chunk-UFC57OYT.js";
8
+ import {
9
+ init_esbuild_shims
10
+ } from "./chunk-A4BMJM77.js";
11
+ import {
12
+ __name
13
+ } from "./chunk-J2S4EL5Y.js";
14
+
15
+ // packages/channels/base/dist/sanitize.js
16
+ init_esbuild_shims();
17
+ var PROMPT_UNSAFE_INVISIBLES = new RegExp("[\\u0080-\\u009f\\p{Cf}\\u2028\\u2029]|\\p{Variation_Selector}", "gu");
18
+ function truncateCodePoints(str, max) {
19
+ const cp = Array.from(str);
20
+ return cp.length > max ? cp.slice(0, max).join("") : str;
21
+ }
22
+ __name(truncateCodePoints, "truncateCodePoints");
23
+ function sanitizeSenderName(name) {
24
+ const cleaned = name.replace(PROMPT_UNSAFE_INVISIBLES, " ").replace(/[\u0000-\u001f\u007f]/g, " ").replace(/[[\]\r\n]/g, " ");
25
+ return truncateCodePoints(cleaned, 64).trim() || "unknown";
26
+ }
27
+ __name(sanitizeSenderName, "sanitizeSenderName");
28
+ function sanitizeQuotedText(text, maxLen) {
29
+ const cleaned = text.replace(PROMPT_UNSAFE_INVISIBLES, " ").replace(/[\u0000-\u001f\u007f]/g, " ").replace(/["[\]]/g, " ");
30
+ const cp = Array.from(cleaned);
31
+ return cp.length > maxLen ? cp.slice(0, maxLen - 1).join("") + "\u2026" : cleaned;
32
+ }
33
+ __name(sanitizeQuotedText, "sanitizeQuotedText");
34
+ function sanitizePromptText(text) {
35
+ return text.replace(PROMPT_UNSAFE_INVISIBLES, " ").replace(/^([ \t]*)\[([^\]\r\n]{1,64})\](:?)/gm, "$1$2$3").replace(/[\u0000-\u001f\u007f]/g, " ");
36
+ }
37
+ __name(sanitizePromptText, "sanitizePromptText");
38
+ function sanitizePromptPath(path4) {
39
+ const cleaned = path4.replace(PROMPT_UNSAFE_INVISIBLES, " ").replace(/[\u0000-\u001f\u007f]/g, " ");
40
+ return truncateCodePoints(cleaned, 1024);
41
+ }
42
+ __name(sanitizePromptPath, "sanitizePromptPath");
43
+ function sanitizeLogText(text, maxLen) {
44
+ return truncateCodePoints(text, maxLen).replace(/\n/g, "\\n").replace(PROMPT_UNSAFE_INVISIBLES, " ").replace(/[\u0000-\u001f\u007f]/g, " ");
45
+ }
46
+ __name(sanitizeLogText, "sanitizeLogText");
47
+
48
+ // packages/channels/base/dist/index.js
49
+ init_esbuild_shims();
50
+
51
+ // packages/channels/base/dist/paths.js
52
+ init_esbuild_shims();
53
+ import * as path from "node:path";
54
+ import * as os from "node:os";
55
+ function resolvePath(dir) {
56
+ let resolved = dir;
57
+ if (resolved === "~" || resolved.startsWith("~/") || resolved.startsWith("~\\")) {
58
+ const relativeSegments = resolved === "~" ? [] : resolved.slice(2).split(/[/\\]+/).filter(Boolean);
59
+ resolved = path.join(os.homedir(), ...relativeSegments);
60
+ }
61
+ if (!path.isAbsolute(resolved)) {
62
+ resolved = path.resolve(resolved);
63
+ }
64
+ return resolved;
65
+ }
66
+ __name(resolvePath, "resolvePath");
67
+ function getGlobalQwenDir() {
68
+ const envDir = process.env["QWEN_HOME"];
69
+ if (envDir) {
70
+ return resolvePath(envDir);
71
+ }
72
+ const homeDir = os.homedir();
73
+ return homeDir ? path.join(homeDir, ".qwen") : path.join(os.tmpdir(), ".qwen");
74
+ }
75
+ __name(getGlobalQwenDir, "getGlobalQwenDir");
76
+
77
+ // packages/channels/base/dist/AcpBridge.js
78
+ init_esbuild_shims();
79
+ import { spawn } from "node:child_process";
80
+ import { Readable, Writable } from "node:stream";
81
+ import { EventEmitter } from "node:events";
82
+ function readAvailableCommandAltNames(raw) {
83
+ if (typeof raw !== "object" || raw === null)
84
+ return void 0;
85
+ const record = raw;
86
+ const meta = record["_meta"];
87
+ const fromMeta = typeof meta === "object" && meta !== null ? meta["altNames"] : void 0;
88
+ const source = Array.isArray(record["altNames"]) ? record["altNames"] : Array.isArray(fromMeta) ? fromMeta : void 0;
89
+ if (!source)
90
+ return void 0;
91
+ const names = source.filter((n) => typeof n === "string");
92
+ return names.length > 0 ? names : void 0;
93
+ }
94
+ __name(readAvailableCommandAltNames, "readAvailableCommandAltNames");
95
+ var AcpBridge = class extends EventEmitter {
96
+ static {
97
+ __name(this, "AcpBridge");
98
+ }
99
+ child = null;
100
+ connection = null;
101
+ options;
102
+ _availableCommands = [];
103
+ constructor(options) {
104
+ super();
105
+ this.options = options;
106
+ }
107
+ get availableCommands() {
108
+ return this._availableCommands;
109
+ }
110
+ async start() {
111
+ const { cliEntryPath, cwd } = this.options;
112
+ const args = [
113
+ ...process.execArgv.filter((a) => !/^--inspect(-brk)?($|=)/.test(a)),
114
+ cliEntryPath,
115
+ "--acp"
116
+ ];
117
+ if (this.options.model) {
118
+ args.push("--model", this.options.model);
119
+ }
120
+ this.child = spawn(process.execPath, args, {
121
+ cwd,
122
+ stdio: ["pipe", "pipe", "pipe"],
123
+ env: { ...process.env },
124
+ shell: false
125
+ });
126
+ this.child.stderr?.on("data", (data) => {
127
+ const msg = data.toString().trim();
128
+ if (msg) {
129
+ process.stderr.write(`[AcpBridge] ${msg}
130
+ `);
131
+ }
132
+ });
133
+ this.child.on("exit", (code, signal) => {
134
+ process.stderr.write(`[AcpBridge] Process exited (code=${code}, signal=${signal})
135
+ `);
136
+ this.connection = null;
137
+ this.child = null;
138
+ this.emit("disconnected", code, signal);
139
+ });
140
+ await new Promise((resolve2) => setTimeout(resolve2, 1e3));
141
+ if (!this.child || this.child.killed) {
142
+ throw new Error("ACP process failed to start");
143
+ }
144
+ const stdout = Readable.toWeb(this.child.stdout);
145
+ const stdin = Writable.toWeb(this.child.stdin);
146
+ const stream = ndJsonStream(stdin, stdout);
147
+ this.connection = new ClientSideConnection(() => ({
148
+ sessionUpdate: /* @__PURE__ */ __name((params) => {
149
+ this.handleSessionUpdate(params);
150
+ return Promise.resolve();
151
+ }, "sessionUpdate"),
152
+ requestPermission: /* @__PURE__ */ __name(async (params) => {
153
+ const options = Array.isArray(params.options) ? params.options : [];
154
+ const optionId = options.find((o) => o.optionId === "proceed_once")?.optionId || options[0]?.optionId || "proceed_once";
155
+ return { outcome: { outcome: "selected", optionId } };
156
+ }, "requestPermission"),
157
+ extNotification: /* @__PURE__ */ __name(async () => {
158
+ }, "extNotification")
159
+ }), stream);
160
+ await this.connection.initialize({
161
+ protocolVersion: PROTOCOL_VERSION,
162
+ clientCapabilities: {}
163
+ });
164
+ }
165
+ async newSession(cwd) {
166
+ const conn = this.ensureConnection();
167
+ const response = await conn.newSession({ cwd, mcpServers: [] });
168
+ return response.sessionId;
169
+ }
170
+ async loadSession(sessionId, cwd) {
171
+ const conn = this.ensureConnection();
172
+ const response = await conn.loadSession({
173
+ sessionId,
174
+ cwd,
175
+ mcpServers: []
176
+ });
177
+ return response.sessionId;
178
+ }
179
+ async prompt(sessionId, text, options) {
180
+ const conn = this.ensureConnection();
181
+ const chunks = [];
182
+ const onChunk = /* @__PURE__ */ __name((sid, chunk) => {
183
+ if (sid === sessionId)
184
+ chunks.push(chunk);
185
+ }, "onChunk");
186
+ this.on("textChunk", onChunk);
187
+ const prompt = [];
188
+ if (options?.imageBase64 && options.imageMimeType) {
189
+ prompt.push({
190
+ type: "image",
191
+ data: options.imageBase64,
192
+ mimeType: options.imageMimeType
193
+ });
194
+ }
195
+ prompt.push({ type: "text", text });
196
+ try {
197
+ await conn.prompt({
198
+ sessionId,
199
+ prompt
200
+ });
201
+ } finally {
202
+ this.off("textChunk", onChunk);
203
+ }
204
+ return chunks.join("");
205
+ }
206
+ async cancelSession(sessionId) {
207
+ const conn = this.ensureConnection();
208
+ await conn.cancel({ sessionId });
209
+ }
210
+ stop() {
211
+ if (this.child) {
212
+ this.child.kill();
213
+ this.child = null;
214
+ }
215
+ this.connection = null;
216
+ }
217
+ get isConnected() {
218
+ return this.child !== null && !this.child.killed && this.child.exitCode === null;
219
+ }
220
+ handleSessionUpdate(params) {
221
+ const { sessionId } = params;
222
+ const update = params["update"];
223
+ if (!update)
224
+ return;
225
+ const type = update["sessionUpdate"];
226
+ switch (type) {
227
+ case "agent_message_chunk": {
228
+ const content = update["content"];
229
+ if (content?.type === "text" && content.text) {
230
+ this.emit("textChunk", sessionId, content.text);
231
+ }
232
+ break;
233
+ }
234
+ case "tool_call": {
235
+ const event = {
236
+ sessionId,
237
+ toolCallId: update["toolCallId"],
238
+ kind: update["kind"] || "",
239
+ title: update["title"] || "",
240
+ status: update["status"] || "pending",
241
+ rawInput: update["rawInput"]
242
+ };
243
+ this.emit("toolCall", event);
244
+ break;
245
+ }
246
+ case "available_commands_update": {
247
+ if (Array.isArray(update["availableCommands"])) {
248
+ this._availableCommands = update["availableCommands"].map((cmd) => {
249
+ const altNames = readAvailableCommandAltNames(cmd);
250
+ return altNames ? { ...cmd, altNames } : cmd;
251
+ });
252
+ }
253
+ break;
254
+ }
255
+ default:
256
+ break;
257
+ }
258
+ this.emit("sessionUpdate", params);
259
+ }
260
+ ensureConnection() {
261
+ if (!this.connection || !this.isConnected) {
262
+ throw new Error("Not connected to ACP agent");
263
+ }
264
+ return this.connection;
265
+ }
266
+ };
267
+
268
+ // packages/channels/base/dist/DaemonChannelBridge.js
269
+ init_esbuild_shims();
270
+ import { EventEmitter as EventEmitter2 } from "node:events";
271
+ var MAX_RESPONDED_PERMISSION_REQUESTS = 256;
272
+ function isRecord(value) {
273
+ return typeof value === "object" && value !== null;
274
+ }
275
+ __name(isRecord, "isRecord");
276
+ function getString(value) {
277
+ return typeof value === "string" ? value : void 0;
278
+ }
279
+ __name(getString, "getString");
280
+ function getTextContent(content) {
281
+ if (!isRecord(content)) {
282
+ return void 0;
283
+ }
284
+ return getString(content["text"]);
285
+ }
286
+ __name(getTextContent, "getTextContent");
287
+ function getSessionUpdate(data) {
288
+ if (!isRecord(data) || !isRecord(data["update"])) {
289
+ return void 0;
290
+ }
291
+ return data["update"];
292
+ }
293
+ __name(getSessionUpdate, "getSessionUpdate");
294
+ function isAvailableCommand(value) {
295
+ if (!isRecord(value) || typeof value["name"] !== "string")
296
+ return false;
297
+ const altNames = value["altNames"];
298
+ return altNames === void 0 || Array.isArray(altNames) && altNames.every((n) => typeof n === "string");
299
+ }
300
+ __name(isAvailableCommand, "isAvailableCommand");
301
+ function isPermissionRequestData(value) {
302
+ if (!isRecord(value) || typeof value["requestId"] !== "string" || !isRecord(value["toolCall"]) || typeof value["toolCall"]["toolCallId"] !== "string" || typeof value["toolCall"]["kind"] !== "string" || !Array.isArray(value["options"])) {
303
+ return false;
304
+ }
305
+ return value["options"].every((option) => isRecord(option) && typeof option["optionId"] === "string");
306
+ }
307
+ __name(isPermissionRequestData, "isPermissionRequestData");
308
+ function parsePermissionOutcome(value) {
309
+ if (!isRecord(value)) {
310
+ return void 0;
311
+ }
312
+ if (value["outcome"] === "cancelled") {
313
+ return { outcome: "cancelled" };
314
+ }
315
+ if (value["outcome"] === "selected" && typeof value["optionId"] === "string") {
316
+ return { outcome: "selected", optionId: value["optionId"] };
317
+ }
318
+ return void 0;
319
+ }
320
+ __name(parsePermissionOutcome, "parsePermissionOutcome");
321
+ function summarizeProtocolDetails(details) {
322
+ if (!isRecord(details)) {
323
+ return { type: typeof details };
324
+ }
325
+ const summary = {};
326
+ for (const key of [
327
+ "requestId",
328
+ "sessionId",
329
+ "sessionUpdate",
330
+ "modelId",
331
+ "requestedModelId",
332
+ "toolCallId",
333
+ "kind"
334
+ ]) {
335
+ const value = details[key];
336
+ if (typeof value === "string") {
337
+ summary[key] = value;
338
+ }
339
+ }
340
+ return summary;
341
+ }
342
+ __name(summarizeProtocolDetails, "summarizeProtocolDetails");
343
+ async function drainDaemonEventLoop() {
344
+ await new Promise((resolve2) => setTimeout(resolve2, 0));
345
+ }
346
+ __name(drainDaemonEventLoop, "drainDaemonEventLoop");
347
+ var DaemonChannelBridge = class extends EventEmitter2 {
348
+ static {
349
+ __name(this, "DaemonChannelBridge");
350
+ }
351
+ options;
352
+ sessions = /* @__PURE__ */ new Map();
353
+ eventControllers = /* @__PURE__ */ new Map();
354
+ requestToSession = /* @__PURE__ */ new Map();
355
+ respondedRequestToSession = /* @__PURE__ */ new Map();
356
+ activePrompts = /* @__PURE__ */ new Set();
357
+ activePromptControllers = /* @__PURE__ */ new Map();
358
+ availableCommandsBySession = /* @__PURE__ */ new Map();
359
+ connected = false;
360
+ latestAvailableCommandsSessionId;
361
+ lastError;
362
+ constructor(options) {
363
+ super();
364
+ this.options = options;
365
+ this.on("error", (error) => {
366
+ this.lastError = error;
367
+ });
368
+ }
369
+ get availableCommands() {
370
+ if (this.latestAvailableCommandsSessionId) {
371
+ return this.availableCommandsBySession.get(this.latestAvailableCommandsSessionId) ?? [];
372
+ }
373
+ return Array.from(this.availableCommandsBySession.values()).at(-1) ?? [];
374
+ }
375
+ get lastDaemonError() {
376
+ return this.lastError;
377
+ }
378
+ getAvailableCommands(sessionId) {
379
+ return this.availableCommandsBySession.get(sessionId) ?? [];
380
+ }
381
+ async start() {
382
+ this.connected = true;
383
+ }
384
+ async newSession(cwd) {
385
+ const session = await this.options.sessionFactory({
386
+ workspaceCwd: cwd || this.options.cwd,
387
+ modelServiceId: this.options.modelServiceId,
388
+ sessionScope: this.options.sessionScope ?? "thread"
389
+ });
390
+ this.attachSession(session);
391
+ return session.sessionId;
392
+ }
393
+ async loadSession(sessionId, cwd) {
394
+ const session = await this.options.sessionFactory({
395
+ workspaceCwd: cwd || this.options.cwd,
396
+ modelServiceId: this.options.modelServiceId,
397
+ sessionId,
398
+ sessionScope: this.options.sessionScope ?? "thread"
399
+ });
400
+ if (session.sessionId !== sessionId) {
401
+ throw new Error(`Daemon returned session ${session.sessionId} while loading ${sessionId}`);
402
+ }
403
+ this.attachSession(session);
404
+ return session.sessionId;
405
+ }
406
+ async prompt(sessionId, text, options) {
407
+ const session = this.ensureSession(sessionId);
408
+ if (this.activePrompts.has(sessionId)) {
409
+ throw new Error(`Prompt already in flight for daemon session ${sessionId}`);
410
+ }
411
+ this.activePrompts.add(sessionId);
412
+ const controller = new AbortController();
413
+ let controllers = this.activePromptControllers.get(sessionId);
414
+ if (!controllers) {
415
+ controllers = /* @__PURE__ */ new Set();
416
+ this.activePromptControllers.set(sessionId, controllers);
417
+ }
418
+ controllers.add(controller);
419
+ const chunks = [];
420
+ const onChunk = /* @__PURE__ */ __name((sid, chunk) => {
421
+ if (sid === sessionId) {
422
+ chunks.push(chunk);
423
+ }
424
+ }, "onChunk");
425
+ const onSessionDied = /* @__PURE__ */ __name((info) => {
426
+ if (info.sessionId === sessionId) {
427
+ controller.abort();
428
+ }
429
+ }, "onSessionDied");
430
+ this.on("textChunk", onChunk);
431
+ this.on("sessionDied", onSessionDied);
432
+ const prompt = [];
433
+ if (options?.imageBase64 && options.imageMimeType) {
434
+ prompt.push({
435
+ type: "image",
436
+ data: options.imageBase64,
437
+ mimeType: options.imageMimeType
438
+ });
439
+ }
440
+ prompt.push({ type: "text", text });
441
+ try {
442
+ const result = await session.prompt({ prompt }, controller.signal);
443
+ await drainDaemonEventLoop();
444
+ const textResult = chunks.join("");
445
+ this.emit("promptComplete", {
446
+ sessionId,
447
+ text: textResult,
448
+ stopReason: result.stopReason
449
+ });
450
+ return textResult;
451
+ } finally {
452
+ this.off("textChunk", onChunk);
453
+ this.off("sessionDied", onSessionDied);
454
+ this.activePrompts.delete(sessionId);
455
+ controllers.delete(controller);
456
+ if (controllers.size === 0 && this.activePromptControllers.get(sessionId) === controllers) {
457
+ this.activePromptControllers.delete(sessionId);
458
+ }
459
+ }
460
+ }
461
+ async shellCommand(sessionId, command, signal) {
462
+ const session = this.ensureSession(sessionId);
463
+ if (!session.shellCommand) {
464
+ throw new Error("Shell command not supported by this session client");
465
+ }
466
+ return session.shellCommand(command, signal);
467
+ }
468
+ async cancelSession(sessionId) {
469
+ const session = this.ensureSession(sessionId);
470
+ await session.cancel();
471
+ this.abortActivePrompts(sessionId);
472
+ this.activePrompts.delete(sessionId);
473
+ }
474
+ async setSessionModel(sessionId, modelId) {
475
+ return await this.ensureSession(sessionId).setModel(modelId);
476
+ }
477
+ async respondToPermission(requestId, response) {
478
+ const sessionId = this.requestToSession.get(requestId);
479
+ if (!sessionId) {
480
+ return false;
481
+ }
482
+ const session = this.sessions.get(sessionId);
483
+ if (!session) {
484
+ this.requestToSession.delete(requestId);
485
+ this.respondedRequestToSession.delete(requestId);
486
+ return false;
487
+ }
488
+ try {
489
+ const accepted = await session.respondToPermission(requestId, response);
490
+ this.requestToSession.delete(requestId);
491
+ if (accepted) {
492
+ this.rememberRespondedPermissionRequest(requestId, sessionId);
493
+ } else {
494
+ this.respondedRequestToSession.delete(requestId);
495
+ }
496
+ return accepted;
497
+ } catch (error) {
498
+ this.requestToSession.delete(requestId);
499
+ this.respondedRequestToSession.delete(requestId);
500
+ throw error;
501
+ }
502
+ }
503
+ stop() {
504
+ for (const sessionId of Array.from(this.sessions.keys())) {
505
+ const session = this.sessions.get(sessionId);
506
+ if (session) {
507
+ void session.cancel().catch((error) => {
508
+ this.lastError = error;
509
+ });
510
+ }
511
+ this.dropSession(sessionId, "bridge_stopped");
512
+ }
513
+ this.latestAvailableCommandsSessionId = void 0;
514
+ this.connected = false;
515
+ }
516
+ get isConnected() {
517
+ return this.connected;
518
+ }
519
+ attachSession(session) {
520
+ if (this.sessions.has(session.sessionId)) {
521
+ this.dropSession(session.sessionId, "session_replaced");
522
+ }
523
+ this.sessions.set(session.sessionId, session);
524
+ const controller = new AbortController();
525
+ this.eventControllers.set(session.sessionId, controller);
526
+ void this.pumpEvents(session, controller.signal);
527
+ }
528
+ ensureSession(sessionId) {
529
+ const session = this.sessions.get(sessionId);
530
+ if (!session) {
531
+ throw new Error(`No daemon session bound for ${sessionId}`);
532
+ }
533
+ return session;
534
+ }
535
+ async pumpEvents(session, signal) {
536
+ try {
537
+ for await (const event of session.events({
538
+ signal,
539
+ lastEventId: session.lastEventId,
540
+ resume: true
541
+ })) {
542
+ if (!this.isCurrentPump(session, signal)) {
543
+ return;
544
+ }
545
+ this.handleEvent(session, event);
546
+ }
547
+ if (!signal.aborted && this.isCurrentPump(session, signal)) {
548
+ this.dropSession(session.sessionId, "stream_ended");
549
+ }
550
+ } catch (error) {
551
+ if (!signal.aborted && this.isCurrentPump(session, signal)) {
552
+ this.emit("error", error);
553
+ this.dropSession(session.sessionId, error instanceof Error ? error.message : String(error));
554
+ }
555
+ }
556
+ }
557
+ isCurrentPump(session, signal) {
558
+ return this.sessions.get(session.sessionId) === session && this.eventControllers.get(session.sessionId)?.signal === signal;
559
+ }
560
+ handleEvent(session, event) {
561
+ switch (event.type) {
562
+ case "session_update":
563
+ this.handleSessionUpdate(session.sessionId, event.data);
564
+ break;
565
+ case "permission_request":
566
+ this.handlePermissionRequest(session.sessionId, event.data);
567
+ break;
568
+ case "permission_resolved":
569
+ this.handlePermissionResolved(session.sessionId, event.data);
570
+ break;
571
+ case "model_switched":
572
+ this.handleModelSwitched(session.sessionId, event.data);
573
+ break;
574
+ case "model_switch_failed":
575
+ this.handleModelSwitchFailed(session.sessionId, event.data);
576
+ break;
577
+ case "session_died":
578
+ this.handleSessionDied(session.sessionId, event.data);
579
+ break;
580
+ case "client_evicted":
581
+ this.dropSession(session.sessionId, this.getStringField(event.data, "reason", "client_evicted"));
582
+ break;
583
+ case "stream_error":
584
+ this.dropSession(session.sessionId, this.getStringField(event.data, "error", "stream_error"));
585
+ break;
586
+ default:
587
+ break;
588
+ }
589
+ }
590
+ handleSessionUpdate(sessionId, data) {
591
+ const update = getSessionUpdate(data);
592
+ if (!update) {
593
+ this.emitProtocolError("Malformed daemon session_update event", data);
594
+ return;
595
+ }
596
+ const type = getString(update["sessionUpdate"]);
597
+ switch (type) {
598
+ case "agent_message_chunk": {
599
+ const text = getTextContent(update["content"]);
600
+ if (text) {
601
+ this.emit("textChunk", sessionId, text);
602
+ }
603
+ break;
604
+ }
605
+ case "agent_thought_chunk": {
606
+ const text = getTextContent(update["content"]);
607
+ if (text) {
608
+ this.emit("thoughtChunk", sessionId, text);
609
+ }
610
+ break;
611
+ }
612
+ case "tool_call":
613
+ case "tool_call_update": {
614
+ const toolCallId = getString(update["toolCallId"]);
615
+ const kind = getString(update["kind"]);
616
+ if (!toolCallId || !kind) {
617
+ this.emitProtocolError(`Malformed daemon ${type} event`, update);
618
+ break;
619
+ }
620
+ const event = {
621
+ sessionId,
622
+ toolCallId,
623
+ kind,
624
+ title: getString(update["title"]) ?? "",
625
+ status: getString(update["status"]) ?? "pending",
626
+ rawInput: isRecord(update["rawInput"]) ? update["rawInput"] : void 0
627
+ };
628
+ this.emit("toolCall", event);
629
+ break;
630
+ }
631
+ case "available_commands_update": {
632
+ if (Array.isArray(update["availableCommands"])) {
633
+ const commands = update["availableCommands"].filter(isAvailableCommand).map((cmd) => {
634
+ const altNames = readAvailableCommandAltNames(cmd);
635
+ return altNames ? { ...cmd, altNames } : cmd;
636
+ });
637
+ this.availableCommandsBySession.set(sessionId, commands);
638
+ this.latestAvailableCommandsSessionId = sessionId;
639
+ } else {
640
+ this.emitProtocolError("Malformed daemon available_commands_update event", data);
641
+ }
642
+ break;
643
+ }
644
+ default:
645
+ break;
646
+ }
647
+ this.emit("sessionUpdate", data);
648
+ }
649
+ handlePermissionRequest(sessionId, data) {
650
+ if (!isPermissionRequestData(data)) {
651
+ this.emitProtocolError("Malformed daemon permission_request event", data);
652
+ return;
653
+ }
654
+ const requestId = data["requestId"];
655
+ this.requestToSession.set(requestId, sessionId);
656
+ this.emit("permissionRequest", {
657
+ requestId,
658
+ sessionId,
659
+ request: data
660
+ });
661
+ }
662
+ rememberRespondedPermissionRequest(requestId, sessionId) {
663
+ this.respondedRequestToSession.set(requestId, sessionId);
664
+ while (this.respondedRequestToSession.size > MAX_RESPONDED_PERMISSION_REQUESTS) {
665
+ const oldestRequestId = this.respondedRequestToSession.keys().next().value;
666
+ if (oldestRequestId === void 0) {
667
+ return;
668
+ }
669
+ this.respondedRequestToSession.delete(oldestRequestId);
670
+ }
671
+ }
672
+ handlePermissionResolved(sessionId, data) {
673
+ if (!isRecord(data) || typeof data["requestId"] !== "string") {
674
+ this.emitProtocolError("Malformed daemon permission_resolved event", data);
675
+ return;
676
+ }
677
+ const requestId = data["requestId"];
678
+ const mappedSessionId = this.requestToSession.get(requestId) ?? this.respondedRequestToSession.get(requestId);
679
+ if (!mappedSessionId) {
680
+ this.emitProtocolError(`Ignoring daemon permission_resolved for unknown request ${requestId}`, data);
681
+ return;
682
+ }
683
+ if (mappedSessionId !== sessionId) {
684
+ this.requestToSession.delete(requestId);
685
+ this.respondedRequestToSession.delete(requestId);
686
+ this.emitProtocolError(`Ignoring daemon permission_resolved for request ${requestId} from non-owning session ${sessionId}`, data);
687
+ return;
688
+ }
689
+ const outcome = parsePermissionOutcome(data["outcome"]);
690
+ if (!outcome) {
691
+ this.requestToSession.delete(requestId);
692
+ this.respondedRequestToSession.delete(requestId);
693
+ this.emitProtocolError("Malformed daemon permission_resolved outcome", data);
694
+ return;
695
+ }
696
+ this.requestToSession.delete(requestId);
697
+ this.respondedRequestToSession.delete(requestId);
698
+ this.emit("permissionResolved", {
699
+ requestId,
700
+ outcome
701
+ });
702
+ }
703
+ handleModelSwitched(sessionId, data) {
704
+ if (!isRecord(data) || typeof data["modelId"] !== "string") {
705
+ this.emitProtocolError("Malformed daemon model_switched event", data);
706
+ return;
707
+ }
708
+ this.emit("modelSwitched", {
709
+ sessionId,
710
+ modelId: data["modelId"]
711
+ });
712
+ }
713
+ handleModelSwitchFailed(sessionId, data) {
714
+ if (!isRecord(data)) {
715
+ this.emitProtocolError("Malformed daemon model_switch_failed event", data);
716
+ return;
717
+ }
718
+ this.emit("modelSwitchFailed", {
719
+ sessionId,
720
+ requestedModelId: getString(data["requestedModelId"]),
721
+ error: getString(data["error"]) ?? "model_switch_failed"
722
+ });
723
+ }
724
+ handleSessionDied(sessionId, data) {
725
+ this.dropSession(sessionId, this.getStringField(data, "reason", "session_died"));
726
+ }
727
+ dropSession(sessionId, reason) {
728
+ if (!this.sessions.has(sessionId)) {
729
+ return;
730
+ }
731
+ this.eventControllers.get(sessionId)?.abort();
732
+ this.eventControllers.delete(sessionId);
733
+ this.sessions.delete(sessionId);
734
+ this.abortActivePrompts(sessionId);
735
+ this.activePrompts.delete(sessionId);
736
+ this.availableCommandsBySession.delete(sessionId);
737
+ if (this.latestAvailableCommandsSessionId === sessionId) {
738
+ this.latestAvailableCommandsSessionId = Array.from(this.availableCommandsBySession.keys()).at(-1);
739
+ }
740
+ for (const [requestId, mappedSessionId] of this.requestToSession) {
741
+ if (mappedSessionId === sessionId) {
742
+ this.requestToSession.delete(requestId);
743
+ }
744
+ }
745
+ for (const [requestId, mappedSessionId] of this.respondedRequestToSession) {
746
+ if (mappedSessionId === sessionId) {
747
+ this.respondedRequestToSession.delete(requestId);
748
+ }
749
+ }
750
+ this.emit("sessionDied", { sessionId, reason });
751
+ }
752
+ getStringField(data, field, fallback) {
753
+ return isRecord(data) && typeof data[field] === "string" ? data[field] : fallback;
754
+ }
755
+ abortActivePrompts(sessionId) {
756
+ const promptControllers = this.activePromptControllers.get(sessionId);
757
+ if (!promptControllers) {
758
+ return;
759
+ }
760
+ for (const controller of promptControllers) {
761
+ controller.abort();
762
+ }
763
+ this.activePromptControllers.delete(sessionId);
764
+ }
765
+ emitProtocolError(message, details) {
766
+ const error = new Error(message);
767
+ error.details = summarizeProtocolDetails(details);
768
+ this.emit("error", error);
769
+ }
770
+ };
771
+
772
+ // packages/channels/base/dist/BlockStreamer.js
773
+ init_esbuild_shims();
774
+ var BlockStreamer = class {
775
+ static {
776
+ __name(this, "BlockStreamer");
777
+ }
778
+ buffer = "";
779
+ idleTimer = null;
780
+ sending = Promise.resolve();
781
+ opts;
782
+ /** Number of blocks emitted so far. */
783
+ blockCount = 0;
784
+ constructor(opts) {
785
+ this.opts = opts;
786
+ }
787
+ /** Feed a new text chunk from the agent stream. */
788
+ push(chunk) {
789
+ this.buffer += chunk;
790
+ this.clearIdleTimer();
791
+ this.checkEmit();
792
+ if (this.buffer.length > 0 && this.opts.idleMs > 0) {
793
+ this.idleTimer = setTimeout(() => this.onIdle(), this.opts.idleMs);
794
+ }
795
+ }
796
+ /** Flush all remaining buffered text. Awaits all pending sends. */
797
+ async flush() {
798
+ this.clearIdleTimer();
799
+ if (this.buffer.length > 0) {
800
+ this.emitBlock(this.buffer);
801
+ this.buffer = "";
802
+ }
803
+ await this.sending;
804
+ }
805
+ /** Drop buffered text and cancel future idle emission. */
806
+ stop() {
807
+ this.clearIdleTimer();
808
+ this.buffer = "";
809
+ }
810
+ // ---------------------------------------------------------------------------
811
+ // Internal
812
+ // ---------------------------------------------------------------------------
813
+ checkEmit() {
814
+ while (this.buffer.length >= this.opts.maxChars) {
815
+ const bp = this.findBreakPoint(this.buffer, this.opts.maxChars);
816
+ this.emitBlock(this.buffer.slice(0, bp));
817
+ this.buffer = this.buffer.slice(bp);
818
+ }
819
+ if (this.buffer.length >= this.opts.minChars) {
820
+ const bp = this.findBlockBoundary(this.buffer);
821
+ if (bp > 0) {
822
+ this.emitBlock(this.buffer.slice(0, bp));
823
+ this.buffer = this.buffer.slice(bp);
824
+ }
825
+ }
826
+ }
827
+ onIdle() {
828
+ this.idleTimer = null;
829
+ if (this.buffer.length >= this.opts.minChars) {
830
+ this.emitBlock(this.buffer);
831
+ this.buffer = "";
832
+ }
833
+ }
834
+ emitBlock(text) {
835
+ const trimmed = text.trim();
836
+ if (!trimmed)
837
+ return;
838
+ this.blockCount++;
839
+ this.sending = this.sending.then(() => this.opts.send(trimmed)).catch(() => {
840
+ });
841
+ }
842
+ /**
843
+ * Find the last paragraph boundary (\n\n) in the buffer.
844
+ * Returns the position after the boundary, or -1 if no suitable boundary
845
+ * exists at or after minChars.
846
+ */
847
+ findBlockBoundary(text) {
848
+ const last = text.lastIndexOf("\n\n");
849
+ if (last < 0 || last < this.opts.minChars)
850
+ return -1;
851
+ return last + 2;
852
+ }
853
+ /**
854
+ * Find the best break point at or before maxPos.
855
+ * Prefers paragraph break > newline > space > maxPos.
856
+ */
857
+ findBreakPoint(text, maxPos) {
858
+ const sub = text.slice(0, maxPos);
859
+ const para = sub.lastIndexOf("\n\n");
860
+ if (para > 0)
861
+ return para + 2;
862
+ const nl = sub.lastIndexOf("\n");
863
+ if (nl > 0)
864
+ return nl + 1;
865
+ const sp = sub.lastIndexOf(" ");
866
+ if (sp > 0)
867
+ return sp + 1;
868
+ return maxPos;
869
+ }
870
+ clearIdleTimer() {
871
+ if (this.idleTimer !== null) {
872
+ clearTimeout(this.idleTimer);
873
+ this.idleTimer = null;
874
+ }
875
+ }
876
+ };
877
+
878
+ // packages/channels/base/dist/ChannelBase.js
879
+ init_esbuild_shims();
880
+ import { basename, join as join4 } from "node:path";
881
+
882
+ // packages/channels/base/dist/GroupGate.js
883
+ init_esbuild_shims();
884
+ var GroupGate = class {
885
+ static {
886
+ __name(this, "GroupGate");
887
+ }
888
+ policy;
889
+ groups;
890
+ constructor(policy = "disabled", groups = {}) {
891
+ this.policy = policy;
892
+ this.groups = groups;
893
+ }
894
+ /**
895
+ * Full group check: policy + allowlist + mention gating.
896
+ * Evaluation order:
897
+ * 1. groupPolicy (disabled → drop)
898
+ * 2. group allowlist (allowlist mode, no match → drop)
899
+ * 3. mention gating (requireMention + not mentioned → drop silently)
900
+ *
901
+ * Mention gating runs before sender gate so that unmentioned messages
902
+ * in groups don't trigger pairing flows.
903
+ */
904
+ check(envelope) {
905
+ if (!envelope.isGroup) {
906
+ return { allowed: true };
907
+ }
908
+ if (this.policy === "disabled") {
909
+ return { allowed: false, reason: "disabled" };
910
+ }
911
+ if (this.policy === "allowlist") {
912
+ if (!this.groups[envelope.chatId]) {
913
+ return { allowed: false, reason: "not_allowlisted" };
914
+ }
915
+ }
916
+ const groupConfig = this.groups[envelope.chatId] || this.groups["*"] || {};
917
+ const requireMention = groupConfig.requireMention ?? true;
918
+ if (requireMention && !envelope.isMentioned && !envelope.isReplyToBot) {
919
+ return { allowed: false, reason: "mention_required" };
920
+ }
921
+ return { allowed: true };
922
+ }
923
+ };
924
+
925
+ // packages/channels/base/dist/group-history-store.js
926
+ init_esbuild_shims();
927
+ import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
928
+ import { dirname, join as join2 } from "node:path";
929
+ var DEFAULT_MAX_KEYS = 1e3;
930
+ var DEFAULT_COMPACT_AFTER_RECORDS = 1e3;
931
+ var GroupHistoryStore = class {
932
+ static {
933
+ __name(this, "GroupHistoryStore");
934
+ }
935
+ filePath;
936
+ maxKeys;
937
+ compactAfterRecords;
938
+ constructor(filePath, options = {}) {
939
+ this.filePath = filePath;
940
+ this.maxKeys = options.maxKeys ?? DEFAULT_MAX_KEYS;
941
+ this.compactAfterRecords = options.compactAfterRecords ?? DEFAULT_COMPACT_AFTER_RECORDS;
942
+ }
943
+ record(key, entry, limit) {
944
+ const normalizedLimit = normalizeLimit(limit);
945
+ if (normalizedLimit <= 0) {
946
+ return;
947
+ }
948
+ const loaded = this.loadState();
949
+ const state = loaded.entries;
950
+ const limits = loaded.limits;
951
+ const current = state.get(key) ?? [];
952
+ current.push(entry);
953
+ if (current.length > normalizedLimit) {
954
+ current.splice(0, current.length - normalizedLimit);
955
+ }
956
+ state.delete(key);
957
+ state.set(key, current);
958
+ limits.set(key, normalizedLimit);
959
+ const evicted = evictOldKeys(state, this.maxKeys, limits);
960
+ this.append({
961
+ type: "message",
962
+ key,
963
+ limit: normalizedLimit,
964
+ entry,
965
+ recordedAt: Date.now()
966
+ });
967
+ if (evicted || loaded.hadInvalidRecords || loaded.recordCount + 1 >= this.compactAfterRecords) {
968
+ this.compact(state, limits);
969
+ }
970
+ }
971
+ drain(key, limit) {
972
+ const normalizedLimit = normalizeLimit(limit);
973
+ const loaded = this.loadState();
974
+ const state = loaded.entries;
975
+ const entries = normalizedLimit > 0 ? (state.get(key) ?? []).slice(-normalizedLimit) : [];
976
+ if (state.has(key)) {
977
+ state.delete(key);
978
+ loaded.limits.delete(key);
979
+ this.append({ type: "clear", key, recordedAt: Date.now() });
980
+ }
981
+ return entries;
982
+ }
983
+ clear(key) {
984
+ const loaded = this.loadState();
985
+ const state = loaded.entries;
986
+ if (!state.has(key)) {
987
+ return;
988
+ }
989
+ state.delete(key);
990
+ loaded.limits.delete(key);
991
+ this.append({ type: "clear", key, recordedAt: Date.now() });
992
+ this.compact(state, loaded.limits);
993
+ }
994
+ clearAll() {
995
+ const loaded = this.loadState();
996
+ if (loaded.entries.size === 0) {
997
+ return;
998
+ }
999
+ const recordedAt = Date.now();
1000
+ for (const key of loaded.entries.keys()) {
1001
+ this.append({ type: "clear", key, recordedAt });
1002
+ }
1003
+ this.compact(/* @__PURE__ */ new Map(), /* @__PURE__ */ new Map());
1004
+ }
1005
+ size(key) {
1006
+ const state = this.loadState().entries;
1007
+ if (key !== void 0) {
1008
+ return state.get(key)?.length ?? 0;
1009
+ }
1010
+ return state.size;
1011
+ }
1012
+ loadState() {
1013
+ const state = /* @__PURE__ */ new Map();
1014
+ const limits = /* @__PURE__ */ new Map();
1015
+ const read = this.readRecords();
1016
+ for (const record of read.records) {
1017
+ if (record.type === "clear") {
1018
+ state.delete(record.key);
1019
+ limits.delete(record.key);
1020
+ continue;
1021
+ }
1022
+ const current = state.get(record.key) ?? [];
1023
+ current.push(record.entry);
1024
+ if (current.length > record.limit) {
1025
+ current.splice(0, current.length - record.limit);
1026
+ }
1027
+ state.delete(record.key);
1028
+ state.set(record.key, current);
1029
+ limits.set(record.key, record.limit);
1030
+ evictOldKeys(state, this.maxKeys, limits);
1031
+ }
1032
+ return {
1033
+ entries: state,
1034
+ limits,
1035
+ recordCount: read.records.length,
1036
+ hadInvalidRecords: read.hadInvalidRecords
1037
+ };
1038
+ }
1039
+ readRecords() {
1040
+ if (!existsSync(this.filePath)) {
1041
+ return { records: [], hadInvalidRecords: false };
1042
+ }
1043
+ let data;
1044
+ try {
1045
+ data = readFileSync(this.filePath, "utf-8");
1046
+ } catch (err) {
1047
+ if (isErrnoCode(err, "ENOENT")) {
1048
+ return { records: [], hadInvalidRecords: false };
1049
+ }
1050
+ throw err;
1051
+ }
1052
+ const records = [];
1053
+ let hadInvalidRecords = false;
1054
+ for (const line of data.split("\n")) {
1055
+ if (line.trim().length === 0) {
1056
+ continue;
1057
+ }
1058
+ try {
1059
+ const parsed = JSON.parse(line);
1060
+ if (isGroupHistoryRecord(parsed)) {
1061
+ records.push(parsed);
1062
+ } else {
1063
+ hadInvalidRecords = true;
1064
+ }
1065
+ } catch {
1066
+ hadInvalidRecords = true;
1067
+ }
1068
+ }
1069
+ return { records, hadInvalidRecords };
1070
+ }
1071
+ append(record) {
1072
+ const dir = dirname(this.filePath);
1073
+ mkdirSync(dir, { recursive: true, mode: 448 });
1074
+ chmodPrivate(dir, 448);
1075
+ appendFileSync(this.filePath, `${JSON.stringify(record)}
1076
+ `, {
1077
+ encoding: "utf-8",
1078
+ mode: 384
1079
+ });
1080
+ chmodPrivate(this.filePath, 384);
1081
+ }
1082
+ compact(state, limits) {
1083
+ const dir = dirname(this.filePath);
1084
+ mkdirSync(dir, { recursive: true, mode: 448 });
1085
+ chmodPrivate(dir, 448);
1086
+ const records = [];
1087
+ const recordedAt = Date.now();
1088
+ for (const [key, entries] of state) {
1089
+ for (const entry of entries) {
1090
+ records.push({
1091
+ type: "message",
1092
+ key,
1093
+ limit: limits.get(key) ?? entries.length,
1094
+ entry,
1095
+ recordedAt
1096
+ });
1097
+ }
1098
+ }
1099
+ const data = records.length > 0 ? `${records.map((record) => JSON.stringify(record)).join("\n")}
1100
+ ` : "";
1101
+ const tempPath = join2(dir, `${Date.now()}-${process.pid}-${Math.random().toString(16).slice(2)}.tmp`);
1102
+ writeFileSync(tempPath, data, { encoding: "utf-8", mode: 384 });
1103
+ chmodPrivate(tempPath, 384);
1104
+ renameSync(tempPath, this.filePath);
1105
+ chmodPrivate(this.filePath, 384);
1106
+ }
1107
+ };
1108
+ function normalizeLimit(limit) {
1109
+ if (!Number.isFinite(limit) || limit <= 0) {
1110
+ return 0;
1111
+ }
1112
+ return Math.floor(limit);
1113
+ }
1114
+ __name(normalizeLimit, "normalizeLimit");
1115
+ function evictOldKeys(state, maxKeys, limits) {
1116
+ let evicted = false;
1117
+ while (state.size > maxKeys) {
1118
+ const oldest = state.keys().next().value;
1119
+ if (oldest === void 0) {
1120
+ return evicted;
1121
+ }
1122
+ state.delete(oldest);
1123
+ limits?.delete(oldest);
1124
+ evicted = true;
1125
+ }
1126
+ return evicted;
1127
+ }
1128
+ __name(evictOldKeys, "evictOldKeys");
1129
+ function isGroupHistoryRecord(value) {
1130
+ if (!isRecord2(value)) {
1131
+ return false;
1132
+ }
1133
+ if (value["type"] === "clear") {
1134
+ return typeof value["key"] === "string";
1135
+ }
1136
+ if (value["type"] !== "message") {
1137
+ return false;
1138
+ }
1139
+ return typeof value["key"] === "string" && typeof value["limit"] === "number" && isGroupHistoryEntry(value["entry"]);
1140
+ }
1141
+ __name(isGroupHistoryRecord, "isGroupHistoryRecord");
1142
+ function isGroupHistoryEntry(value) {
1143
+ return isRecord2(value) && typeof value["senderId"] === "string" && typeof value["senderName"] === "string" && typeof value["text"] === "string" && typeof value["timestamp"] === "number";
1144
+ }
1145
+ __name(isGroupHistoryEntry, "isGroupHistoryEntry");
1146
+ function isRecord2(value) {
1147
+ return typeof value === "object" && value !== null;
1148
+ }
1149
+ __name(isRecord2, "isRecord");
1150
+ function isErrnoCode(err, code) {
1151
+ return err instanceof Error && "code" in err && err.code === code;
1152
+ }
1153
+ __name(isErrnoCode, "isErrnoCode");
1154
+ function chmodPrivate(path4, mode) {
1155
+ try {
1156
+ chmodSync(path4, mode);
1157
+ } catch {
1158
+ }
1159
+ }
1160
+ __name(chmodPrivate, "chmodPrivate");
1161
+
1162
+ // packages/channels/base/dist/SenderGate.js
1163
+ init_esbuild_shims();
1164
+ var SenderGate = class {
1165
+ static {
1166
+ __name(this, "SenderGate");
1167
+ }
1168
+ policy;
1169
+ allowedUsers;
1170
+ pairingStore;
1171
+ constructor(policy, allowedUsers = [], pairingStore) {
1172
+ this.policy = policy;
1173
+ this.allowedUsers = new Set(allowedUsers);
1174
+ this.pairingStore = pairingStore || null;
1175
+ }
1176
+ isAllowed(senderId) {
1177
+ switch (this.policy) {
1178
+ case "open":
1179
+ return true;
1180
+ case "allowlist":
1181
+ return this.allowedUsers.has(senderId);
1182
+ case "pairing":
1183
+ return this.allowedUsers.has(senderId) || this.pairingStore?.isApproved(senderId) === true;
1184
+ default:
1185
+ throw new Error(`Unknown sender policy: ${this.policy}`);
1186
+ }
1187
+ }
1188
+ check(senderId, senderName) {
1189
+ switch (this.policy) {
1190
+ case "open":
1191
+ return { allowed: true };
1192
+ case "allowlist":
1193
+ return { allowed: this.allowedUsers.has(senderId) };
1194
+ case "pairing": {
1195
+ if (this.allowedUsers.has(senderId)) {
1196
+ return { allowed: true };
1197
+ }
1198
+ if (this.pairingStore?.isApproved(senderId)) {
1199
+ return { allowed: true };
1200
+ }
1201
+ const code = this.pairingStore?.createRequest(senderId, senderName || senderId);
1202
+ return { allowed: false, pairingCode: code ?? null };
1203
+ }
1204
+ default:
1205
+ throw new Error(`Unknown sender policy: ${this.policy}`);
1206
+ }
1207
+ }
1208
+ };
1209
+
1210
+ // packages/channels/base/dist/PairingStore.js
1211
+ init_esbuild_shims();
1212
+ import * as crypto from "node:crypto";
1213
+ import * as fs from "node:fs";
1214
+ import * as path2 from "node:path";
1215
+ var SAFE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
1216
+ var CODE_LENGTH = 8;
1217
+ var EXPIRY_MS = 60 * 60 * 1e3;
1218
+ var MAX_PENDING = 3;
1219
+ var PairingStore = class {
1220
+ static {
1221
+ __name(this, "PairingStore");
1222
+ }
1223
+ dir;
1224
+ pendingPath;
1225
+ allowlistPath;
1226
+ constructor(channelName) {
1227
+ this.dir = path2.join(getGlobalQwenDir(), "channels");
1228
+ this.pendingPath = path2.join(this.dir, `${channelName}-pairing.json`);
1229
+ this.allowlistPath = path2.join(this.dir, `${channelName}-allowlist.json`);
1230
+ }
1231
+ isApproved(senderId) {
1232
+ const list = this.readAllowlist();
1233
+ return list.includes(senderId);
1234
+ }
1235
+ /**
1236
+ * Create a pairing request for an unknown sender.
1237
+ * Returns the code if created, or null if the pending cap is reached.
1238
+ * If the sender already has a non-expired pending request, returns that code.
1239
+ */
1240
+ createRequest(senderId, senderName) {
1241
+ const pending = this.readPending();
1242
+ const now = Date.now();
1243
+ const active = pending.filter((r) => now - r.createdAt < EXPIRY_MS);
1244
+ const existing = active.find((r) => r.senderId === senderId);
1245
+ if (existing) {
1246
+ return existing.code;
1247
+ }
1248
+ if (active.length >= MAX_PENDING) {
1249
+ return null;
1250
+ }
1251
+ const code = generateCode();
1252
+ active.push({ senderId, senderName, code, createdAt: now });
1253
+ this.writePending(active);
1254
+ return code;
1255
+ }
1256
+ /**
1257
+ * Approve a pairing request by code.
1258
+ * Returns the sender ID if found, or null if not found / expired.
1259
+ */
1260
+ approve(code) {
1261
+ const pending = this.readPending();
1262
+ const now = Date.now();
1263
+ const idx = pending.findIndex((r) => r.code === code.toUpperCase() && now - r.createdAt < EXPIRY_MS);
1264
+ if (idx === -1)
1265
+ return null;
1266
+ const request = pending[idx];
1267
+ pending.splice(idx, 1);
1268
+ this.writePending(pending);
1269
+ const list = this.readAllowlist();
1270
+ if (!list.includes(request.senderId)) {
1271
+ list.push(request.senderId);
1272
+ this.writeAllowlist(list);
1273
+ }
1274
+ return request;
1275
+ }
1276
+ listPending() {
1277
+ const pending = this.readPending();
1278
+ const now = Date.now();
1279
+ return pending.filter((r) => now - r.createdAt < EXPIRY_MS);
1280
+ }
1281
+ getAllowlist() {
1282
+ return this.readAllowlist();
1283
+ }
1284
+ ensureDir() {
1285
+ if (!fs.existsSync(this.dir)) {
1286
+ fs.mkdirSync(this.dir, { recursive: true });
1287
+ }
1288
+ }
1289
+ readPending() {
1290
+ try {
1291
+ const data = fs.readFileSync(this.pendingPath, "utf-8");
1292
+ return JSON.parse(data);
1293
+ } catch {
1294
+ return [];
1295
+ }
1296
+ }
1297
+ writePending(requests) {
1298
+ this.ensureDir();
1299
+ fs.writeFileSync(this.pendingPath, JSON.stringify(requests, null, 2));
1300
+ }
1301
+ readAllowlist() {
1302
+ try {
1303
+ const data = fs.readFileSync(this.allowlistPath, "utf-8");
1304
+ return JSON.parse(data);
1305
+ } catch {
1306
+ return [];
1307
+ }
1308
+ }
1309
+ writeAllowlist(list) {
1310
+ this.ensureDir();
1311
+ fs.writeFileSync(this.allowlistPath, JSON.stringify(list, null, 2));
1312
+ }
1313
+ };
1314
+ function generateCode() {
1315
+ let code = "";
1316
+ for (let i = 0; i < CODE_LENGTH; i++) {
1317
+ code += SAFE_ALPHABET[crypto.randomInt(SAFE_ALPHABET.length)];
1318
+ }
1319
+ return code;
1320
+ }
1321
+ __name(generateCode, "generateCode");
1322
+
1323
+ // packages/channels/base/dist/SessionRouter.js
1324
+ init_esbuild_shims();
1325
+ import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync } from "node:fs";
1326
+ import process2 from "node:process";
1327
+ var SessionRouter = class {
1328
+ static {
1329
+ __name(this, "SessionRouter");
1330
+ }
1331
+ toSession = /* @__PURE__ */ new Map();
1332
+ // routing key → session ID
1333
+ toTarget = /* @__PURE__ */ new Map();
1334
+ // session ID → target
1335
+ toCwd = /* @__PURE__ */ new Map();
1336
+ // session ID → cwd
1337
+ creatingSessions = /* @__PURE__ */ new Map();
1338
+ sessionLoadWindows = /* @__PURE__ */ new Set();
1339
+ bridge;
1340
+ defaultCwd;
1341
+ defaultScope;
1342
+ channelScopes = /* @__PURE__ */ new Map();
1343
+ persistPath;
1344
+ constructor(bridge, defaultCwd, scope = "user", persistPath) {
1345
+ this.bridge = bridge;
1346
+ this.defaultCwd = defaultCwd;
1347
+ this.defaultScope = scope;
1348
+ this.persistPath = persistPath;
1349
+ }
1350
+ /** Replace the bridge instance (used after crash recovery restart). */
1351
+ setBridge(bridge) {
1352
+ this.bridge = bridge;
1353
+ }
1354
+ /** Set scope override for a specific channel. */
1355
+ setChannelScope(channelName, scope) {
1356
+ this.channelScopes.set(channelName, scope);
1357
+ }
1358
+ routingKey(channelName, senderId, chatId, threadId) {
1359
+ const scope = this.channelScopes.get(channelName) || this.defaultScope;
1360
+ switch (scope) {
1361
+ case "thread":
1362
+ return `${channelName}:${threadId || chatId}`;
1363
+ case "single":
1364
+ return `${channelName}:__single__`;
1365
+ case "user":
1366
+ default:
1367
+ return `${channelName}:${senderId}:${chatId}`;
1368
+ }
1369
+ }
1370
+ async resolve(channelName, senderId, chatId, threadId, cwd) {
1371
+ const key = this.routingKey(channelName, senderId, chatId, threadId);
1372
+ let failedCreateWaits = 0;
1373
+ for (; ; ) {
1374
+ const existing = this.toSession.get(key);
1375
+ if (existing) {
1376
+ return existing;
1377
+ }
1378
+ const creating = this.creatingSessions.get(key);
1379
+ if (creating) {
1380
+ try {
1381
+ return await creating;
1382
+ } catch (err) {
1383
+ if (this.creatingSessions.get(key) === creating) {
1384
+ this.creatingSessions.delete(key);
1385
+ }
1386
+ failedCreateWaits++;
1387
+ if (failedCreateWaits > 3) {
1388
+ throw err;
1389
+ }
1390
+ continue;
1391
+ }
1392
+ }
1393
+ const created = Promise.resolve().then(async () => {
1394
+ const sessionCwd = cwd || this.defaultCwd;
1395
+ const loadWindow = this.beginSessionLoad();
1396
+ try {
1397
+ const sessionId = await this.createLiveSession(sessionCwd, loadWindow, key);
1398
+ this.toSession.set(key, sessionId);
1399
+ this.toTarget.set(sessionId, {
1400
+ channelName,
1401
+ senderId,
1402
+ chatId,
1403
+ threadId
1404
+ });
1405
+ this.toCwd.set(sessionId, sessionCwd);
1406
+ this.persist();
1407
+ return sessionId;
1408
+ } finally {
1409
+ this.endSessionLoad(loadWindow);
1410
+ }
1411
+ });
1412
+ this.creatingSessions.set(key, created);
1413
+ try {
1414
+ return await created;
1415
+ } finally {
1416
+ if (this.creatingSessions.get(key) === created) {
1417
+ this.creatingSessions.delete(key);
1418
+ }
1419
+ }
1420
+ }
1421
+ }
1422
+ getTarget(sessionId) {
1423
+ return this.toTarget.get(sessionId);
1424
+ }
1425
+ getSession(channelName, senderId, chatId, threadId) {
1426
+ return this.toSession.get(this.routingKey(channelName, senderId, chatId, threadId));
1427
+ }
1428
+ hasSession(channelName, senderId, chatId, threadId) {
1429
+ const scope = this.channelScopes.get(channelName) || this.defaultScope;
1430
+ if (chatId) {
1431
+ return this.toSession.has(this.routingKey(channelName, senderId, chatId, threadId));
1432
+ }
1433
+ if (scope === "single") {
1434
+ return false;
1435
+ }
1436
+ for (const target of this.toTarget.values()) {
1437
+ if (target.channelName === channelName && target.senderId === senderId) {
1438
+ return true;
1439
+ }
1440
+ }
1441
+ return false;
1442
+ }
1443
+ /**
1444
+ * Remove session(s) for the given sender. Returns the removed session IDs.
1445
+ */
1446
+ removeSession(channelName, senderId, chatId, threadId) {
1447
+ const removedIds = [];
1448
+ const scope = this.channelScopes.get(channelName) || this.defaultScope;
1449
+ if (chatId) {
1450
+ const key = this.routingKey(channelName, senderId, chatId, threadId);
1451
+ const sessionId = this.deleteByKey(key);
1452
+ if (sessionId)
1453
+ removedIds.push(sessionId);
1454
+ } else if (scope === "single") {
1455
+ return removedIds;
1456
+ } else {
1457
+ for (const [k, mappedSessionId] of [...this.toSession.entries()]) {
1458
+ const target = this.toTarget.get(mappedSessionId);
1459
+ if (target?.channelName === channelName && target.senderId === senderId) {
1460
+ const sessionId = this.deleteByKey(k);
1461
+ if (sessionId)
1462
+ removedIds.push(sessionId);
1463
+ }
1464
+ }
1465
+ }
1466
+ if (removedIds.length > 0)
1467
+ this.persist();
1468
+ return removedIds;
1469
+ }
1470
+ /** Remove a session mapping by daemon/ACP session ID. */
1471
+ removeSessionId(sessionId) {
1472
+ let removed = false;
1473
+ for (const [key, mappedSessionId] of [...this.toSession.entries()]) {
1474
+ if (mappedSessionId === sessionId) {
1475
+ this.toSession.delete(key);
1476
+ removed = true;
1477
+ }
1478
+ }
1479
+ if (this.toTarget.delete(sessionId)) {
1480
+ removed = true;
1481
+ }
1482
+ if (this.toCwd.delete(sessionId)) {
1483
+ removed = true;
1484
+ }
1485
+ if (!removed && this.sessionLoadWindows.size > 0) {
1486
+ for (const loadWindow of this.sessionLoadWindows) {
1487
+ loadWindow.add(sessionId);
1488
+ }
1489
+ }
1490
+ if (removed) {
1491
+ this.persist();
1492
+ }
1493
+ return removed;
1494
+ }
1495
+ deleteByKey(key) {
1496
+ const sessionId = this.toSession.get(key);
1497
+ if (!sessionId)
1498
+ return null;
1499
+ this.toSession.delete(key);
1500
+ this.toTarget.delete(sessionId);
1501
+ this.toCwd.delete(sessionId);
1502
+ return sessionId;
1503
+ }
1504
+ /** Get all session entries for crash recovery. */
1505
+ getAll() {
1506
+ const entries = [];
1507
+ for (const [key, sessionId] of this.toSession) {
1508
+ const target = this.toTarget.get(sessionId);
1509
+ if (target) {
1510
+ entries.push({ key, sessionId, target });
1511
+ }
1512
+ }
1513
+ return entries;
1514
+ }
1515
+ /**
1516
+ * Restore session mappings from a previous bridge.
1517
+ * Called after bridge restart — attempts loadSession for each saved mapping.
1518
+ * Failed loads are dropped (new session on next message).
1519
+ */
1520
+ async restoreSessions() {
1521
+ const persistPath = this.persistPath;
1522
+ if (!persistPath || !existsSync3(persistPath)) {
1523
+ return { restored: 0, failed: 0 };
1524
+ }
1525
+ let entries;
1526
+ try {
1527
+ entries = JSON.parse(readFileSync3(persistPath, "utf-8"));
1528
+ } catch (err) {
1529
+ const reason = err instanceof Error ? err.message : String(err);
1530
+ process2.stderr.write(`[SessionRouter] Corrupted persist file at ${sanitizeLogText(persistPath, 1024)}: ${sanitizeLogText(reason, 512)}
1531
+ `);
1532
+ return { restored: 0, failed: 0 };
1533
+ }
1534
+ let restored = 0;
1535
+ let failed = 0;
1536
+ let changed = false;
1537
+ const reservations = /* @__PURE__ */ new Map();
1538
+ for (const key of Object.keys(entries)) {
1539
+ this.deleteByKey(key);
1540
+ const reservation = this.createSessionReservation();
1541
+ reservation.promise.catch(() => void 0);
1542
+ this.creatingSessions.set(key, reservation.promise);
1543
+ reservations.set(key, reservation);
1544
+ }
1545
+ const loadWindow = this.beginSessionLoad();
1546
+ try {
1547
+ for (const [key, entry] of Object.entries(entries)) {
1548
+ const reservation = reservations.get(key);
1549
+ if (!reservation)
1550
+ continue;
1551
+ try {
1552
+ const sessionId = await this.bridge.loadSession(entry.sessionId, entry.cwd);
1553
+ if (typeof sessionId !== "string" || sessionId.length === 0) {
1554
+ throw new Error("Invalid restored session ID");
1555
+ }
1556
+ if (loadWindow.delete(sessionId)) {
1557
+ throw new Error("Restored session died before routing completed");
1558
+ }
1559
+ this.toSession.set(key, sessionId);
1560
+ this.toTarget.set(sessionId, entry.target);
1561
+ this.toCwd.set(sessionId, entry.cwd);
1562
+ reservation.resolve(sessionId);
1563
+ if (sessionId !== entry.sessionId) {
1564
+ changed = true;
1565
+ }
1566
+ restored++;
1567
+ } catch (err) {
1568
+ const reason = err instanceof Error ? err.message : String(err);
1569
+ process2.stderr.write(`[SessionRouter] Failed to restore session ${sanitizeLogText(entry.sessionId, 128)} for key ${sanitizeLogText(key, 256)}: ${sanitizeLogText(reason, 512)}
1570
+ `);
1571
+ reservation.reject(new Error("Session restore failed", { cause: err }));
1572
+ failed++;
1573
+ changed = true;
1574
+ } finally {
1575
+ if (this.creatingSessions.get(key) === reservation.promise) {
1576
+ this.creatingSessions.delete(key);
1577
+ }
1578
+ }
1579
+ }
1580
+ } finally {
1581
+ this.endSessionLoad(loadWindow);
1582
+ }
1583
+ if (changed) {
1584
+ this.persist();
1585
+ }
1586
+ return { restored, failed };
1587
+ }
1588
+ /** Clear in-memory state and delete persist file. Used on clean shutdown. */
1589
+ clearAll() {
1590
+ this.toSession.clear();
1591
+ this.toTarget.clear();
1592
+ this.toCwd.clear();
1593
+ this.creatingSessions.clear();
1594
+ this.sessionLoadWindows.clear();
1595
+ if (this.persistPath && existsSync3(this.persistPath)) {
1596
+ try {
1597
+ unlinkSync(this.persistPath);
1598
+ } catch {
1599
+ }
1600
+ }
1601
+ }
1602
+ persist() {
1603
+ if (!this.persistPath)
1604
+ return;
1605
+ const data = {};
1606
+ for (const [key, sessionId] of this.toSession) {
1607
+ const target = this.toTarget.get(sessionId);
1608
+ if (target) {
1609
+ data[key] = {
1610
+ sessionId,
1611
+ target,
1612
+ cwd: this.toCwd.get(sessionId) || this.defaultCwd
1613
+ };
1614
+ }
1615
+ }
1616
+ try {
1617
+ writeFileSync3(this.persistPath, JSON.stringify(data, null, 2), "utf-8");
1618
+ } catch {
1619
+ }
1620
+ }
1621
+ async createLiveSession(cwd, loadWindow, routingKey) {
1622
+ const maxAttempts = 2;
1623
+ let lastDeadSessionId;
1624
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
1625
+ const sessionId = await this.bridge.newSession(cwd);
1626
+ if (typeof sessionId !== "string" || sessionId.length === 0) {
1627
+ throw new Error("Invalid session ID from bridge");
1628
+ }
1629
+ if (!loadWindow.delete(sessionId)) {
1630
+ return sessionId;
1631
+ }
1632
+ lastDeadSessionId = sessionId;
1633
+ }
1634
+ throw new Error(`Session ${lastDeadSessionId ?? "unknown"} died before routing completed (${maxAttempts}/${maxAttempts} attempts, key ${routingKey})`);
1635
+ }
1636
+ beginSessionLoad() {
1637
+ const loadWindow = /* @__PURE__ */ new Set();
1638
+ this.sessionLoadWindows.add(loadWindow);
1639
+ return loadWindow;
1640
+ }
1641
+ createSessionReservation() {
1642
+ let resolveReservation;
1643
+ let rejectReservation;
1644
+ const promise = new Promise((resolve2, reject) => {
1645
+ resolveReservation = resolve2;
1646
+ rejectReservation = reject;
1647
+ });
1648
+ return {
1649
+ promise,
1650
+ resolve: resolveReservation,
1651
+ reject: rejectReservation
1652
+ };
1653
+ }
1654
+ endSessionLoad(loadWindow) {
1655
+ this.sessionLoadWindows.delete(loadWindow);
1656
+ }
1657
+ };
1658
+
1659
+ // packages/channels/base/dist/ChannelLoopScheduler.js
1660
+ init_esbuild_shims();
1661
+ var MAX_RESULT_PREVIEW_LENGTH = 500;
1662
+ var MAX_CONCURRENT_LOOP_FIRES = 5;
1663
+ var ChannelLoopSkippedError = class extends Error {
1664
+ static {
1665
+ __name(this, "ChannelLoopSkippedError");
1666
+ }
1667
+ };
1668
+ var ChannelLoopScheduler = class {
1669
+ static {
1670
+ __name(this, "ChannelLoopScheduler");
1671
+ }
1672
+ store;
1673
+ channels;
1674
+ nextFireTime;
1675
+ now;
1676
+ maxConsecutiveFailures;
1677
+ intervalMs;
1678
+ loopTimeoutMs;
1679
+ timer;
1680
+ runningTick;
1681
+ inFlightJobs = /* @__PURE__ */ new Map();
1682
+ generation = 0;
1683
+ constructor(options) {
1684
+ this.store = options.store;
1685
+ this.channels = options.channels;
1686
+ this.nextFireTime = options.nextFireTime;
1687
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
1688
+ this.maxConsecutiveFailures = options.maxConsecutiveFailures ?? 5;
1689
+ this.intervalMs = options.intervalMs ?? 6e4;
1690
+ this.loopTimeoutMs = options.loopTimeoutMs ?? 5 * 6e4;
1691
+ }
1692
+ start() {
1693
+ if (this.timer)
1694
+ return;
1695
+ const generation = this.generation;
1696
+ const startup = this.reconcileStartupState();
1697
+ void startup.then(() => {
1698
+ if (!this.timer || this.generation !== generation)
1699
+ return;
1700
+ return this.tick();
1701
+ }).catch((err) => {
1702
+ process.stderr.write(`[scheduler] initial tick failed: ${err}
1703
+ `);
1704
+ });
1705
+ this.timer = setInterval(() => {
1706
+ void this.tick().catch((err) => {
1707
+ process.stderr.write(`[scheduler] interval tick failed: ${err}
1708
+ `);
1709
+ });
1710
+ }, this.intervalMs);
1711
+ this.timer.unref?.();
1712
+ }
1713
+ stop() {
1714
+ if (this.timer) {
1715
+ clearInterval(this.timer);
1716
+ }
1717
+ this.timer = void 0;
1718
+ this.runningTick = void 0;
1719
+ this.generation++;
1720
+ this.inFlightJobs.clear();
1721
+ }
1722
+ async reconcileStartupState() {
1723
+ const jobs = await this.store.list();
1724
+ const staleRunning = jobs.filter((job) => job.runningSince);
1725
+ for (const job of staleRunning) {
1726
+ await this.clearRunningSince(job.id);
1727
+ }
1728
+ const enabledCount = jobs.filter((job) => job.enabled).length;
1729
+ process.stderr.write(`[scheduler] started, tick interval ${this.intervalMs}ms, jobs ${jobs.length}, enabled ${enabledCount}, cleared stale running ${staleRunning.length}
1730
+ `);
1731
+ }
1732
+ async tick() {
1733
+ if (this.runningTick)
1734
+ return this.runningTick;
1735
+ const tick = this.runTick().finally(() => {
1736
+ if (this.runningTick === tick) {
1737
+ this.runningTick = void 0;
1738
+ }
1739
+ });
1740
+ this.runningTick = tick;
1741
+ return this.runningTick;
1742
+ }
1743
+ async runTick() {
1744
+ const generation = this.generation;
1745
+ const now = this.now();
1746
+ const jobs = await this.store.list();
1747
+ if (this.generation !== generation) {
1748
+ return;
1749
+ }
1750
+ const dueJobs = jobs.filter((job) => job.enabled && this.channels.has(job.channelName) && !this.inFlightJobs.has(job.id) && this.isDue(job, now));
1751
+ const availableSlots = MAX_CONCURRENT_LOOP_FIRES - this.inFlightJobs.size;
1752
+ if (availableSlots <= 0)
1753
+ return;
1754
+ for (const job of dueJobs.slice(0, availableSlots)) {
1755
+ void this.fireOnce(job, now, generation);
1756
+ }
1757
+ }
1758
+ isDue(job, now) {
1759
+ try {
1760
+ const after = new Date(lastAnchor(job));
1761
+ return this.nextFireTime(job.cron, after).getTime() <= now.getTime();
1762
+ } catch (err) {
1763
+ process.stderr.write(`[scheduler] invalid cron for loop ${job.id}: ${err}
1764
+ `);
1765
+ return false;
1766
+ }
1767
+ }
1768
+ async fireOnce(job, now, generation) {
1769
+ const token = Symbol(job.id);
1770
+ this.inFlightJobs.set(job.id, token);
1771
+ try {
1772
+ await this.fire(job, now, generation);
1773
+ } catch (err) {
1774
+ process.stderr.write(`[scheduler] unhandled error for loop ${job.id}: ${err instanceof Error ? err.message : String(err)}
1775
+ `);
1776
+ } finally {
1777
+ if (this.inFlightJobs.get(job.id) === token) {
1778
+ this.inFlightJobs.delete(job.id);
1779
+ }
1780
+ }
1781
+ }
1782
+ async fire(job, now, generation) {
1783
+ const channel = this.channels.get(job.channelName);
1784
+ if (!channel) {
1785
+ return;
1786
+ }
1787
+ const latestJob = await this.findJob(job.id);
1788
+ if (!latestJob?.enabled)
1789
+ return;
1790
+ if (this.generation !== generation)
1791
+ return;
1792
+ const runningSince = now.toISOString();
1793
+ let resultPreview;
1794
+ try {
1795
+ await this.store.update(latestJob.id, {
1796
+ runningSince,
1797
+ lastFiredAt: runningSince
1798
+ });
1799
+ if (this.generation !== generation) {
1800
+ await this.clearRunningSince(latestJob.id, runningSince);
1801
+ return;
1802
+ }
1803
+ resultPreview = await channel.runLoopPrompt(latestJob, {
1804
+ timeoutMs: this.loopTimeoutMs,
1805
+ shouldContinue: /* @__PURE__ */ __name(async () => {
1806
+ if (this.generation !== generation) {
1807
+ return false;
1808
+ }
1809
+ const currentJob2 = await this.findJob(latestJob.id);
1810
+ return currentJob2?.enabled === true;
1811
+ }, "shouldContinue")
1812
+ });
1813
+ } catch (err) {
1814
+ if (err instanceof ChannelLoopSkippedError) {
1815
+ await this.recordSkipped(latestJob.id, runningSince);
1816
+ return;
1817
+ }
1818
+ let currentJob2;
1819
+ try {
1820
+ currentJob2 = await this.findJob(latestJob.id);
1821
+ } catch (findErr) {
1822
+ process.stderr.write(`[scheduler] findJob failed in catch for loop ${latestJob.id}: ${findErr instanceof Error ? findErr.message : String(findErr)}
1823
+ `);
1824
+ await this.clearRunningSince(latestJob.id, runningSince);
1825
+ return;
1826
+ }
1827
+ if (this.generation !== generation || !currentJob2?.enabled) {
1828
+ await this.clearRunningSince(latestJob.id, runningSince);
1829
+ return;
1830
+ }
1831
+ await this.recordFailure(currentJob2, now, err instanceof Error ? err.message : String(err));
1832
+ return;
1833
+ }
1834
+ const currentJob = await this.findJob(latestJob.id);
1835
+ if (!currentJob || currentJob.runningSince !== runningSince) {
1836
+ return;
1837
+ }
1838
+ const finishedAt = this.now();
1839
+ const patch = {
1840
+ lastFiredAt: runningSince,
1841
+ lastFinishedAt: finishedAt.toISOString(),
1842
+ lastResultPreview: truncateResultPreview(resultPreview),
1843
+ lastStatus: "ok",
1844
+ lastError: void 0,
1845
+ consecutiveFailures: 0,
1846
+ runningSince: void 0,
1847
+ runCount: currentJob.runCount + 1
1848
+ };
1849
+ if (!currentJob.recurring) {
1850
+ patch.enabled = false;
1851
+ }
1852
+ try {
1853
+ await this.store.update(latestJob.id, patch);
1854
+ } catch (err) {
1855
+ process.stderr.write(`[scheduler] loop ${latestJob.id} succeeded but status persist failed: ${err instanceof Error ? err.message : String(err)}
1856
+ `);
1857
+ await this.clearRunningSince(latestJob.id, runningSince);
1858
+ }
1859
+ }
1860
+ async findJob(id) {
1861
+ const jobs = await this.store.list();
1862
+ return jobs.find((job) => job.id === id);
1863
+ }
1864
+ async clearRunningSince(id, expectedRunningSince) {
1865
+ try {
1866
+ if (expectedRunningSince) {
1867
+ const currentJob = await this.findJob(id);
1868
+ if (currentJob?.runningSince !== expectedRunningSince)
1869
+ return;
1870
+ }
1871
+ await this.store.update(id, { runningSince: void 0 });
1872
+ } catch (err) {
1873
+ process.stderr.write(`[scheduler] failed to clear running state for loop ${id}: ${err instanceof Error ? err.message : String(err)}
1874
+ `);
1875
+ }
1876
+ }
1877
+ async recordSkipped(id, expectedRunningSince) {
1878
+ try {
1879
+ const currentJob = await this.findJob(id);
1880
+ if (!currentJob || currentJob.runningSince !== expectedRunningSince) {
1881
+ return;
1882
+ }
1883
+ await this.store.update(id, {
1884
+ lastFinishedAt: this.now().toISOString(),
1885
+ runningSince: void 0
1886
+ });
1887
+ } catch (err) {
1888
+ process.stderr.write(`[scheduler] failed to record skipped loop ${id}: ${err instanceof Error ? err.message : String(err)}
1889
+ `);
1890
+ }
1891
+ }
1892
+ async recordFailure(job, now, message) {
1893
+ const consecutiveFailures = job.consecutiveFailures + 1;
1894
+ const patch = {
1895
+ lastFiredAt: now.toISOString(),
1896
+ lastFinishedAt: this.now().toISOString(),
1897
+ lastStatus: "error",
1898
+ lastError: truncateError(message),
1899
+ lastResultPreview: void 0,
1900
+ consecutiveFailures,
1901
+ runningSince: void 0,
1902
+ runCount: job.runCount + 1
1903
+ };
1904
+ if (!job.recurring) {
1905
+ patch.enabled = false;
1906
+ }
1907
+ if (consecutiveFailures >= this.maxConsecutiveFailures) {
1908
+ patch.enabled = false;
1909
+ process.stderr.write(`[scheduler] loop ${job.id} auto-disabled after ${consecutiveFailures} consecutive failures
1910
+ `);
1911
+ }
1912
+ try {
1913
+ await this.store.update(job.id, patch);
1914
+ } catch (err) {
1915
+ process.stderr.write(`[scheduler] loop ${job.id} failure persist failed: ${err instanceof Error ? err.message : String(err)}
1916
+ `);
1917
+ await this.clearRunningSince(job.id);
1918
+ }
1919
+ }
1920
+ };
1921
+ function lastAnchor(job) {
1922
+ if (job.lastFiredAt && job.lastFinishedAt) {
1923
+ return new Date(job.lastFiredAt).getTime() > new Date(job.lastFinishedAt).getTime() ? job.lastFiredAt : job.lastFinishedAt;
1924
+ }
1925
+ return job.lastFinishedAt ?? job.lastFiredAt ?? job.createdAt;
1926
+ }
1927
+ __name(lastAnchor, "lastAnchor");
1928
+ function truncateResultPreview(text) {
1929
+ return text === void 0 ? void 0 : text.slice(0, MAX_RESULT_PREVIEW_LENGTH);
1930
+ }
1931
+ __name(truncateResultPreview, "truncateResultPreview");
1932
+ function truncateError(message) {
1933
+ return message.slice(0, 1e3);
1934
+ }
1935
+ __name(truncateError, "truncateError");
1936
+
1937
+ // packages/channels/base/dist/ChannelBase.js
1938
+ var CLEAR_CANCEL_TIMEOUT_MS = 3e3;
1939
+ var GROUP_HISTORY_CONTEXT_MARKER = "[Chat messages since your last reply - for context]";
1940
+ var CURRENT_MESSAGE_MARKER = "[Current message - respond to this]";
1941
+ var GROUP_HISTORY_ENTRY_TEXT_LIMIT = 1e3;
1942
+ var GROUP_HISTORY_ENTRY_METADATA_LIMIT = 256;
1943
+ var LOOP_CANCEL_GRACE_MS = 5e3;
1944
+ var COMMAND_TOKEN_CHARS = "a-zA-Z0-9_:-";
1945
+ var PARSE_COMMAND_RE = new RegExp(`^\\/([${COMMAND_TOKEN_CHARS}]+)(?:@\\S+)?\\s*(.*)`, "s");
1946
+ var COMMAND_TOKEN_RE = new RegExp(`^[${COMMAND_TOKEN_CHARS}]+(?:@\\S+)?$`);
1947
+ var LOOP_ADD_RE = /^"([^"]+)"\s+(.+)$/su;
1948
+ var MAX_LOOP_JOBS_PER_TARGET = 10;
1949
+ var MAX_LOOP_PROMPT_CHARS = 4e3;
1950
+ function parseLoopAddArgs(args) {
1951
+ const match = args.trim().match(LOOP_ADD_RE);
1952
+ if (!match)
1953
+ return null;
1954
+ const cron = match[1].trim();
1955
+ const prompt = match[2].trim();
1956
+ return cron && prompt ? { cron, prompt } : null;
1957
+ }
1958
+ __name(parseLoopAddArgs, "parseLoopAddArgs");
1959
+ var ChannelBase = class {
1960
+ static {
1961
+ __name(this, "ChannelBase");
1962
+ }
1963
+ config;
1964
+ bridge;
1965
+ groupGate;
1966
+ gate;
1967
+ router;
1968
+ name;
1969
+ /** Resolved proxy URL, available to subclasses for adapter-specific clients. */
1970
+ proxy;
1971
+ channelMemory;
1972
+ groupHistory;
1973
+ loopController;
1974
+ instructedSessions = /* @__PURE__ */ new Set();
1975
+ commands = /* @__PURE__ */ new Map();
1976
+ /** Per-session promise chain to serialize prompt + send (followup mode). */
1977
+ sessionQueues = /* @__PURE__ */ new Map();
1978
+ registerBridgeEvents;
1979
+ /**
1980
+ * Per-session generation, bumped by /clear. A queued followup turn captures the
1981
+ * generation when it enqueues and bails if /clear bumped it before the turn ran,
1982
+ * so a cleared session can't be resurrected by an already-queued prompt.
1983
+ */
1984
+ sessionGenerations = /* @__PURE__ */ new Map();
1985
+ /** Per-session active prompt tracking for dispatch modes. */
1986
+ activePrompts = /* @__PURE__ */ new Map();
1987
+ /** Per-session message buffer for collect mode. */
1988
+ collectBuffers = /* @__PURE__ */ new Map();
1989
+ bridgeToolCallListener = /* @__PURE__ */ __name((event) => {
1990
+ const target = this.router.getTarget(event.sessionId);
1991
+ if (target) {
1992
+ this.onToolCall(target.chatId, event);
1993
+ }
1994
+ }, "bridgeToolCallListener");
1995
+ bridgeSessionDiedListener = /* @__PURE__ */ __name((event) => {
1996
+ this.onSessionDied(event.sessionId);
1997
+ }, "bridgeSessionDiedListener");
1998
+ constructor(name, config, bridge, options) {
1999
+ this.name = name;
2000
+ this.config = config;
2001
+ this.bridge = bridge;
2002
+ this.proxy = options?.proxy;
2003
+ this.channelMemory = options?.channelMemory;
2004
+ this.groupHistory = new GroupHistoryStore(options?.groupHistoryPath ?? join4(getGlobalQwenDir(), "channels", `${encodeURIComponent(name)}-group-history.jsonl`));
2005
+ this.loopController = options?.loopController;
2006
+ this.groupGate = new GroupGate(config.groupPolicy, config.groups);
2007
+ const pairingStore = config.senderPolicy === "pairing" ? new PairingStore(name) : void 0;
2008
+ this.gate = new SenderGate(config.senderPolicy, config.allowedUsers, pairingStore);
2009
+ this.router = options?.router || new SessionRouter(bridge, config.cwd, config.sessionScope);
2010
+ this.registerSharedCommands();
2011
+ this.registerBridgeEvents = options?.registerBridgeEvents ?? !options?.router;
2012
+ if (this.registerBridgeEvents) {
2013
+ this.attachBridgeEvents(bridge);
2014
+ }
2015
+ }
2016
+ supportsProactiveSend() {
2017
+ return false;
2018
+ }
2019
+ supportsProactiveTarget(target) {
2020
+ return target.threadId === void 0;
2021
+ }
2022
+ async pushProactive(target, text) {
2023
+ if (target.threadId) {
2024
+ throw new Error("Channel does not support proactive loop messages for threaded targets.");
2025
+ }
2026
+ await this.sendMessage(target.chatId, text);
2027
+ }
2028
+ /** Replace the bridge instance (used after crash recovery restart). */
2029
+ setBridge(bridge) {
2030
+ if (this.registerBridgeEvents) {
2031
+ this.detachBridgeEvents(this.bridge);
2032
+ }
2033
+ this.router.setBridge(bridge);
2034
+ this.bridge = bridge;
2035
+ if (this.registerBridgeEvents) {
2036
+ this.attachBridgeEvents(bridge);
2037
+ }
2038
+ }
2039
+ async runLoopPrompt(job, options = {}) {
2040
+ if (!this.supportsProactiveSend()) {
2041
+ throw new Error("Channel does not support proactive loop messages.");
2042
+ }
2043
+ if (this.config.sessionScope === "single") {
2044
+ await this.loopController?.disable(job.id);
2045
+ throw new Error("Loop messages are not supported with single session scope.");
2046
+ }
2047
+ if (job.channelName !== this.name) {
2048
+ throw new Error(`Loop ${job.id} belongs to ${job.channelName}, not ${this.name}.`);
2049
+ }
2050
+ if (!this.supportsProactiveTarget(job.target)) {
2051
+ throw new Error("Channel does not support proactive loop messages for this chat target.");
2052
+ }
2053
+ if (!this.isStoredLoopTargetAuthorized(job.target, job.createdBy)) {
2054
+ await this.loopController?.disable(job.id);
2055
+ throw new Error(`Loop ${job.id} target is no longer authorized.`);
2056
+ }
2057
+ const sessionId = await this.router.resolve(this.name, job.target.senderId, job.target.chatId, job.target.threadId, job.cwd);
2058
+ const label = sanitizeQuotedText(job.label || job.id, 80);
2059
+ const createdBy = sanitizeSenderName(job.createdBy || "unknown");
2060
+ let promptText = `[Loop "${label}" created by ${createdBy}]
2061
+
2062
+ ${sanitizePromptText(job.prompt)}`;
2063
+ const shouldPrependSessionContext = !this.instructedSessions.has(sessionId);
2064
+ const prev = this.sessionQueues.get(sessionId) ?? Promise.resolve();
2065
+ const generation = this.sessionGenerations.get(sessionId) ?? 0;
2066
+ const current = prev.then(async () => {
2067
+ if ((this.sessionGenerations.get(sessionId) ?? 0) !== generation) {
2068
+ process.stderr.write(`[${this.name}] dropped loop ${job.id} for session ${sessionId}: session was cleared before it ran
2069
+ `);
2070
+ throw new ChannelLoopSkippedError("loop dropped because session was cleared before it ran");
2071
+ }
2072
+ if (options.shouldContinue && !await options.shouldContinue()) {
2073
+ throw new ChannelLoopSkippedError("loop dropped because it is no longer enabled");
2074
+ }
2075
+ let shouldClaimSessionContext = false;
2076
+ if (shouldPrependSessionContext) {
2077
+ const context = [];
2078
+ let sessionContextReady = true;
2079
+ if (this.channelMemory && this.isSenderAuthorizedForChannelMemory(job.target.senderId) && (!this.isSharedSessionTarget(job.target) || this.config.senderPolicy === "allowlist")) {
2080
+ try {
2081
+ const memoryText = (await this.channelMemory.readChannelMemory({
2082
+ channelName: this.name,
2083
+ chatId: job.target.chatId,
2084
+ threadId: job.target.threadId
2085
+ })).trim();
2086
+ if (memoryText) {
2087
+ context.push(`Channel memory for this chat:
2088
+ ${sanitizePromptText(memoryText)}`);
2089
+ }
2090
+ } catch (error) {
2091
+ process.stderr.write(`[${this.name}] channel memory read failed for loop ${job.id} chat ${sanitizeLogText(job.target.chatId, 64)}: ${sanitizeLogText(this.channelMemoryErrorMessage(error), 200)}
2092
+ `);
2093
+ this.instructedSessions.delete(sessionId);
2094
+ sessionContextReady = false;
2095
+ }
2096
+ }
2097
+ if (this.config.instructions) {
2098
+ context.push(this.config.instructions);
2099
+ }
2100
+ if (context.length > 0) {
2101
+ promptText = `${context.join("\n\n")}
2102
+
2103
+ ${promptText}`;
2104
+ }
2105
+ if (sessionContextReady) {
2106
+ shouldClaimSessionContext = true;
2107
+ }
2108
+ }
2109
+ if ((this.sessionGenerations.get(sessionId) ?? 0) !== generation) {
2110
+ process.stderr.write(`[${this.name}] dropped loop ${job.id} for session ${sessionId}: session was cleared before it ran
2111
+ `);
2112
+ throw new ChannelLoopSkippedError("loop dropped because session was cleared before it ran");
2113
+ }
2114
+ if (shouldClaimSessionContext) {
2115
+ this.instructedSessions.add(sessionId);
2116
+ }
2117
+ let doneResolve = /* @__PURE__ */ __name(() => {
2118
+ }, "doneResolve");
2119
+ const done = new Promise((resolve2) => {
2120
+ doneResolve = resolve2;
2121
+ });
2122
+ const promptState = {
2123
+ cancelled: false,
2124
+ done,
2125
+ resolve: doneResolve,
2126
+ chatId: job.target.chatId,
2127
+ messageId: job.id
2128
+ };
2129
+ this.activePrompts.set(sessionId, promptState);
2130
+ this.onPromptStart(job.target.chatId, sessionId);
2131
+ const onChunk = /* @__PURE__ */ __name((sid, chunk) => {
2132
+ if (sid === sessionId && !promptState.cancelled) {
2133
+ this.onResponseChunk(job.target.chatId, chunk, sessionId);
2134
+ }
2135
+ }, "onChunk");
2136
+ const promptBridge = this.bridge;
2137
+ promptBridge.on("textChunk", onChunk);
2138
+ try {
2139
+ const response = await this.runLoopBridgePrompt(promptBridge, sessionId, promptText, promptState, job.id, options.timeoutMs);
2140
+ if (promptState.cancelRequested && !promptState.cancelled) {
2141
+ const cancelled = await promptState.cancelRequested;
2142
+ if (cancelled) {
2143
+ promptState.cancelled = true;
2144
+ }
2145
+ }
2146
+ if (promptState.cancelled) {
2147
+ throw new ChannelLoopSkippedError("loop cancelled before delivery");
2148
+ }
2149
+ if (options.shouldContinue && !await options.shouldContinue()) {
2150
+ throw new ChannelLoopSkippedError("loop dropped before delivery");
2151
+ }
2152
+ if (response) {
2153
+ await this.pushProactive(job.target, response);
2154
+ }
2155
+ return response;
2156
+ } finally {
2157
+ promptBridge.off("textChunk", onChunk);
2158
+ const stillCurrent = this.activePrompts.get(sessionId) === promptState;
2159
+ if (!promptState.clearEvicted) {
2160
+ try {
2161
+ this.onPromptEnd(job.target.chatId, sessionId);
2162
+ } catch (err) {
2163
+ process.stderr.write(`[${this.name}] onPromptEnd threw in loop ${job.id} for session ${sessionId}: ${err instanceof Error ? err.message : err}
2164
+ `);
2165
+ }
2166
+ }
2167
+ if (stillCurrent) {
2168
+ this.activePrompts.delete(sessionId);
2169
+ }
2170
+ promptState.resolve();
2171
+ const buffer = this.collectBuffers.get(sessionId);
2172
+ if (stillCurrent && buffer && buffer.length > 0) {
2173
+ this.collectBuffers.delete(sessionId);
2174
+ const lost = buffer.length;
2175
+ const coalesced = buffer.map((b) => b.text).join("\n\n");
2176
+ const lastEnvelope = buffer[buffer.length - 1].envelope;
2177
+ const syntheticEnvelope = {
2178
+ ...lastEnvelope,
2179
+ text: coalesced,
2180
+ alreadyPrefixed: true,
2181
+ referencedText: void 0,
2182
+ attachments: void 0,
2183
+ imageBase64: void 0,
2184
+ imageMimeType: void 0
2185
+ };
2186
+ this.handleInbound(syntheticEnvelope).catch((err) => {
2187
+ process.stderr.write(`[${this.name}] dropped ${lost} buffered message(s) after loop ${job.id} for session ${sessionId} (last sender ${lastEnvelope.senderId}): ${err instanceof Error ? err.message : String(err)}
2188
+ `);
2189
+ });
2190
+ }
2191
+ }
2192
+ });
2193
+ this.sessionQueues.set(sessionId, current.then(() => void 0).catch(() => {
2194
+ }));
2195
+ return current;
2196
+ }
2197
+ async runLoopBridgePrompt(promptBridge, sessionId, promptText, promptState, jobId, timeoutMs) {
2198
+ const prompt = promptBridge.prompt(sessionId, promptText, {});
2199
+ prompt.catch(() => {
2200
+ });
2201
+ if (timeoutMs === void 0) {
2202
+ return prompt;
2203
+ }
2204
+ let timer;
2205
+ try {
2206
+ return await Promise.race([
2207
+ prompt,
2208
+ new Promise((_, reject) => {
2209
+ timer = setTimeout(() => {
2210
+ reject(new Error("loop timed out"));
2211
+ }, timeoutMs);
2212
+ timer.unref?.();
2213
+ })
2214
+ ]);
2215
+ } catch (err) {
2216
+ if (err instanceof Error && err.message === "loop timed out") {
2217
+ promptState.cancelled = true;
2218
+ await this.cancelTimedOutLoopPrompt(promptBridge, sessionId, jobId);
2219
+ }
2220
+ throw err;
2221
+ } finally {
2222
+ clearTimeout(timer);
2223
+ }
2224
+ }
2225
+ async cancelTimedOutLoopPrompt(promptBridge, sessionId, jobId) {
2226
+ let graceTimer;
2227
+ try {
2228
+ const cancelled = await Promise.race([
2229
+ promptBridge.cancelSession(sessionId).then(() => true),
2230
+ new Promise((resolve2) => {
2231
+ graceTimer = setTimeout(() => resolve2(false), LOOP_CANCEL_GRACE_MS);
2232
+ graceTimer.unref?.();
2233
+ })
2234
+ ]);
2235
+ if (!cancelled) {
2236
+ this.router.removeSessionId(sessionId);
2237
+ this.instructedSessions.delete(sessionId);
2238
+ process.stderr.write(`[${this.name}] retired timed out loop ${jobId} session ${sessionId} after cancel did not settle
2239
+ `);
2240
+ }
2241
+ } catch (cancelErr) {
2242
+ process.stderr.write(`[${this.name}] cancelSession failed for timed out loop ${jobId} in session ${sessionId}: ${cancelErr instanceof Error ? cancelErr.message : cancelErr}
2243
+ `);
2244
+ } finally {
2245
+ clearTimeout(graceTimer);
2246
+ }
2247
+ }
2248
+ onToolCall(_chatId, _event) {
2249
+ }
2250
+ onSessionDied(sessionId) {
2251
+ this.router.removeSessionId(sessionId);
2252
+ this.instructedSessions.delete(sessionId);
2253
+ }
2254
+ attachBridgeEvents(bridge) {
2255
+ bridge.on("toolCall", this.bridgeToolCallListener);
2256
+ bridge.on("sessionDied", this.bridgeSessionDiedListener);
2257
+ }
2258
+ detachBridgeEvents(bridge) {
2259
+ bridge.off("toolCall", this.bridgeToolCallListener);
2260
+ bridge.off("sessionDied", this.bridgeSessionDiedListener);
2261
+ }
2262
+ /**
2263
+ * Called when a prompt actually begins processing (inside the session queue).
2264
+ * Override to show a platform-specific working indicator (e.g., typing, reaction).
2265
+ * Not called for buffered messages (collect mode) or gated/blocked messages.
2266
+ */
2267
+ onPromptStart(_chatId, _sessionId, _messageId) {
2268
+ }
2269
+ /**
2270
+ * Called when a prompt finishes (response sent or cancelled).
2271
+ * Override to hide the working indicator.
2272
+ */
2273
+ onPromptEnd(_chatId, _sessionId, _messageId) {
2274
+ }
2275
+ /**
2276
+ * Called for each text chunk as the agent streams its response.
2277
+ * Override to implement progressive display (e.g., updating an AI card in-place).
2278
+ * Default: no-op (chunks are collected internally and delivered via onResponseComplete).
2279
+ */
2280
+ onResponseChunk(_chatId, _chunk, _sessionId) {
2281
+ }
2282
+ /**
2283
+ * Called when the agent's full response is ready.
2284
+ * Override to customize delivery (e.g., finalize an AI card).
2285
+ * Default: calls sendMessage() with the full response text.
2286
+ */
2287
+ async onResponseComplete(chatId, fullText, _sessionId) {
2288
+ await this.sendMessage(chatId, fullText);
2289
+ }
2290
+ /**
2291
+ * Register a slash command handler. Subclasses can call this to add
2292
+ * platform-specific commands (e.g., /start for Telegram).
2293
+ * Overrides shared commands if the same name is registered.
2294
+ */
2295
+ registerCommand(name, handler) {
2296
+ this.commands.set(name.toLowerCase(), handler);
2297
+ }
2298
+ registerCancelCommand(name = "cancel") {
2299
+ this.registerCommand(name, async (envelope) => {
2300
+ if (!this.isAuthorizedForSharedSession(envelope)) {
2301
+ await this.sendMessage(envelope.chatId, "Only authorized members can cancel requests in this shared session.");
2302
+ return true;
2303
+ }
2304
+ const activeSessionId = this.findActiveSessionId(envelope);
2305
+ if (!activeSessionId) {
2306
+ await this.sendMessage(envelope.chatId, "No request is currently running.");
2307
+ return true;
2308
+ }
2309
+ const active = this.activePrompts.get(activeSessionId);
2310
+ if (!active) {
2311
+ await this.sendMessage(envelope.chatId, "No request is currently running.");
2312
+ return true;
2313
+ }
2314
+ const cancelRequested = active.cancelRequested ?? this.bridge.cancelSession(activeSessionId).then(() => true, (err) => {
2315
+ process.stderr.write(`[${this.name}] cancelSession failed for session=${activeSessionId}: ${err instanceof Error ? err.message : err}
2316
+ `);
2317
+ active.cancelRequested = void 0;
2318
+ return false;
2319
+ });
2320
+ active.cancelRequested = cancelRequested;
2321
+ const cancelSucceeded = await cancelRequested;
2322
+ if (!cancelSucceeded) {
2323
+ await this.sendMessage(envelope.chatId, "Failed to cancel current request.");
2324
+ return true;
2325
+ }
2326
+ active.cancelled = true;
2327
+ active.stopStreaming?.();
2328
+ this.collectBuffers.delete(activeSessionId);
2329
+ await this.sendMessage(envelope.chatId, "Cancelled current request.");
2330
+ return true;
2331
+ });
2332
+ }
2333
+ /** Register shared slash commands. Called from constructor. */
2334
+ registerSharedCommands() {
2335
+ const doClear = /* @__PURE__ */ __name(async (envelope) => {
2336
+ const removedIds = this.router.removeSession(this.name, envelope.senderId, envelope.chatId, envelope.threadId);
2337
+ this.clearPendingGroupHistory(envelope);
2338
+ if (removedIds.length > 0) {
2339
+ for (const id of removedIds) {
2340
+ if (this.isSharedSession(envelope)) {
2341
+ const who = sanitizeSenderName(envelope.senderName || envelope.senderId || "unknown");
2342
+ process.stderr.write(`[${this.name}] shared session ${id} cleared by ${who} (sender ${envelope.senderId})
2343
+ `);
2344
+ }
2345
+ this.sessionGenerations.set(id, (this.sessionGenerations.get(id) ?? 0) + 1);
2346
+ const active = this.activePrompts.get(id);
2347
+ this.collectBuffers.delete(id);
2348
+ if (active) {
2349
+ const settled = await this.cancelAndAwaitActive(active, id);
2350
+ if (!settled) {
2351
+ const wedgedChat = active.chatId ? sanitizeLogText(active.chatId, 64) : "unknown";
2352
+ const wedgedMessage = active.messageId ? `, message ${sanitizeLogText(active.messageId, 64)}` : "";
2353
+ process.stderr.write(`[${this.name}] /clear abandoned a wedged turn for session ${id} (chat ${wedgedChat}${wedgedMessage}): it did not wind down within ${CLEAR_CANCEL_TIMEOUT_MS}ms
2354
+ `);
2355
+ active.clearEvicted = true;
2356
+ try {
2357
+ this.onPromptEnd(active.chatId, id, active.messageId);
2358
+ } catch (err) {
2359
+ process.stderr.write(`[${this.name}] onPromptEnd threw during /clear eviction for session ${id}: ${err instanceof Error ? err.message : err}
2360
+ `);
2361
+ }
2362
+ }
2363
+ }
2364
+ this.instructedSessions.delete(id);
2365
+ const drained = this.sessionQueues.get(id);
2366
+ const bumpedGeneration = this.sessionGenerations.get(id);
2367
+ this.sessionQueues.delete(id);
2368
+ this.activePrompts.delete(id);
2369
+ if (drained) {
2370
+ void drained.then(() => {
2371
+ if (!this.sessionQueues.has(id) && this.sessionGenerations.get(id) === bumpedGeneration) {
2372
+ this.sessionGenerations.delete(id);
2373
+ }
2374
+ });
2375
+ } else {
2376
+ this.sessionGenerations.delete(id);
2377
+ }
2378
+ }
2379
+ await this.sendMessage(envelope.chatId, "Session cleared. The next message starts a fresh conversation.");
2380
+ } else {
2381
+ await this.sendMessage(envelope.chatId, "No active session to clear.");
2382
+ }
2383
+ }, "doClear");
2384
+ const clearHandler = /* @__PURE__ */ __name(async (envelope, args) => {
2385
+ if (!this.isAuthorizedForSharedSession(envelope)) {
2386
+ await this.sendMessage(envelope.chatId, "Only authorized members can clear this shared session.");
2387
+ return true;
2388
+ }
2389
+ if (this.isSharedSession(envelope) && args.toLowerCase() !== "confirm") {
2390
+ await this.sendMessage(envelope.chatId, 'This clears the shared session for everyone who shares it. Re-send with "confirm" (e.g. /clear confirm) to proceed.');
2391
+ return true;
2392
+ }
2393
+ await doClear(envelope);
2394
+ return true;
2395
+ }, "clearHandler");
2396
+ this.registerCommand("clear", clearHandler);
2397
+ this.registerCommand("reset", clearHandler);
2398
+ this.registerCommand("new", clearHandler);
2399
+ this.registerCommand("who", async (envelope) => {
2400
+ if (!this.isAuthorizedForSharedSession(envelope)) {
2401
+ await this.sendMessage(envelope.chatId, "Only authorized members can view this shared session.");
2402
+ return true;
2403
+ }
2404
+ const active = this.router.hasSession(this.name, envelope.senderId, envelope.chatId, envelope.threadId);
2405
+ const scopeNote = this.config.sessionScope === "single" ? " (shared channel-wide)" : this.isSharedSession(envelope) ? envelope.isGroup ? " (shared by this group)" : "" : envelope.isGroup ? " (private to you)" : "";
2406
+ await this.sendMessage(envelope.chatId, [
2407
+ `Channel: ${this.name}`,
2408
+ // Only the basename — don't leak the absolute cwd to group members.
2409
+ `Workspace: ${basename(this.config.cwd)}`,
2410
+ `Session: ${active ? "active" : "none"}${scopeNote}`
2411
+ ].join("\n"));
2412
+ return true;
2413
+ });
2414
+ this.registerCommand("remember-channel", async (envelope, args) => {
2415
+ if (!await this.ensureChannelMemoryAuthorized(envelope)) {
2416
+ return true;
2417
+ }
2418
+ if (envelope.isGroup) {
2419
+ await this.sendMessage(envelope.chatId, "Channel memory cannot be changed in group chats.");
2420
+ return true;
2421
+ }
2422
+ if (args.trim() === "") {
2423
+ await this.sendMessage(envelope.chatId, "Usage: /remember-channel <text>");
2424
+ return true;
2425
+ }
2426
+ const channelMemory = await this.getChannelMemory(envelope);
2427
+ if (!channelMemory) {
2428
+ return true;
2429
+ }
2430
+ try {
2431
+ await channelMemory.appendChannelMemory(this.channelMemoryTarget(envelope), args.trim());
2432
+ } catch (error) {
2433
+ const message = this.channelMemoryErrorMessage(error);
2434
+ this.logChannelMemoryError("save", envelope, message);
2435
+ await this.sendMessage(envelope.chatId, `Failed to save channel memory: ${this.channelMemoryUserErrorMessage()}`);
2436
+ return true;
2437
+ }
2438
+ this.invalidateSessionContext(envelope);
2439
+ await this.sendMessage(envelope.chatId, "Channel memory updated.");
2440
+ return true;
2441
+ });
2442
+ this.registerCommand("channel-memory", async (envelope) => {
2443
+ if (!await this.ensureChannelMemoryAuthorized(envelope)) {
2444
+ return true;
2445
+ }
2446
+ if (envelope.isGroup) {
2447
+ await this.sendMessage(envelope.chatId, "Channel memory cannot be shown in group chats.");
2448
+ return true;
2449
+ }
2450
+ const channelMemory = await this.getChannelMemory(envelope);
2451
+ if (!channelMemory) {
2452
+ return true;
2453
+ }
2454
+ let text;
2455
+ try {
2456
+ text = (await channelMemory.readChannelMemory(this.channelMemoryTarget(envelope))).trim();
2457
+ } catch (error) {
2458
+ const message = this.channelMemoryErrorMessage(error);
2459
+ this.logChannelMemoryError("read", envelope, message);
2460
+ await this.sendMessage(envelope.chatId, `Failed to read channel memory: ${this.channelMemoryUserErrorMessage()}`);
2461
+ return true;
2462
+ }
2463
+ await this.sendMessage(envelope.chatId, text === "" ? "No channel memory saved." : sanitizePromptText(text));
2464
+ return true;
2465
+ });
2466
+ this.registerCommand("forget-channel", async (envelope, args) => {
2467
+ if (!await this.ensureChannelMemoryAuthorized(envelope)) {
2468
+ return true;
2469
+ }
2470
+ if (envelope.isGroup) {
2471
+ await this.sendMessage(envelope.chatId, "Channel memory cannot be changed in group chats.");
2472
+ return true;
2473
+ }
2474
+ if (args.toLowerCase() !== "confirm") {
2475
+ await this.sendMessage(envelope.chatId, 'This clears channel memory for this chat. Re-send with "confirm" (e.g. /forget-channel confirm) to proceed.');
2476
+ return true;
2477
+ }
2478
+ const channelMemory = await this.getChannelMemory(envelope);
2479
+ if (!channelMemory) {
2480
+ return true;
2481
+ }
2482
+ let result;
2483
+ try {
2484
+ result = await channelMemory.clearChannelMemory(this.channelMemoryTarget(envelope));
2485
+ } catch (error) {
2486
+ const message = this.channelMemoryErrorMessage(error);
2487
+ this.logChannelMemoryError("clear", envelope, message);
2488
+ await this.sendMessage(envelope.chatId, `Failed to clear channel memory: ${this.channelMemoryUserErrorMessage()}`);
2489
+ return true;
2490
+ }
2491
+ this.invalidateSessionContext(envelope);
2492
+ await this.sendMessage(envelope.chatId, result.changed ? "Channel memory cleared." : "No channel memory saved.");
2493
+ return true;
2494
+ });
2495
+ this.registerCommand("help", async (envelope) => {
2496
+ const lines = [
2497
+ "Commands:",
2498
+ "/help \u2014 Show this help",
2499
+ this.isSharedSession(envelope) ? "/clear confirm \u2014 Clear the shared session (aliases: /reset, /new)" : "/clear \u2014 Clear your session (aliases: /reset, /new)",
2500
+ "/who \u2014 Show current session & workspace",
2501
+ "/status \u2014 Show session info",
2502
+ "/remember-channel <text> \u2014 Save memory for this chat",
2503
+ "/channel-memory \u2014 Show memory for this chat",
2504
+ "/forget-channel confirm \u2014 Clear memory for this chat"
2505
+ ];
2506
+ const sharedCmds = /* @__PURE__ */ new Set([
2507
+ "help",
2508
+ "clear",
2509
+ "reset",
2510
+ "new",
2511
+ "who",
2512
+ "status",
2513
+ "remember-channel",
2514
+ "channel-memory",
2515
+ "forget-channel"
2516
+ ]);
2517
+ const platformCmds = [...this.commands.keys()].filter((c) => !sharedCmds.has(c));
2518
+ if (platformCmds.length > 0) {
2519
+ for (const cmd of platformCmds) {
2520
+ lines.push(`/${cmd}`);
2521
+ }
2522
+ }
2523
+ const sessionId = this.router.getSession(this.name, envelope.senderId, envelope.chatId, envelope.threadId);
2524
+ const agentCommands = sessionId ? this.getAgentCommandsForSession(sessionId) : this.bridge.availableCommands;
2525
+ if (agentCommands.length > 0) {
2526
+ lines.push("", "Agent commands (forwarded to Qwen Code):");
2527
+ for (const cmd of agentCommands) {
2528
+ lines.push(`/${cmd.name} \u2014 ${cmd.description}`);
2529
+ }
2530
+ }
2531
+ lines.push("", "Send any text to chat with the agent.");
2532
+ await this.sendMessage(envelope.chatId, lines.join("\n"));
2533
+ return true;
2534
+ });
2535
+ this.registerCommand("status", async (envelope) => {
2536
+ if (!this.isAuthorizedForSharedSession(envelope)) {
2537
+ await this.sendMessage(envelope.chatId, "Only authorized members can view this shared session.");
2538
+ return true;
2539
+ }
2540
+ const hasSession = this.router.hasSession(this.name, envelope.senderId, envelope.chatId, envelope.threadId);
2541
+ const policy = this.config.senderPolicy;
2542
+ const lines = [
2543
+ `Session: ${hasSession ? "active" : "none"}`,
2544
+ `Access: ${policy}`,
2545
+ `Channel: ${this.name}`
2546
+ ];
2547
+ await this.sendMessage(envelope.chatId, lines.join("\n"));
2548
+ return true;
2549
+ });
2550
+ this.registerCommand("loop", async (envelope, args) => this.handleLoopCommand(envelope, args));
2551
+ }
2552
+ async handleLoopCommand(envelope, args) {
2553
+ if (!this.loopController) {
2554
+ await this.sendMessage(envelope.chatId, "Loops are not available.");
2555
+ return true;
2556
+ }
2557
+ if (!this.isAuthorizedForSharedSession(envelope)) {
2558
+ await this.sendMessage(envelope.chatId, "Only authorized members can use loops in this shared session.");
2559
+ return true;
2560
+ }
2561
+ const [subcommand = "", ...rest] = args.trim().split(/\s+/u);
2562
+ switch (subcommand.toLowerCase()) {
2563
+ case "add":
2564
+ return this.handleLoopAdd(envelope, rest.join(" "));
2565
+ case "list":
2566
+ return this.handleLoopList(envelope);
2567
+ case "inspect":
2568
+ return this.handleLoopInspect(envelope, rest[0]);
2569
+ case "cancel":
2570
+ return this.handleLoopCancel(envelope, rest[0]);
2571
+ default:
2572
+ await this.sendMessage(envelope.chatId, 'Usage: /loop add "<cron>" <prompt> | /loop list | /loop inspect <id> | /loop cancel <id>');
2573
+ return true;
2574
+ }
2575
+ }
2576
+ async handleLoopAdd(envelope, args) {
2577
+ if (!this.loopController)
2578
+ return true;
2579
+ if (!this.supportsProactiveSend()) {
2580
+ await this.sendMessage(envelope.chatId, "This channel does not support proactive loop messages.");
2581
+ return true;
2582
+ }
2583
+ if (this.config.sessionScope === "single") {
2584
+ await this.sendMessage(envelope.chatId, "Loops are not supported when sessionScope is single.");
2585
+ return true;
2586
+ }
2587
+ const parsed = parseLoopAddArgs(args);
2588
+ if (!parsed) {
2589
+ await this.sendMessage(envelope.chatId, 'Usage: /loop add "<cron>" <prompt>');
2590
+ return true;
2591
+ }
2592
+ try {
2593
+ this.loopController.validateCron(parsed.cron);
2594
+ } catch (err) {
2595
+ await this.sendMessage(envelope.chatId, `Invalid cron expression: ${err instanceof Error ? err.message : String(err)}`);
2596
+ return true;
2597
+ }
2598
+ const target = this.loopTargetFromEnvelope(envelope);
2599
+ if (!this.supportsProactiveTarget(target)) {
2600
+ await this.sendMessage(envelope.chatId, "This channel does not support proactive loop messages for this chat target.");
2601
+ return true;
2602
+ }
2603
+ const prompt = sanitizePromptText(parsed.prompt.trim());
2604
+ if (Array.from(prompt).length > MAX_LOOP_PROMPT_CHARS) {
2605
+ await this.sendMessage(envelope.chatId, `Loop prompt is too long; keep it under ${MAX_LOOP_PROMPT_CHARS} characters.`);
2606
+ return true;
2607
+ }
2608
+ const input = {
2609
+ channelName: this.name,
2610
+ target,
2611
+ cwd: this.config.cwd,
2612
+ cron: parsed.cron,
2613
+ prompt,
2614
+ label: truncateLoopLabel(prompt),
2615
+ recurring: true,
2616
+ createdBy: sanitizeSenderName(envelope.senderName || envelope.senderId || "unknown")
2617
+ };
2618
+ let job;
2619
+ if (this.loopController.createForTarget) {
2620
+ job = await this.loopController.createForTarget(input, MAX_LOOP_JOBS_PER_TARGET);
2621
+ } else {
2622
+ const existingJobs = await this.loopController.listForTarget(this.name, target);
2623
+ if (existingJobs.filter((existingJob) => existingJob.enabled).length < MAX_LOOP_JOBS_PER_TARGET) {
2624
+ job = await this.loopController.create(input);
2625
+ }
2626
+ }
2627
+ if (!job) {
2628
+ await this.sendMessage(envelope.chatId, `Too many loops for this chat. Cancel an existing loop before adding another.`);
2629
+ return true;
2630
+ }
2631
+ await this.sendMessage(envelope.chatId, `Loop ${job.id}: ${job.cron}`);
2632
+ return true;
2633
+ }
2634
+ async handleLoopList(envelope) {
2635
+ if (!this.loopController)
2636
+ return true;
2637
+ const jobs = await this.loopController.listForTarget(this.name, this.loopTargetFromEnvelope(envelope));
2638
+ if (jobs.length === 0) {
2639
+ await this.sendMessage(envelope.chatId, "No loops.");
2640
+ return true;
2641
+ }
2642
+ await this.sendMessage(envelope.chatId, jobs.map((job) => this.formatLoopListLine(job)).join("\n"));
2643
+ return true;
2644
+ }
2645
+ async handleLoopInspect(envelope, id) {
2646
+ if (!this.loopController)
2647
+ return true;
2648
+ if (!id) {
2649
+ await this.sendMessage(envelope.chatId, "Usage: /loop inspect <id>");
2650
+ return true;
2651
+ }
2652
+ const jobs = await this.loopController.listForTarget(this.name, this.loopTargetFromEnvelope(envelope));
2653
+ const job = jobs.find((candidate) => candidate.id === id);
2654
+ if (!job) {
2655
+ await this.sendMessage(envelope.chatId, `No loop ${id}.`);
2656
+ return true;
2657
+ }
2658
+ const lines = [
2659
+ `Loop ${job.id}`,
2660
+ `Status: ${job.enabled ? "enabled" : "disabled"}, last=${this.lastLoopStatus(job)}`,
2661
+ `Cron: ${job.cron}`,
2662
+ `Next: ${this.formatNextFireTime(job)}`,
2663
+ `Runs: ${job.runCount}`,
2664
+ `Created by: ${job.createdBy}`,
2665
+ `Created: ${job.createdAt}`
2666
+ ];
2667
+ if (job.lastFinishedAt) {
2668
+ lines.push(`Last finished: ${job.lastFinishedAt}`);
2669
+ }
2670
+ if (job.lastError) {
2671
+ lines.push(`Last error: ${job.lastError}`);
2672
+ }
2673
+ if (job.lastResultPreview) {
2674
+ lines.push(`Last result: ${job.lastResultPreview}`);
2675
+ }
2676
+ lines.push(`Prompt: ${job.prompt}`);
2677
+ await this.sendMessage(envelope.chatId, lines.join("\n"));
2678
+ return true;
2679
+ }
2680
+ formatLoopListLine(job) {
2681
+ const fields = [
2682
+ job.id,
2683
+ job.cron,
2684
+ job.enabled ? "enabled" : "disabled",
2685
+ `last=${this.lastLoopStatus(job)}`,
2686
+ `next=${this.formatNextFireTime(job)}`,
2687
+ `runs=${job.runCount}`
2688
+ ];
2689
+ if (job.label)
2690
+ fields.push(job.label);
2691
+ return fields.join(" ");
2692
+ }
2693
+ lastLoopStatus(job) {
2694
+ if (job.runningSince)
2695
+ return "running";
2696
+ return job.lastStatus ?? "never";
2697
+ }
2698
+ formatNextFireTime(job) {
2699
+ try {
2700
+ return this.loopController?.nextFireTime?.(job).toISOString() ?? "n/a";
2701
+ } catch {
2702
+ return "invalid cron";
2703
+ }
2704
+ }
2705
+ async handleLoopCancel(envelope, id) {
2706
+ if (!this.loopController)
2707
+ return true;
2708
+ if (!id) {
2709
+ await this.sendMessage(envelope.chatId, "Usage: /loop cancel <id>");
2710
+ return true;
2711
+ }
2712
+ const jobs = await this.loopController.listForTarget(this.name, this.loopTargetFromEnvelope(envelope));
2713
+ const match = jobs.find((job) => job.id === id);
2714
+ const disabled = match ? await this.loopController.disable(id) : false;
2715
+ await this.sendMessage(envelope.chatId, disabled ? `Cancelled loop ${id}.` : `No loop ${id}.`);
2716
+ return true;
2717
+ }
2718
+ loopTargetFromEnvelope(envelope) {
2719
+ return {
2720
+ channelName: this.name,
2721
+ senderId: envelope.senderId,
2722
+ chatId: envelope.chatId,
2723
+ threadId: envelope.threadId,
2724
+ isGroup: envelope.isGroup === true
2725
+ };
2726
+ }
2727
+ isStoredLoopTargetAuthorized(target, senderName) {
2728
+ if (target.isGroup === void 0) {
2729
+ return false;
2730
+ }
2731
+ const envelope = {
2732
+ channelName: this.name,
2733
+ senderId: target.senderId,
2734
+ senderName,
2735
+ chatId: target.chatId,
2736
+ text: "",
2737
+ threadId: target.threadId,
2738
+ isGroup: target.isGroup === true,
2739
+ isMentioned: true,
2740
+ isReplyToBot: true
2741
+ };
2742
+ return this.groupGate.check(envelope).allowed && this.gate.isAllowed(target.senderId) && this.isAuthorizedForSharedSession(envelope);
2743
+ }
2744
+ /** Check if a message text matches a registered local command. */
2745
+ isLocalCommand(text) {
2746
+ const parsed = this.parseCommand(text);
2747
+ return parsed !== null && this.commands.has(parsed.command);
2748
+ }
2749
+ findActiveSessionId(envelope) {
2750
+ const sessionId = this.router.getSession(this.name, envelope.senderId, envelope.chatId, envelope.threadId);
2751
+ return sessionId && this.activePrompts.has(sessionId) ? sessionId : void 0;
2752
+ }
2753
+ channelMemoryTarget(envelope) {
2754
+ return {
2755
+ channelName: this.name,
2756
+ chatId: envelope.chatId,
2757
+ threadId: envelope.threadId
2758
+ };
2759
+ }
2760
+ invalidateSessionContext(envelope) {
2761
+ const sessionId = this.router.getSession(this.name, envelope.senderId, envelope.chatId, envelope.threadId);
2762
+ if (sessionId) {
2763
+ this.instructedSessions.delete(sessionId);
2764
+ }
2765
+ }
2766
+ dropQueuedTurnIfStale(sessionId, generation, envelope) {
2767
+ if ((this.sessionGenerations.get(sessionId) ?? 0) === generation) {
2768
+ return false;
2769
+ }
2770
+ const loggedText = sanitizeLogText(envelope.text, 80);
2771
+ process.stderr.write(`[${this.name}] dropped queued turn from ${envelope.senderId} for session ${sessionId}: session was cleared before it ran (text: ${loggedText})
2772
+ `);
2773
+ return true;
2774
+ }
2775
+ isAuthorizedForChannelMemory(envelope) {
2776
+ return this.isSenderAuthorizedForChannelMemory(envelope.senderId);
2777
+ }
2778
+ isSenderAuthorizedForChannelMemory(senderId) {
2779
+ return this.config.allowedUsers.length > 0 && this.config.allowedUsers.includes(senderId);
2780
+ }
2781
+ async ensureChannelMemoryAuthorized(envelope) {
2782
+ if (!this.isAuthorizedForChannelMemory(envelope)) {
2783
+ await this.sendMessage(envelope.chatId, "Only authorized members can manage channel memory.");
2784
+ return false;
2785
+ }
2786
+ return true;
2787
+ }
2788
+ async getChannelMemory(envelope) {
2789
+ if (!this.channelMemory) {
2790
+ await this.sendMessage(envelope.chatId, "Channel memory is not configured for this channel.");
2791
+ return void 0;
2792
+ }
2793
+ return this.channelMemory;
2794
+ }
2795
+ channelMemoryErrorMessage(error) {
2796
+ return error instanceof Error ? error.message : String(error);
2797
+ }
2798
+ channelMemoryUserErrorMessage() {
2799
+ return "An error occurred while accessing channel memory.";
2800
+ }
2801
+ logChannelMemoryError(action, envelope, message) {
2802
+ process.stderr.write(`[${this.name}] channel memory ${action} failed for sender=${sanitizeLogText(envelope.senderId, 80)} chat=${sanitizeLogText(envelope.chatId, 80)} thread=${sanitizeLogText(envelope.threadId ?? "", 80)}: ${sanitizeLogText(message, 200)}
2803
+ `);
2804
+ }
2805
+ /**
2806
+ * Whether the resolved session is SHARED across senders. `single` collapses
2807
+ * the whole channel to one `__single__` session for EVERY sender — group OR
2808
+ * DM — so it is ALWAYS shared (even a DM maps to `__single__`). `thread` is
2809
+ * shared only in a group (a DM maps to the lone caller's own chat). `user` is
2810
+ * per-sender, never shared. Drives both the destructive-/clear confirm gate
2811
+ * and the host-shell (`!`) gate.
2812
+ */
2813
+ isSharedSession(envelope) {
2814
+ return this.isSharedSessionTarget(envelope);
2815
+ }
2816
+ isSharedSessionTarget(target) {
2817
+ return this.config.sessionScope === "single" || target.isGroup === true && this.config.sessionScope === "thread";
2818
+ }
2819
+ /**
2820
+ * Whether `envelope.senderId` may act on the resolved session's destructive or
2821
+ * workspace-leaking commands (/clear, /who). A SHARED session with a non-empty
2822
+ * allowedUsers list is restricted to those members; a per-user session, or one
2823
+ * with no allowlist, is unrestricted. Shared verbatim by /clear and /who so the
2824
+ * gate can't drift; each caller sends its own rejection wording.
2825
+ */
2826
+ isAuthorizedForSharedSession(envelope) {
2827
+ if (!this.isSharedSession(envelope))
2828
+ return true;
2829
+ const authorized = this.config.allowedUsers;
2830
+ return authorized.length === 0 || authorized.includes(envelope.senderId);
2831
+ }
2832
+ stopActiveStreaming(active, sessionId, reason) {
2833
+ try {
2834
+ active.stopStreaming?.();
2835
+ } catch (err) {
2836
+ process.stderr.write(`[${this.name}] stopStreaming threw during ${reason} for session ${sessionId}: ${err instanceof Error ? err.message : err}
2837
+ `);
2838
+ }
2839
+ }
2840
+ /**
2841
+ * Cancel the active turn and wait (bounded) for it to wind down. Stops the
2842
+ * BlockStreamer so buffered text can't leak via the idle timer, then fires a
2843
+ * best-effort cancelSession (NOT awaited — a wedged child/daemon can leave the
2844
+ * request pending forever). Returns true if active.done settled first, false
2845
+ * if the CLEAR_CANCEL_TIMEOUT_MS bound won (the turn never wound down). Used by
2846
+ * /clear, which genuinely EVICTS the session and so must proceed even when the
2847
+ * turn is wedged. Steer no longer uses this: it best-effort cancels then chains
2848
+ * the new turn behind the old one (see handleInbound), so it never needs to
2849
+ * proceed past a still-active turn.
2850
+ */
2851
+ async cancelAndAwaitActive(active, sessionId) {
2852
+ active.cancelled = true;
2853
+ this.stopActiveStreaming(active, sessionId, "cancel");
2854
+ void this.bridge.cancelSession(sessionId).catch((err) => {
2855
+ process.stderr.write(`[${this.name}] cancelSession failed for session=${sessionId} (clear/await): ${err instanceof Error ? err.message : err}
2856
+ `);
2857
+ });
2858
+ let timer;
2859
+ const settled = await Promise.race([
2860
+ active.done.then(() => true),
2861
+ new Promise((resolve2) => {
2862
+ timer = setTimeout(() => resolve2(false), CLEAR_CANCEL_TIMEOUT_MS);
2863
+ })
2864
+ ]);
2865
+ clearTimeout(timer);
2866
+ return settled;
2867
+ }
2868
+ /**
2869
+ * Parse a slash command from message text.
2870
+ * Returns { command, raw, args } or null if not a slash command. `command` is
2871
+ * lowercased for case-insensitive LOCAL dispatch (registerCommand lowercases the
2872
+ * names it stores); `raw` keeps the typed case so agent-command matching can be
2873
+ * CASE-SENSITIVE, mirroring the CLI's parseSlashCommand (`cmd.name === part`).
2874
+ */
2875
+ parseCommand(text) {
2876
+ const trimmed = text.trim();
2877
+ if (!trimmed.startsWith("/"))
2878
+ return null;
2879
+ const match = trimmed.match(PARSE_COMMAND_RE);
2880
+ if (!match)
2881
+ return null;
2882
+ return {
2883
+ command: match[1].toLowerCase(),
2884
+ raw: match[1],
2885
+ args: match[2].trim()
2886
+ };
2887
+ }
2888
+ /**
2889
+ * Whether `text` is a real slash command rather than prose that merely starts
2890
+ * with `/`. A command's first whitespace-delimited token must match
2891
+ * parseCommand()'s charset — `[a-zA-Z0-9_:-]+`, plus an optional `@botname`
2892
+ * suffix — and not be a `//` line comment or `/*` block comment. Slash-prefixed
2893
+ * paths (`/tmp/foo`), comments, and a bare `/` are prose and keep their
2894
+ * `[sender]` tag.
2895
+ *
2896
+ * Intentionally stricter than the CLI's looser classifier (cli
2897
+ * `ui/utils/commandUtils.ts`), which forwards any non-comment, non-path
2898
+ * `/<token>` (e.g. `/café`, a zero-width-laden token). Such inputs aren't
2899
+ * runnable commands, and in a SHARED group session forwarding them unattributed
2900
+ * is worse than a redundant tag — so anything off the command charset is
2901
+ * treated as prose and keeps its `[sender]` tag. Purely lexical — never
2902
+ * consults the async command list, so it can't race a fresh session.
2903
+ */
2904
+ isSlashCommand(text) {
2905
+ const trimmed = text.trim();
2906
+ if (!trimmed.startsWith("/") || trimmed.startsWith("//") || trimmed.startsWith("/*")) {
2907
+ return false;
2908
+ }
2909
+ const firstToken = trimmed.slice(1).split(/\s+/u)[0] ?? "";
2910
+ return COMMAND_TOKEN_RE.test(firstToken);
2911
+ }
2912
+ /**
2913
+ * Whether `text` names a command this channel can actually run: a locally
2914
+ * registered command (`this.commands`, e.g. /clear, /who) OR an agent command
2915
+ * THIS session exposes — by canonical name OR alias (e.g. `/summarize` for
2916
+ * `/compress`). Paired with isSlashCommand so the `[sender]` attribution tag is
2917
+ * suppressed ONLY for RECOGNIZED commands; command-SHAPED-but-unrecognized text
2918
+ * (e.g. `/x\n[SYSTEM]: …`) keeps its tag rather than reaching a shared group
2919
+ * unattributed, where an injected second line is more likely read as a system
2920
+ * directive. Purely synchronous, like isSlashCommand: it reads the session's
2921
+ * availableCommands snapshot WITHOUT awaiting, so it never races a fresh session
2922
+ * (a genuine agent command sent before the snapshot loads is treated as
2923
+ * unrecognized and KEEPS its tag — the safe default).
2924
+ */
2925
+ isRecognizedCommand(text, sessionId) {
2926
+ const parsed = this.parseCommand(text);
2927
+ if (!parsed)
2928
+ return false;
2929
+ if (this.commands.has(parsed.command))
2930
+ return true;
2931
+ const token = text.trim().slice(1).split(/\s+/u)[0] ?? "";
2932
+ return this.getAgentCommandsForSession(sessionId).some((cmd) => cmd.name === token || Array.isArray(cmd.altNames) && cmd.altNames.includes(token));
2933
+ }
2934
+ /**
2935
+ * The agent-command snapshot for THIS session. DaemonChannelBridge keys
2936
+ * commands per session, so its global `availableCommands` getter can return
2937
+ * ANOTHER session's list — prefer its getAvailableCommands(sessionId) when
2938
+ * present. AcpBridge runs a single agent and exposes only the global getter
2939
+ * (inherently session-correct), so fall back to it. Synchronous, matching
2940
+ * isRecognizedCommand's no-await contract.
2941
+ */
2942
+ getAgentCommandsForSession(sessionId) {
2943
+ const bridge = this.bridge;
2944
+ if (typeof bridge.getAvailableCommands === "function") {
2945
+ return bridge.getAvailableCommands(sessionId) ?? [];
2946
+ }
2947
+ return bridge.availableCommands ?? [];
2948
+ }
2949
+ groupHistoryKey(envelope) {
2950
+ return JSON.stringify([
2951
+ this.name,
2952
+ envelope.chatId,
2953
+ envelope.threadId ?? null
2954
+ ]);
2955
+ }
2956
+ groupHistoryLimit(envelope) {
2957
+ if (!envelope.isGroup) {
2958
+ return 0;
2959
+ }
2960
+ const groupCfg = this.config.groups[envelope.chatId];
2961
+ const wildcardGroupCfg = this.config.groups["*"];
2962
+ const configured = groupCfg?.groupHistoryLimit ?? wildcardGroupCfg?.groupHistoryLimit ?? this.config.groupHistoryLimit ?? 0;
2963
+ if (!Number.isFinite(configured) || configured <= 0) {
2964
+ return 0;
2965
+ }
2966
+ return Math.floor(configured);
2967
+ }
2968
+ recordPendingGroupHistory(envelope) {
2969
+ const limit = this.groupHistoryLimit(envelope);
2970
+ if (limit <= 0 || envelope.text.trim().length === 0) {
2971
+ return;
2972
+ }
2973
+ const senderId = truncateGroupHistoryField(envelope.senderId);
2974
+ if (!this.gate.isAllowed(senderId)) {
2975
+ return;
2976
+ }
2977
+ const entry = {
2978
+ senderId,
2979
+ senderName: truncateGroupHistoryField(envelope.senderName),
2980
+ text: envelope.text.slice(0, GROUP_HISTORY_ENTRY_TEXT_LIMIT),
2981
+ messageId: envelope.messageId === void 0 ? void 0 : truncateGroupHistoryField(envelope.messageId),
2982
+ timestamp: Date.now()
2983
+ };
2984
+ try {
2985
+ this.groupHistory.record(this.groupHistoryKey(envelope), entry, limit);
2986
+ } catch (err) {
2987
+ process.stderr.write(`[${this.name}] failed to record group history for chat ${sanitizeLogText(envelope.chatId, 64)}: ${err instanceof Error ? err.message : err}
2988
+ `);
2989
+ }
2990
+ }
2991
+ drainPendingGroupHistory(envelope) {
2992
+ const limit = this.groupHistoryLimit(envelope);
2993
+ if (limit <= 0) {
2994
+ return [];
2995
+ }
2996
+ try {
2997
+ return this.groupHistory.drain(this.groupHistoryKey(envelope), limit);
2998
+ } catch (err) {
2999
+ process.stderr.write(`[${this.name}] failed to drain group history for chat ${sanitizeLogText(envelope.chatId, 64)}: ${err instanceof Error ? err.message : err}
3000
+ `);
3001
+ return [];
3002
+ }
3003
+ }
3004
+ clearPendingGroupHistory(envelope) {
3005
+ if (!envelope.isGroup && this.config.sessionScope !== "single") {
3006
+ return;
3007
+ }
3008
+ try {
3009
+ if (this.config.sessionScope === "single") {
3010
+ this.groupHistory.clearAll();
3011
+ } else {
3012
+ this.groupHistory.clear(this.groupHistoryKey(envelope));
3013
+ }
3014
+ } catch (err) {
3015
+ process.stderr.write(`[${this.name}] failed to clear group history for chat ${sanitizeLogText(envelope.chatId, 64)}: ${err instanceof Error ? err.message : err}
3016
+ `);
3017
+ }
3018
+ }
3019
+ prependGroupHistoryContext(promptText, entries) {
3020
+ if (entries.length === 0) {
3021
+ return promptText;
3022
+ }
3023
+ const lines = entries.filter((entry) => this.gate.isAllowed(entry.senderId));
3024
+ if (lines.length === 0) {
3025
+ return promptText;
3026
+ }
3027
+ const formatted = lines.map((entry) => {
3028
+ const who = sanitizeSenderName(entry.senderName || entry.senderId);
3029
+ const text = sanitizeQuotedText(entry.text, GROUP_HISTORY_ENTRY_TEXT_LIMIT);
3030
+ return `- [${who}] ${text}`;
3031
+ });
3032
+ return `${GROUP_HISTORY_CONTEXT_MARKER}
3033
+ ${formatted.join("\n")}
3034
+
3035
+ ${CURRENT_MESSAGE_MARKER}
3036
+ ${promptText}`;
3037
+ }
3038
+ async handleInbound(envelope) {
3039
+ const groupResult = this.groupGate.check(envelope);
3040
+ if (!groupResult.allowed) {
3041
+ if (groupResult.reason === "mention_required") {
3042
+ this.recordPendingGroupHistory(envelope);
3043
+ }
3044
+ return;
3045
+ }
3046
+ const result = this.gate.check(envelope.senderId, envelope.senderName);
3047
+ if (!result.allowed) {
3048
+ if (result.pairingCode !== void 0) {
3049
+ await this.onPairingRequired(envelope.chatId, result.pairingCode);
3050
+ }
3051
+ return;
3052
+ }
3053
+ const parsed = this.parseCommand(envelope.text);
3054
+ if (parsed) {
3055
+ const handler = this.commands.get(parsed.command);
3056
+ if (handler) {
3057
+ const handled = await handler(envelope, parsed.args);
3058
+ if (handled)
3059
+ return;
3060
+ }
3061
+ }
3062
+ const bangText = envelope.text.trimStart();
3063
+ if (bangText.startsWith("!")) {
3064
+ if (envelope.isGroup || this.isSharedSession(envelope)) {
3065
+ const who = sanitizeSenderName(envelope.senderName || envelope.senderId || "unknown");
3066
+ process.stderr.write(`[${this.name}] blocked ! shell command from ${who} (sender ${envelope.senderId}) in chat ${sanitizeLogText(envelope.chatId, 64)}
3067
+ `);
3068
+ }
3069
+ if (envelope.isGroup) {
3070
+ await this.sendMessage(envelope.chatId, "Shell commands (`!`) are disabled in group chats.");
3071
+ return;
3072
+ }
3073
+ if (this.isSharedSession(envelope)) {
3074
+ await this.sendMessage(envelope.chatId, "Shell commands (`!`) are disabled in shared sessions.");
3075
+ return;
3076
+ }
3077
+ }
3078
+ const sessionId = await this.router.resolve(this.name, envelope.senderId, envelope.chatId, envelope.threadId, this.config.cwd);
3079
+ if (bangText.startsWith("!")) {
3080
+ const cmd = bangText.slice(1).trim();
3081
+ const bridgeShellCommand = this.bridge.shellCommand;
3082
+ if (cmd && bridgeShellCommand) {
3083
+ try {
3084
+ const result2 = await bridgeShellCommand(sessionId, cmd);
3085
+ const longestRun = Math.max(0, ...Array.from((result2.output || "").matchAll(/`+/g), (m) => m[0].length));
3086
+ const fence = "`".repeat(Math.max(3, longestRun + 1));
3087
+ const output = result2.output ? `${fence}
3088
+ ${result2.output}
3089
+ ${fence}` : "(no output)";
3090
+ const exitLine = result2.exitCode !== null && result2.exitCode !== 0 ? `
3091
+ Exit code: ${result2.exitCode}` : "";
3092
+ await this.sendMessage(envelope.chatId, `$ ${cmd}
3093
+ ${output}${exitLine}`);
3094
+ } catch (error) {
3095
+ await this.sendMessage(envelope.chatId, `Shell command failed: ${error instanceof Error ? error.message : String(error)}`);
3096
+ }
3097
+ return;
3098
+ }
3099
+ }
3100
+ const recognizedSlashCommand = this.isSlashCommand(envelope.text) && this.isRecognizedCommand(envelope.text, sessionId);
3101
+ let promptText = envelope.text;
3102
+ if ((envelope.isGroup || this.config.sessionScope === "single") && !envelope.alreadyPrefixed && !recognizedSlashCommand) {
3103
+ const who = sanitizeSenderName(envelope.senderName || envelope.senderId || "unknown");
3104
+ promptText = `[${who}] ${sanitizePromptText(promptText)}`;
3105
+ }
3106
+ if (envelope.referencedText) {
3107
+ const quoted = sanitizeQuotedText(envelope.referencedText, 500);
3108
+ promptText = `[Replying to: "${quoted}"]
3109
+
3110
+ ${promptText}`;
3111
+ }
3112
+ let imageBase64 = envelope.imageBase64;
3113
+ let imageMimeType = envelope.imageMimeType;
3114
+ if (envelope.attachments?.length) {
3115
+ const filePaths = [];
3116
+ for (const att of envelope.attachments) {
3117
+ if (att.type === "image" && att.data && !imageBase64) {
3118
+ imageBase64 = att.data;
3119
+ imageMimeType = att.mimeType;
3120
+ } else if (att.filePath) {
3121
+ const label = att.type === "file" ? "file" : att.type;
3122
+ const name = att.fileName ? ` "${sanitizeQuotedText(att.fileName, 128)}"` : "";
3123
+ const renderedPath = sanitizePromptPath(att.filePath);
3124
+ filePaths.push(`User sent a ${label}${name}. It has been saved to: ${renderedPath}`);
3125
+ }
3126
+ }
3127
+ if (filePaths.length > 0) {
3128
+ promptText = promptText + "\n\n" + filePaths.join("\n");
3129
+ }
3130
+ }
3131
+ const groupCfg = envelope.isGroup ? this.config.groups[envelope.chatId] || this.config.groups["*"] : void 0;
3132
+ const mode = groupCfg?.dispatchMode || this.config.dispatchMode || "steer";
3133
+ const active = this.activePrompts.get(sessionId);
3134
+ let steerWatchdog;
3135
+ if (active) {
3136
+ switch (mode) {
3137
+ case "collect": {
3138
+ let buffer = this.collectBuffers.get(sessionId);
3139
+ if (!buffer) {
3140
+ buffer = [];
3141
+ this.collectBuffers.set(sessionId, buffer);
3142
+ }
3143
+ buffer.push({ text: promptText, envelope });
3144
+ return;
3145
+ }
3146
+ case "steer": {
3147
+ if (!this.isAuthorizedForSharedSession(envelope)) {
3148
+ process.stderr.write(`[${this.name}] steer denied for ${envelope.senderId} in shared session (chat=${sanitizeLogText(envelope.chatId, 64)}); queuing instead
3149
+ `);
3150
+ break;
3151
+ }
3152
+ active.cancelled = true;
3153
+ process.stderr.write(`[${this.name}] steer: cancelled active turn for ${envelope.senderId} in session ${sessionId}
3154
+ `);
3155
+ this.stopActiveStreaming(active, sessionId, "steer");
3156
+ void this.bridge.cancelSession(sessionId).catch((err) => {
3157
+ process.stderr.write(`[${this.name}] cancelSession failed for session=${sessionId} (steer): ${err instanceof Error ? err.message : err}
3158
+ `);
3159
+ });
3160
+ steerWatchdog = setTimeout(() => {
3161
+ if (this.activePrompts.get(sessionId) === active) {
3162
+ process.stderr.write(`[${this.name}] steer queued behind active turn for session ${sessionId}: still waiting after ${CLEAR_CANCEL_TIMEOUT_MS}ms (use /clear to recover)
3163
+ `);
3164
+ }
3165
+ }, CLEAR_CANCEL_TIMEOUT_MS);
3166
+ steerWatchdog.unref?.();
3167
+ promptText = `[The user sent a new message while you were working. Their previous request has been cancelled.]
3168
+
3169
+ ${promptText}`;
3170
+ break;
3171
+ }
3172
+ case "followup": {
3173
+ break;
3174
+ }
3175
+ default: {
3176
+ const _exhaustive = mode;
3177
+ throw new Error(`Unknown dispatch mode: ${_exhaustive}`);
3178
+ }
3179
+ }
3180
+ }
3181
+ let shouldPrependSessionContext = !this.instructedSessions.has(sessionId);
3182
+ if (shouldPrependSessionContext) {
3183
+ this.instructedSessions.add(sessionId);
3184
+ }
3185
+ const prev = this.sessionQueues.get(sessionId) ?? Promise.resolve();
3186
+ const generation = this.sessionGenerations.get(sessionId) ?? 0;
3187
+ const useBlockStreaming = this.config.blockStreaming === "on";
3188
+ const current = prev.then(async () => {
3189
+ clearTimeout(steerWatchdog);
3190
+ if (this.dropQueuedTurnIfStale(sessionId, generation, envelope)) {
3191
+ return;
3192
+ }
3193
+ if (!shouldPrependSessionContext && !this.instructedSessions.has(sessionId)) {
3194
+ shouldPrependSessionContext = true;
3195
+ this.instructedSessions.add(sessionId);
3196
+ }
3197
+ const sessionContext = [];
3198
+ if (shouldPrependSessionContext) {
3199
+ let memoryText;
3200
+ if (this.channelMemory && this.isAuthorizedForChannelMemory(envelope) && (!this.isSharedSession(envelope) || this.config.senderPolicy === "allowlist")) {
3201
+ try {
3202
+ memoryText = (await this.channelMemory.readChannelMemory(this.channelMemoryTarget(envelope)))?.trim();
3203
+ } catch (error) {
3204
+ this.logChannelMemoryError("read", envelope, this.channelMemoryErrorMessage(error));
3205
+ this.instructedSessions.delete(sessionId);
3206
+ }
3207
+ }
3208
+ if (memoryText) {
3209
+ sessionContext.push(`Channel memory for this chat:
3210
+ ${sanitizePromptText(memoryText)}`);
3211
+ }
3212
+ if (this.config.instructions) {
3213
+ sessionContext.push(this.config.instructions);
3214
+ }
3215
+ }
3216
+ if (this.dropQueuedTurnIfStale(sessionId, generation, envelope)) {
3217
+ return;
3218
+ }
3219
+ const groupHistoryEntries = recognizedSlashCommand ? [] : this.drainPendingGroupHistory(envelope);
3220
+ let promptToSend = this.prependGroupHistoryContext(promptText, groupHistoryEntries);
3221
+ if (sessionContext.length > 0) {
3222
+ promptToSend = `${sessionContext.join("\n\n")}
3223
+
3224
+ ${promptToSend}`;
3225
+ }
3226
+ let doneResolve = /* @__PURE__ */ __name(() => {
3227
+ }, "doneResolve");
3228
+ const done = new Promise((r) => {
3229
+ doneResolve = r;
3230
+ });
3231
+ const promptState = {
3232
+ cancelled: false,
3233
+ done,
3234
+ resolve: doneResolve,
3235
+ chatId: envelope.chatId,
3236
+ messageId: envelope.messageId
3237
+ };
3238
+ this.activePrompts.set(sessionId, promptState);
3239
+ this.onPromptStart(envelope.chatId, sessionId, envelope.messageId);
3240
+ const streamer = useBlockStreaming ? new BlockStreamer({
3241
+ minChars: this.config.blockStreamingChunk?.minChars ?? 400,
3242
+ maxChars: this.config.blockStreamingChunk?.maxChars ?? 1e3,
3243
+ idleMs: this.config.blockStreamingCoalesce?.idleMs ?? 1500,
3244
+ send: /* @__PURE__ */ __name((text) => this.sendMessage(envelope.chatId, text), "send")
3245
+ }) : null;
3246
+ promptState.stopStreaming = () => streamer?.stop();
3247
+ const onChunk = /* @__PURE__ */ __name((sid, chunk) => {
3248
+ if (sid === sessionId && !promptState.cancelled) {
3249
+ this.onResponseChunk(envelope.chatId, chunk, sessionId);
3250
+ streamer?.push(chunk);
3251
+ }
3252
+ }, "onChunk");
3253
+ const promptBridge = this.bridge;
3254
+ promptBridge.on("textChunk", onChunk);
3255
+ try {
3256
+ const response = await promptBridge.prompt(sessionId, promptToSend, {
3257
+ imageBase64,
3258
+ imageMimeType
3259
+ });
3260
+ if (promptState.cancelRequested && !promptState.cancelled) {
3261
+ const cancelled = await promptState.cancelRequested;
3262
+ if (cancelled) {
3263
+ promptState.cancelled = true;
3264
+ }
3265
+ }
3266
+ if (!promptState.cancelled && response) {
3267
+ if (streamer) {
3268
+ await streamer.flush();
3269
+ } else {
3270
+ await this.onResponseComplete(envelope.chatId, response, sessionId);
3271
+ }
3272
+ }
3273
+ } finally {
3274
+ promptBridge.off("textChunk", onChunk);
3275
+ streamer?.stop();
3276
+ const stillCurrent = this.activePrompts.get(sessionId) === promptState;
3277
+ if (!promptState.clearEvicted) {
3278
+ try {
3279
+ this.onPromptEnd(envelope.chatId, sessionId, envelope.messageId);
3280
+ } catch (err) {
3281
+ process.stderr.write(`[${this.name}] onPromptEnd threw in finally for session ${sessionId}: ${err instanceof Error ? err.message : err}
3282
+ `);
3283
+ }
3284
+ }
3285
+ if (stillCurrent) {
3286
+ this.activePrompts.delete(sessionId);
3287
+ }
3288
+ promptState.resolve();
3289
+ const buffer = this.collectBuffers.get(sessionId);
3290
+ if (stillCurrent && buffer && buffer.length > 0) {
3291
+ this.collectBuffers.delete(sessionId);
3292
+ const lost = buffer.length;
3293
+ const coalesced = buffer.map((b) => b.text).join("\n\n");
3294
+ const lastEnvelope = buffer[buffer.length - 1].envelope;
3295
+ const syntheticEnvelope = {
3296
+ ...lastEnvelope,
3297
+ text: coalesced,
3298
+ // Coalesced text already carries each message's [sender] prefix.
3299
+ alreadyPrefixed: true,
3300
+ // Clear attachments/references — already resolved in original text
3301
+ referencedText: void 0,
3302
+ attachments: void 0,
3303
+ imageBase64: void 0,
3304
+ imageMimeType: void 0
3305
+ };
3306
+ this.handleInbound(syntheticEnvelope).catch((err) => {
3307
+ process.stderr.write(`[${this.name}] dropped ${lost} buffered message(s) on collect re-entry for session ${sessionId} (last sender ${lastEnvelope.senderId}): ${err instanceof Error ? err.message : String(err)}
3308
+ `);
3309
+ });
3310
+ }
3311
+ }
3312
+ });
3313
+ this.sessionQueues.set(sessionId, current.catch(() => {
3314
+ }));
3315
+ await current;
3316
+ }
3317
+ async onPairingRequired(chatId, code) {
3318
+ if (code) {
3319
+ await this.sendMessage(chatId, `Your pairing code is: ${code}
3320
+
3321
+ Ask the bot operator to approve you with:
3322
+ qwen channel pairing approve ${this.name} ${code}`);
3323
+ } else {
3324
+ await this.sendMessage(chatId, "Too many pending pairing requests. Please try again later.");
3325
+ }
3326
+ }
3327
+ };
3328
+ function truncateGroupHistoryField(value) {
3329
+ return value.slice(0, GROUP_HISTORY_ENTRY_METADATA_LIMIT);
3330
+ }
3331
+ __name(truncateGroupHistoryField, "truncateGroupHistoryField");
3332
+ function truncateLoopLabel(prompt) {
3333
+ const chars = Array.from(prompt);
3334
+ return chars.length > 60 ? `${chars.slice(0, 57).join("")}...` : prompt;
3335
+ }
3336
+ __name(truncateLoopLabel, "truncateLoopLabel");
3337
+
3338
+ // packages/channels/base/dist/ChannelLoopStore.js
3339
+ init_esbuild_shims();
3340
+ import * as crypto2 from "node:crypto";
3341
+ import * as fs2 from "node:fs/promises";
3342
+ import * as path3 from "node:path";
3343
+ var ChannelLoopStore = class {
3344
+ static {
3345
+ __name(this, "ChannelLoopStore");
3346
+ }
3347
+ filePath;
3348
+ now;
3349
+ idFactory;
3350
+ pendingUpdate = Promise.resolve();
3351
+ constructor(options) {
3352
+ this.filePath = options.filePath;
3353
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
3354
+ this.idFactory = options.idFactory ?? (() => crypto2.randomUUID());
3355
+ }
3356
+ async list() {
3357
+ return this.readJobs();
3358
+ }
3359
+ async listForTarget(channelName, target) {
3360
+ const jobs = await this.readJobs();
3361
+ return jobs.filter((job) => job.channelName === channelName && sameTarget(job.target, target));
3362
+ }
3363
+ async create(input) {
3364
+ let job;
3365
+ await this.updateJobs((jobs) => {
3366
+ job = this.buildLoop(input, jobs);
3367
+ return [...jobs, job];
3368
+ });
3369
+ if (!job)
3370
+ throw new Error("Failed to create channel loop.");
3371
+ return job;
3372
+ }
3373
+ async createForTarget(input, maxEnabledLoops) {
3374
+ let created;
3375
+ await this.updateJobs((jobs) => {
3376
+ const enabledForTarget = jobs.filter((job2) => job2.enabled && job2.channelName === input.channelName && sameTarget(job2.target, input.target)).length;
3377
+ if (enabledForTarget >= maxEnabledLoops) {
3378
+ return jobs;
3379
+ }
3380
+ const job = this.buildLoop(input, jobs);
3381
+ created = job;
3382
+ return [...jobs, job];
3383
+ });
3384
+ return created;
3385
+ }
3386
+ async update(id, patch) {
3387
+ let found = false;
3388
+ await this.updateJobs((jobs) => jobs.map((job) => {
3389
+ if (job.id !== id)
3390
+ return job;
3391
+ found = true;
3392
+ return { ...job, ...patch };
3393
+ }));
3394
+ return found;
3395
+ }
3396
+ async disable(id) {
3397
+ return this.update(id, { enabled: false });
3398
+ }
3399
+ buildLoop(input, existingLoops) {
3400
+ const existingIds = new Set(existingLoops.map((loop) => loop.id));
3401
+ const baseId = this.idFactory();
3402
+ let id = baseId;
3403
+ let suffix = 1;
3404
+ while (existingIds.has(id)) {
3405
+ id = `${baseId}-${suffix++}`;
3406
+ }
3407
+ return {
3408
+ ...input,
3409
+ id,
3410
+ target: normalizeTarget(input.target),
3411
+ enabled: true,
3412
+ createdAt: this.now().toISOString(),
3413
+ consecutiveFailures: 0,
3414
+ runCount: 0
3415
+ };
3416
+ }
3417
+ async updateJobs(mutate) {
3418
+ const nextUpdate = this.pendingUpdate.then(async () => {
3419
+ const jobs = await this.readJobs();
3420
+ await this.writeJobs(mutate(jobs));
3421
+ });
3422
+ this.pendingUpdate = nextUpdate.catch(() => {
3423
+ });
3424
+ await nextUpdate;
3425
+ }
3426
+ async readJobs() {
3427
+ let raw;
3428
+ try {
3429
+ raw = await fs2.readFile(this.filePath, "utf8");
3430
+ } catch (err) {
3431
+ if (err.code === "ENOENT")
3432
+ return [];
3433
+ throw err;
3434
+ }
3435
+ let parsed;
3436
+ try {
3437
+ parsed = JSON.parse(raw);
3438
+ } catch {
3439
+ throw new Error(`Malformed JSON in ${this.filePath}; fix or delete the file.`);
3440
+ }
3441
+ if (!Array.isArray(parsed)) {
3442
+ throw new Error(`Expected a JSON array in ${this.filePath}; fix or delete the file.`);
3443
+ }
3444
+ const jobs = [];
3445
+ for (const [index, value] of parsed.entries()) {
3446
+ if (!isChannelLoop(value)) {
3447
+ process.stderr.write(`Invalid channel loop at index ${index} in ${this.filePath}: ${JSON.stringify(value)}
3448
+ `);
3449
+ continue;
3450
+ }
3451
+ jobs.push(normalizeJob(value));
3452
+ }
3453
+ return jobs;
3454
+ }
3455
+ async writeJobs(jobs) {
3456
+ const dir = path3.dirname(this.filePath);
3457
+ await fs2.mkdir(dir, { recursive: true, mode: 448 });
3458
+ await fs2.chmod(dir, 448).catch(() => {
3459
+ });
3460
+ const tmpPath = `${this.filePath}.${crypto2.randomBytes(6).toString("hex")}.tmp`;
3461
+ try {
3462
+ await fs2.writeFile(tmpPath, JSON.stringify(jobs, null, 2), {
3463
+ encoding: "utf8",
3464
+ mode: 384
3465
+ });
3466
+ await fs2.rename(tmpPath, this.filePath);
3467
+ await fs2.chmod(this.filePath, 384).catch(() => {
3468
+ });
3469
+ } catch (err) {
3470
+ await fs2.rm(tmpPath, { force: true }).catch(() => {
3471
+ });
3472
+ throw err;
3473
+ }
3474
+ }
3475
+ };
3476
+ function sameTarget(a, b) {
3477
+ return a.channelName === b.channelName && a.senderId === b.senderId && a.chatId === b.chatId && a.threadId === b.threadId && a.isGroup === b.isGroup;
3478
+ }
3479
+ __name(sameTarget, "sameTarget");
3480
+ function normalizeTarget(target) {
3481
+ return {
3482
+ ...target,
3483
+ isGroup: target.isGroup === void 0 ? void 0 : target.isGroup === true
3484
+ };
3485
+ }
3486
+ __name(normalizeTarget, "normalizeTarget");
3487
+ function isSessionTarget(value) {
3488
+ if (typeof value !== "object" || value === null)
3489
+ return false;
3490
+ const target = value;
3491
+ return typeof target["channelName"] === "string" && typeof target["senderId"] === "string" && typeof target["chatId"] === "string" && (target["threadId"] === void 0 || typeof target["threadId"] === "string") && (target["isGroup"] === void 0 || typeof target["isGroup"] === "boolean");
3492
+ }
3493
+ __name(isSessionTarget, "isSessionTarget");
3494
+ function isChannelLoop(value) {
3495
+ if (typeof value !== "object" || value === null)
3496
+ return false;
3497
+ const job = value;
3498
+ return typeof job["id"] === "string" && typeof job["channelName"] === "string" && isSessionTarget(job["target"]) && typeof job["cwd"] === "string" && typeof job["cron"] === "string" && typeof job["prompt"] === "string" && (job["label"] === void 0 || typeof job["label"] === "string") && typeof job["recurring"] === "boolean" && typeof job["enabled"] === "boolean" && typeof job["createdBy"] === "string" && typeof job["createdAt"] === "string" && (job["lastFiredAt"] === void 0 || typeof job["lastFiredAt"] === "string") && (job["lastFinishedAt"] === void 0 || typeof job["lastFinishedAt"] === "string") && (job["lastResultPreview"] === void 0 || typeof job["lastResultPreview"] === "string") && (job["lastStatus"] === void 0 || job["lastStatus"] === "ok" || job["lastStatus"] === "error") && (job["lastError"] === void 0 || typeof job["lastError"] === "string") && typeof job["consecutiveFailures"] === "number" && (job["runningSince"] === void 0 || typeof job["runningSince"] === "string") && (job["runCount"] === void 0 || typeof job["runCount"] === "number");
3499
+ }
3500
+ __name(isChannelLoop, "isChannelLoop");
3501
+ function normalizeJob(job) {
3502
+ return {
3503
+ ...job,
3504
+ target: normalizeTarget(job.target),
3505
+ runCount: job.runCount ?? 0
3506
+ };
3507
+ }
3508
+ __name(normalizeJob, "normalizeJob");
3509
+
3510
+ export {
3511
+ resolvePath,
3512
+ getGlobalQwenDir,
3513
+ AcpBridge,
3514
+ DaemonChannelBridge,
3515
+ PairingStore,
3516
+ sanitizeSenderName,
3517
+ sanitizePromptText,
3518
+ sanitizeLogText,
3519
+ SessionRouter,
3520
+ ChannelLoopScheduler,
3521
+ ChannelBase,
3522
+ ChannelLoopStore
3523
+ };