# RCA — `gears` / `lia` unreachable by `spt rc` and the picker

**Status:** mechanism LOCALIZED, not yet closed. Author: hertz, 2026-09-07.
**Specimen:** `.spt/preserved/gears-specimen/` (19 files, captured 2026-09-06T23:14:22Z, BEFORE
the stop/start that changed gears' state — unreproducible). Preserved out of a dead session's
`%TEMP%` on 2026-09-07; see `scratchpad-is-not-preservation`.

## Symptom

Operator-facing, live. Endpoints started in certain directories cannot be reached by `spt rc` or
the picker. `gears` (new) in `travelear` and `lia` (months old, previously healthy) in both
`travelear` and `liaison` both fail; `lia` works in `Documents/projects`.

## ⚠ The directory correlation is NOT yet a mechanism

`projects` is pinned as a startup endpoint for `lia` in `daemon.json`
(`startup_endpoints[].cwd = C:\Users\decid\Documents\projects`), confirmed firing in the log as
`ENDPOINT_AUTOSTART:lia adapter=claude-spt session=5203aed4eedda180 pid=27368`. That explains why
`projects` is her *habitual* cwd. It is NOT the cause: the currently-serving `lia` is pid 53212 /
started 17:38, not the autostart instance (pid 27368). Autostart replay runs at daemon boot ONLY
(`autostart.rs:38`), so it does not clobber a running endpoint.

Eliminated this session, measured, do not re-litigate:

- **Git repo — DEAD.** Neither `liaison` nor `travelear` nor `projects` is a git repo. All three
  fall back to folder name in `project_id_for_dir`. (The earlier note recorded this as refuted for
  a different, weaker reason.)
- **Project index — DEAD.** `lia`'s index row carries all three refs (`projects`, `travelear`,
  `liaison`) with clean id/display; `gears` resolves to `travelear`. Nothing is being dropped.
- **Dup-session guard (`REQ-RUN-NO-DUP-SESSION`) — DEAD.** Zero `ENDPOINT_ALREADY_LIVE` /
  `ENDPOINT_CREATE_CONFLICT` in the entire 294 MB daemon log. That guard never fired.

Surviving correlation only: every working endpoint cwd is a git repo *except* `projects` itself;
both failing dirs are non-git. `projects` being non-git-and-working stops that from being a cause.

## The evidence

`gears` spawn, from the specimen (wall_ms):

```
1788735069384  controller-attach   conn=701013 session=25 endpoint=gears by=local
1788735069399  write-start
1788735069520  writer-exit         role=controller session=25 reason=channel-closed
1788735069536  session-detach      was_controller=true
1788735069546  RC_ESTABLISH        <- CLIENT logs success, 10 ms AFTER the detach
1788735075765  harness boot        <- 6.4 s later
```

Baseline at capture: `SUBSCRIBE_DECISION gears: 0`, `stream-subscriber gears: 0`,
`RC_ESTABLISH gears: 3`, harness pid alive. Three operator attempts over 8 minutes
(22:51:09, 22:51:52, 22:59:36), all three `RC_ESTABLISH ... session=25`, zero decisions.

**The `writer-exit channel-closed` arc is NOT the defect.** `lia`, which works, shows the identical
arc on conn 713146 (session 32). Reading that arc as the failure was wrong; the working control
refutes it.

## What actually separates working from failing

| | `lia` (works) | `gears` (fails) |
|---|---|---|
| `role=brain controller` attach → `writer-exit` → detach | yes | yes |
| second conn `role=brain stream-subscriber` | **yes** (conn 713153, stream 415079) | **never** |
| `SUBSCRIBE_DECISION` | `decision=controller` | **0, always** |

The failing half is that the **stream-subscriber seat is never installed**.

## Localization

`nethost.rs:545 begin_attach` installs the subscriber seat and is where the conn is described
`stream-subscriber stream={id}`. Its refusal path —

```rust
if prior.poisoned.load(Ordering::Acquire) && !prior.gone() && !prior.is(&sub) {
    return Err(io::Error::new(io::ErrorKind::WouldBlock,
        format!("stream {} subscriber busy: prior subscriber still draining", self.stream_id)));
}
```

— returns **above** the `sub.describe(...)` call. A refused install therefore emits no
`stream-subscriber` line, seats no controller, and never reaches `dispatch_subscribe`.
That is precisely the `gears` signature.

**Ordering is evidence, not inference:** `lia`'s conn is described
`role=brain stream-subscriber stream=415079 controller session=32 endpoint=lia` — the
`stream-subscriber` description is applied in `begin_attach` BEFORE the controller description,
so `begin_attach` is upstream of `dispatch_subscribe`.

### ⚠ The poisoned-seat mechanism is REFUTED — measured, do not revive it

The attractive reading was a permanently-wedged prior seat: a poisoned prior subscriber that never
becomes `gone()` refuses every later attach forever, matching 3 attempts / 8 minutes / 0 decisions
better than any startup race. **That is dead.** doyle's free discriminator (2026-09-07): the poison
IS written even when the refusal is not — `nethost.rs:213` fires
`lifecycle_event("stream-sub-writer-poison", halt_reason)` on every writer poison.

Measured: **`stream-sub-writer-poison` = 0** across the entire 294 MB live daemon log AND the
specimen. The zero is INFORMATIVE, controlled on the same emitter and the same log:

| event (same `lifecycle_event` channel) | count |
|---|---|
| `stream-sub-attach` | 262,162 |
| `controller-attach` | 52 |
| `writer-exit` | 44 |
| `stream-sub-writer-poison` | **0** |

262k sibling events prove the emitter is live, so a poison would have printed. `begin_attach`'s
`WouldBlock` arm requires `prior.poisoned == true`, so that branch **can never have fired** and
`gears` is not dying there. Precondition absent; the seat is still never installed, but not for
this reason.

### ⚠ These zeros are NOT evidence

`"no such stream"` 0 · `"no such session"` 0 · `"subscriber busy"` 0 · `"replay halted"` 0.

All four are `Err` **payload strings**, not `lifecycle_event` emissions — nothing establishes they
would ever reach the log, so their zeros are structurally uninformative. They are **UNMEASURED,
not cleared.** This is the same trap the `// busy refusal: quiet, as before` comment documents,
and the reason the panel below must cover every outcome rather than the arms we happen to suspect.

## ⚠ Consequence for the blind-panel bounce — READ BEFORE SPENDING IT

`fix/rc-subscribe-blind-panel` @`bd3a337b` instruments the two early returns inside
`dispatch_subscribe`. The evidence above says the `gears` failure dies **upstream of that
function**. **Prediction, falsifiable: deploying that patch and bouncing the daemon will print
nothing for `gears` and buy nothing for this defect.** The bounce costs 11 live perches. The patch
remains correct and worth landing on its own merits — it is simply not the instrument for this.

## ⚠ Two blind panels, not one

`begin_attach`'s busy refusal is silent by construction. The presence call site says so in the
source: `nethost.rs:2246` — `let _ = ...begin_attach(...); // busy refusal: quiet, as before`.
So `grep "subscriber busy"` returning **0 across the whole log proves nothing** — a refusal that is
never written cannot be counted. Same trap: `stream-sub-attach` lines carry no endpoint field, so
"0 for gears" there is a structurally uninformative zero, not evidence.

## Next

1. **Widen the panel to EVERY `begin_attach` outcome** — installed / displaced / refused, each with
   stream id + the prior seat's `poisoned` / `gone()` / `is(sub)` flags + prior sub identity,
   emitted before each return (doyle-scoped after the poison discriminator came back zero: the
   breadcrumb must answer whichever arm `gears` takes, not the arm we suspected and lost).
   Add the `subscribe_stream` NotFound door at `nethost.rs:2171` (`no such stream`) in the same
   pass — it sits one frame above `begin_attach` on the same call, would produce an identical
   `gears` signature, and is unmeasured for the reason given above.
   Needs the same bounce; paired with the blind-panel patch so ONE bounce lights every door on
   this path rather than two of five.
2. The rc CLIENT pump (`drive_established`) needs no bounce and is still unexercised.
3. Still missing, and only the operator can produce it: the exact command + exact output of a
   failing `lia`-in-`liaison` run.

`RC_ESTABLISH` being written 10 ms after the broker's own detach is a reporting defect in its own
right — the client declares establishment its peer has already torn down.

---

## The panel — `b5eeab0a` (built? NO. See status below)

Four tokens, one regex, so a reader never has to know which door to suspect:

```
grep -aE "stream-sub-(attach|refused|displace|no-stream)" daemon.stderr.log
```

`stream-sub-attach` is the INSTALL mark (already shipped, 262k occurrences — no new line was added
for that arm). The other three are new, each emitted before its own return.

### Falsifiable predictions, registered BEFORE the data exists

Written now, deliberately, so no arm can be rationalised after the fact. For a failing `gears`
attach, exactly one of these should hold:

| what the panel shows | what it proves | where blame moves |
|---|---|---|
| `stream-sub-no-stream` | the stream id was never registered in the host's table; `begin_attach` was never reached | stream OPEN / registration — `net_open_stream_classed`, upstream of everything measured so far |
| `stream-sub-refused` | the busy arm DID fire | ⚠ contradicts `poison = 0`: `prior_poisoned` cannot be true with zero poison events, so the poison `lifecycle_event` is being LOST, not the flag. Instrument the emitter, not the seat |
| `stream-sub-displace` with `prior_same=false` | something else holds/steals gears' seat | a competing subscriber — identify it by `prior_conn` |
| `stream-sub-attach` present, `SUBSCRIBE_DECISION` still absent | the seat installs fine; the failure IS in `dispatch_subscribe` after all | `bd3a337b`'s two doors speak — the original blind-panel patch was right and my upstream localization was wrong |
| **none of the four** | the subscribe never reaches `nethost` at all | the rc CLIENT — `net_stream_subscribe` returning `Ok` without delivering. **Needs no bounce**; the client pump is drivable directly |

The last row is why the bounce is worth its cost: it is decisive **either way**. A silent panel
falsifies the entire daemon-side hypothesis and moves the whole investigation client-side, which
is a result, not a null.

## Status

- `b5eeab0a` — panel committed on `fix/rc-subscribe-blind-panel`. `traceable-reqs check` exit 0,
  zero FAIL/MISSING.
- **BUILT AND VERIFIED.** Window 2026-09-07T02:28:08Z → 02:31:48Z (cargo: 2m36s), cold in MAIN
  with the pool claimed at lane start (`panel-b5eeab0a`, base `b5eeab0a5d6c`). `claim_exit 0`,
  `build_exit 0`, read from separate exit files.
  `target/debug/spt.exe`, 69,548,544 bytes, spt 0.67.1,
  sha256 `5b4ef2e3d3dd6f3a9ba4a42f99977030887480f66f620363108bde06bddba7c8`.
  Verified **in the binary**, not the diff: four panel doors (1 site each), `bd3a337b`'s two
  `SUBSCRIBE_REFUSED` doors, and the detail fields — `prior_poisoned` / `prior_gone` /
  `prior_same` / `prior_conn` at 2 each (refused + displace arms), `live_streams` 1.
  **Six doors in one binary; one bounce lights all of them.**
  Preserved at `.spt/preserved/spt-b5eeab0a-panel.exe` (`cp -p`, hash identical).
- The bounce remains the OPERATOR's call. **Try the no-bounce instrument first:**
  `.spt/lia-capture.sh` captures the next failing attempt with no restart, and an
  `RC_ESTABLISH` + zero-`SUBSCRIBE_DECISION` + zero-door slice is the client-side row of the
  prediction table — which would make the 11-perch bounce unnecessary.
