fix(context): honor the caller's retrieval limit or reject it explicitly

A final page carries no continuation hint, so it uses the whole budget and
honors any limit exactly. A non-final page's content plus hint must fit the
limit; a limit too small to hold a progressing page and its hint is now
rejected with a clear message rather than silently exceeding the documented
maximum.
This commit is contained in:
Ahmed Allam
2026-07-26 00:51:34 +00:00
parent 7028b54a84
commit 9564857686
2 changed files with 40 additions and 18 deletions
+26 -18
View File
@@ -258,24 +258,32 @@ def read_stored_output(output_id: str, *, offset: int = 0, limit: int = _PAGE_MA
start = _boundary_offset(path, max(0, offset)) if offset > 0 else 0
if start >= size:
return ""
# Reserve the continuation hint's bytes out of the effective ceiling so the
# *complete* response (content + hint) honours both the caller's ``limit``
# and the global page cap — retrieval bypasses the result-bounding wrapper,
# so this is the only ceiling. 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 even when ``limit`` is smaller than the hint.
effective = min(max(4, limit), _PAGE_MAX_BYTES)
# A final page carries no continuation hint, so the whole remaining output
# can use the budget and any limit is honoured exactly.
if size - start <= effective:
with path.open("rb") as handle:
handle.seek(start)
return handle.read().decode("utf-8", errors="replace")
# A non-final page's complete response is content + a continuation hint, and
# the whole thing must fit ``effective``. next_offset can never exceed the
# file size, so formatting with ``size`` is an exact upper bound on the hint.
# A page needs at least one char (4 bytes) of content to make progress, so a
# limit too small to hold that plus the hint is rejected rather than silently
# exceeded.
hint_reserve = len(_CONTINUATION_HINT.format(output_id=output_id, offset=size))
content_budget = max(4, min(max(4, limit), _PAGE_MAX_BYTES) - hint_reserve)
content_budget = effective - hint_reserve
if content_budget < 4:
return (
f"limit={limit} is too small to page this output; "
f"request at least {hint_reserve + 4} bytes."
)
with path.open("rb") as handle:
handle.seek(start)
chunk = handle.read(content_budget)
has_more = start + len(chunk) < size
if has_more:
chunk = _trim_incomplete_utf8_tail(chunk)
shown = chunk.decode("utf-8", errors="replace")
if has_more:
next_offset = start + len(chunk)
shown += _CONTINUATION_HINT.format(output_id=output_id, offset=next_offset)
return shown
chunk = _trim_incomplete_utf8_tail(handle.read(content_budget))
next_offset = start + len(chunk)
return chunk.decode("utf-8", errors="replace") + _CONTINUATION_HINT.format(
output_id=output_id, offset=next_offset
)
+14
View File
@@ -130,6 +130,20 @@ def test_read_stored_output_paginates(tmp_path: Path) -> None:
assert match is not None and int(match.group(1)) > 0
def test_read_stored_output_rejects_limit_too_small_for_a_page(tmp_path: Path) -> None:
# A non-final page needs room for content plus the continuation hint; a limit
# too small to hold both is rejected rather than silently exceeded.
configure_output_store(tmp_path)
text = "\n".join(str(i) for i in range(1000))
output_id = re.search(
r'output_id="([0-9a-f]{32})"',
bound_and_store(text, max_lines=4, max_bytes=1_000_000),
)
assert output_id is not None
result = read_stored_output(output_id.group(1), offset=0, limit=10)
assert "too small" in result
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.