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:
Vivian Fang
2026-09-16 01:16:35 +00:00
committed by copyberry
parent fac58c1153
commit 5bf132cd52
14 changed files with 367 additions and 48 deletions
+61 -6
View File
@@ -11,6 +11,7 @@ use super::turn_context::TurnContext;
use crate::config::Config;
use crate::config::CurrentTimeReminderConfig;
use crate::context::ContextualUserFragment;
use crate::context::CurrentTimeUnavailable;
use crate::context_manager::is_user_turn_boundary;
pub(super) fn apply_persistent_defaults(config: &mut Config) {
@@ -40,6 +41,7 @@ pub(super) fn apply_persistent_defaults(config: &mut Config) {
pub(crate) struct CurrentTimeReminderState {
last_delivery_time: Option<DateTime<Utc>>,
last_window_id: Option<String>,
last_clock_failure: Option<(String, uuid::Uuid)>,
pending_user_or_tool_output_boundary: bool,
}
@@ -94,6 +96,59 @@ impl CurrentTimeReminderState {
}
}
impl Session {
pub(super) async fn read_clock_for_context(
&self,
turn_context: &TurnContext,
clock_read: &'static str,
) -> CodexResult<Option<DateTime<Utc>>> {
let error = match self
.services
.time_provider
.current_time(self.thread_id)
.await
{
Ok(time) => return Ok(Some(time)),
Err(error) => error,
};
if !turn_context
.config
.features
.enabled(Feature::NonfatalClockReadErrors)
{
return Err(CodexErr::Fatal(format!(
"failed to read current time: {error:#}"
)));
}
tracing::error!(
clock_read,
thread_id = %self.thread_id,
turn_id = %turn_context.sub_id,
"failed to read current time; the clock provider may be stalled"
);
{
let mut state = self.state.lock().await;
let failure = (
turn_context.sub_id.clone(),
state.auto_compact_window_ids().window_id,
);
// A compacted window may no longer contain the earlier notice.
if state.current_time_reminder.last_clock_failure.as_ref() == Some(&failure) {
return Ok(None);
}
state.current_time_reminder.last_clock_failure = Some(failure);
}
let response_item = ContextualUserFragment::into(CurrentTimeUnavailable);
self.record_conversation_items(
turn_context,
turn_context.model_info(),
std::slice::from_ref(&response_item),
)
.await;
Ok(None)
}
}
pub(super) async fn maybe_record_current_time_reminder(
sess: &Session,
turn_context: &TurnContext,
@@ -110,12 +165,12 @@ pub(super) async fn maybe_record_current_time_reminder(
return Ok(());
};
let current_time = sess
.services
.time_provider
.current_time(sess.thread_id)
.await
.map_err(|err| CodexErr::Fatal(format!("failed to read current time: {err:#}")))?;
let Some(current_time) = sess
.read_clock_for_context(turn_context, "reminder")
.await?
else {
return Ok(());
};
let reminder_is_due = {
let mut state = sess.state.lock().await;
+9 -10
View File
@@ -25,7 +25,6 @@ use crate::context::world_state::WorldState;
use codex_connectors::AppToolPolicyEvaluator;
use codex_extension_api::WorldStateContributionInput;
use codex_features::Feature;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result as CodexResult;
use codex_protocol::models::BaseInstructionsProvenance;
@@ -199,19 +198,19 @@ impl Session {
}
if turn_context.config.include_environment_context {
let current_date = self
.services
.time_provider
.current_time(self.thread_id())
.await
.map_err(|err| CodexErr::Fatal(format!("failed to read current time: {err:#}")))?
.with_timezone(&chrono::Local)
.format("%Y-%m-%d")
.to_string();
.read_clock_for_context(turn_context, "environment_date")
.await?
.map(|current_time| {
current_time
.with_timezone(&chrono::Local)
.format("%Y-%m-%d")
.to_string()
});
world_state.add_section(
EnvironmentsState::from_turn_context_with_environments(
turn_context,
&step_context.environments,
Some(current_date),
current_date,
)
.await
.with_subagents(environment_subagents),