@turmind/halo 2.2.1 → 2.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- <!DOCTYPE html><!--j4QFRrUtg33DNMUIgg4ij--><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/8fc43fce83e5ce5d.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-9532ec7f1fa9a757.js"/><script src="/_next/static/chunks/1b86ba38-4193816aebc732b9.js" async=""></script><script src="/_next/static/chunks/9698-0157e7ac70c3f9fc.js" async=""></script><script src="/_next/static/chunks/main-app-5e981adb363c52e5.js" async=""></script><script src="/_next/static/chunks/1989-3b2faa2731d973c4.js" async=""></script><script src="/_next/static/chunks/app/layout-0ed29966e0abc26b.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-9532ec7f1fa9a757.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,[\"1989\",\"static/chunks/1989-3b2faa2731d973c4.js\",\"7177\",\"static/chunks/app/layout-0ed29966e0abc26b.js\"],\"ThemeProvider\"]\n3:I[24554,[\"1989\",\"static/chunks/1989-3b2faa2731d973c4.js\",\"7177\",\"static/chunks/app/layout-0ed29966e0abc26b.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/8fc43fce83e5ce5d.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"j4QFRrUtg33DNMUIgg4ij\",\"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/8fc43fce83e5ce5d.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><!--u9pg60rITCj73MBYeXsW1--><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/8fc43fce83e5ce5d.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-9532ec7f1fa9a757.js"/><script src="/_next/static/chunks/1b86ba38-4193816aebc732b9.js" async=""></script><script src="/_next/static/chunks/9698-0157e7ac70c3f9fc.js" async=""></script><script src="/_next/static/chunks/main-app-5e981adb363c52e5.js" async=""></script><script src="/_next/static/chunks/1989-3b2faa2731d973c4.js" async=""></script><script src="/_next/static/chunks/app/layout-0ed29966e0abc26b.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-9532ec7f1fa9a757.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,[\"1989\",\"static/chunks/1989-3b2faa2731d973c4.js\",\"7177\",\"static/chunks/app/layout-0ed29966e0abc26b.js\"],\"ThemeProvider\"]\n3:I[24554,[\"1989\",\"static/chunks/1989-3b2faa2731d973c4.js\",\"7177\",\"static/chunks/app/layout-0ed29966e0abc26b.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/8fc43fce83e5ce5d.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"u9pg60rITCj73MBYeXsW1\",\"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/8fc43fce83e5ce5d.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><!--j4QFRrUtg33DNMUIgg4ij--><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/8fc43fce83e5ce5d.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-9532ec7f1fa9a757.js"/><script src="/_next/static/chunks/1b86ba38-4193816aebc732b9.js" async=""></script><script src="/_next/static/chunks/9698-0157e7ac70c3f9fc.js" async=""></script><script src="/_next/static/chunks/main-app-5e981adb363c52e5.js" async=""></script><script src="/_next/static/chunks/1989-3b2faa2731d973c4.js" async=""></script><script src="/_next/static/chunks/app/layout-0ed29966e0abc26b.js" async=""></script><script src="/_next/static/chunks/1d2d5650-c677408087984c7a.js" async=""></script><script src="/_next/static/chunks/1607-bf227fc5e4db45b7.js" async=""></script><script src="/_next/static/chunks/app/page-10a4afac9cbc27a2.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-9532ec7f1fa9a757.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,[\"1989\",\"static/chunks/1989-3b2faa2731d973c4.js\",\"7177\",\"static/chunks/app/layout-0ed29966e0abc26b.js\"],\"ThemeProvider\"]\n3:I[24554,[\"1989\",\"static/chunks/1989-3b2faa2731d973c4.js\",\"7177\",\"static/chunks/app/layout-0ed29966e0abc26b.js\"],\"AppI18nProvider\"]\n4:I[63693,[],\"\"]\n5:I[74713,[],\"\"]\n6:I[5444,[],\"ClientPageRoot\"]\n7:I[47333,[\"4560\",\"static/chunks/1d2d5650-c677408087984c7a.js\",\"1607\",\"static/chunks/1607-bf227fc5e4db45b7.js\",\"1989\",\"static/chunks/1989-3b2faa2731d973c4.js\",\"8974\",\"static/chunks/app/page-10a4afac9cbc27a2.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/8fc43fce83e5ce5d.css\",\"style\"]\n:HL[\"/_next/static/css/659eccb5db697b76.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"j4QFRrUtg33DNMUIgg4ij\",\"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/8fc43fce83e5ce5d.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><!--u9pg60rITCj73MBYeXsW1--><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/8fc43fce83e5ce5d.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-9532ec7f1fa9a757.js"/><script src="/_next/static/chunks/1b86ba38-4193816aebc732b9.js" async=""></script><script src="/_next/static/chunks/9698-0157e7ac70c3f9fc.js" async=""></script><script src="/_next/static/chunks/main-app-5e981adb363c52e5.js" async=""></script><script src="/_next/static/chunks/1989-3b2faa2731d973c4.js" async=""></script><script src="/_next/static/chunks/app/layout-0ed29966e0abc26b.js" async=""></script><script src="/_next/static/chunks/1d2d5650-c677408087984c7a.js" async=""></script><script src="/_next/static/chunks/1607-bf227fc5e4db45b7.js" async=""></script><script src="/_next/static/chunks/app/page-10a4afac9cbc27a2.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-9532ec7f1fa9a757.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,[\"1989\",\"static/chunks/1989-3b2faa2731d973c4.js\",\"7177\",\"static/chunks/app/layout-0ed29966e0abc26b.js\"],\"ThemeProvider\"]\n3:I[24554,[\"1989\",\"static/chunks/1989-3b2faa2731d973c4.js\",\"7177\",\"static/chunks/app/layout-0ed29966e0abc26b.js\"],\"AppI18nProvider\"]\n4:I[63693,[],\"\"]\n5:I[74713,[],\"\"]\n6:I[5444,[],\"ClientPageRoot\"]\n7:I[47333,[\"4560\",\"static/chunks/1d2d5650-c677408087984c7a.js\",\"1607\",\"static/chunks/1607-bf227fc5e4db45b7.js\",\"1989\",\"static/chunks/1989-3b2faa2731d973c4.js\",\"8974\",\"static/chunks/app/page-10a4afac9cbc27a2.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/8fc43fce83e5ce5d.css\",\"style\"]\n:HL[\"/_next/static/css/659eccb5db697b76.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"u9pg60rITCj73MBYeXsW1\",\"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/8fc43fce83e5ce5d.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/8fc43fce83e5ce5d.css","style"]
17
17
  :HL["/_next/static/css/659eccb5db697b76.css","style"]
18
- 0:{"P":null,"b":"j4QFRrUtg33DNMUIgg4ij","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/8fc43fce83e5ce5d.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":"u9pg60rITCj73MBYeXsW1","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/8fc43fce83e5ce5d.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"}]]
@@ -308,7 +308,7 @@ The hub repo keeps one directory per extension (`glb` / `ipynb` commit their ven
308
308
  - `ipynb` 2.0.1 — `.ipynb`, `default`, `save`: notebook editor (markdown, KaTeX math, highlighted code, outputs incl. images / HTML tables; no widgets / JS outputs) that runs cells through the Jupyter proxy above when the host sends `init.jupyter` (Halo ≥ 1.6.1); older hosts edit and save without run controls.
309
309
  - `drawio` 1.1.0 — `.drawio .dio`, `default`, `['save']`; offline draw.io v32.0.2 (~22 MB zip).
310
310
  - `excalidraw` 1.1.0 — `.excalidraw`, `default`, `['save']`; offline Excalidraw 0.18 (~16 MB zip).
311
- - `htrans` 1.0.9 — `.htrans`, `bundle`, `['media', 'transcribe']`, platforms `web` / `desktop-mac` / `desktop-win`: meeting recorder (mic + system sound, live captions through the [transcription proxy](#streaming-transcription-proxy), optionally labelled by speaker — the "Identify speakers" checkbox, off by default, stored in `meeting.json` `asr.speakers` — timed screenshots). One Start / Pause / Resume button sits in the header bar next to the timer; pausing ends the current run and Resume starts the next run in the same meeting. A failed package write is kept — logged in the browser's IndexedDB before it is sent — and retried in order until it lands, with a red notice ("login expired" when the `fs-result` carries `auth`); what a reload or crash left unsaved is written the next time the package opens in the same browser.
311
+ - `htrans` 1.0.10 — `.htrans`, `bundle`, `['media', 'transcribe', 'stt-local']`, platforms `web` / `desktop-mac` / `desktop-win`: meeting recorder (mic + system sound, live captions, optionally labelled by speaker — the "Identify speakers" checkbox, off by default, stored in `meeting.json` `asr.speakers` — timed screenshots). A transcription-engine picker chooses Amazon Transcribe through the [transcription proxy](#streaming-transcription-proxy) (default) or on-device SenseVoice through the [stt-local](stt-local.md) WS; "Identify speakers" works with both (the local engine gets `speakers=1`). Because it declares `stt-local`, installing it starts the shared STT model download. One Start / Pause / Resume button sits in the header bar next to the timer; pausing ends the current run and Resume starts the next run in the same meeting. A failed package write is kept — logged in the browser's IndexedDB before it is sent — and retried in order until it lands, with a red notice ("login expired" when the `fs-result` carries `auth`); what a reload or crash left unsaved is written the next time the package opens in the same browser.
312
312
  - `megadrive` 2.1.0 — `.mega`, `bundle`, `['fs-read']`: offline Mega Drive / Genesis (+ Master System / Game Gear) emulator (EmulatorJS + Genesis Plus GX); the ROM is picked anywhere on the machine and read in place. No ROMs included.
313
313
  - `arcade` 2.2.0 — `.arcade`, `bundle`, `['fs-read']`: offline arcade emulator (EmulatorJS + FinalBurn Neo), romset `.zip` read in place the same way. No ROMs included.
314
314
  - `halo-city` 1.0.2 — `extensions: []` + `activity`; `minHalo` 1.6.5: the [Halo City](halo-city.md) runtime visualizer as an activity-bar page.
@@ -16,7 +16,7 @@ The server's agents work on the server's filesystem. Users also want them to tou
16
16
  | Routes | `nodes/routes.ts` | Admin CRUD (`/api/nodes/accounts`) |
17
17
  | Client | `nodes/node-client.ts` | `runNode()` — the `halo node` process: connect, reconnect with backoff, execute requests |
18
18
  | Executor | `nodes/node-exec.ts` + `node-path.ts` | Machine side. Wraps the **same** `createWorkspaceTools(base, { accessLevel: 'full' })` the server's agents use; adds the root check |
19
- | Go client | `packages/node-go/` | `halo-node`: a static Go binary (linux amd64 / arm64 / armv7, no cgo, ~7 MB) speaking the same protocol v1 with the same flags. It ports the client, the executor and the parts of `workspace-tools.ts` / `sandbox.ts` it needs (the seven ops, rm guard, process-group kill), with JS value, UTF-16 and V8 regex semantics (`internal/jsv`, `internal/jsregex` over regexp2). Ships as release assets via `.github/workflows/node-go.yml`, not npm |
19
+ | Go client | `packages/node-go/` | `halo-node`: a standalone Go binary (Linux amd64 / arm64 / armv7 and Windows amd64, no cgo) speaking the same protocol v1 with the same flags. It ports the client, the executor and the parts of `workspace-tools.ts` / `sandbox.ts` it needs (the seven ops, POSIX rm guard, process-group / Windows Job Object kill), with JS value, UTF-16 and V8 regex semantics (`internal/jsv`, `internal/jsregex` over regexp2). Ships as release assets via `.github/workflows/node-go.yml`, not npm |
20
20
  | Conformance | `packages/server/test/node-go-conformance.test.ts` + `packages/node-go/conformance/` | The TS executor is the oracle: a real hub connects TS `runNode` (`test/helpers/ts-node-main.ts`) and the Go binary side by side, and every result, file and log line is compared. Skipped unless `HALO_NODE_GO_BIN` is set. node-go.yml runs it, along with rm-guard vector `--check` and a 20k regex differential fuzz, on any PR touching either side, so a TS change goes red until Go follows. Accepted differences: [below](#go-client-known-differences-from-the-ts-client) |
21
21
 
22
22
  Not built in AgentCore mode (`nodeHub = AGENTCORE ? null : …`), the CLI / TUI or the evo sandbox — there the tools exist but return `NODES_UNAVAILABLE`.
@@ -42,6 +42,12 @@ Accepted gaps between `halo-node` and `halo node`. In the conformance suite D1 i
42
42
  | D14 | `node_run`'s stdin is a pipe (FIFO), TS's a socketpair: reads and writes behave the same; only a test like `[ -S /dev/stdin ]` tells them apart | nothing writes to it either way |
43
43
  | D15 | Built with `CGO_ENABLED=0`, user lookup reads `/etc/passwd` only, not NSS: for an LDAP / SSSD user the name in the `--allow-shell` log line and the home used when `HOME` is unset can differ (TS asks libc) | log text / an unset `HOME` only |
44
44
  | D16 | A `res` goes out in 32 KiB WebSocket fragments, TS sends one frame: the server's `ws` reassembles before parsing, the 8 MiB cap is per message, and pings can interleave so a slow uplink doesn't starve the pong | wire framing only; pinned by the fragment-count proto check |
45
+ | D17 | Windows cancellation / timeout stops the whole Job Object. Go also observes shell exit independently of output EOF and immediately closes the job, terminating background descendants before draining stdout / stderr. TS's win32 `execAsync` kills only cmd.exe, so workers can survive or keep output pipes open. Go preserves `Command timed out after <ms>ms` and `Command failed: <command>` rather than Node's wrapped `chcp …` error text, and decodes failure output as UTF-8 / GBK too (TS only decodes successful output) | Intentional Windows lifecycle / output correctness; native cancel, timeout, shell-exit and decoder tests. The real TS fix belongs in sandbox.ts's win32 exec branch |
46
+ | D18 | Windows node paths using `\\?\` / `\\.\` namespace or device prefixes (including slash variants), or any component ending in a dot or space other than `.` / `..`, are refused rather than emulated. The same restrictions apply to resolved paths, including shared roots and junction targets, in folder and whole-machine modes | Ordinary Win32 opens can normalize these spellings to a different object after realpath validation. Go rejects them with a node refusal (the existing outside-root message in folder mode); TS does not. Native read/write regressions cover distinct `inside.` / `inside ` directories beside an outward `inside` junction |
47
+
48
+ Windows implementation: the shell starts suspended, is assigned to a `KILL_ON_JOB_CLOSE` job, then resumes. Cancel and timeout call `TerminateJobObject` (no graceful SIGTERM equivalent for hidden cmd.exe), retaining the shared 2 s escalation / 200 ms force-settle ceiling. The byte-capped stdout / stderr streams decode as strict UTF-8, falling back to GBK. Folder checks use case-insensitive Windows `filepath.Rel`, reject other volumes, and resolve the longest existing prefix with `GetFinalPathNameByHandle` (including junctions). A bootstrap cmd.exe gets a new hidden console, runs `chcp 65001`, then starts the command's cmd.exe so it initializes in UTF-8 even without an interactive desktop. The Unicode command passes through `HALO_NODE_COMMAND` in the child environment (delayed expansion in the bootstrap only; the inner `/d /s /c` shell inherits the machine/user `DelayedExpansion` default, like TS), not the bootstrap's legacy-codepage parser. Win32 filesystem errors map through libuv's Node-style errno names. `~/` uses `USERPROFILE`; `~\` stays literal, matching TS.
49
+
50
+ CI keeps the Linux TS-oracle conformance / rm-guard vectors unchanged and adds native Windows vet, tests and exe build. The POSIX conformance fixtures (sh commands, chmod, Unix paths and symlink assumptions) are not run on Windows; native Windows tests cover those platform-specific boundaries instead. The POSIX rm-guard tests are build-excluded there because neither client calls the guard on Windows.
45
51
 
46
52
  ## Two modes
47
53
 
@@ -1,8 +1,8 @@
1
1
  # On-device speech-to-text (`stt-local`)
2
2
 
3
- A canvas extension that declares capability `stt-local` can stream microphone audio to the Halo server and get back text. Everything runs on the server: SenseVoice-Small (int8) decodes, Silero VAD cuts the audio into segments, and `sherpa-onnx-node` hosts both models. There is no cloud call and no credential to configure. Model files are downloaded the first time they're needed. Not mounted in AgentCore mode, since that mode runs neither the extension watcher nor any WS surface.
3
+ A canvas extension that declares capability `stt-local` can stream microphone audio to the Halo server and get back text. Everything runs on the server: SenseVoice-Small (int8) decodes, Silero VAD cuts the audio into segments, optionally pyannote segmentation + CAM++ label who spoke (`speakers=1`), and `sherpa-onnx-node` hosts all of them. There is no cloud call and no credential to configure. Model files are downloaded the first time they're needed. Not mounted in AgentCore mode, since that mode runs neither the extension watcher nor any WS surface.
4
4
 
5
- Code: `packages/server/src/stt/models.ts` (model files + downloader), `stt/engine.ts` (recognizer, VAD, decode queue), `routes/stt-ws.ts` (WS route), `extensions/watcher.ts` (`reconcileSttModels`), mounted in `index.ts` next to the Transcribe proxy.
5
+ Code: `packages/server/src/stt/models.ts` (model files + downloader), `stt/engine.ts` (recognizer, VAD, decode queue, diarizer), `stt/speakers.ts` (speaker matching + split, pure), `routes/stt-ws.ts` (WS route), `extensions/watcher.ts` (`reconcileSttModels`), mounted in `index.ts` next to the Transcribe proxy.
6
6
 
7
7
  ## Models and licenses
8
8
 
@@ -11,6 +11,10 @@ Code: `packages/server/src/stt/models.ts` (model files + downloader), `stt/engin
11
11
  | `sense-voice-zh-en-ja-ko-yue-2024-07-17/model.int8.onnx` | 239233841 | SenseVoice-Small int8 (FunAudioLLM / Alibaba), sherpa-onnx export | [FunASR Model License v1.1](https://github.com/modelscope/FunASR/blob/main/MODEL_LICENSE) |
12
12
  | `sense-voice-zh-en-ja-ko-yue-2024-07-17/tokens.txt` | 315894 | its token table | same |
13
13
  | `silero-vad-v4/silero_vad.onnx` | 1807522 | Silero VAD v4 | MIT |
14
+ | `pyannote-segmentation-3-0/model.int8.onnx` | 1540506 | pyannote/segmentation-3.0 (© CNRS), sherpa-onnx int8 export | MIT |
15
+ | `campplus-zh-en-advanced/3dspeaker_speech_campplus_sv_zh_en_16k-common_advanced.onnx` | 28281164 | 3D-Speaker CAM++ zh/en speaker embedding (Alibaba) | Apache-2.0 |
16
+
17
+ All five are one download set (~271 MB): an install that only uses dictation still fetches the two speaker files (~30 MB) — one state machine, one progress bar, accepted. The speaker models are only *loaded* by a `speakers=1` stream.
14
18
 
15
19
  - **FunASR Model License v1.1** allows free use, copying, modification and sharing, provided you credit the source and the authors and keep the model's name (§2.2). The attribution goes on the **halo-hub extension's description page**. The hub ships no weights: the server pulls them from the sources below, the extension never does.
16
20
  - The VAD is the v4 export (1.8 MB). The 643 KB export the first evaluation used exists only on GitHub releases, not on HF or ModelScope. On sherpa's `test_silero_vad.wav`, the two cut identical segments.
@@ -20,8 +24,9 @@ Code: `packages/server/src/stt/models.ts` (model files + downloader), `stt/engin
20
24
 
21
25
  Each file has its sha256 and size pinned in `STT_MODEL_FILES` (`models.ts`), and response headers are never trusted. Each file tries its sources in order, falling back file by file:
22
26
 
23
- 1. **Hugging Face**, pinned to a commit: `csukuangfj/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17@2365baea…` and `csukuangfj/vad@fba88cd2…`.
24
- 2. **ModelScope** `https://www.modelscope.cn/api/v1/models/<repo>/repo?Revision=master&FilePath=<file>` (`pengzhendong/sherpa-onnx-sense-voice-zh-en-ja-ko-yue`, `pengzhendong/silero-vad` `v4/silero_vad.onnx`). Only this API form 302s to a CDN that honours Range. `resolve/master` returns a full 200 to a Range request.
27
+ 1. **Hugging Face**, pinned to a commit: `csukuangfj/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17@2365baea…`, `csukuangfj/vad@fba88cd2…`, `csukuangfj/sherpa-onnx-pyannote-segmentation-3-0@9403a690…`, `csukuangfj/speaker-embedding-models@0743f301…`.
28
+ 2. **ModelScope** `https://www.modelscope.cn/api/v1/models/<repo>/repo?Revision=master&FilePath=<file>` (`pengzhendong/sherpa-onnx-sense-voice-zh-en-ja-ko-yue`, `pengzhendong/silero-vad` `v4/silero_vad.onnx`, `pengzhendong/sherpa-onnx-pyannote-segmentation-3-0`, `lijiu09/speaker-embedding-models`). Only this API form 302s to a CDN that honours Range. `resolve/master` returns a full 200 to a Range request.
29
+ 3. **GitHub releases** (CAM++ only): `k2-fsa/sherpa-onnx` release `speaker-recongition-models` (sic), honours Range.
25
30
 
26
31
  Mechanics:
27
32
 
@@ -32,7 +37,7 @@ Mechanics:
32
37
  - **Single flight**: one `ensure()` promise. Concurrent triggers join it.
33
38
  - **Shutdown** (`gracefulShutdown` → `stt.close()`) aborts the running request and keeps the `.part`. The next boot resumes it (logged as `resuming at N bytes`).
34
39
 
35
- Measured on the dev box (temp HOME): HF, all three files, 3.5 s total (228 MB model in 2.5 s). ModelScope-only (HF forced to fail): tokens + VAD in 2.4 s. Model aborted at 43 MB, then resumed from ModelScope in 10.9 s. SIGTERM mid-download then reboot resumed from HF in 2.3 s. Every sha matched.
40
+ Measured on the dev box (temp HOME), before the two speaker files were added: HF, all three files then in the set, 3.5 s total (228 MB model in 2.5 s). ModelScope-only (HF forced to fail): tokens + VAD in 2.4 s. Model aborted at 43 MB, then resumed from ModelScope in 10.9 s. SIGTERM mid-download then reboot resumed from HF in 2.3 s. Every sha matched.
36
41
 
37
42
  ### State machine
38
43
 
@@ -50,7 +55,7 @@ missing ──ensure()──▶ downloading{received,total} ──all files ok
50
55
  ### Triggers
51
56
 
52
57
  - **WS connect** (`routes/stt-ws.ts`): if the model isn't ready, `ensure()` runs, and that socket then receives `model` frames until `ready` or `error`.
53
- - **Extensions reconcile** (`extensions/watcher.ts` → `reconcileSttModels`): after `start()`'s first scan and after every `rescanAndBroadcast()` scan (install / uninstall / watcher event). If any valid, switched-on extension declares `stt-local` and the state is neither `ready` nor `downloading`, it calls `ensure()`. This makes installing the extension the moment the ~240 MB fetch starts, and it retries an `error` on the next rescan. The watcher doesn't run in AgentCore mode.
58
+ - **Extensions reconcile** (`extensions/watcher.ts` → `reconcileSttModels`): after `start()`'s first scan and after every `rescanAndBroadcast()` scan (install / uninstall / watcher event). If any valid, switched-on extension declares `stt-local` and the state is neither `ready` nor `downloading`, it calls `ensure()`. This makes installing the extension the moment the ~271 MB fetch starts, and it retries an `error` on the next rescan. The watcher doesn't run in AgentCore mode.
54
59
 
55
60
  ## Engine
56
61
 
@@ -59,6 +64,19 @@ missing ──ensure()──▶ downloading{received,total} ──all files ok
59
64
  - **Global decode queue**: FIFO, concurrency 1 server-wide, via `decodeAsync` (libuv pool; the event loop keeps running). 10 s of speech decodes in ~0.2–0.3 s.
60
65
  - **Caps → `busy`**: more than **32** segments waiting server-wide, or more than **60 s** of one stream's audio queued or decoding. The stream fails, and its own queued segments are dropped.
61
66
  - Results: `lang` `<|zh|>` → `zh` (`zh` / `en` / `yue` / `ja` / `ko`). A segment whose text is empty after trimming is dropped (no frame), but still counts in the segment log.
67
+ - **Speaker diarizer** (`speakers=1` streams only): `OfflineSpeakerDiarization` (pyannote seg int8 + CAM++) and one `SpeakerEmbeddingExtractor` (CAM++), created by the first `load({ speakers: true })`, then resident like the recognizer. A plain stream never loads them. Both constructors are sync (~0.6 s together, once per process). A load failure is `SttError('engine')` (`speaker labelling unavailable: …`) on that stream, retried on the next; the recognizer and plain streams are unaffected. Measured on the dev box: **+86 MB RSS** at load; after a 57 s four-speaker session the process sat at ~610 MB vs ~480 MB without speakers (onnxruntime arenas), growing ~15–20 MB per further session like the plain path does.
68
+ - `sherpa-onnx-node`'s diarization class only has a sync `process()`. The engine calls the addon's `offlineSpeakerDiarizationProcessAsync(handle, samples, progressCb)` instead (exactly 3 args; libuv pool, same segments), imported from `sherpa-onnx-node/addon.js` — the package has no `exports` map, and the ESM `default` doesn't expose the addon. The call is wrapped in a 60 s timeout (see Known limits). An `abort()` during a segment's labelling stops before the next embedding.
69
+
70
+ ### Speaker labels (`speakers=1`)
71
+
72
+ Per VAD segment, inside the same FIFO job as its decode (so a stream's finals stay in order), only when the decoded text is non-empty:
73
+
74
+ 1. **Diarize the segment** (`clustering: { numClusters: -1, threshold: 0.6 }`, `minDurationOn 0.3`, `minDurationOff 0.5`) → local runs with local cluster ids. A segment's VAD cut often holds several people back to back (four.wav's first segment is 0.61–18.03 s, three speakers), which is why a per-segment embedding alone isn't enough (it found 3 of 4 speakers).
75
+ 2. **Local cluster → connection speaker** (`stt/speakers.ts` `createSpeakerTracker`): one CAM++ embedding per cluster, from its runs' audio back to back, capped at **6 s** (`compute` is sync on the event loop: ~9 ms per 0.5 s, ~94 ms per 10 s; the engine yields before each one). Longest cluster first. Cosine against the connection's centroids (running sums of unit embeddings): best **≥ 0.55** → that speaker, centroid updated; else a new speaker. A cluster with **< 1.0 s** of audio never founds a new speaker once one exists: it joins the nearest without moving its centroid. Nothing diarized (pyannote found no speech) → the whole segment is one run with one embedding.
76
+ 3. **Text → runs** (`splitBySpeakerRuns`): SenseVoice's `tokens[]` + `timestamps[]` (seconds into the decoded samples; `tokens.join('') === text`, ITN included, checked on zh / en). Each word token goes to the run containing its timestamp (the later-starting one if runs overlap), else the nearest; punctuation / whitespace tokens stay with the current piece, so a piece never starts with `,`. Consecutive same-speaker tokens form one `final`. A new piece starts at its speaker run's start, clamped between the previous piece's last token and its own first token; the first piece starts at the segment's padded start, the last ends at its padded end, so the finals tile the segment without overlap. A piece that would span zero seconds (overlapping runs clamp its start onto its only token) is merged into its neighbour, and same-speaker neighbours left by that merge are joined. Tokens that don't spell the trimmed text (or no timestamps) → the whole text under the speaker with the most audio in the segment, the same rule as the Transcribe proxy's `splitBySpeaker`.
77
+ 4. **Wire label**: `"0"`, `"1"`… numbered per connection in order of first *sent* appearance.
78
+
79
+ Thresholds: CAM++ cosine on four.wav, pairwise over its nine reference turns (all ≥ 1 s, each embedding capped at 6 s), was 0.75–0.83 for the same speaker (6 pairs) and 0.03–0.52 between different ones (30 pairs); in the engine every match threshold from 0.45 to 0.7 gave the same labels, so 0.55 sits in the gap. **Acceptance** (sherpa's 4-speaker zh test `four.wav`, 56.9 s, 16 kHz mono, converted to s16le and streamed through `createSttEngine()` with the real models in 100 ms frames, `speakers` on): 8 finals labelled `0 1 2 | 0 | 3 | 3 | 2 | 0`, matching the reference (`A B B C | A | D | D | C | A`, sherpa's two S1 runs fall in one piece here) up to renaming, the first segment split at 7.03 s and 13.78 s into its three speakers' sentences. Total 3.4 s for 57 s of audio (1.8 s without speakers); longest event-loop gap 70 ms.
62
80
 
63
81
  ### VAD (one per stream)
64
82
 
@@ -81,12 +99,15 @@ Each stream keeps a 30 s ring of raw samples (it must reach back past a 20 s seg
81
99
 
82
100
  `noServer` ws server behind `ws/upgrade-router.ts`, same structure as the Transcribe proxy (`routes/transcribe-ws.ts`).
83
101
 
102
+ - **Query**: `ext=<id>` (required) and optional `speakers=1`. Only the exact value `1` turns speaker labels on (loading the diarizer if this is the first such stream); absent or any other value = no labels, byte-identical frames to before. voice-input never sends it; htrans ≥ 1.0.10 does when "Identify speakers" is ticked with the on-device engine.
103
+
84
104
  - **Handshake**: login cookie (`isAuthenticated(getTokenFromCookieHeader(cookie))`), else **401**. `ext` must be installed, switched on and declare `stt-local`, else **403**. Frames are capped at 1 MiB (`maxPayload`). Every socket has an `error` listener, so a protocol violation (oversize → 1009, bad UTF-8 → 1007) closes only that socket.
85
105
  - **Server → client** (text JSON):
86
106
  - `{"type":"model","state":"missing"|"downloading"|"ready"|"error","received"?,"total"?,"message"?}`: always the **first** frame. While the model isn't ready, more `model` frames follow (progress ≤ 2/s, plus every state change). `received` / `total` are present only for `downloading`, `message` only for `error`.
87
107
  - `{"type":"ready"}`: model ready **and** engine loaded. Audio is accepted from here on.
88
108
  - `{"type":"final","start":<s>,"end":<s>,"text":"…","lang":"zh"|"en"|"yue"|"ja"|"ko"}`: one per non-empty segment. `start` / `end` are seconds (3 decimals) from this connection's first accepted audio sample.
89
- - `{"type":"error","code":"model"|"engine"|"busy"|"bad-request"|"io","message":"…"}`, then close **1011**. `model` = download failed (the reason is also in the preceding `model` frame's `message`), `engine` = native load / VAD / decode failure, `busy` = queue caps above, `bad-request` = a text frame other than `{"type":"end"}`, `io` = anything else.
109
+ - With `speakers=1`, every `final` also carries `"speaker":"0"|"1"|…`: a **string**, numbered per connection in order of first appearance — the same shape as the Transcribe proxy's (htrans shows `Number(speaker) + 1` as "Speaker N"). A segment several people spoke in arrives as **one `final` per speaker run** (see [Speaker labels](#speaker-labels-speakers1)), in time order, non-overlapping, never zero-length, sharing the segment's `lang`; their `start` / `end` tile the segment's padded bounds.
110
+ - `{"type":"error","code":"model"|"engine"|"busy"|"bad-request"|"io","message":"…"}`, then close **1011**. `model` = download failed (the reason is also in the preceding `model` frame's `message`), `engine` = native load / VAD / decode / diarizer load or run failure, `busy` = queue caps above, `bad-request` = a text frame other than `{"type":"end"}`, `io` = anything else.
90
111
  - **Client → server**:
91
112
  - binary = PCM s16le mono 16 kHz, any frame length (an odd trailing byte carries over). **Binary sent before `ready` is dropped**, because there's no stream yet to feed. A client should start sending only after `ready` (`start` / `end` count from the first audio sent after it).
92
113
  - text `{"type":"end"}`: flush the VAD, wait for every remaining segment of this connection to be decoded and its `final` sent, then close **1000**. Sent before `ready`, it closes 1000 at once (nothing was accepted).
@@ -95,12 +116,15 @@ Each stream keeps a 30 s ring of raw samples (it must reach back past a 20 s seg
95
116
 
96
117
  ## Logs
97
118
 
98
- `[STT] …` only, with counts and durations: `stream open ext=…`, `stream closed after Ns ext=… segments=N audio=Ns`, `stream error ext=… code=…`, `engine loaded in Nms (threads=N)` / `engine load failed: …`, `model download start`, `downloaded <file> from <source> (N MB in Ns)`, `download <file> from <source> failed: <reason>`, `<file>: resuming at N bytes`. **Recognized text and audio are never logged.** The server's default level is `warn`, so these `console.log` lines show up at `general.logging.level: info` (or `HALO_LOG_LEVEL=info`).
119
+ `[STT] …` only, with counts and durations: `stream open ext=…[ speakers=1]`, `stream closed after Ns ext=… segments=N audio=Ns[ speakers=N]`, `stream error ext=… code=…`, `engine loaded in Nms (threads=N)` / `engine load failed: …`, `speaker diarizer loaded in Nms` / `speaker diarizer load failed: …`, `model download start`, `downloaded <file> from <source> (N MB in Ns)`, `download <file> from <source> failed: <reason>`, `<file>: resuming at N bytes`. **Recognized text and audio are never logged.** The server's default level is `warn`, so these `console.log` lines show up at `general.logging.level: info` (or `HALO_LOG_LEVEL=info`).
99
120
 
100
121
  ## Known limits
101
122
 
102
- - **Proxies**: neither `fetch` nor `node:http(s)` reads `HTTPS_PROXY` / `HTTP_PROXY` on Node 22 by default (verified: with an unreachable `HTTPS_PROXY`, both connected directly). `NODE_USE_ENV_PROXY=1` (experimental in Node 22, prints `UNDICI-EHPA` warnings) makes both honour it. A server behind a mandatory egress proxy can't download the models without it. The workaround is to drop the three files into `~/.halo/global/stt-models/` by hand with the exact names and sizes above.
103
- - ~400 MB resident once any stream has loaded the engine (no idle release, see above). The decode burst uses up to 4 cores.
123
+ - **Proxies**: neither `fetch` nor `node:http(s)` reads `HTTPS_PROXY` / `HTTP_PROXY` on Node 22 by default (verified: with an unreachable `HTTPS_PROXY`, both connected directly). `NODE_USE_ENV_PROXY=1` (experimental in Node 22, prints `UNDICI-EHPA` warnings) makes both honour it. A server behind a mandatory egress proxy can't download the models without it. The workaround is to drop the five files into `~/.halo/global/stt-models/` by hand with the exact names and sizes above.
124
+ - ~400 MB resident once any stream has loaded the engine (no idle release, see above), plus ~90–130 MB once a `speakers=1` stream has loaded the diarizer. The decode burst uses up to 4 cores.
125
+ - **Speaker labels are per connection**: a reconnect (htrans's Pause → Resume starts a new run) renumbers from `"0"`, and the same person may get a different number. Labels are never names. Overlapping speech, far-field / room microphones, telephone audio and more than ~4 speakers are **unmeasured** — the only acceptance file is sherpa's clean four-speaker zh test. A speaker whose only turns are under 1 s is folded into the nearest existing one. A speaker change inside a SenseVoice token (rare; tokens are ~0.1–0.3 s) is cut at the token boundary.
126
+ - **The direct-wav acceptance above is the optimistic case.** Through a real browser capture path (htrans browser e2e: Chrome captures the 16 kHz source at 48 kHz, the htrans worklet downsamples back to 16 kHz) one 2.5 s stretch of four.wav (11.2–13.8 s, reference speaker B) was labelled as another speaker in every run, and one run produced a 5th speaker for a misrecognised D sentence. The same 48 → 16 kHz round trip done with ffmpeg reproduces it, and real microphones always go through this path.
127
+ - **Diarization timeout**: one native diarization call is capped at 60 s (`DIAR_TIMEOUT_MS`), then the stream fails `engine` (`speaker diarization failed: timed out`). sherpa-onnx-node 1.13.8's async diarization worker has no `OnError`, so a native exception would otherwise leave its promise pending and stall the server-wide decode queue until restart. The native call itself can't be cancelled: if it is really still running, it keeps one libuv thread busy until it returns.
104
128
  - One decode at a time: with many concurrent speakers, latency grows linearly until `busy`.
105
129
  - No partial results (SenseVoice is non-streaming): a final arrives ~0.5 s after the speaker pauses, at most 20 s after a segment starts.
106
130
  - Desktop (win-x64 / darwin-arm64): `stage-runtime.mjs` `installSherpaBinary()` ships the target's `sherpa-onnx-<platform>-<arch>` package (see [dev/desktop-packaging.md](../dev/desktop-packaging.md)). A build without it fails streams with `engine`, never server boot. The macOS addon declares minimum 14.0; older macOS is unverified.
@@ -98,7 +98,7 @@ Installed extensions are directories under `~/.halo/global/extensions/<id>/` wit
98
98
  | DELETE | `/api/extensions/:id` | cookie | Remove the directory and drop its on / off entry. 400 for an invalid id shape, 404 if not installed. Returns `{ok, id}` |
99
99
  | GET | `/api/extensions/:id/:version/:token/<asset>` | **path token, no cookie** | Static file from the extension directory. 401 unless `verifyScopedToken(token,'ext')`; 404 if `version` ≠ installed version or the asset escapes the directory. Response headers: `Access-Control-Allow-Origin: *`, `Referrer-Policy: no-referrer`, `Cache-Control: public, max-age=31536000, immutable` (the version segment is the cache key) |
100
100
  | WS | `/api/transcribe/stream?ext=<id>&lang=<auto\|xx-XX\|xx-XX,yy-YY,…>[&speakers=1]` | cookie | Streaming transcription proxy to Amazon Transcribe (`routes/transcribe-ws.ts`; not mounted in AgentCore mode). Handshake 401 without the login cookie, 403 unless `ext` is installed, switched on and declares capability `transcribe`. Client → binary PCM s16le mono 16 kHz, text `{"type":"end"}` (flush, server closes 1000). Server → `{"type":"ready"}` (on connect; stream audio right after it), `{"type":"partial"\|"final",start,end,text,lang}` (seconds from the stream's first audio byte); with `speakers=1` (speaker labels, off by default) a `final` also carries `speaker` (`"0"`, `"1"`… — Transcribe's per-stream label, not a name; absent when Transcribe labelled nothing) and a result spoken by several people arrives as one `final` per speaker, `partial` never has it — without `speakers=1` each result is one `final` without `speaker`; `{"type":"error",code,message}` then close; `code` ∈ `credentials` \| `denied` \| `limit` \| `bad-request` \| `io`. Region / languages / credentials from the extension's `ext-<id>` settings (default chain when no keys) — see [design](../design/canvas-extensions.md#streaming-transcription-proxy) |
101
- | WS | `/api/stt/stream?ext=<id>` | cookie | On-device speech-to-text — SenseVoice + Silero VAD via sherpa-onnx, no cloud (`routes/stt-ws.ts`; not mounted in AgentCore mode). Handshake 401 without the login cookie, 403 unless `ext` is installed, switched on and declares capability `stt-local`. Server → `{"type":"model",state,received?,total?,message?}` first (`state` ∈ `missing` \| `downloading` \| `ready` \| `error`; a missing model is downloaded from here, progress pushed ≤ 2/s), `{"type":"ready"}` once model + engine are loaded (binary sent before it is dropped), `{"type":"final",start,end,text,lang}` per VAD segment (seconds from the connection's first accepted sample; `lang` ∈ `zh` \| `en` \| `yue` \| `ja` \| `ko`), `{"type":"error",code,message}` then close 1011; `code` ∈ `model` \| `engine` \| `busy` \| `bad-request` \| `io`. Client → binary PCM s16le mono 16 kHz, text `{"type":"end"}` (remaining finals, then close 1000). No `lang` parameter — see [design/stt-local.md](../design/stt-local.md#ws-apisttstreamextid) |
101
+ | WS | `/api/stt/stream?ext=<id>[&speakers=1]` | cookie | On-device speech-to-text — SenseVoice + Silero VAD via sherpa-onnx, no cloud (`routes/stt-ws.ts`; not mounted in AgentCore mode). Handshake 401 without the login cookie, 403 unless `ext` is installed, switched on and declares capability `stt-local`. Server → `{"type":"model",state,received?,total?,message?}` first (`state` ∈ `missing` \| `downloading` \| `ready` \| `error`; a missing model is downloaded from here, progress pushed ≤ 2/s), `{"type":"ready"}` once model + engine are loaded (binary sent before it is dropped), `{"type":"final",start,end,text,lang}` per VAD segment (seconds from the connection's first accepted sample; `lang` ∈ `zh` \| `en` \| `yue` \| `ja` \| `ko`); with `speakers=1` (on-device speaker labels — only that value; pyannote + CAM++, loaded on the first such stream) every `final` also carries `speaker` (`"0"`, `"1"`… numbered per connection in order of appearance, not a name — same shape as the Transcribe proxy's) and a segment spoken by several people arrives as one `final` per speaker run, in time order, non-overlapping — without it frames are unchanged; `{"type":"error",code,message}` then close 1011; `code` ∈ `model` \| `engine` \| `busy` \| `bad-request` \| `io`. Client → binary PCM s16le mono 16 kHz, text `{"type":"end"}` (remaining finals, then close 1000). No `lang` parameter — see [design/stt-local.md](../design/stt-local.md#ws-apisttstreamextid) |
102
102
  | GET | `/api/jupyter/status` | cookie | Notebook kernels (`routes/jupyter.ts`; not mounted in AgentCore mode). `{ enabled, available, version?, error?, hint? }` — never starts the server; disabled → `enabled:false, available:false`; Jupyter missing → `available:false` + `error` + `hint: 'pip install jupyter_server ipykernel'` |
103
103
  | GET | `/api/jupyter/kernelspecs` | cookie | `{ default, specs: [{ name, displayName, language }] }`. Starts the local jupyter server on demand; 503 `{error, hint?}` when disabled / unavailable / it fails to start |
104
104
  | POST | `/api/jupyter/kernels` | cookie | Body `{ projectId, path, name }` (`path` = workspace-relative notebook path, as in `init.file.path`). 400 for a missing field, unknown project, path outside the workspace or a missing directory. Starts kernel `name` with cwd = the notebook's directory → `{ id, name }` |
@@ -323,7 +323,7 @@ Use only for a new public release; the retained details here are not extra prepa
323
323
  5. **npm**: publish from `packages/cli/dist-pub/` **once**, using the intended dist-tag (`--tag alpha` for an alpha, not `latest`). Confirm `npm view @turmind/halo@x.y.z version` and the intended tag via `npm view @turmind/halo dist-tags.alpha` (or `dist-tags.latest` for stable); both must equal the requested version. Registry replication can lag ~2–3 minutes: wait/recheck, never republish. `dist-pub/` is release-only; desktop staging uses `dist-dev/`. Historical reason: a second v1.3.4 publish put `1.3.4-<sha>` on `latest` for ~90s.
324
324
  6. **GitHub**: `gh release create vx.y.z --notes-from-tag` (or the short CHANGELOG summary); use `--prerelease` for an alpha.
325
325
  7. **Windows exe**: `(cd packages/desktop && HALO_STAGE_FULL=1 CI=true pnpm dist:win)`, then `gh release upload vx.y.z "packages/desktop/dist/Halo Setup x.y.z.exe"`. Verify it appears in `gh release view vx.y.z --json assets`; v1.3.0 and v1.3.1 missed this asset. macOS dmg is the user's step unless requested.
326
- 8. **halo-node binaries**: nothing to build by hand. Publishing the release triggers `.github/workflows/node-go.yml`'s release job (`go vet` + `go test`, then `build.sh`), which uploads `halo-node-linux-{amd64,arm64,armv7}`, `SHA256SUMS` and `install.sh` (with the version stamped in; it isn't listed in `SHA256SUMS`). Wait for it to go green, then check that all five appear in `gh release view vx.y.z --json assets` — the admin's Linux connect command runs this tag's `install.sh`. If the job failed, rerun it with `gh workflow run node-go.yml -f tag=vx.y.z` (`workflow_dispatch`; `--clobber` makes reruns safe). The trigger needs the release created by a person (`gh release create` as yourself), because a release created with `GITHUB_TOKEN` doesn't start other workflows.
326
+ 8. **halo-node binaries**: nothing to build by hand. Publishing the release triggers `.github/workflows/node-go.yml`'s release job (`go vet` + `go test`, then `build.sh`), which uploads `halo-node-linux-{amd64,arm64,armv7}`, `halo-node-windows-amd64.exe`, `SHA256SUMS` and `install.sh` (with the version stamped in; it isn't listed in `SHA256SUMS`). Wait for it to go green, then check that all six appear in `gh release view vx.y.z --json assets` — the admin's Linux connect command runs this tag's `install.sh`. If the job failed, rerun it with `gh workflow run node-go.yml -f tag=vx.y.z` (`workflow_dispatch`; `--clobber` makes reruns safe). The trigger needs the release created by a person (`gh release create` as yourself), because a release created with `GITHUB_TOKEN` doesn't start other workflows.
327
327
 
328
328
  **npm token gotcha**: `npm publish` on this package needs a granular access token created with **"Bypass 2FA"** checked — scope / permission alone yields `403 Two-factor authentication or granular access token with bypass 2fa enabled is required`. `npm whoami` and `npm token list` succeed with a non-bypass token, so neither is a valid pre-flight; check `GET https://registry.npmjs.org/-/npm/v1/tokens` (with the token as bearer) and look for `"bypass_2fa": true` on the token in use before starting a release.
329
329
 
@@ -10,16 +10,16 @@ halo-node --root ~/proj ──WebSocket──────▶ halo server ──
10
10
 
11
11
  Two clients do the same job, with the same flags and the same behaviour:
12
12
 
13
- - **Linux (x86_64 / arm64 / armv7)**: `halo-node`, a single ~7 MB binary attached to every GitHub release. It needs no Node.js and idles at about 9 MB of memory.
14
- - **macOS / Windows** (or a Linux box that already has the npm package): `halo node` from `npm install -g @turmind/halo`.
13
+ - **Linux (x86_64 / arm64 / armv7) and Windows (amd64)**: `halo-node`, a single binary attached to every GitHub release. It needs no Node.js. The Linux build is ~7 MB and idles at about 9 MB of memory.
14
+ - **macOS** (or a Linux / Windows machine that already has the npm package): `halo node` from `npm install -g @turmind/halo`.
15
15
 
16
16
  Neither needs `halo setup` or model keys, and neither writes anything under `~/.halo/`.
17
17
 
18
- Supported: **Linux and macOS**. Windows works on a best-effort basis (not regularly tested; `node_run` commands go to `cmd.exe`).
18
+ Supported: **Linux, macOS and Windows amd64**. The Go client is tested natively on Linux and Windows CI; macOS uses npm. Windows `node_run` commands go to `cmd.exe`, with UTF-8 output and a GBK fallback.
19
19
 
20
20
  ## 1. Create a token
21
21
 
22
- Admin → **Channels → Linked machines** → **Add machine**. Pick the workspace and a machine name (`a-z 0-9 -`, e.g. `mbp`; unique within the workspace — the agent uses it to address the machine). The dialog shows two commands to copy: one installs `halo-node` on Linux and runs it with `~/.local/bin` prepended to `PATH` (where a non-root install lands), the other runs `halo node` on macOS / Windows. The install link points at the server's own release, so the machine and the server come from the same version.
22
+ Admin → **Channels → Linked machines** → **Add machine**. Pick the workspace and a machine name (`a-z 0-9 -`, e.g. `mbp`; unique within the workspace — the agent uses it to address the machine). The dialog shows two commands to copy: one installs `halo-node` on Linux and runs it with `~/.local/bin` prepended to `PATH` (where a non-root install lands), the other runs the npm `halo node` client. On Windows, use the standalone exe below instead. The install link points at the server's own release, so the machine and the server come from the same version.
23
23
 
24
24
  The token stays on the list row (copy button). Deleting the row revokes it at once and disconnects the machine.
25
25
 
@@ -55,7 +55,18 @@ curl -fsL https://github.com/turmind/halo-agent/releases/latest/download/SHA256S
55
55
  ./halo-node --server https://halo.example.com --root ~/proj
56
56
  ```
57
57
 
58
- **macOS / Windows**:
58
+ ### Windows (amd64)
59
+
60
+ Download `halo-node-windows-amd64.exe` from the [GitHub release](https://github.com/turmind/halo-agent/releases/latest) matching your server (no npm or Node.js needed). Put it in e.g. `C:\halo-node`. In PowerShell:
61
+
62
+ ```powershell
63
+ $env:HALO_NODE_TOKEN = '<token>'
64
+ & 'C:\halo-node\halo-node-windows-amd64.exe' --server https://halo.example.com --root 'C:\Users\alice\proj'
65
+ ```
66
+
67
+ The release's `SHA256SUMS` includes the exe; compare its entry with `Get-FileHash C:\halo-node\halo-node-windows-amd64.exe -Algorithm SHA256`. Use either `/` or `\` in paths; `~/` expands from `USERPROFILE`. Folder mode resolves symlinks and junctions before checking the root. Add `--allow-shell` only if you want commands as this Windows user. Stop the process before replacing the exe during an upgrade.
68
+
69
+ **macOS (npm)**:
59
70
 
60
71
  ```bash
61
72
  npm install -g @turmind/halo # once
@@ -97,7 +108,7 @@ Neither client follows `HTTP(S)_PROXY`. To reach the server through a forward pr
97
108
 
98
109
  ### TLS certificates (`https://` servers)
99
110
 
100
- `halo-node` trusts the system's CA store (`/etc/ssl/certs/…`; `SSL_CERT_FILE` / `SSL_CERT_DIR` point it elsewhere). It falls back to a built-in Mozilla root set only on a system with no CA bundle at all. For a private or corporate CA, install it into the system store, or set `SSL_CERT_FILE`. `halo node` (Node.js) does the opposite: it uses Node's built-in roots plus `NODE_EXTRA_CA_CERTS`, and ignores the system store.
111
+ `halo-node` trusts the system's CA store (the Windows certificate store on Windows; `/etc/ssl/certs/…` on Linux, where `SSL_CERT_FILE` / `SSL_CERT_DIR` can point it elsewhere). It falls back to a built-in Mozilla root set only on a system with no CA bundle at all. For a private or corporate CA, install it into the system store, or on Linux set `SSL_CERT_FILE`. `halo node` (Node.js) does the opposite: it uses Node's built-in roots plus `NODE_EXTRA_CA_CERTS`, and ignores the system store.
101
112
 
102
113
  ### Run it as a service (systemd)
103
114
 
@@ -129,6 +140,34 @@ WantedBy=multi-user.target
129
140
 
130
141
  Network drops, server restarts, a busy token (409) and a lockout (429) are handled inside the process, which keeps running, so `on-failure` is enough. Don't use `Restart=always`: it keeps restarting after a revoked token, and each restart is a failed login. After 5 failures in 15 minutes the server locks out that IP address, which blocks every machine connecting from it. ExecStart takes the path the install script printed. For the npm client, set `ExecStart=/usr/bin/halo node …`. After upgrading with the install script, `systemctl restart halo-node` to run the new binary.
131
142
 
143
+ ### Start at boot on Windows (Task Scheduler)
144
+
145
+ Create `C:\halo-node\start.cmd` (replace the token, server and shared folder). Restrict this directory and wrapper to your account, Administrators and SYSTEM: it contains a plaintext token, and the log can contain file paths. The wrapper keeps the token out of the task's command line and appends logs:
146
+
147
+ ```bat
148
+ @echo off
149
+ setlocal
150
+ set "HALO_NODE_TOKEN=<token>"
151
+ "C:\halo-node\halo-node-windows-amd64.exe" --server https://halo.example.com --root "C:\Users\alice\proj" >>"C:\halo-node\halo-node.log" 2>&1
152
+ exit /b %errorlevel%
153
+ ```
154
+
155
+ Run PowerShell **as administrator** once to register a startup task. Set `$user` to the account that owns the shared files (not necessarily the administrator registering the task):
156
+
157
+ ```powershell
158
+ $user = "$env:COMPUTERNAME\alice"
159
+ $action = New-ScheduledTaskAction -Execute "$env:SystemRoot\System32\cmd.exe" -Argument '/d /c "C:\halo-node\start.cmd"' -WorkingDirectory 'C:\halo-node'
160
+ $trigger = New-ScheduledTaskTrigger -AtStartup
161
+ $principal = New-ScheduledTaskPrincipal -UserId $user -LogonType S4U -RunLevel Limited
162
+ $settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit ([TimeSpan]::Zero) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
163
+ Register-ScheduledTask -TaskName 'halo-node' -Action $action -Trigger $trigger -Principal $principal -Settings $settings
164
+ Start-ScheduledTask -TaskName 'halo-node'
165
+ ```
166
+
167
+ S4U runs hidden without a logged-in desktop or a stored password. It cannot use that user's network credentials (mapped drives / authenticated SMB) or EFS-encrypted files; use a local shared folder. Outbound HTTPS with the Halo token still works. `ExecutionTimeLimit` **must be zero**: Task Scheduler's default stops a task after 72 hours. Do not enable restart-on-failure: exit 1 means a rejected token / other permanent refusal, and restarting repeats failed logins. Network failures already retry inside halo-node.
168
+
169
+ If your server is reached through **Tailscale**, enable Tailscale's **Run unattended** on Windows so it connects before login. halo-node retries until the network is ready. Inspect `C:\halo-node\halo-node.log` and `Get-ScheduledTaskInfo halo-node`; after replacing the token or upgrading the exe, stop and start the task. The append-only log is yours to rotate.
170
+
132
171
  ### Settings on the machine
133
172
 
134
173
  `halo-node` takes its shell timeout only from `HALO_SHELL_TIMEOUT` (ms, default 120 s); grep's default of 50 matches and the 5 MiB command-output cap are fixed. `halo node` (npm) also reads them from the machine's own `~/.halo/secrets/` (`timeout.shell_exec` in `config.yaml`, the `general.limits.*` keys in `settings.yaml`). This only matters on a machine that also runs a halo server with those settings changed.
package/dist/index.js CHANGED
@@ -16976,7 +16976,7 @@ async function initObservability(haloProcess = "server") {
16976
16976
  return true;
16977
16977
  }
16978
16978
  const { registerSdk: registerSdk2 } = await Promise.resolve().then(() => (init_otel_sdk(), otel_sdk_exports));
16979
- providers = await registerSdk2(process.env.OTEL_SERVICE_NAME, "2.2.1", haloProcess);
16979
+ providers = await registerSdk2(process.env.OTEL_SERVICE_NAME, "2.2.2", haloProcess);
16980
16980
  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})`);
16981
16981
  return true;
16982
16982
  }
@@ -30007,7 +30007,7 @@ function createSttModelManager(opts = {}) {
30007
30007
  }
30008
30008
  };
30009
30009
  }
30010
- var SENSE_VOICE_MODEL, SENSE_VOICE_TOKENS, SILERO_VAD_MODEL, HF_SENSE_VOICE, MS_SENSE_VOICE, STT_MODEL_FILES, sttModels;
30010
+ var SENSE_VOICE_MODEL, SENSE_VOICE_TOKENS, SILERO_VAD_MODEL, PYANNOTE_SEG_MODEL, CAMPLUS_MODEL, HF_SENSE_VOICE, MS_SENSE_VOICE, STT_MODEL_FILES, sttModels;
30011
30011
  var init_models = __esm({
30012
30012
  "../server/dist/stt/models.js"() {
30013
30013
  "use strict";
@@ -30015,6 +30015,8 @@ var init_models = __esm({
30015
30015
  SENSE_VOICE_MODEL = "sense-voice-zh-en-ja-ko-yue-2024-07-17/model.int8.onnx";
30016
30016
  SENSE_VOICE_TOKENS = "sense-voice-zh-en-ja-ko-yue-2024-07-17/tokens.txt";
30017
30017
  SILERO_VAD_MODEL = "silero-vad-v4/silero_vad.onnx";
30018
+ PYANNOTE_SEG_MODEL = "pyannote-segmentation-3-0/model.int8.onnx";
30019
+ CAMPLUS_MODEL = "campplus-zh-en-advanced/3dspeaker_speech_campplus_sv_zh_en_16k-common_advanced.onnx";
30018
30020
  HF_SENSE_VOICE = "https://huggingface.co/csukuangfj/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17/resolve/2365baeacb507f821a0c8120fcee3d484dba7a07";
30019
30021
  MS_SENSE_VOICE = "https://www.modelscope.cn/api/v1/models/pengzhendong/sherpa-onnx-sense-voice-zh-en-ja-ko-yue/repo?Revision=master&FilePath=";
30020
30022
  STT_MODEL_FILES = [
@@ -30044,29 +30046,205 @@ var init_models = __esm({
30044
30046
  { name: "huggingface", url: "https://huggingface.co/csukuangfj/vad/resolve/fba88cd2e921609e7675c3aaf51e0b9b295da4bc/silero_vad.onnx" },
30045
30047
  { name: "modelscope", url: "https://www.modelscope.cn/api/v1/models/pengzhendong/silero-vad/repo?Revision=master&FilePath=v4/silero_vad.onnx" }
30046
30048
  ]
30049
+ },
30050
+ {
30051
+ path: PYANNOTE_SEG_MODEL,
30052
+ size: 1540506,
30053
+ sha256: "d582f4b4c6b48205de7e0643c57df0df5615a3c176189be3fc461e9d18827b5d",
30054
+ sources: [
30055
+ { name: "huggingface", url: "https://huggingface.co/csukuangfj/sherpa-onnx-pyannote-segmentation-3-0/resolve/9403a6902bb58e3d5ae8c7e77c3422de279db2e0/model.int8.onnx" },
30056
+ { name: "modelscope", url: "https://www.modelscope.cn/api/v1/models/pengzhendong/sherpa-onnx-pyannote-segmentation-3-0/repo?Revision=master&FilePath=model.int8.onnx" }
30057
+ ]
30058
+ },
30059
+ {
30060
+ path: CAMPLUS_MODEL,
30061
+ size: 28281164,
30062
+ sha256: "aa3cfc16963a10586a9393f5035d6d6b57e98d358b347f80c2a30bf4f00ceba2",
30063
+ sources: [
30064
+ { name: "huggingface", url: "https://huggingface.co/csukuangfj/speaker-embedding-models/resolve/0743f301363dec56491a490f6d6cbc9d67f9a3bf/3dspeaker_speech_campplus_sv_zh_en_16k-common_advanced.onnx" },
30065
+ { name: "modelscope", url: "https://www.modelscope.cn/api/v1/models/lijiu09/speaker-embedding-models/repo?Revision=master&FilePath=3dspeaker_speech_campplus_sv_zh_en_16k-common_advanced.onnx" },
30066
+ { name: "github", url: "https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-recongition-models/3dspeaker_speech_campplus_sv_zh_en_16k-common_advanced.onnx" }
30067
+ ]
30047
30068
  }
30048
30069
  ];
30049
30070
  sttModels = createSttModelManager();
30050
30071
  }
30051
30072
  });
30052
30073
 
30074
+ // ../server/dist/stt/speakers.js
30075
+ function normalize(v) {
30076
+ let n = 0;
30077
+ for (const x of v)
30078
+ n += x * x;
30079
+ n = Math.sqrt(n) || 1;
30080
+ return v.map((x) => x / n);
30081
+ }
30082
+ function cosine(a, b) {
30083
+ let d = 0, x = 0, y = 0;
30084
+ for (let i = 0; i < a.length; i++) {
30085
+ d += a[i] * b[i];
30086
+ x += a[i] * a[i];
30087
+ y += b[i] * b[i];
30088
+ }
30089
+ return x && y ? d / Math.sqrt(x * y) : 0;
30090
+ }
30091
+ function createSpeakerTracker(threshold = SPEAKER_MATCH_THRESHOLD) {
30092
+ const centroids = [];
30093
+ return {
30094
+ /** One local cluster (its embedding + seconds of audio) → speaker index. */
30095
+ assign(embedding, seconds) {
30096
+ const e = normalize(embedding);
30097
+ let best = -1;
30098
+ let bestSim = -2;
30099
+ centroids.forEach((c, k) => {
30100
+ const s = cosine(c, e);
30101
+ if (s > bestSim) {
30102
+ bestSim = s;
30103
+ best = k;
30104
+ }
30105
+ });
30106
+ if (best < 0 || bestSim < threshold && seconds >= MIN_NEW_SPEAKER_S) {
30107
+ centroids.push(e);
30108
+ return centroids.length - 1;
30109
+ }
30110
+ if (bestSim >= threshold) {
30111
+ const c = centroids[best];
30112
+ for (let i = 0; i < c.length; i++)
30113
+ c[i] += e[i];
30114
+ }
30115
+ return best;
30116
+ }
30117
+ };
30118
+ }
30119
+ function runAt(runs, t2) {
30120
+ let hit = null;
30121
+ for (const r of runs)
30122
+ if (r.start <= t2 && t2 <= r.end && (!hit || r.start > hit.start))
30123
+ hit = r;
30124
+ if (hit)
30125
+ return hit;
30126
+ let best = runs[0];
30127
+ let dist = Infinity;
30128
+ for (const r of runs) {
30129
+ const d = Math.min(Math.abs(t2 - r.start), Math.abs(t2 - r.end));
30130
+ if (d < dist) {
30131
+ dist = d;
30132
+ best = r;
30133
+ }
30134
+ }
30135
+ return best;
30136
+ }
30137
+ function splitBySpeakerRuns(text6, tokens, timestamps, runs, duration) {
30138
+ const audio = /* @__PURE__ */ new Map();
30139
+ for (const r of runs)
30140
+ audio.set(r.speaker, (audio.get(r.speaker) ?? 0) + r.end - r.start);
30141
+ const top = [...audio.entries()].reduce((a, b) => b[1] > a[1] ? b : a)[0];
30142
+ const whole = [{ start: 0, end: duration, text: text6, speaker: top }];
30143
+ if (audio.size === 1 || !tokens || !timestamps || tokens.length !== timestamps.length || tokens.join("").trim() !== text6)
30144
+ return whole;
30145
+ const pieces = [];
30146
+ for (let i = 0; i < tokens.length; i++) {
30147
+ const cur = pieces.at(-1);
30148
+ if (!WORD.test(tokens[i])) {
30149
+ if (cur)
30150
+ cur.last = i;
30151
+ continue;
30152
+ }
30153
+ const run = runAt(runs, timestamps[i]);
30154
+ if (cur && cur.speaker === run.speaker) {
30155
+ cur.last = i;
30156
+ continue;
30157
+ }
30158
+ pieces.push(cur ? { speaker: run.speaker, first: i, last: i, at: Math.min(Math.max(run.start, timestamps[cur.last]), timestamps[i]) } : { speaker: run.speaker, first: 0, last: i, at: 0 });
30159
+ }
30160
+ for (let k = pieces.length - 1; k >= 0 && pieces.length > 1; k--) {
30161
+ if ((pieces[k + 1]?.at ?? duration) > pieces[k].at)
30162
+ continue;
30163
+ if (k > 0)
30164
+ pieces[k - 1].last = pieces[k].last;
30165
+ else
30166
+ Object.assign(pieces[1], { first: pieces[0].first, at: 0 });
30167
+ pieces.splice(k, 1);
30168
+ }
30169
+ for (let k = pieces.length - 1; k > 0; k--) {
30170
+ if (pieces[k].speaker !== pieces[k - 1].speaker)
30171
+ continue;
30172
+ pieces[k - 1].last = pieces[k].last;
30173
+ pieces.splice(k, 1);
30174
+ }
30175
+ if (pieces.length === 0)
30176
+ return whole;
30177
+ return pieces.map((p, k) => ({
30178
+ start: p.at,
30179
+ end: pieces[k + 1]?.at ?? duration,
30180
+ text: tokens.slice(p.first, p.last + 1).join("").trim(),
30181
+ speaker: p.speaker
30182
+ }));
30183
+ }
30184
+ var SPEAKER_MATCH_THRESHOLD, MIN_NEW_SPEAKER_S, WORD;
30185
+ var init_speakers = __esm({
30186
+ "../server/dist/stt/speakers.js"() {
30187
+ "use strict";
30188
+ SPEAKER_MATCH_THRESHOLD = 0.55;
30189
+ MIN_NEW_SPEAKER_S = 1;
30190
+ WORD = /[\p{L}\p{N}]/u;
30191
+ }
30192
+ });
30193
+
30053
30194
  // ../server/dist/stt/engine.js
30054
30195
  import os8 from "node:os";
30196
+ function withTimeout(p, ms) {
30197
+ let timer2;
30198
+ const timeout = new Promise((_, reject) => {
30199
+ timer2 = setTimeout(() => reject(new Error("timed out")), ms);
30200
+ });
30201
+ return Promise.race([p, timeout]).finally(() => clearTimeout(timer2));
30202
+ }
30055
30203
  function createSttEngine(deps = {}) {
30056
30204
  const loadSherpa = deps.loadSherpa ?? defaultLoadSherpa;
30057
30205
  const models = deps.models ?? sttModels;
30058
30206
  let sherpa = null;
30059
30207
  let recognizer = null;
30060
30208
  let loading = null;
30209
+ let diarizer = null;
30210
+ let extractor = null;
30211
+ let diarLoading = null;
30061
30212
  const queue = [];
30062
30213
  let decoding = false;
30063
- function load2() {
30064
- if (recognizer)
30065
- return Promise.resolve();
30066
- loading ??= doLoad().finally(() => {
30067
- loading = null;
30068
- });
30069
- return loading;
30214
+ async function load2(opts = {}) {
30215
+ if (!recognizer)
30216
+ await (loading ??= doLoad().finally(() => {
30217
+ loading = null;
30218
+ }));
30219
+ if (opts.speakers && !diarizer)
30220
+ await (diarLoading ??= doLoadDiarizer().finally(() => {
30221
+ diarLoading = null;
30222
+ }));
30223
+ }
30224
+ async function doLoadDiarizer() {
30225
+ const t0 = Date.now();
30226
+ try {
30227
+ const embedding = { model: models.filePath(CAMPLUS_MODEL), numThreads: NUM_THREADS, provider: "cpu", debug: 0 };
30228
+ const d = new sherpa.OfflineSpeakerDiarization({
30229
+ segmentation: { pyannote: { model: models.filePath(PYANNOTE_SEG_MODEL) }, numThreads: NUM_THREADS, provider: "cpu", debug: 0 },
30230
+ embedding,
30231
+ clustering: { numClusters: -1, threshold: DIAR_CLUSTER_THRESHOLD },
30232
+ minDurationOn: DIAR_MIN_ON_S,
30233
+ minDurationOff: DIAR_MIN_OFF_S
30234
+ });
30235
+ extractor = new sherpa.SpeakerEmbeddingExtractor(embedding);
30236
+ diarizer = d;
30237
+ console.log(`[STT] speaker diarizer loaded in ${Date.now() - t0}ms`);
30238
+ } catch (err) {
30239
+ console.log(`[STT] speaker diarizer load failed: ${errMsg2(err)}`);
30240
+ throw new SttError("engine", `speaker labelling unavailable: ${errMsg2(err)}`);
30241
+ }
30242
+ }
30243
+ function embed(samples) {
30244
+ const s = extractor.createStream();
30245
+ s.acceptWaveform({ samples: samples.subarray(0, EMBED_MAX_S * SAMPLE_RATE), sampleRate: SAMPLE_RATE });
30246
+ s.inputFinished();
30247
+ return extractor.compute(s, false);
30070
30248
  }
30071
30249
  async function doLoad() {
30072
30250
  const t0 = Date.now();
@@ -30096,20 +30274,32 @@ function createSttEngine(deps = {}) {
30096
30274
  decoding = true;
30097
30275
  while (queue.length > 0) {
30098
30276
  const job = queue.shift();
30277
+ let phase = "decode";
30099
30278
  try {
30100
30279
  const stream = recognizer.createStream();
30101
30280
  stream.acceptWaveform({ samples: job.samples, sampleRate: SAMPLE_RATE });
30102
30281
  await recognizer.decodeAsync(stream);
30103
- job.owner.done(job, recognizer.getResult(stream));
30282
+ const result = recognizer.getResult(stream);
30283
+ let diar = null;
30284
+ if (job.owner.wantsSpeakers() && (result.text ?? "").trim()) {
30285
+ phase = "speaker diarization";
30286
+ diar = await withTimeout(sherpa.offlineSpeakerDiarizationProcessAsync(diarizer.handle, job.samples, () => {
30287
+ }), DIAR_TIMEOUT_MS);
30288
+ }
30289
+ await job.owner.done(job, result, diar);
30104
30290
  } catch (err) {
30105
- job.owner.fail(new SttError("engine", `decode failed: ${errMsg2(err)}`));
30291
+ job.owner.fail(new SttError("engine", `${phase} failed: ${errMsg2(err)}`));
30106
30292
  }
30107
30293
  }
30108
30294
  decoding = false;
30109
30295
  }
30110
- function open(handlers) {
30296
+ function open(handlers, opts = {}) {
30111
30297
  if (!sherpa)
30112
30298
  throw new SttError("engine", "speech engine not loaded");
30299
+ if (opts.speakers && !diarizer)
30300
+ throw new SttError("engine", "speaker labelling not loaded");
30301
+ const speakers = opts.speakers ? createSpeakerTracker() : null;
30302
+ const labels = /* @__PURE__ */ new Map();
30113
30303
  let vad;
30114
30304
  try {
30115
30305
  vad = new sherpa.Vad({
@@ -30165,16 +30355,67 @@ function createSttEngine(deps = {}) {
30165
30355
  handlers.onError(err);
30166
30356
  checkDrain();
30167
30357
  }
30358
+ async function speakerRuns(samples, diar) {
30359
+ const duration = samples.length / SAMPLE_RATE;
30360
+ const tick = async () => {
30361
+ await new Promise((r) => setImmediate(r));
30362
+ return !stopped;
30363
+ };
30364
+ if (diar.length === 0)
30365
+ return await tick() ? [{ start: 0, end: duration, speaker: speakers.assign(embed(samples), duration) }] : null;
30366
+ const clusters = /* @__PURE__ */ new Map();
30367
+ for (const d of [...diar].sort((a, b) => a.start - b.start))
30368
+ clusters.set(d.speaker, [...clusters.get(d.speaker) ?? [], d]);
30369
+ const seconds = (segs) => segs.reduce((n, d) => n + d.end - d.start, 0);
30370
+ const runs = [];
30371
+ for (const segs of [...clusters.values()].sort((a, b) => seconds(b) - seconds(a))) {
30372
+ const parts = segs.map((d) => samples.subarray(Math.round(d.start * SAMPLE_RATE), Math.round(d.end * SAMPLE_RATE)));
30373
+ const audio = new Float32Array(Math.min(parts.reduce((n, p) => n + p.length, 0), EMBED_MAX_S * SAMPLE_RATE));
30374
+ let at = 0;
30375
+ for (const p of parts) {
30376
+ if (at >= audio.length)
30377
+ break;
30378
+ audio.set(p.subarray(0, audio.length - at), at);
30379
+ at += p.length;
30380
+ }
30381
+ if (!await tick())
30382
+ return null;
30383
+ const speaker = speakers.assign(embed(audio), seconds(segs));
30384
+ for (const d of segs)
30385
+ runs.push({ start: d.start, end: d.end, speaker });
30386
+ }
30387
+ return runs;
30388
+ }
30168
30389
  const owner = {
30169
- done(job, result) {
30390
+ wantsSpeakers: () => speakers !== null && !stopped,
30391
+ async done(job, result, diar) {
30392
+ const text6 = (result.text ?? "").trim();
30393
+ let pieces = null;
30394
+ if (text6 && diar && !stopped) {
30395
+ try {
30396
+ const runs = await speakerRuns(job.samples, diar);
30397
+ if (runs)
30398
+ pieces = splitBySpeakerRuns(text6, result.tokens, result.timestamps, runs, job.samples.length / SAMPLE_RATE);
30399
+ } catch (err) {
30400
+ failStream(new SttError("engine", `speaker labelling failed: ${errMsg2(err)}`));
30401
+ }
30402
+ }
30170
30403
  pendingSamples -= job.samples.length;
30171
30404
  inFlight2--;
30172
30405
  if (!stopped) {
30173
30406
  segments++;
30174
30407
  decodedSamples += job.samples.length;
30175
- const text6 = (result.text ?? "").trim();
30176
- if (text6)
30177
- handlers.onFinal({ start: round3(job.start / SAMPLE_RATE), end: round3(job.end / SAMPLE_RATE), text: text6, lang: (result.lang ?? "").replace(/<\|([^|]*)\|>/g, "$1") });
30408
+ const lang = (result.lang ?? "").replace(/<\|([^|]*)\|>/g, "$1");
30409
+ const start3 = job.start / SAMPLE_RATE;
30410
+ const end = job.end / SAMPLE_RATE;
30411
+ if (pieces) {
30412
+ for (const p of pieces) {
30413
+ if (!labels.has(p.speaker))
30414
+ labels.set(p.speaker, String(labels.size));
30415
+ handlers.onFinal({ start: round3(start3 + p.start), end: round3(Math.min(start3 + p.end, end)), text: p.text, lang, speaker: labels.get(p.speaker) });
30416
+ }
30417
+ } else if (text6)
30418
+ handlers.onFinal({ start: round3(start3), end: round3(end), text: text6, lang });
30178
30419
  }
30179
30420
  checkDrain();
30180
30421
  },
@@ -30263,16 +30504,17 @@ function createSttEngine(deps = {}) {
30263
30504
  dropQueued();
30264
30505
  checkDrain();
30265
30506
  },
30266
- stats: () => ({ segments, audioSeconds: round3(decodedSamples / SAMPLE_RATE) })
30507
+ stats: () => ({ segments, audioSeconds: round3(decodedSamples / SAMPLE_RATE), ...speakers ? { speakers: labels.size } : {} })
30267
30508
  };
30268
30509
  }
30269
30510
  return { load: load2, open };
30270
30511
  }
30271
- var SAMPLE_RATE, VAD_WINDOW, VAD_THRESHOLD, VAD_MIN_SILENCE_S, VAD_MIN_SPEECH_S, VAD_MAX_SPEECH_S, PAD_BEFORE_S, PAD_AFTER_S, RING_S, MAX_QUEUED_SEGMENTS, MAX_PENDING_AUDIO_S, NUM_THREADS, SttError, defaultLoadSherpa, errMsg2, round3, sttEngine;
30512
+ var SAMPLE_RATE, VAD_WINDOW, VAD_THRESHOLD, VAD_MIN_SILENCE_S, VAD_MIN_SPEECH_S, VAD_MAX_SPEECH_S, PAD_BEFORE_S, PAD_AFTER_S, RING_S, MAX_QUEUED_SEGMENTS, MAX_PENDING_AUDIO_S, NUM_THREADS, DIAR_CLUSTER_THRESHOLD, DIAR_MIN_ON_S, DIAR_MIN_OFF_S, EMBED_MAX_S, DIAR_TIMEOUT_MS, SttError, defaultLoadSherpa, errMsg2, round3, sttEngine;
30272
30513
  var init_engine = __esm({
30273
30514
  "../server/dist/stt/engine.js"() {
30274
30515
  "use strict";
30275
30516
  init_models();
30517
+ init_speakers();
30276
30518
  SAMPLE_RATE = 16e3;
30277
30519
  VAD_WINDOW = 512;
30278
30520
  VAD_THRESHOLD = 0.5;
@@ -30285,6 +30527,11 @@ var init_engine = __esm({
30285
30527
  MAX_QUEUED_SEGMENTS = 32;
30286
30528
  MAX_PENDING_AUDIO_S = 60;
30287
30529
  NUM_THREADS = Math.min(4, Math.max(1, Math.floor(os8.availableParallelism() / 2)));
30530
+ DIAR_CLUSTER_THRESHOLD = 0.6;
30531
+ DIAR_MIN_ON_S = 0.3;
30532
+ DIAR_MIN_OFF_S = 0.5;
30533
+ EMBED_MAX_S = 6;
30534
+ DIAR_TIMEOUT_MS = 6e4;
30288
30535
  SttError = class extends Error {
30289
30536
  code;
30290
30537
  constructor(code, message) {
@@ -30293,7 +30540,10 @@ var init_engine = __esm({
30293
30540
  this.name = "SttError";
30294
30541
  }
30295
30542
  };
30296
- defaultLoadSherpa = async () => (await import("sherpa-onnx-node")).default;
30543
+ defaultLoadSherpa = async () => {
30544
+ const [m, addon] = await Promise.all([import("sherpa-onnx-node"), import("sherpa-onnx-node/addon.js")]);
30545
+ return { ...m.default, offlineSpeakerDiarizationProcessAsync: addon.default.offlineSpeakerDiarizationProcessAsync };
30546
+ };
30297
30547
  errMsg2 = (err) => err instanceof Error ? err.message : String(err);
30298
30548
  round3 = (s) => Math.round(s * 1e3) / 1e3;
30299
30549
  sttEngine = createSttEngine();
@@ -30325,7 +30575,9 @@ function createSttProxy(deps = {}) {
30325
30575
  }
30326
30576
  });
30327
30577
  wss2.on("connection", (ws, req) => {
30328
- const extId = parseQuery2(req).get("ext");
30578
+ const q = parseQuery2(req);
30579
+ const extId = q.get("ext");
30580
+ const speakers = q.get("speakers") === "1";
30329
30581
  const openedAt = Date.now();
30330
30582
  let closed = false;
30331
30583
  let failed = false;
@@ -30349,14 +30601,14 @@ function createSttProxy(deps = {}) {
30349
30601
  };
30350
30602
  async function startEngine() {
30351
30603
  try {
30352
- await engine.load();
30604
+ await engine.load({ speakers });
30353
30605
  if (closed || failed || ended)
30354
30606
  return;
30355
30607
  stream = engine.open({
30356
30608
  // Recognized text goes to the client only — never to the log.
30357
30609
  onFinal: (f) => send2({ type: "final", ...f }),
30358
30610
  onError: (err) => fail2(err.code, err.message)
30359
- });
30611
+ }, { speakers });
30360
30612
  } catch (err) {
30361
30613
  if (err instanceof SttError)
30362
30614
  fail2(err.code, err.message);
@@ -30381,7 +30633,7 @@ function createSttProxy(deps = {}) {
30381
30633
  else if (s.state === "error")
30382
30634
  fail2("model", s.message ?? "model download failed");
30383
30635
  };
30384
- console.log(`[STT] stream open ext=${extId}`);
30636
+ console.log(`[STT] stream open ext=${extId}${speakers ? " speakers=1" : ""}`);
30385
30637
  ws.on("message", (data, isBinary) => {
30386
30638
  if (ended || failed)
30387
30639
  return;
@@ -30417,7 +30669,7 @@ function createSttProxy(deps = {}) {
30417
30669
  unsubscribe = null;
30418
30670
  stream?.abort();
30419
30671
  const st = stream?.stats();
30420
- console.log(`[STT] stream closed after ${((Date.now() - openedAt) / 1e3).toFixed(1)}s ext=${extId}${st ? ` segments=${st.segments} audio=${st.audioSeconds.toFixed(1)}s` : ""}`);
30672
+ console.log(`[STT] stream closed after ${((Date.now() - openedAt) / 1e3).toFixed(1)}s ext=${extId}${st ? ` segments=${st.segments} audio=${st.audioSeconds.toFixed(1)}s${st.speakers !== void 0 ? ` speakers=${st.speakers}` : ""}` : ""}`);
30421
30673
  });
30422
30674
  const initial = models.status();
30423
30675
  if (initial.state === "ready") {
@@ -32317,7 +32569,7 @@ function buildCard(workspace, interfaceUrl, caps) {
32317
32569
  return {
32318
32570
  name: raw.name,
32319
32571
  description: raw.description,
32320
- version: typeof raw.version === "string" ? raw.version : "2.2.1",
32572
+ version: typeof raw.version === "string" ? raw.version : "2.2.2",
32321
32573
  supportedInterfaces: [{ url: interfaceUrl, protocolBinding: "JSONRPC", protocolVersion: A2A_VERSION }],
32322
32574
  capabilities: { streaming: caps.streaming, pushNotifications: true, extendedAgentCard: false },
32323
32575
  ...caps.tokenAuth === false ? {} : {
@@ -35415,7 +35667,7 @@ function createCoreChecker(opts) {
35415
35667
  }
35416
35668
  function getCoreStatus(force) {
35417
35669
  checker ??= createCoreChecker({
35418
- haloVersion: "2.2.1",
35670
+ haloVersion: "2.2.2",
35419
35671
  fetchLatest: fetchNpmLatest,
35420
35672
  onChange: (status) => broadcast({ type: "updates:changed", scope: "core", status })
35421
35673
  });
@@ -35583,7 +35835,7 @@ function hubBase() {
35583
35835
  }
35584
35836
  function serverChecker() {
35585
35837
  checker2 ??= createExtensionsChecker({
35586
- haloVersion: "2.2.1",
35838
+ haloVersion: "2.2.2",
35587
35839
  installed: () => {
35588
35840
  const s = getSnapshot();
35589
35841
  return [...s.extensions, ...s.disabled];
@@ -41954,7 +42206,7 @@ var init_dist2 = __esm({
41954
42206
  init_git_ssh();
41955
42207
  __dirname3 = path93.dirname(fileURLToPath3(import.meta.url));
41956
42208
  PORT = config.server.port;
41957
- HALO_VERSION = "2.2.1";
42209
+ HALO_VERSION = "2.2.2";
41958
42210
  PROJECT_ROOT = findProjectRoot();
41959
42211
  HALO_HOME4 = path93.join(homedir25(), ".halo");
41960
42212
  GIT_SHA = readGitSha();
@@ -44029,7 +44281,7 @@ function runNode(opts) {
44029
44281
  shellTimeoutMs: config.timeout.shellExec,
44030
44282
  os: process.platform,
44031
44283
  hostname: os13.hostname(),
44032
- version: "2.2.1"
44284
+ version: "2.2.2"
44033
44285
  };
44034
44286
  const exec2 = createNodeExecutor({ root, base, allowShell }, log2);
44035
44287
  const instance = crypto17.randomUUID();
@@ -44545,7 +44797,7 @@ function installUserUnit(opts, run = runCommand) {
44545
44797
  }
44546
44798
 
44547
44799
  // src/index.ts
44548
- var VERSION = "2.2.1";
44800
+ var VERSION = "2.2.2";
44549
44801
  var HELP_TOP = `Usage: halo <command> [options]
44550
44802
 
44551
44803
  Commands:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turmind/halo",
3
- "version": "2.2.1",
3
+ "version": "2.2.2",
4
4
  "description": "Halo — multi-agent collaborative workspace. CLI + TUI + HTTP/WS server in one package.",
5
5
  "type": "module",
6
6
  "license": "MIT",