From c42e24960b4ea2af35351fdd191296ef17e8f5b2 Mon Sep 17 00:00:00 2001 From: Mega Mind <146339422+M3gA-Mind@users.noreply.github.com> Date: Tue, 26 May 2026 16:42:16 +0530 Subject: [PATCH] fix: runtime snapshot timeouts, config perms, stale lock recovery, summarization-v1 tier, loopback OAuth UX (#2690) --- .../components/oauth/OAuthProviderButton.tsx | 5 ++ app/src/lib/i18n/chunks/ar-4.ts | 2 + app/src/lib/i18n/chunks/bn-4.ts | 2 + app/src/lib/i18n/chunks/de-4.ts | 2 + app/src/lib/i18n/chunks/en-4.ts | 2 + app/src/lib/i18n/chunks/es-4.ts | 2 + app/src/lib/i18n/chunks/fr-4.ts | 2 + app/src/lib/i18n/chunks/hi-4.ts | 2 + app/src/lib/i18n/chunks/id-4.ts | 2 + app/src/lib/i18n/chunks/it-4.ts | 2 + app/src/lib/i18n/chunks/ko-4.ts | 2 + app/src/lib/i18n/chunks/pt-4.ts | 2 + app/src/lib/i18n/chunks/ru-4.ts | 2 + app/src/lib/i18n/chunks/zh-CN-4.ts | 1 + app/src/lib/i18n/en.ts | 2 + app/src/utils/loopbackOauthListener.ts | 2 +- src/openhuman/app_state/ops.rs | 64 +++++++++++++++---- src/openhuman/config/mod.rs | 4 +- src/openhuman/config/schema/load.rs | 34 ++++++++-- src/openhuman/config/schema/types.rs | 1 + src/openhuman/credentials/profiles.rs | 6 +- src/openhuman/inference/provider/factory.rs | 7 +- src/openhuman/socket/ws_loop.rs | 11 +++- 23 files changed, 132 insertions(+), 29 deletions(-) diff --git a/app/src/components/oauth/OAuthProviderButton.tsx b/app/src/components/oauth/OAuthProviderButton.tsx index 747df9593..8a18de41d 100644 --- a/app/src/components/oauth/OAuthProviderButton.tsx +++ b/app/src/components/oauth/OAuthProviderButton.tsx @@ -263,6 +263,11 @@ const OAuthProviderButton = ({ }) .catch(err => { warnLog('[%s] loopback callback failed', provider.id, err); + const isTimeout = err instanceof Error && err.message.includes('timed out'); + if (isTimeout) { + setIsLoading(false); + setStartupError(t('oauth.button.loopbackTimeout')); + } }); } diff --git a/app/src/lib/i18n/chunks/ar-4.ts b/app/src/lib/i18n/chunks/ar-4.ts index 9beae8a10..a0edc7989 100644 --- a/app/src/lib/i18n/chunks/ar-4.ts +++ b/app/src/lib/i18n/chunks/ar-4.ts @@ -145,6 +145,8 @@ const ar4: TranslationMap = { 'notifications.center.filterAll': 'تصفية الكل', 'notifications.center.markAllRead': 'تحديد الكل كمقروء', 'notifications.center.title': 'الإشعارات', + 'oauth.button.loopbackTimeout': + 'انتهت مهلة تسجيل الدخول — لم يكتمل المتصفح إعادة توجيه OAuth. يرجى المحاولة مرة أخرى.', 'oauth.button.connecting': 'جارٍ الاتصال...', 'oauth.login.continueWith': 'المتابعة مع', 'onboarding.contextGathering.buildingDesc': 'وصف البناء', diff --git a/app/src/lib/i18n/chunks/bn-4.ts b/app/src/lib/i18n/chunks/bn-4.ts index 045b02eb0..a02cc57d5 100644 --- a/app/src/lib/i18n/chunks/bn-4.ts +++ b/app/src/lib/i18n/chunks/bn-4.ts @@ -146,6 +146,8 @@ const bn4: TranslationMap = { 'notifications.center.filterAll': 'সব ফিল্টার', 'notifications.center.markAllRead': 'সব পঠিত চিহ্নিত করুন', 'notifications.center.title': 'বিজ্ঞপ্তি', + 'oauth.button.loopbackTimeout': + 'সাইন-ইন টাইম আউট হয়েছে — ব্রাউজার OAuth পুনর্নির্দেশনা সম্পন্ন করেনি। অনুগ্রহ করে আবার চেষ্টা করুন।', 'oauth.button.connecting': 'সংযোগ হচ্ছে...', 'oauth.login.continueWith': 'দিয়ে চালিয়ে যান', 'onboarding.contextGathering.buildingDesc': 'বিল্ডিং বিবরণ', diff --git a/app/src/lib/i18n/chunks/de-4.ts b/app/src/lib/i18n/chunks/de-4.ts index 458c8b2ec..5a3489e3b 100644 --- a/app/src/lib/i18n/chunks/de-4.ts +++ b/app/src/lib/i18n/chunks/de-4.ts @@ -146,6 +146,8 @@ const de4: TranslationMap = { 'notifications.center.filterAll': 'Alles filtern', 'notifications.center.markAllRead': 'Alles als gelesen markieren', 'notifications.center.title': 'Benachrichtigungen', + 'oauth.button.loopbackTimeout': + 'Anmeldung abgelaufen — der Browser hat die OAuth-Weiterleitung nicht abgeschlossen. Bitte versuche es erneut.', 'oauth.button.connecting': 'Verbinden...', 'oauth.login.continueWith': 'Weiter mit', 'onboarding.contextGathering.buildingDesc': 'Gebäudebeschreibung', diff --git a/app/src/lib/i18n/chunks/en-4.ts b/app/src/lib/i18n/chunks/en-4.ts index 3f1eb85f4..ff06674f0 100644 --- a/app/src/lib/i18n/chunks/en-4.ts +++ b/app/src/lib/i18n/chunks/en-4.ts @@ -154,6 +154,8 @@ const en4: TranslationMap = { 'notifications.center.filterAll': 'Filter all', 'notifications.center.markAllRead': 'Mark all read', 'notifications.center.title': 'Notifications', + 'oauth.button.loopbackTimeout': + 'Sign-in timed out — the browser did not complete the OAuth redirect. Please try again.', 'oauth.button.connecting': 'Connecting...', 'oauth.login.continueWith': 'Continue with', 'onboarding.contextGathering.buildingDesc': 'Building desc', diff --git a/app/src/lib/i18n/chunks/es-4.ts b/app/src/lib/i18n/chunks/es-4.ts index 28aa71730..267f7639b 100644 --- a/app/src/lib/i18n/chunks/es-4.ts +++ b/app/src/lib/i18n/chunks/es-4.ts @@ -146,6 +146,8 @@ const es4: TranslationMap = { 'notifications.center.filterAll': 'Filtrar todo', 'notifications.center.markAllRead': 'Marcar todo como leído', 'notifications.center.title': 'Notificaciones', + 'oauth.button.loopbackTimeout': + 'El inicio de sesión expiró — el navegador no completó la redirección OAuth. Por favor, inténtalo de nuevo.', 'oauth.button.connecting': 'Conectando...', 'oauth.login.continueWith': 'Continuar con', 'onboarding.contextGathering.buildingDesc': 'Descripción de construcción', diff --git a/app/src/lib/i18n/chunks/fr-4.ts b/app/src/lib/i18n/chunks/fr-4.ts index 457b38e9f..6a11e0c30 100644 --- a/app/src/lib/i18n/chunks/fr-4.ts +++ b/app/src/lib/i18n/chunks/fr-4.ts @@ -146,6 +146,8 @@ const fr4: TranslationMap = { 'notifications.center.filterAll': 'Tout filtrer', 'notifications.center.markAllRead': 'Tout marquer comme lu', 'notifications.center.title': 'Notifications', + 'oauth.button.loopbackTimeout': + "La connexion a expiré — le navigateur n'a pas complété la redirection OAuth. Veuillez réessayer.", 'oauth.button.connecting': 'Connexion en cours…', 'oauth.login.continueWith': 'Continuer avec', 'onboarding.contextGathering.buildingDesc': 'Description de la construction', diff --git a/app/src/lib/i18n/chunks/hi-4.ts b/app/src/lib/i18n/chunks/hi-4.ts index 973102cfe..907c98830 100644 --- a/app/src/lib/i18n/chunks/hi-4.ts +++ b/app/src/lib/i18n/chunks/hi-4.ts @@ -146,6 +146,8 @@ const hi4: TranslationMap = { 'notifications.center.filterAll': 'सभी फिल्टर', 'notifications.center.markAllRead': 'सभी पढ़ा हुआ मार्क करें', 'notifications.center.title': 'नोटिफिकेशन', + 'oauth.button.loopbackTimeout': + 'साइन-इन का समय समाप्त हो गया — ब्राउज़र ने OAuth पुनर्निर्देशन पूरा नहीं किया। कृपया पुनः प्रयास करें।', 'oauth.button.connecting': 'कनेक्ट हो रहा है...', 'oauth.login.continueWith': 'के साथ जारी रखें', 'onboarding.contextGathering.buildingDesc': 'बिल्डिंग विवरण', diff --git a/app/src/lib/i18n/chunks/id-4.ts b/app/src/lib/i18n/chunks/id-4.ts index f520d4bb8..aa9b33980 100644 --- a/app/src/lib/i18n/chunks/id-4.ts +++ b/app/src/lib/i18n/chunks/id-4.ts @@ -146,6 +146,8 @@ const id4: TranslationMap = { 'notifications.center.filterAll': 'Filter semua', 'notifications.center.markAllRead': 'Tandai semua sudah dibaca', 'notifications.center.title': 'Notifikasi', + 'oauth.button.loopbackTimeout': + 'Masuk habis waktu — browser tidak menyelesaikan pengalihan OAuth. Silakan coba lagi.', 'oauth.button.connecting': 'Menghubungkan...', 'oauth.login.continueWith': 'Lanjutkan dengan', 'onboarding.contextGathering.buildingDesc': 'Deskripsi pembangunan', diff --git a/app/src/lib/i18n/chunks/it-4.ts b/app/src/lib/i18n/chunks/it-4.ts index 0179e4774..b0109588f 100644 --- a/app/src/lib/i18n/chunks/it-4.ts +++ b/app/src/lib/i18n/chunks/it-4.ts @@ -146,6 +146,8 @@ const it4: TranslationMap = { 'notifications.center.filterAll': 'Filtra tutte', 'notifications.center.markAllRead': 'Segna tutte come lette', 'notifications.center.title': 'Notifiche', + 'oauth.button.loopbackTimeout': + 'Accesso scaduto — il browser non ha completato il reindirizzamento OAuth. Riprova.', 'oauth.button.connecting': 'Connessione...', 'oauth.login.continueWith': 'Continua con', 'onboarding.contextGathering.buildingDesc': 'Descrizione costruzione', diff --git a/app/src/lib/i18n/chunks/ko-4.ts b/app/src/lib/i18n/chunks/ko-4.ts index a11cc601a..8651e859d 100644 --- a/app/src/lib/i18n/chunks/ko-4.ts +++ b/app/src/lib/i18n/chunks/ko-4.ts @@ -137,6 +137,8 @@ const ko4: TranslationMap = { 'notifications.center.filterAll': '전체 필터', 'notifications.center.markAllRead': '모두 읽음으로 표시', 'notifications.center.title': '알림', + 'oauth.button.loopbackTimeout': + '로그인 시간 초과 — 브라우저가 OAuth 리디렉션을 완료하지 못했습니다. 다시 시도해 주세요.', 'oauth.button.connecting': '연결 중...', 'oauth.login.continueWith': '다음으로 계속:', 'onboarding.contextGathering.buildingDesc': '작성 설명', diff --git a/app/src/lib/i18n/chunks/pt-4.ts b/app/src/lib/i18n/chunks/pt-4.ts index 30af40275..2c4520ff1 100644 --- a/app/src/lib/i18n/chunks/pt-4.ts +++ b/app/src/lib/i18n/chunks/pt-4.ts @@ -147,6 +147,8 @@ const pt4: TranslationMap = { 'notifications.center.filterAll': 'Filtrar todos', 'notifications.center.markAllRead': 'Marcar todos como lidos', 'notifications.center.title': 'Notificações', + 'oauth.button.loopbackTimeout': + 'Login expirou — o navegador não concluiu o redirecionamento OAuth. Por favor, tente novamente.', 'oauth.button.connecting': 'Conectando...', 'oauth.login.continueWith': 'Continuar com', 'onboarding.contextGathering.buildingDesc': 'Descrição de construção', diff --git a/app/src/lib/i18n/chunks/ru-4.ts b/app/src/lib/i18n/chunks/ru-4.ts index 4775c17d1..f7c71e07a 100644 --- a/app/src/lib/i18n/chunks/ru-4.ts +++ b/app/src/lib/i18n/chunks/ru-4.ts @@ -146,6 +146,8 @@ const ru4: TranslationMap = { 'notifications.center.filterAll': 'Все', 'notifications.center.markAllRead': 'Отметить всё прочитанным', 'notifications.center.title': 'Уведомления', + 'oauth.button.loopbackTimeout': + 'Время входа истекло — браузер не завершил перенаправление OAuth. Пожалуйста, попробуйте снова.', 'oauth.button.connecting': 'Подключение...', 'oauth.login.continueWith': 'Продолжить через', 'onboarding.contextGathering.buildingDesc': 'Сборка профиля', diff --git a/app/src/lib/i18n/chunks/zh-CN-4.ts b/app/src/lib/i18n/chunks/zh-CN-4.ts index 21a742ce9..02dc31d08 100644 --- a/app/src/lib/i18n/chunks/zh-CN-4.ts +++ b/app/src/lib/i18n/chunks/zh-CN-4.ts @@ -143,6 +143,7 @@ const zhCN4: TranslationMap = { 'notifications.center.filterAll': '全部', 'notifications.center.markAllRead': '全部标记已读', 'notifications.center.title': '通知', + 'oauth.button.loopbackTimeout': '登录超时 — 浏览器未完成 OAuth 跳转。请重试。', 'oauth.button.connecting': '连接中...', 'oauth.login.continueWith': '继续使用', 'onboarding.contextGathering.buildingDesc': '正在分析你的历史记录...', diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index a191aceba..1169c00fc 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -2254,6 +2254,8 @@ const en: TranslationMap = { 'notifications.center.markAllRead': 'Mark all read', 'notifications.center.title': 'Notifications', 'oauth.button.connecting': 'Connecting...', + 'oauth.button.loopbackTimeout': + 'Sign-in timed out — the browser did not complete the OAuth redirect. Please try again.', 'oauth.login.continueWith': 'Continue with', 'onboarding.contextGathering.buildingDesc': 'Gathering context from your connected accounts…', 'onboarding.contextGathering.buildingProfile': 'Building your profile...', diff --git a/app/src/utils/loopbackOauthListener.ts b/app/src/utils/loopbackOauthListener.ts index 5b90250fa..0693d2460 100644 --- a/app/src/utils/loopbackOauthListener.ts +++ b/app/src/utils/loopbackOauthListener.ts @@ -19,7 +19,7 @@ import { isTauri } from './tauriCommands/common'; */ const DEFAULT_PORT = 53824; -const DEFAULT_TIMEOUT_SECS = 300; +const DEFAULT_TIMEOUT_SECS = 60; const CALLBACK_EVENT = 'loopback-oauth-callback'; export interface LoopbackHandle { diff --git a/src/openhuman/app_state/ops.rs b/src/openhuman/app_state/ops.rs index 9dd41f286..26ccc4aa8 100644 --- a/src/openhuman/app_state/ops.rs +++ b/src/openhuman/app_state/ops.rs @@ -34,6 +34,7 @@ const CURRENT_USER_REFRESH_TTL: Duration = Duration::from_secs(5); const RUNTIME_SNAPSHOT_TTL: Duration = Duration::from_secs(2); const AUTH_FETCH_TIMEOUT: Duration = Duration::from_secs(5); const RUNTIME_SNAPSHOT_TIMEOUT: Duration = Duration::from_secs(10); +const SNAPSHOT_SUB_OP_TIMEOUT: Duration = Duration::from_secs(5); static APP_STATE_FILE_LOCK: Lazy> = Lazy::new(|| Mutex::new(())); static CURRENT_USER_CACHE: Lazy>> = Lazy::new(|| Mutex::new(None)); static RUNTIME_SNAPSHOT_CACHE: Lazy>> = @@ -439,34 +440,71 @@ async fn build_runtime_snapshot(config: &Config, req_id: u64) -> RuntimeSnapshot let (screen_intelligence, local_ai, autocomplete, service) = tokio::join!( async { let t = Instant::now(); - let _ = crate::openhuman::screen_intelligence::global_engine() - .apply_config(si_config) - .await; - let status = crate::openhuman::screen_intelligence::global_engine() - .status() - .await; + let status = match tokio::time::timeout(SNAPSHOT_SUB_OP_TIMEOUT, async { + let _ = crate::openhuman::screen_intelligence::global_engine() + .apply_config(si_config) + .await; + crate::openhuman::screen_intelligence::global_engine() + .status() + .await + }) + .await + { + Ok(s) => s, + Err(_) => { + warn!( + "{LOG_PREFIX} screen_intelligence timed out after {}s; using degraded sub-snapshot req_id={}", + SNAPSHOT_SUB_OP_TIMEOUT.as_secs(), + req_id, + ); + degraded_runtime_snapshot(config).screen_intelligence + } + }; (status, t.elapsed().as_millis()) }, async { let t = Instant::now(); - let status = match crate::openhuman::inference::rpc::inference_status( - &config_for_local_ai, + let status = match tokio::time::timeout( + SNAPSHOT_SUB_OP_TIMEOUT, + crate::openhuman::inference::rpc::inference_status(&config_for_local_ai), ) .await { - Ok(outcome) => outcome.value, - Err(error) => { + Ok(Ok(outcome)) => outcome.value, + Ok(Err(error)) => { warn!("{LOG_PREFIX} local_ai status failed during snapshot: {error}"); crate::openhuman::inference::LocalAiStatus::disabled(&config_for_local_ai) } + Err(_) => { + warn!( + "{LOG_PREFIX} local_ai timed out after {}s; using degraded sub-snapshot req_id={}", + SNAPSHOT_SUB_OP_TIMEOUT.as_secs(), + req_id, + ); + crate::openhuman::inference::LocalAiStatus::disabled(&config_for_local_ai) + } }; (status, t.elapsed().as_millis()) }, async { let t = Instant::now(); - let status = crate::openhuman::autocomplete::global_engine() - .status_with_config(&config_for_autocomplete) - .await; + let status = match tokio::time::timeout( + SNAPSHOT_SUB_OP_TIMEOUT, + crate::openhuman::autocomplete::global_engine() + .status_with_config(&config_for_autocomplete), + ) + .await + { + Ok(s) => s, + Err(_) => { + warn!( + "{LOG_PREFIX} autocomplete timed out after {}s; using degraded sub-snapshot req_id={}", + SNAPSHOT_SUB_OP_TIMEOUT.as_secs(), + req_id, + ); + degraded_runtime_snapshot(config).autocomplete + } + }; (status, t.elapsed().as_millis()) }, async { diff --git a/src/openhuman/config/mod.rs b/src/openhuman/config/mod.rs index 2e3c538a7..295695244 100644 --- a/src/openhuman/config/mod.rs +++ b/src/openhuman/config/mod.rs @@ -40,8 +40,8 @@ pub use schema::{ StorageProviderSection, StreamMode, TeamModelConfig, TelegramConfig, UpdateConfig, UpdateRestartStrategy, VoiceActivationMode, VoiceServerConfig, WebSearchConfig, WebhookConfig, DEFAULT_CLOUD_LLM_MODEL, DEFAULT_MODEL, MODEL_AGENTIC_V1, MODEL_CHAT_V1, MODEL_CODING_V1, - MODEL_REASONING_QUICK_V1, MODEL_REASONING_V1, SEARCH_ENGINE_BRAVE, SEARCH_ENGINE_MANAGED, - SEARCH_ENGINE_PARALLEL, + MODEL_REASONING_QUICK_V1, MODEL_REASONING_V1, MODEL_SUMMARIZATION_V1, SEARCH_ENGINE_BRAVE, + SEARCH_ENGINE_MANAGED, SEARCH_ENGINE_PARALLEL, }; pub use schema::{ clear_active_user, default_projects_dir, default_root_openhuman_dir, pre_login_user_dir, diff --git a/src/openhuman/config/schema/load.rs b/src/openhuman/config/schema/load.rs index 531822204..4746c540a 100644 --- a/src/openhuman/config/schema/load.rs +++ b/src/openhuman/config/schema/load.rs @@ -1120,20 +1120,42 @@ impl Config { if config_path.exists() { #[cfg(unix)] { - use std::os::unix::fs::PermissionsExt; + use std::{fs::Permissions, os::unix::fs::PermissionsExt}; if let Ok(meta) = fs::metadata(&config_path).await { if meta.permissions().mode() & 0o004 != 0 { let warned = WARNED_WORLD_READABLE_CONFIGS .get_or_init(|| Mutex::new(HashSet::new())); - let mut warned_guard = warned.lock().unwrap_or_else(|e| e.into_inner()); - if warned_guard.insert(config_path.clone()) { + // Only attempt to fix paths not yet successfully chmod'd. + // Cache is advanced only on success so a persistent + // failure re-warns and re-attempts on every load. + let already_fixed = warned + .lock() + .unwrap_or_else(|e| e.into_inner()) + .contains(&config_path); + if !already_fixed { tracing::warn!( - "Config file {:?} is world-readable (mode {:o}). \ - Consider restricting with: chmod 600 {:?}", + "[config] Config file {:?} is world-readable (mode {:o}); \ + auto-fixing to 600", config_path, meta.permissions().mode() & 0o777, - config_path, ); + match fs::set_permissions(&config_path, Permissions::from_mode(0o600)) + .await + { + Ok(()) => { + warned + .lock() + .unwrap_or_else(|e| e.into_inner()) + .insert(config_path.clone()); + } + Err(e) => { + tracing::warn!( + path = %config_path.display(), + error = %e, + "[config] failed to auto-fix config file permissions to 600", + ); + } + } } } } diff --git a/src/openhuman/config/schema/types.rs b/src/openhuman/config/schema/types.rs index 1b11ad0c5..81999c11d 100644 --- a/src/openhuman/config/schema/types.rs +++ b/src/openhuman/config/schema/types.rs @@ -24,6 +24,7 @@ pub const MODEL_CHAT_V1: &str = "chat-v1"; /// reasoning is needed. pub const MODEL_REASONING_QUICK_V1: &str = "reasoning-quick-v1"; pub const MODEL_CODING_V1: &str = "coding-v1"; +pub const MODEL_SUMMARIZATION_V1: &str = "summarization-v1"; /// Default model used when no explicit model is configured. /// /// Set to `reasoning-quick-v1` (Kimi K2.6 Turbo on Fireworks — low-latency, diff --git a/src/openhuman/credentials/profiles.rs b/src/openhuman/credentials/profiles.rs index a94f50373..6a5751d48 100644 --- a/src/openhuman/credentials/profiles.rs +++ b/src/openhuman/credentials/profiles.rs @@ -1093,8 +1093,10 @@ impl AuthProfilesStore { let too_old = age.map_or(false, |a| a >= Duration::from_millis(STALE_LOCK_AGE_MS)); // A pidless lock needs only a short grace: no healthy holder leaves the // file without a `pid=` line for more than the microsecond gap between - // `create_new` and the write, so anything older is abandoned. - let malformed_too_old = age.map_or(false, |a| { + // `create_new` and the write, so anything older is abandoned. If mtime + // is unreadable (clock skew, platform limitation) default to stale — + // no legitimate in-flight writer would be undetectable for that long. + let malformed_too_old = age.map_or(true, |a| { a >= Duration::from_millis(MALFORMED_LOCK_GRACE_MS) }); diff --git a/src/openhuman/inference/provider/factory.rs b/src/openhuman/inference/provider/factory.rs index 12329e01d..96f24962c 100644 --- a/src/openhuman/inference/provider/factory.rs +++ b/src/openhuman/inference/provider/factory.rs @@ -79,7 +79,7 @@ pub fn auth_key_for_slug(slug: &str) -> String { pub(crate) fn is_known_openhuman_tier(model: &str) -> bool { use crate::openhuman::config::{ MODEL_AGENTIC_V1, MODEL_CHAT_V1, MODEL_CODING_V1, MODEL_REASONING_QUICK_V1, - MODEL_REASONING_V1, + MODEL_REASONING_V1, MODEL_SUMMARIZATION_V1, }; matches!( model, @@ -88,10 +88,12 @@ pub(crate) fn is_known_openhuman_tier(model: &str) -> bool { | MODEL_AGENTIC_V1 | MODEL_CODING_V1 | MODEL_REASONING_QUICK_V1 + | MODEL_SUMMARIZATION_V1 | "hint:reasoning" | "hint:chat" | "hint:agentic" | "hint:coding" + | "hint:summarization" ) } @@ -416,6 +418,7 @@ fn make_openhuman_backend(config: &Config) -> anyhow::Result<(Box, Some("chat") => crate::openhuman::config::MODEL_REASONING_QUICK_V1.to_string(), Some("agentic") => crate::openhuman::config::MODEL_AGENTIC_V1.to_string(), Some("coding") => crate::openhuman::config::MODEL_CODING_V1.to_string(), + Some("summarization") => crate::openhuman::config::MODEL_SUMMARIZATION_V1.to_string(), Some(_) => { // Unrecognised hint — forward verbatim; the backend decides validity. model @@ -427,7 +430,7 @@ fn make_openhuman_backend(config: &Config) -> anyhow::Result<(Box, log::warn!( "[providers][chat-factory] model '{}' is not a recognized OpenHuman \ backend tier (valid: reasoning-v1, chat-v1, agentic-v1, coding-v1, \ - reasoning-quick-v1); falling back to '{}'", + reasoning-quick-v1, summarization-v1); falling back to '{}'", model, crate::openhuman::config::MODEL_REASONING_V1, ); diff --git a/src/openhuman/socket/ws_loop.rs b/src/openhuman/socket/ws_loop.rs index 8e496bd41..c0e6a75c3 100644 --- a/src/openhuman/socket/ws_loop.rs +++ b/src/openhuman/socket/ws_loop.rs @@ -294,7 +294,10 @@ async fn run_connection( emit_state_change(shared); // 7. Main event loop - let timeout_duration = Duration::from_millis(ping_interval + ping_timeout_ms + 5000); + // Deadline = pingInterval + pingTimeout + 5 s grace so minor server-side + // jitter doesn't cause a spurious reconnect on a healthy connection. + let timeout_ms = ping_interval + ping_timeout_ms + 5_000; + let timeout_duration = Duration::from_millis(timeout_ms); let mut deadline = Instant::now() + timeout_duration; loop { @@ -336,8 +339,10 @@ async fn run_connection( } _ = tokio::time::sleep_until(deadline) => { log::warn!( - "[socket] Ping timeout ({}ms)", - ping_interval + ping_timeout_ms + 5000 + "[socket] No server ping received within {}ms (interval={}ms + timeout={}ms + 5s grace); reconnecting", + timeout_ms, + ping_interval, + ping_timeout_ms, ); return ConnectionOutcome::Lost("Ping timeout".into()); }