{
  "summary": "## Verdict\n\nThe symptoms are related by one stale-positive outcome, but they are **not one defect**:\n\n1. **Raw terminal/PTY/window close without `Ctrl-B, D` is a separate transport-lifetime defect.** The operator process disappears without sending stream FIN. The broker intentionally retains the opened network stream after that Brain connection dies, so the target `serve_attach` worker never receives `NetStreamEof`, never calls `detach_session`, and the live session keeps its controller slot. `controlled`/`driven_by` therefore remain truthfully converged to the broker’s stale in-memory slot. The harness child can remain alive; ONLINE is consequently independent and may still be correct.\n2. **`/exit` is supposed to take the previously fixed 7.27 child-reap path.** When `/exit` actually terminates the hosted harness child, the broker exit waiter removes the session and calls `stamp_reaped`, which clears `controlled`, `driven_by`, and `viewer_count`. It does not require the operator transport to detach first. A persistent control stamp after a genuine child exit is therefore not the raw-close defect; the remaining source-visible explanation is the poll/reap race below, or that `/exit` ended only the client/viewport rather than the broker-owned child.\n3. **Daemon-restart non-convergence for the observed `emphasys` record (`controllable=false`) is a separate cleanup-classification defect.** `reconcile_hosted_liveness` places both OFFLINE transition and no-session control cleanup behind `info.controllable == Some(true)`. The exemption is necessary for **presence**—a harness-hosted relay legitimately has no broker session—but it is too broad for **broker-owned control metadata**. Thus a sessionless `controllable=false` record is never cleared. The same cleanup also omits `viewer_count`.\n4. **There is a real stale-snapshot race between session reap and `KIND_SESSIONS` convergence.** `dispatch_sessions` snapshots `has_controller`/viewer truth while holding the sessions map, drops the lock, and performs `info.json` convergence later. The exit waiter removes the session under the map lock, drops it, and then performs `stamp_reaped`. A poll that captured `controlled=true` before removal can write that stale value after `stamp_reaped` wrote false. This can recreate `+CONTROLLED` after a correct `/exit` or reap. This is residual lifecycle/convergence work, not the original child-reap omission.\n\n## Exact causal chains\n\n### Abrupt terminal/PTY close\n\n`rc::establish_attach` opens an attach stream → target dispatcher runs `serve_attach` → `serve_attach` subscribes as controller → broker `OutputLog::become_controller` records the controller and stamps `driven_by`/`controlled` → operator terminal closes and the rc process/Brain connection drops → broker `handle_conn` removes only subscriptions owned by that Brain connection, but deliberately does **not** finish/retire streams opened by it → target stream remains unfinished in `NetHost` → target `serve_attach` never receives `BrokerEvent::NetStreamEof` → its EOF branch does not send a finished response, does not feed `RestEvent::Detach`, and does not call `broker.detach_session` → controller slot persists → subsequent `KIND_SESSIONS` polls keep converging `controlled=true`.\n\nThis is why waiting for ordinary controller-writer cleanup is unreliable: the target serving Brain and its broker controller writer are still alive. The dead process was the stream opener on the operator side, not the serving subscriber that owns the controller slot.\n\n### Genuine `/exit`\n\n`/exit` reaches the harness PTY as input → harness child exits → broker exit waiter removes the broker-owned session → `OutputLog::stamp_reaped` clears all three stamps. This is exactly what `control_stamp_lifetime.rs::reap_clears_control_and_viewer_stamps` exercises. If the child truly exited and the stamp later reads true, the source-visible relatch ordering is:\n\n1. sessions poll snapshots the still-present controller;\n2. exit waiter removes the session;\n3. exit waiter clears stamps with `stamp_reaped`;\n4. old poll snapshot converges `controlled=true` afterward.\n\nThe inverse ordering is safe; hence the symptom can be intermittent.\n\n### Full daemon stop/start\n\nA real broker restart destroys the old in-memory session/controller/stream tables; the raw-close stale controller cannot survive **in memory** across a full broker process replacement. It can survive on disk and in gossip. The boot/tick no-session sweep is expected to clear it, but currently does so only for `controllable=Some(true)`. For the observed `emphasys` shape (`controllable=false`), the sweep intentionally skips the whole block, leaving `controlled` and `driven_by` untouched and leaving status ONLINE. The intent documented at `livehost.rs:657-663` is to avoid falsely offlining harness-hosted relay endpoints; it does not justify retaining broker control metadata when the restarted broker reports no session.\n\nA brain-only restart is different: broker sessions and streams intentionally survive it, so an unfinished raw-close stream/controller can survive and be redispatched. That is expected under the current ownership model and reinforces the need for an explicit viewport lease rather than relying on daemon refresh.\n\n## Relation to prior issues / hazard boundaries\n\n- **KH 7.27 / `REQ-HAZARD-CONTROL-STAMP-LIFETIME`: partially related.** It fixed the original `/exit` child-reap omission by adding `stamp_reaped`. It covers session death and clears all three fields. It does **not** cover an operator transport disappearing while the broker-owned child/session survives, and its integration test does not overlap a sessions poll with reap.\n- **KH 7.29 / `REQ-HAZARD-CONTROL-STAMP-CONVERGENCE`: opposite-polarity problem, not the raw-close cause.** It repairs a false negative caused by spawn stamping before adapter bind (`controlled=false` on disk while a controller is live). Its poll-based convergence is useful, but its off-lock snapshot creates the residual relatch race. It does not define transport ownership.\n- **B3 no-session boot sweep: directly related to restart persistence, but coverage is too narrow.** Existing tests seed only `controllable=Some(true)` and assert only `controlled`/`driven_by`; production omits `viewer_count` and deliberately leaves `Some(false)`/`None` untouched.\n- **Rest-state/create defects: independent.** Rest events do not own the broker controller slot or the three control stamps. The create/late-bind defect was a missing positive stamp, not a stale positive. Rest-state bugs can independently explain a stale ACTIVE/ONLINE presentation, but cannot explain why a live broker controller slot remains after raw opener death. For the observed `controllable=false` restart case, the stale ONLINE value is specifically outside broker-session liveness reconciliation; deciding it is dead requires the relay/host lifecycle authority, not blindly applying the spt-hosted session predicate.\n\n## Minimal source fixes\n\n1. **Make interactive attach viewport streams connection-bound.** Add an explicit stream lifetime/lease classification to `NetStreamOpenReq`/`NetHost` (for example `ConnectionBound` versus current restart-durable behavior). `rc` attach opens a connection-bound stream. `Broker::handle_conn` records such streams opened by that Brain connection and, on connection EOF/error, finishes their send side. That produces `NetStreamEof` on the peer/serve side, allowing the existing `serve_attach` branch to call `detach_session`. Do not globally retire every stream opened by a dropped Brain: durable stream survival is load-bearing for broker/brain restart and ADR-0038/D4 behavior.\n2. **Separate control cleanup from presence classification in `reconcile_hosted_liveness`.** For every endpoint absent from broker session truth, clear `controlled=false`, `driven_by=None`, and `viewer_count=0` regardless of `controllable`. Keep `controllable == Some(true)` only around `mark_offline`. This preserves relay/legacy presence semantics while healing fields that are broker-owned and cannot be live without a broker session.\n3. **Close the poll/reap relatch race without holding global session/log locks across filesystem I/O.** Give convergence a session identity/generation and revalidate after its disk write. If the snapshotted session is no longer the current session, immediately converge from current truth (zero when absent, or the replacement session’s truth). A dedicated per-endpoint stamp serializer is also valid. Do not move `info.json` I/O under the sessions or `OutputLog` mutex; KH 7.12 explicitly warns against that lock coupling.\n4. **Clear `viewer_count` in every no-session cleanup path.** It is part of the same invariant and is currently missing from the boot sweep.\n\n## Risks and invariants\n\n- A blanket “close all streams on Brain disconnect” would regress brain restart and durable replay. The new lifetime must be explicit and limited to operator viewports.\n- A local socket/process close is observable promptly; a remote half-open transport still needs the existing keepalive/dead-connection detector to make the owner EOF observable.\n- Clearing control fields for sessionless `controllable=false`/legacy records is safe under the current ownership contract because source search shows these fields are written by broker control/viewer lifecycle paths. Presence must remain untouched for those classes.\n- Stream FIN and detach are idempotent. Late close from an old controller must retain `detach_if` identity checking so it cannot unseat a newer controller.\n- Race repair must distinguish a removed old session from a newly created replacement session for the same endpoint; endpoint name alone is insufficient.\n\n## Focused tests\n\n1. **Abrupt controller opener death, child survives:** real broker, real hosted session/perch, real attach stream; wait for `controlled=true`; drop/kill the operator Brain/process without `finished=true`, without `detach_session`, and without killing the child. Assert within a bound that the target receives EOF, broker session remains present, `controller_by=None`, `controlled=false`, `driven_by=None`, and status remains ONLINE. This fails on current code because the stream row is retained.\n2. **Abrupt viewer opener death:** same setup with View intent; kill opener without FIN; assert the child/session remains and `viewer_count` returns to zero. This protects all three stamp dimensions.\n3. **Explicit detach control arm:** send current FIN/detach normally and assert the same metadata transition, proving connection-bound cleanup did not alter graceful semantics.\n4. **Replacement-controller race arm:** abruptly close controller A, let controller B take over before A’s close is processed, and assert A’s late cleanup cannot clear B’s controller slot/stamp.\n5. **Boot no-session matrix:** seed sticky ONLINE and OFFLINE perches with `controllable=Some(true)`, `Some(false)`, and `None`, each with `controlled=true`, `driven_by=Some(node)`, `viewer_count>0`; derive an empty/decoy live-session set from a real broker and run the real reconcile. Assert all classes clear the three control fields; only `Some(true)` is marked OFFLINE, while false/None presence is unchanged.\n6. **Deterministic poll/reap ordering test:** add a test barrier after `dispatch_sessions` captures controller truth but before convergence writes. Reap the child and let `stamp_reaped` finish, then release the old poll. Assert final disk state is false/None/zero. Include a replacement-session arm to ensure generation revalidation converges to the new session rather than clearing it.\n7. **Full broker restart test:** establish a controlled endpoint, stop the broker ungracefully so disk remains sticky, start an empty broker, execute boot reconciliation, and assert the matrix above. For the `controllable=false` arm, assert status is deliberately unchanged but control metadata heals.\n8. **Existing 7.27 regression retained:** child exit with controller and viewer still attached must continue to clear all three fields. This proves the new transport cleanup does not substitute for session-reap cleanup.",
  "files": [
    {
      "path": "crates/spt-daemon/src/broker.rs",
      "description": "Control truth owner. `OutputLog::become_controller`, `add_viewer`, `detach_if`, `clear_controller`, and `stamp_reaped` write/clear `controlled`, `driven_by`, and `viewer_count`. `handle_conn` cleans connection-owned session subscriptions but leaves opened network streams durable. `dispatch_sessions` snapshots session control state under the sessions lock and converges stamps after dropping it, enabling a stale snapshot to relatch after reap."
    },
    {
      "path": "crates/spt-daemon/src/attach.rs",
      "description": "Attach serve lifecycle. The controller/viewer role is acquired via broker subscribe. Only the `NetStreamEof` branch performs prompt controller detach (`feed_rest(Detach)` plus `broker.detach_session`); session `Exit` relies on broker child reap. If opener death produces no stream EOF, the serve worker and role remain."
    },
    {
      "path": "crates/spt-daemon/src/nethost.rs",
      "description": "Durable stream table. `StreamLog` tracks `finished`, ring data, and subscriber independently of the Brain connection that opened it. Streams are retired/finished only by explicit operations; opener connection death is not represented as a viewport lease expiry."
    },
    {
      "path": "crates/spt/src/rc.rs",
      "description": "Operator attach pump. Graceful detach sends stream FIN; raw stdin/process/window termination has no durable broker-side ownership cleanup. Dropping the rc Brain therefore does not currently imply finishing the opened attach stream."
    },
    {
      "path": "crates/spt-daemon/src/livehost.rs",
      "description": "Restart/no-session reconciliation. Lines 657-663 document why false/None controllability classes must not be offlined. Lines 700-706 incorrectly put control cleanup behind `controllable==Some(true)` and clear only `controlled`/`driven_by`, not `viewer_count`. This exactly explains why a sessionless observed `controllable=false` emphasys record can retain stale control metadata after full daemon restart."
    },
    {
      "path": "crates/spt-daemon/src/harnesshost.rs",
      "description": "Brokered harness launch. Spawn is performed through a temporary Brain connection; its drop normally clears the placeholder spawn controller. This does not provide cleanup for a later operator attach stream whose opener dies."
    },
    {
      "path": "crates/spt-daemon/tests/control_stamp_lifetime.rs",
      "description": "Current coverage boundary. `reap_clears_control_and_viewer_stamps` proves child-reap cleanup (7.27). `converge_stamps_on_sessions_poll_after_late_bind` proves the 7.29 false-negative repair but does not overlap poll with reap. The B3 boot-sweep test covers only `controllable=Some(true)` and omits viewer_count assertions."
    },
    {
      "path": "docs/KNOWN-HAZARDS.md",
      "description": "Hazard contracts: 7.27 is session-reap stamp lifetime; 7.29 is late-bind/poll convergence; 7.12 prohibits filesystem stamp I/O under broker session/log locks; ADR-0038/D4 durability constrains any connection-close stream fix."
    },
    {
      "path": "crates/spt/src/picker/model.rs",
      "description": "Presentation consequence. `display_status` renders Controlled when `driven_by.is_some() || controlled`; therefore either stale field is sufficient for ONLINE + CONTROLLED, independently of `controllable` and viewer count."
    }
  ],
  "architecture": "Control state has three layers that must converge but currently have mismatched lifetimes: (1) the operator viewport transport/stream, (2) the broker-owned session role (`controller`/`viewers`), and (3) persisted/gossiped `info.json` stamps. Graceful FIN joins layer 1 to layer 2 through `serve_attach::NetStreamEof -> detach_session`; child reap joins session lifetime to persistence through `stamp_reaped`; periodic `KIND_SESSIONS` polling repairs persistence from broker truth; boot reconciliation repairs sessionless disk state. Raw opener death currently breaks the first join because streams are restart-durable rather than connection-bound. The restart sweep breaks the final join because control cleanup is incorrectly coupled to the `controllable=true` presence classifier. The poll/reap code additionally lacks an epoch/order guarantee, so an old layer-2 snapshot can overwrite newer layer-3 truth. The maintainable design is: explicit connection-bound leases for attach viewports, broker session identity/generation for convergence, all three control fields cleared whenever broker truth has no session, and presence/offline policy kept separate by endpoint hosting class."
}