store and expose parent_thread_id on Threads (#25113)

## Why

This PR
https://github.com/openai/codex/pull/24161#discussion_r3325692763
revealed a subagent data modeling issue, where we overloaded
`forked_from_id` to also mean `parent_thread_id`. That's incorrect since
guardian and review subagents can be a subagent and NOT fork the main
thread's history.

The solution here is to explicitly store a new `parent_thread_id` on
`SessionMeta`, alongside `forked_from_id` which already exists. While
we're at it, also expose it in the app-server protocol on the `Thread`
object.

A thread->subagent relationship and a fork of thread history are
orthogonal concepts.

## What Changed

- Added top-level `parent_thread_id` persistence on `SessionMeta` and
runtime/session plumbing through `SessionConfiguredEvent`,
`CodexSpawnArgs`, `SessionConfiguration`, `ThreadConfigSnapshot`,
`TurnContext`, and `ModelClient`.
- Made turn metadata, request headers, analytics, and subagent-start
events read the separate runtime/top-level parent field instead of
deriving general parent lineage from `SessionSource` or
`forked_from_thread_id`.
- Passed parent lineage separately at delegated subagent, review,
guardian, agent-job, and multi-agent spawn construction sites;
copied-history fork lineage remains derived only from `InitialHistory`.
- Persisted and exposed parent lineage through rollout/thread-store
projections and app-server v2 `Thread.parentThreadId`.
- Updated app-server README text and regenerated app-server schema
fixtures for the additive `parentThreadId` response field.
This commit is contained in:
Owen Lin
2026-05-31 21:33:20 -07:00
committed by GitHub
Unverified
parent 3b7334d099
commit cf0911076f
92 changed files with 504 additions and 111 deletions
+1 -1
View File
@@ -32,5 +32,5 @@ fn register_session_root(session: &Arc<Session>, turn: &Arc<TurnContext>) {
session
.services
.agent_control
.register_session_root(session.conversation_id, &turn.session_source);
.register_session_root(session.conversation_id, turn.parent_thread_id);
}
+12 -10
View File
@@ -54,6 +54,7 @@ pub(crate) enum SpawnAgentForkMode {
pub(crate) struct SpawnAgentOptions {
pub(crate) fork_parent_spawn_call_id: Option<String>,
pub(crate) fork_mode: Option<SpawnAgentForkMode>,
pub(crate) parent_thread_id: Option<ThreadId>,
pub(crate) environments: Option<Vec<TurnEnvironmentSelection>>,
}
@@ -262,12 +263,12 @@ impl AgentControl {
.await?
}
(Some(session_source), None) => {
let forked_from_thread_id = thread_spawn_parent_thread_id(&session_source);
Box::pin(state.spawn_new_thread_with_source(
config.clone(),
self.clone(),
session_source,
forked_from_thread_id,
options.parent_thread_id,
/*forked_from_thread_id*/ None,
/*thread_source*/ Some(ThreadSource::Subagent),
/*persist_extended_history*/ false,
/*metrics_service_name*/ None,
@@ -309,6 +310,7 @@ impl AgentControl {
}
};
let thread_config = new_thread.thread.codex.thread_config_snapshot().await;
let parent_thread_id = thread_config.parent_thread_id;
emit_subagent_session_started(
&new_thread
.thread
@@ -319,7 +321,7 @@ impl AgentControl {
client_metadata,
new_thread.thread.codex.session.session_id(),
new_thread.thread_id,
/*parent_thread_id*/ None,
parent_thread_id,
thread_config,
subagent_source.clone(),
);
@@ -490,6 +492,7 @@ impl AgentControl {
self.clone(),
session_source,
/*thread_source*/ Some(ThreadSource::Subagent),
/*parent_thread_id*/ Some(parent_thread_id),
/*forked_from_thread_id*/ Some(parent_thread_id),
/*persist_extended_history*/ false,
inherited_shell_snapshot,
@@ -638,6 +641,7 @@ impl AgentControl {
.history
.ok_or_else(|| CodexErr::ThreadNotFound(thread_id))?
.items;
let parent_thread_id = stored_thread.parent_thread_id;
let resumed_thread = state
.resume_thread_with_history_with_source(ResumeThreadWithHistoryOptions {
@@ -649,6 +653,7 @@ impl AgentControl {
}),
agent_control: self.clone(),
session_source,
parent_thread_id,
inherited_shell_snapshot,
inherited_exec_policy,
})
@@ -841,9 +846,9 @@ impl AgentControl {
pub(crate) fn register_session_root(
&self,
current_thread_id: ThreadId,
current_session_source: &SessionSource,
current_parent_thread_id: Option<ThreadId>,
) {
if thread_spawn_parent_thread_id(current_session_source).is_none() {
if current_parent_thread_id.is_none() {
self.state.register_root_thread(current_thread_id);
}
}
@@ -1218,7 +1223,8 @@ impl AgentControl {
child_thread_id: ThreadId,
session_source: Option<&SessionSource>,
) {
let Some(parent_thread_id) = session_source.and_then(thread_spawn_parent_thread_id) else {
let Some(parent_thread_id) = session_source.and_then(SessionSource::parent_thread_id)
else {
return;
};
let Some(state_db_ctx) = thread.state_db() else {
@@ -1263,10 +1269,6 @@ impl AgentControl {
}
}
fn thread_spawn_parent_thread_id(session_source: &SessionSource) -> Option<ThreadId> {
session_source.parent_thread_id()
}
fn agent_matches_prefix(agent_path: Option<&AgentPath>, prefix: &AgentPath) -> bool {
if prefix.is_root() {
return true;
+9
View File
@@ -74,6 +74,15 @@ fn assistant_message(text: &str, phase: Option<MessagePhase>) -> ResponseItem {
}
}
#[test]
fn register_session_root_skips_threads_with_explicit_parent() {
let control = AgentControl::default();
control.register_session_root(ThreadId::new(), Some(ThreadId::new()));
assert_eq!(control.state.agent_id_for_path(&AgentPath::root()), None);
}
fn spawn_agent_call(call_id: &str) -> ResponseItem {
ResponseItem::FunctionCall {
id: None,
+7 -6
View File
@@ -173,6 +173,7 @@ struct ModelClientState {
provider: SharedModelProvider,
auth_env_telemetry: AuthEnvTelemetry,
session_source: SessionSource,
parent_thread_id: Option<ThreadId>,
model_verbosity: Option<VerbosityConfig>,
enable_request_compression: bool,
include_timing_metrics: bool,
@@ -321,6 +322,7 @@ impl ModelClient {
installation_id: String,
provider_info: ModelProviderInfo,
session_source: SessionSource,
parent_thread_id: Option<ThreadId>,
model_verbosity: Option<VerbosityConfig>,
enable_request_compression: bool,
include_timing_metrics: bool,
@@ -344,6 +346,7 @@ impl ModelClient {
provider: model_provider,
auth_env_telemetry,
session_source,
parent_thread_id,
model_verbosity,
enable_request_compression,
include_timing_metrics,
@@ -637,7 +640,7 @@ impl ModelClient {
fn build_responses_identity_headers(&self) -> ApiHeaderMap {
let mut extra_headers = self.build_subagent_headers();
if let Some(parent_thread_id) = parent_thread_id_header_value(&self.state.session_source)
if let Some(parent_thread_id) = parent_thread_id_header_value(self.state.parent_thread_id)
&& let Ok(val) = HeaderValue::from_str(&parent_thread_id)
{
extra_headers.insert(X_CODEX_PARENT_THREAD_ID_HEADER, val);
@@ -664,7 +667,7 @@ impl ModelClient {
if let Some(subagent) = subagent_header_value(&self.state.session_source) {
client_metadata.insert(X_OPENAI_SUBAGENT_HEADER.to_string(), subagent);
}
if let Some(parent_thread_id) = parent_thread_id_header_value(&self.state.session_source) {
if let Some(parent_thread_id) = parent_thread_id_header_value(self.state.parent_thread_id) {
client_metadata.insert(
X_CODEX_PARENT_THREAD_ID_HEADER.to_string(),
parent_thread_id,
@@ -1733,10 +1736,8 @@ fn subagent_header_value(session_source: &SessionSource) -> Option<String> {
}
}
fn parent_thread_id_header_value(session_source: &SessionSource) -> Option<String> {
session_source
.parent_thread_id()
.map(|parent_thread_id| parent_thread_id.to_string())
fn parent_thread_id_header_value(parent_thread_id: Option<ThreadId>) -> Option<String> {
parent_thread_id.map(|parent_thread_id| parent_thread_id.to_string())
}
const RESPONSE_STREAM_CHANNEL_CAPACITY: usize = 1600;
+19 -7
View File
@@ -61,6 +61,13 @@ use tracing_subscriber::registry::LookupSpan;
use tracing_subscriber::util::SubscriberInitExt;
fn test_model_client(session_source: SessionSource) -> ModelClient {
test_model_client_with_parent(session_source, /*parent_thread_id*/ None)
}
fn test_model_client_with_parent(
session_source: SessionSource,
parent_thread_id: Option<ThreadId>,
) -> ModelClient {
let provider = create_oss_provider_with_base_url("https://example.com/v1", WireApi::Responses);
let thread_id = ThreadId::new();
ModelClient::new(
@@ -70,6 +77,7 @@ fn test_model_client(session_source: SessionSource) -> ModelClient {
/*installation_id*/ "11111111-1111-4111-8111-111111111111".to_string(),
provider,
session_source,
parent_thread_id,
/*model_verbosity*/ None,
/*enable_request_compression*/ false,
/*include_timing_metrics*/ false,
@@ -272,13 +280,16 @@ fn build_subagent_headers_sets_internal_memory_consolidation_label() {
#[test]
fn build_ws_client_metadata_includes_window_lineage_and_turn_metadata() {
let parent_thread_id = ThreadId::new();
let client = test_model_client(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id,
depth: 2,
agent_path: None,
agent_nickname: None,
agent_role: None,
}));
let client = test_model_client_with_parent(
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id,
depth: 2,
agent_path: None,
agent_nickname: None,
agent_role: None,
}),
Some(parent_thread_id),
);
client.advance_window_generation();
@@ -520,6 +531,7 @@ fn model_client_with_counting_attestation(
/*installation_id*/ "11111111-1111-4111-8111-111111111111".to_string(),
provider,
SessionSource::Exec,
/*parent_thread_id*/ None,
/*model_verbosity*/ None,
/*enable_request_compression*/ false,
/*include_timing_metrics*/ false,
+5 -2
View File
@@ -74,6 +74,8 @@ pub(crate) async fn run_codex_thread_interactive(
) -> Result<Codex, CodexErr> {
let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY);
let (tx_ops, rx_ops) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY);
let conversation_history = initial_history.unwrap_or(InitialHistory::New);
let forked_from_thread_id = conversation_history.forked_from_id();
let CodexSpawnOk { codex, .. } = Box::pin(Codex::spawn(CodexSpawnArgs {
config,
installation_id: parent_session.installation_id.clone(),
@@ -84,9 +86,10 @@ pub(crate) async fn run_codex_thread_interactive(
plugins_manager: Arc::clone(&parent_session.services.plugins_manager),
mcp_manager: Arc::clone(&parent_session.services.mcp_manager),
extensions: Arc::clone(&parent_session.services.extensions),
conversation_history: initial_history.unwrap_or(InitialHistory::New),
conversation_history,
session_source: SessionSource::SubAgent(subagent_source.clone()),
forked_from_thread_id: Some(parent_session.conversation_id),
forked_from_thread_id,
parent_thread_id: Some(parent_session.conversation_id),
thread_source: Some(ThreadSource::Subagent),
agent_control: parent_session.services.agent_control.clone(),
dynamic_tools: Vec::new(),
+2
View File
@@ -7,6 +7,7 @@ use crate::session::SessionSettingsUpdate;
use crate::session::SteerInputError;
use codex_features::Feature;
use codex_otel::SessionTelemetry;
use codex_protocol::ThreadId;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::Personality;
@@ -68,6 +69,7 @@ pub struct ThreadConfigSnapshot {
pub personality: Option<Personality>,
pub collaboration_mode: CollaborationMode,
pub session_source: SessionSource,
pub parent_thread_id: Option<ThreadId>,
pub thread_source: Option<ThreadSource>,
}
+2 -2
View File
@@ -156,8 +156,8 @@ pub(crate) fn is_guardian_reviewer_source(
) -> bool {
matches!(
session_source,
codex_protocol::protocol::SessionSource::SubAgent(SubAgentSource::Other(name))
if name == GUARDIAN_REVIEWER_NAME
codex_protocol::protocol::SessionSource::SubAgent(SubAgentSource::Other(label))
if label == GUARDIAN_REVIEWER_NAME
)
}
@@ -45,6 +45,7 @@ async fn write_rollout_with_user_event(dir: &Path, thread_id: ThreadId) -> io::R
meta: SessionMeta {
id: thread_id,
forked_from_id: None,
parent_thread_id: None,
timestamp: TEST_TIMESTAMP.to_string(),
cwd: std::path::PathBuf::from("."),
originator: "test_originator".to_string(),
@@ -32,6 +32,7 @@ fn stored_thread(cwd: &str, title: &str, first_user_message: &str) -> StoredThre
thread_id: ThreadId::new(),
rollout_path: Some(PathBuf::from("/tmp/rollout.jsonl")),
forked_from_id: None,
parent_thread_id: None,
preview: first_user_message.to_string(),
name: (!title.is_empty()).then(|| title.to_string()),
model_provider: "test-provider".to_string(),
+3
View File
@@ -402,6 +402,7 @@ pub(crate) struct CodexSpawnArgs {
pub(crate) conversation_history: InitialHistory,
pub(crate) session_source: SessionSource,
pub(crate) forked_from_thread_id: Option<ThreadId>,
pub(crate) parent_thread_id: Option<ThreadId>,
pub(crate) thread_source: Option<ThreadSource>,
pub(crate) agent_control: AgentControl,
pub(crate) dynamic_tools: Vec<DynamicToolSpec>,
@@ -467,6 +468,7 @@ impl Codex {
conversation_history,
session_source,
forked_from_thread_id,
parent_thread_id,
thread_source,
agent_control,
dynamic_tools,
@@ -595,6 +597,7 @@ impl Codex {
app_server_client_version: None,
session_source,
forked_from_thread_id,
parent_thread_id,
thread_source,
dynamic_tools,
persist_extended_history,
+2
View File
@@ -90,6 +90,7 @@ pub(super) async fn spawn_review_thread(
sess.session_id().to_string(),
sess.thread_id().to_string(),
forked_from_thread_id,
parent_turn_context.parent_thread_id,
&session_source,
parent_turn_context.thread_source,
review_turn_id.clone(),
@@ -113,6 +114,7 @@ pub(super) async fn spawn_review_thread(
reasoning_effort,
reasoning_summary,
session_source,
parent_thread_id: parent_turn_context.parent_thread_id,
thread_source: parent_turn_context.thread_source,
environments: parent_turn_context.environments.clone(),
available_models,
+11 -1
View File
@@ -97,6 +97,8 @@ pub(crate) struct SessionConfiguration {
pub(super) session_source: SessionSource,
/// Immediate history source copied into this thread, when this thread was forked.
pub(super) forked_from_thread_id: Option<ThreadId>,
/// Immediate control/spawn parent for this thread, when it has one.
pub(super) parent_thread_id: Option<ThreadId>,
/// Optional analytics source classification for this thread.
pub(super) thread_source: Option<ThreadSource>,
pub(super) dynamic_tools: Vec<DynamicToolSpec>,
@@ -187,6 +189,7 @@ impl SessionConfiguration {
personality: self.personality,
collaboration_mode: self.collaboration_mode.clone(),
session_source: self.session_source.clone(),
parent_thread_id: self.parent_thread_id,
thread_source: self.thread_source,
}
}
@@ -511,6 +514,10 @@ impl Session {
.forked_from_thread_id
.or_else(|| initial_history.forked_from_id());
session_configuration.forked_from_thread_id = forked_from_id;
let parent_thread_id = session_configuration
.parent_thread_id
.or_else(|| initial_history.get_resumed_parent_thread_id());
session_configuration.parent_thread_id = parent_thread_id;
let event_persistence_mode = if session_configuration.persist_extended_history {
ThreadEventPersistenceMode::Extended
@@ -550,6 +557,7 @@ impl Session {
CreateThreadParams {
thread_id,
forked_from_id,
parent_thread_id,
source: session_source,
thread_source: session_configuration.thread_source,
base_instructions: BaseInstructions {
@@ -1031,6 +1039,7 @@ impl Session {
installation_id.clone(),
session_configuration.provider.clone(),
session_configuration.session_source.clone(),
session_configuration.parent_thread_id,
config.model_verbosity,
config.features.enabled(Feature::EnableRequestCompression),
config.features.enabled(Feature::RuntimeMetrics),
@@ -1040,7 +1049,7 @@ impl Session {
.with_prompt_cache_key_override(
crate::guardian::prompt_cache_key_override_for_review_session(
&session_configuration.session_source,
session_configuration.forked_from_thread_id,
session_configuration.parent_thread_id,
),
),
code_mode_service: crate::tools::code_mode::CodeModeService::new(),
@@ -1083,6 +1092,7 @@ impl Session {
session_id,
thread_id,
forked_from_id,
parent_thread_id,
thread_source: session_configuration.thread_source,
thread_name: session_configuration.thread_name.clone(),
model: session_configuration.collaboration_mode.model().to_string(),
+13
View File
@@ -433,6 +433,7 @@ fn test_model_client_session() -> crate::client::ModelClientSession {
/*installation_id*/ "11111111-1111-4111-8111-111111111111".to_string(),
ModelProviderInfo::create_openai_provider(/* base_url */ /*base_url*/ None),
codex_protocol::protocol::SessionSource::Exec,
/*parent_thread_id*/ None,
/*model_verbosity*/ None,
/*enable_request_compression*/ false,
/*include_timing_metrics*/ false,
@@ -3096,6 +3097,7 @@ async fn set_rate_limits_retains_previous_credits() {
app_server_client_version: None,
session_source: SessionSource::Exec,
forked_from_thread_id: None,
parent_thread_id: None,
thread_source: None,
dynamic_tools: Vec::new(),
persist_extended_history: false,
@@ -3201,6 +3203,7 @@ async fn set_rate_limits_updates_plan_type_when_present() {
app_server_client_version: None,
session_source: SessionSource::Exec,
forked_from_thread_id: None,
parent_thread_id: None,
thread_source: None,
dynamic_tools: Vec::new(),
persist_extended_history: false,
@@ -3449,6 +3452,7 @@ async fn attach_thread_persistence(session: &mut Session) -> PathBuf {
CreateThreadParams {
thread_id: session.conversation_id,
forked_from_id: None,
parent_thread_id: None,
source: SessionSource::Exec,
thread_source: None,
base_instructions: BaseInstructions::default(),
@@ -3729,6 +3733,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
app_server_client_version: None,
session_source: SessionSource::Exec,
forked_from_thread_id: None,
parent_thread_id: None,
thread_source: None,
dynamic_tools: Vec::new(),
persist_extended_history: false,
@@ -4473,6 +4478,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
app_server_client_version: None,
session_source: SessionSource::Exec,
forked_from_thread_id: None,
parent_thread_id: None,
thread_source: None,
dynamic_tools: Vec::new(),
persist_extended_history: false,
@@ -4583,6 +4589,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
app_server_client_version: None,
session_source: SessionSource::Exec,
forked_from_thread_id: None,
parent_thread_id: None,
thread_source: None,
dynamic_tools: Vec::new(),
persist_extended_history: false,
@@ -4677,6 +4684,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
/*installation_id*/ "11111111-1111-4111-8111-111111111111".to_string(),
session_configuration.provider.clone(),
session_configuration.session_source.clone(),
session_configuration.parent_thread_id,
config.model_verbosity,
config.features.enabled(Feature::EnableRequestCompression),
config.features.enabled(Feature::RuntimeMetrics),
@@ -4818,6 +4826,7 @@ async fn make_session_with_config_and_rx(
app_server_client_version: None,
session_source: SessionSource::Exec,
forked_from_thread_id: None,
parent_thread_id: None,
thread_source: None,
dynamic_tools: Vec::new(),
persist_extended_history: false,
@@ -4922,6 +4931,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
app_server_client_version: None,
session_source: session_source.clone(),
forked_from_thread_id: None,
parent_thread_id: None,
thread_source: None,
dynamic_tools: Vec::new(),
persist_extended_history: false,
@@ -5944,6 +5954,7 @@ async fn shutdown_complete_does_not_append_to_thread_store_after_shutdown() {
CreateThreadParams {
thread_id: session.conversation_id,
forked_from_id: None,
parent_thread_id: None,
source: SessionSource::Exec,
thread_source: None,
base_instructions: BaseInstructions::default(),
@@ -6426,6 +6437,7 @@ where
app_server_client_version: None,
session_source: SessionSource::Exec,
forked_from_thread_id: None,
parent_thread_id: None,
thread_source: None,
dynamic_tools,
persist_extended_history: false,
@@ -6520,6 +6532,7 @@ where
/*installation_id*/ "11111111-1111-4111-8111-111111111111".to_string(),
session_configuration.provider.clone(),
session_configuration.session_source.clone(),
session_configuration.parent_thread_id,
config.model_verbosity,
config.features.enabled(Feature::EnableRequestCompression),
config.features.enabled(Feature::RuntimeMetrics),
@@ -693,6 +693,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
GUARDIAN_REVIEWER_NAME.to_string(),
)),
forked_from_thread_id: None,
parent_thread_id: None,
thread_source: None,
agent_control: AgentControl::default(),
dynamic_tools: Vec::new(),
@@ -5,6 +5,7 @@ use crate::environment_selection::ResolvedTurnEnvironments;
use codex_model_provider::SharedModelProvider;
use codex_model_provider::create_model_provider;
use codex_protocol::SessionId;
use codex_protocol::ThreadId;
use codex_protocol::models::AdditionalPermissionProfile;
use codex_protocol::openai_models::ToolMode;
use codex_protocol::protocol::ThreadSource;
@@ -62,6 +63,7 @@ pub struct TurnContext {
pub(crate) reasoning_effort: Option<ReasoningEffortConfig>,
pub(crate) reasoning_summary: ReasoningSummaryConfig,
pub(crate) session_source: SessionSource,
pub(crate) parent_thread_id: Option<ThreadId>,
pub(crate) thread_source: Option<ThreadSource>,
pub(crate) environments: ResolvedTurnEnvironments,
/// The session's absolute working directory. All relative paths provided
@@ -232,6 +234,7 @@ impl TurnContext {
reasoning_effort,
reasoning_summary: self.reasoning_summary,
session_source: self.session_source.clone(),
parent_thread_id: self.parent_thread_id,
thread_source: self.thread_source,
environments: self.environments.clone(),
#[allow(deprecated)]
@@ -506,6 +509,7 @@ impl Session {
session_id.to_string(),
thread_id.to_string(),
session_configuration.forked_from_thread_id,
session_configuration.parent_thread_id,
&session_configuration.session_source,
session_configuration.thread_source,
sub_id.clone(),
@@ -529,6 +533,7 @@ impl Session {
reasoning_effort,
reasoning_summary,
session_source,
parent_thread_id: session_configuration.parent_thread_id,
thread_source: session_configuration.thread_source,
environments,
#[allow(deprecated)]
+17
View File
@@ -187,6 +187,7 @@ pub(crate) struct ResumeThreadWithHistoryOptions {
pub(crate) initial_history: InitialHistory,
pub(crate) agent_control: AgentControl,
pub(crate) session_source: SessionSource,
pub(crate) parent_thread_id: Option<ThreadId>,
pub(crate) inherited_shell_snapshot: Option<Arc<ShellSnapshot>>,
pub(crate) inherited_exec_policy: Option<Arc<crate::exec_policy::ExecPolicyManager>>,
}
@@ -601,6 +602,7 @@ impl ThreadManager {
Arc::clone(&self.state.auth_manager),
self.agent_control(),
session_source,
/*parent_thread_id*/ None,
forked_from_thread_id,
thread_source,
options.dynamic_tools,
@@ -685,6 +687,7 @@ impl ThreadManager {
auth_manager,
self.agent_control(),
session_source,
/*parent_thread_id*/ None,
/*forked_from_thread_id*/ None,
thread_source,
Vec::new(),
@@ -713,6 +716,7 @@ impl ThreadManager {
InitialHistory::New,
Arc::clone(&self.state.auth_manager),
self.agent_control(),
/*parent_thread_id*/ None,
/*forked_from_thread_id*/ None,
/*thread_source*/ None,
Vec::new(),
@@ -746,6 +750,7 @@ impl ThreadManager {
auth_manager,
self.agent_control(),
session_source,
/*parent_thread_id*/ None,
/*forked_from_thread_id*/ None,
thread_source,
Vec::new(),
@@ -916,6 +921,7 @@ impl ThreadManager {
history,
Arc::clone(&self.state.auth_manager),
self.agent_control(),
/*parent_thread_id*/ None,
forked_from_thread_id,
thread_source,
Vec::new(),
@@ -1039,6 +1045,7 @@ impl ThreadManagerState {
config,
agent_control,
self.session_source.clone(),
/*parent_thread_id*/ None,
/*forked_from_thread_id*/ None,
/*thread_source*/ None,
/*persist_extended_history*/ false,
@@ -1056,6 +1063,7 @@ impl ThreadManagerState {
config: Config,
agent_control: AgentControl,
session_source: SessionSource,
parent_thread_id: Option<ThreadId>,
forked_from_thread_id: Option<ThreadId>,
thread_source: Option<ThreadSource>,
persist_extended_history: bool,
@@ -1073,6 +1081,7 @@ impl ThreadManagerState {
Arc::clone(&self.auth_manager),
agent_control,
session_source,
parent_thread_id,
forked_from_thread_id,
thread_source,
Vec::new(),
@@ -1096,6 +1105,7 @@ impl ThreadManagerState {
initial_history,
agent_control,
session_source,
parent_thread_id,
inherited_shell_snapshot,
inherited_exec_policy,
} = options;
@@ -1108,6 +1118,7 @@ impl ThreadManagerState {
Arc::clone(&self.auth_manager),
agent_control,
session_source,
parent_thread_id,
/*forked_from_thread_id*/ None,
thread_source,
Vec::new(),
@@ -1130,6 +1141,7 @@ impl ThreadManagerState {
agent_control: AgentControl,
session_source: SessionSource,
thread_source: Option<ThreadSource>,
parent_thread_id: Option<ThreadId>,
forked_from_thread_id: Option<ThreadId>,
persist_extended_history: bool,
inherited_shell_snapshot: Option<Arc<ShellSnapshot>>,
@@ -1145,6 +1157,7 @@ impl ThreadManagerState {
Arc::clone(&self.auth_manager),
agent_control,
session_source,
parent_thread_id,
forked_from_thread_id,
thread_source,
Vec::new(),
@@ -1167,6 +1180,7 @@ impl ThreadManagerState {
initial_history: InitialHistory,
auth_manager: Arc<AuthManager>,
agent_control: AgentControl,
parent_thread_id: Option<ThreadId>,
forked_from_thread_id: Option<ThreadId>,
thread_source: Option<ThreadSource>,
dynamic_tools: Vec<codex_protocol::dynamic_tools::DynamicToolSpec>,
@@ -1182,6 +1196,7 @@ impl ThreadManagerState {
auth_manager,
agent_control,
self.session_source.clone(),
parent_thread_id,
forked_from_thread_id,
thread_source,
dynamic_tools,
@@ -1204,6 +1219,7 @@ impl ThreadManagerState {
auth_manager: Arc<AuthManager>,
agent_control: AgentControl,
session_source: SessionSource,
parent_thread_id: Option<ThreadId>,
forked_from_thread_id: Option<ThreadId>,
thread_source: Option<ThreadSource>,
dynamic_tools: Vec<codex_protocol::dynamic_tools::DynamicToolSpec>,
@@ -1258,6 +1274,7 @@ impl ThreadManagerState {
conversation_history: initial_history,
session_source,
forked_from_thread_id,
parent_thread_id,
thread_source,
agent_control,
dynamic_tools,
@@ -211,6 +211,7 @@ async fn run_agent_job_loop(
"agent_job:{job_id}"
)))),
SpawnAgentOptions {
parent_thread_id: Some(session.conversation_id),
environments: Some(turn.environments.to_selections()),
..Default::default()
},
@@ -124,6 +124,7 @@ async fn handle_spawn_agent(
SpawnAgentOptions {
fork_parent_spawn_call_id: args.fork_context.then(|| call_id.clone()),
fork_mode: args.fork_context.then_some(SpawnAgentForkMode::FullHistory),
parent_thread_id: Some(session.conversation_id),
environments: Some(turn.environments.to_selections()),
},
))
@@ -30,7 +30,7 @@ impl ToolExecutor<ToolInvocation> for Handler {
session
.services
.agent_control
.register_session_root(session.conversation_id, &turn.session_source);
.register_session_root(session.conversation_id, turn.parent_thread_id);
let agents = session
.services
.agent_control
@@ -144,6 +144,7 @@ async fn handle_spawn_agent(
SpawnAgentOptions {
fork_parent_spawn_call_id: fork_mode.as_ref().map(|_| call_id.clone()),
fork_mode,
parent_thread_id: Some(session.conversation_id),
environments: Some(turn.environments.to_selections()),
},
),
+4 -6
View File
@@ -261,6 +261,7 @@ impl TurnMetadataState {
session_id: String,
thread_id: String,
forked_from_thread_id: Option<ThreadId>,
parent_thread_id: Option<ThreadId>,
session_source: &SessionSource,
thread_source: Option<ThreadSource>,
turn_id: String,
@@ -278,18 +279,15 @@ impl TurnMetadataState {
)
.to_string(),
);
let (parent_thread_id, subagent_kind) = match session_source {
SessionSource::SubAgent(subagent_source) => (
subagent_source.parent_thread_id().or(forked_from_thread_id),
Some(subagent_source.kind().to_string()),
),
let subagent_kind = match session_source {
SessionSource::SubAgent(subagent_source) => Some(subagent_source.kind().to_string()),
SessionSource::Cli
| SessionSource::VSCode
| SessionSource::Exec
| SessionSource::Mcp
| SessionSource::Custom(_)
| SessionSource::Internal(_)
| SessionSource::Unknown => (None, None),
| SessionSource::Unknown => None,
};
let base_metadata = TurnMetadataBag {
request_kind: None,
+44 -24
View File
@@ -122,6 +122,7 @@ fn turn_metadata_state_uses_platform_sandbox_tag() {
"session-a".to_string(),
"thread-a".to_string(),
/*forked_from_thread_id*/ None,
/*parent_thread_id*/ None,
&SessionSource::Exec,
Some(ThreadSource::User),
"turn-a".to_string(),
@@ -163,6 +164,7 @@ fn turn_metadata_state_uses_explicit_subagent_thread_source() {
"session-a".to_string(),
"thread-a".to_string(),
/*forked_from_thread_id*/ None,
/*parent_thread_id*/ None,
&SessionSource::Exec,
Some(ThreadSource::Subagent),
"turn-a".to_string(),
@@ -191,6 +193,7 @@ fn turn_metadata_state_includes_root_fork_lineage() {
"session-a".to_string(),
"thread-a".to_string(),
Some(source_thread_id),
/*parent_thread_id*/ None,
&SessionSource::Exec,
Some(ThreadSource::User),
"turn-a".to_string(),
@@ -223,6 +226,7 @@ fn turn_metadata_state_includes_thread_spawn_subagent_parent_without_fork() {
"session-a".to_string(),
"thread-a".to_string(),
/*forked_from_thread_id*/ None,
Some(parent_thread_id),
&SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id,
depth: 1,
@@ -261,6 +265,7 @@ fn turn_metadata_state_includes_forked_thread_spawn_subagent_lineage() {
"session-a".to_string(),
"thread-a".to_string(),
Some(parent_thread_id),
Some(parent_thread_id),
&SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id,
depth: 1,
@@ -291,38 +296,46 @@ fn turn_metadata_state_includes_forked_thread_spawn_subagent_lineage() {
}
#[test]
fn turn_metadata_state_includes_known_parent_for_other_subagent() {
fn turn_metadata_state_includes_known_parent_for_non_thread_spawn_subagents_without_fork() {
let temp_dir = TempDir::new().expect("temp dir");
let cwd = temp_dir.path().abs();
let permission_profile = PermissionProfile::read_only();
let parent_thread_id =
ThreadId::from_string("44444444-4444-4444-8444-444444444444").expect("thread id");
let sources = [
(SubAgentSource::Review, "review"),
(SubAgentSource::Other("guardian".to_string()), "guardian"),
(
SubAgentSource::Other("agent_job:job-1".to_string()),
"agent_job:job-1",
),
];
let state = TurnMetadataState::new(
"session-a".to_string(),
"thread-a".to_string(),
Some(parent_thread_id),
&SessionSource::SubAgent(SubAgentSource::Other("guardian".to_string())),
Some(ThreadSource::Subagent),
"turn-a".to_string(),
cwd,
&permission_profile,
WindowsSandboxLevel::Disabled,
/*enforce_managed_network*/ false,
);
for (subagent_source, subagent_kind) in sources {
let state = TurnMetadataState::new(
"session-a".to_string(),
"thread-a".to_string(),
/*forked_from_thread_id*/ None,
Some(parent_thread_id),
&SessionSource::SubAgent(subagent_source),
Some(ThreadSource::Subagent),
"turn-a".to_string(),
cwd.clone(),
&permission_profile,
WindowsSandboxLevel::Disabled,
/*enforce_managed_network*/ false,
);
let header = state.current_header_value().expect("header");
let json: Value = serde_json::from_str(&header).expect("json");
let header = state.current_header_value().expect("header");
let json: Value = serde_json::from_str(&header).expect("json");
assert_eq!(
json["forked_from_thread_id"].as_str(),
Some("44444444-4444-4444-8444-444444444444")
);
assert_eq!(
json["parent_thread_id"].as_str(),
Some("44444444-4444-4444-8444-444444444444")
);
assert_eq!(json["subagent_kind"].as_str(), Some("guardian"));
assert!(json.get("forked_from_thread_id").is_none());
assert_eq!(
json["parent_thread_id"].as_str(),
Some("44444444-4444-4444-8444-444444444444")
);
assert_eq!(json["subagent_kind"].as_str(), Some(subagent_kind));
}
}
#[test]
@@ -335,6 +348,7 @@ fn turn_metadata_state_includes_turn_started_at_unix_ms_after_start() {
"session-a".to_string(),
"thread-a".to_string(),
/*forked_from_thread_id*/ None,
/*parent_thread_id*/ None,
&SessionSource::Exec,
Some(ThreadSource::User),
"turn-a".to_string(),
@@ -364,6 +378,7 @@ fn turn_metadata_state_includes_model_and_reasoning_effort_only_in_request_meta(
"session-a".to_string(),
"thread-a".to_string(),
/*forked_from_thread_id*/ None,
/*parent_thread_id*/ None,
&SessionSource::Exec,
/*thread_source*/ None,
"turn-a".to_string(),
@@ -412,6 +427,7 @@ fn turn_metadata_state_marks_user_input_requested_during_turn_only_for_mcp_reque
"session-a".to_string(),
"thread-a".to_string(),
/*forked_from_thread_id*/ None,
/*parent_thread_id*/ None,
&SessionSource::Exec,
/*thread_source*/ None,
"turn-a".to_string(),
@@ -464,6 +480,7 @@ fn turn_metadata_state_ignores_client_reserved_metadata_before_start() {
"session-a".to_string(),
"thread-a".to_string(),
/*forked_from_thread_id*/ None,
/*parent_thread_id*/ None,
&SessionSource::Exec,
Some(ThreadSource::User),
"turn-a".to_string(),
@@ -511,6 +528,7 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields(
"session-a".to_string(),
"thread-a".to_string(),
Some(source_thread_id),
Some(parent_thread_id),
&SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id,
depth: 1,
@@ -612,6 +630,7 @@ fn turn_metadata_state_overlays_compaction_only_on_compaction_requests() {
"session-a".to_string(),
"thread-a".to_string(),
/*forked_from_thread_id*/ None,
/*parent_thread_id*/ None,
&SessionSource::Exec,
Some(ThreadSource::User),
"turn-a".to_string(),
@@ -671,6 +690,7 @@ async fn turn_metadata_state_preserves_lineage_after_git_enrichment() {
"session-a".to_string(),
"thread-a".to_string(),
Some(parent_thread_id),
Some(parent_thread_id),
&SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id,
depth: 1,