From 5df11614afafaaf9f093bacc1c80c534fe48411d Mon Sep 17 00:00:00 2001 From: Mohammad Dashti Date: Thu, 20 Aug 2026 11:56:56 -0700 Subject: [PATCH] Stopped panicking on a tool call that never reported a result. Filling in a missing tool output was quiet for a function call but called error_or_panic for a custom tool call or a local shell call, which panics in a debug build. Code mode dispatches custom tool calls, so that is the ordinary case rather than an edge one: kill codex during a tool and the thread could not be resumed at all, because the panic took the runtime worker down while the process stayed alive holding the thread's writer lock. A gap there is expected, not a defect. The recovery code already sat directly after the panic, which is the tell. --- .../core/src/context_manager/history_tests.rs | 25 ++++++++++++++----- .../core/src/context_manager/normalize.rs | 14 ++++++----- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/codex-rs/core/src/context_manager/history_tests.rs b/codex-rs/core/src/context_manager/history_tests.rs index 666c708ef7f6..fd9a4720bfa4 100644 --- a/codex-rs/core/src/context_manager/history_tests.rs +++ b/codex-rs/core/src/context_manager/history_tests.rs @@ -1925,10 +1925,11 @@ fn normalize_adds_missing_output_for_tool_search_call() { ); } -#[cfg(debug_assertions)] +/// Code mode dispatches custom tool calls, so a crash between the call and its result is the +/// ordinary way this gap appears. Filling it in has to be quiet, or a resumed thread cannot be +/// sent to the model at all. #[test] -#[should_panic] -fn normalize_adds_missing_output_for_custom_tool_call_panics_in_debug() { +fn normalize_fills_missing_output_for_custom_tool_call() { let items = vec![ResponseItem::CustomToolCall { id: None, status: None, @@ -1940,12 +1941,24 @@ fn normalize_adds_missing_output_for_custom_tool_call_panics_in_debug() { }]; let mut h = create_history_with_items(items); h.normalize_history(&default_input_modalities()); + + let filled = raw_items(&h); + assert_eq!(filled.len(), 2); + let ResponseItem::CustomToolCallOutput { + call_id, output, .. + } = &filled[1] + else { + panic!( + "expected a synthesized custom tool call output, got {:?}", + filled[1] + ); + }; + assert_eq!(call_id, "tool-x"); + assert_eq!(output.body.to_text().as_deref(), Some("aborted")); } -#[cfg(debug_assertions)] #[test] -#[should_panic] -fn normalize_adds_missing_output_for_local_shell_call_with_id_panics_in_debug() { +fn normalize_fills_missing_output_for_local_shell_call_with_id() { let items = vec![ResponseItem::LocalShellCall { id: None, call_id: Some("shell-1".to_string()), diff --git a/codex-rs/core/src/context_manager/normalize.rs b/codex-rs/core/src/context_manager/normalize.rs index 8ceb8c981027..0213f7d022c0 100644 --- a/codex-rs/core/src/context_manager/normalize.rs +++ b/codex-rs/core/src/context_manager/normalize.rs @@ -18,6 +18,12 @@ const AUDIO_CONTENT_OMITTED_PLACEHOLDER: &str = // Changing this value would change model-visible IDs and invalidate prompt caches. const SYNTHETIC_OUTPUT_ID_NAMESPACE: Uuid = Uuid::from_u128(0x90d38d3e_6a5b_4d52_bfe2_2f1e634bfac4); +/// Fill in the output of any tool call that never reported one. +/// +/// A gap here is expected, not a defect: an interrupt or a crash between dispatching a tool and +/// writing its result leaves the call alone in the transcript, and a resumed thread has to be +/// able to send that history to the model. Every kind of call is treated the same way for that +/// reason, including the custom tool calls that code mode dispatches. pub(crate) fn ensure_call_outputs_present(items: &mut Vec) { let mut function_output_ids = HashSet::new(); let mut tool_search_output_ids = HashSet::new(); @@ -82,9 +88,7 @@ pub(crate) fn ensure_call_outputs_present(items: &mut Vec) ResponseItem::CustomToolCall { id, call_id, .. } if !custom_tool_output_ids.contains(call_id.as_str()) => { - error_or_panic(format!( - "Custom tool call output is missing for call id: {call_id}" - )); + info!("Custom tool call output is missing for call id: {call_id}"); missing_outputs_to_insert.push(( idx, ResponseItemEnvelope::new(ResponseItem::CustomToolCallOutput { @@ -102,9 +106,7 @@ pub(crate) fn ensure_call_outputs_present(items: &mut Vec) call_id: Some(call_id), .. } if !function_output_ids.contains(call_id.as_str()) => { - error_or_panic(format!( - "Local shell call output is missing for call id: {call_id}" - )); + info!("Local shell call output is missing for call id: {call_id}"); missing_outputs_to_insert.push(( idx, ResponseItemEnvelope::new(ResponseItem::FunctionCallOutput {