feat(app-server): add optional turn_id to thread/fork (#30277)

## Description

This adds stable optional `turnId` support to `thread/fork`. When
supplied, the fork copies persisted history through that terminal turn,
inclusive, and drops later turns from the new thread.

Omitting or passing `null` preserves the existing full-history fork
behavior, including the interruption marker when the stored source
history ends mid-turn.

## Why

We're deprecating `thread/rollback` and this will help certain UX use
cases work around it by using `thread/fork` + `turn_id` instead.
This commit is contained in:
Owen Lin
2026-06-26 12:35:54 -07:00
committed by GitHub
Unverified
parent 812cd2bb57
commit f72976a5f1
14 changed files with 352 additions and 6 deletions
@@ -839,6 +839,30 @@ fn thread_path_params_deserialize_empty_path_as_none() {
);
}
#[test]
fn thread_fork_last_turn_id_round_trips() {
let params: ThreadForkParams = serde_json::from_value(json!({
"threadId": "thread-1",
"lastTurnId": "turn-2",
}))
.expect("thread/fork params deserialize");
assert_eq!(params.last_turn_id, Some("turn-2".to_string()));
let serialized = serde_json::to_value(params).expect("thread/fork params serialize");
assert_eq!(serialized["lastTurnId"], json!("turn-2"));
let omitted = serde_json::to_value(ThreadForkParams {
thread_id: "thread-1".to_string(),
..Default::default()
})
.expect("thread/fork params without last turn id serialize");
assert_eq!(
omitted["lastTurnId"],
serde_json::Value::Null,
"optional lastTurnId should serialize as null when omitted"
);
}
#[test]
fn fs_get_metadata_response_round_trips_minimal_fields() {
let response = FsGetMetadataResponse {
@@ -494,6 +494,13 @@ impl From<ThreadTurnsListResponse> for TurnsPage {
pub struct ThreadForkParams {
pub thread_id: String,
/// Optional last turn id to fork through, inclusive.
///
/// When specified, turns after `last_turn_id` are omitted from the fork.
/// The referenced turn cannot be in progress.
#[ts(optional = nullable)]
pub last_turn_id: Option<String>,
/// [UNSTABLE] Specify the rollout path to fork from.
/// If specified, the thread_id param will be ignored.
#[experimental("thread/fork.path")]