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
+4 -2
View File
@@ -73,7 +73,6 @@ use codex_hooks::HooksConfig;
use codex_login::AuthManager;
use codex_login::CodexAuth;
use codex_login::auth_env_telemetry::collect_auth_env_telemetry;
use codex_login::default_client::originator;
use codex_mcp::McpConnectionManager;
use codex_mcp::McpResourceClient;
use codex_mcp::McpRuntimeContext;
@@ -433,6 +432,7 @@ pub(crate) struct CodexSpawnArgs {
pub(crate) forked_from_thread_id: Option<ThreadId>,
pub(crate) parent_thread_id: Option<ThreadId>,
pub(crate) thread_source: Option<ThreadSource>,
pub(crate) originator: String,
pub(crate) agent_control: AgentControl,
pub(crate) dynamic_tools: Vec<DynamicToolSpec>,
pub(crate) metrics_service_name: Option<String>,
@@ -522,6 +522,7 @@ impl Codex {
forked_from_thread_id,
parent_thread_id,
thread_source,
originator,
agent_control,
dynamic_tools,
metrics_service_name,
@@ -654,6 +655,7 @@ impl Codex {
forked_from_thread_id,
parent_thread_id,
thread_source,
originator,
dynamic_tools,
user_shell_override,
};
@@ -3913,7 +3915,7 @@ pub(crate) fn emit_subagent_session_started(
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: thread_config.originator.clone(),
client_name,
client_version,
model: thread_config.model,
+1
View File
@@ -118,6 +118,7 @@ pub(super) async fn spawn_review_thread(
reasoning_summary,
session_source,
parent_thread_id: parent_turn_context.parent_thread_id,
originator: parent_turn_context.originator.clone(),
environments: parent_turn_context.environments.clone(),
available_models,
unified_exec_shell_mode,
+11 -1
View File
@@ -106,6 +106,8 @@ pub(crate) struct SessionConfiguration {
pub(super) parent_thread_id: Option<ThreadId>,
/// Optional analytics source classification for this thread.
pub(super) thread_source: Option<ThreadSource>,
/// Effective originator used for this thread's Responses requests and analytics events.
pub(super) originator: String,
pub(super) dynamic_tools: Vec<DynamicToolSpec>,
pub(super) user_shell_override: Option<shell::Shell>,
}
@@ -198,6 +200,7 @@ impl SessionConfiguration {
forked_from_thread_id: self.forked_from_thread_id,
parent_thread_id: self.parent_thread_id,
thread_source: self.thread_source.clone(),
originator: self.originator.clone(),
}
}
@@ -473,6 +476,11 @@ impl Session {
self.services.agent_control.session_id()
}
pub(crate) async fn originator(&self) -> String {
let state = self.state.lock().await;
state.session_configuration.originator.clone()
}
#[instrument(name = "session_init", level = "info", skip_all)]
#[allow(clippy::too_many_arguments)]
pub(crate) async fn new(
@@ -581,6 +589,7 @@ impl Session {
parent_thread_id,
source: session_source,
thread_source: session_configuration.thread_source.clone(),
originator: session_configuration.originator.clone(),
base_instructions: BaseInstructions {
text: session_configuration.base_instructions.clone(),
},
@@ -756,7 +765,7 @@ impl Session {
let auth_mode = auth.map(CodexAuth::auth_mode).map(TelemetryAuthMode::from);
let account_id = auth.and_then(CodexAuth::get_account_id);
let account_email = auth.and_then(CodexAuth::get_account_email);
let originator = originator().value;
let originator = session_configuration.originator.clone();
let terminal_type = user_agent();
let session_model = session_configuration.collaboration_mode.model().to_string();
let auth_env_telemetry = collect_auth_env_telemetry(
@@ -1057,6 +1066,7 @@ impl Session {
thread_id,
session_configuration.provider.clone(),
session_configuration.session_source.clone(),
session_configuration.originator.clone(),
config.model_verbosity,
config.features.enabled(Feature::EnableRequestCompression),
config.features.enabled(Feature::RuntimeMetrics),
+18 -1
View File
@@ -479,6 +479,7 @@ fn test_model_client_session() -> crate::client::ModelClientSession {
thread_id,
ModelProviderInfo::create_openai_provider(/* base_url */ /*base_url*/ None),
codex_protocol::protocol::SessionSource::Exec,
"test_originator".to_string(),
/*model_verbosity*/ None,
/*enable_request_compression*/ false,
/*include_timing_metrics*/ false,
@@ -3485,6 +3486,7 @@ async fn set_rate_limits_retains_previous_credits() {
forked_from_thread_id: None,
parent_thread_id: None,
thread_source: None,
originator: "test_originator".to_string(),
dynamic_tools: Vec::new(),
user_shell_override: None,
};
@@ -3592,6 +3594,7 @@ async fn set_rate_limits_updates_plan_type_when_present() {
forked_from_thread_id: None,
parent_thread_id: None,
thread_source: None,
originator: "test_originator".to_string(),
dynamic_tools: Vec::new(),
user_shell_override: None,
};
@@ -3841,6 +3844,7 @@ async fn attach_thread_persistence(session: &mut Session) -> PathBuf {
parent_thread_id: None,
source: SessionSource::Exec,
thread_source: None,
originator: "test_originator".to_string(),
base_instructions: BaseInstructions::default(),
dynamic_tools: Vec::new(),
multi_agent_version: None,
@@ -4122,13 +4126,14 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
forked_from_thread_id: None,
parent_thread_id: None,
thread_source: None,
originator: "test_originator".to_string(),
dynamic_tools: Vec::new(),
user_shell_override: None,
}
}
#[tokio::test]
async fn emit_subagent_session_started_includes_fork_lineage_from_session_configuration() {
async fn emit_subagent_session_started_includes_fork_lineage_and_originator() {
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
@@ -4204,6 +4209,10 @@ async fn emit_subagent_session_started_includes_fork_lineage_from_session_config
event["event_params"]["forked_from_thread_id"],
forked_from_thread_id.to_string()
);
assert_eq!(
event["event_params"]["app_server_client"]["product_client_id"],
"test_originator"
);
}
async fn resolved_environments_for_configuration(
@@ -4989,6 +4998,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
forked_from_thread_id: None,
parent_thread_id: None,
thread_source: None,
originator: "test_originator".to_string(),
dynamic_tools: Vec::new(),
user_shell_override: None,
};
@@ -5118,6 +5128,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
forked_from_thread_id: None,
parent_thread_id: None,
thread_source: None,
originator: "test_originator".to_string(),
dynamic_tools: Vec::new(),
user_shell_override: None,
};
@@ -5220,6 +5231,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
thread_id,
session_configuration.provider.clone(),
session_configuration.session_source.clone(),
session_configuration.originator.clone(),
config.model_verbosity,
config.features.enabled(Feature::EnableRequestCompression),
config.features.enabled(Feature::RuntimeMetrics),
@@ -5364,6 +5376,7 @@ async fn make_session_with_config_and_rx(
forked_from_thread_id: None,
parent_thread_id: None,
thread_source: None,
originator: "test_originator".to_string(),
dynamic_tools: Vec::new(),
user_shell_override: None,
};
@@ -5471,6 +5484,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
forked_from_thread_id: None,
parent_thread_id: None,
thread_source: None,
originator: "test_originator".to_string(),
dynamic_tools: Vec::new(),
user_shell_override: None,
};
@@ -6713,6 +6727,7 @@ async fn shutdown_complete_does_not_append_to_thread_store_after_shutdown() {
parent_thread_id: None,
source: SessionSource::Exec,
thread_source: None,
originator: "test_originator".to_string(),
base_instructions: BaseInstructions::default(),
dynamic_tools: Vec::new(),
multi_agent_version: None,
@@ -7194,6 +7209,7 @@ where
forked_from_thread_id: None,
parent_thread_id: None,
thread_source: None,
originator: "test_originator".to_string(),
dynamic_tools,
user_shell_override: None,
};
@@ -7295,6 +7311,7 @@ where
thread_id,
session_configuration.provider.clone(),
session_configuration.session_source.clone(),
session_configuration.originator.clone(),
config.model_verbosity,
config.features.enabled(Feature::EnableRequestCompression),
config.features.enabled(Feature::RuntimeMetrics),
@@ -733,6 +733,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
forked_from_thread_id: None,
parent_thread_id: None,
thread_source: None,
originator: "test_originator".to_string(),
agent_control: AgentControl::default(),
dynamic_tools: Vec::new(),
metrics_service_name: None,
+1
View File
@@ -526,6 +526,7 @@ async fn build_skills_and_plugins(
turn_context.model_info.slug.clone(),
sess.thread_id.to_string(),
turn_context.sub_id.clone(),
turn_context.originator.clone(),
);
let loaded_plugins = sess
.services
@@ -113,6 +113,7 @@ pub struct TurnContext {
pub(crate) reasoning_summary: ReasoningSummaryConfig,
pub(crate) session_source: SessionSource,
pub(crate) parent_thread_id: Option<ThreadId>,
pub(crate) originator: String,
pub(crate) environments: TurnEnvironmentSnapshot,
/// The session's absolute working directory. All relative paths provided
/// by the model as well as sandbox policies are resolved against this path
@@ -265,6 +266,7 @@ impl TurnContext {
reasoning_summary: self.reasoning_summary,
session_source: self.session_source.clone(),
parent_thread_id: self.parent_thread_id,
originator: self.originator.clone(),
environments: self.environments.clone(),
#[allow(deprecated)]
cwd: self.cwd.clone(),
@@ -548,6 +550,7 @@ impl Session {
reasoning_summary,
session_source,
parent_thread_id: session_configuration.parent_thread_id,
originator: session_configuration.originator.clone(),
environments,
#[allow(deprecated)]
cwd,