From 6e2950029e7018b644477f14fbcd9fff91862bd7 Mon Sep 17 00:00:00 2001
From: kenny lopez
Date: Thu, 23 Jul 2026 14:57:30 -0700
Subject: [PATCH] Preserve explicit hidden harness starts
Signed-off-by: kenny lopez
---
.../src-tauri/src/commands/agents_tests.rs | 19 +++++++++++
.../src/managed_agents/global_config/mod.rs | 12 +++----
.../src/managed_agents/global_config/tests.rs | 20 +++++++++++
desktop/src/features/agents/AGENTS.md | 5 +--
.../agents/lib/resolvePersonaRuntime.test.mjs | 23 +++++++++++++
.../agents/lib/resolvePersonaRuntime.ts | 13 ++++++++
.../agents/ui/AddTeamToChannelDialog.tsx | 33 ++++++++++++-------
7 files changed, 104 insertions(+), 21 deletions(-)
diff --git a/desktop/src-tauri/src/commands/agents_tests.rs b/desktop/src-tauri/src/commands/agents_tests.rs
index 3897c99d8..b1951f195 100644
--- a/desktop/src-tauri/src/commands/agents_tests.rs
+++ b/desktop/src-tauri/src/commands/agents_tests.rs
@@ -235,6 +235,25 @@ fn deploy_resolver_ignores_defaults_from_a_different_implicit_runtime() {
);
}
+#[test]
+fn deploy_resolver_ignores_mismatched_defaults_without_a_stored_override() {
+ let mut record = bare_agent_record(Some("p1"), None, None);
+ record.agent_command = "buzz-agent".to_string();
+ record.agent_command_override = None;
+ let personas = vec![persona_record("p1", None, None)];
+ let global = crate::managed_agents::GlobalAgentConfig {
+ model: Some("global-model".to_string()),
+ provider: Some("global-provider".to_string()),
+ preferred_runtime: Some("goose".to_string()),
+ ..Default::default()
+ };
+
+ assert_eq!(
+ resolve_deploy_model_provider(&record, &personas, &global),
+ (None, None)
+ );
+}
+
#[test]
fn normalize_relay_mesh_rejects_empty_model_ref() {
let config = RelayMeshConfig {
diff --git a/desktop/src-tauri/src/managed_agents/global_config/mod.rs b/desktop/src-tauri/src/managed_agents/global_config/mod.rs
index 4d4d51423..00ee5950b 100644
--- a/desktop/src-tauri/src/managed_agents/global_config/mod.rs
+++ b/desktop/src-tauri/src/managed_agents/global_config/mod.rs
@@ -235,19 +235,17 @@ pub(crate) fn global_model_provider_for_record<'a>(
return global_values;
}
- let Some(selected_runtime) = record
- .agent_command_override
+ let Some(preferred_runtime) = global
+ .preferred_runtime
.as_deref()
.and_then(crate::managed_agents::known_acp_runtime)
else {
return global_values;
};
- let preferred_runtime = global
- .preferred_runtime
- .as_deref()
- .and_then(crate::managed_agents::known_acp_runtime);
+ let selected_command = crate::managed_agents::record_agent_command(record, personas);
+ let selected_runtime = crate::managed_agents::known_acp_runtime(&selected_command);
- if preferred_runtime.is_some_and(|preferred| std::ptr::eq(preferred, selected_runtime)) {
+ if selected_runtime.is_some_and(|selected| std::ptr::eq(preferred_runtime, selected)) {
global_values
} else {
(None, None)
diff --git a/desktop/src-tauri/src/managed_agents/global_config/tests.rs b/desktop/src-tauri/src/managed_agents/global_config/tests.rs
index 5f50cdca4..a5e6d35d9 100644
--- a/desktop/src-tauri/src/managed_agents/global_config/tests.rs
+++ b/desktop/src-tauri/src/managed_agents/global_config/tests.rs
@@ -491,6 +491,26 @@ fn runtime_fallback_does_not_inherit_defaults_from_a_different_preferred_runtime
);
}
+#[test]
+fn default_command_fallback_does_not_inherit_defaults_from_another_preferred_runtime() {
+ let mut record = bare_record();
+ record.persona_id = Some("p1".to_string());
+ record.agent_command = "buzz-agent".to_string();
+ record.agent_command_override = None;
+ let personas = vec![persona("p1", None, None)];
+ let global = GlobalAgentConfig {
+ model: Some("global-model".to_string()),
+ provider: Some("global-provider".to_string()),
+ preferred_runtime: Some("goose".to_string()),
+ ..Default::default()
+ };
+
+ assert_eq!(
+ resolve_effective_model_provider(&record, &personas, &global),
+ (None, None)
+ );
+}
+
#[test]
fn runtime_fallback_inherits_defaults_when_it_matches_the_preferred_runtime() {
let mut record = bare_record();
diff --git a/desktop/src/features/agents/AGENTS.md b/desktop/src/features/agents/AGENTS.md
index 54361cf66..25a681341 100644
--- a/desktop/src/features/agents/AGENTS.md
+++ b/desktop/src/features/agents/AGENTS.md
@@ -99,8 +99,9 @@ with a TypeScript lookup table or an id comparison in a component.
`resolveStartRuntimeForDefinition` is the shared boundary that filters every
runtime-less start through the device's visible-runtime set; call it instead
of duplicating default selection in a start surface. Team deploys use the
- same visible-runtime set. Definitions already pinned to a hidden runtime
- remain runnable.
+ same visible-runtime set only for members that need an implicit fallback, so
+ a fully pinned team remains deployable even when every installed runtime is
+ hidden. Definitions already pinned to a hidden runtime remain runnable.
10. **The defaults modal is progressively disclosed.** An unset global config
starts on the Buzz Agent-first deployment fallback and carries that visible
harness into the next saved edit. The `progressive-defaults` disclosure
diff --git a/desktop/src/features/agents/lib/resolvePersonaRuntime.test.mjs b/desktop/src/features/agents/lib/resolvePersonaRuntime.test.mjs
index 37796b8bb..f77f056d6 100644
--- a/desktop/src/features/agents/lib/resolvePersonaRuntime.test.mjs
+++ b/desktop/src/features/agents/lib/resolvePersonaRuntime.test.mjs
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
import test from "node:test";
import {
+ canResolveAllPersonaRuntimes,
collectRuntimeWarnings,
resolvePersonaRuntime,
} from "./resolvePersonaRuntime.ts";
@@ -177,3 +178,25 @@ test("collectRuntimeWarnings — override=false behaves identically to no overri
test("collectRuntimeWarnings — empty personas array always returns empty", () => {
assert.deepEqual(collectRuntimeWarnings([], runtimes, goose, true), []);
});
+
+test("team resolution allows explicit runtimes without a fallback", () => {
+ assert.equal(
+ canResolveAllPersonaRuntimes(
+ [{ runtime: "goose" }, { runtime: "claude" }],
+ runtimes,
+ null,
+ ),
+ true,
+ );
+});
+
+test("team resolution requires a fallback for runtime-less definitions", () => {
+ assert.equal(
+ canResolveAllPersonaRuntimes([{ runtime: null }], runtimes, null),
+ false,
+ );
+ assert.equal(
+ canResolveAllPersonaRuntimes([{ runtime: null }], runtimes, goose),
+ true,
+ );
+});
diff --git a/desktop/src/features/agents/lib/resolvePersonaRuntime.ts b/desktop/src/features/agents/lib/resolvePersonaRuntime.ts
index f833d7113..474c218f6 100644
--- a/desktop/src/features/agents/lib/resolvePersonaRuntime.ts
+++ b/desktop/src/features/agents/lib/resolvePersonaRuntime.ts
@@ -142,3 +142,16 @@ export function collectRuntimeWarnings(
}
return warnings;
}
+
+/** Whether every definition can resolve with the supplied optional fallback. */
+export function canResolveAllPersonaRuntimes(
+ personas: readonly { runtime: string | null }[],
+ runtimes: readonly AcpRuntime[],
+ fallbackRuntime: AcpRuntime | null,
+): boolean {
+ return personas.every(
+ (persona) =>
+ resolvePersonaRuntime(persona.runtime, runtimes, fallbackRuntime)
+ .runtime !== null,
+ );
+}
diff --git a/desktop/src/features/agents/ui/AddTeamToChannelDialog.tsx b/desktop/src/features/agents/ui/AddTeamToChannelDialog.tsx
index c105389ae..62475819a 100644
--- a/desktop/src/features/agents/ui/AddTeamToChannelDialog.tsx
+++ b/desktop/src/features/agents/ui/AddTeamToChannelDialog.tsx
@@ -14,6 +14,7 @@ import {
import { shouldPinSelectedRuntimeForDefinition } from "@/features/agents/lib/instanceInputForDefinition";
import { useSelectableAcpRuntimes } from "@/features/agents/lib/runtimeVisibilityPreference";
import {
+ canResolveAllPersonaRuntimes,
collectRuntimeWarnings,
getDefaultPersonaRuntime,
resolvePersonaRuntime,
@@ -86,6 +87,11 @@ export function AddTeamToChannelDialog({
);
const resolved = teamPersonaResolution.resolvedPersonas;
const missingPersonaCount = teamPersonaResolution.missingPersonaCount;
+ const canResolveTeamRuntimes = canResolveAllPersonaRuntimes(
+ resolved,
+ runtimes,
+ defaultProvider,
+ );
// Surface warnings when a persona's preferred runtime is unavailable.
// This dialog has no runtime selector, so the fallback is always
@@ -121,7 +127,7 @@ export function AddTeamToChannelDialog({
channels.find((channel) => channel.id === channelId) ?? null;
async function handleDeploy() {
- if (!team || !selectedChannel || !defaultProvider) {
+ if (!team || !selectedChannel || !canResolveTeamRuntimes) {
return;
}
@@ -136,21 +142,23 @@ export function AddTeamToChannelDialog({
runtimes,
defaultProvider,
);
- const runtimeToUse = personaRuntime ?? defaultProvider;
+ if (!personaRuntime) {
+ throw new Error("No runtime is available for this team member.");
+ }
return {
runtime: {
- id: runtimeToUse.id,
- label: runtimeToUse.label,
- command: runtimeToUse.command,
- defaultArgs: runtimeToUse.defaultArgs,
- mcpCommand: runtimeToUse.mcpCommand,
+ id: personaRuntime.id,
+ label: personaRuntime.label,
+ command: personaRuntime.command,
+ defaultArgs: personaRuntime.defaultArgs,
+ mcpCommand: personaRuntime.mcpCommand,
},
name: persona.displayName,
systemPrompt: persona.systemPrompt,
avatarUrl: persona.avatarUrl ?? undefined,
harnessOverride: shouldPinSelectedRuntimeForDefinition(
persona.runtime,
- runtimeToUse.id,
+ personaRuntime.id,
),
model: persona.model ?? undefined,
personaId: persona.id,
@@ -262,10 +270,11 @@ export function AddTeamToChannelDialog({
) : null}
- {!defaultProvider && !providersQuery.isLoading ? (
+ {!canResolveTeamRuntimes && !providersQuery.isLoading ? (
- No ACP runtimes found. Make sure an agent runtime (e.g. Goose)
- is installed.
+ {runtimes.length === 0
+ ? "No ACP runtimes found. Make sure an agent runtime (e.g. Goose) is installed."
+ : "No enabled fallback runtime is available. Turn on a harness to deploy runtime-less team members."}
) : null}
@@ -307,7 +316,7 @@ export function AddTeamToChannelDialog({
disabled={
!team ||
!selectedChannel ||
- !defaultProvider ||
+ !canResolveTeamRuntimes ||
resolved.length === 0 ||
missingPersonaCount > 0 ||
channelsQuery.isLoading ||