mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[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:
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user