Forks and fresh sessions lose reported cache reuse; session-id affects reuse independently of prompt_cache_key
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 125k
- Forks
- 19.4k
- PR merge metrics
- PR metrics pending
Description
Upstream confirmation — September 17 update
Merged upstream PR #44862 (September 11, merge commit bc5957eac9e89e66f990ed490d11e625a4a3b02c) explicitly documents that ChatGPT derives Responses cache affinity from the session-id header. Its implementation uses the effective prompt cache key for root-agent HTTP and WebSocket session headers, while retaining actual session identity in metadata. This supplies upstream confirmation of the cache-affinity mechanism implicated by the experiments below.
The automatic parent-key inheritance in that change is explicitly limited to ephemeral root forks (config.ephemeral), not all persistent user forks or fresh sessions. It also preserves enabled goal tool definitions on ephemeral forks to avoid changing the tool prefix. This is a merged source change, not a verified deployment or a live post-fix cache benchmark. The individual response IDs below have not received a backend trace interpretation, so physical recomputation versus reported credit is still unproven.
The experimental record below predates that change; its requests for mechanism confirmation should be read with this update. The issue remains open because the merged change does not establish coverage for ordinary persistent forks.
Summary
Forks and fresh sessions can lose reported cache reuse for inherited or repeated context on the ChatGPT-authenticated Codex backend. Controlled experiments implicate HTTP/WebSocket-handshake session-id independently of JSON prompt_cache_key. Preserving only the parent's JSON key is not a demonstrated fix, and a fork does not invariably miss.
The strongest first-fork experiment retained only the parent's HTTP session header while leaving the child's entire body and own cache key unchanged: it reused 40,064 / 41,565 input tokens, before any sibling sent a model request. Native and parent-key-only siblings each reused 12,928 / 41,565. The parent remained warm throughout.
This establishes an effect on reported cached-input usage, not the underlying server mechanism or physical KV-cache recomputation. Backend trace interpretation is requested below. /side is in the original reported scope but was not separately exercised through its UI.
Environment and method
- Installed codex-cli 0.154.0, macOS arm64, existing ChatGPT authentication; no public API-key experiment.
- Valid controlled comparisons requested and returned gpt-6-astra, low reasoning, returned service tier
default. - Real sessions and forks used app-server JSON-RPC with long synthetic reference prompts. HTTP request interventions used a loopback forwarding proxy; direct WebSocket controls used the authenticated Codex responses endpoint and
responses_websockets=2026-02-06. - Frozen replays kept complete request bodies/settings fixed except the named intervention, did not append answers, and did not use
previous_response_id. Usage came from upstreamresponse.completed; the initial app-server benchmark used per-requesttokenUsage.last, not inherited cumulative totals. - Later fork/WebSocket batches spaced request starts at least ten seconds apart. Other account traffic and backend load were not controlled.
- Captured credentials and full requests remained in memory. Saved evidence includes response IDs, usage, hashes, and structural comparisons. No product code or global configuration was changed.
- Source references are pinned to
da20788df913189878ebca7f4963d8a363ee6bf2; tests used the installed binary, not a rebuilt checkout.
Measured results
Initial real-fork benchmark
Two forks were initialized from the same parent snapshot before the parent control prompt.
| Request | Input tokens | Cached tokens |
|---|---|---|
| Parent cold | 35,068 | 0 |
| Parent warm | 40,281 | 34,944 |
| Parent control | 40,299 | 40,064 |
| Fork 1 first | 41,951 | 0 |
| Fork 1 follow-up | 41,969 | 41,728 |
| Fork 2 first | 41,951 | 0 |
| Fork 2 follow-up | 41,969 | 41,728 |
| Parent return | 40,317 | 40,064 |
This reproduced first-fork reported misses, but child inputs were 1,652 tokens larger and full requests were not captured in this initial benchmark, so it did not isolate causation.
Frozen-key, fresh-session, and identity controls
- Frozen fork replays with original, parent, and never-used random JSON cache keys all reused 36,608 / 36,818 input tokens. Frozen parent replays with original and never-used keys both reused 34,944 / 35,166. Bodies excluding the key and forwarded headers were identical within each group. The fork body was already warm: this does not prove that using the parent's key prevents the initial miss.
- Three actual fresh sessions initially reported zero cached tokens, then exact replays reused 34,944. The first session had 35,059 input tokens; the other two had 35,119. Their full bodies differed in input, metadata, and cache key, so equal user prompts were not complete-input controls.
- With frozen 35,059-token input, two fresh identity-bundle interventions retaining the original JSON key yielded 0 and 7,936 cached tokens; key-only interventions retained the 34,944 baseline. The bundle changed body identity metadata and five identity headers together. Later both-changed requests reused already-warmed identities and are not independent cold controls.
- Narrowed single-field tests on 35,059 input tokens: changing only HTTP
session-idyielded 21,376 cached tokens in each of two fresh trials. Changing only JSON key, body thread metadata, or HTTPthread-idretained 34,944. Session-only body hashes were identical, with no other header changes. - Randomized header interaction trials were variable: some fresh identities fully hit, and one final unchanged baseline returned zero. Stable identity therefore does not guarantee reuse, nor does changed identity guarantee a miss.
Paced interventions before the child's first request
Children were initialized from the same parent snapshot. Model, reasoning, and instructions were checked before forwarding. Parent return controls replayed the exact frozen parent request, without growing its history.
| Cohort A: six forks, randomized treatment order | Cached tokens |
|---|---|
| Native child identity/key | 0; 0 |
| Only JSON key replaced with parent's key | 12,928; 0 |
| Only HTTP session-id replaced with parent's value | 40,064; 41,344 |
Parent: 40,295 input / 40,064 cached. Every child: 41,563 input. All six parent returns retained 40,064 cached tokens. The later 41,344 result may benefit from an earlier sibling warming shared context; it cannot be attributed exclusively to the parent.
Cohort B used a new parent and unique synthetic prompt nonce, and ran the session-header intervention before any sibling model request.
| Cohort B, execution order | Input | Cached |
|---|---|---|
| Parent control | 40,297 | 40,064 |
| Child 1: only parent HTTP session-id; child's own JSON key retained | 41,565 | 40,064 |
| Child 2: native identity/key | 41,565 | 12,928 |
| Child 3: only parent's JSON key | 41,565 | 12,928 |
All three interleaved parent returns retained 40,064 cached tokens. Child 1's before/sent body hashes were identical; its sole changed header was session-id. Ignoring item id fields, the first 12 parent/child input items were identical. The child added a 5,665-byte developer item before the final user message, increasing reported input by 1,268 tokens. Ignoring IDs is a structural comparison technique, not an assertion about backend ID semantics.
WebSocket controls
Direct replay of a captured real HTTP request over WebSocket, not a native TUI WebSocket fork test. All responses had 35,118 input tokens, with full inputs/settings fixed except the stated treatment.
| Treatment, execution order | Cached tokens |
|---|---|
| Original socket after HTTP seed | 34,944 |
| Exact repeat, same socket | 34,944 |
| New JSON cache key, same socket | 34,944 |
| New body identity metadata, same socket | 34,944 |
| New socket, unchanged identity/body | 34,944 |
| New socket, only fresh handshake session-id | 0 |
| Another socket, repeat that new session-id | 34,944 |
| New socket, fresh identity-header bundle | 21,376 |
| New socket, change other identity headers but retain original session-id | 34,944 |
| New socket, original baseline | 34,944 |
Reconnecting alone did not reproduce the session-header intervention's miss. A diagnostics request returned Unsupported parameter: prompt_cache_options. Earlier HTTP comparison-diagnostics attempts returned 400 without an explanatory message captured.
Source findings
- Session initialization: normal forks receive a new thread ID and derive a new session ID.
- Default JSON key selection: absent an override or internal-session exception, the key is the session ID. This proves a client key change, not that the key alone causes the observed misses.
- HTTP identity options and WebSocket handshake carry session/thread identities independently.
- Per-turn sticky routing: server-issued
x-codex-turn-statemust be reused within a turn and must not carry into another turn. This token is not a supported cross-fork workaround. - Routing hint construction encodes model and optional service tier; it does not expose backend machine selection.
Limitations and excluded experiments
- The original cache-key-only explanation and proposed key-inheritance fix were too strong. Later controls contradict deterministic key-only causation. Neither key inheritance nor parent-session-header reuse is a validated production fix.
- An initial screening response changed reported input count despite unchanged body. An assertion fired before its counters were saved; that unknown numeric result is excluded. Later harness versions saved anomalous responses before consistency checks.
- One early paced fork cohort omitted explicit fork configuration, returning Astra for the parent and Sol for children. The entire cross-model cohort is excluded. Corrected cohorts explicitly pinned and checked configuration. This harness error is not evidence that native TUI fork changes models.
- Fast exploratory batches sometimes exceeded 15 requests/minute; overflow routing was a possible confound, not a demonstrated explanation for the unchanged-control miss. Later batches were paced.
- Offline evidence verification passes for recorded findings and exclusions; this does not prove universal backend behavior. No separate
/sideUI run or full native TUI WebSocket fork run was performed. - Reported usage cannot establish physical recomputation. Official diagnostics documentation explicitly permits a key-related reported miss without a physical miss. Public API documentation does not establish every behavior of the Codex-authenticated gateway.
Expected outcome and maintainer questions
Preserve eligible inherited-prefix reuse where supported while maintaining independent conversation identities. The mechanism and supported fix require backend confirmation; blindly retaining a parent's session header may alter identity semantics or telemetry.
- Does the Codex gateway override or supplement
prompt_cache_keyusing HTTP/handshakesession-idfor routing, cache accounting, or hidden-context selection? - Can the response IDs below establish whether the native child physically recomputed the inherited prefix or merely lost cached-input credit?
- What supported mechanism preserves fork cache affinity without conflating conversation identities? Is a supported comparison-diagnostics interface available for ChatGPT-authenticated Codex requests?
| Cohort B request | Response ID |
|---|---|
| Parent control | resp_0a22c55f2305c9b4016aa380d1635087d2b1d91fdbd31e8be2 |
| Parent-session-header first child | resp_0a22c55f2305c9b4016aa380db3f1487d2a6c0948c20fcf3b3 |
| Native child | resp_0a22c55f2305c9b4016aa380ef35f887d2aaa25d31d9e6d7df |
| Parent-key-only child | resp_0a22c55f2305c9b4016aa3810327b887d28c56a504f30f3f9d |
Related: #24704 concerns forked subagent cache lineage; this report concerns normal user forks and fresh sessions. The request-level effect is established on the tested paths; the server-internal explanation remains unresolved.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the cited session initialization in codex-rs/core/src/session/session.rs and the identity, handshake, and routing code in codex-rs/core/src/client.rs. Reproduce the documented HTTP and WebSocket controls before drawing conclusions. Done requires maintainer or backend confirmation of supported cache-affinity behavior and coverage for ordinary persistent forks, not just the merged ephemeral-fork change.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- backend, performance
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100