From 44df0020a6b7ca32eac5dcd1cf777f8a1337ca8b Mon Sep 17 00:00:00 2001 From: Ryder Freeman Date: Sun, 17 May 2026 08:14:37 -0700 Subject: [PATCH] fix(run): respect explicit --tool flag when --tier is also specified (#1440) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a caller passed both `--tier ` and `--tool ` (e.g. `csa run --tier tier4 --tool claude-code`), the session would still silently fall over to a different tool (gemini-cli) on slot contention. The tool resolution layer correctly pinned the requested tool, but the runtime failover gate ignored that and enabled cross-tool failover whenever a tier was present. Root cause: `allow_cross_tool_failover` granted cross-tool failover under a tier exception clause that triggered even for `ToolSelectionStrategy::Explicit(_)`. Explicit `--tool` is the user's hard selection and must never be silently replaced; tier remains in charge of model selection for the chosen tool via `resolve_requested_tool_from_tier`. Fix: drop the tier exception. `Explicit(_)` strategies block cross-tool failover regardless of tier or force-ignore-tier-setting. The `resolved_tier_name` and `force_ignore_tier_setting` parameters are preserved (renamed to `_resolved_tier_name` / `_force_ignore_tier_setting`) to keep the call sites stable. Regression test: `explicit_tool_in_tier_blocks_cross_tool_failover` asserts the user's bug scenario — `--tier t4` + explicit `ToolName::ClaudeCode` must not enable cross-tool failover. Co-Authored-By: Claude Opus 4.7 (1M context) --- Cargo.lock | 32 +++++++++---------- Cargo.toml | 2 +- .../src/run_cmd_attempt_support.rs | 9 ++++-- .../src/run_cmd_attempt_tests.rs | 12 ++++--- 4 files changed, 30 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d19a0716..43ef6f13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -509,7 +509,7 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cli-sub-agent" -version = "0.1.733" +version = "0.1.734" dependencies = [ "anyhow", "chrono", @@ -701,7 +701,7 @@ dependencies = [ [[package]] name = "csa-acp" -version = "0.1.733" +version = "0.1.734" dependencies = [ "agent-client-protocol", "anyhow", @@ -721,7 +721,7 @@ dependencies = [ [[package]] name = "csa-config" -version = "0.1.733" +version = "0.1.734" dependencies = [ "anyhow", "chrono", @@ -739,7 +739,7 @@ dependencies = [ [[package]] name = "csa-core" -version = "0.1.733" +version = "0.1.734" dependencies = [ "agent-teams", "chrono", @@ -755,7 +755,7 @@ dependencies = [ [[package]] name = "csa-eval" -version = "0.1.733" +version = "0.1.734" dependencies = [ "anyhow", "chrono", @@ -769,7 +769,7 @@ dependencies = [ [[package]] name = "csa-executor" -version = "0.1.733" +version = "0.1.734" dependencies = [ "agent-teams", "anyhow", @@ -797,7 +797,7 @@ dependencies = [ [[package]] name = "csa-hooks" -version = "0.1.733" +version = "0.1.734" dependencies = [ "anyhow", "chrono", @@ -816,7 +816,7 @@ dependencies = [ [[package]] name = "csa-lock" -version = "0.1.733" +version = "0.1.734" dependencies = [ "anyhow", "chrono", @@ -830,7 +830,7 @@ dependencies = [ [[package]] name = "csa-mcp-hub" -version = "0.1.733" +version = "0.1.734" dependencies = [ "anyhow", "axum", @@ -853,7 +853,7 @@ dependencies = [ [[package]] name = "csa-memory" -version = "0.1.733" +version = "0.1.734" dependencies = [ "anyhow", "async-trait", @@ -872,7 +872,7 @@ dependencies = [ [[package]] name = "csa-process" -version = "0.1.733" +version = "0.1.734" dependencies = [ "anyhow", "chrono", @@ -891,7 +891,7 @@ dependencies = [ [[package]] name = "csa-resource" -version = "0.1.733" +version = "0.1.734" dependencies = [ "anyhow", "csa-core", @@ -907,7 +907,7 @@ dependencies = [ [[package]] name = "csa-scheduler" -version = "0.1.733" +version = "0.1.734" dependencies = [ "anyhow", "chrono", @@ -925,7 +925,7 @@ dependencies = [ [[package]] name = "csa-session" -version = "0.1.733" +version = "0.1.734" dependencies = [ "anyhow", "chrono", @@ -951,7 +951,7 @@ dependencies = [ [[package]] name = "csa-todo" -version = "0.1.733" +version = "0.1.734" dependencies = [ "anyhow", "chrono", @@ -4516,7 +4516,7 @@ dependencies = [ [[package]] name = "weave" -version = "0.1.733" +version = "0.1.734" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index f942c814..a628703f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ default-members = [ resolver = "2" [workspace.package] -version = "0.1.733" +version = "0.1.734" edition = "2024" rust-version = "1.88" license = "Apache-2.0" diff --git a/crates/cli-sub-agent/src/run_cmd_attempt_support.rs b/crates/cli-sub-agent/src/run_cmd_attempt_support.rs index c2a1d861..13acb1fe 100644 --- a/crates/cli-sub-agent/src/run_cmd_attempt_support.rs +++ b/crates/cli-sub-agent/src/run_cmd_attempt_support.rs @@ -7,16 +7,19 @@ use crate::pipeline; pub(crate) fn allow_cross_tool_failover( strategy: ToolSelectionStrategy, - resolved_tier_name: Option<&str>, - force_ignore_tier_setting: bool, + _resolved_tier_name: Option<&str>, + _force_ignore_tier_setting: bool, no_failover: bool, ) -> bool { if no_failover { return false; } + // Explicit `--tool` (from CLI or skill agent_config) is the user's hard + // selection: never silently fall over to a different tool, even when a + // tier is also specified (#1440). Tier still drives model selection for + // the chosen tool via `resolve_requested_tool_from_tier`. !matches!(strategy, ToolSelectionStrategy::Explicit(_)) - || (!force_ignore_tier_setting && resolved_tier_name.is_some()) } pub(crate) fn resolve_attempt_initial_response_timeout_seconds( diff --git a/crates/cli-sub-agent/src/run_cmd_attempt_tests.rs b/crates/cli-sub-agent/src/run_cmd_attempt_tests.rs index d08d8905..125a52a2 100644 --- a/crates/cli-sub-agent/src/run_cmd_attempt_tests.rs +++ b/crates/cli-sub-agent/src/run_cmd_attempt_tests.rs @@ -595,13 +595,15 @@ fn explicit_tool_no_failover_blocks_cross_tool_failover() { )); } +// Regression #1440: explicit `--tool` blocks cross-tool failover even with `--tier`. #[test] -fn explicit_tool_in_tier_keeps_cross_tool_failover_available() { - assert!(allow_cross_tool_failover( - ToolSelectionStrategy::Explicit(ToolName::Codex), - Some("tier-3-complex"), - false, +fn explicit_tool_in_tier_blocks_cross_tool_failover() { + let strategy = ToolSelectionStrategy::Explicit(ToolName::ClaudeCode); + assert!(!allow_cross_tool_failover( + strategy, + Some("t4"), false, + false )); }