Files
codex/codex-rs/core/src/agent_communication.rs
T
Ruslan Nigmatullin 5e5bdee745 Decouple message lifecycle state from model-visible item IDs (#59785)
## Why

A message's model-visible item ID may be unknown until the recipient records it. Message lifecycle observers need to capture publication state before that point.

## What changed

Remove `message_id` from `MessageLifecycleInput` and give each publication's `ExtensionData` a fresh UUID key. Stop assigning an item ID when publishing messages for lifecycle observers.

## Testing

Update the agent mailbox test to store a contributor-specific token in `ExtensionData` at publication and retrieve it at consumption, verifying that publication state survives queued delivery and recipient eviction.

GitOrigin-RevId: e254f94994d0976e61cb46ee23297f3248b9a389
2026-11-06 01:50:20 +00:00

190 lines
5.4 KiB
Rust

//! Direct-message diagnostics and delivery ownership across queues and eviction.
//! Only history commitment consumes mail; dropping its last owner releases its attachments.
use codex_extension_api::ExtensionData;
use codex_extension_api::MessageLifecycleContributor;
use codex_extension_api::MessageLifecycleInput;
use codex_protocol::ThreadId;
use codex_protocol::protocol::InterAgentCommunication;
use serde::Deserialize;
use serde::Serialize;
use std::borrow::Cow;
use std::fmt;
use std::ops::Deref;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::PoisonError;
pub(crate) static PENDING_MAILBOX_MESSAGES: codex_diagnostics::Gauge =
codex_diagnostics::Gauge::new("core.mailbox.pending");
const AGENT_COMMUNICATION_TARGET: &str = "codex_otel.agent_communication";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum AgentCommunicationKind {
Spawn,
Message,
Followup,
Result,
}
impl AgentCommunicationKind {
fn as_str(self) -> &'static str {
match self {
Self::Spawn => "spawn",
Self::Message => "message",
Self::Followup => "followup",
Self::Result => "result",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct AgentCommunicationContext {
kind: AgentCommunicationKind,
pub(crate) sender_thread_id: ThreadId,
}
impl AgentCommunicationContext {
pub(crate) fn new(kind: AgentCommunicationKind, sender_thread_id: ThreadId) -> Self {
Self {
kind,
sender_thread_id,
}
}
}
pub(crate) fn send_log_content(
communication: &InterAgentCommunication,
) -> Option<Cow<'static, str>> {
tracing::enabled!(target: AGENT_COMMUNICATION_TARGET, tracing::Level::INFO).then(|| {
match &communication.encrypted_content {
Some(content) => Cow::Owned(content.clone()),
None => Cow::Borrowed("[plaintext]"),
}
})
}
pub(crate) fn emit_agent_communication_send(
communication_id: &str,
context: &AgentCommunicationContext,
log_content: Option<&str>,
receiver_thread_id: ThreadId,
) {
let Some(content) = log_content else {
return;
};
tracing::info!(
target: AGENT_COMMUNICATION_TARGET,
{
event.name = "codex.agent_communication",
communication_id,
kind = context.kind.as_str(),
state = "send",
sender_thread_id = %context.sender_thread_id,
receiver_thread_id = %receiver_thread_id,
content,
},
"agent communication"
);
}
pub(crate) fn emit_agent_communication_receive(communication_id: &str) {
tracing::info!(
target: AGENT_COMMUNICATION_TARGET,
{
event.name = "codex.agent_communication",
communication_id,
state = "receive",
},
"agent communication"
);
}
/// Pending mail with shared ownership of its captured publication state.
/// Serialization preserves the protocol shape; restored historical mail is not republished.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(transparent)]
pub struct PendingAgentMessage {
communication: InterAgentCommunication,
#[serde(skip)]
pub(crate) delivery: Option<MessageDelivery>,
}
impl From<InterAgentCommunication> for PendingAgentMessage {
fn from(communication: InterAgentCommunication) -> Self {
Self::new(communication, /*delivery*/ None)
}
}
impl Deref for PendingAgentMessage {
type Target = InterAgentCommunication;
fn deref(&self) -> &Self::Target {
&self.communication
}
}
impl PartialEq for PendingAgentMessage {
fn eq(&self, other: &Self) -> bool {
self.communication == other.communication
}
}
impl PendingAgentMessage {
pub(crate) fn new(
communication: InterAgentCommunication,
delivery: Option<MessageDelivery>,
) -> Self {
Self {
communication,
delivery,
}
}
}
/// The publication state retained until this delivery is consumed or abandoned.
struct MessagePublication {
input: MessageLifecycleInput,
observers: Box<[Arc<dyn MessageLifecycleContributor>]>,
}
/// Clones follow one logical delivery through queues and share its consumption state.
#[derive(Clone)]
pub(crate) struct MessageDelivery(Arc<Mutex<Option<MessagePublication>>>);
impl MessageDelivery {
pub(crate) fn publish(
sender: ThreadId,
observers: &[Arc<dyn MessageLifecycleContributor>],
) -> Self {
let input = MessageLifecycleInput {
sender,
data: ExtensionData::new(uuid::Uuid::now_v7().to_string()),
};
for observer in observers {
observer.on_publish(&input);
}
let publication = MessagePublication {
input,
observers: observers.into(),
};
Self(Arc::new(Mutex::new(Some(publication))))
}
/// Commit this delivery and release its publication state, even if clones remain.
pub(crate) fn consumed(&self, recipient: ThreadId) {
let publication = self.0.lock().unwrap_or_else(PoisonError::into_inner).take();
if let Some(publication) = publication {
for observer in &publication.observers {
observer.on_consumed(&publication.input, recipient);
}
}
}
}
impl fmt::Debug for MessageDelivery {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MessageDelivery").finish_non_exhaustive()
}
}