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
|
|
@ -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("—");
|
||||
|
|
|
|||
Loading…
Reference in a new issue