mirror of
https://github.com/openai/codex.git
synced 2026-11-01 19:33:35 +00:00
Add opt-in nonfatal handling for clock read failures (#45825)
## Why Clock provider failures can abort a turn while preparing time context or running clock tools. Allow turns to continue with an explicit indication that the current time is unavailable. ## What changed - Add `features.nonfatal_clock_read_errors`, disabled by default, to report clock failures to the model without failing the turn. - Emit a generic `failed to read current time` notice for context reads and tool errors, without exposing provider error details. Deduplicate context notices per turn and compaction window, and remove inherited notices from forked subagent context. - Omit unavailable environment dates and explicitly clear previously visible dates with `<current_date status="unavailable" />`. - Return external sleep clock failures to the model when the feature is enabled, preserving sleep item completion notifications. ## Testing Add coverage for continued inference after clock failures, notice deduplication across compaction, subagent notice filtering, environment date removal and recovery, and sleep failures during initial and polling reads. GitOrigin-RevId: a39c3723c06a6f786d8ad59d667e8b9f626c973e
This commit is contained in:
@@ -5,19 +5,23 @@ use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::CurrentTimeReadResponse;
|
||||
use codex_app_server_protocol::ItemCompletedNotification;
|
||||
use codex_app_server_protocol::ItemStartedNotification;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::SleepItem;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnCompletedNotification;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
use core_test_support::responses;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use std::time::Duration;
|
||||
use tempfile::TempDir;
|
||||
use test_case::test_case;
|
||||
use tokio::time::timeout;
|
||||
|
||||
use super::analytics::captured_analytics_events;
|
||||
@@ -159,13 +163,19 @@ async fn clock_tools_emit_control_tool_analytics() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(0; "initial read fails")]
|
||||
#[test_case(1; "polling read fails")]
|
||||
#[test_case(3; "successful reads")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn external_sleep_polls_current_time_and_emits_items() -> Result<()> {
|
||||
async fn external_sleep_polls_current_time_and_emits_items(
|
||||
successful_sleep_reads: usize,
|
||||
) -> Result<()> {
|
||||
const CALL_ID: &str = "sleep-1";
|
||||
const DURATION_MS: u64 = 2_000;
|
||||
let read_fails = successful_sleep_reads < 3;
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
responses::mount_sse_sequence(
|
||||
let model = responses::mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
responses::sse(vec![
|
||||
@@ -189,13 +199,15 @@ async fn external_sleep_polls_current_time_and_emits_items() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
MockResponsesConfig::new(&server.uri())
|
||||
.with_root_config("include_environment_context = false")
|
||||
.with_extra_config(
|
||||
r#"[features.current_time_reminder]
|
||||
.with_extra_config(&format!(
|
||||
r#"[features]
|
||||
nonfatal_clock_read_errors = {read_fails}
|
||||
[features.current_time_reminder]
|
||||
enabled = true
|
||||
sleep_tool = true
|
||||
clock_source = "external"
|
||||
"#,
|
||||
)
|
||||
))
|
||||
.write(codex_home.path())?;
|
||||
|
||||
let mut mcp = TestAppServer::builder()
|
||||
@@ -228,21 +240,51 @@ clock_source = "external"
|
||||
// Read once for the initial reminder, then once to establish the sleep deadline.
|
||||
respond_to_current_time_read(&mut mcp, &thread.id, CURRENT_TIME_AT).await?;
|
||||
let started = wait_for_sleep_started(&mut mcp, CALL_ID).await?;
|
||||
respond_to_current_time_read(&mut mcp, &thread.id, CURRENT_TIME_AT).await?;
|
||||
|
||||
// The first poll remains below the deadline, so the provider must request time again.
|
||||
respond_to_current_time_read(&mut mcp, &thread.id, CURRENT_TIME_AT + 1).await?;
|
||||
respond_to_current_time_read(&mut mcp, &thread.id, CURRENT_TIME_AT + 2).await?;
|
||||
for current_time_at in [CURRENT_TIME_AT, CURRENT_TIME_AT + 1, CURRENT_TIME_AT + 2]
|
||||
.into_iter()
|
||||
.take(successful_sleep_reads)
|
||||
{
|
||||
respond_to_current_time_read(&mut mcp, &thread.id, current_time_at).await?;
|
||||
}
|
||||
if read_fails {
|
||||
let request = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_request_message(),
|
||||
)
|
||||
.await??;
|
||||
let ServerRequest::CurrentTimeRead { request_id, params } = request else {
|
||||
panic!("expected clock read, got {request:?}");
|
||||
};
|
||||
assert_eq!(params.thread_id, thread.id);
|
||||
mcp.send_error(
|
||||
request_id,
|
||||
JSONRPCErrorError {
|
||||
code: -32_000,
|
||||
message: "PRIVATE_CLOCK_ERROR".to_string(),
|
||||
data: None,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let completed = wait_for_sleep_completed(&mut mcp, CALL_ID).await?;
|
||||
|
||||
// The next inference boundary reads the same external clock after the sleep completes.
|
||||
// The next inference boundary reads the same external clock after the tool returns.
|
||||
respond_to_current_time_read(&mut mcp, &thread.id, CURRENT_TIME_AT + 2).await?;
|
||||
timeout(
|
||||
let turn_completed: TurnCompletedNotification = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
mcp.read_notification("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(turn_completed.turn.status, TurnStatus::Completed);
|
||||
if read_fails {
|
||||
let requests = model.requests();
|
||||
assert_eq!(requests.len(), 2);
|
||||
assert_eq!(
|
||||
requests[1].function_call_output_text(CALL_ID).as_deref(),
|
||||
Some("failed to read current time")
|
||||
);
|
||||
}
|
||||
|
||||
let expected_item = ThreadItem::Sleep(SleepItem {
|
||||
id: CALL_ID.to_string(),
|
||||
|
||||
Reference in New Issue
Block a user