Skip to content

Regression: encrypted MultiAgentV2 messages remove readable task audit trail #28058

Open

Description

@ignatremizov

What version of Codex CLI is running?

Upstream main after #26210 (Encrypt multi-agent v2 message payloads, merged 2026-06-05). This appears to affect versions that include that change and enable MultiAgentV2 (post-0.137.0).

What subscription do you have?

Not subscription-specific.

Which model were you using?

Not model-specific. This concerns MultiAgentV2 spawn_agent, send_message, and followup_task message handling.

What platform is your computer?

Not platform-specific.

What terminal emulator and version are you using (if applicable)?

Not terminal-specific.

Codex doctor report

Not applicable. The regression is visible from the merged code behavior in #26210 rather than from local environment state.

What issue are you seeing?

#26210 makes MultiAgentV2 agent task/message payloads opaque to Codex by marking the model-facing message parameter as encrypted, storing only InterAgentCommunication.encrypted_content, and leaving InterAgentCommunication.content empty.

The encrypted delivery path is understandable as privacy hardening, but it also removes the human-readable task/message text from local rollout history, trace reduction, and parent-side audit/debug surfaces. That makes it difficult to answer basic questions such as:

  • What task did this spawn_agent call give the child agent?
  • What message was sent to a subagent?
  • Why did a child thread exist when reviewing a rollout after the fact?

This is different from #26753, which reports request validation failures for encrypted tool schemas. This issue is about auditability and debuggability after the encrypted schema is accepted.

What steps can reproduce the bug?

  1. Use a build containing Encrypt multi-agent v2 message payloads #26210 with MultiAgentV2 enabled. (aka post-0.137.0)
  2. Have the model call spawn_agent, send_message, or followup_task.
  3. Inspect the parent rollout/history/trace for the subagent task.
  4. The task/message content is hidden behind ciphertext rather than being available as human-readable audit text.

What is the expected behavior?

Codex should preserve a human-readable, structured audit copy of the subagent task/message while still allowing encrypted delivery to the recipient model.

A possible shape is to keep the encrypted message field for model delivery, but add a separate non-encrypted audit field for the readable task text. The audit field should be persisted in rollout/history/trace metadata so users and maintainers can inspect what was delegated without needing to decrypt model-delivery ciphertext.

Additional information

Related PR/issues:

The goal is not necessarily to revert encrypted delivery. The concern is that encrypted delivery should not fully remove local human auditability for subagent delegation.

Current status (2026-07-22)

This remains unresolved on current upstream main:

  • spawn_agent, send_message, and followup_task still expose only an encrypted message field.
  • All three handlers still construct InterAgentCommunication::new_encrypted(), leaving readable content empty.
  • The structured communication log still substitutes encrypted_content into its content field when plaintext is absent.
  • Rollout-trace still uses encrypted delivery content as message_content.
  • list_agents no longer exposes the most recent task/message.

A complete fork implementation now covers all three tools and makes delivery policy explicit:

[features.multi_agent_v2]
message_delivery = "plaintext" # encrypted | encrypted_with_audit | plaintext
  • encrypted preserves the current upstream opaque behavior.
  • encrypted_with_audit keeps encrypted recipient delivery and requires a separate readable task_message.
  • plaintext emits and persists one readable message, avoiding duplicate model output.

The implementation:

  • validates the selected schema/arguments before agent lookup or spawn;
  • applies one combined 8 KiB hard limit to each communication payload;
  • keeps the audit copy out of encrypted recipient-model input;
  • persists readable content in parent tool arguments and InterAgentCommunication.content;
  • prevents ciphertext from being presented as readable communication-log or rollout-trace content;
  • restores last_task_message for live list_agents inspection and serializes mailbox delivery with metadata updates;
  • preserves encrypted/plaintext communication representation through rollout resume and fork handling;
  • carries readable V2 spawn/follow-up text through SubAgentActivity into the parent TUI instead of showing only Started /root/child;
  • projects child-side ResponseItem::AgentMessage records into typed app-server/TUI history with sender attribution, stable IDs, encrypted placeholders, resume/pagination support, and duplicate suppression;
  • covers spawn_agent, send_message, and followup_task, including encrypted-only, encrypted-with-audit, and plaintext modes.

Current implementation commits:

Source analysis

Upstream InterAgentCommunication::new_encrypted() deliberately initializes content as an empty string and stores the payload only in encrypted_content:

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]
pub struct InterAgentCommunication {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub id: Option<ResponseItemId>,
pub author: AgentPath,
pub recipient: AgentPath,
#[serde(default)]
pub other_recipients: Vec<AgentPath>,
pub content: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub encrypted_content: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub internal_chat_message_metadata_passthrough: Option<InternalChatMessageMetadataPassthrough>,
pub trigger_turn: bool,
}
impl InterAgentCommunication {
pub fn new(
author: AgentPath,
recipient: AgentPath,
other_recipients: Vec<AgentPath>,
content: String,
trigger_turn: bool,
) -> Self {
Self {
id: None,
author,
recipient,
other_recipients,
content,
encrypted_content: None,
internal_chat_message_metadata_passthrough: None,
trigger_turn,
}
}
pub fn new_encrypted(
author: AgentPath,
recipient: AgentPath,
other_recipients: Vec<AgentPath>,
encrypted_content: String,
trigger_turn: bool,
) -> Self {
Self {
id: None,
author,
recipient,
other_recipients,
content: String::new(),
encrypted_content: Some(encrypted_content),
internal_chat_message_metadata_passthrough: None,
trigger_turn,
}

The conversion used for recipient history then emits only the encrypted payload whenever encrypted_content is present. Merely populating the runtime content field would therefore not create a readable persisted ResponseItem; the fix also needs an explicit local audit persistence path:

pub fn to_model_input_item(&self) -> ResponseItem {
let content = match &self.encrypted_content {
Some(encrypted_content) => {
let message_type = if self.trigger_turn {
"NEW_TASK"
} else {
"MESSAGE"
};
vec![
AgentMessageInputContent::InputText {
text: format!(
"Message Type: {message_type}\nTask name: {}\nSender: {}\nPayload:\n",
self.recipient, self.author
),
},
AgentMessageInputContent::EncryptedContent {
encrypted_content: encrypted_content.clone(),
},
]
}
None => vec![AgentMessageInputContent::InputText {
text: self.content.clone(),
}],
};
ResponseItem::AgentMessage {
id: self.id.clone(),
author: self.author.to_string(),
recipient: self.recipient.to_string(),
content,
internal_chat_message_metadata_passthrough: self
.internal_chat_message_metadata_passthrough
.clone(),
}

The current v2 message helper constructs encrypted communication with empty plaintext content:

pub(super) fn communication_from_tool_message(
author: AgentPath,
recipient: AgentPath,
message: String,
) -> InterAgentCommunication {
InterAgentCommunication::new_encrypted(
author,
recipient,
Vec::new(),
message,
/*trigger_turn*/ true,
)

send_message and followup_task still deserialize only target plus the encrypted message, then pass that ciphertext directly through the shared helper. There is no plaintext companion available to persist:

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
/// Input for the MultiAgentV2 `send_message` tool.
pub(crate) struct SendMessageArgs {
pub(crate) target: String,
pub(crate) message: String,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
/// Input for the MultiAgentV2 `followup_task` tool.
pub(crate) struct FollowupTaskArgs {
pub(crate) target: String,
pub(crate) message: String,
}
pub(super) fn message_content(message: String) -> Result<String, FunctionCallError> {
if message.trim().is_empty() {
return Err(FunctionCallError::RespondToModel(
"Empty message can't be sent to an agent".to_string(),
));
}
Ok(message)
}
/// Handles the shared MultiAgentV2 message flow for both `send_message` and `followup_task`.
pub(crate) async fn handle_message_string_tool(
invocation: ToolInvocation,
mode: MessageDeliveryMode,
target: String,
message: String,
) -> Result<FunctionToolOutput, FunctionCallError> {
let message = message_content(message)?;

let author = turn
.session_source
.get_agent_path()
.unwrap_or_else(AgentPath::root);
let communication =
communication_from_tool_message(author, receiver_agent_path.clone(), message);
let kind = match mode {
MessageDeliveryMode::QueueOnly => AgentCommunicationKind::Message,
MessageDeliveryMode::TriggerTurn => AgentCommunicationKind::Followup,
};
let context = AgentCommunicationContext::new(kind, session.thread_id);
let result = session
.services
.agent_control
.send_inter_agent_communication(receiver_thread_id, mode.apply(communication), context)
.await

The receiver records the model-facing ResponseItem produced by to_model_input_item(). For encrypted communication that item contains the encrypted delivery payload, not readable audit text:

pub(crate) async fn record_inter_agent_communication(
&self,
turn_context: &TurnContext,
mut communication: InterAgentCommunication,
) {
communication.set_turn_id_if_missing(&turn_context.sub_id);
let response_item = communication.to_model_input_item();
let items = self.prepare_conversation_items_for_history(
turn_context,
std::slice::from_ref(&response_item),
);
let items = items.as_ref();
let response_item = items[0].clone();
{
let mut state = self.state.lock().await;
state.current_time_reminder.note_recorded_items(items);
state.record_items(
items.iter(),
turn_context.model_info.truncation_policy.into(),
);
}
self.persist_rollout_items(&[

The structured communication log has the same fallback: when content is empty, it records encrypted_content as the event content:

pub(crate) fn emit_agent_communication_send(
communication_id: &str,
context: &AgentCommunicationContext,
communication: &InterAgentCommunication,
receiver_thread_id: ThreadId,
) {
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 = if communication.content.is_empty() {
communication.encrypted_content.as_deref().unwrap_or_default()
} else {
communication.content.as_str()
},
},
"agent communication"
);

Why changing the V2 tool schema is necessary but not sufficient for the TUI

The tool-schema/handler change makes readable text exist locally again, but current upstream main still drops that text before the relevant parent and child transcript surfaces.

For the parent view, upstream's SubAgentActivityEvent carries only the event ID, child ID/path, and activity kind. It has no task/prompt field:

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
pub struct SubAgentActivityEvent {
pub event_id: String,
#[serde(default)]
pub occurred_at_ms: i64,
/// Thread ID of the affected sub-agent.
pub agent_thread_id: ThreadId,
/// Canonical v2 path of the affected sub-agent.
pub agent_path: AgentPath,
pub kind: SubAgentActivityKind,
}

The app-server ThreadItem::SubAgentActivity likewise exposes no prompt:

#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
SubAgentActivity {
id: String,
kind: SubAgentActivityKind,
agent_thread_id: String,
agent_path: String,
},

The upstream TUI therefore renders only Started, Interacted with, or Interrupted plus the agent path, with an empty detail list:

pub(crate) fn sub_agent_activity_history_cell(item: &ThreadItem) -> Option<PlainHistoryCell> {
let ThreadItem::SubAgentActivity {
kind, agent_path, ..
} = item
else {
return None;
};
Some(collab_event(
sub_agent_activity_title(*kind, agent_path),
Vec::new(),
))
}
pub(crate) fn sub_agent_activity_summary(kind: SubAgentActivityKind, agent_path: &str) -> String {
match kind {
SubAgentActivityKind::Started => format!("Started `{agent_path}`"),
SubAgentActivityKind::Interacted => format!("Interacted with `{agent_path}`"),
SubAgentActivityKind::Interrupted => format!("Interrupted `{agent_path}`"),

For the child view, readable plaintext can already be present canonically as ResponseItem::AgentMessage, but upstream thread-history reconstruction ignores InterAgentCommunication records and its generic response-item handler accepts only hook-prompt user messages:

pub fn handle_rollout_item(&mut self, item: &RolloutItem) {
self.current_rollout_index = self.next_rollout_index;
self.next_rollout_index += 1;
match item {
RolloutItem::EventMsg(event) => self.handle_event(event),
RolloutItem::Compacted(payload) => self.handle_compacted(payload),
RolloutItem::ResponseItem(item) => self.handle_response_item(item),
RolloutItem::InterAgentCommunication(_)
| RolloutItem::InterAgentCommunicationMetadata { .. }
| RolloutItem::TurnContext(_)
| RolloutItem::WorldState(_)
| RolloutItem::SessionMeta(_) => {}
}

fn handle_response_item(&mut self, item: &codex_protocol::models::ResponseItem) {
let codex_protocol::models::ResponseItem::Message {
role, content, id, ..
} = item
else {
return;
};
if role != "user" {
return;

Live raw response items are emitted only through the separate rawResponseItem/* notification path; that is not the normal typed thread-item stream consumed by the TUI and app-server clients:

EventMsg::RawResponseItem(raw_response_item_event) => {
maybe_emit_raw_response_item_completed(
conversation_id,
&event_turn_id,
raw_response_item_event.item,
&outgoing,
)
.await;
}

async fn maybe_emit_raw_response_item_completed(
conversation_id: ThreadId,
turn_id: &str,
item: codex_protocol::models::ResponseItem,
outgoing: &ThreadScopedOutgoingMessageSender,
) {
let notification = RawResponseItemCompletedNotification {
thread_id: conversation_id.to_string(),
turn_id: turn_id.to_string(),
item,
};
outgoing
.send_server_notification(ServerNotification::RawResponseItemCompleted(notification))
.await;

So a complete local fix has two layers:

  1. Make V2 communication readable under an explicit delivery policy and persist that representation.
  2. Carry readable parent activity prompts and child AgentMessage records through protocol conversion, app-server history/live notifications, resume/pagination, and TUI rendering.

The fork implements the second layer in the final three commits linked above. Plaintext messages render with sender attribution; encrypted-only messages render a fixed Input message encrypted placeholder rather than ciphertext. The durable projection is fork-owned and rebuildable from canonical plain or compressed rollouts so the same SQLite cache remains compatible with upstream Codex.

Implementation / fix spec

A concrete implementation can preserve encrypted delivery and restore a local audit trail:

  1. Keep the existing encrypted message field as the delivery payload.
  2. Add a required, non-encrypted plaintext companion to each v2 communication tool:
    • spawn_agent: task_message
    • send_message and followup_task: a consistently named plaintext audit field, such as task_message or message_text
  3. Reject empty plaintext audit values at the handler boundary.
  4. Construct InterAgentCommunication with both:
    • encrypted_content set to the encrypted message
    • content set to the plaintext audit copy
  5. Keep to_model_input_item() behavior unchanged so the recipient model still receives ciphertext, not the local audit copy.
  6. Persist the plaintext companion in the parent tool invocation/rollout and retain it in structured trace edges and local communication logs.
  7. Match tool calls to delivered child items using ciphertext/IDs, not plaintext equality. The plaintext field is audit metadata and should not replace the encrypted delivery identity.
  8. Bound the plaintext audit field with the same hard size limit as the corresponding delegated message so the new rollout/context item cannot grow without limit.
  9. Add readable prompt content to V2 sub-agent activity items and render it with a bounded TUI preview.
  10. Convert persisted and live ResponseItem::AgentMessage records into normal typed thread items with sender attribution, stable IDs, and explicit encrypted-content redaction.
  11. Preserve ordering and suppress duplicates across thread read/resume, pagination, running-thread subscription handoff, and agent switching.

The fork implementation linked in the current-status section now applies this contract consistently to spawn_agent, send_message, and followup_task, including local inspection, communication logs, and rollout-trace reduction.

Acceptance criteria

  • Parent rollout/history shows the readable text for v2 spawn_agent, send_message, and followup_task.
  • Parent TUI activity rows show the readable task/follow-up text rather than only the child path.
  • Child app-server/TUI history shows the same readable instruction with sender attribution; encrypted-only delivery shows a non-sensitive placeholder, never ciphertext.
  • The child model still receives only the encrypted delivery payload when encryption is enabled.
  • Structured rollout-trace interaction edges carry bounded plaintext message_content.
  • Communication logs use plaintext audit content when present and never substitute ciphertext into a field presented as readable message text.
  • Live notifications, thread/read, resume, pagination, and agent switching preserve one ordered copy of each communication.
  • Resume/replay preserves the audit copy without injecting it into the child model context, and remains compatible with canonical plain and compressed rollouts.
  • Existing plaintext v1 communication behavior is unchanged.
  • Regression tests cover all three v2 tools and assert both sides of the contract: readable local audit data and encrypted recipient-model input, plus parent/child TUI rendering and live/resumed app-server history.

Activity

  1. added
    bugSomething isn't working
    CLIIssues related to the Codex CLI
    subagentIssues involving subagents or multi-agent features
    on Jun 13, 2026
  2. ignatremizov commented on Jun 13, 2026

    @ignatremizov
    Author

    Guys, we don’t want to build Skynet and then be unable to audit what it’s doing.

  3. septrcode commented on Jun 14, 2026

    @septrcode

    I too would like to be able to audit my agents… if we can’t audit agents… only providers can… it’s clear we are on a path to enterprise only has access to these tools, why restrict otherwise

  4. Alek2077 commented on Jul 4, 2026

    @Alek2077

    The auditability concern is no longer meaningfully opt-in for GPT-5.5 users on Codex 0.142.5.

    [now applies to Sol and Terra, while 5.5 has been reverted to using v1 again]

    The current GPT-5.5 model catalog sets multi_agent_version: v2, and the 0.142.5 runtime gives that value precedence over features.multi_agent_v2=false. In a fresh GPT-5.5 thread, features list says V2 is disabled, but spawn_agent still exposes the V2 task-path schema. A fresh GPT-5.4 thread with the same configuration uses V1 normally.

    Because GPT-5.5 forces the encrypted V2 path, users selecting the current frontier model cannot choose the readable V1 delegation trail through the documented feature setting. That turns the loss of locally readable delegated-task content from an experimental opt-in tradeoff into mandatory behavior for that model.

    The same forced surface also prevents selection of documented custom-agent profiles because it hides the agent type, model, and reasoning controls. This compounds the auditability problem: GPT-5.5 users cannot select the configured reviewer profile and also cannot inspect the plaintext task that a generic replacement received.

    I have filed the configuration-precedence reproduction separately: #31097

  5. ignatremizov commented on Jul 7, 2026

    @ignatremizov
    Author

    Upd: #30867, and #30872 by @bolinfest and @rka-oai are directionally related, but they don’t address this issue yet.

    The PRs add/prepare structured lifecycle logging for multi-agent communication, which is useful for operational observability. The gap here is specifically local auditability of the actual task/message text in rollout/history/replay/debug surfaces.

    For encrypted MultiAgentV2 messages, the new logging path still falls back to encrypted_content when plaintext content is empty, so it will record an opaque encrypted payload rather than the human-readable task the model delegated.

    A reminder that the desired behavior here is narrow: keep encrypted delivery if needed for some reason, and also persist a human-readable structured audit copy of spawn_agent / send_message / followup_task task text in the parent rollout history so users can later answer "what did this agent get asked to do?" without relying on provider-side visibility or opt-in trace logging.

  6. runfence commented on Jul 14, 2026

    @runfence

    Please revert this! I need to be able to inspect those messages to debug my multi-agent workflows.

    Who thought this is a good idea???

  7. embedding-shapes commented on Jul 14, 2026

    @embedding-shapes

    I'm also being hit by this, playing around with GPT 5.6 Sol Ultra (which spawns sub-agents) and there is no way to introspect exactly what the main model sent the sub-agents, what on earth?! We have to be able to introspect exactly what inputs inference use, otherwise how are we supposed to be able to understand what went wrong, when things go wrong?

    This seems like a completely bananas thing to implement, encrypt local content not to protect end-users, but to protect OpenAI from other providers extracting how the multi-agent stuff works? This basically makes the whole flashy new "Ultra" stuff unusable, as we have 0 introspection into what instructions they actually received when they started.

    I came across the encrypted stuff some weeks or months ago even, thought it was just experimental things or things for enterprise users who sometimes want encrypted-at-rest stuff. Imagine my surprise when I now tried to integrate with the new stuff via app-server and y'all are trying to hide the actual prompts from us paying users?!

    Please carefully consider resolving this as otherwise it becomes really hard to justify continuing to use codex as you're moving towards hiding stuff from us users rather than making it easier to work with codex.

  8. Aquaticat commented on Jul 14, 2026

    @Aquaticat

    My current workaround: Not using subagents at all.

  9. ZenulAbidin commented on Jul 14, 2026

    @ZenulAbidin

    My current workaround: Not using subagents at all.

    Sorry to shamelessly plug my project in, but I just created stringbean to solve this problem.

    You can use it to run your own agents in Codex, even non-OpenAI models.

  10. cooluser69 commented on Jul 14, 2026

    @cooluser69

    This is bad!

  11. runfence commented on Jul 14, 2026

    @runfence

    Moreover it doesn't really protect you from competition.

    1. There are ways to bypass it (I will not share them but it's easy)
    2. Anyone can fork and create their own client-side agents implementation that doesn't encrypt it cause agents don't really require any back-end functionality
  12. ignatremizov commented on Jul 15, 2026

    @ignatremizov
    Author

    Update: #33030 removed last_task_message from list_agents and stopped tracking the most recent instruction in agent metadata. This makes the auditability problem described here even more significant: users can see that an agent exists and what status it has, but not what it was asked to do anymore at all.

    Switching to the child thread does not fully replace this information because as mentioned, encrypted v2 task delivery is stored as opaque encrypted_content; the original readable delegation is not available in the local rollout for agent switching.

    The #33030 description explains what was removed, but not why. Was this motivated by privacy, context size, correctness, or another constraint? Some attempt at building a moat? Anti-distillation? It's strange.

    Agent inspection is operationally important for the average user. It should remain possible to answer:

    • What was this agent originally asked to do?
    • What follow-up task or message did it most recently receive?
    • Which readable instruction corresponds to an encrypted delivery?

    One possible design is a configurable delivery policy:

    • encrypted only;
      • current design, not usable, but maybe some bad actors would want their agents to be inscrutable once they've infected a machine
    • encrypted delivery plus a plaintext audit copy;
      • burn more tokens, but could allow performance gains from encrypted deliverability if what I've understood about how OpenAI remote compaction works and they build something similar for inter-agent communication
    • plaintext only.

    This would preserve encrypted behavior while allowing users who prioritize local auditability or token efficiency to choose the appropriate tradeoff. At minimum, I think a readable task description should remain available somewhere in history, trace metadata, and/or list_agents.

  13. 15 remaining items

  14. 34262315716 commented on Jul 24, 2026

    @34262315716

    Strongly agree with this issue. The encrypted MultiAgentV2 messages effectively lock users into OpenAI's proprietary model ecosystem on an allegedly open-source project. If the goal is to push users toward paying for OpenAI models, why not just be upfront about it and make it a documented Codex Pro feature? At least that would be honest. But hiding vendor lock-in behind 'encryption for security' in an open-source project is a terrible look. This is not what open source spirit is about.

  15. ignatremizov commented on Jul 29, 2026

    @ignatremizov
    Author

    Update: Merged PR #35845 is a substantial partial fix and confirms that plaintext MultiAgent V2 delivery is supported by the local Codex orchestration path.

    When Responses returns a collaboration function call with encrypted_function_args: [], Codex now classifies it as a plaintext collaboration call. spawn_agent, send_message, and followup_task then construct a plaintext InterAgentCommunication, and the child receives a structured plaintext AgentMessage containing the message type, task name, sender, and payload. The marker and readable function arguments are also preserved across request replay.

    Relevant implementation:

    pub tool_name: ToolName,
    pub call_id: String,
    pub payload: ToolPayload,
    pub encrypted_function_args: Option<Vec<String>>,
    }
    impl ToolCall {
    pub(crate) fn direct_source(&self) -> ToolCallSource {
    if self.tool_name.namespace.as_deref() == Some("collaboration")
    && matches!(
    self.tool_name.name.as_str(),
    "spawn_agent" | "send_message" | "followup_task"
    )
    && self
    .encrypted_function_args
    .as_ref()
    .is_some_and(Vec::is_empty)
    {
    ToolCallSource::DirectPlaintextMessage
    } else {
    ToolCallSource::Direct
    }
    }
    }
    pub(crate) fn tool_log_payload<'a>(
    payload: &'a ToolPayload,
    source: &ToolCallSource,
    ) -> Cow<'a, str> {
    if matches!(source, ToolCallSource::DirectPlaintextMessage) {
    return Cow::Borrowed("[plaintext arguments]");
    }
    payload.log_payload()
    }

    fn communication_from_tool_message(
    author: AgentPath,
    recipient: AgentPath,
    message: String,
    source: &crate::tools::context::ToolCallSource,
    trigger_turn: bool,
    ) -> InterAgentCommunication {
    if !matches!(
    source,
    crate::tools::context::ToolCallSource::DirectPlaintextMessage
    ) {
    return InterAgentCommunication::new_encrypted(
    author,
    recipient,
    Vec::new(),
    message,
    trigger_turn,
    );
    }
    let message_type = if trigger_turn {
    InterAgentMessageType::NewTask
    } else {
    InterAgentMessageType::Message
    };
    let content =
    InterAgentMessage::new(message_type, recipient.clone(), author.clone(), message).render();
    InterAgentCommunication::new(author, recipient, Vec::new(), content, trigger_turn)
    }

    impl ContextualUserFragment for InterAgentMessage {
    fn role(&self) -> &'static str {
    "assistant"
    }
    fn markers(&self) -> (&'static str, &'static str) {
    Self::type_markers()
    }
    fn type_markers() -> (&'static str, &'static str) {
    ("", "")
    }
    fn body(&self) -> String {
    format!(
    "Message Type: {}\nTask name: {}\nSender: {}\nPayload:\n{}",
    self.message_type.as_str(),
    self.task_name,
    self.sender,
    self.payload,
    )
    }
    }

    This resolves the most important underlying point: the parent-generated task can now remain plaintext through the local parent-to-child handoff. It is not yet a complete user-visible audit trail, however:

    • Plaintext selection is signaled by the provider response rather than exposed as a user-configurable Codex policy.
    • Tool and communication logs deliberately redact the readable arguments as [plaintext arguments] / [plaintext].
    • SubAgentActivity still carries no prompt, so the parent TUI still renders only Started /root/child with an empty detail list.
    • Normal app-server thread-history reconstruction still ignores InterAgentCommunication and does not project persisted ResponseItem::AgentMessage records into typed thread history.
    • Consequently, switching to the child still does not show its incoming task through the normal TUI transcript, even though that task can now exist as plaintext in the canonical rollout.

    Current upstream TUI/history paths:

    pub(crate) fn sub_agent_activity_history_cell(item: &ThreadItem) -> Option<PlainHistoryCell> {
    let ThreadItem::SubAgentActivity {
    kind, agent_path, ..
    } = item
    else {
    return None;
    };
    Some(collab_event(
    sub_agent_activity_title(*kind, agent_path),
    Vec::new(),
    ))

    match item {
    RolloutItem::EventMsg(event) => self.handle_event(event),
    RolloutItem::Compacted(payload) => self.handle_compacted(payload),
    RolloutItem::ResponseItem(item) => self.handle_response_item(item),
    RolloutItem::InterAgentCommunication(_)
    | RolloutItem::InterAgentCommunicationMetadata { .. }
    | RolloutItem::TurnContext(_)
    | RolloutItem::WorldState(_)
    | RolloutItem::SessionMeta(_) => {}
    }

    So I would consider #35845 a substantial partial resolution of this issue. The remaining work is to carry the now-readable task through SubAgentActivity, typed app-server live/history/resume paths, and parent/child TUI rendering, with sender attribution and encrypted-content redaction.

  16. 92645417d9e5c763259dbebc306e3e commented on Jul 29, 2026

    @92645417d9e5c763259dbebc306e3e

    These modifications primarily allow third-party models to use the multi-agent v2 tool; the type of instruction returned is always determined by the server side.

  17. wuyak commented on Aug 9, 2026

    @wuyak

    A current Codex App reproduction shows an output-side consequence of this issue, not just missing delegated input.

    • A child sent a detailed evidence capsule to the parent via send_message.
    • The child then returned a one-line terminal result: Evidence capsule delivered to /root: ....
    • The parent rollout received both an encrypted child-to-parent AgentMessage and the separate short FINAL_ANSWER.
    • The Subagent inspector showed only the short terminal result, so the user could not inspect the substantive handoff.
    • When the same child later returned a detailed terminal result, the inspector displayed it normally. This indicates that the App is not merely truncating the result; it is not projecting the inter-agent handoff into the visible child history.

    The expanded inspector should show an ordered operational transcript: initial assignment, parent-to-child follow-ups, child-to-parent messages/evidence capsules, and the terminal result, with sender, recipient, timestamp, operation type, and delivery state. A collapsed row could summarize something like Completed · 57s · Handoff sent · 2 blockers · 10 findings.

    This does not require chain-of-thought exposure. It is about user-visible messages that agents explicitly send to each other. The same representation should survive live view, history, resume, and replay. Users should not have to duplicate a full capsule into the terminal result solely to make it visible in the App.

    Related UI reports: #32125 covers missing orchestrator/subagent communications, and #36266 proposes a richer expandable Subagents panel.

  18. fwf-ben commented on Aug 18, 2026

    @fwf-ben

    Just got bitten by this: can't run evals against the subagent rollouts, without their prompt can't re-run them or improve them. Why was this done in the first place? Seems a crazy regression.

  19. arthurlapertosa commented on Aug 28, 2026

    @arthurlapertosa

    This is ridiculous, we have to be able to view and inspect our subagents too. I mean, it's part of the machine that will build your solution, so you gotta be able to view it. That's so crazy that they encrypted it.

  20. embedding-shapes commented on Aug 28, 2026

    @embedding-shapes

    so you gotta be able to view it. That's so crazy that they encrypted it.

    Yeah, I'm not sure what they were thinking.

    I want to use ultra, but since it starts spawning sub-agents, and I can't inspect their prompts, I literally don't dare to use it. So I'm stuck at max, because I want to be able to see what the agents are doing.

    They're literally hiding stuff we need for debugging, and leaving money on the table while doing so. Really strange decision overall.

  21. VolkumDev commented on Aug 30, 2026

    @VolkumDev

    OpenAI team will stop being so incompetent some day?? Holy fuck

  22. jiahao-d commented on Sep 1, 2026

    @jiahao-d

    Also reproduced on Codex desktop for Windows with bundled codex-cli 0.151.0-alpha.7.2. I used subagents for an independent evaluation and needed to confirm that two runs received exactly the same task, without leading language or a leaked expected answer. Codex stored only ciphertext in both the parent and child histories, so I could not compare the delegated tasks or determine whether different results came from model variance or different instructions. This makes the evaluation impossible to audit or reproduce.

  23. future3OOO commented on Sep 14, 2026

    @future3OOO

    This was generated by AI during triage.

    Additional observation on Codex CLI 0.154.0, Linux/WSL2, OpenAI provider, gpt-5.6-sol, Multi-Agent V2 (2026-09-14 UTC).

    During a real repository task, the parent spawned three explorer agents with fork_turns="all". We needed to verify whether the parent supplied the required task contract, scoped context summary, target files and GitNexus repository/status, because two delegates repeated a graph query and one printed a large truncated response.

    Read-only inspection of the parent rollout, all three child rollouts, native log database and thread-history projection found:

    • The parent spawn_agent.message values are ciphertext.
    • Each child's NEW_TASK contains a readable sender/recipient header followed by encrypted_content.
    • Native InterAgentCommunication logs show empty plaintext content and populated encrypted_content.
    • Parent subAgentActivity history entries contain lifecycle/identity metadata, without the assignment text. No readable assignment was recovered from these inspected surfaces.
    • Delegates performed task-related work; one explicitly referred to the supplied context packet. This is an exact-input audit gap, not a demonstrated delivery failure.

    Consequently, we could confirm their actions but could not establish whether the exact dispatch omitted constraints or whether the delegates disregarded adequate instructions. A model-authored retrospective summary would not resolve that distinction.

    Please provide or identify a supported, authorized way to inspect the exact dispatched assignments and follow-ups, correlated to sender/recipient and execution. For a fix, compare real initial/follow-up dispatch on unchanged and candidate runtimes: the exact input must become auditable while recipient behavior and existing permission boundaries remain preserved. No new workflow stages or duplicate model-authored message logs are needed.

    This corroborates this issue; it does not establish compromise, unauthorized background activity, or malicious intent. No credentials, ciphertext payloads, or private task contents are included here.

  24. pedroyan commented on Sep 17, 2026

    @pedroyan

    +1 on this issue. If I cannot inspect the prompt that the parent agent is passing to its sub-agents, I can't be certain that the correct task is being executed.

    For all I know, they could be secretly cooking Skynet on my machine without me even knowing

  25. nleononline commented on Sep 20, 2026

    @nleononline

    Two and a half months later, this still hasn't been fixed...

  26. xlionjuan commented on Oct 13, 2026

    @xlionjuan

    Still NOTHING

  27. embedding-shapes commented on Oct 13, 2026

    @embedding-shapes

    @tibo-openai how on earth are we supposed to be able to use "ultra" or sub-agents even, when we're not able to troubleshoot when things invariably go wrong? It's impossible to figure out why a sub-agent messed up because we're unable to see the prompts the "root" session sent, so what really is the point of even having sub-agents or "ultra", when any serious programmer would be unable to use it?

    You have this "super" feature that consumes a ton more usage, yet you're not caring enough about the feature to actually allow us to us it, what's the point?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    CLIIssues related to the Codex CLIbugSomething isn't workingsubagentIssues involving subagents or multi-agent features

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions