From 1c55f8150737f78eee9446d819ec4a809b521b37 Mon Sep 17 00:00:00 2001 From: Lewis Wang Date: Thu, 23 Jul 2026 10:44:14 +1000 Subject: [PATCH] fix(desktop): dial managed agents on the configured relay URL, not the canonical runtime key Since #2122, spawn paths passed ManagedAgentRuntimeKey.relay_url (the canonical identity form) to spawned children as BUZZ_RELAY_URL. The canonicalizer folds every loopback spelling to 127.0.0.1, but the relay derives the community boundary from the Host header, so a community configured as ws://localhost:3000 had its UI post into the localhost:3000 community while every agent connected to the 127.0.0.1:3000 community. Agents discovered zero channels, logged 'no channel subscriptions resolved - agent will sit idle', and silently never answered mentions. Per the contract documented on buzz_core::relay::normalize_relay_url ('Connection code may retain the configured URL; this canonical form is for identity'), keep the canonical key for identity, receipts, and log paths, and dial the caller-supplied requested URL: - spawn_agent_child dials the relay_url parameter verbatim; the runtime key remains identity-only - start_managed_agent_process, start_pair, and launch restore pass their already-resolved requested URL instead of key.relay_url - reconcile_managed_agent_runtimes forwards the requested community URL to start_pair instead of the canonical form - probe_agent_relay_access probes over the requested URL so the HTTP probe hits the same community the child will join - start_pair now reports requested_relay_url in its status rows, and the Active Communities settings card prefers requestedRelayUrl for start/stop/restart actions so a manual restart re-dials the configured URL rather than the canonical echo No new automated test covers the spawn wiring itself (it forks a real child process); verified with cargo test (1560 passed), clippy, fmt, tsc, and biome. Fixes #2444 Co-Authored-By: Claude Fable 5 Signed-off-by: Lewis Wang --- .../src-tauri/src/managed_agents/restore.rs | 2 +- .../src-tauri/src/managed_agents/runtime.rs | 12 ++++++---- .../src/managed_agents/runtime_commands.rs | 22 ++++++++++++++----- .../ui/ActiveAgentCommunitiesSettingsCard.tsx | 2 +- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/desktop/src-tauri/src/managed_agents/restore.rs b/desktop/src-tauri/src/managed_agents/restore.rs index fab481e12b..5bdc417337 100644 --- a/desktop/src-tauri/src/managed_agents/restore.rs +++ b/desktop/src-tauri/src/managed_agents/restore.rs @@ -323,7 +323,7 @@ pub async fn restore_managed_agents_on_launch( spawn_agent_child( app, record, - &key.relay_url, + &relay_url, true, owner_hex_ref, ) diff --git a/desktop/src-tauri/src/managed_agents/runtime.rs b/desktop/src-tauri/src/managed_agents/runtime.rs index cef669ab25..93ed215314 100644 --- a/desktop/src-tauri/src/managed_agents/runtime.rs +++ b/desktop/src-tauri/src/managed_agents/runtime.rs @@ -1673,9 +1673,13 @@ pub fn spawn_agent_child( .map(|p| p.display().to_string()) .unwrap_or_else(|| effective_command.clone()); - // The caller supplies the explicit canonical pair relay. This is the only - // relay this child may connect to, regardless of the record/workspace default. - let effective_relay_url = runtime_key.relay_url.clone(); + // The caller supplies the explicit pair relay. This is the only relay this + // child may connect to, regardless of the record/workspace default. Dial it + // as configured, not via `runtime_key.relay_url`: the canonical form is + // identity-only, and its loopback folding (`localhost` -> `127.0.0.1`) + // changes the Host the relay derives the community boundary from, silently + // parking the child in a different (empty) community than the UI. + let effective_relay_url = relay_url.to_string(); // Augment PATH for DMG launches so child processes can find: // - bundled CLI via ~/.local/bin symlink @@ -2115,7 +2119,7 @@ pub fn start_managed_agent_process( // Scalar PIDs are migration-only and never establish pair liveness. record.runtime_pid = None; - let mut process = spawn_agent_child(app, record, &key.relay_url, false, owner_hex)?; + let mut process = spawn_agent_child(app, record, &relay_url, false, owner_hex)?; let now = now_iso(); let receipt = super::ManagedAgentRuntimeReceipt { key: key.clone(), diff --git a/desktop/src-tauri/src/managed_agents/runtime_commands.rs b/desktop/src-tauri/src/managed_agents/runtime_commands.rs index c0e55184b1..10514ab1ae 100644 --- a/desktop/src-tauri/src/managed_agents/runtime_commands.rs +++ b/desktop/src-tauri/src/managed_agents/runtime_commands.rs @@ -272,7 +272,13 @@ fn start_pair( .get_mut(&key) .is_some_and(|runtime| runtime.child.try_wait().ok().flatten().is_none()) { - let status = status_for(&app, record, &key, runtimes.get(&key), None); + let status = status_for( + &app, + record, + &key, + runtimes.get(&key), + Some(relay_url.clone()), + ); return Ok(status); } runtimes.remove(&key); @@ -283,7 +289,7 @@ fn start_pair( .lock() .ok() .map(|keys| keys.public_key().to_hex()); - let mut process = spawn_agent_child(&app, record, &key.relay_url, lazy, owner.as_deref())?; + let mut process = spawn_agent_child(&app, record, &relay_url, lazy, owner.as_deref())?; let now = crate::util::now_iso(); let receipt = ManagedAgentRuntimeReceipt { key: key.clone(), @@ -302,7 +308,13 @@ fn start_pair( record.last_stopped_at = None; record.last_error = None; runtimes.insert(key.clone(), ManagedAgentPairRuntime::starting(process)); - let status = status_for(&app, record, &key, runtimes.get(&key), None); + let status = status_for( + &app, + record, + &key, + runtimes.get(&key), + Some(relay_url.clone()), + ); drop(runtimes); save_managed_agents(&app, &records)?; emit_status(&app, &status); @@ -403,7 +415,7 @@ async fn probe_agent_relay_access( let key = ManagedAgentRuntimeKey::new(record.pubkey.clone(), &requested_relay_url)?; let keys = nostr::Keys::parse(record.private_key_nsec.trim()) .map_err(|error| format!("invalid managed-agent key: {error}"))?; - let api_base = crate::relay::relay_http_base_url(&key.relay_url); + let api_base = crate::relay::relay_http_base_url(&requested_relay_url); tokio::time::timeout( std::time::Duration::from_secs(10), crate::relay::query_relay_at_with_keys( @@ -504,7 +516,7 @@ pub async fn reconcile_managed_agent_runtimes( Ok((record, key, requested)) => { match start_pair( record.pubkey.clone(), - key.relay_url.clone(), + requested.clone(), true, Some(&record.updated_at), app.clone(), diff --git a/desktop/src/features/settings/ui/ActiveAgentCommunitiesSettingsCard.tsx b/desktop/src/features/settings/ui/ActiveAgentCommunitiesSettingsCard.tsx index 268bd863f2..3a8508d9ab 100644 --- a/desktop/src/features/settings/ui/ActiveAgentCommunitiesSettingsCard.tsx +++ b/desktop/src/features/settings/ui/ActiveAgentCommunitiesSettingsCard.tsx @@ -50,7 +50,7 @@ export function ActiveAgentCommunitiesSettingsCard() { ? "start" : "restart", pubkey: runtime.pubkey, - relayUrl: runtime.relayUrl, + relayUrl: runtime.requestedRelayUrl ?? runtime.relayUrl, }); } finally { setPendingRuntimeKey(null);