@turmind/halo 1.5.4 → 1.5.5

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.
Files changed (75) hide show
  1. package/admin-out/404.html +1 -1
  2. package/admin-out/_next/static/chunks/{133-33aa091fba853037.js → 422-cc74b1fdd3af5415.js} +1 -1
  3. package/admin-out/_next/static/chunks/989-bc743317498d3fbc.js +1 -0
  4. package/admin-out/_next/static/chunks/app/page-0a390059f1914664.js +1 -0
  5. package/admin-out/_next/static/chunks/{webpack-1a264efd965db7ab.js → webpack-edf248e07333566d.js} +1 -1
  6. package/admin-out/_next/static/css/b53542687ebd4c65.css +3 -0
  7. package/admin-out/index.html +1 -1
  8. package/admin-out/index.txt +5 -5
  9. package/bundled-docs/design/agent.md +4 -4
  10. package/bundled-docs/design/agentcore.md +13 -10
  11. package/bundled-docs/design/architecture.md +24 -14
  12. package/bundled-docs/design/command.md +5 -5
  13. package/bundled-docs/design/cron.md +2 -2
  14. package/bundled-docs/design/evolution.md +34 -35
  15. package/bundled-docs/design/express-self.md +6 -4
  16. package/bundled-docs/design/feishu.md +18 -14
  17. package/bundled-docs/design/halo-city.md +5 -4
  18. package/bundled-docs/design/observability.md +9 -9
  19. package/bundled-docs/design/prompt-system.md +12 -12
  20. package/bundled-docs/design/session.md +12 -12
  21. package/bundled-docs/design/slack.md +22 -20
  22. package/bundled-docs/design/storage.md +18 -19
  23. package/bundled-docs/design/telegram.md +26 -8
  24. package/bundled-docs/design/web.md +3 -1
  25. package/bundled-docs/design/wechat.md +8 -6
  26. package/bundled-docs/design/wecom.md +23 -9
  27. package/bundled-docs/design/ws.md +6 -6
  28. package/bundled-docs/dev/acp-adapter.md +8 -8
  29. package/bundled-docs/dev/add-channel.md +12 -9
  30. package/bundled-docs/dev/add-model-provider.md +31 -20
  31. package/bundled-docs/dev/api.md +83 -57
  32. package/bundled-docs/dev/deploy.md +8 -6
  33. package/bundled-docs/dev/env.md +16 -11
  34. package/bundled-docs/dev/logging.md +15 -29
  35. package/bundled-docs/dev/previews.md +20 -11
  36. package/bundled-docs/dev/tools.md +24 -12
  37. package/bundled-docs/guide/README.md +1 -0
  38. package/bundled-docs/guide/agents.md +19 -53
  39. package/bundled-docs/guide/channels/README.md +7 -4
  40. package/bundled-docs/guide/channels/acp.md +3 -1
  41. package/bundled-docs/guide/channels/feishu.md +13 -11
  42. package/bundled-docs/guide/channels/slack.md +12 -10
  43. package/bundled-docs/guide/channels/telegram.md +21 -27
  44. package/bundled-docs/guide/channels/web.md +15 -11
  45. package/bundled-docs/guide/channels/wechat.md +15 -14
  46. package/bundled-docs/guide/channels/wecom.md +7 -2
  47. package/bundled-docs/guide/chat.md +10 -9
  48. package/bundled-docs/guide/cli.md +10 -8
  49. package/bundled-docs/guide/getting-started.md +3 -3
  50. package/bundled-docs/guide/secrets-and-credentials.md +3 -3
  51. package/bundled-docs/guide/sessions.md +3 -3
  52. package/bundled-docs/guide/skills.md +8 -6
  53. package/bundled-docs/guide/testing-agents-and-skills.md +14 -15
  54. package/bundled-docs/guide/workspace.md +27 -15
  55. package/bundled-docs/requirements/agent.md +19 -18
  56. package/bundled-docs/requirements/chat.md +16 -17
  57. package/bundled-docs/requirements/command.md +10 -10
  58. package/bundled-docs/requirements/editor.md +10 -9
  59. package/bundled-docs/requirements/explorer.md +10 -7
  60. package/bundled-docs/requirements/session.md +16 -17
  61. package/bundled-docs/requirements/settings.md +18 -14
  62. package/bundled-docs/requirements/skill.md +4 -4
  63. package/bundled-docs/requirements/source-control.md +4 -3
  64. package/bundled-docs/requirements/terminal.md +13 -13
  65. package/bundled-docs/requirements/workspace.md +19 -13
  66. package/dist/index.js +2038 -3558
  67. package/package.json +1 -1
  68. package/templates/config.yaml +6 -0
  69. package/templates/skills/workspace/share.md +11 -3
  70. package/templates/skills/workspace/stage.py +85 -5
  71. package/admin-out/_next/static/chunks/989-92e9683ae8d39750.js +0 -1
  72. package/admin-out/_next/static/chunks/app/page-7a635969198fc62b.js +0 -1
  73. package/admin-out/_next/static/css/4ea5f674146e54d8.css +0 -3
  74. /package/admin-out/_next/static/{lm8Bz3ugTSORyuSXfyx56 → cuVcldfdg1PXevaQMQSzb}/_buildManifest.js +0 -0
  75. /package/admin-out/_next/static/{lm8Bz3ugTSORyuSXfyx56 → cuVcldfdg1PXevaQMQSzb}/_ssgManifest.js +0 -0
@@ -105,13 +105,14 @@ Unified session log API — list + read session files across all agents.
105
105
 
106
106
  | Method | Path | Purpose |
107
107
  |---|---|---|
108
- | GET | `/api/sessions/logs?projectId=` | List session metadata, keyset-paginated. Default returns each top-level row + all descendants (sidebar tree); `rootOnly=1` returns roots only (chat-panel session tabs). Each row carries `status` (`running` also while a sub-session runs). A `projectId` with no `.halo/` (plain or missing directory) returns `{sessions: [], nextCursor: null}` without scaffolding it — a list never turns a directory into a workspace |
108
+ | GET | `/api/sessions/logs?projectId=` | List session metadata, keyset-paginated. Default returns each top-level row + all descendants (sidebar tree); `rootOnly=1` returns roots only (chat-panel session tabs). A `projectId` with no `.halo/` (plain or missing directory) returns `{sessions: [], nextCursor: null}` without scaffolding it — a list never turns a directory into a workspace |
109
109
  | GET | `/api/sessions/logs/:id?projectId=` | Full session log (scans across agent dirs) — the **active** file only; archived history is a separate call |
110
110
  | GET | `/api/sessions/logs/:id/archive/:n?projectId=` | One archived UI-log segment — see [detail](#get-apisessionslogsidarchivenprojectidabs) |
111
- | DELETE | `/api/sessions/logs/:id?projectId=` | Delete the session log (and all of its archive segments) |
112
- | PATCH | `/api/sessions/logs/:id?projectId=` | Rename a session (admin-only) — updates the log file's title and the mirrored `agent_sessions.title`. Accepts any session id (root or sub-agent); the sidebar exposes the rename affordance on every row |
111
+ | DELETE | `/api/sessions/logs/:id?projectId=` | Permanently delete the session and all its descendants — log files (with archive segments) and `agent_sessions` rows. Returns `{ok, deleted}` |
112
+ | PATCH | `/api/sessions/logs/:id?projectId=` | Rename a session (admin-only; body `{title}`) — updates the log file's title and the mirrored `agent_sessions.title`. Accepts any session id (root or sub-agent); the sidebar exposes the rename affordance on every row |
113
+ | GET | `/api/sessions/goal?projectId=` | Legacy goal binding of the workspace (`{goal: {goalSessionId, workerSessionId, status, round, maxRounds} \| null}`); refresh seed for the goal banner, which only shows when `general.goal_mode_enabled` is on |
113
114
 
114
- The list endpoint returns flat metadata (id / agentId / agentName / title / timestamps / exchangeCount / parentSessionId / stoppedAt / contextTokens / totalOutputTokens / goalSessionId — the last is a retained legacy field, non-null only on a goal-bound worker row; existing bindings are unchanged), served from the mirrored `agent_sessions` columns rather than by parsing each session file (rows predating the columns are backfilled from the file on first read). The frontend builds the tree from `parentSessionId`.
115
+ The list endpoint returns flat metadata (id / agentId / agentName / title / timestamps / exchangeCount / parentSessionId / stoppedAt / archivedAt / contextTokens / totalOutputTokens / status / goalSessionId — the last is a retained legacy field, non-null only on a goal-bound worker row; existing bindings are unchanged), served from the mirrored `agent_sessions` columns rather than by parsing each session file (rows predating the columns are backfilled from the file on first read). The frontend builds the tree from `parentSessionId`.
115
116
 
116
117
  The get endpoint returns the full session file. If only `rawMessages` is present (no event-log `messages`), `convertRawMessages()` transforms it into display format on the fly.
117
118
 
@@ -261,8 +262,8 @@ Every write except `/api/git/init` is gated on the project being a git work-tree
261
262
  | GET | `/api/git/credentials` | List stored HTTPS credentials — `{credentials: [{host, username}]}`. **Never returns the token.** Source of truth is `~/.git-credentials` (one line per host) |
262
263
  | POST | `/api/git/credentials` | Body `{host, username, token}`. Upserts the `https://user:token@host` line in `~/.git-credentials` (0600). 400 if any field missing |
263
264
  | DELETE | `/api/git/credentials/:host` | Remove the credential line(s) for one host (idempotent). `:host` is sent `encodeURIComponent`'d; Hono decodes it |
264
- | GET | `/api/git/ssh/keys` | Private keys found in `~/.ssh` + an `encrypted` flag each. Never returns key contents. Returns `{keys: [...]}` |
265
- | GET | `/api/git/ssh/agent` | ssh-agent reachability + loaded key count |
265
+ | GET | `/api/git/ssh/keys` | Private keys found in `~/.ssh` + an `encrypted` flag each. Never returns key contents. Returns `{keys: [{name, path, encrypted}]}` |
266
+ | GET | `/api/git/ssh/agent` | ssh-agent reachability + loaded keys. Returns `{agentRunning, loadedKeys: string[]}` (`ssh-add -l` lines) |
266
267
  | POST | `/api/git/ssh/unlock` | Body `{keyPath, passphrase}`. Loads a passphrase-protected key into the shared ssh-agent. `keyPath` must resolve to a file **directly inside `~/.ssh`** (rejects traversal / arbitrary paths, 400). Passphrase is fed to `ssh-add` via a throwaway `SSH_ASKPASS` helper (never argv / disk / log); the helper is answer-once so a wrong passphrase returns immediately. Returns `{ok}` on success, `{ok:false, error}` (normalized — never ssh-add's raw stderr) on failure |
267
268
  | GET | `/api/git/remote/protocol?projectId=` | Current `origin` url + detected protocol. Returns `{url, protocol}` |
268
269
  | POST | `/api/git/remote/protocol` | Body `{projectId, to: 'https'\|'ssh'}`. Rewrites `origin` between HTTPS and scp-style SSH. Returns `{ok, url}` |
@@ -301,10 +302,10 @@ gets JSON, and additionally gates on `accessLevel`.
301
302
 
302
303
  | Method | Path | Purpose |
303
304
  |---|---|---|
304
- | POST | `/api/web/chat` | Send message, receive SSE stream. Body: `{message, images?, workspace?, sessionId?, agentId?}` (overrides also accepted as `?workspace=`/`?sessionId=` query or `x-workspace`/`x-session-id` headers; `workspace` only honored when token has `accessLevel: full`). `sessionId` must be owned by the token unless `accessLevel: full` → otherwise 403. |
305
+ | POST | `/api/web/chat` | Send message, receive SSE stream. Body: `{message, images?, workspace?, sessionId?, agentId?}` (overrides also accepted as `?workspace=`/`?sessionId=` query or `x-workspace`/`x-session-id` headers; `workspace` only honored when token has `accessLevel: full`). `sessionId` must be a safe id segment (else 400) and owned by the token unless `accessLevel: full` → otherwise 403. A client that disconnects ends its stream right away; the turn keeps running (reconnect via `/subscribe`, stop via `/stop`). |
305
306
  | POST | `/api/web/sessions` | Mint a root session in the token's own `web_<accountId>_` namespace → `{sessionId}`. Body / query / header: `workspace?` (full tokens only), `agentId?`. Used by the ACP adapter's `session/new`. |
306
307
  | POST | `/api/web/stop` | Stop running task → `{stopped: boolean}` |
307
- | GET | `/api/web/history` | Active session history → `{sessionId, messages[], running}` |
308
+ | GET | `/api/web/history` | Active session history → `{sessionId, messages[], running}`. With an explicit `sessionId` that doesn't exist in the workspace → 404 `{error: "session not found"}` |
308
309
  | GET | `/api/web/subscribe` | Reconnect SSE to running session |
309
310
  | GET | `/api/web/file?path=` | Inline-serve a workspace-relative file (image / video / pdf etc.). Path-traversal-checked against the token's bound workspace: the lexical check is re-verified against the realpath'd root, so a symlink inside the workspace pointing outside it returns 403; a dangling symlink (target doesn't exist) returns 404. Workspace runtime state (`.halo/sessions`, `.halo/logs`, `.halo/evo`, `halo.db*`) is hidden for every access level → 403 `{error: "path is not accessible"}`, checked on the realpath so symlinks into those dirs are caught too. |
310
311
 
@@ -321,7 +322,7 @@ runtime so the frontend can render rooms (workspaces) + characters (sessions).
321
322
  | Method | Path | Purpose |
322
323
  |---|---|---|
323
324
  | GET | `/api/show/state` | `full` or `observer` token → every known workspace; otherwise the account's own. Returns `{ serverTime, uptime, accessLevel, skills[], workspaces[] }` |
324
- | GET | `/api/show/session?ws=&id=` | Inspector-panel detail for a single session. Trimmed message log (last 40, content/tool I/O capped) plus `contextTokens` / `outputTokens` / `maxContextTokens` / `isRunning`. Non-`full`/`observer` tokens may only read their own workspace, and within it only sessions their own account minted (`web_<accountId>_*`); anything else is 403. |
325
+ | GET | `/api/show/session?ws=&id=` | Inspector-panel detail for a single session. Trimmed message log (last 40, content/tool I/O capped) plus `totalMessages` / `contextTokens` / `outputTokens` / `maxContextTokens` / `isRunning`. Non-`full`/`observer` tokens may only read their own workspace, and within it only sessions their own account minted (`web_<accountId>_*`); anything else is 403. |
325
326
 
326
327
  `observer` is more than an aggregate-counts role: past `/show/state`, it can also call `/show/session` to read **any workspace's** session transcript (last 40 messages, content truncated to 600 chars, tool input to 200 chars). Mint it knowing it grants cross-workspace transcript read access, not just dashboard counters.
327
328
 
@@ -339,20 +340,20 @@ File: `packages/server/src/routes/agent-configs.ts`
339
340
  | GET | `/api/agent-configs/tools` | Available workspace tools |
340
341
  | GET | `/api/agent-configs/models` | Available model providers and models |
341
342
  | POST | `/api/agent-configs` | Create a new agent |
342
- | GET | `/api/agent-configs/:id/yaml?scope=&projectId=` | Read agent.yaml |
343
- | PUT | `/api/agent-configs/:id/yaml?scope=&projectId=` | Write agent.yaml |
344
- | DELETE | `/api/agent-configs/:id?scope=&projectId=` | Delete agent (global has "last one" protection) |
343
+ | GET | `/api/agent-configs/:id/yaml?scope=&projectId=` | Read agent.yaml. `scope=workspace` without `projectId` → 400; `scope` other than `global` / `workspace` (omitted = `global`) → 400 `Invalid scope` |
344
+ | PUT | `/api/agent-configs/:id/yaml?scope=&projectId=` | Write agent.yaml. Same 400s as GET |
345
+ | DELETE | `/api/agent-configs/:id?scope=&projectId=` | Delete agent (global has "last one" protection). Same 400s as GET — never falls back to the same-named global agent or skips the last-one guard |
345
346
  | PATCH | `/api/agent-configs/:id/toggle?scope=&projectId=` | Toggle disabled in workspace DB. `projectId` required. Returns `{ ok, disabled }`. |
346
347
  | GET | `/api/agent-configs/:id/md/:fileType` | Read an MD file (AGENT.md / INSTRUCTIONS.md / INDEX.md) |
347
- | PUT | `/api/agent-configs/:id/md/:fileType` | Write an MD file (AGENT.md / INSTRUCTIONS.md) |
348
- | GET | `/api/agent-configs/:id/md-all` | Read every MD at once |
348
+ | PUT | `/api/agent-configs/:id/md/:fileType` | Write an MD file (AGENT.md / INSTRUCTIONS.md). `scope=workspace` without `projectId` → 400 (never writes the global AGENT.md / INSTRUCTIONS.md) |
349
+ | GET | `/api/agent-configs/:id/md-all` | Read every MD at once. `scope=workspace` without `projectId` → 400 |
349
350
  | GET | `/api/agent-configs/:id/sessions?projectId=` | List the agent's session files |
350
351
  | GET | `/api/agent-configs/:id/sessions/:sessionId` | Read a session |
351
352
  | POST | `/api/agent-configs/:id/sessions` | Save / update a session |
352
353
  | DELETE | `/api/agent-configs/:id/sessions/:sessionId` | Delete a session |
353
354
  | DELETE | `/api/agent-configs/:id/sessions?all=1` | Delete every session for the agent |
354
355
 
355
- Every `:id` / `:sessionId` route param that becomes a path segment (here and in `DELETE /api/skills/:id`) is validated with `isSafeIdSegment()` (`routes/workspace-path.ts`) before it reaches `path.join` — `.` / `..` rejected outright, everything else must match `/^[\w.:>\u4e00-\u9fff-]+$/` (the union of the real id charsets: slug + CJK agent names, `__internal__`, and session ids embedding `_ - : . >`). Hono decodes `%2F` / `%2e` into params, so a raw `..%2F..%2Fetc` would otherwise arrive as a traversal. A rejected param is **400** (`Invalid agent id` / `Invalid skill id` / `Invalid session id`), distinct from the **404** a well-formed-but-missing session file gets.
356
+ Every `:id` / `:sessionId` route param that becomes a path segment (here and in `DELETE /api/skills/:id`) is validated with `isSafeIdSegment()` (`routes/workspace-path.ts`) before it reaches `path.join` — `.` / `..` rejected outright, everything else must match `/^[\w.:>\u4e00-\u9fff-]+$/` (the union of the real id charsets: slug + CJK agent names, `__internal__`, and session ids embedding `_ - : . >`). Hono decodes `%2F` / `%2e` into params, so a raw `..%2F..%2Fetc` would otherwise arrive as a traversal. A rejected param is **400** (`Invalid agent id` / `Invalid skill id` / `Invalid session id`, or `Invalid id` on the session read / delete routes), distinct from the **404** a well-formed-but-missing session file gets.
356
357
 
357
358
  ## Skills
358
359
 
@@ -409,18 +410,18 @@ In `PUBLIC_PATHS` (no admin cookie). Auth is a web-channel `x-token` (header or
409
410
 
410
411
  ## Self-Evolution
411
412
 
412
- File: `packages/server/src/routes/evolution.ts`. Surfaces the global `evolution_runs` / `evolution_applies` queues to the admin UI's Evolution tab; see [plans/self-evolution.md](../plans/self-evolution.md) for the full design.
413
+ File: `packages/server/src/routes/evolution.ts`. Surfaces the global `evolution_runs` / `evolution_applies` queues to the admin UI's Evolution tab; see [design/evolution.md](../design/evolution.md) for the full design. Unknown run id → 404 `{error: "not found"}`.
413
414
 
414
415
  | Method | Path | Purpose |
415
416
  |---|---|---|
416
- | GET | `/api/evolution/runs` | List all evolution runs across workspaces. Carries the latest `apply_id`/`apply_status` per run. (Score isn't surfaced in the list — it's read from `score.json` on the detail fetch only.) |
417
+ | GET | `/api/evolution/runs?archived=0\|1&limit=20&before=<createdAt>` | List evolution runs across workspaces, newest first, cursor-paginated (`limit` 1–300, `before` = previous page's `nextCursor`; `archived=1` lists archived runs instead). Returns `{runs, hasMore, nextCursor}`; each row carries the latest `applyId`/`applyStatus`. (Score isn't surfaced in the list — it's read from `score.json` on the detail fetch only.) |
417
418
  | GET | `/api/evolution/runs/:id` | Detail: db row + `patch.md` + `score.json` + `.skip.md` (when `status='skipped'`) + wrapper/sub-cli logs + a snapshot summary (first user message + first assistant reply + message count). Row carries `failureReason` (and `applyFailureReason` from the latest apply) — apply-side values include `phase A': apply cli exited <code>`, `phase A': apply agent aborted: <first line of ABORT.md>`, and `phase A': apply agent didn't produce apply.log`. |
418
- | POST | `/api/evolution/runs/:id/approve` `{reviewerHint?}` | Move run from `awaiting_review` → `approved`, insert a pending `evolution_applies` row that the ticker will pick up. |
419
- | POST | `/api/evolution/runs/:id/reject` | Move run from `awaiting_review` → `rejected`. |
420
- | POST | `/api/evolution/runs/:id/retry` `{hint}` | Reset a finished run back to `pending` with the supplied (required) hint so the wrapper picks it up again. Rejected with 409 when the run is already `running` / `pending`. |
421
- | POST | `/api/evolution/runs/:id/hint` `{hint}` | Append text to `user_hint` — memo only, doesn't change status. |
422
- | DELETE | `/api/evolution/runs/:id` | Delete a finished run: its on-disk artifacts (run dir + archive zip) **and** the DB row. Rejected with 409 for in-flight states (`pending` / `running` / `approved`) so a live wrapper / queued apply isn't pulled out from under. Broadcasts `evolution:run_changed` with `kind:'deleted'`. |
423
- | GET | `/api/evolution/applies` | List apply rows (used for status badges). |
419
+ | POST | `/api/evolution/runs/:id/approve` `{reviewerHint?}` | Move run from `awaiting_review` → `approved`, insert a pending `evolution_applies` row that the ticker will pick up. Returns `{ok, applyId}`; 409 when the run isn't `awaiting_review`. |
420
+ | POST | `/api/evolution/runs/:id/reject` | Move run from `awaiting_review` → `rejected`; 409 otherwise. |
421
+ | POST | `/api/evolution/runs/:id/retry` `{hint}` | Reset a finished run back to `pending` (attempts cleared) with the supplied hint (required, else 400) so the wrapper picks it up again. Rejected with 409 when the run is already `running` / `pending`. |
422
+ | POST | `/api/evolution/runs/:id/hint` `{hint}` | Append text to `user_hint` (hint required, else 400) — memo only, doesn't change status. Returns `{ok, userHint}`. |
423
+ | DELETE | `/api/evolution/runs/:id` | Delete a finished run: its on-disk artifacts (run dir + archive zip + wrapper log) **and** the DB row, plus every apply that references it (artifacts + row, `evolution:apply_changed` `kind:'deleted'`). Rejected with 409 for in-flight states (`pending` / `running` / `approved`) so a live wrapper / queued apply isn't pulled out from under. Broadcasts `evolution:run_changed` with `kind:'deleted'`. |
424
+ | GET | `/api/evolution/applies` | List in-flight apply rows (`pending` / `running` / `syncing`, newest 200; used for status badges). Returns `{applies}`. |
424
425
 
425
426
  ## AgentCore adapter (runtime-mode only)
426
427
 
@@ -434,7 +435,7 @@ Bodies and response shapes for the endpoints agents most commonly help users cal
434
435
 
435
436
  ### POST `/api/auth/login`
436
437
 
437
- Source: [packages/server/src/middleware/auth.ts:123-134](../../../packages/server/src/middleware/auth.ts#L123-L134)
438
+ Source: [packages/server/src/middleware/auth.ts](../../../packages/server/src/middleware/auth.ts)
438
439
 
439
440
  ```json
440
441
  // Request
@@ -492,6 +493,9 @@ Source: [packages/server/src/routes/files.ts](../../../packages/server/src/route
492
493
  ```json
493
494
  // 200
494
495
  {
496
+ "projectId": "<absPath>",
497
+ "root": "<project dir name>",
498
+ "path": "<relPath>",
495
499
  "tree": [
496
500
  { "name": "packages", "path": "packages", "type": "directory", "hasChildren": true },
497
501
  { "name": "README.md", "path": "README.md", "type": "file" }
@@ -499,7 +503,7 @@ Source: [packages/server/src/routes/files.ts](../../../packages/server/src/route
499
503
  }
500
504
  ```
501
505
 
502
- Skipped entries: dotfiles (except `.halo`), `node_modules`, `__pycache__`.
506
+ Skipped entries: `.git`, `.DS_Store`, `node_modules`, `__pycache__` (other dotfiles — `.halo`, `.gitignore`, `.env` — are listed).
503
507
 
504
508
  ### GET `/api/files?path=<rel>&projectId=<abs>`
505
509
 
@@ -507,7 +511,10 @@ Reads file content. Max 10 MB.
507
511
 
508
512
  ```json
509
513
  // 200
510
- { "content": "..." }
514
+ { "path": "...", "content": "...", "size": 123, "modifiedAt": 1714000000000, "createdAt": 1714000000000 }
515
+
516
+ // 413 over 10 MB
517
+ { "error": "File too large (max 10MB)" }
511
518
 
512
519
  // 404 if path doesn't exist
513
520
  { "error": "File not found" }
@@ -515,7 +522,7 @@ Reads file content. Max 10 MB.
515
522
 
516
523
  ### POST `/api/agent-configs`
517
524
 
518
- Source: [packages/server/src/routes/agent-configs.ts:199-255](../../../packages/server/src/routes/agent-configs.ts#L199-L255)
525
+ Source: [packages/server/src/routes/agent-configs.ts](../../../packages/server/src/routes/agent-configs.ts)
519
526
 
520
527
  ```json
521
528
  // Request
@@ -540,12 +547,18 @@ Source: [packages/server/src/routes/agent-configs.ts:199-255](../../../packages/
540
547
  "conflictScope": null // or "global" / "workspace" to warn of override
541
548
  }
542
549
 
550
+ // 400 empty id after slugging ("Invalid agent name") or workspace scope without projectId
551
+ { "error": "Invalid agent name" }
552
+
553
+ // 403 id `goal` while general.goal_mode_enabled is off
554
+ { "error": "Goal mode is disabled." }
555
+
543
556
  // 409 already exists
544
557
  { "error": "Agent already exists" }
545
558
  ```
546
559
 
547
560
  Files created:
548
- - `<agentDir>/agent.yaml` — scaffold with `SCAFFOLD_MODEL`, empty tools/skills
561
+ - `<agentDir>/agent.yaml` — scaffold whose `model` block comes from the provider named by `general.agent.default_provider` (else `aws-bedrock-claude-invoke`, else the first installed provider), empty tools/skills
549
562
  - `<agentDir>/AGENT.md` — `# <name>\n\n<description>\n`
550
563
 
551
564
  ### PUT `/api/agent-configs/:id/yaml?scope=&projectId=`
@@ -560,6 +573,12 @@ Files created:
560
573
  // 400 invalid YAML
561
574
  { "error": "Invalid YAML: <parser message>" }
562
575
 
576
+ // 400 scope=workspace without projectId
577
+ { "error": "projectId required for workspace agents" }
578
+
579
+ // 400 scope other than global / workspace
580
+ { "error": "Invalid scope" }
581
+
563
582
  // 404
564
583
  { "error": "Agent not found" }
565
584
  ```
@@ -574,8 +593,7 @@ Source: [packages/server/src/routes/skills.ts](../../../packages/server/src/rout
574
593
  "name": "Code Review",
575
594
  "description": "Review code for correctness and style",
576
595
  "scope": "workspace",
577
- "projectId": "/abs/ws",
578
- "command": "/review" // optional — registers as slash command
596
+ "projectId": "/abs/ws"
579
597
  }
580
598
 
581
599
  // 201
@@ -588,11 +606,16 @@ Source: [packages/server/src/routes/skills.ts](../../../packages/server/src/rout
588
606
  "scope": "workspace"
589
607
  }
590
608
  }
609
+
610
+ // 409 already exists
611
+ { "error": "Skill already exists" }
591
612
  ```
592
613
 
614
+ Also adds the skill's entry to the scope's `settings.yaml`.
615
+
593
616
  ### GET `/api/sessions/logs?projectId=<abs>&rootOnly=0|1&includeArchived=0|1&cursor=<ms>&limit=<n>`
594
617
 
595
- Source: [packages/server/src/routes/sessions.ts:189-258](../../../packages/server/src/routes/sessions.ts#L189-L258)
618
+ Source: [packages/server/src/routes/sessions.ts](../../../packages/server/src/routes/sessions.ts)
596
619
 
597
620
  Keyset-paginated over `updatedAt` (descending). `limit` defaults to 50 (max 500); the
598
621
  response's `nextCursor` (epoch ms of the last row's `updatedAt`, or `null` on
@@ -618,15 +641,14 @@ Two shapes, selected by `rootOnly`:
618
641
  "agentId": "default",
619
642
  "agentName": "Default",
620
643
  "title": "First user message...",
621
- "source": "explorer",
622
- "createdAt": "2026-04-30T08:00:00Z",
623
- "updatedAt": "2026-04-30T08:05:00Z",
644
+ "createdAt": 1777536000000,
645
+ "updatedAt": 1777536300000,
624
646
  "exchangeCount": 12,
625
- "parentSessionId": null,
626
647
  "contextTokens": 5975,
627
648
  "totalOutputTokens": 6058,
628
649
  "stoppedAt": null,
629
650
  "archivedAt": null,
651
+ "goalSessionId": null,
630
652
  "status": "idle"
631
653
  }
632
654
  ],
@@ -636,7 +658,7 @@ Two shapes, selected by `rootOnly`:
636
658
 
637
659
  Archived sessions are excluded by default; pass `?includeArchived=1` to include them.
638
660
 
639
- `status` is `'running' | 'idle' | 'stopped'`. A root counts as `running` while any of its sub-sessions runs. The admin's chat tab list uses it to show a running mark on a tab that hasn't loaded yet; a loaded tab follows its own stream.
661
+ `status` is `'running' | 'idle' | 'stopped'`. A root counts as `running` while any of its sub-sessions runs. Returned since 1.5.3-alpha; the current admin no longer reads it. `parentSessionId` is present only on sub-sessions (omitted for roots). Timestamps are epoch ms.
640
662
 
641
663
  Rows are served from the mirrored `agent_sessions` metadata columns (`title` / `exchange_count` / `context_tokens` / `total_output_tokens`) — no session file is opened. A row whose `exchangeCount` is `null` predates those columns: the route reads that one file once, mirrors the values back, and never pays the cost again. `exchangeCount` counts **main user turns over the session's lifetime** (kept + archived), so unlike the file's `messageCount` it doesn't shrink when a compact archives history.
642
664
 
@@ -691,7 +713,7 @@ The `n > archiveCount` check is the point, not a formality: `archiveCount` in th
691
713
 
692
714
  ### GET `/api/settings/schema?projectId=<abs>`
693
715
 
694
- Resolves declared schema (from `models/<id>.yaml` `secrets:` and `skills/<id>/config.yaml`) against current settings, returning per-field source/state and a list of orphan keys. Drives the Settings page.
716
+ Resolves declared schema (from `models/<id>.yaml` `secrets:`, `skills/<id>/config.yaml`, and global `agents/<id>/agent-config.yaml`) against current settings, returning per-field source/state and a list of orphan keys. Drives the Settings page.
695
717
 
696
718
  ```json
697
719
  // 200
@@ -699,8 +721,8 @@ Resolves declared schema (from `models/<id>.yaml` `secrets:` and `skills/<id>/co
699
721
  "scope": "global", // or "workspace" when projectId set
700
722
  "sections": [
701
723
  {
702
- "namespaceId": "aws-bedrock-claude-invoke", // 'general' | provider id | skill id
703
- "source": "provider", // 'general' | 'provider' | 'skill'
724
+ "namespaceId": "aws-bedrock-claude-invoke", // 'general' | provider id | skill id | agent id
725
+ "source": "provider", // 'general' | 'provider' | 'skill' | 'agent'
704
726
  "displayName": "AWS Bedrock Claude (Invoke API)",
705
727
  "displayName_zh": "AWS Bedrock Claude(Invoke API)",
706
728
  "description": "...",
@@ -711,7 +733,7 @@ Resolves declared schema (from `models/<id>.yaml` `secrets:` and `skills/<id>/co
711
733
  "kind": "secret", // 'param' | 'secret'
712
734
  "description": "...",
713
735
  "description_zh": "...",
714
- "default": null, // schema-default placeholder
736
+ "default": "...", // schema-default placeholder; omitted when the schema declares none
715
737
  "secret": true, // UI masks input + value
716
738
  "value": "AK****ST", // already masked when secret:true; null = unset
717
739
  "hasValue": true, // any layer has a non-empty value
@@ -744,18 +766,18 @@ Resolves declared schema (from `models/<id>.yaml` `secrets:` and `skills/<id>/co
744
766
  { "ok": true }
745
767
  ```
746
768
 
747
- PUT replaces the full scope; DELETE takes `{scope, projectId, key}` and removes the key. The Settings page uses DELETE for both Reset (current scope removed → falls back to lower scope / unset) and orphan Remove.
769
+ PUT replaces the full scope; PUT / PATCH / DELETE with `scope: "workspace"` reject a `globalOnly` key with 400 (`<key> is global-only and cannot be set per workspace`) and an unresolvable `projectId` with 404 `Project not found`. DELETE takes `{scope, projectId, key}` and removes the key. PUT / PATCH / DELETE fire the server's `onSettingsChange` listeners (e.g. the sandbox hidden-paths reload); a DELETE of an already-absent key is a no-op — `{ ok: true }`, no file write, no notify. The Settings page uses DELETE for both Reset (current scope removed → falls back to lower scope / unset) and orphan Remove.
748
770
 
749
771
  ### POST `/api/wechat/login/start`
750
772
 
751
- Source: [packages/server/src/routes/wechat.ts:39-43](../../../packages/server/src/routes/wechat.ts#L39-L43)
773
+ Source: [packages/server/src/routes/wechat.ts](../../../packages/server/src/routes/wechat.ts)
752
774
 
753
775
  ```json
754
776
  // Request
755
777
  { "sessionKey": "optional-resume-key", "force": false }
756
778
 
757
779
  // 200
758
- { "qrcodeUrl": "https://...", "sessionKey": "abc123" }
780
+ { "qrcodeUrl": "https://...", "message": "...", "sessionKey": "abc123" }
759
781
  ```
760
782
 
761
783
  ### POST `/api/wechat/login/wait`
@@ -766,7 +788,7 @@ Source: [packages/server/src/routes/wechat.ts:39-43](../../../packages/server/sr
766
788
  "sessionKey": "abc123",
767
789
  "workspacePath": "/abs/path", // required, must be absolute
768
790
  "label": "My Bot", // optional
769
- "accessLevel": "readonly", // "full" | "readonly", default readonly
791
+ "accessLevel": "readonly", // "full" | "workspace" | "readonly", default readonly
770
792
  "language": "en", // "en" | "zh", default "en" — controls system message language
771
793
  "timeoutMs": 120000 // optional, how long to wait for scan
772
794
  }
@@ -774,10 +796,11 @@ Source: [packages/server/src/routes/wechat.ts:39-43](../../../packages/server/sr
774
796
  // 200 waiting for scan (retry polling)
775
797
  { "connected": false, "message": "..." }
776
798
 
777
- // 200 connected — account inserted + long-poll started
778
- { "connected": true, "accountId": "abc-im-bot" }
799
+ // 200 connected — account inserted (or updated if it already exists) + long-poll started
800
+ { "connected": true, "accountId": "abc-im-bot", "message": "..." }
779
801
 
780
- // 400 bad input
802
+ // 400 bad input (sessionKey / workspacePath missing, relative path, bad accessLevel;
803
+ // after a successful scan also "workspace path not found")
781
804
  { "error": "workspacePath required" }
782
805
  ```
783
806
 
@@ -797,17 +820,18 @@ Full WS protocol in [design/ws.md](../design/ws.md). The four high-traffic clien
797
820
  "message": "hello",
798
821
  "agentId": "default", // optional override
799
822
  "clientMsgId": "abc123", // optional — ack/resend dedup id
823
+ "accessLevel": "workspace", // optional — "full" | "workspace" | "readonly", idle session only
800
824
  "images": [ { "data": "<base64>", "mimeType": "image/png" } ] // optional
801
825
  }
802
826
  ```
803
827
 
804
- Server behaviour ([handler.ts `handleChat`](../../../packages/server/src/ws/handler.ts#L263-L344)):
828
+ Server behaviour (`handleChat` in `ws/handler.ts`; `sessionId`, `projectId` and `message` are all required, else an `error` frame):
805
829
  - Creates / reuses a session with the specified agent
806
- - Persists pasted images to `<ws>/.halo/web/inbound/<date>/`
830
+ - Persists pasted images to `<ws>/.halo/assets/web/inbound/web/<date>/`
807
831
  - If the model does not support image input (`capabilities.image: false`), images are filtered out and a text notice is appended instead of sending to the API
808
- - Queues if busy/compacting; otherwise runs the agent turn
832
+ - Queues if busy/compacting (a `chat:queued` frame is sent only for the compacting case); otherwise runs the agent turn. `accessLevel` applies only on the idle path and is forced to `full` when the host has no OS sandbox
809
833
  - When `clientMsgId` is present, replies `{ "type": "chat:ack", "clientMsgId" }` once the message is appended to the session log; resends with an already-acked id are re-acked without re-appending (dedup — see [design/ws.md](../design/ws.md#chat-delivery-ack--resend--dedup))
810
- - Streams events back via `message`, `tool_call`, `usage`, etc.
834
+ - Streams events back via `chat:stream`, `agent:tool_call`, `chat:usage`, etc. (table below)
811
835
 
812
836
  ### `subscribe` (C→S)
813
837
 
@@ -871,13 +895,15 @@ It does not remove session files. The full delete (rows + JSON + archive segment
871
895
 
872
896
  | Type | Fields | Purpose |
873
897
  |---|---|---|
874
- | `message` | `content`, `role`, `taskId?` | Text chunk from the agent |
875
- | `tool_call` | `toolName`, `toolInput`, `taskId?` | Tool invocation card |
876
- | `tool_result` | `toolName`, `toolOutput`, `durationMs`, `taskId?` | Tool result |
877
- | `usage` | `usage`, `modelId`, `turnId`, `taskId?` | Token accounting per turn |
878
- | `complete` | `stopReason`, `taskId?` | Turn finished |
898
+ | `chat:stream` | `text`, `agentName`, `turnId`, `taskId?` | Text chunk from the agent |
899
+ | `chat:thinking` | `text`, `agentName`, `turnId`, `taskId?` | Thinking chunk |
900
+ | `agent:tool_call` | `tool`, `toolUseId`, `input`, `agentName`, `turnId`, `taskId?` | Tool invocation card |
901
+ | `agent:tool_result` | `result`, `toolUseId`, `agentName`, `durationMs`, `taskId?` | Tool result |
902
+ | `chat:usage` | `contextTokens`, `outputTokens`, `usage`, `modelId`, `turnId` | Token accounting per model call (root only) |
903
+ | `chat:complete` | `batchBoundary?` | Turn finished |
879
904
  | `error` | `error`, `sessionId?` | Error message |
880
- | `chat:queued` | `reason`, `message`, `sessionId` | Message queued (compact/busy) |
905
+ | `chat:queued` | `reason: 'compact'`, `message`, `sessionId` | Message queued behind a compact |
906
+ | `chat:ack` | `clientMsgId` | Chat appended to the session log (or re-acked on resend) |
881
907
  | `listener:released` | `sessionId` | Event listener reclaimed — re-send `subscribe` (see above) |
882
908
 
883
909
  Events with `taskId` set belong to sub-agent turns (nested sessions); events without are the root agent's. Session-scoped frames carry `sessionId` (the root session they belong to); the admin routes each one to the tab holding that session, and a frame without one goes to the tab on screen. Full list in [design/ws.md](../design/ws.md).
@@ -47,7 +47,7 @@ Nginx is **not required**. Use it only for domain routing, SSL termination, or w
47
47
  ## Prerequisites
48
48
 
49
49
  - Node.js ≥ 22
50
- - pnpm ≥ 9
50
+ - pnpm 11 (pinned by the root `packageManager`; `corepack enable` picks it up)
51
51
  - AWS credentials configured (`~/.aws/credentials` or env vars) with Bedrock access
52
52
 
53
53
  ## 1. Install dependencies and build
@@ -63,7 +63,7 @@ pnpm --filter @turmind/halo-admin build # next build + copy-monaco; never a ba
63
63
 
64
64
  ## 2. Runtime data locations
65
65
 
66
- No directory needs to be created by hand. SQLite databases are created automatically on first use: per-workspace state at `<workspace>/.halo/halo.db`, plus global queues at `~/.halo/global/evo.db`, `~/.halo/global/cron.db` and `~/.halo/global/runs.db`.
66
+ No directory needs to be created by hand. SQLite databases are created automatically on first use: per-workspace state at `<workspace>/.halo/halo.db`, plus global queues at `~/.halo/global/evo.db`, `~/.halo/global/cron.db` and `~/.halo/global/runs.db`, and channel accounts at `~/.halo/secrets/channels/channels.db`.
67
67
 
68
68
  ## 3. Run `halo setup`
69
69
 
@@ -90,7 +90,7 @@ When `HALO_PASSWORD` is set, the password chosen via `halo setup` is ignored at
90
90
 
91
91
  ## 5. Start the server
92
92
 
93
- > **单实例锁**:server 启动时会写 `~/.halo/global/server.lock`(Linux 用 flock,macOS/Windows 回退到 pid 探测),退出时自动清理。如果 lock 里记录的进程还活着,新 server 会拒绝启动并打印 `kill <pid>` 提示。原因是 WeChat 长轮询循环跟 HTTP server 解耦,多个进程并存会导致同一条微信消息被 fan out 到多个 session。陈旧 lock(进程已不在)会被自动识别并清除。要手动重启先 kill 旧的:`kill $(cat ~/.halo/global/server.lock)`。
93
+ > **单实例锁**:server 启动时会写 `~/.halo/global/server.lock`(有 `flock` 命令时用 flock,没有就回退到 pid 探测),退出时自动清理。如果 lock 里记录的进程还活着,新 server 会拒绝启动并打印 `kill <pid>` 提示。原因是 WeChat 长轮询循环跟 HTTP server 解耦,多个进程并存会导致同一条微信消息被 fan out 到多个 session。陈旧 lock(进程已不在)会被自动识别并清除。要手动重启先 kill 旧的:`kill $(cat ~/.halo/global/server.lock)`。
94
94
 
95
95
  ### Option A: quick start
96
96
 
@@ -155,8 +155,8 @@ server as its child (`packages/cli/src/server-supervisor.ts`). Policy:
155
155
  up (exit 1) with a log line telling you to fix the cause and re-run `halo
156
156
  server start -d`.
157
157
  - **2s pause between attempts**, so an instant-crash loop can't spin the CPU.
158
- - Every decision lands in the daemon log as `[respawn] …`, on the same fds the
159
- server writes to (`~/.halo/global/logs/server.log`).
158
+ - Every decision lands in the daemon log as `[respawn] …` — the stdout/stderr
159
+ capture `~/.halo/logs/server.log`, which `halo server logs` tails.
160
160
 
161
161
  `halo server stop` (and `--force`) is not mistaken for a crash: it stamps the
162
162
  server pid into a marker file `~/.halo/global/server.stop` *before* signalling,
@@ -219,6 +219,7 @@ This installs the `halo` binary on `$PATH`. Subcommands available:
219
219
  | `halo tui` | Interactive TUI client |
220
220
  | `halo cli "<prompt>"` | One-shot prompt → reply, exit |
221
221
  | `halo agents` / `halo sessions` | List agents / sessions |
222
+ | `halo acp` | Stdio bridge that lets an ACP client (e.g. Claude Code) drive a halo server — see [acp-adapter.md](acp-adapter.md) |
222
223
 
223
224
  ### Upgrade flow
224
225
 
@@ -239,7 +240,7 @@ Use only for a new public release; the retained details here are not extra prepa
239
240
 
240
241
  **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.
241
242
 
242
- The published package contains a single bundled JS entry (~620 KB), all built-in templates (agents / skills / prompts / models), bundled platform docs, and the admin Web UI static export. Total install footprint ≈ 120 MB after npm dedupes shared deps.
243
+ The published package contains a single bundled JS entry (~1.4 MB), all built-in templates (agents / skills / prompts / models), bundled platform docs, and the admin Web UI static export. Total install footprint ≈ 120 MB after npm dedupes shared deps.
243
244
 
244
245
  ### Non-interactive (Docker / CI) details
245
246
 
@@ -247,6 +248,7 @@ The published package contains a single bundled JS entry (~620 KB), all built-in
247
248
 
248
249
  - Seeds / refreshes `~/.halo/global/` from the bundled templates (per the per-category overwrite policy in `init.ts`)
249
250
  - Generates `server.jwt_secret` if missing
251
+ - Binds the built-in agents to the provider named by `HALO_DEFAULT_PROVIDER`, when set (an unknown id only warns)
250
252
  - **Does not** set a password — supply one via `HALO_PASSWORD` env
251
253
 
252
254
  Minimal Dockerfile:
@@ -51,9 +51,9 @@ nohup node dist/index.js >> /dev/null 2>&1 & # HALO_PASSWORD read from env/con
51
51
  ## Prerequisites
52
52
 
53
53
  - Node.js ≥ 22 (better-sqlite3 native binding is bound to v22)
54
- - pnpm ≥ 9
54
+ - pnpm 11 (pinned by the root `packageManager`; `corepack enable` picks it up)
55
55
  - AWS credentials configured (`~/.aws/credentials` or env vars) with Bedrock access
56
- - bubblewrap (`sudo apt install bubblewrap`) — OS-level sandbox for non-full access levels. Without it, only app-level path validation is active and `shell_exec` is blocked for non-full sessions
56
+ - bubblewrap (`sudo apt install bubblewrap`) — OS-level sandbox for non-full access levels on Linux (macOS uses the built-in Seatbelt). Without one, only app-level path validation is active and `shell_exec` is blocked for non-full sessions
57
57
 
58
58
  ### nvm PATH
59
59
 
@@ -67,7 +67,7 @@ Your deployment scripts should also prepend this line.
67
67
 
68
68
  ## Runtime directories
69
69
 
70
- - SQLite: `<workspace>/.halo/halo.db` (per-workspace, auto-created on first use) + global queues `~/.halo/global/evo.db`, `~/.halo/global/cron.db` and `~/.halo/global/runs.db`
70
+ - SQLite: `<workspace>/.halo/halo.db` (per-workspace, auto-created on first use) + global queues `~/.halo/global/evo.db`, `~/.halo/global/cron.db` and `~/.halo/global/runs.db` + channel accounts `~/.halo/secrets/channels/channels.db`
71
71
  - Session files: `.halo/sessions/{agentId}/{sessionId}.json`
72
72
  - Global config: `~/.halo/global/`
73
73
  - Per-project config: `<workspace>/.halo/`
@@ -78,14 +78,14 @@ Three config file types, precedence **env vars > config.yaml / settings.yaml > c
78
78
 
79
79
  | File | Scope | Contents |
80
80
  |---|---|---|
81
- | `~/.halo/secrets/config.yaml` | System | Port, password, CORS, timeouts, limits, logging — "infrastructure" settings |
82
- | `~/.halo/secrets/settings.yaml` | User | Model, region, session behaviour — "preferences" |
83
- | `<project>/.halo/settings.yaml` | Project | Overrides of global settings |
81
+ | `~/.halo/secrets/config.yaml` | System | Port, password, JWT secret, CORS, timeouts, log rotation — "infrastructure" settings |
82
+ | `~/.halo/secrets/settings.yaml` | User | Provider secrets, skill params, `general.*` server knobs (session, compaction, limits, sandbox, logging) — "preferences" |
83
+ | `<project>/.halo/settings.yaml` | Project | Overrides of provider / skill `params` and `secrets`; every `general.*` key is global-only (a workspace value is rejected by the API and ignored at runtime) |
84
84
 
85
85
  `init.ts` seeds these on first run with a per-category policy. Refresh trigger: `halo setup` always re-runs the seed, and the server's startup check re-runs it automatically when `~/.halo/global/.template-version` is behind the bundled `TEMPLATE_VERSION` (see `init.ts:TEMPLATE_VERSION` + `index.ts` startup block).
86
86
 
87
- - **Always overwritten** (platform-owned, refreshed when the template version moves): `~/.halo/global/{prompts,models,docs}/`, `INSTRUCTIONS.md`, the built-in agent ids (`default`, `executor`, `deep-executor`, `__evo_agent__`, `__score__`, `__apply_agent__`), built-in skill ids (`agent`, `skill`, `workspace`, `cron`, `acp`, `send-file`, `self`, `aws-knowledge`, `nova-web-search`, `halo`).
88
- - **Built-in agents** keep the user's `model:` block on overwrite — the admin UI lets users change which model an agent uses, and that choice survives upgrades.
87
+ - **Always overwritten** (platform-owned, refreshed when the template version moves): `~/.halo/global/{prompts,models,docs}/`, `INSTRUCTIONS.md`, the built-in agent ids (`default`, `executor`, `deep-executor`, `goal`, `__evo_agent__`, `__score__`, `__apply_agent__`), built-in skill ids (`agent`, `skill`, `workspace`, `cron`, `acp`, `send-file`, `aws-knowledge`, `web-search`, `self`, `halo`, `extension`).
88
+ - **Built-in agents** keep the user's `model:` and `context:` blocks on overwrite — the admin UI lets users change which model an agent uses and its context limits, and that choice survives upgrades.
89
89
  - **Optional skills** (`tavily-web-search`) install only when picked via `halo setup`; the opt-in list is `~/.halo/global/.installed-optional-skills`. Picked skills are force-overwritten alongside the always-overwritten set.
90
90
  - **`secrets/config.yaml`** is leaf-merged: existing leaf `value`s preserved, new leaves added when a server upgrade introduces them.
91
91
  - **`secrets/settings.yaml`** is created empty if missing and never touched again. Defaults live in `settings-schema.ts`.
@@ -99,7 +99,7 @@ Source: `packages/server/src/config.ts`
99
99
  | `HALO_PORT` | `9527` | `config.yaml server.port` | Hono listen port |
100
100
  | `HALO_PASSWORD` | (none) | `config.yaml server.password` (scrypt hash) | Plaintext login password. When set, takes precedence over the stored hash and bypasses scrypt entirely — intended for Docker / systemd / CI. It also satisfies the **startup gate**: the server refuses to boot with no password configured, and env plaintext counts as a first-class credential there, so an env-only deployment (`halo setup -y && HALO_PASSWORD=... halo server start`, no stored hash) boots fine. The hash is set by `halo setup` for interactive installs. |
101
101
  | `HALO_CORS_ORIGINS` | empty (reflect any origin) | `config.yaml server.cors_origins` | CORS allowlist (comma-separated). Empty = reflect any incoming Origin so credentials work cross-origin. Set explicit list to enforce strict CORS. |
102
- | `HALO_FRONTEND_DIR` | `packages/admin/out` | — | Static frontend dir (resolved as absolute path from project root) |
102
+ | `HALO_FRONTEND_DIR` | auto (bundled `admin-out`, else `packages/admin/out`) | — | Static frontend dir; resolved against the current working directory |
103
103
  | `HALO_MAX_CONTEXT_TOKENS` | `272000` | — | Model max context |
104
104
  | `HALO_SHELL_TIMEOUT` | `120000` | `config.yaml timeout.shell_exec` | Shell command timeout (ms) |
105
105
  | `HALO_WEB_FETCH_TIMEOUT` | `10000` | `config.yaml timeout.web_fetch` | web_fetch timeout |
@@ -114,6 +114,10 @@ Source: `packages/server/src/config.ts`
114
114
  | `HALO_BADGE` | (none) | — | Environment badge (e.g. `DEV`) returned by `GET /api/auth/check` on **both** the 200 and 401 branches, so the admin can brand the login screen too. The admin repaints the favicon with a badge band and prefixes the tab title, making parallel dev/prod tabs tellable apart. dev and prod serve the same static build, so this has to be a runtime signal. Unset / whitespace-only → `null` → stock branding. Because the 401 branch carries it, the value is **public to unauthenticated clients** — keep it a label (`DEV`, `staging`), never anything sensitive. |
115
115
  | `HALO_RUNTIME_MODE` | (none) | — | `agentcore` runs the server as an Amazon Bedrock AgentCore Runtime container (see [design/agentcore.md](../design/agentcore.md)) |
116
116
  | `HALO_WORKSPACE` | `process.cwd()` | — | Workspace root in agentcore mode |
117
+ | `HALO_DEFAULT_PROVIDER` | (none) | — | Read by `halo setup --non-interactive`: binds the built-in agents to this provider id (and its default model). An unknown id only prints a warning |
118
+ | `HALO_CLI` | `halo` (`halo.cmd` on Windows) | — | `halo` cli executable that cron runs and the evolution wrapper spawn — a dev override |
119
+ | `HALO_VERSION` | `dev` | — | Version string reported by `GET /api/health` and the cli; stamped by the bundle / desktop launcher, so normally not set by hand |
120
+ | `HALO_PROMPT` | (none) | — | `plain` makes `halo setup` use numbered-menu prompts instead of the raw-mode TUI |
117
121
 
118
122
  settings.yaml only (no env override):
119
123
  - `general.session.max_queue_size` (default 256)
@@ -140,7 +144,7 @@ settings.yaml only (no env override):
140
144
  When a provider manifest declares a `default: <<NAME>>` for a secret, `halo setup` offers a "Use env $NAME" action. Picking it **writes the literal `<<NAME>>` placeholder into settings.yaml**, which the standard env-var interpolation ([storage.md](../design/storage.md#env-var-interpolation-env_name)) expands against `process.env` at read time. There is no separate runtime env fallback — the placeholder in the file is the only mechanism (an unset env var leaves the literal `<<NAME>>` visible in the request, failing loudly).
141
145
 
142
146
  `<skill-id>.params.*` (referenceable from skills via `{{<skill-id>.params.<key>}}`, or short form `{{params.<key>}}` inside SKILL.md; declared in `skills/<id>/config.yaml`):
143
- - `tavily-search.params.api_key` — example only; declared by whichever skill needs it
147
+ - `tavily-web-search.params.api_key` — example only; declared by whichever skill needs it
144
148
 
145
149
  Hardcoded (config.ts, no override):
146
150
  - `auth.tokenMaxAge` — 14 days
@@ -152,7 +156,8 @@ settings.yaml-driven `general.limits.*` (no env override):
152
156
  - `general.limits.web_fetch_bytes` (default 50 KiB)
153
157
  - `general.limits.grep_default_matches` (default 50)
154
158
  - `general.limits.tool_result_render_chars` (default 8000)
155
- - `general.limits.ws_event_buffer` (default 5000)
159
+ - `general.limits.tool_result_ui_chars` (default 64 KiB)
160
+ - `general.limits.auto_report_chars` (default 8192)
156
161
  - `general.limits.terminal_scrollback_bytes` (default 50000)
157
162
 
158
163
  Two ways to authenticate:
@@ -6,28 +6,26 @@ File: `packages/server/src/logger.ts`
6
6
 
7
7
  ## Architecture
8
8
 
9
- On startup the logger intercepts `console.log` / `console.error` / `console.warn` / `console.debug`. Every call writes twice:
9
+ On startup the logger intercepts `console.log` / `console.error` / `console.warn` / `console.debug`. A call that passes the level gate (`general.logging.level` / `HALO_LOG_LEVEL`, default `warn`) writes to:
10
10
  1. The original console (stdout/stderr) — live monitoring
11
11
  2. A disk log file — persistent history
12
+ 3. The OTel logger, when observability is on (see [design/observability.md](../design/observability.md))
12
13
 
13
- ## Log locations
14
+ Any `?token=…` value in a message is replaced with `?token=<redacted>` before it reaches a sink.
14
15
 
15
- | Scenario | Directory | File |
16
- |---|---|---|
17
- | Default (no workspace) | `~/.halo/logs/` | `server.log` |
18
- | Workspace open | `<project>/.halo/logs/` | `server.log` |
16
+ ## Log locations
19
17
 
20
- When the WS handler receives a `subscribe` or `chat` with `projectId`, it calls `setLogDir()` to switch to the project's log directory.
18
+ One file for the whole server: `~/.halo/global/logs/server.log`. A single server process serves many workspaces, so there is no per-workspace log. Cron runs and evolution runs keep their own logs under `~/.halo/global/logs/{cron,evo}/`. `halo server start -d` additionally captures the process's stdout/stderr in `~/.halo/logs/server.log` (plus the supervisor's `[respawn]` lines) — that is the file `halo server logs` tails.
21
19
 
22
20
  ## Log format
23
21
 
24
22
  ```
25
- 2026-04-19T10:30:00.000Z INFO [Server] Hono server listening on http://localhost:9527
26
- 2026-04-19T10:30:01.234Z ERROR [WS] Chat error: ThrottlingException
27
- 2026-04-19T10:30:02.567Z WARN [Orchestrator] Context overflow, auto-compacting...
23
+ time=2026-04-19T10:30:00.000Z level=info [Server] Hono server listening on http://localhost:9527
24
+ time=2026-04-19T10:30:01.234Z level=error [WS] Chat error: ThrottlingException
25
+ time=2026-04-19T10:30:02.567Z level=warn [SessionManager] Context overflow, auto-compacting...
28
26
  ```
29
27
 
30
- Every line: `ISO timestamp` + `level (DEBUG/INFO/WARN/ERROR)` + `message`.
28
+ Every line is logfmt-flavored: `time=<ISO timestamp> level=<debug|info|warn|error> <message>`; the message keeps its `[Module]` prefix.
31
29
 
32
30
  ## Rotation
33
31
 
@@ -41,8 +39,9 @@ When the log file exceeds the cap:
41
39
 
42
40
  | Config key | Default | Env var | Purpose |
43
41
  |---|---|---|---|
44
- | `logging.maxFileSize` | 10 MB | `HALO_LOG_MAX_SIZE` | Rotation threshold |
45
- | `logging.maxFiles` | 5 | `HALO_LOG_MAX_FILES` | Retained rotated files |
42
+ | `logging.max_file_size` (`config.yaml`) | 10 MB | `HALO_LOG_MAX_SIZE` | Rotation threshold |
43
+ | `logging.max_files` (`config.yaml`) | 5 | `HALO_LOG_MAX_FILES` | Retained rotated files |
44
+ | `general.logging.level` (`settings.yaml`) | `warn` | `HALO_LOG_LEVEL` | Minimum level written: debug \| info \| warn \| error |
46
45
 
47
46
  Defined in `packages/server/src/config.ts`.
48
47
 
@@ -54,18 +53,8 @@ initLogger()
54
53
  ```
55
54
 
56
55
  Called once from `packages/server/src/index.ts` at startup. Steps:
57
- 1. Create the log directory
58
- 2. Replace console methods with the interceptor
59
- 3. Each interceptor calls the original method and appends to the log file
60
-
61
- ## Workspace switching
62
-
63
- ```typescript
64
- import { setLogDir } from './logger.js'
65
- setLogDir('/path/to/project')
66
- ```
67
-
68
- Called from `packages/server/src/ws/handler.ts` when the client subscribes to a workspace.
56
+ 1. Replace console methods with the interceptor
57
+ 2. Each interceptor calls the original method and appends to the log file (the directory is created on first write)
69
58
 
70
59
  ## Error handling
71
60
 
@@ -78,8 +67,5 @@ Log writes use synchronous `fs.appendFileSync` and fail silently — logging mus
78
67
  tail -f /tmp/server.log
79
68
 
80
69
  # file logs (always written, regardless of nohup redirection)
81
- tail -f ~/.halo/logs/server.log
82
-
83
- # project-specific logs
84
- tail -f /path/to/your-project/.halo/logs/server.log
70
+ tail -f ~/.halo/global/logs/server.log
85
71
  ```