diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index e2764ba24c..d9043f1de3 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -4119,7 +4119,7 @@ "type": "boolean" }, "excludeTurns": { - "description": "When true, return only thread metadata and live fork state without populating `thread.turns`. This is useful when the client plans to call `thread/turns/list` immediately after forking.", + "description": "When true, return only thread metadata and live fork state without populating `thread.turns`. This is useful when the client plans to call `thread/turns/list` immediately after forking. Full-history hydration is deprecated for paginated threads; use this with `thread/turns/list` and `thread/items/list` instead.", "type": "boolean" }, "lastTurnId": { @@ -4505,7 +4505,7 @@ "ThreadReadParams": { "properties": { "includeTurns": { - "description": "When true, include turns and their items from rollout history.", + "description": "When true, include turns and their items from rollout history. Full-history hydration is deprecated for paginated threads; prefer a metadata-only read and page with `thread/turns/list` and `thread/items/list`.", "type": "boolean" }, "threadId": { @@ -4720,7 +4720,7 @@ ] }, "excludeTurns": { - "description": "When true, return only thread metadata and live-resume state without populating `thread.turns`. This is useful when the client plans to call `thread/turns/list` immediately after resuming.", + "description": "When true, return only thread metadata and live-resume state without populating `thread.turns`. This is useful when the client plans to call `thread/turns/list` immediately after resuming. Full-history hydration is deprecated for paginated threads; use this with `thread/turns/list` and `thread/items/list` instead.", "type": "boolean" }, "model": { 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 6a39e921f7..2bd1ece011 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 @@ -20164,7 +20164,7 @@ "type": "boolean" }, "excludeTurns": { - "description": "When true, return only thread metadata and live fork state without populating `thread.turns`. This is useful when the client plans to call `thread/turns/list` immediately after forking.", + "description": "When true, return only thread metadata and live fork state without populating `thread.turns`. This is useful when the client plans to call `thread/turns/list` immediately after forking. Full-history hydration is deprecated for paginated threads; use this with `thread/turns/list` and `thread/items/list` instead.", "type": "boolean" }, "lastTurnId": { @@ -21739,7 +21739,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "includeTurns": { - "description": "When true, include turns and their items from rollout history.", + "description": "When true, include turns and their items from rollout history. Full-history hydration is deprecated for paginated threads; prefer a metadata-only read and page with `thread/turns/list` and `thread/items/list`.", "type": "boolean" }, "threadId": { @@ -22366,7 +22366,7 @@ ] }, "excludeTurns": { - "description": "When true, return only thread metadata and live-resume state without populating `thread.turns`. This is useful when the client plans to call `thread/turns/list` immediately after resuming.", + "description": "When true, return only thread metadata and live-resume state without populating `thread.turns`. This is useful when the client plans to call `thread/turns/list` immediately after resuming. Full-history hydration is deprecated for paginated threads; use this with `thread/turns/list` and `thread/items/list` instead.", "type": "boolean" }, "model": { 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 d27cc316bb..13da6880b2 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 @@ -17899,7 +17899,7 @@ "type": "boolean" }, "excludeTurns": { - "description": "When true, return only thread metadata and live fork state without populating `thread.turns`. This is useful when the client plans to call `thread/turns/list` immediately after forking.", + "description": "When true, return only thread metadata and live fork state without populating `thread.turns`. This is useful when the client plans to call `thread/turns/list` immediately after forking. Full-history hydration is deprecated for paginated threads; use this with `thread/turns/list` and `thread/items/list` instead.", "type": "boolean" }, "lastTurnId": { @@ -19474,7 +19474,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "includeTurns": { - "description": "When true, include turns and their items from rollout history.", + "description": "When true, include turns and their items from rollout history. Full-history hydration is deprecated for paginated threads; prefer a metadata-only read and page with `thread/turns/list` and `thread/items/list`.", "type": "boolean" }, "threadId": { @@ -20101,7 +20101,7 @@ ] }, "excludeTurns": { - "description": "When true, return only thread metadata and live-resume state without populating `thread.turns`. This is useful when the client plans to call `thread/turns/list` immediately after resuming.", + "description": "When true, return only thread metadata and live-resume state without populating `thread.turns`. This is useful when the client plans to call `thread/turns/list` immediately after resuming. Full-history hydration is deprecated for paginated threads; use this with `thread/turns/list` and `thread/items/list` instead.", "type": "boolean" }, "model": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json index 3d454b27bd..6908b54cb8 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json @@ -127,7 +127,7 @@ "type": "boolean" }, "excludeTurns": { - "description": "When true, return only thread metadata and live fork state without populating `thread.turns`. This is useful when the client plans to call `thread/turns/list` immediately after forking.", + "description": "When true, return only thread metadata and live fork state without populating `thread.turns`. This is useful when the client plans to call `thread/turns/list` immediately after forking. Full-history hydration is deprecated for paginated threads; use this with `thread/turns/list` and `thread/items/list` instead.", "type": "boolean" }, "lastTurnId": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadParams.json index 5fb1bcc17b..920e6c346d 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadParams.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "includeTurns": { - "description": "When true, include turns and their items from rollout history.", + "description": "When true, include turns and their items from rollout history. Full-history hydration is deprecated for paginated threads; prefer a metadata-only read and page with `thread/turns/list` and `thread/items/list`.", "type": "boolean" }, "threadId": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json index 0fbe27f8f2..6722e6e180 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json @@ -1439,7 +1439,7 @@ ] }, "excludeTurns": { - "description": "When true, return only thread metadata and live-resume state without populating `thread.turns`. This is useful when the client plans to call `thread/turns/list` immediately after resuming.", + "description": "When true, return only thread metadata and live-resume state without populating `thread.turns`. This is useful when the client plans to call `thread/turns/list` immediately after resuming. Full-history hydration is deprecated for paginated threads; use this with `thread/turns/list` and `thread/items/list` instead.", "type": "boolean" }, "model": { diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst index 570c8718d2..33d6d70de8 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index eaf12ba5ff..0df4766976 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts index 34eb1fef17..88ff693611 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts @@ -36,6 +36,8 @@ approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, conf threadSource?: ThreadSource | null, /** * When true, return only thread metadata and live fork state without * populating `thread.turns`. This is useful when the client plans to call - * `thread/turns/list` immediately after forking. + * `thread/turns/list` immediately after forking. Full-history hydration + * is deprecated for paginated threads; use this with `thread/turns/list` + * and `thread/items/list` instead. */ excludeTurns?: boolean}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadReadParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadReadParams.ts index c26e896481..9a17ee6245 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadReadParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadReadParams.ts @@ -5,5 +5,8 @@ export type ThreadReadParams = { threadId: string, /** * When true, include turns and their items from rollout history. + * Full-history hydration is deprecated for paginated threads; prefer a + * metadata-only read and page with `thread/turns/list` and + * `thread/items/list`. */ includeTurns?: boolean, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts index 050cc85e8d..d9918d91ea 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts @@ -33,6 +33,8 @@ model?: string | null, modelProvider?: string | null, serviceTier?: string | nul approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, /** * When true, return only thread metadata and live-resume state without * populating `thread.turns`. This is useful when the client plans to call - * `thread/turns/list` immediately after resuming. + * `thread/turns/list` immediately after resuming. Full-history hydration + * is deprecated for paginated threads; use this with `thread/turns/list` + * and `thread/items/list` instead. */ excludeTurns?: boolean}; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs index 8026b42f47..9df4bfbef9 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs @@ -397,7 +397,9 @@ pub struct ThreadResumeParams { pub personality: Option, /// When true, return only thread metadata and live-resume state without /// populating `thread.turns`. This is useful when the client plans to call - /// `thread/turns/list` immediately after resuming. + /// `thread/turns/list` immediately after resuming. Full-history hydration + /// is deprecated for paginated threads; use this with `thread/turns/list` + /// and `thread/items/list` instead. #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub exclude_turns: bool, /// When present, include a `thread/turns/list` page in the resume response @@ -585,7 +587,9 @@ pub struct ThreadForkParams { pub thread_source: Option, /// When true, return only thread metadata and live fork state without /// populating `thread.turns`. This is useful when the client plans to call - /// `thread/turns/list` immediately after forking. + /// `thread/turns/list` immediately after forking. Full-history hydration + /// is deprecated for paginated threads; use this with `thread/turns/list` + /// and `thread/items/list` instead. #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub exclude_turns: bool, /// When true, carry the source thread's current goal into the fork without @@ -1646,6 +1650,9 @@ pub enum ThreadActiveFlag { pub struct ThreadReadParams { pub thread_id: String, /// When true, include turns and their items from rollout history. + /// Full-history hydration is deprecated for paginated threads; prefer a + /// metadata-only read and page with `thread/turns/list` and + /// `thread/items/list`. #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub include_turns: bool, } diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 651d80572e..bdcf1f1707 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -367,7 +367,7 @@ To continue a stored session, call `thread/resume` with the `thread.id` you prev Parent-owned Multi-Agent V2 children are an exception: `thread/resume` ignores configuration overrides and reattaches to the existing child. An unloaded child is reloaded through its actual, currently loaded parent using parent-derived configuration. If that owner-controlled reload cannot be performed, the request returns JSON-RPC error `-32600`; resume the parent first, or use `thread/read` or `thread/turns/list` to inspect the child's stored history without loading it. This policy follows the child's multi-agent runtime, including leaf workers whose models cannot delegate further. -By default, `thread/resume` includes the reconstructed turn history in `thread.turns`. Clients can pass `excludeTurns: true` to return only thread metadata and live resume state, then call `thread/turns/list` separately if they want to page the turn history over the network. A cold paginated resume can still replay persisted `thread/tokenUsage/updated` when it can identify the corresponding stored turn; resuming an already-loaded thread waits for the next live update. +By default, `thread/resume` includes the reconstructed turn history in `thread.turns`. Full-history hydration is deprecated for paginated threads and emits `deprecationNotice`; clients should pass `excludeTurns: true` to return only thread metadata and live resume state, then page with `thread/turns/list` and `thread/items/list`. A cold paginated resume can still replay persisted `thread/tokenUsage/updated` when it can identify the corresponding stored turn; resuming an already-loaded thread waits for the next live update. Paginated threads keep the same resume contract as legacy threads. A default resume materializes the full projected history into `thread.turns`; `excludeTurns: true` keeps that array empty and includes `turnsBackwardsCursor` and `itemsBackwardsCursor` for the durable history visible at the resume boundary. Pass each cursor directly to its matching list API with `sortDirection: "desc"`; the first page includes the row identified by the cursor, while newer records arrive through live notifications. Either cursor is `null` when there is no durable row yet. @@ -423,7 +423,7 @@ To branch from a stored session, call `thread/fork` with the `thread.id`. This c { "method": "thread/started", "params": { "thread": { … } } } ``` -Like `thread/resume`, clients can pass `excludeTurns: true` to `thread/fork` to return only thread metadata in `thread.turns` and page history with `thread/turns/list`. Metadata-only forks do not replay restored `thread/tokenUsage/updated`. Ephemeral forks of paginated threads require `excludeTurns: true`. +Like `thread/resume`, full-history hydration is deprecated for paginated `thread/fork` and emits `deprecationNotice`. Clients should pass `excludeTurns: true` to return only thread metadata in `thread.turns` and page history with `thread/turns/list` and `thread/items/list`. Metadata-only forks do not replay restored `thread/tokenUsage/updated`. Ephemeral forks of paginated threads require `excludeTurns: true`. ### Example: List threads (with pagination & filters) @@ -574,8 +574,10 @@ Later, after the idle unload timeout: Use `thread/read` to fetch a stored thread by id without resuming it. Pass `includeTurns` when you want thread history loaded into `thread.turns`. The returned thread includes `parentThreadId`, `agentNickname`, and `agentRole` for subagent threads when available. -Paginated threads can also use `includeTurns: true`, but clients should prefer -`thread/turns/list` and `thread/items/list` for incremental history loading. +Paginated threads can also use `includeTurns: true`, but full-history hydration +is deprecated and emits `deprecationNotice`. Clients should omit `includeTurns` +(or set it to `false`), then use `thread/turns/list` and `thread/items/list` for +incremental history loading. ```json { "method": "thread/read", "id": 22, "params": { "threadId": "thr_123" } } diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 7bc54ca0dc..a4009236a2 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -1329,7 +1329,7 @@ impl MessageProcessor { self.thread_processor.thread_loaded_list(params).await } ClientRequest::ThreadRead { params, .. } => { - self.thread_processor.thread_read(params).await + self.thread_processor.thread_read(&request_id, params).await } ClientRequest::ThreadTurnsList { params, .. } => { self.thread_processor.thread_turns_list(params).await diff --git a/codex-rs/app-server/src/request_processors/thread_processor.rs b/codex-rs/app-server/src/request_processors/thread_processor.rs index 686d5dd201..d443e90f12 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor.rs @@ -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, 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, 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 diff --git a/codex-rs/app-server/tests/suite/v2/thread_fork.rs b/codex-rs/app-server/tests/suite/v2/thread_fork.rs index f27dd8d554..5a47d56f1c 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_fork.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_fork.rs @@ -14,6 +14,7 @@ use codex_app_server_protocol::ActivePermissionProfile; use codex_app_server_protocol::ApprovalsReviewer; use codex_app_server_protocol::AskForApproval; use codex_app_server_protocol::ClientRequest; +use codex_app_server_protocol::DeprecationNoticeNotification; use codex_app_server_protocol::JSONRPCError; use codex_app_server_protocol::JSONRPCMessage; use codex_app_server_protocol::JSONRPCResponse; @@ -1494,6 +1495,76 @@ async fn thread_fork_creates_reference_backed_paginated_thread() -> Result<()> { Ok(()) } +#[tokio::test] +async fn thread_fork_warns_for_paginated_full_history_hydration() -> Result<()> { + let server = create_mock_responses_server_repeating_assistant("Done").await; + let codex_home = TempDir::new()?; + MockResponsesConfig::new(&server.uri()).write(codex_home.path())?; + + let conversation_id = create_fake_paginated_rollout( + codex_home.path(), + "2025-01-05T12-00-00", + "2025-01-05T12:00:00Z", + "Saved user message", + Some("mock_provider"), + /*git_info*/ None, + )?; + let mut mcp = TestAppServer::builder() + .with_codex_home(codex_home.path()) + .without_auto_env() + .build_initialized() + .await?; + + let fork_id = mcp + .send_thread_fork_request(ThreadForkParams { + thread_id: conversation_id.clone(), + ..Default::default() + }) + .await?; + let _: DeprecationNoticeNotification = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_notification("deprecationNotice"), + ) + .await??; + let _: ThreadForkResponse = timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(fork_id)).await??; + + mcp.clear_message_buffer(); + let metadata_fork_id = mcp + .send_thread_fork_request(ThreadForkParams { + thread_id: conversation_id.clone(), + exclude_turns: true, + ..Default::default() + }) + .await?; + let _: ThreadForkResponse = + timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(metadata_fork_id)).await??; + assert!( + !mcp.pending_notification_methods() + .contains(&"deprecationNotice".to_string()) + ); + + mcp.clear_message_buffer(); + let invalid_fork_id = mcp + .send_thread_fork_request(ThreadForkParams { + thread_id: conversation_id, + last_turn_id: Some("turn-1".to_string()), + before_turn_id: Some("turn-1".to_string()), + ..Default::default() + }) + .await?; + let _: JSONRPCError = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(invalid_fork_id)), + ) + .await??; + assert!( + !mcp.pending_notification_methods() + .contains(&"deprecationNotice".to_string()) + ); + + Ok(()) +} + #[tokio::test] async fn thread_fork_freezes_active_paginated_turn_as_interrupted() -> Result<()> { assert_thread_fork_freezes_active_paginated_turn_as_interrupted(MultiAgentVersion::V1).await 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 7d38b8205b..87298c6b79 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_read.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_read.rs @@ -11,6 +11,7 @@ use codex_app_server::in_process; use codex_app_server::in_process::InProcessStartArgs; use codex_app_server_protocol::ClientInfo; use codex_app_server_protocol::ClientRequest; +use codex_app_server_protocol::DeprecationNoticeNotification; use codex_app_server_protocol::InitializeCapabilities; use codex_app_server_protocol::InitializeParams; use codex_app_server_protocol::JSONRPCError; @@ -209,6 +210,10 @@ async fn thread_read_can_include_turns() -> Result<()> { other => panic!("expected user message item, got {other:?}"), } assert_eq!(thread.status, ThreadStatus::NotLoaded); + assert!( + !mcp.pending_notification_methods() + .contains(&"deprecationNotice".to_string()) + ); Ok(()) } @@ -244,6 +249,31 @@ async fn paginated_stored_thread_routes_projected_turns() -> Result<()> { timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(read_id)).await??; assert_eq!(thread.history_mode, ThreadHistoryMode::Paginated); assert!(thread.turns.is_empty()); + assert!( + !mcp.pending_notification_methods() + .contains(&"deprecationNotice".to_string()) + ); + + let full_read_id = mcp + .send_thread_read_request(ThreadReadParams { + thread_id: conversation_id.clone(), + include_turns: true, + }) + .await?; + let notice: DeprecationNoticeNotification = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_notification("deprecationNotice"), + ) + .await??; + assert_eq!( + notice, + DeprecationNoticeNotification { + summary: "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`.".to_string(), + details: None, + } + ); + let _: ThreadReadResponse = + timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(full_read_id)).await??; let list_id = mcp .send_thread_list_request(ThreadListParams { 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 7250a3d752..f0f5f75351 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -24,6 +24,7 @@ use codex_app_server_protocol::ClientInfo; use codex_app_server_protocol::ClientRequest; use codex_app_server_protocol::CommandExecutionApprovalDecision; use codex_app_server_protocol::CommandExecutionRequestApprovalResponse; +use codex_app_server_protocol::DeprecationNoticeNotification; use codex_app_server_protocol::FileChangeApprovalDecision; use codex_app_server_protocol::FileChangeRequestApprovalResponse; use codex_app_server_protocol::ItemStartedNotification; @@ -2385,6 +2386,80 @@ async fn thread_resume_can_skip_turns_for_metadata_only_resume() -> Result<()> { Ok(()) } +#[tokio::test] +async fn thread_resume_warns_for_paginated_full_history_hydration() -> Result<()> { + let server = create_mock_responses_server_repeating_assistant("Done").await; + let codex_home = TempDir::new()?; + mock_responses_config(&server.uri()).write(codex_home.path())?; + + let conversation_id = create_fake_paginated_rollout( + codex_home.path(), + "2025-01-05T12-00-00", + "2025-01-05T12:00:00Z", + "Saved user message", + Some("mock_provider"), + /*git_info*/ None, + )?; + let mut mcp = TestAppServer::builder() + .with_codex_home(codex_home.path()) + .without_auto_env() + .build_initialized() + .await?; + + let cold_resume_id = mcp + .send_thread_resume_request(ThreadResumeParams { + thread_id: conversation_id.clone(), + ..Default::default() + }) + .await?; + let notice: DeprecationNoticeNotification = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_notification("deprecationNotice"), + ) + .await??; + assert_eq!( + notice, + DeprecationNoticeNotification { + summary: "Full-history hydration is deprecated for paginated threads; use `excludeTurns: true`, then page with `thread/turns/list` and `thread/items/list`.".to_string(), + details: None, + } + ); + let _: ThreadResumeResponse = + timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(cold_resume_id)).await??; + + mcp.clear_message_buffer(); + let loaded_resume_id = mcp + .send_thread_resume_request(ThreadResumeParams { + thread_id: conversation_id.clone(), + ..Default::default() + }) + .await?; + let _: DeprecationNoticeNotification = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_notification("deprecationNotice"), + ) + .await??; + let _: ThreadResumeResponse = + timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(loaded_resume_id)).await??; + + mcp.clear_message_buffer(); + let metadata_resume_id = mcp + .send_thread_resume_request(ThreadResumeParams { + thread_id: conversation_id, + exclude_turns: true, + ..Default::default() + }) + .await?; + let _: ThreadResumeResponse = + timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(metadata_resume_id)).await??; + assert!( + !mcp.pending_notification_methods() + .contains(&"deprecationNotice".to_string()) + ); + + Ok(()) +} + #[tokio::test] async fn thread_resume_rejects_archived_session_by_id() -> Result<()> { let server = create_mock_responses_server_repeating_assistant("Done").await; diff --git a/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__safety_buffering__safety_retry_committed_steer_history.snap b/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__safety_buffering__safety_retry_committed_steer_history.snap index 91e7e27887..3480bc55d1 100644 --- a/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__safety_buffering__safety_retry_committed_steer_history.snap +++ b/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__safety_buffering__safety_retry_committed_steer_history.snap @@ -4,4 +4,4 @@ expression: rendered_retry --- › Handle the safety-buffered request Keep the accepted steer -• done +⚠ 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`.• done