mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Correlate image requests and explicit interrupts with turns (#36092)
## What changed - Send the active turn ID in the `x-codex-image-turn-id` header for image generation and edit requests. - Add `explicit_client_interrupt_requested_at_ms` to turn analytics. Record the earliest explicit interrupt request that receives a successful response, while excluding rejected requests. - Track errors returned by `turn/interrupt` so pending analytics state is cleaned up correctly. ## Testing - Cover turn ID headers for image generation and editing. - Cover accepted, retried, and rejected explicit interrupt requests in analytics tests. GitOrigin-RevId: 80fe996a161b4952b79c5c38238f75b7661b92af
This commit is contained in:
1 parent
88ec932e96
commit
6219b7c40f
10 files changed
+365
-17
No files matched your search
@@ -137,6 +137,7 @@ use codex_app_server_protocol::Turn;
|
||||
use codex_app_server_protocol::TurnCompletedNotification;
|
||||
use codex_app_server_protocol::TurnDiffUpdatedNotification;
|
||||
use codex_app_server_protocol::TurnError as AppServerTurnError;
|
||||
use codex_app_server_protocol::TurnInterruptResponse;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartedNotification;
|
||||
use codex_app_server_protocol::TurnStatus as AppServerTurnStatus;
|
||||
@@ -485,6 +486,10 @@ fn sample_turn_steer_response(turn_id: &str) -> ClientResponsePayload {
|
||||
})
|
||||
}
|
||||
|
||||
fn sample_turn_interrupt_response() -> ClientResponsePayload {
|
||||
ClientResponsePayload::TurnInterrupt(TurnInterruptResponse {})
|
||||
}
|
||||
|
||||
fn no_active_turn_steer_error() -> JSONRPCErrorError {
|
||||
JSONRPCErrorError {
|
||||
code: -32600,
|
||||
@@ -4073,6 +4078,7 @@ fn turn_event_serializes_expected_shape() {
|
||||
num_input_images: 2,
|
||||
is_first_turn: true,
|
||||
status: Some(TurnStatus::Completed),
|
||||
explicit_client_interrupt_requested_at_ms: None,
|
||||
turn_error: None,
|
||||
codex_error_kind: None,
|
||||
codex_error_http_status_code: None,
|
||||
@@ -4147,6 +4153,7 @@ fn turn_event_serializes_expected_shape() {
|
||||
"num_input_images": 2,
|
||||
"is_first_turn": true,
|
||||
"status": "completed",
|
||||
"explicit_client_interrupt_requested_at_ms": null,
|
||||
"turn_error": null,
|
||||
"codex_error_kind": null,
|
||||
"codex_error_http_status_code": null,
|
||||
@@ -4942,7 +4949,7 @@ async fn turn_lifecycle_emits_failed_turn_event() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_lifecycle_emits_interrupted_turn_event_without_error() {
|
||||
async fn rejected_turn_interrupt_does_not_tag_interrupted_turn_event() {
|
||||
let mut reducer = AnalyticsReducer::default();
|
||||
let mut out = Vec::new();
|
||||
|
||||
@@ -4955,6 +4962,28 @@ async fn turn_lifecycle_emits_interrupted_turn_event_without_error() {
|
||||
/*include_token_usage*/ false,
|
||||
)
|
||||
.await;
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::ExplicitClientInterruptRequest {
|
||||
connection_id: 7,
|
||||
request_id: RequestId::Integer(4),
|
||||
turn_id: "turn-2".to_string(),
|
||||
requested_at_ms: 1716000000123,
|
||||
},
|
||||
&mut out,
|
||||
)
|
||||
.await;
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::ErrorResponse {
|
||||
connection_id: 7,
|
||||
request_id: RequestId::Integer(4),
|
||||
error: no_active_turn_steer_error(),
|
||||
error_type: None,
|
||||
},
|
||||
&mut out,
|
||||
)
|
||||
.await;
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(sample_turn_completed_notification(
|
||||
@@ -4970,10 +4999,148 @@ async fn turn_lifecycle_emits_interrupted_turn_event_without_error() {
|
||||
assert_eq!(out.len(), 1);
|
||||
let payload = serde_json::to_value(&out[0]).expect("serialize turn event");
|
||||
assert_eq!(payload["event_params"]["status"], json!("interrupted"));
|
||||
assert_eq!(
|
||||
payload["event_params"]["explicit_client_interrupt_requested_at_ms"],
|
||||
json!(null)
|
||||
);
|
||||
assert_eq!(payload["event_params"]["turn_error"], json!(null));
|
||||
assert_eq!(payload["event_params"]["codex_error_kind"], json!(null));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn accepted_turn_interrupt_records_requested_at_on_turn_event() {
|
||||
let mut reducer = AnalyticsReducer::default();
|
||||
let mut out = Vec::new();
|
||||
|
||||
ingest_turn_prerequisites(
|
||||
&mut reducer,
|
||||
&mut out,
|
||||
/*include_initialize*/ true,
|
||||
/*include_resolved_config*/ true,
|
||||
/*include_started*/ true,
|
||||
/*include_token_usage*/ false,
|
||||
)
|
||||
.await;
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::ExplicitClientInterruptRequest {
|
||||
connection_id: 7,
|
||||
request_id: RequestId::Integer(4),
|
||||
turn_id: "turn-2".to_string(),
|
||||
requested_at_ms: 1716000000123,
|
||||
},
|
||||
&mut out,
|
||||
)
|
||||
.await;
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::ClientResponse {
|
||||
connection_id: 7,
|
||||
request_id: RequestId::Integer(4),
|
||||
response: Box::new(sample_turn_interrupt_response()),
|
||||
thread_originator: None,
|
||||
},
|
||||
&mut out,
|
||||
)
|
||||
.await;
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(sample_turn_completed_notification(
|
||||
"thread-2",
|
||||
"turn-2",
|
||||
AppServerTurnStatus::Interrupted,
|
||||
/*codex_error_info*/ None,
|
||||
))),
|
||||
&mut out,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(out.len(), 1);
|
||||
let payload = serde_json::to_value(&out[0]).expect("serialize turn event");
|
||||
assert_eq!(
|
||||
payload["event_params"]["explicit_client_interrupt_requested_at_ms"],
|
||||
json!(1716000000123_u64)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn accepted_turn_interrupt_retries_preserve_earliest_requested_at() {
|
||||
let mut reducer = AnalyticsReducer::default();
|
||||
let mut out = Vec::new();
|
||||
|
||||
ingest_turn_prerequisites(
|
||||
&mut reducer,
|
||||
&mut out,
|
||||
/*include_initialize*/ true,
|
||||
/*include_resolved_config*/ true,
|
||||
/*include_started*/ true,
|
||||
/*include_token_usage*/ false,
|
||||
)
|
||||
.await;
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::ExplicitClientInterruptRequest {
|
||||
connection_id: 7,
|
||||
request_id: RequestId::Integer(4),
|
||||
turn_id: "turn-2".to_string(),
|
||||
requested_at_ms: 1716000000123,
|
||||
},
|
||||
&mut out,
|
||||
)
|
||||
.await;
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::ExplicitClientInterruptRequest {
|
||||
connection_id: 7,
|
||||
request_id: RequestId::Integer(5),
|
||||
turn_id: "turn-2".to_string(),
|
||||
requested_at_ms: 1716000000456,
|
||||
},
|
||||
&mut out,
|
||||
)
|
||||
.await;
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::ClientResponse {
|
||||
connection_id: 7,
|
||||
request_id: RequestId::Integer(5),
|
||||
response: Box::new(sample_turn_interrupt_response()),
|
||||
thread_originator: None,
|
||||
},
|
||||
&mut out,
|
||||
)
|
||||
.await;
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::ClientResponse {
|
||||
connection_id: 7,
|
||||
request_id: RequestId::Integer(4),
|
||||
response: Box::new(sample_turn_interrupt_response()),
|
||||
thread_originator: None,
|
||||
},
|
||||
&mut out,
|
||||
)
|
||||
.await;
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(sample_turn_completed_notification(
|
||||
"thread-2",
|
||||
"turn-2",
|
||||
AppServerTurnStatus::Interrupted,
|
||||
/*codex_error_info*/ None,
|
||||
))),
|
||||
&mut out,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(out.len(), 1);
|
||||
let payload = serde_json::to_value(&out[0]).expect("serialize turn event");
|
||||
assert_eq!(
|
||||
payload["event_params"]["explicit_client_interrupt_requested_at_ms"],
|
||||
json!(1716000000123_u64)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_completed_without_started_notification_emits_null_started_at() {
|
||||
let mut reducer = AnalyticsReducer::default();
|
||||
|
||||
Reference in new issue
Block a user