[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
2026-06-09 18:45:54 -07:00
committed by GitHub
parent 4a3eac2144
commit 608b8b1cc6
23 changed files with 412 additions and 24 deletions
+22 -7
View File
@@ -377,18 +377,31 @@ WHERE thread_id = ?
self.get_thread_goal(thread_id).await
}
pub async fn delete_thread_goal(&self, thread_id: ThreadId) -> anyhow::Result<bool> {
let result = sqlx::query(
pub async fn delete_thread_goal(
&self,
thread_id: ThreadId,
) -> anyhow::Result<Option<crate::ThreadGoal>> {
let row = sqlx::query(
r#"
DELETE FROM thread_goals
WHERE thread_id = ?
RETURNING
thread_id,
goal_id,
objective,
status,
token_budget,
tokens_used,
time_used_seconds,
created_at_ms,
updated_at_ms
"#,
)
.bind(thread_id.to_string())
.execute(self.pool.as_ref())
.fetch_optional(self.pool.as_ref())
.await?;
Ok(result.rows_affected() > 0)
row.map(|row| thread_goal_from_row(&row)).transpose()
}
pub async fn account_thread_goal_usage(
@@ -622,7 +635,8 @@ mod tests {
assert_eq!(0, replaced.tokens_used);
assert_eq!(0, replaced.time_used_seconds);
assert!(
assert_eq!(
Some(replaced),
runtime
.thread_goals()
.delete_thread_goal(thread_id)
@@ -637,8 +651,9 @@ mod tests {
.await
.unwrap()
);
assert!(
!runtime
assert_eq!(
None,
runtime
.thread_goals()
.delete_thread_goal(thread_id)
.await
+1 -1
View File
@@ -891,7 +891,7 @@ ON CONFLICT(id) DO UPDATE SET
let rows_affected = result.rows_affected();
self.memories.delete_thread_memory(thread_id).await?;
if rows_affected > 0 {
self.thread_goals.delete_thread_goal(thread_id).await?;
let _ = self.thread_goals.delete_thread_goal(thread_id).await?;
}
Ok(rows_affected)
}