Skip to main content

agentmux_srv/backend/blockcontroller/persistent/
input.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! User-facing input surface: user messages, AskUserQuestion answers/denials,
5//! tool-permission decisions, raw stdin, and inbound control frames.
6
7use super::*;
8
9impl PersistentSubprocessController {
10    /// What a dead-air fallback needs to record its re-delivered line — see
11    /// [`RedeliveryPersister`].
12    pub(super) fn redelivery_persister(&self) -> RedeliveryPersister {
13        RedeliveryPersister {
14            broker: self.broker.clone(),
15            filestore: self.filestore.clone(),
16            mstore: self.mstore.clone(),
17            block_id: self.block_id.clone(),
18        }
19    }
20
21    /// Encode a message as the stream-json stdin line the CLI expects.
22    pub(super) fn encode_user_message(message: &str) -> String {
23        serde_json::json!({
24            "type": "user",
25            "message": {
26                "role": "user",
27                "content": message
28            }
29        })
30        .to_string()
31    }
32
33    /// Write an already-encoded stdin line, given a held `inner` guard.
34    ///
35    /// Callers must hold the lock across the must-wait check, the queue
36    /// update and this write, so no concurrent sender or watchdog tick can
37    /// slip between them.
38    ///
39    /// On success the line is also tracked into the current generation's
40    /// resume retry batch, so every write site (the send path and the
41    /// watchdog) gets it without having to remember to.
42    pub(super) fn try_write_stdin_locked(
43        inner: &mut PersistentInner,
44        line: &str,
45    ) -> Result<(), String> {
46        // reagentx P1 on PR #2360 (sixth review pass, round 7):
47        // `spawn_process` sets `stdin_tx` synchronously, well before the
48        // queued message that triggered the spawn is actually delivered by
49        // the background drain task (`drain_queue_after_successful_spawn`).
50        // Gating purely on `stdin_tx.is_some()` let a message land in that
51        // window and `try_send` straight to the live channel, jumping ahead
52        // of whatever was still queued. Checked in the SAME acquisition as
53        // `stdin_tx`, so nothing slips between two checks.
54        if inner.spawning_in_progress {
55            return Err("persistent process is still starting up — try again shortly".to_string());
56        }
57        // A committed deferred restart leaves `stdin_tx` live until the process
58        // actually exits, and a line written into a process about to be killed
59        // is lost, even though its caller was told it was delivered. The human
60        // path (`decide_send_action`) already refuses DeliverDirect here (codex
61        // P1 on PR #2858); this is the same rule for every write through this
62        // helper (codex P1 on #3562).
63        if inner.restart_pending {
64            return Err("persistent process is restarting for a config change — try again shortly".to_string());
65        }
66        // Likewise once a kill has been requested: the process is going down
67        // and a line written now dies with it (see `stop_pending`). Once it
68        // has exited, "not running" below is the accurate answer.
69        if inner.stop_pending && inner.stdin_tx.is_some() {
70            return Err("persistent process is stopping — try again shortly".to_string());
71        }
72        let tx = inner
73            .stdin_tx
74            .as_ref()
75            .ok_or("persistent process not running")?;
76        tx.try_send(line.to_string())
77            .map_err(|e| format!("stdin send failed: {e}"))?;
78        // codex P1 + reagent P1 on PR #3523, found independently by both:
79        // track this into the CURRENT generation's retry batch, exactly as
80        // `send_message`'s `DeliverDirect` branch does. That fix covered the
81        // typed-in-the-UI path and missed this one — the MuxBus/reactive
82        // injection path (`deliver_agent_message`).
83        //
84        // The gap matters most for eager resume, which spawns with an
85        // unconfirmed `--resume` and NO seed message, so the retry batch
86        // starts empty. An injected prompt written straight to stdin and never
87        // appended leaves it empty, so when the resumed sid turns out to be
88        // stale the retry fires with nothing to redeliver — and the prompt is
89        // gone, even though the caller was told it was delivered and the live
90        // blockfile append already rendered it to the operator.
91        //
92        // Read the generation and apply under the caller's SAME lock
93        // acquisition (already held for `try_send`) — a separate one risks a
94        // concurrent respawn bumping `spawn_generation` in between, making the
95        // event carry a stale generation that `update()`'s catch-all silently
96        // ignores. A no-op when no unconfirmed resume is in flight.
97        let generation = inner.spawn_generation;
98        let seq = inner.take_next_message_seq();
99        inner.apply_resume_event(persistent_resume::ResumeEvent::MessageAppendedToRetryBatch {
100            generation,
101            entry: persistent_resume::QueuedRetryEntry { seq, json: line.to_string() },
102        });
103        Ok(())
104    }
105
106    /// Deliver a user message to the **already-running** persistent process,
107    /// without a spawn config. Unlike `send_message`, this never spawns — it errors
108    /// if the process is not running. Used for controller-aware muxbus/reactive
109    /// delivery (`deliver_agent_message`), where the agent is live (busy or idle)
110    /// and we have no `PersistentSpawnConfig` to hand.
111    ///
112    /// A live process gets the message at once, whatever the agent is doing —
113    /// mid-turn included; the CLI reads it at its next inference step. It is
114    /// queued only while the process cannot take a write yet: a spawn in
115    /// flight, a committed restart, a requested stop, or another writer owning
116    /// stdin (see [`Self::deferred_must_wait_locked`]). Refusing it then used
117    /// to drop the message outright (`SPEC_NO_MIDTURN_DELIVERY_2026_09_23.md`
118    /// §3.2.1). Spec: `SPEC_JEKT_IMMEDIATE_DELIVERY_2026_09_28.md` §2.1.
119    #[cfg(test)]
120    pub fn send_user_message(&self, message: String) -> Result<(), String> {
121        self.send_user_message_outcome(message).map(|_| ())
122    }
123
124    /// [`send_user_message`], reporting whether the message went to the agent
125    /// now or is queued until the process can take it (it is starting up,
126    /// restarting or stopping) — so an automated sender can tell its caller
127    /// the truth (MCP `SendMessage`).
128    #[cfg(test)]
129    pub fn send_user_message_outcome(&self, message: String) -> Result<SendOutcome, String> {
130        self.send_user_message_outcome_from(message, None)
131    }
132
133    /// [`Self::send_user_message_outcome`], saying who the input is from (turn
134    /// provenance, SPEC_AGENT_SELF_QUIT §6.3) — the Swarm broadcast, which is
135    /// the user's own message (SPEC_SWARM_BROADCAST_AS_USER_MESSAGE §6 q2).
136    /// The label is applied only when this message is the only one written: a
137    /// backlog written with it was queued by automated senders, and a message
138    /// left queued (process starting, restarting or stopping) is written later
139    /// by the drain with whatever else is queued. Both stay unlabelled, as
140    /// every automated delivery is, so the label can never vouch for input
141    /// that isn't the user's.
142    pub fn send_user_message_outcome_from(
143        &self,
144        message: String,
145        origin: Option<crate::backend::blockcontroller::health::TurnInput>,
146    ) -> Result<SendOutcome, String> {
147        // Pre-turn fence — see `send_message`.
148        self.fence_check()?;
149        let json_str = Self::encode_user_message(&message);
150
151        // ONE lock acquisition covers the must-wait check, the enqueue and the
152        // write. Because the watchdog takes this same lock, there is no window
153        // in which a message is enqueued against a queue a concurrent drain
154        // has already finished with.
155        //
156        // `publish_status`/`spawn_status_heartbeat` must NOT be called while
157        // this guard is held — `publish_status` takes `inner` itself and would
158        // deadlock. They run after the guard drops, below.
159        let (written, was_active, still_queued, failure) = {
160            let mut inner = self.inner.lock().unwrap();
161
162            if inner.deferred_deliveries.len() >= MAX_DEFERRED_DELIVERIES {
163                // An explicit error, never a false success. The caller treats
164                // this like any other transient delivery failure and retries.
165                return Err(format!(
166                    "deferred-delivery queue is full ({MAX_DEFERRED_DELIVERIES} messages \
167                     waiting for this agent's process to start up, restart or stop) — retry shortly"
168                ));
169            }
170            // Always enqueue first, then write the whole backlog in order.
171            // Writing this message directly while older ones are queued would
172            // reorder it ahead of them.
173            inner.deferred_deliveries.push_back(json_str);
174
175            // A spawn in flight counts as "wait" rather than as an error. This
176            // is the startup race that used to drop jekts outright
177            // (`SPEC_NO_MIDTURN_DELIVERY_2026_09_23.md` §3.2.1). Not every spawn
178            // ends in a turn — it can fail, or (eager resume) succeed with
179            // nothing to say — so anything left queued here is the watchdog's
180            // to finish, below.
181            let mut failure = None;
182            let mut written = Vec::new();
183            if !self.deferred_must_wait_locked(&inner) {
184                let (w, err) = Self::flush_deferred_locked(&mut inner, &self.block_id);
185                written = w;
186                if let Some(e) = err {
187                    // This call's own message is the newest entry, so it was
188                    // not written. Take it back: this call reports failure,
189                    // so the caller owns the retry. Anything older stays
190                    // queued — it was already accepted, and whoever queued it
191                    // armed the watchdog, so it keeps its retry.
192                    inner.deferred_deliveries.pop_back();
193                    failure = Some(e);
194                }
195            }
196            // Marked under this same guard, so the turn state a concurrent
197            // caller or the turn-end handler reads is never behind the write.
198            // Only a fully successful write of this message alone: with a
199            // failure, the one entry written is an older queued one and this
200            // message was taken back (#4401).
201            let label = if failure.is_none() && written.len() == 1 { origin } else { None };
202            let was_active = (!written.is_empty()).then(|| self.mark_turn_active_locked_from(label));
203            (written, was_active, !inner.deferred_deliveries.is_empty(), failure)
204        };
205
206        // Anything still queued needs a guaranteed way out. Called after EVERY
207        // enqueue that leaves the queue non-empty, so it cannot miss a watchdog
208        // that exited just before.
209        if still_queued {
210            self.ensure_deferred_watchdog();
211        }
212
213        if let Some(was_active) = was_active {
214            // Everything below needs the `inner` lock released:
215            // `publish_status` takes it, and `spawn_status_heartbeat`'s task
216            // does too. The heartbeat is re-armed only when resuming from idle
217            // — a mid-turn send already has one running, and re-spawning per
218            // call would leak duplicate heartbeat tasks.
219            if !was_active {
220                self.spawn_status_heartbeat();
221            }
222            self.publish_status();
223            for line in &written {
224                self.append_delivered_message(line);
225            }
226        }
227        if let Some(e) = failure {
228            return Err(e);
229        }
230        if still_queued {
231            tracing::info!(
232                block_id = %self.block_id,
233                "delivery queued — the agent's process is starting up, restarting or stopping; \
234                 it will be written once the process can take it"
235            );
236            return Ok(SendOutcome::Deferred);
237        }
238        Ok(SendOutcome::Sent)
239    }
240
241    /// Mark the turn active while the caller already holds `inner`, so the
242    /// flip is never behind the write that caused it. Lock order is `inner` →
243    /// `health_monitor`, matching the turn-end handler in `stdout_reader.rs`;
244    /// `health_monitor` never takes `inner`, so this cannot deadlock. Returns
245    /// the pre-call value.
246    fn mark_turn_active_locked(&self) -> bool {
247        self.health_monitor.mark_turn_active_returning_was_active()
248    }
249
250    /// [`Self::mark_turn_active_locked`] with the input's provenance; `None`
251    /// is exactly the unlabelled mark.
252    fn mark_turn_active_locked_from(
253        &self,
254        origin: Option<crate::backend::blockcontroller::health::TurnInput>,
255    ) -> bool {
256        self.health_monitor.mark_turn_active_from(origin)
257    }
258
259    /// Whether a queued message must keep waiting rather than be written now.
260    /// Only states in which the process cannot take a write: what the agent is
261    /// doing (mid-turn or idle) is not one of them
262    /// (`SPEC_JEKT_IMMEDIATE_DELIVERY_2026_09_28.md` §2.1). Shared by the send
263    /// path and the watchdog, so the two can never disagree.
264    ///
265    /// - Another writer owns stdin — see
266    ///   [`Self::stdin_owned_by_another_writer`]; this includes a spawn in
267    ///   flight, whose write `try_write_stdin_locked` would refuse.
268    /// - A committed config restart (`restart_pending`), but only while the
269    ///   process being restarted still holds `stdin_tx`: it is about to be
270    ///   killed, so writes must wait for the replacement. Once it has exited,
271    ///   the ordinary rules apply. The exit handler's respawn
272    ///   (`respawn_after_config_restart`) holds the spawn claim, so the queue
273    ///   keeps waiting for it (a spawn in flight, below), and if no spawn is in
274    ///   flight, whoever was to start one declined or failed. Waiting on
275    ///   `restart_pending` alone then held jekts forever, since only a
276    ///   successful spawn clears it (Korp, 2026-09-29; ReAgent P1 on #3990: a
277    ///   competing spawn claim that fails leaves it set). The watchdog's grace
278    ///   window covers the moment between the exit and the respawn's claim.
279    /// - A requested kill (`stop_pending`), but only while the dying process
280    ///   still holds `stdin_tx`: writes are refused then, and a refusal must
281    ///   not reach an automated caller as an error (reagent P1 on #3562). Once
282    ///   it exits, the ordinary no-process rules apply. A send fails into the
283    ///   caller's respawn fallback, and the watchdog's grace window runs, so
284    ///   nothing parks unreported behind a stop.
285    pub(super) fn deferred_must_wait_locked(&self, inner: &PersistentInner) -> bool {
286        (inner.restart_pending && inner.stdin_tx.is_some())
287            || (inner.stop_pending && inner.stdin_tx.is_some())
288            || Self::stdin_owned_by_another_writer(inner)
289    }
290
291    /// Another writer is feeding stdin, and messages it carries were accepted
292    /// earlier than anything in the deferred queue, so they must not be
293    /// overtaken:
294    ///
295    /// - a spawn is in flight — its seed message and drain go first;
296    /// - a stale-resume retry batch is being replayed (`drain_claim`), possibly
297    ///   into this same live process by a background task writing to the same
298    ///   channel;
299    /// - human messages are queued behind a spawn.
300    ///
301    /// Queued messages count only while a process exists to drain them. A
302    /// drain's "second stall" (`drain_queue_with_claim`) releases its claim
303    /// with leftovers queued and no process. Nothing owns that backlog, and
304    /// treating it as a writer made a send defer instead of failing into
305    /// the no-process fallback, and kept the watchdog resetting its orphan
306    /// timer forever (codex P1 on #3562).
307    ///
308    /// Also part of [`Self::deferred_must_wait_locked`], which every caller of
309    /// [`Self::flush_deferred_locked`] checks first under the same lock.
310    fn stdin_owned_by_another_writer(inner: &PersistentInner) -> bool {
311        inner.spawning_in_progress
312            || inner.drain_claim
313            || (!inner.pending_send_messages.is_empty() && inner.stdin_tx.is_some())
314    }
315
316    /// Write the queued messages to stdin, oldest first, as far as stdin takes
317    /// them. Returns the lines written, in order, and the error that stopped
318    /// it early, if any: the entry that failed stays at the head of the queue
319    /// with everything behind it, so a failed write never drops a message.
320    ///
321    /// The whole backlog at once, not one at a time: nothing is held for what
322    /// the agent is doing any more (`SPEC_JEKT_IMMEDIATE_DELIVERY_2026_09_28.md`
323    /// §2.1), only for whether the process can take a write.
324    ///
325    /// The caller holds `inner` across this, has checked
326    /// [`Self::deferred_must_wait_locked`] under that same guard, and marks the
327    /// turn active before dropping it if anything was written.
328    pub(super) fn flush_deferred_locked(
329        inner: &mut PersistentInner,
330        block_id: &str,
331    ) -> (Vec<String>, Option<String>) {
332        let mut written = Vec::new();
333        while let Some(head) = inner.deferred_deliveries.front().cloned() {
334            if let Err(e) = Self::try_write_stdin_locked(inner, &head) {
335                tracing::warn!(
336                    block_id = %block_id,
337                    error = %e,
338                    queued = inner.deferred_deliveries.len(),
339                    "deferred flush failed; leaving the queue for the watchdog to retry"
340                );
341                return (written, Some(e));
342            }
343            inner.deferred_deliveries.pop_front();
344            written.push(head);
345        }
346        (written, None)
347    }
348
349    /// The turn-boundary decision for a `result` frame, under `inner`: the turn
350    /// is over, and a deferred runtime-config restart may now apply. Returns
351    /// whether it does.
352    ///
353    /// Returns `None`, touching nothing, when `generation` is no longer the
354    /// current one. A fallback respawn can install a new process before the
355    /// old reader drains a buffered `result`. That `result` ends nothing the
356    /// new process is doing, so acting on it would mark the replacement's
357    /// running turn idle, or restart it (codex P1 on #3562).
358    ///
359    /// The deferred restart applies only when nothing is queued. Killing the
360    /// process with an entry still queued would strand it; the flag stays set,
361    /// and the next `result` (the watchdog's write starts a turn) applies it.
362    ///
363    /// `stats`: the `result` frame's figures. They go to the turn the pass
364    /// belonged to in the same step that idles it, so input arriving right
365    /// after (which may open a new turn) can never take them (#4492).
366    pub(super) fn turn_boundary_locked(
367        inner: &mut PersistentInner,
368        health: &TurnActivityTracker,
369        block_id: &str,
370        generation: u64,
371        stats: Option<crate::backend::blockcontroller::health::PassStats>,
372    ) -> Option<bool> {
373        if inner.spawn_generation != generation {
374            tracing::debug!(
375                block_id = %block_id,
376                reader_generation = generation,
377                current_generation = inner.spawn_generation,
378                "result from a replaced process — not a boundary for the current one"
379            );
380            return None;
381        }
382        let apply_deferred_restart = inner.deferred_deliveries.is_empty()
383            && std::mem::replace(&mut inner.restart_when_idle, false);
384        health.end_pass(stats);
385        if apply_deferred_restart {
386            inner.restart_pending = true;
387            // The restart token, set in the acquisition that commits the
388            // restart: nothing later may re-arm it after a Stop cleared it.
389            inner.config_restart_generation = Some(generation);
390        }
391        Some(apply_deferred_restart)
392    }
393
394    /// Make sure a watchdog task is looking after a non-empty deferred queue.
395    ///
396    /// Messages are queued only while the process cannot take a write (see
397    /// [`Self::deferred_must_wait_locked`]), and nothing else writes them once
398    /// it can, so this is how every queued message gets out:
399    ///
400    /// - a message queued behind a spawn — delivered once the process is up,
401    ///   whether or not that spawn starts a turn (eager resume spawns with no
402    ///   seed message);
403    /// - a message queued behind a committed restart or a requested stop —
404    ///   delivered to the replacement process;
405    /// - a write that failed (e.g. the bounded stdin channel was momentarily
406    ///   full) — retried;
407    /// - a message queued behind a spawn that then FAILED, or behind a process
408    ///   that exited and was never replaced — reported stranded after the
409    ///   grace window.
410    ///
411    /// Idempotent: at most one watchdog per controller. A no-op for an
412    /// instance without `set_self_ref` (only tests construct those) or outside
413    /// a Tokio runtime.
414    pub(super) fn ensure_deferred_watchdog(&self) {
415        let Some(ctrl) = self.self_ref.lock().unwrap().as_ref().and_then(|w| w.upgrade()) else {
416            return;
417        };
418        let Ok(runtime) = tokio::runtime::Handle::try_current() else {
419            return;
420        };
421        {
422            let mut inner = self.inner.lock().unwrap();
423            if inner.deferred_watchdog_armed {
424                return;
425            }
426            inner.deferred_watchdog_armed = true;
427        }
428        let weak = Arc::downgrade(&ctrl);
429        drop(ctrl);
430        runtime.spawn(async move {
431            let mut orphaned_ticks = 0u32;
432            loop {
433                tokio::time::sleep(DEFERRED_WATCHDOG_TICK).await;
434                // Hold no strong reference across the sleep, so the watchdog
435                // never keeps a torn-down controller alive.
436                let Some(ctrl) = weak.upgrade() else { return };
437                if ctrl.sweep_deferred_once(&mut orphaned_ticks) == WatchdogStep::Exit {
438                    return;
439                }
440            }
441        });
442    }
443
444    /// One watchdog tick. Split out of the task so tests can drive it
445    /// directly, without a real process or real time.
446    pub(super) fn sweep_deferred_once(&self, orphaned_ticks: &mut u32) -> WatchdogStep {
447        let (released, stranded) = {
448            let mut inner = self.inner.lock().unwrap();
449            if inner.deferred_deliveries.is_empty() {
450                // Disarm under the same lock that guards enqueue: any message
451                // pushed after this point is followed by its own
452                // `ensure_deferred_watchdog`, which will see "disarmed".
453                inner.deferred_watchdog_armed = false;
454                return WatchdogStep::Exit;
455            }
456            if self.deferred_must_wait_locked(&inner) {
457                // Something in flight will bring the process up (or fail into
458                // a state this watchdog sees on a later tick).
459                *orphaned_ticks = 0;
460                return WatchdogStep::Continue;
461            }
462            if inner.stdin_tx.is_none() {
463                // No process and nothing starting one. Give a respawn the
464                // grace window to begin, then stop pretending these will be
465                // delivered.
466                *orphaned_ticks += 1;
467                if *orphaned_ticks < DEFERRED_ORPHAN_GRACE_TICKS {
468                    return WatchdogStep::Continue;
469                }
470                inner.deferred_watchdog_armed = false;
471                (None, inner.deferred_deliveries.drain(..).collect::<Vec<_>>())
472            } else {
473                *orphaned_ticks = 0;
474                // A failed write is transient; whatever is left is retried on
475                // the next tick.
476                let (written, _failed) = Self::flush_deferred_locked(&mut inner, &self.block_id);
477                if written.is_empty() {
478                    return WatchdogStep::Continue;
479                }
480                // Flip inside the lock, exactly as the send path does.
481                (Some((written, self.mark_turn_active_locked())), Vec::new())
482            }
483        };
484
485        if !stranded.is_empty() {
486            Self::log_stranded_deferred(
487                &self.block_id,
488                "no process and no spawn in flight for the whole grace window",
489                &stranded,
490            );
491            return WatchdogStep::Exit;
492        }
493        if let Some((written, was_active)) = released {
494            tracing::info!(
495                block_id = %self.block_id,
496                count = written.len(),
497                "the agent's process can take input again — watchdog delivered the queued messages"
498            );
499            if !was_active {
500                self.spawn_status_heartbeat();
501            }
502            self.publish_status();
503            for line in &written {
504                self.append_delivered_message(line);
505            }
506        }
507        WatchdogStep::Continue
508    }
509
510    /// Report every stranded deferred entry, so a message this controller
511    /// accepted is never quietly discarded
512    /// (`SPEC_NO_MIDTURN_DELIVERY_2026_09_23.md` §4.5). Callers drain the
513    /// queue in the same `inner` acquisition that ends delivery (`stop`,
514    /// `shutdown`, the watchdog), so no flush can take an entry first.
515    ///
516    /// **Known gap:** reporting is currently a loud structured log, not a
517    /// failure routed back to the original sender. The queue stores encoded
518    /// stdin lines with no sender identity attached, so there is nothing to
519    /// address a reply to; carrying that identity through is Phase 3. Until
520    /// then a stranded message is visible in the logs rather than in the
521    /// sender's response — better than silence, short of the spec's intent.
522    ///
523    /// Takes `block_id` rather than `&self` so `shutdown`'s `'static` future,
524    /// which holds no controller reference, can report through it too.
525    pub(super) fn log_stranded_deferred(block_id: &str, reason: &str, stranded: &[String]) {
526        if stranded.is_empty() {
527            return;
528        }
529        tracing::warn!(
530            block_id = %block_id,
531            reason = %reason,
532            stranded = stranded.len(),
533            "deferred messages were accepted but never delivered"
534        );
535        for line in stranded {
536            tracing::warn!(block_id = %block_id, message = %line, "stranded deferred message");
537        }
538    }
539
540    /// Persist a delivered injection to the blockfile WITH a live event.
541    ///
542    /// Unlike `send_message` there is no `agent-message-accepted` pending echo
543    /// to pair with (nothing was typed in the UI), so without this the
544    /// injection is invisible to the human operator: a silent injection, which
545    /// SPEC_JEKT_SECURITY_AND_VISIBILITY §3.1/G1 forbids. The live append
546    /// renders it in the open pane; the persisted line lets `parseHistoryLines`
547    /// rebuild the node on reopen.
548    ///
549    /// Called at *delivery* time, not enqueue time, so the transcript shows the
550    /// message where the agent actually received it. A queued message is
551    /// therefore not yet visible — surfacing "N waiting" is a UI question
552    /// tracked as spec §8 Q3.
553    pub(super) fn append_delivered_message(&self, json_str: &str) {
554        let Some(ref broker) = self.broker else { return };
555        let global_zone =
556            super::super::shell::resolve_global_output_zone(&self.mstore, &self.block_id);
557        super::super::shell::append_output_line(
558            broker,
559            &self.block_id,
560            json_str,
561            self.filestore.as_ref(),
562            global_zone.as_deref(),
563        );
564    }
565
566    /// Answer a parked AskUserQuestion via the Agent SDK **control protocol**.
567    ///
568    /// The CLI asked us with a `can_use_tool` control_request (parked in
569    /// `pending_questions` by the stdout reader); we reply with a
570    /// `control_response` carrying `updatedInput.answers`. This is the ONLY
571    /// mechanism the CLI accepts — delivering a `tool_result` on stdin does NOT
572    /// work (the CLI auto-rejects AskUserQuestion within the turn). `answers` is
573    /// the JSON object mapping each question's text to the selected label(s) or
574    /// free-text. Process must already be running (agent is mid-turn, blocked on
575    /// this answer). Spec: docs/specs/SPEC_AGENT_CONTROL_PROTOCOL_2026_06_15.md §2.3.
576    /// `pending_questions` is in-memory-only, scoped to THIS controller
577    /// instance — a fresh instance (pane reopen, or any process respawn)
578    /// starts with an empty map even though the persisted transcript can
579    /// still show the question as the tail node (deliberately preserved by
580    /// `scrubOrphanedInProgress` as "may still be answerable"). The frontend
581    /// (`useAgentQuestions.ts`'s `SAFE_TO_RETRY_VIA_FOLLOWUP` allowlist)
582    /// matches on this error's text (the "no pending AskUserQuestion" prefix)
583    /// to redeliver as a follow-up message instead of rolling back — keep
584    /// that exact prefix stable if this message ever changes. See
585    /// docs/reports/REPORT_WORKING_STATE_REGRESSION_AND_STUCK_QUESTION_PANEL_2026_07_27.md §2.7/§2.8.
586    pub fn answer_question(&self, tool_use_id: String, answers: serde_json::Value) -> Result<(), String> {
587        let resume_msg = build_answer_resume_message(&answers);
588        self.reply_to_pending_question(
589            tool_use_id,
590            "persistent process not running (cannot deliver answer)",
591            |questions, tool_use_id| {
592                serde_json::json!({
593                    "behavior": "allow",
594                    "updatedInput": { "questions": questions, "answers": answers },
595                    "toolUseID": tool_use_id,
596                })
597            },
598            resume_msg,
599            &ANSWER_DEAD_AIR_LOGS,
600        )
601    }
602
603    /// Decline a parked AskUserQuestion via the Agent SDK **control protocol**
604    /// — the Cancel button / Escape in `AgentQuestionPanel.tsx`. Structurally a
605    /// mirror of `answer_question` (same `pending_questions` lookup/removal,
606    /// same dead-air safety net — the CLI can abandon a pending tool_use whose
607    /// turn already ended regardless of whether the response was an allow or a
608    /// deny), but sends `behavior: "deny"` with a `message` instead of
609    /// `behavior: "allow"` with `updatedInput`. This is a general, documented
610    /// Agent SDK mechanism (`PermissionResult` deny case for the `canUseTool`
611    /// callback) — AskUserQuestion goes through the exact same callback as
612    /// ordinary tool permission requests, confirmed against the official Agent
613    /// SDK docs (code.claude.com/docs/en/agent-sdk/user-input). Spec:
614    /// docs/specs/SPEC_AGENT_CONTROL_PROTOCOL_2026_06_15.md.
615    ///
616    /// See `answer_question`'s doc comment for the `pending_questions`
617    /// in-memory-only caveat and the exact error-prefix stability requirement
618    /// (`useAgentQuestions.ts`'s `SAFE_TO_RETRY_VIA_FOLLOWUP` allowlist matches
619    /// on this method's error text too, since it shares the identical lookup).
620    pub fn deny_question(&self, tool_use_id: String, message: String) -> Result<(), String> {
621        let resume_msg = build_deny_resume_message(&message);
622        self.reply_to_pending_question(
623            tool_use_id,
624            "persistent process not running (cannot deliver decline)",
625            |_questions, tool_use_id| {
626                serde_json::json!({
627                    "behavior": "deny",
628                    "message": message,
629                    "toolUseID": tool_use_id,
630                })
631            },
632            resume_msg,
633            &DENY_DEAD_AIR_LOGS,
634        )
635    }
636
637    /// Removes the parked AskUserQuestion `tool_use_id` and replies to it with
638    /// `response_body(questions, tool_use_id)` via
639    /// [`Self::send_control_response_with_fallback`]. The "no pending
640    /// AskUserQuestion" error prefix is matched by the frontend.
641    fn reply_to_pending_question(
642        &self,
643        tool_use_id: String,
644        not_running_error: &'static str,
645        response_body: impl FnOnce(serde_json::Value, &str) -> serde_json::Value,
646        resume_msg: String,
647        logs: &'static DeadAirLogs,
648    ) -> Result<(), String> {
649        let (request_id, questions, tx) = {
650            let mut inner = self.inner.lock().unwrap();
651            let (rid, qs) = inner
652                .pending_questions
653                .remove(&tool_use_id)
654                .ok_or_else(|| format!(
655                    "no pending AskUserQuestion for tool_use_id {tool_use_id} — this controller \
656                     instance never recorded it (process likely respawned since the question was \
657                     asked, e.g. a pane close/reopen); the caller should redeliver as a follow-up message"
658                ))?;
659            let tx = inner
660                .stdin_tx
661                .as_ref()
662                .ok_or(not_running_error)?
663                .clone();
664            (rid, qs, tx)
665        };
666        let body = response_body(questions, &tool_use_id);
667        self.send_control_response_with_fallback(tx, request_id, body, tool_use_id, resume_msg, logs)
668    }
669
670    /// Sends the `control_response` for a parked `can_use_tool` request and
671    /// arms the dead-air fallback, which re-delivers `resume_msg` as a
672    /// follow-up user turn if no stdout activity follows the response.
673    fn send_control_response_with_fallback(
674        &self,
675        tx: mpsc::Sender<String>,
676        request_id: String,
677        response_body: serde_json::Value,
678        tool_use_id: String,
679        resume_msg: String,
680        logs: &'static DeadAirLogs,
681    ) -> Result<(), String> {
682        let control_response = serde_json::json!({
683            "type": "control_response",
684            "response": {
685                "subtype": "success",
686                "request_id": request_id,
687                "response": response_body,
688            }
689        });
690        // Snapshot stdout activity BEFORE sending, so a fast resume that emits
691        // between the send and the snapshot can't be mistaken for "no
692        // activity".
693        let stdout_seq = Arc::clone(&self.stdout_seq);
694        let before_seq = stdout_seq.load(Ordering::Relaxed);
695
696        tx.try_send(control_response.to_string())
697            .map_err(|e| format!("control_response send failed: {e}"))?;
698
699        // Dead-air safety net. The CLI *abandons* a pending tool_use if its
700        // turn already ended, silently dropping the control_response above
701        // (allow or deny), and the model stalls on an empty message
702        // (SPEC_ASK_USER_QUESTION_2026_06_15.md §9/§10.1). No stdout activity
703        // shortly after means the turn did not resume, so re-deliver the
704        // choice as a follow-up user turn. Gated on stdout activity (every
705        // frame, incl. control frames), so it never double-delivers.
706        let inner = Arc::clone(&self.inner);
707        let block_id = self.block_id.clone();
708        let persist = self.redelivery_persister();
709        tokio::spawn(async move {
710            tokio::time::sleep(std::time::Duration::from_millis(ANSWER_RESUME_FALLBACK_MS)).await;
711            // Any stdout frame since the snapshot means the turn resumed — nothing to do.
712            if stdout_seq.load(Ordering::Relaxed) != before_seq {
713                return;
714            }
715            let line = serde_json::json!({
716                "type": "user",
717                "message": { "role": "user", "content": resume_msg }
718            })
719            .to_string();
720            // The line is recorded in the transcript (spec §6.9) only if it was
721            // written, and not while a restart or stop is committed (the
722            // process is going down and may never read it; History must not
723            // claim a decision it never got). Recorded under the same `inner`
724            // acquisition as the send, so the fallbacks can't interleave with
725            // each other; ordering against a concurrent ordinary send is the
726            // same as between two ordinary sends (each records after its own
727            // send). `persist.write` never takes `inner`.
728            let guard = inner.lock().unwrap();
729            let stdin_tx = guard.stdin_tx.clone();
730            match stdin_tx {
731                Some(stdin_tx) if stdin_tx.try_send(line.clone()).is_ok() => {
732                    if !guard.restart_pending && !guard.stop_pending {
733                        persist.write(&line);
734                    }
735                    drop(guard);
736                    tracing::warn!(
737                        block_id = %block_id,
738                        tool_use_id = %tool_use_id,
739                        fallback_ms = ANSWER_RESUME_FALLBACK_MS,
740                        "{}",
741                        logs.redelivered
742                    );
743                }
744                Some(_) => tracing::warn!(block_id = %block_id, "{}", logs.send_failed),
745                None => tracing::warn!(block_id = %block_id, "{}", logs.not_running),
746            }
747        });
748        Ok(())
749    }
750
751    /// Decide a parked ordinary tool-permission request via the Agent SDK
752    /// **control protocol** — the eventual `AgentDecisionPanel` Allow/Deny
753    /// buttons, once `should_route_to_decision_panel` is flipped on (Phase 2,
754    /// `SPEC_DECISION_PROMPT_2026_04_24.md` / `SPEC_AGENT_CONTROL_PROTOCOL_2026_06_15.md`
755    /// §5). Mirrors `answer_question`/`deny_question` exactly — same
756    /// `pending_*` map lookup/removal, same dead-air safety net — combined
757    /// into one method because the frontend's `tool:decision` RPC already
758    /// carries `outcome` as a single field rather than two separate
759    /// commands.
760    ///
761    /// `outcome` must be `"allow"` or `"deny"` — validated by the caller
762    /// (`websocket.rs`'s `tooldecision` handler) before this is reached, the
763    /// same division of responsibility as that handler's existing `scope`
764    /// validation. An unrecognized value is treated as `"deny"` (fail
765    /// closed) rather than panicking or silently allowing.
766    ///
767    /// On allow, `updatedInput` echoes the ORIGINAL input verbatim — this
768    /// method does not support editing the call before approving it (no UI
769    /// for that exists; `SPEC_DECISION_PROMPT_2026_04_24.md` never scoped
770    /// one). On deny, `feedback` becomes the CLI-facing `message`
771    /// (`SPEC_DECISION_PROMPT`'s G6: "Denials carry user-typed feedback
772    /// verbatim to the agent"); a caller with no feedback gets a generic
773    /// default so the model still learns the call was refused.
774    ///
775    /// See `answer_question`'s doc comment for the `pending_permissions`
776    /// in-memory-only caveat (a fresh controller instance — pane reopen, any
777    /// process respawn — starts with an empty map) and for why the error
778    /// text names the tool_use_id and the likely cause.
779    pub fn decide_tool_permission(
780        &self,
781        tool_use_id: String,
782        outcome: &str,
783        feedback: Option<String>,
784    ) -> Result<(), String> {
785        let (request_id, tool_name, input, tx) = {
786            let mut inner = self.inner.lock().unwrap();
787            let (rid, tool_name, input) = inner
788                .pending_permissions
789                .remove(&tool_use_id)
790                .ok_or_else(|| format!(
791                    "no pending tool-permission request for tool_use_id {tool_use_id} — this \
792                     controller instance never recorded it (process likely respawned since the \
793                     request was made, e.g. a pane close/reopen); the caller should redeliver \
794                     as a follow-up message"
795                ))?;
796            let tx = inner
797                .stdin_tx
798                .as_ref()
799                .ok_or("persistent process not running (cannot deliver decision)")?
800                .clone();
801            (rid, tool_name, input, tx)
802        };
803
804        let allow = outcome == "allow";
805        let response_body = if allow {
806            serde_json::json!({
807                "behavior": "allow",
808                "updatedInput": input,
809                "toolUseID": tool_use_id,
810            })
811        } else {
812            serde_json::json!({
813                "behavior": "deny",
814                "message": feedback.clone().unwrap_or_else(|| "Denied by user.".to_string()),
815                "toolUseID": tool_use_id,
816            })
817        };
818        let resume_msg = build_tool_decision_resume_message(&tool_name, outcome, feedback.as_deref());
819        self.send_control_response_with_fallback(
820            tx,
821            request_id,
822            response_body,
823            tool_use_id,
824            resume_msg,
825            &TOOL_DECISION_DEAD_AIR_LOGS,
826        )
827    }
828
829    /// Push a raw NDJSON line to the live stdin (used to emit control_responses
830    /// from the stdout-reader task, which only holds an `Arc<Mutex<Inner>>`).
831    pub(super) fn push_stdin(inner: &Arc<Mutex<PersistentInner>>, line: String) {
832        let guard = inner.lock().unwrap();
833        if let Some(tx) = guard.stdin_tx.as_ref() {
834            let _ = tx.try_send(line);
835        }
836    }
837
838    /// Handle a control-protocol frame from the CLI's stdout. `control_request`
839    /// of subtype `can_use_tool`: AskUserQuestion is **parked** (the frontend
840    /// panel — rendered from the assistant stream — answers it via
841    /// `answer_question`); every other tool is routed to
842    /// `should_route_to_decision_panel`, which today always says no, so it is
843    /// **auto-allowed** to preserve the current bypass/yolo UX (Phase 1; see
844    /// that function's own doc comment for why Phase 2, #551, isn't simply
845    /// "flip it to true"). `control_response` frames (replies to requests we
846    /// initiate, none today) are logged and dropped. These frames are NOT
847    /// conversation output and never reach the blockfile.
848    /// Spec: docs/specs/SPEC_AGENT_CONTROL_PROTOCOL_2026_06_15.md §4.2.
849    pub(super) fn handle_control_frame(
850        kind: &str,
851        parsed: &serde_json::Value,
852        block_id: &str,
853        inner: &Arc<Mutex<PersistentInner>>,
854    ) {
855        if kind == "control_response" {
856            return;
857        }
858        // control_request
859        let req = match parsed.get("request") {
860            Some(r) => r,
861            None => return,
862        };
863        let subtype = req.get("subtype").and_then(|v| v.as_str()).unwrap_or("");
864        let request_id = parsed
865            .get("request_id")
866            .and_then(|v| v.as_str())
867            .unwrap_or("")
868            .to_string();
869
870        if subtype != "can_use_tool" {
871            tracing::info!(block_id = %block_id, subtype = %subtype, "persistent control_request: unhandled subtype, ignoring");
872            return;
873        }
874
875        let tool_name = req.get("tool_name").and_then(|v| v.as_str()).unwrap_or("");
876        let tool_use_id = req
877            .get("tool_use_id")
878            .and_then(|v| v.as_str())
879            .unwrap_or("")
880            .to_string();
881        let input = req.get("input").cloned().unwrap_or_else(|| serde_json::json!({}));
882
883        if tool_name == "AskUserQuestion" {
884            // Park; the frontend question panel will answer via answer_question().
885            let questions = input
886                .get("questions")
887                .cloned()
888                .unwrap_or_else(|| serde_json::json!([]));
889            {
890                let mut guard = inner.lock().unwrap();
891                guard
892                    .pending_questions
893                    .insert(tool_use_id.clone(), (request_id, questions));
894            }
895            tracing::info!(block_id = %block_id, tool_use_id = %tool_use_id, "AskUserQuestion parked; awaiting user answer");
896        } else if should_route_to_decision_panel(tool_name) {
897            // PHASE2-GATE: unreachable in production today (see that
898            // function's doc comment). Park exactly like AskUserQuestion
899            // above; the eventual AgentDecisionPanel Allow/Deny answers via
900            // `PersistentSubprocessController::decide_tool_permission`.
901            park_tool_permission_request(inner, tool_use_id.clone(), request_id, tool_name.to_string(), input);
902            tracing::info!(block_id = %block_id, tool_use_id = %tool_use_id, tool_name = %tool_name, "tool-permission request parked; awaiting user decision");
903        } else {
904            // Auto-allow every other tool (preserve today's bypass UX).
905            let resp = serde_json::json!({
906                "type": "control_response",
907                "response": {
908                    "subtype": "success",
909                    "request_id": request_id,
910                    "response": { "behavior": "allow", "updatedInput": input }
911                }
912            });
913            Self::push_stdin(inner, resp.to_string());
914        }
915    }
916}
917
918/// The warn-level lines one dead-air fallback logs: re-delivered, stdin send
919/// failed, and skipped because the process is gone.
920struct DeadAirLogs {
921    redelivered: &'static str,
922    send_failed: &'static str,
923    not_running: &'static str,
924}
925
926const ANSWER_DEAD_AIR_LOGS: DeadAirLogs = DeadAirLogs {
927    redelivered: "AskUserQuestion answer did not resume the turn — re-delivered as a follow-up message (dead-air fallback)",
928    send_failed: "AskUserQuestion dead-air fallback: stdin send failed",
929    not_running: "AskUserQuestion dead-air fallback skipped: process not running",
930};
931
932const DENY_DEAD_AIR_LOGS: DeadAirLogs = DeadAirLogs {
933    redelivered: "AskUserQuestion decline did not resume the turn — re-delivered as a follow-up message (dead-air fallback)",
934    send_failed: "AskUserQuestion deny dead-air fallback: stdin send failed",
935    not_running: "AskUserQuestion deny dead-air fallback skipped: process not running",
936};
937
938const TOOL_DECISION_DEAD_AIR_LOGS: DeadAirLogs = DeadAirLogs {
939    redelivered: "tool-permission decision did not resume the turn — re-delivered as a follow-up message (dead-air fallback)",
940    send_failed: "tool-permission dead-air fallback: stdin send failed",
941    not_running: "tool-permission dead-air fallback skipped: process not running",
942};
943
944/// Records a dead-air re-delivery in the transcript. The three dead-air
945/// fallbacks (answer, decline, tool-permission decision) re-send the user's
946/// choice as a follow-up stdin line when the CLI abandoned the pending tool
947/// call; the CLI then writes no tool result, so without this the choice is
948/// gone on reload, absent from History, and lost when the live feed rolls the
949/// turn off (spec §6.9; Codex review of #3703). Written like every other
950/// stdin line (`persist_message_to_blockfile`), after the send succeeded.
951pub(super) struct RedeliveryPersister {
952    broker: Option<Arc<mps::Broker>>,
953    filestore: Option<Arc<FileStore>>,
954    mstore: Option<Arc<Store>>,
955    block_id: String,
956}
957
958impl RedeliveryPersister {
959    pub(super) fn write(&self, line: &str) {
960        let global_zone =
961            super::super::shell::resolve_global_output_zone(&self.mstore, &self.block_id);
962        let record = format!("{line}\n");
963        super::super::shell::persist_user_line(
964            self.broker.as_deref(),
965            &self.block_id,
966            record.as_bytes(),
967            self.filestore.as_ref(),
968            global_zone.as_deref(),
969        );
970    }
971}