diff --git a/crates/infrastructure/src/session/codex.rs b/crates/infrastructure/src/session/codex.rs index 39447e8a..3304f529 100644 --- a/crates/infrastructure/src/session/codex.rs +++ b/crates/infrastructure/src/session/codex.rs @@ -699,17 +699,21 @@ impl CodexExecSession { } fn event_supersedes_pending_final(event: &ReplyEvent) -> bool { - !matches!( - event, - ReplyEvent::Heartbeat - | ReplyEvent::RateLimited { .. } - | ReplyEvent::Progress { - progress: ReplyProgress { - kind: ReplyProgressKind::Turn, - .. - } - } - ) + match event { + ReplyEvent::Progress { progress } => { + progress.kind != ReplyProgressKind::Turn + && matches!( + progress.stage, + ReplyProgressStage::Started | ReplyProgressStage::Delta + ) + } + ReplyEvent::ToolActivity { .. } | ReplyEvent::Error { .. } => true, + ReplyEvent::Final { .. } + | ReplyEvent::TextDelta { .. } + | ReplyEvent::Announcement { .. } + | ReplyEvent::Heartbeat + | ReplyEvent::RateLimited { .. } => false, + } } fn agent_message_progress_from_line(line: &str) -> Result, AgentSessionError> { diff --git a/crates/infrastructure/src/session/mod.rs b/crates/infrastructure/src/session/mod.rs index 30af9f35..22dbe2c6 100644 --- a/crates/infrastructure/src/session/mod.rs +++ b/crates/infrastructure/src/session/mod.rs @@ -1385,6 +1385,41 @@ mod tests { ); } + #[tokio::test] + async fn codex_agent_message_survives_late_completed_item_as_final() { + let fake = FakeCli::printing(&[ + r#"{"type":"thread.started","thread_id":"c"}"#, + r#"{"type":"turn.started"}"#, + r#"{"type":"item.completed","item":{"id":"i0","type":"agent_message","text":"réponse exploitable"}}"#, + r#"{"type":"item.completed","item":{"id":"i1","type":"reasoning","text":"post-traitement"}}"#, + r#"{"type":"turn.completed","usage":{}}"#, + ]); + let s = CodexExecSession::new( + SessionId::new_random(), + fake.command(), + "/", + None, + Vec::new(), + Vec::new(), + None, + None, + ); + + let events: Vec<_> = s.send("x").await.expect("send").collect(); + let finals: Vec<_> = events + .iter() + .filter_map(|event| match event { + ReplyEvent::Final { content } => Some(content.as_str()), + _ => None, + }) + .collect(); + assert_eq!( + finals, + vec!["réponse exploitable"], + "a late completed Codex item must not erase the last agent_message" + ); + } + #[tokio::test] async fn codex_zero_agent_message_has_no_final() { let fake = FakeCli::printing(&[