add turn items view to app-server turns (#21063)

## Why

`Turn.items` currently overloads an empty array to mean either that no
items exist or that the server intentionally did not load them for this
response. That ambiguity blocks future lazy-loading work where clients
need to distinguish unloaded, summary, and fully hydrated turn payloads.

## What changed

- add a new `TurnItemsView` enum with `notLoaded`, `summary`, and `full`
variants
- add required `itemsView` metadata to app-server `Turn` payloads
- mark reconstructed persisted history as `full` and live shell-style
turn payloads as `notLoaded`
- keep current `thread/turns/list` behavior unchanged and document that
it still returns `full` turns today
- regenerate the JSON and TypeScript protocol fixtures

## Verification

- `just write-app-server-schema`
- `cargo test -p codex-app-server-protocol`
- `cargo test -p codex-app-server thread_read_can_include_turns`
- `cargo test -p codex-app-server
thread_turns_list_can_page_backward_and_forward`
- `cargo test -p codex-app-server
thread_resume_rejects_history_when_thread_is_running`
- `just fix -p codex-app-server-protocol`
- `just fix -p codex-app-server`
- `just fmt`
This commit is contained in:
rhan-oai authored and GitHub committed 2026-05-05 19:17:16 +00:00
1 parent b6d4c4ea6b
commit 9e0c191c13
48 files changed
+821 -27

No files matched your search

@@ -116,6 +116,7 @@ async fn live_app_server_turn_completed_clears_working_status_after_answer_item(
thread_id: "thread-1".to_string(),
turn: AppServerTurn {
id: "turn-1".to_string(),
items_view: codex_app_server_protocol::TurnItemsView::Full,
items: Vec::new(),
status: AppServerTurnStatus::InProgress,
error: None,
@@ -159,6 +160,7 @@ async fn live_app_server_turn_completed_clears_working_status_after_answer_item(
thread_id: "thread-1".to_string(),
turn: AppServerTurn {
id: "turn-1".to_string(),
items_view: codex_app_server_protocol::TurnItemsView::Full,
items: Vec::new(),
status: AppServerTurnStatus::Completed,
error: None,
@@ -183,6 +185,7 @@ async fn live_app_server_turn_started_sets_feedback_turn_id() {
thread_id: "thread-1".to_string(),
turn: AppServerTurn {
id: "turn-1".to_string(),
items_view: codex_app_server_protocol::TurnItemsView::Full,
items: Vec::new(),
status: AppServerTurnStatus::InProgress,
error: None,
@@ -542,6 +545,7 @@ async fn live_app_server_failed_turn_does_not_duplicate_error_history() {
thread_id: "thread-1".to_string(),
turn: AppServerTurn {
id: "turn-1".to_string(),
items_view: codex_app_server_protocol::TurnItemsView::Full,
items: Vec::new(),
status: AppServerTurnStatus::InProgress,
error: None,
@@ -576,6 +580,7 @@ async fn live_app_server_failed_turn_does_not_duplicate_error_history() {
thread_id: "thread-1".to_string(),
turn: AppServerTurn {
id: "turn-1".to_string(),
items_view: codex_app_server_protocol::TurnItemsView::Full,
items: Vec::new(),
status: AppServerTurnStatus::Failed,
error: Some(AppServerTurnError {
@@ -604,6 +609,7 @@ async fn live_app_server_stream_recovery_restores_previous_status_header() {
thread_id: "thread-1".to_string(),
turn: AppServerTurn {
id: "turn-1".to_string(),
items_view: codex_app_server_protocol::TurnItemsView::Full,
items: Vec::new(),
status: AppServerTurnStatus::InProgress,
error: None,
@@ -661,6 +667,7 @@ async fn live_app_server_server_overloaded_error_renders_warning() {
thread_id: "thread-1".to_string(),
turn: AppServerTurn {
id: "turn-1".to_string(),
items_view: codex_app_server_protocol::TurnItemsView::Full,
items: Vec::new(),
status: AppServerTurnStatus::InProgress,
error: None,
@@ -702,6 +709,7 @@ async fn live_app_server_cyber_policy_error_renders_dedicated_notice() {
thread_id: "thread-1".to_string(),
turn: AppServerTurn {
id: "turn-1".to_string(),
items_view: codex_app_server_protocol::TurnItemsView::Full,
items: Vec::new(),
status: AppServerTurnStatus::InProgress,
error: None,
@@ -1086,6 +1086,7 @@ pub(super) fn app_server_turn(
) -> AppServerTurn {
AppServerTurn {
id: turn_id.to_string(),
items_view: codex_app_server_protocol::TurnItemsView::Full,
items: Vec::new(),
status,
error,
@@ -623,6 +623,7 @@ async fn replayed_retryable_app_server_error_keeps_turn_running() {
thread_id: "thread-1".to_string(),
turn: AppServerTurn {
id: "turn-1".to_string(),
items_view: codex_app_server_protocol::TurnItemsView::Full,
items: Vec::new(),
status: AppServerTurnStatus::InProgress,
error: None,
@@ -774,6 +775,7 @@ async fn live_reasoning_summary_is_not_rendered_twice_when_item_completes() {
thread_id: "thread-1".to_string(),
turn: AppServerTurn {
id: "turn-1".to_string(),
items_view: codex_app_server_protocol::TurnItemsView::Full,
items: Vec::new(),
status: AppServerTurnStatus::InProgress,
error: None,
@@ -842,6 +844,7 @@ async fn replayed_in_progress_turn_marks_task_running() {
chat.replay_thread_turns(
vec![AppServerTurn {
id: "turn-1".to_string(),
items_view: codex_app_server_protocol::TurnItemsView::Full,
items: Vec::new(),
status: AppServerTurnStatus::InProgress,
error: None,
@@ -807,6 +807,7 @@ async fn plan_implementation_popup_skips_replayed_turn_complete() {
chat.replay_thread_turns(
vec![AppServerTurn {
id: "turn-1".to_string(),
items_view: codex_app_server_protocol::TurnItemsView::Full,
items: vec![AppServerThreadItem::AgentMessage {
id: "msg-plan".to_string(),
text: "Plan details".to_string(),
@@ -844,6 +845,7 @@ async fn plan_implementation_popup_shows_once_when_replay_precedes_live_turn_com
chat.replay_thread_turns(
vec![AppServerTurn {
id: "turn-1".to_string(),
items_view: codex_app_server_protocol::TurnItemsView::Full,
items: vec![AppServerThreadItem::AgentMessage {
id: "msg-plan-replay".to_string(),
text: "Plan details".to_string(),
@@ -1128,6 +1130,7 @@ async fn submit_user_message_queues_while_compaction_turn_is_running() {
thread_id: thread_id.to_string(),
turn: AppServerTurn {
id: "turn-1".to_string(),
items_view: codex_app_server_protocol::TurnItemsView::Full,
items: Vec::new(),
status: AppServerTurnStatus::InProgress,
error: None,
@@ -1172,6 +1175,7 @@ async fn submit_user_message_queues_while_compaction_turn_is_running() {
thread_id: thread_id.to_string(),
turn: AppServerTurn {
id: "turn-1".to_string(),
items_view: codex_app_server_protocol::TurnItemsView::Full,
items: Vec::new(),
status: AppServerTurnStatus::Completed,
error: None,
@@ -1163,6 +1163,7 @@ async fn interrupted_turn_after_goal_budget_limited_uses_budget_message_snapshot
thread_id: "thread-1".to_string(),
turn: codex_app_server_protocol::Turn {
id: "turn-1".to_string(),
items_view: codex_app_server_protocol::TurnItemsView::Full,
items: Vec::new(),
status: codex_app_server_protocol::TurnStatus::InProgress,
error: None,
@@ -1199,6 +1200,7 @@ async fn interrupted_turn_after_goal_budget_limited_uses_budget_message_snapshot
thread_id: "thread-1".to_string(),
turn: codex_app_server_protocol::Turn {
id: "turn-1".to_string(),
items_view: codex_app_server_protocol::TurnItemsView::Full,
items: Vec::new(),
status: codex_app_server_protocol::TurnStatus::Interrupted,
error: None,