Repository navigation
Expand file tree
/
Copy pathcontext.go
More file actions
1040 lines (984 loc) · 43.5 KB
/
Copy pathcontext.go
File metadata and controls
1040 lines (984 loc) · 43.5 KB
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
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package exec
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
"github.com/Agent-Field/agentfield/sdk/go/ai"
"github.com/Agent-Field/codeaf/internal/ctxbudget"
)
// How many bytes of raw tool output the transcript may carry before older
// results start fading. Everything past the window is still referenced, just
// not quoted.
//
// The four sizes below, and the window computed from them, are ctxbudget's.
// They are aliased here rather than restated because the planner asks the same
// question this loop answers — how much material one worker holds at a time —
// before any worker exists, and a number two packages both need is a number
// that drifts unless exactly one of them owns it. The reasoning behind each
// figure is written down where it is owned; only the names are local.
const (
observationBudget = ctxbudget.ObservationFloorBytes
minObservationBudget = ctxbudget.MinObservationBytes
defaultObservationBudget = ctxbudget.DefaultObservationBytes
observationBytesPerToken = ctxbudget.BytesPerToken
observationFixedFloorTokens = ctxbudget.ObservationFixedFloorTokens
)
// observationWindow is ctxbudget.ObservationBytes under the name the rest of
// this package has always called it. The derivation moved; nothing about it
// changed.
//
// What the deleted local ceiling was worth naming: it stood at 64KB and it was
// a cost decision wearing a memory decision's clothes: the window is re-sent
// every turn and billed against the leaf's spend ceiling, so a big window on a
// small grant was measured crossing the wrap-up threshold before the leaf had
// done any work. What replaced it is not that literal coming back — the pot the
// fill law is taken of is now min(window, working set), which is a named
// setting with a stated default rather than a number this file believed about
// every model in existence. On a 1M-context model the fill law alone handed a
// leaf a 2.2MB observation window against 181KB of tool output across TWELVE
// nodes: the decayer fired zero times, nothing ever left any transcript, and
// 90% of every input token billed was material the model had already been
// shown.
func observationWindow(contextTokens int) int {
return ctxbudget.ObservationBytes(contextTokens)
}
// observationSafetyWindow is the last-resort bound for material that has not
// been used yet. The ordinary observation window is intentionally a working-set
// target: it limits repeated billing, not what the provider can accept. Treating
// that soft target as a hard memory limit made a gather-before-write task throw
// away its earliest reads before it had a chance to use them, then spend the
// remaining turns reading its own spill files.
//
// A known model gets the fill law applied to its real context window without
// the working-set clamp. Unknown models have no defensible larger bound, so the
// ordinary named fallback remains their safety bound.
func observationSafetyWindow(contextTokens int) int {
budget := ctxbudget.For(contextTokens).WithFloor(observationFixedFloorTokens)
if !budget.Known() {
return observationWindow(contextTokens)
}
if window := budget.Bytes(); window > minObservationBudget {
return window
}
return minObservationBudget
}
// decayLowWaterPercent is where a firing decay pass stops before the loop has
// measured anything, as a percentage of the budget it fired at.
//
// Trimming to exactly the budget every turn looks frugal and is the most
// expensive thing the loop does. Every call rides a prefix cache: the provider
// bills the cached rate for the longest prefix that is byte-identical to the
// previous call and full price for everything after the first changed byte.
// Stubbing one old result rewrites a message near the front of the transcript,
// so the whole tail behind it — every assistant turn, every surviving result —
// is re-billed cold. In steady state the leaf adds N bytes of output per turn
// and must retire N bytes, so a trim-to-budget pass fires on *every* turn and
// the transcript is never cached past the fade line.
//
// Hysteresis converts that into one rewrite per K turns. On crossing the budget
// the pass retires in a single batch down to a low-water mark, leaving headroom
// behind it; the next several turns fit inside that headroom and touch nothing,
// so their prefixes are byte-identical and hit warm. One batch of stubs every K
// turns costs one invalidation where per-turn trimming costs K.
//
// The price is window size: on average the transcript carries less raw output
// than a trim-to-budget pass would leave. Nothing is lost either way — every
// stub still points at the spill file holding its bytes — so the whole question
// is what K comes out at, and that is where a fixed fraction went wrong. K is
// headroom divided by inflow, and the fraction only ever set the numerator. At
// the 25KB window this constant was tuned against, a quarter was 6.25KB of
// headroom against a measured 3.7KB of observation bytes per turn: K was 1.7,
// the batch bought less than two quiet turns, and hysteresis was paying its
// price in window size while delivering almost none of its benefit.
//
// So the mark is computed from the denominator instead, and this constant is
// only what stands in before the loop has enough turns to have measured it.
const decayLowWaterPercent = 75
// decayHeadroomTurns is the K the mark is solved for: how many turns of
// measured inflow one firing pass must buy before the next one. Seven sits in
// the middle of the six-to-eight range where the invalidation is amortized well
// enough that the remaining cost is noise, and going higher only trades window
// for a saving that is no longer there.
const decayHeadroomTurns = 7
// minInflowSamples is how many turns must have been measured before their mean
// is trusted over the fixed fraction. Three is enough to tell a leaf reading
// whole files from one running short checks, and few enough that the measured
// mark governs almost the entire run.
const minInflowSamples = 3
// decayHeadroomFloorPercent and decayHeadroomCeilingPercent bound the computed
// mark, and both bounds are about measurement rather than policy.
//
// A leaf whose turns each add more than the whole window would solve for a
// negative mark and retire everything, turning one batch into a full reset; the
// floor stops the pass giving up more than half the window in a single fire,
// which is the deepest the old fixed-fraction reasoning ever contemplated. At
// the other end a leaf whose first turns happen to be near-silent would solve
// for a mark so close to the budget that a single ordinary result crosses it
// again immediately — the every-turn rewrite the hysteresis exists to prevent,
// arriving through the measurement instead of through the arithmetic.
const (
decayHeadroomFloorPercent = 50
decayHeadroomCeilingPercent = 90
)
// inflowMeter is the running mean of how many bytes of raw tool output one turn
// adds to the transcript. The loop already sees every result on its way into
// the messages, so this is a counter rather than a measurement pass.
type inflowMeter struct {
turns int
bytes int
}
func (m *inflowMeter) observe(bytes int) {
if bytes < 0 {
return
}
m.turns++
m.bytes += bytes
}
// mean reports the measured per-turn inflow, and whether there is enough of it
// to act on.
func (m *inflowMeter) mean() (int, bool) {
if m.turns < minInflowSamples {
return 0, false
}
return m.bytes / m.turns, true
}
// spillFunc preserves a decaying observation's full body in the workspace and
// returns the workspace-relative path it went to. ok=false means the bytes
// could not be written and the caller must not claim a path.
type spillFunc func(toolCallID, body string) (path string, ok bool)
// foldKeepAssistantTurns is how many of the newest assistant messages the fold
// pass may never touch, and it is the invariant the original "never touched"
// rule was really protecting.
//
// The rule was written as "assistant messages are compressed state", and that
// reasoning is sound about the RECENT ones and only about those. The model's
// last few turns are its live working memory — the plan it is executing, the
// conclusion it just reached, the draft that becomes the deliverable when the
// loop ends — and folding any of them would be deleting the thing the next turn
// is written against. Twenty turns back, the same message is a paragraph about a
// file that has since been rewritten, re-billed on every remaining turn: it is
// spent, exactly as a tool result is spent, and the measured evidence is that
// assistant output was 45% of context growth while the decayer could not reach
// a byte of it.
//
// Three keeps the current thought and the two it was built on. It is deliberately
// smaller than the eight-to-sixteen turns a well-sized leaf runs, because a fold
// that kept most of the run would reclaim nothing, and deliberately more than
// one, because a model that has just been told its own last message is a stub
// has been made to distrust its own memory.
//
// The deliverable is covered by this same keep and needs no separate rule: the
// leaf's contract is that the final message IS the deliverable (see
// systemPrompt), the fold runs at the top of a turn before that message exists,
// and lastAssistantText — what land() hands over when a leaf is stopped early —
// reads the newest assistant text there is, which is never foldable.
const foldKeepAssistantTurns = 3
// decayer shrinks old tool results in place, losslessly, and — only when that
// is not enough — folds aged assistant text the same way.
//
// The asymmetry is the whole idea, and it is an ORDER rather than an exemption.
// A tool result is *spent raw material*: by the time three more turns have
// happened its value has usually already been extracted into the reasoning above
// it, and all it does is get re-billed on every remaining turn, so it retires
// first and it retires deep. An assistant message is *compressed state* — the
// model already read the raw output and wrote down what mattered — so it is
// worth many times its bytes and it is touched last, never while stubbing tool
// output would do, and never within foldKeepAssistantTurns of the live edge.
//
// What is not true, and was assumed for a long time, is that compressed state is
// worth its bytes forever. See fold.
//
// Newest results are kept in full until the budget runs out, then everything
// older collapses to a single line naming what it was and where its bytes
// went: before a result is stubbed, its full body is written to the
// workspace's observation directory, so the stub is a pointer rather than a
// tombstone — the agent can re-read the file with sh if it turns out to
// matter. Budgeting by bytes rather than by count means a turn full of small
// results all survive, while one huge one retires early — which is the right
// trade, because the huge one is what costs.
//
// Fading happens in batches, not continuously. See decayLowWaterPercent: a pass
// that fires clears down to the low-water mark so that most turns can leave the
// transcript untouched, because an untouched transcript is a cached one.
//
// A tool message is only ever shortened, never removed. Its ToolCallID pairs
// with the assistant turn that requested it, and an unpaired tool_call is a
// hard provider error rather than a degraded prompt.
type decayer struct {
labels map[string]string // tool_call_id → what the call was
spill spillFunc // writes a body to the workspace; nil when there is no filesystem
// spilled remembers every tool_call_id already stubbed, and the path its
// bytes went to ("" when no file could be written). It is both the
// write-once guard and the already-stubbed detector: decay runs over the
// same transcript every turn, and without it the same result would be
// re-spilled and re-stubbed each time.
spilled map[string]string
// preserved is the set of results whose bytes are already on disk before
// any decay pass has looked at them, and the path each one went to. Only
// the pointer mechanism puts anything here — see observations.preserve —
// and it is deliberately not the same map as spilled, which doubles as the
// already-stubbed detector: a result recorded there would never be stubbed
// at all.
//
// It carries an invariant the pointer mechanism depends on absolutely. A
// pointer written into the transcript is never revisited, so its target
// must still be readable at the end of the run as well as at the moment it
// was written. Preserving first and stubbing later is what makes that true
// regardless of what happens on the way: the retire pass below reads a
// preserved path instead of writing a fresh spill, so a disk that has
// filled up or a directory that has gone away between the two can no longer
// turn a live target into a pathless tombstone with pointers aimed at it.
preserved map[string]string
// consumedFrom is how far into the transcript this leaf has demonstrably
// acted. Everything below the mark was already in front of the model at the
// moment it last changed the workspace, so its value has been extracted;
// everything at or above it is material the model has read and not yet used.
// An index is identity enough because the transcript is append-only — the
// same property the "#index" fallback key below already relies on.
consumedFrom int
// consumedKeys is the same fact for the results an index cannot speak for:
// a result whose bytes were quoted again later, where the second copy is a
// pointer naming a durable path that outlives the stub. See observations.
consumedKeys map[string]bool
// inflow is what this leaf actually adds per turn, which is what decides
// how deep a firing pass has to reach. See decayHeadroomTurns.
inflow inflowMeter
}
func newDecayer(labels map[string]string, spill spillFunc) *decayer {
return &decayer{labels: labels, spill: spill, spilled: map[string]string{},
preserved: map[string]string{}, consumedKeys: map[string]bool{}}
}
// actedPast records that everything the transcript held below index was in
// front of the model when it last changed the workspace. The loop calls it with
// the workspace's monotonic mutation revision, the same world-backed signal the
// no-progress guard reads. A revision rather than a distinct-path count matters:
// editing the same file again still consumes what the model read before it.
func (d *decayer) actedPast(index int) {
if index > d.consumedFrom {
d.consumedFrom = index
}
}
// used records that one result's bytes have been asked for a second time and
// answered from somewhere durable, which spends the copy in the transcript.
func (d *decayer) used(key string) { d.consumedKeys[key] = true }
// retired reports whether a result has already been shortened to a stub — so
// its bytes are no longer quoted where the model can read them — and the
// address they went to, which is "" when no file could be written.
func (d *decayer) retired(key string) (string, bool) {
path, stubbed := d.spilled[key]
return path, stubbed
}
// inherit hands a durable address that already exists to a second key holding
// exactly those bytes, so the retire pass reuses the file rather than writing a
// second copy of material that is already on disk.
func (d *decayer) inherit(key, path string) {
if key != "" && path != "" {
d.preserved[key] = path
}
}
// preserve writes a result's bytes to the workspace ahead of any decay, and
// reports the path they can be read back from. It is idempotent by key: the
// same result is written once however many times it repeats.
//
// ok=false means these bytes cannot be given a durable address, and the caller
// must then treat them as unpointable rather than claiming a path that does not
// exist.
func (d *decayer) preserve(key, body string) (string, bool) {
if path := d.preserved[key]; path != "" {
return path, true
}
// Decay may already have written exactly these bytes under exactly this
// key, in which case the address exists and writing it again would only
// cost a syscall to produce the same file.
if path := d.spilled[key]; path != "" {
d.preserved[key] = path
return path, true
}
if d.spill == nil {
return "", false
}
path, ok := d.spill(key, body)
if !ok {
// Not recorded as a failure: a full disk now may not be a full disk in
// three turns, and the only cost of asking again is one write attempt
// on a body that has already repeated once.
return "", false
}
d.preserved[key] = path
return path, true
}
// observe records one turn's worth of new raw tool output. The loop calls it
// once per turn, with the bytes it is about to append.
func (d *decayer) observe(bytes int) { d.inflow.observe(bytes) }
// decay stubs raw tool output once the live window outgrows the budget, and
// does nothing at all until then. It returns how many results were newly
// stubbed.
//
// The check is the cheap half and the point of the whole design: most turns
// answer "still under budget" by summing lengths, mutate nothing, and leave the
// prompt prefix byte-identical to the previous turn. When the window does cross
// the line, one pass retires in bulk to the low-water mark rather than shaving
// off exactly the overflow, buying several quiet turns before the next rewrite.
func (d *decayer) decay(messages []ai.Message, budget int) int {
return d.decayWithin(messages, budget, budget)
}
// decayWithin distinguishes the cost target from the provider-safety limit.
// Crossing the working-set target may retire material the leaf has acted on;
// it may not evict still-needed observations merely to save tokens. Only the
// larger safety limit permits that last-resort degradation.
//
// A soft pass is also all-or-nothing with respect to its low-water target. A
// handful of spent bytes that cannot buy the intended headroom are left alone,
// because changing them would invalidate the provider prefix for no useful
// reduction and is exactly the repeated tiny-fold pathology this mechanism is
// meant to prevent.
func (d *decayer) decayWithin(messages []ai.Message, workingBudget, safetyBudget int) int {
live := liveObservationBytes(messages)
if live <= workingBudget {
return 0
}
workingTarget := d.lowWater(workingBudget)
if d.canRetireTo(messages, live, workingTarget, retireSpent) {
return d.retireTo(messages, live, workingTarget, retireSpent)
}
if safetyBudget > workingBudget && live <= safetyBudget {
return 0
}
retired := d.retireTo(messages, live, d.lowWater(safetyBudget),
retireSpent, retireNeeded)
live = liveObservationBytes(messages)
if live > safetyBudget {
retired += d.retireTo(messages, live, safetyBudget, retireFresh)
}
return retired
}
// lowWater is where this leaf's firing pass stops: far enough below the budget
// that decayHeadroomTurns of its own measured inflow fit in the gap.
//
// Solving for the gap rather than declaring it is the whole of the fix. The
// same fraction is a batch that buys eight quiet turns for a leaf running short
// checks and a batch that buys one for a leaf reading files, and the leaf is the
// only thing that knows which it is.
func (d *decayer) lowWater(budget int) int {
inflow, measured := d.inflow.mean()
if !measured || inflow <= 0 {
return decayLowWater(budget)
}
mark := budget - decayHeadroomTurns*inflow
if floor := budget * decayHeadroomFloorPercent / 100; mark < floor {
mark = floor
}
if ceiling := budget * decayHeadroomCeilingPercent / 100; mark > ceiling {
mark = ceiling
}
return mark
}
// decayLowWater is the mark before anything has been measured, and the shape
// every mark must keep: at or below the budget, so the post-decay invariant the
// loop depends on — raw live bytes never exceed the budget once decay has run —
// holds unchanged.
func decayLowWater(budget int) int {
mark := budget * decayLowWaterPercent / 100
if mark < 0 {
mark = 0
}
return mark
}
// liveObservationBytes is the raw cost of the transcript's tool output as it
// currently stands: already-stubbed results count as their stub, because that
// is what the provider is actually billed for.
func liveObservationBytes(messages []ai.Message) int {
total := 0
for _, message := range messages {
if message.Role != "tool" {
continue
}
for _, part := range message.Content {
total += len(part.Text)
}
}
return total
}
// retireOrder classifies observations by how urgently their verbatim bytes
// must remain in the prompt.
type retireOrder int
const (
// retireFresh is the newest assistant turn's own results: raw material the
// model has not read yet, measured against the full budget.
retireFresh retireOrder = iota
// retireNeeded is history the leaf has not been shown to have acted on.
retireNeeded
// retireSpent is history whose value has already been extracted.
retireSpent
)
// Retirement chooses what goes by how long ago each result was last needed.
//
// THE LAW: AN OBSERVATION IS RETIRED IN ORDER OF HOW LONG AGO IT WAS LAST
// NEEDED, NOT HOW LONG AGO IT ARRIVED. A leaf that must read more material than
// the window holds before it can act on any of it was losing the earliest reads
// first — the ones it had not used yet — and spent the rest of its budget
// getting the same material back. Age is the wrong question: what a result has
// cost is the same whenever it arrived, and what it is still worth is whether
// the model has acted past it.
//
// The ordinary working-set pass considers spent results only, oldest first,
// and does not run unless those results can buy the intended headroom. A
// separate pass at the model-derived provider-safety limit may continue into
// still-needed history. The live turn's own results are last of all and are
// shortened only as far as the hard limit requires. That makes "never at a
// cost threshold; last at a provider threshold" the enforceable rule.
//
// The most recent turn's results are measured against the full safety budget
// rather than its low-water mark. Decay runs *before* a turn, so those bytes
// are raw material the model has not read yet; the low-water mark governs how
// deep into history the emergency batch reaches, never whether the current
// turn gets to see its own output.
// canRetireTo is the no-rewrite preflight for a soft pass. Stubs cannot save
// more than their whole bodies, so failing this cheap upper-bound test proves a
// pass cannot reach its target. The exact path-bearing stub length is handled by
// retireTo if the pass is viable.
func (d *decayer) canRetireTo(messages []ai.Message, live, target int, orders ...retireOrder) bool {
if live <= target {
return true
}
wanted := map[retireOrder]bool{}
for _, order := range orders {
wanted[order] = true
}
boundary := newestAssistant(messages)
available := 0
for index := range messages {
if messages[index].Role != "tool" {
continue
}
key := observationKey(messages[index], index)
if !wanted[d.orderOf(key, index, boundary)] {
continue
}
if _, done := d.retired(key); done {
continue
}
body := contentOf(messages[index])
stub := fmt.Sprintf("[%s — %d bytes, superseded]",
labelFor(d.labels, messages[index].ToolCallID), len(body))
if len(stub) < len(body) {
available += len(body)
}
}
if live-available > target {
return false
}
// The cheap check above proved the raw bodies are large enough. Now price
// the exact path-bearing pointers in retirement order. This may preserve
// bodies to disk, but it still leaves the prompt byte-identical if their
// real stubs cannot reach the target.
available = 0
for _, order := range orders {
for index := range messages {
if messages[index].Role != "tool" {
continue
}
key := observationKey(messages[index], index)
if d.orderOf(key, index, boundary) != order {
continue
}
if _, done := d.retired(key); done {
continue
}
body := contentOf(messages[index])
stub, _, ok := d.supersession(key,
labelFor(d.labels, messages[index].ToolCallID), body)
if !ok {
continue
}
available += len(body) - len(stub)
if live-available <= target {
return true
}
}
}
return false
}
// retireTo walks least-valuable results first: spent, then still-needed, then
// the newest unread results. Inside each class the oldest goes first. The last
// two classes are supplied only by the provider-safety pass.
func (d *decayer) retireTo(messages []ai.Message, live, target int, orders ...retireOrder) int {
decayed := 0
boundary := newestAssistant(messages)
for _, order := range orders {
for index := range messages {
if live <= target || messages[index].Role != "tool" {
continue
}
key := observationKey(messages[index], index)
if d.orderOf(key, index, boundary) != order {
continue
}
body := contentOf(messages[index])
if _, done := d.retired(key); done || !d.supersede(&messages[index], key, body) {
continue
}
live -= len(body) - len(contentOf(messages[index]))
decayed++
}
}
return decayed
}
func newestAssistant(messages []ai.Message) int {
for index := len(messages) - 1; index >= 0; index-- {
if messages[index].Role == "assistant" {
return index
}
}
return -1
}
// orderOf places one result in the retirement order. live is the index of the
// newest assistant message, so anything after it belongs to the turn about to
// be read.
func (d *decayer) orderOf(key string, index, live int) retireOrder {
switch {
case index > live:
return retireFresh
case index < d.consumedFrom || d.consumedKeys[key]:
return retireSpent
default:
return retireNeeded
}
}
// observationKey is how a result is named for the life of the leaf. The
// transcript is append-only, so a message's index is a stable identity even for
// the rare tool message without a call id.
func observationKey(message ai.Message, index int) string {
if message.ToolCallID != "" {
return message.ToolCallID
}
return fmt.Sprintf("#%d", index)
}
// supersede shortens one result in place to a line naming what it was and where
// its bytes went, and reports whether it did. It answers false for a result
// already smaller than that note — checked before anything is written, so no
// file is produced for a result that is kept.
//
// The bytes are preserved before the message is shortened: decay must defer
// detail, never destroy it. A result the pointer mechanism already preserved is
// not written again, and its recorded path is used verbatim. That is not an
// optimisation: pointers elsewhere in the transcript already name that path and
// are never rewritten, so the stub replacing their target has to agree with
// them, and a fresh write here could fail and leave the target pathless with
// pointers still aimed at it.
func (d *decayer) supersede(message *ai.Message, key, body string) bool {
label := labelFor(d.labels, message.ToolCallID)
stub, path, ok := d.supersession(key, label, body)
if !ok {
return false
}
d.spilled[key] = path
message.Content = text(stub)
return true
}
// supersession prepares the exact replacement before the transcript changes.
// Giving a result a durable address is allowed during preflight; rewriting the
// model's prompt is not. path is empty when the compact pathless note is the
// only useful replacement.
func (d *decayer) supersession(key, label, body string) (stub, path string, ok bool) {
stub = fmt.Sprintf("[%s — %d bytes, superseded]", label, len(body))
if len(stub) >= len(body) {
return "", "", false
}
path, kept := d.preserved[key]
if !kept && d.spill != nil {
path, kept = d.spill(key, body)
if kept {
d.preserved[key] = path
}
}
if kept {
withPath := fmt.Sprintf("[%s — %d bytes, spilled to %s]", label, len(body), path)
if len(withPath) < len(body) {
return withPath, path, true
}
}
return stub, "", true
}
// liveTranscriptBytes is what the whole re-sent body of the transcript costs as
// it currently stands — spent raw material and compressed state together, each
// counted as what the provider is actually billed for.
//
// It is the quantity the working set is a statement about. liveObservationBytes
// answers a narrower question (how much raw output is quoted) and is what the
// tool-decay pass fires on; this one is what decides whether that pass was
// enough.
func liveTranscriptBytes(messages []ai.Message) int {
total := 0
for _, message := range messages {
switch message.Role {
case "tool", "assistant":
for _, part := range message.Content {
total += len(part.Text)
}
}
}
return total
}
// fold retires aged assistant text, and only once retiring tool output has
// failed to bring the transcript back inside the working set.
//
// The order is the policy. decay runs first and reaches as deep as it likes into
// spent raw material; fold is asked afterwards, sees what that left, and does
// nothing at all unless the whole live body — results and reasoning together —
// is still over. So on the ordinary leaf, whose bulk is tool output, this pass
// measures and mutates nothing, and the "assistant messages are never touched"
// behaviour of every run before this one is exactly what still happens. It fires
// on the leaf whose own prose became the context: measured, assistant output was
// 45% of context growth, and no governor in the harness could see a byte of it.
//
// When it does fire it folds enough eligible messages in one batch to reach the
// same low-water target as observations. A soft pass is skipped when already-
// consumed reasoning cannot buy that full headroom: a rewrite anywhere in the
// transcript re-bills the entire tail cold, so a tiny correct rewrite can cost
// more than leaving the bytes alone. Everything newer than
// foldKeepAssistantTurns remains untouchable even at the safety line, which
// keeps the live edge and the deliverable intact.
//
// Nothing is lost. The full text goes to the same workspace spill the decay pass
// writes to, under the same write-once bookkeeping, and the stub left behind
// names that path — so a model that finds it needs its own earlier reasoning can
// read it back with sh, exactly as it can for a superseded result. A message
// whose bytes cannot be given a durable address is left alone rather than
// summarised away: a fold that cannot point is a deletion.
//
// Tool-call pairing is preserved absolutely. Only the text content of a message
// is replaced; its ToolCalls travel untouched, because an assistant tool_call
// with no answering tool message — or a tool message answering nothing — is a
// hard provider rejection rather than a degraded prompt.
func (d *decayer) fold(messages []ai.Message, budget int) int {
return d.foldWithin(messages, budget, budget)
}
// foldWithin gives assistant reasoning the same use boundary as observations.
// At the working-set target, only reasoning that preceded a workspace mutation
// may fold. Still-active reasoning is eligible only at the provider-safety
// limit. This matters for read-many/edit-once work: the model's notes are part
// of the work, not overhead merely because three newer turns exist.
func (d *decayer) foldWithin(messages []ai.Message, workingBudget, safetyBudget int) int {
live := liveTranscriptBytes(messages)
if live <= workingBudget {
return 0
}
workingTarget := d.lowWater(workingBudget)
if d.canFoldTo(messages, live, workingTarget, true) {
return d.foldTo(messages, live, workingTarget, true)
}
if safetyBudget > workingBudget && live <= safetyBudget {
return 0
}
return d.foldTo(messages, live, d.lowWater(safetyBudget), false)
}
func (d *decayer) canFoldTo(messages []ai.Message, live, target int, consumedOnly bool) bool {
if live <= target {
return true
}
ordinal, turns := assistantOrdinals(messages)
available := 0
for index := range messages {
if !d.foldable(messages, ordinal, turns, index, consumedOnly) {
continue
}
body := contentOf(messages[index])
key := fmt.Sprintf("assistant#%d", index)
if _, done := d.spilled[key]; done || len(foldStub(ordinal[index], len(body), "")) >= len(body) {
continue
}
available += len(body)
}
if live-available > target {
return false
}
available = 0
for index := range messages {
if !d.foldable(messages, ordinal, turns, index, consumedOnly) {
continue
}
body := contentOf(messages[index])
key := fmt.Sprintf("assistant#%d", index)
if _, done := d.spilled[key]; done {
continue
}
stub, ok := d.foldSupersession(key, ordinal[index], body)
if !ok {
continue
}
available += len(body) - len(stub)
if live-available <= target {
return true
}
}
return false
}
func (d *decayer) foldTo(messages []ai.Message, live, target int, consumedOnly bool) int {
// Which assistant message this is, counting from the start, so a stub can
// say which turn's reasoning it stands for. The transcript is append-only,
// so the ordinal is stable for the life of the leaf.
ordinal, turns := assistantOrdinals(messages)
folded := 0
for index := range messages {
if live <= target || !d.foldable(messages, ordinal, turns, index, consumedOnly) {
continue
}
body := contentOf(messages[index])
if body == "" {
continue
}
key := fmt.Sprintf("assistant#%d", index)
if _, done := d.spilled[key]; done {
// Already a stub from an earlier pass; it stays exactly as it is.
continue
}
stub, ok := d.foldSupersession(key, ordinal[index], body)
if !ok {
continue
}
d.spilled[key] = d.preserved[key]
messages[index].Content = text(stub)
live -= len(body) - len(stub)
folded++
}
return folded
}
func (d *decayer) foldSupersession(key string, turn int, body string) (string, bool) {
if len(foldStub(turn, len(body), "")) >= len(body) {
return "", false
}
path, kept := d.preserved[key]
if !kept && d.spill != nil {
path, kept = d.spill(key, body)
if kept {
d.preserved[key] = path
}
}
if !kept {
// No durable address, so no pointer may be made: the bytes are
// carried again, which is the cheap failure. See observations.
return "", false
}
stub := foldStub(turn, len(body), path)
if len(stub) >= len(body) {
return "", false
}
return stub, true
}
func assistantOrdinals(messages []ai.Message) ([]int, int) {
ordinal := make([]int, len(messages))
turns := 0
for index := range messages {
if messages[index].Role == "assistant" {
turns++
ordinal[index] = turns
}
}
return ordinal, turns
}
func (d *decayer) foldable(messages []ai.Message, ordinal []int, turns, index int, consumedOnly bool) bool {
if messages[index].Role != "assistant" || ordinal[index] > turns-foldKeepAssistantTurns {
return false
}
return !consumedOnly || index < d.consumedFrom
}
// foldStub is what a folded turn leaves in the transcript. It is written in the
// model's own first person because that is whose message it replaces, and it
// names the file rather than merely admitting something was there — a fold that
// does not say where is a deletion with a receipt.
func foldStub(turn, bytes int, path string) string {
if path == "" {
return fmt.Sprintf("[my turn %d — %d bytes of my own earlier reasoning, folded]", turn, bytes)
}
return fmt.Sprintf("[my turn %d — %d bytes of my own earlier reasoning, folded; the full text is in %s — read it with sh if it matters]",
turn, bytes, path)
}
func labelFor(labels map[string]string, id string) string {
if label, ok := labels[id]; ok && label != "" {
return label
}
return "earlier tool result"
}
func contentOf(message ai.Message) string {
total := 0
for _, part := range message.Content {
total += len(part.Text)
}
if total == 0 {
return ""
}
body := make([]byte, 0, total)
for _, part := range message.Content {
body = append(body, part.Text...)
}
return string(body)
}
// observations content-addresses everything the transcript is asked to carry,
// so the same bytes are paid for once.
//
// It replaces a memo keyed on the *call*, which was the wrong key twice over.
// The memo answered a repeated call from the previous result without running
// it, which is wrong the moment anything has changed underneath — so it had to
// be emptied whenever a turn ran sh, write or edit, and sh is also the read
// tool and appears in 87% of turns. Measured over 332 leaf turns it caught none
// of the 27 duplicate fetches that actually happened, while 12.5% of every
// observation byte the loop paid for was material it had already been shown.
//
// Keying on the bytes instead inverts both halves. The call still runs, so
// nothing is ever answered from a stale copy — the result is fetched fresh and
// only then compared — and mutation needs no invalidation rule at all, because
// bytes identical to bytes already in the transcript are identical whatever
// happened in between. What is saved is not the call, which was never the
// expensive part; it is the second and third copy of the same material riding
// every remaining turn of the leaf.
//
// A pointer is only ever emitted at something the model can still reach, and
// reachability is established before the pointer exists rather than checked as
// it is written.
//
// The difference matters because a pointer, once in the transcript, is never
// revisited — rewriting a message is exactly the cost this mechanism exists to
// avoid. Checking at write time is therefore a check about the present tense
// used to make a promise about the future: a target that is live when the
// pointer is written can decay three turns later, and if the spill that decay
// attempts happens to fail, the target becomes a pathless tombstone with
// pointers already aimed at it and no way left to reach the bytes.
//
// So the first time a body repeats, the canonical copy is written to the
// workspace before any pointer at it is emitted, and the decay pass reuses that
// exact path when it later stubs the message. If that write cannot be made, no
// pointer is emitted at all and the bytes are carried again — the saving is
// forfeited, which is the cheap failure. The expensive one is a model sent to
// read something that is not there.
//
// The cost is one file per body that actually repeats, written with the content
// already in hand, and never a file for a body that only ever appears once.
type observations struct {
fade *decayer
// first maps the hash of a body to the earliest copy of it still in play.
// It is never overwritten by a pointer, so a third copy of the same bytes
// points back at the original rather than at a pointer to it.
first map[string]observationCopy
}
// observationCopy is where one body's earliest copy lives: which turn produced
// it, what the call was, the decayer's key for the message holding it — which
// is what makes the spill state readable from here — and the durable path its
// bytes were written to before anything was allowed to point at them.
type observationCopy struct {
turn int
key string
label string
path string
}
func newObservations(fade *decayer) *observations {
return &observations{fade: fade, first: map[string]observationCopy{}}
}
// admit returns the text the transcript should carry for one finished call:
// the body itself the first time these bytes appear, a pointer to the earlier
// copy every time after.
func (o *observations) admit(turn int, call ai.ToolCall, result Result) string {
body := result.Content
if result.IsError {
body = "ERROR: " + body
}
// Two kinds of result are never content-addressed. An error is short and
// its whole value is being read where it happened. And a result with
// multimodal follow-up content is not its text — the image is what the
// model looks at, and a pointer would hand it a sentence instead.
if result.IsError || len(result.Followup) > 0 || call.ID == "" {
return body
}
// THE JOB REPORT IS NOT PART OF THE FACT, SO IT IS NOT PART OF THE ADDRESS.
// [Toolbox.finishResult] foots a result with the state of every outstanding
// job, and that state carries an elapsed time — it differs on every single
// call. Hashing it made every result unique for as long as any job was out,
// which turned this whole mechanism off in exactly the sessions that run
// longest. So the body is addressed by what the TOOL said, and the report
// rides on the pointer that replaces it: the model still reads where its
// streams are, and it reads it about now rather than about three turns ago.
//
// A result that is NOTHING BUT a report has no fact under it to address, and
// two reports are never the same fact however alike they read, so it is
// carried whole — which is what [Result.reportedJobs] is narrowed to saying.
fact, report := body, ""
if result.reportedJobs {
fact = stripJobReport(body)
report = body[len(fact):]
if strings.TrimSpace(fact) == "" {
return body
}
}
sum := sha256.Sum256([]byte(fact))
hash := hex.EncodeToString(sum[:])
if earlier, repeated := o.first[hash]; repeated {
// A RE-READ OF RETIRED BYTES IS ANSWERED WITH THE BYTES. Once decay has
// stubbed the earlier copy, a pointer at it would answer the model's
// re-read with a description of the material it just asked for again,
// and the only way left to it is reading the spill file in slices —
// each slice new bytes, each pushing the window over once more. So the
// body is carried whole and THIS copy becomes the canonical one, taking
// over the address the stub already names: nothing is written twice,
// and later duplicates point at a copy that is quoted.
if path, stubbed := o.fade.retired(earlier.key); stubbed {
o.fade.inherit(call.ID, path)
o.first[hash] = observationCopy{turn: turn, key: call.ID, label: callLabel(call), path: path}
return body
}
// The bytes in hand are the canonical copy's bytes — that is what the
// hash match means — so the durable copy can be written from here, now,
// without going back to whatever produced it. It is the FACT that is
// preserved: the earlier copy's own job report was about the earlier
// copy's moment, and writing this one's in its place would file a stale
// clock beside material that has none.
if earlier.path == "" {
if path, ok := o.fade.preserve(earlier.key, fact); ok {
earlier.path = path
o.first[hash] = earlier
}
}
// A pointer that is not shorter than what it replaces has saved
// nothing and cost the model a hop; the same discipline the decay
// stub applies to itself.
if pointer, reachable := o.pointerTo(earlier); reachable && len(pointer) < len(fact) {
// The earlier copy has now been read twice and is answerable from a
// durable address either way, so it is spent: the decay pass may