From 7f3b8f3f1a9558ce31f5658a543ae1294f85754e Mon Sep 17 00:00:00 2001 From: otiscuilei Date: Fri, 10 Jul 2026 14:30:22 +0800 Subject: [PATCH] fix(memory): use split("\n") in insert() to match view()/str_replace() The filesystem memory tool's insert() split file content with str.splitlines(), which treats ~8 extra Unicode/control characters (\r, \v, \f, \x1c-\x1e, NEL, U+2028, U+2029) as line boundaries. Every other command (view, str_replace) splits only on "\n", so insert() counted and located lines differently from what view() reports to the model, placing inserted text at the wrong offset, and rejoined with "\n", silently discarding those boundary characters. Switch both the sync and async insert() to content.split("\n") so line indexing is consistent across all commands and content is preserved. --- .../lib/tools/_beta_builtin_memory_tool.py | 4 +- .../lib/tools/memory_tools/test_filesystem.py | 52 +++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/anthropic/lib/tools/_beta_builtin_memory_tool.py b/src/anthropic/lib/tools/_beta_builtin_memory_tool.py index b995fbb3d..5242469fc 100644 --- a/src/anthropic/lib/tools/_beta_builtin_memory_tool.py +++ b/src/anthropic/lib/tools/_beta_builtin_memory_tool.py @@ -547,7 +547,7 @@ def insert(self, command: BetaMemoryTool20250818InsertCommand) -> str: raise ToolError(f"The path {command.path} is not a file.") content = _read_file_content(full_path, command.path) - lines = content.splitlines() + lines = content.split("\n") if command.insert_line < 0 or command.insert_line > len(lines): raise ToolError( @@ -848,7 +848,7 @@ async def insert(self, command: BetaMemoryTool20250818InsertCommand) -> str: raise ToolError(f"The path {command.path} is not a file.") content = await _async_read_file_content(full_path, command.path) - lines = content.splitlines() + lines = content.split("\n") if command.insert_line < 0 or command.insert_line > len(lines): raise ToolError( diff --git a/tests/lib/tools/memory_tools/test_filesystem.py b/tests/lib/tools/memory_tools/test_filesystem.py index 0ab3c8fa9..1f0fb083c 100644 --- a/tests/lib/tools/memory_tools/test_filesystem.py +++ b/tests/lib/tools/memory_tools/test_filesystem.py @@ -372,6 +372,32 @@ def test_insert_error_for_invalid_insert_line( ) ) + def test_insert_preserves_non_newline_line_boundaries( + self, sync_local_filesystem_tool: BetaLocalFilesystemMemoryTool + ) -> None: + # U+2028 and form-feed are line boundaries for str.splitlines() but not for "\n". + # view()/str_replace() split on "\n", so insert() must too, otherwise these + # characters are silently rewritten to "\n" and the text lands at the wrong line. + sync_local_filesystem_tool.create( + BetaMemoryTool20250818CreateCommand( + command="create", file_text="alpha\u2028beta\x0cgamma", path="/memories/insert_unicode_test.txt" + ) + ) + + view_result = sync_local_filesystem_tool.view( + BetaMemoryTool20250818ViewCommand(command="view", path="/memories/insert_unicode_test.txt") + ) + assert len(view_result.split(":\n", 1)[1].split("\n")) == 1 + + sync_local_filesystem_tool.insert( + BetaMemoryTool20250818InsertCommand( + command="insert", path="/memories/insert_unicode_test.txt", insert_line=1, insert_text="INSERTED" + ) + ) + + dir_snapshot = get_directory_snapshot(str(sync_local_filesystem_tool.base_path)) + assert dir_snapshot == {"memories/insert_unicode_test.txt": "alpha\u2028beta\x0cgamma\nINSERTED\n"} + def test_delete(self, sync_local_filesystem_tool: BetaLocalFilesystemMemoryTool) -> None: sync_local_filesystem_tool.create( BetaMemoryTool20250818CreateCommand( @@ -862,6 +888,32 @@ async def test_insert_error_for_invalid_insert_line( ) ) + async def test_insert_preserves_non_newline_line_boundaries( + self, async_local_filesystem_tool: BetaAsyncLocalFilesystemMemoryTool + ) -> None: + # U+2028 and form-feed are line boundaries for str.splitlines() but not for "\n". + # view()/str_replace() split on "\n", so insert() must too, otherwise these + # characters are silently rewritten to "\n" and the text lands at the wrong line. + await async_local_filesystem_tool.create( + BetaMemoryTool20250818CreateCommand( + command="create", file_text="alpha\u2028beta\x0cgamma", path="/memories/insert_unicode_test.txt" + ) + ) + + view_result = await async_local_filesystem_tool.view( + BetaMemoryTool20250818ViewCommand(command="view", path="/memories/insert_unicode_test.txt") + ) + assert len(view_result.split(":\n", 1)[1].split("\n")) == 1 + + await async_local_filesystem_tool.insert( + BetaMemoryTool20250818InsertCommand( + command="insert", path="/memories/insert_unicode_test.txt", insert_line=1, insert_text="INSERTED" + ) + ) + + dir_snapshot = get_directory_snapshot(str(async_local_filesystem_tool.base_path)) + assert dir_snapshot == {"memories/insert_unicode_test.txt": "alpha\u2028beta\x0cgamma\nINSERTED\n"} + async def test_delete(self, async_local_filesystem_tool: BetaAsyncLocalFilesystemMemoryTool) -> None: await async_local_filesystem_tool.create( BetaMemoryTool20250818CreateCommand(