mercury-agent 0.16.3 → 0.17.0

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 (60) hide show
  1. package/README.md +12 -0
  2. package/container/Dockerfile +1 -0
  3. package/container/Dockerfile.base +1 -0
  4. package/docs/autopilot/config.yaml +43 -0
  5. package/docs/behavior-layers.md +3 -0
  6. package/docs/container-lifecycle.md +1 -0
  7. package/docs/extensions.md +43 -0
  8. package/docs/goals/football-reporter-profile/decisions.md +37 -1
  9. package/docs/goals/football-reporter-profile/roadmap.md +63 -5
  10. package/docs/permissions.md +15 -0
  11. package/docs/profile-guide.md +304 -18
  12. package/docs/skills-guide.md +24 -0
  13. package/examples/extensions/feed-watch/config.ts +36 -0
  14. package/examples/extensions/feed-watch/digest.ts +230 -32
  15. package/examples/extensions/feed-watch/index.ts +8 -0
  16. package/examples/extensions/feed-watch/items.ts +48 -4
  17. package/examples/extensions/overview/actions.ts +264 -0
  18. package/examples/extensions/overview/gate.ts +82 -0
  19. package/examples/extensions/overview/index.ts +123 -0
  20. package/examples/extensions/overview/skill/SKILL.md +80 -0
  21. package/examples/profiles/_template/config.yaml +34 -0
  22. package/examples/profiles/football-reporter/AGENTS.md +24 -6
  23. package/examples/profiles/football-reporter/README.md +8 -0
  24. package/examples/profiles/football-reporter/config.yaml +21 -2
  25. package/examples/profiles/football-reporter/tasks/daily-article.md +6 -4
  26. package/package.json +7 -3
  27. package/resources/templates/mercury.example.yaml +5 -0
  28. package/src/adapters/whatsapp-media.ts +80 -3
  29. package/src/agent/container-entry.ts +100 -15
  30. package/src/agent/container-env.ts +11 -0
  31. package/src/agent/container-error.ts +12 -1
  32. package/src/agent/container-runner.ts +59 -7
  33. package/src/agent/pi-jsonl-parser.ts +166 -5
  34. package/src/bridges/discord.ts +4 -0
  35. package/src/bridges/slack.ts +4 -0
  36. package/src/bridges/teams.ts +4 -0
  37. package/src/bridges/telegram.ts +33 -1
  38. package/src/cli/mercury.ts +1 -0
  39. package/src/config-file.ts +6 -0
  40. package/src/config.ts +23 -0
  41. package/src/core/attachment-notes.ts +4 -0
  42. package/src/core/confirmation.ts +3 -1
  43. package/src/core/handler.ts +2 -1
  44. package/src/core/media.ts +104 -2
  45. package/src/core/outbox.ts +23 -3
  46. package/src/core/reply-context.ts +61 -0
  47. package/src/core/routes/chat.ts +70 -3
  48. package/src/core/routes/dashboard.ts +80 -5
  49. package/src/core/runtime.ts +139 -11
  50. package/src/core/task-output.ts +36 -18
  51. package/src/core/task-scheduler.ts +18 -1
  52. package/src/extensions/catalog.ts +1 -0
  53. package/src/extensions/hooks.ts +8 -0
  54. package/src/extensions/loader.ts +68 -7
  55. package/src/extensions/types.ts +35 -2
  56. package/src/main.ts +19 -0
  57. package/src/server.ts +15 -8
  58. package/src/storage/db.ts +231 -12
  59. package/src/text/sanitize-text.ts +118 -0
  60. package/src/types.ts +73 -1
package/README.md CHANGED
@@ -417,10 +417,22 @@ Supported OAuth providers: Anthropic, GitHub Copilot, Google Gemini CLI, Antigra
417
417
  | `MERCURY_CONTAINER_TIMEOUT_MS` | `300000` | Container timeout (5 min) |
418
418
  | `MERCURY_CONTAINER_RUNTIME` | `runc` | `runc` (default) or `runsc` ([gVisor](https://gvisor.dev)) |
419
419
  | `MERCURY_CONTAINER_BWRAP_DOCKER_COMPAT` | `false` | Set `true` on Linux Docker Engine (see note below) |
420
+ | `MERCURY_CONTAINER_ALLOW_UNCONFINED` | `false` | Set `true` to accept the Docker Desktop sandbox relaxation (see note below) |
420
421
  | `MERCURY_AGENT_ID` | — | Unique ID for this assistant (required when running multiple assistants on the same Docker daemon) |
421
422
 
422
423
  > **Multiple assistants on the same machine:** Mercury builds a derived Docker image (`mercury-agent-ext:<hash>`) from the base image plus your extensions. When a new image is built, older tags in the same repo are pruned. If two assistants share a Docker daemon without distinct `MERCURY_AGENT_ID` values, they share the same image repo and **one will silently prune the other's image**, causing container launch failures. Set `MERCURY_AGENT_ID` to a unique value per project (e.g. in `.env`). Cloud console deployments set this automatically.
423
424
 
425
+ > **Docker Desktop (macOS/Windows):** Mercury detects Docker Desktop and would
426
+ > otherwise relax the sandbox automatically (`--security-opt seccomp=unconfined
427
+ > --cap-add SYS_ADMIN`) so `bwrap` can nest. It now **refuses to do that
428
+ > silently**: containers fail to start with a message naming the remedy.
429
+ > Set `MERCURY_CONTAINER_ALLOW_UNCONFINED=true` (or `agent.allow_unconfined:
430
+ > true` in `mercury.yaml`) to accept the trade knowingly — it is then logged at
431
+ > `warn` on every spawn. **Upgrading an existing Docker Desktop install requires
432
+ > this**, or every message gets a generic container failure. Linux hosts are
433
+ > unaffected; `MERCURY_CONTAINER_RUNTIME=runsc` (gVisor) avoids the relaxation
434
+ > entirely, and the flag is ignored when `container_bwrap_docker_compat` is set.
435
+
424
436
  > **Linux Docker Engine:** Mercury uses [bubblewrap](https://github.com/containers/bubblewrap) for in-container sandboxing. On Linux Docker Engine (not Docker Desktop), bwrap cannot mount `/proc` without extra privileges. Either set `container_bwrap_docker_compat: true` in `mercury.yaml` (adds `--privileged` to `docker run`), or install [gVisor](https://gvisor.dev/docs/user_guide/install/) and set `MERCURY_CONTAINER_RUNTIME=runsc` to skip bwrap entirely.
425
437
 
426
438
  **KB Distillation:**
@@ -111,6 +111,7 @@ COPY src/cli/mrctl.ts /app/src/cli/mrctl.ts
111
111
  COPY src/cli/mrctl-http.ts /app/src/cli/mrctl-http.ts
112
112
  COPY src/extensions/reserved.ts /app/src/extensions/reserved.ts
113
113
  COPY src/extensions/permission-guard.ts /app/src/extensions/permission-guard.ts
114
+ COPY src/text/sanitize-text.ts /app/src/text/sanitize-text.ts
114
115
  COPY src/types.ts /app/src/types.ts
115
116
  COPY resources/ /app/resources/
116
117
  COPY examples/extensions/ /tmp/examples-extensions/
@@ -80,6 +80,7 @@ COPY src/cli/mrctl.ts /app/src/cli/mrctl.ts
80
80
  COPY src/cli/mrctl-http.ts /app/src/cli/mrctl-http.ts
81
81
  COPY src/extensions/reserved.ts /app/src/extensions/reserved.ts
82
82
  COPY src/extensions/permission-guard.ts /app/src/extensions/permission-guard.ts
83
+ COPY src/text/sanitize-text.ts /app/src/text/sanitize-text.ts
83
84
  COPY src/types.ts /app/src/types.ts
84
85
  COPY resources/ /app/resources/
85
86
  COPY examples/extensions/ /tmp/examples-extensions/
@@ -0,0 +1,43 @@
1
+ # Autopilot Configuration
2
+ # Read by the orchestrator each iteration. See docs/templates/TEMPLATE-AUTOPILOT-CONFIG.yaml.
3
+
4
+ gates:
5
+ max_iterations: 10
6
+ max_features_without_review: 3
7
+ max_retries_same_task: 3
8
+ iteration_timeout_minutes: 30
9
+
10
+ risk:
11
+ sensitive_paths: # changes to these = immediate stop for review
12
+ - "**/auth/**"
13
+ - "**/migration*"
14
+ - "**/.env*"
15
+ - "**/schema*"
16
+ # Container spawn shape: sandbox flags, mounts, network, runtime selection.
17
+ # A wrong change here passes `bun run check` and is only observable in the
18
+ # running container — the check gate cannot verify it. Human-only.
19
+ - "src/agent/container-runner.ts"
20
+ - "src/agent/container-entry.ts"
21
+ auto_stop_risk_threshold: 8
22
+
23
+ scope:
24
+ allowed_actions:
25
+ - implement_feature
26
+ - continue_feature
27
+ - fix_bug
28
+ - execute_refactor
29
+ - sync_docs
30
+ skip_slugs: # never touch autonomously
31
+ # Both are design decisions, not mechanical fixes, and neither is verifiable
32
+ # by the check suite. See docs/bugs/ for the filed reports.
33
+ - docker-desktop-sandbox-relaxation-silent # fix = a product call: warn, or refuse and break Docker Desktop users
34
+ - container-egress-unrestricted # architecture: needs a chosen proxy/allowlist shape
35
+ # attachment-ingest-type-gating — unblocked 2026-08-27 (warn-only decision
36
+ # recorded in the spec). Eligible once it is listed under ROADMAP Now/Next.
37
+
38
+ planning:
39
+ allow_autonomous_planning: false
40
+
41
+ notifications:
42
+ on_stop: console
43
+ decision_log: docs/autopilot/decision-log.md
@@ -29,6 +29,9 @@ the repo, after a pi upgrade.
29
29
  ## Run budget (tools-capable models only)
30
30
  ## Destructive Operations — Confirmation Required
31
31
  ## Presenting tool results ("simple lists", "never show JSON/commands")
32
+ ## Delivery ("[reply] … [/reply] — only the span inside is sent";
33
+ pi-jsonl-parser.ts extractReplyEnvelope honours it,
34
+ a reply with no markers is sent whole)
32
35
  ## Character (mrctl character flow + "standing instruction → set a preference"
33
36
  — only for a caller holding prefs.set)
34
37
  reply-anchor sentence (swipe-replies only)
@@ -192,6 +192,7 @@ The pi agent runs inside `bwrap`, which creates a minimal mount namespace with o
192
192
  | Env Var | Purpose |
193
193
  |---------|---------|
194
194
  | `MERCURY_CONTAINER_BWRAP_DOCKER_COMPAT=1` | **Host only.** Adds `docker run --security-opt seccomp=unconfined --cap-add SYS_ADMIN` so `bwrap` can nest inside the agent container (e.g. Docker Desktop). Keeps bubblewrap on. |
195
+ | `MERCURY_CONTAINER_ALLOW_UNCONFINED=true` | **Host only.** Accepts the *auto-detected* Docker Desktop relaxation (same flags as above). Defaults to `false`, so on Docker Desktop containers refuse to start until it is set — the relaxation is never applied without the operator seeing it. Logged at `warn` on every spawn once enabled. Ignored under `runsc` and when `BWRAP_DOCKER_COMPAT` is set. |
195
196
  | `MERCURY_DISABLE_BUBBLEWRAP=1` | Disable bubblewrap; run pi directly (last resort / debugging) |
196
197
 
197
198
  If you see `bwrap: Creating new namespace failed: Operation not permitted`, try **`MERCURY_CONTAINER_BWRAP_DOCKER_COMPAT=1`** first so you keep defense-in-depth. Only use `MERCURY_DISABLE_BUBBLEWRAP=1` if compat mode is not enough.
@@ -397,6 +397,49 @@ mercury extensions list # or: mercury ext list
397
397
 
398
398
  Shows all installed extensions (user + built-in) with features and descriptions.
399
399
 
400
+ ## Cross-Space Overview (`overview`)
401
+
402
+ `examples/extensions/overview/` gives **one** designated space a read-only
403
+ window onto the others — the only cross-space read an agent has. It is not a
404
+ builtin: install it deliberately.
405
+
406
+ ```bash
407
+ mercury add ./examples/extensions/overview
408
+ MERCURY_MANAGEMENT_SPACE_ID=<space id> # host-only; unset = every read denied
409
+ ```
410
+
411
+ The agent calls it with `mrctl capability overview <action> '<json>'`:
412
+
413
+ | Action | Body | Returns |
414
+ |--------|------|---------|
415
+ | `list` | — | every space with `messageCount`, `conversationCount`, `platforms`, `lastActivity` |
416
+ | `recent` | `{spaceId, limit?}` | that space's latest messages, newest first |
417
+ | `search` | `{q, spaceId?, limit?}` | substring matches; every space when `spaceId` is omitted |
418
+ | `tasks` | `{spaceId?, limit?}` | scheduled tasks, one space or all |
419
+ | `roles` | `{spaceId}` | role assignments |
420
+
421
+ `limit` defaults to 20 and is capped at 100; message bodies — and a task's
422
+ `prompt` and `lastError` — are truncated at 500 characters with
423
+ `truncated: true`. Every action is a plain `SELECT` — there is no
424
+ cross-space write, and `listMutes` is deliberately not exposed because it deletes
425
+ expired rows.
426
+
427
+ Three gates run before any action, in the handler, and they are the whole
428
+ boundary:
429
+
430
+ 1. `MERCURY_MANAGEMENT_SPACE_ID` is set — **absence denies**.
431
+ 2. The caller's token-bound space id equals it, by exact string equality.
432
+ 3. The caller is a global admin (`config.admins` / `dmAutoSpaceAdminIds`).
433
+
434
+ The `overview` permission checked by the capability route is **not** a scope:
435
+ `admin` is granted every registered permission in its own space, so every space
436
+ admin in the deployment passes it. Only the three gates above stop them.
437
+
438
+ The skill is installed into the management space's own workspace on
439
+ `workspace_init`, not via `mercury.skill()` — that copies into the global skills
440
+ dir mounted into *every* container, which would advertise the capability to
441
+ spaces that cannot use it.
442
+
400
443
  ## Examples
401
444
 
402
445
  See [`examples/extensions/`](../examples/extensions/) for complete, working extensions ranging from minimal (charts — CLI + skill) to full-featured (napkin — hooks, jobs, config, widgets, KB distillation).
@@ -475,5 +475,41 @@
475
475
  implementing: `## Current State (as of …)` is not matched by the injector, so
476
476
  every note napkin had written for this space was reaching the model as a
477
477
  summary line with no facts —
478
- `docs/bugs/episode-current-state-heading-suffix-drops-body.md`.
478
+ `docs/debug/major/2026-08-31-episode-current-state-heading-suffix-drops-body.md`.
479
479
  - **Used by:** reporter-notebook (M2.3, re-cut), football-reporter-fixture-harness (provenance source = `episodes/`), napkin (distill addendum)
480
+
481
+ ## D-020: Live tuning is adopted into the repo, not reverted — and scans carry a fixed club emoji (decided 2026-08-31)
482
+ - **Category:** layer placement / process (refines [D-003](#d-003-layer-placement-for-football), [D-013](#d-013-tone-is-editorial-and-scan-length-is-per-item-decided-2026-08-20))
483
+ - **Decided:** (1) When the owner edits the live `AGENTS.md` on the box while
484
+ the bot is running, the repo **adopts** the live text (copy live → repo,
485
+ read the whole file for the contradiction the edit introduced, fix that in
486
+ the repo, `apply`) — it never reverts it with `apply`. The live file is the
487
+ owner's most recent editorial judgement and the repo's job is to record it;
488
+ `check` failing is the signal to reconcile, not to overwrite. (2) The
489
+ 2026-08-27 edits are now the standard: every scan item opens with one fixed
490
+ club emoji (😈 United, 🔵 Barcelona, ⚪ Real, 🔴 Hapoel TA, 💛 Beitar,
491
+ 🟢 Maccabi Haifa; none for an item outside those six, none invented), items
492
+ are grouped under one club heading when several concern one club, an item
493
+ is ≤ 200 characters including its `(source, dd/mm)`, items are separated by
494
+ a blank line, the displayed date is `dd/mm` while the year is still verified
495
+ in the source, and the article stays at zero emoji.
496
+ - **Alternatives considered:**
497
+ - *Revert the box to the repo with `apply`.* Rejected: the edits are the
498
+ owner's, dated, and were running for four days; the repo was the stale
499
+ copy.
500
+ - *Keep "0 emoji in scheduled output" as decided at the M1 checkpoint.*
501
+ Rejected by the owner's edit: a fixed per-club marker is a format element
502
+ (it tells a phone reader which club before the sentence), not decoration.
503
+ M3.1's emoji lint becomes an **allowlist** (exactly one of the six, at line
504
+ start, scan only) rather than a zero count.
505
+ - **Reasoning:** `check` on 2026-08-31 reported one difference — `AGENTS.md`
506
+ — whose diff was purely additive owner tuning from 2026-08-27 15:39. The
507
+ same file's `## סגנון` still said "scan and article: zero", so the adoption
508
+ also fixed the in-file contradiction the guide's §4.5 warns about. The
509
+ fourth bullet the owner added ("no English working sentence as a first
510
+ line") is a prose rule over a host defect and did not hold (four leaks after
511
+ it); it stays in the file as documentation of intent but the fix is
512
+ `docs/bugs/narration-in-same-text-block-leaks-into-reply.md`.
513
+ - **Revisit if:** live edits become frequent — then the right tool is a
514
+ profile PR flow from the box (edit → `dump` → commit), not a rule.
515
+ - **Used by:** football-reporter profile (`AGENTS.md`), football-reporter-fixture-harness (M3.1 lint spec)
@@ -1,7 +1,7 @@
1
1
  # Roadmap: Football Reporter Profile
2
2
 
3
3
  **Goal**: [football-reporter-profile](goal.md)
4
- **Last updated**: 2026-08-25 (M2.4 shipped`persona.exclusive` and permission-gated platform sections; deploy pending an image rebuild)
4
+ **Last updated**: 2026-08-31 (M2.1/M2.3/M2.4 live; the M2 cost failure is diagnosed, fixed and verified on the live bot verify-run prefix 118 K → 50.6 K, projection ≈ $5.4/day, ledger confirmation due 09-01 09-03; live `AGENTS.md` tuning adopted per D-020)
5
5
 
6
6
  > Sequence chosen by the owner on 2026-08-20: (1) profile + one editorial
7
7
  > layer → (2) deterministic feed → (3) proof of quality, with a parallel bug
@@ -55,11 +55,11 @@ the group's reaction to the first two articles is recorded in the goal's notes.
55
55
 
56
56
  | ID | Story | Slug | Depends on | Status |
57
57
  |----|-------|------|------------|--------|
58
- | M2.1 | Feed Watch — host-side poller, digest into the article, verify one-shots (existing spec, absorbed) | feed-watch | M1.1 | done (merged `e07b239` 2026-08-21; **deploy not yet run** — runbook in `pending-verification.md`) |
58
+ | M2.1 | Feed Watch — host-side poller, digest into the article, verify one-shots (existing spec, absorbed) | feed-watch | M1.1 | done (merged `e07b239` 2026-08-21; **live since 2026-08-25 17:16 UTC**, enabled by the owner from chat the runbook's 19 checks were never walked; cost evidence below) |
59
59
  | M2.2 | Scheduled task model-leg policy: primary-or-skip for research runs (F2) | scheduled-task-model-leg-policy | — | deferred until after M3.1 (D-015) |
60
- | M2.3 | Reporter notebook, re-cut 2026-08-21 (D-019): topic notes are napkin-shaped **episodes** the profile seeds once and napkin maintains nightly, written in-turn before posting; seeded `MEMORY.md` for standing facts; priorities tie-break in `AGENTS.md`; `workspace_seed` in `space-profile`; per-space distillation addendum in napkin; member perms and stale-note hygiene (audit R1/R6/R7, D-016, D-019) | reporter-notebook | M1.1 | done (merged 2026-08-22; **deploy not yet run**runbook in `pending-verification.md`) |
61
- | M2.4 | One voice per space: `persona.exclusive` drops the global character and global `AGENTS.md` for a space that owns its standard; platform-prompt capability paragraphs gated on the caller's permissions (audit R2, D-017) | one-voice-per-space | — | done (merged 2026-08-25; **deploy not yet run** needs an agent image rebuild, runbook in `pending-verification.md`) |
62
- | M2.5 | Chat window holds conversation, not procedure (scheduled prompts out of the turn count, no halving on swipe-reply); silence is a legal chat reply (audit R3/R4, D-018) | chat-window-and-silence | — | backlog (host) |
60
+ | M2.3 | Reporter notebook, re-cut 2026-08-21 (D-019): topic notes are napkin-shaped **episodes** the profile seeds once and napkin maintains nightly, written in-turn before posting; seeded `MEMORY.md` for standing facts; priorities tie-break in `AGENTS.md`; `workspace_seed` in `space-profile`; per-space distillation addendum in napkin; member perms and stale-note hygiene (audit R1/R6/R7, D-016, D-019) | reporter-notebook | M1.1 | done (merged 2026-08-22; **deployed 2026-08-22 23:07 IDT**; 19 episode notes on the box by 08-31, `MEMORY.md` written by the bot 7 of 10 evidence checks still unticked in `pending-verification.md`) |
61
+ | M2.4 | One voice per space: `persona.exclusive` drops the global character and global `AGENTS.md` for a space that owns its standard; platform-prompt capability paragraphs gated on the caller's permissions (audit R2, D-017) | one-voice-per-space | — | done (merged 2026-08-25; **deployed 2026-08-25 16:44 IDT** with the image rebuild; `persona.exclusive=true` live; 6 behaviour checks still unticked) |
62
+ | M2.5 | Chat window holds conversation, not procedure (scheduled prompts out of the turn count, no halving on swipe-reply); silence is a legal chat reply (audit R3/R4, D-018) | chat-window-and-silence | — | done (merged 2026-08-31; live checks queued in `pending-verification.md` §M2.5, deploy-gated) |
63
63
 
64
64
  > **Revisit 2026-08-21 — napkin works now** (`docs/notes/napkin-revisit-2026-08-21.md`). **Approved by the owner the same day ([D-019](decisions.md)):** M2.3 re-cut so the topic notes are napkin-maintained episodes under `knowledge/episodes/` (the only injected dir) that the profile seeds once, plus a per-space distillation addendum. Nothing removed; M2.4/M2.5/M3.x unchanged. Idea parked: `docs/ideas/napkin-member-notes.md`.
65
65
 
@@ -91,6 +91,58 @@ and a member run's system prompt carries no preference-management paragraph; a
91
91
  swipe-reply follow-up sees the bot's own post from the previous evening; a
92
92
  "don't answer" message in `main` gets no reply.
93
93
 
94
+ > **Checkpoint evidence, read from the box 2026-08-31** (ledger `task_runs`,
95
+ > `space_config`, `messages`; nothing gated, everything reported per D-008):
96
+ >
97
+ > - **Passed:** task 28 and `tasks/scan.md` are gone; verify one-shots produce
98
+ > either a sourced item line or `NO_UPDATE` (08-31 sample: 5 posted / 6
99
+ > no_update); the 09:00 article runs daily with the digest (`last_status=ok`,
100
+ > 608-char summary on 08-31); `persona.exclusive=true` is live and the image
101
+ > carries `callerMay`; every run since 08-25 is single-leg (no fallback
102
+ > possible under the pin).
103
+ > - **Failed:** the **cost direction**. 46–48 runs/day, ~70 % `no_update`,
104
+ > **$14–20/day** for the space (08-26 → 08-30) against "well under
105
+ > ~$5.30/day" and a pre-M2 baseline of ~$3/day.
106
+ > **Diagnosis corrected 2026-08-31 (second read, from `token_usage` and the
107
+ > traces, not `task_runs`):** the standard + notebook is *not* the cost — a
108
+ > verify run on that prefix was ~37 K tokens / $0.09 on 08-25. Since 08-26
109
+ > every run cache-writes ~121 K tokens / $0.30, because the daily article's
110
+ > expanded 103 KB digest is persisted and replayed through the history
111
+ > window — `docs/debug/major/2026-08-31-feed-watch-digest-persisted-and-replayed.md`, on the
112
+ > bug track below. Cost law: `cost/day ≈ runs × prefix tokens × $2.50/M`.
113
+ > Fix order: host persists the pre-append prompt (removes the replay) →
114
+ > digest without links + a cap (the article's own prefix) → profile levers
115
+ > `max_per_hour` 1, `batch_minutes` 15, `exclude` mined from `NO_UPDATE`
116
+ > replies (the run count). Expected after the first two: ~$4.5/day at
117
+ > today's run count, ~$2.5–3 with the levers.
118
+ > **All three shipped and were verified on the live bot 2026-08-31** (`fad12f9`,
119
+ > `81d022e`; two on-demand article runs against the test group rather than
120
+ > waiting for 09:00). The article turn is 105,482 → 4,322 chars, the digest
121
+ > block 36.0 KB for a *larger* window, and a verify run's first-round prefix
122
+ > **118 K → 50.6 K**. Projection ≈ $5.4/day; the ledger over 09-01 → 09-03 is
123
+ > what closes the spend check. `exclude` mining was **not** done — the run
124
+ > count is already capped at 1/hour and the prefix is the bigger factor.
125
+ > Correction to the measurement method, worth more than the number: the
126
+ > comparable prefix is the **first round's** `cacheWrite` from the trace, not
127
+ > `token_usage.cache_write_tokens`, which sums every round and so makes a
128
+ > 16-round article look more expensive than a 1-round `NO_UPDATE`. **The "per-task cheaper leg"
129
+ > rationale for re-opening M2.2 does not survive this** — decide M2.2 on
130
+ > safety after the bug is fixed, not on cost before it. Second-order levers
131
+ > filed as ideas: `isolated-verify-task-runs.md`, `cross-run-prompt-caching.md`.
132
+ > The ledger that hid this for six days is its own bug:
133
+ > `task-runs-ledger-input-tokens-is-uncached-sliver.md`.
134
+ > - **Changed under us:** the space's `model.active` is `claude-sonnet-5`
135
+ > since 2026-08-24 (owner's `/model switch`), so "zero posts from a
136
+ > non-primary leg" is true only because there is no fallback; the primary is
137
+ > no longer Opus. Sonnet narrates: 10 of 121 replies since 08-22 open with an
138
+ > English working sentence (`docs/bugs/narration-in-same-text-block-leaks-into-reply.md`).
139
+ > - **Not evidenced:** the trace checks (topic note read on a factual
140
+ > question, no `AGENTS.md` mount for the exclusive space during a run), the
141
+ > swipe-reply continuity and the "don't answer" silence — all need a person
142
+ > watching a run; still open in `pending-verification.md`.
143
+ > - **Drift:** the owner tuned the live `AGENTS.md` on 08-27; adopted into the
144
+ > repo 08-31 ([D-020](decisions.md)).
145
+
94
146
  > **Extended 2026-08-21 ([D-016](decisions.md)–[D-018](decisions.md)).** The
95
147
  > 2026-08-21 audit (`docs/notes/football-audit-2026-08-21.md`) traced both
96
148
  > owner-reported failures — a hallucinated name and a joke refused as a
@@ -146,6 +198,12 @@ is filed the same way, never fixed inside the story.
146
198
  | — | `model-switch-disables-fallback.md` | moderate | M4.1 |
147
199
  | audit-2 C9 | `feed-watch-seen-ttl-not-refreshed.md` | moderate | M4.1 (fix **before** the feed-watch deploy) |
148
200
  | audit-2 C10 | `feed-watch-runas-non-user-principal.md` | moderate | M4.1 (fix **before** the feed-watch deploy) |
201
+ | M2 checkpoint | `feed-watch-digest-persisted-and-replayed.md` | major | **DONE** 2026-08-31 — fixed, deployed, verified on the live bot (`fad12f9` + `81d022e`). M2.2 is now unblocked and must be decided on *safety*, not cost |
202
+ | M2 checkpoint | `task-runs-ledger-input-tokens-is-uncached-sliver.md` | minor | **DONE** 2026-08-31 — `cache_read_tokens`/`cache_write_tokens`/`prompt_prefix_tokens`/`rounds` on the row, one derived `billedPromptTokens` for every reader, dashboard shows Prefix · Rounds · Billed. Deployed with an image rebuild (`src/types.ts` is baked); audited 2026-08-31 against raw traces and a smoke run (298). Post-mortem in `docs/debug/minor/` |
203
+ | M2.3 checkpoint | `episode-current-state-over-budget.md` | moderate | **Re-ranked 2026-08-31** — only ONE episode is injected per run, not all: 1-12% of the prompt (barcelona 11.6 KB worst case), not the "24 KB every run" filed. Still worth the pruning rule for note *quality*; it is not the cost lever it was filed as |
204
+ | — | `football-group-went-silent-under-the-feed.md` | minor | posting budget for a test group; stale `space_roles` tidy |
205
+ | — | `episode-current-state-heading-suffix-drops-body.md` | major (latent) | **DONE** 2026-08-31 (`b18e9ca`, deployed with a rebuild) — does not reproduce today: every live note carries an HTML-comment workaround telling the distiller to keep the heading bare. Fixed at the parser so one rewrite cannot silently return it |
206
+ | M2 follow-on | `history-window-dominates-the-run-prefix.md` | major | **NEW** 2026-08-31 — `<history>` is 65–81% of every prompt (~68 KB, near-constant across runs). The largest remaining prefix term by a wide margin, and never a bug — a default meeting an unusual space. **Read M2.5's architecture first**: this may be evidence for that story rather than a separate fix |
149
207
 
150
208
  The two `feed-watch-*` rows come from the 2026-08-21 audit's second pass
151
209
  (`docs/notes/football-audit-2026-08-21.md` §3.1): a `seen` TTL that counts
@@ -210,6 +210,21 @@ Permissions are **per-space**:
210
210
  - Custom role permissions are space-specific
211
211
  - No global roles (except seeded admins, which apply on first interaction per space)
212
212
 
213
+ ### An extension permission is not a scope
214
+
215
+ `admin` is granted **all** permissions — built-in *and* extension-registered — in
216
+ its own space. So a permission registered by an extension gates members, and
217
+ nothing more: every space admin in the deployment passes `checkPerm(<ext>)`
218
+ automatically, including for a capability meant for one space only.
219
+
220
+ An extension that must be restricted to particular spaces or particular callers
221
+ has to enforce that inside its own handler. The `overview` extension
222
+ (`examples/extensions/overview/`, see [extensions.md](extensions.md#cross-space-overview-overview))
223
+ is the worked example: it reads other spaces' messages, and its real boundary is
224
+ three in-handler checks — a host-only `MERCURY_MANAGEMENT_SPACE_ID` env var
225
+ (absence denies), exact equality against the caller's token-bound space id, and
226
+ `isGlobalAdmin`. Its `defaultRoles: []` keeps members out and does nothing else.
227
+
213
228
  ## API
214
229
 
215
230
  ### `resolveRole(db, spaceId, platformUserId, seededAdmins)`