{
  "summary": "## Findings\n\nThe current checkout has three separate runtime paths that merely meet inside the broker process:\n\n1. **`fetch --apply` restart/promotion:** `cmd_update_fetch` stages the signed set and calls `cmd_update_apply(false)`. With a live daemon, `apply_staged` swaps the executable, writes `releases/applied-state.json` as `AppliedPending`, and sends `KIND_BRAIN_RESTART`. The broker raises `BrainRestart`; `supervise_brain` kills and waits for the old child, increments the generation, and spawns the candidate with `StartReason::Update`. The candidate connects, calls `resume_sessions`, writes `brain.ready`, and can be promoted before its dispatcher/peer pump start on the first 500 ms heartbeat.\n2. **Peer pump IPC:** the peer pump owns a distinct `Brain::cold_start_pump` IPC connection. That connection is split into one blocking `pump-ipc-reader` thread plus an `mpsc` channel; pump calls use a 30 s `recv_timeout`. It does not share the main brain's IPC stream, reader channel, or `SharedSend` with PTY controller writers.\n3. **PTY controller writers:** every controlled broker session owns a depth-4096 `sync_channel` and a dedicated `controller_writer`, but every writer attached through the same brain connection shares that connection's single `Arc<Mutex<SendHalf>>`. `controller_writer` sets `write_blocked_since=Some(now)` **before acquiring that shared send mutex**, not after. Therefore the measured 15 s “blocked write” interval includes both an actual named-pipe `write_frame` and time queued behind another writer holding the send mutex. One blocked writer can create a same-connection convoy in which sibling session writers also age into `BRAIN_SUBSCRIBER_STALL_EVICT`.\n\n### Promotion discriminator limitations in the current source\n\n`ProductionTrialEnv::old_gen_drained()` is `!Broker::any_local_controller_wedged()`. The latter reports a controller only when `(by == None) && write_blocked_since.age >= 15 s`. Consequently:\n\n- a write blocked for less than 15 s reads as “drained” and does not delay immediate promotion;\n- `by == None` means any local controller, not a controller tagged with an old brain generation;\n- remote controllers (`by == Some(node)`) never gate promotion;\n- there is no connection id, brain pid, or generation stored in `ControllerSink`, so a `STALL_EVICT` line cannot be directly attributed to old versus new brain from the line alone.\n\n### Source-to-field discrepancy that must be respected\n\nThe factual v0.30.5 incident log says `BRAIN_RESUMED: re-established ... cursor(s)` and describes a cursor-only deployed path. This checkout has no `resume_session_cursors` symbol. Its current `Brain::resume_sessions` inserts the cursor and then calls private `subscribe`, which sends `AttachIntent::Control, by=None` for every broker session; a remote incumbent makes that local resume a Viewer, while an empty/local slot becomes Controller. Before interpreting a new field run, capture `spt --json daemon status` (`broker_image`, `broker_stale`) and the exact `BRAIN_RESUMED` wording to establish which runtime is actually deployed.\n\n### Peer deadlines and failure classification\n\n- Broker QUIC dial/open/send work runs on `NetHost`'s dedicated two-worker Tokio runtime and is wrapped by `NetHost::bounded_block_on` at **10 s**. Expiry is deliberately returned as `io::ErrorKind::Other`, producing an ordinary broker error reply.\n- The pump's own IPC response budget is **30 s** (`PUMP_PEER_IO_TIMEOUT`). `Brain::read_frame_until` turns channel expiry into `TimedOut(\"brain IPC read deadline elapsed\")`.\n- `run_peer_pump` processes peers and due workers sequentially. A normal broker error logs `PUMP_PEER_FAIL:<peer>:...`, drops that peer's cached connection, and continues. Any `TimedOut` bubbles through `peer_outcome`, aborts the entire pump body, logs `PEER_PUMP_FAIL`, and starts the supervised backoff/restart.\n- `request_update` and `request_sync` both use the same 30 s no-progress deadline and re-arm it only when data arrives on their own stream. In this checkout they propagate the raw `TimedOut`; the exact text `peer reply-read: no progress within budget` from the symptom report does not exist under `crates/`, another deployed-source/version discriminator.\n- Every pump restart creates a fresh split connection and logs `PUMP_IPC_READER: spawned`. The old reader handle is dropped without joining; a reader still blocked in `read_frame` exits only when its old connection errors. Spawned-minus-exited counts therefore diagnose outstanding old pump readers, not main-brain PTY subscribers.\n\n### Shared-resource inventory\n\n- **Shared with PTY controller writes:** per-connection `SharedSend = Arc<Mutex<SendHalf>>`; all controller/viewer writers and synchronous broker replies on that same IPC connection serialize through it. A main brain that resumes multiple sessions puts those session writers on one `SharedSend`.\n- **Not shared across sessions:** each `OutputLog` mutex, depth-4096 controller channel, `write_blocked_since` mutex, writer thread, and PTY drain are per session.\n- **Promotion read:** `any_local_controller_wedged` takes the global `sessions` mutex and then each session's `OutputLog` mutex, but performs no I/O. It does not evict.\n- **Eviction triggers:** the 15 s `STALL_EVICT` check runs on `resolve_subscribe` and on `KIND_SESSIONS`/`reap_dead_controller`; it is not a free-running timer. Output load has a separate 5 s full-controller-channel eviction in `OutputLog::append`.\n- **Peer pump separation:** pump and dispatcher each create independent broker IPC connections/handler threads. They share broker process scheduling, the broker-owned `NetHost`, and the two-worker network runtime, but not the PTY controller `SharedSend`, controller channels, or output-log locks. `EffectJournal` is broker-global for journaled net/input effects, but `apply_once` releases its mutex around the effect; PTY output subscriber writes do not traverse it.\n- **Network subscriber caveat:** `StreamLog::append/attach/finish` writes inline through the pump connection's own `SharedSend` while holding that stream log mutex, on a network-runtime task. That can stall the pump's own carrier/runtime worker, but it still does not share the main brain/PTTY controller connection mutex.\n\n## Prioritized live probes for doyle\n\nSend these one at a time.\n\n**1. Timestamp the complete apply window.** In one PowerShell, then run `spt update fetch --apply` in another:\n```powershell\n$h = if ($env:SPT_HOME) { $env:SPT_HOME } else { Join-Path $env:LOCALAPPDATA 'spt-core' }\n$l = Join-Path $h 'logs\\daemon.stderr.log'\nGet-Content $l -Tail 0 -Wait | ForEach-Object { '{0:o} {1}' -f [DateTime]::UtcNow, $_ } | Tee-Object \"$env:TEMP\\spt-update-timeline.log\"\n```\nDecision evidence: `STALL_EVICT` within a `BRAIN_UPDATE_RESTART → BRAIN_UP(new pid/gen) → BRAIN_RESUMED → BRAIN_PROMOTED` window is update-correlated. Subtract roughly 15 s from the captured eviction time to bound when its `write_blocked_since` began. An eviction with no restart marker and unchanged generation/pid is steady-state. Record `BRAIN_RESUMED` wording because current checkout and v0.30.5 differ.\n\n**2. At the freeze, sample independent liveness three times, six seconds apart.**\n```powershell\n$h = if ($env:SPT_HOME) { $env:SPT_HOME } else { Join-Path $env:LOCALAPPDATA 'spt-core' }\n1..3 | ForEach-Object {\n  $s = spt --json daemon status | ConvertFrom-Json\n  [pscustomobject]@{\n    utc=[DateTime]::UtcNow.ToString('o'); broker=$s.broker_image; stale=$s.broker_stale\n    pump=$s.pump_heartbeat_ms; evicts=$s.stall_evict_count; last_evict=$s.stall_evict_last_ms\n    ready_mtime=(Get-Item (Join-Path $h 'brain.ready')).LastWriteTimeUtc.ToString('o')\n    ready=(Get-Content (Join-Path $h 'brain.ready') -Raw)\n    applied=(Get-Content (Join-Path $h 'releases\\applied-state.json') -Raw)\n  } | ConvertTo-Json -Compress\n  Start-Sleep 6\n}\n```\nDecision evidence: advancing `brain.ready` mtime means the main brain is completing its 500 ms `net_status` loop; advancing pump heartbeat means the separate peer pump loop is alive. Fresh pump + frozen PTYs isolates the symptom away from a whole-pump stall. Stale pump plus fresh main-brain ready isolates a pump-only failure. Both stale, combined with the timeline, identifies a broader brain/IPC or process stall. The status connection is separate and queries the stall tally without running the `KIND_SESSIONS` reap.\n\n**3. Arm the built-in pump arbiter for the next controlled reproduction.** Start the daemon from the same non-service PowerShell so it inherits the variable:\n```powershell\n$env:SPT_PUMP_DIAG = Join-Path $env:TEMP 'spt-pump-diag.log'\nspt daemon stop --force\nspt daemon start\n```\nThen reproduce once and return the diag file plus daemon stderr. Interpret in order: `LEG_ENTER i=0/1/2/3` means registry/notif/sync/update; sync also emits `SYNC_REQ_SENT`; `NET_DIAL_DISPATCH → DIAL_ENTER → DIAL_EXIT ~10000ms Err` proves the broker's 10 s bound replied normally; a `LEG_ENTER` without `LEG_EXIT` until ~30 s followed by `PEER_PUMP_FAIL: brain IPC read deadline elapsed` pins the pump's reply-read/carrier timeout. Compare those epoch-ms stamps with Probe 1. This probes the pump's separate connection and must not be treated by itself as proof that it blocked a PTY connection.\n\n**4. Capture the process tree immediately before apply and again while frozen.**\n```powershell\nGet-CimInstance Win32_Process |\n  Where-Object { $_.Name -match '^(spt|claude|claude-spt)(\\.exe)?$' } |\n  Select-Object ProcessId,ParentProcessId,Name,CreationDate,CommandLine |\n  Sort-Object ParentProcessId,ProcessId | Format-Table -AutoSize -Wrap\n```\nDecision evidence: one old `daemon brain` disappearing before one new generation appears matches the intended update cycle; overlapping brain children is update-specific process evidence; a stable single brain across a spontaneous eviction is steady-state. The same snapshot confirms or rejects the observed stale psyche/interactive duplicate-process condition without relying on endpoint labels alone.\n\nNo fix is proposed. Investigation was read-only; formatters, linters, builds, and tests were intentionally skipped per assignment.",
  "files": [
    {
      "path": "spt-core/docs/UPDATE-WEDGE-SYMPTOMS-FOR-RCA.md",
      "description": "Factual field evidence: invariant STALL_EVICT signature, update and steady-state incidents, v0.30.5 generation window, pump failures, process observations, and recovery behavior."
    },
    {
      "path": "spt-core/crates/spt/src/cli.rs",
      "description": "`cmd_update_fetch` (5298) stages and optionally calls `cmd_update_apply`; `cmd_update_apply` (4665) selects live-daemon `apply_staged`; `cmd_daemon_status` and `DaemonStatusJson` expose broker image, pump heartbeat, and stall-evict tally."
    },
    {
      "path": "spt-core/crates/spt-daemon/src/applyhost.rs",
      "description": "`swap_and_record` performs executable aside/land and writes `AppliedPending`; `apply_staged` connects before swap and calls `Brain::request_brain_restart` after swap."
    },
    {
      "path": "spt-core/crates/spt-daemon/src/daemon.rs",
      "description": "`Daemon::run` wires `spawn_brain_supervisor` to the live `Broker` via `Broker::set_brain_restart`, giving promotion an in-process broker truth handle."
    },
    {
      "path": "spt-core/crates/spt-daemon/src/brainproc.rs",
      "description": "`BrainRestart`, `run_brain`, `spawn_net_consumers`, `ProductionTrialEnv::old_gen_drained`, `run_trial`, `supervise_brain`, `spawn_brain_child`, and `spawn_brain_supervisor` implement kill/wait, generation/reason, resume, readiness, promotion/rollback, and post-ready consumer startup."
    },
    {
      "path": "spt-core/crates/spt-daemon/src/brain.rs",
      "description": "`BrainConn::{Whole,Split}`, `split_with_reader`, `cold_start_pump`, `read_frame_until`, `call_deadline`, `resume_sessions`, `subscribe`, net RPCs, and `request_brain_restart` define IPC connection isolation, 30 s pump reads, current active session resume, and restart request/ack."
    },
    {
      "path": "spt-core/crates/spt-daemon/src/broker.rs",
      "description": "`SharedSend`, `ControllerSink`, `OutputLog`, `become_controller`, `controller_writer`, `controller_write_stalled`, `stall_evict_controller`, `reap_dead_controller`, `any_local_controller_wedged`, `handle_conn`, `dispatch_brain_restart`, and `dispatch_net_dial` define per-connection writer coupling, eviction/promotion predicates, per-connection broker threads, and restart/dial dispatch."
    },
    {
      "path": "spt-core/crates/spt-daemon/src/msg.rs",
      "description": "Wire kinds and `SessionInfo`/`SessionsReply`; `controller_by=None` is explicitly ambiguous between no controller and a local controller, while `resume_seq` is broker cursor-of-record."
    },
    {
      "path": "spt-core/crates/spt-daemon/src/pump/mod.rs",
      "description": "`PUMP_PEER_IO_TIMEOUT`, `run_peer_pump`, `ensure_conn`, `peer_outcome`, `supervise_pump`, and `spawn_peer_pump` implement the single-threaded/sequential pump, distinct split connection, 30 s poison classification, and restart loop."
    },
    {
      "path": "spt-core/crates/spt-daemon/src/propagate.rs",
      "description": "`request_update` opens/subscribes/sends on a peer stream, then waits under the pump's 30 s no-progress deadline, re-arming only on its own stream data."
    },
    {
      "path": "spt-core/crates/spt-daemon/src/sync.rs",
      "description": "`request_sync` has the parallel 30 s reply-read path and emits `SYNC_REQ_SENT` when pump diagnostics are armed."
    },
    {
      "path": "spt-core/crates/spt-daemon/src/nethost.rs",
      "description": "`NetHost` owns a two-thread Tokio runtime; `bounded_block_on` enforces the 10 s ordinary-error deadline; `dial` emits diagnostic stages; `StreamLog` writes through its subscriber connection's `SharedSend` inline."
    },
    {
      "path": "spt-core/crates/spt-daemon/src/pumpdiag.rs",
      "description": "Opt-in `SPT_PUMP_DIAG` epoch-ms diagnostic sink and the documented dispatch/dial interpretation matrix."
    },
    {
      "path": "spt-core/crates/spt-daemon/src/endpoint.rs",
      "description": "Canonical `brain.ready` and `identity/pump-heartbeat.json` paths used by the readiness and pump-liveness probes."
    }
  ],
  "architecture": "```mermaid\nflowchart TD\n  F[cmd_update_fetch --apply] --> A[cmd_update_apply]\n  A --> AS[apply_staged]\n  AS --> SW[swap_and_record: AppliedPending]\n  SW --> RR[request_brain_restart]\n  RR --> HC[Broker::handle_conn, apply CLI connection]\n  HC --> DBR[dispatch_brain_restart]\n  DBR --> SIG[BrainRestart AtomicBool]\n  SIG --> SB[supervise_brain]\n  SB --> K[old child kill + wait]\n  K --> C[new child, generation+1, StartReason::Update]\n  C --> RB[run_brain: Whole main IPC conn]\n  RB --> RS[resume_sessions: sessions query + current Control/by=None subscribes]\n  RS --> WR[write_ready]\n  WR --> RT[run_trial: ready_seen + old_gen_drained]\n  RT --> P[BRAIN_PROMOTED + Applied]\n  WR --> HB[500ms heartbeat]\n  HB --> NC[spawn dispatcher + peer pump once]\n\n  NC --> D[dispatcher: separate Whole IPC conn]\n  NC --> PP[peer pump: separate Split IPC conn]\n  PP --> PR[pump-ipc-reader thread]\n  PR --> CH[mpsc frames channel]\n  PP --> SEQ[sequential peers/workers]\n  SEQ --> ND[net RPC, 30s IPC budget]\n  ND --> BH[broker per-connection handler]\n  BH --> NH[NetHost 2-thread Tokio runtime, 10s QUIC bound]\n\n  RS --> C1[session controller_writer 1]\n  RS --> C2[session controller_writer 2..N]\n  C1 --> SS[one SharedSend mutex for main brain connection]\n  C2 --> SS\n  BR[broker replies on same connection] --> SS\n  SS --> PIPE[named-pipe SendHalf]\n  C1 -->|mark blocked before mutex| B1[write_blocked_since 1]\n  C2 -->|mark blocked before mutex| B2[write_blocked_since 2..N]\n  B1 --> EV[15s stall predicate]\n  B2 --> EV\n  EV -->|subscribe or sessions reap| LOG[BRAIN_SUBSCRIBER_STALL_EVICT]\n  EV -->|by None only| G[any_local_controller_wedged promotion read]\n```\n\nThe important boundary is the pair of IPC connections: peer-pump failures can consume the network runtime and broker process scheduling, but they cannot directly occupy the main brain connection's `SharedSend`. The direct multi-session coupling is instead inside the main connection itself, where controller writers and replies share one blocking send mutex. Live evidence should therefore classify generation transition, main-brain readiness, pump heartbeat/diag, and process overlap independently rather than infer causation from nearby log lines."
}