mirror of
https://github.com/openai/codex.git
synced 2026-11-09 08:40:33 +00:00
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:
1 parent
5cb7a35de9
commit
d132b69219
19 files changed
+261
-29
No files matched your search
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user