mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
## What changed Move content-filter guidance recording from the sampling loop into `handle_response_stream_error`. Pass `StepContext` so the handler can resolve model-specific guidance and record it before retry decisions for sampling requests. Keep remote compaction excluded from guidance recording and update callers and the existing retry-deadline test to use the new signature. GitOrigin-RevId: 030bde1d4f1597fc4f3971ad837478e5304c5aa9
101 lines
3.9 KiB
Rust
101 lines
3.9 KiB
Rust
use super::ResponsesStreamRequest;
|
|
use super::ResponsesStreamRetryState;
|
|
use super::handle_response_stream_error;
|
|
use super::log_retry;
|
|
use crate::realtime_history::RealtimeHistoryState;
|
|
use crate::session::step_context::StepContext;
|
|
use crate::session::tests::make_session_and_context;
|
|
use codex_http_client::RetryAfter;
|
|
use codex_protocol::error::CodexErr;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
use tokio::sync::Mutex;
|
|
use tokio::time::Instant;
|
|
use tracing_test::internal::MockWriter;
|
|
|
|
#[tokio::test]
|
|
async fn sampling_retry_logs_stream_error_context() {
|
|
let (_session, turn_context) = make_session_and_context().await;
|
|
let buffer: &'static std::sync::Mutex<Vec<u8>> =
|
|
Box::leak(Box::new(std::sync::Mutex::new(Vec::new())));
|
|
let subscriber = tracing_subscriber::fmt()
|
|
.with_ansi(false)
|
|
.with_max_level(tracing::Level::WARN)
|
|
.with_writer(MockWriter::new(buffer))
|
|
.finish();
|
|
let _subscriber_guard = tracing::subscriber::set_default(subscriber);
|
|
|
|
log_retry(
|
|
ResponsesStreamRequest::Sampling,
|
|
&turn_context,
|
|
&CodexErr::Stream("websocket closed by server before response.completed".to_string()),
|
|
/*retries*/ 2,
|
|
/*max_retries*/ 5,
|
|
Duration::from_secs(1),
|
|
);
|
|
|
|
let logs = String::from_utf8(
|
|
buffer
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
.clone(),
|
|
)
|
|
.expect("retry log should be valid utf-8");
|
|
assert!(logs.contains("stream disconnected - retrying sampling request"));
|
|
assert!(logs.contains(&format!("turn_id={}", turn_context.sub_id)));
|
|
assert!(logs.contains("retries=2"));
|
|
assert!(logs.contains("max_retries=5"));
|
|
assert!(logs.contains(
|
|
"sampling_error=stream disconnected before completion: websocket closed by server before response.completed"
|
|
));
|
|
}
|
|
|
|
/// Time spent reporting a retry must count toward the server's original deadline.
|
|
#[tokio::test]
|
|
#[expect(
|
|
clippy::await_holding_invalid_type,
|
|
reason = "test holds the event-delivery lock to delay notification while virtual time advances"
|
|
)]
|
|
async fn stream_retry_preserves_deadline_across_delayed_notification() {
|
|
let (mut session, turn_context) = make_session_and_context().await;
|
|
let step_context = StepContext::for_test(Arc::new(turn_context));
|
|
session.realtime_history = Some(Mutex::new(RealtimeHistoryState::default()));
|
|
let mut client_session = session.services.model_client.new_session();
|
|
// The second retry emits a notification even when release builds hide the first.
|
|
let mut retry_state = ResponsesStreamRetryState {
|
|
retries: 1,
|
|
..Default::default()
|
|
};
|
|
|
|
tokio::time::pause();
|
|
let advice = RetryAfter::from_delay(Duration::from_secs(10)).expect("retry deadline");
|
|
tokio::time::advance(Duration::from_secs(4)).await;
|
|
|
|
// Hold the event-delivery lock so reporting the error consumes part of the deadline.
|
|
let history_guard = session.realtime_history.as_ref().unwrap().lock().await;
|
|
let retry = handle_response_stream_error(
|
|
&mut retry_state,
|
|
/*max_retries*/ 2,
|
|
CodexErr::InternalServerError.with_retry_after(advice),
|
|
&mut client_session,
|
|
&session,
|
|
&step_context,
|
|
ResponsesStreamRequest::Sampling,
|
|
);
|
|
tokio::pin!(retry);
|
|
assert!(futures::poll!(&mut retry).is_pending());
|
|
tokio::time::advance(Duration::from_secs(4)).await;
|
|
drop(history_guard);
|
|
|
|
assert!(futures::poll!(&mut retry).is_pending());
|
|
tokio::time::advance(Duration::from_secs(1)).await;
|
|
assert!(futures::poll!(&mut retry).is_pending());
|
|
retry.await.expect("retry should be allowed");
|
|
// Tokio rounds timer deadlines up to the next millisecond.
|
|
let resumed_at = Instant::now();
|
|
assert!(
|
|
(advice.deadline()..=advice.deadline() + Duration::from_millis(1)).contains(&resumed_at),
|
|
"retry resumed at {resumed_at:?}, expected {advice:?}"
|
|
);
|
|
}
|