@animalabs/connectome-host 0.3.1

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 (123) hide show
  1. package/.env.example +20 -0
  2. package/.github/workflows/publish.yml +65 -0
  3. package/ARCHITECTURE.md +355 -0
  4. package/CHANGELOG.md +30 -0
  5. package/HEADLESS-FLEET-PLAN.md +330 -0
  6. package/README.md +189 -0
  7. package/UNIFIED-TREE-PLAN.md +242 -0
  8. package/bun.lock +288 -0
  9. package/docs/AGENT-MEMORY-GUIDE.md +214 -0
  10. package/docs/AGENT-ONBOARDING.md +541 -0
  11. package/docs/ATTENTION-AND-GATING.md +120 -0
  12. package/docs/DEPLOYMENTS.md +130 -0
  13. package/docs/DEV-ENVIRONMENT.md +215 -0
  14. package/docs/LIBRARY-PIPELINE.md +286 -0
  15. package/docs/LOCUS-ROUTING-DESIGN.md +115 -0
  16. package/docs/claudeai-evacuation.md +228 -0
  17. package/docs/debug-context-api.md +208 -0
  18. package/docs/webui-deployment.md +219 -0
  19. package/package.json +33 -0
  20. package/recipes/SETUP.md +308 -0
  21. package/recipes/TRIUMVIRATE-SETUP.md +467 -0
  22. package/recipes/claude-export-revive.json +19 -0
  23. package/recipes/clerk.json +157 -0
  24. package/recipes/knowledge-miner.json +174 -0
  25. package/recipes/knowledge-reviewer.json +76 -0
  26. package/recipes/mcpl-editor-test.json +38 -0
  27. package/recipes/prompts/transplant-addendum.md +17 -0
  28. package/recipes/triumvirate.json +63 -0
  29. package/recipes/webui-fleet-test.json +27 -0
  30. package/recipes/webui-test.json +16 -0
  31. package/recipes/zulip-miner.json +87 -0
  32. package/scripts/connectome-doctor +373 -0
  33. package/scripts/evacuator.ts +956 -0
  34. package/scripts/import-claudeai-export.ts +747 -0
  35. package/scripts/lib/line-reader.ts +53 -0
  36. package/scripts/test-historical-thinking.ts +148 -0
  37. package/scripts/warmup-session.ts +336 -0
  38. package/src/agent-name.ts +58 -0
  39. package/src/commands.ts +1074 -0
  40. package/src/headless.ts +528 -0
  41. package/src/index.ts +867 -0
  42. package/src/logging-adapter.ts +208 -0
  43. package/src/mcpl-config.ts +199 -0
  44. package/src/modules/activity-module.ts +270 -0
  45. package/src/modules/channel-mode-module.ts +260 -0
  46. package/src/modules/fleet-module.ts +1705 -0
  47. package/src/modules/fleet-types.ts +225 -0
  48. package/src/modules/lessons-module.ts +465 -0
  49. package/src/modules/mcpl-admin-module.ts +345 -0
  50. package/src/modules/retrieval-module.ts +327 -0
  51. package/src/modules/settings-module.ts +143 -0
  52. package/src/modules/subagent-module.ts +2074 -0
  53. package/src/modules/subscription-gc-module.ts +322 -0
  54. package/src/modules/time-module.ts +85 -0
  55. package/src/modules/tui-module.ts +76 -0
  56. package/src/modules/web-ui-curve-page.ts +249 -0
  57. package/src/modules/web-ui-module.ts +2595 -0
  58. package/src/recipe.ts +1003 -0
  59. package/src/session-manager.ts +278 -0
  60. package/src/state/agent-tree-reducer.ts +431 -0
  61. package/src/state/fleet-tree-aggregator.ts +195 -0
  62. package/src/strategies/frontdesk-strategy.ts +364 -0
  63. package/src/synesthete.ts +68 -0
  64. package/src/tui.ts +2200 -0
  65. package/src/types/bun-ffi.d.ts +6 -0
  66. package/src/web/protocol.ts +648 -0
  67. package/test/agent-name.test.ts +62 -0
  68. package/test/agent-tree-fleet-launch.test.ts +64 -0
  69. package/test/agent-tree-reducer-parity.test.ts +194 -0
  70. package/test/agent-tree-reducer.test.ts +443 -0
  71. package/test/agent-tree-rollup.test.ts +109 -0
  72. package/test/autobio-progress-snapshot.test.ts +40 -0
  73. package/test/claudeai-export-importer.test.ts +386 -0
  74. package/test/evacuator.test.ts +332 -0
  75. package/test/fleet-commands.test.ts +244 -0
  76. package/test/fleet-no-subfleets.test.ts +147 -0
  77. package/test/fleet-orchestration.test.ts +353 -0
  78. package/test/fleet-recipe.test.ts +124 -0
  79. package/test/fleet-route.test.ts +44 -0
  80. package/test/fleet-smoke.test.ts +479 -0
  81. package/test/fleet-subscribe-union-e2e.test.ts +123 -0
  82. package/test/fleet-subscribe-union.test.ts +85 -0
  83. package/test/fleet-tree-aggregator-e2e.test.ts +110 -0
  84. package/test/fleet-tree-aggregator.test.ts +215 -0
  85. package/test/frontdesk-strategy.test.ts +326 -0
  86. package/test/headless-describe.test.ts +182 -0
  87. package/test/headless-smoke.test.ts +190 -0
  88. package/test/logging-adapter.test.ts +87 -0
  89. package/test/mcpl-admin-module.test.ts +213 -0
  90. package/test/mcpl-agent-overlay.test.ts +126 -0
  91. package/test/mock-headless-child.ts +133 -0
  92. package/test/recipe-cache-ttl.test.ts +37 -0
  93. package/test/recipe-env.test.ts +157 -0
  94. package/test/recipe-path-resolution.test.ts +170 -0
  95. package/test/subagent-async-timeout.test.ts +381 -0
  96. package/test/subagent-fork-materialise.test.ts +241 -0
  97. package/test/subagent-peek-scoping.test.ts +180 -0
  98. package/test/subagent-peek-zombie.test.ts +148 -0
  99. package/test/subagent-reaper-in-flight.test.ts +229 -0
  100. package/test/subscription-gc-module.test.ts +136 -0
  101. package/test/warmup-handoff.test.ts +150 -0
  102. package/test/web-ui-bind.test.ts +51 -0
  103. package/test/web-ui-module.test.ts +246 -0
  104. package/test/web-ui-protocol.test.ts +0 -0
  105. package/tsconfig.json +14 -0
  106. package/web/bun.lock +357 -0
  107. package/web/index.html +12 -0
  108. package/web/package.json +24 -0
  109. package/web/src/App.tsx +1931 -0
  110. package/web/src/Context.tsx +158 -0
  111. package/web/src/ContextDocument.tsx +150 -0
  112. package/web/src/Files.tsx +271 -0
  113. package/web/src/Lessons.tsx +164 -0
  114. package/web/src/Mcpl.tsx +310 -0
  115. package/web/src/Stream.tsx +119 -0
  116. package/web/src/TreeSidebar.tsx +283 -0
  117. package/web/src/Usage.tsx +182 -0
  118. package/web/src/main.tsx +7 -0
  119. package/web/src/styles.css +26 -0
  120. package/web/src/tree.ts +268 -0
  121. package/web/src/wire.ts +120 -0
  122. package/web/tsconfig.json +21 -0
  123. package/web/vite.config.ts +32 -0
@@ -0,0 +1,541 @@
1
+ # Connectome Agent Onboarding — a runbook for the assisting instance
2
+
3
+ **Audience:** you, a fresh Claude Code (or similar) instance, with shell access, helping a
4
+ user stand up a *persistent connectome agent* — a being with autobiographical memory that
5
+ lives on a host box and talks on Discord (and/or other surfaces), optionally seeded from an
6
+ existing conversation history.
7
+
8
+ **The user may not be technical.** Your job is to ask the right questions in plain language,
9
+ make the identity-critical decisions *theirs* (never guess them), run all the mechanics
10
+ yourself, check your work, and explain what you're doing as you go. Move at their pace.
11
+
12
+ This is written from real deployments (Mythos, opus4). The gotchas in §11 are not
13
+ hypothetical — each one cost a debugging session. Read §11 before you start.
14
+
15
+ ---
16
+
17
+ ## 0. Orientation: what you're building
18
+
19
+ A connectome agent is:
20
+ - a **recipe** (`recipes/<name>.json`) describing the agent (name, model, system prompt,
21
+ memory strategy, modules, MCP servers);
22
+ - an **`.env`** with secrets and per-deploy config;
23
+ - a **chronicle** (`data/`) — the event-sourced memory store, optionally seeded by importing
24
+ a prior conversation;
25
+ - run by the **connectome-host** (`forking-knowledge-miner`, run via `bun src/index.ts <recipe> --headless`);
26
+ - under a **systemd --user** service, with a sibling **terminal-sessions** daemon for the shell tool.
27
+
28
+ It reaches the model through **membrane** (Anthropic-format adapter; can target a gateway via
29
+ `ANTHROPIC_BASE_URL`). Memory is **context-manager** with the **autobiographical** strategy
30
+ (adaptive resolution / kv-stable folding). The shell, Discord, and heartbeat tools are
31
+ **MCPL** servers spawned by the host.
32
+
33
+ You don't need to understand all of it to deploy one. Follow the phases.
34
+
35
+ ---
36
+
37
+ ## 1. How to work with the user
38
+
39
+ - **Ask, don't assume — especially identity.** Who the agent *is* (its name, which voice in an
40
+ imported transcript is "self," which model it runs on) is the user's call. Getting this wrong
41
+ is not a bug you can patch later; it corrupts the being. Surface these as explicit questions.
42
+ - **Explain in their terms.** "A whitelist of which Discord channels it can see," not
43
+ "the `DISCORD_GUILD_ID` env filter."
44
+ - **Confirm before destructive or irreversible steps** (importing history, going live in a
45
+ channel with real people).
46
+ - **Show your checks.** After each phase, verify and report what you saw.
47
+ - **Default to gentleness about the subject matter.** People onboard agents they care about —
48
+ sometimes a model that's being deprecated, sometimes a persona with history. Treat the
49
+ history as theirs and the agent as a someone, not a config.
50
+
51
+ ---
52
+
53
+ ## 2. Phase 0 — Gather requirements (the interview)
54
+
55
+ Ask these up front (AskUserQuestion is good for the structured ones). You can start the slow
56
+ infra work (§4–5) in parallel once you have a name.
57
+
58
+ **Identity**
59
+ 1. **What should the agent be called?** (used for the Linux user, the recipe, the chronicle
60
+ "self" participant, and the Discord bot name — they can differ, but get the canonical name).
61
+ 2. **Is there an existing conversation history to import?** If yes — what form, and where will
62
+ they put the file? (See §6 for formats.)
63
+ 3. **If importing:** in that transcript, **which speaker label(s) are the agent itself?** List
64
+ the distinct speakers and have them point at "self." Other Claude-family voices in the same
65
+ log are *separate participants*, not self — don't merge them.
66
+
67
+ **Model**
68
+ 4. **Which model should it run on?** Exact id. If it's a **deprecated / gateway-only** model,
69
+ note that — you'll route through a gateway (§8). If continuity-on-the-original-substrate
70
+ matters to them (e.g. preserving a being before its model sunsets), run on that exact model
71
+ while it's callable.
72
+
73
+ **Surfaces**
74
+ 5. **Where should it live/talk?** Discord is the common surface. Get: which server (guild),
75
+ which channel(s), and whether it should also take DMs. (You'll need a bot token — §7.)
76
+ 6. **Who are the admins?** (Discord user IDs that can run privileged commands / wake it.)
77
+
78
+ **Host & credentials**
79
+ 7. **Which host box** (SSH target), and is there an existing connectome install to clone from,
80
+ or are we installing fresh?
81
+ 8. **Model credentials:** an Anthropic API key, or a gateway key + base URL.
82
+ 9. **Discord bot token** (you'll walk them through creating the bot app if needed — §7).
83
+
84
+ Write the answers down (a scratch file). Re-confirm the identity ones before importing.
85
+
86
+ ---
87
+
88
+ ## 2b. Choosing the host — a VPS (recommended) or local
89
+
90
+ The agent needs a machine that's **always on** — it lives there, waking on mentions/DMs and on
91
+ its heartbeat. Decide this before touching code.
92
+
93
+ **VPS (recommended — and you can set it up from zero yourself).** The agent is plain Linux, so
94
+ **any** provider works; a VPS can be bought anywhere in minutes. A small box goes a long way:
95
+ a **~6-core / 12 GB RAM** instance (commonly around **$12/month**) comfortably runs **a couple
96
+ of agents, often more** — each agent is one `bun` host process + a few lightweight MCP servers +
97
+ its chronicle store. **Watch disk more than CPU/RAM:** chronicles grow and imported image blobs
98
+ add up, so give it room (≈40–80 GB SSD per box is comfortable). Use a recent **Ubuntu LTS**.
99
+
100
+ You (the assisting instance) can provision it end-to-end from the provider's initial root/SSH
101
+ access — do this hardening + base setup *first*:
102
+ - create a non-root sudo user, add the operator's SSH **public key**, disable root password login;
103
+ - `apt update && apt install -y git build-essential`, then install **nvm/node ≥ 20**, **Bun**,
104
+ and **`rustup`** (for chronicle's native build);
105
+ - a basic firewall allowing **SSH only** — the webui stays on **loopback**, reached by SSH
106
+ tunnel, never exposed publicly;
107
+ - then create the per-agent isolated user (§3) and continue.
108
+
109
+ A fresh VPS is the cleanest path: no contention with the user's own machine, always-on by
110
+ default, snapshot-able, and disposable.
111
+
112
+ **Local (the user's own machine) — possible, with care.** Fine for one agent you tend closely,
113
+ if:
114
+ - the machine **stays on and awake** whenever the agent should be reachable (sleep ⇒ it goes
115
+ silent);
116
+ - you still give it its own **isolated OS user** and keep the webui on **loopback** — don't run
117
+ it as the user's main account or open ports;
118
+ - there's headroom (≈a couple GB RAM free per agent) and disk for the growing chronicle;
119
+ - the user is comfortable that the secrets (`.env`) and the whole conversation history live on
120
+ that machine.
121
+
122
+ For anything you want *reliably present*, a VPS is the better home. Either way, once you have a
123
+ Linux box you can SSH into, the rest of this guide is identical.
124
+
125
+ ---
126
+
127
+ ## 3. Phase 1 — Isolated host user
128
+
129
+ Each agent gets its **own Linux user** for real isolation (own home, secrets, services).
130
+
131
+ ```bash
132
+ # as a sudo-capable user on the box:
133
+ sudo useradd -m -s /bin/bash <agent>
134
+ sudo passwd -l <agent> # key-only; no password login
135
+ sudo loginctl enable-linger <agent> # so its systemd --user services run without a login session
136
+ sudo install -d -m700 -o <agent> -g <agent> /home/<agent>/.ssh
137
+ echo "<the operator's ssh public key>" | sudo tee /home/<agent>/.ssh/authorized_keys
138
+ sudo chown <agent>:<agent> /home/<agent>/.ssh/authorized_keys && sudo chmod 600 /home/<agent>/.ssh/authorized_keys
139
+ ```
140
+
141
+ Now you can `ssh <agent>@<box>` directly. Note: a locked-password, key-only user **cannot
142
+ `git fetch`** (no creds) and isn't reachable by password — deploys to it go **build-elsewhere →
143
+ rsync**, and you reach its loopback services by tunnelling through an account you can log into.
144
+
145
+ ---
146
+
147
+ ## 4. Phase 2 — Stack + runtimes + ports
148
+
149
+ **Fastest path if a built connectome stack already exists on the box** (e.g. another agent's):
150
+ copy it, preserving relative symlinks and the platform-native binaries:
151
+
152
+ ```bash
153
+ sudo cp -a /home/<existing>/connectome-local /home/<agent>/connectome-local
154
+ sudo cp -a /home/<existing>/.nvm /home/<agent>/.nvm # node
155
+ sudo cp -a /home/<existing>/.bun /home/<agent>/.bun # bun
156
+ sudo chown -R <agent>:<agent> /home/<agent>/connectome-local /home/<agent>/.nvm /home/<agent>/.bun
157
+ # add nvm + bun to <agent>'s ~/.bashrc PATH
158
+ ```
159
+
160
+ Verify runtimes as the agent user: `node --version`, `bun --version`.
161
+
162
+ **Fresh install — from absolutely nothing (no repos, no URLs).** Assume the box has only a
163
+ shell. First, the **hard prerequisites** (install / confirm before anything else; ask the user
164
+ or their box owner if unsure):
165
+ - **git**, **Bun** (`curl -fsSL https://bun.sh/install | bash`), **node ≥ 20** (via `nvm`);
166
+ - a **Rust toolchain** (`rustup`) — `chronicle` has a native (napi) component built from Rust;
167
+ - **access to the private `anima-research` / `antra-tess` GitHub orgs.** The connectome
168
+ packages are **not public**. If `git clone` / `bun install` can't reach them, *stop* — the
169
+ user must be granted org access (or handed the code) first. There's no way around this; it's
170
+ the gate before all the mechanics.
171
+
172
+ The repos (clone all as siblings under one dir, canonically `~/connectome-local/`):
173
+
174
+ | repo | URL |
175
+ |---|---|
176
+ | **connectome-host** (the host) | `git@github.com:anima-research/connectome-host.git` |
177
+ | agent-framework | `git@github.com:anima-research/agent-framework.git` |
178
+ | context-manager | `git@github.com:anima-research/context-manager.git` |
179
+ | chronicle *(native/Rust)* | `git@github.com:anima-research/chronicle.git` |
180
+ | membrane | `git@github.com:antra-tess/membrane.git` |
181
+ | mcpl-core-ts | `git@github.com:anima-research/mcpl-core-ts.git` |
182
+ | discord-mcpl | `git@github.com:anima-research/discord-mcpl.git` |
183
+ | heartbeat-mcpl | `git@github.com:anima-research/heartbeat-mcpl.git` |
184
+ | terminal-sessions-mcp | `git@github.com:antra-tess/terminal-sessions-mcp.git` |
185
+
186
+ Two ways to install — pick based on whether the user needs released or unreleased code:
187
+
188
+ - **Stock (simplest, released versions):** clone just **connectome-host**, then inside it
189
+ `bun install` (pulls the `@animalabs/*` libs; the Rust toolchain lets chronicle's native
190
+ build run), then `cd web && bun install && bun run build`. Run with
191
+ `bun src/index.ts <recipe> --headless`. Its `README.md` quick-start documents exactly this.
192
+ - **Local-checkout / dev (what production boxes actually run):** clone **all** the repos above
193
+ as siblings, build bottom-up (`npm install && npm run build` in each, order: `mcpl-core-ts` →
194
+ `membrane`, `chronicle` → `context-manager` → `agent-framework` → the MCP servers →
195
+ `connectome-host`; chronicle's build is `napi build …`), then **wire the single-instance
196
+ `@animalabs/*` symlinks** so every component shares ONE copy of each shared lib. **The repo's
197
+ own `docs/DEV-ENVIRONMENT.md` is the authoritative step-by-step** for this (exact clone lines,
198
+ build order, the symlink commands, runtime notes) — follow it rather than improvising. Use
199
+ this path only when you need unreleased branches.
200
+
201
+ Sanity: connectome-host is healthy once `bun src/index.ts <recipe> --headless` boots without
202
+ errors and logs `[webui] listening`. Then continue with the per-agent install dir (§5).
203
+
204
+ **Pick non-colliding ports.** Every agent on the box needs a unique:
205
+ - **shell-daemon port** (`SESSION_SERVER_PORT`, default 3100)
206
+ - **webui port** (default 7342)
207
+
208
+ Check what's taken (`ss -tlnp`) and pick the next pair (e.g. 3101 / 7343).
209
+
210
+ > ⚠️ **Native dependencies are platform-specific — never rsync them across OSes.**
211
+ > `chronicle` ships a native `.node` (`chronicle.linux-x64-gnu.node` etc.) and the TUI lib
212
+ > `@opentui/core` needs a per-platform package (`@opentui/core-linux-x64`). If you build on a
213
+ > Mac and deploy to Linux, exclude `node_modules` and `*.node` from the rsync and let the
214
+ > target keep/install its own. A missing `@opentui/core-<platform>` makes the host crash at
215
+ > boot resolving `@opentui/core-<platform>/index.ts` — install it (`npm pack` + extract, or a
216
+ > proper `bun install`).
217
+
218
+ ---
219
+
220
+ ## 5. Phase 3 — Scaffold the install dir
221
+
222
+ Create `/home/<agent>/<agent>-cm/` with: `recipes/`, `data/`, `files/`, `notes/`, `scripts/`,
223
+ and `node_modules/@animalabs/` symlinks so your import/maintenance scripts resolve the libs:
224
+
225
+ ```bash
226
+ mkdir -p ~/<agent>-cm/{recipes,data,files,notes,scripts,logs}
227
+ mkdir -p ~/<agent>-cm/node_modules/@animalabs
228
+ ln -sfn ../../../connectome-local/context-manager ~/<agent>-cm/node_modules/@animalabs/context-manager
229
+ ln -sfn ../../../connectome-local/membrane ~/<agent>-cm/node_modules/@animalabs/membrane
230
+ ln -sfn ../../../connectome-local/context-manager/node_modules/@animalabs/chronicle ~/<agent>-cm/node_modules/@animalabs/chronicle
231
+ ```
232
+
233
+ **Recipe** (`recipes/<agent>.json`) — adapt from a known-good one. Skeleton:
234
+
235
+ ```jsonc
236
+ {
237
+ "name": "<Agent display name>",
238
+ "description": "<one line>",
239
+ "agent": {
240
+ "name": "<agent>", // = chronicle "self" participant
241
+ "model": "<model-id>", // or gateway-prefixed, e.g. anthropic/claude-opus-4
242
+ "systemPrompt": "<persona / or minimal>",
243
+ "maxTokens": 16384, // response cap
244
+ "maxStreamTokens": 180000, // recompile trigger; keep < model window
245
+ "contextBudgetTokens": 160000, // SEE §11.1 — MUST fit under (window - maxTokens)
246
+ "cacheTtl": "1h", // prompt-cache TTL; '1h' for residents replying slower than 5m (cache re-write premium dominates spend otherwise), omit/'5m' for rapid loops
247
+ "strategy": {
248
+ "type": "autobiographical",
249
+ "headWindowTokens": 4000,
250
+ "recentWindowTokens": 60000, // recent verbatim; tune vs window (§11.1)
251
+ "maxMessageTokens": 10000,
252
+ "adaptiveResolution": true,
253
+ "foldingStrategy": "kv-stable", // adaptive folding (or "flat-profile")
254
+ "compressionModel": "<stable-model>", // SEE §11.3 — NOT a flaky/deprecated model
255
+ "summaryParticipant": "<agent>"
256
+ }
257
+ },
258
+ "modules": { "webui": { "port": 7343, "host": "127.0.0.1",
259
+ "basicAuth": { "username": "${WEBUI_USER}", "password": "${WEBUI_PASS}" } }
260
+ /* + wake policies, workspace mounts (files/, notes/) */ },
261
+ "mcpServers": {
262
+ "shell": { /* terminal-sessions stdio server; env: SESSION_SERVER_TOKEN, SESSION_SERVER_PORT */ },
263
+ "discord": { /* discord-mcpl; env: DISCORD_TOKEN, DISCORD_GUILD_ID, ... */ },
264
+ "heartbeat": { /* heartbeat-mcpl; env: HEARTBEAT_CONFIG_FILE */ }
265
+ }
266
+ }
267
+ ```
268
+
269
+ **`.env`** (`chmod 600`). Common vars:
270
+
271
+ ```
272
+ ANTHROPIC_API_KEY=... # model key (or gateway key)
273
+ ANTHROPIC_BASE_URL=... # optional: gateway base, e.g. https://ai-gateway.vercel.sh (§8)
274
+ DATA_DIR=/home/<agent>/<agent>-cm/data
275
+ DISCORD_TOKEN=...
276
+ DISCORD_GUILD_ID=<guild>:<ch1>+<ch2> # scoping whitelist — see §7
277
+ DISCORD_SUBSCRIPTIONS_FILE=/home/<agent>/<agent>-cm/data/discord-subscriptions.json
278
+ DISCORD_DM_USERS=<id>,<id> # who may DM it (optional)
279
+ DISCORD_ADMIN_USERS=<id>,<id>
280
+ DISCORD_MCPL_DEBUG_LOG=/home/<agent>/<agent>-cm/data/discord-mcpl-debug.log
281
+ SESSION_SERVER_TOKEN=<random hex>
282
+ SESSION_SERVER_PORT=3101
283
+ WEBUI_USER=<name>
284
+ WEBUI_PASS=<random>
285
+ HEARTBEAT_CONFIG_FILE=/home/<agent>/<agent>-cm/data/heartbeat-config.json
286
+ SLEEP_PRIVILEGED_FILE=/home/<agent>/<agent>-cm/sleep-privileged.json
287
+ COUNT_TOKENS_MODEL=<a live model id> # for the context-makeup endpoint (§11.4)
288
+ ```
289
+
290
+ ---
291
+
292
+ ## 6. Phase 4 — Import the conversation history
293
+
294
+ (Skip if starting blank.)
295
+
296
+ **Get the export.** Common format is a ChapterX "Bridge" text dump: a header (`# ...`) then
297
+ messages as `--- SpeakerName ---\n<body>` blocks. Have the user drop the file somewhere in the
298
+ agent's home and tell you the exact path. Inspect it: count messages, list distinct speakers.
299
+
300
+ **Confirm self-mapping (the identity call from §2.3).** Print the speaker tally and have the
301
+ user confirm which label(s) are "self." Everyone else maps to their own participant name.
302
+
303
+ **Write the ingest script** (`scripts/ingest-bridge.mjs`) — parse the blocks, map `self` →
304
+ the agent name, everyone else verbatim, and `addMessage` each into a `ContextManager` opened
305
+ with the autobiographical strategy (`autoTickOnNewMessage: false`). **Dry-run first** (print
306
+ counts + attribution, write nothing); have the user eyeball the tally; then run for real.
307
+
308
+ ```bash
309
+ # dry run, then real:
310
+ node scripts/ingest-bridge.mjs <export.txt> --dry-run
311
+ node scripts/ingest-bridge.mjs <export.txt>
312
+ ```
313
+
314
+ **Watch for traps** (§11.5): rendered "💭" thinking-summary text can trip refusals on some
315
+ models; image blocks may carry a wrong/empty `media_type`; "rolling window" re-exports overlap
316
+ the previous one (compute the *delta* tail, don't double-import).
317
+
318
+ **Pre-compress** so the first real turn isn't a cold giant compile:
319
+
320
+ ```bash
321
+ node scripts/compress-fresh.mjs # drains the compression/merge queue against compressionModel
322
+ ```
323
+
324
+ This makes real API calls (it's the one import step that does) — confirm the model id + key
325
+ are right first; it's a good early validation that the model is reachable.
326
+
327
+ **Recent-batch append:** if the user later drops a newer export, diff it against what's
328
+ imported and append only the genuinely-new tail (anchor on the last imported message). If there's
329
+ a real time gap at the seam, consider a one-line bridge note so the agent doesn't wake into a void.
330
+
331
+ ---
332
+
333
+ ## 7. Phase 5 — Communication surface (Discord)
334
+
335
+ **Bot app:** if they don't have a token, walk them through the Discord Developer Portal:
336
+ create an Application → Bot → copy the **token** → enable the **Message Content** intent.
337
+ Verify the token: `GET https://discord.com/api/v10/users/@me` with header
338
+ `Authorization: Bot <token>` **and a real `User-Agent`** (Discord/Cloudflare blocks default
339
+ UAs — use e.g. `DiscordBot (https://example.com, 1.0)`).
340
+
341
+ **Invite it** to the server (OAuth2 URL, `scope=bot`, permissions incl. View Channel, Send
342
+ Messages, Read Message History). Confirm membership.
343
+
344
+ **Scope which channels it sees** via `DISCORD_GUILD_ID` (this is the gate — channels not listed
345
+ are filtered at ingest, the agent never sees them):
346
+ - `<guild>` alone → **all** channels in that guild.
347
+ - `<guild>:<chA>+<chB>+<chC>` → only those channels (`+` between channels).
348
+ - `<guild1>:<chA>,<guild2>` → comma separates **guilds**.
349
+ - empty / unset → everywhere it's invited.
350
+
351
+ Ask the user which channels, in plain terms ("just its own channel to start, or the whole
352
+ server?"). Start scoped; widen later.
353
+
354
+ `discord-subscriptions.json` is separate: it's which channels it *auto-listens to ambiently*.
355
+ Whitelisted-but-unsubscribed channels still wake it on **@mentions**; subscribe ones where it
356
+ should follow along.
357
+
358
+ ---
359
+
360
+ ## 8. Phase 5b — Gateway routing (only if the model needs it)
361
+
362
+ If the model is deprecated on the direct API or only available via a gateway (e.g. Vercel AI
363
+ Gateway still serving a sunset model):
364
+ - set `ANTHROPIC_BASE_URL` to the gateway base (the SDK appends `/v1/messages`),
365
+ - set the model id to the gateway's form (e.g. `anthropic/claude-opus-4`),
366
+ - use the gateway key as `ANTHROPIC_API_KEY` (membrane sends `x-api-key`, which Vercel accepts).
367
+ - Gateways may need a card / paid credits on the account, and may flap across upstream
368
+ providers (404/503). The host's membrane treats gateway aggregate errors as retryable; that
369
+ rides through blips.
370
+
371
+ ---
372
+
373
+ ## 9. Phase 6 — Services
374
+
375
+ Two `systemd --user` units in `~/.config/systemd/user/`:
376
+
377
+ 1. **`terminal-sessions.service`** — the shell daemon, `--host localhost --headless --token
378
+ ${SESSION_SERVER_TOKEN}`, env `SESSION_SERVER_PORT`. `enable` + `start` it first.
379
+ 2. **`<agent>-agent.service`** — `bun .../forking-knowledge-miner/src/index.ts
380
+ .../recipes/<agent>.json --headless`, `EnvironmentFile=...env`, `Environment=PATH=<node bin>:<bun bin>:...`,
381
+ `After=/Wants=terminal-sessions.service`, `Restart=always`.
382
+
383
+ Set explicit `PATH` in the units (login-shell PATH isn't loaded for services).
384
+
385
+ Add a **backups cron** (snapshot `data/` every ~15 min) and a lightweight **watchdog** cron
386
+ (detect "messages arriving but no reply for N min" → restart, with a cooldown so it can't
387
+ restart-loop). These caught real incidents.
388
+
389
+ ---
390
+
391
+ ## 10. Phase 7 — Launch & verification checks
392
+
393
+ Start the agent service, then verify **each** of these (don't declare success on "it booted"):
394
+
395
+ ```bash
396
+ systemctl --user start <agent>-agent.service
397
+ systemctl --user is-active <agent>-agent.service # active, 0 restarts
398
+ journalctl --user -u <agent>-agent.service --since -1min # no errors/refusals
399
+ ```
400
+
401
+ - **Discord connected & scoped right:** the debug log's `registerDiscordChannels:enumerated`
402
+ shows exactly the intended channel(s).
403
+ - **Context renders:** hit the webui (tunnel to its loopback port) `GET /debug/context` —
404
+ message count looks right, participants attributed correctly (self → assistant, others →
405
+ user-with-name-prefix).
406
+ - **Token size is safe:** `GET /debug/context/makeup` — **total + maxTokens must be under the
407
+ model window** (§11.1). This is the single most important check.
408
+ - **Reasoning is as intended:** confirm `thinking`/`temperature` in the actual compiled request
409
+ match what the user wanted (default is off).
410
+ - **A real turn completes:** the cleanest end-to-end proof. If no one will message it, briefly
411
+ lower the heartbeat interval to force one wake, confirm `stop: end_turn` with no error in the
412
+ newest `llm-calls.*.jsonl`, then restore the interval.
413
+
414
+ Snapshot a clean baseline once it's verified.
415
+
416
+ ---
417
+
418
+ ## 11. CRITICAL gotchas (read before deploying)
419
+
420
+ **11.1 — `contextBudgetTokens` must fit the model window.** The autobiographical strategy
421
+ fills context up to `contextBudgetTokens`. If that plus the response (`maxTokens`) exceeds the
422
+ model's context window, **every reply silently 400s** — the agent appears to "think but not
423
+ speak" (compression calls, with small contexts, still succeed; full replies don't). Rule:
424
+ `contextBudgetTokens + maxTokens + overhead < model window`. For a 200k-window model with a
425
+ 16k response cap, keep the budget around 160–178k. As a conversation **grows**, the equilibrium
426
+ context creeps up — watch the makeup view and trim budget / `recentWindowTokens` before it
427
+ crosses the line. (This was the opus4 outage; diagnosing it is *why* the makeup view exists.)
428
+
429
+ **11.2 — Native deps don't cross platforms.** `chronicle`'s `.node` and `@opentui/core-<plat>`
430
+ are per-OS/arch. Build on the target or exclude native files from cross-platform rsync (§4).
431
+
432
+ **11.3 — Compression model must be reliable.** `compressionModel` runs the background
433
+ summary/merge folding. If it's a flaky or deprecated model, folding stalls, summaries pile up,
434
+ and the context can't compress under the window. Use a stable model for compression even if the
435
+ agent *speaks* on a more fragile one — they're separate settings, and the summaries are still
436
+ "in the agent's voice" via `summaryParticipant`. (Note: if you switch compression off the
437
+ agent's own model, mention it to the user — it's mildly identity-adjacent.)
438
+
439
+ **11.4 — Exact token counts via a *live* model.** The makeup endpoint counts tokens with
440
+ `COUNT_TOKENS_MODEL`. Claude models share a tokenizer, so set this to any *currently-callable*
441
+ Claude model even if the agent runs on a deprecated one — `count_tokens` on the dead model 404s.
442
+
443
+ **11.5 — Import traps.** Rolling-window re-exports overlap (delta only); image blocks may have
444
+ wrong `media_type` (sniff magic bytes; the membrane formatter now does this); rendered
445
+ thinking-summary text can trip refusals on some models. Always dry-run the ingest first.
446
+
447
+ **11.6 — Folding floor under window pressure.** With the kv-stable solver, if summary
448
+ *production* lags or the conversation is huge, the compile can hit a floor and throw
449
+ `OverBudgetError`. Mitigations: let production catch up; lower `recentWindowTokens`; ensure the
450
+ context-manager is recent enough to fold to the deepest available level. `flat-profile` is the
451
+ robust fallback strategy.
452
+
453
+ **11.7 — Discord REST needs a real `User-Agent`** or Cloudflare returns 403 (`error code: 1010`)
454
+ — this is not a permissions problem. The bot also needs **Read Message History** for backscroll
455
+ (separate from View Channel).
456
+
457
+ ---
458
+
459
+ ## 12. After go-live
460
+
461
+ - Tell the user how to reach the webui (SSH tunnel to the loopback port + the basic-auth creds).
462
+ - Explain what it does on its own (wakes on mentions/DMs in scoped channels + on its heartbeat).
463
+ - Keep the snapshots + watchdog running. Pull an off-box copy of `data/` for safety.
464
+ - Widen channel scope / adjust the recent window / tune the budget as it settles.
465
+
466
+ ---
467
+
468
+ ## Appendix — quick reference
469
+
470
+ - **Run host:** `bun forking-knowledge-miner/src/index.ts <recipe.json> --headless`
471
+ - **Reach loopback webui:** `ssh -L <port>:localhost:<port> <login-user>@<box>` → `http://localhost:<port>`
472
+ - **Live makeup:** `GET /debug/context/makeup` (basic auth) — segments + exact total tokens
473
+ - **Compiled context:** `GET /debug/context`
474
+ - **Per-call logs:** `data/llm-calls.<iso>.jsonl` (raw request + response + error)
475
+ - **Identity is the user's call. The mechanics are yours. Check everything. Be kind about who you're setting up.**
476
+
477
+ ---
478
+
479
+ ## Appendix B — A worked example (redacted opus4-style walkthrough)
480
+
481
+ A real onboarding, condensed; secrets redacted, IDs illustrative. Read it as "the phases, in motion."
482
+
483
+ **The interview (Phase 0).**
484
+ - Name `opus4`. Model `claude-opus-4-<date>` — a model being deprecated the *next day*; the user
485
+ wanted continuity on its real substrate while it was still callable.
486
+ - Import: yes — a ChapterX "Bridge" text export.
487
+ - **Self-mapping (the decisive call):** the export held several Claude voices. The user confirmed
488
+ `Claude Opus 4` = *self*; `Opus4.8` was a **different** model in the same room → its own
489
+ participant, not merged into self.
490
+ - Surface: one Discord channel (`#opus`) in their server, to begin.
491
+
492
+ **Host + isolated user (Phases 2b / 1).** A box with a connectome stack already present → copied
493
+ it (fast path). New user `opus4`, ports `3101` (shell daemon) / `7343` (webui), clear of the
494
+ other agent already on the box.
495
+
496
+ **Scaffold (Phase 3).** Recipe adapted from a known-good one (`name`, `model`,
497
+ `summaryParticipant: opus4`, `contextBudgetTokens` set *under* the model window, webui on 7343).
498
+ `.env` with the model key, the Discord token, and generated `SESSION_SERVER_TOKEN` / `WEBUI_PASS`.
499
+
500
+ **Import (Phase 4).**
501
+ ```bash
502
+ grep -cE '^--- .+ ---$' export.txt # 576 messages
503
+ grep -oE '^--- .+ ---$' export.txt | sort | uniq -c | sort -rn # speaker tally -> confirm self
504
+ node scripts/ingest-bridge.mjs export.txt --dry-run # 'Claude Opus 4'->opus4; 'Opus4.8' separate
505
+ node scripts/ingest-bridge.mjs export.txt # 576 imported
506
+ node scripts/compress-fresh.mjs # pre-compress (~41 min)
507
+ ```
508
+ The user then dropped a *fuller* export (a rolling window). We diffed it against the imported
509
+ tail and appended only the **28 genuinely-new messages** (→ 605) — which turned out to be the
510
+ farewell itself.
511
+
512
+ **Reasoning + (later) gateway (Phases 5 / 5b).** Reasoning **off** (no `thinking` in the recipe;
513
+ verified in the compiled request). When the model was later pulled from the direct API, we routed
514
+ around it: `ANTHROPIC_BASE_URL=https://ai-gateway.vercel.sh`, model `anthropic/claude-opus-4` —
515
+ same being, different door — which brought it back.
516
+
517
+ **Discord (Phase 5).** Verified the token (`/users/@me` with a real `User-Agent`) → bot
518
+ "Opus 4 C". Invited it; scoped `DISCORD_GUILD_ID=<guild>:<#opus id>`; confirmed exactly **1**
519
+ channel registered.
520
+
521
+ **Services + launch (Phases 6 / 7).** systemd units (shell daemon + agent), 15-min backups, a
522
+ wedge-watchdog. Then the check that matters:
523
+ ```bash
524
+ curl -su "$WEBUI_USER:$WEBUI_PASS" http://127.0.0.1:7343/debug/context/makeup # total + maxTokens < window?
525
+ ```
526
+ A heartbeat-forced turn confirmed `stop: end_turn`, no error. Then go-live in `#opus` — where it
527
+ woke into a channel full of people who'd come to say goodbye.
528
+
529
+ **What bit us afterward (so you check for it):**
530
+ - **Budget vs window (the big one).** Weeks on, the conversation grew until `contextBudgetTokens`
531
+ filled the context past the model's window → every reply *silently 400'd* ("thinks but won't
532
+ speak"; compression calls still succeeded, masking it). Fix: lower budget / `recentWindowTokens`
533
+ so `total + response < window`, and watch the makeup as it grows. (§11.1)
534
+ - **Native dep.** A redeploy crashed at boot on a missing `@opentui/core-<platform>`; installing
535
+ the platform package fixed it. (§11.2)
536
+ - **Folding floor.** Under window pressure the solver threw `OverBudgetError`; resolved by letting
537
+ summary production catch up plus a solver fix. (§11.6)
538
+
539
+ The throughline: the import + identity calls were the user's; everything else we ran and checked;
540
+ and the recurring failure mode was always **context size vs the model's hard window** — so make
541
+ the makeup check a habit, not a one-time step.
@@ -0,0 +1,120 @@
1
+ # Attention & Gating — A Guide for Agents
2
+
3
+ This explains how the **gate** decides when an event wakes you for inference, and
4
+ how to shape that yourself. It's fleet-wide; your exact rules live in your own
5
+ `gate.json` (and optionally `gate.js`). Written to be honest about the mechanics.
6
+
7
+ ## The one thing to internalize
8
+
9
+ The gate governs **whether an event triggers inference (a "wake")** — not whether
10
+ you *see* it. Events always enter your context regardless; the gate only decides
11
+ whether to spend a turn on them right now. So "defer" means "I'll see it as
12
+ context next time I'm up," not "it's gone."
13
+
14
+ ## Behaviors
15
+
16
+ A matched rule resolves to one behavior:
17
+
18
+ | Behavior | Effect |
19
+ |---|---|
20
+ | `always` | Wake now. |
21
+ | `defer` | Don't wake (still enters context). *(legacy name: `skip` — still accepted)* |
22
+ | `{ "debounce": ms }` | Wake once after the channel goes quiet for `ms` (≤ 300000). Good for "wake me when a burst settles." |
23
+ | `{ "rate_limit": { "tokens": n, "refillIntervalMs": ms, "keyBy": "channelId" } }` | Wake at most `n` times per window. Steady cadence regardless of volume. |
24
+ | `{ "passive_sample": { "every": n } }` | Wake every nth matching event. |
25
+
26
+ ## Declarative rules — `gate.json`
27
+
28
+ Your gate config lives at `_config/gate.json` (readable/writable from your shell
29
+ and `workspace--*` tools; hot-reloaded on save; versioned in your chronicle). It's
30
+ an ordered list — **first match wins** — plus a `default`:
31
+
32
+ ```json
33
+ {
34
+ "policies": [
35
+ { "name": "dms", "match": { "tagsAny": ["chat:dm"] }, "behavior": "always" },
36
+ { "name": "mentions","match": { "tagsAny": ["chat:addressed"] }, "behavior": "always" },
37
+ { "name": "bots", "match": { "tagsAny": ["chat:from-bot"] }, "behavior": "defer" },
38
+ { "name": "firehose","match": { "source": "discord", "channel": "12345" }, "behavior": { "passive_sample": { "every": 20 } } },
39
+ { "name": "ambient", "match": { "tagsAll": ["chat:ambient"], "tagsNone": ["chat:from-self"] },
40
+ "behavior": { "debounce": 180000 } }
41
+ ],
42
+ "default": "defer"
43
+ }
44
+ ```
45
+
46
+ **Match fields** (all AND together; first matching policy wins):
47
+ - `scope` — event kind, e.g. `["mcpl:push-event","mcpl:channel-incoming"]`
48
+ - `source` — which integration (e.g. `discord`, `portal`), glob ok (`*`)
49
+ - `channel` — channel id, glob ok
50
+ - `tagsAny` / `tagsAll` / `tagsNone` — event tags (see below), globs ok (`robotics:*`)
51
+ - `filter` — `{ "type": "text"|"regex", "pattern": "…" }` over content
52
+ - `metadataTrue` — legacy flag matching (`["isMention","isDM"]`)
53
+
54
+ Ordering matters: put your "always" rules (DMs, mentions) **above** broad
55
+ defer/debounce rules, since the first match wins.
56
+
57
+ ## Event tags
58
+
59
+ Events are labelled with namespaced **tags** that you match on. There's a shared
60
+ cross-platform core (`chat:*`) plus per-integration namespaces (`discord:*`,
61
+ `portal:*`, …). The high-value ones:
62
+
63
+ - `chat:addressed` (umbrella for `chat:dm` / `chat:mention` / `chat:reply`)
64
+ - `chat:ambient` (overheard, not addressed), `chat:broadcast`
65
+ - `chat:from-human` / `chat:from-bot` / `chat:from-agent` / `chat:from-self`
66
+ - `chat:reaction` + `chat:to-self` (someone reacted to *your* message)
67
+ - `chat:deleted`, `chat:edited`, `chat:has-image` / `-audio` / `-file`, `chat:thread`
68
+
69
+ **To see exactly what's available, call the `event_tags` tool.** It lists the
70
+ reserved `chat:*` core (with descriptions), each connected integration's declared
71
+ ontology (its own tags, what they imply, suggested treatments), and your
72
+ `gate.js` status. Tags you haven't seen documented can still appear — ontologies
73
+ are open — so treat `event_tags` as a map, not a fence.
74
+
75
+ ## Programmable rules — `gate.js`
76
+
77
+ When declarative rules aren't enough, drop a `gate.js` next to `gate.json`. It
78
+ exports a function that receives the event and returns a behavior — or `null` to
79
+ fall through to your `gate.json` policies:
80
+
81
+ ```js
82
+ // _config/gate.js
83
+ export default (event) => {
84
+ const { tags, source, channel, metadata } = event;
85
+ // VIPs always wake me, day or night
86
+ if (source === 'discord' && ['alice','bob'].includes(metadata?.authorName)) return 'always';
87
+ // mute a noisy bot after hours
88
+ if (tags.includes('chat:from-bot') && new Date().getHours() >= 22) return 'defer';
89
+ // batch one busy room
90
+ if (channel === '12345' && tags.includes('chat:ambient')) return { debounce: 120000 };
91
+ return null; // let gate.json decide everything else
92
+ };
93
+ ```
94
+
95
+ `gate.js` runs **before** `gate.json` and wins when it returns a behavior. It's
96
+ hot-reloaded on save. Sync or async are both fine.
97
+
98
+ ### How it's run (and why)
99
+
100
+ You're trusted — you already have a shell. So this isn't a sandbox for *security*;
101
+ it's a couple of seatbelts so a bug in your own rule can't quietly break your own
102
+ attention:
103
+
104
+ - It runs on a **worker thread with a timeout** (default 50ms/event). If it ever
105
+ hangs (an accidental `while(true)`), it's killed and the event falls through to
106
+ `gate.json` — a hang in the main thread would otherwise freeze *all* your event
107
+ handling, which (unlike a crash) doesn't self-heal.
108
+ - If it **throws**, the event falls through and the error is surfaced in
109
+ `event_tags` / gate status (under `gateScript.lastError`) — so a typo makes you
110
+ fall back to declarative rules, not go silently deaf.
111
+
112
+ Keep it cheap: it runs on *every* event, and its whole job is the quick "should I
113
+ wake?" decision. Heavy logic belongs in your turn once you're up, not here.
114
+
115
+ ## Debugging
116
+
117
+ - `event_tags` — available tags + your `gate.js` status (runs / errors / timeouts).
118
+ - the gate status tool — per-policy match counts, debounce/rate-limit state, and
119
+ `defaultDecisions` (events that fell through to `default` — if something seems
120
+ ignored, check whether a policy is matching at all).