Skip to main content

cookie_agent_protocol/session/
client.rs

1//! Shared protocol-10 JSON-RPC client session.
2
3mod runtime;
4
5use std::{
6    collections::{HashMap, HashSet},
7    fmt,
8    sync::{
9        Arc, Mutex as StdMutex,
10        atomic::{AtomicUsize, Ordering},
11    },
12    time::Duration,
13};
14
15use crate::{
16    AgentUsageParams, AgentUsageResult, ApprovalListParams, ApprovalListResult,
17    ApprovalRespondParams, ApprovalRespondResult, ClientHello, EventPayload,
18    EventSubscriptionMessage, EventsSubscribeParams, EventsSubscribeResult, GlobalUsageParams,
19    GlobalUsageResult, JsonRpcError, JsonRpcId, McpAuthBeginParams, McpAuthBeginResult,
20    McpAuthCancelParams, McpAuthCancelResult, McpServerAddParams, McpServerEditParams,
21    McpServerListParams, McpServerListResult, McpServerMutationResult, McpServerNameParams,
22    McpServerPersistParams, McpServerSetEnabledParams, MessageFrame, Notification, OutputDelta,
23    OutputGap, OutputSnapshotEnvelope, OutputStream, ProtocolVersion, ProviderConnectParams,
24    ProviderConnectResult, ProviderDisconnectParams, ProviderDisconnectResult, Response,
25    RunCancelParams, RunCancelResult, RunRecallSteerParams, RunRecallSteerResult, RunStartParams,
26    RunStartResult, RunSteerParams, RunSteerResult, RunToolStdinParams, RunToolStdinResult,
27    RuntimeSnapshotResult, ServerHello, SessionChildrenParams, SessionChildrenResult,
28    SessionCompactParams, SessionCompactResult, SessionCreateParams, SessionCreateResult,
29    SessionForkParams, SessionForkResult, SessionGetParams, SessionGetResult, SessionGoalGetParams,
30    SessionGoalGetResult, SessionGoalLifecycleParams, SessionGoalLifecycleResult,
31    SessionGoalSetParams, SessionGoalSetResult, SessionId, SessionListParams, SessionListResult,
32    SessionPermissionClearParams, SessionPermissionGetParams, SessionPermissionGetResult,
33    SessionPermissionMutationResult, SessionPermissionSetParams, SessionProducersParams,
34    SessionProducersResult, SessionRenameParams, SessionRenameResult, SessionResumeParams,
35    SessionResumeResult, SessionRevertParams, SessionRevertResult, SessionSetPermissionModeParams,
36    SessionSetPermissionModeResult, SessionTreeParams, SessionTreeResult, SessionTreeUsageResult,
37    SessionUsageParams, SessionUsageResult, SkillsGetParams, SkillsGetResult, SkillsListParams,
38    SkillsListResult, StoredEvent, ToolCallId, Transport, TransportError,
39};
40use serde::{Serialize, de::DeserializeOwned};
41use serde_json::Value;
42use thiserror::Error;
43use tokio::sync::{Mutex, mpsc, oneshot};
44use tokio::time::Instant;
45use zeroize::{Zeroize, Zeroizing};
46
47const COMMAND_QUEUE_CAPACITY: usize = 128;
48const RECOVERY_ATTEMPTS: usize = 6;
49const REPLAY_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5);
50
51/// The sole ordered delivery stream consumed by the UI.
52///
53/// The connection task is its only producer. Replays are injected into this
54/// stream before the live notifications buffered while their RPC is in flight.
55#[derive(Clone, Debug)]
56pub enum ClientDelivery {
57    Live {
58        message: Box<EventSubscriptionMessage>,
59        generation: u64,
60    },
61    ReplayStart {
62        session_id: SessionId,
63        generation: u64,
64        final_seq: u64,
65        rebuild: bool,
66    },
67    ReplayEvent {
68        session_id: SessionId,
69        generation: u64,
70        final_seq: u64,
71        event: Box<StoredEvent>,
72    },
73    ReplayEnd {
74        session_id: SessionId,
75        generation: u64,
76        final_seq: u64,
77    },
78    OutputSnapshot(OutputSnapshotEnvelope),
79    OutputDelta(OutputDelta),
80    OutputGap(OutputGap),
81    PluginEvent(crate::ExtensionBusEventParams),
82    RecoveryFailed {
83        session_id: Option<SessionId>,
84        error: String,
85    },
86    Disconnected {
87        error: String,
88    },
89    RuntimeChanged(Box<crate::RuntimeChangedNotification>),
90}
91
92/// Consumer of ordered protocol notifications and replay deliveries.
93pub trait ClientEventSink: Send + Sync + 'static {
94    fn deliver(&self, delivery: ClientDelivery);
95
96    /// Whether to retain raw tool output in replay buffers and deliveries.
97    /// Snapshot barrier bookkeeping is performed regardless of this preference.
98    fn wants_raw_tool_output(&self) -> bool {
99        true
100    }
101}
102
103struct DisplayOnlySink(mpsc::UnboundedSender<ClientDelivery>);
104
105impl ClientEventSink for DisplayOnlySink {
106    fn deliver(&self, delivery: ClientDelivery) {
107        self.0.deliver(delivery);
108    }
109
110    fn wants_raw_tool_output(&self) -> bool {
111        false
112    }
113}
114
115/// Client end of the protocol contract over shared session mechanics.
116#[async_trait::async_trait]
117pub trait ClientProtocol: Send + Sync {
118    async fn handshake(&self) -> Result<ServerHello, ClientError>;
119    async fn create_session(
120        &self,
121        params: SessionCreateParams,
122    ) -> Result<SessionCreateResult, ClientError>;
123    async fn list_sessions(
124        &self,
125        params: SessionListParams,
126    ) -> Result<SessionListResult, ClientError>;
127    async fn get_session(&self, params: SessionGetParams) -> Result<SessionGetResult, ClientError>;
128    async fn get_session_goal(
129        &self,
130        params: SessionGoalGetParams,
131    ) -> Result<SessionGoalGetResult, ClientError>;
132    async fn set_session_goal(
133        &self,
134        params: SessionGoalSetParams,
135    ) -> Result<SessionGoalSetResult, ClientError>;
136    async fn change_session_goal_lifecycle(
137        &self,
138        params: SessionGoalLifecycleParams,
139    ) -> Result<SessionGoalLifecycleResult, ClientError>;
140    async fn session_producers(
141        &self,
142        params: SessionProducersParams,
143    ) -> Result<SessionProducersResult, ClientError>;
144    async fn session_usage(
145        &self,
146        params: SessionUsageParams,
147    ) -> Result<SessionUsageResult, ClientError>;
148    async fn session_tree_usage(
149        &self,
150        params: SessionUsageParams,
151    ) -> Result<SessionTreeUsageResult, ClientError>;
152    async fn agent_usage(&self, params: AgentUsageParams) -> Result<AgentUsageResult, ClientError>;
153    async fn global_usage(&self) -> Result<GlobalUsageResult, ClientError>;
154    async fn session_children(
155        &self,
156        params: SessionChildrenParams,
157    ) -> Result<SessionChildrenResult, ClientError>;
158    async fn session_tree(
159        &self,
160        params: SessionTreeParams,
161    ) -> Result<SessionTreeResult, ClientError>;
162    async fn resume_session(
163        &self,
164        params: SessionResumeParams,
165    ) -> Result<SessionResumeResult, ClientError>;
166    async fn rename_session(
167        &self,
168        params: SessionRenameParams,
169    ) -> Result<SessionRenameResult, ClientError>;
170    async fn set_permission_mode(
171        &self,
172        params: SessionSetPermissionModeParams,
173    ) -> Result<SessionSetPermissionModeResult, ClientError>;
174    async fn get_session_permissions(
175        &self,
176        params: SessionPermissionGetParams,
177    ) -> Result<SessionPermissionGetResult, ClientError>;
178    async fn set_session_permission(
179        &self,
180        params: SessionPermissionSetParams,
181    ) -> Result<SessionPermissionMutationResult, ClientError>;
182    async fn clear_session_permission(
183        &self,
184        params: SessionPermissionClearParams,
185    ) -> Result<SessionPermissionMutationResult, ClientError>;
186    async fn list_skills(&self, params: SkillsListParams) -> Result<SkillsListResult, ClientError>;
187    async fn get_skill(&self, params: SkillsGetParams) -> Result<SkillsGetResult, ClientError>;
188    async fn compact_session(
189        &self,
190        params: SessionCompactParams,
191    ) -> Result<SessionCompactResult, ClientError>;
192    async fn revert_session(
193        &self,
194        params: SessionRevertParams,
195    ) -> Result<SessionRevertResult, ClientError>;
196    async fn fork_session(
197        &self,
198        params: SessionForkParams,
199    ) -> Result<SessionForkResult, ClientError>;
200    async fn start_run(&self, params: RunStartParams) -> Result<RunStartResult, ClientError>;
201    async fn steer_run(&self, params: RunSteerParams) -> Result<RunSteerResult, ClientError>;
202    async fn recall_steer(
203        &self,
204        params: RunRecallSteerParams,
205    ) -> Result<RunRecallSteerResult, ClientError>;
206    async fn cancel_run(&self, params: RunCancelParams) -> Result<RunCancelResult, ClientError>;
207    async fn tool_stdin(
208        &self,
209        params: RunToolStdinParams,
210    ) -> Result<RunToolStdinResult, ClientError>;
211    async fn respond_approval(
212        &self,
213        params: ApprovalRespondParams,
214    ) -> Result<ApprovalRespondResult, ClientError>;
215    async fn list_approvals(
216        &self,
217        params: ApprovalListParams,
218    ) -> Result<ApprovalListResult, ClientError>;
219    async fn begin_mcp_auth(
220        &self,
221        params: McpAuthBeginParams,
222    ) -> Result<McpAuthBeginResult, ClientError>;
223    async fn cancel_mcp_auth(
224        &self,
225        params: McpAuthCancelParams,
226    ) -> Result<McpAuthCancelResult, ClientError>;
227    async fn list_mcp_servers(&self) -> Result<McpServerListResult, ClientError>;
228    async fn add_mcp_server(
229        &self,
230        params: McpServerAddParams,
231    ) -> Result<McpServerMutationResult, ClientError>;
232    async fn edit_mcp_server(
233        &self,
234        params: McpServerEditParams,
235    ) -> Result<McpServerMutationResult, ClientError>;
236    async fn remove_mcp_server(
237        &self,
238        params: McpServerNameParams,
239    ) -> Result<McpServerMutationResult, ClientError>;
240    async fn set_mcp_server_enabled(
241        &self,
242        params: McpServerSetEnabledParams,
243    ) -> Result<McpServerMutationResult, ClientError>;
244    async fn reconnect_mcp_server(
245        &self,
246        params: McpServerNameParams,
247    ) -> Result<McpServerMutationResult, ClientError>;
248    async fn persist_mcp_server(
249        &self,
250        params: McpServerPersistParams,
251    ) -> Result<McpServerMutationResult, ClientError>;
252    async fn subscribe_events(
253        &self,
254        session_id: SessionId,
255        cursor: Option<u64>,
256    ) -> Result<(), ClientError>;
257    async fn runtime_snapshot(&self) -> Result<RuntimeSnapshotResult, ClientError>;
258    async fn connect_provider(
259        &self,
260        params: ProviderConnectParams,
261    ) -> Result<ProviderConnectResult, ClientError>;
262    async fn disconnect_provider(
263        &self,
264        params: ProviderDisconnectParams,
265    ) -> Result<ProviderDisconnectResult, ClientError>;
266    fn recover_session(&self, session_id: SessionId, full_replay: bool);
267    fn shutdown(&self);
268}
269
270impl ClientEventSink for mpsc::UnboundedSender<ClientDelivery> {
271    fn deliver(&self, delivery: ClientDelivery) {
272        let _ = self.send(delivery);
273    }
274}
275
276/// Errors returned by the transport or a JSON-RPC operation.
277#[derive(Debug, Error)]
278pub enum ClientError {
279    #[error("transport closed")]
280    Closed,
281    #[error("transport failed: {0}")]
282    Transport(String),
283    #[error("invalid JSON-RPC frame: {0}")]
284    InvalidFrame(#[from] serde_json::Error),
285    #[error("{}", crate::diagnostics::rpc(.0))]
286    Rpc(JsonRpcError),
287    #[error("websocket error: {0}")]
288    WebSocket(String),
289    #[error("daemon authentication token is unavailable")]
290    TokenUnavailable,
291    #[error("daemon authentication token path is unsafe")]
292    UnsafeToken,
293    #[error("a subscription replay is already in progress")]
294    ReplayInProgress,
295    #[error("subscription replay response timed out")]
296    ReplayTimedOut,
297}
298
299struct ReplayRequest {
300    session_id: SessionId,
301    generation: u64,
302    rebuild: bool,
303    attempt: u64,
304}
305
306struct Command {
307    id: i64,
308    method: String,
309    params: SerializedParams,
310    replay: Option<ReplayRequest>,
311    response: oneshot::Sender<Result<Value, ClientError>>,
312}
313
314struct SerializedParams {
315    value: Value,
316    sensitive: bool,
317}
318
319impl Drop for SerializedParams {
320    fn drop(&mut self) {
321        if self.sensitive {
322            zeroize_json(&mut self.value);
323            record_sensitive_serialized_wipe();
324        }
325    }
326}
327
328pub(in crate::session::client) struct SensitiveJson(Value);
329
330impl SensitiveJson {
331    pub(in crate::session::client) fn object() -> Self {
332        Self(Value::Object(serde_json::Map::new()))
333    }
334
335    pub(in crate::session::client) fn object_mut(&mut self) -> &mut serde_json::Map<String, Value> {
336        self.0
337            .as_object_mut()
338            .expect("sensitive JSON owner was created as an object")
339    }
340
341    fn take(&mut self) -> Value {
342        std::mem::replace(&mut self.0, Value::Null)
343    }
344}
345
346impl Drop for SensitiveJson {
347    fn drop(&mut self) {
348        if self.0 != Value::Null {
349            zeroize_json(&mut self.0);
350            record_sensitive_serialized_wipe();
351        }
352    }
353}
354
355#[derive(Serialize)]
356struct BorrowedRequest<'a> {
357    jsonrpc: &'static str,
358    id: i64,
359    method: &'a str,
360    params: &'a Value,
361}
362
363enum OutboundFrame {
364    Public(MessageFrame),
365    Sensitive(SensitiveFrame),
366}
367
368/// A serialized secret-bearing request retained in a wiping buffer until the
369/// transport accepts ownership. Once dispatched, WebSocket/channel/kernel
370/// buffers are transport-owned and cannot honestly be promised to be wiped by
371/// the TUI.
372struct SensitiveFrame {
373    text: Zeroizing<String>,
374}
375
376impl SensitiveFrame {
377    fn new(text: String) -> Self {
378        Self {
379            text: Zeroizing::new(text),
380        }
381    }
382}
383
384impl fmt::Debug for SensitiveFrame {
385    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
386        formatter.write_str("SensitiveFrame(<redacted>)")
387    }
388}
389
390impl Drop for SensitiveFrame {
391    fn drop(&mut self) {
392        if !self.text.is_empty() {
393            self.text.zeroize();
394            record_sensitive_frame_wipe();
395        }
396    }
397}
398
399struct PendingCommand {
400    replay: Option<ReplayRequest>,
401    response: oneshot::Sender<Result<Value, ClientError>>,
402}
403
404#[derive(Default)]
405struct Subscription {
406    cursor: u64,
407    rollback_cursor: u64,
408    generation: u64,
409    next_attempt: u64,
410    active_attempt: u64,
411    fetching: bool,
412    rebuild: bool,
413    final_seq: u64,
414    replay_tools: HashSet<ToolCallId>,
415    awaiting_snapshots: HashSet<(ToolCallId, String)>,
416    snapshot_deadline: Option<Instant>,
417    buffered: Vec<ClientDelivery>,
418    recovery_requested: Option<bool>,
419}
420
421struct RecoveryQueue {
422    sender: mpsc::UnboundedSender<(bool, Option<SessionId>)>,
423    state: StdMutex<RecoveryQueueState>,
424}
425
426#[derive(Default)]
427struct RecoveryQueueState {
428    queued: bool,
429    follow_up: bool,
430    follow_up_full: bool,
431    follow_up_session: Option<SessionId>,
432}
433
434enum ConnectionControl {
435    RecoveryFailed {
436        session_id: Option<SessionId>,
437        error: String,
438    },
439}
440
441/// A cloneable client handle. Calls and the single ordered delivery stream
442/// share one serialized message stream, regardless of transport.
443#[derive(Clone)]
444pub struct Client {
445    commands: mpsc::Sender<Command>,
446    deliveries: Arc<StdMutex<Option<mpsc::UnboundedReceiver<ClientDelivery>>>>,
447    subscriptions: Arc<Mutex<HashMap<SessionId, Subscription>>>,
448    recovery: Arc<RecoveryQueue>,
449    shutdown: Arc<ClientShutdown>,
450    #[cfg(test)]
451    pending_command_count: Arc<AtomicUsize>,
452}
453
454struct ClientShutdown(tokio_util::sync::CancellationToken);
455
456impl Drop for ClientShutdown {
457    fn drop(&mut self) {
458        self.0.cancel();
459    }
460}
461
462impl Client {
463    /// Connect an already-created message stream and start its routing task.
464    pub fn connect_stream<T>(transport: T) -> Self
465    where
466        T: Transport + 'static,
467    {
468        // The connection task is the RPC router and never awaits UI
469        // consumption. This sole-consumer queue is lossless; a permanently
470        // stalled UI may grow memory, but it is terminal rather than lossy.
471        let (delivery_sender, delivery_receiver) = mpsc::unbounded_channel();
472        Self::connect_stream_with_sink(transport, delivery_sender, Some(delivery_receiver))
473    }
474
475    /// Connect a display-only consumer without queuing raw tool snapshots,
476    /// deltas, or gaps. Durable events (including display and terminal results)
477    /// and replay barriers are unchanged. Runtime output retention is unaffected.
478    pub fn connect_display_stream<T>(transport: T) -> Self
479    where
480        T: Transport + 'static,
481    {
482        let (sender, receiver) = mpsc::unbounded_channel();
483        Self::connect_stream_with_sink(transport, DisplayOnlySink(sender), Some(receiver))
484    }
485
486    /// Connect a transport and forward ordered protocol deliveries to a consumer-defined sink.
487    pub fn connect_with_event_sink<T, E>(transport: T, sink: E) -> Self
488    where
489        T: Transport + 'static,
490        E: ClientEventSink,
491    {
492        Self::connect_stream_with_sink(transport, sink, None)
493    }
494
495    fn connect_stream_with_sink<T, E>(
496        transport: T,
497        sink: E,
498        delivery_receiver: Option<mpsc::UnboundedReceiver<ClientDelivery>>,
499    ) -> Self
500    where
501        T: Transport + 'static,
502        E: ClientEventSink,
503    {
504        let (commands, command_rx) = mpsc::channel(COMMAND_QUEUE_CAPACITY);
505        let deliveries = Arc::new(StdMutex::new(delivery_receiver));
506        let subscriptions = Arc::new(Mutex::new(HashMap::new()));
507        let (recovery_sender, recovery_receiver) = mpsc::unbounded_channel();
508        let recovery = Arc::new(RecoveryQueue {
509            sender: recovery_sender,
510            state: StdMutex::new(RecoveryQueueState::default()),
511        });
512        let (control_sender, control_rx) = mpsc::unbounded_channel();
513        let pending_command_count = Arc::new(AtomicUsize::new(0));
514        let shutdown = Arc::new(ClientShutdown(tokio_util::sync::CancellationToken::new()));
515        tokio::spawn(connection_task(
516            transport,
517            ConnectionTask {
518                commands: command_rx,
519                controls: control_rx,
520                deliveries: Arc::new(sink),
521                subscriptions: subscriptions.clone(),
522                recovery: recovery.clone(),
523                pending_command_count: pending_command_count.clone(),
524                shutdown: shutdown.0.clone(),
525            },
526        ));
527        let client = Self {
528            commands,
529            deliveries,
530            subscriptions,
531            recovery: recovery.clone(),
532            shutdown,
533            #[cfg(test)]
534            pending_command_count,
535        };
536        spawn_recovery_worker(
537            client.commands.clone(),
538            client.subscriptions.clone(),
539            recovery_receiver,
540            recovery,
541            control_sender,
542            REPLAY_RESPONSE_TIMEOUT,
543        );
544        client
545    }
546
547    /// Complete the versioned protocol handshake.
548    pub async fn handshake(&self) -> Result<ServerHello, ClientError> {
549        let hello: ServerHello = self
550            .call(
551                "handshake",
552                &ClientHello {
553                    protocol_version: ProtocolVersion::current(),
554                },
555            )
556            .await?;
557        Ok(hello)
558    }
559
560    /// Take the sole live, replay, and output delivery receiver. It never
561    /// backpressures the connection task.
562    #[must_use]
563    pub fn subscribe_deliveries(&self) -> Option<mpsc::UnboundedReceiver<ClientDelivery>> {
564        self.deliveries
565            .lock()
566            .expect("delivery receiver lock")
567            .take()
568    }
569
570    /// End the connection and fail outstanding calls with [`ClientError::Closed`].
571    pub fn shutdown(&self) {
572        self.shutdown.0.cancel();
573    }
574
575    pub async fn create_session(
576        &self,
577        params: SessionCreateParams,
578    ) -> Result<SessionCreateResult, ClientError> {
579        self.call("session.create", &params).await
580    }
581
582    pub async fn list_sessions(
583        &self,
584        params: SessionListParams,
585    ) -> Result<SessionListResult, ClientError> {
586        self.call("session.list", &params).await
587    }
588
589    pub async fn get_session(
590        &self,
591        params: SessionGetParams,
592    ) -> Result<SessionGetResult, ClientError> {
593        self.call("session.get", &params).await
594    }
595
596    pub async fn get_session_goal(
597        &self,
598        params: SessionGoalGetParams,
599    ) -> Result<SessionGoalGetResult, ClientError> {
600        self.call(crate::SESSION_GOAL_GET_METHOD, &params).await
601    }
602
603    pub async fn set_session_goal(
604        &self,
605        params: SessionGoalSetParams,
606    ) -> Result<SessionGoalSetResult, ClientError> {
607        self.call(crate::SESSION_GOAL_SET_METHOD, &params).await
608    }
609
610    pub async fn change_session_goal_lifecycle(
611        &self,
612        params: SessionGoalLifecycleParams,
613    ) -> Result<SessionGoalLifecycleResult, ClientError> {
614        self.call(crate::SESSION_GOAL_LIFECYCLE_METHOD, &params)
615            .await
616    }
617
618    pub async fn session_producers(
619        &self,
620        params: SessionProducersParams,
621    ) -> Result<SessionProducersResult, ClientError> {
622        self.call(crate::SESSION_PRODUCERS_METHOD, &params).await
623    }
624
625    pub async fn session_usage(
626        &self,
627        params: SessionUsageParams,
628    ) -> Result<SessionUsageResult, ClientError> {
629        self.call("session.usage", &params).await
630    }
631
632    pub async fn session_tree_usage(
633        &self,
634        params: SessionUsageParams,
635    ) -> Result<SessionTreeUsageResult, ClientError> {
636        self.call("session.tree_usage", &params).await
637    }
638
639    pub async fn agent_usage(
640        &self,
641        params: AgentUsageParams,
642    ) -> Result<AgentUsageResult, ClientError> {
643        self.call("agent.usage", &params).await
644    }
645
646    pub async fn global_usage(&self) -> Result<GlobalUsageResult, ClientError> {
647        self.call("usage.global", &GlobalUsageParams {}).await
648    }
649
650    pub async fn session_children(
651        &self,
652        params: SessionChildrenParams,
653    ) -> Result<SessionChildrenResult, ClientError> {
654        self.call("session.children", &params).await
655    }
656
657    pub async fn session_tree(
658        &self,
659        params: SessionTreeParams,
660    ) -> Result<SessionTreeResult, ClientError> {
661        self.call("session.tree", &params).await
662    }
663
664    pub async fn resume_session(
665        &self,
666        params: SessionResumeParams,
667    ) -> Result<SessionResumeResult, ClientError> {
668        self.call("session.resume", &params).await
669    }
670
671    pub async fn rename_session(
672        &self,
673        params: SessionRenameParams,
674    ) -> Result<SessionRenameResult, ClientError> {
675        self.call("session.rename", &params).await
676    }
677
678    pub async fn set_permission_mode(
679        &self,
680        params: SessionSetPermissionModeParams,
681    ) -> Result<SessionSetPermissionModeResult, ClientError> {
682        self.call("session.set_permission_mode", &params).await
683    }
684
685    pub async fn get_session_permissions(
686        &self,
687        params: SessionPermissionGetParams,
688    ) -> Result<SessionPermissionGetResult, ClientError> {
689        self.call("session.permission.get", &params).await
690    }
691
692    pub async fn set_session_permission(
693        &self,
694        params: SessionPermissionSetParams,
695    ) -> Result<SessionPermissionMutationResult, ClientError> {
696        self.call("session.permission.set", &params).await
697    }
698
699    pub async fn clear_session_permission(
700        &self,
701        params: SessionPermissionClearParams,
702    ) -> Result<SessionPermissionMutationResult, ClientError> {
703        self.call("session.permission.clear", &params).await
704    }
705
706    pub async fn list_skills(
707        &self,
708        params: SkillsListParams,
709    ) -> Result<SkillsListResult, ClientError> {
710        self.call("skills.list", &params).await
711    }
712
713    pub async fn get_skill(&self, params: SkillsGetParams) -> Result<SkillsGetResult, ClientError> {
714        self.call("skills.get", &params).await
715    }
716
717    pub async fn compact_session(
718        &self,
719        params: SessionCompactParams,
720    ) -> Result<SessionCompactResult, ClientError> {
721        self.call("session.compact", &params).await
722    }
723
724    pub async fn revert_session(
725        &self,
726        params: SessionRevertParams,
727    ) -> Result<SessionRevertResult, ClientError> {
728        self.call("session.revert", &params).await
729    }
730
731    pub async fn fork_session(
732        &self,
733        params: SessionForkParams,
734    ) -> Result<SessionForkResult, ClientError> {
735        self.call("session.fork", &params).await
736    }
737
738    pub async fn start_run(&self, params: RunStartParams) -> Result<RunStartResult, ClientError> {
739        self.call("run.start", &params).await
740    }
741
742    pub async fn steer_run(&self, params: RunSteerParams) -> Result<RunSteerResult, ClientError> {
743        self.call("run.steer", &params).await
744    }
745
746    pub async fn recall_steer(
747        &self,
748        params: RunRecallSteerParams,
749    ) -> Result<RunRecallSteerResult, ClientError> {
750        self.call("run.recall_steer", &params).await
751    }
752
753    pub async fn cancel_run(
754        &self,
755        params: RunCancelParams,
756    ) -> Result<RunCancelResult, ClientError> {
757        self.call("run.cancel", &params).await
758    }
759
760    pub async fn tool_stdin(
761        &self,
762        params: RunToolStdinParams,
763    ) -> Result<RunToolStdinResult, ClientError> {
764        self.call("run.tool_stdin", &params).await
765    }
766
767    pub async fn respond_approval(
768        &self,
769        params: ApprovalRespondParams,
770    ) -> Result<ApprovalRespondResult, ClientError> {
771        self.call("approval.respond", &params).await
772    }
773
774    pub async fn list_approvals(
775        &self,
776        params: ApprovalListParams,
777    ) -> Result<ApprovalListResult, ClientError> {
778        self.call("approval.list", &params).await
779    }
780
781    pub async fn begin_mcp_auth(
782        &self,
783        params: McpAuthBeginParams,
784    ) -> Result<McpAuthBeginResult, ClientError> {
785        self.call("mcp.auth.begin", &params).await
786    }
787
788    pub async fn cancel_mcp_auth(
789        &self,
790        params: McpAuthCancelParams,
791    ) -> Result<McpAuthCancelResult, ClientError> {
792        self.call("mcp.auth.cancel", &params).await
793    }
794
795    pub async fn list_mcp_servers(&self) -> Result<McpServerListResult, ClientError> {
796        self.call("mcp.server.list", &McpServerListParams {}).await
797    }
798
799    pub async fn add_mcp_server(
800        &self,
801        params: McpServerAddParams,
802    ) -> Result<McpServerMutationResult, ClientError> {
803        self.call("mcp.server.add", &params).await
804    }
805
806    pub async fn edit_mcp_server(
807        &self,
808        params: McpServerEditParams,
809    ) -> Result<McpServerMutationResult, ClientError> {
810        self.call("mcp.server.edit", &params).await
811    }
812
813    pub async fn remove_mcp_server(
814        &self,
815        params: McpServerNameParams,
816    ) -> Result<McpServerMutationResult, ClientError> {
817        self.call("mcp.server.remove", &params).await
818    }
819
820    pub async fn set_mcp_server_enabled(
821        &self,
822        params: McpServerSetEnabledParams,
823    ) -> Result<McpServerMutationResult, ClientError> {
824        self.call("mcp.server.set_enabled", &params).await
825    }
826
827    pub async fn reconnect_mcp_server(
828        &self,
829        params: McpServerNameParams,
830    ) -> Result<McpServerMutationResult, ClientError> {
831        self.call("mcp.server.reconnect", &params).await
832    }
833
834    pub async fn persist_mcp_server(
835        &self,
836        params: McpServerPersistParams,
837    ) -> Result<McpServerMutationResult, ClientError> {
838        self.call("mcp.server.persist", &params).await
839    }
840
841    /// Start an initial cursor replay. Its contents are delivered only through
842    /// [`Client::subscribe_deliveries`].
843    pub async fn subscribe_events(
844        &self,
845        session_id: SessionId,
846        cursor: Option<u64>,
847    ) -> Result<(), ClientError> {
848        let cursor = cursor.unwrap_or(0);
849        let request = self
850            .prepare_subscription(session_id, cursor, false, cursor == 0)
851            .await?;
852        if let Err(error) = request_replay(&self.commands, request, cursor).await {
853            self.abort_replay(session_id).await;
854            Self::schedule_recovery_queue(&self.recovery, cursor == 0, Some(session_id));
855            return Err(error);
856        }
857        Ok(())
858    }
859
860    /// Recover one session. `full_replay` is used when its local projection is
861    /// no longer trustworthy.
862    pub fn recover_session(&self, session_id: SessionId, full_replay: bool) {
863        Self::schedule_recovery_queue(&self.recovery, full_replay, Some(session_id));
864    }
865
866    fn schedule_recovery_queue(
867        recovery: &Arc<RecoveryQueue>,
868        full_replay: bool,
869        session_id: Option<SessionId>,
870    ) {
871        let mut state = recovery.state.lock().expect("recovery queue lock");
872        if state.queued {
873            state.follow_up = true;
874            state.follow_up_full |= full_replay;
875            if state.follow_up_session != session_id {
876                state.follow_up_session = None;
877            }
878            return;
879        }
880        state.queued = true;
881        state.follow_up_session = session_id;
882        if recovery.sender.send((full_replay, session_id)).is_err() {
883            state.queued = false;
884        }
885    }
886
887    async fn prepare_subscription(
888        &self,
889        session_id: SessionId,
890        cursor: u64,
891        recovering: bool,
892        rebuild: bool,
893    ) -> Result<ReplayRequest, ClientError> {
894        prepare_subscription(&self.subscriptions, session_id, cursor, recovering, rebuild).await
895    }
896
897    async fn abort_replay(&self, session_id: SessionId) {
898        let mut subscriptions = self.subscriptions.lock().await;
899        if let Some(subscription) = subscriptions.get_mut(&session_id) {
900            subscription.cursor = subscription.rollback_cursor;
901            subscription.fetching = false;
902            subscription.awaiting_snapshots.clear();
903            subscription.replay_tools.clear();
904        }
905    }
906
907    pub async fn call<P, R>(&self, method: &str, params: &P) -> Result<R, ClientError>
908    where
909        P: Serialize + ?Sized,
910        R: DeserializeOwned,
911    {
912        let value = send_command(&self.commands, method, params, None, false).await?;
913        Ok(serde_json::from_value(value)?)
914    }
915
916    /// Send a secret-bearing request from wiping process-owned buffers.
917    ///
918    /// The owned params are dropped immediately after serialization. The
919    /// serialized JSON is wiped when dispatch completes or is cancelled.
920    pub async fn call_sensitive<P, R>(&self, method: &str, params: P) -> Result<R, ClientError>
921    where
922        P: Serialize,
923        R: DeserializeOwned,
924    {
925        let value = SensitiveJson(serde_json::to_value(&params)?);
926        drop(params);
927        let result = send_sensitive_command(&self.commands, method, value).await?;
928        Ok(serde_json::from_value(result)?)
929    }
930
931    #[cfg(test)]
932    fn pending_command_count(&self) -> usize {
933        self.pending_command_count.load(Ordering::Relaxed)
934    }
935}
936
937#[async_trait::async_trait]
938impl ClientProtocol for Client {
939    async fn handshake(&self) -> Result<ServerHello, ClientError> {
940        Client::handshake(self).await
941    }
942
943    async fn create_session(
944        &self,
945        params: SessionCreateParams,
946    ) -> Result<SessionCreateResult, ClientError> {
947        Client::create_session(self, params).await
948    }
949    async fn list_sessions(
950        &self,
951        params: SessionListParams,
952    ) -> Result<SessionListResult, ClientError> {
953        Client::list_sessions(self, params).await
954    }
955    async fn get_session(&self, params: SessionGetParams) -> Result<SessionGetResult, ClientError> {
956        Client::get_session(self, params).await
957    }
958    async fn get_session_goal(
959        &self,
960        params: SessionGoalGetParams,
961    ) -> Result<SessionGoalGetResult, ClientError> {
962        Client::get_session_goal(self, params).await
963    }
964    async fn set_session_goal(
965        &self,
966        params: SessionGoalSetParams,
967    ) -> Result<SessionGoalSetResult, ClientError> {
968        Client::set_session_goal(self, params).await
969    }
970    async fn change_session_goal_lifecycle(
971        &self,
972        params: SessionGoalLifecycleParams,
973    ) -> Result<SessionGoalLifecycleResult, ClientError> {
974        Client::change_session_goal_lifecycle(self, params).await
975    }
976    async fn session_producers(
977        &self,
978        params: SessionProducersParams,
979    ) -> Result<SessionProducersResult, ClientError> {
980        Client::session_producers(self, params).await
981    }
982    async fn session_usage(
983        &self,
984        params: SessionUsageParams,
985    ) -> Result<SessionUsageResult, ClientError> {
986        Client::session_usage(self, params).await
987    }
988    async fn session_tree_usage(
989        &self,
990        params: SessionUsageParams,
991    ) -> Result<SessionTreeUsageResult, ClientError> {
992        Client::session_tree_usage(self, params).await
993    }
994    async fn agent_usage(&self, params: AgentUsageParams) -> Result<AgentUsageResult, ClientError> {
995        Client::agent_usage(self, params).await
996    }
997    async fn global_usage(&self) -> Result<GlobalUsageResult, ClientError> {
998        Client::global_usage(self).await
999    }
1000    async fn session_children(
1001        &self,
1002        params: SessionChildrenParams,
1003    ) -> Result<SessionChildrenResult, ClientError> {
1004        Client::session_children(self, params).await
1005    }
1006    async fn session_tree(
1007        &self,
1008        params: SessionTreeParams,
1009    ) -> Result<SessionTreeResult, ClientError> {
1010        Client::session_tree(self, params).await
1011    }
1012    async fn resume_session(
1013        &self,
1014        params: SessionResumeParams,
1015    ) -> Result<SessionResumeResult, ClientError> {
1016        Client::resume_session(self, params).await
1017    }
1018    async fn rename_session(
1019        &self,
1020        params: SessionRenameParams,
1021    ) -> Result<SessionRenameResult, ClientError> {
1022        Client::rename_session(self, params).await
1023    }
1024    async fn set_permission_mode(
1025        &self,
1026        params: SessionSetPermissionModeParams,
1027    ) -> Result<SessionSetPermissionModeResult, ClientError> {
1028        Client::set_permission_mode(self, params).await
1029    }
1030    async fn get_session_permissions(
1031        &self,
1032        params: SessionPermissionGetParams,
1033    ) -> Result<SessionPermissionGetResult, ClientError> {
1034        Client::get_session_permissions(self, params).await
1035    }
1036    async fn set_session_permission(
1037        &self,
1038        params: SessionPermissionSetParams,
1039    ) -> Result<SessionPermissionMutationResult, ClientError> {
1040        Client::set_session_permission(self, params).await
1041    }
1042    async fn clear_session_permission(
1043        &self,
1044        params: SessionPermissionClearParams,
1045    ) -> Result<SessionPermissionMutationResult, ClientError> {
1046        Client::clear_session_permission(self, params).await
1047    }
1048    async fn list_skills(&self, params: SkillsListParams) -> Result<SkillsListResult, ClientError> {
1049        Client::list_skills(self, params).await
1050    }
1051    async fn get_skill(&self, params: SkillsGetParams) -> Result<SkillsGetResult, ClientError> {
1052        Client::get_skill(self, params).await
1053    }
1054    async fn compact_session(
1055        &self,
1056        params: SessionCompactParams,
1057    ) -> Result<SessionCompactResult, ClientError> {
1058        Client::compact_session(self, params).await
1059    }
1060    async fn revert_session(
1061        &self,
1062        params: SessionRevertParams,
1063    ) -> Result<SessionRevertResult, ClientError> {
1064        Client::revert_session(self, params).await
1065    }
1066    async fn fork_session(
1067        &self,
1068        params: SessionForkParams,
1069    ) -> Result<SessionForkResult, ClientError> {
1070        Client::fork_session(self, params).await
1071    }
1072    async fn start_run(&self, params: RunStartParams) -> Result<RunStartResult, ClientError> {
1073        Client::start_run(self, params).await
1074    }
1075    async fn steer_run(&self, params: RunSteerParams) -> Result<RunSteerResult, ClientError> {
1076        Client::steer_run(self, params).await
1077    }
1078    async fn recall_steer(
1079        &self,
1080        params: RunRecallSteerParams,
1081    ) -> Result<RunRecallSteerResult, ClientError> {
1082        Client::recall_steer(self, params).await
1083    }
1084    async fn cancel_run(&self, params: RunCancelParams) -> Result<RunCancelResult, ClientError> {
1085        Client::cancel_run(self, params).await
1086    }
1087    async fn tool_stdin(
1088        &self,
1089        params: RunToolStdinParams,
1090    ) -> Result<RunToolStdinResult, ClientError> {
1091        Client::tool_stdin(self, params).await
1092    }
1093    async fn respond_approval(
1094        &self,
1095        params: ApprovalRespondParams,
1096    ) -> Result<ApprovalRespondResult, ClientError> {
1097        Client::respond_approval(self, params).await
1098    }
1099    async fn list_approvals(
1100        &self,
1101        params: ApprovalListParams,
1102    ) -> Result<ApprovalListResult, ClientError> {
1103        Client::list_approvals(self, params).await
1104    }
1105    async fn begin_mcp_auth(
1106        &self,
1107        params: McpAuthBeginParams,
1108    ) -> Result<McpAuthBeginResult, ClientError> {
1109        Client::begin_mcp_auth(self, params).await
1110    }
1111    async fn cancel_mcp_auth(
1112        &self,
1113        params: McpAuthCancelParams,
1114    ) -> Result<McpAuthCancelResult, ClientError> {
1115        Client::cancel_mcp_auth(self, params).await
1116    }
1117    async fn list_mcp_servers(&self) -> Result<McpServerListResult, ClientError> {
1118        Client::list_mcp_servers(self).await
1119    }
1120    async fn add_mcp_server(
1121        &self,
1122        params: McpServerAddParams,
1123    ) -> Result<McpServerMutationResult, ClientError> {
1124        Client::add_mcp_server(self, params).await
1125    }
1126    async fn edit_mcp_server(
1127        &self,
1128        params: McpServerEditParams,
1129    ) -> Result<McpServerMutationResult, ClientError> {
1130        Client::edit_mcp_server(self, params).await
1131    }
1132    async fn remove_mcp_server(
1133        &self,
1134        params: McpServerNameParams,
1135    ) -> Result<McpServerMutationResult, ClientError> {
1136        Client::remove_mcp_server(self, params).await
1137    }
1138    async fn set_mcp_server_enabled(
1139        &self,
1140        params: McpServerSetEnabledParams,
1141    ) -> Result<McpServerMutationResult, ClientError> {
1142        Client::set_mcp_server_enabled(self, params).await
1143    }
1144    async fn reconnect_mcp_server(
1145        &self,
1146        params: McpServerNameParams,
1147    ) -> Result<McpServerMutationResult, ClientError> {
1148        Client::reconnect_mcp_server(self, params).await
1149    }
1150    async fn persist_mcp_server(
1151        &self,
1152        params: McpServerPersistParams,
1153    ) -> Result<McpServerMutationResult, ClientError> {
1154        Client::persist_mcp_server(self, params).await
1155    }
1156    async fn subscribe_events(
1157        &self,
1158        session_id: SessionId,
1159        cursor: Option<u64>,
1160    ) -> Result<(), ClientError> {
1161        Client::subscribe_events(self, session_id, cursor).await
1162    }
1163    async fn runtime_snapshot(&self) -> Result<RuntimeSnapshotResult, ClientError> {
1164        Client::runtime_snapshot(self).await
1165    }
1166    async fn connect_provider(
1167        &self,
1168        params: ProviderConnectParams,
1169    ) -> Result<ProviderConnectResult, ClientError> {
1170        Client::connect_provider(self, params).await
1171    }
1172    async fn disconnect_provider(
1173        &self,
1174        params: ProviderDisconnectParams,
1175    ) -> Result<ProviderDisconnectResult, ClientError> {
1176        Client::disconnect_provider(self, params).await
1177    }
1178
1179    fn recover_session(&self, session_id: SessionId, full_replay: bool) {
1180        Client::recover_session(self, session_id, full_replay);
1181    }
1182
1183    fn shutdown(&self) {
1184        Client::shutdown(self);
1185    }
1186}
1187
1188#[async_trait::async_trait]
1189impl<T> ClientProtocol for T
1190where
1191    T: std::ops::Deref<Target = Client> + Send + Sync,
1192{
1193    async fn handshake(&self) -> Result<ServerHello, ClientError> {
1194        self.deref().handshake().await
1195    }
1196    async fn create_session(
1197        &self,
1198        params: SessionCreateParams,
1199    ) -> Result<SessionCreateResult, ClientError> {
1200        self.deref().create_session(params).await
1201    }
1202    async fn list_sessions(
1203        &self,
1204        params: SessionListParams,
1205    ) -> Result<SessionListResult, ClientError> {
1206        self.deref().list_sessions(params).await
1207    }
1208    async fn get_session(&self, params: SessionGetParams) -> Result<SessionGetResult, ClientError> {
1209        self.deref().get_session(params).await
1210    }
1211    async fn get_session_goal(
1212        &self,
1213        params: SessionGoalGetParams,
1214    ) -> Result<SessionGoalGetResult, ClientError> {
1215        self.deref().get_session_goal(params).await
1216    }
1217    async fn set_session_goal(
1218        &self,
1219        params: SessionGoalSetParams,
1220    ) -> Result<SessionGoalSetResult, ClientError> {
1221        self.deref().set_session_goal(params).await
1222    }
1223    async fn change_session_goal_lifecycle(
1224        &self,
1225        params: SessionGoalLifecycleParams,
1226    ) -> Result<SessionGoalLifecycleResult, ClientError> {
1227        self.deref().change_session_goal_lifecycle(params).await
1228    }
1229    async fn session_producers(
1230        &self,
1231        params: SessionProducersParams,
1232    ) -> Result<SessionProducersResult, ClientError> {
1233        self.deref().session_producers(params).await
1234    }
1235    async fn session_usage(
1236        &self,
1237        params: SessionUsageParams,
1238    ) -> Result<SessionUsageResult, ClientError> {
1239        self.deref().session_usage(params).await
1240    }
1241    async fn session_tree_usage(
1242        &self,
1243        params: SessionUsageParams,
1244    ) -> Result<SessionTreeUsageResult, ClientError> {
1245        self.deref().session_tree_usage(params).await
1246    }
1247    async fn agent_usage(&self, params: AgentUsageParams) -> Result<AgentUsageResult, ClientError> {
1248        self.deref().agent_usage(params).await
1249    }
1250    async fn global_usage(&self) -> Result<GlobalUsageResult, ClientError> {
1251        self.deref().global_usage().await
1252    }
1253    async fn session_children(
1254        &self,
1255        params: SessionChildrenParams,
1256    ) -> Result<SessionChildrenResult, ClientError> {
1257        self.deref().session_children(params).await
1258    }
1259    async fn session_tree(
1260        &self,
1261        params: SessionTreeParams,
1262    ) -> Result<SessionTreeResult, ClientError> {
1263        self.deref().session_tree(params).await
1264    }
1265    async fn resume_session(
1266        &self,
1267        params: SessionResumeParams,
1268    ) -> Result<SessionResumeResult, ClientError> {
1269        self.deref().resume_session(params).await
1270    }
1271    async fn rename_session(
1272        &self,
1273        params: SessionRenameParams,
1274    ) -> Result<SessionRenameResult, ClientError> {
1275        self.deref().rename_session(params).await
1276    }
1277    async fn set_permission_mode(
1278        &self,
1279        params: SessionSetPermissionModeParams,
1280    ) -> Result<SessionSetPermissionModeResult, ClientError> {
1281        self.deref().set_permission_mode(params).await
1282    }
1283    async fn get_session_permissions(
1284        &self,
1285        params: SessionPermissionGetParams,
1286    ) -> Result<SessionPermissionGetResult, ClientError> {
1287        self.deref().get_session_permissions(params).await
1288    }
1289    async fn set_session_permission(
1290        &self,
1291        params: SessionPermissionSetParams,
1292    ) -> Result<SessionPermissionMutationResult, ClientError> {
1293        self.deref().set_session_permission(params).await
1294    }
1295    async fn clear_session_permission(
1296        &self,
1297        params: SessionPermissionClearParams,
1298    ) -> Result<SessionPermissionMutationResult, ClientError> {
1299        self.deref().clear_session_permission(params).await
1300    }
1301    async fn list_skills(&self, params: SkillsListParams) -> Result<SkillsListResult, ClientError> {
1302        self.deref().list_skills(params).await
1303    }
1304    async fn get_skill(&self, params: SkillsGetParams) -> Result<SkillsGetResult, ClientError> {
1305        self.deref().get_skill(params).await
1306    }
1307    async fn compact_session(
1308        &self,
1309        params: SessionCompactParams,
1310    ) -> Result<SessionCompactResult, ClientError> {
1311        self.deref().compact_session(params).await
1312    }
1313    async fn revert_session(
1314        &self,
1315        params: SessionRevertParams,
1316    ) -> Result<SessionRevertResult, ClientError> {
1317        self.deref().revert_session(params).await
1318    }
1319    async fn fork_session(
1320        &self,
1321        params: SessionForkParams,
1322    ) -> Result<SessionForkResult, ClientError> {
1323        self.deref().fork_session(params).await
1324    }
1325    async fn start_run(&self, params: RunStartParams) -> Result<RunStartResult, ClientError> {
1326        self.deref().start_run(params).await
1327    }
1328    async fn steer_run(&self, params: RunSteerParams) -> Result<RunSteerResult, ClientError> {
1329        self.deref().steer_run(params).await
1330    }
1331    async fn recall_steer(
1332        &self,
1333        params: RunRecallSteerParams,
1334    ) -> Result<RunRecallSteerResult, ClientError> {
1335        self.deref().recall_steer(params).await
1336    }
1337    async fn cancel_run(&self, params: RunCancelParams) -> Result<RunCancelResult, ClientError> {
1338        self.deref().cancel_run(params).await
1339    }
1340    async fn tool_stdin(
1341        &self,
1342        params: RunToolStdinParams,
1343    ) -> Result<RunToolStdinResult, ClientError> {
1344        self.deref().tool_stdin(params).await
1345    }
1346    async fn respond_approval(
1347        &self,
1348        params: ApprovalRespondParams,
1349    ) -> Result<ApprovalRespondResult, ClientError> {
1350        self.deref().respond_approval(params).await
1351    }
1352    async fn list_approvals(
1353        &self,
1354        params: ApprovalListParams,
1355    ) -> Result<ApprovalListResult, ClientError> {
1356        self.deref().list_approvals(params).await
1357    }
1358    async fn begin_mcp_auth(
1359        &self,
1360        params: McpAuthBeginParams,
1361    ) -> Result<McpAuthBeginResult, ClientError> {
1362        self.deref().begin_mcp_auth(params).await
1363    }
1364    async fn cancel_mcp_auth(
1365        &self,
1366        params: McpAuthCancelParams,
1367    ) -> Result<McpAuthCancelResult, ClientError> {
1368        self.deref().cancel_mcp_auth(params).await
1369    }
1370    async fn list_mcp_servers(&self) -> Result<McpServerListResult, ClientError> {
1371        self.deref().list_mcp_servers().await
1372    }
1373    async fn add_mcp_server(
1374        &self,
1375        params: McpServerAddParams,
1376    ) -> Result<McpServerMutationResult, ClientError> {
1377        self.deref().add_mcp_server(params).await
1378    }
1379    async fn edit_mcp_server(
1380        &self,
1381        params: McpServerEditParams,
1382    ) -> Result<McpServerMutationResult, ClientError> {
1383        self.deref().edit_mcp_server(params).await
1384    }
1385    async fn remove_mcp_server(
1386        &self,
1387        params: McpServerNameParams,
1388    ) -> Result<McpServerMutationResult, ClientError> {
1389        self.deref().remove_mcp_server(params).await
1390    }
1391    async fn set_mcp_server_enabled(
1392        &self,
1393        params: McpServerSetEnabledParams,
1394    ) -> Result<McpServerMutationResult, ClientError> {
1395        self.deref().set_mcp_server_enabled(params).await
1396    }
1397    async fn reconnect_mcp_server(
1398        &self,
1399        params: McpServerNameParams,
1400    ) -> Result<McpServerMutationResult, ClientError> {
1401        self.deref().reconnect_mcp_server(params).await
1402    }
1403    async fn persist_mcp_server(
1404        &self,
1405        params: McpServerPersistParams,
1406    ) -> Result<McpServerMutationResult, ClientError> {
1407        self.deref().persist_mcp_server(params).await
1408    }
1409    async fn subscribe_events(
1410        &self,
1411        session_id: SessionId,
1412        cursor: Option<u64>,
1413    ) -> Result<(), ClientError> {
1414        self.deref().subscribe_events(session_id, cursor).await
1415    }
1416    async fn runtime_snapshot(&self) -> Result<RuntimeSnapshotResult, ClientError> {
1417        self.deref().runtime_snapshot().await
1418    }
1419    async fn connect_provider(
1420        &self,
1421        params: ProviderConnectParams,
1422    ) -> Result<ProviderConnectResult, ClientError> {
1423        self.deref().connect_provider(params).await
1424    }
1425    async fn disconnect_provider(
1426        &self,
1427        params: ProviderDisconnectParams,
1428    ) -> Result<ProviderDisconnectResult, ClientError> {
1429        self.deref().disconnect_provider(params).await
1430    }
1431    fn recover_session(&self, session_id: SessionId, full_replay: bool) {
1432        self.deref().recover_session(session_id, full_replay);
1433    }
1434    fn shutdown(&self) {
1435        self.deref().shutdown();
1436    }
1437}
1438
1439async fn prepare_subscription(
1440    subscriptions: &Arc<Mutex<HashMap<SessionId, Subscription>>>,
1441    session_id: SessionId,
1442    cursor: u64,
1443    recovering: bool,
1444    rebuild: bool,
1445) -> Result<ReplayRequest, ClientError> {
1446    let mut subscriptions = subscriptions.lock().await;
1447    let subscription = subscriptions.entry(session_id).or_default();
1448    if subscription.fetching {
1449        if recovering {
1450            subscription.recovery_requested =
1451                Some(subscription.recovery_requested.unwrap_or(false) || rebuild);
1452        }
1453        return Err(ClientError::ReplayInProgress);
1454    }
1455    subscription.rollback_cursor = subscription.cursor;
1456    subscription.cursor = cursor;
1457    subscription.fetching = true;
1458    subscription.rebuild = rebuild;
1459    if rebuild && recovering {
1460        subscription.generation += 1;
1461    }
1462    subscription.next_attempt += 1;
1463    subscription.active_attempt = subscription.next_attempt;
1464    Ok(ReplayRequest {
1465        session_id,
1466        generation: subscription.generation,
1467        rebuild,
1468        attempt: subscription.active_attempt,
1469    })
1470}
1471
1472async fn request_replay(
1473    commands: &mpsc::Sender<Command>,
1474    replay: ReplayRequest,
1475    cursor: u64,
1476) -> Result<(), ClientError> {
1477    request_replay_with_timeout(commands, replay, cursor, REPLAY_RESPONSE_TIMEOUT).await
1478}
1479
1480async fn request_replay_with_timeout(
1481    commands: &mpsc::Sender<Command>,
1482    replay: ReplayRequest,
1483    cursor: u64,
1484    timeout: Duration,
1485) -> Result<(), ClientError> {
1486    let params = EventsSubscribeParams {
1487        session_id: replay.session_id,
1488        cursor: Some(cursor),
1489    };
1490    let _ = tokio::time::timeout(
1491        timeout,
1492        send_command(commands, "events.subscribe", &params, Some(replay), false),
1493    )
1494    .await
1495    .map_err(|_| ClientError::ReplayTimedOut)??;
1496    Ok(())
1497}
1498
1499async fn send_command<P>(
1500    commands: &mpsc::Sender<Command>,
1501    method: &str,
1502    params: &P,
1503    replay: Option<ReplayRequest>,
1504    sensitive: bool,
1505) -> Result<Value, ClientError>
1506where
1507    P: Serialize + ?Sized,
1508{
1509    let (response, receiver) = oneshot::channel();
1510    let id = NEXT_REQUEST_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1511    let params = SerializedParams {
1512        value: serde_json::to_value(params)?,
1513        sensitive,
1514    };
1515    commands
1516        .send(Command {
1517            id,
1518            method: method.to_owned(),
1519            params,
1520            replay,
1521            response,
1522        })
1523        .await
1524        .map_err(|_| ClientError::Closed)?;
1525    receiver.await.map_err(|_| ClientError::Closed)?
1526}
1527
1528pub(in crate::session::client) async fn send_sensitive_command(
1529    commands: &mpsc::Sender<Command>,
1530    method: &str,
1531    mut params: SensitiveJson,
1532) -> Result<Value, ClientError> {
1533    let (response, receiver) = oneshot::channel();
1534    let id = NEXT_REQUEST_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1535    let params = SerializedParams {
1536        value: params.take(),
1537        sensitive: true,
1538    };
1539    commands
1540        .send(Command {
1541            id,
1542            method: method.to_owned(),
1543            params,
1544            replay: None,
1545            response,
1546        })
1547        .await
1548        .map_err(|_| ClientError::Closed)?;
1549    receiver.await.map_err(|_| ClientError::Closed)?
1550}
1551
1552fn zeroize_json(value: &mut Value) {
1553    match value {
1554        Value::String(value) => value.zeroize(),
1555        Value::Array(values) => values.iter_mut().for_each(zeroize_json),
1556        Value::Object(values) => values.values_mut().for_each(zeroize_json),
1557        Value::Null | Value::Bool(_) | Value::Number(_) => {}
1558    }
1559}
1560
1561fn spawn_recovery_worker(
1562    commands: mpsc::Sender<Command>,
1563    subscriptions: Arc<Mutex<HashMap<SessionId, Subscription>>>,
1564    mut receiver: mpsc::UnboundedReceiver<(bool, Option<SessionId>)>,
1565    recovery: Arc<RecoveryQueue>,
1566    controls: mpsc::UnboundedSender<ConnectionControl>,
1567    replay_timeout: Duration,
1568) {
1569    let weak_recovery = Arc::downgrade(&recovery);
1570    tokio::spawn(async move {
1571        while let Some((mut full_replay, mut only_session)) = receiver.recv().await {
1572            loop {
1573                for attempt in 0..RECOVERY_ATTEMPTS {
1574                    if weak_recovery.upgrade().is_none() {
1575                        return;
1576                    }
1577                    match recover_all(
1578                        &commands,
1579                        &subscriptions,
1580                        full_replay,
1581                        only_session,
1582                        replay_timeout,
1583                    )
1584                    .await
1585                    {
1586                        Ok(()) => break,
1587                        Err(error) => {
1588                            if attempt + 1 == RECOVERY_ATTEMPTS {
1589                                let _ = controls.send(ConnectionControl::RecoveryFailed {
1590                                    session_id: only_session,
1591                                    error: error.to_string(),
1592                                });
1593                                break;
1594                            }
1595                            let delay = 50_u64.saturating_mul(1_u64 << attempt).min(5_000);
1596                            tokio::time::sleep(Duration::from_millis(delay)).await;
1597                        }
1598                    }
1599                }
1600                let next = {
1601                    let Some(recovery) = weak_recovery.upgrade() else {
1602                        return;
1603                    };
1604                    let mut state = recovery.state.lock().expect("recovery queue lock");
1605                    if state.follow_up {
1606                        state.follow_up = false;
1607                        Some((
1608                            std::mem::take(&mut state.follow_up_full),
1609                            state.follow_up_session.take(),
1610                        ))
1611                    } else {
1612                        state.queued = false;
1613                        None
1614                    }
1615                };
1616                let Some(next) = next else { break };
1617                (full_replay, only_session) = next;
1618            }
1619        }
1620    });
1621}
1622
1623async fn recover_all(
1624    commands: &mpsc::Sender<Command>,
1625    subscriptions: &Arc<Mutex<HashMap<SessionId, Subscription>>>,
1626    full_replay: bool,
1627    only_session: Option<SessionId>,
1628    replay_timeout: Duration,
1629) -> Result<(), ClientError> {
1630    let targets = {
1631        let mut subscriptions = subscriptions.lock().await;
1632        subscriptions
1633            .iter_mut()
1634            .filter_map(|(session_id, subscription)| {
1635                if only_session.is_some_and(|wanted| wanted != *session_id) {
1636                    return None;
1637                }
1638                if subscription.fetching {
1639                    subscription.recovery_requested =
1640                        Some(subscription.recovery_requested.unwrap_or(false) || full_replay);
1641                    return None;
1642                }
1643                subscription.rollback_cursor = subscription.cursor;
1644                let rebuild = full_replay;
1645                let cursor = if rebuild { 0 } else { subscription.cursor };
1646                subscription.cursor = cursor;
1647                subscription.fetching = true;
1648                subscription.rebuild = rebuild;
1649                if rebuild {
1650                    subscription.generation += 1;
1651                }
1652                subscription.next_attempt += 1;
1653                subscription.active_attempt = subscription.next_attempt;
1654                Some((
1655                    ReplayRequest {
1656                        session_id: *session_id,
1657                        generation: subscription.generation,
1658                        rebuild,
1659                        attempt: subscription.active_attempt,
1660                    },
1661                    cursor,
1662                ))
1663            })
1664            .collect::<Vec<_>>()
1665    };
1666    for (request, cursor) in targets {
1667        let session_id = request.session_id;
1668        if let Err(error) =
1669            request_replay_with_timeout(commands, request, cursor, replay_timeout).await
1670        {
1671            let mut subscriptions = subscriptions.lock().await;
1672            if let Some(subscription) = subscriptions.get_mut(&session_id) {
1673                subscription.cursor = subscription.rollback_cursor;
1674                subscription.fetching = false;
1675                subscription.awaiting_snapshots.clear();
1676                subscription.replay_tools.clear();
1677            }
1678            return Err(error);
1679        }
1680    }
1681    Ok(())
1682}
1683
1684static NEXT_REQUEST_ID: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(1);
1685
1686struct ConnectionTask {
1687    commands: mpsc::Receiver<Command>,
1688    controls: mpsc::UnboundedReceiver<ConnectionControl>,
1689    deliveries: Arc<dyn ClientEventSink>,
1690    subscriptions: Arc<Mutex<HashMap<SessionId, Subscription>>>,
1691    recovery: Arc<RecoveryQueue>,
1692    pending_command_count: Arc<AtomicUsize>,
1693    shutdown: tokio_util::sync::CancellationToken,
1694}
1695
1696async fn connection_task<S>(mut stream: S, mut task: ConnectionTask)
1697where
1698    S: Transport,
1699{
1700    let mut pending = HashMap::new();
1701    let mut tool_sessions = HashMap::new();
1702    let mut failure = None;
1703    let mut replay_timeout = tokio::time::interval(Duration::from_millis(25));
1704    loop {
1705        tokio::select! {
1706            () = task.shutdown.cancelled() => break,
1707            Some(command) = task.commands.recv() => {
1708                prune_cancelled_commands(&mut pending);
1709                if !command.response.is_closed() {
1710                    let request = BorrowedRequest {
1711                        jsonrpc: "2.0",
1712                        id: command.id,
1713                        method: &command.method,
1714                        params: &command.params.value,
1715                    };
1716                    match serialize_outbound_frame(request, command.params.sensitive) {
1717                        Ok(frame) => match send_outbound_frame(&mut stream, frame).await {
1718                            Ok(()) => { pending.insert(command.id, PendingCommand { replay: command.replay, response: command.response }); }
1719                            Err(error) => {
1720                                let message = format!("sending {}: {}", command.method, crate::diagnostics::error_chain(&error));
1721                                let _ = command.response.send(Err(ClientError::Transport(message.clone())));
1722                                failure = Some(message);
1723                                break;
1724                            }
1725                        },
1726                        Err(error) => { let _ = command.response.send(Err(ClientError::InvalidFrame(error))); }
1727                    }
1728                }
1729            }
1730            Some(control) = task.controls.recv() => match control {
1731                ConnectionControl::RecoveryFailed { session_id, error } => {
1732                    task.deliveries.deliver(ClientDelivery::RecoveryFailed { session_id, error });
1733                }
1734            },
1735            _ = replay_timeout.tick() => {
1736                prune_cancelled_commands(&mut pending);
1737                release_expired_replays(&task.subscriptions, task.deliveries.as_ref(), &task.recovery, &mut tool_sessions).await;
1738            }
1739            incoming = stream.recv() => {
1740                let frame = match incoming {
1741                    Ok(Some(frame)) => frame,
1742                    Ok(None) => break,
1743                    Err(error) => {
1744                        failure = Some(format!("receiving response: {}", crate::diagnostics::error_chain(&error)));
1745                        break;
1746                    }
1747                };
1748                prune_cancelled_commands(&mut pending);
1749                if let Err(error) = handle_frame(
1750                    frame,
1751                    &mut pending,
1752                    task.deliveries.as_ref(),
1753                    &task.subscriptions,
1754                    &task.recovery,
1755                    &mut tool_sessions,
1756                ).await {
1757                    resolve_malformed_response(error, &mut pending);
1758                }
1759            }
1760            else => break,
1761        }
1762        task.pending_command_count
1763            .store(pending.len(), Ordering::Relaxed);
1764    }
1765    for (_, pending) in pending {
1766        let error = failure.as_ref().map_or(ClientError::Closed, |message| {
1767            ClientError::Transport(message.clone())
1768        });
1769        let _ = pending.response.send(Err(error));
1770    }
1771    if let Some(error) = failure {
1772        task.deliveries
1773            .deliver(ClientDelivery::Disconnected { error });
1774    }
1775    task.pending_command_count.store(0, Ordering::Relaxed);
1776}
1777
1778fn serialize_outbound_frame(
1779    request: BorrowedRequest<'_>,
1780    sensitive: bool,
1781) -> Result<OutboundFrame, serde_json::Error> {
1782    if sensitive {
1783        serde_json::to_string(&request)
1784            .map(SensitiveFrame::new)
1785            .map(OutboundFrame::Sensitive)
1786    } else {
1787        serde_json::to_value(request)
1788            .map(MessageFrame::Value)
1789            .map(OutboundFrame::Public)
1790    }
1791}
1792
1793async fn send_outbound_frame<S>(stream: &mut S, frame: OutboundFrame) -> Result<(), TransportError>
1794where
1795    S: Transport,
1796{
1797    match frame {
1798        OutboundFrame::Public(frame) => stream.send(frame).await,
1799        OutboundFrame::Sensitive(mut frame) => {
1800            // Move the sole serialized allocation into the transport. Dropping
1801            // before this point wipes it; after this point the transport owns
1802            // any frame/socket copies and defines their lifetime.
1803            let text = std::mem::take(&mut *frame.text);
1804            stream.send(MessageFrame::Text(text)).await
1805        }
1806    }
1807}
1808
1809#[cfg(test)]
1810static PROVIDER_CONNECT_WIPE_COUNT: AtomicUsize = AtomicUsize::new(0);
1811
1812#[cfg(test)]
1813static SENSITIVE_SERIALIZED_WIPE_COUNT: AtomicUsize = AtomicUsize::new(0);
1814
1815#[cfg(test)]
1816static SENSITIVE_FRAME_WIPE_COUNT: AtomicUsize = AtomicUsize::new(0);
1817
1818#[cfg(test)]
1819fn record_provider_connect_wipe() {
1820    PROVIDER_CONNECT_WIPE_COUNT.fetch_add(1, Ordering::Relaxed);
1821}
1822
1823#[cfg(not(test))]
1824fn record_provider_connect_wipe() {}
1825
1826#[cfg(test)]
1827fn record_sensitive_serialized_wipe() {
1828    SENSITIVE_SERIALIZED_WIPE_COUNT.fetch_add(1, Ordering::Relaxed);
1829}
1830
1831#[cfg(not(test))]
1832fn record_sensitive_serialized_wipe() {}
1833
1834#[cfg(test)]
1835fn record_sensitive_frame_wipe() {
1836    SENSITIVE_FRAME_WIPE_COUNT.fetch_add(1, Ordering::Relaxed);
1837}
1838
1839#[cfg(not(test))]
1840fn record_sensitive_frame_wipe() {}
1841
1842fn prune_cancelled_commands(pending: &mut HashMap<i64, PendingCommand>) {
1843    pending.retain(|_, command| !command.response.is_closed());
1844}
1845
1846fn resolve_malformed_response(error: ClientError, pending: &mut HashMap<i64, PendingCommand>) {
1847    let detail = error.to_string();
1848    let mut pending = std::mem::take(pending).into_iter();
1849    if let Some((_, command)) = pending.next() {
1850        let _ = command.response.send(Err(error));
1851    }
1852    for (_, command) in pending {
1853        let error = serde_json::Error::io(std::io::Error::other(detail.clone()));
1854        let _ = command.response.send(Err(ClientError::InvalidFrame(error)));
1855    }
1856}
1857
1858async fn handle_frame(
1859    frame: MessageFrame,
1860    pending: &mut HashMap<i64, PendingCommand>,
1861    deliveries: &dyn ClientEventSink,
1862    subscriptions: &Arc<Mutex<HashMap<SessionId, Subscription>>>,
1863    recovery: &Arc<RecoveryQueue>,
1864    tool_sessions: &mut HashMap<ToolCallId, SessionId>,
1865) -> Result<(), ClientError> {
1866    let value = match frame {
1867        MessageFrame::Value(value) => value,
1868        MessageFrame::Text(text) => serde_json::from_str(&text)?,
1869    };
1870    if value.get("id").is_some() {
1871        let request_id = value
1872            .get("id")
1873            .cloned()
1874            .and_then(|id| serde_json::from_value::<JsonRpcId>(id).ok());
1875        let response: Response = match serde_json::from_value(value) {
1876            Ok(response) => response,
1877            Err(error) => {
1878                if let Some(JsonRpcId::Number(id)) = request_id
1879                    && let Some(command) = pending.remove(&id)
1880                {
1881                    let _ = command.response.send(Err(ClientError::InvalidFrame(error)));
1882                    return Ok(());
1883                }
1884                return Err(ClientError::InvalidFrame(error));
1885            }
1886        };
1887        let (id, result) = match response {
1888            Response::Success(response) => (response.id, Ok(response.result)),
1889            Response::Error(response) => (response.id, Err(ClientError::Rpc(response.error))),
1890        };
1891        if let JsonRpcId::Number(id) = id
1892            && let Some(command) = pending.remove(&id)
1893        {
1894            match (command.replay, result) {
1895                (Some(replay), Ok(value)) => {
1896                    match serde_json::from_value::<EventsSubscribeResult>(value) {
1897                        Ok(result) => {
1898                            // The application can now start draining the sole
1899                            // mpsc consumer while this task injects a large
1900                            // replay under natural channel backpressure.
1901                            let _ = command.response.send(Ok(Value::Null));
1902                            begin_replay(
1903                                replay,
1904                                result.events,
1905                                subscriptions,
1906                                deliveries,
1907                                recovery,
1908                                tool_sessions,
1909                            )
1910                            .await;
1911                        }
1912                        Err(error) => {
1913                            let _ = command.response.send(Err(ClientError::InvalidFrame(error)));
1914                        }
1915                    }
1916                }
1917                (_, result) => {
1918                    let _ = command.response.send(result);
1919                }
1920            }
1921        }
1922        return Ok(());
1923    }
1924    let notification: Notification = serde_json::from_value(value)?;
1925    let Some(params) = notification.params else {
1926        return Ok(());
1927    };
1928    match notification.method.as_str() {
1929        "events.subscription" => {
1930            let message: EventSubscriptionMessage = serde_json::from_value(params)?;
1931            route_live(message, deliveries, subscriptions, recovery, tool_sessions).await;
1932        }
1933        "events.tool_output_snapshot" => {
1934            let snapshot = serde_json::from_value(params)?;
1935            route_output(
1936                ClientDelivery::OutputSnapshot(snapshot),
1937                deliveries,
1938                subscriptions,
1939                recovery,
1940                tool_sessions,
1941            )
1942            .await;
1943        }
1944        "events.tool_output_delta" => {
1945            let delta = serde_json::from_value(params)?;
1946            route_output(
1947                ClientDelivery::OutputDelta(delta),
1948                deliveries,
1949                subscriptions,
1950                recovery,
1951                tool_sessions,
1952            )
1953            .await;
1954        }
1955        "events.tool_output_gap" => {
1956            let gap = serde_json::from_value(params)?;
1957            route_output(
1958                ClientDelivery::OutputGap(gap),
1959                deliveries,
1960                subscriptions,
1961                recovery,
1962                tool_sessions,
1963            )
1964            .await;
1965        }
1966        "events.plugin" => {
1967            let event = serde_json::from_value(params)?;
1968            deliveries.deliver(ClientDelivery::PluginEvent(event));
1969        }
1970        crate::RUNTIME_CHANGED_METHOD => {
1971            let changed = serde_json::from_value(params)?;
1972            deliveries.deliver(ClientDelivery::RuntimeChanged(Box::new(changed)));
1973        }
1974        _ => {}
1975    }
1976    Ok(())
1977}
1978
1979async fn begin_replay(
1980    replay: ReplayRequest,
1981    events: Vec<StoredEvent>,
1982    subscriptions: &Arc<Mutex<HashMap<SessionId, Subscription>>>,
1983    deliveries: &dyn ClientEventSink,
1984    recovery: &Arc<RecoveryQueue>,
1985    tool_sessions: &mut HashMap<ToolCallId, SessionId>,
1986) {
1987    let active_tools = active_tools(&events);
1988    let final_seq = {
1989        let mut subscriptions = subscriptions.lock().await;
1990        let Some(subscription) = subscriptions.get_mut(&replay.session_id) else {
1991            return;
1992        };
1993        if !subscription.fetching
1994            || subscription.generation != replay.generation
1995            || subscription.active_attempt != replay.attempt
1996        {
1997            return;
1998        }
1999        let final_seq = events
2000            .iter()
2001            .map(|event| event.seq)
2002            .max()
2003            .unwrap_or(subscription.cursor);
2004        subscription.final_seq = final_seq;
2005        subscription.rebuild = replay.rebuild;
2006        subscription.replay_tools = active_tools.iter().copied().collect();
2007        subscription.awaiting_snapshots = events
2008            .iter()
2009            .filter_map(|event| match &event.payload {
2010                EventPayload::ToolCallStarted { start }
2011                    if active_tools.contains(&start.tool_call_id) =>
2012                {
2013                    Some(start)
2014                }
2015                _ => None,
2016            })
2017            .flat_map(|start| {
2018                start
2019                    .output
2020                    .channels()
2021                    .into_iter()
2022                    .map(|name| (start.tool_call_id, name.unwrap_or_default()))
2023            })
2024            .collect();
2025        subscription.snapshot_deadline = (!subscription.awaiting_snapshots.is_empty())
2026            .then(|| Instant::now() + Duration::from_millis(250));
2027        final_seq
2028    };
2029    deliveries.deliver(ClientDelivery::ReplayStart {
2030        session_id: replay.session_id,
2031        generation: replay.generation,
2032        final_seq,
2033        rebuild: replay.rebuild,
2034    });
2035    for event in events {
2036        if let EventPayload::ToolCallStarted { start } = &event.payload {
2037            tool_sessions.insert(start.tool_call_id, event.session_id);
2038        }
2039        deliveries.deliver(ClientDelivery::ReplayEvent {
2040            session_id: replay.session_id,
2041            generation: replay.generation,
2042            final_seq,
2043            event: Box::new(event),
2044        });
2045    }
2046    finish_ready_replay(
2047        replay.session_id,
2048        subscriptions,
2049        deliveries,
2050        recovery,
2051        tool_sessions,
2052    )
2053    .await;
2054}
2055
2056fn active_tools(events: &[StoredEvent]) -> HashSet<ToolCallId> {
2057    let mut tools = HashSet::new();
2058    for event in events {
2059        match &event.payload {
2060            EventPayload::ToolCallStarted { start } => {
2061                tools.insert(start.tool_call_id);
2062            }
2063            EventPayload::ToolCallTerminated { termination } => {
2064                tools.remove(&termination.tool_call_id);
2065            }
2066            _ => {}
2067        }
2068    }
2069    tools
2070}
2071
2072async fn route_live(
2073    message: EventSubscriptionMessage,
2074    deliveries: &dyn ClientEventSink,
2075    subscriptions: &Arc<Mutex<HashMap<SessionId, Subscription>>>,
2076    recovery: &Arc<RecoveryQueue>,
2077    tool_sessions: &mut HashMap<ToolCallId, SessionId>,
2078) {
2079    let session_id = match &message {
2080        EventSubscriptionMessage::Event { event } => {
2081            if let EventPayload::ToolCallStarted { start } = &event.payload {
2082                tool_sessions.insert(start.tool_call_id, event.session_id);
2083            }
2084            event.session_id
2085        }
2086        EventSubscriptionMessage::Gap { session_id, .. } => *session_id,
2087    };
2088    let publish;
2089    let mut recover = None;
2090    {
2091        let mut subscriptions = subscriptions.lock().await;
2092        let subscription = subscriptions.entry(session_id).or_default();
2093        let generation = subscription.generation;
2094        if subscription.fetching {
2095            if matches!(message, EventSubscriptionMessage::Gap { .. }) {
2096                subscription.recovery_requested =
2097                    Some(subscription.recovery_requested.unwrap_or(false));
2098            }
2099            subscription.buffered.push(ClientDelivery::Live {
2100                message: Box::new(message),
2101                generation,
2102            });
2103            return;
2104        }
2105        match &message {
2106            EventSubscriptionMessage::Event { event } if event.seq > subscription.cursor + 1 => {
2107                recover = Some((false, Some(session_id)));
2108            }
2109            EventSubscriptionMessage::Event { event } if event.seq == subscription.cursor + 1 => {
2110                subscription.cursor = event.seq;
2111            }
2112            EventSubscriptionMessage::Gap {
2113                last_delivered_seq, ..
2114            } => {
2115                subscription.cursor = *last_delivered_seq;
2116                recover = Some((false, Some(session_id)));
2117            }
2118            _ => {}
2119        }
2120        publish = ClientDelivery::Live {
2121            message: Box::new(message),
2122            generation,
2123        };
2124    }
2125    deliveries.deliver(publish);
2126    if let Some((full, session)) = recover {
2127        Client::schedule_recovery_queue(recovery, full, session);
2128    }
2129}
2130
2131async fn route_output(
2132    delivery: ClientDelivery,
2133    deliveries: &dyn ClientEventSink,
2134    subscriptions: &Arc<Mutex<HashMap<SessionId, Subscription>>>,
2135    recovery: &Arc<RecoveryQueue>,
2136    tool_sessions: &mut HashMap<ToolCallId, SessionId>,
2137) {
2138    let retain_output = deliveries.wants_raw_tool_output();
2139    let (call_id, stream) = match &delivery {
2140        ClientDelivery::OutputSnapshot(snapshot) => {
2141            (snapshot.snapshot.call_id, stream_key(&snapshot.stream))
2142        }
2143        ClientDelivery::OutputDelta(delta) => (delta.call_id, stream_key(&delta.stream)),
2144        ClientDelivery::OutputGap(gap) => (gap.call_id, stream_key(&gap.stream)),
2145        _ => return,
2146    };
2147    let mut buffered = false;
2148    let mut completes_replay = None;
2149    if let Some(session_id) = tool_sessions.get(&call_id).copied() {
2150        let mut subscriptions = subscriptions.lock().await;
2151        if let Some(subscription) = subscriptions.get_mut(&session_id)
2152            && subscription.fetching
2153        {
2154            let replay_output = subscription.replay_tools.contains(&call_id);
2155            if matches!(&delivery, ClientDelivery::OutputSnapshot(_))
2156                && subscription.awaiting_snapshots.remove(&(call_id, stream))
2157            {
2158                completes_replay = Some(session_id);
2159            } else if !replay_output && retain_output {
2160                subscription.buffered.push(delivery.clone());
2161                buffered = true;
2162            }
2163        }
2164    }
2165    if !buffered && retain_output {
2166        deliveries.deliver(delivery);
2167    }
2168    if let Some(session_id) = completes_replay {
2169        finish_ready_replay(
2170            session_id,
2171            subscriptions,
2172            deliveries,
2173            recovery,
2174            tool_sessions,
2175        )
2176        .await;
2177    }
2178    let _ = recovery;
2179}
2180
2181async fn finish_ready_replay(
2182    session_id: SessionId,
2183    subscriptions: &Arc<Mutex<HashMap<SessionId, Subscription>>>,
2184    deliveries: &dyn ClientEventSink,
2185    recovery: &Arc<RecoveryQueue>,
2186    tool_sessions: &mut HashMap<ToolCallId, SessionId>,
2187) {
2188    let (generation, final_seq, buffered, recovery_requested, tail_has_gap) = {
2189        let mut subscriptions = subscriptions.lock().await;
2190        let Some(subscription) = subscriptions.get_mut(&session_id) else {
2191            return;
2192        };
2193        if !subscription.fetching || !subscription.awaiting_snapshots.is_empty() {
2194            return;
2195        }
2196        subscription.fetching = false;
2197        let (tail_cursor, tail_has_gap) = subscription.buffered.iter().fold(
2198            (subscription.final_seq, false),
2199            |(cursor, has_gap), delivery| match delivery {
2200                ClientDelivery::Live { message, .. } => match message.as_ref() {
2201                    EventSubscriptionMessage::Event { event } if event.seq <= cursor => {
2202                        (cursor, has_gap)
2203                    }
2204                    EventSubscriptionMessage::Event { event } if event.seq == cursor + 1 => {
2205                        (event.seq, has_gap)
2206                    }
2207                    EventSubscriptionMessage::Event { .. }
2208                    | EventSubscriptionMessage::Gap { .. } => (cursor, true),
2209                },
2210                _ => (cursor, has_gap),
2211            },
2212        );
2213        subscription.cursor = tail_cursor;
2214        subscription.replay_tools.clear();
2215        subscription.snapshot_deadline = None;
2216        (
2217            subscription.generation,
2218            subscription.final_seq,
2219            std::mem::take(&mut subscription.buffered),
2220            subscription.recovery_requested.take(),
2221            tail_has_gap,
2222        )
2223    };
2224    deliveries.deliver(ClientDelivery::ReplayEnd {
2225        session_id,
2226        generation,
2227        final_seq,
2228    });
2229    for delivery in buffered {
2230        if let ClientDelivery::Live { message, .. } = &delivery
2231            && let EventSubscriptionMessage::Event { event } = message.as_ref()
2232        {
2233            if event.seq <= final_seq {
2234                continue;
2235            }
2236            if let EventPayload::ToolCallStarted { start } = &event.payload {
2237                tool_sessions.insert(start.tool_call_id, event.session_id);
2238            }
2239        }
2240        deliveries.deliver(delivery);
2241    }
2242    if let Some(full) = recovery_requested.or(tail_has_gap.then_some(false)) {
2243        Client::schedule_recovery_queue(recovery, full, (!full).then_some(session_id));
2244    }
2245}
2246
2247async fn release_expired_replays(
2248    subscriptions: &Arc<Mutex<HashMap<SessionId, Subscription>>>,
2249    deliveries: &dyn ClientEventSink,
2250    recovery: &Arc<RecoveryQueue>,
2251    tool_sessions: &mut HashMap<ToolCallId, SessionId>,
2252) {
2253    let sessions = {
2254        let subscriptions = subscriptions.lock().await;
2255        subscriptions
2256            .iter()
2257            .filter_map(|(session_id, subscription)| {
2258                (subscription.fetching
2259                    && subscription
2260                        .snapshot_deadline
2261                        .is_some_and(|deadline| deadline <= Instant::now()))
2262                .then_some(*session_id)
2263            })
2264            .collect::<Vec<_>>()
2265    };
2266    // Server output-tail setup is bounded. Do not hold an event replay open
2267    // indefinitely if a call completed between its persisted start and output
2268    // hub registration; live output remains ordered through the same stream.
2269    for session_id in sessions {
2270        if let Some(subscription) = subscriptions.lock().await.get_mut(&session_id) {
2271            subscription.awaiting_snapshots.clear();
2272        }
2273        finish_ready_replay(
2274            session_id,
2275            subscriptions,
2276            deliveries,
2277            recovery,
2278            tool_sessions,
2279        )
2280        .await;
2281    }
2282    let _ = recovery;
2283}
2284
2285fn stream_key(stream: &OutputStream) -> String {
2286    stream.name().to_owned()
2287}
2288
2289#[cfg(test)]
2290mod tests;