Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,13 @@ windows-sys = { version = "0.59", features = [
# cef::Window internal handle, not the visible Chrome_WidgetWin_1
# top-level frame, so we walk the OS window list ourselves (#1607).
"Win32_UI_WindowsAndMessaging",
# CreateMutexW / CloseHandle — used by the pre-CEF single-instance guard
# (see run() in lib.rs) that detects a second launch before CefRuntime::init
# fires (Sentry OPENHUMAN-TAURI-A).
# Win32_Security is required because CreateMutexW's SECURITY_ATTRIBUTES
# parameter is gated behind it in windows-sys 0.59.
"Win32_System_Threading",
"Win32_Security",
] }

[features]
Expand Down
58 changes: 58 additions & 0 deletions app/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1404,6 +1404,64 @@ pub fn run() {
// Install the ring provider once before any HTTPS client is built.
let _ = rustls::crypto::ring::default_provider().install_default();

// ── Windows pre-CEF single-instance guard (Sentry OPENHUMAN-TAURI-A) ──
//
// `tauri_plugin_single_instance` detects a second launch inside its
// `.setup()` hook — but `.setup()` runs AFTER `Builder::build()` which
// calls `CefRuntime::init` → `cef::initialize()`. On a second launch,
// `cef::initialize()` returns 0 because the primary holds the CEF
// cache lock; the vendored runtime asserts `result == 1` and panics
// (left: 0, right: 1, fatal, Windows-only, 598 events).
//
// Fix: acquire a named Win32 mutex at the very top of `run()` — before
// any CEF or builder work — so any secondary instance sees
// `ERROR_ALREADY_EXISTS` and exits immediately. The mutex name uses
// a `-cef-init` suffix distinct from the plugin's own `-sim` mutex so
// the two guards don't interfere; the plugin still handles WM_COPYDATA
// forwarding for graceful "focus primary" behaviour once the app is
// fully initialised.
//
// The RAII guard holds the mutex handle for the lifetime of `run()`.
// Windows releases all process handles automatically on exit, so
// explicit cleanup is only needed if `run()` returns normally.
#[cfg(windows)]
let _cef_init_mutex_guard = {
use windows_sys::Win32::Foundation::{CloseHandle, GetLastError, ERROR_ALREADY_EXISTS};
use windows_sys::Win32::System::Threading::CreateMutexW;

// Must match the bundle identifier in tauri.conf.json.
// Changing the app identifier requires updating this string too.
let mutex_name: Vec<u16> = "com.openhuman.app-cef-init\0".encode_utf16().collect();

// SAFETY: mutex_name is null-terminated UTF-16; handle is checked below.
let handle = unsafe { CreateMutexW(std::ptr::null(), 0, mutex_name.as_ptr()) };

if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS {
// Another instance is already past this point — exit before we
// touch CEF at all. The plugin's WM_COPYDATA path won't run
// here (it needs an AppHandle from setup()), but the primary
// is already showing its window so the user experience is fine.
if !handle.is_null() {
unsafe { CloseHandle(handle) };
}
log::info!(
"[single-instance] pre-CEF mutex held by primary; secondary exiting (OPENHUMAN-TAURI-A fix)"
);
std::process::exit(0);
}

// Primary: hold the handle until run() returns.
struct OwnedMutex(isize);
impl Drop for OwnedMutex {
fn drop(&mut self) {
if self.0 != 0 {
unsafe { CloseHandle(self.0 as _) };
}
}
}
OwnedMutex(handle as isize)
};

// CEF cache-lock preflight (macOS only): if another OpenHuman instance
// is already holding the CEF user-data-dir, the vendored
// `tauri-runtime-cef` panics inside `cef::initialize` with a Rust
Expand Down
29 changes: 24 additions & 5 deletions src/openhuman/providers/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -404,15 +404,34 @@ pub fn create_intelligent_routing_provider(
config: &crate::openhuman::config::Config,
options: &ProviderRuntimeOptions,
) -> anyhow::Result<Box<dyn Provider>> {
let backend = create_backend_inference_provider(inference_url, backend_url, api_key, options)?;
let raw_backend =
create_backend_inference_provider(inference_url, backend_url, api_key, options)?;
// Wrap the raw backend in ReliableProvider so transient 502/503/504 errors
// are retried before propagating to the agent turn. Without this, a single
// 502 from the backend bypasses the retry layer entirely and surfaces as a
// fatal `run_single` failure.
log::debug!(
"[providers] initialising reliable wrapper: retries={} backoff_ms={} fallbacks={}",
config.reliability.provider_retries,
config.reliability.provider_backoff_ms,
config.reliability.model_fallbacks.len()
);
let reliable_backend: Box<dyn Provider> = Box::new(
reliable::ReliableProvider::new(
vec![(INFERENCE_BACKEND_ID.to_string(), raw_backend)],
config.reliability.provider_retries,
config.reliability.provider_backoff_ms,
)
.with_model_fallbacks(config.reliability.model_fallbacks.clone()),
);
let default_model = config
.default_model
.as_deref()
.unwrap_or(crate::openhuman::config::DEFAULT_MODEL);

// When the user has configured `model_routes` (custom provider via
// BackendProviderPanel), wrap the remote in a RouterProvider so abstract
// tier names like `reasoning-v1` get translated to the configured
// BackendProviderPanel), wrap the reliable remote in a RouterProvider so
// abstract tier names like `reasoning-v1` get translated to the configured
// provider-specific model id (e.g. `gpt-5.5`) BEFORE the request leaves
// the host. Without this step the abstract tier name would reach
// `custom_openai` and 404. The OpenHuman backend can dispatch tier names
Expand All @@ -424,10 +443,10 @@ pub fn create_intelligent_routing_provider(
inference_url.is_some()
);
let remote: Box<dyn Provider> = if config.model_routes.is_empty() {
backend
reliable_backend
} else {
let providers: Vec<(String, Box<dyn Provider>)> =
vec![(INFERENCE_BACKEND_ID.to_string(), backend)];
vec![(INFERENCE_BACKEND_ID.to_string(), reliable_backend)];
let routes: Vec<(String, router::Route)> = config
.model_routes
.iter()
Expand Down
Loading