fix: undo() peeks without popping, breaking repeated undo and leaking redo stack#71
Conversation
… redo stack undo() used self.undo_stack.back() (peek) instead of pop_back(), so repeated undo always returned the same checkpoint while pushing to the redo stack unboundedly. Additionally, redo() did not save the current state to the undo stack, breaking the undo/redo cycle. Changes: - undo(): change back() to pop_back(), return owned Checkpoint - redo(): accept current_turn/current_messages params, save current state to undo stack before popping from redo stack - Update process_undo/process_redo callers in agent_loop.rs - Add tests for repeated undo, undo/redo cycling, stack size invariant
Summary of ChangesHello @AI-Reviewer-QS, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request addresses critical issues in the undo/redo mechanism, ensuring that these operations behave as expected. The changes prevent state loss, allow for proper traversal through historical states, and maintain the integrity of the undo and redo stacks. The core logic of state management for these features has been refactored and thoroughly tested to provide a robust and reliable experience. Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request effectively addresses critical bugs in the undo/redo functionality, ensuring the undo operation correctly pops from the stack and the redo operation properly saves the current state. However, a security audit identified two significant issues: a high-severity deadlock risk in agent_loop.rs caused by inconsistent locking order between Session and UndoManager, and a critical IDOR vulnerability in the maybe_hydrate_thread method that allows unauthorized access to conversation threads. It is strongly recommended to implement ownership checks in maybe_hydrate_thread and standardize the locking order to prevent deadlocks. Additionally, there is a suggestion to refactor a small piece of duplicated code for improved maintainability.
Address review feedback: - Standardize lock order (Session before UndoManager) in process_undo and process_redo to match process_user_input and prevent deadlocks - Extract push_undo() helper to deduplicate push-and-trim logic shared by checkpoint() and redo()
|
@ilblackdragon can you help me do a code review for this PR? Thank you! |
Code Review: Fix undo() peeks without poppingPR Details
SummaryThis PR fixes a critical bug in the undo/redo stack implementation where
The fix properly changes Pros✅ Correct core fix: Changing ✅ Symmetric undo/redo design: Both operations now properly save current state before restoring from their respective stacks, maintaining a consistent state management approach. ✅ Excellent test coverage: Added three comprehensive tests:
✅ Deadlock prevention: Reordered lock acquisition in both ✅ Reduced cloning: Removed unnecessary ✅ Code cleanup: Extracted ✅ Return type ownership: Changing from Concerns
Suggestions
ConclusionStatus: ✅ Approve This is a well-structured PR that correctly fixes a critical bug with proper test coverage. The changes are well-thought-out, with good attention to deadlock prevention and performance optimization. The test additions significantly increase confidence in the fix and prevent regression. The minor concerns are mostly about documentation and verification rather than actual bugs. I recommend merging once the documentation updates are applied (or as a follow-up). |
Address review feedback requesting documentation about the ownership semantics of undo/redo parameters and the stack size invariant.
|
Thanks for the thorough review @tribendu! Addressed the actionable feedback in the latest push (6530614): Done:
Already addressed in prior commit (05db445):
Out of scope / noted:
|
ilblackdragon
left a comment
There was a problem hiding this comment.
Review: Approve
This PR correctly identifies and fixes two real bugs in UndoManager:
Bug Fixes (Correct)
-
undo()peeked instead of popping --undo_stack.back()returned a reference without removing the entry, so repeated undos always returned the same checkpoint and the redo stack grew unboundedly. Changing topop_back()and returning an ownedCheckpointis the right fix. -
redo()did not save current state to undo stack -- After redoing, the pre-redo state was lost, breaking undo-after-redo cycles. Addingcurrent_turnandcurrent_messagesparameters toredo()and pushing them onto the undo stack before popping redo mirrors the symmetric design ofundo().
Lock Ordering Fix (Correct)
The PR also fixes a latent deadlock hazard in process_undo and process_redo. Previously:
process_undo/process_redo: lockedundo_mgrfirst, thensessionprocess_user_input(checkpoint code at line ~816): lockedsessionfirst, thenundo_mgr
Opposite lock ordering on two mutexes is a classic deadlock setup. The PR moves session.lock().await before get_undo_manager + undo_mgr.lock().await in both functions, matching the order used in process_user_input. This is a valuable incidental fix.
Code Quality
- The
push_undo()helper consolidates the push-and-trim logic previously duplicated incheckpoint(), reducing the chance of future divergence. Clean refactor. - Return type change from
Option<&Checkpoint>toOption<Checkpoint>eliminates the need for.clone()in the caller and the awkward "extract values before consuming the reference" dance. Good simplification. - The
redo()early return guard (if self.redo_stack.is_empty()) is necessary -- without it, callingredo()on an empty redo stack would still push to the undo stack, silently corrupting state. - Doc comments on the struct and both methods clearly describe the invariant (
undo_count() + redo_count()stays constant across undo/redo cycles). Helpful for future maintainers.
Test Coverage
The three new tests are well-designed:
test_repeated_undo_advances_through_stack-- directly reproduces the original bug (two undos must return different checkpoints)test_undo_redo_cycle_preserves_state-- verifies the full undo->redo->undo round-triptest_undo_redo_stack_sizes_consistent-- verifies the count invariant documented on the struct
Existing tests are correctly updated for the new signatures.
Minor Observations (Non-blocking)
-
pop_undo()is now dead code -- It has zero callers in the codebase (before or after this PR). Consider removing it or adding#[allow(dead_code)]with a comment explaining its purpose if it's part of a public API contract. Not blocking since clippy CI passed (it may be suppressed by thepubvisibility). -
Trimming in
push_undoduringredo()-- Whenmax_checkpointsis small and a user does many redo operations,push_undowill trim the oldest undo entries. This is correct bounded-memory behavior, but it means redo can silently reduce how far back you can undo. The doc comment could mention this, but the current behavior is reasonable.
CI is green (fmt, clippy, tests all pass). The core fix is correct, the lock ordering improvement is a welcome bonus, and test coverage is thorough.
GitHub PR Review Batch 3 - nearai/ironclawPR #71: Fix undo/redo deadlock and checkpoint behaviorSummaryThis PR addresses critical concurrency issues in the undo/redo system. The main changes are:
Pros
Concerns
Suggestions
PR #66: Update README architecture diagram to use Unicode box drawingSummaryThis PR updates the README.md architecture diagram from ASCII art to Unicode box drawing characters. The change improves readability and visual appeal of the system architecture diagram, using characters like Pros
Concerns
Suggestions
PR #63: Memory Guardian and Cognitive RoutinesSummaryThis is a large PR (894 lines of new code) adding a comprehensive memory management system with two layers: Cognitive Routines (Prompt-Level):
Memory Guardian (System-Level):
The PR also adds line number tracking to memory chunks for citation support (V9 migration), checkpoint tracker to Thread struct, and extensive testing (15 new tests). Pros
Concerns
Suggestions
PR #62: Add Tinfoil private inference providerSummaryThis PR adds support for Tinfoil as a new LLM backend. The changes include:
Pros
Concerns
Suggestions
PR #61: Add PostgreSQL test workflow for CISummaryThis PR adds a PostgreSQL with pgvector service to the GitHub Actions test workflow. The changes enable running tests against a real PostgreSQL database instead of libSQL-only testing. The service is configured with pgvector/pgvector:pg16 image and health checks. Pros
Concerns
Suggestions
PR #57: Sandbox job monitoring and credential grants persistenceSummaryThis is a large PR (~6333 lines touched) introducing several features:
Pros
Concerns
Suggestions
SummaryHighest Quality PRs
Needs Splitting
Minor Concerns
Recommendations
|
… redo stack (nearai#71) * fix: undo() peeks without popping, breaking repeated undo and leaking redo stack undo() used self.undo_stack.back() (peek) instead of pop_back(), so repeated undo always returned the same checkpoint while pushing to the redo stack unboundedly. Additionally, redo() did not save the current state to the undo stack, breaking the undo/redo cycle. Changes: - undo(): change back() to pop_back(), return owned Checkpoint - redo(): accept current_turn/current_messages params, save current state to undo stack before popping from redo stack - Update process_undo/process_redo callers in agent_loop.rs - Add tests for repeated undo, undo/redo cycling, stack size invariant * fix: standardize lock ordering and extract push_undo helper Address review feedback: - Standardize lock order (Session before UndoManager) in process_undo and process_redo to match process_user_input and prevent deadlocks - Extract push_undo() helper to deduplicate push-and-trim logic shared by checkpoint() and redo() * docs: add move-semantics notes and stack invariant to UndoManager Address review feedback requesting documentation about the ownership semantics of undo/redo parameters and the stack size invariant. --------- Co-authored-by: Yi LIU <yi@quantstamp.com> Co-authored-by: firat.sertgoz <f@nuff.tech>
… redo stack (nearai#71) * fix: undo() peeks without popping, breaking repeated undo and leaking redo stack undo() used self.undo_stack.back() (peek) instead of pop_back(), so repeated undo always returned the same checkpoint while pushing to the redo stack unboundedly. Additionally, redo() did not save the current state to the undo stack, breaking the undo/redo cycle. Changes: - undo(): change back() to pop_back(), return owned Checkpoint - redo(): accept current_turn/current_messages params, save current state to undo stack before popping from redo stack - Update process_undo/process_redo callers in agent_loop.rs - Add tests for repeated undo, undo/redo cycling, stack size invariant * fix: standardize lock ordering and extract push_undo helper Address review feedback: - Standardize lock order (Session before UndoManager) in process_undo and process_redo to match process_user_input and prevent deadlocks - Extract push_undo() helper to deduplicate push-and-trim logic shared by checkpoint() and redo() * docs: add move-semantics notes and stack invariant to UndoManager Address review feedback requesting documentation about the ownership semantics of undo/redo parameters and the stack size invariant. --------- Co-authored-by: Yi LIU <yi@quantstamp.com> Co-authored-by: firat.sertgoz <f@nuff.tech>
Summary
UndoManager::undo()callsself.undo_stack.back()which peeks without removing, so repeated undo always returns the same checkpoint while the redo stack grows unboundedlyUndoManager::redo()does not save the current state to the undo stack, so undo-after-redo loses the pre-redo stateundo()to usepop_back()andredo()to accept current state parameters and push to undo stack before popping redoagent_loop.rsto match the new signaturesTest plan
test_repeated_undo_advances_through_stack— verifies two undos return different checkpoints and stack shrinkstest_undo_redo_cycle_preserves_state— verifies undo→redo→undo cycles correctlytest_undo_redo_stack_sizes_consistent— verifies undo_count + redo_count stays constantcargo clippy --all --all-featuresclean