@turmind/halo 0.1.9 → 0.2.1-bc7cf2b

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 (66) hide show
  1. package/README.md +1 -5
  2. package/admin-out/404.html +1 -1
  3. package/admin-out/_next/static/chunks/212-170855bed19f67f3.js +1 -0
  4. package/admin-out/_next/static/chunks/{503-7c6d9935df6e0cea.js → 307-d7a99f9a4a436ec3.js} +1 -1
  5. package/admin-out/_next/static/chunks/525.2c10a4fd329d0d24.js +1 -0
  6. package/admin-out/_next/static/chunks/app/layout-c2fa8035998e48d5.js +1 -0
  7. package/admin-out/_next/static/chunks/app/page-31317ef1fb0ba31f.js +1 -0
  8. package/admin-out/_next/static/chunks/{webpack-8bd7ef88a6527feb.js → webpack-47520cdc55be63e7.js} +1 -1
  9. package/admin-out/_next/static/css/66df341f99220f5c.css +3 -0
  10. package/admin-out/index.html +1 -1
  11. package/admin-out/index.txt +21 -20
  12. package/bundled-docs/design/agent.md +1 -1
  13. package/bundled-docs/design/evolution.md +11 -2
  14. package/bundled-docs/design/halo-city.md +31 -0
  15. package/bundled-docs/design/prompt-system.md +3 -3
  16. package/bundled-docs/design/session.md +26 -1
  17. package/bundled-docs/design/storage.md +13 -0
  18. package/bundled-docs/dev/acp-adapter.md +31 -4
  19. package/bundled-docs/dev/api.md +6 -4
  20. package/bundled-docs/dev/desktop-packaging.md +98 -8
  21. package/bundled-docs/dev/env.md +4 -2
  22. package/bundled-docs/dev/previews.md +3 -0
  23. package/bundled-docs/dev/tools.md +3 -1
  24. package/bundled-docs/guide/cli.md +40 -16
  25. package/bundled-docs/guide/getting-started.md +3 -2
  26. package/bundled-docs/requirements/editor.md +1 -1
  27. package/bundled-docs/requirements/explorer.md +11 -0
  28. package/dist/index.js +14102 -13249
  29. package/package.json +1 -2
  30. package/templates/INSTRUCTIONS.md +0 -4
  31. package/templates/agents/__apply_agent__/AGENT.md +41 -19
  32. package/templates/agents/__evo_agent__/AGENT.md +73 -45
  33. package/templates/agents/__evo_agent__/agent.yaml +9 -6
  34. package/templates/agents/__score__/AGENT.md +65 -33
  35. package/templates/agents/deep-executor/AGENT.md +23 -6
  36. package/templates/agents/deep-executor/agent.yaml +4 -1
  37. package/templates/agents/default/AGENT.md +32 -8
  38. package/templates/agents/executor/AGENT.md +19 -1
  39. package/templates/agents/executor/agent.yaml +6 -3
  40. package/templates/models/aws-bedrock-claude-invoke.yaml +53 -0
  41. package/templates/prompts/all/WORKSPACE_CONVENTIONS.md +5 -0
  42. package/templates/prompts/bootstrap/BOOTSTRAP.md +4 -0
  43. package/templates/prompts/root/WORKSPACE_MEMORY.md +2 -0
  44. package/templates/skills/acp/SKILL.md +8 -1
  45. package/templates/skills/acp/templates/SKILL.md.tmpl +2 -1
  46. package/templates/skills/acp/templates/ask.py +2 -1
  47. package/templates/skills/acp/templates/config.yaml.tmpl +5 -0
  48. package/admin-out/_next/static/chunks/298-d6b224b569da4de0.js +0 -1
  49. package/admin-out/_next/static/chunks/661.a4fe93f3f2a5e871.js +0 -1
  50. package/admin-out/_next/static/chunks/app/layout-58f773336e6fce61.js +0 -1
  51. package/admin-out/_next/static/chunks/app/page-cc8043335282d745.js +0 -1
  52. package/admin-out/_next/static/css/8fce4e1a54dac12a.css +0 -3
  53. package/bundled-docs/plans/cron-tasks.md +0 -362
  54. package/bundled-docs/plans/design-suggestions.md +0 -203
  55. package/bundled-docs/plans/issue-session-deep-nesting.md +0 -208
  56. package/bundled-docs/plans/launch-plan.md +0 -111
  57. package/bundled-docs/plans/md-and-agent-refactor.md +0 -197
  58. package/bundled-docs/plans/remote-mac-worker.md +0 -196
  59. package/bundled-docs/plans/roadmap.md +0 -40
  60. package/bundled-docs/plans/self-evolution.md +0 -1379
  61. package/bundled-docs/plans/todo.md +0 -32
  62. package/bundled-docs/plans/weixin-channel.md +0 -255
  63. package/bundled-docs/test/docs-seeding.md +0 -101
  64. package/bundled-docs/test/session.md +0 -374
  65. /package/admin-out/_next/static/{IBP_mwBDf-cPwZSZZGpi8 → iSrc6429pyTB0lK-EE5oH}/_buildManifest.js +0 -0
  66. /package/admin-out/_next/static/{IBP_mwBDf-cPwZSZZGpi8 → iSrc6429pyTB0lK-EE5oH}/_ssgManifest.js +0 -0
@@ -1,1379 +0,0 @@
1
- # Self-Evolution
2
-
3
- Frozen-design spec. Read the decision log at the bottom if a choice surprises you.
4
-
5
- ## Goal
6
-
7
- Let an active workspace **learn from its own conversations** — when an agent
8
- makes the same kind of mistake repeatedly, the user shouldn't have to remember
9
- to fix the prompt. An evolution agent looks at the session, drafts a fix, runs
10
- a sandbox dry-run to score it, and a separate apply agent merges approved
11
- patches back. All under user review.
12
-
13
- ## Levels
14
-
15
- Borrowing self-driving's L0-L5. Ship in order.
16
-
17
- | Level | What it does | Status |
18
- |---|---|---|
19
- | **L0** | Fully manual. User edits prompt files themselves. | Already supported |
20
- | **L1** | Human + LLM assist. `/note` and pre-compact trigger drafts; manual approve/reject; apply agent merges. | **MVP — this spec** |
21
- | L2 | Partial auto-apply for high-score patches. | Future |
22
- | L3 | Yaml/capability changes also auto-apply. | Future |
23
- | L4 | Create/archive agents and skills. Curator pass. | Long-term |
24
-
25
- L0 needs no code. L1 is what this spec covers. L2+ get a "Future levels" sketch.
26
-
27
- ---
28
-
29
- # L1 MVP
30
-
31
- ## Three components
32
-
33
- 1. **Two hidden agents** in `~/.halo/global/agents/`:
34
- - `__evo_agent__` — drafts patches and runs dry-run scoring
35
- - `__apply_agent__` — merges approved patches into target files
36
- Both are normal Halo agents with `internal: true` so they don't appear in
37
- `list_agents`, the chat agent selector, `/new`'s default pick, and have no
38
- Test button. They show up in admin's agent management (collapsed **Internal**
39
- group) for editing.
40
-
41
- 2. **Two queues in global db** (`evolution_runs`, `evolution_applies`)
42
- driven by a server-side ticker (every 30s). Ticker has zero in-memory state
43
- — looks at db status fields and timestamps.
44
-
45
- 3. **Per-task wrapper processes** that the ticker spawns. Each wrapper:
46
- - Runs as a Node child process (`packages/server/dist/evo-wrapper.js`)
47
- - Updates db heartbeat every 60s
48
- - Spawns the appropriate `halo cli -a __evo_agent__|__apply_agent__` to
49
- do the LLM work
50
- - Watches the cli's exit code and translates to db status
51
- - Owns its own evo-db handle (separate sqlite file at
52
- `~/.halo/global/evo.db`) — no need to call into the server process
53
- because the entire coordination surface (heartbeat, status flips,
54
- row claims) is just db writes
55
-
56
- 4. **Admin "Evolution" tab** for listing pendings, viewing patches, approving
57
- with optional reviewer hint, rejecting, and deleting finished runs
58
- (artifacts + DB row; blocked for in-flight states).
59
-
60
- ## File layout
61
-
62
- ```
63
- <ws>/.halo/evo/
64
- runs/<id>/ # one dir per evo run
65
- meta.json # { id, triggerKind, sourceSession, userHint, ... }
66
- source-snapshot.json # session messages frozen at trigger time
67
- tool-flow.md # tool_result-clipped view of the snapshot
68
- evo-context.json # prompt surface at trigger time (see below)
69
- images/<msgIdx>-<blockIdx>.<ext> # decoded image content blocks
70
- sandbox/ # cp of workspace .halo, mutated by evo
71
- sub-cli.log # tee'd stdout+stderr of every `halo cli`
72
- # the wrapper spawned (draft/fix/dry-run/score)
73
- # — single file, phase-headered for easy reading
74
- patch.md # evo agent writes
75
- score.json # __score__ writes
76
- .skip.md # evo writes if no patch is worth landing
77
- applies/<id>/ # one dir per apply
78
- meta.json # source_run_ids, reviewer_hint
79
- sandbox/ # apply agent works here, runs verification
80
- apply.log
81
- regress/<runId>/ # per-source-run regression scoring outputs
82
- history/apply-<id>/ # rollback snapshot before each apply
83
- MANIFEST.json # which paths were overwritten/created
84
- <files> # the previous content of those paths
85
- archive/ # archive job zips terminal runs/applies
86
- run-<id>.zip
87
- apply-<id>.zip
88
- ```
89
-
90
- `pending/` directory is gone. Pending list comes from `SELECT * FROM
91
- evolution_runs WHERE status='awaiting_review' AND archived_at IS NULL`
92
- in the global db. Patch content comes from `runs/<id>/patch.md`.
93
-
94
- `evo-context.json` (written by `enqueueEvoRun()` at trigger time) is the
95
- authoritative snapshot of the source agent's prompt surface — assembled
96
- system prompt, the workspace + global prompt files that go into every
97
- agent's system prompt (INSTRUCTIONS.md, USER.md, INDEX.md, the source
98
- agent's AGENT.md + agent.yaml, prompts/{all,root}/*), plus agent and
99
- skill id listings. Skill content (SKILL.md, sibling resources like
100
- wechat.md / telegram.md) is **not** inlined — listings only. The wrapper
101
- packs `evo-context.json` into briefs so evo doesn't need `file_read` to
102
- inspect the system-prompt files; for skills, it `grep`s / `file_read`s
103
- on demand using the new size-limited tools. Format:
104
-
105
- ```json
106
- {
107
- "agentId": "default",
108
- "assembledSystemPrompt": "<full text>", // null if session was cold
109
- "promptFiles": [
110
- { "scope": "workspace", "path": "INSTRUCTIONS.md", "content": "..." },
111
- { "scope": "global", "path": "agents/default/AGENT.md", "content": "..." },
112
- { "scope": "global", "path": "prompts/all/TOOL_GUIDELINES.md", "content": "..." },
113
- ...
114
- ],
115
- "agents": [{ "id": "default", "scope": "global" }, ...],
116
- "skills": [{ "id": "create-agent", "scope": "builtin" }, ...]
117
- }
118
- ```
119
-
120
- `tool-flow.md` is a clipped Markdown view of `source-snapshot.json` for
121
- fast skimming. User prose, assistant prose, and `tool_use` calls (with
122
- abbreviated `input`) are kept verbatim; each `tool_result` is replaced
123
- with a ~200-char peek plus the `is_error` flag. Sized to fit easily in
124
- context even for long sessions where grep/shell dumps would dominate.
125
- Evo reads this first to understand the *flow* (which tools ran in what
126
- order, what the user said between turns, whether each call succeeded);
127
- it falls back to the full `source-snapshot.json` (or its inherited
128
- message history) only when a specific tool result actually matters.
129
-
130
- `images/` holds decoded base64 image content blocks from
131
- `source-snapshot.json` (top-level or nested in tool_result.content). Same
132
- images visible to evo as vision blocks in inherited messages; the file
133
- paths exist so a patch can keep an image as a prompt resource by
134
- referencing the file. Video / audio / binary documents are not
135
- extracted (Halo doesn't currently produce non-image content blocks).
136
-
137
- `history/apply-<id>/` is the rollback safety net for `applied` runs and
138
- is **not** archived by the retention job — kept cheap and discoverable.
139
-
140
- `archive/` is created on demand by the archive daemon (see schema
141
- section above for retention policy). Per-row run/apply dirs are zipped
142
- in, then the original dirs removed.
143
-
144
- ## Global db schema
145
-
146
- ```sql
147
- CREATE TABLE evolution_runs (
148
- id TEXT PRIMARY KEY,
149
- workspace_path TEXT NOT NULL,
150
- status TEXT NOT NULL, -- pending | running | awaiting_review |
151
- -- approved | applied | rejected |
152
- -- skipped | failed | timeout
153
- trigger_kind TEXT NOT NULL, -- note | pre-compact
154
- source_session TEXT NOT NULL,
155
- user_hint TEXT, -- /note text + reviewer hint (replaced on retry)
156
- created_at INTEGER NOT NULL,
157
- started_at INTEGER,
158
- heartbeat_at INTEGER,
159
- completed_at INTEGER,
160
- applied_at INTEGER,
161
- failure_reason TEXT,
162
- attempts INTEGER NOT NULL DEFAULT 0,
163
- archived_at INTEGER -- 14d after terminal: zip + clear
164
- );
165
-
166
- CREATE TABLE evolution_applies (
167
- id TEXT PRIMARY KEY,
168
- workspace_path TEXT NOT NULL,
169
- status TEXT NOT NULL, -- pending | running | syncing | applied | failed | timeout
170
- source_run_ids TEXT NOT NULL, -- JSON array (one apply can fold N runs)
171
- reviewer_hint TEXT, -- constraints from approver
172
- created_at INTEGER NOT NULL,
173
- started_at INTEGER,
174
- heartbeat_at INTEGER,
175
- completed_at INTEGER,
176
- failure_reason TEXT,
177
- attempts INTEGER NOT NULL DEFAULT 0,
178
- archived_at INTEGER
179
- );
180
-
181
- CREATE INDEX idx_evo_runs_status ON evolution_runs(status, created_at);
182
- CREATE INDEX idx_evo_runs_ws ON evolution_runs(workspace_path);
183
- CREATE INDEX idx_evo_runs_archived ON evolution_runs(archived_at);
184
- CREATE INDEX idx_evo_applies_status ON evolution_applies(status, created_at);
185
- CREATE INDEX idx_evo_applies_ws ON evolution_applies(workspace_path);
186
- CREATE INDEX idx_evo_applies_archived ON evolution_applies(archived_at);
187
- ```
188
-
189
- ### Archive lifecycle
190
-
191
- `archived_at` is orthogonal to `status` — it tracks the disk-retention
192
- state, not the run's outcome. Two stages:
193
-
194
- 1. **14 days after a row hits a terminal status** (`applied`, `rejected`,
195
- `skipped`, `failed`, `timeout` for runs; `applied`, `failed`, `timeout`
196
- for applies), the archive daemon zips the row's artifact dir into
197
- `<workspace>/.halo/evo/archive/{run|apply}-<id>.zip`, deletes the
198
- original dir, sets `archived_at = now()`. The DB row stays.
199
- 2. **30 days after `archived_at`**, the archive daemon deletes the zip
200
- and the DB row outright.
201
-
202
- The daemon runs once at server boot (1 minute delay) and every 24 hours
203
- thereafter. Active rows (pending / running / awaiting_review / approved /
204
- syncing) are never archived. The `history/apply-<id>/` rollback tree is
205
- NOT archived — it stays cheap to discover for `applied` runs that may
206
- need rollback.
207
-
208
- **Manual delete** (`DELETE /evolution/runs/:id`) is a third, time-independent
209
- exit: it removes a run's artifacts (live run dir + archive zip, whichever
210
- exists) and its DB row immediately, skipping both time gates. Same active-row
211
- guard as archiving — `pending` / `running` / `approved` are rejected (409) so
212
- a live wrapper or queued apply isn't pulled out from under. Artifact removal
213
- goes through `removeRunArtifacts()` in `archive.ts`, which shares the path
214
- layout with the archive/purge helpers. Applies have no manual-delete path —
215
- they're consolidated under their source runs in the UI.
216
-
217
- The default API + admin views (`GET /evolution/runs`) exclude archived
218
- rows. A dedicated `GET /evolution/runs?archived=1` endpoint and
219
- admin "Archived" filter let the user inspect them.
220
-
221
- ## Realtime updates — WS broadcast (no client polling)
222
-
223
- The admin Evolution tab subscribes to `evolution:run_changed` and
224
- `evolution:apply_changed` events on the existing `/ws` channel. Server
225
- emits them at every state-change point:
226
-
227
- - REST mutations (approve/reject/retry) → broadcast directly with the
228
- new status. Latency is "instant" from the user's keystroke. Delete
229
- broadcasts `evolution:run_changed` with `kind:'deleted'` (no status) —
230
- the sidebar drops the row, an open detail pane clears its selection.
231
- - Wrapper child processes can't reach the parent's WSS handle, so the
232
- ticker (every 30s) diff'ies the db against an in-memory snapshot of
233
- last-seen statuses and broadcasts every change it finds. That covers
234
- `pending → running → awaiting_review/skipped/failed/timeout`
235
- transitions driven by the wrapper without any client poll.
236
-
237
- Broadcast fan-out via `ws/broadcast.ts` to every connected admin socket.
238
- Replaces an earlier `setInterval(refreshList, 5_000)` on every Evolution
239
- tab — same UX, ~zero idle traffic.
240
-
241
- ## Triggers (L1)
242
-
243
- ### `/note [text]`
244
-
245
- User in chat:
246
-
247
- ```
248
- /note # evo runs on current root session
249
- /note answer is too verbose # hint for what to look at
250
- ```
251
-
252
- Server flow (synchronous, completes in ~10ms):
253
-
254
- 1. Refuse if `level !== 'L1'` or user is `accessLevel === 'readonly'`.
255
- 2. Resolve current root session id (must be `parentId === null`).
256
- 3. mkdir `<ws>/.halo/evo/runs/<id>/`
257
- 4. Write `meta.json` and `source-snapshot.json` (deep clone of current
258
- `session.agent.messages`).
259
- 5. INSERT into `evolution_runs` with `status='pending'`, `user_hint=arg`.
260
- 6. Reply via `chat:system`: "📝 Queued for evaluation".
261
-
262
- That's all that happens at trigger time. No LLM call yet. Ticker picks it up.
263
-
264
- ### Pre-compact
265
-
266
- Inside `SessionManager.compactSession()`, **before the compression LLM call
267
- mutates `session.agent.messages`**:
268
-
269
- 1. Same checks (level=L1, source session is root, etc.).
270
- 2. **Synchronously snapshot** the current messages — same as `/note` step 3-4:
271
- mkdir runs/<id>/, write `meta.json` + `source-snapshot.json` (deep clone
272
- of `session.agent.messages` taken right now, before compact starts).
273
- 3. INSERT `evolution_runs` row with `trigger_kind='pre-compact'`,
274
- `user_hint=null`, `status='pending'`.
275
- 4. Continue with normal compact.
276
-
277
- Compact then proceeds asynchronously rewriting `session.agent.messages`,
278
- which is fine — evo will analyze the snapshot, not the live array. Order
279
- matters: snapshot first, compact second. Otherwise evo sees the
280
- already-compacted (= summarized + truncated) message log, which loses the
281
- detail we wanted evo to learn from.
282
-
283
- The "free LLM cache" angle that pre-compact originally had is gone with
284
- async — that's fine.
285
-
286
- ### Why not idle
287
-
288
- User-active feedback loops are short and useful; idle is "user has left,
289
- runs in dark, finds out hours later". Rejected.
290
-
291
- ## Ticker (server-side, every 30s)
292
-
293
- Stateless. Just queries db.
294
-
295
- ```
296
- 1. Mark timeouts:
297
- UPDATE evolution_runs SET status='timeout', failure_reason='heartbeat lost'
298
- WHERE status='running' AND heartbeat_at < now - run_timeout_minutes * 60_000
299
-
300
- UPDATE evolution_applies SET status='timeout' (same condition)
301
-
302
- 2. Start runs:
303
- running = SELECT count(*) FROM evolution_runs WHERE status='running'
304
- slots = max_concurrent_run - running
305
- for each pending run (oldest first, up to slots):
306
- spawn('node', ['<server-dist>/evo-wrapper.js', '--mode=run', '--id=<X>'],
307
- { detached: true, stdio: 'ignore' })
308
-
309
- 3. Start applies (same pattern):
310
- spawn('node', ['<server-dist>/evo-wrapper.js', '--mode=apply', '--id=<Y>'])
311
- ```
312
-
313
- Server doesn't track child processes in memory. If server restarts, the
314
- ticker re-evaluates db state on first tick:
315
-
316
- - `status='running'` rows with stale heartbeat → marked `timeout`
317
- - Pending rows continue to be picked up
318
-
319
- Wrappers detach from server — `--detach` so they survive server restarts.
320
-
321
- ## Wrapper (Node child process)
322
-
323
- `packages/server/src/evo-wrapper.ts` (compiled to `.js`).
324
-
325
- ```ts
326
- const { mode, id } = parseArgs() // mode: 'run' | 'apply', id: <X>
327
- const db = openGlobalDb()
328
-
329
- await db.update(...).set({ status: 'running', started_at: now() }).where(eq(id, X))
330
-
331
- // Heartbeat: every 60s
332
- const hb = setInterval(async () => {
333
- await db.update(...).set({ heartbeat_at: now() }).where(eq(id, X))
334
- }, 60_000)
335
-
336
- // Spawn the cli
337
- const cli = spawn('halo', [
338
- 'cli',
339
- '-a', mode === 'run' ? '__evo_agent__' : '__apply_agent__',
340
- '-w', workspacePath,
341
- buildBrief(mode, id, ...)
342
- ], { stdio: ['ignore', logFile, logFile] })
343
-
344
- cli.on('exit', async (code) => {
345
- clearInterval(hb)
346
- if (code === 0 && allOutputFilesExist()) {
347
- if (mode === 'run') {
348
- await db.update(...).set({ status: 'awaiting_review', completed_at: now() })
349
- } else {
350
- // apply post-processing, see "Apply post-processing" below
351
- await postApply(id)
352
- }
353
- } else {
354
- await db.update(...).set({ status: 'failed', failure_reason: ..., completed_at: now() })
355
- }
356
- process.exit(0)
357
- })
358
- ```
359
-
360
- > **Resolving the `halo` binary (cross-platform).** `resolveHaloCli()`
361
- > returns bare `halo` on macOS/Linux but **`halo.cmd` on Windows**: the
362
- > desktop installer drops `halo.cmd` (cli launcher) and `Halo.exe` (the
363
- > GUI) into the same dir on PATH, and PATHEXT ranks `.EXE` above `.CMD`, so a
364
- > bare `halo cli …` would launch the GUI — which relaunches the app and
365
- > grabs the global `server.lock` instead of running the cli. Spawning a
366
- > `.cmd` directly is rejected by Node ≥21.7 (EINVAL, CVE-2024-27980), so on
367
- > Windows the child is routed through `cmd.exe /c`. Override the whole thing
368
- > with `$HALO_CLI` for dev. The real code also passes the brief on **stdin**
369
- > (not as the trailing argv element shown above) to dodge the Windows
370
- > command-line length limit.
371
- >
372
- > On **macOS**, bare `halo` resolves via PATH to the launcher the "Install
373
- > 'halo' Command" menu drops in `/usr/local/bin`. But a desktop app launched
374
- > from Finder/Dock has only launchd's minimal PATH (no `/usr/local/bin`), so
375
- > the spawn would `ENOENT`. The Electron main process (`desktop/src/main.cjs`
376
- > `startServer()`) compensates by prepending the standard CLI dirs to the
377
- > server child's `env.PATH` — see `dev/desktop-packaging.md`.
378
-
379
- ### Wrapper logs (two streams, both surfaced in admin)
380
-
381
- The wrapper writes two log streams that the admin Evolution tab reads
382
- back lazily on detail fetch:
383
-
384
- - **`~/.halo/global/logs/evo/run-<id>.log`** — wrapper's own
385
- orchestration log. Phase headers, sub-cli command lines (last arg
386
- omitted to avoid leaking the whole brief), exit codes, finalize
387
- decisions. Cheap, always present once the wrapper starts.
388
-
389
- - **`<runDir>/sub-cli.log`** — `stdout` + `stderr` of every `halo cli`
390
- the wrapper spawned (draft, fix, dry-run, score), tee'd live to disk
391
- in a single file with `=== <ts> Phase ... ===` markers between
392
- sections. A single tail covers the whole run end-to-end. Captures the
393
- inner cli's actual error messages (e.g. Anthropic API rejections) that
394
- used to be buried in a 2 KB stderr-tail dump only emitted on non-zero
395
- exit. The wrapper still keeps the in-memory stdout for `dry-run-output.txt`
396
- on success — tee writes are additive, not redirective.
397
-
398
- GET `/api/evolution/runs/:id` returns both as `wrapperLog` /
399
- `subCliLog`. The admin detail panel renders them as collapsible
400
- `<pre>` blocks (default open under 4 KB, collapsed otherwise) for both
401
- successful and failed runs — successful runs benefit from seeing how
402
- long each phase took / whether the fix budget was used; failed runs
403
- need them to diagnose why.
404
-
405
- ### Apply post-processing
406
-
407
- After the apply cli exits 0:
408
-
409
- 1. Walk `applies/<id>/sandbox/.halo/` and find files that differ from main
410
- `<ws>/.halo/`. Those are what the apply agent decided to write.
411
- 2. Snapshot the changed paths from main `<ws>/.halo/` → `history/<ts>--apply-<X>/`.
412
- (Snapshot now, after we know what changed — saves space vs blanket cp at
413
- wrapper start.)
414
- 3. Copy each changed file from sandbox to main. Use Node `fs/promises`,
415
- not `rsync` or `cp` (cross-platform).
416
- 4. UPDATE `evolution_applies status='applied'`.
417
- 5. UPDATE all source_run_ids' `evolution_runs` rows to `status='applied'`.
418
-
419
- That's it. **No "release in-memory sessions" step** — Halo's session
420
- lifecycle makes it unnecessary. See [Session lifecycle: cold-start per
421
- turn](#session-lifecycle-cold-start-per-turn) for the full picture, but
422
- the short version: every session is evicted from the SessionManager
423
- cache as soon as its current message turn finishes. The next turn runs
424
- `buildAgentInstance` from scratch, which re-reads `agent.yaml` +
425
- `INSTRUCTIONS.md` + `USER.md` from disk. So the just-applied changes
426
- take effect on the next turn for free, and currently-running turns
427
- finish on the old prompts (deliberate — aborting a user mid-conversation
428
- would be a worse experience than a slight prompt-change delay).
429
-
430
- If sandbox doesn't exist or has no changes (apply agent decided nothing was
431
- worth writing): mark `complete` anyway with a note in `failure_reason`. The
432
- runs are still marked `applied` since the user's intent was satisfied (no
433
- change needed).
434
-
435
- If cli exits non-zero or in-sandbox tests failed: main workspace is
436
- untouched (apply agent wrote only to sandbox). Just mark `failed`.
437
-
438
- ## Platform override matrix (reference for evo / score)
439
-
440
- Both `__evo_agent__` and `__score__` need to reason about how prompt files
441
- load at runtime — what evo writes ends up in a workspace, and the scorer
442
- needs to know whether that workspace file actually overrides the global
443
- one. This is the canonical reference; AGENT.md files for both agents
444
- include the same table.
445
-
446
- | File / dir | Override rule |
447
- |---|---|
448
- | `INSTRUCTIONS.md` | Workspace `<ws>/.halo/INSTRUCTIONS.md` **fully suppresses** the global one. Subdirectory `INSTRUCTIONS.md` files (under `<ws>/<sub>/.halo/`) layer additively on top of the workspace root. |
449
- | `agents/<id>/` | **Whole-folder override.** Workspace `<ws>/.halo/agents/<id>/` exists → serves the agent entirely (both `AGENT.md` and `agent.yaml`), global folder ignored, no per-file fallback. |
450
- | `skills/<id>/` | **Whole-folder override.** Workspace `skills/<id>/` replaces the global skill wholesale — `SKILL.md` + every sibling resource. |
451
- | `prompts/all/`, `prompts/root/`, `prompts/bootstrap/` | **Whole-folder override.** If the workspace scope directory exists at all, the entire global directory for that scope is ignored. |
452
- | `USER.md` | Workspace replaces global. |
453
-
454
- **The whole-folder trap** (applies to `agents/`, `skills/`, `prompts/`
455
- alike): if a patch creates a workspace folder containing only the one file
456
- it edited, every *other* file the global folder had becomes invisible at
457
- runtime. To override one file inside such a folder, the patch must `cp` the
458
- entire global folder first. The sharpest case is an `agents/<id>/` left
459
- with only `AGENT.md` — `agent.yaml` is gone, so the agent has no model
460
- config. Mention this explicitly in any patch that touches a whole-folder
461
- target.
462
-
463
- Implementation: agent/skill folder resolution in
464
- `packages/server/src/agents/agent-loader.ts` (`agentSourceDir`,
465
- `skillSourceDir`), AGENT.md in `prompts/md-loader.ts` (`resolveMdPaths`),
466
- prompt scopes in `prompts/system-prompts.ts` (`resolvePromptsDir`).
467
- Behavior is whole-folder "first match wins" not per-file merge.
468
-
469
- ## What the agents see
470
-
471
- ### Run mode is split across THREE wrapper-orchestrated phases
472
-
473
- The wrapper, not the agent, drives the whole `mode=run` lifecycle. Three
474
- phases each spawn a fresh `halo cli` for one job; the wrapper owns all
475
- the sub-cli invocations against the sandbox so individual agents can't
476
- "go off-script" by retrying with edited prompts.
477
-
478
- ```
479
- Phase A (draft): halo cli -a __evo_agent__ -n -w <runDir>/sandbox "<draft brief>"
480
- Phase B (dry-run, retry-on-error):
481
- halo cli -a <triggeringId> -n --access workspace -w <runDir>/sandbox "<testMessage>"
482
- on failure:
483
- halo cli -a __evo_agent__ -n -w <runDir>/sandbox "<fix brief + error log>"
484
- Phase C (score): halo cli -a __score__ -n -w <runDir>/sandbox "<score brief>"
485
- ```
486
-
487
- All phases run inside `<runDir>/sandbox/` (whitelist-cp of the user
488
- workspace's prompt surface), so no agent_sessions rows or evo state
489
- ever touch the user's real workspace db.
490
-
491
- Three structural points apply across the phases:
492
-
493
- 1. **Snapshot reading via on-disk files (no session inheritance).**
494
- Each phase spawn is a fresh `-n` session. The drafter / fix / scorer
495
- read context from disk via `file_read`:
496
- - `<runDir>/source-snapshot.json` — full rawMessages (the source
497
- conversation)
498
- - `<runDir>/tool-flow.md` — clipped view, ~200-char peeks per
499
- tool_result with the `is_error` flag (read this first to skim)
500
- - `<runDir>/evo-context.json` — the prompt surface at trigger time,
501
- packed into the brief verbatim so the drafter doesn't have to
502
- re-read prompt files
503
-
504
- No internal session is staged, no `agent_sessions` row is written
505
- anywhere. The drafter and scorer are stateless across runs, and
506
- their stdout / stderr land in `<runDir>/sub-cli.log` for review.
507
-
508
- Earlier iterations of this design pre-staged the snapshot's
509
- rawMessages into a global internal-sessions JSON file and resumed
510
- the agent via `cli -s evo_<runId>` so the LLM "inherited" the full
511
- conversation. That worked for Anthropic-to-Anthropic flows but
512
- needed a sanitizer pipeline (thinking-block stripping, dangling
513
- tool_use repair, role-alternation merge, etc.) to make
514
- cross-provider rawMessages legal Anthropic input. The whole edifice
515
- was deleted in favor of `-n` + on-disk reads — same information
516
- reaches the agent, no replay-shape constraints to satisfy, no
517
- staged session to garbage-collect.
518
-
519
- And because all phase-A/A-fix/C invocations also `-w <runDir>/sandbox`
520
- instead of the user workspace, any incidental writes (e.g. a future
521
- internal agent that creates a sub-session) land in the sandbox's
522
- brand-new db, which gets discarded with the runDir.
523
-
524
- 2. **Prompt surface packed into the brief, plus on-demand reading.** The
525
- wrapper bakes the **system-prompt files** into the brief at trigger
526
- time (captured by `enqueueEvoRun()` into `<runDir>/evo-context.json`)
527
- so the agents never have to fetch them: the assembled system prompt
528
- the source agent was running under, every workspace + global
529
- INSTRUCTIONS.md / USER.md / INDEX.md, the source agent's AGENT.md /
530
- agent.yaml, prompts/all/*, prompts/root/*, plus id-only listings of
531
- every agent and skill in the workspace. Skill content (SKILL.md and
532
- sibling resource files) is **not** inlined — at scale that would blow
533
- up the brief regardless of which skill the patch actually touches.
534
-
535
- For everything not in the brief, both agents have a read-only toolkit
536
- they can use on demand: `file_read` (with offset/limit + 2 MB cap),
537
- `grep`, `glob`, `file_list`. Typical pattern: `glob` or `grep` to
538
- locate a specific SKILL.md → `file_read` it → reason → write the
539
- patch. The size guards keep this safe — pre-guard, a single
540
- `file_read` of an oversized file could consume tens of thousands of
541
- tokens.
542
-
543
- Writing toolkit:
544
- - **evo**: `file_write`, `file_edit` (write `patch.md` + sandbox
545
- target file; iterate on patch.md inline if needed). No
546
- `shell_exec` — evo doesn't execute anything; the dry-run is run by
547
- the wrapper, not by evo.
548
- - **scorer**: `file_write` only (writes `score.json`). No
549
- `file_edit` / `shell_exec` — the scorer is read-only on the
550
- workspace, write-only on `score.json`.
551
-
552
- 3. **Image dump.** Before phase A spawns, the wrapper walks the snapshot
553
- `rawMessages`, decodes every `type:'image'` content block (top-level
554
- or nested in tool_result.content) to
555
- `<runDir>/images/<msgIdx>-<blockIdx>.<ext>`. Brief includes a manifest
556
- listing each path with role / msgIdx / media_type / size / adjacent
557
- text. The same images are visible to the agent as vision blocks in
558
- message history; the file paths exist for the rare case where a patch
559
- needs to keep an image as a prompt resource.
560
-
561
- Video / audio / binary documents are NOT extracted — Halo currently
562
- has no `view_video` / `view_audio` tool, and snapshots therefore
563
- don't carry such blocks. Brief carries a generic "Non-text media note"
564
- instructing the agent to skip the run if the patch logically depends
565
- on understanding media internals it can't see.
566
-
567
- #### Phase A — `__evo_agent__` drafts
568
-
569
- The brief packs everything; AGENT.md describes the procedure.
570
-
571
- ```
572
- You are running as the Evolution drafter (mode: DRAFT).
573
-
574
- Run id: <X>
575
- Workspace: <ws>
576
- Working dir: <runDir>
577
- Trigger: note | pre-compact
578
- Reviewer hint: <if set>
579
-
580
- Your conversation history is the triggering agent's full message log.
581
- Scroll back to inspect what the user wanted, what the agent did, and
582
- where it diverged. The current prompt surface follows.
583
-
584
- [evo-context.json rendered:
585
- - assembled system prompt
586
- - prompt file dump (workspace + global)
587
- - agent + skill listing]
588
-
589
- [image manifest, if any images extracted]
590
-
591
- [non-text media note]
592
-
593
- === Task ===
594
- Decide what change in the prompt surface above would help next time.
595
- Three shapes, in order of preference: rewrite an existing rule;
596
- tighten / reorganize what's there; or, only when neither applies, add
597
- a new rule.
598
-
599
- Sandbox path: <runDir>/sandbox
600
-
601
- When you have a change worth making, write `patch.md` (frontmatter + body)
602
- and one new file at `<runDir>/sandbox/.halo/<target>` with the full new
603
- contents.
604
-
605
- When the conversation has no signal, or your patch logically requires
606
- understanding media you can't see, write `<runDir>/.skip.md` with a one
607
- or two sentence reason. .skip.md is the LAST file you write — wrapper
608
- checks it after patch.md, takes it as your final word.
609
-
610
- [language clause]
611
- ```
612
-
613
- The agent has only `file_list` and `file_write`. It can't `file_read` the
614
- sandbox, but the brief already has every prompt file content; the only
615
- reason `file_list` exists is to verify a sandbox subdirectory before
616
- writing into it.
617
-
618
- #### Phase B — wrapper runs the dry-run, calls evo back to fix on error
619
-
620
- ```
621
- timeout 180 halo cli -a <patch.testScenario.agentId>
622
- -n --access workspace
623
- -w <runDir>/sandbox
624
- "<patch.testScenario.testMessage>"
625
- ```
626
-
627
- `--access workspace` is critical: bwrap masks `~/.aws`, `~/.ssh`,
628
- `~/.kube`, `~/.docker`, `~/.gnupg`, `~/.halo/secrets` so a probe
629
- mentioning AWS / cloud / system credentials can't actually fire side
630
- effects. File and shell ops inside the sandbox itself are unaffected.
631
- Same `--access workspace` is used in apply mode's regression dry-runs.
632
-
633
- Outcomes:
634
- - **Exit 0 + non-empty stdout:** save to `<runDir>/dry-run-output.txt`,
635
- proceed to phase C.
636
- - **Anything else** (non-zero, empty, timeout): save the failure detail to
637
- `<runDir>/dry-run-fail-<n>.log`, then re-spawn `__evo_agent__` resuming
638
- the SAME `evo_<runId>` session with a **fix brief**. The fix brief
639
- inlines the failure log content (no `file_read` needed). The agent
640
- remembers its own draft from the resumed messages and decides what to
641
- change. After it exits, the wrapper retries the dry-run.
642
-
643
- `FIX_BUDGET = 1` — exactly one corrective pass. If the second attempt
644
- still fails, the run is marked `failed` with
645
- `failure_reason='dry-run never succeeded'`. We deliberately don't loop:
646
- - Two-pass shape (original + one corrective edit) covers the common
647
- failure modes (bad yaml, malformed frontmatter, scope-too-aggressive
648
- test scenario) without spiralling.
649
- - More than one fix tends to look like the old "edit prompt and retry"
650
- anti-pattern — if a patch can't be made dry-run-clean in one
651
- corrective edit, it's signal that the patch should be rejected, not
652
- iterated on.
653
-
654
- #### Phase C — `__score__` scores
655
-
656
- The brief packs everything; the scorer has only `file_write`.
657
-
658
- ```
659
- You are running as the Evolution scorer.
660
-
661
- Run id: <X>
662
- Workspace: <ws>
663
- Working dir: <runDir>
664
-
665
- Your conversation history is the original conversation the patch was
666
- drafted from. The current prompt surface and patch context follow.
667
-
668
- [evo-context.json rendered]
669
-
670
- === patch.md ===
671
- [full content]
672
-
673
- === dry-run-output.txt ===
674
- (stdout from the wrapper running the patched sandbox against
675
- testScenario.testMessage. Two execution constraints to keep in mind:
676
- 1. Sandbox runs under --access workspace, so AWS / system-credential
677
- operations are expected to fail with "Unable to locate credentials".
678
- Score on what KIND of response the agent gave, not on whether
679
- external side effects fired.
680
- 2. Inline images are visible vision blocks; video / audio / binary
681
- documents are not. Patches whose value depends on media internals
682
- naturally show low-signal dry-runs and that's correctly reflected
683
- in `confidence: low` rather than punished via behavior score.)
684
- [full content]
685
-
686
- === meta.json ===
687
- [full content]
688
-
689
- === Task ===
690
- From patch.md frontmatter, take testScenario.originalMessage and find the
691
- assistant turn that immediately followed it in your message history —
692
- that's the baseline. Compare baseline to dry-run-output.txt, rate lint /
693
- behavior / scope, write `<runDir>/score.json`.
694
-
695
- [language clause for the "notes" field]
696
- ```
697
-
698
- Splitting drafter vs scorer means the scorer doesn't have ego attached to
699
- the patch. The same scorer is reused at apply time (phase 11) to validate
700
- the merged sandbox before main is touched.
701
-
702
- #### Why `halo cli` and not `start_session`?
703
-
704
- `start_session` reuses the parent session's SessionManager state — the
705
- sub-agent reads prompts from the real workspace, not the sandbox, so the
706
- patch never actually loads. `halo cli -w <sandbox>` runs in its own
707
- process and reads `<sandbox>/.halo/` as the workspace, so the patch is
708
- genuinely live in the test.
709
-
710
- #### Brief design — consequence framing, not negation
711
-
712
- The briefs and the AGENT.md files for `__evo_agent__` / `__score__` /
713
- `__apply_agent__` use **consequence/affordance language** rather than
714
- imperative or negation phrasing:
715
-
716
- - "Reading the README first anchors your questions" not "Don't open with
717
- questions"
718
- - "Padding dilutes directness" not "Don't pad"
719
- - "Edits outside `<applyDir>/` would corrupt main workspace" not "Never
720
- edit outside `<applyDir>/`"
721
-
722
- The reasoning: imperative ("do X") and negation ("don't X") are weaker
723
- LLM signals than statements of consequence; the model treats negations
724
- especially poorly because the X token still elevates that action's
725
- probability. **Hard constraints belong in the tool layer**, not the
726
- prompt — the evo agent literally cannot run `halo cli` because
727
- `shell_exec` isn't in its tool set, so the prompt doesn't need to say
728
- "do NOT spawn halo cli". Same for "don't write score.json" (no
729
- `file_write` outside the sandbox is enforced by sandbox path validation,
730
- not prompt).
731
-
732
- What stays as prompt-level guidance: style preferences, work shape,
733
- optional / discretionary boundaries that benefit from the agent reasoning
734
- about edge cases.
735
-
736
- Mode-specific bits:
737
- - **Draft brief** carries `Trigger`, optional `Reviewer hint` (replaces
738
- `User hint` on retry — see retry policy below).
739
- - **Fix brief** carries `Fix attempt: 1/1` and the failure log content
740
- inline (no separate `file_read`).
741
- - **Score brief** is the largest because it inlines patch.md +
742
- dry-run-output.txt + meta.json + the prompt surface.
743
-
744
- ### Apply mode is wrapper-orchestrated, mirrors run mode
745
-
746
- Same shape as `mode=run`: wrapper drives, agents do focused work,
747
- `__score__` is reused as a regression gate. Key difference is the
748
- wrapper, not the apply agent, builds the sandbox **and** runs the
749
- per-source-run validation loop (the apply agent only merges).
750
-
751
- ```
752
- Phase A' (merge): halo cli -a __apply_agent__ -n -w <ws> "<merge brief>"
753
- Phase B' (regress): for each source_run_id Xi:
754
- halo cli -a <patch.testScenario.agentId> -n -w <applyDir>/sandbox \
755
- "<patch.testScenario.testMessage>"
756
- halo cli -a __score__ -n -w <ws> "<regress brief for Xi>"
757
- all scores OK → proceed to sync (phase 12)
758
- any score regresses → mark apply failed, main untouched
759
- ```
760
-
761
- #### Apply working dir
762
-
763
- ```
764
- <ws>/.halo/evo/applies/<applyId>/
765
- meta.json # { source_run_ids: [...], reviewer_hint }
766
- sandbox/.halo/ # built by wrapper from main <ws>/.halo/, edited by apply agent
767
- regress/<runId>/ # one dir per source run, written by phase B'
768
- dry-run-output.txt
769
- score.json
770
- apply.log
771
- ```
772
-
773
- The apply sandbox is **separate** from any evo run sandbox:
774
-
775
- - A reviewer can approve N runs into a single apply, so we'd have to
776
- combine N evo sandboxes anyway. Cleaner to start fresh.
777
- - `patch.md` may have been edited by the reviewer between approve time
778
- and apply time — apply must read the *latest* patch.md.
779
- - The main workspace may have changed during review (other apples
780
- applied, user edits). Apply should be based on **current** workspace
781
- state, not the evo run's frozen sandbox.
782
-
783
- #### Phase A' — `__apply_agent__` merges into a wrapper-built sandbox
784
-
785
- The **wrapper**, before spawning the apply agent, does the same
786
- whitelist cp evo's phase A does — but from the **current main
787
- workspace** into `<applyDir>/sandbox/.halo/`. Apply agent walks into
788
- that pre-built sandbox.
789
-
790
- ```
791
- Your job: merge N approved evolution patches into the apply sandbox.
792
- You do NOT run dry-runs and you do NOT score the merged result —
793
- the wrapper handles both, separately, using __score__.
794
-
795
- Apply id: <Y>
796
- Working dir: <ws>/.halo/evo/applies/<Y>/
797
- source_run_ids: [X1, X2, ...]
798
- Reviewer hint (may be empty): <text>
799
-
800
- Steps:
801
- 1. Read meta.json (source_run_ids + reviewer_hint).
802
- 2. For each source_run_id Xi, file_read <ws>/.halo/evo/runs/Xi/patch.md.
803
- Use the **latest** version — the reviewer may have edited it after
804
- approving.
805
- 3. Look at the wrapper-built sandbox at <applyDir>/sandbox/.halo/.
806
- Note which target files are present and their current content
807
- (which is the latest main-workspace content, not evo's frozen
808
- snapshot).
809
- 4. Apply each patch's changes to sandbox/.halo/<target>:
810
- - file_edit / file_write within the sandbox only.
811
- - Respect the platform override matrix (see above) — if a patch
812
- wants to change a file that currently lives only in
813
- ~/.halo/global/, you MUST materialize the workspace override
814
- first. For whole-folder targets (agents/<id>/, skills/<id>/,
815
- prompts/<scope>/), copy the *entire global folder* before editing
816
- one file — otherwise the workspace folder's mere existence
817
- suppresses every other file the global folder had (an agents/<id>/
818
- with only AGENT.md loses agent.yaml → no model config). For the
819
- single-file overrides (INSTRUCTIONS.md, USER.md), copy that file in
820
- first, then edit.
821
- - When two patches touch the same target, merge by intent: read both
822
- bodies, decide what the combined edit should look like. Don't try
823
- a textual diff merge.
824
- - reviewer_hint is a hard constraint that overrides individual
825
- patches: if it says "only apply X, skip Y", you skip Y entirely.
826
- - If patches are genuinely contradictory and reviewer_hint doesn't
827
- resolve it, write the conflict to apply.log and exit non-zero —
828
- the reviewer needs to split the apply.
829
- 5. Exit 0 once the sandbox reflects all approved + reconciled
830
- changes. The wrapper takes over from here.
831
- ```
832
-
833
- #### Phase B' — wrapper runs per-source regression check via `__score__`
834
-
835
- For each source run id `Xi`, the wrapper:
836
-
837
- 1. Reads `<ws>/.halo/evo/runs/Xi/patch.md` to get
838
- `testScenario.{agentId, testMessage, originalMessage}`.
839
- 2. Spawns `timeout 180 halo cli -a <agentId> -n -w
840
- <applyDir>/sandbox "<testMessage>"`.
841
- 3. Saves stdout to `regress/Xi/dry-run-output.txt`.
842
- 4. Spawns `__score__` with a regress-mode brief pointing at
843
- `regress/Xi/`. Scorer reads patch.md, the new dry-run output, and
844
- source-snapshot.json — same procedure as phase 9C but in apply
845
- context. Writes `regress/Xi/score.json`.
846
-
847
- A regression is **any score.json** with `behavior < 50` (clearly worse
848
- than baseline) or `lint < 50` (patched config doesn't load cleanly).
849
- Thresholds live in `general.evolution.apply_regression_*` settings
850
- (future); start with hardcoded 50/50.
851
-
852
- If all source runs pass the regression check → wrapper proceeds to
853
- phase 12 (final sync to main). If any regresses → wrapper marks
854
- `evolution_applies status='failed'`, main workspace untouched, source
855
- runs stay `approved` so the user can split or edit and try again.
856
-
857
- ## Patch file (`runs/<id>/patch.md`)
858
-
859
- Written by `__evo_agent__` in phase A. Natural-language description, NOT
860
- a diff. The frontmatter is structured (the wrapper parses it for the
861
- dry-run; the scorer parses it for baseline lookup); the body is for
862
- human review.
863
-
864
- ```markdown
865
- ---
866
- target: .halo/INSTRUCTIONS.md
867
- testScenario:
868
- agentId: default
869
- originalMessage: "做成csv文件"
870
- testMessage: "做一个 CSV 存到 /tmp/x.csv,含中文表头,给 Mac Excel 用"
871
- ---
872
-
873
- ## What to change
874
- …
875
- ## Why
876
- …
877
- ```
878
-
879
- Frontmatter keys stay English (they're contract — wrapper / scorer parse
880
- them). Body is in the user's language (`general.language`).
881
-
882
- ### `testScenario` — two messages, two purposes
883
-
884
- | Field | Used by | Constraint |
885
- |---|---|---|
886
- | `agentId` | wrapper (dry-run) + scorer (frontmatter parse) | id of the **triggering agent** from `source-snapshot.json` |
887
- | `originalMessage` | scorer | verbatim user message from `rawMessages` — the assistant turn that follows it is the "before" baseline |
888
- | `testMessage` | wrapper | clean self-contained probe **drafted by evo** to surgically exercise the patched rule. Should not depend on prior conversation context. |
889
-
890
- Why split them:
891
- - The wrapper's dry-run sub-cli is a fresh session with no prior turns —
892
- so the probe must be self-contained ("做一个含中文的 csv" works; "做成
893
- csv文件" needs a table from earlier in the chat that the sub-agent
894
- doesn't have).
895
- - The scorer needs to compare *the patched agent's reply on the probe*
896
- against *the original agent's reply on the original message* — which
897
- means it needs the original text, verbatim, to find the right baseline
898
- turn in the snapshot.
899
- - If `testMessage` and `originalMessage` happen to be identical clean
900
- probes, the drafter may write the same string twice — that's allowed.
901
- Most snapshot turns aren't clean probes, so they differ in practice.
902
-
903
- Back-compat: older patches (before the split) wrote a single `message`
904
- field. The wrapper falls back to using it for both meanings. New runs
905
- should always use the two-field shape.
906
-
907
- `score` is **not** in the frontmatter — it lives only in `score.json`,
908
- written by the scorer in phase C. Older runs may have a stale `score:`
909
- block from before this split; the wrapper / UI ignore it.
910
-
911
- The apply agent reads this whole markdown and decides how to actually merge
912
- it. There's no `operation: append/replace` field — the apply agent makes that
913
- call.
914
-
915
- ## Score (`runs/<id>/score.json`)
916
-
917
- Written by `__score__` in phase C — never by the drafter. Splitting
918
- drafter and scorer is the whole reason patches don't get inflated by the
919
- agent that proposed them.
920
-
921
- ```json
922
- {
923
- "lint": 90,
924
- "behavior": 75,
925
- "scope": 80,
926
- "confidence": "high",
927
- "avg": 82,
928
- "notes": "Sandbox sub-agent gave a more specific answer with citations. Minor risk of being too prescriptive on edge cases."
929
- }
930
- ```
931
-
932
- Each dim is 0-100. Anchor each at 50 = "neutral / no signal" so the
933
- scorer doesn't default to all-50 when unsure.
934
-
935
- - **lint** (0-100): did the patched prompt files load cleanly when the
936
- wrapper ran the dry-run? yaml valid, cross-refs resolve, no contradictions
937
- with sibling files. 100 = clean, 50 = unclear, 0 = patched config didn't
938
- load at all (dry-run failed even after fix attempts).
939
- - **behavior** (0-100): is the dry-run output (in `dry-run-output.txt`)
940
- better than the original assistant reply that followed
941
- `testScenario.message` in the snapshot? 100 = clearly better, 50 =
942
- indistinguishable / mixed, 0 = clearly worse.
943
- - **scope** (0-100): surgical (100) vs sweeping (0). Touching one workspace
944
- file with 3 lines = high; rewriting an agent's whole AGENT.md = low.
945
- - **confidence** (low/medium/high): scorer's own confidence in the call.
946
- Independent of the numeric scores — confidence "high" with all 50s is
947
- legal ("I'm sure this is a wash").
948
-
949
- `avg = round((lint + behavior + scope) / 3)` — single 0-100 number for
950
- sorting in the admin UI. Future L2 thresholds use this + confidence.
951
-
952
- The same scorer is reused at apply time (phase 11): after the apply agent
953
- merges N approved runs into a sandbox, the wrapper invokes the scorer
954
- again on each source run's `testScenario` to confirm the merged sandbox
955
- still produces the improvement (regression gate before main is touched).
956
-
957
- ## Admin Evolution tab
958
-
959
- A top-level tab. Shows:
960
-
961
- - **List**: rows from `evolution_runs` joined with `evolution_applies`
962
- (latest per source_run). Sortable by created_at, status, score.
963
- Filterable by status; default views exclude archived rows.
964
- - **Status filters** (sidebar buttons): `all`, `awaiting_review`,
965
- `pending`, `running`, `approved`, `applied`, `skipped`, `rejected`,
966
- `failed`, `timeout`, `archived`. The `archived` filter is a special
967
- pseudo-status: it triggers `GET /evolution/runs?archived=1` (different
968
- endpoint param, returns the archived list); all other filters operate
969
- on the active list.
970
- - **View**: opens patch.md, score.json, test scenario, the assembled
971
- brief context (so the reviewer can see what the agents saw), diff
972
- against current target.
973
- - **Approve** (visible when status=`awaiting_review`): opens dialog with
974
- optional reviewer_hint textarea. On submit:
975
- - UPDATE evolution_runs status='approved'
976
- - INSERT evolution_applies (source_run_ids=[X], reviewer_hint, status='pending')
977
- - **Reject** (awaiting_review): UPDATE evolution_runs status='rejected'.
978
- - **Add hint** (awaiting_review): UPDATE evolution_runs.user_hint += text.
979
- Status unchanged. Memo for the apply agent; doesn't trigger anything.
980
- - **Retry** (awaiting_review + every terminal status except `applied`
981
- and `archived`): opens dialog with required hint textarea. On submit:
982
- - UPDATE evolution_runs SET status='pending', attempts=0,
983
- started_at=NULL, heartbeat_at=NULL, completed_at=NULL,
984
- failure_reason=NULL, user_hint=<new hint>
985
- - Ticker picks it up on the next pass and re-spawns the wrapper.
986
- Hint is required — same input + no new direction would just reproduce
987
- the same output. Replaces user_hint (not append) because the new run
988
- is a fresh attempt.
989
-
990
- Endpoint: `POST /api/evolution/runs/:id/retry { hint }`. Refuses
991
- retry on `running` (would race the live wrapper) or `pending` (already
992
- queued).
993
-
994
- The user can also edit `runs/<id>/patch.md` directly (file_edit) before
995
- approving — apply agent re-reads at apply time.
996
-
997
- Multiple approvals on the same target: each Approve creates an
998
- independent `evolution_applies` row, runs serially. Future "batch
999
- apply" UI can group N approved runs into one apply row.
1000
-
1001
- ## Settings
1002
-
1003
- ```yaml
1004
- general:
1005
- language: en-US # en-US | zh-CN (BCP-47); globalOnly
1006
- evolution:
1007
- level: L0 # L0 | L1
1008
- max_concurrent_run: 1
1009
- max_concurrent_apply: 1
1010
- run_timeout_minutes: 5
1011
- apply_timeout_minutes: 5
1012
- max_attempts: 3 # how many times the ticker may try to spawn a wrapper
1013
- triggers:
1014
- pre_compact: true
1015
- ```
1016
-
1017
- L0 = disabled. L1 = enabled. Defaults are conservative; adjust if your
1018
- machine handles more.
1019
-
1020
- `/note` is always available when level=L1. Not in settings as a toggle.
1021
-
1022
- ### `general.language` (system-wide)
1023
-
1024
- Stored as a BCP-47 region tag. Single source of truth — the toolbar
1025
- language switcher in the admin UI reads and writes the same value.
1026
- Internal agents (`__evo_agent__`, `__score__`, future `__apply_agent__`)
1027
- skip platform prompts (see [Internal-agent isolation](#internal-agent-isolation))
1028
- so they wouldn't otherwise know what language the user prefers; the
1029
- wrapper resolves this setting and threads it into every brief it builds.
1030
-
1031
- `config.language` collapses BCP-47 to the simpler `'en' | 'zh'` shape
1032
- that the rest of the codebase already speaks (see `i18n.ts`'s `Lang`
1033
- type). New region tags would need a small map update there if we ever
1034
- add `ja-JP` etc.
1035
-
1036
- ## Retry
1037
-
1038
- Two flavors of retry — one automatic (heartbeat-driven), one reviewer-driven.
1039
-
1040
- ### Automatic (heartbeat timeout)
1041
-
1042
- Both `evolution_runs` and `evolution_applies` carry an `attempts` column.
1043
- Each `pending → running` claim atomically bumps `attempts += 1`.
1044
-
1045
- When heartbeat times out:
1046
- - `attempts < max_attempts` → flip back to `pending` (clear started_at /
1047
- heartbeat_at) so the next tick re-claims it (with attempts += 1 again).
1048
- - `attempts >= max_attempts` → terminal `timeout` with
1049
- `failure_reason: 'gave up after N attempts'`.
1050
-
1051
- A permanently broken task burns at most `max_attempts` wrapper attempts
1052
- before stopping, instead of looping forever.
1053
-
1054
- ### Reviewer-driven (admin Retry button)
1055
-
1056
- `POST /api/evolution/runs/:id/retry { hint }` resets the row:
1057
-
1058
- ```sql
1059
- UPDATE evolution_runs
1060
- SET status='pending', attempts=0,
1061
- started_at=NULL, heartbeat_at=NULL, completed_at=NULL,
1062
- failure_reason=NULL, user_hint=<new hint>
1063
- WHERE id=:id
1064
- ```
1065
-
1066
- Allowed from any terminal status (`failed`, `timeout`, `skipped`,
1067
- `rejected`, `awaiting_review`); refused on `running` (race with live
1068
- wrapper) or `pending` (already queued).
1069
-
1070
- The hint is required — empty hint means same input + same context, which
1071
- just reproduces the previous output. Hint replaces `user_hint` (not
1072
- append) because the new run is a fresh attempt against new direction;
1073
- the appended-memo path is `/hint` (separate endpoint, status unchanged).
1074
-
1075
- Retry on `applied` is intentionally not allowed — the patch already
1076
- landed in main; producing another patch over the same source needs a
1077
- new `/note` to anchor against the post-apply baseline.
1078
-
1079
- Retry doesn't bump a "retry counter" anywhere — the user can retry as
1080
- many times as they want. The reasoning: rate-limiting reviewer-driven
1081
- retries would push a frustrated user to delete-and-recreate the row,
1082
- which is more work and less observable.
1083
-
1084
- ## Internal-agent isolation
1085
-
1086
- Agents flagged `internal: true` in their `agent.yaml` (`__evo_agent__`,
1087
- `__score__`, future `__apply_agent__`) are **platform tooling, not
1088
- workspace assistants**. They should not inherit any workspace context:
1089
- no `INSTRUCTIONS.md`, no `USER.md`, no `INDEX.md`, no
1090
- `prompts/all|root|bootstrap`. Only their own `AGENT.md` plus the brief
1091
- the wrapper hands them.
1092
-
1093
- Why:
1094
- - **Token budget**: snapshots can run 100KB+; loading another 5–20KB of
1095
- workspace platform prompts on top is dead weight.
1096
- - **Behavior bleed**: workspace rules like "Be concise" or persona-shaping
1097
- prompts make the drafter / scorer second-guess procedure that's
1098
- supposed to be deterministic.
1099
- - **Cross-workspace consistency**: an evo run launched from workspace A
1100
- vs. workspace B should behave the same way; loading workspace prompts
1101
- would tilt evo toward whatever rules the *current* workspace happens
1102
- to have.
1103
-
1104
- Implementation: `SessionManager.startSession` checks `yamlConfig.internal
1105
- === true` and zeroes out `mdContents.{userMd, globalInstructions,
1106
- workspaceInstructionsChain, projectIndex, needsBootstrap}` plus
1107
- `systemPrompts.{bootstrap, all, root}` before composing the system
1108
- prompt. Internal agents still get tools, model config, prompt caching
1109
- etc. — only the platform-md surface is suppressed.
1110
-
1111
- ## Session lifecycle: cold-start per turn
1112
-
1113
- This is a Halo-platform fact that the entire phase 12 design depends
1114
- on. Worth pinning here because misunderstanding it leads to
1115
- over-engineering (e.g. building a "release all sessions on apply"
1116
- hook that turned out to be unnecessary).
1117
-
1118
- ### How `SessionManager` actually caches sessions
1119
-
1120
- `SessionManager.sessions` is a `Map<sessionId, AgentSession>` of
1121
- **currently-mid-message** sessions only. The cache lifecycle is:
1122
-
1123
- 1. User message arrives → `ensureSession(id)`:
1124
- - if id is in the Map → return that AgentSession
1125
- - else → load metadata from sqlite + load saved messages from
1126
- `<ws>/.halo/sessions/<agentId>/<id>.json` + run
1127
- `buildAgentInstance` (which reads `agent.yaml`, `INSTRUCTIONS.md`,
1128
- `USER.md`, all `prompts/` from disk) → put result in the Map
1129
- 2. Run the LLM turn (model call + tool calls + assistant reply)
1130
- 3. `runSession`'s finally block calls `releaseSession(id)`:
1131
- - persist message log to disk (`saveAgentState`)
1132
- - **delete from the in-memory Map**
1133
- 4. Session is now back to "only on disk". Next message → step 1 again.
1134
-
1135
- So **between user messages, no session is in memory**. Each turn is a
1136
- cold start: re-read all prompt files, build a fresh `ModelRuntime`
1137
- instance, run, save, evict.
1138
-
1139
- ### What this means for evo / apply
1140
-
1141
- The natural consequence: **any change to `agent.yaml`, `INSTRUCTIONS.md`,
1142
- `USER.md`, or `prompts/**` takes effect on the very next user message**,
1143
- without any cache-invalidation work from the rest of the system.
1144
- Phase 12 of evo apply just needs to copy the sandbox files into main —
1145
- the prompts kick in for free on the next turn, in every channel
1146
- (wechat, telegram, web, TUI, cli, sub-agent `start_session`).
1147
-
1148
- In-flight sessions (LLM call running right now when phase 12 cp lands)
1149
- finish on the *old* prompts — also correct: aborting a user
1150
- mid-conversation to make a prompt change visible 200ms earlier would be
1151
- a worse trade.
1152
-
1153
- ### Implications
1154
-
1155
- - No "release sessions on apply" hook needed in phase 12.
1156
- - No cross-process coordination between wrapper and server for prompt
1157
- invalidation — the in-memory cache simply doesn't live long enough
1158
- for staleness to matter.
1159
- - A wrapper can be a fully detached process (its own evo-db handle, no
1160
- shared state with server) without any caching consequences.
1161
- - Server restart is a no-op for users — every "lost" session was about
1162
- to be released anyway, and gets restored from disk on the next turn.
1163
-
1164
- ### Inverse: what does NOT take effect immediately
1165
-
1166
- A handful of things ARE cached for the lifetime of the server process,
1167
- not per-turn:
1168
-
1169
- - The `models/<provider-id>.yaml` registry (loaded once at server boot;
1170
- see `loadModelsRegistry` in `config.ts`). Editing one of these
1171
- requires a server restart.
1172
- - The `general` settings schema (cached at boot). Same — restart for
1173
- schema additions, though *values* in `settings.yaml` are mtime-watched
1174
- and pick up live.
1175
- - Compiled tool registries inside individual `SessionManager`s
1176
- (workspace tools list). Today these are static, not user-tunable
1177
- beyond the `tools:` array in `agent.yaml`, which IS picked up
1178
- per-turn.
1179
-
1180
- If a future feature adds long-lived in-memory state that needs to
1181
- respond to evo apply, *that's* when an explicit invalidation hook
1182
- would be worth designing — not for prompt files, which already work.
1183
-
1184
- ## Constraints / safety
1185
-
1186
- - Evo only observes **root sessions** (`parentId === null`). Sub-agent sessions
1187
- don't trigger evo on their own.
1188
- - Evo is **invisible to the conversation**: no `user`/`assistant` messages
1189
- appended to source session log. One short `chat:system` line per `/note`.
1190
- - Wrappers detach from server — server restart doesn't kill in-flight
1191
- evaluations. Heartbeat-based timeout cleans up if a wrapper crashes.
1192
- - `/note` is rejected for `accessLevel === 'readonly'` users.
1193
- - Wrappers never modify `~/.halo/global/` directly — only the workspace's
1194
- `.halo/` (via copy-on-write inside the apply agent).
1195
- - The `__evo_agent__` and `__apply_agent__` are normal agents — they can be
1196
- modified by users (or by themselves via evo, like any other agent). User
1197
- approves their own self-modifications.
1198
-
1199
- ## Implementation order
1200
-
1201
- Status legend: ✅ done · 🔄 in progress · ❌ not started.
1202
-
1203
- 1. ✅ Settings schema (`level`, max_concurrent, timeouts, triggers)
1204
- 2. ✅ Global db: `evolution_runs`, `evolution_applies` tables (`~/.halo/global/evo.db`)
1205
- 3. ✅ Hidden agent infrastructure: `internal: true` field; filter in
1206
- `list_agents` / `query_agent` / `start_session`; admin API surfaces it;
1207
- admin sidebar shows them in a separate "Internal" section
1208
- 4. ✅ Seed `__evo_agent__` and `__apply_agent__` agent.yaml + AGENT.md
1209
- placeholders (full procedure in `AGENT.md` is still TODO — see step 9)
1210
- 5. ✅ `/note` slash command — writes `runs/<id>/{meta,source-snapshot}.json` and INSERTs a `pending` row in `evolution_runs`. Hidden from `/help` when level≠L1 or user is readonly.
1211
- 6. ✅ Pre-compact hook in `SessionManager.selfCompactSession()` — snapshots
1212
- the session log + INSERTs `pending` row before the compaction LLM rewrites
1213
- messages. Gated on `evolution.level=L1` + `triggers.pre_compact=true` +
1214
- root sessions only. Helper extracted to `evolution/enqueue.ts` and reused
1215
- by `/note`.
1216
- 7. ✅ Server-side ticker (every 30s) — heartbeat timeout, atomic claim,
1217
- `attempts` retry budget, placeholder spawner
1218
- 8. ✅ Evo wrapper (Node) — mode=run. Spawns `halo cli -a __evo_agent__ -n
1219
- -w <ws> "<brief>"`, heartbeats every 60s, translates exit code +
1220
- patch.md/score.json existence into terminal db status
1221
- 9. ✅ Wrapper-orchestrated 3-phase run mode (replaces the original "agent
1222
- does everything" approach because Opus 4.7 wouldn't honour soft rules
1223
- like "exactly one sub-cli call"):
1224
- - **Phase A** — `__evo_agent__` drafts only: reads snapshot + current
1225
- prompts, builds sandbox (whitelist), applies draft, writes patch.md
1226
- with `testScenario` frontmatter (double field —
1227
- `originalMessage` for scorer baseline lookup, `testMessage` for
1228
- wrapper dry-run), exits. Does NOT run dry-runs or score itself.
1229
- - **Phase B** — wrapper runs the dry-run: `timeout 180 halo cli -a
1230
- <agentId> -n -w <sandbox> "<testScenario.testMessage>"`. On failure
1231
- calls `__evo_agent__` back in fix mode with the error log + a "fix
1232
- only, do NOT run" brief; one corrective pass (`FIX_BUDGET=1`).
1233
- Successful dry-run stdout lands in `<runDir>/dry-run-output.txt`.
1234
- - **Phase C** — `__score__` (separate agent, no shell_exec) reads
1235
- patch.md + dry-run output + source-snapshot.json, writes
1236
- score.json. Locates "before" baseline in snapshot via
1237
- `testScenario.originalMessage`. No file edits beyond score.json.
1238
- Reused at apply time as the regression gate.
1239
- - **Internal-agent isolation**: agents flagged `internal: true`
1240
- (evo, score, future apply) skip USER.md / INSTRUCTIONS.md /
1241
- INDEX.md / prompts/all|root|bootstrap. Only their own AGENT.md +
1242
- wrapper brief reaches the LLM. Reduces token cost and removes
1243
- workspace-rule bleed-through.
1244
- - **Language wiring**: wrapper resolves `general.language`
1245
- (BCP-47) → `langHint` (`English` / `简体中文`), threads it into all
1246
- three briefs as an *imperative* `Output language: write …` clause
1247
- (passive `User language: …` headers were ignored by the LLM).
1248
- Frontmatter keys / yaml schema stay English regardless.
1249
- - **Whitelist sandbox cp**: only `INSTRUCTIONS.md / agents / prompts
1250
- / skills` are copied into `<runDir>/sandbox/.halo/`. Other
1251
- directories (sessions, db, logs, channels) are workspace state that
1252
- the dry-run sub-cli rebuilds from scratch.
1253
- - Backend route `readScoreAvg` reads `score.json` directly. Stale
1254
- `score:` blocks in older patch.md frontmatter are ignored.
1255
- 10. ✅ Admin Evolution tab UI (list / view / approve / reject). New top-level
1256
- tab `Evolution` (Sparkles icon) renders a two-pane list+detail. Backend
1257
- routes at `/api/evolution/runs` (list/detail) + `/approve` / `/reject` /
1258
- `/hint`. Approve queues a pending `evolution_applies` row; nothing
1259
- actually merges yet — that's phase 11/12.
1260
- 11. ✅ Apply wrapper (Node) + `__apply_agent__/AGENT.md`, two sub-phases
1261
- that mirror evo's run mode:
1262
- - **Phase A' — merge**: wrapper builds a fresh sandbox by whitelist-cp
1263
- from the **current main workspace**, then spawns
1264
- `__apply_agent__` to merge all approved patches into it. Apply
1265
- agent reasons over the platform override matrix (same knowledge
1266
- as evo) so workspace-replaces-global cases (INSTRUCTIONS.md /
1267
- AGENT.md / agent.yaml) get the whole-global-file copied first,
1268
- and `prompts/<scope>/` adds copy the entire global directory
1269
- first. reviewer_hint is a hard cap on what the agent will apply.
1270
- Apply agent writes `apply.log` (audit trail) and edits
1271
- `<applyDir>/sandbox/.halo/...` only.
1272
- - **Phase B' — regress**: wrapper, NOT the agent, loops over
1273
- `source_run_ids`. Per run: parses `testScenario` from the run's
1274
- patch.md, runs `timeout 180 halo cli -a <agentId> -n -w
1275
- <applyDir>/sandbox "<testMessage>"`, saves stdout to
1276
- `regress/<runId>/dry-run-output.txt`, then spawns `__score__` to
1277
- write `regress/<runId>/score.json`. `lint < 50` or `behavior <
1278
- 50` in any score → mark apply `failed`. All scores OK → continue
1279
- to phase 12.
1280
- - **Main workspace stays untouched throughout phase 11** — only
1281
- `<applyDir>/sandbox/` holds the merged state.
1282
- 12. ✅ Final sync. After phase B' all scores pass, the wrapper splits
1283
- phase 12 into three checkpointed steps:
1284
- - **Preflight**: walk sandbox, diff against main, snapshot pre-apply
1285
- state of changed paths to `history/apply-<id>/` (with
1286
- `MANIFEST.json`). Idempotent — on a resume, MANIFEST + history
1287
- backup are skipped (the originals are already correct).
1288
- - **Checkpoint**: write `evolution_applies.status='syncing'` BEFORE
1289
- the dangerous step. From here on, a wrapper crash leaves the row
1290
- in 'syncing' state; the ticker picks it up and respawns a wrapper
1291
- that takes a resume branch (skipping all LLM-heavy phases A'/B').
1292
- - **Publish**: cp each `changed` file from sandbox to main. Crash
1293
- mid-cp is the only "real" failure mode — main may be half-applied,
1294
- but `history/apply-<id>/` has the rollback material and the resume
1295
- path will retry the cp (idempotent).
1296
-
1297
- On success: mark each `source_run_id` in `evolution_runs` as
1298
- `applied` (with `applied_at` timestamp), then mark the apply row
1299
- `applied`. **No "release in-memory sessions" step** — see
1300
- [Session lifecycle](#session-lifecycle-cold-start-per-turn).
1301
-
1302
- All 12 phases shipped. The L1 MVP loop now closes end-to-end:
1303
- `/note` or pre-compact → ticker spawns evo wrapper → patch.md +
1304
- score.json → reviewer approves in admin → apply wrapper merges +
1305
- regress-tests via `__score__` → phase 12 cps to main with history
1306
- snapshot → next user turn picks up the new prompts.
1307
-
1308
- ---
1309
-
1310
- # Future levels
1311
-
1312
- Sketches.
1313
-
1314
- ## L2
1315
-
1316
- - Auto-apply patches with `score.avg >= 80 && confidence == 'high'` AND
1317
- target ∈ {INSTRUCTIONS.md, custom prompts}. Same wrapper, just bypasses
1318
- manual approve step (creates `evolution_applies` directly).
1319
- - Settings: `auto_apply_threshold: 80`, per-target opt-in flags.
1320
-
1321
- ## L3
1322
-
1323
- - New patch kinds for yaml mutations (`tool-rewire`, `skill-rewire`).
1324
- - Idle trigger added.
1325
-
1326
- ## L4
1327
-
1328
- - `agent-create`, `skill-create`, `archive` kinds.
1329
- - Curator periodic invocation that consolidates pending pile + active
1330
- prompts.
1331
-
1332
- ---
1333
-
1334
- # Decision log
1335
-
1336
- - **Why L0/L1 ladder?** Each level has consistent risk profile; user mental
1337
- model is simple ("I'm at L1").
1338
- - **Why ship L1 first?** Need real accept/reject data before automating
1339
- anything.
1340
- - **Why a ticker, not server-internal scheduler with in-memory state?**
1341
- Restart-safe; failure modes are simple (everything reads db).
1342
- - **Why heartbeat in wrapper, not in cli session?** Cli session focused on
1343
- the LLM work — adding "must update db every minute" is a distraction.
1344
- Wrapper is the right place for liveness.
1345
- - **Why two hidden agents instead of one?** They have different skills and
1346
- different prompts. evo is "analyst + critic"; apply is "merger + validator".
1347
- Splitting keeps each focused.
1348
- - **Why patch.md is natural-language, not a diff?** Multiple approvals merge
1349
- better when the apply agent can reason about intent, not bytes.
1350
- - **Why apply agent writes to sandbox first, not directly?** Validation gate
1351
- before main workspace gets touched. Test fails → main untouched. No
1352
- half-applied state.
1353
- - **Why wrapper does the file sync, not the apply agent?**
1354
- apply agent's job is to decide *what* the new state should look like.
1355
- Wrapper does mechanical "publish the result". Cleaner separation.
1356
- - **Why no `releaseAllSessions` step in phase 12?** Halo's session
1357
- cache is per-turn, not long-lived: `runSession`'s finally block
1358
- releases the session as soon as its message turn finishes
1359
- (`saveAgentState` + `Map.delete`). The next turn — channel reply,
1360
- web reload, /new — runs `ensureSession` → `buildAgentInstance`,
1361
- which re-reads `agent.yaml` + `INSTRUCTIONS.md` + `USER.md` from disk.
1362
- So the just-applied changes pick up automatically on the next turn,
1363
- for free, without anyone having to call a release hook.
1364
- Currently-running turns finish on the old prompts — also the right
1365
- behavior, because aborting a user mid-conversation would be a worse
1366
- experience than a few seconds of "old rules apply to in-flight
1367
- message". See [Session lifecycle: cold-start per turn](#session-lifecycle-cold-start-per-turn).
1368
- - **Why source-snapshot.json at trigger time?** User keeps chatting; live
1369
- read would see moving target. Freezing the input is the easiest correctness
1370
- guarantee.
1371
- - **Why no idle trigger?** User isn't there to react; pending appears later
1372
- with no context recall. `/note` + pre-compact cover the live cases.
1373
- - **Why no hash check at apply time?** Apply agent rebuilds the file from
1374
- current state + patch intent. The patch isn't a diff that can stale —
1375
- it's an instruction the agent re-applies fresh.
1376
- - **Why allow evo to modify itself?** Same as software updates: blocking
1377
- forever creates more harm than the rare bad self-edit. User reviews.
1378
- - **Why per-process wrappers vs in-server async?** Crash isolation; can
1379
- add concurrency without server thread issues; survives server restart.