From 8d9f33c87ca2e7cb89490703fb03f74c9a26578b Mon Sep 17 00:00:00 2001 From: rhan-oai Date: Wed, 10 Jun 2026 23:07:06 -0700 Subject: [PATCH] [codex-analytics] Emit structured compaction codex errors (#27082) ## Summary - replace raw compaction `error` analytics with `codex_error_kind` and `codex_error_http_status_code` - derive compaction error telemetry from `CodexErr` using the same `CodexErrKind` mapping and HTTP status helper used by turn events - remove the pre-compact hook stop reason from the internal compaction outcome now that it is no longer emitted as raw analytics text ## Why Compaction `error` was a raw `CodexErr::to_string()` value, which can carry free-form provider or user-derived text. Structured Codex error fields preserve useful low-cardinality telemetry without sending the raw string. ## Validation - `just fmt` - `just test -p codex-analytics` - `just test -p codex-core compact::tests::build_token_limited_compacted_history_appends_summary_message` Attempted `just test -p codex-core`; the changed crate compiled, but the full target failed in unrelated environment-dependent tests such as missing helper binaries and shell snapshot timeouts. --- .../analytics/src/analytics_client_tests.rs | 21 +++++++++++++++---- codex-rs/analytics/src/events.rs | 6 ++++-- codex-rs/analytics/src/facts.rs | 5 +++-- codex-rs/analytics/src/lib.rs | 1 + codex-rs/core/src/compact.rs | 20 ++++++++++-------- codex-rs/core/src/compact_remote.rs | 14 ++++++------- codex-rs/core/src/compact_remote_v2.rs | 14 ++++++------- codex-rs/core/src/hook_runtime.rs | 6 ++---- 8 files changed, 52 insertions(+), 35 deletions(-) diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index 1a1c7d2d5..49cbf0a85 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -43,6 +43,7 @@ use crate::facts::AppInvocation; use crate::facts::AppMentionedInput; use crate::facts::AppUsedInput; use crate::facts::CodexCompactionEvent; +use crate::facts::CodexErrKind; use crate::facts::CompactionImplementation; use crate::facts::CompactionPhase; use crate::facts::CompactionReason; @@ -1278,7 +1279,8 @@ fn compaction_event_serializes_expected_shape() { phase: CompactionPhase::MidTurn, strategy: CompactionStrategy::Memento, status: CompactionStatus::Completed, - error: None, + codex_error_kind: None, + codex_error_http_status_code: None, active_context_tokens_before: 120_000, active_context_tokens_after: 18_000, retained_image_count: None, @@ -1329,7 +1331,8 @@ fn compaction_event_serializes_expected_shape() { "phase": "mid_turn", "strategy": "memento", "status": "completed", - "error": null, + "codex_error_kind": null, + "codex_error_http_status_code": null, "active_context_tokens_before": 120000, "active_context_tokens_after": 18000, "retained_image_count": null, @@ -1852,7 +1855,8 @@ async fn compaction_event_ingests_custom_fact() { phase: CompactionPhase::StandaloneTurn, strategy: CompactionStrategy::Memento, status: CompactionStatus::Failed, - error: Some("context limit exceeded".to_string()), + codex_error_kind: Some(CodexErrKind::ContextWindowExceeded), + codex_error_http_status_code: None, active_context_tokens_before: 131_000, active_context_tokens_after: 131_000, retained_image_count: None, @@ -1873,6 +1877,14 @@ async fn compaction_event_ingests_custom_fact() { assert_eq!(payload[0]["event_params"]["session_id"], "session-thread-1"); assert_eq!(payload[0]["event_params"]["thread_id"], "thread-1"); assert_eq!(payload[0]["event_params"]["turn_id"], "turn-compact"); + assert_eq!( + payload[0]["event_params"]["codex_error_kind"], + json!("context_window_exceeded") + ); + assert_eq!( + payload[0]["event_params"]["codex_error_http_status_code"], + json!(null) + ); assert_eq!( payload[0]["event_params"]["app_server_client"]["product_client_id"], DEFAULT_ORIGINATOR @@ -2779,7 +2791,8 @@ async fn subagent_events_use_inherited_connection_unless_turn_connection_is_expl phase: CompactionPhase::StandaloneTurn, strategy: CompactionStrategy::Memento, status: CompactionStatus::Completed, - error: None, + codex_error_kind: None, + codex_error_http_status_code: None, active_context_tokens_before: 131_000, active_context_tokens_after: 64_000, retained_image_count: None, diff --git a/codex-rs/analytics/src/events.rs b/codex-rs/analytics/src/events.rs index 5213bf070..997ee51c5 100644 --- a/codex-rs/analytics/src/events.rs +++ b/codex-rs/analytics/src/events.rs @@ -762,7 +762,8 @@ pub(crate) struct CodexCompactionEventParams { pub(crate) phase: CompactionPhase, pub(crate) strategy: CompactionStrategy, pub(crate) status: CompactionStatus, - pub(crate) error: Option, + pub(crate) codex_error_kind: Option, + pub(crate) codex_error_http_status_code: Option, pub(crate) active_context_tokens_before: i64, pub(crate) active_context_tokens_after: i64, pub(crate) retained_image_count: Option, @@ -1000,7 +1001,8 @@ pub(crate) fn codex_compaction_event_params( phase: input.phase, strategy: input.strategy, status: input.status, - error: input.error, + codex_error_kind: input.codex_error_kind, + codex_error_http_status_code: input.codex_error_http_status_code, active_context_tokens_before: input.active_context_tokens_before, active_context_tokens_after: input.active_context_tokens_after, retained_image_count: input.retained_image_count, diff --git a/codex-rs/analytics/src/facts.rs b/codex-rs/analytics/src/facts.rs index 62ce7f9a9..ab278e223 100644 --- a/codex-rs/analytics/src/facts.rs +++ b/codex-rs/analytics/src/facts.rs @@ -137,7 +137,7 @@ impl TurnCodexErrorFact { #[derive(Clone, Copy, Debug, Serialize)] #[serde(rename_all = "snake_case")] -pub(crate) enum CodexErrKind { +pub enum CodexErrKind { TurnAborted, Stream, ContextWindowExceeded, @@ -408,7 +408,8 @@ pub struct CodexCompactionEvent { pub phase: CompactionPhase, pub strategy: CompactionStrategy, pub status: CompactionStatus, - pub error: Option, + pub codex_error_kind: Option, + pub codex_error_http_status_code: Option, pub active_context_tokens_before: i64, pub active_context_tokens_after: i64, pub retained_image_count: Option, diff --git a/codex-rs/analytics/src/lib.rs b/codex-rs/analytics/src/lib.rs index 35bd539cd..fd236cbce 100644 --- a/codex-rs/analytics/src/lib.rs +++ b/codex-rs/analytics/src/lib.rs @@ -24,6 +24,7 @@ pub use facts::AcceptedLineFingerprint; pub use facts::AnalyticsJsonRpcError; pub use facts::AppInvocation; pub use facts::CodexCompactionEvent; +pub use facts::CodexErrKind; pub use facts::CodexGoalEvent; pub use facts::CodexTurnSteerEvent; pub use facts::CompactionImplementation; diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs index abfac7ddd..26b5e33bf 100644 --- a/codex-rs/core/src/compact.rs +++ b/codex-rs/core/src/compact.rs @@ -143,17 +143,17 @@ async fn run_compact_task_inner( let pre_compact_outcome = run_pre_compact_hooks(&sess, &turn_context, trigger).await; match pre_compact_outcome { PreCompactHookOutcome::Continue => {} - PreCompactHookOutcome::Stopped { reason } => { - let error = reason.unwrap_or_else(|| "PreCompact hook stopped execution".to_string()); + PreCompactHookOutcome::Stopped => { + let error = CodexErr::TurnAborted; attempt .track( sess.as_ref(), CompactionStatus::Interrupted, - Some(error), + Some(&error), CompactionAnalyticsDetails::default(), ) .await; - return Err(CodexErr::TurnAborted); + return Err(error); } } let result = run_compact_task_inner_impl( @@ -165,7 +165,7 @@ async fn run_compact_task_inner( ) .await; let status = compaction_status_from_result(&result); - let error = result.as_ref().err().map(ToString::to_string); + let codex_error = result.as_ref().err(); if result.is_ok() { let post_compact_outcome = run_post_compact_hooks(&sess, &turn_context, trigger).await; if let PostCompactHookOutcome::Stopped = post_compact_outcome { @@ -173,7 +173,7 @@ async fn run_compact_task_inner( .track( sess.as_ref(), status, - error, + codex_error, CompactionAnalyticsDetails::default(), ) .await; @@ -184,7 +184,7 @@ async fn run_compact_task_inner( .track( sess.as_ref(), status, - error, + codex_error, CompactionAnalyticsDetails::default(), ) .await; @@ -373,7 +373,7 @@ impl CompactionAnalyticsAttempt { self, sess: &Session, status: CompactionStatus, - error: Option, + codex_error: Option<&CodexErr>, details: CompactionAnalyticsDetails, ) { let CompactionAnalyticsDetails { @@ -396,7 +396,9 @@ impl CompactionAnalyticsAttempt { phase: self.phase, strategy: CompactionStrategy::Memento, status, - error, + codex_error_kind: codex_error.map(Into::into), + codex_error_http_status_code: codex_error + .and_then(CodexErr::http_status_code_value), active_context_tokens_before, active_context_tokens_after, retained_image_count, diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs index 866756f65..93457053f 100644 --- a/codex-rs/core/src/compact_remote.rs +++ b/codex-rs/core/src/compact_remote.rs @@ -113,17 +113,17 @@ async fn run_remote_compact_task_inner( let pre_compact_outcome = run_pre_compact_hooks(sess, turn_context, trigger).await; match pre_compact_outcome { PreCompactHookOutcome::Continue => {} - PreCompactHookOutcome::Stopped { reason } => { - let error = reason.unwrap_or_else(|| "PreCompact hook stopped execution".to_string()); + PreCompactHookOutcome::Stopped => { + let error = CodexErr::TurnAborted; attempt .track( sess.as_ref(), codex_analytics::CompactionStatus::Interrupted, - Some(error), + Some(&error), analytics_details, ) .await; - return Err(CodexErr::TurnAborted); + return Err(error); } } let result = run_remote_compact_task_inner_impl( @@ -135,18 +135,18 @@ async fn run_remote_compact_task_inner( ) .await; let status = compaction_status_from_result(&result); - let error = result.as_ref().err().map(ToString::to_string); + let codex_error = result.as_ref().err(); if result.is_ok() { let post_compact_outcome = run_post_compact_hooks(sess, turn_context, trigger).await; if let PostCompactHookOutcome::Stopped = post_compact_outcome { attempt - .track(sess.as_ref(), status, error, analytics_details) + .track(sess.as_ref(), status, codex_error, analytics_details) .await; return Err(CodexErr::TurnAborted); } } attempt - .track(sess.as_ref(), status, error.clone(), analytics_details) + .track(sess.as_ref(), status, codex_error, analytics_details) .await; if let Err(err) = result { sess.track_turn_codex_error(turn_context, &err); diff --git a/codex-rs/core/src/compact_remote_v2.rs b/codex-rs/core/src/compact_remote_v2.rs index d25484910..5ab7a68a5 100644 --- a/codex-rs/core/src/compact_remote_v2.rs +++ b/codex-rs/core/src/compact_remote_v2.rs @@ -127,17 +127,17 @@ async fn run_remote_compact_task_inner( let pre_compact_outcome = run_pre_compact_hooks(sess, turn_context, trigger).await; match pre_compact_outcome { PreCompactHookOutcome::Continue => {} - PreCompactHookOutcome::Stopped { reason } => { - let error = reason.unwrap_or_else(|| "PreCompact hook stopped execution".to_string()); + PreCompactHookOutcome::Stopped => { + let error = CodexErr::TurnAborted; attempt .track( sess.as_ref(), codex_analytics::CompactionStatus::Interrupted, - Some(error), + Some(&error), analytics_details, ) .await; - return Err(CodexErr::TurnAborted); + return Err(error); } } let result = run_remote_compact_task_inner_impl( @@ -150,18 +150,18 @@ async fn run_remote_compact_task_inner( ) .await; let status = compaction_status_from_result(&result); - let error = result.as_ref().err().map(ToString::to_string); + let codex_error = result.as_ref().err(); if result.is_ok() { let post_compact_outcome = run_post_compact_hooks(sess, turn_context, trigger).await; if let PostCompactHookOutcome::Stopped = post_compact_outcome { attempt - .track(sess.as_ref(), status, error, analytics_details) + .track(sess.as_ref(), status, codex_error, analytics_details) .await; return Err(CodexErr::TurnAborted); } } attempt - .track(sess.as_ref(), status, error.clone(), analytics_details) + .track(sess.as_ref(), status, codex_error, analytics_details) .await; if let Err(err) = result { sess.track_turn_codex_error(turn_context, &err); diff --git a/codex-rs/core/src/hook_runtime.rs b/codex-rs/core/src/hook_runtime.rs index 8501c6259..1097f7bc7 100644 --- a/codex-rs/core/src/hook_runtime.rs +++ b/codex-rs/core/src/hook_runtime.rs @@ -385,9 +385,7 @@ pub(crate) async fn run_pre_compact_hooks( let outcome = sess.hooks().run_pre_compact(request).await; emit_hook_completed_events(sess, turn_context, outcome.hook_events).await; if outcome.should_stop { - PreCompactHookOutcome::Stopped { - reason: outcome.stop_reason, - } + PreCompactHookOutcome::Stopped } else { PreCompactHookOutcome::Continue } @@ -395,7 +393,7 @@ pub(crate) async fn run_pre_compact_hooks( pub(crate) enum PreCompactHookOutcome { Continue, - Stopped { reason: Option }, + Stopped, } pub(crate) enum PostCompactHookOutcome {