From c1bbcf9712955f69fd2f0a2cc72995bca27aa68d Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Tue, 15 Sep 2026 11:49:15 -0700 Subject: [PATCH] fix(agent): hint-preview truncation log names the real remedy; invariant tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the two salvaged commits (#111777, #111781 by @KoNit-K): - agent/prompt_builder.py::_truncate_content — with queue_warning=False the logged line no longer tells the operator to "pin a larger context_file_max_chars, or use a larger-context model": the subdirectory hint cap is a constant neither knob raises. It now points at the read_file recovery the marker already discloses. - tests/gateway/test_startup_environment_probe.py — replace the call-detection test with the behavioural invariant: an oversized SOUL.md in HERMES_HOME and a warm-up leave the truncation-warning queue empty for the next default-executor task (the api_server turn path runs on that executor without copy_context, which is how the boot warning reached a foreign session). - tests/agent/test_subdirectory_hints.py — fold the new drain assertion into the existing oversized-hint test (same fixture) and pin that the log carries no context_file_max_chars advice. - agent/AGENTS.md, website/docs/.../context-files.md — the hint cap is 32,000 (docs said 8,000) and is fixed; document that it is logged, not surfaced as a chat warning. --- agent/AGENTS.md | 3 ++- agent/prompt_builder.py | 12 +++++++----- tests/agent/test_subdirectory_hints.py | 19 ++++--------------- .../gateway/test_startup_environment_probe.py | 14 ++++++++------ .../docs/user-guide/features/context-files.md | 2 +- 5 files changed, 22 insertions(+), 28 deletions(-) diff --git a/agent/AGENTS.md b/agent/AGENTS.md index 77cd5f0bf2..364d6d8131 100644 --- a/agent/AGENTS.md +++ b/agent/AGENTS.md @@ -57,7 +57,8 @@ Adding one: register in that table (no `if name == ...` chain); `tools/todo_tool a conversation; the ONLY context mutation is compression. Anything that must inject content mid-conversation rides a **user message or tool result**, never the system prompt: skill slash commands (`agent/skill_commands.py`) inject as a user message; subdirectory `AGENTS.md` hints - (`agent/subdirectory_hints.py`) append to the tool result (head+tail truncated past `_MAX_HINT_CHARS = 32_000`, with a warning). + (`agent/subdirectory_hints.py`) append to the tool result (head+tail truncated past `_MAX_HINT_CHARS = 32_000`; + the truncation is logged, never queued as a chat status warning — `context_file_max_chars` does not raise that cap). - **Strict role alternation.** Never two same-role messages in a row; never a synthetic user message injected mid-loop. The one exception is `/steer`, delivered as a standalone user row after a tool result (`assistant(tool_calls) → tool → user` is legal on every provider path) — diff --git a/agent/prompt_builder.py b/agent/prompt_builder.py index 771f946bf6..92cdf21d4b 100644 --- a/agent/prompt_builder.py +++ b/agent/prompt_builder.py @@ -1425,16 +1425,18 @@ def _truncate_content( read_path: Optional[str] = None, queue_warning: bool = True, ) -> str: """Head/tail truncation with a marker in the middle; ``read_path`` (default ``filename``) is what the - agent is told to ``read_file`` to recover the full content. ``queue_warning`` controls whether startup - context-file callers surface the truncation through the chat warning queue.""" + agent is told to ``read_file`` to recover the full content. ``queue_warning=False`` is for bounded + previews (subdirectory hints) whose fixed cap no config key or model raises: the truncation is logged + with the marker as the only disclosure, never queued for the chat status line.""" if max_chars is None: max_chars = _get_context_file_max_chars(context_length) if len(content) <= max_chars: return content - msg = ( - f"⚠️ Context file {filename} TRUNCATED: {len(content)} chars exceeds limit of {max_chars} — " - f"trim the file, pin a larger context_file_max_chars, or use a larger-context model!" + remedy = ( + "trim the file, pin a larger context_file_max_chars, or use a larger-context model!" if queue_warning + else f"the full file stays readable with read_file: {read_path or filename}" ) + msg = f"⚠️ Context file {filename} TRUNCATED: {len(content)} chars exceeds limit of {max_chars} — {remedy}" logger.warning(msg) if queue_warning: if (warnings := _truncation_warnings.get()) is None: diff --git a/tests/agent/test_subdirectory_hints.py b/tests/agent/test_subdirectory_hints.py index 3214b23236..eef49f3eee 100644 --- a/tests/agent/test_subdirectory_hints.py +++ b/tests/agent/test_subdirectory_hints.py @@ -128,6 +128,7 @@ class TestSubdirectoryHintTracker: (sub / "AGENTS.md").write_text(body, encoding="utf-8") tracker = SubdirectoryHintTracker(working_dir=str(tmp_path)) + drain_truncation_warnings() 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 @@ -135,22 +136,10 @@ class TestSubdirectoryHintTracker: 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_truncation_of_large_hints_does_not_queue_context_file_warning(self, tmp_path): - """Hint previews retain their marker without surfacing a startup-context warning in chat (#111772).""" - from agent import subdirectory_hints as sh - - drain_truncation_warnings() - sub = tmp_path / "bigdir" - sub.mkdir() - (sub / "AGENTS.md").write_text("x" * (sh._MAX_HINT_CHARS + 1), encoding="utf-8") - - tracker = SubdirectoryHintTracker(working_dir=str(tmp_path)) - result = tracker.check_tool_call("read_file", {"path": str(sub / "file.py")}) - - assert result is not None - assert "truncated AGENTS.md" in result + # A preview capped by a constant is not a context_file_max_chars problem: no chat status warning is + # queued and the log does not send the user to a knob that cannot raise the cap (#111772). assert drain_truncation_warnings() == [] + assert "context_file_max_chars" not in caplog.text 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.""" diff --git a/tests/gateway/test_startup_environment_probe.py b/tests/gateway/test_startup_environment_probe.py index 60d395ba0d..b57903352b 100644 --- a/tests/gateway/test_startup_environment_probe.py +++ b/tests/gateway/test_startup_environment_probe.py @@ -38,15 +38,17 @@ def test_warmup_leaves_probe_cached_for_first_prompt(tmp_path, monkeypatch): assert calls == [1] # single worker; the first turn reuses the cache -def test_warmup_does_not_build_context_files_without_turn_context(tmp_path, monkeypatch): - monkeypatch.setattr( - prompt_builder, - "build_context_files_prompt", - lambda: pytest.fail("gateway warm-up must not build context files"), - ) +def test_warmup_queues_no_context_file_warning_for_later_turns(tmp_path, monkeypatch): + """Boot has no agent and no model, so the warm-up must not read context files: an identity file over + the model-less default cap would otherwise queue a truncation warning into the executor thread's context, + where the next default-executor turn drains it as its own (#111773).""" + (tmp_path / "SOUL.md").write_text("x" * (prompt_builder.CONTEXT_FILE_MAX_CHARS + 1), encoding="utf-8") + prompt_builder.drain_truncation_warnings() _warm(tmp_path, monkeypatch, {}, "local") + assert prompt_builder.drain_truncation_warnings() == [] + @pytest.mark.parametrize("agent_section,backend", [({"environment_probe": False}, "local"), ({}, "ssh")]) def test_warmup_skips_probe_when_disabled_or_remote(tmp_path, monkeypatch, agent_section, backend): diff --git a/website/docs/user-guide/features/context-files.md b/website/docs/user-guide/features/context-files.md index 2906c4f780..02fc7b4ec8 100644 --- a/website/docs/user-guide/features/context-files.md +++ b/website/docs/user-guide/features/context-files.md @@ -139,7 +139,7 @@ Context files are loaded by `build_context_files_prompt()` in `agent/prompt_buil 2. **Ancestor walk** — the directory and up to 5 parent directories are checked (stopping at already-visited directories) 3. **Hint loading** — if an `AGENTS.md`, `CLAUDE.md`, or `.cursorrules` is found, it's loaded (first match per directory) 4. **Security scan** — same prompt injection scan as startup files -5. **Truncation** — capped at 8,000 characters per file +5. **Truncation** — capped at 32,000 characters per file (a fixed preview cap; `context_file_max_chars` and the model's context window do not change it). An oversized hint keeps its head/tail marker pointing at the full file and is logged, but does not raise the chat truncation warning that startup context files do 6. **Injection** — appended to the tool result, so the model sees it in context naturally The final prompt section looks roughly like: