codex-in-claude v0.13.0
A review-honesty and rate-limit-recovery release. codex_review_changes no longer reports an
unreviewed working tree — the all-untracked shape most agent work takes — as a high-confidence
pass, and codex_status reads live rate-limit quota from the codex app-server again after
codex 0.144 moved it off the codex exec stream. Both are breaking on the agent-visible
surface: the result fingerprint moves twice (codex-in-claude/0.1/schema-44 → schema-46) and
RESULT_FORMAT twice (2 → 4), so pre-1.0 this is a minor release and clients that cache by
fingerprint re-fetch the contract.
Changed
-
codex_review_changesno longer reports an unreviewed tree as a high-confidence pass
(#319, breaking). A working tree whose only changes were untracked (new) files — the most
common shape of agent work — used to short-circuit toverdict: "pass",confidence: "high"
with no model call, indistinguishable from a genuinely clean review. Now:- The result carries top-level
review_status(completed|not_run) and acoverageobject
(statuscomplete|partial; pathspec-scopeduntracked_files_detected/included/omitted
counts, null outsideworking_treescope; a closedomission_reasonsset ofuntracked_omitted
/truncated/redacted). Untracked files are inventoried withgit ls-files --others— a
count only, no contents read, so the blind spot is disclosed at zero egress. - A review that never ran the model returns
verdict: "unknown",confidence: "low",
review_status: "not_run"— neverpass. A modelpassoverpartialcoverage (omitted
untracked files, a truncated diff, or a redacted file) is surfaced asunknown/lowwith the
caveat prefixed tosummary; concretefail/concernsfindings are always retained. - A new
untrackedinput (explicit_onlydefault |include|exclude) on
codex_review_changes,codex_review_changes_async, andcodex_dry_run.explicit_only
preserves #74 (only untracked files named inpathsare reviewed);includereviews every
non-ignored untracked file (opt-in egress — it sends their contents);excludeincludes none. codex_dry_runnow reportswould_call_modeland the samecoverageobject, and its
prompt_bytesis0when the previewed call would send nothing — matching the paid path
instead of reporting the size of a prompt never sent (#320).- Git invocations in the diff-gathering path now run with
-c core.fsmonitor=false, so a
working-tree review of an untrusted repo cannot execute a repo-configured fsmonitor program in
the server process. - Hardening (from an implementation review): the untracked inventory is stream-counted in
bounded chunks (an untrusted workspace with arbitrarily many untracked files cannot exhaust
memory); an invaliduntrackedpolicy reaching the core is rejected asinvalid_arguments
rather than silently behaving likeexclude; the coverage counts come from a single
enumeration sodetected == included + omittedcan't be violated under concurrent mutation,
andCoveragenow validates that invariant;review_status/would_call_modelare required
(no unsafe positive default); and the empty-review repair hint is tailored to the active
untrackedpolicy.
Bumps
FINGERPRINT(schema-44→schema-45) andRESULT_FORMAT(2→3); clients that
cache byfingerprintre-fetch the contract, and cross-release job replay of a review result
written by an older version is refused rather than misread. - The result carries top-level
-
codex_delegate_dry_run's worktree preview counts in bounded memory (#323, #326). All three
counts inworktree.plan()— untracked files, tracked files/bytes (git ls-tree -r --long), and
uncommitted tracked files (git diff --numstat) — previously materialized their whole git listing
in memory. The untracked count now delegates to the sharedgitdiff.count_untrackedinventory
(the same NUL-delimited, fsmonitor-hardened enumerationcodex_review_changes/codex_dry_run
use), and the other two stream through a new shared_core/gitproc.run_linesrunner (bounded
per-line reader, concurrent capped stderr drain, process-group kill/reap on timeout or consumer
failure — lifecycle guarantees ported from the diff streamer), so a repo with a pathological
number of tracked, changed, or untracked files is counted without exhausting memory. Reported
counts and failure semantics are unchanged — a git failure surfaces as a structured
worktree_error(or, fornumstat, still degrades to0) instead of a silently-authoritative
0— so nofingerprintchange. (The newline over-count originally filed as #323 did not
reproduce: git C-quotes control characters, newline included, by default, soplan()'s non--z
line-count was already correct.)
Fixed
-
codex_statusreports live rate-limit quota again on codex 0.144+ (#321, breaking).
codex 0.144 removed thetoken_countevent that carried the quota block on thecodex exec
stream, sorate_limithad gone permanentlyunknownwhile the note told you to "run any Codex
call to populate it" — advice that could never work. The data had moved to the app-server
protocol, not disappeared. Now:codex_statusfetches quota live fromcodex app-server(account/rateLimits/read) — a
read-only call with no model-token spend — reusing the hardened one-shot client that backs
codex_transfer. The read is ephemeral: nothing is persisted, socodex_statusstays a
genuinely read-only call and no stale cache can mislead a spend decision.rate_limit.source
isapp_server_live.- Windows are re-slotted by duration, not by the app-server's slot order:
primaryis the
shorter/rolling window (historically 5-hour),secondarythe longer (weekly). The 0.144
app-server reports only the windows that currently bind an account and may place the weekly
window in theprimaryslot with no secondary — so a naive field rename would have kept the
bug. An absent window is no longer treated as "unobserved," so a single healthy window now
correctly reportsavailableinstead of a permanentunknown. - New
rate_limit.statusvalueunavailable(this codex/account exposes no quota data) and
rate_limit.sourcevalueapp_server_live;codex_status's meaning changes from a cached
paid-run snapshot to a live read (breaking under the versioning rules — a closed-schema
output meaning changed, andmeta.rate_limitis nownullon current CLIs). A read that finds
the method missing, the protocol drifted, or a malformed result is surfaced asunavailable
(never as a plausible "no quota") with a note that the plugin may need an update — a loud
signal, not another silentunknown. A committed real-shape fixture plus an integration test
against the live app-server guard against the next such drift. - Untrusted app-server output is hardened:
planTypeis length-bounded and
rateLimitReachedTypeis accepted only from the known enum (an unknown value is dropped, never
trusted as a falseexhausted); a cached reason code degrades tounknownonce every window
has reset; a pathological numeric field (e.g. a 400-digitusedPercent) degrades to absent
instead of raising; two windows are duration-sorted soprimaryis always the shorter horizon;
and the read response is correlated on an unpredictable request id so a prequeued/unsolicited
message can't be trusted as quota. - The dead exec-stream quota parser (
normalize.parse_rate_limit), the per-run capture, and the
snapshot cache (CODEX_IN_CLAUDE_RATE_LIMIT_FILE) are removed.
Bumps
FINGERPRINT(schema-45→schema-46) andRESULT_FORMAT(3→4) for the added
enum values and the changed meaning of therate_limitblock.