#!/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
}

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

# ---- MCP servers for every account (lib/mcp_registry.py) -------------------------
# Codex keeps MCP servers in CODEX_HOME (`<dir>/config.toml`), so under the pool there
# is one MCP configuration per account — and a stock `codex mcp add` lands in ONE random
# one. The pool's registry (mcp-servers.json, synced) plus a machine-local overlay
# (mcp-servers.local.json) are reconciled into the picked account right before exec, so
# a session gets the same servers whichever account it drew. Bash only on the hot path:
# python runs when the registry, the overlay or the account's own config changed since
# the last verified reconcile — the stamp in <dir>/.mcp-applied — and never otherwise.
# Fail-open throughout: a missing python, lib or a broken registry costs nothing but
# the servers themselves. Same contract as bin/claude's mcp_ensure.
MCP_PY="$SELF_DIR/../lib/mcp_registry.py"
MCP_PYBIN="${CLAUDE_MULTIACC_PYTHON:-python3}"
mcp_sig() { # $1 acct dir -> "m:z,m:z,m:z" (registry, overlay, account config; - = absent)
  local f out="" s
  for f in "$ACC_ROOT/mcp-servers.json" "$ACC_ROOT/mcp-servers.local.json" "$1/config.toml"; do
    if [ -f "$f" ]; then s="$(file_sig "$f")"; else s="-"; fi
    out="$out${out:+,}$s"
  done
  printf '%s\n' "$out"
}
mcp_ensure() { # $1 acct dir — silent, never changes the exit status
  [ "${CODEX_MULTIACC_MCP:-1}" = "0" ] && return 0
  [ -f "$ACC_ROOT/mcp-servers.json" ] || [ -f "$ACC_ROOT/mcp-servers.local.json" ] || return 0
  [ -f "$MCP_PY" ] || return 0
  local stamp=""
  [ -f "$1/.mcp-applied" ] && IFS= read -r stamp < "$1/.mcp-applied" 2>/dev/null
  [ "${stamp:-}" = "$(mcp_sig "$1")" ] && return 0
  "$MCP_PYBIN" "$MCP_PY" --root "$ACC_ROOT" --provider codex apply --account-dir "$1" \
    --fail-open --quiet >/dev/null 2>&1 </dev/null || true
  return 0
}

# `codex mcp add|remove` writes ONE account's config.toml — the one it ran under — and
# nobody else's. So the shim runs it as a child instead of exec-ing it, then mirrors what
# it wrote into the registry and every other account (`learn` diffs the account against a
# snapshot taken just before). The real CLI's own parser and writer do the work; the shim
# only copies the outcome. Exits with the real binary's status; a mirror failure is one
# hint line. Returns 1 when this run is not such a command.
mcp_mirror_run() { # $1 acct dir, then the user's argv — exits when it handled the run
  local d="$1" snap="" rc
  shift
  [ "${1:-}" = "mcp" ] || return 1
  case "${2:-}" in add|remove) ;; *) return 1 ;; esac
  [ "${CODEX_MULTIACC_MCP:-1}" != "0" ] && [ -f "$MCP_PY" ] || return 1
  mcp_ensure "$d"
  snap="$(mktemp "${TMPDIR:-/tmp}/codex-mcp-snap.XXXXXX" 2>/dev/null)" || snap=""
  if [ -n "$snap" ] && ! "$MCP_PYBIN" "$MCP_PY" --root "$ACC_ROOT" --provider codex snapshot \
       --account-dir "$d" > "$snap" 2>/dev/null </dev/null; then
    rm -f "$snap" 2>/dev/null; snap=""
  fi
  "$REAL" "$@"
  rc=$?
  if [ "$rc" -eq 0 ] && [ -n "$snap" ]; then
    "$MCP_PYBIN" "$MCP_PY" --root "$ACC_ROOT" --provider codex learn --account-dir "$d" \
      --before "$snap" </dev/null \
      || printf 'codex-multiacc: could not mirror the MCP change to the other accounts — run: codex-accounts mcp apply\n' >&2
  elif [ "$rc" -eq 0 ]; then
    printf 'codex-multiacc: could not mirror the MCP change to the other accounts — run: codex-accounts mcp apply\n' >&2
  fi
  [ -n "$snap" ] && rm -f "$snap" 2>/dev/null
  exit "$rc"
}

# A pooled SESSION exports CODEX_HOME=<acct> and CODEX_SHIM_ACTIVE=1, and every child
# process inherits both — so a `codex mcp add` run from INSIDE a session (an agent's
# Bash tool, `npx appinspire-mcp install` run by an agent) arrives at the fast passthrough
# below with the account already chosen, not at the selection path where the mirror
# lives. That is the very write the 2026-09-22 outage was made of, in the place MCP
# servers are most often installed. The account is known, so the same mirror applies:
# exits inside when it handled the run; anything else stays byte-identical passthrough.
mcp_nested_mirror() {
  [ -f "$MANIFEST" ] || return 1
  [ "${CODEX_MULTIACC_DISABLE:-0}" = "1" ] && return 1
  [ "${CODEX_MULTIACC_MCP:-1}" = "0" ] && return 1
  [ -n "${CODEX_HOME:-}" ] || return 1
  local verb="${1:-}" cfg="${CODEX_HOME%/}" d=""
  [ "$verb" = "mcp" ] || return 1
  # A DIRECT child of the pool root, by its given spelling or its physical one (an
  # adopted account is a symlink whose target is the operator's ~/.claude).
  case "$cfg" in
    "$ACC_ROOT"/acct-*) case "${cfg#"$ACC_ROOT"/}" in */*) ;; *) d="$cfg" ;; esac ;;
  esac
  if [ -z "$d" ]; then
    local root_c; root_c="$(canon_path "$ACC_ROOT")"
    case "$(canon_path "$cfg")" in
      "$root_c"/acct-*) case "${cfg##*/}" in acct-*) d="$cfg" ;; esac ;;
    esac
  fi
  [ -n "$d" ] && [ -d "$d" ] || return 1
  mcp_mirror_run "$d" "$@"
}

# 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
  # An auto-resume probe (see "auto-resume" below) asks which account selection would
  # pick; with no selection to run, the answer is none — it must never start a session.
  if [ "${CODEX_MULTIACC_AR_PROBE:-0}" = "1" ]; then printf 'pick= tier=none\n'; exit 3; fi
  mcp_nested_mirror "$@" || true
  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

# 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
}

# ---- one index for the shared session tree -----------------------------------------
# codex keeps a SQLite index of the rollout tree beside it (state_<schema>.sqlite) and
# refuses to start until that index has been BACKFILLED from every rollout under
# $CODEX_HOME/sessions — each file read whole. The installed layout shares one tree
# across every account (<acct>/sessions -> ~/.codex/sessions), so a private index per
# account meant one full scan of the whole tree per account: 10 GB × 12 on a busy Mac,
# minutes each, and a launch that met another process's unfinished scan died after 30 s
# with "timed out waiting for state db backfill … (status: running)" — surfaced as the
# CLI's damaged-database message. A scan cut short (a probe with a timeout, Ctrl-C) also
# left its 15-minute worker lease behind, so the account stayed unstartable long after
# its killer was gone (my-mini 2026-09-09: six fresh accounts, every launch refused).
#
# A shared tree gets ONE shared index: <acct>/state_N.sqlite is a symlink to the same
# file under ~/.codex, exactly like the tree it describes. SQLite resolves that symlink
# before naming its -wal/-shm companions, so every process shares one lock set — the
# multi-process mode a single CODEX_HOME already runs in. A private index that already
# exists and nothing holds open is retired beside the link (never deleted), or promoted
# into the home when the home has none yet; and the index the installed binary will
# create next is linked ahead of time, so a schema bump still costs one backfill, not one
# per account. Structural on purpose: a real (private) session tree keeps a private index.
# lib/common.sh carries the same function for seeding — keep the two in step.
# CORE — byte-identical in lib/common.sh; tests diff them.
state_index_names() { # $1 acct dir, $2 home dir -> the index file names either side holds
  local f name seen=" "
  for f in "$2"/state_[0-9]*.sqlite "$1"/state_[0-9]*.sqlite; do
    [ -e "$f" ] || [ -L "$f" ] || continue          # an unmatched glob is the pattern itself
    name="${f##*/}"
    case "$seen" in *" $name "*) continue ;; esac
    seen="$seen$name "
    printf '%s\n' "$name"
  done
}

installed_state_index_name() { # the index file the installed binary creates; memoized per binary
  local cache="$ACC_ROOT/.state-index" bin="$REAL" real id line name="" nat
  # macOS `stat` reports a SYMLINK's own mtime, and codex is usually installed behind a
  # stable launcher symlink — so the memo has to key on the file the link resolves to.
  real="$(canon_path "$bin")"
  id="$real:$(file_mtime "$real")"
  if [ -f "$cache" ]; then
    IFS= read -r line < "$cache" 2>/dev/null || line=""
    case "$line" in "$id "*) printf '%s\n' "${line#"$id "}"; return 0 ;; esac
  fi
  # The npm launcher is a script; the schema name lives in the native binary vendored
  # beside it. Try the launcher first (a bare binary answers directly), then the vendor.
  for nat in "$bin" "$(dirname "$(canon_path "$bin")")"/../node_modules/@openai/codex-*/vendor/*/bin/codex; do
    [ -f "$nat" ] || continue
    name="$(LC_ALL=C grep -a -o -m1 'state_[0-9][0-9]*\.sqlite' "$nat" 2>/dev/null | head -1)"
    [ -n "$name" ] && break
  done
  case "$name" in *[!A-Za-z0-9_.]*) name="" ;; esac
  { printf '%s %s\n' "$id" "$name" > "$cache.$$" && mv -f "$cache.$$" "$cache"; } 2>/dev/null \
    || rm -f "$cache.$$" 2>/dev/null
  printf '%s\n' "$name"
}

# CORE — byte-identical in lib/common.sh; tests diff them.
shared_index_rejected() { # $1 acct dir, $2 index name, $3 shared target
  # True when codex's own corruption recovery moved THIS account's link out of the way:
  # it renames the database it judged damaged (and its -wal/-shm) into
  # <CODEX_HOME>/db-backups/sqlite-<ts>-<n>/ and rebuilds, and under a link that renames
  # the LINK. A link sitting in there pointing at the shared file is codex's verdict on
  # that file, so this account keeps the index codex rebuilt for it and this name is
  # left alone — handing the link back would hand the damage back.
  local marker
  for marker in "$1"/db-backups/*/"$2"; do
    [ -L "$marker" ] || continue
    [ "$(readlink "$marker" 2>/dev/null)" = "$3" ] && return 0
  done
  return 1
}

# CORE — byte-identical in lib/common.sh; tests diff them.
share_state_index_links() { # $1 acct dir, $2 home dir, $3 extra index name ('' for none)
  local d="$1" home="$2" name names target link retired n
  [ -L "$d" ] && return 0                              # adopted: the dir IS the home
  [ -L "$d/sessions" ] || return 0                     # a private tree keeps its private index
  [ "$(readlink "$d/sessions" 2>/dev/null)" = "$home/sessions" ] || return 0
  [ -d "$home" ] || return 0
  names="$(state_index_names "$d" "$home")"
  case "${3:-}" in ''|*[!A-Za-z0-9_.]*) ;; *) names="$names $3" ;; esac
  for name in $names; do
    target="$home/$name"; link="$d/$name"
    [ -L "$link" ] && continue                         # already shared (or pointed elsewhere on purpose)
    shared_index_rejected "$d" "$name" "$target" && continue
    if [ ! -e "$link" ]; then
      ln -s "$target" "$link" 2>/dev/null || true      # a new account: the whole point
      continue
    fi
    # This account has an index of its own, and MOVING one that a process can still open
    # is unsafe. codex holds the state database through an sqlx pool that opens its
    # connections lazily and BY PATH (max_connections(5), create_if_missing(true) —
    # codex-rs/state/src/sqlite.rs open_read_write_pool), so a rename under a live holder
    # leaves connection 1 on the old inode while every connection the pool opens
    # afterwards follows the new link: one process, two databases. A -wal or a -shm beside
    # the file is that proof. A codex killed mid-write leaves them behind too, and the
    # next clean session on the account removes them, so this heals itself in time.
    { [ -e "$link-wal" ] || [ -e "$link-shm" ]; } && continue
    if [ -e "$target" ]; then
      retired="$link.private"; n=0                     # never overwrite an earlier copy
      while [ -e "$retired" ] && [ "$n" -lt 100 ]; do n=$((n+1)); retired="$link.private.$n"; done
      [ -e "$retired" ] && continue
      mv "$link" "$retired" 2>/dev/null || continue
    else
      # Atomic or nothing: link(2) refuses an existing target, so two shims racing to be
      # the first to promote cannot rename one's fresh symlink onto the file the other
      # just promoted — which is how a self-referential shared index (ELOOP, and every
      # account on the Mac unable to start) could appear. A cross-device link simply
      # fails and this account keeps its own index.
      ln "$link" "$target" 2>/dev/null || continue
      [ -L "$link" ] || rm -f "$link"
    fi
    ln -s "$target" "$link" 2>/dev/null || true
  done
  return 0
}

share_state_index() { # $1 acct dir — at launch: the home's index, plus the one this binary will create
  share_state_index_links "$1" "${HOME:-/nonexistent}/.codex" "$(installed_state_index_name)"
}

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"; }

# ---- auto-resume -------------------------------------------------------------------
# An interactive codex that runs into a usage limit or a dead login does NOT exit: the
# TUI stays open on "You've hit your usage limit ... try again at <date>" and waits for a
# human. Auto-resume moves that session — same session id — onto another pooled account
# without a keystroke, and it does so without touching the exec below: the real codex
# still replaces this shim (same pid, same terminal), while a detached watcher
# (lib/autoresume.py) tails the session's rollout beside it. On a limit/auth verdict the
# watcher asks THIS shim which account a relaunch would get (probe mode), stops codex, and
# types ` CODEX_MULTIACC_AR=<token>:<sid> [CODEX_ACCOUNTS_ROOT=<root>] <this shim's path>`
# into the tmux pane's SHELL — never into the TUI (the pool root only when it is not the
# default one). The relaunch comes back through here: the token names a single-use relaunch
# file carrying the verdict and the argv (`resume <kept opts> <sid> <prompt>`); the old
# account is parked the way the exec retry path below parks one, and ordinary selection
# picks the next account. bin/claude carries the same hooks; docs/AUTORESUME.md is the
# reference. Fail open everywhere: nothing here may block before exec — no network, no
# waiting, no python in the foreground — and every path that cannot finish falls through
# to exactly today's exec. The one exception is a typed relaunch whose token is gone: it
# names the session to resume and exits 2, because its own (empty) argv would start a
# fresh session in the stopped one's place.
AR_DIR="$ACC_ROOT/tmp/autoresume"
AR_DEPTH=0          # how many relaunches this session's chain has already taken
AR_AVOID=""         # acct-NN:until,... — accounts the chain must not land on again
AR_HIST=""          # class:epoch,... — the chain's verdicts, for the watcher's budgets
AR_CHAIN=""         # the chain's id, as the watcher named it
AR_ARGV=()
AR_ORIG_ARGV=()
AR_TIER=""          # which cut produced the pick: eligible | soft | hard (probe answer)
AR_R_SID=""         # the session a relaunch file named, for the dead-token hint
AR_PROBE=0
[ "${CODEX_MULTIACC_AR_PROBE:-0}" = "1" ] && AR_PROBE=1
# Spelled out, never ranges: under a UTF-8 locale bash matches `[a-z]` by collation, so
# `*[!a-z]*` lets "BAD" and "é" through. These validate what the relaunch file hands us.
AR_LOWER=abcdefghijklmnopqrstuvwxyz
AR_ALNUM="ABCDEFGHIJKLMNOPQRSTUVWXYZ${AR_LOWER}0123456789"

# Byte-identical with bin/claude's: a session id is hex and dashes, never an option.
sess_id_ok() { case "$1" in ''|-*|*[!0-9a-fA-F-]*) return 1 ;; *) return 0 ;; esac; }

ar_uuid_ok() { # $1 — a session id a relaunch may name: a bounded, dashed sess_id_ok
  sess_id_ok "$1" || return 1
  [ "${#1}" -le 64 ] || return 1
  case "$1" in *-*) return 0 ;; esac
  return 1
}

ar_word_ok() { # $1 value, $2 min length — [A-Za-z0-9]{min,64}: relaunch tokens, chain ids
  case "$1" in ''|*[!$AR_ALNUM]*) return 1 ;; esac
  [ "${#1}" -ge "$2" ] && [ "${#1}" -le 64 ]
}

ar_avoid_add() { # $1 comma list acct-NN:until -> appended to AR_AVOID, in-force entries only
  # Every entry is re-validated here because the list arrives from a file or the probe's
  # environment, and ar_avoided below does arithmetic on the until half.
  local rest="$1" e id u n=0
  while [ -n "$rest" ] && [ "$n" -lt 64 ]; do
    e="${rest%%,*}"
    case "$rest" in *,*) rest="${rest#*,}" ;; *) rest="" ;; esac
    n=$((n + 1))
    id="${e%%:*}"; u="${e#*:}"
    case "$id" in acct-*[!0-9]*|acct-) continue ;; acct-*) ;; *) continue ;; esac
    num_ok "$u" && [ "$u" -gt "$now" ] || continue
    AR_AVOID="${AR_AVOID:+$AR_AVOID,}$id:$u"
  done
  return 0
}

ar_avoided() { # $1 acct dir — true while the relaunch chain asked to skip this account
  # Fork-free: this runs once per candidate on every invocation, and AR_AVOID is empty
  # for every launch that is not a relaunch or a probe.
  local id="${1##*/}" rest="$AR_AVOID" e u
  while [ -n "$rest" ]; do
    e="${rest%%,*}"
    case "$rest" in *,*) rest="${rest#*,}" ;; *) rest="" ;; esac
    u="${e#*:}"
    [ "${e%%:*}" = "$id" ] && num_ok "$u" && [ "$u" -gt "$now" ] && return 0
  done
  return 1
}

ar_set_hist() { # $1 comma list class:epoch -> AR_HIST: well-formed entries, newest 32
  local rest="$1" e c t n=0 i keep=()
  AR_HIST=""
  while [ -n "$rest" ] && [ "$n" -lt 256 ]; do
    e="${rest%%,*}"
    case "$rest" in *,*) rest="${rest#*,}" ;; *) rest="" ;; esac
    n=$((n + 1))
    c="${e%%:*}"; t="${e#*:}"
    case "$c" in ''|*[!$AR_LOWER]*) continue ;; esac
    [ "${#c}" -le 16 ] && num_ok "$t" || continue
    keep+=("$c:$t")
  done
  n=${#keep[@]}
  i=$((n > 32 ? n - 32 : 0))
  while [ "$i" -lt "$n" ]; do
    AR_HIST="${AR_HIST:+$AR_HIST,}${keep[$i]}"
    i=$((i + 1))
  done
  return 0
}

# The watcher's verdict, written in the exec retry path's own marker vocabulary (its park
# writers at the bottom of this file) and never weakening a park that already reaches
# further. A QUOTA verdict is only ever a
# 10-minute error-cooldown, never a client:7d marker: a weekly park is sticky until its
# reset (codex-accounts keeps it through every telemetry pass), and a watcher that read one
# rollout line must not pin an account out for days — a peer may have redeemed its reset
# in the meantime. The scheduled 5-minute limits pass marks the real window properly.
ar_mark_cooldown() { # $1 acct dir
  local m="$1/.limited" cur="" until=$((now + 600))
  if [ -f "$m" ]; then
    IFS= read -r cur < "$m" 2>/dev/null || cur=""
    num_ok "$cur" || cur=0
    [ "$cur" -ge "$until" ] && return 0
  fi
  {
    echo "$until"
    echo "bucket=error-cooldown percent=? marked_at=$(date -u +%Y-%m-%dT%H:%M:%SZ) reason=error-cooldown"
  } 2>/dev/null > "$1/.limited.$$" \
    && mv -f "$1/.limited.$$" "$m" 2>/dev/null \
    || rm -f "$1/.limited.$$" 2>/dev/null || true
  return 0
}

ar_mark_auth() { # $1 acct dir — the retry path's soft auth park (1h, self-healing)
  local m="$1/.expired" soft=$((now + 3600)) cur
  # A park still in force that is proven (no soft stamp: codex-accounts' verdict) or
  # longer (org-blocked's 6h) says more than one failed session does: keep it.
  if expired_marked "$1"; then
    cur="$(LC_ALL=C sed -n 's/.*soft_until=\([0-9][0-9]*\).*/\1/p' "$m" 2>/dev/null | head -1)"
    num_ok "$cur" || return 0
    [ "$cur" -ge "$soft" ] && return 0
  fi
  {
    echo "$now"
    echo "reason=auth-error soft_until=$soft marked_at=$(date -u +%Y-%m-%dT%H:%M:%SZ) detail=interactive session failed to authenticate"
  } 2>/dev/null > "$1/.expired.$$" \
    && mv -f "$1/.expired.$$" "$m" 2>/dev/null \
    || rm -f "$1/.expired.$$" 2>/dev/null || true
  sel_log "$(basename "$1") parked (auth-error until $soft) — see: codex-accounts expired"
  return 0
}

# A typed relaunch whose token cannot be honoured (missing, expired, malformed). The
# watcher has already stopped the session and the typed line carries no argv of its own,
# so running on would open a FRESH session in its place: say how to get it back, and stop.
ar_token_dead() { # $1 session id (the token's, else the file's) or empty
  local sid="$1"
  ar_uuid_ok "$sid" || sid=""
  sel_log "autoresume expired sid=${sid:--}"
  if [ -n "$sid" ]; then
    printf 'codex-multiacc: auto-resume could not continue automatically (the resume token expired). Resume with: codex resume %s\n' "$sid" >&2
  else
    printf 'codex-multiacc: auto-resume could not continue automatically (the resume token expired).\n' >&2
  fi
  exit 2
}

# Relaunch entry: load the single-use relaunch file a watcher left for this token. Sets
# AR_ARGV (the argv to run) and the AR_* chain state; returns 1 when there is nothing
# usable (AR_R_SID then holds the file's session id, if it named one).
ar_relaunch() { # $1 token
  local tok="$1" base f line k v n=0 a age
  local ver="" prov="" cls="" acct="" sid="" cwd="" depth="" avoid="" hist="" chain=""
  ar_word_ok "$tok" 8 || return 1
  base="$AR_DIR/r-$tok"
  f="$base.relaunch"
  if [ ! -f "$f" ]; then
    rm -f "$base.argv" 2>/dev/null
    return 1
  fi
  while IFS= read -r line || [ -n "$line" ]; do
    n=$((n + 1)); [ "$n" -le 64 ] || break
    k="${line%%=*}"; v="${line#*=}"
    [ "$k" != "$line" ] || continue
    # Known keys only, each validated before anything can act on it.
    case "$k" in
      v) num_ok "$v" && ver="$v" ;;
      provider) prov="$v" ;;
      class) case "$v" in ''|*[!$AR_LOWER]*) ;; *) [ "${#v}" -le 16 ] && cls="$v" ;; esac ;;
      acct) case "$v" in acct-*[!0-9]*|acct-) ;; acct-*) acct="$v" ;; esac ;;
      sid) ar_uuid_ok "$v" && sid="$v" ;;
      cwd) case "$v" in /*) cwd="$v" ;; esac ;;
      depth) num_ok "$v" && depth="$v" ;;
      avoid) avoid="$v" ;;
      hist) hist="$v" ;;
      chain) ar_word_ok "$v" 1 && chain="$v" ;;
    esac
  done < "$f"
  age=$((now - $(file_mtime "$f")))
  if [ "$ver" != "1" ] || [ "$prov" != "codex" ] || [ "$age" -gt 600 ] || [ ! -f "$base.argv" ]; then
    rm -f "$f" "$base.argv" 2>/dev/null
    AR_R_SID="$sid"
    return 1
  fi
  n=0
  while IFS= read -r -d '' a; do
    n=$((n + 1)); [ "$n" -le 256 ] || break
    AR_ARGV+=("$a")
  done < "$base.argv"
  rm -f "$f" "$base.argv" 2>/dev/null
  if [ "${#AR_ARGV[@]}" -eq 0 ]; then
    AR_R_SID="$sid"
    return 1
  fi
  if [ -n "$acct" ] && [ -d "$ACC_ROOT/$acct" ]; then
    case "$cls" in
      quota) ar_mark_cooldown "$ACC_ROOT/$acct" ;;
      auth) ar_mark_auth "$ACC_ROOT/$acct" ;;
    esac
  fi
  AR_DEPTH="${depth:-0}"
  ar_avoid_add "$avoid"
  ar_set_hist "$hist"
  AR_CHAIN="$chain"
  if [ -n "$cwd" ] && [ -d "$cwd" ] && [ "$cwd" != "$PWD" ]; then
    # Everything this shim resolved against the old directory has to survive the cd.
    case "$ACC_ROOT" in /*) ;; *) ACC_ROOT="$PWD/$ACC_ROOT"; MANIFEST="$ACC_ROOT/accounts.json"; AR_DIR="$ACC_ROOT/tmp/autoresume" ;; esac
    case "$REAL" in /*) ;; *) REAL="$PWD/$REAL" ;; esac
    cd "$cwd" 2>/dev/null || true
  fi
  # Field 2 is the word `autoresume`, never an account id: lib/report.py and the
  # *-accounts tools read an acct-NN in field 2 as a pick.
  sel_log "autoresume relaunch chain=${AR_CHAIN:--} from=${acct:--} class=${cls:--} depth=$AR_DEPTH"
  return 0
}

# The argv shapes a relaunch can rebuild as `resume <kept opts> <sid> <prompt>` — anything
# else (exec/e, -p profile, -c overrides, any subcommand, an unknown flag) gets no watcher.
# A positional must contain a space to count as a prompt: a single word may be a
# subcommand.
ar_argv_ok() { # the argv codex is about to get
  local a resume=0 sid=0 prompt=0 want=0
  for a in "$@"; do
    if [ "$want" = 1 ]; then
      case "$a" in ''|-*) return 1 ;; esac
      want=0
      continue
    fi
    case "$a" in
      --dangerously-bypass-approvals-and-sandbox|--yolo) ;;
      -m|--model) want=1 ;;
      --model=?*) ;;
      --last)
        [ "$resume" = 1 ] && [ "$sid" = 0 ] || return 1
        sid=1 ;;
      resume)
        [ "$resume" = 0 ] && [ "$prompt" = 0 ] || return 1
        resume=1 ;;
      -*) return 1 ;;
      *)
        if [ "$resume" = 1 ] && [ "$sid" = 0 ]; then
          ar_uuid_ok "$a" || return 1          # codex reads it as SESSION_ID
          sid=1
        else
          [ "$prompt" = 0 ] || return 1
          case "$a" in *" "*) prompt=1 ;; *) return 1 ;; esac
        fi ;;
    esac
  done
  [ "$want" = 0 ] || return 1
  # A bare `resume` opens the session picker: there is no session to follow yet.
  [ "$resume" = 0 ] || [ "$sid" = 1 ]
}

# The relaunch line is typed into the pane's shell unquoted — this shim's own path, and the
# pool root when it is not the default — so only paths that never need quoting qualify.
ar_path_ok() { # $1 absolute path
  case "$1" in /*) ;; *) return 1 ;; esac
  case "$1" in *[!$AR_ALNUM/._+-]*) return 1 ;; esac
  return 0
}

ar_find_python() { # -> AR_PY, a python3 the watcher can run on. Never RUNS a candidate.
  AR_PY=""
  if [ -n "${CODEX_MULTIACC_PYTHON:-}" ] && [ -f "$CODEX_MULTIACC_PYTHON" ] \
    && [ -x "$CODEX_MULTIACC_PYTHON" ]; then
    AR_PY="$CODEX_MULTIACC_PYTHON"
    return 0
  fi
  AR_PY="$(command -v python3 2>/dev/null)" || AR_PY=""
  [ -n "$AR_PY" ] || return 1
  # macOS ships /usr/bin/python3 as a stub that pops the Command Line Tools installer
  # when nothing backs it — a GUI dialog out of a background watcher. Only use it when
  # the CLT python or the developer dir `xcode-select -p` names is actually present, and
  # find that out with file tests (the xcode_select_link it reads, and Xcode's default
  # location it falls back to) rather than by running anything.
  case "${OSTYPE:-}" in darwin*)
    if [ "$AR_PY" = "/usr/bin/python3" ] \
      && [ ! -x /Library/Developer/CommandLineTools/usr/bin/python3 ] \
      && [ ! -d /var/db/xcode_select_link ] \
      && [ ! -d /Applications/Xcode.app/Contents/Developer ]; then
      AR_PY=""
      return 1
    fi ;;
  esac
  return 0
}

# Gate + spawn, called ONLY from the plain interactive exec at the bottom, immediately
# before it. Returns without doing anything unless every precondition holds, and never
# waits on what it starts: the watcher is detached and the exec happens right after.
ar_spawn_watcher() { # $1 provider, $2 picked acct dir, then the argv codex is about to get
  local prov="$1" pick="$2" lib st root nl='
'
  shift 2
  case "${CODEX_MULTIACC_AUTORESUME:-1}" in 0|false|no|off) return 1 ;; esac
  [ -e "$ACC_ROOT/autoresume.off" ] && return 1
  # A pin is the caller overriding selection. A valid one execs long before this; an
  # invalid one fell through to a random pick, and moving that session is not ours to do.
  [ -z "${CODEX_ACCOUNT:-}" ] || return 1
  if [ "${CODEX_MULTIACC_AR_TEST_TTY:-0}" != "1" ]; then
    [ -t 0 ] && [ -t 1 ] || return 1
  fi
  [ -n "${TMUX:-}" ] && [ -n "${TMUX_PANE:-}" ] || return 1
  ar_argv_ok "$@" || return 1
  ar_find_python || return 1
  lib="$SELF_DIR/../lib/autoresume.py"
  [ -f "$lib" ] || return 1
  # The state file is line-oriented key=value: a value carrying a newline cannot be
  # written faithfully, so such a launch simply goes unsupervised.
  case "$PWD$TMUX$TMUX_PANE$SELF$ACC_ROOT$pick" in *"$nl"*) return 1 ;; esac
  root="$ACC_ROOT"
  case "$root" in /*) ;; *) root="$PWD/$root" ;; esac
  ar_path_ok "$SELF" && ar_path_ok "$root" || return 1
  [ -d "$AR_DIR" ] || mkdir -p "$AR_DIR" 2>/dev/null || return 1
  st="$AR_DIR/$$.state"
  # The ORIGINAL argv (A2), NUL-separated: the watcher rebuilds the relaunch from it.
  if [ "${#AR_ORIG_ARGV[@]}" -gt 0 ]; then
    printf '%s\0' "${AR_ORIG_ARGV[@]}" 2>/dev/null > "$AR_DIR/$$.argv.tmp"
  else
    : 2>/dev/null > "$AR_DIR/$$.argv.tmp"
  fi && mv -f "$AR_DIR/$$.argv.tmp" "$AR_DIR/$$.argv" 2>/dev/null || {
    rm -f "$AR_DIR/$$.argv.tmp" 2>/dev/null
    return 1
  }
  {
    printf 'v=1\nprovider=%s\npid=%s\nppid=%s\n' "$prov" "$$" "$PPID"
    printf 'acct=%s\nacct_dir=%s\ncwd=%s\nlaunched=%s\n' "${pick##*/}" "$pick" "$PWD" "$(date +%s)"
    printf 'tmux=%s\npane=%s\nself=%s\nacc_root=%s\n' "$TMUX" "$TMUX_PANE" "$SELF" "$root"
    printf 'depth=%s\navoid=%s\nhist=%s\nchain=%s\n' "$AR_DEPTH" "$AR_AVOID" "$AR_HIST" "$AR_CHAIN"
  } 2>/dev/null > "$st.tmp" && mv -f "$st.tmp" "$st" 2>/dev/null || {
    rm -f "$st.tmp" "$AR_DIR/$$.argv" 2>/dev/null
    return 1
  }
  # Detached and never waited on: the subshell returns as soon as the watcher is forked,
  # and the watcher ignores the terminal's signals until its own setsid().
  ( trap '' INT QUIT HUP TSTP
    exec "$AR_PY" -I "$lib" watch --state "$st" </dev/null >/dev/null 2>&1 & ) >/dev/null 2>&1
  return 0
}

ar_scrub_env() { # the TUI (and everything it spawns) never sees an auto-resume variable
  local v
  for v in ${!CODEX_MULTIACC_AR@}; do
    case "$v" in CODEX_MULTIACC_AR|CODEX_MULTIACC_AR_*) unset "$v" ;; esac
  done
  return 0
}

# A1 — the relaunch entry, before any selection (the pin included): a token is consumed
# exactly once, and a probe never consumes one. A2 — the argv selection starts from is
# what a later relaunch rebuilds; codex never rewrites "$@" after this point.
# The typed variable is <token>[:<session id>]: the id rides along so a token that cannot
# be honoured still names the session to resume.
if [ -n "${CODEX_MULTIACC_AR+x}" ]; then
  ar_tok="$CODEX_MULTIACC_AR"
  unset CODEX_MULTIACC_AR
  if [ "$AR_PROBE" != "1" ] && [ -n "$ar_tok" ]; then
    ar_sid=""
    case "$ar_tok" in *:*) ar_sid="${ar_tok#*:}"; ar_tok="${ar_tok%%:*}" ;; esac
    ar_relaunch "$ar_tok" || ar_token_dead "${ar_sid:-$AR_R_SID}"
    set -- ${AR_ARGV[@]+"${AR_ARGV[@]}"}
  fi
fi
[ -n "${CODEX_MULTIACC_AR_AVOID:-}" ] && ar_avoid_add "$CODEX_MULTIACC_AR_AVOID"
AR_ORIG_ARGV=("$@")

# 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.
# A probe asks what SELECTION would pick, so it never takes (and never execs) a pin.
if [ -n "${CODEX_ACCOUNT:-}" ] && [ "$AR_PROBE" != "1" ]; then
  d="$ACC_ROOT/$CODEX_ACCOUNT"
  if [ -d "$d" ]; then
    sel_log "$CODEX_ACCOUNT pinned pwd=$PWD"
    share_state_index "$d"
    export CODEX_HOME="$d"
    export CODEX_SHIM_ACTIVE=1
    # A pinned account carries every registered MCP server too, and a pinned
    # `codex mcp add|remove` is mirrored like any other (exits inside).
    mcp_ensure "$d"
    mcp_mirror_run "$d" "$@" || true
    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
  # An auto-resume relaunch (or its probe) skips the account the session just left until
  # the reset it was handed. `eligible` only: the all-limited fallback still sees it.
  ar_avoided "$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
  # A probe has nothing to offer and must never exec (nor log a fallback it never took).
  if [ "$AR_PROBE" = "1" ]; then printf 'pick= tier=none\n'; exit 3; fi
  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
  AR_TIER=eligible
  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
  # A probe only ASKS: the fallback lines below would claim a pick nobody launched.
  if [ "${#soft[@]}" -gt 0 ]; then
    AR_TIER=soft
    PICK_STRICT=1
    pick_best "${soft[@]}"
    PICK_STRICT=0
    [ "$AR_PROBE" = "1" ] \
      || sel_log "all-limited fallback=$(basename "$PICK_DIR") weekly=$(fresh_field "$PICK_DIR" weekly_percent || echo '?')%"
  else
    AR_TIER=hard
    # 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
    [ "$AR_PROBE" = "1" ] \
      || 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 ] && [ "$AR_PROBE" != "1" ]; 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"
# A probe answers here, before anything that would count as a launch: no rotation
# memory, no limits kick, no pick line in selection.log — and never an exec.
if [ "$AR_PROBE" = "1" ]; then
  printf 'pick=%s tier=%s\n' "${pick##*/}" "$AR_TIER"
  exit 0
fi
# 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"
# The picked account carries every registered MCP server before the client starts.
mcp_ensure "$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

# A `codex mcp add|remove` is run as a child under the picked account and mirrored to
# every other one (exits inside); anything else falls through to exec.
if [ "${1:-}" = "mcp" ]; then
  share_state_index "$pick"
  export CODEX_HOME="$pick"
  mcp_mirror_run "$pick" "$@" || true
fi

# 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

share_state_index "$pick"
if [ "$wants_retry" = "0" ]; then
  export CODEX_HOME="$pick"
  # Auto-resume: a gated, detached watcher beside this exec — the exec itself (same pid,
  # same argv, same environment minus the auto-resume variables) is unchanged.
  ar_spawn_watcher codex "$pick" "$@" || true
  ar_scrub_env
  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"
      share_state_index "$cur"
      # 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"
      mcp_ensure "$cur"
      attempt=2
      continue
    fi
  fi
  break
done

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