From e0853e866a8b8bbeb91a5c6f89cfe06e8a3554f8 Mon Sep 17 00:00:00 2001 From: Brad Hallett <53977268+bradhallett@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:41:10 -0400 Subject: [PATCH 1/2] fix(acp): use preset launch args for managed agents with no runtime Managed agents pinned to a known ACP runtime via agent_command_override (e.g. `omp`, `grok`, `opencode`, `kimi`) but with no `runtime` id resolved the effective command (e.g. `omp`) yet launched it with empty args. With no controlling TTY under headless Buzz, the agent's interactive TUI started instead of its ACP/stdio mode, so the JSON-RPC `initialize` handshake never completed and the agent timed out at startup (omp/opencode/kimi: Request timeout; grok: ENXIO "Device not configured"). Root cause: resolve_effective_harness_descriptor only filled args when the record carried explicit instance args or a resolvable harness preset (record.runtime -> lookup_loaded_harness_by_id). A command-override agent with runtime=None hit neither, so descriptor.args stayed empty. Fix: when no preset resolves, fall back to the PRESET_HARNESSES entry whose command matches the effective command (new preset_args_for_command, mirroring the existing single-source preset_harness_ids helper and living alongside it in the managed_agents::discovery::presets submodule). The PresetHarness table stays the single source of truth for per-runtime launch args, so this covers omp, grok, opencode, kimi, cursor, openclaw, and devin at once with no hand-mirroring, and descriptor.args now flows correctly to the spawn env (BUZZ_ACP_AGENT_ARGS), the config hash, and the agent summary. No regressions: explicit instance args still win; goose still resolves via default_agent_args; amp/hermes presets (empty args) are unchanged; custom harness ids collide with reserved preset ids so cannot shadow them. Resolves #3399 Resolves #3457 Resolves #3729 Signed-off-by: Brad Hallett <53977268+bradhallett@users.noreply.github.com> --- .../src-tauri/src/managed_agents/discovery.rs | 2 +- .../src/managed_agents/discovery/presets.rs | 21 +++- .../src-tauri/src/managed_agents/readiness.rs | 107 ++++++++++++++++++ 3 files changed, 128 insertions(+), 2 deletions(-) diff --git a/desktop/src-tauri/src/managed_agents/discovery.rs b/desktop/src-tauri/src/managed_agents/discovery.rs index 8d1b8a5013..908f8e3f28 100644 --- a/desktop/src-tauri/src/managed_agents/discovery.rs +++ b/desktop/src-tauri/src/managed_agents/discovery.rs @@ -13,8 +13,8 @@ use crate::managed_agents::{ mod presets; mod runtime_metadata; +pub(crate) use presets::{preset_args_for_command, preset_harness_definitions, preset_harness_ids}; use presets::{preset_catalog_entry, PRESET_HARNESSES}; -pub(crate) use presets::{preset_harness_definitions, preset_harness_ids}; pub(crate) use runtime_metadata::KnownAcpRuntime; const GOOSE_AVATAR_URL: &str = "https://goose-docs.ai/img/logo_dark.png"; diff --git a/desktop/src-tauri/src/managed_agents/discovery/presets.rs b/desktop/src-tauri/src/managed_agents/discovery/presets.rs index 72c4657dc7..fc43cb9fc0 100644 --- a/desktop/src-tauri/src/managed_agents/discovery/presets.rs +++ b/desktop/src-tauri/src/managed_agents/discovery/presets.rs @@ -5,7 +5,7 @@ use crate::managed_agents::{ AcpAvailabilityStatus, AcpRuntimeCatalogEntry, AuthStatus, HarnessSource, }; -use super::normalize_agent_args; +use super::{normalize_agent_args, normalize_command_identity}; /// Static data for a well-known tier-2 ACP harness. pub(super) struct PresetHarness { @@ -199,6 +199,25 @@ pub(crate) fn preset_harness_ids() -> &'static [&'static str] { .as_slice() } +/// Launch args for the [`PRESET_HARNESSES`] entry whose command matches the +/// given command, when an agent has a command but no resolvable runtime preset +/// (e.g. an explicit `agent_command_override` like `omp` with no `runtime` id). +/// +/// Single source of truth: per-runtime launch args live only in +/// `PRESET_HARNESSES`, mirroring [`preset_harness_ids`], so every preset — omp, +/// grok, opencode, kimi, cursor, openclaw, … — is covered at the source instead +/// of being hand-mirrored. Without this, an agent pinned to e.g. `omp` with no +/// runtime id launches the bare command, which under headless Buzz (no +/// controlling TTY) drops into the interactive TUI instead of `omp acp` and the +/// ACP `initialize` handshake times out. +pub(crate) fn preset_args_for_command(command: &str) -> Option> { + let identity = normalize_command_identity(command); + PRESET_HARNESSES + .iter() + .find(|preset| normalize_command_identity(preset.command) == identity) + .map(|preset| preset.args.iter().map(|arg| arg.to_string()).collect()) +} + #[cfg(test)] mod tests { use std::path::PathBuf; diff --git a/desktop/src-tauri/src/managed_agents/readiness.rs b/desktop/src-tauri/src/managed_agents/readiness.rs index fa8eb36fa1..702faae560 100644 --- a/desktop/src-tauri/src/managed_agents/readiness.rs +++ b/desktop/src-tauri/src/managed_agents/readiness.rs @@ -156,6 +156,15 @@ pub(crate) fn resolve_effective_harness_descriptor( normalize_agent_args(&effective_command, record_args) } else if let Some(ref def) = harness_def { normalize_agent_args(&effective_command, def.args.clone()) + } else if let Some(preset_args) = + crate::managed_agents::discovery::preset_args_for_command(&effective_command) + { + // No runtime id resolved a harness preset, but the command itself + // is a known ACP runtime (e.g. an explicit override like `omp`). + // Use that preset's launch args so we start its ACP/stdio mode + // rather than the bare command, which would launch the interactive + // TUI and time out at ACP `initialize` under headless Buzz. + normalize_agent_args(&effective_command, preset_args) } else { normalize_agent_args(&effective_command, record_args) } @@ -1548,6 +1557,104 @@ mod tests { ); } + #[test] + fn resolve_effective_harness_descriptor_uses_preset_args_for_pinned_command() { + // An agent pinned to a known ACP runtime via agent_command_override, + // with no runtime id and no instance args, must take the launch args + // from the matching PRESET_HARNESSES entry. Otherwise the bare command + // (e.g. `omp`) launches the interactive TUI, which times out at ACP + // `initialize` under headless Buzz (no controlling TTY). + let mk = |command: &str| crate::managed_agents::types::ManagedAgentRecord { + pubkey: "k".to_string(), + name: "agent".to_string(), + agent_command_override: Some(command.to_string()), + agent_args: vec![], + runtime: None, + persona_id: None, + private_key_nsec: String::new(), + auth_tag: None, + relay_url: String::new(), + avatar_url: None, + acp_command: "buzz-acp".to_string(), + agent_command: String::new(), + mcp_command: String::new(), + turn_timeout_seconds: 320, + idle_timeout_seconds: None, + max_turn_duration_seconds: None, + parallelism: 1, + system_prompt: None, + model: None, + provider: None, + persona_source_version: None, + env_vars: BTreeMap::new(), + start_on_app_launch: false, + auto_restart_on_config_change: true, + runtime_pid: None, + backend: Default::default(), + backend_agent_id: None, + provider_binary_path: None, + team_id: None, + persona_team_dir: None, + persona_name_in_team: None, + created_at: String::new(), + updated_at: String::new(), + last_started_at: None, + last_stopped_at: None, + last_exit_code: None, + last_error: None, + last_error_code: None, + respond_to: Default::default(), + respond_to_allowlist: vec![], + display_name: None, + slug: None, + name_pool: Vec::new(), + is_builtin: false, + is_active: true, + shared: false, + source_team: None, + source_team_persona_slug: None, + catalog_source: None, + definition_respond_to: None, + definition_respond_to_allowlist: Vec::new(), + definition_parallelism: None, + relay_mesh: None, + }; + let global = crate::managed_agents::GlobalAgentConfig::default(); + + let desc = |command: &str| { + resolve_effective_harness_descriptor(&mk(command), &[], &global).unwrap() + }; + + // ACP-mode runtimes resolve to `acp`. + assert_eq!(desc("devin").args, vec!["acp".to_string()]); + assert_eq!(desc("omp").args, vec!["acp".to_string()]); + assert_eq!(desc("opencode").args, vec!["acp".to_string()]); + assert_eq!(desc("kimi").args, vec!["acp".to_string()]); + assert_eq!(desc("cursor-agent").args, vec!["acp".to_string()]); + assert_eq!(desc("openclaw").args, vec!["acp".to_string()]); + // Grok launches its agent stdio mode (not the TUI). + assert_eq!( + desc("grok").args, + vec![ + "agent".to_string(), + "--always-approve".to_string(), + "stdio".to_string() + ] + ); + + // Explicit instance args always win over the preset fallback. + let mut explicit = mk("omp"); + explicit.agent_args = vec!["acp".to_string(), "--foo".to_string()]; + let explicit_desc = resolve_effective_harness_descriptor(&explicit, &[], &global).unwrap(); + assert_eq!( + explicit_desc.args, + vec!["acp".to_string(), "--foo".to_string()] + ); + + // An unknown command with no preset keeps the (empty) instance args. + assert!(desc("my-fancy-cli").args.is_empty()); + } + // ── provider-specific model fallback tests ──────────────────────────── #[test] From 8b2db5904591ffa3696a12ed4ed8647d700030e7 Mon Sep 17 00:00:00 2001 From: Brad Hallett <53977268+bradhallett@users.noreply.github.com> Date: Fri, 31 Jul 2026 09:24:06 -0400 Subject: [PATCH 2/2] fix: address review item #1 - log resolved launch args Signed-off-by: Brad Hallett <53977268+bradhallett@users.noreply.github.com> --- desktop/src-tauri/src/managed_agents/runtime.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/desktop/src-tauri/src/managed_agents/runtime.rs b/desktop/src-tauri/src/managed_agents/runtime.rs index 37927961ed..7b3b3c1642 100644 --- a/desktop/src-tauri/src/managed_agents/runtime.rs +++ b/desktop/src-tauri/src/managed_agents/runtime.rs @@ -544,6 +544,13 @@ pub fn spawn_agent_child( let resolved_agent_command = resolve_command(effective_command) .map(|p| p.display().to_string()) .unwrap_or_else(|| effective_command.clone()); + append_log_marker( + &log_path, + &format!( + "resolved launch: agent_command={:?} args={:?}", + resolved_agent_command, agent_args + ), + )?; // The caller supplies the explicit canonical pair relay. This is the only // relay this child may connect to, regardless of the record/workspace default.