#!/usr/bin/env bash
# claude-multiacc-shim — PATH-shadows the real `claude` binary; never replaces or edits it.
# Every invocation runs under a randomly picked subscription account with limit headroom.
# Self-contained on purpose: no sourcing, so a broken repo file can never break `claude`.
# Selection: CLAUDE_CONFIG_DIR/CLAUDE_CODE_OAUTH_TOKEN passthrough > CLAUDE_ACCOUNT pin >
# random among limit-eligible accounts > least-utilized fallback (degraded beats down).
# Accounts whose login is DEAD (expired refresh token, or a `.expired` marker from a
# failed auth) are never selected — not even as the all-limited fallback — because they
# fail every call outright; `claude-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.
# CLAUDE_ACCOUNTS_ROOT scopes the pool to one app-robot instance; CLAUDE_ACCOUNTS_DIR
# is the older spelling and still works. Same precedence as lib/common.sh, so the shim
# and claude-accounts always look at the same pool.
ACC_ROOT="${CLAUDE_ACCOUNTS_ROOT:-${CLAUDE_ACCOUNTS_DIR:-${HOME:-/nonexistent}/.claude-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
}

is_shim_file() { head -c 300 "$1" 2>/dev/null | grep -q claude-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/claude"
    [ -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 `claude update`/reinstalls keep working.
  for cand in "${HOME:-/nonexistent}/.local/bin/claude" /usr/local/bin/claude /opt/homebrew/bin/claude /usr/bin/claude; 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 'claude-multiacc shim: real claude binary not found (PATH or fallback locations)\n' >&2
  exit 127
}

# Fast passthrough: caller pinned a config dir or token, addon disabled, recursion
# guard, or no account data yet. Byte-identical behavior to stock claude.
if [ -n "${CLAUDE_CONFIG_DIR:-}" ] || [ -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ] \
  || [ "${CLAUDE_MULTIACC_DISABLE:-0}" = "1" ] || [ -n "${CLAUDE_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 "${CLAUDE_MULTIACC_THRESHOLD:-}" ]; then
  CLAUDE_MULTIACC_THRESHOLD="$(sed -n 's/.*"threshold"[^0-9]*\([0-9][0-9]*\).*/\1/p' "$MANIFEST" 2>/dev/null | head -1)"
  CLAUDE_MULTIACC_THRESHOLD="$CLAUDE_MULTIACC_THRESHOLD"
fi
# Scraped or handed in by the caller, it has to be a number bash can compare without
# complaining to stderr.
case "$CLAUDE_MULTIACC_THRESHOLD" in ''|*[!0-9]*|??????*) CLAUDE_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
}

# How old telemetry may be and still rank. 900s was below the floor the usage endpoint
# ITSELF enforces: it answers a caller at most about once an hour (429 + Retry-After
# 3600), so a 15-minute window declared the data stale for most of every hour even on a
# perfectly healthy pool — and stale data ranks NEUTRAL, which is the same as not
# ranking at all. One hour matches what the endpoint is willing to give.
STALE_AFTER="${CLAUDE_MULTIACC_STALE_AFTER:-3600}"
case "$STALE_AFTER" in ''|*[!0-9]*|0) STALE_AFTER=3600 ;; esac

# EXCLUSION keeps the old, tight window on purpose. Ranking and the >=90% cutoff are
# not the same kind of judgement: ranking picks between working accounts and an hour-old
# number is plenty, while the cutoff decides that an account is UNUSABLE — and an
# account reading 89% an hour ago may be well past 90% now. Trusting one window for both
# would have quietly extended a stale "89%" into 45 extra minutes of eligibility.
EXCLUDE_STALE_AFTER=900
[ "$EXCLUDE_STALE_AFTER" -gt "$STALE_AFTER" ] && EXCLUDE_STALE_AFTER="$STALE_AFTER"

telem_fetched_at() { # $1 = acct dir -> epoch of the last successful fetch, or fail
  local f="$1/limits.json" fetched
  [ -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
  printf '%s\n' "$fetched"
}

limits_field() { # limits_field <acct dir> <json key> -> integer, or fail
  local v
  v="$(sed -n "s/.*\"$2\"[^0-9]*\([0-9][0-9]*\).*/\1/p" "$1/limits.json" 2>/dev/null | head -1)"
  num_ok "$v" || return 1
  printf '%s\n' "$v"
}

# fresh_field/cutoff_field parse fetched_at inline rather than through
# telem_fetched_at: they run several times per account on EVERY invocation, and the
# difference is a fork apiece. (This file is deliberately fork-frugal — see
# sessions_owned.) telem_fetched_at exists for the once-per-account callers.
within_window() { # $1 = acct dir, $2 = window seconds
  local f="$1/limits.json" fetched
  [ -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 "$2" ]
}

fresh_field() { # fresh_field <acct dir> <json key> -> integer if telemetry fresh, else fail
  within_window "$1" "$STALE_AFTER" || return 1
  limits_field "$1" "$2"
}

# Same, but for the >=90% cutoff, which gets the tighter window (see EXCLUDE_STALE_AFTER).
cutoff_field() { # $1 = acct dir, $2 = json key
  within_window "$1" "$EXCLUDE_STALE_AFTER" || return 1
  limits_field "$1" "$2"
}

# LAST-RESORT ranking input, used only when NOTHING in the pool is fresh (see the blind
# guard below). A weekly bucket only rises until its reset, so until that moment an old
# weekly reading is still a true lower bound on today's usage — strictly more information
# than the neutral 50 that erases every difference between accounts and turns selection
# into a coin flip. Once the reset has passed, the number describes a week that is over
# and is worth exactly nothing, so it is refused.
stale_weekly() { # $1 = acct dir
  local resets
  resets="$(limits_field "$1" weekly_resets_epoch)" || return 1
  [ "$resets" -gt "$now" ] || return 1
  limits_field "$1" weekly_percent
}

# TRUE when this account contributes nothing to ranking: no in-window telemetry at all.
# When every candidate is blind, every score is the same neutral constant, pick_best
# sees one enormous tie, and selection quietly becomes uniform random — the failure
# this whole file exists to prevent.
telem_blind() { # $1 = acct dir
  ! within_window "$1" "$STALE_AFTER"
}

# Human age for the warning line: seconds -> "3h" / "11d". Never fails.
age_human() { # $1 = seconds
  local s="$1"
  if [ "$s" -ge 86400 ]; then printf '%dd\n' $((s / 86400))
  elif [ "$s" -ge 3600 ]; then printf '%dh\n' $((s / 3600))
  else printf '%dm\n' $((s / 60)); fi
}

# RANKING score — lower is better (more headroom). Weekly headroom dominates: a weekly
# bucket only refills on the account's fixed weekly reset (days away), while the 5h
# session bucket self-heals, so session is a mild tiebreaker only. (Anthropic's docs
# confirm this reset asymmetry — an account whose only near-full bucket is the cheap
# session one must NOT rank behind one burning durable weekly headroom.)
#   score = weekly%*1000 + session%      weekly,session in [0,100]
# Stale/unreadable telemetry ranks LAST (weekly 100, session 100), never "free" —
# EXCEPT in a blind pool (SEL_DEGRADED=1), where a still-valid stale weekly reading is
# used instead. Unknown data must never beat a truthful usage reading; when every
# candidate is unknown, the equal worst-case scores still preserve fail-open selection.
SEL_DEGRADED=0
sel_score_of() { # $1 = acct dir
  local w s
  if ! w="$(fresh_field "$1" weekly_percent)" && ! w="$(fresh_field "$1" max_percent)"; then
    if [ "$SEL_DEGRADED" = 1 ]; then w="$(stale_weekly "$1")" || w=100; else w=100; fi
  fi
  s="$(fresh_field "$1" session_percent)" || s=100
  printf '%s\n' $((w * 1000 + s))
}

# Peak of ALL buckets (session included) — the EXCLUSION signal. Stale/unknown => 50.
util_of() {
  local v
  v="$(fresh_field "$1" max_percent)" || v=50
  printf '%s\n' "$v"
}

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

# ---- client-reported rate limits ---------------------------------------------
# The usage API is not the only source of truth, and it is the one that fails exactly
# when it matters: it rate-limits its own callers (429 + Retry-After 3600), so
# limits.json can be hours or days stale at the very moment an account runs dry.
# Claude Code itself records every rejection in the session transcript:
#   {..."error":"rate_limit","apiErrorStatus":429,
#       "quotaLimits":{"status":"rejected","resetsAt":<epoch>,"rateLimitType":"five_hour",...}}
# That record is free, instant, offline, and carries the REAL reset time. Reading it is
# what lets an INTERACTIVE session take its own account out of the pool: the -p retry
# path below never sees a TUI run, so before this, a 5h limit hit in tmux left no trace
# at all and the next `claude` could walk straight back into the same dead account.
#
# Transcripts are NOT account-scoped ($acct/projects is a shared symlink by design —
# lib/common.sh), so the session -> account mapping comes from $acct/sessions/<pid>.json,
# which the client maintains for the lifetime of every run. sess_index_refresh() harvests
# those ids while the runs are alive; sel_capture_session() catches the run THIS
# invocation is about to exec into, so the id outlives the session that recorded the hit.
# ...and that registry has to be private to the account, or it says nothing about who
# ran what: one rejection would then mark the whole pool LIMITED.
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 a shared layout is built
  # (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
}

SESS_INDEX_MAX=12          # session ids remembered per account
QUOTA_SCAN_BYTES=262144    # transcript tail read per session (records land at the end)
QUOTA_SCAN_MAX_AGE=21600   # 6h: a 5h window plus slack. A limit older than that has
                           # either reset, or been re-recorded by a newer session.
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

# Hex and dashes only, and never a LEADING dash: an id like "-e" is inside that class
# and would be handed to grep as an option, which then eats the file operand and blocks
# on the shim's own stdin — a hang before exec, the one failure this file may never have.
# Every grep below also gets `--` so the class is not the only thing standing in the way.
sess_id_ok() { case "$1" in ''|-*|*[!0-9a-fA-F-]*) return 1 ;; *) return 0 ;; esac; }

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.
iso_key() { local t="${1%%.*}"; t="$(printf '%s' "$t" | LC_ALL=C tr -cd '0-9')"; printf '%s\n' "${t}"; }

# Index line: "<session id> <ISO claim>". The claim is the session's OWN start time, and
# an id belongs to exactly ONE account.
# Why both: `claude --continue` resumes the SAME session id under whichever account the
# pool hands out next (the client only mints a new id with --fork-session), and the
# transcript is shared — so without a single owner one rejection would mark every account
# that ever touched that session, and without the claim time the new owner would inherit
# a rejection the PREVIOUS owner earned. Each rule only ever removes attribution: the
# failure mode is a missed limit, never an invented one.
sess_index_add() { # $1 acct dir, $2 session id, $3 start epoch (optional)
  local idx="$1/.sessions-index" tmp oidx claim=""
  sess_id_ok "$2" || return 0
  # `( |$)` also matches a claim-less line from an older build, so such an entry is still
  # deduped and can still be released when another account takes the session over.
  [ -f "$idx" ] && LC_ALL=C grep -qE -- "^$2( |$)" "$idx" 2>/dev/null && return 0
  num_ok "${3:-}" && claim="$(iso_of_epoch "$3")"
  [ -n "$claim" ] || claim="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
  for oidx in "$ACC_ROOT"/acct-*/.sessions-index; do
    [ -f "$oidx" ] || continue
    [ "$oidx" = "$idx" ] && continue
    LC_ALL=C grep -qE -- "^$2( |$)" "$oidx" 2>/dev/null || continue
    # `grep -v` exits 1 when it filters everything out, which is a perfectly good result
    # here — the `true` keeps the emptied index from being thrown away.
    if { LC_ALL=C grep -vE -- "^$2( |$)" "$oidx" 2>/dev/null; true; } 2>/dev/null > "$oidx.$$"; then
      mv -f "$oidx.$$" "$oidx" 2>/dev/null || rm -f "$oidx.$$" 2>/dev/null
    else
      rm -f "$oidx.$$" 2>/dev/null
    fi
  done
  tmp="$idx.$$"
  { [ -f "$idx" ] && cat "$idx" 2>/dev/null; printf '%s %s\n' "$2" "$claim"; } \
    | tail -n "$SESS_INDEX_MAX" 2>/dev/null > "$tmp" \
    && mv -f "$tmp" "$idx" 2>/dev/null || rm -f "$tmp" 2>/dev/null
  return 0
}

sess_index_refresh() { # $1 acct dir — record every run currently live in this account
  local f id started known="" ln
  sessions_owned "$1" || return 0
  # Read the index ONCE with the builtin, so the steady state (every live session already
  # claimed) costs one sed per session and not a grep and a second sed on top.
  if [ -f "$1/.sessions-index" ]; then
    while IFS= read -r ln; do known="$known ${ln%% *}"; done < "$1/.sessions-index"
  fi
  for f in "$1"/sessions/*.json; do
    [ -f "$f" ] || continue
    # A symlinked registry entry would let another pool's session id in under this
    # account's name (second-pass codex-review finding, nested-symlink variant).
    [ -L "$f" ] && continue
    id="$(LC_ALL=C sed -n 's/.*"sessionId"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$f" 2>/dev/null | head -1)"
    sess_id_ok "$id" || continue
    case "$known" in *" $id "*|*" $id") continue ;; esac
    # startedAt is epoch MILLIseconds; using the session's real start (not "now") is what
    # lets a session that has been running since before this shim was installed still be
    # attributed correctly.
    started="$(LC_ALL=C sed -n 's/.*"startedAt"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$f" 2>/dev/null | head -1)"
    num_ok "$started" && started=$((started / 1000)) || started=""
    sess_index_add "$1" "$id" "$started"
    known="$known $id"
  done
  return 0
}

# Two accounts can first-sight the same session id at the same instant and both claim it
# (sess_index_add releases the id from the others, but two concurrent releases can cross,
# and neither run repairs it afterwards). Checked HERE, at the one moment it decides
# something: if any other account holds the id with a claim at least as new as ours, that
# account owns the session now and the rejection is not ours to answer for. An unreadable
# rival claim counts as a conflict too — ambiguity means no attribution.
claim_conflicted() { # $1 acct dir, $2 session id, $3 our ISO claim
  local oidx other ok mk
  mk="$(iso_key "${3:-}")"
  num_ok "$mk" || return 0
  for oidx in "$ACC_ROOT"/acct-*/.sessions-index; do
    [ -f "$oidx" ] || continue
    [ "$oidx" = "$1/.sessions-index" ] && continue
    # EVERY matching line, not just the first: an index can hold the same id twice (an
    # older claim followed by a newer one), and it is the newest rival that decides.
    while IFS= read -r other; do
      [ -n "$other" ] || continue
      case "$other" in *' '*) ok="$(iso_key "${other#* }")" ;; *) return 0 ;; esac
      num_ok "$ok" || return 0
      [ "$ok" -ge "$mk" ] && return 0
    done <<EOF
$(LC_ALL=C grep -E -- "^$2( |$)" "$oidx" 2>/dev/null)
EOF
  done
  return 1
}

# Sets SESS_TRANSCRIPT rather than printing it: this is called once per examined index
# entry, and a command substitution here is a fork per entry per account per run.
SESS_TRANSCRIPT=""
sess_transcript() { # $1 acct dir, $2 session id
  local p
  SESS_TRANSCRIPT=""
  for p in "$1"/projects/*/"$2".jsonl; do
    [ -f "$p" ] && { SESS_TRANSCRIPT="$p"; return 0; }
  done
  return 1
}

# Newest still-in-force rejection this account's own sessions recorded.
# Prints "<reset-epoch> <rateLimitType>"; fails when there is none.
# This runs on EVERY invocation, so it is bounded on purpose: newest session first,
# stop at the first in-force rejection, and never read more than QUOTA_SCAN_MAX_FILES
# transcripts. Missing an older rejection costs nothing — a limit that is still in force
# rejects the very next request too, and that lands in a newer transcript.
client_limit_scan() { # $1 acct dir
  local idx="$1/.sessions-index" memo="$1/.client-scan" id p line r t read_n=0 i last=""
  local ln claim ts ck ak ttl
  local ids=() claims=()
  [ "${CLAUDE_MULTIACC_CLIENT_LIMITS:-1}" = "0" ] && return 1
  [ -f "$idx" ] || return 1
  # A CLEAN result is remembered for a few seconds: a tight loop of `claude -p` runs
  # must not re-read the same transcript tails on every single invocation. Only the
  # clean answer is memoized — a rejection becomes a .limited marker, and marker_active
  # short-circuits this scan entirely from then on. Worst case, a limit hit in the last
  # few seconds is noticed one run late.
  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="${CLAUDE_MULTIACC_CLIENT_SCAN_TTL:-20}"
    num_ok "$ttl" || ttl=20
    [ $((now - last)) -lt "$ttl" ] && return 1
  fi
  while IFS= read -r ln; do
    id="${ln%% *}"
    claim=""
    case "$ln" in *' '*) claim="${ln#* }" ;; esac
    sess_id_ok "$id" && { ids+=("$id"); claims+=("$claim"); }
  done < "$idx"
  # The budget bounds ENTRIES EXAMINED, not just transcripts read: an entry that fails the
  # staleness test still costs a glob and a stat, so a large pool with a full index would
  # otherwise pay for all of them on every single run and never reach a cap at all.
  i=$(( ${#ids[@]} - 1 ))
  while [ "$i" -ge 0 ] && [ "$read_n" -lt "$QUOTA_SCAN_MAX_FILES" ]; do
    id="${ids[$i]}"
    claim="${claims[$i]}"
    i=$((i - 1))
    read_n=$((read_n + 1))
    sess_transcript "$1" "$id" || continue
    p="$SESS_TRANSCRIPT"
    [ $((now - $(file_mtime "$p"))) -le "$QUOTA_SCAN_MAX_AGE" ] || continue
    # One grep, not three: this runs on every invocation, and the two seds below only
    # ever run on a line that already matched. '^{"' drops the partial first line a
    # byte-oriented tail can leave behind.
    line="$(tail -c "$QUOTA_SCAN_BYTES" "$p" 2>/dev/null \
      | LC_ALL=C grep -a '^{".*"error"[[:space:]]*:[[:space:]]*"rate_limit"' \
      | tail -1)"
    [ -n "$line" ] || continue
    case "$line" in *'"status":"rejected"'*|*'"status": "rejected"'*) ;; *) continue ;; esac
    # A rejection recorded BEFORE this account took the session over belongs to whoever
    # was running it then, not to us. Undatable => not attributed (fail open).
    if [ -n "$claim" ]; then
      ts="$(printf '%s' "$line" | LC_ALL=C sed -n 's/.*"timestamp"[[:space:]]*:[[:space:]]*"\([0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[^"]*\)".*/\1/p')"
      [ -n "$ts" ] || continue
      ck="$(iso_key "$ts")"; ak="$(iso_key "$claim")"
      num_ok "$ck" || continue
      num_ok "$ak" || continue
      [ "$ck" -lt "$ak" ] && continue
      claim_conflicted "$1" "$id" "$claim" && continue
    fi
    r="$(printf '%s' "$line" | LC_ALL=C sed -n 's/.*"resetsAt"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p')"
    num_ok "$r" || continue
    [ "$r" -gt "$now" ] || continue
    t="$(printf '%s' "$line" | LC_ALL=C sed -n 's/.*"rateLimitType"[[:space:]]*:[[:space:]]*"\([A-Za-z0-9_.-]*\)".*/\1/p')"
    printf '%s %s\n' "$r" "${t:-unknown}"
    return 0
  done
  printf '%s\n' "$now" 2>/dev/null > "$memo.$$" \
    && mv -f "$memo.$$" "$memo" 2>/dev/null || rm -f "$memo.$$" 2>/dev/null
  return 1
}

# A TUI auth failure cannot use the -p retry path because the client owns the terminal.
# Harvest the same account-owned transcripts used for quota detection so the next launch
# parks a rejected setup-token instead of selecting it again.
client_auth_scan() { # $1 acct dir
  local idx="$1/.sessions-index" ln id claim p line ts ck ak read_n=0 i
  local ids=() claims=()
  [ -f "$idx" ] || return 1
  while IFS= read -r ln; do
    id="${ln%% *}"; claim=""
    case "$ln" in *' '*) claim="${ln#* }" ;; esac
    sess_id_ok "$id" && { ids+=("$id"); claims+=("$claim"); }
  done < "$idx"
  i=$(( ${#ids[@]} - 1 ))
  while [ "$i" -ge 0 ] && [ "$read_n" -lt "$QUOTA_SCAN_MAX_FILES" ]; do
    id="${ids[$i]}"; claim="${claims[$i]}"; i=$((i - 1)); read_n=$((read_n + 1))
    sess_transcript "$1" "$id" || continue
    p="$SESS_TRANSCRIPT"
    [ $((now - $(file_mtime "$p"))) -le "$QUOTA_SCAN_MAX_AGE" ] || continue
    line="$(tail -c "$QUOTA_SCAN_BYTES" "$p" 2>/dev/null \
      | LC_ALL=C grep -a '"error"[[:space:]]*:[[:space:]]*"authentication_failed"' | tail -1)"
    [ -n "$line" ] || continue
    [ -n "$claim" ] || continue
    ts="$(printf '%s' "$line" | LC_ALL=C sed -n 's/.*"timestamp"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
    ck="$(iso_key "$ts")"; ak="$(iso_key "$claim")"
    num_ok "$ck" || continue; num_ok "$ak" || continue
    [ "$ck" -lt "$ak" ] && continue
    claim_conflicted "$1" "$id" "$claim" && continue
    return 0
  done
  return 1
}

mark_client_auth_dead() { # $1 acct dir
  local soft=$((now + 3600)) marked
  marked="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
  { echo "$now"; echo "reason=auth-error soft_until=$soft marked_at=$marked" \
      "detail=client session failed to authenticate"; } \
    2>/dev/null > "$1/.expired.$$" && mv -f "$1/.expired.$$" "$1/.expired" 2>/dev/null \
    || rm -f "$1/.expired.$$" 2>/dev/null || true
  sel_log "$(basename "$1") parked (auth-error until $soft) — client-reported"
}

mark_client_limit() { # $1 acct dir, $2 reset epoch, $3 rate limit type
  local m="$1/.limited" cur=""
  # Never shorten a marker that already reaches further out (a weekly park must
  # survive a 5h report), 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=100 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 session ($3, resets $2) — client-reported"
  return 0
}

# The run this shim is about to BECOME writes $acct/sessions/<pid>.json for its whole
# lifetime, and exec keeps the pid — so $$ is that file's name. A detached poll records
# it (and every other live run of the account) in the index, because by the time the
# user quits a limit-hit session and starts a new one, the client has already deleted
# its registry file and nothing else can name the transcript that holds the evidence.
sel_capture_session() { # $1 acct dir
  local d="$1" pid=$$
  [ "${CLAUDE_MULTIACC_CLIENT_LIMITS:-1}" = "0" ] && return 0
  (
    # Ctrl-C / a closed terminal must not kill the capture: it is bounded (60s) and
    # stops the moment the run it is watching is gone.
    trap '' INT HUP TERM QUIT
    i=0
    while [ "$i" -lt 600 ]; do
      kill -0 "$pid" 2>/dev/null || break
      sess_index_refresh "$d"
      [ -f "$d/sessions/$pid.json" ] && break
      sleep 0.1
      i=$((i + 1))
    done
    sess_index_refresh "$d"
  ) >/dev/null 2>&1 </dev/null &
  return 0
}

# An empty credentials file is NOT auth (an interrupted write must not make a
# dead account selectable and turn a working stock run into an auth failure).
has_auth() { [ -s "$1/.credentials.json" ] || [ -s "$1/server.token" ]; }

cred_num() { # cred_num <file> <json key> -> integer (epoch ms) or empty
  # The leading quote makes "expiresAt" unambiguous: "refreshTokenExpiresAt" cannot match it.
  LC_ALL=C sed -n "s/.*\"$2\"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p" "$1" 2>/dev/null | head -1
}

# TRUE when <dir>/.credentials.json exists but can no longer authenticate: the access
# token is expired AND nothing can renew it (no refresh token, or the refresh token is
# itself expired). Such an account fails EVERY run with "OAuth session expired and
# could not be refreshed", so selecting it is strictly worse than not having it.
# Anything unparseable fails OPEN (treated as alive) — telemetry-free guesswork must
# never take a working account out of the pool.
creds_dead() { # $1 = acct dir
  local f="$1/.credentials.json" exp rexp
  [ -s "$f" ] || return 1
  exp="$(cred_num "$f" expiresAt)"
  case "$exp" in ''|*[!0-9]*) exp=0 ;; esac
  [ $((exp / 1000)) -gt "$now" ] && return 1        # access token still valid
  # Access token expired: only a live refresh token can save it.
  grep -q '"refreshToken"[[:space:]]*:[[:space:]]*"[^"]' "$f" 2>/dev/null || return 0
  rexp="$(cred_num "$f" refreshTokenExpiresAt)"
  case "$rexp" in ''|*[!0-9]*) return 1 ;; esac     # unknown expiry => fail open
  [ $((rexp / 1000)) -le "$now" ]
}

# `.expired` is the persistent "this account needs a re-login" marker: written by
# claude-accounts (refresh grant expired/revoked) and by the retry path below when a
# real call fails with an auth error. It SELF-HEALS: any credential written after the
# marker (successful re-login, or a refresh by another process) clears it, and a
# successful `claude-accounts limits` fetch removes it outright.
# 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 claude-accounts —
# a refresh grant that answered "revoked", a real verify call — carry no soft_until and
# stay until the account provably works again.
expired_marked() { # $1 = acct dir
  local m="$1/.expired" mt f 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) is about the account, not the
  # credential: refreshing its token does not re-enable Claude Code for it. Letting a
  # credential rewrite clear it silently returned blocked accounts to the pool every
  # few hours (the limits refresher rewrites credentials), so runs kept failing with
  # "Your organization has disabled Claude subscription access".
  if [ "$reason" != "org-blocked" ]; then
    mt="$(file_mtime "$m")"
    for f in "$1/.credentials.json" "$1/server.token"; do
      if [ -f "$f" ] && [ "$(file_mtime "$f")" -gt "$mt" ]; then
        rm -f "$m" 2>/dev/null
        return 1
      fi
    done
  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
}

# Auth that cannot work right now. A non-empty server.token is an independent
# credential (no expiry we can read), so it keeps an account alive even when the
# OAuth credential beside it is dead — the token is exported instead.
auth_dead() { # $1 = acct dir
  expired_marked "$1" && return 0
  [ -s "$1/server.token" ] && return 1
  creds_dead "$1"
}

acct_token() { # $1 = acct dir; prints token if the dir must authenticate by token
  # Token-auth dirs (no local creds), and dirs whose OAuth credential is dead but which
  # still carry a portable setup-token — the token is the only thing that can work there.
  if [ -s "$1/server.token" ] && { [ ! -f "$1/.credentials.json" ] || creds_dead "$1"; }; then
    tr -d '[:space:]' < "$1/server.token"
  fi
}

# 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 "${CLAUDE_ACCOUNT:-}" ]; then
  d="$ACC_ROOT/$CLAUDE_ACCOUNT"
  if [ -d "$d" ]; then
    sel_log "$CLAUDE_ACCOUNT pinned pwd=$PWD"
    export CLAUDE_CONFIG_DIR="$d"
    export CLAUDE_SHIM_ACTIVE=1
    # Same rule as every other path (acct_token): a DEAD credential beside a portable
    # token must not shadow the token. Testing only for the credential's absence made a
    # pinned account with a stale login fail outright ("OAuth session expired") while
    # the very same account worked unpinned.
    tok="$(acct_token "$d")"
    [ -n "$tok" ] && export CLAUDE_CODE_OAUTH_TOKEN="$tok"
    sel_capture_session "$d"
    exec "$REAL" "$@"
  fi
  sel_log "pin-invalid account=$CLAUDE_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 "OAuth session expired" failure, so it
  # can never be the "degraded beats down" fallback either.
  if auth_dead "$d"; then
    expired+=("$d")
    continue
  fi
  valid+=("$d")
  sess_index_refresh "$d"
  if client_auth_scan "$d"; then
    mark_client_auth_dead "$d"
    valid=("${valid[@]:0:${#valid[@]}-1}")
    expired+=("$d")
    continue
  fi
  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
  # `claude-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: claude-accounts expired)"
  # One actionable line, at most hourly, and only on a terminal — a service-spawned
  # `claude -p` 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 'claude-multiacc: %s account(s) unusable (%s) — see: claude-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: claude-accounts expired)"
    # Terminal only: a service-spawned `claude -p` must keep its stderr byte-clean, and
    # the fallback may well succeed on the machine's own login. The reason is always in
    # selection.log, and `claude-accounts expired` spells it out.
    [ -t 2 ] && printf 'claude-multiacc: no pool account is usable (%s) — see: claude-accounts expired, then: claude-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 `claude -p` 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 claude 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"
}

# Telemetry going stale is not a per-run detail, it is a pool-wide outage: with no
# in-window data ANYWHERE every account scores the identical NEUTRAL value, the tie
# spans the whole pool, and "pick the account with the most headroom" silently becomes
# "pick any account at all". That is how a fresh session lands on the one account
# already at 80% of its weekly limit while `claude-accounts status` still shows a
# reassuring 2% from eleven days ago. It cost eleven days of blind picks once.
# Two answers, and the order matters: rank on whatever old readings are still true
# BEFORE picking, and say out loud which of the two happened.
blind=1           # 1 = no candidate has in-window telemetry
blind_age=0       # newest stale reading among the candidates; 0 = never fetched at all
degraded=0        # 1 = blind, but every candidate had a stale reading still worth using
assess_telemetry() { # args: the dirs actually being chosen between
  local d f n=0 stale_ok=0
  blind=1; blind_age=0; degraded=0
  for d in "$@"; do
    n=$((n + 1))
    if ! telem_blind "$d"; then blind=0; return 0; fi
    f="$(telem_fetched_at "$d" || echo 0)"
    # The NEWEST stale reading is the honest age of the outage; an account that was
    # never fetched at all must not make the pool look older than it is.
    [ "$f" -gt 0 ] && { [ "$blind_age" -eq 0 ] || [ $((now - f)) -lt "$blind_age" ]; } \
      && blind_age=$((now - f))
    stale_weekly "$d" >/dev/null && stale_ok=$((stale_ok + 1))
  done
  # All or nothing. A candidate whose reading has no horizon — a limits.json written
  # before this field existed, or one whose week has already turned — scores neutral
  # 50, and 50 would beat a NEIGHBOUR's true-but-worse 70. Mixing the two makes the
  # degraded ranking actively wrong, so it is only used when every candidate can be
  # compared on the same footing.
  [ "$n" -gt 0 ] && [ "$stale_ok" -eq "$n" ] && degraded=1
  return 0
}

if [ "${#eligible[@]}" -gt 0 ]; then
  # Blindness is judged over the accounts actually being chosen between, not over every
  # valid one: a FRESH account sitting behind a .limited marker is not a candidate, and
  # letting it clear the flag would leave the real candidates ranking neutral.
  assess_telemetry "${eligible[@]}"
  [ "$degraded" = 1 ] && SEL_DEGRADED=1
  if [ "${CLAUDE_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).
  assess_telemetry "${valid[@]}"
  [ "$degraded" = 1 ] && SEL_DEGRADED=1
  pick_best "${valid[@]}"
  # Report the number this fallback ACTUALLY ranked on. Asking fresh_field here printed
  # `weekly=?%` even when the pick was made on a perfectly good stale reading, so anyone
  # reading only this event concluded the choice had no usage input at all.
  if [ "$degraded" = 1 ]; then
    sel_log "all-limited fallback=$(basename "$PICK_DIR") weekly=$(stale_weekly "$PICK_DIR" || echo '?')% ranking=DEGRADED"
  elif [ "$blind" = 1 ]; then
    sel_log "all-limited fallback=$(basename "$PICK_DIR") weekly=?% ranking=BLIND"
  else
    sel_log "all-limited fallback=$(basename "$PICK_DIR") weekly=$(fresh_field "$PICK_DIR" weekly_percent || echo '?')%"
  fi
fi
pick="$PICK_DIR"
# Remember the pick so the NEXT run does not hand back the same account. An explicit
# CLAUDE_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/claude-accounts" ]; then
  last=0
  [ -f "$kick" ] && last="$(file_mtime "$kick")"
  if [ $((now - last)) -gt 600 ]; then
    : 2>/dev/null > "$kick" || true
    ( "$SELF_DIR/claude-accounts" limits --quiet >/dev/null 2>&1 & ) >/dev/null 2>&1
  fi
fi

acct="$(basename "$pick")"
if [ "$blind" = 1 ]; then
  # Two genuinely different states, and an operator debugging this needs to know which:
  # DEGRADED still ranks, on old readings that remain true; BLIND cannot rank at all and
  # is a coin flip. Calling both of them "random" would send someone hunting the wrong bug.
  if [ "$degraded" = 1 ]; then
    sel_log "$acct weekly=$(stale_weekly "$pick" || echo '?')% session=?% ranking=DEGRADED telemetry-age=${blind_age}s pwd=$PWD"
  else
    sel_log "$acct weekly=?% session=?% ranking=BLIND telemetry-age=${blind_age}s pwd=$PWD"
  fi
  # Terminal only, at most hourly — a service-spawned `claude -p` must keep its stderr
  # byte-clean, and this is advice, never a failure.
  if [ -t 2 ]; then
    n="$ACC_ROOT/.stale-notice"
    last=0
    [ -f "$n" ] && last="$(file_mtime "$n")"
    if [ $((now - last)) -gt 3600 ]; then
      : 2>/dev/null > "$n" || true
      if [ "$degraded" = 1 ]; then
        printf 'claude-multiacc: usage telemetry is %s old — ranking on the last readings that are still valid, not on current usage. Fix: claude-accounts limits --force, then claude-accounts status\n' \
          "$(age_human "$blind_age")" >&2
      elif [ "$blind_age" -gt 0 ]; then
        printf 'claude-multiacc: usage telemetry is %s old for EVERY account and too old to mean anything — selection is running blind (random, not by headroom). Fix: claude-accounts limits --force, then claude-accounts status\n' \
          "$(age_human "$blind_age")" >&2
      else
        printf 'claude-multiacc: no usage telemetry for ANY account — selection is running blind (random, not by headroom). Fix: claude-accounts limits --force, then claude-accounts status\n' >&2
      fi
    fi
  fi
else
  sel_log "$acct weekly=$(fresh_field "$pick" weekly_percent || echo '?')% session=$(fresh_field "$pick" session_percent || echo '?')% pwd=$PWD"
fi

export CLAUDE_SHIM_ACTIVE=1

# Auto-retry applies only to -p/--print 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 [ "${CLAUDE_SHIM_RETRY:-1}" != "0" ] && [ "${#eligible[@]}" -ge 2 ]; then
  for a in "$@"; do
    case "$a" in -p|--print) wants_retry=1; break ;; esac
  done
  if [ "$wants_retry" = "1" ]; then
    # A TTY cannot be buffered or replayed: `claude -p` with no prompt argument reads
    # the terminal, 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 CLAUDE_CONFIG_DIR="$pick"
  tok="$(acct_token "$pick")"
  [ -n "$tok" ] && export CLAUDE_CODE_OAUTH_TOKEN="$tok"
  sel_capture_session "$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 CLAUDE_CONFIG_DIR="$pick"
  tok="$(acct_token "$pick")"
  [ -n "$tok" ] && export CLAUDE_CODE_OAUTH_TOKEN="$tok"
  sel_capture_session "$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 CLAUDE_CONFIG_DIR="$pick"
  tok="$(acct_token "$pick")"
  [ -n "$tok" ] && export CLAUDE_CODE_OAUTH_TOKEN="$tok"
  sel_capture_session "$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, unchanged behavior).
# 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 (a `-p` run that merely *discusses* a 403 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 fetch that authenticates clears it.
#   PARK_ORG   — the account's organization turned Claude Code access off; 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='failed to authenticate|oauth (session|token)[a-z ]{0,20}(expired|invalid|revoked)|could not be refreshed|invalid api key|authentication_error|invalid bearer token|please run /login|run /login to'
PARK_ORG='organization has disabled|subscription access[a-z ]{0,20}disabl|disabled claude subscription|ask your admin to enable|not authorized to use claude code'
# A usage limit can be scoped to ONE MODEL: "You've reached your Fable 5 limit.
# Switch to another model, or manage usage credits..." — the account still has
# capacity for every other model, and the endpoint says so in the message itself.
#
# This phrasing matched NOTHING below: "reached your <model> limit" is not "limit
# reached", and "usage credits" is not "credit balance". So on 2026-08-24, when
# every account crossed that scoped bucket inside one hour, the shim did not even
# enter its retry branch — every task died in under a second having done no work,
# and the pool looked healthy the whole time.
MODEL_LIMITPAT="reached your [^.]{0,40} limit|switch to another model"
LIMITPAT='rate[ _-]?limit|usage limit|limit (reached|exceeded)|overloaded|"?529"?|credit balance'"|$MODEL_LIMITPAT"
ERRPAT="$LIMITPAT|$PARK_AUTH|$PARK_ORG"'|401|403|unauthorized|authentication[_ ]error|invalid[_ ](bearer|token|api key)|token (expired|revoked|invalid)|oauth.*(error|expired|invalid)'
# Deliberately a full id, not an alias: the whole point of pinning --model is that
# an unpinned run inherits whatever the operator was last using.
FALLBACK_MODEL="${CLAUDE_MULTIACC_FALLBACK_MODEL:-claude-opus-5}"

# Read the --model the caller pinned (both spellings). Empty = unpinned.
argv_model() {
  local i=0 n="${#ARGV[@]}"
  while [ "$i" -lt "$n" ]; do
    case "${ARGV[$i]}" in
      --model) i=$((i+1)); [ "$i" -lt "$n" ] && printf '%s\n' "${ARGV[$i]}"; return 0 ;;
      --model=*) printf '%s\n' "${ARGV[$i]#--model=}"; return 0 ;;
    esac
    i=$((i+1))
  done
  return 0
}

# Rewrite ARGV onto a different model, preserving the caller's spelling. An
# unpinned run gets the flag appended rather than left to inherit.
argv_set_model() { # $1 = model id
  local i=0 n="${#ARGV[@]}" found=0
  ARGV_FB=()
  while [ "$i" -lt "$n" ]; do
    case "${ARGV[$i]}" in
      --model)   ARGV_FB+=("--model" "$1");   i=$((i+2)); found=1; continue ;;
      --model=*) ARGV_FB+=("--model=$1");     i=$((i+1)); found=1; continue ;;
    esac
    ARGV_FB+=("${ARGV[$i]}"); i=$((i+1))
  done
  [ "$found" = 0 ] && ARGV_FB+=("--model" "$1")
}

PARK_SOFT_AUTH=3600      # 1h: a mis-parked healthy account is back within the hour
PARK_SOFT_ORG=21600      # 6h: an org policy will not change in minutes

# True when the tail of stdout carries a stream-json result marked is_error. Bounded
# to the tail because that is where the final object lands, and because a task log
# runs to hundreds of KB of transcript that must not be rescanned per attempt.
stream_reported_error() {
  tail -c 8192 "$tmpd/out" 2>/dev/null \
    | LC_ALL=C grep -qE '"type"[[:space:]]*:[[:space:]]*"result"' 2>/dev/null || return 1
  tail -c 8192 "$tmpd/out" 2>/dev/null \
    | LC_ALL=C grep -qE '"is_error"[[:space:]]*:[[:space:]]*true' 2>/dev/null
}

attempt=1
cur="$pick"
rc=0
stream_err=0       # the run exited 0 but the stream said otherwise
rotated=0          # at most one account rotation, exactly as before
model_fb_used=0    # ...and at most one model fallback after it
ARGV=("$@")
while :; do
  # Per ATTEMPT: a stream error seen on an earlier account must never decide the
  # exit status of a later one that failed for its own, real reason.
  stream_err=0
  tok="$(acct_token "$cur")"
  if [ -n "$stdin_file" ]; then exec 3< "$stdin_file"; else exec 3< /dev/null; fi
  if [ -n "$tok" ]; then
    CLAUDE_CONFIG_DIR="$cur" CLAUDE_CODE_OAUTH_TOKEN="$tok" "$REAL" "${ARGV[@]}" <&3 > "$tmpd/out" 2> "$tmpd/err"
  else
    CLAUDE_CONFIG_DIR="$cur" "$REAL" "${ARGV[@]}" <&3 > "$tmpd/out" 2> "$tmpd/err"
  fi
  rc=$?
  exec 3<&-
  # A `--output-format stream-json` run reports an API error INSIDE the stream and
  # still exits 0: the failure arrives as the final result object carrying
  # "is_error":true, not as a non-zero status. EVERY app-robot task takes that path,
  # so gating the retry branch on $rc alone meant the pool never rotated and never
  # fell back for the exact failures it exists for — the shim saw a clean success
  # every time while the task died in a second having done no work.
  #
  # Only the FINAL result counts. A 429 that appears mid-stream and is retried by
  # the client itself ends in a healthy result, and re-running that would throw away
  # a completed run.
  if [ "$rc" -eq 0 ] && stream_reported_error; then rc=1; stream_err=1; fi
  if [ "$rc" -ne 0 ] && { [ "$rotated" = 0 ] || [ "$model_fb_used" = 0 ]; } \
    && 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="the account's organization has disabled Claude Code subscription access"
      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
    # An account that ran out of ONE model has not run out. Cooling it down would
    # take a perfectly usable account out of the pool for every other model too —
    # and if the whole pool shares the bucket, cool the ENTIRE pool down at once.
    model_scoped=0
    grep -qiE "$MODEL_LIMITPAT" "$tmpd/out" "$tmpd/err" 2>/dev/null && model_scoped=1
    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: claude-accounts expired"
    elif [ "$model_scoped" = 0 ]; then
      {
        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" ] && [ "$rotated" = 0 ]; 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"
      rotated=1
      attempt=2
      continue
    fi
    # Another ACCOUNT could not help. If what ran out was one MODEL, the pool still
    # has capacity — switch to it rather than failing a task that has done no work.
    # Last resort by design: a healthy account must still serve the model the caller
    # pinned, so this only fires once rotation has already been tried and refused.
    if [ "$model_fb_used" = 0 ] \
      && grep -qiE "$MODEL_LIMITPAT" "$tmpd/out" "$tmpd/err" 2>/dev/null; then
      cm="$(argv_model)"
      if [ "$cm" != "$FALLBACK_MODEL" ]; then
        argv_set_model "$FALLBACK_MODEL"
        ARGV=("${ARGV_FB[@]}")
        model_fb_used=1
        attempt=2
        sel_log "model fallback ${cm:-<unpinned>} -> $FALLBACK_MODEL on $(basename "$cur") (scoped limit)"
        continue
      fi
    fi
  fi
  break
done

cat "$tmpd/out"
cat "$tmpd/err" >&2
# The exit status belongs to the CLI, not to us: a stream-json caller parses the
# stream and expects 0 here. We only borrowed rc to decide whether to retry.
[ "$stream_err" = 1 ] && [ "$rc" = 1 ] && rc=0
exit "$rc"
