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
2026-05-05 12:17:16 -07:00
committed by GitHub
Unverified
parent b6d4c4ea6b
commit 9e0c191c13
48 changed files with 821 additions and 27 deletions
@@ -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 {
+1
View File
@@ -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,
+2
View File
@@ -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,
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
+7 -4
View File
@@ -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<ThreadItem>, status: TurnStatus,
items: Array<ThreadItem>,
/**
* Describes how much of `items` has been loaded for this turn.
*/
itemsView: TurnItemsView, status: TurnStatus,
/**
* Only populated when the Turn's status is failed.
*/
@@ -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";
@@ -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";
@@ -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<PendingTurn> 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 {
@@ -5434,10 +5434,11 @@ impl From<CoreTokenUsage> 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<ThreadItem>,
/// 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<TurnError>,
@@ -5452,6 +5453,19 @@ pub struct Turn {
pub duration_ms: Option<i64>,
}
#[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::<ThreadListParams>(json!({
+2
View File
@@ -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 threads 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",
@@ -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));
+2
View File
@@ -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,
@@ -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;
@@ -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,
@@ -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?;
}
@@ -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::<ReviewStartResponse>(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::<ReviewStartResponse>(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"
@@ -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::<ThreadTurnsListResponse>(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");
@@ -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::<TurnStartResponse>(running_turn_resp)?;
assert_eq!(running_turn.items_view, TurnItemsView::NotLoaded);
timeout(
DEFAULT_READ_TIMEOUT,
primary.read_stream_until_notification_message("turn/started"),
@@ -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(())
}
@@ -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,
@@ -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 {
+3
View File
@@ -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,
@@ -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,
@@ -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,
+3
View File
@@ -4089,6 +4089,7 @@ async fn height_shrink_schedules_resize_reflow() {
fn test_turn(turn_id: &str, status: TurnStatus, items: Vec<ThreadItem>) -> 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(),
+1
View File
@@ -364,6 +364,7 @@ mod tests {
fn test_turn(turn_id: &str, status: TurnStatus, items: Vec<ThreadItem>) -> Turn {
Turn {
id: turn_id.to_string(),
items_view: codex_app_server_protocol::TurnItemsView::Full,
items,
status,
error: None,
+1
View File
@@ -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(),
+2
View File
@@ -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,
@@ -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,