diff --git a/AGENTS.md b/AGENTS.md index 79012ae..236b34f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 | diff --git a/README.md b/README.md index 4f95d4f..974806e 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/implementations/Crafter.Review-Run.cpp b/implementations/Crafter.Review-Run.cpp index b2795fe..2bc286e 100644 --- a/implementations/Crafter.Review-Run.cpp +++ b/implementations/Crafter.Review-Run.cpp @@ -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::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(Json&)>([this](Json& job) { return CacheKey(job); }) : nullptr, .warmProbe = warm ? std::function([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(), diff --git a/implementations/Crafter.Review-SeatPool.cpp b/implementations/Crafter.Review-SeatPool.cpp index 19d0215..3a1ac89 100644 --- a/implementations/Crafter.Review-SeatPool.cpp +++ b/implementations/Crafter.Review-SeatPool.cpp @@ -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& env) { + namespace { + // A finite number >= 0 in Dyn::ParseFloat's syntax, else the default. + double EnvNumber(const std::map& env, const std::string& name, double fallback) { + const std::string text = EnvOf(env, name); + const std::optional 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(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& 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(EnvNumber(env, "REVIEW_GATES_OAUTH_CONCURRENCY", static_cast(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(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(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 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 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 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() + 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() + 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()) 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(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> 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()) { diff --git a/implementations/Crafter.Review-Seats.cpp b/implementations/Crafter.Review-Seats.cpp index a84da78..6a81336 100644 --- a/implementations/Crafter.Review-Seats.cpp +++ b/implementations/Crafter.Review-Seats.cpp @@ -827,6 +827,19 @@ namespace Crafter::Review::Seats { return std::format("{:02d}:{:02d}", d.tm_hour, d.tm_min); } + std::optional 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(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("—"); diff --git a/interfaces/Crafter.Review-SeatPool.cppm b/interfaces/Crafter.Review-SeatPool.cppm index 90d1c39..7306efb 100644 --- a/interfaces/Crafter.Review-SeatPool.cppm +++ b/interfaces/Crafter.Review-SeatPool.cppm @@ -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 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& 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& 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 ExpireWindows(Json& seat, double now); diff --git a/interfaces/Crafter.Review-Seats.cppm b/interfaces/Crafter.Review-Seats.cppm index 0fe9663..43177f9 100644 --- a/interfaces/Crafter.Review-Seats.cppm +++ b/interfaces/Crafter.Review-Seats.cppm @@ -86,6 +86,14 @@ export namespace Crafter::Review::Seats { std::pair BindingWindow(const Json& windows); // Quantised for ranking. std::pair 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 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); diff --git a/process/prepush.md b/process/prepush.md index 5e29206..9c9e228 100644 --- a/process/prepush.md +++ b/process/prepush.md @@ -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. diff --git a/tests/Drivers/main.cpp b/tests/Drivers/main.cpp index 5d5257b..6480b29 100644 --- a/tests/Drivers/main.cpp +++ b/tests/Drivers/main.cpp @@ -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() + 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() + 4.0, std::format("{} {}", DumpSpaced(sm["foreign"]), base.dump())); const std::optional 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 weeklySeats = {san}; + const std::vector 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 diff --git a/tests/SeatPool/main.cpp b/tests/SeatPool/main.cpp index dd21e2e..4a7d7e1 100644 --- a/tests/SeatPool/main.cpp +++ b/tests/SeatPool/main.cpp @@ -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() == 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 pair = {f15, nf}; + std::vector order; + for (const Json* s : SeatPool::Ranked(pair, now, px, acfg)) order.push_back(s->at("id").get()); + 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{"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 baseEnv = {{"PATH", "/bin"}, {"ANTHROPIC_API_KEY", "leak"}, {"CLAUDE_CODE_OAUTH_TOKEN", "leak2"}, {"HOME", td.string()}}; diff --git a/tests/Seats/main.cpp b/tests/Seats/main.cpp index f9a7bce..7628db2 100644 --- a/tests/Seats/main.cpp +++ b/tests/Seats/main.cpp @@ -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);