fix(tools): align claude-memory insert with memory_20250818 line semantics

Anthropic's memory_20250818 spec defines insert as: insert_text is inserted
AFTER line insert_line, 0 inserts at the beginning of the file, and the valid
range is [0, n_lines]. The implementation treated insert_line as a 1-based
insert-BEFORE index with range [1, n_lines + 1].

Since the caller of this tool is Claude itself, which is trained on the spec
semantics, every model-driven insert landed one line earlier than intended,
insert_line: 0 (insert at top of file) was rejected as invalid, and
insert_line: n_lines (append) inserted before the last line instead of after
it.

Fix the validation range to [0, n_lines], splice at insert_line directly
(0-based insert-after), and update the error and success messages to match.
One existing tool-operations test encoded the old insert-before behavior; its
insert_line is adjusted so its expected output is unchanged under spec
semantics. Adds four regression tests covering top-of-file, middle,
append, and both out-of-range directions.
This commit is contained in:
Rohit 2026-07-18 17:56:12 +05:30
parent bec73e28ad
commit a949311131
3 changed files with 83 additions and 7 deletions

View file

@ -149,6 +149,80 @@ describe("ClaudeMemoryTool exact-file matching", () => {
})
})
describe("ClaudeMemoryTool insert line semantics", () => {
let tool: ClaudeMemoryTool
beforeEach(() => {
searchExecute.mockReset()
addMock.mockReset()
mockDocument(FILE_CONTENT)
tool = new ClaudeMemoryTool("test-api-key")
})
// The memory_20250818 spec: insert_text is inserted AFTER line insert_line,
// 0 inserts at the beginning of the file, and the valid range is [0, n_lines].
it("insert_line: 0 inserts at the beginning of the file", async () => {
const result = await tool.handleCommand({
command: "insert",
path: FILE_PATH,
insert_line: 0,
insert_text: "header",
})
expect(result.success).toBe(true)
const stored = addMock.mock.calls[0]?.[0]?.content as string
expect(stored).toBe("header\nline1\nline2\nline3\nline4\nline5")
})
it("inserts AFTER the given line, not before it", async () => {
const result = await tool.handleCommand({
command: "insert",
path: FILE_PATH,
insert_line: 2,
insert_text: "after2",
})
expect(result.success).toBe(true)
const stored = addMock.mock.calls[0]?.[0]?.content as string
// Regression guard: the old 1-based insert-BEFORE landed this one line early.
expect(stored).toBe("line1\nline2\nafter2\nline3\nline4\nline5")
})
it("insert_line: n_lines appends at the end of the file", async () => {
const result = await tool.handleCommand({
command: "insert",
path: FILE_PATH,
insert_line: 5,
insert_text: "tail",
})
expect(result.success).toBe(true)
const stored = addMock.mock.calls[0]?.[0]?.content as string
expect(stored).toBe("line1\nline2\nline3\nline4\nline5\ntail")
})
it("rejects insert_line outside [0, n_lines] without writing", async () => {
const below = await tool.handleCommand({
command: "insert",
path: FILE_PATH,
insert_line: -1,
insert_text: "x",
})
expect(below.success).toBe(false)
expect(below.error).toContain("[0, 5]")
const above = await tool.handleCommand({
command: "insert",
path: FILE_PATH,
insert_line: 6,
insert_text: "x",
})
expect(above.success).toBe(false)
expect(addMock).not.toHaveBeenCalled()
})
})
describe("ClaudeMemoryTool str_replace replacement literalness", () => {
let tool: ClaudeMemoryTool

View file

@ -437,16 +437,18 @@ export class ClaudeMemoryTool {
readResult.document.raw || readResult.document.content || ""
const lines = originalContent.split("\n")
// Validate line number
if (insertLine < 1 || insertLine > lines.length + 1) {
// Validate line number. Per the memory_20250818 spec the valid range
// is [0, n_lines]: text is inserted AFTER `insert_line`, and 0 means
// the beginning of the file.
if (insertLine < 0 || insertLine > lines.length) {
return {
success: false,
error: `Invalid line number: ${insertLine}. File has ${lines.length} lines.`,
error: `Invalid insert_line parameter: ${insertLine}. It should be within the range of lines of the file: [0, ${lines.length}]`,
}
}
// Insert the text (insertLine is 1-based)
lines.splice(insertLine - 1, 0, insertText)
// Insert the text after line `insertLine` (0-based insert-after)
lines.splice(insertLine, 0, insertText)
const newContent = lines.join("\n")
// Update the document
@ -464,7 +466,7 @@ export class ClaudeMemoryTool {
return {
success: true,
content: `Text inserted at line ${insertLine} in file: ${filePath}`,
content: `Text inserted after line ${insertLine} in file: ${filePath}`,
}
} catch (error) {
return {

View file

@ -211,7 +211,7 @@ describe("ClaudeMemoryTool", () => {
const result = await tool.handleCommand({
command: "insert",
path: FILE_PATH,
insert_line: 2,
insert_line: 1,
insert_text: "",
})