Skip to content

fix(memory): use split("\n") in insert() to match view()/str_replace() - #1748

Open
otiscuilei wants to merge 1 commit into
anthropics:nextfrom
otiscuilei:fix/memory-tool-insert-splitlines
Open

fix(memory): use split("\n") in insert() to match view()/str_replace()#1748
otiscuilei wants to merge 1 commit into
anthropics:nextfrom
otiscuilei:fix/memory-tool-insert-splitlines

Conversation

@otiscuilei

Copy link
Copy Markdown

Who hits this

Anyone using the built-in filesystem memory tool (BetaLocalFilesystemMemoryTool / BetaAsyncLocalFilesystemMemoryTool) whose memory files contain any of the characters str.splitlines() treats as line boundaries but "\n" does not: carriage return, vertical tab, form feed (\x0c), file/group/record separators (\x1c-\x1e), NEL (\x85), U+2028, or U+2029. When the model issues an insert command against such a file, the insert lands at the wrong line and those characters are silently rewritten to \n.

Root cause

Every memory command reads and splits file content the same way except insert. view() (line 451) and str_replace() (line 526) split on "\n", but both the sync and async insert() used content.splitlines(). splitlines() breaks on ~8 extra Unicode/control boundaries in addition to \n. As a result insert():

  • counted and located lines differently from what view() reports to the model, so an insert_line chosen from the numbers view() showed landed at the wrong offset (text spliced into the middle of what the model saw as a single line), and
  • rejoined the lines with "\n", discarding the boundary characters that were part of the file's content.

Fix

Change content.splitlines() to content.split("\n") in both insert() methods, making line indexing consistent with view() and str_replace() and preserving the file's characters. No behavior change for ordinary \n-delimited files.

Before / after

from anthropic.lib.tools._beta_builtin_memory_tool import BetaLocalFilesystemMemoryTool
from anthropic.types.beta import (
    BetaMemoryTool20250818CreateCommand,
    BetaMemoryTool20250818InsertCommand,
)

tool = BetaLocalFilesystemMemoryTool(base_path="/tmp/mem-demo")

# A file whose content contains a form feed (\x0c). view() reports it as ONE line.
tool.create(BetaMemoryTool20250818CreateCommand(
    command="create", file_text="alpha beta\x0cgamma", path="/memories/f.txt"
))

# Insert after that single line.
tool.insert(BetaMemoryTool20250818InsertCommand(
    command="insert", path="/memories/f.txt", insert_line=1, insert_text="INSERTED"
))

print(repr(open("/tmp/mem-demo/memories/f.txt").read()))
# Before: 'alpha beta\nINSERTED\ngamma\n'   -> \x0c lost, INSERTED spliced into the middle
# After:  'alpha beta\x0cgamma\nINSERTED\n' -> \x0c preserved, INSERTED appended after the line

Tests

Adds one regression test (sync and async) in tests/lib/tools/memory_tools/test_filesystem.py that creates a file containing U+2028 and a form feed, asserts view() numbers it as a single line, runs insert(insert_line=1, ...), and asserts the boundary characters are preserved and the text is appended after the whole line. Fails on the current code, passes with the fix. Full memory-tool suite: 65 passed. ruff check / ruff format clean.

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.
@otiscuilei
otiscuilei requested a review from a team as a code owner July 10, 2026 06:40
@otiscuilei
otiscuilei changed the base branch from main to next July 26, 2026 12:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant