mercury-agent 0.24.2 → 0.24.3-beta.1

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 (100) hide show
  1. package/docs/goals/space-links/decisions.md +45 -0
  2. package/docs/goals/space-links/roadmap.md +2 -2
  3. package/docs/pending-verification.md +36 -0
  4. package/docs/permissions.md +9 -2
  5. package/docs/plans/{unattended-1003 → archive/2026-10-05-unattended-1003}/plan.md +2 -2
  6. package/docs/plans/archive/2026-10-05-unattended-1003/sessions/S1.md +43 -0
  7. package/docs/plans/archive/2026-10-05-unattended-1003/sessions/S2.md +54 -0
  8. package/docs/plans/archive/2026-10-05-unattended-1003/sessions/S3.md +38 -0
  9. package/docs/plans/archive/2026-10-05-unattended-1003/sessions/S4.md +46 -0
  10. package/docs/plans/archive/2026-10-05-unattended-1003/sessions/S5.md +32 -0
  11. package/docs/plans/archive/2026-10-05-unattended-1003/sessions/S6.md +64 -0
  12. package/docs/plans/archive/2026-10-05-unattended-1003/sessions/S7.md +78 -0
  13. package/docs/plans/archive/2026-10-05-unattended-1003/sessions/S8.md +55 -0
  14. package/docs/plans/archive/2026-10-05-unattended-1003/sessions/S9.md +49 -0
  15. package/docs/plans/{bench-patterns-0915 → archive/2026-10-07-bench-patterns-0915}/plan.md +4 -4
  16. package/docs/plans/{search-bugs-0930 → archive/2026-10-07-search-bugs-0930}/plan.md +2 -2
  17. package/docs/plans/general-0928/sessions/S2.md +53 -0
  18. package/docs/plans/monitoring-1005/briefs/S1.md +68 -0
  19. package/docs/plans/monitoring-1005/briefs/S10.md +72 -0
  20. package/docs/plans/monitoring-1005/briefs/S11.md +75 -0
  21. package/docs/plans/monitoring-1005/briefs/S12.md +74 -0
  22. package/docs/plans/monitoring-1005/briefs/S13.md +77 -0
  23. package/docs/plans/monitoring-1005/briefs/S2.md +72 -0
  24. package/docs/plans/monitoring-1005/briefs/S3.md +66 -0
  25. package/docs/plans/monitoring-1005/briefs/S4.md +68 -0
  26. package/docs/plans/monitoring-1005/briefs/S5.md +62 -0
  27. package/docs/plans/monitoring-1005/briefs/S6.md +67 -0
  28. package/docs/plans/monitoring-1005/briefs/S7.md +83 -0
  29. package/docs/plans/monitoring-1005/briefs/S8.md +74 -0
  30. package/docs/plans/monitoring-1005/briefs/S9.md +78 -0
  31. package/docs/plans/monitoring-1005/decisions.md +10 -0
  32. package/docs/plans/monitoring-1005/plan.md +182 -0
  33. package/docs/plans/monitoring-1005/sessions/S2.md +51 -0
  34. package/docs/plans/monitoring-1005/sessions/S3.md +55 -0
  35. package/docs/plans/monitoring-1005/sessions/S4.md +50 -0
  36. package/docs/plans/search-quality-0930/briefs/S12.md +1 -1
  37. package/docs/plans/search-quality-0930/sessions/S7.md +50 -0
  38. package/package.json +1 -1
  39. package/resources/skills/permissions/SKILL.md +1 -1
  40. package/resources/skills/spaces/SKILL.md +34 -4
  41. package/src/adapters/whatsapp-media.ts +275 -78
  42. package/src/agent/container-entry.ts +8 -0
  43. package/src/agent/pi-failure-class.ts +34 -0
  44. package/src/agent/user-error-messages.ts +5 -20
  45. package/src/cli/mrctl-http.ts +293 -0
  46. package/src/cli/mrctl.ts +168 -20
  47. package/src/core/api-types.ts +24 -0
  48. package/src/core/api.ts +2 -0
  49. package/src/core/handler.ts +36 -11
  50. package/src/core/permissions.ts +8 -0
  51. package/src/core/routes/conversations.ts +46 -12
  52. package/src/core/routes/index.ts +1 -0
  53. package/src/core/routes/links.ts +610 -0
  54. package/src/core/routes/spaces.ts +44 -1
  55. package/src/core/system-messages.ts +3 -0
  56. package/src/extensions/reserved.ts +1 -0
  57. package/src/server.ts +27 -2
  58. package/src/storage/db.ts +176 -98
  59. package/examples/extensions/pinchtab/lib/session-injector.ts +0 -166
  60. /package/docs/plans/{unattended-1003 → archive/2026-10-05-unattended-1003}/briefs/S1.md +0 -0
  61. /package/docs/plans/{unattended-1003 → archive/2026-10-05-unattended-1003}/briefs/S2.md +0 -0
  62. /package/docs/plans/{unattended-1003 → archive/2026-10-05-unattended-1003}/briefs/S3.md +0 -0
  63. /package/docs/plans/{unattended-1003 → archive/2026-10-05-unattended-1003}/briefs/S4.md +0 -0
  64. /package/docs/plans/{unattended-1003 → archive/2026-10-05-unattended-1003}/briefs/S5.md +0 -0
  65. /package/docs/plans/{unattended-1003 → archive/2026-10-05-unattended-1003}/briefs/S6.md +0 -0
  66. /package/docs/plans/{unattended-1003 → archive/2026-10-05-unattended-1003}/briefs/S7.md +0 -0
  67. /package/docs/plans/{unattended-1003 → archive/2026-10-05-unattended-1003}/briefs/S8.md +0 -0
  68. /package/docs/plans/{unattended-1003 → archive/2026-10-05-unattended-1003}/briefs/S9.md +0 -0
  69. /package/docs/plans/{unattended-1003 → archive/2026-10-05-unattended-1003}/decisions.md +0 -0
  70. /package/docs/plans/{bench-patterns-0915 → archive/2026-10-07-bench-patterns-0915}/briefs/S10.md +0 -0
  71. /package/docs/plans/{bench-patterns-0915 → archive/2026-10-07-bench-patterns-0915}/briefs/S11.md +0 -0
  72. /package/docs/plans/{bench-patterns-0915 → archive/2026-10-07-bench-patterns-0915}/briefs/S12.md +0 -0
  73. /package/docs/plans/{bench-patterns-0915 → archive/2026-10-07-bench-patterns-0915}/briefs/S4.md +0 -0
  74. /package/docs/plans/{bench-patterns-0915 → archive/2026-10-07-bench-patterns-0915}/briefs/S5.md +0 -0
  75. /package/docs/plans/{bench-patterns-0915 → archive/2026-10-07-bench-patterns-0915}/briefs/S6.md +0 -0
  76. /package/docs/plans/{bench-patterns-0915 → archive/2026-10-07-bench-patterns-0915}/briefs/S7.md +0 -0
  77. /package/docs/plans/{bench-patterns-0915 → archive/2026-10-07-bench-patterns-0915}/briefs/S8.md +0 -0
  78. /package/docs/plans/{bench-patterns-0915 → archive/2026-10-07-bench-patterns-0915}/briefs/S9.md +0 -0
  79. /package/docs/plans/{bench-patterns-0915 → archive/2026-10-07-bench-patterns-0915}/decisions.md +0 -0
  80. /package/docs/plans/{bench-patterns-0915 → archive/2026-10-07-bench-patterns-0915}/sessions/S10.md +0 -0
  81. /package/docs/plans/{bench-patterns-0915 → archive/2026-10-07-bench-patterns-0915}/sessions/S11.md +0 -0
  82. /package/docs/plans/{bench-patterns-0915 → archive/2026-10-07-bench-patterns-0915}/sessions/S12.md +0 -0
  83. /package/docs/plans/{bench-patterns-0915 → archive/2026-10-07-bench-patterns-0915}/sessions/S4.md +0 -0
  84. /package/docs/plans/{bench-patterns-0915 → archive/2026-10-07-bench-patterns-0915}/sessions/S5.md +0 -0
  85. /package/docs/plans/{bench-patterns-0915 → archive/2026-10-07-bench-patterns-0915}/sessions/S6.md +0 -0
  86. /package/docs/plans/{bench-patterns-0915 → archive/2026-10-07-bench-patterns-0915}/sessions/S7.md +0 -0
  87. /package/docs/plans/{bench-patterns-0915 → archive/2026-10-07-bench-patterns-0915}/sessions/S8.md +0 -0
  88. /package/docs/plans/{bench-patterns-0915 → archive/2026-10-07-bench-patterns-0915}/sessions/S9.md +0 -0
  89. /package/docs/plans/{search-bugs-0930 → archive/2026-10-07-search-bugs-0930}/briefs/S1.md +0 -0
  90. /package/docs/plans/{search-bugs-0930 → archive/2026-10-07-search-bugs-0930}/briefs/S2.md +0 -0
  91. /package/docs/plans/{search-bugs-0930 → archive/2026-10-07-search-bugs-0930}/briefs/S3.md +0 -0
  92. /package/docs/plans/{search-bugs-0930 → archive/2026-10-07-search-bugs-0930}/briefs/S4.md +0 -0
  93. /package/docs/plans/{search-bugs-0930 → archive/2026-10-07-search-bugs-0930}/briefs/S5.md +0 -0
  94. /package/docs/plans/{search-bugs-0930 → archive/2026-10-07-search-bugs-0930}/briefs/S6.md +0 -0
  95. /package/docs/plans/{search-bugs-0930 → archive/2026-10-07-search-bugs-0930}/decisions.md +0 -0
  96. /package/docs/plans/{search-bugs-0930 → archive/2026-10-07-search-bugs-0930}/sessions/S1.md +0 -0
  97. /package/docs/plans/{search-bugs-0930 → archive/2026-10-07-search-bugs-0930}/sessions/S2.md +0 -0
  98. /package/docs/plans/{search-bugs-0930 → archive/2026-10-07-search-bugs-0930}/sessions/S3.md +0 -0
  99. /package/docs/plans/{search-bugs-0930 → archive/2026-10-07-search-bugs-0930}/sessions/S5.md +0 -0
  100. /package/docs/plans/{search-bugs-0930 → archive/2026-10-07-search-bugs-0930}/sessions/S6.md +0 -0
@@ -441,3 +441,48 @@
441
441
  without running a command (guessing), or picking the wrong space. The next
442
442
  step is a skill with worked examples.
443
443
  - **Used by:** act-as-linked-space, messages-recent-range, link-ask-as-space
444
+
445
+ ## D-018: A link's `note` is the global admin's alone; everything else about an edge stays visible to the spaces it touches
446
+ - **Category:** architecture
447
+ - **Decided:**
448
+ - `note` is returned only to a global admin: in `list`, `inspect` and the
449
+ create/update/revoke responses, and inside audit rows' `detail`
450
+ (`before` / `after`). For anyone else it is `null` on the edge and absent
451
+ from the audit, whether or not one is set. This holds on every edge, not
452
+ only on `*` edges, and the holder's admin does not see it either.
453
+ - Unchanged, and still shown to every admin of a space the edge touches
454
+ (holder, target, or any space a `*` edge reaches): holder and target,
455
+ level, `disclosure`, who created or changed it and when, and its audit
456
+ history.
457
+ - **Alternatives considered:**
458
+ - The full record for every space an edge touches (as M1.2 first shipped).
459
+ Rejected: a `*` edge is read by every space's admin, so a note written as
460
+ an operator's remark ("client X trial ends in Nov") would reach every
461
+ client at once, and the audit keeps old notes, so it cannot be taken back.
462
+ - A `*` edge visible to other spaces only as inbound reach, with no record
463
+ (the spec's most literal reading). Rejected: it also hides `disclosure`,
464
+ which D-007 addresses to the target side, and the history that makes a
465
+ link accountable (D-006).
466
+ - **Reasoning:** Each free-text field has one audience, enforced in code rather
467
+ than by the operator remembering who reads it. `disclosure` is what the
468
+ target was told, so the target reads it; `note` is the operator's.
469
+ - **Open:** `createdBy` / `updatedBy` / the audit's `actorId` still show the
470
+ global admin's raw platform id to other spaces' admins. Showing a label
471
+ instead was offered and not chosen on 2026-10-05.
472
+ - **Revisit if:** a deployment wants notes shared with a target; that text
473
+ belongs in `disclosure`.
474
+ - **Used by:** mrctl-links-admin-commands, act-as-linked-space
475
+
476
+ ## D-019: An act-as audit row records the space actually acted on, never `*`
477
+ - **Category:** architecture
478
+ - **Decided:**
479
+ - When M1.3 writes the per-request audit row (D-008) for a request granted by
480
+ a `*` edge, `target_space_id` is the space the request acted on. `*` stays
481
+ only on the create/update/revoke rows of the edge itself.
482
+ - M1.3 carries a test: a request through `mgmt → *` into C is not in B's
483
+ `mrctl links audit`.
484
+ - **Reasoning:** `GET /api/links/audit` shows a space's admin every row whose
485
+ target is `*` (the reading that lets every space see a wildcard's lifecycle,
486
+ D-013). An act-as row stamped `*` would show management's requests into every
487
+ space, route and body summary included, to every other space's admin.
488
+ - **Used by:** act-as-linked-space
@@ -24,8 +24,8 @@
24
24
  | ID | Story | Slug | Depends on | Status |
25
25
  |----|-------|------|------------|--------|
26
26
  | M1.1 | Link table (incl. `*` target), levels, direct-edge resolver, audit table | space-link-graph-and-grant-resolver | — | done |
27
- | M1.2 | `mrctl links list/inspect/create/update/revoke/audit` + `/api/links` routes | mrctl-links-admin-commands | M1.1 | blocked |
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 |
27
+ | M1.2 | `mrctl links list/inspect/create/update/revoke/audit` + `/api/links` routes | mrctl-links-admin-commands | M1.1 | done |
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 | launchable |
29
29
  | M1.4 | `mrctl messages recent --since 24h`: time-range history read, paged | messages-recent-range | — | backlog |
30
30
 
31
31
  **Checkpoint:** on the Tagula bot (Hetzner), where the management space and the
@@ -2284,6 +2284,20 @@ The review of the seven 2026-10-02 fixes landed follow-ups on `dev`. Three baked
2284
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
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
2286
 
2287
+ ## Plan unattended-1003 / S6
2288
+
2289
+ The WhatsApp media download now closes its socket when a bound fires, through a Baileys patch (`patches/`). *(Superseded 2026-10-07: the patch never reached npm-installed hosts, so it was removed and the media `fetch` is now owned by `fetchDecryptedMedia` — `docs/debug/moderate/2026-10-07-baileys-patch-never-reaches-an-npm-installed-host.md`. The greps below for `signal: options.signal` in `node_modules` now print 0 by design.)* Host-only, nothing baked: a **sync** (which runs `bun install` because `package.json` and `bun.lock` changed) **and a restart**. No image rebuild.
2290
+
2291
+ - [x] *(Verified 2026-10-05 on the WSL box, dev `6ddb895`, base `mercury-agent:0.24.2-beta.2` = `63637858e06b`, derived `mercury-agent-ext:b2acae430168` (built 13:36:22 IDT, 482 s), restart 13:28:17 IDT: the sync ran `bun install` and the grep prints `1`.)* MANUAL-VERIFY(2026-10-04): After the sync, `grep -c -F "signal: options.signal" ~/mercury-src/node_modules/@whiskeysockets/baileys/lib/Utils/messages-media.js` prints 1. A 0 means `bun install` did not run or did not apply the patch, and the fix is not deployed even though the code is.
2292
+ - [x] *(Verified 2026-10-05 on the WSL box, dev `6ddb895`, base `mercury-agent:0.24.2-beta.2` = `63637858e06b`, derived `mercury-agent-ext:b2acae430168` (built 13:36:22 IDT, 482 s), restart 13:28:17 IDT. Rehearsed, not waited for: the deployed `downloadWhatsAppMedia` from `~/mercury-src`, with production bounds and its patched `node_modules`, against a local raw-TCP server. Open stall: `bound fired while opening` at 30.0 s, `download_failed` to `onSkipped`, server saw the socket close 2 ms later. Body stall after 64 KB: `read bound fired` (idle) at 30.0 s, `download_failed`, socket closed 1 ms later. The live-journal half is still worth a glance the next time a real download stalls.)* MANUAL-VERIFY(2026-10-04): The next time the journal shows `WhatsApp media read bound fired` or `bound fired while opening`, `ss -tnp | grep -c <bun pid>` taken a few seconds later shows no connection to the WhatsApp media host left open for that download, and the user was told the file could not be downloaded.
2293
+ - [x] *(Verified 2026-10-05: `docker build -f Dockerfile.host` from `~/mercury-src` at `6ddb895` exited 0, `COPY patches/` ran before `bun install --production`, and the built image's `node_modules/@whiskeysockets/baileys/lib/Utils/messages-media.js` has `signal: options.signal` (1). Throwaway tag removed.)* MANUAL-VERIFY(2026-10-04): If any box builds `Dockerfile.host`, its next build passes the `bun install --production` step (it now copies `patches/` first; without it the install fails with `Couldn't find patch file`).
2294
+
2295
+ ## baileys-patch-never-reaches-an-npm-installed-host (2026-10-07)
2296
+
2297
+ The media download fetches and decrypts itself instead of through a patched Baileys. Host-only: a sync (`bun install`, since `package.json` and `bun.lock` changed) and a restart; no image rebuild.
2298
+
2299
+ - [ ] MANUAL-VERIFY(2026-10-07): After deploying, send the bot a photo, a voice note and a PDF in a group it answers in, and confirm it describes each one — real WhatsApp media must decrypt through the new path, which the tests only exercise with media they encrypted themselves.
2300
+
2287
2301
  ## Plan unattended-1003 / S7
2288
2302
 
2289
2303
  `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.
@@ -2318,3 +2332,25 @@ A refused credential now falls back to the next model leg and still alerts the o
2318
2332
 
2319
2333
  - [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
2334
  - [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.
2335
+
2336
+ ## napkin sender name — tagula box on 0.24.2-beta.2 (deployed 2026-10-05 00:41 IDT)
2337
+ - [ ] MANUAL-VERIFY(2026-10-05): On the tagula box, after a day with group messages, the newest `<space>/.messages/<date>.jsonl` in the customer's group space has `"author_name"` on most `"role":"user"` lines: `grep -c '"author_name"' <file>` is more than 0, and lines with no name have no `author_name` key at all. Kulmus's `author_name` filter then finds that client's messages (fix `napkin-message-export-drops-the-sender-name`, post-mortem `docs/debug/minor/2026-10-04-napkin-message-export-drops-the-sender-name.md`).
2338
+
2339
+ ## Plan unattended-1003 / S2
2340
+
2341
+ A content-filter block now ends its leg after one attempt instead of being retried `max_retries_per_leg` times. `pi-failure-class.ts` is baked, so this needs **an image rebuild** (`cd ~/mercury-src && bash ./container/build.sh`), then a restart so the derived image follows (`docs/debug/minor/2026-10-04-pi-failure-class-retries-a-content-filter-block.md`).
2342
+
2343
+ - [x] *(Verified 2026-10-05 on the WSL box, dev `6ddb895`, base `mercury-agent:0.24.2-beta.2` = `63637858e06b`, derived `mercury-agent-ext:b2acae430168` (built 13:36:22 IDT, 482 s), restart 13:28:17 IDT: prints `1`. Also run inside that image: `classifyAttemptFailure` on a `PiExitFailure` whose classify text is the Anthropic block returns `fallbackable` (the leg loop breaks after one attempt), the same words only in stdout stay `retryable`, a 503 stays `retryable`.)* MANUAL-VERIFY(2026-10-04): After the rebuild and the `Built derived agent image` line, `docker run --rm --entrypoint sh <derived mercury-agent-ext tag> -c 'grep -c -F "content\\s+filtering\\s+policy" /app/src/agent/pi-failure-class.ts'` prints 1.
2344
+ - [ ] MANUAL-VERIFY(2026-10-04): The next time a provider blocks a reply ("Output blocked by content filtering policy"), that run's trace in `~/whatsapp-bot/.mercury/traces/` has exactly **one** `piSpawnedAtIso` line, not four, and the user still gets the content-filter message, not "try again". *(2026-10-05: still open. One attempt on a shadow (`greece2026`, Opus 5.5) asking for full song lyrics: the model declined by itself, so the provider filter never fired. Not forced further; wait for a real block.)*
2345
+
2346
+ ## Hebrew host notices — restart-only (host-side `src/core/handler.ts`)
2347
+
2348
+ The ⏳ heartbeat now uses the space's `messages.locale`; `main`, `tel-aviv-flat-search` and `@global` are `he` since 2026-09-26.
2349
+
2350
+ - [ ] MANUAL-VERIFY(2026-10-05): After the restart, a request in `main` or the flat-search group that runs past 30 s shows `⏳ עובד על זה… (30 שניות)`, not `⏳ Processing… (30s)`; the next restart/failure notice there is Hebrew too (fix `host-notices-are-english-in-hebrew-spaces`).
2351
+
2352
+ ## space-links D-018 — `note` is global-admin-only (with the M1.2 deploy)
2353
+
2354
+ Host-only (`src/core/routes/links.ts`, not baked): it rides whatever restart deploys `mrctl-links-admin-commands`.
2355
+
2356
+ - [ ] MANUAL-VERIFY(2026-10-05): On the box that runs M1.2, the global admin creates `mrctl links create <mgmt> '*' --level read --note "probe-note" --disclosure "probe-disclosure"`. Then, in another space, ask that space's admin's bot "who can access this space?": the reply names the management space and the disclosure, and never says "probe-note". The owner's own `mrctl links list` still shows `note: probe-note`. Revoke the probe link afterwards.
@@ -54,10 +54,11 @@ Custom roles can be created by assigning permissions to any role name.
54
54
  | `permissions.get` | View role permissions |
55
55
  | `permissions.set` | Modify role permissions |
56
56
  | `spaces.list` | View all spaces |
57
- | `spaces.rename` | Rename a space and link/unlink conversations |
57
+ | `spaces.rename` | Rename the current space |
58
58
  | `spaces.delete` | Delete current space and all related DB data |
59
59
  | `model.list` | List the configured model chain (`mrctl model list`, chat `/model list`) |
60
60
  | `model.switch` | Pin the space to one model leg (`mrctl model switch`, chat `/model switch`) or clear that pin (`mrctl model reset`, chat `/model reset`) — takes effect on the next agent run. A pinned space has no fallback; reset is refused while the space has an active scheduled task |
61
+ | `links.list` | See the space links touching this space, which spaces reach it and at what level, and their audit (`mrctl links list/inspect/audit`). Admin-only by default: it names every space that reaches this one. Creating, changing or revoking a link (`mrctl links create/update/revoke`) is **not** a permission — those verbs are global-admin-only, and no role grant enables them |
61
62
  | `media.receive` | Incoming attachments are saved to `inbox/` and shown to the agent |
62
63
  | `media.send` | Outbox files produced on this caller's turn are delivered back to the chat |
63
64
 
@@ -137,9 +138,15 @@ mrctl conversations list
137
138
 
138
139
  # Show only conversations that are not yet linked
139
140
  mrctl conversations list --unlinked
141
+
142
+ # Create a new space (global admin only)
143
+ mrctl spaces create family-chat "Family chat"
144
+
145
+ # Link a conversation (id from `conversations list`) to a space (global admin only)
146
+ mrctl conversations link 12 family-chat
140
147
  ```
141
148
 
142
- Space names are stored in the database and shown in logs/dashboard for easier identification. Conversations remain platform-native and are linked into spaces. Conversation listing uses `spaces.list`; linking and unlinking use `spaces.rename`.
149
+ Space names are stored in the database and shown in logs/dashboard for easier identification. Conversations remain platform-native and are linked into spaces. Conversation listing uses `spaces.list`. Creating a space and linking/unlinking a conversation reach past the caller's own space, so they are not gated by a space permission: they require a **global admin** (`permissions.admins` / `dmAutoSpaceAdminIds`), the same rule as `mrctl links create`. (Chat `/spaces` is narrower: seeded `permissions.admins` only.) With no global admins configured, nobody can do them through the API — use the dashboard.
143
150
 
144
151
  ## Seeding Admins
145
152
 
@@ -1,7 +1,7 @@
1
1
  # Plan: unattended-1003
2
2
 
3
- **Status**: Active
4
- **Created**: 2026-10-03 **Last updated**: 2026-10-03
3
+ **Status**: Archived
4
+ **Created**: 2026-10-03 **Last updated**: 2026-10-05
5
5
  **Landing mode**: pr
6
6
  **Max width**: 6
7
7
  **Inputs**: six unattended lanes: S1 auth-rejection-never-falls-back-to-the-next-model-leg + pi-failure-class-retries-a-content-filter-block; S2 consent-rows-are-writable-by-space-profile-apply; S3 gemini-leg-refused-no-google-credential-env-var; S4 core-media-url-download-has-no-time-bound + whatsapp-media-read-bound-does-not-close-the-download-socket; S5 pinchtab-session-inject-script-is-refused-401-and-reports-success + pinchtab-session-injector-lib-computes-a-different-etld1; S6 space-link-graph-and-grant-resolver --width 6 (one item per session, so the three paired lanes became file-ordered pairs; IDs below are the plan's, not the lane numbers)
@@ -0,0 +1,43 @@
1
+ ## Launched — 2026-10-03T18:06Z
2
+ Branch: worktree-fix-auth-rejection-never-falls-back-to-the-next-model-leg
3
+
4
+ ## Report — 2026-10-03T18:21Z
5
+ Outcome: blocked
6
+ Delivered: —
7
+ Blocked-on: owner lands — fix is complete and gated on worktree-fix-auth-rejection-never-falls-back-to-the-next-model-leg (4 wip commits); stopped at the confirm-before-archive prompt per the unattended brief, so no archive move, squash-merge or push
8
+ Deploy needs: rebuild
9
+ Files changed:
10
+ - docs/debug/auth-rejection-never-falls-back-to-the-next-model-leg.md
11
+ - docs/pending-verification.md
12
+ - src/agent/container-entry.ts
13
+ - src/agent/container-runner.ts
14
+ - src/agent/pi-failure-class.ts
15
+ - src/agent/provider-auth-rejection.ts
16
+ - tests/pi-failure-class.test.ts
17
+ - tests/provider-auth-rejection.test.ts
18
+ Siblings must know:
19
+ - S4: src/agent/container-runner.ts is touched in consumeResult only (~3262-3285): the authRejected read moved before the `ok === false` branch and calls parseRefusedLegs instead of parseAuthRejected (import line ~66 changed). Owner approved this in-session. Expect a clean rebase unless S4 edits consumeResult.
20
+ - S2: the auth arm of classifyPiFailure now returns "fallbackable" (only cursor is failFast). failFastError is gone, replaced by refusedLeg + chainFailure(…, refused). After a refused first leg, the run's user-facing error is now the LAST leg's error (e.g. 529 overloaded), not the 401. This matters for user-error-messages.ts.
21
+ - result.json `authRejected` is now an array of {provider, model, error}, written on successful runs too. Deploy the image rebuild and the host restart in the same gap.
22
+ --- final report (verbatim) ---
23
+ ~~~
24
+ S1 — auth-rejection-never-falls-back-to-the-next-model-leg: fixed on branch, not landed.
25
+
26
+ Fix: a provider-refused credential is now `fallbackable`. runModelChain records each refused leg with
27
+ its own error and moves to the next leg without retrying it. The list rides every end of the chain
28
+ (success, budget exhausted, fail-fast, last-leg failure) as result.json `authRejected: [{provider, model, error}]`.
29
+ The host's consumeResult alerts every refused leg before branching on ok, so a run that a later leg
30
+ served still pages the operator once per cooldown. parseRefusedLegs accepts only chain legs, once,
31
+ whose error passes isPiAuthFailure (the review fork's forged-entry finding), and still reads an older
32
+ image's single-object form.
33
+
34
+ Gate: ~/check-mercury.sh check <worktree> — all 16 links pass, 6522 pass / 0 fail.
35
+ Negative tests: removing refused.push, gating the host alert on ok===false, or removing the
36
+ isPiAuthFailure clause each turns its tests red.
37
+ Scope: container-runner.ts (S4's surface) was touched with owner approval (AskUserQuestion, this session).
38
+ Post-mortem: docs/debug/auth-rejection-never-falls-back-to-the-next-model-leg.md (filled, not yet
39
+ moved to docs/debug/moderate/; Step 13 archive is behind the confirm prompt).
40
+ Pending verification: docs/pending-verification.md "## Plan unattended-1003 / S1" (2 lines).
41
+ Claim pushed: 6ec6e92 on origin/dev (docs-only promotion, via a scratch worktree).
42
+ Deploy: image rebuild + restart in the same gap.
43
+ ~~~
@@ -0,0 +1,54 @@
1
+ ## Launched — 2026-10-04T10:09Z
2
+ Branch: worktree-fix-pi-failure-class-retries-a-content-filter-block
3
+
4
+ ## Report — 2026-10-04T10:19Z
5
+ Outcome: blocked
6
+ Delivered: —
7
+ Blocked-on: f-bug-fix Step 12 confirm-completion prompt — owner lands (archive, rebase, squash-merge)
8
+ Deploy needs: rebuild
9
+ Files changed:
10
+ - docs/debug/pi-failure-class-retries-a-content-filter-block.md
11
+ - docs/pending-verification.md
12
+ - src/agent/pi-failure-class.ts
13
+ - src/agent/user-error-messages.ts
14
+ - tests/pi-failure-class.test.ts
15
+ Siblings must know:
16
+ - isContentFilterError now lives in src/agent/pi-failure-class.ts (baked); user-error-messages.ts re-exports it, so existing imports still work.
17
+ - isPiAuthFailure now returns false for content-filter text, so refusedLeg never reports a block that quotes 401/403 as a refused credential.
18
+ --- final report (verbatim) ---
19
+ ~~~
20
+ S2 pi-failure-class-retries-a-content-filter-block: stopped at Step 12 (the confirm-completion prompt), per the brief. Nothing pushed except the docs-only claim (95a5cd1).
21
+
22
+ Branch: worktree-fix-pi-failure-class-retries-a-content-filter-block (worktree .claude/worktrees/fix-pi-failure-class-retries-a-content-filter-block), 3 commits on origin/dev 95a5cd1:
23
+ 62f3289 wip: content-filter block is fallbackable, one matcher in the baked classifier
24
+ 0cb5bee wip: format
25
+ 27d654d docs: post-mortem and verification lines for content-filter retry fix
26
+
27
+ Evidence: trace mercury-1790674324824-46 shows 4 pi spawns in one container. Each sends one request to anthropic/claude-opus-5-5, each is blocked ("Output blocked by content filtering policy"), and each exits 0. That is 1 attempt + max_retries_per_leg: 3. The "10-12 times" in the report was grep hits (3 per attempt). Traces -47/-61/-62 are no longer on the box, so they are unconfirmed.
28
+
29
+ Fix: the content-filter matcher moved into the baked pi-failure-class.ts, and user-error-messages.ts re-exports it (one statement). classifyPiFailure: content filter -> fallbackable, checked first. isPiAuthFailure: false for a block. No Dockerfile change, because the file is already COPY'd and has no imports.
30
+
31
+ Gate: ~/check-mercury.sh check <worktree>: all 16 links passed, 6655 pass / 0 fail. Negative-tested: removing the arm turns 4 tests red, removing the auth guard turns 1 red. Session code review found no issues.
32
+
33
+ Still to do (owner): f-bug-fix Steps 13-16 (archive to docs/debug/minor/2026-10-04-…, rebase, squash-merge into dev, clean up), then rebuild the image.
34
+ ~~~
35
+
36
+ ## Report — 2026-10-04T11:16Z
37
+ Outcome: delivered
38
+ Delivered: —
39
+ Blocked-on: —
40
+ Deploy needs: rebuild
41
+ Files changed:
42
+ - docs/debug/minor/2026-10-04-pi-failure-class-retries-a-content-filter-block.md
43
+ - docs/debug/pi-failure-class-retries-a-content-filter-block.md
44
+ - docs/pending-verification.md
45
+ - src/agent/pi-failure-class.ts
46
+ - src/agent/user-error-messages.ts
47
+ - tests/pi-failure-class.test.ts
48
+ Siblings must know:
49
+ - isContentFilterError now lives in src/agent/pi-failure-class.ts (baked); user-error-messages.ts re-exports it.
50
+ - isPiAuthFailure returns false for content-filter text.
51
+ --- final report (verbatim) ---
52
+ ~~~
53
+ Owner approved archive + squash-merge (2026-10-04). Post-mortem archived to docs/debug/minor/2026-10-04-pi-failure-class-retries-a-content-filter-block.md, branch rebased onto origin/dev 45ea243 (no semantic overlap), squash-merged into local dev as 80eb998. Worktree and branch removed. NOT pushed: local dev is 1 ahead of origin/dev. Deploy needs an image rebuild.
54
+ ~~~
@@ -0,0 +1,38 @@
1
+ ## Launched — 2026-10-03T18:20Z
2
+ Branch: worktree-fix-consent-rows-are-writable-by-space-profile-apply
3
+
4
+ ## Report — 2026-10-03T18:28Z
5
+ Outcome: blocked
6
+ Delivered: —
7
+ Blocked-on: owner landing — /f-bug-fix Step 12 confirm (branch worktree-fix-consent-rows-are-writable-by-space-profile-apply @ 4b8f6c6, gate green, not squash-merged)
8
+ Deploy needs: restart
9
+ Files changed:
10
+ - docs/bugs/runtime-owned-config-keys-have-no-guard-at-the-store.md
11
+ - docs/debug/consent-rows-are-writable-by-space-profile-apply.md
12
+ - docs/pending-verification.md
13
+ - src/core/routes/config-builtin.ts
14
+ - src/extensions/reserved.ts
15
+ - src/profile/space-profile.ts
16
+ - tests/extension-loader.test.ts
17
+ - tests/space-profile.test.ts
18
+ Siblings must know:
19
+ - S9 (owns src/storage/db.ts): filed docs/bugs/runtime-owned-config-keys-have-no-guard-at-the-store.md. It proposes refusing isRuntimeOwnedConfigKey in Db.setSpaceConfig. Not in S9's scope; just don't be surprised by it.
20
+ - `security` is now a reserved extension name (src/extensions/reserved.ts).
21
+ --- final report (verbatim) ---
22
+ ~~~
23
+ S3: consent-rows-are-writable-by-space-profile-apply (bug, moderate). Stopped at /f-bug-fix Step 12 (confirm before archive and merge), per the brief.
24
+
25
+ Fix (branch worktree-fix-consent-rows-are-writable-by-space-profile-apply, 3 wip commits on origin/dev 7197d5f):
26
+ - src/profile/space-profile.ts: assertProfileWritableConfigKey refuses isRuntimeOwnedConfigKey keys with a ProfileError, at extension_config parse time and again at the top of applyProfile before any write.
27
+ - src/extensions/reserved.ts: `security` reserved.
28
+ - src/core/routes/config-builtin.ts: docstring corrected.
29
+ - tests: space-profile.test.ts "runtime-owned config keys" (both consent keys, at load and at apply), extension-loader.test.ts (`security` refused). Each guard negative-tested by deleting it: 2/2/1 failures.
30
+ - Residual filed: docs/bugs/runtime-owned-config-keys-have-no-guard-at-the-store.md (store-level guard; db.ts was S9's this round).
31
+ - Post-mortem filled in docs/debug/consent-rows-are-writable-by-space-profile-apply.md. Not yet moved to docs/debug/moderate/ (Step 13 runs after the confirm).
32
+ - pending-verification: 2 lines under "## Plan unattended-1003 / S3".
33
+
34
+ Gate: ~/check-mercury.sh check: all 16 links passed (6518+ tests, 0 fail). Session code review: no issues.
35
+ Claim push: 7197d5f to origin/dev (docs only, cherry-picked onto a fresh base).
36
+ Deploy: restart. reserved.ts is COPY'd into the image but nothing there imports it (the image contract checks existence only), so a rebuild is not needed for the fix.
37
+ Overlap preflight: exit 0, no PLAN-PASSED lines.
38
+ ~~~
@@ -0,0 +1,46 @@
1
+ ## Launched — 2026-10-03T18:10Z
2
+ Branch: worktree-fix-gemini-leg-refused-no-google-credential-env-var
3
+
4
+ ## Report — 2026-10-03T18:22Z
5
+ Outcome: blocked
6
+ Delivered: —
7
+ Blocked-on: f-bug-fix Step 12 confirm-completion — owner lands (archive + squash-merge into dev), per the unattended brief
8
+ Deploy needs: restart
9
+ Files changed:
10
+ - docs/bugs/model-provider-passthrough-list-misses-pi-api-key-providers.md
11
+ - docs/bugs/pi-auth-test-catalog-mock-restore-reinstalls-the-stub.md
12
+ - docs/debug/gemini-leg-refused-no-google-credential-env-var.md
13
+ - docs/pending-updates/gemini-leg-refused-no-google-credential-env-var.md
14
+ - docs/pending-verification.md
15
+ - src/agent/container-runner.ts
16
+ - src/preflight/checks/credential.ts
17
+ - src/storage/pi-auth.ts
18
+ - tests/container-runner-chain-credential-guard.test.ts
19
+ - tests/container-runner-chain-credentials.test.ts
20
+ - tests/pi-auth-provider-env-vars.test.ts
21
+ - tests/pi-auth.test.ts
22
+ - tests/preflight-credential.test.ts
23
+ Siblings must know:
24
+ - S1 (provider-auth-rejection.ts, not edited here): `providerCredentialEnvVar` now maps every single-variable pi provider (`google` → `GEMINI_API_KEY`, `openai` → `OPENAI_API_KEY`, …), so `credentialVarsFor` returns a var for API-key providers where it used to return []. Recovery text via `envSource` follows automatically.
25
+ - New export `providerOAuthHandoffEnvVar(provider)` in src/storage/pi-auth.ts: auth.json OAuth is handed over only for anthropic and github-copilot. Use it, not `providerCredentialEnvVar`, for "should I refresh auth.json for this provider".
26
+ - src/preflight/checks/credential.ts was edited (outside predicted surfaces, owned by no session): the auth.json branch now uses the hand-off predicate.
27
+ --- final report (verbatim) ---
28
+ ~~~
29
+ S4 / gemini-leg-refused-no-google-credential-env-var: fixed on branch worktree-fix-gemini-leg-refused-no-google-credential-env-var (5 local commits on origin/dev f37097e). Stopped at /f-bug-fix Step 12, before archive and squash-merge, as the unattended brief requires.
30
+
31
+ Root cause: `PROVIDER_CREDENTIAL_ENV_VAR` (src/storage/pi-auth.ts) listed only anthropic and github-copilot, but the host-side fail-fast gate reads it as "which variable keys this leg". So every API-key leg (google, openai, groq, …) was refused, while the key was already reaching the container as GEMINI_API_KEY through the passthrough.
32
+
33
+ Fix:
34
+ - The map now lists every builtin pi provider that reads exactly one env var. Bedrock, vertex, cloudflare and openai-codex are left out on purpose.
35
+ - New `providerOAuthHandoffEnvVar` keeps the auth.json OAuth hand-off to anthropic and copilot, in both injection and preflight. Without it, a kimi-coding OAuth entry would have paged the operator on every spawn; the session review caught this.
36
+ - tests/pi-auth-provider-env-vars.test.ts pins the map against the installed pi-ai's own resolvers in both directions.
37
+ - Gemini guard tests cover the real passthrough, a missing key, an empty key and a pinned leg.
38
+ - Every new test was negative-tested and goes red.
39
+
40
+ Gate: ~/check-mercury.sh check → all 16 links passed.
41
+ QA: repro path refused → now served when MERCURY_GEMINI_API_KEY is set (pass); absent or empty key still refused, naming MERCURY_GEMINI_API_KEY (pass); checklist all ticked (pass).
42
+ Claim push: f37097e on origin/dev (two retries, lost races to S7 and S9 claims).
43
+ Deploy: host-only (neither file is a Dockerfile COPY line, no version bump) → restart in a gap.
44
+ Pending verification: one real Gemini turn on pi 0.87.1 / @google/genai 2.21, under "Plan unattended-1003 / S4".
45
+ Filed: docs/bugs/pi-auth-test-catalog-mock-restore-reinstalls-the-stub.md, docs/bugs/model-provider-passthrough-list-misses-pi-api-key-providers.md.
46
+ ~~~
@@ -0,0 +1,32 @@
1
+ ## Launched — 2026-10-03T18:10Z
2
+ Branch: worktree-fix-core-media-url-download-has-no-time-bound
3
+
4
+ ## Report — 2026-10-03T18:29Z
5
+ Outcome: blocked
6
+ Delivered: —
7
+ Blocked-on: f-bug-fix Step 12 confirm-before-archive — owner lands (brief: stop after local commits + gate)
8
+ Deploy needs: restart
9
+ Files changed:
10
+ - docs/debug/core-media-url-download-has-no-time-bound.md
11
+ - docs/ideas/url-download-drops-not-reported-to-agent.md
12
+ - docs/pending-updates/core-media-url-download-has-no-time-bound.md
13
+ - docs/pending-verification.md
14
+ - src/adapters/whatsapp-media.ts
15
+ - src/core/media.ts
16
+ - tests/media.test.ts
17
+ Siblings must know:
18
+ - `MediaReadBounds` and `MEDIA_READ_BOUNDS` (idle 30 s, total 10 min) moved from `src/adapters/whatsapp-media.ts` to `src/core/media.ts`; whatsapp-media.ts imports them from there and no longer exports them. S6 (whatsapp read bound) should change bound values only in core/media.ts.
19
+ --- final report (verbatim) ---
20
+ ~~~
21
+ S5 core-media-url-download-has-no-time-bound: fix complete on worktree-fix-core-media-url-download-has-no-time-bound (4 wip commits on origin/dev bb81c78), stopped at f-bug-fix Step 12 (confirm before archive) per the brief.
22
+
23
+ - Claim pushed: bb81c78 docs: promote core-media-url-download-has-no-time-bound to in-progress (cherry-picked onto origin/dev; local dev also carries ec9f61a).
24
+ - src/core/media.ts: MediaReadBounds / MEDIA_READ_BOUNDS moved here (one statement for both readers). The `within()` helper owns a ref'd timer and awaits the race. readBodyCapped bounds every read by min(idle, time left of total); when a bound fires it logs a warning, cancels the reader and throws UrlMediaTimeoutError. The fetch open is raced under the idle bound with an AbortController. A fired bound aborts the controller and reports onSkipped download_failed.
25
+ - src/adapters/whatsapp-media.ts: imports the bounds; no behaviour change.
26
+ - tests/media.test.ts: 6 tests (idle, total trickle, open, server stall after one chunk, invalid bounds refused, values pinned). Each of the idle, open and total bounds was negative-tested: with the bound disabled, its test went red at the test's own timeout.
27
+ - Session code review: no bugs; 3 hardening suggestions applied (trickle deflake, 4 s margins, abort the socket on read timeout).
28
+ - Gate: ~/check-mercury.sh check: 16/16 links, 6518 pass / 0 fail.
29
+ - Docs: the post-mortem is filled in docs/debug/core-media-url-download-has-no-time-bound.md (still In-Progress; Step 13 moves it to docs/debug/minor/2026-10-03-…). The residual is noted in docs/ideas/url-download-drops-not-reported-to-agent.md. docs/pending-updates/core-media-url-download-has-no-time-bound.md was added, and one MANUAL-VERIFY line was appended under "## Plan unattended-1003 / S5".
30
+
31
+ Next: owner resumes /f-bug-fix core-media-url-download-has-no-time-bound at Step 12 (confirm) → Step 13 archive → 14 rebase → 15 squash-merge + push.
32
+ ~~~
@@ -0,0 +1,64 @@
1
+ ## Launched — 2026-10-04T10:12Z
2
+ Branch: worktree-fix-whatsapp-media-read-bound-does-not-close-the-download-socket
3
+
4
+ ## Report — 2026-10-04T10:29Z
5
+ Outcome: blocked
6
+ Delivered: —
7
+ Blocked-on: f-bug-fix Step 12 confirm-before-archive — owner lands (branch worktree-fix-whatsapp-media-read-bound-does-not-close-the-download-socket @ b87f233, gate green, not squash-merged)
8
+ Deploy needs: sync (runs bun install: package.json + bun.lock changed, applies patches/) + restart; no image rebuild
9
+ Files changed:
10
+ - Dockerfile.host
11
+ - bun.lock
12
+ - docs/debug/minor/2026-10-02-whatsapp-media-read-hangs-when-the-body-errors-mid-download.md
13
+ - docs/debug/moderate/2026-10-02-whatsapp-media-download-buffers-before-size-check-without-file-length.md
14
+ - docs/debug/whatsapp-media-read-bound-does-not-close-the-download-socket.md
15
+ - docs/pending-verification.md
16
+ - package.json
17
+ - patches/@whiskeysockets%2Fbaileys@7.0.0-rc14.patch
18
+ - src/adapters/whatsapp-media.ts
19
+ - tests/whatsapp-ingest-gates.test.ts
20
+ - tests/whatsapp-media-socket-close.test.ts
21
+ Siblings must know:
22
+ - First `patches/` entry in the repo (Baileys 7.0.0-rc14, `patchedDependencies`). Every `bun install` from a copied package.json now needs `patches/` beside it, or it fails with "Couldn't find patch file" (Dockerfile.host fixed here).
23
+ - After this lands on dev, every check mirror that borrows ~/mercury-src/node_modules fails 6 tests in tests/whatsapp-media-socket-close.test.ts until the box syncs (bun install). The first test names the cause. Workaround: a real `bun install --frozen-lockfile` in that mirror.
24
+ - tests/whatsapp-ingest-gates.test.ts leaked its Baileys stub into every later suite (`{ ...realBaileys }` over a live namespace); fixed with a REAL_BAILEYS copy taken at import.
25
+ - `MediaDownloadOptions.download` now receives a third argument, `MediaDownloadContext` (signal, reuploadRequest).
26
+ - When archiving, add the post-mortem to tests/fixtures/release-gate/regression-map.json (category code), or the release-gate map test goes red.
27
+ --- final report (verbatim) ---
28
+ ~~~
29
+ S6 whatsapp-media-read-bound-does-not-close-the-download-socket — fixed on branch, gate green, stopped at the Step 12 confirm (owner lands).
30
+
31
+ Root cause, measured on Bun 1.3.14: destroying the Readable.fromWeb body (which our unpipe chain already did) releases the reader but leaves the socket draining. The server was still writing 18.5 MB later. An AbortSignal on fetch closes it within 1 ms, and Baileys' getHttpStream passed fetch no signal.
32
+
33
+ Fix 1: the first patches/ entry (bun patch, Baileys 7.0.0-rc14) threads options.signal into getHttpStream's fetch. downloadBytes aborts one AbortController on every exit. Tests run Baileys' real downloadMediaMessage against a local server and read the close on the server's side: idle, cap, total and open bounds, plus a clean encrypted body. Negative-tested: the 4 close tests go red with the abort removed, and red with the patch line removed.
34
+ Fix 2: the re-upload gets its own bound, WHATSAPP_REUPLOAD_BOUND_MS = 2 min (rationale in code), and the open bound is re-armed around it. Finding: on Baileys rc14 the re-upload is unreachable. It triggers on error.status, while its Boom error carries output.statusCode, so a 410 never asks the phone (measured: 0 calls). A test pins that, so a Baileys bump that makes the path live goes red.
35
+ Also: Dockerfile.host copies patches/ before bun install (it would have failed); the ingest-gates test's leaky Baileys mock restore is fixed; corrections are appended to both 2026-10-02 post-mortems; 3 MANUAL-VERIFY lines are under "Plan unattended-1003 / S6"; the post-mortem is written in docs/debug/ (not archived).
36
+ Gate: ~/check-mercury.sh check on the worktree — all 16 links passed, 6661 pass / 0 fail (the mirror has its own patched node_modules).
37
+ Not done (owner): Step 13 archive (minor/2026-10-04-…) plus a regression-map entry, the squash-merge, and the push. Local dev carries a duplicate of the pushed promotion commit (f204433 = 1333321); the next pull --rebase drops it.
38
+ ~~~
39
+
40
+ ## Report — 2026-10-04T11:05Z
41
+ Outcome: blocked
42
+ Delivered: —
43
+ Blocked-on: owner push — squash-merged into local dev as e3681f7 (archive + regression-map entry included), not pushed
44
+ Deploy needs: sync (runs bun install: package.json + bun.lock changed, applies patches/) + restart; no image rebuild
45
+ Files changed:
46
+ - Dockerfile.host
47
+ - bun.lock
48
+ - docs/debug/minor/2026-10-02-whatsapp-media-read-hangs-when-the-body-errors-mid-download.md
49
+ - docs/debug/minor/2026-10-04-whatsapp-media-read-bound-does-not-close-the-download-socket.md
50
+ - docs/debug/moderate/2026-10-02-whatsapp-media-download-buffers-before-size-check-without-file-length.md
51
+ - docs/pending-verification.md
52
+ - package.json
53
+ - patches/@whiskeysockets%2Fbaileys@7.0.0-rc14.patch
54
+ - src/adapters/whatsapp-media.ts
55
+ - tests/fixtures/release-gate/regression-map.json
56
+ - tests/whatsapp-ingest-gates.test.ts
57
+ - tests/whatsapp-media-socket-close.test.ts
58
+ Siblings must know:
59
+ - Local dev is red on one test that is not S6's: S2's post-mortem minor/2026-10-04-pi-failure-class-retries-a-content-filter-block.md (80eb998) has no regression-map.json entry.
60
+ - After the push, check mirrors borrowing ~/mercury-src/node_modules fail the socket-close tests until the box syncs (bun install).
61
+ --- final report (verbatim) ---
62
+ ~~~
63
+ Owner approved archive + squash-merge. Archived to docs/debug/minor/2026-10-04-whatsapp-media-read-bound-does-not-close-the-download-socket.md with a regression-map entry, rebased cleanly onto local dev (which carries S2's 80eb998), and squash-merged as e3681f7 (13 files). The worktree and branch were removed. Gate on the rebased tree: 6668 pass / 1 fail, the one failure being S2's post-mortem missing from regression-map.json. Not pushed.
64
+ ~~~
@@ -0,0 +1,78 @@
1
+ ## Launched — 2026-10-03T18:10Z
2
+ Branch: worktree-fix-pinchtab-session-inject-script-is-refused-401-and-reports-success
3
+
4
+ ## Report — 2026-10-03T18:20Z
5
+ Outcome: blocked
6
+ Delivered: —
7
+ Blocked-on: owner landing — /f-bug-fix Step 12 confirm (branch worktree-fix-pinchtab-session-inject-script-is-refused-401-and-reports-success @ 284b3ba, gate green, not squash-merged)
8
+ Deploy needs: restart (the derived image rebuilds on the first run, because its hash includes the new bins); no base rebuild
9
+ Files changed:
10
+ - docs/bugs/pinchtab-cookies-and-evaluate-endpoints-are-off-so-sessions-cannot-inject.md
11
+ - docs/debug/pinchtab-session-inject-script-is-refused-401-and-reports-success.md
12
+ - docs/pending-verification.md
13
+ - examples/extensions/pinchtab/bin/pinchtab_nav
14
+ - examples/extensions/pinchtab/bin/pinchtab_session_inject.cjs
15
+ - examples/extensions/pinchtab/index.ts
16
+ - tests/pinchtab-prompt.test.ts
17
+ Siblings must know:
18
+ - S8: the live eTLD+1 lookup moved out of the index.ts template literal into examples/extensions/pinchtab/bin/pinchtab_session_inject.cjs, as `siteKey(hostname)` (exported when required). The prompt no longer carries any JSSCRIPT heredoc. Compare lib/session-injector.ts against that function.
19
+ - New bug filed: docs/bugs/pinchtab-cookies-and-evaluate-endpoints-are-off-so-sessions-cannot-inject.md. pinchtab 0.13 answers /cookies and /evaluate with 403 by default, so injection still cannot succeed until the owner decides.
20
+ --- final report (verbatim) ---
21
+ ~~~
22
+ S7 — pinchtab-session-inject-script-is-refused-401-and-reports-success: fixed on the worktree branch, gate green, stopped for the owner to land.
23
+
24
+ Branch worktree-fix-pinchtab-session-inject-script-is-refused-401-and-reports-success (4 commits on top of claim 15508c4, which is pushed to origin/dev):
25
+ - examples/extensions/pinchtab/bin/pinchtab_session_inject.cjs (new):
26
+ - token: Bearer server.token, or PINCHTAB_TOKEN;
27
+ - sends X-Agent-Id: $PINCHTAB_AGENT_ID;
28
+ - checks every response status; 90 s ref'd bound;
29
+ - exits 0 injected / 3 no session / otherwise failed.
30
+ - examples/extensions/pinchtab/bin/pinchtab_nav (new): falls back to `pinchtab nav` on failure and says so on stderr.
31
+ - examples/extensions/pinchtab/index.ts: both ship as mercury.cli bins next to pinchtab_ensure; the prompt no longer defines a function or a heredoc.
32
+ - tests/pinchtab-prompt.test.ts:
33
+ - a fake 401-ing bridge exercises the real injector, plus stubbed pinchtab_nav paths;
34
+ - every guard was negative-tested.
35
+ - docs/bugs/pinchtab-cookies-and-evaluate-endpoints-are-off-so-sessions-cannot-inject.md (new, owner decision).
36
+ - docs/pending-verification.md: two lines under "## Plan unattended-1003 / S7".
37
+ - Post-mortem filled in docs/debug/pinchtab-session-inject-script-is-refused-401-and-reports-success.md. Not yet moved to moderate/: that is the archive step, after the owner confirms.
38
+
39
+ Gate: ~/check-mercury.sh check <worktree> → all 16 links passed, 6526 pass / 0 fail.
40
+
41
+ Measured in throwaway mercury-agent-ext:3947692c9434 containers (pinchtab 0.13.2, node 22):
42
+ - The bearer token and X-Agent-Id work.
43
+ - With the shipped config, /cookies answers 403 cookies_disabled. The injector now fails loudly and pinchtab_nav falls back with a notice.
44
+ - With allowCookies and allowEvaluate on, the cookie and localStorage land in the run's own tab.
45
+
46
+ Deploy: restart. The derived image rebuilds on its first run; verify in the installed snapshot ~/whatsapp-bot/.mercury/extensions/pinchtab/bin/ and in the derived image's /usr/local/bin.
47
+ No push, no PR, no merge (unattended rules). docs/pending-updates/ does not exist on dev and /f-bug-fix has no step that writes it, so nothing was written there.
48
+ ~~~
49
+
50
+ ## Report — 2026-10-03T22:23Z
51
+ Outcome: delivered
52
+ Delivered: pushed-unverified
53
+ Blocked-on: —
54
+ Deploy needs: restart (derived image rebuilds on first run); needs a push of local dev first
55
+ Files changed:
56
+ - docs/bugs/pinchtab-cookies-and-evaluate-endpoints-are-off-so-sessions-cannot-inject.md
57
+ - docs/debug/moderate/2026-10-04-pinchtab-session-inject-script-is-refused-401-and-reports-success.md
58
+ - docs/debug/pinchtab-session-inject-script-is-refused-401-and-reports-success.md
59
+ - docs/pending-verification.md
60
+ - examples/extensions/pinchtab/bin/pinchtab_nav
61
+ - examples/extensions/pinchtab/bin/pinchtab_session_inject.cjs
62
+ - examples/extensions/pinchtab/index.ts
63
+ - tests/fixtures/release-gate/regression-map.json
64
+ - tests/pinchtab-prompt.test.ts
65
+ Siblings must know:
66
+ - siteKey() now lives in examples/extensions/pinchtab/bin/pinchtab_session_inject.cjs (S8 compares against it)
67
+ - local dev was rebased onto origin/dev (duplicate claim commits dropped); 685f109 is local only
68
+ --- final report (verbatim) ---
69
+ ~~~
70
+ Landed locally on dev as 685f109 (squash), owner-instructed ("review and land the fix locally yourself"). NOT pushed.
71
+ Review: no functional findings; post-rebase gate caught two real defects, fixed on the branch:
72
+ the two new bins were committed 100644 (now 100755), and the post-mortem lacked a
73
+ release-gate regression-map row (added, category code). Gate after: 16/16 links, 6527 pass / 0 fail.
74
+ Post-mortem: docs/debug/moderate/2026-10-04-pinchtab-session-inject-script-is-refused-401-and-reports-success.md
75
+ Local dev reconciled first: rebased onto origin/dev, five patch-identical claim commits dropped,
76
+ 3488286 kept as 5819124. Worktree and branch removed.
77
+ Open: docs/bugs/pinchtab-cookies-and-evaluate-endpoints-are-off-so-sessions-cannot-inject.md (owner decision).
78
+ ~~~
@@ -0,0 +1,55 @@
1
+ ## Launched — 2026-10-04T10:12Z
2
+ Branch: worktree-fix-pinchtab-session-injector-lib-computes-a-different-etld1
3
+
4
+ ## Report — 2026-10-04T10:20Z
5
+ Outcome: blocked
6
+ Delivered: —
7
+ Blocked-on: confirm-completion prompt (f-bug-fix Step 12) — owner archives and lands
8
+ Deploy needs: none
9
+ Files changed:
10
+ - docs/debug/pinchtab-session-injector-lib-computes-a-different-etld1.md
11
+ - docs/pending-updates/pinchtab-session-injector-lib-computes-a-different-etld1.md
12
+ - examples/extensions/pinchtab/lib/session-injector.ts
13
+ - tests/pinchtab-prompt.test.ts
14
+ Siblings must know:
15
+ - examples/extensions/pinchtab/lib/ no longer exists; bin/pinchtab_session_inject.cjs siteKey() is the only lookup key, and tests/pinchtab-prompt.test.ts fails if a second .slice(-2|-3) definition appears anywhere under examples/extensions/pinchtab/
16
+ --- final report (verbatim) ---
17
+ ~~~
18
+ S8: pinchtab-session-injector-lib-computes-a-different-etld1. Fixed locally, not landed.
19
+
20
+ Branch: worktree-fix-pinchtab-session-injector-lib-computes-a-different-etld1
21
+ Worktree: .claude/worktrees/fix-pinchtab-session-injector-lib-computes-a-different-etld1
22
+ Three wip commits on top of origin/dev 45ea243 (the promotion, claim-pushed).
23
+
24
+ Fix: deleted examples/extensions/pinchtab/lib/session-injector.ts. Nothing imported it, and its extractDomain gave co.uk for www.example.co.uk. bin/pinchtab_session_inject.cjs siteKey() (S7's) is now the only lookup-key definition. The existing test already pins www.example.co.uk -> example.co.uk. A new test in tests/pinchtab-prompt.test.ts asserts the .cjs is the only file under the extension with a .slice(-2|-3) cut: red before the deletion, naming exactly that file, green after.
25
+ Post-mortem filled in docs/debug/pinchtab-session-injector-lib-computes-a-different-etld1.md (not yet archived to minor/). pending-updates file written.
26
+ Gate: ~/check-mercury.sh check on the worktree, all 16 links passed, 6650 pass / 0 fail.
27
+ Session code review: no issues (two non-blocking notes: the test comment cites the minor/2026-10-04-… archive path, so archive today or fix the comment; the guard is a ratchet for the .slice style only).
28
+ Overlap: preflight clean (exit 0); no PLAN-PASSED lines.
29
+ Deploy: none (unreferenced file deleted; installed snapshots drop it on the next restart).
30
+
31
+ Stopped at f-bug-fix Step 12 (confirm before archive/merge), as the brief requires.
32
+ ~~~
33
+
34
+ ## Report — 2026-10-05T09:29Z
35
+ Outcome: delivered
36
+ Delivered: —
37
+ Blocked-on: —
38
+ Deploy needs: none
39
+ Files changed:
40
+ - docs/debug/minor/2026-10-05-pinchtab-session-injector-lib-computes-a-different-etld1.md
41
+ - docs/pending-updates/pinchtab-session-injector-lib-computes-a-different-etld1.md
42
+ - examples/extensions/pinchtab/lib/session-injector.ts
43
+ - tests/fixtures/release-gate/regression-map.json
44
+ - tests/pinchtab-prompt.test.ts
45
+ Siblings must know:
46
+ - examples/extensions/pinchtab/lib/ no longer exists; bin/pinchtab_session_inject.cjs siteKey() is the only lookup key
47
+ --- final report (verbatim) ---
48
+ ~~~
49
+ S8 archived and squash-merged into LOCAL dev as 82b7f09 (owner: "archive and merge locally"). Not pushed.
50
+ Post-mortem: docs/debug/minor/2026-10-05-pinchtab-session-injector-lib-computes-a-different-etld1.md
51
+ Rebased onto local dev (which carries S2/S6 landings unpushed and has diverged from origin/dev by 7 napkin commits); not onto origin/dev.
52
+ Added the release-gate regression-map row the archived post-mortem requires.
53
+ Gate on the rebased tree: all 16 links passed, 6671 pass / 0 fail — after replacing the check mirror's borrowed node_modules with a real patched install (Baileys patch gotcha).
54
+ Worktree and branch removed.
55
+ ~~~