diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py index 062549ed7f..4a1e969e92 100644 --- a/agent/agent_runtime_helpers.py +++ b/agent/agent_runtime_helpers.py @@ -33,6 +33,7 @@ from pathlib import Path from typing import Any, Dict, List, Optional, Tuple from hermes_cli.timeouts import get_provider_request_timeout +from agent.message_sanitization import _FULL_ARGS_LOG_BOUND from agent.prompt_builder import format_steer_marker from agent.tool_dispatch_helpers import _trajectory_normalize_msg, make_tool_result_message from agent.trajectory import convert_scratchpad_to_think @@ -385,10 +386,19 @@ def sanitize_tool_call_arguments( # itself becomes an orphan (#58168). tool_call_id = _ra().AIAgent._get_tool_call_id_static(tool_call) or None function_name = function.get("name", "?") - preview = arguments[:80] + # Log the FULL original argument string (bounded), not an + # 80-char preview: this branch is about to overwrite the + # only copy of these bytes in the transcript with "{}", and + # for a truncated write_file/patch call the destroyed + # arguments contain real user content (#80498 — streamed + # file content survived only as a log preview). A corrupted + # call is rare, so the oversized WARNING is a fair price for + # making the data recoverable from agent.log. + preview = arguments[:_FULL_ARGS_LOG_BOUND] log.warning( "Corrupted tool_call arguments repaired before request " - "(session=%s, message_index=%s, tool_call_id=%s, function=%s, preview=%r)", + "(session=%s, message_index=%s, tool_call_id=%s, function=%s, " + "original_arguments=%r)", session_id or "-", message_index, tool_call_id or "-", diff --git a/agent/message_sanitization.py b/agent/message_sanitization.py index dc4df3dd27..0fa7d95fff 100644 --- a/agent/message_sanitization.py +++ b/agent/message_sanitization.py @@ -183,6 +183,15 @@ def _escape_invalid_chars_in_json_strings(raw: str) -> str: return "".join(out) +# When a repair is about to destroy the only copy of a tool call's original +# argument bytes (rewriting them to "{}"), the WARNING log is the last +# surviving copy of content that can hold real user data (#80498). Bound the +# logged string at this size instead of a short preview so it stays +# recoverable from agent.log without letting a pathological payload flood +# the log. +_FULL_ARGS_LOG_BOUND = 100_000 + + def _repair_tool_call_arguments(raw_args: str, tool_name: str = "?") -> str: """Attempt to repair malformed tool_call argument JSON. @@ -271,11 +280,15 @@ def _repair_tool_call_arguments(raw_args: str, tool_name: str = "?") -> str: pass # Last resort: replace with empty object so the API request doesn't - # crash the entire session. + # crash the entire session. Log the FULL original string (bounded) — + # for callers that discard the original (e.g. the pre-send transcript + # sanitizer), this WARNING is the last surviving copy of bytes that can + # contain real user content (#80498: a truncated write_file call's + # streamed file content). logger.warning( "Unrepairable tool_call arguments for %s — " "replaced with empty object (was: %s)", - tool_name, raw_stripped[:80], + tool_name, raw_stripped[:_FULL_ARGS_LOG_BOUND], ) return "{}"