Skip to content
Open
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
13 changes: 10 additions & 3 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -769,9 +769,11 @@ exponential_base = 2.0
# ─────────────────────────────────────────────────────────────────────────────────
# Auto-compaction is a saved UI setting edited with `/config` (`auto_compact`).
# The optional saved threshold setting is `auto_compact_threshold_percent`
# (default 80). There is no config-file
# `[compaction]` table yet; runtime compaction budgets are chosen by the TUI
# from the active model/context window.
# (default 80). `[compaction].enabled` is the engine-level replacement
# compaction switch; when unset, the engine keeps using the saved auto_compact
# behavior.
[compaction]
# enabled = true

# Append-only Flash seams are experimental and opt-in while the v0.7.5
# context/cache audit validates prefix-cache behavior.
Expand All @@ -785,6 +787,11 @@ l2_threshold = 384000
l3_threshold = 576000
seam_model = "deepseek-v4-flash"

# Optional explicit alias for the context seam manager master switch. When set,
# this overrides `[context].enabled`; thresholds and model stay under [context].
# [seam_manager]
# enabled = false

# ─────────────────────────────────────────────────────────────────────────────────
# Workshop / Large-Output Routing (#548)
# ─────────────────────────────────────────────────────────────────────────────────
Expand Down
48 changes: 48 additions & 0 deletions crates/tui/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1682,6 +1682,22 @@ pub struct ContextConfig {
pub seam_model: Option<String>,
}

/// Explicit Flash seam-manager switch.
#[derive(Debug, Clone, Deserialize, Default)]
pub struct SeamManagerConfig {
/// Overrides `[context].enabled` when set. Default: inherit context.enabled.
#[serde(default)]
pub enabled: Option<bool>,
}

/// Engine replacement-compaction switch.
#[derive(Debug, Clone, Deserialize, Default)]
pub struct CompactionRuntimeConfig {
/// Overrides the settings-derived auto-compaction default when set.
#[serde(default)]
pub enabled: Option<bool>,
}

/// Sub-agent model overrides. Keys in `models` can be role names (`worker`,
/// `explorer`, `awaiter`) or type names (`general`, `explore`, `plan`,
/// `review`, `custom`). Per-call explicit model choices still win.
Expand Down Expand Up @@ -1998,6 +2014,16 @@ pub struct Config {
#[serde(default)]
pub context: ContextConfig,

/// Explicit Flash seam-manager switch (#3765). This is a narrow alias for
/// `[context].enabled`; threshold/model fields stay under `[context]`.
#[serde(default, alias = "seamManager")]
pub seam_manager: SeamManagerConfig,

/// Engine replacement-compaction switch (#3765). When unset, the runtime
/// keeps using the settings-derived `auto_compact` behavior.
#[serde(default)]
pub compaction: CompactionRuntimeConfig,

/// Agent Fleet trust/security/role/exec config.
#[serde(default)]
pub fleet: Option<codewhale_config::FleetConfigToml>,
Expand Down Expand Up @@ -3640,6 +3666,19 @@ impl Config {
self.context.project_pack.unwrap_or(true)
}

#[must_use]
pub fn seam_manager_enabled(&self) -> bool {
self.seam_manager
.enabled
.or(self.context.enabled)
.unwrap_or(false)
}

#[must_use]
pub fn compaction_enabled(&self, settings_default: bool) -> bool {
self.compaction.enabled.unwrap_or(settings_default)
}

/// Return whether shell execution is allowed. Defaults to `false`: shell
/// access must be opted into explicitly (GHSA-72w5-pf8h-xfp4).
#[must_use]
Expand Down Expand Up @@ -5536,6 +5575,15 @@ fn merge_config(base: Config, override_cfg: Config) -> Config {
.or(base.context.l3_threshold),
seam_model: override_cfg.context.seam_model.or(base.context.seam_model),
},
seam_manager: SeamManagerConfig {
enabled: override_cfg
.seam_manager
.enabled
.or(base.seam_manager.enabled),
},
compaction: CompactionRuntimeConfig {
enabled: override_cfg.compaction.enabled.or(base.compaction.enabled),
},
fleet: override_cfg.fleet.or(base.fleet),
subagents: override_cfg.subagents.or(base.subagents),
strict_tool_mode: override_cfg.strict_tool_mode.or(base.strict_tool_mode),
Expand Down
74 changes: 74 additions & 0 deletions crates/tui/src/config/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3509,6 +3509,7 @@ fn normalize_model_name_accepts_provider_prefixed_deepseek_ids() {
fn default_context_seams_are_opt_in() {
let config = Config::default();
assert!(!config.context.enabled.unwrap_or(false));
assert!(!config.seam_manager_enabled());
assert_eq!(config.context.l1_threshold.unwrap_or(192_000), 192_000);
assert_eq!(
config
Expand All @@ -3520,6 +3521,57 @@ fn default_context_seams_are_opt_in() {
);
}

#[test]
fn seam_manager_enabled_can_use_dedicated_table() -> Result<()> {
let config: Config = toml::from_str(
r#"
[context]
enabled = true

[seam_manager]
enabled = false
"#,
)?;

assert_eq!(config.context.enabled, Some(true));
assert_eq!(config.seam_manager.enabled, Some(false));
assert!(!config.seam_manager_enabled());

let config: Config = toml::from_str(
r#"
[seam_manager]
enabled = true
"#,
)?;

assert!(config.seam_manager_enabled());
Ok(())
}

#[test]
fn compaction_enabled_uses_config_override_when_present() -> Result<()> {
let config = Config::default();
assert!(config.compaction_enabled(true));
assert!(!config.compaction_enabled(false));

let disabled: Config = toml::from_str(
r#"
[compaction]
enabled = false
"#,
)?;
assert!(!disabled.compaction_enabled(true));

let enabled: Config = toml::from_str(
r#"
[compaction]
enabled = true
"#,
)?;
assert!(enabled.compaction_enabled(false));
Ok(())
}

#[test]
fn profile_without_context_does_not_disable_base_context() {
let mut profiles = HashMap::new();
Expand All @@ -3539,6 +3591,28 @@ fn profile_without_context_does_not_disable_base_context() {
assert_eq!(merged.context.enabled, Some(true));
}

#[test]
fn profile_without_context_gates_does_not_disable_base_gates() {
let mut profiles = HashMap::new();
profiles.insert("work".to_string(), Config::default());
let config = ConfigFile {
base: Config {
seam_manager: SeamManagerConfig {
enabled: Some(true),
},
compaction: CompactionRuntimeConfig {
enabled: Some(false),
},
..Default::default()
},
profiles: Some(profiles),
};

let merged = apply_profile(config, Some("work")).expect("profile");
assert_eq!(merged.seam_manager.enabled, Some(true));
assert_eq!(merged.compaction.enabled, Some(false));
}

#[test]
fn profile_skills_config_merges_individual_fields() {
let mut profiles = HashMap::new();
Expand Down
1 change: 1 addition & 0 deletions crates/tui/src/config_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,7 @@ fn validate_document(doc: &ConfigUiDocument) -> Result<()> {
fn reload_runtime_config(app: &mut App, config: &mut Config) -> Result<()> {
let reloaded = Config::load(app.config_path.clone(), app.config_profile.as_deref())?;
*config = reloaded.clone();
app.refresh_config_runtime_overrides(config);
app.api_provider = reloaded.api_provider();
app.reasoning_effort =
ReasoningEffort::from_setting(reloaded.reasoning_effort().unwrap_or_else(|| {
Expand Down
2 changes: 1 addition & 1 deletion crates/tui/src/core/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -916,7 +916,7 @@ impl Engine {
// is worth the extra request and transcript mutation.
let seam_manager = deepseek_client.as_ref().map(|main_client| {
let seam_config = SeamConfig {
enabled: api_config.context.enabled.unwrap_or(false),
enabled: api_config.seam_manager_enabled(),
verbatim_window_turns: api_config
.context
.verbatim_window_turns
Expand Down
2 changes: 1 addition & 1 deletion crates/tui/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7027,7 +7027,7 @@ async fn run_exec_agent(
)
};
let compaction = CompactionConfig {
enabled: auto_compact_enabled,
enabled: execution_config.compaction_enabled(auto_compact_enabled),
model: effective_model.clone(),
token_threshold: crate::route_budget::compaction_threshold_for_route_at_percent(
effective_provider,
Expand Down
4 changes: 2 additions & 2 deletions crates/tui/src/runtime_threads.rs
Original file line number Diff line number Diff line change
Expand Up @@ -874,7 +874,7 @@ impl RuntimeThreadManager {
)
};
let compaction = crate::compaction::CompactionConfig {
enabled: auto_compact_enabled,
enabled: cfg.compaction_enabled(auto_compact_enabled),
model: String::new(), // per-engine, filled below
token_threshold: compaction_threshold_for_model_at_percent(
&cfg.default_text_model.clone().unwrap_or_default(),
Expand Down Expand Up @@ -2506,7 +2506,7 @@ impl RuntimeThreadManager {
auto_compact_default_for_model(&thread.model)
};
let compaction = CompactionConfig {
enabled: auto_compact_enabled,
enabled: cfg.compaction_enabled(auto_compact_enabled),
model: thread.model.clone(),
token_threshold: compaction_threshold_for_model_at_percent(
&thread.model,
Expand Down
13 changes: 12 additions & 1 deletion crates/tui/src/tui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1696,6 +1696,7 @@ pub struct App {
#[allow(dead_code)]
pub system_prompt: Option<SystemPrompt>,
pub auto_compact: bool,
pub compaction_enabled_override: Option<bool>,
pub auto_compact_user_configured: bool,
pub auto_compact_threshold_percent: f64,
pub calm_mode: bool,
Expand Down Expand Up @@ -2628,6 +2629,7 @@ impl App {
bracketed_paste_seen: false,
system_prompt: None,
auto_compact,
compaction_enabled_override: config.compaction.enabled,
Comment thread
nightt5879 marked this conversation as resolved.
auto_compact_user_configured,
auto_compact_threshold_percent,
calm_mode,
Expand Down Expand Up @@ -5692,13 +5694,22 @@ impl App {

pub fn compaction_config(&self) -> CompactionConfig {
CompactionConfig {
enabled: self.auto_compact,
enabled: self.automatic_compaction_enabled(),
token_threshold: self.compact_threshold,
model: self.effective_model_for_budget().to_string(),
..Default::default()
}
}

pub fn automatic_compaction_enabled(&self) -> bool {
self.compaction_enabled_override
.unwrap_or(self.auto_compact)
}

pub fn refresh_config_runtime_overrides(&mut self, config: &Config) {
self.compaction_enabled_override = config.compaction.enabled;
}

pub fn fallback_chain_entries(&self) -> Vec<(usize, ApiProvider, bool)> {
let Some(chain) = &self.provider_chain else {
return Vec::new();
Expand Down
49 changes: 48 additions & 1 deletion crates/tui/src/tui/app/tests.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use super::*;
use crate::config::{ApiProvider, Config, ProviderConfig, ProvidersConfig};
use crate::config::{
ApiProvider, CompactionRuntimeConfig, Config, ProviderConfig, ProvidersConfig,
};
use crate::test_support::{EnvVarGuard, lock_test_env};
use crate::tools::plan::{PlanItemArg, StepStatus, UpdatePlanArgs};
use crate::tools::todo::TodoStatus;
Expand Down Expand Up @@ -1955,6 +1957,51 @@ fn test_compaction_config() {
assert_eq!(config.model, "deepseek-v4-flash");
}

#[test]
fn compaction_config_respects_config_enabled_override() {
let config = Config {
compaction: CompactionRuntimeConfig {
enabled: Some(false),
},
..Default::default()
};
let mut app = App::new(test_options(false), &config);
app.auto_compact = true;
assert!(!app.compaction_config().enabled);

let config = Config {
compaction: CompactionRuntimeConfig {
enabled: Some(true),
},
..Default::default()
};
let mut app = App::new(test_options(false), &config);
app.auto_compact = false;
assert!(app.compaction_config().enabled);
}

#[test]
fn app_refreshes_compaction_override_from_runtime_config() {
let mut app = App::new(test_options(false), &Config::default());
app.compaction_enabled_override = Some(true);

let config = Config {
compaction: CompactionRuntimeConfig {
enabled: Some(false),
},
..Default::default()
};
app.refresh_config_runtime_overrides(&config);
assert_eq!(app.compaction_enabled_override, Some(false));
assert!(!app.automatic_compaction_enabled());

let config = Config::default();
app.auto_compact = true;
app.refresh_config_runtime_overrides(&config);
assert_eq!(app.compaction_enabled_override, None);
assert!(app.automatic_compaction_enabled());
}

#[test]
fn test_update_model_compaction_budget() {
let mut app = App::new(test_options(false), &Config::default());
Expand Down
7 changes: 4 additions & 3 deletions crates/tui/src/tui/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7654,6 +7654,7 @@ async fn apply_command_result(
match Config::load(app.config_path.clone(), Some(&profile)) {
Ok(new_config) => {
*config = new_config.clone();
app.refresh_config_runtime_overrides(config);
app.api_provider = config.api_provider();
let new_model = config.default_model();
app.set_model_selection(new_model.clone());
Expand Down Expand Up @@ -10636,8 +10637,8 @@ fn maybe_warn_context_pressure(app: &mut App) {
return;
}

let recommendation = if !app.auto_compact {
"Consider enabling auto_compact or use /compact."
let recommendation = if !app.automatic_compaction_enabled() {
"Automatic compaction is disabled; use /compact or enable auto_compact/[compaction].enabled."
} else if percent >= configured_threshold {
"Auto-compaction will run before the next send."
} else {
Expand All @@ -10664,7 +10665,7 @@ fn maybe_warn_context_pressure(app: &mut App) {
}

fn should_auto_compact_before_send(app: &App) -> bool {
if !app.auto_compact {
if !app.automatic_compaction_enabled() {
return false;
}
context_usage_snapshot(app)
Expand Down
Loading
Loading