@turmind/halo 0.1.9

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 (296) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +143 -0
  3. package/admin-out/404.html +1 -0
  4. package/admin-out/_next/static/IBP_mwBDf-cPwZSZZGpi8/_buildManifest.js +1 -0
  5. package/admin-out/_next/static/IBP_mwBDf-cPwZSZZGpi8/_ssgManifest.js +1 -0
  6. package/admin-out/_next/static/chunks/1d2d5650-810a3ca1ae20cab5.js +18 -0
  7. package/admin-out/_next/static/chunks/200.3b8f8a74f005a134.js +1 -0
  8. package/admin-out/_next/static/chunks/220.3dc41cd6269b88e6.js +1 -0
  9. package/admin-out/_next/static/chunks/298-d6b224b569da4de0.js +1 -0
  10. package/admin-out/_next/static/chunks/304eda7d.b1ee7f523653e027.js +1 -0
  11. package/admin-out/_next/static/chunks/330.3b784cf0dcd69163.js +1 -0
  12. package/admin-out/_next/static/chunks/442.f4302087bddb1ec1.js +1 -0
  13. package/admin-out/_next/static/chunks/443.d1b289781793f8de.js +25 -0
  14. package/admin-out/_next/static/chunks/503-7c6d9935df6e0cea.js +1 -0
  15. package/admin-out/_next/static/chunks/533.5f2d4bfe3d076095.js +1 -0
  16. package/admin-out/_next/static/chunks/579.a2718ddd395a2f3a.js +1 -0
  17. package/admin-out/_next/static/chunks/635-3dd0043ed0520b9d.js +1 -0
  18. package/admin-out/_next/static/chunks/65.859665ba6717967a.js +1 -0
  19. package/admin-out/_next/static/chunks/661.a4fe93f3f2a5e871.js +1 -0
  20. package/admin-out/_next/static/chunks/688.4e014ac6cfc7e152.js +1 -0
  21. package/admin-out/_next/static/chunks/868.0c95dfe283400a00.js +1 -0
  22. package/admin-out/_next/static/chunks/app/_not-found/page-8e35399c8fd5016d.js +1 -0
  23. package/admin-out/_next/static/chunks/app/layout-58f773336e6fce61.js +1 -0
  24. package/admin-out/_next/static/chunks/app/page-cc8043335282d745.js +1 -0
  25. package/admin-out/_next/static/chunks/db534642.09d3c650ff587175.js +1 -0
  26. package/admin-out/_next/static/chunks/fb4ec4d6-3fa44f69484f91b9.js +1 -0
  27. package/admin-out/_next/static/chunks/framework-c3031ae54a63fe16.js +1 -0
  28. package/admin-out/_next/static/chunks/main-14b48e3c2588354f.js +1 -0
  29. package/admin-out/_next/static/chunks/main-app-483b436873410ddc.js +1 -0
  30. package/admin-out/_next/static/chunks/pages/_app-b2fc6e5f920d1b1a.js +1 -0
  31. package/admin-out/_next/static/chunks/pages/_error-f3696a8f3d67d150.js +1 -0
  32. package/admin-out/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  33. package/admin-out/_next/static/chunks/webpack-8bd7ef88a6527feb.js +1 -0
  34. package/admin-out/_next/static/css/659eccb5db697b76.css +1 -0
  35. package/admin-out/_next/static/css/8fce4e1a54dac12a.css +3 -0
  36. package/admin-out/_next/static/media/19cfc7226ec3afaa-s.woff2 +0 -0
  37. package/admin-out/_next/static/media/21350d82a1f187e9-s.woff2 +0 -0
  38. package/admin-out/_next/static/media/8e9860b6e62d6359-s.woff2 +0 -0
  39. package/admin-out/_next/static/media/ba9851c3c22cd980-s.woff2 +0 -0
  40. package/admin-out/_next/static/media/c5fe6dc8356a8c31-s.woff2 +0 -0
  41. package/admin-out/_next/static/media/df0a9ae256c0569c-s.woff2 +0 -0
  42. package/admin-out/_next/static/media/e4af272ccee01ff0-s.p.woff2 +0 -0
  43. package/admin-out/favicon.ico +0 -0
  44. package/admin-out/icon.svg +18 -0
  45. package/admin-out/index.html +1 -0
  46. package/admin-out/index.txt +23 -0
  47. package/admin-out/monaco/vs/_commonjsHelpers-CT9FvmAN.js +1 -0
  48. package/admin-out/monaco/vs/abap-D-t0cyap.js +1 -0
  49. package/admin-out/monaco/vs/apex-CcIm7xu6.js +1 -0
  50. package/admin-out/monaco/vs/assets/css.worker-HnVq6Ewq.js +93 -0
  51. package/admin-out/monaco/vs/assets/editor.worker-Be8ye1pW.js +26 -0
  52. package/admin-out/monaco/vs/assets/html.worker-B51mlPHg.js +470 -0
  53. package/admin-out/monaco/vs/assets/json.worker-DKiEKt88.js +58 -0
  54. package/admin-out/monaco/vs/assets/ts.worker-CMbG-7ft.js +67731 -0
  55. package/admin-out/monaco/vs/azcli-BA0tQDCg.js +1 -0
  56. package/admin-out/monaco/vs/basic-languages/monaco.contribution.js +1 -0
  57. package/admin-out/monaco/vs/bat-C397hTD6.js +1 -0
  58. package/admin-out/monaco/vs/bicep-DF5aW17k.js +2 -0
  59. package/admin-out/monaco/vs/cameligo-plsz8qhj.js +1 -0
  60. package/admin-out/monaco/vs/clojure-Y2auQMzK.js +1 -0
  61. package/admin-out/monaco/vs/coffee-Bu45yuWE.js +1 -0
  62. package/admin-out/monaco/vs/cpp-CkKPQIni.js +1 -0
  63. package/admin-out/monaco/vs/csharp-CX28MZyh.js +1 -0
  64. package/admin-out/monaco/vs/csp-D8uWnyxW.js +1 -0
  65. package/admin-out/monaco/vs/css-CaeNmE3S.js +3 -0
  66. package/admin-out/monaco/vs/cssMode-CjiAH6dQ.js +1 -0
  67. package/admin-out/monaco/vs/cypher-DVThT8BS.js +1 -0
  68. package/admin-out/monaco/vs/dart-CmGfCvrO.js +1 -0
  69. package/admin-out/monaco/vs/dockerfile-CZqqYdch.js +1 -0
  70. package/admin-out/monaco/vs/ecl-30fUercY.js +1 -0
  71. package/admin-out/monaco/vs/editor/editor.main.css +1 -0
  72. package/admin-out/monaco/vs/editor/editor.main.js +5 -0
  73. package/admin-out/monaco/vs/editor.api-CalNCsUg.js +903 -0
  74. package/admin-out/monaco/vs/elixir-xjPaIfzF.js +1 -0
  75. package/admin-out/monaco/vs/flow9-DqtmStfK.js +1 -0
  76. package/admin-out/monaco/vs/freemarker2-Cz_sV6Md.js +3 -0
  77. package/admin-out/monaco/vs/fsharp-BOMdg4U1.js +1 -0
  78. package/admin-out/monaco/vs/go-D_hbi-Jt.js +1 -0
  79. package/admin-out/monaco/vs/graphql-CKUU4kLG.js +1 -0
  80. package/admin-out/monaco/vs/handlebars-OwglfO-1.js +1 -0
  81. package/admin-out/monaco/vs/hcl-DTaboeZW.js +1 -0
  82. package/admin-out/monaco/vs/html-Pa1xEWsY.js +1 -0
  83. package/admin-out/monaco/vs/htmlMode-Bz67EXwp.js +1 -0
  84. package/admin-out/monaco/vs/ini-CsNwO04R.js +1 -0
  85. package/admin-out/monaco/vs/java-CI4ZMsH9.js +1 -0
  86. package/admin-out/monaco/vs/javascript-PczUCGdz.js +1 -0
  87. package/admin-out/monaco/vs/jsonMode-DULH5oaX.js +7 -0
  88. package/admin-out/monaco/vs/julia-BwzEvaQw.js +1 -0
  89. package/admin-out/monaco/vs/kotlin-IUYPiTV8.js +1 -0
  90. package/admin-out/monaco/vs/language/css/monaco.contribution.js +1 -0
  91. package/admin-out/monaco/vs/language/html/monaco.contribution.js +1 -0
  92. package/admin-out/monaco/vs/language/json/monaco.contribution.js +1 -0
  93. package/admin-out/monaco/vs/language/typescript/monaco.contribution.js +1 -0
  94. package/admin-out/monaco/vs/less-C0eDYdqa.js +2 -0
  95. package/admin-out/monaco/vs/lexon-iON-Kj97.js +1 -0
  96. package/admin-out/monaco/vs/liquid-DqKjdPGy.js +1 -0
  97. package/admin-out/monaco/vs/loader.js +1368 -0
  98. package/admin-out/monaco/vs/lspLanguageFeatures-kM9O9rjY.js +4 -0
  99. package/admin-out/monaco/vs/lua-DtygF91M.js +1 -0
  100. package/admin-out/monaco/vs/m3-CsR4AuFi.js +1 -0
  101. package/admin-out/monaco/vs/markdown-C_rD0bIw.js +1 -0
  102. package/admin-out/monaco/vs/mdx-DEWtB1K5.js +1 -0
  103. package/admin-out/monaco/vs/mips-CiYP61RB.js +1 -0
  104. package/admin-out/monaco/vs/monaco.contribution-D2OdxNBt.js +1 -0
  105. package/admin-out/monaco/vs/monaco.contribution-DO3azKX8.js +1 -0
  106. package/admin-out/monaco/vs/monaco.contribution-EcChJV6a.js +1 -0
  107. package/admin-out/monaco/vs/monaco.contribution-qLAYrEOP.js +1 -0
  108. package/admin-out/monaco/vs/msdax-C38-sJlp.js +1 -0
  109. package/admin-out/monaco/vs/mysql-CdtbpvbG.js +1 -0
  110. package/admin-out/monaco/vs/nls.messages-loader.js +1 -0
  111. package/admin-out/monaco/vs/nls.messages.cs.js.js +17 -0
  112. package/admin-out/monaco/vs/nls.messages.de.js.js +17 -0
  113. package/admin-out/monaco/vs/nls.messages.es.js.js +17 -0
  114. package/admin-out/monaco/vs/nls.messages.fr.js.js +15 -0
  115. package/admin-out/monaco/vs/nls.messages.it.js.js +15 -0
  116. package/admin-out/monaco/vs/nls.messages.ja.js.js +17 -0
  117. package/admin-out/monaco/vs/nls.messages.js.js +10 -0
  118. package/admin-out/monaco/vs/nls.messages.ko.js.js +25 -0
  119. package/admin-out/monaco/vs/nls.messages.pl.js.js +17 -0
  120. package/admin-out/monaco/vs/nls.messages.pt-br.js.js +6 -0
  121. package/admin-out/monaco/vs/nls.messages.ru.js.js +17 -0
  122. package/admin-out/monaco/vs/nls.messages.tr.js.js +15 -0
  123. package/admin-out/monaco/vs/nls.messages.zh-cn.js.js +17 -0
  124. package/admin-out/monaco/vs/nls.messages.zh-tw.js.js +15 -0
  125. package/admin-out/monaco/vs/objective-c-CntZFaHX.js +1 -0
  126. package/admin-out/monaco/vs/pascal-r6kuqfl_.js +1 -0
  127. package/admin-out/monaco/vs/pascaligo-BiXoTmXh.js +1 -0
  128. package/admin-out/monaco/vs/perl-DABw_TcH.js +1 -0
  129. package/admin-out/monaco/vs/pgsql-me_jFXeX.js +1 -0
  130. package/admin-out/monaco/vs/php-D_kh-9LK.js +1 -0
  131. package/admin-out/monaco/vs/pla-VfZjczW0.js +1 -0
  132. package/admin-out/monaco/vs/postiats-BBSzz8Pk.js +1 -0
  133. package/admin-out/monaco/vs/powerquery-Dt-g_2cc.js +1 -0
  134. package/admin-out/monaco/vs/powershell-B-7ap1zc.js +1 -0
  135. package/admin-out/monaco/vs/protobuf-BmtuEB1A.js +2 -0
  136. package/admin-out/monaco/vs/pug-BRpRNeEb.js +1 -0
  137. package/admin-out/monaco/vs/python-Cr0UkIbn.js +1 -0
  138. package/admin-out/monaco/vs/qsharp-BzsFaUU9.js +1 -0
  139. package/admin-out/monaco/vs/r-f8dDdrp4.js +1 -0
  140. package/admin-out/monaco/vs/razor-BYAHOTkz.js +1 -0
  141. package/admin-out/monaco/vs/redis-fvZQY4PI.js +1 -0
  142. package/admin-out/monaco/vs/redshift-45Et0LQi.js +1 -0
  143. package/admin-out/monaco/vs/restructuredtext-C7UUFKFD.js +1 -0
  144. package/admin-out/monaco/vs/ruby-CZO8zYTz.js +1 -0
  145. package/admin-out/monaco/vs/rust-Bfetafyc.js +1 -0
  146. package/admin-out/monaco/vs/sb-3GYllVck.js +1 -0
  147. package/admin-out/monaco/vs/scala-foMgrKo1.js +1 -0
  148. package/admin-out/monaco/vs/scheme-CHdMtr7p.js +1 -0
  149. package/admin-out/monaco/vs/scss-C1cmLt9V.js +3 -0
  150. package/admin-out/monaco/vs/shell-ClXCKCEW.js +1 -0
  151. package/admin-out/monaco/vs/solidity-MZ6ExpPy.js +1 -0
  152. package/admin-out/monaco/vs/sophia-DWkuSsPQ.js +1 -0
  153. package/admin-out/monaco/vs/sparql-AUGFYSyk.js +1 -0
  154. package/admin-out/monaco/vs/sql-32GpJSV2.js +1 -0
  155. package/admin-out/monaco/vs/st-CuDFIVZ_.js +1 -0
  156. package/admin-out/monaco/vs/swift-n-2HociN.js +3 -0
  157. package/admin-out/monaco/vs/systemverilog-Ch4vA8Yt.js +1 -0
  158. package/admin-out/monaco/vs/tcl-D74tq1nH.js +1 -0
  159. package/admin-out/monaco/vs/tsMode-CZz1Umrk.js +11 -0
  160. package/admin-out/monaco/vs/twig-C6taOxMV.js +1 -0
  161. package/admin-out/monaco/vs/typescript-DfOrAzoV.js +1 -0
  162. package/admin-out/monaco/vs/typespec-D-PIh9Xw.js +1 -0
  163. package/admin-out/monaco/vs/vb-Dyb2648j.js +1 -0
  164. package/admin-out/monaco/vs/wgsl-BhLXMOR0.js +298 -0
  165. package/admin-out/monaco/vs/workers-DcJshg-q.js +1 -0
  166. package/admin-out/monaco/vs/xml-CdsdnY8S.js +1 -0
  167. package/admin-out/monaco/vs/yaml-DYGvmE88.js +1 -0
  168. package/bin/halo.js +9 -0
  169. package/bundled-docs/design/agent.md +145 -0
  170. package/bundled-docs/design/architecture.md +270 -0
  171. package/bundled-docs/design/background-dispatch.md +151 -0
  172. package/bundled-docs/design/command.md +58 -0
  173. package/bundled-docs/design/cron.md +88 -0
  174. package/bundled-docs/design/evolution.md +383 -0
  175. package/bundled-docs/design/express-self.md +145 -0
  176. package/bundled-docs/design/feishu.md +224 -0
  177. package/bundled-docs/design/halo-city.md +112 -0
  178. package/bundled-docs/design/prompt-system.md +411 -0
  179. package/bundled-docs/design/session.md +321 -0
  180. package/bundled-docs/design/slack.md +212 -0
  181. package/bundled-docs/design/storage.md +578 -0
  182. package/bundled-docs/design/telegram.md +161 -0
  183. package/bundled-docs/design/web.md +222 -0
  184. package/bundled-docs/design/wechat.md +146 -0
  185. package/bundled-docs/design/ws.md +144 -0
  186. package/bundled-docs/dev/acp-adapter.md +337 -0
  187. package/bundled-docs/dev/add-channel.md +376 -0
  188. package/bundled-docs/dev/add-model-provider.md +386 -0
  189. package/bundled-docs/dev/api.md +686 -0
  190. package/bundled-docs/dev/deploy.md +198 -0
  191. package/bundled-docs/dev/desktop-packaging.md +327 -0
  192. package/bundled-docs/dev/env.md +158 -0
  193. package/bundled-docs/dev/logging.md +85 -0
  194. package/bundled-docs/dev/previews.md +225 -0
  195. package/bundled-docs/dev/tools.md +331 -0
  196. package/bundled-docs/guide/README.md +29 -0
  197. package/bundled-docs/guide/agents.md +264 -0
  198. package/bundled-docs/guide/channels/README.md +62 -0
  199. package/bundled-docs/guide/channels/acp.md +150 -0
  200. package/bundled-docs/guide/channels/feishu.md +154 -0
  201. package/bundled-docs/guide/channels/slack.md +156 -0
  202. package/bundled-docs/guide/channels/telegram.md +112 -0
  203. package/bundled-docs/guide/channels/web.md +149 -0
  204. package/bundled-docs/guide/channels/wechat.md +110 -0
  205. package/bundled-docs/guide/chat.md +112 -0
  206. package/bundled-docs/guide/cli.md +197 -0
  207. package/bundled-docs/guide/getting-started.md +93 -0
  208. package/bundled-docs/guide/secrets-and-credentials.md +161 -0
  209. package/bundled-docs/guide/sessions.md +104 -0
  210. package/bundled-docs/guide/skills.md +302 -0
  211. package/bundled-docs/guide/testing-agents-and-skills.md +136 -0
  212. package/bundled-docs/guide/workspace.md +131 -0
  213. package/bundled-docs/plans/cron-tasks.md +362 -0
  214. package/bundled-docs/plans/design-suggestions.md +203 -0
  215. package/bundled-docs/plans/issue-session-deep-nesting.md +208 -0
  216. package/bundled-docs/plans/launch-plan.md +111 -0
  217. package/bundled-docs/plans/md-and-agent-refactor.md +197 -0
  218. package/bundled-docs/plans/remote-mac-worker.md +196 -0
  219. package/bundled-docs/plans/roadmap.md +40 -0
  220. package/bundled-docs/plans/self-evolution.md +1379 -0
  221. package/bundled-docs/plans/todo.md +32 -0
  222. package/bundled-docs/plans/weixin-channel.md +255 -0
  223. package/bundled-docs/requirements/agent.md +92 -0
  224. package/bundled-docs/requirements/chat.md +81 -0
  225. package/bundled-docs/requirements/command.md +130 -0
  226. package/bundled-docs/requirements/editor.md +82 -0
  227. package/bundled-docs/requirements/explorer.md +89 -0
  228. package/bundled-docs/requirements/overview.md +62 -0
  229. package/bundled-docs/requirements/session.md +73 -0
  230. package/bundled-docs/requirements/settings.md +224 -0
  231. package/bundled-docs/requirements/skill.md +125 -0
  232. package/bundled-docs/requirements/source-control.md +82 -0
  233. package/bundled-docs/requirements/terminal.md +118 -0
  234. package/bundled-docs/requirements/workspace.md +80 -0
  235. package/bundled-docs/test/docs-seeding.md +101 -0
  236. package/bundled-docs/test/session.md +374 -0
  237. package/dist/index.js +26750 -0
  238. package/package.json +67 -0
  239. package/templates/INSTRUCTIONS.md +25 -0
  240. package/templates/agents/__apply_agent__/AGENT.md +229 -0
  241. package/templates/agents/__apply_agent__/agent.yaml +30 -0
  242. package/templates/agents/__evo_agent__/AGENT.md +323 -0
  243. package/templates/agents/__evo_agent__/agent.yaml +29 -0
  244. package/templates/agents/__score__/AGENT.md +177 -0
  245. package/templates/agents/__score__/agent.yaml +29 -0
  246. package/templates/agents/deep-executor/AGENT.md +60 -0
  247. package/templates/agents/deep-executor/agent.yaml +25 -0
  248. package/templates/agents/default/AGENT.md +38 -0
  249. package/templates/agents/default/agent.yaml +51 -0
  250. package/templates/agents/executor/AGENT.md +38 -0
  251. package/templates/agents/executor/agent.yaml +25 -0
  252. package/templates/aliases.yaml +26 -0
  253. package/templates/canvas/self.html +660 -0
  254. package/templates/config.yaml +69 -0
  255. package/templates/models/anthropic.yaml +47 -0
  256. package/templates/models/aws-bedrock-claude-invoke.yaml +156 -0
  257. package/templates/models/aws-bedrock-mantle.yaml +89 -0
  258. package/templates/models/deepseek.yaml +62 -0
  259. package/templates/models/doubao.yaml +137 -0
  260. package/templates/models/hunyuan.yaml +56 -0
  261. package/templates/models/kimi.yaml +41 -0
  262. package/templates/models/mimo-token-plan-china.yaml +61 -0
  263. package/templates/models/minimax.yaml +80 -0
  264. package/templates/models/openai.yaml +51 -0
  265. package/templates/models/qwen.yaml +78 -0
  266. package/templates/optional-skills/tavily-web-search/SKILL.md +91 -0
  267. package/templates/optional-skills/tavily-web-search/config.yaml +10 -0
  268. package/templates/prompts/all/TOOL_GUIDELINES.md +35 -0
  269. package/templates/prompts/all/TOOL_SHELL.md +4 -0
  270. package/templates/prompts/all/TOOL_SHELL.windows.md +7 -0
  271. package/templates/prompts/bootstrap/BOOTSTRAP.md +26 -0
  272. package/templates/prompts/root/WORKSPACE_MEMORY.md +11 -0
  273. package/templates/schema.sql +30 -0
  274. package/templates/skills/acp/SKILL.md +236 -0
  275. package/templates/skills/acp/templates/SKILL.md.tmpl +74 -0
  276. package/templates/skills/acp/templates/ask.py +176 -0
  277. package/templates/skills/acp/templates/config.yaml.tmpl +33 -0
  278. package/templates/skills/agent/SKILL.md +128 -0
  279. package/templates/skills/aws-knowledge/SKILL.md +82 -0
  280. package/templates/skills/aws-knowledge/scripts/aws_kb.py +61 -0
  281. package/templates/skills/cron/SKILL.md +217 -0
  282. package/templates/skills/cron/manage-cron.py +434 -0
  283. package/templates/skills/halo/SKILL.md +98 -0
  284. package/templates/skills/nova-web-search/SKILL.md +65 -0
  285. package/templates/skills/self/SKILL.md +139 -0
  286. package/templates/skills/send-file/SKILL.md +28 -0
  287. package/templates/skills/send-file/feishu.md +16 -0
  288. package/templates/skills/send-file/slack.md +7 -0
  289. package/templates/skills/send-file/telegram.md +9 -0
  290. package/templates/skills/send-file/web.md +6 -0
  291. package/templates/skills/send-file/wechat.md +8 -0
  292. package/templates/skills/skill/SKILL.md +131 -0
  293. package/templates/skills/workspace/SKILL.md +28 -0
  294. package/templates/skills/workspace/setup-tidy.md +265 -0
  295. package/templates/skills/workspace/share.md +187 -0
  296. package/templates/skills/workspace/stage.py +487 -0
@@ -0,0 +1,270 @@
1
+ # Backend Architecture
2
+
3
+ > Drawn from the actual code, not a design brief. Use it to assess module coupling and responsibility boundaries.
4
+
5
+ ## Global topology
6
+
7
+ ```
8
+ ┌─────────────────────────────────────────────────────────────────────┐
9
+ │ index.ts (entry) │
10
+ │ Create Hono app → mount routes → start HTTP server → upgrade WS │
11
+ │ │
12
+ │ Owned instances: │
13
+ │ registry: SessionManagerRegistry │
14
+ │ channelDb: ChannelDb (~/.halo/secrets/channels/channels.db) │
15
+ │ wss: WebSocketServer │
16
+ │ │
17
+ │ Route mounts (direct): │
18
+ │ /api/auth/* → createAuthRoutes() │
19
+ │ /api/files/* → createFileRoutes() │
20
+ │ /api/fs/* → createFileRoutes (home/exists/browse/resolve)│
21
+ │ /api/agent-configs/*→ createAgentConfigRoutes() │
22
+ │ /api/skills/* → createSkillRoutes() │
23
+ │ /api/settings/* → createSettingsRoutes() │
24
+ │ /api/evolution/* → createEvolutionRoutes() │
25
+ │ /api/cron/* → createCronRoutes() │
26
+ │ /api/sessions/* → createSessionRoutes() │
27
+ │ /api/show/* → createShowRoutes() (halo-city snapshot) │
28
+ │ /api/commands → createCommandRoutes() │
29
+ │ Route mounts (via bootChannels — registry-driven): │
30
+ │ /api/web/* (web descriptor) │
31
+ │ /api/telegram/* (telegram descriptor) │
32
+ │ /api/weixin/* (wechat descriptor) │
33
+ │ /api/slack/* (slack descriptor) │
34
+ │ /api/feishu/* (feishu descriptor) │
35
+ │ /ws → setupWebSocketHandler({wss,registry}) │
36
+ │ /* → static frontend (Next.js out/) │
37
+ └─────────────────────────────────────────────────────────────────────┘
38
+ ```
39
+
40
+ ## Key modules
41
+
42
+ ### SessionManager — the central hub
43
+
44
+ Manages the full lifecycle of every agent session (root + sub-agent). Each session is 1:1 with a `ModelRuntime` instance (provider-agnostic, see below). See [design/session.md](session.md) and [design/agent.md](agent.md).
45
+
46
+ Depends directly on 10+ modules: ModelRuntime / Database / SessionStore / UILogBuilder / AgentLoader / MdLoader / WorkspaceTools / ConversationRepair / Compact / Config.
47
+
48
+ Responsibilities:
49
+ 1. Agent instance lifecycle (create / restore / release)
50
+ 2. Session state machine (idle / running / compacting)
51
+ 3. Message routing (user→agent / agent→agent)
52
+ 4. The turn-execution loop + compaction
53
+
54
+ Five concerns were split into their own files (SessionManager keeps thin pass-throughs and owns the instances, each taking `this` as host): **SessionUIStore** — UIState reducer + persistence + event dispatch (`emitEvent` → listener); **SessionQueryStore** — read-only session-metadata queries + the row→SessionInfo status projection; **SessionAgentBuilder** — agent.yaml → ModelRuntime + system prompt + tools + /context metadata; **SessionSkillCommands** — skill-backed slash-command permission resolution; **SessionStateStore** — `rawMessages` disk persistence (save/load agent state). See below.
55
+
56
+ ### ModelRuntime — LLM interaction layer (provider-agnostic)
57
+
58
+ Files: `agents/model-runtime.ts` (interface + dispatcher), `agents/bedrock-agent.ts` (the `aws-bedrock-claude-invoke` implementation).
59
+
60
+ **Interface**:
61
+
62
+ ```ts
63
+ interface ModelRuntime {
64
+ messages: AnthropicMessage[]
65
+ run(input, opts?): AsyncGenerator<AgentEvent>
66
+ }
67
+ createModelRuntime(providerId: string, cfg: ModelRuntimeConfig): ModelRuntime
68
+ ```
69
+
70
+ **Dispatch**: `agent.yaml`'s `model.provider` is looked up against `~/.halo/global/models/<providerId>.yaml`; the dispatcher's switch instantiates the matching implementation.
71
+
72
+ **Modality capabilities**: Each model in the manifest declares `capabilities.image` / `capabilities.video` / `capabilities.audio` (boolean). SessionManager checks `modelSupportsImage()` at session creation in two places: (1) `createWorkspaceTools()` is passed `supportsVision` so the `view_image` tool is dropped from the tool list when the model can't ingest vision blocks — no exposed tool, no errant call, no provider 400; (2) user-supplied images on inbound messages are stripped at `buildInput()` with a text notice. Query functions: `config.ts` exports `modelSupportsImage()` / `modelSupportsVideo()` / `modelSupportsAudio()`.
73
+
74
+ **Current providers**:
75
+ - `aws-bedrock-claude-invoke` → `BedrockAgent` (uses the Bedrock InvokeModel API, non-streaming)
76
+
77
+ **Adding a new provider**:
78
+ 1. Add a manifest at `models/<providerId>.yaml` (include modality flags)
79
+ 2. Add a case in `model-runtime.ts` returning your runtime class
80
+ 3. Implement `callModel()` (returns `Promise<ModelCallResult>`) and maintain `messages`
81
+
82
+ **Core loop** (`AgentLoop.run()`, shared by all providers):
83
+ 1. Append the user message to `messages`
84
+ 2. `callModel()` → invoke the provider API (non-streaming), get complete response
85
+ 3. Yield `thinking` / `text` / `usage` / `tool_call` events
86
+ 4. `stop_reason=tool_use` → execute tools, yield `tool_result` events → loop
87
+ 5. Otherwise yield a `stop` event and return
88
+
89
+ State: `messages: AnthropicMessage[]` (the full conversation history; external code can mutate it for compact/repair).
90
+
91
+ ### WebSocket Handler — real-time transport layer
92
+
93
+ File: `ws/handler.ts`. See [design/ws.md](ws.md).
94
+
95
+ One `ConnectedClient` per WS connection, holding: `sessionManager` / `agentSessionId` / `terminalManager` / `fileWatcher` / `backgroundSaves`. UI state (messageLog / streamBuffer / tokens) belongs to SessionManager's `UIState`, not the client.
96
+
97
+ Message dispatch:
98
+ - `chat` / `chat:stop` / `subscribe` / `session:clear` / `session:delete` → handled directly in `ws/handler.ts`
99
+ - `command:*` → `dispatchCommand` (`channels/shared/commands.ts`) — same dispatcher every channel uses
100
+ - `terminal:*` → TerminalManager
101
+
102
+ ### EventProcessor — event translation
103
+
104
+ File: `ws/event-processor.ts` (pure functions).
105
+
106
+ `sendWsNotification(event, state, turnId, ctx)` — converts OrchestratorEvent into a WS JSON message and sends it. Called *after* `applyEvent` has already mutated UIState. Does NOT mutate state itself. `bufferDetachedNotification(event, pendingEvents)` is the offline equivalent that buffers structural events for replay.
107
+
108
+ ### Broadcast — cross-client server-pushed events
109
+
110
+ File: `ws/broadcast.ts`. Per-client `sendJson` is for chat-stream events that belong to one socket; `broadcast(event)` fans an event out to *every* connected admin client. Used for shared-state changes that any tab/browser cares about: evolution run state transitions, cron job/run state, channel binding changes. Replaces the `setInterval(fetch, ...)` polling pattern that earlier admin views used to detect server-side changes.
111
+
112
+ `setBroadcastWss(wss)` is called once at server boot from `index.ts`; modules then `import { broadcast }` without threading the wss handle through. Callers that emit:
113
+
114
+ - `evolution/ticker.ts` — diffs db status against an in-memory snapshot every 30s, emits `evolution:run_changed` / `evolution:apply_changed` for wrapper-driven transitions (the wrapper child can't reach the parent's wss directly)
115
+ - `routes/evolution.ts` — REST mutations (approve/reject/retry) emit immediately so user-action latency is "instant"
116
+ - `cron/runner.ts` — `runJob` insert + `finalize` emit `cron:run_changed`; `reconcileFromDb` emits `cron:job_changed kind=reconciled|deleted` for out-of-band db edits (e.g. the cron skill writing the db directly)
117
+ - `routes/cron.ts` — REST mutations emit `cron:job_changed kind=created|updated|deleted` immediately
118
+
119
+ ### UILogBuilder — UI state reducer
120
+
121
+ File: `sessions/ui-log-builder.ts` (pure functions).
122
+
123
+ `applyEvent(state, event) → ApplyResult` is the core reducer. No I/O, no WS — callers decide when to persist.
124
+
125
+ UIState fields: `messageLog` / `streamBuffer` / `contextTokens` / `outputTokens` / `subSessionLogs` / `turnToolCalls` / `turnContentBlocks` / ...
126
+
127
+ ### SessionUIStore — UI-log state + event routing
128
+
129
+ File: `agents/session-ui-store.ts`. The stateful host around UILogBuilder, split out of SessionManager. Owns the per-root-session UIState map, the debounced persist timers, and the event-listener registry; drives `applyEvent` and decides flush-vs-debounce. SessionManager constructs it with `this` as the `SessionUIStoreHost` (db / workspaceRoot / in-memory session lookup / delete tombstone / the single tombstone-honouring `persistSessionFile`) and exposes same-named thin pass-throughs so external callers stay unchanged. Dependency is one-directional: store → host, never the reverse.
130
+
131
+ ### SessionQueryStore — read-only session-metadata queries
132
+
133
+ File: `agents/session-query-store.ts`. Stateless; split out of SessionManager alongside SessionUIStore. Owns `listSessions` / `listDescendants` / `findLatestByPrefix` / `getSessionById` / `getSessionTree` and the `toSessionInfo` projection. All SQLite reads plus one cross-read: status derivation fuses the row's `stopped_at`, the in-memory run map (`promise !== null` ⇒ running), and a batched active-child lookup — in that precedence. Takes `this` as `SessionQueryStoreHost` (db / workspaceRoot / in-memory session lookup); SessionManager keeps same-named pass-throughs so the 20+ `getSessionById` callers and the `SessionManagerInternals` contract are unchanged.
134
+
135
+ ### SessionAgentBuilder — agent construction pipeline
136
+
137
+ File: `agents/session-agent-builder.ts`. Stateless. Turns an agentId + agent.yaml into a live ModelRuntime plus system prompt, tool set, context/thinking config, and `/session context` metadata (`AgentMeta`/`BuiltAgent` are defined + exported here). `createSession` / `ensureSession` / the rerun paths call `buildAgentInstance`. Host surface: workspaceRoot / db / `createSessionTools`.
138
+
139
+ ### SessionSkillCommands — skill-command permissions
140
+
141
+ File: `agents/session-skill-commands.ts`. Stateless, pure reads. Resolves which skill-backed slash commands an agent may invoke (yaml `skills:` whitelist ∩ not-disabled ∩ access gate via SKILL.md `requiresAccess`). Source of truth for the slash-suggest popup and the server-side check in `execSkillCommand`. Host surface: workspaceRoot / db.
142
+
143
+ ### SessionStateStore — rawMessages disk persistence
144
+
145
+ File: `agents/session-state-store.ts`. Stateless. Saves/loads an agent's `rawMessages` (LLM-facing history) to its `.json` file via read-merge-write — the `rawMessages` half of session persistence (SessionUIStore owns the UI-log half; both write the same file). `saveAgentState` takes a narrow `SavableSession` (6 fields), not the full AgentSession. Host surface: workspaceRoot + `isSessionDeleted` (the tombstone short-circuit so a late save can't resurrect a deleted file).
146
+
147
+ ## Helper modules
148
+
149
+ ### SessionStore — disk persistence
150
+ File: `sessions/session-store.ts`. Path: `.halo/sessions/{agentId}/{sessionId}.json`. Format described in [design/storage.md](storage.md).
151
+
152
+ ### AgentLoader — agent config loader
153
+ File: `agents/agent-loader.ts`. `loadAgentYaml(agentId, wsRoot)` loads with workspace > global precedence.
154
+
155
+ ### MdLoader — prompt assembly
156
+ File: `prompts/md-loader.ts`. See [design/prompt-system.md](prompt-system.md).
157
+
158
+ ### WorkspaceTools — agent tool set
159
+ File: `tools/workspace-tools.ts`. 9 tools: file_read / view_image / file_write / file_edit / file_list / shell_exec / grep / glob / web_fetch. See [dev/tools.md](../dev/tools.md).
160
+
161
+ ### BackgroundHandler — post-disconnect event buffer
162
+ File: `ws/background-handler.ts`. After WS disconnect it takes over events and buffers structural events for replay on reconnect.
163
+
164
+ ### WorkspaceWatcher — file watching
165
+ File: `ws/file-watcher.ts`. chokidar → 300ms debounce + per-path Map dedup → callback. Ignores node_modules / .git / .next / dist. `.halo/sessions/` and `.halo/logs/` are **not** ignored — the dedup keeps volume low, and the front-end drops `change` events for files not open in the editor, so Explorer can reflect session deletions/creations while chat streaming stays cheap.
166
+
167
+ ### TerminalManager — PTY management
168
+ File: `ws/terminal-manager.ts`. Spawns a shell via node-pty. On disconnect, detaches with a 50KB ring buffer; replays on reconnect.
169
+
170
+ ### Self-Evolution — workspace prompt-tuning loop
171
+ Files: `evolution/{ticker,evo-wrapper,enqueue,spawn,archive}.ts`, `db/evo-db.ts`, `routes/evolution.ts`. Internal agents `__evo_agent__` / `__score__` / `__apply_agent__` (in `templates/agents/`) drive a 12-phase orchestration: snapshot → evo drafts → wrapper dry-runs → scorer grades → reviewer approves → apply agent merges → wrapper history-snapshots + cps to main. Per-task wrapper Node child process owns all sub-cli calls; ticker is stateless and lives in the server. State in `~/.halo/global/evo.db`. See [plans/self-evolution.md](../plans/self-evolution.md).
172
+
173
+ ## REST routes
174
+
175
+ See [dev/api.md](../dev/api.md).
176
+
177
+ ## @turmind/halo-cli package
178
+
179
+ Standalone terminal client — imports agent-core modules from `@turmind/halo-server` via subpath exports, bypassing all HTTP/WS infrastructure.
180
+
181
+ Entry: `packages/cli/src/index.ts` → `harness.ts` (wraps SessionManager) → `cli.ts` (non-interactive) / `tui.ts` (interactive readline).
182
+
183
+ Uses `dispatchCommand()` from `channels/shared/commands.ts` for `/session new`, `/session list`, `/session switch`, `/session compact`, etc. Session prefix: `cli_`. Sessions are persisted identically to admin/channel sessions and are visible in the admin panel.
184
+
185
+ See [guide/cli.md](../guide/cli.md).
186
+
187
+ ## @turmind/halo-core package
188
+
189
+ ### Workspace (`core/src/workspace/workspace.ts`)
190
+ Methods: `init(name)` / `readFile(path)` / `writeFile(path, content)` / `listFiles(dir?, recursive?)` / `fileExists(path)` / `validatePath(path)` (path-traversal check).
191
+
192
+ ### GitManager (`core/src/workspace/git-manager.ts`)
193
+ Methods: `init(dir)` / `commitAll(dir, msg)` / `getDiff(dir, path)`. Backed by simple-git.
194
+
195
+ ## Dependency matrix
196
+
197
+ ```
198
+ │ SM │ WS │ EP │ BG │ Routes │ Core │ CLI │
199
+ ─────────────────────────┼────┼────┼────┼────┼────────┼──────┼─────┤
200
+ ModelRuntime │ ✦ │ │ │ │ │ │ │
201
+ SessionManager │ │ ✦ │ │ │ │ │ ✦ │
202
+ SessionUIStore │ ✦ │ │ │ │ │ │ │
203
+ SessionQueryStore │ ✦ │ │ │ │ │ │ │
204
+ SessionAgentBuilder │ ✦ │ │ │ │ │ │ │
205
+ SessionSkillCommands │ ✦ │ │ │ │ │ │ │
206
+ SessionStateStore │ ✦ │ │ │ │ │ │ │
207
+ EventProcessor │ │ ✦ │ │ ✦ │ │ │
208
+ UILogBuilder │ ✦ │ │ │ │ │ │
209
+ SessionStore │ ✦ │ ✦ │ ✦ │ ✦ │ ✦(Ses) │ │
210
+ AgentLoader │ ✦ │ │ │ │ ✦(AC) │ │
211
+ MdLoader │ ✦ │ │ │ │ ✦(AC) │ │
212
+ WorkspaceTools │ ✦ │ │ │ │ │ │
213
+ ConversationRepair │ ✦ │ │ │ │ │ │
214
+ Compact │ ✦ │ │ │ │ │ │
215
+ Config │ ✦ │ ✦ │ │ │ ✦ │ │ ✦ │
216
+ Database │ ✦ │ ✦ │ │ │ ✦ │ │ │
217
+ Workspace │ │ │ │ │ ✦(F) │ │
218
+ GitManager │ │ │ │ │ ✦(F) │ │
219
+ WorkspaceWatcher │ │ ✦ │ │ │ │ │
220
+ TerminalManager │ │ ✦ │ │ │ │ │
221
+ BackgroundHandler │ │ ✦ │ │ │ │ │
222
+ ```
223
+
224
+ ## Storage responsibility
225
+
226
+ SQLite only holds metadata indexes; all content lives on the filesystem.
227
+
228
+ | Medium | Writer | Contents |
229
+ |---|---|---|
230
+ | SQLite `sessions` | SessionRoutes, WS handler | Frontend session metadata |
231
+ | SQLite `agent_sessions` | SessionManager | Agent session metadata (root + children; includes `working_dir` and `access_level` columns) |
232
+ | `.halo/sessions/{agentId}/{sid}.json` | SessionManager, SessionStore | Session messages |
233
+ | `.halo/sessions/{agentId}/{sid}.events.jsonl` | SessionManager | Event audit log |
234
+ | `.halo/agents/{id}/agent.yaml` | AgentConfigRoutes | Agent YAML (workspace scope) |
235
+ | `.halo/agents/{id}/AGENT.md` | AgentConfigRoutes | Agent personality |
236
+ | `~/.halo/global/agents/{id}/agent.yaml` | AgentConfigRoutes | Agent YAML (global scope) |
237
+ | `.halo/skills/{id}/SKILL.md` | SkillRoutes | Skill definition (workspace scope) |
238
+ | `~/.halo/global/skills/{id}/SKILL.md` | SkillRoutes | Skill definition (global scope) |
239
+ | `~/.halo/secrets/settings.yaml` | SettingsRoutes | Global settings |
240
+ | `<project>/.halo/settings.yaml` | SettingsRoutes | Per-project overrides |
241
+ | `~/.halo/global/models/<provider>.yaml` | Manual edit | Model registry — one file per provider, scanned at startup, used to dispatch to the matching runtime |
242
+ | `~/.halo/global/prompts/{bootstrap,all,root}/*.md` | init.ts seed + user | System prompts |
243
+ | `~/.halo/logs/server.log` or `<ws>/.halo/logs/server.log` | Logger | Server logs (10 MB rotation) |
244
+ | `~/.halo/secrets/channels/channels.db` | All channels | Unified channel accounts (Telegram, Web, WeChat) — see [storage.md](storage.md#channel_accounts) |
245
+
246
+ ## Coupling hot spots
247
+
248
+ **SessionManager (~2260 lines)** — the central hub. Five concerns were carved out into one-directional sibling classes (see Key modules): UI-log/events (SessionUIStore), metadata queries (SessionQueryStore), agent construction (SessionAgentBuilder), skill-command permissions (SessionSkillCommands), rawMessages persistence (SessionStateStore). The carve-out is **complete** — what remains is the genuine high-cohesion core, all bound to the `sessions: Map` mutable state: the turn-execution loop, session lifecycle, the concurrency guards (`locks` init-mutex + `deletedSessionIds` delete-tombstone), and compaction.
249
+
250
+ Compaction stays in, by deliberate decision at two levels. Cluster-level: it's bidirectionally interwoven with the turn loop (a beforeCallModel hook that itself runs a turn) and mutates shared per-session state, so extraction would widen the host interface and tangle control flow. Function-level: `selfCompactSession` is one connected chain (compute cut → summarize via LLM → rebuild `[summary+recent]` → write back) — pulling the pure bits into compact.ts would sever a coherent single-use method and scatter it across files, trading cohesion for testability. A long, single-purpose, sequentially-coupled method is fine; the inherent complexity of session handling doesn't shrink by relocating it.
251
+
252
+ **Medium: WS Handler** — depends on SessionManager for all agent operations, owns connection lifecycle, compact orchestration, session switching, terminal/file watcher. It used to have two-way state sync with SessionManager (resolved: UIState now belongs to SessionManager).
253
+
254
+ **Low: REST Routes / Core / tools** — no cross-dependencies; interact via DB or filesystem.
255
+
256
+ **Dual writes**: Session data lives in both SQLite and the on-disk JSON; deletion must synchronise all three (SQLite → JSON → JSONL).
257
+
258
+ ## Frontend (short version)
259
+
260
+ ```
261
+ packages/admin
262
+ ├── Next.js 15 static export → out/ → served by Hono
263
+ ├── Talks to the backend via REST + WebSocket
264
+ └── No SSR, pure client SPA
265
+ ```
266
+
267
+ Code structure:
268
+ - `app/` — Next.js app router entry
269
+ - `features/` — UI modules grouped by domain (agents / chat / editor / explorer / terminal / workspace / settings / skills / auth)
270
+ - `shared/` — cross-feature resources (stores / components / ws-handlers / ws-client / api-client / types / utils)
@@ -0,0 +1,151 @@
1
+ # Background Session Event Dispatch
2
+
3
+ When the user hits `/session new` while a sub-agent is still running — the old session's agent has to stay alive, its events have to be routed to the right session file, and the user has to see everything seamlessly when they switch back.
4
+
5
+ ## Problem
6
+
7
+ SessionManager emits events through a per-tree event listener system (`eventListeners: Map<rootId, Set<handler>>`). The WS handler converts events into WS messages for the frontend. When the user hits `/session new`:
8
+ - A new session starts (fresh conversation)
9
+ - The old session's agent may still be running
10
+ - Old-session events **must not** leak into the new session
11
+ - Events must persist to the old session's file
12
+ - Switching back should show the full history
13
+
14
+ ## Architecture
15
+
16
+ ### Event flow layers
17
+
18
+ ```
19
+ SessionManager.emitEvent(sessionId, event)
20
+ ├─ appendEventLog(sessionId, event) ← JSONL audit trail
21
+ ├─ reduceIntoUIState(rootId, event) ← updates UIState (messageLog, tokens, etc.)
22
+ └─ eventListeners.get(rootId) → forEach(listener(event, state, turnId))
23
+ ```
24
+
25
+ **Key detail**: `emitEvent` first mutates the UIState via `reduceIntoUIState`, then calls listeners with the *pre-mutation* `turnId`. Listeners receive already-applied state — they do NOT mutate state themselves.
26
+
27
+ ### Three event handler states
28
+
29
+ | State | Listener | Events go to |
30
+ |---|---|---|
31
+ | **Connected** | `registerEventListener(rootId, handler)` | `sendWsNotification(event, state, turnId, ctx)` → WS JSON |
32
+ | **Background** (after `/session new`) | Inline `bufferDetachedNotification` closure | `pendingEvents[]` (structural events only for later replay) |
33
+ | **Detached** (WS disconnect) | Same `bufferDetachedNotification` pattern | `pendingEvents[]` on `DetachedSession` |
34
+
35
+ State is NOT duplicated — all handlers read from `SessionManager.getUIState(rootId)`.
36
+
37
+ ## `/session new` (session:clear) flow
38
+
39
+ Source: `packages/server/src/ws/handler.ts` (`session:clear` case)
40
+
41
+ ```
42
+ User clicks /session new
43
+ │
44
+ ▼
45
+ session:clear handler
46
+ │
47
+ ├─ ctx.saveSession() ← persist current UIState to file
48
+ │
49
+ ├─ create inline bgHandler:
50
+ │ (event, state, turnId) => bufferDetachedNotification(event, pendingEvents)
51
+ │
52
+ ├─ sm.registerEventListener(agentSessionId, bgHandler)
53
+ │ ← old session's future events go to the buffer
54
+ │
55
+ ├─ backgroundSaves.set(prevSessionId, () => ctx.saveSession())
56
+ │ ← register a save fn for when user switches back or disconnects
57
+ │
58
+ ├─ reset client state (agentSessionId=null, sessionId=null)
59
+ └─ send { type: 'session:cleared' }
60
+ ```
61
+
62
+ ### `createBackgroundHandler` (utility)
63
+
64
+ Location: `packages/server/src/ws/background-handler.ts`
65
+
66
+ A helper used in contexts outside session:clear (e.g. detached sessions). Takes 3 args:
67
+
68
+ ```typescript
69
+ createBackgroundHandler(sessionId: string, projectPath: string | null, sm: SessionManager)
70
+ → { handler, save, pendingEvents }
71
+ ```
72
+
73
+ - **handler**: `(event, state, turnId) => bufferDetachedNotification(event, pendingEvents)` — buffers structural WS events
74
+ - **save**: reads live UIState from `sm.getUIState(sessionId)`, creates a snapshot, writes to file
75
+ - **pendingEvents**: array of buffered structural events for replay on reconnect
76
+
77
+ The handler does NOT hold its own messageLog/streamBuffer/tokenState. All state lives in SessionManager's UIState, maintained by `reduceIntoUIState → applyEvent`.
78
+
79
+ ### When save fires
80
+
81
+ Background state persists in three scenarios:
82
+
83
+ 1. **`backgroundSaves.get(sessionId)?.()` in subscribe** — user switches back to the old session; called before re-attaching the listener
84
+ 2. **`backgroundSaves` flush on disconnect** — WS closes, flush every pending bg
85
+ 3. **Periodic auto-save by UIState reducer** — `complete`, `tool_call`, `tool_result`, `usage` events trigger save via the reducer
86
+
87
+ ## Subscribe (switching back) flow
88
+
89
+ ```
90
+ User clicks old session in sidebar
91
+ │
92
+ ▼
93
+ Frontend: { type: 'subscribe', sessionId: oldId, projectId }
94
+ │
95
+ ▼
96
+ subscribe handler
97
+ │
98
+ ├─ if switching session → saveSession(current)
99
+ │
100
+ ├─ backgroundSaves.get(sessionId)?.() ← flush bg state to disk
101
+ │ backgroundSaves.delete(sessionId)
102
+ │
103
+ ├─ re-attach the event listener to this session tree
104
+ │ (replaces the bg buffer handler with the live WS handler)
105
+ │
106
+ ├─ Load UIState from SessionManager (or from file if not in memory)
107
+ │
108
+ └─ Send state:snapshot carrying the full messageLog
109
+ ```
110
+
111
+ ## When disconnect happens while background is still running
112
+
113
+ ```
114
+ disconnect handler
115
+ │
116
+ ├─ (if agent running) → detach session with bufferDetachedNotification
117
+ ├─ (otherwise) → saveSession
118
+ │
119
+ └─ flush every backgroundSaves
120
+ for (const [sid, saveFn] of backgroundSaves)
121
+ saveFn()
122
+ ```
123
+
124
+ ## Relevant files
125
+
126
+ | File | Relevant code |
127
+ |---|---|
128
+ | `packages/server/src/ws/background-handler.ts` | `createBackgroundHandler()` utility |
129
+ | `packages/server/src/ws/handler.ts` | `session:clear` case — inline bgHandler + `backgroundSaves` registration |
130
+ | `packages/server/src/ws/event-processor.ts` | `sendWsNotification()`, `bufferDetachedNotification()` |
131
+ | `packages/server/src/agents/session-manager.ts` | `emitEvent()`, `reduceIntoUIState()`, `registerEventListener()` |
132
+ | `packages/server/src/sessions/ui-log-builder.ts` | `applyEvent()`, `createSaveSnapshot()`, UIState type |
133
+ | `packages/server/src/sessions/session-store.ts` | `saveSessionToFile()`, `loadSessionMessages()` |
134
+
135
+ ## Historical bug fixes (2026-04-20/21)
136
+
137
+ ### 1. After `/session new`, sub-agent events routed to the wrong handler
138
+ **Root cause**: old code captured `const onEvent = this.eventHandler` at session start. When the handler was replaced, already-running sub-agents still used the old reference.
139
+ **Fix**: switched to `emitEvent()` which does a live lookup on `eventListeners.get(rootId)`. New listeners immediately receive events from running sub-agents.
140
+
141
+ ### 2. `client.messageLog` polluted by stale sub-agent events
142
+ **Root cause**: before #1 was fixed, sub-agent events went through the live WS handler, pushing messages into the new session's state.
143
+ **Fix**: (a) fixing #1 made event routing tree-scoped. (b) session:clear explicitly saves before switching and resets client state.
144
+
145
+ ### 3. `createBackgroundHandler` missing a return
146
+ **Root cause**: the function defined `handler` and `save` locally but did not `return { handler, save }`.
147
+ **Fix**: add `return { handler, save, pendingEvents }` at the end.
148
+
149
+ ### 4. Stream buffer not flushed before saveSession in session:clear
150
+ **Root cause**: in-flight stream text wasn't captured before save.
151
+ **Fix**: UIState reducer now incrementally persists on every structural event — stream text is folded into messageLog by `applyEvent` before save triggers.
@@ -0,0 +1,58 @@
1
+ # Command — Design
2
+
3
+ Unified command processing layer — all channels (WS, WeChat, Telegram, Web, CLI/TUI) share a single dispatch path.
4
+
5
+ ## Architecture
6
+
7
+ ```
8
+ Channel handlers Shared layer SessionManager
9
+ ────────────── ──────────── ──────────────
10
+ WS handler ─────┐
11
+ WeChat handler ──┼──► dispatchCommand() ──────────► SM methods
12
+ Telegram handler ┤ (channels/shared/commands.ts) (compact, create, delete, …)
13
+ Web handler ─────┤
14
+ CLI/TUI ─────────┘ │
15
+ └── execSkillCommand() (skill fallback)
16
+ (commands/skill-command.ts)
17
+ ```
18
+
19
+ WS handler special-cases `/session compact` because it needs UI progress callbacks: `compact` → `sm.compactSession(sid, { onProgress })` directly. Every other command routes through `dispatchCommand`.
20
+
21
+ ## Key modules
22
+
23
+ | Module | File | Purpose |
24
+ |---|---|---|
25
+ | `dispatchCommand` | `channels/shared/commands.ts` | Single switch routing all slash commands to exec functions |
26
+ | `CommandRegistry` | `commands/registry.ts` | Listing-only — stores `CommandDescriptor` for REST `/api/commands` discovery |
27
+ | `scanSkillDescriptors` | `commands/skill-command.ts` | Scan skill dirs → return `CommandDescriptor[]` for registry |
28
+ | `execSkillCommand` | `commands/skill-command.ts` | Execute a skill slash command (read SKILL.md, render, send to SM) |
29
+
30
+ ## Command dispatch flow
31
+
32
+ 1. Channel receives a slash command from user input (or WS `command:<name>` message)
33
+ 2. Channel builds a `CommandContext` (shared interface: sm, userId, sessionPrefix, accessLevel, workspacePath, lang)
34
+ 3. Channel calls `dispatchCommand(ctx, '/command', args)`
35
+ 4. `dispatchCommand` switch: `/help` and `/evo` call `exec*` directly; object commands (`/session`, `/agent`, `/skill`, `/workspace`) and the default case route through `routeObjectOrSkill`
36
+ 5. `routeObjectOrSkill` tries the builtin noun-verb table first (`SUBCOMMAND_ROUTES`, per-verb access via `verbAccessMap`), else falls through to `execSkillCommand` for the same-named skill — verb/permission model in [requirements/command.md](../requirements/command.md)
37
+ 6. Returns `CommandResult { text, switchTo?, workspace? }` — channel formats and sends to user
38
+
39
+ `scanSkillDescriptors` drops skill commands that collide with a built-in (or another skill) at scan time, so dispatch and the discovery API stay consistent — see [requirements/command.md](../requirements/command.md#conflict-detection).
40
+
41
+ ## File structure
42
+
43
+ ```
44
+ packages/server/src/commands/
45
+ types.ts — CommandDescriptor interface
46
+ registry.ts — CommandRegistry (listing-only, for REST discovery)
47
+ skill-command.ts — scanSkillDescriptors() + execSkillCommand()
48
+ index.ts — Registry singleton + built-in descriptor registration
49
+
50
+ packages/server/src/channels/shared/
51
+ commands.ts — dispatchCommand(), all exec* functions, CommandContext, CommandResult
52
+ ```
53
+
54
+ ## Frontend integration
55
+
56
+ - `features/chat/slash-commands.ts` — `refreshCommands(projectId)` pulls from REST API and merges client-only fallbacks
57
+ - `features/chat/use-chat.ts` — handles client-only commands locally; server commands sent as `command:<name>` via WS
58
+ - `features/chat/chat-panel.tsx` — calls `refreshCommands()` on project switch
@@ -0,0 +1,88 @@
1
+ # Cron — Scheduled Tasks Design
2
+
3
+ Run an agent on a schedule (daily report, periodic broadcast) and fan the result out to channels. Jobs live in the global db, fire via [croner](https://github.com/hexagon/croner), and execute as a fresh `halo cli` child process.
4
+
5
+ ## Architecture
6
+
7
+ ```
8
+ cron_jobs (global db) registered at boot:
9
+ │ ┌── slack/cron-dispatcher
10
+ ▼ reloadAll / reconcileFromDb ├── feishu/cron-dispatcher
11
+ runner.ts ──── croner schedule ──┐ ├── telegram/cron-dispatcher
12
+ │ │ └── wechat/cron-dispatcher
13
+ │ fire │ ▲
14
+ ▼ │ │ registerCronDispatcher
15
+ spawn `halo cli -n -s cron-<id>` │ dispatcher.ts (registry)
16
+ │ stdout │ ▲
17
+ ▼ └──────────────┘ dispatchToTargets(text, targets)
18
+ cron_runs (audit row + log file) ───────────────┘
19
+ ```
20
+
21
+ Two halves, cleanly split:
22
+ - **runner.ts** — the scheduler. Owns the croner instances, spawns the cli, persists audit rows.
23
+ - **dispatcher.ts** — a channel-agnostic registry. Knows how to look up a dispatcher by `channelType` and hand off; knows nothing about any specific channel.
24
+
25
+ ## Scheduling model
26
+
27
+ Schedules are **durable** — the source of truth is the `cron_jobs` table; in-memory croner state is rebuilt on every server boot (`startCronDaemon` → `reloadAll`). A restart never loses a schedule.
28
+
29
+ - **Recurring**: standard cron expression (`job.schedule`), optional `timezone`.
30
+ - **One-shot**: `job.runAt` (epoch ms) — croner fires once at that instant. After it completes, `finalize` sets `enabled=0` so it never re-fires. A `runAt` already in the past at schedule time is marked `lastRunStatus='missed'` and disabled (rather than firing immediately or retrying every reconcile).
31
+
32
+ ### Hot-reload + out-of-band edits
33
+
34
+ REST route mutations call `scheduleJob` / `unscheduleJob` directly. But the `cron` skill (and manual ops) edit `cron.db` over a *different* sqlite connection. `reconcileFromDb` (10s timer) catches those:
35
+
36
+ - Fast path: `PRAGMA data_version` flips only when *another* connection commits. If unchanged since last pass, skip the full select entirely — one pragma read (~µs) vs. a select-all + per-row fingerprint compare.
37
+ - On change: diff db rows against the in-memory `_fingerprint` map (`enabled|schedule|runAt|timezone`); schedule new rows, unschedule deleted ones, re-instantiate croner only for rows whose fingerprint changed. Broadcasts a coalesced `cron:job_changed` per affected job.
38
+
39
+ ## Execution (`runJob`)
40
+
41
+ Each fire:
42
+
43
+ 1. **Concurrency guard**: if the same `jobId` is already running in this process (in-memory `_inflight` set), insert a `cron_runs` row with `status='skipped'`, `failureReason='previous run still in progress'`, broadcast, and bail without spawning. Applies to both scheduled fires and manual run-now clicks. Two overlapping cli children would double-write the same `cron-<jobId>` on-disk session state; SessionManager's per-session lock is in-process and can't see across cli children, so this set is the cheapest place to enforce serialization. Lost on server restart by design — no stale-entry cleanup needed.
44
+ 2. Insert a `cron_runs` row with `status='running'` up front (UI sees it mid-flight); broadcast `cron:run_changed`.
45
+ 3. Check `job.workspacePath` exists — fail loudly if the workspace was moved/deleted.
46
+ 4. Spawn `halo cli -a <agent> -s cron-<jobId> -w <workspace>`, prompt on **stdin** (avoids Windows argv length limits). The stable `cron-<jobId>` session id means each fire *resumes the same session*, so the conversation accumulates and is reviewable in the admin Sessions tab.
47
+ 5. Tee stdout + stderr live to `~/.halo/global/logs/cron/<runId>.log`; capture stdout to memory for dispatch.
48
+ 6. Enforce a **3600s timeout** via a Node timer (kills child, reports exit code 124 — cross-platform, not the Linux-only `timeout(1)`). Generous because overlap-of-the-same-job is already blocked up-front in step 1; this is just the long-stop reaper for a truly stuck child.
49
+ 7. Classify: exit 124 → `timeout`; non-zero → `failed`; zero but empty stdout → `failed` ("nothing to dispatch"); else `succeeded`.
50
+ 8. On success only, `dispatchToTargets(stdout, targets)`. If every target fails, downgrade to `failed`.
51
+ 9. `finalize`: write the terminal `cron_runs` row, update `cron_jobs.lastRun*`, disable one-shot jobs, broadcast. The `_inflight` entry is released in `finally`, regardless of outcome.
52
+
53
+ The cli executable is resolved via `resolveHaloCli()` (`$HALO_CLI` override; `halo.cmd` on Windows to dodge the GUI `Halo.exe` on PATH).
54
+
55
+ ## Dispatch model
56
+
57
+ `dispatcher.ts` is a registry keyed by `channelType`. Each channel ships a `CronChannelDispatcher` inside its own directory and calls `registerCronDispatcher(...)` once at boot — **adding a channel never edits dispatcher.ts**.
58
+
59
+ ```ts
60
+ interface CronChannelDispatcher {
61
+ channelType: string
62
+ dispatch: (accountId, text, chatId?) => Promise<DispatchResult[]>
63
+ listTargets?: () => CronTargetOption[]
64
+ }
65
+ ```
66
+
67
+ - `dispatchToTargets` is **never throws** — every target's outcome (ok / error) is captured into a `DispatchResult[]` persisted to `cron_runs.dispatch_results`. One channel being down doesn't block the others.
68
+ - **`chatId` semantics**: when a job is created from inside a chat, the target pins to that conversation (`chatId` set). When unset, the channel's own fan-out / default-recipient logic decides (e.g. Telegram fans out to its whitelist; Slack/Feishu require an explicit `chatId`).
69
+ - `listTargets()` aggregates across all dispatchers (`listAllCronTargets`) to feed the admin create-form dropdown without the routes knowing any channel.
70
+
71
+ ## Retention
72
+
73
+ - **Runs**: keep newest `RUNS_PER_JOB_KEEP` (100) rows per job; older pruned on each new run via a single `DELETE … NOT IN (SELECT … LIMIT 100)` over the `(job_id, started_at)` index.
74
+ - **Logs**: files older than 30 days deleted on a daily timer (db row kept; UI shows "log unavailable"). `cron_runs` rows older than 60 days are also dropped.
75
+
76
+ ## Key files
77
+
78
+ - `packages/server/src/cron/runner.ts` — scheduler: croner lifecycle, `runJob` spawn/capture/finalize, reconcile, retention.
79
+ - `packages/server/src/cron/dispatcher.ts` — channel-agnostic dispatch registry (`registerCronDispatcher`, `dispatchToTargets`, `listAllCronTargets`).
80
+ - `packages/server/src/db/cron-db.ts` — `cron_jobs` + `cron_runs` schema, `getCronDb`.
81
+ - `packages/server/src/channels/<ch>/cron-dispatcher.ts` — per-channel `dispatch` + `listTargets`.
82
+ - `packages/server/templates/skills/cron/SKILL.md` — the agent-facing skill that CRUDs `cron_jobs` (edits the db directly; `reconcileFromDb` picks it up).
83
+
84
+ ## Scope
85
+
86
+ Supported: recurring (cron expr) + one-shot (`runAt`) schedules; per-timezone; multi-target fan-out; stable accumulating session per job; run-now (manual trigger); admin UI history with per-run logs + dispatch results.
87
+
88
+ Not supported: sub-minute schedules below croner's resolution; in-process job execution (always a fresh cli child); conversation context carried *between different jobs* (each job has its own `cron-<jobId>` session).