Skip to main content

codetether_agent/session/delegation/
lambda.rs

1//! Forgetting factor `λ` resolution + range clamp (Phase C step 32).
2//!
3//! CADMAS-CTX recommends `λ ∈ [0.9, 1.0)` for cyclical-drift mitigation:
4//! tighter values forget too quickly, looser values are indistinguishable
5//! from no decay. `1.0` (and above) disables decay entirely; values below
6//! `0.9` collapse the posterior toward the latest observation.
7
8use super::config::DEFAULT_LAMBDA;
9
10/// Lower bound for the forgetting factor.
11pub const LAMBDA_MIN: f64 = 0.9;
12/// Upper bound (exclusive): `1.0` would mean "never forget" — handled by
13/// the caller, not by this clamp. We saturate at the largest representable
14/// value strictly below `1.0`.
15pub const LAMBDA_MAX_INCLUSIVE: f64 = 1.0;
16
17/// Read `CODETETHER_DELEGATION_LAMBDA` and clamp into `[0.9, 1.0)`.
18///
19/// Returns `None` when the env var is absent or unparseable so the caller
20/// can fall back to its persisted [`DelegationConfig::lambda`].
21///
22/// [`DelegationConfig::lambda`]: super::config::DelegationConfig::lambda
23pub fn env_lambda_override() -> Option<f64> {
24    let raw = std::env::var("CODETETHER_DELEGATION_LAMBDA").ok()?;
25    let value: f64 = raw.trim().parse().ok()?;
26    Some(clamp_lambda(value))
27}
28
29/// Clamp `raw` into the documented `[0.9, 1.0)` range.
30///
31/// `1.0` is preserved as-is — that signals "no decay" at the call site,
32/// matching the historical [`DEFAULT_LAMBDA`]. Values strictly below `0.9`
33/// snap up to `0.9`; values strictly above `1.0` (or non-finite) fall back
34/// to the default so misconfiguration cannot cancel the bandit out.
35pub fn clamp_lambda(raw: f64) -> f64 {
36    if !raw.is_finite() {
37        return DEFAULT_LAMBDA;
38    }
39    if raw <= 0.0 || raw > LAMBDA_MAX_INCLUSIVE {
40        return DEFAULT_LAMBDA;
41    }
42    if raw < LAMBDA_MIN {
43        return LAMBDA_MIN;
44    }
45    raw
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51
52    #[test]
53    fn clamps_below_min_to_min() {
54        assert!((clamp_lambda(0.5) - LAMBDA_MIN).abs() < 1e-12);
55    }
56
57    #[test]
58    fn one_passes_through_above_one_falls_back() {
59        assert!((clamp_lambda(1.0) - 1.0).abs() < 1e-12);
60        assert!((clamp_lambda(2.5) - DEFAULT_LAMBDA).abs() < 1e-12);
61    }
62
63    #[test]
64    fn passes_through_in_range() {
65        assert!((clamp_lambda(0.95) - 0.95).abs() < 1e-12);
66    }
67
68    #[test]
69    fn nan_falls_back_to_default() {
70        assert!((clamp_lambda(f64::NAN) - DEFAULT_LAMBDA).abs() < 1e-12);
71    }
72}