Deprecate full-history hydration for paginated threads (#40676)

## Why

Paginated thread history should be loaded incrementally through
`thread/turns/list` and `thread/items/list` instead of being fully reconstructed
in `thread.turns`.

## What changed

- Emit `deprecationNotice` when `thread/read`, `thread/resume`, or `thread/fork`
  requests full history for a paginated thread.
- Direct clients to omit `includeTurns` for reads or use `excludeTurns: true` for
  resumes and forks, then page history through the list APIs.
- Document the deprecation in the protocol schemas and app-server README.

## Testing

- Cover notices for paginated reads, cold and loaded resumes, and forks.
- Verify metadata-only requests, legacy threads, and rejected forks do not emit
  the notice.

GitOrigin-RevId: 0d387b46d73608f90b7960120a54af4ef332e510
This commit is contained in:
Owen Lin
2026-08-25 19:06:42 +00:00
committed by copyberry
parent 5cb7a35de9
commit d132b69219
19 changed files with 261 additions and 29 deletions
@@ -7,6 +7,7 @@ use super::thread_input::ensure_direct_input_allowed;
use super::*;
use crate::error_code::method_not_found;
use codex_app_server_protocol::SelectedCapabilityRoot;
use codex_app_server_protocol::ThreadHistoryMode as ApiThreadHistoryMode;
use codex_app_server_protocol::ThreadRevertParams;
use codex_app_server_protocol::ThreadRevertResponse;
use codex_app_server_protocol::ThreadRevertedNotification;
@@ -27,6 +28,8 @@ pub(super) const THREAD_LIST_MAX_LIMIT: usize = 100;
const CODEX_TUI_CLIENT_NAME: &str = "codex-tui";
const THREAD_ROLLBACK_DEPRECATION_SUMMARY: &str =
"thread/rollback is deprecated and will be removed soon";
const PAGINATED_FULL_HISTORY_DEPRECATION_SUMMARY: &str = "Full-history hydration is deprecated for paginated threads; use `excludeTurns: true`, then page with `thread/turns/list` and `thread/items/list`.";
const PAGINATED_THREAD_READ_DEPRECATION_SUMMARY: &str = "Full-history hydration is deprecated for paginated threads; omit `includeTurns` or set it to `false`, then page with `thread/turns/list` and `thread/items/list`.";
async fn stage_pending_project_metadata(
thread_manager: &ThreadManager,
@@ -787,20 +790,23 @@ impl ThreadRequestProcessor {
app_server_client_name: Option<&str>,
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
if app_server_client_name != Some(CODEX_TUI_CLIENT_NAME) {
self.send_thread_rollback_deprecation_notice(request_id.connection_id)
.await;
self.send_deprecation_notice(
request_id.connection_id,
THREAD_ROLLBACK_DEPRECATION_SUMMARY,
)
.await;
}
self.thread_rollback_inner(request_id, params)
.await
.map(|()| None)
}
async fn send_thread_rollback_deprecation_notice(&self, connection_id: ConnectionId) {
async fn send_deprecation_notice(&self, connection_id: ConnectionId, summary: &str) {
self.outgoing
.send_server_notification_to_connections(
&[connection_id],
ServerNotification::DeprecationNotice(DeprecationNoticeNotification {
summary: THREAD_ROLLBACK_DEPRECATION_SUMMARY.to_string(),
summary: summary.to_string(),
details: None,
}),
)
@@ -845,11 +851,24 @@ impl ThreadRequestProcessor {
pub(crate) async fn thread_read(
&self,
request_id: &ConnectionRequestId,
params: ThreadReadParams,
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
self.thread_read_response_inner(params)
.await
.map(|response| Some(response.into()))
let include_turns = params.include_turns;
let response = self.thread_read_response_inner(params).await?;
if include_turns
&& matches!(
response.thread.history_mode,
ApiThreadHistoryMode::Paginated
)
{
self.send_deprecation_notice(
request_id.connection_id,
PAGINATED_THREAD_READ_DEPRECATION_SUMMARY,
)
.await;
}
Ok(Some(response.into()))
}
pub(crate) async fn thread_turns_list(
@@ -3636,6 +3655,13 @@ impl ThreadRequestProcessor {
matches!(thread.history_mode, ThreadHistoryMode::Paginated).then_some(thread.thread_id)
});
let paginated_resume = paginated_thread_id.is_some();
if paginated_resume && include_turns {
self.send_deprecation_notice(
request_id.connection_id,
PAGINATED_FULL_HISTORY_DEPRECATION_SUMMARY,
)
.await;
}
// Parent-owned V2 children must resume through their owner, not caller configuration.
if let InitialHistory::Resumed(resumed_history) = &thread_history
@@ -4144,6 +4170,13 @@ impl ThreadRequestProcessor {
let redact_resume_payloads =
should_redact_thread_resume_payloads(app_server_client_name.as_deref());
let include_turns = !params.exclude_turns;
if paginated_resume && include_turns {
self.send_deprecation_notice(
request_id.connection_id,
PAGINATED_FULL_HISTORY_DEPRECATION_SUMMARY,
)
.await;
}
let needs_history =
!paginated_resume && (include_turns || params.initial_turns_page.is_some());
if needs_history {
@@ -4660,6 +4693,13 @@ impl ThreadRequestProcessor {
"ephemeral paginated thread/fork requires `excludeTurns: true`",
));
}
if paginated_source && include_turns {
self.send_deprecation_notice(
request_id.connection_id,
PAGINATED_FULL_HISTORY_DEPRECATION_SUMMARY,
)
.await;
}
let source_thread_id = source_thread.thread_id;
let source_thread_name = source_thread
.name