Files
codex/codex-rs/app-server-protocol/src/protocol/thread_history_projection.rs
T
Won Park d7748e1185 Add opt-in structured errors for Guardian circuit-breaker interruptions (#48796)
## Why

Guardian denial-limit interruptions lack a structured error identifying the cause. Make this opt-in because older clients may not recognize the new error in shared history.

## What changed

- Add `auto_review.circuit_break_action = "strict"` to attach `TooManyDenials` to the interrupted turn. The default leaves the error unset.
- Expose `turn.error.codexErrorInfo = "tooManyDenials"` in app-server notifications and saved history, and update protocol schemas and generated types.
- Preserve the warning, denial limit, and interrupted status without emitting a separate error notification.

## Testing

Add an integration test covering omitted, `default`, and `strict` settings, checking warnings, live turn errors, the absence of separate error notifications, and persisted turn errors after restart.

GitOrigin-RevId: cb5d379e64728d4d7b9daf834c13756e4a70db31
2026-09-27 22:05:19 +00:00

105 lines
4.5 KiB
Rust

//! Stateless projection from canonical paginated rollout records to thread-history changes.
//!
//! This module is only for the new paginated rollout format that persists canonical
//! `ItemCompleted(TurnItem)` records, not legacy event-only rollouts.
use codex_protocol::protocol::EventMsg;
use codex_rollout::RolloutItem;
use codex_rollout::RolloutLine;
use crate::protocol::thread_history::ThreadHistoryChangeSet;
use crate::protocol::thread_history::ThreadHistoryItemChange;
use crate::protocol::thread_history::ThreadHistoryTurnMetadata;
use crate::protocol::v2::ThreadItem;
use crate::protocol::v2::TurnError;
use crate::protocol::v2::TurnStatus;
/// Project one durable rollout line without reconstructing earlier history.
///
/// Callers that replay a JSONL suffix should invoke it once per line, in ordinal order, so storage
/// can preserve the first and latest timestamps for repeated item snapshots independently.
pub fn project_rollout_line(line: &RolloutLine) -> ThreadHistoryChangeSet {
match &line.item {
RolloutItem::EventMsg(EventMsg::TurnStarted(event)) => ThreadHistoryChangeSet {
changed_turns: vec![ThreadHistoryTurnMetadata {
turn_id: event.turn_id.clone(),
root_turn_id: event.root_turn_id.clone(),
status: TurnStatus::InProgress,
error: None,
started_at: event.started_at,
completed_at: None,
duration_ms: None,
}],
..Default::default()
},
RolloutItem::EventMsg(EventMsg::TurnComplete(event)) => ThreadHistoryChangeSet {
changed_turns: vec![ThreadHistoryTurnMetadata {
turn_id: event.turn_id.clone(),
root_turn_id: None,
status: if event.error.is_some() {
TurnStatus::Failed
} else {
TurnStatus::Completed
},
error: event.error.as_ref().map(|error| TurnError {
misalignment: error.misalignment.clone().map(Into::into),
message: error.message.clone(),
codex_error_info: error.codex_error_info.clone().map(Into::into),
additional_details: None,
}),
started_at: event.started_at,
completed_at: event.completed_at,
duration_ms: event.duration_ms,
}],
..Default::default()
},
RolloutItem::EventMsg(EventMsg::TurnAborted(event)) => {
let Some(turn_id) = event.turn_id.as_ref() else {
return ThreadHistoryChangeSet::default();
};
ThreadHistoryChangeSet {
changed_turns: vec![ThreadHistoryTurnMetadata {
turn_id: turn_id.clone(),
root_turn_id: None,
status: TurnStatus::Interrupted,
error: event.error.as_ref().map(|error| TurnError {
message: error.message.clone(),
codex_error_info: error.codex_error_info.clone().map(Into::into),
misalignment: error.misalignment.clone().map(Into::into),
additional_details: None,
}),
started_at: event.started_at,
completed_at: event.completed_at,
duration_ms: event.duration_ms,
}],
..Default::default()
}
}
RolloutItem::EventMsg(EventMsg::ItemCompleted(event)) => ThreadHistoryChangeSet {
changed_items: vec![ThreadHistoryItemChange {
turn_id: event.turn_id.clone(),
item: ThreadItem::from(event.item.clone()),
started_at_ms: event.started_at_ms,
completed_at_ms: (event.completed_at_ms != 0).then_some(event.completed_at_ms),
}],
..Default::default()
},
RolloutItem::SessionMeta(_)
| RolloutItem::ResponseItem(_)
| RolloutItem::InterAgentCommunication(_)
| RolloutItem::InterAgentCommunicationMetadata { .. }
| RolloutItem::Compacted(_)
| RolloutItem::TurnContext(_)
| RolloutItem::TokenUsageRecord(_)
| RolloutItem::WorldState(_)
| RolloutItem::RealtimeItem(_)
| RolloutItem::RetainedContext(_)
| RolloutItem::SecurityRiskScore(_)
| RolloutItem::EventMsg(_) => ThreadHistoryChangeSet::default(),
}
}
#[cfg(test)]
#[path = "thread_history_projection_tests.rs"]
mod tests;