mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix: runtime snapshot timeouts, config perms, stale lock recovery, summarization-v1 tier, loopback OAuth UX (#2690)
This commit is contained in:
@@ -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'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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': 'وصف البناء',
|
||||
|
||||
@@ -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': 'বিল্ডিং বিবরণ',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': 'बिल्डिंग विवरण',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': '작성 설명',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': 'Сборка профиля',
|
||||
|
||||
@@ -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': '正在分析你的历史记录...',
|
||||
|
||||
@@ -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...',
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<Mutex<()>> = Lazy::new(|| Mutex::new(()));
|
||||
static CURRENT_USER_CACHE: Lazy<Mutex<Option<CachedCurrentUser>>> = Lazy::new(|| Mutex::new(None));
|
||||
static RUNTIME_SNAPSHOT_CACHE: Lazy<Mutex<Option<CachedRuntimeSnapshot>>> =
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
|
||||
|
||||
@@ -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<dyn Provider>,
|
||||
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<dyn Provider>,
|
||||
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,
|
||||
);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user