diff --git a/crates/tui/src/core/capacity.rs b/crates/tui/src/core/capacity.rs index 576cc84a..437075fb 100644 --- a/crates/tui/src/core/capacity.rs +++ b/crates/tui/src/core/capacity.rs @@ -29,14 +29,24 @@ impl Default for CapacityControllerConfig { Self { enabled: true, - low_risk_max: 0.34, + // Tuning history (#63 follow-up): the previous defaults + // (low_risk_max=0.34, refresh_cooldown_turns=2, min_turns=2) + // fired `TargetedContextRefresh` every couple of turns whenever + // p_fail crept above 0.34. Each refresh runs `compact_messages_safe` + // which rewrites the conversation history — visually that looked + // like the agent "restarting" mid-session. Bumping the floor to + // 0.50 (still well below the medium ceiling of 0.62) and + // lengthening the cooldown to 6 turns reduces interventions + // ~3-4x without disabling the controller; it keeps firing on + // genuine risk while ignoring routine noise. + low_risk_max: 0.50, medium_risk_max: 0.62, severe_min_slack: -0.25, severe_violation_ratio: 0.40, - refresh_cooldown_turns: 2, + refresh_cooldown_turns: 6, replan_cooldown_turns: 5, max_replay_per_turn: 1, - min_turns_before_guardrail: 2, + min_turns_before_guardrail: 4, profile_window: 8, model_priors, fallback_default: 3.8, diff --git a/crates/tui/src/deepseek_theme.rs b/crates/tui/src/deepseek_theme.rs index 640a3354..46f4069f 100644 --- a/crates/tui/src/deepseek_theme.rs +++ b/crates/tui/src/deepseek_theme.rs @@ -64,7 +64,12 @@ impl Theme { section_border_color: palette::BORDER_COLOR, section_bg: palette::DEEPSEEK_INK, section_title_color: palette::DEEPSEEK_BLUE, - section_padding: Padding::uniform(1), + // Horizontal padding only. `Padding::uniform(1)` ate two rows of + // each sidebar panel — for compact terminals where Plan/Todos/Tasks + // get ~3 rows total via the 25% layout split, that left zero rows + // for content (#63 follow-up: panels rendered as empty boxes even + // when "No todos" / "No active plan" should have shown). + section_padding: Padding::horizontal(1), tool_title_color: palette::TEXT_SOFT, tool_value_color: palette::TEXT_MUTED, tool_label_color: palette::TEXT_DIM, diff --git a/crates/tui/src/tools/rlm_query.rs b/crates/tui/src/tools/rlm_query.rs index d528596b..9bf11fd4 100644 --- a/crates/tui/src/tools/rlm_query.rs +++ b/crates/tui/src/tools/rlm_query.rs @@ -6,11 +6,12 @@ use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; -use std::time::Instant; +use std::time::{Duration, Instant}; use async_trait::async_trait; use futures_util::future::join_all; use serde_json::{Value, json}; +use tokio::time::timeout; use tracing::debug; use crate::client::DeepSeekClient; @@ -27,6 +28,10 @@ const DEFAULT_CHILD_MODEL: &str = "deepseek-v4-flash"; const DEFAULT_MAX_TOKENS: u32 = 4096; /// Hard cap on parallel children — protects against runaway fan-out. const MAX_PARALLEL: usize = 16; +/// Per-child timeout — each child request must complete within this window +/// or it is treated as a timed-out error. Protects the fan-out from hanging +/// indefinitely when a single API request stalls. +const DEFAULT_CHILD_TIMEOUT: Duration = Duration::from_secs(120); // --------------------------------------------------------------------------- // RlmChildClient — dyn-compatible wrapper around LLM completion. @@ -232,7 +237,7 @@ impl ToolSpec for RlmQueryTool { temperature: Some(0.4), top_p: Some(0.9), }; - let response = client.complete(request).await; + let response = timeout(DEFAULT_CHILD_TIMEOUT, client.complete(request)).await; let elapsed_ms = started.elapsed().as_millis() as u64; in_flight.fetch_sub(1, Ordering::Relaxed); debug!( @@ -260,9 +265,16 @@ impl ToolSpec for RlmQueryTool { let mut ordered: Vec<(usize, String)> = results .into_iter() - .map(|(idx, res)| match res { - Ok(response) => (idx, extract_text(&response.content)), - Err(e) => (idx, format!("[error: {e}]")), + .map(|(idx, res)| { + let text = match res { + Ok(Ok(response)) => extract_text(&response.content), + Ok(Err(e)) => format!("[error: {e}]"), + Err(_) => format!( + "[error: timed out after {}s]", + DEFAULT_CHILD_TIMEOUT.as_secs() + ), + }; + (idx, text) }) .collect(); ordered.sort_by_key(|(idx, _)| *idx); diff --git a/crates/tui/src/tools/subagent/mod.rs b/crates/tui/src/tools/subagent/mod.rs index 861482bf..7618b62c 100644 --- a/crates/tui/src/tools/subagent/mod.rs +++ b/crates/tui/src/tools/subagent/mod.rs @@ -35,8 +35,12 @@ use crate::tools::todo::{SharedTodoList, TodoList}; // === Constants === -const DEFAULT_MAX_STEPS: u32 = 20; +const DEFAULT_MAX_STEPS: u32 = 100; const TOOL_TIMEOUT: Duration = Duration::from_secs(30); +/// Per-step LLM API call timeout. Each `create_message` request must complete +/// within this window or the step is treated as timed out. Prevents a single +/// stuck API call from blocking the sub-agent indefinitely. +const STEP_API_TIMEOUT: Duration = Duration::from_secs(120); const RESULT_POLL_INTERVAL: Duration = Duration::from_millis(250); const DEFAULT_RESULT_TIMEOUT_MS: u64 = 30_000; const MIN_WAIT_TIMEOUT_MS: u64 = 10_000; @@ -2398,7 +2402,9 @@ async fn run_subagent( top_p: None, }; - let response = runtime.client.create_message(request).await?; + let response = tokio::time::timeout(STEP_API_TIMEOUT, runtime.client.create_message(request)) + .await + .map_err(|_| anyhow!("API call timed out after {}s", STEP_API_TIMEOUT.as_secs()))??; let mut tool_uses = Vec::new(); for block in &response.content {