Skip to content

Add opt-in deterministic trace IDs via LANGFUSE_CODEX_TRACE_SEED#24

Merged
jannikmaierhoefer merged 1 commit into
mainfrom
claude/langfuse-deterministic-trace-ids-c3fbf3
Jul 13, 2026
Merged

Add opt-in deterministic trace IDs via LANGFUSE_CODEX_TRACE_SEED#24
jannikmaierhoefer merged 1 commit into
mainfrom
claude/langfuse-deterministic-trace-ids-c3fbf3

Conversation

@jannikmaierhoefer

Copy link
Copy Markdown
Member

What

Adds an opt-in trace_seed config (env: LANGFUSE_CODEX_TRACE_SEED) that makes turn trace IDs deterministic and externally precomputable:

  • Main-thread turn N (1-based, rollout order): createTraceId(${seed}:${N}) = hex(sha256("<seed>:<N>")).slice(0, 32) — the thread ID is deliberately excluded so external systems can compute the ID before the run starts.
  • Subagent-thread rollouts: createTraceId(${seed}:${childThreadId}:${N}), scoped by thread ID to avoid collisions. Subagent detection uses the parent_thread_id / thread_source: "subagent" markers Codex writes into session_meta.
  • Seed unset → behavior is unchanged (auto-generated IDs).

Why

External systems (CI harnesses, benchmark runners, dataset-experiment services) that run codex exec headlessly currently have to poll the Langfuse API to discover the trace a run produced, because they can't know the Codex thread ID up front. With a seed, a harness can precompute the trace ID and e.g. link it to a dataset run item via POST /api/public/dataset-run-items before the run even starts — no polling. The README has a complete shell example.

How

  • config.ts: trace_seed resolved through the existing precedence chain (defaults → ~/.codex/langfuse.json<project>/.codex/langfuse.json → env).
  • trace.ts: the derived ID is pinned on the root turn span via a placeholder remote parentSpanContext — the pattern the Langfuse TS SDK documents for custom trace IDs, using the SDK's own createTraceId helper. Generations, tool spans, and nested subagent observations land in the same trace as before; the sidecar dedup is untouched. Turn numbering runs over the full parsed rollout, so IDs stay stable across hook re-invocations even when earlier turns are skipped by dedup.
  • Fail-open: any derivation error falls back to auto-generated IDs and never blocks the upload (LANGFUSE_CODEX_FAIL_ON_ERROR rethrows for testing).

Tests

  • Seeded main-thread IDs verified for N=1..2 against an independent node:crypto sha256 implementation (not the SDK helper).
  • Subagent rollout gets the thread-scoped seed and doesn't collide with main-thread IDs.
  • Nested subagent turns stay inside the parent's seeded trace.
  • Seed unset → root spans stay parentless with random trace IDs (current behavior).
  • Sidecar dedup keeps working with a seed set, and turn numbering is stable when earlier turns are deduped.
  • New trace-seed-failure.test.ts mocks createTraceId to throw: upload still happens with an auto-generated ID; fail_on_error: true propagates.
  • Additionally verified end-to-end: ran the built dist/index.mjs as the real Stop hook against a local fake Langfuse ingestion server — the uploaded OTLP payload contained exactly the externally precomputed ID for <seed>:1, and the README's shasum one-liner matches createTraceId byte-for-byte.

Notes for reviewers

  • With a seed set, the root span carries a synthetic parent span ID (0123456789abcdef, never a real span). Langfuse still renders the turn as the trace root; this is the documented SDK pattern for custom trace IDs. Without a seed, spans are exported exactly as before.
  • The child-thread test fixture now carries parent_thread_id / thread_source in session_meta, mirroring what Codex actually writes (SessionMeta in codex-rs/protocol).
  • dist/index.mjs is rebuilt and committed per the repo's bundle policy (pnpm run lint passes).

🤖 Generated with Claude Code

When trace_seed is set (config file or LANGFUSE_CODEX_TRACE_SEED), the
N-th main-thread turn's trace id is hex(sha256(`${seed}:${N}`)).slice(0, 32)
via the Langfuse SDK's createTraceId, so external harnesses can precompute
trace ids before a codex exec run. Subagent-thread rollouts are scoped as
`${seed}:${threadId}:${N}` to avoid collisions. Unset seed keeps current
behavior; derivation failures fall back to auto-generated ids.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jannikmaierhoefer jannikmaierhoefer merged commit 030d69d into main Jul 13, 2026
2 checks passed
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