@adelie/server 0.0.0-stage → 0.4.2

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 (455) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +53 -2
  3. package/dist/api/types.d.ts +6195 -0
  4. package/dist/api/types.js +10 -0
  5. package/dist/api/types.js.map +1 -0
  6. package/dist/auth-token.d.ts +50 -0
  7. package/dist/auth-token.js +55 -0
  8. package/dist/auth-token.js.map +1 -0
  9. package/dist/chunk-B5IP37ZQ.js +56 -0
  10. package/dist/chunk-B5IP37ZQ.js.map +1 -0
  11. package/dist/chunk-BQZC56BR.js +78 -0
  12. package/dist/chunk-BQZC56BR.js.map +1 -0
  13. package/dist/chunk-C2S2LVQ7.js +14 -0
  14. package/dist/chunk-C2S2LVQ7.js.map +1 -0
  15. package/dist/chunk-CXGWSWJA.js +485 -0
  16. package/dist/chunk-CXGWSWJA.js.map +1 -0
  17. package/dist/chunk-GO6G445C.js +83 -0
  18. package/dist/chunk-GO6G445C.js.map +1 -0
  19. package/dist/chunk-HMTT2PX4.js +93 -0
  20. package/dist/chunk-HMTT2PX4.js.map +1 -0
  21. package/dist/chunk-IZEMORXV.js +16 -0
  22. package/dist/chunk-IZEMORXV.js.map +1 -0
  23. package/dist/chunk-K4ZSUT72.js +34 -0
  24. package/dist/chunk-K4ZSUT72.js.map +1 -0
  25. package/dist/chunk-WPYTS7TI.js +178 -0
  26. package/dist/chunk-WPYTS7TI.js.map +1 -0
  27. package/dist/chunk-ZZUQGQD6.js +954 -0
  28. package/dist/chunk-ZZUQGQD6.js.map +1 -0
  29. package/dist/hmr/manifest.d.ts +1 -0
  30. package/dist/hmr/manifest.js +16 -0
  31. package/dist/hmr/manifest.js.map +1 -0
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.js +81551 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/initial-password.d.ts +14 -0
  36. package/dist/initial-password.js +12 -0
  37. package/dist/initial-password.js.map +1 -0
  38. package/dist/install.ps1 +492 -0
  39. package/dist/install.sh +494 -0
  40. package/dist/lock.d.ts +26 -0
  41. package/dist/lock.js +20 -0
  42. package/dist/lock.js.map +1 -0
  43. package/dist/machine-status.d.ts +18 -0
  44. package/dist/machine-status.js +38 -0
  45. package/dist/machine-status.js.map +1 -0
  46. package/dist/plugin/index.d.ts +3002 -0
  47. package/dist/plugin/index.js +1 -0
  48. package/dist/plugin/index.js.map +1 -0
  49. package/dist/reset-admin-password.d.ts +25 -0
  50. package/dist/reset-admin-password.js +45 -0
  51. package/dist/reset-admin-password.js.map +1 -0
  52. package/dist/secret-file.d.ts +3 -0
  53. package/dist/secret-file.js +19 -0
  54. package/dist/secret-file.js.map +1 -0
  55. package/dist/version-report.d.ts +12 -0
  56. package/dist/version-report.js +9 -0
  57. package/dist/version-report.js.map +1 -0
  58. package/package.json +106 -3
  59. package/web-dist/adelie-icon.svg +46 -0
  60. package/web-dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  61. package/web-dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  62. package/web-dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  63. package/web-dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  64. package/web-dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  65. package/web-dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  66. package/web-dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  67. package/web-dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  68. package/web-dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  69. package/web-dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  70. package/web-dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  71. package/web-dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  72. package/web-dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  73. package/web-dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  74. package/web-dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  75. package/web-dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  76. package/web-dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  77. package/web-dist/assets/KaTeX_Size3-Regular-gV2CO0n9.woff2 +0 -0
  78. package/web-dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  79. package/web-dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  80. package/web-dist/assets/addon-clipboard-BU5GZsef.js +1 -0
  81. package/web-dist/assets/addon-fit-B_mXSH9E.js +1 -0
  82. package/web-dist/assets/addon-web-links-CrIdpbXy.js +1 -0
  83. package/web-dist/assets/c-BG-tZi7o.js +1 -0
  84. package/web-dist/assets/c-BIGW1oBm.js +1 -0
  85. package/web-dist/assets/cpp-CaeG3uDQ.js +1 -0
  86. package/web-dist/assets/cpp-DIPi6g--.js +1 -0
  87. package/web-dist/assets/css-Br67jR8j.js +1 -0
  88. package/web-dist/assets/css-CLj8gQPS.js +1 -0
  89. package/web-dist/assets/diff-D97Zzqfu.js +1 -0
  90. package/web-dist/assets/diff-jW5kmiru.js +1 -0
  91. package/web-dist/assets/github-dark-BQV5ugrc.js +1 -0
  92. package/web-dist/assets/github-dark-DHJKELXO.js +1 -0
  93. package/web-dist/assets/github-light-DAi9KRSo.js +1 -0
  94. package/web-dist/assets/github-light-Rcb2q3L5.js +1 -0
  95. package/web-dist/assets/go-B7VTfpnm.js +1 -0
  96. package/web-dist/assets/go-C27-OAKa.js +1 -0
  97. package/web-dist/assets/highlighter-BDxr2Reu.js +2 -0
  98. package/web-dist/assets/highlighter-core-C91BgiW7.js +152 -0
  99. package/web-dist/assets/highlighter.worker-CPvC3l9a.js +152 -0
  100. package/web-dist/assets/html-BW_6VhB7.js +1 -0
  101. package/web-dist/assets/html-pp8916En.js +1 -0
  102. package/web-dist/assets/ibm-plex-sans-condensed-latin-600-normal-CRd5VyFf.woff2 +0 -0
  103. package/web-dist/assets/ibm-plex-sans-condensed-latin-ext-600-normal-CBdNZ9Ci.woff2 +0 -0
  104. package/web-dist/assets/ibm-plex-sans-latin-ext-wght-normal-CIII54If.woff2 +0 -0
  105. package/web-dist/assets/ibm-plex-sans-latin-wght-normal-IvpUvPa2.woff2 +0 -0
  106. package/web-dist/assets/index-By5tJCWt.js +1080 -0
  107. package/web-dist/assets/index-DU13e4im.css +1 -0
  108. package/web-dist/assets/ini-BEwlwnbL.js +1 -0
  109. package/web-dist/assets/ini-Dix3pTmg.js +1 -0
  110. package/web-dist/assets/java-BTDmaRNO.js +1 -0
  111. package/web-dist/assets/java-CylS5w8V.js +1 -0
  112. package/web-dist/assets/javascript-WyGyM1wV.js +1 -0
  113. package/web-dist/assets/javascript-wDzz0qaB.js +1 -0
  114. package/web-dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  115. package/web-dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  116. package/web-dist/assets/json-B8JryHcM.js +1 -0
  117. package/web-dist/assets/json-Cp-IABpG.js +1 -0
  118. package/web-dist/assets/jsx-8cLv2Six.js +1 -0
  119. package/web-dist/assets/jsx-g9-lgVsj.js +1 -0
  120. package/web-dist/assets/log-2UxHyX5q.js +1 -0
  121. package/web-dist/assets/log-CcaF_xNv.js +1 -0
  122. package/web-dist/assets/markdown-Cvjx9yec.js +1 -0
  123. package/web-dist/assets/markdown-DX-TNHnc.js +1 -0
  124. package/web-dist/assets/misans-400-100-DHkMCix2.woff2 +0 -0
  125. package/web-dist/assets/misans-400-101-CwgSLsmc.woff2 +0 -0
  126. package/web-dist/assets/misans-400-102-BQgefHPQ.woff2 +0 -0
  127. package/web-dist/assets/misans-400-103-CP58Mc_T.woff2 +0 -0
  128. package/web-dist/assets/misans-400-104-Ta_Xu1gd.woff2 +0 -0
  129. package/web-dist/assets/misans-400-105-CDop11AR.woff2 +0 -0
  130. package/web-dist/assets/misans-400-106-Dv8y0ubk.woff2 +0 -0
  131. package/web-dist/assets/misans-400-107-CjnHtp9E.woff2 +0 -0
  132. package/web-dist/assets/misans-400-108-BA8nQUBw.woff2 +0 -0
  133. package/web-dist/assets/misans-400-109-B6uEN0pG.woff2 +0 -0
  134. package/web-dist/assets/misans-400-110-BQqnShrY.woff2 +0 -0
  135. package/web-dist/assets/misans-400-111-CHB-Jj5X.woff2 +0 -0
  136. package/web-dist/assets/misans-400-112-D1dh4Cjx.woff2 +0 -0
  137. package/web-dist/assets/misans-400-113-Bw_wNCFp.woff2 +0 -0
  138. package/web-dist/assets/misans-400-114-CUb-5IzE.woff2 +0 -0
  139. package/web-dist/assets/misans-400-115-BICOatsO.woff2 +0 -0
  140. package/web-dist/assets/misans-400-116-B3_gmYW6.woff2 +0 -0
  141. package/web-dist/assets/misans-400-117-B8k_y3cv.woff2 +0 -0
  142. package/web-dist/assets/misans-400-118-C-2Cf5C6.woff2 +0 -0
  143. package/web-dist/assets/misans-400-119-CmLP7xnX.woff2 +0 -0
  144. package/web-dist/assets/misans-400-21-DsSSD_Zj.woff2 +0 -0
  145. package/web-dist/assets/misans-400-22-rtkxWOv4.woff2 +0 -0
  146. package/web-dist/assets/misans-400-23-CDDB4M1H.woff2 +0 -0
  147. package/web-dist/assets/misans-400-24-Dqf_XeOL.woff2 +0 -0
  148. package/web-dist/assets/misans-400-25-C4U1tJ5J.woff2 +0 -0
  149. package/web-dist/assets/misans-400-26-CEXOK-nT.woff2 +0 -0
  150. package/web-dist/assets/misans-400-27-BrNOAScF.woff2 +0 -0
  151. package/web-dist/assets/misans-400-28-CPpEnCkG.woff2 +0 -0
  152. package/web-dist/assets/misans-400-29-C9JMVw8g.woff2 +0 -0
  153. package/web-dist/assets/misans-400-30-CD-QimXa.woff2 +0 -0
  154. package/web-dist/assets/misans-400-31-S4k06NbS.woff2 +0 -0
  155. package/web-dist/assets/misans-400-32-zRMBDkz6.woff2 +0 -0
  156. package/web-dist/assets/misans-400-33-sQ_tNZSM.woff2 +0 -0
  157. package/web-dist/assets/misans-400-34-8LEGljja.woff2 +0 -0
  158. package/web-dist/assets/misans-400-35-CxSPyuiU.woff2 +0 -0
  159. package/web-dist/assets/misans-400-36-BY1i4WtO.woff2 +0 -0
  160. package/web-dist/assets/misans-400-37-C0oF42pu.woff2 +0 -0
  161. package/web-dist/assets/misans-400-38-y7xcB90w.woff2 +0 -0
  162. package/web-dist/assets/misans-400-39-C307YgZx.woff2 +0 -0
  163. package/web-dist/assets/misans-400-40-ByZr736f.woff2 +0 -0
  164. package/web-dist/assets/misans-400-41-ChKEw5zV.woff2 +0 -0
  165. package/web-dist/assets/misans-400-42-CD__zJu5.woff2 +0 -0
  166. package/web-dist/assets/misans-400-43-CqNV6_TG.woff2 +0 -0
  167. package/web-dist/assets/misans-400-44-zE-akmW8.woff2 +0 -0
  168. package/web-dist/assets/misans-400-45-Du98otwc.woff2 +0 -0
  169. package/web-dist/assets/misans-400-46-B6JXbEy2.woff2 +0 -0
  170. package/web-dist/assets/misans-400-47-DpqW0kSq.woff2 +0 -0
  171. package/web-dist/assets/misans-400-48-BprON5Q_.woff2 +0 -0
  172. package/web-dist/assets/misans-400-49-N-ct3sN6.woff2 +0 -0
  173. package/web-dist/assets/misans-400-5-ton_4jCN.woff2 +0 -0
  174. package/web-dist/assets/misans-400-50-JgU49P0U.woff2 +0 -0
  175. package/web-dist/assets/misans-400-51-DSS3Moge.woff2 +0 -0
  176. package/web-dist/assets/misans-400-52-BMoXFmwE.woff2 +0 -0
  177. package/web-dist/assets/misans-400-53-BaLMGl4z.woff2 +0 -0
  178. package/web-dist/assets/misans-400-54-CaeSn3OR.woff2 +0 -0
  179. package/web-dist/assets/misans-400-55-BBqrtLxN.woff2 +0 -0
  180. package/web-dist/assets/misans-400-56-DAZ5BnbO.woff2 +0 -0
  181. package/web-dist/assets/misans-400-57-F-n3b0gI.woff2 +0 -0
  182. package/web-dist/assets/misans-400-58-DEUuPQAq.woff2 +0 -0
  183. package/web-dist/assets/misans-400-59-DXP5rhCL.woff2 +0 -0
  184. package/web-dist/assets/misans-400-6-DWCCUO_c.woff2 +0 -0
  185. package/web-dist/assets/misans-400-60-BE6IO6zJ.woff2 +0 -0
  186. package/web-dist/assets/misans-400-61-5Qb1gaxK.woff2 +0 -0
  187. package/web-dist/assets/misans-400-62-D_tJKNAj.woff2 +0 -0
  188. package/web-dist/assets/misans-400-63-hwmne8yB.woff2 +0 -0
  189. package/web-dist/assets/misans-400-64-BGedxReM.woff2 +0 -0
  190. package/web-dist/assets/misans-400-65-BaKPsugb.woff2 +0 -0
  191. package/web-dist/assets/misans-400-66-IdaOMzCz.woff2 +0 -0
  192. package/web-dist/assets/misans-400-67-BdWyS83H.woff2 +0 -0
  193. package/web-dist/assets/misans-400-68-DrZ_1swq.woff2 +0 -0
  194. package/web-dist/assets/misans-400-69-B3jsSD92.woff2 +0 -0
  195. package/web-dist/assets/misans-400-70-D6UZI2Ua.woff2 +0 -0
  196. package/web-dist/assets/misans-400-71-HFxyMCYg.woff2 +0 -0
  197. package/web-dist/assets/misans-400-72-CP5A-yQP.woff2 +0 -0
  198. package/web-dist/assets/misans-400-73-BoD_bu6s.woff2 +0 -0
  199. package/web-dist/assets/misans-400-74-CbJlhZlZ.woff2 +0 -0
  200. package/web-dist/assets/misans-400-75-BsbWuELX.woff2 +0 -0
  201. package/web-dist/assets/misans-400-76-DkwiQhof.woff2 +0 -0
  202. package/web-dist/assets/misans-400-77-6pVHXHnF.woff2 +0 -0
  203. package/web-dist/assets/misans-400-78-CfUF6pih.woff2 +0 -0
  204. package/web-dist/assets/misans-400-79-BCS3Dx4e.woff2 +0 -0
  205. package/web-dist/assets/misans-400-80-BEYSdQ_N.woff2 +0 -0
  206. package/web-dist/assets/misans-400-81-BzL-Gncx.woff2 +0 -0
  207. package/web-dist/assets/misans-400-82-CaENmdUU.woff2 +0 -0
  208. package/web-dist/assets/misans-400-83-Bvq1eBcA.woff2 +0 -0
  209. package/web-dist/assets/misans-400-84-Ctd90hiP.woff2 +0 -0
  210. package/web-dist/assets/misans-400-85-DWuOxfL1.woff2 +0 -0
  211. package/web-dist/assets/misans-400-86-D5niZS5c.woff2 +0 -0
  212. package/web-dist/assets/misans-400-87-DkOIADQJ.woff2 +0 -0
  213. package/web-dist/assets/misans-400-88-CGY4WTcC.woff2 +0 -0
  214. package/web-dist/assets/misans-400-89-CA3eoMTt.woff2 +0 -0
  215. package/web-dist/assets/misans-400-90-CzxR9MYb.woff2 +0 -0
  216. package/web-dist/assets/misans-400-91-Dyr85txc.woff2 +0 -0
  217. package/web-dist/assets/misans-400-97-qEZrCUNe.woff2 +0 -0
  218. package/web-dist/assets/misans-400-cyrillic--hFcG0Xp.woff2 +0 -0
  219. package/web-dist/assets/misans-400-extra-ZRI9n2Hu.woff2 +0 -0
  220. package/web-dist/assets/misans-400-latin-BRDR1WaL.woff2 +0 -0
  221. package/web-dist/assets/misans-400-latin-ext-6xS-MvHW.woff2 +0 -0
  222. package/web-dist/assets/misans-400-vietnamese-D7xD7kBB.woff2 +0 -0
  223. package/web-dist/assets/misans-500-100-BHuGA96E.woff2 +0 -0
  224. package/web-dist/assets/misans-500-101-CDMYUz0c.woff2 +0 -0
  225. package/web-dist/assets/misans-500-102-CWcqYYCV.woff2 +0 -0
  226. package/web-dist/assets/misans-500-103-BE1QSEzY.woff2 +0 -0
  227. package/web-dist/assets/misans-500-104-CDp743BV.woff2 +0 -0
  228. package/web-dist/assets/misans-500-105-y7dY6Foi.woff2 +0 -0
  229. package/web-dist/assets/misans-500-106-CP-kGZ6j.woff2 +0 -0
  230. package/web-dist/assets/misans-500-107-CUc8Hvle.woff2 +0 -0
  231. package/web-dist/assets/misans-500-108-ItN_x8jX.woff2 +0 -0
  232. package/web-dist/assets/misans-500-109-CEtf3R4D.woff2 +0 -0
  233. package/web-dist/assets/misans-500-110-BSZSHYRL.woff2 +0 -0
  234. package/web-dist/assets/misans-500-111-DfdArrjn.woff2 +0 -0
  235. package/web-dist/assets/misans-500-112-DRtNyrQw.woff2 +0 -0
  236. package/web-dist/assets/misans-500-113-CDT7tKuB.woff2 +0 -0
  237. package/web-dist/assets/misans-500-114-CCh4jwL8.woff2 +0 -0
  238. package/web-dist/assets/misans-500-115-DplUgPp0.woff2 +0 -0
  239. package/web-dist/assets/misans-500-116-C7eCklyK.woff2 +0 -0
  240. package/web-dist/assets/misans-500-117-BA0KabyT.woff2 +0 -0
  241. package/web-dist/assets/misans-500-118-V_M0eu2q.woff2 +0 -0
  242. package/web-dist/assets/misans-500-119-Cgf3Hszt.woff2 +0 -0
  243. package/web-dist/assets/misans-500-21-Di6DyR6O.woff2 +0 -0
  244. package/web-dist/assets/misans-500-22-CSnXvtL-.woff2 +0 -0
  245. package/web-dist/assets/misans-500-23-A06Hm9a6.woff2 +0 -0
  246. package/web-dist/assets/misans-500-24-Df97RCik.woff2 +0 -0
  247. package/web-dist/assets/misans-500-25-bOSJ6YXH.woff2 +0 -0
  248. package/web-dist/assets/misans-500-26-C65icgBb.woff2 +0 -0
  249. package/web-dist/assets/misans-500-27-C_GRm0va.woff2 +0 -0
  250. package/web-dist/assets/misans-500-28-CzZkqO0l.woff2 +0 -0
  251. package/web-dist/assets/misans-500-29-BReyeRyX.woff2 +0 -0
  252. package/web-dist/assets/misans-500-30-DzfjlROz.woff2 +0 -0
  253. package/web-dist/assets/misans-500-31-B2nLpgdU.woff2 +0 -0
  254. package/web-dist/assets/misans-500-32-Bz3rHm8X.woff2 +0 -0
  255. package/web-dist/assets/misans-500-33-AU3lZ1vY.woff2 +0 -0
  256. package/web-dist/assets/misans-500-34-CBAWkH-I.woff2 +0 -0
  257. package/web-dist/assets/misans-500-35-VgnGd14P.woff2 +0 -0
  258. package/web-dist/assets/misans-500-36-B2kfXv6A.woff2 +0 -0
  259. package/web-dist/assets/misans-500-37-DhKo8ykb.woff2 +0 -0
  260. package/web-dist/assets/misans-500-38-D8tTxFuc.woff2 +0 -0
  261. package/web-dist/assets/misans-500-39-DTm3aqJQ.woff2 +0 -0
  262. package/web-dist/assets/misans-500-40-C_akwj8_.woff2 +0 -0
  263. package/web-dist/assets/misans-500-41-RVM1JHQW.woff2 +0 -0
  264. package/web-dist/assets/misans-500-42-MVWIN_il.woff2 +0 -0
  265. package/web-dist/assets/misans-500-43-BtLZYNyD.woff2 +0 -0
  266. package/web-dist/assets/misans-500-44-D0QHMl6G.woff2 +0 -0
  267. package/web-dist/assets/misans-500-45-DRlclX7-.woff2 +0 -0
  268. package/web-dist/assets/misans-500-46-BEWswL4N.woff2 +0 -0
  269. package/web-dist/assets/misans-500-47-COSoxWaW.woff2 +0 -0
  270. package/web-dist/assets/misans-500-48-DZAISq3n.woff2 +0 -0
  271. package/web-dist/assets/misans-500-49-KSfi7MW0.woff2 +0 -0
  272. package/web-dist/assets/misans-500-5-h1SBcwuW.woff2 +0 -0
  273. package/web-dist/assets/misans-500-50-CIVMfG11.woff2 +0 -0
  274. package/web-dist/assets/misans-500-51-BdeNBhBL.woff2 +0 -0
  275. package/web-dist/assets/misans-500-52-CEW4e7ex.woff2 +0 -0
  276. package/web-dist/assets/misans-500-53-lmPCsEnt.woff2 +0 -0
  277. package/web-dist/assets/misans-500-54-B_7AqKQW.woff2 +0 -0
  278. package/web-dist/assets/misans-500-55-B-3RtUnH.woff2 +0 -0
  279. package/web-dist/assets/misans-500-56-g6D0e0cW.woff2 +0 -0
  280. package/web-dist/assets/misans-500-57-D5hp5Yqr.woff2 +0 -0
  281. package/web-dist/assets/misans-500-58-ilnDgEKt.woff2 +0 -0
  282. package/web-dist/assets/misans-500-59-DJYXz8tr.woff2 +0 -0
  283. package/web-dist/assets/misans-500-6-uCdpdDQy.woff2 +0 -0
  284. package/web-dist/assets/misans-500-60-B3Iaqx0p.woff2 +0 -0
  285. package/web-dist/assets/misans-500-61-CMxqg5hc.woff2 +0 -0
  286. package/web-dist/assets/misans-500-62-7FY8O2NQ.woff2 +0 -0
  287. package/web-dist/assets/misans-500-63-JP6vEuJf.woff2 +0 -0
  288. package/web-dist/assets/misans-500-64-Cdub62jh.woff2 +0 -0
  289. package/web-dist/assets/misans-500-65-DVHk_mlC.woff2 +0 -0
  290. package/web-dist/assets/misans-500-66-BX39wRtF.woff2 +0 -0
  291. package/web-dist/assets/misans-500-67-BcsepI3X.woff2 +0 -0
  292. package/web-dist/assets/misans-500-68-etUIUjrh.woff2 +0 -0
  293. package/web-dist/assets/misans-500-69-DfhVmnuA.woff2 +0 -0
  294. package/web-dist/assets/misans-500-70-D6dgy6-B.woff2 +0 -0
  295. package/web-dist/assets/misans-500-71-PkeKjcVn.woff2 +0 -0
  296. package/web-dist/assets/misans-500-72-D66Fnb30.woff2 +0 -0
  297. package/web-dist/assets/misans-500-73-jr9z9870.woff2 +0 -0
  298. package/web-dist/assets/misans-500-74-DsCkPKwe.woff2 +0 -0
  299. package/web-dist/assets/misans-500-75-Dkooq6KM.woff2 +0 -0
  300. package/web-dist/assets/misans-500-76-B0sfieHl.woff2 +0 -0
  301. package/web-dist/assets/misans-500-77-8tflq1ji.woff2 +0 -0
  302. package/web-dist/assets/misans-500-78-COKdMbdp.woff2 +0 -0
  303. package/web-dist/assets/misans-500-79-Bm0rBjSF.woff2 +0 -0
  304. package/web-dist/assets/misans-500-80-B9OoAnyy.woff2 +0 -0
  305. package/web-dist/assets/misans-500-81-DsQ9tpfX.woff2 +0 -0
  306. package/web-dist/assets/misans-500-82-VKsL2Tk2.woff2 +0 -0
  307. package/web-dist/assets/misans-500-83-Ce6mfHYD.woff2 +0 -0
  308. package/web-dist/assets/misans-500-84-D7kPSZLm.woff2 +0 -0
  309. package/web-dist/assets/misans-500-85-W5L0cEfd.woff2 +0 -0
  310. package/web-dist/assets/misans-500-86-DukGOEET.woff2 +0 -0
  311. package/web-dist/assets/misans-500-87-BJ_Lok6j.woff2 +0 -0
  312. package/web-dist/assets/misans-500-88-DbJ11zcA.woff2 +0 -0
  313. package/web-dist/assets/misans-500-89-LNKcEq6M.woff2 +0 -0
  314. package/web-dist/assets/misans-500-90-DNDDgSCd.woff2 +0 -0
  315. package/web-dist/assets/misans-500-91-rT8Q0evu.woff2 +0 -0
  316. package/web-dist/assets/misans-500-97-c8zW_WRq.woff2 +0 -0
  317. package/web-dist/assets/misans-500-cyrillic-CLHOGwgx.woff2 +0 -0
  318. package/web-dist/assets/misans-500-extra-CTAMEQ59.woff2 +0 -0
  319. package/web-dist/assets/misans-500-latin-Be606ott.woff2 +0 -0
  320. package/web-dist/assets/misans-500-latin-ext-BzDyL4sU.woff2 +0 -0
  321. package/web-dist/assets/misans-500-vietnamese-DBOLOqZH.woff2 +0 -0
  322. package/web-dist/assets/mona-sans-latin-ext-wght-normal-BqSk0Z3x.woff2 +0 -0
  323. package/web-dist/assets/mona-sans-latin-wght-normal-Pz49MTQZ.woff2 +0 -0
  324. package/web-dist/assets/noto-sans-sc-100-wght-normal-DrqXJETY.woff2 +0 -0
  325. package/web-dist/assets/noto-sans-sc-101-wght-normal-c94LAl5p.woff2 +0 -0
  326. package/web-dist/assets/noto-sans-sc-102-wght-normal-C1RtbCZr.woff2 +0 -0
  327. package/web-dist/assets/noto-sans-sc-103-wght-normal-DGHo20nu.woff2 +0 -0
  328. package/web-dist/assets/noto-sans-sc-104-wght-normal-CJT2ioDJ.woff2 +0 -0
  329. package/web-dist/assets/noto-sans-sc-105-wght-normal-BFoiJwz2.woff2 +0 -0
  330. package/web-dist/assets/noto-sans-sc-106-wght-normal-D6uUHw3w.woff2 +0 -0
  331. package/web-dist/assets/noto-sans-sc-107-wght-normal-kFQzJDLH.woff2 +0 -0
  332. package/web-dist/assets/noto-sans-sc-108-wght-normal-7aXvqIa2.woff2 +0 -0
  333. package/web-dist/assets/noto-sans-sc-109-wght-normal-CnjNbNmw.woff2 +0 -0
  334. package/web-dist/assets/noto-sans-sc-110-wght-normal-D2JBr045.woff2 +0 -0
  335. package/web-dist/assets/noto-sans-sc-111-wght-normal-ClFr5QXM.woff2 +0 -0
  336. package/web-dist/assets/noto-sans-sc-112-wght-normal-CiZxJMyY.woff2 +0 -0
  337. package/web-dist/assets/noto-sans-sc-113-wght-normal-OgQXOX6x.woff2 +0 -0
  338. package/web-dist/assets/noto-sans-sc-114-wght-normal-CRH9SDZu.woff2 +0 -0
  339. package/web-dist/assets/noto-sans-sc-115-wght-normal-BN__2iBG.woff2 +0 -0
  340. package/web-dist/assets/noto-sans-sc-116-wght-normal-vdFVzbO2.woff2 +0 -0
  341. package/web-dist/assets/noto-sans-sc-117-wght-normal-Dsz3vkzU.woff2 +0 -0
  342. package/web-dist/assets/noto-sans-sc-118-wght-normal-BPEb0gM9.woff2 +0 -0
  343. package/web-dist/assets/noto-sans-sc-119-wght-normal-BfzSEbFz.woff2 +0 -0
  344. package/web-dist/assets/noto-sans-sc-21-wght-normal-DIu_1ZBI.woff2 +0 -0
  345. package/web-dist/assets/noto-sans-sc-22-wght-normal-VXjdYcT-.woff2 +0 -0
  346. package/web-dist/assets/noto-sans-sc-23-wght-normal-4HrHKhpL.woff2 +0 -0
  347. package/web-dist/assets/noto-sans-sc-24-wght-normal-B58DWgHS.woff2 +0 -0
  348. package/web-dist/assets/noto-sans-sc-25-wght-normal-C3Jm6l7O.woff2 +0 -0
  349. package/web-dist/assets/noto-sans-sc-26-wght-normal-YNOygvr_.woff2 +0 -0
  350. package/web-dist/assets/noto-sans-sc-27-wght-normal-BP2pEGQH.woff2 +0 -0
  351. package/web-dist/assets/noto-sans-sc-28-wght-normal-BWPKDbtH.woff2 +0 -0
  352. package/web-dist/assets/noto-sans-sc-29-wght-normal-45aVmHn_.woff2 +0 -0
  353. package/web-dist/assets/noto-sans-sc-30-wght-normal-B8_s30jZ.woff2 +0 -0
  354. package/web-dist/assets/noto-sans-sc-31-wght-normal-DW0t445S.woff2 +0 -0
  355. package/web-dist/assets/noto-sans-sc-32-wght-normal-ShbWPcei.woff2 +0 -0
  356. package/web-dist/assets/noto-sans-sc-33-wght-normal-BOTZYTjM.woff2 +0 -0
  357. package/web-dist/assets/noto-sans-sc-34-wght-normal-BF1rqDSt.woff2 +0 -0
  358. package/web-dist/assets/noto-sans-sc-35-wght-normal-Bosspiyr.woff2 +0 -0
  359. package/web-dist/assets/noto-sans-sc-36-wght-normal-CtU9CyKw.woff2 +0 -0
  360. package/web-dist/assets/noto-sans-sc-37-wght-normal-ghDiyiuv.woff2 +0 -0
  361. package/web-dist/assets/noto-sans-sc-38-wght-normal-4Nz8k_dt.woff2 +0 -0
  362. package/web-dist/assets/noto-sans-sc-39-wght-normal-Ddtm1XVC.woff2 +0 -0
  363. package/web-dist/assets/noto-sans-sc-4-wght-normal-BRbPMj-C.woff2 +0 -0
  364. package/web-dist/assets/noto-sans-sc-40-wght-normal-BVn4-kgX.woff2 +0 -0
  365. package/web-dist/assets/noto-sans-sc-41-wght-normal-CZqajZnl.woff2 +0 -0
  366. package/web-dist/assets/noto-sans-sc-42-wght-normal-CQ4lY28u.woff2 +0 -0
  367. package/web-dist/assets/noto-sans-sc-43-wght-normal-psppu0d8.woff2 +0 -0
  368. package/web-dist/assets/noto-sans-sc-44-wght-normal-cbKJlTUe.woff2 +0 -0
  369. package/web-dist/assets/noto-sans-sc-45-wght-normal-MKIEVRIC.woff2 +0 -0
  370. package/web-dist/assets/noto-sans-sc-46-wght-normal-CErGc6Mt.woff2 +0 -0
  371. package/web-dist/assets/noto-sans-sc-47-wght-normal-C5Kgg1_m.woff2 +0 -0
  372. package/web-dist/assets/noto-sans-sc-48-wght-normal-DFMjDDY6.woff2 +0 -0
  373. package/web-dist/assets/noto-sans-sc-49-wght-normal-B7l_3BZr.woff2 +0 -0
  374. package/web-dist/assets/noto-sans-sc-5-wght-normal-DI846rD9.woff2 +0 -0
  375. package/web-dist/assets/noto-sans-sc-50-wght-normal-C5KfAmFp.woff2 +0 -0
  376. package/web-dist/assets/noto-sans-sc-51-wght-normal-C2bq6hC8.woff2 +0 -0
  377. package/web-dist/assets/noto-sans-sc-52-wght-normal-P18e_Y0j.woff2 +0 -0
  378. package/web-dist/assets/noto-sans-sc-53-wght-normal-Bgkuo2tF.woff2 +0 -0
  379. package/web-dist/assets/noto-sans-sc-54-wght-normal-DL5lmlUd.woff2 +0 -0
  380. package/web-dist/assets/noto-sans-sc-55-wght-normal-BV_Oy6IF.woff2 +0 -0
  381. package/web-dist/assets/noto-sans-sc-56-wght-normal-B4L7Hj8f.woff2 +0 -0
  382. package/web-dist/assets/noto-sans-sc-57-wght-normal-DU8YBsDj.woff2 +0 -0
  383. package/web-dist/assets/noto-sans-sc-58-wght-normal-CE0BKS9u.woff2 +0 -0
  384. package/web-dist/assets/noto-sans-sc-59-wght-normal-Azv2Ku9_.woff2 +0 -0
  385. package/web-dist/assets/noto-sans-sc-6-wght-normal-ozUCEY9O.woff2 +0 -0
  386. package/web-dist/assets/noto-sans-sc-60-wght-normal-C1lngC61.woff2 +0 -0
  387. package/web-dist/assets/noto-sans-sc-61-wght-normal-BxWkGF5M.woff2 +0 -0
  388. package/web-dist/assets/noto-sans-sc-62-wght-normal-CXXMAq-5.woff2 +0 -0
  389. package/web-dist/assets/noto-sans-sc-63-wght-normal-4La5uKvS.woff2 +0 -0
  390. package/web-dist/assets/noto-sans-sc-64-wght-normal-CbIuD2Eg.woff2 +0 -0
  391. package/web-dist/assets/noto-sans-sc-65-wght-normal-BGqvKda2.woff2 +0 -0
  392. package/web-dist/assets/noto-sans-sc-66-wght-normal-BvNozWOY.woff2 +0 -0
  393. package/web-dist/assets/noto-sans-sc-67-wght-normal-CC-WRBV-.woff2 +0 -0
  394. package/web-dist/assets/noto-sans-sc-68-wght-normal-c0FjfUaj.woff2 +0 -0
  395. package/web-dist/assets/noto-sans-sc-69-wght-normal-D596Jjl2.woff2 +0 -0
  396. package/web-dist/assets/noto-sans-sc-70-wght-normal-ColEcmtJ.woff2 +0 -0
  397. package/web-dist/assets/noto-sans-sc-71-wght-normal-ChZfXpdg.woff2 +0 -0
  398. package/web-dist/assets/noto-sans-sc-72-wght-normal-D1VyfEG3.woff2 +0 -0
  399. package/web-dist/assets/noto-sans-sc-73-wght-normal-DwSJRYPJ.woff2 +0 -0
  400. package/web-dist/assets/noto-sans-sc-74-wght-normal-Cte7o1QG.woff2 +0 -0
  401. package/web-dist/assets/noto-sans-sc-75-wght-normal-B0qL41rF.woff2 +0 -0
  402. package/web-dist/assets/noto-sans-sc-76-wght-normal-BPLMO_2x.woff2 +0 -0
  403. package/web-dist/assets/noto-sans-sc-77-wght-normal-Dkbi3pye.woff2 +0 -0
  404. package/web-dist/assets/noto-sans-sc-78-wght-normal-BFUXeSw6.woff2 +0 -0
  405. package/web-dist/assets/noto-sans-sc-79-wght-normal-DoJG4UTB.woff2 +0 -0
  406. package/web-dist/assets/noto-sans-sc-80-wght-normal-BEFypWOO.woff2 +0 -0
  407. package/web-dist/assets/noto-sans-sc-81-wght-normal-ed_TZlfo.woff2 +0 -0
  408. package/web-dist/assets/noto-sans-sc-82-wght-normal-DcxfR5OJ.woff2 +0 -0
  409. package/web-dist/assets/noto-sans-sc-83-wght-normal-C9UjAcMM.woff2 +0 -0
  410. package/web-dist/assets/noto-sans-sc-84-wght-normal-C7NxT478.woff2 +0 -0
  411. package/web-dist/assets/noto-sans-sc-85-wght-normal-DBcDBu1n.woff2 +0 -0
  412. package/web-dist/assets/noto-sans-sc-86-wght-normal-CLJIoPpl.woff2 +0 -0
  413. package/web-dist/assets/noto-sans-sc-87-wght-normal-DYfJ-vGE.woff2 +0 -0
  414. package/web-dist/assets/noto-sans-sc-88-wght-normal-ClNDOCGW.woff2 +0 -0
  415. package/web-dist/assets/noto-sans-sc-89-wght-normal-IQU_KQA8.woff2 +0 -0
  416. package/web-dist/assets/noto-sans-sc-90-wght-normal-BWcrRNoG.woff2 +0 -0
  417. package/web-dist/assets/noto-sans-sc-91-wght-normal-CGekvDdH.woff2 +0 -0
  418. package/web-dist/assets/noto-sans-sc-97-wght-normal-BsBHkXi3.woff2 +0 -0
  419. package/web-dist/assets/noto-sans-sc-98-wght-normal-BZqSA1r_.woff2 +0 -0
  420. package/web-dist/assets/noto-sans-sc-99-wght-normal-Q_IptJM5.woff2 +0 -0
  421. package/web-dist/assets/noto-sans-sc-cyrillic-wght-normal-CRA5_Pf1.woff2 +0 -0
  422. package/web-dist/assets/noto-sans-sc-latin-ext-wght-normal-CqPqLCfe.woff2 +0 -0
  423. package/web-dist/assets/noto-sans-sc-latin-wght-normal-DYAKOyvl.woff2 +0 -0
  424. package/web-dist/assets/noto-sans-sc-vietnamese-wght-normal-y5SU35Rl.woff2 +0 -0
  425. package/web-dist/assets/php-CNeTOyEV.js +1 -0
  426. package/web-dist/assets/php-Csjmro_R.js +1 -0
  427. package/web-dist/assets/python-B6Ihcp-E.js +1 -0
  428. package/web-dist/assets/python-B6aJPvgy.js +1 -0
  429. package/web-dist/assets/ruby-B13GW9FM.js +1 -0
  430. package/web-dist/assets/ruby-CYiNE6ps.js +1 -0
  431. package/web-dist/assets/rust-B1yitclQ.js +1 -0
  432. package/web-dist/assets/rust-CxuBE0J8.js +1 -0
  433. package/web-dist/assets/shellscript-DgJyDXW6.js +1 -0
  434. package/web-dist/assets/shellscript-Yzrsuije.js +1 -0
  435. package/web-dist/assets/sql-CRqJ_cUM.js +1 -0
  436. package/web-dist/assets/sql-Dq2MiB0T.js +1 -0
  437. package/web-dist/assets/toml-Ms459DBw.js +1 -0
  438. package/web-dist/assets/toml-vGWfd6FD.js +1 -0
  439. package/web-dist/assets/tsx-COt5Ahok.js +1 -0
  440. package/web-dist/assets/tsx-D6116Ycy.js +1 -0
  441. package/web-dist/assets/typescript-BPQ3VLAy.js +1 -0
  442. package/web-dist/assets/typescript-BqvgUolh.js +1 -0
  443. package/web-dist/assets/xml-BkAJuBCc.js +1 -0
  444. package/web-dist/assets/xml-sdJ4AIDG.js +1 -0
  445. package/web-dist/assets/xterm-BT4qZAkH.js +9 -0
  446. package/web-dist/assets/yaml-Bopit2L3.js +1 -0
  447. package/web-dist/assets/yaml-Buea-lGh.js +1 -0
  448. package/web-dist/fonts-licenses/ibm-plex-sans-condensed.txt +93 -0
  449. package/web-dist/fonts-licenses/ibm-plex-sans.txt +93 -0
  450. package/web-dist/fonts-licenses/jetbrains-mono.txt +93 -0
  451. package/web-dist/fonts-licenses/misans.txt +119 -0
  452. package/web-dist/fonts-licenses/mona-sans.txt +93 -0
  453. package/web-dist/fonts-licenses/noto-sans-sc.txt +93 -0
  454. package/web-dist/index.html +29 -0
  455. package/web-dist/workflow-ui.css +261 -0
@@ -0,0 +1,3002 @@
1
+ import { SpawnConfiner, ThinkingLevelName, OmniMessage, ToolCallPayload, ProjectConfig, ModelRef, PluginTables, ListEndpointModelsOptions, ModelRequestContext, ProxyEnvPolicy, ControlEnvContext, AgentAssembly, ApproveFn, StopReason, CompactAvailability, SessionTitleResult, BackgroundCommandInfo, ToolDetachResult, BackgroundSubagentInfo, SubagentMessageOptions, SubagentMessageOutcome } from '@adelie/core';
2
+ import { SandboxProviderSource, SandboxSettings, SandboxDimension, SandboxLimit } from '@adelie/core/plugin';
3
+ import { Slot, Opaque } from '@adelie/core/kernel';
4
+ import { ApprovalMode, ModelRefDto, ChatDefaultsDto, CommandPolicyDto, ModelVisionDetectRequest, ModelVisionDetectResponse, ModelTestRequest, ModelTestResponse, ModelProtocolDetectRequest, ModelProtocolDetectResponse, EndpointModelListRequest, EndpointModelListResponse, ModelsResponse, ModelsUpdateRequest, ServerEvent, MessagingChannel, UnavailableSandboxBackend, SessionSandboxPreset, SessionSandbox, SessionInfo, SessionCategory, SessionCategoryCounts, ModelScopeAuthFlowStatusResponse, SessionStatus, SessionBackgroundTasks, PendingSteeringInfo, PendingFollowUpInfo, AgentConfigDto, AgentConfigUpdateRequest, AgentKernelUpdateResponse, VaultResponse, VaultUpdateRequest, PluginUpdateRef, MessagingRuntimeStatus, PluginConfiguration, ContributionsResponse, RendererRef } from '../api/types.js';
5
+ export { PluginConfigField } from '../api/types.js';
6
+ import { ThinkingLevelName as ThinkingLevelName$1 } from '@adelie/core/interfaces';
7
+ import { SessionSource } from '@adelie/core/omnimessage';
8
+ import { Hono } from 'hono';
9
+ import '@adelie/hmr';
10
+
11
+ interface UserRow {
12
+ userId: string;
13
+ passwordHash: string;
14
+ isAdmin: boolean;
15
+ /** Still using the initial password (seeded / set by an admin); cleared to 0 once the user changes it. */
16
+ passwordIsInitial: boolean;
17
+ /** Nickname shown in place of the id; null = never set. */
18
+ displayName: string | null;
19
+ /** Avatar as a data URL; null = never set. */
20
+ avatar: string | null;
21
+ /** Most recent successful sign-in (ISO); null = never signed in — an account that exists but has not been used yet. */
22
+ lastLoginAt: string | null;
23
+ createdAt: string;
24
+ }
25
+
26
+ /**
27
+ * "desktop" and "setup" may set a password without the old one (theirs is random and was never
28
+ * shown); desktop-only routes open to "desktop" alone. "token" is the local API token's Bearer
29
+ * header — per request, never a stored row. Anything else reads as "password".
30
+ */
31
+ type SessionVia = "password" | "desktop" | "setup" | "token";
32
+
33
+ declare class SandboxService {
34
+ private mounted;
35
+ /**
36
+ * name → why it FAILED: it could not load, or failed its check on a host it is meant for —
37
+ * never silently absent. Surfaced in the fail-closed message and on the settings card.
38
+ */
39
+ private loadErrors;
40
+ /**
41
+ * The backends that declined: this host is not theirs (a Linux backend on Windows). Nothing
42
+ * is wrong with a deployment that installs one backend per platform, so a decline is not a
43
+ * failure — it is listed only when NO backend serves, where it is the explanation.
44
+ */
45
+ private declinedNames;
46
+ /** Every registration in routing order, with how its latest load settled. */
47
+ private readonly registrations;
48
+ /**
49
+ * Ships with confinement OFF (`danger-full-access`): the default flips to
50
+ * workspace-write together with the deployment-facing config surface, so a
51
+ * deployment gains the switch before the enforcement — otherwise every host without
52
+ * a usable backend would fail every command the moment this code arrives.
53
+ */
54
+ private settings;
55
+ private readonly ready;
56
+ /**
57
+ * @param registrations - the backends plugins registered (see the sandbox registry on
58
+ * PenguinInterface). Loading is async, but create() and the confiner are sync: a
59
+ * confining policy resolved before loading settles fails closed rather than
60
+ * waiting. The default settings never consult a backend, so that window only
61
+ * exists for deployments flipping the mode in the first milliseconds after boot.
62
+ */
63
+ constructor(registrations?: Iterable<[string, SandboxProviderSource]>);
64
+ /** Rebuilds the mounted / failed / declined views from the registrations, in routing order. */
65
+ private record;
66
+ /**
67
+ * Loads again every backend whose load failed and that registered a loader (a bare promise
68
+ * cannot be re-run). Called after the sandbox's settings are saved: a backend whose check
69
+ * failed on its own setting mounts once that setting is fixed. Resolves when those settle.
70
+ */
71
+ retryFailed(): Promise<void>;
72
+ /** Resolves when every registered backend has loaded (or failed to). */
73
+ whenReady(): Promise<void>;
74
+ /** Replaces the active settings (the config surface's write path). */
75
+ configure(settings: SandboxSettings): void;
76
+ /** The active settings as a plain copy (the config surface's read side). */
77
+ currentSettings(): SandboxSettings;
78
+ /**
79
+ * The active settings as parked state, or undefined for the pristine default. The
80
+ * omission is load-bearing compatibility: a default deployment keeps parking
81
+ * `{ motd }`, which any platform bundle's context schema accepts. Once confinement
82
+ * is configured the field enters the document, and from then on pushing a
83
+ * sandbox-ignorant bundle is BLOCKED by schema validation — the right failure:
84
+ * refusing the swap beats a swap that silently un-confines the deployment.
85
+ */
86
+ parkedSettings(): SandboxSettings | undefined;
87
+ /** The backends that failed to load, each with why (diagnostics / the config surface). */
88
+ failures(): Array<{
89
+ name: string;
90
+ reason: string;
91
+ }>;
92
+ /** The backends that declined because this host is not theirs (the config surface). */
93
+ declined(): string[];
94
+ /**
95
+ * The mounted backends and what each implements, in routing preference: the one a policy
96
+ * goes to first (diagnostics / the config surface).
97
+ */
98
+ backends(): Array<{
99
+ name: string;
100
+ dimensions: readonly SandboxDimension[];
101
+ mechanism?: string;
102
+ limits?: readonly SandboxLimit[];
103
+ }>;
104
+ /**
105
+ * The closure handed to the runtime: rewrites each spawn's argv under the active
106
+ * settings. Pure and self-contained, so the previous platform's confiner keeps
107
+ * serving during a hot-swap freeze window.
108
+ */
109
+ confiner(): SpawnConfiner;
110
+ /**
111
+ * The same closure under a policy the caller keeps — a Session's own snapshot. The
112
+ * backends are still this service's, so a backend change reaches every Session.
113
+ */
114
+ confinerFor(policyOf: () => SandboxSettings): SpawnConfiner;
115
+ /**
116
+ * The preferred mounted backend implementing every required dimension (byPreference), or a
117
+ * fail-closed throw.
118
+ */
119
+ private pick;
120
+ private failedSuffix;
121
+ }
122
+ declare const Sandbox_base: (abstract new () => Pick<SandboxService, "configure" | "currentSettings" | "parkedSettings" | "backends" | "failures" | "declined" | "retryFailed" | "confiner" | "confinerFor" | "whenReady">) & (<C>(target: C, context: ClassDecoratorContext) => C);
123
+ /** Confinement: settings, the active confiner, and which backends are mounted. */
124
+ declare abstract class Sandbox extends Sandbox_base {
125
+ }
126
+ interface SandboxSlots {
127
+ /**
128
+ * A backend: its static half here (name, the dimensions it implements), its code half
129
+ * bound by the contributor — a provider, or a promise of one for backends that probe.
130
+ */
131
+ providers: Slot<{
132
+ name: string;
133
+ dimensions: SandboxDimension[];
134
+ }, SandboxProviderSource>;
135
+ }
136
+
137
+ /**
138
+ * Screen snapshot -> ANSI restore stream, plus plain-text capture.
139
+ *
140
+ * This is what makes a browser refresh (or a reconnect from a second device) show the
141
+ * terminal exactly as it was: the daemon keeps an authoritative headless xterm per pty, and
142
+ * on attach it *re-renders* that grid as an escape-sequence stream the client's real xterm
143
+ * replays into an identical screen. That beats the two obvious alternatives:
144
+ *
145
+ * - Replaying the raw pty byte log: unbounded, and full-screen programs (vim, top) replay
146
+ * as a flicker of stale frames.
147
+ * - Sending a JSON cell grid: a 200k-object payload per attach, parsed on the UI thread.
148
+ *
149
+ * Cursor alignment is the subtle part. The restore writes `scrollback + rows` lines, so the
150
+ * client's viewport ends up holding exactly the server's viewport, and only then is the
151
+ * cursor placed with a viewport-relative CUP. Trailing blank rows are therefore written on
152
+ * purpose — dropping them would shift every subsequent row and land the cursor in the wrong
153
+ * place.
154
+ */
155
+
156
+ interface CaptureLinesOptions {
157
+ /** Line index into scrollback+viewport; negative counts from the end (-1 = last line). */
158
+ start?: number;
159
+ end?: number;
160
+ }
161
+ interface CaptureLinesResult {
162
+ lines: string[];
163
+ totalLines: number;
164
+ }
165
+
166
+ interface TerminalExitInfo {
167
+ exitCode: number;
168
+ signal: number | null;
169
+ lastOutputLines: string[];
170
+ }
171
+ interface CreateTerminalSessionOptions {
172
+ id?: string;
173
+ /** Absolute working directory (already resolved and validated by the manager). */
174
+ cwd: string;
175
+ /** Owning user id — a shell is the most privileged thing this server hands out. */
176
+ ownerUserId: string;
177
+ /**
178
+ * Stable per-user display number (tmux-style), assigned by the manager at creation. It
179
+ * travels with the terminal for its whole life — UI numbering must never depend on list
180
+ * position, which changes under user-reordered tabs.
181
+ */
182
+ seq?: number;
183
+ name?: string;
184
+ shell?: string;
185
+ cols?: number;
186
+ rows?: number;
187
+ env?: Record<string, string>;
188
+ /**
189
+ * Where a pushed bundle's node-pty assets live (the hmr capability's assetsDir);
190
+ * absent falls back to the packaged require (see pty-module.ts).
191
+ */
192
+ assets?: () => string | null;
193
+ }
194
+ interface TerminalSessionInfo {
195
+ id: string;
196
+ seq: number;
197
+ name: string;
198
+ cwd: string;
199
+ ownerUserId: string;
200
+ title: string | null;
201
+ cols: number;
202
+ rows: number;
203
+ pid: number;
204
+ createdAt: string;
205
+ alive: boolean;
206
+ exit: TerminalExitInfo | null;
207
+ }
208
+ type TerminalOutputListener = (data: string) => void;
209
+ type TerminalExitListener = (info: TerminalExitInfo) => void;
210
+ type TerminalTitleListener = (title: string | null) => void;
211
+ declare class TerminalSession {
212
+ readonly id: string;
213
+ readonly seq: number;
214
+ readonly cwd: string;
215
+ readonly ownerUserId: string;
216
+ readonly createdAt: Date;
217
+ private name;
218
+ private readonly terminal;
219
+ private readonly ptyProcess;
220
+ private readonly inputMode;
221
+ private readonly outputListeners;
222
+ private readonly exitListeners;
223
+ private readonly titleListeners;
224
+ private title;
225
+ private pendingTitle;
226
+ private titleTimer;
227
+ private sizeState;
228
+ private exitInfo;
229
+ private disposed;
230
+ constructor(options: CreateTerminalSessionOptions);
231
+ /** DA1 / DSR / OSC colour queries — see OSC_COLOR_QUERY_RESPONSES. */
232
+ private registerCapabilityResponders;
233
+ get alive(): boolean;
234
+ get exit(): TerminalExitInfo | null;
235
+ info(): TerminalSessionInfo;
236
+ rename(name: string): void;
237
+ write(data: string): void;
238
+ /**
239
+ * Applies a size request under the claim/update ownership rule, resizing both the pty and
240
+ * the daemon's emulator so snapshots keep matching what the shell laid out.
241
+ */
242
+ resize(request: {
243
+ connectionId: string;
244
+ cols: number;
245
+ rows: number;
246
+ intent: "claim" | "update";
247
+ }): boolean;
248
+ releaseSize(connectionId: string): void;
249
+ get size(): {
250
+ cols: number;
251
+ rows: number;
252
+ };
253
+ /** The attach payload: the whole screen as an ANSI stream (see snapshot.ts). */
254
+ restoreStream(options?: {
255
+ scrollbackLines?: number;
256
+ }): string;
257
+ capture(options?: CaptureLinesOptions): CaptureLinesResult;
258
+ onOutput(listener: TerminalOutputListener): () => void;
259
+ onExit(listener: TerminalExitListener): () => void;
260
+ onTitleChange(listener: TerminalTitleListener): () => void;
261
+ kill(signal?: string): void;
262
+ dispose(): void;
263
+ }
264
+
265
+ interface CreateTerminalRequest {
266
+ cwd: string;
267
+ ownerUserId: string;
268
+ name?: string;
269
+ cols?: number;
270
+ rows?: number;
271
+ shell?: string;
272
+ }
273
+ /** A live pty: a host-like object (it owns a process), compared by name. */
274
+ type Terminal = Opaque<"TerminalSession", TerminalSession>;
275
+ /** Live ptys: the spawn primitive behind /api/terminals and the terminal WebSocket. */
276
+ declare abstract class Terminals {
277
+ abstract create(request: CreateTerminalRequest): Promise<Terminal>;
278
+ abstract adopt(ids: readonly string[]): void;
279
+ abstract quiesce(): void;
280
+ abstract handleIds(): string[];
281
+ abstract require(id: string, userId: string): Terminal;
282
+ abstract get(id: string): Terminal | undefined;
283
+ abstract list(userId: string): Terminal[];
284
+ abstract listInfo(userId: string): TerminalSessionInfo[];
285
+ abstract kill(id: string, userId: string): void;
286
+ abstract disposeAll(): void;
287
+ }
288
+
289
+ /**
290
+ * The resolved, in-force limits for one request, in bytes. Read fresh per request from the
291
+ * settings repo rather than snapshotted at boot, so an admin's change applies to the very next
292
+ * upload without a restart.
293
+ */
294
+ interface AttachmentLimits {
295
+ /** Per-file cap. */
296
+ maxBytes: number;
297
+ /** Per-request total of decoded attachment bytes. */
298
+ totalBytes: number;
299
+ /** Per-request file count. */
300
+ maxCount: number;
301
+ }
302
+
303
+ /**
304
+ * A queued message's file attachment as held for recall: the scratchpad path it was written
305
+ * to, plus what is needed to rebuild the composer-shaped part — the submitted name and media
306
+ * type (the bytes stay on disk; queued entries never hold file payloads in memory).
307
+ */
308
+ interface RecallableFile {
309
+ fileName: string;
310
+ path: string;
311
+ mime: string;
312
+ }
313
+
314
+ /** SessionIndex: the mechanism SessionsRepo implements. */
315
+ declare abstract class SessionIndex {
316
+ abstract insert(row: SessionRow): void;
317
+ abstract insertOrIgnore(row: SessionRow): void;
318
+ abstract insertFork(sourceSessionId: string, row: SessionRow): SessionRow;
319
+ abstract markOrgClient(sessionIds: readonly string[]): void;
320
+ abstract markHasTrace(sessionId: string): void;
321
+ abstract markDriven(sessionId: string, at: string): void;
322
+ abstract touchLastActive(sessionId: string, at: string): void;
323
+ abstract findById(sessionId: string): SessionRow | null;
324
+ abstract listByAgent(projectId: string, agentId: string): SessionRow[];
325
+ abstract listByProject(projectId: string): SessionRow[];
326
+ abstract latestByProjects(projectIds: readonly string[]): SessionRow | null;
327
+ abstract updateApprovalMode(sessionId: string, mode: ApprovalMode): void;
328
+ abstract updateSandbox(sessionId: string, sandbox: SandboxSettings): void;
329
+ abstract updateThinkingLevel(sessionId: string, level: ThinkingLevelName): void;
330
+ abstract updateTitle(sessionId: string, title: string): void;
331
+ abstract updateTitleIfNull(sessionId: string, title: string): void;
332
+ abstract updateModel(sessionId: string, provider: string, modelId: string): void;
333
+ abstract setArchived(sessionId: string, archivedAt: string | null): void;
334
+ abstract replaceId(oldSessionId: string, newSessionId: string): void;
335
+ abstract deleteByAgent(projectId: string, agentId: string): void;
336
+ abstract deleteByProject(projectId: string): void;
337
+ abstract deleteById(sessionId: string): void;
338
+ }
339
+ /** SessionOrigins: the mechanism SessionSources implements. */
340
+ declare abstract class SessionOrigins {
341
+ /** `null`: the Session's Trace head records no source (written before it was required). */
342
+ abstract set(sessionId: string, source: SessionSource | null): void;
343
+ abstract get(sessionId: string): SessionSource | null | undefined;
344
+ abstract delete(sessionId: string): void;
345
+ }
346
+
347
+ /**
348
+ * sessions table repo:
349
+ * Session index, approval mode, and auto-generated title; Session-level routes use this to look up project ownership.
350
+ */
351
+
352
+ interface SessionRow {
353
+ sessionId: string;
354
+ projectId: string;
355
+ agentId: string;
356
+ /**
357
+ * Provider group of the session's **current** model (pairs with `modelId` to form the model
358
+ * reference). Moves with each in-session switch (`updateModel`); the Trace's latest
359
+ * `session_meta` is the durable truth, and the runtime reconciles a row it disagrees with.
360
+ */
361
+ provider: string;
362
+ /** Upstream model_id of the session's current model (sent as-is to AgentHub; never concatenated). */
363
+ modelId: string;
364
+ workspace: string;
365
+ approvalMode: ApprovalMode;
366
+ /**
367
+ * Thinking level pinned for THIS session (PATCH /api/sessions/:id); NULL = not pinned,
368
+ * so runs that carry no level of their own follow the Agent config as before. Sessions
369
+ * are never inserted with one — pinning is an explicit act inside a live conversation.
370
+ */
371
+ thinkingLevel?: ThinkingLevelName$1 | null;
372
+ /** Auto-generated session title; NULL = not yet generated (frontend shows "New Conversation"). */
373
+ title: string | null;
374
+ /** Archive timestamp, ISO; NULL = not archived (omitting on insert defaults to NULL). */
375
+ archivedAt?: string | null;
376
+ /**
377
+ * Creating client: "web" (created via the Web App), "cli" (created through the API by
378
+ * the CLI, or adopted from a Trace a legacy CLI-direct run left behind), "org" (opened by
379
+ * the organization runtime — a desk or a ticket session — or spawned as a sub-session of
380
+ * one, which inherits the stamp at registration), "api" (created by an Agent API run — the
381
+ * only rows the API may continue); NULL = legacy row from before the column existed,
382
+ * treated as web. Free text in SQLite, so a new value needs no migration.
383
+ * Provenance that outlives its organization: development mode's list is the one reader
384
+ * that filters on it (through the list's `excludeOrg`), and it hides "org" rows whether or
385
+ * not the organization still exists. The schedule/subagent SOURCE is deliberately NOT a row field — core
386
+ * session_meta in the Trace stays the single source of truth for it
387
+ * (runtime/session-sources.ts); `client` is a separate, DB-only axis that meta never
388
+ * records.
389
+ */
390
+ client?: "web" | "cli" | "org" | "api" | null;
391
+ /** Cache: a Trace record exists (set at task start / adoption / subagent registration; backfilled by list hydration). */
392
+ hasTrace?: boolean;
393
+ /**
394
+ * Last activity this server drove for the session, ISO: stamped once when a run starts
395
+ * and once when it ends (see SessionManager.drive — a goal run is ONE drive over all its
396
+ * rounds, so it stamps twice, not per round). Set to createdAt at insert, and only ever
397
+ * moves forward (markDriven/touchLastActive clamp with MAX, so a backwards clock step
398
+ * cannot regress it). Rows this server never drives — CLI-adopted Sessions, subagent
399
+ * rows (their activity is driven through the parent's entry) — keep createdAt.
400
+ */
401
+ lastActiveAt: string;
402
+ createdAt: string;
403
+ /**
404
+ * The Session's own sandbox policy, snapshotted from the server's settings at creation.
405
+ * NULL = a row from before the column: it takes the settings at its next command.
406
+ */
407
+ sandbox?: SandboxSettings | null;
408
+ }
409
+
410
+ /**
411
+ * Tool call approval: ApproveFn factory + pending
412
+ * approval registry.
413
+ *
414
+ * Approval mode semantics match the CLI (packages/cli/src/approval.ts):
415
+ * allow-all auto-approves; deny-all auto-denies; read-only allows read-only tools
416
+ * (permission==="r") and routes the rest to manual approval; always-ask routes
417
+ * everything to manual approval. Routing to manual approval registers a pending entry
418
+ * and pushes an `approval_request` via SSE, suspending until the frontend decides via
419
+ * `POST /approvals/:toolCallId`; no timeout — pending approvals are resolved to deny
420
+ * when the Task is interrupted (then proceeds through the abort flow).
421
+ *
422
+ * An unattended Session takes that one route differently: there is no frontend to ask, so
423
+ * the call is denied on the spot instead of suspending forever (see `unattended` below).
424
+ *
425
+ * Every approval decision re-reads the current approval_mode (`getMode` reads the DB),
426
+ * so mode changes take effect immediately.
427
+ * Docs: /docs/tools § "Approval".
428
+ */
429
+
430
+ interface PendingApproval {
431
+ toolCall: OmniMessage<ToolCallPayload>;
432
+ origin?: string[];
433
+ }
434
+
435
+ /** A numbered channel event; `id` is `<epoch>-<seq>`, `data` is serialized single-line JSON. */
436
+ interface ChannelEvent {
437
+ id: string;
438
+ /** SSE event name; omitted (OmniMessage) means no `event:` line. */
439
+ event?: string;
440
+ data: string;
441
+ }
442
+ type ChannelListener = (evt: ChannelEvent) => void;
443
+ interface ChannelOptions {
444
+ maxBufferCount?: number;
445
+ maxBufferBytes?: number;
446
+ }
447
+ declare class Channel {
448
+ /** Channel epoch: generated at instance creation, prefixed onto event ids (necessarily changes after recycle/recreate or restart). */
449
+ readonly epoch: string;
450
+ private nextSeq;
451
+ private buffer;
452
+ private bufferBytes;
453
+ /** Max seq among evicted events (0 means never evicted): lower bound for hit checks. */
454
+ private lastEvictedSeq;
455
+ private readonly listeners;
456
+ private readonly maxCount;
457
+ private readonly maxBytes;
458
+ /** Timestamp of last activity (publish/subscription change), used for idle-reclaim checks. */
459
+ lastActivityMs: number;
460
+ constructor(opts?: ChannelOptions);
461
+ get subscriberCount(): number;
462
+ /**
463
+ * Id of the most recently assigned event (`<epoch>-<seq>`; seq 0 when none was assigned
464
+ * yet). Unicast (sendTo) seqs count too — the value is a position marker, not a buffer
465
+ * lookup key. Used as the live-tail cursor on GET /messages: captured in the same
466
+ * synchronous tick as the fragment snapshot, it tells the client which buffered events
467
+ * the snapshot already covers.
468
+ */
469
+ get lastEventId(): string;
470
+ /** Broadcast an event: number it, buffer it (evicting the oldest), notify all subscribers. */
471
+ publish(data: unknown, event?: string): ChannelEvent;
472
+ /** Unicast an event to a single subscriber: consumes a seq but doesn't buffer or broadcast (used for replay at subscribe time). */
473
+ sendTo(listener: ChannelListener, data: unknown, event?: string): ChannelEvent;
474
+ subscribe(listener: ChannelListener): () => void;
475
+ /**
476
+ * Compute replay from a Last-Event-ID (`<epoch>-<seq>`): a mismatched epoch (channel
477
+ * recycled/recreated, process restarted, or malformed id) always misses; a matching
478
+ * epoch hits the buffer (if no events after that seq have been evicted and the seq was
479
+ * indeed assigned by this channel) and returns the buffered events after it; otherwise
480
+ * miss (the caller should send `resync_required` first).
481
+ */
482
+ replayAfter(lastEventId: string): {
483
+ hit: boolean;
484
+ events: ChannelEvent[];
485
+ };
486
+ private makeEvent;
487
+ }
488
+ interface ChannelHubOptions {
489
+ idleMs?: number;
490
+ /**
491
+ * Active check: keys for which this returns true are excluded from idle reclaim (app
492
+ * assembly injects `manager.statusOf(key) !== "idle"`, so a running/compacting
493
+ * Session channel is never reclaimed no matter how long since its last publish; a
494
+ * user channel key looks like `user:<id>` and is always considered active).
495
+ */
496
+ isActive?: (key: string) => boolean;
497
+ }
498
+ /**
499
+ * Channel collection: lazily created by key (Session id or `user:<user_id>`);
500
+ * a channel whose Session is idle and has had no subscribers for over 30 minutes is
501
+ * reclaimed, releasing its buffer as well.
502
+ */
503
+ declare class ChannelHub {
504
+ private readonly channels;
505
+ private readonly timer;
506
+ private readonly idleMs;
507
+ private isActive;
508
+ constructor(opts?: ChannelHubOptions);
509
+ /**
510
+ * Replaces the activity probe. "Is this session busy" is a business question the hub
511
+ * (runtime mechanism) cannot answer itself; the App installs its answer at create over
512
+ * the claimed hub — ordinary capability use, not a registry entry. Overwrite-only, the
513
+ * cross-generation discipline: a successor installs its own probe and a dead
514
+ * generation's is simply left behind (its manager has drained, so it reports idle —
515
+ * never wrong, only replaced).
516
+ */
517
+ setActivityProbe(probe: (key: string) => boolean): void;
518
+ get(key: string): Channel;
519
+ peek(key: string): Channel | undefined;
520
+ /**
521
+ * Publishes to every live channel whose key starts with `prefix` — global
522
+ * notices (e.g. `user:` for "the web assets were hot-swapped, reload").
523
+ * Only reaches channels that exist (i.e. someone connected at least once);
524
+ * that is the right scope for a notice that only matters to live clients.
525
+ */
526
+ broadcast(prefix: string, data: unknown, event?: string): void;
527
+ /** Reclaim idle channels (skips active Sessions: no reclaim even without a publish while awaiting approval); `now` is injectable for tests. */
528
+ sweep(now?: number): void;
529
+ dispose(): void;
530
+ }
531
+
532
+ /** Attribution context for one record (top-level Session scope). */
533
+ interface UsageContext {
534
+ projectId: string;
535
+ agentId: string;
536
+ /** Top-level Session id (the current actual id after self-heal). */
537
+ sessionId: string;
538
+ /** Vendor grouping for the top-level Session's model (paired with modelId; the fallback attribution when the origin mapping has no hit). */
539
+ provider: string;
540
+ /** Upstream model_id of the top-level Session (paired with provider). */
541
+ modelId: string;
542
+ }
543
+
544
+ /**
545
+ * Usage statistics query.
546
+ *
547
+ * Cost is **fixed when a Request completes**: every usage_records row carries the cost the
548
+ * recorder computed from the rates in force at that moment, and nothing here re-prices it — a
549
+ * price edited, synced or discounted later only changes what later Requests cost. The repo
550
+ * returns sums broken down by `(provider, model_id)` paired reference, and this service folds
551
+ * them into cost / hasUncosted (a row whose model had no price is excluded from cost and flags
552
+ * hasUncosted).
553
+ * Summary cards (today / last 7 days / cumulative), grouped aggregation (date /
554
+ * agent / model / session, with the session dimension supporting agentId drill-down
555
+ * filtering), and the zero-filled time series the cost center's charts draw.
556
+ * Server-side error statistics (error_records) ride along on the same response:
557
+ * the statistics center fetches everything in one request, and filters are
558
+ * naturally shared; unattributed errors (login failures, process crashes, and other
559
+ * errors with no Project context) are visible only to admins, see the ErrorsRepo
560
+ * file header.
561
+ */
562
+
563
+ /** The three pricing buckets (usd_per_mtok convention), returned by the pricing lookup callback. */
564
+ interface PricingRates {
565
+ cacheRead: number;
566
+ cacheWrite: number;
567
+ output: number;
568
+ }
569
+
570
+ /** Errors: the mechanism ErrorRecorder implements. */
571
+ declare abstract class Errors {
572
+ abstract record(args: ErrorRecordArgs): void;
573
+ }
574
+
575
+ /** Capture-site source (maps one-to-one to error_records.source). */
576
+ type ErrorSource = "http" | "organization" | "session" | "llm" | "environment" | "compaction" | "usage" | "title" | "subagent" | "process" | "schedule" | "messaging" | "id_suggest";
577
+ /** Error classification: see file header — the criterion is "does a human need to step in". */
578
+ type ErrorKind = "expected" | "unexpected";
579
+ /** Attribution context (all optional: the login endpoint has no Project, and process-level fallback has no request at all). */
580
+ interface ErrorContext {
581
+ projectId?: string;
582
+ agentId?: string;
583
+ sessionId?: string;
584
+ }
585
+ interface ErrorRecordArgs {
586
+ source: ErrorSource;
587
+ /** The caught error (unknown: the value caught may not be an Error; failures from the message stream pass the reason text directly). */
588
+ err: unknown;
589
+ ctx?: ErrorContext;
590
+ /** Semantic code (required for non-HTTP sources, e.g. session_run_failed); defaults to HttpError.code. */
591
+ code?: string;
592
+ /** HTTP status code; leave empty for non-HTTP sources. */
593
+ status?: number;
594
+ /** Explicit classification (see file header); defaults to inferring from `HttpError` — HTTP sources rely on this, other sources should pass it explicitly. */
595
+ kind?: ErrorKind;
596
+ }
597
+ declare class ErrorRecorder implements Errors {
598
+ /** Dedup table: `source \0 code \0 projectId` → timestamp of the last **persist** (see file header). */
599
+ private readonly lastSeen;
600
+ private readonly errors;
601
+ private readonly clock;
602
+ /** Record an error (synchronous, fails silently; same-window duplicates are dropped outright, see file header). */
603
+ record(args: ErrorRecordArgs): void;
604
+ /** true if a same-kind error was already recorded within the window (drop it); otherwise register this persist timestamp and keep the dedup table bounded. */
605
+ private deduped;
606
+ /** Keep the dedup table bounded (see file header): clear expired entries first; if still over the limit (hundreds/thousands of distinct error codes erupting at once), clear it entirely. */
607
+ private evict;
608
+ }
609
+ /** Minimal dependency a capture site needs on the recorder (tests inject a fake; structurally matches SessionManager's UsageRecorderLike). */
610
+ type ErrorSink = Pick<ErrorRecorder, "record">;
611
+
612
+ /** Host-side parameters for one title-generation request. */
613
+ interface TitleRequest {
614
+ /** The user input this run started from: its first words become the immediate fallback title. */
615
+ fallbackText: string;
616
+ /** Material override (the main session passes its input text, a subagent the prompt that spawned it — assistant material stays empty either way, so the request never waits on model output); defaults to the material self-collected by the core Session. */
617
+ material?: {
618
+ userText: string;
619
+ assistantText: string;
620
+ };
621
+ /** The channel to push the `session_title` event to; defaults to `ctx.sessionId`. A
622
+ * subagent has no SSE channel of its own, so its title must reach the frontend via
623
+ * the **parent Session's** channel (the list updates in place by sessionId). */
624
+ notifyOn?: string;
625
+ }
626
+ /** session-manager's minimal dependency on the title generator (tests inject a fake implementation). */
627
+ interface TitleNotifier {
628
+ maybeGenerate(ctx: UsageContext, session: Pick<RuntimeSession, "generateTitle">, req: TitleRequest): void;
629
+ }
630
+
631
+ /**
632
+ * In-process registry of Session sources, derived from core `session_meta` — the single
633
+ * source of truth for what kind of conversation a Session is (the DB stores no `source`
634
+ * column).
635
+ *
636
+ * Populated wherever the server actually has the meta in hand: Session creation
637
+ * (SessionService reads the just-created core Session's meta), subagent registration
638
+ * (SessionManager reads the forwarded child meta), forks (always `user`), and Trace adoption /
639
+ * lazy list resolution (the trace index's head read). A value read back from a Trace is
640
+ * narrowed through core's `normalizeSessionSource` (the retired `benchmark` reads as `cli`),
641
+ * except that a head recording no source is kept as `null`: what it reads as depends on the
642
+ * Session's index row (`readRecordedSource`), which a head read does not have. An absent entry
643
+ * means "unknown": the list path resolves it from the Trace once per process lifetime.
644
+ */
645
+
646
+ /**
647
+ * What a Session's Trace head records as its source: one of the sources, or `null` for a head
648
+ * written before the source was required, which records none.
649
+ */
650
+ type RecordedSource = SessionSource | null;
651
+
652
+ /** One indexed Trace shard. */
653
+ interface TraceFileRow {
654
+ projectId: string;
655
+ agentId: string;
656
+ sessionId: string;
657
+ fileIndex: number;
658
+ date: string;
659
+ sizeBytes: number;
660
+ }
661
+ /** One Session's registration-time facts (earliest shard's head). */
662
+ interface TraceSessionRow {
663
+ sessionId: string;
664
+ projectId: string;
665
+ agentId: string;
666
+ /**
667
+ * The head's session_meta source, narrowed; `null` when the head records none (written before
668
+ * the source was required; read it with the row's client, `readRecordedSource`). Meaningful
669
+ * only once `metaRead`.
670
+ */
671
+ source: RecordedSource;
672
+ workspace: string;
673
+ title: string | null;
674
+ provider: string | null;
675
+ modelId: string | null;
676
+ firstTs: string | null;
677
+ metaRead: boolean;
678
+ }
679
+
680
+ /** TraceIndexStore: the mechanism TraceIndexRepo implements. */
681
+ declare abstract class TraceIndexStore {
682
+ abstract upsertFile(row: TraceFileRow): void;
683
+ abstract updateFileSize(projectId: string, agentId: string, sessionId: string, fileIndex: number, sizeBytes: number): void;
684
+ abstract getPageStats(projectId: string, agentId: string, sessionId: string, fileIndex: number): string | null;
685
+ abstract setPageStats(projectId: string, agentId: string, sessionId: string, fileIndex: number, sizeBytes: number, pageStats: string): void;
686
+ abstract deleteFile(projectId: string, agentId: string, sessionId: string, fileIndex: number): void;
687
+ abstract listFilesByAgent(projectId: string, agentId: string): TraceFileRow[];
688
+ abstract listFilesBySession(projectId: string, agentId: string, sessionId: string): TraceFileRow[];
689
+ abstract findAgentBySession(projectId: string, sessionId: string): string | null;
690
+ abstract upsertSession(row: TraceSessionRow): void;
691
+ abstract getSession(sessionId: string): TraceSessionRow | null;
692
+ abstract listSessionsByAgent(projectId: string, agentId: string): TraceSessionRow[];
693
+ abstract deleteBySession(sessionId: string): void;
694
+ abstract deleteByAgent(projectId: string, agentId: string): void;
695
+ abstract deleteByProject(projectId: string): void;
696
+ }
697
+ /** TraceIndex: the mechanism TraceIndexService implements. */
698
+ declare abstract class TraceIndex {
699
+ abstract readonly counters: {
700
+ gateStats: number;
701
+ dirScans: number;
702
+ headReads: number;
703
+ };
704
+ abstract reconcileAgent(projectId: string, agentId: string, opts?: {
705
+ force?: boolean;
706
+ }): Promise<void>;
707
+ abstract reconcileProject(projectId: string, opts?: {
708
+ force?: boolean;
709
+ }): Promise<void>;
710
+ abstract registerImportedFile(args: {
711
+ projectId: string;
712
+ agentId: string;
713
+ sessionId: string;
714
+ fileIndex: number;
715
+ date: string;
716
+ sizeBytes: number;
717
+ records: OmniMessage[];
718
+ }): void;
719
+ abstract removeSession(projectId: string, agentId: string, sessionId: string): void;
720
+ abstract removeAgent(projectId: string, agentId: string): void;
721
+ abstract removeProject(projectId: string): void;
722
+ }
723
+
724
+ interface PlatformCatalogPricing {
725
+ unit: "usd_per_mtok";
726
+ cacheRead: number;
727
+ cacheWrite: number;
728
+ output: number;
729
+ }
730
+ /** A platform model normalized into the fields Penguin persists for a newly discovered row. */
731
+ interface PlatformCatalogModel {
732
+ modelId: string;
733
+ displayName: string;
734
+ contextWindow: number;
735
+ supportsVision: boolean;
736
+ /**
737
+ * List price, which is what a Project stores: the platform's `listPricing` when it runs a
738
+ * promotion on the model, its billed `pricing` otherwise.
739
+ */
740
+ pricing: PlatformCatalogPricing;
741
+ /** Fraction off `pricing` the platform is running (0.5 = half price); absent when none. */
742
+ discount?: number;
743
+ baseUrl: string;
744
+ clientType: "gemini-3.8" | "deepseek-v4";
745
+ }
746
+ /** Validated snapshot returned by Penguin Go's client-model catalog. */
747
+ interface PlatformModelCatalog {
748
+ models: PlatformCatalogModel[];
749
+ }
750
+ /** Catalog merge result. `updated` counts existing rows whose list price, client type or promotion changed. */
751
+ interface PlatformModelApplyResult {
752
+ added: number;
753
+ updated: number;
754
+ applied: number;
755
+ }
756
+
757
+ type RawTable = Record<string, unknown>;
758
+ /**
759
+ * What one utility completion produced: the model's text, or why there is none. A bare null
760
+ * would say "no answer" and nothing else — and every one of these failures (no model
761
+ * configured, a missing credential, a provider rejection, a timeout, an answer that was all
762
+ * thinking) is one the user can act on, so the reason travels to the caller, which records it.
763
+ */
764
+ type UtilityCompletion = {
765
+ ok: true;
766
+ text: string;
767
+ } | {
768
+ ok: false;
769
+ /** `no_model`: the Project names no default model, or names one it has no entry for — nothing was asked. `failed`: the model was asked and produced no text. */
770
+ cause: "no_model" | "failed";
771
+ error: string;
772
+ };
773
+
774
+ /** ProjectConfigStore: the mechanism ProjectConfigService implements. */
775
+ declare abstract class ProjectConfigStore {
776
+ abstract readRaw(projectId: string): Promise<RawTable>;
777
+ abstract loadConfig(projectId: string): Promise<ProjectConfig>;
778
+ abstract writeRaw(projectId: string, data: RawTable): Promise<void>;
779
+ abstract writeInitialConfig(projectId: string, name: string): Promise<void>;
780
+ abstract ensurePresetModels(projectId: string): Promise<boolean>;
781
+ /** Fills in a display name for an adopted Project that has none (see the implementation's own doc). */
782
+ abstract ensureDisplayName(projectId: string, name: string): Promise<void>;
783
+ abstract seedPresetPromotions(projectId: string): Promise<void>;
784
+ abstract getName(projectId: string): Promise<string | undefined>;
785
+ abstract setName(projectId: string, name: string): Promise<void>;
786
+ abstract getDefaultModelRef(projectId: string): Promise<ModelRef | undefined>;
787
+ abstract setDefaultModelRef(projectId: string, ref: ModelRefDto): Promise<ModelRefDto>;
788
+ abstract getChatDefaults(projectId: string): Promise<ChatDefaultsDto>;
789
+ abstract setChatDefaults(projectId: string, req: ChatDefaultsDto): Promise<ChatDefaultsDto>;
790
+ /** The `[plugins]` key: the shared table and each machine's own, package name → requirement, in the file's order. */
791
+ abstract getPluginTables(projectId: string): Promise<PluginTables>;
792
+ /** Replaces the tables (a declarative PUT); answers what was written. */
793
+ abstract setPluginTables(projectId: string, tables: PluginTables): Promise<PluginTables>;
794
+ abstract getCommandPolicy(projectId: string): Promise<CommandPolicyDto>;
795
+ abstract setCommandPolicy(projectId: string, req: {
796
+ enabled?: boolean;
797
+ rules: {
798
+ name: string;
799
+ pattern: string;
800
+ description?: string;
801
+ enabled?: boolean;
802
+ }[];
803
+ }): Promise<CommandPolicyDto>;
804
+ abstract getPricing(projectId: string, provider: string, modelId: string, at: Date): Promise<PricingRates | undefined>;
805
+ abstract detectVision(projectId: string, req: ModelVisionDetectRequest): Promise<ModelVisionDetectResponse>;
806
+ abstract testModel(projectId: string, req: ModelTestRequest): Promise<ModelTestResponse>;
807
+ abstract detectProtocol(projectId: string, req: ModelProtocolDetectRequest): Promise<ModelProtocolDetectResponse>;
808
+ abstract listEndpointModels(req: EndpointModelListRequest, listImpl?: (options: ListEndpointModelsOptions) => Promise<string[]>, timeoutMs?: number): Promise<EndpointModelListResponse>;
809
+ abstract getModels(projectId: string): Promise<ModelsResponse>;
810
+ abstract updateModels(projectId: string, req: ModelsUpdateRequest): Promise<ModelsResponse>;
811
+ abstract setGroupApiKey(projectId: string, provider: string, apiKey: string): Promise<number>;
812
+ abstract setGroupApiKeyWithProviderAuthToken(projectId: string, provider: string, apiKey: string, token: Omit<ModelProviderAuthToken, "provider" | "updatedAt">, options?: {
813
+ expectedRefreshToken?: string;
814
+ }): Promise<number>;
815
+ abstract getGroupApiKey(projectId: string, provider: string): Promise<string | undefined>;
816
+ /**
817
+ * The key a group's account balance is read with: the stored group key, or else the
818
+ * environment key a Session on one of the group's rows would use, when that variable's
819
+ * official endpoint is the balance endpoint's own host. Server-side only.
820
+ */
821
+ abstract getGroupBalanceKey(projectId: string, provider: string): Promise<string | undefined>;
822
+ abstract setModelApiKeyResolver(resolver: (context: ModelRequestContext) => Promise<string | undefined>): void;
823
+ abstract mergePlatformModels(projectId: string, provider: string, catalog: PlatformModelCatalog, apiKey: string, applyKeyToExisting: boolean): Promise<PlatformModelApplyResult>;
824
+ abstract completeOnce(projectId: string, prompt: string): Promise<UtilityCompletion>;
825
+ }
826
+ interface ModelProviderAuthToken {
827
+ provider: string;
828
+ refreshToken: string;
829
+ accessTokenExpiresAt?: string;
830
+ updatedAt: string;
831
+ }
832
+
833
+ /**
834
+ * The order a Session list is served in: `created` (the default) is newest creation first;
835
+ * `activity` is most recent `lastActiveAt` first — the order the sidebar displays, and the only
836
+ * one an {@link ActivityCursor} pages.
837
+ */
838
+ type SessionListOrder = "created" | "activity";
839
+ /** A row's place in the activity order; as a `before` cursor, the last row the client holds. */
840
+ interface ActivityCursor {
841
+ lastActiveAt: string;
842
+ sessionId: string;
843
+ }
844
+ /**
845
+ * A slice of a Session list: `offset` rows skipped (the `created` contract, also accepted under
846
+ * `activity`), or every row strictly below `before` in the activity order.
847
+ */
848
+ type SessionListPaging = {
849
+ offset: number;
850
+ limit: number;
851
+ } | {
852
+ before: ActivityCursor;
853
+ limit: number;
854
+ };
855
+ interface SessionServiceDeps {
856
+ root: string;
857
+ sessions: SessionIndex;
858
+ manager: SessionManager;
859
+ projectConfig: ProjectConfigStore;
860
+ /** In-process origin registry derived from session_meta (the DB stores no source column). */
861
+ sources: SessionOrigins;
862
+ /** Trace-file index: discovery / adoption / stats serve from it (mtime-gated reconciler; no per-request walks). */
863
+ traceIndex: TraceIndex;
864
+ /** The index rows themselves (files and sessions), for the reads the service does directly. */
865
+ traceStore: TraceIndexStore;
866
+ /**
867
+ * Admin proxy-settings threading (same getter the session loader passes, see
868
+ * createCoreSessionLoader): the runtime created here is adopted by the manager and
869
+ * runs the Session's first Task, so it needs the command-subprocess proxy policy too.
870
+ */
871
+ proxyEnv?: () => ProxyEnvPolicy | null;
872
+ /**
873
+ * Harness-control env threading (same policy the session loader passes): the server's
874
+ * API URL/token plus the Session coordinates, injected into command subprocesses so
875
+ * agents can drive the harness back through the CLI/API.
876
+ */
877
+ controlEnv?: (ctx: ControlEnvContext) => Record<string, string>;
878
+ /**
879
+ * PATH threading (same getter the session loader passes): the shim directory holding
880
+ * this harness's own `penguin`, put in front of every command an Agent runs.
881
+ */
882
+ pathPrepend?: () => string[];
883
+ /**
884
+ * Tells every user of a Project that a Session now exists. The list only ever learns
885
+ * about rows it did not create itself from this: a Session started by the CLI, by
886
+ * another tab, by a schedule, or by an agent spawning a child sat invisible until the
887
+ * next full reload without it. Optional so the service keeps unit-testing without a
888
+ * channel registry; absent means nobody is told.
889
+ */
890
+ notifyProjectUsers?: (projectId: string, event: ServerEvent) => void;
891
+ /**
892
+ * The channel of the Session's ENABLED messaging binding, or null when none is enabled
893
+ * (SessionInfo.messagingChannel, the sidebar row's per-channel indicator — saved-but-
894
+ * disabled configs stay off the row). A lookup lambda rather than the repo, so the
895
+ * service stays decoupled from the bindings table; absent (older assemblies/tests)
896
+ * means the field is never set.
897
+ */
898
+ messagingChannel?: (sessionId: string) => MessagingChannel | null;
899
+ /**
900
+ * Company mode: the organization owning a Session (a desk session, or a session
901
+ * contributing to a ticket), for `SessionInfo.orgId` — development mode's list hides
902
+ * those rows, the company sidebar groups them. Two shapes because the two flows cost
903
+ * differently: the single-Session GET asks about one id, a list asks once for the whole
904
+ * Project and looks its rows up in the returned map, so a long list never costs a query
905
+ * per row. Lambdas rather than the repo, so the service stays decoupled from the
906
+ * company-mode caches; absent (older assemblies/tests) means the field is never set.
907
+ */
908
+ orgIdOfSession?: (sessionId: string) => string | undefined;
909
+ orgIdsOfProject?: (projectId: string) => ReadonlyMap<string, string>;
910
+ /** Spawn-confinement getter (the sandbox module's), forwarded into core beside proxyEnv. */
911
+ confineSpawn?: (ctx: ControlEnvContext) => SpawnConfiner | null;
912
+ /** The server's Sandbox settings: what a new Session's policy is snapshotted from. */
913
+ sandboxDefaults?: () => SandboxSettings;
914
+ /** Host-owned model-request hooks, forwarded to core for every Session LLM. */
915
+ assembly?: AgentAssembly;
916
+ /** The dimensions the mounted sandbox backends implement between them (none when absent). */
917
+ sandboxDimensions?: () => readonly SandboxDimension[];
918
+ /** The enabled sandbox backends that failed to load or failed their check, with why. */
919
+ sandboxUnavailable?: () => readonly UnavailableSandboxBackend[];
920
+ /** The names of the sandbox backends in use, in routing preference (none when absent). */
921
+ sandboxBackends?: () => readonly string[];
922
+ /** The Sandbox card's presets table, in table order (absent: the view carries none). */
923
+ sandboxPresets?: () => readonly SessionSandboxPreset[];
924
+ /** The Sandbox card's switch: whether new Sessions start confined (absent: not reported). */
925
+ sandboxSwitchOn?: () => boolean;
926
+ /** The approval mode a new Session starts with when its request names none (the default preset's). */
927
+ sandboxDefaultApproval?: () => ApprovalMode | undefined;
928
+ }
929
+ declare class SessionService {
930
+ private readonly deps;
931
+ constructor(deps: SessionServiceDeps);
932
+ /** The policy a new Session starts with: the server's Sandbox settings. */
933
+ defaultSandbox(): SandboxSettings;
934
+ /** Whether this server can enforce the `local` network level right now. */
935
+ localNetworkSupported(): boolean;
936
+ private sandboxDimensions;
937
+ /** A policy as the composer sees it, with which of its levels this server can enforce. */
938
+ sandboxView(policy: SandboxSettings): SessionSandbox;
939
+ /**
940
+ * What a new Session starts from, as the composer's draft reads it (the chat defaults): the
941
+ * settings' policy, plus the approval mode the default preset gives a request naming none.
942
+ */
943
+ defaultsView(): SessionSandbox;
944
+ /**
945
+ * The approval mode a Session created without one starts with. An organization's Session
946
+ * keeps `allow-all`: its runtime names the mode it wants, and nobody is there to answer an
947
+ * ask a preset might bring. The other unattended creators — a scheduled run, a workflow —
948
+ * pass `allow-all` themselves (scheduler.ts, workflows/service.ts).
949
+ */
950
+ private startApproval;
951
+ /** A Session's policy: its snapshot, or — for a row from before snapshots — the settings. */
952
+ sandboxOf(row: SessionRow): SandboxSettings;
953
+ /**
954
+ * The policy a pick would give one Session, checked — the non-admin ceiling and what this
955
+ * server can enforce — and not written: a PATCH checks every field before it writes any.
956
+ */
957
+ pickSandbox(row: SessionRow, pick: Partial<SessionSandbox>, isAdmin: boolean): SandboxSettings;
958
+ /** Stores one Session's policy (`pickSandbox`'s): its next command runs under it. */
959
+ updateSandbox(sessionId: string, policy: SandboxSettings): void;
960
+ /**
961
+ * DB row -> SessionInfo (run status and pending approval count come from session-manager).
962
+ * Async because `source` is derived from session_meta: a registry miss (Session predating
963
+ * this process) falls back to reading the Trace head once (see sourceOf). `traces` is the
964
+ * list flow's one-walk discovery result; without it a miss locates the shard itself.
965
+ *
966
+ * `orgIds` is the list flow's one-query organization map (see listSessions); without it the
967
+ * organization is a point lookup, which is what the single-Session paths want.
968
+ */
969
+ toInfo(row: SessionRow, hasTrace: boolean, orgIds?: ReadonlyMap<string, string>): Promise<SessionInfo>;
970
+ /**
971
+ * A Session's source, with session_meta as the single source of truth: the in-process
972
+ * registry answers first (populated at creation / subagent registration / forks / adoption /
973
+ * index registration); on a miss (a Session created before this process started) the
974
+ * trace index's registration-time facts answer — the reconciler head-read the earliest
975
+ * shard once when the file first appeared, so no file is touched here. A head that records
976
+ * no source is read with the row's client (readRecordedSource). A Session with no Trace yet
977
+ * is NOT cached — its meta appears with the first run — and reads as its row says
978
+ * (unrunSource): the organization runtime's as `company`, the Agent API's as `api`, any other
979
+ * as unknown.
980
+ */
981
+ private sourceOf;
982
+ /** Whether this Session already has a Trace record (a Task has been run): answered by the index (reconciled first). */
983
+ hasTrace(row: SessionRow): Promise<boolean>;
984
+ /**
985
+ * The list category of a row (see listCategory): none for a `company` Session, archived or
986
+ * not, which the list never serves; otherwise archived wins (an explicit user action), then
987
+ * its source's.
988
+ */
989
+ private categoryOf;
990
+ /**
991
+ * List, sorted by createdAt descending — or, with `order: "activity"`, by lastActiveAt
992
+ * descending (ties by sessionId, see {@link compareActivityDesc}). Every row is served
993
+ * **straight from the DB**, whichever client created it, with no Trace directory scanning —
994
+ * the answer to many-session sidebar reloads re-walking the filesystem on every request (#139).
995
+ * Sessions living only in the Trace directory were adopted into the index by the
996
+ * boot-time sweep (`adoptUnmanagedTraceSessions`), so listing never discovers. One
997
+ * lazy discovery walk still runs for a list call that contains rows this process has
998
+ * not classified yet (no in-process source entry): it supplies the Trace locations for
999
+ * the one-time head reads and backfills the `has_trace` cache; once every row is
1000
+ * classified, list calls touch only the DB.
1001
+ *
1002
+ * Optional `paging` returns just that slice (the sidebar pages with limit+1 to detect
1003
+ * "has more"); slicing happens before toInfo, so per-request source derivation (lazy
1004
+ * Trace-head reads) stays bounded by the page size.
1005
+ *
1006
+ * The `before` form pages the activity order by cursor: only rows strictly below it are
1007
+ * walked and served. An offset cannot page that order, because a row below the offset that
1008
+ * becomes active moves above it and the next page would skip a row; a cursor stays put — the
1009
+ * moved row is simply not served again (the client learns of it from `session_state`). The
1010
+ * offset form keeps serving the `created` order exactly as before.
1011
+ *
1012
+ * `category` filters to one sidebar bucket **before** paging, so offset/limit page
1013
+ * within the category. Filtering needs each walked row's category (a possible
1014
+ * Trace-head read per row, cached in the sources registry); without `withCounts`
1015
+ * the walk stops as soon as the requested page is complete. `withCounts` classifies
1016
+ * every row, a cursor or not, and returns per-category totals over the whole list — plus
1017
+ * the same totals broken down by Workspace path, and each path's newest Session's
1018
+ * `createdAt` — so the sidebar can label the collapsed folders, list every Workspace that holds
1019
+ * Sessions (not only the ones its loaded pages happen to touch) and place the groups
1020
+ * by recency, all without loading them.
1021
+ *
1022
+ * `workspaceGroup` filters the same way, to one Workspace group (see workspace-group.ts),
1023
+ * so a sidebar grouped by Workspace pages each group down its OWN stream instead of
1024
+ * sharing one per-Agent cursor — without it, one group's "load more" consumes the page
1025
+ * its siblings were about to read, and their rows move on screen untouched. The two
1026
+ * filters compose; the returned counts stay whole-Agent either way.
1027
+ *
1028
+ * A `company` Session — company mode's desk and ticket Sessions, which only company mode's
1029
+ * own views list — belongs to no category (listCategory), archived or not: every classified
1030
+ * form of the list (`category`, `workspaceGroup` or `withCounts`) leaves it out of the page,
1031
+ * the totals, the Workspace breakdown and its stamps alike. The plain form, which classifies
1032
+ * nothing, still serves every row.
1033
+ *
1034
+ * `excludeOrg` drops the rows an organization owns (its desk and ticket sessions, and the
1035
+ * sub-sessions they spawned) from the stream BEFORE anything else looks at it — the page,
1036
+ * `counts`, `workspaceCounts`, `workspaceLatest` and the limit+1 "has more" all describe the
1037
+ * same own-rows stream. It is what development mode's list asks for: that list draws the
1038
+ * user's own conversations, and a total or a stamp that still counted a desk or a ticket
1039
+ * session would make its Workspace appear as a group the list can never fill. The `company`
1040
+ * rule above does not replace it: the sub-sessions are `subagent` Sessions, a person's
1041
+ * conversation attached to a ticket is stamped `org` but stays `user`, and a row the
1042
+ * organization caches name before the reconcile pass stamps it reads as `user`. Without the
1043
+ * flag every row is served, whichever client created it, but for that rule.
1044
+ */
1045
+ listSessions(projectId: string, agentId: string, opts?: {
1046
+ paging?: SessionListPaging;
1047
+ order?: SessionListOrder;
1048
+ category?: SessionCategory;
1049
+ workspaceGroup?: string;
1050
+ withCounts?: boolean;
1051
+ excludeOrg?: boolean;
1052
+ }): Promise<{
1053
+ sessions: SessionInfo[];
1054
+ counts?: SessionCategoryCounts;
1055
+ workspaceCounts?: Record<string, SessionCategoryCounts>;
1056
+ workspaceLatest?: Record<string, string>;
1057
+ }>;
1058
+ /**
1059
+ * Session stats (Agents list card): total count = size of the union of DB index
1060
+ * ∪ Trace directory discovery; activity = number of active Sessions per day over
1061
+ * the last `days` days (deduplicated count of Sessions created that day or with a
1062
+ * Trace record that day; index 0 = earliest, last index = today). Counts only —
1063
+ * does not backfill index rows.
1064
+ */
1065
+ sessionStats(projectId: string, agentId: string, days: number): Promise<{
1066
+ sessionCount: number;
1067
+ activity: number[];
1068
+ }>;
1069
+ /**
1070
+ * Create a Session: the model reference is a complete `(provider, modelId)` pair.
1071
+ * Half a reference is a client error, never something to resolve — the missing half
1072
+ * is never guessed, since a guessed provider would send an entry's credential to a
1073
+ * vendor nobody named. Omitting both falls back to the Project's default reference
1074
+ * (400 prompting to configure a model first if there is none). `workspace` is already
1075
+ * validated by the route guard. The new Session is added to the active table
1076
+ * (idle).
1077
+ */
1078
+ createSession(args: {
1079
+ projectId: string;
1080
+ agentId: string;
1081
+ /** Upstream id of the session's model; always paired with provider. Omit both for the Project's default reference. */
1082
+ modelId?: string;
1083
+ /** The provider group for `modelId`; always paired with modelId, never inferred. */
1084
+ provider?: string;
1085
+ workspace?: string;
1086
+ approvalMode?: ApprovalMode;
1087
+ /** The composer's sandbox picks; omitted halves take the server's settings. */
1088
+ sandbox?: Partial<SessionSandbox>;
1089
+ /** Whether the creator is an administrator (may loosen past the settings). Default false. */
1090
+ isAdmin?: boolean;
1091
+ /**
1092
+ * What kind of conversation this is, recorded in the Session's session_meta: `schedule`
1093
+ * from the scheduler, `company` from the organization runtime, `cli` from `penguin run`
1094
+ * (the only source a request may name), `api` from an Agent API run; absent means `user`,
1095
+ * a person's conversation.
1096
+ */
1097
+ source?: SessionSource;
1098
+ /**
1099
+ * Creating-client hint stored on the index row (`POST .../sessions` body `client`):
1100
+ * "cli" from the CLI, defaulting to "web". "org" and "api" are not accepted over HTTP —
1101
+ * the organization runtime and the Agent API's run handler call this method directly and
1102
+ * are the only callers that pass them, so no request can claim either provenance for
1103
+ * itself ("api" is what lets the Agent API continue a Session).
1104
+ */
1105
+ client?: "web" | "cli" | "org" | "api";
1106
+ }): Promise<SessionInfo>;
1107
+ /** A new Session's snapshot: the settings, with the creator's picks (checked) on top. */
1108
+ private snapshotSandbox;
1109
+ /**
1110
+ * Absolute path of a Session's **latest** Trace file (the current context shard);
1111
+ * undefined when no Trace exists. Costs a directory walk, so only the single-session
1112
+ * GET includes it in the DTO (see SessionInfo.tracePath) — the web's `/model` switch
1113
+ * hands it to the new session's `[model_switch_from]` block so the model can read the
1114
+ * source history itself when it needs it.
1115
+ */
1116
+ latestTracePath(row: SessionRow): Promise<string | undefined>;
1117
+ /**
1118
+ * One walk over the Trace directory: session_id → its **earliest** shard (the shard
1119
+ * whose head carries the original session_meta). Discovery (which Sessions have
1120
+ * records) and the meta-read location come out of a single pass, so classifying every
1121
+ * row (`counts=1`) costs one directory walk total instead of one per Session.
1122
+ */
1123
+ private discoverTraces;
1124
+ /**
1125
+ * Startup adoption sweep: walks the whole trace tree once per boot and adopts every
1126
+ * unmanaged Session (a Trace with no index row — legacy CLI-direct runs) as a
1127
+ * `client: "cli"` row, so lists stay pure-SQLite afterwards. Enumerates
1128
+ * `<root>/<project>/agents/<agent>/` directories directly — legacy Traces can live
1129
+ * under Projects the DB has never seen — and reuses the existing discovery/adoption
1130
+ * path per Agent (mtime-gated TraceIndexService reconcile, then registration-time
1131
+ * facts; adoption reads no file itself). Idempotent (insertOrIgnore), so re-running —
1132
+ * e.g. after a hot swap re-assembles the business surface — only costs the gated
1133
+ * reconcile. Returns the number of rows adopted.
1134
+ */
1135
+ adoptUnmanagedTraceSessions(): Promise<number>;
1136
+ /**
1137
+ * Adopts a Session that exists only in the Trace directory, from the index's
1138
+ * registration-time facts (the reconciler head-read the earliest shard's session_meta
1139
+ * once when the file first appeared — adoption itself reads no file).
1140
+ */
1141
+ private adoptTraceSession;
1142
+ }
1143
+
1144
+ type ModelScopeAuthStatusResult = ModelScopeAuthFlowStatusResponse & {
1145
+ changed?: true;
1146
+ };
1147
+ interface ModelScopeCredential {
1148
+ accessToken: string;
1149
+ refreshToken?: string;
1150
+ expiresAt?: string;
1151
+ }
1152
+ interface ModelScopeRefreshResult {
1153
+ changed: boolean;
1154
+ }
1155
+ interface ModelScopeAuthDeps {
1156
+ /** Base URL of the bridge, without a trailing slash (config.modelscopeBridgeUrl). */
1157
+ bridgeUrl: string;
1158
+ /** Writes the delivered credential; resolves to how many rows took the access token. */
1159
+ applyCredential: (projectId: string, credential: ModelScopeCredential, options?: {
1160
+ expectedRefreshToken?: string;
1161
+ }) => Promise<number>;
1162
+ /** Reads the server-side refresh metadata for the group. */
1163
+ readStoredCredential?: (projectId: string) => ModelProviderAuthToken | undefined;
1164
+ fetchImpl?: typeof fetch;
1165
+ now?: () => number;
1166
+ }
1167
+ declare class ModelScopeAuthService {
1168
+ private readonly deps;
1169
+ private readonly flows;
1170
+ private readonly refreshes;
1171
+ private readonly refreshFailures;
1172
+ private readonly fetchImpl;
1173
+ private readonly now;
1174
+ constructor(deps: ModelScopeAuthDeps);
1175
+ private url;
1176
+ private isPending;
1177
+ private request;
1178
+ ensureFresh(input: {
1179
+ projectId: string;
1180
+ provider: string;
1181
+ }): Promise<ModelScopeRefreshResult>;
1182
+ start(input: {
1183
+ projectId: string;
1184
+ userId: string;
1185
+ }): Promise<{
1186
+ flowId: string;
1187
+ authorizeUrl: string;
1188
+ expiresAt: string;
1189
+ }>;
1190
+ status(input: {
1191
+ flowId: string;
1192
+ projectId: string;
1193
+ userId: string;
1194
+ }): Promise<ModelScopeAuthStatusResult>;
1195
+ retryApply(input: {
1196
+ flowId: string;
1197
+ projectId: string;
1198
+ userId: string;
1199
+ }): Promise<ModelScopeAuthStatusResult>;
1200
+ cancel(input: {
1201
+ flowId: string;
1202
+ projectId: string;
1203
+ userId: string;
1204
+ }): void;
1205
+ private refreshState;
1206
+ private refreshNow;
1207
+ private reauthorizationRequired;
1208
+ private pollBridge;
1209
+ private apply;
1210
+ private publicFlow;
1211
+ private requireFlow;
1212
+ private sweep;
1213
+ private evictOldest;
1214
+ }
1215
+ declare const ModelScopeAuth_base: (abstract new () => Pick<ModelScopeAuthService, "start" | "status" | "retryApply" | "cancel" | "ensureFresh">) & (<C>(target: C, context: ClassDecoratorContext) => C);
1216
+ /** The per-App ModelScope key authorization capability consumed by Project model routes. */
1217
+ declare abstract class ModelScopeAuth extends ModelScopeAuth_base {
1218
+ }
1219
+
1220
+ /** Minimal interface for a runtime Session (satisfied by core Session; tests may inject a fake implementation). */
1221
+ interface RuntimeSession {
1222
+ readonly sessionId: string;
1223
+ run(newMessages: OmniMessage[], opts: {
1224
+ approve: ApproveFn;
1225
+ signal: AbortSignal;
1226
+ }): AsyncGenerator<OmniMessage>;
1227
+ compact(opts: {
1228
+ signal: AbortSignal;
1229
+ }): AsyncGenerator<OmniMessage>;
1230
+ /**
1231
+ * The running context's model (core `Session.provider` / `Session.modelId`, getters over
1232
+ * the context's session_meta): the row's pair at creation, moved by each in-session switch
1233
+ * — and, after a restart, whatever the Trace resumed on. Optional: test fakes may omit them,
1234
+ * in which case the entry keeps the row's pair.
1235
+ */
1236
+ readonly provider?: string;
1237
+ readonly modelId?: string;
1238
+ /**
1239
+ * Switches the Session's model in place (core `Session.switchModel`): closes the running
1240
+ * context on the model it ran on, then opens the next one on the target. A completed switch
1241
+ * streams the new context's main-session `session_meta` last, and the getters above answer
1242
+ * the target by then; a refusal is thrown before the first event; a Session that never ran
1243
+ * switches without streaming anything. Optional: test fakes may omit it, reading as
1244
+ * "switching is not available".
1245
+ */
1246
+ switchModel?(opts: {
1247
+ provider: string;
1248
+ modelId: string;
1249
+ signal: AbortSignal;
1250
+ }): AsyncGenerator<OmniMessage, StopReason>;
1251
+ /**
1252
+ * The Session's thinking level (core `Session.thinkingLevel`, plain state): soft-limited —
1253
+ * an assignment applies from the Session's very next LLM request. Optional: test fakes may
1254
+ * omit it.
1255
+ */
1256
+ thinkingLevel?: ThinkingLevelName;
1257
+ /** Whether compaction is possible and why; when not ok, compact() yields no messages (see core ContextEngine.compactability). */
1258
+ compactability(): CompactAvailability;
1259
+ /** Queues a mid-run steering input (core `Session.steer`); false when no Task is running. */
1260
+ steer(input: OmniMessage[]): boolean;
1261
+ /**
1262
+ * Withdraws a steering input queued via `steer` before core delivers it (core
1263
+ * `Session.unsteer`, matched by input-list identity); false when it is no longer queued —
1264
+ * already delivered, or the run exited. Optional: test fakes may omit it, in which case
1265
+ * every recall reports "already delivered".
1266
+ */
1267
+ unsteer?(input: OmniMessage[]): boolean;
1268
+ /** Skips the in-progress reconnect backoff, firing the next retry immediately (core `Session.skipReconnectWait`); false when no wait is in progress. */
1269
+ skipReconnectWait(): boolean;
1270
+ /**
1271
+ * Runs the named package's `user_prompt` hook (core `Session.runUserPromptHook` — hooks
1272
+ * run in core and nowhere else); null = the package is not installed or names no such
1273
+ * command. Optional: test fakes may omit it, reading as not installed.
1274
+ */
1275
+ runUserPromptHook?(name: string, prompt: string, extras?: Record<string, string | number | boolean>): Promise<{
1276
+ context?: string;
1277
+ } | null>;
1278
+ /** A tool's permission level from the running context's toolset (core `Session.toolPermission`; strict-tier — rebuilt at rotation). */
1279
+ toolPermission(name: string): "r" | "rw" | undefined;
1280
+ /**
1281
+ * Out-of-band one-shot request for title generation (core `Session.generateTitle`,
1282
+ * writes no history/Trace). Material defaults to what the Session collects itself
1283
+ * (the first Task's text gathered during run); `material` overrides this for
1284
+ * subagents.
1285
+ */
1286
+ generateTitle(args?: {
1287
+ material?: {
1288
+ userText: string;
1289
+ assistantText: string;
1290
+ };
1291
+ signal?: AbortSignal;
1292
+ }): Promise<SessionTitleResult>;
1293
+ /**
1294
+ * Subscribes the idle-arrival signal for background-task completion notices (core
1295
+ * `Session.onBackgroundNotice`): fired when a `run_in_background` launch settles while no
1296
+ * Task is running — the manager then takes the queue and submits it as an ordinary task.
1297
+ * Optional: test fakes may omit it.
1298
+ */
1299
+ onBackgroundNotice?(listener: () => void): void;
1300
+ /** Takes the queued background completion notices as task input (core `Session.takeBackgroundNotices`). Optional, like onBackgroundNotice. */
1301
+ takeBackgroundNotices?(): OmniMessage[];
1302
+ /** Whether completion notices are still queued (core `Session.hasPendingBackgroundNotices`); pins the entry against idle eviction. Optional, like onBackgroundNotice. */
1303
+ hasPendingBackgroundNotices?(): boolean;
1304
+ /** Refreshes the listen-port probes behind the process list's `serviceUrl` (core `Session.probeBackgroundCommandServices`). Optional: test fakes may omit it. */
1305
+ probeBackgroundCommandServices?(): Promise<void>;
1306
+ /** Subscribes live-forwarded background-subagent messages (core `Session.onBackgroundMessage`); the manager publishes them to the session channel. Optional: test fakes may omit it. */
1307
+ onBackgroundMessage?(listener: (msg: OmniMessage) => void): void;
1308
+ /** Background command processes owned by the Session's environment (core `Session.listBackgroundCommands`). Optional: test fakes may omit it. */
1309
+ listBackgroundCommands?(): BackgroundCommandInfo[];
1310
+ /** Kills one background command process (core `Session.killBackgroundCommand`); false when the id is unknown. Optional, like listBackgroundCommands. */
1311
+ killBackgroundCommand?(processId: string): boolean;
1312
+ /** Hands one EXECUTING tool call back as a background task (core `Session.detachToolCall`). Optional: test fakes may omit it, reading as "not_running". */
1313
+ detachToolCall?(toolCallId: string): ToolDetachResult;
1314
+ /** Whether a background subagent is mid-round (core `Session.hasRunningBackgroundSubagents`); pins the entry against idle eviction. Optional, like listBackgroundCommands. */
1315
+ hasRunningBackgroundSubagents?(): boolean;
1316
+ /** All live subagent child sessions of the Session's environment (core `Session.listBackgroundSubagents`). Optional, like listBackgroundCommands. */
1317
+ listBackgroundSubagents?(): BackgroundSubagentInfo[];
1318
+ /** Host message to one child session — steering mid-run, a follow-up run when idle, a revival when released (core `Session.sendToBackgroundSubagent`). Optional. */
1319
+ sendToBackgroundSubagent?(childSessionId: string, messages: OmniMessage[], opts?: SubagentMessageOptions): Promise<SubagentMessageOutcome>;
1320
+ /** Subscribes the host's session-lifetime fallback approval sink for child sessions (core `Session.setSubagentApprovalFallback`). Optional. */
1321
+ setSubagentApprovalFallback?(approve: ApproveFn): void;
1322
+ /** Aborts one child session's current run, keeping the session (core `Session.abortBackgroundSubagentRun`); false when unknown or idle. Optional. */
1323
+ abortBackgroundSubagentRun?(childSessionId: string): boolean;
1324
+ /** Pins one live child session's thinking level (core `Session.setBackgroundSubagentThinkingLevel`); false when the child is not live. Optional. */
1325
+ setBackgroundSubagentThinkingLevel?(childSessionId: string, level: ThinkingLevelName): boolean;
1326
+ /** Subscribes subagent run-state changes (core `Session.onSubagentState`): the manager republishes `task_state` with the fresh live listing. Optional. */
1327
+ onSubagentState?(listener: () => void): void;
1328
+ /** Subscribes background-task state changes (core `Session.onBackgroundState`): the manager re-counts the live registries and publishes `session_background` when the counts moved. Optional. */
1329
+ onBackgroundState?(listener: () => void): void;
1330
+ /** Releases environment resources — kills the remaining background processes (core `Session.dispose`). Optional, idempotent. */
1331
+ dispose?(): void;
1332
+ }
1333
+ /** The underlying loader behind get-or-resume-or-heal. */
1334
+ interface SessionLoader {
1335
+ /**
1336
+ * Load a runtime Session from an index row: recover (with a Trace) or self-heal
1337
+ * rebuild (no Trace, session_id will change). Throws HttpError(409) for unrecoverable
1338
+ * cases such as a missing Workspace.
1339
+ */
1340
+ load(row: SessionRow): Promise<RuntimeSession>;
1341
+ }
1342
+ interface UsageRecorderLike {
1343
+ record(ctx: UsageContext, msg: OmniMessage): Promise<void>;
1344
+ }
1345
+ interface SessionManagerDeps {
1346
+ sessions: SessionIndex;
1347
+ channels: ChannelHub;
1348
+ loader: SessionLoader;
1349
+ /** Session-origin registry (session_meta is the single source of truth; subagent registration records the forwarded meta's source here). */
1350
+ sources: SessionOrigins;
1351
+ recorder: UsageRecorderLike;
1352
+ /** Automatic Session title generation (optional: not injected in tests or when disabled). */
1353
+ titles?: TitleNotifier;
1354
+ /** Error persistence (optional: without it, only logs — same as before this was wired up). */
1355
+ errors?: ErrorSink;
1356
+ /** Provider auth refresh (optional for tests that do not exercise external model credentials). */
1357
+ modelScopeAuth?: ModelScopeAuth;
1358
+ log?: (line: string) => void;
1359
+ /** Goal run-state persistence (optional like `titles`: without it, goals run but leave no restorable record). */
1360
+ /**
1361
+ * Publishes a Session-scoped event on the user-level channel of everyone who can see the
1362
+ * Project. The audience lookup (owner + members) and the `user:<id>` channel binding stay in
1363
+ * the app layer, the same split the scheduler's `notify` uses — this class holds no Project
1364
+ * membership repos. Optional: without it only the per-Session channel is served, which is
1365
+ * what unit tests that don't wire it get.
1366
+ */
1367
+ notifyProjectUsers?: (projectId: string, event: ServerEvent) => void;
1368
+ /**
1369
+ * Clock for persisted timestamps (last_active_at). Injected like the other services' so a
1370
+ * stubbed clock moves this and `usage_records.ts` together — the pairing the legacy
1371
+ * backfill assumes when it reads MAX(ts) as a session's last activity.
1372
+ */
1373
+ now?: () => Date;
1374
+ /**
1375
+ * Called when a Task starts (or is queued) from a PERSON's input — a message typed in the
1376
+ * Web App, the CLI or a bound messaging bot, as opposed to one the harness injected
1377
+ * (`sender: "server"` / `"harness"`). Company mode uses it to reset the desk's @-chain
1378
+ * accounting: a person's message opens a fresh chain (hop 0), whatever mention last woke
1379
+ * the desk. Optional; unit tests that do not wire it get nothing.
1380
+ */
1381
+ onHumanInput?: (sessionId: string) => void;
1382
+ }
1383
+ /**
1384
+ * A queued message's original content, held while it waits in a queue so a recall (DELETE
1385
+ * /steer/:id, DELETE /follow-ups/:id) can hand it back to the composer for editing (#287).
1386
+ * `text` and `images` are the same strings the queued input already references (no extra
1387
+ * copy); files stay on disk in the Session scratchpad and are read back only at recall.
1388
+ */
1389
+ interface RecallStore {
1390
+ text: string;
1391
+ images: string[];
1392
+ files: RecallableFile[];
1393
+ }
1394
+ declare class SessionManager {
1395
+ private readonly deps;
1396
+ private readonly entries;
1397
+ /**
1398
+ * Each registered subagent Session's ROOT Session: a subagent runs under its root's
1399
+ * sandbox policy, as it runs under its root's approval mode, so a change the person makes
1400
+ * in the composer reaches the children already running.
1401
+ */
1402
+ private readonly childRoots;
1403
+ /** The Session whose policy governs `sessionId`: its root for a subagent, else itself. */
1404
+ rootSessionOf(sessionId: string): string;
1405
+ /** Per-Session mutex (serializes get-or-load and status flips); auto-cleaned once the chain drains. */
1406
+ private readonly locks;
1407
+ private readonly log;
1408
+ /** Graceful-shutdown flag: once set, new Tasks/compactions are rejected (503). */
1409
+ private closed;
1410
+ /** Agents currently being deleted (key = agentKey): new Tasks/compactions are always rejected with 409 during this window. */
1411
+ private readonly deletingAgents;
1412
+ /** Sessions currently being deleted (guards against the entry/Trace file being rebuilt and reviving it inside the deletion race window). */
1413
+ private readonly deletingSessions;
1414
+ /** Per-Agent config generation (key = agentKey), bumped by invalidateAgentRuntimes when a Project's credentials change. */
1415
+ private readonly agentGenerations;
1416
+ /** Open streaming fragments of running sessions (fed by drive, served to GET /messages; see live-tail.ts). */
1417
+ private readonly liveTail;
1418
+ private readonly sweepTimer;
1419
+ /** Clock for persisted timestamps (see SessionManagerDeps.now); wall clock unless injected. */
1420
+ private readonly now;
1421
+ constructor(deps: SessionManagerDeps);
1422
+ statusOf(sessionId: string): SessionStatus;
1423
+ pendingApprovalCount(sessionId: string): number;
1424
+ pendingApprovals(sessionId: string): PendingApproval[];
1425
+ /** Number of queued follow-up tasks (`queueIfBusy`) awaiting auto-start. */
1426
+ pendingFollowUpCount(sessionId: string): number;
1427
+ /**
1428
+ * Background-task counts of a LOADED session, read live (see SessionInfo.backgroundTasks);
1429
+ * undefined when the session is not in the active table or nothing is running — the list
1430
+ * row and the single GET omit the field in both cases.
1431
+ */
1432
+ backgroundTasksOf(sessionId: string): SessionBackgroundTasks | undefined;
1433
+ /** Steering messages queued but not yet delivered to the model (display mirror; see RuntimeEntry.pendingSteering). */
1434
+ pendingSteeringOf(sessionId: string): PendingSteeringInfo[];
1435
+ /** Steering a finished run never delivered, waiting for the composer to take it back (see RuntimeEntry.returnedSteering). */
1436
+ returnedSteeringOf(sessionId: string): PendingSteeringInfo[];
1437
+ /**
1438
+ * Live subagent children of an ACTIVE runtime entry (empty when the session is not loaded
1439
+ * — after a restart there is no in-process child left to report, so empty is the truth).
1440
+ * Rides `task_state` events and the SSE subscribe snapshot; the panel renders child
1441
+ * running marks from this instead of parsing tool-output text.
1442
+ */
1443
+ subagentsOf(sessionId: string): BackgroundSubagentInfo[];
1444
+ /**
1445
+ * Host message to one child session — a user input on the child, whatever its state:
1446
+ * steering while it runs, a follow-up run while it is idle, a revival (resume-session
1447
+ * semantics) when it is no longer live (core `Session.sendToBackgroundSubagent`). The
1448
+ * parent runtime itself is loaded on demand (ensureEntry — the same get-or-resume path a
1449
+ * task uses), so messaging a child of a long-idle conversation works after a restart too;
1450
+ * the resume fallback names the child's owning Agent from its own session row, which must
1451
+ * belong to the parent's project.
1452
+ */
1453
+ sendToSubagent(sessionId: string, childSessionId: string, messages: OmniMessage[]): Promise<SubagentMessageOutcome>;
1454
+ /**
1455
+ * Sets a Session's thinking level on its loaded runtime (core `Session.thinkingLevel`,
1456
+ * plain state): the row's value is what the loader applies at load, so this only needs to
1457
+ * reach a runtime that is already in the active table — or a live child session, which
1458
+ * has an index row and a panel picker of its own but runs inside its parent's runtime.
1459
+ * The level is soft-limited: it applies from the Session's very next LLM request
1460
+ * (mid-context — the UI advises compacting first, since the change invalidates the
1461
+ * provider's cached context). No-op when nothing is loaded.
1462
+ */
1463
+ setThinkingLevel(sessionId: string, level: ThinkingLevelName): void;
1464
+ /** Host abort of one child session's current run (core `Session.abortBackgroundSubagentRun`); false when the parent runtime is not loaded, the child is unknown, or it is idle. */
1465
+ abortSubagentRun(sessionId: string, childSessionId: string): boolean;
1466
+ /** Queued follow-up tasks awaiting auto-start, as their display/recall info (id + content summary). */
1467
+ pendingFollowUpsOf(sessionId: string): PendingFollowUpInfo[];
1468
+ /**
1469
+ * Live tail of a running session: one synthetic `partial_* start` OmniMessage per open
1470
+ * streaming fragment, carrying the full accumulated content so far (see live-tail.ts).
1471
+ * Empty when idle or when nothing is streaming. GET /messages attaches this (with a
1472
+ * channel cursor) so a client joining mid-stream can render the in-progress message.
1473
+ */
1474
+ liveFragments(sessionId: string): OmniMessage[];
1475
+ /**
1476
+ * The running Task's input messages as published at launch; empty when idle. The engine
1477
+ * writes these exact envelopes to the Trace only after the first run's bootstrap (MCP
1478
+ * connect + discovery), so GET /messages appends whichever of them the Trace read has
1479
+ * not caught up to yet — without this, a client rebuilding history during the connect
1480
+ * (the draft flow subscribes only after the input publish) loses the user's own message.
1481
+ */
1482
+ pendingInputs(sessionId: string): OmniMessage[];
1483
+ /** The running Task's streamed bootstrap records (see RuntimeEntry.pendingBootstrap); empty when idle. */
1484
+ pendingBootstrap(sessionId: string): OmniMessage[];
1485
+ /** Number of Sessions for this Agent that are currently running / compacting. */
1486
+ activeCountForAgent(projectId: string, agentId: string): number;
1487
+ /** Add a newly created Session to the active table (status idle), avoiding a redundant load on the next Task. */
1488
+ adopt(row: SessionRow, session: RuntimeSession): void;
1489
+ /**
1490
+ * Subscribes the background hooks on a runtime Session entering the active table — every
1491
+ * insertion path must call it (ensureEntry's loads, adopt's fresh creations):
1492
+ * - the idle-arrival signal for completion notices (mid-run arrivals are delivered inside
1493
+ * the run by core; this signal is the only trigger left when the session sits idle);
1494
+ * - live-forwarded background-subagent messages, published to the session channel (the
1495
+ * same feed SSE relays) and recorded for usage — a background child streams to the
1496
+ * frontend in real time past the launching turn's end, until its terminal state;
1497
+ * - subagent run-state changes, republished as `task_state` so the panel's running
1498
+ * marks track child rounds structurally instead of parsing tool-output text;
1499
+ * - background-task state changes (a command promoted or exited, a subagent promoted,
1500
+ * settling or released), re-counted and published as `session_background` on the user
1501
+ * channel so every Session list shows which rows still own background work.
1502
+ */
1503
+ private registerNoticeListener;
1504
+ /**
1505
+ * The entry-lifetime approval callback: the registry, the per-decision approval-mode
1506
+ * re-read, and the SSE escalation are all entry-scoped, so one instance serves every run
1507
+ * of the entry and the children's session-lifetime fallback sink alike.
1508
+ *
1509
+ * This is also where the one cross-cutting approval override lives — wrapping the callback
1510
+ * handed to `Session.run` rather than teaching the engine anything: an organization's
1511
+ * sessions run with nobody watching, so a call their approval mode would hand to a person
1512
+ * is denied here instead of waiting for an answer that is never coming (see `unattended`
1513
+ * in approvals.ts). The marker is the row's durable `client = "org"` stamp — desk, ticket
1514
+ * and the sub-sessions that inherit it — read per decision, so a row the reconcile pass
1515
+ * stamps later is covered from its next decision on.
1516
+ */
1517
+ private entryApprove;
1518
+ /** Publishes one live background-subagent message and records its usage (fire-and-forget; the child's own Trace is the durable record). */
1519
+ private forwardBackgroundMessage;
1520
+ /**
1521
+ * Bumps one Agent's config generation so every runtime built before it is discarded on
1522
+ * its next idle access and re-resumed via the loader (history is preserved through the
1523
+ * Trace). What warrants it is a Session-fixed fact changing under the runtime — the
1524
+ * Project's credentials (see invalidateProjectRuntimes). Agent State changes never do:
1525
+ * core reads them into the next model context itself. A Task already in flight is neither
1526
+ * aborted nor hot-swapped: it keeps the values it started with, and its entry is rebuilt
1527
+ * on the first access after it returns to idle (see ensureEntry).
1528
+ */
1529
+ invalidateAgentRuntimes(projectId: string, agentId: string): void;
1530
+ /**
1531
+ * After a Project's models/credentials change: invalidate every cached runtime in this
1532
+ * Project, so the next Task re-resumes with the new api_key / base_url. Same
1533
+ * effective-value semantics as invalidateAgentRuntimes — no hot swap into a Task already
1534
+ * in flight. Iterating the active table is complete here, not a shortcut: the generation
1535
+ * map only matters for runtimes that are already cached, and an Agent with no cached
1536
+ * entry builds fresh through the loader anyway.
1537
+ */
1538
+ invalidateProjectRuntimes(projectId: string): void;
1539
+ /**
1540
+ * Cheap, lock-free rehearsal of the 409/503 conditions startTask checks, throwing exactly the
1541
+ * same HttpErrors. **Advisory only**: it neither takes the Session lock nor loads an entry, so
1542
+ * a session that isn't in the active table reads as acceptable and a status change racing this
1543
+ * call is not caught — the authoritative check is still the one inside startTask.
1544
+ *
1545
+ * It exists so a caller that has irreversible work to do first (POST /tasks writes the
1546
+ * message's file attachments to disk) can find out about the ordinary "a Task is already
1547
+ * running" rejection before doing it, instead of undoing it afterwards.
1548
+ */
1549
+ assertCanAcceptTask(sessionId: string, opts?: {
1550
+ queueIfBusy?: boolean;
1551
+ }): void;
1552
+ /**
1553
+ * Run a metadata operation at an idle Session boundary under the same mutex as Task and
1554
+ * compaction starts. Session fork uses this to snapshot an append-only Trace without a Task
1555
+ * beginning between its status check and final read. An uncached Session is idle by definition;
1556
+ * unlike startTask this path deliberately does not load a heavyweight runtime.
1557
+ */
1558
+ atIdleBoundary<T>(sessionId: string, operation: () => Promise<T>): Promise<T>;
1559
+ /**
1560
+ * Start a Task: get-or-load → 409
1561
+ * mutual-exclusion check → publish the input messages first → drive run in the
1562
+ * background. Returns the current actual session_id (the new id after self-heal).
1563
+ * With `queueIfBusy`, a busy session (running/compacting) enqueues the input as a
1564
+ * follow-up instead of 409: it auto-starts as an ordinary next task once the session
1565
+ * returns to idle (`queued: true` in the result; see startQueuedFollowUp).
1566
+ * `opts.recall` is that queued message's original
1567
+ * content — optional, because only the HTTP route knows the parts of it the input does
1568
+ * not carry (the pre-attachment text and where each file landed on disk); every other
1569
+ * caller's store is read off the input, so a queued follow-up is recallable and shows
1570
+ * its content whichever path queued it.
1571
+ */
1572
+ startTask(sessionId: string, input: OmniMessage[], opts?: {
1573
+ queueIfBusy?: boolean;
1574
+ recall?: RecallStore;
1575
+ }): Promise<{
1576
+ sessionId: string;
1577
+ queued: boolean;
1578
+ }>;
1579
+ /**
1580
+ * Start a goal run: like startTask, but the goal plugin composes the input and its stop
1581
+ * hook drives every later round inside this one `session.run` call — so the Session
1582
+ * stays `running` for the whole goal (every round), the existing abort endpoint
1583
+ * interrupts the entire loop and schedules queue behind it as usual. The plugin's
1584
+ * `user_prompt` hook (hooks run in core and nowhere else — `Session.runUserPromptHook`)
1585
+ * writes the Session's goal file and answers the round-1 protocol message; it runs under
1586
+ * the session lock AFTER the idle check, since it overwrites the goal file a running goal
1587
+ * is reading. Not installed (null) is the route's 409; an empty answer a 500. Round inputs
1588
+ * are yielded by core and published like any streamed message; progress additionally goes
1589
+ * out as goal_* server events (the run state itself is the goal file the plugin maintains).
1590
+ */
1591
+ startGoal(sessionId: string, args: {
1592
+ /** The user's own message(s) exactly as submitted — text and images, no wrapping. */
1593
+ messages: OmniMessage[];
1594
+ /** The user's own objective text (leading marker blocks stripped): what the plugin records, the goal_started event and the title material. */
1595
+ objective: string;
1596
+ budget: number;
1597
+ }): Promise<{
1598
+ sessionId: string;
1599
+ }>;
1600
+ /**
1601
+ * Taps a goal run's stream for `drive`: round boundaries become goal_round events —
1602
+ * round 1 from the seeded input (core never yields a run's initial input, so it is
1603
+ * counted here, where startGoal published it), later rounds from the stop-hook continues
1604
+ * core yields, each announced by the hook's `continue` event; a harness-stamped input
1605
+ * nothing announced is another package's user_prompt context riding behind the
1606
+ * objective, not a round — and the goal hook's `stop` event becomes the
1607
+ * goal_finished server event. `used` is what the hook last recorded in its event's
1608
+ * `output` — the same number its budget check used — so the UI never shows a different
1609
+ * figure. A stream that ends without the hook's terminal event (a cut-off run, an
1610
+ * infrastructure failure) closes as `aborted`.
1611
+ */
1612
+ private goalStream;
1613
+ /** Closes a goal as aborted for the UI (stream cut off / infrastructure failure). */
1614
+ private finishAborted;
1615
+ /** Shared task launch (fresh tasks and auto-started follow-ups): flips to running, publishes the input, and drives the run. Caller holds the session lock and has verified idle. */
1616
+ private launchTask;
1617
+ /**
1618
+ * Auto-start of queued follow-ups: called from drive's finally whenever a Task or
1619
+ * compaction finishes (abort included — follow-ups are future tasks the user queued, so
1620
+ * an abort of the current run does not discard them). Re-validates everything under the
1621
+ * session lock (another task may have snuck in, the server may be shutting down, the
1622
+ * session may be getting deleted) and launches exactly one queued input — the next
1623
+ * finish picks up the one after, keeping order and one-at-a-time semantics.
1624
+ */
1625
+ private startQueuedFollowUp;
1626
+ /** Manually compact the context: 409 if already running; compaction output also flows into the SSE channel. */
1627
+ startCompact(sessionId: string): Promise<{
1628
+ sessionId: string;
1629
+ }>;
1630
+ /**
1631
+ * Switches the Session's model in place (`POST /switch-model`) — core `Session.switchModel`,
1632
+ * gated like a compaction (open, not deleting, idle). Core decides everything before its
1633
+ * first event, so the head of its stream is pulled here, under the lock, and tells the three
1634
+ * outcomes apart:
1635
+ *
1636
+ * - it throws — a refusal, answered 409 with the code of its reason (see `switchRefusal`);
1637
+ * - it is already done — a Session that never ran has no context to close: core re-assembled
1638
+ * its first context on the target, nothing streams, and the row carries the new pair by
1639
+ * the time this returns (`switched: true`; the route answers with the fresh DTO);
1640
+ * - it yields — a driven run like a compaction (`switched: false`): status `compacting`,
1641
+ * idle when it ends. The head is a compaction's begin, the first record of a bootstrap (a
1642
+ * Session loaded from its Trace opens its context first), or — for a context with no
1643
+ * completed turn, which streams no pair — the opener's first record.
1644
+ *
1645
+ * The switch is the entry's run from that pull on — it can be stopped, and a deletion or a
1646
+ * shutdown waits for it — while the entry only reads `compacting` once there is something
1647
+ * to stream, so a refusal never flickers a state.
1648
+ *
1649
+ * The entry and the row follow the runtime as the new context's `session_meta` passes
1650
+ * through the drive (see `drive`); a switch that does not complete streams no meta and
1651
+ * leaves them where they were. Usage: a drive bills to the model it started on, so the
1652
+ * switch's compaction request goes to the model that served it and the next run to the new
1653
+ * one.
1654
+ */
1655
+ startSwitch(sessionId: string, ref: ModelRefDto): Promise<{
1656
+ sessionId: string;
1657
+ switched: boolean;
1658
+ }>;
1659
+ /** Who is told when a Session's model moves (see `onModelChanged`). */
1660
+ private readonly modelListeners;
1661
+ /**
1662
+ * Subscribes to a Session's model moving: an in-session switch completing, or a row brought
1663
+ * back in step with its Trace when the Session loads. The listener runs once the entry and
1664
+ * the row carry the new pair. Returns the unsubscribe. Company mode listens: an employee's
1665
+ * model is its desk Session's.
1666
+ */
1667
+ onModelChanged(listener: (sessionId: string, model: ModelRefDto) => void): () => void;
1668
+ /**
1669
+ * Moves the entry — and the row it caches into — to the model the runtime reports itself on,
1670
+ * when that differs: the pair the next drive attributes usage to, and the one
1671
+ * `GET /sessions/:id` describes. A runtime that reports none (test fakes) leaves the entry
1672
+ * alone. The row write is guarded like the other bookkeeping writes (see `touchRow`): a
1673
+ * switch must not be stranded mid-drive by a closed DB handle, and the entry is right either
1674
+ * way. The same holds for whoever listens: a listener that throws is recorded, not rethrown.
1675
+ */
1676
+ private syncEntryModel;
1677
+ /** Submit an approval decision; returns false if the pending approval doesn't exist (already decided/unknown). */
1678
+ decideApproval(sessionId: string, toolCallId: string, decision: "allow" | "deny"): boolean;
1679
+ /**
1680
+ * Mid-run steering: forward the message to the running Session (core delivers it between
1681
+ * turns as a standalone `[user_steering]` user message followed by its images — no SSE
1682
+ * event of its own; the messages arrive through the stream the drive loop already
1683
+ * publishes). 409 when the Session isn't running a Task (idle / compacting / not loaded)
1684
+ * or the run finished in the race window — the caller falls back to submitting a normal
1685
+ * task, which carries the same text and images.
1686
+ *
1687
+ * `recall` is the queued message's original content: its summary (with a fresh id) is
1688
+ * mirrored on the entry and broadcast via `task_state` (and the SSE subscribe snapshot)
1689
+ * until the delivered `[user_steering]` message is observed on the stream — that is what
1690
+ * keeps the composer's "steering queued" hint, content included, alive across reloads —
1691
+ * and the content itself is what a recall (recallSteering) hands back to the composer.
1692
+ */
1693
+ steer(sessionId: string, input: OmniMessage[], recall: RecallStore): void;
1694
+ /**
1695
+ * Recall an undelivered steering message (#287): withdraw it from core's queue and the
1696
+ * display mirror, re-broadcast state, and hand back its original content for the composer
1697
+ * to restore. 409 `not_pending` when the entry is no longer queued — already delivered to
1698
+ * the model (core's `unsteer` says so authoritatively; the mirror can lag behind delivery
1699
+ * until the `[user_steering]` message is observed on the stream), the run exited, or the
1700
+ * id is unknown — all the same outcome for the caller: nothing left to take back.
1701
+ */
1702
+ recallSteering(sessionId: string, steerId: string): RecallStore;
1703
+ /**
1704
+ * Recall a queued follow-up task (#287): remove it from the queue before it auto-starts,
1705
+ * re-broadcast state, and hand back its original content (plus the thinking level it was
1706
+ * queued with). Being in the queue is the whole condition — every queued entry carries a
1707
+ * recall store (see QueuedFollowUp.recall), so no queued message is ever refused. 409
1708
+ * `follow_up_started` when the id is not in the queue: it already auto-started, or the
1709
+ * runtime was released and took the queue with it. Its own code, distinct from steering's
1710
+ * `not_pending`, because the two mean different things to the user — a follow-up became a
1711
+ * task of its own, a steering message reached the model mid-run.
1712
+ */
1713
+ recallFollowUp(sessionId: string, followUpId: string): {
1714
+ recall: RecallStore;
1715
+ };
1716
+ /**
1717
+ * "Retry now" on the reconnect countdown: skip the in-progress backoff wait and fire
1718
+ * the next retry immediately (the attempt counter is unchanged — the skipped wait does
1719
+ * not consume an extra attempt). Returns false as a benign no-op when the session has
1720
+ * no active runtime or no reconnect wait is in progress — timing races (the wait
1721
+ * elapsed right before the click) must not surface as errors. Mirrors the steer seam:
1722
+ * manager → RuntimeSession → core Session → engine.
1723
+ */
1724
+ retryNow(sessionId: string): boolean;
1725
+ /**
1726
+ * Interrupt the current Task/compaction: pending approvals converge to deny first,
1727
+ * then the AbortSignal fires. Returns false if nothing is in progress (the route
1728
+ * treats this as a 204 no-op).
1729
+ */
1730
+ abortTask(sessionId: string): boolean;
1731
+ /**
1732
+ * Background command processes of a LOADED session (empty when the entry is not in
1733
+ * the active table — a resumed entry starts with a fresh environment and can only
1734
+ * ever report an empty list, so nothing is resurrected just to answer a poll).
1735
+ */
1736
+ listProcesses(sessionId: string): BackgroundCommandInfo[];
1737
+ /**
1738
+ * Refreshes the listen-port probes behind the process list's `serviceUrl` (core
1739
+ * `Session.probeBackgroundCommandServices`): called by the processes route before
1740
+ * listing, so the first fetch already carries probed URLs. Bounded by core's own probe
1741
+ * timeout and TTL; a session that isn't loaded has no processes to probe.
1742
+ */
1743
+ probeProcessServices(sessionId: string): Promise<void>;
1744
+ /**
1745
+ * Hands one EXECUTING tool call of a loaded session back as a background task (core
1746
+ * `Session.detachToolCall`): the call closes with a registry handle, its work keeps
1747
+ * running, and the turn carries on. An unloaded session has nothing executing, so it
1748
+ * answers "not_running" like an unknown id.
1749
+ */
1750
+ detachToolCall(sessionId: string, toolCallId: string): ToolDetachResult;
1751
+ /** Kills one background command process of a loaded session; false when the session isn't loaded or the id is unknown. */
1752
+ killProcess(sessionId: string, processId: string): boolean;
1753
+ /**
1754
+ * Removes one EXITED background command entry from a loaded session's process list.
1755
+ * A running process is refused ("running") — stopping is killProcess's job, and a
1756
+ * bare removal must never surprise-signal a live process group. "not_found" covers
1757
+ * an unloaded session and an unknown id alike: the entry is gone either way. The
1758
+ * removal itself reuses the kill path — core's registry removal signals the (dead)
1759
+ * group defensively and drops the row, exactly like input_command's post-exit reap.
1760
+ * The only race, an entry observed running that exits before a retry, is benign:
1761
+ * the caller gets "running", refreshes, and sees the row exited on the next look.
1762
+ *
1763
+ * Note what leaves with the row: the registry entry owns the ManagedSession holding
1764
+ * that process's captured output, so after a removal input_command on the same
1765
+ * process_id answers "unknown process_id". Removal is a deliberate discard, not just
1766
+ * a list tidy-up — the Web App says so at the button and in the docs.
1767
+ */
1768
+ removeProcess(sessionId: string, processId: string): "removed" | "running" | "not_found";
1769
+ /**
1770
+ * Disposes a just-removed entry's runtime — after its in-flight drive (if any)
1771
+ * settles, so interrupt cleanup never races a dying environment. Deleting a Session /
1772
+ * Agent / Project is the one intent that must also end the background processes the
1773
+ * conversation started (a dev server surviving its deleted conversation is
1774
+ * unreachable from every UI, running forever).
1775
+ */
1776
+ private disposeRemoved;
1777
+ /**
1778
+ * Before deleting a Project, converge all its active runs and clear them out of the
1779
+ * active table. Returns the in-flight drive Promises of the affected entries: the
1780
+ * caller (deleteProject) should await them before removing the directory, so that
1781
+ * interrupt-cleanup Trace writes don't recreate the directory after deletion.
1782
+ */
1783
+ abortProject(projectId: string): Promise<void>[];
1784
+ /**
1785
+ * Before deleting an Agent, converge all its active runs and clear them out of the
1786
+ * active table (same semantics as abortProject). Also marks this Agent as "being
1787
+ * deleted": new Tasks/compactions entering during the deletion process are always
1788
+ * rejected with 409 (assertAgentNotDeleting), closing the race window where a new
1789
+ * task recreates the directory and revives an already-deleted Agent between the
1790
+ * abortAgent snapshot and the directory removal. The caller must call
1791
+ * endAgentDeletion once deletion finishes (success or failure).
1792
+ */
1793
+ beginAgentDeletion(projectId: string, agentId: string): Promise<void>[];
1794
+ endAgentDeletion(projectId: string, agentId: string): void;
1795
+ /**
1796
+ * Before deleting a single Session, converge its active run and clear it out of the
1797
+ * active table (same semantics as beginAgentDeletion). Also marks this Session as
1798
+ * "being deleted": new Tasks/compactions entering during the deletion process are
1799
+ * always rejected with 409 (assertSessionNotDeleting), closing the race window where
1800
+ * a new task recreates the entry and Trace file, reviving an already-deleted Session
1801
+ * between the abort snapshot and the file removal. The caller must call
1802
+ * endSessionDeletion once deletion finishes (success or failure). Returns the
1803
+ * in-flight drive Promise: the caller should await it before deleting the Trace file,
1804
+ * so cleanup writes don't recreate the file.
1805
+ */
1806
+ beginSessionDeletion(sessionId: string): Promise<void>[];
1807
+ endSessionDeletion(sessionId: string): void;
1808
+ /** Graceful shutdown: reject new tasks (503), interrupt all active runs, and wait for them to finish (default ≤5s). */
1809
+ shutdown(timeoutMs?: number): Promise<void>;
1810
+ /**
1811
+ * Active-table idle eviction: removes entries that are idle (idle status, no pending
1812
+ * approvals, no in-flight drive) and have been inactive past the timeout, releasing
1813
+ * the core Session's full in-memory history. This is purely memory reclamation: the
1814
+ * next access re-resumes via the loader, so correctness is unaffected. Lock-table
1815
+ * entries are auto-cleaned by withLock once their chain drains (including leftover
1816
+ * entries under the old id after self-heal). `now` / `idleMs` are injectable for
1817
+ * tests and timers.
1818
+ */
1819
+ sweepIdle(now?: number, idleMs?: number): void;
1820
+ private assertOpen;
1821
+ /** The Agent owning this Session is being deleted → 409 (guards against directory recreation inside the deletion race window). */
1822
+ private assertAgentNotDeleting;
1823
+ /** This Session is being deleted → 409 (guards against the entry/Trace being rebuilt and reviving it inside the deletion race window). */
1824
+ private assertSessionNotDeleting;
1825
+ private assertIdle;
1826
+ private generationOf;
1827
+ private refreshProviderCredentialIfNeeded;
1828
+ /** get-or-resume-or-heal: use directly on an active-table hit; otherwise load via the loader, updating the index's primary key on self-heal. */
1829
+ private ensureEntry;
1830
+ /**
1831
+ * Auto-start of queued background completion notices: called when core signals an
1832
+ * idle-session arrival, and from drive's finally for notices that raced a run's exit.
1833
+ * Revalidates under the session lock; only an idle entry launches, and only when the
1834
+ * queue is non-empty — a busy entry skips, because the running/queued task's engine
1835
+ * drains the same queue at its own boundaries (each notice is delivered exactly once).
1836
+ */
1837
+ private startBackgroundNoticeTask;
1838
+ /**
1839
+ * Drive the output stream in the background: publish each message + persist usage +
1840
+ * persist LLM/tool errors; on completion (including errors) resets to idle and pushes
1841
+ * the status. `titleSource` is passed only for Task runs (compaction doesn't generate
1842
+ * a title): its user text drives automatic title generation at run start.
1843
+ */
1844
+ private drive;
1845
+ /**
1846
+ * Register a subagent: persisted only when the origin message is session_meta
1847
+ * (agentId is derived from the agent_state path: `<…>/<agentId>/agent_state`).
1848
+ * **The title is left blank here** — the registration site in drive generates it
1849
+ * right away from the spawning prompt (fallback first words + LLM replacement), so
1850
+ * it never waits for the subagent's own output. Idempotent (children dedup +
1851
+ * insertOrIgnore); a subagent has its own Trace, so it's visible in both the list
1852
+ * and the trace view. On successful registration, the entry is put into `children`
1853
+ * and returned; a duplicate session_meta returns null.
1854
+ */
1855
+ private registerChildSession;
1856
+ /**
1857
+ * Runs one `sessions` row write for a driven run's bookkeeping (has_trace /
1858
+ * last_active_at), swallowing failures the same way the per-message `recorder.record`
1859
+ * call does: bookkeeping must never take down a run's lifecycle. The two call sites both
1860
+ * sit outside a covering try — the run-start write precedes drive's try block, the
1861
+ * run-end one lives in its finally — and the realistic failure (the DatabaseSync handle
1862
+ * closed by shutdown while a run outlived its drain window) throws ERR_INVALID_STATE.
1863
+ */
1864
+ private touchRow;
1865
+ private publishState;
1866
+ private publishEvent;
1867
+ /**
1868
+ * Re-counts a loaded session's background tasks and, when the counts moved, publishes
1869
+ * `session_background` on the user channel (the list's mark is a user-channel affair like
1870
+ * `session_state`; the open conversation reads the same row). A change is Session
1871
+ * activity for the idle sweep: the entry stays loaded for the usual idle window after its
1872
+ * last background task ends, so the finished process list can still be read. An entry
1873
+ * that already left the table (a delete disposing its runtime) publishes nothing — its
1874
+ * row is gone as well.
1875
+ */
1876
+ private publishBackgroundTasks;
1877
+ /**
1878
+ * Publishes `session_approvals` on the user channel: how many tool calls of this Session wait
1879
+ * for a person now. The Session's own stream carries the calls; this is what lets a list, or
1880
+ * a host watching every Session of a Project, see which one waits without subscribing to each.
1881
+ * Sent for an entry that is being disposed too, so a mark it had is cleared.
1882
+ */
1883
+ private publishApprovals;
1884
+ /** Serialize (mutually exclude) execution by sessionId; cleans up the lock-table entry once its chain drains (avoids unbounded growth). */
1885
+ private withLock;
1886
+ }
1887
+ declare const Sessions_base: (abstract new () => Pick<SessionManager, "steer" | "statusOf" | "pendingApprovalCount" | "pendingApprovals" | "pendingFollowUpCount" | "pendingSteeringOf" | "subagentsOf" | "sendToSubagent" | "abortSubagentRun" | "pendingFollowUpsOf" | "liveFragments" | "pendingInputs" | "pendingBootstrap" | "activeCountForAgent" | "invalidateAgentRuntimes" | "invalidateProjectRuntimes" | "assertCanAcceptTask" | "startTask" | "startGoal" | "startCompact" | "startSwitch" | "onModelChanged" | "decideApproval" | "recallSteering" | "recallFollowUp" | "retryNow" | "abortTask" | "listProcesses" | "probeProcessServices" | "killProcess" | "removeProcess" | "abortProject" | "beginAgentDeletion" | "endAgentDeletion" | "beginSessionDeletion" | "endSessionDeletion" | "atIdleBoundary" | "shutdown" | "sweepIdle">) & (<C>(target: C, context: ClassDecoratorContext) => C);
1888
+ /** The session runtime: one entry per live Session, task mutex, approvals, streaming. */
1889
+ declare abstract class Sessions extends Sessions_base {
1890
+ }
1891
+ declare const SessionServiceIface_base: (abstract new () => Pick<SessionService, "toInfo" | "hasTrace" | "listSessions" | "sessionStats" | "createSession" | "defaultSandbox" | "pickSandbox" | "updateSandbox" | "latestTracePath" | "adoptUnmanagedTraceSessions">) & (<C>(target: C, context: ClassDecoratorContext) => C);
1892
+ declare abstract class SessionServiceIface extends SessionServiceIface_base {
1893
+ }
1894
+
1895
+ interface AgentConfigView {
1896
+ agentsMd: string;
1897
+ systemConfigYaml: string;
1898
+ config: AgentConfigDto;
1899
+ stateDir: string;
1900
+ }
1901
+ declare class AgentConfigService implements AgentConfig {
1902
+ private readonly paths;
1903
+ private get root();
1904
+ /** Whether the Agent exists (determined by the presence of system_config.yaml, matching the CLI's convention). */
1905
+ exists(projectId: string, agentId: string): Promise<boolean>;
1906
+ requireExists(projectId: string, agentId: string): Promise<void>;
1907
+ /**
1908
+ * Read list-card metadata: name / description + tool count (sum of tools.builtin
1909
+ * and tools.mcpServers entries; MCP counted per server). Silently falls back to
1910
+ * empty / 0 if the file is corrupt.
1911
+ */
1912
+ readCardMeta(projectId: string, agentId: string): Promise<{
1913
+ name?: string;
1914
+ description?: string;
1915
+ toolCount: number;
1916
+ version: number;
1917
+ kernelOutdated: boolean;
1918
+ }>;
1919
+ /** Structured config view (matching the edit form's shape) + raw text + AGENTS.md + State path. */
1920
+ getConfig(projectId: string, agentId: string): Promise<AgentConfigView>;
1921
+ /**
1922
+ * PUT accepts any subset: only the provided keys are updated (parseDocument
1923
+ * preserves comments and untouched content); agentsMd is overwritten in full.
1924
+ * Numeric validation: >0 or -1; thinkingLevel / mode are validated as enums.
1925
+ */
1926
+ updateConfig(projectId: string, agentId: string, req: AgentConfigUpdateRequest): Promise<void>;
1927
+ /**
1928
+ * Overwrites system_config.yaml with the current code defaults (core's
1929
+ * resetSystemConfigToDefaults) — same semantics as updating an installed skill:
1930
+ * only the identity fields (name / description / version) survive; the system
1931
+ * prompt, max_turns, model/compaction settings and the tool list (incl. MCP
1932
+ * servers) become the current defaults. AGENTS.md, skills and the vault are
1933
+ * untouched.
1934
+ */
1935
+ resetConfig(projectId: string, agentId: string): Promise<void>;
1936
+ /**
1937
+ * Smart-merges the config up to the current defaults generation (core's applyKernelUpdate):
1938
+ * a settings tab still carrying a recorded generation's default follows the new defaults,
1939
+ * a tab the user changed is kept whole and reported; the config is stamped with the new
1940
+ * kernel version.
1941
+ * The destructive full-refresh alternative stays resetConfig.
1942
+ */
1943
+ kernelUpdate(projectId: string, agentId: string): Promise<AgentKernelUpdateResponse>;
1944
+ private applyConfigUpdate;
1945
+ /**
1946
+ * Inserts a feature's section placeholder into the Agent's prompt template — the explicit
1947
+ * adoption path mirroring the Memory tab's endpoint; nothing ever inserts automatically.
1948
+ * For skills/vault the insert is migration-first (core's helpers): a stored template still
1949
+ * carrying the legacy hardcoded section verbatim gets it replaced in place by the
1950
+ * placeholder, otherwise the placeholder is inserted before `# Environment`. Idempotent;
1951
+ * returns the refreshed config view (the route picks out the feature's DTO).
1952
+ */
1953
+ insertTemplatePlaceholder(projectId: string, agentId: string, feature: "vault" | "skills" | "schedules"): Promise<AgentConfigView>;
1954
+ /** Read the Agent vault (agent_state/.vault.toml): values are always masked, plaintext is never sent to the client. */
1955
+ getVault(projectId: string, agentId: string): Promise<VaultResponse>;
1956
+ /**
1957
+ * PUT replaces the whole vault table (same semantics as models): keys absent from
1958
+ * the body are deleted; omitting value keeps the existing value (a new key must
1959
+ * provide a value). Key names are validated against shell environment variable
1960
+ * naming rules (same rule as core); deleting everything removes the whole
1961
+ * .vault.toml file.
1962
+ */
1963
+ updateVault(projectId: string, agentId: string, req: VaultUpdateRequest): Promise<VaultResponse>;
1964
+ }
1965
+
1966
+ /** AgentConfig: the mechanism AgentConfigService implements. */
1967
+ declare abstract class AgentConfig {
1968
+ abstract exists(projectId: string, agentId: string): Promise<boolean>;
1969
+ abstract requireExists(projectId: string, agentId: string): Promise<void>;
1970
+ abstract readCardMeta(projectId: string, agentId: string): Promise<{
1971
+ name?: string;
1972
+ description?: string;
1973
+ toolCount: number;
1974
+ version: number;
1975
+ kernelOutdated: boolean;
1976
+ }>;
1977
+ abstract getConfig(projectId: string, agentId: string): Promise<AgentConfigView>;
1978
+ abstract updateConfig(projectId: string, agentId: string, req: AgentConfigUpdateRequest): Promise<void>;
1979
+ abstract resetConfig(projectId: string, agentId: string): Promise<void>;
1980
+ abstract kernelUpdate(projectId: string, agentId: string): Promise<AgentKernelUpdateResponse>;
1981
+ abstract insertTemplatePlaceholder(projectId: string, agentId: string, feature: "vault" | "skills" | "schedules"): Promise<AgentConfigView>;
1982
+ abstract getVault(projectId: string, agentId: string): Promise<VaultResponse>;
1983
+ abstract updateVault(projectId: string, agentId: string, req: VaultUpdateRequest): Promise<VaultResponse>;
1984
+ }
1985
+ /** AgentLifecycle: the mechanism AgentService implements. */
1986
+ declare abstract class AgentLifecycle {
1987
+ abstract listAgents(projectId: string): Promise<AgentListItem[]>;
1988
+ /** Every id `createAgent` refuses as taken, as names only, including a folder that does not list. */
1989
+ abstract takenAgentIds(projectId: string): Promise<string[]>;
1990
+ abstract deleteAgent(projectId: string, agentId: string): Promise<void>;
1991
+ abstract createAgent(projectId: string, agentId: string, name?: string, description?: string, skillNames?: readonly string[], directory?: {
1992
+ path: string;
1993
+ names: readonly string[];
1994
+ }, archive?: Buffer<ArrayBufferLike>): Promise<AgentListItem>;
1995
+ abstract pluginVersion(projectId: string, agentId: string, pluginName: string): Promise<{
1996
+ installed: string | null;
1997
+ library: string | null;
1998
+ }>;
1999
+ abstract updatePlugin(projectId: string, agentId: string, pluginName: string): Promise<void>;
2000
+ }
2001
+
2002
+ interface AgentListItem {
2003
+ agentId: string;
2004
+ name?: string;
2005
+ description?: string;
2006
+ createdAt?: string;
2007
+ /** Last config modification time: the later of system_config.yaml / AGENTS.md mtime. */
2008
+ updatedAt?: string;
2009
+ /** Tool count: number of tools.builtin + tools.mcpServers entries (MCP counted per server). */
2010
+ toolCount: number;
2011
+ /** Agent State version number (missing field treated as 1). */
2012
+ version: number;
2013
+ /** Whether the config's kernel stamp is behind the current defaults generation (missing stamp = outdated) — the list card's update hint. */
2014
+ kernelOutdated: boolean;
2015
+ /** Number of vault keys. */
2016
+ vaultKeyCount: number;
2017
+ /** Number of scheduled tasks (count of .toml files under schedule/, including invalid ones). */
2018
+ scheduleCount: number;
2019
+ /** Number of installed Skills (count of skills/<name>/ directories that contain a SKILL.md). */
2020
+ skillCount: number;
2021
+ /** Number of installed hook packages (hooks/<name>/ directories with a hooks.json). */
2022
+ hookCount: number;
2023
+ /** Installed skills and hook packages the library has a newer version of, by plugin, each carrying that library version. */
2024
+ pluginUpdates: PluginUpdateRef[];
2025
+ /** Number of memory topic files across every scope directory under memory/ (independent of the memory switch, like skillCount). */
2026
+ memoryCount: number;
2027
+ }
2028
+ declare class AgentService implements AgentLifecycle {
2029
+ private readonly paths;
2030
+ private get root();
2031
+ private readonly agents;
2032
+ private readonly agentConfig;
2033
+ private readonly snapshots;
2034
+ /** Union of DB index ∪ directory scan; unmanaged directory Agents are backfilled into the DB. */
2035
+ listAgents(projectId: string): Promise<AgentListItem[]>;
2036
+ /**
2037
+ * Every id `createAgent` refuses as taken, as names only: the index rows and every entry under
2038
+ * `agents/`. Unlike `listAgents`, this includes a folder with no `system_config.yaml`, and it
2039
+ * opens no Agent.
2040
+ */
2041
+ takenAgentIds(projectId: string): Promise<string[]>;
2042
+ /** Number of vault keys (falls back to 0 on read failure). */
2043
+ private vaultKeyCount;
2044
+ /** Number of scheduled tasks: count of .toml files under schedule/ (0 if the directory doesn't exist). */
2045
+ private scheduleCount;
2046
+ /**
2047
+ * One pass over `skills/` and one over `hooks/`, answering the card's questions: how many
2048
+ * skills and hook packages are installed, and which of them the plugin library has moved
2049
+ * past — by plugin, since a plugin is what gets reinstalled to catch up.
2050
+ *
2051
+ * Only the HEAD of each SKILL.md is read, and that bound is load-bearing rather than a
2052
+ * micro-optimization: a preinstalled library is ~180 KB of SKILL.md across seventeen files
2053
+ * per Agent, so a Project of ten Agents would move ~2 MB through every `GET /agents` — and
2054
+ * the plugin page reloads that list after each update. The frontmatter block is at the head
2055
+ * by definition (the parser's regex is anchored there) and the largest one in the library is
2056
+ * under 1 KB, so {@link SKILL_HEAD_BYTES} covers it many times over; a block that somehow
2057
+ * overran it would be truncated, fail to parse, and read as an empty version — the same
2058
+ * fallback an unparseable file already takes, and one that sorts before every real version.
2059
+ *
2060
+ * The directory name is the identity (core's listings say so, and install/uninstall address
2061
+ * by it), so that — not a frontmatter `name` — is what the library is looked up by: a skill
2062
+ * by the plugin that ships it, a hook package by the plugin of the same name. A skill the
2063
+ * library does not carry (installed from a zip or a picked directory) has no library version
2064
+ * to be behind and is never an update. An unreadable skill degrades to "not installed" / "no
2065
+ * update": a card hint must not be the thing that makes the Agent list fail. The library is
2066
+ * the caller's, read once per listing rather than once per installed skill.
2067
+ */
2068
+ private installedPlugins;
2069
+ /**
2070
+ * Where one library plugin stands on one Agent: `installed` is the OLDEST version among the
2071
+ * plugin's installed skills and its installed hook package — the same "any component behind
2072
+ * the library is an update" rule {@link installedPlugins} draws the list cards' hints from —
2073
+ * and null when the Agent carries none of them; `library` is the version the library offers
2074
+ * now, null when the library has no such plugin. Only the head of each SKILL.md is read
2075
+ * (see SKILL_HEAD_BYTES), so the answer costs one small read per skill the plugin ships.
2076
+ *
2077
+ * The organization runtime asks this per employee per pass to keep company plugins current.
2078
+ * A skill the Agent does not carry — never installed, or removed by hand — counts for
2079
+ * nothing: what is compared is the oldest version the Agent actually has, so a plugin whose
2080
+ * skills are all current reads as current even where one of them is missing. Restoring a
2081
+ * deleted skill is the Agents page's business, not a pass's.
2082
+ */
2083
+ pluginVersion(projectId: string, agentId: string, pluginName: string): Promise<{
2084
+ installed: string | null;
2085
+ library: string | null;
2086
+ }>;
2087
+ /**
2088
+ * Reinstall one library plugin over an Agent's copy: the same whole-plugin update the
2089
+ * Agents page performs, through the same `installPlugin` writer, which overwrites every
2090
+ * skill the plugin ships and its hook package with library content. Throws 404
2091
+ * `unknown_plugin` when the library does not carry the name. Callers that may hold a cached
2092
+ * runtime for the Agent invalidate it afterwards — hook packages are bound when a core
2093
+ * Session is built, unlike skills, which are read from disk on demand.
2094
+ */
2095
+ updatePlugin(projectId: string, agentId: string, pluginName: string): Promise<void>;
2096
+ /** Number of memory topic files: regular `*.md` files (minus each scope's MEMORY.md index) summed over the scope directories under memory/ (0 if the directory doesn't exist). */
2097
+ private memoryCount;
2098
+ /** Last config modification time: the later of system_config.yaml and AGENTS.md mtime; omitted if neither is readable. */
2099
+ private configUpdatedAt;
2100
+ /**
2101
+ * Delete an Agent: the sole built-in Agent
2102
+ * default_agent (shared with the CLI, the default conversation Agent) cannot be
2103
+ * deleted; callers must first drain any active run via manager.abortAgent.
2104
+ * The directory is deleted recursively (including Trace), and the DB's
2105
+ * agents/sessions index rows are removed along with it; usage records are kept
2106
+ * (historical stats are unaffected).
2107
+ */
2108
+ deleteAgent(projectId: string, agentId: string): Promise<void>;
2109
+ /**
2110
+ * Create an Agent: the id is chosen by the creator (a semantic id, checked for
2111
+ * duplicates against both the DB and the directory within the Project — a 409
2112
+ * if taken, which naturally also blocks built-in Agent ids) → initialize State →
2113
+ * write name/description (name defaults to the id) → install the seed plugins.
2114
+ *
2115
+ * `pluginNames` are library plugin names picked at creation; they go through the same
2116
+ * `installPlugin` writer the plugin routes use, inside the same cleanup window as the rest
2117
+ * of initialization, so an Agent never survives with only part of its picked set. They are
2118
+ * resolved before anything is created, so an unknown name creates no directory at all.
2119
+ *
2120
+ * `archive` is an exported Agent State snapshot package: the fresh Agent is seeded from
2121
+ * it instead of the default template (explicit name/description override the package's
2122
+ * values, absent ones keep them — no id fallback). Mutually exclusive with seeding: the
2123
+ * package carries its own skills and hooks. An invalid package fails the whole creation
2124
+ * inside the same cleanup window, leaving no empty Agent behind.
2125
+ */
2126
+ createAgent(projectId: string, agentId: string, name?: string, description?: string, pluginNames?: readonly string[], directory?: {
2127
+ path: string;
2128
+ names: readonly string[];
2129
+ }, archive?: Buffer): Promise<AgentListItem>;
2130
+ }
2131
+
2132
+ /** MessagingBindings: the mechanism MessagingBindingsRepo implements. */
2133
+ declare abstract class MessagingBindings {
2134
+ abstract find(sessionId: string, channel: string): MessagingBindingRow | null;
2135
+ abstract findEnabled(sessionId: string): MessagingBindingRow | null;
2136
+ abstract listForSession(sessionId: string): MessagingBindingRow[];
2137
+ abstract findEnabledByAccount(channel: string, accountId: string): MessagingBindingRow | null;
2138
+ abstract listAll(): MessagingBindingRow[];
2139
+ abstract upsert(args: {
2140
+ sessionId: string;
2141
+ channel: string;
2142
+ accountId: string;
2143
+ config: Record<string, unknown>;
2144
+ linePerMessage?: boolean;
2145
+ finalReplyOnly?: boolean;
2146
+ renderMarkdown?: boolean;
2147
+ }): MessagingBindingRow;
2148
+ abstract setEnabled(sessionId: string, channel: string, enabled: boolean): void;
2149
+ abstract recordChat(sessionId: string, channel: string, chatId: string, isDirect: boolean): void;
2150
+ abstract recordInboundWatermark(sessionId: string, channel: string, messageId: string | null): void;
2151
+ abstract delete(sessionId: string, channel: string): void;
2152
+ abstract deleteSession(sessionId: string): void;
2153
+ }
2154
+
2155
+ interface MessagingBindingRow {
2156
+ sessionId: string;
2157
+ /** Messaging channel discriminator (`feishu` | `telegram` | `qq` | `wechat`). */
2158
+ channel: string;
2159
+ /** Channel-scoped bot/app identity (feishu: the app_id; telegram: the bot token's numeric id; qq: the App ID; wechat: the scanned bot id); never secret. */
2160
+ accountId: string;
2161
+ /** Channel-specific config document (feishu: appId/appSecret/baseDomain; telegram: botToken; qq: appId/appSecret; wechat: botId/botToken/baseUrl/userId). */
2162
+ config: Record<string, unknown>;
2163
+ /**
2164
+ * INTENT state: whether the binding should hold a live connection (the connection's
2165
+ * runtime status stays in memory). Saving credentials never flips it — only the
2166
+ * explicit state toggle does — and new bindings start disabled. Flipping it on is what
2167
+ * binds the account to this Session: at most one of a Session's rows is enabled, and at
2168
+ * most one row per `(channel, accountId)` is enabled across all Sessions (both
2169
+ * route-enforced).
2170
+ */
2171
+ enabled: boolean;
2172
+ /**
2173
+ * DELIVERY preference: send each non-blank line of a relayed assistant reply as its own
2174
+ * message instead of one message per reply. Off by default, and off reproduces the
2175
+ * original behaviour exactly. Ordinary saved state — the PUT owns it, like the config.
2176
+ */
2177
+ linePerMessage: boolean;
2178
+ /**
2179
+ * DELIVERY preference: relay only the LAST completed assistant message of a run, sent when
2180
+ * the run ends, instead of mirroring each of them as it completes. Off by default, and off
2181
+ * reproduces the original behaviour exactly. Independent of `linePerMessage`: with both set
2182
+ * that one final message is what gets split per line. Ordinary saved state, like the field
2183
+ * above.
2184
+ */
2185
+ finalReplyOnly: boolean;
2186
+ /**
2187
+ * DELIVERY preference: render a relayed assistant reply's Markdown in the channel's own
2188
+ * markup rather than sending its characters as written. ON by default — the model writes
2189
+ * Markdown, and relaying it raw is what this replaced. What survives differs per channel
2190
+ * (Telegram has no headings, lists or tables; QQ has no code or tables; Feishu has all of
2191
+ * them) and a rendering the channel refuses falls back to the plain source, so the setting
2192
+ * can cost formatting and never a message.
2193
+ */
2194
+ renderMarkdown: boolean;
2195
+ /**
2196
+ * Most recent inbound chat (null until the bot is messaged once). The channel's connector
2197
+ * mints this string and is the only thing that may read it: Telegram encodes the forum
2198
+ * topic into it, so it is not always a bare chat id. Pass it to `MessagingClient.sendText`,
2199
+ * never to a channel API directly.
2200
+ */
2201
+ lastChatId: string | null;
2202
+ /** Whether that chat is a direct chat; group chats prefer reply-to-message. */
2203
+ lastChatIsDirect: boolean;
2204
+ /**
2205
+ * Channel id of the most recently processed inbound message (null until one arrives).
2206
+ * The bridge's redelivery guard is in memory and dies with the process; this is the one
2207
+ * fact that survives it, so a channel replaying an already-processed event across a
2208
+ * restart is recognized rather than run again (see MessagingBridge's recentInbound).
2209
+ */
2210
+ lastInboundMessageId: string | null;
2211
+ createdAt: string;
2212
+ updatedAt: string;
2213
+ }
2214
+
2215
+ /**
2216
+ * The messaging-channel connector seam: what the MessagingBridge needs from one chat
2217
+ * platform (Feishu, Telegram, QQ and WeChat today; further channels implement the same interface
2218
+ * and register in app assembly). A connector owns everything channel-specific — credential
2219
+ * shape, wire protocol, event normalization — and hands the bridge a channel-neutral
2220
+ * view: a client for outbound sends and credential checks, and a long-lived event
2221
+ * connection delivering normalized inbound messages (text, images and files, each as far as
2222
+ * the channel actually carries them).
2223
+ *
2224
+ * Config documents are the repo's stored per-channel JSON (`MessagingBindingRow.config`);
2225
+ * every connector method validates its own shape and throws a readable error on a
2226
+ * malformed one — the bridge treats that like any other channel failure.
2227
+ */
2228
+
2229
+ /** One inbound image's bytes, once fetched, with the MIME type the bridge needs for its data URL. */
2230
+ interface MessagingInboundImageData {
2231
+ data: Buffer;
2232
+ /** e.g. `image/png` — the connector's best answer, from the channel's own type or the bytes. */
2233
+ mimeType: string;
2234
+ }
2235
+ /**
2236
+ * An image attached to an inbound message: a handle, not the bytes.
2237
+ *
2238
+ * The bytes are deliberately NOT eagerly downloaded by the connector. Most of what makes
2239
+ * an inbound message uninteresting is decided after the event is normalized — a
2240
+ * redelivery is dropped before anything else happens — and a channel that downloaded
2241
+ * first would pay a full image transfer for every replay. Fetching lazily also puts the
2242
+ * cap where the transfer is: `fetch` refuses anything over `maxBytes` rather than handing
2243
+ * the bridge something it would only discard, which is what keeps a 100MB attachment from
2244
+ * ever being resident in this process.
2245
+ */
2246
+ interface MessagingInboundImage {
2247
+ /**
2248
+ * Downloads the image. Two distinct failures, and the bridge answers them differently:
2249
+ * `MessagingMediaTooLargeError` (see media.ts) for anything past `maxBytes`, which the
2250
+ * user fixes by sending something smaller, and any other Error — carrying the channel's
2251
+ * OWN reason, since a permission the bot lacks and a network blip are not the same
2252
+ * problem — for a transfer that could not be made at all. That reason reaches the chat,
2253
+ * so a connector must keep credentials out of it (see telegram-api's fetchErrorText).
2254
+ */
2255
+ fetch(maxBytes: number): Promise<MessagingInboundImageData>;
2256
+ }
2257
+ /**
2258
+ * A non-image file attached to an inbound message: a handle, not the bytes, for exactly the
2259
+ * reasons MessagingInboundImage is one — a redelivery is dropped before anything is
2260
+ * transferred, and the cap rides into `fetch` so an oversized attachment is refused at the
2261
+ * byte that crosses it rather than buffered whole and measured afterwards. A file's ceiling
2262
+ * is the server's per-file attachment limit, which is far larger than an image's, so the
2263
+ * difference between refusing during and after the transfer is correspondingly larger.
2264
+ *
2265
+ * There is no MIME type on this seam. An image needs one because its bytes become a `data:`
2266
+ * URL inside the conversation; a file's bytes go to the Session scratchpad and reach the
2267
+ * model as a PATH, so the name's extension is the whole of what says what it is — and a
2268
+ * channel's declared media type is the sender's claim about a file nothing here parses.
2269
+ */
2270
+ interface MessagingInboundFile {
2271
+ /**
2272
+ * The sender's own file name, extension included — what the model sees at the end of the
2273
+ * `[attached file: …]` path, and what a refusal notice names. Channel text, so it is
2274
+ * neither trusted nor pre-sanitized here: the attachment writer maps it onto a name that
2275
+ * is safe on disk (see services/task-attachments.ts). A channel with no name for a file
2276
+ * says so with a plain fallback rather than inventing one, since a made-up extension
2277
+ * would tell the model the file is something it is not.
2278
+ */
2279
+ fileName: string;
2280
+ /** Downloads the file; the same two failure shapes MessagingInboundImage.fetch documents. */
2281
+ fetch(maxBytes: number): Promise<Buffer>;
2282
+ }
2283
+ /** One inbound chat message, normalized across channels. */
2284
+ interface MessagingInboundMessage {
2285
+ /**
2286
+ * Channel-scoped chat id, and the reply target for direct chats. Opaque to the bridge, for
2287
+ * the same reason `messageId` is: the connector both mints it here and consumes it in
2288
+ * `sendText`, so a channel whose replies need more routing context than a chat identity
2289
+ * encodes it (Telegram appends the forum topic). It is stored verbatim as the binding's
2290
+ * last known chat, so whatever is encoded here is what survives a restart.
2291
+ */
2292
+ chatId: string;
2293
+ /** Direct chat with the bot, or a group chat (groups prefer reply-to-message). */
2294
+ chatKind: "direct" | "group";
2295
+ /**
2296
+ * Channel-scoped id of the inbound message itself (the group reply target). Opaque to
2297
+ * the bridge: the connector both mints it here and consumes it in `replyText`, so a
2298
+ * channel whose native message ids are not globally unique encodes whatever context a
2299
+ * reply needs (Telegram packs `chatId:messageId`). It is also the bridge's inbound
2300
+ * dedupe key, so it must identify the MESSAGE and not the delivery: a channel that
2301
+ * redelivers one message must mint the same id both times. `""` opts the channel out of
2302
+ * deduplication entirely — the honest answer for a connector with no message identity,
2303
+ * and better than every message after the first reading as a duplicate.
2304
+ */
2305
+ messageId: string;
2306
+ /**
2307
+ * The message's plain text; null for anything that carries none (stickers, voice, …) — a
2308
+ * message with no text, no image and no file gets the not-supported notice. An image or a
2309
+ * file sent with a caption puts the caption here: the caption IS that message's text, so
2310
+ * a channel needs no second field for it and the bridge no second rule. A channel that
2311
+ * carries a caption on media it does NOT deliver must still report null, or the model
2312
+ * answers a question about an attachment it never received.
2313
+ */
2314
+ text: string | null;
2315
+ /**
2316
+ * Images attached to this message (absent or empty when there are none). Handles rather
2317
+ * than bytes — see MessagingInboundImage. A channel that carries several images in one
2318
+ * message lists them in the order the user sees them.
2319
+ */
2320
+ images?: readonly MessagingInboundImage[];
2321
+ /**
2322
+ * Non-image files attached to this message, in the order the user sees them (absent or
2323
+ * empty when there are none) — see MessagingInboundFile.
2324
+ *
2325
+ * Optional, like `images`, so a channel that delivers no files simply never populates it
2326
+ * rather than carrying an empty list through its whole normalizer. A message may carry
2327
+ * text, images and files together; the bridge composes one composer input out of whatever
2328
+ * is there.
2329
+ */
2330
+ files?: readonly MessagingInboundFile[];
2331
+ /** Sender display name when the channel's event carries one. */
2332
+ senderName?: string;
2333
+ }
2334
+ interface MessagingConnectorHandlers {
2335
+ onMessage(msg: MessagingInboundMessage): void | Promise<void>;
2336
+ /** The connection completed a handshake (may fire again after an automatic reconnect). */
2337
+ onReady?(): void;
2338
+ /**
2339
+ * The connection failed and the channel gave up (or the initial connect failed). A
2340
+ * {@link MessagingChannelError} says whether the outage clears without anyone acting.
2341
+ */
2342
+ onError?(err: unknown): void;
2343
+ }
2344
+ /** A live inbound event stream; `close` ends it (idempotent). */
2345
+ interface MessagingConnection {
2346
+ close(): void;
2347
+ }
2348
+ /**
2349
+ * A successful credential check's optional payload: whatever the probe learned that the test
2350
+ * endpoint's feedback can act on. Every member is independently optional — a channel reports
2351
+ * the ones its check happens to answer and omits the rest, so a check that learned nothing
2352
+ * beyond "these credentials work" returns an empty object.
2353
+ */
2354
+ interface MessagingAccountInfo {
2355
+ /** A short human-readable label of the account the credentials sign in as (Telegram: the bot's `@username`). */
2356
+ accountLabel?: string;
2357
+ /**
2358
+ * Whether this ACCOUNT is set up to receive ordinary messages in the groups it belongs to
2359
+ * — as opposed to only the ones a platform hands a bot by default.
2360
+ *
2361
+ * Telegram's privacy mode is the case that needs saying: it is on for every bot whose
2362
+ * owner has not turned it off in @BotFather, and under it `getUpdates` simply omits
2363
+ * everything in a group that is not a command addressed to this bot or a reply to one of
2364
+ * its messages. Nothing errors — the messages are never delivered, so a binding that works
2365
+ * perfectly in a direct chat looks dead in a group. It is one account-wide setting and
2366
+ * Telegram overrides it in any group the bot administers, so `false` says the account is
2367
+ * muted where it is an ordinary member, never that some particular group is silent. Absent
2368
+ * when the channel has no such notion, or reports nothing about it: unknown must never be
2369
+ * reported as a problem.
2370
+ */
2371
+ readsGroupMessages?: boolean;
2372
+ }
2373
+ /**
2374
+ * What a send had to give up in order to land, as a short readable phrase. The message DID
2375
+ * arrive — a send that did not throws — so this is not a failure: it says the message is
2376
+ * somewhere less right than it was addressed to, which is otherwise invisible from the
2377
+ * outside. A channel that always delivers as addressed returns nothing at all; Telegram is
2378
+ * the one that reports, a forum topic deleted under a live conversation sending the reply to
2379
+ * General instead.
2380
+ */
2381
+ type MessagingSendNote = string;
2382
+ /** One file on its way out to a chat: the bytes, plus the name the chat should show. */
2383
+ interface MessagingOutboundFile {
2384
+ /** Display name — the base name of the Workspace-relative path the reply mentioned. */
2385
+ fileName: string;
2386
+ data: Buffer;
2387
+ }
2388
+ /** How one outbound text should be rendered. Absent throughout means plain text. */
2389
+ interface MessagingSendOptions {
2390
+ /**
2391
+ * Read the text as Markdown and render it in whatever this channel's markup is.
2392
+ *
2393
+ * A REQUEST, not an instruction: the connector owns both what its channel can show and
2394
+ * what to do when the channel refuses the rendered form, and it must fall back to sending
2395
+ * this same text plainly rather than let a formatting failure cost the message. What
2396
+ * survives the conversion differs per channel and is documented at each renderer
2397
+ * (telegram-html.ts, feishu-card.ts, qq-markdown.ts).
2398
+ *
2399
+ * Only a relayed assistant message ever sets it. The fixed notices and the test message
2400
+ * carry no markup and are sent as they are written, so a bug in a renderer can never
2401
+ * reach the one message a user sends to check whether the binding works at all.
2402
+ */
2403
+ markdown?: boolean;
2404
+ }
2405
+ /** Outbound half of one bound account. Every method throws on failure with a readable reason. */
2406
+ interface MessagingClient {
2407
+ /** Credential check (used by the test endpoint); resolving means the config signs in. */
2408
+ checkCredentials(): Promise<MessagingAccountInfo | null>;
2409
+ /** Sends a text message into a chat by chat id; resolves a MessagingSendNote when it degraded. */
2410
+ sendText(chatId: string, text: string, opts?: MessagingSendOptions): Promise<MessagingSendNote | void>;
2411
+ /** Replies a text message to a specific inbound message (threads correctly in group chats). */
2412
+ replyText(messageId: string, text: string, opts?: MessagingSendOptions): Promise<MessagingSendNote | void>;
2413
+ /**
2414
+ * Sends a picture into a chat, so a chart the Agent drew arrives as something the reader
2415
+ * can see rather than as a download. Channels that need an upload step first do it here —
2416
+ * the bridge holds bytes and a name, never a channel's file handle.
2417
+ */
2418
+ sendImage(chatId: string, file: MessagingOutboundFile): Promise<void>;
2419
+ /** Sends any other file into a chat as an attachment. */
2420
+ sendFile(chatId: string, file: MessagingOutboundFile): Promise<void>;
2421
+ }
2422
+ interface MessagingChannelConnector {
2423
+ readonly channel: MessagingChannel;
2424
+ /**
2425
+ * How many outbound messages this channel will accept in answer to ONE inbound message,
2426
+ * or undefined where no such limit exists (Feishu and Telegram both send freely).
2427
+ *
2428
+ * A channel that declares one enforces it itself — the connector is the only place that
2429
+ * knows what the platform rejects and how to combine messages so nothing is lost. What
2430
+ * the bridge does with the number is narrower: it caps the one-message-per-line split at
2431
+ * it, because that option's own ceiling is sized for a channel with no such budget and
2432
+ * would otherwise ask for more messages than the channel can ever deliver.
2433
+ */
2434
+ readonly replyBudget?: number;
2435
+ /** Builds the outbound client for one stored config (throws on a malformed document). */
2436
+ createClient(config: Record<string, unknown>): Promise<MessagingClient>;
2437
+ /**
2438
+ * Opens the inbound event stream for one stored config. Resolves as soon as the
2439
+ * connection is constructed and connecting — lifecycle arrives via the handlers
2440
+ * (`onReady` / `onError`), because channels reconnect on their own and a single promise
2441
+ * cannot carry a lifecycle.
2442
+ */
2443
+ connect(config: Record<string, unknown>, handlers: MessagingConnectorHandlers): Promise<MessagingConnection>;
2444
+ }
2445
+
2446
+ /**
2447
+ * Auth middleware: `Authorization: Bearer <local API token>` -> the built-in admin, or
2448
+ * session cookie -> auth_sessions row -> user; either way the user is injected into c.var.
2449
+ *
2450
+ * The Bearer path authenticates the boot's local API token (`<root>/api-token`, see
2451
+ * auth/api-token.ts) as the admin — the CLI's and agents' machine-local credential; it
2452
+ * applies to every route behind this middleware, SSE endpoints included (the CLI
2453
+ * consumes SSE via fetch with headers). A Bearer header that does not match fails the
2454
+ * request rather than falling back to the cookie: silently downgrading a wrong explicit
2455
+ * credential would mask misconfiguration. A few writes refuse the token all the same and take a
2456
+ * person's sign-in (requireHuman).
2457
+ *
2458
+ * Accessing a protected API while logged out -> 401 `{error:{code:"unauthorized"}}`.
2459
+ * CSRF: SameSite=Lax plus a Content-Type an HTML form cannot forge (see jsonOnlyWrites).
2460
+ * That guard stays for Bearer requests too — the CLI always sends application/json.
2461
+ */
2462
+
2463
+ /** Hono env: variables injected by the auth middleware. */
2464
+ type AppEnv = {
2465
+ Variables: {
2466
+ user: UserRow;
2467
+ /** How the current session was established — see {@link SessionVia}. */
2468
+ sessionVia: SessionVia;
2469
+ };
2470
+ };
2471
+
2472
+ /** Minimal dependency on SessionManager (eases test doubles; mirrors ScheduleTaskRunner). */
2473
+ interface MessagingTaskRunnerShape {
2474
+ statusOf(sessionId: string): string;
2475
+ startTask(sessionId: string, input: OmniMessage[],
2476
+ /**
2477
+ * `recall` is what a queued follow-up hands back when the user withdraws it in the Web
2478
+ * App. It is optional because the manager derives one from the input for every caller
2479
+ * that has nothing to add; the inbound path supplies its own only when it wrote file
2480
+ * attachments, whose on-disk paths the input does not carry (see inboundRecallStore).
2481
+ */
2482
+ opts: {
2483
+ queueIfBusy: boolean;
2484
+ recall?: RecallStore;
2485
+ }): Promise<{
2486
+ sessionId: string;
2487
+ queued: boolean;
2488
+ }>;
2489
+ }
2490
+ /**
2491
+ * Minimal dependency on the sessions index: existence checks for reconcile/cascade, the
2492
+ * Session's Project/Agent so a messaging failure can be filed under them (see recordError),
2493
+ * and the one field an outbound file needs — the Workspace a mentioned path resolves against.
2494
+ */
2495
+ interface MessagingSessionIndex {
2496
+ findById(sessionId: string): {
2497
+ projectId: string;
2498
+ agentId: string;
2499
+ workspace: string;
2500
+ } | null;
2501
+ }
2502
+ /**
2503
+ * Minimal dependency on the Workspace file service. Narrow, but deliberately NOT a
2504
+ * reimplementation: both methods carry that service's containment rules (a path that leaves
2505
+ * the Workspace — by `..`, by an absolute path, or through a symlink — is refused there,
2506
+ * and reads happen on the canonical path). A second resolver living in the bridge is
2507
+ * exactly the kind of thing that ends up one security fix behind.
2508
+ */
2509
+ interface MessagingWorkspaceFiles {
2510
+ /**
2511
+ * The subset of `rels` that exist as regular files inside the Workspace, each with the
2512
+ * time it was last written — which is what separates a file this run produced from one
2513
+ * the reply merely named (see deliverFiles).
2514
+ */
2515
+ statExistingWithMtime(workspace: string, rels: string[]): Promise<{
2516
+ rel: string;
2517
+ mtimeMs: number;
2518
+ }[]>;
2519
+ /** Reads a file, at most `maxBytes` of it. */
2520
+ read(workspace: string, rel: string, options?: {
2521
+ maxBytes?: number;
2522
+ }): Promise<{
2523
+ data: Buffer;
2524
+ fileName: string;
2525
+ }>;
2526
+ }
2527
+ interface MessagingBridgeDeps {
2528
+ repo: MessagingBindings;
2529
+ sessions: MessagingSessionIndex;
2530
+ files: MessagingWorkspaceFiles;
2531
+ /**
2532
+ * The data root, so an inbound file can be written into the Session scratchpad the web
2533
+ * composer's uploads land in (`scratchpadDir(root, projectId, agentId)/<sessionId>`,
2534
+ * deleted with the Session). The bridge holds the root rather than a pre-resolved
2535
+ * directory because the Project and Agent are per Session, not per binding.
2536
+ */
2537
+ root: string;
2538
+ /**
2539
+ * The attachment limits in force right now. A function, not a snapshot: the per-file and
2540
+ * per-message ceilings are admin-settable and read fresh per request everywhere else, so
2541
+ * a change has to reach the next inbound message without a restart.
2542
+ */
2543
+ attachmentLimits: () => AttachmentLimits;
2544
+ channels: ChannelHub;
2545
+ runner: MessagingTaskRunner;
2546
+ /** One connector per channel; a stored binding whose channel has no connector is skipped with an error record. */
2547
+ connectors: readonly MessagingChannelConnector[];
2548
+ errors: ErrorSink;
2549
+ log?: (line: string) => void;
2550
+ now?: () => number;
2551
+ /** Test hook: the pace between a per-line reply's messages (default MESSAGING_LINE_DELAY_MS; tests collapse it to zero). */
2552
+ lineDelayMs?: number;
2553
+ /** Test hook: one binding's inbound image budget (default MESSAGING_INBOUND_IMAGE_BUDGET_BYTES). */
2554
+ inboundImageBudgetBytes?: number;
2555
+ }
2556
+ declare class MessagingBridge {
2557
+ private readonly deps;
2558
+ private readonly entries;
2559
+ /**
2560
+ * Processed inbound ids per BINDING (`sessionId:channel`), deliberately not per
2561
+ * connection: re-saving an enabled binding restarts its connector, and a message
2562
+ * already turned into a Task must not run again because the connector is new.
2563
+ *
2564
+ * This map dies with the process; `messaging_bindings.last_inbound_message_id` is the
2565
+ * half that does not, and `connect` folds it back in here so the two are one memory
2566
+ * rather than two guards. The row holds ONE id — the last message the bridge finished
2567
+ * with — which is what Feishu's WebSocket resume needs and all it needs: that stream
2568
+ * replays events it never saw acknowledged, and the SDK acknowledges each one only
2569
+ * after our handler returns, so at most the one in flight when the process ended is
2570
+ * still owed. Telegram redelivers nothing across a restart — its poller advances
2571
+ * `offset` past an update before handing it over, and a fresh connection drains with
2572
+ * `getUpdates({offset: -1})`, discarding whatever was sent while nobody was connected —
2573
+ * so on that channel the row is still written and still seeded, and simply never has a
2574
+ * replay to catch.
2575
+ */
2576
+ private readonly recentInbound;
2577
+ /** Inbound image budgets, keyed like recentInbound: per BINDING, surviving a reconnect. */
2578
+ private readonly imageBudgets;
2579
+ private readonly connectors;
2580
+ private readonly now;
2581
+ private readonly log;
2582
+ private readonly lineDelayMs;
2583
+ private readonly inboundImageBudgetBytes;
2584
+ constructor(deps: MessagingBridgeDeps);
2585
+ /**
2586
+ * Server startup: connect every ENABLED binding (disabled ones keep their credentials
2587
+ * and stay dark; at most one per Session is enabled). A binding whose Session no longer
2588
+ * exists (deleted while this server was down, or by a bulk Agent/Project delete that
2589
+ * bypassed the per-session cascade) is reconciled away instead of connected.
2590
+ */
2591
+ start(): Promise<void>;
2592
+ /** App dispose: close every connection; bindings persist for the successor's start(). */
2593
+ stop(): void;
2594
+ /**
2595
+ * Align the live connection with the stored intent: an enabled binding exists →
2596
+ * (re)connect with its CURRENT config, none → disconnect. The state toggle calls this
2597
+ * after flipping intent, and a credential save calls it only while the saved binding is
2598
+ * enabled — the restart that keeps stored config and live connection from diverging.
2599
+ */
2600
+ sync(sessionId: string): Promise<void>;
2601
+ /** Route DELETE: disconnect (when this channel holds the connection) and drop the row. No-op when unbound. */
2602
+ unbind(sessionId: string, channel: string): void;
2603
+ /** Session-delete cascade: disconnect and drop every channel's config. */
2604
+ unbindSession(sessionId: string): void;
2605
+ /**
2606
+ * One channel's runtime status (only the connected channel is ever anything but
2607
+ * disconnected), plus what this connection has actually seen: when a message last arrived,
2608
+ * what last failed after one did, and the last connection failure even after it recovered.
2609
+ * A binding can be `connected` with nothing wrong and still never receive anything, and
2610
+ * that combination is invisible without them.
2611
+ */
2612
+ statusOf(sessionId: string, channel: string): MessagingRuntimeStatus;
2613
+ /**
2614
+ * Credential probe for the test endpoints: ok/error with latency, never a throw. A
2615
+ * channel whose check identifies the account (Telegram: the bot's @username) passes
2616
+ * that label through for the route's success feedback, along with anything else the
2617
+ * probe learned that the user should act on (see MessagingAccountInfo).
2618
+ */
2619
+ testCredentials(channel: string, config: Record<string, unknown>): Promise<{
2620
+ ok: boolean;
2621
+ latencyMs?: number;
2622
+ accountLabel?: string;
2623
+ readsGroupMessages?: boolean;
2624
+ error?: string;
2625
+ }>;
2626
+ /**
2627
+ * Test-message endpoints: a short fixed text to the binding's last known chat. The
2628
+ * route has already established that the binding and its chat exist; a race that
2629
+ * removed either since surfaces as the send failing.
2630
+ */
2631
+ sendTestMessage(row: MessagingBindingRow): Promise<void>;
2632
+ private connectorFor;
2633
+ private connect;
2634
+ private disconnect;
2635
+ /** Every timestamp this bridge writes comes from the injected clock, never from `Date` directly. */
2636
+ private nowIso;
2637
+ /** Status write, guarded against a stale entry (replaced by a newer connect). */
2638
+ private setStatus;
2639
+ /**
2640
+ * A connection failure, put in the three places one belongs: the live `error` state, the
2641
+ * copy kept across recovery, and the error table.
2642
+ *
2643
+ * Behind the same stale-entry guard `setStatus` uses, applied to all three — a connector
2644
+ * callback firing after a newer connect replaced this entry is reporting an attempt that was
2645
+ * already abandoned, and neither its status writes (which nothing reads) nor its record
2646
+ * (which would be filed against a binding since reconfigured) belong to the binding as it
2647
+ * now stands.
2648
+ */
2649
+ private recordConnectionFailure;
2650
+ /**
2651
+ * One error record for a messaging failure, filed under the Session's Project and Agent.
2652
+ *
2653
+ * The attribution is load-bearing, not decoration: `GET /api/projects/:id/usage/errors`
2654
+ * selects by project, and a record with none is "unattributed" — served only to admins
2655
+ * (`includeGlobalErrors`), so an ordinary member could never see a failure of their own
2656
+ * binding anywhere. The lookup uses the sessions index the bridge already holds; a row
2657
+ * deleted since leaves the record unattributed rather than dropping it.
2658
+ */
2659
+ private recordError;
2660
+ /**
2661
+ * A failure AFTER an inbound message was accepted: recorded like any other, and also put
2662
+ * on the binding's runtime status so the panel can say it. The error table is a Project-wide
2663
+ * dashboard nobody visits to debug one bot; the binding's own panel is where the question
2664
+ * is asked.
2665
+ *
2666
+ * A later success does not clear it. An intermittent failure — a group admin who revokes
2667
+ * send rights for a minute, a rate limit — would otherwise be erased by the next ordinary
2668
+ * message and never be seen at all, which is the same blind spot `lastConnectionError`
2669
+ * exists to close. The panel states `at` so a stale one reads as stale.
2670
+ */
2671
+ private recordDeliveryFailure;
2672
+ /**
2673
+ * A send that landed somewhere less right than it was addressed to. Not a failure — the
2674
+ * message arrived — so it stays out of `lastDeliveryError` and off the panel, and goes to
2675
+ * the log, which is where "the replies moved to General a while ago" is answerable at all.
2676
+ */
2677
+ private noteSend;
2678
+ private clientFor;
2679
+ /**
2680
+ * Has this binding already processed this message? A duplicate is a complete no-op —
2681
+ * ahead of the chat record and the text-only reply, not just the Task start, because a
2682
+ * replayed sticker would otherwise answer with the notice twice.
2683
+ *
2684
+ * Nothing downstream is idempotent: `startTask` with `queueIfBusy` appends to the
2685
+ * follow-up queue unconditionally, and a queued input is published to the Session
2686
+ * channel, so one redelivery becomes two runs in the chat AND two messages in the Web
2687
+ * App. A connector that mints no id opts out rather than having every message after the
2688
+ * first read as a duplicate.
2689
+ */
2690
+ private isRedelivery;
2691
+ /** Get-or-create one binding's processed-id memory (see recentInbound). */
2692
+ private inboundMemoryOf;
2693
+ private onInbound;
2694
+ /**
2695
+ * Turns a message carrying attachments into one Task, or resolves the notice to answer
2696
+ * the chat with when it could not be made whole.
2697
+ *
2698
+ * All-or-nothing across BOTH kinds, and in this order: the images are downloaded first,
2699
+ * then the files, and only then is anything written to disk. A refusal on either side
2700
+ * stops the message — a message is one thing the user sent, and half of it reaching the
2701
+ * Agent is not a partial success but a misleading one — and refusing before the write
2702
+ * means a rejected message leaves nothing behind in the scratchpad to clean up.
2703
+ */
2704
+ private startAttachedTask;
2705
+ /**
2706
+ * Lands this message's files in the Session scratchpad and folds their path lines into the
2707
+ * input. Returns the input untouched when there are none, so a message with no files
2708
+ * creates no directory.
2709
+ *
2710
+ * A failure here is NOT answered in the chat, unlike a refused download: a write that
2711
+ * fails is a fault of this server (a full disk, a scratchpad that is not writable), the
2712
+ * same class as a Session that cannot load, and it reaches the user through
2713
+ * recordDeliveryFailure and the binding's status line. The chat is told about the things
2714
+ * the person in it can act on.
2715
+ */
2716
+ private writeInboundFiles;
2717
+ /**
2718
+ * Downloads the message's files into composer attachments, or the notice to answer with
2719
+ * when one of them could not be delivered.
2720
+ *
2721
+ * The caps are the server's attachment limits — the very numbers an authenticated
2722
+ * composer upload answers to — because these files land in exactly the same place, the
2723
+ * Session scratchpad, and are read back the same way, through the model's bounded file
2724
+ * tools. Both byte budgets apply:
2725
+ *
2726
+ * - the per-file ceiling rides into each `fetch`, so an oversized attachment is refused at
2727
+ * the byte that crosses it and is never resident in this process;
2728
+ * - the per-message total is spent as the batch is collected, each later file's cap
2729
+ * shrinking to what is left, so a message carrying several cannot add up past it either.
2730
+ *
2731
+ * The per-message file COUNT is deliberately not enforced: attachment-limits.ts calls it a
2732
+ * composer-usability bound rather than a resource one, the byte budgets being what actually
2733
+ * bound memory and disk — and a chat message carries one file today on both channels.
2734
+ *
2735
+ * There is no rolling per-binding budget like the image one, because the cost it exists to
2736
+ * prevent is not paid here: an inline image is written into the Trace and re-read whole on
2737
+ * every history page and every resume, while a file is bytes on disk that nothing re-reads
2738
+ * and that are deleted with the Session.
2739
+ *
2740
+ * A refusal for size is the user's to fix and is not an error record — they sent something
2741
+ * too big and the chat says so. A failure is somebody's fault (a scope the bot was never
2742
+ * granted, most likely) and lands in the error log with the channel's reason, so it is
2743
+ * diagnosable from the dashboard and not only from a chat bubble.
2744
+ */
2745
+ private inboundFileAttachments;
2746
+ /**
2747
+ * Downloads the message's images into `image_url` input parts, or the notice to answer
2748
+ * with when one of them could not be delivered.
2749
+ *
2750
+ * The cap is the server's inline-image ceiling — the same number the web composer's
2751
+ * uploads answer to, because these images land in exactly the same place (the
2752
+ * conversation, and from there the Trace, which is read back whole on every resume). It
2753
+ * happens to match Telegram's own 20MB download ceiling for bots.
2754
+ *
2755
+ * A refusal for size is the user's to fix and is not an error record — they sent
2756
+ * something too big and the chat says so. A failure is somebody's fault (a scope the bot
2757
+ * was never granted, most likely) and lands in the error log with the channel's reason,
2758
+ * so it is diagnosable from the dashboard and not only from a chat bubble.
2759
+ */
2760
+ private inboundImageParts;
2761
+ /** This binding's rolling image budget, created on first use (see InboundImageBudget). */
2762
+ private imageBudgetFor;
2763
+ /** Reply to the inbound message itself: threaded reply in groups, plain send in direct chats. */
2764
+ private replyInbound;
2765
+ /** Channel tap: each main-conversation completed assistant message + the run-state flips. */
2766
+ private observe;
2767
+ /**
2768
+ * Queues one completed assistant message for the bound chat — or holds it back.
2769
+ *
2770
+ * Appended to the entry's send chain rather than started here: a run can complete several
2771
+ * messages within milliseconds, and two sends racing would reach the chat in the wrong
2772
+ * order. Returns immediately either way, so a slow channel never blocks the Session's
2773
+ * stream handling.
2774
+ *
2775
+ * A binding with `finalReplyOnly` set sends nothing from here: the message replaces
2776
+ * whatever this run was already holding, and the last one standing is delivered at the
2777
+ * run's idle edge (see onTaskState). The preference is read per message off the stored row,
2778
+ * as every delivery preference is, and no run ever sees it change: each of the three PUT
2779
+ * handlers calls `sync` on an enabled binding, `sync` reconnects, and `connect` opens with a
2780
+ * disconnect and a fresh entry — so a save made mid-run discards whatever was held at that
2781
+ * moment along with the rest of this entry's memory (see heldReply).
2782
+ */
2783
+ private relay;
2784
+ /**
2785
+ * Whether this binding holds a run's intermediate messages back. A row that cannot be read
2786
+ * at all is deliberately not decided here: relaying is the default, and the same read
2787
+ * inside deliverReply then records the failure rather than letting it pass as a preference.
2788
+ */
2789
+ private finalReplyOnly;
2790
+ /**
2791
+ * Where this entry's traffic would go right now: null before any chat is known, and null
2792
+ * when the row cannot be read at all — a caller holding null falls back to reading it again
2793
+ * at delivery time, which is where a failure to read is recorded rather than swallowed.
2794
+ */
2795
+ private replyTargetOf;
2796
+ private onTaskState;
2797
+ /**
2798
+ * Sends one relayed assistant message to the bound chat, in chunks under the channel's cap.
2799
+ * Which messages arrive here is the caller's business — every completed message of the run,
2800
+ * or its last one alone when the binding holds the rest (`finalReplyOnly`) — and nothing
2801
+ * below is affected by the difference.
2802
+ *
2803
+ * `at` is where to put it. A message relayed as it completes passes none and goes to the
2804
+ * chat the row names now, which is where the conversation is. A held reply passes the target
2805
+ * captured when its run started (see onTaskState), because it leaves at the run's END: a
2806
+ * second user writing in another forum topic meanwhile would otherwise take delivery of a
2807
+ * whole answer to a question they never asked, quoted onto their own message.
2808
+ *
2809
+ * A send failure is recorded, never thrown, so the chain behind it keeps
2810
+ * moving — and so do the messages behind it in THIS reply: one refused message (a 429 on
2811
+ * the third line of twelve) must not abandon the rest, which would leave a reply stopping
2812
+ * mid-sentence with nothing in the chat to say why. A binding with `linePerMessage` set
2813
+ * sends one message per non-blank line instead of one per reply (see splitReplyLines,
2814
+ * capped at the channel's own `replyBudget` where it declares one), paced
2815
+ * MESSAGING_LINE_DELAY_MS apart so the burst stays inside the channel's per-chat
2816
+ * allowance — chunking, threading and ordering are identical either way, and with the flag
2817
+ * off the send sequence is unchanged. In a group the run's
2818
+ * first outbound chunk threads onto the inbound message and
2819
+ * everything after it is a plain send: the reply-to relation names which message is being
2820
+ * answered, and one of them says it — repeating it per message and per chunk stacks quote
2821
+ * headers over the whole conversation.
2822
+ *
2823
+ * A binding with `renderMarkdown` set relays the model's Markdown as formatting rather
2824
+ * than as characters. The bridge only carries the REQUEST: the conversion is the
2825
+ * connector's, because what a channel can show and what it does with a rendering the
2826
+ * channel refuses are both channel-shaped (see MessagingSendOptions). What is not
2827
+ * channel-shaped is where a reply too long for one message may be cut, and that is why the
2828
+ * chunker changes with the flag — a cut through a code fence or an emphasis run costs the
2829
+ * construct everywhere, and on a strict parser costs the whole message's formatting.
2830
+ */
2831
+ private deliverReply;
2832
+ /**
2833
+ * Sends the files THIS RUN produced and its reply mentioned, after its text.
2834
+ *
2835
+ * `replyText` is what the run actually SENT, not everything it said: a binding with
2836
+ * `finalReplyOnly` set delivered one message, so that message is the whole of what this
2837
+ * scans (see onTaskState). A file named only in a held-back working note has nothing in
2838
+ * the chat to explain its arrival.
2839
+ *
2840
+ * `at` is where those words went, for the one caller whose text is not addressed to the live
2841
+ * chat (again the held reply). Files follow their words: a picture landing in a forum topic
2842
+ * where nothing named it is the same defect as one arriving unmentioned.
2843
+ *
2844
+ * Two filters, and both are load-bearing. The reply having said the name is what picks
2845
+ * the one output that was the point out of the dozen a run writes. The file being newer
2846
+ * than the run's start is what keeps that from becoming an exfiltration primitive: the
2847
+ * reply is steerable by whoever is in the chat, and on a mention-only rule a group member
2848
+ * who asks "what is in secrets.json?" gets the file uploaded by the very refusal that
2849
+ * declined to paste it. Containment (the Workspace and nothing narrower) lives in the
2850
+ * file service either way — see MessagingWorkspaceFiles.
2851
+ *
2852
+ * A file the run did not write is dropped SILENTLY, and so is a name matching no file at
2853
+ * all: the rule reads prose, so a reply that mentions the config it read — or describes a
2854
+ * `hello-world.md` it never wrote — is the ordinary case, and announcing those puts an
2855
+ * error-shaped line under correct answers. Both are logged instead. What IS recorded is a
2856
+ * file that exists and still did not go out — over a ceiling, past the count cap, or
2857
+ * refused on the way: the reply promised something that then did not arrive, so it is filed
2858
+ * under the Session's Project, once the batch is done, in one record per cause naming every
2859
+ * file it held back (see messagingFilesNotSentRecords). None of it is posted into the chat,
2860
+ * which carries the reply and its files and nothing about the ones that stayed behind.
2861
+ *
2862
+ * Always plain sends, never a threaded reply: a run that mentions a file has by
2863
+ * definition already sent the text that mentions it, and that message took the group's
2864
+ * one reply-to. Nothing here throws — a batch that fails is recorded, and a single file
2865
+ * that fails does not stop the ones behind it.
2866
+ */
2867
+ private deliverFiles;
2868
+ /**
2869
+ * One mirrored file: read under the outer ceiling, then sent as a picture or an attachment.
2870
+ * Resolves null once the file went out. A file over the ceiling for its kind is not sent and
2871
+ * resolves to that ceiling instead; a read or a send the channel refused throws. The caller
2872
+ * records both (see messagingFilesNotSentRecords).
2873
+ */
2874
+ private deliverOneFile;
2875
+ /** One-line notice (approval waiting) to the last known chat; silent before one exists. */
2876
+ private deliverNotice;
2877
+ /**
2878
+ * Where this entry's outbound traffic goes, with a client for it: null when no chat is
2879
+ * known yet (nothing mirrors before the first inbound message) and null when the binding
2880
+ * or its client could not be read at all, which is recorded like the send it stands in for.
2881
+ */
2882
+ private sendTarget;
2883
+ /**
2884
+ * Whether this entry still holds its Session's connection — asked at the head of every
2885
+ * step of the send chain, the way `observe` asks it of every event.
2886
+ *
2887
+ * The chain outlives what fed it. A run's last messages and the files behind them are
2888
+ * queued on it, and `stop()` — App dispose, a hot swap — drops the entry and takes the
2889
+ * database with it while they are still waiting their turn. A step that ran anyway would
2890
+ * read a closed database, and it would fail again inside its own error recording, leaving
2891
+ * the chain rejected with nothing left to catch it.
2892
+ */
2893
+ private live;
2894
+ /** The pace between a per-line reply's messages; zero (tests) waits for nothing. */
2895
+ private pace;
2896
+ }
2897
+ declare const MessagingTaskRunner_base: (abstract new () => MessagingTaskRunnerShape) & (<C>(target: C, context: ClassDecoratorContext) => C);
2898
+ /** What the bridge needs of the session runtime — declared at the consumer. */
2899
+ declare abstract class MessagingTaskRunner extends MessagingTaskRunner_base {
2900
+ }
2901
+ declare const Messaging_base: (abstract new () => Pick<MessagingBridge, "sync" | "start" | "stop" | "statusOf" | "unbind" | "unbindSession" | "testCredentials" | "sendTestMessage">) & (<C>(target: C, context: ClassDecoratorContext) => C);
2902
+ /** The chat-channel bridge: bindings, delivery, status. */
2903
+ declare abstract class Messaging extends Messaging_base {
2904
+ }
2905
+ interface MessagingSlots {
2906
+ /** A channel connector: which channel it speaks (static), and the connector (code). */
2907
+ connectors: Slot<{
2908
+ channel: "feishu" | "telegram" | "qq" | "wechat";
2909
+ }, MessagingChannelConnector>;
2910
+ }
2911
+
2912
+ /** A settings group as a module declares it: a configuration, and where the page draws it. */
2913
+ interface SettingsGroupDecl extends PluginConfiguration {
2914
+ /** Another group's name (its contribution id): this one is drawn inside that card and saved with it. */
2915
+ parent?: string;
2916
+ /** Position among the groups: lower first (absent = 100), then declaration order. */
2917
+ order?: number;
2918
+ }
2919
+ /** What a module reads: the group it declared, and a watch on it. */
2920
+ declare abstract class PluginConfig {
2921
+ /** The stored values merged onto the declared defaults; `{}` for a name no group answers to. */
2922
+ abstract get(name: string): Record<string, unknown>;
2923
+ /** Fires with the new document after every save of `name`; returns the unsubscribe. */
2924
+ abstract watch(name: string, cb: (values: Record<string, unknown>) => void): () => void;
2925
+ /** Whether anything was ever saved under `name` — what tells a default from a choice. */
2926
+ abstract saved(name: string): boolean;
2927
+ /** The configuration `name` was declared with (what `resolveTable` reads a table by); undefined for none. */
2928
+ abstract schema(name: string): PluginConfiguration | undefined;
2929
+ }
2930
+ interface PluginConfigSlots {
2931
+ /** A settings group, as data: its id is its name, the data its configuration. */
2932
+ groups: Slot<SettingsGroupDecl>;
2933
+ }
2934
+
2935
+ /** The assembled business surface: one request in, one response (or a decline) out. */
2936
+ declare abstract class Http {
2937
+ abstract fetch(request: Opaque<"Request", Request>): Promise<Opaque<"Response", Response>>;
2938
+ }
2939
+ interface HttpSlots {
2940
+ /**
2941
+ * A route group. `auth: "user"` mounts it behind the cookie gate, `"none"` in front of
2942
+ * it; `order` is the mount position (a stable number, since Hono matches in order). The
2943
+ * code half is the group's Hono app.
2944
+ */
2945
+ routes: Slot<{
2946
+ prefix: string;
2947
+ auth: "user" | "none";
2948
+ order: number;
2949
+ }, Opaque<"Hono", Hono<AppEnv>>>;
2950
+ }
2951
+
2952
+ /**
2953
+ * GET /api/contributions — the web slots' contributions, as data. What a pushed platform
2954
+ * or an installed plugin adds to the web app arrives here; the app merges it with its
2955
+ * own module.json and renders what it has a renderer for.
2956
+ */
2957
+
2958
+ /**
2959
+ * The frontend's slots, declared on the SERVER so a server module (or a plugin) can
2960
+ * contribute a page, an Agent settings tab or a Session tab as manifest data. The web
2961
+ * app reads them back through GET /api/contributions and renders the ones whose
2962
+ * renderer it knows: a `builtin` name from its own registry, or an `iframe`. No code
2963
+ * crosses this boundary — only data.
2964
+ */
2965
+ declare abstract class WebShell {
2966
+ /** Every contribution to the web slots, by slot, in module order. */
2967
+ abstract contributions(): ContributionsResponse;
2968
+ }
2969
+ interface WebShellSlots {
2970
+ /** A page: its route, whether it sits in the main nav, whether it is admin-only. */
2971
+ pages: {
2972
+ key: string;
2973
+ path: string;
2974
+ nav: "main" | "none";
2975
+ admin: boolean;
2976
+ renderer: RendererRef;
2977
+ };
2978
+ /** A tab on the Agent settings page. */
2979
+ agentTabs: {
2980
+ key: string;
2981
+ order: number;
2982
+ renderer: RendererRef;
2983
+ };
2984
+ /**
2985
+ * A tab beside a Session's chat. A workflow contributes here too, from its own tree, where
2986
+ * the host publishes this slot under this module's name and scopes the tab to its Agent.
2987
+ */
2988
+ sessionTabs: {
2989
+ key: string;
2990
+ title: string;
2991
+ titleZh?: string;
2992
+ renderer: {
2993
+ builtin: string;
2994
+ } | {
2995
+ iframe: {
2996
+ src: string;
2997
+ };
2998
+ };
2999
+ };
3000
+ }
3001
+
3002
+ export { AgentConfigService, AgentService, Http, type HttpSlots, Messaging, type MessagingSlots, PluginConfig, type PluginConfigSlots, PluginConfiguration, Sandbox, type SandboxSlots, SessionServiceIface, Sessions, type SettingsGroupDecl, Terminals, WebShell, type WebShellSlots };