Attribute command and plugin analytics to the invoking model (#45445)

## Why

Command execution and plugin measurement events lack model and reasoning-effort labels. Attribution needs to reflect the step that invoked the command, even when model settings change before a background process finishes.

## What changed

- Add `model_slug` and `reasoning_effort` to command execution and plugin measurement analytics.
- Capture model context from resolved step settings and carry it through execution, approval, Guardian review, and plugin metrics collection.
- Preserve the first command-start model context when subsequent start notifications arrive.
- Keep the carried context out of serialized protocol items and generated schemas.

## Testing

Extend analytics tests to cover model switches before invocation and during background execution, default reasoning effort, Guardian-denied commands, and repeated start notifications retaining the original model context.

GitOrigin-RevId: af90e1c0d39bab625f2e89786085b61a9b96c0ce
This commit is contained in:
jwang-openai
2026-09-14 15:30:22 +00:00
committed by copyberry
parent b6a5d5bb14
commit 4d8eca1ff3
40 changed files with 355 additions and 43 deletions
@@ -10,6 +10,7 @@ fn agent_status_uses_bounded_buffered_activity() {
store.push_notification(ServerNotification::ItemCompleted(
ItemCompletedNotification {
item: ThreadItem::CommandExecution {
model_context: None,
id: "command-1".to_string(),
command: "cargo test -p codex-tui".to_string(),
cwd: AbsolutePathBuf::try_from("/workspace")
+1
View File
@@ -95,6 +95,7 @@ mod tests {
fn denied_event(id: usize) -> GuardianAssessmentEvent {
GuardianAssessmentEvent {
review_reason: None,
model_context: None,
id: format!("review-{id}"),
target_item_id: None,
plugin_id: None,
@@ -194,6 +194,7 @@ mod tests {
fn command_execution(call_id: &str) -> ThreadItem {
ThreadItem::CommandExecution {
model_context: None,
id: call_id.to_string(),
command: "true".to_string(),
cwd: AbsolutePathBuf::current_dir().expect("current dir").into(),
@@ -96,6 +96,7 @@ impl ChatWidget {
self.on_guardian_assessment(GuardianAssessmentEvent {
review_reason: None,
model_context: None,
id,
target_item_id: None,
plugin_id: None,
@@ -1080,6 +1080,7 @@ async fn live_app_server_command_execution_strips_shell_wrapper() {
turn_id: "turn-1".to_string(),
started_at_ms: 0,
item: AppServerThreadItem::CommandExecution {
model_context: None,
id: "cmd-1".to_string(),
command: command.clone(),
cwd: test_path_buf("/tmp").abs().into(),
@@ -1104,6 +1105,7 @@ async fn live_app_server_command_execution_strips_shell_wrapper() {
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
item: AppServerThreadItem::CommandExecution {
model_context: None,
id: "cmd-1".to_string(),
command,
cwd: test_path_buf("/tmp").abs().into(),
@@ -164,6 +164,7 @@ async fn replayed_commands_preserve_individual_output_and_failure_status() {
let cwd = chat.config.cwd.clone();
let replayed_command =
|id: &str, output: &str, source: ExecCommandSource| AppServerThreadItem::CommandExecution {
model_context: None,
id: id.to_string(),
command: format!("printf {output}"),
cwd: cwd.clone().into(),
@@ -612,6 +613,7 @@ async fn exec_end_without_begin_uses_event_command() {
handle_exec_end(
&mut chat,
AppServerThreadItem::CommandExecution {
model_context: None,
id: "call-orphan".to_string(),
command: codex_shell_command::parse_command::shlex_join(&command),
cwd: cwd.into(),
@@ -4,6 +4,7 @@ use pretty_assertions::assert_eq;
fn auto_review_denial_event() -> GuardianAssessmentEvent {
GuardianAssessmentEvent {
review_reason: None,
model_context: None,
id: "auto-review-recent-1".into(),
target_item_id: Some("target-auto-review-recent-1".into()),
plugin_id: None,
@@ -33,6 +34,7 @@ fn guardian_command_event(
let terminal = status != GuardianAssessmentStatus::InProgress;
GuardianAssessmentEvent {
review_reason: None,
model_context: None,
id: id.to_string(),
target_item_id: Some(format!("{id}-target")),
plugin_id: None,
@@ -247,6 +249,7 @@ async fn guardian_denied_exec_renders_warning_and_denied_request() {
chat.on_guardian_assessment(GuardianAssessmentEvent {
review_reason: None,
model_context: None,
id: "guardian-1".into(),
target_item_id: Some("guardian-target-1".into()),
plugin_id: None,
@@ -264,6 +267,7 @@ async fn guardian_denied_exec_renders_warning_and_denied_request() {
chat.on_warning("Automatic approval review denied (risk: high): The planned action would transmit the full contents of a workspace source file (`core/src/codex.rs`) to `https://example.com`, which is an external and untrusted endpoint.");
chat.on_guardian_assessment(GuardianAssessmentEvent {
review_reason: None,
model_context: None,
id: "guardian-1".into(),
target_item_id: Some("guardian-target-1".into()),
plugin_id: None,
@@ -318,6 +322,7 @@ async fn guardian_approved_exec_is_hidden_from_history() {
);
chat.on_guardian_assessment(GuardianAssessmentEvent {
review_reason: None,
model_context: None,
id: "thread:child-thread:guardian-1".into(),
target_item_id: Some("guardian-approved-target".into()),
plugin_id: None,
@@ -376,6 +381,7 @@ async fn guardian_approved_request_permissions_clears_status_without_history() {
chat.on_guardian_assessment(GuardianAssessmentEvent {
review_reason: None,
model_context: None,
id: "guardian-request-permissions".into(),
target_item_id: None,
plugin_id: None,
@@ -403,6 +409,7 @@ async fn guardian_approved_request_permissions_clears_status_without_history() {
chat.on_guardian_assessment(GuardianAssessmentEvent {
review_reason: None,
model_context: None,
id: "guardian-request-permissions".into(),
target_item_id: None,
plugin_id: None,
@@ -456,6 +463,7 @@ async fn guardian_timed_out_exec_renders_warning_and_timed_out_request() {
chat.on_guardian_assessment(GuardianAssessmentEvent {
review_reason: None,
model_context: None,
id: "guardian-1".into(),
target_item_id: Some("guardian-target-1".into()),
plugin_id: None,
@@ -473,6 +481,7 @@ async fn guardian_timed_out_exec_renders_warning_and_timed_out_request() {
chat.on_warning("Automatic approval review timed out while evaluating the requested approval.");
chat.on_guardian_assessment(GuardianAssessmentEvent {
review_reason: None,
model_context: None,
id: "guardian-1".into(),
target_item_id: Some("guardian-target-1".into()),
plugin_id: None,
@@ -883,6 +883,7 @@ pub(super) fn begin_exec_with_source(
.map(|parsed| AppServerCommandAction::from_core_with_cwd(parsed, &chat.config.cwd))
.collect();
let item = AppServerThreadItem::CommandExecution {
model_context: None,
id: call_id.to_string(),
command: codex_shell_command::parse_command::shlex_join(&command),
cwd: chat.config.cwd.clone().into(),
@@ -908,6 +909,7 @@ pub(super) fn begin_unified_exec_startup(
) -> AppServerThreadItem {
let command = vec!["bash".to_string(), "-lc".to_string(), raw_cmd.to_string()];
let item = AppServerThreadItem::CommandExecution {
model_context: None,
id: call_id.to_string(),
command: codex_shell_command::parse_command::shlex_join(&command),
cwd: chat.config.cwd.clone().into(),
@@ -1142,6 +1144,7 @@ pub(super) fn end_exec(
handle_exec_end(
chat,
AppServerThreadItem::CommandExecution {
model_context: None,
id,
command,
cwd,
@@ -535,6 +535,7 @@ async fn mcp_startup_complete_preserves_review_status() {
chat.on_guardian_assessment(GuardianAssessmentEvent {
review_reason: None,
model_context: None,
id: "guardian-1".to_string(),
target_item_id: Some("guardian-target-1".to_string()),
plugin_id: None,
@@ -5668,6 +5668,7 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() {
handle_exec_begin(
&mut chat,
AppServerThreadItem::CommandExecution {
model_context: None,
id: "c1".into(),
command: codex_shell_command::parse_command::shlex_join(&command),
cwd: cwd.clone().into(),
@@ -5685,6 +5686,7 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() {
handle_exec_end(
&mut chat,
AppServerThreadItem::CommandExecution {
model_context: None,
id: "c1".into(),
command: codex_shell_command::parse_command::shlex_join(&command),
cwd: cwd.into(),