Skip to content

Decouple the Codex captured-env path from the generic exporter - #70

Merged
duncankmckinnon merged 4 commits into
mainfrom
fix/codex-captured-env-gate
Sep 11, 2026
Merged

Decouple the Codex captured-env path from the generic exporter#70
duncankmckinnon merged 4 commits into
mainfrom
fix/codex-captured-env-gate

Conversation

@duncankmckinnon

@duncankmckinnon duncankmckinnon commented Sep 10, 2026

Copy link
Copy Markdown
Owner

Follow-up to #69 (v0.2.1). Two problems with how the codex captured-env fallback landed:

  1. It threaded session_dir_ + a platform == "codex" string check into otel_export._capture_attributes — platform branching in a module whose own docstring says that doesn't belong there.
  2. The attribute-key transform special-cased WB_ (WB_PLANwb.plan, everything else → flat snake_case). Matching was already generic; only the key shape was privileged.

Changes

Generic exporter stays platform-agnostic

  • otel_export.shape_captured_env(raw){attributes, logfire.tags}, with a uniform transform: name.lower().replace("_", "."). WB_PLANwb.plan (unchanged), BUILD_LABELbuild.label. The workbench namespace falls out of the general rule.
  • export_turn / export_spans / export_subagent_turn take an optional keyword captured_env. Omitted → read from os.environ via the configured patterns (claude, cursor, pr_writer: unchanged). Supplied → used verbatim.

Codex owns its own quirk

  • platforms/codex/captured_env.resolve_captured_env(config, session_dir_) — live capture_env first, else the session-start snapshot persisted in meta.extra["captured_env"] (that persist, from Fall back to a persisted env snapshot for Codex turn export #69, is unchanged).
  • codex/hooks.py::notify() and codex/interrupt_marker.py call it and pass captured_env=.

Composes with #71: Config.load() now fills capture_env_patterns from config.yaml, so the codex notify process has patterns even without THIRDEYE_CAPTURE_ENV; this PR covers the case where the values aren't in that process's env.

Tests

  • test_otel_export.py: shape_captured_env uniform transform + tag dedup; export_turn(..., captured_env=...) forwarding and precedence.
  • test_codex_captured_env.py: resolve_captured_env — live wins, meta fallback, both-empty, missing/'malformed meta.
  • test_codex_tracing.py: mocks widened to accept the new kwarg.
  • Full suite: 2728 passed. ruff clean.

🤖 Generated with Claude Code

#69 only consulted the persisted meta snapshot when
config.capture_env_patterns was non-empty. But that value comes from
THIRDEYE_CAPTURE_ENV in os.environ -- the same detached notify
environment that is missing WB_*. When Codex spawns notify with a bare
env (neither var present), the guard was false and the fallback never
ran, so codex turns still exported without wb.* attributes or tags.

Gate on platform == "codex" instead: it is the only platform whose turn
export runs detached from the agent env. Cursor and Claude export from a
real child hook, so their os.environ stays authoritative and a stale
snapshot can never leak in.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
duncankmckinnon and others added 2 commits September 10, 2026 17:07
The earlier approach threaded session_dir_ and a `platform == "codex"`
check into otel_export._capture_attributes -- platform branching in a
module whose docstring says platform branching does not belong there.

Instead:

- otel_export exposes shape_captured_env(raw) -> attributes+tags, and
  export_turn / export_spans / export_subagent_turn take an optional
  keyword `captured_env`. When omitted the raw dict is read from
  os.environ (claude, cursor, pr_writer -- unchanged). When supplied it
  is used verbatim.
- platforms/codex/captured_env.resolve_captured_env() does the Codex-only
  resolution -- live capture, else the session-start snapshot in meta --
  and codex/hooks.py + interrupt_marker.py pass the result in.

Also make the attribute-key transform uniform: every captured name
becomes `name.lower().replace("_", ".")`, so WB_PLAN -> wb.plan and
BUILD_LABEL -> build.label. No pattern is privileged; the workbench
namespace falls out of the general rule rather than a hardcoded prefix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@duncankmckinnon duncankmckinnon changed the title Gate the Codex captured-env fallback on platform, not env Decouple the Codex captured-env path from the generic exporter Sep 11, 2026
@duncankmckinnon
duncankmckinnon merged commit fc63ccd into main Sep 11, 2026
7 checks passed
@duncankmckinnon
duncankmckinnon deleted the fix/codex-captured-env-gate branch September 11, 2026 20:33
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