claude-multiacc 2.0.19 → 2.0.21
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/CLAUDE_ACCS_TASK.md +10 -5
- package/README.md +47 -19
- package/bin/claude +225 -64
- package/bin/claude-accounts +34 -9
- package/bin/codex +202 -41
- package/bin/codex-accounts +4 -1
- package/docs/ACCOUNT_OPERATIONS.md +9 -4
- package/docs/TRACK_PROMPT.md +76 -0
- package/docs/UNIFIED_SELECTOR.md +40 -7
- 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/lib/selector_policy.py +42 -7
- package/lib/selector_primitives.py +25 -16
- package/package.json +1 -1
- package/tests/run-tests.sh +412 -41
- package/tests/test_selector.py +104 -2
package/lib/selector_policy.py
CHANGED
|
@@ -7,6 +7,7 @@ from decimal import Decimal, localcontext
|
|
|
7
7
|
|
|
8
8
|
from selector_primitives import (
|
|
9
9
|
DEFAULT_HEADROOM_BAND,
|
|
10
|
+
DEFAULT_SESSION_GATE,
|
|
10
11
|
SCHEMA,
|
|
11
12
|
SELECTOR_VERSION,
|
|
12
13
|
canonical_sha256,
|
|
@@ -126,17 +127,39 @@ def _winner_key(row: dict) -> tuple:
|
|
|
126
127
|
row["engine"], row["account_id"], row["runner_id"], row["runner_generation"])
|
|
127
128
|
|
|
128
129
|
|
|
130
|
+
def _session_gate(rows: list[dict], gate: Decimal | None) -> list[dict]:
|
|
131
|
+
"""The FIRST cut: accounts whose 5h session bucket still has room.
|
|
132
|
+
|
|
133
|
+
The operator (2026-09-03): "among accounts where high session limits it must
|
|
134
|
+
choose randomly from ones where highest weekly limits." A nearly-spent session
|
|
135
|
+
bucket is about to reject the launch whatever the weekly headroom says, so it
|
|
136
|
+
disqualifies the account outright instead of merely nudging a tie-break. The
|
|
137
|
+
gate COMPARES, it never empties the pool: when nobody clears it the caller
|
|
138
|
+
falls back to every known row. Unknown quota never clears it — a missing
|
|
139
|
+
reading is not evidence of room.
|
|
140
|
+
"""
|
|
141
|
+
if gate is None:
|
|
142
|
+
return []
|
|
143
|
+
return [row for row in rows if Decimal(row["session_pct"]) <= gate]
|
|
144
|
+
|
|
145
|
+
|
|
129
146
|
def _band_floor(rows: list[dict], band: Decimal) -> Decimal | None:
|
|
130
|
-
"""The lowest
|
|
147
|
+
"""The lowest WEEKLY remaining still counted as "as good as the best" this round.
|
|
131
148
|
|
|
132
149
|
Ranking strictly by headroom hands every task to whichever account is on top,
|
|
133
150
|
which is precisely how one account's limit gets burned to zero while three
|
|
134
151
|
others idle: the runner-up only ever wins after the leader has been spent
|
|
135
152
|
below it. Accounts within ``band`` points of the leader are treated as equally
|
|
136
153
|
good, and the tie-break below spreads work across them.
|
|
154
|
+
|
|
155
|
+
The band is measured on weekly remaining, not on min(weekly, session): session
|
|
156
|
+
headroom already had its say in ``_session_gate`` above, and letting it back in
|
|
157
|
+
here both pushed the best weekly account out of the band over a half-spent 5h
|
|
158
|
+
bucket and let a fully spent one stay in. Rows reaching here are the gated set,
|
|
159
|
+
so every one of them has a known reading.
|
|
137
160
|
"""
|
|
138
|
-
known = [Decimal(row["
|
|
139
|
-
if row["quota_known"] and row["
|
|
161
|
+
known = [Decimal(row["weekly_remaining"]) for row in rows
|
|
162
|
+
if row["quota_known"] and row["weekly_remaining"] is not None]
|
|
140
163
|
return max(known) - band if known else None
|
|
141
164
|
|
|
142
165
|
|
|
@@ -197,15 +220,21 @@ def select(request: object) -> dict:
|
|
|
197
220
|
if not eligible:
|
|
198
221
|
return _error("no_candidate", {"eligible_count": 0})
|
|
199
222
|
band, band_text = decimal_value(request.get("headroom_band", DEFAULT_HEADROOM_BAND))
|
|
200
|
-
|
|
223
|
+
gate, gate_text = decimal_value(request.get("session_gate", DEFAULT_SESSION_GATE))
|
|
224
|
+
# Two cuts, in this order (operator's 2026-09-03 decision): the session gate says
|
|
225
|
+
# WHO may be considered, the weekly band says which of those count as equally
|
|
226
|
+
# good. "healthy or known" is the gate stepping aside when nobody clears it.
|
|
227
|
+
known = [row for row in eligible if row["quota_known"]]
|
|
228
|
+
healthy = _session_gate(known, gate)
|
|
229
|
+
ranked = healthy or known
|
|
230
|
+
floor = _band_floor(ranked, band) if band is not None else None
|
|
201
231
|
if floor is None:
|
|
202
232
|
# No usable quota anywhere: nothing to band, so keep the plain ordering
|
|
203
233
|
# (which already ranks unknown-quota rows last and rotates on ties).
|
|
204
234
|
banded = eligible
|
|
205
235
|
winner = min(banded, key=_winner_key)
|
|
206
236
|
else:
|
|
207
|
-
banded = [row for row in
|
|
208
|
-
and Decimal(row["effective_headroom"]) >= floor]
|
|
237
|
+
banded = [row for row in ranked if Decimal(row["weekly_remaining"]) >= floor]
|
|
209
238
|
winner = min(banded, key=_band_key(request["reservation_key"]))
|
|
210
239
|
rows.sort(key=_snapshot_key)
|
|
211
240
|
snapshot = canonical_sha256(rows)
|
|
@@ -214,8 +243,10 @@ def select(request: object) -> dict:
|
|
|
214
243
|
"schema": SCHEMA, "selector_version": SELECTOR_VERSION,
|
|
215
244
|
"database_now": canonical_now, "policy": request["policy"],
|
|
216
245
|
"required_engine": request.get("required_engine"), "reservation_key": request["reservation_key"],
|
|
217
|
-
# The band
|
|
246
|
+
# The band and the gate both change which account wins, so both belong in
|
|
247
|
+
# the proof. (This is why the golden digests moved on 2026-09-03.)
|
|
218
248
|
"headroom_band": band_text,
|
|
249
|
+
"session_gate": gate_text,
|
|
219
250
|
"candidate_snapshot_digest": snapshot, "selected_identity": chosen}
|
|
220
251
|
score_fields = ("quota_known", "weekly_pct", "session_pct", "weekly_remaining",
|
|
221
252
|
"session_remaining", "effective_headroom", "last_selected_at")
|
|
@@ -228,6 +259,10 @@ def select(request: object) -> dict:
|
|
|
228
259
|
"headroom_band": band_text,
|
|
229
260
|
"band_floor": format(floor, "f") if floor is not None else None,
|
|
230
261
|
"band_count": len(banded),
|
|
262
|
+
# ...and how many accounts the session gate let through at all
|
|
263
|
+
# (0 means nobody cleared it and the gate stepped aside).
|
|
264
|
+
"session_gate": gate_text,
|
|
265
|
+
"session_ok_count": len(healthy),
|
|
231
266
|
"candidate_snapshot_digest": snapshot,
|
|
232
267
|
"selection_digest": canonical_sha256(digest_input)}
|
|
233
268
|
if request["policy"] == "reviewer" and not alternatives and _row_identity(winner) == producer:
|
|
@@ -11,13 +11,15 @@ from datetime import datetime, timezone
|
|
|
11
11
|
from decimal import Decimal, InvalidOperation
|
|
12
12
|
|
|
13
13
|
SCHEMA = "claude-multiacc/pool-selection.v2"
|
|
14
|
-
# NOT bumped for the headroom band
|
|
15
|
-
# a
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
14
|
+
# NOT bumped for the headroom band, and NOT bumped for the session gate either. This
|
|
15
|
+
# string is a BUILD FENCE, not a changelog: a queued task records the version that
|
|
16
|
+
# reserved its account, and the claim function (agent-sdk
|
|
17
|
+
# 0005_delivery_claim_functions.sql) only lets a runner advertising the same string
|
|
18
|
+
# claim it — while the runner's value is a hardcoded constant shipped in its bundle.
|
|
19
|
+
# With Macs routinely several builds behind, bumping this stops every lagging Mac from
|
|
20
|
+
# claiming any new work until it updates. Both knobs are additive and optional: the
|
|
21
|
+
# request only grows optional keys, the response only grows fields, and every existing
|
|
22
|
+
# consumer keeps reading exactly what it read before.
|
|
21
23
|
SELECTOR_VERSION = "2.0.1"
|
|
22
24
|
TIME_RE = re.compile(
|
|
23
25
|
r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-5][0-9]"
|
|
@@ -33,9 +35,13 @@ REQUEST_KEYS = {
|
|
|
33
35
|
"schema", "database_now", "policy", "required_engine", "producer_identity",
|
|
34
36
|
"excluded_identities", "candidates", "reservation_key"}
|
|
35
37
|
# Optional because a caller pinned to an older panel build still sends exactly
|
|
36
|
-
# REQUEST_KEYS; absent, the policy applies its own
|
|
37
|
-
|
|
38
|
+
# REQUEST_KEYS; absent, the policy applies its own defaults. app-robot's panel sends
|
|
39
|
+
# headroom_band from its own setting and never sends session_gate, so the gate default
|
|
40
|
+
# below is what every panel launch gets (operator's 2026-09-03 decision: session
|
|
41
|
+
# headroom gates FIRST, then the weekly band picks among the survivors).
|
|
42
|
+
OPTIONAL_REQUEST_KEYS = {"headroom_band", "session_gate"}
|
|
38
43
|
DEFAULT_HEADROOM_BAND = "30"
|
|
44
|
+
DEFAULT_SESSION_GATE = "50"
|
|
39
45
|
POLICIES = {
|
|
40
46
|
"default_claude", "default_codex", "default_both", "explicit", "producer_retry", "reviewer"}
|
|
41
47
|
|
|
@@ -180,13 +186,16 @@ def request_error(request: object) -> tuple[str, dict] | None:
|
|
|
180
186
|
keys = set(request)
|
|
181
187
|
if not REQUEST_KEYS <= keys or keys - REQUEST_KEYS - OPTIONAL_REQUEST_KEYS:
|
|
182
188
|
return "invalid_request", {"field": "$"}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
#
|
|
188
|
-
|
|
189
|
-
|
|
189
|
+
for knob in ("headroom_band", "session_gate"):
|
|
190
|
+
if knob not in request:
|
|
191
|
+
continue
|
|
192
|
+
value, _text = decimal_value(request[knob])
|
|
193
|
+
# decimal_value clamps to [0,100] and rejects non-numeric shapes; a knob the
|
|
194
|
+
# caller cannot express exactly must fail loudly rather than silently widen
|
|
195
|
+
# selection to the whole pool (band) or wave every session-heavy account
|
|
196
|
+
# through (gate). Both are validated identically so the panel can send either.
|
|
197
|
+
if value is None or isinstance(request[knob], bool):
|
|
198
|
+
return "invalid_request", {"field": knob}
|
|
190
199
|
try:
|
|
191
200
|
timestamp(request["database_now"])
|
|
192
201
|
except ValueError:
|