fix(session): un item.completed tardif n'efface plus le final agent_message Codex — #240

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).
This commit is contained in:
2026-08-22 17:27:13 +02:00
parent af90c66e48
commit 1962408eda
2 changed files with 50 additions and 11 deletions

View File

@ -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<Option<ReplyEvent>, AgentSessionError> {

View File

@ -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(&[