fix: a turn is not a unit of size, and a summary of a summary is not a compaction - #119
Merged
Merged
Conversation
…a compaction The recency window counts turns. A long agentic run — one prompt, then a hundred tool calls — is one turn, so the window that protects the last three turns covered the whole history: every cheap stage broke on the first message, safe_cut_index found no boundary whose tail fit and fell back to the newest one, and from the second compaction onward the prefix left to summarize was the previous summary and nothing else. ~105421 → ~105341 tokens, 0% reclaimed, every turn, each one paying for a provider call and a prompt-cache invalidation while the 76k of tool results behind the cut were never a candidate. The escape hatch that ignores the window already existed, wired to cut.zero? — the one shape this case never takes. It now runs whenever the tail that would survive the cut is itself over target: cut.zero? is the case where there is no boundary, this is the case where no boundary helps. The boundary is then re-chosen against the shortened history, and a history that fits is returned as it is rather than summarized for nothing. Two guards behind that. A prefix that is only a previous summary is refused before the call is made, recognised by the SUMMARY_PREFIX the writer and the reader now share rather than by a flag, which a resumed session's JSON round trip would not carry. And a summary that comes back longer than the turns it replaced is discarded, because paying for the call, losing the detail and growing the request is the one outcome with nothing to recommend it. On the reproduction — a short turn, then thirty 8 KB tool results — the same history now goes 60212 → 34249, 74% of the budget reclaimed, target reached, and no provider call at all. Closes #118 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ised a window that is gone Both were found by removing the code and watching the suite stay green. `prefix.size == 1 && summary?` had no coverage at all: the spec named for it reached `calls == 0` through the escape hatch instead, because a history of tool results is truncatable and never gets as far as the guard. It is renamed to what it does show — shortening the turn makes the call unnecessary — and a second one takes its place whose bulk is assistant text, so nothing is truncatable, the cut lands back on the same boundary, and the prefix really is the summary alone. Each of the three changes now fails a spec of its own when reverted. The readme said the recency window is given up for "one oversized `cat` inside the only turn there is". That was true of the branch this replaced. It is now given up whenever no turn boundary leaves a tail that fits, which a session of a hundred tool calls under one prompt reaches routinely — so the sentence says that, and says what still limits the pass: largest first, stopping at the target. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Behebt #118.
Was falsch war
Das Recency-Window zählt Turns, keine Tokens. Ein langer Agentenlauf — ein Prompt, dann hundert Tool-Calls — ist ein Turn, also deckte das Fenster, das die letzten drei Turns schützen soll, die gesamte Historie ab:
window_startgibt0zurück, sobald es nicht mehr echte Turns gibt als das Fenster breit ist → alle vier billigen Stages brechen bei der ersten Nachricht absafe_cut_indexfindet keine Grenze, deren Tail passt, und fällt auf die neuste zurückErgebnis in einer echten Session:
~105421 → ~105341 tokens, 0% of the budget reclaimed · summarize, in jedem Turn — jedes Mal ein Provider-Call und eine Prompt-Cache-Invalidierung, um eine Summary gegen die nächste zu tauschen, während die 76 k Tokens Tool-Output hinter dem Cut nie Kandidat waren.Der Notausgang, der das Fenster ignoriert, existierte schon — er hing an
cut.zero?, also an der einen Form, die dieser Fall nie annimmt.Was sich ändert
cut.zero?ist der Fall ohne Grenze, das hier der Fall, in dem keine Grenze hilft — dieselbe Sackgasse über einen anderen Weg. Danach wird die Grenze gegen die verkürzte Historie neu gewählt, und was jetzt passt, wird zurückgegeben statt umsonst zusammengefasst.SUMMARY_PREFIX, den Schreiber und Leser jetzt teilen. Bewusst am Text statt an einem Flag: eine resumte Session holt ihre Historie über dieselbe JSON zurück wie jede andere Nachricht, und ein Flag, das den Roundtrip nicht überlebt, würde die Guard frisch greifen und resumt aussetzen lassen.Messung
Reproduktion: ein kurzer Turn, dann 30 Tool-Ergebnisse à 8 KB, Budget 70 000.
["summarize"]["truncate"]Für den Review
Fidelity-Tradeoff, bewusst: ein großes
read_file-Ergebnis im laufenden Turn kann jetzt gekürzt werden, bevor das Modell darauf reagiert hat. Vorher machte dercut.zero?-Zweig dieses Zugeständnis nur im entarteten Einzelturn-Fall, jetzt gilt es in jeder Session, in der ein Turn über das Target wächst. Gemildert dadurch, dasstruncate_old_tool_resultsgrößtes-zuerst geht und abbricht, sobald das Target erreicht ist — in der Messung oben bleiben von 240 KB Tool-Output 136 KB stehen, kleine junge Ergebnisse überleben also. Die Alternative wäre ein Request, den der Provider ablehnt.Der
Strategy::None-Fall bleibt still, wenn Compaction nichts ausrichten kann. Das ist unverändert — der altecut.zero?-Pfad gab dasselbe zurück.Tests
Jede der drei Änderungen fällt einzeln durch eine eigene Spec, wenn man sie zurücknimmt — nachgeprüft, nicht behauptet. Die erste Fassung der Summary-Guard-Spec traf die Guard nicht (sie kam über den Notausgang ans Ziel) und ist ersetzt.
crystal spec: 1352 Beispiele, 0 Fehler.crystal tool format --check: sauber.🤖 Generated with Claude Code