claude-multiacc 1.0.14 → 1.0.16

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/lib/report.py CHANGED
@@ -41,6 +41,25 @@ import sys
41
41
  import time
42
42
 
43
43
  SCHEMA = 'claude-multiacc/pool.v1'
44
+
45
+ # Must match the SHIM's ranking window, or this document calls data "fresh" that
46
+ # selection has already stopped ranking on. The two providers differ on purpose:
47
+ # api.anthropic.com hands out Retry-After: 3600, so bin/claude trusts an hour;
48
+ # bin/codex still uses 900 against chatgpt.com, which has not been measured.
49
+ _STALE_DEFAULT = {'claude': 3600, 'codex': 900}
50
+
51
+
52
+ def _stale_after(provider):
53
+ env = {'claude': 'CLAUDE_MULTIACC_STALE_AFTER',
54
+ 'codex': 'CODEX_MULTIACC_STALE_AFTER'}.get(provider)
55
+ default = _STALE_DEFAULT.get(provider, 900)
56
+ try:
57
+ v = int(os.environ.get(env) or default) if env else default
58
+ except ValueError:
59
+ v = default
60
+ return v if v > 0 else default
61
+
62
+
44
63
  VALID_ID = re.compile(r'acct-\d{2}')
45
64
 
46
65
 
@@ -181,7 +200,7 @@ def _codex_credentials(d, jwt_claims):
181
200
  return 'machine-local', detail
182
201
 
183
202
 
184
- def _usage(d):
203
+ def _usage(d, stale_after):
185
204
  lim = _load_json(os.path.join(d, 'limits.json'))
186
205
  if not lim:
187
206
  return None
@@ -200,6 +219,19 @@ def _usage(d):
200
219
  out['age_seconds'] = max(0, int(time.time() - float(fetched)))
201
220
  except (TypeError, ValueError):
202
221
  out['age_seconds'] = None
222
+ # Past this window the shim stops ranking on these numbers entirely (bin/claude
223
+ # STALE_AFTER). A panel that shows "weekly 2%" without it is reporting a number
224
+ # nothing is using — which is how an 11-day telemetry outage stayed invisible.
225
+ out['stale'] = out['age_seconds'] is None or out['age_seconds'] > stale_after
226
+ # The horizon a stale weekly reading stays true until (bin/claude stale_weekly).
227
+ # Absent on documents written before it existed, which is itself the signal that
228
+ # the reading cannot be used for degraded ranking.
229
+ if lim.get('weekly_resets_epoch'):
230
+ out['weekly_resets_epoch'] = lim['weekly_resets_epoch']
231
+ if lim.get('last_error'):
232
+ out['last_error'] = lim['last_error']
233
+ if lim.get('last_error_at'):
234
+ out['last_error_at'] = _iso(lim['last_error_at'])
203
235
  if lim.get('plan'):
204
236
  out['plan'] = lim['plan']
205
237
  if lim.get('retry_after'):
@@ -221,6 +253,44 @@ def _last_pick(root, aid):
221
253
  return last
222
254
 
223
255
 
256
+
257
+ def telemetry_state(accounts, now=None):
258
+ """Three-state verdict on how the shim is CURRENTLY ranking this pool, over the
259
+ same candidate set bin/claude uses: the eligible accounts, or — when every one of
260
+ them is limit-marked — the valid ones it falls back to.
261
+
262
+ 'fresh' at least one candidate has telemetry inside the ranking window
263
+ 'degraded' none do, but EVERY candidate still has a weekly reading whose bucket
264
+ has not reset yet, so the shim ranks on those (all-or-nothing: one
265
+ unusable reading and the comparison is not apples-to-apples)
266
+ 'blind' nothing usable anywhere — every account scores the same and selection
267
+ is effectively a coin flip
268
+ 'none' no candidates at all; nothing to say
269
+
270
+ Kept here rather than beside either caller because `status` and `--json` disagreeing
271
+ with the shim is exactly how an eleven-day outage stayed invisible.
272
+ """
273
+ now = time.time() if now is None else now
274
+ usable = [a for a in accounts if a.get('status') in ('active', 'limited')]
275
+ cands = [a for a in usable if a.get('status') == 'active'] or usable
276
+ if not cands:
277
+ return 'none'
278
+ if any(a.get('usage') and not a['usage'].get('stale') for a in cands):
279
+ return 'fresh'
280
+ for a in cands:
281
+ u = a.get('usage') or {}
282
+ horizon = u.get('weekly_resets_epoch') or 0
283
+ pct = u.get('weekly_percent')
284
+ # bool is an int in Python but not a number to the shim's digit parser, so a
285
+ # document carrying `weekly_percent: true` must read as unusable HERE too —
286
+ # otherwise this says 'degraded' while the shim ranks blind.
287
+ if isinstance(horizon, bool) or isinstance(pct, bool) \
288
+ or not isinstance(horizon, (int, float)) or horizon <= now \
289
+ or not isinstance(pct, (int, float)):
290
+ return 'blind'
291
+ return 'degraded'
292
+
293
+
224
294
  def _account_row(root, a, aid, d, provider, jwt_claims, audit_account, now, kind, machine):
225
295
  """One account's row. Everything it reads is already tolerant; build() catches
226
296
  anything unexpected on top of that and degrades this account alone."""
@@ -256,7 +326,7 @@ def _account_row(root, a, aid, d, provider, jwt_claims, audit_account, now, kind
256
326
  'limit_reset_at': _iso(reset_epoch) if limited and reset_epoch else None,
257
327
  'limit_reset_epoch': reset_epoch if limited and reset_epoch else None,
258
328
  'limit_detail': marker_detail if limited else None,
259
- 'usage': _usage(d),
329
+ 'usage': _usage(d, _stale_after(provider)),
260
330
  }
261
331
  if kind == 'status':
262
332
  row['last_picked'] = _last_pick(root, aid)
@@ -340,6 +410,15 @@ def build(root, provider, machine, kind='list'):
340
410
  'needs_login': sum(1 for a in accounts if a['needs_login']),
341
411
  'portable': sum(1 for a in accounts if a['portable']),
342
412
  'selectable': sum(1 for a in accounts if a['selectable']),
413
+ # The pool-wide verdict, so a dashboard can show the outage rather than a
414
+ # grid of reassuring percentages nothing is ranking on.
415
+ # fresh ranking on current data
416
+ # degraded ranking on old-but-still-true weekly readings
417
+ # blind every account scores the same; selection is a coin flip
418
+ 'telemetry': telemetry_state(accounts, now),
419
+ # Kept as its own boolean because a dashboard alerting on one field should
420
+ # not have to know the vocabulary. True ONLY for the coin-flip case.
421
+ 'ranking_blind': telemetry_state(accounts, now) == 'blind',
343
422
  },
344
423
  'warnings': warnings,
345
424
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-multiacc",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "description": "Multi-account addon for Claude Code and OpenAI Codex CLI: every claude / claude -p and every codex / codex exec runs under a randomly-picked subscription account with the most usage headroom. Mirrors to a deploy server. No API keys.",
5
5
  "type": "module",
6
6
  "bin": {