#!/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 > most-headroom among
# limit-eligible accounts > least-utilized fallback (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 ]; }

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
  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 score — lower is better (more headroom). Weekly headroom dominates: a weekly
# window only refills on its multi-day reset, while the ~5h window self-heals, so
# session is a mild tiebreaker only (same reset asymmetry as the claude pool).
#   score = weekly%*1000 + session%      weekly,session in [0,100]
# Stale/unreadable telemetry ranks LAST (weekly 100, session 100), never "free".
# Equal worst-case scores keep an entirely unknown pool selectable, but an unknown
# account can never beat a candidate with truthful usage telemetry.
sel_score_of() { # $1 = acct dir
  local w s
  w="$(fresh_field "$1" weekly_percent)" || w="$(fresh_field "$1" max_percent)" || w=100
  s="$(fresh_field "$1" session_percent)" || s=100
  printf '%s\n' $((w * 1000 + s))
}

# 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
  [ "${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
      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)"
        num_ok "$pct" || continue
        num_ok "$reset" || continue
        [ "$pct" -ge "$thr" ] || continue
        if [ "$reset" -gt "$best" ]; then best="$reset"; bestwin="$which:$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
  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
}

# Pick the account with the MOST headroom (lowest ranking score = most weekly headroom,
# session as tiebreaker). Ties break randomly so equally-idle accounts still spread load.
# Sets PICK_DIR/PICK_SCORE as globals — it must never touch "$@", which holds the
# user's codex arguments.
PICK_DIR=""
PICK_SCORE=""
pick_best() { # args: candidate dirs
  local d avoid best="" bestv=1000000 ties=0 i n
  local cand=() score=()
  avoid="$(last_pick_id)"
  for d in "$@"; do
    cand+=("$d")
    score+=("$(sel_score_of "$d")")
  done
  n=${#cand[@]}
  i=0
  while [ "$i" -lt "$n" ]; do
    [ "${score[$i]}" -lt "$bestv" ] && bestv="${score[$i]}"
    i=$((i + 1))
  done
  # Reservoir-sample among the equally-best, skipping the account just handed out.
  i=0
  while [ "$i" -lt "$n" ]; do
    if [ "${score[$i]}" -eq "$bestv" ] && [ "${cand[$i]##*/}" != "$avoid" ]; then
      ties=$((ties + 1))
      [ $((RANDOM % ties)) -eq 0 ] && best="${cand[$i]}"
    fi
    i=$((i + 1))
  done
  if [ -z "$best" ]; then
    # The only account at the best score IS the one just used — degraded rotation beats
    # refusing to pick (and in a two-account pool this is the other half of the
    # alternation).
    i=0
    while [ "$i" -lt "$n" ]; do
      if [ "${score[$i]}" -eq "$bestv" ]; then
        ties=$((ties + 1))
        [ $((RANDOM % ties)) -eq 0 ] && best="${cand[$i]}"
      fi
      i=$((i + 1))
    done
  fi
  PICK_DIR="$best"
  PICK_SCORE="$bestv"
}

if [ "${#eligible[@]}" -gt 0 ]; then
  if [ "${CODEX_SHIM_SELECT:-headroom}" = "random" ]; then
    PICK_DIR="${eligible[$((RANDOM % ${#eligible[@]}))]}"
  else
    pick_best "${eligible[@]}"
  fi
else
  # Every account is limit-marked: degraded service beats a hard failure (100% rule).
  pick_best "${valid[@]}"
  sel_log "all-limited fallback=$(basename "$PICK_DIR") weekly=$(fresh_field "$PICK_DIR" weekly_percent || echo '?')%"
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 '?')% 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"
