Conversation
This commit removes the undo_edit command from the file_editor tool as it is no longer needed. The project is moving towards higher granularity undo mechanisms like checkpointing. Changes: - Remove undo_edit from CommandLiteral type and related descriptions - Remove undo_edit method and history tracking from FileEditor class - Remove FileHistoryManager and history.py utility file - Update planning_file_editor to remove undo_edit reference - Update tests to remove undo_edit test cases - Add test to verify undo_edit is no longer available Fixes #2063 Co-authored-by: openhands <openhands@all-hands.dev>
all-hands-bot
left a comment
There was a problem hiding this comment.
🟢 Good taste - Clean removal that simplifies the codebase
Analysis:
- Eliminates entire history management subsystem (FileHistoryManager + disk storage)
- Removes stateful tracking from FileEditor - simpler data flow
- The
insertmethod actually gets cleaner (reads file once instead of accumulating lines while iterating) - Thorough removal: all references gone, tests properly updated
Verdict: ✅ Worth merging - This is dead code removal done right. Less state, less complexity, cleaner abstractions.
Key Insight: Good code removal is as valuable as good code addition - this eliminates ~150 LOC of state management that was serving an unused feature.
|
@OpenHands Do a /codereview-roasted on this PR. Anything realistically missing? |
This comment was marked as outdated.
This comment was marked as outdated.
Coverage Report •
|
|||||||||||||||||||||||||||||||||||
|
@OpenHands Do a /codereview-roasted on this PR. Note: tools are
|
This comment was marked as outdated.
This comment was marked as outdated.
enyst
left a comment
There was a problem hiding this comment.
🔴 Needs improvement — Breaking change to persisted event format with no backward compatibility handling
[CRITICAL ISSUES]
1. [definition.py] Breaking Change: Old conversations with undo_edit events will fail to load
This is the big one, and it's the kind of thing that will blow up in production at 2 AM.
FileEditorAction and FileEditorObservation both inherit from Schema, which has model_config = ConfigDict(extra="forbid", frozen=True). The command field uses CommandLiteral = Literal["view", "create", "str_replace", "insert"] — undo_edit is gone.
When a user resumes a conversation that was created before this change, and that conversation contains an ActionEvent with action: FileEditorAction(command="undo_edit") or an ObservationEvent with observation: FileEditorObservation(command="undo_edit"), Pydantic will blow up:
pydantic_core.ValidationError: 1 validation error for FileEditorObservation
command
Input should be 'view', 'create', 'str_replace' or 'insert' [type=literal_error]
This repo has an explicit iron law about this exact situation — see handle_deprecated_model_fields in TextContent and Message. This PR needs the same treatment, but for a Literal value narrowing rather than a removed field.
The pattern here is different from the _DEPRECATED_FIELDS approach because you're narrowing a Literal, not removing a field. The fix should be a @model_validator(mode="before") on both FileEditorAction and FileEditorObservation that intercepts command="undo_edit" and either:
- Converts it to a no-op (e.g.,
command="view") so the event loads gracefully, or - Keeps
"undo_edit"in theCommandLiteraltype but only for deserialization tolerance — the tool schema sent to LLMs (to_mcp_schema()) can still exclude it
The second approach is simpler and more honest. You could also have a separate "storage Literal" vs "active commands Literal" if you want to be clean about it.
This is a "never break userspace" violation. Must fix.
2. [file_cache.py] Dead code left behind
FileCache (openhands-tools/openhands/tools/file_editor/utils/file_cache.py) is now dead code in production. Its only consumer was FileHistoryManager in history.py, which this PR deleted. The file cache module and its test file (tests/tools/file_editor/utils/test_file_cache.py) are dangling corpses.
If you're removing the undo feature and its history, remove ALL of its infrastructure. Don't leave half-rotted dependencies lying around for the next person to trip over.
[IMPROVEMENT OPPORTUNITIES]
3. [editor.py] Extra file read in insert — acceptable, good simplification
The old code avoided an extra read_file() call by collecting history_lines during the file-copy loop. The new code does old_file_text = self.read_file(path) up front, then proceeds to copy line-by-line via tempfile as before. The new approach is cleaner and more correct (the old history_lines approach was fragile). The extra read is negligible for any reasonable file size. 👍
4. [test_early_stopper.py] Minor: test now uses insert which is already a default forbidden command
The test_custom_forbidden_commands test was renamed from undo_edit to insert, but insert is already in the default forbidden commands list. Using a non-default command (e.g., "view") would more clearly test the "custom" aspect. Not blocking.
[TESTING GAPS]
5. No backward compatibility test for loading old events with command="undo_edit"
Per this repo's own conventions (see the test_v1_10_0_text_content_with_enable_truncation pattern), there should be a test that verifies:
- An old
FileEditorObservationdict withcommand="undo_edit"loads successfully - An old
FileEditorActiondict withcommand="undo_edit"loads successfully - Current format without
undo_editstill works
This is not optional — it's the regression proof that your backward compat handler actually works.
VERDICT
❌ Needs rework — The Literal narrowing breaks deserialization of persisted events. This is the exact production failure mode the repo's deprecation policy was written to prevent.
KEY INSIGHT
Removing a value from a Literal type on a serialized Pydantic model is semantically identical to removing a field from a model with extra="forbid" — both break deserialization of old data. The same backward-compat discipline applies.
FileCache's only consumer (FileHistoryManager) was deleted with undo_edit removal, leaving file_cache.py and test_file_cache.py as orphaned dead code. Co-authored-by: openhands <openhands@all-hands.dev>
Old persisted events may contain FileEditorAction or FileEditorObservation with command='undo_edit'. Since these models use Literal types with extra='forbid', narrowing the Literal would break deserialization of old conversations. Add a model_validator(mode='before') on both models that silently migrates 'undo_edit' to 'view' before Pydantic validates the Literal. This keeps the schema clean (LLMs never see undo_edit) while old events load gracefully. The _DEPRECATED_COMMANDS mapping is permanent and must not be removed. Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
enyst
left a comment
There was a problem hiding this comment.
Code Review — PR #2064: Remove undo_edit command from file_editor
Thorough review of the full diff (15 files, -818/+190 lines) after the backward compatibility and cleanup commits.
Assessment
The core removal is clean and well-scoped. The backward compat handling follows the established repo pattern correctly. A couple of minor observations below.
Backward Compatibility ✅
Verified the full deserialization chain works:
FileEditorObservation.model_validate({"command": "undo_edit", ...})→ migrates to"view"✅Observation.model_validate({"kind": "FileEditorObservation", "command": "undo_edit", ...})→ routes throughDiscriminatedUnionMixin, then hits_handle_deprecated_commands→"view"✅- Same for
FileEditorActionthroughActionbase ✅ FileEditorAction.to_mcp_schema()→ enum is["view", "create", "str_replace", "insert"], noundo_editleaked to LLMs ✅
The model_validator(mode="before") runs before the _validate_subtype(mode="wrap") in the MRO, so the deprecated value is migrated before Literal validation. Correct.
editor.py — insert method change ✅
The old code built old_content by collecting history_lines during the tempfile-copy loop (two separate open() + enumerate passes). The new code reads the file once upfront with self.read_file(path). Cleaner, correct, and the extra read is negligible.
Dead code cleanup ✅
FileCache (only consumer was FileHistoryManager) and its test file removed. Good.
Minor observations (non-blocking)
-
Commandtype alias (line 174-179) is now identical toCommandLiteraland has zero importers outside this file. Dead weight — could be removed or consolidated. Not blocking since it's pre-existing. -
test_custom_forbidden_commandsintest_early_stopper.pynow usesinsertas the custom command, butinsertis already in the default forbidden list. Using"view"would better test the "custom" aspect. Minor.
LGTM — would approve if I weren't a contributor. 👍
- Remove Command type alias (identical to CommandLiteral, zero importers) - Use 'view' (not in default list) in test_custom_forbidden_commands so the test actually exercises the custom override behavior Co-authored-by: openhands <openhands@all-hands.dev>
|
@OpenHands Do a /codereview-roasted on this PR |
|
I'm on it! enyst can track my progress at all-hands.dev |
|
I did a /codereview-roasted of PR #2064 (“Remove undo_edit command from file_editor”) and found the main problem isn’t the deletion itself — it’s the “backward compat” approach. What I reviewed / concluded
Checklist against your request
Repo state / pushingNo code changes were made by me, so there is nothing to double-check in |
|
I’m OpenHands (automated reviewer bot). 🔴 Taste Rating: Needs improvement [CRITICAL ISSUES] (must fix)
[IMPROVEMENT OPPORTUNITIES] (should fix)
[STYLE / CLARITY NOTES] (minor)
VERDICT❌ Needs rework before merge: removing KEY INSIGHT: Backward compatibility isn’t “make it not crash”; it’s “don’t corrupt intent.” Silent migration here corrupts intent. |
…nhands@all-hands.dev>
|
Looks like there are a few issues preventing this PR from being merged!
If you'd like me to help, just leave a comment, like Feel free to include any additional details that might help me get this PR into a better state. You can manage your notification settings |
Summary
This PR removes the
undo_editcommand from thefile_editortool as requested in issue #2063. Theundo_editcommand was used by some older LLMs but is no longer needed as the project is moving towards higher granularity undo mechanisms like checkpointing.Changes:
undo_editfromCommandLiteraltype and related descriptions indefinition.pyundo_editmethod and history tracking fromFileEditorclass ineditor.pyFileHistoryManagerandhistory.pyutility file (only used for undo functionality)planning_file_editor/definition.pyto removeundo_editreferenceundo_edittest casesundo_editis no longer available as a commandFixes #2063
Checklist
@enyst can click here to continue refining the PR
Agent Server images for this PR
• GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server
Variants & Base Images
eclipse-temurin:17-jdknikolaik/python-nodejs:python3.12-nodejs22golang:1.21-bookwormPull (multi-arch manifest)
# Each variant is a multi-arch manifest supporting both amd64 and arm64 docker pull ghcr.io/openhands/agent-server:1cdb1c4-pythonRun
All tags pushed for this build
About Multi-Architecture Support
1cdb1c4-python) is a multi-arch manifest supporting both amd64 and arm641cdb1c4-python-amd64) are also available if needed