mirror of
https://github.com/openai/codex.git
synced 2026-11-07 20:30:33 +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:
@@ -31,6 +31,7 @@ use core_test_support::assert_regex_match;
|
||||
use core_test_support::responses::ResponsesRequest;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_completed_with_tokens;
|
||||
use core_test_support::responses::ev_function_call;
|
||||
use core_test_support::responses::ev_function_call_with_namespace;
|
||||
use core_test_support::responses::ev_response_created;
|
||||
@@ -53,6 +54,8 @@ const SECOND_REMINDER: &str =
|
||||
"<current_time_reminder>It is 2026-06-17 17:35:15 UTC.</current_time_reminder>";
|
||||
const THIRD_REMINDER: &str =
|
||||
"<current_time_reminder>It is 2026-06-17 17:36:15 UTC.</current_time_reminder>";
|
||||
const CLOCK_UNAVAILABLE: &str =
|
||||
"<current_time_unavailable>failed to read current time</current_time_unavailable>";
|
||||
const FIRST_TIME_UNIX_SECONDS: i64 = 1_781_717_655;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -648,6 +651,86 @@ async fn time_provider_failure_stops_before_inference() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn opted_in_clock_failures_reach_the_model_without_aborting() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
const CALL_ID: &str = "failed-current-time";
|
||||
let server = start_mock_server().await;
|
||||
let responses = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_function_call_with_namespace(CALL_ID, "clock", "curr_time", "{}"),
|
||||
ev_completed_with_tokens("resp-1", /*total_tokens*/ 80),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-2"),
|
||||
ev_function_call_with_namespace("clock-before-compact", "clock", "curr_time", "{}"),
|
||||
ev_completed_with_tokens("resp-2", /*total_tokens*/ 500),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-compact"),
|
||||
ev_assistant_message("msg-compact", "compact summary"),
|
||||
ev_completed("resp-compact"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-3"),
|
||||
ev_completed_with_tokens("resp-3", /*total_tokens*/ 80),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
let mut model_provider = built_in_model_providers(/*openai_base_url*/ None)["openai"].clone();
|
||||
model_provider.name = "OpenAI-compatible test provider".to_string();
|
||||
model_provider.base_url = Some(format!("{}/v1", server.uri()));
|
||||
model_provider.supports_websockets = false;
|
||||
let test = test_codex()
|
||||
.with_config(move |config| {
|
||||
config.model_provider = model_provider;
|
||||
config.model_auto_compact_token_limit = Some(200);
|
||||
enable_current_time_reminder(config, /*interval*/ 0, CurrentTimeSource::External);
|
||||
config.include_environment_context = true;
|
||||
config
|
||||
.features
|
||||
.enable(Feature::NonfatalClockReadErrors)
|
||||
.unwrap();
|
||||
})
|
||||
.with_external_time_provider(Arc::new(FailingTimeProvider))
|
||||
.build_with_auto_env(&server)
|
||||
.await?;
|
||||
|
||||
test.submit_turn("continue in a new context window").await?;
|
||||
|
||||
let requests = responses.requests();
|
||||
assert_eq!(requests.len(), 4);
|
||||
assert_eq!(
|
||||
requests[1].function_call_output_text(CALL_ID),
|
||||
Some("failed to read current time".to_string()),
|
||||
);
|
||||
for index in [0, 1, 3] {
|
||||
let request = &requests[index];
|
||||
let developer_messages = request.message_input_texts("developer");
|
||||
assert_eq!(
|
||||
developer_messages
|
||||
.iter()
|
||||
.map(|text| text.matches(CLOCK_UNAVAILABLE).count())
|
||||
.sum::<usize>(),
|
||||
1,
|
||||
"request {index} developer messages: {developer_messages:?}",
|
||||
);
|
||||
assert!(current_time_reminders(request).is_empty());
|
||||
assert!(
|
||||
!request
|
||||
.message_input_texts("user")
|
||||
.iter()
|
||||
.any(|text| text.contains("<current_date>"))
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn current_time_tool_returns_the_latest_time() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
@@ -1623,8 +1623,14 @@ async fn spawned_full_history_v2_child_uses_model_precedence_without_dropping_co
|
||||
.features
|
||||
.enable(Feature::CurrentTimeReminder)
|
||||
.expect("test config should allow feature update");
|
||||
config
|
||||
.features
|
||||
.enable(Feature::NonfatalClockReadErrors)
|
||||
.expect("test config should allow feature update");
|
||||
config.include_environment_context = false;
|
||||
config.current_time_reminder = Some(CurrentTimeReminderConfig {
|
||||
reminder_interval_seconds: 0,
|
||||
clock_source: codex_features::CurrentTimeSource::External,
|
||||
..CurrentTimeReminderConfig::default()
|
||||
});
|
||||
}
|
||||
@@ -1652,6 +1658,31 @@ async fn spawned_full_history_v2_child_uses_model_precedence_without_dropping_co
|
||||
if matches!(selection, FullHistoryV2ModelSelection::WorldStateIdentity) {
|
||||
builder = builder.with_history_mode(ThreadHistoryMode::Paginated);
|
||||
}
|
||||
if matches!(selection, FullHistoryV2ModelSelection::CurrentTimeReminders) {
|
||||
#[derive(Default)]
|
||||
struct FailFirstClockRead(std::sync::atomic::AtomicBool);
|
||||
|
||||
impl codex_core::TimeProvider for FailFirstClockRead {
|
||||
fn current_time(&self, _thread_id: ThreadId) -> codex_core::TimeFuture<'_> {
|
||||
let already_read = self.0.swap(true, std::sync::atomic::Ordering::Relaxed);
|
||||
Box::pin(async move {
|
||||
anyhow::ensure!(already_read, "parent clock unavailable");
|
||||
Ok(chrono::Utc::now())
|
||||
})
|
||||
}
|
||||
|
||||
fn sleep(
|
||||
&self,
|
||||
_thread_id: ThreadId,
|
||||
_duration: Duration,
|
||||
) -> codex_core::SleepFuture<'_> {
|
||||
Box::pin(async { Ok(()) })
|
||||
}
|
||||
}
|
||||
|
||||
builder =
|
||||
builder.with_external_time_provider(std::sync::Arc::new(FailFirstClockRead::default()));
|
||||
}
|
||||
let test = builder.build(&server).await?;
|
||||
if matches!(selection, FullHistoryV2ModelSelection::WorldStateIdentity) {
|
||||
test.codex.submit(Op::Compact).await?;
|
||||
@@ -1806,15 +1837,23 @@ async fn spawned_full_history_v2_child_uses_model_precedence_without_dropping_co
|
||||
);
|
||||
}
|
||||
if matches!(selection, FullHistoryV2ModelSelection::CurrentTimeReminders) {
|
||||
let reminder_count = |request: &ResponsesRequest| {
|
||||
let notice_count = |request: &ResponsesRequest, marker: &str| {
|
||||
request
|
||||
.message_input_texts("developer")
|
||||
.into_iter()
|
||||
.filter(|text| text.starts_with("<current_time_reminder>"))
|
||||
.filter(|text| text.starts_with(marker))
|
||||
.count()
|
||||
};
|
||||
assert_eq!(reminder_count(&parent_request), 2);
|
||||
assert_eq!(reminder_count(&child_request), 1);
|
||||
assert_eq!(
|
||||
notice_count(&parent_request, "<current_time_unavailable>"),
|
||||
1
|
||||
);
|
||||
assert_eq!(notice_count(&parent_request, "<current_time_reminder>"), 1);
|
||||
assert_eq!(
|
||||
notice_count(&child_request, "<current_time_unavailable>"),
|
||||
0
|
||||
);
|
||||
assert_eq!(notice_count(&child_request, "<current_time_reminder>"), 1);
|
||||
}
|
||||
let child_body = child_request.body_json();
|
||||
if matches!(selection, FullHistoryV2ModelSelection::WorldStateIdentity) {
|
||||
|
||||
Reference in New Issue
Block a user