mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08: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:
committed by
GitHub
Unverified
parent
c8fdc74b42
commit
9e41f8ddbe
@@ -1347,6 +1347,7 @@ fn thread_initialized_event_serializes_expected_shape() {
|
||||
initialization_mode: ThreadInitializationMode::New,
|
||||
subagent_source: None,
|
||||
parent_thread_id: None,
|
||||
forked_from_thread_id: None,
|
||||
created_at: 1,
|
||||
},
|
||||
});
|
||||
@@ -1379,6 +1380,7 @@ fn thread_initialized_event_serializes_expected_shape() {
|
||||
"initialization_mode": "new",
|
||||
"subagent_source": null,
|
||||
"parent_thread_id": null,
|
||||
"forked_from_thread_id": null,
|
||||
"created_at": 1
|
||||
}
|
||||
})
|
||||
@@ -2429,6 +2431,7 @@ fn subagent_thread_started_review_serializes_expected_shape() {
|
||||
session_id: "session-root".to_string(),
|
||||
thread_id: "thread-review".to_string(),
|
||||
parent_thread_id: None,
|
||||
forked_from_thread_id: None,
|
||||
product_client_id: "codex-tui".to_string(),
|
||||
client_name: "codex-tui".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"]["subagent_source"], "review");
|
||||
assert_eq!(payload["event_params"]["parent_thread_id"], json!(null));
|
||||
assert_eq!(
|
||||
payload["event_params"]["forked_from_thread_id"],
|
||||
json!(null)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subagent_thread_started_thread_spawn_serializes_parent_thread_id() {
|
||||
fn subagent_thread_started_thread_spawn_serializes_thread_lineage() {
|
||||
let parent_thread_id =
|
||||
codex_protocol::ThreadId::from_string("11111111-1111-1111-1111-111111111111")
|
||||
.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(
|
||||
SubAgentThreadStartedInput {
|
||||
session_id: "session-root".to_string(),
|
||||
thread_id: "thread-spawn".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(),
|
||||
client_name: "codex-tui".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"],
|
||||
"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");
|
||||
}
|
||||
|
||||
@@ -2507,6 +2522,7 @@ fn subagent_thread_started_memory_consolidation_serializes_expected_shape() {
|
||||
session_id: "session-root".to_string(),
|
||||
thread_id: "thread-memory".to_string(),
|
||||
parent_thread_id: None,
|
||||
forked_from_thread_id: None,
|
||||
product_client_id: "codex-tui".to_string(),
|
||||
client_name: "codex-tui".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(),
|
||||
thread_id: "thread-guardian".to_string(),
|
||||
parent_thread_id: None,
|
||||
forked_from_thread_id: None,
|
||||
product_client_id: "codex-tui".to_string(),
|
||||
client_name: "codex-tui".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(),
|
||||
thread_id: "thread-guardian".to_string(),
|
||||
parent_thread_id: Some(parent_thread_id.to_string()),
|
||||
forked_from_thread_id: None,
|
||||
product_client_id: "codex-tui".to_string(),
|
||||
client_name: "codex-tui".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(),
|
||||
thread_id: "thread-review".to_string(),
|
||||
parent_thread_id: None,
|
||||
forked_from_thread_id: None,
|
||||
product_client_id: "codex-tui".to_string(),
|
||||
client_name: "codex-tui".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(),
|
||||
thread_id: "thread-review".to_string(),
|
||||
parent_thread_id: Some(parent_thread_id.to_string()),
|
||||
forked_from_thread_id: None,
|
||||
product_client_id: "parent-client".to_string(),
|
||||
client_name: "parent-client".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(),
|
||||
thread_id: "thread-subagent".to_string(),
|
||||
parent_thread_id: Some("thread-1".to_string()),
|
||||
forked_from_thread_id: None,
|
||||
product_client_id: "codex-tui".to_string(),
|
||||
client_name: "codex-tui".to_string(),
|
||||
client_version: "1.0.0".to_string(),
|
||||
|
||||
@@ -157,6 +157,7 @@ pub(crate) struct ThreadInitializedEventParams {
|
||||
pub(crate) initialization_mode: ThreadInitializationMode,
|
||||
pub(crate) subagent_source: Option<String>,
|
||||
pub(crate) parent_thread_id: Option<String>,
|
||||
pub(crate) forked_from_thread_id: Option<String>,
|
||||
pub(crate) created_at: u64,
|
||||
}
|
||||
|
||||
@@ -1060,6 +1061,7 @@ pub(crate) fn subagent_thread_started_event_request(
|
||||
initialization_mode: ThreadInitializationMode::New,
|
||||
subagent_source: Some(subagent_source_name(&input.subagent_source)),
|
||||
parent_thread_id: input.parent_thread_id,
|
||||
forked_from_thread_id: input.forked_from_thread_id,
|
||||
created_at: input.created_at,
|
||||
};
|
||||
ThreadInitializedEvent {
|
||||
|
||||
@@ -348,6 +348,7 @@ pub struct SubAgentThreadStartedInput {
|
||||
pub session_id: String,
|
||||
pub thread_id: String,
|
||||
pub parent_thread_id: Option<String>,
|
||||
pub forked_from_thread_id: Option<String>,
|
||||
pub product_client_id: String,
|
||||
pub client_name: String,
|
||||
pub client_version: String,
|
||||
|
||||
@@ -1268,6 +1268,7 @@ impl AnalyticsReducer {
|
||||
let session_id = thread.session_id;
|
||||
let thread_id = 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 {
|
||||
return;
|
||||
};
|
||||
@@ -1299,6 +1300,7 @@ impl AnalyticsReducer {
|
||||
initialization_mode,
|
||||
subagent_source: thread_metadata.subagent_source.clone(),
|
||||
parent_thread_id: thread_metadata.parent_thread_id,
|
||||
forked_from_thread_id,
|
||||
created_at: u64::try_from(thread.created_at).unwrap_or_default(),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -701,6 +701,7 @@ mod thread_processor_behavior_tests {
|
||||
},
|
||||
},
|
||||
session_source: SessionSource::Cli,
|
||||
forked_from_thread_id: None,
|
||||
parent_thread_id: None,
|
||||
thread_source: None,
|
||||
};
|
||||
|
||||
@@ -490,6 +490,13 @@ async fn thread_fork_tracks_thread_initialized_analytics() -> Result<()> {
|
||||
"forked",
|
||||
"user",
|
||||
);
|
||||
assert_eq!(
|
||||
event["event_params"]["forked_from_thread_id"],
|
||||
thread
|
||||
.forked_from_id
|
||||
.as_deref()
|
||||
.expect("forked thread has a source thread")
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ pub struct ThreadConfigSnapshot {
|
||||
pub personality: Option<Personality>,
|
||||
pub collaboration_mode: CollaborationMode,
|
||||
pub session_source: SessionSource,
|
||||
pub forked_from_thread_id: Option<ThreadId>,
|
||||
pub parent_thread_id: Option<ThreadId>,
|
||||
pub thread_source: Option<ThreadSource>,
|
||||
}
|
||||
|
||||
@@ -3364,6 +3364,9 @@ pub(crate) fn emit_subagent_session_started(
|
||||
session_id: session_id.to_string(),
|
||||
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(),
|
||||
client_name,
|
||||
client_version,
|
||||
|
||||
@@ -185,6 +185,7 @@ impl SessionConfiguration {
|
||||
personality: self.personality,
|
||||
collaboration_mode: self.collaboration_mode.clone(),
|
||||
session_source: self.session_source.clone(),
|
||||
forked_from_thread_id: self.forked_from_thread_id,
|
||||
parent_thread_id: self.parent_thread_id,
|
||||
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(
|
||||
environment: &Arc<codex_exec_server::Environment>,
|
||||
cwd: &codex_utils_absolute_path::AbsolutePathBuf,
|
||||
|
||||
Reference in New Issue
Block a user