@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.
- package/LICENSE +21 -0
- package/README.md +143 -0
- package/admin-out/404.html +1 -0
- package/admin-out/_next/static/IBP_mwBDf-cPwZSZZGpi8/_buildManifest.js +1 -0
- package/admin-out/_next/static/IBP_mwBDf-cPwZSZZGpi8/_ssgManifest.js +1 -0
- package/admin-out/_next/static/chunks/1d2d5650-810a3ca1ae20cab5.js +18 -0
- package/admin-out/_next/static/chunks/200.3b8f8a74f005a134.js +1 -0
- package/admin-out/_next/static/chunks/220.3dc41cd6269b88e6.js +1 -0
- package/admin-out/_next/static/chunks/298-d6b224b569da4de0.js +1 -0
- package/admin-out/_next/static/chunks/304eda7d.b1ee7f523653e027.js +1 -0
- package/admin-out/_next/static/chunks/330.3b784cf0dcd69163.js +1 -0
- package/admin-out/_next/static/chunks/442.f4302087bddb1ec1.js +1 -0
- package/admin-out/_next/static/chunks/443.d1b289781793f8de.js +25 -0
- package/admin-out/_next/static/chunks/503-7c6d9935df6e0cea.js +1 -0
- package/admin-out/_next/static/chunks/533.5f2d4bfe3d076095.js +1 -0
- package/admin-out/_next/static/chunks/579.a2718ddd395a2f3a.js +1 -0
- package/admin-out/_next/static/chunks/635-3dd0043ed0520b9d.js +1 -0
- package/admin-out/_next/static/chunks/65.859665ba6717967a.js +1 -0
- package/admin-out/_next/static/chunks/661.a4fe93f3f2a5e871.js +1 -0
- package/admin-out/_next/static/chunks/688.4e014ac6cfc7e152.js +1 -0
- package/admin-out/_next/static/chunks/868.0c95dfe283400a00.js +1 -0
- package/admin-out/_next/static/chunks/app/_not-found/page-8e35399c8fd5016d.js +1 -0
- package/admin-out/_next/static/chunks/app/layout-58f773336e6fce61.js +1 -0
- package/admin-out/_next/static/chunks/app/page-cc8043335282d745.js +1 -0
- package/admin-out/_next/static/chunks/db534642.09d3c650ff587175.js +1 -0
- package/admin-out/_next/static/chunks/fb4ec4d6-3fa44f69484f91b9.js +1 -0
- package/admin-out/_next/static/chunks/framework-c3031ae54a63fe16.js +1 -0
- package/admin-out/_next/static/chunks/main-14b48e3c2588354f.js +1 -0
- package/admin-out/_next/static/chunks/main-app-483b436873410ddc.js +1 -0
- package/admin-out/_next/static/chunks/pages/_app-b2fc6e5f920d1b1a.js +1 -0
- package/admin-out/_next/static/chunks/pages/_error-f3696a8f3d67d150.js +1 -0
- package/admin-out/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/admin-out/_next/static/chunks/webpack-8bd7ef88a6527feb.js +1 -0
- package/admin-out/_next/static/css/659eccb5db697b76.css +1 -0
- package/admin-out/_next/static/css/8fce4e1a54dac12a.css +3 -0
- package/admin-out/_next/static/media/19cfc7226ec3afaa-s.woff2 +0 -0
- package/admin-out/_next/static/media/21350d82a1f187e9-s.woff2 +0 -0
- package/admin-out/_next/static/media/8e9860b6e62d6359-s.woff2 +0 -0
- package/admin-out/_next/static/media/ba9851c3c22cd980-s.woff2 +0 -0
- package/admin-out/_next/static/media/c5fe6dc8356a8c31-s.woff2 +0 -0
- package/admin-out/_next/static/media/df0a9ae256c0569c-s.woff2 +0 -0
- package/admin-out/_next/static/media/e4af272ccee01ff0-s.p.woff2 +0 -0
- package/admin-out/favicon.ico +0 -0
- package/admin-out/icon.svg +18 -0
- package/admin-out/index.html +1 -0
- package/admin-out/index.txt +23 -0
- package/admin-out/monaco/vs/_commonjsHelpers-CT9FvmAN.js +1 -0
- package/admin-out/monaco/vs/abap-D-t0cyap.js +1 -0
- package/admin-out/monaco/vs/apex-CcIm7xu6.js +1 -0
- package/admin-out/monaco/vs/assets/css.worker-HnVq6Ewq.js +93 -0
- package/admin-out/monaco/vs/assets/editor.worker-Be8ye1pW.js +26 -0
- package/admin-out/monaco/vs/assets/html.worker-B51mlPHg.js +470 -0
- package/admin-out/monaco/vs/assets/json.worker-DKiEKt88.js +58 -0
- package/admin-out/monaco/vs/assets/ts.worker-CMbG-7ft.js +67731 -0
- package/admin-out/monaco/vs/azcli-BA0tQDCg.js +1 -0
- package/admin-out/monaco/vs/basic-languages/monaco.contribution.js +1 -0
- package/admin-out/monaco/vs/bat-C397hTD6.js +1 -0
- package/admin-out/monaco/vs/bicep-DF5aW17k.js +2 -0
- package/admin-out/monaco/vs/cameligo-plsz8qhj.js +1 -0
- package/admin-out/monaco/vs/clojure-Y2auQMzK.js +1 -0
- package/admin-out/monaco/vs/coffee-Bu45yuWE.js +1 -0
- package/admin-out/monaco/vs/cpp-CkKPQIni.js +1 -0
- package/admin-out/monaco/vs/csharp-CX28MZyh.js +1 -0
- package/admin-out/monaco/vs/csp-D8uWnyxW.js +1 -0
- package/admin-out/monaco/vs/css-CaeNmE3S.js +3 -0
- package/admin-out/monaco/vs/cssMode-CjiAH6dQ.js +1 -0
- package/admin-out/monaco/vs/cypher-DVThT8BS.js +1 -0
- package/admin-out/monaco/vs/dart-CmGfCvrO.js +1 -0
- package/admin-out/monaco/vs/dockerfile-CZqqYdch.js +1 -0
- package/admin-out/monaco/vs/ecl-30fUercY.js +1 -0
- package/admin-out/monaco/vs/editor/editor.main.css +1 -0
- package/admin-out/monaco/vs/editor/editor.main.js +5 -0
- package/admin-out/monaco/vs/editor.api-CalNCsUg.js +903 -0
- package/admin-out/monaco/vs/elixir-xjPaIfzF.js +1 -0
- package/admin-out/monaco/vs/flow9-DqtmStfK.js +1 -0
- package/admin-out/monaco/vs/freemarker2-Cz_sV6Md.js +3 -0
- package/admin-out/monaco/vs/fsharp-BOMdg4U1.js +1 -0
- package/admin-out/monaco/vs/go-D_hbi-Jt.js +1 -0
- package/admin-out/monaco/vs/graphql-CKUU4kLG.js +1 -0
- package/admin-out/monaco/vs/handlebars-OwglfO-1.js +1 -0
- package/admin-out/monaco/vs/hcl-DTaboeZW.js +1 -0
- package/admin-out/monaco/vs/html-Pa1xEWsY.js +1 -0
- package/admin-out/monaco/vs/htmlMode-Bz67EXwp.js +1 -0
- package/admin-out/monaco/vs/ini-CsNwO04R.js +1 -0
- package/admin-out/monaco/vs/java-CI4ZMsH9.js +1 -0
- package/admin-out/monaco/vs/javascript-PczUCGdz.js +1 -0
- package/admin-out/monaco/vs/jsonMode-DULH5oaX.js +7 -0
- package/admin-out/monaco/vs/julia-BwzEvaQw.js +1 -0
- package/admin-out/monaco/vs/kotlin-IUYPiTV8.js +1 -0
- package/admin-out/monaco/vs/language/css/monaco.contribution.js +1 -0
- package/admin-out/monaco/vs/language/html/monaco.contribution.js +1 -0
- package/admin-out/monaco/vs/language/json/monaco.contribution.js +1 -0
- package/admin-out/monaco/vs/language/typescript/monaco.contribution.js +1 -0
- package/admin-out/monaco/vs/less-C0eDYdqa.js +2 -0
- package/admin-out/monaco/vs/lexon-iON-Kj97.js +1 -0
- package/admin-out/monaco/vs/liquid-DqKjdPGy.js +1 -0
- package/admin-out/monaco/vs/loader.js +1368 -0
- package/admin-out/monaco/vs/lspLanguageFeatures-kM9O9rjY.js +4 -0
- package/admin-out/monaco/vs/lua-DtygF91M.js +1 -0
- package/admin-out/monaco/vs/m3-CsR4AuFi.js +1 -0
- package/admin-out/monaco/vs/markdown-C_rD0bIw.js +1 -0
- package/admin-out/monaco/vs/mdx-DEWtB1K5.js +1 -0
- package/admin-out/monaco/vs/mips-CiYP61RB.js +1 -0
- package/admin-out/monaco/vs/monaco.contribution-D2OdxNBt.js +1 -0
- package/admin-out/monaco/vs/monaco.contribution-DO3azKX8.js +1 -0
- package/admin-out/monaco/vs/monaco.contribution-EcChJV6a.js +1 -0
- package/admin-out/monaco/vs/monaco.contribution-qLAYrEOP.js +1 -0
- package/admin-out/monaco/vs/msdax-C38-sJlp.js +1 -0
- package/admin-out/monaco/vs/mysql-CdtbpvbG.js +1 -0
- package/admin-out/monaco/vs/nls.messages-loader.js +1 -0
- package/admin-out/monaco/vs/nls.messages.cs.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.de.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.es.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.fr.js.js +15 -0
- package/admin-out/monaco/vs/nls.messages.it.js.js +15 -0
- package/admin-out/monaco/vs/nls.messages.ja.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.js.js +10 -0
- package/admin-out/monaco/vs/nls.messages.ko.js.js +25 -0
- package/admin-out/monaco/vs/nls.messages.pl.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.pt-br.js.js +6 -0
- package/admin-out/monaco/vs/nls.messages.ru.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.tr.js.js +15 -0
- package/admin-out/monaco/vs/nls.messages.zh-cn.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.zh-tw.js.js +15 -0
- package/admin-out/monaco/vs/objective-c-CntZFaHX.js +1 -0
- package/admin-out/monaco/vs/pascal-r6kuqfl_.js +1 -0
- package/admin-out/monaco/vs/pascaligo-BiXoTmXh.js +1 -0
- package/admin-out/monaco/vs/perl-DABw_TcH.js +1 -0
- package/admin-out/monaco/vs/pgsql-me_jFXeX.js +1 -0
- package/admin-out/monaco/vs/php-D_kh-9LK.js +1 -0
- package/admin-out/monaco/vs/pla-VfZjczW0.js +1 -0
- package/admin-out/monaco/vs/postiats-BBSzz8Pk.js +1 -0
- package/admin-out/monaco/vs/powerquery-Dt-g_2cc.js +1 -0
- package/admin-out/monaco/vs/powershell-B-7ap1zc.js +1 -0
- package/admin-out/monaco/vs/protobuf-BmtuEB1A.js +2 -0
- package/admin-out/monaco/vs/pug-BRpRNeEb.js +1 -0
- package/admin-out/monaco/vs/python-Cr0UkIbn.js +1 -0
- package/admin-out/monaco/vs/qsharp-BzsFaUU9.js +1 -0
- package/admin-out/monaco/vs/r-f8dDdrp4.js +1 -0
- package/admin-out/monaco/vs/razor-BYAHOTkz.js +1 -0
- package/admin-out/monaco/vs/redis-fvZQY4PI.js +1 -0
- package/admin-out/monaco/vs/redshift-45Et0LQi.js +1 -0
- package/admin-out/monaco/vs/restructuredtext-C7UUFKFD.js +1 -0
- package/admin-out/monaco/vs/ruby-CZO8zYTz.js +1 -0
- package/admin-out/monaco/vs/rust-Bfetafyc.js +1 -0
- package/admin-out/monaco/vs/sb-3GYllVck.js +1 -0
- package/admin-out/monaco/vs/scala-foMgrKo1.js +1 -0
- package/admin-out/monaco/vs/scheme-CHdMtr7p.js +1 -0
- package/admin-out/monaco/vs/scss-C1cmLt9V.js +3 -0
- package/admin-out/monaco/vs/shell-ClXCKCEW.js +1 -0
- package/admin-out/monaco/vs/solidity-MZ6ExpPy.js +1 -0
- package/admin-out/monaco/vs/sophia-DWkuSsPQ.js +1 -0
- package/admin-out/monaco/vs/sparql-AUGFYSyk.js +1 -0
- package/admin-out/monaco/vs/sql-32GpJSV2.js +1 -0
- package/admin-out/monaco/vs/st-CuDFIVZ_.js +1 -0
- package/admin-out/monaco/vs/swift-n-2HociN.js +3 -0
- package/admin-out/monaco/vs/systemverilog-Ch4vA8Yt.js +1 -0
- package/admin-out/monaco/vs/tcl-D74tq1nH.js +1 -0
- package/admin-out/monaco/vs/tsMode-CZz1Umrk.js +11 -0
- package/admin-out/monaco/vs/twig-C6taOxMV.js +1 -0
- package/admin-out/monaco/vs/typescript-DfOrAzoV.js +1 -0
- package/admin-out/monaco/vs/typespec-D-PIh9Xw.js +1 -0
- package/admin-out/monaco/vs/vb-Dyb2648j.js +1 -0
- package/admin-out/monaco/vs/wgsl-BhLXMOR0.js +298 -0
- package/admin-out/monaco/vs/workers-DcJshg-q.js +1 -0
- package/admin-out/monaco/vs/xml-CdsdnY8S.js +1 -0
- package/admin-out/monaco/vs/yaml-DYGvmE88.js +1 -0
- package/bin/halo.js +9 -0
- package/bundled-docs/design/agent.md +145 -0
- package/bundled-docs/design/architecture.md +270 -0
- package/bundled-docs/design/background-dispatch.md +151 -0
- package/bundled-docs/design/command.md +58 -0
- package/bundled-docs/design/cron.md +88 -0
- package/bundled-docs/design/evolution.md +383 -0
- package/bundled-docs/design/express-self.md +145 -0
- package/bundled-docs/design/feishu.md +224 -0
- package/bundled-docs/design/halo-city.md +112 -0
- package/bundled-docs/design/prompt-system.md +411 -0
- package/bundled-docs/design/session.md +321 -0
- package/bundled-docs/design/slack.md +212 -0
- package/bundled-docs/design/storage.md +578 -0
- package/bundled-docs/design/telegram.md +161 -0
- package/bundled-docs/design/web.md +222 -0
- package/bundled-docs/design/wechat.md +146 -0
- package/bundled-docs/design/ws.md +144 -0
- package/bundled-docs/dev/acp-adapter.md +337 -0
- package/bundled-docs/dev/add-channel.md +376 -0
- package/bundled-docs/dev/add-model-provider.md +386 -0
- package/bundled-docs/dev/api.md +686 -0
- package/bundled-docs/dev/deploy.md +198 -0
- package/bundled-docs/dev/desktop-packaging.md +327 -0
- package/bundled-docs/dev/env.md +158 -0
- package/bundled-docs/dev/logging.md +85 -0
- package/bundled-docs/dev/previews.md +225 -0
- package/bundled-docs/dev/tools.md +331 -0
- package/bundled-docs/guide/README.md +29 -0
- package/bundled-docs/guide/agents.md +264 -0
- package/bundled-docs/guide/channels/README.md +62 -0
- package/bundled-docs/guide/channels/acp.md +150 -0
- package/bundled-docs/guide/channels/feishu.md +154 -0
- package/bundled-docs/guide/channels/slack.md +156 -0
- package/bundled-docs/guide/channels/telegram.md +112 -0
- package/bundled-docs/guide/channels/web.md +149 -0
- package/bundled-docs/guide/channels/wechat.md +110 -0
- package/bundled-docs/guide/chat.md +112 -0
- package/bundled-docs/guide/cli.md +197 -0
- package/bundled-docs/guide/getting-started.md +93 -0
- package/bundled-docs/guide/secrets-and-credentials.md +161 -0
- package/bundled-docs/guide/sessions.md +104 -0
- package/bundled-docs/guide/skills.md +302 -0
- package/bundled-docs/guide/testing-agents-and-skills.md +136 -0
- package/bundled-docs/guide/workspace.md +131 -0
- package/bundled-docs/plans/cron-tasks.md +362 -0
- package/bundled-docs/plans/design-suggestions.md +203 -0
- package/bundled-docs/plans/issue-session-deep-nesting.md +208 -0
- package/bundled-docs/plans/launch-plan.md +111 -0
- package/bundled-docs/plans/md-and-agent-refactor.md +197 -0
- package/bundled-docs/plans/remote-mac-worker.md +196 -0
- package/bundled-docs/plans/roadmap.md +40 -0
- package/bundled-docs/plans/self-evolution.md +1379 -0
- package/bundled-docs/plans/todo.md +32 -0
- package/bundled-docs/plans/weixin-channel.md +255 -0
- package/bundled-docs/requirements/agent.md +92 -0
- package/bundled-docs/requirements/chat.md +81 -0
- package/bundled-docs/requirements/command.md +130 -0
- package/bundled-docs/requirements/editor.md +82 -0
- package/bundled-docs/requirements/explorer.md +89 -0
- package/bundled-docs/requirements/overview.md +62 -0
- package/bundled-docs/requirements/session.md +73 -0
- package/bundled-docs/requirements/settings.md +224 -0
- package/bundled-docs/requirements/skill.md +125 -0
- package/bundled-docs/requirements/source-control.md +82 -0
- package/bundled-docs/requirements/terminal.md +118 -0
- package/bundled-docs/requirements/workspace.md +80 -0
- package/bundled-docs/test/docs-seeding.md +101 -0
- package/bundled-docs/test/session.md +374 -0
- package/dist/index.js +26750 -0
- package/package.json +67 -0
- package/templates/INSTRUCTIONS.md +25 -0
- package/templates/agents/__apply_agent__/AGENT.md +229 -0
- package/templates/agents/__apply_agent__/agent.yaml +30 -0
- package/templates/agents/__evo_agent__/AGENT.md +323 -0
- package/templates/agents/__evo_agent__/agent.yaml +29 -0
- package/templates/agents/__score__/AGENT.md +177 -0
- package/templates/agents/__score__/agent.yaml +29 -0
- package/templates/agents/deep-executor/AGENT.md +60 -0
- package/templates/agents/deep-executor/agent.yaml +25 -0
- package/templates/agents/default/AGENT.md +38 -0
- package/templates/agents/default/agent.yaml +51 -0
- package/templates/agents/executor/AGENT.md +38 -0
- package/templates/agents/executor/agent.yaml +25 -0
- package/templates/aliases.yaml +26 -0
- package/templates/canvas/self.html +660 -0
- package/templates/config.yaml +69 -0
- package/templates/models/anthropic.yaml +47 -0
- package/templates/models/aws-bedrock-claude-invoke.yaml +156 -0
- package/templates/models/aws-bedrock-mantle.yaml +89 -0
- package/templates/models/deepseek.yaml +62 -0
- package/templates/models/doubao.yaml +137 -0
- package/templates/models/hunyuan.yaml +56 -0
- package/templates/models/kimi.yaml +41 -0
- package/templates/models/mimo-token-plan-china.yaml +61 -0
- package/templates/models/minimax.yaml +80 -0
- package/templates/models/openai.yaml +51 -0
- package/templates/models/qwen.yaml +78 -0
- package/templates/optional-skills/tavily-web-search/SKILL.md +91 -0
- package/templates/optional-skills/tavily-web-search/config.yaml +10 -0
- package/templates/prompts/all/TOOL_GUIDELINES.md +35 -0
- package/templates/prompts/all/TOOL_SHELL.md +4 -0
- package/templates/prompts/all/TOOL_SHELL.windows.md +7 -0
- package/templates/prompts/bootstrap/BOOTSTRAP.md +26 -0
- package/templates/prompts/root/WORKSPACE_MEMORY.md +11 -0
- package/templates/schema.sql +30 -0
- package/templates/skills/acp/SKILL.md +236 -0
- package/templates/skills/acp/templates/SKILL.md.tmpl +74 -0
- package/templates/skills/acp/templates/ask.py +176 -0
- package/templates/skills/acp/templates/config.yaml.tmpl +33 -0
- package/templates/skills/agent/SKILL.md +128 -0
- package/templates/skills/aws-knowledge/SKILL.md +82 -0
- package/templates/skills/aws-knowledge/scripts/aws_kb.py +61 -0
- package/templates/skills/cron/SKILL.md +217 -0
- package/templates/skills/cron/manage-cron.py +434 -0
- package/templates/skills/halo/SKILL.md +98 -0
- package/templates/skills/nova-web-search/SKILL.md +65 -0
- package/templates/skills/self/SKILL.md +139 -0
- package/templates/skills/send-file/SKILL.md +28 -0
- package/templates/skills/send-file/feishu.md +16 -0
- package/templates/skills/send-file/slack.md +7 -0
- package/templates/skills/send-file/telegram.md +9 -0
- package/templates/skills/send-file/web.md +6 -0
- package/templates/skills/send-file/wechat.md +8 -0
- package/templates/skills/skill/SKILL.md +131 -0
- package/templates/skills/workspace/SKILL.md +28 -0
- package/templates/skills/workspace/setup-tidy.md +265 -0
- package/templates/skills/workspace/share.md +187 -0
- 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).
|