[codex-analytics] emit goal lifecycle analytics (#27078)

## Why
- Currently, there is no analytics event for `/goal` behavior
- Existing events cannot identify goal execution or its resulting
outcome
- The original update in
[#26182](https://github.com/openai/codex/pull/26182) was implemented
before `/goal` moved into `codex-goal-extension`.

## What Changed
- Adds `codex_goal_event` serialization and enrichment to
`codex-analytics`
- Emits goal events from the canonical `codex-goal-extension` mutation
and accounting paths:
  - `created` when a new logical goal is persisted
  - `usage_accounted` when cumulative goal usage is persisted
  - `status_changed` when the stored goal status changes
  - `cleared` when the goal is deleted
- Preserves causal `turn_id` for turn driven events and uses null
attribution for external or idle lifecycle events
- Changes goal deletion to return the deleted row so `cleared` retains
the stable goal ID

## Event Details

Includes standard analytics metadata along with goal specific fields:
- `goal_id`: Stable ID stored in the local SQLite goal row and shared
across the goal's events
- `event_kind`: Observed operation (see the 4 lifecycle events cited in
the above bullet)
- `goal_status`: Resulting or last stored status: `active`, `paused`,
`blocked`, `usage_limited`, etc.
  - `has_token_budget`: Indicates whether a token budget is configured
  - `turn_id`: Causal turn ID, or null when no causal turn exists
- `cumulative_tokens_accounted`: Cumulative tokens on `usage_accounted`
events; null otherwise
- `cumulative_time_accounted_seconds`: Cumulative active time on
`usage_accounted` events; null otherwise

## Validation
- `just test -p codex-analytics -p codex-state -p codex-goal-extension`
- `just test -p codex-core -E 'test(/goal/)'`
- `just test -p codex-app-server`
- `cargo build -p codex-analytics -p codex-core -p codex-state -p
codex-app-server`
This commit is contained in:
marksteinbrick-oai authored and GitHub committed 2026-06-09 18:45:54 -07:00
1 parent 4a3eac2144
commit 608b8b1cc6
23 files changed
+412 -24

No files matched your search

+25
View File
@@ -17,6 +17,8 @@ use serde::Serialize;
use crate::accounting::BudgetLimitedGoalDisposition;
use crate::accounting::GoalAccountingState;
use crate::analytics::GoalAnalytics;
use crate::analytics::GoalEventAttribution;
use crate::events::GoalEventEmitter;
use crate::metrics::GoalMetrics;
use crate::spec::CREATE_GOAL_TOOL_NAME;
@@ -32,6 +34,7 @@ pub(crate) struct GoalToolExecutor {
thread_id: ThreadId,
state_db: Arc<codex_state::StateRuntime>,
accounting_state: Arc<GoalAccountingState>,
analytics: GoalAnalytics,
event_emitter: GoalEventEmitter,
metrics: GoalMetrics,
}
@@ -75,6 +78,7 @@ impl GoalToolExecutor {
thread_id: ThreadId,
state_db: Arc<codex_state::StateRuntime>,
accounting_state: Arc<GoalAccountingState>,
analytics: GoalAnalytics,
event_emitter: GoalEventEmitter,
metrics: GoalMetrics,
) -> Self {
@@ -83,6 +87,7 @@ impl GoalToolExecutor {
thread_id,
state_db,
accounting_state,
analytics,
event_emitter,
metrics,
}
@@ -92,6 +97,7 @@ impl GoalToolExecutor {
thread_id: ThreadId,
state_db: Arc<codex_state::StateRuntime>,
accounting_state: Arc<GoalAccountingState>,
analytics: GoalAnalytics,
event_emitter: GoalEventEmitter,
metrics: GoalMetrics,
) -> Self {
@@ -100,6 +106,7 @@ impl GoalToolExecutor {
thread_id,
state_db,
accounting_state,
analytics,
event_emitter,
metrics,
}
@@ -109,6 +116,7 @@ impl GoalToolExecutor {
thread_id: ThreadId,
state_db: Arc<codex_state::StateRuntime>,
accounting_state: Arc<GoalAccountingState>,
analytics: GoalAnalytics,
event_emitter: GoalEventEmitter,
metrics: GoalMetrics,
) -> Self {
@@ -117,6 +125,7 @@ impl GoalToolExecutor {
thread_id,
state_db,
accounting_state,
analytics,
event_emitter,
metrics,
}
@@ -200,6 +209,10 @@ impl GoalToolExecutor {
.accounting_state
.mark_current_turn_goal_active(goal.goal_id.clone());
self.metrics.record_created();
self.analytics.created(
&goal,
GoalEventAttribution::Turn(invocation.turn_id.as_str()),
);
let goal = protocol_goal_from_state(goal);
self.emit_goal_updated_from_tool_call(&invocation, turn_id, goal.clone());
goal_response(Some(goal), CompletionBudgetReport::Omit)
@@ -259,6 +272,11 @@ impl GoalToolExecutor {
})?;
self.metrics
.record_terminal_if_status_changed(previous_status, &goal);
self.analytics.status_changed(
&goal,
previous_status,
GoalEventAttribution::Turn(invocation.turn_id.as_str()),
);
let goal = protocol_goal_from_state(goal);
let turn_id = self.accounting_state.clear_current_turn_goal();
self.emit_goal_updated_from_tool_call(&invocation, turn_id, goal.clone());
@@ -324,6 +342,13 @@ impl GoalToolExecutor {
codex_state::GoalAccountingOutcome::Updated(goal) => {
self.metrics
.record_terminal_if_status_changed(previous_status, &goal);
self.analytics
.usage_accounted(&goal, GoalEventAttribution::Turn(turn_id.as_str()));
self.analytics.status_changed(
&goal,
previous_status,
GoalEventAttribution::Turn(turn_id.as_str()),
);
self.accounting_state.mark_progress_accounted_for_status(
turn_id.as_str(),
&snapshot,