fix: address codex review round 2

- spill-policy reserves the spill notice's byte cost inside maxInlineBytes, so
  the replacement (preview + notice) never exceeds the documented model-facing
  cap. When the notice alone fills the budget the preview is empty; when even a
  notice-only replacement is not smaller than the original, the inline result is
  kept (spilling would only add bytes).
- retention TextRetainer trims an oversized single suffix chunk to the last
  suffixCap bytes on push, so tail/headTail retention stays bounded by suffixCap
  instead of retaining and re-copying the whole chunk in finish() — this is the
  spill preview path, which pushes the whole result as one chunk.
This commit is contained in:
Dudu-0223
2026-07-09 09:51:35 +08:00
parent d0c2f0916d
commit 326b199f25
5 changed files with 71 additions and 22 deletions

View File

@@ -355,6 +355,19 @@ export class TextRetainer {
this.suffixHeld -= head.length
head = this.suffixChunks[0]
}
// The head chunk can still hold leading bytes beyond the last `suffixCap`
// — a single chunk LARGER than the window is retained whole by the loop
// above (dropping the only chunk would leave < cap). Trim those leading
// bytes so the accumulator (and finish()'s concat) stays bounded by
// `suffixCap` instead of allocating/copying the full chunk again;
// finish() only ever reads the last `suffixLen ≤ suffixCap` bytes, so this
// drops nothing it would return. (head.length > excess by the loop
// invariant `suffixHeld - head.length < suffixCap`, so the slice is non-empty.)
if (head !== undefined && this.suffixHeld > this.suffixCap) {
const excess = this.suffixHeld - this.suffixCap
this.suffixChunks[0] = head.subarray(excess)
this.suffixHeld -= excess
}
}
// Dropped = bytes that no side can keep. Compute cumulative omission the