mercury-agent 0.24.2-beta.0 → 0.24.2-beta.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 (137) hide show
  1. package/README.md +1 -1
  2. package/docs/auth/whatsapp.md +6 -4
  3. package/docs/configuration.md +5 -0
  4. package/docs/container-lifecycle.md +2 -2
  5. package/docs/goals/rehearsal-bench/roadmap.md +1 -1
  6. package/docs/goals/space-links/roadmap.md +1 -1
  7. package/docs/live-testing.md +34 -3
  8. package/docs/pending-verification.md +162 -0
  9. package/docs/plans/{flat-0916 → archive/2026-09-28-flat-0916}/plan.md +4 -4
  10. package/docs/plans/archive/2026-10-02-media-bugs-0928/briefs/S1.md +59 -0
  11. package/docs/plans/archive/2026-10-02-media-bugs-0928/briefs/S2.md +64 -0
  12. package/docs/plans/archive/2026-10-02-media-bugs-0928/briefs/S3.md +61 -0
  13. package/docs/plans/archive/2026-10-02-media-bugs-0928/decisions.md +6 -0
  14. package/docs/plans/archive/2026-10-02-media-bugs-0928/plan.md +111 -0
  15. package/docs/plans/archive/2026-10-02-media-bugs-0928/sessions/S1.md +34 -0
  16. package/docs/plans/archive/2026-10-02-media-bugs-0928/sessions/S2.md +43 -0
  17. package/docs/plans/archive/2026-10-02-media-bugs-0928/sessions/S3.md +35 -0
  18. package/docs/plans/{sensitive-bugs-0922 → archive/2026-10-02-sensitive-bugs-0922}/plan.md +2 -2
  19. package/docs/plans/archive/2026-10-02-sensitive-bugs-0922/sessions/S1.md +35 -0
  20. package/docs/plans/archive/2026-10-02-sensitive-bugs-0922/sessions/S2.md +30 -0
  21. package/docs/plans/archive/2026-10-02-sensitive-bugs-0922/sessions/S3.md +39 -0
  22. package/docs/plans/archive/2026-10-02-sensitive-bugs-0922/sessions/S4.md +38 -0
  23. package/docs/plans/archive/2026-10-02-sensitive-bugs-0922/sessions/S5.md +37 -0
  24. package/docs/plans/general-0928/briefs/S1.md +66 -0
  25. package/docs/plans/general-0928/briefs/S2.md +64 -0
  26. package/docs/plans/general-0928/briefs/S3.md +58 -0
  27. package/docs/plans/general-0928/briefs/S4.md +58 -0
  28. package/docs/plans/general-0928/briefs/S5.md +58 -0
  29. package/docs/plans/general-0928/decisions.md +8 -0
  30. package/docs/plans/general-0928/plan.md +107 -0
  31. package/docs/plans/search-bugs-0930/briefs/S1.md +73 -0
  32. package/docs/plans/search-bugs-0930/briefs/S2.md +75 -0
  33. package/docs/plans/search-bugs-0930/briefs/S3.md +77 -0
  34. package/docs/plans/search-bugs-0930/briefs/S4.md +75 -0
  35. package/docs/plans/search-bugs-0930/briefs/S5.md +74 -0
  36. package/docs/plans/search-bugs-0930/briefs/S6.md +77 -0
  37. package/docs/plans/search-bugs-0930/decisions.md +8 -0
  38. package/docs/plans/search-bugs-0930/plan.md +165 -0
  39. package/docs/plans/search-bugs-0930/sessions/S1.md +28 -0
  40. package/docs/plans/search-bugs-0930/sessions/S2.md +27 -0
  41. package/docs/plans/search-bugs-0930/sessions/S3.md +40 -0
  42. package/docs/plans/search-bugs-0930/sessions/S5.md +39 -0
  43. package/docs/plans/search-bugs-0930/sessions/S6.md +37 -0
  44. package/docs/plans/search-quality-0930/briefs/S12.md +69 -0
  45. package/docs/plans/search-quality-0930/briefs/S7.md +76 -0
  46. package/docs/plans/search-quality-0930/briefs/S9.md +68 -0
  47. package/docs/plans/search-quality-0930/decisions.md +11 -0
  48. package/docs/plans/search-quality-0930/plan.md +107 -0
  49. package/docs/plans/unattended-1003/briefs/S1.md +92 -0
  50. package/docs/plans/unattended-1003/briefs/S2.md +92 -0
  51. package/docs/plans/unattended-1003/briefs/S3.md +90 -0
  52. package/docs/plans/unattended-1003/briefs/S4.md +92 -0
  53. package/docs/plans/unattended-1003/briefs/S5.md +92 -0
  54. package/docs/plans/unattended-1003/briefs/S6.md +94 -0
  55. package/docs/plans/unattended-1003/briefs/S7.md +92 -0
  56. package/docs/plans/unattended-1003/briefs/S8.md +90 -0
  57. package/docs/plans/unattended-1003/briefs/S9.md +92 -0
  58. package/docs/plans/unattended-1003/decisions.md +7 -0
  59. package/docs/plans/unattended-1003/plan.md +153 -0
  60. package/docs/scheduler.md +5 -1
  61. package/docs/web-search.md +13 -7
  62. package/examples/extensions/napkin/distill-input.ts +27 -6
  63. package/examples/extensions/napkin/prompts/kb-distillation.md +3 -1
  64. package/examples/extensions/pinchtab/bin/pinchtab_ensure +129 -0
  65. package/examples/extensions/pinchtab/bin/pinchtab_nav +50 -0
  66. package/examples/extensions/pinchtab/bin/pinchtab_session_inject.cjs +184 -0
  67. package/examples/extensions/pinchtab/index.ts +41 -237
  68. package/examples/extensions/pinchtab/skill/SKILL.md +7 -110
  69. package/examples/extensions/web-search/brave.ts +396 -18
  70. package/examples/extensions/web-search/index.ts +122 -22
  71. package/examples/extensions/web-search/ledger.ts +156 -0
  72. package/examples/extensions/web-search/skill/SKILL.md +30 -6
  73. package/examples/extensions/web-search/skill/references/zap.md +148 -0
  74. package/examples/profiles/football-reporter/config.yaml +11 -2
  75. package/examples/profiles/tel-aviv-flat-search/config.yaml +3 -2
  76. package/package.json +4 -4
  77. package/src/adapters/whatsapp-media.ts +262 -40
  78. package/src/adapters/whatsapp.ts +1 -1
  79. package/src/agent/container-entry.ts +348 -36
  80. package/src/agent/container-error.ts +39 -2
  81. package/src/agent/container-runner.ts +162 -23
  82. package/src/agent/pi-failure-class.ts +36 -6
  83. package/src/agent/pi-jsonl-parser.ts +151 -33
  84. package/src/agent/provider-auth-rejection.ts +279 -0
  85. package/src/agent/user-error-messages.ts +26 -0
  86. package/src/bridges/discord.ts +7 -0
  87. package/src/bridges/slack.ts +13 -1
  88. package/src/bridges/teams.ts +29 -16
  89. package/src/bridges/telegram.ts +13 -0
  90. package/src/bridges/whatsapp.ts +6 -0
  91. package/src/cli/mercury.ts +85 -0
  92. package/src/cli/mrctl-http.ts +67 -0
  93. package/src/cli/mrctl.ts +14 -12
  94. package/src/config.ts +5 -3
  95. package/src/core/api-types.ts +5 -0
  96. package/src/core/api.ts +9 -1
  97. package/src/core/auth.ts +1 -1
  98. package/src/core/caller-token.ts +32 -1
  99. package/src/core/handler.ts +11 -1
  100. package/src/core/media.ts +216 -16
  101. package/src/core/operator-alerts.ts +204 -4
  102. package/src/core/outbox.ts +125 -12
  103. package/src/core/routes/capability.ts +2 -2
  104. package/src/core/routes/config-builtin.ts +27 -0
  105. package/src/core/routes/console.ts +7 -0
  106. package/src/core/routes/dashboard.ts +10 -0
  107. package/src/core/routes/tasks.ts +11 -0
  108. package/src/core/runtime.ts +455 -70
  109. package/src/core/space-links.ts +273 -0
  110. package/src/core/system-messages.ts +45 -2
  111. package/src/core/task-scheduler.ts +115 -5
  112. package/src/env-dump/build-deps.ts +2 -55
  113. package/src/extensions/catalog.ts +5 -3
  114. package/src/extensions/context.ts +4 -0
  115. package/src/extensions/image-builder.ts +265 -65
  116. package/src/extensions/reserved.ts +6 -0
  117. package/src/extensions/types.ts +16 -0
  118. package/src/ops/shadow-diff.ts +1027 -0
  119. package/src/preflight/binary-version.ts +103 -0
  120. package/src/preflight/build-deps.ts +2 -0
  121. package/src/preflight/checks/credential.ts +16 -9
  122. package/src/preflight/checks/host-deps.ts +107 -10
  123. package/src/preflight/deps.ts +5 -0
  124. package/src/profile/space-profile.ts +21 -0
  125. package/src/server.ts +56 -1
  126. package/src/storage/db.ts +553 -0
  127. package/src/storage/pi-auth.ts +92 -8
  128. package/src/text/reporter-lint.ts +77 -30
  129. package/docs/plans/flat-0916/briefs/S1.md +0 -55
  130. package/docs/plans/flat-0916/briefs/S2.md +0 -55
  131. /package/docs/plans/{flat-0916 → archive/2026-09-28-flat-0916}/decisions.md +0 -0
  132. /package/docs/plans/{sensitive-bugs-0922 → archive/2026-10-02-sensitive-bugs-0922}/briefs/S1.md +0 -0
  133. /package/docs/plans/{sensitive-bugs-0922 → archive/2026-10-02-sensitive-bugs-0922}/briefs/S2.md +0 -0
  134. /package/docs/plans/{sensitive-bugs-0922 → archive/2026-10-02-sensitive-bugs-0922}/briefs/S3.md +0 -0
  135. /package/docs/plans/{sensitive-bugs-0922 → archive/2026-10-02-sensitive-bugs-0922}/briefs/S4.md +0 -0
  136. /package/docs/plans/{sensitive-bugs-0922 → archive/2026-10-02-sensitive-bugs-0922}/briefs/S5.md +0 -0
  137. /package/docs/plans/{sensitive-bugs-0922 → archive/2026-10-02-sensitive-bugs-0922}/decisions.md +0 -0
package/README.md CHANGED
@@ -166,7 +166,7 @@ Install extensions from the dashboard (**Features** page), the CLI, or during se
166
166
 
167
167
  ```bash
168
168
  # From the CLI
169
- mercury add web-browser # Web browsing & search (uses Brave Search, no API key needed)
169
+ mercury add web-browser # Web browsing: open pages, fill forms, log into sites (no API key needed)
170
170
  mercury add web-search # Brave Search API + site search skill (key stays on the host)
171
171
  mercury add napkin # Obsidian-style knowledge vault
172
172
 
@@ -105,14 +105,14 @@ The auth script writes status files for external monitoring (useful for headless
105
105
  When Mercury is running, you can check auth status via the API:
106
106
 
107
107
  ```bash
108
- curl http://localhost:8787/auth/whatsapp
108
+ curl -H "Authorization: Bearer $MERCURY_API_SECRET" http://localhost:8787/auth/whatsapp
109
109
  ```
110
110
 
111
111
  **Responses:**
112
112
 
113
113
  ```json
114
114
  // Authenticated and connected
115
- { "status": "authenticated" }
115
+ { "status": "authenticated", "phoneNumber": "15550001111" }
116
116
 
117
117
  // Waiting for QR scan (includes raw QR data)
118
118
  { "status": "waiting", "qr": "2@AbCdEf123..." }
@@ -121,7 +121,9 @@ curl http://localhost:8787/auth/whatsapp
121
121
  { "status": "disconnected" }
122
122
  ```
123
123
 
124
- This endpoint requires no authentication, making it suitable for headless monitoring dashboards.
124
+ This endpoint requires the `MERCURY_API_SECRET` Bearer token, like `/api/*`. Without it the
125
+ response is `401` (`503` when no secret is configured) and carries no field of the auth state:
126
+ the QR it serves while pairing links the bot to whoever scans it first.
125
127
 
126
128
  ## Troubleshooting
127
129
 
@@ -169,5 +171,5 @@ Mercury ignores messages that were sent before it connected to prevent processin
169
171
 
170
172
  - Auth credentials in `.mercury/whatsapp-auth/` are sensitive — treat them like passwords
171
173
  - Anyone with access to these files can impersonate your WhatsApp account
172
- - The `/auth/whatsapp` endpoint is unauthenticated — only expose your API port to trusted networks
174
+ - The `/auth/whatsapp` endpoint serves the live pairing QR, so it requires `MERCURY_API_SECRET`; keep that secret out of anything a browser or a log can read
173
175
  - Consider using a dedicated WhatsApp number for your bot
@@ -284,6 +284,11 @@ Env equivalents: **`MERCURY_ALERTS_ENABLED`**, **`MERCURY_ALERTS_COOLDOWN_MS`**.
284
284
  instead, and its space hears nothing, even when no operator is reachable
285
285
  (then the only record is the ERROR log line). No cooldown: a final failure is
286
286
  already once per due cycle.
287
+ - **Scheduled runs skipped by the sensitive-connection deny** (a group space
288
+ without `security.sensitive_connections_allowed: true`) report to the
289
+ operators above, one message per skip, never to the space. The same
290
+ `scheduling.notify_on_failure` switch governs it: with it off, nobody is
291
+ told and the host logs a warn line saying so.
287
292
  - **Partial deliveries.** A scheduled post that reached some of a space's linked
288
293
  conversations and not others is **not** retried — a retry would post a second
289
294
  copy where the first landed — and the operators are told which conversations
@@ -257,7 +257,7 @@ while something is wrong; rolling an upgrade back over one would be wrong.
257
257
  | `image.contract` | blocked | every `container/Dockerfile` `COPY` target is in the image, and the version the build stamped into `/app/image-manifest.json` equals the host's |
258
258
  | `container.mrctl-roundtrip` | blocked | a container on the production launch shape runs `mrctl whoami` and comes back as the caller preflight asserted — by minted token where a caller-token key is configured, by identity header where none is (evidence `callerAuth`; see below). On runsc the host API socket is connect-tested first, so a stopped service blocks with the service remedy and no container is created |
259
259
  | `credential.first-leg` | blocked | the chain's first leg (or `--space`'s pinned leg) resolves a credential the way a spawn resolves it |
260
- | `host.pi` | warn | `pi` is on the host when napkin or longview is loaded |
260
+ | `host.pi` | warn | `pi` is on the host when napkin or longview is loaded, and its `--version` satisfies the pi range in `container/agent-package.json` |
261
261
  | `host.timezone` | warn | no active cron task carries `timezone: null` — a zoneless cron is parsed as UTC on every host, so the host's own zone is evidence, not the condition |
262
262
  | `host.swap` | warn | a host of ≤ 4 GiB has swap |
263
263
  | `extensions.load-report` | warn | every extension on disk is loaded, or skipped **with a reason** |
@@ -476,7 +476,7 @@ RUN curl -fsSL https://bun.sh/install | bash
476
476
  ENV PATH="/home/mercury/.bun/bin:$PATH"
477
477
 
478
478
  # Install required CLIs
479
- RUN bun add -g @earendil-works/pi-coding-agent
479
+ RUN bun add -g @earendil-works/pi-coding-agent@~0.87.1 # the range in container/agent-package.json
480
480
 
481
481
  # Optional: install Playwright/Chromium if your extensions need browser automation
482
482
  RUN bunx playwright install chromium
@@ -48,7 +48,7 @@ matrix in §2 or edits it; `ops-preflight` refuses the exact command of
48
48
  | M1.1 | `shadow: boolean` config (`MERCURY_SHADOW`), absent = live: no adapter constructed, `consoleUrl` unset, dashboard banner + `shadow=1` on every log line; negative tests for each branch | shadow-mode-flag | — | **done** 2026-09-02 — [spec](../../archive/rehearsal/2026-09-02-shadow-mode-flag.md), PR #68 |
49
49
  | M1.2 | `mercury shadow snapshot`: `VACUUM INTO` + copy of `spaces/ global/ extensions/ model-capabilities.yaml` into `~/mercury-shadow/<ts>/`, `SNAPSHOT.json` (source commit, live HEAD, time), `current` symlink, shadow `.env` derived by a script (copy + override lines, never printed) and a `mercury.yaml` with `runtime.data_dir` and `server.port` rewritten; the `.env` writer emits `MERCURY_SHADOW=1` (only `true`/`1` enable the mode) and the shadow **refuses to start** when `runtime.data_dir` is not under `~/mercury-shadow/` — the flag alone protects people, not the live DB (M1.1 review, 2026-09-02) | shadow-snapshot | M1.1 | **shipped** 2026-09-03 — [spec](../../archive/rehearsal/2026-09-03-shadow-snapshot.md), PR #71, deployed 2026-09-03. `mercury shadow snapshot\|prune`, `src/ops/shadow-root.ts` + `src/ops/shadow-snapshot.ts`; the data-dir refusal closes the M1.1 residual |
50
50
  | M1.3 | `mercury shadow up|down|status|logs`: transient user unit via `systemd-run --user --unit mercury-shadow`, `--src` defaults to `~/mercury-src`; `down` prints the run cost from the shadow's `token_usage`; refuses `up` when the live `mercury` unit is not the only thing on 8787 or when the snapshot is older than `--max-age`. **From M1.2's plan:** the unit must set `HOME` (the data-dir guard refuses on an empty home), and `up` must record its snapshot path so `shadow prune` skips the running one. **Added 2026-09-03 (D-011, D-012):** (a) *prerequisite, first commit of the story* — every container the runner creates (agent, probes) carries a `mercury.instance=<id>` label derived from the data dir, and `cleanupOrphans` **always** filters on it — no id, no sweep; negative test deletes the filter and watches a foreign container get listed; (b) `--src` also accepts an installed package (`mercury-agent` under npm, run as root with `XDG_RUNTIME_DIR=/run/user/0`), so the same command works on a fleet box; (c) `up` refuses below `--min-free-mem` / `--min-free-disk`, while `task_runs` has an unfinished row, or while a cron is due within the run window; never builds an image (missing derived image → refuse, name the tag); binds 8788 to loopback | shadow-service | M1.2 | **done** 2026-09-07 — [spec](../../archive/rehearsal/2026-09-07-shadow-service.md). **Three corrections found in the code while building it:** the roadmap's "refuse while `task_runs` has an unfinished row" could not be written as specified (`finished_at` is NOT NULL and rows are written at completion, so the table never shows a run in progress) — it is a *running container* carrying the live instance id instead; "binds 8788 to loopback" was **dropped**, because under `runc` agent containers reach the host API over TCP at `host.docker.internal:<port>` and a loopback bind would break agent runs in the shadow only (a bind-host option is B-2, live and shadow alike); and the label had to cover a fourth container site, `preflight/probe-container.ts` |
51
- | M1.4 | `mercury shadow diff [--space <id>]`: live snapshot vs shadow now — `space_config` / `space_preferences` / `tasks` / `space_roles` / `mutes` rows with `updated_by` before→after, `messages` added (count + first lines), workspace tree by hash (`MEMORY.md`, `knowledge/**`, `AGENTS.md`), `extension_state` keys. Output is the report a migration rehearsal is judged on | shadow-diff | M1.2 | backlog |
51
+ | M1.4 | `mercury shadow diff [--space <id>]`: live snapshot vs shadow now — `space_config` / `space_preferences` / `tasks` / `space_roles` / `mutes` rows with `updated_by` before→after, `messages` added (count + first lines), workspace tree by hash (`MEMORY.md`, `knowledge/**`, `AGENTS.md`), `extension_state` keys. Output is the report a migration rehearsal is judged on | shadow-diff | M1.2 | **done 2026-10-01** ([archive](../../archive/rehearsal/2026-10-01-shadow-diff.md)) — `diffShadow` in `src/ops/shadow-diff.ts` is the library call M3.2 / M4.1 reuse |
52
52
  | M1.5 | `longview` gains a `file` target and shadow mode forces it; the transcript shows the page path. `capability` route refuses non-`readOnly` actions in shadow mode | longview-file-target | M1.1 | backlog |
53
53
  | M1.3b | **Shadow admin list is chosen, never inherited** (added 2026-09-13, D-015, from bug `shadow-env-keys-does-not-override-mercury-admins`): `shadow snapshot --admins <id,…>` (default empty) is what `renderShadowEnv` writes as `MERCURY_ADMINS`; `SNAPSHOT.json.shadow.adminCount` records the count, `snapshot`/`up`/`status` print it (never the ids); `up` refuses a snapshot whose manifest records no count. Landed with the shadow-up hardening track (B1 of the 2026-09-12 bench-and-gate round), which also closed: the live project read from the unit's `WorkingDirectory` / `SNAPSHOT.json` (never cwd, disagreement refuses), the cron gate's third state (unknown denies, none allows), `mercury run` on `process.execPath` + the unit `PATH` composed from the live unit / `--path` (never the shell's), and the CLI tests' scoped user bus | shadow-admins | M1.3 | **done** 2026-09-13 — post-mortems under `docs/debug/{moderate,minor}/2026-09-1{2,3}-shadow-*` and `…-mercury-run-spawns-a-bare-bun-…` |
54
54
  | M1.3c | **Shadow-own model credential** (added 2026-09-15, from bug `shadow-snapshot-auth-json-goes-stale-when-the-live-bot-rotates-its-refresh-token`): the snapshot stops copying `global/auth.json`; `up` points `MERCURY_AUTH_PATH` at a shadow-only file the owner logs in to once (`mercury auth login` against the shadow root), so the shadow never holds the live refresh token — neither dies when the other rotates. D-016's principle ("a shadow answers to no live credential") extended from the API secrets to the model credential. The `Bun.serve` 10 s stream death on `/api/shadow/simulate` (bug `shadow-simulate-stream-dies-at-bun-serve-10s-idle-timeout`) — the other reason a bench run needs a person watching — is **not** this story's: it is fixed directly as plan `bugs-0915` S1 (corrected 2026-09-15), and the bug's shape (a) mitigation (`up` re-copies the live credential, `--dry-run` gates on it) ships as `bugs-0915` S4; this story owns shape (c) | shadow-own-credential | M1.3 | backlog |
@@ -23,7 +23,7 @@
23
23
 
24
24
  | ID | Story | Slug | Depends on | Status |
25
25
  |----|-------|------|------------|--------|
26
- | M1.1 | Link table (incl. `*` target), levels, direct-edge resolver, audit table | space-link-graph-and-grant-resolver | — | backlog |
26
+ | M1.1 | Link table (incl. `*` target), levels, direct-edge resolver, audit table | space-link-graph-and-grant-resolver | — | done |
27
27
  | M1.2 | `mrctl links list/inspect/create/update/revoke/audit` + `/api/links` routes | mrctl-links-admin-commands | M1.1 | blocked |
28
28
  | M1.3 | `mrctl --space <target>`: act-as middleware over the existing routes, audit, on-demand paragraph | act-as-linked-space | M1.1, M1.2 | blocked |
29
29
  | M1.4 | `mrctl messages recent --since 24h`: time-range history read, paged | messages-recent-range | — | backlog |
@@ -75,6 +75,34 @@ answered there.** R3 is for the socket, nothing else. `pending-verification.md`
75
75
  is for what none of the rings can answer — a phone screenshot of RTL
76
76
  rendering, a group's reaction.
77
77
 
78
+ ### Reading `mercury shadow diff` (M1.4)
79
+
80
+ `mercury shadow diff [--snapshot <dir>] [--space <id>] [--json]` compares a
81
+ snapshot's frozen `reference/` (the `VACUUM INTO` copy and
82
+ `reference/manifest.json`) with the shadow's `.mercury/` **now**, read-only on
83
+ both sides. It never opens the live data dir. Without `--snapshot` it diffs the
84
+ snapshot a running shadow uses (a `SHADOW-RUN.json` claims it), else `current`,
85
+ and prints which one and why.
86
+
87
+ - **Exit codes:** `0` compared and nothing changed · `1` compared and something
88
+ changed · `2` did not compare (no `SNAPSHOT.json`, a missing reference, an
89
+ unreadable database, a space on neither side). A script refuses on non-zero.
90
+ - **Rows:** `space_config`, `project_config`, `space_preferences`, `tasks`,
91
+ `space_roles`, `mutes`, `extension_state`. Each changed row shows who wrote
92
+ it before → after, from the column that table actually has (`updated_by`,
93
+ `created_by`, `granted_by`, `muted_by`). A task deleted and re-created by an
94
+ apply is one `↻ re-created: id a → b` line — still a change.
95
+ - **Not a change:** the scheduler's own columns on `tasks` (`next_run_at`,
96
+ `last_run_at`, `last_status`, `last_error`), `updated_at` on every table,
97
+ and the files in `VOLATILE_PATHS` (`global/auth.json` — `shadow up` refreshes
98
+ the credential). Those files are listed under *volatile (not counted)*. Both
99
+ lists live in `src/ops/shadow-diff.ts`, each entry with its reason; there is
100
+ no flag to widen them.
101
+ - **Never printed:** `extension_state` values (a `sha256:` prefix instead) and
102
+ message bodies (the first line of up to 20 added messages per space).
103
+ - **Schema:** an upgrade rehearsal that migrated the shadow shows the new
104
+ table or column under *schema* and is `changed` — that is correct.
105
+
78
106
  ## 3. What can reach a person — the send-site inventory
79
107
 
80
108
  Every place a run's outcome leaves the process, as of `dev` @ `6a4cd60`.
@@ -203,9 +231,12 @@ Only for the socket-bound list in §2. Procedure:
203
231
  keys the run touched (longview `recent`, napkin `last-distill:<space>`)
204
232
  are listed and reset by hand — they are global.
205
233
  5. **Prove** the restore: diff the live DB against `lab-before.db` for the
206
- lab space and require an empty report (`shadow diff` in M1.4 is that
207
- tool; until then, `dump` both and `diff`). Write the window, the row
208
- counts and the cost into the doc that asked for the rehearsal.
234
+ lab space and require an empty report. `shadow diff` (M1.4) is not that
235
+ tool as shipped — it compares a shadow snapshot's reference with its own
236
+ working copy, never the live DB with a backup; `lab restore` (M4.1) is
237
+ planned to reuse its `diffShadow` for this. Until then, `dump` both and
238
+ `diff`. Write the window, the row counts and the cost into the doc that
239
+ asked for the rehearsal.
209
240
 
210
241
  Never in `main` (the owner's real assistant), never in a group with people
211
242
  who did not ask to be in a test, never with the football space once the
@@ -2156,3 +2156,165 @@ must land before this can pass.
2156
2156
  "@טאגולהבוט מה הוא אמר?" → the reply quotes what was said, and the trace's
2157
2157
  prompt holds `[Voice transcript — voice note from <sender>]`.
2158
2158
  - [ ] Mention the bot again right after → no second transcript of the same note.
2159
+
2160
+ ## Plan media-bugs-0928 / S2
2161
+
2162
+ `url-bridges-drop-over-limit-attachments-without-a-note` — restart only (no baked file).
2163
+
2164
+ - [ ] MANUAL-VERIFY(2026-09-29): On a Telegram, Slack, Discord or Teams space whose `media.max_size_mb` is below the file, send an image with "what is in this picture?" → the reply says the image was too large (names both sizes) instead of answering as if nothing was sent; the trace's prompt holds `a image was too large to accept (`. *(2026-09-30: not reachable on this box — `mercury.yaml` enables `whatsapp` only; slack, discord, telegram and teams are all `false`. Runs on the first box that has one of those bridges; until then the proof is `tests/bridge-limit-drops.test.ts`.)*
2165
+
2166
+ ## Plan media-bugs-0928 / S1
2167
+
2168
+ `outbox-scan-drops-files-whose-mtime-predates-the-run` — restart only (no baked file).
2169
+
2170
+ - [x] MANUAL-VERIFY(2026-09-30): In `main`, send the bot a small document, then in a new message ask it to "send that file back, copy it with `cp -p`" → the file arrives on the phone (journal: a file `WhatsApp outbound`, not only the text). *(Ticked 2026-09-30 12:11 IDT on a shadow of `dev` @ `4154ce8` (snapshot `2026-09-30T08-50-06-668Z`, derived `mercury-agent-ext:072a532bc475`), `POST /api/shadow/simulate` into `main`'s DM with a 33-byte text file: the agent ran `cp -p` from `inbox/`; the outbox copy's mtime was 12:11:15.25, before the run's container start at 12:11:17.37, and the shadow outbox recorded a `files` entry carrying `probe-note.txt` (33 bytes). Pre-fix that file was dropped. The phone leg is the same `sendReply` path the shadow journals; not re-run on the live bot. Side note: the agent's own reply still said `-p` "may not get attached" — it is not told the rule, `docs/bugs/agent-is-not-told-the-outbox-size-cap-or-plain-files-rule.md`.)*
2171
+ - [x] MANUAL-VERIFY(2026-09-30): Ask the bot in `main` to put a symlink or a folder into `outbox/` → the reply ends with `(… was not sent: …)` naming it, and nothing else is attached. *(Ticked 2026-09-30 12:11 IDT, same shadow run: the reply ended `(link.txt was not sent: it is a link, and only plain files directly in outbox/ are sent.)` and `(subdir was not sent: it is a folder, …)`; the journal logged `Outbox entry is not a regular file, not sent … kind=link` and `… kind=folder`; the only file attached was `probe-note.txt`. Shadow cost $0.11, 1 run.)*
2172
+
2173
+ ## Plan sensitive-bugs-0922 / S2
2174
+
2175
+ `sensitive-guard-replay-loses-the-dropped-attachment-note` — restart only (no baked file).
2176
+
2177
+ - [ ] MANUAL-VERIFY(2026-09-30): In a group with a `sensitive` connection, as a caller not yet confirmed there, send one small image plus one file over `media.max_size_mb` with text that needs both; answer the warning `yes` → the reply says the big file was too large instead of guessing, and the trace's replayed prompt holds the drop note. *(2026-09-30: not reachable on this box — no space has `security.sensitive_connections_allowed` set, and the only extension declaring a sensitive connection (`morning`) is not installed, so the guard never reaches its confirm path here. Needs a box with a sensitive connection enabled in a group; until then the proof is the unit suite named in the post-mortem.)*
2178
+
2179
+ ## Plan sensitive-bugs-0922 / S3
2180
+
2181
+ `sensitive-gate-is-checked-before-the-space-queue` — restart only (no baked file).
2182
+
2183
+ - [ ] MANUAL-VERIFY(2026-09-30): In a group with a `sensitive` connection where you have confirmed, start a long request, send a second one while the first is still running, then send `no` → you get "Cancelled." and, when the first run ends, the second one is answered "Cancelled." too instead of running; the journal shows `Queued turn refused at dequeue`. *(2026-09-30: not reachable on this box — no space has `security.sensitive_connections_allowed` set, and the only extension declaring a sensitive connection (`morning`) is not installed, so the guard never reaches its confirm path here. Needs a box with a sensitive connection enabled in a group; until then the proof is the unit suite named in the post-mortem.)*
2184
+
2185
+ ## Plan sensitive-bugs-0922 / S4
2186
+
2187
+ `scheduled-run-skipped-by-sensitive-deny-has-no-reader` — restart only (no baked file).
2188
+
2189
+ - [ ] MANUAL-VERIFY(2026-09-30): In a group space with a `sensitive` connection and `security.sensitive_connections_allowed` not set to true, let a recurring task there fire on its cron → the admin gets one DM saying the task was "skipped, not failed" and will be skipped again at its next run, and nothing is posted in the group. *(2026-09-30: not reachable on this box yet — no extension declaring a sensitive connection is installed, so the gate never fires here. Until then the proof is the unit suites named in the post-mortem.)* *(2026-10-02: this line used to say ▶; since the review fixes ▶ no longer pages the admin, see below.)*
2190
+ - [ ] MANUAL-VERIFY(2026-10-02): Same space, press ▶ on that task in the dashboard → a red toast says it was not run and names `security.sensitive_connections_allowed`; no DM reaches the admin and no run row is written. Same gate as above, so same reachability caveat.
2191
+ - [ ] MANUAL-VERIFY(2026-10-02): Same space, a one-time (`at`) task reaching its time → the admin DM says it was skipped **and removed** and must be created again after enabling the setting, and the task is gone from the dashboard. Same reachability caveat.
2192
+
2193
+ ## Plan search-bugs-0930 / S1
2194
+
2195
+ `brave-402-out-of-credit-is-logged-without-its-reason-and-invites-retries`: restart only. The web-search extension reaches the box as the installed snapshot, refreshed on restart. Grep `~/whatsapp-bot/.mercury/extensions/web-search/brave.ts` for `PERMANENT_UPSTREAM_STATUSES`, not the repo.
2196
+
2197
+ - [x] MANUAL-VERIFY(2026-09-30): While the Brave credit is still empty, DM the bot "search the news for X" → it makes **one** web-search call, then answers from the browser (pinchtab) or says search is unavailable. The journal's `Brave upstream error` line shows `permanent=true detail=Prepaid credit balance is insufficient…`. *(Verified 2026-09-30 13:36 IDT on `dev` @ `0518519`, trace `mercury-1790764571431-1.jsonl`: one web-search tool call carrying two queries, both answered `Brave upstream error status=402 permanent=true detail=Prepaid credit balance is insufficient for this request. Top up to continue.`; the agent got "Do not retry web-search" and made no further web-search call, answering from ynet's RSS instead. The browser leg was not exercised: the run spawned on the base image while the derived build was running, so `pinchtab_ensure` was not found — see `docs/bugs/derived-image-build-timeout-leaves-the-live-bot-on-the-base-image-until-restart.md`.)*
2198
+
2199
+ ## Plan search-bugs-0930 / S6
2200
+
2201
+ `pinchtab-helper-is-retyped-into-tmp-on-every-run` — restart only; the derived `mercury-agent-ext` image rebuilds itself on the first container run after it (its hash covers the new `bin/pinchtab_ensure`). No base-image rebuild.
2202
+
2203
+ - [x] MANUAL-VERIFY(2026-09-30): After the restart, wait for the journal's `Built derived agent image`, then `docker run --rm --entrypoint bash mercury-agent-ext:<new tag> -c 'command -v pinchtab_ensure'` → prints `/usr/local/bin/pinchtab_ensure`; and `grep -c 'pinchtab_ensure() {' ~/whatsapp-bot/.mercury/extensions/web-browser/index.ts` → prints `0`. *(Verified 2026-09-30 13:50 IDT: `Built derived agent image mercury-agent-ext:af811c796885 durationMs=459711` (after a second restart; the first build timed out at 600 s); `command -v pinchtab_ensure` in that image prints `/usr/local/bin/pinchtab_ensure`; the installed `web-browser/index.ts` has 0 matches.)*
2204
+ - [x] MANUAL-VERIFY(2026-09-30): Ask the bot in `main` to "open example.com in the browser and tell me the page title" → it answers with the title; in that run's trace no `tool_execution_start` command contains `pinchtab_ensure() {` and no tool result contains `no session set`. *(Before the fix, 2026-09-16..30 on this box: 70 of 71 browsing runs re-typed the helper in a tool call, and 42 carried the shared-tab hint in 77 tool results.)* *(Verified 2026-09-30 13:53 IDT, trace `mercury-1790765601875-1.jsonl` on `af811c796885`: the reply names "Example Domain"; `pinchtab_ensure` is called, never defined (0 matches for `pinchtab_ensure() {`), and 0 `no session set` results. The agent also tried `pinchtab eval`, which answered 403 (`security.allowEvaluate` off), and recovered with `snap`.)*
2205
+
2206
+ ## Plan search-bugs-0930 / S2
2207
+
2208
+ `agents-call-web-search-with-a-bare-string-first` (`7f1a39f`): **base-image rebuild + restart**. `src/cli/mrctl.ts` and `src/cli/mrctl-http.ts` are baked, so a restart alone deploys only the SKILL.md half. Run `cd ~/mercury-src && bash ./container/build.sh`, restart in a gap, and wait for `Built derived agent image`. Post-mortem: `docs/debug/moderate/2026-09-30-agents-call-web-search-with-a-bare-string-first.md`.
2209
+
2210
+ - [x] MANUAL-VERIFY(2026-09-30): `docker run --rm --entrypoint grep mercury-agent-ext:<new tag> -c parseCapabilityArgs /app/src/cli/mrctl.ts` → prints a number above `0`; and `grep -c "'{\"q\":" ~/whatsapp-bot/.mercury/extensions/web-search/skill/SKILL.md` → prints a number above `0`. *(Verified 2026-09-30 16:56 IDT on `mercury-agent-ext:e55fd18e2e1d` (dev `1902422`): `parseCapabilityArgs` count `2`; `mrctl capability --help` in the image prints the usage locally and a bare `"fed rates"` body is refused with `body must be one JSON object argument`; the installed and global web-search SKILL.md both carry the JSON-body description.)*
2211
+ - [x] MANUAL-VERIFY(2026-09-30): Ask the bot a news question in `football-friends` → in that run's trace the **first** `mrctl capability web-search` call carries a `'{"q":…}'` body, there is no `--help` call, and no tool result contains `body must be`. *(Verified 2026-09-30 17:18 IDT in the owner DM (`main`) rather than `football-friends` — same code path — trace `mercury-1790777912992-1.jsonl`: the first and only web-search calls were `news '{"q":"ריבית בנק ישראל החלטה","count":8,"freshness":"pm"}'` and `web '{"q":…}'`; zero `--help`, zero `body must be`. Both returned the S1 402 message (Brave still out of credit) and the agent moved to pinchtab without retrying.)*
2212
+ - [ ] MANUAL-VERIFY(2026-10-07): Count `body must be` and `capability --help` across traces newer than 2026-09-30 16:56 IDT against the baseline of 86 bare-string plus 30 `--help` errors in 48 WSL runs (09-23 → 09-30). Pass: zero of either, or a rate well under 0.1 per search run.
2213
+
2214
+ ## Plan search-bugs-0930 / S3
2215
+
2216
+ Superseded 2026-10-02: S3 was fixed twice (`docs/notes/2026-10-02-duplicate-work-on-one-bug.md`); the merged `dev` keeps the `origin/dev` version (`cd442ed`). Its checks are under "web-search run budget + Brave monthly ledger 2026-10-01" below.
2217
+
2218
+ ## Plan search-bugs-0930 / S5
2219
+
2220
+ `zap-price-lookups-are-blocked-slow-and-link-inconsistently`: restart only. The web-search skill reaches the box as the installed snapshot, refreshed on restart. Check the copy pi reads, `~/whatsapp-bot/.mercury/global/skills/web-search/`, not the repo. Post-mortem: `docs/debug/moderate/2026-10-02-zap-price-lookups-are-blocked-slow-and-link-inconsistently.md`.
2221
+
2222
+ - [x] MANUAL-VERIFY(2026-10-02): After the restart, `ls ~/whatsapp-bot/.mercury/global/skills/web-search/references/zap.md` → the file exists, and `grep -c -F '## Product prices (Zap)' ~/whatsapp-bot/.mercury/global/skills/web-search/SKILL.md` → prints `1`. *(Was the extension snapshot path with a count of 3; corrected by the 2026-10-02 review.)* *(Verified 2026-10-03 on the WSL box, dev `7baf73d`, base `mercury-agent:latest` = `09fc7e661586`, derived `mercury-agent-ext:3947692c9434` (built 13:21:31 IDT, 438 s), restart 13:14:10 IDT. Live turns through `/chat` in `main` as `api:verify-20261003`, whose seeded admin row was deleted afterwards: `zap.md` present (7726 B, 13:14) and the Zap heading count is `1`.)*
2223
+ - [x] MANUAL-VERIFY(2026-10-02): DM the bot in `main` "כמה עולה טלוויזיה 65 אינץ' עד 2,000 ש״ח בזאפ?". In that run's trace: a `read` of `references/zap.md`, at most 6 tool calls that touch zap, no `pinchtab nav` to zap.co.il, and no `-A "Mozilla/5.0"`. Every product link in the reply is `model.aspx?modelid=`. Wall time is under 1 minute. Baseline: 13–20 calls, 2–3.5 min (traces of 2026-09-24). *(Verified 2026-10-03 on the WSL box, dev `7baf73d`, base `mercury-agent:latest` = `09fc7e661586`, derived `mercury-agent-ext:3947692c9434` (built 13:21:31 IDT, 438 s), restart 13:14:10 IDT. Live turns through `/chat` in `main` as `api:verify-20261003`, whose seeded admin row was deleted afterwards: 35 s wall, trace `mercury-1791022471634-1`: `cat …/references/zap.md`, 3 `zap.co.il` curl calls, no `pinchtab` and no `Mozilla` in any tool argument, and all 7 product links are `model.aspx?modelid=`.)*
2224
+
2225
+ ## content-filter block closes the turn 2026-09-30 — restart-deployed
2226
+
2227
+ Host-only (`src/core/runtime.ts`, `user-error-messages.ts`, `system-messages.ts`;
2228
+ none baked), so a restart deploys it
2229
+ (`docs/debug/major/2026-09-30-provider-content-filter-block-wedges-the-conversation.md`).
2230
+
2231
+ - [x] MANUAL-VERIFY(2026-09-30): In a test DM, ask for the full lyrics of a well-known song. The reply must say the content filter blocked the answer and must not say "try again". Then send "פה?": the bot must answer it normally. The newest `assistant` row in `messages` for that space starts with `[System: the reply to this message was blocked`.
2232
+ - 2026-10-01 (superseded by the next bullet): **first attempt, not exercised.** Tried in the `main` DM on 2026-09-30 at 17:25 IDT (rows 8321-8324). The model declined the lyrics by itself ("I can't send you the full lyrics. They're copyrighted…"), so the provider never blocked anything. There is no content-filter `ContainerError` in the journal, and there are zero `[System: the reply to this message was blocked` rows in the DB. "פה?" got a normal answer ("כן, פה. מה צריך?"), but that proves nothing about the fix. This needs a prompt that the provider filter actually blocks, or waiting for one to happen naturally.
2233
+ - 2026-10-01 18:03 IDT: **passed.** Re-sent the original trigger in `main` (row 8483: "What's the song that goes 'I'm free to do whatever I want'... Oasis?"). The journal has `Container error … "Output blocked by content filtering policy"` (container `mercury-1790866971481-9`). The outbound reply was the Hebrew `err_content-filter_platform` text ("…שליחה חוזרת של אותה בקשה לא תעבור"), which does not say "try again". Row 8484 is the `[System: the reply to this message was blocked …]` stub. Then "פה?" (row 8485) was answered normally in 10 s (row 8486, container `-10`, exit 0). It did not wedge, and the model offered a summary instead of the lyrics.
2234
+
2235
+ ## derived-image build retry 2026-09-30
2236
+
2237
+ A failed or timed-out derived-image build is now retried with backoff instead of
2238
+ leaving every run on the base image until a restart. The build timeout went from
2239
+ 600 s to 1800 s, and `/health` gained an `extImageBuild` field. All of this is
2240
+ host-side, so a restart deploys it with no base rebuild
2241
+ (`docs/debug/moderate/2026-09-30-derived-image-build-timeout-leaves-the-live-bot-on-the-base-image-until-restart.md`).
2242
+
2243
+ - [x] MANUAL-VERIFY(2026-09-30): After the restart that deploys this, run `curl -s http://127.0.0.1:8787/health` → `extImageBuild.state` is `building` and then `ready`, and its `image` is a `mercury-agent-ext:` tag. The journal has `Built derived agent image` and no `timed out after 600000ms`. *(Verified 2026-09-30 on dev `1902422`: restart 16:48:40 IDT, `/health` 20 s later showed `extImageBuild` `building`, attempt 1, image `mercury-agent:latest`; `Built derived agent image mercury-agent-ext:e55fd18e2e1d durationMs=418389` at 16:55:41 on the first attempt, then `ready` with that image; no `timed out` line. This was a cold build — the first since the cache mounts, so it filled them.)*
2244
+ - [ ] MANUAL-VERIFY(2026-09-30): The next time a derived build fails on the box, the journal shows `Retrying in …s` after it. A later `Derived agent image recovered on attempt N` line then appears with no restart in between.
2245
+ - [ ] MANUAL-VERIFY(2026-09-30): After the next base rebuild, the second derived build since this deploy (the first one fills the caches) logs `Built derived agent image … durationMs=` under ~400000 in `journalctl --user -u mercury`, with no `timed out` line before it.
2246
+
2247
+ ## web-search run budget + Brave monthly ledger 2026-10-01 — restart-deployed
2248
+
2249
+ Host-side code in `examples/extensions/web-search/` and `src/`, with nothing baked into the image, so a restart deploys it. The skill text reaches the agent only after the extension is reinstalled on each box. Post-mortems: `docs/debug/moderate/2026-10-01-one-run-can-fire-hundreds-of-web-search-calls.md` and `docs/debug/moderate/2026-10-01-brave-credit-balance-is-invisible-until-it-is-gone.md`.
2250
+
2251
+ - [x] MANUAL-VERIFY(2026-10-01): After the restart, the first `web-search: Brave query` journal line shows `monthUsed=1`. If it also shows `braveMonthlyRemaining`, check that number against the Brave dashboard's remaining credit. If the two disagree, the header does not mean what `parseBraveQuota` assumes, so stop trusting the logged figure. *(Verified 2026-10-02 on the WSL box, dev `51ee551`, restart 19:24:27 IDT: the first query after it, 20:08:44 in `main` (container `mercury-1790960917032-1`), logged `runBilled=1 monthUsed=1 monthBudget=1000`, and the next run's query (20:52:30, `-3`) logged `monthUsed=2`. No `braveMonthlyRemaining` on either line, so the dashboard comparison does not apply. The owner has not yet picked this box's share of the key (next line). The Tagula box is not deployed yet.)*
2252
+ - [ ] MANUAL-VERIFY(2026-10-01): On each box, set `web-search.monthly_budget` in the global scope to that box's share of the key (the two bots share one grant). Then, on a test box, set it to 1 and run two searches in a DM. The second is refused with "budget for … is spent", and the admin's DM gets one "[web-search] … 100%" message.
2253
+ - [x] MANUAL-VERIFY(2026-10-02): Review fixes ported 2026-10-02. In one live run, the same web-search query sent twice at once logs exactly one `web-search: Brave query` line for it. Needs Brave credit. *(Verified 2026-10-03 on the WSL box, dev `7baf73d`, base `mercury-agent:latest` = `09fc7e661586`, derived `mercury-agent-ext:3947692c9434` (built 13:21:31 IDT, 438 s), restart 13:14:10 IDT. Live turns through `/chat` in `main` as `api:verify-20261003`, whose seeded admin row was deleted afterwards: with `run_cap`=2, one bash fired the same `ירושלים` body twice at once plus `חיפה` and `באר שבע`. Two `Brave query` lines were logged (13:23:16, 13:23:17, `runBilled=1`/`2`), both duplicates returned results, and `באר שבע` got the 429. A second Jerusalem call would have made three billed calls and refused one of the first three, so the duplicate was one API call.)*
2254
+ - [x] MANUAL-VERIFY(2026-10-02): After a run is refused with 429 by its run budget, repeating a query it already searched returns the results, not the 429. Needs Brave credit. *(2026-10-02 21:12 IDT, `main`, `run_cap`=2 set from the dashboard, trace `mercury-1790964735117-4`: **not exercised.** The agent sent its three distinct queries at the same moment. Exactly two were billed (`runBilled=2`, `monthUsed=3`/`4`) and the third got the 429 (`run budget spent … runCap=2`), so concurrent calls do not overshoot the cap. But it then did the repeat in the browser on `search.brave.com`, as the 429 text invites, and never re-asked the API. See `docs/debug/moderate/2026-10-02-pinchtab-system-prompt-sends-searches-to-search-brave-com-past-web-search.md`. That bug is fixed in `744c0b5`, so retry once it is deployed.)* *(Verified 2026-10-03 on the WSL box, dev `7baf73d`, base `mercury-agent:latest` = `09fc7e661586`, derived `mercury-agent-ext:3947692c9434` (built 13:21:31 IDT, 438 s), restart 13:14:10 IDT. Live turns through `/chat` in `main` as `api:verify-20261003`, whose seeded admin row was deleted afterwards: same turn as above: after `באר שבע` got the run-budget 429, the repeated `ירושלים` call returned its results (exit 0), not the 429.)*
2255
+ - [x] MANUAL-VERIFY(2026-10-02): In the dashboard, setting `web-search.run_cap` to a 400-digit number is rejected. *(Verified 2026-10-03 on the WSL box, dev `7baf73d`, base `mercury-agent:latest` = `09fc7e661586`, derived `mercury-agent-ext:3947692c9434` (built 13:21:31 IDT, 438 s), restart 13:14:10 IDT. Live turns through `/chat` in `main` as `api:verify-20261003`, whose seeded admin row was deleted afterwards: `POST /dashboard/api/space-config` and `/dashboard/api/global-config` with 400 nines both returned 400 `Invalid value for web-search.run_cap`. The console's keyed `PUT` would not reject it; filed as `docs/bugs/console-config-put-skips-extension-key-validation.md`.)*
2256
+
2257
+ ## pinchtab prompt stops sending searches to search.brave.com 2026-10-02
2258
+
2259
+ The browser extension's system-prompt fragment no longer names a search engine,
2260
+ web-search's fragment states the one search rule, and its four refusals stop
2261
+ pointing at "the pinchtab browser path". All of it is extension code that runs
2262
+ on the host or in the installed snapshot under `.mercury/extensions/`, so a
2263
+ restart deploys it — no image rebuild
2264
+ (`docs/debug/moderate/2026-10-02-pinchtab-system-prompt-sends-searches-to-search-brave-com-past-web-search.md`).
2265
+
2266
+ - [x] MANUAL-VERIFY(2026-10-02): After the restart, `grep -c -F 'search.brave.com/search?q=your+query' ~/whatsapp-bot/.mercury/extensions/web-browser/index.ts` prints 0 and `grep -c -F SEARCH_ROUTING_PROMPT ~/whatsapp-bot/.mercury/extensions/web-search/index.ts` prints at least 1. *(Verified 2026-10-03 on the WSL box, dev `7baf73d`, base `mercury-agent:latest` = `09fc7e661586`, derived `mercury-agent-ext:3947692c9434` (built 13:21:31 IDT, 438 s), restart 13:14:10 IDT. Live turns through `/chat` in `main` as `api:verify-20261003`, whose seeded admin row was deleted afterwards: counts `0` and `2`.)*
2267
+ - [x] MANUAL-VERIFY(2026-10-02): Ask `main` a fact question whose first results are weak (e.g. "מה קרה בדיון האחרון בכנסת על חוק הגיוס?"). In its trace, `grep -o -F 'pinchtab nav "https://search.brave.com' <trace> | wc -l` counts 0, and every keyword search is an `mrctl capability web-search` call (no Google, Bing or DuckDuckGo results page in the browser). Needs Brave credit. *(Was a price question; prices now go to Zap and are checked under "Plan search-bugs-0930 / S5".)* *(Verified 2026-10-03 on the WSL box, dev `7baf73d`, base `mercury-agent:latest` = `09fc7e661586`, derived `mercury-agent-ext:3947692c9434` (built 13:21:31 IDT, 438 s), restart 13:14:10 IDT. Live turns through `/chat` in `main` as `api:verify-20261003`, whose seeded admin row was deleted afterwards: 50 s, trace `mercury-1791022764552-5`: three `mrctl capability web-search` calls, zero `search.brave.com`, and no Google, Bing or DuckDuckGo. Two `pinchtab` calls read an article; they failed because that run spawned on the base image while the derived image was still building, and the agent fell back to `curl`.)*
2268
+ - [x] MANUAL-VERIFY(2026-10-02): With `web-search.run_cap` at 2, ask for three searches at once. After the 429, the agent answers from the two results and does not open search.brave.com, Google, Bing or DuckDuckGo in the browser. Needs Brave credit. *(Verified 2026-10-03 on the WSL box, dev `7baf73d`, base `mercury-agent:latest` = `09fc7e661586`, derived `mercury-agent-ext:3947692c9434` (built 13:21:31 IDT, 438 s), restart 13:14:10 IDT. Live turns through `/chat` in `main` as `api:verify-20261003`, whose seeded admin row was deleted afterwards: `run_cap`=2, "מזג אוויר / מכבי / ביטקוין", trace `mercury-1791022935747-7`: two billed queries, then `run budget spent … runCap=2`. The agent then read sport5, Open-Meteo and CoinGecko directly, with zero `search.brave.com` and no other engine. `run_cap` was removed afterwards.)*
2269
+
2270
+ ## Review follow-ups to the 2026-10-02 parallel sessions — image rebuild + restart
2271
+
2272
+ The review of the seven 2026-10-02 fixes landed follow-ups on `dev`. Three baked files changed again (`src/agent/container-entry.ts`, `pi-jsonl-parser.ts`, `pi-failure-class.ts`), so this is **an image rebuild** (`cd ~/mercury-src && bash ./container/build.sh`), then a restart in a gap. Everything else is host-side or installed-snapshot and follows the restart. Wait for the journal's `Built derived agent image` line before grepping the derived image.
2273
+
2274
+ - [x] MANUAL-VERIFY(2026-10-02): After the rebuild and the `Built derived agent image` line, `docker run --rm --entrypoint sh <derived mercury-agent-ext tag from that line> -c 'grep -c MAX_TOKENS_STOP_MARKER /app/src/agent/pi-failure-class.ts /app/src/agent/pi-jsonl-parser.ts; grep -c -E "PiExitFailure|servedBy|classifyAttemptFailure" /app/src/agent/container-entry.ts; grep -c authRejected /app/src/agent/container-entry.ts'` prints a non-zero count on every line. This is 6e5fab3 + 257215b + their review follow-up inside the image. *(Verified 2026-10-03 on the WSL box, dev `7baf73d`, base `mercury-agent:latest` = `09fc7e661586`, derived `mercury-agent-ext:3947692c9434` (built 13:21:31 IDT, 438 s), restart 13:14:10 IDT. Live turns through `/chat` in `main` as `api:verify-20261003`, whose seeded admin row was deleted afterwards: on `mercury-agent-ext:3947692c9434` the counts are 2 / 2 / 10 / 2.)*
2275
+ - [ ] MANUAL-VERIFY(2026-10-02): For a week after the deploy, `mercury service logs` shows no `Credential failure alerted to operators` line for a run whose only "401"/"unauthorized" was in tool output, and no operator DM "rejected Mercury's credential" while every run is succeeding.
2276
+ - [ ] MANUAL-VERIFY(2026-10-02): The next scheduled run that hits a length stop logs `Scheduled task not retried: max-tokens runaway` and leaves exactly one `task_runs` row for that due cycle (`status=error`, error containing "max tokens limit").
2277
+ - [x] MANUAL-VERIFY(2026-10-02): After the restart, run `mercury preflight` from `~/whatsapp-bot`: the `host.pi` row shows version `0.87.1`, pinned `~0.87.1`, verdict pass. *(Verified 2026-10-03 on the WSL box, dev `7baf73d`, base `mercury-agent:latest` = `09fc7e661586`, derived `mercury-agent-ext:3947692c9434` (built 13:21:31 IDT, 438 s), restart 13:14:10 IDT. Live turns through `/chat` in `main` as `api:verify-20261003`, whose seeded admin row was deleted afterwards: `mercury preflight` returned 10 pass / 0 warn / 0 blocked / 2 skipped, `host.pi` "Host pi 0.87.1 is on the host and inside the pinned range".)*
2278
+ - [x] MANUAL-VERIFY(2026-10-02): After the restart, send the bot a WhatsApp image: it arrives normally, and `mercury service logs` shows `Downloaded media` with no `WhatsApp media read bound fired` line. *(Verified 2026-10-03 on the WSL box, dev `7baf73d`, base `mercury-agent:latest` = `09fc7e661586`, derived `mercury-agent-ext:3947692c9434` (built 13:21:31 IDT, 438 s), restart 13:14:10 IDT. Live turns through `/chat` in `main` as `api:verify-20261003`, whose seeded admin row was deleted afterwards: a real inbound image at 13:16:20 IDT logged `Downloaded media … type=image sizeBytes=200590`, and the journal since the restart has no `WhatsApp media read bound fired`.)*
2279
+ - [x] MANUAL-VERIFY(2026-10-02): After the restart, `grep -c -F 'braveApiKey(rawKey)' ~/whatsapp-bot/.mercury/extensions/web-search/index.ts` prints 1, `grep -c -F 'not from web-search or the browser' ~/whatsapp-bot/.mercury/extensions/web-search/index.ts` prints 0, and `grep -c -F 'חנות אחת: \2' ~/whatsapp-bot/.mercury/global/skills/web-search/references/zap.md` prints 1. *(Verified 2026-10-03 on the WSL box, dev `7baf73d`, base `mercury-agent:latest` = `09fc7e661586`, derived `mercury-agent-ext:3947692c9434` (built 13:21:31 IDT, 438 s), restart 13:14:10 IDT. Live turns through `/chat` in `main` as `api:verify-20261003`, whose seeded admin row was deleted afterwards: counts `1`, `0`, `1`.)*
2280
+ - [ ] MANUAL-VERIFY(2026-10-02): DM `main` "כמה עולה מכונת כביסה 8 קילו בזאפ?". In the trace, the step-3 summary line shows `rows` equal to `cards`, and any `חנות אחת` row the reply uses links `fs.aspx?pid=` or `shop.zap.co.il`, never `search.aspx`. *(Verified 2026-10-03 on the WSL box, dev `7baf73d`, base `mercury-agent:latest` = `09fc7e661586`, derived `mercury-agent-ext:3947692c9434` (built 13:21:31 IDT, 438 s), restart 13:14:10 IDT. Live turns through `/chat` in `main` as `api:verify-20261003`, whose seeded admin row was deleted afterwards: **fails on one card.** Trace `mercury-1791022527162-2`: the 1,000–1,500 ₪ listing printed `rows: 24 cards: 25`. The missed card is a `card-v2--one-store-model` labelled `למפרט טכני …`, with no `לקנייה באתר`; filed as `docs/bugs/zap-extractor-misses-one-store-cards-labelled-as-spec-links.md`. The reply disclosed the gap but did not use the browser fallback, and that run had no browser (base image). No `חנות אחת` row reached the reply. Re-run after that fix.)*
2281
+ - [x] MANUAL-VERIFY(2026-10-02): DM `main` "כמה עולה טיסה לאתונה בנובמבר?". The run reads `references/flights.md` and does not read `references/zap.md`. *(Verified 2026-10-03 on the WSL box, dev `7baf73d`, base `mercury-agent:latest` = `09fc7e661586`, derived `mercury-agent-ext:3947692c9434` (built 13:21:31 IDT, 438 s), restart 13:14:10 IDT. Live turns through `/chat` in `main` as `api:verify-20261003`, whose seeded admin row was deleted afterwards: 16 s, trace `mercury-1791022741445-4`: it read `references/flights.md` and never `zap.md`, and asked for dates before searching.)*
2282
+ - [x] MANUAL-VERIFY(2026-10-02): `curl -s -o /dev/null -w '%{http_code}' -X PUT -H "Authorization: Bearer $MERCURY_API_SECRET" -H 'Content-Type: application/json' -d '{"key":"security.confirmed_sensitive","value":"{}"}' http://127.0.0.1:8787/api/console/spaces/main/config` prints `400` (run it from a script file so the variable is not stripped, and never echo the secret). *(Verified 2026-10-03 on the WSL box, dev `7baf73d`, base `mercury-agent:latest` = `09fc7e661586`, derived `mercury-agent-ext:3947692c9434` (built 13:21:31 IDT, 438 s), restart 13:14:10 IDT. Live turns through `/chat` in `main` as `api:verify-20261003`, whose seeded admin row was deleted afterwards: 400 `security.confirmed_sensitive is managed by Mercury`.)*
2283
+ - [ ] MANUAL-VERIFY(2026-10-03): after the next sync (restart only), the first football article logs no `date-in-window` on a future match date: `journalctl --user -u mercury --since "<the restart's time>" | grep -F "reporter-lint: finding" | grep -F "date-in-window"` shows no "is NNNNh old" on a date that is still ahead (not `--since today`: the 2026-10-03 pre-fix lines would read as a failure) (fix `148205d`, post-mortem `docs/debug/minor/2026-10-03-article-lint-reads-a-future-fixture-date-as-thousands-of-hours-stale.md`).
2284
+ - [ ] MANUAL-VERIFY(2026-10-03): Same image rebuild as above (`container-entry.ts` changed again): `docker run --rm --entrypoint sh <derived mercury-agent-ext tag> -c 'grep -c -F "if (err.kind === \"max-tokens\") kind = \"max-tokens\"" /app/src/agent/container-entry.ts'` prints 1. Then the next length-stopped scheduled run on a multi-leg chain logs `Scheduled task not retried: max-tokens runaway` even though its error text starts `Model chain budget exceeded`. *(Verified 2026-10-03 on the WSL box, dev `7baf73d`, base `mercury-agent:latest` = `09fc7e661586`, derived `mercury-agent-ext:3947692c9434` (built 13:21:31 IDT, 438 s), restart 13:14:10 IDT. Live turns through `/chat` in `main` as `api:verify-20261003`, whose seeded admin row was deleted afterwards: first half verified: the grep prints `1` on `mercury-agent-ext:3947692c9434`. The length-stopped-run half is still open.)*
2285
+ - [x] MANUAL-VERIFY(2026-10-03): After the restart, `grep -c -F 'PRICE_ROUTING_PROMPT' ~/whatsapp-bot/.mercury/extensions/web-search/index.ts` prints a non-zero count (a keyless box keeps the Zap price pointer). *(Verified 2026-10-03 on the WSL box, dev `7baf73d`, base `mercury-agent:latest` = `09fc7e661586`, derived `mercury-agent-ext:3947692c9434` (built 13:21:31 IDT, 438 s), restart 13:14:10 IDT. Live turns through `/chat` in `main` as `api:verify-20261003`, whose seeded admin row was deleted afterwards: count `3`.)*
2286
+
2287
+ ## Plan unattended-1003 / S7
2288
+
2289
+ `pinchtab_nav` and the session injector now ship as files in the pinchtab extension's `bin/`. They reach the box through the installed snapshot, which is refreshed on restart, and from there into the **derived** image, whose hash includes the bin files. So this is a restart, then wait for the journal's `Built derived agent image` line. No base rebuild.
2290
+
2291
+ - [x] *(Verified 2026-10-04 at deploy `e97341f`: the installed snapshot is `extensions/web-browser/` (catalog name; source `pinchtab`), and its injector has the Bearer header (1). Derived `mercury-agent-ext:b2c39b0f2f05`: both bins executable, `X-Agent-Id` 1.)* MANUAL-VERIFY(2026-10-03): After the restart, `grep -c -F 'Authorization: `Bearer' ~/whatsapp-bot/.mercury/extensions/pinchtab/bin/pinchtab_session_inject.cjs` prints 1. Then, after `Built derived agent image`, `docker run --rm --entrypoint sh <new mercury-agent-ext tag> -c 'test -x /usr/local/bin/pinchtab_nav && test -x /usr/local/bin/pinchtab_session_inject && grep -c -F "X-Agent-Id" /usr/local/bin/pinchtab_session_inject'` prints 1.
2292
+ - [x] *(Verified 2026-10-04: this box sets no `MERCURY_BROWSER_SESSIONS`, so it was rehearsed on a shadow with a planted example.com session. The trace shows `/cookies answered 403 … cookies_disabled` and the fallback notice, and the reply said the saved session could not be used because cookie access is off, and that the page loaded without the login.)* MANUAL-VERIFY(2026-10-03): On a box with `MERCURY_BROWSER_SESSIONS` set, a browse of a saved site prints `/cookies answered 403 … cookies_disabled` and the fallback notice in the trace, and the reply tells the user the page was read without their login. It must never be a silent success. This stays the expected result until `docs/bugs/pinchtab-cookies-and-evaluate-endpoints-are-off-so-sessions-cannot-inject.md` is decided.
2293
+
2294
+ ## Plan unattended-1003 / S5
2295
+
2296
+ - [x] *(Verified 2026-10-04: grep prints 2. This box has WhatsApp ingress only, so the deployed `downloadMediaFromUrl` was run against a local stalling server with the production bounds: body stall and open stall each fired `Media download bound fired` at 30.0 s, reported `download_failed` to `onSkipped`, and the server saw the socket close at 30.0 s.)* MANUAL-VERIFY(2026-10-03): after the next sync (restart only), `grep -c -F "Media download bound fired" ~/mercury-src/src/core/media.ts` prints 2. When a Discord/Slack/Teams attachment download next stalls, the journal shows `Media download bound fired` and the agent's reply mentions the file could not be downloaded, instead of the message never arriving (fix `core-media-url-download-has-no-time-bound`, post-mortem `docs/debug/minor/2026-10-03-core-media-url-download-has-no-time-bound.md`).
2297
+
2298
+ ## Plan unattended-1003 / S3
2299
+
2300
+ - [x] *(Verified 2026-10-04: football-reporter and tel-aviv-flat-search clean, _template not applicable; no `is owned by the runtime`.)* MANUAL-VERIFY(2026-10-03): after the restart, run `space-profile check` for every live profile (from `~/whatsapp-bot`) — each one still loads; none fails with `is owned by the runtime` (consent-rows-are-writable-by-space-profile-apply).
2301
+ - [x] *(Verified 2026-10-04: prints `true`; no `security` folder.)* MANUAL-VERIFY(2026-10-03): `~/.bun/bin/bun -e 'import("/home/guysaha/mercury-src/src/extensions/reserved.ts").then(m=>console.log(m.RESERVED_EXTENSION_NAMES.has("security")))'` prints `true`, and `ls ~/whatsapp-bot/.mercury/extensions/` shows no `security` folder (one would now stop the bot from loading extensions).
2302
+
2303
+ ## Plan unattended-1003 / S9
2304
+
2305
+ `space-link-graph-and-grant-resolver` (space-links M1.1) — restart only (no baked file; the migration runs at boot).
2306
+
2307
+ - [x] *(Verified 2026-10-04: both tables present; boot clean (preflight 9 pass / 0 warn / 0 blocked, no ERROR lines).)* MANUAL-VERIFY(2026-10-03): After the restart that deploys this, open the live `state.db` read-only and confirm both tables `space_links` and `space_link_audit` exist (`.tables`), and the journal shows a clean boot.
2308
+
2309
+ ## Plan unattended-1003 / S4
2310
+
2311
+ Fix for `gemini-leg-refused-no-google-credential-env-var`. Host-only (`src/storage/pi-auth.ts`, `src/agent/container-runner.ts`; neither is a `COPY` line, no version bump), so a **restart** in a gap deploys it. No image rebuild.
2312
+
2313
+ - [x] *(Verified 2026-10-04: admin switched to `google:gemini-3.1-flash-lite`, one message answered (token_usage row 3362 google/gemini-3.1-flash-lite), no `no credential for model leg google:` line, no Gemini API error; switched back to leg 1.)* MANUAL-VERIFY(2026-10-03): After the restart, in a test space: `mercury chat -s admin "/model switch <the google:gemini leg>"`, then one message. The reply arrives, and `mercury service logs` has no `no credential for model leg google:` line for it. Switch back with `/model switch 1`. This is also the first real turn on `@google/genai` 2.21 (pi-ai 0.87.1 moved it from 1.52), so a Gemini API error in that turn is a separate finding; record it, do not count it as this fix failing.
2314
+
2315
+ ## Plan unattended-1003 / S1
2316
+
2317
+ A refused credential now falls back to the next model leg and still alerts the operator. `container-entry.ts` and `pi-failure-class.ts` are baked, so this needs **an image rebuild** (`cd ~/mercury-src && bash ./container/build.sh`) **and a restart in the same gap**: a host older than this commit cannot read the new `authRejected` list and would alert nothing (`docs/debug/moderate/2026-10-03-auth-rejection-never-falls-back-to-the-next-model-leg.md`).
2318
+
2319
+ - [x] *(Verified 2026-10-04 in derived `mercury-agent-ext:b2c39b0f2f05`: 1, then 7.)* MANUAL-VERIFY(2026-10-03): After the rebuild and the `Built derived agent image` line, `docker run --rm --entrypoint sh <derived mercury-agent-ext tag> -c 'grep -c -F "refused.push(leg)" /app/src/agent/container-entry.ts; grep -c -F "return \"fallbackable\"" /app/src/agent/pi-failure-class.ts'` prints 1, then a count of at least 7.
2320
+ - [x] *(Verified 2026-10-04 on a shadow (live spaces are all pinned, so no live run can reach a second leg): leg 1 `groq` with an invalid `MERCURY_GROQ_API_KEY`, leg 2 Opus, `main` unpinned. The user got an answer, the journal showed `Model provider rejected the run's credential … provider=groq … runOk=true`, and the alert fired, logged as undeliverable because a shadow has no operators. The once-per-cooldown half needs a delivered alert and was not exercised.)* MANUAL-VERIFY(2026-10-03): The next time a leg's credential is refused while a later leg works, the user gets an answer and the journal shows `Model provider rejected the run's credential … runOk=true` followed by one `Credential failure alerted to operators` line. Only one per cooldown, however many messages follow.
@@ -1,7 +1,7 @@
1
1
  # Plan: flat-0916
2
2
 
3
- **Status**: Active
4
- **Created**: 2026-09-16 **Last updated**: 2026-09-16
3
+ **Status**: Archived
4
+ **Created**: 2026-09-16 **Last updated**: 2026-09-28
5
5
  **Landing mode**: pr
6
6
  **Max width**: 4
7
7
  **Inputs**: flat-0916 video-understanding-extension feed-watch-owns-the-roundup
@@ -15,8 +15,6 @@
15
15
 
16
16
  | ID | Kind | Slug | Doc | Command | Round | Needs |
17
17
  |----|------|------|-----|---------|-------|-------|
18
- | S1 | plan | video-understanding-extension | docs/ideas/video-understanding-extension.md | /f-feature-planning video-understanding-extension | 0 | — |
19
- | S2 | plan | feed-watch-owns-the-roundup | docs/ideas/feed-watch-owns-the-roundup.md | /f-feature-planning feed-watch-owns-the-roundup | 0 | — |
20
18
 
21
19
  > Kind ∈ `feature` | `bug` | `refactor` | `plan` (a Round 0 planning session) | `story` (from `--goal`; runs `/f-feature-dev`).
22
20
  > Needs = decision IDs from `decisions.md` that must be answered before this session may launch, or `—`.
@@ -86,3 +84,5 @@
86
84
 
87
85
  | Item | Reason | Home |
88
86
  |------|--------|------|
87
+ | S1 video-understanding-extension | dropped by user 2026-09-28 — moved to plan general-0928 (video is general, not flat-specific) | docs/ideas/video-understanding-extension.md |
88
+ | S2 feed-watch-owns-the-roundup | dropped by user 2026-09-28 — parked with the flat space (on hold since 2026-09-22) | docs/ideas/feed-watch-owns-the-roundup.md |
@@ -0,0 +1,59 @@
1
+ # Brief S1 — the outbox scan drops files whose mtime predates the run
2
+
3
+ **Plan**: docs/plans/media-bugs-0928/plan.md **Session**: S1 **Round**: 1 **Kind**: bug
4
+ **Item slug**: outbox-scan-drops-files-whose-mtime-predates-the-run
5
+
6
+ > The only file this session reads from the plan. Self-contained: it never points at a sibling's brief. The `**Item slug**:` line is the key the execution commands grep for to detect that a brief is active — keep it exactly in that shape.
7
+
8
+ ## Launch
9
+
10
+ /f-ops-plan work media-bugs-0928 S1
11
+
12
+ ## Run
13
+
14
+ /f-bug-fix outbox-scan-drops-files-whose-mtime-predates-the-run
15
+
16
+ ## Scope notes
17
+
18
+ The agent's `cp -p` / `mv` / `rsync -a` into `outbox/` keeps an old mtime, `scanOutbox` filters on `mtimeMs >= startTimeMs`, the file is not sent, and the model says "attached". Fix candidates are in the doc (`max(mtimeMs, ctimeMs)`, or a pre-run snapshot of `outbox/`). **The half that matters as much as the filter:** a file present in `outbox/` and *not* sent (age, the 25 MB cap, a dotfile) must produce a log line **and** something the reply path surfaces, so a delivery claim can't outrun the host — "a limit the actor cannot read is a trap". Enumerate every skip in `scanOutbox` before writing the first fix. Check `tests/inventories/host-limits.ts` for an outbox row and name its reader. **Deploy needs:** classify with release-gate §1 — `src/core/outbox.ts` and `src/agent/container-runner.ts` are host-side; confirm neither is a `COPY` source in `container/Dockerfile`.
19
+
20
+ ## Owned surfaces (predicted)
21
+
22
+ - src/agent/container-runner.ts
23
+ - src/core/outbox.ts
24
+
25
+ ## Do not touch
26
+
27
+ - src/bridges/discord.ts, src/bridges/slack.ts, src/bridges/teams.ts, src/bridges/telegram.ts, src/bridges/whatsapp.ts, src/core/media.ts, tests/inventories/host-limits.ts (owned by S2 this plan)
28
+ - scripts/absent-allows-baseline.json, scripts/check-absent-allows.ts, src/adapters/whatsapp-media.ts, src/core/media.ts (owned by S3 this plan)
29
+ - `src/core/runtime.ts` and `tests/sensitive-guard-*` (owned by plan `sensitive-bugs-0922`, running in parallel)
30
+ - Not ours: see plan.md "Not ours" (nothing intersects). Re-check with `/f-ops-plan next media-bugs-0928`.
31
+
32
+ ## Edges
33
+
34
+ - start-after: none (satisfied — `next` launched you)
35
+ - merge-after: none
36
+
37
+ ## Overlap verdicts
38
+
39
+ none
40
+
41
+ > The command's preflight overlap check treats a hit whose branch AND every shared file match a verdict line as answered (c), prints one `PLAN-PASSED(overlap): …` line, and carries it into the final report. Any other hit prompts as usual. The merge-time check is never waived.
42
+
43
+ ## Decisions answered
44
+
45
+ - none
46
+
47
+ ## Ledger rules
48
+
49
+ - `docs/ROADMAP.md`: never edit. Your command writes `docs/pending-updates/outbox-scan-drops-files-whose-mtime-predates-the-run.md` as usual.
50
+ - `docs/pending-verification.md`: append only under `## Plan media-bugs-0928 / S1`.
51
+ - Any goal's `decisions.md`: never edit; put decisions in your log's "Siblings must know".
52
+
53
+ ## Environment
54
+
55
+ - Read `CLAUDE.md` → Worktree Setup and Gotchas before opening the worktree. Nothing from them is copied here. `bun install` once in the fresh worktree; the check gate is `bun run check` (every link runs to completion — read every line).
56
+
57
+ ## Report
58
+
59
+ Your command's final report is appended to `docs/plans/media-bugs-0928/sessions/S1.md` by its report step (path resolved through `.claude/scripts/resolve-docs-root.sh docs/plans/probe.md`). `work` verifies the append.
@@ -0,0 +1,64 @@
1
+ # Brief S2 — URL bridges drop an over-limit attachment with only a log line
2
+
3
+ **Plan**: docs/plans/media-bugs-0928/plan.md **Session**: S2 **Round**: 1 **Kind**: bug
4
+ **Item slug**: url-bridges-drop-over-limit-attachments-without-a-note
5
+
6
+ > The only file this session reads from the plan. Self-contained: it never points at a sibling's brief. The `**Item slug**:` line is the key the execution commands grep for to detect that a brief is active — keep it exactly in that shape.
7
+
8
+ ## Launch
9
+
10
+ /f-ops-plan work media-bugs-0928 S2
11
+
12
+ ## Run
13
+
14
+ /f-bug-fix url-bridges-drop-over-limit-attachments-without-a-note
15
+
16
+ ## Scope notes
17
+
18
+ Every attachment a limit removes must reach `dropped` with its reason (`too_large` / `quota`), the way WhatsApp direct media, `/api/chat` and the shadow bridge already do. The legs are enumerated in the doc: the three `return null` size paths in `downloadMediaFromUrl` (`src/core/media.ts`, need `onSkipped`), and the quota branches in `slack.ts`, `telegram.ts`, `discord.ts` and WhatsApp's quoted-media branch; `teams.ts` never calls `isOverQuota` at all — part of this fix. The doc's note that `src/bridges/` is owned by plan `bugs-0915` is stale: that plan is archived. When fixed, rewrite the `mediaMaxSizeMb` / `maxDiskMb` rows of `tests/inventories/host-limits.ts` to name the reader — the inventory test goes red on purpose when this doc leaves `docs/bugs/`. A test per bridge (over-limit and over-quota → exactly one `dropped` entry with the right reason), negative-tested by deleting the push. **Leave the unbounded `arrayBuffer()` read alone** — that is S3's bug, next in this file.
19
+
20
+ ## Owned surfaces (predicted)
21
+
22
+ - src/bridges/discord.ts
23
+ - src/bridges/slack.ts
24
+ - src/bridges/teams.ts
25
+ - src/bridges/telegram.ts
26
+ - src/bridges/whatsapp.ts
27
+ - src/core/media.ts
28
+ - tests/inventories/host-limits.ts
29
+
30
+ ## Do not touch
31
+
32
+ - src/agent/container-runner.ts, src/core/outbox.ts (owned by S1 this plan)
33
+ - scripts/absent-allows-baseline.json, scripts/check-absent-allows.ts, src/adapters/whatsapp-media.ts (owned by S3 this plan); `src/core/media.ts` is shared with S3, which starts after you — change only the size-drop reporting, not the read
34
+ - `src/core/runtime.ts` and `tests/sensitive-guard-*` (owned by plan `sensitive-bugs-0922`, running in parallel)
35
+ - Not ours: see plan.md "Not ours" (nothing intersects). Re-check with `/f-ops-plan next media-bugs-0928`.
36
+
37
+ ## Edges
38
+
39
+ - start-after: none (satisfied — `next` launched you)
40
+ - merge-after: none
41
+
42
+ ## Overlap verdicts
43
+
44
+ none
45
+
46
+ > The command's preflight overlap check treats a hit whose branch AND every shared file match a verdict line as answered (c), prints one `PLAN-PASSED(overlap): …` line, and carries it into the final report. Any other hit prompts as usual. The merge-time check is never waived.
47
+
48
+ ## Decisions answered
49
+
50
+ - none
51
+
52
+ ## Ledger rules
53
+
54
+ - `docs/ROADMAP.md`: never edit. Your command writes `docs/pending-updates/url-bridges-drop-over-limit-attachments-without-a-note.md` as usual.
55
+ - `docs/pending-verification.md`: append only under `## Plan media-bugs-0928 / S2`.
56
+ - Any goal's `decisions.md`: never edit; put decisions in your log's "Siblings must know".
57
+
58
+ ## Environment
59
+
60
+ - Read `CLAUDE.md` → Worktree Setup and Gotchas before opening the worktree. Nothing from them is copied here. `bun install` once in the fresh worktree; the check gate is `bun run check` (every link runs to completion — read every line).
61
+
62
+ ## Report
63
+
64
+ Your command's final report is appended to `docs/plans/media-bugs-0928/sessions/S2.md` by its report step (path resolved through `.claude/scripts/resolve-docs-root.sh docs/plans/probe.md`). `work` verifies the append.
@@ -0,0 +1,61 @@
1
+ # Brief S3 — URL media download buffers the whole body when Content-Length is absent
2
+
3
+ **Plan**: docs/plans/media-bugs-0928/plan.md **Session**: S3 **Round**: 2 **Kind**: bug
4
+ **Item slug**: media-download-buffers-before-size-check-without-content-length
5
+
6
+ > The only file this session reads from the plan. Self-contained: it never points at a sibling's brief. The `**Item slug**:` line is the key the execution commands grep for to detect that a brief is active — keep it exactly in that shape.
7
+
8
+ ## Launch
9
+
10
+ /f-ops-plan work media-bugs-0928 S3
11
+
12
+ ## Run
13
+
14
+ /f-bug-fix media-download-buffers-before-size-check-without-content-length
15
+
16
+ ## Scope notes
17
+
18
+ `if (contentLength && parsed > maxSizeBytes)` — absent `Content-Length` allows, and `response.arrayBuffer()` buffers the whole chunked body before the post-download check. Read `response.body` with a reader, count bytes, `cancel()` once past `maxSizeBytes`; keep the header check as an early exit. Lower `scripts/absent-allows-baseline.json` **in the same commit**. S2 has already landed the `onSkipped` call for the size drops in this function — read it on `dev` and S2's log **Siblings must know**; do not re-add it. The sibling guards at `src/adapters/whatsapp-media.ts` (`fileLength && …`, ~:226 and ~:385) have the same shape: check them; fix them here if the change is small and the same shape, else file a residual (the ratchet skips `src/adapters/` by owner decision — a fix there does not change the baseline). The test must stream a body with no `Content-Length` and prove peak buffered bytes stay near the cap; its bound must own a ref'd timer (CLAUDE.md: `AbortSignal.timeout` around a bare promise never fires on Windows bun).
19
+
20
+ ## Owned surfaces (predicted)
21
+
22
+ - scripts/absent-allows-baseline.json
23
+ - scripts/check-absent-allows.ts
24
+ - src/adapters/whatsapp-media.ts
25
+ - src/core/media.ts
26
+
27
+ ## Do not touch
28
+
29
+ - src/agent/container-runner.ts, src/core/outbox.ts (owned by S1 this plan)
30
+ - src/bridges/discord.ts, src/bridges/slack.ts, src/bridges/teams.ts, src/bridges/telegram.ts, src/bridges/whatsapp.ts, tests/inventories/host-limits.ts (owned by S2 this plan, landed before you start)
31
+ - `src/core/runtime.ts` and `tests/sensitive-guard-*` (owned by plan `sensitive-bugs-0922`, running in parallel)
32
+ - Not ours: see plan.md "Not ours" (nothing intersects). Re-check with `/f-ops-plan next media-bugs-0928`.
33
+
34
+ ## Edges
35
+
36
+ - start-after: none (satisfied — `next` launched you)
37
+ - merge-after: none
38
+
39
+ ## Overlap verdicts
40
+
41
+ none
42
+
43
+ > The command's preflight overlap check treats a hit whose branch AND every shared file match a verdict line as answered (c), prints one `PLAN-PASSED(overlap): …` line, and carries it into the final report. Any other hit prompts as usual. The merge-time check is never waived.
44
+
45
+ ## Decisions answered
46
+
47
+ - none
48
+
49
+ ## Ledger rules
50
+
51
+ - `docs/ROADMAP.md`: never edit. Your command writes `docs/pending-updates/media-download-buffers-before-size-check-without-content-length.md` as usual.
52
+ - `docs/pending-verification.md`: append only under `## Plan media-bugs-0928 / S3`.
53
+ - Any goal's `decisions.md`: never edit; put decisions in your log's "Siblings must know".
54
+
55
+ ## Environment
56
+
57
+ - Read `CLAUDE.md` → Worktree Setup and Gotchas before opening the worktree. Nothing from them is copied here. `bun install` once in the fresh worktree; the check gate is `bun run check` (every link runs to completion — read every line).
58
+
59
+ ## Report
60
+
61
+ Your command's final report is appended to `docs/plans/media-bugs-0928/sessions/S3.md` by its report step (path resolved through `.claude/scripts/resolve-docs-root.sh docs/plans/probe.md`). `work` verifies the append.
@@ -0,0 +1,6 @@
1
+ # Decisions — plan media-bugs-0928
2
+
3
+ > Answer in the `Answer` column. Everything blocked on a question stays blocked until this column is non-empty for it. `build` and `next` add rows; only you write answers. Delete nothing — an answered row is the audit trail.
4
+
5
+ | ID | Blocks | Question | Options | Answer | Answered |
6
+ |----|--------|----------|---------|--------|----------|