Preserve turn attribution when queued mail wakes durable sleep (#51419)

## Why

Queue-only agent mail can wake a sleeping session without starting a new task. The wake turn needs to retain the previous turn's attribution so delegated work reports completion to the initiating agent, including after a reload.

## What changed

Capture the previous turn's attribution while reserving the pending-work turn. Reuse its trigger, parent and root turn IDs, and initiating agent path for queue-only wakes. Mail that explicitly starts a new task continues to supply its own attribution.

## Testing

Extend regression coverage for live and reloaded sleep wakes, delegated and unknown attribution, settings changes while asleep, and subsequent task-triggering mail. Verify that a reloaded sleeping worker routes completion to the initiating turn.

GitOrigin-RevId: 28e38474028bdbf35a0538c1202165f056bb3dc3
This commit is contained in:
Eddie Chen
2026-10-06 17:09:56 +00:00
committed by copyberry
parent cfc946f4e1
commit f1dd04959c
4 changed files with 227 additions and 41 deletions
+30
View File
@@ -657,6 +657,36 @@ impl Session {
}
}
#[expect(
clippy::await_holding_invalid_type,
reason = "The admission lock keeps the previous turn stable while capturing its attribution."
)]
pub(crate) async fn reserve_pending_work_turn(
&self,
) -> Option<(Arc<tokio::sync::Mutex<TurnState>>, TurnStartOptions)> {
let mut active_turn = self.active_turn.lock().await;
if active_turn.is_some() {
return None;
}
let needs_new_turn = self.input_queue.has_trigger_turn_mailbox_items().await;
if !needs_new_turn && !self.has_outstanding_durable_sleep() {
return None;
}
let active_turn = active_turn.get_or_insert_with(ActiveTurn::default);
let previous_options = if needs_new_turn {
Default::default()
} else {
self.state
.lock()
.await
.turn_attribution
.as_ref()
.map(codex_history::TurnAttribution::start_options)
.unwrap_or_default()
};
Some((Arc::clone(&active_turn.turn_state), previous_options))
}
pub(crate) async fn route_realtime_text_input(
self: &Arc<Self>,
text: String,
+16 -19
View File
@@ -457,20 +457,12 @@ impl Session {
self: &Arc<Self>,
sub_id: String,
) {
if !self.input_queue.has_pending_mailbox_items().await
|| (!self.input_queue.has_trigger_turn_mailbox_items().await
&& !self.has_outstanding_durable_sleep())
{
if !self.input_queue.has_pending_mailbox_items().await {
return;
}
let turn_state = {
let mut active_turn = self.active_turn.lock().await;
if active_turn.is_some() {
return;
}
let active_turn = active_turn.get_or_insert_with(ActiveTurn::default);
Arc::clone(&active_turn.turn_state)
let Some((turn_state, previous_options)) = self.reserve_pending_work_turn().await else {
return;
};
self.services
@@ -493,6 +485,7 @@ impl Session {
|item| matches!(item, TurnInput::InterAgentCommunication(mail) if mail.trigger_turn),
) {
// Queue-only mail wakes durable sleep without selecting a new task's settings.
start_options = previous_options;
start_options.cyber_access_program = self
.reference_context_item()
.await
@@ -511,14 +504,18 @@ impl Session {
turn_context.turn_metadata_state.set_turn_trigger(trigger);
}
if let Some(id) = start_options.parent_turn_id {
if let Some(initiating_agent_path) = input.iter().find_map(|item| {
let TurnInput::InterAgentCommunication(communication) = item else {
return None;
};
communication
.trigger_turn
.then(|| communication.author.clone())
}) {
if let Some(initiating_agent_path) = input
.iter()
.find_map(|item| {
let TurnInput::InterAgentCommunication(communication) = item else {
return None;
};
communication
.trigger_turn
.then(|| communication.author.clone())
})
.or(start_options.initiating_agent_path)
{
turn_context
.turn_metadata_state
.set_initiating_agent_path(initiating_agent_path);