Sometimes the action log would not auto-accept agent edits when commiting. Gpt-5.4 identified this race condition: This fixes a race where `keep_committed_edits` could run after `head_commit` changed but before the new git base text had been applied, leaving committed agent edits marked as unreviewed; `ActionLog` now waits for an explicit `BufferDiffEvent::BaseTextChanged` instead of inferring readiness from generic `DiffChanged` activity, so it only accepts edits after the diff base itself is actually updated. - `ReloadGitState` updates `head_commit` before `ReloadBufferDiffBases` finishes loading and applying the new HEAD text. - In that gap, an unrelated `DiffChanged` can fire from a normal diff recalculation. - The old logic treated that event as the commit signal and ran `keep_committed_edits` too early. - `keep_committed_edits` then read stale diff base text, so it failed to match the committed agent edits. - When the real base-text update arrived later, the HEAD had already been overwritten (`old_head`), and the edits stayed unreviewed. Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Closes #ISSUE Release Notes: - Fixed an issue where committing agent written code would sometimes not mark edits as accepted
Showing the changes to crates/buffer_diff/src/buffer_diff.rs (1 of 4 files). Show all
Unified · Split
| @@ -1515,11 +1515,17 @@ pub struct DiffChanged { · 1514 unmodified lines | |||
| 1515 | 1515 | ||
| 1516 | #[derive(Clone, Debug)] | 1516 | #[derive(Clone, Debug)] |
| 1517 | pub enum BufferDiffEvent { | 1517 | pub enum BufferDiffEvent { |
| 1518 | BaseTextChanged, | ||
| 1518 | DiffChanged(DiffChanged), | 1519 | DiffChanged(DiffChanged), |
| 1519 | LanguageChanged, | 1520 | LanguageChanged, |
| 1520 | HunksStagedOrUnstaged(Option<Rope>), | 1521 | HunksStagedOrUnstaged(Option<Rope>), |
| 1521 | } | 1522 | } |
| 1522 | 1523 | ||
| 1524 | struct SetSnapshotResult { | ||
| 1525 | change: DiffChanged, | ||
| 1526 | base_text_changed: bool, | ||
| 1527 | } | ||
| 1528 | |||
| 1523 | impl EventEmitter<BufferDiffEvent> for BufferDiff {} | 1529 | impl EventEmitter<BufferDiffEvent> for BufferDiff {} |
| 1524 | 1530 | ||
| 1525 | impl BufferDiff { | 1531 | impl BufferDiff { |
| @@ -1784,7 +1790,7 @@ impl BufferDiff { · 258 unmodified lines | |||
| 1784 | secondary_diff_change: Option<Range<Anchor>>, | 1790 | secondary_diff_change: Option<Range<Anchor>>, |
| 1785 | clear_pending_hunks: bool, | 1791 | clear_pending_hunks: bool, |
| 1786 | cx: &mut Context<Self>, | 1792 | cx: &mut Context<Self>, |
| 1787 | ) -> impl Future<Output = DiffChanged> + use<> { | 1793 | ) -> impl Future<Output = SetSnapshotResult> + use<> { |
| 1788 | log::debug!("set snapshot with secondary {secondary_diff_change:?}"); | 1794 | log::debug!("set snapshot with secondary {secondary_diff_change:?}"); |
| 1789 | 1795 | ||
| 1790 | let old_snapshot = self.snapshot(cx); | 1796 | let old_snapshot = self.snapshot(cx); |
| @@ -1904,10 +1910,13 @@ impl BufferDiff { · 113 unmodified lines | |||
| 1904 | if let Some(parsing_idle) = parsing_idle { | 1910 | if let Some(parsing_idle) = parsing_idle { |
| 1905 | parsing_idle.await; | 1911 | parsing_idle.await; |
| 1906 | } | 1912 | } |
| 1907 | DiffChanged { | 1913 | SetSnapshotResult { |
| 1908 | changed_range, | 1914 | change: DiffChanged { |
| 1909 | base_text_changed_range, | 1915 | changed_range, |
| 1910 | extended_range, | 1916 | base_text_changed_range, |
| 1917 | extended_range, | ||
| 1918 | }, | ||
| 1919 | base_text_changed, | ||
| 1911 | } | 1920 | } |
| 1912 | } | 1921 | } |
| 1913 | } | 1922 | } |
| @@ -1938,12 +1947,15 @@ impl BufferDiff { · 24 unmodified lines | |||
| 1938 | ); | 1947 | ); |
| 1939 | 1948 | ||
| 1940 | cx.spawn(async move |this, cx| { | 1949 | cx.spawn(async move |this, cx| { |
| 1941 | let change = fut.await; | 1950 | let result = fut.await; |
| 1942 | this.update(cx, |_, cx| { | 1951 | this.update(cx, |_, cx| { |
| 1943 | cx.emit(BufferDiffEvent::DiffChanged(change.clone())); | 1952 | if result.base_text_changed { |
| 1953 | cx.emit(BufferDiffEvent::BaseTextChanged); | ||
| 1954 | } | ||
| 1955 | cx.emit(BufferDiffEvent::DiffChanged(result.change.clone())); | ||
| 1944 | }) | 1956 | }) |
| 1945 | .ok(); | 1957 | .ok(); |
| 1946 | change.changed_range | 1958 | result.change.changed_range |
| 1947 | }) | 1959 | }) |
| 1948 | } | 1960 | } |
| 1949 | 1961 | ||
| @@ -2019,8 +2031,11 @@ impl BufferDiff { · 69 unmodified lines | |||
| 2019 | let fg_executor = cx.foreground_executor().clone(); | 2031 | let fg_executor = cx.foreground_executor().clone(); |
| 2020 | let snapshot = fg_executor.block_on(fut); | 2032 | let snapshot = fg_executor.block_on(fut); |
| 2021 | let fut = self.set_snapshot_with_secondary_inner(snapshot, buffer, None, false, cx); | 2033 | let fut = self.set_snapshot_with_secondary_inner(snapshot, buffer, None, false, cx); |
| 2022 | let change = fg_executor.block_on(fut); | 2034 | let result = fg_executor.block_on(fut); |
| 2023 | cx.emit(BufferDiffEvent::DiffChanged(change)); | 2035 | if result.base_text_changed { |
| 2036 | cx.emit(BufferDiffEvent::BaseTextChanged); | ||
| 2037 | } | ||
| 2038 | cx.emit(BufferDiffEvent::DiffChanged(result.change)); | ||
| 2024 | } | 2039 | } |
| 2025 | 2040 | ||
| 2026 | pub fn base_text_buffer(&self) -> &Entity<language::Buffer> { | 2041 | pub fn base_text_buffer(&self) -> &Entity<language::Buffer> { |