claude-multiacc 2.0.45 → 2.0.46

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.
Files changed (29) hide show
  1. package/bin/codex +14 -1
  2. package/docs/AUTORESUME.md +6 -0
  3. package/lib/__pycache__/audit.cpython-312.pyc +0 -0
  4. package/lib/__pycache__/autoresume.cpython-312.pyc +0 -0
  5. package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
  6. package/lib/__pycache__/claude_token_identity.cpython-312.pyc +0 -0
  7. package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
  8. package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
  9. package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
  10. package/lib/__pycache__/mcp_registry.cpython-312.pyc +0 -0
  11. package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
  12. package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
  13. package/lib/__pycache__/shim_path.cpython-312.pyc +0 -0
  14. package/lib/autoresume.py +5 -2
  15. package/package.json +1 -1
  16. package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
  17. package/tests/__pycache__/test_autoresume.cpython-312.pyc +0 -0
  18. package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
  19. package/tests/__pycache__/test_claude_token_identity.cpython-312.pyc +0 -0
  20. package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
  21. package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
  22. package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
  23. package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
  24. package/tests/__pycache__/test_token_identity_ceremony.cpython-312.pyc +0 -0
  25. package/tests/__pycache__/test_token_identity_integration.cpython-312.pyc +0 -0
  26. package/tests/__pycache__/test_token_identity_retry.cpython-312.pyc +0 -0
  27. package/tests/__pycache__/test_token_identity_sync.cpython-312.pyc +0 -0
  28. package/tests/run-tests.sh +10 -0
  29. package/tests/test_autoresume.py +7 -1
package/bin/codex CHANGED
@@ -1363,8 +1363,21 @@ fi
1363
1363
  pick="$PICK_DIR"
1364
1364
  # A probe answers here, before anything that would count as a launch: no rotation
1365
1365
  # memory, no limits kick, no pick line in selection.log — and never an exec.
1366
+ # A limited tier also names when it lifts (resets_at=<epoch>, only when known): hard is
1367
+ # the soonest reset among the exhausted accounts, soft the picked account's own marker.
1368
+ # This is the machine twin of the tty-only notice above — callers read stdout, and a
1369
+ # service's stderr stays byte-clean.
1366
1370
  if [ "$AR_PROBE" = "1" ]; then
1367
- printf 'pick=%s tier=%s\n' "${pick##*/}" "$AR_TIER"
1371
+ ar_reset=0
1372
+ case "$AR_TIER" in
1373
+ hard) ar_reset="$best_reset" ;;
1374
+ soft) ar_reset="$(limited_reset_of "$pick")" ;;
1375
+ esac
1376
+ if [ "$ar_reset" -gt 0 ] 2>/dev/null; then
1377
+ printf 'pick=%s tier=%s resets_at=%s\n' "${pick##*/}" "$AR_TIER" "$ar_reset"
1378
+ else
1379
+ printf 'pick=%s tier=%s\n' "${pick##*/}" "$AR_TIER"
1380
+ fi
1368
1381
  exit 0
1369
1382
  fi
1370
1383
  # Remember the pick so the NEXT run does not hand back the same account. An explicit
@@ -293,6 +293,12 @@ CLAUDE_MULTIACC_AR_PROBE=1 CLAUDE_MULTIACC_AR_AVOID=acct-07:1790000000 claude
293
293
  - `soft`: only the all-limited fallback;
294
294
  - `hard`: only exhausted accounts are left;
295
295
  - `none`: nothing is usable; exit 3.
296
+ - **`resets_at`** (codex only): a `soft` or `hard` answer may end with
297
+ ` resets_at=<epoch>`, when that tier's reset is known. For `hard` it is the soonest
298
+ reset among the exhausted accounts. For `soft` it is the picked account's own reset.
299
+ An `eligible` answer never carries it. This is the machine-readable form of the
300
+ terminal-only "every usable account is at a limit" notice; stderr stays empty.
301
+ Parsers must accept trailing `key=value` fields.
296
302
  - **What the probe runs:** the same candidate loop as a launch, including marker upkeep
297
303
  and its log lines, stopping before exec.
298
304
  - **Environment:** run it with no `CLAUDE_CONFIG_DIR`, `CLAUDE_ACCOUNT` or
package/lib/autoresume.py CHANGED
@@ -1118,10 +1118,13 @@ def probe_env(environ, provider, avoid):
1118
1118
 
1119
1119
 
1120
1120
  def parse_probe(out):
1121
- """The probe's ``pick=<acct> tier=<eligible|soft|hard|none>`` -> (pick, tier)."""
1121
+ """The probe's ``pick=<acct> tier=<eligible|soft|hard|none>`` -> (pick, tier).
1122
+
1123
+ Trailing ``key=value`` fields (the codex shim's ``resets_at=<epoch>``) are accepted
1124
+ and ignored here."""
1122
1125
  pick, tier = "", ""
1123
1126
  for line in (out or "").split("\n"):
1124
- m = re.match(r"^pick=(\S*) tier=(\S+)\s*$", line.strip())
1127
+ m = re.match(r"^pick=(\S*) tier=(\S+)(?:\s+\w+=\S*)*\s*$", line.strip())
1125
1128
  if m:
1126
1129
  pick, tier = m.group(1), m.group(2)
1127
1130
  if pick and not ACCT_RE.match(pick):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-multiacc",
3
- "version": "2.0.45",
3
+ "version": "2.0.46",
4
4
  "description": "Unified Claude Code and OpenAI Codex subscription pooling with quota-aware selection.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -5695,6 +5695,10 @@ cxlj 99 10 99 > "$CX/acct-01/limits.json"
5695
5695
  cxlj 7 100 100 > "$CX/acct-02/limits.json"
5696
5696
  out="$(codex 2>&1)"
5697
5697
  check "codex: a still-serving limited account beats an exhausted one with more headroom" "CFG=acct-01" "$out"
5698
+ out="$(CODEX_MULTIACC_AR_PROBE=1 codex 2>&1)"; rc=$?
5699
+ [ "$rc:$out" = "0:pick=acct-01 tier=soft resets_at=$((now+3600))" ] \
5700
+ && t_ok "codex probe: a soft pick carries its own marker's resets_at" \
5701
+ || t_fail "codex probe soft resets_at" "rc=$rc out=$out"
5698
5702
  # The codex-review case (2026-09-04): the exhausted account is the only one INSIDE the
5699
5703
  # session gate; the gate must not resurrect it over a still-serving account outside it.
5700
5704
  printf '%s\nbucket=7d percent=100 marked_at=x reason=limits\n' "$((now+3600))" > "$CX/acct-01/.limited"
@@ -5713,6 +5717,12 @@ printf '%s\nbucket=5h percent=100 marked_at=x reason=limits\n' "$((now+7200))" >
5713
5717
  printf '%s\nbucket=5h percent=100 marked_at=x reason=limits\n' "$((now+600))" > "$CX/acct-02/.limited"
5714
5718
  out="$(codex 2>&1)"
5715
5719
  check "codex: all exhausted: the soonest reset is handed out" "CFG=acct-02" "$out"
5720
+ # The probe is the machine channel for the tty-only notice: it names the soonest reset,
5721
+ # and its stderr stays empty.
5722
+ out="$(CODEX_MULTIACC_AR_PROBE=1 codex 2>"$WORK/cx-probe.err")"; rc=$?
5723
+ [ "$rc:$out" = "0:pick=acct-02 tier=hard resets_at=$((now+600))" ] && [ ! -s "$WORK/cx-probe.err" ] \
5724
+ && t_ok "codex probe: all exhausted answers tier=hard resets_at=<soonest reset>, stderr clean" \
5725
+ || t_fail "codex probe hard resets_at" "rc=$rc out=$out err=$(cat "$WORK/cx-probe.err")"
5716
5726
  grep -q "all-exhausted resets_in=" "$CX/selection.log" \
5717
5727
  && t_ok "codex: the all-exhausted pick is logged with its reset" || t_fail "codex all-exhausted log" "no line"
5718
5728
  rm -f "$CX"/acct-*/.limited "$CX"/acct-*/limits.json
@@ -697,7 +697,13 @@ class StateTests(unittest.TestCase):
697
697
  self.assertEqual(ar.parse_probe("noise\npick=acct-02 tier=eligible\n"),
698
698
  ("acct-02", "eligible"))
699
699
  self.assertEqual(ar.parse_probe("pick= tier=none\n"), ("", "none"))
700
- for bad in ("", "pick=acct-02", "pick=../x tier=eligible", "pick=acct-02 tier=great"):
700
+ # The codex shim appends resets_at=<epoch> to a limited tier; old lines still parse.
701
+ self.assertEqual(ar.parse_probe("pick=acct-03 tier=hard resets_at=1790000000\n"),
702
+ ("acct-03", "hard"))
703
+ self.assertEqual(ar.parse_probe("pick=acct-03 tier=soft resets_at=1790000000"),
704
+ ("acct-03", "soft"))
705
+ for bad in ("", "pick=acct-02", "pick=../x tier=eligible", "pick=acct-02 tier=great",
706
+ "pick=acct-02 tier=hard resets_at", "pick=acct-02 tier=hard x y"):
701
707
  self.assertEqual(ar.parse_probe(bad), ("", ""), bad)
702
708
  self.assertTrue(ar.probe_allows("quota", "acct-02", "eligible", "acct-01"))
703
709
  self.assertFalse(ar.probe_allows("quota", "acct-01", "eligible", "acct-01"))