#4661
fix: separate file edit progress ids
Summary
- keep provider tool_call IDs immutable after final response parsing
- add a separate file-edit progress_id used only to correlate streamed placeholders with final file-edit events
- update WebUI file-edit merging to prefer progress_id for file-edit event coalescing while keeping call_id for provider/tool trace correlation
Fixes #4603
Verification
- uv run pytest tests/utils/test_file_edit_events.py -q
- uv run ruff check nanobot/utils/file_edit_events.py nanobot/agent/runner.py nanobot/providers/base.py tests/utils/test_file_edit_events.py
- cd webui && bun run build
Notes
- Targeted Vitest command
bun run test -- useNanobotStreamcurrently fails before collecting tests in this environment becauselocalStorageis undefined insrc/tests/setup.ts.
Activity
3Review — fix: separate file edit progress ids
What this PR does: Implements the #4603 refactor properly:
- Adds
progress_id: str | NonetoFileEditTrackerdataclass. - Adds
progress_id_for(tool_call)method onStreamingFileEditTrackerthat returns the canonical ID without mutating the tool call. - Makes
apply_final_call_idsa deprecated no-op (preserved for compat). - Threads
file_edit_progress_idthrough_execute_tools→_run_tool→prepare_file_edit_tracker. - Adds
file_edit_progress_ids: list[str | None]field toLLMResponse. - Runner now populates
response.file_edit_progress_ids = [live_file_edits.progress_id_for(tc) for tc in response.tool_calls]instead of mutating IDs. - WebUI updated to prefer
progress_idovercall_idfor file-edit event merging (fileEditKey,fileEditProgressKey,fileEditCallKey).
What's good
- Provider
tool_call.idis now immutable after final response parsing. This is the core invariant #4603 asked for. - Separate correlation key (
progress_id) is the right abstraction. It's carried in file-edit events only and doesn't pollute provider protocol state. - Deprecated
apply_final_call_idsis kept as a no-op rather than removed — defensive against any external callers. - WebUI changes are minimal and consistent.
progress_id || call_idfallback handles old events gracefully. - Test coverage is thorough:
progress_id_forreturns canonical,final.idstays asprovider-final-id, multiple file-edit tools with different IDs, vitest test for the WebUI merge path.
Concerns / questions
- Vitest note in PR body is concerning. "Targeted Vitest command
bun run test -- useNanobotStreamcurrently fails before collecting tests in this environment becauselocalStorageis undefined insrc/tests/setup.ts." The author is honest about this, but it means the new WebUI test (upgrades streamed file_edit placeholders by progress_id) wasn't actually run locally. CI must verify it passes. progress_iddefaults tocall_idinbuild_file_edit_pending_event:"progress_id": str(call_id or ""). This is correct for streamed placeholders (wherecall_idis the early ID) but means old clients that don't know aboutprogress_idcan still correlate viacall_id. Good backwards-compat.apply_final_call_idsshould arguably log a deprecation warning when called, to surface any stale callers. Currently silent. Minor.id(tool_call)is used as the dict key inprogress_id_by_callto look up the progress ID per tool call. This works becausetool_callobjects are unique instances, but if any caller copies the list (e.g.,list(tool_calls)) the IDs would differ. Verify the runner doesn't copy between_execute_toolsinvocation and the_run_toolcall. Looking at the diff, the sametool_callobjects flow through, so this is safe — but fragile to future refactors.
Verdict
Approve. This is the cleanest of the architectural PRs in this batch. The only blocker is verifying the vitest test actually passes on CI.
Review — fix: separate file edit progress ids
What this PR does: Implements the #4603 refactor properly:
progress_id: str | NonetoFileEditTrackerdataclass.progress_id_for(tool_call)method onStreamingFileEditTrackerthat returns the canonical ID without mutating the tool call.apply_final_call_idsa deprecated no-op (preserved for compat).file_edit_progress_idthrough_execute_tools→_run_tool→prepare_file_edit_tracker.file_edit_progress_ids: list[str | None]field toLLMResponse.response.file_edit_progress_ids = [live_file_edits.progress_id_for(tc) for tc in response.tool_calls]instead of mutating IDs.progress_idovercall_idfor file-edit event merging (fileEditKey,fileEditProgressKey,fileEditCallKey).What's good
tool_call.idis now immutable after final response parsing. This is the core invariant #4603 asked for.progress_id) is the right abstraction. It's carried in file-edit events only and doesn't pollute provider protocol state.apply_final_call_idsis kept as a no-op rather than removed — defensive against any external callers.progress_id || call_idfallback handles old events gracefully.progress_id_forreturns canonical,final.idstays asprovider-final-id, multiple file-edit tools with different IDs, vitest test for the WebUI merge path.Concerns / questions
bun run test -- useNanobotStreamcurrently fails before collecting tests in this environment becauselocalStorageis undefined insrc/tests/setup.ts." The author is honest about this, but it means the new WebUI test (upgrades streamed file_edit placeholders by progress_id) wasn't actually run locally. CI must verify it passes.progress_iddefaults tocall_idinbuild_file_edit_pending_event:"progress_id": str(call_id or ""). This is correct for streamed placeholders (wherecall_idis the early ID) but means old clients that don't know aboutprogress_idcan still correlate viacall_id. Good backwards-compat.apply_final_call_idsshould arguably log a deprecation warning when called, to surface any stale callers. Currently silent. Minor.id(tool_call)is used as the dict key inprogress_id_by_callto look up the progress ID per tool call. This works becausetool_callobjects are unique instances, but if any caller copies the list (e.g.,list(tool_calls)) the IDs would differ. Verify the runner doesn't copy between_execute_toolsinvocation and the_run_toolcall. Looking at the diff, the sametool_callobjects flow through, so this is safe — but fragile to future refactors.Verdict
Approve. This is the cleanest of the architectural PRs in this batch. The only blocker is verifying the vitest test actually passes on CI.