yoagent 0.24.0

Simple, effective agent loop with tool execution and event streaming
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
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
# Compaction defaults — offline sweep

Issue [#164](https://github.com/yologdev/yoagent/issues/164) asks for the
compaction constants nobody had measured to be measured instead of chosen, and
[#150](https://github.com/yologdev/yoagent/issues/150) still has one open
question: whether `MIN_HEADROOM_RATIO` causes the post-compaction cliff. This
page reports what an offline sweep of the real compaction code says about each
one.

Each section ends in a verdict: *confirmed*, *change to X* (with the
trade-off quantified), or *inconclusive offline* (with the live experiment
that would settle it). The sweep itself changed no default. Its one *change*
verdict was adopted afterwards: `MIN_HEADROOM_RATIO` is 0.30 since 0.22 (it
was 0.15). The tables below were measured when the floor was 0.15; rows
labelled `[current]` refer to that value.

## Method

```text
cargo run --release --example compaction_sweep            # every axis, ~30 s
cargo run --release --example compaction_sweep -- floor   # one axis
```

`examples/compaction_sweep/` generalises `examples/headroom_sweep.rs` (#158).
Like it, it drives the **real** code: `ContextConfig::effective_target_ratio`,
`compact_messages`, `truncate_tool_output_keyed` and `LlmCompaction`. Nothing is
reimplemented. Each request follows the loop's order:

1. measure growth since the last compaction;
2. resolve the ratio;
3. compact, feeding the result back;
4. account for the request;
5. append the response, capping tool output on the way in, as
   `truncate_tool_output_on_append` does by default.

It is deterministic (seeded PRNG; output byte-identical across runs). Each cell
is 5 seeds × 240 requests.

What it adds over `headroom_sweep`:

- **Realistic message shapes.** `headroom_sweep` grew history with pairs of
  *user* messages, which level 1 (tool-output truncation) and level 2
  (summarize old assistant turns) never touch, so every compaction it measured
  was a level-3 cut. Here turns carry assistant tool calls, parallel tool
  results and user messages, so all three tiers and the orphan guards run.
  This changes the picture materially (see the floor section).
- **Four growth profiles**:
  - `chat`: no tools, ~0.7K tokens/turn.
  - `records`: #150's live shape from `long_horizon.rs`, one 60-line record per
    turn, ~1.6K/turn.
  - `coding`: 1–3 parallel calls over `bash` (3–3000 lines, heavy-tailed),
    `read_file` (40–500 lines; exempt from the cap as shipped) and edits,
    ~3.4K/turn after the cap.
  - `mixed`: alternating 10-turn phases of chat and coding.
- **Two effective budgets**: 26K (#150: 30K configured − 4K reserve) and 96K
  (the crate default, 100K − 4K).
- **Retention measures that matter to the model**:
  - whether the opening task prompt survives (`head-`);
  - whether the latest user message survives (`ask-`);
  - how many turns remain in full detail (`turns`);
  - compactions that leave nothing but the marker (`mkr`);
  - dangling tool calls or results (`orph`).
- **An ideal-prefix-cache proxy** (`hit%`, `cost K`): tokens each request
  shares verbatim with the previous one count as hits.
- **`LlmCompaction` with a gated summarizer.** Each summarization request is
  held until the simulation releases it `L` loop turns later. That makes the
  splice race deterministic, and lets it be measured as "how many turns does a
  briefing get".

Column legend (from the run):

```text
cmp/100: compactions per 100 requests. hit%: ideal prefix-cache hit rate. in K: mean
input per request. cost K: input cost per request in K-token-equivalents at Anthropic
cache rates (write 1.25x, read 0.1x). after%/min%: history left after a compaction,
mean/min, as % of budget. turns: turns still present in full detail. head-/ask-:
compactions that lost the opening task prompt / the latest user message. mkr:
compactions that left only the marker. orph: dangling tool calls/results (must be 0).
```

Current values, read from the code at the start of the run (the issue's table
was accurate):

```text
shipped: MIN_HEADROOM_RATIO=0.15 compact_headroom_turns=Some(30) compact_target_ratio=0.7 keep_recent=10 keep_first=2 tool_output_max_lines=200 trigger_ratio=0.6
```

## First: #150's live log, reproduced offline

#150 recorded two compaction outcomes it could not explain in isolation:
`25 msgs / 19504 tok -> 3 msgs / 1665 tok`, and a collapse to
`1 msgs / 22 tok`. `long_horizon` ran `keep_recent: 6`, and its compactions
fired at ~19.5K message tokens (the calibrated budget). The same shapes,
through the real `compact_messages`:

```text
=== #150 reproduction (budget 19.5K calibrated, keep_recent 6, 40 requests) ===

  records — live: 25 msgs / 19504 tok -> 3 msgs / 1665 tok
  shipped floor 0.15:
    request  13: ratio 0.150   26 msgs /  19627 tok ->   3 msgs /   1680 tok   task prompt kept: false
    request  24: ratio 0.150   26 msgs /  19702 tok ->   3 msgs /   1648 tok   task prompt kept: false
    request  35: ratio 0.150   26 msgs /  19704 tok ->   3 msgs /   1652 tok   task prompt kept: false
  floor 0.35:
    request  13: ratio 0.350   26 msgs /  19627 tok ->  17 msgs /   5139 tok   task prompt kept: true
    request  22: ratio 0.350   36 msgs /  19911 tok ->  27 msgs /   5222 tok   task prompt kept: true
    request  31: ratio 0.350   46 msgs /  19958 tok ->  37 msgs /   5383 tok   task prompt kept: true
    request  40: ratio 0.350   55 msgs /  20122 tok ->  46 msgs /   5447 tok   task prompt kept: true

  coding  — live: 22 msgs / 21371 tok -> 1 msgs / 22 tok
  shipped floor 0.15:
    request   7: ratio 0.150   15 msgs /  23310 tok ->   1 msgs /     22 tok   task prompt kept: false
    request  10: ratio 0.150    8 msgs /  21423 tok ->   1 msgs /     22 tok   task prompt kept: false
    request  17: ratio 0.150   19 msgs /  21222 tok ->   1 msgs /     22 tok   task prompt kept: false
    request  22: ratio 0.150   11 msgs /  21097 tok ->   1 msgs /     22 tok   task prompt kept: false
    request  28: ratio 0.150   16 msgs /  19722 tok ->   3 msgs /   1015 tok   task prompt kept: false
    request  33: ratio 0.150   18 msgs /  27145 tok ->   2 msgs /     37 tok   task prompt kept: false
    request  39: ratio 0.150   17 msgs /  19576 tok ->   2 msgs /     37 tok   task prompt kept: false
  floor 0.35:
    request   7: ratio 0.350   15 msgs /  23310 tok ->   1 msgs /     22 tok   task prompt kept: false
    request  10: ratio 0.350    8 msgs /  21423 tok ->   3 msgs /   4558 tok   task prompt kept: false
    request  17: ratio 0.350   21 msgs /  25758 tok ->   1 msgs /     22 tok   task prompt kept: false
    request  22: ratio 0.350   11 msgs /  21097 tok ->   3 msgs /   4338 tok   task prompt kept: false
    request  27: ratio 0.350   16 msgs /  23045 tok ->   1 msgs /     22 tok   task prompt kept: false
    request  33: ratio 0.350   18 msgs /  27145 tok ->   2 msgs /     37 tok   task prompt kept: false
    request  39: ratio 0.350   17 msgs /  19576 tok ->   4 msgs /   3350 tok   task prompt kept: false
```

Both live figures are reproduced, and the mechanism is now clear:

1. **The floor sets the target**: 0.15 × 19.5K ≈ 2.9K.
2. **Level 3 cannot reach it without cutting into the protected tail.** It keeps
   `keep_first` messages and at least `keep_recent` recent messages. When those
   alone exceed the target, it hands off to `keep_within_budget(result, target)`.
3. **`keep_within_budget` drops the head first**, the task prompt included. It
   then keeps only as many of the newest messages as fit in the target, and the
   orphan guard (`safe_tail_start`) drops any tool results whose assistant
   message did not fit. When the newest turn (assistant plus all its parallel
   results) is bigger than the target, **nothing survives but the 22-token
   marker.** That is the `22 → 1` collapse: the model loses even the tool
   results it just asked for.

The `22 → 1` collapse is therefore not a separate bug in the live path. It
comes from `compact_messages`, under the adapted ratio. #150's isolation probe
used `compact_target_ratio: 0.7`, which is why it did not reproduce there.

## `MIN_HEADROOM_RATIO` (0.15)

```text
=== MIN_HEADROOM_RATIO (floor), headroom Some(30) ===

  -- chat @ 26K --
  value                      cmp/100   hit%    in K  cost K  after%   min%  turns  head-   ask-   mkr  orph
  0.15 [current]                 2.9   94.6    14.8    2.41    22.5   13.0   19.4    20%     0%     0     0
  0.20                           2.9   94.6    14.9    2.43    22.9   19.6   19.3     0%     0%     0     0
  0.25                           2.9   94.6    15.3    2.48    25.2   24.3   19.1     0%     0%     0     0
  0.30                           3.2   94.4    15.8    2.60    29.7   28.6   18.5     0%     0%     0     0
  0.35                           3.3   94.3    16.3    2.70    34.4   28.6   17.7     0%     0%     0     0
  0.40                           3.6   94.1    16.9    2.83    38.8   28.6   17.1     0%     0%     0     0
  0.50                           4.2   93.7    17.8    3.07    47.7   28.6   15.6     0%     0%     0     0
  protect-set (cand.)            2.9   94.6    14.9    2.42    22.7   15.6   19.4     0%     0%     0     0
  headroom None (0.7)            5.9   92.3    19.4    3.66    64.3   28.6   13.5     0%     0%     0     0

  -- records (#150) @ 26K --
  value                      cmp/100   hit%    in K  cost K  after%   min%  turns  head-   ask-   mkr  orph
  0.15 [current]                 6.7   87.4    13.9    3.40    12.8   12.6    8.4   100%    62%     0     0
  0.20                           7.5   87.1    14.6    3.63    19.1   18.8    8.8   100%    67%     0     0
  0.25                           7.5   87.0    14.6    3.63    19.3   18.9    8.8   100%    64%     0     0
  0.30                           7.9   86.8    15.3    3.87    25.5   25.0    9.3   100%    42%     0     0
  0.35                           8.8   86.5    16.8    4.29    34.6   32.2    9.8     0%     0%     0     0
  0.40                           9.2   86.4    17.0    4.37    37.5   32.2    9.5     0%     0%     0     0
  0.50                           9.2   86.8    17.1    4.31    38.3   32.2    9.5     0%     0%     0     0
  protect-set (cand.)            8.8   86.4    16.4    4.21    33.1   32.2    9.8     0%     0%     0     0
  headroom None (0.7)            9.2   86.8    17.1    4.31    38.3   32.2    9.5     0%     0%     0     0

  -- coding (tool-heavy) @ 26K --
  value                      cmp/100   hit%    in K  cost K  after%   min%  turns  head-   ask-   mkr  orph
  0.15 [current]                11.7   77.5    12.2    4.39     2.7    0.1    4.5   100%    95%   102     0
  0.20                          12.3   77.0    12.4    4.54     6.7    0.1    4.6   100%    90%    79     0
  0.25                          12.8   76.9    12.9    4.73    10.3    0.1    4.7   100%    90%    62     0
  0.30                          13.3   77.0    13.5    4.94    15.7    0.1    4.9   100%    82%    43     0
  0.35                          14.0   76.4    13.9    5.16    21.2    0.1    4.9   100%    78%    33     0
  0.40                          15.0   76.4    14.8    5.51    26.7    0.1    5.1   100%    72%    18     0
  0.50                          17.0   75.0    15.6    6.06    36.3    0.1    5.2   100%    71%     8     0
  protect-set (cand.)           26.4   67.3    18.8    8.95    63.3   14.0    5.9    81%    47%     0     0
  headroom None (0.7)           21.6   72.4    18.1    7.55    52.5   14.0    5.7    96%    51%     0     0

  -- mixed @ 26K --
  value                      cmp/100   hit%    in K  cost K  after%   min%  turns  head-   ask-   mkr  orph
  0.15 [current]                 7.0   86.0    12.2    3.20     4.4    0.1    8.1   100%    69%    43     0
  0.20                           7.3   86.2    12.8    3.31     8.6    0.1    8.3   100%    62%    33     0
  0.25                           7.5   85.8    12.6    3.31    11.2    0.1    8.2   100%    64%    31     0
  0.30                           8.1   85.8    13.4    3.52    16.3    0.1    8.4   100%    58%    23     0
  0.35                           8.3   86.0    14.3    3.74    21.8    0.1    8.5    98%    56%    15     0
  0.40                           8.8   85.4    14.7    3.92    26.9    0.1    8.5    96%    50%     8     0
  0.50                           9.9   85.2    15.5    4.17    33.6    0.1    8.5    97%    39%     6     0
  protect-set (cand.)           12.3   82.2    16.6    5.07    49.3   12.1    9.1    44%    23%     0     0
  headroom None (0.7)           11.8   84.3    17.5    4.91    49.7    0.1    8.6    94%    20%     1     0

  -- chat @ 96K --
  value                      cmp/100   hit%    in K  cost K  after%   min%  turns  head-   ask-   mkr  orph
  0.15 [current]                 0.4   98.5    50.7    5.94    24.1   22.8   64.5     0%     0%     0     0
  0.20                           0.4   98.5    50.7    5.94    24.1   22.8   64.5     0%     0%     0     0
  0.25                           0.4   98.5    50.7    5.94    24.1   22.8   64.5     0%     0%     0     0
  0.30                           0.4   98.5    50.7    5.94    24.1   22.8   64.5     0%     0%     0     0
  0.35                           0.4   98.5    50.7    5.94    24.1   22.8   64.5     0%     0%     0     0
  0.40                           0.4   98.5    50.7    5.94    24.1   22.8   64.5     0%     0%     0     0
  0.50                           0.4   98.5    50.7    5.94    24.1   22.8   64.5     0%     0%     0     0
  protect-set (cand.)            0.4   98.5    50.7    5.94    24.1   22.8   64.5     0%     0%     0     0
  headroom None (0.7)            0.4   98.5    50.7    5.94    24.1   22.8   64.5     0%     0%     0     0

  -- records (#150) @ 96K --
  value                      cmp/100   hit%    in K  cost K  after%   min%  turns  head-   ask-   mkr  orph
  0.15 [current]                 1.7   96.4    49.1    6.94    10.5    9.3   29.1     0%     0%     0     0
  0.20                           1.7   96.4    49.1    6.94    10.5    9.3   29.1     0%     0%     0     0
  0.25                           1.7   96.4    49.1    6.94    10.5    9.3   29.1     0%     0%     0     0
  0.30                           1.7   96.4    49.1    6.94    10.5    9.3   29.1     0%     0%     0     0
  0.35                           1.7   96.4    49.1    6.94    10.5    9.3   29.1     0%     0%     0     0
  0.40                           1.7   96.4    49.1    6.94    10.5    9.3   29.1     0%     0%     0     0
  0.50                           1.7   96.4    49.1    6.94    10.5    9.3   29.1     0%     0%     0     0
  protect-set (cand.)            1.7   96.4    49.1    6.94    10.5    9.3   29.1     0%     0%     0     0
  headroom None (0.7)            1.7   96.4    49.1    6.94    10.5    9.3   29.1     0%     0%     0     0

  -- coding (tool-heavy) @ 96K --
  value                      cmp/100   hit%    in K  cost K  after%   min%  turns  head-   ask-   mkr  orph
  0.15 [current]                 3.7   92.9    50.7    9.21    11.1    4.1   15.1    91%    41%     0     0
  0.20                           3.8   92.9    52.3    9.49    15.3    6.0   15.5    58%    27%     0     0
  0.25                           3.8   93.0    54.0    9.77    18.6    9.5   15.4    26%    13%     0     0
  0.30                           3.8   93.0    54.7    9.85    19.7    9.9   15.4     0%     2%     0     0
  0.35                           3.8   93.1    54.8    9.86    19.7    9.9   15.4     0%     4%     0     0
  0.40                           3.9   93.0    54.9    9.90    20.4    9.9   15.3     0%     0%     0     0
  0.50                           3.9   93.0    54.9    9.90    20.4    9.9   15.3     0%     0%     0     0
  protect-set (cand.)            3.8   92.9    54.0    9.79    18.3    6.0   15.5     0%     2%     0     0
  headroom None (0.7)            3.9   93.0    54.9    9.90    20.4    9.9   15.3     0%     0%     0     0

  -- mixed @ 96K --
  value                      cmp/100   hit%    in K  cost K  after%   min%  turns  head-   ask-   mkr  orph
  0.15 [current]                 2.3   95.7    53.3    7.95    23.3    7.4   23.7     0%     0%     0     0
  0.20                           2.3   95.7    53.3    7.95    23.3    7.4   23.7     0%     0%     0     0
  0.25                           2.3   95.7    53.5    7.97    23.5    7.4   23.7     0%     0%     0     0
  0.30                           2.3   95.8    53.5    7.97    23.9    7.4   23.7     0%     0%     0     0
  0.35                           2.3   95.8    53.5    7.97    23.9    7.4   23.7     0%     0%     0     0
  0.40                           2.3   95.8    53.5    7.97    23.9    7.4   23.7     0%     0%     0     0
  0.50                           2.3   95.8    53.6    7.96    24.4    7.4   23.7     0%     0%     0     0
  protect-set (cand.)            2.3   95.7    53.3    7.95    23.3    7.4   23.7     0%     0%     0     0
  headroom None (0.7)            2.3   95.8    53.6    7.96    24.4    7.4   23.7     0%     0%     0     0
```

The candidate rows:

- **0.20–0.50** apply `real_ratio.max(f)`. That is exactly the shipped code
  with a higher constant, for any `f` ≥ 0.15.
- **protect-set** is *not* shipped code. It raises the ratio to whatever the
  `keep_first` head plus the `keep_recent` tail need (+1%). It approximates a
  structural fix: never let the target fall below the protected set.

What the data says:

- **At the default budget, raising the floor is nearly free, and 0.15 is
  actively harmful for coding agents.**

  | coding @ 96K | cmp/100 | hit% | cost K | head- | ask- |
  |---|---|---|---|---|---|
  | 0.15 (current) | 3.7 | 92.9 | 9.21 | 91% | 41% |
  | 0.30 | 3.8 | 93.0 | 9.85 | 0% | 2% |

  - Under 0.15, 91% of compactions leave a session without its task prompt,
    and 41% without the user's latest message.
  - At 0.30 both go to ~0, with the same compaction count and the same ideal
    hit rate.
  - The +7% input cost is simply the extra history now retained (mean
    post-compaction size 11.1% → 19.7% of budget).
  - `chat` and `records` at 96K are unaffected (identical rows for every
    floor). `mixed` moves by at most 1.1 pt of post-compaction size, with the
    same compaction count.
- **At small budgets no fraction is right, because the protected set is sized
  in messages, not budget.**
  - `records` @ 26K loses the task in 100% of compactions up to a floor of
    0.30, and 0% from 0.35: +31% compactions (6.7 → 8.8 per 100), −0.9 pt
    hit rate.
  - `coding` @ 26K still collapses to the marker at 0.50 (8 marker-only
    compactions). The newest turn alone can exceed half the budget.
  - Only the protect-set candidate removes marker-only collapses everywhere. It
    costs 2.3× the compactions for coding @ 26K, because a 10-message tail of
    bulky results is ~60% of a 26K budget.
- **The cliff at 96K is mostly not the floor.** For `records` @ 96K, the
  post-compaction size is 10.5% of budget under *every* floor, and under
  `headroom None` too. With tool-shaped history, level 2 decides the size, not
  the ratio: it summarizes **every** turn older than `keep_recent`, whatever the
  target. The 8–12K "min after" that `headroom_sweep` reported for `Some(30)`
  came from level-3 cuts, which user-only transcripts force. With real
  transcripts, level 2 usually lands below any target first.

**Verdict: change to 0.30** as the constant-only step. It is zero-cost at 96K
(+0.1 compactions per 100, hit rate unchanged) and ends task-prompt loss there
for tool-heavy work.

It does **not** settle small budgets. What the data suggests instead is a code
change: `level3_drop_middle` should fall back to `keep_within_budget(result,
budget)` rather than `(…, target)`, or otherwise keep the head and the newest
complete turn. In other words, overshoot the target rather than discard the
protected set. The target is a headroom *aim*; only the budget is a hard limit.
The protect-set row is the offline estimate of that behaviour.

## `keep_recent` (10) — and its interaction with the floor

```text
=== keep_recent (messages) ===

  -- chat @ 26K --
  value                      cmp/100   hit%    in K  cost K  after%   min%  turns  head-   ask-   mkr  orph
  2                              2.9   94.6    14.9    2.42    22.5   15.4   16.1     0%     0%     0     0
  4                              2.9   94.6    14.9    2.42    22.6   15.3   16.9     0%     0%     0     0
  6                              2.9   94.6    14.9    2.42    22.7   15.3   17.7     0%     0%     0     0
  10 [current]                   2.9   94.6    14.8    2.41    22.5   13.0   19.4    20%     0%     0     0
  16                             2.9   94.6    14.9    2.42    22.1   13.0   21.5    74%     0%     0     0
  24                             2.9   94.6    14.9    2.42    22.0   13.0   22.4   100%     0%     0     0

  -- records (#150) @ 26K --
  value                      cmp/100   hit%    in K  cost K  after%   min%  turns  head-   ask-   mkr  orph
  2                              6.7   87.8    13.9    3.34    12.4    7.3    7.6     0%     0%     0     0
  4                              6.7   87.7    14.4    3.49    14.8   13.5    8.4     0%     0%     0     0
  6                              6.7   87.4    13.9    3.40    12.8   12.6    8.4   100%    62%     0     0
  10 [current]                   6.7   87.4    13.9    3.40    12.8   12.6    8.4   100%    62%     0     0
  16                             6.7   87.4    13.9    3.40    12.8   12.6    8.4   100%    62%     0     0
  24                             6.7   87.4    13.9    3.40    12.8   12.6    8.4   100%    62%     0     0

  -- coding (tool-heavy) @ 26K --
  value                      cmp/100   hit%    in K  cost K  after%   min%  turns  head-   ask-   mkr  orph
  2                             11.6   77.3    12.1    4.37     2.4    0.1    4.3    95%    85%    54     0
  4                             11.7   77.4    12.2    4.37     2.5    0.1    4.4    99%    90%   103     0
  6                             11.7   77.4    12.2    4.38     2.6    0.1    4.5   100%    94%   102     0
  10 [current]                  11.7   77.5    12.2    4.39     2.7    0.1    4.5   100%    95%   102     0
  16                            11.7   77.5    12.2    4.39     2.7    0.1    4.5   100%    95%   102     0
  24                            11.7   77.5    12.2    4.39     2.7    0.1    4.5   100%    95%   102     0

  -- mixed @ 26K --
  value                      cmp/100   hit%    in K  cost K  after%   min%  turns  head-   ask-   mkr  orph
  2                              7.0   86.0    12.2    3.20     4.7    0.1    7.6    81%    58%    19     0
  4                              7.1   86.2    12.4    3.22     4.7    0.1    7.8   100%    59%    42     0
  6                              7.0   86.0    12.3    3.21     4.6    0.1    8.0   100%    65%    42     0
  10 [current]                   7.0   86.0    12.2    3.20     4.4    0.1    8.1   100%    69%    43     0
  16                             7.1   86.2    12.4    3.21     4.4    0.1    8.2   100%    67%    45     0
  24                             7.1   86.2    12.4    3.21     4.4    0.1    8.2   100%    67%    45     0

  -- chat @ 96K --
  value                      cmp/100   hit%    in K  cost K  after%   min%  turns  head-   ask-   mkr  orph
  2                              0.4   98.5    49.8    5.83    21.7   19.9   62.9     0%     0%     0     0
  4                              0.4   98.5    50.0    5.86    22.4   20.7   63.3     0%     0%     0     0
  6                              0.4   98.5    50.3    5.89    23.0   21.4   63.7     0%     0%     0     0
  10 [current]                   0.4   98.5    50.7    5.94    24.1   22.8   64.5     0%     0%     0     0
  16                             0.4   98.5    51.3    6.00    25.7   24.7   65.6     0%     0%     0     0
  24                             0.4   98.5    52.1    6.09    27.8   26.7   67.2     0%     0%     0     0

  -- records (#150) @ 96K --
  value                      cmp/100   hit%    in K  cost K  after%   min%  turns  head-   ask-   mkr  orph
  2                              1.7   96.5    47.2    6.62     3.8    2.6   27.9     0%     0%     0     0
  4                              1.7   96.5    47.6    6.69     5.5    4.3   28.1     0%     0%     0     0
  6                              1.7   96.4    48.0    6.77     7.2    5.9   28.4     0%     0%     0     0
  10 [current]                   1.7   96.4    49.1    6.94    10.5    9.3   29.1     0%     0%     0     0
  16                             1.7   96.4    51.3    7.25    15.6   14.4   30.4     0%     0%     0     0
  24                             1.7   96.5    55.3    7.78    22.3   21.1   32.9     0%     0%     0     0

  -- coding (tool-heavy) @ 96K --
  value                      cmp/100   hit%    in K  cost K  after%   min%  turns  head-   ask-   mkr  orph
  2                              3.6   93.1    49.2    8.83     8.6    0.2   13.8     0%     5%     0     0
  4                              3.6   93.1    49.8    8.93     9.1    0.0   14.3    28%    21%     2     0
  6                              3.6   93.0    50.3    9.09    10.8    2.4   14.8    77%    30%     0     0
  10 [current]                   3.7   92.9    50.7    9.21    11.1    4.1   15.1    91%    41%     0     0
  16                             3.7   93.0    51.4    9.30    11.7    4.1   15.5    98%    48%     0     0
  24                             3.7   93.0    51.3    9.29    11.7    4.1   15.6   100%    52%     0     0

  -- mixed @ 96K --
  value                      cmp/100   hit%    in K  cost K  after%   min%  turns  head-   ask-   mkr  orph
  2                              2.1   96.0    51.6    7.56    17.0    4.9   21.9     0%     0%     0     0
  4                              2.1   95.9    52.2    7.65    17.8    5.6   22.3     0%     0%     0     0
  6                              2.1   95.9    52.0    7.65    18.3    5.3   23.0     0%     4%     0     0
  10 [current]                   2.3   95.7    53.3    7.95    23.3    7.4   23.7     0%     0%     0     0
  16                             2.4   95.6    53.7    8.10    25.2   10.9   25.4    21%     3%     0     0
  24                             2.5   95.5    55.0    8.37    28.8   15.7   26.6    57%     3%     0     0
```

Joint with the floor (0.15 vs 0.30):

```text
  -- coding (tool-heavy) @ 96K --
  value                      cmp/100   hit%    in K  cost K  after%   min%  turns  head-   ask-   mkr  orph
  kr 4 floor 0.15                3.6   93.1    49.8    8.93     9.1    0.0   14.3    28%    21%     2     0
  kr 6 floor 0.15                3.6   93.0    50.3    9.09    10.8    2.4   14.8    77%    30%     0     0
  kr 10 floor 0.15 [current]     3.7   92.9    50.7    9.21    11.1    4.1   15.1    91%    41%     0     0
  kr 16 floor 0.15               3.7   93.0    51.4    9.30    11.7    4.1   15.5    98%    48%     0     0
  kr 4 floor 0.30                3.6   93.2    51.2    9.13    11.3    3.0   14.1     0%     0%     0     0
  kr 6 floor 0.30                3.8   93.1    52.6    9.47    15.9    7.3   14.5     0%     0%     0     0
  kr 10 floor 0.30               3.8   93.0    54.7    9.85    19.7    9.9   15.4     0%     2%     0     0
  kr 16 floor 0.30               4.2   92.6    55.5   10.27    25.0   15.3   16.5    60%    28%     0     0
```

```text
  -- records (#150) @ 26K --
  value                      cmp/100   hit%    in K  cost K  after%   min%  turns  head-   ask-   mkr  orph
  kr 4 floor 0.15                6.7   87.7    14.4    3.49    14.8   13.5    8.4     0%     0%     0     0
  kr 6 floor 0.15                6.7   87.4    13.9    3.40    12.8   12.6    8.4   100%    62%     0     0
  kr 10 floor 0.15 [current]     6.7   87.4    13.9    3.40    12.8   12.6    8.4   100%    62%     0     0
  kr 16 floor 0.15               6.7   87.4    13.9    3.40    12.8   12.6    8.4   100%    62%     0     0
  kr 4 floor 0.30                7.1   88.1    14.8    3.52    19.3   13.5    8.1     0%     0%     0     0
  kr 6 floor 0.30                7.9   87.6    15.6    3.80    25.7   19.7    8.6     0%     0%     0     0
  kr 10 floor 0.30               7.9   86.8    15.3    3.87    25.5   25.0    9.3   100%    42%     0     0
  kr 16 floor 0.30               7.9   86.8    15.3    3.87    25.4   25.0    9.3   100%    42%     0     0
```

What the data says:

- **Orphans: 0 in every cell of every axis** (340 deterministic rows, 5 seeds
  each). That includes parallel tool calls. The orphan guards hold.
- **`keep_recent` barely moves compaction count or cache hit rate** (≤ 0.1
  compactions per 100 at 96K). It trades a little retention (`turns`, e.g.
  28.1 → 30.4 for `records` @ 96K from 4 → 16) against how big the protected
  tail is.
- **Its real effect is through the floor.** The bigger the tail, the more
  often it exceeds `floor × budget` and triggers the head-dropping
  `keep_within_budget` path. For `coding` @ 96K: 0% task loss at
  `keep_recent` 2, then 28% at 4, 77% at 6, 91% at 10, 98% at 16. With the
  floor at 0.30, 10 is safe (0%) and 16 is not (60%).

**Verdict: confirmed, conditionally.** 10 is fine once the floor is 0.30 at the
default budget. With the current floor, lower values are safer, but that fixes
the wrong knob. The underlying issue is that a message count and a budget
fraction are different units: a structural fix (above) makes this constant
independent of the floor.

## `keep_first` (2)

```text
  -- chat @ 26K --
  value                      cmp/100   hit%    in K  cost K  after%   min%  turns  head-   ask-   mkr  orph
  0                              2.9   94.5    14.9    2.42    22.6   13.0   19.4   100%     0%     0     0
  1                              2.9   94.6    14.9    2.42    22.5   13.0   19.4    20%     0%     0     0
  2 [current]                    2.9   94.6    14.8    2.41    22.5   13.0   19.4    20%     0%     0     0
  3                              2.9   94.6    14.9    2.41    22.5   13.0   19.4    20%     0%     0     0
  4                              2.9   94.6    14.9    2.41    22.5   13.0   19.4    20%     0%     0     0
```

```text
  -- coding (tool-heavy) @ 96K --
  value                      cmp/100   hit%    in K  cost K  after%   min%  turns  head-   ask-   mkr  orph
  0                              3.7   92.9    50.7    9.21    11.1    4.1   15.1    93%    41%     0     0
  1                              3.7   92.9    50.7    9.21    11.1    4.1   15.1    91%    41%     0     0
  2 [current]                    3.7   92.9    50.7    9.21    11.1    4.1   15.1    91%    41%     0     0
  3                              3.7   92.9    50.7    9.21    11.1    4.1   15.1    91%    41%     0     0
  4                              3.7   92.9    50.7    9.21    11.1    4.1   15.1    91%    41%     0     0
```

```text
  -- mixed @ 96K --
  value                      cmp/100   hit%    in K  cost K  after%   min%  turns  head-   ask-   mkr  orph
  0                              2.3   95.7    53.3    7.95    23.3    7.4   23.7    29%     0%     0     0
  1                              2.3   95.7    53.3    7.95    23.3    7.4   23.7     0%     0%     0     0
  2 [current]                    2.3   95.7    53.3    7.95    23.3    7.4   23.7     0%     0%     0     0
  3                              2.3   95.7    53.3    7.95    23.3    7.4   23.7     0%     0%     0     0
  4                              2.3   95.7    53.3    7.94    23.3    7.4   23.7     0%     0%     0     0
```

- **Values 1–4 are indistinguishable in every cell.** Level 2 runs before
  level 3 computes the head, so by then message 1 (the first assistant turn) is
  already a one-line `[Summary]`. The first tool output never survives,
  whatever `keep_first` is. `safe_head_end` also pulls a head that ends on a
  tool call back to the prompt.
- **0 is harmful**: the task prompt is lost in 100% of `chat` @ 26K
  compactions and 29% of `mixed` @ 96K.
- **Whether the opening turn survives is decided by the floor, not by
  `keep_first`.** The head is only ever dropped by `keep_within_budget`, which
  ignores `keep_first`.

**Verdict: confirmed.** 2 costs nothing over 1 and guards against 0. "Survives
usefully" means the task prompt plus a one-line summary of the first reply,
never the first tool output.

## `compact_target_ratio` (0.7)

```text
  -- chat @ 26K --
  value                      cmp/100   hit%    in K  cost K  after%   min%  turns  head-   ask-   mkr  orph
  0.5 Some(30)                   2.9   94.6    14.8    2.41    22.5   13.0   19.4    20%     0%     0     0
  0.6 Some(30)                   2.9   94.6    14.8    2.41    22.5   13.0   19.4    20%     0%     0     0
  0.7 Some(30) [current]         2.9   94.6    14.8    2.41    22.5   13.0   19.4    20%     0%     0     0
  0.8 Some(30)                   2.9   94.6    14.8    2.41    22.5   13.0   19.4    20%     0%     0     0
  0.9 Some(30)                   2.9   94.6    14.8    2.41    22.5   13.0   19.4    20%     0%     0     0
  0.5 None                       4.2   93.7    17.8    3.07    47.7   28.6   15.6     0%     0%     0     0
  0.6 None                       4.8   93.3    18.7    3.32    56.1   28.6   14.6     0%     0%     0     0
  0.7 None                       5.9   92.3    19.4    3.66    64.3   28.6   13.5     0%     0%     0     0
  0.8 None                       7.6   90.8    20.0    4.11    72.8   28.6   12.6     0%     0%     0     0
  0.9 None                      11.4   88.9    20.5    4.67    82.6   28.6   11.9     0%     0%     0     0
```

```text
  -- coding (tool-heavy) @ 26K --
  value                      cmp/100   hit%    in K  cost K  after%   min%  turns  head-   ask-   mkr  orph
  0.5 Some(30)                  11.7   77.5    12.2    4.39     2.7    0.1    4.5   100%    95%   102     0
  0.6 Some(30)                  11.7   77.5    12.2    4.39     2.7    0.1    4.5   100%    95%   102     0
  0.7 Some(30) [current]        11.7   77.5    12.2    4.39     2.7    0.1    4.5   100%    95%   102     0
  0.8 Some(30)                  11.7   77.5    12.2    4.39     2.7    0.1    4.5   100%    95%   102     0
  0.9 Some(30)                  11.7   77.5    12.2    4.39     2.7    0.1    4.5   100%    95%   102     0
  0.5 None                      17.0   75.0    15.6    6.06    36.3    0.1    5.2   100%    71%     8     0
  0.6 None                      19.8   73.7    17.4    6.99    46.8    0.1    5.6   100%    59%     1     0
  0.7 None                      21.6   72.4    18.1    7.55    52.5   14.0    5.7    96%    51%     0     0
  0.8 None                      23.7   70.6    18.6    8.13    57.8   14.0    5.9    95%    45%     0     0
  0.9 None                      26.8   67.7    19.0    8.94    63.7   14.2    5.9    88%    40%     0     0
```

- **Under the default `compact_headroom_turns: Some(30)`, the ratio is inert
  in every cell:** rows 0.5–0.9 are identical in all 8 profile × budget
  sections. The headroom policy's derived ratio is always lower, or level 2
  lands below the target anyway.
- **It only matters with `headroom: None`.** There, lower is better on every
  offline measure except raw token retention. For `chat` @ 26K, 0.5 vs 0.7
  gives 4.2 vs 5.9 compactions per 100, 93.7% vs 92.3% hit rate, 15.6 vs 13.5
  turns in detail, and 3.07 vs 3.66 cost per request.

**Verdict: confirmed** (no change warranted). It is a ceiling that the
shipped headroom policy never reaches. A `None` user would do marginally better
at 0.5–0.6, but that is not the default path.

## `tool_output_max_lines` (200)

```text
  -- coding (tool-heavy) @ 96K --
  value                      cmp/100   hit%    in K  cost K  after%   min%  turns  head-   ask-   mkr  orph  g/req trunc% hidden%
  50                             2.8   94.6    50.9    8.27    11.9    0.0   19.6    59%    32%     2     0   2656   23.4    54.1
  100                            3.1   93.8    50.5    8.64    11.7    0.0   17.6    70%    35%     1     0   2954   19.7    48.7
  200 [current]                  3.7   92.9    50.7    9.21    11.1    4.1   15.1    91%    41%     0     0   3416   13.1    40.4
  400                            4.1   92.1    50.8    9.69    10.6    0.0   13.6    98%    71%     3     0   3920    6.9    31.3
  800                            4.6   90.7    47.1    9.75     7.2    0.0   11.7    98%    78%    15     0   4555    4.8    19.8
  off                            5.4   89.4    45.4   10.09     6.4    0.0   10.7    98%    74%    25     0   5654    0.0     0.0
```

```text
  -- coding (tool-heavy) @ 26K --
  value                      cmp/100   hit%    in K  cost K  after%   min%  turns  head-   ask-   mkr  orph  g/req trunc% hidden%
  50                             9.2   81.2    11.8    3.74     3.4    0.1    5.8   100%    85%    70     0   2656   23.4    54.1
  100                           10.2   79.5    11.7    3.94     2.8    0.1    5.1   100%    93%    85     0   2954   19.7    48.7
  200 [current]                 11.7   77.5    12.2    4.39     2.7    0.1    4.5   100%    95%   102     0   3416   13.1    40.4
  400                           13.2   73.3    11.3    4.59     1.8    0.1    3.8   100%    89%   122     0   3920    6.9    31.3
  800                           14.7   71.6    10.8    4.62     1.4    0.1    3.6   100%    89%   141     0   4555    4.8    19.8
  off                           15.6   72.1    10.2    4.28     1.0    0.1    3.5   100%    90%   153     0   5654    0.0     0.0
```

```text
  -- records (#150) @ 26K --
  value                      cmp/100   hit%    in K  cost K  after%   min%  turns  head-   ask-   mkr  orph  g/req trunc% hidden%
  50                             5.4   90.0    13.9    2.99    11.7   10.1   10.6   100%    69%     0     0   1304  100.0    21.1
  100                            6.7   87.4    13.9    3.40    12.8   12.6    8.4   100%    62%     0     0   1646    0.0     0.0
  200 [current]                  6.7   87.4    13.9    3.40    12.8   12.6    8.4   100%    62%     0     0   1646    0.0     0.0
  400                            6.7   87.4    13.9    3.40    12.8   12.6    8.4   100%    62%     0     0   1646    0.0     0.0
  800                            6.7   87.4    13.9    3.40    12.8   12.6    8.4   100%    62%     0     0   1646    0.0     0.0
  off                            6.7   87.4    13.9    3.40    12.8   12.6    8.4   100%    62%     0     0   1646    0.0     0.0
```

- **Every offline metric favours a lower cap.** For `coding` @ 96K, going
  200 → 100 gives:
  - compactions 3.7 → 3.1 per 100;
  - ideal hit rate 92.9% → 93.8%;
  - turns in detail 15.1 → 17.6;
  - cost 9.21 → 8.64.

  Going 200 → 400 or off moves the other way (5.4 compactions per 100 and 10.7
  turns uncapped). Larger single results also push more compactions into the
  floor's head-dropping path: 98% task loss at 400.
- **The price is hidden content.** At 200, 13.1% of coding tool results are
  cut, hiding 40.4% of raw tool-output tokens. At 100 it is 19.7% of results
  and 48.7% of tokens. `records` (60-line outputs) is untouched by any cap of
  100 or more.
- **Retrievability is not the default.** Hidden text is stashed only when a
  `tool_output_sink` is configured (`Agent::with_shared_state`), so by default
  the middle is gone. Where a sink exists, retrievability argues for a
  *lower* cap, not a higher one: hiding becomes cheap. The cost moves to a
  retrieval round trip, plus the full text re-entering context uncapped
  (`shared_state` is exempt).

**Verdict: inconclusive offline; keep 200.** The deciding quantity is how
often the model actually needs a hidden middle, and nothing offline measures
that. The smallest live experiment would run a coding task (like
`long_horizon`'s harness, with bulky `bash` output) with `with_shared_state`
at caps 100 / 200 / 400. Per cap, record:

- `shared_state get` calls on `tool-out-*` keys per truncated result (the
  fetch rate);
- task success;
- session cost.

If the fetch rate at 100 stays low and success holds, lower the cap for
sink-equipped agents first.

## `LlmCompaction::trigger_ratio` (0.6)

Here the real `LlmCompaction` drives the session, and each summarization is
released `L` loop turns after it starts. `win` is the number of turns between a
request starting and the next budget crossing: a briefing splices only if
`L ≤ win`.

```text
  -- records (#150) @ 26K --
  trigger / headroom        L  reqs splice% wasted  win   med  p10 cmp/100  turns
  0.35 Some(30)             1   235    100%      0    2   5.0    2    19.2   10.9
  0.35 Some(30)             2   235    100%      0    2   5.0    2    19.2   10.9
  0.35 Some(30)             3    80     35%     40    2   5.5    2     9.6    8.8
  0.50 Some(30)             1   235    100%      0    3   5.0    3    19.2   11.3
  0.50 Some(30)             2   235    100%      0    3   5.0    3    19.2   11.3
  0.50 Some(30)             3   235    100%      0    3   5.0    3    19.2   11.3
  0.60 Some(30) [current]   1   230    100%      0    5   5.0    5    18.8   11.7
  0.60 Some(30) [current]   2   230    100%      0    5   5.0    5    18.8   11.7
  0.60 Some(30) [current]   3   230    100%      0    5   5.0    5    18.8   11.7
  0.70 Some(30)             1   215    100%      0    4   5.0    4    17.5   11.5
  0.70 Some(30)             2   215    100%      0    4   5.0    4    17.5   11.5
  0.70 Some(30)             3   215    100%      0    4   5.0    4    17.5   11.5
  0.80 Some(30)             1   165    100%      0    3   3.0    3    13.3   10.8
  0.80 Some(30)             2   165    100%      0    3   3.0    3    13.3   10.8
  0.80 Some(30)             3   165    100%      0    3   3.0    3    13.3   10.8
```

```text
  -- coding (tool-heavy) @ 26K --
  trigger / headroom        L  reqs splice% wasted  win   med  p10 cmp/100  turns
  0.35 Some(30)             1   203     85%      0    1   2.0    1    19.9    4.9
  0.35 Some(30)             2   113     48%     32    1   3.0    1    14.2    4.6
  0.35 Some(30)             3    96     33%     43    1   3.0    1    13.2    4.5
  0.50 Some(30)             1   202     84%      0    1   2.0    1    19.9    4.9
  0.50 Some(30)             2   114     49%     31    1   3.0    1    14.2    4.6
  0.50 Some(30)             3    99     32%     46    1   3.0    1    13.4    4.6
  0.60 Some(30) [current]   1   204     83%      0    1   2.0    1    20.1    5.0
  0.60 Some(30) [current]   2   110     44%     34    1   2.0    1    14.2    4.6
  0.60 Some(30) [current]   3    94     29%     47    1   2.0    1    13.4    4.6
  0.70 Some(30)             1   204     83%      0    1   2.0    1    20.2    5.0
  0.70 Some(30)             2   113     42%     38    1   2.0    1    14.6    4.7
  0.70 Some(30)             3    83     20%     50    1   2.0    1    13.1    4.5
  0.80 Some(30)             1   171     73%      0    1   2.0    1    19.2    5.1
  0.80 Some(30)             2    95     32%     40    1   2.0    1    13.9    4.6
  0.80 Some(30)             3    76     13%     55    1   2.0    1    12.8    4.5
```

```text
  -- coding (tool-heavy) @ 96K --
  trigger / headroom        L  reqs splice% wasted  win   med  p10 cmp/100  turns
  0.35 Some(30)             1   105    100%      0    1  12.0    2     8.3   19.7
  0.35 Some(30)             2    97     96%      2    1  11.0    2     7.8   18.8
  0.35 Some(30)             3    83     83%      8    1  10.0    3     7.2   18.2
  0.50 Some(30)             1   104    100%      0    4  10.0    6     8.2   20.7
  0.50 Some(30)             2   104    100%      0    4  10.0    6     8.2   20.7
  0.50 Some(30)             3   104    100%      0    4  10.0    6     8.2   20.7
  0.60 Some(30) [current]   1   102    100%      0    4  10.0    6     8.2   20.9
  0.60 Some(30) [current]   2   102    100%      0    4  10.0    6     8.2   20.9
  0.60 Some(30) [current]   3   102    100%      0    4  10.0    6     8.2   20.9
  0.70 Some(30)             1    85    100%      0    4   8.0    5     6.8   19.8
  0.70 Some(30)             2    85    100%      0    4   8.0    5     6.8   19.8
  0.70 Some(30)             3    85    100%      0    4   8.0    5     6.8   19.8
  0.80 Some(30)             1    70    100%      0    2   5.0    3     5.7   18.5
  0.80 Some(30)             2    70    100%      0    2   5.0    3     5.7   18.5
  0.80 Some(30)             3    66     94%      2    2   5.0    3     5.5   18.2
```

(Rows with `compact_headroom_turns: None` are in the full output. They differ
only where the deterministic fallback runs.)

What the data says:

- **At 96K, 0.6 splices 100% for summarizers up to 3 turns slow**, in every
  profile. 0.35 and 0.8 are the values that start to lose splices (coding:
  83% and 94% at `L = 3`).
- **The trigger barely changes the window where it is short.** For `coding` @
  26K the median window is 2 turns at every trigger, and splice rates for
  `L = 1/2/3` are 83/44/29% at 0.6 against 85/48/33% at 0.35.

  This is the offline explanation of #150's "0.6 and 0.35 behaved
  identically". After a splice, history is still well above the trigger, so
  the next request starts at the splice itself. The window is then
  (budget − post-splice size) / growth, which the trigger cannot move.
- **Post-splice history is large, so `LlmCompaction` compacts more often.**
  The cut is fixed when the request *starts*, so the retained tail includes
  all growth until the splice. At 0.6, `records` @ 26K compacts 18.8 times per
  100 requests against 6.7 for the deterministic path, and `coding` @ 96K 8.2
  against 3.7.
  - This contradicts the module docs' "6 vs 6" rewrites, which
    `prefix_cache_harness` measured on text-only turns; worth re-measuring
    with tool-shaped history.
  - A higher trigger trades the other way: 0.8 cuts compactions to 13.3 and
    5.7 but shrinks the window (median 3 and 5 turns).
- **For #150's shape, the window is not the reason every compaction fell
  back.** At `records` @ 26K the minimum window at 0.6 is 5 turns, and
  splicing is 100% for `L ≤ 3`. #150's live 100% fallback rate is better
  explained by the two causes already fixed in 0.18.1: the retry underflow
  panic and a slow loop-model summarizer.

**Verdict: confirmed.** 0.6 is at or next to the best splice rate and retention
in every cell. What decides whether briefings land is summarizer latency against
growth rate, not the trigger. **Inconclusive offline:** `L` in wall-clock terms.
The smallest live experiment is `long_horizon` with a fast summarizer (Haiku)
and a slow one (the loop model), timing summarization requests against loop
turns.

## #150: conclusion

- **Does the `MIN_HEADROOM_RATIO` floor cause the cliff described?** Yes, for
  what #150 actually observed: the `3 msgs / 1665 tok` and `1 msg / 22 tok`
  results and the lost task (`LOST`). Both are reproduced offline at the floor,
  through `keep_within_budget(…, target)`, which drops the head and, when the
  newest turn exceeds the target, everything except the marker. At the default
  96K budget, though, the post-compaction *size* in tool sessions is mostly set
  by level 2, not the floor.
- **What the data suggests instead:**
  - Raise the floor to 0.30. It is free at 96K and ends task loss there for
    tool-heavy work.
  - More durably, never let the headroom target discard the protected set:
    fall back against the budget, not the target. That is the only variant
    that removes marker-only collapses at small budgets.
  - Separately, level 2's all-or-nothing summarization is the remaining cliff
    at 96K. It could summarize the oldest turns only until the target is met,
    mirroring level 3's smallest-span rule.
- **Does this settle #150?** It settles the *mechanism* and the "not
  explained" `22 → 1` collapse, and it answers the floor question offline.
  #150's own acceptance criterion, **prefix-cache hit rate on a real provider**
  under the current vs a higher floor, still needs a live run. The ideal-cache
  proxy puts the difference at ≤ 0.1 pt at 96K, and −0.9 pt for `records` @
  26K at the 0.35 floor it needs. That is small enough that it would not
  justify keeping the current floor. The live check:

  ```text
  YO_HEADROOM=30 ANTHROPIC_API_KEY=... cargo run --example long_horizon
  ```

  run once as shipped and once with the floor at 0.30. That needs the constant
  changed on a branch, since it is not configurable. Compare the cache-read
  totals and the final recall answer.

## Caveats

- **Ideal cache.** `hit%` counts every verbatim-shared prefix token as a hit.
  Real providers cache at breakpoints, in blocks, with TTLs. It ranks
  configurations; it does not predict a live rate.
- **Fixed calibrated budget.** The loop re-derives the budget from provider
  usage every turn; the sweep holds it fixed (`system_prompt_tokens: 0`).
- **Synthetic content.** Sizes follow the stated distributions, and token
  counts use the crate's own chars/4 estimate. What the model would *do* with
  less history (re-fetch, re-ask, fail) is not modelled. `head-` and `ask-` say
  what it no longer sees, not what that costs.
- **Task loss is sticky.** Once the prompt is dropped it never returns, so one
  early loss counts against every later compaction.
- **Latency in turns.** Summarizer latency is a whole number of loop turns.
  Real latency is wall-clock and varies.
- **Candidates are candidates.** "protect-set" approximates a code change; it is
  not an implementation of one.

Numbers from commit `cdbb3df`, run with the command above.