[codex-analytics] rework thread_source for thread analytics (#20949)

## Summary
- make `thread_source` an explicit optional thread-level field on
`thread/start`, `thread/fork`, and returned thread payloads
- persist `thread_source` in rollout/session metadata so resumed live
threads retain the original value
- replace the old best-effort `session_source` -> `thread_source`
mapping with an explicit caller-supplied analytics classification

## Why
Before this change, analytics `thread_source` was populated by a
best-effort mapping from `session_source`. `session_source` describes
the runtime/client surface, not the actual thread-level origin, so that
projection was not accurate enough to distinguish cases such as `user`,
`subagent`, `memory_consolidation`, and future thread origins reliably.

Making `thread_source` explicit keeps one thread-level analytics field
while letting callers provide the real classification directly instead
of recovering it indirectly from `session_source`.

## Impact
For new analytics events, `thread_source` now reflects the explicit
thread-level classification supplied by the caller rather than an
inferred value derived from `session_source`. Existing protocol fields
remain optional; callers that omit `threadSource` now produce `null`
instead of a best-effort inferred value.

## Validation
- `just write-app-server-schema`
- `cargo test -p codex-analytics -p codex-core -p
codex-app-server-protocol --no-run`
- `cargo test -p codex-app-server-protocol
generated_ts_optional_nullable_fields_only_in_params`
- `cargo test -p codex-analytics
thread_initialized_event_serializes_expected_shape`
- `cargo test -p codex-core
resume_stopped_thread_from_rollout_preserves_thread_source`
This commit is contained in:
rhan-oai
2026-05-06 02:12:31 +00:00
committed by GitHub
parent 94db03d5af
commit b3d4f1a9f0
98 changed files with 896 additions and 90 deletions
+1
View File
@@ -137,6 +137,7 @@ SELECT
threads.created_at_ms AS created_at,
threads.updated_at_ms AS updated_at,
threads.source,
threads.thread_source,
threads.agent_path,
threads.agent_nickname,
threads.agent_role,
@@ -48,6 +48,7 @@ pub(super) fn test_thread_metadata(
created_at: now,
updated_at: now,
source: "cli".to_string(),
thread_source: None,
agent_nickname: None,
agent_role: None,
agent_path: None,
+19 -2
View File
@@ -13,6 +13,7 @@ SELECT
threads.created_at_ms AS created_at,
threads.updated_at_ms AS updated_at,
threads.source,
threads.thread_source,
threads.agent_nickname,
threads.agent_role,
threads.agent_path,
@@ -486,6 +487,7 @@ INSERT INTO threads (
created_at_ms,
updated_at_ms,
source,
thread_source,
agent_nickname,
agent_role,
agent_path,
@@ -505,7 +507,7 @@ INSERT INTO threads (
git_branch,
git_origin_url,
memory_mode
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO NOTHING
"#,
)
@@ -516,6 +518,11 @@ ON CONFLICT(id) DO NOTHING
.bind(datetime_to_epoch_millis(metadata.created_at))
.bind(datetime_to_epoch_millis(updated_at))
.bind(metadata.source.as_str())
.bind(
metadata
.thread_source
.map(codex_protocol::protocol::ThreadSource::as_str),
)
.bind(metadata.agent_nickname.as_deref())
.bind(metadata.agent_role.as_deref())
.bind(metadata.agent_path.as_deref())
@@ -683,6 +690,7 @@ INSERT INTO threads (
created_at_ms,
updated_at_ms,
source,
thread_source,
agent_nickname,
agent_role,
agent_path,
@@ -702,7 +710,7 @@ INSERT INTO threads (
git_branch,
git_origin_url,
memory_mode
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
rollout_path = excluded.rollout_path,
created_at = excluded.created_at,
@@ -710,6 +718,7 @@ ON CONFLICT(id) DO UPDATE SET
created_at_ms = excluded.created_at_ms,
updated_at_ms = excluded.updated_at_ms,
source = excluded.source,
thread_source = excluded.thread_source,
agent_nickname = excluded.agent_nickname,
agent_role = excluded.agent_role,
agent_path = excluded.agent_path,
@@ -737,6 +746,11 @@ ON CONFLICT(id) DO UPDATE SET
.bind(datetime_to_epoch_millis(metadata.created_at))
.bind(datetime_to_epoch_millis(updated_at))
.bind(metadata.source.as_str())
.bind(
metadata
.thread_source
.map(codex_protocol::protocol::ThreadSource::as_str),
)
.bind(metadata.agent_nickname.as_deref())
.bind(metadata.agent_role.as_deref())
.bind(metadata.agent_path.as_deref())
@@ -958,6 +972,7 @@ SELECT
threads.created_at_ms AS created_at,
threads.updated_at_ms AS updated_at,
threads.source,
threads.thread_source,
threads.agent_nickname,
threads.agent_role,
threads.agent_path,
@@ -1361,6 +1376,7 @@ mod tests {
originator: String::new(),
cli_version: String::new(),
source: SessionSource::Cli,
thread_source: None,
agent_path: None,
agent_nickname: None,
agent_role: None,
@@ -1419,6 +1435,7 @@ mod tests {
originator: String::new(),
cli_version: String::new(),
source: SessionSource::Cli,
thread_source: None,
agent_path: None,
agent_nickname: None,
agent_role: None,