feat(seats): accounts without a 5-hour limit
Anthropic is removing the 5-hour limit in stages (from 2026-10-31), and an account without it reports the weekly window alone. Until now the pool took such an account for an unprobed seat: always eligible, ranked after every other, never held to the weekly cutoff, with no cap on its passes. - The burst window is the 5-hour one, else the weekly one (Seats::BurstWindow). An account whose burst window is the weekly one is ranked by it and admitted under the 95 % cutoff with the passes in flight projected on it, at the measured share per pass (0.04 at xhigh, 0.125 otherwise; REVIEW_GATES_LENS_COST_7D_PCT), and waits for the weekly reset when over it. - Every OAuth account and the local login takes at most 128 passes at once, every driver's counted (REVIEW_GATES_OAUTH_CONCURRENCY): every run measured with 149 or more in flight on one account drew usage-limit rejections, none of the 119 with 139 or fewer. - A 'concurrents' rejection pauses the seat for 20 s or its retry-after, not until a window's reset (a seat sat two hours at 10 % on one). - Board entries, the folded foreign record and the board's view carry cost7d, so the drivers see each other's weekly load. - `seats` and the log table say `no limit` in such an account's 5-hour column; the board's others-line names its weekly projection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a2974a8139
commit
c28307601b
11 changed files with 209 additions and 26 deletions
|
|
@ -107,6 +107,12 @@ Conventions:
|
|||
|
||||
No file the drivers write holds these in practice, and a file that fails to parse is treated as a damaged one.
|
||||
- **HTTP.** `FetchHTTP1` ignores `*_proxy` variables. The OpenRouter requests send `User-Agent: crafter-review`; `app_ready` sends `review-gates`. Reason phrases are the standard ones, not the server's.
|
||||
- **Seat placement** (SeatPool, Seats, Drivers):
|
||||
- An account whose 5-hour limit Anthropic has removed reports the weekly window alone. Its burst window (`Seats::BurstWindow`) is the weekly one: it is ranked and admitted by the weekly window with the passes in flight projected on it (0.04 points per pass at xhigh, 0.125 otherwise), under the 95 % cutoff.
|
||||
- Every OAuth account and the local login takes at most 128 passes at once, every driver's counted.
|
||||
- A 'concurrents' rejection pauses the seat for 20 s or its retry-after, not until a window's reset.
|
||||
- Board entries and the folded `foreign` record carry `cost7d`, and so does the dump's seat summary.
|
||||
- The `seats` report and the log table say `no limit` in such an account's 5-hour column.
|
||||
- **Board identity and liveness, process lifetime, stop:** see "Inside the Flatpak".
|
||||
- **Edge cases:**
|
||||
- Usage errors of `run`, `freeze`, `slots`, `config` and the other commands exit 3, the preflight's code, and so does an INVARIANT failure of `run` and `slots`. The helpers exit 64.
|
||||
|
|
@ -174,6 +180,8 @@ What the binary reads at run time, and how. A variable the driver only sets or s
|
|||
| `REVIEW_GATES_SEAT_POLICY` | `:Seats` ConfigFromEnv | `spread` | Exactly `pack` (case-sensitive, untrimmed) is pack; anything else spread. | pack: the first eligible OAuth seat by id; spread: the lowest projected one |
|
||||
| `REVIEW_GATES_SEAT_WAIT_MIN` | `:Seats` ConfigFromEnv | `15` | As MAX_UTIL | Shown by `seats --json` as `waitMaxMin`; nothing else reads it |
|
||||
| `REVIEW_GATES_SEAT_HANDOVER_PCT` | `:Seats` ConfigFromEnv | `90` | As MAX_UTIL | Shown in the `seats` report and `--json` only |
|
||||
| `REVIEW_GATES_OAUTH_CONCURRENCY` | `:SeatPool` PoolConfig | `128` | As MAX_UTIL, truncated toward zero. | Passes at once on one OAuth account or the local login, every driver's counted |
|
||||
| `REVIEW_GATES_LENS_COST_7D_PCT` | `:SeatPool` PoolConfig | `0.04` at xhigh, `0.125` otherwise | As MAX_UTIL. | Percentage points of the weekly window a running pass is projected to add: the placement of an account without a 5-hour limit, and `cost7d` on the board |
|
||||
| `REVIEW_GATES_SEAT_ORDER` | `:SeatPool` ApiConfig | `api-first` | Exactly `oauth-first` is oauth-first; anything else api-first. | Whether the paid seats (API, OpenRouter) or the OAuth/local ones rank first |
|
||||
| `REVIEW_GATES_API_BUDGET_USD` | `:SeatPool` ApiConfig (run, seats) | `200.0` | MAX_UTIL's syntax, kept if >= 0 (`inf` means no cap); empty, garbage, negative, `nan` give the default. `--api-budget` overrides it in run. | USD one API key may spend per cycle; at it the key is out |
|
||||
| `REVIEW_GATES_API_LENS_COST_USD` | `:SeatPool` ApiConfig | `1.0` | As API_BUDGET_USD | USD reserved per running process: a pass fits while spent + (running+1) × this + other drivers' cost ≤ budget |
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ alias crafter-review='flatpak run net.catcrafts.CrafterReview'
|
|||
What the machine provides:
|
||||
|
||||
- **The Claude Code CLI.** It is proprietary and not bundled. crafter-review looks for it in this order: `CLAUDE_BIN` or `REVIEW_GATES_CLAUDE_BIN`, the PATH, the editor extensions' bundled binary (newest first), then `~/.claude/local` and `~/.local/bin`. It runs inside the sandbox, so every pass is measured and every pass's process tree can be killed.
|
||||
- **Seats.** The lens processes run on the seats in the repository's `.env` (`CLAUDE_CODE_OAUTH_TOKEN1..6`, `ANTHROPIC_API_KEY1..6`), else this machine's own login. `crafter-review seats` probes and reports them. It never prints a token.
|
||||
- **Seats.** The lens processes run on the seats in the repository's `.env` (`CLAUDE_CODE_OAUTH_TOKEN1..6`, `ANTHROPIC_API_KEY1..6`), else this machine's own login. `crafter-review seats` probes and reports them. It never prints a token. An account whose 5-hour limit Anthropic has removed shows `no limit` in that column and is placed by its weekly window; every account takes at most 128 passes at once.
|
||||
- **Chromium, for a lens with the browser.** Run `crafter-review browser install` once. It installs the Chromium build the pinned `@playwright/mcp@0.0.83` drives into `~/.cache/ms-playwright`, where the MCP server looks. It runs Playwright's own installer, which also removes the browser builds no installed Playwright on the machine refers to.
|
||||
- **A local Supabase stack, for a lens with the database** (`supabase start`, or the repository's own bring-up). The `sql` helper speaks the Postgres protocol itself: no `psql` and no driver to install. Every call is one transaction, rolled back.
|
||||
- **gpg, if your commits are signed.** `freeze` reads each commit's signature through gnupg's keyboxd socket, which is all the sandbox is given. The agent, which signs, stays out of reach.
|
||||
|
|
|
|||
|
|
@ -649,7 +649,7 @@ namespace Crafter::Review::Run {
|
|||
} catch (const std::runtime_error& e) {
|
||||
Die(e.what());
|
||||
}
|
||||
const Json pc = SeatPool::PoolConfig(processEnv);
|
||||
const Json pc = SeatPool::PoolConfig(processEnv, a.effort);
|
||||
board = std::make_unique<SeatPool::Board>(SeatPool::BoardPath(), std::string(), std::nullopt, log);
|
||||
board->Join({{"run", std::format("commits {} @ {}", Basename(Dyn::RStrip(a.artifact, "/")), h8)},
|
||||
{"dump", Basename(dumpPath)},
|
||||
|
|
@ -673,7 +673,7 @@ namespace Crafter::Review::Run {
|
|||
const double now = Platform::Now();
|
||||
board->Publish(seats, now, !a.noProbe);
|
||||
board->Fold(seats, now);
|
||||
for (const std::string& line : board->OthersLines(seats, now, SeatPool::PoolConfig(processEnv), apiCfg, a.concurrency)) log(line);
|
||||
for (const std::string& line : board->OthersLines(seats, now, SeatPool::PoolConfig(processEnv, a.effort), apiCfg, a.concurrency)) log(line);
|
||||
}
|
||||
if (a.harness == "opencode") {
|
||||
runner->opencodeHome = OpenCode::PrepareHome(scratch, repo, modelIds);
|
||||
|
|
@ -1467,6 +1467,7 @@ namespace Crafter::Review::Run {
|
|||
.cacheKey = warm ? std::function<std::optional<std::string>(Json&)>([this](Json& job) { return CacheKey(job); }) : nullptr,
|
||||
.warmProbe = warm ? std::function<bool(const Json&)>([this](const Json& job) { return WarmProbe(job); }) : nullptr,
|
||||
.seats = &seats,
|
||||
.seatConfig = SeatPool::PoolConfig(Platform::Environment(), a.effort),
|
||||
.apiConfig = apiCfg,
|
||||
.ledger = ledger.get(),
|
||||
.admission = admission.get(),
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ namespace Crafter::Review::SeatPool {
|
|||
}
|
||||
|
||||
Json ZeroForeign() {
|
||||
return {{"count", 0}, {"cost5h", 0.0}, {"costUsd", 0.0}};
|
||||
return {{"count", 0}, {"cost5h", 0.0}, {"cost7d", 0.0}, {"costUsd", 0.0}};
|
||||
}
|
||||
|
||||
std::string Kind(const Json& seat) {
|
||||
|
|
@ -510,16 +510,50 @@ namespace Crafter::Review::SeatPool {
|
|||
}
|
||||
|
||||
// ---- placement -------------------------------------------------------------------------
|
||||
Json PoolConfig(const std::map<std::string, std::string>& env) {
|
||||
namespace {
|
||||
// A finite number >= 0 in Dyn::ParseFloat's syntax, else the default.
|
||||
double EnvNumber(const std::map<std::string, std::string>& env, const std::string& name, double fallback) {
|
||||
const std::string text = EnvOf(env, name);
|
||||
const std::optional<double> n = Dyn::ParseFloat(text);
|
||||
return n && !Dyn::Strip(text).empty() && std::isfinite(*n) && *n >= 0 ? *n : fallback;
|
||||
}
|
||||
|
||||
// A pass's share of the weekly window, as measured: 0.04 at xhigh,
|
||||
// 0.125 at max and for any other effort.
|
||||
double Cost7d(const Json& config) {
|
||||
return Dyn::Has(config, "lensCost7dPct") ? AsDouble(config.at("lensCost7dPct")) : 0.125;
|
||||
}
|
||||
|
||||
// The weekly window with the passes in flight and this one projected
|
||||
// on it stays under the cutoff: the room of an account whose burst
|
||||
// window is the weekly one.
|
||||
bool WeeklyRoom(const Json& s, const Json& config) {
|
||||
const double ahead = static_cast<double>(Running(s) + 1) * Cost7d(config) + Num(Dyn::Get(Foreign(s), "cost7d"));
|
||||
const Json& windows = Dyn::At(s, "windows");
|
||||
for (auto it = windows.begin(); it != windows.end(); ++it) {
|
||||
if (it.key() == "five_hour" || Seats::IsFableWindow(it.key())) continue;
|
||||
if (Num(Dyn::Get(it.value(), "utilization")) + ahead >= Seats::LongWindowCutoff) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Json PoolConfig(const std::map<std::string, std::string>& env, std::string_view effort) {
|
||||
Json config = Seats::ConfigFromEnv(env);
|
||||
if (Dyn::Strip(EnvOf(env, "REVIEW_GATES_LENS_COST_PCT")).empty()) config["lensCostPct"] = 0.25;
|
||||
config["lensCost7dPct"] = EnvNumber(env, "REVIEW_GATES_LENS_COST_7D_PCT", effort == "xhigh" ? 0.04 : 0.125);
|
||||
config["oauthConcurrency"] = static_cast<std::int64_t>(EnvNumber(env, "REVIEW_GATES_OAUTH_CONCURRENCY", static_cast<double>(OauthConcurrency)));
|
||||
return config;
|
||||
}
|
||||
|
||||
double OauthProjected(const Json& s, const Json& config) {
|
||||
// Where the seat's 5-hour window will be with its running processes -
|
||||
// Where the seat's burst window will be with its running processes -
|
||||
// this driver's at its projection, the other drivers' at theirs (the
|
||||
// board). Unknown = at the ceiling, ranked last.
|
||||
// board): the 5-hour window, or the weekly one on an account without
|
||||
// a 5-hour limit. Unknown = at the ceiling, ranked last.
|
||||
if (Seats::BurstWindow(Dyn::At(s, "windows")) == "seven_day") {
|
||||
return AsDouble(s.at("windows").at("seven_day").at("utilization")) + static_cast<double>(Running(s)) * Cost7d(config) + Num(Dyn::Get(Foreign(s), "cost7d"));
|
||||
}
|
||||
const Json& five = Dyn::Get(FiveHour(s), "utilization");
|
||||
const double base = five.is_null() ? AsDouble(config.at("maxUtil")) : AsDouble(five);
|
||||
return base + static_cast<double>(Running(s)) * AsDouble(config.at("lensCostPct")) + AsDouble(Foreign(s).at("cost5h"));
|
||||
|
|
@ -552,9 +586,17 @@ namespace Crafter::Review::SeatPool {
|
|||
// Its pause is over (checked above): back in placement, its windows
|
||||
// decide.
|
||||
if (s.at("state") == "limited") s["state"] = "ok";
|
||||
// No 5-hour reading (no probe, or it failed): usable, ranked at the
|
||||
// ceiling - after every known seat.
|
||||
if (Dyn::Get(FiveHour(s), "utilization").is_null()) return true;
|
||||
// So many passes per account at once, every driver's counted,
|
||||
// whatever its windows (2026-10-31).
|
||||
const std::int64_t cap = Dyn::Has(config, "oauthConcurrency") ? Int(AsDouble(config.at("oauthConcurrency"))) : OauthConcurrency;
|
||||
if (Running(s) + Int(AsDouble(Foreign(s).at("count"))) >= cap) return false;
|
||||
const std::optional<std::string> burst = Seats::BurstWindow(s.at("windows"));
|
||||
// No reading (no probe, or it failed): usable, ranked at the ceiling -
|
||||
// after every known seat.
|
||||
if (!burst) return true;
|
||||
// No 5-hour limit on the account: the weekly cutoff alone, with the
|
||||
// passes projected on it.
|
||||
if (*burst == "seven_day") return WeeklyRoom(s, config);
|
||||
return OauthProjected(s, config) + AsDouble(config.at("lensCostPct")) <= AsDouble(config.at("maxUtil")) && LongOk(s);
|
||||
}
|
||||
|
||||
|
|
@ -628,11 +670,14 @@ namespace Crafter::Review::SeatPool {
|
|||
std::vector<double> times;
|
||||
for (const Json& s : seats) {
|
||||
if (s.at("state") == "out" || s.at("state") == "invalid") continue;
|
||||
const Json& five = FiveHour(s);
|
||||
if (AsDouble(s.at("pauseUntil")) > now) {
|
||||
times.push_back(AsDouble(s.at("pauseUntil")));
|
||||
} else if (Kind(s) != "api" && Dyn::Truthy(Dyn::Get(five, "resetsAt")) && AsDouble(five.at("resetsAt")) + 30 > now) {
|
||||
times.push_back(AsDouble(five.at("resetsAt")) + 30);
|
||||
} else if (Kind(s) != "api") {
|
||||
// The reset of the seat's burst window: the 5-hour one, the
|
||||
// weekly one on an account without it.
|
||||
const std::optional<std::string> burst = Seats::BurstWindow(s.at("windows"));
|
||||
const Json& window = burst ? Dyn::Get(s.at("windows"), *burst) : Dyn::EmptyDict();
|
||||
if (Dyn::Truthy(Dyn::Get(window, "resetsAt")) && AsDouble(window.at("resetsAt")) + 30 > now) times.push_back(AsDouble(window.at("resetsAt")) + 30);
|
||||
}
|
||||
if (recheck && AsDouble(Foreign(s).at("count")) > 0) times.push_back(now + *recheck);
|
||||
}
|
||||
|
|
@ -730,6 +775,16 @@ namespace Crafter::Review::SeatPool {
|
|||
return true;
|
||||
}
|
||||
const std::string rateType = Dyn::StrOr(Dyn::Get(record, "rateType"), "");
|
||||
if (rateType == "concurrents") {
|
||||
// Not a window: the account had too many requests in flight
|
||||
// at once (a burst of passes released onto one seat when its
|
||||
// prompt prefix became cached). Seconds, not the window's
|
||||
// reset.
|
||||
s["pauseUntil"] = std::max(AsDouble(s.at("pauseUntil")), now + RetryAfter(reason, ConcurrentBackoffS));
|
||||
s["state"] = "limited";
|
||||
log(std::format("seat {}: too many requests at once ({}) - paused {} s, the process is placed again", Id(s), Dyn::Slice(reason, 0, 40), Int(AsDouble(s.at("pauseUntil")) - now)));
|
||||
return true;
|
||||
}
|
||||
const Json& binding = s.at("binding");
|
||||
if (rateType.starts_with("seven_day") || (Dyn::StrOr(binding, "").starts_with("seven_day") && Num(Dyn::Get(s, "util")) >= 100)) {
|
||||
s["state"] = "out";
|
||||
|
|
@ -1112,17 +1167,20 @@ namespace Crafter::Review::SeatPool {
|
|||
const Json& inflight = Dyn::Or(Dyn::Get(b, "inflight"), Dyn::EmptyDict());
|
||||
std::int64_t count = 0;
|
||||
double cost5h = 0.0;
|
||||
double cost7d = 0.0;
|
||||
double costUsd = 0.0;
|
||||
for (auto it = inflight.begin(); it != inflight.end(); ++it) {
|
||||
const std::string owner = Owner(it.key());
|
||||
if (owner == me) continue;
|
||||
++count;
|
||||
cost5h += Num(Dyn::Get(it.value(), "cost5h"));
|
||||
cost7d += Num(Dyn::Get(it.value(), "cost7d"));
|
||||
costUsd += Num(Dyn::Get(it.value(), "costUsd"));
|
||||
f["drivers"][owner] = Dyn::Has(f["drivers"], owner) ? f["drivers"][owner].get<std::int64_t>() + 1 : 1;
|
||||
}
|
||||
f["count"] = count;
|
||||
f["cost5h"] = cost5h;
|
||||
f["cost7d"] = cost7d;
|
||||
f["costUsd"] = costUsd;
|
||||
if (!IsPaid(s) && !MergeWindows(s["windows"], Dyn::Get(b, "windows"), now).empty()) Reutil(s);
|
||||
const double pause = Num(Dyn::Get(b, "pauseUntil"));
|
||||
|
|
@ -1140,6 +1198,7 @@ namespace Crafter::Review::SeatPool {
|
|||
}
|
||||
}
|
||||
f["cost5h"] = Dyn::Round(AsDouble(f.at("cost5h")), 4);
|
||||
f["cost7d"] = Dyn::Round(AsDouble(f.at("cost7d")), 4);
|
||||
f["costUsd"] = Dyn::Round(AsDouble(f.at("costUsd")), 4);
|
||||
s["foreign"] = std::move(f);
|
||||
}
|
||||
|
|
@ -1309,6 +1368,7 @@ namespace Crafter::Review::SeatPool {
|
|||
{"since", now},
|
||||
{"deadline", now + 60 * (Dyn::Truthy(timeout) ? AsDouble(timeout) : 20.0) * 1.5 + InflightGraceS},
|
||||
{"cost5h", paid ? Json(0) : config.at("lensCostPct")},
|
||||
{"cost7d", paid ? Json(0) : Json(Cost7d(config))},
|
||||
{"costUsd", paid ? LensCostValue(seat, apiConfig) : Json(0)},
|
||||
};
|
||||
if (!Dyn::Get(job, "_admitted").is_null()) entry["mb"] = Dyn::Round(AsDouble(job.at("_admitted")), 1);
|
||||
|
|
@ -1362,6 +1422,7 @@ namespace Crafter::Review::SeatPool {
|
|||
Json by = Json::object();
|
||||
std::int64_t count = 0;
|
||||
double cost5h = 0.0;
|
||||
double cost7d = 0.0;
|
||||
double costUsd = 0.0;
|
||||
Json oldest;
|
||||
const Json& inflight = Dyn::Or(Dyn::Get(s, "inflight"), Dyn::EmptyDict());
|
||||
|
|
@ -1370,6 +1431,7 @@ namespace Crafter::Review::SeatPool {
|
|||
by[owner] = Dyn::Has(by, owner) ? by[owner].get<std::int64_t>() + 1 : 1;
|
||||
++count;
|
||||
cost5h += Num(Dyn::Get(e.value(), "cost5h"));
|
||||
cost7d += Num(Dyn::Get(e.value(), "cost7d"));
|
||||
costUsd += Num(Dyn::Get(e.value(), "costUsd"));
|
||||
const double since = Num(Dyn::Get(e.value(), "since"));
|
||||
if (oldest.is_null() || since < oldest.get<double>()) oldest = since;
|
||||
|
|
@ -1386,6 +1448,7 @@ namespace Crafter::Review::SeatPool {
|
|||
{"kind", Dyn::Get(s, "kind")},
|
||||
{"inflight", count},
|
||||
{"cost5h", Dyn::Round(cost5h, 4)},
|
||||
{"cost7d", Dyn::Round(cost7d, 4)},
|
||||
{"costUsd", Dyn::Round(costUsd, 4)},
|
||||
{"by", by},
|
||||
{"oldest", oldest},
|
||||
|
|
@ -1436,6 +1499,8 @@ namespace Crafter::Review::SeatPool {
|
|||
const std::int64_t c = Concurrency(s, apiConfig);
|
||||
lines.push_back(std::format(" seat {}: {} in flight ({}) - {} of {} slots free, ${:.2f} reserved", Id(s), count, Dyn::Join(who, ", "), std::max<std::int64_t>(0, c - count - Running(s)), c,
|
||||
AsDouble(f.at("costUsd"))));
|
||||
} else if (Seats::BurstWindow(s.at("windows")) == "seven_day") {
|
||||
lines.push_back(std::format(" seat {}: {} passes in flight ({}) - projected 7d {:.1f} %, no 5-hour limit", Id(s), count, Dyn::Join(who, ", "), OauthProjected(s, config)));
|
||||
} else {
|
||||
lines.push_back(std::format(" seat {}: {} passes in flight ({}) - projected 5h {:.1f} %", Id(s), count, Dyn::Join(who, ", "), OauthProjected(s, config)));
|
||||
}
|
||||
|
|
@ -1475,10 +1540,7 @@ namespace Crafter::Review::SeatPool {
|
|||
std::vector<std::vector<std::string>> rows = {{"seat", "kind", "state", "5h", "7d", "fable", "running", "foreign", "done", "spent", "note"}};
|
||||
for (const Json& s : seats) {
|
||||
const Json& windows = s.at("windows");
|
||||
auto w = [&](std::string_view k) {
|
||||
const Json& u = Dyn::Get(Dyn::Or(Dyn::Get(windows, k), Dyn::EmptyDict()), "utilization");
|
||||
return u.is_null() ? std::string("—") : std::format("{}%", Seats::Fmt(u));
|
||||
};
|
||||
auto w = [&](std::string_view k) { return Seats::WindowCell(windows, k); };
|
||||
std::string fable = "—";
|
||||
for (auto it = windows.begin(); it != windows.end(); ++it) {
|
||||
if (Seats::IsFableWindow(it.key()) && !Dyn::Get(it.value(), "utilization").is_null()) {
|
||||
|
|
|
|||
|
|
@ -827,6 +827,19 @@ namespace Crafter::Review::Seats {
|
|||
return std::format("{:02d}:{:02d}", d.tm_hour, d.tm_min);
|
||||
}
|
||||
|
||||
std::optional<std::string> BurstWindow(const Json& windows) {
|
||||
for (const std::string_view window : {std::string_view("five_hour"), std::string_view("seven_day")}) {
|
||||
if (!Dyn::Get(Dyn::Or(Dyn::Get(windows, window), Dyn::EmptyDict()), "utilization").is_null()) return std::string(window);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string WindowCell(const Json& windows, std::string_view window) {
|
||||
const Json& u = Dyn::Get(Dyn::Or(Dyn::Get(windows, window), Dyn::EmptyDict()), "utilization");
|
||||
if (!u.is_null()) return std::format("{}%", Fmt(u));
|
||||
return window == "five_hour" && BurstWindow(windows) == "seven_day" ? std::string("no limit") : std::string("—");
|
||||
}
|
||||
|
||||
std::string FormatWhen(double epoch, double now, const Zone& zone) {
|
||||
const auto diffMin = static_cast<std::int64_t>(JsRound((epoch - now) / 60));
|
||||
std::string relative;
|
||||
|
|
@ -876,10 +889,7 @@ namespace Crafter::Review::Seats {
|
|||
if (anyInflight) header.emplace_back("in flight");
|
||||
rows.push_back(header);
|
||||
for (const Json& s : ordered) {
|
||||
auto w = [&](std::string_view k) {
|
||||
if (!Dyn::Has(Dyn::Or(Dyn::Get(s, "windows"), Dyn::EmptyDict()), k)) return std::string("—");
|
||||
return std::format("{}%", Fmt(Dyn::At(Window(s, k), "utilization")));
|
||||
};
|
||||
auto w = [&](std::string_view k) { return WindowCell(Dyn::Or(Dyn::Get(s, "windows"), Dyn::EmptyDict()), k); };
|
||||
auto when = [&](std::string_view k) {
|
||||
const Json& at = Dyn::Get(Dyn::Or(Window(s, k), Dyn::EmptyDict()), "resetsAt");
|
||||
return Dyn::Truthy(at) ? FormatWhen(AsDouble(at), now, zone) : std::string("—");
|
||||
|
|
|
|||
|
|
@ -32,6 +32,17 @@ export namespace Crafter::Review::SeatPool {
|
|||
// An API seat's pause after a 429.
|
||||
constexpr double ApiBackoffS = 90;
|
||||
constexpr double OauthFallbackPauseS = 300;
|
||||
// An OAuth seat's pause after a 'concurrents' rejection: too many requests
|
||||
// at once on the account, cleared as the ones in flight answer. Not the
|
||||
// window's reset (2026-10-31: a seat sat two hours at 10 % on one).
|
||||
constexpr double ConcurrentBackoffS = 20;
|
||||
// The passes one OAuth account (or the local login) takes at once, every
|
||||
// driver's counted. Until 2026-10-31 the 5-hour projection was the only
|
||||
// limit, and an account without a 5-hour limit has none: on the record,
|
||||
// every run with 149 or more in flight on one account drew usage-limit
|
||||
// rejections, none of the 119 with 139 or fewer.
|
||||
// REVIEW_GATES_OAUTH_CONCURRENCY overrides.
|
||||
constexpr std::int64_t OauthConcurrency = 128;
|
||||
// The kinds whose processes cost money the ledger counts.
|
||||
constexpr std::array<std::string_view, 2> Paid = {"api", "openrouter"};
|
||||
// The kinds whose out/invalid verdict crosses drivers on the board.
|
||||
|
|
@ -90,9 +101,13 @@ export namespace Crafter::Review::SeatPool {
|
|||
void ApplyProbe(Json& seat, const Json& result);
|
||||
std::string Describe(const Json& seat);
|
||||
|
||||
// The OAuth placement knobs, with the pool's own per-process default (0.25:
|
||||
// the 2026-10-15 load test moved a 20x seat's window 0.08 points a pass).
|
||||
Json PoolConfig(const std::map<std::string, std::string>& env);
|
||||
// The OAuth placement knobs, with the pool's own per-process projections:
|
||||
// 0.25 of the 5-hour window (the 2026-10-15 load test moved a 20x seat's
|
||||
// window 0.08 points a pass) and, for an account without a 5-hour limit,
|
||||
// the measured share of the weekly window at the run's effort (0.04 at
|
||||
// xhigh, 0.125 otherwise; REVIEW_GATES_LENS_COST_7D_PCT); and the
|
||||
// per-account cap.
|
||||
Json PoolConfig(const std::map<std::string, std::string>& env, std::string_view effort = {});
|
||||
double OauthProjected(const Json& seat, const Json& config);
|
||||
// A window whose reset has passed counts as empty until the next reading.
|
||||
std::vector<std::string> ExpireWindows(Json& seat, double now);
|
||||
|
|
|
|||
|
|
@ -86,6 +86,14 @@ export namespace Crafter::Review::Seats {
|
|||
std::pair<Json, std::string> BindingWindow(const Json& windows);
|
||||
// Quantised for ranking.
|
||||
std::pair<Json, std::string> SeatUtil(const Json& windows);
|
||||
// The window a burst of passes moves: the 5-hour one, else the weekly one.
|
||||
// Anthropic removes the 5-hour limit in stages (from 2026-10-31), and an
|
||||
// account without it reports no 5-hour window. nullopt for a seat with no
|
||||
// reading.
|
||||
std::optional<std::string> BurstWindow(const Json& windows);
|
||||
// A window's utilisation as a report cell: `no limit` in the 5-hour column
|
||||
// of an account without that limit.
|
||||
std::string WindowCell(const Json& windows, std::string_view window);
|
||||
std::string ModelFamily(std::string_view model);
|
||||
bool IsFableWindow(std::string_view key);
|
||||
const Json* FableWindowOf(const Json& seat);
|
||||
|
|
|
|||
|
|
@ -248,7 +248,10 @@ per hash: a second on the same hash is refused while the first is live
|
|||
drivers share a scratch dir. `flatpak run net.catcrafts.CrafterReview seats` shows the
|
||||
drivers running and what they hold. The seat pool still places
|
||||
every process by its seat's projected 5-hour headroom
|
||||
(`REVIEW_GATES_LENS_COST_PCT`, `REVIEW_GATES_SEAT_MAX_UTIL`), and a pass
|
||||
(`REVIEW_GATES_LENS_COST_PCT`, `REVIEW_GATES_SEAT_MAX_UTIL`) — or, on an
|
||||
account whose 5-hour limit Anthropic has removed, by its weekly window
|
||||
under the 95 % cutoff — at most 128 passes per account, every driver's
|
||||
counted (`REVIEW_GATES_OAUTH_CONCURRENCY`), and a pass
|
||||
launches only when its environment's instrument is free and it fits in
|
||||
free memory (How the passes share the stacks): the driver logs per gate
|
||||
how many passes of each environment memory and the stacks allow. Run
|
||||
|
|
@ -572,3 +575,12 @@ repository.
|
|||
repository up and ships the skill, `doc` the texts, `browser install` the
|
||||
Chromium, `stop` ends a run from any sandbox. A driver takes its passes
|
||||
with it however it ends.
|
||||
- 2026-10-31 — **accounts without a 5-hour limit** (Jorijn: "antrophic is
|
||||
gradually removing the 5h limits"). Anthropic removes the 5-hour limit in
|
||||
stages, and an account without it reports the weekly window alone. Such
|
||||
an account is ranked and admitted by its weekly window with the passes in
|
||||
flight projected on it, never taken for an unprobed seat; every OAuth
|
||||
account takes at most 128 passes at once (every run measured with 149 or
|
||||
more in flight on one account drew usage-limit rejections); a 'concurrents' rejection backs the seat off for seconds,
|
||||
not until a window's reset; `seats` shows `no limit` in such an account's
|
||||
5-hour column.
|
||||
|
|
|
|||
|
|
@ -339,7 +339,8 @@ namespace {
|
|||
// projection.
|
||||
const Json base = sm["windows"]["five_hour"]["utilization"];
|
||||
Check("a driver's passes fold at its own projection (2 % each) into another driver's (0.25 %)",
|
||||
Dyn::Equal(sm["foreign"], {{"count", 2}, {"cost5h", 4.0}, {"costUsd", 0}, {"drivers", {{D.Me(), 2}}}}) && base == 40 && SeatPool::OauthProjected(sm, cfg) == base.get<double>() + 4.0,
|
||||
Dyn::Equal(sm["foreign"], {{"count", 2}, {"cost5h", 4.0}, {"cost7d", 0.25}, {"costUsd", 0}, {"drivers", {{D.Me(), 2}}}}) && base == 40
|
||||
&& SeatPool::OauthProjected(sm, cfg) == base.get<double>() + 4.0,
|
||||
std::format("{} {}", DumpSpaced(sm["foreign"]), base.dump()));
|
||||
const std::optional<std::string> unviewed = Platform::ReadFile(path);
|
||||
Json v = A.View(now);
|
||||
|
|
@ -347,6 +348,18 @@ namespace {
|
|||
Check("view: the drivers and, per account, the passes in flight by driver - nothing written",
|
||||
v["drivers"].contains(A.Me()) && v["drivers"].contains(D.Me()) && v["seats"][km]["inflight"] == 2 && Dyn::Equal(v["seats"][km]["by"], {{D.Me(), 2}}) && v["drivers"][D.Me()]["inflight"] == 2
|
||||
&& Platform::ReadFile(path) == unviewed);
|
||||
// 2026-10-31: an account without a 5-hour limit, as another driver's
|
||||
// pass loads it.
|
||||
Json sdn = mk("7", "oauth", "t7");
|
||||
Json san = mk("7", "oauth", "t7");
|
||||
sdn["windows"] = {{"seven_day", {{"utilization", 10}, {"resetsAt", now + 86400}}}};
|
||||
san["windows"] = sdn.at("windows");
|
||||
place1(D, sdn, cfg, chooserFor("7", "n0"));
|
||||
fold1(A, san, now);
|
||||
std::vector<Json> weeklySeats = {san};
|
||||
const std::vector<std::string> ol = A.OthersLines(weeklySeats, now, cfg, acfg);
|
||||
Check("others: an account without a 5-hour limit shows the other driver's pass on its weekly projection",
|
||||
std::ranges::any_of(ol, [](const std::string& l) { return l.contains("seat 7: 1 passes in flight") && l.contains("projected 7d 10.1 %, no 5-hour limit"); }), Dyn::Join(ol, " | "));
|
||||
D.Leave();
|
||||
Check("no secret on the board", !DumpSpaced(Read(path).at("seats")).contains("t1") && !Platform::ReadFile(path).value_or("").contains("k1"));
|
||||
// Memory across drivers (2026-10-31): what the board's passes were
|
||||
|
|
|
|||
|
|
@ -97,6 +97,51 @@ int main() {
|
|||
requeued = SeatPool::Settle(api2, credits, now, cfg, acfg, &ledger, log);
|
||||
Check("credit error: out, recorded in the ledger, requeued", requeued && api2.at("state") == "out" && Dyn::Truthy(ledger.Entry("api2").at("outSince")));
|
||||
|
||||
// 2026-10-31: a 'concurrents' rejection (too many requests at once on the
|
||||
// account) is a short back-off, not the window's reset.
|
||||
Json sc = With(SeatPool::NewSeat("c", "oauth", "t"), {{"state", "ok"}, {"util", 10}, {"running", 1}, {"pauseUntil", 0}, {"windows", {{"five_hour", {{"utilization", 10}, {"resetsAt", now + 7000}}}}}});
|
||||
Json concurrents = {{"status", "limit"}, {"reason", "You've hit your usage limit"}, {"rateType", "concurrents"}, {"rateLimit", {{"five_hour", {{"utilization", 10}, {"resetsAt", now + 7000}}}}}};
|
||||
requeued = SeatPool::Settle(sc, concurrents, now, cfg, acfg, &ledger, log);
|
||||
Check("a 'concurrents' rejection on an oauth seat: requeued, paused for seconds, not until the 5h reset",
|
||||
requeued && sc.at("state") == "limited" && sc.at("pauseUntil").get<double>() == now + SeatPool::ConcurrentBackoffS && !SeatPool::Eligible(sc, now, cfg, acfg));
|
||||
Json backOk = {{"status", "ok"}, {"rateLimit", {{"five_hour", {{"utilization", 11}, {"resetsAt", now + 7000}}}}}};
|
||||
requeued = SeatPool::Settle(sc, backOk, now + SeatPool::ConcurrentBackoffS + 1, cfg, acfg, &ledger, log);
|
||||
Check("...and the seat is back after the back-off", !requeued && sc.at("state") == "ok" && SeatPool::Eligible(sc, now + SeatPool::ConcurrentBackoffS + 1, cfg, acfg));
|
||||
// 2026-10-31: an account whose 5-hour limit is removed reports the weekly
|
||||
// window alone; until then the pool took it for an unprobed seat - ranked
|
||||
// after every other, never held to the weekly cutoff. The cutoff is 95 %.
|
||||
const Json px = SeatPool::PoolConfig({}, "xhigh");
|
||||
Json nf = With(SeatPool::NewSeat("n", "oauth", "tn"), {{"state", "ok"}, {"running", 0}, {"pauseUntil", 0}, {"windows", {{"seven_day", {{"utilization", 3}, {"resetsAt", now + 6 * 86400}}}}}});
|
||||
Json f15 = With(SeatPool::NewSeat("f", "oauth", "tf"), {{"state", "ok"}, {"running", 0}, {"pauseUntil", 0},
|
||||
{"windows", {{"five_hour", {{"utilization", 15}, {"resetsAt", now + 3600}}}, {"seven_day", {{"utilization", 11}, {"resetsAt", now + 86400}}}}}});
|
||||
std::vector<Json> pair = {f15, nf};
|
||||
std::vector<std::string> order;
|
||||
for (const Json* s : SeatPool::Ranked(pair, now, px, acfg)) order.push_back(s->at("id").get<std::string>());
|
||||
Check("an account without a 5-hour window is placed by its weekly one: ranked by it, ahead of a seat at 5h 15 %, its running passes projected at the weekly figure",
|
||||
order == std::vector<std::string>{"n", "f"} && std::abs(SeatPool::OauthProjected(With(nf, {{"running", 100}}), px) - 7.0) < 1e-9, Dyn::Join(order, ","));
|
||||
Json n93 = With(nf, {{"windows", {{"seven_day", {{"utilization", 93}, {"resetsAt", now + 86400}}}}}});
|
||||
Json n93r40 = With(n93, {{"running", 40}});
|
||||
Json n93r60 = With(n93, {{"running", 60}});
|
||||
Check("...under the weekly cutoff alone, no 5-hour ceiling: at 93 % it takes a pass with 40 running, not with 60 (93 + 61 x 0.04 >= 95)",
|
||||
SeatPool::Eligible(n93r40, now, px, acfg) && !SeatPool::Eligible(n93r60, now, px, acfg));
|
||||
Json n95 = With(nf, {{"windows", {{"seven_day", {{"utilization", 95}, {"resetsAt", now + 86400}}}}}});
|
||||
Check("...and over the cutoff it waits for its weekly reset + 30 s", !SeatPool::Eligible(n95, now, px, acfg) && SeatPool::EarliestResume({n95}, now) == now + 86430);
|
||||
Json n127 = With(nf, {{"running", 127}});
|
||||
Json n128 = With(nf, {{"running", 128}});
|
||||
Json nShared = With(nf, {{"running", 100}, {"foreign", {{"count", 28}, {"cost5h", 0.0}, {"cost7d", 0.0}, {"costUsd", 0.0}}}});
|
||||
Json f127 = With(f15, {{"running", 127}});
|
||||
Json f128 = With(f15, {{"running", 128}});
|
||||
Check("128 passes per OAuth account, every driver counted, whatever its windows; REVIEW_GATES_OAUTH_CONCURRENCY overrides",
|
||||
SeatPool::Eligible(n127, now, px, acfg) && !SeatPool::Eligible(n128, now, px, acfg) && !SeatPool::Eligible(nShared, now, px, acfg) && SeatPool::Eligible(f127, now, px, acfg)
|
||||
&& !SeatPool::Eligible(f128, now, px, acfg) && px.at("oauthConcurrency") == 128 && SeatPool::PoolConfig({{"REVIEW_GATES_OAUTH_CONCURRENCY", "64"}}, "xhigh").at("oauthConcurrency") == 64);
|
||||
Json weekly = {{"status", "ok"}, {"rateLimit", {{"seven_day", {{"utilization", 4}, {"resetsAt", now + 6 * 86400}}}}}};
|
||||
requeued = SeatPool::Settle(nf, weekly, now, px, acfg, &ledger, log);
|
||||
Check("...a finished pass's reading keeps it a weekly seat, unpaused; the log table says `no limit` in its 5h column",
|
||||
!requeued && Seats::BurstWindow(nf.at("windows")) == "seven_day" && nf.at("pauseUntil") == 0 && SeatPool::Table({nf}).contains("| no limit |"), SeatPool::Table({nf}));
|
||||
Check("the weekly share of a pass follows the run's effort (0.04 at xhigh, 0.125 otherwise); the env overrides it",
|
||||
Dyn::Equal(px.at("lensCost7dPct"), 0.04) && Dyn::Equal(SeatPool::PoolConfig({}).at("lensCost7dPct"), 0.125) && Dyn::Equal(SeatPool::PoolConfig({}, "max").at("lensCost7dPct"), 0.125)
|
||||
&& Dyn::Equal(SeatPool::PoolConfig({{"REVIEW_GATES_LENS_COST_7D_PCT", "0.5"}}, "xhigh").at("lensCost7dPct"), 0.5));
|
||||
|
||||
const fs::path scratch = td / "scratch";
|
||||
fs::create_directories(scratch);
|
||||
const std::map<std::string, std::string> baseEnv = {{"PATH", "/bin"}, {"ANTHROPIC_API_KEY", "leak"}, {"CLAUDE_CODE_OAUTH_TOKEN", "leak2"}, {"HOME", td.string()}};
|
||||
|
|
|
|||
|
|
@ -153,6 +153,15 @@ int main() {
|
|||
r = RenderStatusReport({MergeFableProbe(revoked, revoked)}, cfg, now, utc);
|
||||
Eq(r.contains("not usable: seat 3 invalid (token revoked)"), true);
|
||||
Eq(r.contains("fable probe:"), false);
|
||||
// 2026-10-31: an account whose 5-hour limit is removed reports the weekly
|
||||
// window alone - `no limit` in its 5h cell, the weekly window its burst
|
||||
// window, ranked by it.
|
||||
const Json nf = {{"seat", 3}, {"state", "ok"}, {"windows", {{"seven_day", {{"utilization", 3}, {"resetsAt", nowS + 6 * 86400}}}}}, {"util", 5}, {"binding", "seven_day"}, {"resetsAt", nowS + 6 * 86400}};
|
||||
r = RenderStatusReport({seat(2, 61, 18), nf}, cfg, now, utc);
|
||||
Eq(Lines(r).at(3), std::string("| 3 | no limit | — | 3% | Sun 13 Sept, 10:00 (in 6d 0h) | — |"));
|
||||
Eq(Lines(r).at(6), std::string("eligible now: seat 3 (80% headroom), seat 2 (25% headroom) → next lens: seat 3"));
|
||||
Eq(BurstWindow(nf.at("windows")) == "seven_day" && BurstWindow(seat(2, 61).at("windows")) == "five_hour" && !BurstWindow(Json::object()), true);
|
||||
Eq(std::format("{}|{}|{}", WindowCell(nf.at("windows"), "five_hour"), WindowCell(Json::object(), "five_hour"), WindowCell(nf.at("windows"), "seven_day")), std::string("no limit|—|3%"));
|
||||
// The local login is seat 0, named `local`.
|
||||
r = RenderStatusReport({seat(0, 30), seat(2, 50)}, cfg, now, utc);
|
||||
Eq(Lines(r).at(2).starts_with("| local | 30%"), true);
|
||||
|
|
|
|||
Loading…
Reference in a new issue