From 1962408edabddfafdb4d61d925e3dc8ceea55a0c Mon Sep 17 00:00:00 2001 From: Blomios Date: Sat, 22 Aug 2026 17:27:13 +0200 Subject: [PATCH 1/2] =?UTF-8?q?fix(session):=20un=20item.completed=20tardi?= =?UTF-8?q?f=20n'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(&[ From 41a850f2e918b938f8c4112724b17598c8ea555a Mon Sep 17 00:00:00 2001 From: Blomios Date: Sat, 22 Aug 2026 17:27:16 +0200 Subject: [PATCH 2/2] =?UTF-8?q?fix(chat):=20bulles=20d'erreur=20=C3=A9tiqu?= =?UTF-8?q?et=C3=A9es=20par=20type=20de=20session=20structur=C3=A9e=20?= =?UTF-8?q?=E2=80=94=20#240?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L'erreur « La réponse du modèle n'a pas pu être finalisée. » indique désormais l'adapter en cause (Session Codex / Claude / OpenCode / HTTP) pour lever l'ambiguïté constatée sur GameTime entre session Codex et Claude. --- .../agents/CustomAgentChatView.test.tsx | 1 + .../features/agents/CustomAgentChatView.tsx | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/frontend/src/features/agents/CustomAgentChatView.test.tsx b/frontend/src/features/agents/CustomAgentChatView.test.tsx index dfbea426..45f84f31 100644 --- a/frontend/src/features/agents/CustomAgentChatView.test.tsx +++ b/frontend/src/features/agents/CustomAgentChatView.test.tsx @@ -383,6 +383,7 @@ describe("CustomAgentChatView", () => { expect(agentBubble).toBeTruthy(); expect(errorBubble).toBeTruthy(); + expect(within(errorBubble!).getByText("Session Codex")).toBeTruthy(); expect( within(agentBubble!).getByText(expectedChatTime(firstDeltaReceivedAtMs)), ).toBeTruthy(); diff --git a/frontend/src/features/agents/CustomAgentChatView.tsx b/frontend/src/features/agents/CustomAgentChatView.tsx index 78aa665b..c2905929 100644 --- a/frontend/src/features/agents/CustomAgentChatView.tsx +++ b/frontend/src/features/agents/CustomAgentChatView.tsx @@ -144,6 +144,23 @@ function describeOpenFailure(e: unknown): string { return message || "Cause technique non disponible."; } +function structuredSessionLabel( + adapter: AgentProfile["structuredAdapter"], +): string | null { + switch (adapter) { + case "codex": + return "Session Codex"; + case "claude": + return "Session Claude"; + case "openCode": + return "Session OpenCode"; + case "openAiCompatible": + return "Session HTTP"; + default: + return null; + } +} + function instrumentationError(e: unknown): Record { if (e && typeof e === "object") { const err = e as GatewayError & { name?: string }; @@ -1018,6 +1035,7 @@ export function CustomAgentChatView({ [agentId, conversationId, projectId], ); const displayTurns = turns; + const sessionLabel = structuredSessionLabel(profile.structuredAdapter); const [requesterDelegation, setRequesterDelegation] = useState({ active: false }); @@ -2162,6 +2180,7 @@ export function CustomAgentChatView({ key={index} turn={turn} requesterName={agentName} + sessionLabel={sessionLabel} expandedProgressKeys={expandedProgressKeys} onToggleProgressExpanded={(key) => setExpandedProgressKeys((prev) => { @@ -2352,11 +2371,13 @@ export function CustomAgentChatView({ function ChatBubble({ turn, requesterName, + sessionLabel, expandedProgressKeys, onToggleProgressExpanded, }: { turn: ChatTurn; requesterName: string; + sessionLabel: string | null; expandedProgressKeys: Set; onToggleProgressExpanded: (key: string) => void; }) { @@ -2406,6 +2427,9 @@ function ChatBubble({ labelClassName="font-semibold text-danger" /> )} + {turn.role === "error" && sessionLabel && ( +

{sessionLabel}

+ )} {turn.role === "unknown" ? "Chunk inconnu reçu: " : ""} {turn.text}