From 9668f2ed237c8c3b92655109c86755fde46d6e9d Mon Sep 17 00:00:00 2001 From: cyrus Date: Fri, 27 Mar 2026 17:53:02 +0530 Subject: [PATCH 1/4] fix: sync selected thread state and messages view updates - Ensured the `messages` view updates when a new message is added to the currently selected thread. - Simplified `useEffect` logic in `Conversations.tsx` to always set the selected thread for consistent message synchronization. - Removed redundant `setSelectedThread` call during message dispatch to prevent state management issues. --- src/pages/Conversations.tsx | 9 ++++----- src/store/threadSlice.ts | 5 +++++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/pages/Conversations.tsx b/src/pages/Conversations.tsx index 2446ba868..877322287 100644 --- a/src/pages/Conversations.tsx +++ b/src/pages/Conversations.tsx @@ -152,10 +152,10 @@ const Conversations = () => { ); } - if (selectedThreadId !== DEFAULT_THREAD_ID) { - dispatch(setSelectedThread(DEFAULT_THREAD_ID)); - } - }, [dispatch, selectedThreadId, threads]); + // Always set selected thread to ensure messages view is synced from persisted storage + dispatch(setSelectedThread(DEFAULT_THREAD_ID)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dispatch]); useEffect(() => { setIsLoadingModels(true); @@ -340,7 +340,6 @@ const Conversations = () => { }; dispatch(addMessageLocal({ threadId: sendingThreadId, message: userMessage })); - dispatch(setSelectedThread(sendingThreadId)); const historySnapshot = messages.filter( m => !m.id.startsWith('optimistic-') && m.id !== userMessage.id diff --git a/src/store/threadSlice.ts b/src/store/threadSlice.ts index 8ef714c3b..1602aeae7 100644 --- a/src/store/threadSlice.ts +++ b/src/store/threadSlice.ts @@ -249,6 +249,11 @@ const threadSlice = createSlice({ } state.messagesByThreadId[threadId].push(message); + // Also update current messages view if this is the selected thread + if (threadId === state.selectedThreadId) { + state.messages.push(message); + } + // Update thread metadata const thread = state.threads.find(t => t.id === threadId); if (thread) { From 70bfd7231a9c4b0f19118b4a8509afe63bf1e5bb Mon Sep 17 00:00:00 2001 From: cyrus Date: Fri, 27 Mar 2026 18:09:39 +0530 Subject: [PATCH 2/4] Refactor error handling and deduplicate error events Updated error handling in chat commands to reduce redundant event emissions by logging errors instead. Enhanced error reporting for HTTP client and inference response failures. Added frontend logic to prevent duplicate error messages in the UI. --- skills | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/src/commands/chat.rs | 90 +++++++++++++++++++++++++--------- src/pages/Conversations.tsx | 14 ++++++ 4 files changed, 82 insertions(+), 26 deletions(-) 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.', From c4b261529d7cbe949afe2e9ee13d14f63098f55f Mon Sep 17 00:00:00 2001 From: cyrus Date: Fri, 27 Mar 2026 18:13:04 +0530 Subject: [PATCH 3/4] Refactor: improve code readability with consistent formatting in handlers, migration logic, and skill generation scripts. --- src-tauri/src/ai/encryption.rs | 2 +- src-tauri/src/bin/openhuman-cli.rs | 12 +- src-tauri/src/commands/auth.rs | 1 - src-tauri/src/commands/chat.rs | 125 +++--- src-tauri/src/commands/memory.rs | 24 +- src-tauri/src/commands/mod.rs | 4 +- src-tauri/src/commands/openhuman.rs | 11 +- src-tauri/src/commands/runtime.rs | 161 ++++--- src-tauri/src/commands/unified_skills.rs | 8 +- src-tauri/src/core_process.rs | 11 +- src-tauri/src/lib.rs | 6 +- src-tauri/src/memory/mod.rs | 51 ++- src-tauri/src/openhuman/agent/agent.rs | 30 +- src-tauri/src/openhuman/agent/dispatcher.rs | 4 +- src-tauri/src/openhuman/agent/loop_.rs | 29 +- src-tauri/src/openhuman/channels/dingtalk.rs | 6 +- src-tauri/src/openhuman/channels/discord.rs | 5 +- src-tauri/src/openhuman/channels/prompt.rs | 13 +- .../openhuman/channels/runtime/dispatch.rs | 8 +- .../src/openhuman/channels/runtime/startup.rs | 6 +- .../openhuman/channels/runtime/supervision.rs | 7 +- src-tauri/src/openhuman/channels/signal.rs | 3 +- src-tauri/src/openhuman/channels/telegram.rs | 10 +- .../src/openhuman/channels/tests/common.rs | 6 +- .../src/openhuman/channels/tests/health.rs | 2 +- .../src/openhuman/channels/tests/identity.rs | 2 +- .../src/openhuman/channels/tests/memory.rs | 7 +- .../src/openhuman/channels/tests/prompt.rs | 5 +- .../channels/tests/runtime_dispatch.rs | 235 +++++----- .../channels/tests/runtime_tool_calls.rs | 12 +- src-tauri/src/openhuman/config/mod.rs | 12 +- .../src/openhuman/config/schema/autonomy.rs | 2 +- src-tauri/src/openhuman/config/schema/load.rs | 24 +- src-tauri/src/openhuman/cron/scheduler.rs | 5 +- src-tauri/src/openhuman/daemon/mod.rs | 81 ++-- src-tauri/src/openhuman/doctor/mod.rs | 43 +- .../src/openhuman/gateway/handlers/health.rs | 4 +- .../src/openhuman/gateway/handlers/linq.rs | 8 +- .../src/openhuman/gateway/handlers/pair.rs | 3 +- .../src/openhuman/gateway/handlers/webhook.rs | 54 +-- .../openhuman/gateway/handlers/whatsapp.rs | 8 +- src-tauri/src/openhuman/gateway/rate_limit.rs | 3 +- src-tauri/src/openhuman/gateway/server.rs | 5 +- src-tauri/src/openhuman/gateway/state.rs | 2 +- src-tauri/src/openhuman/gateway/tests.rs | 8 +- src-tauri/src/openhuman/health/mod.rs | 6 +- .../src/openhuman/integrations/registry.rs | 4 +- src-tauri/src/openhuman/migration.rs | 13 +- src-tauri/src/openhuman/mod.rs | 4 +- src-tauri/src/openhuman/onboard/mod.rs | 4 +- src-tauri/src/openhuman/onboard/models.rs | 10 +- .../openhuman/peripherals/arduino_flash.rs | 5 +- src-tauri/src/openhuman/peripherals/mod.rs | 12 +- .../src/openhuman/peripherals/uno_q_setup.rs | 3 +- .../src/openhuman/providers/anthropic.rs | 6 +- src-tauri/src/openhuman/providers/bedrock.rs | 6 +- .../src/openhuman/providers/compatible.rs | 16 +- src-tauri/src/openhuman/providers/copilot.rs | 6 +- src-tauri/src/openhuman/providers/gemini.rs | 6 +- src-tauri/src/openhuman/providers/ollama.rs | 6 +- src-tauri/src/openhuman/providers/openai.rs | 6 +- .../src/openhuman/providers/openrouter.rs | 6 +- src-tauri/src/openhuman/providers/traits.rs | 4 +- src-tauri/src/openhuman/security/detect.rs | 12 +- src-tauri/src/openhuman/service/mod.rs | 18 +- src-tauri/src/openhuman/tools/browser.rs | 7 +- src-tauri/src/openhuman/tools/http_request.rs | 3 +- src-tauri/src/openhuman/tools/shell.rs | 4 +- src-tauri/src/openhuman/tunnel/mod.rs | 4 +- src-tauri/src/runtime/bridge/db.rs | 30 +- src-tauri/src/runtime/bridge/net.rs | 6 +- src-tauri/src/runtime/bridge/skills_bridge.rs | 4 +- src-tauri/src/runtime/cron_scheduler.rs | 15 +- src-tauri/src/runtime/mod.rs | 4 +- src-tauri/src/runtime/ping_scheduler.rs | 5 +- src-tauri/src/runtime/qjs_engine.rs | 32 +- src-tauri/src/runtime/qjs_skill_instance.rs | 339 +++++++++------ src-tauri/src/runtime/skill_registry.rs | 5 +- src-tauri/src/runtime/socket_manager.rs | 48 +-- src-tauri/src/runtime/types.rs | 4 +- src-tauri/src/runtime/utils.rs | 4 +- src-tauri/src/services/mod.rs | 1 - .../services/quickjs_libs/qjs_ops/ops_core.rs | 187 +++++--- .../quickjs_libs/qjs_ops/ops_state.rs | 263 +++++++----- .../quickjs_libs/qjs_ops/ops_storage.rs | 401 +++++++++++------- .../services/quickjs_libs/qjs_ops/types.rs | 7 +- .../src/services/quickjs_libs/storage.rs | 26 +- src-tauri/src/unified_skills/generator.rs | 37 +- src-tauri/src/unified_skills/llm_generator.rs | 110 +++-- src-tauri/src/unified_skills/mod.rs | 9 +- src-tauri/src/unified_skills/self_evolve.rs | 14 +- src-tauri/src/unified_skills/skill_tester.rs | 29 +- src/pages/Conversations.tsx | 3 +- 93 files changed, 1664 insertions(+), 1158 deletions(-) diff --git a/src-tauri/src/ai/encryption.rs b/src-tauri/src/ai/encryption.rs index 42ca25afb..51b2413c4 100644 --- a/src-tauri/src/ai/encryption.rs +++ b/src-tauri/src/ai/encryption.rs @@ -4,12 +4,12 @@ //! encrypted at rest using AES-256-GCM. Keys are derived from a user //! password via Argon2id. +use aes_gcm::aead::rand_core::RngCore; use aes_gcm::{ aead::{Aead, KeyInit, OsRng}, Aes256Gcm, Nonce, }; use argon2::{self, Algorithm, Argon2, Params, Version}; -use aes_gcm::aead::rand_core::RngCore; use serde::{Deserialize, Serialize}; use std::path::PathBuf; diff --git a/src-tauri/src/bin/openhuman-cli.rs b/src-tauri/src/bin/openhuman-cli.rs index fe23dd7f7..e1a0c8956 100644 --- a/src-tauri/src/bin/openhuman-cli.rs +++ b/src-tauri/src/bin/openhuman-cli.rs @@ -80,14 +80,10 @@ enum Command { }, /// Encrypt a secret - Encrypt { - plaintext: String, - }, + Encrypt { plaintext: String }, /// Decrypt a secret - Decrypt { - ciphertext: String, - }, + Decrypt { ciphertext: String }, /// Toggle browser allow-all runtime flag BrowserAllowAll { @@ -324,7 +320,9 @@ fn load_config() -> Result { .map_err(|e| format!("failed to load config: {e}")) } -fn status_value(status: openhuman::openhuman::service::ServiceStatus) -> Result { +fn status_value( + status: openhuman::openhuman::service::ServiceStatus, +) -> Result { serde_json::to_value(status).map_err(|e| format!("failed to serialize service status: {e}")) } diff --git a/src-tauri/src/commands/auth.rs b/src-tauri/src/commands/auth.rs index 66f5f52e0..26a66d9d2 100644 --- a/src-tauri/src/commands/auth.rs +++ b/src-tauri/src/commands/auth.rs @@ -8,7 +8,6 @@ use std::sync::Arc; pub static SESSION_SERVICE: Lazy> = Lazy::new(|| Arc::new(SessionService::new())); - /// Get the current authentication state #[tauri::command] pub fn get_auth_state() -> AuthState { diff --git a/src-tauri/src/commands/chat.rs b/src-tauri/src/commands/chat.rs index 54fb36668..7721c535a 100644 --- a/src-tauri/src/commands/chat.rs +++ b/src-tauri/src/commands/chat.rs @@ -341,9 +341,7 @@ fn is_read_tool(name: &str) -> bool { /// Tool names are namespaced as `{skill_id}__{tool_name}`. /// Read-only tools are excluded — their data comes from the memory layer (Step 2 context recall). #[cfg(not(any(target_os = "android", target_os = "ios")))] -fn discover_tools( - engine: &crate::runtime::qjs_engine::RuntimeEngine, -) -> Vec { +fn discover_tools(engine: &crate::runtime::qjs_engine::RuntimeEngine) -> Vec { engine .all_tools() .into_iter() @@ -536,12 +534,15 @@ async fn chat_send_inner( .build() .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, - }); + let _ = app.emit( + "chat:error", + ChatErrorEvent { + thread_id: thread_id.to_string(), + message: msg.clone(), + error_type: "network".to_string(), + round: None, + }, + ); msg })?; @@ -580,7 +581,11 @@ async fn chat_send_inner( .map(|(skill_id, _)| skill_id) .collect(); - log::info!("[chat] Recalling skill contexts for {} skill(s): {:?}", skill_ids.len(), skill_ids); + log::info!( + "[chat] Recalling skill contexts for {} skill(s): {:?}", + skill_ids.len(), + skill_ids + ); let mut skill_contexts: Vec = Vec::new(); for sid in &skill_ids { @@ -769,19 +774,19 @@ async fn chat_send_inner( } // Parse the completion response - let completion: ChatCompletionResponse = response - .json() - .await - .map_err(|e| { - let msg = format!("Failed to parse inference response: {}", e); - let _ = app.emit("chat:error", ChatErrorEvent { + let completion: ChatCompletionResponse = response.json().await.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 - })?; + }, + ); + msg + })?; // Accumulate token usage if let Some(ref usage) = completion.usage { @@ -789,19 +794,19 @@ async fn chat_send_inner( total_output_tokens += usage.completion_tokens; } - let choice = completion - .choices - .first() - .ok_or_else(|| { - let msg = "No choices in inference response".to_string(); - let _ = app.emit("chat:error", ChatErrorEvent { + let choice = completion.choices.first().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 - })?; + }, + ); + msg + })?; log::info!( "[chat] Round {} — finish_reason={:?}, tool_calls={}", @@ -844,9 +849,8 @@ async fn chat_send_inner( let (skill_id, tool_name) = parse_tool_name(&tc.function.name); - let args_value: serde_json::Value = - serde_json::from_str(&tc.function.arguments) - .unwrap_or_else(|_| serde_json::Value::Object(serde_json::Map::new())); + let args_value: serde_json::Value = serde_json::from_str(&tc.function.arguments) + .unwrap_or_else(|_| serde_json::Value::Object(serde_json::Map::new())); // Emit tool_call event before executing let _ = app.emit( @@ -911,9 +915,7 @@ async fn chat_send_inner( .content .iter() .filter_map(|c| match c { - crate::runtime::types::ToolContent::Text { text } => { - Some(text.as_str()) - } + crate::runtime::types::ToolContent::Text { text } => Some(text.as_str()), crate::runtime::types::ToolContent::Json { .. } => None, }) .collect::>() @@ -922,9 +924,7 @@ async fn chat_send_inner( // Check for JSON error pattern (matching TS behaviour) let (final_tool_str, final_success) = if !tool_result.is_error { if let Ok(parsed) = serde_json::from_str::(&tool_content) { - if let Some(error_str) = - parsed.get("error").and_then(|e| e.as_str()) - { + if let Some(error_str) = parsed.get("error").and_then(|e| e.as_str()) { (format!("Error: {}", error_str), false) } else { (tool_content.clone(), true) @@ -1019,12 +1019,15 @@ async fn chat_send_mobile( .build() .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, - }); + let _ = app.emit( + "chat:error", + ChatErrorEvent { + thread_id: thread_id.to_string(), + message: msg.clone(), + error_type: "network".to_string(), + round: None, + }, + ); msg })?; @@ -1167,38 +1170,38 @@ async fn chat_send_mobile( return Err(msg); } - let completion: ChatCompletionResponse = response - .json() - .await - .map_err(|e| { - let msg = format!("Failed to parse inference response: {}", e); - let _ = app.emit("chat:error", ChatErrorEvent { + let completion: ChatCompletionResponse = response.json().await.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 - })?; + }, + ); + msg + })?; let (total_input_tokens, total_output_tokens) = completion .usage .map(|u| (u.prompt_tokens, u.completion_tokens)) .unwrap_or((0, 0)); - let choice = completion - .choices - .first() - .ok_or_else(|| { - let msg = "No choices in inference response".to_string(); - let _ = app.emit("chat:error", ChatErrorEvent { + let choice = completion.choices.first().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 - })?; + }, + ); + msg + })?; let full_response = choice.message.content.clone().unwrap_or_default(); diff --git a/src-tauri/src/commands/memory.rs b/src-tauri/src/commands/memory.rs index 87d890a54..794de0f70 100644 --- a/src-tauri/src/commands/memory.rs +++ b/src-tauri/src/commands/memory.rs @@ -86,7 +86,10 @@ pub async fn init_memory_client( jwt_token: String, state: tauri::State<'_, MemoryState>, ) -> Result<(), String> { - log::info!("[memory] init_memory_client: entry (token_present={})", !jwt_token.trim().is_empty()); + log::info!( + "[memory] init_memory_client: entry (token_present={})", + !jwt_token.trim().is_empty() + ); let client = MemoryClient::from_token(jwt_token).map(Arc::new); if client.is_none() { log::warn!("[memory] init_memory_client: exit — empty token, memory layer disabled"); @@ -149,7 +152,10 @@ pub async fn memory_query( .query_skill_context(&skill_id, &integration_id, &query, max_chunks.unwrap_or(10)) .await; match &result { - Ok(ctx) => log::info!("[memory] memory_query: exit — ok (context_len={})", ctx.len()), + Ok(ctx) => log::info!( + "[memory] memory_query: exit — ok (context_len={})", + ctx.len() + ), Err(e) => log::warn!("[memory] memory_query: exit — error: {e}"), } result @@ -218,9 +224,10 @@ pub async fn memory_query_namespace( ) -> Result { let client = state.0.lock().map_err(|e| e.to_string())?.clone(); match client { - Some(c) => c - .query_namespace_context(&namespace, &query, max_chunks.unwrap_or(10)) - .await, + Some(c) => { + c.query_namespace_context(&namespace, &query, max_chunks.unwrap_or(10)) + .await + } None => Err("Memory layer not configured — JWT token not yet set".into()), } } @@ -233,9 +240,10 @@ pub async fn memory_recall_namespace( ) -> Result, String> { let client = state.0.lock().map_err(|e| e.to_string())?.clone(); match client { - Some(c) => c - .recall_namespace_context(&namespace, max_chunks.unwrap_or(10)) - .await, + Some(c) => { + c.recall_namespace_context(&namespace, max_chunks.unwrap_or(10)) + .await + } None => Err("Memory layer not configured — JWT token not yet set".into()), } } diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 84642db05..11c64786d 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -2,9 +2,9 @@ pub mod auth; pub mod chat; pub mod memory; pub mod model; +pub mod openhuman; pub mod runtime; pub mod socket; -pub mod openhuman; pub mod unified_skills; #[cfg(desktop)] @@ -15,9 +15,9 @@ pub use auth::*; pub use chat::{chat_cancel, chat_send}; pub use memory::*; pub use model::*; +pub use openhuman::*; pub use runtime::*; pub use socket::*; -pub use openhuman::*; #[cfg(desktop)] pub use window::*; diff --git a/src-tauri/src/commands/openhuman.rs b/src-tauri/src/commands/openhuman.rs index 55b6f01ed..edfce9ba3 100644 --- a/src-tauri/src/commands/openhuman.rs +++ b/src-tauri/src/commands/openhuman.rs @@ -1,10 +1,10 @@ //! Tauri command proxies for the standalone openhuman core process. -use crate::openhuman::{doctor, hardware, integrations, migration, onboard, service}; use crate::core_server::{ BrowserSettingsUpdate, CommandResponse, ConfigSnapshot, GatewaySettingsUpdate, MemorySettingsUpdate, ModelSettingsUpdate, RuntimeFlags, RuntimeSettingsUpdate, }; +use crate::openhuman::{doctor, hardware, integrations, migration, onboard, service}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use tauri::Manager; @@ -347,7 +347,8 @@ pub async fn openhuman_hardware_introspect( /// Return whether the local core agent server is reachable. #[tauri::command] pub async fn openhuman_agent_server_status() -> Result, String> { - let url = std::env::var("OPENHUMAN_CORE_RPC_URL").unwrap_or_else(|_| DEFAULT_CORE_RPC_URL.to_string()); + let url = std::env::var("OPENHUMAN_CORE_RPC_URL") + .unwrap_or_else(|_| DEFAULT_CORE_RPC_URL.to_string()); let running = crate::core_rpc::ping().await; Ok(CommandResponse { result: AgentServerStatus { running, url }, @@ -389,8 +390,7 @@ pub async fn openhuman_service_stop( app: tauri::AppHandle, ) -> Result, String> { let config = load_config_local().await?; - let status = service::stop(&config) - .map_err(|e| e.to_string())?; + let status = service::stop(&config).map_err(|e| e.to_string())?; // Also stop any locally managed core process in this Tauri app. if let Some(core) = app.try_state::() { @@ -424,8 +424,7 @@ pub async fn openhuman_service_uninstall( app: tauri::AppHandle, ) -> Result, String> { let config = load_config_local().await?; - let status = service::uninstall(&config) - .map_err(|e| e.to_string())?; + let status = service::uninstall(&config).map_err(|e| e.to_string())?; if let Some(core) = app.try_state::() { let core_handle: crate::core_process::CoreProcessHandle = (*core).clone(); diff --git a/src-tauri/src/commands/runtime.rs b/src-tauri/src/commands/runtime.rs index b3d4cdf24..de680ac22 100644 --- a/src-tauri/src/commands/runtime.rs +++ b/src-tauri/src/commands/runtime.rs @@ -9,10 +9,10 @@ use crate::models::socket::SocketState; use crate::runtime::socket_manager::SocketManager; use crate::utils::config::get_backend_url; -use std::sync::Arc; -use std::collections::HashMap; -use tauri::State; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::sync::Arc; +use tauri::State; // Desktop-only imports #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -329,15 +329,24 @@ mod desktop { schemas.push(schema); } - log::info!("🔧 [RUNTIME] Generated {} ZeroClaw tool schemas", schemas.len()); + log::info!( + "🔧 [RUNTIME] Generated {} ZeroClaw tool schemas", + schemas.len() + ); // Log tools that contain 'notion' or 'gmail' for debugging - let gmail_notion_tools: Vec = schemas.iter() + let gmail_notion_tools: Vec = schemas + .iter() .map(|s| &s.function.name) - .filter(|name| name.to_lowercase().contains("gmail") || name.to_lowercase().contains("notion")) + .filter(|name| { + name.to_lowercase().contains("gmail") || name.to_lowercase().contains("notion") + }) .cloned() .collect(); - log::info!("🔧 [RUNTIME] Gmail/Notion tools found: {:?}", gmail_notion_tools); + log::info!( + "🔧 [RUNTIME] Gmail/Notion tools found: {:?}", + gmail_notion_tools + ); Ok(schemas) } @@ -351,12 +360,20 @@ mod desktop { ) -> Result { let start_time = std::time::Instant::now(); - log::info!("🔧 [RUNTIME] Executing ZeroClaw tool: {} with args: {}", tool_id, args); + log::info!( + "🔧 [RUNTIME] Executing ZeroClaw tool: {} with args: {}", + tool_id, + args + ); // Parse tool_id to get skill_id and tool_name (format: "skill_id_tool_name") let (skill_id, tool_name) = match parse_tool_id(&tool_id) { Ok((skill, tool)) => { - log::info!("🔧 [RUNTIME] Parsed tool_id: skill_id='{}', tool_name='{}'", skill, tool); + log::info!( + "🔧 [RUNTIME] Parsed tool_id: skill_id='{}', tool_name='{}'", + skill, + tool + ); (skill, tool) } Err(e) => { @@ -372,30 +389,51 @@ mod desktop { }; // Log runtime state before execution - log::info!("🔧 [RUNTIME] Attempting to call tool '{}' on skill '{}'", tool_name, skill_id); + log::info!( + "🔧 [RUNTIME] Attempting to call tool '{}' on skill '{}'", + tool_name, + skill_id + ); // Get available skills for debugging let skills = engine.list_skills(); - log::info!("🔧 [RUNTIME] Available skills: {:?}", skills.iter().map(|s| &s.skill_id).collect::>()); + log::info!( + "🔧 [RUNTIME] Available skills: {:?}", + skills.iter().map(|s| &s.skill_id).collect::>() + ); // Check if the specific skill exists if let Some(skill) = skills.iter().find(|s| s.skill_id == skill_id) { - log::info!("🔧 [RUNTIME] Found skill '{}' with state: {:?}, tools: {:?}", - skill_id, skill.state, skill.tools.iter().map(|t| &t.name).collect::>()); + log::info!( + "🔧 [RUNTIME] Found skill '{}' with state: {:?}, tools: {:?}", + skill_id, + skill.state, + skill.tools.iter().map(|t| &t.name).collect::>() + ); } else { log::error!("🔧 [RUNTIME] Skill '{}' not found in runtime!", skill_id); } // Execute the tool using the existing command - log::info!("🔧 [RUNTIME] Calling engine.call_tool('{}', '{}', {})", skill_id, tool_name, args); + log::info!( + "🔧 [RUNTIME] Calling engine.call_tool('{}', '{}', {})", + skill_id, + tool_name, + args + ); match engine.call_tool(&skill_id, &tool_name, args).await { Ok(result) => { let execution_time = start_time.elapsed().as_millis() as u64; - log::info!("🔧 [RUNTIME] Tool execution completed in {}ms, is_error: {}", execution_time, result.is_error); + log::info!( + "🔧 [RUNTIME] Tool execution completed in {}ms, is_error: {}", + execution_time, + result.is_error + ); if result.is_error { - let error_message = result.content + let error_message = result + .content .iter() .filter(|c| matches!(c, crate::runtime::types::ToolContent::Text { .. })) .map(|c| match c { @@ -405,7 +443,10 @@ mod desktop { .collect::>() .join("\n"); - log::error!("🔧 [RUNTIME] Tool execution failed with error: {}", error_message); + log::error!( + "🔧 [RUNTIME] Tool execution failed with error: {}", + error_message + ); Ok(ZeroClawToolResult { success: false, @@ -414,12 +455,14 @@ mod desktop { execution_time: Some(execution_time), }) } else { - let output = result.content + let output = result + .content .iter() .map(|c| match c { crate::runtime::types::ToolContent::Text { text } => text.clone(), crate::runtime::types::ToolContent::Json { data } => { - serde_json::to_string(data).unwrap_or_else(|_| "Invalid JSON".to_string()) + serde_json::to_string(data) + .unwrap_or_else(|_| "Invalid JSON".to_string()) } }) .collect::>() @@ -467,7 +510,9 @@ mod desktop { } // Helper function to convert MCP schema to OpenAI function calling format - pub fn convert_to_openai_schema(mcp_schema: serde_json::Value) -> Result { + pub fn convert_to_openai_schema( + mcp_schema: serde_json::Value, + ) -> Result { // If it's already in OpenAI format, return as-is if mcp_schema.is_object() && mcp_schema.get("type").is_some() { return Ok(mcp_schema); @@ -514,7 +559,9 @@ mod mobile { } #[tauri::command] - pub async fn runtime_get_skill_state(_skill_id: String) -> Result, String> { + pub async fn runtime_get_skill_state( + _skill_id: String, + ) -> Result, String> { Ok(None) } @@ -562,7 +609,10 @@ mod mobile { } #[tauri::command] - pub async fn runtime_skill_kv_get(_skill_id: String, _key: String) -> Result { + pub async fn runtime_skill_kv_get( + _skill_id: String, + _key: String, + ) -> Result { Err(MOBILE_ERROR.to_string()) } @@ -585,7 +635,10 @@ mod mobile { } #[tauri::command] - pub async fn runtime_skill_data_read(_skill_id: String, _filename: String) -> Result { + pub async fn runtime_skill_data_read( + _skill_id: String, + _filename: String, + ) -> Result { Err(MOBILE_ERROR.to_string()) } @@ -703,8 +756,8 @@ mod tests { } }, "required": ["message"] - }) - } + }), + }, }; // Test serialization @@ -714,8 +767,8 @@ mod tests { assert!(json.contains("A test tool")); // Test deserialization - let deserialized: ZeroClawToolSchema = serde_json::from_str(&json) - .expect("Should deserialize from JSON"); + let deserialized: ZeroClawToolSchema = + serde_json::from_str(&json).expect("Should deserialize from JSON"); assert_eq!(deserialized.type_field, "function"); assert_eq!(deserialized.function.name, "test_tool"); } @@ -726,7 +779,7 @@ mod tests { success: true, output: "Test output".to_string(), error: None, - execution_time: Some(1500) + execution_time: Some(1500), }; // Test serialization @@ -740,7 +793,7 @@ mod tests { success: false, output: String::new(), error: Some("Tool not found".to_string()), - execution_time: Some(100) + execution_time: Some(100), }; let error_json = serde_json::to_string(&error_result).expect("Should serialize error"); @@ -751,13 +804,13 @@ mod tests { #[test] fn test_parse_tool_id_valid_formats() { // Test valid tool ID formats - let (skill_id, tool_name) = desktop::parse_tool_id("github_list_issues") - .expect("Should parse valid tool ID"); + let (skill_id, tool_name) = + desktop::parse_tool_id("github_list_issues").expect("Should parse valid tool ID"); assert_eq!(skill_id, "github"); assert_eq!(tool_name, "list_issues"); - let (skill_id, tool_name) = desktop::parse_tool_id("notion_create_page") - .expect("Should parse valid tool ID"); + let (skill_id, tool_name) = + desktop::parse_tool_id("notion_create_page").expect("Should parse valid tool ID"); assert_eq!(skill_id, "notion"); assert_eq!(tool_name, "create_page"); @@ -771,10 +824,22 @@ mod tests { #[test] fn test_parse_tool_id_invalid_formats() { // Test invalid formats - assert!(desktop::parse_tool_id("nounderscore").is_err(), "Should fail for no underscore"); - assert!(desktop::parse_tool_id("_empty_skill").is_err(), "Should fail for empty skill ID"); - assert!(desktop::parse_tool_id("empty_tool_").is_err(), "Should fail for empty tool name"); - assert!(desktop::parse_tool_id("").is_err(), "Should fail for empty string"); + assert!( + desktop::parse_tool_id("nounderscore").is_err(), + "Should fail for no underscore" + ); + assert!( + desktop::parse_tool_id("_empty_skill").is_err(), + "Should fail for empty skill ID" + ); + assert!( + desktop::parse_tool_id("empty_tool_").is_err(), + "Should fail for empty tool name" + ); + assert!( + desktop::parse_tool_id("").is_err(), + "Should fail for empty string" + ); } #[test] @@ -823,8 +888,8 @@ mod tests { "state": {"type": "string", "enum": ["open", "closed", "all"], "default": "open"} }, "required": ["owner", "repo"] - }) - } + }), + }, }; // Serialize and check format @@ -860,10 +925,8 @@ mod tests { #[tokio::test] async fn test_mobile_stub_runtime_execute_tool() { - let result = mobile::runtime_execute_tool( - "test_tool".to_string(), - "{}".to_string() - ).await; + let result = + mobile::runtime_execute_tool("test_tool".to_string(), "{}".to_string()).await; // Mobile should return error assert!(result.is_err()); @@ -881,7 +944,8 @@ mod tests { "description": "test", "parameters": {} } - })).expect("Should deserialize from JSON"); + })) + .expect("Should deserialize from JSON"); assert_eq!(tool_schema.type_field, "function"); assert_eq!(tool_schema.function.name, "test"); @@ -892,7 +956,8 @@ mod tests { "output": "result", "error": null, "execution_time": 1000 - })).expect("Should deserialize tool result"); + })) + .expect("Should deserialize tool result"); assert!(tool_result.success); assert_eq!(tool_result.output, "result"); @@ -906,7 +971,7 @@ mod tests { success: false, output: String::new(), error: Some("Connection timeout".to_string()), - execution_time: Some(30000) // 30 second timeout + execution_time: Some(30000), // 30 second timeout }; let json = serde_json::to_string(&error_result).expect("Should serialize error"); @@ -915,8 +980,8 @@ mod tests { assert!(json.contains("30000")); // Test deserialization back - let parsed: ZeroClawToolResult = serde_json::from_str(&json) - .expect("Should parse error result"); + let parsed: ZeroClawToolResult = + serde_json::from_str(&json).expect("Should parse error result"); assert!(!parsed.success); assert_eq!(parsed.error, Some("Connection timeout".to_string())); } diff --git a/src-tauri/src/commands/unified_skills.rs b/src-tauri/src/commands/unified_skills.rs index f4f7665a9..48106d68c 100644 --- a/src-tauri/src/commands/unified_skills.rs +++ b/src-tauri/src/commands/unified_skills.rs @@ -7,8 +7,8 @@ //! Commands are desktop-only — mobile stubs return empty/error results. use crate::runtime::types::{UnifiedSkillEntry, UnifiedSkillResult}; -use crate::unified_skills::GenerateSkillSpec; use crate::unified_skills::self_evolve::{SelfEvolveRequest, SelfEvolveResult}; +use crate::unified_skills::GenerateSkillSpec; use std::sync::Arc; use tauri::State; @@ -140,12 +140,10 @@ mod mobile { #[cfg(not(any(target_os = "android", target_os = "ios")))] pub use desktop::{ - unified_execute_skill, unified_generate_skill, unified_list_skills, - unified_self_evolve_skill, + unified_execute_skill, unified_generate_skill, unified_list_skills, unified_self_evolve_skill, }; #[cfg(any(target_os = "android", target_os = "ios"))] pub use mobile::{ - unified_execute_skill, unified_generate_skill, unified_list_skills, - unified_self_evolve_skill, + unified_execute_skill, unified_generate_skill, unified_list_skills, unified_self_evolve_skill, }; diff --git a/src-tauri/src/core_process.rs b/src-tauri/src/core_process.rs index 68a43cd12..f4974e092 100644 --- a/src-tauri/src/core_process.rs +++ b/src-tauri/src/core_process.rs @@ -23,7 +23,10 @@ impl CoreProcessHandle { pub async fn ensure_running(&self) -> Result<(), String> { if crate::core_rpc::ping().await { - log::info!("[core] found existing core rpc endpoint at {}", self.rpc_url()); + log::info!( + "[core] found existing core rpc endpoint at {}", + self.rpc_url() + ); return Ok(()); } @@ -38,7 +41,11 @@ impl CoreProcessHandle { .arg("--port") .arg(self.port.to_string()); - log::info!("[core] spawning core process: {:?} core serve --port {}", cmd.as_std().get_program(), self.port); + log::info!( + "[core] spawning core process: {:?} core serve --port {}", + cmd.as_std().get_program(), + self.port + ); let child = cmd .spawn() diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e48fa5e2b..d24b6f419 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -9,7 +9,6 @@ //! - Native notifications mod ai; -pub mod openhuman; mod auth; mod commands; mod core_process; @@ -17,6 +16,7 @@ mod core_rpc; pub mod core_server; pub mod memory; mod models; +pub mod openhuman; mod runtime; mod services; mod unified_skills; @@ -457,9 +457,7 @@ fn is_daemon_mode() -> bool { fn daemon_foreground_requested() -> bool { matches!( - std::env::var("OPENHUMAN_DAEMON_FOREGROUND") - .ok() - .as_deref(), + std::env::var("OPENHUMAN_DAEMON_FOREGROUND").ok().as_deref(), Some("1") | Some("true") | Some("TRUE") | Some("yes") | Some("YES") ) } diff --git a/src-tauri/src/memory/mod.rs b/src-tauri/src/memory/mod.rs index ce83b4416..be65e081a 100644 --- a/src-tauri/src/memory/mod.rs +++ b/src-tauri/src/memory/mod.rs @@ -22,15 +22,20 @@ impl MemoryClient { /// Construct from a JWT token (sourced from `authSlice.token` in the Redux store). /// Returns `None` if the token is empty or client construction fails. pub fn from_token(jwt_token: String) -> Option { - log::info!("[memory] from_token: entry (token_len={})", jwt_token.trim().len()); + log::info!( + "[memory] from_token: entry (token_len={})", + jwt_token.trim().len() + ); if jwt_token.trim().is_empty() { log::warn!("[memory] from_token: exit — token is empty, returning None"); return None; } - let config = if let Ok(base_url) = std::env::var("OPENHUMAN_BASE_URL") - .or_else(|_| std::env::var("TINYHUMANS_BASE_URL")) + let config = if let Ok(base_url) = + std::env::var("OPENHUMAN_BASE_URL").or_else(|_| std::env::var("TINYHUMANS_BASE_URL")) { - log::info!("[memory] from_token: constructing client (base_url={base_url}, source=memory_env)"); + log::info!( + "[memory] from_token: constructing client (base_url={base_url}, source=memory_env)" + ); TinyHumanConfig::new(jwt_token).with_base_url(base_url) } else { let backend_url = crate::utils::config::get_backend_url(); @@ -120,11 +125,7 @@ impl MemoryClient { match self.inner.ingestion_job_status(&job_id).await { Ok(status_resp) => { - let state = status_resp - .data - .state - .as_deref() - .unwrap_or("unknown"); + let state = status_resp.data.state.as_deref().unwrap_or("unknown"); log::info!( "[memory] ingestion job status: job_id={job_id}, state={state}, \ @@ -199,11 +200,16 @@ impl MemoryClient { }) .await .map_err(|e| { - log::warn!("[memory] query_skill_context: exit — error (namespace={namespace}): {e}"); + log::warn!( + "[memory] query_skill_context: exit — error (namespace={namespace}): {e}" + ); format!("Memory query failed: {e}") })?; let response = res.data.response.unwrap_or_default(); - log::info!("[memory] query_skill_context: exit — ok (namespace={namespace}, response_len={})", response.len()); + log::info!( + "[memory] query_skill_context: exit — ok (namespace={namespace}, response_len={})", + response.len() + ); Ok(response) } @@ -227,7 +233,9 @@ impl MemoryClient { }) .await .map_err(|e| { - log::warn!("[memory] recall_skill_context: exit — error (namespace={namespace}): {e}"); + log::warn!( + "[memory] recall_skill_context: exit — error (namespace={namespace}): {e}" + ); format!("Memory recall failed: {e}") })?; let response = res.data.context; @@ -247,7 +255,11 @@ impl MemoryClient { } /// Delete a specific document from a namespace. - pub async fn delete_document(&self, document_id: &str, namespace: &str) -> Result { + pub async fn delete_document( + &self, + document_id: &str, + namespace: &str, + ) -> Result { self.inner .delete_document(document_id, namespace) .await @@ -300,7 +312,8 @@ impl MemoryClient { let namespace = format!("skill:{skill_id}:{integration_id}"); log::info!("[memory] clear_skill_memory: entry (namespace={namespace})"); log::debug!("[memory] clear_skill_memory: payload → namespace={namespace}"); - let result = self.inner + let result = self + .inner .delete_memory(DeleteMemoryParams { namespace: Some(namespace.clone()), }) @@ -309,7 +322,9 @@ impl MemoryClient { .map_err(|e| format!("Memory delete failed: {e}")); match &result { Ok(()) => log::info!("[memory] clear_skill_memory: exit — ok (namespace={namespace})"), - Err(e) => log::warn!("[memory] clear_skill_memory: exit — error (namespace={namespace}): {e}"), + Err(e) => { + log::warn!("[memory] clear_skill_memory: exit — error (namespace={namespace}): {e}") + } } result } @@ -350,11 +365,9 @@ mod tests { #[tokio::test] #[ignore] async fn test_memory_skill_sync_flow() { - let jwt_token = - std::env::var("JWT_TOKEN").expect("JWT_TOKEN must be set"); + let jwt_token = std::env::var("JWT_TOKEN").expect("JWT_TOKEN must be set"); - let client = MemoryClient::from_token(jwt_token) - .expect("client creation failed"); + let client = MemoryClient::from_token(jwt_token).expect("client creation failed"); let skill_id = "gmail"; let integration_id = "test@openhuman.dev"; diff --git a/src-tauri/src/openhuman/agent/agent.rs b/src-tauri/src/openhuman/agent/agent.rs index 02202ad52..6e9953be9 100644 --- a/src-tauri/src/openhuman/agent/agent.rs +++ b/src-tauri/src/openhuman/agent/agent.rs @@ -133,7 +133,10 @@ impl AgentBuilder { self } - pub fn identity_config(mut self, identity_config: crate::openhuman::config::IdentityConfig) -> Self { + pub fn identity_config( + mut self, + identity_config: crate::openhuman::config::IdentityConfig, + ) -> Self { self.identity_config = Some(identity_config); self } @@ -664,7 +667,10 @@ mod tests { serde_json::json!({"type": "object"}) } - async fn execute(&self, _args: serde_json::Value) -> Result { + async fn execute( + &self, + _args: serde_json::Value, + ) -> Result { Ok(crate::openhuman::tools::ToolResult { success: true, output: "tool-out".into(), @@ -687,10 +693,16 @@ mod tests { ..crate::openhuman::config::MemoryConfig::default() }; let mem: Arc = Arc::from( - crate::openhuman::memory::create_memory(&memory_cfg, std::path::Path::new("/tmp"), None).unwrap(), + crate::openhuman::memory::create_memory( + &memory_cfg, + std::path::Path::new("/tmp"), + None, + ) + .unwrap(), ); - let observer: Arc = Arc::from(crate::openhuman::observability::NoopObserver {}); + let observer: Arc = + Arc::from(crate::openhuman::observability::NoopObserver {}); let mut agent = Agent::builder() .provider(provider) .tools(vec![Box::new(MockTool)]) @@ -729,10 +741,16 @@ mod tests { ..crate::openhuman::config::MemoryConfig::default() }; let mem: Arc = Arc::from( - crate::openhuman::memory::create_memory(&memory_cfg, std::path::Path::new("/tmp"), None).unwrap(), + crate::openhuman::memory::create_memory( + &memory_cfg, + std::path::Path::new("/tmp"), + None, + ) + .unwrap(), ); - let observer: Arc = Arc::from(crate::openhuman::observability::NoopObserver {}); + let observer: Arc = + Arc::from(crate::openhuman::observability::NoopObserver {}); let mut agent = Agent::builder() .provider(provider) .tools(vec![Box::new(MockTool)]) diff --git a/src-tauri/src/openhuman/agent/dispatcher.rs b/src-tauri/src/openhuman/agent/dispatcher.rs index 2592ff090..fceff39bd 100644 --- a/src-tauri/src/openhuman/agent/dispatcher.rs +++ b/src-tauri/src/openhuman/agent/dispatcher.rs @@ -1,4 +1,6 @@ -use crate::openhuman::providers::{ChatMessage, ChatResponse, ConversationMessage, ToolResultMessage}; +use crate::openhuman::providers::{ + ChatMessage, ChatResponse, ConversationMessage, ToolResultMessage, +}; use crate::openhuman::tools::{Tool, ToolSpec}; use serde_json::Value; use std::fmt::Write; diff --git a/src-tauri/src/openhuman/agent/loop_.rs b/src-tauri/src/openhuman/agent/loop_.rs index 3d8a9a308..8599e3f36 100644 --- a/src-tauri/src/openhuman/agent/loop_.rs +++ b/src-tauri/src/openhuman/agent/loop_.rs @@ -966,7 +966,9 @@ pub(crate) async fn run_tool_call_loop( model: model.to_string(), duration: llm_started_at.elapsed(), success: false, - error_message: Some(crate::openhuman::providers::sanitize_api_error(&e.to_string())), + error_message: Some(crate::openhuman::providers::sanitize_api_error( + &e.to_string(), + )), }); return Err(e); } @@ -1567,7 +1569,10 @@ pub async fn run( final_output = response.clone(); if let Err(e) = crate::openhuman::channels::Channel::send( &cli, - &crate::openhuman::channels::traits::SendMessage::new(format!("\n{response}\n"), "user"), + &crate::openhuman::channels::traits::SendMessage::new( + format!("\n{response}\n"), + "user", + ), ) .await { @@ -2792,7 +2797,10 @@ browser_open/url>https://example.com"#; fn parse_tool_calls_closing_tag_only_returns_text() { let response = "Some text more text"; let (text, calls) = parse_tool_calls(response); - assert!(calls.is_empty(), "closing tag only should not produce calls"); + assert!( + calls.is_empty(), + "closing tag only should not produce calls" + ); assert!( !text.is_empty(), "text around orphaned closing tag should be preserved" @@ -2841,7 +2849,11 @@ browser_open/url>https://example.com"#; Let me check the result."#; let (text, calls) = parse_tool_calls(response); - assert_eq!(calls.len(), 1, "should extract one tool call from mixed content"); + assert_eq!( + calls.len(), + 1, + "should extract one tool call from mixed content" + ); assert_eq!(calls[0].name, "shell"); assert!( text.contains("help you"), @@ -2863,7 +2875,10 @@ Let me check the result."#; fn scrub_credentials_no_sensitive_data() { let input = "normal text without any secrets"; let result = scrub_credentials(input); - assert_eq!(result, input, "non-sensitive text should pass through unchanged"); + assert_eq!( + result, input, + "non-sensitive text should pass through unchanged" + ); } #[test] @@ -2887,7 +2902,9 @@ Let me check the result."#; #[test] fn trim_history_system_only() { - let mut history = vec![crate::openhuman::providers::ChatMessage::system("system prompt")]; + let mut history = vec![crate::openhuman::providers::ChatMessage::system( + "system prompt", + )]; trim_history(&mut history, 10); assert_eq!(history.len(), 1); assert_eq!(history[0].role, "system"); diff --git a/src-tauri/src/openhuman/channels/dingtalk.rs b/src-tauri/src/openhuman/channels/dingtalk.rs index a1af78641..b0c9abef2 100644 --- a/src-tauri/src/openhuman/channels/dingtalk.rs +++ b/src-tauri/src/openhuman/channels/dingtalk.rs @@ -330,7 +330,8 @@ client_id = "app_id_123" client_secret = "secret_456" allowed_users = ["user1", "*"] "#; - let config: crate::openhuman::config::schema::DingTalkConfig = toml::from_str(toml_str).unwrap(); + let config: crate::openhuman::config::schema::DingTalkConfig = + toml::from_str(toml_str).unwrap(); assert_eq!(config.client_id, "app_id_123"); assert_eq!(config.client_secret, "secret_456"); assert_eq!(config.allowed_users, vec!["user1", "*"]); @@ -342,7 +343,8 @@ allowed_users = ["user1", "*"] client_id = "id" client_secret = "secret" "#; - let config: crate::openhuman::config::schema::DingTalkConfig = toml::from_str(toml_str).unwrap(); + let config: crate::openhuman::config::schema::DingTalkConfig = + toml::from_str(toml_str).unwrap(); assert!(config.allowed_users.is_empty()); } diff --git a/src-tauri/src/openhuman/channels/discord.rs b/src-tauri/src/openhuman/channels/discord.rs index 0671ae40b..ad0bbe422 100644 --- a/src-tauri/src/openhuman/channels/discord.rs +++ b/src-tauri/src/openhuman/channels/discord.rs @@ -857,7 +857,10 @@ mod tests { msg.push_str(&"x".repeat(1990)); msg.push_str("\n```\nMore text after code block"); let parts = split_message_for_discord(&msg); - assert!(parts.len() >= 2, "code block spanning boundary should split"); + assert!( + parts.len() >= 2, + "code block spanning boundary should split" + ); for part in &parts { assert!( part.len() <= DISCORD_MAX_MESSAGE_LENGTH, diff --git a/src-tauri/src/openhuman/channels/prompt.rs b/src-tauri/src/openhuman/channels/prompt.rs index 384343cba..8b94cd45d 100644 --- a/src-tauri/src/openhuman/channels/prompt.rs +++ b/src-tauri/src/openhuman/channels/prompt.rs @@ -124,7 +124,11 @@ pub fn build_system_prompt( for skill in skills { let _ = writeln!(prompt, " "); let _ = writeln!(prompt, " {}", skill.name); - let _ = writeln!(prompt, " {}", skill.description); + let _ = writeln!( + prompt, + " {}", + skill.description + ); let location = skill.location.clone().unwrap_or_else(|| { workspace_dir .join("skills") @@ -217,7 +221,12 @@ pub fn build_system_prompt( } /// Inject a single workspace file into the prompt with truncation and missing-file markers. -fn inject_workspace_file(prompt: &mut String, workspace_dir: &Path, filename: &str, max_chars: usize) { +fn inject_workspace_file( + prompt: &mut String, + workspace_dir: &Path, + filename: &str, + max_chars: usize, +) { use std::fmt::Write; let path = workspace_dir.join(filename); diff --git a/src-tauri/src/openhuman/channels/runtime/dispatch.rs b/src-tauri/src/openhuman/channels/runtime/dispatch.rs index 31e440fdb..5399a90db 100644 --- a/src-tauri/src/openhuman/channels/runtime/dispatch.rs +++ b/src-tauri/src/openhuman/channels/runtime/dispatch.rs @@ -1,16 +1,16 @@ //! Channel runtime loop and message processing. +use crate::openhuman::agent::loop_::run_tool_call_loop; use crate::openhuman::channels::context::{ - build_memory_context, compact_sender_history, conversation_history_key, conversation_memory_key, - is_context_window_overflow_error, ChannelRuntimeContext, CHANNEL_TYPING_REFRESH_INTERVAL_SECS, - MAX_CHANNEL_HISTORY, + build_memory_context, compact_sender_history, conversation_history_key, + conversation_memory_key, is_context_window_overflow_error, ChannelRuntimeContext, + CHANNEL_TYPING_REFRESH_INTERVAL_SECS, MAX_CHANNEL_HISTORY, }; use crate::openhuman::channels::routes::{ get_or_create_provider, get_route_selection, handle_runtime_command_if_needed, }; use crate::openhuman::channels::traits; use crate::openhuman::channels::{Channel, SendMessage}; -use crate::openhuman::agent::loop_::run_tool_call_loop; use crate::openhuman::providers::{self, ChatMessage}; use crate::openhuman::util::truncate_with_ellipsis; use std::sync::Arc; diff --git a/src-tauri/src/openhuman/channels/runtime/startup.rs b/src-tauri/src/openhuman/channels/runtime/startup.rs index ed489b700..cab7cc5cb 100644 --- a/src-tauri/src/openhuman/channels/runtime/startup.rs +++ b/src-tauri/src/openhuman/channels/runtime/startup.rs @@ -1,12 +1,12 @@ //! Channel startup wiring. +use super::dispatch::run_message_dispatch_loop; +use super::supervision::{compute_max_in_flight_messages, spawn_supervised_listener}; +use crate::openhuman::agent::loop_::build_tool_instructions; use crate::openhuman::channels::context::{ effective_channel_message_timeout_secs, ChannelRuntimeContext, DEFAULT_CHANNEL_INITIAL_BACKOFF_SECS, DEFAULT_CHANNEL_MAX_BACKOFF_SECS, }; -use super::dispatch::run_message_dispatch_loop; -use super::supervision::{compute_max_in_flight_messages, spawn_supervised_listener}; -use crate::openhuman::agent::loop_::build_tool_instructions; use crate::openhuman::channels::dingtalk::DingTalkChannel; use crate::openhuman::channels::discord::DiscordChannel; use crate::openhuman::channels::email_channel::EmailChannel; diff --git a/src-tauri/src/openhuman/channels/runtime/supervision.rs b/src-tauri/src/openhuman/channels/runtime/supervision.rs index 006ff5961..85b778f17 100644 --- a/src-tauri/src/openhuman/channels/runtime/supervision.rs +++ b/src-tauri/src/openhuman/channels/runtime/supervision.rs @@ -1,11 +1,10 @@ //! Supervisor helpers for channel listeners. +use super::super::context::{ + CHANNEL_MAX_IN_FLIGHT_MESSAGES, CHANNEL_MIN_IN_FLIGHT_MESSAGES, CHANNEL_PARALLELISM_PER_CHANNEL, +}; use super::super::traits; use super::super::Channel; -use super::super::context::{ - CHANNEL_MAX_IN_FLIGHT_MESSAGES, CHANNEL_MIN_IN_FLIGHT_MESSAGES, - CHANNEL_PARALLELISM_PER_CHANNEL, -}; use std::sync::Arc; use std::time::Duration; diff --git a/src-tauri/src/openhuman/channels/signal.rs b/src-tauri/src/openhuman/channels/signal.rs index f5406dcd6..fc184a389 100644 --- a/src-tauri/src/openhuman/channels/signal.rs +++ b/src-tauri/src/openhuman/channels/signal.rs @@ -92,7 +92,8 @@ impl SignalChannel { fn http_client(&self) -> Client { let builder = Client::builder().connect_timeout(Duration::from_secs(10)); - let builder = crate::openhuman::config::apply_runtime_proxy_to_builder(builder, "channel.signal"); + let builder = + crate::openhuman::config::apply_runtime_proxy_to_builder(builder, "channel.signal"); builder.build().expect("Signal HTTP client should build") } diff --git a/src-tauri/src/openhuman/channels/telegram.rs b/src-tauri/src/openhuman/channels/telegram.rs index f106c65d1..9ea508047 100644 --- a/src-tauri/src/openhuman/channels/telegram.rs +++ b/src-tauri/src/openhuman/channels/telegram.rs @@ -45,10 +45,7 @@ fn split_message_for_telegram(message: &str) -> Vec { pos + 1 } else { // Try space as fallback - search_area - .rfind(' ') - .unwrap_or(hard_split) - + 1 + search_area.rfind(' ').unwrap_or(hard_split) + 1 } } else if let Some(pos) = search_area.rfind(' ') { pos + 1 @@ -2844,7 +2841,10 @@ mod tests { msg.push_str(&"x".repeat(4085)); msg.push_str("\n```\nMore text after code block"); let parts = split_message_for_telegram(&msg); - assert!(parts.len() >= 2, "code block spanning boundary should split"); + assert!( + parts.len() >= 2, + "code block spanning boundary should split" + ); for part in &parts { assert!( part.len() <= TELEGRAM_MAX_MESSAGE_LENGTH, diff --git a/src-tauri/src/openhuman/channels/tests/common.rs b/src-tauri/src/openhuman/channels/tests/common.rs index 562b66054..073db2d8d 100644 --- a/src-tauri/src/openhuman/channels/tests/common.rs +++ b/src-tauri/src/openhuman/channels/tests/common.rs @@ -11,7 +11,11 @@ pub(super) fn make_workspace() -> TempDir { let tmp = TempDir::new().unwrap(); // Create minimal workspace files std::fs::write(tmp.path().join("SOUL.md"), "# Soul\nBe helpful.").unwrap(); - std::fs::write(tmp.path().join("IDENTITY.md"), "# Identity\nName: OpenHuman").unwrap(); + std::fs::write( + tmp.path().join("IDENTITY.md"), + "# Identity\nName: OpenHuman", + ) + .unwrap(); std::fs::write(tmp.path().join("USER.md"), "# User\nName: Test User").unwrap(); std::fs::write( tmp.path().join("AGENTS.md"), diff --git a/src-tauri/src/openhuman/channels/tests/health.rs b/src-tauri/src/openhuman/channels/tests/health.rs index f4995af81..4841f4baa 100644 --- a/src-tauri/src/openhuman/channels/tests/health.rs +++ b/src-tauri/src/openhuman/channels/tests/health.rs @@ -1,7 +1,7 @@ -use super::common::AlwaysFailChannel; use super::super::commands::{classify_health_result, ChannelHealthState}; use super::super::runtime::spawn_supervised_listener; use super::super::{traits, Channel, SendMessage}; +use super::common::AlwaysFailChannel; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use std::time::Duration; diff --git a/src-tauri/src/openhuman/channels/tests/identity.rs b/src-tauri/src/openhuman/channels/tests/identity.rs index 118a3e56e..683b023fc 100644 --- a/src-tauri/src/openhuman/channels/tests/identity.rs +++ b/src-tauri/src/openhuman/channels/tests/identity.rs @@ -1,5 +1,5 @@ -use super::common::make_workspace; use super::super::prompt::build_system_prompt; +use super::common::make_workspace; #[test] fn aieos_identity_from_file() { diff --git a/src-tauri/src/openhuman/channels/tests/memory.rs b/src-tauri/src/openhuman/channels/tests/memory.rs index 0279c3eb9..3f3a78b1f 100644 --- a/src-tauri/src/openhuman/channels/tests/memory.rs +++ b/src-tauri/src/openhuman/channels/tests/memory.rs @@ -1,7 +1,10 @@ -use super::common::{HistoryCaptureProvider, NoopMemory, RecordingChannel}; -use super::super::context::{build_memory_context, conversation_memory_key, ChannelRuntimeContext, CHANNEL_MESSAGE_TIMEOUT_SECS, MAX_CHANNEL_HISTORY}; +use super::super::context::{ + build_memory_context, conversation_memory_key, ChannelRuntimeContext, + CHANNEL_MESSAGE_TIMEOUT_SECS, MAX_CHANNEL_HISTORY, +}; use super::super::runtime::process_channel_message; use super::super::{traits, Channel}; +use super::common::{HistoryCaptureProvider, NoopMemory, RecordingChannel}; use crate::openhuman::memory::{Memory, MemoryCategory, SqliteMemory}; use crate::openhuman::observability::NoopObserver; use crate::openhuman::providers::{self, ChatMessage, Provider}; diff --git a/src-tauri/src/openhuman/channels/tests/prompt.rs b/src-tauri/src/openhuman/channels/tests/prompt.rs index e09b2c6df..a7a9bfcdd 100644 --- a/src-tauri/src/openhuman/channels/tests/prompt.rs +++ b/src-tauri/src/openhuman/channels/tests/prompt.rs @@ -1,5 +1,5 @@ -use super::common::make_workspace; use super::super::prompt::{build_system_prompt, BOOTSTRAP_MAX_CHARS}; +use super::common::make_workspace; use tempfile::TempDir; #[test] @@ -205,7 +205,8 @@ fn channel_log_truncation_is_utf8_safe_for_multibyte_text() { let msg = "Hello from OpenHuman 🌍. Current status is healthy, and café-style UTF-8 text stays safe in logs."; // Reproduces the production crash path where channel logs truncate at 80 chars. - let result = std::panic::catch_unwind(|| crate::openhuman::util::truncate_with_ellipsis(msg, 80)); + let result = + std::panic::catch_unwind(|| crate::openhuman::util::truncate_with_ellipsis(msg, 80)); assert!( result.is_ok(), "truncate_with_ellipsis should never panic on UTF-8" diff --git a/src-tauri/src/openhuman/channels/tests/runtime_dispatch.rs b/src-tauri/src/openhuman/channels/tests/runtime_dispatch.rs index 94a18b769..d58d5e804 100644 --- a/src-tauri/src/openhuman/channels/tests/runtime_dispatch.rs +++ b/src-tauri/src/openhuman/channels/tests/runtime_dispatch.rs @@ -1,7 +1,7 @@ -use super::common::{NoopMemory, RecordingChannel, SlowProvider}; use super::super::context::{ChannelRuntimeContext, CHANNEL_MESSAGE_TIMEOUT_SECS}; use super::super::runtime::{process_channel_message, run_message_dispatch_loop}; use super::super::{traits, Channel}; +use super::common::{NoopMemory, RecordingChannel, SlowProvider}; use crate::openhuman::observability::NoopObserver; use crate::openhuman::providers::{self, Provider}; use std::collections::HashMap; @@ -9,132 +9,131 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; - #[tokio::test] async fn message_dispatch_processes_messages_in_parallel() { -let channel_impl = Arc::new(RecordingChannel::default()); -let channel: Arc = channel_impl.clone(); + let channel_impl = Arc::new(RecordingChannel::default()); + let channel: Arc = channel_impl.clone(); -let mut channels_by_name = HashMap::new(); -channels_by_name.insert(channel.name().to_string(), channel); + let mut channels_by_name = HashMap::new(); + channels_by_name.insert(channel.name().to_string(), channel); -let runtime_ctx = Arc::new(ChannelRuntimeContext { - channels_by_name: Arc::new(channels_by_name), - provider: Arc::new(SlowProvider { - delay: Duration::from_millis(250), - }), - default_provider: Arc::new("test-provider".to_string()), - memory: Arc::new(NoopMemory), - tools_registry: Arc::new(vec![]), - observer: Arc::new(NoopObserver), - system_prompt: Arc::new("test-system-prompt".to_string()), - model: Arc::new("test-model".to_string()), - temperature: 0.0, - auto_save_memory: false, - max_tool_iterations: 10, - min_relevance_score: 0.0, - conversation_histories: Arc::new(Mutex::new(HashMap::new())), - provider_cache: Arc::new(Mutex::new(HashMap::new())), - route_overrides: Arc::new(Mutex::new(HashMap::new())), - api_key: None, - api_url: None, - reliability: Arc::new(crate::openhuman::config::ReliabilityConfig::default()), - provider_runtime_options: providers::ProviderRuntimeOptions::default(), - workspace_dir: Arc::new(std::env::temp_dir()), - message_timeout_secs: CHANNEL_MESSAGE_TIMEOUT_SECS, - multimodal: crate::openhuman::config::MultimodalConfig::default(), -}); + let runtime_ctx = Arc::new(ChannelRuntimeContext { + channels_by_name: Arc::new(channels_by_name), + provider: Arc::new(SlowProvider { + delay: Duration::from_millis(250), + }), + default_provider: Arc::new("test-provider".to_string()), + memory: Arc::new(NoopMemory), + tools_registry: Arc::new(vec![]), + observer: Arc::new(NoopObserver), + system_prompt: Arc::new("test-system-prompt".to_string()), + model: Arc::new("test-model".to_string()), + temperature: 0.0, + auto_save_memory: false, + max_tool_iterations: 10, + min_relevance_score: 0.0, + conversation_histories: Arc::new(Mutex::new(HashMap::new())), + provider_cache: Arc::new(Mutex::new(HashMap::new())), + route_overrides: Arc::new(Mutex::new(HashMap::new())), + api_key: None, + api_url: None, + reliability: Arc::new(crate::openhuman::config::ReliabilityConfig::default()), + provider_runtime_options: providers::ProviderRuntimeOptions::default(), + workspace_dir: Arc::new(std::env::temp_dir()), + message_timeout_secs: CHANNEL_MESSAGE_TIMEOUT_SECS, + multimodal: crate::openhuman::config::MultimodalConfig::default(), + }); -let (tx, rx) = tokio::sync::mpsc::channel::(4); -tx.send(traits::ChannelMessage { - id: "1".to_string(), - sender: "alice".to_string(), - reply_target: "alice".to_string(), - content: "hello".to_string(), - channel: "test-channel".to_string(), - timestamp: 1, - thread_ts: None, -}) -.await -.unwrap(); -tx.send(traits::ChannelMessage { - id: "2".to_string(), - sender: "bob".to_string(), - reply_target: "bob".to_string(), - content: "world".to_string(), - channel: "test-channel".to_string(), - timestamp: 2, - thread_ts: None, -}) -.await -.unwrap(); -drop(tx); - -let started = Instant::now(); -run_message_dispatch_loop(rx, runtime_ctx, 2).await; -let elapsed = started.elapsed(); - -assert!( - elapsed < Duration::from_millis(430), - "expected parallel dispatch (<430ms), got {:?}", - elapsed -); - -let sent_messages = channel_impl.sent_messages.lock().await; -assert_eq!(sent_messages.len(), 2); -} - -#[tokio::test] -async fn process_channel_message_cancels_scoped_typing_task() { -let channel_impl = Arc::new(RecordingChannel::default()); -let channel: Arc = channel_impl.clone(); - -let mut channels_by_name = HashMap::new(); -channels_by_name.insert(channel.name().to_string(), channel); - -let runtime_ctx = Arc::new(ChannelRuntimeContext { - channels_by_name: Arc::new(channels_by_name), - provider: Arc::new(SlowProvider { - delay: Duration::from_millis(20), - }), - default_provider: Arc::new("test-provider".to_string()), - memory: Arc::new(NoopMemory), - tools_registry: Arc::new(vec![]), - observer: Arc::new(NoopObserver), - system_prompt: Arc::new("test-system-prompt".to_string()), - model: Arc::new("test-model".to_string()), - temperature: 0.0, - auto_save_memory: false, - max_tool_iterations: 10, - min_relevance_score: 0.0, - conversation_histories: Arc::new(Mutex::new(HashMap::new())), - provider_cache: Arc::new(Mutex::new(HashMap::new())), - route_overrides: Arc::new(Mutex::new(HashMap::new())), - api_key: None, - api_url: None, - reliability: Arc::new(crate::openhuman::config::ReliabilityConfig::default()), - provider_runtime_options: providers::ProviderRuntimeOptions::default(), - workspace_dir: Arc::new(std::env::temp_dir()), - message_timeout_secs: CHANNEL_MESSAGE_TIMEOUT_SECS, - multimodal: crate::openhuman::config::MultimodalConfig::default(), -}); - -process_channel_message( - runtime_ctx, - traits::ChannelMessage { - id: "typing-msg".to_string(), + let (tx, rx) = tokio::sync::mpsc::channel::(4); + tx.send(traits::ChannelMessage { + id: "1".to_string(), sender: "alice".to_string(), - reply_target: "chat-typing".to_string(), + reply_target: "alice".to_string(), content: "hello".to_string(), channel: "test-channel".to_string(), timestamp: 1, thread_ts: None, - }, -) -.await; + }) + .await + .unwrap(); + tx.send(traits::ChannelMessage { + id: "2".to_string(), + sender: "bob".to_string(), + reply_target: "bob".to_string(), + content: "world".to_string(), + channel: "test-channel".to_string(), + timestamp: 2, + thread_ts: None, + }) + .await + .unwrap(); + drop(tx); -let starts = channel_impl.start_typing_calls.load(Ordering::SeqCst); -let stops = channel_impl.stop_typing_calls.load(Ordering::SeqCst); -assert_eq!(starts, 1, "start_typing should be called once"); -assert_eq!(stops, 1, "stop_typing should be called once"); + let started = Instant::now(); + run_message_dispatch_loop(rx, runtime_ctx, 2).await; + let elapsed = started.elapsed(); + + assert!( + elapsed < Duration::from_millis(430), + "expected parallel dispatch (<430ms), got {:?}", + elapsed + ); + + let sent_messages = channel_impl.sent_messages.lock().await; + assert_eq!(sent_messages.len(), 2); +} + +#[tokio::test] +async fn process_channel_message_cancels_scoped_typing_task() { + let channel_impl = Arc::new(RecordingChannel::default()); + let channel: Arc = channel_impl.clone(); + + let mut channels_by_name = HashMap::new(); + channels_by_name.insert(channel.name().to_string(), channel); + + let runtime_ctx = Arc::new(ChannelRuntimeContext { + channels_by_name: Arc::new(channels_by_name), + provider: Arc::new(SlowProvider { + delay: Duration::from_millis(20), + }), + default_provider: Arc::new("test-provider".to_string()), + memory: Arc::new(NoopMemory), + tools_registry: Arc::new(vec![]), + observer: Arc::new(NoopObserver), + system_prompt: Arc::new("test-system-prompt".to_string()), + model: Arc::new("test-model".to_string()), + temperature: 0.0, + auto_save_memory: false, + max_tool_iterations: 10, + min_relevance_score: 0.0, + conversation_histories: Arc::new(Mutex::new(HashMap::new())), + provider_cache: Arc::new(Mutex::new(HashMap::new())), + route_overrides: Arc::new(Mutex::new(HashMap::new())), + api_key: None, + api_url: None, + reliability: Arc::new(crate::openhuman::config::ReliabilityConfig::default()), + provider_runtime_options: providers::ProviderRuntimeOptions::default(), + workspace_dir: Arc::new(std::env::temp_dir()), + message_timeout_secs: CHANNEL_MESSAGE_TIMEOUT_SECS, + multimodal: crate::openhuman::config::MultimodalConfig::default(), + }); + + process_channel_message( + runtime_ctx, + traits::ChannelMessage { + id: "typing-msg".to_string(), + sender: "alice".to_string(), + reply_target: "chat-typing".to_string(), + content: "hello".to_string(), + channel: "test-channel".to_string(), + timestamp: 1, + thread_ts: None, + }, + ) + .await; + + let starts = channel_impl.start_typing_calls.load(Ordering::SeqCst); + let stops = channel_impl.stop_typing_calls.load(Ordering::SeqCst); + assert_eq!(starts, 1, "start_typing should be called once"); + assert_eq!(stops, 1, "stop_typing should be called once"); } diff --git a/src-tauri/src/openhuman/channels/tests/runtime_tool_calls.rs b/src-tauri/src/openhuman/channels/tests/runtime_tool_calls.rs index a61b1217a..caeaafd73 100644 --- a/src-tauri/src/openhuman/channels/tests/runtime_tool_calls.rs +++ b/src-tauri/src/openhuman/channels/tests/runtime_tool_calls.rs @@ -1,11 +1,13 @@ -use super::common::{ - HistoryCaptureProvider, IterativeToolProvider, MockPriceTool, ModelCaptureProvider, - NoopMemory, RecordingChannel, SlowProvider, TelegramRecordingChannel, ToolCallingAliasProvider, - ToolCallingProvider, +use super::super::context::{ + ChannelRouteSelection, ChannelRuntimeContext, CHANNEL_MESSAGE_TIMEOUT_SECS, }; -use super::super::context::{ChannelRuntimeContext, ChannelRouteSelection, CHANNEL_MESSAGE_TIMEOUT_SECS}; use super::super::runtime::{process_channel_message, run_message_dispatch_loop}; use super::super::{traits, Channel, SendMessage}; +use super::common::{ + HistoryCaptureProvider, IterativeToolProvider, MockPriceTool, ModelCaptureProvider, NoopMemory, + RecordingChannel, SlowProvider, TelegramRecordingChannel, ToolCallingAliasProvider, + ToolCallingProvider, +}; use crate::openhuman::observability::NoopObserver; use crate::openhuman::providers::{self, ChatMessage, Provider}; use crate::openhuman::tools::Tool; diff --git a/src-tauri/src/openhuman/config/mod.rs b/src-tauri/src/openhuman/config/mod.rs index d38110e3b..b54420c83 100644 --- a/src-tauri/src/openhuman/config/mod.rs +++ b/src-tauri/src/openhuman/config/mod.rs @@ -1,5 +1,5 @@ -pub mod schema; pub mod daemon; +pub mod schema; #[allow(unused_imports)] pub use daemon::DaemonConfig; @@ -9,11 +9,11 @@ pub use schema::{ apply_runtime_proxy_to_builder, build_runtime_proxy_client, build_runtime_proxy_client_with_timeouts, runtime_proxy_config, set_runtime_proxy_config, AgentConfig, AuditConfig, AutonomyConfig, BrowserComputerUseConfig, BrowserConfig, - ChannelsConfig, ClassificationRule, CloudflareTunnelConfig, ComposioConfig, Config, - CostConfig, CronConfig, CustomTunnelConfig, DelegateAgentConfig, DiscordConfig, - DockerRuntimeConfig, EmbeddingRouteConfig, GatewayConfig, HardwareConfig, HardwareTransport, - HeartbeatConfig, HttpRequestConfig, IMessageConfig, IdentityConfig, LarkConfig, MatrixConfig, - MemoryConfig, ModelRouteConfig, MultimodalConfig, NgrokTunnelConfig, ObservabilityConfig, + ChannelsConfig, ClassificationRule, CloudflareTunnelConfig, ComposioConfig, Config, CostConfig, + CronConfig, CustomTunnelConfig, DelegateAgentConfig, DiscordConfig, DockerRuntimeConfig, + EmbeddingRouteConfig, GatewayConfig, HardwareConfig, HardwareTransport, HeartbeatConfig, + HttpRequestConfig, IMessageConfig, IdentityConfig, LarkConfig, MatrixConfig, MemoryConfig, + ModelRouteConfig, MultimodalConfig, NgrokTunnelConfig, ObservabilityConfig, PeripheralBoardConfig, PeripheralsConfig, ProxyConfig, ProxyScope, QueryClassificationConfig, ReliabilityConfig, ResourceLimitsConfig, RuntimeConfig, SandboxBackend, SandboxConfig, SchedulerConfig, SecretsConfig, SecurityConfig, SlackConfig, StorageConfig, diff --git a/src-tauri/src/openhuman/config/schema/autonomy.rs b/src-tauri/src/openhuman/config/schema/autonomy.rs index 45a46edd2..4f2a8a2dd 100644 --- a/src-tauri/src/openhuman/config/schema/autonomy.rs +++ b/src-tauri/src/openhuman/config/schema/autonomy.rs @@ -1,7 +1,7 @@ //! Autonomy and security policy configuration. -use crate::openhuman::security::AutonomyLevel; use super::defaults; +use crate::openhuman::security::AutonomyLevel; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; diff --git a/src-tauri/src/openhuman/config/schema/load.rs b/src-tauri/src/openhuman/config/schema/load.rs index 9d29df79d..1ebd87027 100644 --- a/src-tauri/src/openhuman/config/schema/load.rs +++ b/src-tauri/src/openhuman/config/schema/load.rs @@ -307,12 +307,15 @@ impl Config { let contents = fs::read_to_string(&config_path) .await .context("Failed to read config file")?; - let mut config: Config = toml::from_str(&contents) - .with_context(|| format!("Failed to parse config file {}", config_path.display()))?; + let mut config: Config = toml::from_str(&contents).with_context(|| { + format!("Failed to parse config file {}", config_path.display()) + })?; config.config_path = config_path.clone(); config.workspace_dir = workspace_dir; - let store = - crate::openhuman::security::SecretStore::new(&openhuman_dir, config.secrets.encrypt); + let store = crate::openhuman::security::SecretStore::new( + &openhuman_dir, + config.secrets.encrypt, + ); decrypt_optional_secret(&store, &mut config.api_key, "config.api_key")?; decrypt_optional_secret( &store, @@ -396,9 +399,10 @@ impl Config { self.default_provider = Some(provider); } } else if let Ok(provider) = std::env::var("PROVIDER") { - let should_apply_legacy_provider = self.default_provider.as_deref().map_or(true, |c| { - c.trim().eq_ignore_ascii_case("openrouter") - }); + let should_apply_legacy_provider = self + .default_provider + .as_deref() + .map_or(true, |c| c.trim().eq_ignore_ascii_case("openrouter")); if should_apply_legacy_provider && !provider.is_empty() { self.default_provider = Some(provider); } @@ -412,7 +416,8 @@ impl Config { if let Ok(workspace) = std::env::var("OPENHUMAN_WORKSPACE") { if !workspace.is_empty() { - let (_, workspace_dir) = resolve_config_dir_for_workspace(&PathBuf::from(workspace)); + let (_, workspace_dir) = + resolve_config_dir_for_workspace(&PathBuf::from(workspace)); self.workspace_dir = workspace_dir; } } @@ -425,8 +430,7 @@ impl Config { } } - if let Ok(host) = - std::env::var("OPENHUMAN_GATEWAY_HOST").or_else(|_| std::env::var("HOST")) + if let Ok(host) = std::env::var("OPENHUMAN_GATEWAY_HOST").or_else(|_| std::env::var("HOST")) { if !host.is_empty() { self.gateway.host = host; diff --git a/src-tauri/src/openhuman/cron/scheduler.rs b/src-tauri/src/openhuman/cron/scheduler.rs index 631f0a66c..ef57d8fb6 100644 --- a/src-tauri/src/openhuman/cron/scheduler.rs +++ b/src-tauri/src/openhuman/cron/scheduler.rs @@ -95,7 +95,10 @@ async fn process_due_jobs(config: &Config, security: &Arc, jobs: while let Some((job_id, success)) = in_flight.next().await { if !success { - crate::openhuman::health::mark_component_error("scheduler", format!("job {job_id} failed")); + crate::openhuman::health::mark_component_error( + "scheduler", + format!("job {job_id} failed"), + ); } } } diff --git a/src-tauri/src/openhuman/daemon/mod.rs b/src-tauri/src/openhuman/daemon/mod.rs index 11a8a9da8..39ac020c6 100644 --- a/src-tauri/src/openhuman/daemon/mod.rs +++ b/src-tauri/src/openhuman/daemon/mod.rs @@ -62,16 +62,16 @@ pub async fn run( } log::info!("[openhuman] Daemon supervisor started"); - log::info!( - "[openhuman] data_dir: {}", - config.data_dir.display() - ); + log::info!("[openhuman] data_dir: {}", config.data_dir.display()); log::info!( "[openhuman] backoff: {}s initial, {}s max", initial_backoff, max_backoff ); - log::info!("[openhuman] health: Events will be emitted every {}s to frontend", STATUS_FLUSH_SECONDS); + log::info!( + "[openhuman] health: Events will be emitted every {}s to frontend", + STATUS_FLUSH_SECONDS + ); // Wait for cancellation (Tauri exit) cancel.cancelled().await; @@ -107,11 +107,10 @@ pub async fn run_full( crate::openhuman::health::mark_component_ok("daemon"); if config.heartbeat.enabled { - let _ = - crate::openhuman::heartbeat::engine::HeartbeatEngine::ensure_heartbeat_file( - &config.workspace_dir, - ) - .await; + let _ = crate::openhuman::heartbeat::engine::HeartbeatEngine::ensure_heartbeat_file( + &config.workspace_dir, + ) + .await; } let mut handles: Vec> = @@ -127,9 +126,7 @@ pub async fn run_full( move || { let cfg = gateway_cfg.clone(); let host = gateway_host.clone(); - async move { - crate::openhuman::gateway::run_gateway(&host, port, cfg).await - } + async move { crate::openhuman::gateway::run_gateway(&host, port, cfg).await } }, )); } @@ -198,7 +195,6 @@ pub async fn run_full( Ok(()) } - /// Get the path to the daemon state file shared between internal and external processes. pub fn state_file_path(config: &Config) -> PathBuf { config @@ -246,8 +242,7 @@ async fn run_heartbeat_worker(config: Config) -> Result<()> { ); let interval_mins = config.heartbeat.interval_minutes.max(5); - let mut interval = - tokio::time::interval(Duration::from_secs(u64::from(interval_mins) * 60)); + let mut interval = tokio::time::interval(Duration::from_secs(u64::from(interval_mins) * 60)); loop { interval.tick().await; @@ -260,15 +255,9 @@ async fn run_heartbeat_worker(config: Config) -> Result<()> { for task in tasks { let prompt = format!("[Heartbeat Task] {task}"); let temp = config.default_temperature; - if let Err(e) = crate::openhuman::agent::run( - config.clone(), - Some(prompt), - None, - None, - temp, - vec![], - ) - .await + if let Err(e) = + crate::openhuman::agent::run(config.clone(), Some(prompt), None, None, temp, vec![]) + .await { crate::openhuman::health::mark_component_error("heartbeat", e.to_string()); log::warn!("Heartbeat task failed: {e}"); @@ -327,7 +316,10 @@ async fn spawn_state_writer( let _ = tokio::fs::create_dir_all(parent).await; } - log::info!("[openhuman] Health state writer starting ({}s intervals)", STATUS_FLUSH_SECONDS); + log::info!( + "[openhuman] Health state writer starting ({}s intervals)", + STATUS_FLUSH_SECONDS + ); log::info!("[openhuman] Health state file: {}", state_path.display()); let mut interval = tokio::time::interval(Duration::from_secs(STATUS_FLUSH_SECONDS)); @@ -335,17 +327,17 @@ async fn spawn_state_writer( loop { tokio::select! { - _ = interval.tick() => { - event_count += 1; - if event_count % 12 == 1 { // Log every minute (12 * 5s = 60s) -// log::info!("[openhuman] Health monitoring active (event #{})", event_count); + _ = interval.tick() => { + event_count += 1; + if event_count % 12 == 1 { // Log every minute (12 * 5s = 60s) + // log::info!("[openhuman] Health monitoring active (event #{})", event_count); + } + }, + _ = cancel.cancelled() => { + log::info!("[openhuman] Health state writer received shutdown signal"); + break; + } } - }, - _ = cancel.cancelled() => { - log::info!("[openhuman] Health state writer received shutdown signal"); - break; - } - } let mut json = crate::openhuman::health::snapshot_json(); if let Some(obj) = json.as_object_mut() { @@ -353,22 +345,22 @@ async fn spawn_state_writer( "written_at".into(), serde_json::json!(Utc::now().to_rfc3339()), ); - obj.insert( - "event_count".into(), - serde_json::json!(event_count), - ); + obj.insert("event_count".into(), serde_json::json!(event_count)); } // Emit Tauri event for frontend consumption // log::debug!("[openhuman] Emitting health event #{}: {:?}", event_count, json); // Removed noisy log if let Err(e) = app_handle.emit("openhuman:health", &json) { - log::error!("[openhuman] Failed to emit health event #{}: {}", event_count, e); + log::error!( + "[openhuman] Failed to emit health event #{}: {}", + event_count, + e + ); } // log::debug!("[openhuman] Health event #{} emitted successfully", event_count); // Removed noisy log // Also persist to disk - let data = - serde_json::to_vec_pretty(&json).unwrap_or_else(|_| b"{}".to_vec()); + let data = serde_json::to_vec_pretty(&json).unwrap_or_else(|_| b"{}".to_vec()); if let Err(e) = tokio::fs::write(&state_path, data).await { log::debug!("[openhuman] Failed to write health state to disk: {}", e); } @@ -446,8 +438,7 @@ mod tests { #[tokio::test] async fn supervisor_marks_unexpected_exit_as_error() { - let handle = - spawn_component_supervisor("th-daemon-test-exit", 1, 1, || async { Ok(()) }); + let handle = spawn_component_supervisor("th-daemon-test-exit", 1, 1, || async { Ok(()) }); tokio::time::sleep(Duration::from_millis(50)).await; handle.abort(); diff --git a/src-tauri/src/openhuman/doctor/mod.rs b/src-tauri/src/openhuman/doctor/mod.rs index 1be9cb655..4ec4f891f 100644 --- a/src-tauri/src/openhuman/doctor/mod.rs +++ b/src-tauri/src/openhuman/doctor/mod.rs @@ -87,7 +87,11 @@ pub fn run(config: &Config) -> Result { Ok(DoctorReport { items, - summary: DoctorSummary { ok, warnings, errors }, + summary: DoctorSummary { + ok, + warnings, + errors, + }, }) } @@ -618,7 +622,10 @@ fn check_daemon_state(config: &Config, items: &mut Vec) { let raw = match std::fs::read_to_string(&state_file) { Ok(r) => r, Err(e) => { - items.push(DiagnosticItem::error(cat, format!("cannot read state file: {e}"))); + items.push(DiagnosticItem::error( + cat, + format!("cannot read state file: {e}"), + )); return; } }; @@ -626,7 +633,10 @@ fn check_daemon_state(config: &Config, items: &mut Vec) { let snapshot: serde_json::Value = match serde_json::from_str(&raw) { Ok(v) => v, Err(e) => { - items.push(DiagnosticItem::error(cat, format!("invalid state JSON: {e}"))); + items.push(DiagnosticItem::error( + cat, + format!("invalid state JSON: {e}"), + )); return; } }; @@ -642,7 +652,10 @@ fn check_daemon_state(config: &Config, items: &mut Vec) { .signed_duration_since(ts.with_timezone(&Utc)) .num_seconds(); if age <= DAEMON_STALE_SECONDS { - items.push(DiagnosticItem::ok(cat, format!("heartbeat fresh ({age}s ago)"))); + items.push(DiagnosticItem::ok( + cat, + format!("heartbeat fresh ({age}s ago)"), + )); } else { items.push(DiagnosticItem::error( cat, @@ -687,7 +700,10 @@ fn check_daemon_state(config: &Config, items: &mut Vec) { )); } } else { - items.push(DiagnosticItem::warn(cat, "scheduler component not tracked yet")); + items.push(DiagnosticItem::warn( + cat, + "scheduler component not tracked yet", + )); } // Channels @@ -711,7 +727,10 @@ fn check_daemon_state(config: &Config, items: &mut Vec) { }); if status_ok && age <= CHANNEL_STALE_SECONDS { - items.push(DiagnosticItem::ok(cat, format!("{name} fresh ({age}s ago)"))); + items.push(DiagnosticItem::ok( + cat, + format!("{name} fresh ({age}s ago)"), + )); } else { stale += 1; items.push(DiagnosticItem::error( @@ -722,7 +741,10 @@ fn check_daemon_state(config: &Config, items: &mut Vec) { } if channel_count == 0 { - items.push(DiagnosticItem::warn(cat, "no channel components tracked yet")); + items.push(DiagnosticItem::warn( + cat, + "no channel components tracked yet", + )); } else if stale > 0 { items.push(DiagnosticItem::warn( cat, @@ -762,7 +784,12 @@ fn check_environment(items: &mut Vec) { check_command_available("curl", &["--version"], cat, items); } -fn check_command_available(cmd: &str, args: &[&str], cat: &'static str, items: &mut Vec) { +fn check_command_available( + cmd: &str, + args: &[&str], + cat: &'static str, + items: &mut Vec, +) { match std::process::Command::new(cmd) .args(args) .stdout(std::process::Stdio::piped()) diff --git a/src-tauri/src/openhuman/gateway/handlers/health.rs b/src-tauri/src/openhuman/gateway/handlers/health.rs index b8d01ef12..46901d5a6 100644 --- a/src-tauri/src/openhuman/gateway/handlers/health.rs +++ b/src-tauri/src/openhuman/gateway/handlers/health.rs @@ -26,8 +26,8 @@ pub async fn handle_metrics(State(state): State) -> impl IntoResponse .observer .as_ref() .as_any() - .downcast_ref::() - { + .downcast_ref::( + ) { prom.encode() } else { String::from( diff --git a/src-tauri/src/openhuman/gateway/handlers/linq.rs b/src-tauri/src/openhuman/gateway/handlers/linq.rs index 33eab88c5..c926089ea 100644 --- a/src-tauri/src/openhuman/gateway/handlers/linq.rs +++ b/src-tauri/src/openhuman/gateway/handlers/linq.rs @@ -1,8 +1,8 @@ //! Linq webhook handlers. use super::webhook::run_gateway_chat_with_multimodal; -use crate::openhuman::channels::SendMessage; use crate::openhuman::channels::traits::Channel; +use crate::openhuman::channels::SendMessage; use crate::openhuman::gateway::state::AppState; use crate::openhuman::memory::MemoryCategory; use crate::openhuman::util::truncate_with_ellipsis; @@ -48,7 +48,11 @@ pub async fn handle_linq_webhook( ) { tracing::warn!( "Linq webhook signature verification failed (signature: {})", - if signature.is_empty() { "missing" } else { "invalid" } + if signature.is_empty() { + "missing" + } else { + "invalid" + } ); return ( StatusCode::UNAUTHORIZED, diff --git a/src-tauri/src/openhuman/gateway/handlers/pair.rs b/src-tauri/src/openhuman/gateway/handlers/pair.rs index 6bab051e5..32a946e46 100644 --- a/src-tauri/src/openhuman/gateway/handlers/pair.rs +++ b/src-tauri/src/openhuman/gateway/handlers/pair.rs @@ -21,7 +21,8 @@ pub async fn handle_pair( ConnectInfo(peer_addr): ConnectInfo, headers: HeaderMap, ) -> impl IntoResponse { - let rate_key = client_key_from_request(Some(peer_addr), &headers, state.trust_forwarded_headers); + let rate_key = + client_key_from_request(Some(peer_addr), &headers, state.trust_forwarded_headers); if !state.rate_limiter.allow_pair(&rate_key) { tracing::warn!("/pair rate limit exceeded"); let err = serde_json::json!({ diff --git a/src-tauri/src/openhuman/gateway/handlers/webhook.rs b/src-tauri/src/openhuman/gateway/handlers/webhook.rs index 539c1ee22..30168dc8e 100644 --- a/src-tauri/src/openhuman/gateway/handlers/webhook.rs +++ b/src-tauri/src/openhuman/gateway/handlers/webhook.rs @@ -72,7 +72,8 @@ pub async fn handle_webhook( headers: HeaderMap, body: Result, axum::extract::rejection::JsonRejection>, ) -> impl IntoResponse { - let rate_key = client_key_from_request(Some(peer_addr), &headers, state.trust_forwarded_headers); + let rate_key = + client_key_from_request(Some(peer_addr), &headers, state.trust_forwarded_headers); if !state.rate_limiter.allow_webhook(&rate_key) { tracing::warn!("/webhook rate limit exceeded"); let err = serde_json::json!({ @@ -110,8 +111,7 @@ pub async fn handle_webhook( Some(val) if constant_time_eq(&val, secret_hash.as_ref()) => {} _ => { tracing::warn!("Webhook: rejected request — invalid or missing X-Webhook-Secret"); - let err = - serde_json::json!({"error": "Unauthorized — invalid or missing X-Webhook-Secret header"}); + let err = serde_json::json!({"error": "Unauthorized — invalid or missing X-Webhook-Secret header"}); return (StatusCode::UNAUTHORIZED, Json(err)); } } @@ -179,44 +179,44 @@ pub async fn handle_webhook( let model_label = state.model.clone(); let started_at = Instant::now(); - state - .observer - .record_event(&crate::openhuman::observability::ObserverEvent::AgentStart { + state.observer.record_event( + &crate::openhuman::observability::ObserverEvent::AgentStart { provider: provider_label.clone(), model: model_label.clone(), - }); - state - .observer - .record_event(&crate::openhuman::observability::ObserverEvent::LlmRequest { + }, + ); + state.observer.record_event( + &crate::openhuman::observability::ObserverEvent::LlmRequest { provider: provider_label.clone(), model: model_label.clone(), messages_count: 1, - }); + }, + ); match run_gateway_chat_with_multimodal(&state, &provider_label, message).await { Ok(response) => { let duration = started_at.elapsed(); - state - .observer - .record_event(&crate::openhuman::observability::ObserverEvent::LlmResponse { + state.observer.record_event( + &crate::openhuman::observability::ObserverEvent::LlmResponse { provider: provider_label.clone(), model: model_label.clone(), duration, success: true, error_message: None, - }); + }, + ); state.observer.record_metric( &crate::openhuman::observability::traits::ObserverMetric::RequestLatency(duration), ); - state - .observer - .record_event(&crate::openhuman::observability::ObserverEvent::AgentEnd { + state.observer.record_event( + &crate::openhuman::observability::ObserverEvent::AgentEnd { provider: provider_label, model: model_label, duration, tokens_used: None, cost_usd: None, - }); + }, + ); let body = serde_json::json!({"response": response, "model": state.model}); (StatusCode::OK, Json(body)) @@ -225,15 +225,15 @@ pub async fn handle_webhook( let duration = started_at.elapsed(); let sanitized = providers::sanitize_api_error(&e.to_string()); - state - .observer - .record_event(&crate::openhuman::observability::ObserverEvent::LlmResponse { + state.observer.record_event( + &crate::openhuman::observability::ObserverEvent::LlmResponse { provider: provider_label.clone(), model: model_label.clone(), duration, success: false, error_message: Some(sanitized.clone()), - }); + }, + ); state.observer.record_metric( &crate::openhuman::observability::traits::ObserverMetric::RequestLatency(duration), ); @@ -243,15 +243,15 @@ pub async fn handle_webhook( component: "gateway".to_string(), message: sanitized.clone(), }); - state - .observer - .record_event(&crate::openhuman::observability::ObserverEvent::AgentEnd { + state.observer.record_event( + &crate::openhuman::observability::ObserverEvent::AgentEnd { provider: provider_label, model: model_label, duration, tokens_used: None, cost_usd: None, - }); + }, + ); tracing::error!("Webhook provider error: {}", sanitized); let err = serde_json::json!({"error": "LLM request failed"}); diff --git a/src-tauri/src/openhuman/gateway/handlers/whatsapp.rs b/src-tauri/src/openhuman/gateway/handlers/whatsapp.rs index af0833860..e7ffd42ca 100644 --- a/src-tauri/src/openhuman/gateway/handlers/whatsapp.rs +++ b/src-tauri/src/openhuman/gateway/handlers/whatsapp.rs @@ -1,8 +1,8 @@ //! WhatsApp webhook handlers and signature verification. use super::webhook::run_gateway_chat_with_multimodal; -use crate::openhuman::channels::SendMessage; use crate::openhuman::channels::traits::Channel; +use crate::openhuman::channels::SendMessage; use crate::openhuman::gateway::models::WhatsAppVerifyQuery; use crate::openhuman::gateway::state::AppState; use crate::openhuman::memory::MemoryCategory; @@ -91,7 +91,11 @@ pub async fn handle_whatsapp_message( if !verify_whatsapp_signature(app_secret, &body, signature) { tracing::warn!( "WhatsApp webhook signature verification failed (signature: {})", - if signature.is_empty() { "missing" } else { "invalid" } + if signature.is_empty() { + "missing" + } else { + "invalid" + } ); return ( StatusCode::UNAUTHORIZED, diff --git a/src-tauri/src/openhuman/gateway/rate_limit.rs b/src-tauri/src/openhuman/gateway/rate_limit.rs index 8cc5763b7..11800af5c 100644 --- a/src-tauri/src/openhuman/gateway/rate_limit.rs +++ b/src-tauri/src/openhuman/gateway/rate_limit.rs @@ -85,7 +85,8 @@ pub struct GatewayRateLimiter { impl GatewayRateLimiter { pub fn new(pair_per_minute: u32, webhook_per_minute: u32, max_keys: usize) -> Self { - let window = Duration::from_secs(crate::openhuman::gateway::constants::RATE_LIMIT_WINDOW_SECS); + let window = + Duration::from_secs(crate::openhuman::gateway::constants::RATE_LIMIT_WINDOW_SECS); Self { pair: SlidingWindowRateLimiter::new(pair_per_minute, window, max_keys), webhook: SlidingWindowRateLimiter::new(webhook_per_minute, window, max_keys), diff --git a/src-tauri/src/openhuman/gateway/server.rs b/src-tauri/src/openhuman/gateway/server.rs index 9e90fbde9..0d5e88a63 100644 --- a/src-tauri/src/openhuman/gateway/server.rs +++ b/src-tauri/src/openhuman/gateway/server.rs @@ -247,8 +247,9 @@ pub async fn run_gateway(host: &str, port: u16, config: Config) -> Result<()> { crate::openhuman::health::mark_component_ok("gateway"); // Build shared state - let observer: Arc = - Arc::from(crate::openhuman::observability::create_observer(&config.observability)); + let observer: Arc = Arc::from( + crate::openhuman::observability::create_observer(&config.observability), + ); let state = AppState { config: config_state, diff --git a/src-tauri/src/openhuman/gateway/state.rs b/src-tauri/src/openhuman/gateway/state.rs index 477b937f4..9a59e527a 100644 --- a/src-tauri/src/openhuman/gateway/state.rs +++ b/src-tauri/src/openhuman/gateway/state.rs @@ -2,10 +2,10 @@ use crate::openhuman::channels::{LinqChannel, WhatsAppChannel}; use crate::openhuman::config::Config; +use crate::openhuman::gateway::rate_limit::{GatewayRateLimiter, IdempotencyStore}; use crate::openhuman::memory::Memory; use crate::openhuman::providers::Provider; use crate::openhuman::security::pairing::PairingGuard; -use crate::openhuman::gateway::rate_limit::{GatewayRateLimiter, IdempotencyStore}; use parking_lot::Mutex; use std::sync::Arc; diff --git a/src-tauri/src/openhuman/gateway/tests.rs b/src-tauri/src/openhuman/gateway/tests.rs index bf2339a8a..84da84d14 100644 --- a/src-tauri/src/openhuman/gateway/tests.rs +++ b/src-tauri/src/openhuman/gateway/tests.rs @@ -104,7 +104,9 @@ async fn metrics_endpoint_returns_hint_when_prometheus_is_disabled() { observer: Arc::new(crate::openhuman::observability::NoopObserver), }; - let response = handle_metrics(axum::extract::State(state)).await.into_response(); + let response = handle_metrics(axum::extract::State(state)) + .await + .into_response(); assert_eq!(response.status(), axum::http::StatusCode::OK); assert_eq!( response @@ -147,7 +149,9 @@ async fn metrics_endpoint_renders_prometheus_output() { observer, }; - let response = handle_metrics(axum::extract::State(state)).await.into_response(); + let response = handle_metrics(axum::extract::State(state)) + .await + .into_response(); assert_eq!(response.status(), axum::http::StatusCode::OK); let body = response.into_body().collect().await.unwrap().to_bytes(); diff --git a/src-tauri/src/openhuman/health/mod.rs b/src-tauri/src/openhuman/health/mod.rs index 0231aebf3..7811e883c 100644 --- a/src-tauri/src/openhuman/health/mod.rs +++ b/src-tauri/src/openhuman/health/mod.rs @@ -71,7 +71,11 @@ pub fn mark_component_ok(component: &str) { #[allow(clippy::needless_pass_by_value)] pub fn mark_component_error(component: &str, error: impl ToString) { let err = error.to_string(); - log::warn!("[openhuman:health] Component '{}' error: {}", component, err); + log::warn!( + "[openhuman:health] Component '{}' error: {}", + component, + err + ); upsert_component(component, move |entry| { entry.status = "error".into(); entry.last_error = Some(err); diff --git a/src-tauri/src/openhuman/integrations/registry.rs b/src-tauri/src/openhuman/integrations/registry.rs index 61b97e0c9..8367af345 100644 --- a/src-tauri/src/openhuman/integrations/registry.rs +++ b/src-tauri/src/openhuman/integrations/registry.rs @@ -725,7 +725,9 @@ pub fn all_integrations() -> Vec { #[cfg(test)] mod tests { use super::*; - use crate::openhuman::config::schema::{IMessageConfig, MatrixConfig, StreamMode, TelegramConfig}; + use crate::openhuman::config::schema::{ + IMessageConfig, MatrixConfig, StreamMode, TelegramConfig, + }; use crate::openhuman::config::Config; #[test] diff --git a/src-tauri/src/openhuman/migration.rs b/src-tauri/src/openhuman/migration.rs index d4fc4295d..0cd18f498 100644 --- a/src-tauri/src/openhuman/migration.rs +++ b/src-tauri/src/openhuman/migration.rs @@ -291,7 +291,13 @@ fn normalize_key(raw: &str, idx: usize) -> String { trimmed .chars() - .map(|c| if c.is_alphanumeric() || c == '-' || c == '_' { c } else { '_' }) + .map(|c| { + if c.is_alphanumeric() || c == '-' || c == '_' { + c + } else { + '_' + } + }) .collect::() .trim_matches('_') .to_string() @@ -380,10 +386,7 @@ fn pick_column_expr<'a>( fallback } -fn pick_optional_column_expr<'a>( - columns: &'a [String], - candidates: &[&'a str], -) -> Option<&'a str> { +fn pick_optional_column_expr<'a>(columns: &'a [String], candidates: &[&'a str]) -> Option<&'a str> { for candidate in candidates { if columns.iter().any(|c| c.eq_ignore_ascii_case(candidate)) { return Some(candidate); diff --git a/src-tauri/src/openhuman/mod.rs b/src-tauri/src/openhuman/mod.rs index 5a0b1cbf1..deaa88e8d 100644 --- a/src-tauri/src/openhuman/mod.rs +++ b/src-tauri/src/openhuman/mod.rs @@ -11,8 +11,8 @@ // Many types/functions are not yet consumed but are intentionally exported. #![allow(dead_code)] -pub mod approval; pub mod agent; +pub mod approval; pub mod channels; pub mod config; pub mod cost; @@ -28,8 +28,8 @@ pub mod integrations; pub mod memory; pub mod migration; pub mod multimodal; -pub mod onboard; pub mod observability; +pub mod onboard; pub mod peripherals; pub mod providers; pub mod rag; diff --git a/src-tauri/src/openhuman/onboard/mod.rs b/src-tauri/src/openhuman/onboard/mod.rs index d0777b27e..bc697a2bb 100644 --- a/src-tauri/src/openhuman/onboard/mod.rs +++ b/src-tauri/src/openhuman/onboard/mod.rs @@ -2,9 +2,7 @@ pub mod models; -pub use models::{ - run_models_refresh, ModelCacheSnapshot, ModelRefreshResult, ModelRefreshSource, -}; +pub use models::{run_models_refresh, ModelCacheSnapshot, ModelRefreshResult, ModelRefreshSource}; #[cfg(test)] mod tests { diff --git a/src-tauri/src/openhuman/onboard/models.rs b/src-tauri/src/openhuman/onboard/models.rs index 3c509ed6c..20b1d5627 100644 --- a/src-tauri/src/openhuman/onboard/models.rs +++ b/src-tauri/src/openhuman/onboard/models.rs @@ -118,9 +118,8 @@ pub fn run_models_refresh( }); } - Err(error).with_context(|| { - format!("failed to refresh models for provider '{provider_name}'") - }) + Err(error) + .with_context(|| format!("failed to refresh models for provider '{provider_name}'")) } } } @@ -501,7 +500,10 @@ fn save_model_cache_state(workspace_dir: &Path, state: &ModelCacheState) -> Resu let path = model_cache_path(workspace_dir); if let Some(parent) = path.parent() { fs::create_dir_all(parent).with_context(|| { - format!("failed to create model cache directory {}", parent.display()) + format!( + "failed to create model cache directory {}", + parent.display() + ) })?; } diff --git a/src-tauri/src/openhuman/peripherals/arduino_flash.rs b/src-tauri/src/openhuman/peripherals/arduino_flash.rs index 160feb6e7..756281c28 100644 --- a/src-tauri/src/openhuman/peripherals/arduino_flash.rs +++ b/src-tauri/src/openhuman/peripherals/arduino_flash.rs @@ -132,7 +132,10 @@ pub fn flash_arduino_firmware(port: &str) -> Result<()> { } /// Resolve port from config or path. Returns the path to use for flashing. -pub fn resolve_port(config: &crate::openhuman::config::Config, path_override: Option<&str>) -> Option { +pub fn resolve_port( + config: &crate::openhuman::config::Config, + path_override: Option<&str>, +) -> Option { if let Some(p) = path_override { return Some(p.to_string()); } diff --git a/src-tauri/src/openhuman/peripherals/mod.rs b/src-tauri/src/openhuman/peripherals/mod.rs index f14a42989..4085b3c29 100644 --- a/src-tauri/src/openhuman/peripherals/mod.rs +++ b/src-tauri/src/openhuman/peripherals/mod.rs @@ -115,12 +115,12 @@ pub async fn create_peripheral_tools(config: &PeripheralsConfig) -> Result = config.boards.iter().map(|b| b.board.clone()).collect(); tools.push(Box::new(HardwareMemoryMapTool::new(board_names.clone()))); - tools.push(Box::new(crate::openhuman::tools::HardwareBoardInfoTool::new( - board_names.clone(), - ))); - tools.push(Box::new(crate::openhuman::tools::HardwareMemoryReadTool::new( - board_names, - ))); + tools.push(Box::new( + crate::openhuman::tools::HardwareBoardInfoTool::new(board_names.clone()), + )); + tools.push(Box::new( + crate::openhuman::tools::HardwareMemoryReadTool::new(board_names), + )); } // Phase C: Add hardware_capabilities tool when any serial boards diff --git a/src-tauri/src/openhuman/peripherals/uno_q_setup.rs b/src-tauri/src/openhuman/peripherals/uno_q_setup.rs index 028ce339b..48239fa20 100644 --- a/src-tauri/src/openhuman/peripherals/uno_q_setup.rs +++ b/src-tauri/src/openhuman/peripherals/uno_q_setup.rs @@ -114,7 +114,8 @@ fn write_embedded_bridge(dest: &std::path::Path) -> Result<()> { let sketch_ino = include_str!("../../firmware/openhuman-uno-q-bridge/sketch/sketch.ino"); let sketch_yaml = include_str!("../../firmware/openhuman-uno-q-bridge/sketch/sketch.yaml"); let main_py = include_str!("../../firmware/openhuman-uno-q-bridge/python/main.py"); - let requirements = include_str!("../../firmware/openhuman-uno-q-bridge/python/requirements.txt"); + let requirements = + include_str!("../../firmware/openhuman-uno-q-bridge/python/requirements.txt"); std::fs::write(dest.join("app.yaml"), app_yaml)?; std::fs::create_dir_all(dest.join("sketch"))?; diff --git a/src-tauri/src/openhuman/providers/anthropic.rs b/src-tauri/src/openhuman/providers/anthropic.rs index f111c2973..d0d2411b8 100644 --- a/src-tauri/src/openhuman/providers/anthropic.rs +++ b/src-tauri/src/openhuman/providers/anthropic.rs @@ -400,7 +400,11 @@ impl AnthropicProvider { } fn http_client(&self) -> Client { - crate::openhuman::config::build_runtime_proxy_client_with_timeouts("provider.anthropic", 120, 10) + crate::openhuman::config::build_runtime_proxy_client_with_timeouts( + "provider.anthropic", + 120, + 10, + ) } } diff --git a/src-tauri/src/openhuman/providers/bedrock.rs b/src-tauri/src/openhuman/providers/bedrock.rs index cd5241ce7..6c3d8d76a 100644 --- a/src-tauri/src/openhuman/providers/bedrock.rs +++ b/src-tauri/src/openhuman/providers/bedrock.rs @@ -349,7 +349,11 @@ impl BedrockProvider { } fn http_client(&self) -> Client { - crate::openhuman::config::build_runtime_proxy_client_with_timeouts("provider.bedrock", 120, 10) + crate::openhuman::config::build_runtime_proxy_client_with_timeouts( + "provider.bedrock", + 120, + 10, + ) } /// Percent-encode the model ID for URL path: only encode `:` to `%3A`. diff --git a/src-tauri/src/openhuman/providers/compatible.rs b/src-tauri/src/openhuman/providers/compatible.rs index 1944f2ee8..b11ed22c8 100644 --- a/src-tauri/src/openhuman/providers/compatible.rs +++ b/src-tauri/src/openhuman/providers/compatible.rs @@ -159,8 +159,10 @@ impl OpenAiCompatibleProvider { .timeout(std::time::Duration::from_secs(120)) .connect_timeout(std::time::Duration::from_secs(10)) .default_headers(headers); - let builder = - crate::openhuman::config::apply_runtime_proxy_to_builder(builder, "provider.compatible"); + let builder = crate::openhuman::config::apply_runtime_proxy_to_builder( + builder, + "provider.compatible", + ); return builder.build().unwrap_or_else(|error| { tracing::warn!("Failed to build proxied timeout client with user-agent: {error}"); @@ -168,7 +170,11 @@ impl OpenAiCompatibleProvider { }); } - crate::openhuman::config::build_runtime_proxy_client_with_timeouts("provider.compatible", 120, 10) + crate::openhuman::config::build_runtime_proxy_client_with_timeouts( + "provider.compatible", + 120, + 10, + ) } /// Build the full URL for chat completions, detecting if base_url already includes the path. @@ -233,7 +239,9 @@ impl OpenAiCompatibleProvider { } } - fn tool_specs_to_openai_format(tools: &[crate::openhuman::tools::ToolSpec]) -> Vec { + fn tool_specs_to_openai_format( + tools: &[crate::openhuman::tools::ToolSpec], + ) -> Vec { tools .iter() .map(|tool| { diff --git a/src-tauri/src/openhuman/providers/copilot.rs b/src-tauri/src/openhuman/providers/copilot.rs index f98a65e2a..1bf622cb4 100644 --- a/src-tauri/src/openhuman/providers/copilot.rs +++ b/src-tauri/src/openhuman/providers/copilot.rs @@ -208,7 +208,11 @@ impl CopilotProvider { } fn http_client(&self) -> Client { - crate::openhuman::config::build_runtime_proxy_client_with_timeouts("provider.copilot", 120, 10) + crate::openhuman::config::build_runtime_proxy_client_with_timeouts( + "provider.copilot", + 120, + 10, + ) } /// Required headers for Copilot API requests (editor identification). diff --git a/src-tauri/src/openhuman/providers/gemini.rs b/src-tauri/src/openhuman/providers/gemini.rs index 5486c2e1f..b48c59174 100644 --- a/src-tauri/src/openhuman/providers/gemini.rs +++ b/src-tauri/src/openhuman/providers/gemini.rs @@ -276,7 +276,11 @@ impl GeminiProvider { } fn http_client(&self) -> Client { - crate::openhuman::config::build_runtime_proxy_client_with_timeouts("provider.gemini", 120, 10) + crate::openhuman::config::build_runtime_proxy_client_with_timeouts( + "provider.gemini", + 120, + 10, + ) } fn build_generate_content_request( diff --git a/src-tauri/src/openhuman/providers/ollama.rs b/src-tauri/src/openhuman/providers/ollama.rs index 25e2f1552..b2367920f 100644 --- a/src-tauri/src/openhuman/providers/ollama.rs +++ b/src-tauri/src/openhuman/providers/ollama.rs @@ -124,7 +124,11 @@ impl OllamaProvider { } fn http_client(&self) -> Client { - crate::openhuman::config::build_runtime_proxy_client_with_timeouts("provider.ollama", 300, 10) + crate::openhuman::config::build_runtime_proxy_client_with_timeouts( + "provider.ollama", + 300, + 10, + ) } fn resolve_request_details(&self, model: &str) -> anyhow::Result<(String, bool)> { diff --git a/src-tauri/src/openhuman/providers/openai.rs b/src-tauri/src/openhuman/providers/openai.rs index b5eb2949f..aa40ceebb 100644 --- a/src-tauri/src/openhuman/providers/openai.rs +++ b/src-tauri/src/openhuman/providers/openai.rs @@ -250,7 +250,11 @@ impl OpenAiProvider { } fn http_client(&self) -> Client { - crate::openhuman::config::build_runtime_proxy_client_with_timeouts("provider.openai", 120, 10) + crate::openhuman::config::build_runtime_proxy_client_with_timeouts( + "provider.openai", + 120, + 10, + ) } } diff --git a/src-tauri/src/openhuman/providers/openrouter.rs b/src-tauri/src/openhuman/providers/openrouter.rs index c2b0b0768..6db698078 100644 --- a/src-tauri/src/openhuman/providers/openrouter.rs +++ b/src-tauri/src/openhuman/providers/openrouter.rs @@ -221,7 +221,11 @@ impl OpenRouterProvider { } fn http_client(&self) -> Client { - crate::openhuman::config::build_runtime_proxy_client_with_timeouts("provider.openrouter", 120, 10) + crate::openhuman::config::build_runtime_proxy_client_with_timeouts( + "provider.openrouter", + 120, + 10, + ) } } diff --git a/src-tauri/src/openhuman/providers/traits.rs b/src-tauri/src/openhuman/providers/traits.rs index 604f31410..4cb2cda9c 100644 --- a/src-tauri/src/openhuman/providers/traits.rs +++ b/src-tauri/src/openhuman/providers/traits.rs @@ -239,9 +239,7 @@ pub enum ToolsPayload { fn should_log_prompts() -> bool { matches!( - std::env::var("OPENHUMAN_LOG_PROMPTS") - .ok() - .as_deref(), + std::env::var("OPENHUMAN_LOG_PROMPTS").ok().as_deref(), Some("1") | Some("true") | Some("TRUE") | Some("yes") | Some("YES") ) } diff --git a/src-tauri/src/openhuman/security/detect.rs b/src-tauri/src/openhuman/security/detect.rs index 7118baa63..bf290693d 100644 --- a/src-tauri/src/openhuman/security/detect.rs +++ b/src-tauri/src/openhuman/security/detect.rs @@ -25,9 +25,7 @@ pub fn create_sandbox(config: &SecurityConfig) -> Arc { } } } - log::warn!( - "Landlock requested but not available, falling back to application-layer" - ); + log::warn!("Landlock requested but not available, falling back to application-layer"); Arc::new(super::traits::NoopSandbox) } SandboxBackend::Firejail => { @@ -37,9 +35,7 @@ pub fn create_sandbox(config: &SecurityConfig) -> Arc { return Arc::new(sandbox); } } - log::warn!( - "Firejail requested but not available, falling back to application-layer" - ); + log::warn!("Firejail requested but not available, falling back to application-layer"); Arc::new(super::traits::NoopSandbox) } SandboxBackend::Bubblewrap => { @@ -52,9 +48,7 @@ pub fn create_sandbox(config: &SecurityConfig) -> Arc { } } } - log::warn!( - "Bubblewrap requested but not available, falling back to application-layer" - ); + log::warn!("Bubblewrap requested but not available, falling back to application-layer"); Arc::new(super::traits::NoopSandbox) } SandboxBackend::Docker => { diff --git a/src-tauri/src/openhuman/service/mod.rs b/src-tauri/src/openhuman/service/mod.rs index 494f17f4f..e6db33281 100644 --- a/src-tauri/src/openhuman/service/mod.rs +++ b/src-tauri/src/openhuman/service/mod.rs @@ -86,9 +86,7 @@ pub fn start(config: &Config) -> Result { .arg(&plist), ); if let Err(err) = bootstrap_ok { - log::warn!( - "[service] launchctl bootstrap failed, falling back to load -w: {err}" - ); + log::warn!("[service] launchctl bootstrap failed, falling back to load -w: {err}"); run_checked(Command::new("launchctl").arg("load").arg("-w").arg(&plist))?; } } else { @@ -217,7 +215,11 @@ pub fn stop(config: &Config) -> Result { // Compatibility fallback. run_best_effort(Command::new("launchctl").arg("stop").arg(SERVICE_LABEL)); - run_best_effort(Command::new("launchctl").arg("stop").arg(LEGACY_SERVICE_LABEL)); + run_best_effort( + Command::new("launchctl") + .arg("stop") + .arg(LEGACY_SERVICE_LABEL), + ); return status(config); } @@ -579,8 +581,12 @@ fn run_best_effort(cmd: &mut Command) { /// Check if the macOS LaunchAgent service is loaded (regardless of running state) fn is_service_loaded_macos() -> Result { - if run_checked(Command::new("launchctl").arg("print").arg(macos_target(SERVICE_LABEL)?)) - .is_ok() + if run_checked( + Command::new("launchctl") + .arg("print") + .arg(macos_target(SERVICE_LABEL)?), + ) + .is_ok() { return Ok(true); } diff --git a/src-tauri/src/openhuman/tools/browser.rs b/src-tauri/src/openhuman/tools/browser.rs index 6f4126954..03acd875a 100644 --- a/src-tauri/src/openhuman/tools/browser.rs +++ b/src-tauri/src/openhuman/tools/browser.rs @@ -430,8 +430,7 @@ impl BrowserTool { anyhow::bail!("Blocked local/private host: {host}"); } - if !self.allowed_domains.is_empty() - && !host_matches_allowlist(&host, &self.allowed_domains) + if !self.allowed_domains.is_empty() && !host_matches_allowlist(&host, &self.allowed_domains) { anyhow::bail!("Host '{host}' not in browser.allowed_domains"); } @@ -2077,9 +2076,7 @@ fn is_non_global_v6(v6: std::net::Ipv6Addr) -> bool { fn allow_all_browser_domains() -> bool { matches!( - std::env::var("OPENHUMAN_BROWSER_ALLOW_ALL") - .ok() - .as_deref(), + std::env::var("OPENHUMAN_BROWSER_ALLOW_ALL").ok().as_deref(), Some("1") | Some("true") | Some("TRUE") | Some("yes") | Some("YES") ) } diff --git a/src-tauri/src/openhuman/tools/http_request.rs b/src-tauri/src/openhuman/tools/http_request.rs index 19565ca3c..264652284 100644 --- a/src-tauri/src/openhuman/tools/http_request.rs +++ b/src-tauri/src/openhuman/tools/http_request.rs @@ -118,7 +118,8 @@ impl HttpRequestTool { .timeout(Duration::from_secs(self.timeout_secs)) .connect_timeout(Duration::from_secs(10)) .redirect(reqwest::redirect::Policy::none()); - let builder = crate::openhuman::config::apply_runtime_proxy_to_builder(builder, "tool.http_request"); + let builder = + crate::openhuman::config::apply_runtime_proxy_to_builder(builder, "tool.http_request"); let client = builder.build()?; let mut request = client.request(method, url); diff --git a/src-tauri/src/openhuman/tools/shell.rs b/src-tauri/src/openhuman/tools/shell.rs index e29c83650..29a396093 100644 --- a/src-tauri/src/openhuman/tools/shell.rs +++ b/src-tauri/src/openhuman/tools/shell.rs @@ -363,8 +363,8 @@ mod tests { .unwrap(); assert!(allowed.success); - let _ = - tokio::fs::remove_file(std::env::temp_dir().join("openhuman_shell_approval_test")).await; + let _ = tokio::fs::remove_file(std::env::temp_dir().join("openhuman_shell_approval_test")) + .await; } // ── §5.2 Shell timeout enforcement tests ───────────────── diff --git a/src-tauri/src/openhuman/tunnel/mod.rs b/src-tauri/src/openhuman/tunnel/mod.rs index 17fad2b26..a086a3cb6 100644 --- a/src-tauri/src/openhuman/tunnel/mod.rs +++ b/src-tauri/src/openhuman/tunnel/mod.rs @@ -130,9 +130,7 @@ pub fn create_tunnel(config: &TunnelConfig) -> Result>> { #[cfg(test)] mod tests { use super::*; - use crate::openhuman::config::{ - CloudflareTunnelConfig, CustomTunnelConfig, NgrokTunnelConfig, - }; + use crate::openhuman::config::{CloudflareTunnelConfig, CustomTunnelConfig, NgrokTunnelConfig}; /// Helper: assert `create_tunnel` returns an error containing `needle`. fn assert_tunnel_err(cfg: &TunnelConfig, needle: &str) { diff --git a/src-tauri/src/runtime/bridge/db.rs b/src-tauri/src/runtime/bridge/db.rs index 45ba62557..4f6e25ea6 100644 --- a/src-tauri/src/runtime/bridge/db.rs +++ b/src-tauri/src/runtime/bridge/db.rs @@ -59,7 +59,10 @@ impl SkillDb { /// Execute a SQL statement (no result rows expected). pub fn exec(&self, sql: &str, params: &[JsonValue]) -> Result { - let conn = self.conn.lock().map_err(|e| format!("DB lock error: {e}"))?; + let conn = self + .conn + .lock() + .map_err(|e| format!("DB lock error: {e}"))?; let sqlite_params = json_values_to_sqlite(params); conn.execute(sql, params_from_iter(sqlite_params.iter())) .map_err(|e| format!("SQL exec error: {e}")) @@ -67,7 +70,10 @@ impl SkillDb { /// Fetch a single row as a JSON object. Returns `null` if no row matches. pub fn get(&self, sql: &str, params: &[JsonValue]) -> Result { - let conn = self.conn.lock().map_err(|e| format!("DB lock error: {e}"))?; + let conn = self + .conn + .lock() + .map_err(|e| format!("DB lock error: {e}"))?; let sqlite_params = json_values_to_sqlite(params); let mut stmt = conn .prepare(sql) @@ -91,7 +97,10 @@ impl SkillDb { /// Fetch all matching rows as a JSON array of objects. pub fn all(&self, sql: &str, params: &[JsonValue]) -> Result { - let conn = self.conn.lock().map_err(|e| format!("DB lock error: {e}"))?; + let conn = self + .conn + .lock() + .map_err(|e| format!("DB lock error: {e}"))?; let sqlite_params = json_values_to_sqlite(params); let mut stmt = conn .prepare(sql) @@ -124,10 +133,7 @@ impl SkillDb { match result { JsonValue::Null => Ok(JsonValue::Null), obj => { - let val_str = obj - .get("value") - .and_then(|v| v.as_str()) - .unwrap_or("null"); + let val_str = obj.get("value").and_then(|v| v.as_str()).unwrap_or("null"); serde_json::from_str(val_str) .map_err(|e| format!("Failed to parse KV value for '{key}': {e}")) } @@ -213,12 +219,10 @@ fn row_to_json(row: &rusqlite::Row<'_>, column_names: &[String]) -> Result { - JsonValue::String(base64::Engine::encode( - &base64::engine::general_purpose::STANDARD, - b, - )) - } + Ok(rusqlite::types::ValueRef::Blob(b)) => JsonValue::String(base64::Engine::encode( + &base64::engine::general_purpose::STANDARD, + b, + )), Err(e) => return Err(format!("Failed to read column '{}': {}", name, e)), }; obj.insert(name.clone(), value); diff --git a/src-tauri/src/runtime/bridge/net.rs b/src-tauri/src/runtime/bridge/net.rs index fd6f2248e..532ff05e4 100644 --- a/src-tauri/src/runtime/bridge/net.rs +++ b/src-tauri/src/runtime/bridge/net.rs @@ -91,11 +91,7 @@ fn do_fetch(url: &str, options_json: &str) -> Result { let headers: HashMap = resp .headers() .iter() - .filter_map(|(k, v)| { - v.to_str() - .ok() - .map(|val| (k.to_string(), val.to_string())) - }) + .filter_map(|(k, v)| v.to_str().ok().map(|val| (k.to_string(), val.to_string()))) .collect(); let body = resp .text() diff --git a/src-tauri/src/runtime/bridge/skills_bridge.rs b/src-tauri/src/runtime/bridge/skills_bridge.rs index 1b2e8ccd5..a59e00c6a 100644 --- a/src-tauri/src/runtime/bridge/skills_bridge.rs +++ b/src-tauri/src/runtime/bridge/skills_bridge.rs @@ -68,7 +68,9 @@ pub fn call_tool( .build() .map_err(|e| format!("Failed to create runtime: {e}")) .map(|rt| { - rt.block_on(async { registry.call_tool(&target, &tool, arguments_clone).await }) + rt.block_on(async { + registry.call_tool(&target, &tool, arguments_clone).await + }) }) }); diff --git a/src-tauri/src/runtime/cron_scheduler.rs b/src-tauri/src/runtime/cron_scheduler.rs index 86a445e3e..f40fe935b 100644 --- a/src-tauri/src/runtime/cron_scheduler.rs +++ b/src-tauri/src/runtime/cron_scheduler.rs @@ -98,13 +98,8 @@ impl CronScheduler { /// Unregister all schedules for a skill (called when skill stops). pub fn unregister_all_for_skill(&self, skill_id: &str) { let prefix = format!("{}:", skill_id); - self.entries - .write() - .retain(|k, _| !k.starts_with(&prefix)); - log::info!( - "[cron] Unregistered all schedules for skill '{}'", - skill_id - ); + self.entries.write().retain(|k, _| !k.starts_with(&prefix)); + log::info!("[cron] Unregistered all schedules for skill '{}'", skill_id); } /// List all registered schedules for a skill. @@ -200,11 +195,7 @@ impl CronScheduler { if let Some(registry) = registry { for (skill_id, schedule_id) in to_fire { - log::info!( - "[cron] Firing '{}' for skill '{}'", - schedule_id, - skill_id - ); + log::info!("[cron] Firing '{}' for skill '{}'", schedule_id, skill_id); if let Err(e) = registry.trigger_cron(&skill_id, &schedule_id).await { log::warn!( "[cron] Failed to trigger '{}:{}': {e}", diff --git a/src-tauri/src/runtime/mod.rs b/src-tauri/src/runtime/mod.rs index a8fdbb773..332a7e272 100644 --- a/src-tauri/src/runtime/mod.rs +++ b/src-tauri/src/runtime/mod.rs @@ -22,8 +22,8 @@ pub mod cron_scheduler; #[cfg(not(any(target_os = "android", target_os = "ios")))] pub mod ping_scheduler; #[cfg(not(any(target_os = "android", target_os = "ios")))] -pub mod skill_registry; -#[cfg(not(any(target_os = "android", target_os = "ios")))] pub mod qjs_engine; #[cfg(not(any(target_os = "android", target_os = "ios")))] pub mod qjs_skill_instance; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub mod skill_registry; diff --git a/src-tauri/src/runtime/ping_scheduler.rs b/src-tauri/src/runtime/ping_scheduler.rs index 4ef822427..e6edaaec8 100644 --- a/src-tauri/src/runtime/ping_scheduler.rs +++ b/src-tauri/src/runtime/ping_scheduler.rs @@ -78,7 +78,10 @@ impl PingScheduler { let mut stop_rx = self.stop_tx.subscribe(); tokio::spawn(async move { - log::info!("[ping] Scheduler started ({}s interval)", PING_INTERVAL.as_secs()); + log::info!( + "[ping] Scheduler started ({}s interval)", + PING_INTERVAL.as_secs() + ); loop { tokio::select! { diff --git a/src-tauri/src/runtime/qjs_engine.rs b/src-tauri/src/runtime/qjs_engine.rs index 278a6e24f..d432f2830 100644 --- a/src-tauri/src/runtime/qjs_engine.rs +++ b/src-tauri/src/runtime/qjs_engine.rs @@ -13,10 +13,10 @@ use crate::runtime::cron_scheduler::CronScheduler; use crate::runtime::manifest::SkillManifest; use crate::runtime::ping_scheduler::PingScheduler; use crate::runtime::preferences::PreferencesStore; +use crate::runtime::qjs_skill_instance::{BridgeDeps, QjsSkillInstance}; use crate::runtime::skill_registry::SkillRegistry; use crate::runtime::socket_manager::SocketManager; use crate::runtime::types::{events, SkillMessage, SkillSnapshot, SkillStatus, ToolResult}; -use crate::runtime::qjs_skill_instance::{BridgeDeps, QjsSkillInstance}; // IdbStorage removed during runtime cleanup /// The central runtime engine using QuickJS. @@ -248,10 +248,7 @@ impl RuntimeEngine { let manifest_path = skill_dir.join("manifest.json"); if !manifest_path.exists() { - return Err(format!( - "Skill '{}' not found (no manifest.json)", - skill_id - )); + return Err(format!("Skill '{}' not found (no manifest.json)", skill_id)); } let manifest = SkillManifest::from_path(&manifest_path).await?; @@ -266,12 +263,18 @@ impl RuntimeEngine { let data_dir = self.skills_data_dir.join(skill_id); // Create the QuickJS skill instance - log::info!("[runtime] Creating QuickJS skill instance for '{}'", skill_id); + log::info!( + "[runtime] Creating QuickJS skill instance for '{}'", + skill_id + ); log::info!("[runtime] Config: {:?}", config); log::info!("[runtime] Skill dir: {:?}", skill_dir); log::info!("[runtime] Data dir: {:?}", data_dir); let (instance, rx) = QjsSkillInstance::new(config.clone(), skill_dir, data_dir.clone()); - log::info!("[runtime] QuickJS skill instance created for '{}'", skill_id); + log::info!( + "[runtime] QuickJS skill instance created for '{}'", + skill_id + ); // Bundle bridge dependencies (no creation lock needed for QuickJS) let deps = BridgeDeps { @@ -380,7 +383,9 @@ impl RuntimeEngine { tool_name: &str, arguments: serde_json::Value, ) -> Result { - self.registry.call_tool(skill_id, tool_name, arguments).await + self.registry + .call_tool(skill_id, tool_name, arguments) + .await } /// Broadcast a server event to all running skills. @@ -505,7 +510,11 @@ impl RuntimeEngine { params: serde_json::Value::Object(load_params), }; if let Err(e) = self.registry.send_message(skill_id, msg) { - log::warn!("[runtime] Failed to send LoadParams to skill '{}': {}", skill_id, e); + log::warn!( + "[runtime] Failed to send LoadParams to skill '{}': {}", + skill_id, + e + ); } } Ok(serde_json::json!({ "ok": true })) @@ -566,10 +575,7 @@ impl RuntimeEngine { } "tools/call" => { - let tool_name = params - .get("name") - .and_then(|v| v.as_str()) - .unwrap_or(""); + let tool_name = params.get("name").and_then(|v| v.as_str()).unwrap_or(""); let arguments = params .get("arguments") .cloned() diff --git a/src-tauri/src/runtime/qjs_skill_instance.rs b/src-tauri/src/runtime/qjs_skill_instance.rs index 257a913fd..aa6d3cd71 100644 --- a/src-tauri/src/runtime/qjs_skill_instance.rs +++ b/src-tauri/src/runtime/qjs_skill_instance.rs @@ -129,7 +129,10 @@ impl QjsSkillInstance { let mut s = state.write(); s.status = SkillStatus::Error; s.error = Some(format!("Failed to read {}: {e}", config.entry_point)); - log::error!("[skill:{}] Failed to read entry point: {e}", config.skill_id); + log::error!( + "[skill:{}] Failed to read entry point: {e}", + config.skill_id + ); return; } }; @@ -141,7 +144,10 @@ impl QjsSkillInstance { let mut s = state.write(); s.status = SkillStatus::Error; s.error = Some(format!("Failed to create QuickJS runtime: {e}")); - log::error!("[skill:{}] Failed to create QuickJS runtime: {e}", config.skill_id); + log::error!( + "[skill:{}] Failed to create QuickJS runtime: {e}", + config.skill_id + ); return; } }; @@ -173,53 +179,55 @@ impl QjsSkillInstance { // Register ops and run bootstrap + skill code let skill_id = config.skill_id.clone(); - let init_result = ctx.with(|js_ctx| { - // Register native ops as __ops global - let skill_context = qjs_ops::SkillContext { - skill_id: skill_id.clone(), - data_dir: data_dir.clone(), - app_handle: _deps.app_handle.clone(), - }; + let init_result = ctx + .with(|js_ctx| { + // Register native ops as __ops global + let skill_context = qjs_ops::SkillContext { + skill_id: skill_id.clone(), + data_dir: data_dir.clone(), + app_handle: _deps.app_handle.clone(), + }; - if let Err(e) = qjs_ops::register_ops( - &js_ctx, - storage.clone(), - skill_context, - published_state.clone(), - timer_state.clone(), - ws_state.clone(), - ) { - return Err(format!("Failed to register ops: {e}")); - } + if let Err(e) = qjs_ops::register_ops( + &js_ctx, + storage.clone(), + skill_context, + published_state.clone(), + timer_state.clone(), + ws_state.clone(), + ) { + return Err(format!("Failed to register ops: {e}")); + } - // Load bootstrap - let bootstrap_code = include_str!("../services/quickjs_libs/bootstrap.js"); - if let Err(e) = js_ctx.eval::(bootstrap_code) { - let detail = format_js_exception(&js_ctx, &e); - return Err(format!("Bootstrap failed: {detail}")); - } + // Load bootstrap + let bootstrap_code = include_str!("../services/quickjs_libs/bootstrap.js"); + if let Err(e) = js_ctx.eval::(bootstrap_code) { + let detail = format_js_exception(&js_ctx, &e); + return Err(format!("Bootstrap failed: {detail}")); + } - // Set skill ID - let bridge_code = format!( - r#"globalThis.__skillId = "{}";"#, - skill_id.replace('"', r#"\""#) - ); - if let Err(e) = js_ctx.eval::(bridge_code.as_bytes()) { - let detail = format_js_exception(&js_ctx, &e); - return Err(format!("Skill init failed: {detail}")); - } + // Set skill ID + let bridge_code = format!( + r#"globalThis.__skillId = "{}";"#, + skill_id.replace('"', r#"\""#) + ); + if let Err(e) = js_ctx.eval::(bridge_code.as_bytes()) { + let detail = format_js_exception(&js_ctx, &e); + return Err(format!("Skill init failed: {detail}")); + } - // Execute the skill's entry point - if let Err(e) = js_ctx.eval::(js_source.as_bytes()) { - let detail = format_js_exception(&js_ctx, &e); - return Err(format!("Skill load failed: {detail}")); - } + // Execute the skill's entry point + if let Err(e) = js_ctx.eval::(js_source.as_bytes()) { + let detail = format_js_exception(&js_ctx, &e); + return Err(format!("Skill load failed: {detail}")); + } - // Extract tool definitions - extract_tools(&js_ctx, &state); + // Extract tool definitions + extract_tools(&js_ctx, &state); - Ok(()) - }).await; + Ok(()) + }) + .await; if let Err(e) = init_result { let mut s = state.write(); @@ -271,7 +279,17 @@ impl QjsSkillInstance { drive_jobs(&rt).await; // Run the event loop - run_event_loop(&rt, &ctx, &mut rx, &state, &config.skill_id, &timer_state, &published_state, _deps.app_handle.as_ref()).await; + run_event_loop( + &rt, + &ctx, + &mut rx, + &state, + &config.skill_id, + &timer_state, + &published_state, + _deps.app_handle.as_ref(), + ) + .await; }) } } @@ -331,7 +349,17 @@ async fn run_event_loop( // messages (events, pings, etc.) but queue any new CallTool. match rx.try_recv() { Ok(msg) => { - let should_stop = handle_message(rt, ctx, msg, state, skill_id, &mut pending_tool, app_handle, ops_state).await; + let should_stop = handle_message( + rt, + ctx, + msg, + state, + skill_id, + &mut pending_tool, + app_handle, + ops_state, + ) + .await; if should_stop { break; } @@ -341,7 +369,10 @@ async fn run_event_loop( } Err(mpsc::error::TryRecvError::Disconnected) => { // Channel closed, exit - log::info!("[skill:{}] Message channel disconnected, stopping", skill_id); + log::info!( + "[skill:{}] Message channel disconnected, stopping", + skill_id + ); break; } } @@ -351,12 +382,13 @@ async fn run_event_loop( // 4. Check if pending async tool call has completed if pending_tool.is_some() { - let done = ctx.with(|js_ctx| { - js_ctx - .eval::(b"globalThis.__pendingToolDone === true") - .unwrap_or(false) - }) - .await; + let done = ctx + .with(|js_ctx| { + js_ctx + .eval::(b"globalThis.__pendingToolDone === true") + .unwrap_or(false) + }) + .await; if done { // Read the resolved value and send it back @@ -397,7 +429,11 @@ async fn run_event_loop( "state": new_map, }); if let Err(e) = handle.emit_to("main", "skill-state-changed", payload) { - log::warn!("[skill:{}] Failed to emit skill-state-changed to main: {}", skill_id, e); + log::warn!( + "[skill:{}] Failed to emit skill-state-changed to main: {}", + skill_id, + e + ); } } } @@ -435,7 +471,8 @@ async fn fire_timer_callback(ctx: &rquickjs::AsyncContext, timer_id: u32) { if let Err(e) = js_ctx.eval::(code.as_bytes()) { log::error!("[timer] Callback for timer {} failed: {}", timer_id, e); } - }).await; + }) + .await; } /// Handle a single message from the channel. @@ -451,7 +488,11 @@ async fn handle_message( ops_state: &Arc>, ) -> bool { match msg { - SkillMessage::CallTool { tool_name, arguments, reply } => { + SkillMessage::CallTool { + tool_name, + arguments, + reply, + } => { // Lazy-load persisted OAuth credential before calling the tool restore_oauth_credential(ctx, skill_id).await; @@ -494,7 +535,8 @@ async fn handle_message( })()"#; ctx.with(|js_ctx| { let _ = js_ctx.eval::(clear_code.as_bytes()); - }).await; + }) + .await; state.write().status = SkillStatus::Stopped; log::info!("[skill:{}] Stopped (OAuth credential cleared)", skill_id); let _ = reply.send(()); @@ -505,7 +547,11 @@ async fn handle_message( let result = handle_js_call(rt, ctx, "onSetupStart", "{}").await; let _ = reply.send(result); } - SkillMessage::SetupSubmit { step_id, values, reply } => { + SkillMessage::SetupSubmit { + step_id, + values, + reply, + } => { let args = serde_json::json!({ "stepId": step_id, "values": values }); let result = handle_js_call(rt, ctx, "onSetupSubmit", &args.to_string()).await; let _ = reply.send(result); @@ -540,10 +586,19 @@ async fn handle_message( SkillMessage::LoadParams { params } => { let params_str = serde_json::to_string(¶ms).unwrap_or_else(|_| "{}".to_string()); if let Err(e) = handle_js_void_call(rt, ctx, "onLoad", ¶ms_str).await { - log::warn!("[skill:{}] onLoad failed (skill may not export it): {}", skill_id, e); + log::warn!( + "[skill:{}] onLoad failed (skill may not export it): {}", + skill_id, + e + ); } } - SkillMessage::Error { error_type, message, source, recoverable } => { + SkillMessage::Error { + error_type, + message, + source, + recoverable, + } => { let args = serde_json::json!({ "type": error_type, "message": message, @@ -554,19 +609,24 @@ async fn handle_message( log::warn!("[skill:{}] onError() handler failed: {e}", skill_id); } } - SkillMessage::Rpc { method, params, reply } => { - // Resolve the optional memory client once for this RPC call. - // State is registered as MemoryState(Mutex>), not - // Option directly, so we must use the newtype wrapper. + SkillMessage::Rpc { + method, + params, + reply, + } => { + // Resolve the optional memory client once for this RPC call. + // State is registered as MemoryState(Mutex>), not + // Option directly, so we must use the newtype wrapper. let memory_client_opt = app_handle.and_then(|ah| { ah.try_state::() - .and_then(|s| s.0.lock().ok().and_then(|g| g.clone())) + .and_then(|s| s.0.lock().ok().and_then(|g| g.clone())) }); let result = match method.as_str() { "oauth/complete" => { // Set credential on the oauth bridge + persist to store - let cred_json = serde_json::to_string(¶ms).unwrap_or_else(|_| "null".to_string()); + let cred_json = + serde_json::to_string(¶ms).unwrap_or_else(|_| "null".to_string()); let code = format!( r#"(function() {{ if (typeof globalThis.oauth !== 'undefined' && globalThis.oauth.__setCredential) {{ @@ -580,14 +640,17 @@ async fn handle_message( ); ctx.with(|js_ctx| { let _ = js_ctx.eval::(code.as_bytes()); - }).await; - log::info!("[skill:{}] OAuth credential set and persisted to store", skill_id); - let params_str = serde_json::to_string(¶ms).unwrap_or_else(|_| "{}".to_string()); + }) + .await; + log::info!( + "[skill:{}] OAuth credential set and persisted to store", + skill_id + ); + let params_str = + serde_json::to_string(¶ms).unwrap_or_else(|_| "{}".to_string()); handle_js_call(rt, ctx, "onOAuthComplete", ¶ms_str).await } - "skill/ping" => { - handle_js_call(rt, ctx, "onPing", "{}").await - } + "skill/ping" => handle_js_call(rt, ctx, "onPing", "{}").await, "skill/sync" => { let result = handle_js_call(rt, ctx, "onSync", "{}").await; // Fire-and-forget: persist published ops state to TinyHumans memory. @@ -601,24 +664,16 @@ async fn handle_message( if !state_snapshot.is_empty() { if let Some(client) = memory_client_opt.clone() { let skill = skill_id.to_string(); - let content = serde_json::to_string_pretty( - &serde_json::Value::Object(state_snapshot), - ) + let content = serde_json::to_string_pretty(&serde_json::Value::Object( + state_snapshot, + )) .unwrap_or_else(|_| "{}".to_string()); let title = format!("{} periodic sync", skill); tokio::spawn(async move { if let Err(e) = client .store_skill_sync( - &skill, - "default", - &title, - &content, - None, - None, - None, - None, - None, - None, + &skill, "default", &title, &content, None, None, None, + None, None, None, ) .await { @@ -642,7 +697,8 @@ async fn handle_message( })()"#; ctx.with(|js_ctx| { let _ = js_ctx.eval::(clear_code.as_bytes()); - }).await; + }) + .await; log::info!("[skill:{}] OAuth credential cleared from store", skill_id); // Fire-and-forget: delete memory for this integration @@ -654,16 +710,23 @@ async fn handle_message( .unwrap_or("unknown") .to_string(); tokio::spawn(async move { - if let Err(e) = client.clear_skill_memory(&skill, &integration_id).await { + if let Err(e) = client.clear_skill_memory(&skill, &integration_id).await + { log::warn!("[memory] clear_skill_memory failed: {e}"); } else { - log::info!("[memory] Cleared memory for {}:{}", skill, integration_id); + log::info!( + "[memory] Cleared memory for {}:{}", + skill, + integration_id + ); } }); } - let params_str = serde_json::to_string(¶ms).unwrap_or_else(|_| "{}".to_string()); - handle_js_void_call(rt, ctx, "onOAuthRevoked", ¶ms_str).await + let params_str = + serde_json::to_string(¶ms).unwrap_or_else(|_| "{}".to_string()); + handle_js_void_call(rt, ctx, "onOAuthRevoked", ¶ms_str) + .await .map(|()| serde_json::json!({ "ok": true })) } _ => { @@ -693,12 +756,8 @@ fn format_js_exception(js_ctx: &rquickjs::Ctx<'_>, err: &rquickjs::Error) -> Str // Try to get .message and .stack from the exception object if let Some(obj) = exception.as_object() { - let message: String = obj - .get::<_, String>("message") - .unwrap_or_default(); - let stack: String = obj - .get::<_, String>("stack") - .unwrap_or_default(); + let message: String = obj.get::<_, String>("message").unwrap_or_default(); + let stack: String = obj.get::<_, String>("stack").unwrap_or_default(); if !message.is_empty() { if !stack.is_empty() { @@ -764,11 +823,13 @@ async fn call_lifecycle( loop { drive_jobs(rt).await; - let done = ctx.with(|js_ctx| { - js_ctx - .eval::(b"globalThis.__pendingLifecycleDone === true") - .unwrap_or(false) - }).await; + let done = ctx + .with(|js_ctx| { + js_ctx + .eval::(b"globalThis.__pendingLifecycleDone === true") + .unwrap_or(false) + }) + .await; if done { let error = ctx.with(|js_ctx| { @@ -830,16 +891,14 @@ fn extract_tools(js_ctx: &rquickjs::Ctx<'_>, state: &Arc>) { // eval with String type hint tells rquickjs to convert the result to a Rust String match js_ctx.eval::(code) { - Ok(json_str) => { - match serde_json::from_str::>(&json_str) { - Ok(tools) => { - state.write().tools = tools; - } - Err(e) => { - log::warn!("[tools] Failed to parse tools JSON: {e}"); - } + Ok(json_str) => match serde_json::from_str::>(&json_str) { + Ok(tools) => { + state.write().tools = tools; } - } + Err(e) => { + log::warn!("[tools] Failed to parse tools JSON: {e}"); + } + }, Err(e) => { log::warn!("[tools] Failed to extract tools: {e}"); } @@ -859,8 +918,8 @@ async fn start_async_tool_call( tool_name: &str, arguments: serde_json::Value, ) -> Result, String> { - let args_str = serde_json::to_string(&arguments) - .map_err(|e| format!("Failed to serialize args: {e}"))?; + let args_str = + serde_json::to_string(&arguments).map_err(|e| format!("Failed to serialize args: {e}"))?; let tool_name = tool_name.to_string(); let eval_result = ctx @@ -928,9 +987,7 @@ async fn start_async_tool_call( /// Read the result of a completed async tool call from JS globals. /// Call this only after `globalThis.__pendingToolDone === true`. -async fn read_pending_tool_result( - ctx: &rquickjs::AsyncContext, -) -> Result { +async fn read_pending_tool_result(ctx: &rquickjs::AsyncContext) -> Result { let result_text = ctx .with(|js_ctx| { let code = r#"(function() { @@ -1018,11 +1075,13 @@ async fn handle_server_event( loop { drive_jobs(rt).await; - let done = ctx.with(|js_ctx| { - js_ctx - .eval::(b"globalThis.__pendingEventDone === true") - .unwrap_or(false) - }).await; + let done = ctx + .with(|js_ctx| { + js_ctx + .eval::(b"globalThis.__pendingEventDone === true") + .unwrap_or(false) + }) + .await; if done { let error = ctx.with(|js_ctx| { @@ -1111,11 +1170,13 @@ async fn handle_cron_trigger( loop { drive_jobs(rt).await; - let done = ctx.with(|js_ctx| { - js_ctx - .eval::(b"globalThis.__pendingCronDone === true") - .unwrap_or(false) - }).await; + let done = ctx + .with(|js_ctx| { + js_ctx + .eval::(b"globalThis.__pendingCronDone === true") + .unwrap_or(false) + }) + .await; if done { let error = ctx.with(|js_ctx| { @@ -1215,11 +1276,13 @@ async fn handle_js_call( loop { drive_jobs(rt).await; - let done = ctx.with(|js_ctx| { - js_ctx - .eval::(b"globalThis.__pendingRpcDone === true") - .unwrap_or(false) - }).await; + let done = ctx + .with(|js_ctx| { + js_ctx + .eval::(b"globalThis.__pendingRpcDone === true") + .unwrap_or(false) + }) + .await; if done { let result = ctx.with(|js_ctx| { @@ -1324,11 +1387,13 @@ async fn handle_js_void_call( loop { drive_jobs(rt).await; - let done = ctx.with(|js_ctx| { - js_ctx - .eval::(b"globalThis.__pendingRpcVoidDone === true") - .unwrap_or(false) - }).await; + let done = ctx + .with(|js_ctx| { + js_ctx + .eval::(b"globalThis.__pendingRpcVoidDone === true") + .unwrap_or(false) + }) + .await; if done { let error = ctx.with(|js_ctx| { @@ -1384,9 +1449,9 @@ async fn restore_oauth_credential(ctx: &rquickjs::AsyncContext, skill_id: &str) return false; })()"#; - let restored = ctx.with(|js_ctx| { - js_ctx.eval::(code.as_bytes()).unwrap_or(false) - }).await; + let restored = ctx + .with(|js_ctx| js_ctx.eval::(code.as_bytes()).unwrap_or(false)) + .await; if restored { log::info!("[skill:{}] Restored OAuth credential from store", skill_id); diff --git a/src-tauri/src/runtime/skill_registry.rs b/src-tauri/src/runtime/skill_registry.rs index b62952219..ad60fbbe2 100644 --- a/src-tauri/src/runtime/skill_registry.rs +++ b/src-tauri/src/runtime/skill_registry.rs @@ -254,7 +254,10 @@ impl SkillRegistry { match sender.try_send(msg) { Ok(()) => { - log::info!("[runtime] Successfully sent message to skill '{}'", skill_id); + log::info!( + "[runtime] Successfully sent message to skill '{}'", + skill_id + ); Ok(()) } Err(e) => { diff --git a/src-tauri/src/runtime/socket_manager.rs b/src-tauri/src/runtime/socket_manager.rs index ebdd80d5a..43507ce81 100644 --- a/src-tauri/src/runtime/socket_manager.rs +++ b/src-tauri/src/runtime/socket_manager.rs @@ -61,9 +61,8 @@ struct SharedState { // --------------------------------------------------------------------------- #[cfg(not(target_os = "android"))] -type WsStream = tokio_tungstenite::WebSocketStream< - tokio_tungstenite::MaybeTlsStream, ->; +type WsStream = + tokio_tungstenite::WebSocketStream>; // --------------------------------------------------------------------------- // Connection outcome (desktop + iOS) @@ -191,8 +190,7 @@ impl SocketManager { let payload = serde_json::to_string(&json!([event, data])).map_err(|e| format!("{e}"))?; let msg = format!("42{}", payload); - tx.send(msg) - .map_err(|_| "Socket not connected".to_string()) + tx.send(msg).map_err(|_| "Socket not connected".to_string()) } else { Err("Not connected".to_string()) } @@ -377,13 +375,12 @@ async fn run_connection( let (mut ws_write, mut ws_read) = ws_stream.split(); // 4. Read Engine.IO OPEN packet - let open_data = match tokio::time::timeout(Duration::from_secs(10), read_eio_open(&mut ws_read)) - .await - { - Ok(Ok(data)) => data, - Ok(Err(e)) => return ConnectionOutcome::Failed(format!("EIO OPEN: {e}")), - Err(_) => return ConnectionOutcome::Failed("Timeout waiting for EIO OPEN".into()), - }; + let open_data = + match tokio::time::timeout(Duration::from_secs(10), read_eio_open(&mut ws_read)).await { + Ok(Ok(data)) => data, + Ok(Err(e)) => return ConnectionOutcome::Failed(format!("EIO OPEN: {e}")), + Err(_) => return ConnectionOutcome::Failed("Timeout waiting for EIO OPEN".into()), + }; let ping_interval = open_data .get("pingInterval") @@ -393,10 +390,7 @@ async fn run_connection( .get("pingTimeout") .and_then(|v| v.as_u64()) .unwrap_or(20000); - let eio_sid = open_data - .get("sid") - .and_then(|v| v.as_str()) - .unwrap_or("?"); + let eio_sid = open_data.get("sid").and_then(|v| v.as_str()).unwrap_or("?"); log::info!( "[socket-mgr] EIO OPEN: sid={}, ping={}ms, timeout={}ms", eio_sid, @@ -407,10 +401,7 @@ async fn run_connection( // 5. Send Socket.IO CONNECT with auth let connect_payload = json!({"token": token}); let connect_msg = format!("40{}", serde_json::to_string(&connect_payload).unwrap()); - if let Err(e) = ws_write - .send(WsMessage::Text(connect_msg.into())) - .await - { + if let Err(e) = ws_write.send(WsMessage::Text(connect_msg.into())).await { return ConnectionOutcome::Failed(format!("Send SIO CONNECT: {e}")); } @@ -661,7 +652,11 @@ fn handle_sio_packet( } b'4' => { // Socket.IO CONNECT_ERROR - let error_str = if text.len() > 1 { &text[1..] } else { "unknown" }; + let error_str = if text.len() > 1 { + &text[1..] + } else { + "unknown" + }; log::error!("[socket-mgr] SIO CONNECT_ERROR: {}", error_str); } _ => { @@ -770,10 +765,7 @@ async fn handle_mcp_list_tools( Vec::new() }; - log::info!( - "[socket-mgr] mcp:listToolsResponse — {} tools", - tools.len() - ); + log::info!("[socket-mgr] mcp:listToolsResponse — {} tools", tools.len()); emit_via_channel( emit_tx, @@ -884,11 +876,7 @@ fn build_ws_url(url: &str) -> String { /// Send a Socket.IO event through the emit channel. /// Formats: `42["eventName", data]` #[cfg(not(target_os = "android"))] -fn emit_via_channel( - tx: &mpsc::UnboundedSender, - event: &str, - data: serde_json::Value, -) { +fn emit_via_channel(tx: &mpsc::UnboundedSender, event: &str, data: serde_json::Value) { let payload = serde_json::to_string(&json!([event, data])).unwrap_or_default(); let msg = format!("42{}", payload); if let Err(e) = tx.send(msg) { diff --git a/src-tauri/src/runtime/types.rs b/src-tauri/src/runtime/types.rs index 86ab4c4b2..ca7d2166d 100644 --- a/src-tauri/src/runtime/types.rs +++ b/src-tauri/src/runtime/types.rs @@ -102,9 +102,7 @@ pub enum SkillMessage { }, /// Load params from frontend (e.g. wallet address for wallet skill). /// Delivered after skill/load RPC; skill may export onLoad(params) to receive them. - LoadParams { - params: serde_json::Value, - }, + LoadParams { params: serde_json::Value }, } /// Result of executing a tool. diff --git a/src-tauri/src/runtime/utils.rs b/src-tauri/src/runtime/utils.rs index 32bc55dc4..5d98ba08f 100644 --- a/src-tauri/src/runtime/utils.rs +++ b/src-tauri/src/runtime/utils.rs @@ -66,7 +66,7 @@ where let result = match runtime_result { Ok(value) => Ok(value), - Err(e) => Err(format!("Runtime creation failed: {}", e)) + Err(e) => Err(format!("Runtime creation failed: {}", e)), }; let _ = tx.send(result); @@ -137,4 +137,4 @@ mod tests { // Inside async context assert!(is_in_runtime()); } -} \ No newline at end of file +} diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs index 33f89268e..655f8a4a6 100644 --- a/src-tauri/src/services/mod.rs +++ b/src-tauri/src/services/mod.rs @@ -6,4 +6,3 @@ pub mod quickjs_libs; #[cfg(desktop)] pub mod notification_service; - diff --git a/src-tauri/src/services/quickjs_libs/qjs_ops/ops_core.rs b/src-tauri/src/services/quickjs_libs/qjs_ops/ops_core.rs index 1cd669920..e92c2a2eb 100644 --- a/src-tauri/src/services/quickjs_libs/qjs_ops/ops_core.rs +++ b/src-tauri/src/services/quickjs_libs/qjs_ops/ops_core.rs @@ -7,84 +7,127 @@ use std::time::{Duration, Instant}; use super::types::{TimerEntry, TimerState, ALLOWED_ENV_VARS}; -pub fn register<'js>(ctx: &Ctx<'js>, ops: &Object<'js>, timer_state: Arc>) -> rquickjs::Result<()> { +pub fn register<'js>( + ctx: &Ctx<'js>, + ops: &Object<'js>, + timer_state: Arc>, +) -> rquickjs::Result<()> { // ======================================================================== // Console (3) // ======================================================================== - ops.set("console_log", Function::new(ctx.clone(), |msg: String| { - log::info!("[js] {}", msg); - }))?; + ops.set( + "console_log", + Function::new(ctx.clone(), |msg: String| { + log::info!("[js] {}", msg); + }), + )?; - ops.set("console_warn", Function::new(ctx.clone(), |msg: String| { - log::warn!("[js] {}", msg); - }))?; + ops.set( + "console_warn", + Function::new(ctx.clone(), |msg: String| { + log::warn!("[js] {}", msg); + }), + )?; - ops.set("console_error", Function::new(ctx.clone(), |msg: String| { - log::error!("[js] {}", msg); - }))?; + ops.set( + "console_error", + Function::new(ctx.clone(), |msg: String| { + log::error!("[js] {}", msg); + }), + )?; // ======================================================================== // Crypto (3) // ======================================================================== - ops.set("crypto_random", Function::new(ctx.clone(), |len: usize| -> Vec { - use rand::RngCore; - let mut buf = vec![0u8; len]; - rand::thread_rng().fill_bytes(&mut buf); - buf - }))?; + ops.set( + "crypto_random", + Function::new(ctx.clone(), |len: usize| -> Vec { + use rand::RngCore; + let mut buf = vec![0u8; len]; + rand::thread_rng().fill_bytes(&mut buf); + buf + }), + )?; - ops.set("atob", Function::new(ctx.clone(), |input: String| -> rquickjs::Result { - use base64::Engine; - let bytes = base64::engine::general_purpose::STANDARD - .decode(&input) - .map_err(|e| super::types::js_err(e.to_string()))?; - String::from_utf8(bytes).map_err(|e| super::types::js_err(e.to_string())) - }))?; + ops.set( + "atob", + Function::new(ctx.clone(), |input: String| -> rquickjs::Result { + use base64::Engine; + let bytes = base64::engine::general_purpose::STANDARD + .decode(&input) + .map_err(|e| super::types::js_err(e.to_string()))?; + String::from_utf8(bytes).map_err(|e| super::types::js_err(e.to_string())) + }), + )?; - ops.set("btoa", Function::new(ctx.clone(), |input: String| -> String { - use base64::Engine; - base64::engine::general_purpose::STANDARD.encode(input.as_bytes()) - }))?; + ops.set( + "btoa", + Function::new(ctx.clone(), |input: String| -> String { + use base64::Engine; + base64::engine::general_purpose::STANDARD.encode(input.as_bytes()) + }), + )?; // ======================================================================== // Performance (1) // ======================================================================== - ops.set("performance_now", Function::new(ctx.clone(), || -> f64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs_f64() - * 1000.0 - }))?; + ops.set( + "performance_now", + Function::new(ctx.clone(), || -> f64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs_f64() + * 1000.0 + }), + )?; // ======================================================================== // Platform (2) // ======================================================================== - ops.set("platform_os", Function::new(ctx.clone(), || -> &'static str { - if cfg!(target_os = "windows") { "windows" } - else if cfg!(target_os = "macos") { "macos" } - else if cfg!(target_os = "linux") { "linux" } - else if cfg!(target_os = "android") { "android" } - else if cfg!(target_os = "ios") { "ios" } - else { "unknown" } - }))?; + ops.set( + "platform_os", + Function::new(ctx.clone(), || -> &'static str { + if cfg!(target_os = "windows") { + "windows" + } else if cfg!(target_os = "macos") { + "macos" + } else if cfg!(target_os = "linux") { + "linux" + } else if cfg!(target_os = "android") { + "android" + } else if cfg!(target_os = "ios") { + "ios" + } else { + "unknown" + } + }), + )?; - ops.set("platform_env", Function::new(ctx.clone(), |key: String| -> Option { - if ALLOWED_ENV_VARS.contains(&key.as_str()) { - std::env::var(&key).ok() - } else { - None - } - }))?; + ops.set( + "platform_env", + Function::new(ctx.clone(), |key: String| -> Option { + if ALLOWED_ENV_VARS.contains(&key.as_str()) { + std::env::var(&key).ok() + } else { + None + } + }), + )?; - ops.set("get_session_token", Function::new(ctx.clone(), || -> String { - let token = crate::commands::auth::SESSION_SERVICE.get_token().unwrap_or_default(); - return token; - }))?; + ops.set( + "get_session_token", + Function::new(ctx.clone(), || -> String { + let token = crate::commands::auth::SESSION_SERVICE + .get_token() + .unwrap_or_default(); + return token; + }), + )?; // ======================================================================== // Timers (2) @@ -92,24 +135,34 @@ pub fn register<'js>(ctx: &Ctx<'js>, ops: &Object<'js>, timer_state: Arc( { let ss = skill_state.clone(); - ops.set("state_get", Function::new(ctx.clone(), - move |key: String| -> rquickjs::Result { - let state = ss.read(); - let value = state.data.get(&key).cloned().unwrap_or(serde_json::Value::Null); - serde_json::to_string(&value).map_err(|e| js_err(e.to_string())) - }, - ))?; + ops.set( + "state_get", + Function::new( + ctx.clone(), + move |key: String| -> rquickjs::Result { + let state = ss.read(); + let value = state + .data + .get(&key) + .cloned() + .unwrap_or(serde_json::Value::Null); + serde_json::to_string(&value).map_err(|e| js_err(e.to_string())) + }, + ), + )?; } { let ss = skill_state.clone(); - ops.set("state_set", Function::new(ctx.clone(), - move |key: String, value_json: String| -> rquickjs::Result<()> { - let value: serde_json::Value = - serde_json::from_str(&value_json).map_err(|e| js_err(e.to_string()))?; - let mut state = ss.write(); - state.data.insert(key, value); - state.dirty = true; - Ok(()) - }, - ))?; + ops.set( + "state_set", + Function::new( + ctx.clone(), + move |key: String, value_json: String| -> rquickjs::Result<()> { + let value: serde_json::Value = + serde_json::from_str(&value_json).map_err(|e| js_err(e.to_string()))?; + let mut state = ss.write(); + state.data.insert(key, value); + state.dirty = true; + Ok(()) + }, + ), + )?; } { let ss = skill_state; - ops.set("state_set_partial", Function::new(ctx.clone(), - move |partial_json: String| -> rquickjs::Result<()> { - let partial: serde_json::Map = - serde_json::from_str(&partial_json).map_err(|e| js_err(e.to_string()))?; - let mut state = ss.write(); - for (k, v) in partial { - state.data.insert(k, v); - } - state.dirty = true; - Ok(()) - }, - ))?; + ops.set( + "state_set_partial", + Function::new( + ctx.clone(), + move |partial_json: String| -> rquickjs::Result<()> { + let partial: serde_json::Map = + serde_json::from_str(&partial_json).map_err(|e| js_err(e.to_string()))?; + let mut state = ss.write(); + for (k, v) in partial { + state.data.insert(k, v); + } + state.dirty = true; + Ok(()) + }, + ), + )?; } // ======================================================================== @@ -79,22 +95,30 @@ pub fn register<'js>( { let sc = skill_context.clone(); - ops.set("data_read", Function::new(ctx.clone(), - move |filename: String| -> rquickjs::Result { - let path = sc.data_dir.join(&filename); - std::fs::read_to_string(&path).map_err(|e| js_err(e.to_string())) - }, - ))?; + ops.set( + "data_read", + Function::new( + ctx.clone(), + move |filename: String| -> rquickjs::Result { + let path = sc.data_dir.join(&filename); + std::fs::read_to_string(&path).map_err(|e| js_err(e.to_string())) + }, + ), + )?; } { let sc = skill_context.clone(); - ops.set("data_write", Function::new(ctx.clone(), - move |filename: String, content: String| -> rquickjs::Result<()> { - let path = sc.data_dir.join(&filename); - std::fs::write(&path, content).map_err(|e| js_err(e.to_string())) - }, - ))?; + ops.set( + "data_write", + Function::new( + ctx.clone(), + move |filename: String, content: String| -> rquickjs::Result<()> { + let path = sc.data_dir.join(&filename); + std::fs::write(&path, content).map_err(|e| js_err(e.to_string())) + }, + ), + )?; } // ======================================================================== @@ -103,80 +127,93 @@ pub fn register<'js>( { let sc = skill_context; - ops.set("memory_insert", Function::new(ctx.clone(), - move |metadata_json: String| -> rquickjs::Result<()> { - let input: JsMemoryInsertInput = - serde_json::from_str(&metadata_json).map_err(|e| js_err(e.to_string()))?; - if input.title.trim().is_empty() { - return Err(js_err("memory.insert requires a non-empty title")); - } - if input.content.trim().is_empty() { - return Err(js_err("memory.insert requires non-empty content")); - } - - let app_handle = sc - .app_handle - .clone() - .ok_or_else(|| js_err("App handle not available for memory insert"))?; - - let memory_state = app_handle - .try_state::() - .ok_or_else(|| js_err("Memory state not available"))?; - - let client_opt = memory_state - .0 - .lock() - .map_err(|_| js_err("Failed to lock memory state"))? - .clone(); - - let client = client_opt.ok_or_else(|| js_err("Memory client is not initialized"))?; - let skill_id = sc.skill_id.clone(); - let integration_id = sc.skill_id.clone(); - let source_type = match input.source_type.as_deref() { - Some("doc") => Some(SourceType::Doc), - Some("chat") => Some(SourceType::Chat), - Some("email") => Some(SourceType::Email), - Some(_) => return Err(js_err("sourceType must be one of: doc, chat, email")), - None => None, - }; - let priority = match input.priority.as_deref() { - Some("high") => Some(Priority::High), - Some("medium") => Some(Priority::Medium), - Some("low") => Some(Priority::Low), - Some(_) => return Err(js_err("priority must be one of: high, medium, low")), - None => None, - }; - let metadata = input.metadata.unwrap_or_else(|| serde_json::json!({})); - - tokio::spawn(async move { - if let Err(e) = client - .store_skill_sync( - &skill_id, - &integration_id, - &input.title, - &input.content, - source_type, - Some(metadata), - priority, - input.created_at, - input.updated_at, - input.document_id, - ) - .await - { - log::warn!("[quickjs] memory_insert failed for '{}': {}", integration_id, e); - } else { - log::info!( - "[quickjs] memory_insert stored '{}': title='{}'", - integration_id, - input.title - ); + ops.set( + "memory_insert", + Function::new( + ctx.clone(), + move |metadata_json: String| -> rquickjs::Result<()> { + let input: JsMemoryInsertInput = + serde_json::from_str(&metadata_json).map_err(|e| js_err(e.to_string()))?; + if input.title.trim().is_empty() { + return Err(js_err("memory.insert requires a non-empty title")); + } + if input.content.trim().is_empty() { + return Err(js_err("memory.insert requires non-empty content")); } - }); - Ok(()) - }, - ))?; + let app_handle = sc + .app_handle + .clone() + .ok_or_else(|| js_err("App handle not available for memory insert"))?; + + let memory_state = app_handle + .try_state::() + .ok_or_else(|| js_err("Memory state not available"))?; + + let client_opt = memory_state + .0 + .lock() + .map_err(|_| js_err("Failed to lock memory state"))? + .clone(); + + let client = + client_opt.ok_or_else(|| js_err("Memory client is not initialized"))?; + let skill_id = sc.skill_id.clone(); + let integration_id = sc.skill_id.clone(); + let source_type = match input.source_type.as_deref() { + Some("doc") => Some(SourceType::Doc), + Some("chat") => Some(SourceType::Chat), + Some("email") => Some(SourceType::Email), + Some(_) => { + return Err(js_err("sourceType must be one of: doc, chat, email")) + } + None => None, + }; + let priority = match input.priority.as_deref() { + Some("high") => Some(Priority::High), + Some("medium") => Some(Priority::Medium), + Some("low") => Some(Priority::Low), + Some(_) => { + return Err(js_err("priority must be one of: high, medium, low")) + } + None => None, + }; + let metadata = input.metadata.unwrap_or_else(|| serde_json::json!({})); + + tokio::spawn(async move { + if let Err(e) = client + .store_skill_sync( + &skill_id, + &integration_id, + &input.title, + &input.content, + source_type, + Some(metadata), + priority, + input.created_at, + input.updated_at, + input.document_id, + ) + .await + { + log::warn!( + "[quickjs] memory_insert failed for '{}': {}", + integration_id, + e + ); + } else { + log::info!( + "[quickjs] memory_insert stored '{}': title='{}'", + integration_id, + input.title + ); + } + }); + + Ok(()) + }, + ), + )?; } Ok(()) diff --git a/src-tauri/src/services/quickjs_libs/qjs_ops/ops_storage.rs b/src-tauri/src/services/quickjs_libs/qjs_ops/ops_storage.rs index 69f21c67b..2317d8428 100644 --- a/src-tauri/src/services/quickjs_libs/qjs_ops/ops_storage.rs +++ b/src-tauri/src/services/quickjs_libs/qjs_ops/ops_storage.rs @@ -5,132 +5,206 @@ use rquickjs::{Ctx, Function, Object}; use super::types::{js_err, SkillContext}; use crate::services::quickjs_libs::storage::IdbStorage; -pub fn register<'js>(ctx: &Ctx<'js>, ops: &Object<'js>, storage: IdbStorage, skill_context: SkillContext) -> rquickjs::Result<()> { +pub fn register<'js>( + ctx: &Ctx<'js>, + ops: &Object<'js>, + storage: IdbStorage, + skill_context: SkillContext, +) -> rquickjs::Result<()> { // ======================================================================== // IndexedDB (11) - all sync // ======================================================================== { let s = storage.clone(); - ops.set("idb_open", Function::new(ctx.clone(), - move |name: String, version: u32| -> rquickjs::Result { - let result = s.open_database(&name, version).map_err(|e| js_err(e))?; - serde_json::to_string(&result).map_err(|e| js_err(e.to_string())) - }, - ))?; + ops.set( + "idb_open", + Function::new( + ctx.clone(), + move |name: String, version: u32| -> rquickjs::Result { + let result = s.open_database(&name, version).map_err(|e| js_err(e))?; + serde_json::to_string(&result).map_err(|e| js_err(e.to_string())) + }, + ), + )?; } { let s = storage.clone(); - ops.set("idb_close", Function::new(ctx.clone(), move |name: String| { - s.close_database(&name); - }))?; + ops.set( + "idb_close", + Function::new(ctx.clone(), move |name: String| { + s.close_database(&name); + }), + )?; } { let s = storage.clone(); - ops.set("idb_delete_database", Function::new(ctx.clone(), - move |name: String| -> rquickjs::Result<()> { + ops.set( + "idb_delete_database", + Function::new(ctx.clone(), move |name: String| -> rquickjs::Result<()> { s.delete_database(&name).map_err(|e| js_err(e)) - }, - ))?; + }), + )?; } { let s = storage.clone(); - ops.set("idb_create_object_store", Function::new(ctx.clone(), - move |db_name: String, store_name: String, options: String| -> rquickjs::Result<()> { - let opts: serde_json::Value = - serde_json::from_str(&options).map_err(|e| js_err(e.to_string()))?; - let key_path = opts["keyPath"].as_str(); - let auto_increment = opts["autoIncrement"].as_bool().unwrap_or(false); - s.create_object_store(&db_name, &store_name, key_path, auto_increment) - .map_err(|e| js_err(e)) - }, - ))?; + ops.set( + "idb_create_object_store", + Function::new( + ctx.clone(), + move |db_name: String, + store_name: String, + options: String| + -> rquickjs::Result<()> { + let opts: serde_json::Value = + serde_json::from_str(&options).map_err(|e| js_err(e.to_string()))?; + let key_path = opts["keyPath"].as_str(); + let auto_increment = opts["autoIncrement"].as_bool().unwrap_or(false); + s.create_object_store(&db_name, &store_name, key_path, auto_increment) + .map_err(|e| js_err(e)) + }, + ), + )?; } { let s = storage.clone(); - ops.set("idb_delete_object_store", Function::new(ctx.clone(), - move |db_name: String, store_name: String| -> rquickjs::Result<()> { - s.delete_object_store(&db_name, &store_name).map_err(|e| js_err(e)) - }, - ))?; + ops.set( + "idb_delete_object_store", + Function::new( + ctx.clone(), + move |db_name: String, store_name: String| -> rquickjs::Result<()> { + s.delete_object_store(&db_name, &store_name) + .map_err(|e| js_err(e)) + }, + ), + )?; } { let s = storage.clone(); - ops.set("idb_get", Function::new(ctx.clone(), - move |db_name: String, store_name: String, key: String| -> rquickjs::Result { - let key_val: serde_json::Value = - serde_json::from_str(&key).map_err(|e| js_err(e.to_string()))?; - let result = s.get(&db_name, &store_name, &key_val).map_err(|e| js_err(e))?; - serde_json::to_string(&result).map_err(|e| js_err(e.to_string())) - }, - ))?; + ops.set( + "idb_get", + Function::new( + ctx.clone(), + move |db_name: String, + store_name: String, + key: String| + -> rquickjs::Result { + let key_val: serde_json::Value = + serde_json::from_str(&key).map_err(|e| js_err(e.to_string()))?; + let result = s + .get(&db_name, &store_name, &key_val) + .map_err(|e| js_err(e))?; + serde_json::to_string(&result).map_err(|e| js_err(e.to_string())) + }, + ), + )?; } { let s = storage.clone(); - ops.set("idb_put", Function::new(ctx.clone(), - move |db_name: String, store_name: String, key: String, value: String| -> rquickjs::Result<()> { - let key_val: serde_json::Value = - serde_json::from_str(&key).map_err(|e| js_err(e.to_string()))?; - let value_val: serde_json::Value = - serde_json::from_str(&value).map_err(|e| js_err(e.to_string()))?; - s.put(&db_name, &store_name, &key_val, &value_val).map_err(|e| js_err(e)) - }, - ))?; + ops.set( + "idb_put", + Function::new( + ctx.clone(), + move |db_name: String, + store_name: String, + key: String, + value: String| + -> rquickjs::Result<()> { + let key_val: serde_json::Value = + serde_json::from_str(&key).map_err(|e| js_err(e.to_string()))?; + let value_val: serde_json::Value = + serde_json::from_str(&value).map_err(|e| js_err(e.to_string()))?; + s.put(&db_name, &store_name, &key_val, &value_val) + .map_err(|e| js_err(e)) + }, + ), + )?; } { let s = storage.clone(); - ops.set("idb_delete", Function::new(ctx.clone(), - move |db_name: String, store_name: String, key: String| -> rquickjs::Result<()> { - let key_val: serde_json::Value = - serde_json::from_str(&key).map_err(|e| js_err(e.to_string()))?; - s.delete(&db_name, &store_name, &key_val).map_err(|e| js_err(e)) - }, - ))?; + ops.set( + "idb_delete", + Function::new( + ctx.clone(), + move |db_name: String, store_name: String, key: String| -> rquickjs::Result<()> { + let key_val: serde_json::Value = + serde_json::from_str(&key).map_err(|e| js_err(e.to_string()))?; + s.delete(&db_name, &store_name, &key_val) + .map_err(|e| js_err(e)) + }, + ), + )?; } { let s = storage.clone(); - ops.set("idb_clear", Function::new(ctx.clone(), - move |db_name: String, store_name: String| -> rquickjs::Result<()> { - s.clear(&db_name, &store_name).map_err(|e| js_err(e)) - }, - ))?; + ops.set( + "idb_clear", + Function::new( + ctx.clone(), + move |db_name: String, store_name: String| -> rquickjs::Result<()> { + s.clear(&db_name, &store_name).map_err(|e| js_err(e)) + }, + ), + )?; } { let s = storage.clone(); - ops.set("idb_get_all", Function::new(ctx.clone(), - move |db_name: String, store_name: String, count: Option| -> rquickjs::Result { - let result = s.get_all(&db_name, &store_name, count).map_err(|e| js_err(e))?; - serde_json::to_string(&result).map_err(|e| js_err(e.to_string())) - }, - ))?; + ops.set( + "idb_get_all", + Function::new( + ctx.clone(), + move |db_name: String, + store_name: String, + count: Option| + -> rquickjs::Result { + let result = s + .get_all(&db_name, &store_name, count) + .map_err(|e| js_err(e))?; + serde_json::to_string(&result).map_err(|e| js_err(e.to_string())) + }, + ), + )?; } { let s = storage.clone(); - ops.set("idb_get_all_keys", Function::new(ctx.clone(), - move |db_name: String, store_name: String, count: Option| -> rquickjs::Result { - let result = s.get_all_keys(&db_name, &store_name, count).map_err(|e| js_err(e))?; - serde_json::to_string(&result).map_err(|e| js_err(e.to_string())) - }, - ))?; + ops.set( + "idb_get_all_keys", + Function::new( + ctx.clone(), + move |db_name: String, + store_name: String, + count: Option| + -> rquickjs::Result { + let result = s + .get_all_keys(&db_name, &store_name, count) + .map_err(|e| js_err(e))?; + serde_json::to_string(&result).map_err(|e| js_err(e.to_string())) + }, + ), + )?; } { let s = storage.clone(); - ops.set("idb_count", Function::new(ctx.clone(), - move |db_name: String, store_name: String| -> rquickjs::Result { - s.count(&db_name, &store_name).map_err(|e| js_err(e)) - }, - ))?; + ops.set( + "idb_count", + Function::new( + ctx.clone(), + move |db_name: String, store_name: String| -> rquickjs::Result { + s.count(&db_name, &store_name).map_err(|e| js_err(e)) + }, + ), + )?; } // ======================================================================== @@ -140,72 +214,99 @@ pub fn register<'js>(ctx: &Ctx<'js>, ops: &Object<'js>, storage: IdbStorage, ski { let s = storage.clone(); let sc = skill_context.clone(); - ops.set("db_exec", Function::new(ctx.clone(), - move |sql: String, params_json: Option| -> rquickjs::Result { - let params: Vec = if let Some(p) = params_json { - serde_json::from_str(&p).map_err(|e| js_err(e.to_string()))? - } else { - Vec::new() - }; - let rows = s.skill_db_exec(&sc.skill_id, &sql, ¶ms).map_err(|e| js_err(e))?; - Ok(rows as i64) - }, - ))?; + ops.set( + "db_exec", + Function::new( + ctx.clone(), + move |sql: String, params_json: Option| -> rquickjs::Result { + let params: Vec = if let Some(p) = params_json { + serde_json::from_str(&p).map_err(|e| js_err(e.to_string()))? + } else { + Vec::new() + }; + let rows = s + .skill_db_exec(&sc.skill_id, &sql, ¶ms) + .map_err(|e| js_err(e))?; + Ok(rows as i64) + }, + ), + )?; } { let s = storage.clone(); let sc = skill_context.clone(); - ops.set("db_get", Function::new(ctx.clone(), - move |sql: String, params_json: Option| -> rquickjs::Result { - let params: Vec = if let Some(p) = params_json { - serde_json::from_str(&p).map_err(|e| js_err(e.to_string()))? - } else { - Vec::new() - }; - let result = s.skill_db_get(&sc.skill_id, &sql, ¶ms).map_err(|e| js_err(e))?; - serde_json::to_string(&result).map_err(|e| js_err(e.to_string())) - }, - ))?; + ops.set( + "db_get", + Function::new( + ctx.clone(), + move |sql: String, params_json: Option| -> rquickjs::Result { + let params: Vec = if let Some(p) = params_json { + serde_json::from_str(&p).map_err(|e| js_err(e.to_string()))? + } else { + Vec::new() + }; + let result = s + .skill_db_get(&sc.skill_id, &sql, ¶ms) + .map_err(|e| js_err(e))?; + serde_json::to_string(&result).map_err(|e| js_err(e.to_string())) + }, + ), + )?; } { let s = storage.clone(); let sc = skill_context.clone(); - ops.set("db_all", Function::new(ctx.clone(), - move |sql: String, params_json: Option| -> rquickjs::Result { - let params: Vec = if let Some(p) = params_json { - serde_json::from_str(&p).map_err(|e| js_err(e.to_string()))? - } else { - Vec::new() - }; - let result = s.skill_db_all(&sc.skill_id, &sql, ¶ms).map_err(|e| js_err(e))?; - serde_json::to_string(&result).map_err(|e| js_err(e.to_string())) - }, - ))?; + ops.set( + "db_all", + Function::new( + ctx.clone(), + move |sql: String, params_json: Option| -> rquickjs::Result { + let params: Vec = if let Some(p) = params_json { + serde_json::from_str(&p).map_err(|e| js_err(e.to_string()))? + } else { + Vec::new() + }; + let result = s + .skill_db_all(&sc.skill_id, &sql, ¶ms) + .map_err(|e| js_err(e))?; + serde_json::to_string(&result).map_err(|e| js_err(e.to_string())) + }, + ), + )?; } { let s = storage.clone(); let sc = skill_context.clone(); - ops.set("db_kv_get", Function::new(ctx.clone(), - move |key: String| -> rquickjs::Result { - let result = s.skill_kv_get(&sc.skill_id, &key).map_err(|e| js_err(e))?; - serde_json::to_string(&result).map_err(|e| js_err(e.to_string())) - }, - ))?; + ops.set( + "db_kv_get", + Function::new( + ctx.clone(), + move |key: String| -> rquickjs::Result { + let result = s.skill_kv_get(&sc.skill_id, &key).map_err(|e| js_err(e))?; + serde_json::to_string(&result).map_err(|e| js_err(e.to_string())) + }, + ), + )?; } { let s = storage.clone(); let sc = skill_context.clone(); - ops.set("db_kv_set", Function::new(ctx.clone(), - move |key: String, value_json: String| -> rquickjs::Result<()> { - let value: serde_json::Value = - serde_json::from_str(&value_json).map_err(|e| js_err(e.to_string()))?; - s.skill_kv_set(&sc.skill_id, &key, &value).map_err(|e| js_err(e)) - }, - ))?; + ops.set( + "db_kv_set", + Function::new( + ctx.clone(), + move |key: String, value_json: String| -> rquickjs::Result<()> { + let value: serde_json::Value = + serde_json::from_str(&value_json).map_err(|e| js_err(e.to_string()))?; + s.skill_kv_set(&sc.skill_id, &key, &value) + .map_err(|e| js_err(e)) + }, + ), + )?; } // ======================================================================== @@ -215,45 +316,59 @@ pub fn register<'js>(ctx: &Ctx<'js>, ops: &Object<'js>, storage: IdbStorage, ski { let s = storage.clone(); let sc = skill_context.clone(); - ops.set("store_get", Function::new(ctx.clone(), - move |key: String| -> rquickjs::Result { - let result = s.skill_store_get(&sc.skill_id, &key).map_err(|e| js_err(e))?; - serde_json::to_string(&result).map_err(|e| js_err(e.to_string())) - }, - ))?; + ops.set( + "store_get", + Function::new( + ctx.clone(), + move |key: String| -> rquickjs::Result { + let result = s + .skill_store_get(&sc.skill_id, &key) + .map_err(|e| js_err(e))?; + serde_json::to_string(&result).map_err(|e| js_err(e.to_string())) + }, + ), + )?; } { let s = storage.clone(); let sc = skill_context.clone(); - ops.set("store_set", Function::new(ctx.clone(), - move |key: String, value_json: String| -> rquickjs::Result<()> { - let value: serde_json::Value = - serde_json::from_str(&value_json).map_err(|e| js_err(e.to_string()))?; - s.skill_store_set(&sc.skill_id, &key, &value).map_err(|e| js_err(e)) - }, - ))?; + ops.set( + "store_set", + Function::new( + ctx.clone(), + move |key: String, value_json: String| -> rquickjs::Result<()> { + let value: serde_json::Value = + serde_json::from_str(&value_json).map_err(|e| js_err(e.to_string()))?; + s.skill_store_set(&sc.skill_id, &key, &value) + .map_err(|e| js_err(e)) + }, + ), + )?; } { let s = storage.clone(); let sc = skill_context.clone(); - ops.set("store_delete", Function::new(ctx.clone(), - move |key: String| -> rquickjs::Result<()> { - s.skill_store_delete(&sc.skill_id, &key).map_err(|e| js_err(e)) - }, - ))?; + ops.set( + "store_delete", + Function::new(ctx.clone(), move |key: String| -> rquickjs::Result<()> { + s.skill_store_delete(&sc.skill_id, &key) + .map_err(|e| js_err(e)) + }), + )?; } { let s = storage; let sc = skill_context; - ops.set("store_keys", Function::new(ctx.clone(), - move || -> rquickjs::Result { + ops.set( + "store_keys", + Function::new(ctx.clone(), move || -> rquickjs::Result { let keys = s.skill_store_keys(&sc.skill_id).map_err(|e| js_err(e))?; serde_json::to_string(&keys).map_err(|e| js_err(e.to_string())) - }, - ))?; + }), + )?; } Ok(()) diff --git a/src-tauri/src/services/quickjs_libs/qjs_ops/types.rs b/src-tauri/src/services/quickjs_libs/qjs_ops/types.rs index a211f31ca..76105a3f6 100644 --- a/src-tauri/src/services/quickjs_libs/qjs_ops/types.rs +++ b/src-tauri/src/services/quickjs_libs/qjs_ops/types.rs @@ -121,12 +121,7 @@ pub struct WebSocketState { // Constants & Helpers // ============================================================================ -pub const ALLOWED_ENV_VARS: &[&str] = &[ - "VITE_BACKEND_URL", - "BACKEND_URL", - "JWT_TOKEN", - "NODE_ENV", -]; +pub const ALLOWED_ENV_VARS: &[&str] = &["VITE_BACKEND_URL", "BACKEND_URL", "JWT_TOKEN", "NODE_ENV"]; /// Sanitize error message for use with QuickJS/rquickjs. /// Some messages (e.g. from SQLite "Invalid symbol 45, offset 19") can trigger diff --git a/src-tauri/src/services/quickjs_libs/storage.rs b/src-tauri/src/services/quickjs_libs/storage.rs index 55b308ed4..2282773ec 100644 --- a/src-tauri/src/services/quickjs_libs/storage.rs +++ b/src-tauri/src/services/quickjs_libs/storage.rs @@ -201,7 +201,10 @@ impl IdbStorage { // Remove from registry conn_guard - .execute("DELETE FROM _idb_stores WHERE name = ?", params![store_name]) + .execute( + "DELETE FROM _idb_stores WHERE name = ?", + params![store_name], + ) .map_err(|e| format!("Failed to delete object store: {e}"))?; // Drop the table @@ -401,8 +404,7 @@ impl IdbStorage { .map(|v| -> Box { Box::new(json_to_sql(v)) }) .collect(); - let params_refs: Vec<&dyn rusqlite::ToSql> = - params.iter().map(|b| b.as_ref()).collect(); + let params_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|b| b.as_ref()).collect(); conn_guard .execute(sql, params_refs.as_slice()) @@ -425,8 +427,7 @@ impl IdbStorage { .map(|v| -> Box { Box::new(json_to_sql(v)) }) .collect(); - let params_refs: Vec<&dyn rusqlite::ToSql> = - params.iter().map(|b| b.as_ref()).collect(); + let params_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|b| b.as_ref()).collect(); let mut stmt = conn_guard .prepare(sql) @@ -470,8 +471,7 @@ impl IdbStorage { .map(|v| -> Box { Box::new(json_to_sql(v)) }) .collect(); - let params_refs: Vec<&dyn rusqlite::ToSql> = - params.iter().map(|b| b.as_ref()).collect(); + let params_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|b| b.as_ref()).collect(); let mut stmt = conn_guard .prepare(sql) @@ -519,9 +519,7 @@ impl IdbStorage { .ok(); match result { - Some(v) => { - serde_json::from_str(&v).map_err(|e| format!("Failed to parse value: {e}")) - } + Some(v) => serde_json::from_str(&v).map_err(|e| format!("Failed to parse value: {e}")), None => Ok(serde_json::Value::Null), } } @@ -624,7 +622,13 @@ impl IdbStorage { /// Sanitize a name for use as a table name. fn sanitize_name(name: &str) -> String { name.chars() - .map(|c| if c.is_alphanumeric() || c == '_' { c } else { '_' }) + .map(|c| { + if c.is_alphanumeric() || c == '_' { + c + } else { + '_' + } + }) .collect() } diff --git a/src-tauri/src/unified_skills/generator.rs b/src-tauri/src/unified_skills/generator.rs index f0969d2db..528bceaef 100644 --- a/src-tauri/src/unified_skills/generator.rs +++ b/src-tauri/src/unified_skills/generator.rs @@ -45,9 +45,12 @@ pub async fn generate_openhuman( "auto_start": false }); let manifest_path = skill_dir.join("manifest.json"); - tokio::fs::write(&manifest_path, serde_json::to_string_pretty(&manifest).unwrap()) - .await - .map_err(|e| format!("Failed to write manifest.json: {e}"))?; + tokio::fs::write( + &manifest_path, + serde_json::to_string_pretty(&manifest).unwrap(), + ) + .await + .map_err(|e| format!("Failed to write manifest.json: {e}"))?; // Write index.js — use full LLM-generated source when available, // otherwise build from the minimal template. @@ -55,9 +58,10 @@ pub async fn generate_openhuman( let index_js_content: String = if let Some(full) = spec.full_index_js.as_deref() { full.to_string() } else { - let tool_code = spec.tool_code.as_deref().unwrap_or( - "return { result: 'Generated skill executed successfully', args };", - ); + let tool_code = spec + .tool_code + .as_deref() + .unwrap_or("return { result: 'Generated skill executed successfully', args };"); let tool_fn_name = sanitize_fn_name(&spec.name); build_index_js(&tool_fn_name, &spec.description, tool_code) }; @@ -123,14 +127,17 @@ pub async fn generate_openclaw(spec: &GenerateSkillSpec) -> Result Result { /// always correctly escaped for embedding in a JS string literal. fn build_index_js(tool_fn: &str, description: &str, tool_code: &str) -> String { // serde_json::to_string produces a quoted, escaped JSON string literal. - let desc_json = serde_json::to_string(description) - .unwrap_or_else(|_| r#""unknown""#.to_string()); + let desc_json = + serde_json::to_string(description).unwrap_or_else(|_| r#""unknown""#.to_string()); format!( r#"// Auto-generated openhuman skill @@ -204,7 +211,13 @@ fn sanitize_fn_name(name: &str) -> String { let joined = name .to_lowercase() .chars() - .map(|c| if c.is_alphanumeric() || c == '_' { c } else { '_' }) + .map(|c| { + if c.is_alphanumeric() || c == '_' { + c + } else { + '_' + } + }) .collect::() .split('_') .filter(|s| !s.is_empty()) diff --git a/src-tauri/src/unified_skills/llm_generator.rs b/src-tauri/src/unified_skills/llm_generator.rs index de2e6a5f7..df8ae95b0 100644 --- a/src-tauri/src/unified_skills/llm_generator.rs +++ b/src-tauri/src/unified_skills/llm_generator.rs @@ -81,13 +81,9 @@ impl LlmGenerator { } /// Generate a skill from scratch based on `task_description`. - pub async fn generate_spec( - &self, - task_description: &str, - ) -> Result { - let prompt = format!( - "Generate a QuickJS skill for the following task:\n\n{task_description}" - ); + pub async fn generate_spec(&self, task_description: &str) -> Result { + let prompt = + format!("Generate a QuickJS skill for the following task:\n\n{task_description}"); let code = self.call_anthropic(SYSTEM_PROMPT, &prompt).await?; Ok(self.build_spec(task_description, code)) } @@ -114,11 +110,7 @@ impl LlmGenerator { // ----------------------------------------------------------------------- /// POST a single-turn chat to the Anthropic API and return the text response. - async fn call_anthropic( - &self, - system: &str, - user_message: &str, - ) -> Result { + async fn call_anthropic(&self, system: &str, user_message: &str) -> Result { let client = Client::builder() .timeout(std::time::Duration::from_secs(90)) .build() @@ -163,9 +155,7 @@ impl LlmGenerator { } else { &body }; - return Err(format!( - "Anthropic API error {status}: {trimmed}" - )); + return Err(format!("Anthropic API error {status}: {trimmed}")); } let resp: AnthropicResponse = response @@ -193,10 +183,7 @@ impl LlmGenerator { GenerateSkillSpec { name, - description: task_description - .chars() - .take(200) - .collect::(), + description: task_description.chars().take(200).collect::(), skill_type: "openhuman".to_string(), // `tool_code` holds the full source when `full_index_js` is set; // this ensures the fallback path in `generate_openhuman` also has @@ -224,37 +211,75 @@ impl LlmGenerator { fn smart_name(task_description: &str) -> String { // Preambles are checked case-insensitively (longest first to avoid partial matches). const PREAMBLES: &[&str] = &[ - "create a skill that ", "create a skill to ", "create a skill which ", - "create a skill for ", "create a skill ", - "write a skill that ", "write a skill to ", "write a skill which ", - "write a skill for ", "write a skill ", - "build a skill that ", "build a skill to ", "build a skill which ", - "build a skill for ", "build a skill ", - "make a skill that ", "make a skill to ", "make a skill which ", - "make a skill for ", "make a skill ", - "generate a skill that ","generate a skill to ","generate a skill which ", - "generate a skill for ", "generate a skill ", - "a skill that ", "a skill to ", "a skill which ", "a skill for ", + "create a skill that ", + "create a skill to ", + "create a skill which ", + "create a skill for ", + "create a skill ", + "write a skill that ", + "write a skill to ", + "write a skill which ", + "write a skill for ", + "write a skill ", + "build a skill that ", + "build a skill to ", + "build a skill which ", + "build a skill for ", + "build a skill ", + "make a skill that ", + "make a skill to ", + "make a skill which ", + "make a skill for ", + "make a skill ", + "generate a skill that ", + "generate a skill to ", + "generate a skill which ", + "generate a skill for ", + "generate a skill ", + "a skill that ", + "a skill to ", + "a skill which ", + "a skill for ", ]; // Leading fillers that add no meaning when they open the core phrase. const LEAD_FILLERS: &[&str] = &[ - "returns ", "return ", "fetches ", "fetch ", "gets ", "get ", - "shows ", "show ", "displays ", "display ", "outputs ", "output ", - "reads ", "read ", "calculates ", "calculate ", "computes ", "compute ", - "lists ", "list ", "the ", "a ", "an ", + "returns ", + "return ", + "fetches ", + "fetch ", + "gets ", + "get ", + "shows ", + "show ", + "displays ", + "display ", + "outputs ", + "output ", + "reads ", + "read ", + "calculates ", + "calculate ", + "computes ", + "compute ", + "lists ", + "list ", + "the ", + "a ", + "an ", ]; // Stop words skipped when selecting the 3 display tokens. const STOP_WORDS: &[&str] = &[ - "a", "an", "the", "of", "from", "in", "at", "by", "for", - "with", "using", "via", "and", "or", "as", "to", "that", + "a", "an", "the", "of", "from", "in", "at", "by", "for", "with", "using", "via", "and", + "or", "as", "to", "that", ]; let lower = task_description.to_lowercase(); // Step 1 — strip preamble - let preamble_skip = PREAMBLES.iter() + let preamble_skip = PREAMBLES + .iter() .find(|p| lower.starts_with(*p)) .map(|p| p.len()) .unwrap_or(0); @@ -262,12 +287,14 @@ fn smart_name(task_description: &str) -> String { // Step 2 — strip leading filler (up to two passes, e.g. "returns the ") let core_lower = core.to_lowercase(); - let after_filler1 = LEAD_FILLERS.iter() + let after_filler1 = LEAD_FILLERS + .iter() .find(|f| core_lower.starts_with(*f)) .map(|f| core[f.len()..].trim()) .unwrap_or(core); let after_filler1_lower = after_filler1.to_lowercase(); - let content = LEAD_FILLERS.iter() + let content = LEAD_FILLERS + .iter() .find(|f| after_filler1_lower.starts_with(*f)) .map(|f| after_filler1[f.len()..].trim()) .unwrap_or(after_filler1); @@ -427,10 +454,7 @@ mod tests { #[test] fn smart_name_get_btc_eth() { - assert_eq!( - smart_name("Get BTC/ETH prices (live)"), - "BTC ETH Prices" - ); + assert_eq!(smart_name("Get BTC/ETH prices (live)"), "BTC ETH Prices"); } #[test] diff --git a/src-tauri/src/unified_skills/mod.rs b/src-tauri/src/unified_skills/mod.rs index 18427f5d2..a4d37ff69 100644 --- a/src-tauri/src/unified_skills/mod.rs +++ b/src-tauri/src/unified_skills/mod.rs @@ -88,7 +88,10 @@ impl UnifiedSkillRegistry { id: manifest.id.clone(), name: manifest.name.clone(), skill_type: manifest.skill_type.clone(), - version: manifest.version.clone().unwrap_or_else(|| "0.1.0".to_string()), + version: manifest + .version + .clone() + .unwrap_or_else(|| "0.1.0".to_string()), description: manifest.description.clone().unwrap_or_default(), tools, setup: manifest.setup.clone(), @@ -227,7 +230,9 @@ impl UnifiedSkillRegistry { setup: None, }) } - other => Err(format!("Unknown skill_type: '{other}'. Use 'openhuman' or 'openclaw'.")), + other => Err(format!( + "Unknown skill_type: '{other}'. Use 'openhuman' or 'openclaw'." + )), } } } diff --git a/src-tauri/src/unified_skills/self_evolve.rs b/src-tauri/src/unified_skills/self_evolve.rs index 8317b27c5..42f8cc080 100644 --- a/src-tauri/src/unified_skills/self_evolve.rs +++ b/src-tauri/src/unified_skills/self_evolve.rs @@ -88,13 +88,7 @@ impl SkillEvolver { let loop_result = tokio::time::timeout( std::time::Duration::from_secs(timeout_secs), - Self::run_loop( - registry, - api_key, - task_description, - max_iter, - on_progress, - ), + Self::run_loop(registry, api_key, task_description, max_iter, on_progress), ) .await; @@ -137,11 +131,7 @@ impl SkillEvolver { let prev_code = last_spec .as_ref() .and_then(|s| s.full_index_js.clone()) - .or_else(|| { - last_spec - .as_ref() - .and_then(|s| s.tool_code.clone()) - }) + .or_else(|| last_spec.as_ref().and_then(|s| s.tool_code.clone())) .unwrap_or_default(); generator_llm diff --git a/src-tauri/src/unified_skills/skill_tester.rs b/src-tauri/src/unified_skills/skill_tester.rs index f392106d6..fc55c4804 100644 --- a/src-tauri/src/unified_skills/skill_tester.rs +++ b/src-tauri/src/unified_skills/skill_tester.rs @@ -45,10 +45,7 @@ impl SkillTester { // rquickjs AsyncRuntime is Send so we can use tokio::task::spawn_blocking // or just run inline — we use spawn_blocking to avoid blocking the executor // on a potentially long-running JS init/start sequence. - let result = tokio::task::spawn_blocking(move || { - run_in_sync_context(&js_source) - }) - .await; + let result = tokio::task::spawn_blocking(move || run_in_sync_context(&js_source)).await; match result { Ok(test_result) => test_result, @@ -306,20 +303,15 @@ async fn run_async_test(js_source: &str) -> TestResult { } Ok(s) if s == "__promise__" => { // Drive promises until done - let deadline = - tokio::time::Instant::now() + std::time::Duration::from_secs(10); + let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(10); loop { drive_jobs(&qjs_rt).await; let done = ctx .with(move |js_ctx| { - let check = format!( - "globalThis.__testToolDone_{idx} === true", - idx = idx - ); - js_ctx - .eval::(check.as_bytes()) - .unwrap_or(false) + let check = + format!("globalThis.__testToolDone_{idx} === true", idx = idx); + js_ctx.eval::(check.as_bytes()).unwrap_or(false) }) .await; @@ -388,10 +380,7 @@ async fn run_async_test(js_source: &str) -> TestResult { TestResult { passed: true, output: if tool_outputs.is_empty() { - format!( - "All {} tool(s) passed", - tool_count - ) + format!("All {} tool(s) passed", tool_count) } else { tool_outputs.join("; ") }, @@ -481,11 +470,7 @@ async fn call_lifecycle_fn( }) .await; - return if err.is_empty() { - Ok(()) - } else { - Err(err) - }; + return if err.is_empty() { Ok(()) } else { Err(err) }; } if tokio::time::Instant::now() > deadline { diff --git a/src/pages/Conversations.tsx b/src/pages/Conversations.tsx index 5c90e0cd5..ad55aef5b 100644 --- a/src/pages/Conversations.tsx +++ b/src/pages/Conversations.tsx @@ -297,8 +297,7 @@ const Conversations = () => { const currentState = store.getState() as { thread: { messagesByThreadId: Record }; }; - const threadMessages = - currentState.thread.messagesByThreadId[event.thread_id] || []; + const threadMessages = currentState.thread.messagesByThreadId[event.thread_id] || []; const lastMsg = threadMessages[threadMessages.length - 1]; if ( lastMsg?.sender === 'agent' && From 5e29a5dddbdd1a7d357b95a0b65727ec51870d85 Mon Sep 17 00:00:00 2001 From: cyrus Date: Fri, 27 Mar 2026 18:13:28 +0530 Subject: [PATCH 4/4] docs: add mandatory pre-completion checks to contribution guidelines - Included formatting, linting, and compilation checks for JavaScript and Rust. - Specified tools and commands for verifying code quality before marking tasks complete. --- CLAUDE.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 79de7a705..49f8bbc96 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -471,6 +471,12 @@ Key updates from recent commits (cd9ebcd to current): ## Key Patterns +- **MANDATORY: Pre-completion checks**: Before considering ANY task complete, ALWAYS run these checks and fix all errors: + 1. `npx prettier --check .` (formatting) + 2. `npx eslint .` (lint) + 3. `npx tsc --noEmit` (TypeScript) + 4. `cargo fmt --manifest-path src-tauri/Cargo.toml` (Rust formatting, if Rust files were changed) + 5. `cargo check --manifest-path src-tauri/Cargo.toml` (Rust compilation, if Rust files were changed) - **Code Quality**: ESLint and Prettier enforce code standards with Husky hooks. Use type-only imports (`import type`) and consolidate imports from same modules. - **No dynamic imports**: All imports must be static `import` statements at the top of the file. Do not use `await import()` or `import().then()` inside functions or code blocks. Use try/catch around Tauri API calls for non-Tauri environments instead. - **No localStorage**: Avoid `localStorage` and `sessionStorage`; use Redux (and persist) for app state. Remove any direct usage when working on affected code.