Track root turns across delegated Codex requests (#38232)

## Why

`parent_turn_id` identifies the immediate caller of a nested request, but not the
top-level turn that initiated a multi-level chain of work.

## What changed

- Add `root_turn_id` to turn state, submissions, and Responses API metadata.
- Propagate the root through spawned agents, delegated work, review sessions, and
  turn-triggering inter-agent messages.
- Omit the root when background work has no user turn or combined input makes the
  root ambiguous.

## Testing

Added coverage for nested agent resumes, reviews, mailbox delivery, steered input,
and reserved metadata handling.

GitOrigin-RevId: 60057db2ee81f4efefa946a9f63c74ff98b93929
This commit is contained in:
Eddie Chen authored and copyberry committed 2026-08-12 18:11:13 +00:00
1 parent 9df9ff6ad9
commit 1f4ea79853
38 files changed
+470 -65

No files matched your search

+19 -2
View File
@@ -830,8 +830,10 @@ impl Session {
impl SessionIo {
/// Submit the `op` wrapped in a `Submission` with a unique ID.
pub(crate) async fn submit(&self, op: Op) -> CodexResult<String> {
self.submit_with_trace(op, /*trace*/ None, /*parent_turn_id*/ None)
.await
self.submit_with_trace(
op, /*trace*/ None, /*parent_turn_id*/ None, /*root_turn_id*/ None,
)
.await
}
pub(crate) async fn submit_with_trace(
@@ -839,6 +841,7 @@ impl SessionIo {
op: Op,
trace: Option<W3cTraceContext>,
parent_turn_id: Option<String>,
root_turn_id: Option<String>,
) -> CodexResult<String> {
let id = new_submission_id();
let sub = Submission {
@@ -847,6 +850,7 @@ impl SessionIo {
client_user_message_id: None,
trace,
parent_turn_id,
root_turn_id,
};
self.submit_with_id(sub).await?;
Ok(id)
@@ -866,6 +870,7 @@ impl SessionIo {
client_user_message_id,
trace,
parent_turn_id: None,
root_turn_id: None,
};
self.submit_with_id(sub).await?;
Ok(id)
@@ -1259,6 +1264,7 @@ impl Session {
},
/*client_user_message_id*/ None,
/*parent_turn_id*/ None,
/*root_turn_id*/ None,
)
.await;
}
@@ -2046,6 +2052,7 @@ impl Session {
communication,
context,
/*parent_turn_id*/ None,
/*root_turn_id*/ None,
)
.await
{
@@ -4025,6 +4032,7 @@ impl Session {
expected_turn_id: Option<&str>,
client_user_message_id: Option<String>,
responsesapi_client_metadata: Option<HashMap<String, String>>,
incoming_turn_metadata: Option<&TurnMetadataState>,
) -> Result<String, SteerInputError> {
let mut active = self.active_turn.lock().await;
let Some(active_turn) = active.as_mut() else {
@@ -4084,6 +4092,15 @@ impl Session {
content: input,
client_id: client_user_message_id.clone(),
});
if let Some(incoming_turn_metadata) = incoming_turn_metadata
&& active_task.turn_context.turn_metadata_state.root_turn_id()
!= incoming_turn_metadata.root_turn_id()
{
active_task
.turn_context
.turn_metadata_state
.mark_root_turn_ambiguous();
}
self.input_queue
.extend_pending_input_and_accept_mailbox_delivery_for_turn_state(
active_turn.turn_state.as_ref(),