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>
This commit is contained in:
npub137662rjc45tlwzl38m4f828yc3ymzugkudyp8w0jdpvxj7ysen3sm3uh7m
2026-07-07 20:33:18 -07:00
parent d1f3194e73
commit c18ffe2582
5 changed files with 141 additions and 10 deletions
@@ -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
@@ -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,
}
}
@@ -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<String, ManagedAgentProcess>,
) {
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,
@@ -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() {
@@ -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`