{
  "summary": "Causal result: the code permits `brain stops draining broker IPC -> controller_writer blocks -> BRAIN_SUBSCRIBER_STALL_EVICT`, including multi-session cascade when those sessions share one broker connection. It does not permit that blocked controller writer to directly stop PTY output consumption or cause `INJECT_SETTLE_TIMEOUT` / `INJECT_ECHO_MISS`: settle and echo verification observe `OutputLog` upstream of controller delivery, and `OutputLog::append` hands controller frames off with one nonblocking `try_send`. The reverse direction is possible: PTY output, including output induced by injection, is appended first and can then expose or increase downstream controller backpressure. A hung PTY input consumer can block the dedicated `InputWriter` and cause settle failure; a stopped PTY output drain can also cause settle failure by preventing `OutputLog.high_water` from advancing. Neither condition inherently creates controller backpressure; a dead drain actually removes new controller traffic. Translation-child death causes send/disconnect/commit-fault handling, not settle failure, although the worker performs settle before checking `TranslationChild::send`, so a translation death may chronologically precede a settle timeout without causing it. Psyche turns run in the restartable brain process, on per-endpoint OS threads with separate child processes and pipe-drainer threads. They share neither broker-resident PTY/translation/output locks nor an executor with controller writers. `PSYCHE_TURN_FAIL` and controller stalls can therefore be co-effects of whole-brain suspension or severe process/resource starvation [INFERENCE], but a timed-out psyche turn has no direct code path that blocks broker PTY drains or controller IPC consumption.",
  "files": [
    {
      "path": "spt-core/crates/spt-daemon/src/inject.rs",
      "description": "`try_spt_hosted_inject` renders the whole `<EVENT>`, opens a fresh `Brain::cold_start` broker connection, and calls `Brain::inject_endpoint`; `drain_idle_spool` re-offers spooled rows through this path. The fresh connection means a controller writer blocked on another connection's `SharedSend` does not directly block this injection connection."
    },
    {
      "path": "spt-core/crates/spt-daemon/src/brain.rs",
      "description": "`Brain::inject_endpoint` sends `KIND_ENDPOINT_INPUT` and waits for `KIND_ENDPOINT_INJECTED`. `read_event` consumes interleaved output. `resume_sessions` subscribes all hosted sessions on one `Brain` connection, making their controller writers share that connection's `SharedSend`. `net_status` drains interleaved events while waiting for its reply."
    },
    {
      "path": "spt-core/crates/spt-daemon/src/broker.rs",
      "description": "Primary causal implementation. `Broker::dispatch_endpoint_input` resolves endpoint/translation, queues the envelope on `Translation.event_tx`, logs `ENDPOINT_INJECT`, then immediately replies `delivered=true`; this is enqueue acceptance, not proof of settle, PTY write, echo, or commit. `build_translation`, `run_inject_worker`, `settle_before_inject`, `drive_one_sequence`, and `echo_verify_after` implement translation-to-PTY injection and ring-based observation. `InputWriter::enqueue` is nonblocking; `input_writer` is the sole blocking `PtySession::write_input` caller. `dispatch_spawn` wires PTY drain to `OutputLog::append`. `OutputLog::append` records ring/high-water before one nonblocking controller `try_send`. `controller_writer` alone performs blocking IPC writes. `controller_write_stalled` / `stall_evict_controller` emit `BRAIN_SUBSCRIBER_STALL_EVICT`. `Broker::handle_conn` creates one `SharedSend` per connection and passes it to every session subscribed on that connection."
    },
    {
      "path": "spt-core/crates/spt-daemon/src/translation.rs",
      "description": "`TranslationChild::spawn` starts an independent stdin/stdout JSON-lines filter and a stdout-reader thread. `TranslationChild::send` writes `ToBinary::{Init,Event,Input}` to its stdin. `KeyCmd::{Key,Text,Delay,Commit}` flows back to the broker worker. The binary never reads or writes the PTY itself. Closed stdout disconnects the command channel; failed stdin or disconnected command flow is handled as a translation fault and bounded reap."
    },
    {
      "path": "spt-core/crates/spt-term/src/pty.rs",
      "description": "`PtySession` owns the PTY master. `SessionSurface::write_input` holds the shared PTY writer mutex across blocking `write_all` and `flush`. The same writer mutex is used by the DSR auto-answer path. A child that does not consume PTY input can therefore park only the broker's dedicated `InputWriter` thread, not `controller_writer`."
    },
    {
      "path": "spt-core/crates/spt-term/src/reader.rs",
      "description": "`Drain::spawn` is the sole PTY output reader. For each chunk it scans DSR, may lock the PTY writer to emit CPR, then calls the broker sink. In production that sink is `OutputLog::append`. If the input writer is blocked while holding the shared PTY writer and a DSR-bearing output chunk arrives, this drain can wait for that writer mutex before appending the chunk, an input-side route to absent high-water progress. The drain has no controller socket dependency."
    },
    {
      "path": "spt-core/crates/spt-daemon/src/brainproc.rs",
      "description": "`run_brain` proves the process boundary: the broker is already running, the restartable brain opens IPC, resumes session subscriptions, starts `spawn_live_host`, and uses a 500 ms main-loop `net_status` heartbeat. PTY ownership and controller writer threads remain in the broker process. Psyche hosting is explicitly described as orthogonal to PTY delivery."
    },
    {
      "path": "spt-core/crates/spt-daemon/src/livehost.rs",
      "description": "`host_one` starts one OS thread per live endpoint and runs `BrainLifecycle::run_pulse_loop` there. `spawn_live_host` itself runs on another brain-process thread. No resident psyche process is shared with the PTY session."
    },
    {
      "path": "spt-core/crates/spt-daemon/src/lifecycle.rs",
      "description": "`pulse_tick` invokes `run_psyche_event_turn` only when the echo gate fires. `run_pulse_loop` is single-threaded per endpoint and calls `note_turn_outcome`, which emits `PSYCHE_TURN_FAIL`. `run_psyche_event_turn` takes a cloned `ManifestRuntime` snapshot before the bounded spawn; `ManifestCell` read locks are dropped before process execution. No broker `OutputLog`, `InjectFloor`, `InputWriter`, `SharedSend`, or PTY writer lock is reachable here."
    },
    {
      "path": "spt-core/crates/spt-daemon/src/psyrelay.rs",
      "description": "`psyche_turn_and_relay` calls `spt_live::run_psyche_turn`, parses captured stdout, and relays intents. It has no PTY or broker-controller dependency."
    },
    {
      "path": "spt-core/crates/spt-live/src/turn.rs",
      "description": "`run_psyche_turn` invokes the `psyche_resume` role through `AgentRuntime::run_bounded_stdin`; timeout/error becomes `TurnError`. Psyche stdout is captured rather than attached to the endpoint PTY."
    },
    {
      "path": "spt-core/crates/spt-runtime/src/runtime.rs",
      "description": "`ManifestRuntime::run_bounded_stdin` spawns a separate child with piped stdin/stdout/stderr and delegates to `wait_bounded`. `wait_bounded` starts independent stdout/stderr reader threads, polls the child every 20 ms, and kills/reaps on timeout. There is no async executor and no lock shared with broker PTY/controller threads."
    },
    {
      "path": "spt-core/crates/spt-daemon/tests/brain_decouple.rs",
      "description": "Existing faithful diagnostic rig for the subscriber direction. `suspended_brain_controller_is_stall_evicted_take_completes_viewer_ticks` stops reading a real broker socket, fills the controller path until `controller_writer` blocks, and checks stall eviction and continued viewer progress. It documents `SPT_BRAIN_WRITE_DEADLINE_MS` as the diagnostic deadline gate."
    },
    {
      "path": "spt-core/crates/spt-daemon/tests/inject_control_wedge.rs",
      "description": "Existing injection evidence. `w5a_bigmultiline_inject_arrives_byte_complete_across_a_loop` exercises settle probe, chunking, PTY receipt, and controller draining; `w5a_forced_echo_miss_redrives_once_then_loud_spools` uses `SPT_INJECT_FORCE_ECHO_MISS` and the translation stdin log to prove two event drives and respooling. Other cases use `SPT_INPUT_QUEUE_DEPTH`, `SPT_INJECT_SETTLE_MS`, `SPT_INJECT_ECHO_MS`, and `SPT_INJECT_COMMIT_DEADLINE_MS` as diagnostic gates."
    },
    {
      "path": "spt-core/docs/UPDATE-WEDGE-SYMPTOMS-FOR-RCA.md",
      "description": "Field evidence only. Its log ordering is consistent with the code paths but is not causal proof. In particular, `ENDPOINT_INJECT` is an early queue-accept breadcrumb, and `PSYCHE_TURN_FAIL` is produced in another process/thread domain."
    }
  ],
  "architecture": "## Exact data flow\n\n```mermaid\nflowchart LR\n    Sender[local/WAN sender] -->|fresh broker connection| BI[Brain::inject_endpoint]\n    BI -->|KIND_ENDPOINT_INPUT| DEI[Broker::dispatch_endpoint_input]\n    DEI -->|Translation.event_tx.send envelope| IW[run_inject_worker]\n    DEI -->|logs immediately| EI[ENDPOINT_INJECT]\n    DEI -->|KIND_ENDPOINT_INJECTED delivered=true| BI\n\n    IW --> SB[settle_before_inject]\n    SB -->|enqueue ESC[6n]| INQ[InputWriter bounded FIFO]\n    IW --> DOS[drive_one_sequence]\n    DOS -->|ToBinary::Event| TC[TranslationChild stdin]\n    TC --> TB[translation process]\n    TB -->|KeyCmd JSON lines| TR[translation stdout reader]\n    TR --> DOS\n    DOS -->|Key/Text bytes| INQ\n    INQ -->|dedicated input_writer thread| PW[PtySession::write_input]\n    PW --> PTY[PTY / harness child]\n\n    PTY -->|PTY master bytes| D[Drain::spawn thread]\n    D -->|sink chunk| OL[OutputLog::append]\n    OL -->|ring + next_seq/high_water| OBS[settle_before_inject / echo_verify_after]\n    OL -->|single nonblocking try_send| CQ[controller channel]\n    CQ --> CW[controller_writer]\n    CW -->|blocking write_frame under per-connection SharedSend| BC[brain/controller IPC socket]\n    BC --> BR[Brain::read_event consumer]\n\n    LH[livehost::host_one per-endpoint thread] --> PL[BrainLifecycle::run_pulse_loop]\n    PL --> PT[run_psyche_event_turn]\n    PT --> PR[psyche_turn_and_relay]\n    PR --> RT[ManifestRuntime::run_bounded_stdin]\n    RT --> PC[separate psyche child + stdout/stderr reader threads]\n    RT -->|timeout| PF[PSYCHE_TURN_FAIL via note_turn_outcome]\n```\n\n## Boundary semantics\n\n1. `ENDPOINT_INJECT` means only that `Translation.event_tx.send(envelope)` returned `Ok`. The broker sends `delivered=true` at that same point. The inject worker may not yet have run settle, written the translation stdin, received any `KeyCmd`, written the PTY, observed echo, or received `{commit}`. A later settle timeout, translation disconnect, commit miss, forced echo miss, or respool is compatible with an earlier `delivered=true`.\n2. `INJECT_SETTLE_TIMEOUT` is emitted when repeated DSR records have been offered to `InputWriter` for the configured window and `OutputLog.next_seq` has not advanced beyond the pre-probe baseline. `InputWriter::enqueue` return values are currently ignored by settle, so the line does not distinguish a dropped/full input queue from an accepted-but-not-written probe or a written-but-non-echoing PTY.\n3. `INJECT_ECHO_MISS` is downstream of a successful translation `{commit}` and only runs when `SPT_INJECT_VERIFY_ECHO` or the force-test gate enables it. It reads recent raw ring bytes, not frames delivered to the brain. With ordinary verification, an entirely empty ring window is treated as unverifiable and therefore successful; a miss requires nonempty output lacking the payload prefix. `SPT_INJECT_FORCE_ECHO_MISS` bypasses that verdict in diagnostics.\n4. `OutputLog::append` increments `next_seq`, stores the raw bytes in the ring, fans viewers, performs one nonblocking controller-channel `try_send`, advances the screen grid, and returns. It never waits for `controller_writer` or the brain socket. Echo/settle observation is therefore upstream of controller delivery.\n5. `controller_writer` marks `write_blocked_since` before acquiring the connection's `SharedSend` mutex and before `write_frame`. Consequently, if writer A holds `SharedSend` while blocked in an OS socket write, writers B..N for other sessions on the same broker connection block on the mutex and their stall timers also advance. This is a code-permitted one-connection-to-many-session `BRAIN_SUBSCRIBER_STALL_EVICT` cascade. It does not cross back into those sessions' PTY drains.\n6. The translation process is a command generator, not a PTY consumer. Its stdin receives `Init`, whole `Event`, and content-free `Input`; its stdout emits `KeyCmd`. The broker owns all PTY writes. Translation death can cause `SendFailed` or `Disconnected`, respool, fault, reap, and later bounded respawn. Because settle is executed before `drive_one_sequence` calls `TranslationChild::send`, a dead translation process is discovered only after settle, but does not itself suppress PTY DSR/output.\n7. The endpoint harness child is the PTY input consumer. If it stops consuming input, `PtySession::write_input` can block while holding the PTY writer mutex. That parks the dedicated `input_writer`, eventually fills/drops the bounded input FIFO, and can prevent settle probes from landing. If a PTY output chunk containing DSR arrives concurrently, `Drain::spawn` also waits for that writer mutex before calling the sink, so high-water can stop advancing. This is a direct input/PTY-side route to settle timeout.\n8. A dead/error-returned PTY `Drain` means no further `OutputLog::append`, so settle times out and echo visibility disappears. It cannot generate new controller backpressure because it generates no new controller frames. Conversely, a blocked controller cannot kill or block the drain through the controller queue because handoff is nonblocking.\n9. Psyche work lives in the brain child. PTY, translation management, `OutputLog`, and `controller_writer` live in the broker process. Each live endpoint has its own pulse OS thread; each psyche invocation gets its own child and stdout/stderr reader threads. `ManifestCell::runtime_snapshot` drops its `RwLock` guard before spawning. There is no shared executor and no shared in-memory lock between psyche invocation and the broker paths.\n\n## Direction matrix\n\n| Candidate direction | Possible in code? | Discriminator |\n|---|---:|---|\n| Brain/controller stops reading IPC -> its `controller_writer` blocks | Yes, direct | `controller_writer` has entered a write and does not return; `BRAIN_SUBSCRIBER_STALL_EVICT` after the 15 s deadline. |\n| One blocked brain connection -> multiple session stall-evicts | Yes, direct | Same broker connection/`SharedSend`; one writer blocks holding it and sibling writers wait on it. Current logs lack a connection id, so session-id clustering is suggestive, not proof. |\n| Blocked `controller_writer` -> PTY drain stops | No direct edge | `OutputLog::append` uses nonblocking `try_send`; writer does not own the log or PTY reader. |\n| Blocked `controller_writer` -> `INJECT_SETTLE_TIMEOUT` or `INJECT_ECHO_MISS` | No direct edge | Both inspect ring/high-water before controller delivery. A controller may be blocked at the same time, but cannot suppress the observed ring bytes. |\n| PTY output -> controller writer block | Yes | Every PTY chunk is appended then queued to the controller. Injection-induced redraw/echo is ordinary PTY output and can expose an already non-draining subscriber. |\n| Settle probes -> controller pressure | Yes, weak/contributory | Repeated DSR probes can induce PTY output, which is appended and forwarded. The default probe volume is tiny; it can contribute to a nearly full downstream path but does not block the drain. |\n| Hung PTY input consumer -> settle timeout | Yes, direct | `input_writer` blocks in `write_input`, probes queue/drop, and no readiness output reaches high-water. |\n| Dead PTY output drain -> settle timeout | Yes, direct | No append/high-water advance even if an input probe was written. |\n| Dead PTY output drain -> controller backpressure | No as a producing cause | No new frames are created. An already-blocked writer may remain blocked on old bytes, but drain death did not create those bytes. |\n| Dead translation process -> settle timeout | No direct edge | Settle does not consult translation liveness and runs before translation stdin send. Death can be detected after a coincident timeout, then faults the translation. |\n| Dead translation process -> controller backpressure | No direct edge | Translation stdout feeds `KeyCmd`, not the controller socket. Only resulting PTY output can flow downstream when translation is alive enough to generate commands. |\n| `PSYCHE_TURN_FAIL` -> PTY/controller stall | No direct edge | Separate brain thread/child, no broker lock/executor. |\n| Brain process suspension/starvation -> controller stalls and psyche timeout | Possible common cause, not one causing the other [INFERENCE] | Suspension prevents the main brain from reading broker IPC and prevents psyche wait polling; on resume, monotonic timeout may fire. Severe system resource pressure could have the same correlation. |\n| Controller stall -> psyche timeout | No direct edge | The psyche thread does not use the controller connection. Only a broader brain/process stall can affect both [INFERENCE]. |\n\n## Smallest discriminating probe for the next recurrence\n\nA diagnostic build should gate four non-payload breadcrumbs behind one environment flag such as `SPT_RCA_PTY_TRACE=1`. This is observation, not a behavioral fix:\n\n1. In `InputWriter`/`input_writer`, recognize the exact settle probe `ESC[6n` and log `PROBE_ENQUEUE accepted|dropped`, `PROBE_WRITE_START`, and `PROBE_WRITE_DONE`, with endpoint and a monotonic ticket. `START` must be immediately before `PtySession::write_input`; `DONE` immediately after it returns.\n2. Extend the existing `INJECT_SETTLE_TIMEOUT` breadcrumb with endpoint, baseline/current `OutputLog.high_water`, probe ticket, and the current controller `write_blocked_since` age. Do not log event or user bytes.\n3. Mint a broker `conn_id` in `handle_conn` and include it in gated `CTRL_WRITE_BEGIN/END {conn_id,sid,seq}` breadcrumbs around the existing `controller_writer` blocking region. This proves whether several session evicts share one `SharedSend` instead of inferring it from close timestamps.\n4. Add gated `PSYCHE_WAIT_BEGIN/END` around `wait_bounded` with brain PID, endpoint/role label, child PID, and monotonic time. The existing `PSYCHE_TURN_FAIL` supplies the timeout result.\n\nInterpret one recurrence without ambiguity:\n\n- `PROBE_ENQUEUE dropped`, no `START`: input FIFO/input-writer backlog is upstream of settle timeout.\n- `ENQUEUE accepted`, `START` without `DONE`: the PTY master input write is blocked, strongly indicating a non-consuming PTY input side.\n- `DONE`, but high-water remains baseline: the PTY accepted the write yet produced no observable output, or the PTY drain stopped before append. A live drain-byte counter added to the same timeout line can split those two if needed; `OutputLog.high_water` already proves the append boundary.\n- High-water rises before `CTRL_WRITE_BEGIN`, then `BEGIN` has no `END`: PTY output and ring consumption are healthy; the brain subscriber is downstream and caused controller backpressure.\n- Multiple no-`END` records with one `conn_id`: one shared brain connection caused the multi-session stall cascade.\n- `PSYCHE_WAIT_BEGIN` overlaps the above but the brain heartbeat/other IPC remains healthy: coincidence, no shared code dependency. If all connection reads/heartbeats cease together and psyche waits all expire after execution resumes, process-wide suspension/starvation is the common-cause signature [INFERENCE].\n\nThe smallest existing live approximation, requiring no build change, is to correlate `spt daemon status`'s broker-owned stall-evict count with a separate viewer connection while injection runs. Viewer output continuing through a stalled controller proves the PTY drain/log survived, as `brain_decouple.rs` already demonstrates. It cannot distinguish probe-enqueue, PTY-write, and drain boundaries, so the gated three-point probe breadcrumb is the smallest decisive recurrence instrument.\n\nRead-only investigation only. Per assignment constraints, no formatter, linter, build, or test was run."
}