From c18ffe25822eb7de5cfcbad72e886dfe40034d2f Mon Sep 17 00:00:00 2001 From: npub137662rjc45tlwzl38m4f828yc3ymzugkudyp8w0jdpvxj7ysen3sm3uh7m <8fb5a50e58ad17f70bf13eea93a8e4c449b17116e34813b9f26858697890cce3@sprout-oss.stage.blox.sqprod.co> Date: Tue, 7 Jul 2026 20:33:18 -0700 Subject: [PATCH] fix(desktop): restart agents after setup fixes Co-authored-by: npub137662rjc45tlwzl38m4f828yc3ymzugkudyp8w0jdpvxj7ysen3sm3uh7m <8fb5a50e58ad17f70bf13eea93a8e4c449b17116e34813b9f26858697890cce3@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub137662rjc45tlwzl38m4f828yc3ymzugkudyp8w0jdpvxj7ysen3sm3uh7m <8fb5a50e58ad17f70bf13eea93a8e4c449b17116e34813b9f26858697890cce3@sprout-oss.stage.blox.sqprod.co> --- .../src-tauri/src/commands/agent_models.rs | 16 ++-- .../src/managed_agents/process_lifecycle.rs | 2 + .../src-tauri/src/managed_agents/runtime.rs | 84 ++++++++++++++++++- .../src/managed_agents/runtime/tests.rs | 42 +++++++++- desktop/src-tauri/src/managed_agents/types.rs | 7 ++ 5 files changed, 141 insertions(+), 10 deletions(-) diff --git a/desktop/src-tauri/src/commands/agent_models.rs b/desktop/src-tauri/src/commands/agent_models.rs index f54a1dc114..ffe7cadc80 100644 --- a/desktop/src-tauri/src/commands/agent_models.rs +++ b/desktop/src-tauri/src/commands/agent_models.rs @@ -12,10 +12,11 @@ use crate::{ managed_agents::{ build_managed_agent_summary, current_instance_id, default_agent_workdir, discovery_env_with_baked_floor, find_managed_agent_mut, known_acp_runtime, - load_managed_agents, load_personas, managed_agent_avatar_url, missing_command_message, - normalize_agent_args, resolve_command, save_managed_agents, sync_managed_agent_processes, - try_regenerate_nest, AgentModelInfo, AgentModelsResponse, UpdateManagedAgentRequest, - UpdateManagedAgentResponse, DEFAULT_ACP_COMMAND, + load_managed_agents, load_personas, managed_agent_avatar_url, + maybe_restart_setup_mode_agent, missing_command_message, normalize_agent_args, + resolve_command, save_managed_agents, sync_managed_agent_processes, try_regenerate_nest, + AgentModelInfo, AgentModelsResponse, UpdateManagedAgentRequest, UpdateManagedAgentResponse, + DEFAULT_ACP_COMMAND, }, relay::{relay_ws_url_with_override, sync_managed_agent_profile}, util::now_iso, @@ -786,9 +787,8 @@ async fn run_agent_models_command( /// Update mutable fields on an existing managed agent record. /// -/// Does NOT auto-restart the agent. Runtime config changes (system prompt, -/// parallelism, commands, toolsets) take effect on the next agent spawn. -/// Name changes are synced to the relay immediately via a kind:0 re-publish. +/// Does NOT auto-restart healthy agents; only setup-mode listeners are respawned. +/// Other runtime config changes take effect on next spawn; renames sync now. #[tauri::command] pub async fn update_managed_agent( input: UpdateManagedAgentRequest, @@ -910,6 +910,8 @@ pub async fn update_managed_agent( record.updated_at = now_iso(); + maybe_restart_setup_mode_agent(&app, &state, record, &mut runtimes); + save_managed_agents(&app, &records)?; let record = records diff --git a/desktop/src-tauri/src/managed_agents/process_lifecycle.rs b/desktop/src-tauri/src/managed_agents/process_lifecycle.rs index e253489291..b9d383a09d 100644 --- a/desktop/src-tauri/src/managed_agents/process_lifecycle.rs +++ b/desktop/src-tauri/src/managed_agents/process_lifecycle.rs @@ -134,6 +134,7 @@ pub fn finish_spawn( child: std::process::Child, log_path: std::path::PathBuf, spawn_config_hash: u64, + in_setup_mode: bool, agent_name: &str, ) -> super::ManagedAgentProcess { let job = create_job_for_child(child.id()); @@ -147,6 +148,7 @@ pub fn finish_spawn( child, log_path, spawn_config_hash, + in_setup_mode, job, } } diff --git a/desktop/src-tauri/src/managed_agents/runtime.rs b/desktop/src-tauri/src/managed_agents/runtime.rs index 0e12066961..e42cbc4fbf 100644 --- a/desktop/src-tauri/src/managed_agents/runtime.rs +++ b/desktop/src-tauri/src/managed_agents/runtime.rs @@ -5,6 +5,7 @@ use tauri::AppHandle; use super::agent_env::build_buzz_agent_provider_defaults; use crate::{ + app_state::AppState, managed_agents::{ append_log_marker, known_acp_runtime, login_shell_path, managed_agent_log_path, missing_command_message, normalize_agent_args, open_log_file, resolve_command, @@ -1620,7 +1621,7 @@ pub fn spawn_agent_child( // // The JSON format mirrors `setup_mode::SetupPayload` in buzz-acp: // { "agent_name": "...", "agent_pubkey": "...", "requirements": [{ "surface": "...", ... }] } - { + let in_setup_mode = { use crate::managed_agents::{ agent_readiness, resolve_effective_agent_env, AgentReadiness, Requirement, }; @@ -1682,6 +1683,7 @@ pub fn spawn_agent_child( command.env_remove("BUZZ_ACP_SETUP_PAYLOAD"); // Set the payload only when desktop computed NotReady. + let in_setup_mode = setup_payload_json.is_some(); if let Some(json) = setup_payload_json { command.env("BUZZ_ACP_SETUP_PAYLOAD", json); eprintln!( @@ -1689,7 +1691,8 @@ pub fn spawn_agent_child( record.name ); } - } + in_setup_mode + }; // Only emit BUZZ_ACP_IDLE_TIMEOUT when the user has explicitly set an // override. When unset, the buzz-acp harness applies its own default // (see `DEFAULT_IDLE_TIMEOUT_SECS` in crates/buzz-acp/src/config.rs), @@ -1890,6 +1893,7 @@ pub fn spawn_agent_child( child, log_path, spawn_config_hash, + in_setup_mode, &record.name, )); #[cfg(not(windows))] @@ -1897,9 +1901,85 @@ pub fn spawn_agent_child( child, log_path, spawn_config_hash, + in_setup_mode, }) } +/// Returns `true` when a process that was spawned in setup mode would pass +/// readiness with the record's current config. Setup-mode buzz-acp is a +/// nudge-only listener and does not re-read desktop config after spawn, so this +/// is the predicate for auto-restarting after an Edit Agent save. +pub fn setup_mode_now_ready( + record: &ManagedAgentRecord, + personas: &[crate::managed_agents::types::PersonaRecord], + was_spawned_in_setup_mode: bool, +) -> bool { + use crate::managed_agents::{agent_readiness, resolve_effective_agent_env, AgentReadiness}; + + if !was_spawned_in_setup_mode { + return false; + } + + let effective_command = crate::managed_agents::effective_agent_command( + record.persona_id.as_deref(), + personas, + record.agent_command_override.as_deref(), + ); + let runtime_meta = known_acp_runtime(&effective_command); + let effective = resolve_effective_agent_env(record, personas, runtime_meta); + matches!(agent_readiness(&effective), AgentReadiness::Ready) +} + +/// Stop and respawn a setup-mode process if the current record now passes +/// readiness. Called after Edit Agent saves mutate the record but before the +/// store is persisted. +pub fn maybe_restart_setup_mode_agent( + app: &AppHandle, + state: &AppState, + record: &mut ManagedAgentRecord, + runtimes: &mut HashMap, +) { + let was_setup_mode = runtimes + .get(&record.pubkey) + .is_some_and(|runtime| runtime.in_setup_mode); + let personas = super::load_personas(app).unwrap_or_default(); + if !setup_mode_now_ready(record, &personas, was_setup_mode) { + return; + } + + let restart = (|| -> Result<(), String> { + let owner_hex = { + let keys = state.keys.lock().map_err(|e| e.to_string())?; + keys.public_key().to_hex() + }; + stop_managed_agent_process(app, record, runtimes)?; + let process = spawn_agent_child(app, record, Some(&owner_hex))?; + let now = now_iso(); + record.updated_at = now.clone(); + record.runtime_pid = Some(process.child.id()); + record.last_started_at = Some(now); + record.last_stopped_at = None; + record.last_exit_code = None; + record.last_error = None; + runtimes.insert(record.pubkey.clone(), process); + Ok(()) + })(); + + match restart { + Ok(()) => eprintln!( + "buzz-desktop: auto-restarted agent {} out of setup mode", + record.name + ), + Err(error) => { + eprintln!( + "buzz-desktop: failed to auto-restart agent {} out of setup mode: {error}", + record.name + ); + record.last_error = Some(format!("auto-restart failed: {error}")); + } + } +} + fn child_rust_log_filter() -> String { match std::env::var("RUST_LOG") { Ok(existing) if existing.contains("buzz_acp") => existing, diff --git a/desktop/src-tauri/src/managed_agents/runtime/tests.rs b/desktop/src-tauri/src/managed_agents/runtime/tests.rs index f675ed0a10..9953547819 100644 --- a/desktop/src-tauri/src/managed_agents/runtime/tests.rs +++ b/desktop/src-tauri/src/managed_agents/runtime/tests.rs @@ -434,7 +434,47 @@ fn non_persona_agent_never_drifts() { assert!(!orphaned); } -use super::runtime_metadata_env_vars; +use super::{runtime_metadata_env_vars, setup_mode_now_ready}; + +#[test] +fn setup_mode_now_ready_requires_setup_mode_stamp() { + let mut record = fixture(RespondTo::Anyone, vec![], Some("tag".into())); + record.agent_command_override = Some("buzz-agent".into()); + record.provider = Some("databricks_v2".into()); + record.model = Some("goose-claude-4-6-opus".into()); + record.env_vars.insert( + "DATABRICKS_HOST".into(), + "https://databricks.example.com".into(), + ); + + assert!(setup_mode_now_ready(&record, &[], true)); + assert!( + !setup_mode_now_ready(&record, &[], false), + "healthy running agents must not be auto-restarted just because they are ready" + ); +} + +#[test] +fn setup_mode_now_ready_stays_false_until_missing_config_is_fixed() { + let mut record = fixture(RespondTo::Anyone, vec![], Some("tag".into())); + record.agent_command_override = Some("buzz-agent".into()); + record.provider = Some("databricks_v2".into()); + record.env_vars.insert( + "DATABRICKS_HOST".into(), + "https://databricks.example.com".into(), + ); + + assert!( + !setup_mode_now_ready(&record, &[], true), + "missing model keeps the setup listener in place" + ); + + record.model = Some("goose-claude-4-6-opus".into()); + assert!( + setup_mode_now_ready(&record, &[], true), + "filling the missing model makes the setup-mode process eligible for restart" + ); +} #[test] fn runtime_metadata_env_vars_injects_model_and_provider() { diff --git a/desktop/src-tauri/src/managed_agents/types.rs b/desktop/src-tauri/src/managed_agents/types.rs index 3b152c0261..95806bbbc4 100644 --- a/desktop/src-tauri/src/managed_agents/types.rs +++ b/desktop/src-tauri/src/managed_agents/types.rs @@ -247,6 +247,13 @@ pub struct ManagedAgentProcess { /// `runtime_pid` have no `ManagedAgentProcess` entry, so their spawn /// config is unknown and the badge stays off. pub spawn_config_hash: u64, + /// `true` when this process was spawned with `BUZZ_ACP_SETUP_PAYLOAD` set — + /// readiness failed at spawn and buzz-acp is running as a nudge-only setup + /// listener instead of the real agent pool. Runtime-only — never persisted. + /// `update_managed_agent` uses this stamp to restart the harness once an + /// edit satisfies readiness, because setup-mode processes do not re-read + /// desktop config after launch. + pub in_setup_mode: bool, /// Win32 Job Object owning the harness + its entire process tree. Closing /// the handle (via `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`) kills the whole /// tree — the Windows mirror of the Unix process-group teardown. `None`