@turmind/halo 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin-out/404.html +1 -1
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +1 -1
- package/bundled-docs/design/architecture.md +1 -1
- package/bundled-docs/design/background-dispatch.md +2 -2
- package/bundled-docs/design/cron.md +3 -3
- package/bundled-docs/design/session.md +5 -3
- package/bundled-docs/design/storage.md +2 -2
- package/bundled-docs/dev/add-model-provider.md +1 -0
- package/bundled-docs/dev/api.md +1 -1
- package/bundled-docs/dev/deploy.md +2 -0
- package/dist/index.js +47 -15
- package/package.json +1 -1
- /package/admin-out/_next/static/{snq8MT7CRlizWQcyu-Kt4 → 52ayDC9phpvb-95S7qHHq}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{snq8MT7CRlizWQcyu-Kt4 → 52ayDC9phpvb-95S7qHHq}/_ssgManifest.js +0 -0
package/admin-out/404.html
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<!DOCTYPE html><!--
|
|
1
|
+
<!DOCTYPE html><!--52ayDC9phpvb_95S7qHHq--><html lang="en" class="dark"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" href="/_next/static/media/e4af272ccee01ff0-s.p.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="stylesheet" href="/_next/static/css/7b736202f0b1dbfd.css" data-precedence="next"/><link rel="stylesheet" href="/_next/static/css/e890aa0d13e94290.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-74f577c1b094f942.js"/><script src="/_next/static/chunks/1b86ba38-c29611e086b2ce25.js" async=""></script><script src="/_next/static/chunks/698-19872069fed27164.js" async=""></script><script src="/_next/static/chunks/main-app-340a5b58ed229c4d.js" async=""></script><script src="/_next/static/chunks/989-cb287787a4aca288.js" async=""></script><script src="/_next/static/chunks/app/layout-ea161287d934ba96.js" async=""></script><meta name="robots" content="noindex"/><meta name="next-size-adjust" content=""/><title>404: This page could not be found.</title><title>Halo - Multi-Agent Workspace</title><meta name="description" content="Multi-agent collaboration workspace for complex project delivery"/><link rel="icon" href="/favicon.ico" type="image/x-icon" sizes="16x16"/><link rel="icon" href="/icon.svg?3a6863548daca964" type="image/svg+xml" sizes="any"/><script>try{var t=localStorage.getItem('halo_theme');if(t==='light'||t==='midnight'||t==='warm')document.documentElement.dataset.theme=t}catch(e){}</script><script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body class="__variable_d8ebd1 font-sans antialiased"><div hidden=""><!--$--><!--/$--></div><div style="font-family:system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">This page could not be found.</h2></div></div></div><!--$--><!--/$--><script src="/_next/static/chunks/webpack-74f577c1b094f942.js" id="_R_" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[58321,[\"989\",\"static/chunks/989-cb287787a4aca288.js\",\"177\",\"static/chunks/app/layout-ea161287d934ba96.js\"],\"ThemeProvider\"]\n3:I[24554,[\"989\",\"static/chunks/989-cb287787a4aca288.js\",\"177\",\"static/chunks/app/layout-ea161287d934ba96.js\"],\"AppI18nProvider\"]\n4:I[63693,[],\"\"]\n5:I[74713,[],\"\"]\n6:I[23632,[],\"OutletBoundary\"]\n8:I[86889,[],\"AsyncMetadataOutlet\"]\na:I[23632,[],\"ViewportBoundary\"]\nc:I[23632,[],\"MetadataBoundary\"]\nd:\"$Sreact.suspense\"\nf:I[1935,[],\"\"]\n:HL[\"/_next/static/media/e4af272ccee01ff0-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n:HL[\"/_next/static/css/7b736202f0b1dbfd.css\",\"style\"]\n:HL[\"/_next/static/css/e890aa0d13e94290.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"52ayDC9phpvb-95S7qHHq\",\"p\":\"\",\"c\":[\"\",\"_not-found\"],\"i\":false,\"f\":[[[\"\",{\"children\":[\"/_not-found\",{\"children\":[\"__PAGE__\",{}]}]},\"$undefined\",\"$undefined\",true],[\"\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/7b736202f0b1dbfd.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}],[\"$\",\"link\",\"1\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/e890aa0d13e94290.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"className\":\"dark\",\"suppressHydrationWarning\":true,\"children\":[[\"$\",\"head\",null,{\"children\":[\"$\",\"script\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"try{var t=localStorage.getItem('halo_theme');if(t==='light'||t==='midnight'||t==='warm')document.documentElement.dataset.theme=t}catch(e){}\"}}]}],[\"$\",\"body\",null,{\"className\":\"__variable_d8ebd1 font-sans antialiased\",\"children\":[\"$\",\"$L2\",null,{\"children\":[\"$\",\"$L3\",null,{\"children\":[\"$\",\"$L4\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L5\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]}]}]]}]]}],{\"children\":[\"/_not-found\",[\"$\",\"$1\",\"c\",{\"children\":[null,[\"$\",\"$L4\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L5\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]]}],{\"children\":[\"__PAGE__\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],null,[\"$\",\"$L6\",null,{\"children\":[\"$L7\",[\"$\",\"$L8\",null,{\"promise\":\"$@9\"}]]}]]}],{},null,false]},null,false]},null,false],[\"$\",\"$1\",\"h\",{\"children\":[[\"$\",\"meta\",null,{\"name\":\"robots\",\"content\":\"noindex\"}],[[\"$\",\"$La\",null,{\"children\":\"$Lb\"}],[\"$\",\"meta\",null,{\"name\":\"next-size-adjust\",\"content\":\"\"}]],[\"$\",\"$Lc\",null,{\"children\":[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$d\",null,{\"fallback\":null,\"children\":\"$Le\"}]}]}]]}],false]],\"m\":\"$undefined\",\"G\":[\"$f\",[]],\"s\":false,\"S\":true}\n"])</script><script>self.__next_f.push([1,"b:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\n7:null\n"])</script><script>self.__next_f.push([1,"10:I[88809,[],\"IconMark\"]\n9:{\"metadata\":[[\"$\",\"title\",\"0\",{\"children\":\"Halo - Multi-Agent Workspace\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Multi-agent collaboration workspace for complex project delivery\"}],[\"$\",\"link\",\"2\",{\"rel\":\"icon\",\"href\":\"/favicon.ico\",\"type\":\"image/x-icon\",\"sizes\":\"16x16\"}],[\"$\",\"link\",\"3\",{\"rel\":\"icon\",\"href\":\"/icon.svg?3a6863548daca964\",\"type\":\"image/svg+xml\",\"sizes\":\"any\"}],[\"$\",\"$L10\",\"4\",{}]],\"error\":null,\"digest\":\"$undefined\"}\n"])</script><script>self.__next_f.push([1,"e:\"$9:metadata\"\n"])</script></body></html>
|
package/admin-out/index.html
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<!DOCTYPE html><!--
|
|
1
|
+
<!DOCTYPE html><!--52ayDC9phpvb_95S7qHHq--><html lang="en" class="dark"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" href="/_next/static/media/e4af272ccee01ff0-s.p.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="stylesheet" href="/_next/static/css/7b736202f0b1dbfd.css" data-precedence="next"/><link rel="stylesheet" href="/_next/static/css/e890aa0d13e94290.css" data-precedence="next"/><link rel="stylesheet" href="/_next/static/css/659eccb5db697b76.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-74f577c1b094f942.js"/><script src="/_next/static/chunks/1b86ba38-c29611e086b2ce25.js" async=""></script><script src="/_next/static/chunks/698-19872069fed27164.js" async=""></script><script src="/_next/static/chunks/main-app-340a5b58ed229c4d.js" async=""></script><script src="/_next/static/chunks/989-cb287787a4aca288.js" async=""></script><script src="/_next/static/chunks/app/layout-ea161287d934ba96.js" async=""></script><script src="/_next/static/chunks/1d2d5650-d8d130c85727fdbb.js" async=""></script><script src="/_next/static/chunks/87-6a2716c98a819c4e.js" async=""></script><script src="/_next/static/chunks/app/page-c9fb252d0cf34c20.js" async=""></script><meta name="next-size-adjust" content=""/><title>Halo - Multi-Agent Workspace</title><meta name="description" content="Multi-agent collaboration workspace for complex project delivery"/><link rel="icon" href="/favicon.ico" type="image/x-icon" sizes="16x16"/><link rel="icon" href="/icon.svg?3a6863548daca964" type="image/svg+xml" sizes="any"/><script>try{var t=localStorage.getItem('halo_theme');if(t==='light'||t==='midnight'||t==='warm')document.documentElement.dataset.theme=t}catch(e){}</script><script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body class="__variable_d8ebd1 font-sans antialiased"><div hidden=""><!--$--><!--/$--></div><div class="flex h-screen w-screen items-center justify-center bg-[var(--background)]"><div class="text-sm text-[var(--muted-foreground)]">Loading...</div></div><!--$--><!--/$--><script src="/_next/static/chunks/webpack-74f577c1b094f942.js" id="_R_" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[58321,[\"989\",\"static/chunks/989-cb287787a4aca288.js\",\"177\",\"static/chunks/app/layout-ea161287d934ba96.js\"],\"ThemeProvider\"]\n3:I[24554,[\"989\",\"static/chunks/989-cb287787a4aca288.js\",\"177\",\"static/chunks/app/layout-ea161287d934ba96.js\"],\"AppI18nProvider\"]\n4:I[63693,[],\"\"]\n5:I[74713,[],\"\"]\n6:I[5444,[],\"ClientPageRoot\"]\n7:I[3124,[\"560\",\"static/chunks/1d2d5650-d8d130c85727fdbb.js\",\"87\",\"static/chunks/87-6a2716c98a819c4e.js\",\"989\",\"static/chunks/989-cb287787a4aca288.js\",\"974\",\"static/chunks/app/page-c9fb252d0cf34c20.js\"],\"default\"]\na:I[23632,[],\"OutletBoundary\"]\nc:I[86889,[],\"AsyncMetadataOutlet\"]\ne:I[23632,[],\"ViewportBoundary\"]\n10:I[23632,[],\"MetadataBoundary\"]\n11:\"$Sreact.suspense\"\n13:I[1935,[],\"\"]\n:HL[\"/_next/static/media/e4af272ccee01ff0-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n:HL[\"/_next/static/css/7b736202f0b1dbfd.css\",\"style\"]\n:HL[\"/_next/static/css/e890aa0d13e94290.css\",\"style\"]\n:HL[\"/_next/static/css/659eccb5db697b76.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"52ayDC9phpvb-95S7qHHq\",\"p\":\"\",\"c\":[\"\",\"\"],\"i\":false,\"f\":[[[\"\",{\"children\":[\"__PAGE__\",{}]},\"$undefined\",\"$undefined\",true],[\"\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/7b736202f0b1dbfd.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}],[\"$\",\"link\",\"1\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/e890aa0d13e94290.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"className\":\"dark\",\"suppressHydrationWarning\":true,\"children\":[[\"$\",\"head\",null,{\"children\":[\"$\",\"script\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"try{var t=localStorage.getItem('halo_theme');if(t==='light'||t==='midnight'||t==='warm')document.documentElement.dataset.theme=t}catch(e){}\"}}]}],[\"$\",\"body\",null,{\"className\":\"__variable_d8ebd1 font-sans antialiased\",\"children\":[\"$\",\"$L2\",null,{\"children\":[\"$\",\"$L3\",null,{\"children\":[\"$\",\"$L4\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L5\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],[]],\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]}]}]]}]]}],{\"children\":[\"__PAGE__\",[\"$\",\"$1\",\"c\",{\"children\":[[\"$\",\"$L6\",null,{\"Component\":\"$7\",\"searchParams\":{},\"params\":{},\"promises\":[\"$@8\",\"$@9\"]}],[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/659eccb5db697b76.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]],[\"$\",\"$La\",null,{\"children\":[\"$Lb\",[\"$\",\"$Lc\",null,{\"promise\":\"$@d\"}]]}]]}],{},null,false]},null,false],[\"$\",\"$1\",\"h\",{\"children\":[null,[[\"$\",\"$Le\",null,{\"children\":\"$Lf\"}],[\"$\",\"meta\",null,{\"name\":\"next-size-adjust\",\"content\":\"\"}]],[\"$\",\"$L10\",null,{\"children\":[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$11\",null,{\"fallback\":null,\"children\":\"$L12\"}]}]}]]}],false]],\"m\":\"$undefined\",\"G\":[\"$13\",[]],\"s\":false,\"S\":true}\n"])</script><script>self.__next_f.push([1,"8:{}\n9:\"$0:f:0:1:2:children:1:props:children:0:props:params\"\n"])</script><script>self.__next_f.push([1,"f:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\nb:null\n"])</script><script>self.__next_f.push([1,"14:I[88809,[],\"IconMark\"]\nd:{\"metadata\":[[\"$\",\"title\",\"0\",{\"children\":\"Halo - Multi-Agent Workspace\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Multi-agent collaboration workspace for complex project delivery\"}],[\"$\",\"link\",\"2\",{\"rel\":\"icon\",\"href\":\"/favicon.ico\",\"type\":\"image/x-icon\",\"sizes\":\"16x16\"}],[\"$\",\"link\",\"3\",{\"rel\":\"icon\",\"href\":\"/icon.svg?3a6863548daca964\",\"type\":\"image/svg+xml\",\"sizes\":\"any\"}],[\"$\",\"$L14\",\"4\",{}]],\"error\":null,\"digest\":\"$undefined\"}\n"])</script><script>self.__next_f.push([1,"12:\"$d:metadata\"\n"])</script></body></html>
|
package/admin-out/index.txt
CHANGED
|
@@ -15,7 +15,7 @@ e:I[23632,[],"ViewportBoundary"]
|
|
|
15
15
|
:HL["/_next/static/css/7b736202f0b1dbfd.css","style"]
|
|
16
16
|
:HL["/_next/static/css/e890aa0d13e94290.css","style"]
|
|
17
17
|
:HL["/_next/static/css/659eccb5db697b76.css","style"]
|
|
18
|
-
0:{"P":null,"b":"
|
|
18
|
+
0:{"P":null,"b":"52ayDC9phpvb-95S7qHHq","p":"","c":["",""],"i":false,"f":[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/7b736202f0b1dbfd.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/_next/static/css/e890aa0d13e94290.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]],["$","html",null,{"lang":"en","className":"dark","suppressHydrationWarning":true,"children":[["$","head",null,{"children":["$","script",null,{"dangerouslySetInnerHTML":{"__html":"try{var t=localStorage.getItem('halo_theme');if(t==='light'||t==='midnight'||t==='warm')document.documentElement.dataset.theme=t}catch(e){}"}}]}],["$","body",null,{"className":"__variable_d8ebd1 font-sans antialiased","children":["$","$L2",null,{"children":["$","$L3",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]}]]}]]}],{"children":["__PAGE__",["$","$1","c",{"children":[["$","$L6",null,{"Component":"$7","searchParams":{},"params":{},"promises":["$@8","$@9"]}],[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/659eccb5db697b76.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]],["$","$La",null,{"children":["$Lb",["$","$Lc",null,{"promise":"$@d"}]]}]]}],{},null,false]},null,false],["$","$1","h",{"children":[null,[["$","$Le",null,{"children":"$Lf"}],["$","meta",null,{"name":"next-size-adjust","content":""}]],["$","$L10",null,{"children":["$","div",null,{"hidden":true,"children":["$","$11",null,{"fallback":null,"children":"$L12"}]}]}]]}],false]],"m":"$undefined","G":["$13",[]],"s":false,"S":true}
|
|
19
19
|
8:{}
|
|
20
20
|
9:"$0:f:0:1:2:children:1:props:children:0:props:params"
|
|
21
21
|
f:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]
|
|
@@ -159,7 +159,7 @@ File: `agents/session-skill-commands.ts`. Stateless, pure reads. Resolves which
|
|
|
159
159
|
|
|
160
160
|
### SessionStateStore — rawMessages disk persistence
|
|
161
161
|
|
|
162
|
-
File: `agents/session-state-store.ts`. Stateless. Saves/loads an agent's `rawMessages` (LLM-facing history) to its
|
|
162
|
+
File: `agents/session-state-store.ts`. Stateless (the writer's append baseline lives in `session-log.ts`). Saves/loads an agent's `rawMessages` (LLM-facing history) to its `<seg>.raw.jsonl`, one message per line — a save appends the new messages, rewrites the last line in place when only it changed, and does an atomic full rewrite for anything else (compaction, repair, deleteExchange) — plus the row's `output` / `lastActivityAt` (throttled on the per-event save points). It is the raw half of session persistence; SessionUIStore owns the UI-log half in its own `<seg>.ui.jsonl`, so neither rewrites the other. `saveAgentState` takes a narrow `SavableSession` (5 fields), not the full AgentSession. Host surface: workspaceRoot + `isSessionDeleted` (the tombstone short-circuit so a late save can't resurrect a deleted file) + `metaDb` (the db holding the session's row).
|
|
163
163
|
|
|
164
164
|
## Helper modules
|
|
165
165
|
|
|
@@ -63,7 +63,7 @@ bindOrCreateSession → createSession(...) → subscribeSession(client, sm, sid)
|
|
|
63
63
|
Nothing is lost without a listener: `SessionUIStore.emitEvent` folds every event into the root's `UIState` (`reduceIntoUIState → applyEvent`) and persists the log it changed **before** it looks up listeners. Listeners are a pure fan-out for live UI; the persistence path is independent of them.
|
|
64
64
|
|
|
65
65
|
- **Which file**: `applyEvent` returns the log it touched. A root event writes the root's session file; a sub-agent event (`taskId` set) writes only that sub-session's **own** file from `subSessionLogs[taskId]` — the root's file isn't rewritten for sub-agent traffic. The sub log is dropped from memory at its `agent_done` (its file is complete by then).
|
|
66
|
-
- **When**: `applyEvent`'s `persist` hint — `'flush'` writes now (root `complete`, a sub's `agent_done`), `'debounce'` coalesces within 500 ms (
|
|
66
|
+
- **When**: `applyEvent`'s `persist` hint — `'flush'` writes now (`tool_call`, `tool_result`, `usage`, root `complete`, a sub's `agent_done`, `error`, `call_discarded`), `'debounce'` coalesces within 500 ms (`user` / `system` / `context` rows), absent = no write (the next write carries the change). Timers are keyed by the log's own id, so root and sub writes don't cancel each other.
|
|
67
67
|
- **Dirty tracking**: a root event marks the root `uiStateDirty` until `persistLog` lands it. `isUIStateDirty(rootId)` is what the WS saves below gate on — a state only seeded from disk (viewing a session another process, e.g. a cron `halo cli` child, is driving) is clean and is never written back, so it can't overwrite that process's newer file.
|
|
68
68
|
|
|
69
69
|
### When save fires
|
|
@@ -73,7 +73,7 @@ Background state persists in four scenarios:
|
|
|
73
73
|
1. **SessionUIStore's own persist** — the per-event rule above; this is what keeps a background session's files current
|
|
74
74
|
2. **`unsubscribeSession`** — a session leaves the connection's set (`unsubscribe`, or a workspace switch dropping every subscription); `saveSession` runs before the listener is released. It returns early unless `isUIStateDirty`
|
|
75
75
|
3. **Disconnect** — WS closes (or errors): every subscription without active work in its tree is saved and released (same dirty gate); the rest detach (below)
|
|
76
|
-
4. **Server shutdown** — `landForShutdown()` (SessionManager
|
|
76
|
+
4. **Server shutdown** — `landForShutdown()` (SessionManager). Its UI half, SessionUIStore `flushAll()`, lands every loaded tree: a dirty root plus every live sub log, since pending debounce timers would never fire. Its raw log + row half covers only the sessions this process is running (`promise !== null`, subs too) — an idle in-memory copy was saved on the spot and may be stale ([session.md](session.md#session-files))
|
|
77
77
|
|
|
78
78
|
Until 1.5.3-alpha, scenarios 2 and 3 went through `client.backgroundSaves`: the save closures `session:clear` registered, run on the next subscribe to that session or flushed on disconnect.
|
|
79
79
|
|
|
@@ -29,8 +29,8 @@ Schedules are **durable** — the source of truth is the `cron_jobs` table; in-m
|
|
|
29
29
|
|
|
30
30
|
The runner arms its own `setTimeout` toward each target in hops of at most 30 s; a hop that wakes early re-arms toward the **same** target, and the next target is computed (`nextRun(now)`) only after a fire — a sleep / clock jump over several fires runs once, no catch-up. Why not croner's own timers: in croner 10.0.1 a timer waking a fraction of a ms early re-plans from a fresh clock read, and when the clock crosses the fire instant mid-re-plan, an instance that has never fired (every job right after a restart, every one-shot) silently skips that fire — ~10% of first fires after a restart in production; the upstream fix is only in 11.0.0-dev (`test/cron-fire-race.test.ts`).
|
|
31
31
|
|
|
32
|
-
- **Recurring**: standard cron expression (`job.schedule`), optional `timezone`, optional **active window** `[activeFrom, activeUntil)` (epoch ms, `active_from` / `active_until`, NULL = unbounded; `CRON_MIGRATIONS` slot 2). The window is passed to croner as native `startAt` / `stopAt` (`activeWindowOptions` in `runner.ts`; `startAt` is `activeFrom − 1 ms` because croner's startAt is exclusive), so fires outside it simply never happen — no `cron_runs` row. "Pause until X" = push `activeFrom` to X; there is no skip-list for a gap in the middle. Past `activeUntil` the job stays `enabled` (so it can be extended later) but `scheduleJob` creates no croner and logs `past activeUntil; not scheduled` once — the id goes into `_expired` so reconcile's "fingerprint cached but not in `_active`" retry branch treats it as handled instead of re-scheduling (and re-logging) every 10s. The REST `nextRunAt` uses the same options, so the list shows the first post-pause fire; the admin badges a job "active from …" / "ended". Run-now ignores the window. Recurring only: a window together with `runAt` is rejected at every write surface (REST on the merged row, skill helper).
|
|
33
|
-
- **One-shot**: `job.runAt` (epoch ms) — the runner's timer fires once at that instant (no croner). After it completes, `finalize` sets `enabled=0` so it never re-fires. A `runAt` already in the past at schedule time is marked `lastRunStatus='missed'` and disabled (rather than firing immediately or retrying every reconcile). The `run_at` column is `CRON_MIGRATIONS` slot 0 in `db/cron-db.ts`, run through the shared `runMigrations` / `PRAGMA user_version` mechanism (see [storage.md](storage.md#schema-change-rules)).
|
|
32
|
+
- **Recurring**: standard cron expression (`job.schedule`), optional `timezone`, optional **active window** `[activeFrom, activeUntil)` (epoch ms, `active_from` / `active_until`, NULL = unbounded; `CRON_MIGRATIONS` slot 2). The window is passed to croner as native `startAt` / `stopAt` (`activeWindowOptions` in `runner.ts`; `startAt` is `activeFrom − 1 ms` because croner's startAt is exclusive), so fires outside it simply never happen — no `cron_runs` row. "Pause until X" = push `activeFrom` to X; there is no skip-list for a gap in the middle. Past `activeUntil` the job stays `enabled` (so it can be extended later) but `scheduleJob` creates no croner and logs `past activeUntil; not scheduled` once — the id goes into `_expired` so reconcile's "fingerprint cached but not in `_active`" retry branch treats it as handled instead of re-scheduling (and re-logging) every 10s. A schedule or timezone croner rejects is handled the same way: one `schedule invalid` warn per fingerprint (the timezone in it), retried once the row changes. REST validates the expression but not the timezone, and croner throws on a bad timezone only at `nextRun()`, so either can reach this from REST or an out-of-band write. The REST `nextRunAt` uses the same options, so the list shows the first post-pause fire; the admin badges a job "active from …" / "ended". Run-now ignores the window. Recurring only: a window together with `runAt` is rejected at every write surface (REST on the merged row, skill helper).
|
|
33
|
+
- **One-shot**: `job.runAt` (epoch ms) — the runner's timer fires once at that instant (no croner). After it completes, `finalize` sets `enabled=0` so it never re-fires. A `runAt` already in the past at schedule time is marked `lastRunStatus='missed'` and disabled (rather than firing immediately or retrying every reconcile) — unless its own run is still in flight (`_inflight`, e.g. `POST /api/cron/reload` mid-run): `finalize` then records the real status. At boot, the orphan sweep already finalized a one-shot whose run it found (`enabled=0`, `failed`), so the reload never sees it as missed. The `run_at` column is `CRON_MIGRATIONS` slot 0 in `db/cron-db.ts`, run through the shared `runMigrations` / `PRAGMA user_version` mechanism (see [storage.md](storage.md#schema-change-rules)).
|
|
34
34
|
- **Folder + stateless** (`CRON_MIGRATIONS` slot 4, v5): `cron_jobs.working_dir` — the folder the run focuses on, passed as `halo cli --dir` (same semantics as a sub-agent's `working_dir`: the folder's `.halo/INSTRUCTIONS.md` chain joins the prompt, plus a `Working directory:` line); stored workspace-relative, NULL = the workspace root; validated at every write surface (inside the workspace, existing — REST re-checks a stored folder when `workspacePath` moves). `cron_jobs.stateless` (0/1) — **new session every run**: each fire gets a fresh root session `cron-<id>-<base36 ms>` (`perRunSessionId`, same no-double-prefix rule as `effectiveSessionId`), so nothing accumulates across fires (each job keeps only its own newest 10 runs; per-run sessions are deleted along with their run rows, see [Retention](#retention)); mutually exclusive with `session_id` (`400` on the merged row). `cron_runs.session_id` — the session that run used, the only record of a stateless run's id (NULL on rows from before v5). `cron_runs.workspace_path` (slot 5, v6) — the workspace the run fired against (the job's at fire time), i.e. where a stateless run's session lives even after the job moves workspace or is deleted; v6 backfills it from the job row, leaving NULL on rows whose job was already gone.
|
|
35
35
|
|
|
36
36
|
### Trigger-mode exclusivity (the db contract)
|
|
@@ -93,7 +93,7 @@ The step-1 guard is per-process, so it has one blind spot: the server dies (SIGK
|
|
|
93
93
|
1. Take the session the orphan was spawned on from the run row (`cron_runs.session_id` — the only source for a stateless job's per-run id; a row from before that column falls back to the job row's effective session, a job deleted since to the default rule on its id), then re-enter its jobId into `_inflight` and its session key into `_inflightSessions` first — the core invariant: while the orphan may be alive, no new cli for that job *or for any other job on that session* is spawned (a fire in that window is recorded as `skipped`, same as normal overlap).
|
|
94
94
|
2. If the row has a pid (POSIX): **identity-check before killing** — `ps -p <pid> -o args=` must carry the session id as a whole argv token (the `-s <session>` value; whole-token so `cron-a` never matches `cron-ab`), so a recycled pid never kills an innocent process. If the session came from the job row (pre-v5 run row) and was re-pointed after the orphan was spawned, the fingerprint won't match and the orphan is left alone — the safe direction. Verified → SIGTERM (graceful path, same as timeout phase 1); still alive after a 30s async grace (`unref`ed timer, identity re-checked — pid reuse again) → SIGKILL the descendant tree.
|
|
95
95
|
3. No pid / already dead / fingerprint mismatch / Windows: no kill, log only. Windows deliberately marks-only — there's no cheap command-line identity check (WMI is slow and sometimes policy-blocked), and a full-tree `taskkill /T /F` on a reused pid is irreversible while a double-written session is fixable; the orphan-survives-server-death window is also naturally smaller there.
|
|
96
|
-
4. Either way the row is finalized as `failed` (`failureReason` notes the orphan disposition, `completedAt` stamped) so the UI stops showing a ghost `running`, and the jobId / session key are released once the orphan is confirmed dead (or immediately when nothing was killable).
|
|
96
|
+
4. Either way the row is finalized as `failed` (`failureReason` notes the orphan disposition, `completedAt` stamped) so the UI stops showing a ghost `running` — for a one-shot its job row too, as `finalize` would (`enabled=0`, `lastRunStatus='failed'`, `lastRun*` = this run, `cron:job_changed` pushed); a recurring job's row is left alone — and the jobId / session key are released once the orphan is confirmed dead (or immediately when nothing was killable).
|
|
97
97
|
|
|
98
98
|
Only the kill grace is asynchronous; the boot-blocking part is one small select + row updates.
|
|
99
99
|
|
|
@@ -22,7 +22,7 @@ Their metadata row lives in a global db next to the files, `~/.halo/global/inter
|
|
|
22
22
|
|
|
23
23
|
No file holds metadata. Everything the 1.x header carried is derived or in the session's db row: agentId = directory, leaf id = file name, archive count = segment files on disk; `title`, `exchange_count`, `context_tokens`, `total_output_tokens`, `output`, `last_activity_at` = row columns (see [SQLite metadata](#sqlite-metadata)).
|
|
24
24
|
|
|
25
|
-
**Writes append.** `sessions/session-log.ts` keeps a process-local writer cache per file (what this process last wrote or read, stamped with the file's ino / size / mtime). A save diffs memory against it **by object identity** — a persisted row or raw message is never edited in place; every mutator replaces the object (raw: or its `content` array, or changes its length) — and appends only the new records with one `write` on an `O_APPEND` fd. A full atomic rewrite (tmp + rename) happens only for real non-append changes: compaction (raw), archive / `replaceMessageLog` / `deleteExchange`, a stamp that no longer matches (another process — a cron `halo cli` child, CLI/TUI — wrote the file: rewrite from this process's memory, the same last-writer-wins as 1.x), or the first write of a file in this process. The stamp is checked at the write itself, not by a stat before every save: an append or tail write compares it with the `fstat` of the fd it writes through and turns into a full rewrite on a mismatch; only a save that is a full rewrite anyway stats first (its exchange-count delta is relative to what the file holds); a save with nothing new touches nothing. **Another process's write** (a cron `halo cli` child, the TUI, a manual `halo cli -s`) is read, not overwritten, before this process uses its cached copies again. The root's two copies — the UIState (kept past release) and an idle AgentSession a view loaded — can part wherever one of them is read from disk while the other is still cached, so the check sits at those points, one stat per cached copy against its writer baseline: `appendUserMessage` (a turn's user row, checked before it lands — dropping the state after would discard it; this precedes the `ensureSession` check on the WS path), `ensureSession`'s restore (raw about to be read; the cached UIState is checked), `ensureUIState`'s disk seed (the UI about to be read; a cached idle root AgentSession whose raw changed reloads its history in place — `refreshStaleHistory`, in place because callers such as `querySession` hold the object and its queue), plus `deleteExchange` (reads UI before raw), manual `compactSession` (its UI seed runs after `beginCompact`, past the seed's guard) and `querySession` waking an idle root with both copies cached (no point above runs: `ensureSession` returns the cached session, the emit doesn't seed) — there only the UIState is dropped, after `ensureSession`'s await; the emit re-seeds it and `refreshStaleHistory` reloads the history in place. Never the AgentSession: a sub's auto-report calls `querySession` from its `finally`, before its release, and dropping the tree there would skip that release's flush and save. A file with no baseline — the load found a torn line or another process's open in-flight row — counts as written elsewhere unless the UIState holds unsaved rows. On a hit the tree's idle copies are dropped (`forgetExternalWrite`, which cron also calls when its child exits) and both files reload from disk; the dropped UIState's pending debounced writes are cancelled, not flushed (a flush would rewrite the other writer's file from the stale state) — a `system` / `user` row from the last ≤500ms before the other writer wrote is the cost. The view path (`prepareForView` re-seeding a root this process doesn't drive) cancels the same way when the file was written elsewhere, and flushes otherwise. Skipped while the tree has a turn or compact in flight or a queued message — live state isn't dropped, and the two writers race (the last full rewrite wins). So a normal turn after a release stats the UI log twice (`appendUserMessage`, then the restore). Known gap: another process starting a turn in the few ms between `appendUserMessage` and `ensureSession`'s restore (the `buildAgentInstance` await) makes the restore's check drop the UIState — this process's user row, already in it, is then missing from the UI log, though the turn and its raw copy run. Not when the other process is mid-turn: the seed taken over its open in-flight row has no baseline, and a state with unsaved rows and no baseline isn't dropped. Whenever a save does replace another process's write (a copy still cached with no check in between: a notification, a Stop fold), the session's other log is rewritten whole from memory in the same save (`writeUILog` → `foreign`, `writeRawLog` → true; `persistUILog` / `saveAgentState` pair them), so the two files stay a pair, never one with the other process's turns beside this process's. One exception: a sub-session's raw write after its `agent_done` rewrites raw only — its UI log left the root's UIState at `agent_done`, so there is no copy to pair it with. Never spliced into the other's file either way. Loaders that hand their result to the live session (`prime`) seed the cache, so the first save after a restore appends. Writes stay synchronous on the same call sites as 1.x, so UI and raw read→diff→write never interleave, the tombstone check and the write share one synchronous slice, and `flushAll` lands before exit. A torn last line (crash mid-write, or an EFS unmount) fails to parse: the reader drops it and the next write is a full rewrite.
|
|
25
|
+
**Writes append.** `sessions/session-log.ts` keeps a process-local writer cache per file (what this process last wrote or read, stamped with the file's ino / size / mtime). A save diffs memory against it **by object identity** — a persisted row or raw message is never edited in place; every mutator replaces the object (raw: or its `content` array, or changes its length) — and appends only the new records with one `write` on an `O_APPEND` fd. A full atomic rewrite (tmp + rename) happens only for real non-append changes: compaction (raw), archive / `replaceMessageLog` / `deleteExchange`, a stamp that no longer matches (another process — a cron `halo cli` child, CLI/TUI — wrote the file: rewrite from this process's memory, the same last-writer-wins as 1.x), or the first write of a file in this process. The stamp is checked at the write itself, not by a stat before every save: an append or tail write compares it with the `fstat` of the fd it writes through and turns into a full rewrite on a mismatch; only a save that is a full rewrite anyway stats first (its exchange-count delta is relative to what the file holds); a save with nothing new touches nothing. **Another process's write** (a cron `halo cli` child, the TUI, a manual `halo cli -s`) is read, not overwritten, before this process uses its cached copies again. The root's two copies — the UIState (kept past release) and an idle AgentSession a view loaded — can part wherever one of them is read from disk while the other is still cached, so the check sits at those points, one stat per cached copy against its writer baseline: `appendUserMessage` (a turn's user row, checked before it lands — dropping the state after would discard it; this precedes the `ensureSession` check on the WS path), `ensureSession`'s restore (raw about to be read; the cached UIState is checked), `ensureUIState`'s disk seed (the UI about to be read; a cached idle root AgentSession whose raw changed reloads its history in place — `refreshStaleHistory`, in place because callers such as `querySession` hold the object and its queue), plus `deleteExchange` (reads UI before raw), manual `compactSession` (its UI seed runs after `beginCompact`, past the seed's guard) and `querySession` waking an idle root with both copies cached (no point above runs: `ensureSession` returns the cached session, the emit doesn't seed) — only when the session was already in memory before the await (a cold wake's restore has just checked), and there only the UIState is dropped, after `ensureSession`'s await; the emit re-seeds it and `refreshStaleHistory` reloads the history in place. Never the AgentSession: a sub's auto-report calls `querySession` from its `finally`, before its release, and dropping the tree there would skip that release's flush and save. A file with no baseline — the load found a torn line or another process's open in-flight row — counts as written elsewhere unless the UIState holds unsaved rows. On a hit the tree's idle copies are dropped (`forgetExternalWrite`, which cron also calls when its child exits) and both files reload from disk; the dropped UIState's pending debounced writes are cancelled, not flushed, when the UI log itself was written elsewhere (a flush would rewrite the other writer's file from the stale state) — a `system` / `user` row from the last ≤500ms before the other writer wrote is the cost; after a raw-only write elsewhere the UI file is still this process's, and they flush as usual. The view path (`prepareForView` re-seeding a root this process doesn't drive) cancels the same way when the file was written elsewhere, and flushes otherwise. Skipped while the tree has a turn or compact in flight or a queued message — live state isn't dropped, and the two writers race (the last full rewrite wins). So a normal turn after a release stats the UI log twice (`appendUserMessage`, then the restore). Known gap: another process starting a turn in the few ms between `appendUserMessage` and `ensureSession`'s restore (the `buildAgentInstance` await) makes the restore's check drop the UIState — this process's user row, already in it, is then missing from the UI log, though the turn and its raw copy run. Not when the other process is mid-turn: the seed taken over its open in-flight row has no baseline, and a state with unsaved rows and no baseline isn't dropped. Whenever a save does replace another process's write (a copy still cached with no check in between: a notification, a Stop fold), the session's other log is rewritten whole from memory in the same save (`writeUILog` → `foreign`, `writeRawLog` → true; `persistUILog` / `saveAgentState` pair them), so the two files stay a pair, never one with the other process's turns beside this process's. One exception: a sub-session's raw write after its `agent_done` rewrites raw only — its UI log left the root's UIState at `agent_done`, so there is no copy to pair it with. Never spliced into the other's file either way. Loaders that hand their result to the live session (`prime`) seed the cache, so the first save after a restore appends. Writes stay synchronous on the same call sites as 1.x, so UI and raw read→diff→write never interleave, the tombstone check and the write share one synchronous slice, and `flushAll` lands before exit. A torn last line (crash mid-write, or an EFS unmount) fails to parse: the reader drops it and the next write is a full rewrite.
|
|
26
26
|
|
|
27
27
|
**The in-flight assistant has a stable id.** `TurnState.inflight = { id, at }` is assigned by the first snapshot of a streaming reply and reused by `flushAssistantMessage` / `flushCompletedAssistantMessage` for the finalized row. Every persist mid-stream writes an `inflight` record carrying only the changed blocks; the final `add` with the same id retires it — exactly one row, and live views (WS / web snapshots) show the same id. An interrupt flush writes the partial reply under that id, raw gets the streamed text + `INTERRUPTED_REPLY_MARKER`, so after a reload both halves hold the partial reply once. SIGKILL loses what streamed after the last write.
|
|
28
28
|
|
|
@@ -395,14 +395,14 @@ Frontend network issues don't affect the backend:
|
|
|
395
395
|
| Account-level error — **`httpStatus` 401 / 402 / 403** when a status is available; keyword match (insufficient balance / suspended / invalid key / unauthorized / authentication) only when none is | Unrecoverable — report to user, **no** retry |
|
|
396
396
|
| Rate limiting / throttling — `err.name === 'ThrottlingException'`, `httpStatus 429`, or keyword (`throttl` / `rate limit` / `ServiceUnavailableException`) | Exponential backoff (`2s * 2^attempt` + jitter, capped at 60s: 2s/4s/8s/16s…), retry |
|
|
397
397
|
| **Transient server-side error (5xx / timeout)** | Same exponential backoff as throttling — see [Transient server-error classification](#transient-server-error-classification) below |
|
|
398
|
-
| Transient transport error (`fetch failed`, `ECONNRESET`, headers timeout,
|
|
398
|
+
| Transient transport error (`fetch failed`, `ECONNRESET`, headers timeout, …; a stream that ends before its end events, `AnthropicStreamTruncated` / `OpenAIStreamTruncated`) | Short backoff (`1s * 2^attempt` + jitter), retry |
|
|
399
399
|
| Corrupted messages (`tool_use ids without tool_result`) | Repair + retry |
|
|
400
400
|
| **4xx multimodal rejection** (`Multimodal data is corrupted` / `Could not process image`) | Replace all image blocks in history with text placeholders, persist, retry — see [Multimodal 4xx degrade](#multimodal-4xx-degrade) below |
|
|
401
401
|
| Unrecoverable error | Report to user, stop |
|
|
402
402
|
|
|
403
403
|
**Refusal stop.** `stop_reason: "refusal"` (Anthropic models on Bedrock invoke + generic anthropic) is an HTTP 200 with `stop_details {category, explanation}` — not an error, so it never enters the retry matrix above. `agent-loop.ts` handles it explicitly: no assistant message is pushed, any partial `tool_use` is **not** executed, the loop ends; `session-manager.ts` emits a `system` event `⚠️ [<agent>] Model declined to respond (<category>): <explanation>` suggesting `/new`. Previously it fell through the `end_turn` path and looked like a silent stall.
|
|
404
404
|
|
|
405
|
-
**Classification order: structured signal first, message text last.** The interrupt branch is decided by the attempt's own `AbortController` signal (every interrupt path we own goes through `abortReason()` on it), never by the words `cancelled` / `aborted` in the message — an upstream error body can legitimately read `"The operation was aborted due to …"` (Bedrock), and matching that swallowed a real failure as a user interrupt: silent `break`, no retry, no error event, an empty reply. Likewise the account-level branch trusts `httpStatus` when one was recovered: 401/402/403 is terminal, any *other* status is not, whatever the body says — an OpenAI-compatible 503 whose body reads `"authentication service temporarily unavailable"` used to be classified as a dead key and never retried. The keyword list is only consulted when no status could be extracted. Pinned by `turn-retry-idempotent.test.ts` (
|
|
405
|
+
**Classification order: structured signal first, message text last.** The interrupt branch is decided by the attempt's own `AbortController` signal (every interrupt path we own goes through `abortReason()` on it), never by the words `cancelled` / `aborted` in the message — an upstream error body can legitimately read `"The operation was aborted due to …"` (Bedrock), and matching that swallowed a real failure as a user interrupt: silent `break`, no retry, no error event, an empty reply. Likewise the account-level branch trusts `httpStatus` when one was recovered: 401/402/403 is terminal, any *other* status is not, whatever the body says — an OpenAI-compatible 503 whose body reads `"authentication service temporarily unavailable"` used to be classified as a dead key and never retried. The keyword list is only consulted when no status could be extracted. Pinned by `turn-retry-idempotent.test.ts` (seven classification cases).
|
|
406
406
|
|
|
407
407
|
**Abort reasons are normalized through the `abortReason()` helper** — all three abort call sites (graceful interrupt after `tool_result`, `interruptSession`'s hard interrupt, `stopSession`'s stop) wrap the reason string in `new DOMException(reason, 'AbortError')` instead of passing it raw. Node 22 gotcha: `fetch` rejects with the abort reason **as-is**, so `controller.abort('interrupt')` surfaced the bare string `'interrupt'` (not an Error, `errName === ''`) in `runAgentTurn`'s catch — it missed the AbortError branch and logged a fake "Unrecoverable: interrupt" error. Contract pinned by a queue-semantics test.
|
|
408
408
|
|
|
@@ -433,6 +433,8 @@ Throttling is checked the same way one branch earlier — structured `Throttling
|
|
|
433
433
|
|
|
434
434
|
Backoff is identical to throttling: `2s * 2^attempt` + up to 1s jitter, capped at 60s, for up to `config.agent.maxRetries` (5) attempts.
|
|
435
435
|
|
|
436
|
+
**A stream that ends early without an error is network-class.** `AnthropicStreamAccumulator.finish()` (`agents/anthropic-stream.ts` — Bedrock, generic anthropic, MiniMax, Qwen) throws `AnthropicStreamTruncated` when the stream ended cleanly but neither a `message_delta` with a `stop_reason` nor a `message_stop` arrived (seen on Bedrock: minutes of thinking, then a clean end, out=0). Both events must be missing — a compatible provider may send only one of them. `ChatCompletionAccumulator.finish()` (`agents/openai-chat-stream.ts` — openai / deepseek / kimi / zhipu / doubao / hunyuan / gemini) throws `OpenAIStreamTruncated` when no `finish_reason` arrived, or one of DeepSeek's server-side cut-offs, `insufficient_system_resource` (lack of inference resources) or `aborted` (generation interrupted — our own aborts go through the signal, never a `finish_reason`); every live-probed provider sends one on its last choices chunk, before the usage chunk and `[DONE]`. `classifyModelError` maps both to `network` (`model-error.ts` branch 3c), so the turn gets the short backoff instead of ending as an empty `end_turn` reply. An abort still surfaces as an AbortError: the callers check their signal before `finish()`.
|
|
437
|
+
|
|
436
438
|
**Why this matters (original pain point)**: Bedrock returns a generic 500 with the message `"… is unable to process your request"` — no `Throttling`/`Overloaded`/`rate limit` keyword. The old logic, which classified retryable errors only by message-string keywords, matched nothing and let the turn die on attempt 1. Keying the retry on the **structured** `errName` / `httpStatus` instead of substrings is what makes the 5xx/timeout retry fire across every provider, Bedrock included.
|
|
437
439
|
|
|
438
440
|
## Compaction paths
|
|
@@ -60,7 +60,7 @@ Defines the persisted-data format for every Halo surface. Format changes must re
|
|
|
60
60
|
│ └── root/ # Overrides ~/.halo/global/prompts/root/ if present
|
|
61
61
|
├── sessions/<agentId>/ # Session files: <seg>.ui.jsonl, <seg>.raw.jsonl, <seg>.ui.arch.<N>.<h>.jsonl.gz
|
|
62
62
|
├── memory/ # Project memory (dated entries)
|
|
63
|
-
├── canvas/self.html # Agent's visual face — platform-owned, force-copied
|
|
63
|
+
├── canvas/self.html # Agent's visual face — platform-owned, force-copied whenever ensureWorkspaceHalo runs (see below; design/express-self.md)
|
|
64
64
|
├── goal/<goalId>/ # Retained legacy goal-mode artifacts (GOAL_SPEC.md + decision-<n>.md); existing goals unchanged
|
|
65
65
|
├── logs/
|
|
66
66
|
├── evo/ # Self-evolution per-workspace artifacts
|
|
@@ -80,7 +80,7 @@ Defines the persisted-data format for every Halo surface. Format changes must re
|
|
|
80
80
|
└── docs/ # Project docs (requirements/design/dev/test/plans)
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
**When `.halo/` is created.** `ensureWorkspaceHalo()` (`init.ts`) creates `sessions/ agents/ skills/ logs/ memory/ canvas/ tmp/ evo/{runs,applies,history}` and copies in `canvas/self.html`. It runs each time a directory is opened as a workspace: `POST /api/fs/workspace/resolve` (the admin folder picker), channel account binding, `/workspace switch`,
|
|
83
|
+
**When `.halo/` is created.** `ensureWorkspaceHalo()` (`init.ts`) creates `sessions/ agents/ skills/ logs/ memory/ canvas/ tmp/ evo/{runs,applies,history}` and copies in `canvas/self.html`. It runs each time a directory is opened as a workspace: `POST /api/fs/workspace/resolve` (the admin folder picker), channel account binding and every inbound channel message (`resolveAccountWorkspace`), `/workspace switch`, every `getWorkspaceDb()` call, and `SessionManagerRegistry.getOrCreate()` on a cache miss only (a hit returns the manager built, and `.halo/` scaffolded, earlier in this process). `halo.db` appears the first time the workspace db is opened. `runtime.lock` appears when the server first claims the runtime. Read-only list routes (`GET /api/sessions/logs`, `GET /api/agent-configs`) call `hasWorkspaceHalo()` first. For a directory with no `.halo/` they return an empty or global-only list, so a list request never turns a directory into a workspace. The cron form's path field used to scaffold every real directory the user typed through. Show routes use `registry.peek()` for the same reason (see [halo-city.md](halo-city.md)). Any new read-only route that takes a workspace path should do the same.
|
|
84
84
|
|
|
85
85
|
Precedence: workspace > global, **at folder granularity**. For the same id, a workspace `agents/<id>/` or `skills/<id>/` folder entirely replaces the global one — every file in it, no per-file fallback to global (a workspace agent folder with only `AGENT.md` loses the global `agent.yaml`). Same whole-folder rule for `prompts/{bootstrap,all,root}/` (workspace scope directory replaces the global one). INSTRUCTIONS.md is the single-file exception: workspace root suppresses global.
|
|
86
86
|
|
|
@@ -149,6 +149,7 @@ export class MyProviderAgent extends AgentLoop {
|
|
|
149
149
|
- **Tool results format.** Anthropic stores tool results as `{ role: 'user', content: [{ type: 'tool_result', ... }] }`. OpenAI-compatible APIs need separate `{ role: 'tool', tool_call_id, content }` messages, and tool-result images ride in a user message right after them. `toChatMessages()` in openai-chat-format.ts does both.
|
|
150
150
|
- **Thinking/reasoning replay.** If the provider returns `reasoning_content` and requires it in follow-up messages (Kimi, DeepSeek with tool calls), it must be stored in `result.assistantBlocks` as a `{ type: 'thinking', thinking: '...' }` block and replayed as `reasoning_content` — `chatCompletionResult()` stores it and `toChatMessages()` replays it.
|
|
151
151
|
- **Reasoning field name varies.** OpenAI o-series/DeepSeek emit reasoning in `message.reasoning_content`; Ollama / llama.cpp's OpenAI-compat layer uses `message.reasoning`. The shared stream accumulator (`ChatCompletionAccumulator` in `openai-chat-stream.ts`) folds both into one reasoning stream (`reasoning_content ?? reasoning`), so every `fetchChatCompletionStream` provider gets it. A vendor-specific class should read whichever its API uses.
|
|
152
|
+
- **The stream must carry a `finish_reason`.** `ChatCompletionAccumulator.finish()` treats a stream that ends without one as cut off: it throws `OpenAIStreamTruncated`, which the turn retries with the network backoff (a usage chunk or `[DONE]` alone doesn't count as the end). Every built-in OpenAI-family provider sends it on the last choices chunk (live-probed 2026-10-17); probe a new endpoint for it before onboarding — one that never sends it would retry every turn and then fail.
|
|
152
153
|
- **Malformed tool arguments.** Parse tool-call arguments with `parseToolInput(json, toolName)` (`tool-input.ts`) — every built-in path does (Anthropic stream accumulator, `chatCompletionResult`, Mantle). It returns `{ input, error? }` and never throws: empty / missing arguments are `{}` with no error; arguments that aren't valid JSON or aren't a plain JSON object (`null`, an array, a scalar) become `{}` plus an `error` and a `[ToolInput]` warn (tool name and the first 200 chars). Pass the error through as `inputError` on the `toolCalls[]` entry (keep `input: {}` in `assistantBlocks`) — the loop then skips that call and returns an `is_error` result asking the model to resend complete JSON, instead of running the tool with `{}`. A `max_tokens` stop never runs any of the reply's tool calls; the loop answers each with an `is_error` result itself.
|
|
153
154
|
- **Cancellation.** The caller passes `signal`; pass it to `fetch()` directly.
|
|
154
155
|
- **Max tokens.** Use `config.maxTokens ?? resolveMaxOutputTokens(config.modelId)` for consistency.
|
package/bundled-docs/dev/api.md
CHANGED
|
@@ -210,7 +210,7 @@ No `/search` endpoint: the cron form takes the WeCom `chatId` verbatim (a `useri
|
|
|
210
210
|
|
|
211
211
|
## Cron
|
|
212
212
|
|
|
213
|
-
File: `packages/server/src/routes/cron.ts`. Admin cookie auth. REST CRUD over `cron_jobs` + read-only `cron_runs`. Mutations call `
|
|
213
|
+
File: `packages/server/src/routes/cron.ts`. Admin cookie auth. REST CRUD over `cron_jobs` + read-only `cron_runs`. Mutations call `scheduleJob()` / `unscheduleJob()` (`POST /cron/reload` calls `reloadAll()`) so the runner's in-memory schedules — its own fire timers (`armTimer` / `onTimer`), with croner only computing `nextRun` from a paused instance — stay in sync with the db. Each mutation also broadcasts `cron:job_changed`.
|
|
214
214
|
|
|
215
215
|
### Jobs
|
|
216
216
|
|
|
@@ -304,6 +304,8 @@ This installs the `halo` binary on `$PATH`. Subcommands available:
|
|
|
304
304
|
1. `halo upgrade` — bumps the on-disk npm package
|
|
305
305
|
2. `halo server restart` (under a user or system unit alike; `sudo systemctl restart halo` also works for a system unit) — server's startup check sees `~/.halo/global/.template-version` is behind the new bundled `TEMPLATE_VERSION`, runs `ensureHaloHome` automatically, then starts (`halo cli` / `halo tui` run the same check on start, so a CLI-only install refreshes without a server). Refreshes `docs/`, built-in agents, built-in skills, system prompts, and the model registry. User-owned files (USER.md, custom agents/skills, INSTRUCTIONS.md overrides) are left alone. See `init.ts` for the per-category overwrite policy.
|
|
306
306
|
|
|
307
|
+
**1.x → 2.0** (new session store): stop the server → `halo sessions migrate v1 v2 -w <ws>` once per workspace → start it — see [guide/cli.md → Session store migration](../guide/cli.md#session-store-migration-halo-sessions-migrate-v1-v2). On prod, 30 workspaces / 1171 sessions migrated in ~93 s, ~1.6 min of downtime in total. First run `find <ws>/.halo -not -user <service user>`: a root-owned file there fails the migration with `EACCES` (agentcore-workspace did in the 2.0 prod upgrade).
|
|
308
|
+
|
|
307
309
|
### Public release command reference
|
|
308
310
|
|
|
309
311
|
Use only for a new public release; the retained details here are not extra preparation for installing an existing version.
|
package/dist/index.js
CHANGED
|
@@ -2959,7 +2959,7 @@ function classifyKind(msg, errName, httpStatus) {
|
|
|
2959
2959
|
if (errName === "InternalServerException" || errName === "ModelTimeoutException" || errName === "ModelStreamErrorException" || errName === "ServiceUnavailableException" || httpStatus === 500 || httpStatus === 502 || httpStatus === 503 || httpStatus === 504 || httpStatus === 529 || httpStatus === 408 || streamCode === "server_error" || streamCode === "internal_server_error" || streamCode === "service_unavailable") {
|
|
2960
2960
|
return "server_error";
|
|
2961
2961
|
}
|
|
2962
|
-
if (msg === "fetch failed" || msg === "Model request timed out" || msg.includes("http2 request did not get a response") || msg.includes("AnthropicStreamTruncated") || msg.includes("UND_ERR_HEADERS_TIMEOUT") || msg.includes("HeadersTimeoutError") || msg.includes("socket hang up") || msg.includes("ECONNRESET") || msg.includes("ECONNREFUSED") || msg.includes("ETIMEDOUT") || msg.includes("EAI_AGAIN")) {
|
|
2962
|
+
if (msg === "fetch failed" || msg === "Model request timed out" || msg.includes("http2 request did not get a response") || msg.includes("AnthropicStreamTruncated") || msg.includes("OpenAIStreamTruncated") || msg.includes("UND_ERR_HEADERS_TIMEOUT") || msg.includes("HeadersTimeoutError") || msg.includes("socket hang up") || msg.includes("ECONNRESET") || msg.includes("ECONNREFUSED") || msg.includes("ETIMEDOUT") || msg.includes("EAI_AGAIN")) {
|
|
2963
2963
|
return "network";
|
|
2964
2964
|
}
|
|
2965
2965
|
if (msg.includes("MantleEmptyResponse")) {
|
|
@@ -11447,6 +11447,12 @@ var init_session_ui_store = __esm({
|
|
|
11447
11447
|
return false;
|
|
11448
11448
|
return logWrittenElsewhere(this.uiLogFile(rootId, rootId)) ?? !this.uiStateDirty.has(rootId);
|
|
11449
11449
|
}
|
|
11450
|
+
/** Whether the root's UI log is still the file this process last wrote /
|
|
11451
|
+
* read (one stat): a flush of its pending writes appends to its own file.
|
|
11452
|
+
* False when another process wrote it or there is no baseline to tell. */
|
|
11453
|
+
uiLogIsOwn(rootId) {
|
|
11454
|
+
return logWrittenElsewhere(this.uiLogFile(rootId, rootId)) === false;
|
|
11455
|
+
}
|
|
11450
11456
|
/** UI log file of `id` (the root or one of its subs) under the root state's
|
|
11451
11457
|
* project path. */
|
|
11452
11458
|
uiLogFile(rootId, id) {
|
|
@@ -12160,12 +12166,13 @@ async function fetchChatCompletionStream(opts) {
|
|
|
12160
12166
|
throw new DOMException("Model call aborted", "AbortError");
|
|
12161
12167
|
return acc.finish();
|
|
12162
12168
|
}
|
|
12163
|
-
var ChatCompletionAccumulator;
|
|
12169
|
+
var OPENAI_STREAM_TRUNCATED, ChatCompletionAccumulator;
|
|
12164
12170
|
var init_openai_chat_stream = __esm({
|
|
12165
12171
|
"../server/dist/agents/openai-chat-stream.js"() {
|
|
12166
12172
|
"use strict";
|
|
12167
12173
|
init_agent_loop();
|
|
12168
12174
|
init_sse();
|
|
12175
|
+
OPENAI_STREAM_TRUNCATED = "OpenAIStreamTruncated: stream ended without a finish_reason";
|
|
12169
12176
|
ChatCompletionAccumulator = class {
|
|
12170
12177
|
startTime;
|
|
12171
12178
|
onDelta;
|
|
@@ -12214,8 +12221,14 @@ var init_openai_chat_stream = __esm({
|
|
|
12214
12221
|
call.function.arguments += tc.function?.arguments ?? "";
|
|
12215
12222
|
}
|
|
12216
12223
|
}
|
|
12217
|
-
/** Build the agent-facing result. Call once, after the stream has ended
|
|
12224
|
+
/** Build the agent-facing result. Call once, after the stream has ended
|
|
12225
|
+
* (the caller checks its abort signal first, so an abort stays AbortError). */
|
|
12218
12226
|
finish() {
|
|
12227
|
+
if (this.finishReason === void 0)
|
|
12228
|
+
throw new Error(OPENAI_STREAM_TRUNCATED);
|
|
12229
|
+
if (this.finishReason === "insufficient_system_resource" || this.finishReason === "aborted") {
|
|
12230
|
+
throw new Error(`OpenAIStreamTruncated: finish_reason ${this.finishReason}`);
|
|
12231
|
+
}
|
|
12219
12232
|
return {
|
|
12220
12233
|
message: {
|
|
12221
12234
|
...this.content ? { content: this.content } : {},
|
|
@@ -15442,7 +15455,7 @@ async function initObservability(haloProcess = "server") {
|
|
|
15442
15455
|
return true;
|
|
15443
15456
|
}
|
|
15444
15457
|
const { registerSdk: registerSdk2 } = await Promise.resolve().then(() => (init_otel_sdk(), otel_sdk_exports));
|
|
15445
|
-
providers = await registerSdk2(process.env.OTEL_SERVICE_NAME, "2.0.
|
|
15458
|
+
providers = await registerSdk2(process.env.OTEL_SERVICE_NAME, "2.0.1", haloProcess);
|
|
15446
15459
|
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})`);
|
|
15447
15460
|
return true;
|
|
15448
15461
|
}
|
|
@@ -16640,12 +16653,14 @@ var init_session_manager = __esm({
|
|
|
16640
16653
|
* (canDropTree): live state can't be discarded, and the two writers race —
|
|
16641
16654
|
* the accepted cost of sharing a session.
|
|
16642
16655
|
*
|
|
16643
|
-
* The UIState's pending debounced writes are cancelled, not flushed
|
|
16644
|
-
*
|
|
16645
|
-
*
|
|
16656
|
+
* The UIState's pending debounced writes are cancelled, not flushed, when
|
|
16657
|
+
* the UI log itself is no longer this process's (uiLogIsOwn): a flush
|
|
16658
|
+
* would rewrite the other writer's file whole from the stale state just
|
|
16659
|
+
* before the re-read. What they carried is made again after it (a
|
|
16646
16660
|
* restore's `context` row) or not yet in memory (the user row at turn
|
|
16647
16661
|
* start); a notice from the last ≤500ms is lost — the other writer wrote
|
|
16648
|
-
* in that window.
|
|
16662
|
+
* in that window. Another process's raw-only write leaves the UI file
|
|
16663
|
+
* ours: the writes land as usual. */
|
|
16649
16664
|
forgetExternalWrite(rootId) {
|
|
16650
16665
|
if (!this.canDropTree(rootId))
|
|
16651
16666
|
return false;
|
|
@@ -16655,9 +16670,17 @@ var init_session_manager = __esm({
|
|
|
16655
16670
|
this.stateStore.forgetRowWrites(id);
|
|
16656
16671
|
}
|
|
16657
16672
|
}
|
|
16658
|
-
this.
|
|
16673
|
+
this.dropStaleUIState(rootId);
|
|
16659
16674
|
return true;
|
|
16660
16675
|
}
|
|
16676
|
+
/** Drop the root's cached UIState for a re-read: its pending writes are
|
|
16677
|
+
* flushed when the UI log is still this process's file, else cancelled
|
|
16678
|
+
* (see forgetExternalWrite). */
|
|
16679
|
+
dropStaleUIState(rootId) {
|
|
16680
|
+
if (!this.uiStore.getCachedUIState(rootId))
|
|
16681
|
+
return;
|
|
16682
|
+
this.uiStore.dropUIState(rootId, { discard: !this.uiStore.uiLogIsOwn(rootId) });
|
|
16683
|
+
}
|
|
16661
16684
|
/** The tree's in-memory copies may be dropped: no turn / compact in flight
|
|
16662
16685
|
* and no queued message — one enqueued as a turn ended waits on the idle
|
|
16663
16686
|
* session for the next turn, and a drop would lose it. */
|
|
@@ -17474,14 +17497,15 @@ ${q.text}` : q.text}`
|
|
|
17474
17497
|
async querySession(targetSessionId, sourceSessionId, message, interrupt = false) {
|
|
17475
17498
|
console.debug(`[SessionManager] querySession: ${sourceSessionId} \u2192 ${targetSessionId} (interrupt=${interrupt}) \u2014 message: ${message.slice(0, 150)}`);
|
|
17476
17499
|
let target;
|
|
17500
|
+
const cached2 = this.sessions.get(targetSessionId);
|
|
17477
17501
|
try {
|
|
17478
17502
|
target = await this.ensureSession(targetSessionId);
|
|
17479
17503
|
} catch {
|
|
17480
17504
|
return JSON.stringify({ code: 1, error: `session ${targetSessionId} not found` });
|
|
17481
17505
|
}
|
|
17482
|
-
if (target.parentId === null && this.canDropTree(targetSessionId) && this.cachedCopiesWrittenElsewhere(targetSessionId)) {
|
|
17506
|
+
if (target === cached2 && target.parentId === null && this.canDropTree(targetSessionId) && this.cachedCopiesWrittenElsewhere(targetSessionId)) {
|
|
17483
17507
|
console.debug(`[SessionManager] querySession: ${targetSessionId} written by another process since cached \u2014 re-reading from disk`);
|
|
17484
|
-
this.
|
|
17508
|
+
this.dropStaleUIState(targetSessionId);
|
|
17485
17509
|
}
|
|
17486
17510
|
const agentQueued = target.messageQueue.filter((q) => q.sourceSessionId !== void 0).length;
|
|
17487
17511
|
if (!interrupt && target.promise !== null && agentQueued >= config.session.maxQueueSize) {
|
|
@@ -30540,7 +30564,7 @@ function buildCard(workspace, interfaceUrl, caps) {
|
|
|
30540
30564
|
return {
|
|
30541
30565
|
name: raw.name,
|
|
30542
30566
|
description: raw.description,
|
|
30543
|
-
version: typeof raw.version === "string" ? raw.version : "2.0.
|
|
30567
|
+
version: typeof raw.version === "string" ? raw.version : "2.0.1",
|
|
30544
30568
|
supportedInterfaces: [{ url: interfaceUrl, protocolBinding: "JSONRPC", protocolVersion: A2A_VERSION }],
|
|
30545
30569
|
capabilities: { streaming: caps.streaming, pushNotifications: true, extendedAgentCard: false },
|
|
30546
30570
|
...caps.tokenAuth === false ? {} : {
|
|
@@ -31515,6 +31539,10 @@ function sweepOrphanRuns(opts) {
|
|
|
31515
31539
|
}).where(eq16(cronRuns.id, runId)).run();
|
|
31516
31540
|
broadcast({ type: "cron:run_changed", jobId, runId, status: "failed" });
|
|
31517
31541
|
console.log(`[Cron] orphan sweep: run ${runId} (job ${jobId}) \u2014 ${disposition}`);
|
|
31542
|
+
if (job?.runAt) {
|
|
31543
|
+
db3.update(cronJobs).set({ enabled: 0, lastRunStatus: "failed", lastRunAt: now, lastRunId: runId, updatedAt: now }).where(eq16(cronJobs.id, jobId)).run();
|
|
31544
|
+
broadcast({ type: "cron:job_changed", jobId, lastRunStatus: "failed", lastRunAt: now, ...jobChangedFields(job) });
|
|
31545
|
+
}
|
|
31518
31546
|
if (!needsKill || pid == null) {
|
|
31519
31547
|
release();
|
|
31520
31548
|
continue;
|
|
@@ -31623,6 +31651,8 @@ function scheduleJob(jobId) {
|
|
|
31623
31651
|
try {
|
|
31624
31652
|
if (job.runAt) {
|
|
31625
31653
|
if (job.runAt <= Date.now()) {
|
|
31654
|
+
if (_inflight.has(jobId))
|
|
31655
|
+
return;
|
|
31626
31656
|
const now = Date.now();
|
|
31627
31657
|
console.warn(`[Cron] ${jobId} runAt was ${new Date(job.runAt).toISOString()} (past); marking missed`);
|
|
31628
31658
|
db3.update(cronJobs).set({ enabled: 0, lastRunStatus: "missed", lastRunAt: now, updatedAt: now }).where(eq16(cronJobs.id, jobId)).run();
|
|
@@ -31641,7 +31671,9 @@ function scheduleJob(jobId) {
|
|
|
31641
31671
|
target = cron.nextRun();
|
|
31642
31672
|
}
|
|
31643
31673
|
} catch (err) {
|
|
31644
|
-
|
|
31674
|
+
_expired.add(jobId);
|
|
31675
|
+
_fingerprint.set(jobId, jobFingerprint(job));
|
|
31676
|
+
console.warn(`[Cron] ${jobId} schedule invalid (${job.schedule}${job.timezone ? `, tz ${job.timezone}` : ""}): ${err instanceof Error ? err.message : String(err)}`);
|
|
31645
31677
|
return;
|
|
31646
31678
|
}
|
|
31647
31679
|
const entry = { jobId, cron, timer: null };
|
|
@@ -38843,7 +38875,7 @@ var init_dist2 = __esm({
|
|
|
38843
38875
|
init_git_ssh();
|
|
38844
38876
|
__dirname3 = path88.dirname(fileURLToPath3(import.meta.url));
|
|
38845
38877
|
PORT = config.server.port;
|
|
38846
|
-
HALO_VERSION = "2.0.
|
|
38878
|
+
HALO_VERSION = "2.0.1";
|
|
38847
38879
|
PROJECT_ROOT = findProjectRoot();
|
|
38848
38880
|
HALO_HOME4 = path88.join(homedir25(), ".halo");
|
|
38849
38881
|
GIT_SHA = readGitSha();
|
|
@@ -40453,7 +40485,7 @@ function installUserUnit(opts, run = runCommand) {
|
|
|
40453
40485
|
}
|
|
40454
40486
|
|
|
40455
40487
|
// src/index.ts
|
|
40456
|
-
var VERSION = "2.0.
|
|
40488
|
+
var VERSION = "2.0.1";
|
|
40457
40489
|
var HELP_TOP = `Usage: halo <command> [options]
|
|
40458
40490
|
|
|
40459
40491
|
Commands:
|
package/package.json
CHANGED
/package/admin-out/_next/static/{snq8MT7CRlizWQcyu-Kt4 → 52ayDC9phpvb-95S7qHHq}/_buildManifest.js
RENAMED
|
File without changes
|
/package/admin-out/_next/static/{snq8MT7CRlizWQcyu-Kt4 → 52ayDC9phpvb-95S7qHHq}/_ssgManifest.js
RENAMED
|
File without changes
|