Preserve parent cache affinity for ephemeral forks (#44862)

## Why

ChatGPT derives Responses cache affinity from the `session-id` header. Ephemeral forks need to reuse their parent's cache routing while retaining their own session and thread identities.

## What changed

- Inherit the parent session ID as the `prompt_cache_key` for ephemeral root forks.
- Use the prompt cache key for root-agent Responses `session-id` headers, including WebSocket handshakes. Preserve the actual session identity in turn metadata and leave non-root-agent routing unchanged.
- Keep enabled goal tool definitions visible on ephemeral threads, but reject execution with `Goal tools require a persistent thread.` and disable automatic goal continuation without persistent state.

## Testing

Add regression coverage for inherited cache routing with distinct fork identities, matching parent and fork tool definitions, WebSocket session headers, and rejection of ephemeral goal tool execution.

GitOrigin-RevId: d235e97630068b27f7ebd562dd23ac6266ebc1b9
This commit is contained in:
jif authored and copyberry committed 2026-09-11 16:41:56 +00:00
1 parent 0818b6550b
commit bc5957eac9
8 files changed
+167 -22

No files matched your search

@@ -158,3 +158,63 @@ async fn api_key_subagent_uses_session_id_as_prompt_cache_key() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn ephemeral_fork_shares_cache_routing_but_keeps_session_identity() -> Result<()> {
use codex_core::ForkSnapshot;
use codex_core::StartThreadOptions;
use core_test_support::responses::mount_sse_sequence;
let server = start_mock_server().await;
let requests = mount_sse_sequence(
&server,
vec![
sse(vec![ev_completed("parent")]),
sse(vec![ev_completed("fork")]),
],
)
.await;
let mut test = test_codex().build_with_auto_env(&server).await?;
test.submit_text_turn("parent").await?;
test.codex.flush_rollout().await?;
let parent_session = test.session_configured.session_id.to_string();
let mut config = test.config.clone();
config.ephemeral = true;
let fork = test
.thread_manager
.fork_thread(
ForkSnapshot::TruncateBeforeNthUserMessage(usize::MAX),
StartThreadOptions {
environments: Some(test.codex.environment_selections().await),
..StartThreadOptions::new(config)
},
test.codex.rollout_path().expect("parent rollout"),
)
.await?;
let fork_session = fork.session_configured.session_id.to_string();
assert_ne!(fork_session, parent_session);
test.codex = fork.thread;
test.submit_text_turn("side").await?;
let requests = requests.requests();
let body = requests[1].body_json();
let metadata: Value = serde_json::from_str(
body["client_metadata"]["x-codex-turn-metadata"]
.as_str()
.expect("turn metadata"),
)?;
assert_eq!(
json!({
"cache": body["prompt_cache_key"],
"route": requests[1].header("session-id"),
"session": metadata["session_id"],
"thread": requests[1].header("thread-id"),
"tools": body["tools"],
}),
json!({
"cache": parent_session, "route": parent_session,
"session": fork_session, "thread": fork.thread_id.to_string(),
"tools": requests[0].body_json()["tools"],
}),
);
Ok(())
}