[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

@@ -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::*;