feat(app-server, threadstore): Thread pagination APIs and ThreadStore contract (#21566)

## Why
The goal of this PR is to align on app-server and `ThreadStore` API
updates for paginating through large threads.


#### app-server
##### `thread/turns/list`
- Updates `thread/turns/list` to support `itemsView?: "notLoaded" |
"summary" | "full" | null`, defaulting to `summary`.
- Implements the current `thread/turns/list` behavior over the existing
persisted rollout-history fallback:
  - `notLoaded` returns turn envelopes with empty `items`.
- `summary` returns the first user message and final assistant message
when available.
  - `full` preserves the existing full item behavior.

Note that this method still uses the naive approach of loading the
entire rollout file, and returns just the filtered slice of the data.
Real pagination will come later by leveraging SQLite.

##### `thread/turns/items/list`
- Adds the experimental `thread/turns/items/list` protocol, schema,
dispatcher, and processor stub. The app-server currently returns
JSON-RPC `-32601` with `thread/turns/items/list is not supported yet`.

#### ThreadStore
- Adds the experimental `thread/turns/items/list` protocol, schema,
dispatcher, and processor stub. The app-server currently returns
JSON-RPC `-32601` with `thread/turns/items/list is not supported yet`.
- Adds `ThreadStore` contract types and stubbed methods for listing
thread turns and listing items within a turn.
- Adds a typed `StoredTurnStatus` and `StoredTurnError` to avoid baking
app-server API enums or lossy string status values into the store-facing
turn contract.
- Adds a typed `StoredTurnStatus` and `StoredTurnError` to avoid baking
app-server API enums or lossy string status values into the store-facing
turn contract.

This also sketches the storage abstraction we expect to need once turns
are indexed/stored. In particular, `notLoaded` is useful only if
ThreadStore can eventually list turn metadata without loading every
persisted item for each turn.

## Validation

- Added/updated protocol serialization coverage for the new request and
response shapes.
- Added app-server integration coverage for `thread/turns/list` default
summary behavior and all three `itemsView` modes.
- Added app-server integration coverage that `thread/turns/items/list`
returns the expected unsupported JSON-RPC error when experimental APIs
are enabled.
- Added thread-store coverage that the default trait methods return
`ThreadStoreError::Unsupported`.

No developers.openai.com documentation update is needed for this
internal experimental app-server API surface.
This commit is contained in:
Owen Lin
2026-05-07 15:44:43 -07:00
committed by GitHub
parent 54ef99a365
commit 0d0835dd53
17 changed files with 608 additions and 5 deletions

View File

@@ -95,6 +95,59 @@ fn turn_defaults_legacy_missing_items_view_to_full() {
assert_eq!(turn.items_view, TurnItemsView::Full);
}
#[test]
fn thread_turns_list_params_accepts_items_view() {
let params = serde_json::from_value::<ThreadTurnsListParams>(json!({
"threadId": "thr_123",
"cursor": null,
"limit": 25,
"sortDirection": "desc",
"itemsView": "notLoaded",
}))
.expect("thread turns list params should deserialize");
assert_eq!(params.thread_id, "thr_123");
assert_eq!(params.items_view, Some(TurnItemsView::NotLoaded));
}
#[test]
fn thread_turns_items_list_round_trips() {
let params = ThreadTurnsItemsListParams {
thread_id: "thr_123".to_string(),
turn_id: "turn_456".to_string(),
cursor: Some("cursor_1".to_string()),
limit: Some(50),
sort_direction: Some(SortDirection::Asc),
};
assert_eq!(
serde_json::to_value(&params).expect("serialize params"),
json!({
"threadId": "thr_123",
"turnId": "turn_456",
"cursor": "cursor_1",
"limit": 50,
"sortDirection": "asc",
})
);
let response = ThreadTurnsItemsListResponse {
data: vec![ThreadItem::ContextCompaction {
id: "item_1".to_string(),
}],
next_cursor: None,
backwards_cursor: Some("cursor_0".to_string()),
};
assert_eq!(
serde_json::to_value(&response).expect("serialize response"),
json!({
"data": [{"type": "contextCompaction", "id": "item_1"}],
"nextCursor": null,
"backwardsCursor": "cursor_0",
})
);
}
#[test]
fn thread_list_params_accepts_single_cwd() {
let params = serde_json::from_value::<ThreadListParams>(json!({

View File

@@ -6,9 +6,11 @@ use super::PermissionProfileSelectionParams;
use super::SandboxMode;
use super::SandboxPolicy;
use super::Thread;
use super::ThreadItem;
use super::ThreadSource;
use super::Turn;
use super::TurnEnvironmentParams;
use super::TurnItemsView;
use super::shared::v2_enum_from_core;
use codex_experimental_api_macros::ExperimentalApi;
use codex_protocol::config_types::Personality;
@@ -1005,6 +1007,9 @@ pub struct ThreadTurnsListParams {
/// Optional turn pagination direction; defaults to descending.
#[ts(optional = nullable)]
pub sort_direction: Option<SortDirection>,
/// How much item detail to include for each returned turn; defaults to summary.
#[ts(optional = nullable)]
pub items_view: Option<TurnItemsView>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
@@ -1022,6 +1027,36 @@ pub struct ThreadTurnsListResponse {
pub backwards_cursor: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadTurnsItemsListParams {
pub thread_id: String,
pub turn_id: String,
/// Opaque cursor to pass to the next call to continue after the last item.
#[ts(optional = nullable)]
pub cursor: Option<String>,
/// Optional item page size.
#[ts(optional = nullable)]
pub limit: Option<u32>,
/// Optional item pagination direction; defaults to ascending.
#[ts(optional = nullable)]
pub sort_direction: Option<SortDirection>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadTurnsItemsListResponse {
pub data: Vec<ThreadItem>,
/// Opaque cursor to pass to the next call to continue after the last item.
/// if None, there are no more items to return.
pub next_cursor: Option<String>,
/// Opaque cursor to pass as `cursor` when reversing `sortDirection`.
/// This is only populated when the page contains at least one item.
pub backwards_cursor: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]