@turmind/halo 1.6.5 → 1.6.6

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/README.md CHANGED
@@ -199,6 +199,7 @@ Halo is young — treat it as an early-stage project:
199
199
  - **The sandbox guards writes and credentials, not the network.** In `workspace` / `readonly` sessions, bubblewrap (Linux) or Seatbelt (macOS) limits writes to the workspace and temp dirs (`readonly`: temp dirs only) and hides `~/.halo/secrets`, `~/.ssh`, `~/.aws` and similar paths; the rest of the host stays readable and outbound connections work. The threat model is accidental damage and path escape by a trusted agent — not containing a malicious skill or extension, or a prompt-injected session sending out what it can read.
200
200
  - **Windows hosts have no sandbox.** There a `workspace` session is effectively `full`, and `readonly` can read any path the halo process can — don't hand out non-`full` channel tokens on a Windows host expecting isolation ([dev/tools.md](.halo/docs/dev/tools.md)).
201
201
  - **The admin is single-tenant, on plain HTTP, on all interfaces.** One password (5 failed logins lock that IP out for 15 minutes); whoever holds the admin cookie controls the server — every workspace, shell as the server user, every channel credential. Keep port 9527 behind a firewall, VPN or SSH tunnel, or a reverse proxy with TLS ([deploy.md §10](.halo/docs/dev/deploy.md#10-optional-nginx-reverse-proxy)), and give other people channel accounts with their own access levels instead of the admin login.
202
+ - **No MCP support.** Agents can't use MCP servers as tools, and Halo doesn't serve MCP itself. To reach an MCP server, wrap it in a skill whose script calls it — the bundled `aws-knowledge` skill does this for the AWS Knowledge MCP server.
202
203
  - **Tests are unit/regression-level.** CI runs them on every push and PR to `main`; there's no end-to-end or integration suite yet.
203
204
  - **APIs and on-disk formats may still change between versions.** Expect rough edges while things stabilize.
204
205
 
@@ -1 +1 @@
1
- <!DOCTYPE html><!--8v1hCXbUqJhyMuK7LVeet--><html lang="en" class="dark"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" href="/_next/static/media/e4af272ccee01ff0-s.p.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="stylesheet" href="/_next/static/css/7b736202f0b1dbfd.css" data-precedence="next"/><link rel="stylesheet" href="/_next/static/css/e890aa0d13e94290.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-74f577c1b094f942.js"/><script src="/_next/static/chunks/1b86ba38-c29611e086b2ce25.js" async=""></script><script src="/_next/static/chunks/698-19872069fed27164.js" async=""></script><script src="/_next/static/chunks/main-app-340a5b58ed229c4d.js" async=""></script><script src="/_next/static/chunks/989-066a874961bd519f.js" async=""></script><script src="/_next/static/chunks/app/layout-ea161287d934ba96.js" async=""></script><meta name="robots" content="noindex"/><meta name="next-size-adjust" content=""/><title>404: This page could not be found.</title><title>Halo - Multi-Agent Workspace</title><meta name="description" content="Multi-agent collaboration workspace for complex project delivery"/><link rel="icon" href="/favicon.ico" type="image/x-icon" sizes="16x16"/><link rel="icon" href="/icon.svg?3a6863548daca964" type="image/svg+xml" sizes="any"/><script>try{var t=localStorage.getItem('halo_theme');if(t==='light'||t==='midnight'||t==='warm')document.documentElement.dataset.theme=t}catch(e){}</script><script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body class="__variable_d8ebd1 font-sans antialiased"><div hidden=""><!--$--><!--/$--></div><div style="font-family:system-ui,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">This page could not be found.</h2></div></div></div><!--$--><!--/$--><script src="/_next/static/chunks/webpack-74f577c1b094f942.js" id="_R_" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[58321,[\"989\",\"static/chunks/989-066a874961bd519f.js\",\"177\",\"static/chunks/app/layout-ea161287d934ba96.js\"],\"ThemeProvider\"]\n3:I[24554,[\"989\",\"static/chunks/989-066a874961bd519f.js\",\"177\",\"static/chunks/app/layout-ea161287d934ba96.js\"],\"AppI18nProvider\"]\n4:I[63693,[],\"\"]\n5:I[74713,[],\"\"]\n6:I[23632,[],\"OutletBoundary\"]\n8:I[86889,[],\"AsyncMetadataOutlet\"]\na:I[23632,[],\"ViewportBoundary\"]\nc:I[23632,[],\"MetadataBoundary\"]\nd:\"$Sreact.suspense\"\nf:I[1935,[],\"\"]\n:HL[\"/_next/static/media/e4af272ccee01ff0-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n:HL[\"/_next/static/css/7b736202f0b1dbfd.css\",\"style\"]\n:HL[\"/_next/static/css/e890aa0d13e94290.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"8v1hCXbUqJhyMuK7LVeet\",\"p\":\"\",\"c\":[\"\",\"_not-found\"],\"i\":false,\"f\":[[[\"\",{\"children\":[\"/_not-found\",{\"children\":[\"__PAGE__\",{}]}]},\"$undefined\",\"$undefined\",true],[\"\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/7b736202f0b1dbfd.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}],[\"$\",\"link\",\"1\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/e890aa0d13e94290.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"className\":\"dark\",\"suppressHydrationWarning\":true,\"children\":[[\"$\",\"head\",null,{\"children\":[\"$\",\"script\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"try{var t=localStorage.getItem('halo_theme');if(t==='light'||t==='midnight'||t==='warm')document.documentElement.dataset.theme=t}catch(e){}\"}}]}],[\"$\",\"body\",null,{\"className\":\"__variable_d8ebd1 font-sans antialiased\",\"children\":[\"$\",\"$L2\",null,{\"children\":[\"$\",\"$L3\",null,{\"children\":[\"$\",\"$L4\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L5\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]}]}]]}]]}],{\"children\":[\"/_not-found\",[\"$\",\"$1\",\"c\",{\"children\":[null,[\"$\",\"$L4\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L5\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]]}],{\"children\":[\"__PAGE__\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],null,[\"$\",\"$L6\",null,{\"children\":[\"$L7\",[\"$\",\"$L8\",null,{\"promise\":\"$@9\"}]]}]]}],{},null,false]},null,false]},null,false],[\"$\",\"$1\",\"h\",{\"children\":[[\"$\",\"meta\",null,{\"name\":\"robots\",\"content\":\"noindex\"}],[[\"$\",\"$La\",null,{\"children\":\"$Lb\"}],[\"$\",\"meta\",null,{\"name\":\"next-size-adjust\",\"content\":\"\"}]],[\"$\",\"$Lc\",null,{\"children\":[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$d\",null,{\"fallback\":null,\"children\":\"$Le\"}]}]}]]}],false]],\"m\":\"$undefined\",\"G\":[\"$f\",[]],\"s\":false,\"S\":true}\n"])</script><script>self.__next_f.push([1,"b:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\n7:null\n"])</script><script>self.__next_f.push([1,"10:I[88809,[],\"IconMark\"]\n9:{\"metadata\":[[\"$\",\"title\",\"0\",{\"children\":\"Halo - Multi-Agent Workspace\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Multi-agent collaboration workspace for complex project delivery\"}],[\"$\",\"link\",\"2\",{\"rel\":\"icon\",\"href\":\"/favicon.ico\",\"type\":\"image/x-icon\",\"sizes\":\"16x16\"}],[\"$\",\"link\",\"3\",{\"rel\":\"icon\",\"href\":\"/icon.svg?3a6863548daca964\",\"type\":\"image/svg+xml\",\"sizes\":\"any\"}],[\"$\",\"$L10\",\"4\",{}]],\"error\":null,\"digest\":\"$undefined\"}\n"])</script><script>self.__next_f.push([1,"e:\"$9:metadata\"\n"])</script></body></html>
1
+ <!DOCTYPE html><!--Yc05ORrVk5IE9P1Gd8Ojh--><html lang="en" class="dark"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" href="/_next/static/media/e4af272ccee01ff0-s.p.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="stylesheet" href="/_next/static/css/7b736202f0b1dbfd.css" data-precedence="next"/><link rel="stylesheet" href="/_next/static/css/e890aa0d13e94290.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-74f577c1b094f942.js"/><script src="/_next/static/chunks/1b86ba38-c29611e086b2ce25.js" async=""></script><script src="/_next/static/chunks/698-19872069fed27164.js" async=""></script><script src="/_next/static/chunks/main-app-340a5b58ed229c4d.js" async=""></script><script src="/_next/static/chunks/989-066a874961bd519f.js" async=""></script><script src="/_next/static/chunks/app/layout-ea161287d934ba96.js" async=""></script><meta name="robots" content="noindex"/><meta name="next-size-adjust" content=""/><title>404: This page could not be found.</title><title>Halo - Multi-Agent Workspace</title><meta name="description" content="Multi-agent collaboration workspace for complex project delivery"/><link rel="icon" href="/favicon.ico" type="image/x-icon" sizes="16x16"/><link rel="icon" href="/icon.svg?3a6863548daca964" type="image/svg+xml" sizes="any"/><script>try{var t=localStorage.getItem('halo_theme');if(t==='light'||t==='midnight'||t==='warm')document.documentElement.dataset.theme=t}catch(e){}</script><script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body class="__variable_d8ebd1 font-sans antialiased"><div hidden=""><!--$--><!--/$--></div><div style="font-family:system-ui,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">This page could not be found.</h2></div></div></div><!--$--><!--/$--><script src="/_next/static/chunks/webpack-74f577c1b094f942.js" id="_R_" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[58321,[\"989\",\"static/chunks/989-066a874961bd519f.js\",\"177\",\"static/chunks/app/layout-ea161287d934ba96.js\"],\"ThemeProvider\"]\n3:I[24554,[\"989\",\"static/chunks/989-066a874961bd519f.js\",\"177\",\"static/chunks/app/layout-ea161287d934ba96.js\"],\"AppI18nProvider\"]\n4:I[63693,[],\"\"]\n5:I[74713,[],\"\"]\n6:I[23632,[],\"OutletBoundary\"]\n8:I[86889,[],\"AsyncMetadataOutlet\"]\na:I[23632,[],\"ViewportBoundary\"]\nc:I[23632,[],\"MetadataBoundary\"]\nd:\"$Sreact.suspense\"\nf:I[1935,[],\"\"]\n:HL[\"/_next/static/media/e4af272ccee01ff0-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n:HL[\"/_next/static/css/7b736202f0b1dbfd.css\",\"style\"]\n:HL[\"/_next/static/css/e890aa0d13e94290.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"Yc05ORrVk5IE9P1Gd8Ojh\",\"p\":\"\",\"c\":[\"\",\"_not-found\"],\"i\":false,\"f\":[[[\"\",{\"children\":[\"/_not-found\",{\"children\":[\"__PAGE__\",{}]}]},\"$undefined\",\"$undefined\",true],[\"\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/7b736202f0b1dbfd.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}],[\"$\",\"link\",\"1\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/e890aa0d13e94290.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"className\":\"dark\",\"suppressHydrationWarning\":true,\"children\":[[\"$\",\"head\",null,{\"children\":[\"$\",\"script\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"try{var t=localStorage.getItem('halo_theme');if(t==='light'||t==='midnight'||t==='warm')document.documentElement.dataset.theme=t}catch(e){}\"}}]}],[\"$\",\"body\",null,{\"className\":\"__variable_d8ebd1 font-sans antialiased\",\"children\":[\"$\",\"$L2\",null,{\"children\":[\"$\",\"$L3\",null,{\"children\":[\"$\",\"$L4\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L5\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]}]}]]}]]}],{\"children\":[\"/_not-found\",[\"$\",\"$1\",\"c\",{\"children\":[null,[\"$\",\"$L4\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L5\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]]}],{\"children\":[\"__PAGE__\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],null,[\"$\",\"$L6\",null,{\"children\":[\"$L7\",[\"$\",\"$L8\",null,{\"promise\":\"$@9\"}]]}]]}],{},null,false]},null,false]},null,false],[\"$\",\"$1\",\"h\",{\"children\":[[\"$\",\"meta\",null,{\"name\":\"robots\",\"content\":\"noindex\"}],[[\"$\",\"$La\",null,{\"children\":\"$Lb\"}],[\"$\",\"meta\",null,{\"name\":\"next-size-adjust\",\"content\":\"\"}]],[\"$\",\"$Lc\",null,{\"children\":[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$d\",null,{\"fallback\":null,\"children\":\"$Le\"}]}]}]]}],false]],\"m\":\"$undefined\",\"G\":[\"$f\",[]],\"s\":false,\"S\":true}\n"])</script><script>self.__next_f.push([1,"b:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\n7:null\n"])</script><script>self.__next_f.push([1,"10:I[88809,[],\"IconMark\"]\n9:{\"metadata\":[[\"$\",\"title\",\"0\",{\"children\":\"Halo - Multi-Agent Workspace\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Multi-agent collaboration workspace for complex project delivery\"}],[\"$\",\"link\",\"2\",{\"rel\":\"icon\",\"href\":\"/favicon.ico\",\"type\":\"image/x-icon\",\"sizes\":\"16x16\"}],[\"$\",\"link\",\"3\",{\"rel\":\"icon\",\"href\":\"/icon.svg?3a6863548daca964\",\"type\":\"image/svg+xml\",\"sizes\":\"any\"}],[\"$\",\"$L10\",\"4\",{}]],\"error\":null,\"digest\":\"$undefined\"}\n"])</script><script>self.__next_f.push([1,"e:\"$9:metadata\"\n"])</script></body></html>
@@ -1 +1 @@
1
- <!DOCTYPE html><!--8v1hCXbUqJhyMuK7LVeet--><html lang="en" class="dark"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" href="/_next/static/media/e4af272ccee01ff0-s.p.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="stylesheet" href="/_next/static/css/7b736202f0b1dbfd.css" data-precedence="next"/><link rel="stylesheet" href="/_next/static/css/e890aa0d13e94290.css" data-precedence="next"/><link rel="stylesheet" href="/_next/static/css/659eccb5db697b76.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-74f577c1b094f942.js"/><script src="/_next/static/chunks/1b86ba38-c29611e086b2ce25.js" async=""></script><script src="/_next/static/chunks/698-19872069fed27164.js" async=""></script><script src="/_next/static/chunks/main-app-340a5b58ed229c4d.js" async=""></script><script src="/_next/static/chunks/989-066a874961bd519f.js" async=""></script><script src="/_next/static/chunks/app/layout-ea161287d934ba96.js" async=""></script><script src="/_next/static/chunks/1d2d5650-d8d130c85727fdbb.js" async=""></script><script src="/_next/static/chunks/87-6a2716c98a819c4e.js" async=""></script><script src="/_next/static/chunks/app/page-812bcbb46db628b6.js" async=""></script><meta name="next-size-adjust" content=""/><title>Halo - Multi-Agent Workspace</title><meta name="description" content="Multi-agent collaboration workspace for complex project delivery"/><link rel="icon" href="/favicon.ico" type="image/x-icon" sizes="16x16"/><link rel="icon" href="/icon.svg?3a6863548daca964" type="image/svg+xml" sizes="any"/><script>try{var t=localStorage.getItem('halo_theme');if(t==='light'||t==='midnight'||t==='warm')document.documentElement.dataset.theme=t}catch(e){}</script><script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body class="__variable_d8ebd1 font-sans antialiased"><div hidden=""><!--$--><!--/$--></div><div class="flex h-screen w-screen items-center justify-center bg-[var(--background)]"><div class="text-sm text-[var(--muted-foreground)]">Loading...</div></div><!--$--><!--/$--><script src="/_next/static/chunks/webpack-74f577c1b094f942.js" id="_R_" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[58321,[\"989\",\"static/chunks/989-066a874961bd519f.js\",\"177\",\"static/chunks/app/layout-ea161287d934ba96.js\"],\"ThemeProvider\"]\n3:I[24554,[\"989\",\"static/chunks/989-066a874961bd519f.js\",\"177\",\"static/chunks/app/layout-ea161287d934ba96.js\"],\"AppI18nProvider\"]\n4:I[63693,[],\"\"]\n5:I[74713,[],\"\"]\n6:I[5444,[],\"ClientPageRoot\"]\n7:I[3124,[\"560\",\"static/chunks/1d2d5650-d8d130c85727fdbb.js\",\"87\",\"static/chunks/87-6a2716c98a819c4e.js\",\"989\",\"static/chunks/989-066a874961bd519f.js\",\"974\",\"static/chunks/app/page-812bcbb46db628b6.js\"],\"default\"]\na:I[23632,[],\"OutletBoundary\"]\nc:I[86889,[],\"AsyncMetadataOutlet\"]\ne:I[23632,[],\"ViewportBoundary\"]\n10:I[23632,[],\"MetadataBoundary\"]\n11:\"$Sreact.suspense\"\n13:I[1935,[],\"\"]\n:HL[\"/_next/static/media/e4af272ccee01ff0-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n:HL[\"/_next/static/css/7b736202f0b1dbfd.css\",\"style\"]\n:HL[\"/_next/static/css/e890aa0d13e94290.css\",\"style\"]\n:HL[\"/_next/static/css/659eccb5db697b76.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"8v1hCXbUqJhyMuK7LVeet\",\"p\":\"\",\"c\":[\"\",\"\"],\"i\":false,\"f\":[[[\"\",{\"children\":[\"__PAGE__\",{}]},\"$undefined\",\"$undefined\",true],[\"\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/7b736202f0b1dbfd.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}],[\"$\",\"link\",\"1\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/e890aa0d13e94290.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"className\":\"dark\",\"suppressHydrationWarning\":true,\"children\":[[\"$\",\"head\",null,{\"children\":[\"$\",\"script\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"try{var t=localStorage.getItem('halo_theme');if(t==='light'||t==='midnight'||t==='warm')document.documentElement.dataset.theme=t}catch(e){}\"}}]}],[\"$\",\"body\",null,{\"className\":\"__variable_d8ebd1 font-sans antialiased\",\"children\":[\"$\",\"$L2\",null,{\"children\":[\"$\",\"$L3\",null,{\"children\":[\"$\",\"$L4\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L5\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],[]],\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]}]}]]}]]}],{\"children\":[\"__PAGE__\",[\"$\",\"$1\",\"c\",{\"children\":[[\"$\",\"$L6\",null,{\"Component\":\"$7\",\"searchParams\":{},\"params\":{},\"promises\":[\"$@8\",\"$@9\"]}],[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/659eccb5db697b76.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]],[\"$\",\"$La\",null,{\"children\":[\"$Lb\",[\"$\",\"$Lc\",null,{\"promise\":\"$@d\"}]]}]]}],{},null,false]},null,false],[\"$\",\"$1\",\"h\",{\"children\":[null,[[\"$\",\"$Le\",null,{\"children\":\"$Lf\"}],[\"$\",\"meta\",null,{\"name\":\"next-size-adjust\",\"content\":\"\"}]],[\"$\",\"$L10\",null,{\"children\":[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$11\",null,{\"fallback\":null,\"children\":\"$L12\"}]}]}]]}],false]],\"m\":\"$undefined\",\"G\":[\"$13\",[]],\"s\":false,\"S\":true}\n"])</script><script>self.__next_f.push([1,"8:{}\n9:\"$0:f:0:1:2:children:1:props:children:0:props:params\"\n"])</script><script>self.__next_f.push([1,"f:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\nb:null\n"])</script><script>self.__next_f.push([1,"14:I[88809,[],\"IconMark\"]\nd:{\"metadata\":[[\"$\",\"title\",\"0\",{\"children\":\"Halo - Multi-Agent Workspace\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Multi-agent collaboration workspace for complex project delivery\"}],[\"$\",\"link\",\"2\",{\"rel\":\"icon\",\"href\":\"/favicon.ico\",\"type\":\"image/x-icon\",\"sizes\":\"16x16\"}],[\"$\",\"link\",\"3\",{\"rel\":\"icon\",\"href\":\"/icon.svg?3a6863548daca964\",\"type\":\"image/svg+xml\",\"sizes\":\"any\"}],[\"$\",\"$L14\",\"4\",{}]],\"error\":null,\"digest\":\"$undefined\"}\n"])</script><script>self.__next_f.push([1,"12:\"$d:metadata\"\n"])</script></body></html>
1
+ <!DOCTYPE html><!--Yc05ORrVk5IE9P1Gd8Ojh--><html lang="en" class="dark"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" href="/_next/static/media/e4af272ccee01ff0-s.p.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="stylesheet" href="/_next/static/css/7b736202f0b1dbfd.css" data-precedence="next"/><link rel="stylesheet" href="/_next/static/css/e890aa0d13e94290.css" data-precedence="next"/><link rel="stylesheet" href="/_next/static/css/659eccb5db697b76.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-74f577c1b094f942.js"/><script src="/_next/static/chunks/1b86ba38-c29611e086b2ce25.js" async=""></script><script src="/_next/static/chunks/698-19872069fed27164.js" async=""></script><script src="/_next/static/chunks/main-app-340a5b58ed229c4d.js" async=""></script><script src="/_next/static/chunks/989-066a874961bd519f.js" async=""></script><script src="/_next/static/chunks/app/layout-ea161287d934ba96.js" async=""></script><script src="/_next/static/chunks/1d2d5650-d8d130c85727fdbb.js" async=""></script><script src="/_next/static/chunks/87-6a2716c98a819c4e.js" async=""></script><script src="/_next/static/chunks/app/page-812bcbb46db628b6.js" async=""></script><meta name="next-size-adjust" content=""/><title>Halo - Multi-Agent Workspace</title><meta name="description" content="Multi-agent collaboration workspace for complex project delivery"/><link rel="icon" href="/favicon.ico" type="image/x-icon" sizes="16x16"/><link rel="icon" href="/icon.svg?3a6863548daca964" type="image/svg+xml" sizes="any"/><script>try{var t=localStorage.getItem('halo_theme');if(t==='light'||t==='midnight'||t==='warm')document.documentElement.dataset.theme=t}catch(e){}</script><script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body class="__variable_d8ebd1 font-sans antialiased"><div hidden=""><!--$--><!--/$--></div><div class="flex h-screen w-screen items-center justify-center bg-[var(--background)]"><div class="text-sm text-[var(--muted-foreground)]">Loading...</div></div><!--$--><!--/$--><script src="/_next/static/chunks/webpack-74f577c1b094f942.js" id="_R_" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[58321,[\"989\",\"static/chunks/989-066a874961bd519f.js\",\"177\",\"static/chunks/app/layout-ea161287d934ba96.js\"],\"ThemeProvider\"]\n3:I[24554,[\"989\",\"static/chunks/989-066a874961bd519f.js\",\"177\",\"static/chunks/app/layout-ea161287d934ba96.js\"],\"AppI18nProvider\"]\n4:I[63693,[],\"\"]\n5:I[74713,[],\"\"]\n6:I[5444,[],\"ClientPageRoot\"]\n7:I[3124,[\"560\",\"static/chunks/1d2d5650-d8d130c85727fdbb.js\",\"87\",\"static/chunks/87-6a2716c98a819c4e.js\",\"989\",\"static/chunks/989-066a874961bd519f.js\",\"974\",\"static/chunks/app/page-812bcbb46db628b6.js\"],\"default\"]\na:I[23632,[],\"OutletBoundary\"]\nc:I[86889,[],\"AsyncMetadataOutlet\"]\ne:I[23632,[],\"ViewportBoundary\"]\n10:I[23632,[],\"MetadataBoundary\"]\n11:\"$Sreact.suspense\"\n13:I[1935,[],\"\"]\n:HL[\"/_next/static/media/e4af272ccee01ff0-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n:HL[\"/_next/static/css/7b736202f0b1dbfd.css\",\"style\"]\n:HL[\"/_next/static/css/e890aa0d13e94290.css\",\"style\"]\n:HL[\"/_next/static/css/659eccb5db697b76.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"Yc05ORrVk5IE9P1Gd8Ojh\",\"p\":\"\",\"c\":[\"\",\"\"],\"i\":false,\"f\":[[[\"\",{\"children\":[\"__PAGE__\",{}]},\"$undefined\",\"$undefined\",true],[\"\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/7b736202f0b1dbfd.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}],[\"$\",\"link\",\"1\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/e890aa0d13e94290.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"className\":\"dark\",\"suppressHydrationWarning\":true,\"children\":[[\"$\",\"head\",null,{\"children\":[\"$\",\"script\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"try{var t=localStorage.getItem('halo_theme');if(t==='light'||t==='midnight'||t==='warm')document.documentElement.dataset.theme=t}catch(e){}\"}}]}],[\"$\",\"body\",null,{\"className\":\"__variable_d8ebd1 font-sans antialiased\",\"children\":[\"$\",\"$L2\",null,{\"children\":[\"$\",\"$L3\",null,{\"children\":[\"$\",\"$L4\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L5\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],[]],\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]}]}]]}]]}],{\"children\":[\"__PAGE__\",[\"$\",\"$1\",\"c\",{\"children\":[[\"$\",\"$L6\",null,{\"Component\":\"$7\",\"searchParams\":{},\"params\":{},\"promises\":[\"$@8\",\"$@9\"]}],[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/659eccb5db697b76.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]],[\"$\",\"$La\",null,{\"children\":[\"$Lb\",[\"$\",\"$Lc\",null,{\"promise\":\"$@d\"}]]}]]}],{},null,false]},null,false],[\"$\",\"$1\",\"h\",{\"children\":[null,[[\"$\",\"$Le\",null,{\"children\":\"$Lf\"}],[\"$\",\"meta\",null,{\"name\":\"next-size-adjust\",\"content\":\"\"}]],[\"$\",\"$L10\",null,{\"children\":[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$11\",null,{\"fallback\":null,\"children\":\"$L12\"}]}]}]]}],false]],\"m\":\"$undefined\",\"G\":[\"$13\",[]],\"s\":false,\"S\":true}\n"])</script><script>self.__next_f.push([1,"8:{}\n9:\"$0:f:0:1:2:children:1:props:children:0:props:params\"\n"])</script><script>self.__next_f.push([1,"f:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\nb:null\n"])</script><script>self.__next_f.push([1,"14:I[88809,[],\"IconMark\"]\nd:{\"metadata\":[[\"$\",\"title\",\"0\",{\"children\":\"Halo - Multi-Agent Workspace\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Multi-agent collaboration workspace for complex project delivery\"}],[\"$\",\"link\",\"2\",{\"rel\":\"icon\",\"href\":\"/favicon.ico\",\"type\":\"image/x-icon\",\"sizes\":\"16x16\"}],[\"$\",\"link\",\"3\",{\"rel\":\"icon\",\"href\":\"/icon.svg?3a6863548daca964\",\"type\":\"image/svg+xml\",\"sizes\":\"any\"}],[\"$\",\"$L14\",\"4\",{}]],\"error\":null,\"digest\":\"$undefined\"}\n"])</script><script>self.__next_f.push([1,"12:\"$d:metadata\"\n"])</script></body></html>
@@ -15,7 +15,7 @@ e:I[23632,[],"ViewportBoundary"]
15
15
  :HL["/_next/static/css/7b736202f0b1dbfd.css","style"]
16
16
  :HL["/_next/static/css/e890aa0d13e94290.css","style"]
17
17
  :HL["/_next/static/css/659eccb5db697b76.css","style"]
18
- 0:{"P":null,"b":"8v1hCXbUqJhyMuK7LVeet","p":"","c":["",""],"i":false,"f":[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/7b736202f0b1dbfd.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/_next/static/css/e890aa0d13e94290.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]],["$","html",null,{"lang":"en","className":"dark","suppressHydrationWarning":true,"children":[["$","head",null,{"children":["$","script",null,{"dangerouslySetInnerHTML":{"__html":"try{var t=localStorage.getItem('halo_theme');if(t==='light'||t==='midnight'||t==='warm')document.documentElement.dataset.theme=t}catch(e){}"}}]}],["$","body",null,{"className":"__variable_d8ebd1 font-sans antialiased","children":["$","$L2",null,{"children":["$","$L3",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]}]]}]]}],{"children":["__PAGE__",["$","$1","c",{"children":[["$","$L6",null,{"Component":"$7","searchParams":{},"params":{},"promises":["$@8","$@9"]}],[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/659eccb5db697b76.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]],["$","$La",null,{"children":["$Lb",["$","$Lc",null,{"promise":"$@d"}]]}]]}],{},null,false]},null,false],["$","$1","h",{"children":[null,[["$","$Le",null,{"children":"$Lf"}],["$","meta",null,{"name":"next-size-adjust","content":""}]],["$","$L10",null,{"children":["$","div",null,{"hidden":true,"children":["$","$11",null,{"fallback":null,"children":"$L12"}]}]}]]}],false]],"m":"$undefined","G":["$13",[]],"s":false,"S":true}
18
+ 0:{"P":null,"b":"Yc05ORrVk5IE9P1Gd8Ojh","p":"","c":["",""],"i":false,"f":[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/7b736202f0b1dbfd.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/_next/static/css/e890aa0d13e94290.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]],["$","html",null,{"lang":"en","className":"dark","suppressHydrationWarning":true,"children":[["$","head",null,{"children":["$","script",null,{"dangerouslySetInnerHTML":{"__html":"try{var t=localStorage.getItem('halo_theme');if(t==='light'||t==='midnight'||t==='warm')document.documentElement.dataset.theme=t}catch(e){}"}}]}],["$","body",null,{"className":"__variable_d8ebd1 font-sans antialiased","children":["$","$L2",null,{"children":["$","$L3",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]}]]}]]}],{"children":["__PAGE__",["$","$1","c",{"children":[["$","$L6",null,{"Component":"$7","searchParams":{},"params":{},"promises":["$@8","$@9"]}],[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/659eccb5db697b76.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]],["$","$La",null,{"children":["$Lb",["$","$Lc",null,{"promise":"$@d"}]]}]]}],{},null,false]},null,false],["$","$1","h",{"children":[null,[["$","$Le",null,{"children":"$Lf"}],["$","meta",null,{"name":"next-size-adjust","content":""}]],["$","$L10",null,{"children":["$","div",null,{"hidden":true,"children":["$","$11",null,{"fallback":null,"children":"$L12"}]}]}]]}],false]],"m":"$undefined","G":["$13",[]],"s":false,"S":true}
19
19
  8:{}
20
20
  9:"$0:f:0:1:2:children:1:props:children:0:props:params"
21
21
  f:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]
@@ -42,7 +42,7 @@ The URL is the workspace path relative to the base. `/home/u/A/B` becomes:
42
42
  - `defaultInputModes` / `defaultOutputModes`: `text/plain` + the four image types.
43
43
  - `version`: the user's, else `HALO_VERSION`.
44
44
 
45
- Served with `Cache-Control: private, max-age=300` and an ETag (304 on match). An invalid or incomplete card file → 500 to the authenticated caller and `[A2A] invalid agent-card.json …` in the log.
45
+ Served with `Cache-Control: private, max-age=300` and an ETag (304 on match). The ETag is weak (`W/"…"`) because the gzipped and plain bodies share it; a 200 (gzipped or not) and its 304 carry the same tag, and If-None-Match is compared weakly (RFC 9110). An invalid or incomplete card file → 500 to the authenticated caller and `[A2A] invalid agent-card.json …` in the log.
46
46
 
47
47
  **Interface URL:** `general.a2a.public_url` (env `HALO_A2A_PUBLIC_URL` overrides) + `/a2a/<rel>/`. Unset → derived from the request (`Host`, plus `X-Forwarded-Proto` when `general.server.trust_proxy` is on).
48
48
 
@@ -114,7 +114,7 @@ interface CronChannelDispatcher {
114
114
  - no `supportsMedia` (telegram, feishu, wecom) → the **original text with `MEDIA:` lines intact** — the attachment degrades to a visible path instead of silently vanishing.
115
115
  - `CronMedia.workspacePath` is the **job's** workspace (where the run produced its files), not the target account's — it's the sandbox root for `isMediaPathAllowed` (paths outside workspace/OS-tmp are rejected with a failed result row). Cron passes no access level, so the `full`-account relaxation the chat handlers get does not apply here — the job workspace + OS tmp rule stays.
116
116
  - Wiring telegram/feishu up for real requires refactoring first, not copying: telegram's file send is an inline `sendPhoto/…/sendDocument` switch in `handler.ts`'s responder; feishu's `sendFeishuMedia` is anchored to an inbound `messageId` a cron run doesn't have. Extract the send-to-chat half in the channel module, then flip `supportsMedia: true` (details in each `cron-dispatcher.ts` header).
117
- - **Per-channel size limits are the dispatcher's job**, not the runner's: wechat splits the text at `WECHAT_TEXT_LIMIT` (3500 chars, shared `splitText` from `channels/shared/chunk.ts`) and sends the chunks in order — the ilink gateway rejects any single `sendmessage` over 16 KB with `ret=-2`. A failure on chunk *i* of *n* is recorded as `chunk i/n: <error>` in the target's `DispatchResult`, so the admin run row shows how much of the report already landed (a `failed` row no longer means "nothing arrived"). Telegram does the same at `TELEGRAM_TEXT_LIMIT` (4000 chars, the responder's limit — the Bot API caps a message at 4096), with the same `chunk i/n: <error>` failure row. The slack / feishu / wecom dispatchers still send the whole report as one message — same class of gap, unfixed.
117
+ - **Per-channel size limits are the dispatcher's job**, not the runner's: wechat splits the text at `WECHAT_TEXT_LIMIT` (3500 chars, shared `splitText` from `channels/shared/chunk.ts`) and sends the chunks in order — the ilink gateway rejects any single `sendmessage` over 16 KB with `ret=-2`. A failure on chunk *i* of *n* is recorded as `chunk i/n: <error>` in the target's `DispatchResult`, so the admin run row shows how much of the report already landed (a `failed` row no longer means "nothing arrived"). All five chunked dispatchers cut at their channel's chat-responder limit, exported from that channel's `event-adapter.ts` so cron and chat replies stay aligned: telegram `TELEGRAM_TEXT_LIMIT` (4000 chars — the Bot API caps a message at 4096), slack `SLACK_TEXT_LIMIT` (35 000 — Slack caps a body at ~40k), feishu `FEISHU_TEXT_LIMIT` (4500), wecom `WECOM_TEXT_LIMIT` (5000 chars — under the 20480-byte cap at 4 bytes/char), with the same sequential send and `chunk i/n: <error>` failure row (later chunks are not sent). Slack and feishu also run each chunk through the responder's formatter, in the responder's order (cut the raw text first, then format each chunk): `formatForSlack` (CommonMark → mrkdwn) and `formatForFeishu` (strip markup — the text msg type shows `**` literally), both in `channels/shared/markdown.ts`. Telegram, wechat and wecom send the chunks as-is. On slack a failed text chunk still lets the `MEDIA:` uploads run.
118
118
  - **`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 default-recipient logic decides: WeChat falls back to the QR-bind owner, then the last active chat; Telegram/Slack/Feishu/WeCom require an explicit `chatId` and throw without one. WeCom is the one dispatcher with **no HTTP send path**: it borrows the handler's live long-connect `WSClient` (`liveClients`), so a stopped / kicked / auth-exhausted account fails the target loudly (`wecom long-connect not active`) instead of queueing.
119
119
  - `listTargets()` aggregates across all dispatchers (`listAllCronTargets`) to feed the admin create-form dropdown without the routes knowing any channel.
120
120
 
@@ -62,6 +62,8 @@ The feishu cron-dispatcher (`channels/feishu/cron-dispatcher.ts`) requires an ex
62
62
  1. **Explicit `chatId` on the target** — set when the cron was created from inside a Feishu chat (e.g. cron created from a group thread auto-pins to that thread). Sends only to that chat (top-level message, not nested in the thread — Feishu's open API v1 doesn't support thread-targeting in sendMessage).
63
63
  2. **Admin-UI cron without explicit target** — runs silently if no chatId is provided.
64
64
 
65
+ The report goes through the same steps as a chat reply. It is split with `splitText(text, FEISHU_TEXT_LIMIT)` (4500 chars, exported from `event-adapter.ts`), each chunk runs through `formatForFeishu`, and the chunks are sent in order as `text` messages. A failed chunk stops the send and is recorded as `chunk i/n: <error>`; the chunks before it were already delivered. Same contract as the other channels (see [cron.md](cron.md#dispatch-model)).
66
+
65
67
  ## Modules
66
68
 
67
69
  Files: `packages/server/src/channels/feishu/`
@@ -19,6 +19,10 @@ Halo 服务器运行态的**街景式**像素可视化:每个 **workspace 是一
19
19
  hub [README.md](https://github.com/turmind/halo-hub/blob/main/extensions/halo-city/README.md#和-halo-同源)。
20
20
  - `/api/show/state` — 无活跃 UIState 的会话(idle/stopped/重启后)token
21
21
  从会话文件头读取(按会话 `updatedAt` 缓存,未变化时不随轮询重读),不显示 0;含 `messageCount` 字段
22
+ - `/api/show/state` 带弱 `ETag` + `Cache-Control: private, no-cache`:哈希不含
23
+ `serverTime`,`uptime` 按分钟计入,所以服务器空闲时 7 秒一次的轮询绝大多数回
24
+ 空 body 的 304,每分钟最多一次 200。浏览器 `fetch` 默认缓存模式自动带
25
+ `If-None-Match`,扩展不用改;鉴权仍在最前,不同 scope 的调用方 ETag 不同
22
26
 
23
27
  > **`messageCount` 与 admin 的 `exchangeCount` 是两个语义,不要互相对齐**:
24
28
  > 这里读的是会话文件里的 `messageCount`(活跃 UI 日志的长度),城市只把它
@@ -161,8 +161,8 @@ Slack cron-dispatcher plugs into the unified cron/dispatcher.ts registry. At fir
161
161
 
162
162
  1. Retrieve the Slack account (must be enabled)
163
163
  2. Parse the target's `chatId` (format: `D…`, `C…`, or `C…:thread_ts`)
164
- 3. Call `postMessage()` with the chatId (and threadTs if present)
165
- 4. Return dispatch result (ok / error)
164
+ 3. Split the text with `splitText(text, SLACK_TEXT_LIMIT)` (35 000 chars, exported from `event-adapter.ts`, the same limit as chat replies), run each chunk through `formatForSlack`, and call `postMessage()` for each in order with the chatId (and threadTs if present). A failed chunk stops the text send.
165
+ 4. Return dispatch result: one row for the text (`ok`, or `chunk i/n: <error>`), then one row per `MEDIA:` upload. Uploads still run after a text failure.
166
166
 
167
167
  If no explicit chatId is provided, dispatch fails with a clear message. This prevents silent failures in a shared-bot scenario where the "last messager" might be unrelated to whoever set up the cron.
168
168
 
@@ -65,7 +65,7 @@ The wecom cron-dispatcher (`channels/wecom/cron-dispatcher.ts`) requires an expl
65
65
  1. **Explicit `chatId` on the target** — set when the cron was created from inside a WeCom chat (auto-pinned via `CommandContext.channel.chatId`). Shape: `<userid>` for a single chat, `<chatid>` for a group — exactly what `pickConversation` cached as `chatKey`. `sendMessage(chatId, { msgtype: 'markdown', … })` omits `chat_type`; the server's compat mode (`chat_type = 0`) resolves single vs group from the id.
66
66
  2. **Admin-UI cron without explicit target** — throws `wecom cron target requires an explicit chatId …` and the run fails visibly. There is no `/search` route (WeCom exposes no directory API on this channel), so the target must be typed as `wecom:<accountId>:<chatId>`.
67
67
 
68
- Because the push rides the handler's socket, the dispatcher borrows it through the exported `liveClients: Map<accountId, WSClient>` (set on `authenticated`, cleared on stop / kick / auth exhaustion). No live client → `{ ok: false, error: 'wecom long-connect not active' }` rather than queueing. WeCom additionally refuses pushes to a user / group that has never messaged the bot, and rate-limits each conversation to 30 msgs/min · 1000/hour (replies and pushes combined).
68
+ Because the push rides the handler's socket, the dispatcher borrows it through the exported `liveClients: Map<accountId, WSClient>` (set on `authenticated`, cleared on stop / kick / auth exhaustion). No live client → `{ ok: false, error: 'wecom long-connect not active' }` rather than queueing. The report is split with `splitText(text, WECOM_TEXT_LIMIT)` (5000 chars, exported from `event-adapter.ts`, the same limit as chat replies) and the chunks are sent in order as `markdown` messages, with no formatter. A failed chunk stops the send and is recorded as `chunk i/n: <error>`. WeCom additionally refuses pushes to a user / group that has never messaged the bot, and rate-limits each conversation to 30 msgs/min · 1000/hour (replies and pushes combined).
69
69
 
70
70
  **`MEDIA:` is not implemented for cron** — the dispatcher declares no `supportsMedia`, so `dispatchToTargets` hands it the original text with `MEDIA:` lines intact (the path degrades to visible text, same caveat as telegram / feishu in [cron.md](cron.md#dispatch-model)). The realtime uploader (`sendWecomMedia` in `handler.ts`) already falls back to `sendMediaMessage(chatid, …)` for a route without a `req_id` (restored at startup); wiring cron up means calling it with `{ reqId: '', chatId }` and flipping `supportsMedia: true`.
71
71
 
@@ -33,7 +33,7 @@ Runtime names (`runtime:` value → class): `bedrock-invoke` → `BedrockAgent`,
33
33
  | `gemini` | `gemini` | `GeminiAgent` | `gemini-agent.ts` | Google Gemini 3.x on the Gemini API's OpenAI-compatible surface (`generativelanguage.googleapis.com/v1beta/openai`, Bearer key), streaming via `fetchChatCompletionStream`. **Thought signatures**: the first tool call of a reply carries `extra_content.google.thought_signature`, and a follow-up that replays a current-turn call without it 400s. The runtime takes the signatures from the raw frames (the `onChunk` hook) and re-attaches them by tool-call id. A call it never saw (rebuilt instance) gets Google's placeholder `skip_thought_signature_validator`. Tool-call replies end with `finish_reason: "stop"`, so they're folded as `tool_calls`. Thinking can't be turned off on Gemini 3. The level goes as `extra_body.google.thinking_config.thinking_level` with `include_thoughts` (this 400s next to `reasoning_effort`). The summary streams inline as `<thought>…</thought>` at the start of `content` and is split out into thinking. Levels per model: 3.8-flash low / medium / high, 3.5-flash-lite minimal…high, xhigh / max invalid everywhere. Usage: `completion_tokens` excludes thought tokens, so output = `total_tokens − prompt_tokens`. Implicit caching via `prompt_tokens_details.cached_tokens` (included in `prompt_tokens`, subtracted). Vision via `image_url` data URLs (probed 2026-10-12) |
34
34
  | `openai` | `openai-chat` | `OpenAIAgent` | `openai-agent.ts` | **Generic OpenAI-compatible**. Use for native OpenAI or any third-party OpenAI gateway (incl. Ollama / llama.cpp). Ships a short Bedrock Mantle list callable with a Bedrock API key (`openai.gpt-5.5` default, `openai.gpt-5.4`; `defaultEndpoint` Mantle us-east-1 `/openai/v1`, presets us-east-1 / us-west-2 / api.openai.com — GPT-6.x / 5.6 reject `max_tokens`, so they stay on the Responses-API providers) plus the legacy `gpt-4o` placeholder (official API only); any other endpoint + model id can be typed, and capabilities for unlisted ids are openly editable in the form. Streaming via `fetchChatCompletionStream` — always sends `stream: true` + `stream_options: { include_usage: true }` (OpenAI's own field; Ollama ≥ 0.3 / vLLM / LM Studio accept it too). A gateway that rejects unknown fields would 400 — accepted trade-off, no non-streaming fallback. Forgiving about cache field names (`cachedPromptTokens`: first value > 0 of `prompt_tokens_details.cached_tokens`, top-level `cached_tokens`, `prompt_cache_hit_tokens`, `cache_read_tokens`) and reasoning field names (`reasoning_content` for OpenAI o-series/DeepSeek, `reasoning` for Ollama/llama.cpp). |
35
35
  | `anthropic` | `anthropic-messages` | `AnthropicAgent` | `anthropic-agent.ts` | **Generic Anthropic Messages**. Use for api.anthropic.com or any Anthropic-compatible gateway. Ships a short Bedrock Mantle list callable with a Bedrock API key as `x-api-key` (`anthropic.claude-opus-5-5` default, `anthropic.claude-haiku-4-5`; `defaultEndpoint` Mantle us-east-1 `/anthropic`, presets us-east-1 / us-west-2 / api.anthropic.com) plus the legacy `claude-sonnet-4-6` placeholder (official API only); any other endpoint + model id can be typed. Streaming via `fetchAnthropicStream` (the gateway must support `stream: true` SSE — the reference Anthropic behaviour). Standard `cache_control: ephemeral` with optional `ttl: '1h'`. |
36
- | `mimo-token-plan-china` | `anthropic-messages` | `AnthropicAgent` | (reuses `anthropic-agent.ts`) | Xiaomi MiMo (UI name "Xiaomi MiMo"; the id keeps its historical name so existing configs don't break) — Anthropic-compatible gateway, streams like `anthropic` (gateway built for Claude Code, which always streams; not live-probed); no dedicated class, just `runtime: anthropic-messages` in its yaml (`defaultEndpoint` `token-plan-cn.xiaomimimo.com/anthropic`). The key type picks the endpoint: Token Plan `tp-`/`ttp-` keys only work on `token-plan-{cn,sgp,ams}.xiaomimimo.com/anthropic`, pay-as-you-go `sk-` keys only on `api.xiaomimimo.com/anthropic` — all four ship as `endpointPresets` (pay-as-you-go live-probed 2026-09-24; sgp / ams from the docs only). V2.6 (`mimo-v2.6-pro` / `-flash`, both vision, 1M / 131072) is current; `mimo-v2.5(-pro)` kept in the template until the official 2026-10-21 deprecation, then drop |
36
+ | `mimo-token-plan-china` | `anthropic-messages` | `AnthropicAgent` | (reuses `anthropic-agent.ts`) | Xiaomi MiMo (UI name "Xiaomi MiMo"; the id keeps its historical name so existing configs don't break) — Anthropic-compatible gateway, streams like `anthropic` (gateway built for Claude Code, which always streams; not live-probed); no dedicated class, just `runtime: anthropic-messages` in its yaml (`defaultEndpoint` `token-plan-cn.xiaomimimo.com/anthropic`). The key type picks the endpoint: Token Plan `tp-`/`ttp-` keys only work on `token-plan-{cn,sgp,ams}.xiaomimimo.com/anthropic`, pay-as-you-go `sk-` keys only on `api.xiaomimimo.com/anthropic` — all four ship as `endpointPresets` (pay-as-you-go live-probed 2026-09-24; sgp / ams from the docs only). V2.6 (`mimo-v2.6-pro` / `-flash`, both vision, 1M / 131072) is the only generation listed; `mimo-v2.5(-pro)` (officially retired 2026-10-21) were dropped from the template in revision 2026101401 |
37
37
  | `aws-bedrock-mantle` | `bedrock-mantle` | `MantleAgent` | `mantle-agent.ts` | OpenAI GPT-6.1 Sol, GPT-6 (Astra / Sol / Luna) and GPT-5.6 (Sol / Terra / Luna) on Amazon Bedrock via the **OpenAI Responses API** (not Completions/Converse/InvokeModel). Vision, reasoning via `reasoning.effort` (low/med/high/xhigh/max, no budget_tokens; on 5.6 `max` is accepted live but absent from the model's own validation list — measured ~6% more reasoning_tokens than xhigh; on Astra it's on the official list), `text.verbosity` (low/medium/high, same on both generations). **Region split** (live-probed 2026-09-24): us-east-1 serves GPT-6.1 Sol + GPT-6 Sol / Luna + every 5.6 model (404s Astra; GPT-6.1 Sol probed 2026-09-30, us-east-1 only); us-east-2 serves only the 5.6 trio; us-west-2 serves Astra + 5.6 Terra / Luna. Template ships all three as `endpointPresets` (us-east-1 / us-west-2 / us-east-2, default us-east-1, `openai.gpt-6-sol` default model) and pins each model's regions with per-model `endpoints:` (Astra us-west-2; GPT-6.1 Sol / GPT-6 Sol / Luna us-east-1; GPT-5.6 Sol us-east-1 + us-east-2; Terra / Luna none = all presets), so the form only offers — and on a model switch moves to — a region that serves the model; `extractRegionFromHost` falls back to us-east-1. GPT-6 Astra / Sol / Luna: 1,050,000 context / 128,000 max output; GPT-6.1 Sol 1M / 131,072 (card; Mantle accepts any `max_output_tokens` ≥ 16, so the cap isn't enforced server-side); same effort (none/low/medium/high/xhigh/max) + verbosity surface. Input beyond 272K tokens bills at 2x, so every entry ships `contextWindow: 272000` (what a new agent gets) with the 1M-class figure as `maxContextWindow`; raise `context.maxTokens` past 272K only on purpose. Never surfaces reasoning summaries — `output[].reasoning.summary` stays empty even with `reasoning.summary=detailed` while `reasoning_tokens` counts up (verified live), so no thinking text in the UI is upstream behavior, not a parsing gap. Streams (`stream: true`, Responses API SSE folded by `readResponsesStream` in the same file): `response.output_text.delta` frames go out live, the terminal `response.completed` / `response.incomplete` frame carries the full final response object; this host sends **no** `data: [DONE]` (live-probed 2026-09-29). |
38
38
  | `aws-bedrock-openai` | `bedrock-mantle` | `MantleAgent` | (reuses `mantle-agent.ts`) | Frontier models on Bedrock via the **bedrock-runtime** host's OpenAI Responses surface (`https://bedrock-runtime.<region>.amazonaws.com/openai/v1` — AWS's recommended endpoint for new apps; Mantle is the compatibility one). Same wire format, so its yaml just says `runtime: bedrock-mantle` + `extractRegionFromHost` accepts both hostnames (SigV4 signs with the host's region — a mismatched region 401s "Credential should be scoped to a valid region"). Rules that differ from Mantle: model ids must be cross-Region inference profiles (`global.*` / `us.*`; bare ids 400 "on-demand throughput isn't supported"); **no region split** — live-probed 2026-09-24, all nine `global.*` ids answer 200 from us-east-1 / us-west-2 / eu-west-1 / ap-northeast-1 alike; **no server-side tools** — `tools:[{type:"web_search"}]` 400s "web search is not supported for this request", which is why the web-search skill's deep gear stays on Mantle. Ships the same seven **GPT-6.1 / 6 / 5.6** entries as the Mantle template (`global.openai.gpt-6.1-sol`, `global.openai.gpt-6-{astra,sol,luna}`, `global.openai.gpt-5.6-{sol,terra,luna}`; identical effort none/low/medium/high/xhigh/max + verbosity surface per each model's own validation error — except Astra and GPT-6.1 Sol 400 on `none`, which the runtime never sends), plus **Grok 4.6** (500K ctx, 2x input price past 200K, max output undisclosed → 64000 carry-over, effort **only** low/medium/high/xhigh — `none`/`max` 400, so no Max preset; vision OK; $2/$6) and **Kimi K3** (1M ctx / 131072 out, effort none…max all accepted, ladder advisory on short prompts; vision OK; $3/$15). All: implicit prompt caching verified (run 1 `cache_write_tokens`, runs 2+ `cached_tokens` ≈ prefix), no reasoning summaries. Streams like Mantle (same event vocabulary) but this host **does** append a trailing `data: [DONE]`, which `readSseJson` skips; grok-4.6 emits a `reasoning` output item (no summary text deltas) before the `message` item. |
39
39
 
@@ -6,7 +6,7 @@ Auth: most `/api/*` routes require a valid JWT cookie (`halo_token`). Exceptions
6
6
 
7
7
  **Trust model.** The admin cookie is a single-tenant, all-or-nothing credential: there is one password (`~/.halo/secrets/config.yaml`), one JWT secret, and no roles. A valid `halo_token` equals full control of the server process — every workspace's files (`/api/files/*` project-scoped, `/api/fs/browse` unrestricted), shell via the WS terminal, channel credentials, git credentials, cron jobs, and the ability to mint web-channel tokens at any access level. Routes under the cookie therefore don't re-check "who" beyond the cookie itself; per-user isolation is the job of channel accounts (`x-token` with `readonly` / `workspace` / `full`), not of the admin. Consequences worth knowing when adding routes: (1) never put a cookie-gated route in `PUBLIC_PATHS` to "make it easier for a client" — that's the entire boundary; (2) a route that accepts a path or id from the client still needs traversal / shape checks (`isSafeIdSegment`, `assertPathAllowed`), because the *web-channel* surface reuses the same helpers and is multi-user; (3) if multi-user admin ever lands, the first change is a role claim in the JWT plus per-route checks, and this paragraph becomes the checklist.
8
8
 
9
- Compression: every response negotiates gzip/deflate via `hono/compress` (`threshold: 1024` — bodies under 1 KiB pass through uncompressed), mounted after CORS so it covers both `/api/*` JSON and the served admin static assets. `text/event-stream` (SSE) is excluded, so `/api/web/chat`'s streamed reply is unaffected; WS upgrades never enter the Hono fetch pipeline either.
9
+ Compression: every response negotiates gzip/deflate via `compressResponses` (`middleware/compress.ts`, wrapping `hono/compress`; `threshold: 1024` — bodies under 1 KiB pass through uncompressed). The threshold holds for every compressible body: one without `Content-Length` (`c.json` / `c.text`) is read up to 1 KiB first and, if it ends there, sent with a `Content-Length`. A 304 carries `Vary: Accept-Encoding`, merged into any existing `Vary`. Mounted after CORS so it covers both `/api/*` JSON and the served admin static assets. `text/event-stream` (SSE) is excluded, so `/api/web/chat`'s streamed reply is unaffected; WS upgrades never enter the Hono fetch pipeline either.
10
10
 
11
11
  ## Health
12
12
 
@@ -334,7 +334,7 @@ Files: `packages/server/src/a2a/routes.ts` (JSON-RPC handler), `exposure.ts` (pa
334
334
  | Method | Path | Purpose |
335
335
  |---|---|---|
336
336
  | POST | `/a2a/<rel>` (trailing `/` ok) | JSON-RPC 2.0 for the workspace `<A2A base>/<rel>` |
337
- | GET | `/a2a/<rel>/.well-known/agent-card.json` | The agent card. `Cache-Control: private, max-age=300` + `ETag` (304 on `If-None-Match`); invalid `.halo/agent-card.json` → 500 |
337
+ | GET | `/a2a/<rel>/.well-known/agent-card.json` | The agent card. `Cache-Control: private, max-age=300` + weak `ETag` (`W/"…"`, the same on a 200 gzipped or not and on its 304; 304 on a weak `If-None-Match` match); invalid `.halo/agent-card.json` → 500 |
338
338
  | POST | `/a2a-push/:pushId` | Webhook receiver for remotes this server dispatched to (`a2a_send`) |
339
339
 
340
340
  **Exposure**: `<rel>` is the workspace path relative to the base dir (`HALO_A2A_ROOT`, default the user's home), each segment percent-encoded. Only a workspace containing `.halo/agent-card.json` is served; empty / `.` / `..` segments, separators inside a segment, and symlinks leaving the base are refused. The card's interface URL is `general.a2a.public_url` (env `HALO_A2A_PUBLIC_URL`) + `/a2a/<rel>/`, else derived from the request host.
@@ -393,7 +393,7 @@ runtime so the frontend can render rooms (workspaces) + characters (sessions).
393
393
 
394
394
  | Method | Path | Purpose |
395
395
  |---|---|---|
396
- | GET | `/api/show/state` | Admin cookie, `full` or `observer` token → every known workspace; otherwise the account's own. Returns `{ serverTime, uptime, accessLevel, skills[], workspaces[] }` |
396
+ | GET | `/api/show/state` | Admin cookie, `full` or `observer` token → every known workspace; otherwise the account's own. Returns `{ serverTime, uptime, accessLevel, skills[], workspaces[] }`. Sends a weak `ETag` (over `accessLevel` / `skills` / `workspaces` plus `uptime` by the minute — never `serverTime`) and `Cache-Control: private, no-cache`; a weakly-matching `If-None-Match` (list or `*`) gets an empty 304 after auth, so an idle server answers 200 at most once a minute. On a 304 the cached copy's `serverTime` / `uptime` are those of the last 200. |
397
397
  | GET | `/api/show/session?ws=&id=` | Inspector-panel detail for a single session. Trimmed message log (last 40, content/tool I/O capped) plus `totalMessages` / `contextTokens` / `outputTokens` / `maxContextTokens` / `isRunning`. Tokens other than `full`/`observer` may only read their own workspace, and within it only sessions their own account minted (`web_<accountId>_*`); anything else is 403. The admin cookie reads any workspace. |
398
398
 
399
399
  `observer` is more than an aggregate-counts role: past `/show/state`, it can also call `/show/session` to read **any workspace's** session transcript (last 40 messages, content truncated to 600 chars, tool input to 200 chars). Mint it knowing it grants cross-workspace transcript read access, not just dashboard counters.
package/dist/index.js CHANGED
@@ -4149,7 +4149,7 @@ var init_init = __esm({
4149
4149
  path14.resolve(__dirname, "..", "bundled-docs"),
4150
4150
  path14.resolve(__dirname, "..", "..", "..", ".halo", "docs")
4151
4151
  ];
4152
- TEMPLATE_VERSION = 102;
4152
+ TEMPLATE_VERSION = 103;
4153
4153
  VERSION_FILE = ".template-version";
4154
4154
  SKIP_NAMES = /* @__PURE__ */ new Set([".DS_Store", "schema.sql", "__pycache__", ".pytest_cache"]);
4155
4155
  BUILTIN_AGENT_IDS = /* @__PURE__ */ new Set([
@@ -14303,7 +14303,7 @@ async function initObservability(haloProcess = "server") {
14303
14303
  return true;
14304
14304
  }
14305
14305
  const { registerSdk: registerSdk2 } = await Promise.resolve().then(() => (init_otel_sdk(), otel_sdk_exports));
14306
- providers = await registerSdk2(process.env.OTEL_SERVICE_NAME, "1.6.5", haloProcess);
14306
+ providers = await registerSdk2(process.env.OTEL_SERVICE_NAME, "1.6.6", haloProcess);
14307
14307
  console.log(`[Observability] OTLP export \u2192 ${process.env.OTEL_EXPORTER_OTLP_ENDPOINT} (service.name=${process.env.OTEL_SERVICE_NAME}, halo.process=${haloProcess}, capture_content=${config.observability.captureContent})`);
14308
14308
  return true;
14309
14309
  }
@@ -25556,10 +25556,27 @@ var init_session_types = __esm({
25556
25556
  }
25557
25557
  });
25558
25558
 
25559
+ // ../server/dist/routes/etag.js
25560
+ function etagMatches(header, etag) {
25561
+ if (!header)
25562
+ return false;
25563
+ const bare = etag.replace(/^W\//, "");
25564
+ return header.split(",").some((t2) => {
25565
+ const tag = t2.trim();
25566
+ return tag === "*" || tag.replace(/^W\//, "") === bare;
25567
+ });
25568
+ }
25569
+ var init_etag = __esm({
25570
+ "../server/dist/routes/etag.js"() {
25571
+ "use strict";
25572
+ }
25573
+ });
25574
+
25559
25575
  // ../server/dist/routes/halo-city.js
25560
25576
  import { Hono as Hono12 } from "hono";
25561
25577
  import fs59 from "node:fs";
25562
25578
  import path67 from "node:path";
25579
+ import crypto6 from "node:crypto";
25563
25580
  import Database7 from "better-sqlite3";
25564
25581
  function scanSkills2(dir) {
25565
25582
  let names;
@@ -25843,6 +25860,10 @@ function discoverWorkspaces(registry2) {
25843
25860
  }
25844
25861
  return out;
25845
25862
  }
25863
+ function stateEtag(accessLevel, skills, workspaces, uptime) {
25864
+ const json = JSON.stringify({ accessLevel, skills, workspaces, uptimeMin: Math.floor(uptime / 60) });
25865
+ return `W/"${crypto6.createHash("sha1").update(json).digest("base64url")}"`;
25866
+ }
25846
25867
  function createShowRoutes(registry2) {
25847
25868
  const app2 = new Hono12();
25848
25869
  function auth(c) {
@@ -25875,11 +25896,18 @@ function createShowRoutes(registry2) {
25875
25896
  }
25876
25897
  }
25877
25898
  workspaces.sort((x, y) => workspaceBirth(x.path) - workspaceBirth(y.path) || x.key.localeCompare(y.key));
25899
+ const uptime = process.uptime();
25900
+ const skills = scanSkills2(GLOBAL_SKILLS_DIR);
25901
+ const etag = stateEtag(a.accessLevel, skills, workspaces, uptime);
25902
+ c.header("ETag", etag);
25903
+ c.header("Cache-Control", "private, no-cache");
25904
+ if (etagMatches(c.req.header("if-none-match"), etag))
25905
+ return c.body(null, 304);
25878
25906
  return c.json({
25879
25907
  serverTime: Date.now(),
25880
- uptime: process.uptime(),
25908
+ uptime,
25881
25909
  accessLevel: a.accessLevel,
25882
- skills: scanSkills2(GLOBAL_SKILLS_DIR),
25910
+ skills,
25883
25911
  workspaces
25884
25912
  });
25885
25913
  });
@@ -25957,6 +25985,7 @@ var init_halo_city = __esm({
25957
25985
  init_session_store();
25958
25986
  init_session_types();
25959
25987
  init_mtime_cache();
25988
+ init_etag();
25960
25989
  SESSIONS_PER_WS = 80;
25961
25990
  _skillCache = createMtimeCache();
25962
25991
  _metaCache = /* @__PURE__ */ new Map();
@@ -28876,7 +28905,7 @@ CREATE INDEX IF NOT EXISTS idx_cron_runs_status ON cron_runs(status);
28876
28905
  // ../server/dist/a2a/exposure.js
28877
28906
  import fs65 from "node:fs";
28878
28907
  import path74 from "node:path";
28879
- import crypto6 from "node:crypto";
28908
+ import crypto7 from "node:crypto";
28880
28909
  function samePath(a, b, platform = process.platform) {
28881
28910
  if (platform !== "win32")
28882
28911
  return a === b;
@@ -28942,7 +28971,7 @@ function buildCard(workspace, interfaceUrl, caps) {
28942
28971
  return {
28943
28972
  name: raw.name,
28944
28973
  description: raw.description,
28945
- version: typeof raw.version === "string" ? raw.version : "1.6.5",
28974
+ version: typeof raw.version === "string" ? raw.version : "1.6.6",
28946
28975
  supportedInterfaces: [{ url: interfaceUrl, protocolBinding: "JSONRPC", protocolVersion: A2A_VERSION }],
28947
28976
  capabilities: { streaming: caps.streaming, pushNotifications: true, extendedAgentCard: false },
28948
28977
  ...caps.tokenAuth === false ? {} : {
@@ -28958,7 +28987,7 @@ function buildCard(workspace, interfaceUrl, caps) {
28958
28987
  };
28959
28988
  }
28960
28989
  function etagOf(body) {
28961
- return `"${crypto6.createHash("sha1").update(body).digest("base64url")}"`;
28990
+ return `W/"${crypto7.createHash("sha1").update(body).digest("base64url")}"`;
28962
28991
  }
28963
28992
  var CARD_FILE, IO_MODES, baseCache, homeStrategies;
28964
28993
  var init_exposure = __esm({
@@ -29001,7 +29030,7 @@ var init_exposure = __esm({
29001
29030
  // ../server/dist/a2a/routes.js
29002
29031
  import { Hono as Hono16 } from "hono";
29003
29032
  import { streamSSE } from "hono/streaming";
29004
- import crypto7 from "node:crypto";
29033
+ import crypto8 from "node:crypto";
29005
29034
  function str(v) {
29006
29035
  return typeof v === "string" ? v : "";
29007
29036
  }
@@ -29111,7 +29140,7 @@ async function dispatchMessage(x, p) {
29111
29140
  }
29112
29141
  const newSession = !sessionId;
29113
29142
  if (newSession)
29114
- sessionId = `${sessionPrefixFor(x.caller)}${Date.now().toString(36)}${crypto7.randomBytes(3).toString("hex")}`;
29143
+ sessionId = `${sessionPrefixFor(x.caller)}${Date.now().toString(36)}${crypto8.randomBytes(3).toString("hex")}`;
29115
29144
  const followUp = task !== null;
29116
29145
  if (!task) {
29117
29146
  try {
@@ -29315,7 +29344,7 @@ function createA2ARoutes(deps) {
29315
29344
  const etag = etagOf(body);
29316
29345
  c.header("Cache-Control", "private, max-age=300");
29317
29346
  c.header("ETag", etag);
29318
- if (c.req.header("if-none-match") === etag)
29347
+ if (etagMatches(c.req.header("if-none-match"), etag))
29319
29348
  return c.body(null, 304);
29320
29349
  return c.body(body, 200, { "Content-Type": "application/json" });
29321
29350
  });
@@ -29390,6 +29419,7 @@ var init_routes = __esm({
29390
29419
  init_commands2();
29391
29420
  init_accounts();
29392
29421
  init_session_prefix();
29422
+ init_etag();
29393
29423
  init_url_policy();
29394
29424
  init_files();
29395
29425
  init_exposure();
@@ -29428,7 +29458,7 @@ var init_routes = __esm({
29428
29458
  import fs66 from "node:fs";
29429
29459
  import os10 from "node:os";
29430
29460
  import path75 from "node:path";
29431
- import crypto8 from "node:crypto";
29461
+ import crypto9 from "node:crypto";
29432
29462
  import { Hono as Hono17 } from "hono";
29433
29463
  function readLease(file) {
29434
29464
  let raw;
@@ -29611,7 +29641,7 @@ var init_agentcore = __esm({
29611
29641
  WorkspaceLease = class {
29612
29642
  file;
29613
29643
  hooks;
29614
- owner = crypto8.randomUUID();
29644
+ owner = crypto9.randomUUID();
29615
29645
  _held = false;
29616
29646
  timer = null;
29617
29647
  waitLogged = false;
@@ -32720,7 +32750,7 @@ var init_descriptor = __esm({
32720
32750
  });
32721
32751
 
32722
32752
  // ../server/dist/channels/wechat/api.js
32723
- import crypto9 from "node:crypto";
32753
+ import crypto10 from "node:crypto";
32724
32754
  function buildClientVersion(version) {
32725
32755
  const [ma, mi, pa] = version.split(".").map((p) => parseInt(p, 10));
32726
32756
  return (ma & 255) << 16 | (mi & 255) << 8 | pa & 255;
@@ -32729,7 +32759,7 @@ function buildBaseInfo() {
32729
32759
  return { channel_version: CHANNEL_VERSION };
32730
32760
  }
32731
32761
  function randomWechatUin() {
32732
- const n = crypto9.randomBytes(4).readUInt32BE(0);
32762
+ const n = crypto10.randomBytes(4).readUInt32BE(0);
32733
32763
  return Buffer.from(String(n), "utf-8").toString("base64");
32734
32764
  }
32735
32765
  function commonHeaders() {
@@ -34106,7 +34136,7 @@ var init_descriptor2 = __esm({
34106
34136
  // ../server/dist/routes/web.js
34107
34137
  import { Hono as Hono22 } from "hono";
34108
34138
  import { streamSSE as streamSSE2 } from "hono/streaming";
34109
- import crypto10 from "node:crypto";
34139
+ import crypto11 from "node:crypto";
34110
34140
  import fs77 from "node:fs";
34111
34141
  function streamWithKeepalive(c, chunks) {
34112
34142
  return streamSSE2(c, async (stream) => {
@@ -34144,8 +34174,8 @@ function createWebRoutes(deps) {
34144
34174
  const bodyError = accountBodyError(body, ACCOUNT_ACCESS_LEVELS);
34145
34175
  if (bodyError)
34146
34176
  return c.json({ error: bodyError }, 400);
34147
- const accountId = crypto10.randomUUID().slice(0, 8);
34148
- const token = crypto10.randomBytes(24).toString("base64url");
34177
+ const accountId = crypto11.randomUUID().slice(0, 8);
34178
+ const token = crypto11.randomBytes(24).toString("base64url");
34149
34179
  insertAccount2(db3, {
34150
34180
  accountId,
34151
34181
  token,
@@ -34483,7 +34513,7 @@ function formatForFeishu(input) {
34483
34513
  inFence = !inFence;
34484
34514
  continue;
34485
34515
  }
34486
- if (inFence) {
34516
+ if (inFence || /^MEDIA:/.test(raw)) {
34487
34517
  out.push(raw);
34488
34518
  continue;
34489
34519
  }
@@ -34508,16 +34538,16 @@ var init_markdown = __esm({
34508
34538
  });
34509
34539
 
34510
34540
  // ../server/dist/channels/slack/event-adapter.js
34511
- var HARD_CHARS, SlackResponder;
34541
+ var SLACK_TEXT_LIMIT, SlackResponder;
34512
34542
  var init_event_adapter3 = __esm({
34513
34543
  "../server/dist/channels/slack/event-adapter.js"() {
34514
34544
  "use strict";
34515
34545
  init_markdown();
34516
34546
  init_responder();
34517
- HARD_CHARS = 35e3;
34547
+ SLACK_TEXT_LIMIT = 35e3;
34518
34548
  SlackResponder = class extends ChunkedResponder {
34519
34549
  constructor(deps) {
34520
- super(deps, { limit: HARD_CHARS, logTag: "Slack", format: formatForSlack });
34550
+ super(deps, { limit: SLACK_TEXT_LIMIT, logTag: "Slack", format: formatForSlack });
34521
34551
  }
34522
34552
  };
34523
34553
  }
@@ -35253,18 +35283,17 @@ async function dispatch3(accountId, text6, explicitChatId, media) {
35253
35283
  const { channel, threadTs } = parseChatId(explicitChatId);
35254
35284
  const out = [];
35255
35285
  if (text6) {
35256
- try {
35257
- await postMessage({ botToken: acct.botToken, channel, threadTs, text: text6 });
35258
- out.push({ channelType: "slack", accountId, chatId: explicitChatId, ok: true });
35259
- } catch (err) {
35260
- out.push({
35261
- channelType: "slack",
35262
- accountId,
35263
- chatId: explicitChatId,
35264
- ok: false,
35265
- error: err instanceof Error ? err.message : String(err)
35266
- });
35286
+ const chunks = splitText(text6, SLACK_TEXT_LIMIT);
35287
+ let failed;
35288
+ for (const [i, chunk] of chunks.entries()) {
35289
+ try {
35290
+ await postMessage({ botToken: acct.botToken, channel, threadTs, text: formatForSlack(chunk) });
35291
+ } catch (err) {
35292
+ failed = `chunk ${i + 1}/${chunks.length}: ${err instanceof Error ? err.message : String(err)}`;
35293
+ break;
35294
+ }
35267
35295
  }
35296
+ out.push(failed ? { channelType: "slack", accountId, chatId: explicitChatId, ok: false, error: failed } : { channelType: "slack", accountId, chatId: explicitChatId, ok: true });
35268
35297
  }
35269
35298
  for (const filePath of media?.paths ?? []) {
35270
35299
  if (!isMediaPathAllowed(filePath, media.workspacePath)) {
@@ -35303,6 +35332,9 @@ var init_cron_dispatcher3 = __esm({
35303
35332
  init_channel_db();
35304
35333
  init_accounts5();
35305
35334
  init_api2();
35335
+ init_event_adapter3();
35336
+ init_chunk();
35337
+ init_markdown();
35306
35338
  init_media();
35307
35339
  init_dispatcher();
35308
35340
  }
@@ -35380,16 +35412,16 @@ var init_accounts6 = __esm({
35380
35412
  });
35381
35413
 
35382
35414
  // ../server/dist/channels/feishu/event-adapter.js
35383
- var HARD_CHARS2, FeishuResponder;
35415
+ var FEISHU_TEXT_LIMIT, FeishuResponder;
35384
35416
  var init_event_adapter4 = __esm({
35385
35417
  "../server/dist/channels/feishu/event-adapter.js"() {
35386
35418
  "use strict";
35387
35419
  init_markdown();
35388
35420
  init_responder();
35389
- HARD_CHARS2 = 4500;
35421
+ FEISHU_TEXT_LIMIT = 4500;
35390
35422
  FeishuResponder = class extends ChunkedResponder {
35391
35423
  constructor(deps) {
35392
- super(deps, { limit: HARD_CHARS2, logTag: "Feishu", format: formatForFeishu });
35424
+ super(deps, { limit: FEISHU_TEXT_LIMIT, logTag: "Feishu", format: formatForFeishu });
35393
35425
  }
35394
35426
  };
35395
35427
  }
@@ -36211,25 +36243,28 @@ async function dispatch4(accountId, text6, explicitChatId) {
36211
36243
  throw new Error('feishu cron target requires an explicit chatId (e.g. "oc_abcd1234"). Create the cron from inside a feishu chat to auto-pin, or pass --targets feishu:<accountId>:<chatId>.');
36212
36244
  }
36213
36245
  const { chatId } = parseChatId2(explicitChatId);
36214
- try {
36215
- await sendMessage3({
36216
- appId: acct.appId,
36217
- appSecret: acct.appSecret,
36218
- receiveIdType: "chat_id",
36219
- receiveId: chatId,
36220
- msgType: "text",
36221
- content: { text: text6 }
36222
- });
36223
- return [{ channelType: "feishu", accountId, chatId: explicitChatId, ok: true }];
36224
- } catch (err) {
36225
- return [{
36226
- channelType: "feishu",
36227
- accountId,
36228
- chatId: explicitChatId,
36229
- ok: false,
36230
- error: err instanceof Error ? err.message : String(err)
36231
- }];
36246
+ const chunks = splitText(text6, FEISHU_TEXT_LIMIT);
36247
+ for (const [i, chunk] of chunks.entries()) {
36248
+ try {
36249
+ await sendMessage3({
36250
+ appId: acct.appId,
36251
+ appSecret: acct.appSecret,
36252
+ receiveIdType: "chat_id",
36253
+ receiveId: chatId,
36254
+ msgType: "text",
36255
+ content: { text: formatForFeishu(chunk) }
36256
+ });
36257
+ } catch (err) {
36258
+ return [{
36259
+ channelType: "feishu",
36260
+ accountId,
36261
+ chatId: explicitChatId,
36262
+ ok: false,
36263
+ error: `chunk ${i + 1}/${chunks.length}: ${err instanceof Error ? err.message : String(err)}`
36264
+ }];
36265
+ }
36232
36266
  }
36267
+ return [{ channelType: "feishu", accountId, chatId: explicitChatId, ok: true }];
36233
36268
  }
36234
36269
  function listTargets4() {
36235
36270
  const cdb = getChannelDb();
@@ -36251,6 +36286,9 @@ var init_cron_dispatcher4 = __esm({
36251
36286
  init_channel_db();
36252
36287
  init_accounts6();
36253
36288
  init_api3();
36289
+ init_event_adapter4();
36290
+ init_chunk();
36291
+ init_markdown();
36254
36292
  init_dispatcher();
36255
36293
  }
36256
36294
  });
@@ -36324,15 +36362,15 @@ var init_accounts7 = __esm({
36324
36362
  });
36325
36363
 
36326
36364
  // ../server/dist/channels/wecom/event-adapter.js
36327
- var HARD_CHARS3, WecomResponder;
36365
+ var WECOM_TEXT_LIMIT, WecomResponder;
36328
36366
  var init_event_adapter5 = __esm({
36329
36367
  "../server/dist/channels/wecom/event-adapter.js"() {
36330
36368
  "use strict";
36331
36369
  init_responder();
36332
- HARD_CHARS3 = 5e3;
36370
+ WECOM_TEXT_LIMIT = 5e3;
36333
36371
  WecomResponder = class extends ChunkedResponder {
36334
36372
  constructor(deps) {
36335
- super(deps, { limit: HARD_CHARS3, logTag: "WeCom" });
36373
+ super(deps, { limit: WECOM_TEXT_LIMIT, logTag: "WeCom" });
36336
36374
  }
36337
36375
  };
36338
36376
  }
@@ -36838,18 +36876,21 @@ async function dispatch5(accountId, text6, explicitChatId) {
36838
36876
  if (!client) {
36839
36877
  return [{ channelType: "wecom", accountId, chatId: explicitChatId, ok: false, error: "wecom long-connect not active" }];
36840
36878
  }
36841
- try {
36842
- await client.sendMessage(explicitChatId, { msgtype: "markdown", markdown: { content: text6 } });
36843
- return [{ channelType: "wecom", accountId, chatId: explicitChatId, ok: true }];
36844
- } catch (err) {
36845
- return [{
36846
- channelType: "wecom",
36847
- accountId,
36848
- chatId: explicitChatId,
36849
- ok: false,
36850
- error: err instanceof Error ? err.message : String(err)
36851
- }];
36879
+ const chunks = splitText(text6, WECOM_TEXT_LIMIT);
36880
+ for (const [i, chunk] of chunks.entries()) {
36881
+ try {
36882
+ await client.sendMessage(explicitChatId, { msgtype: "markdown", markdown: { content: chunk } });
36883
+ } catch (err) {
36884
+ return [{
36885
+ channelType: "wecom",
36886
+ accountId,
36887
+ chatId: explicitChatId,
36888
+ ok: false,
36889
+ error: `chunk ${i + 1}/${chunks.length}: ${err instanceof Error ? err.message : String(err)}`
36890
+ }];
36891
+ }
36852
36892
  }
36893
+ return [{ channelType: "wecom", accountId, chatId: explicitChatId, ok: true }];
36853
36894
  }
36854
36895
  function listTargets5() {
36855
36896
  const cdb = getChannelDb();
@@ -36871,6 +36912,8 @@ var init_cron_dispatcher5 = __esm({
36871
36912
  init_channel_db();
36872
36913
  init_accounts7();
36873
36914
  init_handler7();
36915
+ init_event_adapter5();
36916
+ init_chunk();
36874
36917
  init_dispatcher();
36875
36918
  }
36876
36919
  });
@@ -36915,6 +36958,64 @@ var init_descriptors = __esm({
36915
36958
  }
36916
36959
  });
36917
36960
 
36961
+ // ../server/dist/middleware/compress.js
36962
+ import { compress, COMPRESSIBLE_CONTENT_TYPE_REGEX } from "hono/compress";
36963
+ function compressResponses(threshold) {
36964
+ const inner = compress({ threshold });
36965
+ return (c, next) => inner(c, async () => {
36966
+ await next();
36967
+ if (c.res.status === 304)
36968
+ varyAcceptEncoding(c);
36969
+ else
36970
+ await setProbedLength(c, threshold);
36971
+ });
36972
+ }
36973
+ function varyAcceptEncoding(c) {
36974
+ const vary = c.res.headers.get("Vary");
36975
+ if (vary === "*" || vary && VARY_ACCEPT_ENCODING_RE.test(vary))
36976
+ return;
36977
+ c.header("Vary", vary ? `${vary}, Accept-Encoding` : "Accept-Encoding");
36978
+ }
36979
+ async function setProbedLength(c, threshold) {
36980
+ const res = c.res;
36981
+ const type = res.headers.get("Content-Type");
36982
+ if (res.headers.has("Content-Length") || res.headers.has("Content-Encoding") || res.headers.has("Transfer-Encoding") || c.req.method === "HEAD" || res.status === 204 || res.status === 206 || !res.body || !type || !COMPRESSIBLE_CONTENT_TYPE_REGEX.test(type))
36983
+ return;
36984
+ const reader = res.body.getReader();
36985
+ const head = [];
36986
+ let size = 0;
36987
+ while (size < threshold) {
36988
+ const { done, value } = await reader.read();
36989
+ if (done) {
36990
+ c.res = new Response(Buffer.concat(head, size), res);
36991
+ c.res.headers.set("Content-Length", String(size));
36992
+ return;
36993
+ }
36994
+ head.push(value);
36995
+ size += value.byteLength;
36996
+ }
36997
+ c.res = new Response(new ReadableStream({
36998
+ async pull(ctrl) {
36999
+ const chunk = head.shift();
37000
+ if (chunk)
37001
+ return ctrl.enqueue(chunk);
37002
+ const { done, value } = await reader.read();
37003
+ if (done)
37004
+ ctrl.close();
37005
+ else
37006
+ ctrl.enqueue(value);
37007
+ },
37008
+ cancel: (reason) => reader.cancel(reason)
37009
+ }), res);
37010
+ }
37011
+ var VARY_ACCEPT_ENCODING_RE;
37012
+ var init_compress = __esm({
37013
+ "../server/dist/middleware/compress.js"() {
37014
+ "use strict";
37015
+ VARY_ACCEPT_ENCODING_RE = /(?:^|,)\s*accept-encoding\s*(?:,|$)/i;
37016
+ }
37017
+ });
37018
+
36918
37019
  // ../server/dist/listen-banner.js
36919
37020
  function formatListenBanner(address, port, version) {
36920
37021
  if (address === "::" || address === "0.0.0.0") {
@@ -36936,7 +37037,6 @@ var init_listen_banner = __esm({
36936
37037
  var dist_exports = {};
36937
37038
  import { Hono as Hono26 } from "hono";
36938
37039
  import { cors } from "hono/cors";
36939
- import { compress } from "hono/compress";
36940
37040
  import { serve } from "@hono/node-server";
36941
37041
  import { serveStatic } from "@hono/node-server/serve-static";
36942
37042
  import { WebSocketServer as WebSocketServer4 } from "ws";
@@ -37128,6 +37228,7 @@ var init_dist2 = __esm({
37128
37228
  init_registry4();
37129
37229
  init_descriptors();
37130
37230
  init_auth();
37231
+ init_compress();
37131
37232
  init_logger();
37132
37233
  init_listen_banner();
37133
37234
  init_otel();
@@ -37137,7 +37238,7 @@ var init_dist2 = __esm({
37137
37238
  init_git_ssh();
37138
37239
  __dirname3 = path87.dirname(fileURLToPath3(import.meta.url));
37139
37240
  PORT = config.server.port;
37140
- HALO_VERSION = "1.6.5";
37241
+ HALO_VERSION = "1.6.6";
37141
37242
  PROJECT_ROOT = findProjectRoot();
37142
37243
  HALO_HOME4 = path87.join(homedir25(), ".halo");
37143
37244
  GIT_SHA = readGitSha();
@@ -37181,7 +37282,7 @@ var init_dist2 = __esm({
37181
37282
  maxAge: 86400,
37182
37283
  credentials: allowlist.length > 0
37183
37284
  }));
37184
- app.use("/*", compress({ threshold: 1024 }));
37285
+ app.use("/*", compressResponses(1024));
37185
37286
  jupyter = AGENTCORE ? null : createJupyterManager();
37186
37287
  if (!AGENTCORE) {
37187
37288
  app.use("/api/*", authMiddleware());
@@ -38747,7 +38848,7 @@ function installUserUnit(opts, run = runCommand) {
38747
38848
  }
38748
38849
 
38749
38850
  // src/index.ts
38750
- var VERSION = "1.6.5";
38851
+ var VERSION = "1.6.6";
38751
38852
  var HELP_TOP = `Usage: halo <command> [options]
38752
38853
 
38753
38854
  Commands:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turmind/halo",
3
- "version": "1.6.5",
3
+ "version": "1.6.6",
4
4
  "description": "Halo — multi-agent collaborative workspace. CLI + TUI + HTTP/WS server in one package.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,6 +1,6 @@
1
1
  id: mimo-token-plan-china
2
2
  runtime: anthropic-messages
3
- revision: 2026101002
3
+ revision: 2026101401
4
4
  displayName: Xiaomi MiMo
5
5
  displayName_zh: 小米 MiMo
6
6
  description: Xiaomi MiMo via its Anthropic-compatible gateway — Token Plan (China / Singapore / Europe clusters) or pay-as-you-go. Supports 1M context window.
@@ -28,9 +28,8 @@ secrets:
28
28
  # gives `mimo-v2.6-pro` / `mimo-v2.6-flash` 1M context, 128K (131072) max
29
29
  # output, full-modal understanding (image input on both — V2.5 only had it on
30
30
  # the non-pro tier). `mimo-v2.6-pro-ultraspeed` is "contact us" only, not
31
- # listed. `mimo-v2.5-pro` / `mimo-v2.5` are deprecated 2026-10-21 10:00 CST —
32
- # kept at the tail (tagged) so agents still on them keep their registry
33
- # capabilities until the cutoff; drop both after that date. Thinking shape unchanged from V2.5 — Anthropic-protocol
31
+ # listed. `mimo-v2.5-pro` / `mimo-v2.5` (officially retired 2026-10-21) were
32
+ # dropped from this list on 2026-10-14. Thinking shape unchanged from V2.5 — Anthropic-protocol
34
33
  # `thinking.type` enabled/disabled, on by default; the docs say effort levels
35
34
  # are not differentiated yet, so the adaptive presets below are a UI knob the
36
35
  # gateway accepts, not a real intensity dial. Not live-probed (no valid key at
@@ -85,54 +84,3 @@ models:
85
84
  - { value: medium, label: Medium }
86
85
  - { value: high, label: High }
87
86
  - { value: max, label: Max }
88
-
89
- # --- deprecated 2026-10-21 10:00 CST (official notice) — remove after cutoff ---
90
- - id: mimo-v2.5-pro
91
- displayName: MiMo v2.5 Pro (deprecated 2026-10-21)
92
- maxOutputTokens: 131072
93
- contextWindow: 272000
94
- maxContextWindow: 1000000
95
- compressAt: 0.9
96
- capabilities:
97
- image: false
98
- video: false
99
- audio: false
100
- promptCaching:
101
- default: 1h
102
- ttlPresets:
103
- - { value: 5m, label: 5min }
104
- - { value: 1h, label: 1hour }
105
- thinking:
106
- mode: adaptive
107
- defaultEnabled: true
108
- default: high
109
- effortPresets:
110
- - { value: low, label: Low }
111
- - { value: medium, label: Medium }
112
- - { value: high, label: High }
113
- - { value: max, label: Max }
114
-
115
- - id: mimo-v2.5
116
- displayName: MiMo v2.5 (deprecated 2026-10-21)
117
- maxOutputTokens: 131072
118
- contextWindow: 272000
119
- maxContextWindow: 1000000
120
- compressAt: 0.9
121
- capabilities:
122
- image: true
123
- video: false
124
- audio: false
125
- promptCaching:
126
- default: 1h
127
- ttlPresets:
128
- - { value: 5m, label: 5min }
129
- - { value: 1h, label: 1hour }
130
- thinking:
131
- mode: adaptive
132
- defaultEnabled: true
133
- default: high
134
- effortPresets:
135
- - { value: low, label: Low }
136
- - { value: medium, label: Medium }
137
- - { value: high, label: High }
138
- - { value: max, label: Max }