[codex] route sleep through time providers (#29973)

## Summary

- add a cancellable sleep operation to `TimeProvider`
- route `clock.sleep` through the configured provider
- extend the supported sleep duration to 12 hours
- complete the sleep turn item before propagating provider failures

## Why

This isolates the core clock abstraction needed by external clock
integrations. Existing system and app-server behavior remains wall-clock
based in this PR; the stacked follow-up supplies app-server sleeps from
an external clock.
This commit is contained in:
rka-oai
2026-06-24 22:17:43 -07:00
committed by GitHub
Unverified
parent 22f12568e1
commit f66d793a2d
5 changed files with 130 additions and 12 deletions
+15 -1
View File
@@ -1,6 +1,7 @@
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use anyhow::Result;
use anyhow::anyhow;
@@ -12,10 +13,16 @@ use codex_protocol::ThreadId;
use crate::config::CurrentTimeReminderConfig;
pub type TimeFuture<'a> = Pin<Box<dyn Future<Output = Result<DateTime<Utc>>> + Send + 'a>>;
pub type SleepFuture<'a> = Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
/// Host integration boundary for obtaining the current time.
/// Host integration boundary for reading and waiting on the current time.
pub trait TimeProvider: Send + Sync {
fn current_time(&self, thread_id: ThreadId) -> TimeFuture<'_>;
/// Waits for the given duration on this provider's clock.
///
/// Dropping the returned future cancels the wait.
fn sleep(&self, thread_id: ThreadId, duration: Duration) -> SleepFuture<'_>;
}
pub(crate) struct SystemTimeProvider;
@@ -24,6 +31,13 @@ impl TimeProvider for SystemTimeProvider {
fn current_time(&self, _thread_id: ThreadId) -> TimeFuture<'_> {
Box::pin(async { Ok(Utc::now()) })
}
fn sleep(&self, _thread_id: ThreadId, duration: Duration) -> SleepFuture<'_> {
Box::pin(async move {
tokio::time::sleep(duration).await;
Ok(())
})
}
}
pub(crate) fn resolve_time_provider(
+1
View File
@@ -189,6 +189,7 @@ pub use client_common::ResponseEvent;
pub use client_common::ResponseStream;
pub use codex_prompts::REVIEW_PROMPT;
pub use compact::content_items_to_text;
pub use current_time::SleepFuture;
pub use current_time::TimeFuture;
pub use current_time::TimeProvider;
pub use event_mapping::parse_turn_item;
+20 -8
View File
@@ -21,7 +21,7 @@ use std::time::Instant;
const NAMESPACE: &str = "clock";
const TOOL_NAME: &str = "sleep";
const MAX_SLEEP_DURATION_MS: u64 = 3_600_000;
const MAX_SLEEP_DURATION_MS: u64 = 12 * 60 * 60 * 1000;
pub struct SleepHandler;
@@ -102,24 +102,36 @@ impl ToolExecutor<ToolInvocation> for SleepHandler {
.input_queue
.subscribe_activity(turn_state.as_deref())
.await;
let interrupted = if pending_activity.is_some() {
true
let sleep_result: Result<bool, FunctionCallError> = if pending_activity.is_some() {
Ok(true)
} else {
let sleep = tokio::time::sleep(Duration::from_millis(args.duration_ms));
let sleep = session
.services
.time_provider
.sleep(session.thread_id, Duration::from_millis(args.duration_ms));
tokio::pin!(sleep);
tokio::select! {
() = &mut sleep => false,
result = &mut sleep => result
.map(|()| false)
.map_err(|err| {
FunctionCallError::Fatal(format!("failed to sleep: {err:#}"))
}),
result = activity_rx.changed() => {
if result.is_ok() {
true
Ok(true)
} else {
sleep.await;
false
sleep
.await
.map(|()| false)
.map_err(|err| {
FunctionCallError::Fatal(format!("failed to sleep: {err:#}"))
})
}
}
}
};
session.emit_turn_item_completed(turn.as_ref(), item).await;
let interrupted = sleep_result?;
let message = if interrupted {
"Sleep interrupted by new input."