mirror of
https://github.com/openai/codex.git
synced 2026-11-13 22:47:20 +00:00
[codex-analytics] emit forked thread id on initialization (#26248)
## Why - Thread initialization analytics do not identify the source thread for forked threads. - The session viewer needs this lineage to construct thread trees. - Depends on openai/openai#987854. Do not release this change before that backend schema change is deployed. ## What Changed - Adds optional `forked_from_thread_id` to `codex_thread_initialized`. - Populates it from the existing thread fork lineage for app-server and in-process subagent initialization paths. - Keeps it null for non-forked threads. ## Verification - `just fmt` - `just test -p codex-analytics` - `just test -p codex-app-server thread_fork_tracks_thread_initialized_analytics`
This commit is contained in:
1 parent
c8fdc74b42
commit
9e41f8ddbe
10 files changed
+119
-1
No files matched your search
@@ -1347,6 +1347,7 @@ fn thread_initialized_event_serializes_expected_shape() {
|
|||||||
initialization_mode: ThreadInitializationMode::New,
|
initialization_mode: ThreadInitializationMode::New,
|
||||||
subagent_source: None,
|
subagent_source: None,
|
||||||
parent_thread_id: None,
|
parent_thread_id: None,
|
||||||
|
forked_from_thread_id: None,
|
||||||
created_at: 1,
|
created_at: 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1379,6 +1380,7 @@ fn thread_initialized_event_serializes_expected_shape() {
|
|||||||
"initialization_mode": "new",
|
"initialization_mode": "new",
|
||||||
"subagent_source": null,
|
"subagent_source": null,
|
||||||
"parent_thread_id": null,
|
"parent_thread_id": null,
|
||||||
|
"forked_from_thread_id": null,
|
||||||
"created_at": 1
|
"created_at": 1
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -2429,6 +2431,7 @@ fn subagent_thread_started_review_serializes_expected_shape() {
|
|||||||
session_id: "session-root".to_string(),
|
session_id: "session-root".to_string(),
|
||||||
thread_id: "thread-review".to_string(),
|
thread_id: "thread-review".to_string(),
|
||||||
parent_thread_id: None,
|
parent_thread_id: None,
|
||||||
|
forked_from_thread_id: None,
|
||||||
product_client_id: "codex-tui".to_string(),
|
product_client_id: "codex-tui".to_string(),
|
||||||
client_name: "codex-tui".to_string(),
|
client_name: "codex-tui".to_string(),
|
||||||
client_version: "1.0.0".to_string(),
|
client_version: "1.0.0".to_string(),
|
||||||
@@ -2461,18 +2464,26 @@ fn subagent_thread_started_review_serializes_expected_shape() {
|
|||||||
assert_eq!(payload["event_params"]["initialization_mode"], "new");
|
assert_eq!(payload["event_params"]["initialization_mode"], "new");
|
||||||
assert_eq!(payload["event_params"]["subagent_source"], "review");
|
assert_eq!(payload["event_params"]["subagent_source"], "review");
|
||||||
assert_eq!(payload["event_params"]["parent_thread_id"], json!(null));
|
assert_eq!(payload["event_params"]["parent_thread_id"], json!(null));
|
||||||
|
assert_eq!(
|
||||||
|
payload["event_params"]["forked_from_thread_id"],
|
||||||
|
json!(null)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn subagent_thread_started_thread_spawn_serializes_parent_thread_id() {
|
fn subagent_thread_started_thread_spawn_serializes_thread_lineage() {
|
||||||
let parent_thread_id =
|
let parent_thread_id =
|
||||||
codex_protocol::ThreadId::from_string("11111111-1111-1111-1111-111111111111")
|
codex_protocol::ThreadId::from_string("11111111-1111-1111-1111-111111111111")
|
||||||
.expect("valid thread id");
|
.expect("valid thread id");
|
||||||
|
let forked_from_thread_id =
|
||||||
|
codex_protocol::ThreadId::from_string("22222222-2222-4222-8222-222222222222")
|
||||||
|
.expect("valid thread id");
|
||||||
let event = TrackEventRequest::ThreadInitialized(subagent_thread_started_event_request(
|
let event = TrackEventRequest::ThreadInitialized(subagent_thread_started_event_request(
|
||||||
SubAgentThreadStartedInput {
|
SubAgentThreadStartedInput {
|
||||||
session_id: "session-root".to_string(),
|
session_id: "session-root".to_string(),
|
||||||
thread_id: "thread-spawn".to_string(),
|
thread_id: "thread-spawn".to_string(),
|
||||||
parent_thread_id: Some(parent_thread_id.to_string()),
|
parent_thread_id: Some(parent_thread_id.to_string()),
|
||||||
|
forked_from_thread_id: Some(forked_from_thread_id.to_string()),
|
||||||
product_client_id: "codex-tui".to_string(),
|
product_client_id: "codex-tui".to_string(),
|
||||||
client_name: "codex-tui".to_string(),
|
client_name: "codex-tui".to_string(),
|
||||||
client_version: "1.0.0".to_string(),
|
client_version: "1.0.0".to_string(),
|
||||||
@@ -2497,6 +2508,10 @@ fn subagent_thread_started_thread_spawn_serializes_parent_thread_id() {
|
|||||||
payload["event_params"]["parent_thread_id"],
|
payload["event_params"]["parent_thread_id"],
|
||||||
"11111111-1111-1111-1111-111111111111"
|
"11111111-1111-1111-1111-111111111111"
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
payload["event_params"]["forked_from_thread_id"],
|
||||||
|
"22222222-2222-4222-8222-222222222222"
|
||||||
|
);
|
||||||
assert_eq!(payload["event_params"]["session_id"], "session-root");
|
assert_eq!(payload["event_params"]["session_id"], "session-root");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2507,6 +2522,7 @@ fn subagent_thread_started_memory_consolidation_serializes_expected_shape() {
|
|||||||
session_id: "session-root".to_string(),
|
session_id: "session-root".to_string(),
|
||||||
thread_id: "thread-memory".to_string(),
|
thread_id: "thread-memory".to_string(),
|
||||||
parent_thread_id: None,
|
parent_thread_id: None,
|
||||||
|
forked_from_thread_id: None,
|
||||||
product_client_id: "codex-tui".to_string(),
|
product_client_id: "codex-tui".to_string(),
|
||||||
client_name: "codex-tui".to_string(),
|
client_name: "codex-tui".to_string(),
|
||||||
client_version: "1.0.0".to_string(),
|
client_version: "1.0.0".to_string(),
|
||||||
@@ -2533,6 +2549,7 @@ fn subagent_thread_started_other_serializes_expected_shape() {
|
|||||||
session_id: "session-root".to_string(),
|
session_id: "session-root".to_string(),
|
||||||
thread_id: "thread-guardian".to_string(),
|
thread_id: "thread-guardian".to_string(),
|
||||||
parent_thread_id: None,
|
parent_thread_id: None,
|
||||||
|
forked_from_thread_id: None,
|
||||||
product_client_id: "codex-tui".to_string(),
|
product_client_id: "codex-tui".to_string(),
|
||||||
client_name: "codex-tui".to_string(),
|
client_name: "codex-tui".to_string(),
|
||||||
client_version: "1.0.0".to_string(),
|
client_version: "1.0.0".to_string(),
|
||||||
@@ -2558,6 +2575,7 @@ fn subagent_thread_started_other_serializes_explicit_parent_thread_id() {
|
|||||||
session_id: "session-root".to_string(),
|
session_id: "session-root".to_string(),
|
||||||
thread_id: "thread-guardian".to_string(),
|
thread_id: "thread-guardian".to_string(),
|
||||||
parent_thread_id: Some(parent_thread_id.to_string()),
|
parent_thread_id: Some(parent_thread_id.to_string()),
|
||||||
|
forked_from_thread_id: None,
|
||||||
product_client_id: "codex-tui".to_string(),
|
product_client_id: "codex-tui".to_string(),
|
||||||
client_name: "codex-tui".to_string(),
|
client_name: "codex-tui".to_string(),
|
||||||
client_version: "1.0.0".to_string(),
|
client_version: "1.0.0".to_string(),
|
||||||
@@ -2588,6 +2606,7 @@ async fn subagent_thread_started_publishes_without_initialize() {
|
|||||||
session_id: "session-root".to_string(),
|
session_id: "session-root".to_string(),
|
||||||
thread_id: "thread-review".to_string(),
|
thread_id: "thread-review".to_string(),
|
||||||
parent_thread_id: None,
|
parent_thread_id: None,
|
||||||
|
forked_from_thread_id: None,
|
||||||
product_client_id: "codex-tui".to_string(),
|
product_client_id: "codex-tui".to_string(),
|
||||||
client_name: "codex-tui".to_string(),
|
client_name: "codex-tui".to_string(),
|
||||||
client_version: "1.0.0".to_string(),
|
client_version: "1.0.0".to_string(),
|
||||||
@@ -2662,6 +2681,7 @@ async fn subagent_thread_started_inherits_parent_connection_for_new_thread() {
|
|||||||
session_id: "session-root".to_string(),
|
session_id: "session-root".to_string(),
|
||||||
thread_id: "thread-review".to_string(),
|
thread_id: "thread-review".to_string(),
|
||||||
parent_thread_id: Some(parent_thread_id.to_string()),
|
parent_thread_id: Some(parent_thread_id.to_string()),
|
||||||
|
forked_from_thread_id: None,
|
||||||
product_client_id: "parent-client".to_string(),
|
product_client_id: "parent-client".to_string(),
|
||||||
client_name: "parent-client".to_string(),
|
client_name: "parent-client".to_string(),
|
||||||
client_version: "1.0.0".to_string(),
|
client_version: "1.0.0".to_string(),
|
||||||
@@ -2732,6 +2752,7 @@ async fn subagent_tool_items_inherit_parent_connection_metadata() {
|
|||||||
session_id: "session-root".to_string(),
|
session_id: "session-root".to_string(),
|
||||||
thread_id: "thread-subagent".to_string(),
|
thread_id: "thread-subagent".to_string(),
|
||||||
parent_thread_id: Some("thread-1".to_string()),
|
parent_thread_id: Some("thread-1".to_string()),
|
||||||
|
forked_from_thread_id: None,
|
||||||
product_client_id: "codex-tui".to_string(),
|
product_client_id: "codex-tui".to_string(),
|
||||||
client_name: "codex-tui".to_string(),
|
client_name: "codex-tui".to_string(),
|
||||||
client_version: "1.0.0".to_string(),
|
client_version: "1.0.0".to_string(),
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ pub(crate) struct ThreadInitializedEventParams {
|
|||||||
pub(crate) initialization_mode: ThreadInitializationMode,
|
pub(crate) initialization_mode: ThreadInitializationMode,
|
||||||
pub(crate) subagent_source: Option<String>,
|
pub(crate) subagent_source: Option<String>,
|
||||||
pub(crate) parent_thread_id: Option<String>,
|
pub(crate) parent_thread_id: Option<String>,
|
||||||
|
pub(crate) forked_from_thread_id: Option<String>,
|
||||||
pub(crate) created_at: u64,
|
pub(crate) created_at: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1060,6 +1061,7 @@ pub(crate) fn subagent_thread_started_event_request(
|
|||||||
initialization_mode: ThreadInitializationMode::New,
|
initialization_mode: ThreadInitializationMode::New,
|
||||||
subagent_source: Some(subagent_source_name(&input.subagent_source)),
|
subagent_source: Some(subagent_source_name(&input.subagent_source)),
|
||||||
parent_thread_id: input.parent_thread_id,
|
parent_thread_id: input.parent_thread_id,
|
||||||
|
forked_from_thread_id: input.forked_from_thread_id,
|
||||||
created_at: input.created_at,
|
created_at: input.created_at,
|
||||||
};
|
};
|
||||||
ThreadInitializedEvent {
|
ThreadInitializedEvent {
|
||||||
|
|||||||
@@ -348,6 +348,7 @@ pub struct SubAgentThreadStartedInput {
|
|||||||
pub session_id: String,
|
pub session_id: String,
|
||||||
pub thread_id: String,
|
pub thread_id: String,
|
||||||
pub parent_thread_id: Option<String>,
|
pub parent_thread_id: Option<String>,
|
||||||
|
pub forked_from_thread_id: Option<String>,
|
||||||
pub product_client_id: String,
|
pub product_client_id: String,
|
||||||
pub client_name: String,
|
pub client_name: String,
|
||||||
pub client_version: String,
|
pub client_version: String,
|
||||||
|
|||||||
@@ -1268,6 +1268,7 @@ impl AnalyticsReducer {
|
|||||||
let session_id = thread.session_id;
|
let session_id = thread.session_id;
|
||||||
let thread_id = thread.id;
|
let thread_id = thread.id;
|
||||||
let parent_thread_id = thread.parent_thread_id;
|
let parent_thread_id = thread.parent_thread_id;
|
||||||
|
let forked_from_thread_id = thread.forked_from_id;
|
||||||
let Some(connection_state) = self.connections.get(&connection_id) else {
|
let Some(connection_state) = self.connections.get(&connection_id) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -1299,6 +1300,7 @@ impl AnalyticsReducer {
|
|||||||
initialization_mode,
|
initialization_mode,
|
||||||
subagent_source: thread_metadata.subagent_source.clone(),
|
subagent_source: thread_metadata.subagent_source.clone(),
|
||||||
parent_thread_id: thread_metadata.parent_thread_id,
|
parent_thread_id: thread_metadata.parent_thread_id,
|
||||||
|
forked_from_thread_id,
|
||||||
created_at: u64::try_from(thread.created_at).unwrap_or_default(),
|
created_at: u64::try_from(thread.created_at).unwrap_or_default(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -701,6 +701,7 @@ mod thread_processor_behavior_tests {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
session_source: SessionSource::Cli,
|
session_source: SessionSource::Cli,
|
||||||
|
forked_from_thread_id: None,
|
||||||
parent_thread_id: None,
|
parent_thread_id: None,
|
||||||
thread_source: None,
|
thread_source: None,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -490,6 +490,13 @@ async fn thread_fork_tracks_thread_initialized_analytics() -> Result<()> {
|
|||||||
"forked",
|
"forked",
|
||||||
"user",
|
"user",
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
event["event_params"]["forked_from_thread_id"],
|
||||||
|
thread
|
||||||
|
.forked_from_id
|
||||||
|
.as_deref()
|
||||||
|
.expect("forked thread has a source thread")
|
||||||
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ pub struct ThreadConfigSnapshot {
|
|||||||
pub personality: Option<Personality>,
|
pub personality: Option<Personality>,
|
||||||
pub collaboration_mode: CollaborationMode,
|
pub collaboration_mode: CollaborationMode,
|
||||||
pub session_source: SessionSource,
|
pub session_source: SessionSource,
|
||||||
|
pub forked_from_thread_id: Option<ThreadId>,
|
||||||
pub parent_thread_id: Option<ThreadId>,
|
pub parent_thread_id: Option<ThreadId>,
|
||||||
pub thread_source: Option<ThreadSource>,
|
pub thread_source: Option<ThreadSource>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3364,6 +3364,9 @@ pub(crate) fn emit_subagent_session_started(
|
|||||||
session_id: session_id.to_string(),
|
session_id: session_id.to_string(),
|
||||||
thread_id: thread_id.to_string(),
|
thread_id: thread_id.to_string(),
|
||||||
parent_thread_id: parent_thread_id.map(|thread_id| thread_id.to_string()),
|
parent_thread_id: parent_thread_id.map(|thread_id| thread_id.to_string()),
|
||||||
|
forked_from_thread_id: thread_config
|
||||||
|
.forked_from_thread_id
|
||||||
|
.map(|thread_id| thread_id.to_string()),
|
||||||
product_client_id: client_name.clone(),
|
product_client_id: client_name.clone(),
|
||||||
client_name,
|
client_name,
|
||||||
client_version,
|
client_version,
|
||||||
|
|||||||
@@ -185,6 +185,7 @@ impl SessionConfiguration {
|
|||||||
personality: self.personality,
|
personality: self.personality,
|
||||||
collaboration_mode: self.collaboration_mode.clone(),
|
collaboration_mode: self.collaboration_mode.clone(),
|
||||||
session_source: self.session_source.clone(),
|
session_source: self.session_source.clone(),
|
||||||
|
forked_from_thread_id: self.forked_from_thread_id,
|
||||||
parent_thread_id: self.parent_thread_id,
|
parent_thread_id: self.parent_thread_id,
|
||||||
thread_source: self.thread_source,
|
thread_source: self.thread_source,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3831,6 +3831,85 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn emit_subagent_session_started_includes_fork_lineage_from_session_configuration() {
|
||||||
|
use wiremock::Mock;
|
||||||
|
use wiremock::MockServer;
|
||||||
|
use wiremock::ResponseTemplate;
|
||||||
|
use wiremock::matchers::method;
|
||||||
|
use wiremock::matchers::path;
|
||||||
|
|
||||||
|
let server = MockServer::start().await;
|
||||||
|
Mock::given(method("POST"))
|
||||||
|
.and(path("/codex/analytics-events/events"))
|
||||||
|
.respond_with(ResponseTemplate::new(200))
|
||||||
|
.mount(&server)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let auth_manager =
|
||||||
|
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
|
||||||
|
let analytics_events_client = AnalyticsEventsClient::new(
|
||||||
|
auth_manager,
|
||||||
|
server.uri(),
|
||||||
|
/*analytics_enabled*/ Some(true),
|
||||||
|
);
|
||||||
|
|
||||||
|
let parent_thread_id = ThreadId::new();
|
||||||
|
let forked_from_thread_id = ThreadId::new();
|
||||||
|
let child_thread_id = ThreadId::new();
|
||||||
|
let mut session_configuration = make_session_configuration_for_tests().await;
|
||||||
|
session_configuration.forked_from_thread_id = Some(forked_from_thread_id);
|
||||||
|
|
||||||
|
emit_subagent_session_started(
|
||||||
|
&analytics_events_client,
|
||||||
|
AppServerClientMetadata {
|
||||||
|
client_name: Some("codex-tui".to_string()),
|
||||||
|
client_version: Some("1.0.0".to_string()),
|
||||||
|
},
|
||||||
|
SessionId::from(child_thread_id),
|
||||||
|
child_thread_id,
|
||||||
|
Some(parent_thread_id),
|
||||||
|
session_configuration.thread_config_snapshot(),
|
||||||
|
SubAgentSource::ThreadSpawn {
|
||||||
|
parent_thread_id,
|
||||||
|
depth: 1,
|
||||||
|
agent_path: None,
|
||||||
|
agent_nickname: None,
|
||||||
|
agent_role: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let event = timeout(Duration::from_secs(1), async {
|
||||||
|
'wait_for_event: loop {
|
||||||
|
if let Some(requests) = server.received_requests().await {
|
||||||
|
for request in requests {
|
||||||
|
let payload: serde_json::Value =
|
||||||
|
serde_json::from_slice(&request.body).expect("valid analytics payload");
|
||||||
|
if let Some(event) = payload["events"].as_array().and_then(|events| {
|
||||||
|
events
|
||||||
|
.iter()
|
||||||
|
.find(|event| event["event_type"] == "codex_thread_initialized")
|
||||||
|
}) {
|
||||||
|
break 'wait_for_event event.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("subagent initialization analytics should be emitted");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
event["event_params"]["parent_thread_id"],
|
||||||
|
parent_thread_id.to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
event["event_params"]["forked_from_thread_id"],
|
||||||
|
forked_from_thread_id.to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn turn_environments_for_tests(
|
fn turn_environments_for_tests(
|
||||||
environment: &Arc<codex_exec_server::Environment>,
|
environment: &Arc<codex_exec_server::Environment>,
|
||||||
cwd: &codex_utils_absolute_path::AbsolutePathBuf,
|
cwd: &codex_utils_absolute_path::AbsolutePathBuf,
|
||||||
|
|||||||
Reference in new issue
Block a user