[codex-analytics] add protocol-native turn timestamps (#16638)

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/16638).
* #16870
* #16706
* #16659
* #16641
* #16640
* __->__ #16638
This commit is contained in:
rhan-oai
2026-04-06 16:22:59 -07:00
committed by GitHub
Unverified
parent e88c2cf4d7
commit 756c45ec61
58 changed files with 1134 additions and 36 deletions
+6
View File
@@ -1060,6 +1060,9 @@ mod tests {
items: Vec::new(),
status: codex_app_server_protocol::TurnStatus::Completed,
error: None,
started_at: None,
completed_at: Some(0),
duration_ms: Some(1),
},
})
}
@@ -1834,6 +1837,9 @@ mod tests {
items: Vec::new(),
status: codex_app_server_protocol::TurnStatus::Completed,
error: None,
started_at: None,
completed_at: Some(0),
duration_ms: None,
},
}
)
@@ -3532,6 +3532,22 @@
},
"Turn": {
"properties": {
"completedAt": {
"description": "Unix timestamp (in seconds) when the turn completed.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"durationMs": {
"description": "Duration between turn start and completion in milliseconds, if known.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"error": {
"anyOf": [
{
@@ -3553,6 +3569,14 @@
},
"type": "array"
},
"startedAt": {
"description": "Unix timestamp (in seconds) when the turn started.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"status": {
"$ref": "#/definitions/TurnStatus"
}
@@ -14329,6 +14329,22 @@
},
"Turn": {
"properties": {
"completedAt": {
"description": "Unix timestamp (in seconds) when the turn completed.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"durationMs": {
"description": "Duration between turn start and completion in milliseconds, if known.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"error": {
"anyOf": [
{
@@ -14350,6 +14366,14 @@
},
"type": "array"
},
"startedAt": {
"description": "Unix timestamp (in seconds) when the turn started.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"status": {
"$ref": "#/definitions/v2/TurnStatus"
}
@@ -12184,6 +12184,22 @@
},
"Turn": {
"properties": {
"completedAt": {
"description": "Unix timestamp (in seconds) when the turn completed.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"durationMs": {
"description": "Duration between turn start and completion in milliseconds, if known.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"error": {
"anyOf": [
{
@@ -12205,6 +12221,14 @@
},
"type": "array"
},
"startedAt": {
"description": "Unix timestamp (in seconds) when the turn started.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"status": {
"$ref": "#/definitions/TurnStatus"
}
@@ -1267,6 +1267,22 @@
},
"Turn": {
"properties": {
"completedAt": {
"description": "Unix timestamp (in seconds) when the turn completed.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"durationMs": {
"description": "Duration between turn start and completion in milliseconds, if known.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"error": {
"anyOf": [
{
@@ -1288,6 +1304,14 @@
},
"type": "array"
},
"startedAt": {
"description": "Unix timestamp (in seconds) when the turn started.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"status": {
"$ref": "#/definitions/TurnStatus"
}
@@ -1856,6 +1856,22 @@
},
"Turn": {
"properties": {
"completedAt": {
"description": "Unix timestamp (in seconds) when the turn completed.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"durationMs": {
"description": "Duration between turn start and completion in milliseconds, if known.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"error": {
"anyOf": [
{
@@ -1877,6 +1893,14 @@
},
"type": "array"
},
"startedAt": {
"description": "Unix timestamp (in seconds) when the turn started.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"status": {
"$ref": "#/definitions/TurnStatus"
}
@@ -1614,6 +1614,22 @@
},
"Turn": {
"properties": {
"completedAt": {
"description": "Unix timestamp (in seconds) when the turn completed.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"durationMs": {
"description": "Duration between turn start and completion in milliseconds, if known.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"error": {
"anyOf": [
{
@@ -1635,6 +1651,14 @@
},
"type": "array"
},
"startedAt": {
"description": "Unix timestamp (in seconds) when the turn started.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"status": {
"$ref": "#/definitions/TurnStatus"
}
@@ -1614,6 +1614,22 @@
},
"Turn": {
"properties": {
"completedAt": {
"description": "Unix timestamp (in seconds) when the turn completed.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"durationMs": {
"description": "Duration between turn start and completion in milliseconds, if known.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"error": {
"anyOf": [
{
@@ -1635,6 +1651,14 @@
},
"type": "array"
},
"startedAt": {
"description": "Unix timestamp (in seconds) when the turn started.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"status": {
"$ref": "#/definitions/TurnStatus"
}
@@ -1614,6 +1614,22 @@
},
"Turn": {
"properties": {
"completedAt": {
"description": "Unix timestamp (in seconds) when the turn completed.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"durationMs": {
"description": "Duration between turn start and completion in milliseconds, if known.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"error": {
"anyOf": [
{
@@ -1635,6 +1651,14 @@
},
"type": "array"
},
"startedAt": {
"description": "Unix timestamp (in seconds) when the turn started.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"status": {
"$ref": "#/definitions/TurnStatus"
}
@@ -1856,6 +1856,22 @@
},
"Turn": {
"properties": {
"completedAt": {
"description": "Unix timestamp (in seconds) when the turn completed.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"durationMs": {
"description": "Duration between turn start and completion in milliseconds, if known.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"error": {
"anyOf": [
{
@@ -1877,6 +1893,14 @@
},
"type": "array"
},
"startedAt": {
"description": "Unix timestamp (in seconds) when the turn started.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"status": {
"$ref": "#/definitions/TurnStatus"
}
@@ -1614,6 +1614,22 @@
},
"Turn": {
"properties": {
"completedAt": {
"description": "Unix timestamp (in seconds) when the turn completed.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"durationMs": {
"description": "Duration between turn start and completion in milliseconds, if known.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"error": {
"anyOf": [
{
@@ -1635,6 +1651,14 @@
},
"type": "array"
},
"startedAt": {
"description": "Unix timestamp (in seconds) when the turn started.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"status": {
"$ref": "#/definitions/TurnStatus"
}
@@ -1856,6 +1856,22 @@
},
"Turn": {
"properties": {
"completedAt": {
"description": "Unix timestamp (in seconds) when the turn completed.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"durationMs": {
"description": "Duration between turn start and completion in milliseconds, if known.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"error": {
"anyOf": [
{
@@ -1877,6 +1893,14 @@
},
"type": "array"
},
"startedAt": {
"description": "Unix timestamp (in seconds) when the turn started.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"status": {
"$ref": "#/definitions/TurnStatus"
}
@@ -1614,6 +1614,22 @@
},
"Turn": {
"properties": {
"completedAt": {
"description": "Unix timestamp (in seconds) when the turn completed.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"durationMs": {
"description": "Duration between turn start and completion in milliseconds, if known.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"error": {
"anyOf": [
{
@@ -1635,6 +1651,14 @@
},
"type": "array"
},
"startedAt": {
"description": "Unix timestamp (in seconds) when the turn started.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"status": {
"$ref": "#/definitions/TurnStatus"
}
@@ -1614,6 +1614,22 @@
},
"Turn": {
"properties": {
"completedAt": {
"description": "Unix timestamp (in seconds) when the turn completed.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"durationMs": {
"description": "Duration between turn start and completion in milliseconds, if known.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"error": {
"anyOf": [
{
@@ -1635,6 +1651,14 @@
},
"type": "array"
},
"startedAt": {
"description": "Unix timestamp (in seconds) when the turn started.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"status": {
"$ref": "#/definitions/TurnStatus"
}
@@ -1267,6 +1267,22 @@
},
"Turn": {
"properties": {
"completedAt": {
"description": "Unix timestamp (in seconds) when the turn completed.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"durationMs": {
"description": "Duration between turn start and completion in milliseconds, if known.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"error": {
"anyOf": [
{
@@ -1288,6 +1304,14 @@
},
"type": "array"
},
"startedAt": {
"description": "Unix timestamp (in seconds) when the turn started.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"status": {
"$ref": "#/definitions/TurnStatus"
}
@@ -1267,6 +1267,22 @@
},
"Turn": {
"properties": {
"completedAt": {
"description": "Unix timestamp (in seconds) when the turn completed.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"durationMs": {
"description": "Duration between turn start and completion in milliseconds, if known.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"error": {
"anyOf": [
{
@@ -1288,6 +1304,14 @@
},
"type": "array"
},
"startedAt": {
"description": "Unix timestamp (in seconds) when the turn started.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"status": {
"$ref": "#/definitions/TurnStatus"
}
@@ -1267,6 +1267,22 @@
},
"Turn": {
"properties": {
"completedAt": {
"description": "Unix timestamp (in seconds) when the turn completed.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"durationMs": {
"description": "Duration between turn start and completion in milliseconds, if known.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"error": {
"anyOf": [
{
@@ -1288,6 +1304,14 @@
},
"type": "array"
},
"startedAt": {
"description": "Unix timestamp (in seconds) when the turn started.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"status": {
"$ref": "#/definitions/TurnStatus"
}
@@ -15,4 +15,16 @@ items: Array<ThreadItem>, status: TurnStatus,
/**
* Only populated when the Turn's status is failed.
*/
error: TurnError | null, };
error: TurnError | null,
/**
* Unix timestamp (in seconds) when the turn started.
*/
startedAt: number | null,
/**
* Unix timestamp (in seconds) when the turn completed.
*/
completedAt: number | null,
/**
* Duration between turn start and completion in milliseconds, if known.
*/
durationMs: number | null, };
@@ -864,22 +864,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);
}
}
@@ -888,15 +895,18 @@ impl ThreadHistoryBuilder {
self.current_turn = Some(
self.new_turn(Some(payload.turn_id.clone()))
.with_status(TurnStatus::InProgress)
.with_started_at(payload.started_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.
@@ -905,7 +915,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;
}
@@ -915,13 +925,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();
}
}
@@ -954,7 +968,7 @@ impl ThreadHistoryBuilder {
if turn.items.is_empty() && !turn.opened_explicitly && !turn.saw_compaction {
return;
}
self.turns.push(turn.into());
self.turns.push(Turn::from(turn));
}
}
@@ -964,6 +978,9 @@ impl ThreadHistoryBuilder {
items: Vec::new(),
error: None,
status: TurnStatus::Completed,
started_at: None,
completed_at: None,
duration_ms: None,
opened_explicitly: false,
saw_compaction: false,
rollout_start_index: self.current_rollout_index,
@@ -1082,6 +1099,9 @@ struct PendingTurn {
items: Vec<ThreadItem>,
error: Option<TurnError>,
status: TurnStatus,
started_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,
@@ -1102,6 +1122,11 @@ impl PendingTurn {
self.status = status;
self
}
fn with_started_at(mut self, started_at: Option<i64>) -> Self {
self.started_at = started_at;
self
}
}
impl From<PendingTurn> for Turn {
@@ -1111,6 +1136,9 @@ impl From<PendingTurn> for Turn {
items: value.items,
error: value.error,
status: value.status,
started_at: value.started_at,
completed_at: value.completed_at,
duration_ms: value.duration_ms,
}
}
}
@@ -1122,6 +1150,9 @@ impl From<&PendingTurn> for Turn {
items: value.items.clone(),
error: value.error.clone(),
status: value.status.clone(),
started_at: value.started_at,
completed_at: value.completed_at,
duration_ms: value.duration_ms,
}
}
}
@@ -1273,6 +1304,7 @@ mod tests {
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: turn_id.to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -1293,6 +1325,8 @@ mod tests {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: turn_id.to_string(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
}),
];
@@ -1345,6 +1379,7 @@ mod tests {
let items = vec![
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-image".into(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
})),
@@ -1364,6 +1399,8 @@ mod tests {
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-image".into(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
})),
];
@@ -1375,6 +1412,9 @@ mod tests {
id: "turn-image".into(),
status: TurnStatus::Completed,
error: None,
started_at: None,
completed_at: None,
duration_ms: None,
items: vec![
ThreadItem::UserMessage {
id: "item-1".into(),
@@ -1464,6 +1504,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(),
@@ -1661,6 +1703,7 @@ mod tests {
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-a".into(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -1679,6 +1722,8 @@ mod tests {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-a".into(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
}),
];
@@ -1715,6 +1760,7 @@ mod tests {
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".into(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -1820,6 +1866,7 @@ mod tests {
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".into(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -1879,6 +1926,7 @@ mod tests {
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".into(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -1966,6 +2014,7 @@ mod tests {
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".into(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -2038,6 +2087,7 @@ mod tests {
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".into(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -2096,6 +2146,7 @@ mod tests {
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-a".into(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -2108,9 +2159,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(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -2142,6 +2196,8 @@ mod tests {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-b".into(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
}),
];
@@ -2179,6 +2235,7 @@ mod tests {
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-a".into(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -2191,9 +2248,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(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -2225,6 +2285,8 @@ mod tests {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-b".into(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
}),
];
@@ -2257,6 +2319,7 @@ mod tests {
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: turn_id.to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -2320,6 +2383,7 @@ mod tests {
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: turn_id.to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -2382,6 +2446,7 @@ mod tests {
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-a".into(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -2394,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(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -2409,6 +2477,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(),
@@ -2418,6 +2488,8 @@ mod tests {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-b".into(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
}),
];
@@ -2437,6 +2509,7 @@ mod tests {
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-a".into(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -2449,9 +2522,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(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -2464,6 +2540,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(),
@@ -2489,6 +2567,7 @@ mod tests {
let items = vec![
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-compact".into(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
})),
@@ -2499,6 +2578,8 @@ mod tests {
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-compact".into(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
})),
];
@@ -2509,6 +2590,9 @@ mod tests {
id: "turn-compact".into(),
status: TurnStatus::Completed,
error: None,
started_at: None,
completed_at: None,
duration_ms: None,
items: Vec::new(),
}]
);
@@ -2726,6 +2810,7 @@ mod tests {
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-a".into(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -2738,6 +2823,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(),
@@ -2757,6 +2844,9 @@ mod tests {
id: "turn-a".into(),
status: TurnStatus::Completed,
error: None,
started_at: None,
completed_at: None,
duration_ms: None,
items: vec![ThreadItem::UserMessage {
id: "item-1".into(),
content: vec![UserInput::Text {
@@ -2773,6 +2863,7 @@ mod tests {
let events = vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-a".into(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}),
@@ -2791,6 +2882,8 @@ mod tests {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-a".into(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
}),
];
@@ -2826,6 +2919,7 @@ mod tests {
let items = vec![
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-a".into(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
})),
@@ -2839,6 +2933,8 @@ mod tests {
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-a".into(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
})),
];
@@ -2869,6 +2965,7 @@ mod tests {
let items = vec![
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-a".into(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
})),
@@ -2884,6 +2981,8 @@ mod tests {
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-a".into(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
})),
];
@@ -3693,6 +3693,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 started_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)]
+128 -14
View File
@@ -127,6 +127,8 @@ use codex_protocol::protocol::RealtimeEvent;
use codex_protocol::protocol::ReviewDecision;
use codex_protocol::protocol::ReviewOutputEvent;
use codex_protocol::protocol::TokenCountEvent;
use codex_protocol::protocol::TurnAbortedEvent;
use codex_protocol::protocol::TurnCompleteEvent;
use codex_protocol::protocol::TurnDiffEvent;
use codex_protocol::request_permissions::PermissionGrantScope as CorePermissionGrantScope;
use codex_protocol::request_permissions::RequestPermissionProfile as CoreRequestPermissionProfile;
@@ -190,6 +192,9 @@ pub(crate) async fn apply_bespoke_event_handling(
items: Vec::new(),
error: None,
status: TurnStatus::InProgress,
started_at: payload.started_at,
completed_at: None,
duration_ms: None,
})
};
let notification = TurnStartedNotification {
@@ -201,14 +206,21 @@ pub(crate) async fn apply_bespoke_event_handling(
.await;
}
}
EventMsg::TurnComplete(_ev) => {
EventMsg::TurnComplete(turn_complete_event) => {
// All per-thread requests are bound to a turn, so abort them.
outgoing.abort_pending_server_requests().await;
let turn_failed = thread_state.lock().await.turn_summary.last_error.is_some();
thread_watch_manager
.note_turn_completed(&conversation_id.to_string(), turn_failed)
.await;
handle_turn_complete(conversation_id, event_turn_id, &outgoing, &thread_state).await;
handle_turn_complete(
conversation_id,
event_turn_id,
turn_complete_event,
&outgoing,
&thread_state,
)
.await;
}
EventMsg::SkillsUpdateAvailable => {
if let ApiVersion::V2 = api_version {
@@ -1704,7 +1716,14 @@ pub(crate) async fn apply_bespoke_event_handling(
thread_watch_manager
.note_turn_interrupted(&conversation_id.to_string())
.await;
handle_turn_interrupted(conversation_id, event_turn_id, &outgoing, &thread_state).await;
handle_turn_interrupted(
conversation_id,
event_turn_id,
turn_aborted_event,
&outgoing,
&thread_state,
)
.await;
}
EventMsg::ThreadRolledBack(_rollback_event) => {
let pending = {
@@ -1866,11 +1885,18 @@ async fn handle_turn_plan_update(
}
}
struct TurnCompletionMetadata {
status: TurnStatus,
error: Option<TurnError>,
started_at: Option<i64>,
completed_at: Option<i64>,
duration_ms: Option<i64>,
}
async fn emit_turn_completed_with_status(
conversation_id: ThreadId,
event_turn_id: String,
status: TurnStatus,
error: Option<TurnError>,
turn_completion_metadata: TurnCompletionMetadata,
outgoing: &ThreadScopedOutgoingMessageSender,
) {
let notification = TurnCompletedNotification {
@@ -1878,8 +1904,11 @@ async fn emit_turn_completed_with_status(
turn: Turn {
id: event_turn_id,
items: vec![],
error,
status,
error: turn_completion_metadata.error,
status: turn_completion_metadata.status,
started_at: turn_completion_metadata.started_at,
completed_at: turn_completion_metadata.completed_at,
duration_ms: turn_completion_metadata.duration_ms,
},
};
outgoing
@@ -2073,6 +2102,7 @@ async fn find_and_remove_turn_summary(
async fn handle_turn_complete(
conversation_id: ThreadId,
event_turn_id: String,
turn_complete_event: TurnCompleteEvent,
outgoing: &ThreadScopedOutgoingMessageSender,
thread_state: &Arc<Mutex<ThreadState>>,
) {
@@ -2083,22 +2113,40 @@ async fn handle_turn_complete(
None => (TurnStatus::Completed, None),
};
emit_turn_completed_with_status(conversation_id, event_turn_id, status, error, outgoing).await;
emit_turn_completed_with_status(
conversation_id,
event_turn_id,
TurnCompletionMetadata {
status,
error,
started_at: turn_summary.started_at,
completed_at: turn_complete_event.completed_at,
duration_ms: turn_complete_event.duration_ms,
},
outgoing,
)
.await;
}
async fn handle_turn_interrupted(
conversation_id: ThreadId,
event_turn_id: String,
turn_aborted_event: TurnAbortedEvent,
outgoing: &ThreadScopedOutgoingMessageSender,
thread_state: &Arc<Mutex<ThreadState>>,
) {
find_and_remove_turn_summary(conversation_id, thread_state).await;
let turn_summary = find_and_remove_turn_summary(conversation_id, thread_state).await;
emit_turn_completed_with_status(
conversation_id,
event_turn_id,
TurnStatus::Interrupted,
/*error*/ None,
TurnCompletionMetadata {
status: TurnStatus::Interrupted,
error: None,
started_at: turn_summary.started_at,
completed_at: turn_aborted_event.completed_at,
duration_ms: turn_aborted_event.duration_ms,
},
outgoing,
)
.await;
@@ -2871,6 +2919,9 @@ mod tests {
Arc::new(Mutex::new(ThreadState::default()))
}
const TEST_TURN_COMPLETED_AT: i64 = 1_716_000_456;
const TEST_TURN_DURATION_MS: i64 = 1_234;
async fn recv_broadcast_message(
rx: &mut mpsc::Receiver<OutgoingEnvelope>,
) -> Result<OutgoingMessage> {
@@ -2884,6 +2935,24 @@ mod tests {
}
}
fn turn_complete_event(turn_id: &str) -> TurnCompleteEvent {
TurnCompleteEvent {
turn_id: turn_id.to_string(),
last_agent_message: None,
completed_at: Some(TEST_TURN_COMPLETED_AT),
duration_ms: Some(TEST_TURN_DURATION_MS),
}
}
fn turn_aborted_event(turn_id: &str) -> TurnAbortedEvent {
TurnAbortedEvent {
turn_id: Some(turn_id.to_string()),
reason: codex_protocol::protocol::TurnAbortReason::Interrupted,
completed_at: Some(TEST_TURN_COMPLETED_AT),
duration_ms: Some(TEST_TURN_DURATION_MS),
}
}
fn command_execution_completion_item(command: &str) -> CommandExecutionCompletionItem {
CommandExecutionCompletionItem {
command: command.to_string(),
@@ -3648,10 +3717,25 @@ mod tests {
ThreadId::new(),
);
let thread_state = new_thread_state();
{
let mut state = thread_state.lock().await;
state.track_current_turn_event(&EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: event_turn_id.clone(),
started_at: Some(42),
model_context_window: None,
collaboration_mode_kind: Default::default(),
},
));
state.track_current_turn_event(&EventMsg::TurnComplete(turn_complete_event(
&event_turn_id,
)));
}
handle_turn_complete(
conversation_id,
event_turn_id.clone(),
turn_complete_event(&event_turn_id),
&outgoing,
&thread_state,
)
@@ -3663,6 +3747,9 @@ mod tests {
assert_eq!(n.turn.id, event_turn_id);
assert_eq!(n.turn.status, TurnStatus::Completed);
assert_eq!(n.turn.error, None);
assert_eq!(n.turn.started_at, Some(42));
assert_eq!(n.turn.completed_at, Some(TEST_TURN_COMPLETED_AT));
assert_eq!(n.turn.duration_ms, Some(TEST_TURN_DURATION_MS));
}
other => bail!("unexpected message: {other:?}"),
}
@@ -3696,6 +3783,7 @@ mod tests {
handle_turn_interrupted(
conversation_id,
event_turn_id.clone(),
turn_aborted_event(&event_turn_id),
&outgoing,
&thread_state,
)
@@ -3707,6 +3795,8 @@ mod tests {
assert_eq!(n.turn.id, event_turn_id);
assert_eq!(n.turn.status, TurnStatus::Interrupted);
assert_eq!(n.turn.error, None);
assert_eq!(n.turn.completed_at, Some(TEST_TURN_COMPLETED_AT));
assert_eq!(n.turn.duration_ms, Some(TEST_TURN_DURATION_MS));
}
other => bail!("unexpected message: {other:?}"),
}
@@ -3740,6 +3830,7 @@ mod tests {
handle_turn_complete(
conversation_id,
event_turn_id.clone(),
turn_complete_event(&event_turn_id),
&outgoing,
&thread_state,
)
@@ -3758,6 +3849,8 @@ mod tests {
additional_details: None,
})
);
assert_eq!(n.turn.completed_at, Some(TEST_TURN_COMPLETED_AT));
assert_eq!(n.turn.duration_ms, Some(TEST_TURN_DURATION_MS));
}
other => bail!("unexpected message: {other:?}"),
}
@@ -4000,7 +4093,14 @@ mod tests {
&thread_state,
)
.await;
handle_turn_complete(conversation_a, a_turn1.clone(), &outgoing, &thread_state).await;
handle_turn_complete(
conversation_a,
a_turn1.clone(),
turn_complete_event(&a_turn1),
&outgoing,
&thread_state,
)
.await;
// Turn 1 on conversation B
let b_turn1 = "b_turn1".to_string();
@@ -4014,11 +4114,25 @@ mod tests {
&thread_state,
)
.await;
handle_turn_complete(conversation_b, b_turn1.clone(), &outgoing, &thread_state).await;
handle_turn_complete(
conversation_b,
b_turn1.clone(),
turn_complete_event(&b_turn1),
&outgoing,
&thread_state,
)
.await;
// Turn 2 on conversation A
let a_turn2 = "a_turn2".to_string();
handle_turn_complete(conversation_a, a_turn2.clone(), &outgoing, &thread_state).await;
handle_turn_complete(
conversation_a,
a_turn2.clone(),
turn_complete_event(&a_turn2),
&outgoing,
&thread_state,
)
.await;
// Verify: A turn 1
let msg = recv_broadcast_message(&mut rx).await?;
@@ -6581,6 +6581,9 @@ impl CodexMessageProcessor {
items: vec![],
error: None,
status: TurnStatus::InProgress,
started_at: None,
completed_at: None,
duration_ms: None,
};
let response = TurnStartResponse { turn };
@@ -6917,6 +6920,9 @@ impl CodexMessageProcessor {
items,
error: None,
status: TurnStatus::InProgress,
started_at: None,
completed_at: None,
duration_ms: None,
}
}
@@ -9566,6 +9572,7 @@ mod tests {
state.track_current_turn_event(&EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: "turn-1".to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
},
+3
View File
@@ -826,6 +826,9 @@ mod tests {
items: Vec::new(),
status: TurnStatus::Completed,
error: None,
started_at: None,
completed_at: Some(0),
duration_ms: None,
},
})
));
+7 -1
View File
@@ -45,6 +45,7 @@ pub(crate) enum ThreadListenerCommand {
/// Per-conversation accumulation of the latest states e.g. error message while a turn runs.
#[derive(Default, Clone)]
pub(crate) struct TurnSummary {
pub(crate) started_at: Option<i64>,
pub(crate) file_change_started: HashSet<String>,
pub(crate) command_execution_started: HashSet<String>,
pub(crate) last_error: Option<TurnError>,
@@ -110,8 +111,13 @@ impl ThreadState {
}
pub(crate) fn track_current_turn_event(&mut self, event: &EventMsg) {
if let EventMsg::TurnStarted(payload) = event {
self.turn_summary.started_at = payload.started_at;
}
self.current_turn_history.handle_event(event);
if !self.current_turn_history.has_active_turn() {
if matches!(event, EventMsg::TurnAborted(_) | EventMsg::TurnComplete(_))
&& !self.current_turn_history.has_active_turn()
{
self.current_turn_history.reset();
}
}
@@ -492,6 +492,7 @@ async fn thread_resume_and_read_interrupt_incomplete_rollout_turn_when_thread_is
"type": "event_msg",
"payload": serde_json::to_value(EventMsg::TurnStarted(TurnStartedEvent {
turn_id: turn_id.to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
}))?,
+9
View File
@@ -256,6 +256,7 @@ async fn get_status_returns_not_found_without_manager() {
async fn on_event_updates_status_from_task_started() {
let status = agent_status_from_event(&EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: ModeKind::Default,
}));
@@ -267,6 +268,8 @@ async fn on_event_updates_status_from_task_complete() {
let status = agent_status_from_event(&EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".to_string(),
last_agent_message: Some("done".to_string()),
completed_at: None,
duration_ms: None,
}));
let expected = AgentStatus::Completed(Some("done".to_string()));
assert_eq!(status, Some(expected));
@@ -288,6 +291,8 @@ async fn on_event_updates_status_from_turn_aborted() {
let status = agent_status_from_event(&EventMsg::TurnAborted(TurnAbortedEvent {
turn_id: Some("turn-1".to_string()),
reason: TurnAbortReason::Interrupted,
completed_at: None,
duration_ms: None,
}));
let expected = AgentStatus::Interrupted;
@@ -1200,6 +1205,8 @@ async fn multi_agent_v2_completion_ignores_dead_direct_parent() {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: tester_turn.sub_id.clone(),
last_agent_message: Some("done".to_string()),
completed_at: None,
duration_ms: None,
}),
)
.await;
@@ -1284,6 +1291,8 @@ async fn multi_agent_v2_completion_queues_message_for_direct_parent() {
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: tester_turn.sub_id.clone(),
last_agent_message: Some("done".to_string()),
completed_at: None,
duration_ms: None,
}),
)
.await;
@@ -128,6 +128,7 @@ async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lif
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: turn_id.clone(),
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -145,6 +146,8 @@ async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lif
codex_protocol::protocol::TurnCompleteEvent {
turn_id,
last_agent_message: None,
completed_at: None,
duration_ms: None,
},
)),
];
@@ -190,6 +193,7 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_com
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: first_turn_id.clone(),
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -209,11 +213,14 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_com
codex_protocol::protocol::TurnCompleteEvent {
turn_id: first_turn_id,
last_agent_message: None,
completed_at: None,
duration_ms: None,
},
)),
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: rolled_back_turn_id.clone(),
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -233,6 +240,8 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_com
codex_protocol::protocol::TurnCompleteEvent {
turn_id: rolled_back_turn_id,
last_agent_message: None,
completed_at: None,
duration_ms: None,
},
)),
RolloutItem::EventMsg(EventMsg::ThreadRolledBack(
@@ -280,6 +289,7 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_inc
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: first_turn_id.clone(),
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -299,11 +309,14 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_inc
codex_protocol::protocol::TurnCompleteEvent {
turn_id: first_turn_id,
last_agent_message: None,
completed_at: None,
duration_ms: None,
},
)),
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: incomplete_turn_id,
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -365,6 +378,7 @@ async fn reconstruct_history_rollback_skips_non_user_turns_for_history_and_metad
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: first_turn_id.clone(),
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -384,11 +398,14 @@ async fn reconstruct_history_rollback_skips_non_user_turns_for_history_and_metad
codex_protocol::protocol::TurnCompleteEvent {
turn_id: first_turn_id,
last_agent_message: None,
completed_at: None,
duration_ms: None,
},
)),
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: second_turn_id.clone(),
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -407,11 +424,14 @@ async fn reconstruct_history_rollback_skips_non_user_turns_for_history_and_metad
codex_protocol::protocol::TurnCompleteEvent {
turn_id: second_turn_id,
last_agent_message: None,
completed_at: None,
duration_ms: None,
},
)),
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: standalone_turn_id.clone(),
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -421,6 +441,8 @@ async fn reconstruct_history_rollback_skips_non_user_turns_for_history_and_metad
codex_protocol::protocol::TurnCompleteEvent {
turn_id: standalone_turn_id,
last_agent_message: None,
completed_at: None,
duration_ms: None,
},
)),
RolloutItem::EventMsg(EventMsg::ThreadRolledBack(
@@ -471,6 +493,7 @@ async fn reconstruct_history_rollback_counts_inter_agent_assistant_turns() {
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: first_turn_id.clone(),
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -490,11 +513,14 @@ async fn reconstruct_history_rollback_counts_inter_agent_assistant_turns() {
codex_protocol::protocol::TurnCompleteEvent {
turn_id: first_turn_id,
last_agent_message: None,
completed_at: None,
duration_ms: None,
},
)),
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: assistant_turn_id.clone(),
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -506,6 +532,8 @@ async fn reconstruct_history_rollback_counts_inter_agent_assistant_turns() {
codex_protocol::protocol::TurnCompleteEvent {
turn_id: assistant_turn_id,
last_agent_message: None,
completed_at: None,
duration_ms: None,
},
)),
RolloutItem::EventMsg(EventMsg::ThreadRolledBack(
@@ -551,6 +579,7 @@ async fn reconstruct_history_rollback_clears_history_and_metadata_when_exceeding
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: only_turn_id.clone(),
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -570,6 +599,8 @@ async fn reconstruct_history_rollback_clears_history_and_metadata_when_exceeding
codex_protocol::protocol::TurnCompleteEvent {
turn_id: only_turn_id,
last_agent_message: None,
completed_at: None,
duration_ms: None,
},
)),
RolloutItem::EventMsg(EventMsg::ThreadRolledBack(
@@ -599,6 +630,7 @@ async fn record_initial_history_resumed_rollback_skips_only_user_turns() {
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: user_turn_id.clone(),
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -616,12 +648,15 @@ async fn record_initial_history_resumed_rollback_skips_only_user_turns() {
codex_protocol::protocol::TurnCompleteEvent {
turn_id: user_turn_id,
last_agent_message: None,
completed_at: None,
duration_ms: None,
},
)),
// Standalone task turn (no UserMessage) should not consume rollback skips.
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: standalone_turn_id.clone(),
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -630,6 +665,8 @@ async fn record_initial_history_resumed_rollback_skips_only_user_turns() {
codex_protocol::protocol::TurnCompleteEvent {
turn_id: standalone_turn_id,
last_agent_message: None,
completed_at: None,
duration_ms: None,
},
)),
RolloutItem::EventMsg(EventMsg::ThreadRolledBack(
@@ -663,6 +700,7 @@ async fn record_initial_history_resumed_rollback_drops_incomplete_user_turn_comp
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: previous_turn_id.clone(),
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -680,11 +718,14 @@ async fn record_initial_history_resumed_rollback_drops_incomplete_user_turn_comp
codex_protocol::protocol::TurnCompleteEvent {
turn_id: previous_turn_id,
last_agent_message: None,
completed_at: None,
duration_ms: None,
},
)),
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: incomplete_turn_id,
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -815,6 +856,7 @@ async fn reconstruct_history_legacy_compaction_without_replacement_history_clear
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: current_turn_id.clone(),
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -832,6 +874,8 @@ async fn reconstruct_history_legacy_compaction_without_replacement_history_clear
codex_protocol::protocol::TurnCompleteEvent {
turn_id: current_turn_id,
last_agent_message: None,
completed_at: None,
duration_ms: None,
},
)),
];
@@ -876,6 +920,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: previous_turn_id.clone(),
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -898,6 +943,8 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
codex_protocol::protocol::TurnCompleteEvent {
turn_id: previous_turn_id,
last_agent_message: None,
completed_at: None,
duration_ms: None,
},
)),
];
@@ -979,6 +1026,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: previous_turn_id.clone(),
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -996,11 +1044,14 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
codex_protocol::protocol::TurnCompleteEvent {
turn_id: previous_turn_id,
last_agent_message: None,
completed_at: None,
duration_ms: None,
},
)),
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: aborted_turn_id,
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -1017,6 +1068,8 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
codex_protocol::protocol::TurnAbortedEvent {
turn_id: None,
reason: TurnAbortReason::Interrupted,
completed_at: None,
duration_ms: None,
},
)),
RolloutItem::Compacted(CompactedItem {
@@ -1080,6 +1133,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: previous_turn_id.clone(),
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -1097,11 +1151,14 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo
codex_protocol::protocol::TurnCompleteEvent {
turn_id: previous_turn_id,
last_agent_message: None,
completed_at: None,
duration_ms: None,
},
)),
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: current_turn_id.clone(),
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -1118,6 +1175,8 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo
codex_protocol::protocol::TurnAbortedEvent {
turn_id: Some(unmatched_abort_turn_id),
reason: TurnAbortReason::Interrupted,
completed_at: None,
duration_ms: None,
},
)),
RolloutItem::TurnContext(current_context_item.clone()),
@@ -1125,6 +1184,8 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo
codex_protocol::protocol::TurnCompleteEvent {
turn_id: current_turn_id,
last_agent_message: None,
completed_at: None,
duration_ms: None,
},
)),
];
@@ -1187,6 +1248,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: previous_turn_id.clone(),
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -1204,11 +1266,14 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea
codex_protocol::protocol::TurnCompleteEvent {
turn_id: previous_turn_id,
last_agent_message: None,
completed_at: None,
duration_ms: None,
},
)),
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: incomplete_turn_id,
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -1258,6 +1323,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_preserves_turn_
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: current_turn_id,
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -1332,6 +1398,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: previous_turn_id.clone(),
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -1349,11 +1416,14 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
codex_protocol::protocol::TurnCompleteEvent {
turn_id: previous_turn_id,
last_agent_message: None,
completed_at: None,
duration_ms: None,
},
)),
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: compacted_incomplete_turn_id,
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -1375,6 +1445,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: replacing_turn_id,
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -53,6 +53,8 @@ async fn forward_events_cancelled_while_send_blocked_shuts_down_delegate() {
msg: EventMsg::TurnAborted(TurnAbortedEvent {
turn_id: Some("turn-1".to_string()),
reason: TurnAbortReason::Interrupted,
completed_at: None,
duration_ms: None,
}),
})
.await
+41 -7
View File
@@ -235,13 +235,19 @@ async fn interrupting_regular_turn_waiting_on_startup_prewarm_emits_turn_aborted
.await
.expect("expected turn aborted event")
.expect("channel open");
assert!(matches!(
second.msg,
EventMsg::TurnAborted(TurnAbortedEvent {
turn_id: Some(turn_id),
reason: TurnAbortReason::Interrupted,
}) if turn_id == tc.sub_id
));
let EventMsg::TurnAborted(TurnAbortedEvent {
turn_id,
reason,
completed_at,
duration_ms,
}) = second.msg
else {
panic!("expected turn aborted event");
};
assert_eq!(turn_id, Some(tc.sub_id.clone()));
assert_eq!(reason, TurnAbortReason::Interrupted);
assert!(completed_at.is_some());
assert!(duration_ms.is_some());
}
fn test_model_client_session() -> crate::client::ModelClientSession {
@@ -1300,6 +1306,7 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() {
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: turn_id.clone(),
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -1317,6 +1324,8 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() {
codex_protocol::protocol::TurnCompleteEvent {
turn_id,
last_agent_message: None,
completed_at: None,
duration_ms: None,
},
)),
];
@@ -1481,6 +1490,7 @@ async fn thread_rollback_recomputes_previous_turn_settings_and_reference_context
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: first_turn_id.clone(),
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -1499,10 +1509,13 @@ async fn thread_rollback_recomputes_previous_turn_settings_and_reference_context
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: first_turn_id,
last_agent_message: None,
completed_at: None,
duration_ms: None,
})),
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: rolled_back_turn_id.clone(),
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -1521,6 +1534,8 @@ async fn thread_rollback_recomputes_previous_turn_settings_and_reference_context
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: rolled_back_turn_id,
last_agent_message: None,
completed_at: None,
duration_ms: None,
})),
])
.await;
@@ -1579,6 +1594,7 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: first_turn_id.clone(),
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -1595,10 +1611,13 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: first_turn_id,
last_agent_message: None,
completed_at: None,
duration_ms: None,
})),
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: compact_turn_id.clone(),
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -1610,10 +1629,13 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: compact_turn_id,
last_agent_message: None,
completed_at: None,
duration_ms: None,
})),
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: rolled_back_turn_id.clone(),
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -1634,6 +1656,8 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: rolled_back_turn_id,
last_agent_message: None,
completed_at: None,
duration_ms: None,
})),
])
.await;
@@ -1661,6 +1685,7 @@ async fn thread_rollback_persists_marker_and_replays_cumulatively() {
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: "turn-1".to_string(),
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -1677,10 +1702,13 @@ async fn thread_rollback_persists_marker_and_replays_cumulatively() {
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".to_string(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
})),
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: "turn-2".to_string(),
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -1697,10 +1725,13 @@ async fn thread_rollback_persists_marker_and_replays_cumulatively() {
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-2".to_string(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
})),
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: "turn-3".to_string(),
started_at: None,
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
@@ -1717,6 +1748,8 @@ async fn thread_rollback_persists_marker_and_replays_cumulatively() {
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-3".to_string(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
})),
])
.await;
@@ -4624,6 +4657,7 @@ async fn task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input()
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id,
last_agent_message: None,
..
}) if turn_id == tc.sub_id
));
}
+1
View File
@@ -74,6 +74,7 @@ pub(crate) async fn run_compact_task(
) -> CodexResult<()> {
let start_event = EventMsg::TurnStarted(TurnStartedEvent {
turn_id: turn_context.sub_id.clone(),
started_at: turn_context.turn_timing_state.started_at_unix_secs().await,
model_context_window: turn_context.model_context_window(),
collaboration_mode_kind: turn_context.collaboration_mode.mode,
});
+1
View File
@@ -40,6 +40,7 @@ pub(crate) async fn run_remote_compact_task(
) -> CodexResult<()> {
let start_event = EventMsg::TurnStarted(TurnStartedEvent {
turn_id: turn_context.sub_id.clone(),
started_at: turn_context.turn_timing_state.started_at_unix_secs().await,
model_context_window: turn_context.model_context_window(),
collaboration_mode_kind: turn_context.collaboration_mode.mode,
});
+13
View File
@@ -511,9 +511,15 @@ impl Session {
&[("token_type", "reasoning_output"), tmp_mem],
);
}
let (completed_at, duration_ms) = turn_context
.turn_timing_state
.completed_at_and_duration_ms()
.await;
let event = EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: turn_context.sub_id.clone(),
last_agent_message,
completed_at,
duration_ms,
});
self.send_event(turn_context.as_ref(), event).await;
@@ -588,9 +594,16 @@ impl Session {
self.flush_rollout().await;
}
let (completed_at, duration_ms) = task
.turn_context
.turn_timing_state
.completed_at_and_duration_ms()
.await;
let event = EventMsg::TurnAborted(TurnAbortedEvent {
turn_id: Some(task.turn_context.sub_id.clone()),
reason,
completed_at,
duration_ms,
});
self.send_event(task.turn_context.as_ref(), event).await;
}
+1
View File
@@ -46,6 +46,7 @@ impl SessionTask for RegularTask {
// not wait on startup prewarm resolution.
let event = EventMsg::TurnStarted(TurnStartedEvent {
turn_id: ctx.sub_id.clone(),
started_at: ctx.turn_timing_state.started_at_unix_secs().await,
model_context_window: ctx.model_context_window(),
collaboration_mode_kind: ctx.collaboration_mode.mode,
});
+1
View File
@@ -111,6 +111,7 @@ pub(crate) async fn execute_user_shell_command(
// freshly reinjected context before the summary/replacement history is applied.
let event = EventMsg::TurnStarted(TurnStartedEvent {
turn_id: turn_context.sub_id.clone(),
started_at: turn_context.turn_timing_state.started_at_unix_secs().await,
model_context_window: turn_context.model_context_window(),
collaboration_mode_kind: turn_context.collaboration_mode.mode,
});
+2
View File
@@ -1010,6 +1010,8 @@ fn append_interrupted_boundary(history: InitialHistory, turn_id: Option<String>)
let aborted_event = RolloutItem::EventMsg(EventMsg::TurnAborted(TurnAbortedEvent {
turn_id,
reason: TurnAbortReason::Interrupted,
completed_at: None,
duration_ms: None,
}));
match history {
+12
View File
@@ -164,6 +164,7 @@ fn out_of_range_truncation_drops_pre_user_active_turn_prefix() {
RolloutItem::ResponseItem(assistant_msg("a1")),
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-2".to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
})),
@@ -320,6 +321,8 @@ fn interrupted_fork_snapshot_appends_interrupt_boundary() {
RolloutItem::EventMsg(EventMsg::TurnAborted(TurnAbortedEvent {
turn_id: None,
reason: TurnAbortReason::Interrupted,
completed_at: None,
duration_ms: None,
})),
])
.expect("serialize expected interrupted fork history"),
@@ -334,6 +337,8 @@ fn interrupted_fork_snapshot_appends_interrupt_boundary() {
RolloutItem::EventMsg(EventMsg::TurnAborted(TurnAbortedEvent {
turn_id: None,
reason: TurnAbortReason::Interrupted,
completed_at: None,
duration_ms: None,
})),
])
.expect("serialize expected interrupted empty history"),
@@ -349,6 +354,8 @@ fn interrupted_snapshot_is_not_mid_turn() {
RolloutItem::EventMsg(EventMsg::TurnAborted(TurnAbortedEvent {
turn_id: Some("turn-1".to_string()),
reason: TurnAbortReason::Interrupted,
completed_at: None,
duration_ms: None,
})),
]);
@@ -485,6 +492,8 @@ async fn interrupted_fork_snapshot_does_not_synthesize_turn_id_for_legacy_histor
EventMsg::TurnAborted(TurnAbortedEvent {
turn_id: expected_turn_id,
reason: TurnAbortReason::Interrupted,
completed_at: None,
duration_ms: None,
}),
))
.expect("serialize interrupted abort event");
@@ -536,6 +545,7 @@ async fn interrupted_fork_snapshot_preserves_explicit_turn_id() {
InitialHistory::Forked(vec![
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-explicit".to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: Default::default(),
})),
@@ -594,6 +604,8 @@ async fn interrupted_fork_snapshot_preserves_explicit_turn_id() {
RolloutItem::EventMsg(EventMsg::TurnAborted(TurnAbortedEvent {
turn_id: Some(turn_id),
reason: TurnAbortReason::Interrupted,
completed_at: None,
duration_ms: None,
})) if turn_id == "turn-explicit"
)
}));
@@ -837,6 +837,8 @@ async fn multi_agent_v2_list_agents_returns_completed_status_and_last_task_messa
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: child_turn.sub_id.clone(),
last_agent_message: Some("done".to_string()),
completed_at: None,
duration_ms: None,
}),
)
.await;
@@ -1337,6 +1339,8 @@ async fn multi_agent_v2_followup_task_completion_notifies_parent_on_every_turn()
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: first_turn.sub_id.clone(),
last_agent_message: Some("first done".to_string()),
completed_at: None,
duration_ms: None,
}),
)
.await;
@@ -1363,6 +1367,8 @@ async fn multi_agent_v2_followup_task_completion_notifies_parent_on_every_turn()
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: second_turn.sub_id.clone(),
last_agent_message: Some("second done".to_string()),
completed_at: None,
duration_ms: None,
}),
)
.await;
@@ -1518,6 +1524,8 @@ async fn multi_agent_v2_interrupted_turn_does_not_notify_parent() {
EventMsg::TurnAborted(TurnAbortedEvent {
turn_id: Some(aborted_turn.sub_id.clone()),
reason: TurnAbortReason::Interrupted,
completed_at: None,
duration_ms: None,
}),
)
.await;
+24
View File
@@ -1,5 +1,7 @@
use std::time::Duration;
use std::time::Instant;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
use codex_otel::metrics::names::TURN_TTFM_DURATION_METRIC;
use codex_otel::metrics::names::TURN_TTFT_DURATION_METRIC;
@@ -45,6 +47,7 @@ pub(crate) struct TurnTimingState {
#[derive(Debug, Default)]
struct TurnTimingStateInner {
started_at: Option<Instant>,
started_at_unix_secs: Option<i64>,
first_token_at: Option<Instant>,
first_message_at: Option<Instant>,
}
@@ -53,10 +56,24 @@ impl TurnTimingState {
pub(crate) async fn mark_turn_started(&self, started_at: Instant) {
let mut state = self.state.lock().await;
state.started_at = Some(started_at);
state.started_at_unix_secs = Some(now_unix_timestamp_secs());
state.first_token_at = None;
state.first_message_at = None;
}
pub(crate) async fn started_at_unix_secs(&self) -> Option<i64> {
self.state.lock().await.started_at_unix_secs
}
pub(crate) async fn completed_at_and_duration_ms(&self) -> (Option<i64>, Option<i64>) {
let state = self.state.lock().await;
let completed_at = Some(now_unix_timestamp_secs());
let duration_ms = state
.started_at
.map(|started_at| i64::try_from(started_at.elapsed().as_millis()).unwrap_or(i64::MAX));
(completed_at, duration_ms)
}
pub(crate) async fn record_ttft_for_response_event(
&self,
event: &ResponseEvent,
@@ -77,6 +94,13 @@ impl TurnTimingState {
}
}
fn now_unix_timestamp_secs() -> i64 {
let duration = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
i64::try_from(duration.as_secs()).unwrap_or(i64::MAX)
}
impl TurnTimingStateInner {
fn record_turn_ttft(&mut self) -> Option<Duration> {
if self.first_token_at.is_some() {
@@ -53,6 +53,7 @@ fn resume_history(
history: vec![
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
turn_id: turn_id.clone(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: ModeKind::Default,
})),
@@ -66,6 +67,8 @@ fn resume_history(
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id,
last_agent_message: None,
completed_at: None,
duration_ms: None,
})),
],
rollout_path: rollout_path.to_path_buf(),
@@ -167,6 +167,9 @@ fn turn_completed_recovers_final_message_from_turn_items() {
}],
status: TurnStatus::Completed,
error: None,
started_at: None,
completed_at: Some(0),
duration_ms: None,
},
},
));
@@ -211,6 +214,9 @@ fn turn_completed_overwrites_stale_final_message_from_turn_items() {
}],
status: TurnStatus::Completed,
error: None,
started_at: None,
completed_at: Some(0),
duration_ms: None,
},
},
));
@@ -251,6 +257,9 @@ fn turn_completed_preserves_streamed_final_message_when_turn_items_are_empty() {
items: Vec::new(),
status: TurnStatus::Completed,
error: None,
started_at: None,
completed_at: Some(0),
duration_ms: None,
},
},
));
@@ -291,6 +300,9 @@ fn turn_failed_clears_stale_final_message() {
items: Vec::new(),
status: TurnStatus::Failed,
error: None,
started_at: None,
completed_at: Some(0),
duration_ms: None,
},
},
));
@@ -332,6 +344,9 @@ fn turn_interrupted_clears_stale_final_message() {
items: Vec::new(),
status: TurnStatus::Interrupted,
error: None,
started_at: None,
completed_at: Some(0),
duration_ms: None,
},
},
));
@@ -38,6 +38,9 @@ fn failed_turn_does_not_overwrite_output_last_message_file() {
additional_details: None,
codex_error_info: None,
}),
started_at: None,
completed_at: Some(0),
duration_ms: None,
},
},
));
+9
View File
@@ -268,6 +268,9 @@ fn turn_items_for_thread_returns_matching_turn_items() {
}],
status: codex_app_server_protocol::TurnStatus::Completed,
error: None,
started_at: None,
completed_at: None,
duration_ms: None,
},
codex_app_server_protocol::Turn {
id: "turn-2".to_string(),
@@ -277,6 +280,9 @@ fn turn_items_for_thread_returns_matching_turn_items() {
}],
status: codex_app_server_protocol::TurnStatus::Completed,
error: None,
started_at: None,
completed_at: None,
duration_ms: None,
},
],
};
@@ -303,6 +309,9 @@ fn should_backfill_turn_completed_items_skips_ephemeral_threads() {
items: Vec::new(),
status: codex_app_server_protocol::TurnStatus::Completed,
error: None,
started_at: None,
completed_at: None,
duration_ms: None,
},
});
@@ -144,6 +144,9 @@ fn turn_started_emits_turn_started_event() {
items: Vec::new(),
status: TurnStatus::InProgress,
error: None,
started_at: None,
completed_at: None,
duration_ms: None,
},
}));
@@ -1066,6 +1069,9 @@ fn plan_update_emits_started_then_updated_then_completed() {
items: Vec::new(),
status: TurnStatus::Completed,
error: None,
started_at: None,
completed_at: None,
duration_ms: None,
},
},
));
@@ -1122,6 +1128,9 @@ fn plan_update_after_completion_starts_new_todo_list_with_new_id() {
items: Vec::new(),
status: TurnStatus::Completed,
error: None,
started_at: None,
completed_at: None,
duration_ms: None,
},
},
));
@@ -1201,6 +1210,9 @@ fn token_usage_update_is_emitted_on_turn_completion() {
items: Vec::new(),
status: TurnStatus::Completed,
error: None,
started_at: None,
completed_at: None,
duration_ms: None,
},
},
));
@@ -1236,6 +1248,9 @@ fn turn_completion_recovers_final_message_from_turn_items() {
}],
status: TurnStatus::Completed,
error: None,
started_at: None,
completed_at: None,
duration_ms: None,
},
},
));
@@ -1310,6 +1325,9 @@ fn turn_completion_reconciles_started_items_from_turn_items() {
}],
status: TurnStatus::Completed,
error: None,
started_at: None,
completed_at: None,
duration_ms: None,
},
},
));
@@ -1367,6 +1385,9 @@ fn turn_completion_overwrites_stale_final_message_from_turn_items() {
}],
status: TurnStatus::Completed,
error: None,
started_at: None,
completed_at: None,
duration_ms: None,
},
},
));
@@ -1407,6 +1428,9 @@ fn turn_completion_preserves_streamed_final_message_when_turn_items_are_empty()
items: Vec::new(),
status: TurnStatus::Completed,
error: None,
started_at: None,
completed_at: None,
duration_ms: None,
},
},
));
@@ -1455,6 +1479,9 @@ fn failed_turn_clears_stale_final_message() {
additional_details: None,
codex_error_info: None,
}),
started_at: None,
completed_at: None,
duration_ms: None,
},
},
));
@@ -1478,6 +1505,9 @@ fn turn_completion_falls_back_to_final_plan_text() {
}],
status: TurnStatus::Completed,
error: None,
started_at: None,
completed_at: None,
duration_ms: None,
},
},
));
@@ -1526,6 +1556,9 @@ fn turn_failure_prefers_structured_error_message() {
items: Vec::new(),
status: TurnStatus::Failed,
error: None,
started_at: None,
completed_at: None,
duration_ms: None,
},
},
));
+23 -1
View File
@@ -1864,11 +1864,23 @@ pub struct ContextCompactedEvent;
pub struct TurnCompleteEvent {
pub turn_id: String,
pub last_agent_message: Option<String>,
/// Unix timestamp (in seconds) when the turn completed.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "number | null", optional)]
pub completed_at: Option<i64>,
/// Duration between turn start and completion in milliseconds, if known.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "number | null", optional)]
pub duration_ms: Option<i64>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct TurnStartedEvent {
pub turn_id: String,
/// Unix timestamp (in seconds) when the turn started.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "number | null", optional)]
pub started_at: Option<i64>,
// TODO(aibrahim): make this not optional
pub model_context_window: Option<i64>,
#[serde(default)]
@@ -3375,6 +3387,14 @@ pub struct Chunk {
pub struct TurnAbortedEvent {
pub turn_id: Option<String>,
pub reason: TurnAbortReason,
/// Unix timestamp (in seconds) when the turn was aborted.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "number | null", optional)]
pub completed_at: Option<i64>,
/// Duration between turn start and abort in milliseconds, if known.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "number | null", optional)]
pub duration_ms: Option<i64>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
@@ -4543,7 +4563,9 @@ mod tests {
}))?;
match event {
EventMsg::TurnAborted(TurnAbortedEvent { turn_id, reason }) => {
EventMsg::TurnAborted(TurnAbortedEvent {
turn_id, reason, ..
}) => {
assert_eq!(turn_id, None);
assert_eq!(reason, TurnAbortReason::Interrupted);
}
+18 -2
View File
@@ -9186,13 +9186,19 @@ guardian_approval = true
items,
status,
error: None,
started_at: None,
completed_at: None,
duration_ms: None,
}
}
fn turn_started_notification(thread_id: ThreadId, turn_id: &str) -> ServerNotification {
ServerNotification::TurnStarted(TurnStartedNotification {
thread_id: thread_id.to_string(),
turn: test_turn(turn_id, TurnStatus::InProgress, Vec::new()),
turn: Turn {
started_at: Some(0),
..test_turn(turn_id, TurnStatus::InProgress, Vec::new())
},
})
}
@@ -9203,7 +9209,11 @@ guardian_approval = true
) -> ServerNotification {
ServerNotification::TurnCompleted(TurnCompletedNotification {
thread_id: thread_id.to_string(),
turn: test_turn(turn_id, status, Vec::new()),
turn: Turn {
completed_at: Some(0),
duration_ms: Some(1),
..test_turn(turn_id, status, Vec::new())
},
})
}
@@ -10424,6 +10434,9 @@ guardian_approval = true
}],
status: TurnStatus::Completed,
error: None,
started_at: None,
completed_at: None,
duration_ms: None,
},
Turn {
id: "turn-2".to_string(),
@@ -10444,6 +10457,9 @@ guardian_approval = true
],
status: TurnStatus::Completed,
error: None,
started_at: None,
completed_at: None,
duration_ms: None,
},
],
events: Vec::new(),
+41 -1
View File
@@ -501,6 +501,7 @@ fn server_notification_thread_events(
id: String::new(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
turn_id: notification.turn.id,
started_at: notification.turn.started_at,
model_context_window: None,
collaboration_mode_kind: ModeKind::default(),
}),
@@ -676,6 +677,7 @@ fn turn_snapshot_events(
id: String::new(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
turn_id: turn.id.clone(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: ModeKind::default(),
}),
@@ -741,6 +743,8 @@ fn append_terminal_turn_events(events: &mut Vec<Event>, turn: &Turn, include_fai
msg: EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: turn.id.clone(),
last_agent_message: None,
completed_at: turn.completed_at,
duration_ms: turn.duration_ms,
}),
}),
TurnStatus::Interrupted => events.push(Event {
@@ -748,6 +752,8 @@ fn append_terminal_turn_events(events: &mut Vec<Event>, turn: &Turn, include_fai
msg: EventMsg::TurnAborted(TurnAbortedEvent {
turn_id: Some(turn.id.clone()),
reason: TurnAbortReason::Interrupted,
completed_at: turn.completed_at,
duration_ms: turn.duration_ms,
}),
}),
TurnStatus::Failed => {
@@ -768,6 +774,8 @@ fn append_terminal_turn_events(events: &mut Vec<Event>, turn: &Turn, include_fai
msg: EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: turn.id.clone(),
last_agent_message: None,
completed_at: turn.completed_at,
duration_ms: turn.duration_ms,
}),
});
}
@@ -1103,6 +1111,9 @@ mod tests {
items: Vec::new(),
status: TurnStatus::Completed,
error: None,
started_at: None,
completed_at: Some(0),
duration_ms: None,
},
}),
)
@@ -1121,6 +1132,8 @@ mod tests {
};
assert_eq!(completed.turn_id, turn_id);
assert_eq!(completed.last_agent_message, None);
assert_eq!(completed.completed_at, Some(0));
assert_eq!(completed.duration_ms, None);
}
#[test]
@@ -1284,6 +1297,9 @@ mod tests {
}],
status: TurnStatus::Completed,
error: None,
started_at: None,
completed_at: None,
duration_ms: None,
}],
};
@@ -1315,6 +1331,9 @@ mod tests {
items: Vec::new(),
status: TurnStatus::Interrupted,
error: None,
started_at: None,
completed_at: Some(0),
duration_ms: None,
},
}),
)
@@ -1351,6 +1370,9 @@ mod tests {
codex_error_info: Some(CodexErrorInfo::Other),
additional_details: None,
}),
started_at: None,
completed_at: Some(0),
duration_ms: None,
},
}),
)
@@ -1453,12 +1475,18 @@ mod tests {
],
status: TurnStatus::Completed,
error: None,
started_at: None,
completed_at: None,
duration_ms: None,
},
Turn {
id: "turn-interrupted".to_string(),
items: Vec::new(),
status: TurnStatus::Interrupted,
error: None,
started_at: None,
completed_at: None,
duration_ms: None,
},
Turn {
id: "turn-failed".to_string(),
@@ -1469,6 +1497,9 @@ mod tests {
codex_error_info: Some(CodexErrorInfo::Other),
additional_details: None,
}),
started_at: None,
completed_at: None,
duration_ms: None,
},
],
},
@@ -1481,7 +1512,10 @@ mod tests {
assert!(matches!(events[2].msg, EventMsg::ItemCompleted(_)));
assert!(matches!(events[3].msg, EventMsg::TurnComplete(_)));
assert!(matches!(events[4].msg, EventMsg::TurnStarted(_)));
let EventMsg::TurnAborted(TurnAbortedEvent { turn_id, reason }) = &events[5].msg else {
let EventMsg::TurnAborted(TurnAbortedEvent {
turn_id, reason, ..
}) = &events[5].msg
else {
panic!("expected interrupted turn replay");
};
assert_eq!(turn_id.as_deref(), Some("turn-interrupted"));
@@ -1528,6 +1562,9 @@ mod tests {
],
status: TurnStatus::Completed,
error: None,
started_at: None,
completed_at: None,
duration_ms: None,
},
/*show_raw_agent_reasoning*/ false,
);
@@ -1571,6 +1608,9 @@ mod tests {
}],
status: TurnStatus::Completed,
error: None,
started_at: None,
completed_at: None,
duration_ms: None,
},
/*show_raw_agent_reasoning*/ true,
);
@@ -676,6 +676,9 @@ mod tests {
items: Vec::new(),
status: TurnStatus::Completed,
error: None,
started_at: None,
completed_at: Some(0),
duration_ms: Some(1),
},
})
}
+3
View File
@@ -1287,6 +1287,9 @@ mod tests {
],
status: TurnStatus::Completed,
error: None,
started_at: None,
completed_at: None,
duration_ms: None,
}],
},
model: "gpt-5.4".to_string(),
+6
View File
@@ -5889,6 +5889,9 @@ impl ChatWidget {
items,
status,
error,
started_at,
completed_at,
duration_ms,
} = turn;
if matches!(status, TurnStatus::InProgress) {
self.last_non_retry_error = None;
@@ -5909,6 +5912,9 @@ impl ChatWidget {
items: Vec::new(),
status,
error,
started_at,
completed_at,
duration_ms,
},
},
Some(replay_kind),
@@ -93,6 +93,9 @@ async fn live_app_server_turn_completed_clears_working_status_after_answer_item(
items: Vec::new(),
status: AppServerTurnStatus::InProgress,
error: None,
started_at: Some(0),
completed_at: None,
duration_ms: None,
},
}),
/*replay_kind*/ None,
@@ -132,6 +135,9 @@ async fn live_app_server_turn_completed_clears_working_status_after_answer_item(
items: Vec::new(),
status: AppServerTurnStatus::Completed,
error: None,
started_at: None,
completed_at: Some(0),
duration_ms: None,
},
}),
/*replay_kind*/ None,
@@ -415,6 +421,9 @@ async fn live_app_server_failed_turn_does_not_duplicate_error_history() {
items: Vec::new(),
status: AppServerTurnStatus::InProgress,
error: None,
started_at: Some(0),
completed_at: None,
duration_ms: None,
},
}),
/*replay_kind*/ None,
@@ -450,6 +459,9 @@ async fn live_app_server_failed_turn_does_not_duplicate_error_history() {
codex_error_info: None,
additional_details: None,
}),
started_at: None,
completed_at: Some(0),
duration_ms: None,
},
}),
/*replay_kind*/ None,
@@ -471,6 +483,9 @@ async fn live_app_server_stream_recovery_restores_previous_status_header() {
items: Vec::new(),
status: AppServerTurnStatus::InProgress,
error: None,
started_at: Some(0),
completed_at: None,
duration_ms: None,
},
}),
/*replay_kind*/ None,
@@ -525,6 +540,9 @@ async fn live_app_server_server_overloaded_error_renders_warning() {
items: Vec::new(),
status: AppServerTurnStatus::InProgress,
error: None,
started_at: Some(0),
completed_at: None,
duration_ms: None,
},
}),
/*replay_kind*/ None,
@@ -618,6 +618,8 @@ async fn interrupted_turn_restore_keeps_active_mode_for_resubmission() {
msg: EventMsg::TurnAborted(codex_protocol::protocol::TurnAbortedEvent {
turn_id: Some("turn-1".to_string()),
reason: TurnAbortReason::Interrupted,
completed_at: None,
duration_ms: None,
}),
});
@@ -1040,6 +1042,8 @@ async fn interrupt_restores_queued_messages_into_composer() {
msg: EventMsg::TurnAborted(codex_protocol::protocol::TurnAbortedEvent {
turn_id: Some("turn-1".to_string()),
reason: TurnAbortReason::Interrupted,
completed_at: None,
duration_ms: None,
}),
});
@@ -1079,6 +1083,8 @@ async fn interrupt_prepends_queued_messages_before_existing_composer_text() {
msg: EventMsg::TurnAborted(codex_protocol::protocol::TurnAbortedEvent {
turn_id: Some("turn-1".to_string()),
reason: TurnAbortReason::Interrupted,
completed_at: None,
duration_ms: None,
}),
});
@@ -635,6 +635,7 @@ async fn unified_exec_wait_after_final_agent_message_snapshot() {
id: "turn-1".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: ModeKind::Default,
}),
@@ -649,6 +650,8 @@ async fn unified_exec_wait_after_final_agent_message_snapshot() {
msg: EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".to_string(),
last_agent_message: Some("Final response.".into()),
completed_at: None,
duration_ms: None,
}),
});
@@ -667,6 +670,7 @@ async fn unified_exec_wait_before_streamed_agent_message_snapshot() {
id: "turn-1".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: ModeKind::Default,
}),
@@ -691,6 +695,8 @@ async fn unified_exec_wait_before_streamed_agent_message_snapshot() {
msg: EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".to_string(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
}),
});
@@ -756,6 +762,8 @@ async fn unified_exec_waiting_multiple_empty_snapshots() {
msg: EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".to_string(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
}),
});
@@ -834,6 +842,8 @@ async fn unified_exec_non_empty_then_empty_snapshots() {
msg: EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".to_string(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
}),
});
@@ -1259,6 +1269,8 @@ async fn interrupt_preserves_unified_exec_processes() {
msg: EventMsg::TurnAborted(codex_protocol::protocol::TurnAbortedEvent {
turn_id: Some("turn-1".to_string()),
reason: TurnAbortReason::Interrupted,
completed_at: None,
duration_ms: None,
}),
});
@@ -1291,6 +1303,7 @@ async fn interrupt_preserves_unified_exec_wait_streak_snapshot() {
id: "turn-1".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: ModeKind::Default,
}),
@@ -1304,6 +1317,8 @@ async fn interrupt_preserves_unified_exec_wait_streak_snapshot() {
msg: EventMsg::TurnAborted(codex_protocol::protocol::TurnAbortedEvent {
turn_id: Some("turn-1".to_string()),
reason: TurnAbortReason::Interrupted,
completed_at: None,
duration_ms: None,
}),
});
@@ -1331,6 +1346,8 @@ async fn turn_complete_keeps_unified_exec_processes() {
msg: EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".to_string(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
}),
});
@@ -536,6 +536,9 @@ async fn replayed_retryable_app_server_error_keeps_turn_running() {
items: Vec::new(),
status: AppServerTurnStatus::InProgress,
error: None,
started_at: Some(0),
completed_at: None,
duration_ms: None,
},
}),
Some(ReplayKind::ThreadSnapshot),
@@ -686,6 +689,9 @@ async fn live_reasoning_summary_is_not_rendered_twice_when_item_completes() {
items: Vec::new(),
status: AppServerTurnStatus::InProgress,
error: None,
started_at: Some(0),
completed_at: None,
duration_ms: None,
},
}),
/*replay_kind*/ None,
@@ -731,6 +737,7 @@ async fn replayed_turn_started_does_not_mark_task_running() {
chat.replay_initial_messages(vec![EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: ModeKind::Default,
})]);
@@ -747,6 +754,7 @@ async fn thread_snapshot_replayed_turn_started_marks_task_running() {
id: "turn-1".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: ModeKind::Default,
}),
@@ -771,6 +779,9 @@ async fn replayed_in_progress_turn_marks_task_running() {
items: Vec::new(),
status: AppServerTurnStatus::InProgress,
error: None,
started_at: None,
completed_at: None,
duration_ms: None,
}],
ReplayKind::ResumeInitialMessages,
);
@@ -813,6 +824,7 @@ async fn thread_snapshot_replayed_stream_recovery_restores_previous_status_heade
id: "task".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: ModeKind::Default,
}),
@@ -853,6 +865,7 @@ async fn resume_replay_interrupted_reconnect_does_not_leave_stale_working_state(
chat.replay_initial_messages(vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: ModeKind::Default,
}),
@@ -884,6 +897,7 @@ async fn replayed_interrupted_reconnect_footer_row_snapshot() {
chat.replay_initial_messages(vec![
EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: ModeKind::Default,
}),
@@ -909,6 +923,7 @@ async fn stream_recovery_restores_previous_status_header() {
id: "task".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: ModeKind::Default,
}),
@@ -34,6 +34,7 @@ async fn mcp_startup_complete_does_not_clear_running_task() {
id: "task-1".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: ModeKind::Default,
}),
@@ -566,6 +566,8 @@ async fn plan_implementation_popup_skips_replayed_turn_complete() {
chat.replay_initial_messages(vec![EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".to_string(),
last_agent_message: Some("Plan details".to_string()),
completed_at: None,
duration_ms: None,
})]);
let popup = render_bottom_popup(&chat, /*width*/ 80);
@@ -590,6 +592,8 @@ async fn plan_implementation_popup_shows_once_when_replay_precedes_live_turn_com
chat.replay_initial_messages(vec![EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".to_string(),
last_agent_message: Some("Plan details".to_string()),
completed_at: None,
duration_ms: None,
})]);
let replay_popup = render_bottom_popup(&chat, /*width*/ 80);
assert!(
@@ -602,6 +606,8 @@ async fn plan_implementation_popup_shows_once_when_replay_precedes_live_turn_com
msg: EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".to_string(),
last_agent_message: Some("Plan details".to_string()),
completed_at: None,
duration_ms: None,
}),
});
@@ -623,6 +629,8 @@ async fn plan_implementation_popup_shows_once_when_replay_precedes_live_turn_com
msg: EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".to_string(),
last_agent_message: Some("Plan details".to_string()),
completed_at: None,
duration_ms: None,
}),
});
let duplicate_popup = render_bottom_popup(&chat, /*width*/ 80);
@@ -850,6 +858,9 @@ async fn submit_user_message_queues_while_compaction_turn_is_running() {
items: Vec::new(),
status: AppServerTurnStatus::InProgress,
error: None,
started_at: Some(0),
completed_at: None,
duration_ms: None,
},
}),
/*replay_kind*/ None,
@@ -893,6 +904,9 @@ async fn submit_user_message_queues_while_compaction_turn_is_running() {
items: Vec::new(),
status: AppServerTurnStatus::Completed,
error: None,
started_at: None,
completed_at: Some(0),
duration_ms: None,
},
}),
/*replay_kind*/ None,
@@ -61,6 +61,8 @@ async fn interrupted_turn_restores_queued_messages_with_images_and_elements() {
msg: EventMsg::TurnAborted(codex_protocol::protocol::TurnAbortedEvent {
turn_id: Some("turn-1".to_string()),
reason: TurnAbortReason::Interrupted,
completed_at: None,
duration_ms: None,
}),
});
@@ -146,6 +148,7 @@ async fn steer_rejection_queues_review_follow_up_before_existing_queued_messages
id: "turn-start".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: ModeKind::Default,
}),
@@ -229,6 +232,8 @@ async fn steer_rejection_queues_review_follow_up_before_existing_queued_messages
msg: EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".to_string(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
}),
});
@@ -248,6 +253,8 @@ async fn steer_rejection_queues_review_follow_up_before_existing_queued_messages
msg: EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-2".to_string(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
}),
});
@@ -932,6 +939,8 @@ async fn replaced_turn_clears_pending_steers_but_keeps_queued_drafts() {
msg: EventMsg::TurnAborted(codex_protocol::protocol::TurnAbortedEvent {
turn_id: Some("turn-1".to_string()),
reason: TurnAbortReason::Replaced,
completed_at: None,
duration_ms: None,
}),
});
@@ -1155,6 +1164,8 @@ async fn interrupt_exec_marks_failed_snapshot() {
msg: EventMsg::TurnAborted(codex_protocol::protocol::TurnAbortedEvent {
turn_id: Some("turn-1".to_string()),
reason: TurnAbortReason::Interrupted,
completed_at: None,
duration_ms: None,
}),
});
@@ -1180,6 +1191,7 @@ async fn interrupted_turn_error_message_snapshot() {
id: "task-1".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: ModeKind::Default,
}),
@@ -1191,6 +1203,8 @@ async fn interrupted_turn_error_message_snapshot() {
msg: EventMsg::TurnAborted(codex_protocol::protocol::TurnAbortedEvent {
turn_id: Some("turn-1".to_string()),
reason: TurnAbortReason::Interrupted,
completed_at: None,
duration_ms: None,
}),
});
@@ -1217,6 +1231,7 @@ async fn interrupted_turn_pending_steers_message_snapshot() {
id: "task-1".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: ModeKind::Default,
}),
@@ -1227,6 +1242,8 @@ async fn interrupted_turn_pending_steers_message_snapshot() {
msg: EventMsg::TurnAborted(codex_protocol::protocol::TurnAbortedEvent {
turn_id: Some("turn-1".to_string()),
reason: TurnAbortReason::Interrupted,
completed_at: None,
duration_ms: None,
}),
});
@@ -1323,6 +1340,8 @@ async fn review_ended_keeps_unified_exec_processes() {
msg: EventMsg::TurnAborted(codex_protocol::protocol::TurnAbortedEvent {
turn_id: Some("turn-1".to_string()),
reason: TurnAbortReason::ReviewEnded,
completed_at: None,
duration_ms: None,
}),
});
@@ -1355,6 +1374,7 @@ async fn enter_submits_steer_while_review_is_running() {
id: "turn-start".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: ModeKind::Default,
}),
@@ -1403,6 +1423,7 @@ async fn review_queues_user_messages_snapshot() {
id: "turn-start".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: ModeKind::Default,
}),
@@ -97,6 +97,8 @@ async fn slash_copy_state_tracks_turn_complete_final_reply() {
msg: EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".to_string(),
last_agent_message: Some("Final reply **markdown**".to_string()),
completed_at: None,
duration_ms: None,
}),
});
@@ -127,6 +129,8 @@ async fn slash_copy_state_tracks_plan_item_completion() {
msg: EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".to_string(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
}),
});
@@ -160,6 +164,8 @@ async fn slash_copy_state_is_preserved_during_running_task() {
msg: EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".to_string(),
last_agent_message: Some("Previous completed reply".to_string()),
completed_at: None,
duration_ms: None,
}),
});
chat.on_task_started();
@@ -179,6 +185,8 @@ async fn slash_copy_state_clears_on_thread_rollback() {
msg: EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".to_string(),
last_agent_message: Some("Reply that will be rolled back".to_string()),
completed_at: None,
duration_ms: None,
}),
});
chat.handle_codex_event(Event {
@@ -207,6 +215,8 @@ async fn slash_copy_is_unavailable_when_legacy_agent_message_is_not_repeated_on_
msg: EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".to_string(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
}),
});
let _ = drain_insert_history(&mut rx);
@@ -232,6 +242,7 @@ async fn slash_copy_uses_agent_message_item_when_turn_complete_omits_final_text(
id: "turn-1".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: ModeKind::Default,
}),
@@ -248,6 +259,8 @@ async fn slash_copy_uses_agent_message_item_when_turn_complete_omits_final_text(
msg: EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".to_string(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
}),
});
let _ = drain_insert_history(&mut rx);
@@ -277,6 +290,7 @@ async fn slash_copy_does_not_return_stale_output_after_thread_rollback() {
id: "turn-1".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: ModeKind::Default,
}),
@@ -293,6 +307,8 @@ async fn slash_copy_does_not_return_stale_output_after_thread_rollback() {
msg: EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".to_string(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
}),
});
let _ = drain_insert_history(&mut rx);
@@ -656,6 +672,7 @@ async fn compact_queues_user_messages_snapshot() {
id: "turn-start".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: ModeKind::Default,
}),
@@ -73,6 +73,7 @@ async fn turn_started_uses_runtime_context_window_before_first_token_count() {
id: "turn-start".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".to_string(),
started_at: None,
model_context_window: Some(950_000),
collaboration_mode_kind: ModeKind::Default,
}),
@@ -628,6 +629,7 @@ async fn ui_snapshots_small_heights_task_running() {
id: "task-1".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: ModeKind::Default,
}),
@@ -661,6 +663,7 @@ async fn status_widget_and_approval_modal_snapshot() {
id: "task-1".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: ModeKind::Default,
}),
@@ -723,6 +726,7 @@ async fn status_widget_active_snapshot() {
id: "task-1".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: ModeKind::Default,
}),
@@ -877,6 +881,8 @@ async fn status_line_branch_refreshes_after_turn_complete() {
msg: EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".to_string(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
}),
});
@@ -895,6 +901,8 @@ async fn status_line_branch_refreshes_after_interrupt() {
msg: EventMsg::TurnAborted(codex_protocol::protocol::TurnAbortedEvent {
turn_id: Some("turn-1".to_string()),
reason: TurnAbortReason::Interrupted,
completed_at: None,
duration_ms: None,
}),
});
@@ -1120,6 +1128,7 @@ async fn multiple_agent_messages_in_single_turn_emit_multiple_headers() {
id: "s1".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: ModeKind::Default,
}),
@@ -1142,6 +1151,8 @@ async fn multiple_agent_messages_in_single_turn_emit_multiple_headers() {
msg: EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".to_string(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
}),
});
@@ -1425,6 +1436,7 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() {
id: "t1".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: ModeKind::Default,
}),
@@ -1477,6 +1489,7 @@ async fn chatwidget_markdown_code_blocks_vt100_snapshot() {
id: "t1".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: ModeKind::Default,
}),
@@ -1551,6 +1564,8 @@ printf 'fenced within fenced\n'
msg: EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".to_string(),
last_agent_message: None,
completed_at: None,
duration_ms: None,
}),
});
for lines in drain_insert_history(&mut rx) {
@@ -1572,6 +1587,7 @@ async fn chatwidget_tall() {
id: "t1".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: ModeKind::Default,
}),