#!/usr/bin/env bash
# codex-multiacc-shim — PATH-shadows the real `codex` binary; never replaces or edits it.
# Every invocation runs under a randomly picked ChatGPT subscription account with limit
# headroom. Self-contained on purpose: no sourcing, so a broken repo file can never
# break `codex`.
# Selection: CODEX_HOME passthrough > CODEX_ACCOUNT pin > random among the limit-eligible
# accounts that clear the 50-point session gate and then sit in the 30-point weekly
# headroom band > all-limited fallback: the same two cuts over the still-serving limited
# accounts, strict weekly (degraded beats down).
# Accounts whose login is DEAD (a `.expired` marker from a failed refresh/verify/run)
# are never selected — not even as the all-limited fallback — because they fail every
# call outright; `codex-accounts expired` / `relogin` fix them.

set -u

# ${HOME:-} guards: with HOME stripped (env -i, some cron/systemd units) the shim
# must still fail OPEN into plain passthrough, never abort on an unbound variable.
# CODEX_ACCOUNTS_ROOT scopes the pool to one app-robot instance; CODEX_ACCOUNTS_DIR
# is the older spelling and still works. Same precedence as lib/common.sh, so the shim
# and codex-accounts always look at the same pool.
ACC_ROOT="${CODEX_ACCOUNTS_ROOT:-${CODEX_ACCOUNTS_DIR:-${HOME:-/nonexistent}/.codex-accounts}}"
MANIFEST="$ACC_ROOT/accounts.json"

canon_path() {
  local p="$1" t i=0 d b
  case "$p" in /*) ;; *) p="$PWD/$p" ;; esac
  while [ -L "$p" ] && [ "$i" -lt 40 ]; do
    t="$(readlink "$p")" || break
    case "$t" in /*) p="$t" ;; *) p="$(dirname "$p")/$t" ;; esac
    i=$((i+1))
  done
  d="$(cd "$(dirname "$p")" 2>/dev/null && pwd -P)" || { printf '%s\n' "$p"; return 0; }
  b="$(basename "$p")"
  if [ "$d" = "/" ]; then printf '/%s\n' "$b"; else printf '%s/%s\n' "$d" "$b"; fi
}

# 'multiacc-shim' matches this file AND bin/claude — a shim must never exec a shim.
is_shim_file() { head -c 300 "$1" 2>/dev/null | grep -q multiacc-shim; }

SELF="$(canon_path "$0")"
SELF_DIR="$(dirname "$SELF")"

find_real() {
  local cand c d
  local oldifs="$IFS"
  IFS=':'; set -f
  # shellcheck disable=SC2086
  set -- $PATH
  IFS="$oldifs"; set +f
  for d in "$@"; do
    [ -n "$d" ] || continue
    cand="$d/codex"
    [ -f "$cand" ] && [ -x "$cand" ] || continue
    c="$(canon_path "$cand")"
    [ "$c" = "$SELF" ] && continue
    case "$c" in "$ACC_ROOT"/*) continue ;; esac
    is_shim_file "$c" && continue
    printf '%s\n' "$cand"; return 0
  done
  # Fallbacks: resolved dynamically at exec time, so `codex update`/reinstalls keep working.
  for cand in "${HOME:-/nonexistent}/.local/bin/codex" /usr/local/bin/codex /opt/homebrew/bin/codex /usr/bin/codex; do
    [ -f "$cand" ] && [ -x "$cand" ] || continue
    c="$(canon_path "$cand")"
    [ "$c" = "$SELF" ] && continue
    is_shim_file "$c" && continue
    printf '%s\n' "$cand"; return 0
  done
  return 1
}

REAL="$(find_real)" || {
  printf 'codex-multiacc shim: real codex binary not found (PATH or fallback locations)\n' >&2
  exit 127
}

# Fast passthrough: caller pinned a config dir, addon disabled, recursion guard, or
# no account data yet. Byte-identical behavior to stock codex.
if [ -n "${CODEX_HOME:-}" ] \
  || [ "${CODEX_MULTIACC_DISABLE:-0}" = "1" ] || [ -n "${CODEX_SHIM_ACTIVE:-}" ] \
  || [ ! -f "$MANIFEST" ]; then
  exec "$REAL" "$@"
fi

now="$(date +%s)"

# Threshold used by the telemetry backstop below; the manifest is the source of
# truth, but a corrupt/unreadable manifest must never break selection => plain
# sed with a safe default, never a JSON parse.
if [ -z "${CODEX_MULTIACC_THRESHOLD:-}" ]; then
  CODEX_MULTIACC_THRESHOLD="$(sed -n 's/.*"threshold"[^0-9]*\([0-9][0-9]*\).*/\1/p' "$MANIFEST" 2>/dev/null | head -1)"
  CODEX_MULTIACC_THRESHOLD="$CODEX_MULTIACC_THRESHOLD"
fi
# Scraped or handed in by the caller, it has to be a number bash can compare without
# complaining to stderr.
case "$CODEX_MULTIACC_THRESHOLD" in ''|*[!0-9]*|??????*) CODEX_MULTIACC_THRESHOLD=90 ;; esac

if [ "$(uname -s)" = "Darwin" ]; then
  file_mtime() { stat -f %m "$1" 2>/dev/null || echo 0; }
else
  file_mtime() { stat -c %Y "$1" 2>/dev/null || echo 0; }
fi


# A number this shim will do ARITHMETIC on: digits only, and short enough that bash
# cannot go out of range. An over-range value makes `[ x -lt y ]` print
# "integer expression expected" on stderr — which a service-spawned run must never see —
# and makes $((x + 1)) wrap negative. Pool state is a file anyone can corrupt, so every
# scraped number goes through here.
num_ok() { case "$1" in ''|*[!0-9]*) return 1 ;; esac; [ "${#1}" -le 18 ]; }

iso_of_epoch() { # $1 seconds -> UTC ISO8601 ('' when neither date(1) dialect works)
  date -u -r "$1" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d "@$1" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null
}

# Comparable digit string for an ISO timestamp: 2026-08-21T17:07:59.321Z -> 20260821170759.
# Locale-proof (plain integers), and short enough that num_ok always passes. Byte-parallel
# with bin/claude (~637), because client_limit_scan below compares a rollout record's
# timestamp against the .client-limit-cleared watermark exactly the way that shim does.
iso_key() { local t="${1%%.*}"; t="$(printf '%s' "$t" | LC_ALL=C tr -cd '0-9')"; printf '%s\n' "${t}"; }

sel_log() {
  printf '%s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" 2>/dev/null >> "$ACC_ROOT/selection.log" || true
}

marker_active() { # true if $1/.limited is still in force; clears cleanly-expired markers
  # Parity with bin/claude's client_marker_recovered, by construction: this shim has NO
  # telemetry-based clearing path. A marker leaves here only when its OWN reset epoch has
  # passed, so no usage reading — informative or not — can unpark an account.
  # The bucket names this pool actually carries are written by client_limit_scan below
  # from the rollout's own `window_minutes`: `bucket=client:7d` (>= 1440 minutes) and
  # `bucket=client:5h` (under it), plus a bare `bucket=client:primary|secondary` for a
  # report that named no window at all. Those two raw key names are what the scan wrote
  # before 2026-09-04 and they are NOT weekly tokens — live payloads report `primary` as
  # the 10080-minute window — so the weekly guard could never match a codex marker.
  # That guard is the rule 2026-09-04 forced on the claude side: acct-13/acct-14 were
  # served fake-zero telemetry (every bucket percent 0, resets_at null), which read as 0%
  # and deleted their truthful client:seven_day markers, and an interactive session was
  # handed both weekly-exhausted accounts in a row. Codex's one telemetry-driven clear
  # lives in `codex-accounts limits`: it keeps a client:7d marker until its own reset and
  # clears an aged client:5h one on an informative pass (#22, 2026-09-03), stamping
  # `.client-limit-cleared` as it does so this scan cannot re-mark from the same rollout.
  # If a recovery path is ever added HERE, it must apply that identical rule.
  local m="$1/.limited" reset=""
  [ -f "$m" ] || return 1
  IFS= read -r reset < "$m" 2>/dev/null || reset=""
  if ! num_ok "$reset"; then
    # Empty/partial/garbled/absurd marker — e.g. read during a concurrent rewrite.
    # Treat as ACTIVE and never delete: deleting here could destroy a marker
    # another process is mid-write. The next limits refresh rewrites or clears it.
    return 0
  fi
  if [ "$now" -ge "$reset" ]; then
    rm -f "$m" 2>/dev/null
    return 1
  fi
  return 0
}

STALE_AFTER=900

fresh_field() { # fresh_field <acct dir> <json key> -> integer if telemetry fresh, else fail
  local f="$1/limits.json" fetched v
  [ -f "$f" ] || return 1
  fetched="$(sed -n 's/.*"fetched_at"[^0-9]*\([0-9][0-9]*\).*/\1/p' "$f" 2>/dev/null | head -1)"
  num_ok "$fetched" || return 1
  [ $((now - fetched)) -le "$STALE_AFTER" ] || return 1
  v="$(sed -n "s/.*\"$2\"[^0-9]*\([0-9][0-9]*\).*/\1/p" "$f" 2>/dev/null | head -1)"
  num_ok "$v" || return 1
  printf '%s\n' "$v"
}

# RANKING — TWO CUTS, in this order, then chance. The operator's ask (2026-09-03):
# "among accounts where high session limits it must choose randomly from ones where
# highest weekly limits."
#   1. SESSION GATE (SESSION_GATE, default 50): a candidate clears the gate when its ~5h
#      session usage is KNOWN and at or under the gate. When at least one candidate clears
#      it, only those are ranked further — an account whose session bucket is nearly spent
#      is about to be rejected whatever its weekly headroom is. When NOBODY clears it the
#      gate steps aside and every candidate is ranked: the gate compares candidates, it
#      never empties the pool.
#   2. WEEKLY BAND (HEADROOM_BAND, default 30, kept at the operator's request): among the
#      gated candidates the lowest KNOWN weekly usage leads, and every gated candidate
#      within the band of it is a peer. Weekly dominates because a weekly window only
#      refills on its multi-day reset while the ~5h window self-heals (same asymmetry as
#      the claude pool) — which is exactly why session acts as the gate and is NOT a
#      tiebreaker inside the band any more: the operator asked for a random choice among
#      the best-weekly accounts.
# Stale/unreadable telemetry never clears the gate and never enters the band — an unknown
# account can never beat a candidate with truthful usage telemetry — but when NOTHING is
# known the candidates all tie, which keeps an entirely blind pool selectable.
# Band 0 (or the all-limited PICK_STRICT fallback) means exact weekly ties only; gate 100
# turns the gate off.

HEADROOM_BAND="${CODEX_MULTIACC_HEADROOM_BAND:-30}"
case "$HEADROOM_BAND" in ''|*[!0-9]*|??????*) HEADROOM_BAND=30 ;; esac
[ "$HEADROOM_BAND" -gt 100 ] && HEADROOM_BAND=100
SESSION_GATE="${CODEX_MULTIACC_SESSION_GATE:-50}"
case "$SESSION_GATE" in ''|*[!0-9]*|??????*) SESSION_GATE=50 ;; esac
[ "$SESSION_GATE" -gt 100 ] && SESSION_GATE=100
# What the selection log prints for the gate: "off" in random mode, where no gate ran —
# the log must never claim a cut that was not made.
SESSION_GATE_LOG="$SESSION_GATE"
# The codex shim has no DEGRADED ranking mode (the claude shim ranks on still-valid stale
# weekly readings when nothing in its pool is fresh). The constant exists so pick_best
# can stay byte-identical with bin/claude.
SEL_DEGRADED=0

# weekly_percent ONLY. A reading without it is unknown here, exactly as it is unknown to
# pool-selection.v2 (which never sees max_percent). The old max_percent fallback let a
# weekly-less file rank — and, once the session gate existed, CLEAR the gate — on a number
# that may well be the session bucket's own peak (codex review, 2026-09-04).
rank_weekly_of() { # $1 = acct dir -> comparable weekly use, or fail when unknown
  local w
  if w="$(fresh_field "$1" weekly_percent)"; then
    printf '%s\n' "$w"
    return 0
  fi
  return 1
}

rank_session_of() { # $1 = acct dir -> fresh session use, or fail when unknown
  fresh_field "$1" session_percent
}

# The .limited marker's own fields (line 1: reset epoch; line 2: "bucket=…
# percent=… … reason=…"). All tolerant: an unreadable or bare marker answers
# "unknown", never an error — these feed the all-limited fallback only.
limited_reset_of() { # $1 acct dir -> the marker's reset epoch, or 0 (unknown)
  local m="$1/.limited" r=""
  [ -f "$m" ] && { IFS= read -r r < "$m" 2>/dev/null || r=""; }
  if num_ok "$r"; then printf '%s\n' "$r"; else printf '0\n'; fi
}

limited_percent_of() { # $1 acct dir -> the marked window's percent, or -1 (unknown)
  local m="$1/.limited" line=""
  [ -f "$m" ] && line="$(sed -n 2p "$m" 2>/dev/null)"
  case "$line" in
    *percent=*) line="${line#*percent=}"; line="${line%% *}" ;;
    *) line="" ;;
  esac
  if num_ok "$line"; then printf '%s\n' "$line"; else printf '%s\n' "-1"; fi
}

# Rejected RIGHT NOW, not merely near the threshold: the marked window is exhausted
# (100%), or the marker records a real client rejection (a 429 the server sent) or an
# error cooldown. A window at 90-99% still answers requests — the difference the
# all-limited fallback lives on, because "degraded service beats a hard failure" only
# holds for an account that can actually serve. Same rule as the claude shim.
limited_hard_blocked() { # $1 acct dir
  local m="$1/.limited" line="" p
  if [ -f "$m" ]; then
    line="$(sed -n 2p "$m" 2>/dev/null)"
    case "$line" in *reason=client-rate-limit*|*reason=error-cooldown*) return 0 ;; esac
    p="$(limited_percent_of "$1")"
    if [ "$p" -ge 0 ] 2>/dev/null; then
      [ "$p" -ge 100 ]
      return
    fi
    return 1
  fi
  # No marker (the over-threshold backstop put it in valid-but-not-eligible):
  # fresh telemetry's peak decides; stale/unknown reads as still serving.
  p="$(fresh_field "$1" max_percent)" || return 1
  [ "$p" -ge 100 ]
}

# Backstop for a lost/failed marker write: fresh telemetry with ANY bucket at/over the
# threshold excludes the account even if .limited is missing. Stale/unreadable => not
# over (fail open — telemetry must never invent exclusions).
over_threshold() { # $1 = acct dir
  local v
  v="$(fresh_field "$1" max_percent)" || return 1
  [ "$v" -ge "${CODEX_MULTIACC_THRESHOLD:-90}" ]
}

# ---- client-reported rate limits ---------------------------------------------
# Same idea as the claude shim: do not depend on the usage endpoint to notice that an
# account ran dry. The codex CLI writes every `token_count` event into the run's rollout
# with the windows the server just reported:
#   "rate_limits":{"primary":{"used_percent":97.4,"window_minutes":10080,"resets_at":<epoch>},
#                  "secondary":{...}}
# Rollouts live under $CODEX_HOME/sessions/<Y>/<M>/<D>/. When that tree really belongs to
# the account they need no session->account index — but the installed layout SYMLINKS it
# to a shared ~/.codex/sessions so `codex resume` finds every session, and there it proves
# nothing about who spent the quota. sessions_owned() below is what keeps one account's
# spend from marking the whole pool; on the shared layout this scan simply stays off and
# the usage endpoint remains the only limit signal for codex.
# Usage only grows inside a window, so a report from earlier in the same window is still a
# valid lower bound — which is why an in-force `resets_at` is the only freshness test.
# ...and only when that tree is private to the account: a shared one says nothing
# about who spent the quota.
sessions_owned() { # $1 acct dir
  # Structural and deliberately FORK-FREE: this runs for every account on every single
  # invocation, and a pair of canon_path calls here cost more than the whole scan.
  # A session tree is this account's own evidence only when neither the account dir nor
  # its sessions dir is a symlink — which is exactly how the installed layout shares them
  # (lib/common.sh seeds codex accounts with sessions -> ~/.codex/sessions, and an account
  # dir may itself be a symlink to ~/.codex). Anything shared fails OPEN: no ownership,
  # no exclusion, and the usage endpoint stays the only limit signal for that account.
  [ -d "$1/sessions" ] || return 1
  [ -L "$1/sessions" ] && return 1
  [ -L "$1" ] && return 1
  return 0
}

QUOTA_SCAN_BYTES=262144    # rollout tail read per session (the newest report is last)
QUOTA_SCAN_MAX_AGE=21600   # 6h of rollout mtime — older runs are re-reported by newer ones
QUOTA_SCAN_MAX_FILES=3     # hard cap per account: a limit still in force rejects the
                           # newest sessions too, so older ones can only repeat the news

rl_field() { # rl_field <json fragment> <key> -> leading integer of that key's value
  printf '%s' "$1" | LC_ALL=C sed -n "s/.*\"$2\"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p" | head -1
}

# Newest still-in-force over-threshold window this account's own runs recorded.
# Prints "<reset-epoch> <window>"; fails when there is none.
client_limit_scan() { # $1 acct dir
  local day f line frag pct reset best=0 bestwin="" scanned=0 thr="${CODEX_MULTIACC_THRESHOLD:-90}" which
  local memo="$1/.client-scan" last="" ttl mins win ts ck sk cleared
  [ "${CODEX_MULTIACC_CLIENT_LIMITS:-1}" = "0" ] && return 1
  sessions_owned "$1" || return 1
  # A CLEAN result is remembered for a few seconds so a tight loop of `codex exec` runs
  # does not re-read the same rollout tails every time. Only the clean answer is
  # memoized — a spent window becomes a .limited marker, and marker_active short-circuits
  # this scan from then on.
  if [ -f "$memo" ]; then
    IFS= read -r last < "$memo" 2>/dev/null || last=""
    num_ok "$last" || last=0
    # The TTL is caller-supplied, so it goes through num_ok too: `[ x -lt bogus ]` would
    # print "integer expression expected" on the caller's stderr before exec.
    ttl="${CODEX_MULTIACC_CLIENT_SCAN_TTL:-20}"
    num_ok "$ttl" || ttl=20
    [ $((now - last)) -lt "$ttl" ] && return 1
  fi
  # NEWEST FIRST, always. Both day dirs and rollout filenames start with an ISO timestamp,
  # so lexicographic order IS chronological order — walking it forwards would spend the
  # whole file budget on the oldest runs and never reach the one that actually hit the
  # wall (codex-review finding: 13 rollouts, only the newest over threshold). The first
  # file that reports a spent window wins: usage only grows inside a window, so nothing
  # older can be more current.
  #
  # find(1), not a glob, for two reasons a second review pass turned up:
  #   * with -P (the default) find never descends a SYMLINKED component, so a nested
  #     sessions/<year> -> /somewhere/shared cannot smuggle another pool's rollouts in
  #     under an account whose own sessions/ dir is real;
  #   * `tail -n` bounds the list inside the pipe, so a tree with a hundred thousand stale
  #     rollouts never becomes a hundred-thousand-element shell array before the cap.
  local days=() files=() di fx
  while IFS= read -r day; do
    # find(1) output is newline-delimited, so a pool file whose NAME contains a newline
    # arrives as two lines and its tail would resolve relative to $PWD — outside the pool
    # entirely. Every path is therefore re-checked against the tree it must have come from.
    case "$day" in "$1"/sessions/?*) ;; *) continue ;; esac
    days+=("$day")
  done <<EOF
$(find "$1/sessions" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | LC_ALL=C sort | tail -3)
EOF
  di=$(( ${#days[@]} - 1 ))
  while [ "$di" -ge 0 ] && [ "$scanned" -lt "$QUOTA_SCAN_MAX_FILES" ]; do
    day="${days[$di]}"
    di=$((di - 1))
    files=()
    while IFS= read -r f; do
      case "$f" in "$day"/rollout-?*) ;; *) continue ;; esac
      files+=("$f")
    done <<EOF
$(find "$day" -maxdepth 1 -type f -name 'rollout-*.jsonl' 2>/dev/null | LC_ALL=C sort | tail -n "$QUOTA_SCAN_MAX_FILES")
EOF
    fx=$(( ${#files[@]} - 1 ))
    # A file that fails the staleness test still costs a stat, so it spends budget too.
    while [ "$fx" -ge 0 ] && [ "$scanned" -lt "$QUOTA_SCAN_MAX_FILES" ]; do
      f="${files[$fx]}"
      fx=$((fx - 1))
      scanned=$((scanned + 1))
      [ -L "$f" ] && continue
      [ $((now - $(file_mtime "$f"))) -le "$QUOTA_SCAN_MAX_AGE" ] || continue
      line="$(tail -c "$QUOTA_SCAN_BYTES" "$f" 2>/dev/null \
        | LC_ALL=C grep -a '^{".*"rate_limits"' \
        | tail -1)"
      [ -n "$line" ] || continue
      # A clean `codex-accounts limits` pass that DELETED this account's client marker
      # supersedes every report at or before its stamp. Without this watermark the clear
      # achieved nothing: the rollout is still on disk, so the very next launch re-read
      # the same tail and rewrote the same park — a delete/rewrite flap once per pass,
      # which is exactly what codex gaining 5h clearing bought us on 2026-09-04.
      # bin/claude's scan (~800) has consumed the same file, with the same name and the
      # same comparison, since the #22 five-hour clearing landed.
      ts="$(printf '%s' "$line" | LC_ALL=C sed -n \
        's/.*"timestamp"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
      ck="$(iso_key "$ts")"
      cleared=""
      [ -f "$1/.client-limit-cleared" ] \
        && { IFS= read -r cleared < "$1/.client-limit-cleared" 2>/dev/null || cleared=""; }
      if num_ok "$cleared"; then
        sk="$(iso_key "$(iso_of_epoch "$cleared")")"
        if num_ok "$ck" && num_ok "$sk"; then
          [ "$ck" -le "$sk" ] && continue
        elif [ "$(file_mtime "$f")" -le "$cleared" ]; then
          # An undatable report falls back to the rollout's mtime, exactly as the claude
          # scan does: no timestamp, no way to prove it is newer than the clear.
          continue
        fi
      fi
      for which in primary secondary; do
        frag="$(printf '%s' "$line" | LC_ALL=C sed -n "s/.*\"$which\"[[:space:]]*:[[:space:]]*{\([^}]*\)}.*/\1/p")"
        [ -n "$frag" ] || continue
        pct="$(rl_field "$frag" used_percent)"
        reset="$(rl_field "$frag" resets_at)"
        mins="$(rl_field "$frag" window_minutes)"
        num_ok "$pct" || continue
        num_ok "$reset" || continue
        [ "$pct" -ge "$thr" ] || continue
        # The marker's bucket has to name the KIND of window that was spent, because that
        # token is the only thing the weekly guard (codex-accounts weekly_marker) can read.
        # `primary`/`secondary` are just the rollout's key names and map to nothing fixed —
        # live payloads report primary as the 10080-minute window — so a marker named after
        # them was never protected by that guard (2026-09-04). window_minutes rides in the
        # same fragment, so the label is derived HERE, at write time: a day or more is the
        # weekly window (7d), which only refills on its multi-day reset and therefore
        # outlives every later usage payload; anything shorter is the self-healing 5h
        # bucket that #22 (2026-09-03) had to keep clearable. A report carrying no
        # window_minutes keeps the raw key name and so stays clearable too — the same
        # fail-open direction, still bounded by the marker's own reset epoch.
        if num_ok "$mins" && [ "$mins" -ge 1440 ]; then
          win="7d"
        elif num_ok "$mins"; then
          win="5h"
        else
          win="$which"
        fi
        if [ "$reset" -gt "$best" ]; then best="$reset"; bestwin="$win:$pct"; fi
      done
      [ "$best" -gt "$now" ] && break 2
    done
  done
  if [ "$best" -le "$now" ]; then
    printf '%s\n' "$now" 2>/dev/null > "$memo.$$" \
      && mv -f "$memo.$$" "$memo" 2>/dev/null || rm -f "$memo.$$" 2>/dev/null
    return 1
  fi
  printf '%s %s\n' "$best" "$bestwin"
}

mark_client_limit() { # $1 acct dir, $2 reset epoch, $3 window:pct (window: 7d|5h|primary|secondary)
  # $3's window half is the SEMANTIC label client_limit_scan derived from window_minutes,
  # so line 2 reads `bucket=client:7d` / `bucket=client:5h` and the writer's weekly guard
  # matches it. A `client:primary` / `client:secondary` marker still on disk was written
  # before 2026-09-04: it names no window, so it keeps the pre-guard, clearable behavior
  # and simply expires at its own reset epoch. The sel_log line below keeps showing the
  # window:pct detail either way.
  local m="$1/.limited" cur=""
  # Never shorten a marker that already reaches further out, and never rewrite the
  # same one on every invocation.
  if [ -f "$m" ]; then
    IFS= read -r cur < "$m" 2>/dev/null || cur=""
    num_ok "$cur" || cur=0
    [ "$cur" -ge "$2" ] && return 0
  fi
  {
    echo "$2"
    echo "bucket=client:${3%%:*} percent=${3##*:} marked_at=$(date -u +%Y-%m-%dT%H:%M:%SZ) reason=client-rate-limit"
  } 2>/dev/null > "$1/.limited.$$" \
    && mv -f "$1/.limited.$$" "$m" 2>/dev/null \
    || rm -f "$1/.limited.$$" 2>/dev/null || true
  sel_log "$(basename "$1") LIMITED by its own run ($3, resets $2) — client-reported"
  return 0
}

# Auth is a ChatGPT login: auth.json carrying a non-empty access token. An API-key-only
# auth.json is NOT auth here (subscription-only by design), and an empty file is NOT
# auth (an interrupted write must not make a dead account selectable).
has_auth() {
  [ -s "$1/auth.json" ] \
    && LC_ALL=C grep -q '"access_token"[[:space:]]*:[[:space:]]*"[^"]' "$1/auth.json" 2>/dev/null
}

# `.expired` is the persistent "this account needs a re-login" marker: written by
# codex-accounts (refresh grant expired/revoked, failed verify) and by the retry path
# below when a real call fails with an auth error. It SELF-HEALS: any auth.json written
# after the marker (successful re-login, or a refresh by another process) clears it.
# A marker written by the SHIM (a guess from one failed run) also carries
# `soft_until=<epoch>`: once that passes the account returns to the pool by itself, so
# a misread never costs an account permanently. Markers written by codex-accounts —
# a refresh grant that answered invalid_grant, a failed real call — carry no soft_until
# and stay until the account provably works again.
expired_marked() { # $1 = acct dir
  local m="$1/.expired" mt soft reason
  [ -f "$m" ] || return 1
  reason="$(LC_ALL=C sed -n 's/.*reason=\([A-Za-z0-9._-][A-Za-z0-9._-]*\).*/\1/p' "$m" 2>/dev/null | head -1)"
  # CREDENTIAL-scoped parks clear the moment a newer credential lands — that is the
  # evidence they were about. A POLICY park (org-blocked: a workspace admin turned
  # Codex off for the account) is about the account, not the credential: refreshing
  # its token does not re-enable Codex, so only a passing real call or a re-login
  # lifts it.
  if [ "$reason" != "org-blocked" ]; then
    mt="$(file_mtime "$m")"
    if [ -f "$1/auth.json" ] && [ "$(file_mtime "$1/auth.json")" -gt "$mt" ]; then
      rm -f "$m" 2>/dev/null
      return 1
    fi
  fi
  soft="$(LC_ALL=C sed -n 's/.*soft_until=\([0-9][0-9]*\).*/\1/p' "$m" 2>/dev/null | head -1)"
  case "$soft" in
    ''|*[!0-9]*) return 0 ;;                       # no soft stamp => proven dead, keep
    *) [ "$now" -lt "$soft" ] && return 0
       rm -f "$m" 2>/dev/null                      # soft window elapsed: give it another go
       return 1 ;;
  esac
}

# Codex access-token expiry lives inside a JWT (not scrapeable with sed), so unlike
# the claude shim there is no plaintext creds_dead check here: the codex CLI refreshes
# a stale token itself at startup, and a DEAD refresh grant is detected by the limits
# refresher / verify / the retry path below — all of which write `.expired`.
auth_dead() { expired_marked "$1"; }

# Explicit pin wins over everything — markers, and even missing auth: the
# add/login ceremony pins to a dir that has no credentials yet, and the login
# must land exactly there, never in a randomly selected account's dir.
if [ -n "${CODEX_ACCOUNT:-}" ]; then
  d="$ACC_ROOT/$CODEX_ACCOUNT"
  if [ -d "$d" ]; then
    sel_log "$CODEX_ACCOUNT pinned pwd=$PWD"
    export CODEX_HOME="$d"
    export CODEX_SHIM_ACTIVE=1
    exec "$REAL" "$@"
  fi
  sel_log "pin-invalid account=$CODEX_ACCOUNT (no such dir; random fallback)"
fi

valid=()
eligible=()
expired=()
for d in "$ACC_ROOT"/acct-*; do
  [ -d "$d" ] || continue
  has_auth "$d" || continue
  # Expired logins are excluded BEFORE anything else: unlike a limit marker (degraded
  # but working), dead auth guarantees a hard failure, so it can never be the
  # "degraded beats down" fallback either.
  if auth_dead "$d"; then
    expired+=("$d")
    continue
  fi
  valid+=("$d")
  marker_active "$d" && continue
  # The account's own records are consulted BEFORE telemetry: what the server told a real
  # call is first-hand and carries the real reset, while limits.json can be days stale —
  # the usage endpoint rate-limits its own callers. Marking here (rather than lazily, on
  # whichever account happens to be picked) is what makes the marker visible to
  # `codex-accounts status`, to a concurrent run in another terminal, and to sync.
  # The cost is bounded by the scan's own file budget and its clean-result memo.
  if lim="$(client_limit_scan "$d")"; then
    mark_client_limit "$d" "${lim%% *}" "${lim##* }"
    continue
  fi
  over_threshold "$d" && continue
  eligible+=("$d")
done

if [ "${#expired[@]}" -gt 0 ]; then
  ids=""
  for d in "${expired[@]}"; do ids="$ids $(basename "$d")"; done
  sel_log "skipped-expired:$ids (unusable — see: codex-accounts expired)"
  # One actionable line, at most hourly, and only on a terminal — a service-spawned
  # `codex exec` must keep its stderr byte-clean.
  if [ -t 2 ]; then
    n="$ACC_ROOT/.expired-notice"
    last=0
    [ -f "$n" ] && last="$(file_mtime "$n")"
    if [ $((now - last)) -gt 3600 ]; then
      : 2>/dev/null > "$n" || true
      printf 'codex-multiacc: %s account(s) unusable (%s) — see: codex-accounts expired\n' \
        "${#expired[@]}" "${ids# }" >&2
    fi
  fi
fi

# No usable accounts => stock behavior (fail open, never block work), but say WHY when
# the pool is merely un-authenticated: this is the one case the user can actually fix.
if [ "${#valid[@]}" -eq 0 ]; then
  if [ "${#expired[@]}" -gt 0 ]; then
    sel_log "all-expired: falling back to the default login (see: codex-accounts expired)"
    # Terminal only: a service-spawned `codex exec` must keep its stderr byte-clean,
    # and the fallback may well succeed on the machine's own login.
    [ -t 2 ] && printf 'codex-multiacc: no pool account is usable (%s) — see: codex-accounts expired, then: codex-accounts relogin\n' \
      "${ids# }" >&2
  fi
  exec "$REAL" "$@"
fi

# ---- rotation ----------------------------------------------------------------
# Deliberately NOT a full least-recently-used order: just "do not hand back the account
# you were on a moment ago". That is the whole of the bug (quit a session that ran into
# its limit, start another, land straight back on it), and it is the only part that can
# be done without serialising selection. Among equally-ranked candidates the most recent
# pick is dropped and the REST ARE SAMPLED RANDOMLY — so a burst of parallel `codex exec` runs
# still spreads across the pool instead of every one of them computing the same "oldest"
# account and piling onto it.
# The state is one id in one file. A pool root that cannot be written just leaves a stale
# id there, which costs one avoided account and nothing else — it can never starve one.
last_pick_id() { # -> id this pool last handed out, or empty
  local v=""
  [ -f "$ACC_ROOT/.last-pick" ] && { IFS= read -r v < "$ACC_ROOT/.last-pick" 2>/dev/null || v=""; }
  case "$v" in acct-[0-9][0-9]) printf '%s\n' "$v" ;; esac
}

remember_pick() { # $1 acct dir — best effort. stderr is silenced BEFORE the redirect, or
                  # a read-only pool root prints "Permission denied" on every single run.
  local f="$ACC_ROOT/.last-pick" id="${1##*/}"
  printf '%s\n' "$id" 2>/dev/null > "$f.$$" \
    && mv -f "$f.$$" "$f" 2>/dev/null || rm -f "$f.$$" 2>/dev/null
  return 0
}

# Two cuts, then chance: the session gate first, the weekly headroom band second, and a
# random peer with the account just handed out avoided. Sets PICK_DIR / PICK_BAND_COUNT /
# PICK_GATE_COUNT as globals — it must never touch "$@", which holds the user's codex
# arguments.
PICK_DIR=""
PICK_BAND_COUNT=0
PICK_GATE_COUNT=0
PICK_STRICT=0
pick_best() { # args: candidate dirs
  local d w s k sk avoid best="" bestw=101 band ceiling ties=0 i n
  local cand=() weekly=() known=() gated=() pool=()
  avoid="$(last_pick_id)"
  PICK_GATE_COUNT=0
  for d in "$@"; do
    cand+=("$d")
    w="$(rank_weekly_of "$d")" && k=1 || k=0
    s="$(rank_session_of "$d")" && sk=1 || sk=0
    # "Known" takes BOTH readings — pool-selection.v2's quota_known rule. A weekly figure
    # on its own neither ranks nor clears the gate, and a session figure on its own could
    # otherwise be the sole gate-clearer and win the all-gated tie over an account whose
    # truthful weekly reading merely failed the gate. The one exception is a DEGRADED pool
    # (SEL_DEGRADED=1): nothing is fresh anywhere, the gate has necessarily stepped aside,
    # and a still-valid stale weekly reading is the only truth there is. (The writers DO
    # emit one-signal documents — per-signal informative aggregation, 2026-09-04 — and
    # such a document is exactly as unknown here as an empty one; parity with
    # lib/selector_policy.py's quota_known.)
    if [ "$k" = 1 ] && { [ "$sk" = 1 ] || [ "$SEL_DEGRADED" = 1 ]; }; then
      weekly+=("$w"); known+=(1)
    else
      weekly+=(100); known+=(0)
    fi
    if [ "$k" = 1 ] && [ "$sk" = 1 ] && [ "$s" -le "$SESSION_GATE" ]; then
      gated+=(1); PICK_GATE_COUNT=$((PICK_GATE_COUNT + 1))
    else
      gated+=(0)
    fi
  done
  n=${#cand[@]}
  i=0
  while [ "$i" -lt "$n" ]; do
    # Nobody clears the gate => everybody does: the gate compares, it never empties the pool.
    [ "$PICK_GATE_COUNT" -eq 0 ] && gated[$i]=1
    if [ "${gated[$i]}" = 1 ] && [ "${known[$i]}" = 1 ] && [ "${weekly[$i]}" -lt "$bestw" ]; then
      bestw="${weekly[$i]}"
    fi
    i=$((i + 1))
  done
  band="$HEADROOM_BAND"
  [ "$PICK_STRICT" = 1 ] && band=0
  ceiling=$((bestw + band)); [ "$ceiling" -gt 100 ] && ceiling=100
  i=0
  while [ "$i" -lt "$n" ]; do
    if [ "${gated[$i]}" = 1 ]; then
      if [ "$bestw" -gt 100 ]; then
        pool+=("$i")                       # no weekly reading anywhere: all gated tie
      elif [ "${known[$i]}" = 1 ] && [ "${weekly[$i]}" -le "$ceiling" ]; then
        pool+=("$i")
      fi
    fi
    i=$((i + 1))
  done
  PICK_BAND_COUNT=${#pool[@]}
  # Reservoir-sample the peers, skipping the account just handed out.
  for i in "${pool[@]}"; do
    if [ "${cand[$i]##*/}" != "$avoid" ]; then
      ties=$((ties + 1))
      [ $((RANDOM % ties)) -eq 0 ] && best="${cand[$i]}"
    fi
  done
  if [ -z "$best" ]; then
    for i in "${pool[@]}"; do
      ties=$((ties + 1))
      [ $((RANDOM % ties)) -eq 0 ] && best="${cand[$i]}"
    done
  fi
  PICK_DIR="$best"
}

if [ "${#eligible[@]}" -gt 0 ]; then
  if [ "${CODEX_SHIM_SELECT:-headroom}" = "random" ]; then
    PICK_DIR="${eligible[$((RANDOM % ${#eligible[@]}))]}"
    PICK_BAND_COUNT=${#eligible[@]}
    PICK_GATE_COUNT=${#eligible[@]}
    SESSION_GATE_LOG=off
  else
    pick_best "${eligible[@]}"
  fi
else
  # Every account is limit-marked: degraded service beats a hard failure (100% rule) —
  # but not every limited account is equally dead. A window at 90-99% still answers;
  # one at 100% (or a real client 429) rejects every request until its reset. The claude
  # shim learned this on 2026-08-29; the codex shim ranked the whole valid set on weekly
  # headroom until the session gate reached the fallback, where an exhausted account
  # that happened to clear the gate would beat a still-serving one that did not
  # (codex review, 2026-09-04).
  soft=()
  hard=()
  for d in "${valid[@]}"; do
    if limited_hard_blocked "$d"; then hard+=("$d"); else soft+=("$d"); fi
  done
  if [ "${#soft[@]}" -gt 0 ]; then
    PICK_STRICT=1
    pick_best "${soft[@]}"
    PICK_STRICT=0
    sel_log "all-limited fallback=$(basename "$PICK_DIR") weekly=$(fresh_field "$PICK_DIR" weekly_percent || echo '?')%"
  else
    # Every account is exhausted RIGHT NOW: nothing serves, so hand out the one that
    # unblocks first — its rejection window is the shortest.
    PICK_DIR=""
    best_reset=0
    for d in "${hard[@]}"; do
      r="$(limited_reset_of "$d")"
      if [ -z "$PICK_DIR" ]; then
        PICK_DIR="$d"; best_reset="$r"; continue
      fi
      if [ "$r" -gt 0 ] && { [ "$best_reset" -eq 0 ] || [ "$r" -lt "$best_reset" ]; }; then
        PICK_DIR="$d"; best_reset="$r"
      fi
    done
    sel_log "all-limited fallback=$(basename "$PICK_DIR") all-exhausted resets_in=$((best_reset > now ? best_reset - now : 0))s"
    # Every candidate rejects right now, so this pick WILL fail: say so on a terminal
    # instead of letting the operator read the client's bare limit error as a bad
    # choice by the pool. Throttled, and never on a service's stderr.
    if [ -t 2 ]; then
      exn="$ACC_ROOT/.exhausted-notice"
      exlast=0
      [ -f "$exn" ] && exlast="$(file_mtime "$exn")"
      if [ $((now - exlast)) -gt 600 ]; then
        : 2>/dev/null > "$exn" || true
        printf 'codex-multiacc: every usable account is at a limit right now; %s frees up in %ds and was chosen for that.\n' \
          "$(basename "$PICK_DIR")" "$((best_reset > now ? best_reset - now : 0))" >&2
      fi
    fi
  fi
fi
pick="$PICK_DIR"
# Remember the pick so the NEXT run does not hand back the same account. An explicit
# CODEX_ACCOUNT pin deliberately does not: a pin is a caller overriding selection,
# not a turn in the rotation.
remember_pick "$pick"

# Opportunistic limits refresh: non-blocking, throttled, backgrounded. The windows are
# deliberately wide (10m, matching the 5m scheduled pass): the usage endpoint rate-limits
# its OWN callers, and a fleet of machines polling one account too eagerly earns a 429
# with Retry-After 3600 — telemetry then goes stale for an hour at a time, which is
# exactly how every account ends up scoring NEUTRAL.
kick="$ACC_ROOT/.limits-kick"
stale=0
for d in "${valid[@]}"; do
  f="$d/limits.json"
  if [ ! -f "$f" ] || [ $((now - $(file_mtime "$f"))) -gt 600 ]; then stale=1; break; fi
done
if [ "$stale" = 1 ] && [ -x "$SELF_DIR/codex-accounts" ]; then
  last=0
  [ -f "$kick" ] && last="$(file_mtime "$kick")"
  if [ $((now - last)) -gt 600 ]; then
    : 2>/dev/null > "$kick" || true
    ( "$SELF_DIR/codex-accounts" limits --quiet >/dev/null 2>&1 & ) >/dev/null 2>&1
  fi
fi

acct="$(basename "$pick")"
sel_log "$acct weekly=$(fresh_field "$pick" weekly_percent || echo '?')%" \
  "session=$(fresh_field "$pick" session_percent || echo '?')%" \
  "band=${HEADROOM_BAND} band-count=${PICK_BAND_COUNT} session-gate=${SESSION_GATE_LOG} session-ok=${PICK_GATE_COUNT} pwd=$PWD"

export CODEX_SHIM_ACTIVE=1

# Auto-retry applies only to `codex exec` runs with an alternative account available,
# and only when stdin is finite (tty, regular file, or char device like /dev/null).
# A service-spawned pipe that never EOFs must take the plain exec path, or the
# stdin pre-buffering below would hang the call.
wants_retry=0
if [ "${CODEX_SHIM_RETRY:-1}" != "0" ] && [ "${#eligible[@]}" -ge 2 ]; then
  for a in "$@"; do
    case "$a" in exec|e) wants_retry=1; break ;; esac
  done
  if [ "$wants_retry" = "1" ]; then
    # A TTY cannot be buffered or replayed: `codex exec` with no prompt argument reads
    # stdin, and the retry path would hand it /dev/null. Plain exec instead —
    # stdin is inherited untouched. A pipe that never EOFs would hang the pre-buffer,
    # so only finite stdin (regular file, /dev/null-style char device) takes the retry
    # path; everything else execs directly.
    if [ -t 0 ]; then
      wants_retry=0
    elif [ -f /dev/fd/0 ] || [ -c /dev/fd/0 ]; then
      :
    else
      wants_retry=0
    fi
  fi
fi

if [ "$wants_retry" = "0" ]; then
  export CODEX_HOME="$pick"
  exec "$REAL" "$@"
fi

# Retry path: buffer stdio so a retried call never double-emits partial output.
mkdir -p "$ACC_ROOT/tmp" 2>/dev/null || true
tmpd="$(mktemp -d "$ACC_ROOT/tmp/shim.XXXXXX" 2>/dev/null)" || {
  export CODEX_HOME="$pick"
  exec "$REAL" "$@"
}
trap 'rm -rf "$tmpd"' EXIT

# The output buffers must be writable BEFORE the run: if redirection failed at exec
# time (disk full), the real binary would never launch and the shim would exit
# nonzero — a hard failure. Verify now, fall back to plain exec if we cannot.
if ! : > "$tmpd/out" 2>/dev/null || ! : > "$tmpd/err" 2>/dev/null; then
  export CODEX_HOME="$pick"
  exec "$REAL" "$@"
fi

stdin_file=""
if [ ! -t 0 ]; then
  stdin_file="$tmpd/in"
  # If buffering fails midway (disk full) stdin is already partly consumed and cannot
  # be rewound — keep whatever landed rather than silently substituting /dev/null.
  cat > "$stdin_file" 2>/dev/null || [ -s "$stdin_file" ] || stdin_file=""
fi

# ERRPAT decides whether to RETRY at all (deliberately broad). The two PARK patterns
# below decide whether the failed account is also taken out of the pool, and they are
# deliberately NARROW: this grep also sees the model's own answer on stdout (an exec
# run that merely *discusses* a 401 must not cost an account).
#   PARK_AUTH  — the credential is dead: a cooldown would just re-fail, so the account
#                is parked until a re-login / a refresh that works clears it.
#   PARK_ORG   — a workspace admin turned Codex access off for the account; no
#                re-login fixes that.
# Both shim-written parks carry a soft_until stamp, so even a false positive returns to
# the pool on its own — the shim's guess must never outlive the evidence for it.
PARK_AUTH='not (logged|signed) in|authentication (required|failed)|please run `?codex login|run `?codex login`? to|401 unauthorized|token .{0,12}(expired|revoked)|refresh token.{0,20}(expired|invalid|revoked)|invalid_grant|could not refresh'
PARK_ORG='disabled by (your )?(workspace )?admin|admin (has )?disabled|(workspace|organization) has disabled (codex|chatgpt)|codex.{0,20}disabled for (your|this) (workspace|organization)'
LIMITPAT='rate[ _-]?limit|usage limit|limit (reached|exceeded)|too many requests|"?429"?|quota exceeded|hit your usage limit'
ERRPAT="$LIMITPAT|$PARK_AUTH|$PARK_ORG"'|401|403|unauthorized|authentication[_ ]error|invalid[_ ](bearer|token|api key)|token (expired|revoked|invalid)|oauth.*(error|expired|invalid)'
PARK_SOFT_AUTH=3600      # 1h: a mis-parked healthy account is back within the hour
PARK_SOFT_ORG=21600      # 6h: a workspace policy will not change in minutes

attempt=1
cur="$pick"
rc=0
while :; do
  if [ -n "$stdin_file" ]; then exec 3< "$stdin_file"; else exec 3< /dev/null; fi
  CODEX_HOME="$cur" "$REAL" "$@" <&3 > "$tmpd/out" 2> "$tmpd/err"
  rc=$?
  exec 3<&-
  if [ "$rc" -ne 0 ] && [ "$attempt" -eq 1 ] \
    && grep -qiE "$ERRPAT" "$tmpd/out" "$tmpd/err" 2>/dev/null; then
    # Atomic marker writes: a reader must never observe a half-written marker
    # (it would parse as garbage and, before, could be deleted as "expired").
    # A rate limit wins the classification: a limit message that happens to mention an
    # auth word must get the self-expiring cooldown, never a park.
    park_reason=""
    park_soft=0
    if grep -qiE "$LIMITPAT" "$tmpd/out" "$tmpd/err" 2>/dev/null; then
      :
    elif grep -qiE "$PARK_ORG" "$tmpd/out" "$tmpd/err" 2>/dev/null; then
      park_reason="org-blocked"
      park_detail="a workspace admin has disabled Codex access for the account"
      park_soft=$((now + PARK_SOFT_ORG))
    elif grep -qiE "$PARK_AUTH" "$tmpd/out" "$tmpd/err" 2>/dev/null; then
      park_reason="auth-error"
      park_detail="run failed to authenticate"
      park_soft=$((now + PARK_SOFT_AUTH))
    fi
    if [ -n "$park_reason" ]; then
      {
        echo "$now"
        echo "reason=$park_reason soft_until=$park_soft marked_at=$(date -u +%Y-%m-%dT%H:%M:%SZ) detail=$park_detail"
      } 2>/dev/null > "$cur/.expired.$$" \
        && mv -f "$cur/.expired.$$" "$cur/.expired" 2>/dev/null \
        || rm -f "$cur/.expired.$$" 2>/dev/null || true
      sel_log "$(basename "$cur") parked ($park_reason until $park_soft) — see: codex-accounts expired"
    else
      {
        echo $((now + 600))
        echo "bucket=error-cooldown percent=? marked_at=$(date -u +%Y-%m-%dT%H:%M:%SZ) reason=error-cooldown"
      } 2>/dev/null > "$cur/.limited.$$" \
        && mv -f "$cur/.limited.$$" "$cur/.limited" 2>/dev/null \
        || rm -f "$cur/.limited.$$" 2>/dev/null || true
    fi
    next=""
    n="${#eligible[@]}"
    start=$((RANDOM % n))
    i=0
    while [ "$i" -lt "$n" ]; do
      c="${eligible[$(((start + i) % n))]}"
      if [ "$c" != "$cur" ]; then next="$c"; break; fi
      i=$((i+1))
    done
    if [ -n "$next" ]; then
      sel_log "retry from=$(basename "$cur") to=$(basename "$next") rc=$rc"
      cur="$next"
      # The account that actually serves the work is the one the next run should rotate
      # away from — not the one that bounced.
      remember_pick "$cur"
      attempt=2
      continue
    fi
  fi
  break
done

cat "$tmpd/out"
cat "$tmpd/err" >&2
exit "$rc"
