[Bug]: mail addressed to a bare terminal handle has no reader — reply and runtime escalation write there, and no check variant returns it
- Dominant language
- TypeScript
- Stars
- 71.3k
- Forks
- 4.7k
- Avg merge
- 14h 54m
- Merged PRs (30d)
- 520
Description
## Summary
Orchestration mail lives in two row families, and only one of them has a reader.
A message addressed to `run:` (or `dispatch:`) is returned by `orchestration check`. A message addressed to a bare terminal handle is not — not by `check`, not by `check --peek`, not by `check --all`, and not by `check --terminal ` — as soon as the recipient's pane is bound to a Run or holds an active Dispatch, which is exactly the state every coordinator and every supervised worker is in. The row is stored, the sender gets `ok: true`, and the recipient's only way to see it is `orchestration inbox`, which never marks anything read.
First-party code writes into that family:
- `orchestration reply` always addresses `original.from_handle` (`src/main/runtime/rpc/methods/orchestration.ts:1061`). Reply to a message that came from a Run-bound coordinator, and the reply lands where that coordinator's `check` will never look.
- Group addresses (`@all`, `@worktree:`) fan out to one bare-handle row per recipient (`orchestration.ts:650`).
- The runtime's own agent-exit escalation is addressed to `run.coordinator_handle` (`src/main/runtime/orca-runtime.ts:15246`).
- `send --to ` is documented in `--help` as `legacy_handle`, but it is accepted, is filed under the caller's current Run, and is stored with `delivery_contract = 'current_delivery'`.
## Reproduction
Orca 1.4.176, macOS. Every line below is real output, trimmed to the fields that matter.
```
$ orca orchestration run-create --objective "scratch: mail delivery reproduction" --json
run: run_9b5e0cd948ae
coordinator_handle: term_e796f595-5803-45d0-8ad6-498999dd8eb5
$ orca orchestration send --to run:run_9b5e0cd948ae --subject "run-addressed status" --type status
$ orca orchestration send --to term_e796f595-... --subject "handle-addressed status" --type status
$ orca orchestration check --json
runId run_9b5e0cd948ae, deliveryId delivery_41f69edaa765, count 1
to_handle "run:run_9b5e0cd948ae", subject "run-addressed status"
$ orca orchestration check --all --json # count 1 — the run row only
$ orca orchestration check --peek --json # count 1 — the run row only
$ orca orchestration check --terminal term_e796f595-... --peek --json
count 1, runId run_9b5e0cd948ae # still the Run mailbox, not the handle's mail
$ orca orchestration inbox --terminal term_e796f595-... --json
count 1, to_handle "term_e796f595-...", subject "handle-addressed status"
```
Repeating both sends for every message type gives a clean split. Run-addressed: all seven of `dispatch`, `merge_ready`, `escalation`, `handoff`, `decision_gate`, `question`, `heartbeat` came back from `check --peek` (the `status` row above had already been acknowledged by then). Handle-addressed: none of the eight stored types came back from anything. `worker_done` is refused for a sender with no active Dispatch, which is correct and unrelated.
`--wait` does not bridge it either, because `notifyMessageArrived` keys waiters by address, so a handle-addressed row never wakes a coordinator waiting on its Run:
```
$ orca orchestration check --ack delivery_41f69edaa765 --json # run mailbox now empty
$ orca orchestration check --wait --timeout-ms 5000 --json
count 0, timedOut true # 5s elapsed, two unread handle-addressed rows pending
```
And `reply`, with no `--to` involved at all:
```
$ orca orchestration reply --id msg_4d8f3d5203b2 --body "reply body" --json
to_handle "term_e796f595-...", run_id "run_9b5e0cd948ae", delivery_contract "current_delivery"
$ orca orchestration check --peek --json
count 0
```
Reading the store afterwards (`~/Library/Application Support/orca/orchestration.db`), the scratch Run:
| family | rows | read | delivered_at set |
|---|---|---|---|
| `run:` | 8 | 1 | 0 |
| bare terminal handle | 9 | 0 | 0 |
The one read row is the one `--ack` acknowledged. Across this whole store, 238 of 340 message rows are bare-handle rows and 116 of those are unread.
## Where it stops in the source
`orchestration.check` picks a mailbox before it picks a mode (`src/main/runtime/rpc/methods/orchestration.ts:698`). If the caller's pane is bound to a Run, every path inside that branch reads the Run address only — the FIFO Delivery via `getOrCreateRunDelivery` (`orchestration/db.ts:2735`, `WHERE run_id = ? AND to_handle = ?` with `to_handle = 'run:'`), and `--peek` / `--all` via `getRunMailboxHistory` (`db.ts:2821`, same `to_handle` predicate). The per-handle branch at `orchestration.ts:926` is unreachable for that terminal. A worker with an active Dispatch takes the equivalent branch at `orchestration.ts:876` and reads `dispatch:` only.
Push-on-idle is the one mechanism that does read bare-handle rows (`orca-runtime.ts:31935`, `deliverPendingMessages` defaults `mailboxHandle` to the terminal's own handle). It requires a live, writable, locally-observed-idle agent pane, and it types the message into the pane rather than returning it to a caller. It is not a pull path, and it cannot fire for a coordinator that is currently blocked in `check --wait`.
## About the `delivered_at` / `read` columns
We came into this expecting the columns to prove non-delivery, and they do not. They are worth stating precisely so the next person does not chase them:
- `delivered_at` is stamped only by push-on-idle (`markAsDelivered`). A Run-mailbox row read through `check` and acknowledged is `read = 1` with `delivered_at` still `NULL`. `delivered_at IS NULL` therefore does not mean undelivered.
- `read` on a Run-mailbox row is set only by `check --ack ` (`db.ts:2804`).
- The "every heartbeat is read and delivered, nothing else is" shape is not evidence that heartbeats reach anyone. `suppressEarlierHeartbeats` calls `markAsReadAndDelivered` on the earlier heartbeats of a Dispatch when its `worker_done` arrives (`orchestration/lifecycle-reconciliation.ts:346`). It is garbage collection, not delivery. In our store that accounts for 21 of 25 read run-mailbox heartbeats.
For the 2026-08-10 wave that sent us looking (`run_4bef7c0c058c`), the eleven `worker_done` rows are all correctly addressed to `run:run_4bef7c0c058c` and all unacknowledged. Those were readable by `check` the whole time; nothing lost them. That part of our report was wrong, and we are correcting it here rather than filing it.
## Why it matters
The two families are indistinguishable at the send site. Both are accepted, both return `ok: true`, both are filed under the same Run with the same `current_delivery` contract, and both show up in `inbox`. Only the address decides whether the recipient can ever pull the message, and the address is chosen for you by `reply` and by the runtime's escalation path. A coordinator that trusts its mailbox therefore misses replies to its own messages and misses the runtime's own "agent exited unexpectedly" escalation, with no error anywhere to explain the silence. That is what pushes wave orchestration back to polling the filesystem for artifacts.
## Smallest change that would fix it
We think this is a design call rather than a patch, which is why there is no PR attached. The question is which reader owns the legacy family, and the options are not equivalent:
1. **Give the recipient a pull path.** Have `check --peek` / `check --all` on a Run-bound pane also return that terminal's own bare-handle rows. Read-only surfaces, so the Delivery/ack protocol is untouched, and `--all` starts matching its documented meaning ("return every message for the handle"). It does not fix `--wait`, and it leaves two families visible in one listing.
2. **Reroute at the writer.** Make `reply` (and the runtime escalation) address the recipient's Run mailbox when the recipient is Run-bound. This is what a caller expects, but it folds ad-hoc conversation into the fenced-Delivery protocol lifecycle mail depends on, where a new consumer generation replays it — the same objection that argued against rerouting group fan-out in #13639.
3. **Alias the handle to the mailbox at read time.** Have the Run mailbox query accept `to_handle IN ('run:', '')`. One reader, no writer changes, but it silently merges the family that fan-out deliberately keeps separate for independent read tracking (§4.5).
Whichever is chosen, the existing rows deserve a decision too: 116 unread bare-handle rows in one store are not reachable by any command that can mark them read.
## Relation to existing issues
- #13363 and PR #13639 cover the **sender** side of the same split: the send receipt does not tell you the address is unreadable. #13639 states the invisibility rule and reports it as a warning, and deliberately does not reroute. It changes `orchestration.send` only. `reply` gets no warning and no check from it, and no sender-side warning helps a recipient with rows already in the store.
- #11787 asked for Run-mailbox mail to be pushed on idle. That landed in #12988 (`orca-runtime.ts:31232` pushes `run:` for the bound pane), and it is the opposite direction from this report.
- #10406 is about addressing the wrong handle after a restart. Here every handle is live and correct; the reader simply never queries it.
## Environment
- Orca 1.4.176, macOS 15 (Darwin 25.4.0)
- Source read at `upstream/main` c0c893d171 (1.4.178-rc.2)
Contributor guide
Assessment
This issue has not been assessed yet.