@turmind/halo 1.5.3-alpha → 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 +11 -10
  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 +19 -21
  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-c6c7772db8a50926.js +0 -1
  72. package/admin-out/_next/static/chunks/app/page-5f41b086cd5ad353.js +0 -1
  73. package/admin-out/_next/static/css/8809d33379458514.css +0 -3
  74. /package/admin-out/_next/static/{Wqi_ugna9MYsiV7ECuawQ → cuVcldfdg1PXevaQMQSzb}/_buildManifest.js +0 -0
  75. /package/admin-out/_next/static/{Wqi_ugna9MYsiV7ECuawQ → cuVcldfdg1PXevaQMQSzb}/_ssgManifest.js +0 -0
@@ -8,12 +8,18 @@ Location: [`packages/admin/src/features/editor/previews/`](../../../packages/adm
8
8
 
9
9
  ```
10
10
  previews/
11
- ├── FilePreview.tsx Public entry. Looks up plugin by extension,
12
- │ renders <Suspense><Component/></Suspense>.
13
- ├── types.ts PreviewPlugin, PreviewProps
14
- ├── registry.ts register() / getPlugin() / registeredExtensions() / isHeavyPreview() / loadExtensions()
11
+ ├── FilePreview.tsx Public entry. Asks the registry for the candidates for the
12
+ │ extension, renders the chosen one (<Suspense><Component/></Suspense>
13
+ │ for a built-in, the iframe host for an extension); too-large and
14
+ │ no-viewer placeholders.
15
+ ├── types.ts PreviewPlugin, PreviewProps, Resolved
16
+ ├── registry.ts register() / setExtensions() / loadExtensions() / resolve() / canPreview() / isHeavyPreview()
17
+ │ + subscribe() / useRegistryVersion() so open tabs re-resolve on change
18
+ ├── extension-host.tsx Iframe host for installed preview extensions (+ extension-host-logic.ts,
19
+ │ extension-token.ts) — see design/canvas-extensions.md
15
20
  ├── ui/
16
- │ ├── preview-shell.tsx Standard header (filename + Open-as-Text + Download + extraToolbar)
21
+ │ ├── preview-shell.tsx Standard header (filename + extraToolbar + Open with + Open-as-Text + Download)
22
+ │ ├── open-with-menu.tsx "Open with" picker, shown only when a file has more than one viewer
17
23
  │ ├── use-preview-fetch.ts Hook: fetch + AbortController + parse, returns {data, error, loading}
18
24
  │ ├── use-data-fetch.ts JSON-endpoint sibling: abortable fetcher → {data, error, loading},
19
25
  │ │ for previews that parse server-side instead of raw bytes
@@ -60,6 +66,8 @@ interface PreviewProps {
60
66
  viewUrl: string // for inline viewing, supports HTTP Range
61
67
  downloadUrl: string // for forced download (used by the shell's Download button)
62
68
  onOpenAsText?: () => void // set when the file can also be force-opened as text
69
+ tooLarge?: boolean // file over the editor's 10MB read cap; FilePreview shows a placeholder instead of the plugin
70
+ size?: number // on-disk size in bytes, shown by that placeholder
63
71
  }
64
72
  ```
65
73
 
@@ -121,7 +129,7 @@ register(fooPlugin)
121
129
 
122
130
  Done. The Canvas panel will:
123
131
  - Treat `.foo` / `.foobar` as non-text (routes to preview instead of Monaco)
124
- - Mount `FooPreview` inside an MRU cache (up to 5 concurrent plugins cached)
132
+ - Mount `FooPreview` inside an MRU cache (up to 5 concurrent previews cached)
125
133
 
126
134
  ### Routing API: `canPreview()` vs `loadExtensions()`
127
135
 
@@ -129,6 +137,7 @@ Done. The Canvas panel will:
129
137
 
130
138
  - **One-shot routing decisions** (`isBinaryExtension` in `editor-panel.tsx`: click-open, tab restore after reload, open-to-side) must `await loadExtensions()` before concluding "text", because an extension may claim an extension-only type (`.glb`) or a text suffix (`.json`). A preview verdict needs no wait — extensions only add viewers ahead of text, and `FilePreview` re-picks the viewer on a registry version bump.
131
139
  - The wait is capped at `INITIAL_LOAD_WAIT_MS` (3 s, `registry.ts`), counted from the first call; on timeout callers proceed as if nothing were installed. The request isn't cancelled — a late result still lands via `setExtensions`.
140
+ - Candidates come from `resolve(ext)`, best first: `default` extensions → the built-in plugin → `option` extensions → Open as Text (always last). More than one candidate puts an **Open with** menu in the header.
132
141
  - It never rejects and is never re-issued: a failed fetch leaves the extension layer empty without delaying later opens. The WS-reconnect re-fetch and `extension:changed` frames fill it in.
133
142
 
134
143
  ## PreviewShell — the standard header
@@ -211,21 +220,21 @@ const { data } = usePreviewFetch(viewUrl, (buf, signal) =>
211
220
 
212
221
  ## `heavy: true` — when to use it
213
222
 
214
- Set `heavy: true` when the preview:
223
+ Set `heavy: true` on a built-in when the preview:
215
224
  - Needs DOM access (can't run in a Worker — e.g. `pptx-preview` draws to canvas)
216
225
  - Is the dominant memory cost of the page (large canvas, many cached elements)
217
226
 
218
227
  Effect: the MRU cache skips this plugin. Only the **active** instance mounts; switching away unmounts the component (releases memory and stops any in-flight work). Switching back re-fetches/re-renders.
219
228
 
220
- Only pptx currently uses this. Don't set it by default — the MRU cache gives much faster switches.
229
+ Only pptx currently uses this among built-ins (read-only preview extensions are also treated as heavy, judged by `isHeavyPreview()`). Don't set it by default — the MRU cache gives much faster switches.
221
230
 
222
231
  ## `onOpenAsText`
223
232
 
224
- When the user right-clicks a previewable file in Explorer and picks "Open as Text", Canvas closes the preview tab and opens the raw file in Monaco. The preview plugin receives this as `props.onOpenAsText` — pass it straight through to `PreviewShell` and the button appears automatically.
233
+ The editor panel passes `props.onOpenAsText` to every preview tab. Calling it closes the preview tab and opens the raw file in Monaco. Pass it straight through to `PreviewShell` and the button appears automatically (the **Open with** menu also lists Open as text).
225
234
 
226
235
  ## Two plugin patterns: raw-bytes vs. server-parsed
227
236
 
228
- Most built-in plugins (pdf, docx, xlsx, pptx, media) are **raw-bytes**: `usePreviewFetch` downloads the whole file as an ArrayBuffer via `GET /api/files/download?inline=1`, then parsing happens client-side (a Worker for the heavy formats). This is the pattern documented above.
237
+ Most built-in plugins (pdf, docx, xlsx, pptx, media) are **raw-bytes**: they load the file from `viewUrl` (`GET /api/files/download?inline=1`). docx and xlsx use `usePreviewFetch` to download it as an ArrayBuffer and parse client-side in a Worker; pptx fetches it itself and renders on the main thread; pdf and media just hand `viewUrl` to the browser (`<iframe>` / `<img>` / `<video>` / `<audio>`). This is the pattern documented above.
229
238
 
230
239
  Parquet, SQLite and CSV/TSV are **server-parsed** instead: the server does the parsing and hands back one page of JSON rows at a time via `GET /api/data-preview/*` (route details in [`dev/api.md`](api.md#data-preview)), so a multi-GB file never has to reach the browser.
231
240
 
@@ -288,4 +297,4 @@ See [`dev/api.md`](api.md) for the route details.
288
297
  - Open-as-Text button works (closes preview, opens in Monaco)
289
298
  - Closing the preview tab cleanly aborts any in-flight fetch/parse
290
299
  - Opening a second file of the same type reuses the worker (no second worker instance — check DevTools → Application → Service Workers / the process graph)
291
- - Switching between up to 5 preview tabs is instant (MRU cache); the 6th oldest unmounts
300
+ - Switching between up to 5 preview tabs is instant (MRU cache); the 6th oldest unmounts (a dirty extension tab is never evicted)
@@ -31,13 +31,14 @@ Write a file (creates parent dirs on demand).
31
31
 
32
32
  ### file_edit
33
33
 
34
- Replace a string in a file (exact match).
34
+ Replace a string in a file (exact match). Fails when `old_string` is empty, identical to `new_string`, not found, or (without `replace_all`) found more than once.
35
35
 
36
36
  | Arg | Type | Required | Description |
37
37
  |---|---|---|---|
38
38
  | path | string | yes | File path |
39
39
  | old_string | string | yes | The exact text to find |
40
40
  | new_string | string | yes | The replacement |
41
+ | replace_all | boolean | no | Replace every occurrence (default: false) |
41
42
 
42
43
  ### view_image
43
44
 
@@ -58,7 +59,7 @@ Processing before the bytes go out — only the payload changes, the file on dis
58
59
 
59
60
  How many images history can hold in total is capped separately at the session level — see [design/session.md → History image budget](../design/session.md#history-image-budget).
60
61
 
61
- **Vision gating**: this tool is only injected into the agent's tool list when the underlying model declares `capabilities.image: true` in its provider manifest. For text-only models (DeepSeek and others), `view_image` is silently dropped at `createWorkspaceTools()` time so the model never sees it — calling it would otherwise produce a 400 from the provider.
62
+ **Vision gating**: this tool is only injected into the agent's tool list when the underlying model declares `capabilities.image: true` in its provider manifest (or the agent's `model.image` in `agent.yaml` overrides it). For text-only models (DeepSeek and others), `view_image` is silently dropped at `createWorkspaceTools()` time so the model never sees it — calling it would otherwise produce a 400 from the provider.
62
63
 
63
64
  ### file_list
64
65
 
@@ -103,7 +104,7 @@ Find files by glob.
103
104
  | pattern | string | yes | Glob, e.g. `**/*.ts`, `src/**/*.tsx` |
104
105
  | path | string | no | Starting dir (default: workspace root) |
105
106
 
106
- Returns paths relative to workspace root, alphabetically. Same skip list as grep.
107
+ Returns paths relative to workspace root, alphabetically; stops collecting at 5,000 matches. Same skip list as grep.
107
108
 
108
109
  ### web_fetch
109
110
 
@@ -125,7 +126,7 @@ Each session carries `accessLevel: 'readonly' | 'workspace' | null` (persisted i
125
126
 
126
127
  | Level | DB value | Tools (with OS sandbox) | Tools (without) | Sandbox |
127
128
  |---|---|---|---|---|
128
- | `null` (full) | `full` | All 9 tools | All 9 tools | None (rm guard still applies) |
129
+ | `null` (full) | `NULL` | All 9 tools | All 9 tools | None (rm guard still applies) |
129
130
  | `workspace` | `workspace` | All 9 tools | All 9 tools | workspace rw, sensitive paths + workspace runtime state hidden |
130
131
  | `readonly` | `readonly` | All 9 tools | file_read, view_image, file_list, grep, glob (5 tools) | workspace ro, sensitive paths + workspace runtime state hidden |
131
132
 
@@ -182,7 +183,7 @@ Sensitive directories and files are hidden from workspace/readonly sessions via
182
183
  | `hidden_files` | `~/.npmrc,~/.bash_history,~/.gitconfig,~/.git-credentials,~/.netrc,~/.halo/global/{evo,cron,runs}.db` + their `-wal`/`-shm` files | bwrap `--ro-bind ~/.halo/.sandbox-empty` (reads as empty); Seatbelt `literal` deny |
183
184
  | `writable_dirs` | (empty) | bwrap `--bind` read-write / Seatbelt write allow — for external CLIs that keep local state (e.g. `~/.kiro`); not applied to readonly sessions |
184
185
 
185
- Changes saved through the settings API (admin Settings page, `PUT` / `PATCH /api/settings`) take effect immediately — the save fires `onSettingsChange`, which re-reads the lists into `sandbox.ts` (`setSandboxHiddenPaths`), so the next tool call uses them. A hand edit of `settings.yaml` is not watched for these keys: it applies at the next restart or the next API save. These keys are `globalOnly` in the schema — a workspace `settings.yaml` cannot override them, since they define the security boundary agents run inside.
186
+ Changes saved through the settings API (admin Settings page, `PUT` / `PATCH` / `DELETE /api/settings`) take effect immediately — the save fires `onSettingsChange`, which re-reads the lists into `sandbox.ts` (`setSandboxHiddenPaths`), so the next tool call uses them. A hand edit of `settings.yaml` is not watched for these keys: it applies at the next restart or the next API save. These keys are `globalOnly` in the schema — a workspace `settings.yaml` cannot override them, since they define the security boundary agents run inside.
186
187
 
187
188
  **Workspace-relative set (hardcoded)** — the workspace's own runtime state, which holds other channels'/users' conversations on a shared workspace. Code constants in `sandbox.ts` (`WORKSPACE_HIDDEN_DIRS` / `WORKSPACE_HIDDEN_FILES`), deliberately not settings: this is a security boundary (a config edit must not be able to open it), and the entries are workspace-relative while the settings lists are absolute/`~` paths.
188
189
 
@@ -198,7 +199,7 @@ The rest of `.halo/` (INSTRUCTIONS.md, INDEX.md, docs/, memory/, skills/, agents
198
199
  Per-channel defaults: every channel session (Web, Telegram, Slack, Feishu, WeCom, WeChat) inherits its account's `access_level` — `full` → full, `workspace` → workspace, `readonly` / `observer` → readonly — and new accounts default to `readonly` (DB column default, `insertAccount` fallback and the admin create forms). Level sources for every entry point (admin, cron, cli, relay, goal, sub-agents): [guide/delegation-and-access.md](../guide/delegation-and-access.md#where-a-sessions-level-comes-from).
199
200
 
200
201
  ### Binary file detection
201
- `grep` and `glob` read the first 512 bytes looking for a null byte and skip binaries.
202
+ `grep` reads the first 512 bytes of each file looking for a null byte and skips binaries (`glob` matches paths only and never opens files).
202
203
 
203
204
  ### Tool result budget
204
205
  The orchestrator truncates tool results over 8000 chars and appends a `[Content truncated]` hint telling the agent to use `grep` for a targeted search. `activate_skill` results are exempt — a SKILL.md body is instructions, not data, and the built-in acp / cron / self skills exceed 8K.
@@ -227,6 +228,7 @@ Start a sub-agent session asynchronously. When the sub-agent finishes, its **wra
227
228
 
228
229
  - Success: `{"code": 0, "session_id": "<childSessionId>"}`
229
230
  - Unknown agent: `{"code": 1, "error": "agent \"<id>\" not found..."}`
231
+ - Agent outside the caller's `team`: `{"code": 1, "error": "agent \"<id>\" is not in your team..."}`
230
232
  - Depth exceeded: `{"code": 1, "error": "Maximum nesting depth (N) reached..."}`
231
233
  - Working dir invalid: `{"code": 1, "error": "working_dir \"...\" is outside the workspace"}` (or does-not-exist / not-a-directory)
232
234
 
@@ -244,12 +246,21 @@ No arguments. Returns JSON:
244
246
  "sessions": [
245
247
  {
246
248
  "id": "root>sid_xxx",
249
+ "parentId": "root",
247
250
  "agentId": "coder",
248
251
  "agentName": "Coder",
249
- "title": "Implement login page",
250
252
  "description": "Implement login page",
251
253
  "status": "running",
252
- "createdAt": 1714000000000
254
+ "accessLevel": null,
255
+ "goalSessionId": null,
256
+ "createdAt": 1714000000000,
257
+ "updatedAt": 1714000005000,
258
+ "stoppedAt": null,
259
+ "archivedAt": null,
260
+ "title": "Implement login page",
261
+ "exchangeCount": 1,
262
+ "contextTokens": 12345,
263
+ "totalOutputTokens": 678
253
264
  }
254
265
  ],
255
266
  "count": 1
@@ -258,10 +269,11 @@ No arguments. Returns JSON:
258
269
 
259
270
  `status`: `running` / `idle` / `stopped`. Archived sessions are excluded.
260
271
 
261
- `title` is the human-assigned label (set by renaming the session in the admin
262
- sidebar), read from the per-session jsonl log so it matches what the UI shows.
263
- It falls back to `description` (the `start_session` task summary) when no title
264
- was set, so the field is never empty — lets a caller dispatch work by title.
272
+ Each entry is the session's `SessionInfo` row. `title` is the human-assigned
273
+ label (set by renaming the session in the admin sidebar) mirrored onto the row
274
+ from the session file, so it matches what the UI shows. It falls back to
275
+ `description` (the `start_session` task summary) when no title was set, so the
276
+ field is never empty — lets a caller dispatch work by title.
265
277
 
266
278
  ### query_session
267
279
 
@@ -20,6 +20,7 @@ Aimed at Halo end-users. Pick the doc that matches your situation.
20
20
  - [Telegram](channels/telegram.md) — BotFather token, long-poll
21
21
  - [Slack](channels/slack.md) — Socket Mode (no public webhook needed)
22
22
  - [Feishu / Lark](channels/feishu.md) — long-connect with appId + appSecret
23
+ - [WeCom](channels/wecom.md) — 企业微信 intelligent bot, long-connect with Bot ID + Secret
23
24
  - [WeChat](channels/wechat.md) — QR-scan iLink bot
24
25
  - [ACP](channels/acp.md) — Claude Code or other ACP clients driving a remote halo
25
26
 
@@ -6,17 +6,17 @@ The agent is Halo's core abstraction: personality (AGENT.md) + config (agent.yam
6
6
 
7
7
  | Kind | Description |
8
8
  |---|---|
9
- | Built-in | Server-shipped agents (`default`, `executor`, `deep-executor` + the three internal `__evo_agent__` / `__score__` / `__apply_agent__`). Live under `~/.halo/global/agents/<id>/`. **Force-overwritten on every server startup** — local edits to these files will be lost on the next start. To customize: copy into the workspace scope (workspace replaces global) and edit there. |
9
+ | Built-in | Server-shipped agents (`default`, `executor`, `deep-executor` + the internal `goal` / `__evo_agent__` / `__score__` / `__apply_agent__`; `goal` is seeded like the rest but only usable when `general.goal_mode_enabled` is set — off by default, no UI). Live under `~/.halo/global/agents/<id>/`. **Force-overwritten on every server startup** — local edits to these files will be lost on the next start. To customize: copy into the workspace scope (workspace replaces global) and edit there. |
10
10
  | Global | Any other agent under `~/.halo/global/agents/<id>/`, e.g. one you created via the admin UI. Shared across projects. Never overwritten by the server. |
11
11
  | Workspace | `<project>/.halo/agents/<id>/`, private to the current project; same-id workspace wins over global. Override is **whole-folder**: the workspace agent folder replaces the global one entirely, so copy *both* `agent.yaml` and `AGENT.md` when customizing — a folder with only `AGENT.md` has no model config and won't load. |
12
12
 
13
13
  ## Open the Agents panel
14
14
 
15
- Click the `👥 Agents` icon in the Activity Bar.
15
+ Click the `🤖 Agents` icon in the Activity Bar.
16
16
 
17
17
  ```
18
18
  ┌─────────────────┬──────────────────────────┐
19
- │ Agents │ Form / YAML / MD Editor │
19
+ │ Agents │ Form / Edit │
20
20
  │─────────────────│ │
21
21
  │ ▼ Global (2) │ │
22
22
  │ 🤖 Default │ │
@@ -28,38 +28,32 @@ Click the `👥 Agents` icon in the Activity Bar.
28
28
 
29
29
  ## Create an agent
30
30
 
31
- Top-of-sidebar `+` button; modal asks for:
31
+ Click the `+` on the **Global** or **Workspace** group header in the sidebar (the group you click decides the scope; Workspace needs an open workspace). Two prompts follow:
32
32
  - **Name**: display name (e.g. "Coder")
33
- - **Description**: one-line description
34
- - **Scope**: Global / Workspace
33
+ - **Description**: one-line description (optional)
35
34
 
36
35
  The backend calls `defaultAgentYaml(name, description)` to generate a minimal `agent.yaml`.
37
36
 
38
37
  ## Edit an agent
39
38
 
40
- Click an agent in the sidebar to open the right-hand editor. Three views:
39
+ Click an agent in the sidebar to open the right-hand editor. Two views:
41
40
 
42
41
  ### Form view
43
- Data-driven form for name / description / model / tools / skills / thinking — click Save.
42
+ Data-driven form for name / description / model / tools / skills / thinking. Changes auto-save about half a second after you stop editing — there's no Save button. The agent's AGENT.md is shown below the form as a read-only preview.
44
43
 
45
44
  **Model section** has four fields:
46
45
  - `provider` — dropdown populated from `~/.halo/global/models/*.yaml`
47
- - `id` — combobox (input + datalist); preset options filtered by current provider, also accepts manual input of any Bedrock model ID
46
+ - `id` — combobox (input + datalist); preset options filtered by current provider, also accepts manual input of any model ID
48
47
  - `endpoint` — combobox (input + datalist); full endpoint URL (e.g. `https://bedrock-runtime.ap-northeast-1.amazonaws.com`), supports custom proxy URLs
49
48
  - `maxTokens` — optional; defaults to `maxOutputTokens` in the provider yaml
50
49
 
51
- Switching provider auto-resets the model id to the first model of the new provider.
50
+ Switching provider resets the model id to the new provider's default model (its `defaultModelId`), keeping the current id when the new provider lists it too.
52
51
 
53
52
  **Capability buttons** (Prompt Caching / Thinking) appear when the selected model matches a registry entry with declared capabilities. For manually entered model IDs not in the registry, default presets are shown (5min/1hour for caching, Low/Medium/High/Max for thinking).
54
53
 
55
- ### YAML view
56
- Edit `agent.yaml` directly (Monaco). Use this for advanced settings (like `context.maxTokens`, `promptCaching`).
57
-
58
- ### MD view
59
- Edit `AGENT.md` (personality) and `INSTRUCTIONS.md` (preferences).
54
+ ### Edit view
55
+ The **Edit** button (top right) opens the agent's folder in a mini workspace (file tree + Monaco); **Back** returns to the form. Edit `agent.yaml` directly for advanced settings (like `context.maxTokens`, `promptCaching`) and `AGENT.md` for the personality.
60
56
  - `AGENT.md` takes precedence over YAML's `system_prompt`
61
- - `INSTRUCTIONS.md` is injected into every agent, not just this one
62
- - When a workspace is open, the top-right toggles **Global / Workspace**; both MDs can be edited independently of which scope the current agent lives in. Switching to a non-existent file shows `(new)`; saving auto-creates the dir/file.
63
57
  - AGENT.md supports `{{var}}` placeholders (`{{<skill-id>.params.<key>}}` / `<<ENV>>` / built-ins) rendered on agent start — see [skills.md placeholder section](skills.md#placeholders-template-variables)
64
58
 
65
59
  ## Tool configuration
@@ -75,7 +69,7 @@ team: # a non-empty team is what enables delegation
75
69
  - executor # the agent ids this one may spawn
76
70
  ```
77
71
 
78
- **Workspace tools**: `file_read / file_write / file_edit / file_list / shell_exec / grep / glob / web_fetch` — listed by name under `tools`.
72
+ **Workspace tools**: `file_read / view_image / file_write / file_edit / file_list / shell_exec / grep / glob / web_fetch` — listed by name under `tools`.
79
73
 
80
74
  **Session tools**: `start_session / session_list / query_session / interrupt_session / stop_session / archive_session / get_session_output / query_agent` — **not** listed under `tools`. The whole bundle is granted automatically the moment an agent declares a non-empty `team`; an empty/absent `team` means no delegation (no session tools, no roster). To let a sub-agent delegate further, give it its own `team`. Add the agent's own id to its `team` to enable parallel self-spawn.
81
75
 
@@ -123,13 +117,14 @@ Internal agents (`internal: true`, e.g. self-evolution agents) have no Test butt
123
117
 
124
118
  ## Delete
125
119
 
126
- Right-click → Delete. Constraints:
120
+ Hover a **workspace** agent in the sidebar and click its trash icon. Global agents show a crown instead (no delete button there). Constraints:
127
121
  - The last global agent cannot be deleted (server-enforced)
128
122
  - Workspace agents delete freely
123
+ - `/agent delete <name>` (full access) removes either kind
129
124
 
130
125
  ## Common workflows
131
126
 
132
- **Copy a global agent into workspace for customisation**: edit it in Form view, switch scope to Workspace, Save — creates an independent copy under `<project>/.halo/agents/<id>/`.
127
+ **Copy a global agent into workspace for customisation**: copy the whole folder `~/.halo/global/agents/<id>/` to `<project>/.halo/agents/<id>/` and edit there. (Clicking `+` on Workspace with the same name creates a fresh scaffold under the same id, not a copy.)
133
128
 
134
129
  **Multi-agent collaboration**: the Default agent sees its teammates in the prompt roster, starts them with `start_session`, and the sub-agent auto-reports on completion. See [sessions.md](sessions.md).
135
130
 
@@ -146,7 +141,7 @@ priority: 0
146
141
 
147
142
  model:
148
143
  provider: aws-bedrock-claude-invoke # required, matches ~/.halo/global/models/<provider>.yaml
149
- id: global.anthropic.claude-sonnet-4-6 # default from settings.yaml params.model.default
144
+ id: global.anthropic.claude-sonnet-4-6 # the provider yaml's defaultModelId
150
145
  endpoint: https://bedrock-runtime.us-west-2.amazonaws.com # full endpoint URL; supports custom proxy
151
146
  maxTokens: 16384 # optional, max output tokens (default from provider yaml)
152
147
 
@@ -226,41 +221,12 @@ nano-banana:
226
221
  api_key: <<NANO_BANANA_KEY>>
227
222
  ```
228
223
 
229
- …where the schema lives at `extensions/skills/nano-banana/config.yaml`.
224
+ …where the schema lives in the skill's own `config.yaml` (`skills/nano-banana/config.yaml`).
230
225
 
231
226
  With `export NANO_BANANA_KEY=sk-xxx` in the env, the agent's system prompt gets the real key on start.
232
227
 
233
228
  ## Model registry — adding a new provider
234
229
 
235
- Halo ships with `aws-bedrock-claude-invoke` only. Hooking up OpenAI / direct Anthropic / another Bedrock variant takes two steps:
236
-
237
- ### 1. Declare the provider
238
-
239
- Create `~/.halo/global/models/<providerId>.yaml`:
240
-
241
- ```yaml
242
- id: openai # required; matches the filename
243
- displayName: OpenAI
244
- description: OpenAI Chat Completions
245
- models:
246
- - id: gpt-4o
247
- displayName: GPT-4o
248
- maxOutputTokens: 16384
249
- capabilities:
250
- promptCaching:
251
- ttlPresets:
252
- - { value: 5m, label: 5min }
253
- # No thinking block → UI automatically hides the Thinking button
254
- ```
255
-
256
- ### 2. Implement the runtime
257
-
258
- Add a case in the switch inside [packages/server/src/agents/model-runtime.ts](../../../packages/server/src/agents/model-runtime.ts) returning a class that implements `ModelRuntime` (`messages` + `run()`). Interface detail in [design/agent.md](../design/agent.md#agent-instance).
259
-
260
- ### Usage
261
-
262
- Set `model.provider: openai` in your agent.yaml; the Provider dropdown in Form view lists it automatically.
263
-
264
- ### Current state
230
+ Halo ships 13 providers (`aws-bedrock-claude-invoke`, `aws-bedrock-mantle`, `aws-bedrock-openai`, `anthropic`, `openai`, `kimi`, `deepseek`, `minimax`, `qwen`, `hunyuan`, `doubao`, `zhipu`, `mimo-token-plan-china`); each one is a yaml under `~/.halo/global/models/` plus a matching case in [packages/server/src/agents/model-runtime.ts](../../../packages/server/src/agents/model-runtime.ts). For any OpenAI- or Anthropic-compatible endpoint you don't need new code: pick `openai` or `anthropic` as the provider and fill in the endpoint and model id in Form view.
265
231
 
266
- Only `aws-bedrock-claude-invoke` ships an implementation. If you drop a provider yaml without a runtime implementation, session spawn fails with `Unknown provider "..."`.
232
+ A truly new provider takes a yaml manifest plus a runtime case; the step-by-step is in [dev/add-model-provider.md](../dev/add-model-provider.md). If you drop a provider yaml without a runtime case, session spawn fails with `Unknown provider "..."`.
@@ -28,7 +28,7 @@ This folder has one onboarding guide per channel. Pick the one you want and foll
28
28
  SessionManager (per workspace)
29
29
  ```
30
30
 
31
- Each channel handler is a thin adapter between its native protocol and halo's `SessionManager`. Slash commands (`/session` `/agent` `/skill` `/workspace` `/cron` `/acp` `/evo` `/help`) are shared across all of them — see `channels/shared/commands.ts`.
31
+ Each channel handler is a thin adapter between its native protocol and halo's `SessionManager`. Slash commands (`/session` `/agent` `/skill` `/workspace` `/cron` `/acp` `/evo` `/extension` `/help`) are shared across all of them — see `channels/shared/commands.ts`.
32
32
 
33
33
  The ACP adapter is **not** a channel — it's a stdio bridge that translates ACP JSON-RPC into the web channel's HTTP+SSE. Counted here only because users go through the same "set up an account, get a token" flow.
34
34
 
@@ -42,16 +42,19 @@ What the admin UI actually asks you for. Auto-filled fields (botUsername, botUse
42
42
  | Telegram | Bot token, workspace path | Label, access level, language, allowed users | BotFather `/newbot` |
43
43
  | Slack | Bot token (`xoxb-`), App token (`xapp-`), workspace path | Label, access level, language | api.slack.com → Create App → install + Socket Mode |
44
44
  | Feishu | App ID, App Secret, workspace path | Verification token, encrypt key, label, access level, language | open.feishu.cn → Create App + add bot capability |
45
- | WeCom | Bot ID, Secret, workspace path | Label, access level, language, allowed users | work.weixin.qq.com → 智能机器人 → create bot, copy Bot ID + Secret |
45
+ | WeCom | Bot ID, Secret, workspace path | Label, access level, language | work.weixin.qq.com → 智能机器人 → create bot, copy Bot ID + Secret |
46
46
  | WeChat | Workspace path (set before scanning) | Label, access level, language | QR scan in admin UI; bot token comes back from the scan |
47
47
 
48
+ In the admin account list every row shows an access-level badge next to its name, and a red **(path does not exist)** after the workspace path when the bound folder is missing on disk (the account is skipped at startup until you re-bind it). Secret fields in the Add forms (Telegram bot token, Slack bot / app tokens, Feishu App Secret and Encrypt Key, WeCom Secret) are password inputs — pasted values are masked.
49
+
48
50
  ## Access level
49
51
 
50
52
  Every channel account carries an `accessLevel`:
51
53
 
52
- - `readonly` (default) — sandboxed, workspace mounted read-only, secrets dir not mounted, only read-only tools (`file_read`, `view_image`, `file_list`, `grep`, `glob`) injected
54
+ - `readonly` (default) — sandboxed, workspace mounted read-only, secrets dir not mounted. With an OS sandbox the agent gets all its tools (writes fail); without one only the read-only tools (`file_read`, `view_image`, `file_list`, `grep`, `glob`)
53
55
  - `workspace` — sandboxed, workspace mounted read-write, secrets dir not mounted
54
- - `full` — no sandbox, all tools, can `/workspace <path>` to another workspace
56
+ - `full` — no sandbox, all tools, can `/workspace switch <path>` to another workspace
57
+ - `observer` — Web accounts only: global read-only, meant for dashboards / metrics (Halo City, `/api/metrics`); chat runs at `readonly`. A chat-channel account that still holds `observer` from older data shows an Observer badge, and its edit form keeps that level unless you pick another one
55
58
 
56
59
  When in doubt start at `readonly` and raise it later. The setting is on the **account row**, so a single Slack workspace can have one `readonly` bot account in #general and a separate `full` account in #ops.
57
60
 
@@ -54,6 +54,8 @@ halo acp \
54
54
  | `--token` | yes | Web-channel token. `full` access required for multi-workspace use |
55
55
  | `--workspace` | yes | Absolute server-side path for this adapter |
56
56
  | `--agent-id` | no | Halo agent profile to use when ACP `session/new` creates a fresh session. Default `default` (alias `--agent`) |
57
+ | `--scheme` | no | `http` (default) or `https` — use `https` for a server behind a TLS reverse proxy |
58
+ | `--header "Name: value"` | no | Repeatable. Extra HTTP header sent on every request (like `curl -H`), for auth in front of the server — proxy cookie, `CF-Access-*`, basic auth |
57
59
 
58
60
  Each adapter process binds to **one** workspace. To drive multiple workspaces concurrently from one token, run multiple adapter processes — one per workspace.
59
61
 
@@ -139,7 +141,7 @@ For now: if you want the agent to see a Mac-side file, paste it into the prompt.
139
141
  | `401` on first prompt | Token typo, or token was deleted from admin |
140
142
  | `403` when launching with `--workspace /some/other/path` | Token is `readonly` / `workspace` access — use a `full` token or omit `--workspace` |
141
143
  | Tool calls don't appear in Claude Code | Expected — halo doesn't translate every event back as ACP `tool_call`. See [docs/dev/acp-adapter.md](../../dev/acp-adapter.md) for the full mapping |
142
- | Two `session/prompt` calls on the same id, second hangs | Halo queues messages when a session is busy; ACP adapter ends the response with `[queued]`. Wait for the first to finish |
144
+ | Two `session/prompt` calls on the same id, second hangs | Halo queues messages when a session is busy; ACP adapter ends the response with `[queued — session busy]`. Wait for the first to finish |
143
145
  | `/workspace switch <path>` worked but other tokens broke | You changed the db-level default. Switch back with another `/workspace switch`, or stop using slash commands from the adapter |
144
146
 
145
147
  ## Reference
@@ -77,16 +77,16 @@ After approval the bot is visible in your Feishu tenant. Repeat this step **ever
77
77
 
78
78
  ## Step 7 — Add the account in halo admin
79
79
 
80
- Open halo admin → **Channels** → **Feishu** → **Add Account**:
80
+ Open halo admin → **Channels** → **Feishu** → **Add Bot**:
81
81
 
82
82
  | Field | Value |
83
83
  |---|---|
84
84
  | App ID | from Step 2 |
85
85
  | App Secret | from Step 2 |
86
- | Verification token | leave blank (long-connect doesn't use it) |
87
- | Encrypt key | leave blank (long-connect doesn't encrypt the wire) |
88
- | Workspace path | absolute path, e.g. `/home/ubuntu/my-project` |
89
- | Label | optional |
86
+ | Verification Token (optional) | leave blank (long-connect doesn't use it) |
87
+ | Encrypt Key (optional) | leave blank (long-connect doesn't encrypt the wire) |
88
+ | Bind to workspace | absolute path, e.g. `/home/ubuntu/my-project` |
89
+ | Name (optional) | a label for the account |
90
90
  | Access level | `readonly` (default), `workspace`, or `full` |
91
91
  | Language | `en` or `zh` |
92
92
 
@@ -109,7 +109,7 @@ In Feishu desktop / mobile:
109
109
  1. Top-bar search → type the bot's name → click into the bot's profile → start a DM → send `hello` → expect a streamed reply
110
110
  2. Add the bot to a group: open a group → 设置 → 群机器人 → add → `@halo hi` → expect a reply
111
111
 
112
- If nothing happens, check halo server logs (`/tmp/halo-server.log`) for `[feishu]` lines.
112
+ If nothing happens, check halo server logs (`~/.halo/global/logs/server.log`) for `[Feishu]` lines.
113
113
 
114
114
  ## How halo handles inbound
115
115
 
@@ -117,18 +117,20 @@ If nothing happens, check halo server logs (`/tmp/halo-server.log`) for `[feishu
117
117
  - **Groups** — only `@`-mentions wake the bot up
118
118
  - **Threads** — Feishu's `root_id` is used as the session boundary; replies stay in-thread
119
119
  - **Files** — images go to the LLM as multimodal content; other files are saved under `<workspace>/.halo/assets/feishu/inbound/<accountId>/<date>/`
120
+ - **After a server restart** — if the agent was mid-task when halo restarted, its reply still reaches the most recent 1:1 DM the bot talked in; group threads reconnect on their next `@`-mention
120
121
  - **Self-loop** — bot-authored events are dropped to prevent the bot replying to itself
121
122
 
122
123
  ## Slash commands
123
124
 
124
- Same set as the other channels — type as plain text in a DM or thread:
125
+ Same set as the other channels — type as plain text, **in a 1:1 DM only** (in a group the text goes to the agent as a normal message):
125
126
 
126
127
  | Command | Effect |
127
128
  |---|---|
128
- | `/session <verb>` | Session lifecycle: `new` / `list` / `switch <n>` / `stop` / `interrupt` / `compact` / `context` |
129
+ | `/session <verb>` | Session lifecycle: `new` / `list` / `switch <n>` / `stop` / `interrupt` / `compact` / `context` / `info` |
129
130
  | `/agent <verb>` | Manage agents (`list` / `switch` / `desc` open to all; `delete` full; `create` / `update` via skill, full) |
130
131
  | `/skill <verb>` | Manage skills (`list` / `desc` open; `disable` / `enable` workspace; `delete` full; `create` / `update` via skill, full) |
131
132
  | `/workspace <verb>` | Workspace: `info` (all) / `switch <path>` (full) / `setup` / `tidy` (workspace) / `share` (full) |
133
+ | `/cron` `/acp` `/extension` | Skill-backed object commands (full access); `/evo [hint]` queues a self-evolution run |
132
134
  | `/help` | List commands — object commands show only the verbs you can run |
133
135
 
134
136
  ## Cron jobs targeting Feishu
@@ -145,12 +147,12 @@ Search depends on the contact and `im:chat:readonly` scopes from Step 4. If auto
145
147
 
146
148
  | Symptom | Cause / fix |
147
149
  |---|---|
148
- | `99991663` on Add Account | App Secret wrong — copy it again from Step 2 |
149
- | `99991668` on Add Account | Bot capability not enabled — go back to Step 3 |
150
+ | `99991663` on Add Bot | App Secret wrong — copy it again from Step 2 |
151
+ | `99991668` on Add Bot | Bot capability not enabled — go back to Step 3 |
150
152
  | Bot exists but receives no events | Step 5 mode is set to webhook (not long-connect), or Step 6 wasn't done |
151
153
  | Permission errors when sending images / reading group info | New scope was approved but app version wasn't republished — go to Step 6 |
152
154
  | Can't find the bot in Feishu search | Step 6 wasn't done. Search results are tenant-wide and only show **published** apps |
153
- | Multiple halo instances reply to the same message | Only one process should hold the long-connect. Check `~/.halo/global/server.pid` for the lock |
155
+ | Multiple halo instances reply to the same message | Only one process should hold the long-connect. Check `~/.halo/global/server.lock` for the lock |
154
156
 
155
157
  ## Multi-tenant setup
156
158
 
@@ -83,14 +83,14 @@ In the Slack desktop client, fully quit and reopen (or `Cmd+R` to reload) — ot
83
83
 
84
84
  ## Step 7 — Add the account in halo admin
85
85
 
86
- Open halo admin → **Channels** → **Slack** → **Add Account**:
86
+ Open halo admin → **Channels** → **Slack** → **Add Bot**:
87
87
 
88
88
  | Field | Value |
89
89
  |---|---|
90
- | Bot token | the `xoxb-…` from Step 5 |
91
- | App token | the `xapp-…` from Step 2 |
92
- | Workspace path | absolute path to the workspace this bot drives, e.g. `/home/ubuntu/my-project` |
93
- | Label | optional, e.g. "Engineering bot" |
90
+ | Bot Token (xoxb-…) | the `xoxb-…` from Step 5 |
91
+ | App Token (xapp-…) | the `xapp-…` from Step 2 |
92
+ | Bind to workspace | absolute path to the workspace this bot drives, e.g. `/home/ubuntu/my-project` |
93
+ | Name (optional) | e.g. "Engineering bot" |
94
94
  | Access level | `readonly` (default), `workspace`, or `full` |
95
95
  | Language | `en` or `zh` |
96
96
 
@@ -111,7 +111,7 @@ Pick the level with that in mind:
111
111
  1. In Slack, click the bot's name in the sidebar to start a DM → type `hello` → expect a streamed reply
112
112
  2. Invite the bot into a channel: `/invite @halo` → mention it: `@halo what files are in this workspace?` → expect a reply in the same thread
113
113
 
114
- If nothing happens, check the halo server logs (`/tmp/halo-server.log`) for `[slack]` lines.
114
+ If nothing happens, check the halo server logs (`~/.halo/global/logs/server.log`) for `[Slack]` lines.
115
115
 
116
116
  ## How halo handles inbound
117
117
 
@@ -119,18 +119,20 @@ If nothing happens, check the halo server logs (`/tmp/halo-server.log`) for `[sl
119
119
  - **Channels / groups** — only `@`-mentions wake the bot up, plain messages are ignored
120
120
  - **Threads** — the bot replies in-thread; one halo session per thread, so a long thread stays in one conversation
121
121
  - **Files** — images go to the LLM as multimodal content; other files are saved under `<workspace>/.halo/assets/slack/inbound/<accountId>/<date>/`
122
+ - **After a server restart** — if the agent was mid-task when halo restarted, its reply still reaches the most recent conversation (DM or thread) the bot talked in (one per bot); other threads reconnect on their next message
122
123
  - **`bot_id` self-loop** — messages posted by any bot (including this one) are dropped, so the agent can't reply to itself
123
124
 
124
125
  ## Slash commands
125
126
 
126
- Same set as the other channels — type these as plain text in a DM or thread:
127
+ Same set as the other channels, but **only in a DM with the bot** — in a channel or thread the text goes to the agent as a normal message. Slack's own client intercepts a leading `/`, so type `!` instead (e.g. `!session list`); halo treats `!cmd` as `/cmd` and shows `!…` in its replies. The table below uses `/`:
127
128
 
128
129
  | Command | Effect |
129
130
  |---|---|
130
- | `/session <verb>` | Session lifecycle: `new` / `list` / `switch <n>` / `stop` / `interrupt` / `compact` / `context` |
131
+ | `/session <verb>` | Session lifecycle: `new` / `list` / `switch <n>` / `stop` / `interrupt` / `compact` / `context` / `info` |
131
132
  | `/agent <verb>` | Manage agents (`list` / `switch` / `desc` open to all; `delete` full; `create` / `update` via skill, full) |
132
133
  | `/skill <verb>` | Manage skills (`list` / `desc` open; `disable` / `enable` workspace; `delete` full; `create` / `update` via skill, full) |
133
134
  | `/workspace <verb>` | Workspace: `info` (all) / `switch <path>` (full) / `setup` / `tidy` (workspace) / `share` (full) |
135
+ | `/cron` `/acp` `/extension` | Skill-backed object commands (full access); `/evo [hint]` queues a self-evolution run |
134
136
  | `/help` | List commands — object commands show only the verbs you can run |
135
137
 
136
138
  ## Cron jobs targeting Slack
@@ -149,10 +151,10 @@ The search depends on the `users:read` / `channels:read` scopes added in Step 4.
149
151
  |---|---|
150
152
  | **"向此应用发送消息的功能已关闭"** banner on the DM page | Step 6 wasn't done — open App Home, enable Messages Tab, reload Slack |
151
153
  | Account created but no events arrive | Socket Mode token (`xapp-`) is wrong, or `connections:write` scope missing on it |
152
- | `not_authed` / `invalid_auth` error on Add Account | Bot token (`xoxb-`) typo, or you pasted the App token in the Bot field by mistake |
154
+ | `not_authed` / `invalid_auth` error on Add Bot | Bot token (`xoxb-`) typo, or you pasted the App token in the Bot field by mistake |
153
155
  | Bot replies to itself in a loop | Should never happen — halo drops all `bot_id`-tagged messages. If it does, file a bug |
154
156
  | Search dropdown empty in cron form | Missing `users:read` / `channels:read` scopes — add them, then Reinstall to Workspace |
155
- | Multiple halo instances reply to the same message | Only one process should hold the Socket Mode connection. Check `~/.halo/global/server.pid` for the lock |
157
+ | Multiple halo instances reply to the same message | Only one process should hold the Socket Mode connection. Check `~/.halo/global/server.lock` for the lock |
156
158
 
157
159
  ## Multi-workspace setup
158
160