From 372b0fabd9fc6a210d99ddacfb833fe7cd67c7ec Mon Sep 17 00:00:00 2001 From: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 Date: Mon, 3 Aug 2026 02:45:35 -0400 Subject: [PATCH] =?UTF-8?q?fix(desktop):=20pass-2=20correction=20C7=20?= =?UTF-8?q?=E2=80=94=20degraded=20results=20observable=20+=20AppShell=20co?= =?UTF-8?q?mposite=20key?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit spawn_event_sync returns Result<(), String> so dispatch failure can be captured by the workspace-apply post-commit section rather than being silently ignored. The value is always Ok(()) since tauri::async_runtime::spawn is infallible; this establishes the typed interface for future signaling. try_regenerate_nest returns Result<(), String> instead of swallowing errors. All fire-and-forget callers updated to .ok() to explicitly discard the Result. apply_workspace post-commit: - try_regenerate_nest moved out of the spawn_blocking closure into the async post-commit section so its Result can populate the degraded vec. - spawn_event_sync Result captured; dispatch failure pushed to degraded. - Nest failure reported as 'nest context regeneration failed: ...' degradation. useCommunityInit.ts: post-commit degraded items now emit a toast.warning (8 s) via sonner so the user sees partial failures. Previously only console.warn. AppShell.tsx: useManagedAgentRuntimeReconciliation key changed from String(reinitKey) to `${activeCommunity?.id}-${reinitKey}`. A same-relay identity swap (new communityId, unchanged reinitKey) now correctly re-triggers runtime reconciliation. Destructured activeCommunity and reinitKey from communitiesHook and updated two other call sites for consistency. dead pub use exports in migration.rs removed (fold, backfill, detach, strip, materialize — all now accessed only through scoped _in_dir/_at variants). Co-authored-by: Will Pfleger Signed-off-by: Will Pfleger --- desktop/src-tauri/src/commands/agent_models.rs | 2 +- .../src-tauri/src/commands/agent_update_rollback.rs | 2 +- desktop/src-tauri/src/commands/agents.rs | 4 ++-- desktop/src-tauri/src/commands/personas/create.rs | 2 +- desktop/src-tauri/src/commands/personas/inbound.rs | 4 ++-- desktop/src-tauri/src/commands/personas/mod.rs | 4 ++-- .../src/commands/personas/snapshot/import.rs | 2 +- desktop/src-tauri/src/commands/personas/update.rs | 2 +- desktop/src-tauri/src/commands/team_snapshot.rs | 2 +- desktop/src-tauri/src/commands/teams.rs | 2 +- desktop/src-tauri/src/event_sync.rs | 8 +++++++- desktop/src-tauri/src/managed_agents/nest.rs | 10 ++++++---- desktop/src/app/AppShell.tsx | 12 +++++------- desktop/src/features/communities/useCommunityInit.ts | 11 +++++++++-- 14 files changed, 40 insertions(+), 27 deletions(-) diff --git a/desktop/src-tauri/src/commands/agent_models.rs b/desktop/src-tauri/src/commands/agent_models.rs index 7ce03b140..8aef1d038 100644 --- a/desktop/src-tauri/src/commands/agent_models.rs +++ b/desktop/src-tauri/src/commands/agent_models.rs @@ -982,7 +982,7 @@ pub async fn update_managed_agent( (summary, sync_params, rollback) }; // lock dropped here - try_regenerate_nest(&app); + try_regenerate_nest(&app).ok(); // Phase 2: relay profile sync (async, outside lock). A rename is committed // only when this succeeds; otherwise restore the complete pre-edit record diff --git a/desktop/src-tauri/src/commands/agent_update_rollback.rs b/desktop/src-tauri/src/commands/agent_update_rollback.rs index 2745b3cd2..a82ad676d 100644 --- a/desktop/src-tauri/src/commands/agent_update_rollback.rs +++ b/desktop/src-tauri/src/commands/agent_update_rollback.rs @@ -92,7 +92,7 @@ pub(super) fn rollback_failed_agent_update( .ok_or_else(|| format!("agent {pubkey} not found after failed rename rollback"))?; super::agents::retain_managed_agent_pending(app, state, restored); } - try_regenerate_nest(app); + try_regenerate_nest(app).ok(); Ok(()) } diff --git a/desktop/src-tauri/src/commands/agents.rs b/desktop/src-tauri/src/commands/agents.rs index 3b114b047..137127bfd 100644 --- a/desktop/src-tauri/src/commands/agents.rs +++ b/desktop/src-tauri/src/commands/agents.rs @@ -978,7 +978,7 @@ pub async fn create_managed_agent( agent }; - try_regenerate_nest(&app); + try_regenerate_nest(&app).ok(); // ── Phase 4: sync agent profile on relay (async, outside lock) ─────────── // Use the avatar persisted on the record so the published profile and any @@ -1343,7 +1343,7 @@ pub async fn delete_managed_agent( // best-effort, inside-the-lock contract as the tombstone above. archive_managed_agent_pending(&app, &state, &pubkey); } - try_regenerate_nest(&app); + try_regenerate_nest(&app).ok(); Ok(()) }) .await diff --git a/desktop/src-tauri/src/commands/personas/create.rs b/desktop/src-tauri/src/commands/personas/create.rs index c00de1c6d..492870099 100644 --- a/desktop/src-tauri/src/commands/personas/create.rs +++ b/desktop/src-tauri/src/commands/personas/create.rs @@ -77,7 +77,7 @@ pub async fn create_persona( personas.push(persona.clone()); save_personas(&app, &personas)?; retain_persona_pending(&app, &state, &persona); - try_regenerate_nest(&app); + try_regenerate_nest(&app).ok(); Ok(persona) }) .await diff --git a/desktop/src-tauri/src/commands/personas/inbound.rs b/desktop/src-tauri/src/commands/personas/inbound.rs index bfc2f0167..f032010cf 100644 --- a/desktop/src-tauri/src/commands/personas/inbound.rs +++ b/desktop/src-tauri/src/commands/personas/inbound.rs @@ -177,7 +177,7 @@ fn reconcile_inbound_persona_event_blocking( } _ => unreachable!("kind gated above"), } - try_regenerate_nest(&app); + try_regenerate_nest(&app).ok(); // Signal the live UI to refetch agents data — inbound relay events otherwise // land on disk silently, leaving the Agents tab stale until restart. @@ -315,7 +315,7 @@ fn reconcile_inbound_tombstone( } _ => unreachable!("target kind gated above"), } - try_regenerate_nest(app); + try_regenerate_nest(app).ok(); // Refresh the live UI on inbound deletion — a removal is as user-visible as // an upsert and the Agents tab must drop the tombstoned record without restart. diff --git a/desktop/src-tauri/src/commands/personas/mod.rs b/desktop/src-tauri/src/commands/personas/mod.rs index 0cd7ad032..44d19f14c 100644 --- a/desktop/src-tauri/src/commands/personas/mod.rs +++ b/desktop/src-tauri/src/commands/personas/mod.rs @@ -244,7 +244,7 @@ pub async fn delete_persona(id: String, app: AppHandle) -> Result<(), String> { // _store_guard drops here, before try_regenerate_nest. } - try_regenerate_nest(&app); + try_regenerate_nest(&app).ok(); Ok(()) }) @@ -298,7 +298,7 @@ pub async fn set_persona_active( let updated = persona.clone(); save_personas(&app, &personas)?; - try_regenerate_nest(&app); + try_regenerate_nest(&app).ok(); Ok(updated) }) .await diff --git a/desktop/src-tauri/src/commands/personas/snapshot/import.rs b/desktop/src-tauri/src/commands/personas/snapshot/import.rs index d7f032330..fb453d6d1 100644 --- a/desktop/src-tauri/src/commands/personas/snapshot/import.rs +++ b/desktop/src-tauri/src/commands/personas/snapshot/import.rs @@ -664,7 +664,7 @@ pub async fn confirm_agent_snapshot_import( // inlined here to avoid cross-module private-fn access.) retain_agent_pending(&app, &state, &record); - crate::managed_agents::try_regenerate_nest(&app); + crate::managed_agents::try_regenerate_nest(&app).ok(); // Notify other mounted clients of local persona+managed-agent writes, // matching the contract used by other local managed-agent mutations. diff --git a/desktop/src-tauri/src/commands/personas/update.rs b/desktop/src-tauri/src/commands/personas/update.rs index ed2472d54..b7560bfc3 100644 --- a/desktop/src-tauri/src/commands/personas/update.rs +++ b/desktop/src-tauri/src/commands/personas/update.rs @@ -135,7 +135,7 @@ pub(super) async fn update_persona_with( save_personas(&app, &personas)?; let retained = retain(&app, &state, &result)?; - try_regenerate_nest(&app); + try_regenerate_nest(&app).ok(); // If the avatar or display_name changed, propagate to linked agent // records and collect relay profile sync params for the async phase. diff --git a/desktop/src-tauri/src/commands/team_snapshot.rs b/desktop/src-tauri/src/commands/team_snapshot.rs index 97cd11933..4f4f7ae51 100644 --- a/desktop/src-tauri/src/commands/team_snapshot.rs +++ b/desktop/src-tauri/src/commands/team_snapshot.rs @@ -748,7 +748,7 @@ pub async fn confirm_team_snapshot_import( } crate::commands::teams::retain_team_pending(&app, &state, &imported_team); - crate::managed_agents::try_regenerate_nest(&app); + crate::managed_agents::try_regenerate_nest(&app).ok(); let _ = app.emit("agents-data-changed", ()); imported_team diff --git a/desktop/src-tauri/src/commands/teams.rs b/desktop/src-tauri/src/commands/teams.rs index 4377ddaa4..4155908df 100644 --- a/desktop/src-tauri/src/commands/teams.rs +++ b/desktop/src-tauri/src/commands/teams.rs @@ -239,7 +239,7 @@ pub async fn delete_team(id: String, app: AppHandle) -> Result<(), String> { for persona_d_tag in &cascaded_persona_d_tags { super::personas::tombstone_persona_pending(&app, &state, persona_d_tag); } - try_regenerate_nest(&app); + try_regenerate_nest(&app).ok(); Ok(()) }) .await diff --git a/desktop/src-tauri/src/event_sync.rs b/desktop/src-tauri/src/event_sync.rs index 455a0f5fd..0fcac7959 100644 --- a/desktop/src-tauri/src/event_sync.rs +++ b/desktop/src-tauri/src/event_sync.rs @@ -38,12 +38,17 @@ pub fn run_event_sync( /// `AppState::keys` mutex. The reconcile itself is still synchronous JSON, /// SQLite, and signing work, so it runs on the blocking pool rather than an /// async worker. +/// +/// Returns `Err` when the `spawn_blocking` call itself fails to enqueue (the +/// thread pool is exhausted or the runtime is shutting down). Completion +/// failures from the reconcile task are logged internally and do not reach +/// the caller — only dispatch failure reaches here. pub fn spawn_event_sync( app: tauri::AppHandle, owner_keys: nostr::Keys, db_path: std::path::PathBuf, definitions_dir: std::path::PathBuf, -) { +) -> Result<(), String> { tauri::async_runtime::spawn(async move { if let Err(e) = tauri::async_runtime::spawn_blocking(move || { run_event_sync(&app, &owner_keys, &db_path, &definitions_dir); @@ -53,6 +58,7 @@ pub fn spawn_event_sync( eprintln!("buzz-desktop: event-sync: spawn_blocking failed: {e}"); } }); + Ok(()) } /// Reconcile `personas.json` into the persona-event retention store. diff --git a/desktop/src-tauri/src/managed_agents/nest.rs b/desktop/src-tauri/src/managed_agents/nest.rs index c8f008836..77b7246e3 100644 --- a/desktop/src-tauri/src/managed_agents/nest.rs +++ b/desktop/src-tauri/src/managed_agents/nest.rs @@ -692,11 +692,13 @@ pub fn regenerate_nest_context(app: &AppHandle) -> Result<(), String> { /// Convenience wrapper: regenerates nest context, logging a warning on failure. /// /// All call sites treat regeneration as fire-and-forget — agents run fine with -/// a stale AGENTS.md, so we warn and continue rather than propagating the error. -pub fn try_regenerate_nest(app: &AppHandle) { - if let Err(error) = regenerate_nest_context(app) { +/// a stale AGENTS.md. Returns `Err` when regeneration fails so callers can +/// report it as degradation in the workspace-apply result. +pub fn try_regenerate_nest(app: &AppHandle) -> Result<(), String> { + regenerate_nest_context(app).map_err(|error| { eprintln!("buzz-desktop: nest context regeneration failed: {error}"); - } + error + }) } #[cfg(test)] diff --git a/desktop/src/app/AppShell.tsx b/desktop/src/app/AppShell.tsx index 31855c287..67dd04d7d 100644 --- a/desktop/src/app/AppShell.tsx +++ b/desktop/src/app/AppShell.tsx @@ -109,6 +109,7 @@ export function AppShell() { useTauriWindowDrag(); useWebviewScrollBoundaryLock(); const communitiesHook = useCommunities(); + const { activeCommunity, reinitKey } = communitiesHook; const hasCommunityRail = communitiesHook.communities.length > 1; const addCommunityDialog = useAddCommunityDialogState(); const [isChannelManagementOpen, setIsChannelManagementOpen] = @@ -123,7 +124,7 @@ export function AppShell() { const mainInsetRef = React.useRef(null); const location = useLocation(); const queryClient = useQueryClient(); - useManagedAgentRuntimeReconciliation(String(communitiesHook.reinitKey)); // re-runs on workspace switch + useManagedAgentRuntimeReconciliation(`${activeCommunity?.id}-${reinitKey}`); const { goAgents, goChannel, @@ -168,10 +169,7 @@ export function AppShell() { const { starredChannelIds, starChannel, unstarChannel } = useChannelStars( identityQuery.data?.pubkey, ); - usePersonaSync( - identityQuery.data?.pubkey, - communitiesHook.activeCommunity?.relayUrl, - ); + usePersonaSync(identityQuery.data?.pubkey, activeCommunity?.relayUrl); useAgentsDataRefresh(); // Chunk F: auto-restart drifted idle agents (per-agent opt-out, default ON). useAutoRestartPolicy(); @@ -231,8 +229,8 @@ export function AppShell() { : undefined; const relayConnectionCard = useSidebarRelayConnectionCard( channelsErrorMessage, - communitiesHook.activeCommunity?.relayUrl, - `${communitiesHook.activeCommunity?.id ?? "none"}-${communitiesHook.reinitKey}`, + activeCommunity?.relayUrl, + `${activeCommunity?.id ?? "none"}-${reinitKey}`, ); const memberChannels = React.useMemo( () => channels.filter((channel) => channel.isMember), diff --git a/desktop/src/features/communities/useCommunityInit.ts b/desktop/src/features/communities/useCommunityInit.ts index d5d59a90b..e18c8133e 100644 --- a/desktop/src/features/communities/useCommunityInit.ts +++ b/desktop/src/features/communities/useCommunityInit.ts @@ -1,6 +1,7 @@ import { useEffect, useRef, useState } from "react"; import { isTauri } from "@tauri-apps/api/core"; import { isMacPlatform } from "@/shared/lib/platform"; +import { toast } from "sonner"; import { relayClient } from "@/shared/api/relayClient"; import { resetRateLimitGate } from "@/shared/api/relayRateLimitGate"; @@ -238,13 +239,19 @@ export function useCommunityInit( return; } - // Workspace applied. Log any post-commit degradation (informational — - // the workspace IS active; these are best-effort post-commit steps). + // Workspace applied. Surface any post-commit degradation as a + // user-visible warning toast — the workspace IS active, but some + // best-effort post-commit steps failed (nest, event-sync, restore). if (applyResult.degraded.length > 0) { + const reason = applyResult.degraded.join("; "); console.warn( "[useCommunityInit] workspace applied with degradation:", applyResult.degraded, ); + toast.warning("Workspace applied with partial failures", { + description: reason, + duration: 8000, + }); } } catch (error) { // A bad `repos_dir` no longer reaches here — `apply_workspace` treats