diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index acfdfb921..7303fa1ca 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -535,6 +535,7 @@ "enum": [ "pendingInit", "running", + "interrupted", "completed", "errored", "shutdown", diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 720f7b0e7..124f21a14 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -5691,6 +5691,7 @@ "enum": [ "pendingInit", "running", + "interrupted", "completed", "errored", "shutdown", diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 25d688373..b726a1876 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -2292,6 +2292,7 @@ "enum": [ "pendingInit", "running", + "interrupted", "completed", "errored", "shutdown", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json index 4a8358944..b447fc339 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json @@ -41,6 +41,7 @@ "enum": [ "pendingInit", "running", + "interrupted", "completed", "errored", "shutdown", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json index 4940cb19a..1b02f4418 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json @@ -41,6 +41,7 @@ "enum": [ "pendingInit", "running", + "interrupted", "completed", "errored", "shutdown", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json index 15b66e990..b8e83ba34 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json @@ -155,6 +155,7 @@ "enum": [ "pendingInit", "running", + "interrupted", "completed", "errored", "shutdown", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index e0f1bd351..e57c84b46 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -217,6 +217,7 @@ "enum": [ "pendingInit", "running", + "interrupted", "completed", "errored", "shutdown", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json index 341069d24..f9f943055 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json @@ -155,6 +155,7 @@ "enum": [ "pendingInit", "running", + "interrupted", "completed", "errored", "shutdown", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json index f502f7620..c652c1cb4 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json @@ -155,6 +155,7 @@ "enum": [ "pendingInit", "running", + "interrupted", "completed", "errored", "shutdown", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json index a902b747e..b0ca838cd 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json @@ -155,6 +155,7 @@ "enum": [ "pendingInit", "running", + "interrupted", "completed", "errored", "shutdown", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index 84fb084f5..6809f9715 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -217,6 +217,7 @@ "enum": [ "pendingInit", "running", + "interrupted", "completed", "errored", "shutdown", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json index 2207ea5d0..2288caa50 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json @@ -155,6 +155,7 @@ "enum": [ "pendingInit", "running", + "interrupted", "completed", "errored", "shutdown", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index bbe829dc1..e994a2b00 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -217,6 +217,7 @@ "enum": [ "pendingInit", "running", + "interrupted", "completed", "errored", "shutdown", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json index aa821520d..3eabf9eeb 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json @@ -155,6 +155,7 @@ "enum": [ "pendingInit", "running", + "interrupted", "completed", "errored", "shutdown", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json index de94a4ced..f6738ff21 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json @@ -155,6 +155,7 @@ "enum": [ "pendingInit", "running", + "interrupted", "completed", "errored", "shutdown", diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json index 7f49860e8..079a81ad0 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json @@ -155,6 +155,7 @@ "enum": [ "pendingInit", "running", + "interrupted", "completed", "errored", "shutdown", diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json index 5a5117502..17f04c51d 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json @@ -155,6 +155,7 @@ "enum": [ "pendingInit", "running", + "interrupted", "completed", "errored", "shutdown", diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json index 3800ee102..59171e42d 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json @@ -155,6 +155,7 @@ "enum": [ "pendingInit", "running", + "interrupted", "completed", "errored", "shutdown", diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/CollabAgentStatus.ts b/codex-rs/app-server-protocol/schema/typescript/v2/CollabAgentStatus.ts index 3672d19da..66d3119ba 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/CollabAgentStatus.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/CollabAgentStatus.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type CollabAgentStatus = "pendingInit" | "running" | "completed" | "errored" | "shutdown" | "notFound"; +export type CollabAgentStatus = "pendingInit" | "running" | "interrupted" | "completed" | "errored" | "shutdown" | "notFound"; diff --git a/codex-rs/app-server-protocol/src/protocol/thread_history.rs b/codex-rs/app-server-protocol/src/protocol/thread_history.rs index 65f2d99ff..6a570b7fa 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs @@ -2417,6 +2417,74 @@ mod tests { ); } + #[test] + fn reconstructs_interrupted_send_input_as_completed_collab_call() { + // `send_input(interrupt=true)` first stops the child's active turn, then redirects it with + // new input. The transient interrupted status should remain visible in agent state, but the + // collab tool call itself is still a successful redirect rather than a failed operation. + let sender = ThreadId::try_from("00000000-0000-0000-0000-000000000001") + .expect("valid sender thread id"); + let receiver = ThreadId::try_from("00000000-0000-0000-0000-000000000002") + .expect("valid receiver thread id"); + let events = vec![ + EventMsg::UserMessage(UserMessageEvent { + message: "redirect".into(), + images: None, + text_elements: Vec::new(), + local_images: Vec::new(), + }), + EventMsg::CollabAgentInteractionBegin( + codex_protocol::protocol::CollabAgentInteractionBeginEvent { + call_id: "send-1".into(), + sender_thread_id: sender, + receiver_thread_id: receiver, + prompt: "new task".into(), + }, + ), + EventMsg::CollabAgentInteractionEnd( + codex_protocol::protocol::CollabAgentInteractionEndEvent { + call_id: "send-1".into(), + sender_thread_id: sender, + receiver_thread_id: receiver, + receiver_agent_nickname: None, + receiver_agent_role: None, + prompt: "new task".into(), + status: AgentStatus::Interrupted, + }, + ), + ]; + + let items = events + .into_iter() + .map(RolloutItem::EventMsg) + .collect::>(); + let turns = build_turns_from_rollout_items(&items); + assert_eq!(turns.len(), 1); + assert_eq!(turns[0].items.len(), 2); + assert_eq!( + turns[0].items[1], + ThreadItem::CollabAgentToolCall { + id: "send-1".into(), + tool: CollabAgentTool::SendInput, + status: CollabAgentToolCallStatus::Completed, + sender_thread_id: sender.to_string(), + receiver_thread_ids: vec![receiver.to_string()], + prompt: Some("new task".into()), + model: None, + reasoning_effort: None, + agents_states: [( + receiver.to_string(), + CollabAgentState { + status: crate::protocol::v2::CollabAgentStatus::Interrupted, + message: None, + }, + )] + .into_iter() + .collect(), + } + ); + } + #[test] fn rollback_failed_error_does_not_mark_turn_failed() { let events = vec![ diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index a074ae647..524205c67 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -4542,6 +4542,7 @@ pub enum CollabAgentToolCallStatus { pub enum CollabAgentStatus { PendingInit, Running, + Interrupted, Completed, Errored, Shutdown, @@ -4567,6 +4568,10 @@ impl From for CollabAgentState { status: CollabAgentStatus::Running, message: None, }, + CoreAgentStatus::Interrupted => Self { + status: CollabAgentStatus::Interrupted, + message: None, + }, CoreAgentStatus::Completed(message) => Self { status: CollabAgentStatus::Completed, message, @@ -5886,6 +5891,17 @@ mod tests { absolute_path("readable") } + #[test] + fn collab_agent_state_maps_interrupted_status() { + assert_eq!( + CollabAgentState::from(CoreAgentStatus::Interrupted), + CollabAgentState { + status: CollabAgentStatus::Interrupted, + message: None, + } + ); + } + #[test] fn command_execution_request_approval_rejects_relative_additional_permission_paths() { let err = serde_json::from_value::(json!({ diff --git a/codex-rs/core/src/agent/control_tests.rs b/codex-rs/core/src/agent/control_tests.rs index d78c448b2..26819309a 100644 --- a/codex-rs/core/src/agent/control_tests.rs +++ b/codex-rs/core/src/agent/control_tests.rs @@ -207,7 +207,7 @@ async fn on_event_updates_status_from_turn_aborted() { reason: TurnAbortReason::Interrupted, })); - let expected = AgentStatus::Errored("Interrupted".to_string()); + let expected = AgentStatus::Interrupted; assert_eq!(status, Some(expected)); } diff --git a/codex-rs/core/src/agent/status.rs b/codex-rs/core/src/agent/status.rs index 74981513f..c343e1950 100644 --- a/codex-rs/core/src/agent/status.rs +++ b/codex-rs/core/src/agent/status.rs @@ -7,7 +7,12 @@ pub(crate) fn agent_status_from_event(msg: &EventMsg) -> Option { match msg { EventMsg::TurnStarted(_) => Some(AgentStatus::Running), EventMsg::TurnComplete(ev) => Some(AgentStatus::Completed(ev.last_agent_message.clone())), - EventMsg::TurnAborted(ev) => Some(AgentStatus::Errored(format!("{:?}", ev.reason))), + EventMsg::TurnAborted(ev) => match ev.reason { + codex_protocol::protocol::TurnAbortReason::Interrupted => { + Some(AgentStatus::Interrupted) + } + _ => Some(AgentStatus::Errored(format!("{:?}", ev.reason))), + }, EventMsg::Error(ev) => Some(AgentStatus::Errored(ev.message.clone())), EventMsg::ShutdownComplete => Some(AgentStatus::Shutdown), _ => None, @@ -15,5 +20,8 @@ pub(crate) fn agent_status_from_event(msg: &EventMsg) -> Option { } pub(crate) fn is_final(status: &AgentStatus) -> bool { - !matches!(status, AgentStatus::PendingInit | AgentStatus::Running) + !matches!( + status, + AgentStatus::PendingInit | AgentStatus::Running | AgentStatus::Interrupted + ) } diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index d50152492..b0ce0f09d 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -1260,6 +1260,7 @@ fn format_collab_status(status: &AgentStatus) -> String { match status { AgentStatus::PendingInit => "pending init".to_string(), AgentStatus::Running => "running".to_string(), + AgentStatus::Interrupted => "interrupted".to_string(), AgentStatus::Completed(Some(message)) => { let preview = truncate_preview(message.trim(), 120); if preview.is_empty() { @@ -1289,6 +1290,7 @@ fn style_for_agent_status( match status { AgentStatus::PendingInit | AgentStatus::Shutdown => processor.dimmed, AgentStatus::Running => processor.cyan, + AgentStatus::Interrupted => processor.yellow, AgentStatus::Completed(_) => processor.green, AgentStatus::Errored(_) | AgentStatus::NotFound => processor.red, } diff --git a/codex-rs/exec/src/event_processor_with_jsonl_output.rs b/codex-rs/exec/src/event_processor_with_jsonl_output.rs index 8bec82b15..6eba6eaf6 100644 --- a/codex-rs/exec/src/event_processor_with_jsonl_output.rs +++ b/codex-rs/exec/src/event_processor_with_jsonl_output.rs @@ -815,6 +815,10 @@ impl From for CollabAgentState { status: CollabAgentStatus::Running, message: None, }, + CoreAgentStatus::Interrupted => Self { + status: CollabAgentStatus::Interrupted, + message: None, + }, CoreAgentStatus::Completed(message) => Self { status: CollabAgentStatus::Completed, message, diff --git a/codex-rs/exec/src/exec_events.rs b/codex-rs/exec/src/exec_events.rs index 368098f16..d356a6a70 100644 --- a/codex-rs/exec/src/exec_events.rs +++ b/codex-rs/exec/src/exec_events.rs @@ -228,6 +228,7 @@ pub enum CollabTool { pub enum CollabAgentStatus { PendingInit, Running, + Interrupted, Completed, Errored, Shutdown, diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index fb97783b9..f1f60e163 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -1518,6 +1518,8 @@ pub enum AgentStatus { PendingInit, /// Agent is currently running. Running, + /// Agent's current turn was interrupted and it may receive more input. + Interrupted, /// Agent is done. Contains the final assistant message. Completed(Option), /// Agent encountered an error. diff --git a/codex-rs/tui/src/multi_agents.rs b/codex-rs/tui/src/multi_agents.rs index 00a1c0429..93616b97e 100644 --- a/codex-rs/tui/src/multi_agents.rs +++ b/codex-rs/tui/src/multi_agents.rs @@ -537,10 +537,13 @@ fn status_summary_line(status: &AgentStatus) -> Line<'static> { status_summary_spans(status).into() } +// Allow `.yellow()` +#[allow(clippy::disallowed_methods)] fn status_summary_spans(status: &AgentStatus) -> Vec> { match status { AgentStatus::PendingInit => vec![Span::from("Pending init").cyan()], AgentStatus::Running => vec![Span::from("Running").cyan().bold()], + AgentStatus::Interrupted => vec![Span::from("Interrupted").yellow()], AgentStatus::Completed(message) => { let mut spans = vec![Span::from("Completed").green()]; if let Some(message) = message.as_ref() { @@ -762,6 +765,25 @@ mod tests { assert_eq!(title.spans[6].style.fg, Some(Color::Magenta)); } + #[test] + fn collab_resume_interrupted_snapshot() { + let sender_thread_id = ThreadId::from_string("00000000-0000-0000-0000-000000000001") + .expect("valid sender thread id"); + let robie_id = ThreadId::from_string("00000000-0000-0000-0000-000000000002") + .expect("valid robie thread id"); + + let cell = resume_end(CollabResumeEndEvent { + call_id: "call-resume".to_string(), + sender_thread_id, + receiver_thread_id: robie_id, + receiver_agent_nickname: Some("Robie".to_string()), + receiver_agent_role: Some("explorer".to_string()), + status: AgentStatus::Interrupted, + }); + + assert_snapshot!("collab_resume_interrupted", cell_to_text(&cell)); + } + fn cell_to_text(cell: &PlainHistoryCell) -> String { cell.display_lines(200) .iter() diff --git a/codex-rs/tui/src/snapshots/codex_tui__multi_agents__tests__collab_resume_interrupted.snap b/codex-rs/tui/src/snapshots/codex_tui__multi_agents__tests__collab_resume_interrupted.snap new file mode 100644 index 000000000..17401ba3e --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__multi_agents__tests__collab_resume_interrupted.snap @@ -0,0 +1,6 @@ +--- +source: tui/src/multi_agents.rs +expression: cell_to_text(&cell) +--- +• Resumed Robie [explorer] + └ Interrupted