Skip to main content

winx_code_agent/daemon/
server.rs

1use std::collections::{HashMap, HashSet, VecDeque};
2use std::io::ErrorKind;
3use std::os::unix::fs::PermissionsExt;
4use std::path::{Path, PathBuf};
5use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
6use std::sync::Arc;
7use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
8
9use tokio::net::{UnixListener, UnixStream};
10use tokio::sync::{watch, Mutex, Notify, RwLock};
11
12use super::protocol::{
13    read_json_frame, write_json_frame, CancelActionParams, ConfigureSessionParams,
14    ConfigureSessionResult, ConfigureSessionTransition, HelloResult, JournalRead,
15    JournalReadParams, RpcError, RpcRequest, RpcResponse, RunActionParams, RunActionResult,
16    SessionInfo, SessionParams, WireShellError, CANCELLABLE_ACTION_RESERVATIONS_CAPABILITY,
17    COMPACT_ACTION_OUTPUT_CAPABILITY, GENERATION_BOUND_ACTIONS_CAPABILITY, MAX_FRAME_BYTES,
18    PROTOCOL_MAJOR, PROTOCOL_MINOR, TYPED_ACTION_RESULT_CAPABILITY,
19};
20use crate::errors::{Result, WinxError};
21use crate::runtime::{lock_session_store, ShellExecutionToken, ShellTarget};
22use crate::state::bash_state::BashState;
23use crate::state::pty::SharedPtyShell;
24use crate::tools::bash_command::{guardian_foreground_shell_needs_reset, ShellDeliveryCursor};
25use crate::types::normalize_thread_id;
26
27type SharedState = Arc<Mutex<Option<BashState>>>;
28
29struct TimedEntry<T> {
30    value: T,
31    last_seen: Instant,
32}
33
34type CachedActionResult = std::result::Result<RunActionResult, String>;
35
36#[derive(Clone)]
37enum ActionCompletion {
38    InFlight(Arc<watch::Sender<Option<CachedActionResult>>>),
39    Complete(Box<CachedActionResult>),
40}
41
42enum ActionReservation {
43    Owner(Arc<watch::Sender<Option<CachedActionResult>>>),
44    Waiter(Arc<watch::Sender<Option<CachedActionResult>>>),
45    Cached(Box<CachedActionResult>),
46    Rejected { error: WinxError, preserve_cancellation: bool },
47}
48
49struct ActionOwnerGuard {
50    completion: Arc<watch::Sender<Option<CachedActionResult>>>,
51    armed: bool,
52}
53
54impl ActionOwnerGuard {
55    fn new(completion: Arc<watch::Sender<Option<CachedActionResult>>>) -> Self {
56        Self { completion, armed: true }
57    }
58
59    fn disarm(&mut self) {
60        self.armed = false;
61    }
62}
63
64impl Drop for ActionOwnerGuard {
65    fn drop(&mut self) {
66        if self.armed {
67            self.completion.send_replace(Some(Err(
68                "single-flight shell action owner disappeared before completion".to_string(),
69            )));
70        }
71    }
72}
73
74struct ActionEntry {
75    fingerprint: Vec<u8>,
76    cancellation_key: Option<String>,
77    completion: ActionCompletion,
78    last_seen: Instant,
79}
80
81struct DaemonSession {
82    state: SharedState,
83    /// Reset/configure takes the writer; actions, snapshots, cursors, and
84    /// interrupts take readers. This binds execution-token validation to the
85    /// exact shell incarnation on which the operation is performed.
86    operation_barrier: Arc<RwLock<()>>,
87    completed: Mutex<HashMap<String, ActionEntry>>,
88    journal: Mutex<OutputJournal>,
89    observed: Mutex<HashMap<String, (u64, String)>>,
90    background_ids: Mutex<HashSet<String>>,
91    command_id: Mutex<Option<String>>,
92    delivery_cursors: Mutex<HashMap<String, TimedEntry<Arc<Mutex<ShellDeliveryCursor>>>>>,
93    launch_reservations: Mutex<HashMap<String, TimedEntry<Arc<AtomicBool>>>>,
94    created_at_unix_ms: u64,
95    last_activity_unix_ms: AtomicU64,
96    last_command_at_unix_ms: AtomicU64,
97    ever_ran_command: AtomicBool,
98    drainer_started: AtomicBool,
99    activity: Notify,
100    session_epoch: Arc<Mutex<String>>,
101}
102
103impl DaemonSession {
104    fn new() -> Self {
105        let now = unix_ms();
106        Self {
107            state: Arc::new(Mutex::new(None)),
108            operation_barrier: Arc::new(RwLock::new(())),
109            completed: Mutex::new(HashMap::new()),
110            journal: Mutex::new(OutputJournal::default()),
111            observed: Mutex::new(HashMap::new()),
112            background_ids: Mutex::new(HashSet::new()),
113            command_id: Mutex::new(None),
114            delivery_cursors: Mutex::new(HashMap::new()),
115            launch_reservations: Mutex::new(HashMap::new()),
116            created_at_unix_ms: now,
117            last_activity_unix_ms: AtomicU64::new(now),
118            last_command_at_unix_ms: AtomicU64::new(0),
119            ever_ran_command: AtomicBool::new(false),
120            drainer_started: AtomicBool::new(false),
121            activity: Notify::new(),
122            session_epoch: Arc::new(Mutex::new(new_epoch())),
123        }
124    }
125
126    fn note_activity(&self, command: bool) {
127        let now = unix_ms();
128        self.last_activity_unix_ms.store(now, Ordering::Release);
129        if command {
130            self.last_command_at_unix_ms.store(now, Ordering::Release);
131            self.ever_ran_command.store(true, Ordering::Release);
132        }
133        self.activity.notify_one();
134    }
135}
136
137fn new_epoch() -> String {
138    format!("{:016x}", rand::random::<u64>())
139}
140
141const MAX_JOURNAL_BYTES: usize = 4 * 1024 * 1024;
142const MAX_SESSION_CACHE_ENTRIES: usize = 256;
143const MAX_IDENTIFIER_BYTES: usize = 256;
144const SESSION_CACHE_TTL: Duration = Duration::from_secs(60 * 60);
145const ACTION_OWNER_TIMEOUT: Duration = Duration::from_secs(11 * 60);
146const DRAIN_CHANGED_INTERVAL: Duration = Duration::from_millis(20);
147const DRAIN_ACTIVE_INTERVAL: Duration = Duration::from_millis(100);
148const DRAIN_IDLE_INTERVAL: Duration = Duration::from_secs(1);
149
150#[derive(Clone, Copy, Default)]
151struct CaptureState {
152    changed: bool,
153    active: bool,
154}
155
156impl CaptureState {
157    fn merge(&mut self, other: Self) {
158        self.changed |= other.changed;
159        self.active |= other.active;
160    }
161
162    fn next_delay(self) -> Duration {
163        if self.changed {
164            DRAIN_CHANGED_INTERVAL
165        } else if self.active {
166            DRAIN_ACTIVE_INTERVAL
167        } else {
168            DRAIN_IDLE_INTERVAL
169        }
170    }
171}
172
173#[derive(Default)]
174struct OutputJournal {
175    chunks: VecDeque<(u64, String)>,
176    cursors: HashMap<String, TimedEntry<u64>>,
177    next_seq: u64,
178    bytes: usize,
179}
180
181fn prune_timed_entries<T>(entries: &mut HashMap<String, TimedEntry<T>>, now: Instant) {
182    entries.retain(|_, entry| now.duration_since(entry.last_seen) <= SESSION_CACHE_TTL);
183}
184
185fn evict_oldest_if_full<T>(entries: &mut HashMap<String, TimedEntry<T>>, incoming: &str) {
186    if entries.contains_key(incoming) || entries.len() < MAX_SESSION_CACHE_ENTRIES {
187        return;
188    }
189    if let Some(oldest) =
190        entries.iter().min_by_key(|(_, entry)| entry.last_seen).map(|(key, _)| key.clone())
191    {
192        entries.remove(&oldest);
193    }
194}
195
196async fn launch_cancellation_flag(
197    session: &DaemonSession,
198    cancellation_key: &str,
199) -> Result<Arc<AtomicBool>> {
200    let now = Instant::now();
201    let mut reservations = session.launch_reservations.lock().await;
202    reservations.retain(|_, entry| {
203        now.duration_since(entry.last_seen) <= SESSION_CACHE_TTL
204            || Arc::strong_count(&entry.value) > 1
205    });
206    if !reservations.contains_key(cancellation_key)
207        && reservations.len() >= MAX_SESSION_CACHE_ENTRIES
208    {
209        let oldest = reservations
210            .iter()
211            .filter(|(_, entry)| {
212                Arc::strong_count(&entry.value) == 1 && !entry.value.load(Ordering::Acquire)
213            })
214            .min_by_key(|(_, entry)| entry.last_seen)
215            .map(|(key, _)| key.clone());
216        if let Some(oldest) = oldest {
217            reservations.remove(&oldest);
218        }
219    }
220    if !reservations.contains_key(cancellation_key)
221        && reservations.len() >= MAX_SESSION_CACHE_ENTRIES
222    {
223        return Err(WinxError::ResourceAllocationError {
224            message: format!(
225                "all {MAX_SESSION_CACHE_ENTRIES} action cancellation reservations are active"
226            ),
227        });
228    }
229    let entry = reservations
230        .entry(cancellation_key.to_string())
231        .or_insert_with(|| TimedEntry { value: Arc::new(AtomicBool::new(false)), last_seen: now });
232    entry.last_seen = now;
233    Ok(Arc::clone(&entry.value))
234}
235
236async fn consume_launch_cancellation_flag(
237    session: &DaemonSession,
238    cancellation_key: &str,
239    expected: &Arc<AtomicBool>,
240) {
241    let mut reservations = session.launch_reservations.lock().await;
242    if reservations.get(cancellation_key).is_some_and(|entry| Arc::ptr_eq(&entry.value, expected)) {
243        reservations.remove(cancellation_key);
244    }
245}
246
247async fn consume_unowned_launch_cancellation_flag(
248    session: &DaemonSession,
249    cancellation_key: Option<&str>,
250) {
251    let Some(cancellation_key) = cancellation_key else {
252        return;
253    };
254
255    // Owners publish their ActionEntry before acquiring the flag. Keep that
256    // registry locked through the cleanup decision so a rejected/replayed
257    // attempt cannot erase a cancellation reserved for an in-flight owner.
258    let completed = session.completed.lock().await;
259    let owned = completed.values().any(|entry| {
260        entry.cancellation_key.as_deref() == Some(cancellation_key)
261            && matches!(entry.completion, ActionCompletion::InFlight(_))
262            && !action_entry_is_terminal(entry)
263    });
264    if owned {
265        return;
266    }
267    let mut reservations = session.launch_reservations.lock().await;
268    if reservations.get(cancellation_key).is_some_and(|entry| Arc::strong_count(&entry.value) == 1)
269    {
270        reservations.remove(cancellation_key);
271    }
272}
273
274fn validate_identifier(label: &str, value: &str) -> Result<()> {
275    if value.len() > MAX_IDENTIFIER_BYTES {
276        return Err(WinxError::InvalidInput(format!(
277            "{label} exceeds the {MAX_IDENTIFIER_BYTES}-byte limit"
278        )));
279    }
280    Ok(())
281}
282
283impl OutputJournal {
284    fn append(&mut self, output: String) {
285        if output.is_empty() {
286            return;
287        }
288        self.next_seq = self.next_seq.saturating_add(1);
289        self.bytes = self.bytes.saturating_add(output.len());
290        self.chunks.push_back((self.next_seq, output));
291        while self.bytes > MAX_JOURNAL_BYTES && self.chunks.len() > 1 {
292            if let Some((_, removed)) = self.chunks.pop_front() {
293                self.bytes = self.bytes.saturating_sub(removed.len());
294            }
295        }
296    }
297
298    fn read(&mut self, consumer_id: &str) -> JournalRead {
299        let now = Instant::now();
300        prune_timed_entries(&mut self.cursors, now);
301        evict_oldest_if_full(&mut self.cursors, consumer_id);
302
303        let oldest = self.chunks.front().map_or(self.next_seq.saturating_add(1), |(seq, _)| *seq);
304        let cursor = self.cursors.get(consumer_id).map_or(0, |entry| entry.value);
305        let gap = cursor.saturating_add(1) < oldest;
306        let mut output = String::new();
307        let mut next_seq = cursor;
308        for (seq, chunk) in self.chunks.iter().filter(|(seq, _)| *seq > cursor) {
309            output.push_str(chunk);
310            next_seq = *seq;
311        }
312        self.cursors
313            .insert(consumer_id.to_string(), TimedEntry { value: next_seq, last_seen: now });
314        JournalRead { output, next_seq, gap }
315    }
316}
317
318/// Long-lived JSON-RPC shell owner listening on a Unix-domain socket.
319pub struct DaemonServer {
320    listener: UnixListener,
321    socket_path: PathBuf,
322    sessions: Arc<Mutex<HashMap<String, Arc<DaemonSession>>>>,
323    epoch: String,
324}
325
326impl DaemonServer {
327    pub async fn bind(socket_path: impl AsRef<Path>) -> Result<Self> {
328        let socket_path = socket_path.as_ref().to_path_buf();
329        let parent = socket_path.parent().ok_or_else(|| {
330            WinxError::ConfigurationError("daemon socket must have a parent directory".to_string())
331        })?;
332        tokio::fs::create_dir_all(parent).await?;
333        tokio::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700)).await?;
334
335        if tokio::fs::try_exists(&socket_path).await? {
336            if UnixStream::connect(&socket_path).await.is_ok() {
337                return Err(WinxError::ConfigurationError(format!(
338                    "a winxd instance is already listening at {}",
339                    socket_path.display()
340                )));
341            }
342            tokio::fs::remove_file(&socket_path).await?;
343        }
344
345        let listener = UnixListener::bind(&socket_path)?;
346        tokio::fs::set_permissions(&socket_path, std::fs::Permissions::from_mode(0o600)).await?;
347        let epoch = format!("{:016x}", rand::random::<u64>());
348        Ok(Self { listener, socket_path, sessions: Arc::new(Mutex::new(HashMap::new())), epoch })
349    }
350
351    pub async fn serve(self) -> Result<()> {
352        loop {
353            let (stream, _) = self.listener.accept().await?;
354            if !same_uid(&stream)? {
355                tracing::warn!("Rejected winxd connection from a different uid");
356                continue;
357            }
358            let sessions = self.sessions.clone();
359            let epoch = self.epoch.clone();
360            tokio::spawn(async move {
361                if let Err(error) = serve_connection(stream, sessions, epoch).await {
362                    tracing::debug!("winxd client disconnected: {error}");
363                }
364            });
365        }
366    }
367}
368
369impl Drop for DaemonServer {
370    fn drop(&mut self) {
371        let _ = std::fs::remove_file(&self.socket_path);
372    }
373}
374
375async fn serve_connection(
376    mut stream: UnixStream,
377    sessions: Arc<Mutex<HashMap<String, Arc<DaemonSession>>>>,
378    epoch: String,
379) -> Result<()> {
380    loop {
381        let request: RpcRequest = match read_json_frame(&mut stream).await {
382            Ok(request) => request,
383            Err(error) if error.kind() == ErrorKind::UnexpectedEof => return Ok(()),
384            Err(error) => return Err(error.into()),
385        };
386        let response = dispatch(request, &sessions, &epoch).await;
387        write_json_frame(&mut stream, &response).await?;
388    }
389}
390
391#[allow(clippy::too_many_lines)]
392async fn dispatch(
393    request: RpcRequest,
394    sessions: &Arc<Mutex<HashMap<String, Arc<DaemonSession>>>>,
395    epoch: &str,
396) -> RpcResponse {
397    if request.jsonrpc != "2.0" {
398        return rpc_error(request.id, -32600, "JSON-RPC version must be 2.0");
399    }
400
401    match request.method.as_str() {
402        "winx.hello" | "session.negotiate" => rpc_result(
403            request.id,
404            &HelloResult {
405                protocol_major: PROTOCOL_MAJOR,
406                protocol_minor: PROTOCOL_MINOR,
407                capabilities: vec![
408                    "session.configure".to_string(),
409                    "session.negotiate".to_string(),
410                    "shell.run_action".to_string(),
411                    TYPED_ACTION_RESULT_CAPABILITY.to_string(),
412                    COMPACT_ACTION_OUTPUT_CAPABILITY.to_string(),
413                    GENERATION_BOUND_ACTIONS_CAPABILITY.to_string(),
414                    CANCELLABLE_ACTION_RESERVATIONS_CAPABILITY.to_string(),
415                    "session.list".to_string(),
416                    "session.read_output".to_string(),
417                    "session.kill".to_string(),
418                    "multi_consumer_cursors".to_string(),
419                    "idempotency".to_string(),
420                    "session_activity_timestamps".to_string(),
421                    "attach_or_create".to_string(),
422                ],
423                max_frame_bytes: MAX_FRAME_BYTES,
424                daemon_epoch: epoch.to_string(),
425                daemon_pid: std::process::id(),
426            },
427        ),
428        "session.configure" => {
429            let params: ConfigureSessionParams = match serde_json::from_value(request.params) {
430                Ok(params) => params,
431                Err(error) => return rpc_error(request.id, -32602, &error.to_string()),
432            };
433            match configure_session(sessions, params).await {
434                Ok(result) => rpc_result(request.id, &result),
435                Err(error) => rpc_error(request.id, -32603, &error.to_string()),
436            }
437        }
438        "shell.run_action" => {
439            let params: RunActionParams = match serde_json::from_value(request.params) {
440                Ok(params) => params,
441                Err(error) => return rpc_error(request.id, -32602, &error.to_string()),
442            };
443            match run_action(sessions, params, epoch).await {
444                Ok(result) => rpc_result(request.id, &result),
445                Err(error) => rpc_error(request.id, -32603, &error.to_string()),
446            }
447        }
448        "shell.cancel_action" => {
449            let params: CancelActionParams = match serde_json::from_value(request.params) {
450                Ok(params) => params,
451                Err(error) => return rpc_error(request.id, -32602, &error.to_string()),
452            };
453            if params.expected_guardian_epoch.as_deref().is_some_and(|expected| expected != epoch) {
454                return rpc_result(request.id, &false);
455            }
456            if let Err(error) = validate_identifier("cancellation_key", &params.cancellation_key) {
457                return rpc_error(request.id, -32602, &error.to_string());
458            }
459            let session =
460                sessions.lock().await.get(&normalize_thread_id(&params.thread_id)).cloned();
461            match session {
462                Some(session) => {
463                    match launch_cancellation_flag(&session, &params.cancellation_key).await {
464                        Ok(flag) => {
465                            flag.store(true, Ordering::SeqCst);
466                            rpc_result(request.id, &true)
467                        }
468                        Err(error) => rpc_error(request.id, -32603, &error.to_string()),
469                    }
470                }
471                None => rpc_result(request.id, &false),
472            }
473        }
474        "session.list" => {
475            let entries = sessions.lock().await.values().cloned().collect::<Vec<_>>();
476            let mut infos = Vec::with_capacity(entries.len());
477            for session in entries {
478                if let Some(info) = session_info(&session).await {
479                    infos.push(info);
480                }
481            }
482            infos.sort_by(|left, right| left.thread_id.cmp(&right.thread_id));
483            rpc_result(request.id, &infos)
484        }
485        "session.info" => {
486            let params: SessionParams = match serde_json::from_value(request.params) {
487                Ok(params) => params,
488                Err(error) => return rpc_error(request.id, -32602, &error.to_string()),
489            };
490            let session =
491                sessions.lock().await.get(&normalize_thread_id(&params.thread_id)).cloned();
492            match session {
493                Some(session) => match session_info(&session).await {
494                    Some(info) => rpc_result(request.id, &info),
495                    None => rpc_error(request.id, -32004, "session not initialized"),
496                },
497                None => rpc_error(request.id, -32004, "session not found"),
498            }
499        }
500        "session.read_output" => {
501            let params: JournalReadParams = match serde_json::from_value(request.params) {
502                Ok(params) => params,
503                Err(error) => return rpc_error(request.id, -32602, &error.to_string()),
504            };
505            if let Err(error) = validate_identifier("consumer_id", &params.consumer_id) {
506                return rpc_error(request.id, -32602, &error.to_string());
507            }
508            let thread_id = normalize_thread_id(&params.thread_id);
509            let session = sessions.lock().await.get(&thread_id).cloned();
510            match session {
511                Some(session) => {
512                    session.note_activity(false);
513                    capture_session_outputs(&session).await;
514                    let read = session.journal.lock().await.read(&params.consumer_id);
515                    rpc_result(request.id, &read)
516                }
517                None => rpc_error(request.id, -32004, "session not found"),
518            }
519        }
520        "session.kill" => {
521            let params: SessionParams = match serde_json::from_value(request.params) {
522                Ok(params) => params,
523                Err(error) => return rpc_error(request.id, -32602, &error.to_string()),
524            };
525            let session = sessions.lock().await.remove(&normalize_thread_id(&params.thread_id));
526            match session {
527                Some(session) => {
528                    let _operation = session.operation_barrier.write().await;
529                    let background_ids = session.background_ids.lock().await.clone();
530                    {
531                        let mut store = lock_session_store();
532                        for id in background_ids {
533                            store.remove_shell(&id);
534                        }
535                    }
536                    *session.state.lock().await = None;
537                    rpc_result(request.id, &true)
538                }
539                None => rpc_result(request.id, &false),
540            }
541        }
542        "session.interrupt" => {
543            let params: SessionParams = match serde_json::from_value(request.params) {
544                Ok(params) => params,
545                Err(error) => return rpc_error(request.id, -32602, &error.to_string()),
546            };
547            let session =
548                sessions.lock().await.get(&normalize_thread_id(&params.thread_id)).cloned();
549            match session {
550                Some(session) => match interrupt_session(&session, &params, epoch).await {
551                    Ok(interrupted) => rpc_result(request.id, &interrupted),
552                    Err(error) => rpc_error(request.id, -32005, &error.to_string()),
553                },
554                None => rpc_error(request.id, -32004, "session not found"),
555            }
556        }
557        _ => rpc_error(request.id, -32601, "method not found"),
558    }
559}
560
561async fn configure_session(
562    sessions: &Arc<Mutex<HashMap<String, Arc<DaemonSession>>>>,
563    params: ConfigureSessionParams,
564) -> Result<ConfigureSessionResult> {
565    let thread_id = normalize_thread_id(&params.snapshot.chat_id);
566    if thread_id.is_empty() {
567        return Err(WinxError::ThreadIdMismatch(
568            "daemon session configuration requires an explicit thread_id".to_string(),
569        ));
570    }
571
572    let (session, entry_created) = {
573        let mut sessions = sessions.lock().await;
574        match params.transition {
575            ConfigureSessionTransition::FirstCall => match sessions.entry(thread_id) {
576                std::collections::hash_map::Entry::Occupied(entry) => (entry.get().clone(), false),
577                std::collections::hash_map::Entry::Vacant(entry) => {
578                    let session = Arc::new(DaemonSession::new());
579                    entry.insert(session.clone());
580                    (session, true)
581                }
582            },
583            _ => (
584                sessions.get(&thread_id).cloned().ok_or(WinxError::BashStateNotInitialized)?,
585                false,
586            ),
587        }
588    };
589    ensure_drainer(&session);
590    session.note_activity(false);
591    let _operation = session.operation_barrier.write().await;
592
593    let (attach_hint, snapshot, attached_existing) = {
594        let mut guard = session.state.lock().await;
595        let mut attached_existing = false;
596        match params.transition {
597            ConfigureSessionTransition::FirstCall => {
598                if entry_created || guard.is_none() {
599                    let mut state = BashState::new();
600                    state.apply_snapshot(&params.snapshot);
601                    if state.cwd.exists() {
602                        state.init_pty_shell().await?;
603                    }
604                    *guard = Some(state);
605                } else {
606                    // Attach-or-create: preserve the guardian-owned PTY, cwd, mode,
607                    // output journal, and command state across adapter reconnects.
608                    attached_existing = true;
609                }
610            }
611            ConfigureSessionTransition::ModeChange => {
612                let state = guard.as_mut().ok_or(WinxError::BashStateNotInitialized)?;
613                let daemon_cwd = state.cwd.clone();
614                state.apply_snapshot(&params.snapshot);
615                state.cwd = daemon_cwd;
616            }
617            ConfigureSessionTransition::Reset => {
618                let state = guard.as_mut().ok_or(WinxError::BashStateNotInitialized)?;
619                let daemon_cwd = state.cwd.clone();
620                state.apply_snapshot(&params.snapshot);
621                state.cwd = daemon_cwd;
622                state.init_pty_shell().await?;
623                *session.session_epoch.lock().await = new_epoch();
624            }
625            ConfigureSessionTransition::WorkspaceChange => {
626                let state = guard.as_mut().ok_or(WinxError::BashStateNotInitialized)?;
627                state.apply_snapshot(&params.snapshot);
628            }
629        }
630        let state = guard.as_ref().ok_or(WinxError::BashStateNotInitialized)?;
631        let attach_hint = {
632            let shell = state.pty_shell.lock().await;
633            shell.as_ref().and_then(|shell| shell.attach_hint.clone())
634        };
635        (attach_hint, state.snapshot(), attached_existing)
636    };
637
638    if matches!(params.transition, ConfigureSessionTransition::Reset)
639        || (matches!(params.transition, ConfigureSessionTransition::FirstCall)
640            && !attached_existing)
641    {
642        *session.command_id.lock().await = None;
643        // Idempotency entries intentionally survive reset. A response-lost
644        // owner or waiter must still observe the original operation result;
645        // its execution token identifies the old incarnation unambiguously.
646        session.delivery_cursors.lock().await.clear();
647        session.observed.lock().await.remove("main");
648    }
649    capture_session_outputs_locked(&session).await;
650    session.activity.notify_one();
651    Ok(ConfigureSessionResult { attach_hint, snapshot: Some(snapshot), attached_existing })
652}
653
654#[allow(clippy::too_many_lines)] // session setup, execution, capture, and idempotent caching
655async fn run_action(
656    sessions: &Arc<Mutex<HashMap<String, Arc<DaemonSession>>>>,
657    mut params: RunActionParams,
658    guardian_epoch: &str,
659) -> Result<RunActionResult> {
660    validate_identifier("request_key", &params.request_key)?;
661    validate_identifier("consumer_id", &params.consumer_id)?;
662    if let Some(key) = params.options.cancellation_key.as_deref() {
663        validate_identifier("cancellation_key", key)?;
664    }
665    let thread_id = normalize_thread_id(&params.command.thread_id);
666    if thread_id.is_empty() {
667        return Err(WinxError::ThreadIdMismatch(
668            "daemon requests require an explicit thread_id".to_string(),
669        ));
670    }
671
672    let session = {
673        let mut sessions = sessions.lock().await;
674        sessions.entry(thread_id.clone()).or_insert_with(|| Arc::new(DaemonSession::new())).clone()
675    };
676    let cancellation_key = params.options.cancellation_key.clone();
677    let fingerprint = canonical_action_fingerprint(&params)?;
678
679    let request_key = params.request_key.clone();
680    let reservation = {
681        let mut completed = session.completed.lock().await;
682        let now = Instant::now();
683        prune_action_entries(&mut completed, now);
684        if let Some(entry) = completed.get_mut(&request_key) {
685            if entry.fingerprint == fingerprint {
686                entry.last_seen = now;
687                match &entry.completion {
688                    ActionCompletion::Complete(result) => {
689                        ActionReservation::Cached(Box::new((**result).clone()))
690                    }
691                    ActionCompletion::InFlight(sender) => {
692                        ActionReservation::Waiter(Arc::clone(sender))
693                    }
694                }
695            } else {
696                let preserve_cancellation = entry.cancellation_key == cancellation_key
697                    && matches!(entry.completion, ActionCompletion::InFlight(_))
698                    && !action_entry_is_terminal(entry);
699                ActionReservation::Rejected {
700                    error: WinxError::InvalidInput(
701                        "request_key was reused with different shell action parameters".to_string(),
702                    ),
703                    preserve_cancellation,
704                }
705            }
706        } else if evict_oldest_action_if_full(&mut completed) {
707            let completion = Arc::new(watch::channel(None).0);
708            completed.insert(
709                request_key.clone(),
710                ActionEntry {
711                    fingerprint,
712                    cancellation_key: cancellation_key.clone(),
713                    completion: ActionCompletion::InFlight(Arc::clone(&completion)),
714                    last_seen: now,
715                },
716            );
717            ActionReservation::Owner(completion)
718        } else {
719            ActionReservation::Rejected {
720                error: WinxError::ResourceAllocationError {
721                    message: format!(
722                        "all {MAX_SESSION_CACHE_ENTRIES} shell action reservations are in flight; retry after one completes"
723                    ),
724                },
725                preserve_cancellation: false,
726            }
727        }
728    };
729
730    match reservation {
731        ActionReservation::Cached(result) => {
732            consume_unowned_launch_cancellation_flag(&session, cancellation_key.as_deref()).await;
733            cached_action_result(*result)
734        }
735        ActionReservation::Rejected { error, preserve_cancellation } => {
736            if !preserve_cancellation {
737                consume_unowned_launch_cancellation_flag(&session, cancellation_key.as_deref())
738                    .await;
739            }
740            Err(error)
741        }
742        ActionReservation::Waiter(completion) => {
743            wait_for_action_completion(completion.subscribe()).await
744        }
745        ActionReservation::Owner(completion) => {
746            let launch_reservation = if let Some(key) = cancellation_key {
747                match launch_cancellation_flag(&session, &key).await {
748                    Ok(flag) => {
749                        params.options.launch_cancelled = Some(Arc::clone(&flag));
750                        Some((key, flag))
751                    }
752                    Err(error) => {
753                        complete_action(
754                            &session,
755                            &request_key,
756                            Err(error.to_string()),
757                            &completion,
758                        )
759                        .await;
760                        return Err(error);
761                    }
762                }
763            } else {
764                None
765            };
766            let owner_session = Arc::clone(&session);
767            let owner_epoch = guardian_epoch.to_string();
768            let owner_completion = Arc::clone(&completion);
769            tokio::spawn(async move {
770                let mut owner_guard = ActionOwnerGuard::new(Arc::clone(&owner_completion));
771                let outcome = match tokio::time::timeout(
772                    ACTION_OWNER_TIMEOUT,
773                    execute_action(&owner_session, params, &owner_epoch),
774                )
775                .await
776                {
777                    Ok(outcome) => outcome,
778                    Err(_) => Err(WinxError::CommandTimeout {
779                        command: "guardian shell action".to_string(),
780                        timeout_seconds: ACTION_OWNER_TIMEOUT.as_secs(),
781                    }),
782                };
783                if let Some((key, flag)) = launch_reservation {
784                    consume_launch_cancellation_flag(&owner_session, &key, &flag).await;
785                }
786                let cached = outcome.map_err(|error| error.to_string());
787                complete_action(&owner_session, &request_key, cached, &owner_completion).await;
788                owner_guard.disarm();
789            });
790            wait_for_action_completion(completion.subscribe()).await
791        }
792    }
793}
794
795fn cached_action_result(cached: CachedActionResult) -> Result<RunActionResult> {
796    cached.map_err(WinxError::CommandExecutionError)
797}
798
799async fn wait_for_action_completion(
800    mut completion: watch::Receiver<Option<CachedActionResult>>,
801) -> Result<RunActionResult> {
802    loop {
803        if let Some(result) = completion.borrow().clone() {
804            return cached_action_result(result);
805        }
806        if completion.changed().await.is_err() {
807            return Err(WinxError::CommandExecutionError(
808                "single-flight shell action owner disappeared".to_string(),
809            ));
810        }
811    }
812}
813
814async fn complete_action(
815    session: &DaemonSession,
816    request_key: &str,
817    cached: CachedActionResult,
818    completion: &watch::Sender<Option<CachedActionResult>>,
819) {
820    {
821        let mut completed = session.completed.lock().await;
822        if let Some(entry) = completed.get_mut(request_key) {
823            entry.completion = ActionCompletion::Complete(Box::new(cached.clone()));
824            entry.last_seen = Instant::now();
825        }
826    }
827    completion.send_replace(Some(cached));
828}
829
830fn canonical_action_fingerprint(params: &RunActionParams) -> Result<Vec<u8>> {
831    let value = serde_json::to_value((
832        &params.snapshot,
833        &params.command,
834        &params.consumer_id,
835        &params.options,
836    ))
837    .map_err(|error| WinxError::SerializationError(error.to_string()))?;
838    serde_json::to_vec(&canonical_json(value))
839        .map_err(|error| WinxError::SerializationError(error.to_string()))
840}
841
842fn canonical_json(value: serde_json::Value) -> serde_json::Value {
843    match value {
844        serde_json::Value::Array(values) => {
845            serde_json::Value::Array(values.into_iter().map(canonical_json).collect())
846        }
847        serde_json::Value::Object(values) => {
848            let mut entries = values.into_iter().collect::<Vec<_>>();
849            entries.sort_by(|(left, _), (right, _)| left.cmp(right));
850            serde_json::Value::Object(
851                entries.into_iter().map(|(key, value)| (key, canonical_json(value))).collect(),
852            )
853        }
854        scalar => scalar,
855    }
856}
857
858#[allow(clippy::too_many_lines)]
859async fn execute_action(
860    session: &Arc<DaemonSession>,
861    params: RunActionParams,
862    guardian_epoch: &str,
863) -> Result<RunActionResult> {
864    let foreground_command = matches!(
865        &params.command.action_json,
866        crate::types::BashCommandAction::Command { is_background: false, .. }
867    );
868    // Foreground preflight takes the writer before state, cursor, or command
869    // gates. Other actions take a reader. This global order avoids upgrading a
870    // reader while another reader is blocked on a lock held by this action.
871    let (write_operation, read_operation) = if foreground_command {
872        (Some(Arc::clone(&session.operation_barrier).write_owned().await), None)
873    } else {
874        (None, Some(Arc::clone(&session.operation_barrier).read_owned().await))
875    };
876    if params
877        .options
878        .expected_guardian_epoch
879        .as_deref()
880        .is_some_and(|expected| expected != guardian_epoch)
881        || params
882            .options
883            .expected_execution
884            .as_ref()
885            .is_some_and(|expected| expected.guardian_epoch != guardian_epoch)
886    {
887        return Err(WinxError::InvalidInput(
888            "execution precondition refers to a different guardian epoch".to_string(),
889        ));
890    }
891    let session_epoch = session.session_epoch.lock().await.clone();
892    if params.options.expected_execution.as_ref().is_some_and(|expected| {
893        expected.session_epoch != session_epoch
894            || params
895                .options
896                .expected_generation
897                .is_some_and(|generation| generation != expected.generation)
898    }) {
899        return Err(WinxError::InvalidInput(
900            "execution precondition refers to a different session incarnation".to_string(),
901        ));
902    }
903    let is_command =
904        matches!(&params.command.action_json, crate::types::BashCommandAction::Command { .. });
905    ensure_drainer(session);
906    session.note_activity(is_command);
907
908    let preflight_reset = {
909        let mut guard = session.state.lock().await;
910        let state = if let Some(state) = guard.as_mut() {
911            let daemon_cwd = state.cwd.clone();
912            state.apply_snapshot(&params.snapshot);
913            state.cwd = daemon_cwd;
914            state
915        } else {
916            let mut state = BashState::new();
917            state.apply_snapshot(&params.snapshot);
918            guard.insert(state)
919        };
920        let needs_shell = state.pty_shell.lock().await.is_none();
921        if needs_shell && state.cwd.exists() {
922            state.init_pty_shell().await?;
923        }
924        if foreground_command && !params.options.is_launch_cancelled() {
925            guardian_foreground_shell_needs_reset(state, &params.options).await?
926        } else {
927            false
928        }
929    };
930    if preflight_reset {
931        let mut guard = session.state.lock().await;
932        let state = guard.as_mut().ok_or(WinxError::BashStateNotInitialized)?;
933        *session.session_epoch.lock().await = new_epoch();
934        if let Err(error) = state.init_pty_shell().await {
935            tracing::warn!(%error, "failed to reset guardian PTY during exclusive preflight");
936        }
937    }
938    let _operation = if let Some(operation) = write_operation {
939        Some(operation.downgrade())
940    } else {
941        read_operation
942    };
943
944    if is_command {
945        *session.command_id.lock().await = Some(params.request_key.clone());
946    }
947    let consumer_id = if params.consumer_id.is_empty() {
948        "legacy-adapter".to_string()
949    } else {
950        params.consumer_id
951    };
952    let cursor_key = delivery_cursor_key(&consumer_id, &params.command.action_json);
953    let cursor = {
954        let now = Instant::now();
955        let mut cursors = session.delivery_cursors.lock().await;
956        prune_timed_entries(&mut cursors, now);
957        evict_oldest_if_full(&mut cursors, &cursor_key);
958        let entry = cursors.entry(cursor_key).or_insert_with(|| TimedEntry {
959            value: Arc::new(Mutex::new(ShellDeliveryCursor::default())),
960            last_seen: now,
961        });
962        entry.last_seen = now;
963        entry.value.clone()
964    };
965    let outcome = crate::tools::bash_command::handle_embedded_tool_call_with_cursor_detailed(
966        &session.state,
967        params.command,
968        &cursor,
969        params.options,
970        foreground_command,
971    )
972    .await;
973    let session_epoch = session.session_epoch.lock().await.clone();
974    if let Ok(result) = &outcome {
975        if let Some(id) = result.result.state.background_id.as_ref() {
976            session.background_ids.lock().await.insert(id.clone());
977        }
978    }
979    capture_session_outputs_locked(session).await;
980    let snapshot =
981        session.state.lock().await.as_ref().ok_or(WinxError::BashStateNotInitialized)?.snapshot();
982    let result = match outcome {
983        Ok(result) => {
984            let command_generation = result.command_generation;
985            let execution_token = command_generation.map(|generation| ShellExecutionToken {
986                guardian_epoch: guardian_epoch.to_string(),
987                session_epoch,
988                generation,
989            });
990            // Compact and legacy are mutually exclusive on the wire. Compact
991            // rendering leaves the stable legacy string empty at runtime.
992            let (output, compact_output) = match result.compact_output {
993                Some(compact) => (None, Some(compact)),
994                None => (Some(result.result.output), None),
995            };
996            RunActionResult {
997                output,
998                compact_output,
999                command_generation,
1000                execution_token,
1001                output_truncated: result.output_truncated,
1002                state: Some(result.result.state),
1003                snapshot,
1004                error: None,
1005            }
1006        }
1007        Err(error) => RunActionResult {
1008            output: None,
1009            compact_output: None,
1010            command_generation: None,
1011            execution_token: None,
1012            output_truncated: false,
1013            state: None,
1014            snapshot,
1015            error: Some(to_wire_error(error)),
1016        },
1017    };
1018
1019    session.activity.notify_one();
1020    Ok(result)
1021}
1022
1023fn prune_action_entries(entries: &mut HashMap<String, ActionEntry>, now: Instant) {
1024    entries.retain(|_, entry| {
1025        !action_entry_is_terminal(entry) || now.duration_since(entry.last_seen) <= SESSION_CACHE_TTL
1026    });
1027}
1028
1029fn action_entry_is_terminal(entry: &ActionEntry) -> bool {
1030    match &entry.completion {
1031        ActionCompletion::Complete(_) => true,
1032        ActionCompletion::InFlight(completion) => completion.borrow().is_some(),
1033    }
1034}
1035
1036fn evict_oldest_action_if_full(entries: &mut HashMap<String, ActionEntry>) -> bool {
1037    if entries.len() < MAX_SESSION_CACHE_ENTRIES {
1038        return true;
1039    }
1040    if let Some(oldest) = entries
1041        .iter()
1042        .filter(|(_, entry)| action_entry_is_terminal(entry))
1043        .min_by_key(|(_, entry)| entry.last_seen)
1044        .map(|(key, _)| key.clone())
1045    {
1046        entries.remove(&oldest);
1047    }
1048    entries.len() < MAX_SESSION_CACHE_ENTRIES
1049}
1050
1051fn delivery_cursor_key(consumer_id: &str, action: &crate::types::BashCommandAction) -> String {
1052    let background_id = match action {
1053        crate::types::BashCommandAction::StatusCheck { bg_command_id, .. }
1054        | crate::types::BashCommandAction::SendText { bg_command_id, .. }
1055        | crate::types::BashCommandAction::SendSpecials { bg_command_id, .. }
1056        | crate::types::BashCommandAction::SendAscii { bg_command_id, .. }
1057        | crate::types::BashCommandAction::Screen { bg_command_id, .. }
1058        | crate::types::BashCommandAction::WaitForTurn { bg_command_id, .. } => {
1059            bg_command_id.as_deref()
1060        }
1061        crate::types::BashCommandAction::Command { .. } => None,
1062    };
1063    match background_id {
1064        Some(id) => format!("{consumer_id}:background:{id}"),
1065        None => format!("{consumer_id}:main"),
1066    }
1067}
1068
1069fn ensure_drainer(session: &Arc<DaemonSession>) {
1070    if session.drainer_started.swap(true, Ordering::AcqRel) {
1071        session.activity.notify_one();
1072        return;
1073    }
1074    let session = session.clone();
1075    tokio::spawn(async move {
1076        loop {
1077            if Arc::strong_count(&session) == 1 {
1078                return;
1079            }
1080            let capture = capture_session_outputs(&session).await;
1081            tokio::select! {
1082                () = tokio::time::sleep(capture.next_delay()) => {}
1083                () = session.activity.notified() => {}
1084            }
1085        }
1086    });
1087}
1088
1089async fn capture_session_outputs(session: &Arc<DaemonSession>) -> CaptureState {
1090    let _operation = session.operation_barrier.read().await;
1091    capture_session_outputs_locked(session).await
1092}
1093
1094async fn capture_session_outputs_locked(session: &Arc<DaemonSession>) -> CaptureState {
1095    let (thread_id, main) = {
1096        let state = session.state.lock().await;
1097        let Some(state) = state.as_ref() else { return CaptureState::default() };
1098        (state.current_thread_id.clone(), state.pty_shell.clone())
1099    };
1100    let mut capture = capture_shell_output(session, "main", &main).await;
1101
1102    let background_ids = session.background_ids.lock().await.clone();
1103    for id in background_ids {
1104        let shell = {
1105            let store = lock_session_store();
1106            store.resolve(&thread_id, &ShellTarget::Background(id.clone()))
1107        };
1108        if let Some(shell) = shell {
1109            capture.merge(capture_shell_output(session, &id, &shell).await);
1110        }
1111    }
1112    capture
1113}
1114
1115async fn capture_shell_output(
1116    session: &Arc<DaemonSession>,
1117    terminal_id: &str,
1118    shell: &SharedPtyShell,
1119) -> CaptureState {
1120    let (generation, snapshot, active) = {
1121        let mut guard = shell.lock().await;
1122        let Some(shell) = guard.as_mut() else { return CaptureState::default() };
1123        shell.poll_output_nonblocking();
1124        (shell.command_generation(), shell.output_snapshot(), shell.command_running)
1125    };
1126    let delta = {
1127        let mut observed = session.observed.lock().await;
1128        let (previous_generation, previous) =
1129            observed.entry(terminal_id.to_string()).or_insert_with(|| (generation, String::new()));
1130        if *previous_generation != generation {
1131            *previous_generation = generation;
1132            previous.clear();
1133        }
1134        let delta = snapshot
1135            .strip_prefix(previous.as_str())
1136            .map_or_else(|| snapshot.clone(), str::to_string);
1137        previous.clone_from(&snapshot);
1138        delta
1139    };
1140    let changed = !delta.is_empty();
1141    if changed {
1142        session.last_activity_unix_ms.store(unix_ms(), Ordering::Release);
1143        session.journal.lock().await.append(delta);
1144    }
1145    CaptureState { changed, active }
1146}
1147
1148async fn session_info(session: &Arc<DaemonSession>) -> Option<SessionInfo> {
1149    let _operation = session.operation_barrier.read().await;
1150    let (thread_id, cwd, main) = {
1151        let state = session.state.lock().await;
1152        let state = state.as_ref()?;
1153        (
1154            state.current_thread_id.clone(),
1155            state.cwd.to_string_lossy().into_owned(),
1156            state.pty_shell.clone(),
1157        )
1158    };
1159    let (shell_pid, running) = {
1160        let guard = main.lock().await;
1161        guard.as_ref().map_or((None, false), |shell| (shell.process_id(), shell.command_running))
1162    };
1163    let background_command_ids = active_background_command_ids(session, &thread_id).await;
1164    let last_command_at_unix_ms = session.last_command_at_unix_ms.load(Ordering::Acquire);
1165    Some(SessionInfo {
1166        thread_id,
1167        cwd,
1168        shell_pid,
1169        command_id: session.command_id.lock().await.clone(),
1170        running,
1171        background_command_ids,
1172        created_at_unix_ms: Some(session.created_at_unix_ms),
1173        last_activity_unix_ms: Some(session.last_activity_unix_ms.load(Ordering::Acquire)),
1174        last_command_at_unix_ms: (last_command_at_unix_ms > 0).then_some(last_command_at_unix_ms),
1175        ever_ran_command: session.ever_ran_command.load(Ordering::Acquire),
1176    })
1177}
1178
1179async fn active_background_command_ids(
1180    session: &Arc<DaemonSession>,
1181    thread_id: &str,
1182) -> Vec<String> {
1183    let candidates = session.background_ids.lock().await.clone();
1184    let mut active = Vec::new();
1185    for id in candidates {
1186        let shell = {
1187            let store = lock_session_store();
1188            store.resolve(thread_id, &ShellTarget::Background(id.clone()))
1189        };
1190        let Some(shell) = shell else { continue };
1191        // Capture the final queued bytes before deciding that a completed shell is
1192        // no longer active. Otherwise pruning the id here could make the journal
1193        // drainer stop one poll too early.
1194        capture_shell_output(session, &id, &shell).await;
1195        let running = {
1196            let guard = shell.lock().await;
1197            guard.as_ref().is_some_and(|shell| shell.command_running)
1198        };
1199        if running {
1200            active.push(id);
1201        }
1202    }
1203
1204    let active_set = active.iter().cloned().collect::<HashSet<_>>();
1205    session.background_ids.lock().await.retain(|id| active_set.contains(id));
1206    active.sort();
1207    active
1208}
1209
1210async fn interrupt_session(
1211    session: &Arc<DaemonSession>,
1212    params: &SessionParams,
1213    guardian_epoch: &str,
1214) -> Result<bool> {
1215    session.note_activity(false);
1216    let _operation = session.operation_barrier.read().await;
1217    if params.expected_guardian_epoch.as_deref().is_some_and(|expected| expected != guardian_epoch)
1218        || params
1219            .expected_execution
1220            .as_ref()
1221            .is_some_and(|expected| expected.guardian_epoch != guardian_epoch)
1222    {
1223        return Ok(false);
1224    }
1225    let session_epoch = session.session_epoch.lock().await;
1226    if params.expected_execution.as_ref().is_some_and(|expected| {
1227        expected.session_epoch != *session_epoch
1228            || params
1229                .expected_generation
1230                .is_some_and(|generation| generation != expected.generation)
1231    }) {
1232        return Ok(false);
1233    }
1234    drop(session_epoch);
1235    let shell = {
1236        let state = session.state.lock().await;
1237        state.as_ref().map(|state| state.pty_shell.clone())
1238    };
1239    let Some(shell) = shell else { return Ok(false) };
1240    {
1241        let mut guard = shell.lock().await;
1242        if let Some(shell) = guard.as_mut() {
1243            if params.expected_generation.is_some_and(|expected| {
1244                shell.command_generation() != expected || !shell.command_running
1245            }) {
1246                return Ok(false);
1247            }
1248            shell.send_interrupt().map_err(|error| {
1249                WinxError::CommandExecutionError(format!("failed to interrupt shell: {error}"))
1250            })?;
1251        }
1252    }
1253    let deadline = std::time::Instant::now() + Duration::from_secs(3);
1254    while std::time::Instant::now() < deadline {
1255        let recovered = {
1256            let mut guard = shell.lock().await;
1257            match guard.as_mut() {
1258                Some(shell) => shell.poll_output_nonblocking() || !shell.command_running,
1259                None => true,
1260            }
1261        };
1262        if recovered {
1263            capture_session_outputs_locked(session).await;
1264            return Ok(true);
1265        }
1266        tokio::time::sleep(Duration::from_millis(50)).await;
1267    }
1268    Err(WinxError::CommandExecutionError(
1269        "interrupted shell did not return to a prompt within 3 seconds".to_string(),
1270    ))
1271}
1272
1273fn unix_ms() -> u64 {
1274    SystemTime::now()
1275        .duration_since(UNIX_EPOCH)
1276        .ok()
1277        .and_then(|duration| u64::try_from(duration.as_millis()).ok())
1278        .unwrap_or(0)
1279}
1280
1281fn to_wire_error(error: WinxError) -> WireShellError {
1282    match error {
1283        WinxError::BashStateNotInitialized => WireShellError::BashStateNotInitialized,
1284        WinxError::ShellInitializationError(message) => {
1285            WireShellError::ShellInitialization(message)
1286        }
1287        WinxError::CommandExecutionError(message) => WireShellError::CommandExecution(message),
1288        WinxError::NoActiveCommand(message) => WireShellError::NoActiveCommand(message),
1289        WinxError::BackgroundSessionNotFound(message) => {
1290            WireShellError::BackgroundSessionNotFound(message)
1291        }
1292        WinxError::EmptyInteractiveInput { action } => {
1293            WireShellError::EmptyInteractiveInput(action)
1294        }
1295        WinxError::InteractiveTargetNotRunning(message) => {
1296            WireShellError::InteractiveTargetNotRunning(message)
1297        }
1298        WinxError::CommandAlreadyRunning { current_command, duration_seconds } => {
1299            WireShellError::CommandAlreadyRunning { current_command, duration_seconds }
1300        }
1301        WinxError::CommandNotAllowed(message) => WireShellError::CommandNotAllowed(message),
1302        WinxError::ThreadIdMismatch(message) => WireShellError::ThreadIdMismatch(message),
1303        WinxError::InvalidInput(message) | WinxError::ArgumentParseError(message) => {
1304            WireShellError::InvalidInput(message)
1305        }
1306        other => WireShellError::Other(other.to_string()),
1307    }
1308}
1309
1310fn rpc_result<T: serde::Serialize>(id: u64, result: &T) -> RpcResponse {
1311    match serde_json::to_value(result) {
1312        Ok(result) => {
1313            RpcResponse { jsonrpc: "2.0".to_string(), id, result: Some(result), error: None }
1314        }
1315        Err(error) => rpc_error(id, -32603, &error.to_string()),
1316    }
1317}
1318
1319fn rpc_error(id: u64, code: i32, message: &str) -> RpcResponse {
1320    RpcResponse {
1321        jsonrpc: "2.0".to_string(),
1322        id,
1323        result: None,
1324        error: Some(RpcError { code, message: message.to_string() }),
1325    }
1326}
1327
1328fn same_uid(stream: &UnixStream) -> Result<bool> {
1329    let credentials = stream.peer_cred()?;
1330    Ok(credentials.uid() == crate::os::unix::effective_uid())
1331}
1332
1333#[cfg(test)]
1334mod tests {
1335    #![allow(clippy::expect_used)]
1336    use std::collections::HashMap;
1337    use std::sync::atomic::Ordering;
1338    use std::sync::Arc;
1339    use std::time::{Duration, Instant};
1340
1341    use tokio::sync::{watch, Mutex};
1342
1343    use super::{
1344        canonical_action_fingerprint, configure_session, consume_launch_cancellation_flag,
1345        interrupt_session, launch_cancellation_flag, run_action, validate_identifier,
1346        ActionCompletion, ActionEntry, ActionOwnerGuard, CaptureState, DaemonSession,
1347        OutputJournal, MAX_SESSION_CACHE_ENTRIES,
1348    };
1349    use crate::daemon::protocol::{
1350        ConfigureSessionParams, ConfigureSessionTransition, RunActionParams, SessionParams,
1351        WireShellError,
1352    };
1353    use crate::errors::WinxError;
1354    use crate::runtime::ShellActionOptions;
1355    use crate::state::bash_state::BashState;
1356    use crate::types::BashCommand;
1357
1358    fn action_params(state: &BashState, command: &str, request_key: &str) -> RunActionParams {
1359        let command: BashCommand = serde_json::from_value(serde_json::json!({
1360            "action_json": {
1361                "type": "command",
1362                "command": command,
1363                "is_background": false,
1364                "allow_multi": true
1365            },
1366            "thread_id": state.current_thread_id,
1367            "wait_for_seconds": 2.0
1368        }))
1369        .expect("valid command");
1370        RunActionParams {
1371            snapshot: state.snapshot(),
1372            command,
1373            request_key: request_key.to_string(),
1374            consumer_id: "single-flight-test".to_string(),
1375            options: ShellActionOptions::default(),
1376        }
1377    }
1378
1379    #[test]
1380    fn late_consumer_is_told_when_journal_head_was_dropped() {
1381        let mut journal = OutputJournal::default();
1382        journal.append("a".repeat(3 * 1024 * 1024));
1383        journal.append("b".repeat(3 * 1024 * 1024));
1384
1385        let read = journal.read("late");
1386        assert!(read.gap);
1387        assert!(read.output.starts_with('b'));
1388    }
1389
1390    #[test]
1391    fn journal_consumer_cursors_are_lru_bounded() {
1392        let mut journal = OutputJournal::default();
1393        journal.append("output".to_string());
1394        for index in 0..(MAX_SESSION_CACHE_ENTRIES + 100) {
1395            let _ = journal.read(&format!("consumer-{index}"));
1396        }
1397        assert_eq!(journal.cursors.len(), MAX_SESSION_CACHE_ENTRIES);
1398    }
1399
1400    #[test]
1401    fn daemon_identifiers_have_a_small_memory_bound() {
1402        assert!(validate_identifier("consumer_id", &"x".repeat(256)).is_ok());
1403        assert!(validate_identifier("consumer_id", &"x".repeat(257)).is_err());
1404    }
1405
1406    #[test]
1407    fn drainer_backoff_tracks_output_and_command_activity() {
1408        assert!(
1409            CaptureState { changed: true, active: false }.next_delay()
1410                < CaptureState { changed: false, active: true }.next_delay()
1411        );
1412        assert!(
1413            CaptureState { changed: false, active: true }.next_delay()
1414                < CaptureState { changed: false, active: false }.next_delay()
1415        );
1416    }
1417
1418    #[tokio::test]
1419    async fn request_key_is_single_flight_and_replays_lost_response() {
1420        let temp = tempfile::tempdir().expect("temporary directory");
1421        let marker = temp.path().join("single-flight.marker");
1422        let mut state = BashState::new();
1423        state.cwd = temp.path().to_path_buf();
1424        state.workspace_root = temp.path().to_path_buf();
1425        state.current_thread_id = "single_flight".to_string();
1426        let command = format!("printf x >> {}; sleep 0.1", marker.display());
1427        let params = action_params(&state, &command, "same-request");
1428        let sessions = Arc::new(Mutex::new(HashMap::<String, Arc<DaemonSession>>::new()));
1429
1430        let (first, concurrent) = tokio::join!(
1431            run_action(&sessions, params.clone(), "guardian-a"),
1432            run_action(&sessions, params.clone(), "guardian-a")
1433        );
1434        assert!(first.expect("owner result").error.is_none());
1435        assert!(concurrent.expect("shared result").error.is_none());
1436        let replay = run_action(&sessions, params, "guardian-a").await.expect("cached replay");
1437        assert!(replay.error.is_none());
1438        assert_eq!(std::fs::read_to_string(marker).expect("marker"), "x");
1439    }
1440
1441    #[tokio::test]
1442    async fn request_key_collision_rejects_different_parameters() {
1443        let temp = tempfile::tempdir().expect("temporary directory");
1444        let mut state = BashState::new();
1445        state.cwd = temp.path().to_path_buf();
1446        state.workspace_root = temp.path().to_path_buf();
1447        state.current_thread_id = "request_collision".to_string();
1448        let sessions = Arc::new(Mutex::new(HashMap::<String, Arc<DaemonSession>>::new()));
1449        run_action(&sessions, action_params(&state, "printf first", "collision"), "guardian-a")
1450            .await
1451            .expect("first action");
1452
1453        let error = run_action(
1454            &sessions,
1455            action_params(&state, "printf second", "collision"),
1456            "guardian-a",
1457        )
1458        .await
1459        .expect_err("collision must fail");
1460        assert!(error.to_string().contains("different shell action parameters"));
1461    }
1462
1463    #[tokio::test]
1464    async fn non_launching_action_paths_consume_unowned_cancellation_tombstones() {
1465        let temp = tempfile::tempdir().expect("temporary directory");
1466        let mut state = BashState::new();
1467        state.cwd = temp.path().to_path_buf();
1468        state.workspace_root = temp.path().to_path_buf();
1469        state.current_thread_id = "early_cancellation_cleanup".to_string();
1470        let session = Arc::new(DaemonSession::new());
1471        let sessions = Arc::new(Mutex::new(HashMap::from([(
1472            state.current_thread_id.clone(),
1473            Arc::clone(&session),
1474        )])));
1475
1476        let mut replay = action_params(&state, "printf replay", "replay-request");
1477        replay.options.cancellation_key = Some("replay-cancel".to_string());
1478        run_action(&sessions, replay.clone(), "guardian-a").await.expect("initial replay action");
1479        let replay_tombstone =
1480            launch_cancellation_flag(&session, "replay-cancel").await.expect("replay tombstone");
1481        replay_tombstone.store(true, Ordering::Release);
1482        drop(replay_tombstone);
1483        run_action(&sessions, replay, "guardian-a").await.expect("cached replay");
1484        assert!(!session.launch_reservations.lock().await.contains_key("replay-cancel"));
1485
1486        let mut first_collision =
1487            action_params(&state, "printf collision-one", "collision-request");
1488        first_collision.options.cancellation_key = Some("collision-cancel".to_string());
1489        run_action(&sessions, first_collision, "guardian-a")
1490            .await
1491            .expect("initial collision action");
1492        let collision_tombstone = launch_cancellation_flag(&session, "collision-cancel")
1493            .await
1494            .expect("collision tombstone");
1495        collision_tombstone.store(true, Ordering::Release);
1496        drop(collision_tombstone);
1497        let mut collision = action_params(&state, "printf collision-two", "collision-request");
1498        collision.options.cancellation_key = Some("collision-cancel".to_string());
1499        assert!(run_action(&sessions, collision, "guardian-a").await.is_err());
1500        assert!(!session.launch_reservations.lock().await.contains_key("collision-cancel"));
1501
1502        {
1503            let mut completed = session.completed.lock().await;
1504            completed.clear();
1505            for index in 0..MAX_SESSION_CACHE_ENTRIES {
1506                completed.insert(
1507                    format!("working-{index}"),
1508                    ActionEntry {
1509                        fingerprint: vec![u8::try_from(index % 255).unwrap_or_default()],
1510                        cancellation_key: None,
1511                        completion: ActionCompletion::InFlight(Arc::new(watch::channel(None).0)),
1512                        last_seen: Instant::now(),
1513                    },
1514                );
1515            }
1516        }
1517        let saturation_tombstone = launch_cancellation_flag(&session, "saturation-cancel")
1518            .await
1519            .expect("saturation tombstone");
1520        saturation_tombstone.store(true, Ordering::Release);
1521        drop(saturation_tombstone);
1522        let mut saturated = action_params(&state, "printf blocked", "overflow-request");
1523        saturated.options.cancellation_key = Some("saturation-cancel".to_string());
1524        assert!(matches!(
1525            run_action(&sessions, saturated, "guardian-a").await,
1526            Err(WinxError::ResourceAllocationError { .. })
1527        ));
1528        assert!(!session.launch_reservations.lock().await.contains_key("saturation-cancel"));
1529    }
1530
1531    #[tokio::test]
1532    async fn action_waiter_does_not_consume_the_owners_cancellation_reservation() {
1533        let temp = tempfile::tempdir().expect("temporary directory");
1534        let started = temp.path().join("single-flight-started.marker");
1535        let release = temp.path().join("single-flight-release.marker");
1536        let mut state = BashState::new();
1537        state.cwd = temp.path().to_path_buf();
1538        state.workspace_root = temp.path().to_path_buf();
1539        state.current_thread_id = "waiter_cancellation_owner".to_string();
1540        let session = Arc::new(DaemonSession::new());
1541        let sessions = Arc::new(Mutex::new(HashMap::from([(
1542            state.current_thread_id.clone(),
1543            Arc::clone(&session),
1544        )])));
1545        let mut params = action_params(
1546            &state,
1547            &format!(
1548                "sh -c 'touch {}; while [ ! -e {} ]; do sleep 0.01; done'",
1549                started.display(),
1550                release.display()
1551            ),
1552            "shared-cancel-request",
1553        );
1554        params.options.cancellation_key = Some("shared-cancel-key".to_string());
1555
1556        let owner_sessions = Arc::clone(&sessions);
1557        let owner_params = params.clone();
1558        let owner =
1559            tokio::spawn(
1560                async move { run_action(&owner_sessions, owner_params, "guardian-a").await },
1561            );
1562        tokio::time::timeout(Duration::from_secs(2), async {
1563            while !started.exists() {
1564                tokio::time::sleep(Duration::from_millis(5)).await;
1565            }
1566        })
1567        .await
1568        .expect("owner command never started");
1569
1570        let waiter_sessions = Arc::clone(&sessions);
1571        let waiter =
1572            tokio::spawn(async move { run_action(&waiter_sessions, params, "guardian-a").await });
1573        tokio::time::sleep(Duration::from_millis(25)).await;
1574        assert!(
1575            session.launch_reservations.lock().await.contains_key("shared-cancel-key"),
1576            "a waiter consumed the owner's launch reservation"
1577        );
1578
1579        std::fs::write(release, b"release").expect("release owner command");
1580        owner.await.expect("owner join").expect("owner result");
1581        waiter.await.expect("waiter join").expect("waiter result");
1582        assert!(!session.launch_reservations.lock().await.contains_key("shared-cancel-key"));
1583    }
1584
1585    #[test]
1586    fn action_fingerprint_is_canonical_across_map_insertion_order() {
1587        let mut first = BashState::new();
1588        first.current_thread_id = "canonical".to_string();
1589        first.whitelist_for_overwrite.insert(
1590            "z".to_string(),
1591            crate::state::bash_state::FileWhitelistData::new("z".into(), vec![(1, 1)], 1),
1592        );
1593        first.whitelist_for_overwrite.insert(
1594            "a".to_string(),
1595            crate::state::bash_state::FileWhitelistData::new("a".into(), vec![(1, 1)], 1),
1596        );
1597        let mut second = BashState::new();
1598        second.current_thread_id = "canonical".to_string();
1599        second.whitelist_for_overwrite.insert(
1600            "a".to_string(),
1601            crate::state::bash_state::FileWhitelistData::new("a".into(), vec![(1, 1)], 1),
1602        );
1603        second.whitelist_for_overwrite.insert(
1604            "z".to_string(),
1605            crate::state::bash_state::FileWhitelistData::new("z".into(), vec![(1, 1)], 1),
1606        );
1607
1608        assert_eq!(
1609            canonical_action_fingerprint(&action_params(&first, "printf stable", "key"))
1610                .expect("first fingerprint"),
1611            canonical_action_fingerprint(&action_params(&second, "printf stable", "key"))
1612                .expect("second fingerprint")
1613        );
1614    }
1615
1616    #[tokio::test]
1617    async fn action_cache_applies_backpressure_when_every_entry_is_in_flight() {
1618        let state = BashState::new();
1619        let session = Arc::new(DaemonSession::new());
1620        {
1621            let mut completed = session.completed.lock().await;
1622            for index in 0..MAX_SESSION_CACHE_ENTRIES {
1623                completed.insert(
1624                    format!("working-{index}"),
1625                    ActionEntry {
1626                        fingerprint: vec![u8::try_from(index % 255).unwrap_or_default()],
1627                        cancellation_key: None,
1628                        completion: ActionCompletion::InFlight(Arc::new(watch::channel(None).0)),
1629                        last_seen: Instant::now(),
1630                    },
1631                );
1632            }
1633        }
1634        let sessions =
1635            Arc::new(Mutex::new(HashMap::from([(state.current_thread_id.clone(), session)])));
1636        let error = run_action(
1637            &sessions,
1638            action_params(&state, "printf blocked", "overflow"),
1639            "guardian-a",
1640        )
1641        .await
1642        .expect_err("all-in-flight cache must reject new reservations");
1643        assert!(matches!(error, WinxError::ResourceAllocationError { .. }));
1644    }
1645
1646    #[tokio::test]
1647    async fn cancellation_reservations_never_evict_active_flags() {
1648        let session = DaemonSession::new();
1649        let mut active = Vec::new();
1650        for index in 0..MAX_SESSION_CACHE_ENTRIES {
1651            active.push(
1652                launch_cancellation_flag(&session, &format!("active-{index}"))
1653                    .await
1654                    .expect("active cancellation reservation"),
1655            );
1656        }
1657        let error = launch_cancellation_flag(&session, "overflow")
1658            .await
1659            .expect_err("all active cancellation flags must apply backpressure");
1660        assert!(matches!(error, WinxError::ResourceAllocationError { .. }));
1661        assert_eq!(session.launch_reservations.lock().await.len(), MAX_SESSION_CACHE_ENTRIES);
1662
1663        drop(active.pop());
1664        launch_cancellation_flag(&session, "overflow")
1665            .await
1666            .expect("inactive cancellation flag should be evictable");
1667        assert_eq!(session.launch_reservations.lock().await.len(), MAX_SESSION_CACHE_ENTRIES);
1668    }
1669
1670    #[tokio::test]
1671    async fn cancelled_tombstones_apply_backpressure_until_consumed() {
1672        let session = DaemonSession::new();
1673        for index in 0..MAX_SESSION_CACHE_ENTRIES {
1674            let flag = launch_cancellation_flag(&session, &format!("cancelled-{index}"))
1675                .await
1676                .expect("cancel tombstone");
1677            flag.store(true, Ordering::Release);
1678        }
1679
1680        let error = launch_cancellation_flag(&session, "overflow")
1681            .await
1682            .expect_err("cancel tombstones must not be evicted for capacity");
1683        assert!(matches!(error, WinxError::ResourceAllocationError { .. }));
1684
1685        let consumed = session
1686            .launch_reservations
1687            .lock()
1688            .await
1689            .get("cancelled-0")
1690            .expect("preserved tombstone")
1691            .value
1692            .clone();
1693        consume_launch_cancellation_flag(&session, "cancelled-0", &consumed).await;
1694        launch_cancellation_flag(&session, "replacement")
1695            .await
1696            .expect("consumed tombstone releases capacity");
1697    }
1698
1699    #[tokio::test]
1700    async fn cancel_before_action_survives_capacity_churn_and_prevents_launch() {
1701        let temp = tempfile::tempdir().expect("temporary directory");
1702        let marker = temp.path().join("cancelled-before-action.marker");
1703        let mut state = BashState::new();
1704        state.cwd = temp.path().to_path_buf();
1705        state.workspace_root = temp.path().to_path_buf();
1706        state.current_thread_id = "cancel_tombstone_churn".to_string();
1707        let session = Arc::new(DaemonSession::new());
1708        let sessions = Arc::new(Mutex::new(HashMap::from([(
1709            state.current_thread_id.clone(),
1710            Arc::clone(&session),
1711        )])));
1712
1713        let cancelled = launch_cancellation_flag(&session, "cancel-before-action")
1714            .await
1715            .expect("cancel reservation");
1716        cancelled.store(true, Ordering::Release);
1717        drop(cancelled);
1718        for index in 0..(MAX_SESSION_CACHE_ENTRIES * 2) {
1719            drop(
1720                launch_cancellation_flag(&session, &format!("churn-{index}"))
1721                    .await
1722                    .expect("inactive churn reservation"),
1723            );
1724        }
1725        assert!(
1726            session
1727                .launch_reservations
1728                .lock()
1729                .await
1730                .get("cancel-before-action")
1731                .is_some_and(|entry| entry.value.load(Ordering::Acquire)),
1732            "capacity churn evicted the cancellation tombstone"
1733        );
1734
1735        let mut params = action_params(
1736            &state,
1737            &format!("printf forbidden > {}", marker.display()),
1738            "cancelled-action-request",
1739        );
1740        params.options.cancellation_key = Some("cancel-before-action".to_string());
1741        let result =
1742            run_action(&sessions, params, "guardian-a").await.expect("cancelled action result");
1743        assert!(matches!(
1744            result.error,
1745            Some(WireShellError::CommandExecution(ref message))
1746                if message.contains("cancelled before command launch")
1747        ));
1748        assert!(!marker.exists(), "cancelled action reached the PTY");
1749        assert!(
1750            !session.launch_reservations.lock().await.contains_key("cancel-before-action"),
1751            "the action did not consume its cancellation tombstone"
1752        );
1753    }
1754
1755    #[tokio::test]
1756    async fn automatic_pty_reset_rotates_before_stale_operations_reach_the_new_pty() {
1757        let temp = tempfile::tempdir().expect("temporary directory");
1758        let marker = temp.path().join("new-incarnation-running.marker");
1759        let completed_marker = temp.path().join("new-incarnation-completed.marker");
1760        let mut state = BashState::new();
1761        state.cwd = temp.path().to_path_buf();
1762        state.workspace_root = temp.path().to_path_buf();
1763        state.current_thread_id = "automatic_pty_reset".to_string();
1764        let sessions = Arc::new(Mutex::new(HashMap::new()));
1765
1766        let first = run_action(
1767            &sessions,
1768            action_params(&state, "printf first-incarnation", "before-auto-reset"),
1769            "guardian-a",
1770        )
1771        .await
1772        .expect("first action");
1773        let prior_token = first.execution_token.expect("first execution token");
1774        let session =
1775            sessions.lock().await.get("automatic_pty_reset").expect("daemon session").clone();
1776
1777        let mut forced = action_params(
1778            &state,
1779            &format!(
1780                "sh -c 'touch {}; sleep 0.4; printf completed > {}'",
1781                marker.display(),
1782                completed_marker.display()
1783            ),
1784            "forced-auto-reset",
1785        );
1786        forced.options.force_clear_to_run_failure = true;
1787        let reset_sessions = Arc::clone(&sessions);
1788        let reset =
1789            tokio::spawn(async move { run_action(&reset_sessions, forced, "guardian-a").await });
1790        tokio::time::timeout(Duration::from_secs(2), async {
1791            while !marker.exists() {
1792                tokio::time::sleep(Duration::from_millis(5)).await;
1793            }
1794        })
1795        .await
1796        .expect("replacement PTY command never started");
1797        assert_ne!(
1798            *session.session_epoch.lock().await,
1799            prior_token.session_epoch,
1800            "epoch was not rotated before the replacement command launched"
1801        );
1802        assert!(!reset.is_finished(), "reset action returned before the interleaving probe");
1803
1804        let mut stale_status = action_params(&state, "printf unused", "stale-after-auto-reset");
1805        stale_status.command = serde_json::from_value(serde_json::json!({
1806            "action_json": {"type": "status_check", "status_check": true},
1807            "thread_id": "automatic_pty_reset",
1808            "wait_for_seconds": 0.0
1809        }))
1810        .expect("status command");
1811        stale_status.options.expected_generation = Some(prior_token.generation);
1812        stale_status.options.expected_execution = Some(prior_token.clone());
1813        assert!(run_action(&sessions, stale_status, "guardian-a").await.is_err());
1814
1815        assert!(!interrupt_session(
1816            &session,
1817            &SessionParams {
1818                thread_id: "automatic_pty_reset".to_string(),
1819                expected_generation: Some(prior_token.generation),
1820                expected_execution: Some(prior_token.clone()),
1821                expected_guardian_epoch: Some("guardian-a".to_string()),
1822            },
1823            "guardian-a",
1824        )
1825        .await
1826        .expect("stale interrupt result"));
1827
1828        let reset = reset.await.expect("reset action join").expect("reset action result");
1829        let current = reset.execution_token.clone().expect("reset execution token");
1830        assert_eq!(prior_token.generation, current.generation);
1831        assert_ne!(prior_token.session_epoch, current.session_epoch);
1832        assert_eq!(
1833            std::fs::read_to_string(completed_marker).expect("replacement completion marker"),
1834            "completed",
1835            "a stale operation disturbed the replacement command: {reset:?}"
1836        );
1837    }
1838
1839    #[tokio::test]
1840    async fn abandoned_action_owner_notifies_waiters() {
1841        let completion = Arc::new(watch::channel(None).0);
1842        let receiver = completion.subscribe();
1843        drop(ActionOwnerGuard::new(completion));
1844        let error = super::wait_for_action_completion(receiver)
1845            .await
1846            .expect_err("abandoned owner must fail its waiters");
1847        assert!(error.to_string().contains("owner disappeared"));
1848    }
1849
1850    #[tokio::test]
1851    #[allow(clippy::too_many_lines)] // One deterministic reset/action/interrupt schedule.
1852    async fn reset_is_atomic_with_action_status_and_interrupt_across_generation_collision() {
1853        let temp = tempfile::tempdir().expect("temporary directory");
1854        let mut state = BashState::new();
1855        state.cwd = temp.path().to_path_buf();
1856        state.workspace_root = temp.path().to_path_buf();
1857        state.current_thread_id = "atomic_reset".to_string();
1858        let session = Arc::new(DaemonSession::new());
1859        let sessions = Arc::new(Mutex::new(HashMap::from([(
1860            state.current_thread_id.clone(),
1861            Arc::clone(&session),
1862        )])));
1863        let held = session.operation_barrier.write().await;
1864        let owner_sessions = Arc::clone(&sessions);
1865        let first_params =
1866            action_params(&state, "sh -c 'sleep 0.25; printf old-incarnation'", "old-action");
1867        let first =
1868            tokio::spawn(
1869                async move { run_action(&owner_sessions, first_params, "guardian-a").await },
1870            );
1871
1872        while !session.completed.lock().await.contains_key("old-action") {
1873            tokio::task::yield_now().await;
1874        }
1875        // Let the owner reach the barrier while the test-held writer makes the
1876        // queue ordering deterministic, then enqueue reset behind it.
1877        tokio::time::sleep(Duration::from_millis(10)).await;
1878        let reset_sessions = Arc::clone(&sessions);
1879        let reset_snapshot = state.snapshot();
1880        let mut reset = tokio::spawn(async move {
1881            configure_session(
1882                &reset_sessions,
1883                ConfigureSessionParams {
1884                    snapshot: reset_snapshot,
1885                    transition: ConfigureSessionTransition::Reset,
1886                },
1887            )
1888            .await
1889        });
1890        drop(held);
1891        assert!(
1892            tokio::time::timeout(Duration::from_millis(50), &mut reset).await.is_err(),
1893            "reset crossed an in-flight action's session barrier"
1894        );
1895
1896        let first = tokio::time::timeout(Duration::from_secs(5), first)
1897            .await
1898            .expect("first action timeout")
1899            .expect("first join")
1900            .expect("first result");
1901        let stale_execution = first.execution_token.expect("old execution token");
1902        tokio::time::timeout(Duration::from_secs(5), reset)
1903            .await
1904            .expect("reset timeout")
1905            .expect("reset join")
1906            .expect("reset result");
1907
1908        let mut new_action =
1909            action_params(&state, "sh -c 'sleep 0.15; printf new-incarnation'", "new-action");
1910        new_action.command.wait_for_seconds = Some(0.001);
1911        let current = tokio::time::timeout(
1912            Duration::from_secs(5),
1913            run_action(&sessions, new_action, "guardian-a"),
1914        )
1915        .await
1916        .expect("new action timeout")
1917        .expect("new action");
1918        let current_token = current.execution_token.expect("current execution token");
1919        assert_eq!(stale_execution.generation, current_token.generation);
1920        assert_ne!(stale_execution.session_epoch, current_token.session_epoch);
1921
1922        let mut stale_status = action_params(&state, "printf unused", "stale-status");
1923        stale_status.command = serde_json::from_value(serde_json::json!({
1924            "action_json": {"type": "status_check", "status_check": true},
1925            "thread_id": "atomic_reset",
1926            "wait_for_seconds": 0.01
1927        }))
1928        .expect("status command");
1929        stale_status.options.expected_generation = Some(stale_execution.generation);
1930        stale_status.options.expected_execution = Some(stale_execution.clone());
1931        assert!(tokio::time::timeout(
1932            Duration::from_secs(5),
1933            run_action(&sessions, stale_status, "guardian-a"),
1934        )
1935        .await
1936        .expect("stale status timeout")
1937        .is_err());
1938
1939        assert!(!interrupt_session(
1940            &session,
1941            &SessionParams {
1942                thread_id: "atomic_reset".to_string(),
1943                expected_generation: Some(stale_execution.generation),
1944                expected_execution: Some(stale_execution),
1945                expected_guardian_epoch: Some("guardian-a".to_string()),
1946            },
1947            "guardian-a",
1948        )
1949        .await
1950        .expect("stale interrupt"));
1951
1952        let mut current_status = action_params(&state, "printf unused", "current-status");
1953        current_status.command = serde_json::from_value(serde_json::json!({
1954            "action_json": {"type": "status_check", "status_check": true},
1955            "thread_id": "atomic_reset",
1956            "wait_for_seconds": 1.0
1957        }))
1958        .expect("current status command");
1959        current_status.options.expected_generation = Some(current_token.generation);
1960        current_status.options.expected_execution = Some(current_token);
1961        let drained = tokio::time::timeout(
1962            Duration::from_secs(5),
1963            run_action(&sessions, current_status, "guardian-a"),
1964        )
1965        .await
1966        .expect("current status timeout")
1967        .expect("current status");
1968        assert!(
1969            drained.output.as_deref().is_some_and(|output| output.contains("new-incarnation")),
1970            "stale operations consumed or interrupted the new incarnation: {drained:?}"
1971        );
1972    }
1973}