[codex-analytics] add item lifecycle timing (#20514)

## Why

Tool families already disagree on what their existing `duration` fields
mean, so lifecycle latency should live on the shared item envelope
instead of being inferred from per-tool execution fields. Carrying that
envelope through app-server notifications gives downstream consumers one
reusable timing signal without pretending every tool has the same
execution semantics.

## What changed

- Adds `started_at_ms` to core `ItemStartedEvent` values and
`completed_at_ms` to core `ItemCompletedEvent` values.
- Populates those timestamps in the shared session lifecycle emitters,
so protocol-native items get timing without each producer tracking its
own clock state.
- Exposes `startedAtMs` on app-server `item/started` notifications and
`completedAtMs` on `item/completed` notifications.
- Maps the lifecycle timestamps through the app-server boundary while
leaving legacy-converted notifications nullable when no lifecycle
timestamp exists.
- Regenerates the app-server JSON schema and TypeScript fixtures for the
notification-envelope change and updates downstream fixtures that
construct those notifications directly.
- Extends the existing web-search and image-generation integration flows
to assert the new lifecycle timestamps on the native item events.

## Verification

- `cargo check -p codex-protocol -p codex-core -p
codex-app-server-protocol -p codex-app-server -p codex-tui -p codex-exec
-p codex-app-server-client`
- `cargo test -p codex-core --test all web_search_item_is_emitted`
- `cargo test -p codex-core --test all
image_generation_call_event_is_emitted`
- `cargo test -p codex-app-server-protocol`

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/20514).
* #18748
* #18747
* #17090
* #17089
* __->__ #20514
This commit is contained in:
rhan-oai
2026-05-04 15:33:20 -07:00
committed by GitHub
Unverified
parent e7e6267ab3
commit aee1fe2659
37 changed files with 332 additions and 11 deletions
+2
View File
@@ -1139,6 +1139,7 @@ mod tests {
ServerNotification::ItemCompleted(codex_app_server_protocol::ItemCompletedNotification {
thread_id: "thread".to_string(),
turn_id: "turn".to_string(),
completed_at_ms: 0,
item: codex_app_server_protocol::ThreadItem::AgentMessage {
id: "item".to_string(),
text: text.to_string(),
@@ -2012,6 +2013,7 @@ mod tests {
codex_app_server_protocol::ItemCompletedNotification {
thread_id: "thread".to_string(),
turn_id: "turn".to_string(),
completed_at_ms: 0,
item: codex_app_server_protocol::ThreadItem::AgentMessage {
id: "item".to_string(),
text: "hello".to_string(),
@@ -1932,6 +1932,11 @@
},
"ItemCompletedNotification": {
"properties": {
"completedAtMs": {
"description": "Unix timestamp (in milliseconds) when this item lifecycle completed.",
"format": "int64",
"type": "integer"
},
"item": {
"$ref": "#/definitions/ThreadItem"
},
@@ -1943,6 +1948,7 @@
}
},
"required": [
"completedAtMs",
"item",
"threadId",
"turnId"
@@ -2030,6 +2036,11 @@
"item": {
"$ref": "#/definitions/ThreadItem"
},
"startedAtMs": {
"description": "Unix timestamp (in milliseconds) when this item lifecycle started.",
"format": "int64",
"type": "integer"
},
"threadId": {
"type": "string"
},
@@ -2039,6 +2050,7 @@
},
"required": [
"item",
"startedAtMs",
"threadId",
"turnId"
],
@@ -5896,4 +5908,4 @@
}
],
"title": "ServerNotification"
}
}
@@ -10109,6 +10109,11 @@
"ItemCompletedNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"completedAtMs": {
"description": "Unix timestamp (in milliseconds) when this item lifecycle completed.",
"format": "int64",
"type": "integer"
},
"item": {
"$ref": "#/definitions/v2/ThreadItem"
},
@@ -10120,6 +10125,7 @@
}
},
"required": [
"completedAtMs",
"item",
"threadId",
"turnId"
@@ -10213,6 +10219,11 @@
"item": {
"$ref": "#/definitions/v2/ThreadItem"
},
"startedAtMs": {
"description": "Unix timestamp (in milliseconds) when this item lifecycle started.",
"format": "int64",
"type": "integer"
},
"threadId": {
"type": "string"
},
@@ -10222,6 +10233,7 @@
},
"required": [
"item",
"startedAtMs",
"threadId",
"turnId"
],
@@ -18261,4 +18273,4 @@
},
"title": "CodexAppServerProtocol",
"type": "object"
}
}
@@ -6762,6 +6762,11 @@
"ItemCompletedNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"completedAtMs": {
"description": "Unix timestamp (in milliseconds) when this item lifecycle completed.",
"format": "int64",
"type": "integer"
},
"item": {
"$ref": "#/definitions/ThreadItem"
},
@@ -6773,6 +6778,7 @@
}
},
"required": [
"completedAtMs",
"item",
"threadId",
"turnId"
@@ -6866,6 +6872,11 @@
"item": {
"$ref": "#/definitions/ThreadItem"
},
"startedAtMs": {
"description": "Unix timestamp (in milliseconds) when this item lifecycle started.",
"format": "int64",
"type": "integer"
},
"threadId": {
"type": "string"
},
@@ -6875,6 +6886,7 @@
},
"required": [
"item",
"startedAtMs",
"threadId",
"turnId"
],
@@ -16146,4 +16158,4 @@
},
"title": "CodexAppServerProtocolV2",
"type": "object"
}
}
@@ -1370,6 +1370,11 @@
}
},
"properties": {
"completedAtMs": {
"description": "Unix timestamp (in milliseconds) when this item lifecycle completed.",
"format": "int64",
"type": "integer"
},
"item": {
"$ref": "#/definitions/ThreadItem"
},
@@ -1381,10 +1386,11 @@
}
},
"required": [
"completedAtMs",
"item",
"threadId",
"turnId"
],
"title": "ItemCompletedNotification",
"type": "object"
}
}
@@ -1373,6 +1373,11 @@
"item": {
"$ref": "#/definitions/ThreadItem"
},
"startedAtMs": {
"description": "Unix timestamp (in milliseconds) when this item lifecycle started.",
"format": "int64",
"type": "integer"
},
"threadId": {
"type": "string"
},
@@ -1382,9 +1387,10 @@
},
"required": [
"item",
"startedAtMs",
"threadId",
"turnId"
],
"title": "ItemStartedNotification",
"type": "object"
}
}
@@ -3,4 +3,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ThreadItem } from "./ThreadItem";
export type ItemCompletedNotification = { item: ThreadItem, threadId: string, turnId: string, };
export type ItemCompletedNotification = { item: ThreadItem, threadId: string, turnId: string,
/**
* Unix timestamp (in milliseconds) when this item lifecycle completed.
*/
completedAtMs: number, };
@@ -3,4 +3,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ThreadItem } from "./ThreadItem";
export type ItemStartedNotification = { item: ThreadItem, threadId: string, turnId: string, };
export type ItemStartedNotification = { item: ThreadItem, threadId: string, turnId: string,
/**
* Unix timestamp (in milliseconds) when this item lifecycle started.
*/
startedAtMs: number, };
@@ -69,6 +69,7 @@ pub fn item_event_to_server_notification(
thread_id,
turn_id: response.turn_id,
item,
completed_at_ms: response.completed_at_ms,
})
}
EventMsg::CollabAgentSpawnBegin(begin_event) => {
@@ -87,6 +88,7 @@ pub fn item_event_to_server_notification(
thread_id,
turn_id,
item,
started_at_ms: begin_event.started_at_ms,
})
}
EventMsg::CollabAgentSpawnEnd(end_event) => {
@@ -125,6 +127,7 @@ pub fn item_event_to_server_notification(
thread_id,
turn_id,
item,
completed_at_ms: end_event.completed_at_ms,
})
}
EventMsg::CollabAgentInteractionBegin(begin_event) => {
@@ -144,6 +147,7 @@ pub fn item_event_to_server_notification(
thread_id,
turn_id,
item,
started_at_ms: begin_event.started_at_ms,
})
}
EventMsg::CollabAgentInteractionEnd(end_event) => {
@@ -171,6 +175,7 @@ pub fn item_event_to_server_notification(
thread_id,
turn_id,
item,
completed_at_ms: end_event.completed_at_ms,
})
}
EventMsg::CollabWaitingBegin(begin_event) => {
@@ -194,6 +199,7 @@ pub fn item_event_to_server_notification(
thread_id,
turn_id,
item,
started_at_ms: begin_event.started_at_ms,
})
}
EventMsg::CollabWaitingEnd(end_event) => {
@@ -229,6 +235,7 @@ pub fn item_event_to_server_notification(
thread_id,
turn_id,
item,
completed_at_ms: end_event.completed_at_ms,
})
}
EventMsg::CollabCloseBegin(begin_event) => {
@@ -247,6 +254,7 @@ pub fn item_event_to_server_notification(
thread_id,
turn_id,
item,
started_at_ms: begin_event.started_at_ms,
})
}
EventMsg::CollabCloseEnd(end_event) => {
@@ -279,6 +287,7 @@ pub fn item_event_to_server_notification(
thread_id,
turn_id,
item,
completed_at_ms: end_event.completed_at_ms,
})
}
EventMsg::CollabResumeBegin(begin_event) => {
@@ -297,6 +306,7 @@ pub fn item_event_to_server_notification(
thread_id,
turn_id,
item,
started_at_ms: begin_event.started_at_ms,
})
}
EventMsg::CollabResumeEnd(end_event) => {
@@ -329,6 +339,7 @@ pub fn item_event_to_server_notification(
thread_id,
turn_id,
item,
completed_at_ms: end_event.completed_at_ms,
})
}
EventMsg::AgentMessageContentDelta(event) => {
@@ -378,6 +389,7 @@ pub fn item_event_to_server_notification(
thread_id,
turn_id,
item: item_started_event.item.into(),
started_at_ms: item_started_event.started_at_ms,
})
}
EventMsg::ItemCompleted(item_completed_event) => {
@@ -385,6 +397,7 @@ pub fn item_event_to_server_notification(
thread_id,
turn_id,
item: item_completed_event.item.into(),
completed_at_ms: item_completed_event.completed_at_ms,
})
}
EventMsg::PatchApplyUpdated(event) => {
@@ -400,6 +413,7 @@ pub fn item_event_to_server_notification(
thread_id,
turn_id,
item: build_command_execution_begin_item(&exec_command_begin_event),
started_at_ms: exec_command_begin_event.started_at_ms,
})
}
EventMsg::ExecCommandOutputDelta(exec_command_output_delta_event) => {
@@ -428,6 +442,7 @@ pub fn item_event_to_server_notification(
thread_id,
turn_id,
item: build_command_execution_end_item(&exec_command_end_event),
completed_at_ms: exec_command_end_event.completed_at_ms,
})
}
_ => unreachable!("unsupported item event"),
@@ -480,6 +495,7 @@ mod tests {
fn collab_resume_begin_maps_to_item_started_resume_agent() {
let event = CollabResumeBeginEvent {
call_id: "call-1".to_string(),
started_at_ms: 123,
sender_thread_id: ThreadId::new(),
receiver_thread_id: ThreadId::new(),
receiver_agent_nickname: None,
@@ -496,6 +512,7 @@ mod tests {
ItemStartedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: event.started_at_ms,
item: ThreadItem::CollabAgentToolCall {
id: event.call_id,
tool: CollabAgentTool::ResumeAgent,
@@ -515,6 +532,7 @@ mod tests {
fn collab_resume_end_maps_to_item_completed_resume_agent() {
let event = CollabResumeEndEvent {
call_id: "call-2".to_string(),
completed_at_ms: 456,
sender_thread_id: ThreadId::new(),
receiver_thread_id: ThreadId::new(),
receiver_agent_nickname: None,
@@ -533,6 +551,7 @@ mod tests {
ItemCompletedNotification {
thread_id: "thread-2".to_string(),
turn_id: "turn-2".to_string(),
completed_at_ms: event.completed_at_ms,
item: ThreadItem::CollabAgentToolCall {
id: event.call_id,
tool: CollabAgentTool::ResumeAgent,
@@ -1356,6 +1356,7 @@ mod tests {
id: "user-item-id".to_string(),
content: Vec::new(),
}),
started_at_ms: 0,
}),
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: turn_id.to_string(),
@@ -1820,6 +1821,7 @@ mod tests {
call_id: "exec-1".into(),
process_id: Some("pid-1".into()),
turn_id: "turn-1".into(),
completed_at_ms: 0,
command: vec!["echo".into(), "hello world".into()],
cwd: test_path_buf("/tmp").abs(),
parsed_cmd: vec![ParsedCommand::Unknown {
@@ -1983,6 +1985,7 @@ mod tests {
codex_protocol::dynamic_tools::DynamicToolCallRequest {
call_id: "dyn-1".into(),
turn_id: "turn-1".into(),
started_at_ms: 0,
namespace: Some("codex_app".into()),
tool: "lookup_ticket".into(),
arguments: serde_json::json!({"id":"ABC-123"}),
@@ -1991,6 +1994,7 @@ mod tests {
EventMsg::DynamicToolCallResponse(DynamicToolCallResponseEvent {
call_id: "dyn-1".into(),
turn_id: "turn-1".into(),
completed_at_ms: 0,
namespace: Some("codex_app".into()),
tool: "lookup_ticket".into(),
arguments: serde_json::json!({"id":"ABC-123"}),
@@ -2046,6 +2050,7 @@ mod tests {
call_id: "exec-declined".into(),
process_id: Some("pid-2".into()),
turn_id: "turn-1".into(),
completed_at_ms: 0,
command: vec!["ls".into()],
cwd: test_path_buf("/tmp").abs(),
parsed_cmd: vec![ParsedCommand::Unknown { cmd: "ls".into() }],
@@ -2293,6 +2298,7 @@ mod tests {
call_id: "exec-late".into(),
process_id: Some("pid-42".into()),
turn_id: "turn-a".into(),
completed_at_ms: 0,
command: vec!["echo".into(), "done".into()],
cwd: test_path_buf("/tmp").abs(),
parsed_cmd: vec![ParsedCommand::Unknown {
@@ -2384,6 +2390,7 @@ mod tests {
call_id: "exec-unknown-turn".into(),
process_id: Some("pid-42".into()),
turn_id: "turn-missing".into(),
completed_at_ms: 0,
command: vec!["echo".into(), "done".into()],
cwd: test_path_buf("/tmp").abs(),
parsed_cmd: vec![ParsedCommand::Unknown {
@@ -2732,6 +2739,7 @@ mod tests {
}),
EventMsg::CollabResumeEnd(codex_protocol::protocol::CollabResumeEndEvent {
call_id: "resume-1".into(),
completed_at_ms: 0,
sender_thread_id: ThreadId::try_from("00000000-0000-0000-0000-000000000001")
.expect("valid sender thread id"),
receiver_thread_id: ThreadId::try_from("00000000-0000-0000-0000-000000000002")
@@ -2788,6 +2796,7 @@ mod tests {
}),
EventMsg::CollabAgentSpawnEnd(codex_protocol::protocol::CollabAgentSpawnEndEvent {
call_id: "spawn-1".into(),
completed_at_ms: 0,
sender_thread_id,
new_thread_id: Some(spawned_thread_id),
new_agent_nickname: Some("Scout".into()),
@@ -2849,6 +2858,7 @@ mod tests {
EventMsg::CollabAgentInteractionBegin(
codex_protocol::protocol::CollabAgentInteractionBeginEvent {
call_id: "send-1".into(),
started_at_ms: 0,
sender_thread_id: sender,
receiver_thread_id: receiver,
prompt: "new task".into(),
@@ -2857,6 +2867,7 @@ mod tests {
EventMsg::CollabAgentInteractionEnd(
codex_protocol::protocol::CollabAgentInteractionEndEvent {
call_id: "send-1".into(),
completed_at_ms: 0,
sender_thread_id: sender,
receiver_thread_id: receiver,
receiver_agent_nickname: None,
@@ -6928,6 +6928,9 @@ pub struct ItemStartedNotification {
pub item: ThreadItem,
pub thread_id: String,
pub turn_id: String,
/// Unix timestamp (in milliseconds) when this item lifecycle started.
#[ts(type = "number")]
pub started_at_ms: i64,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
@@ -6990,6 +6993,9 @@ pub struct ItemCompletedNotification {
pub item: ThreadItem,
pub thread_id: String,
pub turn_id: String,
/// Unix timestamp (in milliseconds) when this item lifecycle completed.
#[ts(type = "number")]
pub completed_at_ms: i64,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
@@ -116,6 +116,8 @@ use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
use tokio::sync::Mutex;
use tokio::sync::oneshot;
use tracing::error;
@@ -816,6 +818,7 @@ pub(crate) async fn apply_bespoke_event_handling(
let notification = ItemStartedNotification {
thread_id: conversation_id.to_string(),
turn_id: turn_id.clone(),
started_at_ms: request.started_at_ms,
item,
};
outgoing
@@ -968,6 +971,7 @@ pub(crate) async fn apply_bespoke_event_handling(
let started = ItemStartedNotification {
thread_id: conversation_id.to_string(),
turn_id: event_turn_id.clone(),
started_at_ms: now_unix_timestamp_ms(),
item: item.clone(),
};
outgoing
@@ -976,6 +980,7 @@ pub(crate) async fn apply_bespoke_event_handling(
let completed = ItemCompletedNotification {
thread_id: conversation_id.to_string(),
turn_id: event_turn_id.clone(),
completed_at_ms: now_unix_timestamp_ms(),
item,
};
outgoing
@@ -1025,6 +1030,7 @@ pub(crate) async fn apply_bespoke_event_handling(
let started = ItemStartedNotification {
thread_id: conversation_id.to_string(),
turn_id: event_turn_id.clone(),
started_at_ms: now_unix_timestamp_ms(),
item: item.clone(),
};
outgoing
@@ -1033,6 +1039,7 @@ pub(crate) async fn apply_bespoke_event_handling(
let completed = ItemCompletedNotification {
thread_id: conversation_id.to_string(),
turn_id: event_turn_id.clone(),
completed_at_ms: now_unix_timestamp_ms(),
item,
};
outgoing
@@ -1368,6 +1375,7 @@ async fn start_command_execution_item(
let notification = ItemStartedNotification {
thread_id: conversation_id.to_string(),
turn_id,
started_at_ms: now_unix_timestamp_ms(),
item: ThreadItem::CommandExecution {
id: item_id,
command,
@@ -1427,6 +1435,7 @@ async fn complete_command_execution_item(
let notification = ItemCompletedNotification {
thread_id: conversation_id.to_string(),
turn_id,
completed_at_ms: now_unix_timestamp_ms(),
item,
};
outgoing
@@ -1474,6 +1483,7 @@ pub(crate) async fn maybe_emit_hook_prompt_item_completed(
let notification = ItemCompletedNotification {
thread_id: conversation_id.to_string(),
turn_id: turn_id.to_string(),
completed_at_ms: now_unix_timestamp_ms(),
item: ThreadItem::HookPrompt {
id: hook_prompt.id,
fragments: hook_prompt
@@ -2077,6 +2087,13 @@ async fn on_command_execution_request_approval_response(
}
}
fn now_unix_timestamp_ms() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis() as i64)
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
+3
View File
@@ -41,6 +41,7 @@ use crate::session_prefix::format_subagent_notification_message;
use crate::skills::SkillRenderSideEffects;
use crate::skills_load_input_from_config;
use crate::turn_metadata::TurnMetadataState;
use crate::turn_timing::now_unix_timestamp_ms;
use async_channel::Receiver;
use async_channel::Sender;
use chrono::Local;
@@ -1652,6 +1653,7 @@ impl Session {
thread_id: self.conversation_id,
turn_id: turn_context.sub_id.clone(),
item: item.clone(),
started_at_ms: now_unix_timestamp_ms(),
}),
)
.await;
@@ -1669,6 +1671,7 @@ impl Session {
thread_id: self.conversation_id,
turn_id: turn_context.sub_id.clone(),
item,
completed_at_ms: now_unix_timestamp_ms(),
}),
)
.await;
+5
View File
@@ -21,6 +21,7 @@ use crate::session::turn_context::TurnContext;
use crate::state::TaskKind;
use crate::tools::format_exec_output_str;
use crate::tools::runtimes::maybe_wrap_shell_lc_with_snapshot;
use crate::turn_timing::now_unix_timestamp_ms;
use crate::user_shell_command::user_shell_command_record_item;
use codex_protocol::exec_output::ExecToolCallOutput;
use codex_protocol::exec_output::StreamOutput;
@@ -164,6 +165,7 @@ pub(crate) async fn execute_user_shell_command(
call_id: call_id.clone(),
process_id: None,
turn_id: turn_context.sub_id.clone(),
started_at_ms: now_unix_timestamp_ms(),
command: display_command.clone(),
cwd: cwd.clone(),
parsed_cmd: parsed_cmd.clone(),
@@ -236,6 +238,7 @@ pub(crate) async fn execute_user_shell_command(
call_id,
process_id: None,
turn_id: turn_context.sub_id.clone(),
completed_at_ms: now_unix_timestamp_ms(),
command: display_command.clone(),
cwd: cwd.clone(),
parsed_cmd: parsed_cmd.clone(),
@@ -260,6 +263,7 @@ pub(crate) async fn execute_user_shell_command(
call_id: call_id.clone(),
process_id: None,
turn_id: turn_context.sub_id.clone(),
completed_at_ms: now_unix_timestamp_ms(),
command: display_command.clone(),
cwd: cwd.clone(),
parsed_cmd: parsed_cmd.clone(),
@@ -304,6 +308,7 @@ pub(crate) async fn execute_user_shell_command(
call_id,
process_id: None,
turn_id: turn_context.sub_id.clone(),
completed_at_ms: now_unix_timestamp_ms(),
command: display_command,
cwd,
parsed_cmd,
+3
View File
@@ -3,6 +3,7 @@ use crate::session::session::Session;
use crate::session::turn_context::TurnContext;
use crate::tools::context::SharedTurnDiffTracker;
use crate::tools::sandboxing::ToolError;
use crate::turn_timing::now_unix_timestamp_ms;
use codex_protocol::error::CodexErr;
use codex_protocol::error::SandboxErr;
use codex_protocol::exec_output::ExecToolCallOutput;
@@ -77,6 +78,7 @@ pub(crate) async fn emit_exec_command_begin(
call_id: ctx.call_id.to_string(),
process_id: process_id.map(str::to_owned),
turn_id: ctx.turn.sub_id.clone(),
started_at_ms: now_unix_timestamp_ms(),
command: command.to_vec(),
cwd: cwd.clone(),
parsed_cmd: parsed_cmd.to_vec(),
@@ -472,6 +474,7 @@ async fn emit_exec_end(
call_id: ctx.call_id.to_string(),
process_id: exec_input.process_id.map(str::to_owned),
turn_id: ctx.turn.sub_id.clone(),
completed_at_ms: now_unix_timestamp_ms(),
command: exec_input.command.to_vec(),
cwd: exec_input.cwd.clone(),
parsed_cmd: exec_input.parsed_cmd.to_vec(),
@@ -7,6 +7,7 @@ use crate::tools::context::ToolPayload;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use crate::turn_timing::now_unix_timestamp_ms;
use codex_protocol::dynamic_tools::DynamicToolCallRequest;
use codex_protocol::dynamic_tools::DynamicToolResponse;
use codex_protocol::models::FunctionCallOutputContentItem;
@@ -102,9 +103,11 @@ async fn request_dynamic_tool(
}
let started_at = Instant::now();
let started_at_ms = now_unix_timestamp_ms();
let event = EventMsg::DynamicToolCallRequest(DynamicToolCallRequest {
call_id: call_id.clone(),
turn_id: turn_id.clone(),
started_at_ms,
namespace: namespace.clone(),
tool: tool.clone(),
arguments: arguments.clone(),
@@ -116,6 +119,7 @@ async fn request_dynamic_tool(
Some(response) => EventMsg::DynamicToolCallResponse(DynamicToolCallResponseEvent {
call_id,
turn_id,
completed_at_ms: now_unix_timestamp_ms(),
namespace,
tool,
arguments,
@@ -127,6 +131,7 @@ async fn request_dynamic_tool(
None => EventMsg::DynamicToolCallResponse(DynamicToolCallResponseEvent {
call_id,
turn_id,
completed_at_ms: now_unix_timestamp_ms(),
namespace,
tool,
arguments,
@@ -1,4 +1,5 @@
use super::*;
use crate::turn_timing::now_unix_timestamp_ms;
pub(crate) struct Handler;
@@ -34,6 +35,7 @@ impl ToolHandler for Handler {
&turn,
CollabCloseBeginEvent {
call_id: call_id.clone(),
started_at_ms: now_unix_timestamp_ms(),
sender_thread_id: session.conversation_id,
receiver_thread_id: agent_id,
}
@@ -54,6 +56,7 @@ impl ToolHandler for Handler {
&turn,
CollabCloseEndEvent {
call_id: call_id.clone(),
completed_at_ms: now_unix_timestamp_ms(),
sender_thread_id: session.conversation_id,
receiver_thread_id: agent_id,
receiver_agent_nickname: receiver_agent.agent_nickname.clone(),
@@ -75,6 +78,7 @@ impl ToolHandler for Handler {
&turn,
CollabCloseEndEvent {
call_id,
completed_at_ms: now_unix_timestamp_ms(),
sender_thread_id: session.conversation_id,
receiver_thread_id: agent_id,
receiver_agent_nickname: receiver_agent.agent_nickname,
@@ -1,5 +1,6 @@
use super::*;
use crate::agent::next_thread_spawn_depth;
use crate::turn_timing::now_unix_timestamp_ms;
use std::sync::Arc;
pub(crate) struct Handler;
@@ -46,6 +47,7 @@ impl ToolHandler for Handler {
&turn,
CollabResumeBeginEvent {
call_id: call_id.clone(),
started_at_ms: now_unix_timestamp_ms(),
sender_thread_id: session.conversation_id,
receiver_thread_id,
receiver_agent_nickname: receiver_agent.agent_nickname.clone(),
@@ -101,6 +103,7 @@ impl ToolHandler for Handler {
&turn,
CollabResumeEndEvent {
call_id,
completed_at_ms: now_unix_timestamp_ms(),
sender_thread_id: session.conversation_id,
receiver_thread_id,
receiver_agent_nickname: receiver_agent.agent_nickname,
@@ -1,5 +1,6 @@
use super::*;
use crate::agent::control::render_input_preview;
use crate::turn_timing::now_unix_timestamp_ms;
pub(crate) struct Handler;
@@ -45,6 +46,7 @@ impl ToolHandler for Handler {
&turn,
CollabAgentInteractionBeginEvent {
call_id: call_id.clone(),
started_at_ms: now_unix_timestamp_ms(),
sender_thread_id: session.conversation_id,
receiver_thread_id,
prompt: prompt.clone(),
@@ -67,6 +69,7 @@ impl ToolHandler for Handler {
&turn,
CollabAgentInteractionEndEvent {
call_id,
completed_at_ms: now_unix_timestamp_ms(),
sender_thread_id: session.conversation_id,
receiver_thread_id,
receiver_agent_nickname: receiver_agent.agent_nickname,
@@ -6,6 +6,7 @@ use crate::agent::exceeds_thread_spawn_depth_limit;
use crate::agent::next_thread_spawn_depth;
use crate::agent::role::DEFAULT_ROLE_NAME;
use crate::agent::role::apply_role_to_config;
use crate::turn_timing::now_unix_timestamp_ms;
pub(crate) struct Handler;
@@ -50,6 +51,7 @@ impl ToolHandler for Handler {
&turn,
CollabAgentSpawnBeginEvent {
call_id: call_id.clone(),
started_at_ms: now_unix_timestamp_ms(),
sender_thread_id: session.conversation_id,
prompt: prompt.clone(),
model: args.model.clone().unwrap_or_default(),
@@ -146,6 +148,7 @@ impl ToolHandler for Handler {
&turn,
CollabAgentSpawnEndEvent {
call_id,
completed_at_ms: now_unix_timestamp_ms(),
sender_thread_id: session.conversation_id,
new_thread_id,
new_agent_nickname,
@@ -1,5 +1,6 @@
use super::*;
use crate::agent::status::is_final;
use crate::turn_timing::now_unix_timestamp_ms;
use codex_protocol::error::CodexErr;
use futures::FutureExt;
use futures::StreamExt;
@@ -73,6 +74,7 @@ impl ToolHandler for Handler {
.send_event(
&turn,
CollabWaitingBeginEvent {
started_at_ms: now_unix_timestamp_ms(),
sender_thread_id: session.conversation_id,
receiver_thread_ids: receiver_thread_ids.clone(),
receiver_agents: receiver_agents.clone(),
@@ -105,6 +107,7 @@ impl ToolHandler for Handler {
CollabWaitingEndEvent {
sender_thread_id: session.conversation_id,
call_id: call_id.clone(),
completed_at_ms: now_unix_timestamp_ms(),
agent_statuses: build_wait_agent_statuses(
&statuses,
&receiver_agents,
@@ -173,6 +176,7 @@ impl ToolHandler for Handler {
CollabWaitingEndEvent {
sender_thread_id: session.conversation_id,
call_id,
completed_at_ms: now_unix_timestamp_ms(),
agent_statuses,
statuses: statuses_by_id,
}
@@ -1,4 +1,5 @@
use super::*;
use crate::turn_timing::now_unix_timestamp_ms;
pub(crate) struct Handler;
@@ -43,6 +44,7 @@ impl ToolHandler for Handler {
&turn,
CollabCloseBeginEvent {
call_id: call_id.clone(),
started_at_ms: now_unix_timestamp_ms(),
sender_thread_id: session.conversation_id,
receiver_thread_id: agent_id,
}
@@ -63,6 +65,7 @@ impl ToolHandler for Handler {
&turn,
CollabCloseEndEvent {
call_id: call_id.clone(),
completed_at_ms: now_unix_timestamp_ms(),
sender_thread_id: session.conversation_id,
receiver_thread_id: agent_id,
receiver_agent_nickname: receiver_agent.agent_nickname.clone(),
@@ -87,6 +90,7 @@ impl ToolHandler for Handler {
&turn,
CollabCloseEndEvent {
call_id,
completed_at_ms: now_unix_timestamp_ms(),
sender_thread_id: session.conversation_id,
receiver_thread_id: agent_id,
receiver_agent_nickname: receiver_agent.agent_nickname,
@@ -5,6 +5,7 @@
use super::*;
use crate::tools::context::FunctionToolOutput;
use crate::turn_timing::now_unix_timestamp_ms;
use codex_protocol::protocol::InterAgentCommunication;
#[derive(Clone, Copy, PartialEq, Eq)]
@@ -97,6 +98,7 @@ async fn handle_message_submission(
&turn,
CollabAgentInteractionBeginEvent {
call_id: call_id.clone(),
started_at_ms: now_unix_timestamp_ms(),
sender_thread_id: session.conversation_id,
receiver_thread_id,
prompt: prompt.clone(),
@@ -132,6 +134,7 @@ async fn handle_message_submission(
&turn,
CollabAgentInteractionEndEvent {
call_id,
completed_at_ms: now_unix_timestamp_ms(),
sender_thread_id: session.conversation_id,
receiver_thread_id,
receiver_agent_nickname: receiver_agent.agent_nickname,
@@ -5,6 +5,7 @@ use crate::agent::control::render_input_preview;
use crate::agent::next_thread_spawn_depth;
use crate::agent::role::DEFAULT_ROLE_NAME;
use crate::agent::role::apply_role_to_config;
use crate::turn_timing::now_unix_timestamp_ms;
use codex_protocol::AgentPath;
use codex_protocol::protocol::InterAgentCommunication;
use codex_protocol::protocol::Op;
@@ -49,6 +50,7 @@ impl ToolHandler for Handler {
&turn,
CollabAgentSpawnBeginEvent {
call_id: call_id.clone(),
started_at_ms: now_unix_timestamp_ms(),
sender_thread_id: session.conversation_id,
prompt: prompt.clone(),
model: args.model.clone().unwrap_or_default(),
@@ -168,6 +170,7 @@ impl ToolHandler for Handler {
&turn,
CollabAgentSpawnEndEvent {
call_id,
completed_at_ms: now_unix_timestamp_ms(),
sender_thread_id: session.conversation_id,
new_thread_id,
new_agent_nickname,
@@ -1,4 +1,5 @@
use super::*;
use crate::turn_timing::now_unix_timestamp_ms;
use std::collections::HashMap;
use std::time::Duration;
use tokio::time::Instant;
@@ -48,6 +49,7 @@ impl ToolHandler for Handler {
.send_event(
&turn,
CollabWaitingBeginEvent {
started_at_ms: now_unix_timestamp_ms(),
sender_thread_id: session.conversation_id,
receiver_thread_ids: Vec::new(),
receiver_agents: Vec::new(),
@@ -71,6 +73,7 @@ impl ToolHandler for Handler {
CollabWaitingEndEvent {
sender_thread_id: session.conversation_id,
call_id,
completed_at_ms: now_unix_timestamp_ms(),
agent_statuses: Vec::new(),
statuses: HashMap::new(),
}
+1 -1
View File
@@ -107,7 +107,7 @@ fn now_unix_timestamp_secs() -> i64 {
now_unix_timestamp_ms() / 1000
}
fn now_unix_timestamp_ms() -> i64 {
pub(crate) fn now_unix_timestamp_ms() -> i64 {
let duration = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
+38 -3
View File
@@ -303,6 +303,15 @@ async fn web_search_item_is_emitted() -> anyhow::Result<()> {
})
.await?;
let started = wait_for_event_match(&codex, |ev| match ev {
EventMsg::ItemStarted(ItemStartedEvent {
item: TurnItem::WebSearch(item),
started_at_ms,
..
}) => Some((item.clone(), *started_at_ms)),
_ => None,
})
.await;
let begin = wait_for_event_match(&codex, |ev| match ev {
EventMsg::WebSearchBegin(event) => Some(event.clone()),
_ => None,
@@ -311,16 +320,20 @@ async fn web_search_item_is_emitted() -> anyhow::Result<()> {
let completed = wait_for_event_match(&codex, |ev| match ev {
EventMsg::ItemCompleted(ItemCompletedEvent {
item: TurnItem::WebSearch(item),
completed_at_ms,
..
}) => Some(item.clone()),
}) => Some((item.clone(), *completed_at_ms)),
_ => None,
})
.await;
assert_eq!(begin.call_id, "web-search-1");
assert_eq!(completed.id, begin.call_id);
assert_eq!(started.0.id, begin.call_id);
assert!(started.1 > 0);
assert_eq!(completed.0.id, begin.call_id);
assert!(completed.1 > 0);
assert_eq!(
completed.action,
completed.0.action,
WebSearchAction::Search {
query: Some("weather seattle".to_string()),
queries: None,
@@ -369,11 +382,29 @@ async fn image_generation_call_event_is_emitted() -> anyhow::Result<()> {
})
.await?;
let started = wait_for_event_match(&codex, |ev| match ev {
EventMsg::ItemStarted(ItemStartedEvent {
item: TurnItem::ImageGeneration(item),
started_at_ms,
..
}) => Some((item.clone(), *started_at_ms)),
_ => None,
})
.await;
let begin = wait_for_event_match(&codex, |ev| match ev {
EventMsg::ImageGenerationBegin(event) => Some(event.clone()),
_ => None,
})
.await;
let completed = wait_for_event_match(&codex, |ev| match ev {
EventMsg::ItemCompleted(ItemCompletedEvent {
item: TurnItem::ImageGeneration(item),
completed_at_ms,
..
}) => Some((item.clone(), *completed_at_ms)),
_ => None,
})
.await;
let end = wait_for_event_match(&codex, |ev| match ev {
EventMsg::ImageGenerationEnd(event) => Some(event.clone()),
_ => None,
@@ -381,6 +412,10 @@ async fn image_generation_call_event_is_emitted() -> anyhow::Result<()> {
.await;
assert_eq!(begin.call_id, call_id);
assert_eq!(started.0.id, call_id);
assert!(started.1 > 0);
assert_eq!(completed.0.id, call_id);
assert!(completed.1 > 0);
assert_eq!(end.call_id, call_id);
assert_eq!(end.status, "completed");
assert_eq!(end.revised_prompt, Some("A tiny blue square".to_string()));
@@ -20,6 +20,7 @@ fn failed_turn_does_not_overwrite_output_last_message_file() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
},
));
@@ -181,6 +181,7 @@ fn command_execution_started_and_completed_translate_to_thread_events() {
item: command_item,
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: 0,
}));
assert_eq!(
started,
@@ -216,6 +217,7 @@ fn command_execution_started_and_completed_translate_to_thread_events() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
},
));
assert_eq!(
@@ -250,6 +252,7 @@ fn empty_reasoning_items_are_ignored() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
},
));
@@ -274,6 +277,7 @@ fn unsupported_items_do_not_consume_synthetic_ids() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
},
));
@@ -295,6 +299,7 @@ fn unsupported_items_do_not_consume_synthetic_ids() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
},
));
@@ -327,6 +332,7 @@ fn reasoning_items_emit_summary_not_raw_content() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
},
));
@@ -362,6 +368,7 @@ fn web_search_completion_preserves_query_and_action() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
},
));
@@ -399,6 +406,7 @@ fn web_search_start_and_completion_reuse_item_id() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: 0,
}));
let completed = processor.collect_thread_events(ServerNotification::ItemCompleted(
@@ -413,6 +421,7 @@ fn web_search_start_and_completion_reuse_item_id() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
},
));
@@ -472,6 +481,7 @@ fn mcp_tool_call_begin_and_end_emit_item_events() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: 0,
}));
let completed = processor.collect_thread_events(ServerNotification::ItemCompleted(
ItemCompletedNotification {
@@ -492,6 +502,7 @@ fn mcp_tool_call_begin_and_end_emit_item_events() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
},
));
@@ -559,6 +570,7 @@ fn mcp_tool_call_failure_sets_failed_status() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
},
));
@@ -604,6 +616,7 @@ fn mcp_tool_call_defaults_arguments_and_preserves_structured_content() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: 0,
}));
let completed = processor.collect_thread_events(ServerNotification::ItemCompleted(
ItemCompletedNotification {
@@ -627,6 +640,7 @@ fn mcp_tool_call_defaults_arguments_and_preserves_structured_content() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
},
));
@@ -695,6 +709,7 @@ fn collab_spawn_begin_and_end_emit_item_events() {
},
thread_id: "thread-parent".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: 0,
}));
let completed = processor.collect_thread_events(ServerNotification::ItemCompleted(
ItemCompletedNotification {
@@ -717,6 +732,7 @@ fn collab_spawn_begin_and_end_emit_item_events() {
},
thread_id: "thread-parent".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
},
));
@@ -795,6 +811,7 @@ fn file_change_completion_maps_change_kinds() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
},
));
@@ -845,6 +862,7 @@ fn file_change_declined_maps_to_failed_status() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
},
));
@@ -882,6 +900,7 @@ fn agent_message_item_updates_final_message() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
},
));
@@ -916,6 +935,7 @@ fn agent_message_item_started_is_ignored() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: 0,
}));
assert_eq!(
@@ -940,6 +960,7 @@ fn reasoning_item_completed_uses_synthetic_id() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
},
));
@@ -1296,6 +1317,7 @@ fn turn_completion_reconciles_started_items_from_turn_items() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: 0,
}));
assert_eq!(
started,
@@ -1378,6 +1400,7 @@ fn turn_completion_overwrites_stale_final_message_from_turn_items() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
},
));
@@ -1426,6 +1449,7 @@ fn turn_completion_preserves_streamed_final_message_when_turn_items_are_empty()
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
},
));
@@ -1470,6 +1494,7 @@ fn failed_turn_clears_stale_final_message() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
},
));
+2
View File
@@ -23,6 +23,8 @@ pub struct DynamicToolCallRequest {
pub call_id: String,
pub turn_id: String,
#[serde(default)]
pub started_at_ms: i64,
#[serde(default)]
pub namespace: Option<String>,
pub tool: String,
pub arguments: JsonValue,
+72
View File
@@ -1828,6 +1828,7 @@ pub struct ItemStartedEvent {
pub thread_id: ThreadId,
pub turn_id: String,
pub item: TurnItem,
pub started_at_ms: i64,
}
impl HasLegacyEvent for ItemStartedEvent {
@@ -1854,6 +1855,15 @@ pub struct ItemCompletedEvent {
pub thread_id: ThreadId,
pub turn_id: String,
pub item: TurnItem,
// Old rollout files may contain ItemCompleted events for PlanItem without
// this field. Default to 0 so those persisted rollouts still deserialize
// after tightening the core event contract.
#[serde(default = "default_item_completed_at_ms")]
pub completed_at_ms: i64,
}
const fn default_item_completed_at_ms() -> i64 {
0
}
pub trait HasLegacyEvent {
@@ -2348,6 +2358,8 @@ pub struct DynamicToolCallResponseEvent {
pub call_id: String,
/// Turn ID that this dynamic tool call belongs to.
pub turn_id: String,
#[serde(default)]
pub completed_at_ms: i64,
/// Dynamic tool namespace, when one was provided.
#[serde(default)]
pub namespace: Option<String>,
@@ -3058,6 +3070,8 @@ pub struct ExecCommandBeginEvent {
pub process_id: Option<String>,
/// Turn ID that this command belongs to.
pub turn_id: String,
#[serde(default)]
pub started_at_ms: i64,
/// The command to be executed.
pub command: Vec<String>,
/// The command's working directory if not the default cwd for the agent.
@@ -3082,6 +3096,8 @@ pub struct ExecCommandEndEvent {
pub process_id: Option<String>,
/// Turn ID that this command belongs to.
pub turn_id: String,
#[serde(default)]
pub completed_at_ms: i64,
/// The command that was executed.
pub command: Vec<String>,
/// The command's working directory if not the default cwd for the agent.
@@ -3750,6 +3766,8 @@ pub enum TurnAbortReason {
pub struct CollabAgentSpawnBeginEvent {
/// Identifier for the collab tool call.
pub call_id: String,
#[serde(default)]
pub started_at_ms: i64,
/// Thread ID of the sender.
pub sender_thread_id: ThreadId,
/// Initial prompt sent to the agent. Can be empty to prevent CoT leaking at the
@@ -3789,6 +3807,8 @@ pub struct CollabAgentStatusEntry {
pub struct CollabAgentSpawnEndEvent {
/// Identifier for the collab tool call.
pub call_id: String,
#[serde(default)]
pub completed_at_ms: i64,
/// Thread ID of the sender.
pub sender_thread_id: ThreadId,
/// Thread ID of the newly spawned agent, if it was created.
@@ -3814,6 +3834,8 @@ pub struct CollabAgentSpawnEndEvent {
pub struct CollabAgentInteractionBeginEvent {
/// Identifier for the collab tool call.
pub call_id: String,
#[serde(default)]
pub started_at_ms: i64,
/// Thread ID of the sender.
pub sender_thread_id: ThreadId,
/// Thread ID of the receiver.
@@ -3827,6 +3849,8 @@ pub struct CollabAgentInteractionBeginEvent {
pub struct CollabAgentInteractionEndEvent {
/// Identifier for the collab tool call.
pub call_id: String,
#[serde(default)]
pub completed_at_ms: i64,
/// Thread ID of the sender.
pub sender_thread_id: ThreadId,
/// Thread ID of the receiver.
@@ -3846,6 +3870,8 @@ pub struct CollabAgentInteractionEndEvent {
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
pub struct CollabWaitingBeginEvent {
#[serde(default)]
pub started_at_ms: i64,
/// Thread ID of the sender.
pub sender_thread_id: ThreadId,
/// Thread ID of the receivers.
@@ -3863,6 +3889,8 @@ pub struct CollabWaitingEndEvent {
pub sender_thread_id: ThreadId,
/// ID of the waiting call.
pub call_id: String,
#[serde(default)]
pub completed_at_ms: i64,
/// Optional receiver metadata paired with final statuses.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub agent_statuses: Vec<CollabAgentStatusEntry>,
@@ -3874,6 +3902,8 @@ pub struct CollabWaitingEndEvent {
pub struct CollabCloseBeginEvent {
/// Identifier for the collab tool call.
pub call_id: String,
#[serde(default)]
pub started_at_ms: i64,
/// Thread ID of the sender.
pub sender_thread_id: ThreadId,
/// Thread ID of the receiver.
@@ -3884,6 +3914,8 @@ pub struct CollabCloseBeginEvent {
pub struct CollabCloseEndEvent {
/// Identifier for the collab tool call.
pub call_id: String,
#[serde(default)]
pub completed_at_ms: i64,
/// Thread ID of the sender.
pub sender_thread_id: ThreadId,
/// Thread ID of the receiver.
@@ -3903,6 +3935,8 @@ pub struct CollabCloseEndEvent {
pub struct CollabResumeBeginEvent {
/// Identifier for the collab tool call.
pub call_id: String,
#[serde(default)]
pub started_at_ms: i64,
/// Thread ID of the sender.
pub sender_thread_id: ThreadId,
/// Thread ID of the receiver.
@@ -3919,6 +3953,8 @@ pub struct CollabResumeBeginEvent {
pub struct CollabResumeEndEvent {
/// Identifier for the collab tool call.
pub call_id: String,
#[serde(default)]
pub completed_at_ms: i64,
/// Thread ID of the sender.
pub sender_thread_id: ThreadId,
/// Thread ID of the receiver.
@@ -4596,6 +4632,7 @@ mod tests {
queries: None,
},
}),
started_at_ms: 0,
};
let legacy_events = event.as_legacy_events(/*show_raw_agent_reasoning*/ false);
@@ -4612,6 +4649,7 @@ mod tests {
thread_id: ThreadId::new(),
turn_id: "turn-1".into(),
item: TurnItem::UserMessage(UserMessageItem::new(&[])),
started_at_ms: 0,
};
assert!(
@@ -4633,6 +4671,7 @@ mod tests {
result: String::new(),
saved_path: None,
}),
started_at_ms: 0,
};
let legacy_events = event.as_legacy_events(/*show_raw_agent_reasoning*/ false);
@@ -4648,6 +4687,7 @@ mod tests {
let event = ItemStartedEvent {
thread_id: ThreadId::new(),
turn_id: "turn-1".into(),
started_at_ms: 0,
item: TurnItem::FileChange(FileChangeItem {
id: "patch-1".into(),
changes: [(
@@ -4683,6 +4723,7 @@ mod tests {
let event = ItemStartedEvent {
thread_id: ThreadId::new(),
turn_id: "turn-1".into(),
started_at_ms: 0,
item: TurnItem::McpToolCall(McpToolCallItem {
id: "mcp-1".into(),
server: "server".into(),
@@ -4724,6 +4765,7 @@ mod tests {
result: "Zm9v".into(),
saved_path: Some(test_path_buf("/tmp/ig-1.png").abs()),
}),
completed_at_ms: 0,
};
let legacy_events = event.as_legacy_events(/*show_raw_agent_reasoning*/ false);
@@ -4748,6 +4790,7 @@ mod tests {
let event = ItemCompletedEvent {
thread_id: ThreadId::new(),
turn_id: "turn-1".into(),
completed_at_ms: 0,
item: TurnItem::FileChange(FileChangeItem {
id: "patch-1".into(),
changes: [(
@@ -4785,6 +4828,7 @@ mod tests {
let event = ItemCompletedEvent {
thread_id: ThreadId::new(),
turn_id: "turn-1".into(),
completed_at_ms: 0,
item: TurnItem::McpToolCall(McpToolCallItem {
id: "mcp-1".into(),
server: "server".into(),
@@ -4821,6 +4865,34 @@ mod tests {
}
}
#[test]
fn item_started_event_requires_started_at_ms() {
let mut value = serde_json::to_value(ItemStartedEvent {
thread_id: ThreadId::new(),
turn_id: "turn-1".into(),
item: TurnItem::UserMessage(UserMessageItem::new(&[])),
started_at_ms: 123,
})
.unwrap();
value.as_object_mut().unwrap().remove("started_at_ms");
assert!(serde_json::from_value::<ItemStartedEvent>(value).is_err());
}
#[test]
fn item_completed_event_defaults_missing_completed_at_ms() {
let mut value = serde_json::to_value(ItemCompletedEvent {
thread_id: ThreadId::new(),
turn_id: "turn-1".into(),
item: TurnItem::UserMessage(UserMessageItem::new(&[])),
completed_at_ms: 123,
})
.unwrap();
value.as_object_mut().unwrap().remove("completed_at_ms");
let event = serde_json::from_value::<ItemCompletedEvent>(value).unwrap();
assert_eq!(event.completed_at_ms, 0);
}
#[test]
fn rollback_failed_error_does_not_affect_turn_status() {
let event = ErrorEvent {
+2
View File
@@ -2634,6 +2634,7 @@ async fn inactive_thread_file_change_approval_recovers_buffered_changes() {
ServerNotification::ItemStarted(ItemStartedNotification {
thread_id: thread_id.to_string(),
turn_id: "turn-approval".to_string(),
started_at_ms: 0,
item: ThreadItem::FileChange {
id: "patch-approval".to_string(),
changes: vec![FileUpdateChange {
@@ -4766,6 +4767,7 @@ async fn replace_chat_widget_reseeds_collab_agent_metadata_for_replay() {
codex_app_server_protocol::ItemStartedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: 0,
item: ThreadItem::CollabAgentToolCall {
id: "wait-1".to_string(),
tool: codex_app_server_protocol::CollabAgentTool::Wait,
@@ -16,6 +16,7 @@ async fn collab_spawn_end_shows_requested_model_and_effort() {
ServerNotification::ItemStarted(ItemStartedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: 0,
item: AppServerThreadItem::CollabAgentToolCall {
id: "call-spawn".to_string(),
tool: AppServerCollabAgentTool::SpawnAgent,
@@ -34,6 +35,7 @@ async fn collab_spawn_end_shows_requested_model_and_effort() {
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
item: AppServerThreadItem::CollabAgentToolCall {
id: "call-spawn".to_string(),
tool: AppServerCollabAgentTool::SpawnAgent,
@@ -90,6 +92,7 @@ async fn live_app_server_user_message_item_completed_does_not_duplicate_rendered
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
item: AppServerThreadItem::UserMessage {
id: "user-1".to_string(),
content: vec![AppServerUserInput::Text {
@@ -135,6 +138,7 @@ async fn live_app_server_turn_completed_clears_working_status_after_answer_item(
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
item: AppServerThreadItem::AgentMessage {
id: "msg-1".to_string(),
text: "Yes. What do you need?".to_string(),
@@ -287,6 +291,7 @@ async fn live_app_server_file_change_item_started_preserves_changes() {
ServerNotification::ItemStarted(ItemStartedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: 0,
item: AppServerThreadItem::FileChange {
id: "patch-1".to_string(),
changes: vec![FileUpdateChange {
@@ -320,6 +325,7 @@ async fn live_app_server_command_execution_strips_shell_wrapper() {
ServerNotification::ItemStarted(ItemStartedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: 0,
item: AppServerThreadItem::CommandExecution {
id: "cmd-1".to_string(),
command: command.clone(),
@@ -341,6 +347,7 @@ async fn live_app_server_command_execution_strips_shell_wrapper() {
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
item: AppServerThreadItem::CommandExecution {
id: "cmd-1".to_string(),
command,
@@ -396,6 +403,7 @@ async fn live_app_server_collab_wait_items_render_history() {
ServerNotification::ItemStarted(ItemStartedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: 0,
item: AppServerThreadItem::CollabAgentToolCall {
id: "wait-1".to_string(),
tool: AppServerCollabAgentTool::Wait,
@@ -418,6 +426,7 @@ async fn live_app_server_collab_wait_items_render_history() {
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
item: AppServerThreadItem::CollabAgentToolCall {
id: "wait-1".to_string(),
tool: AppServerCollabAgentTool::Wait,
@@ -471,6 +480,7 @@ async fn live_app_server_collab_spawn_completed_renders_requested_model_and_effo
ServerNotification::ItemStarted(ItemStartedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: 0,
item: AppServerThreadItem::CollabAgentToolCall {
id: "spawn-1".to_string(),
tool: AppServerCollabAgentTool::SpawnAgent,
@@ -490,6 +500,7 @@ async fn live_app_server_collab_spawn_completed_renders_requested_model_and_effo
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
item: AppServerThreadItem::CollabAgentToolCall {
id: "spawn-1".to_string(),
tool: AppServerCollabAgentTool::SpawnAgent,
@@ -659,6 +659,7 @@ pub(super) fn handle_agent_reasoning_final(chat: &mut ChatWidget) {
.last_turn_id
.clone()
.unwrap_or_else(|| "turn-1".to_string()),
completed_at_ms: 0,
item: AppServerThreadItem::Reasoning {
id: "reasoning-1".to_string(),
summary: Vec::new(),
@@ -677,6 +678,7 @@ pub(super) fn handle_entered_review_mode(chat: &mut ChatWidget, review: impl Int
.last_turn_id
.clone()
.unwrap_or_else(|| "turn-1".to_string()),
started_at_ms: 0,
item: AppServerThreadItem::EnteredReviewMode {
id: "review-start".to_string(),
review: review.into(),
@@ -705,6 +707,7 @@ pub(super) fn handle_exited_review_mode(chat: &mut ChatWidget) {
.last_turn_id
.clone()
.unwrap_or_else(|| "turn-1".to_string()),
completed_at_ms: 0,
item: AppServerThreadItem::ExitedReviewMode {
id: "review-end".to_string(),
review: String::new(),
@@ -761,6 +764,7 @@ pub(super) fn handle_patch_apply_begin(
ServerNotification::ItemStarted(ItemStartedNotification {
thread_id: thread_id(chat),
turn_id: turn_id.into(),
started_at_ms: 0,
item: AppServerThreadItem::FileChange {
id: call_id.into(),
changes: file_update_changes_from_tui(changes),
@@ -782,6 +786,7 @@ pub(super) fn handle_patch_apply_end(
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: thread_id(chat),
turn_id: turn_id.into(),
completed_at_ms: 0,
item: AppServerThreadItem::FileChange {
id: call_id.into(),
changes: file_update_changes_from_tui(changes),
@@ -801,6 +806,7 @@ pub(super) fn handle_view_image_tool_call(
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: thread_id(chat),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
item: AppServerThreadItem::ImageView {
id: call_id.into(),
path,
@@ -820,6 +826,7 @@ pub(super) fn handle_image_generation_end(
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: thread_id(chat),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
item: AppServerThreadItem::ImageGeneration {
id: call_id.into(),
status: "completed".to_string(),
@@ -977,6 +984,7 @@ pub(super) fn handle_exec_begin(chat: &mut ChatWidget, item: AppServerThreadItem
.last_turn_id
.clone()
.unwrap_or_else(|| "turn-1".to_string()),
started_at_ms: 0,
item,
}),
/*replay_kind*/ None,
@@ -1016,6 +1024,7 @@ pub(super) fn complete_assistant_message(
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: chat.thread_id.map(|id| id.to_string()).unwrap_or_default(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
item: AppServerThreadItem::AgentMessage {
id: item_id.to_string(),
text: text.to_string(),
@@ -1058,6 +1067,7 @@ pub(super) fn complete_user_message_for_inputs(
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: chat.thread_id.map(|id| id.to_string()).unwrap_or_default(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
item: AppServerThreadItem::UserMessage {
id: item_id.to_string(),
content,
@@ -1199,6 +1209,7 @@ pub(super) fn handle_exec_end(chat: &mut ChatWidget, item: AppServerThreadItem)
.last_turn_id
.clone()
.unwrap_or_else(|| "turn-1".to_string()),
completed_at_ms: 0,
item,
}),
/*replay_kind*/ None,
@@ -801,6 +801,7 @@ async fn live_reasoning_summary_is_not_rendered_twice_when_item_completes() {
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
item: AppServerThreadItem::Reasoning {
id: "reasoning-1".to_string(),
summary: vec!["Summary only".to_string()],
@@ -154,6 +154,7 @@ async fn live_app_server_review_prompt_item_is_not_rendered() {
ServerNotification::ItemStarted(ItemStartedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: 0,
item: review_mode_item.clone(),
}),
/*replay_kind*/ None,
@@ -166,6 +167,7 @@ async fn live_app_server_review_prompt_item_is_not_rendered() {
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
item: review_mode_item,
}),
/*replay_kind*/ None,
@@ -176,6 +178,7 @@ async fn live_app_server_review_prompt_item_is_not_rendered() {
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
item: AppServerThreadItem::UserMessage {
id: "review-prompt".to_string(),
content: vec![AppServerUserInput::Text {
@@ -1149,6 +1149,7 @@ async fn slash_copy_state_tracks_plan_item_completion() {
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: String::new(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
item: AppServerThreadItem::Plan {
id: "plan-1".to_string(),
text: plan_text.clone(),