From 8e9a6bf9032a3710f935eb0a3c10967b774f320d Mon Sep 17 00:00:00 2001 From: Ahmed Allam Date: Sat, 25 Jul 2026 23:46:11 +0000 Subject: [PATCH] fix(context): reject tool-output byte ceilings below the notice size A configured tool_output_max_bytes smaller than the truncation notice itself can't fit a bounded preview, so a persisted result could exceed the ceiling. Enforce a config floor (ge=1024) so nonsensical values are rejected at load time instead of being worked around at runtime. --- strix/config/settings.py | 6 +++++- strix/tools/output_store.py | 3 ++- tests/test_config_loader.py | 19 ++++++++++++++++++- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/strix/config/settings.py b/strix/config/settings.py index edb0ab09..d7c795b6 100644 --- a/strix/config/settings.py +++ b/strix/config/settings.py @@ -69,7 +69,11 @@ class ContextSettings(BaseSettings): summary_max_tokens: int = Field(default=4_096, gt=0, alias="STRIX_CONTEXT_SUMMARY_TOKENS") tool_output_max_tokens: int = Field(default=8_000, gt=0, alias="STRIX_TOOL_OUTPUT_MAX_TOKENS") tool_output_max_lines: int = Field(default=2_000, gt=0, alias="STRIX_TOOL_OUTPUT_MAX_LINES") - tool_output_max_bytes: int = Field(default=50 * 1024, gt=0, alias="STRIX_TOOL_OUTPUT_MAX_BYTES") + # Floor comfortably above the truncation-notice size so a preview + # (head+tail+notice) always fits within the configured ceiling. + tool_output_max_bytes: int = Field( + default=50 * 1024, ge=1024, alias="STRIX_TOOL_OUTPUT_MAX_BYTES" + ) class RuntimeSettings(BaseSettings): diff --git a/strix/tools/output_store.py b/strix/tools/output_store.py index 3e622c08..6e6e6202 100644 --- a/strix/tools/output_store.py +++ b/strix/tools/output_store.py @@ -49,7 +49,8 @@ def bound_text(text: str, *, max_lines: int, max_bytes: int) -> str: byte size). The removed middle is replaced with a notice recording how many lines and bytes were dropped so the agent knows output was elided. ``max_bytes`` bounds the *entire* joined result, notice and separators - included. + included, and must be large enough to hold the notice itself (guaranteed by + the ``tool_output_max_bytes`` config floor). """ lines = text.split("\n") total_bytes = _byte_len(text) diff --git a/tests/test_config_loader.py b/tests/test_config_loader.py index be0bfb8f..0d37e1e8 100644 --- a/tests/test_config_loader.py +++ b/tests/test_config_loader.py @@ -6,10 +6,11 @@ import json from typing import TYPE_CHECKING import pytest -from pydantic import AliasChoices, Field +from pydantic import AliasChoices, Field, ValidationError from pydantic.fields import FieldInfo from strix.config import loader +from strix.config.settings import ContextSettings if TYPE_CHECKING: @@ -120,6 +121,22 @@ def test_read_json_overrides_uses_json_when_no_alias_in_environ(tmp_path: Path) assert loader._read_json_overrides(path) == {"llm": {"api_key": "sk-file"}} +# --------------------------------------------------------------------------- # +# ContextSettings validation +# --------------------------------------------------------------------------- # + + +def test_tool_output_max_bytes_rejects_sub_notice_values() -> None: + # A ceiling below the truncation notice can't fit a bounded preview, so it + # is rejected at load time rather than producing over-cap persisted output. + with pytest.raises(ValidationError): + ContextSettings(STRIX_TOOL_OUTPUT_MAX_BYTES=64) + + +def test_tool_output_max_bytes_accepts_floor() -> None: + assert ContextSettings(STRIX_TOOL_OUTPUT_MAX_BYTES=1024).tool_output_max_bytes == 1024 + + # --------------------------------------------------------------------------- # # _aliases_for # --------------------------------------------------------------------------- #