Task · CXO-0041

Capture the quota snapshot the 429 error event carries, and emit limit_reached

Description

When an account is refused for quota, the archive error event carries a complete account quota snapshot that nothing reads. This is the one moment the reading matters most, and it is also the only reading available for a turn that died before any codex.rate_limits event was emitted.

Measured shape, from the live capture on 2026-09-12:

{“type”:“error”, “error”:{“type”:“usage_limit_reached”,“message”:“…”,“plan_type”:“prolite”, “resets_at”:<unix seconds, number>,“resets_in_seconds”:, “eligible_promo”:null}, “status_code”:429, “headers”:{ 34 string-valued headers }}

The 34 headers are three parallel limit families with identical sub-shapes: X-Codex-{Primary,Secondary}-{Used-Percent,Window-Minutes,Reset-At,Reset-After-Seconds} X-Codex-Primary-Over-Secondary-Limit-Percent X-Codex-Bengalfox-* (same six, plus X-Codex-Bengalfox-Limit-Name) X-Base-Model-Inference-* (same six, plus X-Base-Model-Inference-Limit-Name) X-Codex-Active-Limit, X-Codex-Plan-Type X-Codex-Credits-{Has-Credits,Balance,Unlimited}

Traps measured, not assumed. Every value is a STRING even when it is numeric. Booleans are the Python spellings “True” and “False”, not “true”/“false”. An absent window is the EMPTY STRING, not “0” and not absent: X-Codex-Secondary-Reset-At was “” while X-Codex-Secondary-Reset-After-Seconds was “0” in the same header set, so empty and zero are different observations and the zero is real. The two *-Limit-Name headers carry model-family names and are the only high-information labels in the set. The header block is absent entirely on the status/400 error shape.

Separately, two fields that ARE already reduced reach no sink at all. Turn.RateLimitReached and Turn.RateLimitAllowed are set by applyRateLimits and appear in no instrument and no Loki field, so “were we actually blocked” has no series. The full-corpus scan saw rate_limits.limit_reached=true and rate_limits.allowed=false for the first time in this capture, so the values are live, not theoretical.

Per-model, the same two fields are decoded into rateLimitBlock and then discarded: the additional_rate_limits loop keeps only the windows. The scan also found a second per-model family, additional_rate_limits.gpt-reserve, carrying allowed, limit_reached, primary.{used_percent,window_minutes,reset_after_seconds,reset_at} and an explicit null secondary. reset_at is a window field the existing RateLimitWindow struct does not carry.

Acceptance Criteria

Definition of Done

Implementation Plan

  1. Freeze quota Turn carriers, attributes and metric names.
  2. Reduce the patterned 429 quota snapshot and rate-limit booleans test-first.
  3. Emit the frozen instruments and verify focused, integrated and live evidence.

Final Summary

Implemented quota-at-failure reduction and metrics, strict empty and boolean parsing, per-model limit state preservation, and bounded attributes. Focused tests, dashboard coverage, and the integrated just check gate passed.

View the source file on GitHub