Skip to main content

soothe_client/
client.rs

1//! Protocol-1 WebSocket transport client with mux and delivery_ack.
2
3use std::collections::{HashMap, VecDeque};
4use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
5use std::sync::Arc;
6use std::time::Duration;
7
8use futures_util::{SinkExt, StreamExt};
9use serde_json::{json, Map, Value};
10use tokio::sync::{mpsc, oneshot, Mutex, Notify};
11use tokio::time::{sleep, timeout};
12use tokio_tungstenite::{connect_async, tungstenite::Message};
13
14use crate::errors::{
15    ConnectionError, DaemonError, DisconnectCause, Error, Result, StaleLoopError, TimeoutError,
16};
17use crate::heartbeat::HeartbeatTracker;
18use crate::inbound_priority::{
19    inbound_frame_drop_priority, DEFAULT_INBOUND_MAX_SIZE, DROP_PRIORITY_NORMAL,
20};
21use crate::protocol::{
22    decode_message, expand_wire_messages, new_connection_init, new_notification, new_ping,
23    new_pong, new_request, new_subscribe, new_unsubscribe, Envelope,
24};
25use crate::stream_terminal::{
26    extract_loop_id_from_inbound, inbound_needs_delivery_ack, stale_pending_frame_label,
27};
28const DEFAULT_MAX_FRAME: usize = 10 * 1024 * 1024;
29
30/// Options for Client construction.
31#[derive(Debug, Clone)]
32pub struct ClientConfig {
33    /// WebSocket URL.
34    pub url: String,
35    /// Max inbound queue depth before priority drop.
36    pub max_inbound: usize,
37    /// Max frame size hint (informational).
38    pub max_frame_size: usize,
39}
40
41impl Default for ClientConfig {
42    fn default() -> Self {
43        Self {
44            url: "ws://127.0.0.1:8765".into(),
45            max_inbound: DEFAULT_INBOUND_MAX_SIZE,
46            max_frame_size: DEFAULT_MAX_FRAME,
47        }
48    }
49}
50
51/// Options for `send_input` / loop_input notification.
52#[derive(Debug, Clone, Default)]
53pub struct SendInputOptions {
54    /// Loop id (required for multi-loop).
55    pub loop_id: Option<String>,
56    /// Preferred subagent.
57    pub preferred_subagent: Option<String>,
58    /// Forced StrangeLoop intake scope (`trivial`|`simple`|`complex`).
59    pub intake_scope: Option<String>,
60    /// Model override.
61    pub model: Option<String>,
62    /// Model params.
63    pub model_params: Option<Value>,
64    /// Router profile.
65    pub router_profile: Option<String>,
66    /// Attachments array.
67    pub attachments: Option<Value>,
68    /// Intent hint.
69    pub intent_hint: Option<String>,
70    /// Response schema.
71    pub response_schema: Option<Value>,
72    /// Response schema name.
73    pub response_schema_name: Option<String>,
74    /// Strict schema.
75    pub response_schema_strict: Option<bool>,
76    /// Clarification mode.
77    pub clarification_mode: Option<String>,
78    /// Clarification answer flag.
79    pub clarification_answer: bool,
80    /// Clarification answers payload.
81    pub clarification_answers: Option<Value>,
82}
83
84type RpcWaiter = oneshot::Sender<std::result::Result<Value, DaemonError>>;
85type StreamDegradedCallback = Arc<dyn Fn(u64, String) + Send + Sync>;
86
87struct Shared {
88    url: String,
89    max_inbound: AtomicUsize,
90    write_tx: Mutex<Option<mpsc::UnboundedSender<Message>>>,
91    rpc_waiters: Mutex<HashMap<String, RpcWaiter>>,
92    inbound: Mutex<VecDeque<Value>>,
93    inbound_notify: Notify,
94    connected: AtomicBool,
95    reader_alive: AtomicBool,
96    handshake_complete: AtomicBool,
97    readiness_state: Mutex<String>,
98    disconnect_cause: Mutex<Option<DisconnectCause>>,
99    disconnect_notify: Notify,
100    inbound_dropped: AtomicU64,
101    delivery_seq: Mutex<HashMap<String, u64>>,
102    heartbeat_interval_ms: Mutex<Option<u64>>,
103    stream_degraded_cb: std::sync::Mutex<Option<StreamDegradedCallback>>,
104    degraded_notified: AtomicBool,
105    heartbeat_tracker: std::sync::Mutex<Option<Arc<HeartbeatTracker>>>,
106}
107
108/// Long-lived protocol-1 WebSocket client.
109#[derive(Clone)]
110pub struct Client {
111    shared: Arc<Shared>,
112}
113
114impl Client {
115    /// Create a client for `url`.
116    pub fn new(url: impl Into<String>) -> Self {
117        Self::with_config(ClientConfig {
118            url: url.into(),
119            ..Default::default()
120        })
121    }
122
123    /// Create with explicit config.
124    pub fn with_config(cfg: ClientConfig) -> Self {
125        Self {
126            shared: Arc::new(Shared {
127                url: cfg.url,
128                max_inbound: AtomicUsize::new(cfg.max_inbound),
129                write_tx: Mutex::new(None),
130                rpc_waiters: Mutex::new(HashMap::new()),
131                inbound: Mutex::new(VecDeque::new()),
132                inbound_notify: Notify::new(),
133                connected: AtomicBool::new(false),
134                reader_alive: AtomicBool::new(false),
135                handshake_complete: AtomicBool::new(false),
136                readiness_state: Mutex::new(String::new()),
137                disconnect_cause: Mutex::new(None),
138                disconnect_notify: Notify::new(),
139                inbound_dropped: AtomicU64::new(0),
140                delivery_seq: Mutex::new(HashMap::new()),
141                heartbeat_interval_ms: Mutex::new(None),
142                stream_degraded_cb: std::sync::Mutex::new(None),
143                degraded_notified: AtomicBool::new(false),
144                heartbeat_tracker: std::sync::Mutex::new(None),
145            }),
146        }
147    }
148
149    /// Daemon URL.
150    pub fn url(&self) -> &str {
151        &self.shared.url
152    }
153
154    /// Whether the socket is connected.
155    pub fn is_connected(&self) -> bool {
156        self.shared.connected.load(Ordering::SeqCst)
157    }
158
159    /// Whether the reader task is alive.
160    pub fn is_connection_alive(&self) -> bool {
161        self.shared.reader_alive.load(Ordering::SeqCst)
162            && self.shared.connected.load(Ordering::SeqCst)
163    }
164
165    /// Whether the protocol-1 handshake has completed.
166    pub fn is_handshake_complete(&self) -> bool {
167        self.shared.handshake_complete.load(Ordering::SeqCst)
168    }
169
170    /// Daemon `readiness_state` from the last `connection_ack` (empty before handshake).
171    pub async fn readiness_state(&self) -> String {
172        self.shared.readiness_state.lock().await.clone()
173    }
174
175    /// Count of dropped inbound frames under backpressure.
176    pub fn inbound_dropped(&self) -> u64 {
177        self.shared.inbound_dropped.load(Ordering::SeqCst)
178    }
179
180    /// Register a hook invoked on the first inbound overflow drop.
181    pub fn set_stream_degraded_callback(&self, cb: Option<Arc<dyn Fn(u64, String) + Send + Sync>>) {
182        *self
183            .shared
184            .stream_degraded_cb
185            .lock()
186            .expect("stream_degraded lock") = cb;
187        self.shared.degraded_notified.store(false, Ordering::SeqCst);
188    }
189
190    /// Enable heartbeat tracking with the default 15s alive threshold.
191    pub fn enable_heartbeat_tracking(&self) -> Arc<HeartbeatTracker> {
192        self.enable_heartbeat_tracking_with_threshold(Duration::from_secs(15))
193    }
194
195    /// Enable heartbeat tracking with a custom alive threshold.
196    pub fn enable_heartbeat_tracking_with_threshold(
197        &self,
198        threshold: Duration,
199    ) -> Arc<HeartbeatTracker> {
200        let tracker = Arc::new(HeartbeatTracker::with_threshold(threshold));
201        *self
202            .shared
203            .heartbeat_tracker
204            .lock()
205            .expect("heartbeat lock") = Some(tracker.clone());
206        tracker
207    }
208
209    /// Disable heartbeat tracking.
210    pub fn disable_heartbeat_tracking(&self) {
211        *self
212            .shared
213            .heartbeat_tracker
214            .lock()
215            .expect("heartbeat lock") = None;
216    }
217
218    /// Current heartbeat tracker, if enabled.
219    pub fn heartbeat_tracker(&self) -> Option<Arc<HeartbeatTracker>> {
220        self.shared
221            .heartbeat_tracker
222            .lock()
223            .expect("heartbeat lock")
224            .clone()
225    }
226
227    /// Whether the tracked daemon is considered alive (true if tracking disabled).
228    pub fn is_daemon_alive(&self) -> bool {
229        match self
230            .shared
231            .heartbeat_tracker
232            .lock()
233            .expect("heartbeat lock")
234            .as_ref()
235        {
236            Some(t) => t.get_health().is_alive,
237            None => true,
238        }
239    }
240
241    /// Disconnect cause if disconnected.
242    pub async fn disconnect_cause(&self) -> Option<DisconnectCause> {
243        *self.shared.disconnect_cause.lock().await
244    }
245
246    /// Wait until disconnected.
247    pub async fn wait_disconnected(&self) -> DisconnectCause {
248        loop {
249            if let Some(c) = *self.shared.disconnect_cause.lock().await {
250                return c;
251            }
252            self.shared.disconnect_notify.notified().await;
253        }
254    }
255
256    /// Dial + handshake (`connection_init` / `connection_ack` ready).
257    pub async fn connect(&self) -> Result<()> {
258        if self.is_connected() {
259            return Ok(());
260        }
261        let (ws, _) = connect_async(&self.shared.url).await.map_err(|e| {
262            ConnectionError::new(
263                &self.shared.url,
264                1,
265                Box::new(e) as Box<dyn std::error::Error + Send + Sync>,
266            )
267        })?;
268        let (mut write, mut read) = ws.split();
269        let (tx, mut rx) = mpsc::unbounded_channel::<Message>();
270        {
271            let mut slot = self.shared.write_tx.lock().await;
272            *slot = Some(tx);
273        }
274        self.shared.connected.store(true, Ordering::SeqCst);
275        self.shared.reader_alive.store(true, Ordering::SeqCst);
276        *self.shared.disconnect_cause.lock().await = None;
277
278        let shared_r2 = self.shared.clone();
279        let client_for_hb = self.clone();
280        tokio::spawn(async move {
281            while let Some(msg) = rx.recv().await {
282                if write.send(msg).await.is_err() {
283                    break;
284                }
285            }
286            let _ = write.close().await;
287        });
288
289        tokio::spawn(async move {
290            while let Some(item) = read.next().await {
291                match item {
292                    Ok(Message::Text(text)) => {
293                        if let Ok(raw) = decode_message(&text) {
294                            for msg in expand_wire_messages(raw) {
295                                shared_r2.route_inbound(msg).await;
296                            }
297                        }
298                    }
299                    Ok(Message::Ping(data)) => {
300                        let _ = shared_r2.send_raw(Message::Pong(data)).await;
301                    }
302                    Ok(Message::Close(_)) => break,
303                    Ok(_) => {}
304                    Err(_) => break,
305                }
306            }
307            shared_r2.mark_disconnected(DisconnectCause::Unclean).await;
308        });
309
310        // Handshake
311        self.send_envelope(new_connection_init()).await?;
312        let ack = self.wait_connection_ack(Duration::from_secs(15)).await?;
313        if let Some(interval) = ack
314            .get("result")
315            .and_then(|r| r.get("heartbeat_interval_ms"))
316            .and_then(|v| v.as_u64())
317        {
318            *self.shared.heartbeat_interval_ms.lock().await = Some(interval);
319            if interval > 0 {
320                let hb_client = client_for_hb;
321                tokio::spawn(async move {
322                    let period = Duration::from_millis(interval.max(5_000));
323                    while hb_client.is_connection_alive() {
324                        sleep(period).await;
325                        if !hb_client.is_connection_alive() {
326                            break;
327                        }
328                        let _ = hb_client.send_envelope(new_ping()).await;
329                    }
330                });
331            }
332        }
333        Ok(())
334    }
335
336    async fn wait_connection_ack(&self, overall: Duration) -> Result<Value> {
337        let deadline = tokio::time::Instant::now() + overall;
338        loop {
339            let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
340            if remaining.is_zero() {
341                return Err(TimeoutError::new("connection_ack", format!("{overall:?}")).into());
342            }
343            let msg = self
344                .read_event_with_timeout(remaining.min(Duration::from_secs(2)))
345                .await?;
346            let Some(msg) = msg else {
347                continue;
348            };
349            let msg_type = msg.get("type").and_then(|v| v.as_str()).unwrap_or("");
350            if msg_type == "status" {
351                continue;
352            }
353            if msg_type == "error" {
354                let err = parse_daemon_error(&msg);
355                // Retryable readiness codes.
356                if matches!(err.code, -32003..=-32001) {
357                    sleep(Duration::from_millis(50)).await;
358                    self.send_envelope(new_connection_init()).await?;
359                    continue;
360                }
361                return Err(err.into());
362            }
363            if msg_type == "connection_ack" {
364                let state = msg
365                    .get("result")
366                    .and_then(|r| r.get("readiness_state"))
367                    .and_then(|v| v.as_str())
368                    .unwrap_or("");
369                if state == "starting" || state == "warming" {
370                    sleep(Duration::from_millis(50)).await;
371                    self.send_envelope(new_connection_init()).await?;
372                    continue;
373                }
374                if state == "ready" || state.is_empty() {
375                    *self.shared.readiness_state.lock().await = if state.is_empty() {
376                        "ready".into()
377                    } else {
378                        state.into()
379                    };
380                    self.shared.handshake_complete.store(true, Ordering::SeqCst);
381                    return Ok(msg);
382                }
383                return Err(Error::protocol(format!(
384                    "connection_ack readiness_state={state}"
385                )));
386            }
387        }
388    }
389
390    /// Close the connection.
391    pub async fn close(&self) -> Result<()> {
392        if self.is_connected() {
393            let _ = self.notify("disconnect", Map::new()).await;
394        }
395        {
396            let mut tx = self.shared.write_tx.lock().await;
397            *tx = None;
398        }
399        self.shared
400            .handshake_complete
401            .store(false, Ordering::SeqCst);
402        *self.shared.readiness_state.lock().await = String::new();
403        self.shared.mark_disconnected(DisconnectCause::Clean).await;
404        Ok(())
405    }
406
407    /// Re-dial and re-handshake after a connection drop.
408    ///
409    /// Does not re-establish loop subscriptions; follow with
410    /// [`Self::reattach_and_probe`] to resume a loop session.
411    pub async fn reconnect(&self) -> Result<()> {
412        {
413            let mut tx = self.shared.write_tx.lock().await;
414            *tx = None;
415        }
416        self.shared.connected.store(false, Ordering::SeqCst);
417        self.shared.reader_alive.store(false, Ordering::SeqCst);
418        self.shared
419            .handshake_complete
420            .store(false, Ordering::SeqCst);
421        *self.shared.readiness_state.lock().await = String::new();
422        *self.shared.disconnect_cause.lock().await = None;
423        self.shared.rpc_waiters.lock().await.clear();
424        self.shared.inbound.lock().await.clear();
425        self.shared.degraded_notified.store(false, Ordering::SeqCst);
426        self.connect().await
427    }
428
429    /// Send a raw envelope.
430    pub async fn send_envelope(&self, env: Envelope) -> Result<()> {
431        let text = env.to_wire_json()?;
432        self.shared.send_raw(Message::Text(text.into())).await
433    }
434
435    /// Fire-and-forget notification.
436    pub async fn notify(&self, method: &str, params: Map<String, Value>) -> Result<()> {
437        self.send_envelope(new_notification(method, params)).await
438    }
439
440    /// RPC request correlated by id.
441    pub async fn request(
442        &self,
443        method: &str,
444        params: Map<String, Value>,
445        req_timeout: Duration,
446    ) -> Result<Map<String, Value>> {
447        let env = new_request(method, params);
448        let id = env.id.clone().unwrap_or_default();
449        let (tx, rx) = oneshot::channel();
450        self.shared.rpc_waiters.lock().await.insert(id.clone(), tx);
451        if let Err(e) = self.send_envelope(env).await {
452            self.shared.rpc_waiters.lock().await.remove(&id);
453            return Err(e);
454        }
455        match timeout(req_timeout, rx).await {
456            Ok(Ok(Ok(Value::Object(m)))) => Ok(m),
457            Ok(Ok(Ok(other))) => {
458                let mut m = Map::new();
459                m.insert("result".into(), other);
460                Ok(m)
461            }
462            Ok(Ok(Err(de))) => Err(de.into()),
463            Ok(Err(_)) => Err(Error::protocol("rpc waiter dropped")),
464            Err(_) => {
465                self.shared.rpc_waiters.lock().await.remove(&id);
466                Err(TimeoutError::new(method, format!("{req_timeout:?}")).into())
467            }
468        }
469    }
470
471    /// Request with a payload map that may include a `type` field as the method name
472    /// (Go `RequestResponse` parity).
473    pub async fn request_response(
474        &self,
475        payload: Map<String, Value>,
476        fallback_method: &str,
477        req_timeout: Duration,
478    ) -> Result<Map<String, Value>> {
479        let method = payload
480            .get("type")
481            .and_then(|v| v.as_str())
482            .unwrap_or(fallback_method)
483            .to_string();
484        let mut params = Map::new();
485        for (k, v) in payload {
486            if k == "type" || k == "request_id" {
487                continue;
488            }
489            params.insert(k, v);
490        }
491        self.request(&method, params, req_timeout).await
492    }
493
494    /// Subscribe; returns subscription id.
495    pub async fn subscribe(
496        &self,
497        method: &str,
498        params: Map<String, Value>,
499        req_timeout: Duration,
500    ) -> Result<String> {
501        let env = new_subscribe(method, params);
502        let id = env.id.clone().unwrap_or_default();
503        // Subscribe confirmation may arrive as next/complete; we treat send success as ok
504        // and rely on subsequent events. Still register a short waiter for error.
505        let (tx, rx) = oneshot::channel();
506        self.shared.rpc_waiters.lock().await.insert(id.clone(), tx);
507        self.send_envelope(env).await?;
508        // Don't block long — many daemons only send `next` without a response.
509        match timeout(req_timeout.min(Duration::from_millis(500)), rx).await {
510            Ok(Ok(Err(de))) => return Err(de.into()),
511            Ok(Ok(Ok(_))) => {}
512            _ => {
513                self.shared.rpc_waiters.lock().await.remove(&id);
514            }
515        }
516        Ok(id)
517    }
518
519    /// Unsubscribe by id.
520    pub async fn unsubscribe(&self, id: &str) -> Result<()> {
521        self.send_envelope(new_unsubscribe(id)).await
522    }
523
524    /// Read next inbound app event (blocks).
525    pub async fn read_event(&self) -> Result<Option<Value>> {
526        loop {
527            {
528                let mut q = self.shared.inbound.lock().await;
529                if let Some(v) = q.pop_front() {
530                    return Ok(Some(v));
531                }
532            }
533            if !self.is_connection_alive() {
534                return Ok(None);
535            }
536            self.shared.inbound_notify.notified().await;
537        }
538    }
539
540    /// Read with timeout; `None` on timeout.
541    pub async fn read_event_with_timeout(&self, dur: Duration) -> Result<Option<Value>> {
542        match timeout(dur, self.read_event()).await {
543            Ok(r) => r,
544            Err(_) => Ok(None),
545        }
546    }
547
548    /// Clear pending inbound events.
549    pub async fn clear_pending_events(&self) {
550        self.shared.inbound.lock().await.clear();
551    }
552
553    /// Peel stale pending control frames at turn start.
554    pub async fn peel_stale_pending_control_events(&self) -> Vec<String> {
555        let mut labels = Vec::new();
556        let mut kept = VecDeque::new();
557        let mut q = self.shared.inbound.lock().await;
558        while let Some(ev) = q.pop_front() {
559            if let Some(label) = stale_pending_frame_label(&ev) {
560                labels.push(label);
561            } else {
562                kept.push_back(ev);
563            }
564        }
565        *q = kept;
566        labels
567    }
568
569    /// Re-queue an event ahead of subsequent [`Client::read_event`] calls.
570    ///
571    /// Applies priority-aware drop when the pending buffer is full
572    /// (Go `PushPendingEvent` parity).
573    pub async fn push_pending_event(&self, ev: Value) {
574        if !ev.is_object() {
575            return;
576        }
577        let mut q = self.shared.inbound.lock().await;
578        self.shared.enqueue_pending_locked(&mut q, ev);
579        self.shared.inbound_notify.notify_one();
580    }
581
582    /// Override the pending-event cap at runtime (Go `SetInboundMaxSize` parity).
583    pub fn set_inbound_max_size(&self, n: usize) {
584        if n > 0 {
585            self.shared.max_inbound.store(n, Ordering::SeqCst);
586        }
587    }
588
589    /// Spawn a background reader that streams inbound frames on a channel.
590    ///
591    /// The channel closes when the connection ends or the client disconnects.
592    /// Solicited frames (RPC responses / subscription confirmations) are still
593    /// routed through the internal mux and are NOT forwarded on this channel;
594    /// only unsolicited app events are forwarded (Go `ReceiveMessages` parity).
595    ///
596    /// Heartbeat, ping/pong, and delivery-ack handling are still performed by
597    /// the internal reader task; this method exposes the already-routed inbound
598    /// queue as a channel for consumers that prefer pull-style streaming.
599    pub fn receive_messages(&self, buffer: usize) -> mpsc::Receiver<Value> {
600        let (tx, rx) = mpsc::channel(if buffer == 0 { 100 } else { buffer });
601        let shared = self.shared.clone();
602        tokio::spawn(async move {
603            loop {
604                let ev = {
605                    let mut q = shared.inbound.lock().await;
606                    q.pop_front()
607                };
608                if let Some(ev) = ev {
609                    if tx.send(ev).await.is_err() {
610                        return;
611                    }
612                    continue;
613                }
614                if !shared.is_connection_alive() {
615                    return;
616                }
617                shared.inbound_notify.notified().await;
618            }
619        });
620        rx
621    }
622
623    /// Notify `loop_input`.
624    pub async fn send_input(&self, text: &str, opts: SendInputOptions) -> Result<()> {
625        let mut params = Map::new();
626        params.insert("content".into(), json!(text));
627        if let Some(lid) = opts.loop_id {
628            params.insert("loop_id".into(), json!(lid));
629        }
630        if let Some(v) = opts.preferred_subagent {
631            params.insert("preferred_subagent".into(), json!(v));
632        }
633        if let Some(v) = opts.intake_scope {
634            params.insert("intake_scope".into(), json!(v));
635        }
636        if let Some(v) = opts.model {
637            params.insert("model".into(), json!(v));
638        }
639        if let Some(v) = opts.model_params {
640            params.insert("model_params".into(), v);
641        }
642        if let Some(v) = opts.router_profile {
643            params.insert("router_profile".into(), json!(v));
644        }
645        if let Some(v) = opts.attachments {
646            params.insert("attachments".into(), v);
647        }
648        if let Some(v) = opts.intent_hint {
649            params.insert("intent_hint".into(), json!(v));
650        }
651        if let Some(v) = opts.response_schema {
652            params.insert("response_schema".into(), v);
653        }
654        if let Some(v) = opts.response_schema_name {
655            params.insert("response_schema_name".into(), json!(v));
656        }
657        if let Some(v) = opts.response_schema_strict {
658            params.insert("response_schema_strict".into(), json!(v));
659        }
660        if let Some(v) = opts.clarification_mode {
661            params.insert("clarification_mode".into(), json!(v));
662        }
663        if opts.clarification_answer {
664            params.insert("clarification_answer".into(), json!(true));
665        }
666        if let Some(v) = opts.clarification_answers {
667            params.insert("clarification_answers".into(), v);
668        }
669        self.notify("loop_input", params).await
670    }
671
672    /// `loop_new` RPC.
673    pub async fn loop_new(&self, params: Map<String, Value>) -> Result<Map<String, Value>> {
674        self.request("loop_new", params, Duration::from_secs(30))
675            .await
676    }
677
678    /// `loop_list`.
679    pub async fn loop_list(&self, limit: u32) -> Result<Map<String, Value>> {
680        let mut params = Map::new();
681        params.insert("limit".into(), json!(limit));
682        self.request("loop_list", params, Duration::from_secs(15))
683            .await
684    }
685
686    /// `loop_get`.
687    pub async fn loop_get(&self, loop_id: &str) -> Result<Map<String, Value>> {
688        let mut params = Map::new();
689        params.insert("loop_id".into(), json!(loop_id));
690        self.request("loop_get", params, Duration::from_secs(15))
691            .await
692    }
693
694    /// `loop_reattach`.
695    pub async fn loop_reattach(&self, loop_id: &str) -> Result<Map<String, Value>> {
696        let mut params = Map::new();
697        params.insert("loop_id".into(), json!(loop_id));
698        self.request("loop_reattach", params, Duration::from_secs(15))
699            .await
700    }
701
702    /// Subscribe to `loop_events`.
703    pub async fn loop_subscribe(&self, loop_id: &str, stream_delivery: &str) -> Result<String> {
704        let mut params = Map::new();
705        params.insert("loop_id".into(), json!(loop_id));
706        params.insert("stream_delivery".into(), json!(stream_delivery));
707        params.insert("wire_tier".into(), json!("full"));
708        self.subscribe("loop_events", params, Duration::from_secs(30))
709            .await
710    }
711
712    /// `loop_history_fetch`.
713    pub async fn loop_history_fetch(&self, loop_id: &str) -> Result<Map<String, Value>> {
714        let mut params = Map::new();
715        params.insert("loop_id".into(), json!(loop_id));
716        self.request("loop_history_fetch", params, Duration::from_secs(30))
717            .await
718    }
719
720    /// `loop_execution_state_fetch` — focused execution-progress snapshot
721    /// (plan, step_index, iteration, status) for the loop's bound checkpoint thread.
722    pub async fn loop_execution_state_fetch(&self, loop_id: &str) -> Result<Map<String, Value>> {
723        let mut params = Map::new();
724        params.insert("loop_id".into(), json!(loop_id));
725        self.request(
726            "loop_execution_state_fetch",
727            params,
728            Duration::from_secs(30),
729        )
730        .await
731    }
732
733    /// `loop_messages`.
734    pub async fn loop_messages(
735        &self,
736        loop_id: &str,
737        limit: u32,
738        offset: u32,
739    ) -> Result<Map<String, Value>> {
740        let mut params = Map::new();
741        params.insert("loop_id".into(), json!(loop_id));
742        params.insert("limit".into(), json!(limit));
743        params.insert("offset".into(), json!(offset));
744        self.request("loop_messages", params, Duration::from_secs(10))
745            .await
746    }
747
748    /// `loop_state_get`.
749    pub async fn loop_state_get(&self, loop_id: &str) -> Result<Map<String, Value>> {
750        let mut params = Map::new();
751        params.insert("loop_id".into(), json!(loop_id));
752        self.request("loop_state_get", params, Duration::from_secs(30))
753            .await
754    }
755
756    /// `loop_state_update`.
757    pub async fn loop_state_update(
758        &self,
759        loop_id: &str,
760        state: Map<String, Value>,
761    ) -> Result<Map<String, Value>> {
762        let mut params = Map::new();
763        params.insert("loop_id".into(), json!(loop_id));
764        params.insert("state".into(), Value::Object(state));
765        self.request("loop_state_update", params, Duration::from_secs(30))
766            .await
767    }
768
769    /// `loop_tree`.
770    pub async fn loop_tree(
771        &self,
772        loop_id: &str,
773        format: Option<&str>,
774    ) -> Result<Map<String, Value>> {
775        let mut params = Map::new();
776        params.insert("loop_id".into(), json!(loop_id));
777        if let Some(f) = format {
778            if !f.is_empty() {
779                params.insert("format".into(), json!(f));
780            }
781        }
782        self.request("loop_tree", params, Duration::from_secs(15))
783            .await
784    }
785
786    /// `loop_prune`.
787    pub async fn loop_prune(
788        &self,
789        loop_id: &str,
790        retention_days: Option<i32>,
791        dry_run: bool,
792    ) -> Result<Map<String, Value>> {
793        let mut params = Map::new();
794        params.insert("loop_id".into(), json!(loop_id));
795        if let Some(d) = retention_days {
796            if d > 0 {
797                params.insert("retention_days".into(), json!(d));
798            }
799        }
800        if dry_run {
801            params.insert("dry_run".into(), json!(true));
802        }
803        self.request("loop_prune", params, Duration::from_secs(30))
804            .await
805    }
806
807    /// `loop_delete`.
808    pub async fn loop_delete(&self, loop_id: &str) -> Result<Map<String, Value>> {
809        let mut params = Map::new();
810        params.insert("loop_id".into(), json!(loop_id));
811        self.request("loop_delete", params, Duration::from_secs(10))
812            .await
813    }
814
815    /// Detach from a loop by unsubscribing (`subscription_id` from `loop_subscribe`).
816    pub async fn loop_detach(&self, subscription_id: &str) -> Result<()> {
817        self.unsubscribe(subscription_id).await
818    }
819
820    /// Authenticate with access/secret keys.
821    pub async fn authenticate(
822        &self,
823        access_key: &str,
824        secret_key: &str,
825    ) -> Result<Map<String, Value>> {
826        let mut params = Map::new();
827        params.insert("access_key".into(), json!(access_key));
828        params.insert("secret_key".into(), json!(secret_key));
829        self.request("auth", params, Duration::from_secs(15)).await
830    }
831
832    /// Refresh auth token.
833    pub async fn refresh_auth_token(&self, refresh_token: &str) -> Result<Map<String, Value>> {
834        let mut params = Map::new();
835        params.insert("refresh_token".into(), json!(refresh_token));
836        self.request("auth_refresh", params, Duration::from_secs(15))
837            .await
838    }
839
840    /// `job_create` on this long-lived connection.
841    pub async fn job_create(
842        &self,
843        goal: &str,
844        workspace: Option<&str>,
845    ) -> Result<Map<String, Value>> {
846        let mut params = Map::new();
847        params.insert("goal".into(), json!(goal));
848        if let Some(ws) = workspace {
849            params.insert("workspace".into(), json!(ws));
850        }
851        self.request("job_create", params, Duration::from_secs(30))
852            .await
853    }
854
855    /// `job_status`.
856    pub async fn job_status(&self, job_id: &str) -> Result<Map<String, Value>> {
857        let mut params = Map::new();
858        params.insert("job_id".into(), json!(job_id));
859        self.request("job_status", params, Duration::from_secs(15))
860            .await
861    }
862
863    /// `job_pause`.
864    pub async fn job_pause(&self, job_id: &str) -> Result<Map<String, Value>> {
865        let mut params = Map::new();
866        params.insert("job_id".into(), json!(job_id));
867        self.request("job_pause", params, Duration::from_secs(15))
868            .await
869    }
870
871    /// `job_resume`.
872    pub async fn job_resume(&self, job_id: &str) -> Result<Map<String, Value>> {
873        let mut params = Map::new();
874        params.insert("job_id".into(), json!(job_id));
875        self.request("job_resume", params, Duration::from_secs(15))
876            .await
877    }
878
879    /// `job_cancel`.
880    pub async fn job_cancel(&self, job_id: &str) -> Result<Map<String, Value>> {
881        let mut params = Map::new();
882        params.insert("job_id".into(), json!(job_id));
883        self.request("job_cancel", params, Duration::from_secs(15))
884            .await
885    }
886
887    /// `job_dag`.
888    pub async fn job_dag(&self, job_id: &str) -> Result<Map<String, Value>> {
889        let mut params = Map::new();
890        params.insert("job_id".into(), json!(job_id));
891        self.request("job_dag", params, Duration::from_secs(15))
892            .await
893    }
894
895    /// `job_guidance`.
896    pub async fn job_guidance(
897        &self,
898        job_id: &str,
899        content: &str,
900        goal_id: Option<&str>,
901    ) -> Result<Map<String, Value>> {
902        let mut params = Map::new();
903        params.insert("job_id".into(), json!(job_id));
904        params.insert("content".into(), json!(content));
905        if let Some(g) = goal_id {
906            params.insert("goal_id".into(), json!(g));
907        }
908        self.request("job_guidance", params, Duration::from_secs(30))
909            .await
910    }
911
912    /// `autopilot_status`.
913    pub async fn autopilot_status(&self) -> Result<Map<String, Value>> {
914        self.request("autopilot_status", Map::new(), Duration::from_secs(15))
915            .await
916    }
917
918    /// `autopilot_submit`.
919    pub async fn autopilot_submit(
920        &self,
921        description: &str,
922        priority: i32,
923        workspace: Option<&str>,
924    ) -> Result<Map<String, Value>> {
925        let mut params = Map::new();
926        params.insert("description".into(), json!(description));
927        params.insert("priority".into(), json!(priority));
928        if let Some(ws) = workspace {
929            params.insert("workspace".into(), json!(ws));
930        }
931        self.request("autopilot_submit", params, Duration::from_secs(30))
932            .await
933    }
934
935    /// `autopilot_list_goals`.
936    pub async fn autopilot_list_goals(&self) -> Result<Map<String, Value>> {
937        self.request("autopilot_list_goals", Map::new(), Duration::from_secs(15))
938            .await
939    }
940
941    /// `autopilot_get_goal`.
942    pub async fn autopilot_get_goal(&self, goal_id: &str) -> Result<Map<String, Value>> {
943        let mut params = Map::new();
944        params.insert("goal_id".into(), json!(goal_id));
945        self.request("autopilot_get_goal", params, Duration::from_secs(15))
946            .await
947    }
948
949    /// `autopilot_cancel_goal`.
950    pub async fn autopilot_cancel_goal(&self, goal_id: &str) -> Result<Map<String, Value>> {
951        let mut params = Map::new();
952        params.insert("goal_id".into(), json!(goal_id));
953        self.request("autopilot_cancel_goal", params, Duration::from_secs(15))
954            .await
955    }
956
957    /// `autopilot_cancel_all`.
958    pub async fn autopilot_cancel_all(&self) -> Result<Map<String, Value>> {
959        self.request("autopilot_cancel_all", Map::new(), Duration::from_secs(15))
960            .await
961    }
962
963    /// `autopilot_wake`.
964    pub async fn autopilot_wake(&self) -> Result<Map<String, Value>> {
965        self.request("autopilot_wake", Map::new(), Duration::from_secs(15))
966            .await
967    }
968
969    /// `autopilot_dream`.
970    pub async fn autopilot_dream(&self) -> Result<Map<String, Value>> {
971        self.request("autopilot_dream", Map::new(), Duration::from_secs(15))
972            .await
973    }
974
975    /// `autopilot_resume`.
976    pub async fn autopilot_resume(&self, goal_id: &str) -> Result<Map<String, Value>> {
977        let mut params = Map::new();
978        params.insert("goal_id".into(), json!(goal_id));
979        self.request("autopilot_resume", params, Duration::from_secs(15))
980            .await
981    }
982
983    /// `autopilot_list_jobs`.
984    pub async fn autopilot_list_jobs(&self) -> Result<Map<String, Value>> {
985        self.request("autopilot_list_jobs", Map::new(), Duration::from_secs(15))
986            .await
987    }
988
989    /// `autopilot_get_job`.
990    pub async fn autopilot_get_job(&self, job_id: &str) -> Result<Map<String, Value>> {
991        let mut params = Map::new();
992        params.insert("job_id".into(), json!(job_id));
993        self.request("autopilot_get_job", params, Duration::from_secs(15))
994            .await
995    }
996
997    /// Subscribe to `autopilot_events` (long-lived worker stream).
998    pub async fn autopilot_subscribe(&self) -> Result<String> {
999        self.subscribe("autopilot_events", Map::new(), Duration::from_secs(15))
1000            .await
1001    }
1002
1003    /// Unsubscribe from an autopilot events subscription.
1004    pub async fn autopilot_unsubscribe(&self, subscription_id: &str) -> Result<()> {
1005        self.unsubscribe(subscription_id).await
1006    }
1007
1008    /// `cron_add`.
1009    pub async fn cron_add(&self, text: &str, priority: Option<i32>) -> Result<Map<String, Value>> {
1010        let mut params = Map::new();
1011        params.insert("text".into(), json!(text));
1012        if let Some(p) = priority {
1013            params.insert("priority".into(), json!(p));
1014        }
1015        self.request("cron_add", params, Duration::from_secs(15))
1016            .await
1017    }
1018
1019    /// `cron_list`.
1020    pub async fn cron_list(&self, status: Option<&str>) -> Result<Map<String, Value>> {
1021        let mut params = Map::new();
1022        if let Some(s) = status {
1023            params.insert("status".into(), json!(s));
1024        }
1025        self.request("cron_list", params, Duration::from_secs(15))
1026            .await
1027    }
1028
1029    /// `cron_show`.
1030    pub async fn cron_show(&self, job_id: &str) -> Result<Map<String, Value>> {
1031        let mut params = Map::new();
1032        params.insert("job_id".into(), json!(job_id));
1033        self.request("cron_show", params, Duration::from_secs(15))
1034            .await
1035    }
1036
1037    /// `cron_cancel`.
1038    pub async fn cron_cancel(&self, job_id: &str) -> Result<Map<String, Value>> {
1039        let mut params = Map::new();
1040        params.insert("job_id".into(), json!(job_id));
1041        self.request("cron_cancel", params, Duration::from_secs(15))
1042            .await
1043    }
1044
1045    /// `memory_stats`.
1046    pub async fn memory_stats(&self, mode: &str) -> Result<Map<String, Value>> {
1047        let mut params = Map::new();
1048        params.insert("mode".into(), json!(mode));
1049        self.request("memory_stats", params, Duration::from_secs(15))
1050            .await
1051    }
1052
1053    /// `skills_list`.
1054    pub async fn list_skills(&self) -> Result<Map<String, Value>> {
1055        self.request("skills_list", Map::new(), Duration::from_secs(15))
1056            .await
1057    }
1058
1059    /// `models_list`.
1060    pub async fn list_models(&self) -> Result<Map<String, Value>> {
1061        self.request("models_list", Map::new(), Duration::from_secs(15))
1062            .await
1063    }
1064
1065    /// `mcp_status`.
1066    pub async fn mcp_status(&self) -> Result<Map<String, Value>> {
1067        self.request("mcp_status", Map::new(), Duration::from_secs(15))
1068            .await
1069    }
1070
1071    /// `daemon_status`.
1072    pub async fn fetch_daemon_status(&self) -> Result<Map<String, Value>> {
1073        self.request("daemon_status", Map::new(), Duration::from_secs(10))
1074            .await
1075    }
1076
1077    /// `invoke_skill` on this connection (stream socket for turn enqueue).
1078    pub async fn invoke_skill(&self, skill: &str, args: &str) -> Result<Map<String, Value>> {
1079        let mut params = Map::new();
1080        params.insert("skill".into(), json!(skill));
1081        if !args.is_empty() {
1082            params.insert("args".into(), json!(args));
1083        }
1084        self.request("invoke_skill", params, Duration::from_secs(120))
1085            .await
1086    }
1087
1088    /// Reattach + subscribe + `loop_get` probe.
1089    pub async fn reattach_and_probe(&self, loop_id: &str) -> Result<()> {
1090        self.loop_reattach(loop_id).await?;
1091        self.loop_subscribe(loop_id, "adaptive").await?;
1092        match self.loop_get(loop_id).await {
1093            Ok(_) => Ok(()),
1094            Err(Error::Daemon(de)) if de.code == -32200 => {
1095                Err(StaleLoopError::new(loop_id, Some(Box::new(de))).into())
1096            }
1097            Err(e) => Err(StaleLoopError::new(loop_id, Some(Box::new(e))).into()),
1098        }
1099    }
1100
1101    // ----- Structured command shorthands (Go `CommandRequest` parity) -----
1102
1103    /// `command_request` RPC (structured slash command).
1104    ///
1105    /// Mirrors Go `CommandRequest`. Default timeout 30s.
1106    pub async fn command_request(
1107        &self,
1108        command: &str,
1109        loop_id: Option<&str>,
1110        params: Option<Map<String, Value>>,
1111    ) -> Result<Map<String, Value>> {
1112        let mut p = Map::new();
1113        p.insert("command".into(), json!(command));
1114        if let Some(lid) = loop_id {
1115            p.insert("loop_id".into(), json!(lid));
1116        }
1117        if let Some(extra) = params {
1118            p.insert("params".into(), Value::Object(extra));
1119        }
1120        self.request("command_request", p, Duration::from_secs(30))
1121            .await
1122    }
1123
1124    /// `/clear` — clear loop conversation history.
1125    pub async fn command_clear(&self, loop_id: &str) -> Result<Map<String, Value>> {
1126        self.command_request("clear", Some(loop_id), None).await
1127    }
1128
1129    /// `/exit` — stop the loop and mark for exit.
1130    pub async fn command_exit(&self, loop_id: &str) -> Result<Map<String, Value>> {
1131        self.command_request("exit", Some(loop_id), None).await
1132    }
1133
1134    /// `/quit` — alias for `/exit`.
1135    pub async fn command_quit(&self, loop_id: &str) -> Result<Map<String, Value>> {
1136        self.command_request("quit", Some(loop_id), None).await
1137    }
1138
1139    /// `/detach` — mark the loop as detached (continues running server-side).
1140    pub async fn command_detach(&self, loop_id: &str) -> Result<Map<String, Value>> {
1141        self.command_request("detach", Some(loop_id), None).await
1142    }
1143
1144    /// `/cancel` — cancel the running query.
1145    pub async fn command_cancel(&self, loop_id: &str) -> Result<Map<String, Value>> {
1146        self.command_request("cancel", Some(loop_id), None).await
1147    }
1148
1149    /// `/memory` — query memory stats.
1150    pub async fn command_memory(&self, loop_id: &str) -> Result<Map<String, Value>> {
1151        self.command_request("memory", Some(loop_id), None).await
1152    }
1153
1154    /// `/policy` — query the active policy profile.
1155    pub async fn command_policy(&self) -> Result<Map<String, Value>> {
1156        self.command_request("policy", None, None).await
1157    }
1158
1159    /// `/history` — query input history.
1160    pub async fn command_history(&self, loop_id: &str) -> Result<Map<String, Value>> {
1161        self.command_request("history", Some(loop_id), None).await
1162    }
1163
1164    /// `/config` — query daemon configuration.
1165    pub async fn command_config(&self) -> Result<Map<String, Value>> {
1166        self.command_request("config", None, None).await
1167    }
1168
1169    /// `/review` — query conversation review.
1170    pub async fn command_review(&self, loop_id: &str) -> Result<Map<String, Value>> {
1171        self.command_request("review", Some(loop_id), None).await
1172    }
1173
1174    /// `/plan` — query current plan.
1175    pub async fn command_plan(&self, loop_id: &str) -> Result<Map<String, Value>> {
1176        self.command_request("plan", Some(loop_id), None).await
1177    }
1178
1179    /// `/autopilot_dashboard` — show autopilot dashboard.
1180    pub async fn command_autopilot_dashboard(&self, loop_id: &str) -> Result<Map<String, Value>> {
1181        self.command_request("autopilot_dashboard", Some(loop_id), None)
1182            .await
1183    }
1184
1185    // ----- Daemon readiness -----
1186
1187    /// Wait for the protocol-1 `connection_ack` handshake to report `readiness_state == "ready"`.
1188    ///
1189    /// Returns immediately when the handshake already completed during [`Client::connect`];
1190    /// otherwise polls inbound frames for an out-of-band `connection_ack`.
1191    /// Default timeout 10s (Go `WaitForDaemonReady` parity).
1192    pub async fn wait_for_daemon_ready(&self, timeout: Duration) -> Result<Map<String, Value>> {
1193        let timeout = if timeout.is_zero() {
1194            Duration::from_secs(10)
1195        } else {
1196            timeout
1197        };
1198        // Handshake already completed during `connect` — do not drain inbound
1199        // looking for a second `connection_ack` (that stalls and drops events).
1200        if self.is_handshake_complete() {
1201            let state = self.readiness_state().await;
1202            if state.is_empty() || state == "ready" {
1203                let mut m = Map::new();
1204                m.insert("readiness_state".into(), json!("ready"));
1205                return Ok(m);
1206            }
1207            return Err(Error::protocol(format!(
1208                "daemon not ready: readiness_state={state}"
1209            )));
1210        }
1211        let deadline = tokio::time::Instant::now() + timeout;
1212        loop {
1213            let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
1214            if remaining.is_zero() {
1215                return Err(TimeoutError::new("connection_ack", format!("{timeout:?}")).into());
1216            }
1217            let msg = self
1218                .read_event_with_timeout(remaining.min(Duration::from_secs(2)))
1219                .await?;
1220            let Some(msg) = msg else {
1221                continue;
1222            };
1223            let msg_type = msg.get("type").and_then(|v| v.as_str()).unwrap_or("");
1224            if msg_type != "connection_ack" {
1225                continue;
1226            }
1227            let state = msg
1228                .get("result")
1229                .and_then(|r| r.get("readiness_state"))
1230                .and_then(|v| v.as_str())
1231                .unwrap_or("");
1232            if state == "ready" {
1233                return Ok(map_from_value(msg));
1234            }
1235            return Err(Error::protocol(format!(
1236                "daemon not ready: readiness_state={state}"
1237            )));
1238        }
1239    }
1240}
1241
1242/// Convert a `Value` (expected Object) into a `Map<String, Value>`.
1243fn map_from_value(v: Value) -> Map<String, Value> {
1244    match v {
1245        Value::Object(m) => m,
1246        other => {
1247            let mut m = Map::new();
1248            m.insert("result".into(), other);
1249            m
1250        }
1251    }
1252}
1253
1254impl Shared {
1255    fn is_connection_alive(&self) -> bool {
1256        self.reader_alive.load(Ordering::SeqCst) && self.connected.load(Ordering::SeqCst)
1257    }
1258
1259    async fn send_raw(&self, msg: Message) -> Result<()> {
1260        let tx = self.write_tx.lock().await;
1261        let Some(tx) = tx.as_ref() else {
1262            return Err(Error::protocol("not connected"));
1263        };
1264        tx.send(msg)
1265            .map_err(|_| Error::protocol("write channel closed"))?;
1266        Ok(())
1267    }
1268
1269    async fn mark_disconnected(&self, cause: DisconnectCause) {
1270        self.connected.store(false, Ordering::SeqCst);
1271        self.reader_alive.store(false, Ordering::SeqCst);
1272        {
1273            let mut slot = self.write_tx.lock().await;
1274            *slot = None;
1275        }
1276        {
1277            let mut c = self.disconnect_cause.lock().await;
1278            if c.is_none() {
1279                *c = Some(cause);
1280            }
1281        }
1282        // Fail pending RPCs.
1283        let mut waiters = self.rpc_waiters.lock().await;
1284        for (_, tx) in waiters.drain() {
1285            let _ = tx.send(Err(DaemonError::new(-1, "disconnected")));
1286        }
1287        self.inbound_notify.notify_waiters();
1288        self.disconnect_notify.notify_waiters();
1289    }
1290
1291    async fn route_inbound(&self, msg: Value) {
1292        // Auto pong for app-level ping.
1293        if msg.get("type").and_then(|v| v.as_str()) == Some("ping") {
1294            let _ = self
1295                .send_raw(Message::Text(
1296                    new_pong().to_wire_json().unwrap_or_default().into(),
1297                ))
1298                .await;
1299            return;
1300        }
1301
1302        if msg.get("type").and_then(|v| v.as_str()) == Some("pong") {
1303            if let Some(tracker) = self.heartbeat_tracker.lock().expect("hb").as_ref() {
1304                tracker.note_pong();
1305            }
1306            return;
1307        }
1308
1309        // Daemon heartbeat custom events (namespace / type heuristics).
1310        let ns = msg
1311            .get("namespace")
1312            .or_else(|| msg.get("type"))
1313            .and_then(|v| v.as_str())
1314            .unwrap_or("");
1315        if ns.contains("heartbeat") {
1316            if let Some(tracker) = self.heartbeat_tracker.lock().expect("hb").as_ref() {
1317                tracker.update(msg.get("data"));
1318            }
1319        }
1320
1321        if inbound_needs_delivery_ack(&msg) {
1322            let loop_id = extract_loop_id_from_inbound(&msg);
1323            if !loop_id.is_empty() {
1324                let seq = {
1325                    let mut map = self.delivery_seq.lock().await;
1326                    let e = map.entry(loop_id.clone()).or_insert(0);
1327                    *e += 1;
1328                    *e
1329                };
1330                let mut params = Map::new();
1331                params.insert("loop_id".into(), json!(loop_id));
1332                params.insert("seq".into(), json!(seq));
1333                let _ = self
1334                    .send_raw(Message::Text(
1335                        new_notification("delivery_ack", params)
1336                            .to_wire_json()
1337                            .unwrap_or_default()
1338                            .into(),
1339                    ))
1340                    .await;
1341            }
1342        }
1343
1344        let msg_type = msg.get("type").and_then(|v| v.as_str()).unwrap_or("");
1345        let id = msg
1346            .get("id")
1347            .and_then(|v| v.as_str())
1348            .unwrap_or("")
1349            .to_string();
1350
1351        if matches!(msg_type, "response" | "error") && !id.is_empty() {
1352            if let Some(waiter) = self.rpc_waiters.lock().await.remove(&id) {
1353                let result = if msg_type == "error" {
1354                    Err(parse_daemon_error(&msg))
1355                } else {
1356                    Ok(msg
1357                        .get("result")
1358                        .cloned()
1359                        .unwrap_or(Value::Object(Map::new())))
1360                };
1361                let _ = waiter.send(result);
1362                return;
1363            }
1364        }
1365
1366        // Push to inbound queue with priority backpressure.
1367        {
1368            let mut q = self.inbound.lock().await;
1369            self.enqueue_pending_locked(&mut q, msg);
1370        }
1371        self.inbound_notify.notify_one();
1372    }
1373
1374    fn enqueue_pending_locked(&self, q: &mut VecDeque<Value>, ev: Value) {
1375        let max = self.max_inbound.load(Ordering::SeqCst);
1376        if q.len() < max {
1377            q.push_back(ev);
1378            return;
1379        }
1380        // Priority-aware drop: remove highest-priority-number (NORMAL) frame if possible.
1381        let mut drop_idx = None;
1382        let mut drop_pri = -1;
1383        for (i, item) in q.iter().enumerate() {
1384            let p = inbound_frame_drop_priority(item.as_object());
1385            if p > drop_pri {
1386                drop_pri = p;
1387                drop_idx = Some(i);
1388            }
1389        }
1390        let incoming_pri = inbound_frame_drop_priority(ev.as_object());
1391        if let Some(idx) = drop_idx {
1392            if drop_pri >= DROP_PRIORITY_NORMAL {
1393                q.remove(idx);
1394                q.push_back(ev);
1395                self.note_inbound_drop("priority_evict");
1396                return;
1397            }
1398        }
1399        if incoming_pri >= DROP_PRIORITY_NORMAL {
1400            self.note_inbound_drop("queue_full");
1401            return;
1402        }
1403        // Incoming is CRITICAL/HIGH: force-drop oldest to admit it.
1404        if !q.is_empty() {
1405            q.pop_front();
1406            self.note_inbound_drop("priority_evict");
1407        }
1408        q.push_back(ev);
1409    }
1410
1411    fn note_inbound_drop(&self, reason: &str) {
1412        let n = self.inbound_dropped.fetch_add(1, Ordering::SeqCst) + 1;
1413        if !self.degraded_notified.swap(true, Ordering::SeqCst) {
1414            if let Some(cb) = self.stream_degraded_cb.lock().expect("cb").as_ref() {
1415                cb(n, reason.to_string());
1416            }
1417        }
1418    }
1419}
1420
1421fn parse_daemon_error(msg: &Value) -> DaemonError {
1422    if let Some(err) = msg.get("error") {
1423        let code = err.get("code").and_then(|c| c.as_i64()).unwrap_or(-1);
1424        let message = err
1425            .get("message")
1426            .and_then(|m| m.as_str())
1427            .unwrap_or("daemon error")
1428            .to_string();
1429        let data = err.get("data").cloned();
1430        let mut de = DaemonError::new(code, message);
1431        if let Some(d) = data {
1432            de = de.with_data(d);
1433        }
1434        return de;
1435    }
1436    DaemonError::new(
1437        -1,
1438        msg.get("message")
1439            .and_then(|m| m.as_str())
1440            .unwrap_or("daemon error"),
1441    )
1442}
1443
1444/// Re-export unwrap helper for appkit.
1445pub use crate::protocol::unwrap_next as unwrap_next_frame;
1446
1447#[cfg(test)]
1448mod tests {
1449    use super::*;
1450
1451    #[test]
1452    fn send_input_options_default() {
1453        let o = SendInputOptions::default();
1454        assert!(o.loop_id.is_none());
1455        assert!(o.preferred_subagent.is_none());
1456    }
1457}