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
@@ -1,5 +1,6 @@
use crate::context::ContextualUserFragment;
use crate::context::CurrentTimeReminder;
use crate::context::CurrentTimeUnavailable;
use crate::function_tool::FunctionCallError;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
@@ -8,6 +9,7 @@ use crate::tools::context::ToolPayload;
use crate::tools::context::boxed_tool_output;
use crate::tools::registry::CoreToolRuntime;
use crate::tools::registry::ToolExecutor;
use codex_features::Feature;
use codex_protocol::models::ResponseInputItem;
use codex_tools::JsonSchema;
use codex_tools::ResponsesApiNamespace;
@@ -98,7 +100,21 @@ impl ToolExecutor<ToolInvocation> for CurrentTimeHandler {
.current_time(invocation.session.thread_id)
.await
.map_err(|err| {
FunctionCallError::Fatal(format!("failed to read current time: {err:#}"))
if invocation
.turn
.config
.features
.enabled(Feature::NonfatalClockReadErrors)
{
tracing::error!(
thread_id = %invocation.session.thread_id,
turn_id = %invocation.turn.sub_id,
"failed to read current time for the clock tool; the clock provider may be stalled"
);
FunctionCallError::RespondToModel(CurrentTimeUnavailable::MESSAGE.to_string())
} else {
FunctionCallError::Fatal(format!("failed to read current time: {err:#}"))
}
})?;
Ok(boxed_tool_output(CurrentTimeOutput(
CurrentTimeReminder::new(current_time),
+17 -13
View File
@@ -1,3 +1,4 @@
use crate::context::CurrentTimeUnavailable;
use crate::function_tool::FunctionCallError;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
@@ -8,6 +9,7 @@ use crate::tools::registry::CoreToolRuntime;
use crate::tools::registry::ToolExecutor;
use codex_extension_items::ExtensionItem;
use codex_extension_items::sleep::SleepItem;
use codex_features::Feature;
use codex_protocol::items::TurnItem;
use codex_tools::JsonSchema;
use codex_tools::ResponsesApiNamespace;
@@ -111,7 +113,7 @@ impl ToolExecutor<ToolInvocation> for SleepHandler {
.input_queue
.subscribe_activity(turn_state.as_deref())
.await;
let sleep_result: Result<bool, FunctionCallError> = if pending_activity.is_some() {
let sleep_result = if pending_activity.is_some() {
Ok(true)
} else {
let sleep = session
@@ -120,27 +122,29 @@ impl ToolExecutor<ToolInvocation> for SleepHandler {
.sleep(session.thread_id, Duration::from_millis(args.duration_ms));
tokio::pin!(sleep);
tokio::select! {
result = &mut sleep => result
.map(|()| false)
.map_err(|err| {
FunctionCallError::Fatal(format!("failed to sleep: {err:#}"))
}),
result = &mut sleep => result.map(|()| false),
result = activity_rx.changed() => {
if result.is_ok() {
Ok(true)
} else {
sleep
.await
.map(|()| false)
.map_err(|err| {
FunctionCallError::Fatal(format!("failed to sleep: {err:#}"))
})
sleep.await.map(|()| false)
}
}
}
};
session.emit_turn_item_completed(turn.as_ref(), item).await;
let interrupted = sleep_result?;
let interrupted = sleep_result.map_err(|err| {
if turn.config.features.enabled(Feature::NonfatalClockReadErrors) {
tracing::error!(
thread_id = %session.thread_id,
turn_id = %turn.sub_id,
"failed to read current time for the sleep tool; the clock provider may be stalled"
);
FunctionCallError::RespondToModel(CurrentTimeUnavailable::MESSAGE.to_string())
} else {
FunctionCallError::Fatal(format!("failed to sleep: {err:#}"))
}
})?;
let message = if interrupted {
"Sleep interrupted by new input."