[codex-analytics] add extensible feature thread sources (#27063)

## Why
- `ThreadSource` currently defines a closed set of core-owned values
- Product features also create threads for background or scheduled work
- Adding every product-specific value to the core enum would require
repeated `codex-rs` protocol changes
- Feature-backed values let product callers provide precise attribution
while preserving the existing core classifications

## What Changed
- Adds `ThreadSource::Feature(String)` for app-owned thread source
values
- Represents all app-server v2 thread sources as scalar strings, so a
feature source is supplied as `"automation"`
- Persists and emits the feature's plain string label, so `"automation"`
produces `thread_source="automation"` in analytics
- Keeps `user`, `subagent`, and `memory_consolidation` as explicit
core-owned values and regenerates the app-server schemas and TypeScript
bindings

## Verification
- `just write-app-server-schema`
- `cargo check --workspace`
- `just test -p codex-protocol
feature_thread_source_serializes_as_its_app_owned_label`
- `just test -p codex-app-server-protocol
thread_sources_round_trip_as_scalar_labels`
- `cargo test -p codex-analytics
thread_initialized_event_serializes_expected_shape`
- `just fmt`
This commit is contained in:
marksteinbrick-oai
2026-06-09 12:27:10 -07:00
committed by GitHub
Unverified
parent 99da697e4c
commit a71e040df5
32 changed files with 123 additions and 113 deletions
+36 -7
View File
@@ -2480,12 +2480,12 @@ impl InitialHistory {
pub fn get_resumed_session_sources(&self) -> Option<(SessionSource, Option<ThreadSource>)> {
let meta = self.get_resumed_session_meta()?;
Some((meta.source.clone(), meta.thread_source))
Some((meta.source.clone(), meta.thread_source.clone()))
}
pub fn get_resumed_thread_source(&self) -> Option<ThreadSource> {
self.get_resumed_session_meta()
.and_then(|meta| meta.thread_source)
.and_then(|meta| meta.thread_source.clone())
}
pub fn get_resumed_parent_thread_id(&self) -> Option<ThreadId> {
@@ -2529,20 +2529,23 @@ pub enum SessionSource {
Unknown,
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(rename_all = "snake_case")]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]
#[serde(try_from = "String", into = "String")]
#[schemars(with = "String")]
#[ts(type = "string")]
pub enum ThreadSource {
User,
Subagent,
Feature(String),
MemoryConsolidation,
}
impl ThreadSource {
pub fn as_str(self) -> &'static str {
pub fn as_str(&self) -> &str {
match self {
ThreadSource::User => "user",
ThreadSource::Subagent => "subagent",
ThreadSource::Feature(feature) => feature,
ThreadSource::MemoryConsolidation => "memory_consolidation",
}
}
@@ -2554,6 +2557,20 @@ impl fmt::Display for ThreadSource {
}
}
impl TryFrom<String> for ThreadSource {
type Error = String;
fn try_from(value: String) -> Result<Self, Self::Error> {
value.parse()
}
}
impl From<ThreadSource> for String {
fn from(value: ThreadSource) -> Self {
value.to_string()
}
}
impl FromStr for ThreadSource {
type Err = String;
@@ -2562,7 +2579,7 @@ impl FromStr for ThreadSource {
"user" => Ok(ThreadSource::User),
"subagent" => Ok(ThreadSource::Subagent),
"memory_consolidation" => Ok(ThreadSource::MemoryConsolidation),
other => Err(format!("unknown thread source: {other}")),
other => Ok(ThreadSource::Feature(other.to_string())),
}
}
}
@@ -4048,6 +4065,18 @@ mod tests {
use tempfile::NamedTempFile;
use tempfile::TempDir;
#[test]
fn feature_thread_source_serializes_as_its_app_owned_label() -> Result<()> {
let source = ThreadSource::Feature("automation".to_string());
assert_eq!(serde_json::to_value(&source)?, json!("automation"));
assert_eq!(
serde_json::from_value::<ThreadSource>(json!("automation"))?,
source
);
Ok(())
}
fn sorted_writable_roots(roots: Vec<WritableRoot>) -> Vec<(PathBuf, Vec<PathBuf>)> {
let mut sorted_roots: Vec<(PathBuf, Vec<PathBuf>)> = roots
.into_iter()