[codex] Omit fork turns from thread started notifications (#19093)

## Why

`thread/fork` responses intentionally include copied history so the
caller can render the fork immediately, but `thread/started` is a
lifecycle notification. The v2 `Thread` contract says notifications
should return `turns: []`, and the fork path was reusing the response
thread directly, causing copied turns to be emitted through
`thread/started` as well.

## What Changed

- Route app-server `thread/started` notification construction through a
helper that clears `thread.turns` before sending.
- Keep `thread/fork` responses unchanged so callers still receive copied
history.
- Add persistent and ephemeral fork coverage that asserts
`thread/started` emits an empty `turns` array while the response retains
fork history.

## Testing

- `just fmt`
- `cargo test -p codex-app-server`
This commit is contained in:
Ruslan Nigmatullin
2026-04-24 12:31:13 -07:00
committed by GitHub
parent 0db6811b7c
commit a3cccbd8ed
2 changed files with 24 additions and 5 deletions

View File

@@ -181,9 +181,16 @@ async fn thread_fork_creates_new_thread_and_emits_started() -> Result<()> {
Some(&Value::Null),
"thread/started must serialize `name: null` when unset"
);
assert_eq!(
started_thread_json.get("turns"),
Some(&json!([])),
"thread/started must not emit copied fork turns"
);
let started: ThreadStartedNotification =
serde_json::from_value(notif.params.expect("params must be present"))?;
assert_eq!(started.thread, thread);
let mut expected_started_thread = thread;
expected_started_thread.turns.clear();
assert_eq!(started.thread, expected_started_thread);
Ok(())
}
@@ -582,9 +589,16 @@ async fn thread_fork_ephemeral_remains_pathless_and_omits_listing() -> Result<()
Some(true),
"thread/started should serialize `ephemeral: true` for ephemeral forks"
);
assert_eq!(
started_thread_json.get("turns"),
Some(&json!([])),
"thread/started must not emit copied ephemeral fork turns"
);
let started: ThreadStartedNotification =
serde_json::from_value(notif.params.expect("params must be present"))?;
assert_eq!(started.thread, thread);
let mut expected_started_thread = thread;
expected_started_thread.turns.clear();
assert_eq!(started.thread, expected_started_thread);
let list_id = mcp
.send_thread_list_request(ThreadListParams {