[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 authored and GitHub committed 2026-05-04 22:33:20 +00:00
1 parent e7e6267ab3
commit aee1fe2659
37 files changed
+332 -11

No files matched your search

+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();