From f6a9043cd116f8e470d20d8543e769591b1375c7 Mon Sep 17 00:00:00 2001 From: jh-block Date: Thu, 12 Mar 2026 14:08:04 +0100 Subject: [PATCH 01/30] feat: separate SSE event streaming from POST work submission Implement long-lived per-session SSE connections with safe reconnection via Last-Event-ID. Split the old POST /reply endpoint into: - GET /sessions/{id}/events: Long-lived SSE stream for events - POST /sessions/{id}/reply: Submit work with client-generated UUIDv7 request_id - POST /sessions/{id}/cancel: Cancel a specific request This fixes the root cause of duplicate agent tasks: reconnecting SSE clients no longer retry the POST that created the work. Frontend now generates unique request_ids and registers SSE listeners before posting, eliminating race conditions. Auto-reconnect with exponential backoff replaced manual retry logic. Fixes: Prevents SSE connection drops from creating duplicate agent work --- crates/goose-server/src/lib.rs | 1 + crates/goose-server/src/main.rs | 1 + crates/goose-server/src/openapi.rs | 6 + crates/goose-server/src/routes/mod.rs | 2 + crates/goose-server/src/routes/reply.rs | 6 +- .../goose-server/src/routes/session_events.rs | 559 ++++++++++++++++++ crates/goose-server/src/session_event_bus.rs | 187 ++++++ crates/goose-server/src/state.rs | 11 + ui/desktop/openapi.json | 161 +++++ ui/desktop/src/api/sessionSseClient.ts | 234 ++++++++ ui/desktop/src/api/types.gen.ts | 93 +++ ui/desktop/src/hooks/useChatStream.ts | 364 ++++++------ ui/desktop/src/hooks/useSessionEvents.ts | 65 ++ 13 files changed, 1494 insertions(+), 196 deletions(-) create mode 100644 crates/goose-server/src/routes/session_events.rs create mode 100644 crates/goose-server/src/session_event_bus.rs create mode 100644 ui/desktop/src/api/sessionSseClient.ts create mode 100644 ui/desktop/src/hooks/useSessionEvents.ts diff --git a/crates/goose-server/src/lib.rs b/crates/goose-server/src/lib.rs index 4403ef7f858f..ab35c400c284 100644 --- a/crates/goose-server/src/lib.rs +++ b/crates/goose-server/src/lib.rs @@ -3,6 +3,7 @@ pub mod configuration; pub mod error; pub mod openapi; pub mod routes; +pub mod session_event_bus; pub mod state; pub mod tls; pub mod tunnel; diff --git a/crates/goose-server/src/main.rs b/crates/goose-server/src/main.rs index 25bf34246d70..66a203694b7f 100644 --- a/crates/goose-server/src/main.rs +++ b/crates/goose-server/src/main.rs @@ -4,6 +4,7 @@ mod error; mod logging; mod openapi; mod routes; +mod session_event_bus; mod state; mod tunnel; diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index 2f021f94fc7f..5cf51afbf345 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -435,6 +435,9 @@ derive_utoipa!(Icon as IconSchema); super::routes::agent::update_session, super::routes::action_required::confirm_tool_action, super::routes::reply::reply, + super::routes::session_events::session_events, + super::routes::session_events::session_reply, + super::routes::session_events::session_cancel, super::routes::session::list_sessions, super::routes::session::search_sessions, super::routes::session::get_session, @@ -523,6 +526,9 @@ derive_utoipa!(Icon as IconSchema); goose::prompt_template::Template, super::routes::action_required::ConfirmToolActionRequest, super::routes::reply::ChatRequest, + super::routes::session_events::SessionReplyRequest, + super::routes::session_events::SessionReplyResponse, + super::routes::session_events::CancelRequest, super::routes::session::ImportSessionRequest, super::routes::session::SessionListResponse, super::routes::session::UpdateSessionNameRequest, diff --git a/crates/goose-server/src/routes/mod.rs b/crates/goose-server/src/routes/mod.rs index 22d4e3e6af31..ea9cbfa2d737 100644 --- a/crates/goose-server/src/routes/mod.rs +++ b/crates/goose-server/src/routes/mod.rs @@ -12,6 +12,7 @@ pub mod recipe; pub mod recipe_utils; pub mod reply; pub mod sampling; +pub mod session_events; pub mod schedule; pub mod session; pub mod setup; @@ -44,5 +45,6 @@ pub fn configure(state: Arc, secret_key: String) -> Rout .merge(gateway::routes(state.clone())) .merge(mcp_ui_proxy::routes(secret_key.clone())) .merge(mcp_app_proxy::routes(secret_key)) + .merge(session_events::routes(state.clone())) .merge(sampling::routes(state)) } diff --git a/crates/goose-server/src/routes/reply.rs b/crates/goose-server/src/routes/reply.rs index 70eafd3353e0..f6b9fdcc57ff 100644 --- a/crates/goose-server/src/routes/reply.rs +++ b/crates/goose-server/src/routes/reply.rs @@ -29,7 +29,7 @@ use tokio::time::timeout; use tokio_stream::wrappers::ReceiverStream; use tokio_util::sync::CancellationToken; -fn track_tool_telemetry(content: &MessageContent, all_messages: &[Message]) { +pub fn track_tool_telemetry(content: &MessageContent, all_messages: &[Message]) { match content { MessageContent::ToolRequest(tool_request) => { if let Ok(tool_call) = &tool_request.tool_call { @@ -123,7 +123,7 @@ impl IntoResponse for SseResponse { } } -#[derive(Debug, Serialize, utoipa::ToSchema)] +#[derive(Debug, Clone, Serialize, utoipa::ToSchema)] #[serde(tag = "type")] pub enum MessageEvent { Message { @@ -152,7 +152,7 @@ pub enum MessageEvent { Ping, } -async fn get_token_state(session_manager: &SessionManager, session_id: &str) -> TokenState { +pub async fn get_token_state(session_manager: &SessionManager, session_id: &str) -> TokenState { session_manager .get_session(session_id, false) .await diff --git a/crates/goose-server/src/routes/session_events.rs b/crates/goose-server/src/routes/session_events.rs new file mode 100644 index 000000000000..2510c340bfdb --- /dev/null +++ b/crates/goose-server/src/routes/session_events.rs @@ -0,0 +1,559 @@ +use crate::routes::errors::ErrorResponse; +use crate::routes::reply::{get_token_state, track_tool_telemetry, MessageEvent}; +use crate::state::AppState; +use axum::{ + extract::{DefaultBodyLimit, Path, State}, + http::{self, HeaderMap}, + response::IntoResponse, + routing::{get, post}, + Json, Router, +}; +use bytes::Bytes; +use futures::{stream::StreamExt, Stream}; +use goose::agents::{AgentEvent, SessionConfig}; +use goose::conversation::message::Message; +use goose::conversation::Conversation; +use serde::{Deserialize, Serialize}; +use std::{ + convert::Infallible, + pin::Pin, + sync::Arc, + task::{Context, Poll}, + time::Duration, +}; +use tokio::sync::mpsc; +use tokio::time::timeout; +use tokio_stream::wrappers::ReceiverStream; + +// ── Request / Response types ──────────────────────────────────────────── + +#[derive(Debug, Deserialize, Serialize, utoipa::ToSchema)] +pub struct SessionReplyRequest { + /// Client-generated UUIDv7 identifying this request. + pub request_id: String, + pub user_message: Message, + #[serde(default)] + pub override_conversation: Option>, + pub recipe_name: Option, + pub recipe_version: Option, +} + +#[derive(Debug, Serialize, utoipa::ToSchema)] +pub struct SessionReplyResponse { + pub request_id: String, +} + +#[derive(Debug, Deserialize, Serialize, utoipa::ToSchema)] +pub struct CancelRequest { + pub request_id: String, +} + +// ── SSE Event Stream Response ─────────────────────────────────────────── + +/// An SSE response that includes `id:` lines for Last-Event-ID reconnection. +pub struct SseEventStream { + rx: ReceiverStream, +} + +impl SseEventStream { + fn new(rx: ReceiverStream) -> Self { + Self { rx } + } +} + +impl Stream for SseEventStream { + type Item = Result; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.rx) + .poll_next(cx) + .map(|opt| opt.map(|s| Ok(Bytes::from(s)))) + } +} + +impl IntoResponse for SseEventStream { + fn into_response(self) -> axum::response::Response { + let body = axum::body::Body::from_stream(self); + http::Response::builder() + .header("Content-Type", "text/event-stream") + .header("Cache-Control", "no-cache") + .header("Connection", "keep-alive") + .body(body) + .unwrap() + } +} + +// ── Helpers ───────────────────────────────────────────────────────────── + +fn format_sse_event(seq: u64, json: &str) -> String { + format!("id: {}\ndata: {}\n\n", seq, json) +} + +fn serialize_session_event( + seq: u64, + request_id: Option<&str>, + event: &MessageEvent, +) -> String { + // Build JSON payload: { request_id?: string, ...event_fields } + // We flatten request_id into the event JSON. + let mut event_json = serde_json::to_value(event).unwrap_or_else(|e| { + serde_json::json!({"type": "Error", "error": format!("Serialization error: {}", e)}) + }); + + if let Some(rid) = request_id { + if let serde_json::Value::Object(ref mut map) = event_json { + map.insert( + "request_id".to_string(), + serde_json::Value::String(rid.to_string()), + ); + } + } + + let json_str = serde_json::to_string(&event_json).unwrap_or_default(); + format_sse_event(seq, &json_str) +} + +// ── GET /sessions/{id}/events ─────────────────────────────────────────── + +#[utoipa::path( + get, + path = "/sessions/{id}/events", + params( + ("id" = String, Path, description = "Session ID"), + ), + responses( + (status = 200, description = "SSE event stream", + content_type = "text/event-stream"), + ) +)] +pub async fn session_events( + State(state): State>, + Path(session_id): Path, + headers: HeaderMap, +) -> SseEventStream { + let last_event_id: Option = headers + .get("Last-Event-ID") + .and_then(|v| v.to_str().ok()) + .and_then(|s| s.parse().ok()); + + let bus = state.get_or_create_event_bus(&session_id).await; + let (replay, mut live_rx) = bus.subscribe(last_event_id).await; + + let (tx, rx) = mpsc::channel::(256); + let stream = ReceiverStream::new(rx); + + tokio::spawn(async move { + // Send replayed events + for event in &replay { + let frame = serialize_session_event( + event.seq, + event.request_id.as_deref(), + &event.event, + ); + if tx.send(frame).await.is_err() { + return; + } + } + + // Send live events + heartbeat pings + let mut heartbeat_interval = tokio::time::interval(Duration::from_millis(500)); + + loop { + tokio::select! { + _ = heartbeat_interval.tick() => { + // Publish a ping through the bus so it gets a sequence number + let seq = bus.publish(None, MessageEvent::Ping).await; + let frame = serialize_session_event(seq, None, &MessageEvent::Ping); + if tx.send(frame).await.is_err() { + return; + } + } + result = live_rx.recv() => { + match result { + Ok(event) => { + // Skip Ping events from the broadcast — we generate our own + if matches!(event.event, MessageEvent::Ping) { + continue; + } + let frame = serialize_session_event( + event.seq, + event.request_id.as_deref(), + &event.event, + ); + if tx.send(frame).await.is_err() { + return; + } + } + Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => { + tracing::warn!("SSE subscriber lagged by {} events", n); + // Continue — client will reconnect if needed + } + Err(tokio::sync::broadcast::error::RecvError::Closed) => { + return; + } + } + } + } + } + }); + + SseEventStream::new(stream) +} + +// ── POST /sessions/{id}/reply ─────────────────────────────────────────── + +#[utoipa::path( + post, + path = "/sessions/{id}/reply", + params( + ("id" = String, Path, description = "Session ID"), + ), + request_body = SessionReplyRequest, + responses( + (status = 200, description = "Request accepted", + body = SessionReplyResponse), + (status = 400, description = "Invalid request"), + (status = 424, description = "Agent not initialized"), + (status = 500, description = "Internal server error"), + ) +)] +pub async fn session_reply( + State(state): State>, + Path(session_id): Path, + Json(request): Json, +) -> Result, ErrorResponse> { + let request_id = request.request_id.clone(); + + // Validate request_id is a valid UUID + if uuid::Uuid::parse_str(&request_id).is_err() { + return Err(ErrorResponse::bad_request( + "request_id must be a valid UUID", + )); + } + + let session_start = std::time::Instant::now(); + + tracing::info!( + monotonic_counter.goose.session_starts = 1, + session_type = "app", + interface = "ui", + "Session started" + ); + + if let Some(recipe_name) = request.recipe_name.clone() { + if state.mark_recipe_run_if_absent(&session_id).await { + let recipe_version = request + .recipe_version + .clone() + .unwrap_or_else(|| "unknown".to_string()); + + tracing::info!( + monotonic_counter.goose.recipe_runs = 1, + recipe_name = %recipe_name, + recipe_version = %recipe_version, + session_type = "app", + interface = "ui", + "Recipe execution started" + ); + } + } + + let bus = state.get_or_create_event_bus(&session_id).await; + let cancel_token = bus.register_request(request_id.clone()).await; + + let user_message = request.user_message; + let override_conversation = request.override_conversation; + + let task_state = state.clone(); + let task_session_id = session_id.clone(); + let task_request_id = request_id.clone(); + let task_cancel = cancel_token.clone(); + let task_bus = bus.clone(); + + drop(tokio::spawn(async move { + let publish = |rid: Option, event: MessageEvent| { + let bus = task_bus.clone(); + async move { + bus.publish(rid, event).await; + } + }; + + let agent = match task_state.get_agent(task_session_id.clone()).await { + Ok(agent) => agent, + Err(e) => { + tracing::error!("Failed to get session agent: {}", e); + publish( + Some(task_request_id.clone()), + MessageEvent::Error { + error: format!("Failed to get session agent: {}", e), + }, + ) + .await; + task_bus.cleanup_request(&task_request_id).await; + return; + } + }; + + let session = match task_state + .session_manager() + .get_session(&task_session_id, true) + .await + { + Ok(metadata) => metadata, + Err(e) => { + tracing::error!( + "Failed to read session for {}: {}", + task_session_id, + e + ); + publish( + Some(task_request_id.clone()), + MessageEvent::Error { + error: format!("Failed to read session: {}", e), + }, + ) + .await; + task_bus.cleanup_request(&task_request_id).await; + return; + } + }; + + let session_config = SessionConfig { + id: task_session_id.clone(), + schedule_id: session.schedule_id.clone(), + max_turns: None, + retry_config: None, + }; + + let mut all_messages = match override_conversation { + Some(history) => { + let conv = Conversation::new_unvalidated(history); + if let Err(e) = task_state + .session_manager() + .replace_conversation(&task_session_id, &conv) + .await + { + tracing::warn!( + "Failed to replace session conversation for {}: {}", + task_session_id, + e + ); + } + conv + } + None => session.conversation.unwrap_or_default(), + }; + all_messages.push(user_message.clone()); + + let mut stream = match agent + .reply( + user_message.clone(), + session_config, + Some(task_cancel.clone()), + ) + .await + { + Ok(stream) => stream, + Err(e) => { + tracing::error!("Failed to start reply stream: {:?}", e); + publish( + Some(task_request_id.clone()), + MessageEvent::Error { + error: e.to_string(), + }, + ) + .await; + task_bus.cleanup_request(&task_request_id).await; + return; + } + }; + + let mut heartbeat_interval = tokio::time::interval(Duration::from_millis(500)); + loop { + tokio::select! { + _ = task_cancel.cancelled() => { + tracing::info!("Agent task cancelled for request {}", task_request_id); + break; + } + _ = heartbeat_interval.tick() => { + // Heartbeat is handled by the SSE event stream endpoint + } + response = timeout(Duration::from_millis(500), stream.next()) => { + match response { + Ok(Some(Ok(AgentEvent::Message(message)))) => { + for content in &message.content { + track_tool_telemetry(content, all_messages.messages()); + } + all_messages.push(message.clone()); + let token_state = get_token_state( + task_state.session_manager(), + &task_session_id, + ) + .await; + publish( + Some(task_request_id.clone()), + MessageEvent::Message { + message, + token_state, + }, + ) + .await; + } + Ok(Some(Ok(AgentEvent::HistoryReplaced(new_messages)))) => { + all_messages = new_messages.clone(); + publish( + Some(task_request_id.clone()), + MessageEvent::UpdateConversation { + conversation: new_messages, + }, + ) + .await; + } + Ok(Some(Ok(AgentEvent::ModelChange { model, mode }))) => { + publish( + Some(task_request_id.clone()), + MessageEvent::ModelChange { model, mode }, + ) + .await; + } + Ok(Some(Ok(AgentEvent::McpNotification((notification_request_id, n))))) => { + publish( + Some(task_request_id.clone()), + MessageEvent::Notification { + request_id: notification_request_id, + message: n, + }, + ) + .await; + } + Ok(Some(Err(e))) => { + tracing::error!("Error processing message: {}", e); + publish( + Some(task_request_id.clone()), + MessageEvent::Error { + error: e.to_string(), + }, + ) + .await; + break; + } + Ok(None) => { + break; + } + Err(_) => { + // Timeout — check if the bus still has subscribers + continue; + } + } + } + } + } + + // Telemetry + let session_duration = session_start.elapsed(); + + if let Ok(session) = task_state + .session_manager() + .get_session(&task_session_id, true) + .await + { + let total_tokens = session.total_tokens.unwrap_or(0); + tracing::info!( + monotonic_counter.goose.session_completions = 1, + session_type = "app", + interface = "ui", + exit_type = "normal", + duration_ms = session_duration.as_millis() as u64, + total_tokens = total_tokens, + message_count = session.message_count, + "Session completed" + ); + + tracing::info!( + monotonic_counter.goose.session_duration_ms = session_duration.as_millis() as u64, + session_type = "app", + interface = "ui", + "Session duration" + ); + + if total_tokens > 0 { + tracing::info!( + monotonic_counter.goose.session_tokens = total_tokens, + session_type = "app", + interface = "ui", + "Session tokens" + ); + } + } else { + tracing::info!( + monotonic_counter.goose.session_completions = 1, + session_type = "app", + interface = "ui", + exit_type = "normal", + duration_ms = session_duration.as_millis() as u64, + total_tokens = 0u64, + message_count = all_messages.len(), + "Session completed" + ); + + tracing::info!( + monotonic_counter.goose.session_duration_ms = session_duration.as_millis() as u64, + session_type = "app", + interface = "ui", + "Session duration" + ); + } + + let final_token_state = + get_token_state(task_state.session_manager(), &task_session_id).await; + + publish( + Some(task_request_id.clone()), + MessageEvent::Finish { + reason: "stop".to_string(), + token_state: final_token_state, + }, + ) + .await; + + task_bus.cleanup_request(&task_request_id).await; + })); + + Ok(Json(SessionReplyResponse { request_id })) +} + +// ── POST /sessions/{id}/cancel ────────────────────────────────────────── + +#[utoipa::path( + post, + path = "/sessions/{id}/cancel", + params( + ("id" = String, Path, description = "Session ID"), + ), + request_body = CancelRequest, + responses( + (status = 200, description = "Cancellation accepted"), + ) +)] +pub async fn session_cancel( + State(state): State>, + Path(session_id): Path, + Json(request): Json, +) -> axum::http::StatusCode { + let bus = state.get_or_create_event_bus(&session_id).await; + bus.cancel_request(&request.request_id).await; + axum::http::StatusCode::OK +} + +// ── Route registration ────────────────────────────────────────────────── + +pub fn routes(state: Arc) -> Router { + Router::new() + .route("/sessions/{id}/events", get(session_events)) + .route( + "/sessions/{id}/reply", + post(session_reply).layer(DefaultBodyLimit::max(50 * 1024 * 1024)), + ) + .route("/sessions/{id}/cancel", post(session_cancel)) + .with_state(state) +} diff --git a/crates/goose-server/src/session_event_bus.rs b/crates/goose-server/src/session_event_bus.rs new file mode 100644 index 000000000000..c8d6f3996266 --- /dev/null +++ b/crates/goose-server/src/session_event_bus.rs @@ -0,0 +1,187 @@ +use crate::routes::reply::MessageEvent; +use std::collections::{HashMap, VecDeque}; +use std::sync::atomic::{AtomicU64, Ordering}; +use tokio::sync::{broadcast, Mutex}; +use tokio_util::sync::CancellationToken; + +const BROADCAST_CAPACITY: usize = 256; +const REPLAY_BUFFER_CAPACITY: usize = 512; + +#[derive(Clone, Debug)] +pub struct SessionEvent { + /// Monotonic sequence number, written as SSE `id:` frame (not in JSON payload). + pub seq: u64, + /// None for Ping events, Some for events associated with a specific request. + pub request_id: Option, + /// The event payload. + pub event: MessageEvent, +} + +pub struct SessionEventBus { + tx: broadcast::Sender, + buffer: Mutex>, + next_seq: AtomicU64, + active_requests: Mutex>, +} + +impl SessionEventBus { + pub fn new() -> Self { + let (tx, _) = broadcast::channel(BROADCAST_CAPACITY); + Self { + tx, + buffer: Mutex::new(VecDeque::with_capacity(REPLAY_BUFFER_CAPACITY)), + next_seq: AtomicU64::new(1), + active_requests: Mutex::new(HashMap::new()), + } + } + + /// Publish an event to the bus. Assigns a monotonic sequence number. + pub async fn publish(&self, request_id: Option, event: MessageEvent) -> u64 { + let seq = self.next_seq.fetch_add(1, Ordering::Relaxed); + let session_event = SessionEvent { + seq, + request_id, + event, + }; + + // Append to replay buffer + { + let mut buf = self.buffer.lock().await; + buf.push_back(session_event.clone()); + while buf.len() > REPLAY_BUFFER_CAPACITY { + buf.pop_front(); + } + } + + // Send on broadcast channel (ignore error if no subscribers) + let _ = self.tx.send(session_event); + + seq + } + + /// Subscribe to live events. If `last_event_id` is provided, replay buffered + /// events with seq > last_event_id. Returns (replay_events, live_receiver). + pub async fn subscribe( + &self, + last_event_id: Option, + ) -> (Vec, broadcast::Receiver) { + let rx = self.tx.subscribe(); + + let replay = if let Some(last_id) = last_event_id { + let buf = self.buffer.lock().await; + buf.iter() + .filter(|e| e.seq > last_id) + .cloned() + .collect() + } else { + Vec::new() + }; + + (replay, rx) + } + + /// Register a new request and return its cancellation token. + pub async fn register_request(&self, request_id: String) -> CancellationToken { + let token = CancellationToken::new(); + let mut requests = self.active_requests.lock().await; + requests.insert(request_id, token.clone()); + token + } + + /// Cancel a specific request by request_id. + pub async fn cancel_request(&self, request_id: &str) -> bool { + let requests = self.active_requests.lock().await; + if let Some(token) = requests.get(request_id) { + token.cancel(); + true + } else { + false + } + } + + /// Remove the cancellation token for a completed request. + pub async fn cleanup_request(&self, request_id: &str) { + let mut requests = self.active_requests.lock().await; + requests.remove(request_id); + } +} + +impl Default for SessionEventBus { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use goose::conversation::message::TokenState; + + #[tokio::test] + async fn test_publish_and_subscribe() { + let bus = SessionEventBus::new(); + + // Publish some events + bus.publish( + Some("req-1".to_string()), + MessageEvent::Ping, + ) + .await; + bus.publish( + Some("req-1".to_string()), + MessageEvent::Finish { + reason: "stop".to_string(), + token_state: TokenState::default(), + }, + ) + .await; + + // Subscribe with replay + let (replay, _rx) = bus.subscribe(Some(0)).await; + assert_eq!(replay.len(), 2); + assert_eq!(replay[0].seq, 1); + assert_eq!(replay[1].seq, 2); + } + + #[tokio::test] + async fn test_subscribe_with_last_event_id() { + let bus = SessionEventBus::new(); + + bus.publish(None, MessageEvent::Ping).await; + bus.publish(None, MessageEvent::Ping).await; + bus.publish(None, MessageEvent::Ping).await; + + // Only get events after seq 2 + let (replay, _rx) = bus.subscribe(Some(2)).await; + assert_eq!(replay.len(), 1); + assert_eq!(replay[0].seq, 3); + } + + #[tokio::test] + async fn test_cancel_request() { + let bus = SessionEventBus::new(); + + let token = bus.register_request("req-1".to_string()).await; + assert!(!token.is_cancelled()); + + let cancelled = bus.cancel_request("req-1").await; + assert!(cancelled); + assert!(token.is_cancelled()); + + // Non-existent request + let cancelled = bus.cancel_request("req-999").await; + assert!(!cancelled); + } + + #[tokio::test] + async fn test_cleanup_request() { + let bus = SessionEventBus::new(); + + bus.register_request("req-1".to_string()).await; + bus.cleanup_request("req-1").await; + + // Should return false since it was cleaned up + let cancelled = bus.cancel_request("req-1").await; + assert!(!cancelled); + } +} diff --git a/crates/goose-server/src/state.rs b/crates/goose-server/src/state.rs index da00db4a56eb..d39312e9b2d3 100644 --- a/crates/goose-server/src/state.rs +++ b/crates/goose-server/src/state.rs @@ -9,6 +9,7 @@ use std::sync::Arc; use tokio::sync::Mutex; use tokio::task::JoinHandle; +use crate::session_event_bus::SessionEventBus; use crate::tunnel::TunnelManager; use goose::agents::ExtensionLoadResult; use goose::gateway::manager::GatewayManager; @@ -26,6 +27,7 @@ pub struct AppState { pub gateway_manager: Arc, pub extension_loading_tasks: ExtensionLoadingTasks, pub inference_runtime: Arc, + session_buses: Arc>>>, } impl AppState { @@ -44,6 +46,7 @@ impl AppState { gateway_manager, extension_loading_tasks: Arc::new(Mutex::new(HashMap::new())), inference_runtime: InferenceRuntime::get_or_init(), + session_buses: Arc::new(Mutex::new(HashMap::new())), })) } @@ -107,6 +110,14 @@ impl AppState { } } + pub async fn get_or_create_event_bus(&self, session_id: &str) -> Arc { + let mut buses = self.session_buses.lock().await; + buses + .entry(session_id.to_string()) + .or_insert_with(|| Arc::new(SessionEventBus::new())) + .clone() + } + pub async fn get_agent(&self, session_id: String) -> anyhow::Result> { self.agent_manager.get_or_create_agent(session_id).await } diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 25e69576eeba..12708edc570e 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -3247,6 +3247,114 @@ ] } }, + "/sessions/{id}/cancel": { + "post": { + "tags": [ + "super::routes::session_events" + ], + "operationId": "session_cancel", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Session ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CancelRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Cancellation accepted" + } + } + } + }, + "/sessions/{id}/events": { + "get": { + "tags": [ + "super::routes::session_events" + ], + "operationId": "session_events", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Session ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "SSE event stream" + } + } + } + }, + "/sessions/{id}/reply": { + "post": { + "tags": [ + "super::routes::session_events" + ], + "operationId": "session_reply", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Session ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionReplyRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Request accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionReplyResponse" + } + } + } + }, + "400": { + "description": "Invalid request" + }, + "424": { + "description": "Agent not initialized" + }, + "500": { + "description": "Internal server error" + } + } + } + }, "/sessions/{session_id}": { "get": { "tags": [ @@ -3940,6 +4048,17 @@ } } }, + "CancelRequest": { + "type": "object", + "required": [ + "request_id" + ], + "properties": { + "request_id": { + "type": "string" + } + } + }, "ChatRequest": { "type": "object", "required": [ @@ -7736,6 +7855,48 @@ } } }, + "SessionReplyRequest": { + "type": "object", + "required": [ + "request_id", + "user_message" + ], + "properties": { + "override_conversation": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Message" + }, + "nullable": true + }, + "recipe_name": { + "type": "string", + "nullable": true + }, + "recipe_version": { + "type": "string", + "nullable": true + }, + "request_id": { + "type": "string", + "description": "Client-generated UUIDv7 identifying this request." + }, + "user_message": { + "$ref": "#/components/schemas/Message" + } + } + }, + "SessionReplyResponse": { + "type": "object", + "required": [ + "request_id" + ], + "properties": { + "request_id": { + "type": "string" + } + } + }, "SessionType": { "type": "string", "enum": [ diff --git a/ui/desktop/src/api/sessionSseClient.ts b/ui/desktop/src/api/sessionSseClient.ts new file mode 100644 index 000000000000..20e45af6e0ac --- /dev/null +++ b/ui/desktop/src/api/sessionSseClient.ts @@ -0,0 +1,234 @@ +/** + * Hand-written SSE client for long-lived session event streams. + * + * Unlike the generated SSE client (which is designed for request-response SSE), + * this client maintains a persistent GET connection per session and supports + * safe reconnection via Last-Event-ID. + */ + +export interface SessionEvent { + type: string; + request_id?: string; + [key: string]: unknown; +} + +type EventHandler = (event: SessionEvent) => void; + +const INITIAL_RECONNECT_DELAY = 1000; +const MAX_RECONNECT_DELAY = 30_000; + +export class SessionSseClient { + private reader: ReadableStreamDefaultReader | null = null; + private lastEventId: string | undefined; + private abortController: AbortController | null = null; + private listeners = new Map>(); + private globalListeners = new Set(); + private reconnectDelay = INITIAL_RECONNECT_DELAY; + private closed = false; + private _connected = false; + private onConnectedChange?: (connected: boolean) => void; + + constructor( + private baseUrl: string, + private sessionId: string, + private headers: Record = {} + ) {} + + get connected(): boolean { + return this._connected; + } + + private setConnected(value: boolean) { + if (this._connected !== value) { + this._connected = value; + this.onConnectedChange?.(value); + } + } + + /** + * Set a callback for connection state changes. + */ + onConnectionChange(callback: (connected: boolean) => void): void { + this.onConnectedChange = callback; + } + + /** + * Open the SSE connection. Auto-reconnects on failure. + */ + connect(): void { + this.closed = false; + this.doConnect(); + } + + /** + * Close the SSE connection permanently. + */ + close(): void { + this.closed = true; + this.setConnected(false); + this.abortController?.abort(); + this.abortController = null; + this.reader = null; + this.listeners.clear(); + this.globalListeners.clear(); + } + + /** + * Register a listener for events with a specific request_id. + * Returns an unsubscribe function. + */ + addListener(requestId: string, handler: EventHandler): () => void { + if (!this.listeners.has(requestId)) { + this.listeners.set(requestId, new Set()); + } + this.listeners.get(requestId)!.add(handler); + + return () => { + const set = this.listeners.get(requestId); + if (set) { + set.delete(handler); + if (set.size === 0) { + this.listeners.delete(requestId); + } + } + }; + } + + /** + * Register a listener for ALL events (regardless of request_id). + * Returns an unsubscribe function. + */ + addGlobalListener(handler: EventHandler): () => void { + this.globalListeners.add(handler); + return () => { + this.globalListeners.delete(handler); + }; + } + + private async doConnect(): Promise { + if (this.closed) return; + + this.abortController = new AbortController(); + + const url = `${this.baseUrl}/sessions/${this.sessionId}/events`; + const headers: Record = { ...this.headers }; + if (this.lastEventId) { + headers['Last-Event-ID'] = this.lastEventId; + } + + try { + const response = await fetch(url, { + method: 'GET', + headers, + signal: this.abortController.signal, + }); + + if (!response.ok) { + throw new Error(`SSE connection failed: ${response.status}`); + } + + if (!response.body) { + throw new Error('SSE response has no body'); + } + + this.setConnected(true); + this.reconnectDelay = INITIAL_RECONNECT_DELAY; + + await this.readStream(response.body); + } catch (error) { + if (this.closed) return; + + if (error instanceof DOMException && error.name === 'AbortError') { + return; + } + + this.setConnected(false); + console.warn('SSE connection error, reconnecting...', error); + + // Exponential backoff + await new Promise((resolve) => setTimeout(resolve, this.reconnectDelay)); + this.reconnectDelay = Math.min(this.reconnectDelay * 2, MAX_RECONNECT_DELAY); + + this.doConnect(); + } + } + + private async readStream(body: ReadableStream): Promise { + const decoder = new TextDecoder(); + this.reader = body.getReader(); + + let buffer = ''; + + try { + while (true) { + const { done, value } = await this.reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + + // Process complete SSE frames (double newline delimited) + const frames = buffer.split('\n\n'); + // Last element is incomplete — keep it in the buffer + buffer = frames.pop() || ''; + + for (const frame of frames) { + if (!frame.trim()) continue; + this.processFrame(frame); + } + } + } catch (error) { + if (this.closed) return; + throw error; + } finally { + this.reader = null; + } + + // Stream ended — reconnect if not closed + if (!this.closed) { + this.setConnected(false); + await new Promise((resolve) => setTimeout(resolve, this.reconnectDelay)); + this.doConnect(); + } + } + + private processFrame(frame: string): void { + let eventId: string | undefined; + let data: string | undefined; + + for (const line of frame.split('\n')) { + if (line.startsWith('id:')) { + eventId = line.slice(3).trim(); + } else if (line.startsWith('data:')) { + data = line.slice(5).trim(); + } + } + + // Track the last event ID for reconnection + if (eventId) { + this.lastEventId = eventId; + } + + if (!data) return; + + try { + const event: SessionEvent = JSON.parse(data); + + // Dispatch to global listeners + for (const handler of this.globalListeners) { + handler(event); + } + + // Dispatch to request-specific listeners + if (event.request_id) { + const handlers = this.listeners.get(event.request_id); + if (handlers) { + for (const handler of handlers) { + handler(event); + } + } + } + } catch (e) { + console.warn('Failed to parse SSE event data:', data, e); + } + } +} diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index 80c8702eb60d..be004dd47376 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -59,6 +59,10 @@ export type CallToolResponse = { structuredContent?: unknown; }; +export type CancelRequest = { + request_id: string; +}; + export type ChatRequest = { /** * Override the server's conversation history. Only use this when you need absolute control @@ -1261,6 +1265,21 @@ export type SessionListResponse = { sessions: Array; }; +export type SessionReplyRequest = { + override_conversation?: Array | null; + recipe_name?: string | null; + recipe_version?: string | null; + /** + * Client-generated UUIDv7 identifying this request. + */ + request_id: string; + user_message: Message; +}; + +export type SessionReplyResponse = { + request_id: string; +}; + export type SessionType = 'user' | 'scheduled' | 'sub_agent' | 'hidden' | 'terminal' | 'gateway'; export type SessionsQuery = { @@ -4107,6 +4126,80 @@ export type SearchSessionsResponses = { export type SearchSessionsResponse = SearchSessionsResponses[keyof SearchSessionsResponses]; +export type SessionCancelData = { + body: CancelRequest; + path: { + /** + * Session ID + */ + id: string; + }; + query?: never; + url: '/sessions/{id}/cancel'; +}; + +export type SessionCancelResponses = { + /** + * Cancellation accepted + */ + 200: unknown; +}; + +export type SessionEventsData = { + body?: never; + path: { + /** + * Session ID + */ + id: string; + }; + query?: never; + url: '/sessions/{id}/events'; +}; + +export type SessionEventsResponses = { + /** + * SSE event stream + */ + 200: unknown; +}; + +export type SessionReplyData = { + body: SessionReplyRequest; + path: { + /** + * Session ID + */ + id: string; + }; + query?: never; + url: '/sessions/{id}/reply'; +}; + +export type SessionReplyErrors = { + /** + * Invalid request + */ + 400: unknown; + /** + * Agent not initialized + */ + 424: unknown; + /** + * Internal server error + */ + 500: unknown; +}; + +export type SessionReplyResponses = { + /** + * Request accepted + */ + 200: SessionReplyResponse; +}; + +export type SessionReplyResponse2 = SessionReplyResponses[keyof SessionReplyResponses]; + export type DeleteSessionData = { body?: never; path: { diff --git a/ui/desktop/src/hooks/useChatStream.ts b/ui/desktop/src/hooks/useChatStream.ts index 4c270949a44e..2bd900cf0a2b 100644 --- a/ui/desktop/src/hooks/useChatStream.ts +++ b/ui/desktop/src/hooks/useChatStream.ts @@ -1,15 +1,15 @@ import { useCallback, useEffect, useMemo, useReducer, useRef } from 'react'; +import { v7 as uuidv7 } from 'uuid'; import { AppEvents } from '../constants/events'; import { ChatState } from '../types/chatState'; -import { toastError } from '../toasts'; import { getSession, Message, - MessageEvent, - reply, resumeAgent, Session, + sessionCancel, + sessionReply, TokenState, updateFromSession, updateSessionUserRecipeValues, @@ -27,6 +27,8 @@ import { import { errorMessage } from '../utils/conversionUtils'; import { showExtensionLoadResults } from '../utils/extensionErrorUtils'; import { maybeHandlePlatformEvent } from '../utils/platform_events'; +import { useSessionEvents } from './useSessionEvents'; +import type { SessionEvent } from '../api/sessionSseClient'; const resultsCache = new Map(); @@ -202,14 +204,16 @@ function prefersReducedMotion(): boolean { const REDUCED_MOTION_BATCH_INTERVAL = 1000; -async function streamFromResponse( - stream: AsyncIterable, +/** + * Creates an event processor that handles individual SSE events for a request. + * Returns an unsubscribe function and a handler to process events. + */ +function createEventProcessor( initialMessages: Message[], dispatch: React.Dispatch, onFinish: (error?: string) => void, sessionId: string, - signal?: AbortController['signal'] -): Promise { +) { let currentMessages = initialMessages; const reduceMotion = prefersReducedMotion(); let latestTokenState: TokenState | null = null; @@ -251,88 +255,71 @@ async function streamFromResponse( } }; - try { - for await (const event of stream) { - switch (event.type) { - case 'Message': { - const msg = event.message; - currentMessages = pushMessage(currentMessages, msg); - - const hasToolConfirmation = msg.content.some( - (content) => - content.type === 'actionRequired' && content.data.actionType === 'toolConfirmation' - ); - - const hasElicitation = msg.content.some( - (content) => - content.type === 'actionRequired' && content.data.actionType === 'elicitation' - ); - - if (hasToolConfirmation || hasElicitation) { - maybeUpdateUI(event.token_state, ChatState.WaitingForUserInput, true); - } else if (getCompactingMessage(msg)) { - maybeUpdateUI(event.token_state, ChatState.Compacting); - } else if (getThinkingMessage(msg)) { - maybeUpdateUI(event.token_state, ChatState.Thinking); - } else { - maybeUpdateUI(event.token_state, ChatState.Streaming); - } - break; - } - case 'Error': { - flushBatchedUpdates(); - onFinish('Stream error: ' + event.error); - return; - } - case 'Finish': { - flushBatchedUpdates(); - onFinish(); - return; - } - case 'ModelChange': { - break; - } - case 'UpdateConversation': { - currentMessages = event.conversation; - if (!reduceMotion) { - dispatch({ type: 'SET_MESSAGES', payload: event.conversation }); - } else { - hasPendingUpdate = true; - } - break; + // Returns true if the event is terminal (Finish or Error) + const processEvent = (event: SessionEvent): boolean => { + switch (event.type) { + case 'Message': { + const msg = (event as Record).message as Message; + const tokenState = (event as Record).token_state as TokenState; + currentMessages = pushMessage(currentMessages, msg); + + const hasToolConfirmation = msg.content.some( + (content) => + content.type === 'actionRequired' && content.data.actionType === 'toolConfirmation' + ); + + const hasElicitation = msg.content.some( + (content) => + content.type === 'actionRequired' && content.data.actionType === 'elicitation' + ); + + if (hasToolConfirmation || hasElicitation) { + maybeUpdateUI(tokenState, ChatState.WaitingForUserInput, true); + } else if (getCompactingMessage(msg)) { + maybeUpdateUI(tokenState, ChatState.Compacting); + } else if (getThinkingMessage(msg)) { + maybeUpdateUI(tokenState, ChatState.Thinking); + } else { + maybeUpdateUI(tokenState, ChatState.Streaming); } - case 'Notification': { - dispatch({ type: 'ADD_NOTIFICATION', payload: event as NotificationEvent }); - maybeHandlePlatformEvent(event.message, sessionId); - break; + return false; + } + case 'Error': { + flushBatchedUpdates(); + onFinish('Stream error: ' + (event as Record).error); + return true; + } + case 'Finish': { + flushBatchedUpdates(); + onFinish(); + return true; + } + case 'ModelChange': { + return false; + } + case 'UpdateConversation': { + const conversation = (event as Record).conversation as Message[]; + currentMessages = conversation; + if (!reduceMotion) { + dispatch({ type: 'SET_MESSAGES', payload: conversation }); + } else { + hasPendingUpdate = true; } - case 'Ping': - break; + return false; + } + case 'Notification': { + dispatch({ type: 'ADD_NOTIFICATION', payload: event as unknown as NotificationEvent }); + maybeHandlePlatformEvent((event as Record).message, sessionId); + return false; } + case 'Ping': + return false; + default: + return false; } + }; - // If we reach here, the stream ended without a Finish or Error event. - // This happens when the connection drops and retries are exhausted — the - // generator exits its loop without yielding a terminal event. We call - // onFinish() without an error to keep the conversation visible (passing - // an error would trigger a full-page error screen via sessionLoadError), - // then show a toast so the user knows the response may be incomplete. - // If the signal was aborted, the user intentionally stopped streaming, - // so we skip the toast. - flushBatchedUpdates(); - onFinish(); - if (!signal?.aborted) { - toastError({ - title: 'Connection lost', - msg: 'The response may be incomplete. You can try sending your message again.', - }); - } - } catch (error) { - flushBatchedUpdates(); - if (error instanceof Error && error.name !== 'AbortError') { - onFinish('Stream error: ' + errorMessage(error)); - } - } + return processEvent; } export function useChatStream({ @@ -342,8 +329,12 @@ export function useChatStream({ }: UseChatStreamProps): UseChatStreamReturn { const [state, dispatch] = useReducer(streamReducer, initialState); - // Refs for values needed in callbacks without causing re-renders - const abortControllerRef = useRef(null); + // Long-lived SSE connection for this session + const { addListener } = useSessionEvents(sessionId); + + // Track the active request for cancellation + const activeRequestIdRef = useRef(null); + const activeUnsubscribeRef = useRef<(() => void) | null>(null); const lastInteractionTimeRef = useRef(Date.now()); const namePollingRef = useRef | null>(null); @@ -368,6 +359,13 @@ export function useChatStream({ const onFinish = useCallback( async (error?: string): Promise => { + // Unsubscribe from event listener + if (activeUnsubscribeRef.current) { + activeUnsubscribeRef.current(); + activeUnsubscribeRef.current = null; + } + activeRequestIdRef.current = null; + if (namePollingRef.current) { clearTimeout(namePollingRef.current); namePollingRef.current = null; @@ -389,13 +387,10 @@ export function useChatStream({ } // Refresh session name after each reply for the first 3 user messages - // The backend regenerates the name after each of the first 3 user messages - // to refine it as more context becomes available if (!error && sessionId) { const currentState = stateRef.current; const userMessageCount = currentState.messages.filter((m) => m.role === 'user').length; - // Only refresh for the first 3 user messages if (userMessageCount <= 3) { try { const response = await getSession({ @@ -409,7 +404,6 @@ export function useChatStream({ ? { ...currentState.session, name: response.data.name } : undefined, }); - // Notify sidebar of the name change window.dispatchEvent( new CustomEvent(AppEvents.SESSION_RENAMED, { detail: { sessionId, newName: response.data.name }, @@ -417,7 +411,6 @@ export function useChatStream({ ); } } catch (refreshError) { - // Silently fail - this is a nice-to-have feature console.warn('Failed to refresh session name:', refreshError); } } @@ -428,6 +421,66 @@ export function useChatStream({ [onStreamFinish, sessionId] ); + /** + * Submit a message via the new POST+SSE pattern. + * 1. Generate request_id + * 2. Register SSE listener BEFORE POST (no race condition) + * 3. POST to /sessions/{id}/reply + * 4. Events arrive on the long-lived SSE connection + */ + const submitToSession = useCallback( + async ( + targetSessionId: string, + userMessage: Message, + currentMessages: Message[], + overrideConversation?: Message[], + recipeName?: string, + recipeVersion?: string, + ) => { + const requestId = uuidv7(); + activeRequestIdRef.current = requestId; + + // Create event processor and register listener BEFORE the POST + const processEvent = createEventProcessor( + currentMessages, + dispatch, + onFinish, + targetSessionId, + ); + + const unsubscribe = addListener(requestId, (event) => { + const isTerminal = processEvent(event); + if (isTerminal) { + unsubscribe(); + activeUnsubscribeRef.current = null; + activeRequestIdRef.current = null; + } + }); + activeUnsubscribeRef.current = unsubscribe; + + try { + await sessionReply({ + path: { id: targetSessionId }, + body: { + request_id: requestId, + user_message: userMessage, + override_conversation: overrideConversation, + recipe_name: recipeName, + recipe_version: recipeVersion, + }, + throwOnError: true, + }); + } catch (error) { + // POST failed — clean up listener and report error + unsubscribe(); + activeUnsubscribeRef.current = null; + activeRequestIdRef.current = null; + onFinish('Submit error: ' + errorMessage(error)); + } + }, + [addListener, onFinish] + ); + // Load session on mount or sessionId change useEffect(() => { if (!sessionId) return; @@ -520,7 +573,6 @@ export function useChatStream({ const { msg: userMessage, images } = input; const currentState = stateRef.current; - // Guard: Don't submit if session hasn't been loaded yet if (!currentState.session || currentState.chatState === ChatState.LoadingConversation) { return; } @@ -528,7 +580,6 @@ export function useChatStream({ const hasExistingMessages = currentState.messages.length > 0; const hasNewMessage = userMessage.trim().length > 0 || images.length > 0; - // Don't submit if there's no message and no conversation to continue if (!hasNewMessage && !hasExistingMessages) { return; } @@ -539,10 +590,8 @@ export function useChatStream({ if (!hasExistingMessages && hasNewMessage) { window.dispatchEvent(new CustomEvent(AppEvents.SESSION_CREATED)); - // Start polling for session name update during streaming - // The backend generates the name in parallel with the response const pollForName = async (attempts = 0) => { - if (attempts >= 20) return; // Max 20 attempts (10 seconds) + if (attempts >= 20) return; try { const response = await getSession({ @@ -553,7 +602,6 @@ export function useChatStream({ const currentName = currentState.session?.name; const newName = response.data?.name; - // Check if name has changed from the initial name if (newName && newName !== currentName) { dispatch({ type: 'SET_SESSION', @@ -566,13 +614,12 @@ export function useChatStream({ detail: { sessionId, newName }, }) ); - return; // Stop polling once name is updated + return; } } catch { // Silently continue polling } - // Continue polling if still streaming const latestState = stateRef.current; if ( latestState.chatState === ChatState.Streaming || @@ -583,7 +630,6 @@ export function useChatStream({ } }; - // Start polling after a short delay to give backend time to start name generation namePollingRef.current = setTimeout(() => pollForName(0), 1000); } @@ -599,38 +645,10 @@ export function useChatStream({ } dispatch({ type: 'START_STREAMING' }); - abortControllerRef.current = new AbortController(); - - try { - const { stream } = await reply({ - body: { - session_id: sessionId, - user_message: newMessage, - }, - throwOnError: true, - signal: abortControllerRef.current.signal, - sseMaxRetryAttempts: 0, - }); - await streamFromResponse( - stream, - currentMessages, - dispatch, - onFinish, - sessionId, - abortControllerRef.current.signal - ); - } catch (error) { - // AbortError is expected when user stops streaming - if (error instanceof Error && error.name === 'AbortError') { - // Silently handle abort - } else { - // Unexpected error during fetch setup (streamFromResponse handles its own errors) - onFinish('Submit error: ' + errorMessage(error)); - } - } + await submitToSession(sessionId, newMessage, currentMessages); }, - [sessionId, onFinish] + [sessionId, onFinish, submitToSession] ); const submitElicitationResponse = useCallback( @@ -648,36 +666,10 @@ export function useChatStream({ dispatch({ type: 'SET_MESSAGES', payload: currentMessages }); dispatch({ type: 'START_STREAMING' }); - abortControllerRef.current = new AbortController(); - - try { - const { stream } = await reply({ - body: { - session_id: sessionId, - user_message: responseMessage, - }, - throwOnError: true, - signal: abortControllerRef.current.signal, - sseMaxRetryAttempts: 0, - }); - await streamFromResponse( - stream, - currentMessages, - dispatch, - onFinish, - sessionId, - abortControllerRef.current.signal - ); - } catch (error) { - if (error instanceof Error && error.name === 'AbortError') { - // Silently handle abort - } else { - onFinish('Submit error: ' + errorMessage(error)); - } - } + await submitToSession(sessionId, responseMessage, currentMessages); }, - [sessionId, onFinish] + [sessionId, onFinish, submitToSession] ); const setRecipeUserParams = useCallback( @@ -694,7 +686,6 @@ export function useChatStream({ }, throwOnError: true, }); - // TODO(Douwe): get this from the server instead of emulating it here dispatch({ type: 'SET_SESSION', payload: { @@ -713,9 +704,6 @@ export function useChatStream({ ); useEffect(() => { - // This should happen on the server when the session is loaded or changed - // use session.id to support changing of sessions rather than depending on the - // stable sessionId. if (state.session) { updateFromSession({ body: { @@ -727,10 +715,27 @@ export function useChatStream({ }, [state.session]); const stopStreaming = useCallback(() => { - abortControllerRef.current?.abort(); + const requestId = activeRequestIdRef.current; + if (requestId) { + // Send cancel request to the server + sessionCancel({ + path: { id: sessionId }, + body: { request_id: requestId }, + }).catch((e) => { + console.warn('Failed to cancel request:', e); + }); + } + + // Clean up listener + if (activeUnsubscribeRef.current) { + activeUnsubscribeRef.current(); + activeUnsubscribeRef.current = null; + } + activeRequestIdRef.current = null; + dispatch({ type: 'SET_CHAT_STATE', payload: ChatState.Idle }); lastInteractionTimeRef.current = Date.now(); - }, []); + }, [sessionId]); const onMessageUpdate = useCallback( async (messageId: string, newContent: string, editType: 'fork' | 'edit' = 'fork') => { @@ -795,34 +800,7 @@ export function useChatStream({ dispatch({ type: 'SET_MESSAGES', payload: messagesForUI }); dispatch({ type: 'START_STREAMING' }); - abortControllerRef.current = new AbortController(); - - try { - const { stream } = await reply({ - body: { - session_id: targetSessionId, - user_message: updatedUserMessage, - }, - throwOnError: true, - signal: abortControllerRef.current.signal, - sseMaxRetryAttempts: 0, - }); - - await streamFromResponse( - stream, - messagesForUI, - dispatch, - onFinish, - targetSessionId, - abortControllerRef.current.signal - ); - } catch (error) { - if (error instanceof Error && error.name === 'AbortError') { - dispatch({ type: 'SET_CHAT_STATE', payload: ChatState.Idle }); - } else { - throw error; - } - } + await submitToSession(targetSessionId, updatedUserMessage, messagesForUI); } else { await handleSubmit({ msg: newContent, images: [] }); } @@ -838,7 +816,7 @@ export function useChatStream({ }); } }, - [sessionId, handleSubmit, onFinish] + [sessionId, handleSubmit, onFinish, submitToSession] ); const setChatState = useCallback((newState: ChatState) => { diff --git a/ui/desktop/src/hooks/useSessionEvents.ts b/ui/desktop/src/hooks/useSessionEvents.ts new file mode 100644 index 000000000000..9aad24234358 --- /dev/null +++ b/ui/desktop/src/hooks/useSessionEvents.ts @@ -0,0 +1,65 @@ +import { useEffect, useRef, useState, useCallback } from 'react'; +import { SessionSseClient, type SessionEvent } from '../api/sessionSseClient'; + +type EventHandler = (event: SessionEvent) => void; + +export function useSessionEvents(sessionId: string) { + const clientRef = useRef(null); + const [connected, setConnected] = useState(false); + + useEffect(() => { + if (!sessionId) return; + + let cancelled = false; + + (async () => { + const baseUrl = String(window.appConfig.get('GOOSE_API_HOST') || ''); + const secretKey = await window.electron.getSecretKey(); + + if (cancelled) return; + + const headers: Record = {}; + if (secretKey) { + headers['X-Secret-Key'] = secretKey; + } + + const client = new SessionSseClient(baseUrl, sessionId, headers); + client.onConnectionChange(setConnected); + client.connect(); + clientRef.current = client; + })(); + + return () => { + cancelled = true; + if (clientRef.current) { + clientRef.current.close(); + clientRef.current = null; + } + setConnected(false); + }; + }, [sessionId]); + + const addListener = useCallback( + (requestId: string, handler: EventHandler): (() => void) => { + const client = clientRef.current; + if (!client) { + return () => {}; + } + return client.addListener(requestId, handler); + }, + [] + ); + + const addGlobalListener = useCallback( + (handler: EventHandler): (() => void) => { + const client = clientRef.current; + if (!client) { + return () => {}; + } + return client.addGlobalListener(handler); + }, + [] + ); + + return { connected, addListener, addGlobalListener }; +} From 51123aabf24ac88e5c478b9c52b2ebdf9831c362 Mon Sep 17 00:00:00 2001 From: jh-block Date: Thu, 12 Mar 2026 14:11:08 +0100 Subject: [PATCH 02/30] style: cargo fmt --- crates/goose-server/src/routes/mod.rs | 2 +- .../goose-server/src/routes/session_events.rs | 25 ++++++------------- crates/goose-server/src/session_event_bus.rs | 12 +++------ 3 files changed, 11 insertions(+), 28 deletions(-) diff --git a/crates/goose-server/src/routes/mod.rs b/crates/goose-server/src/routes/mod.rs index ea9cbfa2d737..c440a0d99157 100644 --- a/crates/goose-server/src/routes/mod.rs +++ b/crates/goose-server/src/routes/mod.rs @@ -12,9 +12,9 @@ pub mod recipe; pub mod recipe_utils; pub mod reply; pub mod sampling; -pub mod session_events; pub mod schedule; pub mod session; +pub mod session_events; pub mod setup; pub mod status; pub mod telemetry; diff --git a/crates/goose-server/src/routes/session_events.rs b/crates/goose-server/src/routes/session_events.rs index 2510c340bfdb..8353ddc8378d 100644 --- a/crates/goose-server/src/routes/session_events.rs +++ b/crates/goose-server/src/routes/session_events.rs @@ -89,16 +89,12 @@ fn format_sse_event(seq: u64, json: &str) -> String { format!("id: {}\ndata: {}\n\n", seq, json) } -fn serialize_session_event( - seq: u64, - request_id: Option<&str>, - event: &MessageEvent, -) -> String { +fn serialize_session_event(seq: u64, request_id: Option<&str>, event: &MessageEvent) -> String { // Build JSON payload: { request_id?: string, ...event_fields } // We flatten request_id into the event JSON. - let mut event_json = serde_json::to_value(event).unwrap_or_else(|e| { - serde_json::json!({"type": "Error", "error": format!("Serialization error: {}", e)}) - }); + let mut event_json = serde_json::to_value(event).unwrap_or_else( + |e| serde_json::json!({"type": "Error", "error": format!("Serialization error: {}", e)}), + ); if let Some(rid) = request_id { if let serde_json::Value::Object(ref mut map) = event_json { @@ -145,11 +141,8 @@ pub async fn session_events( tokio::spawn(async move { // Send replayed events for event in &replay { - let frame = serialize_session_event( - event.seq, - event.request_id.as_deref(), - &event.event, - ); + let frame = + serialize_session_event(event.seq, event.request_id.as_deref(), &event.event); if tx.send(frame).await.is_err() { return; } @@ -301,11 +294,7 @@ pub async fn session_reply( { Ok(metadata) => metadata, Err(e) => { - tracing::error!( - "Failed to read session for {}: {}", - task_session_id, - e - ); + tracing::error!("Failed to read session for {}: {}", task_session_id, e); publish( Some(task_request_id.clone()), MessageEvent::Error { diff --git a/crates/goose-server/src/session_event_bus.rs b/crates/goose-server/src/session_event_bus.rs index c8d6f3996266..2b6e78819eba 100644 --- a/crates/goose-server/src/session_event_bus.rs +++ b/crates/goose-server/src/session_event_bus.rs @@ -69,10 +69,7 @@ impl SessionEventBus { let replay = if let Some(last_id) = last_event_id { let buf = self.buffer.lock().await; - buf.iter() - .filter(|e| e.seq > last_id) - .cloned() - .collect() + buf.iter().filter(|e| e.seq > last_id).cloned().collect() } else { Vec::new() }; @@ -122,11 +119,8 @@ mod tests { let bus = SessionEventBus::new(); // Publish some events - bus.publish( - Some("req-1".to_string()), - MessageEvent::Ping, - ) - .await; + bus.publish(Some("req-1".to_string()), MessageEvent::Ping) + .await; bus.publish( Some("req-1".to_string()), MessageEvent::Finish { From 6bdbd834d140a33c250bb708986f3f70cd2c2802 Mon Sep 17 00:00:00 2001 From: jh-block Date: Thu, 12 Mar 2026 14:14:34 +0100 Subject: [PATCH 03/30] fix: move sessionSseClient out of generated api/ directory --- ui/desktop/src/hooks/useChatStream.ts | 2 +- ui/desktop/src/hooks/useSessionEvents.ts | 2 +- ui/desktop/src/{api => utils}/sessionSseClient.ts | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename ui/desktop/src/{api => utils}/sessionSseClient.ts (100%) diff --git a/ui/desktop/src/hooks/useChatStream.ts b/ui/desktop/src/hooks/useChatStream.ts index 2bd900cf0a2b..8cd4d9a52ae8 100644 --- a/ui/desktop/src/hooks/useChatStream.ts +++ b/ui/desktop/src/hooks/useChatStream.ts @@ -28,7 +28,7 @@ import { errorMessage } from '../utils/conversionUtils'; import { showExtensionLoadResults } from '../utils/extensionErrorUtils'; import { maybeHandlePlatformEvent } from '../utils/platform_events'; import { useSessionEvents } from './useSessionEvents'; -import type { SessionEvent } from '../api/sessionSseClient'; +import type { SessionEvent } from '../utils/sessionSseClient'; const resultsCache = new Map(); diff --git a/ui/desktop/src/hooks/useSessionEvents.ts b/ui/desktop/src/hooks/useSessionEvents.ts index 9aad24234358..061a2b72622c 100644 --- a/ui/desktop/src/hooks/useSessionEvents.ts +++ b/ui/desktop/src/hooks/useSessionEvents.ts @@ -1,5 +1,5 @@ import { useEffect, useRef, useState, useCallback } from 'react'; -import { SessionSseClient, type SessionEvent } from '../api/sessionSseClient'; +import { SessionSseClient, type SessionEvent } from '../utils/sessionSseClient'; type EventHandler = (event: SessionEvent) => void; diff --git a/ui/desktop/src/api/sessionSseClient.ts b/ui/desktop/src/utils/sessionSseClient.ts similarity index 100% rename from ui/desktop/src/api/sessionSseClient.ts rename to ui/desktop/src/utils/sessionSseClient.ts From 3dd313007b8dac76c5f76b4518486e6ee570e617 Mon Sep 17 00:00:00 2001 From: jh-block Date: Thu, 12 Mar 2026 14:19:43 +0100 Subject: [PATCH 04/30] refactor: use generated SSE client for session events endpoint --- .../goose-server/src/routes/session_events.rs | 1 + ui/desktop/openapi.json | 9 +- ui/desktop/src/api/index.ts | 4 +- ui/desktop/src/api/sdk.gen.ts | 33 ++- ui/desktop/src/api/types.gen.ts | 4 +- ui/desktop/src/hooks/useChatStream.ts | 3 +- ui/desktop/src/hooks/useSessionEvents.ts | 87 ++++--- ui/desktop/src/utils/sessionSseClient.ts | 234 ------------------ 8 files changed, 89 insertions(+), 286 deletions(-) delete mode 100644 ui/desktop/src/utils/sessionSseClient.ts diff --git a/crates/goose-server/src/routes/session_events.rs b/crates/goose-server/src/routes/session_events.rs index 8353ddc8378d..f158910982e4 100644 --- a/crates/goose-server/src/routes/session_events.rs +++ b/crates/goose-server/src/routes/session_events.rs @@ -119,6 +119,7 @@ fn serialize_session_event(seq: u64, request_id: Option<&str>, event: &MessageEv ), responses( (status = 200, description = "SSE event stream", + body = MessageEvent, content_type = "text/event-stream"), ) )] diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 12708edc570e..9b5eb52437bc 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -3300,7 +3300,14 @@ ], "responses": { "200": { - "description": "SSE event stream" + "description": "SSE event stream", + "content": { + "text/event-stream": { + "schema": { + "$ref": "#/components/schemas/MessageEvent" + } + } + } } } } diff --git a/ui/desktop/src/api/index.ts b/ui/desktop/src/api/index.ts index 1204578660d4..6a564e9be599 100644 --- a/ui/desktop/src/api/index.ts +++ b/ui/desktop/src/api/index.ts @@ -1,4 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts -export { addExtension, agentAddExtension, agentRemoveExtension, backupConfig, callTool, cancelDownload, cancelLocalModelDownload, checkProvider, configureProviderOauth, confirmToolAction, createCustomProvider, createRecipe, createSchedule, decodeRecipe, deleteLocalModel, deleteModel, deleteRecipe, deleteSchedule, deleteSession, detectProvider, diagnostics, downloadHfModel, downloadModel, encodeRecipe, exportApp, exportSession, forkSession, getCanonicalModelInfo, getCustomProvider, getDictationConfig, getDownloadProgress, getExtensions, getLocalModelDownloadProgress, getModelSettings, getPrompt, getPrompts, getProviderCatalog, getProviderCatalogTemplate, getProviderModels, getRepoFiles, getSession, getSessionExtensions, getSessionInsights, getSlashCommands, getTools, getTunnelStatus, importApp, importSession, initConfig, inspectRunningJob, killRunningJob, listApps, listLocalModels, listModels, listRecipes, listSchedules, listSessions, mcpUiProxy, type Options, parseRecipe, pauseSchedule, providers, readAllConfig, readConfig, readResource, recipeToYaml, recoverConfig, removeConfig, removeCustomProvider, removeExtension, reply, resetPrompt, restartAgent, resumeAgent, runNowHandler, savePrompt, saveRecipe, scanRecipe, scheduleRecipe, searchHfModels, searchSessions, sendTelemetryEvent, sessionsHandler, setConfigProvider, setRecipeSlashCommand, startAgent, startNanogptSetup, startOpenrouterSetup, startTetrateSetup, startTunnel, status, stopAgent, stopTunnel, systemInfo, transcribeDictation, unpauseSchedule, updateAgentProvider, updateCustomProvider, updateFromSession, updateModelSettings, updateSchedule, updateSession, updateSessionName, updateSessionUserRecipeValues, updateWorkingDir, upsertConfig, upsertPermissions, validateConfig } from './sdk.gen'; -export type { ActionRequired, ActionRequiredData, AddExtensionData, AddExtensionErrors, AddExtensionRequest, AddExtensionResponse, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponse, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponse, AgentRemoveExtensionResponses, Annotations, Author, AuthorRequest, BackupConfigData, BackupConfigErrors, BackupConfigResponse, BackupConfigResponses, CallToolData, CallToolErrors, CallToolRequest, CallToolResponse, CallToolResponse2, CallToolResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelLocalModelDownloadData, CancelLocalModelDownloadErrors, CancelLocalModelDownloadResponses, ChatRequest, CheckProviderData, CheckProviderRequest, ClientOptions, CommandType, ConfigKey, ConfigKeyQuery, ConfigResponse, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionRequest, ConfirmToolActionResponses, Content, ContentBlock, Conversation, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponse, CreateCustomProviderResponse2, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeRequest, CreateRecipeResponse, CreateRecipeResponse2, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleRequest, CreateScheduleResponse, CreateScheduleResponses, CspMetadata, DeclarativeProviderConfig, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeRequest, DecodeRecipeResponse, DecodeRecipeResponse2, DecodeRecipeResponses, DeleteLocalModelData, DeleteLocalModelErrors, DeleteLocalModelResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeRequest, DeleteRecipeResponse, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponse, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderRequest, DetectProviderResponse, DetectProviderResponse2, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponse, DiagnosticsResponses, DictationProvider, DictationProviderStatus, DownloadHfModelData, DownloadHfModelErrors, DownloadHfModelResponse, DownloadHfModelResponses, DownloadModelData, DownloadModelErrors, DownloadModelRequest, DownloadModelResponses, DownloadProgress, DownloadStatus, EmbeddedResource, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeRequest, EncodeRecipeResponse, EncodeRecipeResponse2, EncodeRecipeResponses, Envs, EnvVarConfig, ErrorResponse, ExportAppData, ExportAppError, ExportAppErrors, ExportAppResponse, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponse, ExportSessionResponses, ExtensionConfig, ExtensionData, ExtensionEntry, ExtensionLoadResult, ExtensionQuery, ExtensionResponse, ForkRequest, ForkResponse, ForkSessionData, ForkSessionErrors, ForkSessionResponse, ForkSessionResponses, FrontendToolRequest, GetCanonicalModelInfoData, GetCanonicalModelInfoResponse, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponse, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponse, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponse, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponse, GetExtensionsResponses, GetLocalModelDownloadProgressData, GetLocalModelDownloadProgressErrors, GetLocalModelDownloadProgressResponse, GetLocalModelDownloadProgressResponses, GetModelSettingsData, GetModelSettingsErrors, GetModelSettingsResponse, GetModelSettingsResponses, GetPromptData, GetPromptErrors, GetPromptResponse, GetPromptResponses, GetPromptsData, GetPromptsResponse, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponse, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponse, GetProviderCatalogTemplateResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponse, GetProviderModelsResponses, GetRepoFilesData, GetRepoFilesResponse, GetRepoFilesResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponse, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponse, GetSessionInsightsResponses, GetSessionResponse, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponse, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsQuery, GetToolsResponse, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponse, GetTunnelStatusResponses, GooseApp, GooseMode, HfGgufFile, HfModelInfo, HfQuantVariant, Icon, ImageContent, ImportAppData, ImportAppError, ImportAppErrors, ImportAppRequest, ImportAppResponse, ImportAppResponse2, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionRequest, ImportSessionResponse, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponse, InitConfigResponses, InspectJobResponse, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponse, InspectRunningJobResponses, JsonObject, KillJobResponse, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsError, ListAppsErrors, ListAppsRequest, ListAppsResponse, ListAppsResponse2, ListAppsResponses, ListLocalModelsData, ListLocalModelsResponse, ListLocalModelsResponses, ListModelsData, ListModelsResponse, ListModelsResponses, ListRecipeResponse, ListRecipesData, ListRecipesErrors, ListRecipesResponse, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponse, ListSchedulesResponse2, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponse, ListSessionsResponses, LoadedProvider, LocalModelResponse, McpAppResource, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, Message, MessageContent, MessageEvent, MessageMetadata, ModelCapabilities, ModelConfig, ModelDownloadStatus, ModelInfo, ModelInfoData, ModelInfoQuery, ModelInfoResponse, ModelSettings, ModelTemplate, ParseRecipeData, ParseRecipeError, ParseRecipeErrors, ParseRecipeRequest, ParseRecipeResponse, ParseRecipeResponse2, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponse, PauseScheduleResponses, Permission, PermissionLevel, PermissionsMetadata, PrincipalType, PromptContentResponse, PromptsListResponse, ProviderCatalogEntry, ProviderDetails, ProviderEngine, ProviderMetadata, ProvidersData, ProvidersResponse, ProvidersResponse2, ProvidersResponses, ProviderTemplate, ProviderType, RawAudioContent, RawEmbeddedResource, RawImageContent, RawResource, RawTextContent, ReadAllConfigData, ReadAllConfigResponse, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceRequest, ReadResourceResponse, ReadResourceResponse2, ReadResourceResponses, ReasoningContent, Recipe, RecipeManifest, RecipeParameter, RecipeParameterInputType, RecipeParameterRequirement, RecipeToYamlData, RecipeToYamlError, RecipeToYamlErrors, RecipeToYamlRequest, RecipeToYamlResponse, RecipeToYamlResponse2, RecipeToYamlResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponse, RecoverConfigResponses, RedactedThinkingContent, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponse, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponse, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionRequest, RemoveExtensionResponse, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponse, ReplyResponses, RepoVariantsResponse, ResetPromptData, ResetPromptErrors, ResetPromptResponse, ResetPromptResponses, ResourceContents, ResourceMetadata, Response, RestartAgentData, RestartAgentErrors, RestartAgentRequest, RestartAgentResponse, RestartAgentResponse2, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentRequest, ResumeAgentResponse, ResumeAgentResponse2, ResumeAgentResponses, RetryConfig, Role, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponse, RunNowHandlerResponses, RunNowResponse, SamplingConfig, SavePromptData, SavePromptErrors, SavePromptRequest, SavePromptResponse, SavePromptResponses, SaveRecipeData, SaveRecipeError, SaveRecipeErrors, SaveRecipeRequest, SaveRecipeResponse, SaveRecipeResponse2, SaveRecipeResponses, ScanRecipeData, ScanRecipeRequest, ScanRecipeResponse, ScanRecipeResponse2, ScanRecipeResponses, ScheduledJob, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeRequest, ScheduleRecipeResponses, SearchHfModelsData, SearchHfModelsErrors, SearchHfModelsResponse, SearchHfModelsResponses, SearchSessionsData, SearchSessionsErrors, SearchSessionsResponse, SearchSessionsResponses, SendTelemetryEventData, SendTelemetryEventResponses, Session, SessionDisplayInfo, SessionExtensionsResponse, SessionInsights, SessionListResponse, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponse, SessionsHandlerResponses, SessionsQuery, SessionType, SetConfigProviderData, SetProviderRequest, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, SetSlashCommandRequest, Settings, SetupResponse, SlashCommand, SlashCommandsResponse, StartAgentData, StartAgentError, StartAgentErrors, StartAgentRequest, StartAgentResponse, StartAgentResponses, StartNanogptSetupData, StartNanogptSetupResponse, StartNanogptSetupResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponse, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponse, StartTetrateSetupResponses, StartTunnelData, StartTunnelError, StartTunnelErrors, StartTunnelResponse, StartTunnelResponses, StatusData, StatusResponse, StatusResponses, StopAgentData, StopAgentErrors, StopAgentRequest, StopAgentResponse, StopAgentResponses, StopTunnelData, StopTunnelError, StopTunnelErrors, StopTunnelResponses, SubRecipe, SuccessCheck, SystemInfo, SystemInfoData, SystemInfoResponse, SystemInfoResponses, SystemNotificationContent, SystemNotificationType, TaskSupport, TelemetryEventRequest, Template, TextContent, ThinkingContent, TokenState, Tool, ToolAnnotations, ToolConfirmationRequest, ToolExecution, ToolInfo, ToolPermission, ToolRequest, ToolResponse, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponse, TranscribeDictationResponses, TranscribeRequest, TranscribeResponse, TunnelInfo, TunnelState, UiMetadata, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponse, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderRequest, UpdateCustomProviderResponse, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionRequest, UpdateFromSessionResponses, UpdateModelSettingsData, UpdateModelSettingsErrors, UpdateModelSettingsResponse, UpdateModelSettingsResponses, UpdateProviderRequest, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleRequest, UpdateScheduleResponse, UpdateScheduleResponses, UpdateSessionData, UpdateSessionErrors, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameRequest, UpdateSessionNameResponses, UpdateSessionRequest, UpdateSessionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesError, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesRequest, UpdateSessionUserRecipeValuesResponse, UpdateSessionUserRecipeValuesResponse2, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirRequest, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigQuery, UpsertConfigResponse, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsQuery, UpsertPermissionsResponse, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponse, ValidateConfigResponses, WhisperModelResponse, WindowProps } from './types.gen'; +export { addExtension, agentAddExtension, agentRemoveExtension, backupConfig, callTool, cancelDownload, cancelLocalModelDownload, checkProvider, configureProviderOauth, confirmToolAction, createCustomProvider, createRecipe, createSchedule, decodeRecipe, deleteLocalModel, deleteModel, deleteRecipe, deleteSchedule, deleteSession, detectProvider, diagnostics, downloadHfModel, downloadModel, encodeRecipe, exportApp, exportSession, forkSession, getCanonicalModelInfo, getCustomProvider, getDictationConfig, getDownloadProgress, getExtensions, getLocalModelDownloadProgress, getModelSettings, getPrompt, getPrompts, getProviderCatalog, getProviderCatalogTemplate, getProviderModels, getRepoFiles, getSession, getSessionExtensions, getSessionInsights, getSlashCommands, getTools, getTunnelStatus, importApp, importSession, initConfig, inspectRunningJob, killRunningJob, listApps, listLocalModels, listModels, listRecipes, listSchedules, listSessions, mcpUiProxy, type Options, parseRecipe, pauseSchedule, providers, readAllConfig, readConfig, readResource, recipeToYaml, recoverConfig, removeConfig, removeCustomProvider, removeExtension, reply, resetPrompt, restartAgent, resumeAgent, runNowHandler, savePrompt, saveRecipe, scanRecipe, scheduleRecipe, searchHfModels, searchSessions, sendTelemetryEvent, sessionCancel, sessionEvents, sessionReply, sessionsHandler, setConfigProvider, setRecipeSlashCommand, startAgent, startOpenrouterSetup, startTetrateSetup, startTunnel, status, stopAgent, stopTunnel, systemInfo, transcribeDictation, unpauseSchedule, updateAgentProvider, updateCustomProvider, updateFromSession, updateModelSettings, updateSchedule, updateSessionName, updateSessionUserRecipeValues, updateWorkingDir, upsertConfig, upsertPermissions, validateConfig } from './sdk.gen'; +export type { ActionRequired, ActionRequiredData, AddExtensionData, AddExtensionErrors, AddExtensionRequest, AddExtensionResponse, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponse, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponse, AgentRemoveExtensionResponses, Annotations, Author, AuthorRequest, BackupConfigData, BackupConfigErrors, BackupConfigResponse, BackupConfigResponses, CallToolData, CallToolErrors, CallToolRequest, CallToolResponse, CallToolResponse2, CallToolResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelLocalModelDownloadData, CancelLocalModelDownloadErrors, CancelLocalModelDownloadResponses, CancelRequest, ChatRequest, CheckProviderData, CheckProviderRequest, ClientOptions, CommandType, ConfigKey, ConfigKeyQuery, ConfigResponse, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionRequest, ConfirmToolActionResponses, Content, ContentBlock, Conversation, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponse, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeRequest, CreateRecipeResponse, CreateRecipeResponse2, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleRequest, CreateScheduleResponse, CreateScheduleResponses, CspMetadata, DeclarativeProviderConfig, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeRequest, DecodeRecipeResponse, DecodeRecipeResponse2, DecodeRecipeResponses, DeleteLocalModelData, DeleteLocalModelErrors, DeleteLocalModelResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeRequest, DeleteRecipeResponse, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponse, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderRequest, DetectProviderResponse, DetectProviderResponse2, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponse, DiagnosticsResponses, DictationProvider, DictationProviderStatus, DownloadHfModelData, DownloadHfModelErrors, DownloadHfModelResponse, DownloadHfModelResponses, DownloadModelData, DownloadModelErrors, DownloadModelRequest, DownloadModelResponses, DownloadProgress, DownloadStatus, EmbeddedResource, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeRequest, EncodeRecipeResponse, EncodeRecipeResponse2, EncodeRecipeResponses, Envs, EnvVarConfig, ErrorResponse, ExportAppData, ExportAppError, ExportAppErrors, ExportAppResponse, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponse, ExportSessionResponses, ExtensionConfig, ExtensionData, ExtensionEntry, ExtensionLoadResult, ExtensionQuery, ExtensionResponse, ForkRequest, ForkResponse, ForkSessionData, ForkSessionErrors, ForkSessionResponse, ForkSessionResponses, FrontendToolRequest, GetCanonicalModelInfoData, GetCanonicalModelInfoResponse, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponse, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponse, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponse, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponse, GetExtensionsResponses, GetLocalModelDownloadProgressData, GetLocalModelDownloadProgressErrors, GetLocalModelDownloadProgressResponse, GetLocalModelDownloadProgressResponses, GetModelSettingsData, GetModelSettingsErrors, GetModelSettingsResponse, GetModelSettingsResponses, GetPromptData, GetPromptErrors, GetPromptResponse, GetPromptResponses, GetPromptsData, GetPromptsResponse, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponse, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponse, GetProviderCatalogTemplateResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponse, GetProviderModelsResponses, GetRepoFilesData, GetRepoFilesResponse, GetRepoFilesResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponse, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponse, GetSessionInsightsResponses, GetSessionResponse, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponse, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsQuery, GetToolsResponse, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponse, GetTunnelStatusResponses, GooseApp, HfGgufFile, HfModelInfo, HfQuantVariant, Icon, ImageContent, ImportAppData, ImportAppError, ImportAppErrors, ImportAppRequest, ImportAppResponse, ImportAppResponse2, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionRequest, ImportSessionResponse, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponse, InitConfigResponses, InspectJobResponse, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponse, InspectRunningJobResponses, JsonObject, KillJobResponse, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsError, ListAppsErrors, ListAppsRequest, ListAppsResponse, ListAppsResponse2, ListAppsResponses, ListLocalModelsData, ListLocalModelsResponse, ListLocalModelsResponses, ListModelsData, ListModelsResponse, ListModelsResponses, ListRecipeResponse, ListRecipesData, ListRecipesErrors, ListRecipesResponse, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponse, ListSchedulesResponse2, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponse, ListSessionsResponses, LoadedProvider, LocalModelResponse, McpAppResource, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, Message, MessageContent, MessageEvent, MessageMetadata, ModelCapabilities, ModelConfig, ModelDownloadStatus, ModelInfo, ModelInfoData, ModelInfoQuery, ModelInfoResponse, ModelSettings, ModelTemplate, ParseRecipeData, ParseRecipeError, ParseRecipeErrors, ParseRecipeRequest, ParseRecipeResponse, ParseRecipeResponse2, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponse, PauseScheduleResponses, Permission, PermissionLevel, PermissionsMetadata, PrincipalType, PromptContentResponse, PromptsListResponse, ProviderCatalogEntry, ProviderDetails, ProviderEngine, ProviderMetadata, ProvidersData, ProvidersResponse, ProvidersResponse2, ProvidersResponses, ProviderTemplate, ProviderType, RawAudioContent, RawEmbeddedResource, RawImageContent, RawResource, RawTextContent, ReadAllConfigData, ReadAllConfigResponse, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceRequest, ReadResourceResponse, ReadResourceResponse2, ReadResourceResponses, ReasoningContent, Recipe, RecipeManifest, RecipeParameter, RecipeParameterInputType, RecipeParameterRequirement, RecipeToYamlData, RecipeToYamlError, RecipeToYamlErrors, RecipeToYamlRequest, RecipeToYamlResponse, RecipeToYamlResponse2, RecipeToYamlResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponse, RecoverConfigResponses, RedactedThinkingContent, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponse, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponse, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionRequest, RemoveExtensionResponse, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponse, ReplyResponses, RepoVariantsResponse, ResetPromptData, ResetPromptErrors, ResetPromptResponse, ResetPromptResponses, ResourceContents, ResourceMetadata, Response, RestartAgentData, RestartAgentErrors, RestartAgentRequest, RestartAgentResponse, RestartAgentResponse2, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentRequest, ResumeAgentResponse, ResumeAgentResponse2, ResumeAgentResponses, RetryConfig, Role, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponse, RunNowHandlerResponses, RunNowResponse, SamplingConfig, SavePromptData, SavePromptErrors, SavePromptRequest, SavePromptResponse, SavePromptResponses, SaveRecipeData, SaveRecipeError, SaveRecipeErrors, SaveRecipeRequest, SaveRecipeResponse, SaveRecipeResponse2, SaveRecipeResponses, ScanRecipeData, ScanRecipeRequest, ScanRecipeResponse, ScanRecipeResponse2, ScanRecipeResponses, ScheduledJob, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeRequest, ScheduleRecipeResponses, SearchHfModelsData, SearchHfModelsErrors, SearchHfModelsResponse, SearchHfModelsResponses, SearchSessionsData, SearchSessionsErrors, SearchSessionsResponse, SearchSessionsResponses, SendTelemetryEventData, SendTelemetryEventResponses, Session, SessionCancelData, SessionCancelResponses, SessionDisplayInfo, SessionEventsData, SessionEventsResponse, SessionEventsResponses, SessionExtensionsResponse, SessionInsights, SessionListResponse, SessionReplyData, SessionReplyErrors, SessionReplyRequest, SessionReplyResponse, SessionReplyResponse2, SessionReplyResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponse, SessionsHandlerResponses, SessionsQuery, SessionType, SetConfigProviderData, SetProviderRequest, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, SetSlashCommandRequest, Settings, SetupResponse, SlashCommand, SlashCommandsResponse, StartAgentData, StartAgentError, StartAgentErrors, StartAgentRequest, StartAgentResponse, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponse, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponse, StartTetrateSetupResponses, StartTunnelData, StartTunnelError, StartTunnelErrors, StartTunnelResponse, StartTunnelResponses, StatusData, StatusResponse, StatusResponses, StopAgentData, StopAgentErrors, StopAgentRequest, StopAgentResponse, StopAgentResponses, StopTunnelData, StopTunnelError, StopTunnelErrors, StopTunnelResponses, SubRecipe, SuccessCheck, SystemInfo, SystemInfoData, SystemInfoResponse, SystemInfoResponses, SystemNotificationContent, SystemNotificationType, TaskSupport, TelemetryEventRequest, Template, TextContent, ThinkingContent, TokenState, Tool, ToolAnnotations, ToolConfirmationRequest, ToolExecution, ToolInfo, ToolPermission, ToolRequest, ToolResponse, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponse, TranscribeDictationResponses, TranscribeRequest, TranscribeResponse, TunnelInfo, TunnelState, UiMetadata, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponse, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderRequest, UpdateCustomProviderResponse, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionRequest, UpdateFromSessionResponses, UpdateModelSettingsData, UpdateModelSettingsErrors, UpdateModelSettingsResponse, UpdateModelSettingsResponses, UpdateProviderRequest, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleRequest, UpdateScheduleResponse, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameRequest, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesError, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesRequest, UpdateSessionUserRecipeValuesResponse, UpdateSessionUserRecipeValuesResponse2, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirRequest, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigQuery, UpsertConfigResponse, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsQuery, UpsertPermissionsResponse, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponse, ValidateConfigResponses, WhisperModelResponse, WindowProps } from './types.gen'; diff --git a/ui/desktop/src/api/sdk.gen.ts b/ui/desktop/src/api/sdk.gen.ts index 959cc195bc22..4ecd7832f8ba 100644 --- a/ui/desktop/src/api/sdk.gen.ts +++ b/ui/desktop/src/api/sdk.gen.ts @@ -2,7 +2,7 @@ import type { Client, Options as Options2, TDataShape } from './client'; import { client } from './client.gen'; -import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CallToolData, CallToolErrors, CallToolResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelLocalModelDownloadData, CancelLocalModelDownloadErrors, CancelLocalModelDownloadResponses, CheckProviderData, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteLocalModelData, DeleteLocalModelErrors, DeleteLocalModelResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, DownloadHfModelData, DownloadHfModelErrors, DownloadHfModelResponses, DownloadModelData, DownloadModelErrors, DownloadModelResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportAppData, ExportAppErrors, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, ForkSessionData, ForkSessionErrors, ForkSessionResponses, GetCanonicalModelInfoData, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetLocalModelDownloadProgressData, GetLocalModelDownloadProgressErrors, GetLocalModelDownloadProgressResponses, GetModelSettingsData, GetModelSettingsErrors, GetModelSettingsResponses, GetPromptData, GetPromptErrors, GetPromptResponses, GetPromptsData, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetRepoFilesData, GetRepoFilesResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportAppData, ImportAppErrors, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsErrors, ListAppsResponses, ListLocalModelsData, ListLocalModelsResponses, ListModelsData, ListModelsResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceResponses, RecipeToYamlData, RecipeToYamlErrors, RecipeToYamlResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResetPromptData, ResetPromptErrors, ResetPromptResponses, RestartAgentData, RestartAgentErrors, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SavePromptData, SavePromptErrors, SavePromptResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SearchHfModelsData, SearchHfModelsErrors, SearchHfModelsResponses, SearchSessionsData, SearchSessionsErrors, SearchSessionsResponses, SendTelemetryEventData, SendTelemetryEventResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartNanogptSetupData, StartNanogptSetupResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopAgentData, StopAgentErrors, StopAgentResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, SystemInfoData, SystemInfoResponses, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateModelSettingsData, UpdateModelSettingsErrors, UpdateModelSettingsResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionData, UpdateSessionErrors, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; +import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CallToolData, CallToolErrors, CallToolResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelLocalModelDownloadData, CancelLocalModelDownloadErrors, CancelLocalModelDownloadResponses, CheckProviderData, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteLocalModelData, DeleteLocalModelErrors, DeleteLocalModelResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, DownloadHfModelData, DownloadHfModelErrors, DownloadHfModelResponses, DownloadModelData, DownloadModelErrors, DownloadModelResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportAppData, ExportAppErrors, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, ForkSessionData, ForkSessionErrors, ForkSessionResponses, GetCanonicalModelInfoData, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetLocalModelDownloadProgressData, GetLocalModelDownloadProgressErrors, GetLocalModelDownloadProgressResponses, GetModelSettingsData, GetModelSettingsErrors, GetModelSettingsResponses, GetPromptData, GetPromptErrors, GetPromptResponses, GetPromptsData, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetRepoFilesData, GetRepoFilesResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportAppData, ImportAppErrors, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsErrors, ListAppsResponses, ListLocalModelsData, ListLocalModelsResponses, ListModelsData, ListModelsResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceResponses, RecipeToYamlData, RecipeToYamlErrors, RecipeToYamlResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResetPromptData, ResetPromptErrors, ResetPromptResponses, RestartAgentData, RestartAgentErrors, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SavePromptData, SavePromptErrors, SavePromptResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SearchHfModelsData, SearchHfModelsErrors, SearchHfModelsResponses, SearchSessionsData, SearchSessionsErrors, SearchSessionsResponses, SendTelemetryEventData, SendTelemetryEventResponses, SessionCancelData, SessionCancelResponses, SessionEventsData, SessionEventsResponses, SessionReplyData, SessionReplyErrors, SessionReplyResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopAgentData, StopAgentErrors, StopAgentResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, SystemInfoData, SystemInfoResponses, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateModelSettingsData, UpdateModelSettingsErrors, UpdateModelSettingsResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; export type Options = Options2 & { /** @@ -132,15 +132,6 @@ export const updateAgentProvider = (option } }); -export const updateSession = (options: Options) => (options.client ?? client).post({ - url: '/agent/update_session', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } -}); - export const updateWorkingDir = (options: Options) => (options.client ?? client).post({ url: '/agent/update_working_dir', ...options, @@ -317,8 +308,6 @@ export const transcribeDictation = (option } }); -export const startNanogptSetup = (options?: Options) => (options?.client ?? client).post({ url: '/handle_nanogpt', ...options }); - export const startOpenrouterSetup = (options?: Options) => (options?.client ?? client).post({ url: '/handle_openrouter', ...options }); export const startTetrateSetup = (options?: Options) => (options?.client ?? client).post({ url: '/handle_tetrate', ...options }); @@ -507,6 +496,26 @@ export const getSessionInsights = (options export const searchSessions = (options: Options) => (options.client ?? client).get({ url: '/sessions/search', ...options }); +export const sessionCancel = (options: Options) => (options.client ?? client).post({ + url: '/sessions/{id}/cancel', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +export const sessionEvents = (options: Options) => (options.client ?? client).sse.get({ url: '/sessions/{id}/events', ...options }); + +export const sessionReply = (options: Options) => (options.client ?? client).post({ + url: '/sessions/{id}/reply', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + export const deleteSession = (options: Options) => (options.client ?? client).delete({ url: '/sessions/{session_id}', ...options }); export const getSession = (options: Options) => (options.client ?? client).get({ url: '/sessions/{session_id}', ...options }); diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index be004dd47376..9983eb64b553 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -4161,9 +4161,11 @@ export type SessionEventsResponses = { /** * SSE event stream */ - 200: unknown; + 200: MessageEvent; }; +export type SessionEventsResponse = SessionEventsResponses[keyof SessionEventsResponses]; + export type SessionReplyData = { body: SessionReplyRequest; path: { diff --git a/ui/desktop/src/hooks/useChatStream.ts b/ui/desktop/src/hooks/useChatStream.ts index 8cd4d9a52ae8..d4f34381b8ea 100644 --- a/ui/desktop/src/hooks/useChatStream.ts +++ b/ui/desktop/src/hooks/useChatStream.ts @@ -27,8 +27,7 @@ import { import { errorMessage } from '../utils/conversionUtils'; import { showExtensionLoadResults } from '../utils/extensionErrorUtils'; import { maybeHandlePlatformEvent } from '../utils/platform_events'; -import { useSessionEvents } from './useSessionEvents'; -import type { SessionEvent } from '../utils/sessionSseClient'; +import { useSessionEvents, type SessionEvent } from './useSessionEvents'; const resultsCache = new Map(); diff --git a/ui/desktop/src/hooks/useSessionEvents.ts b/ui/desktop/src/hooks/useSessionEvents.ts index 061a2b72622c..83330d1a6a7c 100644 --- a/ui/desktop/src/hooks/useSessionEvents.ts +++ b/ui/desktop/src/hooks/useSessionEvents.ts @@ -1,65 +1,84 @@ import { useEffect, useRef, useState, useCallback } from 'react'; -import { SessionSseClient, type SessionEvent } from '../utils/sessionSseClient'; +import { sessionEvents, type MessageEvent } from '../api'; + +/** + * An SSE event with an optional request_id (added by the server at the + * SSE framing layer, not part of the generated MessageEvent type). + */ +export type SessionEvent = MessageEvent & { request_id?: string }; type EventHandler = (event: SessionEvent) => void; export function useSessionEvents(sessionId: string) { - const clientRef = useRef(null); + const listenersRef = useRef(new Map>()); + const abortRef = useRef(null); const [connected, setConnected] = useState(false); useEffect(() => { if (!sessionId) return; - let cancelled = false; + const abortController = new AbortController(); + abortRef.current = abortController; (async () => { - const baseUrl = String(window.appConfig.get('GOOSE_API_HOST') || ''); - const secretKey = await window.electron.getSecretKey(); + try { + const { stream } = await sessionEvents({ + path: { id: sessionId }, + signal: abortController.signal, + }); - if (cancelled) return; + setConnected(true); - const headers: Record = {}; - if (secretKey) { - headers['X-Secret-Key'] = secretKey; - } + for await (const event of stream) { + if (abortController.signal.aborted) break; + + // The server adds request_id to the JSON at the SSE framing layer + const sessionEvent = event as SessionEvent; + const requestId = sessionEvent.request_id; - const client = new SessionSseClient(baseUrl, sessionId, headers); - client.onConnectionChange(setConnected); - client.connect(); - clientRef.current = client; + if (requestId) { + const handlers = listenersRef.current.get(requestId); + if (handlers) { + for (const handler of handlers) { + handler(sessionEvent); + } + } + } + } + } catch (error) { + if (abortController.signal.aborted) return; + console.warn('SSE connection ended:', error); + } finally { + setConnected(false); + } })(); return () => { - cancelled = true; - if (clientRef.current) { - clientRef.current.close(); - clientRef.current = null; - } + abortController.abort(); + abortRef.current = null; setConnected(false); }; }, [sessionId]); const addListener = useCallback( (requestId: string, handler: EventHandler): (() => void) => { - const client = clientRef.current; - if (!client) { - return () => {}; + if (!listenersRef.current.has(requestId)) { + listenersRef.current.set(requestId, new Set()); } - return client.addListener(requestId, handler); - }, - [] - ); + listenersRef.current.get(requestId)!.add(handler); - const addGlobalListener = useCallback( - (handler: EventHandler): (() => void) => { - const client = clientRef.current; - if (!client) { - return () => {}; - } - return client.addGlobalListener(handler); + return () => { + const set = listenersRef.current.get(requestId); + if (set) { + set.delete(handler); + if (set.size === 0) { + listenersRef.current.delete(requestId); + } + } + }; }, [] ); - return { connected, addListener, addGlobalListener }; + return { connected, addListener }; } diff --git a/ui/desktop/src/utils/sessionSseClient.ts b/ui/desktop/src/utils/sessionSseClient.ts deleted file mode 100644 index 20e45af6e0ac..000000000000 --- a/ui/desktop/src/utils/sessionSseClient.ts +++ /dev/null @@ -1,234 +0,0 @@ -/** - * Hand-written SSE client for long-lived session event streams. - * - * Unlike the generated SSE client (which is designed for request-response SSE), - * this client maintains a persistent GET connection per session and supports - * safe reconnection via Last-Event-ID. - */ - -export interface SessionEvent { - type: string; - request_id?: string; - [key: string]: unknown; -} - -type EventHandler = (event: SessionEvent) => void; - -const INITIAL_RECONNECT_DELAY = 1000; -const MAX_RECONNECT_DELAY = 30_000; - -export class SessionSseClient { - private reader: ReadableStreamDefaultReader | null = null; - private lastEventId: string | undefined; - private abortController: AbortController | null = null; - private listeners = new Map>(); - private globalListeners = new Set(); - private reconnectDelay = INITIAL_RECONNECT_DELAY; - private closed = false; - private _connected = false; - private onConnectedChange?: (connected: boolean) => void; - - constructor( - private baseUrl: string, - private sessionId: string, - private headers: Record = {} - ) {} - - get connected(): boolean { - return this._connected; - } - - private setConnected(value: boolean) { - if (this._connected !== value) { - this._connected = value; - this.onConnectedChange?.(value); - } - } - - /** - * Set a callback for connection state changes. - */ - onConnectionChange(callback: (connected: boolean) => void): void { - this.onConnectedChange = callback; - } - - /** - * Open the SSE connection. Auto-reconnects on failure. - */ - connect(): void { - this.closed = false; - this.doConnect(); - } - - /** - * Close the SSE connection permanently. - */ - close(): void { - this.closed = true; - this.setConnected(false); - this.abortController?.abort(); - this.abortController = null; - this.reader = null; - this.listeners.clear(); - this.globalListeners.clear(); - } - - /** - * Register a listener for events with a specific request_id. - * Returns an unsubscribe function. - */ - addListener(requestId: string, handler: EventHandler): () => void { - if (!this.listeners.has(requestId)) { - this.listeners.set(requestId, new Set()); - } - this.listeners.get(requestId)!.add(handler); - - return () => { - const set = this.listeners.get(requestId); - if (set) { - set.delete(handler); - if (set.size === 0) { - this.listeners.delete(requestId); - } - } - }; - } - - /** - * Register a listener for ALL events (regardless of request_id). - * Returns an unsubscribe function. - */ - addGlobalListener(handler: EventHandler): () => void { - this.globalListeners.add(handler); - return () => { - this.globalListeners.delete(handler); - }; - } - - private async doConnect(): Promise { - if (this.closed) return; - - this.abortController = new AbortController(); - - const url = `${this.baseUrl}/sessions/${this.sessionId}/events`; - const headers: Record = { ...this.headers }; - if (this.lastEventId) { - headers['Last-Event-ID'] = this.lastEventId; - } - - try { - const response = await fetch(url, { - method: 'GET', - headers, - signal: this.abortController.signal, - }); - - if (!response.ok) { - throw new Error(`SSE connection failed: ${response.status}`); - } - - if (!response.body) { - throw new Error('SSE response has no body'); - } - - this.setConnected(true); - this.reconnectDelay = INITIAL_RECONNECT_DELAY; - - await this.readStream(response.body); - } catch (error) { - if (this.closed) return; - - if (error instanceof DOMException && error.name === 'AbortError') { - return; - } - - this.setConnected(false); - console.warn('SSE connection error, reconnecting...', error); - - // Exponential backoff - await new Promise((resolve) => setTimeout(resolve, this.reconnectDelay)); - this.reconnectDelay = Math.min(this.reconnectDelay * 2, MAX_RECONNECT_DELAY); - - this.doConnect(); - } - } - - private async readStream(body: ReadableStream): Promise { - const decoder = new TextDecoder(); - this.reader = body.getReader(); - - let buffer = ''; - - try { - while (true) { - const { done, value } = await this.reader.read(); - if (done) break; - - buffer += decoder.decode(value, { stream: true }); - - // Process complete SSE frames (double newline delimited) - const frames = buffer.split('\n\n'); - // Last element is incomplete — keep it in the buffer - buffer = frames.pop() || ''; - - for (const frame of frames) { - if (!frame.trim()) continue; - this.processFrame(frame); - } - } - } catch (error) { - if (this.closed) return; - throw error; - } finally { - this.reader = null; - } - - // Stream ended — reconnect if not closed - if (!this.closed) { - this.setConnected(false); - await new Promise((resolve) => setTimeout(resolve, this.reconnectDelay)); - this.doConnect(); - } - } - - private processFrame(frame: string): void { - let eventId: string | undefined; - let data: string | undefined; - - for (const line of frame.split('\n')) { - if (line.startsWith('id:')) { - eventId = line.slice(3).trim(); - } else if (line.startsWith('data:')) { - data = line.slice(5).trim(); - } - } - - // Track the last event ID for reconnection - if (eventId) { - this.lastEventId = eventId; - } - - if (!data) return; - - try { - const event: SessionEvent = JSON.parse(data); - - // Dispatch to global listeners - for (const handler of this.globalListeners) { - handler(event); - } - - // Dispatch to request-specific listeners - if (event.request_id) { - const handlers = this.listeners.get(event.request_id); - if (handlers) { - for (const handler of handlers) { - handler(event); - } - } - } - } catch (e) { - console.warn('Failed to parse SSE event data:', data, e); - } - } -} From 77f3421bf53907debb887939d48b922a4ea538bc Mon Sep 17 00:00:00 2001 From: jh-block Date: Thu, 12 Mar 2026 15:16:14 +0100 Subject: [PATCH 05/30] fix: remove unnecessary onFinish from useCallback dependency arrays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ESLint flagged onFinish as an unnecessary dependency in handleSubmit, submitElicitationResponse, and onMessageUpdate — it's already captured via the submitToSession closure. --- ui/desktop/src/hooks/useChatStream.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/desktop/src/hooks/useChatStream.ts b/ui/desktop/src/hooks/useChatStream.ts index d4f34381b8ea..c0415dca7b75 100644 --- a/ui/desktop/src/hooks/useChatStream.ts +++ b/ui/desktop/src/hooks/useChatStream.ts @@ -647,7 +647,7 @@ export function useChatStream({ await submitToSession(sessionId, newMessage, currentMessages); }, - [sessionId, onFinish, submitToSession] + [sessionId, submitToSession] ); const submitElicitationResponse = useCallback( @@ -668,7 +668,7 @@ export function useChatStream({ await submitToSession(sessionId, responseMessage, currentMessages); }, - [sessionId, onFinish, submitToSession] + [sessionId, submitToSession] ); const setRecipeUserParams = useCallback( @@ -815,7 +815,7 @@ export function useChatStream({ }); } }, - [sessionId, handleSubmit, onFinish, submitToSession] + [sessionId, handleSubmit, submitToSession] ); const setChatState = useCallback((newState: ChatState) => { From b9c5454d53f367399c0a3daf7a407e283b3e2984 Mon Sep 17 00:00:00 2001 From: jh-block Date: Mon, 16 Mar 2026 16:09:47 +0000 Subject: [PATCH 06/30] chore: regenerate API client after rebase onto main --- ui/desktop/src/api/index.ts | 4 ++-- ui/desktop/src/api/sdk.gen.ts | 11 ++++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/ui/desktop/src/api/index.ts b/ui/desktop/src/api/index.ts index 6a564e9be599..400202766cb8 100644 --- a/ui/desktop/src/api/index.ts +++ b/ui/desktop/src/api/index.ts @@ -1,4 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts -export { addExtension, agentAddExtension, agentRemoveExtension, backupConfig, callTool, cancelDownload, cancelLocalModelDownload, checkProvider, configureProviderOauth, confirmToolAction, createCustomProvider, createRecipe, createSchedule, decodeRecipe, deleteLocalModel, deleteModel, deleteRecipe, deleteSchedule, deleteSession, detectProvider, diagnostics, downloadHfModel, downloadModel, encodeRecipe, exportApp, exportSession, forkSession, getCanonicalModelInfo, getCustomProvider, getDictationConfig, getDownloadProgress, getExtensions, getLocalModelDownloadProgress, getModelSettings, getPrompt, getPrompts, getProviderCatalog, getProviderCatalogTemplate, getProviderModels, getRepoFiles, getSession, getSessionExtensions, getSessionInsights, getSlashCommands, getTools, getTunnelStatus, importApp, importSession, initConfig, inspectRunningJob, killRunningJob, listApps, listLocalModels, listModels, listRecipes, listSchedules, listSessions, mcpUiProxy, type Options, parseRecipe, pauseSchedule, providers, readAllConfig, readConfig, readResource, recipeToYaml, recoverConfig, removeConfig, removeCustomProvider, removeExtension, reply, resetPrompt, restartAgent, resumeAgent, runNowHandler, savePrompt, saveRecipe, scanRecipe, scheduleRecipe, searchHfModels, searchSessions, sendTelemetryEvent, sessionCancel, sessionEvents, sessionReply, sessionsHandler, setConfigProvider, setRecipeSlashCommand, startAgent, startOpenrouterSetup, startTetrateSetup, startTunnel, status, stopAgent, stopTunnel, systemInfo, transcribeDictation, unpauseSchedule, updateAgentProvider, updateCustomProvider, updateFromSession, updateModelSettings, updateSchedule, updateSessionName, updateSessionUserRecipeValues, updateWorkingDir, upsertConfig, upsertPermissions, validateConfig } from './sdk.gen'; -export type { ActionRequired, ActionRequiredData, AddExtensionData, AddExtensionErrors, AddExtensionRequest, AddExtensionResponse, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponse, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponse, AgentRemoveExtensionResponses, Annotations, Author, AuthorRequest, BackupConfigData, BackupConfigErrors, BackupConfigResponse, BackupConfigResponses, CallToolData, CallToolErrors, CallToolRequest, CallToolResponse, CallToolResponse2, CallToolResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelLocalModelDownloadData, CancelLocalModelDownloadErrors, CancelLocalModelDownloadResponses, CancelRequest, ChatRequest, CheckProviderData, CheckProviderRequest, ClientOptions, CommandType, ConfigKey, ConfigKeyQuery, ConfigResponse, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionRequest, ConfirmToolActionResponses, Content, ContentBlock, Conversation, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponse, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeRequest, CreateRecipeResponse, CreateRecipeResponse2, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleRequest, CreateScheduleResponse, CreateScheduleResponses, CspMetadata, DeclarativeProviderConfig, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeRequest, DecodeRecipeResponse, DecodeRecipeResponse2, DecodeRecipeResponses, DeleteLocalModelData, DeleteLocalModelErrors, DeleteLocalModelResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeRequest, DeleteRecipeResponse, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponse, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderRequest, DetectProviderResponse, DetectProviderResponse2, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponse, DiagnosticsResponses, DictationProvider, DictationProviderStatus, DownloadHfModelData, DownloadHfModelErrors, DownloadHfModelResponse, DownloadHfModelResponses, DownloadModelData, DownloadModelErrors, DownloadModelRequest, DownloadModelResponses, DownloadProgress, DownloadStatus, EmbeddedResource, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeRequest, EncodeRecipeResponse, EncodeRecipeResponse2, EncodeRecipeResponses, Envs, EnvVarConfig, ErrorResponse, ExportAppData, ExportAppError, ExportAppErrors, ExportAppResponse, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponse, ExportSessionResponses, ExtensionConfig, ExtensionData, ExtensionEntry, ExtensionLoadResult, ExtensionQuery, ExtensionResponse, ForkRequest, ForkResponse, ForkSessionData, ForkSessionErrors, ForkSessionResponse, ForkSessionResponses, FrontendToolRequest, GetCanonicalModelInfoData, GetCanonicalModelInfoResponse, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponse, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponse, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponse, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponse, GetExtensionsResponses, GetLocalModelDownloadProgressData, GetLocalModelDownloadProgressErrors, GetLocalModelDownloadProgressResponse, GetLocalModelDownloadProgressResponses, GetModelSettingsData, GetModelSettingsErrors, GetModelSettingsResponse, GetModelSettingsResponses, GetPromptData, GetPromptErrors, GetPromptResponse, GetPromptResponses, GetPromptsData, GetPromptsResponse, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponse, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponse, GetProviderCatalogTemplateResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponse, GetProviderModelsResponses, GetRepoFilesData, GetRepoFilesResponse, GetRepoFilesResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponse, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponse, GetSessionInsightsResponses, GetSessionResponse, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponse, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsQuery, GetToolsResponse, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponse, GetTunnelStatusResponses, GooseApp, HfGgufFile, HfModelInfo, HfQuantVariant, Icon, ImageContent, ImportAppData, ImportAppError, ImportAppErrors, ImportAppRequest, ImportAppResponse, ImportAppResponse2, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionRequest, ImportSessionResponse, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponse, InitConfigResponses, InspectJobResponse, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponse, InspectRunningJobResponses, JsonObject, KillJobResponse, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsError, ListAppsErrors, ListAppsRequest, ListAppsResponse, ListAppsResponse2, ListAppsResponses, ListLocalModelsData, ListLocalModelsResponse, ListLocalModelsResponses, ListModelsData, ListModelsResponse, ListModelsResponses, ListRecipeResponse, ListRecipesData, ListRecipesErrors, ListRecipesResponse, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponse, ListSchedulesResponse2, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponse, ListSessionsResponses, LoadedProvider, LocalModelResponse, McpAppResource, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, Message, MessageContent, MessageEvent, MessageMetadata, ModelCapabilities, ModelConfig, ModelDownloadStatus, ModelInfo, ModelInfoData, ModelInfoQuery, ModelInfoResponse, ModelSettings, ModelTemplate, ParseRecipeData, ParseRecipeError, ParseRecipeErrors, ParseRecipeRequest, ParseRecipeResponse, ParseRecipeResponse2, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponse, PauseScheduleResponses, Permission, PermissionLevel, PermissionsMetadata, PrincipalType, PromptContentResponse, PromptsListResponse, ProviderCatalogEntry, ProviderDetails, ProviderEngine, ProviderMetadata, ProvidersData, ProvidersResponse, ProvidersResponse2, ProvidersResponses, ProviderTemplate, ProviderType, RawAudioContent, RawEmbeddedResource, RawImageContent, RawResource, RawTextContent, ReadAllConfigData, ReadAllConfigResponse, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceRequest, ReadResourceResponse, ReadResourceResponse2, ReadResourceResponses, ReasoningContent, Recipe, RecipeManifest, RecipeParameter, RecipeParameterInputType, RecipeParameterRequirement, RecipeToYamlData, RecipeToYamlError, RecipeToYamlErrors, RecipeToYamlRequest, RecipeToYamlResponse, RecipeToYamlResponse2, RecipeToYamlResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponse, RecoverConfigResponses, RedactedThinkingContent, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponse, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponse, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionRequest, RemoveExtensionResponse, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponse, ReplyResponses, RepoVariantsResponse, ResetPromptData, ResetPromptErrors, ResetPromptResponse, ResetPromptResponses, ResourceContents, ResourceMetadata, Response, RestartAgentData, RestartAgentErrors, RestartAgentRequest, RestartAgentResponse, RestartAgentResponse2, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentRequest, ResumeAgentResponse, ResumeAgentResponse2, ResumeAgentResponses, RetryConfig, Role, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponse, RunNowHandlerResponses, RunNowResponse, SamplingConfig, SavePromptData, SavePromptErrors, SavePromptRequest, SavePromptResponse, SavePromptResponses, SaveRecipeData, SaveRecipeError, SaveRecipeErrors, SaveRecipeRequest, SaveRecipeResponse, SaveRecipeResponse2, SaveRecipeResponses, ScanRecipeData, ScanRecipeRequest, ScanRecipeResponse, ScanRecipeResponse2, ScanRecipeResponses, ScheduledJob, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeRequest, ScheduleRecipeResponses, SearchHfModelsData, SearchHfModelsErrors, SearchHfModelsResponse, SearchHfModelsResponses, SearchSessionsData, SearchSessionsErrors, SearchSessionsResponse, SearchSessionsResponses, SendTelemetryEventData, SendTelemetryEventResponses, Session, SessionCancelData, SessionCancelResponses, SessionDisplayInfo, SessionEventsData, SessionEventsResponse, SessionEventsResponses, SessionExtensionsResponse, SessionInsights, SessionListResponse, SessionReplyData, SessionReplyErrors, SessionReplyRequest, SessionReplyResponse, SessionReplyResponse2, SessionReplyResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponse, SessionsHandlerResponses, SessionsQuery, SessionType, SetConfigProviderData, SetProviderRequest, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, SetSlashCommandRequest, Settings, SetupResponse, SlashCommand, SlashCommandsResponse, StartAgentData, StartAgentError, StartAgentErrors, StartAgentRequest, StartAgentResponse, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponse, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponse, StartTetrateSetupResponses, StartTunnelData, StartTunnelError, StartTunnelErrors, StartTunnelResponse, StartTunnelResponses, StatusData, StatusResponse, StatusResponses, StopAgentData, StopAgentErrors, StopAgentRequest, StopAgentResponse, StopAgentResponses, StopTunnelData, StopTunnelError, StopTunnelErrors, StopTunnelResponses, SubRecipe, SuccessCheck, SystemInfo, SystemInfoData, SystemInfoResponse, SystemInfoResponses, SystemNotificationContent, SystemNotificationType, TaskSupport, TelemetryEventRequest, Template, TextContent, ThinkingContent, TokenState, Tool, ToolAnnotations, ToolConfirmationRequest, ToolExecution, ToolInfo, ToolPermission, ToolRequest, ToolResponse, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponse, TranscribeDictationResponses, TranscribeRequest, TranscribeResponse, TunnelInfo, TunnelState, UiMetadata, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponse, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderRequest, UpdateCustomProviderResponse, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionRequest, UpdateFromSessionResponses, UpdateModelSettingsData, UpdateModelSettingsErrors, UpdateModelSettingsResponse, UpdateModelSettingsResponses, UpdateProviderRequest, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleRequest, UpdateScheduleResponse, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameRequest, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesError, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesRequest, UpdateSessionUserRecipeValuesResponse, UpdateSessionUserRecipeValuesResponse2, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirRequest, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigQuery, UpsertConfigResponse, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsQuery, UpsertPermissionsResponse, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponse, ValidateConfigResponses, WhisperModelResponse, WindowProps } from './types.gen'; +export { addExtension, agentAddExtension, agentRemoveExtension, backupConfig, callTool, cancelDownload, cancelLocalModelDownload, checkProvider, configureProviderOauth, confirmToolAction, createCustomProvider, createRecipe, createSchedule, decodeRecipe, deleteLocalModel, deleteModel, deleteRecipe, deleteSchedule, deleteSession, detectProvider, diagnostics, downloadHfModel, downloadModel, encodeRecipe, exportApp, exportSession, forkSession, getCanonicalModelInfo, getCustomProvider, getDictationConfig, getDownloadProgress, getExtensions, getLocalModelDownloadProgress, getModelSettings, getPrompt, getPrompts, getProviderCatalog, getProviderCatalogTemplate, getProviderModels, getRepoFiles, getSession, getSessionExtensions, getSessionInsights, getSlashCommands, getTools, getTunnelStatus, importApp, importSession, initConfig, inspectRunningJob, killRunningJob, listApps, listLocalModels, listModels, listRecipes, listSchedules, listSessions, mcpUiProxy, type Options, parseRecipe, pauseSchedule, providers, readAllConfig, readConfig, readResource, recipeToYaml, recoverConfig, removeConfig, removeCustomProvider, removeExtension, reply, resetPrompt, restartAgent, resumeAgent, runNowHandler, savePrompt, saveRecipe, scanRecipe, scheduleRecipe, searchHfModels, searchSessions, sendTelemetryEvent, sessionCancel, sessionEvents, sessionReply, sessionsHandler, setConfigProvider, setRecipeSlashCommand, startAgent, startOpenrouterSetup, startTetrateSetup, startTunnel, status, stopAgent, stopTunnel, systemInfo, transcribeDictation, unpauseSchedule, updateAgentProvider, updateCustomProvider, updateFromSession, updateModelSettings, updateSchedule, updateSession, updateSessionName, updateSessionUserRecipeValues, updateWorkingDir, upsertConfig, upsertPermissions, validateConfig } from './sdk.gen'; +export type { ActionRequired, ActionRequiredData, AddExtensionData, AddExtensionErrors, AddExtensionRequest, AddExtensionResponse, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponse, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponse, AgentRemoveExtensionResponses, Annotations, Author, AuthorRequest, BackupConfigData, BackupConfigErrors, BackupConfigResponse, BackupConfigResponses, CallToolData, CallToolErrors, CallToolRequest, CallToolResponse, CallToolResponse2, CallToolResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelLocalModelDownloadData, CancelLocalModelDownloadErrors, CancelLocalModelDownloadResponses, CancelRequest, ChatRequest, CheckProviderData, CheckProviderRequest, ClientOptions, CommandType, ConfigKey, ConfigKeyQuery, ConfigResponse, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionRequest, ConfirmToolActionResponses, Content, ContentBlock, Conversation, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponse, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeRequest, CreateRecipeResponse, CreateRecipeResponse2, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleRequest, CreateScheduleResponse, CreateScheduleResponses, CspMetadata, DeclarativeProviderConfig, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeRequest, DecodeRecipeResponse, DecodeRecipeResponse2, DecodeRecipeResponses, DeleteLocalModelData, DeleteLocalModelErrors, DeleteLocalModelResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeRequest, DeleteRecipeResponse, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponse, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderRequest, DetectProviderResponse, DetectProviderResponse2, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponse, DiagnosticsResponses, DictationProvider, DictationProviderStatus, DownloadHfModelData, DownloadHfModelErrors, DownloadHfModelResponse, DownloadHfModelResponses, DownloadModelData, DownloadModelErrors, DownloadModelRequest, DownloadModelResponses, DownloadProgress, DownloadStatus, EmbeddedResource, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeRequest, EncodeRecipeResponse, EncodeRecipeResponse2, EncodeRecipeResponses, Envs, EnvVarConfig, ErrorResponse, ExportAppData, ExportAppError, ExportAppErrors, ExportAppResponse, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponse, ExportSessionResponses, ExtensionConfig, ExtensionData, ExtensionEntry, ExtensionLoadResult, ExtensionQuery, ExtensionResponse, ForkRequest, ForkResponse, ForkSessionData, ForkSessionErrors, ForkSessionResponse, ForkSessionResponses, FrontendToolRequest, GetCanonicalModelInfoData, GetCanonicalModelInfoResponse, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponse, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponse, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponse, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponse, GetExtensionsResponses, GetLocalModelDownloadProgressData, GetLocalModelDownloadProgressErrors, GetLocalModelDownloadProgressResponse, GetLocalModelDownloadProgressResponses, GetModelSettingsData, GetModelSettingsErrors, GetModelSettingsResponse, GetModelSettingsResponses, GetPromptData, GetPromptErrors, GetPromptResponse, GetPromptResponses, GetPromptsData, GetPromptsResponse, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponse, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponse, GetProviderCatalogTemplateResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponse, GetProviderModelsResponses, GetRepoFilesData, GetRepoFilesResponse, GetRepoFilesResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponse, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponse, GetSessionInsightsResponses, GetSessionResponse, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponse, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsQuery, GetToolsResponse, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponse, GetTunnelStatusResponses, GooseApp, GooseMode, HfGgufFile, HfModelInfo, HfQuantVariant, Icon, ImageContent, ImportAppData, ImportAppError, ImportAppErrors, ImportAppRequest, ImportAppResponse, ImportAppResponse2, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionRequest, ImportSessionResponse, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponse, InitConfigResponses, InspectJobResponse, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponse, InspectRunningJobResponses, JsonObject, KillJobResponse, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsError, ListAppsErrors, ListAppsRequest, ListAppsResponse, ListAppsResponse2, ListAppsResponses, ListLocalModelsData, ListLocalModelsResponse, ListLocalModelsResponses, ListModelsData, ListModelsResponse, ListModelsResponses, ListRecipeResponse, ListRecipesData, ListRecipesErrors, ListRecipesResponse, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponse, ListSchedulesResponse2, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponse, ListSessionsResponses, LoadedProvider, LocalModelResponse, McpAppResource, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, Message, MessageContent, MessageEvent, MessageMetadata, ModelCapabilities, ModelConfig, ModelDownloadStatus, ModelInfo, ModelInfoData, ModelInfoQuery, ModelInfoResponse, ModelSettings, ModelTemplate, ParseRecipeData, ParseRecipeError, ParseRecipeErrors, ParseRecipeRequest, ParseRecipeResponse, ParseRecipeResponse2, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponse, PauseScheduleResponses, Permission, PermissionLevel, PermissionsMetadata, PrincipalType, PromptContentResponse, PromptsListResponse, ProviderCatalogEntry, ProviderDetails, ProviderEngine, ProviderMetadata, ProvidersData, ProvidersResponse, ProvidersResponse2, ProvidersResponses, ProviderTemplate, ProviderType, RawAudioContent, RawEmbeddedResource, RawImageContent, RawResource, RawTextContent, ReadAllConfigData, ReadAllConfigResponse, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceRequest, ReadResourceResponse, ReadResourceResponse2, ReadResourceResponses, ReasoningContent, Recipe, RecipeManifest, RecipeParameter, RecipeParameterInputType, RecipeParameterRequirement, RecipeToYamlData, RecipeToYamlError, RecipeToYamlErrors, RecipeToYamlRequest, RecipeToYamlResponse, RecipeToYamlResponse2, RecipeToYamlResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponse, RecoverConfigResponses, RedactedThinkingContent, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponse, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponse, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionRequest, RemoveExtensionResponse, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponse, ReplyResponses, RepoVariantsResponse, ResetPromptData, ResetPromptErrors, ResetPromptResponse, ResetPromptResponses, ResourceContents, ResourceMetadata, Response, RestartAgentData, RestartAgentErrors, RestartAgentRequest, RestartAgentResponse, RestartAgentResponse2, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentRequest, ResumeAgentResponse, ResumeAgentResponse2, ResumeAgentResponses, RetryConfig, Role, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponse, RunNowHandlerResponses, RunNowResponse, SamplingConfig, SavePromptData, SavePromptErrors, SavePromptRequest, SavePromptResponse, SavePromptResponses, SaveRecipeData, SaveRecipeError, SaveRecipeErrors, SaveRecipeRequest, SaveRecipeResponse, SaveRecipeResponse2, SaveRecipeResponses, ScanRecipeData, ScanRecipeRequest, ScanRecipeResponse, ScanRecipeResponse2, ScanRecipeResponses, ScheduledJob, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeRequest, ScheduleRecipeResponses, SearchHfModelsData, SearchHfModelsErrors, SearchHfModelsResponse, SearchHfModelsResponses, SearchSessionsData, SearchSessionsErrors, SearchSessionsResponse, SearchSessionsResponses, SendTelemetryEventData, SendTelemetryEventResponses, Session, SessionCancelData, SessionCancelResponses, SessionDisplayInfo, SessionEventsData, SessionEventsResponse, SessionEventsResponses, SessionExtensionsResponse, SessionInsights, SessionListResponse, SessionReplyData, SessionReplyErrors, SessionReplyRequest, SessionReplyResponse, SessionReplyResponse2, SessionReplyResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponse, SessionsHandlerResponses, SessionsQuery, SessionType, SetConfigProviderData, SetProviderRequest, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, SetSlashCommandRequest, Settings, SetupResponse, SlashCommand, SlashCommandsResponse, StartAgentData, StartAgentError, StartAgentErrors, StartAgentRequest, StartAgentResponse, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponse, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponse, StartTetrateSetupResponses, StartTunnelData, StartTunnelError, StartTunnelErrors, StartTunnelResponse, StartTunnelResponses, StatusData, StatusResponse, StatusResponses, StopAgentData, StopAgentErrors, StopAgentRequest, StopAgentResponse, StopAgentResponses, StopTunnelData, StopTunnelError, StopTunnelErrors, StopTunnelResponses, SubRecipe, SuccessCheck, SystemInfo, SystemInfoData, SystemInfoResponse, SystemInfoResponses, SystemNotificationContent, SystemNotificationType, TaskSupport, TelemetryEventRequest, Template, TextContent, ThinkingContent, TokenState, Tool, ToolAnnotations, ToolConfirmationRequest, ToolExecution, ToolInfo, ToolPermission, ToolRequest, ToolResponse, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponse, TranscribeDictationResponses, TranscribeRequest, TranscribeResponse, TunnelInfo, TunnelState, UiMetadata, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponse, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderRequest, UpdateCustomProviderResponse, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionRequest, UpdateFromSessionResponses, UpdateModelSettingsData, UpdateModelSettingsErrors, UpdateModelSettingsResponse, UpdateModelSettingsResponses, UpdateProviderRequest, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleRequest, UpdateScheduleResponse, UpdateScheduleResponses, UpdateSessionData, UpdateSessionErrors, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameRequest, UpdateSessionNameResponses, UpdateSessionRequest, UpdateSessionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesError, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesRequest, UpdateSessionUserRecipeValuesResponse, UpdateSessionUserRecipeValuesResponse2, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirRequest, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigQuery, UpsertConfigResponse, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsQuery, UpsertPermissionsResponse, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponse, ValidateConfigResponses, WhisperModelResponse, WindowProps } from './types.gen'; diff --git a/ui/desktop/src/api/sdk.gen.ts b/ui/desktop/src/api/sdk.gen.ts index 4ecd7832f8ba..2296d10fa983 100644 --- a/ui/desktop/src/api/sdk.gen.ts +++ b/ui/desktop/src/api/sdk.gen.ts @@ -2,7 +2,7 @@ import type { Client, Options as Options2, TDataShape } from './client'; import { client } from './client.gen'; -import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CallToolData, CallToolErrors, CallToolResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelLocalModelDownloadData, CancelLocalModelDownloadErrors, CancelLocalModelDownloadResponses, CheckProviderData, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteLocalModelData, DeleteLocalModelErrors, DeleteLocalModelResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, DownloadHfModelData, DownloadHfModelErrors, DownloadHfModelResponses, DownloadModelData, DownloadModelErrors, DownloadModelResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportAppData, ExportAppErrors, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, ForkSessionData, ForkSessionErrors, ForkSessionResponses, GetCanonicalModelInfoData, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetLocalModelDownloadProgressData, GetLocalModelDownloadProgressErrors, GetLocalModelDownloadProgressResponses, GetModelSettingsData, GetModelSettingsErrors, GetModelSettingsResponses, GetPromptData, GetPromptErrors, GetPromptResponses, GetPromptsData, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetRepoFilesData, GetRepoFilesResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportAppData, ImportAppErrors, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsErrors, ListAppsResponses, ListLocalModelsData, ListLocalModelsResponses, ListModelsData, ListModelsResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceResponses, RecipeToYamlData, RecipeToYamlErrors, RecipeToYamlResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResetPromptData, ResetPromptErrors, ResetPromptResponses, RestartAgentData, RestartAgentErrors, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SavePromptData, SavePromptErrors, SavePromptResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SearchHfModelsData, SearchHfModelsErrors, SearchHfModelsResponses, SearchSessionsData, SearchSessionsErrors, SearchSessionsResponses, SendTelemetryEventData, SendTelemetryEventResponses, SessionCancelData, SessionCancelResponses, SessionEventsData, SessionEventsResponses, SessionReplyData, SessionReplyErrors, SessionReplyResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopAgentData, StopAgentErrors, StopAgentResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, SystemInfoData, SystemInfoResponses, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateModelSettingsData, UpdateModelSettingsErrors, UpdateModelSettingsResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; +import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CallToolData, CallToolErrors, CallToolResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelLocalModelDownloadData, CancelLocalModelDownloadErrors, CancelLocalModelDownloadResponses, CheckProviderData, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteLocalModelData, DeleteLocalModelErrors, DeleteLocalModelResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, DownloadHfModelData, DownloadHfModelErrors, DownloadHfModelResponses, DownloadModelData, DownloadModelErrors, DownloadModelResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportAppData, ExportAppErrors, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, ForkSessionData, ForkSessionErrors, ForkSessionResponses, GetCanonicalModelInfoData, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetLocalModelDownloadProgressData, GetLocalModelDownloadProgressErrors, GetLocalModelDownloadProgressResponses, GetModelSettingsData, GetModelSettingsErrors, GetModelSettingsResponses, GetPromptData, GetPromptErrors, GetPromptResponses, GetPromptsData, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetRepoFilesData, GetRepoFilesResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportAppData, ImportAppErrors, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsErrors, ListAppsResponses, ListLocalModelsData, ListLocalModelsResponses, ListModelsData, ListModelsResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceResponses, RecipeToYamlData, RecipeToYamlErrors, RecipeToYamlResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResetPromptData, ResetPromptErrors, ResetPromptResponses, RestartAgentData, RestartAgentErrors, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SavePromptData, SavePromptErrors, SavePromptResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SearchHfModelsData, SearchHfModelsErrors, SearchHfModelsResponses, SearchSessionsData, SearchSessionsErrors, SearchSessionsResponses, SendTelemetryEventData, SendTelemetryEventResponses, SessionCancelData, SessionCancelResponses, SessionEventsData, SessionEventsResponses, SessionReplyData, SessionReplyErrors, SessionReplyResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopAgentData, StopAgentErrors, StopAgentResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, SystemInfoData, SystemInfoResponses, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateModelSettingsData, UpdateModelSettingsErrors, UpdateModelSettingsResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionData, UpdateSessionErrors, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; export type Options = Options2 & { /** @@ -132,6 +132,15 @@ export const updateAgentProvider = (option } }); +export const updateSession = (options: Options) => (options.client ?? client).post({ + url: '/agent/update_session', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + export const updateWorkingDir = (options: Options) => (options.client ?? client).post({ url: '/agent/update_working_dir', ...options, From 2732eeda7f7df0f7ba490a9fa0296f3d3037d0f5 Mon Sep 17 00:00:00 2001 From: jh-block Date: Mon, 16 Mar 2026 16:31:29 +0000 Subject: [PATCH 07/30] fix: address code review feedback on SSE event bus - Preserve notification request_id: use entry().or_insert_with() so Notification events keep their tool-call request_id instead of being overwritten by the chat request UUID. - Keep heartbeat pings out of replay buffer: send SSE comments directly instead of publishing through the bus, preventing ping events from evicting real message history. - Fix duplicate events at replay/live handoff: snapshot the buffer before subscribing to live events, and skip live events with seq <= replay_max_seq. --- .../goose-server/src/routes/session_events.rs | 29 +++++++-------- crates/goose-server/src/session_event_bus.rs | 36 +++++++++++++------ 2 files changed, 40 insertions(+), 25 deletions(-) diff --git a/crates/goose-server/src/routes/session_events.rs b/crates/goose-server/src/routes/session_events.rs index f158910982e4..d2a6f36db270 100644 --- a/crates/goose-server/src/routes/session_events.rs +++ b/crates/goose-server/src/routes/session_events.rs @@ -98,10 +98,10 @@ fn serialize_session_event(seq: u64, request_id: Option<&str>, event: &MessageEv if let Some(rid) = request_id { if let serde_json::Value::Object(ref mut map) = event_json { - map.insert( - "request_id".to_string(), - serde_json::Value::String(rid.to_string()), - ); + // Only insert request_id if the event doesn't already carry one + // (e.g. Notification events have their own request_id for tool-call matching) + map.entry("request_id") + .or_insert_with(|| serde_json::Value::String(rid.to_string())); } } @@ -134,7 +134,7 @@ pub async fn session_events( .and_then(|s| s.parse().ok()); let bus = state.get_or_create_event_bus(&session_id).await; - let (replay, mut live_rx) = bus.subscribe(last_event_id).await; + let (replay, replay_max_seq, mut live_rx) = bus.subscribe(last_event_id).await; let (tx, rx) = mpsc::channel::(256); let stream = ReceiverStream::new(rx); @@ -151,13 +151,17 @@ pub async fn session_events( // Send live events + heartbeat pings let mut heartbeat_interval = tokio::time::interval(Duration::from_millis(500)); + // Heartbeat uses a local counter — not stored in the replay buffer + let mut heartbeat_seq = 0u64; loop { tokio::select! { _ = heartbeat_interval.tick() => { - // Publish a ping through the bus so it gets a sequence number - let seq = bus.publish(None, MessageEvent::Ping).await; - let frame = serialize_session_event(seq, None, &MessageEvent::Ping); + // Send heartbeat directly without publishing to the bus, + // so pings don't evict real events from the replay buffer. + // Use a comment-style SSE id so it won't interfere with Last-Event-ID. + let frame = format!(": ping {}\n\n", heartbeat_seq); + heartbeat_seq += 1; if tx.send(frame).await.is_err() { return; } @@ -165,8 +169,9 @@ pub async fn session_events( result = live_rx.recv() => { match result { Ok(event) => { - // Skip Ping events from the broadcast — we generate our own - if matches!(event.event, MessageEvent::Ping) { + // Skip events already covered by replay to avoid duplicates + // at the replay/live handoff boundary. + if event.seq <= replay_max_seq { continue; } let frame = serialize_session_event( @@ -358,16 +363,12 @@ pub async fn session_reply( } }; - let mut heartbeat_interval = tokio::time::interval(Duration::from_millis(500)); loop { tokio::select! { _ = task_cancel.cancelled() => { tracing::info!("Agent task cancelled for request {}", task_request_id); break; } - _ = heartbeat_interval.tick() => { - // Heartbeat is handled by the SSE event stream endpoint - } response = timeout(Duration::from_millis(500), stream.next()) => { match response { Ok(Some(Ok(AgentEvent::Message(message)))) => { diff --git a/crates/goose-server/src/session_event_bus.rs b/crates/goose-server/src/session_event_bus.rs index 2b6e78819eba..3f406621115b 100644 --- a/crates/goose-server/src/session_event_bus.rs +++ b/crates/goose-server/src/session_event_bus.rs @@ -60,21 +60,33 @@ impl SessionEventBus { } /// Subscribe to live events. If `last_event_id` is provided, replay buffered - /// events with seq > last_event_id. Returns (replay_events, live_receiver). + /// events with seq > last_event_id. Returns (replay_events, replay_max_seq, live_receiver). + /// + /// The buffer is snapshotted *before* subscribing to live events so that + /// any event published between the two steps only appears in `live_rx`. + /// The caller should skip live events with `seq <= replay_max_seq` to + /// avoid duplicates at the handoff boundary. pub async fn subscribe( &self, last_event_id: Option, - ) -> (Vec, broadcast::Receiver) { - let rx = self.tx.subscribe(); - - let replay = if let Some(last_id) = last_event_id { + ) -> (Vec, u64, broadcast::Receiver) { + // Snapshot the buffer first, then subscribe. This way an event + // published between the snapshot and the subscribe will only appear + // in `rx` (not in `replay`), and the caller filters it via seq. + let (replay, replay_max_seq) = { let buf = self.buffer.lock().await; - buf.iter().filter(|e| e.seq > last_id).cloned().collect() - } else { - Vec::new() + if let Some(last_id) = last_event_id { + let events: Vec<_> = buf.iter().filter(|e| e.seq > last_id).cloned().collect(); + let max_seq = events.last().map(|e| e.seq).unwrap_or(last_id); + (events, max_seq) + } else { + (Vec::new(), 0) + } }; - (replay, rx) + let rx = self.tx.subscribe(); + + (replay, replay_max_seq, rx) } /// Register a new request and return its cancellation token. @@ -131,10 +143,11 @@ mod tests { .await; // Subscribe with replay - let (replay, _rx) = bus.subscribe(Some(0)).await; + let (replay, replay_max_seq, _rx) = bus.subscribe(Some(0)).await; assert_eq!(replay.len(), 2); assert_eq!(replay[0].seq, 1); assert_eq!(replay[1].seq, 2); + assert_eq!(replay_max_seq, 2); } #[tokio::test] @@ -146,9 +159,10 @@ mod tests { bus.publish(None, MessageEvent::Ping).await; // Only get events after seq 2 - let (replay, _rx) = bus.subscribe(Some(2)).await; + let (replay, replay_max_seq, _rx) = bus.subscribe(Some(2)).await; assert_eq!(replay.len(), 1); assert_eq!(replay[0].seq, 3); + assert_eq!(replay_max_seq, 3); } #[tokio::test] From dd8a59d4e0982b7eced0cdab47bc12f7b7dfa612 Mon Sep 17 00:00:00 2001 From: jh-block Date: Mon, 16 Mar 2026 16:48:44 +0000 Subject: [PATCH 08/30] fix: route notification events correctly and clamp replay seq - Add chat_request_id field to SSE JSON so notification events (which carry their own MCP tool-call request_id) can still be routed to the correct frontend handler via the chat UUID. - Update useSessionEvents to dispatch on chat_request_id first, falling back to request_id. - Clamp replay_max_seq to the actual buffer max so a stale Last-Event-ID after server restart doesn't suppress all live events. --- .../goose-server/src/routes/session_events.rs | 8 +++++++- crates/goose-server/src/session_event_bus.rs | 5 ++++- ui/desktop/src/hooks/useSessionEvents.ts | 17 ++++++++++++----- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/crates/goose-server/src/routes/session_events.rs b/crates/goose-server/src/routes/session_events.rs index d2a6f36db270..628bf65c6e95 100644 --- a/crates/goose-server/src/routes/session_events.rs +++ b/crates/goose-server/src/routes/session_events.rs @@ -98,7 +98,13 @@ fn serialize_session_event(seq: u64, request_id: Option<&str>, event: &MessageEv if let Some(rid) = request_id { if let serde_json::Value::Object(ref mut map) = event_json { - // Only insert request_id if the event doesn't already carry one + // Always insert chat_request_id for routing (the chat UUID that + // the frontend registered its listener under). + map.insert( + "chat_request_id".to_string(), + serde_json::Value::String(rid.to_string()), + ); + // Also set request_id if the event doesn't already carry one // (e.g. Notification events have their own request_id for tool-call matching) map.entry("request_id") .or_insert_with(|| serde_json::Value::String(rid.to_string())); diff --git a/crates/goose-server/src/session_event_bus.rs b/crates/goose-server/src/session_event_bus.rs index 3f406621115b..cb659ddb6ff6 100644 --- a/crates/goose-server/src/session_event_bus.rs +++ b/crates/goose-server/src/session_event_bus.rs @@ -75,9 +75,12 @@ impl SessionEventBus { // in `rx` (not in `replay`), and the caller filters it via seq. let (replay, replay_max_seq) = { let buf = self.buffer.lock().await; + // Clamp to the actual buffer max so a stale Last-Event-ID + // (e.g. from before a server restart) doesn't suppress live events. + let buf_max = buf.back().map(|e| e.seq).unwrap_or(0); if let Some(last_id) = last_event_id { let events: Vec<_> = buf.iter().filter(|e| e.seq > last_id).cloned().collect(); - let max_seq = events.last().map(|e| e.seq).unwrap_or(last_id); + let max_seq = events.last().map(|e| e.seq).unwrap_or(last_id.min(buf_max)); (events, max_seq) } else { (Vec::new(), 0) diff --git a/ui/desktop/src/hooks/useSessionEvents.ts b/ui/desktop/src/hooks/useSessionEvents.ts index 83330d1a6a7c..f6906d66152e 100644 --- a/ui/desktop/src/hooks/useSessionEvents.ts +++ b/ui/desktop/src/hooks/useSessionEvents.ts @@ -5,7 +5,11 @@ import { sessionEvents, type MessageEvent } from '../api'; * An SSE event with an optional request_id (added by the server at the * SSE framing layer, not part of the generated MessageEvent type). */ -export type SessionEvent = MessageEvent & { request_id?: string }; +export type SessionEvent = MessageEvent & { + request_id?: string; + /** Chat-level request UUID used for routing events to the correct handler. */ + chat_request_id?: string; +}; type EventHandler = (event: SessionEvent) => void; @@ -32,12 +36,15 @@ export function useSessionEvents(sessionId: string) { for await (const event of stream) { if (abortController.signal.aborted) break; - // The server adds request_id to the JSON at the SSE framing layer + // The server adds chat_request_id (the chat UUID) and request_id + // to the JSON at the SSE framing layer. Route using chat_request_id + // so that Notification events (which carry their own MCP tool-call + // request_id) still reach the correct handler. const sessionEvent = event as SessionEvent; - const requestId = sessionEvent.request_id; + const routingId = sessionEvent.chat_request_id ?? sessionEvent.request_id; - if (requestId) { - const handlers = listenersRef.current.get(requestId); + if (routingId) { + const handlers = listenersRef.current.get(routingId); if (handlers) { for (const handler of handlers) { handler(sessionEvent); From 97cdf701d9eff5b36ae9b758e8eb507cdb7f667b Mon Sep 17 00:00:00 2001 From: jh-block Date: Mon, 16 Mar 2026 17:06:01 +0000 Subject: [PATCH 09/30] fix: replay buffered events on first SSE connect and clean up event buses - subscribe(None) now replays all buffered events so events published before the SSE stream establishes are not lost. - Add remove_event_bus() to AppState and call it on session deletion to prevent unbounded memory growth from accumulated replay buffers. - Add tests for first-connect replay and stale Last-Event-ID clamping. --- crates/goose-server/src/routes/session.rs | 3 ++ crates/goose-server/src/session_event_bus.rs | 40 ++++++++++++++++---- crates/goose-server/src/state.rs | 6 +++ 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/crates/goose-server/src/routes/session.rs b/crates/goose-server/src/routes/session.rs index b6a61c21fc2c..562576e0c6e1 100644 --- a/crates/goose-server/src/routes/session.rs +++ b/crates/goose-server/src/routes/session.rs @@ -296,6 +296,9 @@ async fn delete_session( } })?; + // Clean up the event bus to free its replay buffer + state.remove_event_bus(&session_id).await; + Ok(StatusCode::OK) } diff --git a/crates/goose-server/src/session_event_bus.rs b/crates/goose-server/src/session_event_bus.rs index cb659ddb6ff6..5b8ec770640e 100644 --- a/crates/goose-server/src/session_event_bus.rs +++ b/crates/goose-server/src/session_event_bus.rs @@ -78,13 +78,10 @@ impl SessionEventBus { // Clamp to the actual buffer max so a stale Last-Event-ID // (e.g. from before a server restart) doesn't suppress live events. let buf_max = buf.back().map(|e| e.seq).unwrap_or(0); - if let Some(last_id) = last_event_id { - let events: Vec<_> = buf.iter().filter(|e| e.seq > last_id).cloned().collect(); - let max_seq = events.last().map(|e| e.seq).unwrap_or(last_id.min(buf_max)); - (events, max_seq) - } else { - (Vec::new(), 0) - } + let last_id = last_event_id.unwrap_or(0); + let events: Vec<_> = buf.iter().filter(|e| e.seq > last_id).cloned().collect(); + let max_seq = events.last().map(|e| e.seq).unwrap_or(last_id.min(buf_max)); + (events, max_seq) }; let rx = self.tx.subscribe(); @@ -168,6 +165,35 @@ mod tests { assert_eq!(replay_max_seq, 3); } + #[tokio::test] + async fn test_subscribe_without_last_event_id_replays_all() { + let bus = SessionEventBus::new(); + + bus.publish(None, MessageEvent::Ping).await; + bus.publish(None, MessageEvent::Ping).await; + + // First connect (no Last-Event-ID) should replay all buffered events + let (replay, replay_max_seq, _rx) = bus.subscribe(None).await; + assert_eq!(replay.len(), 2); + assert_eq!(replay_max_seq, 2); + } + + #[tokio::test] + async fn test_subscribe_with_stale_last_event_id() { + let bus = SessionEventBus::new(); + + // Buffer has seq 1..3, but client sends Last-Event-ID: 9999 + bus.publish(None, MessageEvent::Ping).await; + bus.publish(None, MessageEvent::Ping).await; + bus.publish(None, MessageEvent::Ping).await; + + let (replay, replay_max_seq, _rx) = bus.subscribe(Some(9999)).await; + // No replay events (all are below 9999) + assert_eq!(replay.len(), 0); + // replay_max_seq should be clamped to buf_max (3), not 9999 + assert_eq!(replay_max_seq, 3); + } + #[tokio::test] async fn test_cancel_request() { let bus = SessionEventBus::new(); diff --git a/crates/goose-server/src/state.rs b/crates/goose-server/src/state.rs index d39312e9b2d3..170f150fda31 100644 --- a/crates/goose-server/src/state.rs +++ b/crates/goose-server/src/state.rs @@ -118,6 +118,12 @@ impl AppState { .clone() } + /// Remove the event bus for a session, freeing its replay buffer. + pub async fn remove_event_bus(&self, session_id: &str) { + let mut buses = self.session_buses.lock().await; + buses.remove(session_id); + } + pub async fn get_agent(&self, session_id: String) -> anyhow::Result> { self.agent_manager.get_or_create_agent(session_id).await } From 3620e5d8414c2294c859b081665a95911db7d140 Mon Sep 17 00:00:00 2001 From: jh-block Date: Tue, 17 Mar 2026 09:53:54 +0000 Subject: [PATCH 10/30] chore: regenerate API client after rebase onto main --- ui/desktop/src/api/index.ts | 4 ++-- ui/desktop/src/api/sdk.gen.ts | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/ui/desktop/src/api/index.ts b/ui/desktop/src/api/index.ts index 400202766cb8..3ab3b3d267dc 100644 --- a/ui/desktop/src/api/index.ts +++ b/ui/desktop/src/api/index.ts @@ -1,4 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts -export { addExtension, agentAddExtension, agentRemoveExtension, backupConfig, callTool, cancelDownload, cancelLocalModelDownload, checkProvider, configureProviderOauth, confirmToolAction, createCustomProvider, createRecipe, createSchedule, decodeRecipe, deleteLocalModel, deleteModel, deleteRecipe, deleteSchedule, deleteSession, detectProvider, diagnostics, downloadHfModel, downloadModel, encodeRecipe, exportApp, exportSession, forkSession, getCanonicalModelInfo, getCustomProvider, getDictationConfig, getDownloadProgress, getExtensions, getLocalModelDownloadProgress, getModelSettings, getPrompt, getPrompts, getProviderCatalog, getProviderCatalogTemplate, getProviderModels, getRepoFiles, getSession, getSessionExtensions, getSessionInsights, getSlashCommands, getTools, getTunnelStatus, importApp, importSession, initConfig, inspectRunningJob, killRunningJob, listApps, listLocalModels, listModels, listRecipes, listSchedules, listSessions, mcpUiProxy, type Options, parseRecipe, pauseSchedule, providers, readAllConfig, readConfig, readResource, recipeToYaml, recoverConfig, removeConfig, removeCustomProvider, removeExtension, reply, resetPrompt, restartAgent, resumeAgent, runNowHandler, savePrompt, saveRecipe, scanRecipe, scheduleRecipe, searchHfModels, searchSessions, sendTelemetryEvent, sessionCancel, sessionEvents, sessionReply, sessionsHandler, setConfigProvider, setRecipeSlashCommand, startAgent, startOpenrouterSetup, startTetrateSetup, startTunnel, status, stopAgent, stopTunnel, systemInfo, transcribeDictation, unpauseSchedule, updateAgentProvider, updateCustomProvider, updateFromSession, updateModelSettings, updateSchedule, updateSession, updateSessionName, updateSessionUserRecipeValues, updateWorkingDir, upsertConfig, upsertPermissions, validateConfig } from './sdk.gen'; -export type { ActionRequired, ActionRequiredData, AddExtensionData, AddExtensionErrors, AddExtensionRequest, AddExtensionResponse, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponse, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponse, AgentRemoveExtensionResponses, Annotations, Author, AuthorRequest, BackupConfigData, BackupConfigErrors, BackupConfigResponse, BackupConfigResponses, CallToolData, CallToolErrors, CallToolRequest, CallToolResponse, CallToolResponse2, CallToolResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelLocalModelDownloadData, CancelLocalModelDownloadErrors, CancelLocalModelDownloadResponses, CancelRequest, ChatRequest, CheckProviderData, CheckProviderRequest, ClientOptions, CommandType, ConfigKey, ConfigKeyQuery, ConfigResponse, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionRequest, ConfirmToolActionResponses, Content, ContentBlock, Conversation, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponse, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeRequest, CreateRecipeResponse, CreateRecipeResponse2, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleRequest, CreateScheduleResponse, CreateScheduleResponses, CspMetadata, DeclarativeProviderConfig, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeRequest, DecodeRecipeResponse, DecodeRecipeResponse2, DecodeRecipeResponses, DeleteLocalModelData, DeleteLocalModelErrors, DeleteLocalModelResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeRequest, DeleteRecipeResponse, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponse, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderRequest, DetectProviderResponse, DetectProviderResponse2, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponse, DiagnosticsResponses, DictationProvider, DictationProviderStatus, DownloadHfModelData, DownloadHfModelErrors, DownloadHfModelResponse, DownloadHfModelResponses, DownloadModelData, DownloadModelErrors, DownloadModelRequest, DownloadModelResponses, DownloadProgress, DownloadStatus, EmbeddedResource, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeRequest, EncodeRecipeResponse, EncodeRecipeResponse2, EncodeRecipeResponses, Envs, EnvVarConfig, ErrorResponse, ExportAppData, ExportAppError, ExportAppErrors, ExportAppResponse, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponse, ExportSessionResponses, ExtensionConfig, ExtensionData, ExtensionEntry, ExtensionLoadResult, ExtensionQuery, ExtensionResponse, ForkRequest, ForkResponse, ForkSessionData, ForkSessionErrors, ForkSessionResponse, ForkSessionResponses, FrontendToolRequest, GetCanonicalModelInfoData, GetCanonicalModelInfoResponse, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponse, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponse, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponse, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponse, GetExtensionsResponses, GetLocalModelDownloadProgressData, GetLocalModelDownloadProgressErrors, GetLocalModelDownloadProgressResponse, GetLocalModelDownloadProgressResponses, GetModelSettingsData, GetModelSettingsErrors, GetModelSettingsResponse, GetModelSettingsResponses, GetPromptData, GetPromptErrors, GetPromptResponse, GetPromptResponses, GetPromptsData, GetPromptsResponse, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponse, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponse, GetProviderCatalogTemplateResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponse, GetProviderModelsResponses, GetRepoFilesData, GetRepoFilesResponse, GetRepoFilesResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponse, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponse, GetSessionInsightsResponses, GetSessionResponse, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponse, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsQuery, GetToolsResponse, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponse, GetTunnelStatusResponses, GooseApp, GooseMode, HfGgufFile, HfModelInfo, HfQuantVariant, Icon, ImageContent, ImportAppData, ImportAppError, ImportAppErrors, ImportAppRequest, ImportAppResponse, ImportAppResponse2, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionRequest, ImportSessionResponse, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponse, InitConfigResponses, InspectJobResponse, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponse, InspectRunningJobResponses, JsonObject, KillJobResponse, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsError, ListAppsErrors, ListAppsRequest, ListAppsResponse, ListAppsResponse2, ListAppsResponses, ListLocalModelsData, ListLocalModelsResponse, ListLocalModelsResponses, ListModelsData, ListModelsResponse, ListModelsResponses, ListRecipeResponse, ListRecipesData, ListRecipesErrors, ListRecipesResponse, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponse, ListSchedulesResponse2, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponse, ListSessionsResponses, LoadedProvider, LocalModelResponse, McpAppResource, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, Message, MessageContent, MessageEvent, MessageMetadata, ModelCapabilities, ModelConfig, ModelDownloadStatus, ModelInfo, ModelInfoData, ModelInfoQuery, ModelInfoResponse, ModelSettings, ModelTemplate, ParseRecipeData, ParseRecipeError, ParseRecipeErrors, ParseRecipeRequest, ParseRecipeResponse, ParseRecipeResponse2, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponse, PauseScheduleResponses, Permission, PermissionLevel, PermissionsMetadata, PrincipalType, PromptContentResponse, PromptsListResponse, ProviderCatalogEntry, ProviderDetails, ProviderEngine, ProviderMetadata, ProvidersData, ProvidersResponse, ProvidersResponse2, ProvidersResponses, ProviderTemplate, ProviderType, RawAudioContent, RawEmbeddedResource, RawImageContent, RawResource, RawTextContent, ReadAllConfigData, ReadAllConfigResponse, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceRequest, ReadResourceResponse, ReadResourceResponse2, ReadResourceResponses, ReasoningContent, Recipe, RecipeManifest, RecipeParameter, RecipeParameterInputType, RecipeParameterRequirement, RecipeToYamlData, RecipeToYamlError, RecipeToYamlErrors, RecipeToYamlRequest, RecipeToYamlResponse, RecipeToYamlResponse2, RecipeToYamlResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponse, RecoverConfigResponses, RedactedThinkingContent, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponse, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponse, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionRequest, RemoveExtensionResponse, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponse, ReplyResponses, RepoVariantsResponse, ResetPromptData, ResetPromptErrors, ResetPromptResponse, ResetPromptResponses, ResourceContents, ResourceMetadata, Response, RestartAgentData, RestartAgentErrors, RestartAgentRequest, RestartAgentResponse, RestartAgentResponse2, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentRequest, ResumeAgentResponse, ResumeAgentResponse2, ResumeAgentResponses, RetryConfig, Role, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponse, RunNowHandlerResponses, RunNowResponse, SamplingConfig, SavePromptData, SavePromptErrors, SavePromptRequest, SavePromptResponse, SavePromptResponses, SaveRecipeData, SaveRecipeError, SaveRecipeErrors, SaveRecipeRequest, SaveRecipeResponse, SaveRecipeResponse2, SaveRecipeResponses, ScanRecipeData, ScanRecipeRequest, ScanRecipeResponse, ScanRecipeResponse2, ScanRecipeResponses, ScheduledJob, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeRequest, ScheduleRecipeResponses, SearchHfModelsData, SearchHfModelsErrors, SearchHfModelsResponse, SearchHfModelsResponses, SearchSessionsData, SearchSessionsErrors, SearchSessionsResponse, SearchSessionsResponses, SendTelemetryEventData, SendTelemetryEventResponses, Session, SessionCancelData, SessionCancelResponses, SessionDisplayInfo, SessionEventsData, SessionEventsResponse, SessionEventsResponses, SessionExtensionsResponse, SessionInsights, SessionListResponse, SessionReplyData, SessionReplyErrors, SessionReplyRequest, SessionReplyResponse, SessionReplyResponse2, SessionReplyResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponse, SessionsHandlerResponses, SessionsQuery, SessionType, SetConfigProviderData, SetProviderRequest, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, SetSlashCommandRequest, Settings, SetupResponse, SlashCommand, SlashCommandsResponse, StartAgentData, StartAgentError, StartAgentErrors, StartAgentRequest, StartAgentResponse, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponse, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponse, StartTetrateSetupResponses, StartTunnelData, StartTunnelError, StartTunnelErrors, StartTunnelResponse, StartTunnelResponses, StatusData, StatusResponse, StatusResponses, StopAgentData, StopAgentErrors, StopAgentRequest, StopAgentResponse, StopAgentResponses, StopTunnelData, StopTunnelError, StopTunnelErrors, StopTunnelResponses, SubRecipe, SuccessCheck, SystemInfo, SystemInfoData, SystemInfoResponse, SystemInfoResponses, SystemNotificationContent, SystemNotificationType, TaskSupport, TelemetryEventRequest, Template, TextContent, ThinkingContent, TokenState, Tool, ToolAnnotations, ToolConfirmationRequest, ToolExecution, ToolInfo, ToolPermission, ToolRequest, ToolResponse, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponse, TranscribeDictationResponses, TranscribeRequest, TranscribeResponse, TunnelInfo, TunnelState, UiMetadata, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponse, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderRequest, UpdateCustomProviderResponse, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionRequest, UpdateFromSessionResponses, UpdateModelSettingsData, UpdateModelSettingsErrors, UpdateModelSettingsResponse, UpdateModelSettingsResponses, UpdateProviderRequest, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleRequest, UpdateScheduleResponse, UpdateScheduleResponses, UpdateSessionData, UpdateSessionErrors, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameRequest, UpdateSessionNameResponses, UpdateSessionRequest, UpdateSessionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesError, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesRequest, UpdateSessionUserRecipeValuesResponse, UpdateSessionUserRecipeValuesResponse2, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirRequest, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigQuery, UpsertConfigResponse, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsQuery, UpsertPermissionsResponse, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponse, ValidateConfigResponses, WhisperModelResponse, WindowProps } from './types.gen'; +export { addExtension, agentAddExtension, agentRemoveExtension, backupConfig, callTool, cancelDownload, cancelLocalModelDownload, checkProvider, configureProviderOauth, confirmToolAction, createCustomProvider, createRecipe, createSchedule, decodeRecipe, deleteLocalModel, deleteModel, deleteRecipe, deleteSchedule, deleteSession, detectProvider, diagnostics, downloadHfModel, downloadModel, encodeRecipe, exportApp, exportSession, forkSession, getCanonicalModelInfo, getCustomProvider, getDictationConfig, getDownloadProgress, getExtensions, getLocalModelDownloadProgress, getModelSettings, getPrompt, getPrompts, getProviderCatalog, getProviderCatalogTemplate, getProviderModels, getRepoFiles, getSession, getSessionExtensions, getSessionInsights, getSlashCommands, getTools, getTunnelStatus, importApp, importSession, initConfig, inspectRunningJob, killRunningJob, listApps, listLocalModels, listModels, listRecipes, listSchedules, listSessions, mcpUiProxy, type Options, parseRecipe, pauseSchedule, providers, readAllConfig, readConfig, readResource, recipeToYaml, recoverConfig, removeConfig, removeCustomProvider, removeExtension, reply, resetPrompt, restartAgent, resumeAgent, runNowHandler, savePrompt, saveRecipe, scanRecipe, scheduleRecipe, searchHfModels, searchSessions, sendTelemetryEvent, sessionCancel, sessionEvents, sessionReply, sessionsHandler, setConfigProvider, setRecipeSlashCommand, startAgent, startNanogptSetup, startOpenrouterSetup, startTetrateSetup, startTunnel, status, stopAgent, stopTunnel, systemInfo, transcribeDictation, unpauseSchedule, updateAgentProvider, updateCustomProvider, updateFromSession, updateModelSettings, updateSchedule, updateSession, updateSessionName, updateSessionUserRecipeValues, updateWorkingDir, upsertConfig, upsertPermissions, validateConfig } from './sdk.gen'; +export type { ActionRequired, ActionRequiredData, AddExtensionData, AddExtensionErrors, AddExtensionRequest, AddExtensionResponse, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponse, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponse, AgentRemoveExtensionResponses, Annotations, Author, AuthorRequest, BackupConfigData, BackupConfigErrors, BackupConfigResponse, BackupConfigResponses, CallToolData, CallToolErrors, CallToolRequest, CallToolResponse, CallToolResponse2, CallToolResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelLocalModelDownloadData, CancelLocalModelDownloadErrors, CancelLocalModelDownloadResponses, CancelRequest, ChatRequest, CheckProviderData, CheckProviderRequest, ClientOptions, CommandType, ConfigKey, ConfigKeyQuery, ConfigResponse, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionRequest, ConfirmToolActionResponses, Content, ContentBlock, Conversation, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponse, CreateCustomProviderResponse2, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeRequest, CreateRecipeResponse, CreateRecipeResponse2, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleRequest, CreateScheduleResponse, CreateScheduleResponses, CspMetadata, DeclarativeProviderConfig, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeRequest, DecodeRecipeResponse, DecodeRecipeResponse2, DecodeRecipeResponses, DeleteLocalModelData, DeleteLocalModelErrors, DeleteLocalModelResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeRequest, DeleteRecipeResponse, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponse, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderRequest, DetectProviderResponse, DetectProviderResponse2, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponse, DiagnosticsResponses, DictationProvider, DictationProviderStatus, DownloadHfModelData, DownloadHfModelErrors, DownloadHfModelResponse, DownloadHfModelResponses, DownloadModelData, DownloadModelErrors, DownloadModelRequest, DownloadModelResponses, DownloadProgress, DownloadStatus, EmbeddedResource, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeRequest, EncodeRecipeResponse, EncodeRecipeResponse2, EncodeRecipeResponses, Envs, EnvVarConfig, ErrorResponse, ExportAppData, ExportAppError, ExportAppErrors, ExportAppResponse, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponse, ExportSessionResponses, ExtensionConfig, ExtensionData, ExtensionEntry, ExtensionLoadResult, ExtensionQuery, ExtensionResponse, ForkRequest, ForkResponse, ForkSessionData, ForkSessionErrors, ForkSessionResponse, ForkSessionResponses, FrontendToolRequest, GetCanonicalModelInfoData, GetCanonicalModelInfoResponse, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponse, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponse, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponse, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponse, GetExtensionsResponses, GetLocalModelDownloadProgressData, GetLocalModelDownloadProgressErrors, GetLocalModelDownloadProgressResponse, GetLocalModelDownloadProgressResponses, GetModelSettingsData, GetModelSettingsErrors, GetModelSettingsResponse, GetModelSettingsResponses, GetPromptData, GetPromptErrors, GetPromptResponse, GetPromptResponses, GetPromptsData, GetPromptsResponse, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponse, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponse, GetProviderCatalogTemplateResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponse, GetProviderModelsResponses, GetRepoFilesData, GetRepoFilesResponse, GetRepoFilesResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponse, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponse, GetSessionInsightsResponses, GetSessionResponse, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponse, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsQuery, GetToolsResponse, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponse, GetTunnelStatusResponses, GooseApp, GooseMode, HfGgufFile, HfModelInfo, HfQuantVariant, Icon, ImageContent, ImportAppData, ImportAppError, ImportAppErrors, ImportAppRequest, ImportAppResponse, ImportAppResponse2, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionRequest, ImportSessionResponse, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponse, InitConfigResponses, InspectJobResponse, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponse, InspectRunningJobResponses, JsonObject, KillJobResponse, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsError, ListAppsErrors, ListAppsRequest, ListAppsResponse, ListAppsResponse2, ListAppsResponses, ListLocalModelsData, ListLocalModelsResponse, ListLocalModelsResponses, ListModelsData, ListModelsResponse, ListModelsResponses, ListRecipeResponse, ListRecipesData, ListRecipesErrors, ListRecipesResponse, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponse, ListSchedulesResponse2, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponse, ListSessionsResponses, LoadedProvider, LocalModelResponse, McpAppResource, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, Message, MessageContent, MessageEvent, MessageMetadata, ModelCapabilities, ModelConfig, ModelDownloadStatus, ModelInfo, ModelInfoData, ModelInfoQuery, ModelInfoResponse, ModelSettings, ModelTemplate, ParseRecipeData, ParseRecipeError, ParseRecipeErrors, ParseRecipeRequest, ParseRecipeResponse, ParseRecipeResponse2, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponse, PauseScheduleResponses, Permission, PermissionLevel, PermissionsMetadata, PrincipalType, PromptContentResponse, PromptsListResponse, ProviderCatalogEntry, ProviderDetails, ProviderEngine, ProviderMetadata, ProvidersData, ProvidersResponse, ProvidersResponse2, ProvidersResponses, ProviderTemplate, ProviderType, RawAudioContent, RawEmbeddedResource, RawImageContent, RawResource, RawTextContent, ReadAllConfigData, ReadAllConfigResponse, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceRequest, ReadResourceResponse, ReadResourceResponse2, ReadResourceResponses, ReasoningContent, Recipe, RecipeManifest, RecipeParameter, RecipeParameterInputType, RecipeParameterRequirement, RecipeToYamlData, RecipeToYamlError, RecipeToYamlErrors, RecipeToYamlRequest, RecipeToYamlResponse, RecipeToYamlResponse2, RecipeToYamlResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponse, RecoverConfigResponses, RedactedThinkingContent, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponse, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponse, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionRequest, RemoveExtensionResponse, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponse, ReplyResponses, RepoVariantsResponse, ResetPromptData, ResetPromptErrors, ResetPromptResponse, ResetPromptResponses, ResourceContents, ResourceMetadata, Response, RestartAgentData, RestartAgentErrors, RestartAgentRequest, RestartAgentResponse, RestartAgentResponse2, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentRequest, ResumeAgentResponse, ResumeAgentResponse2, ResumeAgentResponses, RetryConfig, Role, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponse, RunNowHandlerResponses, RunNowResponse, SamplingConfig, SavePromptData, SavePromptErrors, SavePromptRequest, SavePromptResponse, SavePromptResponses, SaveRecipeData, SaveRecipeError, SaveRecipeErrors, SaveRecipeRequest, SaveRecipeResponse, SaveRecipeResponse2, SaveRecipeResponses, ScanRecipeData, ScanRecipeRequest, ScanRecipeResponse, ScanRecipeResponse2, ScanRecipeResponses, ScheduledJob, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeRequest, ScheduleRecipeResponses, SearchHfModelsData, SearchHfModelsErrors, SearchHfModelsResponse, SearchHfModelsResponses, SearchSessionsData, SearchSessionsErrors, SearchSessionsResponse, SearchSessionsResponses, SendTelemetryEventData, SendTelemetryEventResponses, Session, SessionCancelData, SessionCancelResponses, SessionDisplayInfo, SessionEventsData, SessionEventsResponse, SessionEventsResponses, SessionExtensionsResponse, SessionInsights, SessionListResponse, SessionReplyData, SessionReplyErrors, SessionReplyRequest, SessionReplyResponse, SessionReplyResponse2, SessionReplyResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponse, SessionsHandlerResponses, SessionsQuery, SessionType, SetConfigProviderData, SetProviderRequest, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, SetSlashCommandRequest, Settings, SetupResponse, SlashCommand, SlashCommandsResponse, StartAgentData, StartAgentError, StartAgentErrors, StartAgentRequest, StartAgentResponse, StartAgentResponses, StartNanogptSetupData, StartNanogptSetupResponse, StartNanogptSetupResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponse, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponse, StartTetrateSetupResponses, StartTunnelData, StartTunnelError, StartTunnelErrors, StartTunnelResponse, StartTunnelResponses, StatusData, StatusResponse, StatusResponses, StopAgentData, StopAgentErrors, StopAgentRequest, StopAgentResponse, StopAgentResponses, StopTunnelData, StopTunnelError, StopTunnelErrors, StopTunnelResponses, SubRecipe, SuccessCheck, SystemInfo, SystemInfoData, SystemInfoResponse, SystemInfoResponses, SystemNotificationContent, SystemNotificationType, TaskSupport, TelemetryEventRequest, Template, TextContent, ThinkingContent, TokenState, Tool, ToolAnnotations, ToolConfirmationRequest, ToolExecution, ToolInfo, ToolPermission, ToolRequest, ToolResponse, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponse, TranscribeDictationResponses, TranscribeRequest, TranscribeResponse, TunnelInfo, TunnelState, UiMetadata, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponse, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderRequest, UpdateCustomProviderResponse, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionRequest, UpdateFromSessionResponses, UpdateModelSettingsData, UpdateModelSettingsErrors, UpdateModelSettingsResponse, UpdateModelSettingsResponses, UpdateProviderRequest, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleRequest, UpdateScheduleResponse, UpdateScheduleResponses, UpdateSessionData, UpdateSessionErrors, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameRequest, UpdateSessionNameResponses, UpdateSessionRequest, UpdateSessionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesError, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesRequest, UpdateSessionUserRecipeValuesResponse, UpdateSessionUserRecipeValuesResponse2, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirRequest, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigQuery, UpsertConfigResponse, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsQuery, UpsertPermissionsResponse, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponse, ValidateConfigResponses, WhisperModelResponse, WindowProps } from './types.gen'; diff --git a/ui/desktop/src/api/sdk.gen.ts b/ui/desktop/src/api/sdk.gen.ts index 2296d10fa983..c4056e976edd 100644 --- a/ui/desktop/src/api/sdk.gen.ts +++ b/ui/desktop/src/api/sdk.gen.ts @@ -2,7 +2,7 @@ import type { Client, Options as Options2, TDataShape } from './client'; import { client } from './client.gen'; -import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CallToolData, CallToolErrors, CallToolResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelLocalModelDownloadData, CancelLocalModelDownloadErrors, CancelLocalModelDownloadResponses, CheckProviderData, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteLocalModelData, DeleteLocalModelErrors, DeleteLocalModelResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, DownloadHfModelData, DownloadHfModelErrors, DownloadHfModelResponses, DownloadModelData, DownloadModelErrors, DownloadModelResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportAppData, ExportAppErrors, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, ForkSessionData, ForkSessionErrors, ForkSessionResponses, GetCanonicalModelInfoData, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetLocalModelDownloadProgressData, GetLocalModelDownloadProgressErrors, GetLocalModelDownloadProgressResponses, GetModelSettingsData, GetModelSettingsErrors, GetModelSettingsResponses, GetPromptData, GetPromptErrors, GetPromptResponses, GetPromptsData, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetRepoFilesData, GetRepoFilesResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportAppData, ImportAppErrors, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsErrors, ListAppsResponses, ListLocalModelsData, ListLocalModelsResponses, ListModelsData, ListModelsResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceResponses, RecipeToYamlData, RecipeToYamlErrors, RecipeToYamlResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResetPromptData, ResetPromptErrors, ResetPromptResponses, RestartAgentData, RestartAgentErrors, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SavePromptData, SavePromptErrors, SavePromptResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SearchHfModelsData, SearchHfModelsErrors, SearchHfModelsResponses, SearchSessionsData, SearchSessionsErrors, SearchSessionsResponses, SendTelemetryEventData, SendTelemetryEventResponses, SessionCancelData, SessionCancelResponses, SessionEventsData, SessionEventsResponses, SessionReplyData, SessionReplyErrors, SessionReplyResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopAgentData, StopAgentErrors, StopAgentResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, SystemInfoData, SystemInfoResponses, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateModelSettingsData, UpdateModelSettingsErrors, UpdateModelSettingsResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionData, UpdateSessionErrors, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; +import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CallToolData, CallToolErrors, CallToolResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelLocalModelDownloadData, CancelLocalModelDownloadErrors, CancelLocalModelDownloadResponses, CheckProviderData, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteLocalModelData, DeleteLocalModelErrors, DeleteLocalModelResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, DownloadHfModelData, DownloadHfModelErrors, DownloadHfModelResponses, DownloadModelData, DownloadModelErrors, DownloadModelResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportAppData, ExportAppErrors, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, ForkSessionData, ForkSessionErrors, ForkSessionResponses, GetCanonicalModelInfoData, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetLocalModelDownloadProgressData, GetLocalModelDownloadProgressErrors, GetLocalModelDownloadProgressResponses, GetModelSettingsData, GetModelSettingsErrors, GetModelSettingsResponses, GetPromptData, GetPromptErrors, GetPromptResponses, GetPromptsData, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetRepoFilesData, GetRepoFilesResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportAppData, ImportAppErrors, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsErrors, ListAppsResponses, ListLocalModelsData, ListLocalModelsResponses, ListModelsData, ListModelsResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceResponses, RecipeToYamlData, RecipeToYamlErrors, RecipeToYamlResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResetPromptData, ResetPromptErrors, ResetPromptResponses, RestartAgentData, RestartAgentErrors, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SavePromptData, SavePromptErrors, SavePromptResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SearchHfModelsData, SearchHfModelsErrors, SearchHfModelsResponses, SearchSessionsData, SearchSessionsErrors, SearchSessionsResponses, SendTelemetryEventData, SendTelemetryEventResponses, SessionCancelData, SessionCancelResponses, SessionEventsData, SessionEventsResponses, SessionReplyData, SessionReplyErrors, SessionReplyResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartNanogptSetupData, StartNanogptSetupResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopAgentData, StopAgentErrors, StopAgentResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, SystemInfoData, SystemInfoResponses, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateModelSettingsData, UpdateModelSettingsErrors, UpdateModelSettingsResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionData, UpdateSessionErrors, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; export type Options = Options2 & { /** @@ -317,6 +317,8 @@ export const transcribeDictation = (option } }); +export const startNanogptSetup = (options?: Options) => (options?.client ?? client).post({ url: '/handle_nanogpt', ...options }); + export const startOpenrouterSetup = (options?: Options) => (options?.client ?? client).post({ url: '/handle_openrouter', ...options }); export const startTetrateSetup = (options?: Options) => (options?.client ?? client).post({ url: '/handle_tetrate', ...options }); From 06b2d513a08d9a17bf073449348c3fd3b5f399c6 Mon Sep 17 00:00:00 2001 From: jh-block Date: Tue, 17 Mar 2026 09:57:03 +0000 Subject: [PATCH 11/30] fix: subscribe before snapshot to prevent event gaps, close lagged streams - Reorder subscribe() to create the broadcast receiver before snapshotting the buffer, so no event can fall into a gap between the two steps. The caller deduplicates via replay_max_seq. - Close the SSE stream on RecvError::Lagged so the client reconnects with Last-Event-ID and replays missed events from the buffer, instead of silently losing them. --- .../goose-server/src/routes/session_events.rs | 6 ++++-- crates/goose-server/src/session_event_bus.rs | 17 ++++++++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/crates/goose-server/src/routes/session_events.rs b/crates/goose-server/src/routes/session_events.rs index 628bf65c6e95..6e7afd82a915 100644 --- a/crates/goose-server/src/routes/session_events.rs +++ b/crates/goose-server/src/routes/session_events.rs @@ -190,8 +190,10 @@ pub async fn session_events( } } Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => { - tracing::warn!("SSE subscriber lagged by {} events", n); - // Continue — client will reconnect if needed + tracing::warn!("SSE subscriber lagged by {} events, closing stream so client reconnects with Last-Event-ID", n); + // Close the stream so the client reconnects and + // replays missed events from the buffer. + return; } Err(tokio::sync::broadcast::error::RecvError::Closed) => { return; diff --git a/crates/goose-server/src/session_event_bus.rs b/crates/goose-server/src/session_event_bus.rs index 5b8ec770640e..8a96369398ff 100644 --- a/crates/goose-server/src/session_event_bus.rs +++ b/crates/goose-server/src/session_event_bus.rs @@ -62,17 +62,18 @@ impl SessionEventBus { /// Subscribe to live events. If `last_event_id` is provided, replay buffered /// events with seq > last_event_id. Returns (replay_events, replay_max_seq, live_receiver). /// - /// The buffer is snapshotted *before* subscribing to live events so that - /// any event published between the two steps only appears in `live_rx`. - /// The caller should skip live events with `seq <= replay_max_seq` to - /// avoid duplicates at the handoff boundary. + /// The live receiver is created *before* snapshotting the buffer so that + /// no event can fall into the gap between the two steps. The caller must + /// skip live events with `seq <= replay_max_seq` to deduplicate. pub async fn subscribe( &self, last_event_id: Option, ) -> (Vec, u64, broadcast::Receiver) { - // Snapshot the buffer first, then subscribe. This way an event - // published between the snapshot and the subscribe will only appear - // in `rx` (not in `replay`), and the caller filters it via seq. + // Subscribe first so that any event published while we hold the + // buffer lock is guaranteed to appear in `rx` (possibly duplicating + // a replay entry). The caller deduplicates via replay_max_seq. + let rx = self.tx.subscribe(); + let (replay, replay_max_seq) = { let buf = self.buffer.lock().await; // Clamp to the actual buffer max so a stale Last-Event-ID @@ -84,8 +85,6 @@ impl SessionEventBus { (events, max_seq) }; - let rx = self.tx.subscribe(); - (replay, replay_max_seq, rx) } From d93522227eabe08af7d5aacaf7e2e6efdc1c5056 Mon Sep 17 00:00:00 2001 From: jh-block Date: Tue, 17 Mar 2026 10:17:42 +0000 Subject: [PATCH 12/30] fix: cancel requests against the session that started them Store the originating session ID alongside the active request ID so that stopStreaming sends the cancel to the correct session, even if the user navigated to a different session while streaming was active. --- ui/desktop/src/hooks/useChatStream.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/ui/desktop/src/hooks/useChatStream.ts b/ui/desktop/src/hooks/useChatStream.ts index c0415dca7b75..e7b72a2646d4 100644 --- a/ui/desktop/src/hooks/useChatStream.ts +++ b/ui/desktop/src/hooks/useChatStream.ts @@ -331,8 +331,9 @@ export function useChatStream({ // Long-lived SSE connection for this session const { addListener } = useSessionEvents(sessionId); - // Track the active request for cancellation + // Track the active request for cancellation (includes the session that started it) const activeRequestIdRef = useRef(null); + const activeRequestSessionIdRef = useRef(null); const activeUnsubscribeRef = useRef<(() => void) | null>(null); const lastInteractionTimeRef = useRef(Date.now()); const namePollingRef = useRef | null>(null); @@ -364,6 +365,7 @@ export function useChatStream({ activeUnsubscribeRef.current = null; } activeRequestIdRef.current = null; + activeRequestSessionIdRef.current = null; if (namePollingRef.current) { clearTimeout(namePollingRef.current); @@ -438,6 +440,7 @@ export function useChatStream({ ) => { const requestId = uuidv7(); activeRequestIdRef.current = requestId; + activeRequestSessionIdRef.current = targetSessionId; // Create event processor and register listener BEFORE the POST const processEvent = createEventProcessor( @@ -453,6 +456,7 @@ export function useChatStream({ unsubscribe(); activeUnsubscribeRef.current = null; activeRequestIdRef.current = null; + activeRequestSessionIdRef.current = null; } }); activeUnsubscribeRef.current = unsubscribe; @@ -474,6 +478,7 @@ export function useChatStream({ unsubscribe(); activeUnsubscribeRef.current = null; activeRequestIdRef.current = null; + activeRequestSessionIdRef.current = null; onFinish('Submit error: ' + errorMessage(error)); } }, @@ -715,10 +720,12 @@ export function useChatStream({ const stopStreaming = useCallback(() => { const requestId = activeRequestIdRef.current; - if (requestId) { - // Send cancel request to the server + const requestSessionId = activeRequestSessionIdRef.current; + if (requestId && requestSessionId) { + // Cancel against the session that originally started the request, + // not the current sessionId (which may have changed if user navigated). sessionCancel({ - path: { id: sessionId }, + path: { id: requestSessionId }, body: { request_id: requestId }, }).catch((e) => { console.warn('Failed to cancel request:', e); @@ -731,10 +738,11 @@ export function useChatStream({ activeUnsubscribeRef.current = null; } activeRequestIdRef.current = null; + activeRequestSessionIdRef.current = null; dispatch({ type: 'SET_CHAT_STATE', payload: ChatState.Idle }); lastInteractionTimeRef.current = Date.now(); - }, [sessionId]); + }, []); const onMessageUpdate = useCallback( async (messageId: string, newContent: string, editType: 'fork' | 'edit' = 'fork') => { From 6a52e0ed6a2cd4a42e72145ac8faa2f1d891c68d Mon Sep 17 00:00:00 2001 From: jh-block Date: Tue, 17 Mar 2026 11:05:07 +0000 Subject: [PATCH 13/30] fix: abort in-flight reply POST when user clicks stop If the user clicks stop while the session_reply POST is still in flight, the cancel request could arrive first and no-op on the server. Use an AbortController to abort the fetch so the reply never starts, then still send sessionCancel as a belt-and-suspenders measure. --- ui/desktop/src/hooks/useChatStream.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/ui/desktop/src/hooks/useChatStream.ts b/ui/desktop/src/hooks/useChatStream.ts index e7b72a2646d4..9bafec84ead5 100644 --- a/ui/desktop/src/hooks/useChatStream.ts +++ b/ui/desktop/src/hooks/useChatStream.ts @@ -334,6 +334,7 @@ export function useChatStream({ // Track the active request for cancellation (includes the session that started it) const activeRequestIdRef = useRef(null); const activeRequestSessionIdRef = useRef(null); + const activeAbortRef = useRef(null); const activeUnsubscribeRef = useRef<(() => void) | null>(null); const lastInteractionTimeRef = useRef(Date.now()); const namePollingRef = useRef | null>(null); @@ -366,6 +367,7 @@ export function useChatStream({ } activeRequestIdRef.current = null; activeRequestSessionIdRef.current = null; + activeAbortRef.current = null; if (namePollingRef.current) { clearTimeout(namePollingRef.current); @@ -439,8 +441,10 @@ export function useChatStream({ recipeVersion?: string, ) => { const requestId = uuidv7(); + const abortController = new AbortController(); activeRequestIdRef.current = requestId; activeRequestSessionIdRef.current = targetSessionId; + activeAbortRef.current = abortController; // Create event processor and register listener BEFORE the POST const processEvent = createEventProcessor( @@ -457,6 +461,7 @@ export function useChatStream({ activeUnsubscribeRef.current = null; activeRequestIdRef.current = null; activeRequestSessionIdRef.current = null; + activeAbortRef.current = null; } }); activeUnsubscribeRef.current = unsubscribe; @@ -471,14 +476,18 @@ export function useChatStream({ recipe_name: recipeName, recipe_version: recipeVersion, }, + signal: abortController.signal, throwOnError: true, }); } catch (error) { + // Abort is expected when stopStreaming races with the POST + if (abortController.signal.aborted) return; // POST failed — clean up listener and report error unsubscribe(); activeUnsubscribeRef.current = null; activeRequestIdRef.current = null; activeRequestSessionIdRef.current = null; + activeAbortRef.current = null; onFinish('Submit error: ' + errorMessage(error)); } }, @@ -721,6 +730,13 @@ export function useChatStream({ const stopStreaming = useCallback(() => { const requestId = activeRequestIdRef.current; const requestSessionId = activeRequestSessionIdRef.current; + + // Abort the in-flight POST so the reply never starts if cancel wins the race + if (activeAbortRef.current) { + activeAbortRef.current.abort(); + activeAbortRef.current = null; + } + if (requestId && requestSessionId) { // Cancel against the session that originally started the request, // not the current sessionId (which may have changed if user navigated). From a1d1709f9ca8b0197e540639bb82f58ecd0cf46b Mon Sep 17 00:00:00 2001 From: jh-block Date: Tue, 17 Mar 2026 11:16:04 +0000 Subject: [PATCH 14/30] fix: reconnect SSE stream after normal server-side EOF The backend intentionally closes SSE streams in some cases (e.g. lagged subscribers). Previously the hook exited silently on normal stream completion, leaving the UI stuck in streaming state. Wrap the stream in a retry loop with exponential backoff so that both normal EOF and errors trigger automatic reconnection. --- ui/desktop/src/hooks/useSessionEvents.ts | 66 +++++++++++++++--------- 1 file changed, 41 insertions(+), 25 deletions(-) diff --git a/ui/desktop/src/hooks/useSessionEvents.ts b/ui/desktop/src/hooks/useSessionEvents.ts index f6906d66152e..2e82b71bf017 100644 --- a/ui/desktop/src/hooks/useSessionEvents.ts +++ b/ui/desktop/src/hooks/useSessionEvents.ts @@ -25,39 +25,55 @@ export function useSessionEvents(sessionId: string) { abortRef.current = abortController; (async () => { - try { - const { stream } = await sessionEvents({ - path: { id: sessionId }, - signal: abortController.signal, - }); + let retryDelay = 500; + const MAX_RETRY_DELAY = 10_000; - setConnected(true); + while (!abortController.signal.aborted) { + try { + const { stream } = await sessionEvents({ + path: { id: sessionId }, + signal: abortController.signal, + }); - for await (const event of stream) { - if (abortController.signal.aborted) break; + setConnected(true); + retryDelay = 500; // reset on successful connection + + for await (const event of stream) { + if (abortController.signal.aborted) break; + + // The server adds chat_request_id (the chat UUID) and request_id + // to the JSON at the SSE framing layer. Route using chat_request_id + // so that Notification events (which carry their own MCP tool-call + // request_id) still reach the correct handler. + const sessionEvent = event as SessionEvent; + const routingId = sessionEvent.chat_request_id ?? sessionEvent.request_id; - // The server adds chat_request_id (the chat UUID) and request_id - // to the JSON at the SSE framing layer. Route using chat_request_id - // so that Notification events (which carry their own MCP tool-call - // request_id) still reach the correct handler. - const sessionEvent = event as SessionEvent; - const routingId = sessionEvent.chat_request_id ?? sessionEvent.request_id; - - if (routingId) { - const handlers = listenersRef.current.get(routingId); - if (handlers) { - for (const handler of handlers) { - handler(sessionEvent); + if (routingId) { + const handlers = listenersRef.current.get(routingId); + if (handlers) { + for (const handler of handlers) { + handler(sessionEvent); + } } } } + + // Stream ended normally (e.g. server closed for lagged subscriber). + // Reconnect unless we were intentionally aborted. + if (abortController.signal.aborted) break; + setConnected(false); + } catch (error) { + if (abortController.signal.aborted) break; + console.warn('SSE connection error, reconnecting:', error); + setConnected(false); + + // Back off before retrying + await new Promise((r) => setTimeout(r, retryDelay)); + retryDelay = Math.min(retryDelay * 2, MAX_RETRY_DELAY); } - } catch (error) { - if (abortController.signal.aborted) return; - console.warn('SSE connection ended:', error); - } finally { - setConnected(false); } + + setConnected(false); })(); return () => { From 6868e073c9c432ab83918af201b96dd9f68dbddc Mon Sep 17 00:00:00 2001 From: jh-block Date: Tue, 17 Mar 2026 11:29:09 +0000 Subject: [PATCH 15/30] fix: preserve Last-Event-ID across SSE reconnects Track the last event ID via the onSseEvent callback and pass it as a Last-Event-ID header when reconnecting. This lets the server replay only the events missed since the last received ID, preventing duplicate Message/Notification/Finish events after a reconnect. --- ui/desktop/src/hooks/useSessionEvents.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ui/desktop/src/hooks/useSessionEvents.ts b/ui/desktop/src/hooks/useSessionEvents.ts index 2e82b71bf017..780e183f4d71 100644 --- a/ui/desktop/src/hooks/useSessionEvents.ts +++ b/ui/desktop/src/hooks/useSessionEvents.ts @@ -27,12 +27,19 @@ export function useSessionEvents(sessionId: string) { (async () => { let retryDelay = 500; const MAX_RETRY_DELAY = 10_000; + let lastEventId: string | undefined; while (!abortController.signal.aborted) { try { const { stream } = await sessionEvents({ path: { id: sessionId }, signal: abortController.signal, + headers: lastEventId ? { 'Last-Event-ID': lastEventId } : undefined, + onSseEvent: (event) => { + if (event.id) { + lastEventId = event.id; + } + }, }); setConnected(true); From d0afb53cfb98546b613634cea071f47a0d3ebecb Mon Sep 17 00:00:00 2001 From: jh-block Date: Tue, 17 Mar 2026 11:52:17 +0000 Subject: [PATCH 16/30] fix: clear request listeners on session change Clear listenersRef when the sessionId changes and the old SSE stream is torn down. Previously stale handlers from the prior session were never removed, leaking closures with captured message state. --- ui/desktop/src/hooks/useSessionEvents.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/desktop/src/hooks/useSessionEvents.ts b/ui/desktop/src/hooks/useSessionEvents.ts index 780e183f4d71..863890d2e303 100644 --- a/ui/desktop/src/hooks/useSessionEvents.ts +++ b/ui/desktop/src/hooks/useSessionEvents.ts @@ -86,6 +86,7 @@ export function useSessionEvents(sessionId: string) { return () => { abortController.abort(); abortRef.current = null; + listenersRef.current.clear(); setConnected(false); }; }, [sessionId]); From 734d486e339721be6d1ce88174803383bda3795b Mon Sep 17 00:00:00 2001 From: jh-block Date: Tue, 17 Mar 2026 13:08:13 +0100 Subject: [PATCH 17/30] fix: reject cancel/events requests for unknown sessions session_cancel now uses get_event_bus (lookup-only) instead of get_or_create_event_bus, returning 404 for unknown session IDs. session_events validates the session exists via session_manager before creating a bus. This prevents unbounded growth of the session_buses map from requests against nonexistent sessions. --- .../goose-server/src/routes/session_events.rs | 17 ++++++++++++++--- crates/goose-server/src/state.rs | 6 ++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/crates/goose-server/src/routes/session_events.rs b/crates/goose-server/src/routes/session_events.rs index 6e7afd82a915..6f7dba8943a8 100644 --- a/crates/goose-server/src/routes/session_events.rs +++ b/crates/goose-server/src/routes/session_events.rs @@ -127,13 +127,21 @@ fn serialize_session_event(seq: u64, request_id: Option<&str>, event: &MessageEv (status = 200, description = "SSE event stream", body = MessageEvent, content_type = "text/event-stream"), + (status = 404, description = "Session not found"), ) )] pub async fn session_events( State(state): State>, Path(session_id): Path, headers: HeaderMap, -) -> SseEventStream { +) -> Result { + // Validate the session exists before creating an event bus. + state + .session_manager() + .get_session(&session_id, false) + .await + .map_err(|_| axum::http::StatusCode::NOT_FOUND)?; + let last_event_id: Option = headers .get("Last-Event-ID") .and_then(|v| v.to_str().ok()) @@ -204,7 +212,7 @@ pub async fn session_events( } }); - SseEventStream::new(stream) + Ok(SseEventStream::new(stream)) } // ── POST /sessions/{id}/reply ─────────────────────────────────────────── @@ -539,7 +547,10 @@ pub async fn session_cancel( Path(session_id): Path, Json(request): Json, ) -> axum::http::StatusCode { - let bus = state.get_or_create_event_bus(&session_id).await; + let bus = match state.get_event_bus(&session_id).await { + Some(bus) => bus, + None => return axum::http::StatusCode::NOT_FOUND, + }; bus.cancel_request(&request.request_id).await; axum::http::StatusCode::OK } diff --git a/crates/goose-server/src/state.rs b/crates/goose-server/src/state.rs index 170f150fda31..09d903c9521d 100644 --- a/crates/goose-server/src/state.rs +++ b/crates/goose-server/src/state.rs @@ -118,6 +118,12 @@ impl AppState { .clone() } + /// Get an existing event bus for a session without creating one. + pub async fn get_event_bus(&self, session_id: &str) -> Option> { + let buses = self.session_buses.lock().await; + buses.get(session_id).cloned() + } + /// Remove the event bus for a session, freeing its replay buffer. pub async fn remove_event_bus(&self, session_id: &str) { let mut buses = self.session_buses.lock().await; From f1d689c6b9418a1e6f4ee9e557c4e0d43c850b47 Mon Sep 17 00:00:00 2001 From: jh-block Date: Tue, 17 Mar 2026 15:40:05 +0100 Subject: [PATCH 18/30] fix: validate session exists before queuing reply work session_reply now checks session existence synchronously via session_manager before creating a bus or registering a request. Returns 404 for unknown/deleted session IDs instead of silently allocating resources that can never be cleaned up. --- crates/goose-server/src/routes/session_events.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/goose-server/src/routes/session_events.rs b/crates/goose-server/src/routes/session_events.rs index 6f7dba8943a8..14a2344e61a5 100644 --- a/crates/goose-server/src/routes/session_events.rs +++ b/crates/goose-server/src/routes/session_events.rs @@ -228,6 +228,7 @@ pub async fn session_events( (status = 200, description = "Request accepted", body = SessionReplyResponse), (status = 400, description = "Invalid request"), + (status = 404, description = "Session not found"), (status = 424, description = "Agent not initialized"), (status = 500, description = "Internal server error"), ) @@ -246,6 +247,13 @@ pub async fn session_reply( )); } + // Validate session exists before allocating a bus/registering work + state + .session_manager() + .get_session(&session_id, false) + .await + .map_err(|_| ErrorResponse::not_found(&format!("Session {} not found", session_id)))?; + let session_start = std::time::Instant::now(); tracing::info!( From 1592f9f4f31944a231a3343c0f8b7cdc13c9062e Mon Sep 17 00:00:00 2001 From: jh-block Date: Tue, 17 Mar 2026 17:20:36 +0100 Subject: [PATCH 19/30] fix: address CI lint failures - Remove needless borrow in session_events.rs (clippy) - Copy listenersRef.current to local var in useSessionEvents cleanup (react-hooks/exhaustive-deps) - Regenerate OpenAPI schema and TS client for session_events 404 response --- crates/goose-server/src/routes/session_events.rs | 2 +- ui/desktop/openapi.json | 6 ++++++ ui/desktop/src/api/index.ts | 2 +- ui/desktop/src/api/sdk.gen.ts | 4 ++-- ui/desktop/src/api/types.gen.ts | 11 +++++++++++ ui/desktop/src/hooks/useSessionEvents.ts | 3 ++- 6 files changed, 23 insertions(+), 5 deletions(-) diff --git a/crates/goose-server/src/routes/session_events.rs b/crates/goose-server/src/routes/session_events.rs index 14a2344e61a5..8b49a3454a21 100644 --- a/crates/goose-server/src/routes/session_events.rs +++ b/crates/goose-server/src/routes/session_events.rs @@ -252,7 +252,7 @@ pub async fn session_reply( .session_manager() .get_session(&session_id, false) .await - .map_err(|_| ErrorResponse::not_found(&format!("Session {} not found", session_id)))?; + .map_err(|_| ErrorResponse::not_found(format!("Session {} not found", session_id)))?; let session_start = std::time::Instant::now(); diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 5cdf33acf81e..ba91f7f91e3d 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -3308,6 +3308,9 @@ } } } + }, + "404": { + "description": "Session not found" } } } @@ -3353,6 +3356,9 @@ "400": { "description": "Invalid request" }, + "404": { + "description": "Session not found" + }, "424": { "description": "Agent not initialized" }, diff --git a/ui/desktop/src/api/index.ts b/ui/desktop/src/api/index.ts index a10c6b7cff4b..305d052bb4b4 100644 --- a/ui/desktop/src/api/index.ts +++ b/ui/desktop/src/api/index.ts @@ -1,4 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts export { addExtension, agentAddExtension, agentRemoveExtension, backupConfig, callTool, cancelDownload, cancelLocalModelDownload, checkProvider, configureProviderOauth, confirmToolAction, createCustomProvider, createRecipe, createSchedule, decodeRecipe, deleteLocalModel, deleteModel, deleteRecipe, deleteSchedule, deleteSession, detectProvider, diagnostics, downloadHfModel, downloadModel, encodeRecipe, exportApp, exportSession, forkSession, getCanonicalModelInfo, getCustomProvider, getDictationConfig, getDownloadProgress, getExtensions, getLocalModelDownloadProgress, getModelSettings, getPrompt, getPrompts, getProviderCatalog, getProviderCatalogTemplate, getProviderModels, getRepoFiles, getSession, getSessionExtensions, getSessionInsights, getSlashCommands, getTools, getTunnelStatus, importApp, importSession, initConfig, inspectRunningJob, killRunningJob, listApps, listLocalModels, listModels, listRecipes, listSchedules, listSessions, mcpUiProxy, type Options, parseRecipe, pauseSchedule, providers, readAllConfig, readConfig, readResource, recipeToYaml, recoverConfig, removeConfig, removeCustomProvider, removeExtension, reply, resetPrompt, restartAgent, resumeAgent, runNowHandler, savePrompt, saveRecipe, scanRecipe, scheduleRecipe, searchHfModels, searchSessions, sendTelemetryEvent, sessionCancel, sessionEvents, sessionReply, sessionsHandler, setConfigProvider, setRecipeSlashCommand, startAgent, startNanogptSetup, startOpenrouterSetup, startTetrateSetup, startTunnel, status, stopAgent, stopTunnel, systemInfo, transcribeDictation, unpauseSchedule, updateAgentProvider, updateCustomProvider, updateFromSession, updateModelSettings, updateSchedule, updateSession, updateSessionName, updateSessionUserRecipeValues, updateWorkingDir, upsertConfig, upsertPermissions, validateConfig } from './sdk.gen'; -export type { ActionRequired, ActionRequiredData, AddExtensionData, AddExtensionErrors, AddExtensionRequest, AddExtensionResponse, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponse, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponse, AgentRemoveExtensionResponses, Annotations, Author, AuthorRequest, BackupConfigData, BackupConfigErrors, BackupConfigResponse, BackupConfigResponses, CallToolData, CallToolErrors, CallToolRequest, CallToolResponse, CallToolResponse2, CallToolResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelLocalModelDownloadData, CancelLocalModelDownloadErrors, CancelLocalModelDownloadResponses, CancelRequest, ChatRequest, CheckProviderData, CheckProviderRequest, ClientOptions, CommandType, ConfigKey, ConfigKeyQuery, ConfigResponse, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionRequest, ConfirmToolActionResponses, Content, ContentBlock, Conversation, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponse, CreateCustomProviderResponse2, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeRequest, CreateRecipeResponse, CreateRecipeResponse2, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleRequest, CreateScheduleResponse, CreateScheduleResponses, CspMetadata, DeclarativeProviderConfig, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeRequest, DecodeRecipeResponse, DecodeRecipeResponse2, DecodeRecipeResponses, DeleteLocalModelData, DeleteLocalModelErrors, DeleteLocalModelResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeRequest, DeleteRecipeResponse, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponse, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderRequest, DetectProviderResponse, DetectProviderResponse2, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponse, DiagnosticsResponses, DictationProvider, DictationProviderStatus, DownloadHfModelData, DownloadHfModelErrors, DownloadHfModelResponse, DownloadHfModelResponses, DownloadModelData, DownloadModelErrors, DownloadModelRequest, DownloadModelResponses, DownloadProgress, DownloadStatus, EmbeddedResource, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeRequest, EncodeRecipeResponse, EncodeRecipeResponse2, EncodeRecipeResponses, Envs, EnvVarConfig, ErrorResponse, ExportAppData, ExportAppError, ExportAppErrors, ExportAppResponse, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponse, ExportSessionResponses, ExtensionConfig, ExtensionData, ExtensionEntry, ExtensionLoadResult, ExtensionQuery, ExtensionResponse, ForkRequest, ForkResponse, ForkSessionData, ForkSessionErrors, ForkSessionResponse, ForkSessionResponses, FrontendToolRequest, GetCanonicalModelInfoData, GetCanonicalModelInfoResponse, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponse, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponse, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponse, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponse, GetExtensionsResponses, GetLocalModelDownloadProgressData, GetLocalModelDownloadProgressErrors, GetLocalModelDownloadProgressResponse, GetLocalModelDownloadProgressResponses, GetModelSettingsData, GetModelSettingsErrors, GetModelSettingsResponse, GetModelSettingsResponses, GetPromptData, GetPromptErrors, GetPromptResponse, GetPromptResponses, GetPromptsData, GetPromptsResponse, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponse, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponse, GetProviderCatalogTemplateResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponse, GetProviderModelsResponses, GetRepoFilesData, GetRepoFilesResponse, GetRepoFilesResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponse, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponse, GetSessionInsightsResponses, GetSessionResponse, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponse, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsQuery, GetToolsResponse, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponse, GetTunnelStatusResponses, GooseApp, GooseMode, HfGgufFile, HfModelInfo, HfQuantVariant, Icon, ImageContent, ImportAppData, ImportAppError, ImportAppErrors, ImportAppRequest, ImportAppResponse, ImportAppResponse2, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionRequest, ImportSessionResponse, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponse, InitConfigResponses, InspectJobResponse, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponse, InspectRunningJobResponses, JsonObject, KillJobResponse, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsError, ListAppsErrors, ListAppsRequest, ListAppsResponse, ListAppsResponse2, ListAppsResponses, ListLocalModelsData, ListLocalModelsResponse, ListLocalModelsResponses, ListModelsData, ListModelsResponse, ListModelsResponses, ListRecipeResponse, ListRecipesData, ListRecipesErrors, ListRecipesResponse, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponse, ListSchedulesResponse2, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponse, ListSessionsResponses, LoadedProvider, LocalModelResponse, McpAppResource, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, Message, MessageContent, MessageEvent, MessageMetadata, ModelCapabilities, ModelConfig, ModelDownloadStatus, ModelInfo, ModelInfoData, ModelInfoQuery, ModelInfoResponse, ModelSettings, ModelTemplate, ParseRecipeData, ParseRecipeError, ParseRecipeErrors, ParseRecipeRequest, ParseRecipeResponse, ParseRecipeResponse2, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponse, PauseScheduleResponses, Permission, PermissionLevel, PermissionsMetadata, PrincipalType, PromptContentResponse, PromptsListResponse, ProviderCatalogEntry, ProviderDetails, ProviderEngine, ProviderMetadata, ProvidersData, ProvidersResponse, ProvidersResponse2, ProvidersResponses, ProviderTemplate, ProviderType, RawAudioContent, RawEmbeddedResource, RawImageContent, RawResource, RawTextContent, ReadAllConfigData, ReadAllConfigResponse, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceRequest, ReadResourceResponse, ReadResourceResponse2, ReadResourceResponses, Recipe, RecipeManifest, RecipeParameter, RecipeParameterInputType, RecipeParameterRequirement, RecipeToYamlData, RecipeToYamlError, RecipeToYamlErrors, RecipeToYamlRequest, RecipeToYamlResponse, RecipeToYamlResponse2, RecipeToYamlResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponse, RecoverConfigResponses, RedactedThinkingContent, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponse, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponse, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionRequest, RemoveExtensionResponse, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponse, ReplyResponses, RepoVariantsResponse, ResetPromptData, ResetPromptErrors, ResetPromptResponse, ResetPromptResponses, ResourceContents, ResourceMetadata, Response, RestartAgentData, RestartAgentErrors, RestartAgentRequest, RestartAgentResponse, RestartAgentResponse2, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentRequest, ResumeAgentResponse, ResumeAgentResponse2, ResumeAgentResponses, RetryConfig, Role, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponse, RunNowHandlerResponses, RunNowResponse, SamplingConfig, SavePromptData, SavePromptErrors, SavePromptRequest, SavePromptResponse, SavePromptResponses, SaveRecipeData, SaveRecipeError, SaveRecipeErrors, SaveRecipeRequest, SaveRecipeResponse, SaveRecipeResponse2, SaveRecipeResponses, ScanRecipeData, ScanRecipeRequest, ScanRecipeResponse, ScanRecipeResponse2, ScanRecipeResponses, ScheduledJob, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeRequest, ScheduleRecipeResponses, SearchHfModelsData, SearchHfModelsErrors, SearchHfModelsResponse, SearchHfModelsResponses, SearchSessionsData, SearchSessionsErrors, SearchSessionsResponse, SearchSessionsResponses, SendTelemetryEventData, SendTelemetryEventResponses, Session, SessionCancelData, SessionCancelResponses, SessionDisplayInfo, SessionEventsData, SessionEventsResponse, SessionEventsResponses, SessionExtensionsResponse, SessionInsights, SessionListResponse, SessionReplyData, SessionReplyErrors, SessionReplyRequest, SessionReplyResponse, SessionReplyResponse2, SessionReplyResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponse, SessionsHandlerResponses, SessionsQuery, SessionType, SetConfigProviderData, SetProviderRequest, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, SetSlashCommandRequest, Settings, SetupResponse, SlashCommand, SlashCommandsResponse, StartAgentData, StartAgentError, StartAgentErrors, StartAgentRequest, StartAgentResponse, StartAgentResponses, StartNanogptSetupData, StartNanogptSetupResponse, StartNanogptSetupResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponse, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponse, StartTetrateSetupResponses, StartTunnelData, StartTunnelError, StartTunnelErrors, StartTunnelResponse, StartTunnelResponses, StatusData, StatusResponse, StatusResponses, StopAgentData, StopAgentErrors, StopAgentRequest, StopAgentResponse, StopAgentResponses, StopTunnelData, StopTunnelError, StopTunnelErrors, StopTunnelResponses, SubRecipe, SuccessCheck, SystemInfo, SystemInfoData, SystemInfoResponse, SystemInfoResponses, SystemNotificationContent, SystemNotificationType, TaskSupport, TelemetryEventRequest, Template, TextContent, ThinkingContent, TokenState, Tool, ToolAnnotations, ToolConfirmationRequest, ToolExecution, ToolInfo, ToolPermission, ToolRequest, ToolResponse, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponse, TranscribeDictationResponses, TranscribeRequest, TranscribeResponse, TunnelInfo, TunnelState, UiMetadata, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponse, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderRequest, UpdateCustomProviderResponse, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionRequest, UpdateFromSessionResponses, UpdateModelSettingsData, UpdateModelSettingsErrors, UpdateModelSettingsResponse, UpdateModelSettingsResponses, UpdateProviderRequest, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleRequest, UpdateScheduleResponse, UpdateScheduleResponses, UpdateSessionData, UpdateSessionErrors, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameRequest, UpdateSessionNameResponses, UpdateSessionRequest, UpdateSessionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesError, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesRequest, UpdateSessionUserRecipeValuesResponse, UpdateSessionUserRecipeValuesResponse2, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirRequest, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigQuery, UpsertConfigResponse, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsQuery, UpsertPermissionsResponse, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponse, ValidateConfigResponses, WhisperModelResponse, WindowProps } from './types.gen'; +export type { ActionRequired, ActionRequiredData, AddExtensionData, AddExtensionErrors, AddExtensionRequest, AddExtensionResponse, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponse, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponse, AgentRemoveExtensionResponses, Annotations, Author, AuthorRequest, BackupConfigData, BackupConfigErrors, BackupConfigResponse, BackupConfigResponses, CallToolData, CallToolErrors, CallToolRequest, CallToolResponse, CallToolResponse2, CallToolResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelLocalModelDownloadData, CancelLocalModelDownloadErrors, CancelLocalModelDownloadResponses, CancelRequest, ChatRequest, CheckProviderData, CheckProviderRequest, ClientOptions, CommandType, ConfigKey, ConfigKeyQuery, ConfigResponse, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionRequest, ConfirmToolActionResponses, Content, ContentBlock, Conversation, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponse, CreateCustomProviderResponse2, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeRequest, CreateRecipeResponse, CreateRecipeResponse2, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleRequest, CreateScheduleResponse, CreateScheduleResponses, CspMetadata, DeclarativeProviderConfig, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeRequest, DecodeRecipeResponse, DecodeRecipeResponse2, DecodeRecipeResponses, DeleteLocalModelData, DeleteLocalModelErrors, DeleteLocalModelResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeRequest, DeleteRecipeResponse, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponse, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderRequest, DetectProviderResponse, DetectProviderResponse2, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponse, DiagnosticsResponses, DictationProvider, DictationProviderStatus, DownloadHfModelData, DownloadHfModelErrors, DownloadHfModelResponse, DownloadHfModelResponses, DownloadModelData, DownloadModelErrors, DownloadModelRequest, DownloadModelResponses, DownloadProgress, DownloadStatus, EmbeddedResource, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeRequest, EncodeRecipeResponse, EncodeRecipeResponse2, EncodeRecipeResponses, Envs, EnvVarConfig, ErrorResponse, ExportAppData, ExportAppError, ExportAppErrors, ExportAppResponse, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponse, ExportSessionResponses, ExtensionConfig, ExtensionData, ExtensionEntry, ExtensionLoadResult, ExtensionQuery, ExtensionResponse, ForkRequest, ForkResponse, ForkSessionData, ForkSessionErrors, ForkSessionResponse, ForkSessionResponses, FrontendToolRequest, GetCanonicalModelInfoData, GetCanonicalModelInfoResponse, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponse, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponse, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponse, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponse, GetExtensionsResponses, GetLocalModelDownloadProgressData, GetLocalModelDownloadProgressErrors, GetLocalModelDownloadProgressResponse, GetLocalModelDownloadProgressResponses, GetModelSettingsData, GetModelSettingsErrors, GetModelSettingsResponse, GetModelSettingsResponses, GetPromptData, GetPromptErrors, GetPromptResponse, GetPromptResponses, GetPromptsData, GetPromptsResponse, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponse, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponse, GetProviderCatalogTemplateResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponse, GetProviderModelsResponses, GetRepoFilesData, GetRepoFilesResponse, GetRepoFilesResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponse, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponse, GetSessionInsightsResponses, GetSessionResponse, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponse, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsQuery, GetToolsResponse, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponse, GetTunnelStatusResponses, GooseApp, GooseMode, HfGgufFile, HfModelInfo, HfQuantVariant, Icon, ImageContent, ImportAppData, ImportAppError, ImportAppErrors, ImportAppRequest, ImportAppResponse, ImportAppResponse2, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionRequest, ImportSessionResponse, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponse, InitConfigResponses, InspectJobResponse, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponse, InspectRunningJobResponses, JsonObject, KillJobResponse, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsError, ListAppsErrors, ListAppsRequest, ListAppsResponse, ListAppsResponse2, ListAppsResponses, ListLocalModelsData, ListLocalModelsResponse, ListLocalModelsResponses, ListModelsData, ListModelsResponse, ListModelsResponses, ListRecipeResponse, ListRecipesData, ListRecipesErrors, ListRecipesResponse, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponse, ListSchedulesResponse2, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponse, ListSessionsResponses, LoadedProvider, LocalModelResponse, McpAppResource, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, Message, MessageContent, MessageEvent, MessageMetadata, ModelCapabilities, ModelConfig, ModelDownloadStatus, ModelInfo, ModelInfoData, ModelInfoQuery, ModelInfoResponse, ModelSettings, ModelTemplate, ParseRecipeData, ParseRecipeError, ParseRecipeErrors, ParseRecipeRequest, ParseRecipeResponse, ParseRecipeResponse2, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponse, PauseScheduleResponses, Permission, PermissionLevel, PermissionsMetadata, PrincipalType, PromptContentResponse, PromptsListResponse, ProviderCatalogEntry, ProviderDetails, ProviderEngine, ProviderMetadata, ProvidersData, ProvidersResponse, ProvidersResponse2, ProvidersResponses, ProviderTemplate, ProviderType, RawAudioContent, RawEmbeddedResource, RawImageContent, RawResource, RawTextContent, ReadAllConfigData, ReadAllConfigResponse, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceRequest, ReadResourceResponse, ReadResourceResponse2, ReadResourceResponses, Recipe, RecipeManifest, RecipeParameter, RecipeParameterInputType, RecipeParameterRequirement, RecipeToYamlData, RecipeToYamlError, RecipeToYamlErrors, RecipeToYamlRequest, RecipeToYamlResponse, RecipeToYamlResponse2, RecipeToYamlResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponse, RecoverConfigResponses, RedactedThinkingContent, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponse, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponse, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionRequest, RemoveExtensionResponse, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponse, ReplyResponses, RepoVariantsResponse, ResetPromptData, ResetPromptErrors, ResetPromptResponse, ResetPromptResponses, ResourceContents, ResourceMetadata, Response, RestartAgentData, RestartAgentErrors, RestartAgentRequest, RestartAgentResponse, RestartAgentResponse2, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentRequest, ResumeAgentResponse, ResumeAgentResponse2, ResumeAgentResponses, RetryConfig, Role, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponse, RunNowHandlerResponses, RunNowResponse, SamplingConfig, SavePromptData, SavePromptErrors, SavePromptRequest, SavePromptResponse, SavePromptResponses, SaveRecipeData, SaveRecipeError, SaveRecipeErrors, SaveRecipeRequest, SaveRecipeResponse, SaveRecipeResponse2, SaveRecipeResponses, ScanRecipeData, ScanRecipeRequest, ScanRecipeResponse, ScanRecipeResponse2, ScanRecipeResponses, ScheduledJob, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeRequest, ScheduleRecipeResponses, SearchHfModelsData, SearchHfModelsErrors, SearchHfModelsResponse, SearchHfModelsResponses, SearchSessionsData, SearchSessionsErrors, SearchSessionsResponse, SearchSessionsResponses, SendTelemetryEventData, SendTelemetryEventResponses, Session, SessionCancelData, SessionCancelResponses, SessionDisplayInfo, SessionEventsData, SessionEventsErrors, SessionEventsResponse, SessionEventsResponses, SessionExtensionsResponse, SessionInsights, SessionListResponse, SessionReplyData, SessionReplyErrors, SessionReplyRequest, SessionReplyResponse, SessionReplyResponse2, SessionReplyResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponse, SessionsHandlerResponses, SessionsQuery, SessionType, SetConfigProviderData, SetProviderRequest, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, SetSlashCommandRequest, Settings, SetupResponse, SlashCommand, SlashCommandsResponse, StartAgentData, StartAgentError, StartAgentErrors, StartAgentRequest, StartAgentResponse, StartAgentResponses, StartNanogptSetupData, StartNanogptSetupResponse, StartNanogptSetupResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponse, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponse, StartTetrateSetupResponses, StartTunnelData, StartTunnelError, StartTunnelErrors, StartTunnelResponse, StartTunnelResponses, StatusData, StatusResponse, StatusResponses, StopAgentData, StopAgentErrors, StopAgentRequest, StopAgentResponse, StopAgentResponses, StopTunnelData, StopTunnelError, StopTunnelErrors, StopTunnelResponses, SubRecipe, SuccessCheck, SystemInfo, SystemInfoData, SystemInfoResponse, SystemInfoResponses, SystemNotificationContent, SystemNotificationType, TaskSupport, TelemetryEventRequest, Template, TextContent, ThinkingContent, TokenState, Tool, ToolAnnotations, ToolConfirmationRequest, ToolExecution, ToolInfo, ToolPermission, ToolRequest, ToolResponse, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponse, TranscribeDictationResponses, TranscribeRequest, TranscribeResponse, TunnelInfo, TunnelState, UiMetadata, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponse, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderRequest, UpdateCustomProviderResponse, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionRequest, UpdateFromSessionResponses, UpdateModelSettingsData, UpdateModelSettingsErrors, UpdateModelSettingsResponse, UpdateModelSettingsResponses, UpdateProviderRequest, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleRequest, UpdateScheduleResponse, UpdateScheduleResponses, UpdateSessionData, UpdateSessionErrors, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameRequest, UpdateSessionNameResponses, UpdateSessionRequest, UpdateSessionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesError, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesRequest, UpdateSessionUserRecipeValuesResponse, UpdateSessionUserRecipeValuesResponse2, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirRequest, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigQuery, UpsertConfigResponse, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsQuery, UpsertPermissionsResponse, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponse, ValidateConfigResponses, WhisperModelResponse, WindowProps } from './types.gen'; diff --git a/ui/desktop/src/api/sdk.gen.ts b/ui/desktop/src/api/sdk.gen.ts index c4056e976edd..10a50e37875b 100644 --- a/ui/desktop/src/api/sdk.gen.ts +++ b/ui/desktop/src/api/sdk.gen.ts @@ -2,7 +2,7 @@ import type { Client, Options as Options2, TDataShape } from './client'; import { client } from './client.gen'; -import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CallToolData, CallToolErrors, CallToolResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelLocalModelDownloadData, CancelLocalModelDownloadErrors, CancelLocalModelDownloadResponses, CheckProviderData, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteLocalModelData, DeleteLocalModelErrors, DeleteLocalModelResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, DownloadHfModelData, DownloadHfModelErrors, DownloadHfModelResponses, DownloadModelData, DownloadModelErrors, DownloadModelResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportAppData, ExportAppErrors, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, ForkSessionData, ForkSessionErrors, ForkSessionResponses, GetCanonicalModelInfoData, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetLocalModelDownloadProgressData, GetLocalModelDownloadProgressErrors, GetLocalModelDownloadProgressResponses, GetModelSettingsData, GetModelSettingsErrors, GetModelSettingsResponses, GetPromptData, GetPromptErrors, GetPromptResponses, GetPromptsData, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetRepoFilesData, GetRepoFilesResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportAppData, ImportAppErrors, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsErrors, ListAppsResponses, ListLocalModelsData, ListLocalModelsResponses, ListModelsData, ListModelsResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceResponses, RecipeToYamlData, RecipeToYamlErrors, RecipeToYamlResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResetPromptData, ResetPromptErrors, ResetPromptResponses, RestartAgentData, RestartAgentErrors, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SavePromptData, SavePromptErrors, SavePromptResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SearchHfModelsData, SearchHfModelsErrors, SearchHfModelsResponses, SearchSessionsData, SearchSessionsErrors, SearchSessionsResponses, SendTelemetryEventData, SendTelemetryEventResponses, SessionCancelData, SessionCancelResponses, SessionEventsData, SessionEventsResponses, SessionReplyData, SessionReplyErrors, SessionReplyResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartNanogptSetupData, StartNanogptSetupResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopAgentData, StopAgentErrors, StopAgentResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, SystemInfoData, SystemInfoResponses, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateModelSettingsData, UpdateModelSettingsErrors, UpdateModelSettingsResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionData, UpdateSessionErrors, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; +import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CallToolData, CallToolErrors, CallToolResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelLocalModelDownloadData, CancelLocalModelDownloadErrors, CancelLocalModelDownloadResponses, CheckProviderData, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteLocalModelData, DeleteLocalModelErrors, DeleteLocalModelResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, DownloadHfModelData, DownloadHfModelErrors, DownloadHfModelResponses, DownloadModelData, DownloadModelErrors, DownloadModelResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportAppData, ExportAppErrors, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, ForkSessionData, ForkSessionErrors, ForkSessionResponses, GetCanonicalModelInfoData, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetLocalModelDownloadProgressData, GetLocalModelDownloadProgressErrors, GetLocalModelDownloadProgressResponses, GetModelSettingsData, GetModelSettingsErrors, GetModelSettingsResponses, GetPromptData, GetPromptErrors, GetPromptResponses, GetPromptsData, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetRepoFilesData, GetRepoFilesResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportAppData, ImportAppErrors, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsErrors, ListAppsResponses, ListLocalModelsData, ListLocalModelsResponses, ListModelsData, ListModelsResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceResponses, RecipeToYamlData, RecipeToYamlErrors, RecipeToYamlResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResetPromptData, ResetPromptErrors, ResetPromptResponses, RestartAgentData, RestartAgentErrors, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SavePromptData, SavePromptErrors, SavePromptResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SearchHfModelsData, SearchHfModelsErrors, SearchHfModelsResponses, SearchSessionsData, SearchSessionsErrors, SearchSessionsResponses, SendTelemetryEventData, SendTelemetryEventResponses, SessionCancelData, SessionCancelResponses, SessionEventsData, SessionEventsErrors, SessionEventsResponses, SessionReplyData, SessionReplyErrors, SessionReplyResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartNanogptSetupData, StartNanogptSetupResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopAgentData, StopAgentErrors, StopAgentResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, SystemInfoData, SystemInfoResponses, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateModelSettingsData, UpdateModelSettingsErrors, UpdateModelSettingsResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionData, UpdateSessionErrors, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; export type Options = Options2 & { /** @@ -516,7 +516,7 @@ export const sessionCancel = (options: Opt } }); -export const sessionEvents = (options: Options) => (options.client ?? client).sse.get({ url: '/sessions/{id}/events', ...options }); +export const sessionEvents = (options: Options) => (options.client ?? client).sse.get({ url: '/sessions/{id}/events', ...options }); export const sessionReply = (options: Options) => (options.client ?? client).post({ url: '/sessions/{id}/reply', diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index 4031317c99ab..4b6a1eb0eeea 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -4151,6 +4151,13 @@ export type SessionEventsData = { url: '/sessions/{id}/events'; }; +export type SessionEventsErrors = { + /** + * Session not found + */ + 404: unknown; +}; + export type SessionEventsResponses = { /** * SSE event stream @@ -4177,6 +4184,10 @@ export type SessionReplyErrors = { * Invalid request */ 400: unknown; + /** + * Session not found + */ + 404: unknown; /** * Agent not initialized */ diff --git a/ui/desktop/src/hooks/useSessionEvents.ts b/ui/desktop/src/hooks/useSessionEvents.ts index 863890d2e303..4598b95e6872 100644 --- a/ui/desktop/src/hooks/useSessionEvents.ts +++ b/ui/desktop/src/hooks/useSessionEvents.ts @@ -83,10 +83,11 @@ export function useSessionEvents(sessionId: string) { setConnected(false); })(); + const listeners = listenersRef.current; return () => { abortController.abort(); abortRef.current = null; - listenersRef.current.clear(); + listeners.clear(); setConnected(false); }; }, [sessionId]); From 06158088d3804c39c53be6421b611191aae5fe04 Mon Sep 17 00:00:00 2001 From: jh-block Date: Wed, 18 Mar 2026 09:43:25 +0100 Subject: [PATCH 20/30] fix: cancel in-flight replies on session delete and cap SSE retries - Add cancel_all_requests() to SessionEventBus and call it in delete_session before removing the bus, so spawned agent tasks stop when a user deletes a streaming session. - Add a consecutive error limit (10) to the SSE reconnect loop in useSessionEvents. When exceeded, synthetic Error events are sent to all active listeners so the UI exits streaming state instead of retrying forever. --- crates/goose-server/src/routes/session.rs | 6 ++++- crates/goose-server/src/session_event_bus.rs | 8 +++++++ ui/desktop/src/hooks/useSessionEvents.ts | 25 +++++++++++++++++++- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/crates/goose-server/src/routes/session.rs b/crates/goose-server/src/routes/session.rs index 562576e0c6e1..b076dbc8bcc1 100644 --- a/crates/goose-server/src/routes/session.rs +++ b/crates/goose-server/src/routes/session.rs @@ -296,7 +296,11 @@ async fn delete_session( } })?; - // Clean up the event bus to free its replay buffer + // Cancel any in-flight replies before dropping the bus, so spawned + // agent tasks stop consuming tokens for a deleted session. + if let Some(bus) = state.get_event_bus(&session_id).await { + bus.cancel_all_requests().await; + } state.remove_event_bus(&session_id).await; Ok(StatusCode::OK) diff --git a/crates/goose-server/src/session_event_bus.rs b/crates/goose-server/src/session_event_bus.rs index 8a96369398ff..cc545c7dac6f 100644 --- a/crates/goose-server/src/session_event_bus.rs +++ b/crates/goose-server/src/session_event_bus.rs @@ -107,6 +107,14 @@ impl SessionEventBus { } } + /// Cancel all active requests (e.g. when deleting a session). + pub async fn cancel_all_requests(&self) { + let requests = self.active_requests.lock().await; + for token in requests.values() { + token.cancel(); + } + } + /// Remove the cancellation token for a completed request. pub async fn cleanup_request(&self, request_id: &str) { let mut requests = self.active_requests.lock().await; diff --git a/ui/desktop/src/hooks/useSessionEvents.ts b/ui/desktop/src/hooks/useSessionEvents.ts index 4598b95e6872..a1a636f47d29 100644 --- a/ui/desktop/src/hooks/useSessionEvents.ts +++ b/ui/desktop/src/hooks/useSessionEvents.ts @@ -27,6 +27,8 @@ export function useSessionEvents(sessionId: string) { (async () => { let retryDelay = 500; const MAX_RETRY_DELAY = 10_000; + const MAX_CONSECUTIVE_ERRORS = 10; + let consecutiveErrors = 0; let lastEventId: string | undefined; while (!abortController.signal.aborted) { @@ -44,6 +46,7 @@ export function useSessionEvents(sessionId: string) { setConnected(true); retryDelay = 500; // reset on successful connection + consecutiveErrors = 0; for await (const event of stream) { if (abortController.signal.aborted) break; @@ -71,9 +74,29 @@ export function useSessionEvents(sessionId: string) { setConnected(false); } catch (error) { if (abortController.signal.aborted) break; - console.warn('SSE connection error, reconnecting:', error); + consecutiveErrors++; + console.warn( + `SSE connection error (${consecutiveErrors}/${MAX_CONSECUTIVE_ERRORS}), reconnecting:`, + error, + ); setConnected(false); + if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) { + console.error('SSE reconnect limit reached, notifying active listeners'); + // Send an error event to all active listeners so they can + // transition out of streaming state. + const errorEvent: SessionEvent = { + type: 'Error', + error: 'Lost connection to server', + } as SessionEvent; + for (const [routingId, handlers] of listenersRef.current) { + for (const handler of handlers) { + handler({ ...errorEvent, request_id: routingId, chat_request_id: routingId }); + } + } + break; + } + // Back off before retrying await new Promise((r) => setTimeout(r, retryDelay)); retryDelay = Math.min(retryDelay * 2, MAX_RETRY_DELAY); From f52cdc7e9aed04b2b89c987a004519747db6974b Mon Sep 17 00:00:00 2001 From: jh-block Date: Wed, 18 Mar 2026 10:05:24 +0100 Subject: [PATCH 21/30] fix: assign event sequence under lock and keep SSE reconnecting - Move sequence ID assignment inside the buffer lock in SessionEventBus::publish so concurrent callers cannot reorder events (seq=2 before seq=1). - After hitting the SSE retry cap, reset the counter and continue reconnecting instead of breaking permanently. The error events still fire to unblock the UI from streaming state, but the connection stays alive for future requests. --- crates/goose-server/src/session_event_bus.rs | 26 +++++++++++--------- ui/desktop/src/hooks/useSessionEvents.ts | 5 ++-- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/crates/goose-server/src/session_event_bus.rs b/crates/goose-server/src/session_event_bus.rs index cc545c7dac6f..4a993997b553 100644 --- a/crates/goose-server/src/session_event_bus.rs +++ b/crates/goose-server/src/session_event_bus.rs @@ -36,27 +36,29 @@ impl SessionEventBus { } /// Publish an event to the bus. Assigns a monotonic sequence number. + /// + /// The sequence ID is assigned under the buffer lock so that concurrent + /// callers cannot reorder events (i.e. seq=2 published before seq=1). pub async fn publish(&self, request_id: Option, event: MessageEvent) -> u64 { - let seq = self.next_seq.fetch_add(1, Ordering::Relaxed); - let session_event = SessionEvent { - seq, - request_id, - event, - }; - - // Append to replay buffer - { + let session_event = { let mut buf = self.buffer.lock().await; + let seq = self.next_seq.fetch_add(1, Ordering::Relaxed); + let session_event = SessionEvent { + seq, + request_id, + event, + }; buf.push_back(session_event.clone()); while buf.len() > REPLAY_BUFFER_CAPACITY { buf.pop_front(); } - } + session_event + }; // Send on broadcast channel (ignore error if no subscribers) - let _ = self.tx.send(session_event); + let _ = self.tx.send(session_event.clone()); - seq + session_event.seq } /// Subscribe to live events. If `last_event_id` is provided, replay buffered diff --git a/ui/desktop/src/hooks/useSessionEvents.ts b/ui/desktop/src/hooks/useSessionEvents.ts index a1a636f47d29..cf9742ba743b 100644 --- a/ui/desktop/src/hooks/useSessionEvents.ts +++ b/ui/desktop/src/hooks/useSessionEvents.ts @@ -84,7 +84,8 @@ export function useSessionEvents(sessionId: string) { if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) { console.error('SSE reconnect limit reached, notifying active listeners'); // Send an error event to all active listeners so they can - // transition out of streaming state. + // transition out of streaming state. Reset the counter so + // the loop keeps reconnecting for future requests. const errorEvent: SessionEvent = { type: 'Error', error: 'Lost connection to server', @@ -94,7 +95,7 @@ export function useSessionEvents(sessionId: string) { handler({ ...errorEvent, request_id: routingId, chat_request_id: routingId }); } } - break; + consecutiveErrors = 0; } // Back off before retrying From 27a52e4bf7f341a708c7be24c25b631bd383dbdc Mon Sep 17 00:00:00 2001 From: jh-block Date: Wed, 18 Mar 2026 10:22:13 +0100 Subject: [PATCH 22/30] fix: set sseMaxRetryAttempts so SSE errors surface to outer loop The generated SSE client retries internally forever unless sseMaxRetryAttempts is set. Pass sseMaxRetryAttempts: 1 so fetch failures immediately surface to our outer reconnect loop, which tracks consecutive errors and fires synthetic Error events to unblock the UI from streaming state. --- ui/desktop/src/hooks/useSessionEvents.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ui/desktop/src/hooks/useSessionEvents.ts b/ui/desktop/src/hooks/useSessionEvents.ts index cf9742ba743b..31e2bc5cb4a1 100644 --- a/ui/desktop/src/hooks/useSessionEvents.ts +++ b/ui/desktop/src/hooks/useSessionEvents.ts @@ -37,6 +37,9 @@ export function useSessionEvents(sessionId: string) { path: { id: sessionId }, signal: abortController.signal, headers: lastEventId ? { 'Last-Event-ID': lastEventId } : undefined, + // Disable the inner retry loop so errors surface to our outer + // loop which tracks consecutive failures and notifies listeners. + sseMaxRetryAttempts: 1, onSseEvent: (event) => { if (event.id) { lastEventId = event.id; From 2688b30cc687dc4e7eab54c58da8b8d745257712 Mon Sep 17 00:00:00 2001 From: jh-block Date: Wed, 18 Mar 2026 11:17:57 +0100 Subject: [PATCH 23/30] fix: detect replay buffer overflow and reload conversation When a client reconnects with a Last-Event-ID that has been evicted from the replay buffer, the server now returns an error event ("Client too far behind") instead of silently skipping missed events. The client detects this, exits streaming state, and reloads the full conversation from the server so the UI reflects the actual state. --- .../goose-server/src/routes/session_events.rs | 20 ++++++++++- crates/goose-server/src/session_event_bus.rs | 36 ++++++++++++++----- ui/desktop/src/hooks/useChatStream.ts | 28 +++++++++++++-- ui/desktop/src/hooks/useSessionEvents.ts | 10 +++++- 4 files changed, 82 insertions(+), 12 deletions(-) diff --git a/crates/goose-server/src/routes/session_events.rs b/crates/goose-server/src/routes/session_events.rs index 8b49a3454a21..bdedbc7f7eb7 100644 --- a/crates/goose-server/src/routes/session_events.rs +++ b/crates/goose-server/src/routes/session_events.rs @@ -148,7 +148,25 @@ pub async fn session_events( .and_then(|s| s.parse().ok()); let bus = state.get_or_create_event_bus(&session_id).await; - let (replay, replay_max_seq, mut live_rx) = bus.subscribe(last_event_id).await; + + let (replay, replay_max_seq, mut live_rx) = match bus.subscribe(last_event_id).await { + Ok(result) => result, + Err(_) => { + // Client's Last-Event-ID has been evicted from the replay buffer. + // Send a single error event so the client knows to reload. + let (tx, rx) = mpsc::channel::(1); + let stream = ReceiverStream::new(rx); + let seq = 0; + let error_event = MessageEvent::Error { + error: "Client too far behind — reload conversation".to_string(), + }; + let frame = serialize_session_event(seq, None, &error_event); + tokio::spawn(async move { + let _ = tx.send(frame).await; + }); + return Ok(SseEventStream::new(stream)); + } + }; let (tx, rx) = mpsc::channel::(256); let stream = ReceiverStream::new(rx); diff --git a/crates/goose-server/src/session_event_bus.rs b/crates/goose-server/src/session_event_bus.rs index 4a993997b553..5ecef7dcb48c 100644 --- a/crates/goose-server/src/session_event_bus.rs +++ b/crates/goose-server/src/session_event_bus.rs @@ -7,6 +7,14 @@ use tokio_util::sync::CancellationToken; const BROADCAST_CAPACITY: usize = 256; const REPLAY_BUFFER_CAPACITY: usize = 512; +/// Error returned by [`SessionEventBus::subscribe`]. +#[derive(Debug)] +pub enum SubscribeError { + /// The client's `Last-Event-ID` has been evicted from the replay buffer, + /// so events have been irrecoverably lost. + ClientTooFarBehind, +} + #[derive(Clone, Debug)] pub struct SessionEvent { /// Monotonic sequence number, written as SSE `id:` frame (not in JSON payload). @@ -64,13 +72,17 @@ impl SessionEventBus { /// Subscribe to live events. If `last_event_id` is provided, replay buffered /// events with seq > last_event_id. Returns (replay_events, replay_max_seq, live_receiver). /// + /// Returns `Err(SubscribeError::ClientTooFarBehind)` when `last_event_id` + /// refers to an event that has already been evicted from the replay buffer, + /// meaning the client has irrecoverably missed events. + /// /// The live receiver is created *before* snapshotting the buffer so that /// no event can fall into the gap between the two steps. The caller must /// skip live events with `seq <= replay_max_seq` to deduplicate. pub async fn subscribe( &self, last_event_id: Option, - ) -> (Vec, u64, broadcast::Receiver) { + ) -> Result<(Vec, u64, broadcast::Receiver), SubscribeError> { // Subscribe first so that any event published while we hold the // buffer lock is guaranteed to appear in `rx` (possibly duplicating // a replay entry). The caller deduplicates via replay_max_seq. @@ -78,16 +90,24 @@ impl SessionEventBus { let (replay, replay_max_seq) = { let buf = self.buffer.lock().await; - // Clamp to the actual buffer max so a stale Last-Event-ID - // (e.g. from before a server restart) doesn't suppress live events. let buf_max = buf.back().map(|e| e.seq).unwrap_or(0); + let buf_min = buf.front().map(|e| e.seq).unwrap_or(0); let last_id = last_event_id.unwrap_or(0); + + // If the client sent a Last-Event-ID that has been evicted from + // the buffer, they have irrecoverably missed events. + if last_id > 0 && buf_min > 0 && last_id < buf_min { + return Err(SubscribeError::ClientTooFarBehind); + } + + // Clamp to the actual buffer max so a stale Last-Event-ID + // (e.g. from before a server restart) doesn't suppress live events. let events: Vec<_> = buf.iter().filter(|e| e.seq > last_id).cloned().collect(); let max_seq = events.last().map(|e| e.seq).unwrap_or(last_id.min(buf_max)); (events, max_seq) }; - (replay, replay_max_seq, rx) + Ok((replay, replay_max_seq, rx)) } /// Register a new request and return its cancellation token. @@ -152,7 +172,7 @@ mod tests { .await; // Subscribe with replay - let (replay, replay_max_seq, _rx) = bus.subscribe(Some(0)).await; + let (replay, replay_max_seq, _rx) = bus.subscribe(Some(0)).await.unwrap(); assert_eq!(replay.len(), 2); assert_eq!(replay[0].seq, 1); assert_eq!(replay[1].seq, 2); @@ -168,7 +188,7 @@ mod tests { bus.publish(None, MessageEvent::Ping).await; // Only get events after seq 2 - let (replay, replay_max_seq, _rx) = bus.subscribe(Some(2)).await; + let (replay, replay_max_seq, _rx) = bus.subscribe(Some(2)).await.unwrap(); assert_eq!(replay.len(), 1); assert_eq!(replay[0].seq, 3); assert_eq!(replay_max_seq, 3); @@ -182,7 +202,7 @@ mod tests { bus.publish(None, MessageEvent::Ping).await; // First connect (no Last-Event-ID) should replay all buffered events - let (replay, replay_max_seq, _rx) = bus.subscribe(None).await; + let (replay, replay_max_seq, _rx) = bus.subscribe(None).await.unwrap(); assert_eq!(replay.len(), 2); assert_eq!(replay_max_seq, 2); } @@ -196,7 +216,7 @@ mod tests { bus.publish(None, MessageEvent::Ping).await; bus.publish(None, MessageEvent::Ping).await; - let (replay, replay_max_seq, _rx) = bus.subscribe(Some(9999)).await; + let (replay, replay_max_seq, _rx) = bus.subscribe(Some(9999)).await.unwrap(); // No replay events (all are below 9999) assert_eq!(replay.len(), 0); // replay_max_seq should be clamped to buf_max (3), not 9999 diff --git a/ui/desktop/src/hooks/useChatStream.ts b/ui/desktop/src/hooks/useChatStream.ts index 46eb22875b94..cfdfc9c101d0 100644 --- a/ui/desktop/src/hooks/useChatStream.ts +++ b/ui/desktop/src/hooks/useChatStream.ts @@ -227,6 +227,7 @@ function createEventProcessor( dispatch: React.Dispatch, onFinish: (error?: string) => void, sessionId: string, + onReloadNeeded?: () => void, ) { let currentMessages = initialMessages; const reduceMotion = prefersReducedMotion(); @@ -300,7 +301,13 @@ function createEventProcessor( } case 'Error': { flushBatchedUpdates(); - onFinish('Stream error: ' + (event as Record).error); + const errorMsg = String((event as Record).error ?? ''); + onFinish('Stream error: ' + errorMsg); + // Server indicated we missed events — reload the full conversation + // so the UI reflects the actual state. + if (errorMsg.includes('too far behind') && onReloadNeeded) { + onReloadNeeded(); + } return true; } case 'Finish': { @@ -439,6 +446,22 @@ export function useChatStream({ [onStreamFinish, sessionId] ); + // Reload the full conversation from the server, e.g. after the SSE + // stream indicates the client fell too far behind the replay buffer. + const reloadConversation = useCallback(() => { + updateFromSession({ + body: { session_id: sessionId }, + throwOnError: true, + }).then((response) => { + const messages = (response.data as Record)?.messages as Message[] | undefined; + if (messages) { + dispatch({ type: 'SET_MESSAGES', payload: messages }); + } + }).catch((e) => { + console.warn('Failed to reload conversation after buffer overflow:', e); + }); + }, [sessionId]); + /** * Submit a message via the new POST+SSE pattern. * 1. Generate request_id @@ -467,6 +490,7 @@ export function useChatStream({ dispatch, onFinish, targetSessionId, + reloadConversation, ); const unsubscribe = addListener(requestId, (event) => { @@ -506,7 +530,7 @@ export function useChatStream({ onFinish('Submit error: ' + errorMessage(error)); } }, - [addListener, onFinish] + [addListener, onFinish, reloadConversation] ); // Load session on mount or sessionId change diff --git a/ui/desktop/src/hooks/useSessionEvents.ts b/ui/desktop/src/hooks/useSessionEvents.ts index 31e2bc5cb4a1..81a8571a6dbc 100644 --- a/ui/desktop/src/hooks/useSessionEvents.ts +++ b/ui/desktop/src/hooks/useSessionEvents.ts @@ -61,7 +61,15 @@ export function useSessionEvents(sessionId: string) { const sessionEvent = event as SessionEvent; const routingId = sessionEvent.chat_request_id ?? sessionEvent.request_id; - if (routingId) { + // Server-level errors without a request ID (e.g. "client too far + // behind") affect all active listeners — broadcast to everyone. + if (!routingId && sessionEvent.type === 'Error') { + for (const [id, handlers] of listenersRef.current) { + for (const handler of handlers) { + handler({ ...sessionEvent, request_id: id, chat_request_id: id }); + } + } + } else if (routingId) { const handlers = listenersRef.current.get(routingId); if (handlers) { for (const handler of handlers) { From 6da2476416ed4ce34b78263fe29127eebb356816 Mon Sep 17 00:00:00 2001 From: jh-block Date: Wed, 18 Mar 2026 11:46:20 +0100 Subject: [PATCH 24/30] fix: defer SSE connected state, use getSession for reload, avoid error screen - Only reset backoff/consecutiveErrors after receiving a real SSE event (not after generator creation, since the HTTP request hasn't happened yet). Streams that end with no events are now treated as errors with proper backoff. - Use getSession instead of updateFromSession to reload conversation after buffer overflow, since updateFromSession returns no body. - Call onFinish() without an error string for the "too far behind" case so it doesn't trigger the blocking "Failed to Load Session" screen. --- ui/desktop/src/hooks/useChatStream.ts | 19 ++++++----- ui/desktop/src/hooks/useSessionEvents.ts | 41 +++++++++++++++++++++--- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/ui/desktop/src/hooks/useChatStream.ts b/ui/desktop/src/hooks/useChatStream.ts index cfdfc9c101d0..66efdd6443d0 100644 --- a/ui/desktop/src/hooks/useChatStream.ts +++ b/ui/desktop/src/hooks/useChatStream.ts @@ -302,11 +302,14 @@ function createEventProcessor( case 'Error': { flushBatchedUpdates(); const errorMsg = String((event as Record).error ?? ''); - onFinish('Stream error: ' + errorMsg); - // Server indicated we missed events — reload the full conversation - // so the UI reflects the actual state. if (errorMsg.includes('too far behind') && onReloadNeeded) { + // Server indicated we missed events — end streaming without setting + // an error (which would show a blocking error screen), then reload + // the full conversation so the UI reflects the actual state. + onFinish(); onReloadNeeded(); + } else { + onFinish('Stream error: ' + errorMsg); } return true; } @@ -449,13 +452,13 @@ export function useChatStream({ // Reload the full conversation from the server, e.g. after the SSE // stream indicates the client fell too far behind the replay buffer. const reloadConversation = useCallback(() => { - updateFromSession({ - body: { session_id: sessionId }, + getSession({ + path: { session_id: sessionId }, throwOnError: true, }).then((response) => { - const messages = (response.data as Record)?.messages as Message[] | undefined; - if (messages) { - dispatch({ type: 'SET_MESSAGES', payload: messages }); + const session = response.data as Session; + if (session?.conversation) { + dispatch({ type: 'SET_MESSAGES', payload: session.conversation }); } }).catch((e) => { console.warn('Failed to reload conversation after buffer overflow:', e); diff --git a/ui/desktop/src/hooks/useSessionEvents.ts b/ui/desktop/src/hooks/useSessionEvents.ts index 81a8571a6dbc..4a133a345ad4 100644 --- a/ui/desktop/src/hooks/useSessionEvents.ts +++ b/ui/desktop/src/hooks/useSessionEvents.ts @@ -47,13 +47,20 @@ export function useSessionEvents(sessionId: string) { }, }); - setConnected(true); - retryDelay = 500; // reset on successful connection - consecutiveErrors = 0; + let receivedEvent = false; for await (const event of stream) { if (abortController.signal.aborted) break; + // Only mark as connected after the first real event arrives, + // since the HTTP request doesn't happen until iteration starts. + if (!receivedEvent) { + receivedEvent = true; + setConnected(true); + retryDelay = 500; + consecutiveErrors = 0; + } + // The server adds chat_request_id (the chat UUID) and request_id // to the JSON at the SSE framing layer. Route using chat_request_id // so that Notification events (which carry their own MCP tool-call @@ -79,10 +86,34 @@ export function useSessionEvents(sessionId: string) { } } - // Stream ended normally (e.g. server closed for lagged subscriber). - // Reconnect unless we were intentionally aborted. + // Stream ended. Reconnect unless we were intentionally aborted. if (abortController.signal.aborted) break; setConnected(false); + + // If the stream ended without delivering any events, the connection + // likely failed silently (e.g. 404 with sseMaxRetryAttempts: 1). + // Treat it as an error so backoff and error counting apply. + if (!receivedEvent) { + consecutiveErrors++; + console.warn( + `SSE stream ended with no events (${consecutiveErrors}/${MAX_CONSECUTIVE_ERRORS})` + ); + if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) { + console.error('SSE reconnect limit reached, notifying active listeners'); + const errorEvent: SessionEvent = { + type: 'Error', + error: 'Lost connection to server', + } as SessionEvent; + for (const [routingId, handlers] of listenersRef.current) { + for (const handler of handlers) { + handler({ ...errorEvent, request_id: routingId, chat_request_id: routingId }); + } + } + consecutiveErrors = 0; + } + await new Promise((r) => setTimeout(r, retryDelay)); + retryDelay = Math.min(retryDelay * 2, MAX_RETRY_DELAY); + } } catch (error) { if (abortController.signal.aborted) break; consecutiveErrors++; From 418cb63ce035b9cf4dedbe6343981a46ef067dc5 Mon Sep 17 00:00:00 2001 From: jh-block Date: Wed, 18 Mar 2026 12:16:02 +0100 Subject: [PATCH 25/30] feat: reattach to in-flight replies after remounting the chat view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Server: add ActiveRequests event variant to MessageEvent. When a new SSE subscriber connects, the server checks for active (in-flight) requests on the session bus and sends an ActiveRequests event listing their IDs after replay but before live events. Client: useSessionEvents exposes an ActiveRequests handler callback. useChatStream registers this handler to detect in-flight requests on mount — when ActiveRequests arrives and no local request is active, it bootstraps an event processor and listener for the request so the UI resumes showing the streaming reply. --- crates/goose-server/src/routes/reply.rs | 5 ++ .../goose-server/src/routes/session_events.rs | 15 ++++++ crates/goose-server/src/session_event_bus.rs | 6 +++ ui/desktop/openapi.json | 22 +++++++++ ui/desktop/src/api/types.gen.ts | 3 ++ ui/desktop/src/hooks/useChatStream.ts | 47 ++++++++++++++++++- ui/desktop/src/hooks/useSessionEvents.ts | 16 ++++++- 7 files changed, 112 insertions(+), 2 deletions(-) diff --git a/crates/goose-server/src/routes/reply.rs b/crates/goose-server/src/routes/reply.rs index f6b9fdcc57ff..7df27b434c43 100644 --- a/crates/goose-server/src/routes/reply.rs +++ b/crates/goose-server/src/routes/reply.rs @@ -149,6 +149,11 @@ pub enum MessageEvent { UpdateConversation { conversation: Conversation, }, + /// Sent at the start of an SSE stream to inform the client about + /// in-flight requests it can reattach to. + ActiveRequests { + request_ids: Vec, + }, Ping, } diff --git a/crates/goose-server/src/routes/session_events.rs b/crates/goose-server/src/routes/session_events.rs index bdedbc7f7eb7..3ffc8bb933c1 100644 --- a/crates/goose-server/src/routes/session_events.rs +++ b/crates/goose-server/src/routes/session_events.rs @@ -170,8 +170,10 @@ pub async fn session_events( let (tx, rx) = mpsc::channel::(256); let stream = ReceiverStream::new(rx); + let task_bus = bus.clone(); tokio::spawn(async move { + let bus = task_bus; // Send replayed events for event in &replay { let frame = @@ -181,6 +183,19 @@ pub async fn session_events( } } + // Notify the client about any in-flight requests so it can + // reattach event handlers after a remount / reconnect. + let active_ids = bus.active_request_ids().await; + if !active_ids.is_empty() { + let event = MessageEvent::ActiveRequests { + request_ids: active_ids, + }; + let frame = serialize_session_event(0, None, &event); + if tx.send(frame).await.is_err() { + return; + } + } + // Send live events + heartbeat pings let mut heartbeat_interval = tokio::time::interval(Duration::from_millis(500)); // Heartbeat uses a local counter — not stored in the replay buffer diff --git a/crates/goose-server/src/session_event_bus.rs b/crates/goose-server/src/session_event_bus.rs index 5ecef7dcb48c..d901eb50312a 100644 --- a/crates/goose-server/src/session_event_bus.rs +++ b/crates/goose-server/src/session_event_bus.rs @@ -110,6 +110,12 @@ impl SessionEventBus { Ok((replay, replay_max_seq, rx)) } + /// Return the IDs of all currently active (in-flight) requests. + pub async fn active_request_ids(&self) -> Vec { + let requests = self.active_requests.lock().await; + requests.keys().cloned().collect() + } + /// Register a new request and return its cancellation token. pub async fn register_request(&self, request_id: String) -> CancellationToken { let token = CancellationToken::new(); diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index d4142c03bae7..2a81f2ee4189 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -6175,6 +6175,28 @@ } } }, + { + "type": "object", + "description": "Sent at the start of an SSE stream to inform the client about\nin-flight requests it can reattach to.", + "required": [ + "request_ids", + "type" + ], + "properties": { + "request_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "type": { + "type": "string", + "enum": [ + "ActiveRequests" + ] + } + } + }, { "type": "object", "required": [ diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index 1a926a12a4e4..10b7bfeaccd8 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -700,6 +700,9 @@ export type MessageEvent = { } | { conversation: Conversation; type: 'UpdateConversation'; +} | { + request_ids: Array; + type: 'ActiveRequests'; } | { type: 'Ping'; }; diff --git a/ui/desktop/src/hooks/useChatStream.ts b/ui/desktop/src/hooks/useChatStream.ts index 66efdd6443d0..6df97b5629e1 100644 --- a/ui/desktop/src/hooks/useChatStream.ts +++ b/ui/desktop/src/hooks/useChatStream.ts @@ -354,7 +354,7 @@ export function useChatStream({ const [state, dispatch] = useReducer(streamReducer, initialState); // Long-lived SSE connection for this session - const { addListener } = useSessionEvents(sessionId); + const { addListener, setActiveRequestsHandler } = useSessionEvents(sessionId); // Track the active request for cancellation (includes the session that started it) const activeRequestIdRef = useRef(null); @@ -465,6 +465,51 @@ export function useChatStream({ }); }, [sessionId]); + // Reattach to in-flight replies discovered via the SSE ActiveRequests event. + // This handles the case where the chat view remounts while a reply is still + // running on the server — the new hook instance picks up the existing request + // and starts processing its events. + useEffect(() => { + setActiveRequestsHandler((requestIds: string[]) => { + // Only reattach if we don't already have an active request + if (activeRequestIdRef.current) return; + if (requestIds.length === 0) return; + + // Reattach to the first (most recent) active request. + // Multiple concurrent requests per session aren't supported in the UI. + const requestId = requestIds[0]; + const currentMessages = stateRef.current.messages; + + activeRequestIdRef.current = requestId; + activeRequestSessionIdRef.current = sessionId; + + dispatch({ type: 'SET_CHAT_STATE', payload: ChatState.Streaming }); + + const processEvent = createEventProcessor( + currentMessages, + dispatch, + onFinish, + sessionId, + reloadConversation, + ); + + const unsubscribe = addListener(requestId, (event) => { + const isTerminal = processEvent(event); + if (isTerminal) { + unsubscribe(); + activeUnsubscribeRef.current = null; + activeRequestIdRef.current = null; + activeRequestSessionIdRef.current = null; + } + }); + activeUnsubscribeRef.current = unsubscribe; + }); + + return () => { + setActiveRequestsHandler(null); + }; + }, [sessionId, addListener, onFinish, reloadConversation, setActiveRequestsHandler]); + /** * Submit a message via the new POST+SSE pattern. * 1. Generate request_id diff --git a/ui/desktop/src/hooks/useSessionEvents.ts b/ui/desktop/src/hooks/useSessionEvents.ts index 4a133a345ad4..95c74947cb95 100644 --- a/ui/desktop/src/hooks/useSessionEvents.ts +++ b/ui/desktop/src/hooks/useSessionEvents.ts @@ -12,9 +12,11 @@ export type SessionEvent = MessageEvent & { }; type EventHandler = (event: SessionEvent) => void; +type ActiveRequestsHandler = (requestIds: string[]) => void; export function useSessionEvents(sessionId: string) { const listenersRef = useRef(new Map>()); + const activeRequestsHandlerRef = useRef(null); const abortRef = useRef(null); const [connected, setConnected] = useState(false); @@ -68,6 +70,14 @@ export function useSessionEvents(sessionId: string) { const sessionEvent = event as SessionEvent; const routingId = sessionEvent.chat_request_id ?? sessionEvent.request_id; + // ActiveRequests events notify the client about in-flight requests + // it can reattach to (e.g. after a remount). + if (sessionEvent.type === 'ActiveRequests') { + const ids = (sessionEvent as unknown as { request_ids: string[] }).request_ids; + activeRequestsHandlerRef.current?.(ids); + continue; + } + // Server-level errors without a request ID (e.g. "client too far // behind") affect all active listeners — broadcast to everyone. if (!routingId && sessionEvent.type === 'Error') { @@ -178,5 +188,9 @@ export function useSessionEvents(sessionId: string) { [] ); - return { connected, addListener }; + const setActiveRequestsHandler = useCallback((handler: ActiveRequestsHandler | null) => { + activeRequestsHandlerRef.current = handler; + }, []); + + return { connected, addListener, setActiveRequestsHandler }; } From c6406f57effd234447792366a0f63df13172c2ea Mon Sep 17 00:00:00 2001 From: jh-block Date: Wed, 18 Mar 2026 12:26:49 +0100 Subject: [PATCH 26/30] fix: send ActiveRequests before replay and without SSE id Move the ActiveRequests event before replayed events so the client registers listeners before buffered Message/Finish frames arrive. Emit it without an SSE id: field so it doesn't regress the client's Last-Event-ID cursor on reconnect. --- .../goose-server/src/routes/session_events.rs | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/crates/goose-server/src/routes/session_events.rs b/crates/goose-server/src/routes/session_events.rs index 3ffc8bb933c1..ebb933b1f355 100644 --- a/crates/goose-server/src/routes/session_events.rs +++ b/crates/goose-server/src/routes/session_events.rs @@ -174,23 +174,30 @@ pub async fn session_events( tokio::spawn(async move { let bus = task_bus; - // Send replayed events - for event in &replay { - let frame = - serialize_session_event(event.seq, event.request_id.as_deref(), &event.event); - if tx.send(frame).await.is_err() { - return; - } - } - // Notify the client about any in-flight requests so it can - // reattach event handlers after a remount / reconnect. + // Notify the client about any in-flight requests BEFORE replay + // so it can register event handlers before replayed events arrive. + // Emitted without an SSE `id:` field so it doesn't regress the + // client's Last-Event-ID cursor. let active_ids = bus.active_request_ids().await; if !active_ids.is_empty() { let event = MessageEvent::ActiveRequests { request_ids: active_ids, }; - let frame = serialize_session_event(0, None, &event); + let json_str = serde_json::to_string( + &serde_json::to_value(&event).unwrap_or_default(), + ) + .unwrap_or_default(); + let frame = format!("data: {}\n\n", json_str); + if tx.send(frame).await.is_err() { + return; + } + } + + // Send replayed events + for event in &replay { + let frame = + serialize_session_event(event.seq, event.request_id.as_deref(), &event.event); if tx.send(frame).await.is_err() { return; } From 4093cf6f31b08b5101ff37f58fe4942eec09b6b6 Mon Sep 17 00:00:00 2001 From: jh-block Date: Wed, 18 Mar 2026 12:41:32 +0100 Subject: [PATCH 27/30] fix: guard ref cleanup on request ID and preserve replayed output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Terminal-event handlers in listener closures now check that activeRequestIdRef still matches their own request ID before clearing global refs. This prevents an older request's finish from clobbering a newer request's listener and cancel handles when requests overlap. onFinish no longer clears SSE-specific refs (that's now solely the listener's responsibility), avoiding the same clobber path. When resumeAgent() resolves after an ActiveRequests reattach has already started processing events, only session metadata is loaded — the stale DB message snapshot is skipped so it doesn't overwrite the fresh streaming content from the event processor. --- ui/desktop/src/hooks/useChatStream.ts | 69 ++++++++++++++++++--------- 1 file changed, 46 insertions(+), 23 deletions(-) diff --git a/ui/desktop/src/hooks/useChatStream.ts b/ui/desktop/src/hooks/useChatStream.ts index 6df97b5629e1..f232245c5460 100644 --- a/ui/desktop/src/hooks/useChatStream.ts +++ b/ui/desktop/src/hooks/useChatStream.ts @@ -385,14 +385,9 @@ export function useChatStream({ const onFinish = useCallback( async (error?: string): Promise => { - // Unsubscribe from event listener - if (activeUnsubscribeRef.current) { - activeUnsubscribeRef.current(); - activeUnsubscribeRef.current = null; - } - activeRequestIdRef.current = null; - activeRequestSessionIdRef.current = null; - activeAbortRef.current = null; + // Note: SSE listener/ref cleanup is handled by the terminal-event + // handler in each listener closure (which guards on requestId) so + // that overlapping requests don't clobber each other's state. if (namePollingRef.current) { clearTimeout(namePollingRef.current); @@ -497,9 +492,11 @@ export function useChatStream({ const isTerminal = processEvent(event); if (isTerminal) { unsubscribe(); - activeUnsubscribeRef.current = null; - activeRequestIdRef.current = null; - activeRequestSessionIdRef.current = null; + if (activeRequestIdRef.current === requestId) { + activeUnsubscribeRef.current = null; + activeRequestIdRef.current = null; + activeRequestSessionIdRef.current = null; + } } }); activeUnsubscribeRef.current = unsubscribe; @@ -545,10 +542,14 @@ export function useChatStream({ const isTerminal = processEvent(event); if (isTerminal) { unsubscribe(); - activeUnsubscribeRef.current = null; - activeRequestIdRef.current = null; - activeRequestSessionIdRef.current = null; - activeAbortRef.current = null; + // Only clear global refs if this request is still the active one. + // A newer request may have already replaced them. + if (activeRequestIdRef.current === requestId) { + activeUnsubscribeRef.current = null; + activeRequestIdRef.current = null; + activeRequestSessionIdRef.current = null; + activeAbortRef.current = null; + } } }); activeUnsubscribeRef.current = unsubscribe; @@ -632,12 +633,18 @@ export function useChatStream({ showExtensionLoadResults(extensionResults); window.dispatchEvent(new CustomEvent(AppEvents.SESSION_EXTENSIONS_LOADED)); - dispatch({ - type: 'SESSION_LOADED', - payload: { - session: loadedSession!, - messages: loadedSession?.conversation || [], - tokenState: { + // If an in-flight reply was reattached via ActiveRequests while + // resumeAgent was in flight, the event processor has already been + // updating messages with fresh streaming content. Only load the + // session metadata — don't overwrite messages with the stale DB + // snapshot. + const reattachedToActiveRequest = activeRequestIdRef.current !== null; + + if (reattachedToActiveRequest) { + dispatch({ type: 'SET_SESSION', payload: loadedSession }); + dispatch({ + type: 'SET_TOKEN_STATE', + payload: { inputTokens: loadedSession?.input_tokens ?? 0, outputTokens: loadedSession?.output_tokens ?? 0, totalTokens: loadedSession?.total_tokens ?? 0, @@ -645,8 +652,24 @@ export function useChatStream({ accumulatedOutputTokens: loadedSession?.accumulated_output_tokens ?? 0, accumulatedTotalTokens: loadedSession?.accumulated_total_tokens ?? 0, }, - }, - }); + }); + } else { + dispatch({ + type: 'SESSION_LOADED', + payload: { + session: loadedSession!, + messages: loadedSession?.conversation || [], + tokenState: { + inputTokens: loadedSession?.input_tokens ?? 0, + outputTokens: loadedSession?.output_tokens ?? 0, + totalTokens: loadedSession?.total_tokens ?? 0, + accumulatedInputTokens: loadedSession?.accumulated_input_tokens ?? 0, + accumulatedOutputTokens: loadedSession?.accumulated_output_tokens ?? 0, + accumulatedTotalTokens: loadedSession?.accumulated_total_tokens ?? 0, + }, + }, + }); + } listApps({ throwOnError: true, From 9529590e0752605ff9e2ebd527afc6f310849c24 Mon Sep 17 00:00:00 2001 From: jh-block Date: Wed, 18 Mar 2026 17:06:49 +0100 Subject: [PATCH 28/30] style: cargo fmt --- crates/goose-server/src/routes/session_events.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/goose-server/src/routes/session_events.rs b/crates/goose-server/src/routes/session_events.rs index ebb933b1f355..7f69aa97ed69 100644 --- a/crates/goose-server/src/routes/session_events.rs +++ b/crates/goose-server/src/routes/session_events.rs @@ -184,10 +184,8 @@ pub async fn session_events( let event = MessageEvent::ActiveRequests { request_ids: active_ids, }; - let json_str = serde_json::to_string( - &serde_json::to_value(&event).unwrap_or_default(), - ) - .unwrap_or_default(); + let json_str = serde_json::to_string(&serde_json::to_value(&event).unwrap_or_default()) + .unwrap_or_default(); let frame = format!("data: {}\n\n", json_str); if tx.send(frame).await.is_err() { return; From db31581cf39bef92443a492cec3b5e3349472d11 Mon Sep 17 00:00:00 2001 From: jh-block Date: Wed, 18 Mar 2026 17:32:22 +0100 Subject: [PATCH 29/30] fix: preserve transcript on cold-mount reattach and clear stale errors When ActiveRequests fires before resumeAgent returns (cold mount), defer the event processor setup until session load completes so it starts with the full conversation history instead of an empty list. Also clear sessionLoadError when reattaching so a prior SSE error screen doesn't persist after connectivity recovers. --- ui/desktop/src/hooks/useChatStream.ts | 95 ++++++++++++++++++++------- 1 file changed, 72 insertions(+), 23 deletions(-) diff --git a/ui/desktop/src/hooks/useChatStream.ts b/ui/desktop/src/hooks/useChatStream.ts index f232245c5460..e3b4f811dbe3 100644 --- a/ui/desktop/src/hooks/useChatStream.ts +++ b/ui/desktop/src/hooks/useChatStream.ts @@ -362,11 +362,16 @@ export function useChatStream({ const activeAbortRef = useRef(null); const activeUnsubscribeRef = useRef<(() => void) | null>(null); const lastInteractionTimeRef = useRef(Date.now()); + // When ActiveRequests fires before resumeAgent populates messages (cold mount), + // defer the reattach until the session is loaded so the event processor has + // the full conversation history. + const pendingReattachRequestIdRef = useRef(null); const namePollingRef = useRef | null>(null); // Ref to access latest state in callbacks (avoids stale closures) const stateRef = useRef(state); stateRef.current = state; + const doReattachRef = useRef<((requestId: string, messages: Message[]) => void) | null>(null); useEffect(() => { return () => { @@ -460,28 +465,19 @@ export function useChatStream({ }); }, [sessionId]); - // Reattach to in-flight replies discovered via the SSE ActiveRequests event. - // This handles the case where the chat view remounts while a reply is still - // running on the server — the new hook instance picks up the existing request - // and starts processing its events. - useEffect(() => { - setActiveRequestsHandler((requestIds: string[]) => { - // Only reattach if we don't already have an active request - if (activeRequestIdRef.current) return; - if (requestIds.length === 0) return; - - // Reattach to the first (most recent) active request. - // Multiple concurrent requests per session aren't supported in the UI. - const requestId = requestIds[0]; - const currentMessages = stateRef.current.messages; - + // Perform the actual reattach: wire up an event processor and listener + // for a request that is already in-flight on the server. + const doReattach = useCallback( + (requestId: string, messages: Message[]) => { activeRequestIdRef.current = requestId; activeRequestSessionIdRef.current = sessionId; + pendingReattachRequestIdRef.current = null; dispatch({ type: 'SET_CHAT_STATE', payload: ChatState.Streaming }); + dispatch({ type: 'SET_SESSION_LOAD_ERROR', payload: undefined }); const processEvent = createEventProcessor( - currentMessages, + messages, dispatch, onFinish, sessionId, @@ -500,12 +496,45 @@ export function useChatStream({ } }); activeUnsubscribeRef.current = unsubscribe; + }, + [sessionId, addListener, onFinish, reloadConversation], + ); + doReattachRef.current = doReattach; + + // Reattach to in-flight replies discovered via the SSE ActiveRequests event. + // This handles the case where the chat view remounts while a reply is still + // running on the server — the new hook instance picks up the existing request + // and starts processing its events. + useEffect(() => { + setActiveRequestsHandler((requestIds: string[]) => { + // Only reattach if we don't already have an active request + if (activeRequestIdRef.current) return; + if (requestIds.length === 0) return; + + // Reattach to the first (most recent) active request. + // Multiple concurrent requests per session aren't supported in the UI. + const requestId = requestIds[0]; + const currentMessages = stateRef.current.messages; + + if (currentMessages.length === 0) { + // Cold mount: resumeAgent hasn't populated messages yet. + // Defer reattach until session load completes so the event + // processor starts with the full conversation history. + pendingReattachRequestIdRef.current = requestId; + activeRequestIdRef.current = requestId; + activeRequestSessionIdRef.current = sessionId; + dispatch({ type: 'SET_CHAT_STATE', payload: ChatState.Streaming }); + dispatch({ type: 'SET_SESSION_LOAD_ERROR', payload: undefined }); + return; + } + + doReattach(requestId, currentMessages); }); return () => { setActiveRequestsHandler(null); }; - }, [sessionId, addListener, onFinish, reloadConversation, setActiveRequestsHandler]); + }, [sessionId, addListener, onFinish, reloadConversation, setActiveRequestsHandler, doReattach]); /** * Submit a message via the new POST+SSE pattern. @@ -633,14 +662,34 @@ export function useChatStream({ showExtensionLoadResults(extensionResults); window.dispatchEvent(new CustomEvent(AppEvents.SESSION_EXTENSIONS_LOADED)); - // If an in-flight reply was reattached via ActiveRequests while - // resumeAgent was in flight, the event processor has already been - // updating messages with fresh streaming content. Only load the - // session metadata — don't overwrite messages with the stale DB - // snapshot. + const pendingRequestId = pendingReattachRequestIdRef.current; const reattachedToActiveRequest = activeRequestIdRef.current !== null; - if (reattachedToActiveRequest) { + if (pendingRequestId) { + // Cold-mount reattach: ActiveRequests arrived before resumeAgent + // returned. Load session state first, then complete the reattach + // with the full conversation so the event processor has context. + dispatch({ + type: 'SESSION_LOADED', + payload: { + session: loadedSession!, + messages: loadedSession?.conversation || [], + tokenState: { + inputTokens: loadedSession?.input_tokens ?? 0, + outputTokens: loadedSession?.output_tokens ?? 0, + totalTokens: loadedSession?.total_tokens ?? 0, + accumulatedInputTokens: loadedSession?.accumulated_input_tokens ?? 0, + accumulatedOutputTokens: loadedSession?.accumulated_output_tokens ?? 0, + accumulatedTotalTokens: loadedSession?.accumulated_total_tokens ?? 0, + }, + }, + }); + // Now complete the deferred reattach with the loaded messages + doReattachRef.current?.(pendingRequestId, loadedSession?.conversation || []); + } else if (reattachedToActiveRequest) { + // ActiveRequests already wired up an event processor with existing + // messages — only load session metadata, don't overwrite messages + // with the stale DB snapshot. dispatch({ type: 'SET_SESSION', payload: loadedSession }); dispatch({ type: 'SET_TOKEN_STATE', From 28342283d6a838f05f6beed30aeedbe7e9f0cfe8 Mon Sep 17 00:00:00 2001 From: jh-block Date: Wed, 18 Mar 2026 17:42:45 +0100 Subject: [PATCH 30/30] fix: buffer events during cold-mount reattach and guard POST cleanup Register a buffering listener immediately when ActiveRequests fires on a cold mount so replayed SSE events aren't lost while waiting for resumeAgent. doReattach replays the buffer through the real processor once the session loads. Also guard POST-failure cleanup with a requestId check so an older request's error handler can't clobber a newer request's refs. --- ui/desktop/src/hooks/useChatStream.ts | 55 +++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/ui/desktop/src/hooks/useChatStream.ts b/ui/desktop/src/hooks/useChatStream.ts index e3b4f811dbe3..6a94b0933888 100644 --- a/ui/desktop/src/hooks/useChatStream.ts +++ b/ui/desktop/src/hooks/useChatStream.ts @@ -364,8 +364,9 @@ export function useChatStream({ const lastInteractionTimeRef = useRef(Date.now()); // When ActiveRequests fires before resumeAgent populates messages (cold mount), // defer the reattach until the session is loaded so the event processor has - // the full conversation history. + // the full conversation history. Events are buffered in the meantime. const pendingReattachRequestIdRef = useRef(null); + const pendingReattachBufferRef = useRef([]); const namePollingRef = useRef | null>(null); // Ref to access latest state in callbacks (avoids stale closures) @@ -484,6 +485,34 @@ export function useChatStream({ reloadConversation, ); + // Replay any events that were buffered during cold-mount wait + const buffered = pendingReattachBufferRef.current; + pendingReattachBufferRef.current = []; + let finished = false; + for (const event of buffered) { + if (processEvent(event)) { + finished = true; + break; + } + } + + if (finished) { + // The reply already completed while we were waiting for session load. + // Clean up — the buffering listener will be replaced below but the + // old one captured into activeUnsubscribeRef should be removed. + if (activeUnsubscribeRef.current) { + activeUnsubscribeRef.current(); + activeUnsubscribeRef.current = null; + } + activeRequestIdRef.current = null; + activeRequestSessionIdRef.current = null; + return; + } + + // Replace the buffering listener with a real processing listener + if (activeUnsubscribeRef.current) { + activeUnsubscribeRef.current(); + } const unsubscribe = addListener(requestId, (event) => { const isTerminal = processEvent(event); if (isTerminal) { @@ -518,13 +547,21 @@ export function useChatStream({ if (currentMessages.length === 0) { // Cold mount: resumeAgent hasn't populated messages yet. - // Defer reattach until session load completes so the event + // Defer event processing until session load completes so the // processor starts with the full conversation history. + // Register a buffering listener NOW so replayed events aren't + // lost while we wait. pendingReattachRequestIdRef.current = requestId; + pendingReattachBufferRef.current = []; activeRequestIdRef.current = requestId; activeRequestSessionIdRef.current = sessionId; dispatch({ type: 'SET_CHAT_STATE', payload: ChatState.Streaming }); dispatch({ type: 'SET_SESSION_LOAD_ERROR', payload: undefined }); + + const unsubscribe = addListener(requestId, (event) => { + pendingReattachBufferRef.current.push(event); + }); + activeUnsubscribeRef.current = unsubscribe; return; } @@ -599,12 +636,16 @@ export function useChatStream({ } catch (error) { // Abort is expected when stopStreaming races with the POST if (abortController.signal.aborted) return; - // POST failed — clean up listener and report error + // POST failed — clean up listener and report error. + // Only clear global refs if this request is still the active one; + // a newer request may have already replaced them. unsubscribe(); - activeUnsubscribeRef.current = null; - activeRequestIdRef.current = null; - activeRequestSessionIdRef.current = null; - activeAbortRef.current = null; + if (activeRequestIdRef.current === requestId) { + activeUnsubscribeRef.current = null; + activeRequestIdRef.current = null; + activeRequestSessionIdRef.current = null; + activeAbortRef.current = null; + } onFinish('Submit error: ' + errorMessage(error)); } },