From 459b3ee6cdcd9021dd3cdf33f9b2c01cc5820982 Mon Sep 17 00:00:00 2001 From: Kevin Rajan <7121943+kvnloo@users.noreply.github.com> Date: Wed, 23 Sep 2026 16:43:58 -0500 Subject: [PATCH] fix(agent): detect copied context-compression artifacts (cherry picked from commit 09757f3af1c1b3eec1ccde560089448ffa704c92) --- agent/tool_dispatch_helpers.py | 52 +++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/agent/tool_dispatch_helpers.py b/agent/tool_dispatch_helpers.py index beb5842d62..e645ed8113 100644 --- a/agent/tool_dispatch_helpers.py +++ b/agent/tool_dispatch_helpers.py @@ -19,6 +19,7 @@ from typing import Any, Dict, List, Optional from agent.message_metadata import stamp_message_timestamp from agent.tool_result_classification import ( FILE_MUTATING_TOOL_NAMES as _FILE_MUTATING_TOOLS, + tool_may_have_side_effect, ) from tools.threat_patterns import scan_for_threats @@ -67,6 +68,54 @@ _DESTRUCTIVE_PATTERNS = re.compile( # Output redirects that overwrite files (> but not >>) _REDIRECT_OVERWRITE = re.compile(r'[^>]>[^>]|^>[^>]') +# Legacy compressor builds ended long string leaves with these ambiguous tails. +# Keep the compatibility guard narrow: real historical rewrites had a 200-char head. +_LEGACY_CONTEXT_PRUNED_ARG_TAILS = ("...[truncated]", "…[truncated]") + + +def _context_pruned_argument_paths(tool_name: str, args: Any) -> list[str]: + """Paths whose values contain model-visible context-compression artifacts. + + The compressor's current marker carries numeric omitted/total counts. Match + that rendered shape rather than the prefix alone so Hermes can still edit + source/docs that mention the compression marker constant or its template. + Unknown/plugin/MCP tools stay effect-capable by default; known read-only + tools may inspect or quote compressed history. + """ + if not tool_may_have_side_effect(tool_name): + return [] + + # Late import keeps this helper from making the dispatch module own a second + # copy of the compressor sentinel (and avoids a heavy import at CLI startup). + from agent.context_compressor import _COMPRESSION_MARKER_PREFIX + + marker_re = re.compile( + re.escape(_COMPRESSION_MARKER_PREFIX) + + r"\s+\d[\d,]* of \d[\d,]* chars omitted here by Hermes's context compressor\." + ) + found: list[str] = [] + + def _walk(value: Any, path: str) -> None: + if isinstance(value, str): + stripped = value.rstrip() + if marker_re.search(value) or ( + len(stripped) > 200 and stripped.endswith(_LEGACY_CONTEXT_PRUNED_ARG_TAILS) + ): + found.append(path) + return + if isinstance(value, dict): + for key, child in value.items(): + key_text = str(key) + child_path = f"{path}.{key_text}" if key_text.isidentifier() else f"{path}[{key_text!r}]" + _walk(child, child_path) + return + if isinstance(value, (list, tuple)): + for index, child in enumerate(value): + _walk(child, f"{path}[{index}]") + + _walk(args, "$") + return found + def _is_destructive_command(cmd: str) -> bool: """Heuristic: does this terminal command look like it modifies/deletes files?""" @@ -543,7 +592,8 @@ def _maybe_wrap_untrusted(name: str, content: Any) -> Any: __all__ = [ "_NEVER_PARALLEL_TOOLS", "_PARALLEL_SAFE_TOOLS", "_PATH_SCOPED_TOOLS", "_PATH_SCOPED_READERS", - "_PATH_SCOPED_WRITERS", "_DESTRUCTIVE_PATTERNS", "_REDIRECT_OVERWRITE", "_is_destructive_command", + "_PATH_SCOPED_WRITERS", "_DESTRUCTIVE_PATTERNS", "_REDIRECT_OVERWRITE", "_context_pruned_argument_paths", + "_is_destructive_command", "_plan_tool_batch_segments", "_should_parallelize_tool_batch", "_canonical_path", "_extract_parallel_scope_path", "_extract_parallel_scope_paths", "_paths_overlap", "_is_multimodal_tool_result", "_multimodal_text_summary", "_append_subdir_hint_to_multimodal",