Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion desktop/scripts/check-file-sizes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,11 @@ const overrides = new Map([
// (if let Some(provider_update) = input.provider { record.provider = provider_update; }).
// +8: harness_override thread-through in update_managed_agent so a deliberate
// Custom pin routes to update_time_agent_command_override (comment + call).
["src-tauri/src/commands/agent_models.rs", 1079],
// +54: setup-mode auto-restart in update_managed_agent (stop + respawn a
// live setup-listener process when the edit satisfies readiness) + the
// expanded doc contract. Load-bearing fix for the "needs configuration"
// forever-loop; queued to split with the discovery modules above.
["src-tauri/src/commands/agent_models.rs", 1133],
// draft-persistence predicate: submit-time `loadDraft` check + inline comment
// + deps-array entry in submitMessage closes the never-persisted-boundary
// defect (Thufir Pass-3 finding). Load-bearing correctness fix; queued to
Expand Down
64 changes: 60 additions & 4 deletions desktop/src-tauri/src/commands/agent_models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ use crate::{
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,
normalize_agent_args, resolve_command, save_managed_agents, setup_mode_now_ready,
start_managed_agent_process, stop_managed_agent_process, sync_managed_agent_processes,
try_regenerate_nest, AgentModelInfo, AgentModelsResponse, UpdateManagedAgentRequest,
UpdateManagedAgentResponse, DEFAULT_ACP_COMMAND,
},
Expand Down Expand Up @@ -786,9 +787,17 @@ 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 a healthy 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.
///
/// ONE exception: a live process that was spawned in setup mode (readiness
/// failed at spawn, so buzz-acp is a nudge-only setup listener) is stopped
/// and respawned when this edit makes the record pass readiness — a
/// setup-mode harness never re-evaluates its config, so without the respawn
/// the agent would nudge "needs configuration" forever after the user fixed
/// it. See `setup_mode_now_ready`.
#[tauri::command]
pub async fn update_managed_agent(
input: UpdateManagedAgentRequest,
Expand Down Expand Up @@ -910,6 +919,53 @@ pub async fn update_managed_agent(

record.updated_at = now_iso();

// ── Auto-restart out of setup mode ──────────────────────────────────
//
// A harness spawned with BUZZ_ACP_SETUP_PAYLOAD runs as a nudge-only
// setup listener and NEVER re-evaluates its config — readiness is only
// computed at spawn (see `spawn_agent_child`). Without this, a user
// who fixes the missing model/provider in Edit Agent keeps getting
// "needs configuration" nudges forever because the stale setup-mode
// process is still the one connected to the relay.
//
// So: when this edit makes a live setup-mode process ready, stop it
// and respawn in place. Best-effort — a respawn failure must not fail
// the save itself (the edit already landed); it is surfaced via
// `last_error` on the record like other spawn failures.
let was_setup_mode = runtimes
.get(&record.pubkey)
.is_some_and(|rt| rt.in_setup_mode);
if was_setup_mode {
let personas = load_personas(&app).unwrap_or_default();
if setup_mode_now_ready(record, &personas, true) {
// Same owner-hex read as `commands::agents::workspace_owner_hex`;
// `state.keys` is acquired inside the store lock elsewhere too
// (see `retain_managed_agent_pending`), so ordering is safe.
let owner_hex = {
let keys = state.keys.lock().map_err(|e| e.to_string())?;
keys.public_key().to_hex()
};
let restart =
stop_managed_agent_process(&app, record, &mut runtimes).and_then(|()| {
start_managed_agent_process(&app, record, &mut runtimes, Some(&owner_hex))
Comment on lines +948 to +950

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Run the normal pre-spawn snapshot before auto-restarting

For persona-backed agents this new auto-restart path calls start_managed_agent_process directly, bypassing the persona re-snapshot that the normal start path performs in start_local_agent_with_preflight before every spawn. If the linked persona was edited while the setup listener was running, the restarted child is launched from the stale record snapshot, while the stored spawn hash is computed against the current persona, so the UI can show no restart-needed badge even though the process is not actually running the persona config that a normal Stop/Start would use.

Useful? React with 👍 / 👎.

});
match restart {
Ok(()) => eprintln!(
"buzz-desktop: agent {} config now satisfies readiness — \
auto-restarted out of setup mode",
record.name
),
Err(e) => {
eprintln!(
"buzz-desktop: auto-restart of agent {} out of setup mode failed: {e}",
record.name
);
record.last_error = Some(format!("auto-restart failed: {e}"));
}
}
}
}

save_managed_agents(&app, &records)?;

let record = records
Expand Down
2 changes: 2 additions & 0 deletions desktop/src-tauri/src/managed_agents/process_lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -147,6 +148,7 @@ pub fn finish_spawn(
child,
log_path,
spawn_config_hash,
in_setup_mode,
job,
}
}
41 changes: 39 additions & 2 deletions desktop/src-tauri/src/managed_agents/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1620,7 +1620,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,
};
Expand Down Expand Up @@ -1682,14 +1682,16 @@ 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!(
"buzz-desktop: agent {} not ready — spawning in setup-listener mode",
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),
Expand Down Expand Up @@ -1890,16 +1892,51 @@ pub fn spawn_agent_child(
child,
log_path,
spawn_config_hash,
in_setup_mode,
&record.name,
));
#[cfg(not(windows))]
Ok(crate::managed_agents::ManagedAgentProcess {
child,
log_path,
spawn_config_hash,
in_setup_mode,
})
}

/// Returns `true` when a process spawned in setup mode would now pass the
/// readiness check under the record's current configuration — i.e. an edit
/// has satisfied the missing requirements and only a respawn will get the
/// agent out of the nudge-only setup listener (a setup-mode harness never
/// re-evaluates its config; readiness is only computed at spawn, see
/// `spawn_agent_child`).
///
/// `was_spawned_in_setup_mode` is the flag stamped on
/// [`ManagedAgentProcess::in_setup_mode`] at spawn time. Passing the flag
/// (rather than the process) keeps this unit-testable without a live child.
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;
}
// Mirror the spawn-time readiness computation exactly: resolve the
// effective harness (persona wins unless explicitly pinned), then build
// the effective env the child would receive.
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)
}

fn child_rust_log_filter() -> String {
match std::env::var("RUST_LOG") {
Ok(existing) if existing.contains("buzz_acp") => existing,
Expand Down
57 changes: 57 additions & 0 deletions desktop/src-tauri/src/managed_agents/runtime/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -608,3 +608,60 @@ fn grandchild_inherits_pgid_of_process_group_leader() {
unsafe { libc::kill(-harness_pid, libc::SIGTERM) };
let _ = harness.wait();
}

// ── setup_mode_now_ready tests ───────────────────────────────────────
//
// The auto-restart gate in `update_managed_agent`: a live process spawned in
// setup mode should be respawned exactly when the edited record now passes
// readiness. These drive the pure seam (`setup_mode_now_ready`) with the
// spawn-time flag injected, so no live child process is needed.

use super::setup_mode_now_ready;

/// Record that passes buzz-agent readiness: provider + model + matching
/// credential. No persona and no override, so the effective command resolves
/// to the `buzz-agent` default.
fn ready_record() -> ManagedAgentRecord {
let mut record = fixture(RespondTo::Anyone, vec![], Some("tag".into()));
record.model = Some("claude-opus-4-5".into());
record.provider = Some("anthropic".into());
record
.env_vars
.insert("ANTHROPIC_API_KEY".into(), "sk-test".into());
record
}

#[test]
fn setup_mode_now_ready_true_when_config_fixed() {
// The Fizz bug: spawned in setup mode (model missing), then the user set
// model/provider in Edit Agent → the edit must trigger a respawn.
let record = ready_record();
assert!(setup_mode_now_ready(&record, &[], true));
}

#[test]
fn setup_mode_now_ready_false_when_still_not_ready() {
// Model still missing → stay in setup mode; a respawn would just land
// back in the setup listener.
let mut record = ready_record();
record.model = None;
assert!(!setup_mode_now_ready(&record, &[], true));
}

#[test]
fn setup_mode_now_ready_false_when_credential_still_missing() {
// Provider/model present but the provider credential is absent — the
// same NotReady the spawn path would compute.
let mut record = ready_record();
record.env_vars.remove("ANTHROPIC_API_KEY");
assert!(!setup_mode_now_ready(&record, &[], true));
}

#[test]
fn setup_mode_now_ready_false_for_normal_process() {
// A process NOT spawned in setup mode must never be auto-restarted by an
// edit — edits to healthy agents keep the existing "takes effect on next
// spawn" contract (the needs_restart badge covers drift).
let record = ready_record();
assert!(!setup_mode_now_ready(&record, &[], false));
}
7 changes: 7 additions & 0 deletions desktop/src-tauri/src/managed_agents/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 —
/// i.e. readiness failed at spawn and buzz-acp is running as a nudge-only
/// setup listener instead of the agent pool. Runtime-only — never
/// persisted. `update_managed_agent` uses this to auto-restart the process
/// once an edit satisfies readiness, since a setup-mode harness never
/// re-evaluates its config on its own.
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`
Expand Down
Loading