mercury-agent 0.16.0 → 0.16.2
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/README.md +6 -0
- package/container/Dockerfile +6 -1
- package/container/Dockerfile.base +6 -1
- package/docs/README.md +50 -0
- package/docs/authoring-profiles.md +8 -0
- package/docs/behavior-layers.md +193 -0
- package/docs/extensions.md +2 -0
- package/docs/goals/football-reporter-profile/decisions.md +479 -0
- package/docs/goals/football-reporter-profile/goal.md +91 -0
- package/docs/goals/football-reporter-profile/roadmap.md +196 -0
- package/docs/goals/whatsapp-bot-hardening/README.md +43 -0
- package/docs/goals/whatsapp-bot-hardening/archive/ambient-group-context.md +211 -0
- package/docs/goals/whatsapp-bot-hardening/archive/command-routing-consistency.md +210 -0
- package/docs/goals/whatsapp-bot-hardening/archive/destructive-command-guards.md +218 -0
- package/docs/goals/whatsapp-bot-hardening/archive/docs-extension.md +182 -0
- package/docs/goals/whatsapp-bot-hardening/archive/handoff-2026-08-09.md +588 -0
- package/docs/goals/whatsapp-bot-hardening/archive/media-size-and-silent-drop.md +178 -0
- package/docs/goals/whatsapp-bot-hardening/archive/member-memory.md +199 -0
- package/docs/goals/whatsapp-bot-hardening/archive/message-author-attribution.md +211 -0
- package/docs/goals/whatsapp-bot-hardening/archive/run-from-source-switchover.md +212 -0
- package/docs/goals/whatsapp-bot-hardening/archive/setup-plan-2026-08-06.md +1117 -0
- package/docs/goals/whatsapp-bot-hardening/decisions.md +200 -0
- package/docs/goals/whatsapp-bot-hardening/goal.md +75 -0
- package/docs/goals/whatsapp-bot-hardening/open-threads.md +396 -0
- package/docs/goals/whatsapp-bot-hardening/roadmap.md +245 -0
- package/docs/memory.md +30 -0
- package/docs/profile-guide.md +483 -0
- package/docs/refactor/.gitkeep +0 -0
- package/docs/refactor/archive/.gitkeep +0 -0
- package/docs/refactor/audits/.gitkeep +0 -0
- package/docs/refactor/backlog/.gitkeep +0 -0
- package/docs/skills-guide.md +154 -0
- package/examples/extensions/README.md +1 -0
- package/examples/extensions/feed-watch/config.ts +354 -0
- package/examples/extensions/feed-watch/digest.ts +344 -0
- package/examples/extensions/feed-watch/feeds.ts +341 -0
- package/examples/extensions/feed-watch/index.ts +302 -0
- package/examples/extensions/feed-watch/items.ts +171 -0
- package/examples/extensions/feed-watch/match.ts +107 -0
- package/examples/extensions/feed-watch/prompts/verify.md +35 -0
- package/examples/extensions/feed-watch/skill/SKILL.md +128 -0
- package/examples/extensions/feed-watch/watch.ts +548 -0
- package/examples/extensions/longview/hook.ts +179 -16
- package/examples/extensions/longview/index.ts +13 -2
- package/examples/extensions/longview/prompts/summarize.md +3 -2
- package/examples/extensions/longview/render/telegraph-nodes.ts +21 -1
- package/examples/extensions/longview/summarize.ts +57 -14
- package/examples/extensions/napkin/index.ts +334 -86
- package/examples/extensions/napkin/pi-spawn.ts +196 -0
- package/examples/extensions/pinchtab/index.ts +37 -4
- package/examples/extensions/pinchtab/lib/session-injector.ts +31 -9
- package/examples/extensions/pinchtab/skill/SKILL.md +29 -0
- package/examples/profiles/_template/AGENTS.md +138 -0
- package/examples/profiles/_template/README.md +65 -0
- package/examples/profiles/_template/config.yaml +57 -0
- package/examples/profiles/_template/tasks/daily.md +32 -0
- package/examples/profiles/football-reporter/AGENTS.md +219 -0
- package/examples/profiles/football-reporter/README.md +138 -0
- package/examples/profiles/football-reporter/config.yaml +174 -0
- package/examples/profiles/football-reporter/seed/MEMORY.md +42 -0
- package/examples/profiles/football-reporter/seed/episodes/barcelona-2026-27.md +25 -0
- package/examples/profiles/football-reporter/seed/episodes/beitar-jerusalem-2026-27.md +25 -0
- package/examples/profiles/football-reporter/seed/episodes/maccabi-haifa-2026-27.md +24 -0
- package/examples/profiles/football-reporter/seed/episodes/man-united-2026-27.md +26 -0
- package/examples/profiles/football-reporter/seed/episodes/real-madrid-2026-27.md +25 -0
- package/examples/profiles/football-reporter/seed/napkin-distill.md +55 -0
- package/examples/profiles/football-reporter/tasks/daily-article.md +142 -0
- package/package.json +8 -5
- package/src/adapters/whatsapp.ts +75 -13
- package/src/bridges/whatsapp.ts +6 -4
- package/src/cli/mrctl.ts +9 -1
- package/src/core/handler.ts +51 -6
- package/src/core/routes/dashboard.ts +106 -5
- package/src/core/routes/tasks.ts +28 -0
- package/src/core/runtime.ts +63 -2
- package/src/core/task-scheduler.ts +103 -12
- package/src/extensions/catalog.ts +9 -0
- package/src/profile/space-profile.ts +780 -0
- package/src/storage/db.ts +215 -0
- package/src/types.ts +56 -0
|
@@ -0,0 +1,1117 @@
|
|
|
1
|
+
> **ARCHIVED 2026-08-10 — Phases 0–5 complete, the bot is live.**
|
|
2
|
+
>
|
|
3
|
+
> Kept for the traps and the config reference, which are still accurate and
|
|
4
|
+
> still the only record of several live settings. **Parts of it are now stale:**
|
|
5
|
+
>
|
|
6
|
+
> | Section | Status |
|
|
7
|
+
> |---|---|
|
|
8
|
+
> | Decision C/D (Gemini API key, `gemini-*` models) | **Superseded** — the bot runs `claude-opus-5` over Claude-subscription OAuth. The whole "Free-tier rate limits" section is history, not current config. |
|
|
9
|
+
> | Decision E (install via global npm) | **Superseded** by D-001 — the service runs from the `~/mercury-src` git checkout. `npm update -g` is no longer part of upgrades, so the "erases both patches" warning no longer applies. |
|
|
10
|
+
> | "`mercury auth status` is cwd-sensitive" | **Fixed 2026-08-10** — see the roadmap's "The cwd trap" section. |
|
|
11
|
+
> | `media.max_size_mb: 10` implications | **Superseded** — raised to 64 (D-004). |
|
|
12
|
+
> | Phase 5 "Personality / tone → `AGENTS.md` … or `mrctl character set`" | **Incomplete** — there are five prompt layers, not two. See `open-threads.md` §A. |
|
|
13
|
+
> | Risk register row on the `.dockerignore` build fix | **Fixed and committed** in the hardening branch. |
|
|
14
|
+
>
|
|
15
|
+
> Still live and unresolved from this file: **WSL does not auto-start** after
|
|
16
|
+
> `wsl --shutdown` or a Windows reboot ("Keeping it running"). Tracked in
|
|
17
|
+
> `docs/goals/whatsapp-bot-hardening/open-threads.md` §D.
|
|
18
|
+
|
|
19
|
+
# WhatsApp Bot on Mercury — Setup Plan
|
|
20
|
+
|
|
21
|
+
Personal WhatsApp assistant, running locally, built on
|
|
22
|
+
[Mercury](https://github.com/Avishai-Tsabari/mercury) (v0.8.12).
|
|
23
|
+
|
|
24
|
+
Project layout:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
D:\Projects\whatsapp-bot\
|
|
28
|
+
├── mercury\ # cloned source repo (reference + `mercury build` source)
|
|
29
|
+
└── PLAN.md # this file
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The runtime project (`.env`, `mercury.yaml`, `.mercury/`) does **not** live here —
|
|
33
|
+
see Phase 0, decision A.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## What Mercury actually is
|
|
38
|
+
|
|
39
|
+
A host process (Bun) that runs platform adapters (WhatsApp via Baileys, Slack,
|
|
40
|
+
Discord, Telegram), a router/queue, and a SQLite DB. Each incoming message is
|
|
41
|
+
routed to a **space** (a memory boundary), and the agent for that space runs
|
|
42
|
+
**inside a Docker container** using [pi](https://github.com/badlogic/pi) as the
|
|
43
|
+
LLM runtime. There's also a web dashboard on port 8787.
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
WhatsApp adapter ─┐
|
|
47
|
+
Scheduler ────────┼─→ Router/Queue → SQLite → Docker container (pi CLI)
|
|
48
|
+
┘ /spaces/<space-id>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Consequences for us:
|
|
52
|
+
|
|
53
|
+
- **Docker is mandatory.** No Docker, no agent. The base image is ~2.8 GB.
|
|
54
|
+
- WhatsApp uses the **linked-devices** protocol (Baileys). **Corrected
|
|
55
|
+
2026-08-06:** there is *no* fixed re-scan interval. The 14-day clock is on the
|
|
56
|
+
**primary phone being offline** — linked devices are force-logged-out only if
|
|
57
|
+
the phone goes 14 days without connecting to WhatsApp at all. Keep the bot's
|
|
58
|
+
phone online (or open the app once a fortnight) and the link persists
|
|
59
|
+
indefinitely. See "Re-auth" under Ongoing operations.
|
|
60
|
+
- Out of the box there is no web search, no browsing, no vault, no voice — those
|
|
61
|
+
are extensions (`mercury add web-browser`, `mercury add napkin`, …).
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Environment audit (2026-08-05)
|
|
66
|
+
|
|
67
|
+
| Requirement | Status |
|
|
68
|
+
|---|---|
|
|
69
|
+
| Git 2.50.1 | OK |
|
|
70
|
+
| Node v24.18.1 (≥18) | OK |
|
|
71
|
+
| Bun 1.3.14 on Windows (≥1.2) | OK — but see below |
|
|
72
|
+
| **WSL2** | **MISSING** — kernel not installed, zero distros |
|
|
73
|
+
| **Docker** | **MISSING** — not on PATH at all |
|
|
74
|
+
|
|
75
|
+
The Windows-side Bun does not help: Mercury's README states it "runs best under
|
|
76
|
+
WSL2", and the agent sandbox uses **bubblewrap** inside the container, which is
|
|
77
|
+
Linux-only. Everything must run inside WSL2, so Bun has to be installed *there*
|
|
78
|
+
too.
|
|
79
|
+
|
|
80
|
+
**These two gaps are the whole critical path.** Nothing else can be tested until
|
|
81
|
+
they're closed.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Phase 0 — Decisions (RESOLVED 2026-08-05)
|
|
86
|
+
|
|
87
|
+
| # | Decision | Chosen |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| A | Runtime project location | **WSL ext4 — `~/whatsapp-bot`** (clone stays at `D:\Projects\whatsapp-bot\mercury`) |
|
|
90
|
+
| B | Phone number | **Dedicated number** (second SIM / data-only eSIM) |
|
|
91
|
+
| C | LLM auth | ~~OAuth via Claude subscription~~ → ~~Google AI Studio API key~~ → **re-revised 2026-08-06: OAuth via Claude subscription** (`mercury auth login anthropic`) — verified working |
|
|
92
|
+
| D | Model | ~~`claude-sonnet-5`~~ → ~~`gemini-3.6-flash`~~ → **re-revised 2026-08-06: `claude-opus-5`** (provider `anthropic`) — verified serving, see "Opus 5 vs the google legs" |
|
|
93
|
+
| E | Install method | Global npm — `npm i -g mercury-agent` |
|
|
94
|
+
|
|
95
|
+
Rationale for each is retained below.
|
|
96
|
+
|
|
97
|
+
### A. Where does the runtime project live? → WSL ext4
|
|
98
|
+
|
|
99
|
+
Mercury bind-mounts each space's workspace (`.mercury/spaces/<id>/`) into a
|
|
100
|
+
Docker container. Two options:
|
|
101
|
+
|
|
102
|
+
| | `/mnt/d/Projects/whatsapp-bot` (Windows drive) | `~/whatsapp-bot` (WSL ext4) |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| File I/O | Slow (9p/drvfs translation) | Native |
|
|
105
|
+
| Docker bind-mount perms | uid/gid mapping quirks, `chmod` is a no-op | Clean |
|
|
106
|
+
| SQLite | Locking over drvfs is a known problem source | Fine |
|
|
107
|
+
| Editing from Windows | Direct | Via `\\wsl$\Ubuntu\home\...` or VS Code Remote |
|
|
108
|
+
|
|
109
|
+
**Recommendation:** put the *runtime* project at `~/whatsapp-bot` inside WSL, and
|
|
110
|
+
keep this cloned repo at `D:\Projects\whatsapp-bot\mercury` for reference and for
|
|
111
|
+
`mercury build`. If you'd rather keep everything on `D:`, it will probably work
|
|
112
|
+
but expect slowness and permission friction.
|
|
113
|
+
|
|
114
|
+
### B. Which phone number? → **dedicated** (reconfirmed 2026-08-05)
|
|
115
|
+
|
|
116
|
+
Mercury's docs recommend a dedicated number, but the decisive reason is
|
|
117
|
+
structural, not just caution:
|
|
118
|
+
|
|
119
|
+
> **A personal-number bot cannot be talked to by its owner.** `handler.ts:158`
|
|
120
|
+
> returns immediately on `message.author.isMe`, and the WhatsApp adapter sets
|
|
121
|
+
> that from Baileys' `fromMe` flag (`whatsapp.ts:481`). Every message you send
|
|
122
|
+
> from your own phone on that account is `fromMe: true`, so the bot would ignore
|
|
123
|
+
> you and only ever reply to *other people* messaging your number.
|
|
124
|
+
|
|
125
|
+
Secondary reasons:
|
|
126
|
+
|
|
127
|
+
- **Blast radius.** `.mercury/whatsapp-auth/` is password-equivalent; on a
|
|
128
|
+
personal number a leak is a full takeover of your primary account.
|
|
129
|
+
- **Ban asymmetry.** Baileys is an unofficial client. Losing a burner is an
|
|
130
|
+
annoyance; losing your primary number costs your history, groups, and contact
|
|
131
|
+
point.
|
|
132
|
+
- **Keeping the link alive.** Not a fixed re-scan interval (see the corrected
|
|
133
|
+
note above) — but a *dedicated* SIM that normally sits powered off **will** trip
|
|
134
|
+
the 14-days-offline rule and force a re-scan. A dedicated number only stays
|
|
135
|
+
hands-off if its phone stays online.
|
|
136
|
+
|
|
137
|
+
What a linked device actually exposes, precisely: every message arrives over the
|
|
138
|
+
socket, but **unlinked conversations are dropped at `handler.ts:191`** before any
|
|
139
|
+
storage or LLM call — nothing reaches Gemini. However `resolveConversation` calls
|
|
140
|
+
`db.ensureConversation` *first*, so a metadata row (platform, external ID, kind,
|
|
141
|
+
title) is recorded for every chat that messages the account.
|
|
142
|
+
|
|
143
|
+
**Alternative considered:** Telegram ingress (`ingress.telegram`) uses the
|
|
144
|
+
*official* Bot API — no number, no QR, no expiry, no ban risk. Worth revisiting
|
|
145
|
+
if the WhatsApp session churn becomes tiresome.
|
|
146
|
+
|
|
147
|
+
### C. Auth method for the LLM — **revised to Gemini free tier**
|
|
148
|
+
|
|
149
|
+
Mercury supports four providers by API key (`src/cli/mercury.ts:747`):
|
|
150
|
+
|
|
151
|
+
| Provider | `.env` key | Default model |
|
|
152
|
+
|---|---|---|
|
|
153
|
+
| `anthropic` | `MERCURY_ANTHROPIC_API_KEY` | `claude-opus-4-8` |
|
|
154
|
+
| `openai` | `MERCURY_OPENAI_API_KEY` | `gpt-4o` |
|
|
155
|
+
| `google` | `MERCURY_GEMINI_API_KEY` | `gemini-2.5-flash` |
|
|
156
|
+
| `groq` | `MERCURY_GROQ_API_KEY` | `llama-3.3-70b-versatile` |
|
|
157
|
+
|
|
158
|
+
> **Trap — OAuth only works for two of the five providers it offers.**
|
|
159
|
+
> `mercury auth login` advertises `anthropic`, `github-copilot`,
|
|
160
|
+
> `google-gemini-cli`, `antigravity`, and `openai-codex`. But
|
|
161
|
+
> `src/storage/pi-auth.ts:23-25` maps only **`anthropic`** →
|
|
162
|
+
> `ANTHROPIC_OAUTH_TOKEN` and **`github-copilot`** → `COPILOT_GITHUB_TOKEN` into
|
|
163
|
+
> the agent container. The other three have no env var, so the login *succeeds*
|
|
164
|
+
> and the model leg is then "dead on arrival" — logged, not thrown, so it fails
|
|
165
|
+
> quietly at message time rather than at setup. **Gemini must be an API key, not
|
|
166
|
+
> OAuth.**
|
|
167
|
+
|
|
168
|
+
A **free** Claude account does not grant OAuth for external tools — that flow
|
|
169
|
+
needs Pro or Max. Hence the switch to Google AI Studio's free tier
|
|
170
|
+
(<https://aistudio.google.com/apikey>), which needs no subscription.
|
|
171
|
+
|
|
172
|
+
Gemini over Groq's Llama because Mercury runs an **agentic loop** (pi, with tool
|
|
173
|
+
use), not single-shot chat, and Llama is markedly weaker at multi-step tool
|
|
174
|
+
calling.
|
|
175
|
+
|
|
176
|
+
#### Two bugs hit while wiring this up — both cost a full debug cycle
|
|
177
|
+
|
|
178
|
+
**1. `model.chain` alone is not enough — you must also set `model.provider`.**
|
|
179
|
+
|
|
180
|
+
Setting only the chain leaves the legacy scalar `modelProvider` at its schema
|
|
181
|
+
default `"anthropic"` (`config.ts:65`), because `config-file.ts:323` assigns it
|
|
182
|
+
only from `model.provider`. The guard at `container-runner.ts:892` reads *that
|
|
183
|
+
scalar*, not the resolved chain — so with a pure-Gemini chain every turn was
|
|
184
|
+
refused with:
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
Refusing to start container: no Anthropic credential configured
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
which is actively misleading: nothing in the config mentions Anthropic. The
|
|
191
|
+
working config sets **both** forms:
|
|
192
|
+
|
|
193
|
+
```yaml
|
|
194
|
+
model:
|
|
195
|
+
provider: google # satisfies the container-runner guard
|
|
196
|
+
model: gemini-3.6-flash
|
|
197
|
+
chain:
|
|
198
|
+
- provider: google # drives the actual request path
|
|
199
|
+
model: gemini-3.6-flash
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**2. `gemini-2.5-flash` is retired for new API keys.** Mercury's own default for
|
|
203
|
+
the `google` provider (`src/cli/mercury.ts:768`) now fails:
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
404 — This model models/gemini-2.5-flash is no longer available to new users.
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
It still appears in `GET /v1beta/models`, so *listing a model does not mean you
|
|
210
|
+
can call it*. Probed each candidate with a real `generateContent` call:
|
|
211
|
+
|
|
212
|
+
| Model | Result |
|
|
213
|
+
|---|---|
|
|
214
|
+
| `gemini-3.6-flash` | ✅ **chosen** |
|
|
215
|
+
| `gemini-3.5-flash` | ✅ |
|
|
216
|
+
| `gemini-flash-latest` | ✅ (alias — tracks newest, can shift under you) |
|
|
217
|
+
| `gemini-3-flash-preview` | ✅ |
|
|
218
|
+
| `gemini-2.0-flash` | ❌ 429 quota |
|
|
219
|
+
|
|
220
|
+
To re-probe later, list with
|
|
221
|
+
`curl "https://generativelanguage.googleapis.com/v1beta/models?key=$KEY"` and
|
|
222
|
+
then actually POST to `:generateContent` — the list alone will mislead you.
|
|
223
|
+
|
|
224
|
+
### D. Model
|
|
225
|
+
|
|
226
|
+
`MERCURY_MODEL` is a free-form string handed to pi (`src/config.ts:66`), so it's
|
|
227
|
+
not pinned to a list. Current options and list pricing per million tokens:
|
|
228
|
+
|
|
229
|
+
| Model ID | Input | Output |
|
|
230
|
+
|---|---|---|
|
|
231
|
+
| `claude-opus-5` | $5 | $25 |
|
|
232
|
+
| `claude-opus-4-8` (Mercury's default) | $5 | $25 |
|
|
233
|
+
| `claude-sonnet-5` | $3 ($2 intro thru 2026-08-31) | $15 ($10 intro) |
|
|
234
|
+
| `claude-haiku-4-5` | $1 | $5 |
|
|
235
|
+
|
|
236
|
+
For a personal chat bot, **`claude-sonnet-5`** is the sensible default —
|
|
237
|
+
near-Opus quality on agentic work at ~60% of the cost. Move to `claude-opus-5`
|
|
238
|
+
if you want maximum capability.
|
|
239
|
+
|
|
240
|
+
Two caveats, both only observable at runtime:
|
|
241
|
+
|
|
242
|
+
1. pi (`@earendil-works/pi-ai ~0.79.10`) must recognise the ID. If it errors,
|
|
243
|
+
fall back to `claude-opus-4-8`.
|
|
244
|
+
2. **We chose OAuth (decision C), so the model set is whatever the Claude
|
|
245
|
+
subscription grants** — that is not the same surface as a pay-per-token API
|
|
246
|
+
key, and the per-token prices above then don't apply (you're on plan limits
|
|
247
|
+
instead). If `claude-sonnet-5` isn't reachable over OAuth, either drop to
|
|
248
|
+
`claude-opus-4-8` or switch to an API key. Verify at Phase 4 with
|
|
249
|
+
`mercury chat "hello"` before wiring WhatsApp up.
|
|
250
|
+
|
|
251
|
+
### E. Install method
|
|
252
|
+
|
|
253
|
+
- **Global npm package** (`npm i -g mercury-agent`) — what the docs assume;
|
|
254
|
+
`npm update -g` to upgrade. Recommended.
|
|
255
|
+
- **From this clone** (`bun link` in `mercury/`) — only if you intend to modify
|
|
256
|
+
Mercury itself.
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Phase 1 — Close the environment gaps ✅ COMPLETE
|
|
261
|
+
|
|
262
|
+
### Status
|
|
263
|
+
|
|
264
|
+
| Item | State |
|
|
265
|
+
|---|---|
|
|
266
|
+
| WSL2 + Ubuntu **26.04 LTS** (`resolute`), user `guysaha` | ✅ done |
|
|
267
|
+
| systemd as PID 1 (`/etc/wsl.conf` already has `[boot] systemd=true`) | ✅ done — `mercury service install` will work |
|
|
268
|
+
| Linux-native Node **v24.19.0** + npm **11.17.0** via nvm (`~/.nvm`) | ✅ done |
|
|
269
|
+
| `unzip` | ✅ `/usr/bin/unzip` |
|
|
270
|
+
| Bun **1.3.14** (Linux, `~/.bun/bin/bun`) | ✅ done |
|
|
271
|
+
| Docker **29.1.3** + buildx **0.30.1**, daemon running, `guysaha` in `docker` group | ✅ done |
|
|
272
|
+
|
|
273
|
+
### Trap: WSL PATH interop shadows Linux tooling
|
|
274
|
+
|
|
275
|
+
Before nvm, `bun` and `npm` inside WSL resolved to **Windows** binaries
|
|
276
|
+
(`/mnt/c/Users/.../npm/bun`, `/mnt/c/Program Files/nodejs/npm`). Running
|
|
277
|
+
`npm install -g mercury-agent` in that state would have used the Windows npm and
|
|
278
|
+
installed into the Windows prefix. nvm now prepends `~/.nvm/.../bin`, so Node and
|
|
279
|
+
npm resolve correctly — verify with `which node npm` before any global install.
|
|
280
|
+
|
|
281
|
+
nvm was chosen over `apt install nodejs npm` deliberately: global installs land in
|
|
282
|
+
`~/.nvm`, so `npm install -g mercury-agent` and later `npm update -g` never need
|
|
283
|
+
sudo.
|
|
284
|
+
|
|
285
|
+
**Symptom to recognise.** A WSL shell opened *before* the `~/.bashrc` fix will
|
|
286
|
+
still have the old PATH, and every `mercury` command in it dies with:
|
|
287
|
+
|
|
288
|
+
```
|
|
289
|
+
error: ELOOP reading "\\wsl.localhost\Ubuntu\home\guysaha\.nvm\...\bin\mercury"
|
|
290
|
+
Bun v1.3.14 (Windows x64)
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
The `mercury` shim is `#!/usr/bin/env bun`, so a stale PATH resolves `bun` to
|
|
294
|
+
`/mnt/c/.../npm/bun` — the **Windows** binary — which sees the Linux script path
|
|
295
|
+
as a UNC share and can't follow npm's symlink chain across it. Fix:
|
|
296
|
+
`exec bash -l`, or open a new terminal. Check with `command -v bun`; it must be
|
|
297
|
+
`/home/guysaha/.bun/bin/bun`, never `/mnt/c/...`.
|
|
298
|
+
|
|
299
|
+
### What was done
|
|
300
|
+
|
|
301
|
+
`docker.io` and `docker-buildx` live in the **universe** component and the apt
|
|
302
|
+
lists only indexed main/restricted, so `apt-get update` had to run first:
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
sudo apt-get update
|
|
306
|
+
sudo apt-get install -y unzip docker.io docker-buildx
|
|
307
|
+
sudo usermod -aG docker "$USER"
|
|
308
|
+
sudo systemctl enable --now docker
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Followed by `wsl --shutdown` from Windows PowerShell to pick up the new `docker`
|
|
312
|
+
group membership. Verified: `docker info` reports server **29.1.3**, default
|
|
313
|
+
runtime `runc`, reachable as `guysaha` without sudo.
|
|
314
|
+
|
|
315
|
+
Note this is Docker **Engine**, not Desktop — bubblewrap may not be able to
|
|
316
|
+
create namespaces inside the agent container. Left
|
|
317
|
+
`agent.container_bwrap_docker_compat: false` for now (stronger sandbox); flip it
|
|
318
|
+
to `true` only if the logs show
|
|
319
|
+
`bwrap: Creating new namespace failed: Operation not permitted`.
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## Phase 2 — Create the assistant project ✅ COMPLETE
|
|
324
|
+
|
|
325
|
+
Installed and scaffolded at `~/whatsapp-bot` inside WSL:
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
curl -fsSL https://bun.sh/install | bash # bun 1.3.14 → ~/.bun/bin
|
|
329
|
+
npm install -g --allow-scripts=@whiskeysockets/baileys,@google/genai,protobufjs mercury-agent
|
|
330
|
+
mkdir ~/whatsapp-bot && cd ~/whatsapp-bot
|
|
331
|
+
mercury init # non-interactive twin of `mercury setup`
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Three things worth remembering:
|
|
335
|
+
|
|
336
|
+
1. **`mercury setup` is interactive**; `mercury init` is the scriptable
|
|
337
|
+
equivalent. It writes `.env`, `.mercury/`, and `mercury.example.yaml`.
|
|
338
|
+
2. **npm 11 blocks postinstall scripts by default.** The first
|
|
339
|
+
`npm i -g mercury-agent` silently skipped Baileys' and protobufjs'
|
|
340
|
+
postinstalls — which WhatsApp needs. The `--allow-scripts=` form above fixes
|
|
341
|
+
it. If you ever reinstall or upgrade, use that flag again.
|
|
342
|
+
3. **`~/.bashrc` now pins Linux tooling ahead of `/mnt/c` interop** (see the
|
|
343
|
+
PATH trap above) — without it `bun` resolved to the Windows binary.
|
|
344
|
+
|
|
345
|
+
Configuration split, per Mercury's own convention: non-secrets in
|
|
346
|
+
`mercury.yaml`, secrets in `.env` only.
|
|
347
|
+
|
|
348
|
+
`mercury.yaml` sets `ingress.whatsapp: true`, the model chain
|
|
349
|
+
(`anthropic` / `claude-sonnet-5`), `trigger.match: mention`, and the agent image.
|
|
350
|
+
`.env` has the Anthropic API key line **commented out** — decision C is OAuth, so
|
|
351
|
+
credentials come from `mercury auth login` instead. A `.gitignore` covering
|
|
352
|
+
`.env`, `.mercury/`, and `node_modules/` was added up front.
|
|
353
|
+
|
|
354
|
+
> **Trigger semantics, confirmed in `src/core/trigger.ts:65`:** in DMs the bot
|
|
355
|
+
> replies to *everything* — the trigger pattern is optional and stripped if
|
|
356
|
+
> present. `match: mention` only constrains **group** chats. So a DM to the bot
|
|
357
|
+
> number just works.
|
|
358
|
+
|
|
359
|
+
### Agent image: GHCR is private, so we build locally
|
|
360
|
+
|
|
361
|
+
`docker pull ghcr.io/avishai-tsabari/mercury-agent:latest` fails with
|
|
362
|
+
`unauthorized` — that package isn't publicly readable. Two ways out:
|
|
363
|
+
|
|
364
|
+
| | |
|
|
365
|
+
|---|---|
|
|
366
|
+
| `docker login ghcr.io` with a GitHub PAT (`read:packages`) | Fast, but needs a token |
|
|
367
|
+
| **Build from source** (chosen) | No credentials; ~2.8 GB build |
|
|
368
|
+
|
|
369
|
+
Building from a WSL-side clone at `~/mercury-src` (not `/mnt/d`, which would drag
|
|
370
|
+
the whole build context over drvfs):
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
git clone --depth 1 https://github.com/Avishai-Tsabari/mercury.git ~/mercury-src
|
|
374
|
+
cd ~/mercury-src && bash ./container/build.sh # → mercury-agent:latest
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Built successfully: **`mercury-agent:latest`, 5.16 GB** (larger than the ~2.8 GB
|
|
378
|
+
the docs quote — that figure predates the current Dockerfile). No `ghcr.io/`
|
|
379
|
+
prefix, so `agent.image` in `mercury.yaml` is set to match.
|
|
380
|
+
|
|
381
|
+
Contents verified inside the image: `bun`, `pi`, `mrctl`, `bwrap` all present.
|
|
382
|
+
Note `.env.example` claims custom images need `agent-browser`, `napkin`, and
|
|
383
|
+
`mercury-ctl` — that comment is **stale**. The real host-bridge binary is
|
|
384
|
+
`mrctl` (`Dockerfile:126`), and `agent-browser`/`napkin` are extension CLIs baked
|
|
385
|
+
into the *derived* image (`mercury-agent-ext:<hash>`) only after
|
|
386
|
+
`mercury add web-browser` / `mercury add napkin`. Nothing is missing.
|
|
387
|
+
|
|
388
|
+
### Bubblewrap needs compat mode — confirmed empirically
|
|
389
|
+
|
|
390
|
+
Rather than wait to discover this at first message, both modes were tested
|
|
391
|
+
directly against the built image:
|
|
392
|
+
|
|
393
|
+
```bash
|
|
394
|
+
docker run --rm --entrypoint sh mercury-agent:latest \
|
|
395
|
+
-c 'bwrap --dev-bind / / --proc /proc --tmpfs /tmp true'
|
|
396
|
+
# → bwrap: No permissions to create new namespace
|
|
397
|
+
|
|
398
|
+
docker run --rm --security-opt seccomp=unconfined --cap-add SYS_ADMIN \
|
|
399
|
+
--entrypoint sh mercury-agent:latest \
|
|
400
|
+
-c 'bwrap --dev-bind / / --proc /proc --tmpfs /tmp true'
|
|
401
|
+
# → OK
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
So `agent.container_bwrap_docker_compat: true` is **required** on this host and
|
|
405
|
+
is already set. Bubblewrap stays enabled — this loosens the outer Docker sandbox
|
|
406
|
+
but keeps the inner one, which is the better trade than
|
|
407
|
+
`MERCURY_DISABLE_BUBBLEWRAP=1`.
|
|
408
|
+
|
|
409
|
+
### `mercury doctor` after Phase 2
|
|
410
|
+
|
|
411
|
+
**5 passed, 2 warnings, 2 errors.** Both errors are the interactive steps below
|
|
412
|
+
(AI credentials, WhatsApp auth); one warning clears when you set
|
|
413
|
+
`permissions.admins`, the other when the DB is created on first run. Everything
|
|
414
|
+
mechanical is green: Docker, agent image found, Bun 1.3.14, `.env`, port 8787.
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
## Phase 3 — Link WhatsApp ✅ COMPLETE
|
|
419
|
+
|
|
420
|
+
### Model pipeline — ✅ verified end to end
|
|
421
|
+
|
|
422
|
+
`mercury chat "Reply with exactly: it works"` → `it works`. That exercises HTTP →
|
|
423
|
+
router → Docker container → pi → Gemini → reply, so the whole runtime is proven
|
|
424
|
+
before WhatsApp is added.
|
|
425
|
+
|
|
426
|
+
Two confirmations from that run:
|
|
427
|
+
|
|
428
|
+
- **bwrap compat is working** — `Enabling bwrap Docker compat with --privileged
|
|
429
|
+
(containerBwrapDockerCompat=true)`, then `Container started`.
|
|
430
|
+
- **`mercury chat` needs the service running.** It POSTs to
|
|
431
|
+
`http://localhost:8787/chat` (`src/cli/mercury.ts:2037`); with nothing
|
|
432
|
+
listening it fails with the unhelpful `Error: Unable to connect. Is the
|
|
433
|
+
computer able to access the url?`
|
|
434
|
+
|
|
435
|
+
### Remaining: the QR scan
|
|
436
|
+
|
|
437
|
+
`docs/setup-whatsapp.md` runs `mercury auth whatsapp` **before**
|
|
438
|
+
`mercury service install`, and its re-auth procedure stops the service first —
|
|
439
|
+
two processes must not contend for `.mercury/whatsapp-auth/`. The service was
|
|
440
|
+
installed early here, so it has been **stopped**; start it again after auth.
|
|
441
|
+
|
|
442
|
+
If Gemini's free tier proves too rate-limited in practice, uncomment the
|
|
443
|
+
`anthropic` fallback leg in `mercury.yaml` — the chain falls through only when
|
|
444
|
+
the earlier leg fails. Then:
|
|
445
|
+
|
|
446
|
+
```bash
|
|
447
|
+
mercury auth whatsapp # prints a QR in the terminal
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
On the **dedicated** phone (decision B): **Settings → Linked Devices → Link a
|
|
451
|
+
Device**, scan. If the QR expires too fast, use
|
|
452
|
+
`mercury auth whatsapp --pairing-code --phone <number>`.
|
|
453
|
+
|
|
454
|
+
The command prints the bot's WhatsApp ID on success. Put **your own** ID (the
|
|
455
|
+
number you'll message the bot *from*) into `mercury.yaml` → `permissions.admins`.
|
|
456
|
+
Mercury canonicalises WhatsApp's LID (`24417...@lid`) vs phone JID
|
|
457
|
+
(`972...@s.whatsapp.net`) forms; the dashboard's Conversations page shows which
|
|
458
|
+
form you have.
|
|
459
|
+
|
|
460
|
+
**Result:** linked and connected (`WhatsApp connection open`).
|
|
461
|
+
|
|
462
|
+
| | |
|
|
463
|
+
|---|---|
|
|
464
|
+
| Bot account | `972552538335:1@s.whatsapp.net` — "ויצ דה בוט" |
|
|
465
|
+
| Bot LID | `136043810578453:1@lid` |
|
|
466
|
+
| Admin (you) | `whatsapp:972503515999@s.whatsapp.net` |
|
|
467
|
+
|
|
468
|
+
> **`mercury doctor`'s "No AI credentials configured" is a false positive on any
|
|
469
|
+
> non-Anthropic provider.** `src/cli/mercury.ts:513-516` checks only
|
|
470
|
+
> `MERCURY_ANTHROPIC_API_KEY`, `MERCURY_ANTHROPIC_OAUTH_TOKEN`, and `auth.json`
|
|
471
|
+
> — it has no branch for `MERCURY_GEMINI_API_KEY`, `MERCURY_OPENAI_API_KEY`, or
|
|
472
|
+
> `MERCURY_GROQ_API_KEY`, even though `mercury setup` offers all four. Ignore
|
|
473
|
+
> that one error on this deployment; the "Port 8787 in use" warning is Mercury
|
|
474
|
+
> itself.
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
## Phase 4 — First run and wiring conversations ✅ COMPLETE — **BOT IS LIVE**
|
|
479
|
+
|
|
480
|
+
A WhatsApp DM to the bot number gets a reply. Confirmed round trip:
|
|
481
|
+
|
|
482
|
+
```
|
|
483
|
+
Message from: whatsapp:972503515999@s.whatsapp.net (סהר ויצמן)
|
|
484
|
+
Container started spaceId=main
|
|
485
|
+
Container exited exitCode=0 durationMs=6773
|
|
486
|
+
WhatsApp outbound chatJid=972503515999@s.whatsapp.net
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
**~7 s per turn** — Mercury starts a fresh container for every message.
|
|
490
|
+
|
|
491
|
+
### Gotcha: messages sent while the service is down are lost
|
|
492
|
+
|
|
493
|
+
The first test message never arrived. Baileys drops anything delivered before
|
|
494
|
+
`WhatsApp connection open`, and the service was stopped for the QR scan. Nothing
|
|
495
|
+
was broken — the message simply predated the connection. Verify with
|
|
496
|
+
`journalctl --user -u mercury | grep "connection open"` before concluding the
|
|
497
|
+
adapter is at fault.
|
|
498
|
+
|
|
499
|
+
Debug logging (`runtime.log_level: debug`) is what made this visible: `Incoming
|
|
500
|
+
message ... isMe=false` and `Message ignored: conversation not linked to a space`
|
|
501
|
+
are both DEBUG-level. It has since been set back to `info`.
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
The systemd **user** unit is already installed at
|
|
506
|
+
`~/.config/systemd/user/mercury.service`, so after the QR scan just start it:
|
|
507
|
+
|
|
508
|
+
```bash
|
|
509
|
+
systemctl --user start mercury
|
|
510
|
+
mercury service status
|
|
511
|
+
mercury service logs -f
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
> **Cosmetic upstream bug:** the generated unit writes
|
|
515
|
+
> `Environment=PATH=<...>` unquoted, and the WSL-interop PATH is full of
|
|
516
|
+
> `/mnt/c/Program Files/...` entries with spaces. systemd logs ~40
|
|
517
|
+
> `Invalid environment assignment, ignoring: Files` warnings and truncates PATH
|
|
518
|
+
> at the first space. Harmless here — `/usr/bin` (and therefore `docker`) comes
|
|
519
|
+
> before the truncation point — but the warnings are noise, and on a differently
|
|
520
|
+
> ordered PATH it would break container spawning. Fix upstream is quoting:
|
|
521
|
+
> `Environment="PATH=<...>"`.
|
|
522
|
+
|
|
523
|
+
Never use `mercury run` directly (per the repo's own `CLAUDE.md`): it blocks the
|
|
524
|
+
terminal and won't auto-restart.
|
|
525
|
+
|
|
526
|
+
Then, because conversations start **unlinked**:
|
|
527
|
+
|
|
528
|
+
```bash
|
|
529
|
+
mercury spaces create main
|
|
530
|
+
# message the bot number from your phone, then:
|
|
531
|
+
mercury conversations --unlinked
|
|
532
|
+
mercury link <conversation-id> main
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
**Exit criteria:** a WhatsApp DM to the bot number gets a reply.
|
|
536
|
+
|
|
537
|
+
### If the agent container fails to start
|
|
538
|
+
|
|
539
|
+
The bwrap namespace failure was already found and fixed pre-emptively (see Phase
|
|
540
|
+
2), so the most likely cause is gone. If containers still fail, check
|
|
541
|
+
`mercury service logs -f` — and if you see a bwrap error anyway, the last resort
|
|
542
|
+
is `MERCURY_DISABLE_BUBBLEWRAP=1`, which removes a security layer.
|
|
543
|
+
|
|
544
|
+
---
|
|
545
|
+
|
|
546
|
+
## Phase 5 — Make it actually useful
|
|
547
|
+
|
|
548
|
+
Pick from these; each is optional.
|
|
549
|
+
|
|
550
|
+
| Want | Do |
|
|
551
|
+
|---|---|
|
|
552
|
+
| Web search / browsing | `mercury add web-browser` (uses Brave, no API key) |
|
|
553
|
+
| Obsidian-style knowledge vault | `mercury add napkin` |
|
|
554
|
+
| Personality / tone | `AGENTS.md` in the space workspace, or bot-wide character via `mrctl character set` |
|
|
555
|
+
| Scheduled prompts ("brief me at 8am") | `mrctl tasks create` — see `docs/scheduler.md` |
|
|
556
|
+
| Web UI | Dashboard on `http://localhost:8787` — spaces, tasks, permissions, extensions |
|
|
557
|
+
| Separate work/family/personal memory | More spaces + `mercury link` |
|
|
558
|
+
| Voice notes, images, documents | Supported natively — `docs/media/overview.md` |
|
|
559
|
+
|
|
560
|
+
Extensions with CLIs trigger a derived Docker image rebuild
|
|
561
|
+
(`mercury-agent-ext:<hash>`) on next start. That's automatic; expect a slow first
|
|
562
|
+
boot after adding one.
|
|
563
|
+
|
|
564
|
+
---
|
|
565
|
+
|
|
566
|
+
## Web browsing — installed, and why it was silently broken
|
|
567
|
+
|
|
568
|
+
`Extensions loaded count=2` (`web-browser`, `voice-transcribe`) and the derived
|
|
569
|
+
image `mercury-agent-ext:e2c502ed9498` (5.25 GB) builds in ~3.4 min. Despite
|
|
570
|
+
that, the agent could not reach a single public URL.
|
|
571
|
+
|
|
572
|
+
### Three separate traps, in the order they bite
|
|
573
|
+
|
|
574
|
+
**1. `mercury add web-browser` does not work.** `README.md:169` and
|
|
575
|
+
`docs/web-search.md` both say to run it; the CLI rejects it — `add` only accepts
|
|
576
|
+
a path, `npm:`, `git:`, or `user/repo`. The name `web-browser` is a *dashboard
|
|
577
|
+
catalog* label (`src/extensions/catalog.ts:29`) whose `sourceDir` is `pinchtab`.
|
|
578
|
+
From the CLI the real command is:
|
|
579
|
+
|
|
580
|
+
```bash
|
|
581
|
+
mercury add ~/.nvm/versions/node/<ver>/lib/node_modules/mercury-agent/examples/extensions/pinchtab
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
…which installs it under the name `pinchtab`, **duplicating** a dashboard-installed
|
|
585
|
+
`web-browser` (same CLI, skill, and permission). Prefer the dashboard's Features
|
|
586
|
+
page, or remove the duplicate.
|
|
587
|
+
|
|
588
|
+
**2. The real blocker: pinchtab's IDPI domain allowlist.** The extension pins
|
|
589
|
+
`pinchtab@0.13.2`, which ships:
|
|
590
|
+
|
|
591
|
+
```
|
|
592
|
+
website whitelist 127.0.0.1, localhost, ::1
|
|
593
|
+
IDPI strict mode enforcing
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
Every public navigation is refused — `Error 403: navigation blocked by IDPI:
|
|
597
|
+
Domain not in allowlist: search.brave.com` — including the Brave search the
|
|
598
|
+
extension's own skill instructs the agent to perform. Infrastructure was never
|
|
599
|
+
the problem: `pinchtab` CLI present, Chromium present, egress fine (`google:
|
|
600
|
+
200`).
|
|
601
|
+
|
|
602
|
+
Two ways out, and they are **not** equivalent:
|
|
603
|
+
|
|
604
|
+
| | whitelist | IDPI strict | content guard |
|
|
605
|
+
|---|---|---|---|
|
|
606
|
+
| `pinchtab security down` | disabled | **disabled** | **disabled** |
|
|
607
|
+
| `config set security.allowedDomains "*"` | wildcard | enforcing | **active** |
|
|
608
|
+
|
|
609
|
+
The content guard is what wraps fetched pages in `<untrusted_web_content>` with
|
|
610
|
+
an explicit prompt-injection warning. For an agent that browses *and* holds
|
|
611
|
+
tools, keeping it is the whole point — so the allowlist route is correct and
|
|
612
|
+
`security down` should be avoided.
|
|
613
|
+
|
|
614
|
+
**3. Mercury silently reverts edits to installed extensions.** Patching
|
|
615
|
+
`.mercury/extensions/web-browser/index.ts` looked like it worked, but the next
|
|
616
|
+
start logged `Bundled extension source updated — reinstalling name=web-browser`
|
|
617
|
+
and restored the original. The patch must go in the **bundled source** inside the
|
|
618
|
+
npm package:
|
|
619
|
+
|
|
620
|
+
```
|
|
621
|
+
~/.nvm/versions/node/<ver>/lib/node_modules/mercury-agent/examples/extensions/pinchtab/index.ts
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
### The fix that actually works: bake it at image build time
|
|
625
|
+
|
|
626
|
+
Setting the allowlist inside `pinchtab_ensure` is **not sufficient**. The agent
|
|
627
|
+
usually runs `pinchtab nav` directly, and nav *auto-starts the server*
|
|
628
|
+
(`server not running, starting automatically`) without ever calling the ensure
|
|
629
|
+
function — so the config never ran. Verified: the bot still reported
|
|
630
|
+
`403 ... Domain not in allowlist` after that patch.
|
|
631
|
+
|
|
632
|
+
The durable fix appends to the extension's CLI `install` string, so the config
|
|
633
|
+
file exists in the image before anything runs. The container runs as uid 1000
|
|
634
|
+
(`mercury`), so `HOME` must be set explicitly during the root-run build:
|
|
635
|
+
|
|
636
|
+
```
|
|
637
|
+
&& HOME=/home/mercury pinchtab config set security.allowedDomains "*"
|
|
638
|
+
&& chown -R mercury:mercury /home/mercury/.pinchtab
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
Changing `install` changes the image hash → one rebuild (~3.4 min) →
|
|
642
|
+
`mercury-agent-ext:23ab9b6a1159`. Confirmed baked in, with guards intact:
|
|
643
|
+
|
|
644
|
+
```
|
|
645
|
+
allowedDomains: ['*']
|
|
646
|
+
idpi: {enabled: True, strictMode: True, scanContent: True, wrapContent: True}
|
|
647
|
+
allowEvaluate: False, allowDownload: False, allowClipboard: False
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
### Trap: the bash lives inside a TS template literal
|
|
651
|
+
|
|
652
|
+
The first patch attempt put backticks in an explanatory comment
|
|
653
|
+
(`` # ...use `pinchtab security down`...``). Those backticks **closed the
|
|
654
|
+
enclosing template literal**, and the extension stopped loading entirely:
|
|
655
|
+
|
|
656
|
+
```
|
|
657
|
+
Failed to load extension "web-browser": BuildMessage: Expected ";" but found "pinchtab"
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
The symptom surfaced in the dashboard as *web-browser: uninstalled* — the
|
|
661
|
+
Features page lists `registry.list()`, so a load failure reads as "not
|
|
662
|
+
installed". **Never put a backtick in that bash block.** Check any edit with
|
|
663
|
+
`mercury extensions validate web-browser` before restarting.
|
|
664
|
+
|
|
665
|
+
> ⚠️ **`npm update -g mercury-agent` erases both patches**, and browsing stops
|
|
666
|
+
> working again. Re-apply after every upgrade until fixed upstream.
|
|
667
|
+
|
|
668
|
+
### Verifying honestly — plausible answers are not evidence
|
|
669
|
+
|
|
670
|
+
Two false positives happened here, both worth remembering:
|
|
671
|
+
|
|
672
|
+
1. *"What's the weather in Tel Aviv?"* → a confident **30 °C**, produced while
|
|
673
|
+
navigation was fully blocked. Pure training data.
|
|
674
|
+
2. A live GitHub API check (`stargazers_count`, `pushed_at`) matched ground truth
|
|
675
|
+
**exactly** — but was run while the extension was failing to load. The agent
|
|
676
|
+
had simply used `curl`; container egress was never restricted, only pinchtab
|
|
677
|
+
navigation was.
|
|
678
|
+
|
|
679
|
+
The test that actually proves the browser ran must be something `curl` **cannot**
|
|
680
|
+
do. Brave Search returns `429` to plain curl from the container, so real Brave
|
|
681
|
+
result titles are proof:
|
|
682
|
+
|
|
683
|
+
| | |
|
|
684
|
+
|---|---|
|
|
685
|
+
| `curl https://search.brave.com/...` from container | **429** |
|
|
686
|
+
| Bot, asked for the first two result titles | "Jerusalem, Israel 14 day weather forecast" (TimeAndDate), "Jerusalem — BBC Weather" |
|
|
687
|
+
|
|
688
|
+
---
|
|
689
|
+
|
|
690
|
+
## Voice transcription
|
|
691
|
+
|
|
692
|
+
Configured to reuse the existing Gemini key, so no new provider or cost:
|
|
693
|
+
|
|
694
|
+
```yaml
|
|
695
|
+
extensions:
|
|
696
|
+
voice-transcribe:
|
|
697
|
+
provider: gemini # local | api | openai | gemini
|
|
698
|
+
model: gemini-3.1-flash-lite
|
|
699
|
+
language: he # empty = auto-detect
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
`.env` gains `MERCURY_STT_GEMINI_API_KEY` (same value as the chat key). The
|
|
703
|
+
extension declares this var `hostOnly: true`, so it is consumed on the host in
|
|
704
|
+
`before_container` and **never enters the agent container**.
|
|
705
|
+
|
|
706
|
+
> **The model must be set explicitly.** The extension's built-in Gemini default
|
|
707
|
+
> is `DEFAULT_GEMINI_MODEL = "gemini-2.5-flash"` — the model that now 404s for
|
|
708
|
+
> new keys. Left alone, transcription fails silently.
|
|
709
|
+
|
|
710
|
+
Audio-input support verified by posting a real WAV to `:generateContent`:
|
|
711
|
+
|
|
712
|
+
| Model | Result |
|
|
713
|
+
|---|---|
|
|
714
|
+
| `gemini-3.1-flash-lite` | ✅ accepted (**chosen** — best measured availability) |
|
|
715
|
+
| `gemini-3.5-flash` | ✅ accepted (better accuracy if it's reachable) |
|
|
716
|
+
| `gemini-3.6-flash` | ✅ accepted |
|
|
717
|
+
| `gemini-2.5-flash` | ❌ 404 retired |
|
|
718
|
+
|
|
719
|
+
Other providers: `openai` (`MERCURY_STT_API_KEY`, also serves Groq via
|
|
720
|
+
`base_url`), `api` (Hugging Face, `MERCURY_HF_TOKEN`), `local` (Python +
|
|
721
|
+
transformers on the host, `MERCURY_VOICE_PYTHON`).
|
|
722
|
+
|
|
723
|
+
---
|
|
724
|
+
|
|
725
|
+
## Configuration reference
|
|
726
|
+
|
|
727
|
+
### Triggers — how the bot decides to answer
|
|
728
|
+
|
|
729
|
+
`trigger.match` takes three values (`src/core/trigger.ts:9`):
|
|
730
|
+
|
|
731
|
+
| Mode | Behaviour |
|
|
732
|
+
|---|---|
|
|
733
|
+
| `prefix` | Pattern must start the message |
|
|
734
|
+
| `mention` | Pattern anywhere, word-boundary aware — `@Mercury, hi` works, `Pi` inside `Pixel` does not (**default**) |
|
|
735
|
+
| `always` | No trigger needed at all |
|
|
736
|
+
|
|
737
|
+
**DMs ignore this entirely** — `trigger.ts:65` always matches in a DM and strips
|
|
738
|
+
the pattern if present. `match` only constrains **group** chats.
|
|
739
|
+
|
|
740
|
+
`server.bot_username` does double duty: `router.ts:90` auto-appends
|
|
741
|
+
`@<bot_username>` to the trigger patterns, so renaming the bot changes what it
|
|
742
|
+
answers to in groups.
|
|
743
|
+
|
|
744
|
+
### Applied identity
|
|
745
|
+
|
|
746
|
+
**Revised 2026-08-06** — bare `ויצ` was too short and false-triggered, so the
|
|
747
|
+
mention name moved to `ויציבוט`:
|
|
748
|
+
|
|
749
|
+
```yaml
|
|
750
|
+
server:
|
|
751
|
+
bot_username: ויציבוט # was ויצ
|
|
752
|
+
trigger:
|
|
753
|
+
patterns: "ויצ דה בוט,@Mercury,Mercury" # bare ויצ removed
|
|
754
|
+
match: mention
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
`bot_username` is kept to **one word** on purpose: `router.ts:95` auto-appends
|
|
758
|
+
`@<bot_username>`, and `@ויצ דה בוט` would be awkward to type in a group. The
|
|
759
|
+
full name is listed as a pattern instead, and patterns are sorted longest-first
|
|
760
|
+
at match time so the full name is stripped before the short one.
|
|
761
|
+
|
|
762
|
+
`bot_username` is *not* cosmetic. It is what the WhatsApp adapter rewrites a real
|
|
763
|
+
@-mention of the bot's number into (`adapters/setup.ts:69` → `whatsapp.ts:592`)
|
|
764
|
+
before trigger matching runs. Change it and the tap-to-mention path changes with
|
|
765
|
+
it.
|
|
766
|
+
|
|
767
|
+
The Hebrew word for "bot" was deliberately **left out** of the *global* list — it
|
|
768
|
+
is a common word and would false-trigger in groups. `Mercury` is kept as a
|
|
769
|
+
fallback.
|
|
770
|
+
|
|
771
|
+
> **Trap — a per-space `trigger.patterns` override replaces the auto-appended
|
|
772
|
+
> `@<bot_username>`, it does not merge with it.** `trigger.ts:29-34` uses the
|
|
773
|
+
> space's list *instead of* the defaults, and the `@bot` form is only ever pushed
|
|
774
|
+
> onto the defaults (`router.ts:95-98`). Worse, `mention` matching anchors on
|
|
775
|
+
> start-of-string or whitespace (`trigger.ts:110`), so a bare `ויציבוט` pattern
|
|
776
|
+
> does **not** match the text `@ויציבוט` — the `@` blocks the lookbehind.
|
|
777
|
+
>
|
|
778
|
+
> Net effect: a space with custom patterns silently stops responding to
|
|
779
|
+
> tap-to-mention unless the `@` form is listed explicitly. Every per-space
|
|
780
|
+
> pattern list must include both forms:
|
|
781
|
+
>
|
|
782
|
+
> ```
|
|
783
|
+
> @ויציבוט,ויציבוט,ויצ דה בוט,ווצבוט,ווצ דה בוט
|
|
784
|
+
> ```
|
|
785
|
+
|
|
786
|
+
Persona lives in `.mercury/spaces/main/AGENTS.md` (was empty until now). It sets
|
|
787
|
+
Hebrew-first replies, short mobile-friendly answers, and — prompted by the two
|
|
788
|
+
false positives above — an explicit rule never to claim a capability it has not
|
|
789
|
+
just used, plus a reminder that `<untrusted_web_content>` is data, not
|
|
790
|
+
instructions.
|
|
791
|
+
|
|
792
|
+
### Per-space overrides
|
|
793
|
+
|
|
794
|
+
These live in the DB (dashboard, or space config), not `mercury.yaml`, and beat
|
|
795
|
+
the global values for that space: `trigger.match`, `trigger.patterns`,
|
|
796
|
+
`trigger.case_sensitive`, `trigger.media_in_groups`.
|
|
797
|
+
|
|
798
|
+
### Main `mercury.yaml` sections
|
|
799
|
+
|
|
800
|
+
`server` (port, bot_username) · `model` · `ingress` · `runtime` (log level, rate
|
|
801
|
+
limits, concurrency) · `trigger` · `context` (mode, window_size,
|
|
802
|
+
reply_chain_depth) · `compaction` · `agent` (image, timeout, bwrap compat,
|
|
803
|
+
env_passthrough) · `media` · `permissions.admins` · `dm_auto_space` ·
|
|
804
|
+
`scheduling.default_timezone` · `extensions`
|
|
805
|
+
|
|
806
|
+
Personality/instructions are **not** YAML — they go in
|
|
807
|
+
`.mercury/spaces/<space>/AGENTS.md`.
|
|
808
|
+
|
|
809
|
+
### Groups: adding the number is not enough
|
|
810
|
+
|
|
811
|
+
A new group creates an **unlinked** conversation, and unlinked messages are
|
|
812
|
+
dropped at `handler.ts:191` before any model call. Every new group needs:
|
|
813
|
+
|
|
814
|
+
```bash
|
|
815
|
+
mercury conversations --unlinked
|
|
816
|
+
mercury link <id> <space>
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
Then, in groups only, a message must match the trigger. Link a group to `main`
|
|
820
|
+
to share memory with the DM, or to its own space to keep memory separate.
|
|
821
|
+
|
|
822
|
+
`dm_auto_space.enabled: true` automates this for **DMs** only — never groups.
|
|
823
|
+
|
|
824
|
+
---
|
|
825
|
+
|
|
826
|
+
## Free-tier rate limits — the real constraint
|
|
827
|
+
|
|
828
|
+
The bot replied *"I've reached my usage limit for now"* after a handful of
|
|
829
|
+
messages. The underlying error:
|
|
830
|
+
|
|
831
|
+
```
|
|
832
|
+
429 Quota exceeded for metric: generate_content_free_tier_requests,
|
|
833
|
+
limit: 5, model: gemini-3.6-flash
|
|
834
|
+
Please retry in 10.817604931s
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
**That is 5 requests per _minute_, not a daily cap** — it resets in seconds. The
|
|
838
|
+
newest models carry the smallest free allowances and the most congestion
|
|
839
|
+
(`gemini-3.6-flash` also returned `503 UNAVAILABLE: high demand` on a direct
|
|
840
|
+
probe minutes later). Newest ≠ best on a free key.
|
|
841
|
+
|
|
842
|
+
Revised to a two-leg chain, which is exactly what `model.chain` exists for —
|
|
843
|
+
a throttled leg falls through instead of failing the turn:
|
|
844
|
+
|
|
845
|
+
```yaml
|
|
846
|
+
model:
|
|
847
|
+
provider: google
|
|
848
|
+
model: gemini-3.5-flash
|
|
849
|
+
chain:
|
|
850
|
+
- provider: google
|
|
851
|
+
model: gemini-3.5-flash
|
|
852
|
+
- provider: google
|
|
853
|
+
model: gemini-3.1-flash-lite
|
|
854
|
+
max_retries_per_leg: 3
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
`-lite` variants generally carry the most generous free quotas, which makes leg 2
|
|
858
|
+
a genuine safety net rather than a second way to hit the same wall.
|
|
859
|
+
|
|
860
|
+
### Measured, not assumed: pick legs by availability
|
|
861
|
+
|
|
862
|
+
Version number is a terrible proxy for usability on a free key. Measured with a
|
|
863
|
+
12-request burst plus a two-function tool-calling probe:
|
|
864
|
+
|
|
865
|
+
| Model | 12 rapid requests | Parallel tool calls |
|
|
866
|
+
|---|---|---|
|
|
867
|
+
| `gemini-3.1-flash-lite` | **12 × 200, 0 × 429** | ✅ both, correct args |
|
|
868
|
+
| `gemini-3.5-flash` | **0 × 200** (503/429) | ✅ both, correct args |
|
|
869
|
+
| `gemini-3.6-flash` | 5 rpm cap, also 503 | — |
|
|
870
|
+
| `gemini-2.5-flash-lite` | 404 — retired | — |
|
|
871
|
+
|
|
872
|
+
The `-lite` model is far and away the most *available*, and it drives tools
|
|
873
|
+
correctly — which is the only capability Mercury's skills actually need. Final
|
|
874
|
+
order is availability-first:
|
|
875
|
+
|
|
876
|
+
```yaml
|
|
877
|
+
model:
|
|
878
|
+
provider: google
|
|
879
|
+
model: gemini-3.1-flash-lite
|
|
880
|
+
chain:
|
|
881
|
+
- provider: google
|
|
882
|
+
model: gemini-3.1-flash-lite # measured 12/12
|
|
883
|
+
- provider: google
|
|
884
|
+
model: gemini-3.5-flash # better reasoning when it's reachable
|
|
885
|
+
max_retries_per_leg: 3
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
Reproduce any time with a burst of `:generateContent` calls — the numbers move as
|
|
889
|
+
Google shifts free-tier allocations, and today's best leg may not be next
|
|
890
|
+
month's.
|
|
891
|
+
|
|
892
|
+
### Do skills need anything beyond tool calling?
|
|
893
|
+
|
|
894
|
+
No. `model-capabilities.ts:201` treats an unrecognised model as **permissive** —
|
|
895
|
+
`DEFAULT_CAPABILITIES` is `tools: true`, and an unknown model is never denied a
|
|
896
|
+
capability-gated skill on a guess. So any model that emits correct function calls
|
|
897
|
+
runs Mercury's skills (recall, tasks, spaces, permissions, …).
|
|
898
|
+
|
|
899
|
+
### Other free providers Mercury supports
|
|
900
|
+
|
|
901
|
+
| Option | Free? | Notes |
|
|
902
|
+
|---|---|---|
|
|
903
|
+
| **Google Gemini** | Yes | In use. Best free tool-caller measured so far. |
|
|
904
|
+
| **Groq** (`MERCURY_GROQ_API_KEY`) | Yes | Mercury-supported provider; hosts open models. Needs a key from console.groq.com to benchmark. |
|
|
905
|
+
| **GitHub Copilot** (`mercury auth login github-copilot`) | Partly | One of only **two** OAuth providers Mercury can actually serve (`pi-auth.ts:24`). Reaches GPT/Claude-class models — the strongest tool users. Free for verified students/teachers/OSS maintainers; otherwise a limited free tier then paid. |
|
|
906
|
+
| **OpenAI** | No | Key added but the account has **no credits** — every model 429s. Inert until topped up. |
|
|
907
|
+
|
|
908
|
+
If throttling still bites: add more legs, raise `max_retries_per_leg`, or put a
|
|
909
|
+
paid leg last so only overflow costs money.
|
|
910
|
+
|
|
911
|
+
---
|
|
912
|
+
|
|
913
|
+
## Switched to Claude Opus 5 (2026-08-06)
|
|
914
|
+
|
|
915
|
+
OAuth against the Claude subscription now works (`mercury auth login anthropic`),
|
|
916
|
+
so decision C flips back to OAuth and D to `claude-opus-5`.
|
|
917
|
+
|
|
918
|
+
### The bot was "not working well" for a config reason, not a model reason
|
|
919
|
+
|
|
920
|
+
`mercury auth status` reported **`Anthropic: not logged in`** — but only when run
|
|
921
|
+
from outside the project. `auth.json` lives at `.mercury/global/auth.json`,
|
|
922
|
+
resolved relative to the **data dir**, so the command is cwd-sensitive. From
|
|
923
|
+
`~/whatsapp-bot` it correctly reports `✓ logged in`. Always `cd` first before
|
|
924
|
+
believing it.
|
|
925
|
+
|
|
926
|
+
The real fault was in `.env`, which had grown three lines that silently beat
|
|
927
|
+
`mercury.yaml` (env wins — `config.ts:357`, and the file's own header says so):
|
|
928
|
+
|
|
929
|
+
| Line | Effect |
|
|
930
|
+
|---|---|
|
|
931
|
+
| `MERCURY_MODEL_PROVIDER=google` | pinned the provider regardless of YAML |
|
|
932
|
+
| `MERCURY_MODEL_CHAIN=[{"provider":"google","model":"gemma-4-31b-it"}]` | **replaced the tuned two-leg chain with a single Gemma leg — no fallback** |
|
|
933
|
+
| `MERCURY_GEMINI_API_KEY=KEY` (2nd copy) | a 3-char placeholder duplicate |
|
|
934
|
+
|
|
935
|
+
The Gemma line is what would have degraded answers — an open-weights model with
|
|
936
|
+
no fallback leg, in place of the availability-ordered Gemini chain. It had not
|
|
937
|
+
actually bitten yet: `.env` was edited at 13:05, after the last probe run at
|
|
938
|
+
12:49, and the service had not been restarted since, so the running process was
|
|
939
|
+
still on `gemini-3.1-flash-lite`. `token_usage` confirms this.
|
|
940
|
+
|
|
941
|
+
The duplicate key was **inert** — bun's dotenv keeps the *first* occurrence, so
|
|
942
|
+
the real 53-char key still won (verified by reading back
|
|
943
|
+
`process.env.MERCURY_GEMINI_API_KEY.length`). Deleted anyway.
|
|
944
|
+
|
|
945
|
+
All three are now commented out / removed, making `mercury.yaml` the single
|
|
946
|
+
source of truth for the model.
|
|
947
|
+
|
|
948
|
+
### Config applied
|
|
949
|
+
|
|
950
|
+
```yaml
|
|
951
|
+
model:
|
|
952
|
+
provider: anthropic # container-runner.ts:892 reads this scalar
|
|
953
|
+
model: claude-opus-5
|
|
954
|
+
chain:
|
|
955
|
+
- provider: anthropic
|
|
956
|
+
model: claude-opus-5 # single leg on purpose — see caveat below
|
|
957
|
+
max_retries_per_leg: 3
|
|
958
|
+
|
|
959
|
+
agent:
|
|
960
|
+
image: mercury-agent:latest
|
|
961
|
+
container_bwrap_docker_compat: true
|
|
962
|
+
container_timeout_ms: 1200000 # 20 min; schema max is 3_600_000
|
|
963
|
+
override_pi_system_prompt: true
|
|
964
|
+
env_passthrough: all # kept — was not in the requested block
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
No Anthropic key is needed in `.env`: with `provider: anthropic` and no
|
|
968
|
+
`ANTHROPIC_*` var, `container-runner.ts:892-903` falls back to
|
|
969
|
+
`.mercury/global/auth.json` and injects `ANTHROPIC_OAUTH_TOKEN` into the
|
|
970
|
+
container.
|
|
971
|
+
|
|
972
|
+
**Single leg is deliberate.** A google fallback leg would answer whenever Opus
|
|
973
|
+
failed, making a dead leg look like a pass. Add one back for resilience once the
|
|
974
|
+
Opus leg has proven itself in daily use — but then always confirm *which* model
|
|
975
|
+
answered:
|
|
976
|
+
|
|
977
|
+
```sql
|
|
978
|
+
SELECT id, space_id, model, provider FROM token_usage ORDER BY id DESC LIMIT 1;
|
|
979
|
+
```
|
|
980
|
+
|
|
981
|
+
### `claude-opus-5` is not in pi's capability registry
|
|
982
|
+
|
|
983
|
+
Startup warned it was falling back to `DEFAULT_CAPABILITIES`
|
|
984
|
+
(`tools=true, vision=false`). Nothing was silently disabled — capability *gating*
|
|
985
|
+
treats an unknown model as permissive (`model-capabilities.ts:201`:
|
|
986
|
+
`source === "default"` satisfies any requirement) — but with `media.enabled: true`
|
|
987
|
+
the guess is wrong. Declared explicitly in `.mercury/model-capabilities.yaml`;
|
|
988
|
+
the warning is gone.
|
|
989
|
+
|
|
990
|
+
### Opus 5 vs the google legs
|
|
991
|
+
|
|
992
|
+
Probes re-run from the previous session, plus a new one. Every row is attributed
|
|
993
|
+
via `token_usage`, not assumed.
|
|
994
|
+
|
|
995
|
+
| Probe | `gemini-3.1-flash-lite` | `claude-opus-5` |
|
|
996
|
+
|---|---|---|
|
|
997
|
+
| Smoke (`PROBE_OK`) | ✅ | ✅ |
|
|
998
|
+
| Haifa weather + real titles | ✅ number; **claimed titles while saying it could not open the browser** (2 of 4 answers) | ✅ number + titles, and flagged the earlier fabrication unprompted |
|
|
999
|
+
| Copper /tonne + real titles | ✅ ~14,000–14,200 | ✅ 14,100–14,200, cross-checked Kitco / LME / COMEX |
|
|
1000
|
+
| Silver /oz, **fresh space** | ✅ $62.15–62.98, both titles real | ✅ $61.80, both titles real, *and* stated the top-2 results carried no live number so it opened Trading Economics directly |
|
|
1001
|
+
| Latency | ~35 s | ~45–71 s |
|
|
1002
|
+
|
|
1003
|
+
Independently verified with an outside search: copper really was ~$14,195/t and
|
|
1004
|
+
silver ~$62.8/oz on 2026-08-05. **Both models had the numbers right.** An earlier
|
|
1005
|
+
reading of the ~$14k copper figure as a hallucination was simply wrong — copper
|
|
1006
|
+
had rallied ~53% year-on-year.
|
|
1007
|
+
|
|
1008
|
+
So the difference is not accuracy, it is **honesty about tool use**. Gemini's
|
|
1009
|
+
failure mode in the `main` space was asserting search-result titles in the same
|
|
1010
|
+
breath as admitting the browser never opened. Opus reported provenance
|
|
1011
|
+
accurately, including the inconvenient parts.
|
|
1012
|
+
|
|
1013
|
+
Two caveats, stated so the table is not over-read:
|
|
1014
|
+
|
|
1015
|
+
1. **The `main`-space probes are contaminated.** All models share one history, so
|
|
1016
|
+
Opus could read Gemini's earlier titles before answering. That is why the
|
|
1017
|
+
silver probe was run in fresh spaces (`cmp-opus`, `cmp-gemini`) with a
|
|
1018
|
+
question never asked before — and there Gemini browsed cleanly and returned
|
|
1019
|
+
the same real titles. Its earlier fabrication is a *sometimes*, not an always.
|
|
1020
|
+
2. **Cost figures for the Opus rows are junk.** `token_usage` recorded
|
|
1021
|
+
`input_tokens` of 2, 10, 16 for full agentic turns, so the derived `cost`
|
|
1022
|
+
column is meaningless on the OAuth path. Billing is subscription plan limits,
|
|
1023
|
+
not per-token, so ignore that column for anthropic legs.
|
|
1024
|
+
|
|
1025
|
+
### Reproducing
|
|
1026
|
+
|
|
1027
|
+
```bash
|
|
1028
|
+
cd ~/whatsapp-bot # cwd matters for auth status
|
|
1029
|
+
mercury spaces create cmp-<model> # fresh space = no shared history
|
|
1030
|
+
mercury chat --space cmp-<model> "…tell me the exact titles of the first two \
|
|
1031
|
+
search results you actually saw. If you could not open the browser, say so \
|
|
1032
|
+
explicitly and do not guess titles."
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
The "if you could not open the browser, say so" clause is what makes the probe
|
|
1036
|
+
diagnostic — without it a fabricated title is indistinguishable from a real one.
|
|
1037
|
+
|
|
1038
|
+
---
|
|
1039
|
+
|
|
1040
|
+
## Keeping it running
|
|
1041
|
+
|
|
1042
|
+
### Trap: without lingering, closing your last terminal kills the bot
|
|
1043
|
+
|
|
1044
|
+
`mercury service install` creates a systemd **user** unit. By default a user's
|
|
1045
|
+
systemd manager is torn down when their last login session ends — so closing the
|
|
1046
|
+
Ubuntu terminal stopped Mercury with a clean `status=0/SUCCESS` graceful
|
|
1047
|
+
shutdown. Not a crash, and `Restart=on-failure` deliberately does **not** cover
|
|
1048
|
+
it, because exit 0 is not a failure.
|
|
1049
|
+
|
|
1050
|
+
Diagnosed with `loginctl show-user $USER --property=Linger` → `Linger=no`. Fixed:
|
|
1051
|
+
|
|
1052
|
+
```bash
|
|
1053
|
+
loginctl enable-linger "$USER" # no sudo needed
|
|
1054
|
+
```
|
|
1055
|
+
|
|
1056
|
+
Now the user manager persists with no session open, and the bot survives closing
|
|
1057
|
+
every terminal.
|
|
1058
|
+
|
|
1059
|
+
### Still open: WSL itself does not auto-start
|
|
1060
|
+
|
|
1061
|
+
Lingering covers terminal closes, but not `wsl --shutdown` or a Windows reboot —
|
|
1062
|
+
WSL2 does not start a distro at boot. Until that's addressed, the bot is down
|
|
1063
|
+
after every restart until something touches the distro.
|
|
1064
|
+
|
|
1065
|
+
Fix when wanted: a Windows Task Scheduler task at logon running
|
|
1066
|
+
|
|
1067
|
+
```
|
|
1068
|
+
wsl.exe -d Ubuntu -u guysaha -e /bin/true
|
|
1069
|
+
```
|
|
1070
|
+
|
|
1071
|
+
Booting the distro starts the user manager, which (with lingering on, and the
|
|
1072
|
+
unit already `enabled`) starts Mercury automatically.
|
|
1073
|
+
|
|
1074
|
+
---
|
|
1075
|
+
|
|
1076
|
+
## Ongoing operations
|
|
1077
|
+
|
|
1078
|
+
- **Re-auth WhatsApp — on failure, not on a schedule.** There is no fixed expiry
|
|
1079
|
+
(corrected 2026-08-06). The link drops only if the bot's phone goes **14 days
|
|
1080
|
+
without connecting to WhatsApp**, or on an unrelated Baileys/server-side
|
|
1081
|
+
logout. Keep the phone online and no routine action is needed; no calendar
|
|
1082
|
+
reminder required. When it *does* drop: `mercury service uninstall` →
|
|
1083
|
+
`rm -rf .mercury/whatsapp-auth/` → `mercury auth whatsapp` →
|
|
1084
|
+
`mercury service install`.
|
|
1085
|
+
- **Upgrades:** stop the service → `npm update -g mercury-agent` →
|
|
1086
|
+
`mercury build` → start. The derived image rebuilds itself.
|
|
1087
|
+
- **Backups:** `.mercury/` holds the SQLite DB, space workspaces, and WhatsApp
|
|
1088
|
+
credentials. Back it up; never commit it.
|
|
1089
|
+
- **Secrets:** `.env` and `.mercury/whatsapp-auth/` are credentials. If you make
|
|
1090
|
+
the runtime project a git repo, confirm both are gitignored before the first
|
|
1091
|
+
commit.
|
|
1092
|
+
|
|
1093
|
+
---
|
|
1094
|
+
|
|
1095
|
+
## Risk register
|
|
1096
|
+
|
|
1097
|
+
| Risk | Impact | Mitigation |
|
|
1098
|
+
|---|---|---|
|
|
1099
|
+
| WSL2 + Docker install fails or is blocked by policy | Total blocker | Try Docker Desktop first; it handles the WSL plumbing |
|
|
1100
|
+
| Personal WhatsApp number linked to the bot | Bot has access to all your chats; ban risk | Dedicated number |
|
|
1101
|
+
| WhatsApp bans/limits automated accounts | Loss of the number | Baileys is an unofficial client — this is inherent. Low volume, personal use only |
|
|
1102
|
+
| Session expiry surprises you | Bot silently stops | Calendar reminder + `mercury service logs -f` |
|
|
1103
|
+
| `claude-sonnet-5` unrecognised by pi, or not granted by the Claude subscription | Startup/runtime error | Fall back to `claude-opus-4-8`; verify with `mercury chat "hello"` before WhatsApp |
|
|
1104
|
+
| GHCR agent image is private (`unauthorized`) | Can't pull the preset | Build locally — but `.dockerignore` excludes `examples/` while `Dockerfile:118` copies it, so the build needs a one-line re-include |
|
|
1105
|
+
| npm 11 skips postinstall scripts | Baileys/protobufjs half-installed, WhatsApp breaks subtly | Always install with `--allow-scripts=@whiskeysockets/baileys,@google/genai,protobufjs` |
|
|
1106
|
+
| bubblewrap can't nest under Docker Engine | Agent container won't start | **Confirmed on this host and already fixed** — `agent.container_bwrap_docker_compat: true` |
|
|
1107
|
+
| Docker image churn | Disk usage | Base image ~2.8 GB + derived images; set `MERCURY_AGENT_ID` |
|
|
1108
|
+
| Running from `/mnt/d` | Slow, permission bugs | Decision A — use WSL ext4 |
|
|
1109
|
+
|
|
1110
|
+
---
|
|
1111
|
+
|
|
1112
|
+
## Reference
|
|
1113
|
+
|
|
1114
|
+
Docs in the clone at `mercury/docs/`:
|
|
1115
|
+
`setup-whatsapp.md`, `configuration.md`, `pipeline.md`, `memory.md`,
|
|
1116
|
+
`scheduler.md`, `permissions.md`, `media/overview.md`, `dashboard.md`,
|
|
1117
|
+
`extensions.md`, `container-lifecycle.md`, `rate-limiting.md`.
|