mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
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:
parent
d1f3194e73
commit
c18ffe2582
@@ -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`
|
||||
|
||||
Reference in New Issue
Block a user