@prismshadow/penguin-server 0.2.11 → 0.2.15

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