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
This commit is contained in:
Ruslan Nigmatullin
2026-11-06 01:50:20 +00:00
committed by copyberry
parent d63a342256
commit 5e5bdee745
4 changed files with 11 additions and 20 deletions
+3 -10
View File
@@ -260,20 +260,13 @@ impl LocalAgentControl {
&self,
agent_id: ThreadId,
state: &Arc<ThreadManagerState>,
mut communication: InterAgentCommunication,
communication: InterAgentCommunication,
context: AgentCommunicationContext,
start_options: TurnStartOptions,
) -> CodexResult<String> {
let observers = state.extensions.message_lifecycle_contributors();
let delivery = (!observers.is_empty()).then(|| {
let message_id = communication
.id
.take()
.filter(|id| !id.is_empty())
.unwrap_or_else(|| (self.runtime.item_id_generator)("amsg"));
communication.id = Some(message_id.clone());
MessageDelivery::publish(context.sender_thread_id, message_id, observers)
});
let delivery = (!observers.is_empty())
.then(|| MessageDelivery::publish(context.sender_thread_id, observers));
let log_content = crate::agent_communication::send_log_content(&communication);
{
// Keep unloaded delivery atomic with publication of a reloaded session.
+1 -4
View File
@@ -4,7 +4,6 @@
use codex_extension_api::ExtensionData;
use codex_extension_api::MessageLifecycleContributor;
use codex_extension_api::MessageLifecycleInput;
use codex_protocol::ResponseItemId;
use codex_protocol::ThreadId;
use codex_protocol::protocol::InterAgentCommunication;
use serde::Deserialize;
@@ -156,13 +155,11 @@ pub(crate) struct MessageDelivery(Arc<Mutex<Option<MessagePublication>>>);
impl MessageDelivery {
pub(crate) fn publish(
sender: ThreadId,
message_id: ResponseItemId,
observers: &[Arc<dyn MessageLifecycleContributor>],
) -> Self {
let input = MessageLifecycleInput {
sender,
data: ExtensionData::new(message_id.to_string()),
message_id,
data: ExtensionData::new(uuid::Uuid::now_v7().to_string()),
};
for observer in observers {
observer.on_publish(&input);
+6 -4
View File
@@ -7,7 +7,6 @@ use codex_core::config::Config;
use codex_extension_api::ExtensionRegistryBuilder;
use codex_extension_api::MessageLifecycleContributor;
use codex_extension_api::MessageLifecycleInput;
use codex_protocol::ResponseItemId;
use codex_protocol::ThreadId;
use codex_protocol::protocol::AgentStatus;
use codex_protocol::protocol::Op;
@@ -20,15 +19,18 @@ use std::sync::mpsc;
use tokio::sync::oneshot;
#[derive(Default)]
struct Messages(Mutex<HashSet<ResponseItemId>>);
struct Messages(Mutex<HashSet<uuid::Uuid>>);
impl MessageLifecycleContributor for Messages {
fn on_publish(&self, input: &MessageLifecycleInput) {
let mut pending = self.0.lock().expect("messages lock");
assert!(pending.insert(input.message_id.clone()));
let token = uuid::Uuid::new_v4();
input.data.insert(token);
assert!(pending.insert(token));
}
fn on_consumed(&self, input: &MessageLifecycleInput, _recipient: ThreadId) {
let mut pending = self.0.lock().expect("messages lock");
assert!(pending.remove(&input.message_id));
let token = input.data.remove::<uuid::Uuid>().expect("published token");
assert!(pending.remove(token.as_ref()));
}
}
@@ -1,12 +1,11 @@
//! Capture publication state once; consume it independently for each delivery.
use crate::ExtensionData;
use codex_protocol::ResponseItemId;
use codex_protocol::ThreadId;
/// State shared by all deliveries of a publication, including recipients resolved later.
/// The model-visible item ID may still be unknown until the recipient records the message.
pub struct MessageLifecycleInput {
pub message_id: ResponseItemId,
pub sender: ThreadId,
/// Capture contributor-specific state at publication; read it at consumption.
pub data: ExtensionData,