From 1962408edabddfafdb4d61d925e3dc8ceea55a0c Mon Sep 17 00:00:00 2001 From: Blomios Date: Sat, 22 Aug 2026 17:27:13 +0200 Subject: [PATCH] =?UTF-8?q?fix(session):=20un=20item.completed=20tardif=20?= =?UTF-8?q?n'efface=20plus=20le=20final=20agent=5Fmessage=20Codex=20?= =?UTF-8?q?=E2=80=94=20#240?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit event_supersedes_pending_final ne fait plus annuler le final en attente par tout événement autre que Heartbeat/RateLimited/turn-progress: seuls un progress non-turn Started/Delta, un ToolActivity ou un Error annulent. Le buffering agent_message survive ainsi jusqu'au flush a turn.completed (regréssion codex_agent_message_survives_late_completed_item_as_final). --- crates/infrastructure/src/session/codex.rs | 26 +++++++++------- crates/infrastructure/src/session/mod.rs | 35 ++++++++++++++++++++++ 2 files changed, 50 insertions(+), 11 deletions(-) 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(&[