From f514132ff340d84dc11b846d174f3915858774a9 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Sun, 5 Jul 2026 14:03:53 -0700 Subject: [PATCH] fix: disclose mid-line clamp in truncation hint When a single line exceeds the entire char budget, its tail is unreachable via offset pagination (offsets are line-granular). Tell the model so it doesn't assume it saw the full line. --- tests/tools/test_file_read_guards.py | 15 +++++++++++++++ tools/file_tools.py | 12 ++++++++++++ 2 files changed, 27 insertions(+) diff --git a/tests/tools/test_file_read_guards.py b/tests/tools/test_file_read_guards.py index 90b7f957c0d..23aa53d2b14 100644 --- a/tests/tools/test_file_read_guards.py +++ b/tests/tools/test_file_read_guards.py @@ -275,6 +275,21 @@ class TestCharacterCountGuard(unittest.TestCase): self.assertNotIn("error", result) self.assertTrue(result["content"]) # not empty self.assertEqual(result["next_offset"], 2) # advanced past line 1 + # The hint must disclose that the line was clamped mid-line and its + # remainder is unreachable via offset pagination. + self.assertIn("clamped mid-line", result["hint"]) + + @patch("tools.file_tools._get_file_ops") + @patch("tools.file_tools._get_max_read_chars", return_value=1000) + def test_multiline_truncation_hint_has_no_clamp_note(self, _mock_limit, mock_ops): + """Ordinary multi-line truncation must NOT carry the clamp note.""" + big_content = "\n".join(f"{i}|" + "z" * 98 for i in range(1, 51)) + mock_ops.return_value = _make_fake_ops( + content=big_content, total_lines=50, file_size=len(big_content), + ) + result = json.loads(read_file_tool("/tmp/manylines.txt", task_id="manylines")) + self.assertTrue(result["truncated"]) + self.assertNotIn("clamped mid-line", result["hint"]) @patch("tools.file_tools._get_file_ops") def test_small_read_not_truncated(self, mock_ops): diff --git a/tools/file_tools.py b/tools/file_tools.py index f9047a11c9f..61e6ecc588d 100644 --- a/tools/file_tools.py +++ b/tools/file_tools.py @@ -1240,6 +1240,12 @@ def read_file_tool(path: str, offset: int = 1, limit: int = 500, task_id: str = f"{shown_end} of {total_lines}). Use offset={next_offset} " "to continue." ) + if len(trimmed.split("\n", 1)[0]) >= max_chars: + result_dict["hint"] += ( + " Note: the first line alone exceeded the budget and " + "was clamped mid-line; its remainder is not " + "retrievable via offset." + ) if result_dict["content"]: result_dict["content"] = redact_sensitive_text(result_dict["content"], file_read=True) return json.dumps(result_dict, ensure_ascii=False) @@ -1365,6 +1371,12 @@ def read_file_tool(path: str, offset: int = 1, limit: int = 500, task_id: str = f"{lines_kept} line(s) (showing lines {offset}-{shown_end} of " f"{total_lines}). Use offset={next_offset} to continue." ) + if len(trimmed.split("\n", 1)[0]) >= max_chars: + result_dict["hint"] += ( + " Note: the first line alone exceeded the budget and was " + "clamped mid-line; its remainder is not retrievable via " + "offset." + ) content_len = len(trimmed) # ── Redact secrets (after guard check to skip oversized content) ──