<!-- [doc->REQ-SEND-WINDOW-DRAIN-HONOR] F-035 RCA — report-before-fix. -->
# F-035 RCA — idle-edge parked-drain ignores the delivery-window tag

**Status:** RCA-first, **report before fixing** (doyle-dispatched 2026-07-09).
**REQ:** `REQ-SEND-WINDOW-DRAIN-HONOR` (registered, `required_stages = []` — activate on doyle's fix ruling).
**Investigator:** todlando. **Off:** `origin/main@2c05dc9` (v0.30.2).

---

## Symptom class

A message tagged **`active_only`** — whose entire contract is *"active hook window
only, **never wakes**"* — gets **injected into an idle agent** on the active→idle
edge. The canonical producers:

- `spt send --active-only` (and the hidden back-compat alias `spt send --deferred`)
  — `cli.rs:85` documents the flag verbatim as **"never wakes an idle target"**.
- `send_deferred(...)` shell-context background rows (`reporting.rs:161`) —
  hook-channel background context that must ride only the active poll.

For an **spt-hosted, relay-less** endpoint (the class the idle-edge drain targets),
such a row is delivered through the translation binary at the idle window — the
exact delivery the tag forbids.

---

## Root (pinned)

The two delivery-window surfaces are **asymmetric**. One honors the `window` axis;
the other keys on the legacy `deferred` mirror and **never consults `window`**.

### The delivery-window model (ADR-0028 W2, `spt-store/src/spool.rs`)

| window        | active hook poll | idle / wake |
|---------------|:----------------:|:-----------:|
| `default`     | ✅               | ✅          |
| `idle_only`   | ❌ (suppressed)  | ✅          |
| `active_only` | ✅               | ❌ **never wakes** |

`deferred` is a **retained back-compat mirror**: `deferred = 1` **IFF**
`window = 'active_only'` (`deferred_for`, spool.rs:46).

### ACTIVE drain — honors the axis (correct)

`spool::drain_active_window_inner`:

```
include_deferred ? "WHERE delivered=0 AND \"window\" != 'idle_only'"
                 : "WHERE delivered=0 AND \"window\" = 'default'"
```

Filters on **`window`** — `idle_only` is excluded from the hook poll. Correct.

### IDLE-edge drain — ignores the axis (the bug)

`spool::claim_idle_edge_inner` (reached via `spt_daemon::inject::drain_idle_spool`
— the **one shared leg** the active→idle edge drain `delivery.rs:drain_idle_window`,
the send-time already-idle path, and the pulse re-offer belt all call):

```
include_deferred ? "WHERE delivered=0"
                 : "WHERE delivered=0 AND deferred=0"
```

Filters on the **`deferred` flag only** — **never on `window`**.

`drain_idle_spool` passes:

```
let include_deferred = !crate::resting::deferred_held(&perch_path);
```

So whenever the endpoint is **not resting**, `include_deferred = true` → the idle
claim is `WHERE delivered=0` → it takes **`active_only` (deferred=1)** rows and
injects them at the idle window.

### Why it slipped through

The drain **conflates two orthogonal axes**, using the `deferred` flag for both:

1. **Resting gate** (REQ-INST-6 / KH 1.4): *hold deferred rows while the owner is
   dormant.*
2. **Delivery window** (ADR-0028 W2): *`active_only` is hook-poll-only, always.*

The `deferred = 0` branch **happens to** exclude `active_only` — but it only fires
while resting. The not-resting path (the common case) leaks. The window contract
is an **invariant**, not a resting-conditional one, so gating it on rest state is
the category error.

---

## Candidates refuted

- **"`active_only` rows never reach this spool."** Refuted — `spt send --active-only`
  / `--deferred` and `send_deferred` both mint them into the same per-perch spool
  `claim_idle_edge_inner` reads. Live, not theoretical.
- **"The resting gate already handles it."** Refuted — it only excludes them while
  *resting*; the active→idle edge on a non-resting endpoint is the leak path.
- **"`idle_only` is symmetric-broken too."** No — `idle_only` has `deferred = 0`, so
  both idle-claim branches include it; it correctly rides the idle window. Only the
  `active_only`→idle direction is wrong.
- **"It's `drain_idle_window`'s caller."** No — the caller correctly fires on the
  edge; the defect is the SQL selection in `claim_idle_edge_inner` (the shared spool
  leg), so every one of the three callers inherits it.

---

## Fix shape (doyle to rule — NOT yet applied)

Mirror the active drain: make the idle claim honor the **`window` axis**, excluding
`active_only` unconditionally, and keep the resting hold as an **orthogonal** filter.

```
-- idle-window eligibility (mirror image of drain_active_window_inner):
WHERE delivered=0 AND "window" != 'active_only'
```

The resting gate then narrows *within* the idle-eligible set (hold rows while
dormant) rather than *defining* it. Net effect: `default` + `idle_only` ride the
idle window; `active_only` never does; the two drains become mirror images on the
`window` axis.

**Open ruling for doyle:** whether the resting `include_deferred` parameter on the
idle claim is now **vestigial** (since `active_only` — the only `deferred=1` class —
is excluded by the window filter regardless) or still meaningful. Recommend
collapsing it once the window filter lands, but that is a design call.

---

## Proposed gate (on activation)

- **unit** (`spool`): at the idle window, `claim_idle_edge_*` **excludes** an
  `active_only` row and **includes** a `default` and an `idle_only` row — independent
  of resting state (the resting-orthogonal assertion).
- **int**: an `active_only` row present at an active→idle edge (endpoint **not**
  resting, live translation binary) is **NOT** injected — it stays spooled for the
  active hook poll — while a `default` and an `idle_only` row on the same edge **are**
  injected. Red-first: pre-fix the `active_only` row injects.

Kin: `REQ-MSG-DELIVERY-AXES`, `REQ-MSG-IDLE-EDGE-DRAIN`, `REQ-IDLE-PARKED-DELIVERY`,
`REQ-INST-6` (the resting gate it conflates).
