1pub 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
20pub 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 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 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
162const MOVED_CACHE_FROM: [&str; 3] = ["ocla", "delivery", "cache"];
164
165#[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
181pub(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
242fn 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 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}