[codex-analytics] add protocol-native turn timestamps

This commit is contained in:
rhan-oai
2026-04-06 10:20:07 -07:00
parent 4fd5c35c4f
commit 577abcf681
58 changed files with 1098 additions and 35 deletions
@@ -891,22 +891,29 @@ impl ThreadHistoryBuilder {
}
fn handle_turn_aborted(&mut self, payload: &TurnAbortedEvent) {
let apply_abort = |turn: &mut PendingTurn| {
turn.status = TurnStatus::Interrupted;
turn.completed_at = payload.completed_at;
turn.duration_ms = payload.duration_ms;
};
if let Some(turn_id) = payload.turn_id.as_deref() {
// Prefer an exact ID match so we interrupt the turn explicitly targeted by the event.
if let Some(turn) = self.current_turn.as_mut().filter(|turn| turn.id == turn_id) {
turn.status = TurnStatus::Interrupted;
apply_abort(turn);
return;
}
if let Some(turn) = self.turns.iter_mut().find(|turn| turn.id == turn_id) {
turn.status = TurnStatus::Interrupted;
turn.completed_at = payload.completed_at;
turn.duration_ms = payload.duration_ms;
return;
}
}
// If the event has no ID (or refers to an unknown turn), fall back to the active turn.
if let Some(turn) = self.current_turn.as_mut() {
turn.status = TurnStatus::Interrupted;
apply_abort(turn);
}
}
@@ -915,15 +922,18 @@ impl ThreadHistoryBuilder {
self.current_turn = Some(
self.new_turn(Some(payload.turn_id.clone()))
.with_status(TurnStatus::InProgress)
.with_created_at(payload.created_at)
.opened_explicitly(),
);
}
fn handle_turn_complete(&mut self, payload: &TurnCompleteEvent) {
let mark_completed = |status: &mut TurnStatus| {
if matches!(*status, TurnStatus::Completed | TurnStatus::InProgress) {
*status = TurnStatus::Completed;
let mark_completed = |turn: &mut PendingTurn| {
if matches!(turn.status, TurnStatus::Completed | TurnStatus::InProgress) {
turn.status = TurnStatus::Completed;
}
turn.completed_at = payload.completed_at;
turn.duration_ms = payload.duration_ms;
};
// Prefer an exact ID match from the active turn and then close it.
@@ -932,7 +942,7 @@ impl ThreadHistoryBuilder {
.as_mut()
.filter(|turn| turn.id == payload.turn_id)
{
mark_completed(&mut current_turn.status);
mark_completed(current_turn);
self.finish_current_turn();
return;
}
@@ -942,13 +952,17 @@ impl ThreadHistoryBuilder {
.iter_mut()
.find(|turn| turn.id == payload.turn_id)
{
mark_completed(&mut turn.status);
if matches!(turn.status, TurnStatus::Completed | TurnStatus::InProgress) {
turn.status = TurnStatus::Completed;
}
turn.completed_at = payload.completed_at;
turn.duration_ms = payload.duration_ms;
return;
}
// If the completion event cannot be matched, apply it to the active turn.
if let Some(current_turn) = self.current_turn.as_mut() {
mark_completed(&mut current_turn.status);
mark_completed(current_turn);
self.finish_current_turn();
}
}
@@ -991,6 +1005,9 @@ impl ThreadHistoryBuilder {
items: Vec::new(),
error: None,
status: TurnStatus::Completed,
created_at: None,
completed_at: None,
duration_ms: None,
opened_explicitly: false,
saw_compaction: false,
rollout_start_index: self.current_rollout_index,
@@ -1151,6 +1168,9 @@ struct PendingTurn {
items: Vec<ThreadItem>,
error: Option<TurnError>,
status: TurnStatus,
created_at: Option<i64>,
completed_at: Option<i64>,
duration_ms: Option<i64>,
/// True when this turn originated from an explicit `turn_started`/`turn_complete`
/// boundary, so we preserve it even if it has no renderable items.
opened_explicitly: bool,
@@ -1171,6 +1191,11 @@ impl PendingTurn {
self.status = status;
self
}
fn with_created_at(mut self, created_at: Option<i64>) -> Self {
self.created_at = created_at;
self
}
}
impl From<PendingTurn> for Turn {
@@ -1180,6 +1205,9 @@ impl From<PendingTurn> for Turn {
items: value.items,
error: value.error,
status: value.status,
created_at: value.created_at,
completed_at: value.completed_at,
duration_ms: value.duration_ms,
}
}
}
@@ -1191,6 +1219,9 @@ impl From<&PendingTurn> for Turn {
items: value.items.clone(),
error: value.error.clone(),
status: value.status.clone(),
created_at: value.created_at,
completed_at: value.completed_at,
duration_ms: value.duration_ms,
}
}
}
@@ -1342,6 +1373,7 @@ mod tests {
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: turn_id.to_string(),
created_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -1362,6 +1394,8 @@ mod tests {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: turn_id.to_string(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
}),
];
@@ -1414,6 +1448,7 @@ mod tests {
let items = vec![
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-image".into(),
created_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
})),
@@ -1433,6 +1468,8 @@ mod tests {
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-image".into(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
})),
];
@@ -1444,6 +1481,9 @@ mod tests {
id: "turn-image".into(),
status: TurnStatus::Completed,
error: None,
created_at: None,
completed_at: None,
duration_ms: None,
items: vec![
ThreadItem::UserMessage {
id: "item-1".into(),
@@ -1533,6 +1573,8 @@ mod tests {
EventMsg::TurnAborted(TurnAbortedEvent {
turn_id: Some("turn-1".into()),
reason: TurnAbortReason::Replaced,
completed_at: None,
duration_ms: None,
}),
EventMsg::UserMessage(UserMessageEvent {
message: "Let's try again".into(),
@@ -1730,6 +1772,7 @@ mod tests {
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-a".into(),
created_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -1748,6 +1791,8 @@ mod tests {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-a".into(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
}),
];
@@ -1784,6 +1829,7 @@ mod tests {
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".into(),
created_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -1889,6 +1935,7 @@ mod tests {
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".into(),
created_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -1948,6 +1995,7 @@ mod tests {
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".into(),
created_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -2035,6 +2083,7 @@ mod tests {
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-a".into(),
created_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -2047,9 +2096,12 @@ mod tests {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-a".into(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
}),
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-b".into(),
created_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -2081,6 +2133,8 @@ mod tests {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-b".into(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
}),
];
@@ -2118,6 +2172,7 @@ mod tests {
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-a".into(),
created_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -2130,9 +2185,12 @@ mod tests {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-a".into(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
}),
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-b".into(),
created_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -2164,6 +2222,8 @@ mod tests {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-b".into(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
}),
];
@@ -2196,6 +2256,7 @@ mod tests {
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: turn_id.to_string(),
created_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -2259,6 +2320,7 @@ mod tests {
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: turn_id.to_string(),
created_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -2321,6 +2383,7 @@ mod tests {
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-a".into(),
created_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -2333,9 +2396,12 @@ mod tests {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-a".into(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
}),
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-b".into(),
created_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -2348,6 +2414,8 @@ mod tests {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-a".into(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
}),
EventMsg::AgentMessage(AgentMessageEvent {
message: "still in b".into(),
@@ -2357,6 +2425,8 @@ mod tests {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-b".into(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
}),
];
@@ -2376,6 +2446,7 @@ mod tests {
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-a".into(),
created_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -2388,9 +2459,12 @@ mod tests {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-a".into(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
}),
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-b".into(),
created_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -2403,6 +2477,8 @@ mod tests {
EventMsg::TurnAborted(TurnAbortedEvent {
turn_id: Some("turn-a".into()),
reason: TurnAbortReason::Replaced,
completed_at: None,
duration_ms: None,
}),
EventMsg::AgentMessage(AgentMessageEvent {
message: "still in b".into(),
@@ -2428,6 +2504,7 @@ mod tests {
let items = vec![
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-compact".into(),
created_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
})),
@@ -2438,6 +2515,8 @@ mod tests {
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-compact".into(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
})),
];
@@ -2448,6 +2527,9 @@ mod tests {
id: "turn-compact".into(),
status: TurnStatus::Completed,
error: None,
created_at: None,
completed_at: None,
duration_ms: None,
items: Vec::new(),
}]
);
@@ -2665,6 +2747,7 @@ mod tests {
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-a".into(),
created_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -2677,6 +2760,8 @@ mod tests {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-a".into(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
}),
EventMsg::Error(ErrorEvent {
message: "request-level failure".into(),
@@ -2696,6 +2781,9 @@ mod tests {
id: "turn-a".into(),
status: TurnStatus::Completed,
error: None,
created_at: None,
completed_at: None,
duration_ms: None,
items: vec![ThreadItem::UserMessage {
id: "item-1".into(),
content: vec![UserInput::Text {
@@ -2712,6 +2800,7 @@ mod tests {
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-a".into(),
created_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -2730,6 +2819,8 @@ mod tests {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-a".into(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
}),
];
@@ -2765,6 +2856,7 @@ mod tests {
let items = vec![
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-a".into(),
created_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
})),
@@ -2778,6 +2870,8 @@ mod tests {
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-a".into(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
})),
];
@@ -2808,6 +2902,7 @@ mod tests {
let items = vec![
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-a".into(),
created_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
})),
@@ -2823,6 +2918,8 @@ mod tests {
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-a".into(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
})),
];
@@ -3691,6 +3691,15 @@ pub struct Turn {
pub status: TurnStatus,
/// Only populated when the Turn's status is failed.
pub error: Option<TurnError>,
/// Unix timestamp (in seconds) when the turn started.
#[ts(type = "number | null")]
pub created_at: Option<i64>,
/// Unix timestamp (in seconds) when the turn completed.
#[ts(type = "number | null")]
pub completed_at: Option<i64>,
/// Duration between turn start and completion in milliseconds, if known.
#[ts(type = "number | null")]
pub duration_ms: Option<i64>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]