The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\translation.rs has been updated successfully. (file state is current in your context — no need to Read it back)

pub struct TranslationChild {
    child: Child,
    /// `None` after [`close_stdin`](Self::close_stdin) / drop has taken it.
    stdin: Mutex<Option<ChildStdin>>,
    _reader: JoinHandle<()>,
    pid: Option<u32>,
}

pub struct TranslationChild {
    /// `None` once reaped — behind a `Mutex` so [`terminate`](Self::terminate) is
    /// callable through a shared `Arc` (the broker shares the child between the
    /// inject worker, which kills it on FAULT, and `dispatch_input`, which fires
    /// content-free pings — Layer C).
    child: Mutex<Option<Child>>,
    /// `None` after [`close_stdin`](Self::close_stdin) / reap has taken it.
    stdin: Mutex<Option<ChildStdin>>,
    _reader: JoinHandle<()>,
    pid: Option<u32>,
}

/// forwards them on the caller-supplied channel. The broker owns the receiving

-    /// `None` after [`close_stdin`](Self::close_stdin) / drop has taken it.

+    /// `None` once reaped — behind a `Mutex` so [`terminate`](Self::terminate) is

+    /// callable through a shared `Arc` (the broker shares the child between the

+    /// inject worker, which kills it on FAULT, and `dispatch_input`, which fires

+    /// `None` after [`close_stdin`](Self::close_stdin) / reap has taken it.


Edit

        Ok(TranslationChild {
            child,
            stdin: Mutex::new(Some(stdin)),
            _reader: reader,
            pid,
        })
    }

        Ok(TranslationChild {
            child: Mutex::new(Some(child)),
            stdin: Mutex::new(Some(stdin)),
            _reader: reader,
            pid,
        })
    }


        Ok(TranslationChild {
            child,
            stdin: Mutex::new(Some(stdin)),
            _reader: reader,
            pid,
        })
    }

        Ok(TranslationChild {
            child: Mutex::new(Some(child)),
            stdin: Mutex::new(Some(stdin)),
            _reader: reader,
            pid,
        })
    }

+            child: Mutex::new(Some(child)),


    /// Drop the child's stdin handle, sending EOF — a well-behaved filter then
    /// exits on its own. Idempotent.
    fn close_stdin(&self) {
        let _ = self.stdin.lock().unwrap().take();
    }

    /// Bounded reap (doyle gate #2): close stdin (EOF), give the child
    /// [`REAP_GRACE`] to exit, then kill + `wait` it so no zombie outlives a
    /// session-down. Best-effort — every step swallows errors (a child that
    /// already exited is fine).
    fn reap(&mut self) {
        self.close_stdin();
        let deadline = Instant::now() + REAP_GRACE;
        loop {
            match self.child.try_wait() {
                Ok(Some(_)) => return, // exited on its own after EOF
                Ok(None) => {
                    if Instant::now() >= deadline {
                        break;
                    }
                    thread::sleep(REAP_POLL);
                }
                Err(_) => break,
            }
        }
        let _ = self.child.kill();
        let _ = self.child.wait();
    }
}

impl Drop for TranslationChild {
    fn drop(&mut self) {
        self.reap();
    }
}

    /// Drop the child's stdin handle, sending EOF — a well-behaved filter then
    /// exits on its own. Idempotent.
    fn close_stdin(&self) {
        let _ = self.stdin.lock().unwrap().take();
    }

    /// Bounded reap (doyle gate #2): close stdin (EOF), give the child
    /// [`REAP_GRACE`] to exit, then kill + `wait` it so no zombie outlives a
    /// session-down or a FAULT. Best-effort + idempotent — a child already reaped
    /// (`None`) returns immediately, and every step swallows errors (a child that
    /// already exited is fine). Callable through a shared `Arc` (the worker faults
    /// the binary; Drop reaps on teardown).
    // [impl->REQ-MSG-IDLE-TRANSLATION-BINARY]
    pub fn terminate(&self) {
        self.close_stdin();
        let mut guard = self.child.lock().unwrap();
        let Some(child) = guard.as_mut() else {
            return; // already reaped
        };
        let deadline = Instant::now() + REAP_GRACE;
        loop {
            match child.try_wait() {
                Ok(Some(_)) => {
                    *guard = None;
                    return; // exited on its own after EOF
                }
                Ok(None) => {
                    if Instant::now() >= deadline {
                        break;
                    }
                    thread::sleep(REAP_POLL);
                }
                Err(_) => break,
            }
        }
        let _ = child.kill();
        let _ = child.wait();
        *guard = None;
    }
}

impl Drop for TranslationChild {
    fn drop(&mut self) {
        self.terminate();
    }
}


    /// Drop the child's stdin handle, sending EOF — a well-behaved filter then
    /// exits on its own. Idempotent.
    fn close_stdin(&self) {
        let _ = self.stdin.lock().unwrap().take();
    }

    /// Bounded reap (doyle gate #2): close stdin (EOF), give the child
    /// [`REAP_GRACE`] to exit, then kill + `wait` it so no zombie outlives a
    /// session-down. Best-effort — every step swallows errors (a child that
    /// already exited is fine).
    fn reap(&mut self) {
        self.close_stdin();
        let deadline = Instant::now() + REAP_GRACE;
        loop {
            match self.child.try_wait() {
                Ok(Some(_)) => return, // exited on its own after EOF
                Ok(None) => {
                    if Instant::now() >= deadline {
                        break;
                    }
                    thread::sleep(REAP_POLL);
                }
                Err(_) => break,
            }
        }
        let _ = self.child.kill();
        let _ = self.child.wait();
    }
}

impl Drop for TranslationChild {
    fn drop(&mut self) {
        self.reap();
    }
}

    /// Drop the child's stdin handle, sending EOF — a well-behaved filter then
    /// exits on its own. Idempotent.
    fn close_stdin(&self) {
        let _ = self.stdin.lock().unwrap().take();
    }

    /// Bounded reap (doyle gate #2): close stdin (EOF), give the child
    /// [`REAP_GRACE`] to exit, then kill + `wait` it so no zombie outlives a
    /// session-down or a FAULT. Best-effort + idempotent — a child already reaped
    /// (`None`) returns immediately, and every step swallows errors (a child that
    /// already exited is fine). Callable through a shared `Arc` (the worker faults
    /// the binary; Drop reaps on teardown).
    // [impl->REQ-MSG-IDLE-TRANSLATION-BINARY]
    pub fn terminate(&self) {
        self.close_stdin();
        let mut guard = self.child.lock().unwrap();
        let Some(child) = guard.as_mut() else {
            return; // already reaped
        };
        let deadline = Instant::now() + REAP_GRACE;
        loop {
            match child.try_wait() {
                Ok(Some(_)) => {
                    *guard = None;
                    return; // exited on its own after EOF
                }
                Ok(None) => {
                    if Instant::now() >= deadline {
                        break;
                    }
                    thread::sleep(REAP_POLL);
                }
                Err(_) => break,
            }
        }
        let _ = child.kill();
        let _ = child.wait();
        *guard = None;
    }
}

impl Drop for TranslationChild {
    fn drop(&mut self) {
        self.terminate();
    }
}

-    /// session-down. Best-effort — every step swallows errors (a child that

+    /// session-down or a FAULT. Best-effort + idempotent — a child already reaped

+    /// (`None`) returns immediately, and every step swallows errors (a child that

+    /// already exited is fine). Callable through a shared `Arc` (the worker faults

+    /// the binary; Drop reaps on teardown).

+    // [impl->REQ-MSG-IDLE-TRANSLATION-BINARY]

+        let mut guard = self.child.lock().unwrap();

+        let Some(child) = guard.as_mut() else {

-            match self.child.try_wait() {

-                Ok(Some(_)) => return, // exited on its own after EOF

+                    return; // exited on its own after EOF

Err(_) => break,
}
}
-        let _ = self.child.kill();
-        let _ = self.child.wait();
+        let _ = child.kill();
+        let _ = child.wait();
+        *guard = None;
}
}
impl Drop for TranslationChild {
fn drop(&mut self) {
-        self.reap();
+        self.terminate();
}
}


Now broker.rs. Update imports first:


34	use std::collections::{HashMap, VecDeque};
35	use std::io;
36	use std::sync::atomic::{AtomicU64, Ordering};
37	use std::sync::mpsc::{sync_channel, Receiver, SyncSender, TrySendError};
38	use std::sync::{Arc, Mutex};
39	use std::thread::{self, JoinHandle};
40	use std::time::{Duration, Instant};
41
42	use interprocess::local_socket::{prelude::*, SendHalf, Stream};
43	use spt_term::{Drain, PtySession, SessionSurface, SurfaceSize};
44
45	use spt_net::net::attach::AttachIntent;

use std::collections::{HashMap, VecDeque};
use std::io;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc::{sync_channel, Receiver, SyncSender, TrySendError};
use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};

use interprocess::local_socket::{prelude::*, SendHalf, Stream};
use spt_term::{Drain, PtySession, SessionSurface, SurfaceSize};

use spt_net::net::attach::AttachIntent;


use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc::{sync_channel, Receiver, SyncSender, TrySendError};

use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::mpsc::{channel, sync_channel, Receiver, RecvTimeoutError, Sender, SyncSender, TrySendError};


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\broker.rs has been updated successfully. (file state is current in your context — no need to Read it back)

use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc::{sync_channel, Receiver, SyncSender, TrySendError};

use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::mpsc::{channel, sync_channel, Receiver, RecvTimeoutError, Sender, SyncSender, TrySendError};

-use std::sync::mpsc::{sync_channel, Receiver, SyncSender, TrySendError};

+use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};

+use std::sync::mpsc::{channel, sync_channel, Receiver, RecvTimeoutError, Sender, SyncSender, TrySendError};


45	use spt_net::net::attach::AttachIntent;
46	use spt_net::net::endpoint::EndpointAddr;
47	use spt_store::perch::{resolve_perch_path, ParentHint};
48
49	use crate::codec::{read_frame, write_frame};
50	use crate::effect::{EffectJournal, EffectKind, Outcome};
51	use crate::frame::{Envelope, Role};
52	use crate::msg::{
53	    applied_envelope, decode_bytes, displaced_envelope, endpoint_injected_envelope,
54	    net_dialed_envelope, output_envelope,
55	    size_envelope, subscribed_envelope, BrainRestarted, EndpointInputReq,
56	    ErrorEvent, ExitEvent, InputReq, KillReq, NetDialReq, NetPresenceSubscribeReq, NetSent,
57	    NetStatusReply, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq, NetStreamSubscribeReq,
58	    NetStreamsReply, PairJoinReply, PairJoinReq, ResizeReq, SessionInfo, SessionsReply, SpawnReq,
59	    Spawned, SubscribeOutcome, SubscribeReq, UnsubscribeReq, KIND_BRAIN_RESTART, KIND_BRAIN_RESTARTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT,
60	    KIND_INPUT, KIND_KILL, KIND_NET_DIAL, KIND_NET_DIAL_LOOPBACK, KIND_NET_PRESENCE_SUBSCRIBE,
61	    KIND_NET_SENT,
62	    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,
63	    KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE,
64	    KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,
65	    KIND_SPAWNED, KIND_SUBSCRIBE, KIND_UNSUBSCRIBE,
66	};
67	use crate::nethost::{NetHost, NET_EFFECT_SESSION};
68	use crate::transport::{recv_hello, DaemonTransport, LocalSocketTransport};
69

use spt_net::net::attach::AttachIntent;
use spt_net::net::endpoint::EndpointAddr;
use spt_store::perch::{resolve_perch_path, ParentHint};

use crate::codec::{read_frame, write_frame};
use crate::effect::{EffectJournal, EffectKind, Outcome};
use crate::frame::{Envelope, Role};
use crate::msg::{
    applied_envelope, decode_bytes, displaced_envelope, endpoint_injected_envelope,
    net_dialed_envelope, output_envelope,
    size_envelope, subscribed_envelope, BrainRestarted, EndpointInputReq,
    ErrorEvent, ExitEvent, InputReq, KillReq, NetDialReq, NetPresenceSubscribeReq, NetSent,
    NetStatusReply, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq, NetStreamSubscribeReq,
    NetStreamsReply, PairJoinReply, PairJoinReq, ResizeReq, SessionInfo, SessionsReply, SpawnReq,
    Spawned, SubscribeOutcome, SubscribeReq, UnsubscribeReq, KIND_BRAIN_RESTART, KIND_BRAIN_RESTARTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT,
    KIND_INPUT, KIND_KILL, KIND_NET_DIAL, KIND_NET_DIAL_LOOPBACK, KIND_NET_PRESENCE_SUBSCRIBE,
    KIND_NET_SENT,
    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,
    KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE,
    KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,
    KIND_SPAWNED, KIND_SUBSCRIBE, KIND_UNSUBSCRIBE,
};
use crate::nethost::{NetHost, NET_EFFECT_SESSION};
use crate::transport::{recv_hello, DaemonTransport, LocalSocketTransport};


use crate::nethost::{NetHost, NET_EFFECT_SESSION};
use crate::transport::{recv_hello, DaemonTransport, LocalSocketTransport};

use crate::nethost::{NetHost, NET_EFFECT_SESSION};
use crate::translation::{key_to_bytes, InjectFloor, KeyCmd, ToBinary, TranslationChild};
use crate::transport::{recv_hello, DaemonTransport, LocalSocketTransport};


use crate::nethost::{NetHost, NET_EFFECT_SESSION};
use crate::transport::{recv_hello, DaemonTransport, LocalSocketTransport};

use crate::nethost::{NetHost, NET_EFFECT_SESSION};
use crate::translation::{key_to_bytes, InjectFloor, KeyCmd, ToBinary, TranslationChild};
use crate::transport::{recv_hello, DaemonTransport, LocalSocketTransport};

+use crate::translation::{key_to_bytes, InjectFloor, KeyCmd, ToBinary, TranslationChild};

/// The shared, serialized send side of one brain connection. Output frames,


Now add the deadline const + the `Translation` struct + inject-worker. Read the const region:


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	/// The interactive CONTROLLER's isolated sink (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST).
143	/// Symmetric with [`ViewerSink`] — a dedicated writer thread does the BLOCKING

/// 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);

/// The interactive CONTROLLER's isolated sink (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST).
/// Symmetric with [`ViewerSink`] — a dedicated writer thread does the BLOCKING


/// 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);

/// 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);


/// 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);

+/// 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 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


Now add the `Translation` struct + inject-worker function. Place them right before `HostedSession`:


718	        if let Some(seq) = seq {
719	            advance_delivered(&delivered_through, seq);
720	        }
721	    }
722	}
723
724	/// A child the broker hosts under a PTY — the §B-owned resources for one
725	/// session, plus its gapless-resume output log.
726	struct HostedSession {
727	    /// The PTY + child (PTY master/writer + spawned child process).
728	    session: Arc<PtySession>,
729	    /// The output drain pump (kept so teardown can stop it).
730	    #[allow(dead_code)] // held for ownership/teardown; not read directly
731	    drain: Drain,
732	    /// The sequenced, buffered output + current subscriber.
733	    log: Arc<Mutex<OutputLog>>,
734	    /// The owning endpoint (the spawn's label; empty = unlabeled). The
735	    /// inbound dispatcher's gate subject for remote-drive (D9-1).
736	    endpoint: String,
737	}
738
739	/// The broker process kernel. Owns the listening socket, the table of hosted

        if let Some(seq) = seq {
            advance_delivered(&delivered_through, seq);
        }
    }
}

/// 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 broker process kernel. Owns the listening socket, the table of hosted


/// 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 per-session translation-binary attachment (W2 Layer C, ADR-0022). Present
/// ONLY when the spawn carried a `translation_binary` path; absent → the session
/// uses the degenerate raw-inject path (Layer E fallback) exactly as before.
///
/// Holds the shared inject floor (the controller-input buffer, also touched by
/// `dispatch_input`), the queue that feeds inbound `<EVENT>`s to the inject worker
/// (events serialize — never two uncommitted sequences interleaved), the shared
/// child handle (so `dispatch_input` can fire content-free `{type:input}` pings
/// while the worker drives an event), and the FAULT flag the worker raises when a
/// sequence misses its [`INJECT_COMMIT_DEADLINE`] or the binary dies — after which
/// the session reverts to raw inject and stops pinging.
struct Translation {
    /// Controller-input buffer + inject-floor flag, shared with `dispatch_input`.
    floor: Arc<Mutex<InjectFloor>>,
    /// Queue one inbound `<EVENT>` envelope to the inject worker. Events QUEUE here
    /// and are driven one-at-a-time so two sequences never interleave.
    event_tx: Sender<String>,
    /// The live child — shared so `dispatch_input` fires content-free pings while
    /// the worker (which also holds an `Arc`) drives events / faults the binary.
    child: Arc<TranslationChild>,
    /// Raised by the worker on FAULT (commit-deadline miss / binary death): the
    /// 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.
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);

        if !committed {
            fault_translation(&faulted, &child, "no {commit} within INJECT_COMMIT_DEADLINE");
            return;
        }
    }
    // `event_tx` dropped → session teardown. Reap the child (no zombie).
    child.terminate();
}

/// FAULT a translation binary: mark the session degraded (so inbound delivery
/// reverts to raw inject and `dispatch_input` stops pinging), log it, and reap the
/// child. Never respawns — degrade-to-pass-through, never a dead operator.
fn fault_translation(faulted: &AtomicBool, child: &TranslationChild, why: &str) {
    faulted.store(true, Ordering::Relaxed);
    eprintln!("TRANSLATION_FAULT: {why} — terminating binary, reverting to raw inject");
    child.terminate();
}


/// 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 per-session translation-binary attachment (W2 Layer C, ADR-0022). Present
/// ONLY when the spawn carried a `translation_binary` path; absent → the session
/// uses the degenerate raw-inject path (Layer E fallback) exactly as before.
///
/// Holds the shared inject floor (the controller-input buffer, also touched by
/// `dispatch_input`), the queue that feeds inbound `<EVENT>`s to the inject worker
/// (events serialize — never two uncommitted sequences interleaved), the shared
/// child handle (so `dispatch_input` can fire content-free `{type:input}` pings
/// while the worker drives an event), and the FAULT flag the worker raises when a
/// sequence misses its [`INJECT_COMMIT_DEADLINE`] or the binary dies — after which
/// the session reverts to raw inject and stops pinging.
struct Translation {
    /// Controller-input buffer + inject-floor flag, shared with `dispatch_input`.
    floor: Arc<Mutex<InjectFloor>>,
    /// Queue one inbound `<EVENT>` envelope to the inject worker. Events QUEUE here
    /// and are driven one-at-a-time so two sequences never interleave.
    event_tx: Sender<String>,
    /// The live child — shared so `dispatch_input` fires content-free pings while
    /// the worker (which also holds an `Arc`) drives events / faults the binary.
    child: Arc<TranslationChild>,
    /// Raised by the worker on FAULT (commit-deadline miss / binary death): the
    /// 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.
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);

        if !committed {
            fault_translation(&faulted, &child, "no {commit} within INJECT_COMMIT_DEADLINE");
            return;
        }
    }
    // `event_tx` dropped → session teardown. Reap the child (no zombie).
    child.terminate();
}

/// FAULT a translation binary: mark the session degraded (so inbound delivery
/// reverts to raw inject and `dispatch_input` stops pinging), log it, and reap the
/// child. Never respawns — degrade-to-pass-through, never a dead operator.
fn fault_translation(faulted: &AtomicBool, child: &TranslationChild, why: &str) {
    faulted.store(true, Ordering::Relaxed);
    eprintln!("TRANSLATION_FAULT: {why} — terminating binary, reverting to raw inject");
    child.terminate();
}

+/// The per-session translation-binary attachment (W2 Layer C, ADR-0022). Present

+/// ONLY when the spawn carried a `translation_binary` path; absent → the session

+/// uses the degenerate raw-inject path (Layer E fallback) exactly as before.

+/// Holds the shared inject floor (the controller-input buffer, also touched by

+/// `dispatch_input`), the queue that feeds inbound `<EVENT>`s to the inject worker

+/// (events serialize — never two uncommitted sequences interleaved), the shared

+/// child handle (so `dispatch_input` can fire content-free `{type:input}` pings

+/// while the worker drives an event), and the FAULT flag the worker raises when a

+/// sequence misses its [`INJECT_COMMIT_DEADLINE`] or the binary dies — after which

+/// the session reverts to raw inject and stops pinging.

+    /// Controller-input buffer + inject-floor flag, shared with `dispatch_input`.

+    /// Queue one inbound `<EVENT>` envelope to the inject worker. Events QUEUE here

+    /// and are driven one-at-a-time so two sequences never interleave.

+    /// The live child — shared so `dispatch_input` fires content-free pings while

+    /// the worker (which also holds an `Arc`) drives events / faults the binary.

+    /// Raised by the worker on FAULT (commit-deadline miss / binary death): the

+    /// session reverts to raw inject and `dispatch_input` stops pinging.

/// session, plus its gapless-resume output log.

+    /// 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) {

+        let chunk = floor.lock().unwrap().take_or_release();

+                let _ = session.write_input(&bytes);

+/// 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.

+    // `event_rx` blocks here; events queue → one sequence at a time.

+        // 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

+        while cmd_rx.try_recv().is_ok() {}

+        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");

+        let deadline = Instant::now() + INJECT_COMMIT_DEADLINE;

+            let remaining = deadline.saturating_duration_since(now);

+                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 { .. }) => {

+                // 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);

+            fault_translation(&faulted, &child, "no {commit} within INJECT_COMMIT_DEADLINE");

+    // `event_tx` dropped → session teardown. Reap the child (no zombie).

+/// FAULT a translation binary: mark the session degraded (so inbound delivery

+/// reverts to raw inject and `dispatch_input` stops pinging), log it, and reap the

+/// child. Never respawns — degrade-to-pass-through, never a dead operator.

+fn fault_translation(faulted: &AtomicBool, child: &TranslationChild, why: &str) {

+    faulted.store(true, Ordering::Relaxed);

+    eprintln!("TRANSLATION_FAULT: {why} — terminating binary, reverting to raw inject");

/// The broker process kernel. Owns the listening socket, the table of hosted

/// sessions, and the broker-owned idempotency anchor (B5); holds no logic state

/// (ADR-0004 §B). The [`EffectJournal`] is a §B resource — the durable


Now wire `dispatch_spawn` to build the `Translation` + add it to `HostedSession`. Find the current location:


Bash

grep -n "HostedSession {\|endpoint: req.endpoint.clone()\|let resp = Spawned\|fn dispatch_spawn\|fn local_node_label\|os_hostname" crates/spt-daemon/src/broker.rs | head

Locate spawn insertion point post-edits


751:struct HostedSession {
1211:    fn dispatch_spawn(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {
1294:            HostedSession {
1298:                endpoint: req.endpoint.clone(),
1302:        let resp = Spawned {


1284	                        let _ = write_frame(&mut *s, &frame);
1285	                    }
1286	                }
1287	            }
1288	            // Reap the dead session from the table.
1289	            sessions_exit.lock().unwrap().remove(&id);
1290	        });
1291
1292	        self.sessions.lock().unwrap().insert(
1293	            id,
1294	            HostedSession {
1295	                session,
1296	                drain,
1297	                log,
1298	                endpoint: req.endpoint.clone(),
1299	            },
1300	        );
1301
1302	        let resp = Spawned {
1303	            session_id: id,
1304	            pid,
1305	        };

                        let _ = write_frame(&mut *s, &frame);
                    }
                }
            }
            // Reap the dead session from the table.
            sessions_exit.lock().unwrap().remove(&id);
        });

        self.sessions.lock().unwrap().insert(
            id,
            HostedSession {
                session,
                drain,
                log,
                endpoint: req.endpoint.clone(),
            },
        );

        let resp = Spawned {
            session_id: id,
            pid,
        };


Edit

        self.sessions.lock().unwrap().insert(
            id,
            HostedSession {
                session,
                drain,
                log,
                endpoint: req.endpoint.clone(),
            },
        );

        // Layer C: if the spawn carried a translation binary, bring it up with the
        // session — spawn the pure stdin→stdout filter + the inject worker that
        // owns the atomic PTY apply. A spawn failure FAILS CLOSED to raw inject
        // (logged, `None`) rather than aborting the session — a missing binary must
        // never cost the operator their agent.
        let translation = req.translation_binary.as_deref().and_then(|path| {
            let (cmd_tx, cmd_rx) = channel::<KeyCmd>();
            match TranslationChild::spawn(path, cmd_tx) {
                Ok(child) => {
                    let child = Arc::new(child);
                    // First line: identity (content-free of any PTY bytes).
                    let _ = child.send(&ToBinary::Init {
                        endpoint_id: req.endpoint.clone(),
                        node: spt_store::hostlabel::os_hostname().unwrap_or_default(),
                    });
                    let floor = Arc::new(Mutex::new(InjectFloor::default()));
                    let faulted = Arc::new(AtomicBool::new(false));
                    let (event_tx, event_rx) = channel::<String>();
                    let w_session = Arc::clone(&session);
                    let w_child = Arc::clone(&child);
                    let w_floor = Arc::clone(&floor);
                    let w_faulted = Arc::clone(&faulted);
                    thread::spawn(move || {
                        run_inject_worker(w_session, w_child, w_floor, event_rx, cmd_rx, w_faulted);
                    });
                    Some(Arc::new(Translation {
                        floor,
                        event_tx,
                        child,
                        faulted,
                    }))
                }
                Err(e) => {
                    eprintln!(
                        "TRANSLATION_SPAWN_FAILED:{path}: {e} — degrading to raw inject"
                    );
                    None
                }
            }
        });

        self.sessions.lock().unwrap().insert(
            id,
            HostedSession {
                session,
                drain,
                log,
                endpoint: req.endpoint.clone(),
                translation,
            },
        );


        self.sessions.lock().unwrap().insert(
            id,
            HostedSession {
                session,
                drain,
                log,
                endpoint: req.endpoint.clone(),
            },
        );

        // Layer C: if the spawn carried a translation binary, bring it up with the
        // session — spawn the pure stdin→stdout filter + the inject worker that
        // owns the atomic PTY apply. A spawn failure FAILS CLOSED to raw inject
        // (logged, `None`) rather than aborting the session — a missing binary must
        // never cost the operator their agent.
        let translation = req.translation_binary.as_deref().and_then(|path| {
            let (cmd_tx, cmd_rx) = channel::<KeyCmd>();
            match TranslationChild::spawn(path, cmd_tx) {
                Ok(child) => {
                    let child = Arc::new(child);
                    // First line: identity (content-free of any PTY bytes).
                    let _ = child.send(&ToBinary::Init {
                        endpoint_id: req.endpoint.clone(),
                        node: spt_store::hostlabel::os_hostname().unwrap_or_default(),
                    });
                    let floor = Arc::new(Mutex::new(InjectFloor::default()));
                    let faulted = Arc::new(AtomicBool::new(false));
                    let (event_tx, event_rx) = channel::<String>();
                    let w_session = Arc::clone(&session);
                    let w_child = Arc::clone(&child);
                    let w_floor = Arc::clone(&floor);
                    let w_faulted = Arc::clone(&faulted);
                    thread::spawn(move || {
                        run_inject_worker(w_session, w_child, w_floor, event_rx, cmd_rx, w_faulted);
                    });
                    Some(Arc::new(Translation {
                        floor,
                        event_tx,
                        child,
                        faulted,
                    }))
                }
                Err(e) => {
                    eprintln!(
                        "TRANSLATION_SPAWN_FAILED:{path}: {e} — degrading to raw inject"
                    );
                    None
                }
            }
        });

        self.sessions.lock().unwrap().insert(
            id,
            HostedSession {
                session,
                drain,
                log,
                endpoint: req.endpoint.clone(),
                translation,
            },
        );

+        // Layer C: if the spawn carried a translation binary, bring it up with the

+        // session — spawn the pure stdin→stdout filter + the inject worker that

+        // owns the atomic PTY apply. A spawn failure FAILS CLOSED to raw inject

+        // (logged, `None`) rather than aborting the session — a missing binary must

+        // never cost the operator their agent.

+        let translation = req.translation_binary.as_deref().and_then(|path| {

+            let (cmd_tx, cmd_rx) = channel::<KeyCmd>();

+            match TranslationChild::spawn(path, cmd_tx) {

+                    let child = Arc::new(child);

+                    // First line: identity (content-free of any PTY bytes).

+                    let _ = child.send(&ToBinary::Init {

+                        endpoint_id: req.endpoint.clone(),

+                        node: spt_store::hostlabel::os_hostname().unwrap_or_default(),

+                    let floor = Arc::new(Mutex::new(InjectFloor::default()));

+                    let faulted = Arc::new(AtomicBool::new(false));

+                    let (event_tx, event_rx) = channel::<String>();

+                    let w_session = Arc::clone(&session);

+                    let w_child = Arc::clone(&child);

+                    let w_floor = Arc::clone(&floor);

+                    let w_faulted = Arc::clone(&faulted);

+                    thread::spawn(move || {

+                        run_inject_worker(w_session, w_child, w_floor, event_rx, cmd_rx, w_faulted);

+                    Some(Arc::new(Translation {

+                        "TRANSLATION_SPAWN_FAILED:{path}: {e} — degrading to raw inject"


Check for other `HostedSession` construction sites (resume/restoration paths):


Bash

grep -rn "HostedSession {" crates/spt-daemon/src/

All HostedSession construction sites


crates/spt-daemon/src/broker.rs:751:struct HostedSession {
crates/spt-daemon/src/broker.rs:1335:            HostedSession {


Only one site. Now wire `dispatch_input` — clone `translation` out, fire ping, route through floor. Locate it:


grep -n "fn dispatch_input\|fn dispatch_endpoint_input" crates/spt-daemon/src/broker.rs

Find dispatch fns post-edit


1429:    fn dispatch_input(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {
1484:    fn dispatch_endpoint_input(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {


1429	    fn dispatch_input(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {
1430	        let req: InputReq =
1431	            serde_json::from_value(env.payload).map_err(|e| format!("bad input payload: {e}"))?;
1432	        let bytes = decode_bytes(&req.data_b64).map_err(|e| format!("bad input base64: {e}"))?;
1433	        // Clone the session Arc out from under the sessions lock so the PTY write
1434	        // + fsync happen without holding the table lock (mirrors dispatch_subscribe).
1435	        let session = {
1436	            let sessions = self.sessions.lock().unwrap();
1437	            let h = sessions
1438	                .get(&req.session_id)
1439	                .ok_or_else(|| format!("no such session {}", req.session_id))?;
1440	            Arc::clone(&h.session)
1441	        };
1442	        match req.op_id {
1443	            Some(op) => {
1444	                let key = (req.session_id, op);
1445	                let outcome = self
1446	                    .journal
1447	                    .apply_once(key, EffectKind::PtyWrite, || {
1448	                        session
1449	                            .write_input(&bytes)
1450	                            .map_err(|e| io::Error::other(e.to_string()))
1451	                    })
1452	                    .map_err(|e| format!("write_input failed: {e}"))?;
1453	                // Ack exactly-once: both Applied and Deduped mean "durably landed".
1454	                send_frame(
1455	                    send,
1456	                    &applied_envelope(req.session_id, op, outcome == Outcome::Applied),
1457	                );
1458	                Ok(())
1459	            }
1460	            None => session
1461	                .write_input(&bytes)
1462	                .map_err(|e| format!("write_input failed: {e}")),
1463	        }
1464	    }
1465
1466	    /// Deliver an inbound message to an spt-hosted endpoint by ENDPOINT ID:
1467	    /// resolve the endpoint to its hosted session and inject the bytes straight
1468	    /// into that session's PTY (REQ-SEND-SPT-HOSTED). The endpoint-keyed
1469	    /// counterpart of [`dispatch_input`] — a CLI `spt send` to an spt-hosted
1470	    /// endpoint (broker holds its PTY, no `api listen` relay) lands here instead
1471	    /// of spooling silently. Resolution is atomic under the sessions lock (no
1472	    /// TOCTOU); the bytes are a pre-rendered `<EVENT>` envelope (the caller owns
1473	    /// the wire format — ADR-0020). Fire-and-forget: a direct write, no journal
1474	    /// (a user message is delivered like the relay's emit, not an exactly-once
1475	    /// effect). Replies [`KIND_ENDPOINT_INJECTED`] with `delivered` so the caller
1476	    /// knows whether to fall back to the spool.
1477	    ///
1478	    /// IDLE/DIRECT path only (REQ-SEND-SPT-HOSTED, MVP per CONTEXT:188): a write
1479	    /// arriving mid-agent-turn can interleave with the agent's active output —
1480	    /// activity-gated routing (idle→inject / active→spool-for-poll-drain) is a
1481	    /// deferred follow wave. The write is logged so an active-state inject is
1482	    /// never silent.
1483	    // [impl->REQ-SEND-SPT-HOSTED]

    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 out from under the sessions lock so the PTY write
        // + fsync happen without holding the table lock (mirrors dispatch_subscribe).
        let session = {
            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)
        };
        match req.op_id {
            Some(op) => {
                let key = (req.session_id, op);
                let outcome = self
                    .journal
                    .apply_once(key, EffectKind::PtyWrite, || {
                        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 => 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]


Edit

        // Clone the session Arc out from under the sessions lock so the PTY write
        // + fsync happen without holding the table lock (mirrors dispatch_subscribe).
        let session = {
            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)
        };
        match req.op_id {
            Some(op) => {
                let key = (req.session_id, op);
                let outcome = self
                    .journal
                    .apply_once(key, EffectKind::PtyWrite, || {
                        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 => session
                .write_input(&bytes)
                .map_err(|e| format!("write_input failed: {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}"))
            }
        }
    }


        // Clone the session Arc out from under the sessions lock so the PTY write
        // + fsync happen without holding the table lock (mirrors dispatch_subscribe).
        let session = {
            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)
        };
        match req.op_id {
            Some(op) => {
                let key = (req.session_id, op);
                let outcome = self
                    .journal
                    .apply_once(key, EffectKind::PtyWrite, || {
                        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 => session
                .write_input(&bytes)
                .map_err(|e| format!("write_input failed: {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}"))
            }
        }
    }

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 out from under the sessions lock so the PTY write

-        // + fsync happen without holding the table lock (mirrors dispatch_subscribe).

+        // 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).

+            (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 {

+                .is_some_and(|tr| tr.floor.lock().unwrap().buffer_if_held(bytes))

+                        // 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(());

-                .map_err(|e| format!("write_input failed: {e}")),

+                    .write_input(&bytes)

+                    .map_err(|e| format!("write_input failed: {e}"))


Now Layer E — `dispatch_endpoint_input` routing. Read its body:


1509	    /// activity-gated routing (idle→inject / active→spool-for-poll-drain) is a
1510	    /// deferred follow wave. The write is logged so an active-state inject is
1511	    /// never silent.
1512	    // [impl->REQ-SEND-SPT-HOSTED]
1513	    fn dispatch_endpoint_input(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {
1514	        let req: EndpointInputReq = serde_json::from_value(env.payload)
1515	            .map_err(|e| format!("bad endpoint-input payload: {e}"))?;
1516	        let bytes =
1517	            decode_bytes(&req.data_b64).map_err(|e| format!("bad endpoint-input base64: {e}"))?;
1518	        // Resolve endpoint → session under the lock, cloning the Arc out so the
1519	        // PTY write happens without holding the table lock (dispatch_input rule).
1520	        let session = {
1521	            let sessions = self.sessions.lock().unwrap();
1522	            sessions
1523	                .values()
1524	                .find(|h| h.endpoint == req.endpoint)
1525	                .map(|h| Arc::clone(&h.session))
1526	        };
1527	        match session {
1528	            Some(session) => {
1529	                // IDLE/direct inject. Logged (REQ-SEND-SPT-HOSTED MVP): activity is
1530	                // not yet gated, so note the raw write — never a silent mid-turn
1531	                // interleave passed off as done.
1532	                eprintln!(
1533	                    "ENDPOINT_INJECT:{} ({} bytes, idle-direct — activity not yet gated)",
1534	                    req.endpoint,
1535	                    bytes.len()
1536	                );
1537	                session
1538	                    .write_input(&bytes)
1539	                    .map_err(|e| format!("write_input failed: {e}"))?;
1540	                send_frame(send, &endpoint_injected_envelope(&req.endpoint, true));
1541	                Ok(())
1542	            }
1543	            // No hosted session for this endpoint — tell the caller to spool.
1544	            None => {
1545	                send_frame(send, &endpoint_injected_envelope(&req.endpoint, false));
1546	                Ok(())
1547	            }
1548	        }
1549	    }
1550
1551	    /// Resize a session's PTY — CONTROLLER-EXCLUSIVE (REQ-RCVIEW-1). The broker
1552	    /// rejects a resize arriving on any stream that is not the session's current
1553	    /// controller (a viewer's resize is refused), then updates the stored size
1554	    /// and pushes a `size` frame to every viewer (the letterbox feed).
1555	    fn dispatch_resize(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {
1556	        let req: ResizeReq =
1557	            serde_json::from_value(env.payload).map_err(|e| format!("bad resize payload: {e}"))?;
1558	        let (session, log) = {
1559	            let sessions = self.sessions.lock().unwrap();
1560	            let h = sessions
1561	                .get(&req.session_id)
1562	                .ok_or_else(|| format!("no such session {}", req.session_id))?;
1563	            (Arc::clone(&h.session), Arc::clone(&h.log))

    /// 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 under the lock, cloning the Arc out so the
        // PTY write happens without holding the table lock (dispatch_input rule).
        let session = {
            let sessions = self.sessions.lock().unwrap();
            sessions
                .values()
                .find(|h| h.endpoint == req.endpoint)
                .map(|h| Arc::clone(&h.session))
        };
        match session {
            Some(session) => {
                // IDLE/direct 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))


        // Resolve endpoint → session under the lock, cloning the Arc out so the
        // PTY write happens without holding the table lock (dispatch_input rule).
        let session = {
            let sessions = self.sessions.lock().unwrap();
            sessions
                .values()
                .find(|h| h.endpoint == req.endpoint)
                .map(|h| Arc::clone(&h.session))
        };
        match session {
            Some(session) => {
                // IDLE/direct 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(())
            }
        }
    }

        // 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(())
            }
        }
    }


        // Resolve endpoint → session under the lock, cloning the Arc out so the
        // PTY write happens without holding the table lock (dispatch_input rule).
        let session = {
            let sessions = self.sessions.lock().unwrap();
            sessions
                .values()
                .find(|h| h.endpoint == req.endpoint)
                .map(|h| Arc::clone(&h.session))
        };
        match session {
            Some(session) => {
                // IDLE/direct 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(())
            }
        }
    }

        // 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(())
            }
        }
    }

.map_err(|e| format!("bad endpoint-input payload: {e}"))?;

decode_bytes(&req.data_b64).map_err(|e| format!("bad endpoint-input base64: {e}"))?;

-        // Resolve endpoint → session under the lock, cloning the Arc out so the

-        // PTY write happens without holding the table lock (dispatch_input rule).

+        // Resolve endpoint → (session, translation) under the lock, cloning the

+        // Arcs out so the PTY write happens without holding the table lock

-                .map(|h| Arc::clone(&h.session))

+                .map(|h| (Arc::clone(&h.session), h.translation.clone()))

-                // IDLE/direct inject. Logged (REQ-SEND-SPT-HOSTED MVP): activity is

-                // not yet gated, so note the raw write — never a silent mid-turn

+            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() {

+                                "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


Layers C + E wired. Build the daemon crate to catch errors:


Bash

cargo build -p spt-daemon 2>&1 | grep -E "error|warning: unused|^error\[|cannot find" | head -40; echo "=== full tail ==="; cargo build -p spt-daemon 2>&1 | tail -5

Compile-check spt-daemon


=== full tail ===
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.47s


Clean compile. Run clippy (CI form) + the banked translation units + the W1b int suite (via nextest per F1):


cargo clippy --workspace --all-targets -- -D warnings 2>&1 | grep -E "error|warning" | head -30; echo "=== clippy tail ==="; cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -3

Workspace clippy CI form


=== clippy tail ===
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.54s


cargo nextest run -p spt-daemon translation 2>&1 | tail -25

Run translation module unit tests via nextest


   Compiling spt-daemon v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 31.55s
────────────
 Nextest run ID 9c4aa3ee-07ea-40d4-b169-180533bd239b with nextest profile: default
    Starting 5 tests across 33 binaries (424 tests skipped)
        PASS [   0.015s] (1/5) spt-daemon translation::tests::key_to_bytes_send_keys_map
        PASS [   0.017s] (2/5) spt-daemon translation::tests::to_binary_serializes_to_wire_and_input_is_content_free
        PASS [   0.019s] (3/5) spt-daemon harnesshost::tests::prepare_threads_translation_binary_path_when_declared
        PASS [   0.020s] (4/5) spt-daemon translation::tests::inject_floor_buffers_while_held_then_drains_in_order
        PASS [   0.022s] (5/5) spt-daemon translation::tests::key_cmd_parses_each_wire_shape
────────────
     Summary [   0.023s] 5 tests run: 5 passed, 424 skipped


Banked units green. Run the W1b int suite to confirm no regression:


cargo nextest run -p spt-daemon --test inject_control_wedge 2>&1 | tail -15

Run inject_control_wedge int via nextest


    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.46s
────────────
 Nextest run ID ecc59bd3-b742-4b19-a941-f871671eda46 with nextest profile: default
    Starting 3 tests across 1 binary
        PASS [   3.175s] (1/3) spt-daemon::inject_control_wedge a_journaled_input_wedge_does_not_starve_a_concurrent_rc_attach
        PASS [   3.190s] (2/3) spt-daemon::inject_control_wedge a_backed_up_controller_does_not_wedge_the_session
        PASS [   5.595s] (3/3) spt-daemon::inject_control_wedge injecting_a_large_payload_into_a_stdin_ignoring_child
────────────
     Summary [   5.595s] 3 tests run: 3 passed, 0 skipped


No regression. Layers C+E build clean, all units + W1b int green. Let me check the REQ registry stages before tagging + committing:


grep -n "REQ-MSG-IDLE-TRANSLATION-BINARY\|REQ-HAZARD-INJECT-CONTROL-COEXIST" traceable-reqs.toml | head; echo "=== current stages ==="; grep -A2 'id = "REQ-MSG-IDLE-TRANSLATION-BINARY"' traceable-reqs.toml | grep required_stages; grep -A2 'id = "REQ-HAZARD-INJECT-CONTROL-COEXIST"' traceable-reqs.toml | grep required_stages

Check REQ stages for the two W2 reqs


1189:id = "REQ-HAZARD-INJECT-CONTROL-COEXIST"
1195:id = "REQ-MSG-IDLE-TRANSLATION-BINARY"
1196:title = "spt-hosted idle message delivery via an adapter TRANSLATION BINARY (ADR-0022). New opt-in manifest section `[message-idle-translation-binary]` = a TABLE carrying a `path` scalar (doyle OPT-B ruling: modeled as a table, not a bare top-level scalar, so a preceding section cannot silently absorb it + N+1 extensible; spt-core does NOT deny_unknown_fields, so a future key degrades gracefully); spt-core LIFECYCLE-manages it (spawn when the endpoint comes up, terminate when it goes down). The binary is a PURE stdin→stdout filter; spt-core owns EVERY PTY write. stdin (JSON-lines): `{type:\"init\",endpoint_id,node}` first · `{type:\"event\",envelope:\"<EVENT…>\"}` per inbound message (ADR-0020 envelope) · `{type:\"input\"}` content-free ping on each operator keystroke (binary tracks user-idle for its own idle-gated buffering; PTY input content NOT duplicated). stdout (JSON-lines): keystroke-commands `{key:…}`/`{delay_ms:…}`/`{text:…}` (extensible). spt-core applies the emitted sequence to the broker PTY ATOMICALLY (the W1 coordination — REQ-HAZARD-INJECT-CONTROL-COEXIST). The daemon poll feed is the ONE idle substrate for both topologies (Q1=A): harness-hosted consumer = the Monitor child, spt-hosted consumer = this binary; spt-core PREFERS a perch's poll listener if one exists (so spt-hosted can run a listener AND keep `spt rc`). Idle-only; busy/mid-turn = adapter hook-injection. Closes the current grounding gap: `api bind` registers no listener port → a listener-less spt-hosted perch SPOOLS inbound (only spooling+adapter-poll works today) → this delivers real inbound into the PTY. The v0.11.0 raw inject is the degenerate `{text:payload}{key:enter}` case. (v0.13.0)"
1226:title = "The effect journal serializes EVERY PTY effect under one mutex held ACROSS two fsyncs AND the blocking PTY write — so interactive input stutters and ultimately wedges the daemon hard. ROOT (doyle /diagnose, code-grounded + MEASURED on the operator's real Windows box, 2026-06-19): EffectJournal::apply_once (effect.rs:168-188) takes `inner.lock()` and holds it across `write_line(PENDING)` → `effect()` → `write_line(DONE)`, where write_line (effect.rs:235-239) does flush()+sync_all() (a full FlushFileBuffers) — so each effect pays TWO fsyncs under a GLOBAL lock, and the closure `effect()` (the actual PTY write, broker.rs:1257 EffectKind::PtyWrite via attach.rs:197 send_effect) runs while the lock is held. Two operator-visible facets, ONE root: (A) STUTTER/LAG — every keystroke is a PtyWrite effect = 2× sync_all serialized; measured fsync on %LOCALAPPDATA%\\spt-core = median 6.5ms, spikes to 198ms (C: was recently at 100%), so ~13ms+ per keystroke best case, hundreds under contention → 'many but not all keypresses take 100s of ms, choppy, worsens with volume'. (B) HARD PERMANENT WEDGE — when a PtyWrite `effect()` blocks (ConPTY input buffer full / harness not draining stdin), the journal lock is held INDEFINITELY → the single-threaded inbound-stream dispatch (dispatch.rs serve_attach, which both applies input effects AND opens attaches) can never progress → EVERY subsequent attach (`spt rc --view`/`--take`) fails with 'attach request: brain IPC read deadline elapsed' (confirmed: two retries deadline identically; broker control-plane KIND queries still answer — different thread). This REFUTES the W2-deferred ruling that park-(b)/(c) is 'Windows-benign because ConPTY absorbs 4MiB' — on the real box the input path wedges regardless. DISTINCT from W1 (REQ-HAZARD-INJECT-CONTROL-COEXIST = the OUTPUT drain, correctly fixed @8b5583e; output uses broker.rs:1106 append, NOT the fsync journal). This is the INPUT/effect-journal path W1 never touched, and it is THE wedge the operator hits with --take/--view. FIX DIRECTION (candidates, repro-first — extend inject_control_wedge.rs to a REAL backed-up-PTY-consumer + a real rc-client attach assertion, the gap W1's gate missed): (1) do NOT hold the journal lock across effect() — reserve the key + fsync PENDING under lock, RELEASE, run effect(), re-acquire to fsync DONE + mark applied (preserve crash-idempotency via the per-key reservation, not a global hold); (2) bound/fail-fast the PtyWrite itself (the W2-deferred park bound — write_input must never block indefinitely, DSR-answer must not hold the writer mutex across a blocking write); (3) drop per-keystroke fsync on the interactive path — PtyWrite effects are EPHEMERAL (a keystroke lost on a broker crash is retyped; PTY state is not reconstructed from keystroke replay), so in-memory applied-set dedup suffices (the broker survives the brain — that IS the dedup anchor), with async/batched fsync or no-fsync for EffectKind::PtyWrite while durable kinds (NetSend/NetDial/Registry/Spool) keep their fsync. Combine (1)+(3) at minimum. Add a KNOWN-HAZARDS.md entry on landing. (v0.13.0)"
=== current stages ===
required_stages = ["doc"]  # ACTIVATING v0.13.0 W2 (todlando) — staged: doc DONE (manifest section row in MANIFEST.md inject-seam family + CONTEXT.md:39 delivery section reworded W0 + ADR-0022 wording table-not-scalar + manifest.schema.json regen); impl/unit/int bump in as each layer lands (Layer A manifest section + validate + schema = impl/unit foundation; B translation.rs protocol+driver; C atomic PTY apply w/ controller buffering; D lifecycle via SpawnReq.translation_binary; E routing; F Unix park (b)/(c) bound; G int gate). int gate: a real dummy translation-binary delivers an inbound message into an rc-attached spt-hosted session via the choreography WITHOUT breaking control (composes with W1's invariant). ALSO OWNS the Unix-only park (b)/(c) PTY-write bound deferred from W1 (doyle ruling 2026-06-19): the 'spt-core owns every PTY write, applied atomically' substrate is where the forkpty input-queue park gets bounded — bounded/fail-fast write_input + DSR-answer must not hold the writer mutex across a blocking write; gate on gravity-linux (the cfg(unix) park-(b) assert in inject_control_wedge.rs proves the park is real + keeps it tracked).
required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 W1 (todlando). ROOT confirmed repro-first on a REAL broker + REAL PTY (inject_control_wedge.rs): park (a) = OutputLog::append fanned each chunk to the controller via a SYNCHRONOUS inline write_frame on the session drain thread while holding Mutex<OutputLog> → a backed-up controller socket parked the drain WITH THE LOCK HELD → permanent per-session wedge (KIND_SESSIONS hung; net_status answered = process alive). Parks (b)/(c) (write_input write_all on a full input buffer + DSR-answer writer-mutex contention) confirmed BENIGN on Windows ConPTY (absorbs a 4MiB inject), real only on Unix forkpty. FIX: controller delivery moved OFF the drain thread onto a dedicated writer thread + bounded channel (controller_writer, the viewer_writer pattern) but AUTHORITATIVE — the writer advances delivered_through (now Arc<AtomicU64>, advance_delivered/advanced_cursor monotonic); the drain hands each chunk off via a bounded off-lock send (ControllerJob::deliver, CONTROLLER_WRITE_DEADLINE=5s → detach/clear-driven-by/unlatch via the shared clear_controller, never park forever); displaced/exit notices ride the writer channel (CtrlMsg::Control). impl = broker.rs append/controller_writer/become_controller/clear_controller. unit = controller_deliver_is_bounded_never_a_permanent_park + advance_delivered_is_monotonic. int = inject_control_wedge.rs (a_backed_up_controller_does_not_wedge_the_session: a fully-backed-up controller keeps KIND_SESSIONS answering = no wedge). W1<->W5 SYNERGY: clear_controller IS the lost-controller unlatch W5's reconcile reuses.


REQ is at `["doc"]`; Layer C+E lands impl (broker) on top of the kernel's impl+unit (translation.rs). Tag the broker worker + bump to `["doc","impl","unit"]` (int rides Layer G). Tag the worker:


Edit

/// (session teardown), reaping the child.
fn run_inject_worker(

/// (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(


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\broker.rs has been updated successfully. (file state is current in your context — no need to Read it back)

/// (session teardown), reaping the child.
fn run_inject_worker(

/// (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(

/// stranded), then FAULT — terminate the binary, set `faulted`, and exit (the

/// session degrades to raw inject). The loop ends when `event_tx` is dropped

+/// 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.


required_stages = ["doc"]  # ACTIVATING v0.13.0 W2 (todlando) — staged: doc DONE

required_stages = ["doc", "impl", "unit"]  # ACTIVATING v0.13.0 W2 (todlando) — int rides Layer G. Layers A(@c73eb6a)+B(@611205c)+D(@7166f52)+C-kernel(@79f5a40)+C-proper/E landed: impl = translation.rs (ToBinary/KeyCmd/key_to_bytes/TranslationChild/InjectFloor) + broker.rs (Translation/run_inject_worker/flush_inject_floor/fault_translation + dispatch_spawn bring-up + dispatch_input floor-buffer&ping + dispatch_endpoint_input event routing); unit = translation.rs (wire round-trips, content-free ping, KeyCmd shapes, InjectFloor buffer/drain, send-keys map). int (Layer G, NEXT via spt-test-engineer) = real dummy translation-binary choreography into an rc-attached session w/o breaking control + no-commit-deadline FAULT + content-free + no-zombie. — staged: doc DONE