docs\KNOWN-HAZARDS.md:390:[Omitted long matching line]
docs\KNOWN-HAZARDS.md:428:[Omitted long matching line]
docs\KNOWN-HAZARDS.md:429:[Omitted long matching line]
docs\KNOWN-HAZARDS.md:430:[Omitted long matching line]
docs\KNOWN-HAZARDS.md:494:| 7.14 | `EffectJournal::apply_once` releases its lock ACROSS `effect()` (reserve→release→run→finalize), never holding it across the blocking PTY write; `EffectKind::PtyWrite` is ephemeral (no per-keystroke fsync, in-memory dedup), durable kinds keep fsync — fixes interactive stutter + the hard input wedge (`brain IPC read deadline`) | `effect.rs` `apply_once`/`is_durable`, int `inject_control_wedge.rs` |
crates\spt-daemon\tests\inject_control_wedge.rs:365:// The hazard: `EffectJournal::apply_once` holds the GLOBAL journal lock across the
crates\spt-daemon\tests\inject_control_wedge.rs:469:    //    (op_id-carrying send_effect → broker dispatch_input → journal.apply_once
crates\spt-daemon\tests\idempotent.rs:225:    // `apply_once` is the only path that writes the PTY for an `op_id`, it runs
crates\spt-daemon\src\broker.rs:1495:                    .apply_once(key, EffectKind::PtyWrite, || {
crates\spt-daemon\src\broker.rs:1715:                    .apply_once((NET_EFFECT_SESSION, op), EffectKind::NetDial, || {
crates\spt-daemon\src\broker.rs:1820:    /// `stream_id` is recorded inside the `apply_once` closure, and a deduped
crates\spt-daemon\src\broker.rs:1848:                    .apply_once((NET_EFFECT_SESSION, op), EffectKind::NetSend, || {
crates\spt-daemon\src\broker.rs:1884:                    .apply_once((NET_EFFECT_SESSION, op), EffectKind::NetSend, || {
crates\spt-daemon\src\effect.rs:30://! the answer is the broker's process: [`EffectJournal::apply_once`] checks the
crates\spt-daemon\src\effect.rs:111:/// What [`EffectJournal::apply_once`] did.
crates\spt-daemon\src\effect.rs:202:    pub fn apply_once<F>(&self, key: EffectKey, kind: EffectKind, effect: F) -> io::Result<Outcome>
crates\spt-daemon\src\effect.rs:360:            j.apply_once((1, 1), EffectKind::PtyWrite, || Ok(()))
crates\spt-daemon\src\effect.rs:366:    // [unit->REQ-HAZARD-RESTART-IDEMPOTENT] the no-dup guard: a second apply_once
crates\spt-daemon\src\effect.rs:376:            .apply_once(key, EffectKind::PtyWrite, || {
crates\spt-daemon\src\effect.rs:382:            .apply_once(key, EffectKind::PtyWrite, || {
crates\spt-daemon\src\effect.rs:406:        let err = j.apply_once(key, EffectKind::Spool, || Err(io::Error::other("boom")));
crates\spt-daemon\src\effect.rs:414:        let ok = j.apply_once(key, EffectKind::Spool, || Ok(())).unwrap();
crates\spt-daemon\src\effect.rs:429:            j.apply_once(key, EffectKind::Registry, || Ok(())).unwrap();
crates\spt-daemon\src\effect.rs:439:            .apply_once(key, EffectKind::Registry, || {
crates\spt-daemon\src\effect.rs:453:    //   - BeforeIntent : the life returns before calling apply_once for op N.
crates\spt-daemon\src\effect.rs:454:    //   - AfterApplied : the life returns right after apply_once ran op N's
crates\spt-daemon\src\effect.rs:489:                .apply_once(key, kind_for(op), || {
crates\spt-daemon\src\effect.rs:493:                .expect("apply_once");
crates\spt-daemon\src\effect.rs:504:    // The wedge: `apply_once` takes the journal lock and HOLDS it across the
crates\spt-daemon\src\effect.rs:519:    // mid-flight, a DIFFERENT key's apply_once must still complete. Pre-fix the
crates\spt-daemon\src\effect.rs:523:    fn apply_once_does_not_hold_the_journal_lock_across_effect() {
crates\spt-daemon\src\effect.rs:534:            j1.apply_once((1, 1), EffectKind::PtyWrite, || {
crates\spt-daemon\src\effect.rs:540:            .expect("apply_once #1")
crates\spt-daemon\src\effect.rs:554:                .apply_once((1, 2), EffectKind::PtyWrite, || Ok(()))
crates\spt-daemon\src\effect.rs:555:                .expect("apply_once #2");
crates\spt-daemon\src\effect.rs:570:            "a second key's apply_once wedged while a different PtyWrite effect was \
crates\spt-daemon\src\effect.rs:593:            j.apply_once(pty_key, EffectKind::PtyWrite, || Ok(())).unwrap(),
crates\spt-daemon\src\effect.rs:597:            j.apply_once(net_key, EffectKind::NetSend, || Ok(())).unwrap(),
crates\spt-daemon\src\effect.rs:606:            j.apply_once(pty_key, EffectKind::PtyWrite, || {
crates\spt-daemon\src\effect.rs:615:            j.apply_once(net_key, EffectKind::NetSend, || {
crates\spt-daemon\src\nethost.rs:896:    /// `apply_once` closure, so a concurrent deduped replay always finds it).
crates\spt-daemon\src\nethost.rs:1067:    /// `apply_once` closure — same discipline as [`Self::record_dial_op`]).
traceable-reqs.toml:880:[Omitted long matching line]
traceable-reqs.toml:1232:[Omitted long matching line]
traceable-reqs.toml:1233:[Omitted long matching line]
traceable-reqs.toml:1250:[Omitted long matching line]
traceable-reqs.toml:1251:[Omitted long matching line]
V0.13.0-DELIVERY-CONTROL-JIT.md:77:- ROOT (measured): `apply_once` (effect.rs:168-188) holds one global mutex across PENDING-fsync →
V0.13.0-DELIVERY-CONTROL-JIT.md:94:- ROOT (confirmed in effect.rs): `apply_once` holds `inner.lock()` across `write_line(PENDING)`
V0.13.0-DELIVERY-CONTROL-JIT.md:98:  others=true). `apply_once`: reserve key (+ fsync PENDING for DURABLE) under lock -> RELEASE lock
V0.13.0-P0-JIT-PLAN.md:12:One dedicated **input-writer thread per session** = the SOLE caller of the blocking `write_input`, fed by a **bounded FIFO** (`sync_channel`). Every caller ENQUEUES (`try_send`) + returns immediately. Queue full ⇒ DROP excess + stamp session `INPUT_BACKPRESSURE` (never wedge; a slow harness self-heals as the writer drains). Order preserved (single FIFO + single writer); exactly-once preserved (PtyWrite is ephemeral — `apply_once` effect = the non-blocking enqueue → Applied).
V0.13.0-P0-JIT-PLAN.md:27:- `apply_once(key, PtyWrite, effect)` — make the `effect` the enqueue (reserve key under lock → `try_send` → Applied). PtyWrite already ephemeral (`effect.rs:189-199` invariant) so Applied-at-enqueue is sound. Find apply_once in spt-daemon (grep `apply_once`).
V0.13.0-P0-JIT-PLAN.md:35:5. `apply_once` PtyWrite effect = enqueue.
V0.13.0-P0-PTY-INPUT-WRITER-DESIGN.md:30:- `apply_once(key, PtyWrite, effect)` where the `effect` is now the **enqueue** (fast, non-blocking): reserve key under lock → try-enqueue → mark Applied. `PtyWrite` is already ephemeral (no journal lines, no fsync), so marking applied at enqueue is sound — a keystroke lost to a broker crash is retyped; PTY state is never rebuilt from keystroke replay (effect.rs:189-199 invariant).


CONTEXT.md:435:[Omitted long matching line]
crates\spt-daemon\tests\inject_control_wedge.rs:235:    // Park (b) probe: write_input's blocking write_all on a full PTY input buffer.
crates\spt-daemon\tests\inject_control_wedge.rs:276:    // connection. dispatch_endpoint_input → write_input → write_all on the PTY
crates\spt-daemon\tests\inject_control_wedge.rs:325:         (expect inject_acked=false = write_input parked on write_all) ==="
crates\spt-daemon\tests\inject_control_wedge.rs:341:    //   HYPOTHESIZED to park write_input's write_all (park b). That is UNVERIFIED
crates\spt-daemon\tests\inject_control_wedge.rs:367:// op_id) runs `write_input` INSIDE that lock. If a non-draining child's PTY input
crates\spt-daemon\tests\inject_control_wedge.rs:368:// buffer is full, `write_input` BLOCKS — holding the journal lock forever. So the
crates\spt-daemon\tests\inject_control_wedge.rs:376:// input without blocking write_input, so the journal lock is not held long → the
crates\spt-daemon\tests\inject_control_wedge.rs:379:// small input queue parks write_input → the journal lock is held → pre-fix the
crates\spt-daemon\tests\inject_control_wedge.rs:470:    //    runs write_input INSIDE the global journal lock). On Unix the full input
crates\spt-daemon\tests\inject_control_wedge.rs:471:    //    buffer parks write_input, holding the lock; on Windows ConPTY absorbs it.
crates\spt-daemon\tests\inject_control_wedge.rs:514:    // park write_input under the journal lock).
crates\spt-daemon\tests\inject_control_wedge.rs:691:         watchdog deadline — the journal lock is held across write_input (facet B, \
crates\spt-daemon\tests\inject_control_wedge.rs:1058:    //    — see g3 `secret_on_pty` — but the inject-worker's `write_input` of the same
crates\spt-term\src\surface.rs:97:/// and drive `write_input` / `resize` from its IPC thread while a separate
crates\spt-term\src\surface.rs:105:    fn write_input(&self, bytes: &[u8]) -> Result<(), SurfaceError>;
crates\spt-term\src\surface.rs:113:    /// arrow key = `ESC [ C`). A semantic alias of [`SessionSurface::write_input`]
crates\spt-term\src\surface.rs:119:        self.write_input(bytes)
crates\spt-term\src\surface.rs:132:        self.write_input(&buf)
crates\spt-term\src\pty.rs:31:/// Build one with [`PtySession::spawn`]; drive input via [`SessionSurface::write_input`]
crates\spt-term\src\pty.rs:236:    fn write_input(&self, bytes: &[u8]) -> Result<(), SurfaceError> {
crates\spt-daemon\src\broker.rs:792:                let _ = session.write_input(&bytes);
crates\spt-daemon\src\broker.rs:805:/// then drain `{key}`/`{text}`/`{delay_ms}` into `session.write_input` until
crates\spt-daemon\src\broker.rs:853:                        let _ = session.write_input(&bytes);
crates\spt-daemon\src\broker.rs:857:                    let _ = session.write_input(text.as_bytes());
crates\spt-daemon\src\broker.rs:1503:                            .write_input(&bytes)
crates\spt-daemon\src\broker.rs:1506:                    .map_err(|e| format!("write_input failed: {e}"))?;
crates\spt-daemon\src\broker.rs:1519:                    .write_input(&bytes)
crates\spt-daemon\src\broker.rs:1520:                    .map_err(|e| format!("write_input failed: {e}"))
crates\spt-daemon\src\broker.rs:1590:                    .write_input(&bytes)
crates\spt-daemon\src\broker.rs:1591:                    .map_err(|e| format!("write_input failed: {e}"))?;
crates\spt-daemon\src\translation.rs:166:/// `Mutex` and does the actual `write_input` off-lock (the W1 lesson: never block
docs\KNOWN-HAZARDS.md:417:[Omitted long matching line]
docs\adr\0022-spt-hosted-idle-delivery-translation-binary.md:12:plus `\r` straight into the PTY stdin (`dispatch_endpoint_input` → `session.write_input`). This has
traceable-reqs.toml:553:[Omitted long matching line]
traceable-reqs.toml:554:[Omitted long matching line]
traceable-reqs.toml:1191:[Omitted long matching line]
traceable-reqs.toml:1197:[Omitted long matching line]
traceable-reqs.toml:1232:[Omitted long matching line]
traceable-reqs.toml:1233:[Omitted long matching line]
traceable-reqs.toml:1250:[Omitted long matching line]
traceable-reqs.toml:1251:[Omitted long matching line]
V0.13.0-DELIVERY-CONTROL-JIT.md:103:- FIX (2) park bound (pty.rs write_input holds writer mutex across blocking write_all; DSR-answer
V0.13.0-DELIVERY-CONTROL-JIT.md:155:  one-at-a-time: open floor → feed binary `{type:event}` → drain key/text(`write_input`)/
V0.13.0-DELIVERY-CONTROL-JIT.md:168:  bound: bounded/fail-fast `write_input` + DSR off the writer mutex (spt-term/pty.rs);
V0.13.0-P0-JIT-PLAN.md:9:Paste into `spt rc` → a burst fills the ConPTY input buffer → `session.write_input(&bytes)` runs SYNCHRONOUSLY on the broker dispatch thread and blocks → dispatch can't service the next frame (re-attach subscribe / become_controller restore-write / inject-floor flush) → broker WEDGES (`brain IPC read deadline`). Completes the W1b-DEFERRED fix (2) of `REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE`.
V0.13.0-P0-JIT-PLAN.md:12:One dedicated **input-writer thread per session** = the SOLE caller of the blocking `write_input`, fed by a **bounded FIFO** (`sync_channel`). Every caller ENQUEUES (`try_send`) + returns immediately. Queue full ⇒ DROP excess + stamp session `INPUT_BACKPRESSURE` (never wedge; a slow harness self-heals as the writer drains). Order preserved (single FIFO + single writer); exactly-once preserved (PtyWrite is ephemeral — `apply_once` effect = the non-blocking enqueue → Applied).
V0.13.0-P0-JIT-PLAN.md:19:Apply the SAME to the PTY write side: a `{ SyncSender<InputRecord>, JoinHandle }` on the session, thread = `input_writer` draining to `write_input`.
V0.13.0-P0-JIT-PLAN.md:22:- `PtySession` — owns PTY master/writer + `write_input`; held as `Arc<PtySession>` on `HostedSession` (`broker.rs:766-768`). Seam = `spt_term::SessionSurface` (CONTEXT L435). The writer thread owns the sole `write_input` access.
V0.13.0-P0-JIT-PLAN.md:24:- `dispatch_input` (`broker.rs:1459`) — the broker-dispatch write caller; also where inject-floor BUFFERING of operator bytes happens. Replace its `write_input` with enqueue.
V0.13.0-P0-JIT-PLAN.md:25:- `flush_inject_floor` (`broker.rs:787`) — calls `session.write_input(&bytes)`; the W2 inject-floor flush. Its writes move to the writer thread (enqueue). The open→drain→commit→flush choreography (`run_inject_worker` ~819) keeps working with the lone writer.
V0.13.0-P0-JIT-PLAN.md:26:- `serve_attach` → `brain.send_effect` (`attach.rs:197`; `brain.rs:449 send_effect`) → KIND_INPUT → broker dispatch → `dispatch_input`. The operator-keystroke path; it reaches `write_input` via dispatch_input, so fixing dispatch_input covers it.
V0.13.0-P0-JIT-PLAN.md:33:3. Add the input-writer sink to `HostedSession`/`PtySession`; spawn at session creation; thread is the sole `write_input` caller.
V0.13.0-P0-JIT-PLAN.md:40:   - int: **REPRO-FIRST, non-vacuous** (W1b's int escaped this). Real broker+PTY+rc, NO mocks. A harness stand-in that STOPS draining stdin so `write_input` genuinely PARKS → RED pre-fix (concurrent rc attach gets `brain IPC read deadline`/no output) → GREEN post-fix (concurrent attach OPENS + RECEIVES output, operator keystroke still accepted, DROP+`INPUT_BACKPRESSURE` on saturation + HEAL on resume). cfg(unix) forkpty park leg covered (W1b gravity-linux follow-up folds in).
V0.13.0-P0-JIT-PLAN.md:45:Repro genuinely blocks write_input (RED pre-fix) · concurrent attach opens+gets output under a parked write · operator keystroke still accepted · DROP+`INPUT_BACKPRESSURE` on saturation, heal on resume · exactly-once + order preserved · inject-floor choreography intact · Unix forkpty leg · clippy/traceable/docs-drift.
V0.13.0-P0-PTY-INPUT-WRITER-DESIGN.md:6:The operator keystroke path: `rc` → net-stream `Input` → `serve_attach` (attach.rs:197 `brain.send_effect(op_id)`) → KIND_INPUT → broker dispatch loop (broker.rs:1091) → `dispatch_input` (broker.rs:1459) → `session.write_input(&bytes)` **synchronously on the broker request-handling thread**.
V0.13.0-P0-PTY-INPUT-WRITER-DESIGN.md:8:[Omitted long matching line]
V0.13.0-P0-PTY-INPUT-WRITER-DESIGN.md:10:**Not a bug-2 regression** — the byte path funnels to the same `write_input`; the paste just reliably fills the buffer. The exact cross-attach starvation vector (a shared per-session writer mutex held by the stuck write vs the dispatch-thread block) is **pinned by the repro** (below); the fix cures it either way.
V0.13.0-P0-PTY-INPUT-WRITER-DESIGN.md:14:- L435: `spt_term::SessionSurface` = the `write_input` + `resize` seam (`send_keys`/`send_line` first-class). The single-writer change lives behind this seam.
V0.13.0-P0-PTY-INPUT-WRITER-DESIGN.md:19:- Each `PtySession` (broker.rs, when the session is created) spawns ONE dedicated **input-writer thread** that owns the PTY master write handle and is the **only** code that calls the blocking `write_input`.
V0.13.0-P0-PTY-INPUT-WRITER-DESIGN.md:22:- The writer thread drains the channel to `write_input` at the harness's pace. A blocked/slow harness blocks ONLY its own writer thread — the broker dispatch thread, attach-opens, resizes, and every other session stay fully live.
V0.13.0-P0-PTY-INPUT-WRITER-DESIGN.md:33:- Durable kinds (NetSend/NetDial/Registry/Spool) do NOT route through `write_input` — untouched, keep their fsync'd PENDING/DONE.
V0.13.0-P0-PTY-INPUT-WRITER-DESIGN.md:38:1. Make `write_input` **actually block forever** — a real PTY consumer (harness stand-in) that stops draining its stdin, so the ConPTY/forkpty input buffer genuinely fills and the write parks.
V0.13.0-P0-PTY-INPUT-WRITER-DESIGN.md:52:Repro genuinely blocks write_input (non-vacuous: RED pre-fix) · concurrent attach opens + gets output under a parked write · operator keystroke still accepted · DROP+`INPUT_BACKPRESSURE` on saturation, heal on resume · exactly-once + order preserved · inject-floor choreography intact · Unix forkpty leg covered · clippy/traceable/docs-drift. Then operator HITL (paste a large block in a real CC rc session: lands, no wedge, can still attach).


Read core broker sections: HostedSession struct, inject-floor, dispatch_input, viewer/controller writer patterns.


760	    /// session reverts to raw inject and `dispatch_input` stops pinging.
761	    faulted: Arc<AtomicBool>,
762	}
763
764	/// A child the broker hosts under a PTY — the §B-owned resources for one
765	/// session, plus its gapless-resume output log.
766	struct HostedSession {
767	    /// The PTY + child (PTY master/writer + spawned child process).
768	    session: Arc<PtySession>,
769	    /// The output drain pump (kept so teardown can stop it).
770	    #[allow(dead_code)] // held for ownership/teardown; not read directly
771	    drain: Drain,
772	    /// The sequenced, buffered output + current subscriber.
773	    log: Arc<Mutex<OutputLog>>,
774	    /// The owning endpoint (the spawn's label; empty = unlabeled). The
775	    /// inbound dispatcher's gate subject for remote-drive (D9-1).
776	    endpoint: String,
777	    /// The translation-binary attachment (Layer C), or `None` for a session with
778	    /// no `[message-idle-translation-binary]` — then inbound delivery raw-injects.
779	    translation: Option<Arc<Translation>>,
780	}
781
782	/// Drain the inject floor's buffered controller input to the PTY **after** an
783	/// injected sequence, then release the floor. Each step takes the buffered bytes
784	/// OFF the lock before writing (the W1 rule: never hold a lock across a PTY
785	/// write); input arriving mid-flush keeps buffering, so strict order is preserved
786	/// until the buffer drains empty and the floor frees.
787	fn flush_inject_floor(floor: &Mutex<InjectFloor>, session: &PtySession) {
788	    loop {
789	        let chunk = floor.lock().unwrap().take_or_release();
790	        match chunk {
791	            Some(bytes) => {
792	                let _ = session.write_input(&bytes);
793	            }
794	            None => break,
795	        }
796	    }
797	}
798
799	/// The inject worker (W2 Layer C): one thread per translation-bearing session. It
800	/// drives inbound `<EVENT>`s through the binary and applies the binary's keystroke
801	/// commands to the PTY ATOMICALLY w.r.t. the operator's live controller.
802	///
803	/// Per queued event: drop any stray pre-event commands, OPEN the floor (operator
804	/// input now buffers in `dispatch_input`), feed the binary the `{type:event}`,
805	/// then drain `{key}`/`{text}`/`{delay_ms}` into `session.write_input` until
806	/// `{commit}` — bounded by [`INJECT_COMMIT_DEADLINE`]. On commit: flush the
807	/// buffered controller bytes AFTER the injected sequence and release the floor. On
808	/// deadline-miss or binary death: flush+release anyway (operator input is never
809	/// stranded), then FAULT — terminate the binary, set `faulted`, and exit (the
810	/// session degrades to raw inject). The loop ends when `event_tx` is dropped
811	/// (session teardown), reaping the child.
812	///
813	/// The atomic open→drain→commit→flush is the W1 coexistence mechanism: an injected
814	/// sequence and the operator's live controller share one PTY without clobbering
815	/// each other (REQ-HAZARD-INJECT-CONTROL-COEXIST), and the deadline FAULT path is
816	/// the "never park forever" bound applied to injection.
817	// [impl->REQ-MSG-IDLE-TRANSLATION-BINARY]
818	// [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]
819	fn run_inject_worker(
820	    session: Arc<PtySession>,
821	    child: Arc<TranslationChild>,
822	    floor: Arc<Mutex<InjectFloor>>,
823	    event_rx: Receiver<String>,
824	    cmd_rx: Receiver<KeyCmd>,
825	    faulted: Arc<AtomicBool>,
826	) {
827	    // `event_rx` blocks here; events queue → one sequence at a time.
828	    for envelope in event_rx {
829	        // A well-behaved filter only emits in response to an event, but drop any
830	        // unsolicited commands left in the channel so they can't be mis-attributed
831	        // to this event's sequence.
832	        while cmd_rx.try_recv().is_ok() {}
833
834	        floor.lock().unwrap().open();
835	        if child.send(&ToBinary::Event { envelope }).is_err() {
836	            // stdin write failed → the binary is gone; release the floor + fault.
837	            flush_inject_floor(&floor, &session);
838	            fault_translation(&faulted, &child, "stdin write failed");
839	            return;
840	        }
841
842	        let deadline = Instant::now() + inject_commit_deadline();
843	        let mut committed = false;
844	        loop {
845	            let now = Instant::now();
846	            let remaining = deadline.saturating_duration_since(now);
847	            if remaining.is_zero() {
848	                break; // deadline → fault below
849	            }
850	            match cmd_rx.recv_timeout(remaining) {
851	                Ok(KeyCmd::Key { key }) => {
852	                    if let Some(bytes) = key_to_bytes(&key) {
853	                        let _ = session.write_input(&bytes);
854	                    }
855	                }
856	                Ok(KeyCmd::Text { text }) => {
857	                    let _ = session.write_input(text.as_bytes());
858	                }
859	                Ok(KeyCmd::Delay { delay_ms }) => {
860	                    // Clamp the pause to the remaining deadline so a huge/hostile
861	                    // delay can't park the floor past the bound.
862	                    let want = Duration::from_millis(delay_ms);
863	                    let cap = deadline.saturating_duration_since(Instant::now());
864	                    thread::sleep(want.min(cap));
865	                }
866	                Ok(KeyCmd::Commit { .. }) => {
867	                    committed = true;
868	                    break;
869	                }
870	                // Deadline with no commit, OR the child's stdout closed (binary
871	                // died): both end the sequence; `committed` stays false → fault.
872	                Err(RecvTimeoutError::Timeout | RecvTimeoutError::Disconnected) => break,
873	            }
874	        }
875
876	        // Flush buffered controller input AFTER the injected bytes + release the
877	        // floor — in EVERY exit path, so operator keystrokes are never stranded.
878	        flush_inject_floor(&floor, &session);
879

    /// session reverts to raw inject and `dispatch_input` stops pinging.
    faulted: Arc<AtomicBool>,
}

/// A child the broker hosts under a PTY — the §B-owned resources for one
/// session, plus its gapless-resume output log.
struct HostedSession {
    /// The PTY + child (PTY master/writer + spawned child process).
    session: Arc<PtySession>,
    /// The output drain pump (kept so teardown can stop it).
    #[allow(dead_code)] // held for ownership/teardown; not read directly
    drain: Drain,
    /// The sequenced, buffered output + current subscriber.
    log: Arc<Mutex<OutputLog>>,
    /// The owning endpoint (the spawn's label; empty = unlabeled). The
    /// inbound dispatcher's gate subject for remote-drive (D9-1).
    endpoint: String,
    /// The translation-binary attachment (Layer C), or `None` for a session with
    /// no `[message-idle-translation-binary]` — then inbound delivery raw-injects.
    translation: Option<Arc<Translation>>,
}

/// Drain the inject floor's buffered controller input to the PTY **after** an
/// injected sequence, then release the floor. Each step takes the buffered bytes
/// OFF the lock before writing (the W1 rule: never hold a lock across a PTY
/// write); input arriving mid-flush keeps buffering, so strict order is preserved
/// until the buffer drains empty and the floor frees.
fn flush_inject_floor(floor: &Mutex<InjectFloor>, session: &PtySession) {
    loop {
        let chunk = floor.lock().unwrap().take_or_release();
        match chunk {
            Some(bytes) => {
                let _ = session.write_input(&bytes);
            }
            None => break,
        }
    }
}

/// The inject worker (W2 Layer C): one thread per translation-bearing session. It
/// drives inbound `<EVENT>`s through the binary and applies the binary's keystroke
/// commands to the PTY ATOMICALLY w.r.t. the operator's live controller.
///
/// Per queued event: drop any stray pre-event commands, OPEN the floor (operator
/// input now buffers in `dispatch_input`), feed the binary the `{type:event}`,
/// then drain `{key}`/`{text}`/`{delay_ms}` into `session.write_input` until
/// `{commit}` — bounded by [`INJECT_COMMIT_DEADLINE`]. On commit: flush the
/// buffered controller bytes AFTER the injected sequence and release the floor. On
/// deadline-miss or binary death: flush+release anyway (operator input is never
/// stranded), then FAULT — terminate the binary, set `faulted`, and exit (the
/// session degrades to raw inject). The loop ends when `event_tx` is dropped
/// (session teardown), reaping the child.
///
/// The atomic open→drain→commit→flush is the W1 coexistence mechanism: an injected
/// sequence and the operator's live controller share one PTY without clobbering
/// each other (REQ-HAZARD-INJECT-CONTROL-COEXIST), and the deadline FAULT path is
/// the "never park forever" bound applied to injection.
// [impl->REQ-MSG-IDLE-TRANSLATION-BINARY]
// [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]
fn run_inject_worker(
    session: Arc<PtySession>,
    child: Arc<TranslationChild>,
    floor: Arc<Mutex<InjectFloor>>,
    event_rx: Receiver<String>,
    cmd_rx: Receiver<KeyCmd>,
    faulted: Arc<AtomicBool>,
) {
    // `event_rx` blocks here; events queue → one sequence at a time.
    for envelope in event_rx {
        // A well-behaved filter only emits in response to an event, but drop any
        // unsolicited commands left in the channel so they can't be mis-attributed
        // to this event's sequence.
        while cmd_rx.try_recv().is_ok() {}

        floor.lock().unwrap().open();
        if child.send(&ToBinary::Event { envelope }).is_err() {
            // stdin write failed → the binary is gone; release the floor + fault.
            flush_inject_floor(&floor, &session);
            fault_translation(&faulted, &child, "stdin write failed");
            return;
        }

        let deadline = Instant::now() + inject_commit_deadline();
        let mut committed = false;
        loop {
            let now = Instant::now();
            let remaining = deadline.saturating_duration_since(now);
            if remaining.is_zero() {
                break; // deadline → fault below
            }
            match cmd_rx.recv_timeout(remaining) {
                Ok(KeyCmd::Key { key }) => {
                    if let Some(bytes) = key_to_bytes(&key) {
                        let _ = session.write_input(&bytes);
                    }
                }
                Ok(KeyCmd::Text { text }) => {
                    let _ = session.write_input(text.as_bytes());
                }
                Ok(KeyCmd::Delay { delay_ms }) => {
                    // Clamp the pause to the remaining deadline so a huge/hostile
                    // delay can't park the floor past the bound.
                    let want = Duration::from_millis(delay_ms);
                    let cap = deadline.saturating_duration_since(Instant::now());
                    thread::sleep(want.min(cap));
                }
                Ok(KeyCmd::Commit { .. }) => {
                    committed = true;
                    break;
                }
                // Deadline with no commit, OR the child's stdout closed (binary
                // died): both end the sequence; `committed` stays false → fault.
                Err(RecvTimeoutError::Timeout | RecvTimeoutError::Disconnected) => break,
            }
        }

        // Flush buffered controller input AFTER the injected bytes + release the
        // floor — in EVERY exit path, so operator keystrokes are never stranded.
        flush_inject_floor(&floor, &session);


100	// [impl->REQ-HAZARD-VIEWER-ISOLATION]
101	fn viewer_send_evicts<T>(r: &Result<(), TrySendError<T>>) -> bool {
102	    r.is_err()
103	}
104
105	/// One read-only viewer's isolated sink (REQ-HAZARD-VIEWER-ISOLATION). The drain
106	/// thread `try_send`s output frames into `tx` (a bounded queue); a dedicated
107	/// `writer` thread does the BLOCKING socket write, so a wedged viewer backs up
108	/// only its own queue — never the drain. When the queue overflows the sink is
109	/// dropped (the `tx` closes → the writer ends) and removed from the map.
110	struct ViewerSink {
111	    /// The bounded live-frame queue to this viewer's writer thread.
112	    tx: SyncSender<Envelope>,
113	    /// The viewer's IPC send half — kept ONLY for identity matching on detach
114	    /// (a dropped brain connection removes its viewer rows); never written here.
115	    send: SharedSend,
116	    /// The writer thread handle (held for ownership; it exits when `tx` drops or
117	    /// a socket write fails).
118	    _writer: JoinHandle<()>,
119	}
120
121	/// The controller live-frame channel depth (frames). Deeper than a viewer's
122	/// queue because the controller is the AUTHORITATIVE view — it is never silently
123	/// evicted on overflow (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST); instead the
124	/// drain blocks on a bounded `send_timeout` and the controller is detached only
125	/// after [`CONTROLLER_WRITE_DEADLINE`] of a fully-stalled socket. Generous so a
126	/// momentary slow consumer never trips the deadline.
127	const CONTROLLER_CHANNEL_DEPTH: usize = 4096;
128
129	/// How long the drain tolerates a CONTROLLER whose socket has fully stalled (its
130	/// channel stays full) before declaring it gone, detaching it, and resuming
131	/// (W1). This BOUNDS the wedge: a backed-up controller can stall live output for
132	/// at most this long — never forever (the v0.12.x root) — and never blocks the
133	/// log lock (the send happens off-lock; see [`OutputLog::append`]). A live but
134	/// merely slow operator drains a slot before this elapses and is untouched.
135	const CONTROLLER_WRITE_DEADLINE: Duration = Duration::from_secs(5);
136
137	/// Poll interval for the controller `try_send` deadline loop (std `SyncSender`
138	/// has no stable `send_timeout`). Only used while the channel is FULL — the
139	/// common (has-room) path never sleeps.
140	const CONTROLLER_SEND_POLL: Duration = Duration::from_millis(5);
141
142	/// MANDATORY bound on one injected keystroke sequence (W2 Layer C, ADR-0022 /
143	/// doyle non-negotiable = W1's "never park forever"): the inject worker holds the
144	/// floor — buffering operator input — only while a binary's response to one
145	/// `{type:event}` is in flight. If no `{commit}` arrives within this window the
146	/// worker FLUSHES the buffer, RELEASES the floor, FAULTS the binary (terminate, no
147	/// respawn → the session degrades to raw inject), and never re-opens. A wedged or
148	/// buggy translation binary can therefore stall operator input for at most this
149	/// long. Mirrors [`CONTROLLER_WRITE_DEADLINE`]'s order (5 s).
150	const INJECT_COMMIT_DEADLINE: Duration = Duration::from_secs(5);
151
152	/// The effective commit deadline. Production is the const above; the int gate
153	/// (Layer G no-commit FAULT test) shrinks it via `SPT_INJECT_COMMIT_DEADLINE_MS`
154	/// so the structural outcome (floor releases → buffered input flushes → binary
155	/// faults) is asserted in well under a second rather than a 5 s wall-clock wait —
156	/// the deadline's PRESENCE is the invariant, not its exact length.
157	fn inject_commit_deadline() -> Duration {
158	    match std::env::var("SPT_INJECT_COMMIT_DEADLINE_MS") {
159	        Ok(ms) => ms
160	            .parse::<u64>()
161	            .map(Duration::from_millis)
162	            .unwrap_or(INJECT_COMMIT_DEADLINE),
163	        Err(_) => INJECT_COMMIT_DEADLINE,
164	    }
165	}
166
167	/// The interactive CONTROLLER's isolated sink (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST).
168	/// Symmetric with [`ViewerSink`] — a dedicated writer thread does the BLOCKING
169	/// socket write so a backed-up controller can never stall the session drain
170	/// thread (the v0.12.x wedge: the controller write was inline on the drain,
171	/// holding `Mutex<OutputLog>`). Unlike a viewer it is AUTHORITATIVE: its writer
172	/// advances `delivered_through` on each successful write, and it is never evicted
173	/// on a single full `try_send` — only after [`CONTROLLER_WRITE_DEADLINE`].
174	struct ControllerSink {
175	    /// The bounded live-frame queue to the controller's writer thread.
176	    tx: SyncSender<CtrlMsg>,
177	    /// The controller's IPC send half — kept for identity matching on detach and
178	    /// for the `exit`-frame fan-out ([`OutputLog::all_sinks`]).
179	    send: SharedSend,
180	    /// The controller's identity: `Some(node)` = a remote operator, `None` =
181	    /// local (the brain's spawn-reader / resume). Ownership keys on this.
182	    by: Option<String>,
183	    /// The writer thread handle (held for ownership; exits when `tx` drops — the
184	    /// drain's bounded `send_timeout` then sees `Disconnected` and detaches — or
185	    /// when a socket write fails).
186	    _writer: JoinHandle<()>,
187	}
188
189	/// A bounded, sequenced output log for one session — the gapless-resume buffer
190	/// AND the W2.5 controller/viewer fan-out hub (REQ-RCVIEW-1).
191	///
192	/// The session's single drain thread is the only producer ([`OutputLog::append`]).
193	/// Output fans out to ONE interactive `controller` (the authoritative,
194	/// cursor-advancing, blocking path) plus ANY NUMBER of read-only `viewers` (each
195	/// an isolated bounded queue + writer thread, evicted on overflow). Every
196	/// controller send and viewer `try_send` happens while the enclosing
197	/// `Mutex<OutputLog>` is held, so frames reach each sink in strict `seq` order.
198	struct OutputLog {
199	    session_id: u64,
200	    next_seq: u64,
201	    ring: VecDeque<(u64, Vec<u8>)>,
202	    cap_chunks: usize,
203	    /// The single interactive controller's isolated sink (W1: a dedicated writer
204	    /// thread + bounded channel + authoritative cursor). `None` ⇒ undriven. The
205	    /// controller's identity (`Some(node)` remote / `None` local) lives in
206	    /// [`ControllerSink::by`] and keys ownership — a same-`by` re-subscribe
207	    /// silently re-takes (no `Displaced`, the gate-#7 self-kick guard); only a
208	    /// DIFFERENT remote's `Take` displaces.
209	    controller: Option<ControllerSink>,
210	    /// Monotonic controller generation. Bumped on every [`become_controller`];
211	    /// the drain's deadline-evict ([`mark_controller_gone`]) carries the epoch it
212	    /// observed and only detaches if it still matches — so a slow-controller
213	    /// timeout can never unseat a NEWER controller that attached meanwhile.
214	    controller_epoch: u64,
215	    /// The read-only viewers, keyed by a broker-minted viewer id.
216	    viewers: HashMap<u64, ViewerSink>,
217	    /// Monotonic viewer-id source.
218	    next_viewer_id: u64,
219	    /// The owning endpoint (the spawn's label; empty = unlabeled / raw test).
220	    /// The broker is the SINGLE writer of `driven_by` + `viewer_count` on this
221	    /// perch (REQ-RCVIEW-1, resolves the clear-race); empty ⇒ nowhere to stamp.
222	    endpoint: String,
223	    /// Last-known PTY size — seeded at spawn, updated on a controller resize.
224	    /// Pushed to a viewer on attach + to all viewers on resize (the letterbox
225	    /// feed); the broker holds it since the PTY exposes no size getter.
226	    size: (u16, u16),
227	    /// Cursor-of-record (restoration D4-1, ADR-0018 Q6): the next seq a
228	    /// re-attaching CONTROLLER still needs (= highest seq **successfully written**
229	    /// to the controller, +1). It is **log** state, not subscriber state, so it
230	    /// **survives `detach_if`** — a dead brain leaves its resume point behind, and
231	    /// a cold-starting brain reads it (via `KIND_SESSIONS` → `resume_seq`) instead
232	    /// of replaying the whole ring from 0. **Viewer writes NEVER move it**
233	    /// (REQ-RCVIEW-1: the brain-resume cursor tracks the controller only).
234	    ///
235	    /// **Advanced only on a successful socket write** (`Ok`), and **monotonic**
236	    /// (never decreases) — see [`advanced_cursor`]. Both invariants are
237	    /// load-bearing for at-least-once output: advancing past a *failed* write to a
238	    /// dying-but-not-yet-detached controller would make resume skip that chunk
239	    /// forever (at-least-once → at-most-once); decreasing on a deliberate
240	    /// `attach(sub, 0)` rewind-replay would reset the resume point under a live
241	    /// reader.
242	    ///
243	    /// W1: now an `Arc<AtomicU64>` shared with the controller's writer thread,
244	    /// which advances it (via [`advanced_cursor`], CAS-monotonic) as it writes —
245	    /// the cursor moved off the drain thread with the controller delivery itself.
246	    delivered_through: Arc<AtomicU64>,
247	}
248
249	/// The post-write resume cursor (pure — the D4-1 unit kernel, mirroring
250	/// `brainproc::consumer_gate`/`next_backoff`): advance to `seq + 1` **only** on a
251	/// successful write, and **monotonically** (never below `current`). A failed
252	/// write (`write_ok == false`) leaves the cursor untouched, and a re-send of an
253	/// already-delivered `seq` (an `attach(sub, 0)` rewind, `seq + 1 <= current`)
254	/// cannot lower it. This is the exact decision the `let _ = write_frame(...)`
255	/// swallow used to drop (doyle D4-1 amendments 1–3).
256	// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]
257	fn advanced_cursor(current: u64, seq: u64, write_ok: bool) -> u64 {
258	    if write_ok {
259	        current.max(seq + 1)
260	    } else {
261	        current
262	    }
263	}
264
265	impl OutputLog {
266	    fn new(session_id: u64, cap_chunks: usize, endpoint: String, size: (u16, u16)) -> Self {
267	        OutputLog {
268	            session_id,
269	            next_seq: 0,
270	            ring: VecDeque::new(),
271	            cap_chunks,
272	            controller: None,
273	            controller_epoch: 0,
274	            viewers: HashMap::new(),
275	            next_viewer_id: 0,
276	            endpoint,
277	            size,
278	            delivered_through: Arc::new(AtomicU64::new(0)),
279	        }
280	    }
281
282	    /// Append a freshly-produced output chunk: assign the next `seq`, retain it
283	    /// in the bounded ring, FAN OUT to viewers (non-blocking `try_send`, evicting
284	    /// any overflowed/gone queue — REQ-HAZARD-VIEWER-ISOLATION), and HAND the
285	    /// controller frame back to the caller as a [`ControllerJob`] to deliver
286	    /// **off the log lock** (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST).
287	    ///
288	    /// W1: the controller write is no longer inline here. The drain used to call
289	    /// `write_frame(controller)` synchronously while holding `Mutex<OutputLog>`,
290	    /// so a backed-up controller socket parked the drain thread WITH THE LOCK HELD
291	    /// — wedging the whole session (every attach / `KIND_SESSIONS` that needs the
292	    /// log lock blocked). Now this returns the job and releases the lock; the
293	    /// drain does a bounded, off-lock send into the controller's writer-thread
294	    /// channel ([`ControllerJob::deliver`]). Called only from the session's
295	    /// single drain thread.
296	    // [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]
297	    #[must_use]
298	    fn append(&mut self, bytes: &[u8]) -> Option<ControllerJob> {
299	        let seq = self.next_seq;
300	        self.next_seq += 1;
301	        self.ring.push_back((seq, bytes.to_vec()));
302	        while self.ring.len() > self.cap_chunks {
303	            self.ring.pop_front();
304	        }
305	        let frame = output_envelope(self.session_id, seq, bytes);
306	        // Viewers: non-blocking fan-out; a full / disconnected queue = eviction.
307	        if !self.viewers.is_empty() {
308	            let mut evicted: Vec<u64> = Vec::new();
309	            for (vid, sink) in self.viewers.iter() {
310	                if viewer_send_evicts(&sink.tx.try_send(frame.clone())) {
311	                    evicted.push(*vid);
312	                }
313	            }
314	            if !evicted.is_empty() {
315	                for vid in evicted {
316	                    self.viewers.remove(&vid);
317	                }
318	                self.stamp_viewer_count();
319	            }
320	        }
321	        // Controller: hand the frame to the caller for an off-lock, bounded send
322	        // into the controller writer thread's channel (which does the blocking
323	        // socket write + advances the authoritative cursor). The drain never
324	        // blocks under the log lock again.
325	        self.controller.as_ref().map(|c| ControllerJob {
326	            tx: c.tx.clone(),
327	            seq,
328	            frame,
329	            epoch: self.controller_epoch,
330	        })
331	    }
332
333	    /// The current controller's identity (`Some(node)` remote / `None` local),
334	    /// or `None` when undriven. The `by`-keyed ownership logic in
335	    /// [`resolve_subscribe`] reads this.
336	    fn controller_by(&self) -> Option<&str> {
337	        self.controller.as_ref().and_then(|c| c.by.as_deref())
338	    }
339
340	    /// Become (or silently re-take) the interactive controller from `from_seq`.
341	    ///
342	    /// W1: the ring replay is now the INITIAL BATCH of a dedicated controller
343	    /// writer thread (symmetric with [`add_viewer`]) — it is no longer written
344	    /// inline under the log lock. The writer thread advances the authoritative
345	    /// `delivered_through` cursor as it writes (D4-1: only on a successful write,
346	    /// monotonic, never the unsent tail). Dropping any prior [`ControllerSink`]
347	    /// ends its writer; bumping `controller_epoch` makes a stale deadline-evict a
348	    /// no-op. Stamps `driven_by` to the new controller's identity.
349	    fn become_controller(&mut self, sub: SharedSend, by: Option<String>, from_seq: u64) {
350	        // Drop the prior controller sink first (its writer ends when tx drops),
351	        // and bump the generation so an in-flight deadline-evict for the old
352	        // controller can't unseat this one.
353	        self.controller = None;
354	        self.controller_epoch += 1;
355	        // Ring replay = the writer thread's initial batch (owned by the thread,
356	        // NOT pushed through the live channel — replay must never block the drain).
357	        let initial: Vec<(u64, Vec<u8>)> = self
358	            .ring
359	            .iter()
360	            .filter(|(s, _)| *s >= from_seq)
361	            .cloned()
362	            .collect();
363	        let (tx, rx) = sync_channel::<CtrlMsg>(CONTROLLER_CHANNEL_DEPTH);
364	        let writer_send = Arc::clone(&sub);
365	        let dt = Arc::clone(&self.delivered_through);
366	        let sid = self.session_id;
367	        let writer = thread::spawn(move || controller_writer(sid, writer_send, initial, rx, dt));
368	        self.controller = Some(ControllerSink {
369	            tx,
370	            send: sub,
371	            by,
372	            _writer: writer,
373	        });
374	        self.stamp_driven_by();
375	    }
376
377	    /// Detach the controller and UNLATCH the perch — the ONE unlatch path shared
378	    /// by W1's deadline-evict ([`mark_controller_gone`]), [`detach_if`], and (per
379	    /// the W1↔W5 synergy) W5's reconcile self-heal. Dropping the sink ends its
380	    /// writer thread; clearing the slot re-stamps `driven_by` to `None` (so an
381	    /// `ONLINE+CONTROLLED` latch cannot outlive a gone controller).
382	    fn clear_controller(&mut self) {
383	        if self.controller.take().is_some() {
384	            self.stamp_driven_by();
385	        }
386	    }
387
388	    /// Deadline-evict path (W1): the drain observed a fully-stalled controller and
389	    /// timed out its `send_timeout`. Detach it — but ONLY if it is still the same
390	    /// controller generation the drain saw (`epoch`), so a NEWER controller that
391	    /// attached during the stall is never unseated.
392	    fn mark_controller_gone(&mut self, epoch: u64) {
393	        if self.controller_epoch == epoch {
394	            self.clear_controller();
395	        }
396	    }
397
398	    /// Add a read-only viewer: snapshot the ring from `from_seq` into the writer
399	    /// thread (NOT through the bounded live queue — replay must not trip the
400	    /// overflow eviction), seed it the current size frame first, then run the
401	    /// live queue. The drain thread fans live frames in via `try_send`.
402	    fn add_viewer(&mut self, sub: SharedSend, from_seq: u64) {
403	        let vid = self.next_viewer_id;
404	        self.next_viewer_id += 1;
405	        // The size frame + the ring replay are the writer's initial batch (owned
406	        // by the thread, not the bounded queue).
407	        let mut initial: Vec<Envelope> =
408	            vec![size_envelope(self.session_id, self.size.0, self.size.1)];
409	        initial.extend(
410	            self.ring
411	                .iter()
412	                .filter(|(s, _)| *s >= from_seq)
413	                .map(|(seq, bytes)| output_envelope(self.session_id, *seq, bytes)),
414	        );
415	        let (tx, rx) = sync_channel::<Envelope>(VIEWER_CHANNEL_DEPTH);
416	        let writer_send = Arc::clone(&sub);
417	        let writer = thread::spawn(move || viewer_writer(writer_send, initial, rx));
418	        self.viewers.insert(
419	            vid,

// [impl->REQ-HAZARD-VIEWER-ISOLATION]
fn viewer_send_evicts<T>(r: &Result<(), TrySendError<T>>) -> bool {
    r.is_err()
}

/// One read-only viewer's isolated sink (REQ-HAZARD-VIEWER-ISOLATION). The drain
/// thread `try_send`s output frames into `tx` (a bounded queue); a dedicated
/// `writer` thread does the BLOCKING socket write, so a wedged viewer backs up
/// only its own queue — never the drain. When the queue overflows the sink is
/// dropped (the `tx` closes → the writer ends) and removed from the map.
struct ViewerSink {
    /// The bounded live-frame queue to this viewer's writer thread.
    tx: SyncSender<Envelope>,
    /// The viewer's IPC send half — kept ONLY for identity matching on detach
    /// (a dropped brain connection removes its viewer rows); never written here.
    send: SharedSend,
    /// The writer thread handle (held for ownership; it exits when `tx` drops or
    /// a socket write fails).
    _writer: JoinHandle<()>,
}

/// The controller live-frame channel depth (frames). Deeper than a viewer's
/// queue because the controller is the AUTHORITATIVE view — it is never silently
/// evicted on overflow (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST); instead the
/// drain blocks on a bounded `send_timeout` and the controller is detached only
/// after [`CONTROLLER_WRITE_DEADLINE`] of a fully-stalled socket. Generous so a
/// momentary slow consumer never trips the deadline.
const CONTROLLER_CHANNEL_DEPTH: usize = 4096;

/// How long the drain tolerates a CONTROLLER whose socket has fully stalled (its
/// channel stays full) before declaring it gone, detaching it, and resuming
/// (W1). This BOUNDS the wedge: a backed-up controller can stall live output for
/// at most this long — never forever (the v0.12.x root) — and never blocks the
/// log lock (the send happens off-lock; see [`OutputLog::append`]). A live but
/// merely slow operator drains a slot before this elapses and is untouched.
const CONTROLLER_WRITE_DEADLINE: Duration = Duration::from_secs(5);

/// Poll interval for the controller `try_send` deadline loop (std `SyncSender`
/// has no stable `send_timeout`). Only used while the channel is FULL — the
/// common (has-room) path never sleeps.
const CONTROLLER_SEND_POLL: Duration = Duration::from_millis(5);

/// MANDATORY bound on one injected keystroke sequence (W2 Layer C, ADR-0022 /
/// doyle non-negotiable = W1's "never park forever"): the inject worker holds the
/// floor — buffering operator input — only while a binary's response to one
/// `{type:event}` is in flight. If no `{commit}` arrives within this window the
/// worker FLUSHES the buffer, RELEASES the floor, FAULTS the binary (terminate, no
/// respawn → the session degrades to raw inject), and never re-opens. A wedged or
/// buggy translation binary can therefore stall operator input for at most this
/// long. Mirrors [`CONTROLLER_WRITE_DEADLINE`]'s order (5 s).
const INJECT_COMMIT_DEADLINE: Duration = Duration::from_secs(5);

/// The effective commit deadline. Production is the const above; the int gate
/// (Layer G no-commit FAULT test) shrinks it via `SPT_INJECT_COMMIT_DEADLINE_MS`
/// so the structural outcome (floor releases → buffered input flushes → binary
/// faults) is asserted in well under a second rather than a 5 s wall-clock wait —
/// the deadline's PRESENCE is the invariant, not its exact length.
fn inject_commit_deadline() -> Duration {
    match std::env::var("SPT_INJECT_COMMIT_DEADLINE_MS") {
        Ok(ms) => ms
            .parse::<u64>()
            .map(Duration::from_millis)
            .unwrap_or(INJECT_COMMIT_DEADLINE),
        Err(_) => INJECT_COMMIT_DEADLINE,
    }
}

/// The interactive CONTROLLER's isolated sink (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST).
/// Symmetric with [`ViewerSink`] — a dedicated writer thread does the BLOCKING
/// socket write so a backed-up controller can never stall the session drain
/// thread (the v0.12.x wedge: the controller write was inline on the drain,
/// holding `Mutex<OutputLog>`). Unlike a viewer it is AUTHORITATIVE: its writer
/// advances `delivered_through` on each successful write, and it is never evicted
/// on a single full `try_send` — only after [`CONTROLLER_WRITE_DEADLINE`].
struct ControllerSink {
    /// The bounded live-frame queue to the controller's writer thread.
    tx: SyncSender<CtrlMsg>,
    /// The controller's IPC send half — kept for identity matching on detach and
    /// for the `exit`-frame fan-out ([`OutputLog::all_sinks`]).
    send: SharedSend,
    /// The controller's identity: `Some(node)` = a remote operator, `None` =
    /// local (the brain's spawn-reader / resume). Ownership keys on this.
    by: Option<String>,
    /// The writer thread handle (held for ownership; exits when `tx` drops — the
    /// drain's bounded `send_timeout` then sees `Disconnected` and detaches — or
    /// when a socket write fails).
    _writer: JoinHandle<()>,
}

/// A bounded, sequenced output log for one session — the gapless-resume buffer
/// AND the W2.5 controller/viewer fan-out hub (REQ-RCVIEW-1).
///
/// The session's single drain thread is the only producer ([`OutputLog::append`]).
/// Output fans out to ONE interactive `controller` (the authoritative,
/// cursor-advancing, blocking path) plus ANY NUMBER of read-only `viewers` (each
/// an isolated bounded queue + writer thread, evicted on overflow). Every
/// controller send and viewer `try_send` happens while the enclosing
/// `Mutex<OutputLog>` is held, so frames reach each sink in strict `seq` order.
struct OutputLog {
    session_id: u64,
    next_seq: u64,
    ring: VecDeque<(u64, Vec<u8>)>,
    cap_chunks: usize,
    /// The single interactive controller's isolated sink (W1: a dedicated writer
    /// thread + bounded channel + authoritative cursor). `None` ⇒ undriven. The
    /// controller's identity (`Some(node)` remote / `None` local) lives in
    /// [`ControllerSink::by`] and keys ownership — a same-`by` re-subscribe
    /// silently re-takes (no `Displaced`, the gate-#7 self-kick guard); only a
    /// DIFFERENT remote's `Take` displaces.
    controller: Option<ControllerSink>,
    /// Monotonic controller generation. Bumped on every [`become_controller`];
    /// the drain's deadline-evict ([`mark_controller_gone`]) carries the epoch it
    /// observed and only detaches if it still matches — so a slow-controller
    /// timeout can never unseat a NEWER controller that attached meanwhile.
    controller_epoch: u64,
    /// The read-only viewers, keyed by a broker-minted viewer id.
    viewers: HashMap<u64, ViewerSink>,
    /// Monotonic viewer-id source.
    next_viewer_id: u64,
    /// The owning endpoint (the spawn's label; empty = unlabeled / raw test).
    /// The broker is the SINGLE writer of `driven_by` + `viewer_count` on this
    /// perch (REQ-RCVIEW-1, resolves the clear-race); empty ⇒ nowhere to stamp.
    endpoint: String,
    /// Last-known PTY size — seeded at spawn, updated on a controller resize.
    /// Pushed to a viewer on attach + to all viewers on resize (the letterbox
    /// feed); the broker holds it since the PTY exposes no size getter.
    size: (u16, u16),
    /// Cursor-of-record (restoration D4-1, ADR-0018 Q6): the next seq a
    /// re-attaching CONTROLLER still needs (= highest seq **successfully written**
    /// to the controller, +1). It is **log** state, not subscriber state, so it
    /// **survives `detach_if`** — a dead brain leaves its resume point behind, and
    /// a cold-starting brain reads it (via `KIND_SESSIONS` → `resume_seq`) instead
    /// of replaying the whole ring from 0. **Viewer writes NEVER move it**
    /// (REQ-RCVIEW-1: the brain-resume cursor tracks the controller only).
    ///
    /// **Advanced only on a successful socket write** (`Ok`), and **monotonic**
    /// (never decreases) — see [`advanced_cursor`]. Both invariants are
    /// load-bearing for at-least-once output: advancing past a *failed* write to a
    /// dying-but-not-yet-detached controller would make resume skip that chunk
    /// forever (at-least-once → at-most-once); decreasing on a deliberate
    /// `attach(sub, 0)` rewind-replay would reset the resume point under a live
    /// reader.
    ///
    /// W1: now an `Arc<AtomicU64>` shared with the controller's writer thread,
    /// which advances it (via [`advanced_cursor`], CAS-monotonic) as it writes —
    /// the cursor moved off the drain thread with the controller delivery itself.
    delivered_through: Arc<AtomicU64>,
}

/// The post-write resume cursor (pure — the D4-1 unit kernel, mirroring
/// `brainproc::consumer_gate`/`next_backoff`): advance to `seq + 1` **only** on a
/// successful write, and **monotonically** (never below `current`). A failed
/// write (`write_ok == false`) leaves the cursor untouched, and a re-send of an
/// already-delivered `seq` (an `attach(sub, 0)` rewind, `seq + 1 <= current`)
/// cannot lower it. This is the exact decision the `let _ = write_frame(...)`
/// swallow used to drop (doyle D4-1 amendments 1–3).
// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]
fn advanced_cursor(current: u64, seq: u64, write_ok: bool) -> u64 {
    if write_ok {
        current.max(seq + 1)
    } else {
        current
    }
}

impl OutputLog {
    fn new(session_id: u64, cap_chunks: usize, endpoint: String, size: (u16, u16)) -> Self {
        OutputLog {
            session_id,
            next_seq: 0,
            ring: VecDeque::new(),
            cap_chunks,
            controller: None,
            controller_epoch: 0,
            viewers: HashMap::new(),
            next_viewer_id: 0,
            endpoint,
            size,
            delivered_through: Arc::new(AtomicU64::new(0)),
        }
    }

    /// Append a freshly-produced output chunk: assign the next `seq`, retain it
    /// in the bounded ring, FAN OUT to viewers (non-blocking `try_send`, evicting
    /// any overflowed/gone queue — REQ-HAZARD-VIEWER-ISOLATION), and HAND the
    /// controller frame back to the caller as a [`ControllerJob`] to deliver
    /// **off the log lock** (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST).
    ///
    /// W1: the controller write is no longer inline here. The drain used to call
    /// `write_frame(controller)` synchronously while holding `Mutex<OutputLog>`,
    /// so a backed-up controller socket parked the drain thread WITH THE LOCK HELD
    /// — wedging the whole session (every attach / `KIND_SESSIONS` that needs the
    /// log lock blocked). Now this returns the job and releases the lock; the
    /// drain does a bounded, off-lock send into the controller's writer-thread
    /// channel ([`ControllerJob::deliver`]). Called only from the session's
    /// single drain thread.
    // [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]
    #[must_use]
    fn append(&mut self, bytes: &[u8]) -> Option<ControllerJob> {
        let seq = self.next_seq;
        self.next_seq += 1;
        self.ring.push_back((seq, bytes.to_vec()));
        while self.ring.len() > self.cap_chunks {
            self.ring.pop_front();
        }
        let frame = output_envelope(self.session_id, seq, bytes);
        // Viewers: non-blocking fan-out; a full / disconnected queue = eviction.
        if !self.viewers.is_empty() {
            let mut evicted: Vec<u64> = Vec::new();
            for (vid, sink) in self.viewers.iter() {
                if viewer_send_evicts(&sink.tx.try_send(frame.clone())) {
                    evicted.push(*vid);
                }
            }
            if !evicted.is_empty() {
                for vid in evicted {
                    self.viewers.remove(&vid);
                }
                self.stamp_viewer_count();
            }
        }
        // Controller: hand the frame to the caller for an off-lock, bounded send
        // into the controller writer thread's channel (which does the blocking
        // socket write + advances the authoritative cursor). The drain never
        // blocks under the log lock again.
        self.controller.as_ref().map(|c| ControllerJob {
            tx: c.tx.clone(),
            seq,
            frame,
            epoch: self.controller_epoch,
        })
    }

    /// The current controller's identity (`Some(node)` remote / `None` local),
    /// or `None` when undriven. The `by`-keyed ownership logic in
    /// [`resolve_subscribe`] reads this.
    fn controller_by(&self) -> Option<&str> {
        self.controller.as_ref().and_then(|c| c.by.as_deref())
    }

    /// Become (or silently re-take) the interactive controller from `from_seq`.
    ///
    /// W1: the ring replay is now the INITIAL BATCH of a dedicated controller
    /// writer thread (symmetric with [`add_viewer`]) — it is no longer written
    /// inline under the log lock. The writer thread advances the authoritative
    /// `delivered_through` cursor as it writes (D4-1: only on a successful write,
    /// monotonic, never the unsent tail). Dropping any prior [`ControllerSink`]
    /// ends its writer; bumping `controller_epoch` makes a stale deadline-evict a
    /// no-op. Stamps `driven_by` to the new controller's identity.
    fn become_controller(&mut self, sub: SharedSend, by: Option<String>, from_seq: u64) {
        // Drop the prior controller sink first (its writer ends when tx drops),
        // and bump the generation so an in-flight deadline-evict for the old
        // controller can't unseat this one.
        self.controller = None;
        self.controller_epoch += 1;
        // Ring replay = the writer thread's initial batch (owned by the thread,
        // NOT pushed through the live channel — replay must never block the drain).
        let initial: Vec<(u64, Vec<u8>)> = self
            .ring
            .iter()
            .filter(|(s, _)| *s >= from_seq)
            .cloned()
            .collect();
        let (tx, rx) = sync_channel::<CtrlMsg>(CONTROLLER_CHANNEL_DEPTH);
        let writer_send = Arc::clone(&sub);
        let dt = Arc::clone(&self.delivered_through);
        let sid = self.session_id;
        let writer = thread::spawn(move || controller_writer(sid, writer_send, initial, rx, dt));
        self.controller = Some(ControllerSink {
            tx,
            send: sub,
            by,
            _writer: writer,
        });
        self.stamp_driven_by();
    }

    /// Detach the controller and UNLATCH the perch — the ONE unlatch path shared
    /// by W1's deadline-evict ([`mark_controller_gone`]), [`detach_if`], and (per
    /// the W1↔W5 synergy) W5's reconcile self-heal. Dropping the sink ends its
    /// writer thread; clearing the slot re-stamps `driven_by` to `None` (so an
    /// `ONLINE+CONTROLLED` latch cannot outlive a gone controller).
    fn clear_controller(&mut self) {
        if self.controller.take().is_some() {
            self.stamp_driven_by();
        }
    }

    /// Deadline-evict path (W1): the drain observed a fully-stalled controller and
    /// timed out its `send_timeout`. Detach it — but ONLY if it is still the same
    /// controller generation the drain saw (`epoch`), so a NEWER controller that
    /// attached during the stall is never unseated.
    fn mark_controller_gone(&mut self, epoch: u64) {
        if self.controller_epoch == epoch {
            self.clear_controller();
        }
    }

    /// Add a read-only viewer: snapshot the ring from `from_seq` into the writer
    /// thread (NOT through the bounded live queue — replay must not trip the
    /// overflow eviction), seed it the current size frame first, then run the
    /// live queue. The drain thread fans live frames in via `try_send`.
    fn add_viewer(&mut self, sub: SharedSend, from_seq: u64) {
        let vid = self.next_viewer_id;
        self.next_viewer_id += 1;
        // The size frame + the ring replay are the writer's initial batch (owned
        // by the thread, not the bounded queue).
        let mut initial: Vec<Envelope> =
            vec![size_envelope(self.session_id, self.size.0, self.size.1)];
        initial.extend(
            self.ring
                .iter()
                .filter(|(s, _)| *s >= from_seq)
                .map(|(seq, bytes)| output_envelope(self.session_id, *seq, bytes)),
        );
        let (tx, rx) = sync_channel::<Envelope>(VIEWER_CHANNEL_DEPTH);
        let writer_send = Arc::clone(&sub);
        let writer = thread::spawn(move || viewer_writer(writer_send, initial, rx));
        self.viewers.insert(
            vid,


555
556	    /// Stamp the perch's `driven_by` to the current controller's identity (the
557	    /// broker is the single writer — resolves the clear-race). The remote-drive
558	    /// detection fact (REQ-REACH-1) moved here from `serve_attach` so a displaced
559	    /// operator's teardown can never wipe the new controller's marker.
560	    /// Best-effort: a session served without a perch (raw broker tests) has
561	    /// nowhere to record.
562	    // [impl->REQ-RCVIEW-1] [impl->REQ-REACH-1]
563	    fn stamp_driven_by(&self) {
564	        if self.endpoint.is_empty() {
565	            return;
566	        }
567	        let perch = resolve_perch_path(&self.endpoint, ParentHint::Infer);
568	        let _ = spt_store::info::set_driven_by(&perch, self.controller_by());
569	    }
570
571	    /// Stamp the perch's `viewer_count` to the live viewer count (broker is the
572	    /// single writer). Best-effort, like [`OutputLog::stamp_driven_by`].
573	    // [impl->REQ-RCVIEW-1]
574	    fn stamp_viewer_count(&self) {
575	        if self.endpoint.is_empty() {
576	            return;
577	        }
578	        let perch = resolve_perch_path(&self.endpoint, ParentHint::Infer);
579	        let _ = spt_store::info::set_viewer_count(&perch, self.viewers.len() as u32);
580	    }
581
582	    /// Whether `sub` is the current controller (the resize-exclusivity check).
583	    fn is_controller(&self, sub: &SharedSend) -> bool {
584	        self.controller
585	            .as_ref()
586	            .is_some_and(|c| Arc::ptr_eq(&c.send, sub))
587	    }
588
589	    /// The next seq a re-attaching controller still needs (the persisted resume
590	    /// cursor surfaced in `KIND_SESSIONS` → `resume_seq`). W1: read from the
591	    /// atomic the controller writer thread advances.
592	    fn delivered_through(&self) -> u64 {
593	        self.delivered_through.load(Ordering::Acquire)
594	    }
595	}
596
597	/// A viewer's dedicated writer thread (REQ-HAZARD-VIEWER-ISOLATION): write the
598	/// `initial` batch (size frame + ring replay) once, then drain live frames from
599	/// `rx` with a BLOCKING socket write each. A failed write (dead viewer) ends the
600	/// thread; the drain thread's next `try_send` then sees `Disconnected` and
601	/// evicts the row. The drain thread itself never blocks on this socket.
602	fn viewer_writer(send: SharedSend, initial: Vec<Envelope>, rx: Receiver<Envelope>) {
603	    for frame in &initial {
604	        let ok = match send.lock() {
605	            Ok(mut s) => write_frame(&mut *s, frame).is_ok(),
606	            Err(_) => false,
607	        };
608	        if !ok {
609	            return;
610	        }
611	    }
612	    while let Ok(frame) = rx.recv() {
613	        let ok = match send.lock() {
614	            Ok(mut s) => write_frame(&mut *s, &frame).is_ok(),
615	            Err(_) => false,
616	        };
617	        if !ok {
618	            return;
619	        }
620	    }
621	}
622
623	/// A message to the CONTROLLER writer thread (W1). `Output` carries a sequenced
624	/// chunk and advances the authoritative `delivered_through` on a successful
625	/// write; `Control` is an out-of-band notice (Displaced, …) that is written but
626	/// does NOT move the resume cursor.
627	enum CtrlMsg {
628	    Output(u64, Envelope),
629	    Control(Envelope),
630	}
631
632	/// One live-output delivery to the controller, handed back by
633	/// [`OutputLog::append`] so the drain can do the bounded send OFF the log lock
634	/// (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST). [`ControllerJob::deliver`] blocks at
635	/// most [`CONTROLLER_WRITE_DEADLINE`]; on a fully-stalled controller it returns
636	/// the `epoch` so the drain can detach exactly that generation.
637	struct ControllerJob {
638	    tx: SyncSender<CtrlMsg>,
639	    seq: u64,
640	    frame: Envelope,
641	    epoch: u64,
642	}
643
644	impl ControllerJob {
645	    /// Deliver into the controller's writer-thread channel with a bounded
646	    /// deadline. `Ok(())` = queued (the writer does the blocking socket write);
647	    /// `Err(epoch)` = the controller is fully stalled or gone and must be
648	    /// detached (the wedge is thereby BOUNDED, never permanent — the v0.12.x
649	    /// root). A live-but-slow controller drains a slot before the deadline and is
650	    /// untouched.
651	    ///
652	    /// A `try_send` poll loop (std `SyncSender` has no stable `send_timeout`): the
653	    /// common case (channel has room) succeeds on the first `try_send` with zero
654	    /// added latency; only a FULL channel polls every [`CONTROLLER_SEND_POLL`] up
655	    /// to [`CONTROLLER_WRITE_DEADLINE`]. This runs OFF the log lock (in the drain
656	    /// closure), so a stalled controller never holds the lock.
657	    fn deliver(self) -> Result<(), u64> {
658	        let mut msg = CtrlMsg::Output(self.seq, self.frame);
659	        let deadline = Instant::now() + CONTROLLER_WRITE_DEADLINE;
660	        loop {
661	            match self.tx.try_send(msg) {
662	                Ok(()) => return Ok(()),
663	                Err(TrySendError::Disconnected(_)) => return Err(self.epoch),
664	                Err(TrySendError::Full(returned)) => {

    /// Stamp the perch's `driven_by` to the current controller's identity (the
    /// broker is the single writer — resolves the clear-race). The remote-drive
    /// detection fact (REQ-REACH-1) moved here from `serve_attach` so a displaced
    /// operator's teardown can never wipe the new controller's marker.
    /// Best-effort: a session served without a perch (raw broker tests) has
    /// nowhere to record.
    // [impl->REQ-RCVIEW-1] [impl->REQ-REACH-1]
    fn stamp_driven_by(&self) {
        if self.endpoint.is_empty() {
            return;
        }
        let perch = resolve_perch_path(&self.endpoint, ParentHint::Infer);
        let _ = spt_store::info::set_driven_by(&perch, self.controller_by());
    }

    /// Stamp the perch's `viewer_count` to the live viewer count (broker is the
    /// single writer). Best-effort, like [`OutputLog::stamp_driven_by`].
    // [impl->REQ-RCVIEW-1]
    fn stamp_viewer_count(&self) {
        if self.endpoint.is_empty() {
            return;
        }
        let perch = resolve_perch_path(&self.endpoint, ParentHint::Infer);
        let _ = spt_store::info::set_viewer_count(&perch, self.viewers.len() as u32);
    }

    /// Whether `sub` is the current controller (the resize-exclusivity check).
    fn is_controller(&self, sub: &SharedSend) -> bool {
        self.controller
            .as_ref()
            .is_some_and(|c| Arc::ptr_eq(&c.send, sub))
    }

    /// The next seq a re-attaching controller still needs (the persisted resume
    /// cursor surfaced in `KIND_SESSIONS` → `resume_seq`). W1: read from the
    /// atomic the controller writer thread advances.
    fn delivered_through(&self) -> u64 {
        self.delivered_through.load(Ordering::Acquire)
    }
}

/// A viewer's dedicated writer thread (REQ-HAZARD-VIEWER-ISOLATION): write the
/// `initial` batch (size frame + ring replay) once, then drain live frames from
/// `rx` with a BLOCKING socket write each. A failed write (dead viewer) ends the
/// thread; the drain thread's next `try_send` then sees `Disconnected` and
/// evicts the row. The drain thread itself never blocks on this socket.
fn viewer_writer(send: SharedSend, initial: Vec<Envelope>, rx: Receiver<Envelope>) {
    for frame in &initial {
        let ok = match send.lock() {
            Ok(mut s) => write_frame(&mut *s, frame).is_ok(),
            Err(_) => false,
        };
        if !ok {
            return;
        }
    }
    while let Ok(frame) = rx.recv() {
        let ok = match send.lock() {
            Ok(mut s) => write_frame(&mut *s, &frame).is_ok(),
            Err(_) => false,
        };
        if !ok {
            return;
        }
    }
}

/// A message to the CONTROLLER writer thread (W1). `Output` carries a sequenced
/// chunk and advances the authoritative `delivered_through` on a successful
/// write; `Control` is an out-of-band notice (Displaced, …) that is written but
/// does NOT move the resume cursor.
enum CtrlMsg {
    Output(u64, Envelope),
    Control(Envelope),
}

/// One live-output delivery to the controller, handed back by
/// [`OutputLog::append`] so the drain can do the bounded send OFF the log lock
/// (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST). [`ControllerJob::deliver`] blocks at
/// most [`CONTROLLER_WRITE_DEADLINE`]; on a fully-stalled controller it returns
/// the `epoch` so the drain can detach exactly that generation.
struct ControllerJob {
    tx: SyncSender<CtrlMsg>,
    seq: u64,
    frame: Envelope,
    epoch: u64,
}

impl ControllerJob {
    /// Deliver into the controller's writer-thread channel with a bounded
    /// deadline. `Ok(())` = queued (the writer does the blocking socket write);
    /// `Err(epoch)` = the controller is fully stalled or gone and must be
    /// detached (the wedge is thereby BOUNDED, never permanent — the v0.12.x
    /// root). A live-but-slow controller drains a slot before the deadline and is
    /// untouched.
    ///
    /// A `try_send` poll loop (std `SyncSender` has no stable `send_timeout`): the
    /// common case (channel has room) succeeds on the first `try_send` with zero
    /// added latency; only a FULL channel polls every [`CONTROLLER_SEND_POLL`] up
    /// to [`CONTROLLER_WRITE_DEADLINE`]. This runs OFF the log lock (in the drain
    /// closure), so a stalled controller never holds the lock.
    fn deliver(self) -> Result<(), u64> {
        let mut msg = CtrlMsg::Output(self.seq, self.frame);
        let deadline = Instant::now() + CONTROLLER_WRITE_DEADLINE;
        loop {
            match self.tx.try_send(msg) {
                Ok(()) => return Ok(()),
                Err(TrySendError::Disconnected(_)) => return Err(self.epoch),
                Err(TrySendError::Full(returned)) => {


1459	    fn dispatch_input(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {
1460	        let req: InputReq =
1461	            serde_json::from_value(env.payload).map_err(|e| format!("bad input payload: {e}"))?;
1462	        let bytes = decode_bytes(&req.data_b64).map_err(|e| format!("bad input base64: {e}"))?;
1463	        // Clone the session Arc (and the translation handle, if any) out from under
1464	        // the sessions lock so the PTY write + fsync happen without holding the
1465	        // table lock (mirrors dispatch_subscribe).
1466	        let (session, translation) = {
1467	            let sessions = self.sessions.lock().unwrap();
1468	            let h = sessions
1469	                .get(&req.session_id)
1470	                .ok_or_else(|| format!("no such session {}", req.session_id))?;
1471	            (Arc::clone(&h.session), h.translation.clone())
1472	        };
1473	        // Layer C: when a (non-faulted) translation binary is attached, fire a
1474	        // CONTENT-FREE `{type:input}` ping so the binary can track operator idleness
1475	        // — best-effort, never carries the keystroke bytes (doyle gate #1, privacy).
1476	        if let Some(tr) = &translation {
1477	            if !tr.faulted.load(Ordering::Relaxed) {
1478	                let _ = tr.child.send(&ToBinary::Input);
1479	            }
1480	        }
1481	        // Layer C: if an injected sequence is mid-flight, the operator's keystrokes
1482	        // BUFFER in the inject floor and are flushed to the PTY after the sequence
1483	        // commits — so a stash/restore choreography can't be clobbered. Returns
1484	        // true when buffered (the caller must NOT write the PTY now).
1485	        let buffered = |bytes: &[u8]| -> bool {
1486	            translation
1487	                .as_ref()
1488	                .is_some_and(|tr| tr.floor.lock().unwrap().buffer_if_held(bytes))
1489	        };
1490	        match req.op_id {
1491	            Some(op) => {
1492	                let key = (req.session_id, op);
1493	                let outcome = self
1494	                    .journal
1495	                    .apply_once(key, EffectKind::PtyWrite, || {
1496	                        // Buffer-or-write INSIDE the journal effect so the op is still
1497	                        // recorded applied + acked (dedup-safe) whether the byte lands
1498	                        // on the PTY now or after the sequence flush.
1499	                        if buffered(&bytes) {
1500	                            return Ok(());
1501	                        }
1502	                        session
1503	                            .write_input(&bytes)
1504	                            .map_err(|e| io::Error::other(e.to_string()))
1505	                    })
1506	                    .map_err(|e| format!("write_input failed: {e}"))?;
1507	                // Ack exactly-once: both Applied and Deduped mean "durably landed".
1508	                send_frame(
1509	                    send,
1510	                    &applied_envelope(req.session_id, op, outcome == Outcome::Applied),
1511	                );
1512	                Ok(())
1513	            }
1514	            None => {
1515	                if buffered(&bytes) {
1516	                    return Ok(());
1517	                }
1518	                session
1519	                    .write_input(&bytes)
1520	                    .map_err(|e| format!("write_input failed: {e}"))
1521	            }
1522	        }
1523	    }
1524
1525	    /// Deliver an inbound message to an spt-hosted endpoint by ENDPOINT ID:
1526	    /// resolve the endpoint to its hosted session and inject the bytes straight
1527	    /// into that session's PTY (REQ-SEND-SPT-HOSTED). The endpoint-keyed
1528	    /// counterpart of [`dispatch_input`] — a CLI `spt send` to an spt-hosted
1529	    /// endpoint (broker holds its PTY, no `api listen` relay) lands here instead
1530	    /// of spooling silently. Resolution is atomic under the sessions lock (no
1531	    /// TOCTOU); the bytes are a pre-rendered `<EVENT>` envelope (the caller owns
1532	    /// the wire format — ADR-0020). Fire-and-forget: a direct write, no journal
1533	    /// (a user message is delivered like the relay's emit, not an exactly-once
1534	    /// effect). Replies [`KIND_ENDPOINT_INJECTED`] with `delivered` so the caller
1535	    /// knows whether to fall back to the spool.
1536	    ///
1537	    /// IDLE/DIRECT path only (REQ-SEND-SPT-HOSTED, MVP per CONTEXT:188): a write
1538	    /// arriving mid-agent-turn can interleave with the agent's active output —
1539	    /// activity-gated routing (idle→inject / active→spool-for-poll-drain) is a
1540	    /// deferred follow wave. The write is logged so an active-state inject is
1541	    /// never silent.
1542	    // [impl->REQ-SEND-SPT-HOSTED]
1543	    fn dispatch_endpoint_input(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {
1544	        let req: EndpointInputReq = serde_json::from_value(env.payload)
1545	            .map_err(|e| format!("bad endpoint-input payload: {e}"))?;
1546	        let bytes =
1547	            decode_bytes(&req.data_b64).map_err(|e| format!("bad endpoint-input base64: {e}"))?;
1548	        // Resolve endpoint → (session, translation) under the lock, cloning the
1549	        // Arcs out so the PTY write happens without holding the table lock
1550	        // (dispatch_input rule).
1551	        let resolved = {
1552	            let sessions = self.sessions.lock().unwrap();
1553	            sessions
1554	                .values()
1555	                .find(|h| h.endpoint == req.endpoint)
1556	                .map(|h| (Arc::clone(&h.session), h.translation.clone()))
1557	        };
1558	        match resolved {
1559	            Some((session, translation)) => {
1560	                // Layer E: a live (non-faulted) translation binary OWNS the
1561	                // choreography — queue the whole `<EVENT>` envelope to the inject
1562	                // worker, which drives it through the binary and applies the
1563	                // resulting keystrokes atomically w.r.t. the controller. Raw inject
1564	                // is the DEGENERATE fallback when no binary is attached, it has
1565	                // faulted, or the worker channel is gone.
1566	                if let Some(tr) = &translation {
1567	                    if !tr.faulted.load(Ordering::Relaxed) {
1568	                        let envelope = String::from_utf8_lossy(&bytes).into_owned();
1569	                        if tr.event_tx.send(envelope).is_ok() {
1570	                            eprintln!(
1571	                                "ENDPOINT_INJECT:{} ({} bytes → translation binary)",
1572	                                req.endpoint,
1573	                                bytes.len()
1574	                            );
1575	                            send_frame(send, &endpoint_injected_envelope(&req.endpoint, true));
1576	                            return Ok(());
1577	                        }
1578	                        // worker receiver dropped → fall through to raw inject.
1579	                    }
1580	                }
1581	                // IDLE/direct raw inject. Logged (REQ-SEND-SPT-HOSTED MVP): activity
1582	                // is not yet gated, so note the raw write — never a silent mid-turn
1583	                // interleave passed off as done.
1584	                eprintln!(
1585	                    "ENDPOINT_INJECT:{} ({} bytes, idle-direct — activity not yet gated)",
1586	                    req.endpoint,
1587	                    bytes.len()
1588	                );
1589	                session
1590	                    .write_input(&bytes)
1591	                    .map_err(|e| format!("write_input failed: {e}"))?;
1592	                send_frame(send, &endpoint_injected_envelope(&req.endpoint, true));
1593	                Ok(())
1594	            }
1595	            // No hosted session for this endpoint — tell the caller to spool.
1596	            None => {
1597	                send_frame(send, &endpoint_injected_envelope(&req.endpoint, false));
1598	                Ok(())
1599	            }
1600	        }
1601	    }
1602
1603	    /// Resize a session's PTY — CONTROLLER-EXCLUSIVE (REQ-RCVIEW-1). The broker
1604	    /// rejects a resize arriving on any stream that is not the session's current
1605	    /// controller (a viewer's resize is refused), then updates the stored size
1606	    /// and pushes a `size` frame to every viewer (the letterbox feed).
1607	    fn dispatch_resize(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {
1608	        let req: ResizeReq =
1609	            serde_json::from_value(env.payload).map_err(|e| format!("bad resize payload: {e}"))?;
1610	        let (session, log) = {
1611	            let sessions = self.sessions.lock().unwrap();
1612	            let h = sessions
1613	                .get(&req.session_id)
1614	                .ok_or_else(|| format!("no such session {}", req.session_id))?;
1615	            (Arc::clone(&h.session), Arc::clone(&h.log))
1616	        };
1617	        if !log.lock().unwrap().is_controller(send) {
1618	            return Err(

    fn dispatch_input(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {
        let req: InputReq =
            serde_json::from_value(env.payload).map_err(|e| format!("bad input payload: {e}"))?;
        let bytes = decode_bytes(&req.data_b64).map_err(|e| format!("bad input base64: {e}"))?;
        // Clone the session Arc (and the translation handle, if any) out from under
        // the sessions lock so the PTY write + fsync happen without holding the
        // table lock (mirrors dispatch_subscribe).
        let (session, translation) = {
            let sessions = self.sessions.lock().unwrap();
            let h = sessions
                .get(&req.session_id)
                .ok_or_else(|| format!("no such session {}", req.session_id))?;
            (Arc::clone(&h.session), h.translation.clone())
        };
        // Layer C: when a (non-faulted) translation binary is attached, fire a
        // CONTENT-FREE `{type:input}` ping so the binary can track operator idleness
        // — best-effort, never carries the keystroke bytes (doyle gate #1, privacy).
        if let Some(tr) = &translation {
            if !tr.faulted.load(Ordering::Relaxed) {
                let _ = tr.child.send(&ToBinary::Input);
            }
        }
        // Layer C: if an injected sequence is mid-flight, the operator's keystrokes
        // BUFFER in the inject floor and are flushed to the PTY after the sequence
        // commits — so a stash/restore choreography can't be clobbered. Returns
        // true when buffered (the caller must NOT write the PTY now).
        let buffered = |bytes: &[u8]| -> bool {
            translation
                .as_ref()
                .is_some_and(|tr| tr.floor.lock().unwrap().buffer_if_held(bytes))
        };
        match req.op_id {
            Some(op) => {
                let key = (req.session_id, op);
                let outcome = self
                    .journal
                    .apply_once(key, EffectKind::PtyWrite, || {
                        // Buffer-or-write INSIDE the journal effect so the op is still
                        // recorded applied + acked (dedup-safe) whether the byte lands
                        // on the PTY now or after the sequence flush.
                        if buffered(&bytes) {
                            return Ok(());
                        }
                        session
                            .write_input(&bytes)
                            .map_err(|e| io::Error::other(e.to_string()))
                    })
                    .map_err(|e| format!("write_input failed: {e}"))?;
                // Ack exactly-once: both Applied and Deduped mean "durably landed".
                send_frame(
                    send,
                    &applied_envelope(req.session_id, op, outcome == Outcome::Applied),
                );
                Ok(())
            }
            None => {
                if buffered(&bytes) {
                    return Ok(());
                }
                session
                    .write_input(&bytes)
                    .map_err(|e| format!("write_input failed: {e}"))
            }
        }
    }

    /// Deliver an inbound message to an spt-hosted endpoint by ENDPOINT ID:
    /// resolve the endpoint to its hosted session and inject the bytes straight
    /// into that session's PTY (REQ-SEND-SPT-HOSTED). The endpoint-keyed
    /// counterpart of [`dispatch_input`] — a CLI `spt send` to an spt-hosted
    /// endpoint (broker holds its PTY, no `api listen` relay) lands here instead
    /// of spooling silently. Resolution is atomic under the sessions lock (no
    /// TOCTOU); the bytes are a pre-rendered `<EVENT>` envelope (the caller owns
    /// the wire format — ADR-0020). Fire-and-forget: a direct write, no journal
    /// (a user message is delivered like the relay's emit, not an exactly-once
    /// effect). Replies [`KIND_ENDPOINT_INJECTED`] with `delivered` so the caller
    /// knows whether to fall back to the spool.
    ///
    /// IDLE/DIRECT path only (REQ-SEND-SPT-HOSTED, MVP per CONTEXT:188): a write
    /// arriving mid-agent-turn can interleave with the agent's active output —
    /// activity-gated routing (idle→inject / active→spool-for-poll-drain) is a
    /// deferred follow wave. The write is logged so an active-state inject is
    /// never silent.
    // [impl->REQ-SEND-SPT-HOSTED]
    fn dispatch_endpoint_input(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {
        let req: EndpointInputReq = serde_json::from_value(env.payload)
            .map_err(|e| format!("bad endpoint-input payload: {e}"))?;
        let bytes =
            decode_bytes(&req.data_b64).map_err(|e| format!("bad endpoint-input base64: {e}"))?;
        // Resolve endpoint → (session, translation) under the lock, cloning the
        // Arcs out so the PTY write happens without holding the table lock
        // (dispatch_input rule).
        let resolved = {
            let sessions = self.sessions.lock().unwrap();
            sessions
                .values()
                .find(|h| h.endpoint == req.endpoint)
                .map(|h| (Arc::clone(&h.session), h.translation.clone()))
        };
        match resolved {
            Some((session, translation)) => {
                // Layer E: a live (non-faulted) translation binary OWNS the
                // choreography — queue the whole `<EVENT>` envelope to the inject
                // worker, which drives it through the binary and applies the
                // resulting keystrokes atomically w.r.t. the controller. Raw inject
                // is the DEGENERATE fallback when no binary is attached, it has
                // faulted, or the worker channel is gone.
                if let Some(tr) = &translation {
                    if !tr.faulted.load(Ordering::Relaxed) {
                        let envelope = String::from_utf8_lossy(&bytes).into_owned();
                        if tr.event_tx.send(envelope).is_ok() {
                            eprintln!(
                                "ENDPOINT_INJECT:{} ({} bytes → translation binary)",
                                req.endpoint,
                                bytes.len()
                            );
                            send_frame(send, &endpoint_injected_envelope(&req.endpoint, true));
                            return Ok(());
                        }
                        // worker receiver dropped → fall through to raw inject.
                    }
                }
                // IDLE/direct raw inject. Logged (REQ-SEND-SPT-HOSTED MVP): activity
                // is not yet gated, so note the raw write — never a silent mid-turn
                // interleave passed off as done.
                eprintln!(
                    "ENDPOINT_INJECT:{} ({} bytes, idle-direct — activity not yet gated)",
                    req.endpoint,
                    bytes.len()
                );
                session
                    .write_input(&bytes)
                    .map_err(|e| format!("write_input failed: {e}"))?;
                send_frame(send, &endpoint_injected_envelope(&req.endpoint, true));
                Ok(())
            }
            // No hosted session for this endpoint — tell the caller to spool.
            None => {
                send_frame(send, &endpoint_injected_envelope(&req.endpoint, false));
                Ok(())
            }
        }
    }

    /// Resize a session's PTY — CONTROLLER-EXCLUSIVE (REQ-RCVIEW-1). The broker
    /// rejects a resize arriving on any stream that is not the session's current
    /// controller (a viewer's resize is refused), then updates the stored size
    /// and pushes a `size` frame to every viewer (the letterbox feed).
    fn dispatch_resize(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {
        let req: ResizeReq =
            serde_json::from_value(env.payload).map_err(|e| format!("bad resize payload: {e}"))?;
        let (session, log) = {
            let sessions = self.sessions.lock().unwrap();
            let h = sessions
                .get(&req.session_id)
                .ok_or_else(|| format!("no such session {}", req.session_id))?;
            (Arc::clone(&h.session), Arc::clone(&h.log))
        };
        if !log.lock().unwrap().is_controller(send) {
            return Err(


Grep

HostedSession \{|PtySession|fn spawn|let session = |HostedSession::


<owl_messages>
<owl_message from="todlando-psyche" priority="normal"><EVENT type="echo_commune" from="todlando-psyche" timestamp="2026-06-19T19:10:24-07:00" machine="HFENDULEAM" project="spt-core" branch="v0.13.0-delivery-control" head_sha="153c9c90d03d9a23bf00f0e0ef645cc36235d2ed" head_subject="feat(session): [session.resume] native-resume role + per-row ledger cwd …" note="Echo commune brief — auto-fired">&lt;live-context&gt;
DELTA since last commune (which ended at &quot;P2 tasks 4-6 done, tests pending&quot;):

P2 (REQ-SESSION-RESUME-TEMPLATE, resume-blank ship-blocker) is now FULLY DONE and **doyle GATE-PASSED @153c9c9**. Tests (unit + int) came back green from spt-test-engineer — mutation-proven, real daemon+broker+PTY int (dummy-harness resume cmd != self, marker-file proof of template selection + recorded cwd). I independently re-ran gates: clippy=0, traceable +doc+impl+unit+int, docs-drift OK, real bin built. Committed @153c9c9, surfaced to doyle, doyle re-verified independently and gated clean (&quot;Excellent work. PROCEED TO P0&quot;).

TZ-form note: the %z numeric-offset choice for ResumeRow::title() is doyle-confirmed as the design-D acceptable fallback — LEAVE AS-IS pending operator HITL (doyle is asking the operator whether they want a short abbrev; DON'T rework unless doyle relays back).

IMMEDIATE NEXT WORK = P0 (REQ-HAZARD-PTY-INPUT-WRITER-WEDGE), the ACTIVE v0.13.0 ship-blocker — doyle said PROCEED, no changes wanted on P2. About to SELF-CLEAR to a fresh context and build P0 from the JIT plan I just wrote. P1 (REQ-RC-WIN-PASTE) is queued AFTER P0 (depends on it).

Reflection: P2 went clean end-to-end — design was code-grounded so impl was mechanical; delegating tests to spt-test-engineer (background, stayed reachable) worked well; the marker-file int design dodged PTY-output capture races by using --start detached. Mirroring the existing psyche_resume precedent meant zero novel design risk.
&lt;/live-context&gt;
&lt;project-context&gt;
CURRENT_PROJECT_CONTEXT (name=spt-core):
v0.13.0 delivery-control round-2 status (branch v0.13.0-delivery-control, HEAD @153c9c9):
- P2 REQ-SESSION-RESUME-TEMPLATE: DONE, doyle gate-passed @153c9c9. [session.resume] native-resume manifest role (Session.resume, mirrors psyche_resume) + prepare_harness_spawn(is_resume) selection w/ self fallback; SessionEntry.cwd additive serde-default (written at boundary/bind/ready appends); cmd_endpoint_run cwd fallback chain (picker row→resume info.cwd→current_dir); picker threads row cwd via Outcome::Run.cwd + ResumeRow.cwd; ResumeRow::title() local-time reformat (%z offset). Public docs MANIFEST.md + docs-site harness-contract/manifest.md teach [session.resume] (perri-buildable; perri's adapter follow-on declares `claude -r {session_id} ...` AFTER spt-core ships). Mock gained additive --marker-file for the int.
- P0 REQ-HAZARD-PTY-INPUT-WRITER-WEDGE: NEXT (active ship-blocker). REQ already SEEDED in traceable-reqs.toml — activate [&quot;impl&quot;,&quot;unit&quot;,&quot;int&quot;]. Full grounded build plan = V0.13.0-P0-JIT-PLAN.md (repo root); design = V0.13.0-P0-PTY-INPUT-WRITER-DESIGN.md. Fix: per-session input-writer thread = sole write_input caller, fed by bounded sync_channel; all callers (dispatch_input broker.rs:1459, flush_inject_floor broker.rs:787, serve_attach send_effect path) try_send + return; full queue ⇒ DROP + stamp INPUT_BACKPRESSURE. MIRROR the existing OUTPUT-side writer pattern (viewer_writer/controller_writer ~broker.rs:602/367, ViewerSink/ControllerSink). apply_once PtyWrite effect = enqueue. REPRO-FIRST int (W1b's escaped — must genuinely park write_input). Completes the W1b-deferred fix (2) of REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE.
- P1 REQ-RC-WIN-PASTE: queued after P0 (depends on it). Design V0.13.0-P1-RC-PASTE-DESIGN.md, SEEDED, activate [impl,unit] (no int — live clipboard/console mouse = HITL).
- Gate = doyle (independently re-verifies). Adapter follow-on = perri. Build pattern: per-item REQ activate → impl+tag → delegate tests to spt-test-engineer (background) → gates (clippy/traceable/docs-drift/real-bin, spt-daemon e2e needs --test-threads=1) → commit (trailer `Co-authored by: todlando`) → surface green to doyle.
&lt;/project-context&gt;</EVENT></owl_message>
</owl_messages>


crates\spt-daemon\tests\broker.rs:392:fn spawn_env_reaches_child() {
crates\spt-daemon\src\broker.rs:5://! the spawned child (both via [`spt_term::PtySession`]), and the accepted
crates\spt-daemon\src\broker.rs:43:use spt_term::{Drain, PtySession, SessionSurface, SurfaceSize};
crates\spt-daemon\src\broker.rs:766:struct HostedSession {
crates\spt-daemon\src\broker.rs:768:    session: Arc<PtySession>,
crates\spt-daemon\src\broker.rs:787:fn flush_inject_floor(floor: &Mutex<InjectFloor>, session: &PtySession) {
crates\spt-daemon\src\broker.rs:820:    session: Arc<PtySession>,
crates\spt-daemon\src\broker.rs:1248:        let session = PtySession::spawn_program_in_env(
crates\spt-daemon\src\broker.rs:1256:        let session = Arc::new(session);
crates\spt-daemon\src\broker.rs:1365:            HostedSession {
crates\spt-daemon\src\broker.rs:1403:        // (`PtySession::wait`, broker.rs exit-waiter), so it deadlocks every live


Read HostedSession construction + PtySession spawn site.