@turmind/halo 2.2.5 → 2.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/admin-out/404.html +1 -1
- package/admin-out/_next/static/chunks/1989-3ab059aa2c368f46.js +1 -0
- package/admin-out/_next/static/chunks/app/page-99eba94ecd0f8e81.js +1 -0
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +4 -4
- package/bundled-docs/design/agent.md +0 -1
- package/bundled-docs/design/cron.md +2 -2
- package/bundled-docs/design/feishu.md +10 -0
- package/bundled-docs/design/stt-local.md +1 -1
- package/bundled-docs/dev/api.md +1 -1
- package/bundled-docs/dev/deploy.md +26 -1
- package/bundled-docs/dev/desktop-packaging.md +3 -2
- package/bundled-docs/dev/env.md +0 -1
- package/bundled-docs/guide/cli.md +1 -1
- package/bundled-docs/requirements/explorer.md +10 -3
- package/dist/feishu-sdk.cjs +23128 -0
- package/dist/feishu-sdk.licenses.txt +1465 -0
- package/dist/index.js +97 -29
- package/package.json +1 -2
- package/admin-out/_next/static/chunks/1989-a9d3b40139c1e9a7.js +0 -1
- package/admin-out/_next/static/chunks/app/page-7c5bdfc234c8ac16.js +0 -1
- /package/admin-out/_next/static/{1969BAiACyey4NBiI4j44 → 6I6qmeIcucaJczGu5oP3C}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{1969BAiACyey4NBiI4j44 → 6I6qmeIcucaJczGu5oP3C}/_ssgManifest.js +0 -0
package/admin-out/index.html
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<!DOCTYPE html><!--
|
|
1
|
+
<!DOCTYPE html><!--6I6qmeIcucaJczGu5oP3C--><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/7eb37d975d4027da.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-1970c5140e4019b7.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-3ab059aa2c368f46.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/4320-0fb65ed376b666a8.js" async=""></script><script src="/_next/static/chunks/app/page-99eba94ecd0f8e81.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-1970c5140e4019b7.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-3ab059aa2c368f46.js\",\"7177\",\"static/chunks/app/layout-0ed29966e0abc26b.js\"],\"ThemeProvider\"]\n3:I[24554,[\"1989\",\"static/chunks/1989-3ab059aa2c368f46.js\",\"7177\",\"static/chunks/app/layout-0ed29966e0abc26b.js\"],\"AppI18nProvider\"]\n4:I[63693,[],\"\"]\n5:I[74713,[],\"\"]\n6:I[5444,[],\"ClientPageRoot\"]\n7:I[62368,[\"4560\",\"static/chunks/1d2d5650-c677408087984c7a.js\",\"4320\",\"static/chunks/4320-0fb65ed376b666a8.js\",\"1989\",\"static/chunks/1989-3ab059aa2c368f46.js\",\"8974\",\"static/chunks/app/page-99eba94ecd0f8e81.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/7eb37d975d4027da.css\",\"style\"]\n:HL[\"/_next/static/css/659eccb5db697b76.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"6I6qmeIcucaJczGu5oP3C\",\"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/7eb37d975d4027da.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
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
1:"$Sreact.fragment"
|
|
2
|
-
2:I[58321,["1989","static/chunks/1989-
|
|
3
|
-
3:I[24554,["1989","static/chunks/1989-
|
|
2
|
+
2:I[58321,["1989","static/chunks/1989-3ab059aa2c368f46.js","7177","static/chunks/app/layout-0ed29966e0abc26b.js"],"ThemeProvider"]
|
|
3
|
+
3:I[24554,["1989","static/chunks/1989-3ab059aa2c368f46.js","7177","static/chunks/app/layout-0ed29966e0abc26b.js"],"AppI18nProvider"]
|
|
4
4
|
4:I[63693,[],""]
|
|
5
5
|
5:I[74713,[],""]
|
|
6
6
|
6:I[5444,[],"ClientPageRoot"]
|
|
7
|
-
7:I[62368,["4560","static/chunks/1d2d5650-c677408087984c7a.js","4320","static/chunks/4320-0fb65ed376b666a8.js","1989","static/chunks/1989-
|
|
7
|
+
7:I[62368,["4560","static/chunks/1d2d5650-c677408087984c7a.js","4320","static/chunks/4320-0fb65ed376b666a8.js","1989","static/chunks/1989-3ab059aa2c368f46.js","8974","static/chunks/app/page-99eba94ecd0f8e81.js"],"default"]
|
|
8
8
|
a:I[23632,[],"OutletBoundary"]
|
|
9
9
|
c:I[86889,[],"AsyncMetadataOutlet"]
|
|
10
10
|
e:I[23632,[],"ViewportBoundary"]
|
|
@@ -15,7 +15,7 @@ e:I[23632,[],"ViewportBoundary"]
|
|
|
15
15
|
:HL["/_next/static/css/7b736202f0b1dbfd.css","style"]
|
|
16
16
|
:HL["/_next/static/css/7eb37d975d4027da.css","style"]
|
|
17
17
|
:HL["/_next/static/css/659eccb5db697b76.css","style"]
|
|
18
|
-
0:{"P":null,"b":"
|
|
18
|
+
0:{"P":null,"b":"6I6qmeIcucaJczGu5oP3C","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/7eb37d975d4027da.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"}]]
|
|
@@ -158,7 +158,6 @@ Source: `packages/server/src/config.ts`
|
|
|
158
158
|
| `model.maxContextTokens` | 272,000 | Global-default context window (env: `HALO_MAX_CONTEXT_TOKENS`) — the last tier of the resolution chain below |
|
|
159
159
|
| `model.compressAt` | 0.9 | Auto-compact threshold (90%) |
|
|
160
160
|
| `agent.maxRetries` | 5 | Max retry count (settings: `general.agent.max_retries`) |
|
|
161
|
-
| `session.maxCachedSessions` | 50 | In-memory session cache (env: `HALO_MAX_CACHED_SESSIONS`) |
|
|
162
161
|
| `session.maxQueueSize` | 256 | inter-session message queue cap (settings: `general.session.max_queue_size`) |
|
|
163
162
|
| `session.maxNestingDepth` | 16 | Max session nesting depth (settings: `general.session.max_nesting_depth`) |
|
|
164
163
|
|
|
@@ -77,7 +77,7 @@ A fire runs **in-process**: the prompt goes into the job's session on the server
|
|
|
77
77
|
The run's pointer is a `ReplyTo` branch next to relay / A2A / invocation (`agents/relay.ts`). relay reaches the runner through `CronRunDoor` (`setCronRunDoor({ owns, finish, cancel, released })`, set when `runner.ts` loads — relay can't import the runner, which imports relay); the CLI / TUI never load the runner, so there the door is null and a `{cron}` pointer is left as is.
|
|
78
78
|
|
|
79
79
|
- **`deliverRelayReport`** (the session's tree went quiet): `owns(runId)` = that row is `running` in **this process's** `cron.db` — a DEV server sharing workspace dbs with prod, or an AgentCore process, doesn't own it and leaves the pointer (`left for its owner`). Otherwise `finishRun`. `deliverRelayInterim` sends nothing for `{cron}` — final only.
|
|
80
|
-
- **`finishRun`** — db-driven (run row, job row, targets read now; the `RunCtx` only adds notices, interrupted replies and the timer, so a run resumed after a restart finishes the same way). Synchronous up to the dispatch: `_finishing` claim, untrack, own pointer cleared. Text = what `halo cli` printed: root notices as `ℹ️ <n>` lines (not a sub-session's, not the drain-time sibling-status line, not an out-of-band `notifySystem` one — the evo ticker's auto-apply line landing mid-run is marked `outOfBand` and stays in chat only), a blank line, then the reply
|
|
80
|
+
- **`finishRun`** — db-driven (run row, job row, targets read now; the `RunCtx` only adds notices, interrupted replies and the timer, so a run resumed after a restart finishes the same way). Synchronous up to the dispatch: `_finishing` claim, untrack, own pointer cleared. The reply = `finalOutput || output`, after a webhook run's replies to turns its events cut short (`answers`, see below). Text — what the run row's `output` stores — = what `halo cli` printed: root notices as `ℹ️ <n>` lines (not a sub-session's, not the drain-time sibling-status line, not an out-of-band `notifySystem` one — the evo ticker's auto-apply line landing mid-run is marked `outOfBand` and stays in chat only), a blank line, then the reply. Only the reply is dispatched: the notices stay in the run record and never reach a target. `turnError` → `failed` (500 chars; a refusal on a stateful job appends a hint — delete the job's own session, or for a picked session pick another / clear the pick, or turn on "new session every run" — the refusal sticks to that history); no reply (empty, or notices alone) → `failed` `run produced no output (nothing to dispatch)`, nothing dispatched, the notices still recorded in `output`; else `succeeded`, then `dispatchToTargets(reply, targets, run workspace)` — every target failing downgrades to `failed`. `finalize` is guarded (`WHERE status='running'`): timeout, stop, finish and supersede race, the first wins. `test/cron-cli-parity.test.ts` replays each run's event stream through the cli's `runCli` and asserts the same recorded text, and that a notice is not dispatched.
|
|
81
81
|
- **Self-evolution**: a turn under a `{cron}` pointer never enqueues a pre-compact evo run (`enqueueEvoForCompactedTurn`; [evolution.md](evolution.md#pre-compact-hook)). The evo ticker's auto-apply line (`notifySystem`, `outOfBand`) never joins a run's notices.
|
|
82
82
|
- **Stop / delete / archive** of the session here: `cancelOwedForSession(db, sid, 'stop'|'delete'|'archive')` (the renamed `cancelA2AForSession`, same 4 call sites in `session-manager.ts`) → `cancelRun`: `failed` `session stopped/deleted/archived before the run finished`, nothing dispatched — a Stop no longer ships the partial output.
|
|
83
83
|
- **Supersede**: `takeReplyTo` over a `{cron}` pointer (a relay / A2A taking the session) → the run `failed` `superseded by …`, the new pointer stays.
|
|
@@ -112,7 +112,7 @@ The third trigger mode (`CRON_MIGRATIONS` slot 6, v7): `cron_jobs.webhook_secret
|
|
|
112
112
|
|
|
113
113
|
**Event text** — `[Webhook event · <type> · delivery <id> · received <iso>]`, then a line saying the payload is data, not instructions, then the payload. Over 32 KB the payload is written to `<ws>/.halo/tmp/webhooks/<jobId>/<eventId>.txt` and only its first 4 KB inlined; the UI row is cut at 2000 chars.
|
|
114
114
|
|
|
115
|
-
**Interrupted replies** (`RunCtx.answers`). An event injected mid-run starts a new turn, and `finishRun` reads the last turn only, so the reply to the earlier events would be lost. The listener keeps the final text of a turn an injected event cut short (`queued_message` after an inject — the batch-boundary `complete` drainQueue emits between merged turns is not an idle point and is skipped), and of a turn that ended while the root idled waiting on sub-agents (`injectEvent` saves it); the run's
|
|
115
|
+
**Interrupted replies** (`RunCtx.answers`). An event injected mid-run starts a new turn, and `finishRun` reads the last turn only, so the reply to the earlier events would be lost. The listener keeps the final text of a turn an injected event cut short (`queued_message` after an inject — the batch-boundary `complete` drainQueue emits between merged turns is not an idle point and is skipped), and of a turn that ended while the root idled waiting on sub-agents (`injectEvent` saves it); the run's reply is those joined with `\n\n`, then the last reply. Sub-agent reports alone don't keep anything — the last wrap-up is the answer, as in the cli.
|
|
116
116
|
|
|
117
117
|
**Consumed?** — a finished run marks its events `done` (succeeded) or `failed`. After a restart (`recoverInProcessRun`), an `injected` event whose stamp isn't in the session's raw history (`SessionManager.rawHasStamp` — a queued message dies with the process) is injected again, prefixed `[redelivered after restart]`. At-least-once: a compaction since can also drop a stamp.
|
|
118
118
|
|
|
@@ -16,6 +16,15 @@ Halo server (9527) ──┤├── channels/wechat/ ├── Se
|
|
|
16
16
|
|
|
17
17
|
Feishu uses the official SDK's `Lark.WSClient` to maintain a persistent WebSocket connection for event delivery. Unlike Telegram (long-polling), Feishu's long-connect receives `im.message.receive_v1` events pushed by the server over wss.
|
|
18
18
|
|
|
19
|
+
### SDK loading
|
|
20
|
+
|
|
21
|
+
The SDK (`@larksuiteoapi/node-sdk`) costs about 24 MB of heap once loaded, so it loads on the first account connect, never at startup. `channels/feishu/lark-sdk.ts` is the only module importing its values (it re-exports `WSClient` / `EventDispatcher` / `LoggerLevel`); `handler.ts` only imports its types statically and `await import('./lark-sdk.js')` inside `connect()`. A server with no enabled Feishu account never loads it.
|
|
22
|
+
|
|
23
|
+
- **Source / desktop runs** (tsc output + `node_modules`): the SDK is a normal dependency of `@turmind/halo-server` and `lark-sdk.js` resolves it from there.
|
|
24
|
+
- **npm package**: the SDK is not a dependency of `@turmind/halo`. `packages/cli/scripts/build-bundle.mjs` bundles `lark-sdk.js` with the SDK's own deps (axios, protobufjs, lodash.*, qs; `ws` stays external) into `dist/feishu-sdk.cjs` (~0.7 MB, tree-shaken from the SDK's ESM build down to the three exports), and rewrites the main bundle's import of `lark-sdk.js` to that file. The rewrite is needed because the main bundle has no code splitting: a dynamic import of a bundled module gets inlined into `index.js`, and the SDK source would be on the heap at startup again. The build fails if the SDK ends up inlined. The packages' license texts ship beside it as `dist/feishu-sdk.licenses.txt`.
|
|
25
|
+
- A new SDK export `handler.ts` needs must be added to `lark-sdk.ts`; anything else of the SDK is not in the npm build.
|
|
26
|
+
- The SDK takes its HTTP `User-Agent` version (`oapi-node-sdk/<version>`) from its own `package.json`, which the npm build doesn't ship. The build pins the version read from that file into the SDK's `getSdkVersion()`, and fails if that function can't be found.
|
|
27
|
+
|
|
19
28
|
## Data model
|
|
20
29
|
|
|
21
30
|
### Workspace ↔ Bot mapping
|
|
@@ -71,6 +80,7 @@ Files: `packages/server/src/channels/feishu/`
|
|
|
71
80
|
- `types.ts` — FeishuAccount, FeishuEventEnvelope, FeishuMessageEvent interfaces
|
|
72
81
|
- `accounts.ts` — DAL (listAccounts / getAccount / insertAccount / updateAccount / deleteAccount / findAccountByAppId)
|
|
73
82
|
- `handler.ts` — Lark.WSClient setup, event dispatch, message handling, mention detection, media ingestion; session routing + listener/route bookkeeping come from `channels/shared/inbound.ts` (`InboundBridge` / `deliverInbound` / `dispatchChannelCommand`)
|
|
83
|
+
- `lark-sdk.ts` — the only module importing the SDK's values; `handler.ts` `import()`s it on first connect ([SDK loading](#sdk-loading))
|
|
74
84
|
- `event-adapter.ts` — AgentSessionEvent stream → coalesced Feishu message replies (buffer + flush at paragraph boundaries)
|
|
75
85
|
- `api.ts` — HTTP client (tenant token caching / getBotInfo / sendMessage / replyMessage / uploadImage / uploadFile / downloadResource / decryptWebhookBody / searchFeishuTargets / openLongConnection); authenticated calls go through `fetchWithTenantToken` (401 → drop the cached token, retry once) and replies are parsed by `readFeishuJson`
|
|
76
86
|
- `cron-dispatcher.ts` — registers the cron dispatcher, requires explicit chatId targets
|
|
@@ -60,7 +60,7 @@ missing ──ensure()──▶ downloading{received,total} ──all files ok
|
|
|
60
60
|
## Engine
|
|
61
61
|
|
|
62
62
|
- `sherpa-onnx-node` is `await import()`ed on first `load()`. The CJS package's ESM namespace only has `OnlineRecognizer` + `default`, so `OfflineRecognizer` / `Vad` come off `default`. A failure (unsupported platform, missing native package) is a `SttError('engine')` on that stream and is retried on the next stream. It can never fail server boot.
|
|
63
|
-
- **One recognizer**, created on first use: `numThreads = clamp(floor(os.availableParallelism() / 2), 1, 4)`, `provider: 'cpu'`, `language: ''`, `useInverseTextNormalization: 1`. Load takes ~0.9 s. **It stays resident, about 400 MB RSS** (temp server: 472 MB total after a session). There is no idle release: dropping the recognizer + `global.gc()` for 20 s gave back only 13–119 MB of the ~325 MB it added (varied run to run), and reloading climbs straight back to ~400 MB. sherpa has no explicit free, and the onnxruntime / glibc arenas don't return to the OS.
|
|
63
|
+
- **One recognizer**, created on first use: `numThreads = clamp(floor(os.availableParallelism() / 2), 1, 4)`, `provider: 'cpu'`, `language: ''`, `useInverseTextNormalization: 1`. Load takes ~0.9 s. **It stays resident, about 400 MB RSS** (temp server: 472 MB total after a session). There is no idle release: dropping the recognizer + `global.gc()` for 20 s gave back only 13–119 MB of the ~325 MB it added (varied run to run), and reloading climbs straight back to ~400 MB. sherpa has no explicit free, and the onnxruntime / glibc arenas don't return to the OS. After the load, each decoded segment adds about 0.14 MB that is never returned: a standalone engine was at 471 MB after 10 segments and 486 MB after 120, still growing linearly (2.2.5, 2026-11). A long-running server that has used STT therefore sits several hundred MB above a fresh one; deploy.md's [Memory and swap](../dev/deploy.md#memory-and-swap) has the sizing.
|
|
64
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.
|
|
65
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.
|
|
66
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.
|
package/bundled-docs/dev/api.md
CHANGED
|
@@ -36,7 +36,7 @@ File: `packages/server/src/routes/files.ts`
|
|
|
36
36
|
| GET | `/api/files?path=&projectId=` | Read file content (max 10 MB) |
|
|
37
37
|
| GET | `/api/files/stat?path=&projectId=` | Lightweight mtime + size |
|
|
38
38
|
| GET | `/api/files/download?path=&projectId=&inline=` | Download or inline-preview — streams the file (no full read into memory), supports `Range` (206 Partial Content) so `<video>`/`<audio>` can seek + partial-load; aborts the read if the client disconnects. A **relative** `path` is confined to the workspace (`..` out of it → 403). An **absolute** `path` may be any file on the host, under `/api/fs/raw`'s rules: no `..` segment (→ 400), judged by realpath, the sandbox hidden lists answer like a missing file; admin cookie only (this is how chat `MEDIA:` chips load files from other workspaces / temp dirs). `projectId` is still required. Missing or hidden file → 404 `{error:'File not found'}`; a directory → 400 |
|
|
39
|
-
| GET | `/api/files/download-folder?path
|
|
39
|
+
| GET | `/api/files/download-folder?path=[&path=…]&projectId=[&include=excluded][&estimate=1]` | Folder → one zip, built by yazl while it streams (zip64, UTF-8 names; already-compressed extensions stored, the rest deflated; one file open at a time, paced by backpressure; never staged on disk or in memory). `path` must be a directory inside the project, `''` = workspace root; it may itself be a folder symlink inside the workspace (resolved, then checked like any target); no `/tmp` exception. `Content-Disposition: attachment` with RFC 5987 `filename*` = `<folder name>.zip` (root → workspace dir name); no `Content-Length`, no Range. **`path` repeated** (Explorer multi-select, `planSelectionZip`; a single `path` behaves exactly as above): each may be a file or a folder and must pass the same in-project check; one failure fails the request (403 traversal / 404 project, plus 403 when the realpath is inside but the given path is not, e.g. an outside link pointing in; a missing item → 404). Duplicates and items under another selected folder are dropped. Entries are relative to the nearest common parent of the remaining items, and the file is `<parent name>-selection.zip` (parent = root → workspace dir name). Every selected item gets the walk's per-entry checks (below): a selected `.env` / blocked file, an unsafe name, a folder link or an outside file link is excluded and counted, not refused. A selected blocked folder is excluded the same way, not a 403. A selected `IGNORED_SEGMENTS` folder is walked, since it was picked explicitly. The 413 limits count the whole selection. The server sets no URL-length cap of its own: the admin dialog refuses a selection whose URL passes 8,000 chars without sending a request. **Excluded by default**: any *folder* whose name is in `IGNORED_SEGMENTS` (`ws/file-watcher.ts`); `include=excluded` brings them back. **Always excluded**: `isHiddenHostPath` / `isHiddenWorkspacePath` (also against a nested `.halo/`), plus common credential stores (~/.kube, cloud CLI configs, shell histories — a zip-only list in `folder-zip.ts`, matched relative to the home dir), `.halo/settings.yaml`, `.env` / `.env.*`. Names a zip can't hold — any containing `\`, or a top-level `X:` prefix — are skipped as `unreadable`. Directory symlinks never followed; file symlinks only when the realpath stays inside the workspace and passes those checks. `estimate=1` → `{files, bytes, excluded: {<reason>: n}, ignoredNames: {<folder>: n}}` with reasons `ignored` / `blocked` / `symlink` / `unreadable`, same rules, no content read. More than 200,000 files or a walk over 10 s → **413** `{error}` (both modes, before any header). A blocked target → 403. Anything left out → `_halo-excluded.txt` at the zip root (grouped by reason, a pruned dir listed once, 1000 paths per reason + a count). Mid-stream (status already sent): a file that fails to open is skipped, a read error partway through truncates that entry — both are listed there. A client disconnect stops the walk and destroys the zip stream |
|
|
40
40
|
| PUT | `/api/files` | Save file (body: `{path, content, projectId}`) |
|
|
41
41
|
| PUT | `/api/files/raw?path=&projectId=[&expectMtime=][&create=1][&append=1][&root=]` | Replace an **existing** file's bytes with the raw request body (binary-safe; `PUT /api/files` is utf-8 text only). Used by canvas preview extensions' save path. 404 if the file doesn't exist (save never creates) unless `create=1`; 400 for a directory. `expectMtime` is the `modifiedAt` the client loaded from `/api/files/stat` (float ms — compared after `Math.round`): when the on-disk mtime differs → **409** `{error:'conflict', mtime, size}` so the client can offer overwrite (retry without / with the new `expectMtime`). `create=1` creates a missing file (parent dirs `mkdir -p`); `append=1` appends instead of overwriting (`expectMtime` ignored; with `create=1` a missing file is created first) — the bundle extensions' `fs` write / append. `root=<dir>` (workspace-relative; the bundle host always sends its bundle path): `<dir>` must be an existing directory and `path` strictly inside it, else **404** `{error:'Bundle root not found'}` with nothing created. Success → `{ok, path, mtime, size}` |
|
|
42
42
|
| POST | `/api/files/new` | Create empty file |
|
|
@@ -53,6 +53,31 @@ Nginx is **not required**. Use it only for domain routing, SSL termination, or w
|
|
|
53
53
|
- pnpm 11 — source builds only (pinned by the root `packageManager`; `corepack enable` picks it up)
|
|
54
54
|
- AWS credentials — optional, only for the Bedrock providers (`~/.aws/credentials`, env vars, or an instance role)
|
|
55
55
|
- bubblewrap (Linux) — needed for the `workspace` / `readonly` access levels: `sudo apt install bubblewrap`; on Ubuntu 23.10+ / 24.04 also the [AppArmor profile](#bubblewrap-on-ubuntu-2310-and-2404-apparmor)
|
|
56
|
+
- RAM — 1 GB recommended; a 0.5 GB box only with a 1 GB swap file persisted in `/etc/fstab` ([Memory and swap](#memory-and-swap)). Disk: about 290 MB for the install, plus about 450 MB of npm cache
|
|
57
|
+
|
|
58
|
+
### Memory and swap
|
|
59
|
+
|
|
60
|
+
Measured on Halo 2.2.5, fresh Ubuntu 24.04 arm64 EC2 (t4g.nano 0.5 GB, t4g.micro 1 GB), 2026-11:
|
|
61
|
+
|
|
62
|
+
| What | RAM |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `npm install -g @turmind/halo` | peak ≈ 540 MB (dependency resolution) |
|
|
65
|
+
| Server, idle | ≈ 160 MB (≈ 250 MB for a few seconds at startup) |
|
|
66
|
+
| Server, typical use (admin, editor, terminal, two parallel sub-sessions) | ≈ 210 MB |
|
|
67
|
+
| Each `halo` CLI call (`--version`, `server status`, the `server start -d` launcher) | ≈ 170 MB more while it runs |
|
|
68
|
+
| Local speech-to-text (voice input, meeting recorder), once used | ≈ 400 MB more, resident until restart; ≈ 100 MB more with speaker labels ([stt-local.md](../design/stt-local.md#engine)) |
|
|
69
|
+
|
|
70
|
+
Agent `shell_exec` work (builds, scripts) comes on top of these.
|
|
71
|
+
|
|
72
|
+
- **1 GB (t4g.micro)**: the recommended minimum. Install and typical use passed with no swap and at least 390 MB still available. With local speech-to-text, add swap or use 2 GB.
|
|
73
|
+
- **0.5 GB (t4g.nano)**: only with a 1 GB swap file. Without it, `apt-get install nodejs` and `npm install -g` are OOM-killed, and an installed server is killed by any ~60 MB transient (a `halo` CLI call, the SSM agent's document worker). With swap everything ran at micro's speed, except the first page load / editor open after idle (+0.7–1 s). Fit for a single-user trial, not a deployment.
|
|
74
|
+
- CPU is not the limit: idle is ≈ 0.03 % of one vCPU; startup costs 3–4 s of CPU once.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
sudo fallocate -l 1G /swapfile && sudo chmod 600 /swapfile
|
|
78
|
+
sudo mkswap /swapfile && sudo swapon /swapfile
|
|
79
|
+
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab # without this line swap is gone after a reboot
|
|
80
|
+
```
|
|
56
81
|
|
|
57
82
|
### bubblewrap on Ubuntu 23.10 and 24.04 (AppArmor)
|
|
58
83
|
|
|
@@ -327,7 +352,7 @@ Use only for a new public release; the retained details here are not extra prepa
|
|
|
327
352
|
|
|
328
353
|
**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
354
|
|
|
330
|
-
The published package contains a single bundled JS entry (`dist/index.js`, ~1.7 MB) plus the evo wrapper the server spawns as its own process (`dist/evo-wrapper.js`, ~130 KB; `build-bundle.mjs` fails without it), all built-in templates (agents / skills / prompts / models), bundled platform docs, and the admin Web UI static export. Total install footprint is about
|
|
355
|
+
The published package contains a single bundled JS entry (`dist/index.js`, ~1.7 MB) plus the evo wrapper the server spawns as its own process (`dist/evo-wrapper.js`, ~130 KB; `build-bundle.mjs` fails without it), the Feishu SDK bundled with its deps (`dist/feishu-sdk.cjs`, ~0.7 MB, loaded on the first Feishu bot connect, so it is not an npm dependency; see [feishu.md](../design/feishu.md#sdk-loading)), all built-in templates (agents / skills / prompts / models), bundled platform docs, and the admin Web UI static export. Total install footprint is about 290 MB on disk (`du`, x86_64, after 2.2.5; mostly `node_modules`; 2.2.5 itself is about 320 MB); npm's own cache adds about 450 MB.
|
|
331
356
|
|
|
332
357
|
Two log files: `~/.halo/logs/server.log` is the stdout/stderr capture of `halo server start -d`, whether the user unit or the off-systemd supervisor runs it (what `halo server logs` tails; the supervisor's `[respawn]` lines appear only off systemd; a system unit logs to the journal instead); `~/.halo/global/logs/server.log` is the server's structured, rotated log (level `general.logging.level`, default `warn`).
|
|
333
358
|
|
|
@@ -159,8 +159,9 @@ dependency work. Three levers, in order of how often they help:
|
|
|
159
159
|
generation, and Gatekeeper scanning the ~350 MB of `extraResources`. Neither
|
|
160
160
|
fast-staging nor `npmRebuild:false` touches that; it runs on every build.
|
|
161
161
|
A further win would be de-duplicating `cli-runtime` (~137 MB) against
|
|
162
|
-
`server-runtime` (~92 MB) — they share most deps (
|
|
163
|
-
|
|
162
|
+
`server-runtime` (~92 MB) — they share most deps (drizzle, better-sqlite3,
|
|
163
|
+
aws-sdk; the Feishu SDK only sits in server-runtime's `node_modules`, the cli
|
|
164
|
+
bundle carries it as `dist/feishu-sdk.cjs`) — but that needs the two trees to physically share
|
|
164
165
|
files without breaking cli's standalone require paths; deferred.
|
|
165
166
|
|
|
166
167
|
## Why `CI=true` is mandatory
|
package/bundled-docs/dev/env.md
CHANGED
|
@@ -106,7 +106,6 @@ Source: `packages/server/src/config.ts`
|
|
|
106
106
|
| `HALO_WEB_FETCH_TIMEOUT` | `10000` | `config.yaml timeout.web_fetch` | web_fetch timeout |
|
|
107
107
|
| `HALO_MODEL_TIMEOUT` | `600000` | `config.yaml timeout.model_request` | Per model-call idle timeout (ms, 10 min): every provider streams (Bedrock, the Anthropic-Messages providers anthropic / mimo / minimax / qwen, the OpenAI-family openai / deepseek / kimi / zhipu / doubao / hunyuan, and Mantle aws-bedrock-mantle / aws-bedrock-openai) and re-arms it on any data the stream delivers — text / thinking deltas, pings, tool-argument fragments, empty reasoning chunks, SSE comment frames — so it fires only when the model goes silent for this long, not on a long but live answer. It is also the only bound on a compact's LLM summary (no separate compact timeout). A bare fetch has no default timeout, so a half-open connection would hang the request forever; on expiry the call aborts and the agent loop retries with backoff. |
|
|
108
108
|
| `HALO_SESSION_GRACE` | `300000` | `config.yaml timeout.session_grace` | WS-disconnect session grace |
|
|
109
|
-
| `HALO_MAX_CACHED_SESSIONS` | `50` | — | In-memory session cache |
|
|
110
109
|
| `HALO_LOG_MAX_SIZE` | `10485760` | `config.yaml logging.max_file_size` | Log file size cap (10 MB) |
|
|
111
110
|
| `HALO_LOG_MAX_FILES` | `5` | `config.yaml logging.max_files` | Retained rotated logs |
|
|
112
111
|
| `HALO_LOG_LEVEL` | `warn` | `settings.yaml general.logging.level` | Log level: debug \| info \| warn \| error |
|
|
@@ -77,7 +77,7 @@ To list agents or sessions without running a prompt, use the dedicated subcomman
|
|
|
77
77
|
|
|
78
78
|
- **text format**: stdout carries the agent's **final answer** — the closing text of the last turn, not the live stream. Text the agent emits before a tool call ("let me check…") and the wrap-ups of earlier turns in the same run (e.g. a director absorbing several sub-agent reports) are omitted; if the last turn had no closing text, its full text is printed instead. When stdout is a terminal the answer is rendered as styled markdown (headings, bold, code blocks, tables); when piped or redirected it is written as raw markdown so downstream tools get the text unchanged. Tool calls, thinking, usage — and, with `-v`, the agent's text as it streams — go to stderr.
|
|
79
79
|
|
|
80
|
-
System notices from the root session (retries, compaction, truncated output, a model refusal, `@scope` warnings) are printed on stdout before the answer, one `ℹ️ ` line each and a blank line between them and the answer. Notices from sub-agents and the sibling-status line added when sub-agent reports arrive are left out. Notices from earlier turns in the same run are kept, so a
|
|
80
|
+
System notices from the root session (retries, compaction, truncated output, a model refusal, `@scope` warnings) are printed on stdout before the answer, one `ℹ️ ` line each and a blank line between them and the answer. Notices from sub-agents and the sibling-status line added when sub-agent reports arrive are left out. Notices from earlier turns in the same run are kept, so a run that hit a local-fallback compact still says so in its output. (Scheduled runs record the same text in their run history but push only the answer to chat.)
|
|
81
81
|
|
|
82
82
|
Usage line mirrors the admin chat-panel badge format (timestamp / in / out / ctx / read / write / cache% / ttft / e2e / think / model). `ctx` is the rolled-up context size — `inputTokens + cacheRead + cacheWrite + outputTokens` — so it doesn't look misleading next to a large cache hit:
|
|
83
83
|
|
|
@@ -71,7 +71,7 @@ VS Code-style highlight selection (no checkboxes):
|
|
|
71
71
|
|
|
72
72
|
### Right-click menu
|
|
73
73
|
|
|
74
|
-
Items shown depend on the click target.
|
|
74
|
+
Items shown depend on the click target. Right-clicking inside a multi-selection shows "N items selected" over two items, Download N items and Delete N items. These three labels follow the UI language (zh: 已选 / 下载 / 删除 N 项); the single-item menu is English only. On the workspace root row only New File / New Folder / Open in Integrated Terminal / Reveal in File Manager / Download apply:
|
|
75
75
|
|
|
76
76
|
| Action | Shown when | API / behavior |
|
|
77
77
|
|---|---|---|
|
|
@@ -79,7 +79,7 @@ Items shown depend on the click target. With several items selected the menu off
|
|
|
79
79
|
| Open in Integrated Terminal | Always | Spawns a terminal at the target dir (or file's parent) |
|
|
80
80
|
| Reveal in File Manager | Desktop app only | Opens the OS file manager (Finder / Explorer / Linux FM) at the target — a folder opens itself, a file is highlighted in its parent dir. Hidden in a plain browser (gated on the `window.haloReveal` IPC bridge exposed by the desktop preload) |
|
|
81
81
|
| Open to the Side | File or bundle directory | Splits the editor and opens the file (or bundle) in the right pane |
|
|
82
|
-
| Download | File, folder, workspace root | File: `GET /api/files/download?path=...`. Folder / root: estimate + confirm, then one streamed zip — see [Folder download](#folder-download) |
|
|
82
|
+
| Download | File, folder, workspace root, multi-select | File: `GET /api/files/download?path=...`. Folder / root: estimate + confirm, then one streamed zip. Multi-select (files and folders mixed): the same confirm, one zip of the whole selection — see [Folder download](#folder-download) |
|
|
83
83
|
| Rename | Single file/folder | `POST /api/files/rename` |
|
|
84
84
|
| Delete | Single or multi-select | `DELETE /api/files?path=...` (with confirm) |
|
|
85
85
|
| Open as Workspace | Folder only | Switches the active workspace to that folder, reusing the path-input switch flow (validate → persist → reload). Shown at the bottom of the menu |
|
|
@@ -87,7 +87,7 @@ Items shown depend on the click target. With several items selected the menu off
|
|
|
87
87
|
The menu auto-clamps to the viewport, so right-clicking near the window edge does not clip the bottom items.
|
|
88
88
|
|
|
89
89
|
### Folder download
|
|
90
|
-
Download on a folder (or the workspace root) zips it on the server while it streams — nothing is staged on disk or in memory (`GET /api/files/download-folder`, see [dev/api.md](../dev/api.md#files)).
|
|
90
|
+
Download on a folder (or the workspace root), or Download N items on a multi-selection, zips it on the server while it streams — nothing is staged on disk or in memory (`GET /api/files/download-folder`, see [dev/api.md](../dev/api.md#files)).
|
|
91
91
|
- Clicking Download first fetches an estimate (`estimate=1`, a walk without reading contents) and opens a confirm: `≈ <size> · <n> files`, what is excluded by default with counts (node_modules / .git by name, the rest as "other build/cache folders"), how many items are always excluded or skipped
|
|
92
92
|
- Checkbox "Include node_modules, .git and other build/cache folders" (`include=excluded`) re-fetches the estimate when toggled
|
|
93
93
|
- More than 200,000 files, or a scan longer than 10 s → the dialog shows the server's 413 message ("download a subfolder instead") and Download stays disabled
|
|
@@ -97,6 +97,13 @@ Download on a folder (or the workspace root) zips it on the server while it stre
|
|
|
97
97
|
- **Names a zip can't hold** (containing `\`, or a top-level `C:`-style prefix) are skipped and listed as unreadable
|
|
98
98
|
- **Symlinks**: folder links are never followed; a file link is included only when its target stays inside the workspace and is not excluded
|
|
99
99
|
- When anything was left out, the zip carries `_halo-excluded.txt` at its root: excluded paths grouped by reason (a pruned folder is listed once, not its contents; capped at 1000 per reason plus a count), including unreadable files and skipped links
|
|
100
|
+
- **Multi-selection** (Download N items): the dialog is titled "Download N items as zip"; the estimate, checkbox, 413 handling and every rule above apply. On top of that:
|
|
101
|
+
- Zip entries are relative to the selected items' nearest common folder. Items picked in one folder appear by name (`a.txt`, `sub/…`); `a/b/x.txt` + `a/c` share `a/` and appear as `b/x.txt`, `c/…`
|
|
102
|
+
- The zip is named `<common folder>-selection.zip`; when the common folder is the workspace root, the workspace dir name is used (`<workspace dir>-selection.zip`)
|
|
103
|
+
- An item inside another selected folder (or picked twice) is counted once
|
|
104
|
+
- The 200,000-file and 10 s limits count the whole selection
|
|
105
|
+
- A selected file or link is checked as it would be inside a folder: a selected `.env`, secret, Halo runtime path or unsafe name is left out, and so is a selected folder link (never followed, unlike a single-folder download through a link) or a file link leaving the workspace. All of these are listed in `_halo-excluded.txt` and counted in the estimate. A selected always-excluded folder (e.g. `.halo/sessions`) is left out the same way rather than failing the download. A selected default-excluded folder (e.g. `node_modules`) is included, since it was picked explicitly
|
|
106
|
+
- A selection whose download link would pass 8,000 characters sends no request: the dialog shows "Too many items selected — download the parent folder instead" and Download stays disabled
|
|
100
107
|
|
|
101
108
|
### Modification indicator
|
|
102
109
|
Files with unsaved editor edits show a coloured dot in the tree (synced from `editorStore.modifiedPaths`).
|