claude-multiacc 2.0.22 → 2.0.23

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
@@ -378,6 +378,8 @@ Codex-specific notes:
378
378
  and writes a per-account/window idempotency key before redemption so a lost response
379
379
  or simultaneous fleet poll cannot spend a second reset. Set
380
380
  `CODEX_MULTIACC_AUTO_RESET=0` only for emergency rollback/testing.
381
+ A finished-limit verdict takes precedence over a conflicting percentage. The
382
+ redemption key excludes unused model windows whose reset clocks move between reads.
381
383
  - **API-key logins are rejected** — ChatGPT subscription accounts only, matching the
382
384
  addon's no-API-keys rule.
383
385
 
@@ -16,6 +16,8 @@ import urllib.parse
16
16
  import urllib.request
17
17
  import uuid
18
18
 
19
+ from codex_reset_windows import reset_windows
20
+
19
21
  RESET_AT_USED_PERCENT = 95
20
22
  REDEEM_COOLDOWN_SECONDS = 900
21
23
  STATE_FILE = ".usage-reset.json"
@@ -100,21 +102,8 @@ def _pending_request(account_dir: str, now: int) -> tuple[dict, str]:
100
102
  def _redeem_request_id(account_id: str, usage: dict, headers: dict, now: int) -> str:
101
103
  """Return one fleet-stable UUID for this account's current limit windows."""
102
104
  subject = str(headers.get("chatgpt-account-id") or account_id)
103
- resets = []
104
- stack = [usage]
105
- while stack:
106
- node = stack.pop()
107
- if isinstance(node, dict):
108
- value = node.get("reset_at")
109
- if value is not None:
110
- try:
111
- resets.append(int(float(value)))
112
- except (TypeError, ValueError):
113
- pass
114
- stack.extend(node.values())
115
- elif isinstance(node, list):
116
- stack.extend(node)
117
- cycle = sorted(set(resets)) or [f"hour-{now // 3600}"]
105
+ _, resets = reset_windows(usage, RESET_AT_USED_PERCENT)
106
+ cycle = resets or [f"hour-{now // 3600}"]
118
107
  seed = json.dumps({"account": subject, "resets": cycle}, sort_keys=True)
119
108
  return str(uuid.uuid5(uuid.NAMESPACE_URL, seed))
120
109
 
@@ -124,7 +113,8 @@ def try_auto_redeem(account_dir: str, account_id: str, used_percent: int,
124
113
  now: int | None = None) -> dict:
125
114
  """Redeem one available reset and return a non-secret outcome document."""
126
115
  current = int(now if now is not None else time.time())
127
- if not auto_reset_enabled() or used_percent < RESET_AT_USED_PERCENT:
116
+ finished, _ = reset_windows(usage, RESET_AT_USED_PERCENT)
117
+ if not auto_reset_enabled() or (used_percent < RESET_AT_USED_PERCENT and not finished):
128
118
  return {"status": "not_eligible"}
129
119
  state, disposition = _pending_request(account_dir, current)
130
120
  if disposition == "cooldown":
@@ -0,0 +1,36 @@
1
+ """Redemption eligibility and identity use exhausted windows, never idle model clocks."""
2
+
3
+
4
+ def _number(value):
5
+ try:
6
+ return float(value)
7
+ except (TypeError, ValueError, OverflowError):
8
+ return 0
9
+
10
+
11
+ def reset_windows(usage: dict, threshold: int) -> tuple[bool, list[int]]:
12
+ finished, resets, stack = False, set(), [usage]
13
+ while stack:
14
+ node = stack.pop()
15
+ if isinstance(node, list):
16
+ stack.extend(node)
17
+ continue
18
+ if not isinstance(node, dict):
19
+ continue
20
+ hard_limit = node.get("limit_reached") is True or node.get("allowed") is False
21
+ finished = finished or hard_limit
22
+ windows = [node] if _number(node.get("used_percent")) >= threshold else []
23
+ if hard_limit:
24
+ reported = [node[key] for key in ("primary_window", "secondary_window")
25
+ if isinstance(node.get(key), dict)]
26
+ peak = max((_number(window.get("used_percent")) for window in reported), default=0)
27
+ windows.extend(window for window in reported if _number(window.get("used_percent")) == peak)
28
+ for window in windows:
29
+ try:
30
+ reset = int(float(window.get("reset_at")))
31
+ if reset > 0:
32
+ resets.add(reset)
33
+ except (TypeError, ValueError, OverflowError):
34
+ pass
35
+ stack.extend(node.values())
36
+ return finished, sorted(resets)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-multiacc",
3
- "version": "2.0.22",
3
+ "version": "2.0.23",
4
4
  "description": "Unified Claude Code and OpenAI Codex subscription pooling with quota-aware selection.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -6558,7 +6558,7 @@ unset FAKE_KEYCHAIN_DIR
6558
6558
 
6559
6559
  # The reset-credit contract is easier to prove against a stateful local HTTP server
6560
6560
  # than file:// fixtures: it pins thresholding, credit ordering and idempotent POST retry.
6561
- if CODEX_MULTIACC_AUTO_RESET=1 python3 "$REPO_DIR/tests/test_codex_reset.py"; then
6561
+ if CODEX_MULTIACC_AUTO_RESET=1 python3 -m unittest discover -s "$REPO_DIR/tests" -p 'test_codex_reset*.py'; then
6562
6562
  t_ok "codex: automatic earned-reset integration suite"
6563
6563
  else
6564
6564
  t_fail "codex automatic reset suite" "see unittest output above"
@@ -0,0 +1,43 @@
1
+ """Earned resets honor finished limits and ignore unrelated rolling windows."""
2
+ import time
3
+ import unittest
4
+
5
+ import test_codex_reset as fixtures
6
+
7
+
8
+ class ResetWindowsTest(fixtures.CodexPoolSandbox, unittest.TestCase):
9
+ run_limits = fixtures.CodexResetIntegrationTest.run_limits
10
+
11
+ def test_finished_limit_redeems_even_below_numeric_reset_threshold(self):
12
+ now = int(time.time())
13
+ self.server.state["usage"] = {"rate_limit": {"allowed": False, "limit_reached": True,
14
+ "primary_window": {"used_percent": 94, "limit_window_seconds": 604800, "reset_at": now + 86400}},
15
+ "rate_limit_reset_credits": {"available_count": 1}}
16
+ self.server.state["credits"] = {"available_count": 1, "credits": []}
17
+ self.server.state["responses"] = [(200, {"code": "reset", "windows_reset": 1})]
18
+ result = self.run_limits()
19
+ self.assertEqual(result.returncode, 0, result.stderr)
20
+ self.assertEqual(len(self.server.state["posts"]), 1)
21
+
22
+ def test_unused_model_windows_do_not_change_the_fleet_redemption_key(self):
23
+ now = int(time.time())
24
+ self.server.state["usage"] = {"rate_limit": {"allowed": True,
25
+ "primary_window": {"used_percent": 95, "limit_window_seconds": 604800, "reset_at": now + 86400}},
26
+ "additional_rate_limits": [{"limit_name": "Unused model", "rate_limit": {
27
+ "primary_window": {"used_percent": 0, "limit_window_seconds": 18000, "reset_at": now + 18000}}}],
28
+ "rate_limit_reset_credits": {"available_count": 2}}
29
+ self.server.state["credits"] = {"available_count": 2, "credits": []}
30
+ self.server.state["responses"] = [(200, {"code": "reset", "windows_reset": 1}),
31
+ (200, {"code": "already_redeemed", "windows_reset": 1})]
32
+ self.assertEqual(self.run_limits().returncode, 0)
33
+ # A fleet peer has no local receipt and sees a shifted idle-model window.
34
+ (self.pool / "acct-01/.usage-reset.json").unlink()
35
+ self.server.state["usage"]["additional_rate_limits"][0]["rate_limit"]["primary_window"]["reset_at"] += 30
36
+ self.assertEqual(self.run_limits().returncode, 0)
37
+ posts = self.server.state["posts"]
38
+ self.assertEqual(len(posts), 2)
39
+ self.assertEqual(posts[0]["redeem_request_id"], posts[1]["redeem_request_id"])
40
+
41
+
42
+ if __name__ == "__main__":
43
+ unittest.main()