diff --git a/11-Features.md b/11-Features.md index fcfe1c8..d9b30db 100644 --- a/11-Features.md +++ b/11-Features.md @@ -51,6 +51,7 @@ six weeks, and the table alone will not carry it. | [Durable reply inbox](#durable-reply-inbox) | `broker:` | CB-307 | `msg/AmqpReplyInbox` | | [Set a member's auto-compact window](#set-a-members-auto-compact-window) | per-profile `autoCompactWindow:` | CB-636 | `member/ClaudeCodeLauncher`, `member/OpenCodeLauncher` | | [Leads on different hosts talk over a shared broker](#leads-on-different-hosts-talk-over-a-shared-broker) | `coordinator:` | CB-637 | `msg/LeadMailbox`, `msg/LeadCoordLoop` | +| [A lead can read its own held peer mail](#a-lead-can-read-its-own-held-peer-mail) | `coordinator:` | #421 | `mcp/FleetMcp`, `auth/Authz` | | [Draining an inbox is lead-only, on both entry paths](#draining-an-inbox-is-lead-only-on-both-entry-paths) | (always on, with `auth:`) | #272 | `mcp/FleetMcp.pollAction` | | [Broker password out of the config](#broker-password-out-of-the-config) | `broker.uriEnv:` | CB-635 | `config/FleetConfig` | | [An unreachable broker does not stop the daemon](#an-unreachable-broker-does-not-stop-the-daemon) | (always on) | CB-635 | `Fleetd.selectReplyInbox` | @@ -2324,6 +2325,34 @@ coord-id it was already told. --- +## A lead can read its own held peer mail + +**What.** `fleet_poll{coordId: }` returns this daemon's held lead-to-lead +messages in full, and does not ack them. The messages stay held, so reading twice returns the same +bodies. It reads only *your own* mailbox: passing a peer's coord-id is refused with a message saying +so, because no route exists for reading another daemon's mail. + +**On.** Nothing to configure beyond the `coordinator:` block that lead-to-lead messaging already +needs. Pass your own `selfId` — `fleet_list` reports it as `coordinator.selfId` — as `fleet_poll`'s +`coordId`. `fleet_list` also now reports `heldCount` and `heldDurable` beside `held[]`. + +**Why.** A lead could see *that* it had peer mail but not read it. `fleet_list`'s `held[]` carries +only an 80-character preview, on purpose, so a constantly-called roster scan never dumps a +coordination body. `fleet_poll{target}` drained the wrong store — a worker's reply inbox, not the +coordinator mailbox — and returned an empty list with no error. The only other route, `fleet_ack`, +would have destroyed the message before anyone read it. So the full body was reachable in principle +and unreachable in practice, and the busiest leads were the ones most affected: delivery into a +lead's pane is status-gated, so a lead that never goes idle never drains its own mailbox. + +**Gotcha.** This is **primary-only**, under a new authorization action `COORD_READ`. That includes +refusing an architect, which holds the ordinary `READ` action today. The reason is that `READ`'s +grant rests on the roster carrying no secrets, and a lead-to-lead body is not the roster — it is +where leads discuss host shapes, credential states and unmerged work. Also do not read +`mailbox.pending: 0` as an empty mailbox: `pending` counts only broker-ready messages, so a healthy +mailbox holding real mail reports zero. `heldCount` is the honest number. + +--- + ## Fleet health can finally reach its fault states **What.** `FleetHealthMonitor` ticks on its own timer, well away from the 250ms delivery poller. Each diff --git a/7-Use-Cases.md b/7-Use-Cases.md index e8a2c1e..fd5b078 100644 --- a/7-Use-Cases.md +++ b/7-Use-Cases.md @@ -443,6 +443,7 @@ the merge — and merging on a reviewer's word is delegating it by proxy. | Message a **peer lead** on this host | `fleet_send{sessionId: , content}` — `fleet_list` → `leads` reports it. Coordination only, **never** a task | | Message a **peer lead** on another daemon or host | `fleet_send{coordId: , content}` — needs a `coordinator:` block; your own coord-id is in `fleet_list`. Coordination only, **never** a task | | Answer a peer lead that messaged you | `fleet_send{coordId}` — or `{sessionId}` if they are on this host. **Not** `fleet_reply`: it has no peer route and the publish is refused | +| Read your own held lead-to-lead mail (no ack) | `fleet_poll{coordId: }` — primary-only; never acks, so `fleet_list`'s `held[]` still shows it after. `fleet_list`'s `held[]` gives only a truncated preview — this is the only way to read the full body | | Collect a held reply | `fleet_poll{target}` · then `fleet_ack{target, msgId}` | | Tear down a member | `fleet_stop{paneId}` |