mirror of
https://github.com/openai/codex.git
synced 2026-10-22 17:58:46 +00:00
Preserve queued agent mail across session eviction (#50087)
## Why Unread queue-only messages prevented idle agents from unloading, and sending a message to an evicted agent reloaded its session. Pending mail should not require an idle recipient to occupy a loaded thread slot. ## What changed - Retain queue-only mail in an in-memory runtime mailbox when a session unloads. Queue new messages for locally evicted agents without reloading them or starting a turn. - Add `AgentControl::take_mailbox` and `AgentControl::watch_mailbox` so sessions can consume retained mail and react to host mailbox notifications. - Preserve message order across eviction, reload, and subsequent follow-ups. Keep agents with outstanding durable sleep or turn-triggering mail loaded, and discard retained mail when an agent is removed or its tree shuts down. ## Testing Add integration coverage for retaining mail across eviction, sending while unloaded, and ordering messages around a follow-up after reload. Add a host controller test verifying that mailbox notifications wake `wait_agent` and that the message is delivered exactly once. GitOrigin-RevId: 4b55647f9ee9d1aaf3753f0688863bda81617327
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
use crate::agent::api::AgentControl;
|
||||
use crate::agent_communication::PENDING_MAILBOX_MESSAGES;
|
||||
use crate::state::ActiveTurn;
|
||||
use crate::state::MailboxDeliveryPhase;
|
||||
use crate::state::TurnState;
|
||||
use codex_diagnostics::Gauge;
|
||||
use codex_diagnostics::GaugeGuard;
|
||||
use codex_history::ResponseItemEnvelope;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::protocol::InterAgentCommunication;
|
||||
use codex_protocol::turn_input::TurnStartOptions;
|
||||
@@ -17,8 +19,6 @@ use tokio::sync::watch;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tokio_util::task::AbortOnDropHandle;
|
||||
|
||||
static PENDING_MAILBOX_MESSAGES: Gauge = Gauge::new("core.mailbox.pending");
|
||||
|
||||
/// Host capture metadata belonging to one input, including steers within another turn.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct UserInputMetadata {
|
||||
@@ -94,11 +94,12 @@ pub(crate) struct TurnInputQueue {
|
||||
/// Session-scoped pending input storage and active-turn mailbox delivery coordination.
|
||||
pub(crate) struct InputQueue {
|
||||
activity_tx: watch::Sender<InputQueueActivity>,
|
||||
controller: Option<(ThreadId, Arc<dyn AgentControl>, watch::Receiver<bool>)>,
|
||||
mailbox_pending_mails: Mutex<VecDeque<PendingMailboxCommunication>>,
|
||||
}
|
||||
|
||||
struct PendingMailboxCommunication {
|
||||
communication: InterAgentCommunication,
|
||||
pub(crate) struct PendingMailboxCommunication {
|
||||
pub(crate) communication: InterAgentCommunication,
|
||||
start_options: TurnStartOptions,
|
||||
_diagnostics_guard: GaugeGuard,
|
||||
}
|
||||
@@ -108,10 +109,44 @@ impl InputQueue {
|
||||
let (activity_tx, _) = watch::channel(InputQueueActivity::Mailbox);
|
||||
Self {
|
||||
activity_tx,
|
||||
controller: None,
|
||||
mailbox_pending_mails: Mutex::new(VecDeque::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_controller(thread_id: ThreadId, control: Arc<dyn AgentControl>) -> Self {
|
||||
let updates = control.watch_mailbox(thread_id);
|
||||
Self {
|
||||
controller: Some((thread_id, control, updates)),
|
||||
..Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn mailbox_updates(&self) -> Option<watch::Receiver<bool>> {
|
||||
self.controller
|
||||
.as_ref()
|
||||
.map(|(_, _, updates)| updates.clone())
|
||||
}
|
||||
|
||||
pub(crate) fn notify_mailbox(&self) {
|
||||
self.activity_tx.send_replace(InputQueueActivity::Mailbox);
|
||||
}
|
||||
|
||||
fn read_mailbox(&self, pending: &mut VecDeque<PendingMailboxCommunication>) {
|
||||
if let Some((thread_id, control, _)) = &self.controller {
|
||||
pending.extend(
|
||||
control
|
||||
.take_mailbox(*thread_id)
|
||||
.into_iter()
|
||||
.map(|communication| PendingMailboxCommunication {
|
||||
communication,
|
||||
start_options: TurnStartOptions::default(),
|
||||
_diagnostics_guard: PENDING_MAILBOX_MESSAGES.track(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn subscribe_activity(
|
||||
&self,
|
||||
turn_state: Option<&Mutex<TurnState>>,
|
||||
@@ -172,19 +207,23 @@ impl InputQueue {
|
||||
communication: InterAgentCommunication,
|
||||
start_options: TurnStartOptions,
|
||||
) {
|
||||
self.mailbox_pending_mails
|
||||
.lock()
|
||||
.await
|
||||
.push_back(PendingMailboxCommunication {
|
||||
communication,
|
||||
start_options,
|
||||
_diagnostics_guard: PENDING_MAILBOX_MESSAGES.track(),
|
||||
});
|
||||
let mut pending = self.mailbox_pending_mails.lock().await;
|
||||
// Mail retained while unloaded precedes new submissions to the loaded session.
|
||||
self.read_mailbox(&mut pending);
|
||||
pending.push_back(PendingMailboxCommunication {
|
||||
communication,
|
||||
start_options,
|
||||
_diagnostics_guard: PENDING_MAILBOX_MESSAGES.track(),
|
||||
});
|
||||
self.activity_tx.send_replace(InputQueueActivity::Mailbox);
|
||||
}
|
||||
|
||||
pub(crate) async fn has_pending_mailbox_items(&self) -> bool {
|
||||
!self.mailbox_pending_mails.lock().await.is_empty()
|
||||
|| self
|
||||
.controller
|
||||
.as_ref()
|
||||
.is_some_and(|(_, _, updates)| *updates.borrow())
|
||||
}
|
||||
|
||||
pub(crate) async fn has_trigger_turn_mailbox_items(&self) -> bool {
|
||||
@@ -195,13 +234,14 @@ impl InputQueue {
|
||||
.any(|mail| mail.communication.trigger_turn)
|
||||
}
|
||||
|
||||
pub(crate) async fn drain_mailbox(&self) -> Vec<PendingMailboxCommunication> {
|
||||
let mut pending = self.mailbox_pending_mails.lock().await;
|
||||
self.read_mailbox(&mut pending);
|
||||
pending.drain(..).collect()
|
||||
}
|
||||
|
||||
pub(crate) async fn drain_mailbox_input_items(&self) -> (Vec<TurnInput>, TurnStartOptions) {
|
||||
let pending_mails = self
|
||||
.mailbox_pending_mails
|
||||
.lock()
|
||||
.await
|
||||
.drain(..)
|
||||
.collect::<Vec<_>>();
|
||||
let pending_mails = self.drain_mailbox().await;
|
||||
// A later follow-up supersedes the earlier choice, including an omitted choice.
|
||||
let mut start_options = pending_mails
|
||||
.iter()
|
||||
|
||||
Reference in New Issue
Block a user