Skip to main content

lean_ctx/core/config/
retired.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Retired configuration keys (2026-10-16 settings audit).
3//!
4//! Each key here was accepted by the config but never applied, or lost its
5//! effect. An older `config.toml` that still sets one keeps loading (serde
6//! ignores unknown fields). The automatic config migration removes them from a
7//! writable global config (`Config::migrate_v4_config_document`), and
8//! `lean-ctx doctor` names them instead of calling them unknown.
9
10/// One retired key: its dotted path and what replaced it, if anything.
11pub struct RetiredKey {
12    pub path: &'static str,
13    pub note: &'static str,
14}
15
16const fn retired(path: &'static str, note: &'static str) -> RetiredKey {
17    RetiredKey { path, note }
18}
19
20/// Removed configuration keys. A path names a key or a whole table.
21pub const RETIRED_KEYS: &[RetiredKey] = &[
22    retired(
23        "session_token_limit",
24        "never enforced; per-turn limits are `turn_fresh_limit`",
25    ),
26    retired(
27        "allow_auto_reroot",
28        "re-rooting from an unsuitable jail root is always on",
29    ),
30    retired("shadow.auto_record", "never read"),
31    retired(
32        "shadow.baseline_model",
33        "reports compare each task with the model its host chose",
34    ),
35    retired(
36        "agents.identity_ttl_hours",
37        "the reaper uses `presence_ttl_hours` and `logical_session_ttl_seconds`",
38    ),
39    retired(
40        "agents.max_concurrent_workers",
41        "registration never applied it",
42    ),
43    retired(
44        "agents.max_concurrent_mutating_workers",
45        "registration never applied it",
46    ),
47    retired("context.diet_max_config_tokens", "never read"),
48    retired("context.diet_relevance_threshold", "never read"),
49    retired("context.diet_rebalance_on_change", "never read"),
50    retired("context.diet_staleness_enabled", "never read"),
51    retired(
52        "cloud",
53        "the hosted account and its sync left the engine; accounts live in LeanCTX | Hive",
54    ),
55    retired(
56        "cognitive_mode",
57        "the science features had no measured retrieval gain at +74 % read output",
58    ),
59    retired("code_health.coupling", "coupling is always computed"),
60    retired(
61        "graph.traversal_edges",
62        "learned co-access ranking changed no retrieval result",
63    ),
64    // Read-path autonomy drivers (A/B 2026-10-17): byte-identical responses
65    // and equal retrieval quality without them, 2-14 s more at session start.
66    retired(
67        "autonomy.auto_preload",
68        "the session-start preload changed no response",
69    ),
70    retired(
71        "autonomy.silent_preload",
72        "the import preload changed no response",
73    ),
74    retired(
75        "autonomy.auto_related",
76        "the related-file hint changed no response",
77    ),
78    retired(
79        "autonomy.auto_dedup",
80        "the automatic cache dedup changed no response",
81    ),
82    retired(
83        "autonomy.dedup_threshold",
84        "the automatic cache dedup changed no response",
85    ),
86    // Profile-only `[autonomy]` keys of the same drivers.
87    retired(
88        "autonomy.auto_prefetch",
89        "the post-read prefetch changed no response",
90    ),
91    retired(
92        "autonomy.prefetch_max_files",
93        "the post-read prefetch changed no response",
94    ),
95    retired(
96        "autonomy.prefetch_budget_tokens",
97        "the post-read prefetch changed no response",
98    ),
99    retired("boundary_policy.audit_cross_access", "never read"),
100    retired(
101        "gateway_server.admin_bind_host",
102        "the admin listener left the engine",
103    ),
104    retired(
105        "gateway_server.usage_retention_days",
106        "the admin listener left the engine",
107    ),
108    retired("solution.commercial", "never read"),
109    retired("team_url", "the team savings roll-up left the engine"),
110    retired("team_token", "the team savings roll-up left the engine"),
111    retired("team_auto_push", "the team savings roll-up left the engine"),
112    retired(
113        "gain",
114        "hosted card publication and public rankings left the engine",
115    ),
116    retired("response_shaping.preamble", "`mode` selects the patterns"),
117    retired(
118        "response_shaping.code_repetition",
119        "`mode` selects the patterns",
120    ),
121    retired("response_shaping.narration", "`mode` selects the patterns"),
122    retired(
123        "response_shaping.confirmation",
124        "`mode` selects the patterns",
125    ),
126    retired(
127        "proxy.reasoning_budget",
128        "reasoning effort is the host's choice or `proxy.effort`",
129    ),
130    retired(
131        "proxy.pipeline.enable_effort",
132        "reasoning effort is the host's choice or `proxy.effort`",
133    ),
134    retired(
135        "conversation.summarize_score_range",
136        "summaries cover the scores between `drop_score_below` and `min_score_to_preserve`",
137    ),
138    retired(
139        "conversation.ccr_store_dropped",
140        "dropped messages never had a recoverable copy",
141    ),
142    retired(
143        "cache.l1_max_entries",
144        "the process tier is bounded by `l1_ttl_secs`",
145    ),
146    retired(
147        "cache.l3_gc_threshold",
148        "the disk tier is bounded by `l3_max_bytes`",
149    ),
150    retired("memory.gotcha.retrieval_budget_per_room", "never applied"),
151    retired(
152        "memory.gotcha.default_decay_rate",
153        "decay follows each gotcha's provenance",
154    ),
155    retired(
156        "memory.gotcha.category_decay_overrides",
157        "decay follows each gotcha's provenance",
158    ),
159    retired("memory.gotcha.auto_expire_days", "never applied"),
160];
161
162/// Moved table: `[ocla.delivery.cache]` settings now live in `[cache]`.
163const MOVED_CACHE_FROM: [&str; 3] = ["ocla", "delivery", "cache"];
164
165/// The retired key that `path` (dotted) is, or lies under.
166#[must_use]
167pub fn retired_key(path: &str) -> Option<&'static RetiredKey> {
168    if path.split('.').take(3).eq(MOVED_CACHE_FROM) {
169        return Some(&MOVED_CACHE);
170    }
171    RETIRED_KEYS.iter().find(|key| {
172        path == key.path
173            || path
174                .strip_prefix(key.path)
175                .is_some_and(|rest| rest.starts_with('.'))
176    })
177}
178
179static MOVED_CACHE: RetiredKey = retired("ocla.delivery.cache", "moved to `[cache]`");
180
181/// Removes retired keys from a config document (root and every
182/// `[profiles.<name>]` overlay) and moves `[ocla.delivery.cache]` into
183/// `[cache]` without overriding a value already set there. Returns `None`
184/// when nothing changed; the comments and formatting of every other key are
185/// kept (a comment directly above a removed key goes with it).
186pub(super) fn migrate_retired_document(raw: &str) -> Result<Option<String>, String> {
187    let mut document = raw
188        .parse::<toml_edit::DocumentMut>()
189        .map_err(|error| error.to_string())?;
190    let mut changed = migrate_table(document.as_table_mut());
191    if let Some(profiles) = document
192        .get_mut("profiles")
193        .and_then(toml_edit::Item::as_table_like_mut)
194    {
195        for (_, profile) in profiles.iter_mut() {
196            if let Some(table) = profile.as_table_mut() {
197                changed |= migrate_table(table);
198            }
199        }
200    }
201    Ok(changed.then(|| document.to_string()))
202}
203
204fn migrate_table(root: &mut toml_edit::Table) -> bool {
205    let mut changed = move_delivery_cache(root);
206    changed |= super::renamed::migrate_table(root);
207    for key in RETIRED_KEYS {
208        let segments: Vec<&str> = key.path.split('.').collect();
209        changed |= remove_path(root, &segments);
210    }
211    changed
212}
213
214fn move_delivery_cache(root: &mut toml_edit::Table) -> bool {
215    let [ocla, delivery, cache] = MOVED_CACHE_FROM;
216    let Some(moved) = root
217        .get_mut(ocla)
218        .and_then(toml_edit::Item::as_table_like_mut)
219        .and_then(|table| table.get_mut(delivery))
220        .and_then(toml_edit::Item::as_table_like_mut)
221        .and_then(|table| table.remove(cache))
222    else {
223        return false;
224    };
225    if let Some(values) = moved.as_table_like() {
226        let target = root
227            .entry(cache)
228            .or_insert_with(toml_edit::table)
229            .as_table_like_mut();
230        if let Some(target) = target {
231            for (name, value) in values.iter() {
232                if target.get(name).is_none() {
233                    target.insert(name, value.clone());
234                }
235            }
236        }
237    }
238    prune_empty(root, &[ocla, delivery]);
239    true
240}
241
242/// Removes `segments` under `table`; drops tables the removal left empty.
243fn remove_path(table: &mut dyn toml_edit::TableLike, segments: &[&str]) -> bool {
244    match segments {
245        [] => false,
246        [leaf] => table.remove(leaf).is_some(),
247        [head, rest @ ..] => {
248            let Some(child) = table
249                .get_mut(head)
250                .and_then(toml_edit::Item::as_table_like_mut)
251            else {
252                return false;
253            };
254            let removed = remove_path(child, rest);
255            if removed && child.is_empty() {
256                table.remove(head);
257            }
258            removed
259        }
260    }
261}
262
263fn prune_empty(table: &mut dyn toml_edit::TableLike, segments: &[&str]) {
264    let Some((head, rest)) = segments.split_first() else {
265        return;
266    };
267    let Some(child) = table
268        .get_mut(head)
269        .and_then(toml_edit::Item::as_table_like_mut)
270    else {
271        return;
272    };
273    prune_empty(child, rest);
274    if child.is_empty() {
275        table.remove(head);
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    #[test]
284    fn retired_keys_are_removed_and_the_delivery_cache_moves_to_cache() {
285        let raw = r#"session_token_limit = 1
286# my settings
287ultra_compact_note = "kept"
288
289[shadow]
290enabled = true
291auto_record = false
292baseline_model = "gpt-4o"
293
294[cache]
295l2_ttl_secs = 60
296
297[ocla.delivery]
298max_entries = 7
299
300[ocla.delivery.cache]
301l2_ttl_secs = 900
302l2_max_entries = 50
303l1_max_entries = 3
304
305[solution.commercial]
306fingerprints_enabled = true
307
308[solution.commercial.adaptive]
309enabled = true
310
311[memory.gotcha]
312max_gotchas_per_project = 40
313default_decay_rate = 0.1
314
315[memory.gotcha.category_decay_overrides]
316security = 0.005
317
318[profiles.ci.conversation]
319ccr_store_dropped = false
320"#;
321        let migrated = migrate_retired_document(raw)
322            .expect("parses")
323            .expect("changes");
324        let config: crate::core::config::Config =
325            toml::from_str(&migrated).expect("migrated config loads");
326
327        assert!(migrated.starts_with("# my settings"), "{migrated}");
328        assert!(migrated.contains("ultra_compact_note"));
329        for gone in [
330            "session_token_limit",
331            "auto_record",
332            "baseline_model",
333            "l1_max_entries",
334            "commercial",
335            "default_decay_rate",
336            "category_decay_overrides",
337            "ccr_store_dropped",
338            "[ocla.delivery.cache]",
339        ] {
340            assert!(!migrated.contains(gone), "{gone} survived:\n{migrated}");
341        }
342        assert!(config.shadow.enabled);
343        assert_eq!(config.ocla.delivery.max_entries, 7);
344        // A value already set in `[cache]` wins over the moved copy.
345        assert_eq!(config.cache.l2_ttl_secs, 60);
346        assert_eq!(config.cache.l2_max_entries, 50);
347        assert_eq!(config.memory.gotcha.max_gotchas_per_project, 40);
348
349        assert_eq!(migrate_retired_document(&migrated).expect("parses"), None);
350    }
351
352    #[test]
353    fn lookup_names_retired_keys_and_everything_under_them() {
354        assert!(retired_key("solution.commercial.adaptive.enabled").is_some());
355        assert!(retired_key("ocla.delivery.cache.l2_ttl_secs").is_some());
356        assert!(retired_key("shadow.auto_record").is_some());
357        assert!(retired_key("gain.auto_publish").is_some());
358        assert!(retired_key("shadow.enabled").is_none());
359        assert!(retired_key("cache.l2_ttl_secs").is_none());
360        assert!(retired_key("session_token_limit_x").is_none());
361    }
362}