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.
Files changed (80) hide show
  1. package/README.md +6 -0
  2. package/container/Dockerfile +6 -1
  3. package/container/Dockerfile.base +6 -1
  4. package/docs/README.md +50 -0
  5. package/docs/authoring-profiles.md +8 -0
  6. package/docs/behavior-layers.md +193 -0
  7. package/docs/extensions.md +2 -0
  8. package/docs/goals/football-reporter-profile/decisions.md +479 -0
  9. package/docs/goals/football-reporter-profile/goal.md +91 -0
  10. package/docs/goals/football-reporter-profile/roadmap.md +196 -0
  11. package/docs/goals/whatsapp-bot-hardening/README.md +43 -0
  12. package/docs/goals/whatsapp-bot-hardening/archive/ambient-group-context.md +211 -0
  13. package/docs/goals/whatsapp-bot-hardening/archive/command-routing-consistency.md +210 -0
  14. package/docs/goals/whatsapp-bot-hardening/archive/destructive-command-guards.md +218 -0
  15. package/docs/goals/whatsapp-bot-hardening/archive/docs-extension.md +182 -0
  16. package/docs/goals/whatsapp-bot-hardening/archive/handoff-2026-08-09.md +588 -0
  17. package/docs/goals/whatsapp-bot-hardening/archive/media-size-and-silent-drop.md +178 -0
  18. package/docs/goals/whatsapp-bot-hardening/archive/member-memory.md +199 -0
  19. package/docs/goals/whatsapp-bot-hardening/archive/message-author-attribution.md +211 -0
  20. package/docs/goals/whatsapp-bot-hardening/archive/run-from-source-switchover.md +212 -0
  21. package/docs/goals/whatsapp-bot-hardening/archive/setup-plan-2026-08-06.md +1117 -0
  22. package/docs/goals/whatsapp-bot-hardening/decisions.md +200 -0
  23. package/docs/goals/whatsapp-bot-hardening/goal.md +75 -0
  24. package/docs/goals/whatsapp-bot-hardening/open-threads.md +396 -0
  25. package/docs/goals/whatsapp-bot-hardening/roadmap.md +245 -0
  26. package/docs/memory.md +30 -0
  27. package/docs/profile-guide.md +483 -0
  28. package/docs/refactor/.gitkeep +0 -0
  29. package/docs/refactor/archive/.gitkeep +0 -0
  30. package/docs/refactor/audits/.gitkeep +0 -0
  31. package/docs/refactor/backlog/.gitkeep +0 -0
  32. package/docs/skills-guide.md +154 -0
  33. package/examples/extensions/README.md +1 -0
  34. package/examples/extensions/feed-watch/config.ts +354 -0
  35. package/examples/extensions/feed-watch/digest.ts +344 -0
  36. package/examples/extensions/feed-watch/feeds.ts +341 -0
  37. package/examples/extensions/feed-watch/index.ts +302 -0
  38. package/examples/extensions/feed-watch/items.ts +171 -0
  39. package/examples/extensions/feed-watch/match.ts +107 -0
  40. package/examples/extensions/feed-watch/prompts/verify.md +35 -0
  41. package/examples/extensions/feed-watch/skill/SKILL.md +128 -0
  42. package/examples/extensions/feed-watch/watch.ts +548 -0
  43. package/examples/extensions/longview/hook.ts +179 -16
  44. package/examples/extensions/longview/index.ts +13 -2
  45. package/examples/extensions/longview/prompts/summarize.md +3 -2
  46. package/examples/extensions/longview/render/telegraph-nodes.ts +21 -1
  47. package/examples/extensions/longview/summarize.ts +57 -14
  48. package/examples/extensions/napkin/index.ts +334 -86
  49. package/examples/extensions/napkin/pi-spawn.ts +196 -0
  50. package/examples/extensions/pinchtab/index.ts +37 -4
  51. package/examples/extensions/pinchtab/lib/session-injector.ts +31 -9
  52. package/examples/extensions/pinchtab/skill/SKILL.md +29 -0
  53. package/examples/profiles/_template/AGENTS.md +138 -0
  54. package/examples/profiles/_template/README.md +65 -0
  55. package/examples/profiles/_template/config.yaml +57 -0
  56. package/examples/profiles/_template/tasks/daily.md +32 -0
  57. package/examples/profiles/football-reporter/AGENTS.md +219 -0
  58. package/examples/profiles/football-reporter/README.md +138 -0
  59. package/examples/profiles/football-reporter/config.yaml +174 -0
  60. package/examples/profiles/football-reporter/seed/MEMORY.md +42 -0
  61. package/examples/profiles/football-reporter/seed/episodes/barcelona-2026-27.md +25 -0
  62. package/examples/profiles/football-reporter/seed/episodes/beitar-jerusalem-2026-27.md +25 -0
  63. package/examples/profiles/football-reporter/seed/episodes/maccabi-haifa-2026-27.md +24 -0
  64. package/examples/profiles/football-reporter/seed/episodes/man-united-2026-27.md +26 -0
  65. package/examples/profiles/football-reporter/seed/episodes/real-madrid-2026-27.md +25 -0
  66. package/examples/profiles/football-reporter/seed/napkin-distill.md +55 -0
  67. package/examples/profiles/football-reporter/tasks/daily-article.md +142 -0
  68. package/package.json +8 -5
  69. package/src/adapters/whatsapp.ts +75 -13
  70. package/src/bridges/whatsapp.ts +6 -4
  71. package/src/cli/mrctl.ts +9 -1
  72. package/src/core/handler.ts +51 -6
  73. package/src/core/routes/dashboard.ts +106 -5
  74. package/src/core/routes/tasks.ts +28 -0
  75. package/src/core/runtime.ts +63 -2
  76. package/src/core/task-scheduler.ts +103 -12
  77. package/src/extensions/catalog.ts +9 -0
  78. package/src/profile/space-profile.ts +780 -0
  79. package/src/storage/db.ts +215 -0
  80. package/src/types.ts +56 -0
@@ -0,0 +1,200 @@
1
+ # Decisions: Mercury WhatsApp Bot — Context, Identity & Command Hardening
2
+
3
+ **Goal**: [whatsapp-bot-hardening](goal.md)
4
+
5
+ ## D-001: Run the live bot from the git checkout, not the npm global install
6
+ - **Category:** architecture
7
+ - **Decided:** `npm link` from `~/mercury-src`, reinstall the systemd user unit
8
+ via `mercury service install --user`, keep the npm-installed 0.8.12 in place
9
+ as a <1-minute rollback (`npm unlink` + restore old unit).
10
+ - **Alternatives considered:** keep patching the globally installed package
11
+ (0.8.12) in place; publish a private build.
12
+ - **Reasoning:** fixes become version-controlled, testing is edit→restart, and
13
+ `npm i -g` upgrades can no longer silently revert hand-patches (which already
14
+ happened with the pinchtab fixes).
15
+ - **Revisit if:** the checkout diverges so far from upstream that rebasing costs
16
+ more than maintaining patches.
17
+ - **Used by:** run-from-source-switchover
18
+
19
+ ## D-002: Message authorship goes in the DB schema (`author_id` / `author_name` columns)
20
+ - **Category:** architecture
21
+ - **Decided:** migrate the `messages` table to carry author id + display name;
22
+ render `<user name="…" id="…">` in history XML; back the in-memory
23
+ `pushNames` map with `space_roles.display_name`.
24
+ - **Alternatives considered:** prefix names into the content string (as ambient
25
+ already does at `runtime.ts:604`); a separate authors join table.
26
+ - **Reasoning:** the identity data already exists clean in
27
+ `space_roles.display_name`; columns make it queryable and render-safe, and a
28
+ prefix-in-content hack would be unparseable later. Existing rows backfill as
29
+ NULL and still render.
30
+ - **Revisit if:** upstream lands its own attribution schema first — then adopt
31
+ theirs in the rebase.
32
+ - **Used by:** message-author-attribution, ambient-group-context, member-memory
33
+
34
+ ## D-003: Ambient context gets its own bounded query + retention TTL before it is enabled
35
+ - **Category:** trade-off
36
+ - **Decided:** `recordAmbient` ships only together with a dedicated bounded
37
+ ambient query (not sharing `getRecentTurns`' `turnCount * 5` window) and an
38
+ ambient TTL in `storage-cleanup.ts`.
39
+ - **Alternatives considered:** enable ambient first, tune the budget later.
40
+ - **Reasoning:** once ambient actually flows, every group message becomes a row;
41
+ unbounded it crowds real user/assistant turns out of the context window and
42
+ grows the DB forever. HANDOFF is explicit: do not ship the handler fix
43
+ without the cap.
44
+ - **Revisit if:** measured ambient volume in `greece2026` is trivially small.
45
+ - **Used by:** ambient-group-context
46
+
47
+ ## D-004: Media cap raised to 64 MB; every dropped attachment produces a promptNote
48
+ - **Category:** scope
49
+ - **Decided:** `media.max_size_mb: 64` in `mercury.yaml` (verified
50
+ `MERCURY_MEDIA_MAX_SIZE_MB` is unset so YAML takes effect), plus an
51
+ unconditional system note on any dropped attachment, copying the existing
52
+ `media-gate.ts` promptNote pattern.
53
+ - **Alternatives considered:** 32 MB; download-extract-discard for oversize
54
+ documents (deferred as optional follow-up).
55
+ - **Reasoning:** the observed real files were 49–56 MB; the silence was the
56
+ worse half of the bug — the agent invented a wrong explanation because it was
57
+ told nothing.
58
+ - **Revisit if:** disk or bandwidth pressure appears in practice.
59
+ - **Used by:** media-size-and-silent-drop
60
+
61
+ ## D-005: Document creation is a new `docs` extension modeled on the `pdf` extension
62
+ - **Category:** tech-stack
63
+ - **Decided:** extension with `install` string adding `pandoc` +
64
+ `python-docx openpyxl odfpy` to the derived image, a `SKILL.md` teaching
65
+ docx-to-`outbox/`, `defaultRoles: ["admin", "member"]`.
66
+ - **Alternatives considered:** Google Docs/Workspace integration (real links).
67
+ - **Reasoning:** egress already works (HTML attachments prove it); only
68
+ production of `.docx` is missing. Workspace OAuth is a separate project.
69
+ - **Revisit if:** users specifically need collaborative Google Docs links.
70
+ - **Used by:** docs-extension
71
+
72
+ ## D-006: `mrctl spaces delete` rejects arguments loudly; the API keeps `/current` only
73
+ - **Category:** architecture
74
+ - **Decided:** error out when `mrctl spaces delete` receives an argument,
75
+ pointing to the chat command `/spaces delete <id>`; do **not** add a
76
+ `DELETE /api/spaces/:id` route.
77
+ - **Alternatives considered:** make mrctl honor the id via a new API route.
78
+ - **Reasoning:** the `/current`-only API is a deliberate security boundary
79
+ (`core/routes/spaces.ts` scopes every mutation to the caller's space).
80
+ Silently ignoring the argument is what actually destroyed the baduk space.
81
+ - **Revisit if:** never — widening this API is out of scope by design.
82
+ - **Used by:** destructive-command-guards
83
+
84
+ ## D-007: Slash commands work without the trigger word in groups
85
+ - **Category:** pattern
86
+ - **Decided:** honor `/`-prefixed known commands in groups without the trigger
87
+ word; unknown `/foo` still falls through to ambient.
88
+ - **Alternatives considered:** delete the dead `isCommand` bypass and document
89
+ that commands require the trigger.
90
+ - **Reasoning:** matches the handler's evident intent (the bypass exists, it
91
+ just accomplishes nothing today), and matches what users already type — the
92
+ two live ambient rows are exactly such stranded commands.
93
+ - **Revisit if:** command-name collisions with normal chat starting with `/`.
94
+ - **Used by:** command-routing-consistency
95
+
96
+ ## D-008: Chat stays seeded-admin-only for `/spaces`; `spaces.delete` leaves the blanket admin grant
97
+ - **Category:** trade-off
98
+ - **Decided:** align the divergent gates by tightening the CLI/API side — lift
99
+ `spaces.delete` out of `getDefaultPermissions`' blanket space-admin grant so
100
+ it matches the chat surface's seeded-admin gate.
101
+ - **Alternatives considered:** loosen chat to any space admin.
102
+ - **Reasoning:** the tighter gate is the intended one; today a promoted space
103
+ admin who is denied in chat can destroy the current space via `mrctl` — a
104
+ bypass, not a feature.
105
+ - **Revisit if:** multi-admin spaces become a real use case.
106
+ - **Used by:** command-routing-consistency
107
+
108
+ ## D-009: `/spaces unlink` is removed from chat; `/model active` is dropped; `/compact` vs `/clear` get honest help text, not a merge
109
+ - **Category:** scope
110
+ - **Decided:** remove the unrecoverable one-way door (6c) from the chat surface
111
+ entirely (dashboard/API keep the capability); drop the redundant
112
+ `/model active`; keep both `/compact` and `/clear` but rewrite their help to
113
+ state the actual `session_boundary` vs `clear_boundary` difference.
114
+ - **Alternatives considered:** make unlink recoverable in chat; merge
115
+ compact/clear blind.
116
+ - **Reasoning:** unlink's advertised recovery path (`/spaces switch`) is
117
+ provably unreachable after unlinking; re-link-on-rejoin still ships so the
118
+ group-removal path self-heals. Merging compact/clear without first
119
+ documenting the real difference risks destroying a distinction users need.
120
+ - **Revisit if:** a safe recovery UX for unlink is designed later.
121
+ - **Used by:** command-routing-consistency, destructive-command-guards
122
+
123
+ ## D-010: Member memory = hard identity from DB + soft notes as workspace files
124
+ - **Category:** architecture
125
+ - **Decided:** a `<members>` block generated from `space_roles` (id → name,
126
+ not guessable) plus per-space `knowledge/members/<id>.md` notes written by
127
+ the agent with its existing `write` tool, injected like `<active_episodes>`.
128
+ - **Alternatives considered:** a new DB table for member profiles.
129
+ - **Reasoning:** reuses existing episodic plumbing, needs no migration for the
130
+ notes half, and keeps identity facts (who is who) separate from accumulated
131
+ observations (what they're like).
132
+ - **Revisit if:** notes need querying/aggregation across spaces.
133
+ - **Used by:** member-memory
134
+
135
+ ## D-011: `allowedDomains "*"` stays for this PR, called out as a security trade-off
136
+ - **Category:** trade-off
137
+ - **Decided:** keep the wildcard pinchtab allowlist (the injection guard is
138
+ verified still active); the PR describes it as a deliberate widening of
139
+ attack surface, not a plain bugfix. Tightening to a curated list is deferred.
140
+ - **Alternatives considered:** curated domain list now.
141
+ - **Reasoning:** out of scope for this PR; honesty in the PR description is the
142
+ cheap mitigation available now.
143
+ - **Revisit if:** always — this is queued as a follow-up idea.
144
+ - **Used by:** (PR prep — see roadmap Completion section)
145
+
146
+ ## D-012: One PR at the end, not a PR per story
147
+ - **Category:** scope
148
+ - **Decided:** single PR `fix/whatsapp-context-identity-and-docs` → `main`
149
+ with a section per change, including the four already-merged pinchtab
150
+ commits (framed as upstreaming a live hotfix), the uncommitted
151
+ `.dockerignore` fix, the schema migration, and the explicit list of
152
+ command-surface behaviour changes.
153
+ - **Alternatives considered:** stacked PRs per milestone.
154
+ - **Reasoning:** the branch already carries the pinchtab commits; the fixes are
155
+ interdependent (attribution ↔ ambient); and the repo has a single upstream
156
+ reviewer. The per-section PR body keeps it reviewable.
157
+ - **Revisit if:** upstream asks for it to be split.
158
+ - **Used by:** all stories
159
+
160
+ ## D-013: The D:-clone's `fix/pinchtab-browser-startup` branch is superseded, not pushed
161
+ - **Category:** scope
162
+ - **Decided:** leave the branch in the stale Windows clone
163
+ (`D:\Projects\whatsapp-bot\mercury`) untouched and unpushed; the four pinchtab
164
+ commits reach `main` through `fix/whatsapp-context-identity-and-docs` instead.
165
+ - **Alternatives considered:** push it as its own PR and merge it before the
166
+ hardening PR; delete the branch.
167
+ - **Reasoning:** verified equivalent, not assumed — the combined patch of the
168
+ four commits is byte-identical in both repos (md5 `83aa486e…` over the
169
+ index-line-stripped diff, same three files, 56 insertions / 12 deletions).
170
+ The clone's copy (`3c5d56f..17c92c6`) sits on a base three commits behind
171
+ `origin/main`; the `mercury-src` copy (`bfe5a33..b12d7c0`) is current with
172
+ `origin/main` at `70a5ce3`. Pushing both would put the same change on `main`
173
+ twice. The branch is left in place rather than deleted because it costs
174
+ nothing and is the only copy of those commit hashes.
175
+ - **Revisit if:** the hardening PR is rejected or split, in which case the
176
+ pinchtab commits should be re-proposed on their own from `mercury-src`.
177
+ - **Used by:** (PR prep — see roadmap Completion section)
178
+
179
+ ## D-014: One instruction, one layer — never state the same rule twice
180
+ - **Category:** pattern
181
+ - **Decided:** each voice/behaviour rule lives in exactly **one** of the five
182
+ prompt layers, chosen by scope: global default → `project_config.character`;
183
+ per-space override → `space_preferences`; per-space long-form structure
184
+ (role, workflow, honesty rules) → `.mercury/spaces/<id>/AGENTS.md`.
185
+ `space_config.system_prompt` is treated as unavailable (no setter exists).
186
+ Quantities are written as **countable** rules, never adjectives.
187
+ - **Alternatives considered:** rely on layer precedence to override — e.g. put
188
+ the permissive default in the character and a stricter rule in preferences,
189
+ expecting the more specific layer to win.
190
+ - **Reasoning:** measured, not assumed. On the throwaway `prompttest` space a
191
+ direct `system_prompt`-vs-`preferences` conflict went 8/10 to `system_prompt`
192
+ — but a swapped-language control produced the **opposite** winner, and
193
+ repeating one fixed config gave different winners across runs. Precedence is
194
+ not reliable, so a conflict is a coin flip and must be designed out rather
195
+ than resolved by placement. The countable-rule half is from the emoji data:
196
+ "none at all" → 0%, "minimal" → 33%, "here and there when it fits" → 73%.
197
+ The layer decides *scope*; the wording decides *compliance*.
198
+ - **Revisit if:** upstream introduces explicit, documented precedence between
199
+ the layers — then placement becomes a usable tool again.
200
+ - **Used by:** (prompt/voice work — see open-threads.md §A)
@@ -0,0 +1,75 @@
1
+ # Mercury WhatsApp Bot — Context, Identity & Command Hardening
2
+
3
+ **Status**: Active — all 4 milestones shipped; open items in [open-threads.md](open-threads.md)
4
+ **Slug**: whatsapp-bot-hardening
5
+ **Created**: 2026-08-09
6
+ **Last updated**: 2026-08-10
7
+
8
+ ---
9
+
10
+ ## Goal Statement
11
+
12
+ Fix the six verified defect families documented in the handoff
13
+ (`docs/goals/whatsapp-bot-hardening/archive/handoff-2026-08-09.md`) — ambient group
14
+ context dead code, sender misidentification, silent large-media rejection, no
15
+ document creation, destructive/broken command surface) and add member memory —
16
+ then land everything as one upstream PR from
17
+ `fix/whatsapp-context-identity-and-docs`, with the live bot running the fixed
18
+ code from the git checkout.
19
+
20
+ ## Constraints
21
+
22
+ - All code work happens in **`~/mercury-src` inside WSL** (branch
23
+ `fix/whatsapp-context-identity-and-docs`, based on v0.9.0). The Windows clone
24
+ at `D:\Projects\whatsapp-bot\mercury` is stale reference only — never edit it.
25
+ - The bot is **live** for real family/friends groups. Data-loss bugs (the baduk
26
+ incident) already happened once; every migration must be preceded by a DB
27
+ backup, and destructive-command fixes ship before anything that increases
28
+ message volume is enabled for long.
29
+ - Never read `~/whatsapp-bot/.env` (live API keys). Never run `mercury run`
30
+ directly; restart only via `systemctl --user restart mercury`.
31
+ - Check gate for every story: `cd ~/mercury-src && bun run check`
32
+ (typecheck + lint + tests), run inside WSL with the PATH fix
33
+ (`export PATH=$HOME/.nvm/versions/node/v24.19.0/bin:$HOME/.bun/bin:$PATH`).
34
+ - Debug logging (`log_level: debug`) may be enabled for verification windows
35
+ only and **must** be reverted — it writes chat-content previews to journald.
36
+
37
+ ## Preferences
38
+
39
+ - Follow the fix designs already verified in the archived handoff §4 (required) —
40
+ root causes were confirmed against running code and live data; do not re-derive them.
41
+ - Reuse existing plumbing over new subsystems (required) — e.g. member notes as
42
+ workspace files like episodes, promptNote pattern from `media-gate.ts`.
43
+ - One PR at the end covering all changes, with explicit sections per change and
44
+ the behaviour-change list for the command surface (required).
45
+ - Suggested implementation order from the archived handoff §7 (preferred).
46
+
47
+ ## Target Users
48
+
49
+ - **The owner/admin (Sahar)** — runs the bot, admins the spaces, is the one who
50
+ typed the command that destroyed the baduk space.
51
+ - **Family/friends group members** — talk in groups the bot is linked to; they
52
+ expect the bot to know who said what and to read files they send.
53
+ - **Future upstream users of Mercury** — the PR upstreams fixes for bugs that
54
+ exist verbatim on `origin/main`.
55
+
56
+ ## Success Criteria
57
+
58
+ - Untagged group messages are recorded: `SELECT space_id, role, COUNT(*) FROM
59
+ messages GROUP BY space_id, role` shows `ambient` rows for `greece2026`
60
+ growing, and the bot can answer "what was just said?" after being tagged.
61
+ - Two people talk in a group; a third asks "who said X?" and attribution is
62
+ correct — including quoted replies after a service restart.
63
+ - `mrctl spaces delete <id>` with an argument exits non-zero with an
64
+ explanatory error and issues **no** HTTP request; replying "yes" (as a
65
+ WhatsApp reply) to a `/spaces delete <id>` confirmation deletes the *named*
66
+ space, not the current one.
67
+ - `/stop` aborts a running agent instead of queueing another model run; a bare
68
+ `/spaces list` works untagged in a group; an unlinked group can re-link
69
+ itself from chat.
70
+ - A ~50 MB docx sent to a space is downloaded and readable; an over-limit file
71
+ produces an explicit "too large" note to the agent instead of silence.
72
+ - "Make me a doc" produces a real `.docx` delivered over WhatsApp.
73
+ - `bun run check` is green, the live service runs from `~/mercury-src`, and one
74
+ PR containing all changes (including the four pinchtab commits and the
75
+ `.dockerignore` fix) is open against `main`.