/// Default auto-compact threshold (% of context window) when no source sets it. pub const DEFAULT_AUTO_COMPACT_THRESHOLD_PERCENT: u8 = 85; /// Env-var override for `auto_compact_threshold_percent`. Parsed as `u8`; /// out-of-range or unparseable values are ignored. pub(crate) const ENV_AUTO_COMPACT_THRESHOLD_PERCENT: &str = "KIGI_AUTO_COMPACT_THRESHOLD_PERCENT"; /// Resolve auto-compact threshold percent (0-100) for the given model. /// /// Two scopes (per-model and global) across two tiers (user TOML and /// remote settings). User-tier always wins over remote; within a tier, per-model /// wins over global. Env var sits on top as a per-process override. /// /// Precedence (highest first): /// 1. env `KIGI_AUTO_COMPACT_THRESHOLD_PERCENT` /// 2. user TOML `[model.].auto_compact_threshold_percent` /// (read from `cfg.config_models`; the effective merge of user + /// managed `[model.]` sections) /// 3. user TOML `[session].auto_compact_threshold_percent` /// (read from `cfg.session.auto_compact_threshold_percent: Option`) /// 4. remote settings per-model `ModelInfo.auto_compact_threshold_percent` /// (populated from `kigi_models[i].auto_compact_threshold_percent`; /// intentionally NOT collapsed via `ConfigModelOverride::apply` so the /// user-vs-GB per-model distinction is preserved) /// 5. remote settings global `RemoteSettings.auto_compact_threshold_percent` /// (populated from `kigi_settings.auto_compact_threshold_percent`) /// 6. default `DEFAULT_AUTO_COMPACT_THRESHOLD_PERCENT` (85) /// /// Values outside `0..=100` from the env var are ignored with a debug log and /// the resolver falls through to the next tier. TOML/remote fields are typed /// `u8` and so naturally constrained. pub fn resolve_auto_compact_threshold_percent( cfg: &crate::agent::config::Config, model_id: &str, model: Option<&crate::agent::config::ModelInfo>, ) -> u8 { resolve_auto_compact_threshold_percent_from_tiers( cfg.config_models .get(model_id) .and_then(|m| m.auto_compact_threshold_percent), cfg.session.auto_compact_threshold_percent, model.and_then(|m| m.auto_compact_threshold_percent), cfg.remote_settings .as_ref() .and_then(|r| r.auto_compact_threshold_percent), ) } /// Lower-level form of [`resolve_auto_compact_threshold_percent`] that takes /// the four tiers as plain `Option` values rather than reaching into a /// `Config`. Useful from sites that don't hold a `Config` reference (e.g., /// subagent spawn paths where the parent's config tiers are plumbed in /// explicitly and the per-model lookup uses the SUBAGENT's resolved model id, /// not the parent's). /// /// Precedence: env > `user_per_model` > `user_global` > `gb_per_model` /// > `gb_global` > `DEFAULT_AUTO_COMPACT_THRESHOLD_PERCENT`. pub fn resolve_auto_compact_threshold_percent_from_tiers( user_per_model: Option, user_global: Option, gb_per_model: Option, gb_global: Option, ) -> u8 { fn clamp_env(raw: i64) -> Option { if (0..=100).contains(&raw) { Some(raw as u8) } else { tracing::debug!( source = "env", value = raw, "auto_compact_threshold_percent out of range 0..=100; ignoring" ); None } } let from_env = || -> Option { std::env::var(ENV_AUTO_COMPACT_THRESHOLD_PERCENT) .ok() .and_then(|s| s.parse::().ok()) .and_then(clamp_env) }; from_env() .or(user_per_model) .or(user_global) .or(gb_per_model) .or(gb_global) .unwrap_or(DEFAULT_AUTO_COMPACT_THRESHOLD_PERCENT) } /// Client default per-compaction wall-clock budget (seconds). Fleet p99 of /// successful compactions is ~181s (≈225s at 400K+ input), so 300s clears the /// legit tail with margin while cutting a runaway from the ~600s deadline. pub const DEFAULT_COMPACTION_WALL_CLOCK_BUDGET_SECS: u64 = 300; /// Below this, a configured budget is almost certainly a misconfig (fleet /// success p99 ~181s); logged at `warn`, not clamped. const COMPACTION_WALL_CLOCK_BUDGET_WARN_SECS: u64 = 120; /// Env override for the compaction wall-clock budget (seconds). Parsed as /// `u64`; unparseable values fall through. const ENV_COMPACTION_WALL_CLOCK_BUDGET_SECS: &str = "KIGI_COMPACTION_WALL_CLOCK_SECS"; /// Resolve the per-compaction wall-clock budget (seconds). Precedence: env /// `KIGI_COMPACTION_WALL_CLOCK_SECS` > remote settings global /// `RemoteSettings.compaction_wall_clock_budget_secs` > /// [`DEFAULT_COMPACTION_WALL_CLOCK_BUDGET_SECS`] (a per-model `ModelInfo` tier /// would slot in ahead of the global one). /// /// `0` **disables** it. Low values are warned, not clamped — any "safe" clamp /// (e.g. 30s) would itself cut legit compactions, trading one silent failure for /// another; ops own the value. pub fn resolve_compaction_wall_clock_budget_secs(gb_global: Option) -> u64 { let from_env = std::env::var(ENV_COMPACTION_WALL_CLOCK_BUDGET_SECS) .ok() .and_then(|s| s.trim().parse::().ok()); let resolved = from_env .or(gb_global) .unwrap_or(DEFAULT_COMPACTION_WALL_CLOCK_BUDGET_SECS); if resolved > 0 && resolved < COMPACTION_WALL_CLOCK_BUDGET_WARN_SECS { tracing::warn!( budget_secs = resolved, "compaction wall-clock budget {resolved}s is below {COMPACTION_WALL_CLOCK_BUDGET_WARN_SECS}s \ and may cut legitimate compactions (fleet success p99 ~181s); set 0 to disable" ); } resolved } #[cfg(test)] mod compaction_wall_clock_budget_tests { use super::resolve_compaction_wall_clock_budget_secs as resolve; // Assumes KIGI_COMPACTION_WALL_CLOCK_SECS is unset in the test env. #[test] fn default_global_disable_and_no_clamp() { // client default assert_eq!(resolve(None), 300); // server global wins assert_eq!(resolve(Some(450)), 450); // 0 explicitly disables (no clamp) assert_eq!(resolve(Some(0)), 0); // low values pass through (warned, not clamped) assert_eq!(resolve(Some(5)), 5); } }