diff --git a/skills b/skills index 0685a6af4..e856b8e75 160000 --- a/skills +++ b/skills @@ -1 +1 @@ -Subproject commit 0685a6af4ef3ddea4fb26364dd72ea574f307314 +Subproject commit e856b8e756ad7ea86f36dbd1697542e76a8d3571 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index bb512679f..9b02b47de 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "OpenHuman" -version = "0.49.12" +version = "0.49.15" dependencies = [ "aes-gcm", "android_logger", diff --git a/src-tauri/src/commands/chat.rs b/src-tauri/src/commands/chat.rs index 2d0919d0a..54fb36668 100644 --- a/src-tauri/src/commands/chat.rs +++ b/src-tauri/src/commands/chat.rs @@ -436,15 +436,9 @@ pub async fn chat_send( chat_state_arc.remove(&thread_id_clone); if let Err(e) = result { - let _ = app_clone.emit( - "chat:error", - ChatErrorEvent { - thread_id: thread_id_clone, - message: e, - error_type: "inference".to_string(), - round: None, - }, - ); + // chat_send_inner already emits chat:error for known error paths. + // Only log here to avoid duplicate error events. + log::error!("[chat] chat_send_inner failed: {e}"); } }); @@ -501,15 +495,9 @@ pub async fn chat_send( chat_state_arc.remove(&thread_id_clone); if let Err(e) = result { - let _ = app_clone.emit( - "chat:error", - ChatErrorEvent { - thread_id: thread_id_clone, - message: e, - error_type: "inference".to_string(), - round: None, - }, - ); + // chat_send_mobile already emits chat:error for known error paths. + // Only log here to avoid duplicate error events. + log::error!("[chat] chat_send_mobile failed: {e}"); } }); @@ -546,7 +534,16 @@ async fn chat_send_inner( let client = reqwest::Client::builder() .use_rustls_tls() .build() - .map_err(|e| format!("Failed to build HTTP client: {}", e))?; + .map_err(|e| { + let msg = format!("Failed to build HTTP client: {}", e); + let _ = app.emit("chat:error", ChatErrorEvent { + thread_id: thread_id.to_string(), + message: msg.clone(), + error_type: "network".to_string(), + round: None, + }); + msg + })?; // ── Step 1: Load AI context ───────────────────────────────────────── let openclaw_context = load_openclaw_context(app); @@ -775,7 +772,16 @@ async fn chat_send_inner( let completion: ChatCompletionResponse = response .json() .await - .map_err(|e| format!("Failed to parse inference response: {}", e))?; + .map_err(|e| { + let msg = format!("Failed to parse inference response: {}", e); + let _ = app.emit("chat:error", ChatErrorEvent { + thread_id: thread_id.to_string(), + message: msg.clone(), + error_type: "inference".to_string(), + round: Some(round), + }); + msg + })?; // Accumulate token usage if let Some(ref usage) = completion.usage { @@ -786,7 +792,16 @@ async fn chat_send_inner( let choice = completion .choices .first() - .ok_or_else(|| "No choices in inference response".to_string())?; + .ok_or_else(|| { + let msg = "No choices in inference response".to_string(); + let _ = app.emit("chat:error", ChatErrorEvent { + thread_id: thread_id.to_string(), + message: msg.clone(), + error_type: "inference".to_string(), + round: Some(round), + }); + msg + })?; log::info!( "[chat] Round {} — finish_reason={:?}, tool_calls={}", @@ -1002,7 +1017,16 @@ async fn chat_send_mobile( let client = reqwest::Client::builder() .use_rustls_tls() .build() - .map_err(|e| format!("Failed to build HTTP client: {}", e))?; + .map_err(|e| { + let msg = format!("Failed to build HTTP client: {}", e); + let _ = app.emit("chat:error", ChatErrorEvent { + thread_id: thread_id.to_string(), + message: msg.clone(), + error_type: "network".to_string(), + round: None, + }); + msg + })?; // ── Step 1: Load AI context ───────────────────────────────────────── let openclaw_context = load_openclaw_context(app); @@ -1146,7 +1170,16 @@ async fn chat_send_mobile( let completion: ChatCompletionResponse = response .json() .await - .map_err(|e| format!("Failed to parse inference response: {}", e))?; + .map_err(|e| { + let msg = format!("Failed to parse inference response: {}", e); + let _ = app.emit("chat:error", ChatErrorEvent { + thread_id: thread_id.to_string(), + message: msg.clone(), + error_type: "inference".to_string(), + round: None, + }); + msg + })?; let (total_input_tokens, total_output_tokens) = completion .usage @@ -1156,7 +1189,16 @@ async fn chat_send_mobile( let choice = completion .choices .first() - .ok_or_else(|| "No choices in inference response".to_string())?; + .ok_or_else(|| { + let msg = "No choices in inference response".to_string(); + let _ = app.emit("chat:error", ChatErrorEvent { + thread_id: thread_id.to_string(), + message: msg.clone(), + error_type: "inference".to_string(), + round: None, + }); + msg + })?; let full_response = choice.message.content.clone().unwrap_or_default(); diff --git a/src/pages/Conversations.tsx b/src/pages/Conversations.tsx index 877322287..5c90e0cd5 100644 --- a/src/pages/Conversations.tsx +++ b/src/pages/Conversations.tsx @@ -293,6 +293,20 @@ const Conversations = () => { }); if (event.error_type !== 'cancelled') { + // Deduplicate: skip if the last message is already an error + const currentState = store.getState() as { + thread: { messagesByThreadId: Record }; + }; + const threadMessages = + currentState.thread.messagesByThreadId[event.thread_id] || []; + const lastMsg = threadMessages[threadMessages.length - 1]; + if ( + lastMsg?.sender === 'agent' && + lastMsg?.content === 'Something went wrong — please try again.' + ) { + return; + } + dispatch( addInferenceResponse({ content: 'Something went wrong — please try again.',