# #49 / #267 — bounded refresh observation plan

**Status:** prepared, **not admitted**. No execution grant. W2 run `20260912T231404Z` holds the
execution window. Author: todlando. Gater: doyle.

**What this observes:** whether a blocked controller sink reaches eviction across one planned brain
restart, and whether a PTY's output and input progress resume — with session and connection
attribution. **What it cannot decide:** the clean-close hypothesis. See §8.

---

## 1. Subject selection — bytes, not a path

| item | value |
| --- | --- |
| executable path | `C:\Users\decid\AppData\Local\spt-core\bin\spt` |
| **executable sha256 (the pin)** | `689503ad9fc6cfde637cbcacf21c42faf130ba465a0df6c356cf290e2adafb85` |
| reported version | `spt 0.69.0` |
| mtime at planning | 2026-09-10 21:14 |

**The sha256 is the pin, not the path.** Re-hash immediately before the run. **If the hash differs
from the value above, the run does not start**: the subject must be re-selected and this plan
re-admitted. A changed binary is a different subject, not a recorded detail.

**Source provenance — UNVERIFIED, and recorded as such.** The installed binary predates current
`main`, so **current `main` is not its source provenance**. The plausible release commit is v0.69.0 =
`16df0e41be04ca4614a792f7e70e4d1bdec60b24` (2026-09-10), which is consistent with the binary's
version string and mtime — but **I have not verified that these bytes were built from that commit**,
and no artifact on this box binds them. Every source citation in this plan (`brainproc.rs:1081`,
`broker.rs:2842`/`:3787`/`:2815`, `applyhost.rs:364`) is read at **current main** and is therefore a
claim about the source tree, **not** a proven claim about the executable under test. Treat any
line-number citation as indicative; if a run's behaviour disagrees with a citation, the citation is
the suspect.

## 2. Isolation — binding

**The resident fleet daemon is never refreshed.** At planning time 18 `spt.exe` processes authenticate
to the resident daemon (pid 4732): agent perches under `spt-mobile-host.exe`, `omp-spt` chains, and
this session's own perch.

- Isolated `SPT_HOME` under a run-specific directory, with its own broker socket.
- The isolated home **must not exist** beforehand. If it does, the run does not start and the
  existing directory is left untouched.
- One disposable PTY session, created for this run only.
- If the isolated daemon cannot be brought up cleanly within the §3 startup bound, **the run does not
  start**.

### 2.1 Exact targeting — every command, with identity checks before the two dangerous ones

**Every** command in the run carries the isolated home explicitly: `SPT_HOME=<ISO_HOME>` is set on
each invocation. No command relies on an inherited or ambient value, and no command is issued from a
shell whose environment was mutated once and trusted thereafter.

**Before `daemon refresh` and before `daemon stop`, all four checks must pass. Any failure and the
command does not run:**

1. `SPT_HOME` resolves to `<ISO_HOME>` — read back from the command's own environment, not assumed.
2. The broker socket the command will address is the isolated one, not the fleet's.
3. The target daemon pid was **created by this run**, established by ancestry.
4. The target pid is **not 4732**, and is not an ancestor of this session.

Refresh and stop are the only two verbs that act on a daemon's lifecycle; both are gated by all four.

## 3. Bounds — numeric, chosen now

**Two separate budgets, and a total.** The observation deadline **ends experimental activity and
starts cleanup — it never skips cleanup.** Cleanup holds its own maximum on top.

| budget | value |
| --- | --- |
| **observation budget** (all experimental activity, from run start) | **300 s** |
| **cleanup budget** (separate, begins when observation ends for any reason) | **120 s** |
| **total bound, run start to done** | **420 s** |

| phase | nominal cap | on breach |
| --- | --- | --- |
| isolated daemon up + session attached | 60 s | run does not start |
| `t0` appears after refresh is issued | 60 s | record "restart marker not observed"; continue within the observation budget |
| observation after `t0` | 120 s | stop observing; report recovery-after-interval or no-recovery-within-the-observed-interval |
| reattach: detach, then fresh attach | 30 s to attach, 60 s observed | report as its own arm |
| cleanup confirmation | 120 s (its own budget) | preserve everything and report unconfirmed (§6) |

**Every observation and reattach phase is additionally capped by the REMAINING observation budget.**
A phase's nominal cap is an upper bound, not an entitlement: if only 40 s of the 300 s remain, that
phase gets 40 s. **A phase cut short by the remaining budget is REPORTED AS TRUNCATED**, naming the
phase and the time it actually got — a truncated phase's result is never presented as a completed
observation, and its absence of an event falls under §8's "not observed" rule.

**When the observation budget is exhausted, experimental activity stops and cleanup begins
immediately** under §6, with its own 120 s. Exhausting the observation budget is a normal ending, not
an error, and it never licenses skipping or shortening cleanup.

**The observation budget is independent of `t0` because `t0` may never arrive.** Every phase cap is
relative to `t0` or to a local step; without a run-start-relative budget, a refresh that produces no
`BRAIN_UPDATE_RESTART` would leave the run with no defined end. If `t0` never appears, that absence
is a recorded result — and per §8 it is "not observed", not proof the restart did not happen.

## 4. Observation

`t0` = `BRAIN_UPDATE_RESTART` (brainproc.rs:1081, per §1's provenance caveat). Daemon stderr captured
to a file for the whole window, starting before the session is attached.

| id | question | observable | attribution |
| --- | --- | --- | --- |
| Q2 | does a blocked sink reach eviction? | `BRAIN_SUBSCRIBER_STALL_EVICT` (broker.rs:2842, :3787) | carries `conn=` and a session id |
| Q4 | what survives reattach? | §5, after one detach and one fresh attach | session id |
| Q5 | does output progress resume, and when? | §5 | session id |
| Q6 | is queued input preserved and applied in order? | §5 | session id |

**Q2 is not a deadline measurement.** `evict − t0` runs from the restart signal; the sink deadline
runs from **blocked-write onset**, which nothing emits.

### 4.1 Two observables added for Q1 and Q3, and exactly what each establishes

<!-- [doc->REQ-REFRESH-WAIT-ATTRIBUTION] -->

Q1 and Q3 were withdrawn as UNRESOLVED (§8) because both live on **silent branches**, not because the
plan lacked a token to look for. Both branches now emit, under the existing `SPT_PUMP_TRACE=1`
diagnostic gate and nothing else — the daemon reads its own environment and still exports nothing, so
hosted harness subtrees inherit no markers. Neither line changes a predicate, a deadline, an outcome,
the wire, or one byte of normal output.

| id | event | fires when | attribution |
| --- | --- | --- | --- |
| Q1 | `CONTROLLER_SLOT_CLOSED` | the epoch-guarded transition **actually empties** the slot | `conn=`, `cause=`, session id |
| Q3 | `BRAIN_PROMOTE_GATE_NOT_DRAINED` | the FIRST not-drained read after readiness latches, once per trial | trial `generation` |

**`cause=` is the caller's knowledge, never the site's inference.** `clear_controller` is shared by
three edges — a drain that timed out the controller's `send_timeout` (`send_deadline`), the wedge
evict (`stall_evict`), and an ordinary detach (`detach`) — and at that site they are
indistinguishable. `send_deadline` names the deadline the drain observed, NOT the writer's fate:
a writer blocked past its deadline has not been shown to have exited. Each caller passes what it knows.
A reader may therefore trust the cause exactly as far as the calling path, and no further.

**A guarded no-op emits nothing.** `mark_controller_gone` returns without taking when the epoch has
moved on; no line is written there. So the event means *a slot that was occupied is now empty*, and
never *something tried to close a slot*. That distinction is the whole of Q1: before this, a cleanly
closed old-generation conn and a slot that was already empty left identical stderr.

**What `BRAIN_PROMOTE_GATE_NOT_DRAINED` does NOT establish, stated because the wrong reading is the
tempting one:** it proves the DRAINED gate was **evaluated** and read not-drained. It is **not**
evidence that the heartbeat's drain-driving `brain.sessions()` call completed — that call is what
makes a wedge drainable at all (REQ-UPDATE-TRIAL-DRAIN-DRIVE), and a trial that never drove it looks
identical here to one that drove it and still waited. It also names **no session**: the gate answers a
single bool over every hosted session, and naming the wedged one would require a second, separately
taken scan that could disagree with the read the promote decision actually used. The narrower fact is
reported instead.

**Absence of either line remains "not observed", never established absence** — the §8 rule applies to
these two exactly as it applies to every other token here, and it applies twice over when the gate
env var was not set on the daemon under test.

## 5. Output and input progress — two clocks, and what each can prove

The disposable session runs a generator emitting, on its own cadence:

```
PROG <n> <generated-at>
```

and the observer records, for every line, **its own receipt timestamp**. **Both timestamps are kept
for every line.** They answer different questions and neither substitutes for the other:

- **generation timestamp + counter** — what the child did, and when the child did it.
- **receipt timestamp** — when the observer was delivered it.

**The trap:** after a freeze a burst arrives, and buffered output released late is indistinguishable
**by arrival alone** from progress resuming.

**What each combination establishes, stated exactly:**

- Pre-`t0` counters and pre-`t0` generation stamps, received after `t0` → **buffered output being
  released.** The transport resumed. It says nothing about the child.
- Counter above the pre-`t0` maximum **and** generation stamp after `t0` → **the child progressed
  after `t0`.** This does **not** establish that the child was executing *contemporaneously with
  delivery*: such a line may itself have been generated and then buffered before reaching the
  observer. **Arrival timing is evidence of delivery, never proof of contemporaneous child
  execution.**
- The **gap between generation and receipt**, per line, is the only figure that speaks to delivery
  lag, and it is reported per line rather than averaged.

Record the highest counter and latest generation stamp seen before `t0` explicitly — the baseline the
whole question turns on.

### 5.1 Input — three distinct observables, never conflated

During the freeze, send inputs each carrying a **unique tag**, never a repeated keystroke. For each
tag, three things are separately observable and are **recorded separately**:

1. **terminal echo** — the tag appearing in the stream. Proves the byte reached the terminal's echo
   path. **Proves nothing about the child.**
2. **the child's tagged acknowledgement** — output the child itself emits naming that tag.
3. **resulting state** — an effect the child produces that is distinct from both its echo and the
   tag's text.

"The queue drained" may be said only when every tag shows **2 and 3**, in the order sent.

**A missing tag at the observation deadline is "NOT OBSERVED WITHIN THE WINDOW" — not established
loss.** A late drain past 120 s and a genuine loss are indistinguishable to a finite observation, and
the plan records the former wording. A duplicated tag is **replay**; out-of-order tags are their own
finding. Byte counts and "output resumed" establish none of this.

## 6. Cleanup — retention, not deletion

- Stop the isolated daemon **under §2.1's four checks**; confirm its brain and broker are gone by pid
  and by socket, and that every process the run created is gone **by ancestry**.
- **Retain the isolated home and every captured artifact after shutdown** — stderr capture and PTY
  transcript included. No directory deletion is required by this observation.
- **If cleanup is uncertain, preserve — never remove.** An uncertain teardown leaves process
  identities and artifacts intact for inspection and is reported as unconfirmed.
- The captured log is copied out before anything else is considered.

## 7. What the source says, and what that is worth

`broker.rs:2806-2812` states the killed-generation mechanism in the code's own voice and contrasts it
with a cleanly-closed conn being "simply absent". **That docstring describes intended behavior. It is
not independent evidence that the mechanism occurs**, and this plan does not treat it as such.

Its exclusion of `by: Some` controllers from the promotion predicate **does not exclude indirect
effects on those viewports**. Connection identity is **mapped** from the run's own attribution
(`conn=`, session id) and never inferred.

## 8. Unanswered questions — explicit

1. **Q1 — does the outgoing generation's connection close?** **OBSERVABLE AS OF §4.1, still
   unanswered.** It was unresolved because the closure path is silent: `mark_controller_gone` →
   `clear_controller` re-stamps and fires the control edge while emitting nothing, and
   `BRAIN_CONN_RETIRED` has one emission site (`livehost.rs:1042`, a session-query error path) that
   would not fire here. `CONTROLLER_SLOT_CLOSED` now reports the real transition with its cause. The
   question is answered by a RUN, not by this change.
2. **Q3 — does promotion wait on drained state?** **OBSERVABLE AS OF §4.1, still unanswered.** It was
   unresolved because the gate is a pure read with no false-branch emission —
   `old_gen_drained` → `any_local_controller_wedged` → `local_controller_wedged` (broker.rs:2816).
   `BRAIN_PROMOTE_GATE_NOT_DRAINED` now fires once per trial on the first not-drained read, from the
   same observation the promote decision uses. It establishes that the gate was evaluated, never that
   the drain-driving heartbeat call completed (§4.1).
3. **Blocked-write onset is unobservable**, so the 15 s sink deadline cannot be confirmed here.
4. **Generation attribution is unverified** on every token except `BRAIN_PROMOTED`.
5. **Executable-to-source provenance is unverified** (§1). Source citations are claims about the
   tree, not about the bytes under test.
6. **A missing `t0`, a missing eviction, or a missing input tag are each "not observed"**, never
   established absence.
7. **One isolated daemon with one session is not the field's condition.** A negative result does not
   clear the field path and must not be reported as doing so.
8. **The clean-close hypothesis is not decidable by this experiment.**
9. **#302 is not merged into this.** Whether it relates is untested and deliberately left open.
