@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.
- package/.env.example +20 -0
- package/.github/workflows/publish.yml +65 -0
- package/ARCHITECTURE.md +355 -0
- package/CHANGELOG.md +30 -0
- package/HEADLESS-FLEET-PLAN.md +330 -0
- package/README.md +189 -0
- package/UNIFIED-TREE-PLAN.md +242 -0
- package/bun.lock +288 -0
- package/docs/AGENT-MEMORY-GUIDE.md +214 -0
- package/docs/AGENT-ONBOARDING.md +541 -0
- package/docs/ATTENTION-AND-GATING.md +120 -0
- package/docs/DEPLOYMENTS.md +130 -0
- package/docs/DEV-ENVIRONMENT.md +215 -0
- package/docs/LIBRARY-PIPELINE.md +286 -0
- package/docs/LOCUS-ROUTING-DESIGN.md +115 -0
- package/docs/claudeai-evacuation.md +228 -0
- package/docs/debug-context-api.md +208 -0
- package/docs/webui-deployment.md +219 -0
- package/package.json +33 -0
- package/recipes/SETUP.md +308 -0
- package/recipes/TRIUMVIRATE-SETUP.md +467 -0
- package/recipes/claude-export-revive.json +19 -0
- package/recipes/clerk.json +157 -0
- package/recipes/knowledge-miner.json +174 -0
- package/recipes/knowledge-reviewer.json +76 -0
- package/recipes/mcpl-editor-test.json +38 -0
- package/recipes/prompts/transplant-addendum.md +17 -0
- package/recipes/triumvirate.json +63 -0
- package/recipes/webui-fleet-test.json +27 -0
- package/recipes/webui-test.json +16 -0
- package/recipes/zulip-miner.json +87 -0
- package/scripts/connectome-doctor +373 -0
- package/scripts/evacuator.ts +956 -0
- package/scripts/import-claudeai-export.ts +747 -0
- package/scripts/lib/line-reader.ts +53 -0
- package/scripts/test-historical-thinking.ts +148 -0
- package/scripts/warmup-session.ts +336 -0
- package/src/agent-name.ts +58 -0
- package/src/commands.ts +1074 -0
- package/src/headless.ts +528 -0
- package/src/index.ts +867 -0
- package/src/logging-adapter.ts +208 -0
- package/src/mcpl-config.ts +199 -0
- package/src/modules/activity-module.ts +270 -0
- package/src/modules/channel-mode-module.ts +260 -0
- package/src/modules/fleet-module.ts +1705 -0
- package/src/modules/fleet-types.ts +225 -0
- package/src/modules/lessons-module.ts +465 -0
- package/src/modules/mcpl-admin-module.ts +345 -0
- package/src/modules/retrieval-module.ts +327 -0
- package/src/modules/settings-module.ts +143 -0
- package/src/modules/subagent-module.ts +2074 -0
- package/src/modules/subscription-gc-module.ts +322 -0
- package/src/modules/time-module.ts +85 -0
- package/src/modules/tui-module.ts +76 -0
- package/src/modules/web-ui-curve-page.ts +249 -0
- package/src/modules/web-ui-module.ts +2595 -0
- package/src/recipe.ts +1003 -0
- package/src/session-manager.ts +278 -0
- package/src/state/agent-tree-reducer.ts +431 -0
- package/src/state/fleet-tree-aggregator.ts +195 -0
- package/src/strategies/frontdesk-strategy.ts +364 -0
- package/src/synesthete.ts +68 -0
- package/src/tui.ts +2200 -0
- package/src/types/bun-ffi.d.ts +6 -0
- package/src/web/protocol.ts +648 -0
- package/test/agent-name.test.ts +62 -0
- package/test/agent-tree-fleet-launch.test.ts +64 -0
- package/test/agent-tree-reducer-parity.test.ts +194 -0
- package/test/agent-tree-reducer.test.ts +443 -0
- package/test/agent-tree-rollup.test.ts +109 -0
- package/test/autobio-progress-snapshot.test.ts +40 -0
- package/test/claudeai-export-importer.test.ts +386 -0
- package/test/evacuator.test.ts +332 -0
- package/test/fleet-commands.test.ts +244 -0
- package/test/fleet-no-subfleets.test.ts +147 -0
- package/test/fleet-orchestration.test.ts +353 -0
- package/test/fleet-recipe.test.ts +124 -0
- package/test/fleet-route.test.ts +44 -0
- package/test/fleet-smoke.test.ts +479 -0
- package/test/fleet-subscribe-union-e2e.test.ts +123 -0
- package/test/fleet-subscribe-union.test.ts +85 -0
- package/test/fleet-tree-aggregator-e2e.test.ts +110 -0
- package/test/fleet-tree-aggregator.test.ts +215 -0
- package/test/frontdesk-strategy.test.ts +326 -0
- package/test/headless-describe.test.ts +182 -0
- package/test/headless-smoke.test.ts +190 -0
- package/test/logging-adapter.test.ts +87 -0
- package/test/mcpl-admin-module.test.ts +213 -0
- package/test/mcpl-agent-overlay.test.ts +126 -0
- package/test/mock-headless-child.ts +133 -0
- package/test/recipe-cache-ttl.test.ts +37 -0
- package/test/recipe-env.test.ts +157 -0
- package/test/recipe-path-resolution.test.ts +170 -0
- package/test/subagent-async-timeout.test.ts +381 -0
- package/test/subagent-fork-materialise.test.ts +241 -0
- package/test/subagent-peek-scoping.test.ts +180 -0
- package/test/subagent-peek-zombie.test.ts +148 -0
- package/test/subagent-reaper-in-flight.test.ts +229 -0
- package/test/subscription-gc-module.test.ts +136 -0
- package/test/warmup-handoff.test.ts +150 -0
- package/test/web-ui-bind.test.ts +51 -0
- package/test/web-ui-module.test.ts +246 -0
- package/test/web-ui-protocol.test.ts +0 -0
- package/tsconfig.json +14 -0
- package/web/bun.lock +357 -0
- package/web/index.html +12 -0
- package/web/package.json +24 -0
- package/web/src/App.tsx +1931 -0
- package/web/src/Context.tsx +158 -0
- package/web/src/ContextDocument.tsx +150 -0
- package/web/src/Files.tsx +271 -0
- package/web/src/Lessons.tsx +164 -0
- package/web/src/Mcpl.tsx +310 -0
- package/web/src/Stream.tsx +119 -0
- package/web/src/TreeSidebar.tsx +283 -0
- package/web/src/Usage.tsx +182 -0
- package/web/src/main.tsx +7 -0
- package/web/src/styles.css +26 -0
- package/web/src/tree.ts +268 -0
- package/web/src/wire.ts +120 -0
- package/web/tsconfig.json +21 -0
- 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).
|