diff --git a/codex-rs/app-server-protocol/schema/json/ApplyPatchApprovalResponse.json b/codex-rs/app-server-protocol/schema/json/ApplyPatchApprovalResponse.json index 84842fd1941..84c36edf10b 100644 --- a/codex-rs/app-server-protocol/schema/json/ApplyPatchApprovalResponse.json +++ b/codex-rs/app-server-protocol/schema/json/ApplyPatchApprovalResponse.json @@ -94,6 +94,13 @@ ], "type": "string" }, + { + "description": "Automatic approval review timed out before reaching a decision.", + "enum": [ + "timed_out" + ], + "type": "string" + }, { "description": "User has denied this command and the agent should not do anything until the user's next command.", "enum": [ diff --git a/codex-rs/app-server-protocol/schema/json/ExecCommandApprovalResponse.json b/codex-rs/app-server-protocol/schema/json/ExecCommandApprovalResponse.json index baedafa4038..477109e2b05 100644 --- a/codex-rs/app-server-protocol/schema/json/ExecCommandApprovalResponse.json +++ b/codex-rs/app-server-protocol/schema/json/ExecCommandApprovalResponse.json @@ -94,6 +94,13 @@ ], "type": "string" }, + { + "description": "Automatic approval review timed out before reaching a decision.", + "enum": [ + "timed_out" + ], + "type": "string" + }, { "description": "User has denied this command and the agent should not do anything until the user's next command.", "enum": [ diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index ec368762cf0..5d7c1549ab6 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -1348,6 +1348,7 @@ "inProgress", "approved", "denied", + "timedOut", "aborted" ], "type": "string" 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 429d251c796..4308f8e84a1 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 @@ -3398,6 +3398,13 @@ ], "type": "string" }, + { + "description": "Automatic approval review timed out before reaching a decision.", + "enum": [ + "timed_out" + ], + "type": "string" + }, { "description": "User has denied this command and the agent should not do anything until the user's next command.", "enum": [ @@ -8297,6 +8304,7 @@ "inProgress", "approved", "denied", + "timedOut", "aborted" ], "type": "string" 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 50cc1328beb..378e26dd6c7 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 @@ -5056,6 +5056,7 @@ "inProgress", "approved", "denied", + "timedOut", "aborted" ], "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewCompletedNotification.json index f8b9199bbec..590a7a5d65d 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewCompletedNotification.json @@ -222,6 +222,7 @@ "inProgress", "approved", "denied", + "timedOut", "aborted" ], "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewStartedNotification.json index 9f6c35b2a7c..fdb01f27e54 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewStartedNotification.json @@ -215,6 +215,7 @@ "inProgress", "approved", "denied", + "timedOut", "aborted" ], "type": "string" diff --git a/codex-rs/app-server-protocol/schema/typescript/ReviewDecision.ts b/codex-rs/app-server-protocol/schema/typescript/ReviewDecision.ts index b5193785d86..109f72929ca 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ReviewDecision.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ReviewDecision.ts @@ -7,4 +7,4 @@ import type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment"; /** * User's decision in response to an ExecApprovalRequest. */ -export type ReviewDecision = "approved" | { "approved_execpolicy_amendment": { proposed_execpolicy_amendment: ExecPolicyAmendment, } } | "approved_for_session" | { "network_policy_amendment": { network_policy_amendment: NetworkPolicyAmendment, } } | "denied" | "abort"; +export type ReviewDecision = "approved" | { "approved_execpolicy_amendment": { proposed_execpolicy_amendment: ExecPolicyAmendment, } } | "approved_for_session" | { "network_policy_amendment": { network_policy_amendment: NetworkPolicyAmendment, } } | "denied" | "timed_out" | "abort"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/GuardianApprovalReviewStatus.ts b/codex-rs/app-server-protocol/schema/typescript/v2/GuardianApprovalReviewStatus.ts index b98578b206d..ae59854bde7 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/GuardianApprovalReviewStatus.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/GuardianApprovalReviewStatus.ts @@ -5,4 +5,4 @@ /** * [UNSTABLE] Lifecycle state for a guardian approval review. */ -export type GuardianApprovalReviewStatus = "inProgress" | "approved" | "denied" | "aborted"; +export type GuardianApprovalReviewStatus = "inProgress" | "approved" | "denied" | "timedOut" | "aborted"; diff --git a/codex-rs/app-server-protocol/src/protocol/item_builders.rs b/codex-rs/app-server-protocol/src/protocol/item_builders.rs index 375e5605d4e..e6d9588f1c5 100644 --- a/codex-rs/app-server-protocol/src/protocol/item_builders.rs +++ b/codex-rs/app-server-protocol/src/protocol/item_builders.rs @@ -221,6 +221,9 @@ pub fn guardian_auto_approval_review_notification( codex_protocol::protocol::GuardianAssessmentStatus::Denied => { GuardianApprovalReviewStatus::Denied } + codex_protocol::protocol::GuardianAssessmentStatus::TimedOut => { + GuardianApprovalReviewStatus::TimedOut + } codex_protocol::protocol::GuardianAssessmentStatus::Aborted => { GuardianApprovalReviewStatus::Aborted } @@ -245,6 +248,7 @@ pub fn guardian_auto_approval_review_notification( } codex_protocol::protocol::GuardianAssessmentStatus::Approved | codex_protocol::protocol::GuardianAssessmentStatus::Denied + | codex_protocol::protocol::GuardianAssessmentStatus::TimedOut | codex_protocol::protocol::GuardianAssessmentStatus::Aborted => { ServerNotification::ItemGuardianApprovalReviewCompleted( ItemGuardianApprovalReviewCompletedNotification { 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 a99fe6d4f6a..d2296c75c5d 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs @@ -408,6 +408,7 @@ impl ThreadHistoryBuilder { GuardianAssessmentStatus::Denied | GuardianAssessmentStatus::Aborted => { CommandExecutionStatus::Declined } + GuardianAssessmentStatus::TimedOut => CommandExecutionStatus::Failed, GuardianAssessmentStatus::Approved => return, }; let Some(item) = build_item_from_guardian_event(payload, status) else { diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 6ace1fc51be..d0a280ff8b1 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -1069,6 +1069,7 @@ impl From for CommandExecutionApprovalDecision { }, CoreReviewDecision::Abort => Self::Cancel, CoreReviewDecision::Denied => Self::Decline, + CoreReviewDecision::TimedOut => Self::Decline, } } } @@ -4532,6 +4533,7 @@ pub enum GuardianApprovalReviewStatus { InProgress, Approved, Denied, + TimedOut, Aborted, } diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index d8990483b79..08a51108ae2 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -312,11 +312,19 @@ pub(crate) async fn apply_bespoke_event_handling( &assessment, ); outgoing.send_server_notification(notification).await; - if matches!( - assessment.status, + let completion_status = match assessment.status { codex_protocol::protocol::GuardianAssessmentStatus::Denied - | codex_protocol::protocol::GuardianAssessmentStatus::Aborted - ) && let Some((target_item_id, completion_item)) = pending_command_execution + | codex_protocol::protocol::GuardianAssessmentStatus::Aborted => { + Some(CommandExecutionStatus::Declined) + } + codex_protocol::protocol::GuardianAssessmentStatus::TimedOut => { + Some(CommandExecutionStatus::Failed) + } + codex_protocol::protocol::GuardianAssessmentStatus::InProgress + | codex_protocol::protocol::GuardianAssessmentStatus::Approved => None, + }; + if let Some(completion_status) = completion_status + && let Some((target_item_id, completion_item)) = pending_command_execution { complete_command_execution_item( &conversation_id, @@ -327,7 +335,7 @@ pub(crate) async fn apply_bespoke_event_handling( /*process_id*/ None, CommandExecutionSource::Agent, completion_item.command_actions, - CommandExecutionStatus::Declined, + completion_status, &outgoing, &thread_state, ) @@ -3000,6 +3008,9 @@ mod tests { Some(codex_protocol::protocol::GuardianUserAuthorization::Low), Some("too risky".to_string()), ), + GuardianAssessmentStatus::TimedOut => { + (None, None, Some("review timed out".to_string())) + } GuardianAssessmentStatus::Aborted => (None, None, None), }; GuardianAssessmentEvent { diff --git a/codex-rs/core/src/codex_delegate.rs b/codex-rs/core/src/codex_delegate.rs index 4deb7d3cfdb..55b3619e114 100644 --- a/codex-rs/core/src/codex_delegate.rs +++ b/codex-rs/core/src/codex_delegate.rs @@ -716,7 +716,7 @@ async fn maybe_auto_review_mcp_request_user_input( ReviewDecision::Approved | ReviewDecision::ApprovedExecpolicyAmendment { .. } | ReviewDecision::NetworkPolicyAmendment { .. } => MCP_TOOL_APPROVAL_ACCEPT.to_string(), - ReviewDecision::Denied | ReviewDecision::Abort => { + ReviewDecision::Denied | ReviewDecision::TimedOut | ReviewDecision::Abort => { MCP_TOOL_APPROVAL_DECLINE_SYNTHETIC.to_string() } }; diff --git a/codex-rs/core/src/guardian/review_session.rs b/codex-rs/core/src/guardian/review_session.rs index 5fe194cac6f..f6cd4d02b2a 100644 --- a/codex-rs/core/src/guardian/review_session.rs +++ b/codex-rs/core/src/guardian/review_session.rs @@ -414,7 +414,7 @@ impl GuardianReviewSessionManager { let snapshot = state.last_committed_fork_snapshot.as_ref()?; match &snapshot.initial_history { InitialHistory::Forked(items) => Some(items.clone()), - _ => None, + InitialHistory::New | InitialHistory::Cleared | InitialHistory::Resumed(_) => None, } } diff --git a/codex-rs/core/src/mcp_tool_call.rs b/codex-rs/core/src/mcp_tool_call.rs index e038f6c1c97..54d7419dbf5 100644 --- a/codex-rs/core/src/mcp_tool_call.rs +++ b/codex-rs/core/src/mcp_tool_call.rs @@ -980,7 +980,7 @@ async fn mcp_tool_approval_decision_from_guardian( | ReviewDecision::ApprovedExecpolicyAmendment { .. } | ReviewDecision::NetworkPolicyAmendment { .. } => McpToolApprovalDecision::Accept, ReviewDecision::ApprovedForSession => McpToolApprovalDecision::AcceptForSession, - ReviewDecision::Denied => McpToolApprovalDecision::Decline { + ReviewDecision::Denied | ReviewDecision::TimedOut => McpToolApprovalDecision::Decline { message: Some(guardian_rejection_message(sess, review_id).await), }, ReviewDecision::Abort => McpToolApprovalDecision::Decline { message: None }, diff --git a/codex-rs/core/src/tools/network_approval.rs b/codex-rs/core/src/tools/network_approval.rs index 58467357c9f..737f8dacea4 100644 --- a/codex-rs/core/src/tools/network_approval.rs +++ b/codex-rs/core/src/tools/network_approval.rs @@ -488,7 +488,7 @@ impl NetworkApprovalService { PendingApprovalDecision::Deny } }, - ReviewDecision::Denied | ReviewDecision::Abort => { + ReviewDecision::Denied | ReviewDecision::TimedOut | ReviewDecision::Abort => { if let Some(review_id) = guardian_review_id.as_deref() { if let Some(owner_call) = owner_call.as_ref() { let message = guardian_rejection_message(session.as_ref(), review_id).await; diff --git a/codex-rs/core/src/tools/orchestrator.rs b/codex-rs/core/src/tools/orchestrator.rs index fed1ce8b2cb..a0e563596af 100644 --- a/codex-rs/core/src/tools/orchestrator.rs +++ b/codex-rs/core/src/tools/orchestrator.rs @@ -151,7 +151,7 @@ impl ToolOrchestrator { otel.tool_decision(otel_tn, otel_ci, &decision, otel_source); match decision { - ReviewDecision::Denied | ReviewDecision::Abort => { + ReviewDecision::Denied | ReviewDecision::TimedOut | ReviewDecision::Abort => { let reason = if let Some(review_id) = guardian_review_id.as_deref() { guardian_rejection_message(tool_ctx.session.as_ref(), review_id).await } else { @@ -306,7 +306,9 @@ impl ToolOrchestrator { otel.tool_decision(otel_tn, otel_ci, &decision, otel_source); match decision { - ReviewDecision::Denied | ReviewDecision::Abort => { + ReviewDecision::Denied + | ReviewDecision::TimedOut + | ReviewDecision::Abort => { let reason = if let Some(review_id) = guardian_review_id.as_deref() { guardian_rejection_message(tool_ctx.session.as_ref(), review_id) .await diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index 41195876fc2..dc665b3b726 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -485,7 +485,7 @@ impl CoreShellActionProvider { EscalationDecision::deny(Some("User denied execution".to_string())) } }, - ReviewDecision::Denied => { + ReviewDecision::Denied | ReviewDecision::TimedOut => { let message = if let Some(review_id) = prompt_decision.guardian_review_id.as_deref() { diff --git a/codex-rs/protocol/src/approvals.rs b/codex-rs/protocol/src/approvals.rs index cde6277a517..b154946f427 100644 --- a/codex-rs/protocol/src/approvals.rs +++ b/codex-rs/protocol/src/approvals.rs @@ -105,6 +105,7 @@ pub enum GuardianAssessmentStatus { InProgress, Approved, Denied, + TimedOut, Aborted, } diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 2c2c9cabeca..65d6dab04ce 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -3494,6 +3494,9 @@ pub enum ReviewDecision { #[default] Denied, + /// Automatic approval review timed out before reaching a decision. + TimedOut, + /// User has denied this command and the agent should not do anything until /// the user's next command. Abort, @@ -3514,6 +3517,7 @@ impl ReviewDecision { NetworkPolicyRuleAction::Deny => "denied_with_network_policy_deny", }, ReviewDecision::Denied => "denied", + ReviewDecision::TimedOut => "timed_out", ReviewDecision::Abort => "abort", } } diff --git a/codex-rs/tui/src/app/app_server_requests.rs b/codex-rs/tui/src/app/app_server_requests.rs index 03449b902ab..b1393e95e08 100644 --- a/codex-rs/tui/src/app/app_server_requests.rs +++ b/codex-rs/tui/src/app/app_server_requests.rs @@ -258,6 +258,7 @@ fn file_change_decision(decision: &ReviewDecision) -> Result Ok(FileChangeApprovalDecision::Accept), ReviewDecision::ApprovedForSession => Ok(FileChangeApprovalDecision::AcceptForSession), ReviewDecision::Denied => Ok(FileChangeApprovalDecision::Decline), + ReviewDecision::TimedOut => Ok(FileChangeApprovalDecision::Decline), ReviewDecision::Abort => Ok(FileChangeApprovalDecision::Cancel), ReviewDecision::ApprovedExecpolicyAmendment { .. } => { Err("execpolicy amendment is not a valid file change approval decision".to_string()) diff --git a/codex-rs/tui/src/bottom_pane/approval_overlay.rs b/codex-rs/tui/src/bottom_pane/approval_overlay.rs index 82e74888f9b..96b86aacf6d 100644 --- a/codex-rs/tui/src/bottom_pane/approval_overlay.rs +++ b/codex-rs/tui/src/bottom_pane/approval_overlay.rs @@ -277,7 +277,9 @@ impl ApprovalOverlay { }; let granted_permissions = match decision { ReviewDecision::Approved | ReviewDecision::ApprovedForSession => permissions.clone(), - ReviewDecision::Denied | ReviewDecision::Abort => Default::default(), + ReviewDecision::Denied | ReviewDecision::TimedOut | ReviewDecision::Abort => { + Default::default() + } ReviewDecision::ApprovedExecpolicyAmendment { .. } | ReviewDecision::NetworkPolicyAmendment { .. } => Default::default(), }; @@ -720,6 +722,7 @@ fn exec_options( display_shortcut: None, additional_shortcuts: vec![key_hint::plain(KeyCode::Char('d'))], }), + ReviewDecision::TimedOut => None, ReviewDecision::Abort => Some(ApprovalOption { label: "No, and tell Codex what to do differently".to_string(), decision: ApprovalDecision::Review(ReviewDecision::Abort), diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 5bf1eea3057..228d86f7dbd 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -6914,6 +6914,9 @@ impl ChatWidget { codex_app_server_protocol::GuardianApprovalReviewStatus::Denied => { GuardianAssessmentStatus::Denied } + codex_app_server_protocol::GuardianApprovalReviewStatus::TimedOut => { + GuardianAssessmentStatus::TimedOut + } codex_app_server_protocol::GuardianApprovalReviewStatus::Aborted => { GuardianAssessmentStatus::Aborted } diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 67bde227d9c..68c63cf642e 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -898,6 +898,18 @@ pub fn new_approval_decision_cell( }; ("✗ ".red(), summary) } + TimedOut => { + let snippet = Span::from(exec_snippet(&command)).dim(); + ( + "✗ ".red(), + vec![ + "Review ".into(), + "timed out".bold(), + " before codex could run ".into(), + snippet, + ], + ) + } Abort => { let snippet = Span::from(exec_snippet(&command)).dim(); (