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. 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 a9a1ea124..813504897 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() @@ -436,15 +434,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 +493,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 +532,19 @@ 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); @@ -583,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 { @@ -932,10 +934,19 @@ async fn chat_send_inner( } // Parse the completion response - let completion: ChatCompletionResponse = response - .json() - .await - .map_err(|e| format!("Failed to parse inference response: {}", e))?; + 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 + })?; // Accumulate token usage if let Some(ref usage) = completion.usage { @@ -943,10 +954,19 @@ async fn chat_send_inner( total_output_tokens += usage.completion_tokens; } - let choice = completion - .choices - .first() - .ok_or_else(|| "No choices in inference response".to_string())?; + 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 + })?; log::info!( "[chat] Round {} — finish_reason={:?}, tool_calls={}", @@ -989,9 +1009,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( @@ -1056,9 +1075,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::>() @@ -1067,9 +1084,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) @@ -1162,7 +1177,19 @@ 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); @@ -1307,20 +1334,38 @@ async fn chat_send_mobile( return Err(msg); } - let completion: ChatCompletionResponse = response - .json() - .await - .map_err(|e| format!("Failed to parse inference response: {}", e))?; + 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 + })?; 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(|| "No choices in inference response".to_string())?; + 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 + })?; 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 995a118ba..beb6f02c6 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.get_ingestion_job(&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 = skill_id.to_string(); 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 2446ba868..ad55aef5b 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); @@ -293,6 +293,19 @@ 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.', @@ -340,7 +353,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) {