Move apply-patch file changes into turn items (#20540)

## Why

Apply-patch file changes are now part of the core turn item stream, so
v2 clients can consume the same first-class item lifecycle path used by
other turn items instead of relying on app-server-specific remapping
from legacy patch events.

## What changed

- Added a core `TurnItem::FileChange` carrying apply-patch changes and
completion metadata.
- Updated the apply-patch tool emitter to send `ItemStarted` /
`ItemCompleted` with the new `FileChange` item while preserving legacy
`PatchApplyBegin` / `PatchApplyEnd` fan-out.
- Updated app-server v2 conversion to render the new core item directly
and stopped `event_mapping` from remapping old patch begin/end events
into item notifications.
- Kept thread history reconstruction based on the existing old
apply-patch events for rollout compatibility.

## Verification

- `cargo test -p codex-protocol -p codex-app-server-protocol`
- `cargo test -p codex-core --test all
apply_patch_tool_executes_and_emits_patch_events`
- `cargo test -p codex-app-server bespoke_event_handling`
This commit is contained in:
pakrym-oai
2026-05-01 08:47:18 -07:00
committed by GitHub
parent 0b04d1b3cc
commit f476338f93
10 changed files with 240 additions and 165 deletions
+81 -1
View File
@@ -1841,6 +1841,7 @@ impl HasLegacyEvent for ItemStartedEvent {
call_id: item.id.clone(),
})]
}
TurnItem::FileChange(item) => vec![item.as_legacy_begin_event(self.turn_id.clone())],
_ => Vec::new(),
}
}
@@ -1859,7 +1860,13 @@ pub trait HasLegacyEvent {
impl HasLegacyEvent for ItemCompletedEvent {
fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec<EventMsg> {
self.item.as_legacy_events(show_raw_agent_reasoning)
match &self.item {
TurnItem::FileChange(item) => item
.as_legacy_end_event(self.turn_id.clone())
.into_iter()
.collect(),
_ => self.item.as_legacy_events(show_raw_agent_reasoning),
}
}
}
@@ -3928,6 +3935,7 @@ pub struct CollabResumeEndEvent {
#[cfg(test)]
mod tests {
use super::*;
use crate::items::FileChangeItem;
use crate::items::ImageGenerationItem;
use crate::items::UserMessageItem;
use crate::items::WebSearchItem;
@@ -4630,6 +4638,41 @@ mod tests {
}
}
#[test]
fn item_started_event_from_file_change_emits_patch_begin_event() {
let event = ItemStartedEvent {
thread_id: ThreadId::new(),
turn_id: "turn-1".into(),
item: TurnItem::FileChange(FileChangeItem {
id: "patch-1".into(),
changes: [(
PathBuf::from("new.txt"),
FileChange::Add {
content: "hello".into(),
},
)]
.into_iter()
.collect(),
status: None,
auto_approved: Some(true),
stdout: None,
stderr: None,
}),
};
let legacy_events = event.as_legacy_events(/*show_raw_agent_reasoning*/ false);
assert_eq!(legacy_events.len(), 1);
match &legacy_events[0] {
EventMsg::PatchApplyBegin(event) => {
assert_eq!(event.call_id, "patch-1");
assert_eq!(event.turn_id, "turn-1");
assert!(event.auto_approved);
assert!(event.changes.contains_key(&PathBuf::from("new.txt")));
}
_ => panic!("expected PatchApplyBegin event"),
}
}
#[test]
fn item_completed_event_from_image_generation_emits_end_event() {
let event = ItemCompletedEvent {
@@ -4661,6 +4704,43 @@ mod tests {
}
}
#[test]
fn item_completed_event_from_file_change_emits_patch_end_event() {
let event = ItemCompletedEvent {
thread_id: ThreadId::new(),
turn_id: "turn-1".into(),
item: TurnItem::FileChange(FileChangeItem {
id: "patch-1".into(),
changes: [(
PathBuf::from("new.txt"),
FileChange::Add {
content: "hello".into(),
},
)]
.into_iter()
.collect(),
status: Some(PatchApplyStatus::Completed),
auto_approved: None,
stdout: Some("Done!".into()),
stderr: Some(String::new()),
}),
};
let legacy_events = event.as_legacy_events(/*show_raw_agent_reasoning*/ false);
assert_eq!(legacy_events.len(), 1);
match &legacy_events[0] {
EventMsg::PatchApplyEnd(event) => {
assert_eq!(event.call_id, "patch-1");
assert_eq!(event.turn_id, "turn-1");
assert_eq!(event.stdout, "Done!");
assert!(event.success);
assert_eq!(event.status, PatchApplyStatus::Completed);
assert!(event.changes.contains_key(&PathBuf::from("new.txt")));
}
_ => panic!("expected PatchApplyEnd event"),
}
}
#[test]
fn rollback_failed_error_does_not_affect_turn_status() {
let event = ErrorEvent {