Support includeTurns reads for paginated threads (#36993)

## Why

Clients using `thread/read` with `includeTurns: true` need the legacy full-history
view even when a thread stores its history in paginated form.

## What changed

- Reconstruct full projected turns from paginated history for both stored and
  loaded threads.
- Keep `thread/turns/list` and `thread/items/list` as the preferred incremental
  history APIs.

## Testing

- Cover `thread/read(includeTurns=true)` before and after a paginated thread is
  loaded.

GitOrigin-RevId: 6efdc04e5ed3cad2ff2078d8103933082c7921db
This commit is contained in:
Owen Lin authored and copyberry committed 2026-08-04 23:19:41 +00:00
1 parent 44cb66e4ed
commit e87e2b495b
3 files changed
+62 -35

No files matched your search

@@ -213,8 +213,7 @@ async fn thread_read_can_include_turns() -> Result<()> {
}
#[tokio::test]
async fn paginated_stored_thread_routes_projected_turns_and_rejects_legacy_history_paths()
-> Result<()> {
async fn paginated_stored_thread_routes_projected_turns() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
MockResponsesConfig::new(&server.uri()).write(codex_home.path())?;
@@ -270,23 +269,6 @@ async fn paginated_stored_thread_routes_projected_turns_and_rejects_legacy_histo
.expect("thread/list should include paginated thread");
assert_eq!(listed.history_mode, ThreadHistoryMode::Paginated);
let read_id = mcp
.send_thread_read_request(ThreadReadParams {
thread_id: conversation_id.clone(),
include_turns: true,
})
.await?;
let read_err: JSONRPCError = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(read_id)),
)
.await??;
assert_eq!(read_err.error.code, -32600);
assert_eq!(
read_err.error.message,
"paginated threads do not support thread/read(includeTurns=true)"
);
let turns_list_id = mcp
.send_thread_turns_list_request(ThreadTurnsListParams {
thread_id: conversation_id.clone(),
@@ -1512,7 +1494,7 @@ async fn thread_turns_list_rejects_unmaterialized_loaded_thread() -> Result<()>
}
#[tokio::test]
async fn paginated_history_lists_use_projected_turns_and_items() -> Result<()> {
async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
MockResponsesConfig::new(&server.uri()).write(codex_home.path())?;
@@ -1663,6 +1645,17 @@ async fn paginated_history_lists_use_projected_turns_and_items() -> Result<()> {
};
let expected_full_turns = vec![expected_turn_1_full.clone(), expected_turn_2_full.clone()];
let read_id = mcp
.send_thread_read_request(ThreadReadParams {
thread_id: thread_id.to_string(),
include_turns: true,
})
.await?;
let ThreadReadResponse {
thread: unloaded_thread,
} = timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(read_id)).await??;
assert_eq!(unloaded_thread.turns, expected_full_turns);
let legacy_resume_id = mcp
.send_thread_resume_request(ThreadResumeParams {
thread_id: thread_id.to_string(),
@@ -1915,6 +1908,21 @@ async fn paginated_history_lists_use_projected_turns_and_items() -> Result<()> {
)
.await??;
let read_id = mcp
.send_thread_read_request(ThreadReadParams {
thread_id: thread_id.to_string(),
include_turns: true,
})
.await?;
let ThreadReadResponse {
thread: loaded_thread,
} = timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(read_id)).await??;
assert_eq!(&loaded_thread.turns[..2], expected_full_turns);
assert_eq!(
turn_user_texts(&loaded_thread.turns),
vec!["continue after legacy resume"]
);
Ok(())
}