mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
Track plugin metrics for background unified exec commands (#38276)
## Why Unified exec can yield while a command is still running. Plugin measurement collection must remain active until that background command exits, including when its item completion arrives after the turn has completed. ## What changed - Keep the plugin metrics sidecar with the stored process and let either the exit watcher or a poll that observes completion finalize it exactly once. - Retain completed turn analytics state while tool items are pending so late command completion events can be emitted without duplicating the turn event. ## Testing - Verify that a background command completed after its turn emits a command execution event and does not emit a second turn event. GitOrigin-RevId: ecf715b3e047aa29ca9a417d12d955257fed8557
This commit is contained in:
@@ -5062,6 +5062,86 @@ async fn turn_event_counts_completed_tool_items() {
|
||||
assert_eq!(payload["event_params"]["image_generation_count"], json!(1));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn completed_background_tool_item_emits_after_turn_event() {
|
||||
let mut reducer = AnalyticsReducer::default();
|
||||
let mut out = Vec::new();
|
||||
|
||||
ingest_turn_prerequisites(
|
||||
&mut reducer,
|
||||
&mut out,
|
||||
/*include_initialize*/ true,
|
||||
/*include_resolved_config*/ true,
|
||||
/*include_started*/ true,
|
||||
/*include_token_usage*/ false,
|
||||
)
|
||||
.await;
|
||||
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(ServerNotification::ItemStarted(
|
||||
ItemStartedNotification {
|
||||
thread_id: "thread-2".to_string(),
|
||||
turn_id: "turn-2".to_string(),
|
||||
started_at_ms: 998,
|
||||
item: sample_command_execution_item(
|
||||
CommandExecutionStatus::InProgress,
|
||||
/*exit_code*/ None,
|
||||
/*duration_ms*/ None,
|
||||
),
|
||||
},
|
||||
))),
|
||||
&mut out,
|
||||
)
|
||||
.await;
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(sample_turn_completed_notification(
|
||||
"thread-2",
|
||||
"turn-2",
|
||||
AppServerTurnStatus::Completed,
|
||||
/*codex_error_info*/ None,
|
||||
))),
|
||||
&mut out,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
out.iter()
|
||||
.filter(|event| matches!(event, TrackEventRequest::TurnEvent(_)))
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
thread_id: "thread-2".to_string(),
|
||||
turn_id: "turn-2".to_string(),
|
||||
completed_at_ms: 1_000,
|
||||
item: sample_command_execution_item(
|
||||
CommandExecutionStatus::Completed,
|
||||
Some(0),
|
||||
Some(1),
|
||||
),
|
||||
},
|
||||
))),
|
||||
&mut out,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
out.iter()
|
||||
.filter(|event| matches!(event, TrackEventRequest::TurnEvent(_)))
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert!(
|
||||
out.iter()
|
||||
.any(|event| matches!(event, TrackEventRequest::CommandExecution(_)))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn item_completed_without_turn_state_does_not_create_turn_state() {
|
||||
let mut reducer = AnalyticsReducer::default();
|
||||
|
||||
@@ -424,6 +424,7 @@ struct TurnState {
|
||||
steer_count: usize,
|
||||
tool_counts: TurnToolCounts,
|
||||
resource_skill_invocations: HashSet<String>,
|
||||
turn_event_emitted: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Hash, Eq, PartialEq)]
|
||||
@@ -1757,6 +1758,14 @@ impl AnalyticsReducer {
|
||||
);
|
||||
}
|
||||
self.item_review_summaries.remove(&key);
|
||||
if self
|
||||
.turns
|
||||
.get(¬ification.turn_id)
|
||||
.is_some_and(|turn_state| turn_state.turn_event_emitted)
|
||||
&& !self.has_pending_tool_items_for_turn(¬ification.turn_id)
|
||||
{
|
||||
self.turns.remove(¬ification.turn_id);
|
||||
}
|
||||
}
|
||||
ServerNotification::ItemGuardianApprovalReviewStarted(notification) => {
|
||||
let _ = notification;
|
||||
@@ -2128,6 +2137,9 @@ impl AnalyticsReducer {
|
||||
let Some(turn_state) = self.turns.get(turn_id) else {
|
||||
return;
|
||||
};
|
||||
if turn_state.turn_event_emitted {
|
||||
return;
|
||||
}
|
||||
if turn_state.thread_id.is_none()
|
||||
|| turn_state.num_input_images.is_none()
|
||||
|| turn_state.resolved_config.is_none()
|
||||
@@ -2179,7 +2191,19 @@ impl AnalyticsReducer {
|
||||
input.repo_hash = accepted_line_repo_hash_for_cwd(cwd.as_path()).await;
|
||||
out.extend(accepted_line_fingerprint_event_requests(input));
|
||||
}
|
||||
self.turns.remove(turn_id);
|
||||
if self.has_pending_tool_items_for_turn(turn_id) {
|
||||
if let Some(turn_state) = self.turns.get_mut(turn_id) {
|
||||
turn_state.turn_event_emitted = true;
|
||||
}
|
||||
} else {
|
||||
self.turns.remove(turn_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn has_pending_tool_items_for_turn(&self, turn_id: &str) -> bool {
|
||||
self.tool_items_started_at_ms
|
||||
.keys()
|
||||
.any(|key| key.turn_id == turn_id)
|
||||
}
|
||||
|
||||
/// Resolve the parent connection lazily when a subagent fact arrives first.
|
||||
|
||||
Reference in New Issue
Block a user