@turmind/halo 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin-out/404.html +1 -1
- package/admin-out/_next/static/chunks/212-e217e00ce6cc2bdc.js +1 -0
- package/admin-out/_next/static/chunks/397-fc5a406d80d46ca7.js +1 -0
- package/admin-out/_next/static/chunks/app/page-635d9668d47a2b9e.js +1 -0
- package/admin-out/_next/static/css/57622245d7724ee0.css +3 -0
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +5 -5
- package/bundled-docs/design/cron.md +16 -3
- package/bundled-docs/design/goal-mode.md +141 -0
- package/bundled-docs/design/session.md +16 -0
- package/bundled-docs/design/ws.md +30 -3
- package/bundled-docs/dev/api.md +24 -2
- package/bundled-docs/dev/desktop-packaging.md +20 -0
- package/bundled-docs/dev/env.md +11 -1
- package/bundled-docs/dev/tools.md +56 -1
- package/bundled-docs/guide/cli.md +2 -2
- package/bundled-docs/requirements/chat.md +9 -0
- package/bundled-docs/requirements/command.md +12 -1
- package/dist/index.js +2318 -1176
- package/package.json +1 -1
- package/templates/agents/goal/AGENT.md +165 -0
- package/templates/agents/goal/agent.yaml +34 -0
- package/templates/schema.sql +3 -1
- package/admin-out/_next/static/chunks/212-170855bed19f67f3.js +0 -1
- package/admin-out/_next/static/chunks/307-d7a99f9a4a436ec3.js +0 -1
- package/admin-out/_next/static/chunks/app/page-c432ffb80510ae48.js +0 -1
- package/admin-out/_next/static/css/66df341f99220f5c.css +0 -3
- /package/admin-out/_next/static/{JcY4qrO0pTA-AmuD8SM1y → TmICbj6oGkbeEcs8tFjd3}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{JcY4qrO0pTA-AmuD8SM1y → TmICbj6oGkbeEcs8tFjd3}/_ssgManifest.js +0 -0
|
@@ -12,16 +12,30 @@ File: `packages/server/src/ws/handler.ts`
|
|
|
12
12
|
|
|
13
13
|
### Server-side keepalive
|
|
14
14
|
|
|
15
|
-
The server pings every connection at the WS protocol level every 10 s (keeps reverse-proxy idle timeouts from closing the socket). It tolerates **2 consecutive missed pongs** (~20-30 s of silence) before `ws.terminate()` — a single miss is routinely just laptop sleep/wake or a browser event-loop stall, not a dead peer.
|
|
15
|
+
The server pings every connection at the WS protocol level every 10 s (keeps reverse-proxy idle timeouts from closing the socket). It tolerates **2 consecutive missed pongs** (~20-30 s of silence) before `ws.terminate()` — a single miss is routinely just laptop sleep/wake or a browser event-loop stall, not a dead peer. Protocol-level pings serve the *server's* view only — browser JS never sees them — so the client runs its own **application-level** probe on top: it sends `{type:'__ping__'}` and the server answers `{type:'__pong__'}`, giving the client an inbound frame it can observe.
|
|
16
16
|
|
|
17
17
|
### Client-side liveness & reconnect
|
|
18
18
|
|
|
19
|
-
The browser client ([packages/admin/src/shared/ws-client.ts](../../../packages/admin/src/shared/ws-client.ts)) runs a 15 s self-check timer (`startLiveness`) that catches three half-dead states: socket stuck in `CONNECTING` >10 s, `CLOSED` without `onclose` firing, and
|
|
19
|
+
The browser client ([packages/admin/src/shared/ws-client.ts](../../../packages/admin/src/shared/ws-client.ts)) runs a 15 s self-check timer (`startLiveness`) that catches three half-dead states: socket stuck in `CONNECTING` >10 s, `CLOSED` without `onclose` firing, and a **zombie-OPEN** socket — `readyState === OPEN` but the underlying TCP is dead (laptop sleep, NAT/proxy idle timeout; the server terminated its side but the FIN never arrived). Zombies are proven by a missed round-trip: every tick sends an app-level `__ping__` which the server answers with `__pong__`, so a healthy link sees inbound traffic newer than the previous probe on every tick; **2 consecutive probes with no inbound reply** → force-close so the exponential-backoff reconnect (1 s → 30 s cap) runs. Miss-counting is probe-relative, not wall-clock, so background-tab timer throttling doesn't misread a healthy-but-idle link as dead. (An earlier `bufferedAmount`-drain heuristic was removed — a ~20-byte probe is handed to the kernel instantly even when the peer is gone, so it never detected anything; the kernel keeps retransmitting for ~15 min before `onclose` fires.)
|
|
20
|
+
|
|
21
|
+
Two OS/browser signals supplement the timer:
|
|
22
|
+
|
|
23
|
+
- **`online` event** → `reconnectIfStale(0)`: the NIC was provably down, so the socket is torn down unconditionally (a stalled `CONNECTING` socket included).
|
|
24
|
+
- **`visibilitychange`** → `reconnectIfStale(LINK_STALE_MS)` (45 s), double-gated: the tab must have been **hidden ≥ 5 min** AND inbound traffic must actually be stale. Ungated visibility/focus probes were removed once before — inside iframes (code-server preview, embedded admin) they fire constantly and tore down healthy connections in an endless loop. The hidden-duration gate keeps that fix (iframe flapping hides the tab for milliseconds, never minutes) while restoring coverage for wake-from-sleep on a zombie link where no `online` event ever fires; the staleness gate means a healthy connection (background tabs keep receiving WS traffic fine) sails through untouched.
|
|
20
25
|
|
|
21
26
|
**Auth expiry**: when the WS handshake itself is rejected (close before `onopen` — `verifyClient` returns 401 on an expired JWT cookie, but the browser WS API hides the HTTP status), the client probes `/api/auth/check`. On 401 it stops reconnecting and emits `_auth_expired`; the admin page listens and swaps to the login screen. Any other probe outcome (server restarting, network blip) falls through to normal backoff.
|
|
22
27
|
|
|
23
28
|
After a successful `_connected` event, all subscribers re-issue their session-resume messages: `subscribe` (chat / agent state) and `terminal:reattach` (PTY pool). Both are idempotent on the server.
|
|
24
29
|
|
|
30
|
+
### Chat delivery: ack / resend / dedup
|
|
31
|
+
|
|
32
|
+
Liveness probes bound how long a zombie lives, but a chat sent *into* the zombie window would still vanish silently (`ws.send()` on a zombie-OPEN socket reports nothing). Chat is the one message class that must survive this, so it rides a dedicated ack/resend protocol; everything else stays fire-and-forget.
|
|
33
|
+
|
|
34
|
+
- **Client** ([ws-client.ts](../../../packages/admin/src/shared/ws-client.ts)): every chat carries a client-generated `clientMsgId` and enters a pending-ack table (capacity 100 — past that the oldest entry is failed visibly) before transmission. Transmission is gated on freshness: if inbound has been silent > 30 s, the socket is torn down first and the chat rides the reconnect instead of being trusted to a suspect link. **5 s without `chat:ack`** → force-close, and the `onopen` flush retransmits every unacked entry in insertion order. After **3 transmit attempts** the client stops tearing the link down on that chat's behalf (rapid network flaps would otherwise turn one chat into endless connection churn) but the entry stays pending and still rides every flush. A **2 min wall-clock deadline** per entry is the final arbiter: every chat terminates either in an ack or in a visible `_chat_send_failed` (red "send failed" badge on the bubble + its empty placeholder converged) — no infinite lurking.
|
|
35
|
+
- **Server** ([handler.ts](../../../packages/server/src/ws/handler.ts)): ack semantics = *the message is in the session log*. Each `handleChat` branch calls `rememberChat` **synchronously right after `appendUserMessage`** (no `await` in between — an await there opens a double-append window against a racing resend), then replies `{type:'chat:ack', clientMsgId}` once enqueueing is done. Resends are deduped against a handler-scoped FIFO of the last 500 acked ids (handler-scoped because a resend arrives on a *new* connection): a duplicate is re-acked without re-appending. At-least-once resend + server dedup ≈ exactly-once in the session log.
|
|
36
|
+
- **Indicator**: the connection light is tri-state (`fresh` / `stale` / `down`, [use-websocket.ts](../../../packages/admin/src/shared/use-websocket.ts)) driven by inbound-traffic age rather than last-known socket state — `stale` (amber) at > 45 s of silence (3× probe interval), `down` (red) on close. A zombie link can no longer sit green.
|
|
37
|
+
- **Known boundary (accepted)**: the dedup FIFO is in-memory. If the server restarts inside the narrow window between "ack sent but lost in flight" and the client's resend, the new process re-appends the message and the agent runs it twice. The overlap is seconds-rare, and persisting the table would put a synchronous disk write on the chat hot path — not worth it unless observed in practice.
|
|
38
|
+
|
|
25
39
|
## Client → Server
|
|
26
40
|
|
|
27
41
|
Source: [handler.ts](../../../packages/server/src/ws/handler.ts) — top-level `switch (msg.type)` in the connection handler.
|
|
@@ -29,11 +43,13 @@ Source: [handler.ts](../../../packages/server/src/ws/handler.ts) — top-level `
|
|
|
29
43
|
| Type | Purpose |
|
|
30
44
|
|---|---|
|
|
31
45
|
| `subscribe` | Subscribe to a session (load history, re-attach detached) |
|
|
32
|
-
| `chat` | Send a user message (queued when the agent is busy) |
|
|
46
|
+
| `chat` | Send a user message (queued when the agent is busy); acked per `clientMsgId` — see [Chat delivery](#chat-delivery-ack--resend--dedup) |
|
|
47
|
+
| `__ping__` | Application-level liveness probe; server replies `__pong__` |
|
|
33
48
|
| `chat:stop` | Hard-abort the current generation (ends the turn, no re-run) → `stopUserSession` |
|
|
34
49
|
| `chat:interrupt` | Interrupt the in-flight turn now (aborts a command mid-run); the server then folds any queued messages into one follow-up turn → `interruptSession`. Admin chat esc maps to this. A compacting session cancels the compact instead (same as `chat:stop`). |
|
|
35
50
|
| `session:clear` | Non-destructive /session new: save the current, detach, create fresh (handled inline) |
|
|
36
51
|
| `session:delete` | Delete session files + cascade-delete descendants in SQLite (handled inline) |
|
|
52
|
+
| `exchange:delete` | Delete one exchange (a user turn + its responses): **soft-delete** in the UI log (`deleted: true` markers, kept visible/greyed) + **physical-delete** the whole turn from `rawMessages` (LLM context) → `deleteExchange`. Fields: `userOrdinal` (0-based index among *main-conversation* user turns, i.e. excluding `taskId` sub-agent messages — matches the admin's `isMainConversationMessage` count), optional `sessionId` / `projectId`. Rejected (→ `error`) while the session is running or compacting. See [session.md](session.md#exchange-deletion-soft-ui--hard-raw). |
|
|
37
53
|
| `command:<name>` | Route through shared `dispatchCommand` (see [command.md](command.md)); `/session compact` handled inline for UI callbacks |
|
|
38
54
|
| `terminal:start` | Spawn a new PTY |
|
|
39
55
|
| `terminal:input` | Send keystrokes |
|
|
@@ -44,6 +60,7 @@ Source: [handler.ts](../../../packages/server/src/ws/handler.ts) — top-level `
|
|
|
44
60
|
Optional `chat` fields:
|
|
45
61
|
- `images`: `Array<{data: base64, mimeType}>` — multimodal
|
|
46
62
|
- `agentId`: specify the agent this session should use
|
|
63
|
+
- `clientMsgId`: client-generated id for the ack/resend/dedup protocol (admin always sends it)
|
|
47
64
|
|
|
48
65
|
## Server → Client
|
|
49
66
|
|
|
@@ -72,10 +89,14 @@ Source: [event-processor.ts:48-97](../../../packages/server/src/ws/event-process
|
|
|
72
89
|
| Type | Source | Purpose |
|
|
73
90
|
|---|---|---|
|
|
74
91
|
| `state:snapshot` | handler.ts on connect | Initial state (agents, messages, sessionId) |
|
|
92
|
+
| `chat:ack` | `handleChat` | Chat with `clientMsgId` is persisted in the session log — releases the client's pending-ack entry (see [Chat delivery](#chat-delivery-ack--resend--dedup)) |
|
|
93
|
+
| `__pong__` | `__ping__` handler | Reply to the client's application-level liveness probe |
|
|
75
94
|
| `chat:queued` | `sendUserMessage` returning queued | User-message-queued notification |
|
|
76
95
|
| `file:changed` | WorkspaceWatcher · GitDirWatcher · `routes/git.ts` | File change notification (path + action). Three sources: (1) **WorkspaceWatcher** — recursive workspace watch, deliberately excludes `.git`; (2) **`routes/git.ts`** — every git mutation route re-broadcasts `path:'.git'` itself (the recursive watcher ignores `.git`); (3) **GitDirWatcher** — a non-recursive `.git`-dir watch for command-line git ops, *plus* a degraded "watch the workspace root for `.git` appearing" phase that fires `path:'.git'` on a terminal `git init`/`clone` so the Source Control entry auto-surfaces. See [source-control.md](../requirements/source-control.md#auto-refresh-no-polling). |
|
|
77
96
|
| `terminal:ready` / `terminal:output` / `terminal:exit` / `terminal:reattached` | TerminalManager | PTY output |
|
|
78
97
|
| `session:changed` | `SessionManager` (broadcast to all clients) | Root session list changed — re-fetch. Fires on root-session create *and* on each root turn `complete` (so channel-driven messages refresh the count/title/ordering, not just admin's own turns). |
|
|
98
|
+
| `session:switched` | handler.ts (this client only) | The server rebound this connection to a different session — see [switchTo rebind](#switchto-rebind--sessionswitched) |
|
|
99
|
+
| `goal:changed` | `writeGoalState` (`agents/goal-mode.ts`, broadcast to all clients) | Goal-mode state transition: `{goalSessionId, workerSessionId, status, round, maxRounds}`. Emitted on every goal state write (every transition routes through `writeGoalState`, so the push can never be forgotten) plus the goal-session-delete dissolve path. **No workspace marker** — the admin re-fetches through `GET /api/sessions/goal` under its active project, which naturally filters cross-workspace events. Drives the goal banner, worker input lock, and 🎯 badge refresh. See [goal-mode.md](goal-mode.md#admin-surface--ws). |
|
|
79
100
|
| `session:cleared` | session:clear handler | /session new complete |
|
|
80
101
|
| `session:compacted` | compact handler | Compaction complete |
|
|
81
102
|
| `compact:started` / `compact:summarizing` / `compact:done` | compact handler | Compaction progress |
|
|
@@ -109,6 +130,12 @@ UI state (messageLog / streamBuffer / turnToolCalls / tokens) belongs to Session
|
|
|
109
130
|
- `command:session` with `compact` verb — calls `sm.compactSession(sid, { onProgress })` directly for real-time progress feedback
|
|
110
131
|
- All other `command:*` — builds a shared `CommandContext` and routes through `dispatchCommand()` (see [command.md](command.md))
|
|
111
132
|
|
|
133
|
+
### switchTo rebind & `session:switched`
|
|
134
|
+
|
|
135
|
+
When a command result carries `switchTo` (e.g. `/new`, `/goal create`, `/goal resume`), the WS handler doesn't just report the new id — it **rebinds this client's event stream**: unsubscribe the old listener, set `client.sessionId`, `registerEventListener` on the target, then send `session:switched {sessionId}`. Without the rebind, streaming events from the switched-to session (e.g. the goal agent's intake greeting right after `/goal create`) would never reach the connection — the listener would still point at the old id.
|
|
136
|
+
|
|
137
|
+
The same mechanics run on the **goal-routing overlay** in `handleChat`: a chat aimed at a goal-bound worker is diverted to its goal session (`resolveGoalRoute`), the listener is rebound, and `session:switched` is sent. On receipt the admin clears its chat store, sets the new session id, and **re-subscribes** to pull the disk-seeded snapshot so the target session's existing transcript renders (`chat-handlers.ts`).
|
|
138
|
+
|
|
112
139
|
### Message flow
|
|
113
140
|
|
|
114
141
|
1. `chat` → check if the session exists → `SessionManager.createSession()` or `sendUserMessage()`
|
package/bundled-docs/dev/api.md
CHANGED
|
@@ -8,7 +8,7 @@ Auth: most `/api/*` routes require a valid JWT cookie (`halo_token`). Exceptions
|
|
|
8
8
|
|
|
9
9
|
| Method | Path | Purpose |
|
|
10
10
|
|---|---|---|
|
|
11
|
-
| GET | `/api/health` | Health check — returns `{status, timestamp, uptime, engine}` |
|
|
11
|
+
| GET | `/api/health` | Health check — returns `{status, timestamp, uptime, engine, version, gitSha}` (`gitSha`: short sha on source builds, `-dirty` suffix if tree modified, `null` on published bundles) |
|
|
12
12
|
|
|
13
13
|
## Authentication
|
|
14
14
|
|
|
@@ -64,8 +64,9 @@ Unified session log API — list + read session files across all agents.
|
|
|
64
64
|
| GET | `/api/sessions/logs/:id?projectId=` | Full session log (scans across agent dirs) |
|
|
65
65
|
| DELETE | `/api/sessions/logs/:id?projectId=` | Delete the session log |
|
|
66
66
|
| PATCH | `/api/sessions/logs/:id?projectId=` | Rename a session (admin-only) — updates the log file's title. Accepts any session id (root or sub-agent); the sidebar exposes the rename affordance on every row |
|
|
67
|
+
| GET | `/api/sessions/goal?projectId=` | Latest goal binding for the workspace (goal-mode banner / input-lock seed) — see [detail](#get-apisessionsgoalprojectidabs) |
|
|
67
68
|
|
|
68
|
-
The list endpoint returns flat metadata (id / agentId / agentName / title / timestamps / messageCount / parentSessionId / stoppedAt / contextTokens / totalOutputTokens). The frontend builds the tree from `parentSessionId
|
|
69
|
+
The list endpoint returns flat metadata (id / agentId / agentName / title / timestamps / messageCount / parentSessionId / stoppedAt / contextTokens / totalOutputTokens / goalSessionId). The frontend builds the tree from `parentSessionId`; `goalSessionId` (non-null on a goal-bound worker row) drives the 🎯 badge.
|
|
69
70
|
|
|
70
71
|
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.
|
|
71
72
|
|
|
@@ -527,6 +528,25 @@ Returns the full session file. If only `rawMessages` is present (sub-agent sessi
|
|
|
527
528
|
}
|
|
528
529
|
```
|
|
529
530
|
|
|
531
|
+
### GET `/api/sessions/goal?projectId=<abs>`
|
|
532
|
+
|
|
533
|
+
Source: `packages/server/src/routes/sessions.ts` (`findLatestGoal` in `agents/goal-mode.ts`)
|
|
534
|
+
|
|
535
|
+
Latest goal binding for the workspace — goals are serialized per workspace, so "the" goal is unambiguous. Refresh seed for the admin's goal banner / worker input lock: `goal:changed` WS pushes keep a live tab current, this endpoint restores state after a page reload. Returns `{ "goal": null }` when there is no goal or the latest one is `cleared` (a dismissed record, not a displayable state). See [design/goal-mode.md](../design/goal-mode.md).
|
|
536
|
+
|
|
537
|
+
```json
|
|
538
|
+
// 200
|
|
539
|
+
{
|
|
540
|
+
"goal": {
|
|
541
|
+
"goalSessionId": "goal_mabc123",
|
|
542
|
+
"workerSessionId": "sid_abc",
|
|
543
|
+
"status": "running", // intake | running | paused | halted | done
|
|
544
|
+
"round": 3,
|
|
545
|
+
"maxRounds": 50
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
```
|
|
549
|
+
|
|
530
550
|
### GET `/api/settings/schema?projectId=<abs>`
|
|
531
551
|
|
|
532
552
|
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.
|
|
@@ -634,6 +654,7 @@ Full WS protocol in [design/ws.md](../design/ws.md). The four high-traffic clien
|
|
|
634
654
|
"projectId": "/abs/ws",
|
|
635
655
|
"message": "hello",
|
|
636
656
|
"agentId": "default", // optional override
|
|
657
|
+
"clientMsgId": "abc123", // optional — ack/resend dedup id
|
|
637
658
|
"images": [ { "data": "<base64>", "mimeType": "image/png" } ] // optional
|
|
638
659
|
}
|
|
639
660
|
```
|
|
@@ -643,6 +664,7 @@ Server behaviour ([handler.ts `handleChat`](../../../packages/server/src/ws/hand
|
|
|
643
664
|
- Persists pasted images to `<ws>/.halo/web/inbound/<date>/`
|
|
644
665
|
- 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
|
|
645
666
|
- Queues if busy/compacting; otherwise runs the agent turn
|
|
667
|
+
- 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))
|
|
646
668
|
- Streams events back via `message`, `tool_call`, `usage`, etc.
|
|
647
669
|
|
|
648
670
|
### `subscribe` (C→S)
|
|
@@ -393,6 +393,26 @@ Still good practice: build server + admin **before** `pnpm dist:arm64`.
|
|
|
393
393
|
The real per-target artifacts are `Halo-0.1.5.dmg` (x64) and `Halo Setup
|
|
394
394
|
0.1.5.exe` (win); only trust the arm64 dmg from an actual `dist:arm64` run.
|
|
395
395
|
|
|
396
|
+
- **npm packages can hide ~200-char relative paths that break every Windows
|
|
397
|
+
upgrade (MAX_PATH) — jimp ships its jest snapshots.** `@jimp/*` publishes
|
|
398
|
+
`src/__image_snapshots__/*.png` with ~150-char *file names*; staged under
|
|
399
|
+
`resources/{server,cli}-runtime/node_modules/`, the longest INSTDIR-relative
|
|
400
|
+
path hit 200 chars. Install works (`C:\Users\<u>\AppData\Local\Programs\Halo\`
|
|
401
|
+
+ 200 stays under 260), but **upgrade doesn't**: the old uninstaller's
|
|
402
|
+
`atomicRMDir` renames every file into `%TEMP%\ns<x>.tmp\old-install\<relpath>`
|
|
403
|
+
(a *longer* prefix, ≈253 + username), crosses MAX_PATH, aborts → non-zero exit
|
|
404
|
+
→ the installer shows the misleading "Halo cannot be closed. Please close it
|
|
405
|
+
manually" prompt (`appCannotBeClosed` is its catch-all for *any* uninstaller
|
|
406
|
+
failure — no process check involved, which is why it fires on a fresh boot)
|
|
407
|
+
then "Failed to uninstall old application files". Users "fixed" it by deleting
|
|
408
|
+
the install dir — that removes `uninstall.exe`, so the installer silently
|
|
409
|
+
skips the uninstall step. Fixed in `stage-runtime.mjs pruneImageSnapshots()`
|
|
410
|
+
(full + fast paths — fast reuses old trees the deps fingerprint doesn't
|
|
411
|
+
re-check): drops all `@jimp/*/src/__image_snapshots__` from both runtimes.
|
|
412
|
+
Rule of thumb: after adding any dependency, check the staged tree with
|
|
413
|
+
`find resources/{server,cli}-runtime -type f | awk '{print length($0)}' |
|
|
414
|
+
sort -rn | head -1` — keep the longest relative path well under ~200.
|
|
415
|
+
|
|
396
416
|
## Verifying a build
|
|
397
417
|
|
|
398
418
|
```bash
|
package/bundled-docs/dev/env.md
CHANGED
|
@@ -11,6 +11,16 @@
|
|
|
11
11
|
- Password: set via `halo setup` (stored as scrypt hash in `~/.halo/secrets/config.yaml server.password`) **or** `HALO_PASSWORD` env (plaintext — preferred for Docker / CI). Env wins over hash if both are set.
|
|
12
12
|
- Workspace path: any absolute directory you want to bind
|
|
13
13
|
|
|
14
|
+
## Dev mode (hot reload)
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm run dev # root script: runs all packages' dev in parallel
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- **admin**: `next dev` on :3000 (`next start` is not an option — it rejects `output: 'export'`); API/WS requests are proxied to :9527 via `next.config.ts` rewrites, so the server must be running too
|
|
21
|
+
- **Monaco in dev**: `copy-monaco.mjs --dev` stages `min/vs` into `packages/admin/public/monaco/` (gitignored) so `/monaco/vs` resolves on :3000; the production build path (`out/monaco/vs`) is unchanged
|
|
22
|
+
- **server**: `tsx watch src/index.ts` on :9527
|
|
23
|
+
|
|
14
24
|
## Deploy commands
|
|
15
25
|
|
|
16
26
|
### Frontend build (web only)
|
|
@@ -91,7 +101,7 @@ Source: `packages/server/src/config.ts`
|
|
|
91
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. |
|
|
92
102
|
| `HALO_FRONTEND_DIR` | `packages/admin/out` | — | Static frontend dir (resolved as absolute path from project root) |
|
|
93
103
|
| `HALO_MAX_CONTEXT_TOKENS` | `200000` | — | Model max context |
|
|
94
|
-
| `HALO_SHELL_TIMEOUT` | `
|
|
104
|
+
| `HALO_SHELL_TIMEOUT` | `600000` | `config.yaml timeout.shell_exec` | Shell command timeout (ms) |
|
|
95
105
|
| `HALO_WEB_FETCH_TIMEOUT` | `10000` | `config.yaml timeout.web_fetch` | web_fetch timeout |
|
|
96
106
|
| `HALO_MODEL_TIMEOUT` | `1800000` | `config.yaml timeout.model_request` | Per model-call wall-clock cap (ms, 30 min). A bare fetch has no default timeout, so a half-open connection would hang the request forever; on expiry the call aborts and the agent loop retries with backoff. |
|
|
97
107
|
| `HALO_SESSION_GRACE` | `300000` | `config.yaml timeout.session_grace` | WS-disconnect session grace |
|
|
@@ -68,7 +68,7 @@ Run a shell command. Full shell access.
|
|
|
68
68
|
|---|---|---|---|
|
|
69
69
|
| command | string | yes | Shell command |
|
|
70
70
|
|
|
71
|
-
Timeout
|
|
71
|
+
Timeout 600 s (`HALO_SHELL_TIMEOUT`). Max output 5 MB. The tool description surfaces the effective timeout to the agent and advises backgrounding (`nohup … &` + log polling) for longer tasks.
|
|
72
72
|
|
|
73
73
|
**Windows output encoding.** The Windows path (`sandbox.ts`) prepends `chcp 65001` so cmd built-ins (`echo`, …) emit UTF-8, then captures raw bytes (`encoding: 'buffer'`) and decodes them strict-UTF-8 with a GBK fallback. This is because native Win32 console tools (`ipconfig`, `systeminfo`, …) ignore `chcp` and still emit the OEM code page (GBK/CP936 on zh-CN); decoding such bytes as UTF-8 produced mojibake. The strict-UTF-8 attempt passes genuine UTF-8 through untouched and only falls back to GBK when the bytes aren't valid UTF-8 (GBK double-byte sequences almost always aren't). mac/Linux are unaffected (UTF-8 throughout).
|
|
74
74
|
|
|
@@ -286,6 +286,61 @@ Get an agent's full details: AGENT.md, model config, tools, skills. Use it befor
|
|
|
286
286
|
|
|
287
287
|
Returns: full SKILL.md content (body + resource files list). For progressive disclosure — the system prompt only contains skill metadata (name + description); the agent calls this tool on demand.
|
|
288
288
|
|
|
289
|
+
## Goal tools
|
|
290
|
+
|
|
291
|
+
Injected **only** for the built-in `goal` agent (G) — `session-agent-builder` swaps in this set (`buildGoalTools`, `agents/goal-mode.ts`) instead of the standard session bundle when the session's agent is `GOAL_AGENT_ID`. Every callback re-reads goal state from the workspace db (never a cached copy), so a halt / pause / clear that landed while G was mid-turn is enforced on its next tool call. See [design/goal-mode.md](../design/goal-mode.md).
|
|
292
|
+
|
|
293
|
+
### goal_context
|
|
294
|
+
|
|
295
|
+
Load the goal binding: worker session id, goal dir, `GOAL_SPEC.md` path, caps, status, round, counters (`delegatedCount`/cap, `noProgress`, `startedAt`/`elapsed`). No arguments. Call first in every conversation and after any restart nudge.
|
|
296
|
+
|
|
297
|
+
During `intake` the result also embeds `workerRecent` — the worker's last 20 non-empty user/assistant messages (transcript `role=system` noise skipped, 400 chars each, 8K total budget applied newest-first) plus `workerMessageCount` — so G seeds the intake conversation without parsing transcript files. Running goals don't embed it (G works off delivered round reports; embedding on every call would burn tokens).
|
|
298
|
+
|
|
299
|
+
### goal_attach
|
|
300
|
+
|
|
301
|
+
The hinge from intake conversation to running loop. Preconditions: status `intake` and `GOAL_SPEC.md` written to the goal dir (missing → error naming the expected path). Stamps the spec sha256, records the worker's output-token baseline, applies cap overrides, flips to `running`, and dispatches the kickoff to the worker under a `[Goal work order · round 1/N]` header. Call exactly once, only after the user confirms the contract.
|
|
302
|
+
|
|
303
|
+
| Arg | Type | Required | Description |
|
|
304
|
+
|---|---|---|---|
|
|
305
|
+
| `kickoff` | string | yes | Round-1 work order, sent verbatim (header prepended by the platform) |
|
|
306
|
+
| `caps` | object | no | Overrides pinned during intake: `max_rounds` / `max_hours` / `max_tokens`; omitted fields keep defaults (10 rounds / 4h / no token budget) |
|
|
307
|
+
| `decision_policy` | string | no | One-line record of what kinds of forks the user delegated |
|
|
308
|
+
|
|
309
|
+
### goal_decide
|
|
310
|
+
|
|
311
|
+
Record a delegated decision — a fork G answered on the user's behalf because spec + scene made the answer clear. Writes `decision-<n>.md` to the goal dir **before** the answer is relayed; counts against a cap of 5 per goal (cap reached → error telling G to park the question to the user). Only while `running`.
|
|
312
|
+
|
|
313
|
+
| Arg | Type | Required | Description |
|
|
314
|
+
|---|---|---|---|
|
|
315
|
+
| `question` | string | yes | The fork the worker raised |
|
|
316
|
+
| `decision` | string | yes | What G decided |
|
|
317
|
+
| `rationale` | string | no | Why the contract/scene supports it |
|
|
318
|
+
|
|
319
|
+
### goal_finish
|
|
320
|
+
|
|
321
|
+
Final acceptance: `running → done`, dissolves the binding (clears the worker's back-pointer; the chat surface returns to the worker). G then writes the final report as its reply — it flows to the user and must list every delegated decision.
|
|
322
|
+
|
|
323
|
+
| Arg | Type | Required | Description |
|
|
324
|
+
|---|---|---|---|
|
|
325
|
+
| `summary` | string | yes | One-line result recorded in the goal state |
|
|
326
|
+
|
|
327
|
+
### query_session (goal-scoped)
|
|
328
|
+
|
|
329
|
+
G's **lateral edge**: same name as the standard session tool, different implementation — only the bound worker is reachable, only while `running` (any other status → `lateral edge revoked`), and a `[Goal work order · round N/cap]` header is prepended in code. Halting a goal revokes this edge, which is what makes runaway impossible.
|
|
330
|
+
|
|
331
|
+
| Arg | Type | Required | Description |
|
|
332
|
+
|---|---|---|---|
|
|
333
|
+
| `session_id` | string | yes | Must be the bound worker session id |
|
|
334
|
+
| `message` | string | yes | The work order / relayed answer / steering update |
|
|
335
|
+
|
|
336
|
+
### get_session_output (goal-scoped)
|
|
337
|
+
|
|
338
|
+
Read the full latest-turn output of the worker or any session in the worker's subtree (evidence gathering — round reports are truncated at `limits.autoReportMax`). Scoped to the worker's tree; works regardless of goal status.
|
|
339
|
+
|
|
340
|
+
| Arg | Type | Required | Description |
|
|
341
|
+
|---|---|---|---|
|
|
342
|
+
| `session_id` | string | yes | Worker session id or a descendant (`worker>child`) id |
|
|
343
|
+
|
|
289
344
|
## Self-review tool
|
|
290
345
|
|
|
291
346
|
### draft
|
|
@@ -97,7 +97,7 @@ halo cli -w /path/to/review-ws "review the diff" --format json | jq '.text'
|
|
|
97
97
|
# Pipe content in
|
|
98
98
|
git diff HEAD~1 | halo cli -w /path/to/review-ws "review this diff"
|
|
99
99
|
|
|
100
|
-
# Exit code: 0 = success, 1 = error, 130 = SIGINT
|
|
100
|
+
# Exit code: 0 = success, 1 = error, 130 = SIGINT, 143 = SIGTERM (graceful: session repaired + flushed before exit)
|
|
101
101
|
halo cli "check for issues" && echo "OK" || echo "FAILED"
|
|
102
102
|
```
|
|
103
103
|
|
|
@@ -192,7 +192,7 @@ Attach files or images to your message with `@file` and `@image`, or pull a dire
|
|
|
192
192
|
```
|
|
193
193
|
packages/cli/
|
|
194
194
|
src/
|
|
195
|
-
index.ts — Entry: subcommand dispatch (tui/cli/agents/sessions/server/setup/acp), SIGINT handling
|
|
195
|
+
index.ts — Entry: subcommand dispatch (tui/cli/agents/sessions/server/setup/acp), SIGINT/SIGTERM handling
|
|
196
196
|
harness.ts — Shared agent harness wrapping SessionManager
|
|
197
197
|
cli.ts — Non-interactive: stdin/args → agent → stdout → exit
|
|
198
198
|
tui.tsx — Interactive entry: mounts the ink app
|
|
@@ -17,6 +17,12 @@ The primary surface for talking to an agent.
|
|
|
17
17
|
- Sub-agent messages carry the agent-name label (e.g. "Coder", "Researcher")
|
|
18
18
|
- Streaming text has a cursor animation
|
|
19
19
|
|
|
20
|
+
### User-message actions (Copy / Delete / Show)
|
|
21
|
+
Hover actions on the blue sticky user bubble — real user prompts only (sub-agent report / compact-summary callouts, though user-role, get no buttons):
|
|
22
|
+
- **Copy** — copies the prompt text to the clipboard
|
|
23
|
+
- **Show more/less** — the existing clamp toggle, shown when the text overflows
|
|
24
|
+
- **Delete** (confirm dialog) — removes the whole exchange (the user turn + all responses up to the next user turn) with **two-layer semantics**: the LLM context (`rawMessages`) drops the turn physically — the model never sees it again, freeing context; the UI keeps the messages, rendered greyed-out with a "deleted" tag, as an audit trail. No undo; a deleted exchange loses its Delete button. Rejected with an error toast while the agent is running or compacting. Root sessions only (sub-session logs don't offer Delete). If the turn was already compacted out of raw context, only the UI marking happens (silent degrade). Design details in [design/session.md](../design/session.md#exchange-deletion-soft-ui--hard-raw), protocol in [design/ws.md](../design/ws.md).
|
|
25
|
+
|
|
20
26
|
### Slash commands
|
|
21
27
|
|
|
22
28
|
The full command list is fetched from `GET /api/commands` per session and includes built-ins + skill commands. The following are always-present highlights for the chat surface:
|
|
@@ -31,6 +37,9 @@ The full command list is fetched from `GET /api/commands` per session and includ
|
|
|
31
37
|
|
|
32
38
|
See [requirements/command.md](command.md) for the full command surface.
|
|
33
39
|
|
|
40
|
+
### Goal-mode banner
|
|
41
|
+
When a goal is bound to the current session (see [command.md → `/goal`](command.md) / [design/goal-mode.md](../design/goal-mode.md)), a strip above the composer shows status (intake / running round N/max / paused / halted / done); a label click jumps to the goal session, a `Worker →` button jumps back to the worker. Terminal states (done/halted) are dismissible — dismissal persists per-project in `localStorage` so it survives a page refresh; a new goal gets a different id and un-suppresses automatically. Active states (intake/running/paused) are not dismissible while the lock they explain is still in force.
|
|
42
|
+
|
|
34
43
|
### Graceful interrupt
|
|
35
44
|
Sending a new message while the agent is generating **does not** abort — the message goes to the server queue, the agent finishes the current turn at the next safe checkpoint (after a tool call), and then runs the queued message. Queueing multiple messages is supported; they run in order.
|
|
36
45
|
|
|
@@ -6,7 +6,7 @@ Unified command processing — all slash commands (built-in + skill) are dispatc
|
|
|
6
6
|
|
|
7
7
|
Source: [commands/index.ts](../../../packages/server/src/commands/index.ts) (descriptors) + [channels/shared/commands.ts](../../../packages/server/src/channels/shared/commands.ts) (execution)
|
|
8
8
|
|
|
9
|
-
Top-level built-in slash commands: `/help` `/evo` `/session` `/agent` `/skill` `/workspace`. (`/cron` and `/acp` look like top-level commands too but are provided by same-name skills — see the verb table below; they aren't registered in `commands/index.ts` or `DISPATCH_COMMANDS`.)
|
|
9
|
+
Top-level built-in slash commands: `/help` `/evo` `/session` `/agent` `/skill` `/workspace` `/goal`. (`/cron` and `/acp` look like top-level commands too but are provided by same-name skills — see the verb table below; they aren't registered in `commands/index.ts` or `DISPATCH_COMMANDS`.)
|
|
10
10
|
|
|
11
11
|
| Name | Slash | Type | Purpose |
|
|
12
12
|
|---|---|---|---|
|
|
@@ -23,9 +23,20 @@ The rest are noun-verb **object commands**: `/<obj> <verb> [args]`. Some verbs a
|
|
|
23
23
|
| `/workspace` | info (built-in, no gate) · switch (built-in, full) · setup / tidy (skill verb, workspace) · share (skill verb, full) — `/w` is a built-in alias (see [Command aliases](#command-aliases)) |
|
|
24
24
|
| `/cron` | create / list / update / enable / disable / delete — all skill verbs, full |
|
|
25
25
|
| `/acp` | kiro / claude (ask a local agent directly; question = rest of line) · add / list / remove (manage generated ask-* bindings) — all full |
|
|
26
|
+
| `/goal` | create \[description\] / status / pause / resume / clear — all built-in, **all full** (no skill fall-through; the `goal` agent is internal, not a skill). See [design/goal-mode.md](../design/goal-mode.md) |
|
|
26
27
|
|
|
27
28
|
Session lifecycle actions (`session:clear`, `session:delete`) are handled inline by the WS handler, not as slash commands.
|
|
28
29
|
|
|
30
|
+
### `/goal` verbs
|
|
31
|
+
|
|
32
|
+
All five verbs are gated `requiresAccess: full` — **including `status`** (user ruling): goal mode drives an autonomous multi-round loop (the goal session dispatches work orders that write files, run shell checks, and burn rounds of model budget), so no verb is exposed to workspace-level callers.
|
|
33
|
+
|
|
34
|
+
- `create [description]` — start goal intake on the current session (which becomes the worker; must be a root session and not itself a goal session). Refuses while a goal is active — goals are serialized per workspace — printing the active goal's status instead. Returns `switchTo` to the new goal session so the surface lands in the intake conversation.
|
|
35
|
+
- `status` — print the latest goal (any state): status, round/cap, elapsed, no-progress counter, delegated decisions, both session ids, halt reason if any.
|
|
36
|
+
- `pause` — running → paused, then stop the worker (cascading to its subtree) **and** the goal session. Paused lifts the routing overlay: the user talks to the worker directly (manual takeover).
|
|
37
|
+
- `resume` — paused → running, nudges the goal session to re-read spec + transcript and re-dispatch; returns `switchTo` to the goal session.
|
|
38
|
+
- `clear` — tear down the binding from any active state; worker + goal session stopped, surface returns to the worker. The goal record stays on the goal session's row as history.
|
|
39
|
+
|
|
29
40
|
### `/session info`
|
|
30
41
|
|
|
31
42
|
Built-in verb (no access gate beyond ownership). Prints the **session tree** for the caller's current root session — the root plus every descendant sub-agent, so the user can see the whole delegation fan-out at a glance:
|