From 471efcc2ca6e41f852aac74eb03e9ca86de385d8 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sun, 9 Aug 2026 16:45:23 -0700 Subject: [PATCH] =?UTF-8?q?feat(tools):=20name=20the=20dead=20end=20?= =?UTF-8?q?=E2=80=94=20past-EOF=20and=20empty-file=20notes=20in=20read=5Ff?= =?UTF-8?q?ile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A read past EOF returned content '900|' (a phantom line-number prefix that looks like a real line) and an empty file returned '1|' — both ambiguous silence: indistinguishable, from inside the model, from a broken tool, so it re-reads and widens windows. Name the dead end and its recovery instead: 'offset 900 is beyond the end of the file (412 lines total). Retry with offset <= 412.' / 'File is empty (0 bytes).' Notes, not errors — a fact about the file is not a failure. Boundary pinned by test: offset == total_lines still reads (an off-by-one in a resume hint is a silently corrupted read). Measured (file-only arm, 3 reps, control vs feature): qwen3.8-max -18% tokens, -26% tool calls, -17% turns across the two affected tasks; opus-4.8 flat (within rep noise); accuracy held 1.00. --- tests/tools/test_read_past_eof_note.py | 44 ++++++++++++++++++++++++++ tools/file_operations.py | 25 ++++++++++++++- 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 tests/tools/test_read_past_eof_note.py diff --git a/tests/tools/test_read_past_eof_note.py b/tests/tools/test_read_past_eof_note.py new file mode 100644 index 0000000000000..a2c27380d1739 --- /dev/null +++ b/tests/tools/test_read_past_eof_note.py @@ -0,0 +1,44 @@ +"""Tests for the past-EOF and empty-file notes in read_file. + +An empty content string is ambiguous from inside the model (empty file? +bad offset? broken tool?) — the tool names the dead end and the recovery. +""" + +import json + +from tools.file_tools import read_file_tool + + +class TestPastEofNote: + def test_offset_beyond_eof_names_recovery(self, tmp_path, monkeypatch): + monkeypatch.setenv("TERMINAL_CWD", str(tmp_path)) + p = tmp_path / "r.txt" + p.write_text("\n".join(f"l{i}" for i in range(1, 51)) + "\n") + result = json.loads(read_file_tool(str(p), offset=900, limit=50)) + hint = result.get("hint") or "" + assert "beyond the end" in hint + assert "50" in hint # states actual line count + assert not result.get("error"), "a fact about the file is not an error" + + def test_offset_at_last_line_still_reads(self, tmp_path, monkeypatch): + monkeypatch.setenv("TERMINAL_CWD", str(tmp_path)) + p = tmp_path / "r.txt" + p.write_text("a\nb\nc\n") + result = json.loads(read_file_tool(str(p), offset=3, limit=10)) + assert "c" in result.get("content", "") + assert "beyond" not in (result.get("hint") or "") + + def test_empty_file_says_so(self, tmp_path, monkeypatch): + monkeypatch.setenv("TERMINAL_CWD", str(tmp_path)) + p = tmp_path / "e.txt" + p.write_text("") + result = json.loads(read_file_tool(str(p))) + assert "empty" in (result.get("hint") or "").lower() + assert not result.get("error") + + def test_normal_pagination_hint_unchanged(self, tmp_path, monkeypatch): + monkeypatch.setenv("TERMINAL_CWD", str(tmp_path)) + p = tmp_path / "r.txt" + p.write_text("\n".join(f"l{i}" for i in range(1, 300)) + "\n") + result = json.loads(read_file_tool(str(p), offset=1, limit=100)) + assert "offset=101" in (result.get("hint") or "") diff --git a/tools/file_operations.py b/tools/file_operations.py index 87c8a26efc0ad..de47b89c3add3 100644 --- a/tools/file_operations.py +++ b/tools/file_operations.py @@ -1331,7 +1331,30 @@ class ShellFileOperations(FileOperations): hint = None if truncated: hint = f"Use offset={end_line + 1} to continue reading (showing {offset}-{end_line} of {total_lines} lines)" - + + # Ambiguous-silence guards: an empty content string is + # indistinguishable, from inside the model, from a broken tool — + # it re-reads, widens the window, tries another path. Name the + # dead end and its recovery instead. + if file_size == 0: + return ReadResult( + content="", + total_lines=0, + file_size=0, + hint="File is empty (0 bytes).", + ) + if offset > total_lines > 0: + return ReadResult( + content="", + total_lines=total_lines, + file_size=file_size, + hint=( + f"Note: offset {offset} is beyond the end of the file " + f"({total_lines} lines total). Retry with offset <= " + f"{total_lines}." + ), + ) + return ReadResult( content=self._add_line_numbers(read_output, offset), total_lines=total_lines,