Skip to content

Commit 7234f92

Browse files
committed
Add includeTurns parameter to thread/resume for skipping to pagination
For callers who expect to be paginating the results, they can now call thread/resume with includeTurns:false so it will not fetch any pages of turns, and instead only set up the subscription. That call can be immediately followed by pagination requests to thread/turns/list to fetch pages of turns according to the UI's current interactions.
1 parent ddde50c commit 7234f92

10 files changed

Lines changed: 186 additions & 13 deletions

File tree

codex-rs/app-server-protocol/schema/json/ClientRequest.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/app-server-protocol/src/protocol/v2.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3288,6 +3288,11 @@ pub struct ThreadResumeParams {
32883288
pub developer_instructions: Option<String>,
32893289
#[ts(optional = nullable)]
32903290
pub personality: Option<Personality>,
3291+
/// When false, return only thread metadata and live-resume state without
3292+
/// populating `thread.turns`. This is useful when the client plans to call
3293+
/// `thread/turns/list` immediately after resuming.
3294+
#[ts(optional = nullable)]
3295+
pub include_turns: Option<bool>,
32913296
/// If true, persist additional rollout EventMsg variants required to
32923297
/// reconstruct a richer thread history on subsequent resume/fork/read.
32933298
#[experimental("thread/resume.persistFullHistory")]

codex-rs/app-server/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,8 @@ Valid `personality` values are `"friendly"`, `"pragmatic"`, and `"none"`. When `
257257

258258
To continue a stored session, call `thread/resume` with the `thread.id` you previously recorded. The response shape matches `thread/start`. When the stored session includes persisted token usage, the server emits `thread/tokenUsage/updated` immediately after the response so clients can render restored usage before the next turn starts. You can also pass the same configuration overrides supported by `thread/start`, including `approvalsReviewer`.
259259

260+
By default, `thread/resume` includes the reconstructed turn history in `thread.turns`. Pass `includeTurns: false` to return only thread metadata and live resume state, then call `thread/turns/list` separately if you want to page the turn history over the network.
261+
260262
By default, resume uses the latest persisted `model` and `reasoningEffort` values associated with the thread. Supplying any of `model`, `modelProvider`, `config.model`, or `config.model_reasoning_effort` disables that persisted fallback and uses the explicit overrides plus normal config resolution instead.
261263

262264
Example:
@@ -267,6 +269,12 @@ Example:
267269
"personality": "friendly"
268270
} }
269271
{ "id": 11, "result": { "thread": { "id": "thr_123", } } }
272+
273+
{ "method": "thread/resume", "id": 12, "params": {
274+
"threadId": "thr_123",
275+
"includeTurns": false
276+
} }
277+
{ "id": 12, "result": { "thread": { "id": "thr_123", "turns": [], } } }
270278
```
271279

272280
To branch from a stored session, call `thread/fork` with the `thread.id`. This creates a new thread id and emits a `thread/started` notification for it. When the source history includes persisted token usage, the server also emits `thread/tokenUsage/updated` for the new thread immediately after the response. If the source thread is actively running, the fork snapshots it as if the current turn had been interrupted first. Pass `ephemeral: true` when the fork should stay in-memory only:

codex-rs/app-server/src/codex_message_processor.rs

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4363,8 +4363,10 @@ impl CodexMessageProcessor {
43634363
base_instructions,
43644364
developer_instructions,
43654365
personality,
4366+
include_turns,
43664367
persist_extended_history,
43674368
} = params;
4369+
let include_turns = include_turns.unwrap_or(true);
43684370

43694371
let thread_history = if let Some(history) = history {
43704372
let Some(thread_history) = self
@@ -4471,6 +4473,7 @@ impl CodexMessageProcessor {
44714473
rollout_path.as_path(),
44724474
fallback_model_provider.as_str(),
44734475
persisted_resume_metadata.as_ref(),
4476+
include_turns,
44744477
)
44754478
.await
44764479
{
@@ -4732,6 +4735,7 @@ impl CodexMessageProcessor {
47324735
config_snapshot,
47334736
instruction_sources,
47344737
thread_summary,
4738+
include_turns: params.include_turns.unwrap_or(true),
47354739
}),
47364740
);
47374741
if listener_command_tx.send(command).is_err() {
@@ -4837,6 +4841,7 @@ impl CodexMessageProcessor {
48374841
rollout_path: &Path,
48384842
fallback_provider: &str,
48394843
persisted_resume_metadata: Option<&ThreadMetadata>,
4844+
include_turns: bool,
48404845
) -> std::result::Result<Thread, String> {
48414846
let thread = match thread_history {
48424847
InitialHistory::Resumed(resumed) => {
@@ -4866,13 +4871,15 @@ impl CodexMessageProcessor {
48664871
let mut thread = thread?;
48674872
thread.id = thread_id.to_string();
48684873
thread.path = Some(rollout_path.to_path_buf());
4869-
let history_items = thread_history.get_rollout_items();
4870-
populate_thread_turns(
4871-
&mut thread,
4872-
ThreadTurnSource::HistoryItems(&history_items),
4873-
/*active_turn*/ None,
4874-
)
4875-
.await?;
4874+
if include_turns {
4875+
let history_items = thread_history.get_rollout_items();
4876+
populate_thread_turns(
4877+
&mut thread,
4878+
ThreadTurnSource::HistoryItems(&history_items),
4879+
/*active_turn*/ None,
4880+
)
4881+
.await?;
4882+
}
48764883
self.attach_thread_name(thread_id, &mut thread).await;
48774884
Ok(thread)
48784885
}
@@ -8481,12 +8488,13 @@ async fn handle_pending_thread_resume_request(
84818488
let request_id = pending.request_id;
84828489
let connection_id = request_id.connection_id;
84838490
let mut thread = pending.thread_summary;
8484-
if let Err(message) = populate_thread_turns(
8485-
&mut thread,
8486-
ThreadTurnSource::RolloutPath(pending.rollout_path.as_path()),
8487-
active_turn.as_ref(),
8488-
)
8489-
.await
8491+
if pending.include_turns
8492+
&& let Err(message) = populate_thread_turns(
8493+
&mut thread,
8494+
ThreadTurnSource::RolloutPath(pending.rollout_path.as_path()),
8495+
active_turn.as_ref(),
8496+
)
8497+
.await
84908498
{
84918499
outgoing
84928500
.send_error(
@@ -10337,6 +10345,7 @@ mod tests {
1033710345
base_instructions: None,
1033810346
developer_instructions: None,
1033910347
personality: None,
10348+
include_turns: None,
1034010349
persist_extended_history: false,
1034110350
};
1034210351
let config_snapshot = ThreadConfigSnapshot {

codex-rs/app-server/src/thread_state.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ pub(crate) struct PendingThreadResumeRequest {
3131
pub(crate) config_snapshot: ThreadConfigSnapshot,
3232
pub(crate) instruction_sources: Vec<AbsolutePathBuf>,
3333
pub(crate) thread_summary: codex_app_server_protocol::Thread,
34+
pub(crate) include_turns: bool,
3435
}
3536

3637
// ThreadListenerCommand is used to perform operations in the context of the thread listener, for serialization purposes.

codex-rs/app-server/tests/suite/v2/thread_resume.rs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,45 @@ async fn thread_resume_returns_rollout_history() -> Result<()> {
285285
Ok(())
286286
}
287287

288+
#[tokio::test]
289+
async fn thread_resume_can_skip_turns_for_metadata_only_resume() -> Result<()> {
290+
let server = create_mock_responses_server_repeating_assistant("Done").await;
291+
let codex_home = TempDir::new()?;
292+
create_config_toml(codex_home.path(), &server.uri())?;
293+
294+
let conversation_id = create_fake_rollout_with_text_elements(
295+
codex_home.path(),
296+
"2025-01-05T12-00-00",
297+
"2025-01-05T12:00:00Z",
298+
"Saved user message",
299+
Vec::new(),
300+
Some("mock_provider"),
301+
/*git_info*/ None,
302+
)?;
303+
304+
let mut mcp = McpProcess::new(codex_home.path()).await?;
305+
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
306+
307+
let resume_id = mcp
308+
.send_thread_resume_request(ThreadResumeParams {
309+
thread_id: conversation_id.clone(),
310+
include_turns: Some(false),
311+
..Default::default()
312+
})
313+
.await?;
314+
let resume_resp: JSONRPCResponse = timeout(
315+
DEFAULT_READ_TIMEOUT,
316+
mcp.read_stream_until_response_message(RequestId::Integer(resume_id)),
317+
)
318+
.await??;
319+
let ThreadResumeResponse { thread, .. } = to_response::<ThreadResumeResponse>(resume_resp)?;
320+
321+
assert_eq!(thread.id, conversation_id);
322+
assert!(thread.turns.is_empty());
323+
324+
Ok(())
325+
}
326+
288327
#[tokio::test]
289328
async fn thread_resume_emits_restored_token_usage_before_next_turn() -> Result<()> {
290329
let server = create_mock_responses_server_repeating_assistant("Done").await;
@@ -1339,6 +1378,84 @@ async fn thread_resume_rejoins_running_thread_even_with_override_mismatch() -> R
13391378
Ok(())
13401379
}
13411380

1381+
#[tokio::test]
1382+
async fn thread_resume_can_skip_turns_when_thread_is_running() -> Result<()> {
1383+
let server = responses::start_mock_server().await;
1384+
let _response_mock = responses::mount_sse_once(
1385+
&server,
1386+
responses::sse(vec![
1387+
responses::ev_response_created("resp-1"),
1388+
responses::ev_assistant_message("msg-1", "Done"),
1389+
responses::ev_completed("resp-1"),
1390+
]),
1391+
)
1392+
.await;
1393+
let codex_home = TempDir::new()?;
1394+
create_config_toml(codex_home.path(), &server.uri())?;
1395+
1396+
let mut primary = McpProcess::new(codex_home.path()).await?;
1397+
timeout(DEFAULT_READ_TIMEOUT, primary.initialize()).await??;
1398+
1399+
let start_id = primary
1400+
.send_thread_start_request(ThreadStartParams {
1401+
model: Some("gpt-5.4".to_string()),
1402+
..Default::default()
1403+
})
1404+
.await?;
1405+
let start_resp: JSONRPCResponse = timeout(
1406+
DEFAULT_READ_TIMEOUT,
1407+
primary.read_stream_until_response_message(RequestId::Integer(start_id)),
1408+
)
1409+
.await??;
1410+
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(start_resp)?;
1411+
1412+
let turn_id = primary
1413+
.send_turn_start_request(TurnStartParams {
1414+
thread_id: thread.id.clone(),
1415+
input: vec![UserInput::Text {
1416+
text: "seed history".to_string(),
1417+
text_elements: Vec::new(),
1418+
}],
1419+
..Default::default()
1420+
})
1421+
.await?;
1422+
timeout(
1423+
DEFAULT_READ_TIMEOUT,
1424+
primary.read_stream_until_response_message(RequestId::Integer(turn_id)),
1425+
)
1426+
.await??;
1427+
timeout(
1428+
DEFAULT_READ_TIMEOUT,
1429+
primary.read_stream_until_notification_message("turn/completed"),
1430+
)
1431+
.await??;
1432+
1433+
let mut secondary = McpProcess::new(codex_home.path()).await?;
1434+
timeout(DEFAULT_READ_TIMEOUT, secondary.initialize()).await??;
1435+
1436+
let resume_id = secondary
1437+
.send_thread_resume_request(ThreadResumeParams {
1438+
thread_id: thread.id.clone(),
1439+
include_turns: Some(false),
1440+
..Default::default()
1441+
})
1442+
.await?;
1443+
let resume_resp: JSONRPCResponse = timeout(
1444+
DEFAULT_READ_TIMEOUT,
1445+
secondary.read_stream_until_response_message(RequestId::Integer(resume_id)),
1446+
)
1447+
.await??;
1448+
let ThreadResumeResponse {
1449+
thread: resumed, ..
1450+
} = to_response::<ThreadResumeResponse>(resume_resp)?;
1451+
1452+
assert_eq!(resumed.id, thread.id);
1453+
assert_eq!(resumed.status, ThreadStatus::Idle);
1454+
assert!(resumed.turns.is_empty());
1455+
1456+
Ok(())
1457+
}
1458+
13421459
#[tokio::test]
13431460
async fn thread_resume_replays_pending_command_execution_request_approval() -> Result<()> {
13441461
let responses = vec![

0 commit comments

Comments
 (0)