From 5fb4a4abae864955b63dc5c20136f3269338b4ef Mon Sep 17 00:00:00 2001 From: bandzalkin Date: Wed, 21 Oct 2026 15:26:56 +0300 Subject: [PATCH 1/2] fix: measure redacted file content with lone surrogates A lone surrogate in band_send_room_file's content made the redactor's byte count raise UnicodeEncodeError, dropping the whole tool_call event. Encode with surrogatepass so the placeholder is measured and emitted. --- src/band/runtime/tools/registry.py | 4 ++- .../copilot_sdk/test_tool_bridging.py | 26 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/band/runtime/tools/registry.py b/src/band/runtime/tools/registry.py index 28dc3df16..dcae0f26a 100644 --- a/src/band/runtime/tools/registry.py +++ b/src/band/runtime/tools/registry.py @@ -906,7 +906,9 @@ def redact_tool_call_args(tool_name: str, args: dict[str, Any]) -> dict[str, Any return args return { **args, - "content": file_content_placeholder(len(content.encode("utf-8"))), + "content": file_content_placeholder( + len(content.encode("utf-8", "surrogatepass")) + ), } diff --git a/tests/adapters/copilot_sdk/test_tool_bridging.py b/tests/adapters/copilot_sdk/test_tool_bridging.py index 4646d24ec..f96f736b6 100644 --- a/tests/adapters/copilot_sdk/test_tool_bridging.py +++ b/tests/adapters/copilot_sdk/test_tool_bridging.py @@ -419,3 +419,29 @@ async def test_tool_call_event_redacts_file_content(self): == f"<{len(raw_content.encode('utf-8'))} byte file content>" ) assert raw_content not in call_event["content"] + + @pytest.mark.asyncio + async def test_lone_surrogate_file_content_is_redacted_without_losing_event(self): + client = FakeCopilotClient() + adapter = await make_started_adapter(client, emit=Emit.TOOL_CALLS) + tools = _SendRoomFileFakeTools() + await run_message(adapter, tools) + handler = client.sessions[0].find_tool("band_send_room_file").handler + await handler( + ToolInvocation( + tool_call_id="surrogate-file", + tool_name="band_send_room_file", + arguments={"content": "\ud800", "filename": "notes.txt"}, + ) + ) + call = json.loads( + next(e for e in tools.events_sent if e["message_type"] == "tool_call")[ + "content" + ] + ) + assert call["tool_call_id"] == "surrogate-file" + assert call["name"] == "band_send_room_file" + assert call["args"] == { + "filename": "notes.txt", + "content": "<3 byte file content>", + } From 90292db5453ebb43aa4f77497741f0d35be64406 Mon Sep 17 00:00:00 2001 From: Alexander Zaikman Date: Fri, 23 Oct 2026 05:35:47 +0300 Subject: [PATCH 2/2] test: clarify surrogate redaction event assertion --- tests/adapters/copilot_sdk/test_tool_bridging.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/adapters/copilot_sdk/test_tool_bridging.py b/tests/adapters/copilot_sdk/test_tool_bridging.py index f96f736b6..9f84d48b5 100644 --- a/tests/adapters/copilot_sdk/test_tool_bridging.py +++ b/tests/adapters/copilot_sdk/test_tool_bridging.py @@ -10,9 +10,10 @@ from pydantic import BaseModel, model_validator from band.adapters.copilot_sdk import _COPILOT_SDK_AVAILABLE -from band.core.types import Emit +from band.core.types import Emit, MessageType from band.runtime.custom_tools import declares_turn_effect from band.runtime.tools import ToolCallOutcome, TurnEffect +from band.testing.fake_tools import events_of_type from tests.adapters.copilot_sdk.fakes import ( FakeCopilotClient, FakeCopilotSession, @@ -421,7 +422,9 @@ async def test_tool_call_event_redacts_file_content(self): assert raw_content not in call_event["content"] @pytest.mark.asyncio - async def test_lone_surrogate_file_content_is_redacted_without_losing_event(self): + async def test_lone_surrogate_file_content_is_redacted_without_losing_event( + self, + ) -> None: client = FakeCopilotClient() adapter = await make_started_adapter(client, emit=Emit.TOOL_CALLS) tools = _SendRoomFileFakeTools() @@ -434,11 +437,8 @@ async def test_lone_surrogate_file_content_is_redacted_without_losing_event(self arguments={"content": "\ud800", "filename": "notes.txt"}, ) ) - call = json.loads( - next(e for e in tools.events_sent if e["message_type"] == "tool_call")[ - "content" - ] - ) + (call_event,) = events_of_type(tools, MessageType.TOOL_CALL) + call = json.loads(call_event["content"]) assert call["tool_call_id"] == "surrogate-file" assert call["name"] == "band_send_room_file" assert call["args"] == {