From 20671b4c1caec1d934a63b38a921d32558dfd0df Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Mon, 19 Oct 2026 15:30:20 -0400 Subject: [PATCH] fix(sensor): record the message of a failed Copilot tool's error Copilot's session events record a failed tool's error as an object, {"message": ..., "code": ...}. The parser serialized that object, so ToolUsage.error held a JSON blob such as '{"message": "The user rejected this tool call.", "code": "denied"}'. It now uses the message; plain string errors are handled as before. Co-Authored-By: Claude Opus 5.5 --- Sensor/adr_sensor/parsers/copilot_parser.py | 10 +++++++- Sensor/tests/test_parsers.py | 27 +++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/Sensor/adr_sensor/parsers/copilot_parser.py b/Sensor/adr_sensor/parsers/copilot_parser.py index f3475cf4..9df78635 100644 --- a/Sensor/adr_sensor/parsers/copilot_parser.py +++ b/Sensor/adr_sensor/parsers/copilot_parser.py @@ -299,7 +299,7 @@ def _process_event(self, event: Dict[str, Any], session_data: Dict[str, Any]): if tool is not None: success = bool(data.get("success")) result_text = self._normalize_tool_result(data.get("result")) - error_text = self._normalize_tool_result(data.get("error")) + error_text = self._normalize_tool_error(data.get("error")) tool["result"] = result_text tool["status"] = "success" if success else "error" if not success: @@ -513,6 +513,14 @@ def _normalize_tool_result(self, result: Any) -> Optional[str]: return truncate_middle(text, max_length=MAX_STRING_LENGTH, edge_chars=EDGE_CHARS) + def _normalize_tool_error(self, error: Any) -> Optional[str]: + """Use the message of Copilot's {"message", "code"} error object.""" + if isinstance(error, dict): + message = error.get("message") + if isinstance(message, str) and message: + return truncate_middle(message, max_length=MAX_STRING_LENGTH, edge_chars=EDGE_CHARS) + return self._normalize_tool_result(error) + def _truncate_large_arguments(self, arguments: Dict[str, Any]) -> Dict[str, Any]: """Truncate large string values within tool argument dictionaries.""" if not isinstance(arguments, dict): diff --git a/Sensor/tests/test_parsers.py b/Sensor/tests/test_parsers.py index ffeb2fd2..1baa60fd 100644 --- a/Sensor/tests/test_parsers.py +++ b/Sensor/tests/test_parsers.py @@ -1680,6 +1680,33 @@ def test_tool_failure_uses_error_field_when_result_is_absent(self, tmp_path): assert tool.result is None assert tool.error == "File not found" + def test_tool_failure_uses_message_of_error_object(self, tmp_path): + """Copilot's schema records a failed tool's error as {"message", "code"}.""" + session_dir = tmp_path / "error-object" + events = self._minimal_events("error-object") + [ + { + "type": "tool.execution_start", + "timestamp": "2026-08-10T10:00:02.000Z", + "data": {"toolCallId": "call-1", "toolName": "bash", "arguments": {"command": "rm -rf build"}}, + }, + { + "type": "tool.execution_complete", + "timestamp": "2026-08-10T10:00:03.000Z", + "data": { + "toolCallId": "call-1", + "success": False, + "error": {"message": "The user rejected this tool call.", "code": "denied"}, + }, + }, + ] + self._write_events(session_dir, events) + + entry = CopilotParser(base_path=tmp_path).parse_session_dir(session_dir) + + tool = [tool for message in entry.chat_history for tool in message.tools][0] + assert tool.status == "error" + assert tool.error == "The user rejected this tool call." + def test_default_max_age_days(self): assert CopilotParser().max_age_days == 14