fix(streaming): skip non-file-edit tools in apply_final_call_ids to prevent id corruption
apply_final_call_ids iterated over all final tool calls, including non-file-edit tools like read_file. The greedy path-match in matches_final_tool_call could overwrite a correct unique id with a stale one from a different streaming state, producing duplicate tool_use ids that poison the persisted session. Guard the loop with is_file_edit_tool() so only tracked file-edit tools (write_file, edit_file, apply_patch) are subject to canonical id remapping. Non-file-edit tools keep their authoritative id from get_final_message(). Fixes #4595
This commit is contained in:
@@ -529,6 +529,9 @@ class StreamingFileEditTracker:
|
||||
"""Keep final start/end events keyed to any earlier streamed placeholder."""
|
||||
used_canonicals: set[str] = set()
|
||||
for tool_call in final_tool_calls:
|
||||
name = getattr(tool_call, "name", None)
|
||||
if not is_file_edit_tool(name):
|
||||
continue
|
||||
canonical = self.canonical_call_id_for(tool_call)
|
||||
if canonical and canonical not in used_canonicals:
|
||||
try:
|
||||
|
||||
@@ -412,6 +412,41 @@ def test_streaming_tracker_applies_canonical_call_id_to_final_tool(tmp_path: Pat
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_streaming_tracker_does_not_remap_non_file_edit_final_tool(tmp_path: Path) -> None:
|
||||
events: list[dict] = []
|
||||
|
||||
async def emit(batch: list[dict]) -> None:
|
||||
events.extend(batch)
|
||||
|
||||
async def run() -> None:
|
||||
tracker = StreamingFileEditTracker(workspace=tmp_path, tools={}, emit=emit)
|
||||
await tracker.update({
|
||||
"index": 0,
|
||||
"name": "read_file",
|
||||
"arguments_delta": '{"path":"matched.md"}',
|
||||
})
|
||||
await tracker.update({
|
||||
"index": 1,
|
||||
"name": "write_file",
|
||||
"arguments_delta": '{"path":"matched.md","content":"one\\n',
|
||||
})
|
||||
read_final = SimpleNamespace(
|
||||
id="read-unique",
|
||||
name="read_file",
|
||||
arguments={"path": "matched.md"},
|
||||
)
|
||||
write_final = SimpleNamespace(
|
||||
id="write-final",
|
||||
name="write_file",
|
||||
arguments={"path": "matched.md", "content": "one\n"},
|
||||
)
|
||||
tracker.apply_final_call_ids([read_final, write_final])
|
||||
assert read_final.id == "read-unique"
|
||||
assert write_final.id == "idx:1"
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_streaming_tracker_does_not_restore_duplicate_canonical_ids(tmp_path: Path) -> None:
|
||||
events: list[dict] = []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user