Make MultiAgentV2 interruption markers assistant-authored (#19124)

## Why

`MultiAgentV2` follow-up messages are delivered to agents as
assistant-authored `InterAgentCommunication` envelopes. When
`followup_task` used `interrupt: true`, the interrupted-turn guidance
was still persisted as a contextual user message, so model-visible
history made a system-generated interruption boundary look
user-authored.

This keeps interruption guidance consistent with the rest of the v2
inter-agent message stream while preserving the legacy marker shape for
non-v2 sessions.

## What changed

- Make `interrupted_turn_history_marker` feature-aware.
- Record the interrupted-turn marker as an assistant `OutputText`
message when `Feature::MultiAgentV2` is enabled.
- Keep the existing user contextual fragment for non-v2 sessions.
- Apply the same feature-aware marker to interrupted fork snapshots.
- Add coverage for the live `followup_task` interrupt path and the
helper-level v2 marker shape.

## Testing

- `cargo test -p codex-core
multi_agent_v2_followup_task_interrupts_busy_child_without_losing_message
-- --nocapture`
- `cargo test -p codex-core
multi_agent_v2_interrupted_marker_uses_assistant_output_message --
--nocapture`
- `cargo test -p codex-core interrupted_fork_snapshot -- --nocapture`
This commit is contained in:
jif-oai
2026-04-24 13:39:26 +02:00
committed by GitHub
Unverified
parent 21463a5074
commit 120aa07d81
5 changed files with 136 additions and 21 deletions
+19 -5
View File
@@ -22,6 +22,7 @@ use codex_analytics::AnalyticsEventsClient;
use codex_app_server_protocol::ThreadHistoryBuilder;
use codex_app_server_protocol::TurnStatus;
use codex_exec_server::EnvironmentManager;
use codex_features::Feature;
use codex_login::AuthManager;
use codex_login::CodexAuth;
use codex_model_provider::create_model_provider;
@@ -745,6 +746,7 @@ impl ThreadManager {
let snapshot = snapshot.into();
let history = RolloutRecorder::get_rollout_history(&path).await?;
let snapshot_state = snapshot_turn_state(&history);
let multi_agent_v2_enabled = config.features.enabled(Feature::MultiAgentV2);
let history = match snapshot {
ForkSnapshot::TruncateBeforeNthUserMessage(nth_user_message) => {
truncate_before_nth_user_message(history, nth_user_message, &snapshot_state)
@@ -757,7 +759,11 @@ impl ThreadManager {
InitialHistory::Resumed(resumed) => InitialHistory::Forked(resumed.history),
};
if snapshot_state.ends_mid_turn {
append_interrupted_boundary(history, snapshot_state.active_turn_id)
append_interrupted_boundary(
history,
snapshot_state.active_turn_id,
multi_agent_v2_enabled,
)
} else {
history
}
@@ -1225,7 +1231,11 @@ fn snapshot_turn_state(history: &InitialHistory) -> SnapshotTurnState {
/// Append the same persisted interrupt boundary used by the live interrupt path
/// to an existing fork snapshot after the source thread has been confirmed to
/// be mid-turn.
fn append_interrupted_boundary(history: InitialHistory, turn_id: Option<String>) -> InitialHistory {
fn append_interrupted_boundary(
history: InitialHistory,
turn_id: Option<String>,
multi_agent_v2_enabled: bool,
) -> InitialHistory {
let aborted_event = RolloutItem::EventMsg(EventMsg::TurnAborted(TurnAbortedEvent {
turn_id,
reason: TurnAbortReason::Interrupted,
@@ -1235,18 +1245,22 @@ fn append_interrupted_boundary(history: InitialHistory, turn_id: Option<String>)
match history {
InitialHistory::New | InitialHistory::Cleared => InitialHistory::Forked(vec![
RolloutItem::ResponseItem(interrupted_turn_history_marker()),
RolloutItem::ResponseItem(interrupted_turn_history_marker(multi_agent_v2_enabled)),
aborted_event,
]),
InitialHistory::Forked(mut history) => {
history.push(RolloutItem::ResponseItem(interrupted_turn_history_marker()));
history.push(RolloutItem::ResponseItem(interrupted_turn_history_marker(
multi_agent_v2_enabled,
)));
history.push(aborted_event);
InitialHistory::Forked(history)
}
InitialHistory::Resumed(mut resumed) => {
resumed
.history
.push(RolloutItem::ResponseItem(interrupted_turn_history_marker()));
.push(RolloutItem::ResponseItem(interrupted_turn_history_marker(
multi_agent_v2_enabled,
)));
resumed.history.push(aborted_event);
InitialHistory::Forked(resumed.history)
}