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,396 @@
|
|
|
1
|
+
# Open threads — Mercury WhatsApp Bot
|
|
2
|
+
|
|
3
|
+
**Goal**: [whatsapp-bot-hardening](goal.md)
|
|
4
|
+
**Opened**: 2026-08-10
|
|
5
|
+
|
|
6
|
+
Everything still outstanding, in one place. The eight hardening stories are
|
|
7
|
+
shipped (see [roadmap](roadmap.md)); this is what did *not* fit inside them,
|
|
8
|
+
plus what the prompt-layer investigation turned up.
|
|
9
|
+
|
|
10
|
+
Each item says what it is, why it matters, and what "done" looks like — so any
|
|
11
|
+
of them can be picked up cold.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## A. Prompt layers & voice
|
|
16
|
+
|
|
17
|
+
### A-1. There are **five** prompt layers, not three
|
|
18
|
+
|
|
19
|
+
This was the root of the confusion about `system_prompt` vs preferences. Full
|
|
20
|
+
map, verified against the running code:
|
|
21
|
+
|
|
22
|
+
| # | Layer | Where it lands | Scope | Setter |
|
|
23
|
+
|---|-------|----------------|-------|--------|
|
|
24
|
+
| 1 | `.mercury/global/AGENTS.md` | pi auto-loads from the resource dir (`storage/memory.ts:16`) | global | edit the file |
|
|
25
|
+
| 2 | `project_config.character` | system prompt, as `## Bot Character` via `MERCURY_EXT_SYSTEM_PROMPT` (`runtime.ts:1601`) | global | dashboard, `mrctl character set` |
|
|
26
|
+
| 3 | `space_config.system_prompt` | system prompt (`runtime.ts:1614`) | per-space | **none — see A-2** |
|
|
27
|
+
| 4 | `.mercury/spaces/<id>/AGENTS.md` | pi auto-loads from the space workspace | per-space | edit the file |
|
|
28
|
+
| 5 | `space_preferences` | **user** prompt, as `<preferences>` XML (`preferences-prompt.ts`, `container-entry.ts:783`) | per-space | dashboard, `mrctl prefs set` |
|
|
29
|
+
|
|
30
|
+
Current occupancy: layer 1 is the stock template. Layer 3 is empty everywhere.
|
|
31
|
+
Layer 4 is written for `main` and `admin`, **empty for `19` and `greece2026`** —
|
|
32
|
+
which is why those two spaces run almost entirely on layers 2 and 5.
|
|
33
|
+
|
|
34
|
+
### A-2. `space_config.system_prompt` has no setter
|
|
35
|
+
|
|
36
|
+
Its only writer is `conversation.ts:177` (`seedSpaceConfigIfAbsent`, from
|
|
37
|
+
`autoSpace.defaultSystemPrompt`). Nothing in chat, `mrctl`, or the dashboard
|
|
38
|
+
writes it. So it is a real prompt layer that no user can reach.
|
|
39
|
+
|
|
40
|
+
**Decide:** add a setter (`mrctl config set system_prompt`), or drop the layer.
|
|
41
|
+
Leaning: drop it. Layer 4 (`AGENTS.md`) already covers per-space long-form
|
|
42
|
+
instructions and is editable, so a setter would add a fifth way to say the same
|
|
43
|
+
thing. Either way, stop treating it as a live option.
|
|
44
|
+
|
|
45
|
+
### A-3. Layer precedence is **not** deterministic — do not design around it
|
|
46
|
+
|
|
47
|
+
Measured on the throwaway `prompttest` space over `POST /chat`, using target
|
|
48
|
+
language as the probe (each layer told to answer in a different language):
|
|
49
|
+
|
|
50
|
+
- A direct `system_prompt` vs `preferences` conflict went 8/10 to
|
|
51
|
+
`system_prompt` — but **not 10/10**, and a swapped-language control produced
|
|
52
|
+
the opposite result. Repeating one fixed config gave different winners on
|
|
53
|
+
different runs.
|
|
54
|
+
- The global character contains its own conflict ("answer in the language they
|
|
55
|
+
wrote in" **and** "default Hebrew"); an English probe resolves it
|
|
56
|
+
inconsistently.
|
|
57
|
+
|
|
58
|
+
**Conclusion:** you cannot rely on a lower layer overriding a higher one. The
|
|
59
|
+
only reliable fix is to **not state the same rule in two layers**. Recorded as
|
|
60
|
+
D-014.
|
|
61
|
+
|
|
62
|
+
### A-4. Vague quantities produce vague behaviour — the emoji data
|
|
63
|
+
|
|
64
|
+
Live emoji rate in assistant messages, by space:
|
|
65
|
+
|
|
66
|
+
| Space | Instruction | Emoji rate |
|
|
67
|
+
|-------|-------------|-----------|
|
|
68
|
+
| `19` (baduk) | pref: "בלי אימוג'ים בכלל" (none at all) | **0 / 40 = 0%** |
|
|
69
|
+
| `greece2026` | pref: "minimal emojis" | 33% |
|
|
70
|
+
| `main` | character only: "אימוג'י פה ושם כשזה מתאים" (here and there when it fits) | 73% |
|
|
71
|
+
|
|
72
|
+
Preferences demonstrably work — `19` is at a hard zero. What fails is the
|
|
73
|
+
*wording*: "minimal" and "here and there" are not countable, so the model picks
|
|
74
|
+
its own number. A countable rule is obeyed; an adjective is not.
|
|
75
|
+
|
|
76
|
+
### A-5. Emoji rule and layer cleanup — **applied 2026-08-10**
|
|
77
|
+
|
|
78
|
+
Owner's decision: *at most one emoji per ~5 lines, only if it carries meaning,
|
|
79
|
+
default 0, in every space* — no exception for `19` even though its absolute ban
|
|
80
|
+
had it at a clean 0/40. Applied together with the layer cleanup below; DB backup
|
|
81
|
+
at `~/mercury-backup-prompts-20260810/`.
|
|
82
|
+
|
|
83
|
+
| Layer | Change |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `character` | Rewritten **in English**. Five clauses: persona, language, length, emoji, honesty. Countable emoji rule; anti-fabrication **and** stale-facts lines promoted here (both existed only in `main`/`admin` AGENTS.md, i.e. in neither real group); **"default Hebrew" removed** — matching the message is the only global language rule. |
|
|
86
|
+
| `19` prefs | `tone` reworded (emoji ban dropped, now global; "one of the guys" → group-neutral); `language` = **always Hebrew**, everyone in the group speaks it |
|
|
87
|
+
| `greece2026` prefs | `language.style` **deleted** — it bundled a hard constraint, a tone hint, and a vague "minimal emojis" that would have *loosened* the stricter global rule. Now `language` (English by default, because members share no other common language, **with an explicit-request escape hatch**) and `tone` (family + partners + friends, not just friends). `response.length` **deleted** — length is a global rule now. |
|
|
88
|
+
| `main/AGENTS.md` | Duplicated language, brevity, warmth, anti-fabrication and stale-facts removed; keeps only what is space-specific |
|
|
89
|
+
| `admin/AGENTS.md` | Explicit "this space overrides the global character" banner; Hebrew default removed |
|
|
90
|
+
|
|
91
|
+
Both per-space language rules now **contradict** the global "match the message"
|
|
92
|
+
rule outright, so each states the override in words. `greece2026` is two
|
|
93
|
+
overrides deep — space rule beats global, explicit user request beats space
|
|
94
|
+
rule — and that holds (P3 below).
|
|
95
|
+
|
|
96
|
+
Preferences and character are read per turn (`runtime.ts:1601`, `:1799`), so no
|
|
97
|
+
restart was needed.
|
|
98
|
+
|
|
99
|
+
**Overrides are now stated in words, not implied by position.** `greece2026`'s
|
|
100
|
+
language pref literally says it overrides the global rule, and `admin`'s
|
|
101
|
+
`AGENTS.md` says it overrides the character. Per A-3 an implicit override is a
|
|
102
|
+
coin flip; an explicit one is just an instruction.
|
|
103
|
+
|
|
104
|
+
### A-6. Verification — measured on fresh spaces, 2026-08-10
|
|
105
|
+
|
|
106
|
+
One fresh space per probe, no shared history.
|
|
107
|
+
|
|
108
|
+
| Probe | Result |
|
|
109
|
+
|---|---|
|
|
110
|
+
| Hebrew in, character only | Hebrew out **4/4** |
|
|
111
|
+
| English in, clean space | English out **3/3** (100% Latin) |
|
|
112
|
+
| Hebrew conversation → English message | switches to English |
|
|
113
|
+
| `19` prefs, **English** in | Hebrew out **3/3** — unconditional override beats the incoming message |
|
|
114
|
+
| `greece2026` prefs, Hebrew in | English out **4/4** |
|
|
115
|
+
| `greece2026` prefs + "answer me in Hebrew please" | Hebrew out **3/3** — the escape hatch beats the space rule |
|
|
116
|
+
| Long question (4-day itinerary, 8 people) | 25 and 10 lines, complete, not truncated |
|
|
117
|
+
| Short question (time zone) | **1 line** both times — the length rule did not become "always long" |
|
|
118
|
+
|
|
119
|
+
Dropping `19`'s absolute emoji ban did **not** cause a spike.
|
|
120
|
+
|
|
121
|
+
**Emoji: one leak found and mostly closed.** The first pass held on *count* but
|
|
122
|
+
leaked on *placement* — `19` opened 2 of 3 replies with `מה קורה 👋`. One emoji
|
|
123
|
+
in a 9-line reply is inside the "~1 per 5 lines" budget, so the number was never
|
|
124
|
+
the problem: "never to open or close a message" was too abstract to bind against
|
|
125
|
+
a tone pref pulling toward banter. Naming the exact case ("a wave, a smile or a
|
|
126
|
+
thumbs-up attached to a greeting or a sign-off is decoration, however casual the
|
|
127
|
+
conversation is") took it to **1 emoji over 12 lines, 1 of 4 replies** on
|
|
128
|
+
deliberately greeting-shaped prompts. Same lesson as "minimal" (33%) vs "none at
|
|
129
|
+
all" (0%): concrete beats abstract.
|
|
130
|
+
|
|
131
|
+
Residual, left as a taste call: a bare thank-you (`אחלה, תודה!`) still gets
|
|
132
|
+
`בכיף! 🙂`. One line, one emoji — over budget and carrying nothing. Closing it
|
|
133
|
+
needs one more sentence ("a message of one or two lines gets no emoji at all"),
|
|
134
|
+
at some risk of reading cold in a friends' group.
|
|
135
|
+
|
|
136
|
+
> **Method note, worth keeping.** The first run appeared to show a regression:
|
|
137
|
+
> an English prompt came back in Hebrew, 0/2. It was the *test* that was broken —
|
|
138
|
+
> the English probe ran in `prompttest` straight after four Hebrew turns in the
|
|
139
|
+
> same space, so the model was matching the conversation, not the message. Re-run
|
|
140
|
+
> in virgin spaces it passes 3/3. The detector was also too crude (it scored any
|
|
141
|
+
> reply containing a single Hebrew character as Hebrew, so English prose with a
|
|
142
|
+
> Hebrew greeting counted as a failure); it now scores by majority script.
|
|
143
|
+
> **One-space, sequential prompt probes are not trustworthy — use a fresh space
|
|
144
|
+
> per probe.**
|
|
145
|
+
|
|
146
|
+
### A-6b. Left open after the cleanup
|
|
147
|
+
|
|
148
|
+
- **The `בכיף! 🙂` case** above — one sentence closes it, at some cost to warmth.
|
|
149
|
+
Owner's call.
|
|
150
|
+
- **Re-measure the live emoji rate** after a week of real traffic. Everything
|
|
151
|
+
above is API probes, not `main` and `19` in daily use. Count
|
|
152
|
+
`\p{Extended_Pictographic}` over assistant rows since 2026-08-10 and compare
|
|
153
|
+
against the pre-change baseline (`main` 73%, `greece2026` 33%, `19` 0%).
|
|
154
|
+
- **`admin` is now the only space whose voice is set by `AGENTS.md` rather than
|
|
155
|
+
preferences.** That is deliberate — it is long-form and structural — but it
|
|
156
|
+
means the one space with a genuinely different voice is also the one with no
|
|
157
|
+
dashboard-visible setting. Worth remembering before wondering why `admin`
|
|
158
|
+
reads differently.
|
|
159
|
+
|
|
160
|
+
### A-7. The agent is never told preferences exist — **fixed 2026-08-10**
|
|
161
|
+
|
|
162
|
+
`container-entry.ts` never mentioned `<preferences>` or `mrctl prefs set`, so
|
|
163
|
+
the self-service flow the design implies was never explained to the model. It
|
|
164
|
+
also carried an actively wrong instruction at `container-entry.ts:326`:
|
|
165
|
+
per-space tone lives in `system_prompt` "set from the dashboard" — no such
|
|
166
|
+
setter exists (A-2), and tone belongs in preferences.
|
|
167
|
+
|
|
168
|
+
Both halves are now fixed in the `## Character` section: the `system_prompt`
|
|
169
|
+
sentence is replaced with the four real `mrctl prefs` verbs and when to reach
|
|
170
|
+
for them, plus a closing line carrying D-014 — do not state the same rule in
|
|
171
|
+
two layers, and make an intended override explicit in words rather than relying
|
|
172
|
+
on specificity. Now a PR item to *describe*, not to do.
|
|
173
|
+
|
|
174
|
+
**Live since 2026-08-10.** Prompt-side, so it needed the image rebuild, not just
|
|
175
|
+
a service restart: base rebuilt, derived tag moved `dd9a11d5 → 0597b302`,
|
|
176
|
+
verified by grepping the new image rather than the repo. The rebuild also
|
|
177
|
+
disproved the roadmap's claim that the derived image ignores a base change —
|
|
178
|
+
corrected there.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## B. API exposure — remaining hardening
|
|
183
|
+
|
|
184
|
+
Closed the acute part on 2026-08-10 (both secrets set; see roadmap). Left over:
|
|
185
|
+
|
|
186
|
+
- **B-1. Done 2026-08-10.** `POST /chat` now requires
|
|
187
|
+
`content-type: application/json` and returns 415 otherwise
|
|
188
|
+
(`routes/chat.ts`). The check runs **before** the API-key check, so it holds
|
|
189
|
+
in the configuration it actually protects — the one with no key set. Killing
|
|
190
|
+
the browser drive-by is the point: a `text/plain` body makes a cross-origin
|
|
191
|
+
POST a CORS "simple request" with no preflight, so any open web page could
|
|
192
|
+
start a real agent turn. Demanding JSON forces a preflight this server never
|
|
193
|
+
answers. Covered by `tests/chat-content-type.test.ts`; the only in-repo
|
|
194
|
+
caller (`mercury chat`, `cli/mercury.ts:2067`) already sent the header.
|
|
195
|
+
- **B-2.** Add a bind-host config option. `main.ts:128` calls `Bun.serve` with
|
|
196
|
+
no `hostname`, so it listens on `*:8787`. ⚠️ **Do not fix by hardcoding
|
|
197
|
+
`127.0.0.1`** — agent containers reach the host via
|
|
198
|
+
`host.docker.internal:host-gateway`, so loopback-only binding breaks every
|
|
199
|
+
turn. It needs to be configurable, defaulting to the docker-bridge address.
|
|
200
|
+
- **B-3.** Bare `GET /dashboard` is registered at `server.ts:135`, *before* the
|
|
201
|
+
`/dashboard/*` auth middleware at `server.ts:191`, and Hono's `/dashboard/*`
|
|
202
|
+
does not match the bare path. With auth on, it serves the SPA shell whose data
|
|
203
|
+
calls then 401 — an empty page with no error and no login prompt. It should
|
|
204
|
+
redirect to the login page. (Version disclosure is the lesser half.)
|
|
205
|
+
- **B-4.** `.env` is cwd-bound: hardcoded `join(CWD, ".env")`
|
|
206
|
+
(`cli/mercury.ts:112`), no override. CLI commands needing API keys must run
|
|
207
|
+
from `~/whatsapp-bot`. Not worth patching upstream on its own; note it if
|
|
208
|
+
another `.env` issue comes up.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## C. The PR
|
|
213
|
+
|
|
214
|
+
**Opened 2026-08-10.** Push access arrived; **18 commits** on
|
|
215
|
+
`fix/whatsapp-context-identity-and-docs`, rebased onto `origin/main`. Always
|
|
216
|
+
count with `git rev-list origin/main..HEAD` after a `git fetch` — a stale local
|
|
217
|
+
`main` gave a misleading 16 for most of this goal.
|
|
218
|
+
|
|
219
|
+
All ops docs live in exactly one commit (`docs(ops): add whatsapp-bot-hardening
|
|
220
|
+
goal, roadmap and story archive`) and **no code commit touches `docs/`**, so if
|
|
221
|
+
the maintainer would rather not carry planning docs upstream that single commit
|
|
222
|
+
can be dropped without disturbing anything else.
|
|
223
|
+
|
|
224
|
+
Body covers the six points in the roadmap's Completion section, **plus** these
|
|
225
|
+
accumulated since: B-2, B-3, and the ambient prompt-injection fix below. B-1 and
|
|
226
|
+
A-7 are **done** (2026-08-10), so they are changes the PR describes rather than
|
|
227
|
+
work it still owes.
|
|
228
|
+
|
|
229
|
+
**New behaviour users will feel** (add to the roadmap's list of six): tagging
|
|
230
|
+
the bot now always triggers it, whatever `trigger.patterns` says — see §G. New
|
|
231
|
+
space config key `trigger.mention_always` to turn that off.
|
|
232
|
+
|
|
233
|
+
**Also worth its own section:** ambient message content used to be interpolated
|
|
234
|
+
into the prompt raw (`container-entry.ts`, `${m.content}`); it is now run
|
|
235
|
+
through `escapeXmlText` like every other block, and reply-context attributes go
|
|
236
|
+
through `escapeAttr`. Any group member could previously write XML straight into
|
|
237
|
+
the prompt. That is a security fix and is not currently listed anywhere in the
|
|
238
|
+
PR plan.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## D. Operations
|
|
243
|
+
|
|
244
|
+
- **D-1. WSL does not auto-start.** Carried over from the setup plan. Lingering
|
|
245
|
+
(`loginctl enable-linger`) survives closing a terminal, but not
|
|
246
|
+
`wsl --shutdown` or a Windows reboot — the bot is down until something touches
|
|
247
|
+
the distro. Fix: a Task Scheduler task at logon running
|
|
248
|
+
`wsl.exe -d Ubuntu -u guysaha -e /bin/true`.
|
|
249
|
+
- **D-2. Tighten `allowedDomains` from `"*"`** (D-011). The injection guard is
|
|
250
|
+
verified still on, so this is hardening, not a live hole. Deferred out of the
|
|
251
|
+
PR by decision; revisit after it merges.
|
|
252
|
+
- **D-3. `prompttest` deleted 2026-08-10** (64 messages, 4 roles, 32 token-usage
|
|
253
|
+
rows — all from prompt experiments). Live spaces are now exactly `19`, `admin`,
|
|
254
|
+
`greece2026`, `main`.
|
|
255
|
+
- **D-4. Orphaned workspace directories — cleaned 2026-08-10.** The prompt tests
|
|
256
|
+
left 23 dirs in `.mercury/spaces/` (`prompttest`, `langtest-*`, `v2-*`,
|
|
257
|
+
`v3-*`); removed by hand, leaving exactly `19`, `admin`, `greece2026`, `main`.
|
|
258
|
+
**Not an upstream bug** — `db.deleteSpace()` only clears rows, and every real
|
|
259
|
+
delete path pairs it with `removeSpaceWorkspace` (`routes/spaces.ts:87`,
|
|
260
|
+
`routes/console.ts:555`, `cli/mercury.ts:2362,2380`). The test scripts called
|
|
261
|
+
the DB layer directly and skipped that half.
|
|
262
|
+
**Rule for any future throwaway-space harness: go through the API, or call
|
|
263
|
+
`removeSpaceWorkspace` alongside `deleteSpace`.**
|
|
264
|
+
- **D-5. `agent.env_passthrough` tightened to `claimed` — 2026-08-10.** Startup
|
|
265
|
+
was logging that the blind `MERCURY_*` passthrough carried `MERCURY_AGENT_IMAGE`
|
|
266
|
+
into every space's container, scoped to nothing. `claimed` narrows it: model
|
|
267
|
+
provider credentials stay exempt (otherwise the agent could not reach a model)
|
|
268
|
+
and extension-declared vars are injected separately behind their permission
|
|
269
|
+
gate, so the only var actually withdrawn is `AGENT_IMAGE` — host-side only,
|
|
270
|
+
and nothing in-container reads it (grepped across `src/`, `container/`,
|
|
271
|
+
`examples/`, `resources/`, `docker/`). Startup now logs the `claimed` line.
|
|
272
|
+
Backup: `mercury.yaml.bak-envpassthrough-20260810`.
|
|
273
|
+
**The value of the setting is the audit, not the one var** — it converts every
|
|
274
|
+
future `.env` addition from silently container-wide into a deliberate
|
|
275
|
+
declaration.
|
|
276
|
+
- **D-6. Machine audit against the setup guide — 2026-08-10, clean.** Ubuntu
|
|
277
|
+
26.04, bun 1.3.14, node v24.19.0, docker 29.1.3 (enabled + active, user in
|
|
278
|
+
the `docker` group), git, curl. `mercury` 0.9.0 on PATH; `mercury doctor`
|
|
279
|
+
reports 8 passed / 1 warning / 0 errors, the warning being "port 8787 in use",
|
|
280
|
+
which is Mercury itself. Lingering is on. Derived image carries pandoc 3.1.3,
|
|
281
|
+
python `docx`/`openpyxl`/`odf`, and `mrctl`. `pandoc` is **absent from the
|
|
282
|
+
host** and that is correct — the docs extension needs it in the container.
|
|
283
|
+
The unit at `~/.config/systemd/user/mercury.service` matches what
|
|
284
|
+
`generateSystemdService()` emits, field for field, with one **deliberate**
|
|
285
|
+
documented deviation: `Restart=always` instead of the generated
|
|
286
|
+
`Restart=on-failure`, re-applied by hand because `service install` reverts it
|
|
287
|
+
(see `archive/run-from-source-switchover.md` step 5). Anyone re-running
|
|
288
|
+
`mercury service install` must re-apply it again.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## E. Phone-dependent manual checks — never run
|
|
293
|
+
|
|
294
|
+
Every one of these needs the owner's phone; none block the PR, but the
|
|
295
|
+
milestone checkpoints are not truly closed until they pass. Full detail lives in
|
|
296
|
+
the per-story specs in `docs/goals/whatsapp-bot-hardening/archive/`.
|
|
297
|
+
|
|
298
|
+
| # | Check | From |
|
|
299
|
+
|---|-------|------|
|
|
300
|
+
| E-1 | 3 untagged messages in a test group → exactly 3 new `role='ambient'` rows, with authors | M2 |
|
|
301
|
+
| E-2 | Two people talk, a third asks "who said X?" → correct attribution | M2 |
|
|
302
|
+
| E-3 | Restart the service, then quote-reply an old message → `<reply_to>` shows a **name**, not a raw LID | M2 |
|
|
303
|
+
| E-4 | In a **throwaway** space: `/spaces delete <other-id>` → **reply** "yes" → named space dies, current one survives (the baduk regression) | M3 |
|
|
304
|
+
| E-5 | Bare `/spaces list`, untagged, in a group | M3 |
|
|
305
|
+
| E-6 | Send a ~50 MB docx → bot reads it. Send ~200 MB → bot says *why* it can't | M4 |
|
|
306
|
+
| E-7 | "Make me a doc with X" → `.docx` arrives and opens in Word, triggered by a **member**, not the admin | M4 |
|
|
307
|
+
| E-8 | Unlink a throwaway group, then re-link it entirely from chat | M3 |
|
|
308
|
+
|
|
309
|
+
⚠️ E-1 to E-3 want `log_level: debug` for the test window only — it writes
|
|
310
|
+
chat-content previews into journald, i.e. real family/friends chat into system
|
|
311
|
+
logs. **Set it back to `info` afterwards.**
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## F. Scheduled tasks — the 2026-08-10 football failure
|
|
316
|
+
|
|
317
|
+
Filed in full at
|
|
318
|
+
[`docs/debug/major/2026-08-11-scheduled-task-timeout-silent.md`](../../debug/major/2026-08-11-scheduled-task-timeout-silent.md)
|
|
319
|
+
— fixed and archived. Deferred by decision on 2026-08-10; recorded here so it is
|
|
320
|
+
not lost. The runtime half it spun off is
|
|
321
|
+
[`scheduled-research-task-runtime-drift`](../../debug/moderate/2026-08-19-scheduled-research-task-runtime-drift.md).
|
|
322
|
+
|
|
323
|
+
The daily football update never reached the group. The scheduler fired on time;
|
|
324
|
+
the container ran the full 20-minute timeout and was killed. Two defects:
|
|
325
|
+
|
|
326
|
+
- **F-1 (generic, affects every scheduled task).** A task whose handler throws is
|
|
327
|
+
reported nowhere — the scheduler logs and swallows, and `runtime.ts` only
|
|
328
|
+
sends on success. Worse, `next_run_at` is advanced *before* running, so one
|
|
329
|
+
failure costs a whole cron period with no retry. Same "no silent drops"
|
|
330
|
+
principle M4.1 applied to media, not yet applied to tasks.
|
|
331
|
+
|
|
332
|
+
⚠️ **Re-verified against `origin/main` 9e78a68** ("stop dashboard Run now from
|
|
333
|
+
firing a second agent run"), which rewrote ~156 lines of
|
|
334
|
+
`task-scheduler.ts` after this was first written. **Both halves survive**, at
|
|
335
|
+
new locations: the pre-advance is now `runTask`, `task-scheduler.ts:155-158`,
|
|
336
|
+
and its doc comment states it as deliberate ("Both happen regardless of
|
|
337
|
+
handler success"); the swallow is `task-scheduler.ts:168-174`. So F-1 is a
|
|
338
|
+
design decision to argue with, not an oversight to patch quietly — and any
|
|
339
|
+
fix now has to keep 9e78a68's in-flight/`applyBookkeeping` contract intact.
|
|
340
|
+
- **F-2 (this task).** The prompt asks for 6–8 improvised web searches daily, so
|
|
341
|
+
its runtime is unbounded: 5.9 min on Aug 9, 20.0 min (timeout) on Aug 10, with
|
|
342
|
+
a **byte-identical** prompt. Wants a faster, more deterministic pipeline —
|
|
343
|
+
possibly a skill or script — that keeps the freshness and the named-source
|
|
344
|
+
precision. Investigate before designing.
|
|
345
|
+
|
|
346
|
+
First step for either is capturing a pi trace of one run: the container is
|
|
347
|
+
`--rm`, so today's 20 minutes left no evidence of where the time went.
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## G. Tagging the bot by number — **fixed 2026-08-10**
|
|
352
|
+
|
|
353
|
+
**The bug.** In `greece2026`, tagging the bot's number did nothing. The chain:
|
|
354
|
+
the adapter rewrites a tag into `@<botUsername>` (`@ויציבוט` here); the space
|
|
355
|
+
sets `trigger.patterns = "Wiz, @Wiz"`; and `trigger.ts` *replaces* the default
|
|
356
|
+
pattern list when a space defines its own, discarding the `@<botUsername>` that
|
|
357
|
+
`router.ts` appends to the defaults. So the rewritten name matched nothing and
|
|
358
|
+
a correct tag was filed as ambient.
|
|
359
|
+
|
|
360
|
+
**Caught in the live DB:** `messages#1181`, greece2026, 2026-08-10T07:47:44Z,
|
|
361
|
+
from Sharon — `@ויציבוט : I am very fond of cheeses, what can I expect and
|
|
362
|
+
explore on our holiday?` — stored `role='ambient'`, never answered.
|
|
363
|
+
|
|
364
|
+
**`main` was broken the same way** and nobody had noticed: its patterns are all
|
|
365
|
+
bare (`ויציבוט`, no `@` form), and the mention regex requires start-of-string or
|
|
366
|
+
whitespace before the pattern, so a bare pattern never matches `@`-prefixed
|
|
367
|
+
text. Only space `19` worked, and only because `@ויציבוט` had been added to its
|
|
368
|
+
list by hand.
|
|
369
|
+
|
|
370
|
+
**The fix** stops re-deriving "was I addressed" from text. The adapter already
|
|
371
|
+
knows from the mention's JID (`isBotJid`), so it now sets `isBotMentioned` and
|
|
372
|
+
that flag rides the IngressMessage to both trigger gates (`router.ts`,
|
|
373
|
+
`handler.ts` — they each have one). The text rewrite stays, but only as
|
|
374
|
+
cosmetics for stripping and readability; it is allowed to find nothing.
|
|
375
|
+
|
|
376
|
+
- **Off switch:** space config `trigger.mention_always` (default `true`), set
|
|
377
|
+
per space. `mrctl config set trigger.mention_always false` works as soon as
|
|
378
|
+
the key is in `BUILTIN_CONFIG_KEYS` — but the **dashboard row is not free**.
|
|
379
|
+
`routes/dashboard.ts` builds the "Triggers & ambient" rows from a hardcoded
|
|
380
|
+
list, so the control had to be added there by hand (it reads through
|
|
381
|
+
`mentionAlwaysEnabled()`, the same helper the gates use, so the two cannot
|
|
382
|
+
drift).
|
|
383
|
+
- **Also fixed in passing:** `mergeIngresses` dropped `droppedAttachments`, so
|
|
384
|
+
the M4.1 media-drop note vanished whenever two messages merged inside the
|
|
385
|
+
debounce window — the exact silence that fix existed to remove.
|
|
386
|
+
- Cover: `tests/mention-always-triggers.test.ts` (13 tests), including the live
|
|
387
|
+
failure and the case where the rewrite finds nothing.
|
|
388
|
+
|
|
389
|
+
### G-1. Still open: Discord has the same latent bug
|
|
390
|
+
|
|
391
|
+
`discord-native.ts:430` already computes a structured `isMention` from
|
|
392
|
+
`msg.mentions.users`, then throws it away and relies on the same text rewrite.
|
|
393
|
+
Only the WhatsApp bridge sets `isBotMentioned`. A Discord space with custom
|
|
394
|
+
trigger patterns would fail exactly as `greece2026` did. Not wired up because
|
|
395
|
+
this branch is WhatsApp-scoped — the flag and both gates are already generic, so
|
|
396
|
+
it is a two-line bridge change whenever Discord matters.
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# Roadmap: Mercury WhatsApp Bot — Context, Identity & Command Hardening
|
|
2
|
+
|
|
3
|
+
**Goal**: [whatsapp-bot-hardening](goal.md)
|
|
4
|
+
**Last updated**: 2026-08-10
|
|
5
|
+
**Still open**: [open-threads.md](open-threads.md)
|
|
6
|
+
|
|
7
|
+
**All four milestones are done.** What remains is the PR (parked on push access)
|
|
8
|
+
and the phone-dependent manual checks — both tracked in
|
|
9
|
+
[open-threads.md](open-threads.md), which is now the live to-do list. This file
|
|
10
|
+
is the record of what shipped.
|
|
11
|
+
|
|
12
|
+
Ordering follows the handoff's §7 (archived at
|
|
13
|
+
`docs/goals/whatsapp-bot-hardening/archive/handoff-2026-08-09.md`): context/identity
|
|
14
|
+
first (highest impact,
|
|
15
|
+
shared code path), then the three defects that destroy data or lock you out,
|
|
16
|
+
then the rest of the command surface, then capabilities, then member memory.
|
|
17
|
+
The switchover is Milestone 1 because nothing can be tested live before it.
|
|
18
|
+
|
|
19
|
+
## Milestones
|
|
20
|
+
|
|
21
|
+
### Milestone 1: Run from source
|
|
22
|
+
> The live bot serves from the git checkout; fixes become edit→restart testable.
|
|
23
|
+
|
|
24
|
+
| ID | Story | Slug | Depends on | Status |
|
|
25
|
+
|----|-------|------|------------|--------|
|
|
26
|
+
| M1.1 | Deployment switchover + commit the `.dockerignore` build fix | run-from-source-switchover | — | **done** |
|
|
27
|
+
|
|
28
|
+
**Checkpoint:** DB backed up (all three files: `state.db`, `-wal`, `-shm`);
|
|
29
|
+
`bun run check` green; service restarted and answering a WhatsApp DM;
|
|
30
|
+
`SELECT model, provider FROM token_usage ORDER BY id DESC LIMIT 1` confirms the
|
|
31
|
+
expected model; the 0.9.0 db.ts migration ran cleanly against the existing DB;
|
|
32
|
+
rollback path (`npm unlink` + old unit) verified to exist but not exercised.
|
|
33
|
+
|
|
34
|
+
### Milestone 2: Context & identity (Bugs 1 + 2)
|
|
35
|
+
> The bot sees untagged group messages and knows who said what — with the
|
|
36
|
+
> context budget capped so ambient can't starve real turns.
|
|
37
|
+
|
|
38
|
+
| ID | Story | Slug | Depends on | Status |
|
|
39
|
+
|----|-------|------|------------|--------|
|
|
40
|
+
| M2.1 | Message author attribution (schema, rendering, pushNames persistence, LID resolution) | message-author-attribution | M1.1 | **done** (`a7279e8`) |
|
|
41
|
+
| M2.2 | Ambient group context (recordAmbient, bounded query, retention TTL) | ambient-group-context | M2.1 | **done** (`4bf5505`) |
|
|
42
|
+
|
|
43
|
+
M2.1 lands first so that when ambient starts flowing it is attributed from day
|
|
44
|
+
one — ambient without attribution produces a wall of anonymous text that makes
|
|
45
|
+
misidentification *worse* (HANDOFF §7.1).
|
|
46
|
+
|
|
47
|
+
**Checkpoint:** with `log_level: debug` for the test window only — send 3
|
|
48
|
+
untagged messages in a test group → exactly 3 new `role='ambient'` rows with
|
|
49
|
+
authors; tag the bot and ask what was just said; two people talk, a third asks
|
|
50
|
+
"who said X?" → correct attribution; restart the service, quote-reply an old
|
|
51
|
+
message → `<reply_to>` shows a name, not a raw LID. Then set logging back to
|
|
52
|
+
`info` and confirm ambient rows for `greece2026` keep growing:
|
|
53
|
+
`SELECT space_id, role, COUNT(*) FROM messages GROUP BY space_id, role`.
|
|
54
|
+
|
|
55
|
+
### Milestone 3: Command surface safety
|
|
56
|
+
> No command can silently destroy data, lock a group out, or fail to stop a
|
|
57
|
+
> runaway agent; chat and CLI gates agree.
|
|
58
|
+
|
|
59
|
+
| ID | Story | Slug | Depends on | Status |
|
|
60
|
+
|----|-------|------|------------|--------|
|
|
61
|
+
| M3.1 | Destructive command guards — mrctl delete argument, confirmation matching, unlink lockout, /stop routing (Bugs 5.1, 6a, 6c) | destructive-command-guards | M1.1 | **done** (`c44720f`) |
|
|
62
|
+
| M3.2 | Command routing consistency — untagged slash commands in groups, permission-gate alignment, /resume + /help pause, redundancy cleanup (Bugs 5 rest, 6b, 6d, 6e) | command-routing-consistency | M3.1 | **done** (`4f05fbb`) |
|
|
63
|
+
|
|
64
|
+
**Checkpoint:** in a **throwaway** space/group only — `/spaces delete <other-id>`
|
|
65
|
+
then **reply** "yes" → the named space dies, current one survives (the baduk
|
|
66
|
+
regression test); `mrctl spaces delete foo` refuses with the explanatory error;
|
|
67
|
+
`/stop` during a running turn aborts it; unlink a throwaway group then re-link
|
|
68
|
+
it entirely from chat; bare `/spaces list` works untagged in a group.
|
|
69
|
+
|
|
70
|
+
### Milestone 4: Capabilities & member memory
|
|
71
|
+
> Large files are readable, failures are explained, the bot can produce real
|
|
72
|
+
> documents, and it accumulates per-member knowledge.
|
|
73
|
+
|
|
74
|
+
| ID | Story | Slug | Depends on | Status |
|
|
75
|
+
|----|-------|------|------------|--------|
|
|
76
|
+
| M4.1 | Media size cap + no silent drops (Bug 3) | media-size-and-silent-drop | M1.1 | **done** (`5734c04`) |
|
|
77
|
+
| M4.2 | `docs` extension — real .docx creation (Bug 4) | docs-extension | M1.1 | **done** (`e1208b8`) |
|
|
78
|
+
| M4.3 | Member memory — hard identity + soft notes | member-memory | M2.1 | **done** (`44a7542`) |
|
|
79
|
+
|
|
80
|
+
**Checkpoint:** send a ~50 MB docx → bot reads it; send a ~200 MB file → bot
|
|
81
|
+
says *why* it can't; "make me a doc with X" → a `.docx` arrives in WhatsApp and
|
|
82
|
+
opens in Word, triggered by a member (not just admin); after some group chat,
|
|
83
|
+
ask the bot what it knows about a member → answer draws on
|
|
84
|
+
`knowledge/members/<id>.md` with correct identity.
|
|
85
|
+
|
|
86
|
+
## Dependency Graph
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
M1.1 run-from-source-switchover
|
|
90
|
+
├─→ M2.1 message-author-attribution ─→ M2.2 ambient-group-context
|
|
91
|
+
│ └─→ M4.3 member-memory
|
|
92
|
+
├─→ M3.1 destructive-command-guards ─→ M3.2 command-routing-consistency
|
|
93
|
+
├─→ M4.1 media-size-and-silent-drop
|
|
94
|
+
└─→ M4.2 docs-extension
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
M3.x is independent of M2.x and can be built in parallel, but M3.1 should not
|
|
98
|
+
wait past M2 — it contains the fixes for the defects that already destroyed a
|
|
99
|
+
space once.
|
|
100
|
+
|
|
101
|
+
## Deploying prompt changes — read this before believing a test
|
|
102
|
+
|
|
103
|
+
**`src/agent/container-entry.ts` runs inside the agent container and is baked
|
|
104
|
+
into the image** (`container/Dockerfile:107`). Restarting the service does not
|
|
105
|
+
pick up edits to it. Everything host-side (ambient recording, author storage,
|
|
106
|
+
command guards, media notes) goes live on `systemctl --user restart mercury`;
|
|
107
|
+
everything prompt-side (author names in history, ambient rendering,
|
|
108
|
+
`<members>`, member notes) needs the image rebuilt.
|
|
109
|
+
|
|
110
|
+
The derived image's cache key is
|
|
111
|
+
`sha256(baseImage + "\n" + baseImageId + "\n" + installCommands)`
|
|
112
|
+
(`extensions/image-builder.ts:313-314`). It **does** include the base image ID,
|
|
113
|
+
so `bash ./container/build.sh` alone is enough to change the derived tag and
|
|
114
|
+
force a rebuild — measured 2026-08-10, tag moved `dd9a11d5 → 0597b302` with the
|
|
115
|
+
extension list untouched.
|
|
116
|
+
|
|
117
|
+
The caveat is `resolveBaseImageId` (`image-builder.ts:327`): it shells out to
|
|
118
|
+
`docker image inspect` and returns `null` on failure. A null base ID drops out
|
|
119
|
+
of the key and the stale-cache trap comes back, silently. So `docker rmi` is
|
|
120
|
+
belt-and-braces rather than mandatory — cheap enough to keep in the procedure.
|
|
121
|
+
|
|
122
|
+
Full procedure after touching `container-entry.ts`:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
cd ~/mercury-src && bash ./container/build.sh # rebuild mercury-agent:latest
|
|
126
|
+
docker rmi mercury-agent-ext:<current-hash> # optional; forces it regardless
|
|
127
|
+
systemctl --user restart mercury # ~4 min rebuild, then live
|
|
128
|
+
docker run --rm --entrypoint sh mercury-agent-ext:<new-hash> \
|
|
129
|
+
-c 'grep -c members /app/src/agent/container-entry.ts' # verify, do not assume
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Then confirm the service actually resolved to the new image: its
|
|
133
|
+
`Built derived agent image <tag>` / `Using cached agent image <tag>` log line
|
|
134
|
+
must show the **new** hash, not the one you deleted.
|
|
135
|
+
|
|
136
|
+
This was found by asking the bot to list its `<members>` block and getting an
|
|
137
|
+
honest "there is no members block" — the tests were green and the service had
|
|
138
|
+
been restarted. Verify prompt changes against the running bot, not the repo.
|
|
139
|
+
|
|
140
|
+
Note also that `sync-mercury.sh` only warns you to rebuild when `container/` or
|
|
141
|
+
`docker/` changed. `src/agent/container-entry.ts` is baked in too and is **not**
|
|
142
|
+
in that check, so a prompt-only change syncs and restarts with no warning at
|
|
143
|
+
all.
|
|
144
|
+
|
|
145
|
+
## Completion — the PR (after Milestone 4)
|
|
146
|
+
|
|
147
|
+
Not a story; the goal's exit gate (archived handoff §5). One PR from
|
|
148
|
+
`fix/whatsapp-context-identity-and-docs` → `main`, one section per change,
|
|
149
|
+
explicitly covering:
|
|
150
|
+
|
|
151
|
+
1. The four pinchtab commits as **upstreaming an already-live hotfix**, with
|
|
152
|
+
the `allowedDomains "*"` trade-off stated plainly (D-011).
|
|
153
|
+
2. The `.dockerignore` fix (exists in no commit anywhere until M1.1).
|
|
154
|
+
3. The `messages` schema migration and its NULL-backfill behaviour.
|
|
155
|
+
4. Ambient volume/retention implications.
|
|
156
|
+
5. The baduk incident as motivation for the regression tests.
|
|
157
|
+
6. **Behaviour changes users will feel:** `/stop` now aborts instead of
|
|
158
|
+
prompting; slash commands work untagged in groups; `/spaces unlink` removed
|
|
159
|
+
from chat; `/model active` dropped.
|
|
160
|
+
|
|
161
|
+
### Pre-PR housekeeping — **done** (2026-08-10)
|
|
162
|
+
|
|
163
|
+
| Item | Outcome |
|
|
164
|
+
|------|---------|
|
|
165
|
+
| D:-clone's `fix/pinchtab-browser-startup` | **Superseded, not pushed** (D-013). Combined patch byte-identical to `bfe5a33..b12d7c0` in `mercury-src`; clone's base is 3 commits behind `origin/main`. Branch left in place. |
|
|
166
|
+
| Stray `~/.mercury/state.db` | **Deleted.** Verified schema-only first: 0 messages, 0 spaces, 0 space_roles. Live DB is `~/whatsapp-bot/.mercury/state.db` (1020 messages, 4 spaces, 27 roles) and was untouched. |
|
|
167
|
+
| Debug logging | **Already `info`.** `mercury.yaml:45`; no `MERCURY_LOG_LEVEL` in `.env` (only the three API keys and `MERCURY_AGENT_IMAGE`), so no env override. Debug was never enabled during implementation. |
|
|
168
|
+
|
|
169
|
+
### The cwd trap — root-caused and closed (2026-08-10)
|
|
170
|
+
|
|
171
|
+
The stray DB existed because `data_dir: .mercury` is **relative**, resolved
|
|
172
|
+
against `process.cwd()` by `resolveProjectPath` (`config.ts:412`). The service
|
|
173
|
+
was never at risk (`WorkingDirectory=/home/guysaha/whatsapp-bot`), but running
|
|
174
|
+
`mercury` by hand from `$HOME` created a fresh empty `~/.mercury/state.db` and
|
|
175
|
+
used *that* — which is why `mercury auth status` reported "not logged in".
|
|
176
|
+
|
|
177
|
+
Making `data_dir` absolute is **not sufficient on its own**: `mercury.yaml` is
|
|
178
|
+
itself located relative to cwd (`config-file.ts:315`), so from `$HOME` no config
|
|
179
|
+
is found and the built-in `.mercury` default applies regardless. Both halves
|
|
180
|
+
were required:
|
|
181
|
+
|
|
182
|
+
1. `runtime.data_dir: /home/guysaha/whatsapp-bot/.mercury` in `mercury.yaml`
|
|
183
|
+
2. `export MERCURY_CONFIG_FILE=/home/guysaha/whatsapp-bot/mercury.yaml` in
|
|
184
|
+
`~/.profile` — deliberately **not** `~/.bashrc`, which returns early for
|
|
185
|
+
non-interactive shells (`case $- in *i*) ;; *) return;;`) and so would never
|
|
186
|
+
apply to `wsl -e bash -lc`.
|
|
187
|
+
|
|
188
|
+
Verified by loading the real `loadConfig()` from both directories, with a
|
|
189
|
+
negative control: unsetting `MERCURY_CONFIG_FILE` makes `$HOME` fall back to
|
|
190
|
+
`/home/guysaha/.mercury`, proving the mechanism rather than assuming it. After
|
|
191
|
+
the restart, `mercury auth status` from `$HOME` reports "✓ logged in", no
|
|
192
|
+
`~/.mercury` is recreated, and the live DB is intact.
|
|
193
|
+
|
|
194
|
+
**Residual limit:** `.env` is still cwd-bound — hardcoded `join(CWD, ".env")`
|
|
195
|
+
(`cli/mercury.ts:112`), no override exists. CLI commands that need API keys must
|
|
196
|
+
still run from `~/whatsapp-bot`; `mercury auth status` elsewhere will say "No API
|
|
197
|
+
keys found in .env". Not worth patching upstream for this alone.
|
|
198
|
+
|
|
199
|
+
**`mrctl` is not a host CLI.** It is the in-container agent tool, reading
|
|
200
|
+
`API_URL`/`CALLER_ID`/`SPACE_ID` from the environment and exiting
|
|
201
|
+
"API_URL not set" on the host. Inspect live state by querying `state.db`.
|
|
202
|
+
|
|
203
|
+
Backups: `mercury.yaml.bak-datadir-20260810`, `~/.profile.bak-20260810`.
|
|
204
|
+
|
|
205
|
+
### API exposure — closed 2026-08-10
|
|
206
|
+
|
|
207
|
+
Found while building a phone-free test harness. `Bun.serve` is called with no
|
|
208
|
+
`hostname` (`main.ts:128`), so the server listens on `*:8787`, and **both auth
|
|
209
|
+
secrets were unset**. Unauthenticated, that meant:
|
|
210
|
+
|
|
211
|
+
- `GET /dashboard/page/*` — every dashboard data page
|
|
212
|
+
- `GET /api/*` — caller identity is self-asserted via `x-mercury-caller` /
|
|
213
|
+
`x-mercury-space` headers, so any space's data was readable
|
|
214
|
+
- `POST /chat` — runs a real agent turn: spends tokens, invokes tools
|
|
215
|
+
|
|
216
|
+
Not LAN-reachable (WSL2 default NAT, no `.wslconfig`, no `netsh portproxy`), but
|
|
217
|
+
reachable from any process on the Windows host and — because `/chat` parses a
|
|
218
|
+
`text/plain` body — from **any web page open in a browser**, since that makes a
|
|
219
|
+
cross-origin POST a CORS "simple request" with no preflight. The response can't
|
|
220
|
+
be read, but the turn still executes.
|
|
221
|
+
|
|
222
|
+
Fixed by setting `MERCURY_API_SECRET` and `MERCURY_CHAT_API_KEY` in `.env`
|
|
223
|
+
(backup `.env.bak-secrets-20260810`), then restarting. Verified after: all three
|
|
224
|
+
surfaces return 401, and an authenticated `/chat` turn that runs `mrctl prefs
|
|
225
|
+
list` still succeeds — confirming `API_SECRET` reaches the container
|
|
226
|
+
(`container-runner.ts:950`).
|
|
227
|
+
|
|
228
|
+
`GET /dashboard` still returns 200 unauthenticated. That is the static SPA shell
|
|
229
|
+
only (`dashboard/index.html`, no space data); its `app.get` is registered at
|
|
230
|
+
`server.ts:135`, before the `/dashboard/*` auth middleware at `server.ts:191`,
|
|
231
|
+
and Hono's `/dashboard/*` does not match the bare path. Version disclosure only.
|
|
232
|
+
|
|
233
|
+
**Do not "fix" this by binding to `127.0.0.1`** — containers reach the API via
|
|
234
|
+
`host.docker.internal:host-gateway`, so loopback-only binding breaks every turn.
|
|
235
|
+
|
|
236
|
+
Remaining hardening moved to [open-threads.md](open-threads.md) §B.
|
|
237
|
+
|
|
238
|
+
### Prompt layers — investigated 2026-08-10
|
|
239
|
+
|
|
240
|
+
Triggered by the question "do space preferences actually change behaviour?".
|
|
241
|
+
They do — `19`'s "no emojis at all" holds at 0/40 assistant messages. But the
|
|
242
|
+
investigation found **five** prompt layers rather than the three assumed, one of
|
|
243
|
+
which (`space_config.system_prompt`) has no setter at all, and it measured that
|
|
244
|
+
**precedence between layers is not deterministic** (D-014). Full map, the emoji
|
|
245
|
+
data, and the live conflicts are in [open-threads.md](open-threads.md) §A.
|