Support thread-level originator overrides (#29477)

## Why

Work(TPP) threads can be launched from the Desktop app, but if they all
keep the Desktop app's default originator then downstream attribution
cannot distinguish local Work launches from cloud-backed Work launches.
`thread/start.serviceName` already carries that launch signal, while
`SessionMeta.originator` is the durable thread-level value that survives
resume and fork.

This change converts the Desktop Work service names into an effective
originator at thread creation time, persists that originator with the
thread, and keeps using it for later model requests and memory writes.

## What changed

- Map `CODEX_WORK_LOCAL` and `CODEX_WORK_CLOUD` service names to
per-thread originators, while preserving
`CODEX_INTERNAL_ORIGINATOR_OVERRIDE` as the highest-precedence override.
- Persist the effective originator in `SessionMeta.originator`, read it
back on resume/fork, and inherit the parent originator for subagent
spawns when there is no persisted session metadata.
- Handle truncated `SpawnAgentForkMode::LastNTurns` forks by falling
back to the live parent originator when the forked history no longer
includes `SessionMeta`.
- Thread the per-thread originator through Responses headers,
websocket/compaction request paths, thread-store creation, rollout
metadata, and memory stage-one telemetry.

## Verification

- `just test -p codex-core
agent::control::tests::spawn_thread_subagent_inherits_parent_originator_without_fork
agent::control::tests::spawn_thread_subagent_fork_last_n_turns_inherits_parent_originator_without_session_meta
thread_manager::tests::originator_override_precedes_service_name_remapping`
- `just test -p codex-core
agent::control::tests::resume_thread_subagent_restores_stored_metadata_and_effective_multi_agent_mode`
- `just test -p codex-memories-write`
- `just fix -p codex-core -p codex-memories-write`
- `git diff --check`
This commit is contained in:
alexsong-oai
2026-06-23 17:23:38 -07:00
committed by GitHub
parent 32b65bbf7a
commit 1acb722e8a
44 changed files with 513 additions and 122 deletions
@@ -168,6 +168,17 @@ use std::sync::Arc;
use std::sync::Mutex;
use tokio::sync::mpsc;
const TEST_PRODUCT_CLIENT_ID: &str = "codex_work_desktop";
fn test_tracking_context(thread_id: &str, turn_id: &str) -> TrackEventsContext {
TrackEventsContext {
model_slug: "gpt-5".to_string(),
thread_id: thread_id.to_string(),
turn_id: turn_id.to_string(),
product_client_id: TEST_PRODUCT_CLIENT_ID.to_string(),
}
}
fn sample_thread_with_metadata(
thread_id: &str,
ephemeral: bool,
@@ -1024,11 +1035,7 @@ fn normalize_path_for_skill_id_repo_root_not_in_skill_path_uses_absolute_path()
#[test]
fn app_mentioned_event_serializes_expected_shape() {
let tracking = TrackEventsContext {
model_slug: "gpt-5".to_string(),
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
};
let tracking = test_tracking_context("thread-1", "turn-1");
let event = TrackEventRequest::AppMentioned(CodexAppMentionedEventRequest {
event_type: "codex_app_mentioned",
event_params: codex_app_metadata(
@@ -1052,7 +1059,7 @@ fn app_mentioned_event_serializes_expected_shape() {
"thread_id": "thread-1",
"turn_id": "turn-1",
"app_name": "Calendar",
"product_client_id": originator().value,
"product_client_id": TEST_PRODUCT_CLIENT_ID,
"invoke_type": "explicit",
"model_slug": "gpt-5"
}
@@ -1062,11 +1069,7 @@ fn app_mentioned_event_serializes_expected_shape() {
#[test]
fn app_used_event_serializes_expected_shape() {
let tracking = TrackEventsContext {
model_slug: "gpt-5".to_string(),
thread_id: "thread-2".to_string(),
turn_id: "turn-2".to_string(),
};
let tracking = test_tracking_context("thread-2", "turn-2");
let event = TrackEventRequest::AppUsed(CodexAppUsedEventRequest {
event_type: "codex_app_used",
event_params: codex_app_metadata(
@@ -1090,7 +1093,7 @@ fn app_used_event_serializes_expected_shape() {
"thread_id": "thread-2",
"turn_id": "turn-2",
"app_name": "Google Drive",
"product_client_id": originator().value,
"product_client_id": TEST_PRODUCT_CLIENT_ID,
"invoke_type": "implicit",
"model_slug": "gpt-5"
}
@@ -1381,16 +1384,8 @@ fn app_used_dedupe_is_keyed_by_turn_and_connector() {
invocation_type: Some(InvocationType::Implicit),
};
let turn_1 = TrackEventsContext {
model_slug: "gpt-5".to_string(),
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
};
let turn_2 = TrackEventsContext {
model_slug: "gpt-5".to_string(),
thread_id: "thread-1".to_string(),
turn_id: "turn-2".to_string(),
};
let turn_1 = test_tracking_context("thread-1", "turn-1");
let turn_2 = test_tracking_context("thread-1", "turn-2");
assert_eq!(queue.should_enqueue_app_used(&turn_1, &app), true);
assert_eq!(queue.should_enqueue_app_used(&turn_1, &app), false);
@@ -3009,11 +3004,7 @@ async fn subagent_tool_items_inherit_parent_connection_metadata() {
#[test]
fn plugin_used_event_serializes_expected_shape() {
let tracking = TrackEventsContext {
model_slug: "gpt-5".to_string(),
thread_id: "thread-3".to_string(),
turn_id: "turn-3".to_string(),
};
let tracking = test_tracking_context("thread-3", "turn-3");
let event = TrackEventRequest::PluginUsed(CodexPluginUsedEventRequest {
event_type: "codex_plugin_used",
event_params: codex_plugin_used_metadata(&tracking, sample_plugin_metadata()),
@@ -3033,7 +3024,7 @@ fn plugin_used_event_serializes_expected_shape() {
"has_skills": true,
"mcp_server_count": 2,
"connector_ids": ["calendar", "drive"],
"product_client_id": originator().value,
"product_client_id": TEST_PRODUCT_CLIENT_ID,
"mcp_server_names": ["mcp-1", "mcp-2"],
"thread_id": "thread-3",
"turn_id": "turn-3",
@@ -3132,11 +3123,7 @@ fn plugin_management_event_keeps_plugin_id_local_when_remote_id_exists() {
#[test]
fn hook_run_event_serializes_expected_shape() {
let tracking = TrackEventsContext {
model_slug: "gpt-5".to_string(),
thread_id: "thread-3".to_string(),
turn_id: "turn-3".to_string(),
};
let tracking = test_tracking_context("thread-3", "turn-3");
let event = TrackEventRequest::HookRun(CodexHookRunEventRequest {
event_type: "codex_hook_run",
event_params: codex_hook_run_metadata(
@@ -3158,6 +3145,7 @@ fn hook_run_event_serializes_expected_shape() {
"event_params": {
"thread_id": "thread-3",
"turn_id": "turn-3",
"product_client_id": TEST_PRODUCT_CLIENT_ID,
"model_slug": "gpt-5",
"hook_name": "PreToolUse",
"hook_source": "user",
@@ -3169,11 +3157,7 @@ fn hook_run_event_serializes_expected_shape() {
#[test]
fn hook_run_metadata_maps_sources_and_statuses() {
let tracking = TrackEventsContext {
model_slug: "gpt-5".to_string(),
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
};
let tracking = test_tracking_context("thread-1", "turn-1");
let system = serde_json::to_value(codex_hook_run_metadata(
&tracking,
@@ -3224,11 +3208,7 @@ fn hook_run_metadata_maps_sources_and_statuses() {
#[test]
fn hook_run_metadata_maps_stopped_status() {
let tracking = TrackEventsContext {
model_slug: "gpt-5".to_string(),
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
};
let tracking = test_tracking_context("thread-1", "turn-1");
let stopped = serde_json::to_value(codex_hook_run_metadata(
&tracking,
@@ -3254,16 +3234,8 @@ fn plugin_used_dedupe_is_keyed_by_turn_and_plugin() {
};
let plugin = sample_plugin_metadata();
let turn_1 = TrackEventsContext {
model_slug: "gpt-5".to_string(),
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
};
let turn_2 = TrackEventsContext {
model_slug: "gpt-5".to_string(),
thread_id: "thread-1".to_string(),
turn_id: "turn-2".to_string(),
};
let turn_1 = test_tracking_context("thread-1", "turn-1");
let turn_2 = test_tracking_context("thread-1", "turn-2");
assert_eq!(queue.should_enqueue_plugin_used(&turn_1, &plugin), true);
assert_eq!(queue.should_enqueue_plugin_used(&turn_1, &plugin), false);
@@ -3274,11 +3246,7 @@ fn plugin_used_dedupe_is_keyed_by_turn_and_plugin() {
async fn reducer_ingests_skill_invoked_fact() {
let mut reducer = AnalyticsReducer::default();
let mut events = Vec::new();
let tracking = TrackEventsContext {
model_slug: "gpt-5".to_string(),
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
};
let tracking = test_tracking_context("thread-1", "turn-1");
let skill_path = PathBuf::from("/Users/abc/.codex/skills/doc/SKILL.md");
let expected_skill_id = skill_id_for_local_skill(
/*repo_url*/ None,
@@ -3311,7 +3279,7 @@ async fn reducer_ingests_skill_invoked_fact() {
"skill_id": expected_skill_id,
"skill_name": "doc",
"event_params": {
"product_client_id": originator().value,
"product_client_id": TEST_PRODUCT_CLIENT_ID,
"skill_scope": "user",
"plugin_id": null,
"repo_url": null,
@@ -3328,11 +3296,7 @@ async fn reducer_ingests_skill_invoked_fact() {
async fn reducer_includes_plugin_id_for_plugin_skill_invocations() {
let mut reducer = AnalyticsReducer::default();
let mut events = Vec::new();
let tracking = TrackEventsContext {
model_slug: "gpt-5".to_string(),
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
};
let tracking = test_tracking_context("thread-1", "turn-1");
let skill_path =
PathBuf::from("/Users/abc/.codex/plugins/cache/test/sample/skills/doc/SKILL.md");
@@ -3367,11 +3331,7 @@ async fn reducer_ingests_hook_run_fact() {
reducer
.ingest(
AnalyticsFact::Custom(CustomAnalyticsFact::HookRun(HookRunInput {
tracking: TrackEventsContext {
model_slug: "gpt-5".to_string(),
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
},
tracking: test_tracking_context("thread-1", "turn-1"),
hook: HookRunFact {
event_name: HookEventName::PostToolUse,
hook_source: HookSource::Unknown,
@@ -3394,11 +3354,7 @@ async fn reducer_ingests_hook_run_fact() {
async fn reducer_ingests_app_and_plugin_facts() {
let mut reducer = AnalyticsReducer::default();
let mut events = Vec::new();
let tracking = TrackEventsContext {
model_slug: "gpt-5".to_string(),
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
};
let tracking = test_tracking_context("thread-1", "turn-1");
reducer
.ingest(
@@ -3441,6 +3397,18 @@ async fn reducer_ingests_app_and_plugin_facts() {
assert_eq!(payload[0]["event_type"], "codex_app_mentioned");
assert_eq!(payload[1]["event_type"], "codex_app_used");
assert_eq!(payload[2]["event_type"], "codex_plugin_used");
assert_eq!(
payload[0]["event_params"]["product_client_id"],
TEST_PRODUCT_CLIENT_ID
);
assert_eq!(
payload[1]["event_params"]["product_client_id"],
TEST_PRODUCT_CLIENT_ID
);
assert_eq!(
payload[2]["event_params"]["product_client_id"],
TEST_PRODUCT_CLIENT_ID
);
}
#[tokio::test]
+15 -3
View File
@@ -773,6 +773,7 @@ pub(crate) struct CodexAppUsedEventRequest {
pub(crate) struct CodexHookRunMetadata {
pub(crate) thread_id: Option<String>,
pub(crate) turn_id: Option<String>,
pub(crate) product_client_id: Option<String>,
pub(crate) model_slug: Option<String>,
pub(crate) hook_name: Option<String>,
pub(crate) hook_source: Option<&'static str>,
@@ -1030,13 +1031,20 @@ pub(crate) fn codex_app_metadata(
thread_id: Some(tracking.thread_id.clone()),
turn_id: Some(tracking.turn_id.clone()),
app_name: app.app_name,
product_client_id: Some(originator().value),
product_client_id: Some(tracking.product_client_id.clone()),
invoke_type: app.invocation_type,
model_slug: Some(tracking.model_slug.clone()),
}
}
pub(crate) fn codex_plugin_metadata(plugin: PluginTelemetryMetadata) -> CodexPluginMetadata {
codex_plugin_metadata_with_product_client_id(plugin, originator().value)
}
fn codex_plugin_metadata_with_product_client_id(
plugin: PluginTelemetryMetadata,
product_client_id: String,
) -> CodexPluginMetadata {
let PluginTelemetryMetadata {
plugin_id,
remote_plugin_id,
@@ -1062,7 +1070,7 @@ pub(crate) fn codex_plugin_metadata(plugin: PluginTelemetryMetadata) -> CodexPlu
.map(|connector_id| connector_id.0)
.collect()
}),
product_client_id: Some(originator().value),
product_client_id: Some(product_client_id),
}
}
@@ -1139,7 +1147,10 @@ pub(crate) fn codex_plugin_used_metadata(
.as_ref()
.map(|summary| summary.mcp_server_names.clone());
CodexPluginUsedMetadata {
plugin: codex_plugin_metadata(plugin),
plugin: codex_plugin_metadata_with_product_client_id(
plugin,
tracking.product_client_id.clone(),
),
mcp_server_names,
thread_id: Some(tracking.thread_id.clone()),
turn_id: Some(tracking.turn_id.clone()),
@@ -1154,6 +1165,7 @@ pub(crate) fn codex_hook_run_metadata(
CodexHookRunMetadata {
thread_id: Some(tracking.thread_id.clone()),
turn_id: Some(tracking.turn_id.clone()),
product_client_id: Some(tracking.product_client_id.clone()),
model_slug: Some(tracking.model_slug.clone()),
hook_name: Some(analytics_hook_event_name(hook.event_name).to_owned()),
hook_source: Some(analytics_hook_source(hook.hook_source)),
+3
View File
@@ -41,17 +41,20 @@ pub struct TrackEventsContext {
pub model_slug: String,
pub thread_id: String,
pub turn_id: String,
pub product_client_id: String,
}
pub fn build_track_events_context(
model_slug: String,
thread_id: String,
turn_id: String,
product_client_id: String,
) -> TrackEventsContext {
TrackEventsContext {
model_slug,
thread_id,
turn_id,
product_client_id,
}
}
+1 -1
View File
@@ -729,7 +729,7 @@ impl AnalyticsReducer {
turn_id: Some(tracking.turn_id.clone()),
invoke_type: Some(invocation.invocation_type),
model_slug: Some(tracking.model_slug.clone()),
product_client_id: Some(originator().value),
product_client_id: Some(tracking.product_client_id.clone()),
repo_url,
skill_scope: Some(skill_scope.to_string()),
plugin_id: invocation.plugin_id,