lean-ctx 3.9.15

Context Runtime for AI Agents with CCP. 71 MCP tools, 10 read modes, 95+ compression patterns, cross-session memory (CCP), persistent AI knowledge with temporal facts + contradiction detection, multi-agent context sharing, LITM-aware positioning, AAAK compact format, adaptive compression with Thompson Sampling bandits. Supports 24+ AI tools. Reduces LLM token consumption by up to 99%.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
//! Advanced config sections (proxy, memory subsystems, custom_aliases, setup, llm).
//! Split out of `schema/mod.rs`; `use super::*` re-imports helpers + `SectionSchema`.

#[allow(clippy::wildcard_imports)]
use super::*;
use std::collections::BTreeMap;

pub(super) fn build(sections: &mut BTreeMap<String, SectionSchema>) {
    let cfg = crate::core::config::Config::default();
    let mut proxy = BTreeMap::new();
    proxy.insert(
        "anthropic_upstream".into(),
        key(
            "string?",
            serde_json::json!(cfg.proxy.anthropic_upstream),
            "Custom upstream URL for Anthropic API proxy",
        ),
    );
    proxy.insert(
        "openai_upstream".into(),
        key(
            "string?",
            serde_json::json!(cfg.proxy.openai_upstream),
            "Custom upstream URL for OpenAI API proxy",
        ),
    );
    proxy.insert(
        "chatgpt_upstream".into(),
        key(
            "string?",
            serde_json::json!(cfg.proxy.chatgpt_upstream),
            "Custom upstream URL for ChatGPT/Codex subscription API proxy",
        ),
    );
    proxy.insert(
        "gemini_upstream".into(),
        key(
            "string?",
            serde_json::json!(cfg.proxy.gemini_upstream),
            "Custom upstream URL for Gemini API proxy",
        ),
    );
    proxy.insert(
        "history_mode".into(),
        key_enum_with_env(
            &["cache-aware", "rolling", "off"],
            "cache-aware",
            "History pruning strategy. cache-aware: frozen boundaries that keep provider prompt caches valid (default). rolling: legacy moving window (max raw savings, breaks prompt caching). off: never prune",
            "LEAN_CTX_PROXY_HISTORY_MODE",
        ),
    );
    proxy.insert(
        "allow_insecure_http_upstream".into(),
        key_with_env(
            "bool",
            serde_json::json!(cfg.proxy.allow_insecure_http_upstream.unwrap_or(false)),
            "Allow a non-loopback plaintext http:// upstream (trusted local network only, e.g. http://host.docker.internal:2455 in front of codex-lb). Opt-in; default false",
            "LEAN_CTX_ALLOW_INSECURE_HTTP_UPSTREAM",
        ),
    );
    proxy.insert(
        "allow_custom_upstream".into(),
        key_with_env(
            "bool",
            serde_json::json!(cfg.proxy.allow_custom_upstream.unwrap_or(false)),
            "Allow a custom (non-allowlisted) HTTPS upstream host, e.g. a corporate gateway in front of the provider API. Opt-in; default false. Unlike the env var, this config flag reaches the managed (service-spawned) proxy started by `proxy enable`/`restart` (#590)",
            "LEAN_CTX_ALLOW_CUSTOM_UPSTREAM",
        ),
    );
    proxy.insert(
        "meter_openai_usage".into(),
        key(
            "bool",
            serde_json::json!(cfg.proxy.meters_openai_usage()),
            "Inject stream_options.include_usage into streamed OpenAI Chat Completions so the final chunk reports real token usage for the measured spend meter. Default true",
        ),
    );
    proxy.insert(
        "cost_response_header".into(),
        key(
            "string",
            serde_json::json!(cfg.proxy.cost_response_header()),
            "Extra response header carrying the upstream gateway's billed USD for the turn (e.g. a corporate gateway's cost header). LiteLLM's x-litellm-response-cost is always recognized. Measured header costs beat table estimates; body-reported costs (OpenRouter usage.cost) beat headers",
        ),
    );
    proxy.insert(
        "cold_prefix_repack".into(),
        key_with_env(
            "bool",
            serde_json::json!(cfg.proxy.repacks_cold_prefix()),
            "Opt-in big-gap cold-prefix repack (#480): on a session-resume request the proxy may predict (from idle time vs the provider cache TTL) that the client-cached prefix has already expired, then prune that now-cold prefix to re-seed a leaner cache and keep applying the same deterministic compression on later turns so warm follow-ups hit it (sticky; baselines persist across restarts, #499). A wrong guess re-bills cache reads as writes (~12x), so default false",
            "LEAN_CTX_PROXY_COLD_PREFIX_REPACK",
        ),
    );
    proxy.insert(
        "live_compress".into(),
        key_with_env(
            "bool",
            serde_json::json!(cfg.proxy.live_compresses()),
            "Live-compress non-protected tool_result content on the wire (#481). Default true. Set false for a meter-only proxy — real billed/cache token metering with zero request rewriting (combine with history_mode = \"off\" and no role_aggressiveness for a byte-unchanged body)",
            "LEAN_CTX_PROXY_LIVE_COMPRESS",
        ),
    );
    proxy.insert(
        "live_compress_exclude".into(),
        key(
            "string[]",
            serde_json::json!(cfg.proxy.live_compress_exclude_patterns()),
            "Tool-name patterns (case-insensitive substring) whose tool_result is never live-compressed — treated as protected, like a file read (#481). Unset protects Serena's code-reading tools; set an explicit list to narrow it, or [] to disable",
        ),
    );
    proxy.insert(
        "compress_protect".into(),
        key(
            "string[]",
            serde_json::json!(cfg.proxy.compress_protect.clone().unwrap_or_default()),
            "File-path globs whose reads are never compressed (#1150): a matching path is returned verbatim (full) by the read tools, for files where exact bytes matter more than token savings (golden snapshots, byte-asserted fixtures, security-sensitive configs). Globs (*/**/?) match the path and its file name, so *.snap, **/golden/**, tests/fixtures/* all work. Empty (default) protects nothing — the lossless crushers and beneficial gate already keep compression safe; this is an explicit escape hatch",
        ),
    );
    proxy.insert(
        "ccr_inband".into(),
        key_with_env(
            "bool",
            serde_json::json!(cfg.proxy.ccr_inband_enabled()),
            "Opt-in in-band CCR retrieval for a remote proxy with no shared filesystem (#493). When on, a lossy stub advertises a compact <lc_expand:HASH> marker instead of a local tee path; when the model echoes that marker, the proxy splices the verbatim original (from its local tee store) back inline next turn — one turn of latency, no MCP/filesystem on the agent host. The splice is a strict no-op on marker-less turns, so it never perturbs the provider cache prefix unless the model asked to expand. Default false",
            "LEAN_CTX_PROXY_CCR_INBAND",
        ),
    );
    proxy.insert(
        "counterfactual_metering".into(),
        key_with_env(
            "bool",
            serde_json::json!(cfg.proxy.counterfactual_metering_enabled()),
            "Opt-in counterfactual savings metering (#701): each rewritten Anthropic /v1/messages request fires a free count_tokens probe with the original, uncompressed body, concurrently with the real forward. The provider-counted answer is paired with the same response's billed usage — provider-authoritative savings receipts ('would have cost N, billed M') instead of local tokenizer estimates, shown as verified_savings on /status. The probe never mutates or delays the forwarded request; failures degrade to the estimate. Default false (one extra free HTTP call per compressed request)",
            "LEAN_CTX_PROXY_COUNTERFACTUAL",
        ),
    );
    proxy.insert(
        "cache_breakpoint".into(),
        key_with_env(
            "bool",
            serde_json::json!(cfg.proxy.cache_breakpoint_enabled()),
            "Opt-in active prompt-cache breakpoint injection for Anthropic (#939). When on and the client set no cache_control of its own, the proxy adds one cache_control: {type:ephemeral} marker to the system field so an otherwise-uncached, stable system prompt bills later turns at the cached rate (the win a raw API client leaves on the table). Anthropic-only: OpenAI/Gemini cache prefixes automatically and ignore the marker, so those paths stay byte-unchanged. Deterministic, never adds a second breakpoint, and skipped below Anthropic's minimum cacheable size. Default false",
            "LEAN_CTX_PROXY_CACHE_BREAKPOINT",
        ),
    );
    proxy.insert(
        "cache_aligner".into(),
        key_with_env(
            "bool",
            serde_json::json!(cfg.proxy.cache_aligner_enabled()),
            "Cache-aligner volatile-field telemetry (#940), on by default. The proxy scans each unanchored Anthropic system prompt for volatile, cache-busting fields (ISO dates/datetimes, UUIDs, git SHAs) and reports how many it found on /status cache_safety (volatile_system_requests, volatile_fields_detected) - purely to quantify how much prompt-cache the client leaks. Measurement only: the request body is never mutated, so it is strictly cache-safe, which is why it ships on for every proxy (#986 premium defaults). The deterministic scan is the precursor to the opt-in tail-relocate below. Set false to opt out of the per-request scan. Default true",
            "LEAN_CTX_PROXY_CACHE_ALIGNER",
        ),
    );
    proxy.insert(
        "cache_align_relocate".into(),
        key_with_env(
            "bool",
            serde_json::json!(cfg.proxy.cache_align_relocate_enabled()),
            "Opt-in active cache-aligner relocate (#974). When on, the proxy rewrites an unanchored Anthropic system prompt into a stable block (volatile values - ISO dates/datetimes, UUIDs, git SHAs - replaced by constant placeholders) carrying the cache_control breakpoint, plus an uncached trailing block that re-states the relocated values. The cacheable prefix then stays byte-stable turn-to-turn and finally caches; only the small tail is reprocessed. Anthropic-only, Treatment-arm, gated on a client that anchored nothing and on Anthropic's minimum cacheable size. Deterministic (#498) and idempotent. The cache_aligner telemetry is the precursor that quantifies the saving. Default false",
            "LEAN_CTX_PROXY_CACHE_ALIGN_RELOCATE",
        ),
    );
    proxy.insert(
        "cache_policy".into(),
        key_with_env(
            "bool",
            serde_json::json!(cfg.proxy.cache_policy_enabled()),
            "Cache-economics (#986), on by default. Enables prompt-cache miss attribution telemetry (per turn, classify the outcome as cold start / warm reuse / TTL lapse / prefix change and report cumulative gauges on /status cache_attribution) plus a net-cost gate on the cold-prefix repack that skips re-seeding prefixes too small to be cached (below Anthropic's ~1024-token minimum). The telemetry never mutates the body and the gate only makes repacking more conservative, so it can never bust a cache that would otherwise have been kept - both halves are strictly safe, so every proxy gets them out of the box (#986 premium defaults). Set false to opt out (drops the /status attribution gauges and the per-request prefix hash). Default true",
            "LEAN_CTX_PROXY_CACHE_POLICY",
        ),
    );
    proxy.insert(
        "effort".into(),
        key_enum_with_env(
            &["off", "minimal", "low", "medium", "high"],
            "off",
            "Cache-safe cross-provider reasoning-effort control (#834). off (default) = no-op. minimal|low|medium|high pins the model's reasoning depth across providers: lean-ctx translates it to OpenAI reasoning_effort / reasoning.effort, Anthropic output_config.effort, and Gemini thinkingConfig (thinkingLevel on 3.x, thinkingBudget on 2.5 pro/flash), only on models that accept it and only when the client didn't set its own value. The level is a constant, so it never breaks the provider prompt cache (unlike per-turn effort routing). Anthropic is dialed only when the client already requested adaptive thinking",
            "LEAN_CTX_PROXY_EFFORT",
        ),
    );
    proxy.insert(
        "prose_ranker".into(),
        key_enum_with_env(
            &["auto", "extractive", "truncate"],
            "auto",
            "How the proxy squeezes prose it must shrink (#895). auto (default) and extractive use embedding-based extractive ranking — keeping the most central sentences instead of just the prefix — when the local embedding engine is available, else fall back to truncation; truncate keeps the original deterministic FIFO squeeze and never loads the engine. Wire rewrites are memoized per content so the engine's cold→warm transition never changes an already-emitted frozen-region rewrite (cache-safe, #448/#498)",
            "LEAN_CTX_PROXY_PROSE_RANKER",
        ),
    );
    proxy.insert(
        "output_holdout".into(),
        key_with_env(
            "f64",
            serde_json::json!(cfg.proxy.output_holdout_fraction()),
            "Fraction 0.0-1.0 of conversations placed in the output-savings control arm (#895). 0 (default) = no holdout (every conversation is output-shaped). When > 0, a deterministic cohort = blake3(system + first user message) puts ~this fraction in a control arm that skips output-shaping (effort control + verbosity steer) but is still metered, yielding an honest measured output-token reduction (lean-ctx output-savings). The cohort is a pure function of conversation identity, so a conversation keeps one arm across all turns - cache-safe",
            "LEAN_CTX_PROXY_OUTPUT_HOLDOUT",
        ),
    );
    proxy.insert(
        "verbosity_steer".into(),
        key_with_env(
            "bool",
            serde_json::json!(cfg.proxy.verbosity_steer_enabled()),
            "Opt-in cache-safe wire verbosity steer (#895). When true, the proxy appends a single constant 'be concise' instruction to the last user turn of each request - output-shaping for raw API clients that do not load lean-ctx rules. The suffix is constant and appended strictly after the last cache_control breakpoint (a new trailing text block, never modifying a cache-anchored block), so the provider prompt-cache prefix stays byte-stable. Under an output_holdout the control arm skips it so its effect is measured. Default false",
            "LEAN_CTX_PROXY_VERBOSITY_STEER",
        ),
    );
    proxy.insert(
        "codex_chatgpt_proxy".into(),
        key_with_env(
            "bool",
            serde_json::json!(cfg.proxy.codex_chatgpt_proxy_enabled()),
            "Opt-in routing of a Codex ChatGPT-subscription login through the proxy for model-turn compression (#603/#616). Default false leaves Codex native (history visible, cloud/remote intact, no #597). When true, setup pins model_provider = leanctx-chatgpt + chatgpt_base_url + a [model_providers.leanctx-chatgpt] block, so model turns route through /backend-api/codex/responses (the proxy strips the responses-lite marker so every model incl. gpt-5.5 works); pinning a provider scopes Codex history to it (#597), hence opt-in. Toggle durably with `lean-ctx proxy codex-chatgpt on|off|status`. Default false",
            "LEAN_CTX_CODEX_CHATGPT_PROXY",
        ),
    );
    sections.insert(
        "proxy".into(),
        SectionSchema {
            description: "Proxy upstream configuration for API routing".into(),
            keys: proxy,
        },
    );

    let mut role_aggr = BTreeMap::new();
    role_aggr.insert(
        "system".into(),
        key_with_env(
            "f64",
            serde_json::json!(cfg.proxy.role_aggressiveness.system),
            "Opt-in prose compression intensity (0.0–1.0) for system prompts in the proxy's frozen request region. Unset = leave untouched. Higher = more aggressive. Cache-safe (deterministic, never touches the client-cached prefix)",
            "LEAN_CTX_PROXY_SYSTEM_AGGR",
        ),
    );
    role_aggr.insert(
        "user".into(),
        key_with_env(
            "f64",
            serde_json::json!(cfg.proxy.role_aggressiveness.user),
            "Opt-in prose compression intensity (0.0–1.0) for free-text user turns (never tool results) in the proxy's frozen request region. Unset = leave untouched",
            "LEAN_CTX_PROXY_USER_AGGR",
        ),
    );
    sections.insert(
        "proxy.role_aggressiveness".into(),
        SectionSchema {
            description: "Opt-in per-role prose compression for the proxy's frozen request region (#710). Assistant turns are always passed through verbatim".into(),
            keys: role_aggr,
        },
    );

    let mut cost = BTreeMap::new();
    cost.insert(
        "default_model".into(),
        key(
            "string?",
            serde_json::json!(cfg.cost.default_model),
            "Fallback pricing model for MCP-only IDEs whose real model lean-ctx cannot observe (Cursor, Copilot, Windsurf, …). Unset → blended heuristic. Per-IDE overrides live in [cost.models]",
        ),
    );
    cost.insert(
        "prices".into(),
        key(
            "table?",
            serde_json::json!(cfg.cost.prices.keys().collect::<Vec<_>>()),
            "Operator price overrides per model, USD per million tokens: [cost.prices.\"<model>\"] with input_per_m / output_per_m / cache_write_per_m / cache_read_per_m. For negotiated enterprise rates (committed-use discounts, Azure PTU, zero-rated internal models); overrides embedded and live catalog rows, only a provider-measured bill beats it",
        ),
    );
    sections.insert(
        "cost".into(),
        SectionSchema {
            description: "Model declaration for measured-vs-estimated cost reporting".into(),
            keys: cost,
        },
    );

    let mem = &cfg.memory;
    let mut mem_knowledge = BTreeMap::new();
    mem_knowledge.insert(
        "max_facts".into(),
        key(
            "usize",
            serde_json::json!(mem.knowledge.max_facts),
            "Maximum number of knowledge facts stored per project",
        ),
    );
    mem_knowledge.insert(
        "max_patterns".into(),
        key(
            "usize",
            serde_json::json!(mem.knowledge.max_patterns),
            "Maximum number of patterns stored",
        ),
    );
    mem_knowledge.insert(
        "max_history".into(),
        key(
            "usize",
            serde_json::json!(mem.knowledge.max_history),
            "Maximum history entries retained",
        ),
    );
    mem_knowledge.insert(
        "contradiction_threshold".into(),
        key(
            "f32",
            clean_f32(mem.knowledge.contradiction_threshold),
            "Confidence threshold for contradiction detection",
        ),
    );
    mem_knowledge.insert(
        "recall_facts_limit".into(),
        key(
            "usize",
            serde_json::json!(mem.knowledge.recall_facts_limit),
            "Maximum facts returned per recall query",
        ),
    );
    mem_knowledge.insert(
        "rooms_limit".into(),
        key(
            "usize",
            serde_json::json!(mem.knowledge.rooms_limit),
            "Maximum number of rooms returned",
        ),
    );
    mem_knowledge.insert(
        "timeline_limit".into(),
        key(
            "usize",
            serde_json::json!(mem.knowledge.timeline_limit),
            "Maximum number of timeline entries returned",
        ),
    );
    mem_knowledge.insert(
        "relations_limit".into(),
        key(
            "usize",
            serde_json::json!(mem.knowledge.relations_limit),
            "Maximum number of relations returned",
        ),
    );
    sections.insert(
        "memory.knowledge".into(),
        SectionSchema {
            description: "Knowledge memory budgets (facts, patterns, gotchas)".into(),
            keys: mem_knowledge,
        },
    );

    let mut mem_episodic = BTreeMap::new();
    mem_episodic.insert(
        "max_episodes".into(),
        key(
            "usize",
            serde_json::json!(mem.episodic.max_episodes),
            "Maximum number of episodes retained",
        ),
    );
    mem_episodic.insert(
        "max_actions_per_episode".into(),
        key(
            "usize",
            serde_json::json!(mem.episodic.max_actions_per_episode),
            "Maximum actions tracked per episode",
        ),
    );
    mem_episodic.insert(
        "summary_max_chars".into(),
        key(
            "usize",
            serde_json::json!(mem.episodic.summary_max_chars),
            "Maximum characters in episode summary",
        ),
    );
    sections.insert(
        "memory.episodic".into(),
        SectionSchema {
            description: "Episodic memory budgets (session episodes)".into(),
            keys: mem_episodic,
        },
    );

    let mut mem_procedural = BTreeMap::new();
    mem_procedural.insert(
        "max_procedures".into(),
        key(
            "usize",
            serde_json::json!(mem.procedural.max_procedures),
            "Maximum number of learned procedures stored",
        ),
    );
    mem_procedural.insert(
        "min_repetitions".into(),
        key(
            "usize",
            serde_json::json!(mem.procedural.min_repetitions),
            "Minimum repetitions before a pattern is stored",
        ),
    );
    mem_procedural.insert(
        "min_sequence_len".into(),
        key(
            "usize",
            serde_json::json!(mem.procedural.min_sequence_len),
            "Minimum sequence length for procedure detection",
        ),
    );
    mem_procedural.insert(
        "max_window_size".into(),
        key(
            "usize",
            serde_json::json!(mem.procedural.max_window_size),
            "Maximum window size for pattern analysis",
        ),
    );
    sections.insert(
        "memory.procedural".into(),
        SectionSchema {
            description: "Procedural memory budgets (learned patterns)".into(),
            keys: mem_procedural,
        },
    );

    let mut mem_lifecycle = BTreeMap::new();
    mem_lifecycle.insert(
        "decay_rate".into(),
        key(
            "f32",
            clean_f32(mem.lifecycle.decay_rate),
            "Rate at which knowledge confidence decays over time",
        ),
    );
    mem_lifecycle.insert(
        "low_confidence_threshold".into(),
        key(
            "f32",
            clean_f32(mem.lifecycle.low_confidence_threshold),
            "Threshold below which facts are considered low-confidence",
        ),
    );
    mem_lifecycle.insert(
        "stale_days".into(),
        key(
            "i64",
            serde_json::json!(mem.lifecycle.stale_days),
            "Days after which unused facts are considered stale",
        ),
    );
    mem_lifecycle.insert(
        "similarity_threshold".into(),
        key(
            "f32",
            clean_f32(mem.lifecycle.similarity_threshold),
            "Similarity threshold for deduplication",
        ),
    );
    mem_lifecycle.insert(
        "forgetting_model".into(),
        key(
            "string",
            serde_json::json!(mem.lifecycle.forgetting_model),
            "Forgetting curve: ebbinghaus (default, exponential + spacing) or linear",
        ),
    );
    mem_lifecycle.insert(
        "base_stability_days".into(),
        key(
            "f32",
            clean_f32(mem.lifecycle.base_stability_days),
            "Characteristic memory stability (days) for the Ebbinghaus curve",
        ),
    );
    mem_lifecycle.insert(
        "archetype_aware_decay".into(),
        key(
            "bool",
            serde_json::json!(mem.lifecycle.archetype_aware_decay),
            "Scale Ebbinghaus stability by fact archetype so structural evidence decays slower than inference (default false)",
        ),
    );
    mem_lifecycle.insert(
        "reclaim_headroom_pct".into(),
        key_with_env(
            "f32",
            clean_f32(mem.lifecycle.reclaim_headroom_pct),
            "Proactive headroom on a capacity reclaim: settle a full store at 1 - this fraction (0.25 = 75%) instead of churning at the cap. Lossless — the reclaimed tail is archived and restorable",
            "LEAN_CTX_LIFECYCLE_RECLAIM_HEADROOM_PCT",
        ),
    );
    mem_lifecycle.insert(
        "reclaim_enabled".into(),
        key_with_env(
            "bool",
            serde_json::json!(mem.lifecycle.reclaim_enabled),
            "Master switch for the proactive capacity reclaim (#995). false trims only the overflow (escape hatch, no headroom); eviction stays lossless either way",
            "LEAN_CTX_LIFECYCLE_RECLAIM_ENABLED",
        ),
    );
    sections.insert(
        "memory.lifecycle".into(),
        SectionSchema {
            description: "Knowledge lifecycle policy (decay, staleness, dedup)".into(),
            keys: mem_lifecycle,
        },
    );

    let mut mem_gotcha = BTreeMap::new();
    mem_gotcha.insert(
        "max_gotchas_per_project".into(),
        key(
            "usize",
            serde_json::json!(mem.gotcha.max_gotchas_per_project),
            "Maximum gotchas stored per project",
        ),
    );
    mem_gotcha.insert(
        "retrieval_budget_per_room".into(),
        key(
            "usize",
            serde_json::json!(mem.gotcha.retrieval_budget_per_room),
            "Maximum gotchas retrieved per room per query",
        ),
    );
    mem_gotcha.insert(
        "default_decay_rate".into(),
        key(
            "f32",
            clean_f32(mem.gotcha.default_decay_rate),
            "Default decay rate for gotcha importance",
        ),
    );
    sections.insert(
        "memory.gotcha".into(),
        SectionSchema {
            description: "Gotcha memory settings (project-specific warnings and pitfalls)".into(),
            keys: mem_gotcha,
        },
    );

    let mut mem_embeddings = BTreeMap::new();
    mem_embeddings.insert(
        "max_facts".into(),
        key(
            "usize",
            serde_json::json!(mem.embeddings.max_facts),
            "Maximum number of embedding facts stored",
        ),
    );
    sections.insert(
        "memory.embeddings".into(),
        SectionSchema {
            description: "Embeddings memory settings for semantic search".into(),
            keys: mem_embeddings,
        },
    );

    let mut aliases = BTreeMap::new();
    aliases.insert(
        "command".into(),
        key(
            "string",
            serde_json::json!(""),
            "The command pattern to match (e.g. 'deploy')",
        ),
    );
    aliases.insert(
        "alias".into(),
        key(
            "string",
            serde_json::json!(""),
            "The alias definition to execute",
        ),
    );
    sections.insert("custom_aliases".into(), SectionSchema {
            description: "Custom command aliases (array of {command, alias} entries). Note: field names are 'command' and 'alias' (not 'name')".into(),
            keys: aliases,
        });

    if let Some(root_section) = sections.get_mut("root") {
        root_section.keys.insert(
            "custom_aliases".into(),
            key(
                "array",
                serde_json::json!([]),
                "Custom command aliases (array of {command, alias} entries)",
            ),
        );
    }

    let mut setup_keys = BTreeMap::new();
    setup_keys.insert(
            "auto_inject_rules".into(),
            key(
                "bool?",
                serde_json::json!(null),
                "Inject agent rule files during setup/update. null=auto (inject if already present), true=always, false=never",
            ),
        );
    setup_keys.insert(
            "auto_inject_skills".into(),
            key(
                "bool?",
                serde_json::json!(null),
                "Install SKILL.md files during setup/update. null=auto (install if rules present), true=always, false=never",
            ),
        );
    setup_keys.insert(
        "auto_update_mcp".into(),
        key(
            "bool",
            serde_json::json!(true),
            "Register lean-ctx MCP server in editor configs during setup/update",
        ),
    );
    sections.insert(
            "setup".into(),
            SectionSchema {
                description: "Controls what lean-ctx injects during setup and updates. Fresh installs default to non-invasive (rules/skills off, MCP on).".into(),
                keys: setup_keys,
            },
        );

    let mut llm_keys = BTreeMap::new();
    llm_keys.insert(
        "enabled".into(),
        key(
            "bool",
            serde_json::json!(false),
            "Enable optional LLM enhancements (query expansion, contradiction explanation)",
        ),
    );
    llm_keys.insert(
        "backend".into(),
        key_enum(
            &["ollama", "openrouter", "anthropic"],
            "ollama",
            "LLM backend provider",
        ),
    );
    llm_keys.insert(
        "model".into(),
        key(
            "string",
            serde_json::json!("llama3.2"),
            "Model name for the selected backend",
        ),
    );
    llm_keys.insert(
        "api_key".into(),
        key(
            "string",
            serde_json::json!(""),
            "API key for OpenRouter or Anthropic backends",
        ),
    );
    llm_keys.insert(
        "timeout_secs".into(),
        key(
            "u64",
            serde_json::json!(10),
            "HTTP timeout for LLM requests",
        ),
    );
    sections.insert("llm".into(), SectionSchema {
            description: "Optional LLM enhancement settings (query expansion, contradiction explanation). Deterministic fallback when disabled or unreachable.".into(),
            keys: llm_keys,
        });
}