claude-multiacc 2.0.26 → 2.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -315,6 +315,45 @@ same ownership rule. Note that the **installed layout deliberately shares that t
315
315
  default codex pool this scan stays off and the usage endpoint remains codex's only limit
316
316
  signal. Claude accounts keep a private `sessions/` registry, so it is fully active there.
317
317
 
318
+ A shared tree also gets **one shared rollout index**. codex refuses to start until its
319
+ `state_<schema>.sqlite` has indexed every rollout under `$CODEX_HOME/sessions` (each file
320
+ read whole), so a private index per account meant one full scan of the shared tree per
321
+ account — 10 GB × 12 on a busy Mac — and any launch that met another process's unfinished
322
+ scan died after 30 s with "timed out waiting for state db backfill … (status: running)",
323
+ surfaced as the CLI's "local database appears to be damaged". A scan cut short (a probe with
324
+ a timeout, Ctrl-C) also left its 15-minute worker lease behind, keeping the account
325
+ unstartable long after its killer was gone. Since 2.0.27 `<acct>/state_N.sqlite` is a symlink
326
+ to `~/.codex/state_N.sqlite`, exactly like the tree it describes: SQLite resolves the link
327
+ before naming its `-wal`/`-shm` companions, so every process shares one lock set — the same
328
+ multi-process mode a single `CODEX_HOME` already runs in. The shim applies this at every
329
+ launch, and the index name the installed binary carries is linked ahead of time, so a codex
330
+ schema bump still costs one backfill per machine rather than one per account.
331
+
332
+ An account with **no index of its own** — every newly added one, and the case the change
333
+ exists for — is simply linked. Nothing is renamed and nothing about the shared index's state
334
+ can block it. An account that already has one is only ever moved while **nothing can open
335
+ it** (no `-wal` and no `-shm` beside it). codex holds the state database through an sqlx pool
336
+ whose connections are opened lazily and *by path* (`max_connections(5)`,
337
+ `create_if_missing(true)`), so renaming an index under a live holder would leave its first
338
+ connection on the old inode while every later one follows the new link — one process, two
339
+ databases. A codex killed mid-write leaves the pair behind too, and the next clean session on
340
+ that account removes it, so an account excluded this way rejoins on its own. Once it is
341
+ closed, the account's index is either **retired** beside the link as `*.private` (never
342
+ deleted, never overwritten — a second retirement gets its own suffix) when the home already
343
+ has one, or **promoted** into the home when it does not. The promote goes through `link(2)`,
344
+ which refuses an existing target: two shims racing to be the first to promote could otherwise
345
+ rename one's fresh symlink onto the file the other had just promoted, leaving the shared index
346
+ pointing at itself — `ELOOP`, every account on the machine unable to start, and repaired by
347
+ neither codex nor the shim.
348
+
349
+ Two things stop the sharing entirely. An account whose `sessions/` is a real directory keeps a
350
+ private index — the rule is structural, not a heuristic. And codex's own corruption recovery
351
+ has the last word: when it judges a database damaged it renames it (with its `-wal`/`-shm`)
352
+ into `<CODEX_HOME>/db-backups/sqlite-<ts>-<n>/` and rebuilds, which under a link renames the
353
+ *link*. A link sitting in that folder pointing at the shared file is codex's verdict on that
354
+ file, so the account keeps the index codex rebuilt for it and that name is left alone —
355
+ handing the link back would hand the damage back.
356
+
318
357
  **Auto-retry** (`-p`/`--print` only, default on, `CLAUDE_SHIM_RETRY=0` disables): on an
319
358
  auth- or rate-limit-looking failure the shim retries once on a different account and
320
359
  marks the failed one — a **10-minute cooldown** for a rate limit (it heals on its own),
package/bin/codex CHANGED
@@ -122,6 +122,127 @@ sel_log() {
122
122
  printf '%s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" 2>/dev/null >> "$ACC_ROOT/selection.log" || true
123
123
  }
124
124
 
125
+ # ---- one index for the shared session tree -----------------------------------------
126
+ # codex keeps a SQLite index of the rollout tree beside it (state_<schema>.sqlite) and
127
+ # refuses to start until that index has been BACKFILLED from every rollout under
128
+ # $CODEX_HOME/sessions — each file read whole. The installed layout shares one tree
129
+ # across every account (<acct>/sessions -> ~/.codex/sessions), so a private index per
130
+ # account meant one full scan of the whole tree per account: 10 GB × 12 on a busy Mac,
131
+ # minutes each, and a launch that met another process's unfinished scan died after 30 s
132
+ # with "timed out waiting for state db backfill … (status: running)" — surfaced as the
133
+ # CLI's damaged-database message. A scan cut short (a probe with a timeout, Ctrl-C) also
134
+ # left its 15-minute worker lease behind, so the account stayed unstartable long after
135
+ # its killer was gone (my-mini 2026-09-09: six fresh accounts, every launch refused).
136
+ #
137
+ # A shared tree gets ONE shared index: <acct>/state_N.sqlite is a symlink to the same
138
+ # file under ~/.codex, exactly like the tree it describes. SQLite resolves that symlink
139
+ # before naming its -wal/-shm companions, so every process shares one lock set — the
140
+ # multi-process mode a single CODEX_HOME already runs in. A private index that already
141
+ # exists and nothing holds open is retired beside the link (never deleted), or promoted
142
+ # into the home when the home has none yet; and the index the installed binary will
143
+ # create next is linked ahead of time, so a schema bump still costs one backfill, not one
144
+ # per account. Structural on purpose: a real (private) session tree keeps a private index.
145
+ # lib/common.sh carries the same function for seeding — keep the two in step.
146
+ # CORE — byte-identical in lib/common.sh; tests diff them.
147
+ state_index_names() { # $1 acct dir, $2 home dir -> the index file names either side holds
148
+ local f name seen=" "
149
+ for f in "$2"/state_[0-9]*.sqlite "$1"/state_[0-9]*.sqlite; do
150
+ [ -e "$f" ] || [ -L "$f" ] || continue # an unmatched glob is the pattern itself
151
+ name="${f##*/}"
152
+ case "$seen" in *" $name "*) continue ;; esac
153
+ seen="$seen$name "
154
+ printf '%s\n' "$name"
155
+ done
156
+ }
157
+
158
+ installed_state_index_name() { # the index file the installed binary creates; memoized per binary
159
+ local cache="$ACC_ROOT/.state-index" bin="$REAL" real id line name="" nat
160
+ # macOS `stat` reports a SYMLINK's own mtime, and codex is usually installed behind a
161
+ # stable launcher symlink — so the memo has to key on the file the link resolves to.
162
+ real="$(canon_path "$bin")"
163
+ id="$real:$(file_mtime "$real")"
164
+ if [ -f "$cache" ]; then
165
+ IFS= read -r line < "$cache" 2>/dev/null || line=""
166
+ case "$line" in "$id "*) printf '%s\n' "${line#"$id "}"; return 0 ;; esac
167
+ fi
168
+ # The npm launcher is a script; the schema name lives in the native binary vendored
169
+ # beside it. Try the launcher first (a bare binary answers directly), then the vendor.
170
+ for nat in "$bin" "$(dirname "$(canon_path "$bin")")"/../node_modules/@openai/codex-*/vendor/*/bin/codex; do
171
+ [ -f "$nat" ] || continue
172
+ name="$(LC_ALL=C grep -a -o -m1 'state_[0-9][0-9]*\.sqlite' "$nat" 2>/dev/null | head -1)"
173
+ [ -n "$name" ] && break
174
+ done
175
+ case "$name" in *[!A-Za-z0-9_.]*) name="" ;; esac
176
+ { printf '%s %s\n' "$id" "$name" > "$cache.$$" && mv -f "$cache.$$" "$cache"; } 2>/dev/null \
177
+ || rm -f "$cache.$$" 2>/dev/null
178
+ printf '%s\n' "$name"
179
+ }
180
+
181
+ # CORE — byte-identical in lib/common.sh; tests diff them.
182
+ shared_index_rejected() { # $1 acct dir, $2 index name, $3 shared target
183
+ # True when codex's own corruption recovery moved THIS account's link out of the way:
184
+ # it renames the database it judged damaged (and its -wal/-shm) into
185
+ # <CODEX_HOME>/db-backups/sqlite-<ts>-<n>/ and rebuilds, and under a link that renames
186
+ # the LINK. A link sitting in there pointing at the shared file is codex's verdict on
187
+ # that file, so this account keeps the index codex rebuilt for it and this name is
188
+ # left alone — handing the link back would hand the damage back.
189
+ local marker
190
+ for marker in "$1"/db-backups/*/"$2"; do
191
+ [ -L "$marker" ] || continue
192
+ [ "$(readlink "$marker" 2>/dev/null)" = "$3" ] && return 0
193
+ done
194
+ return 1
195
+ }
196
+
197
+ # CORE — byte-identical in lib/common.sh; tests diff them.
198
+ share_state_index_links() { # $1 acct dir, $2 home dir, $3 extra index name ('' for none)
199
+ local d="$1" home="$2" name names target link retired n
200
+ [ -L "$d" ] && return 0 # adopted: the dir IS the home
201
+ [ -L "$d/sessions" ] || return 0 # a private tree keeps its private index
202
+ [ "$(readlink "$d/sessions" 2>/dev/null)" = "$home/sessions" ] || return 0
203
+ [ -d "$home" ] || return 0
204
+ names="$(state_index_names "$d" "$home")"
205
+ case "${3:-}" in ''|*[!A-Za-z0-9_.]*) ;; *) names="$names $3" ;; esac
206
+ for name in $names; do
207
+ target="$home/$name"; link="$d/$name"
208
+ [ -L "$link" ] && continue # already shared (or pointed elsewhere on purpose)
209
+ shared_index_rejected "$d" "$name" "$target" && continue
210
+ if [ ! -e "$link" ]; then
211
+ ln -s "$target" "$link" 2>/dev/null || true # a new account: the whole point
212
+ continue
213
+ fi
214
+ # This account has an index of its own, and MOVING one that a process can still open
215
+ # is unsafe. codex holds the state database through an sqlx pool that opens its
216
+ # connections lazily and BY PATH (max_connections(5), create_if_missing(true) —
217
+ # codex-rs/state/src/sqlite.rs open_read_write_pool), so a rename under a live holder
218
+ # leaves connection 1 on the old inode while every connection the pool opens
219
+ # afterwards follows the new link: one process, two databases. A -wal or a -shm beside
220
+ # the file is that proof. A codex killed mid-write leaves them behind too, and the
221
+ # next clean session on the account removes them, so this heals itself in time.
222
+ { [ -e "$link-wal" ] || [ -e "$link-shm" ]; } && continue
223
+ if [ -e "$target" ]; then
224
+ retired="$link.private"; n=0 # never overwrite an earlier copy
225
+ while [ -e "$retired" ] && [ "$n" -lt 100 ]; do n=$((n+1)); retired="$link.private.$n"; done
226
+ [ -e "$retired" ] && continue
227
+ mv "$link" "$retired" 2>/dev/null || continue
228
+ else
229
+ # Atomic or nothing: link(2) refuses an existing target, so two shims racing to be
230
+ # the first to promote cannot rename one's fresh symlink onto the file the other
231
+ # just promoted — which is how a self-referential shared index (ELOOP, and every
232
+ # account on the Mac unable to start) could appear. A cross-device link simply
233
+ # fails and this account keeps its own index.
234
+ ln "$link" "$target" 2>/dev/null || continue
235
+ [ -L "$link" ] || rm -f "$link"
236
+ fi
237
+ ln -s "$target" "$link" 2>/dev/null || true
238
+ done
239
+ return 0
240
+ }
241
+
242
+ share_state_index() { # $1 acct dir — at launch: the home's index, plus the one this binary will create
243
+ share_state_index_links "$1" "${HOME:-/nonexistent}/.codex" "$(installed_state_index_name)"
244
+ }
245
+
125
246
  marker_active() { # true if $1/.limited is still in force; clears cleanly-expired markers
126
247
  # Parity with bin/claude's client_marker_recovered, by construction: this shim has NO
127
248
  # telemetry-based clearing path. A marker leaves here only when its OWN reset epoch has
@@ -521,6 +642,7 @@ if [ -n "${CODEX_ACCOUNT:-}" ]; then
521
642
  d="$ACC_ROOT/$CODEX_ACCOUNT"
522
643
  if [ -d "$d" ]; then
523
644
  sel_log "$CODEX_ACCOUNT pinned pwd=$PWD"
645
+ share_state_index "$d"
524
646
  export CODEX_HOME="$d"
525
647
  export CODEX_SHIM_ACTIVE=1
526
648
  exec "$REAL" "$@"
@@ -805,6 +927,7 @@ if [ "${CODEX_SHIM_RETRY:-1}" != "0" ] && [ "${#eligible[@]}" -ge 2 ]; then
805
927
  fi
806
928
  fi
807
929
 
930
+ share_state_index "$pick"
808
931
  if [ "$wants_retry" = "0" ]; then
809
932
  export CODEX_HOME="$pick"
810
933
  exec "$REAL" "$@"
@@ -906,6 +1029,7 @@ while :; do
906
1029
  if [ -n "$next" ]; then
907
1030
  sel_log "retry from=$(basename "$cur") to=$(basename "$next") rc=$rc"
908
1031
  cur="$next"
1032
+ share_state_index "$cur"
909
1033
  # The account that actually serves the work is the one the next run should rotate
910
1034
  # away from — not the one that bounced.
911
1035
  remember_pick "$cur"
package/lib/common.sh CHANGED
@@ -588,12 +588,101 @@ os.replace(sys.argv[1] + '.tmp', sys.argv[1])
588
588
  PYEOF
589
589
  }
590
590
 
591
+ # ---- one index for the shared session tree -----------------------------------------
592
+ # codex refuses to start until its rollout index (state_<schema>.sqlite) has been
593
+ # backfilled from every rollout under $CODEX_HOME/sessions. The codex layout shares that
594
+ # tree across accounts (<acct>/sessions -> ~/.codex/sessions), so the index is shared
595
+ # the same way: <acct>/state_N.sqlite -> ~/.codex/state_N.sqlite. Otherwise every new
596
+ # account re-reads the whole tree (10 GB on a busy Mac) before its first launch, and a
597
+ # launch that meets another process's unfinished scan dies after 30 s with the CLI's
598
+ # "local database appears to be damaged" message. bin/codex applies the same at every
599
+ # launch (and pre-links the name the installed binary will create); seeding covers a
600
+ # CODEX_HOME handed straight to the real binary. The two CORE functions below are
601
+ # byte-identical with bin/codex — tests/run-tests.sh diffs them.
602
+ # CORE — byte-identical in bin/codex; tests diff them.
603
+ state_index_names() { # $1 acct dir, $2 home dir -> the index file names either side holds
604
+ local f name seen=" "
605
+ for f in "$2"/state_[0-9]*.sqlite "$1"/state_[0-9]*.sqlite; do
606
+ [ -e "$f" ] || [ -L "$f" ] || continue # an unmatched glob is the pattern itself
607
+ name="${f##*/}"
608
+ case "$seen" in *" $name "*) continue ;; esac
609
+ seen="$seen$name "
610
+ printf '%s\n' "$name"
611
+ done
612
+ }
613
+
614
+ # CORE — byte-identical in bin/codex; tests diff them.
615
+ shared_index_rejected() { # $1 acct dir, $2 index name, $3 shared target
616
+ # True when codex's own corruption recovery moved THIS account's link out of the way:
617
+ # it renames the database it judged damaged (and its -wal/-shm) into
618
+ # <CODEX_HOME>/db-backups/sqlite-<ts>-<n>/ and rebuilds, and under a link that renames
619
+ # the LINK. A link sitting in there pointing at the shared file is codex's verdict on
620
+ # that file, so this account keeps the index codex rebuilt for it and this name is
621
+ # left alone — handing the link back would hand the damage back.
622
+ local marker
623
+ for marker in "$1"/db-backups/*/"$2"; do
624
+ [ -L "$marker" ] || continue
625
+ [ "$(readlink "$marker" 2>/dev/null)" = "$3" ] && return 0
626
+ done
627
+ return 1
628
+ }
629
+
630
+ # CORE — byte-identical in bin/codex; tests diff them.
631
+ share_state_index_links() { # $1 acct dir, $2 home dir, $3 extra index name ('' for none)
632
+ local d="$1" home="$2" name names target link retired n
633
+ [ -L "$d" ] && return 0 # adopted: the dir IS the home
634
+ [ -L "$d/sessions" ] || return 0 # a private tree keeps its private index
635
+ [ "$(readlink "$d/sessions" 2>/dev/null)" = "$home/sessions" ] || return 0
636
+ [ -d "$home" ] || return 0
637
+ names="$(state_index_names "$d" "$home")"
638
+ case "${3:-}" in ''|*[!A-Za-z0-9_.]*) ;; *) names="$names $3" ;; esac
639
+ for name in $names; do
640
+ target="$home/$name"; link="$d/$name"
641
+ [ -L "$link" ] && continue # already shared (or pointed elsewhere on purpose)
642
+ shared_index_rejected "$d" "$name" "$target" && continue
643
+ if [ ! -e "$link" ]; then
644
+ ln -s "$target" "$link" 2>/dev/null || true # a new account: the whole point
645
+ continue
646
+ fi
647
+ # This account has an index of its own, and MOVING one that a process can still open
648
+ # is unsafe. codex holds the state database through an sqlx pool that opens its
649
+ # connections lazily and BY PATH (max_connections(5), create_if_missing(true) —
650
+ # codex-rs/state/src/sqlite.rs open_read_write_pool), so a rename under a live holder
651
+ # leaves connection 1 on the old inode while every connection the pool opens
652
+ # afterwards follows the new link: one process, two databases. A -wal or a -shm beside
653
+ # the file is that proof. A codex killed mid-write leaves them behind too, and the
654
+ # next clean session on the account removes them, so this heals itself in time.
655
+ { [ -e "$link-wal" ] || [ -e "$link-shm" ]; } && continue
656
+ if [ -e "$target" ]; then
657
+ retired="$link.private"; n=0 # never overwrite an earlier copy
658
+ while [ -e "$retired" ] && [ "$n" -lt 100 ]; do n=$((n+1)); retired="$link.private.$n"; done
659
+ [ -e "$retired" ] && continue
660
+ mv "$link" "$retired" 2>/dev/null || continue
661
+ else
662
+ # Atomic or nothing: link(2) refuses an existing target, so two shims racing to be
663
+ # the first to promote cannot rename one's fresh symlink onto the file the other
664
+ # just promoted — which is how a self-referential shared index (ELOOP, and every
665
+ # account on the Mac unable to start) could appear. A cross-device link simply
666
+ # fails and this account keeps its own index.
667
+ ln "$link" "$target" 2>/dev/null || continue
668
+ [ -L "$link" ] || rm -f "$link"
669
+ fi
670
+ ln -s "$target" "$link" 2>/dev/null || true
671
+ done
672
+ return 0
673
+ }
674
+
675
+ codex_share_state_index() { # $1 acct dir — seed time: whatever index names exist already
676
+ share_state_index_links "$1" "${HOME:-/nonexistent}/.codex" ""
677
+ }
678
+
591
679
  # Seed an account config dir so headless runs never prompt.
592
680
  # claude: stripped .claude.json, copied settings.json, shared projects/ symlink
593
681
  # (shared history => --continue/--resume work regardless of picked account).
594
682
  # codex: copied config.toml (carries project trust + settings, no identity),
595
- # shared sessions/ symlink (=> `codex resume` finds every session), and a
596
- # shared AGENTS.md symlink so global instructions apply under any account.
683
+ # shared sessions/ symlink (=> `codex resume` finds every session) with the
684
+ # shared rollout index that tree needs, and a shared AGENTS.md symlink so
685
+ # global instructions apply under any account.
597
686
  seed_account_dir() { # $1 = acct dir
598
687
  local d="$1"
599
688
  mkdir -p "$d"
@@ -608,6 +697,7 @@ seed_account_dir() { # $1 = acct dir
608
697
  if [ ! -e "$d/AGENTS.md" ] && [ -f "$HOME/.codex/AGENTS.md" ]; then
609
698
  ln -s "$HOME/.codex/AGENTS.md" "$d/AGENTS.md" 2>/dev/null || true
610
699
  fi
700
+ codex_share_state_index "$d"
611
701
  return 0
612
702
  fi
613
703
  if [ ! -f "$d/.claude.json" ] && [ -f "$HOME/.claude.json" ]; then
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-multiacc",
3
- "version": "2.0.26",
3
+ "version": "2.0.27",
4
4
  "description": "Unified Claude Code and OpenAI Codex subscription pooling with quota-aware selection.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4906,6 +4906,296 @@ check "codex: sign-in as registered email refused" "already added as acct-01" "$
4906
4906
  n="$(grep -c '"email": "a@cx"' "$CX/accounts.json")"
4907
4907
  [ "$n" = "1" ] && t_ok "codex: no second entry for a re-signed-in email" || t_fail "codex dup signin" "count=$n"
4908
4908
 
4909
+ # ---- C11b. one index for the shared session tree ------------------------------------
4910
+ # codex refuses to start until state_<schema>.sqlite has indexed every rollout under
4911
+ # $CODEX_HOME/sessions. The layout shares that tree across accounts, so it must share
4912
+ # the index too — a private index per account re-read the whole tree per account and
4913
+ # stranded a 15-minute worker lease whenever a scan was cut short (my-mini 2026-09-09:
4914
+ # six fresh accounts refused every launch as "local database appears to be damaged").
4915
+ CXH="$WORK/cxhome"
4916
+ mkdir -p "$CXH/.codex/sessions"
4917
+ printf 'home index' > "$CXH/.codex/state_5.sqlite"
4918
+ # (a) seeding: a new account links the tree AND the index the home already has
4919
+ out="$(HOME="$CXH" FAKE_EMAIL=idx@cx codex-accounts add 2>&1)"
4920
+ nid="$(printf '%s' "$out" | sed -n 's/.*Registered \(acct-[0-9]*\) .*/\1/p' | head -1)"
4921
+ [ -n "$nid" ] || t_fail "codex shared index seed" "add did not register: $(printf '%s' "$out" | head -c 200)"
4922
+ [ "$(readlink "$CX/$nid/sessions" 2>/dev/null)" = "$CXH/.codex/sessions" ] \
4923
+ && t_ok "codex: a new account shares the home's session tree" \
4924
+ || t_fail "codex shared tree seed" "sessions -> $(readlink "$CX/$nid/sessions" 2>/dev/null)"
4925
+ [ "$(readlink "$CX/$nid/state_5.sqlite" 2>/dev/null)" = "$CXH/.codex/state_5.sqlite" ] \
4926
+ && t_ok "codex: a new account shares the home's rollout index with that tree" \
4927
+ || t_fail "codex shared index seed" "state_5.sqlite -> $(readlink "$CX/$nid/state_5.sqlite" 2>/dev/null)"
4928
+ [ -n "$nid" ] && codex-accounts remove "$nid" --yes >/dev/null 2>&1
4929
+
4930
+ # The launch-time heal, on its own pool so nothing here leaks into the other codex tests.
4931
+ CXS="$WORK/cx-state"
4932
+ mkdir -p "$CXS/tmp" "$CXS/acct-01" "$CXS/acct-02" "$CXS/acct-03"
4933
+ : > "$CXS/.limits-kick"
4934
+ cat > "$CXS/accounts.json" <<EOF2
4935
+ {"version": 1, "server": "root@203.0.113.1", "server_root": "/root/.codex-accounts",
4936
+ "server_repo": "/root/claude-multiacc", "threshold": 90,
4937
+ "accounts": [
4938
+ {"id": "acct-01", "email": "s1@cx", "home": "mac", "added_at": "2026-09-09T00:00:00Z"},
4939
+ {"id": "acct-02", "email": "s2@cx", "home": "mac", "added_at": "2026-09-09T00:00:00Z"},
4940
+ {"id": "acct-03", "email": "s3@cx", "home": "mac", "added_at": "2026-09-09T00:00:00Z"}]}
4941
+ EOF2
4942
+ for i in 01 02 03; do mk_cx_auth "$CXS/acct-$i/auth.json" "s$i@cx" "$FUTURE_EXP"; done
4943
+ CXH2="$WORK/cxhome2"
4944
+ mkdir -p "$CXH2/.codex/sessions"
4945
+ ln -s "$CXH2/.codex/sessions" "$CXS/acct-01/sessions"
4946
+ ln -s "$CXH2/.codex/sessions" "$CXS/acct-02/sessions"
4947
+ mkdir -p "$CXS/acct-03/sessions" # a PRIVATE tree
4948
+ printf 'closed private index' > "$CXS/acct-01/state_5.sqlite"
4949
+ printf 'open private index' > "$CXS/acct-02/state_5.sqlite"
4950
+ printf 'wal' > "$CXS/acct-02/state_5.sqlite-wal"
4951
+ printf 'shm' > "$CXS/acct-02/state_5.sqlite-shm"
4952
+ printf 'private tree index' > "$CXS/acct-03/state_5.sqlite"
4953
+ # (b) an OPEN private index (its -wal beside it) is never moved while the home has no index
4954
+ out="$(HOME="$CXH2" CODEX_ACCOUNTS_DIR="$CXS" CODEX_ACCOUNT=acct-02 codex 2>&1)"
4955
+ check "codex: launch on an account with an open private index" "CFG=acct-02" "$out"
4956
+ { [ -f "$CXS/acct-02/state_5.sqlite" ] && [ ! -L "$CXS/acct-02/state_5.sqlite" ] \
4957
+ && [ ! -e "$CXH2/.codex/state_5.sqlite" ]; } \
4958
+ && t_ok "codex: an open private index is never promoted (one inode, two -shm files, is how WAL corrupts)" \
4959
+ || t_fail "codex open index promote" "$(ls -la "$CXS/acct-02" "$CXH2/.codex" 2>&1 | head -c 400)"
4960
+ # a -shm on its own is just as much a live database as a -wal is
4961
+ mv "$CXS/acct-02/state_5.sqlite-wal" "$CXS/acct-02/held-wal"
4962
+ out="$(HOME="$CXH2" CODEX_ACCOUNTS_DIR="$CXS" CODEX_ACCOUNT=acct-02 codex 2>&1)"
4963
+ check "codex: launch on an account whose index has only a -shm" "CFG=acct-02" "$out"
4964
+ { [ -f "$CXS/acct-02/state_5.sqlite" ] && [ ! -e "$CXH2/.codex/state_5.sqlite" ]; } \
4965
+ && t_ok "codex: a -shm alone also refuses the promote" \
4966
+ || t_fail "codex shm promote" "$(ls -la "$CXS/acct-02" "$CXH2/.codex" 2>&1 | head -c 400)"
4967
+ mv "$CXS/acct-02/held-wal" "$CXS/acct-02/state_5.sqlite-wal"
4968
+ # (c) a CLOSED private index is promoted into the home and linked back
4969
+ out="$(HOME="$CXH2" CODEX_ACCOUNTS_DIR="$CXS" CODEX_ACCOUNT=acct-01 codex 2>&1)"
4970
+ check "codex: launch on an account with a closed private index" "CFG=acct-01" "$out"
4971
+ [ "$(cat "$CXH2/.codex/state_5.sqlite" 2>/dev/null)" = "closed private index" ] \
4972
+ && t_ok "codex: the first closed private index becomes the home's shared index" \
4973
+ || t_fail "codex index promote" "home index: $(cat "$CXH2/.codex/state_5.sqlite" 2>&1 | head -c 80)"
4974
+ [ "$(readlink "$CXS/acct-01/state_5.sqlite" 2>/dev/null)" = "$CXH2/.codex/state_5.sqlite" ] \
4975
+ && t_ok "codex: the promoting account links to the shared index" \
4976
+ || t_fail "codex index promote link" "state_5.sqlite -> $(readlink "$CXS/acct-01/state_5.sqlite" 2>/dev/null)"
4977
+ # (d) the home has an index now, but the account's own is still OPEN: moving it would
4978
+ # split its holder across two databases (codex's sqlx pool re-opens BY PATH), so it
4979
+ # is left alone until the process that has it exits and clears its -wal/-shm.
4980
+ out="$(HOME="$CXH2" CODEX_ACCOUNTS_DIR="$CXS" CODEX_ACCOUNT=acct-02 codex 2>&1)"
4981
+ check "codex: relaunch on the account with the open private index" "CFG=acct-02" "$out"
4982
+ { [ -f "$CXS/acct-02/state_5.sqlite" ] && [ ! -L "$CXS/acct-02/state_5.sqlite" ]; } \
4983
+ && t_ok "codex: an index a process can still open is never moved, even once the home has one" \
4984
+ || t_fail "codex open index retire" "$(ls "$CXS/acct-02" 2>&1 | tr '\n' ' ')"
4985
+ # (d2) its holder exited and SQLite removed the pair: now it is retired beside the link
4986
+ rm -f "$CXS/acct-02/state_5.sqlite-wal" "$CXS/acct-02/state_5.sqlite-shm"
4987
+ out="$(HOME="$CXH2" CODEX_ACCOUNTS_DIR="$CXS" CODEX_ACCOUNT=acct-02 codex 2>&1)"
4988
+ check "codex: relaunch once the private index is closed" "CFG=acct-02" "$out"
4989
+ [ "$(readlink "$CXS/acct-02/state_5.sqlite" 2>/dev/null)" = "$CXH2/.codex/state_5.sqlite" ] \
4990
+ && t_ok "codex: a closed private index is retired for the shared one once the home has it" \
4991
+ || t_fail "codex index retire" "state_5.sqlite -> $(readlink "$CXS/acct-02/state_5.sqlite" 2>/dev/null)"
4992
+ [ "$(cat "$CXS/acct-02/state_5.sqlite.private" 2>/dev/null)" = "open private index" ] \
4993
+ && t_ok "codex: the retired index stays beside the link, never deleted" \
4994
+ || t_fail "codex index retire files" "$(ls "$CXS/acct-02" 2>&1 | tr '\n' ' ')"
4995
+ [ "$(cat "$CXH2/.codex/state_5.sqlite" 2>/dev/null)" = "closed private index" ] \
4996
+ && t_ok "codex: retiring never overwrites the shared index" \
4997
+ || t_fail "codex index retire overwrite" "home index changed"
4998
+ # (d3) an account with NO index of its own is linked even while the shared one is in use —
4999
+ # the case the whole change exists for, and the one liveness must never block
5000
+ mkdir -p "$CXS/acct-06" && ln -s "$CXH2/.codex/sessions" "$CXS/acct-06/sessions"
5001
+ mk_cx_auth "$CXS/acct-06/auth.json" s6@cx "$FUTURE_EXP"
5002
+ printf 'wal' > "$CXH2/.codex/state_5.sqlite-wal"
5003
+ printf 'shm' > "$CXH2/.codex/state_5.sqlite-shm"
5004
+ out="$(HOME="$CXH2" CODEX_ACCOUNTS_DIR="$CXS" CODEX_ACCOUNT=acct-06 codex 2>&1)"
5005
+ check "codex: launch on a brand-new account while the shared index is in use" "CFG=acct-06" "$out"
5006
+ [ "$(readlink "$CXS/acct-06/state_5.sqlite" 2>/dev/null)" = "$CXH2/.codex/state_5.sqlite" ] \
5007
+ && t_ok "codex: an account with no index of its own is linked whatever the shared one is doing" \
5008
+ || t_fail "codex new account link" "$(ls -la "$CXS/acct-06" 2>&1 | head -c 300)"
5009
+ rm -f "$CXH2/.codex/state_5.sqlite-wal" "$CXH2/.codex/state_5.sqlite-shm"
5010
+ # (e) a PRIVATE session tree keeps its private index
5011
+ out="$(HOME="$CXH2" CODEX_ACCOUNTS_DIR="$CXS" CODEX_ACCOUNT=acct-03 codex 2>&1)"
5012
+ check "codex: launch on an account with its own session tree" "CFG=acct-03" "$out"
5013
+ { [ -f "$CXS/acct-03/state_5.sqlite" ] && [ ! -L "$CXS/acct-03/state_5.sqlite" ]; } \
5014
+ && t_ok "codex: an account with a private session tree keeps its private index" \
5015
+ || t_fail "codex private tree index" "$(ls -la "$CXS/acct-03" 2>&1 | head -c 300)"
5016
+ # (f) the index the INSTALLED binary will create is linked ahead of time, so a schema bump
5017
+ # still costs one backfill per Mac, not one per account
5018
+ FAKEBIN_IDX="$WORK/fakebin-idx"
5019
+ mkdir -p "$FAKEBIN_IDX"
5020
+ { cat "$FAKEBIN/codex"; printf '# rollout index name carried by the real binary: state_7.sqlite\n'; } > "$FAKEBIN_IDX/codex"
5021
+ chmod +x "$FAKEBIN_IDX/codex"
5022
+ out="$(HOME="$CXH2" CODEX_ACCOUNTS_DIR="$CXS" CODEX_ACCOUNT=acct-01 PATH="$REPO_DIR/bin:$FAKEBIN_IDX:$PATH" codex 2>&1)"
5023
+ check "codex: launch through a binary that names a newer index" "CFG=acct-01" "$out"
5024
+ [ "$(readlink "$CXS/acct-01/state_7.sqlite" 2>/dev/null)" = "$CXH2/.codex/state_7.sqlite" ] \
5025
+ && t_ok "codex: the index the installed binary creates next is linked before it exists" \
5026
+ || t_fail "codex index pre-link" "state_7.sqlite -> $(readlink "$CXS/acct-01/state_7.sqlite" 2>/dev/null)"
5027
+ [ ! -e "$CXH2/.codex/state_7.sqlite" ] && t_ok "codex: pre-linking creates nothing in the home itself" \
5028
+ || t_fail "codex index pre-link home" "home gained state_7.sqlite"
5029
+ grep -q " state_7.sqlite$" "$CXS/.state-index" 2>/dev/null \
5030
+ && t_ok "codex: the binary's index name is memoized per binary" \
5031
+ || t_fail "codex index name memo" "$(cat "$CXS/.state-index" 2>&1)"
5032
+ # (f2) the launcher is a script that names no index; the schema name lives in the native
5033
+ # binary vendored beside it — the branch that actually fires on an npm install
5034
+ VENDOR="$WORK/npm/node_modules/@openai/codex/bin"
5035
+ mkdir -p "$VENDOR" "$WORK/npm/node_modules/@openai/codex/node_modules/@openai/codex-darwin-arm64/vendor/aarch64-apple-darwin/bin"
5036
+ { cat "$FAKEBIN/codex"; printf '# a launcher script naming no index\n'; } > "$VENDOR/codex.js"
5037
+ chmod +x "$VENDOR/codex.js"
5038
+ printf 'binary bytes state_8.sqlite more bytes' \
5039
+ > "$WORK/npm/node_modules/@openai/codex/node_modules/@openai/codex-darwin-arm64/vendor/aarch64-apple-darwin/bin/codex"
5040
+ FAKEBIN_VENDOR="$WORK/fakebin-vendor"
5041
+ mkdir -p "$FAKEBIN_VENDOR"
5042
+ ln -s "$VENDOR/codex.js" "$FAKEBIN_VENDOR/codex"
5043
+ rm -f "$CXS/.state-index"
5044
+ out="$(HOME="$CXH2" CODEX_ACCOUNTS_DIR="$CXS" CODEX_ACCOUNT=acct-01 PATH="$REPO_DIR/bin:$FAKEBIN_VENDOR:$PATH" codex 2>&1)"
5045
+ check "codex: launch through an npm launcher with a vendored binary" "CFG=acct-01" "$out"
5046
+ [ "$(readlink "$CXS/acct-01/state_8.sqlite" 2>/dev/null)" = "$CXH2/.codex/state_8.sqlite" ] \
5047
+ && t_ok "codex: the index name is read from the vendored binary when the launcher names none" \
5048
+ || t_fail "codex vendored index name" "$(ls "$CXS/acct-01" 2>&1 | tr '\n' ' '); memo=$(cat "$CXS/.state-index" 2>&1)"
5049
+ grep -q "^$(cd "$(dirname "$VENDOR/codex.js")" && pwd -P)/codex.js:" "$CXS/.state-index" 2>/dev/null \
5050
+ && t_ok "codex: the memo keys on the file the launcher symlink resolves to, not the link" \
5051
+ || t_fail "codex memo key" "$(cat "$CXS/.state-index" 2>&1)"
5052
+ rm -f "$CXS/.state-index" "$CXS/acct-01/state_8.sqlite"
5053
+ # (g) an adopted account (the dir IS the home) is never rewritten
5054
+ ln -s "$CXH2/.codex" "$CXS/acct-04"
5055
+ python3 - "$CXS/accounts.json" <<'PYEOF'
5056
+ import json, sys
5057
+ doc = json.load(open(sys.argv[1]))
5058
+ doc['accounts'].append({'id': 'acct-04', 'email': 'adopted@cx', 'home': 'mac', 'added_at': '2026-09-09T00:00:00Z'})
5059
+ json.dump(doc, open(sys.argv[1], 'w'), indent=2)
5060
+ PYEOF
5061
+ mk_cx_auth "$CXH2/.codex/auth.json" adopted@cx "$FUTURE_EXP"
5062
+ out="$(HOME="$CXH2" CODEX_ACCOUNTS_DIR="$CXS" CODEX_ACCOUNT=acct-04 codex 2>&1)"
5063
+ check "codex: launch on an adopted account" "CFG=acct-04" "$out"
5064
+ { [ -f "$CXH2/.codex/state_5.sqlite" ] && [ ! -L "$CXH2/.codex/state_5.sqlite" ]; } \
5065
+ && t_ok "codex: the home's own index is never turned into a link" \
5066
+ || t_fail "codex adopted index" "$(ls -la "$CXH2/.codex" 2>&1 | head -c 300)"
5067
+ # (i) codex's own corruption recovery moved this account's LINK into db-backups/ and
5068
+ # rebuilt a private index in its place: that is codex's verdict on the shared file,
5069
+ # so the account keeps what codex rebuilt and this name is left alone entirely.
5070
+ mkdir -p "$CXS/acct-01/db-backups/sqlite-1700000000-0"
5071
+ mv "$CXS/acct-01/state_5.sqlite" "$CXS/acct-01/db-backups/sqlite-1700000000-0/state_5.sqlite"
5072
+ printf 'rebuilt after damage' > "$CXS/acct-01/state_5.sqlite"
5073
+ homesum="$(cat "$CXH2/.codex/state_5.sqlite" 2>/dev/null)"
5074
+ out="$(HOME="$CXH2" CODEX_ACCOUNTS_DIR="$CXS" CODEX_ACCOUNT=acct-01 codex 2>&1)"
5075
+ check "codex: launch after codex rejected the shared index for this account" "CFG=acct-01" "$out"
5076
+ { [ "$(cat "$CXS/acct-01/state_5.sqlite" 2>/dev/null)" = "rebuilt after damage" ] \
5077
+ && [ ! -L "$CXS/acct-01/state_5.sqlite" ]; } \
5078
+ && t_ok "codex: an account codex rebuilt for keeps that index instead of the shared one" \
5079
+ || t_fail "codex rejected shared index" "$(ls -la "$CXS/acct-01" 2>&1 | head -c 400)"
5080
+ [ "$(cat "$CXH2/.codex/state_5.sqlite" 2>/dev/null)" = "$homesum" ] \
5081
+ && t_ok "codex: the rejected shared index is left exactly as it was, for its other users" \
5082
+ || t_fail "codex rejected shared index home" "home index changed"
5083
+ # the marker only speaks for the file it names: a link pointing elsewhere is not a verdict
5084
+ mkdir -p "$CXS/acct-05/db-backups/sqlite-1700000000-0"
5085
+ mkdir -p "$CXS/acct-05" && ln -s "$CXH2/.codex/sessions" "$CXS/acct-05/sessions"
5086
+ mk_cx_auth "$CXS/acct-05/auth.json" s5@cx "$FUTURE_EXP"
5087
+ ln -s "$CXH2/.codex/state_9.sqlite" "$CXS/acct-05/db-backups/sqlite-1700000000-0/state_5.sqlite"
5088
+ printf 'unrelated private index' > "$CXS/acct-05/state_5.sqlite"
5089
+ out="$(HOME="$CXH2" CODEX_ACCOUNTS_DIR="$CXS" CODEX_ACCOUNT=acct-05 codex 2>&1)"
5090
+ check "codex: launch with a db-backups link naming another file" "CFG=acct-05" "$out"
5091
+ [ "$(readlink "$CXS/acct-05/state_5.sqlite" 2>/dev/null)" = "$CXH2/.codex/state_5.sqlite" ] \
5092
+ && t_ok "codex: a db-backups link that names a different file is not a verdict on the shared one" \
5093
+ || t_fail "codex marker scope" "$(ls -la "$CXS/acct-05" 2>&1 | head -c 300)"
5094
+ # (j) a second retirement keeps the first retired copy
5095
+ rm "$CXS/acct-02/state_5.sqlite"
5096
+ printf 'second private index' > "$CXS/acct-02/state_5.sqlite"
5097
+ out="$(HOME="$CXH2" CODEX_ACCOUNTS_DIR="$CXS" CODEX_ACCOUNT=acct-02 codex 2>&1)"
5098
+ check "codex: launch on an account retiring a second private index" "CFG=acct-02" "$out"
5099
+ { [ "$(cat "$CXS/acct-02/state_5.sqlite.private" 2>/dev/null)" = "open private index" ] \
5100
+ && [ "$(cat "$CXS"/acct-02/state_5.sqlite.private.[0-9]* 2>/dev/null)" = "second private index" ] \
5101
+ && [ -L "$CXS/acct-02/state_5.sqlite" ]; } \
5102
+ && t_ok "codex: retiring never overwrites an earlier retired copy" \
5103
+ || t_fail "codex retire unique" "$(ls "$CXS/acct-02" 2>&1 | tr '\n' ' ')"
5104
+ # (k) several processes promoting the same account at once. `mv` here could rename one
5105
+ # shim's fresh symlink onto the file another had just promoted, leaving
5106
+ # ~/.codex/state_N.sqlite pointing at itself — ELOOP, every account on the Mac unable
5107
+ # to start, and repaired by neither codex (ELOOP is not a corruption code) nor this
5108
+ # shim (`[ -L "$link" ]` would skip the name forever). link(2) refuses an existing
5109
+ # target, so the promote is atomic or it does not happen. The window is narrow, so
5110
+ # this drives the core function directly and repeats until the odds are not the test's.
5111
+ CORE="$WORK/state-index-core.sh"
5112
+ { printf '#!/usr/bin/env bash\nset -u\n'
5113
+ awk '/^state_index_names\(\) \{/,/^}$/' "$REPO_DIR/bin/codex"
5114
+ awk '/^shared_index_rejected\(\) \{/,/^}$/' "$REPO_DIR/bin/codex"
5115
+ awk '/^share_state_index_links\(\) \{/,/^}$/' "$REPO_DIR/bin/codex"
5116
+ printf 'share_state_index_links "$1" "$2" ""\n'; } > "$CORE"
5117
+ chmod +x "$CORE"
5118
+ # The losing interleave is: the winner promotes and links, THEN the loser acts on a
5119
+ # decision it made before either happened. Forcing it beats waiting for it — a `ln`/`mv`
5120
+ # that sleeps puts the loser inside that window on purpose, and the winner is injected
5121
+ # there. With the rename promote this renames the loser's own link onto its target; with
5122
+ # link(2) it simply refuses.
5123
+ SLOWBIN="$WORK/slowbin"
5124
+ mkdir -p "$SLOWBIN"
5125
+ for cmd in ln mv; do
5126
+ printf '#!/usr/bin/env bash\nsleep 1\nfor c in /bin/%s /usr/bin/%s; do [ -x "$c" ] && exec "$c" "$@"; done\nexit 127\n' \
5127
+ "$cmd" "$cmd" > "$SLOWBIN/$cmd"
5128
+ chmod +x "$SLOWBIN/$cmd"
5129
+ done
5130
+ rd="$WORK/promote-interleave"
5131
+ mkdir -p "$rd/home/sessions" "$rd/acct"
5132
+ ln -s "$rd/home/sessions" "$rd/acct/sessions"
5133
+ printf 'the loser index' > "$rd/acct/state_5.sqlite"
5134
+ ( PATH="$SLOWBIN:$PATH" "$CORE" "$rd/acct" "$rd/home" >/dev/null 2>&1 ) &
5135
+ slow=$!
5136
+ sleep 0.4 # the loser has decided the home is empty, and not yet acted
5137
+ printf 'the winner index' > "$rd/home/state_5.sqlite"
5138
+ rm -f "$rd/acct/state_5.sqlite"
5139
+ ln -s "$rd/home/state_5.sqlite" "$rd/acct/state_5.sqlite"
5140
+ wait "$slow" 2>/dev/null
5141
+ { [ -f "$rd/home/state_5.sqlite" ] && [ ! -L "$rd/home/state_5.sqlite" ] \
5142
+ && [ "$(cat "$rd/home/state_5.sqlite" 2>/dev/null)" = "the winner index" ]; } \
5143
+ && t_ok "codex: a promote that loses the race refuses instead of renaming onto the winner" \
5144
+ || t_fail "codex promote atomicity" "shared index is $(ls -la "$rd/home/state_5.sqlite" 2>&1 | head -c 160)"
5145
+ [ "$(cat "$rd/acct/state_5.sqlite" 2>/dev/null)" = "the winner index" ] \
5146
+ && t_ok "codex: the losing account still reads the shared index through its link" \
5147
+ || t_fail "codex promote atomicity" "acct reads $(cat "$rd/acct/state_5.sqlite" 2>&1 | head -c 120)"
5148
+
5149
+ races=0; bad=0; badwhy=""
5150
+
5151
+ # (l) the heal belongs to the LAUNCH, not to the pin: an unpinned selection and a retry
5152
+ # that rotates to another account must both link the account they actually serve.
5153
+ CXU="$WORK/cx-unpinned"; CXHU="$WORK/cxhome-unpinned"
5154
+ mkdir -p "$CXU/tmp" "$CXU/acct-01" "$CXU/acct-02" "$CXHU/.codex/sessions"
5155
+ : > "$CXU/.limits-kick"
5156
+ cat > "$CXU/accounts.json" <<EOF2
5157
+ {"version": 1, "server": "root@203.0.113.1", "server_root": "/root/.codex-accounts",
5158
+ "server_repo": "/root/claude-multiacc", "threshold": 90,
5159
+ "accounts": [
5160
+ {"id": "acct-01", "email": "u1@cx", "home": "mac", "added_at": "2026-09-09T00:00:00Z"},
5161
+ {"id": "acct-02", "email": "u2@cx", "home": "mac", "added_at": "2026-09-09T00:00:00Z"}]}
5162
+ EOF2
5163
+ for i in 01 02; do
5164
+ mk_cx_auth "$CXU/acct-$i/auth.json" "u$i@cx" "$FUTURE_EXP"
5165
+ ln -s "$CXHU/.codex/sessions" "$CXU/acct-$i/sessions"
5166
+ done
5167
+ printf 'the shared index' > "$CXHU/.codex/state_5.sqlite"
5168
+ cxlj 5 5 5 > "$CXU/acct-01/limits.json"
5169
+ cxlj 20 20 20 > "$CXU/acct-02/limits.json"
5170
+ out="$(HOME="$CXHU" CODEX_ACCOUNTS_DIR="$CXU" CODEX_MULTIACC_HEADROOM_BAND=0 codex 2>&1)"
5171
+ picked="$(printf '%s' "$out" | sed -n 's/.*CFG=\(acct-[0-9]*\).*/\1/p' | head -1)"
5172
+ { [ -n "$picked" ] \
5173
+ && [ "$(readlink "$CXU/$picked/state_5.sqlite" 2>/dev/null)" = "$CXHU/.codex/state_5.sqlite" ]; } \
5174
+ && t_ok "codex: an unpinned launch links the account it selected ($picked)" \
5175
+ || t_fail "codex unpinned heal" "picked=${picked:-none} $(ls -la "$CXU"/acct-0*/state_5.sqlite 2>&1 | head -c 300)"
5176
+ rm -f "$CXU"/acct-0*/state_5.sqlite
5177
+ echo "fail:acct-01" > "$FAKE_CTL2"
5178
+ out="$(HOME="$CXHU" CODEX_ACCOUNTS_DIR="$CXU" CODEX_MULTIACC_HEADROOM_BAND=0 codex exec "hello" < /dev/null 2>&1)"
5179
+ rm -f "$FAKE_CTL2" "$CXU/acct-01/.limited"
5180
+ case "$out" in *CFG=acct-02*) t_ok "codex: the retry rotated to the second account" ;;
5181
+ *) t_fail "codex retry heal" "did not land on acct-02: $(printf '%s' "$out" | head -c 200)" ;; esac
5182
+ [ "$(readlink "$CXU/acct-02/state_5.sqlite" 2>/dev/null)" = "$CXHU/.codex/state_5.sqlite" ] \
5183
+ && t_ok "codex: the account a retry rotates onto is linked too" \
5184
+ || t_fail "codex retry heal" "acct-02 -> $(readlink "$CXU/acct-02/state_5.sqlite" 2>/dev/null)"
5185
+
5186
+ # (h) the two copies of the core (shim + lib/common.sh) are byte-identical
5187
+ core_of() { # $1 file, $2 function name -> its body
5188
+ awk -v fn="$2" '$0 ~ "^"fn"\\(\\) \\{" {p=1} p {print} p && /^}$/ {exit}' "$1"
5189
+ }
5190
+ for fn in state_index_names shared_index_rejected share_state_index_links; do
5191
+ if [ "$(core_of "$REPO_DIR/bin/codex" "$fn")" = "$(core_of "$REPO_DIR/lib/common.sh" "$fn")" ] \
5192
+ && [ -n "$(core_of "$REPO_DIR/bin/codex" "$fn")" ]; then
5193
+ t_ok "codex: $fn is byte-identical in bin/codex and lib/common.sh"
5194
+ else
5195
+ t_fail "codex shared-index parity" "$fn differs between bin/codex and lib/common.sh"
5196
+ fi
5197
+ done
5198
+
4909
5199
  # ---- C12. login command + expired worklist + relogin --------------------------------
4910
5200
  # an account with no auth on this machine: login completes it
4911
5201
  codex-accounts import d@cx --id acct-04 --no-sync >/dev/null 2>&1
@@ -5635,8 +5925,11 @@ EOF
5635
5925
  grep -q "rt-rotated-new" "$CX/limits.log" \
5636
5926
  && t_fail "codex token leak" "a refresh token leaked into limits.log" \
5637
5927
  || t_ok "codex: limits.log leaks no tokens"
5638
- # recently-expired token is left alone (a live codex session may own it) — even --force
5639
- mk_cx_auth "$CX/acct-01/auth.json" a@cx "$((now - 100))"
5928
+ # recently-expired token is left alone (a live codex session may own it) — even --force.
5929
+ # The expiry is measured from NOW, not from the suite's start: the gate is a five-minute
5930
+ # window, and pinning it to $now silently makes this assertion depend on how long every
5931
+ # preceding test took.
5932
+ mk_cx_auth "$CX/acct-01/auth.json" a@cx "$(( $(date +%s) - 100 ))"
5640
5933
  cp "$CX/acct-01/auth.json" "$WORK/cx-recent.bak"
5641
5934
  rm -f "$CX/acct-01/limits.json" "$CX/acct-01/.oauth-refresh.json"
5642
5935
  out="$(CODEX_MULTIACC_TOKEN_URL="file://$WORK/cx-token-ok.json" CODEX_MULTIACC_USAGE_URL="file://$WORK/cx-usage-low.json" codex-accounts limits --force 2>&1)"