diff --git a/desktop/src-tauri/src/intelligence/health_snapshot.rs b/desktop/src-tauri/src/intelligence/health_snapshot.rs index 0b309fd..b1588e1 100644 --- a/desktop/src-tauri/src/intelligence/health_snapshot.rs +++ b/desktop/src-tauri/src/intelligence/health_snapshot.rs @@ -47,9 +47,30 @@ pub async fn health_snapshot( ) -> Result { let engines = heartbeat_state.lock().await; - let engine = engines - .get(&agent_id) - .ok_or_else(|| format!("Heartbeat engine not initialized for agent: {}", agent_id))?; + // If heartbeat engine not yet initialized, return a graceful "pending" snapshot + // instead of erroring — this avoids race conditions when HealthPanel mounts + // before the heartbeat bootstrap sequence completes. + let engine = match engines.get(&agent_id) { + Some(e) => e, + None => { + tracing::debug!("[health_snapshot] Engine not initialized for {}, returning pending snapshot", agent_id); + return Ok(HealthSnapshot { + timestamp: chrono::Utc::now().to_rfc3339(), + intelligence: IntelligenceHealth { + engine_running: false, + config: HeartbeatConfig::default(), + last_tick: None, + alert_count_24h: 0, + total_checks: 5, + }, + memory: MemoryHealth { + total_entries: 0, + storage_size_bytes: 0, + last_extraction: None, + }, + }); + } + }; let engine_running = engine.is_running().await; let config = engine.get_config().await; diff --git a/desktop/src/store/configStore.ts b/desktop/src/store/configStore.ts index 4151bab..2ca5692 100644 --- a/desktop/src/store/configStore.ts +++ b/desktop/src/store/configStore.ts @@ -189,7 +189,7 @@ export interface ConfigActionsSlice { description?: string; enabled?: boolean; }) => Promise; - loadSkillsCatalog: () => Promise; + loadSkillsCatalog: (retryCount?: number) => Promise; getSkill: (id: string) => Promise; createSkill: (skill: { name: string; @@ -449,7 +449,7 @@ export const useConfigStore = create((set // === Skill Actions === - loadSkillsCatalog: async () => { + loadSkillsCatalog: async (retryCount = 0) => { const client = get().client; // Path A: via injected client (KernelClient or GatewayClient) @@ -494,10 +494,19 @@ export const useConfigStore = create((set source: ((s.source as string) || 'builtin') as 'builtin' | 'extra', path: s.path as string | undefined, })) }); + return; } } catch (err) { console.warn('[configStore] skill_list direct invoke also failed:', err); } + + // Path C: delayed retry — kernel may still be initializing + if (retryCount < 2) { + const delay = (retryCount + 1) * 1500; // 1.5s, 3s + console.log(`[configStore] Skills empty, retrying in ${delay}ms (attempt ${retryCount + 1}/2)`); + await new Promise((r) => setTimeout(r, delay)); + return get().loadSkillsCatalog(retryCount + 1); + } }, getSkill: async (id: string) => {