fix(context): subdirectory AGENTS.md hints keep head+tail and warn when over the ceiling; ceiling 8k -> 32k

The on-demand subdirectory hint loader (agent/subdirectory_hints.py, #5291) tail-chopped anything past
8,000 chars with a bare marker and no log line. apps/desktop/AGENTS.md (11k) has been arriving cut off in
every Desktop-area session since it was written, and nobody could tell. Compared with the field: Codex
caps its whole instruction chain at 32 KiB (project_doc_max_bytes) and documents it; Claude Code and
Cursor apply no cap to nested files; OpenCode has no nested discovery at all. Our on-demand + cache-safe
+ ancestor-walk design is the strongest of the four; only the constant and the silent cut were wrong.

Now: ceiling 32,000 chars (Codex's number, a guard against a stray huge CLAUDE.md in a vendored tree, not
a target), and truncation goes through prompt_builder._truncate_content — head 70% + tail 20%, a marker
naming the file to read_file for the rest, and a WARNING in the log. Area AGENTS.md files should stay
around 8k anyway: the text lands in a tool result on the first touch of the directory.

Tests: oversized hint keeps head+tail, names the path, and logs; a 12k area file (over the old cap, under
the new) arrives intact.
This commit is contained in:
Teknium
2026-09-04 02:03:08 -07:00
parent 78288b488b
commit d61cff60e3
2 changed files with 32 additions and 13 deletions

View File

@@ -11,14 +11,19 @@ import shlex
from pathlib import Path
from typing import Dict, Any, Optional, Set
from agent.prompt_builder import _read_text_with_timeout, _scan_context_content
from agent.prompt_builder import _read_text_with_timeout, _scan_context_content, _truncate_content
from agent.search_policy import SEARCH_PRUNE_DIR_NAMES
logger = logging.getLogger(__name__)
# Same filenames as prompt_builder.py, in priority order (first match wins per dir).
_HINT_FILENAMES = ["AGENTS.override.md", "AGENTS.md", "agents.md", "CLAUDE.md", "claude.md", ".cursorrules"]
_MAX_HINT_CHARS = 8_000
# Per-file ceiling for on-demand subdirectory hints. 32 KiB matches Codex's `project_doc_max_bytes` default
# (Claude Code and Cursor apply none); it is a guard against a stray huge CLAUDE.md in a vendored tree, not a
# target — keep area AGENTS.md files well under it (~8k) because this text lands in a tool result on the first
# touch of that directory. Over the ceiling: head+tail kept, marker with the path so the agent can read_file it,
# and a WARNING in the log (the old 8k silent tail-chop cut apps/desktop/AGENTS.md for months unnoticed).
_MAX_HINT_CHARS = 32_000
_PATH_ARG_KEYS = {"path", "file_path", "workdir"}
_COMMAND_TOOLS = {"terminal"}
_MAX_ANCESTOR_WALK = 5 # ancestor levels walked per path — bounds deep-path scans
@@ -171,9 +176,8 @@ class SubdirectoryHintTracker:
self._loaded_digests.add(digest)
# Same security scan as startup context loading.
content = _scan_context_content(content, filename)
if len(content) > _MAX_HINT_CHARS:
content = content[:_MAX_HINT_CHARS] + f"\n\n[...truncated {filename}: {len(content):,} chars total]"
rel_path = self._display_path(hint_path)
content = _truncate_content(content, filename, max_chars=_MAX_HINT_CHARS, read_path=rel_path)
logger.debug("Loaded subdirectory hints from %s: %s", directory, [rel_path])
return f"[Subdirectory context discovered: {rel_path}]\n{content}" # first match wins per directory
except Exception as exc:

View File

@@ -96,20 +96,35 @@ class TestSubdirectoryHintTracker:
def test_truncation_of_large_hints(self, tmp_path):
"""Hint files over the limit are truncated."""
def test_truncation_of_large_hints(self, tmp_path, caplog):
"""Over the ceiling: head AND tail survive, the marker names the file to read_file, and it is logged
(the old silent tail-chop hid a truncated apps/desktop/AGENTS.md for months)."""
import logging
from agent import subdirectory_hints as sh
sub = tmp_path / "bigdir"
sub.mkdir()
(sub / "AGENTS.md").write_text("x" * 20_000)
body = "HEAD-MARKER " + ("x" * (sh._MAX_HINT_CHARS + 5_000)) + " TAIL-MARKER"
(sub / "AGENTS.md").write_text(body)
tracker = SubdirectoryHintTracker(working_dir=str(tmp_path))
result = tracker.check_tool_call(
"read_file", {"path": str(sub / "file.py")}
)
with caplog.at_level(logging.WARNING, logger="agent.prompt_builder"):
result = tracker.check_tool_call("read_file", {"path": str(sub / "file.py")})
assert result is not None
assert "truncated" in result.lower()
# Should be capped
assert len(result) < 20_000
assert "HEAD-MARKER" in result and "TAIL-MARKER" in result
assert "truncated AGENTS.md" in result and "bigdir/AGENTS.md" in result
assert len(result) < len(body)
assert any("TRUNCATED" in r.message and "AGENTS.md" in r.message for r in caplog.records)
def test_area_file_under_ceiling_is_delivered_whole(self, tmp_path):
"""An area AGENTS.md sized like ours (well under the ceiling) arrives intact — no marker."""
sub = tmp_path / "gateway"
sub.mkdir()
body = "# Gateway rules\n" + ("- rule\n" * 1500) # ~12k chars: over the OLD 8k cap, under the new one
(sub / "AGENTS.md").write_text(body)
tracker = SubdirectoryHintTracker(working_dir=str(tmp_path))
result = tracker.check_tool_call("read_file", {"path": str(sub / "run.py")})
assert result is not None and "truncated" not in result.lower()
assert result.endswith(body.strip())
def test_empty_args(self, project):
"""Empty args should not crash."""