Skip to content

Attribute Codex requests with canonical session, thread, and turn identity - #1303

Merged
mpscholten merged 1 commit into
masterfrom
feature/codex-turn-attribution
Sep 14, 2026
Merged

mpscholten merged 1 commit into
masterfrom
feature/codex-turn-attribution

Conversation

@mpscholten

@mpscholten mpscholten commented Sep 14, 2026

Copy link
Copy Markdown
Member

Summary

Follow-up to the check against earendil-works/pi#9488: our Codex transport sent none of the identity metadata the official client uses for turn attribution, so tool continuations and retries reached the backend as unlabelled requests. This PR sends the same attribution set as codex-rs.

Wire changes (HTTP /responses and WebSocket response.create)

  • Headers: originator: haskell-agent, session-id, thread-id, x-client-request-id (= thread id), x-codex-window-id (<thread>:<n>), x-codex-turn-metadata (ASCII-escaped JSON with session_id, thread_id, turn_id, window_id, window_number, context_window_id, request_kind, turn_started_at_unix_ms).
  • Body / frame client_metadata: session_id, thread_id, turn_id, x-codex-window-id, x-codex-turn-metadata, merged with the existing x-codex-turn-state and Responses Lite fields.
  • WebSocket handshake: adds the static originator header only; per-turn attribution travels in each frame because a socket outlives turns.

Identity lifecycle

  • Agent.OpenAI.TurnState widens the existing per-connection CodexTurnState: next to the first-write-wins x-codex-turn-state token it holds a UUIDv7 turn id minted on the first request of a logical turn and reused by tool continuations, transport retries, reconnects (copyCodexTurnState), HTTP fallback, and inline compaction. resetCodexTurnState (new prompt, turn completion, backend reset) rotates it.
  • Session and thread id = the request's prompt_cache_key, i.e. the persisted session id — the same rule codex-rs uses for its session-id header. Unpersisted sessions get a stable generated fallback per connection.
  • Compaction requests (remoteCompactionV2RequestOptions, sendWsRequestWithEventsPreservingTurnState) are labelled request_kind: compaction while staying in the current turn. A committed manual or automatic compaction advances the context-window generation for subsequent requests (Agent.Runtime.Providers.OpenAI).
  • Requests made without a turn scope (createCodexMessageWithProviderAt and friends) are attributed as standalone turns.
  • Agent.Uuid (agent-core) provides the RFC 9562 UUID version 7 generator.

Known limits

  • The context-window generation is in-memory: it restarts at 0 on session resume and on an interactive account switch (the compaction count is not persisted). Codex-rs persists it in the rollout.
  • The organization gateway (separate repository) must forward these headers upstream for gateway sessions; frame client_metadata passes through unchanged.

Test plan

  • cabal repl agent-core:test:agent-core-test:main --match Agent.Uuid: 5 examples, 0 failures
  • cabal repl agent-openai:test:agent-openai-test:main: 447 examples, 0 failures, 1 pending (pre-existing)
  • agent-core, agent-openai, agent-runtime libraries load in the GHCi multi-repl without new warnings
  • CI

🤖 Generated with Claude Code

Gateway counterpart: digitallyinduced/haskell-agent-gateway#181 forwards these headers to chatgpt.com.

…ntity

The ChatGPT Codex backend attributes usage to turns from the identity
metadata the official client sends, not from individual requests. Our
transport sent none of it, so every tool continuation and retry was an
unlabelled request that relied on undocumented fallback attribution
(compare earendil-works/pi#9488).

Every HTTP request and WebSocket frame now carries the same attribution
set as codex-rs: the originator, session-id, thread-id,
x-client-request-id, and x-codex-window-id headers plus an ASCII-escaped
x-codex-turn-metadata record, mirrored in the request's client_metadata.
The WebSocket handshake carries the static originator.

Agent.OpenAI.TurnState widens the existing per-connection turn state:
alongside the first-write-wins x-codex-turn-state token it now holds the
turn identifier (a UUID version 7 minted on the first request of a turn
and shared by tool continuations, retries, reconnects, HTTP fallback, and
inline compaction), the context-window generation, and a fallback thread
identity for unpersisted sessions. The prompt cache key, which is the
persisted session identifier, is the session and thread identity exactly
as in the official client. Compaction requests are labelled
request_kind "compaction" without leaving the current turn, and a
committed manual or automatic compaction advances the context window for
subsequent requests.

Agent.Uuid provides the RFC 9562 version 7 generator.

Co-authored-by: Haskell Agent (claude-fable-5-1, xhigh) <agent@digitallyinduced.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@mpscholten
mpscholten merged commit c1e4dd4 into master Sep 14, 2026
16 of 18 checks passed
@mpscholten
mpscholten deleted the feature/codex-turn-attribution branch September 14, 2026 18:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant