fix(context): reserve continuation-hint bytes within the retrieval page ceiling

A full non-final retrieval page consumed the entire byte budget and then
appended the pagination hint, so the returned result (which bypasses the
result-bounding wrapper) exceeded the ceiling. Reserve the hint's bytes out
of the page budget so content plus hint always fits.
This commit is contained in:
Ahmed Allam
2026-07-26 00:26:09 +00:00
parent e5a46b92fd
commit 65fa698b09
2 changed files with 33 additions and 8 deletions
+17 -8
View File
@@ -33,6 +33,14 @@ _DEFAULT_STORE_DIR = Path.home() / ".strix" / "tool-output"
# instead of returned whole.
_PAGE_MAX_BYTES = 50 * 1024
# Appended to a non-final page so the agent can request the next one. Its bytes
# are reserved out of the page budget so a full page plus this hint still fits
# ``_PAGE_MAX_BYTES``.
_CONTINUATION_HINT = (
"\n\n[... more; call read_tool_output("
'output_id="{output_id}", offset={offset}) to continue ...]'
)
# Single-key holder so the configured path can be swapped per scan without a
# module-level ``global`` rebind.
_config: dict[str, Path] = {}
@@ -243,12 +251,16 @@ def read_stored_output(output_id: str, *, offset: int = 0, limit: int = _PAGE_MA
size = path.stat().st_size
if start >= size:
return ""
# Floor at 4 bytes (the max UTF-8 char length) so a page always makes
# progress past a single multi-byte character.
budget = min(max(4, limit), _PAGE_MAX_BYTES)
# Reserve the continuation hint's bytes so a full page plus its appended
# metadata still fits the ceiling (retrieval bypasses the result wrapper).
# next_offset can never exceed the file size, so formatting with ``size``
# is an exact upper bound on the hint length. Floor content at 4 bytes (the
# max UTF-8 char length) so a page always makes progress past one char.
hint_reserve = len(_CONTINUATION_HINT.format(output_id=output_id, offset=size))
content_budget = min(max(4, limit), _PAGE_MAX_BYTES - hint_reserve)
with path.open("rb") as handle:
handle.seek(start)
chunk = handle.read(budget)
chunk = handle.read(content_budget)
has_more = start + len(chunk) < size
if has_more:
@@ -262,8 +274,5 @@ def read_stored_output(output_id: str, *, offset: int = 0, limit: int = _PAGE_MA
chunk = _trim_incomplete_utf8_head(chunk)
shown = chunk.decode("utf-8", errors="replace")
if has_more:
shown += (
"\n\n[... more; call read_tool_output("
f'output_id="{output_id}", offset={next_offset}) to continue ...]'
)
shown += _CONTINUATION_HINT.format(output_id=output_id, offset=next_offset)
return shown
+16
View File
@@ -6,6 +6,7 @@ import re
from typing import TYPE_CHECKING
from strix.tools.output_store import (
_PAGE_MAX_BYTES,
bound_and_store,
bound_text,
configure_output_store,
@@ -125,6 +126,21 @@ def test_read_stored_output_paginates(tmp_path: Path) -> None:
assert "offset=10" in page
def test_read_stored_output_page_including_hint_stays_within_ceiling(tmp_path: Path) -> None:
# A full non-final page plus its continuation hint must not exceed the page
# ceiling — retrieval bypasses the general result-bounding wrapper.
configure_output_store(tmp_path)
text = "x" * (_PAGE_MAX_BYTES * 3)
output_id = re.search(
r'output_id="([0-9a-f]{32})"',
bound_and_store(text, max_lines=4, max_bytes=1_000),
)
assert output_id is not None
page = read_stored_output(output_id.group(1), offset=0, limit=_PAGE_MAX_BYTES)
assert "more;" in page # a hint was appended (non-final page)
assert len(page.encode("utf-8")) <= _PAGE_MAX_BYTES
def test_read_stored_output_pages_long_lines_losslessly(tmp_path: Path) -> None:
# A single line far larger than the page budget must be split across pages
# (never returned whole), and paging forward must reconstruct the output