Skip to content

Codex lineage is read from headers Codex may not send, while the durable thread_id sits unread in the request body #464

Description

@philcunliffe

What

resolveCodexContext (hypaware-core/plugins-workspace/codex/src/exchange-projector.js:678-761) derives a turn's identity and lineage from x-codex-turn-metadata plus a set of bare header names. Read against Codex's own source (protocol.rs, client.rs:929, sturn.rs:342-345):

  1. The authoritative thread_id is in the request BODY, not a header. client_metadata() always puts flat session_id + thread_id in the body. HypAware never reads it. Everything it does read is a header or the metadata blob, both of which are version-dependent.
  2. compatibility_headers emits only four names: x-codex-window-id, x-codex-turn-metadata, x-codex-parent-thread-id, x-openai-subagent. Any other bare name HypAware looks for (readHeader is exact-match) can never match. Worth auditing every readHeader call in this file against that list.
  3. x-codex-turn-metadata is emitted for every ordinary turn (request_kind = Turn), not only Desktop. llp/0083:31 and llp/0141:25-27 state or imply codex-tui never sends it, and PR Codex rollout cwd keys on the thread, not the session container (#459) #462 relied on that premise before its review disproved it. The LLP text was corrected there; the code still reads the blob as if it were a Desktop-only signal.

Why it was not fixed in PR #462

Because reading client_metadata.thread_id and correcting the header names both change recorded rows - conversation_id, parent_thread_id, is_sidechain. That is a data-shape change with backfill and row-identity consequences (see llp/0030, the partition key), not a bug fix, so it belongs to a human. #462 stayed inside the .hypignore cwd defect it was filed for.

Why it matters

Lineage decides is_sidechain and parent_thread_id, and thread identity feeds the cwd resolution that .hypignore is evaluated against (llp/0083, llp/0050). Deriving them from a version-specific surface while a stable one goes unread means the classification silently drifts with the Codex version - the same class of defect as #453 and #459, one layer up.

Suggested shape

Prefer body client_metadata.thread_id/session_id as the authority, keep the headers and metadata blob as fallbacks, and decide explicitly what happens to already-recorded rows (leave them, or backfill). Any change to conversation_id must be checked against row identity: the fallback-hash scope is conversation_id ?? session_id, so moving it silently re-keys history.

Ground-truth gate

Tests must pin: a turn with only body metadata resolves correctly; a turn with only the four real headers resolves correctly; a bare header name Codex never sends resolves to nothing rather than a wrong value; and part_id/message_id are byte-identical to today for existing shapes.

Provenance

Findings (b) and F2 from the review of PR #462 (#462), verified against Codex source. Related: #453, #459, LLP 0030, LLP 0083, LLP 0141.

Metadata

Metadata

Assignees

No one assigned

    Labels

    neutral:fixDelegate this issue to neutral for an autonomous fix attempt (reproduce -> fix -> PR)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions