claude-multiacc 2.0.18 → 2.0.19
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/bin/codex-accounts +11 -2
- package/lib/__pycache__/audit.cpython-312.pyc +0 -0
- package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
- package/package.json +1 -1
- package/tests/test_codex_reset.py +66 -1
package/bin/codex-accounts
CHANGED
|
@@ -1492,8 +1492,17 @@ for acct in manifest.get('accounts', []):
|
|
|
1492
1492
|
b for b in buckets if b['percent'] >= threshold]
|
|
1493
1493
|
mpath = os.path.join(d, '.limited')
|
|
1494
1494
|
if offenders:
|
|
1495
|
-
|
|
1496
|
-
|
|
1495
|
+
# ONE bucket, described consistently: the marker's epoch is the reset of
|
|
1496
|
+
# the bucket its detail line names. It used to pair the highest PERCENT
|
|
1497
|
+
# with the LATEST reset over every offender, and the two came from
|
|
1498
|
+
# different buckets — a 100% five-hour window resetting tonight written
|
|
1499
|
+
# with a seven-day epoch — so everything that trusts the marker parked
|
|
1500
|
+
# the account for days over a window that refills in hours. #20 fixed
|
|
1501
|
+
# this for the claude pool and left the codex writer behind; app-robot
|
|
1502
|
+
# reads both with the same rule. Among offenders the longest-lived wins,
|
|
1503
|
+
# since any bucket over the threshold stays there until its own reset.
|
|
1504
|
+
worst = max(offenders, key=lambda b: (int(b['resets_epoch']), b['percent']))
|
|
1505
|
+
reset_epoch = int(worst['resets_epoch'])
|
|
1497
1506
|
# Atomic: a concurrent shim must never read a half-written marker.
|
|
1498
1507
|
with open(mpath + '.tmp', 'w') as f:
|
|
1499
1508
|
f.write(f'{reset_epoch}\n')
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -61,7 +61,9 @@ class ResetHandler(BaseHTTPRequestHandler):
|
|
|
61
61
|
return
|
|
62
62
|
|
|
63
63
|
|
|
64
|
-
class
|
|
64
|
+
class CodexPoolSandbox:
|
|
65
|
+
"""A one-account codex pool with a fake usage/credits endpoint."""
|
|
66
|
+
|
|
65
67
|
def setUp(self) -> None:
|
|
66
68
|
self.temp = tempfile.TemporaryDirectory(prefix="multiacc-reset-")
|
|
67
69
|
self.pool = Path(self.temp.name) / "pool"
|
|
@@ -88,6 +90,8 @@ class CodexResetIntegrationTest(unittest.TestCase):
|
|
|
88
90
|
self.thread.join(timeout=5)
|
|
89
91
|
self.temp.cleanup()
|
|
90
92
|
|
|
93
|
+
|
|
94
|
+
class CodexResetIntegrationTest(CodexPoolSandbox, unittest.TestCase):
|
|
91
95
|
def run_limits(self) -> subprocess.CompletedProcess:
|
|
92
96
|
base = f"http://127.0.0.1:{self.server.server_port}/wham"
|
|
93
97
|
env = os.environ.copy()
|
|
@@ -166,5 +170,66 @@ class CodexResetIntegrationTest(unittest.TestCase):
|
|
|
166
170
|
self.assertEqual(self.server.state["credit_gets"], 1)
|
|
167
171
|
|
|
168
172
|
|
|
173
|
+
class CodexMarkerNamesOneBucketTest(CodexPoolSandbox, unittest.TestCase):
|
|
174
|
+
"""The marker's epoch is the reset of the bucket its detail line NAMES.
|
|
175
|
+
|
|
176
|
+
The codex writer used to pair the highest-PERCENT bucket's name with the
|
|
177
|
+
LATEST reset over every offender, so a five-hour window at 100% resetting
|
|
178
|
+
tonight was written under a seven-day epoch — and everything that trusts the
|
|
179
|
+
marker parked the account for a week over a window that refills in hours.
|
|
180
|
+
#20 fixed this for the claude pool and left codex behind.
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
def run_limits(self) -> subprocess.CompletedProcess: # no auto-reset: it clears markers
|
|
184
|
+
base = f"http://127.0.0.1:{self.server.server_port}/wham"
|
|
185
|
+
env = os.environ.copy()
|
|
186
|
+
env.update({"CODEX_ACCOUNTS_ROOT": str(self.pool),
|
|
187
|
+
"CODEX_MULTIACC_NO_SYNC": "1", "CODEX_MULTIACC_MIN_FETCH": "0",
|
|
188
|
+
"CODEX_MULTIACC_AUTO_RESET": "0", "PYTHONDONTWRITEBYTECODE": "1",
|
|
189
|
+
"CODEX_MULTIACC_USAGE_URL": f"{base}/usage"})
|
|
190
|
+
return subprocess.run([REPO / "bin/codex-accounts", "limits", "--force"],
|
|
191
|
+
capture_output=True, text=True, env=env, timeout=20, check=False)
|
|
192
|
+
|
|
193
|
+
def _marker(self):
|
|
194
|
+
text = (self.pool / "acct-01/.limited").read_text(encoding="utf-8").splitlines()
|
|
195
|
+
return int(text[0]), text[1]
|
|
196
|
+
|
|
197
|
+
def test_two_offenders_write_one_bucket_with_its_own_reset(self) -> None:
|
|
198
|
+
now = int(time.time())
|
|
199
|
+
session_reset, weekly_reset = now + 3600, now + 6 * 86400
|
|
200
|
+
self.server.state["usage"] = {"plan_type": "pro", "rate_limit": {
|
|
201
|
+
"allowed": True,
|
|
202
|
+
# 100% and back in an hour …
|
|
203
|
+
"primary_window": {"used_percent": 100, "limit_window_seconds": 18000,
|
|
204
|
+
"reset_at": session_reset},
|
|
205
|
+
# … beside 95% that is six days out. Both are over the threshold.
|
|
206
|
+
"secondary_window": {"used_percent": 95, "limit_window_seconds": 604800,
|
|
207
|
+
"reset_at": weekly_reset}}}
|
|
208
|
+
self.assertEqual(self.run_limits().returncode, 0)
|
|
209
|
+
epoch, detail = self._marker()
|
|
210
|
+
# The longest-lived offender is named, and the epoch is ITS reset — the
|
|
211
|
+
# account really is excluded until then, and a reader that takes the
|
|
212
|
+
# detail's percent gets the percent of the very bucket the epoch belongs
|
|
213
|
+
# to rather than a different bucket's.
|
|
214
|
+
self.assertEqual(epoch, weekly_reset)
|
|
215
|
+
self.assertIn("percent=95", detail)
|
|
216
|
+
self.assertIn(time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(weekly_reset)), detail)
|
|
217
|
+
self.assertNotIn("percent=100", detail)
|
|
218
|
+
|
|
219
|
+
def test_a_single_offender_still_names_itself(self) -> None:
|
|
220
|
+
now = int(time.time())
|
|
221
|
+
reset = now + 4 * 86400
|
|
222
|
+
self.server.state["usage"] = {"plan_type": "pro", "rate_limit": {
|
|
223
|
+
"allowed": True,
|
|
224
|
+
"primary_window": {"used_percent": 91, "limit_window_seconds": 604800,
|
|
225
|
+
"reset_at": reset},
|
|
226
|
+
"secondary_window": {"used_percent": 12, "limit_window_seconds": 18000,
|
|
227
|
+
"reset_at": now + 900}}}
|
|
228
|
+
self.assertEqual(self.run_limits().returncode, 0)
|
|
229
|
+
epoch, detail = self._marker()
|
|
230
|
+
self.assertEqual(epoch, reset)
|
|
231
|
+
self.assertIn("percent=91", detail)
|
|
232
|
+
|
|
233
|
+
|
|
169
234
|
if __name__ == "__main__":
|
|
170
235
|
unittest.main()
|