diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index baaaafd75..d2aadc870 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -246,6 +246,7 @@ fn sample_turn_start_response(turn_id: &str) -> ClientResponsePayload { ClientResponsePayload::TurnStart(codex_app_server_protocol::TurnStartResponse { turn: Turn { id: turn_id.to_string(), + items_view: codex_app_server_protocol::TurnItemsView::Full, items: vec![], status: AppServerTurnStatus::InProgress, error: None, @@ -261,6 +262,7 @@ fn sample_turn_started_notification(thread_id: &str, turn_id: &str) -> ServerNot thread_id: thread_id.to_string(), turn: Turn { id: turn_id.to_string(), + items_view: codex_app_server_protocol::TurnItemsView::Full, items: vec![], status: AppServerTurnStatus::InProgress, error: None, @@ -295,6 +297,7 @@ fn sample_turn_completed_notification( thread_id: thread_id.to_string(), turn: Turn { id: turn_id.to_string(), + items_view: codex_app_server_protocol::TurnItemsView::Full, items: vec![], status, error: codex_error_info.map(|codex_error_info| AppServerTurnError { diff --git a/codex-rs/analytics/src/client_tests.rs b/codex-rs/analytics/src/client_tests.rs index 4b6fb54e9..14ce570d7 100644 --- a/codex-rs/analytics/src/client_tests.rs +++ b/codex-rs/analytics/src/client_tests.rs @@ -154,6 +154,7 @@ fn sample_turn_start_response() -> ClientResponsePayload { ClientResponsePayload::TurnStart(TurnStartResponse { turn: Turn { id: "turn-1".to_string(), + items_view: codex_app_server_protocol::TurnItemsView::Full, items: Vec::new(), status: AppServerTurnStatus::InProgress, error: None, diff --git a/codex-rs/app-server-client/src/lib.rs b/codex-rs/app-server-client/src/lib.rs index 1eb16166a..79141da24 100644 --- a/codex-rs/app-server-client/src/lib.rs +++ b/codex-rs/app-server-client/src/lib.rs @@ -1154,6 +1154,7 @@ mod tests { thread_id: "thread".to_string(), turn: codex_app_server_protocol::Turn { id: "turn".to_string(), + items_view: codex_app_server_protocol::TurnItemsView::Full, items: Vec::new(), status: codex_app_server_protocol::TurnStatus::Completed, error: None, @@ -1984,6 +1985,7 @@ mod tests { thread_id: "thread".to_string(), turn: codex_app_server_protocol::Turn { id: "turn".to_string(), + items_view: codex_app_server_protocol::TurnItemsView::Full, items: Vec::new(), status: codex_app_server_protocol::TurnStatus::Completed, error: None, diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 3a1a9744a..046bab211 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -4312,12 +4312,21 @@ "type": "string" }, "items": { - "description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.", + "description": "Thread items currently included in this turn payload.", "items": { "$ref": "#/definitions/ThreadItem" }, "type": "array" }, + "itemsView": { + "allOf": [ + { + "$ref": "#/definitions/TurnItemsView" + } + ], + "default": "full", + "description": "Describes how much of `items` has been loaded for this turn." + }, "startedAt": { "description": "Unix timestamp (in seconds) when the turn started.", "format": "int64", @@ -4400,6 +4409,31 @@ ], "type": "object" }, + "TurnItemsView": { + "oneOf": [ + { + "description": "`items` was not loaded for this turn. The field is intentionally empty.", + "enum": [ + "notLoaded" + ], + "type": "string" + }, + { + "description": "`items` contains only a display summary for this turn.", + "enum": [ + "summary" + ], + "type": "string" + }, + { + "description": "`items` contains every ThreadItem available from persisted app-server history for this turn.", + "enum": [ + "full" + ], + "type": "string" + } + ] + }, "TurnPlanStep": { "properties": { "status": { 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 ff1071f13..dafa256ae 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 @@ -17683,12 +17683,21 @@ "type": "string" }, "items": { - "description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.", + "description": "Thread items currently included in this turn payload.", "items": { "$ref": "#/definitions/v2/ThreadItem" }, "type": "array" }, + "itemsView": { + "allOf": [ + { + "$ref": "#/definitions/v2/TurnItemsView" + } + ], + "default": "full", + "description": "Describes how much of `items` has been loaded for this turn." + }, "startedAt": { "description": "Unix timestamp (in seconds) when the turn started.", "format": "int64", @@ -17812,6 +17821,31 @@ "title": "TurnInterruptResponse", "type": "object" }, + "TurnItemsView": { + "oneOf": [ + { + "description": "`items` was not loaded for this turn. The field is intentionally empty.", + "enum": [ + "notLoaded" + ], + "type": "string" + }, + { + "description": "`items` contains only a display summary for this turn.", + "enum": [ + "summary" + ], + "type": "string" + }, + { + "description": "`items` contains every ThreadItem available from persisted app-server history for this turn.", + "enum": [ + "full" + ], + "type": "string" + } + ] + }, "TurnPlanStep": { "properties": { "status": { 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 29a40ea28..f028f9b3e 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 @@ -15569,12 +15569,21 @@ "type": "string" }, "items": { - "description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.", + "description": "Thread items currently included in this turn payload.", "items": { "$ref": "#/definitions/ThreadItem" }, "type": "array" }, + "itemsView": { + "allOf": [ + { + "$ref": "#/definitions/TurnItemsView" + } + ], + "default": "full", + "description": "Describes how much of `items` has been loaded for this turn." + }, "startedAt": { "description": "Unix timestamp (in seconds) when the turn started.", "format": "int64", @@ -15698,6 +15707,31 @@ "title": "TurnInterruptResponse", "type": "object" }, + "TurnItemsView": { + "oneOf": [ + { + "description": "`items` was not loaded for this turn. The field is intentionally empty.", + "enum": [ + "notLoaded" + ], + "type": "string" + }, + { + "description": "`items` contains only a display summary for this turn.", + "enum": [ + "summary" + ], + "type": "string" + }, + { + "description": "`items` contains every ThreadItem available from persisted app-server history for this turn.", + "enum": [ + "full" + ], + "type": "string" + } + ] + }, "TurnPlanStep": { "properties": { "status": { 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 16abcd780..9afd1ae51 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json @@ -1324,12 +1324,21 @@ "type": "string" }, "items": { - "description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.", + "description": "Thread items currently included in this turn payload.", "items": { "$ref": "#/definitions/ThreadItem" }, "type": "array" }, + "itemsView": { + "allOf": [ + { + "$ref": "#/definitions/TurnItemsView" + } + ], + "default": "full", + "description": "Describes how much of `items` has been loaded for this turn." + }, "startedAt": { "description": "Unix timestamp (in seconds) when the turn started.", "format": "int64", @@ -1377,6 +1386,31 @@ ], "type": "object" }, + "TurnItemsView": { + "oneOf": [ + { + "description": "`items` was not loaded for this turn. The field is intentionally empty.", + "enum": [ + "notLoaded" + ], + "type": "string" + }, + { + "description": "`items` contains only a display summary for this turn.", + "enum": [ + "summary" + ], + "type": "string" + }, + { + "description": "`items` contains every ThreadItem available from persisted app-server history for this turn.", + "enum": [ + "full" + ], + "type": "string" + } + ] + }, "TurnStatus": { "enum": [ "completed", 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 653c5f238..00689feda 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -2225,12 +2225,21 @@ "type": "string" }, "items": { - "description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.", + "description": "Thread items currently included in this turn payload.", "items": { "$ref": "#/definitions/ThreadItem" }, "type": "array" }, + "itemsView": { + "allOf": [ + { + "$ref": "#/definitions/TurnItemsView" + } + ], + "default": "full", + "description": "Describes how much of `items` has been loaded for this turn." + }, "startedAt": { "description": "Unix timestamp (in seconds) when the turn started.", "format": "int64", @@ -2278,6 +2287,31 @@ ], "type": "object" }, + "TurnItemsView": { + "oneOf": [ + { + "description": "`items` was not loaded for this turn. The field is intentionally empty.", + "enum": [ + "notLoaded" + ], + "type": "string" + }, + { + "description": "`items` contains only a display summary for this turn.", + "enum": [ + "summary" + ], + "type": "string" + }, + { + "description": "`items` contains every ThreadItem available from persisted app-server history for this turn.", + "enum": [ + "full" + ], + "type": "string" + } + ] + }, "TurnStatus": { "enum": [ "completed", 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 2f5cbb950..4db2ae464 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json @@ -1675,12 +1675,21 @@ "type": "string" }, "items": { - "description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.", + "description": "Thread items currently included in this turn payload.", "items": { "$ref": "#/definitions/ThreadItem" }, "type": "array" }, + "itemsView": { + "allOf": [ + { + "$ref": "#/definitions/TurnItemsView" + } + ], + "default": "full", + "description": "Describes how much of `items` has been loaded for this turn." + }, "startedAt": { "description": "Unix timestamp (in seconds) when the turn started.", "format": "int64", @@ -1728,6 +1737,31 @@ ], "type": "object" }, + "TurnItemsView": { + "oneOf": [ + { + "description": "`items` was not loaded for this turn. The field is intentionally empty.", + "enum": [ + "notLoaded" + ], + "type": "string" + }, + { + "description": "`items` contains only a display summary for this turn.", + "enum": [ + "summary" + ], + "type": "string" + }, + { + "description": "`items` contains every ThreadItem available from persisted app-server history for this turn.", + "enum": [ + "full" + ], + "type": "string" + } + ] + }, "TurnStatus": { "enum": [ "completed", 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 b9ae59708..003c75e59 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json @@ -1675,12 +1675,21 @@ "type": "string" }, "items": { - "description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.", + "description": "Thread items currently included in this turn payload.", "items": { "$ref": "#/definitions/ThreadItem" }, "type": "array" }, + "itemsView": { + "allOf": [ + { + "$ref": "#/definitions/TurnItemsView" + } + ], + "default": "full", + "description": "Describes how much of `items` has been loaded for this turn." + }, "startedAt": { "description": "Unix timestamp (in seconds) when the turn started.", "format": "int64", @@ -1728,6 +1737,31 @@ ], "type": "object" }, + "TurnItemsView": { + "oneOf": [ + { + "description": "`items` was not loaded for this turn. The field is intentionally empty.", + "enum": [ + "notLoaded" + ], + "type": "string" + }, + { + "description": "`items` contains only a display summary for this turn.", + "enum": [ + "summary" + ], + "type": "string" + }, + { + "description": "`items` contains every ThreadItem available from persisted app-server history for this turn.", + "enum": [ + "full" + ], + "type": "string" + } + ] + }, "TurnStatus": { "enum": [ "completed", 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 cda474c29..50147feca 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json @@ -1675,12 +1675,21 @@ "type": "string" }, "items": { - "description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.", + "description": "Thread items currently included in this turn payload.", "items": { "$ref": "#/definitions/ThreadItem" }, "type": "array" }, + "itemsView": { + "allOf": [ + { + "$ref": "#/definitions/TurnItemsView" + } + ], + "default": "full", + "description": "Describes how much of `items` has been loaded for this turn." + }, "startedAt": { "description": "Unix timestamp (in seconds) when the turn started.", "format": "int64", @@ -1728,6 +1737,31 @@ ], "type": "object" }, + "TurnItemsView": { + "oneOf": [ + { + "description": "`items` was not loaded for this turn. The field is intentionally empty.", + "enum": [ + "notLoaded" + ], + "type": "string" + }, + { + "description": "`items` contains only a display summary for this turn.", + "enum": [ + "summary" + ], + "type": "string" + }, + { + "description": "`items` contains every ThreadItem available from persisted app-server history for this turn.", + "enum": [ + "full" + ], + "type": "string" + } + ] + }, "TurnStatus": { "enum": [ "completed", 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 27cf47f2f..ff774402b 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -2225,12 +2225,21 @@ "type": "string" }, "items": { - "description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.", + "description": "Thread items currently included in this turn payload.", "items": { "$ref": "#/definitions/ThreadItem" }, "type": "array" }, + "itemsView": { + "allOf": [ + { + "$ref": "#/definitions/TurnItemsView" + } + ], + "default": "full", + "description": "Describes how much of `items` has been loaded for this turn." + }, "startedAt": { "description": "Unix timestamp (in seconds) when the turn started.", "format": "int64", @@ -2278,6 +2287,31 @@ ], "type": "object" }, + "TurnItemsView": { + "oneOf": [ + { + "description": "`items` was not loaded for this turn. The field is intentionally empty.", + "enum": [ + "notLoaded" + ], + "type": "string" + }, + { + "description": "`items` contains only a display summary for this turn.", + "enum": [ + "summary" + ], + "type": "string" + }, + { + "description": "`items` contains every ThreadItem available from persisted app-server history for this turn.", + "enum": [ + "full" + ], + "type": "string" + } + ] + }, "TurnStatus": { "enum": [ "completed", 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 e5339f4e9..75b08d53d 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json @@ -1675,12 +1675,21 @@ "type": "string" }, "items": { - "description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.", + "description": "Thread items currently included in this turn payload.", "items": { "$ref": "#/definitions/ThreadItem" }, "type": "array" }, + "itemsView": { + "allOf": [ + { + "$ref": "#/definitions/TurnItemsView" + } + ], + "default": "full", + "description": "Describes how much of `items` has been loaded for this turn." + }, "startedAt": { "description": "Unix timestamp (in seconds) when the turn started.", "format": "int64", @@ -1728,6 +1737,31 @@ ], "type": "object" }, + "TurnItemsView": { + "oneOf": [ + { + "description": "`items` was not loaded for this turn. The field is intentionally empty.", + "enum": [ + "notLoaded" + ], + "type": "string" + }, + { + "description": "`items` contains only a display summary for this turn.", + "enum": [ + "summary" + ], + "type": "string" + }, + { + "description": "`items` contains every ThreadItem available from persisted app-server history for this turn.", + "enum": [ + "full" + ], + "type": "string" + } + ] + }, "TurnStatus": { "enum": [ "completed", 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 7d93606aa..a0f39a29f 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -2225,12 +2225,21 @@ "type": "string" }, "items": { - "description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.", + "description": "Thread items currently included in this turn payload.", "items": { "$ref": "#/definitions/ThreadItem" }, "type": "array" }, + "itemsView": { + "allOf": [ + { + "$ref": "#/definitions/TurnItemsView" + } + ], + "default": "full", + "description": "Describes how much of `items` has been loaded for this turn." + }, "startedAt": { "description": "Unix timestamp (in seconds) when the turn started.", "format": "int64", @@ -2278,6 +2287,31 @@ ], "type": "object" }, + "TurnItemsView": { + "oneOf": [ + { + "description": "`items` was not loaded for this turn. The field is intentionally empty.", + "enum": [ + "notLoaded" + ], + "type": "string" + }, + { + "description": "`items` contains only a display summary for this turn.", + "enum": [ + "summary" + ], + "type": "string" + }, + { + "description": "`items` contains every ThreadItem available from persisted app-server history for this turn.", + "enum": [ + "full" + ], + "type": "string" + } + ] + }, "TurnStatus": { "enum": [ "completed", 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 774686e46..ff7c4a532 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json @@ -1675,12 +1675,21 @@ "type": "string" }, "items": { - "description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.", + "description": "Thread items currently included in this turn payload.", "items": { "$ref": "#/definitions/ThreadItem" }, "type": "array" }, + "itemsView": { + "allOf": [ + { + "$ref": "#/definitions/TurnItemsView" + } + ], + "default": "full", + "description": "Describes how much of `items` has been loaded for this turn." + }, "startedAt": { "description": "Unix timestamp (in seconds) when the turn started.", "format": "int64", @@ -1728,6 +1737,31 @@ ], "type": "object" }, + "TurnItemsView": { + "oneOf": [ + { + "description": "`items` was not loaded for this turn. The field is intentionally empty.", + "enum": [ + "notLoaded" + ], + "type": "string" + }, + { + "description": "`items` contains only a display summary for this turn.", + "enum": [ + "summary" + ], + "type": "string" + }, + { + "description": "`items` contains every ThreadItem available from persisted app-server history for this turn.", + "enum": [ + "full" + ], + "type": "string" + } + ] + }, "TurnStatus": { "enum": [ "completed", 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 64179af7e..1b5aa2968 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json @@ -1675,12 +1675,21 @@ "type": "string" }, "items": { - "description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.", + "description": "Thread items currently included in this turn payload.", "items": { "$ref": "#/definitions/ThreadItem" }, "type": "array" }, + "itemsView": { + "allOf": [ + { + "$ref": "#/definitions/TurnItemsView" + } + ], + "default": "full", + "description": "Describes how much of `items` has been loaded for this turn." + }, "startedAt": { "description": "Unix timestamp (in seconds) when the turn started.", "format": "int64", @@ -1728,6 +1737,31 @@ ], "type": "object" }, + "TurnItemsView": { + "oneOf": [ + { + "description": "`items` was not loaded for this turn. The field is intentionally empty.", + "enum": [ + "notLoaded" + ], + "type": "string" + }, + { + "description": "`items` contains only a display summary for this turn.", + "enum": [ + "summary" + ], + "type": "string" + }, + { + "description": "`items` contains every ThreadItem available from persisted app-server history for this turn.", + "enum": [ + "full" + ], + "type": "string" + } + ] + }, "TurnStatus": { "enum": [ "completed", 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 0739fa31b..e5e2558e9 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json @@ -1324,12 +1324,21 @@ "type": "string" }, "items": { - "description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.", + "description": "Thread items currently included in this turn payload.", "items": { "$ref": "#/definitions/ThreadItem" }, "type": "array" }, + "itemsView": { + "allOf": [ + { + "$ref": "#/definitions/TurnItemsView" + } + ], + "default": "full", + "description": "Describes how much of `items` has been loaded for this turn." + }, "startedAt": { "description": "Unix timestamp (in seconds) when the turn started.", "format": "int64", @@ -1377,6 +1386,31 @@ ], "type": "object" }, + "TurnItemsView": { + "oneOf": [ + { + "description": "`items` was not loaded for this turn. The field is intentionally empty.", + "enum": [ + "notLoaded" + ], + "type": "string" + }, + { + "description": "`items` contains only a display summary for this turn.", + "enum": [ + "summary" + ], + "type": "string" + }, + { + "description": "`items` contains every ThreadItem available from persisted app-server history for this turn.", + "enum": [ + "full" + ], + "type": "string" + } + ] + }, "TurnStatus": { "enum": [ "completed", 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 bc5917ef1..a2eff7fdd 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json @@ -1324,12 +1324,21 @@ "type": "string" }, "items": { - "description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.", + "description": "Thread items currently included in this turn payload.", "items": { "$ref": "#/definitions/ThreadItem" }, "type": "array" }, + "itemsView": { + "allOf": [ + { + "$ref": "#/definitions/TurnItemsView" + } + ], + "default": "full", + "description": "Describes how much of `items` has been loaded for this turn." + }, "startedAt": { "description": "Unix timestamp (in seconds) when the turn started.", "format": "int64", @@ -1377,6 +1386,31 @@ ], "type": "object" }, + "TurnItemsView": { + "oneOf": [ + { + "description": "`items` was not loaded for this turn. The field is intentionally empty.", + "enum": [ + "notLoaded" + ], + "type": "string" + }, + { + "description": "`items` contains only a display summary for this turn.", + "enum": [ + "summary" + ], + "type": "string" + }, + { + "description": "`items` contains every ThreadItem available from persisted app-server history for this turn.", + "enum": [ + "full" + ], + "type": "string" + } + ] + }, "TurnStatus": { "enum": [ "completed", 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 22ad85d90..0952db2ac 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json @@ -1324,12 +1324,21 @@ "type": "string" }, "items": { - "description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.", + "description": "Thread items currently included in this turn payload.", "items": { "$ref": "#/definitions/ThreadItem" }, "type": "array" }, + "itemsView": { + "allOf": [ + { + "$ref": "#/definitions/TurnItemsView" + } + ], + "default": "full", + "description": "Describes how much of `items` has been loaded for this turn." + }, "startedAt": { "description": "Unix timestamp (in seconds) when the turn started.", "format": "int64", @@ -1377,6 +1386,31 @@ ], "type": "object" }, + "TurnItemsView": { + "oneOf": [ + { + "description": "`items` was not loaded for this turn. The field is intentionally empty.", + "enum": [ + "notLoaded" + ], + "type": "string" + }, + { + "description": "`items` contains only a display summary for this turn.", + "enum": [ + "summary" + ], + "type": "string" + }, + { + "description": "`items` contains every ThreadItem available from persisted app-server history for this turn.", + "enum": [ + "full" + ], + "type": "string" + } + ] + }, "TurnStatus": { "enum": [ "completed", diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/Turn.ts b/codex-rs/app-server-protocol/schema/typescript/v2/Turn.ts index 844c09c4f..6505ec345 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/Turn.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/Turn.ts @@ -3,15 +3,18 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { ThreadItem } from "./ThreadItem"; import type { TurnError } from "./TurnError"; +import type { TurnItemsView } from "./TurnItemsView"; import type { TurnStatus } from "./TurnStatus"; export type Turn = { id: string, /** - * Only populated on a `thread/resume` or `thread/fork` response. - * For all other responses and notifications returning a Turn, - * the items field will be an empty list. + * Thread items currently included in this turn payload. */ -items: Array, status: TurnStatus, +items: Array, +/** + * Describes how much of `items` has been loaded for this turn. + */ +itemsView: TurnItemsView, status: TurnStatus, /** * Only populated when the Turn's status is failed. */ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/TurnItemsView.ts b/codex-rs/app-server-protocol/schema/typescript/v2/TurnItemsView.ts new file mode 100644 index 000000000..905692306 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/TurnItemsView.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type TurnItemsView = "notLoaded" | "summary" | "full"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 4484d61ad..547f0f101 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -425,6 +425,7 @@ export type { TurnEnvironmentParams } from "./TurnEnvironmentParams"; export type { TurnError } from "./TurnError"; export type { TurnInterruptParams } from "./TurnInterruptParams"; export type { TurnInterruptResponse } from "./TurnInterruptResponse"; +export type { TurnItemsView } from "./TurnItemsView"; export type { TurnPlanStep } from "./TurnPlanStep"; export type { TurnPlanStepStatus } from "./TurnPlanStepStatus"; export type { TurnPlanUpdatedNotification } from "./TurnPlanUpdatedNotification"; 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 6228d754d..1f45180c9 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs @@ -17,6 +17,7 @@ use crate::protocol::v2::ThreadItem; use crate::protocol::v2::Turn; use crate::protocol::v2::TurnError as V2TurnError; use crate::protocol::v2::TurnError; +use crate::protocol::v2::TurnItemsView; use crate::protocol::v2::TurnStatus; use crate::protocol::v2::UserInput; use crate::protocol::v2::WebSearchAction; @@ -1166,6 +1167,7 @@ impl From for Turn { Self { id: value.id, items: value.items, + items_view: TurnItemsView::Full, error: value.error, status: value.status, started_at: value.started_at, @@ -1180,6 +1182,7 @@ impl From<&PendingTurn> for Turn { Self { id: value.id.clone(), items: value.items.clone(), + items_view: TurnItemsView::Full, error: value.error.clone(), status: value.status.clone(), started_at: value.started_at, @@ -1453,6 +1456,7 @@ mod tests { started_at: None, completed_at: None, duration_ms: None, + items_view: TurnItemsView::Full, items: vec![ ThreadItem::UserMessage { id: "item-1".into(), @@ -2723,6 +2727,7 @@ mod tests { started_at: None, completed_at: None, duration_ms: None, + items_view: TurnItemsView::Full, items: Vec::new(), }] ); @@ -2982,6 +2987,7 @@ mod tests { started_at: None, completed_at: None, duration_ms: None, + items_view: TurnItemsView::Full, items: vec![ThreadItem::UserMessage { id: "item-1".into(), content: vec![UserInput::Text { diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index d7f60e74b..3328cd251 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -5434,10 +5434,11 @@ impl From for TokenUsageBreakdown { #[ts(export_to = "v2/")] pub struct Turn { pub id: String, - /// Only populated on a `thread/resume` or `thread/fork` response. - /// For all other responses and notifications returning a Turn, - /// the items field will be an empty list. + /// Thread items currently included in this turn payload. pub items: Vec, + /// Describes how much of `items` has been loaded for this turn. + #[serde(default)] + pub items_view: TurnItemsView, pub status: TurnStatus, /// Only populated when the Turn's status is failed. pub error: Option, @@ -5452,6 +5453,19 @@ pub struct Turn { pub duration_ms: Option, } +#[derive(Default, Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum TurnItemsView { + /// `items` was not loaded for this turn. The field is intentionally empty. + NotLoaded, + /// `items` contains only a display summary for this turn. + Summary, + /// `items` contains every ThreadItem available from persisted app-server history for this turn. + #[default] + Full, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -8441,6 +8455,22 @@ mod tests { } } + #[test] + fn turn_defaults_legacy_missing_items_view_to_full() { + let turn: Turn = serde_json::from_value(json!({ + "id": "turn_123", + "items": [], + "status": "completed", + "error": null, + "startedAt": null, + "completedAt": null, + "durationMs": null, + })) + .expect("legacy turn should deserialize"); + + assert_eq!(turn.items_view, TurnItemsView::Full); + } + #[test] fn thread_list_params_accepts_single_cwd() { let params = serde_json::from_value::(json!({ diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index c5b3e9a1e..115fe1c1d 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -427,6 +427,8 @@ Use `thread/read` to fetch a stored thread by id without resuming it. Pass `incl Use `thread/turns/list` with `capabilities.experimentalApi = true` to page a stored thread’s turn history without resuming it. By default, results are sorted descending so clients can start at the present and fetch older turns with `nextCursor`. The response also includes `backwardsCursor`; pass it as `cursor` on a later request with `sortDirection: "asc"` to fetch turns newer than the first item from the earlier page. +Every returned `Turn` includes `itemsView`, which tells clients whether the `items` array was omitted intentionally (`notLoaded`), contains only summary items (`summary`), or contains every item available from persisted app-server history (`full`). Current `thread/turns/list` responses return `full` turns. + ```json { "method": "thread/turns/list", "id": 24, "params": { "threadId": "thr_123", diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index 045854c9b..1907f1c3f 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -75,6 +75,7 @@ use codex_app_server_protocol::TurnCompletedNotification; use codex_app_server_protocol::TurnDiffUpdatedNotification; use codex_app_server_protocol::TurnError; use codex_app_server_protocol::TurnInterruptResponse; +use codex_app_server_protocol::TurnItemsView; use codex_app_server_protocol::TurnPlanStep; use codex_app_server_protocol::TurnPlanUpdatedNotification; use codex_app_server_protocol::TurnStartedNotification; @@ -157,15 +158,19 @@ pub(crate) async fn apply_bespoke_event_handling( .await; let turn = { let state = thread_state.lock().await; - state.active_turn_snapshot().unwrap_or_else(|| Turn { + let mut turn = state.active_turn_snapshot().unwrap_or_else(|| Turn { id: payload.turn_id.clone(), items: Vec::new(), + items_view: TurnItemsView::NotLoaded, error: None, status: TurnStatus::InProgress, started_at: payload.started_at, completed_at: None, duration_ms: None, - }) + }); + turn.items.clear(); + turn.items_view = TurnItemsView::NotLoaded; + turn }; let notification = TurnStartedNotification { thread_id: conversation_id.to_string(), @@ -1305,6 +1310,7 @@ async fn emit_turn_completed_with_status( turn: Turn { id: event_turn_id, items: vec![], + items_view: TurnItemsView::NotLoaded, error: turn_completion_metadata.error, status: turn_completion_metadata.status, started_at: turn_completion_metadata.started_at, @@ -3198,6 +3204,91 @@ mod tests { Ok(()) } + #[tokio::test] + async fn turn_started_omits_active_snapshot_items() -> Result<()> { + let codex_home = TempDir::new()?; + let config = load_default_config_for_test(&codex_home).await; + let thread_manager = Arc::new( + codex_core::test_support::thread_manager_with_models_provider_and_home( + CodexAuth::create_dummy_chatgpt_auth_for_testing(), + config.model_provider.clone(), + config.codex_home.to_path_buf(), + Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), + ), + ); + let codex_core::NewThread { + thread_id: conversation_id, + thread: conversation, + .. + } = thread_manager.start_thread(config.clone()).await?; + let thread_state = new_thread_state(); + { + let mut state = thread_state.lock().await; + state.track_current_turn_event( + "turn-1", + &EventMsg::TurnStarted(codex_protocol::protocol::TurnStartedEvent { + turn_id: "turn-1".to_string(), + started_at: Some(42), + model_context_window: None, + collaboration_mode_kind: Default::default(), + }), + ); + state.track_current_turn_event( + "turn-1", + &EventMsg::UserMessage(codex_protocol::protocol::UserMessageEvent { + message: "already tracked".to_string(), + images: None, + local_images: Vec::new(), + text_elements: Vec::new(), + }), + ); + } + let thread_watch_manager = ThreadWatchManager::new(); + let (tx, mut rx) = mpsc::channel(CHANNEL_CAPACITY); + let outgoing = Arc::new(OutgoingMessageSender::new( + tx, + codex_analytics::AnalyticsEventsClient::disabled(), + )); + let outgoing = ThreadScopedOutgoingMessageSender::new( + outgoing, + vec![ConnectionId(1)], + conversation_id, + ); + + apply_bespoke_event_handling( + Event { + id: "turn-1".to_string(), + msg: EventMsg::TurnStarted(codex_protocol::protocol::TurnStartedEvent { + turn_id: "turn-1".to_string(), + started_at: Some(42), + model_context_window: None, + collaboration_mode_kind: Default::default(), + }), + }, + conversation_id, + conversation, + thread_manager, + /*analytics_events_client*/ None, + outgoing, + thread_state, + thread_watch_manager, + Arc::new(tokio::sync::Semaphore::new(/*permits*/ 1)), + "test-provider".to_string(), + ) + .await; + + let msg = recv_broadcast_message(&mut rx).await?; + match msg { + OutgoingMessage::AppServerNotification(ServerNotification::TurnStarted(n)) => { + assert_eq!(n.turn.id, "turn-1"); + assert_eq!(n.turn.items_view, TurnItemsView::NotLoaded); + assert!(n.turn.items.is_empty()); + } + other => bail!("unexpected message: {other:?}"), + } + Ok(()) + } + #[tokio::test] async fn test_handle_turn_complete_emits_completed_without_error() -> Result<()> { let conversation_id = ThreadId::new(); @@ -3245,6 +3336,8 @@ mod tests { OutgoingMessage::AppServerNotification(ServerNotification::TurnCompleted(n)) => { assert_eq!(n.turn.id, event_turn_id); assert_eq!(n.turn.status, TurnStatus::Completed); + assert_eq!(n.turn.items_view, TurnItemsView::NotLoaded); + assert!(n.turn.items.is_empty()); assert_eq!(n.turn.error, None); assert_eq!(n.turn.started_at, Some(42)); assert_eq!(n.turn.completed_at, Some(TEST_TURN_COMPLETED_AT)); diff --git a/codex-rs/app-server/src/in_process.rs b/codex-rs/app-server/src/in_process.rs index e3eeb9034..a8c023e95 100644 --- a/codex-rs/app-server/src/in_process.rs +++ b/codex-rs/app-server/src/in_process.rs @@ -732,6 +732,7 @@ mod tests { use codex_app_server_protocol::ThreadStartResponse; use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnCompletedNotification; + use codex_app_server_protocol::TurnItemsView; use codex_app_server_protocol::TurnStatus; use codex_core::config::ConfigBuilder; use pretty_assertions::assert_eq; @@ -961,6 +962,7 @@ mod tests { turn: Turn { id: "turn-1".to_string(), items: Vec::new(), + items_view: TurnItemsView::NotLoaded, status: TurnStatus::Completed, error: None, started_at: None, diff --git a/codex-rs/app-server/src/request_processors.rs b/codex-rs/app-server/src/request_processors.rs index 7bf28c8f6..be6d55986 100644 --- a/codex-rs/app-server/src/request_processors.rs +++ b/codex-rs/app-server/src/request_processors.rs @@ -222,6 +222,7 @@ use codex_app_server_protocol::TurnEnvironmentParams; use codex_app_server_protocol::TurnError; use codex_app_server_protocol::TurnInterruptParams; use codex_app_server_protocol::TurnInterruptResponse; +use codex_app_server_protocol::TurnItemsView; use codex_app_server_protocol::TurnStartParams; use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::TurnStatus; diff --git a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs index 6072fd422..4f3e476e4 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs @@ -52,6 +52,7 @@ mod thread_processor_behavior_tests { use chrono::DateTime; use chrono::Utc; use codex_app_server_protocol::ServerRequestPayload; + use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::ToolRequestUserInputParams; use codex_config::CloudRequirementsLoader; use codex_config::LoaderOverrides; @@ -205,6 +206,7 @@ mod thread_processor_behavior_tests { text_elements: Vec::new(), }], }], + items_view: TurnItemsView::Full, error: None, status: TurnStatus::InProgress, started_at: None, diff --git a/codex-rs/app-server/src/request_processors/turn_processor.rs b/codex-rs/app-server/src/request_processors/turn_processor.rs index 033ecf600..88ca44184 100644 --- a/codex-rs/app-server/src/request_processors/turn_processor.rs +++ b/codex-rs/app-server/src/request_processors/turn_processor.rs @@ -502,6 +502,7 @@ impl TurnRequestProcessor { let turn = Turn { id: turn_id, items: vec![], + items_view: TurnItemsView::NotLoaded, error: None, status: TurnStatus::InProgress, started_at: None, @@ -807,6 +808,7 @@ impl TurnRequestProcessor { Turn { id: turn_id, items, + items_view: TurnItemsView::NotLoaded, error: None, status: TurnStatus::InProgress, started_at: None, @@ -981,7 +983,7 @@ impl TurnRequestProcessor { request_id, parent_thread, review_request, - display_text.as_str(), + &display_text, thread_id, ) .await?; @@ -992,7 +994,7 @@ impl TurnRequestProcessor { parent_thread_id, parent_thread, review_request, - display_text.as_str(), + &display_text, ) .await?; } diff --git a/codex-rs/app-server/tests/suite/v2/review.rs b/codex-rs/app-server/tests/suite/v2/review.rs index d56b9318e..277885c5a 100644 --- a/codex-rs/app-server/tests/suite/v2/review.rs +++ b/codex-rs/app-server/tests/suite/v2/review.rs @@ -22,6 +22,7 @@ use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; use codex_app_server_protocol::ThreadStartedNotification; use codex_app_server_protocol::ThreadStatusChangedNotification; +use codex_app_server_protocol::TurnItemsView; use codex_app_server_protocol::TurnStartParams; use codex_app_server_protocol::TurnStatus; use codex_app_server_protocol::UserInput as V2UserInput; @@ -85,6 +86,17 @@ async fn review_start_runs_review_turn_and_emits_code_review_item() -> Result<() assert_eq!(review_thread_id, thread_id.clone()); let turn_id = turn.id.clone(); assert_eq!(turn.status, TurnStatus::InProgress); + assert_eq!(turn.items_view, TurnItemsView::NotLoaded); + assert_eq!( + turn.items, + vec![ThreadItem::UserMessage { + id: turn_id.clone(), + content: vec![V2UserInput::Text { + text: "commit 1234567: Tidy UI colors".to_string(), + text_elements: Vec::new(), + }], + }] + ); // Confirm we see the EnteredReviewMode marker on the main thread. let mut saw_entered_review_mode = false; @@ -182,6 +194,17 @@ async fn review_start_exec_approval_item_id_matches_command_execution_item() -> .await??; let ReviewStartResponse { turn, .. } = to_response::(review_resp)?; let turn_id = turn.id.clone(); + assert_eq!(turn.items_view, TurnItemsView::NotLoaded); + assert_eq!( + turn.items, + vec![ThreadItem::UserMessage { + id: turn_id.clone(), + content: vec![V2UserInput::Text { + text: "commit 1234567: Check review approvals".to_string(), + text_elements: Vec::new(), + }], + }] + ); let server_req = timeout( DEFAULT_READ_TIMEOUT, @@ -300,6 +323,17 @@ async fn review_start_with_detached_delivery_returns_new_thread_id() -> Result<( } = to_response::(review_resp)?; assert_eq!(turn.status, TurnStatus::InProgress); + assert_eq!(turn.items_view, TurnItemsView::NotLoaded); + assert_eq!( + turn.items, + vec![ThreadItem::UserMessage { + id: turn.id.clone(), + content: vec![V2UserInput::Text { + text: "detached review".to_string(), + text_elements: Vec::new(), + }], + }] + ); assert_ne!( review_thread_id, thread_id, "detached review should run on a different thread" diff --git a/codex-rs/app-server/tests/suite/v2/thread_read.rs b/codex-rs/app-server/tests/suite/v2/thread_read.rs index 8c46a5ad9..0dc616dc8 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_read.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_read.rs @@ -33,6 +33,7 @@ use codex_app_server_protocol::ThreadStartResponse; use codex_app_server_protocol::ThreadStatus; use codex_app_server_protocol::ThreadTurnsListParams; use codex_app_server_protocol::ThreadTurnsListResponse; +use codex_app_server_protocol::TurnItemsView; use codex_app_server_protocol::TurnStartParams; use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::TurnStatus; @@ -174,6 +175,7 @@ async fn thread_read_can_include_turns() -> Result<()> { assert_eq!(thread.turns.len(), 1); let turn = &thread.turns[0]; assert_eq!(turn.status, TurnStatus::Completed); + assert_eq!(turn.items_view, TurnItemsView::Full); assert_eq!(turn.items.len(), 1, "expected user message item"); match &turn.items[0] { ThreadItem::UserMessage { content, .. } => { @@ -234,6 +236,10 @@ async fn thread_turns_list_can_page_backward_and_forward() -> Result<()> { backwards_cursor, } = to_response::(read_resp)?; assert_eq!(turn_user_texts(&data), vec!["third", "second"]); + assert!( + data.iter() + .all(|turn| turn.items_view == TurnItemsView::Full) + ); let next_cursor = next_cursor.expect("expected nextCursor for older turns"); let backwards_cursor = backwards_cursor.expect("expected backwardsCursor for newest turn"); diff --git a/codex-rs/app-server/tests/suite/v2/thread_resume.rs b/codex-rs/app-server/tests/suite/v2/thread_resume.rs index 55c0d96eb..2e6665e09 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -41,6 +41,7 @@ use codex_app_server_protocol::ThreadResumeResponse; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; use codex_app_server_protocol::ThreadStatus; +use codex_app_server_protocol::TurnItemsView; use codex_app_server_protocol::TurnStartParams; use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::TurnStatus; @@ -1629,6 +1630,7 @@ async fn thread_resume_rejects_history_when_thread_is_running() -> Result<()> { .await??; let TurnStartResponse { turn: running_turn } = to_response::(running_turn_resp)?; + assert_eq!(running_turn.items_view, TurnItemsView::NotLoaded); timeout( DEFAULT_READ_TIMEOUT, primary.read_stream_until_notification_message("turn/started"), diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index 3c5bbd3b6..952d6069d 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -44,6 +44,7 @@ use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; use codex_app_server_protocol::TurnCompletedNotification; use codex_app_server_protocol::TurnEnvironmentParams; +use codex_app_server_protocol::TurnItemsView; use codex_app_server_protocol::TurnStartParams; use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::TurnStartedNotification; @@ -868,6 +869,8 @@ async fn turn_start_emits_notifications_and_accepts_model_override() -> Result<( codex_app_server_protocol::TurnStatus::InProgress ); assert_eq!(started.turn.id, turn.id); + assert_eq!(started.turn.items_view, TurnItemsView::NotLoaded); + assert!(started.turn.items.is_empty()); let completed_notif: JSONRPCNotification = timeout( DEFAULT_READ_TIMEOUT, @@ -882,6 +885,8 @@ async fn turn_start_emits_notifications_and_accepts_model_override() -> Result<( assert_eq!(completed.thread_id, thread.id); assert_eq!(completed.turn.id, turn.id); assert_eq!(completed.turn.status, TurnStatus::Completed); + assert_eq!(completed.turn.items_view, TurnItemsView::NotLoaded); + assert!(completed.turn.items.is_empty()); // Send a second turn that exercises the overrides path: change the model. let turn_req2 = mcp @@ -915,6 +920,8 @@ async fn turn_start_emits_notifications_and_accepts_model_override() -> Result<( assert_eq!(started2.thread_id, thread.id); assert_eq!(started2.turn.id, turn2.id); assert_eq!(started2.turn.status, TurnStatus::InProgress); + assert_eq!(started2.turn.items_view, TurnItemsView::NotLoaded); + assert!(started2.turn.items.is_empty()); let completed_notif2: JSONRPCNotification = timeout( DEFAULT_READ_TIMEOUT, @@ -929,6 +936,8 @@ async fn turn_start_emits_notifications_and_accepts_model_override() -> Result<( assert_eq!(completed2.thread_id, thread.id); assert_eq!(completed2.turn.id, turn2.id); assert_eq!(completed2.turn.status, TurnStatus::Completed); + assert_eq!(completed2.turn.items_view, TurnItemsView::NotLoaded); + assert!(completed2.turn.items.is_empty()); Ok(()) } diff --git a/codex-rs/exec/src/event_processor_with_human_output_tests.rs b/codex-rs/exec/src/event_processor_with_human_output_tests.rs index 87a9ff969..479758f9a 100644 --- a/codex-rs/exec/src/event_processor_with_human_output_tests.rs +++ b/codex-rs/exec/src/event_processor_with_human_output_tests.rs @@ -240,6 +240,7 @@ fn turn_completed_recovers_final_message_from_turn_items() { thread_id: "thread-1".to_string(), turn: Turn { id: "turn-1".to_string(), + items_view: codex_app_server_protocol::TurnItemsView::Full, items: vec![ThreadItem::AgentMessage { id: "msg-1".to_string(), text: "final answer".to_string(), @@ -287,6 +288,7 @@ fn turn_completed_overwrites_stale_final_message_from_turn_items() { thread_id: "thread-1".to_string(), turn: Turn { id: "turn-1".to_string(), + items_view: codex_app_server_protocol::TurnItemsView::Full, items: vec![ThreadItem::AgentMessage { id: "msg-1".to_string(), text: "final answer".to_string(), @@ -335,6 +337,7 @@ fn turn_completed_preserves_streamed_final_message_when_turn_items_are_empty() { thread_id: "thread-1".to_string(), turn: Turn { id: "turn-1".to_string(), + items_view: codex_app_server_protocol::TurnItemsView::Full, items: Vec::new(), status: TurnStatus::Completed, error: None, @@ -378,6 +381,7 @@ fn turn_failed_clears_stale_final_message() { thread_id: "thread-1".to_string(), turn: Turn { id: "turn-1".to_string(), + items_view: codex_app_server_protocol::TurnItemsView::Full, items: Vec::new(), status: TurnStatus::Failed, error: None, @@ -422,6 +426,7 @@ fn turn_interrupted_clears_stale_final_message() { thread_id: "thread-1".to_string(), turn: Turn { id: "turn-1".to_string(), + items_view: codex_app_server_protocol::TurnItemsView::Full, items: Vec::new(), status: TurnStatus::Interrupted, error: None, diff --git a/codex-rs/exec/src/event_processor_with_jsonl_output_tests.rs b/codex-rs/exec/src/event_processor_with_jsonl_output_tests.rs index f83b54504..88fd042f7 100644 --- a/codex-rs/exec/src/event_processor_with_jsonl_output_tests.rs +++ b/codex-rs/exec/src/event_processor_with_jsonl_output_tests.rs @@ -32,6 +32,7 @@ fn failed_turn_does_not_overwrite_output_last_message_file() { 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: TurnStatus::Failed, error: Some(codex_app_server_protocol::TurnError { diff --git a/codex-rs/exec/src/lib_tests.rs b/codex-rs/exec/src/lib_tests.rs index 648d51268..094da6f93 100644 --- a/codex-rs/exec/src/lib_tests.rs +++ b/codex-rs/exec/src/lib_tests.rs @@ -262,6 +262,7 @@ fn turn_items_for_thread_returns_matching_turn_items() { turns: vec![ codex_app_server_protocol::Turn { id: "turn-1".to_string(), + items_view: codex_app_server_protocol::TurnItemsView::Full, items: vec![AppServerThreadItem::AgentMessage { id: "msg-1".to_string(), text: "hello".to_string(), @@ -276,6 +277,7 @@ fn turn_items_for_thread_returns_matching_turn_items() { }, codex_app_server_protocol::Turn { id: "turn-2".to_string(), + items_view: codex_app_server_protocol::TurnItemsView::Full, items: vec![AppServerThreadItem::Plan { id: "plan-1".to_string(), text: "ship it".to_string(), @@ -308,6 +310,7 @@ fn should_backfill_turn_completed_items_skips_ephemeral_threads() { 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::Completed, error: None, diff --git a/codex-rs/exec/tests/event_processor_with_json_output.rs b/codex-rs/exec/tests/event_processor_with_json_output.rs index 5eda08c13..4b01ccccd 100644 --- a/codex-rs/exec/tests/event_processor_with_json_output.rs +++ b/codex-rs/exec/tests/event_processor_with_json_output.rs @@ -142,6 +142,7 @@ fn turn_started_emits_turn_started_event() { thread_id: "thread-1".to_string(), turn: Turn { id: "turn-1".to_string(), + items_view: codex_app_server_protocol::TurnItemsView::Full, items: Vec::new(), status: TurnStatus::InProgress, error: None, @@ -1095,6 +1096,7 @@ fn plan_update_emits_started_then_updated_then_completed() { thread_id: "thread-1".to_string(), turn: Turn { id: "turn-1".to_string(), + items_view: codex_app_server_protocol::TurnItemsView::Full, items: Vec::new(), status: TurnStatus::Completed, error: None, @@ -1154,6 +1156,7 @@ fn plan_update_after_completion_starts_new_todo_list_with_new_id() { thread_id: "thread-1".to_string(), turn: Turn { id: "turn-1".to_string(), + items_view: codex_app_server_protocol::TurnItemsView::Full, items: Vec::new(), status: TurnStatus::Completed, error: None, @@ -1236,6 +1239,7 @@ fn token_usage_update_is_emitted_on_turn_completion() { thread_id: "thread-1".to_string(), turn: Turn { id: "turn-1".to_string(), + items_view: codex_app_server_protocol::TurnItemsView::Full, items: Vec::new(), status: TurnStatus::Completed, error: None, @@ -1270,6 +1274,7 @@ fn turn_completion_recovers_final_message_from_turn_items() { thread_id: "thread-1".to_string(), turn: Turn { id: "turn-1".to_string(), + items_view: codex_app_server_protocol::TurnItemsView::Full, items: vec![ThreadItem::AgentMessage { id: "msg-1".to_string(), text: "final answer".to_string(), @@ -1342,6 +1347,7 @@ fn turn_completion_reconciles_started_items_from_turn_items() { thread_id: "thread-1".to_string(), turn: Turn { id: "turn-1".to_string(), + items_view: codex_app_server_protocol::TurnItemsView::Full, items: vec![ThreadItem::CommandExecution { id: "cmd-1".to_string(), command: "ls".to_string(), @@ -1409,6 +1415,7 @@ fn turn_completion_overwrites_stale_final_message_from_turn_items() { thread_id: "thread-1".to_string(), turn: Turn { id: "turn-1".to_string(), + items_view: codex_app_server_protocol::TurnItemsView::Full, items: vec![ThreadItem::AgentMessage { id: "msg-1".to_string(), text: "final answer".to_string(), @@ -1458,6 +1465,7 @@ fn turn_completion_preserves_streamed_final_message_when_turn_items_are_empty() thread_id: "thread-1".to_string(), turn: Turn { id: "turn-1".to_string(), + items_view: codex_app_server_protocol::TurnItemsView::Full, items: Vec::new(), status: TurnStatus::Completed, error: None, @@ -1506,6 +1514,7 @@ fn failed_turn_clears_stale_final_message() { thread_id: "thread-1".to_string(), turn: Turn { id: "turn-1".to_string(), + items_view: codex_app_server_protocol::TurnItemsView::Full, items: Vec::new(), status: TurnStatus::Failed, error: Some(TurnError { @@ -1533,6 +1542,7 @@ fn turn_completion_falls_back_to_final_plan_text() { thread_id: "thread-1".to_string(), turn: Turn { id: "turn-1".to_string(), + items_view: codex_app_server_protocol::TurnItemsView::Full, items: vec![ThreadItem::Plan { id: "plan-1".to_string(), text: "ship the typed adapter".to_string(), @@ -1587,6 +1597,7 @@ fn turn_failure_prefers_structured_error_message() { thread_id: "thread-1".to_string(), turn: Turn { id: "turn-1".to_string(), + items_view: codex_app_server_protocol::TurnItemsView::Full, items: Vec::new(), status: TurnStatus::Failed, error: None, diff --git a/codex-rs/tui/src/app/pending_interactive_replay.rs b/codex-rs/tui/src/app/pending_interactive_replay.rs index cfcbd7ce9..671e41461 100644 --- a/codex-rs/tui/src/app/pending_interactive_replay.rs +++ b/codex-rs/tui/src/app/pending_interactive_replay.rs @@ -665,6 +665,7 @@ mod tests { thread_id: "thread-1".to_string(), turn: Turn { id: turn_id.to_string(), + items_view: codex_app_server_protocol::TurnItemsView::Full, items: Vec::new(), status: TurnStatus::Completed, error: None, diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 3de69743b..0e59964c3 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -4089,6 +4089,7 @@ async fn height_shrink_schedules_resize_reflow() { fn test_turn(turn_id: &str, status: TurnStatus, items: Vec) -> Turn { Turn { id: turn_id.to_string(), + items_view: codex_app_server_protocol::TurnItemsView::Full, items, status, error: None, @@ -4667,6 +4668,7 @@ async fn replay_thread_snapshot_replays_turn_history_in_order() { turns: vec![ Turn { id: "turn-1".to_string(), + items_view: codex_app_server_protocol::TurnItemsView::Full, items: vec![ThreadItem::UserMessage { id: "user-1".to_string(), content: vec![AppServerUserInput::Text { @@ -4682,6 +4684,7 @@ async fn replay_thread_snapshot_replays_turn_history_in_order() { }, Turn { id: "turn-2".to_string(), + items_view: codex_app_server_protocol::TurnItemsView::Full, items: vec![ ThreadItem::UserMessage { id: "user-2".to_string(), diff --git a/codex-rs/tui/src/app/thread_events.rs b/codex-rs/tui/src/app/thread_events.rs index 5b278bd2c..759adf922 100644 --- a/codex-rs/tui/src/app/thread_events.rs +++ b/codex-rs/tui/src/app/thread_events.rs @@ -364,6 +364,7 @@ mod tests { fn test_turn(turn_id: &str, status: TurnStatus, items: Vec) -> Turn { Turn { id: turn_id.to_string(), + items_view: codex_app_server_protocol::TurnItemsView::Full, items, status, error: None, diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index d3cad503d..67e486986 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -1832,6 +1832,7 @@ mod tests { name: None, turns: vec![Turn { id: "turn-1".to_string(), + items_view: codex_app_server_protocol::TurnItemsView::Full, items: vec![ codex_app_server_protocol::ThreadItem::UserMessage { id: "user-1".to_string(), diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 9e18284a6..0b58a913c 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -5964,6 +5964,7 @@ impl ChatWidget { for turn in turns { let Turn { id: turn_id, + items_view: _, items, status, error, @@ -5987,6 +5988,7 @@ impl ChatWidget { thread_id: self.thread_id.map(|id| id.to_string()).unwrap_or_default(), turn: Turn { id: turn_id, + items_view: codex_app_server_protocol::TurnItemsView::NotLoaded, items: Vec::new(), status, error, diff --git a/codex-rs/tui/src/chatwidget/tests/app_server.rs b/codex-rs/tui/src/chatwidget/tests/app_server.rs index a486c0e0e..3e8a0f631 100644 --- a/codex-rs/tui/src/chatwidget/tests/app_server.rs +++ b/codex-rs/tui/src/chatwidget/tests/app_server.rs @@ -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, diff --git a/codex-rs/tui/src/chatwidget/tests/helpers.rs b/codex-rs/tui/src/chatwidget/tests/helpers.rs index a7474e5d4..3f7c9bd5b 100644 --- a/codex-rs/tui/src/chatwidget/tests/helpers.rs +++ b/codex-rs/tui/src/chatwidget/tests/helpers.rs @@ -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, diff --git a/codex-rs/tui/src/chatwidget/tests/history_replay.rs b/codex-rs/tui/src/chatwidget/tests/history_replay.rs index 180412cae..8c3117599 100644 --- a/codex-rs/tui/src/chatwidget/tests/history_replay.rs +++ b/codex-rs/tui/src/chatwidget/tests/history_replay.rs @@ -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, diff --git a/codex-rs/tui/src/chatwidget/tests/plan_mode.rs b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs index 97cc8d7fc..8af07a6aa 100644 --- a/codex-rs/tui/src/chatwidget/tests/plan_mode.rs +++ b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs @@ -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, diff --git a/codex-rs/tui/src/chatwidget/tests/review_mode.rs b/codex-rs/tui/src/chatwidget/tests/review_mode.rs index 0924d724f..f59e880da 100644 --- a/codex-rs/tui/src/chatwidget/tests/review_mode.rs +++ b/codex-rs/tui/src/chatwidget/tests/review_mode.rs @@ -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,