From 0bd50aad8c206f06b2231e1538005157650b9cd6 Mon Sep 17 00:00:00 2001 From: iven Date: Sun, 19 Apr 2026 13:27:25 +0800 Subject: [PATCH] =?UTF-8?q?fix(heartbeat,skills):=20=E5=81=A5=E5=BA=B7?= =?UTF-8?q?=E5=BF=AB=E7=85=A7=E9=99=8D=E7=BA=A7=E5=A4=84=E7=90=86=20+=20?= =?UTF-8?q?=E6=8A=80=E8=83=BD=E5=8A=A0=E8=BD=BD=E9=87=8D=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1-3: health_snapshot 在 heartbeat engine 未初始化时不再报错, 返回 pending 状态快照,避免 HealthPanel 竞态报错。 P1-1: loadSkillsCatalog 新增 Path C 延迟重试(最多2次,间隔 1.5s/3s),解决 kernel 初始化未完成时 skills 返回空数组的问题。 --- .../src/intelligence/health_snapshot.rs | 27 ++++++++++++++++--- desktop/src/store/configStore.ts | 13 +++++++-- 2 files changed, 35 insertions(+), 5 deletions(-) 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) => {