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,588 @@
|
|
|
1
|
+
> **ARCHIVED 2026-08-10 — fully executed. Do not plan from this file.**
|
|
2
|
+
>
|
|
3
|
+
> Every fix in §4 shipped (8 stories, 13 commits on
|
|
4
|
+
> `fix/whatsapp-context-identity-and-docs`); §3's switchover is live; §5 steps
|
|
5
|
+
> 1–5 are done; all of §6's open decisions were taken (see `decisions.md`
|
|
6
|
+
> D-001…D-013). Kept as the **investigation record** — the root causes here were
|
|
7
|
+
> verified against running code and live data, and the file:line references are
|
|
8
|
+
> still the best map of *why* each fix looks the way it does.
|
|
9
|
+
>
|
|
10
|
+
> Two things in it are now **wrong**, corrected elsewhere:
|
|
11
|
+
> - §1's tree table says the live bot is the npm-installed 0.8.12. It is not —
|
|
12
|
+
> the service has run from `~/mercury-src` since 2026-08-10 (D-001).
|
|
13
|
+
> - §1 says `mercury auth status` is cwd-sensitive. Fixed 2026-08-10; see the
|
|
14
|
+
> roadmap's "The cwd trap" section.
|
|
15
|
+
>
|
|
16
|
+
> Live successor documents:
|
|
17
|
+
> - `docs/goals/whatsapp-bot-hardening/roadmap.md` — status of everything here
|
|
18
|
+
> - `docs/goals/whatsapp-bot-hardening/open-threads.md` — what is still open
|
|
19
|
+
> - `docs/goals/whatsapp-bot-hardening/archive/*.md` — the 8 per-story specs,
|
|
20
|
+
> with retrospectives
|
|
21
|
+
|
|
22
|
+
# Mercury WhatsApp Bot — Bug Fix & Feature Handoff
|
|
23
|
+
|
|
24
|
+
**Written:** 2026-08-09 · **For:** a fresh Claude Code session picking up this work
|
|
25
|
+
**Branch:** `fix/whatsapp-context-identity-and-docs` in `~/mercury-src` (WSL)
|
|
26
|
+
**Status:** investigation complete, pinchtab commits merged, **no code fixes written yet**
|
|
27
|
+
|
|
28
|
+
Read this top to bottom before touching anything. Every root cause below was verified
|
|
29
|
+
against running code and live data — file:line references are to the **v0.9.0 tree in
|
|
30
|
+
`~/mercury-src`**, not the currently-running build.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 1. Environment — read this first
|
|
35
|
+
|
|
36
|
+
There are **three different source trees** and they are not the same. Confusing them
|
|
37
|
+
will waste your time.
|
|
38
|
+
|
|
39
|
+
| Tree | Version | Role |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `~/.nvm/versions/node/v24.19.0/lib/node_modules/mercury-agent` | 0.8.12 | **What is running right now** (systemd `mercury`) |
|
|
42
|
+
| `~/mercury-src` | v0.9.0 + our branch | **The working repo — do all work here** |
|
|
43
|
+
| `D:\Projects\whatsapp-bot\mercury` | 17c92c6 | Stale Windows clone. Will drift. Do not edit. |
|
|
44
|
+
|
|
45
|
+
The live bot runs in **WSL Ubuntu**, project dir `~/whatsapp-bot` (systemd user unit
|
|
46
|
+
`mercury`). This Claude session runs on **Windows**.
|
|
47
|
+
|
|
48
|
+
**Operating rules (learned the hard way):**
|
|
49
|
+
|
|
50
|
+
- Run commands via `wsl.exe -e bash -lc '<cmd>'` and pipe through `tr -d '\0'` —
|
|
51
|
+
output is otherwise NUL-interleaved and unreadable.
|
|
52
|
+
- **Fix PATH on every invocation.** Even `bash -lc` resolves `npm`/`bun` to the
|
|
53
|
+
*Windows* binaries via PATH interop. Prefix with:
|
|
54
|
+
`export PATH=$HOME/.nvm/versions/node/v24.19.0/bin:$HOME/.bun/bin:$PATH`
|
|
55
|
+
- **Edit files via the UNC path** `\\wsl.localhost\Ubuntu\home\guysaha\mercury-src\...`
|
|
56
|
+
with Read/Edit/Write. This works and beats heredocs.
|
|
57
|
+
- **Never Read `~/whatsapp-bot/.env`** (live API keys). Edit it with `awk`/`sed` inside WSL.
|
|
58
|
+
- `MERCURY_*` env vars in `.env` **silently override** `mercury.yaml` (`config.ts`).
|
|
59
|
+
Before debugging any config behaviour, dump the *effective* value, don't read the YAML.
|
|
60
|
+
Currently no media/log overrides are set, so YAML edits will take effect.
|
|
61
|
+
- `mercury auth status` is **cwd-sensitive** — it reports "not logged in" unless you
|
|
62
|
+
`cd ~/whatsapp-bot` first.
|
|
63
|
+
- Restart with `systemctl --user restart mercury`. **Never** run `mercury run` directly,
|
|
64
|
+
**never** kill by port (repo CLAUDE.md).
|
|
65
|
+
- Which model actually served is only knowable from the DB, never the reply:
|
|
66
|
+
`SELECT model, provider FROM token_usage ORDER BY id DESC LIMIT 1`. No `sqlite3`
|
|
67
|
+
installed — use `bun -e` with `bun:sqlite`.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 2. Current repo state
|
|
72
|
+
|
|
73
|
+
Branch `fix/whatsapp-context-identity-and-docs`, based on `70a5ce3` (v0.9.0):
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
b12d7c0 refactor(web-browser): probe the pinchtab bridge port with curl
|
|
77
|
+
1fee658 fix(web-browser): start the pinchtab daemon with the "server" subcommand
|
|
78
|
+
7ae56c7 fix(container): disable core dumps in agent containers
|
|
79
|
+
bfe5a33 fix(web-browser): bake the pinchtab domain allowlist into the agent image
|
|
80
|
+
70a5ce3 (origin/main, v0.9.0)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Nothing has been pushed.** Git identity is set *repo-locally* to
|
|
84
|
+
`saharw01 <Saharw01@gmail.com>` to match existing commits.
|
|
85
|
+
|
|
86
|
+
**Uncommitted, and it must go in the PR:** `~/mercury-src/.dockerignore` has a local
|
|
87
|
+
edit adding `!examples/extensions` + `!examples/extensions/**`. This is a real build
|
|
88
|
+
fix — `.dockerignore:15` ignores `examples/`, but `container/Dockerfile:118` does
|
|
89
|
+
`COPY examples/extensions/ /tmp/examples-extensions/`, which copies nothing without the
|
|
90
|
+
negation. **This exists in no commit on any branch or remote.** Commit it.
|
|
91
|
+
|
|
92
|
+
### Why the pinchtab commits were merged
|
|
93
|
+
|
|
94
|
+
All four were verified necessary (or harmless hardening) by testing the real binary,
|
|
95
|
+
**not** by trusting commit messages:
|
|
96
|
+
|
|
97
|
+
- `pinchtab server` — **real bug.** `pinchtab --help` documents `server` as "Starts the
|
|
98
|
+
full server"; bare `pinchtab` prints a banner and **exits 0**. The old
|
|
99
|
+
`nohup pinchtab &` never started a daemon.
|
|
100
|
+
- `allowedDomains "*"` — **real bug.** pinchtab's TRUST.md documents the navigation
|
|
101
|
+
default as a "Local-only allowlist"; `idpi_domain_blocked` exists in the binary;
|
|
102
|
+
`idpi.enabled/strictMode/scanContent/wrapContent` are all `true` by default. Every
|
|
103
|
+
public URL was refused. ⚠️ **Security note for the PR:** `"*"` removes the domain
|
|
104
|
+
allowlist entirely. The injection guard genuinely stays on (verified), but this is a
|
|
105
|
+
deliberate widening of attack surface and should be described as such, not as a plain bugfix.
|
|
106
|
+
- `--ulimit core=0` — **justified.** WSL2 `/proc/sys/kernel/core_pattern` is literally
|
|
107
|
+
`core` with no handler pipe, so segfaults dump into the CWD = bind-mounted workspace.
|
|
108
|
+
The "~300 MB per dump" figure is the author's observation, **not independently verified**.
|
|
109
|
+
- curl port probe — **hardening, not a live fix.** Correctly labeled `refactor:`. The
|
|
110
|
+
branch's comment is honest that pi runs bash today so `/dev/tcp` works.
|
|
111
|
+
|
|
112
|
+
**Critical:** all four were already hand-patched into the *running* installed package and
|
|
113
|
+
the deployed extension. Switching the service to `~/mercury-src` **without** them would
|
|
114
|
+
have silently regressed the browser extension.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 3. Deployment switchover (do this before testing)
|
|
119
|
+
|
|
120
|
+
Decision already made: **run the bot from the git checkout**, so fixes are
|
|
121
|
+
version-controlled, testing is edit→restart, and `npm i -g` can't revert them.
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# 1. Backup — a restart checkpoints the ~4 MB WAL, so grab all three files
|
|
125
|
+
mkdir -p ~/mercury-backup-$(date +%s) && cp ~/whatsapp-bot/.mercury/state.db{,-wal,-shm} ~/mercury-backup-*/
|
|
126
|
+
|
|
127
|
+
# 2. Deps + gate
|
|
128
|
+
cd ~/mercury-src && bun install && bun run check
|
|
129
|
+
|
|
130
|
+
# 3. Repoint the CLI, reinstall the service the documented way
|
|
131
|
+
npm link # makes `mercury` resolve to ~/mercury-src
|
|
132
|
+
cd ~/whatsapp-bot && mercury service install --user
|
|
133
|
+
|
|
134
|
+
# 4. Re-apply Restart=always (generator writes on-failure), then restart
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Gotchas:**
|
|
138
|
+
- `service install` bakes `Environment=PATH=` from the install shell. Set PATH explicitly
|
|
139
|
+
or the unit won't find bun/node.
|
|
140
|
+
- It also **reverts `Restart=always` → `Restart=on-failure`**. The live unit was
|
|
141
|
+
hand-changed to `always`; re-apply it after install.
|
|
142
|
+
- A **db.ts migration runs on first 0.9.0 start** against the existing `state.db`.
|
|
143
|
+
That is the reason for step 1, not superstition.
|
|
144
|
+
- The derived Docker image may rebuild (cached by content hash, version changed). It
|
|
145
|
+
builds in the background and doesn't block startup, but early replies may be slow.
|
|
146
|
+
- **Leave the npm-installed 0.8.12 in place** as rollback: `npm unlink` + restore the
|
|
147
|
+
old unit gets you back in under a minute.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## 4. The fixes
|
|
152
|
+
|
|
153
|
+
### Bug 1 — Ambient mode is dead code (highest impact)
|
|
154
|
+
|
|
155
|
+
**Symptom:** the bot cannot see any group message it isn't tagged in.
|
|
156
|
+
|
|
157
|
+
**Root cause.** `src/core/handler.ts:237-240`:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
const shouldProcess = triggerResult.matched || (ingress.isReplyToBot && !isDM);
|
|
161
|
+
if (!shouldProcess && !isCommand) return; // ← returns here
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The ambient-write branch lives in `src/core/runtime.ts:591-612`, inside `handleRawInput`,
|
|
165
|
+
which is only reached via `processIngress` — **after** that early return. `ambient.enabled`
|
|
166
|
+
defaults to true and works as designed; it gates code nothing reaches.
|
|
167
|
+
|
|
168
|
+
**Evidence.** Across the whole DB there are exactly **2** ambient rows, both in space `19`,
|
|
169
|
+
both `סהר ויצמן: /spaces…`. That's the tell: `isCommand` is the sole bypass, so only stray
|
|
170
|
+
`/`-prefixed messages leak through. `greece2026` has **0**.
|
|
171
|
+
`handler.ts` is byte-identical on origin/main — **upstream has not fixed this.**
|
|
172
|
+
|
|
173
|
+
**Fix.**
|
|
174
|
+
1. Extract `runtime.ts:591-612` into a public `recordAmbient(ingress: IngressMessage)` on
|
|
175
|
+
`MercuryCoreRuntime` (keep the group-only + `ambient.enabled` + non-DM guards).
|
|
176
|
+
2. In `handler.ts:240`, replace the bare return:
|
|
177
|
+
```ts
|
|
178
|
+
if (!shouldProcess && !isCommand) {
|
|
179
|
+
core.recordAmbient(ingress);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
`ingress` is already built at line 221, so the data is in hand.
|
|
184
|
+
3. **Cap the ambient context budget.** `db.ts:1047 getRecentTurns` fetches `turnCount * 5`
|
|
185
|
+
rows (`db.ts:1068`) and lets ambient share that window. Once ambient actually flows,
|
|
186
|
+
it will crowd out real turns. Give ambient its own bounded query — do **not** ship
|
|
187
|
+
step 2 without this.
|
|
188
|
+
4. **Add an ambient retention TTL** to `storage-cleanup.ts`. Every group message is now a row.
|
|
189
|
+
|
|
190
|
+
**Known side effect (pre-existing, not caused by this fix):** `bridge.normalize` runs at
|
|
191
|
+
`handler.ts:221`, *before* the gate — so media from untriggered group messages is already
|
|
192
|
+
being downloaded and then discarded. Fixing bug 1 doesn't add that cost.
|
|
193
|
+
|
|
194
|
+
**Tests.**
|
|
195
|
+
- Unit: `recordAmbient` writes for a group + `ambient.enabled` unset/true; writes nothing
|
|
196
|
+
for a DM; writes nothing when `ambient.enabled=false`.
|
|
197
|
+
- Unit: handler calls `recordAmbient` exactly once for a non-triggered group message, and
|
|
198
|
+
**not** for a triggered one (no double-write).
|
|
199
|
+
- Unit: ambient context query returns at most N rows and does not starve user/assistant turns.
|
|
200
|
+
- Manual: send 3 untagged messages in a test group → assert 3 new `role='ambient'` rows;
|
|
201
|
+
then tag the bot and ask what was just said.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
### Bug 2 — Sender misidentification + member memory
|
|
206
|
+
|
|
207
|
+
**Symptom:** the bot attributes past messages to the wrong person.
|
|
208
|
+
|
|
209
|
+
**Root cause A (primary).** Stored messages carry **no author**. `runtime.ts:1573` writes
|
|
210
|
+
`addMessage(spaceId, "user", finalPrompt, …)`, and the `messages` table has no author
|
|
211
|
+
column (`id, space_id, role, content, attachments, run_meta, reply_to_id, created_at,
|
|
212
|
+
updated_at`). At render time `agent/container-entry.ts:598` emits `<user>…</user>` with no
|
|
213
|
+
name. The **only** identity anywhere in the prompt is `<caller id=… name=… role=…/>` at
|
|
214
|
+
`container-entry.ts:634` — the *current* speaker.
|
|
215
|
+
|
|
216
|
+
So in a group the model sees a flat run of unattributed turns plus one name. Attributing
|
|
217
|
+
the whole history to that person is the only reading the prompt supports. This is not a
|
|
218
|
+
model glitch.
|
|
219
|
+
|
|
220
|
+
**Root cause B.** Quoted-reply senders resolve to raw LIDs. `buildReplyContext`
|
|
221
|
+
(`adapters/whatsapp.ts:82`) looks names up in `pushNames` (`whatsapp.ts:169`) — an
|
|
222
|
+
**in-memory** map, empty after every restart — and canonicalizes sync-only (no async LID
|
|
223
|
+
lookup). Real row currently in the greece2026 DB:
|
|
224
|
+
`<reply_to name="136043810578453" jid="136043810578453@lid" …>`
|
|
225
|
+
|
|
226
|
+
**Good news:** the identity data already exists and is clean. `space_roles.display_name`
|
|
227
|
+
is fully populated, correctly keyed by canonical phone JID (26 members), e.g.
|
|
228
|
+
`whatsapp:972500000000@s.whatsapp.net → "Member Name"`. Nothing uses it for context.
|
|
229
|
+
|
|
230
|
+
**Fix.**
|
|
231
|
+
1. Migration: add `author_id` / `author_name` to `messages`; populate at `addMessage`.
|
|
232
|
+
2. Render `<user name="…" id="…">` in `buildHistoryXml`; put the author on ambient entries
|
|
233
|
+
too (ambient already prefixes `Name: text` at `runtime.ts:604` — promote it to an attribute).
|
|
234
|
+
3. Back `pushNames` with the DB: seed from `space_roles.display_name` at startup, write
|
|
235
|
+
through per message. Fixes `<reply_to>` across restarts.
|
|
236
|
+
4. Use the **async** `canonicalizeJid` for quoted participants so unknown LIDs resolve.
|
|
237
|
+
|
|
238
|
+
**Member memory feature.** Build on the existing episodic plumbing rather than a new
|
|
239
|
+
subsystem: per-space `knowledge/members/<id>.md`, injected like `<active_episodes>`
|
|
240
|
+
(`container-entry.ts:513`), written by the agent with the `write` tool it already has.
|
|
241
|
+
Pair it with a `<members>` block generated from `space_roles` — identity stays **hard**
|
|
242
|
+
(id → name from the DB, not guessable), characteristics stay **soft** (accumulating notes).
|
|
243
|
+
No schema migration for the notes half; naturally per-space.
|
|
244
|
+
|
|
245
|
+
**Tests.**
|
|
246
|
+
- Migration: runs idempotently on the existing DB; existing rows get NULL authors and
|
|
247
|
+
still render.
|
|
248
|
+
- Unit: history XML for messages from two different users renders two distinct names.
|
|
249
|
+
- Unit: `buildReplyContext` resolves a known LID via the alias store + `display_name`,
|
|
250
|
+
and degrades gracefully to the bare jid when unknown.
|
|
251
|
+
- Unit: `pushNames` survives a simulated restart (seeded from DB).
|
|
252
|
+
- Manual: two people talk in a group, then a third asks "who said X?" — verify correct attribution.
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
### Bug 3 — 50 MB docx rejected, silently
|
|
257
|
+
|
|
258
|
+
**Symptom:** a large docx sent to the greece space was never readable; Google Drive worked.
|
|
259
|
+
|
|
260
|
+
**Root cause.** `mercury.yaml:74` sets `max_size_mb: 10`. `adapters/whatsapp-media.ts:165`
|
|
261
|
+
(and `:284` for the quoted-reply retry) skips anything larger **before** downloading.
|
|
262
|
+
|
|
263
|
+
**Evidence (host journal, 2026-08-09):**
|
|
264
|
+
```
|
|
265
|
+
15:43:57 [WARN] Skipping large media file type=document sizeBytes=51585024 maxBytes=10485760
|
|
266
|
+
15:51:53 [WARN] Skipping large media file type=document sizeBytes=58117658 maxBytes=10485760
|
|
267
|
+
```
|
|
268
|
+
Seven attempts, all rejected — including the reply path, which is why replying to the file
|
|
269
|
+
didn't help.
|
|
270
|
+
|
|
271
|
+
**The worse half is the silence.** `runtime.ts` only surfaces "could not use your
|
|
272
|
+
attachment" when the message has **no text at all**. The file had a caption, so the agent
|
|
273
|
+
was told nothing and invented a wrong explanation ("WhatsApp isn't handing me that .docx"),
|
|
274
|
+
sending the user chasing a non-existent bug.
|
|
275
|
+
|
|
276
|
+
**Fix.**
|
|
277
|
+
1. Raise `media.max_size_mb` to **64**. (Verified `MERCURY_MEDIA_MAX_SIZE_MB` is unset, so
|
|
278
|
+
the YAML edit will take effect.)
|
|
279
|
+
2. **Always** inject a system note when an attachment is dropped and why, regardless of
|
|
280
|
+
whether text was present. `src/core/media-gate.ts` already has exactly this pattern
|
|
281
|
+
(`promptNote`) — copy it.
|
|
282
|
+
3. Optional: for oversize *documents*, download → extract text → discard the binary, so
|
|
283
|
+
huge files stay readable without being stored.
|
|
284
|
+
|
|
285
|
+
**Tests.**
|
|
286
|
+
- Unit: an over-limit attachment with a caption still produces a `promptNote`.
|
|
287
|
+
- Unit: note text distinguishes "too large" from "media disabled" from "download failed".
|
|
288
|
+
- Manual: send a ~50 MB docx → it downloads and is readable. Send a ~200 MB file → the bot
|
|
289
|
+
says *why* it can't read it.
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
### Bug 4 — Cannot create a document
|
|
294
|
+
|
|
295
|
+
**Symptom:** asked to make a doc, the bot attached HTML + markdown and told the user to
|
|
296
|
+
convert manually.
|
|
297
|
+
|
|
298
|
+
**Root cause.** No document-writing capability exists. Installed extensions are
|
|
299
|
+
`napkin, pdf, voice-transcribe, web-browser`. The container (`container/Dockerfile`) has
|
|
300
|
+
python3 + pip, node and bun, but **no `python-docx` and no `pandoc`**.
|
|
301
|
+
|
|
302
|
+
Egress already works — the HTML attachment proves it. The only missing piece is
|
|
303
|
+
*producing* a `.docx`.
|
|
304
|
+
|
|
305
|
+
**Fix.** A `docs` extension modeled directly on the `pdf` one (which is just `index.ts` +
|
|
306
|
+
a skill dir; its `install` string is compiled into the derived image by
|
|
307
|
+
`extensions/image-builder.ts`, which groups apt/pip/npm installs):
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
mercury.cli({
|
|
311
|
+
name: "docs",
|
|
312
|
+
install: "apt-get update && apt-get install -y --no-install-recommends pandoc && "
|
|
313
|
+
+ "python3 -m pip install --break-system-packages python-docx openpyxl odfpy && "
|
|
314
|
+
+ "rm -rf /var/lib/apt/lists/*",
|
|
315
|
+
});
|
|
316
|
+
mercury.permission({ defaultRoles: ["admin", "member"] });
|
|
317
|
+
mercury.skill("./skill");
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Plus a `SKILL.md` teaching it to build the `.docx` and drop it in `outbox/`. Real Word
|
|
321
|
+
files, no Google account. *(Actual Google Docs **links** would be a separate Workspace
|
|
322
|
+
extension + OAuth — out of scope.)*
|
|
323
|
+
|
|
324
|
+
**Tests.**
|
|
325
|
+
- Image build succeeds and `python3 -c "import docx"` works in the derived image.
|
|
326
|
+
- Manual: "make me a doc with X" → a `.docx` arrives in WhatsApp and opens in Word/Docs.
|
|
327
|
+
- Manual: verify a member (not just admin) can trigger it.
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
### Bug 5 — Slash commands imply cross-space reach they don't have
|
|
332
|
+
|
|
333
|
+
**Symptom:** commands look like they can affect other spaces, e.g. `spaces delete <id>`.
|
|
334
|
+
|
|
335
|
+
**Root cause — the two surfaces disagree, and the dangerous one silently ignores its argument.**
|
|
336
|
+
|
|
337
|
+
| Surface | Behaviour |
|
|
338
|
+
|---|---|
|
|
339
|
+
| Chat `/spaces delete <id>` (`runtime.ts:911-933`) | **Honors the id.** Validates the target, stores `pending_delete_id`, deletes that space on confirm. Correct. |
|
|
340
|
+
| `mrctl spaces delete <id>` (`cli/mrctl.ts:384`) | **Silently ignores the argument** — always `DELETE /api/spaces/current`. Deletes the space you're in. |
|
|
341
|
+
|
|
342
|
+
`cli/mrctl.ts:384` is literally `print(await api("DELETE", "/api/spaces/current"))`; `args[2]`
|
|
343
|
+
is never read. It is the **only** mrctl subcommand that ignores an argument — every other
|
|
344
|
+
one reads `args[2]`. The usage line at `cli/mrctl.ts:85` (`mrctl spaces list|name|delete`)
|
|
345
|
+
shows no args at all, so an agent that has watched users type `/spaces delete <id>` will
|
|
346
|
+
naturally type the same thing at `mrctl` and destroy the current space.
|
|
347
|
+
|
|
348
|
+
The **API itself is correctly designed** — `core/routes/spaces.ts` deliberately exposes only
|
|
349
|
+
`/current` and has no `/:id` route, scoping every mutation to the caller's own space via
|
|
350
|
+
`getAuth(c).spaceId`. Do **not** "fix" this by adding a `/:id` route; that would remove a
|
|
351
|
+
real security boundary.
|
|
352
|
+
|
|
353
|
+
**Second, compounding bug — the confirmation can never match on a WhatsApp reply.**
|
|
354
|
+
`handlePendingSpaceDelete` (`runtime.ts:953`) tests `text === "yes"`, where text is
|
|
355
|
+
`message.text.trim().toLowerCase()`. But the WhatsApp adapter **appends a `<reply_to …>`
|
|
356
|
+
block** to the text (`adapters/whatsapp.ts:609`). The confirmation prompt literally says
|
|
357
|
+
"Reply *yes* to confirm" — and replying makes the match fail. Worse, `runtime.ts:981-982`
|
|
358
|
+
clears the pending state **before** evaluating the match, so the correct target is silently
|
|
359
|
+
discarded and the message falls through to the LLM. Typing bare `yes` (not as a reply) is
|
|
360
|
+
currently the only working confirmation.
|
|
361
|
+
|
|
362
|
+
*(This composition destroyed the `baduk` space on 2026-08-07: 559 messages, 5 tasks,
|
|
363
|
+
14 roles. Recovered from the SQLite WAL.)*
|
|
364
|
+
|
|
365
|
+
**Fix.**
|
|
366
|
+
1. `cli/mrctl.ts:384` — **reject the argument loudly** rather than ignoring it:
|
|
367
|
+
```
|
|
368
|
+
Error: `mrctl spaces delete` only deletes the CURRENT space and takes no argument.
|
|
369
|
+
To delete a different space, use the chat command `/spaces delete <id>` as an admin.
|
|
370
|
+
```
|
|
371
|
+
2. `cli/mrctl.ts:85` — usage becomes `mrctl spaces list|name [<name>]|delete` with an
|
|
372
|
+
explicit "(current space only)" note.
|
|
373
|
+
3. `resources/skills/spaces/SKILL.md` — state plainly that `mrctl` is current-space-only
|
|
374
|
+
and that cross-space deletion is chat-admin-only. *(The skill is already honest; make it emphatic.)*
|
|
375
|
+
4. **Normalize confirmation text before matching.** Strip the `<reply_to …>…</reply_to>`
|
|
376
|
+
block (and any appended context) before the `yes`/`no` comparison. Best done as a shared
|
|
377
|
+
helper so every future confirmation flow benefits.
|
|
378
|
+
5. **Reorder `handlePendingSpaceDelete`:** only clear pending state on an actual match,
|
|
379
|
+
an explicit `no`, or expiry — never on an unrecognized message. A stray message must
|
|
380
|
+
not silently cancel a pending delete.
|
|
381
|
+
6. Consider requiring the confirmation to echo the target id (`yes cmp-opus`) for
|
|
382
|
+
cross-space deletes.
|
|
383
|
+
|
|
384
|
+
**Tests.**
|
|
385
|
+
- Unit: `mrctl spaces delete foo` exits non-zero with the explanatory error and issues **no**
|
|
386
|
+
HTTP request.
|
|
387
|
+
- Unit: `mrctl spaces delete` (no arg) still works.
|
|
388
|
+
- Unit: confirmation matches `yes` when a `<reply_to>` block is appended (**this is the
|
|
389
|
+
regression test for the baduk incident**).
|
|
390
|
+
- Unit: an unrelated message does **not** clear pending state; `no` and expiry do.
|
|
391
|
+
- Unit: deleting space A from space B deletes A, not B.
|
|
392
|
+
- Manual: in a throwaway space, `/spaces delete <other-id>` → **reply** "yes" → verify the
|
|
393
|
+
named space died and the current one survived.
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
### Bug 6 — Command-surface audit: five more defects of the same family
|
|
398
|
+
|
|
399
|
+
A full audit of every `/` command found that `spaces delete` was not an isolated slip.
|
|
400
|
+
Ordered by severity.
|
|
401
|
+
|
|
402
|
+
#### 6a. `/stop`, `/compact`, `/clear` silently do nothing (HIGH)
|
|
403
|
+
|
|
404
|
+
`SLASH_COMMANDS` (`core/commands.ts:15-67`) contains only `help, model, spaces, pause,
|
|
405
|
+
resume`. `stop/compact/clear` live in a **separate** `CHAT_COMMANDS` map
|
|
406
|
+
(`core/router.ts:39-43`) which is matched against the *whole prompt*:
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
const cmdWord = prompt.toLowerCase().trim();
|
|
410
|
+
if (cmdWord in CHAT_COMMANDS) { … }
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
`stop` matches. **`/stop` does not** — `cmdWord` is `"/stop"`, slash included. It fails the
|
|
414
|
+
slash-command check too (not in `SLASH_COMMANDS`), falls through both, and is sent to the
|
|
415
|
+
model as an ordinary prompt.
|
|
416
|
+
|
|
417
|
+
So typing `/stop` on a runaway agent **queues another model run instead of aborting one** —
|
|
418
|
+
the exact opposite of the intent, on the emergency-brake command. `/help` lists these under
|
|
419
|
+
*"Bare commands (no slash needed)"*, which reads as "slash optional", not "slash breaks it".
|
|
420
|
+
|
|
421
|
+
**Fix:** strip a leading `/` before the `CHAT_COMMANDS` lookup (or register them as leaf
|
|
422
|
+
entries in `SLASH_COMMANDS`), and reword the help line to say the slash form works.
|
|
423
|
+
|
|
424
|
+
#### 6b. Slash commands in groups silently require the trigger word (HIGH)
|
|
425
|
+
|
|
426
|
+
`routeInput` runs `matchTrigger` at `router.ts:105-106` and returns `ignore` **before** the
|
|
427
|
+
slash parse at `:115`. In a group, a bare `/spaces list` is therefore ignored outright — you
|
|
428
|
+
must write `Wiz /spaces list`.
|
|
429
|
+
|
|
430
|
+
Meanwhile `handler.ts:236-240` has an `isCommand = text.startsWith("/")` bypass that lets
|
|
431
|
+
`/`-prefixed messages skip the trigger gate — but `routeInput` re-applies it and drops them.
|
|
432
|
+
**The bypass accomplishes nothing** except routing the command into ambient storage.
|
|
433
|
+
|
|
434
|
+
**Confirmed by live data.** The only two ambient rows in the entire DB are
|
|
435
|
+
`סהר ויצמן: /spaces` and `סהר ויצמן: /spaces delete cmp-opus` — both typed untagged in the
|
|
436
|
+
baduk group, both silently ignored and filed as chat instead of executed.
|
|
437
|
+
|
|
438
|
+
> **This corrects the incident account in Bug 5.** Those chat commands never ran. The
|
|
439
|
+
> baduk deletion must have come from the agent invoking `mrctl spaces delete <id>`, which
|
|
440
|
+
> ignores its argument and destroys the current space. **That makes the `mrctl` fix (5.1)
|
|
441
|
+
> the load-bearing one**, not the confirmation fix — prioritize accordingly.
|
|
442
|
+
|
|
443
|
+
**Fix:** honor slash commands without the trigger in groups (matching the handler's evident
|
|
444
|
+
intent), gated so an unknown `/foo` still falls through to ambient rather than becoming a
|
|
445
|
+
command. Otherwise delete the dead bypass and document the requirement.
|
|
446
|
+
|
|
447
|
+
#### 6c. `/spaces unlink` is an unrecoverable self-lockout in groups (HIGH)
|
|
448
|
+
|
|
449
|
+
`resolveConversation` (`core/conversation.ts:127`) ends with:
|
|
450
|
+
|
|
451
|
+
```ts
|
|
452
|
+
if (!autoSpace?.enabled || kind !== "dm") return null;
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
For a **group**, once unlinked, resolution returns `null`, and `handler.ts:190-197` drops
|
|
456
|
+
every message — **including slash commands**, because that gate sits upstream of
|
|
457
|
+
`routeInput` entirely.
|
|
458
|
+
|
|
459
|
+
The command's own success text says *"link it to a space with `/spaces switch`"*. That is
|
|
460
|
+
**impossible**: the message will never be routed. Recovery requires the dashboard, the API,
|
|
461
|
+
or a direct DB edit. DMs self-heal via auto-space; groups do not.
|
|
462
|
+
|
|
463
|
+
**Same failure mode reachable without any command:** `onGroupRemoval` (`main.ts`) unlinks
|
|
464
|
+
the conversation when the bot is removed from a group. Re-adding the bot leaves it
|
|
465
|
+
**permanently deaf** — the conversation is still unlinked and every message is dropped.
|
|
466
|
+
|
|
467
|
+
**Fix:** keep an unlinked conversation eligible for *link-restoring commands only*
|
|
468
|
+
(`/spaces switch`, `/spaces list`) instead of dropping everything; correct the message text;
|
|
469
|
+
and re-link (or prompt to re-link) on group re-join.
|
|
470
|
+
|
|
471
|
+
#### 6d. Chat and CLI permission gates disagree (MEDIUM, security-relevant)
|
|
472
|
+
|
|
473
|
+
| Path | Gate |
|
|
474
|
+
|---|---|
|
|
475
|
+
| Chat `/spaces …` | `SEEDED_ADMIN_COMMANDS` (`router.ts:33`) → **only the seeded admin from config** |
|
|
476
|
+
| `mrctl spaces …` / API | `checkPerm(c, "spaces.delete")` → and `getDefaultPermissions` (`permissions.ts:208-210`) grants **every space `admin` all permissions** |
|
|
477
|
+
|
|
478
|
+
A promoted space admin is denied `/spaces delete` in chat, but the agent acting on their
|
|
479
|
+
behalf can call `mrctl spaces delete` and destroy the current space. The tighter chat gate
|
|
480
|
+
is bypassable via the CLI.
|
|
481
|
+
|
|
482
|
+
Members are unaffected — `DEFAULT_MEMBER_PERMISSIONS` (`permissions.ts:192-197`) is only
|
|
483
|
+
`prompt, prefs.get, media.receive, media.send`.
|
|
484
|
+
|
|
485
|
+
**Fix:** align the two deliberately — either loosen chat to `admin`, or put
|
|
486
|
+
`spaces.delete` behind a seeded-admin-only tier on the API side too. Decide which; don't
|
|
487
|
+
leave them divergent.
|
|
488
|
+
|
|
489
|
+
#### 6e. `/resume <arg>` ignores its argument; `/help pause` fails (LOW)
|
|
490
|
+
|
|
491
|
+
- `executeResumeCommand(spaceId, _callerId)` never receives `verb`, so `/resume now` and
|
|
492
|
+
`/resume 5m` silently behave as bare `/resume`. Same class as `spaces delete`, but with no
|
|
493
|
+
destructive effect. Reject unknown args for consistency.
|
|
494
|
+
- The `pause` entry (`commands.ts:59-62`) has no `verbs` array, so `formatCategoryHelp`
|
|
495
|
+
returns null and **`/help pause` answers "No help available for '/pause'."** — even though
|
|
496
|
+
`/pause` takes a duration. `parsePauseDuration` accepts only `^(\d+)(m|h)$` (no days, no
|
|
497
|
+
seconds), which is documented nowhere.
|
|
498
|
+
|
|
499
|
+
#### Redundancy assessment
|
|
500
|
+
|
|
501
|
+
| Command | Verdict |
|
|
502
|
+
|---|---|
|
|
503
|
+
| `/model active` | **Redundant.** `/model list` already marks the active leg with `← active`. Two commands, one fact. Drop it. |
|
|
504
|
+
| `/compact` vs `/clear` | **Near-identical and undifferentiated.** Both take `MAX(id)` for the space and write a boundary (`db.ts:913` vs `:936`); one sets `session_boundary`, the other `clear_boundary`. Help text ("reset the session context window" vs "clear context for this message") does not let a chat user choose correctly. Merge, or rewrite both descriptions to state the actual difference. |
|
|
505
|
+
| `/help <cat>` vs `/<cat>` | Duplicate paths into `formatCategoryHelp`. Harmless — keep. |
|
|
506
|
+
| `/spaces unlink` | **Recommend removing from chat entirely** (see 6c). It is a one-way door whose only advertised recovery path cannot work. |
|
|
507
|
+
| Everything else | Earns its place. |
|
|
508
|
+
|
|
509
|
+
**Tests for Bug 6.**
|
|
510
|
+
- Unit: `/stop`, `/compact`, `/clear` route to `type: "command"`; assert they do **not**
|
|
511
|
+
reach the assistant path (regression for 6a).
|
|
512
|
+
- Unit: a bare `/spaces list` in a group resolves as a command, not ambient (6b).
|
|
513
|
+
- Unit: an unknown `/foo` in a group still falls through to ambient, not a command (6b).
|
|
514
|
+
- Unit: after `unlinkConversation`, `/spaces switch <id>` is still routed and re-links;
|
|
515
|
+
an ordinary message is still dropped (6c).
|
|
516
|
+
- Unit: a space `admin` (not seeded) gets the same answer from the chat and CLI paths (6d).
|
|
517
|
+
- Unit: `/resume extra` errors rather than silently succeeding; `/help pause` returns real
|
|
518
|
+
help (6e).
|
|
519
|
+
- Manual: in a throwaway group, unlink then re-link entirely from chat.
|
|
520
|
+
|
|
521
|
+
---
|
|
522
|
+
|
|
523
|
+
## 5. When the fixes are done
|
|
524
|
+
|
|
525
|
+
1. **Gate:** `cd ~/mercury-src && bun run check` (typecheck + lint + tests) must be green.
|
|
526
|
+
2. **Deploy:** section 3 switchover, then `systemctl --user restart mercury`.
|
|
527
|
+
3. **Verify with debug logging on.** Set `log_level: debug` in `mercury.yaml` for the test
|
|
528
|
+
window. The lines that matter only exist at debug level:
|
|
529
|
+
- `Incoming message` (`handler.ts:151`) — proves untriggered messages arrive
|
|
530
|
+
- `Message ignored: conversation not linked to a space` (`handler.ts:191`)
|
|
531
|
+
- `WhatsApp identity canonicalized` (`whatsapp.ts`) — bug 2
|
|
532
|
+
- `WhatsApp skipping backlog message` — rules out the reconnect filter
|
|
533
|
+
|
|
534
|
+
⚠️ **Turn debug back off afterwards.** It logs message-content previews into journald —
|
|
535
|
+
real chat content from a family/friends group sitting in system logs.
|
|
536
|
+
4. **Confirm with data, not vibes:**
|
|
537
|
+
```sql
|
|
538
|
+
SELECT space_id, role, COUNT(*) FROM messages GROUP BY space_id, role;
|
|
539
|
+
```
|
|
540
|
+
Ambient rows for `greece2026` must be > 0 and growing.
|
|
541
|
+
5. **Housekeeping:** delete the stray `~/.mercury/state.db` (147 KB, created by running a
|
|
542
|
+
mercury command from `$HOME` instead of the project dir — the cwd-scoping trap that makes
|
|
543
|
+
`auth status` misreport). Verify it isn't the live DB first.
|
|
544
|
+
6. **PR:** one PR from `fix/whatsapp-context-identity-and-docs` → `main`, with a section per
|
|
545
|
+
change. Must call out:
|
|
546
|
+
- the four pinchtab commits as **upstreaming an already-live hotfix**, with the
|
|
547
|
+
`allowedDomains "*"` security tradeoff stated plainly
|
|
548
|
+
- the `.dockerignore` build fix (exists in no commit anywhere)
|
|
549
|
+
- the `messages` schema migration and its backfill behaviour
|
|
550
|
+
- the ambient volume/retention implications
|
|
551
|
+
- the baduk incident as motivation for the bug-5 regression tests
|
|
552
|
+
- **behaviour changes in the command surface** (bug 6): `/stop` now aborts instead of
|
|
553
|
+
prompting, slash commands work untagged in groups, `/spaces unlink` removed or made
|
|
554
|
+
recoverable, `/model active` dropped. These change what existing users' muscle memory
|
|
555
|
+
does — list them explicitly rather than burying them in the diff.
|
|
556
|
+
7. **Also decide before pushing:** the 4 pinchtab commits still live only on the D: clone's
|
|
557
|
+
`fix/pinchtab-browser-startup` branch. Either push that branch too or accept that the
|
|
558
|
+
cherry-picks supersede it.
|
|
559
|
+
|
|
560
|
+
---
|
|
561
|
+
|
|
562
|
+
## 6. Open decisions
|
|
563
|
+
|
|
564
|
+
| Decision | Recommendation |
|
|
565
|
+
|---|---|
|
|
566
|
+
| Media cap size | 64 MB |
|
|
567
|
+
| Member notes: workspace files vs DB table | Workspace files (`knowledge/members/<id>.md`) — reuses existing plumbing, no migration |
|
|
568
|
+
| Tighten `allowedDomains` from `"*"` | Worth doing eventually; out of scope for this PR, but say so in the PR |
|
|
569
|
+
| Require echoing target id on cross-space delete | Recommended |
|
|
570
|
+
| Slash commands without the trigger word in groups (6b) | **Allow** — matches the handler's evident intent; keep unknown `/foo` falling through to ambient |
|
|
571
|
+
| Align chat vs CLI permission gates (6d) | Pick one deliberately. Leaning: keep `/spaces` seeded-admin-only and lift `spaces.delete` out of the blanket `admin` grant |
|
|
572
|
+
| Drop `/model active` (redundant) | Yes |
|
|
573
|
+
| Merge `/compact` and `/clear` | Don't merge blind — first document the real difference, then decide |
|
|
574
|
+
| Remove `/spaces unlink` from chat | Recommended — one-way door with an impossible recovery path |
|
|
575
|
+
|
|
576
|
+
## 7. Suggested order
|
|
577
|
+
|
|
578
|
+
1. **Bugs 1 + 2 together** — same code path, and ambient without attribution produces a wall
|
|
579
|
+
of anonymous text that could make misidentification *worse*. Bundle the ambient fix, the
|
|
580
|
+
author columns, and the context cap.
|
|
581
|
+
2. **Bug 5.1 (`mrctl spaces delete` argument) + 6a + 6c** — the three that can destroy data
|
|
582
|
+
or lock you out. 5.1 is the one that actually killed baduk (see 6b); 6a means the
|
|
583
|
+
emergency brake doesn't work; 6c is unrecoverable without DB access. All are small.
|
|
584
|
+
3. **Rest of bug 5 + 6b, 6d** — confirmation matching, trigger-gating, permission alignment.
|
|
585
|
+
4. **Bug 3** — config + prompt-note plumbing.
|
|
586
|
+
5. **Bug 4** — new extension, no core changes.
|
|
587
|
+
6. **6e + redundancy cleanup** — cosmetic; fold into the PR last.
|
|
588
|
+
7. **Member memory** — mostly prompt + skill work once attribution lands.
|