1498 lines
77 KiB
C++
1498 lines
77 KiB
C++
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||
|
|
|
||
|
|
module;
|
||
|
|
#include <cerrno>
|
||
|
|
#include <fcntl.h>
|
||
|
|
#include <unistd.h>
|
||
|
|
|
||
|
|
module Crafter.Review:SeatPool_impl;
|
||
|
|
import :SeatPool;
|
||
|
|
import :Json;
|
||
|
|
import :Text;
|
||
|
|
import :Crypto;
|
||
|
|
import :Platform;
|
||
|
|
import :Http;
|
||
|
|
import :Seats;
|
||
|
|
import std;
|
||
|
|
|
||
|
|
namespace fs = std::filesystem;
|
||
|
|
|
||
|
|
namespace Crafter::Review::SeatPool {
|
||
|
|
const std::string_view ResumePrompt =
|
||
|
|
"The previous attempt of this pass was interrupted by a limit on the account it ran on and continues here on "
|
||
|
|
"another account with the same transcript. Do not repeat work already done: continue from where it stopped, "
|
||
|
|
"finish the pass, and return the structured output exactly as the instructions require.\n";
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
using Log = std::function<void(const std::string&)>;
|
||
|
|
|
||
|
|
// A number as the files and the probes hold it, as a float.
|
||
|
|
double AsDouble(const Json& value) {
|
||
|
|
if (value.is_number_float()) return value.get<double>();
|
||
|
|
if (value.is_boolean()) return value.get<bool>() ? 1.0 : 0.0;
|
||
|
|
if (value.is_number_unsigned()) return static_cast<double>(value.get<std::uint64_t>());
|
||
|
|
if (value.is_number_integer()) return static_cast<double>(value.get<std::int64_t>());
|
||
|
|
if (value.is_string()) {
|
||
|
|
if (const std::optional<double> parsed = Dyn::ParseFloat(value.get_ref<const std::string&>())) return *parsed;
|
||
|
|
throw Dyn::Error("ValueError", Dyn::FloatError(value.get_ref<const std::string&>()));
|
||
|
|
}
|
||
|
|
throw Dyn::Error("TypeError", std::format("float() argument must be a string or a real number, not '{}'", Dyn::TypeName(value)));
|
||
|
|
}
|
||
|
|
|
||
|
|
// The value as a float, 0 when it is falsy.
|
||
|
|
double Num(const Json& value) {
|
||
|
|
return Dyn::Truthy(value) ? AsDouble(value) : 0.0;
|
||
|
|
}
|
||
|
|
|
||
|
|
// A float truncated toward zero, saturating at the ends of int64.
|
||
|
|
std::int64_t Int(double value) {
|
||
|
|
if (std::isinf(value)) throw Dyn::Error("OverflowError", "cannot convert float infinity to integer");
|
||
|
|
if (std::isnan(value)) throw Dyn::Error("ValueError", "cannot convert float NaN to integer");
|
||
|
|
if (value >= 9.2e18) return std::numeric_limits<std::int64_t>::max();
|
||
|
|
if (value <= -9.2e18) return std::numeric_limits<std::int64_t>::min();
|
||
|
|
return static_cast<std::int64_t>(value);
|
||
|
|
}
|
||
|
|
|
||
|
|
// `now or time.time()`
|
||
|
|
double NowOr(std::optional<double> now) {
|
||
|
|
return now && *now != 0 ? *now : Platform::Now();
|
||
|
|
}
|
||
|
|
|
||
|
|
// f'{value:g}'
|
||
|
|
std::string G(double value) {
|
||
|
|
return std::format("{:g}", value);
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string Clock(double epoch) {
|
||
|
|
return Platform::LocalTime("%H:%M:%S", epoch);
|
||
|
|
}
|
||
|
|
|
||
|
|
Json ZeroForeign() {
|
||
|
|
return {{"count", 0}, {"cost5h", 0.0}, {"costUsd", 0.0}};
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string Kind(const Json& seat) {
|
||
|
|
return Dyn::Str(Dyn::At(seat, "kind"));
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string Id(const Json& seat) {
|
||
|
|
return Dyn::Str(Dyn::At(seat, "id"));
|
||
|
|
}
|
||
|
|
|
||
|
|
std::int64_t Running(const Json& seat) {
|
||
|
|
return Int(AsDouble(Dyn::At(seat, "running")));
|
||
|
|
}
|
||
|
|
|
||
|
|
const std::string& EnvOf(const std::map<std::string, std::string>& env, const std::string& key) {
|
||
|
|
static const std::string none;
|
||
|
|
const auto found = env.find(key);
|
||
|
|
return found == env.end() ? none : found->second;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool IsOpenRouter(const Json& seat) {
|
||
|
|
return Kind(seat) == "openrouter";
|
||
|
|
}
|
||
|
|
|
||
|
|
double Budget(const Json& s, const Json& a) {
|
||
|
|
return AsDouble(Dyn::At(a, IsOpenRouter(s) ? "orBudgetUsd" : "budgetUsd"));
|
||
|
|
}
|
||
|
|
|
||
|
|
const Json& LensCostValue(const Json& s, const Json& a) {
|
||
|
|
return Dyn::At(a, IsOpenRouter(s) ? "orLensCostUsd" : "lensCostUsd");
|
||
|
|
}
|
||
|
|
|
||
|
|
double LensCost(const Json& s, const Json& a) {
|
||
|
|
return AsDouble(LensCostValue(s, a));
|
||
|
|
}
|
||
|
|
|
||
|
|
std::int64_t Concurrency(const Json& s, const Json& a) {
|
||
|
|
return Int(AsDouble(Dyn::At(a, IsOpenRouter(s) ? "orConcurrency" : "concurrency")));
|
||
|
|
}
|
||
|
|
|
||
|
|
// The windows a seat's utilisation is read from: all but the Fable
|
||
|
|
// one, or all when that leaves none.
|
||
|
|
void Reutil(Json& s) {
|
||
|
|
Json own = Json::object();
|
||
|
|
const Json& windows = s.at("windows");
|
||
|
|
for (auto it = windows.begin(); it != windows.end(); ++it) {
|
||
|
|
if (!Seats::IsFableWindow(it.key())) own[it.key()] = it.value();
|
||
|
|
}
|
||
|
|
const auto [util, binding] = Seats::SeatUtil(own.empty() ? windows : own);
|
||
|
|
s["util"] = util;
|
||
|
|
s["binding"] = binding;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool LongOk(const Json& s) {
|
||
|
|
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")) >= Seats::LongWindowCutoff) return false;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool ApiRoom(const Json& s, const Json& a) {
|
||
|
|
return AsDouble(s.at("spentUsd")) + static_cast<double>(Running(s) + 1) * LensCost(s, a) + AsDouble(Foreign(s).at("costUsd")) <= Budget(s, a);
|
||
|
|
}
|
||
|
|
|
||
|
|
const Json& FiveHour(const Json& s) {
|
||
|
|
return Dyn::Or(Dyn::Get(Dyn::At(s, "windows"), "five_hour"), Dyn::EmptyDict());
|
||
|
|
}
|
||
|
|
|
||
|
|
// ik.split('/', 1)[0]: the driver an in-flight entry belongs to.
|
||
|
|
std::string Owner(std::string_view inflightKey) {
|
||
|
|
return std::string(inflightKey.substr(0, inflightKey.find('/')));
|
||
|
|
}
|
||
|
|
|
||
|
|
// key.split(':')[0]
|
||
|
|
std::string PidOf(std::string_view key) {
|
||
|
|
return std::string(key.substr(0, key.find(':')));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Board._key
|
||
|
|
std::string KeyOf(const Json& seat) {
|
||
|
|
const Json& key = Dyn::Get(seat, "boardKey");
|
||
|
|
return Dyn::Truthy(key) ? Dyn::Str(key) : SeatKey(Kind(seat), Dyn::StrOr(Dyn::Get(seat, "secret"), ""));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Per-token list prices when the catalogue cannot be fetched
|
||
|
|
// (OpenRouter, 2026-10-12).
|
||
|
|
const Json& BuiltInPrices() {
|
||
|
|
static const Json prices = {{"xiaomi/mimo-v2.6-pro", {{"prompt", 0.435e-6}, {"completion", 0.87e-6}, {"cacheRead", 0.0036e-6}, {"cacheWrite", nullptr}}}};
|
||
|
|
return prices;
|
||
|
|
}
|
||
|
|
|
||
|
|
// model -> prices, filled by FetchOpenRouterPrices.
|
||
|
|
std::map<std::string, Json> Prices;
|
||
|
|
std::mutex PricesLock;
|
||
|
|
|
||
|
|
void WriteAll(std::int32_t fd, std::string_view text) {
|
||
|
|
std::size_t written = 0;
|
||
|
|
while (written < text.size()) {
|
||
|
|
const ssize_t step = write(fd, text.data() + written, text.size() - written);
|
||
|
|
if (step < 0) {
|
||
|
|
if (errno == EINTR) continue;
|
||
|
|
const std::int32_t failure = errno;
|
||
|
|
close(fd);
|
||
|
|
throw std::runtime_error(Dyn::ErrnoText(failure));
|
||
|
|
}
|
||
|
|
written += static_cast<std::size_t>(step);
|
||
|
|
}
|
||
|
|
close(fd);
|
||
|
|
}
|
||
|
|
|
||
|
|
// shutil.copyfile: the bytes only, the destination created as open()
|
||
|
|
// creates it.
|
||
|
|
void CopyContents(const fs::path& from, const fs::path& to) {
|
||
|
|
const std::optional<std::string> bytes = Platform::ReadFile(from);
|
||
|
|
if (!bytes) throw std::runtime_error(std::format("{}: {}", Dyn::ErrnoText(ENOENT), Dyn::Repr(from.string())));
|
||
|
|
const std::int32_t fd = open(to.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0666);
|
||
|
|
if (fd < 0) throw std::runtime_error(std::format("{}: {}", Dyn::ErrnoText(errno), Dyn::Repr(to.string())));
|
||
|
|
WriteAll(fd, *bytes);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
bool IsPaid(const Json& seat) {
|
||
|
|
return std::ranges::contains(Paid, Kind(seat));
|
||
|
|
}
|
||
|
|
|
||
|
|
Json ApiConfig(const std::map<std::string, std::string>& env) {
|
||
|
|
auto num = [&](const std::string& key, double fallback) {
|
||
|
|
const std::optional<double> v = Dyn::ParseFloat(EnvOf(env, key));
|
||
|
|
return v && *v >= 0 ? *v : fallback;
|
||
|
|
};
|
||
|
|
return {
|
||
|
|
{"budgetUsd", num("REVIEW_GATES_API_BUDGET_USD", 200.0)},
|
||
|
|
{"lensCostUsd", num("REVIEW_GATES_API_LENS_COST_USD", 1.0)},
|
||
|
|
{"concurrency", Int(num("REVIEW_GATES_API_CONCURRENCY", 8))},
|
||
|
|
{"cycleDay", std::max<std::int64_t>(1, std::min<std::int64_t>(28, Int(num("REVIEW_GATES_API_CYCLE_DAY", 1))))},
|
||
|
|
{"order", EnvOf(env, "REVIEW_GATES_SEAT_ORDER") == "oauth-first" ? "oauth-first" : "api-first"},
|
||
|
|
// OpenRouter seats: a prepaid key, cheap models - a smaller reserve
|
||
|
|
// per running process.
|
||
|
|
{"orBudgetUsd", num("REVIEW_GATES_OPENROUTER_BUDGET_USD", 100.0)},
|
||
|
|
{"orLensCostUsd", num("REVIEW_GATES_OPENROUTER_LENS_COST_USD", 0.1)},
|
||
|
|
{"orConcurrency", Int(num("REVIEW_GATES_OPENROUTER_CONCURRENCY", 16))},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
Json Foreign(const Json& seat) {
|
||
|
|
const Json& foreign = Dyn::Get(seat, "foreign");
|
||
|
|
return Dyn::Truthy(foreign) ? foreign : ZeroForeign();
|
||
|
|
}
|
||
|
|
|
||
|
|
fs::path LedgerPath(const std::optional<fs::path>& scratchRoot) {
|
||
|
|
fs::path root;
|
||
|
|
if (scratchRoot && !scratchRoot->empty()) {
|
||
|
|
root = *scratchRoot;
|
||
|
|
} else if (const std::optional<std::string> configured = Platform::GetEnv("REVIEW_GATES_SCRATCH"); configured && !configured->empty()) {
|
||
|
|
root = *configured;
|
||
|
|
} else {
|
||
|
|
root = fs::path(Platform::Home()) / ".cache" / "review-gates";
|
||
|
|
}
|
||
|
|
return root / "seat-ledger.json";
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string CycleStart(double now, std::int32_t day) {
|
||
|
|
using namespace std::chrono;
|
||
|
|
const std::string today = Platform::LocalTime("%Y-%m-%d", now);
|
||
|
|
const year_month_day date(year(std::stoi(today.substr(0, 4))), month(static_cast<unsigned>(std::stoi(today.substr(5, 2)))),
|
||
|
|
std::chrono::day(static_cast<unsigned>(std::stoi(today.substr(8, 2)))));
|
||
|
|
auto iso = [](const year_month_day& d) {
|
||
|
|
return std::format("{:04d}-{:02d}-{:02d}", static_cast<std::int32_t>(d.year()), static_cast<std::uint32_t>(d.month()), static_cast<std::uint32_t>(d.day()));
|
||
|
|
};
|
||
|
|
if (static_cast<std::int32_t>(static_cast<std::uint32_t>(date.day())) >= day) {
|
||
|
|
return iso(year_month_day(date.year(), date.month(), std::chrono::day(static_cast<std::uint32_t>(day))));
|
||
|
|
}
|
||
|
|
const year_month_day previous(sys_days(year_month_day(date.year(), date.month(), std::chrono::day(1))) - days(1));
|
||
|
|
const auto last = static_cast<std::int32_t>(static_cast<std::uint32_t>(previous.day()));
|
||
|
|
return iso(year_month_day(previous.year(), previous.month(), std::chrono::day(static_cast<std::uint32_t>(std::min(day, last)))));
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---- the ledger -------------------------------------------------------------------
|
||
|
|
Ledger::Ledger(fs::path path, Json apiConfig, Json data) : path(std::move(path)), apiConfig(std::move(apiConfig)), data(Dyn::Truthy(data) ? std::move(data) : Json::object()) {}
|
||
|
|
|
||
|
|
Ledger Ledger::Load(const fs::path& path, const Json& apiConfig) {
|
||
|
|
Json data;
|
||
|
|
try {
|
||
|
|
data = Dyn::LoadJsonFile(path);
|
||
|
|
} catch (const std::exception&) {
|
||
|
|
data = Json::object();
|
||
|
|
}
|
||
|
|
return Ledger(path, apiConfig, data.is_object() ? data : Json::object());
|
||
|
|
}
|
||
|
|
|
||
|
|
Json& Ledger::Entry(const std::string& seatId, std::optional<double> now) {
|
||
|
|
const std::string cycle = CycleStart(NowOr(now), static_cast<std::int32_t>(Int(AsDouble(Dyn::At(apiConfig, "cycleDay")))));
|
||
|
|
const Json& e = Dyn::Get(data, seatId);
|
||
|
|
// Only a newer cycle starts over: an entry of a later one (another
|
||
|
|
// driver's cycle day) is not wiped.
|
||
|
|
if (!Dyn::Truthy(e) || cycle > Dyn::StrOr(Dyn::Get(e, "cycle"), "")) {
|
||
|
|
data[seatId] = {{"cycle", cycle}, {"spentUsd", 0.0}, {"procs", 0}, {"outSince", nullptr}, {"outReason", nullptr}};
|
||
|
|
}
|
||
|
|
return data[seatId];
|
||
|
|
}
|
||
|
|
|
||
|
|
Json Ledger::Update(const std::string& seatId, const std::function<void(Json&)>& change, std::optional<double> now) {
|
||
|
|
// The file is the record: re-read under its lock, this process only
|
||
|
|
// ever adds to it (2026-10-15: each driver saved the copy it had loaded
|
||
|
|
// and the last writer won - api4 reached the credit error with $157.54
|
||
|
|
// recorded of the $199.66 its processes had cost).
|
||
|
|
Json entry;
|
||
|
|
Platform::LockedUpdate(path, [&](Json& file) {
|
||
|
|
// The file wins where both have a seat.
|
||
|
|
Json merged = data;
|
||
|
|
for (auto it = file.begin(); it != file.end(); ++it) merged[it.key()] = it.value();
|
||
|
|
data = std::move(merged);
|
||
|
|
Json& e = Entry(seatId, now);
|
||
|
|
change(e);
|
||
|
|
entry = e;
|
||
|
|
file = data;
|
||
|
|
return true;
|
||
|
|
}, data, 0600, {});
|
||
|
|
return entry;
|
||
|
|
}
|
||
|
|
|
||
|
|
Json Ledger::Add(const std::string& seatId, double usd, std::optional<double> now) {
|
||
|
|
return Update(seatId, [&](Json& e) {
|
||
|
|
e["spentUsd"] = Dyn::Round(AsDouble(Dyn::At(e, "spentUsd")) + usd, 4);
|
||
|
|
e["procs"] = Dyn::Add(Dyn::At(e, "procs"), Json(1));
|
||
|
|
}, now);
|
||
|
|
}
|
||
|
|
|
||
|
|
Json Ledger::MarkOut(const std::string& seatId, std::string_view reason, std::optional<double> now) {
|
||
|
|
return Update(seatId, [&](Json& e) {
|
||
|
|
e["outSince"] = Platform::LocalTime("%Y-%m-%dT%H:%M:%S", NowOr(now));
|
||
|
|
e["outReason"] = Dyn::Slice(reason, 0, 160);
|
||
|
|
}, now);
|
||
|
|
}
|
||
|
|
|
||
|
|
void Ledger::Save() {
|
||
|
|
try {
|
||
|
|
Platform::WriteAtomic(path, DumpFile(data), 0600);
|
||
|
|
} catch (const std::exception&) {
|
||
|
|
// A record, never a dependency.
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const Json& Ledger::Data() const {
|
||
|
|
return data;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---- the seats ---------------------------------------------------------------------
|
||
|
|
Json NewSeat(const std::string& id, const std::string& kind, const std::string& secret) {
|
||
|
|
return {
|
||
|
|
{"id", id},
|
||
|
|
{"kind", kind},
|
||
|
|
{"secret", secret},
|
||
|
|
{"state", "unknown"},
|
||
|
|
{"windows", Json::object()},
|
||
|
|
{"util", nullptr},
|
||
|
|
{"binding", ""},
|
||
|
|
{"resetsAt", nullptr},
|
||
|
|
{"pauseUntil", 0.0},
|
||
|
|
{"running", 0},
|
||
|
|
{"procs", 0},
|
||
|
|
{"spentUsd", 0.0},
|
||
|
|
{"error", nullptr},
|
||
|
|
{"probedWith", nullptr},
|
||
|
|
{"foreign", ZeroForeign()},
|
||
|
|
// The board's view of the seat.
|
||
|
|
{"boardKey", SeatKey(kind, secret)},
|
||
|
|
{"stateFrom", nullptr},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
std::vector<Json> LoadSeats(const std::map<std::string, std::string>& env, const std::optional<fs::path>& envFile, std::string_view specText, Ledger* ledger, const Json& apiConfig) {
|
||
|
|
std::map<std::string, std::string> merged = env;
|
||
|
|
if (envFile && !envFile->empty()) {
|
||
|
|
for (const auto& [key, value] : Seats::SeatTokensFromEnvFile(Dyn::ReadText(*envFile))) merged[key] = value;
|
||
|
|
}
|
||
|
|
std::vector<std::pair<std::string, Json>> pool;
|
||
|
|
for (const auto& [n, key] : Seats::ApiKeysFromEnv(merged)) pool.emplace_back(std::format("api{}", n), NewSeat(std::format("api{}", n), "api", key));
|
||
|
|
for (const auto& [n, token] : Seats::SeatTokensFromEnv(merged)) pool.emplace_back(std::to_string(n), NewSeat(std::to_string(n), "oauth", token));
|
||
|
|
for (const auto& [n, key] : Seats::OpenRouterKeysFromEnv(merged)) pool.emplace_back(std::format("or{}", n), NewSeat(std::format("or{}", n), "openrouter", key));
|
||
|
|
pool.emplace_back("local", NewSeat("local", "local", ""));
|
||
|
|
auto find = [&](std::string_view id) -> const Json* {
|
||
|
|
for (const auto& [key, seat] : pool) {
|
||
|
|
if (key == id) return &seat;
|
||
|
|
}
|
||
|
|
return nullptr;
|
||
|
|
};
|
||
|
|
const std::string spec = Dyn::Strip(specText.empty() ? std::string_view("auto") : specText);
|
||
|
|
std::vector<std::string> ids;
|
||
|
|
if (spec == "auto") {
|
||
|
|
// Never an OpenRouter seat by default.
|
||
|
|
for (const auto& [key, seat] : pool) {
|
||
|
|
if (Kind(seat) == "api" || Kind(seat) == "oauth") ids.push_back(key);
|
||
|
|
}
|
||
|
|
if (ids.empty()) ids.emplace_back("local");
|
||
|
|
} else if (spec == "local" || spec == "api" || spec == "oauth" || spec == "openrouter") {
|
||
|
|
for (const auto& [key, seat] : pool) {
|
||
|
|
if (Kind(seat) == spec) ids.push_back(key);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
std::vector<std::string> bad;
|
||
|
|
for (const std::string& part : Dyn::Split(spec, ",")) {
|
||
|
|
const std::string id = Dyn::Strip(part);
|
||
|
|
if (id.empty()) continue;
|
||
|
|
ids.push_back(id);
|
||
|
|
if (!find(id)) bad.push_back(id);
|
||
|
|
}
|
||
|
|
if (!bad.empty()) {
|
||
|
|
std::vector<std::string> configured;
|
||
|
|
for (const auto& [key, seat] : pool) {
|
||
|
|
if (key != "local") configured.push_back(key);
|
||
|
|
}
|
||
|
|
throw std::invalid_argument(std::format("--seats names seat(s) not configured: {} (configured: {}, local)", Dyn::Join(bad, ", "), Dyn::Join(configured, ", ")));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
std::vector<Json> seats;
|
||
|
|
for (const std::string& id : ids) seats.push_back(*find(id));
|
||
|
|
if (std::ranges::any_of(seats, IsOpenRouter) && !std::ranges::all_of(seats, IsOpenRouter)) {
|
||
|
|
throw std::invalid_argument("--seats: an OpenRouter seat cannot share a pool with Anthropic seats or the local login - a model name means a different model per endpoint");
|
||
|
|
}
|
||
|
|
if (ledger) {
|
||
|
|
const double now = Platform::Now();
|
||
|
|
for (Json& s : seats) {
|
||
|
|
if (!IsPaid(s)) continue;
|
||
|
|
const Json& e = ledger->Entry(Id(s), now);
|
||
|
|
s["spentUsd"] = AsDouble(Dyn::At(e, "spentUsd"));
|
||
|
|
if (Dyn::Truthy(Dyn::Get(e, "outSince"))) {
|
||
|
|
s["state"] = "out";
|
||
|
|
s["error"] = std::format("ledger: {} since {}", Dyn::StrOr(Dyn::Get(e, "outReason"), "out"), Dyn::Slice(Dyn::Str(e.at("outSince")), 0, 16));
|
||
|
|
} else if (Dyn::Truthy(apiConfig) && AsDouble(s.at("spentUsd")) >= Budget(s, apiConfig)) {
|
||
|
|
s["state"] = "out";
|
||
|
|
s["error"] = std::format("ledger: ${:.2f} spent this cycle, budget ${}", AsDouble(s.at("spentUsd")), G(Budget(s, apiConfig)));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return seats;
|
||
|
|
}
|
||
|
|
|
||
|
|
void ApplyProbe(Json& s, const Json& r) {
|
||
|
|
s["state"] = Dyn::Has(r, "state") ? r.at("state") : Json("unknown");
|
||
|
|
s["windows"] = Dyn::Or(Dyn::Get(r, "windows"), Dyn::EmptyDict());
|
||
|
|
s["util"] = Dyn::Get(r, "util");
|
||
|
|
s["binding"] = Dyn::Truthy(Dyn::Get(r, "binding")) ? r.at("binding") : Json("");
|
||
|
|
s["resetsAt"] = Dyn::Get(r, "resetsAt");
|
||
|
|
s["error"] = Dyn::Get(r, "error");
|
||
|
|
s["probedWith"] = Dyn::Get(r, "probedWith");
|
||
|
|
if (Kind(s) == "oauth" && Dyn::Truthy(s.at("windows"))) Reutil(s);
|
||
|
|
if (s.at("state") == "limited" && Dyn::Truthy(s.at("resetsAt"))) s["pauseUntil"] = AsDouble(s.at("resetsAt")) + 30;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string Describe(const Json& s) {
|
||
|
|
if (IsPaid(s)) {
|
||
|
|
std::string state = Dyn::Str(s.at("state"));
|
||
|
|
if (Dyn::Truthy(Dyn::Get(s, "error")) && s.at("state") != "ok") state += std::format(" ({})", Dyn::Str(s.at("error")));
|
||
|
|
const Json& remaining = Dyn::Get(s, "remainingUsd");
|
||
|
|
return std::format("{}, ${:.2f} spent this cycle{}", state, AsDouble(s.at("spentUsd")), remaining.is_null() ? std::string() : std::format(", ${:.2f} left on the key", AsDouble(remaining)));
|
||
|
|
}
|
||
|
|
if (!Dyn::Truthy(s.at("windows"))) {
|
||
|
|
return Dyn::Str(s.at("state")) + (Dyn::Truthy(Dyn::Get(s, "error")) ? std::format(" ({})", Dyn::Str(s.at("error"))) : std::string());
|
||
|
|
}
|
||
|
|
std::vector<std::string> parts;
|
||
|
|
const Json& windows = s.at("windows");
|
||
|
|
for (auto it = windows.begin(); it != windows.end(); ++it) {
|
||
|
|
const Json& u = Dyn::Get(it.value(), "utilization");
|
||
|
|
if (u.is_null()) continue;
|
||
|
|
std::string label = it.key();
|
||
|
|
if (it.key() == "five_hour") {
|
||
|
|
label = "5h";
|
||
|
|
} else if (it.key() == "seven_day") {
|
||
|
|
label = "7d";
|
||
|
|
} else if (it.key() == Seats::FableWindow || Seats::IsFableWindow(it.key())) {
|
||
|
|
label = "fable";
|
||
|
|
}
|
||
|
|
parts.push_back(std::format("{} {}%", label, Seats::Fmt(u)));
|
||
|
|
}
|
||
|
|
const Json& binding = s.at("binding");
|
||
|
|
return std::format("{}: {}{}", Dyn::Str(s.at("state")), Dyn::Join(parts, ", "), Dyn::Truthy(binding) ? std::format(" (binding {})", Dyn::Str(binding)) : std::string());
|
||
|
|
}
|
||
|
|
|
||
|
|
void Probe(std::vector<Json>& seats, const std::string& claudeBin, double timeout, const std::vector<std::string>& models, const Log& log, const ProbeFunction& probe) {
|
||
|
|
const ProbeFunction run = probe ? probe : ProbeFunction([](std::int32_t seat, const std::string& token, const std::string& model, double t, const std::string& bin, bool local, bool api) {
|
||
|
|
return Seats::ProbeSeat(seat, token, {.model = model, .timeoutS = t, .claudeBin = bin, .local = local, .api = api});
|
||
|
|
});
|
||
|
|
// OAuth seats probe on the lens model when that is Fable (its window
|
||
|
|
// is seen only there), else on Haiku; API seats and the local login
|
||
|
|
// on Haiku.
|
||
|
|
std::optional<std::string> fable;
|
||
|
|
for (const std::string& m : models) {
|
||
|
|
if (Dyn::Lower(m).contains("fable")) {
|
||
|
|
fable = m;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (std::ranges::any_of(seats, IsOpenRouter)) {
|
||
|
|
const Json prices = FetchOpenRouterPrices(models, log);
|
||
|
|
for (auto it = prices.begin(); it != prices.end(); ++it) {
|
||
|
|
const Json& p = it.value();
|
||
|
|
if (!Dyn::Truthy(p)) {
|
||
|
|
log(std::format("openrouter: NO PRICE for {} - its spend is not counted", it.key()));
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
log(std::format("openrouter: {} priced ${:.3f}/${:.3f} per M in/out, cache read {}", it.key(), AsDouble(p.at("prompt")) * 1e6, AsDouble(p.at("completion")) * 1e6,
|
||
|
|
Dyn::Get(p, "cacheRead").is_null() ? std::string("n/a") : std::format("${:.4f}", AsDouble(p.at("cacheRead")) * 1e6)));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const std::string lensModel = fable.value_or(std::string(Seats::ProbeModel));
|
||
|
|
for (Json& s : seats) {
|
||
|
|
if (s.at("state") == "out") continue;
|
||
|
|
const std::string kind = Kind(s);
|
||
|
|
const std::string id = Id(s);
|
||
|
|
const std::string secret = Dyn::StrOr(Dyn::Get(s, "secret"), "");
|
||
|
|
if (kind == "openrouter") {
|
||
|
|
const Json r = ProbeOpenRouter(secret, timeout);
|
||
|
|
ApplyProbe(s, r);
|
||
|
|
s["remainingUsd"] = Dyn::Get(r, "remaining");
|
||
|
|
log(std::format("seat {} ({}): {}", id, kind, Describe(s)));
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const std::int32_t n = kind == "api" ? static_cast<std::int32_t>(Dyn::ParseInt(Dyn::Slice(id, 3)).value_or(0)) : (kind == "local" ? 0 : static_cast<std::int32_t>(Dyn::ParseInt(id).value_or(0)));
|
||
|
|
Json r;
|
||
|
|
if (kind == "api") {
|
||
|
|
r = run(n, secret, std::string(Seats::ProbeModel), timeout, claudeBin, false, true);
|
||
|
|
} else if (kind == "local") {
|
||
|
|
r = run(n, "", lensModel, timeout, claudeBin, true, false);
|
||
|
|
} else {
|
||
|
|
r = run(n, secret, lensModel, timeout, claudeBin, false, false);
|
||
|
|
if (fable && Seats::NeedsHaiku(r)) r = Seats::MergeFableProbe(run(n, secret, std::string(Seats::ProbeModel), timeout, claudeBin, false, false), r);
|
||
|
|
}
|
||
|
|
ApplyProbe(s, r);
|
||
|
|
log(std::format("seat {} ({}): {}", id, kind, Describe(s)));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---- placement -------------------------------------------------------------------------
|
||
|
|
Json PoolConfig(const std::map<std::string, std::string>& env) {
|
||
|
|
Json config = Seats::ConfigFromEnv(env);
|
||
|
|
if (Dyn::Strip(EnvOf(env, "REVIEW_GATES_LENS_COST_PCT")).empty()) config["lensCostPct"] = 0.25;
|
||
|
|
return config;
|
||
|
|
}
|
||
|
|
|
||
|
|
double OauthProjected(const Json& s, const Json& config) {
|
||
|
|
// Where the seat's 5-hour 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.
|
||
|
|
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"));
|
||
|
|
}
|
||
|
|
|
||
|
|
std::vector<std::string> ExpireWindows(Json& s, double now) {
|
||
|
|
// 2026-10-13: S153 sat in a 30-second pause loop for 30 minutes after
|
||
|
|
// the 20:20 resets - nothing was running, so no rate event ever
|
||
|
|
// replaced the 83-89 % readings.
|
||
|
|
std::vector<std::string> done;
|
||
|
|
if (!Dyn::Truthy(Dyn::Get(s, "windows"))) return done;
|
||
|
|
for (auto it = s["windows"].begin(); it != s["windows"].end(); ++it) {
|
||
|
|
Json& w = it.value();
|
||
|
|
if (Dyn::Truthy(Dyn::Get(w, "resetsAt")) && AsDouble(w.at("resetsAt")) + 30 <= now && Num(Dyn::Get(w, "utilization")) > 0) {
|
||
|
|
w["utilization"] = 0;
|
||
|
|
w["resetsAt"] = 0;
|
||
|
|
done.push_back(it.key());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return done;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool Eligible(Json& s, double now, const Json& config, const Json& apiConfig) {
|
||
|
|
if (s.at("state") == "out" || s.at("state") == "invalid" || now < AsDouble(s.at("pauseUntil"))) return false;
|
||
|
|
if (IsPaid(s)) {
|
||
|
|
// The cap counts every driver's processes.
|
||
|
|
return Running(s) + Int(AsDouble(Foreign(s).at("count"))) < Concurrency(s, apiConfig) && ApiRoom(s, apiConfig);
|
||
|
|
}
|
||
|
|
ExpireWindows(s, now);
|
||
|
|
// 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;
|
||
|
|
return OauthProjected(s, config) + AsDouble(config.at("lensCostPct")) <= AsDouble(config.at("maxUtil")) && LongOk(s);
|
||
|
|
}
|
||
|
|
|
||
|
|
std::vector<Json*> Ranked(std::vector<Json>& seats, double now, const Json& config, const Json& apiConfig) {
|
||
|
|
std::vector<Json*> api;
|
||
|
|
std::vector<Json*> oauth;
|
||
|
|
for (Json& s : seats) {
|
||
|
|
if (!Eligible(s, now, config, apiConfig)) continue;
|
||
|
|
(IsPaid(s) ? api : oauth).push_back(&s);
|
||
|
|
}
|
||
|
|
auto spend = [&](const Json* s) {
|
||
|
|
return std::pair(AsDouble(s->at("spentUsd")) + static_cast<double>(Running(*s)) * LensCost(*s, apiConfig) + AsDouble(Foreign(*s).at("costUsd")), Id(*s));
|
||
|
|
};
|
||
|
|
std::ranges::stable_sort(api, [&](const Json* a, const Json* b) { return spend(a) < spend(b); });
|
||
|
|
if (config.at("policy") == "pack") {
|
||
|
|
std::ranges::stable_sort(oauth, [](const Json* a, const Json* b) { return std::pair(Kind(*a) != "oauth", Id(*a)) < std::pair(Kind(*b) != "oauth", Id(*b)); });
|
||
|
|
} else {
|
||
|
|
auto projected = [&](const Json* s) { return std::pair(Seats::Quantize(OauthProjected(*s, config)), Id(*s)); };
|
||
|
|
std::ranges::stable_sort(oauth, [&](const Json* a, const Json* b) { return projected(a) < projected(b); });
|
||
|
|
}
|
||
|
|
const bool apiFirst = apiConfig.at("order") == "api-first";
|
||
|
|
std::vector<Json*> out = apiFirst ? api : oauth;
|
||
|
|
const std::vector<Json*>& rest = apiFirst ? oauth : api;
|
||
|
|
out.insert(out.end(), rest.begin(), rest.end());
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
Json* Pick(std::vector<Json>& seats, double now, const Json& config, const Json& apiConfig) {
|
||
|
|
const std::vector<Json*> ranked = Ranked(seats, now, config, apiConfig);
|
||
|
|
return ranked.empty() ? nullptr : ranked.front();
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---- resuming a pass on another seat ------------------------------------------------
|
||
|
|
fs::path SeatDir(const fs::path& scratch, std::string_view seatId) {
|
||
|
|
return scratch / std::format("seat-{}", seatId);
|
||
|
|
}
|
||
|
|
|
||
|
|
std::optional<fs::path> CarryTranscript(const fs::path& scratch, std::string_view fromSeat, std::string_view toSeat, std::string_view sessionId, const Log& log) {
|
||
|
|
// The CLI keeps a session's transcript under
|
||
|
|
// <CLAUDE_CONFIG_DIR>/projects/<cwd slug>/<session id>.jsonl, and
|
||
|
|
// `claude --resume <id>` continues it; every process runs in the
|
||
|
|
// repository, so the slug is the same on the other seat.
|
||
|
|
if (fromSeat.empty() || toSeat.empty() || sessionId.empty() || fromSeat == toSeat) return std::nullopt;
|
||
|
|
// glob(<seat>/projects/*/<session>.jsonl), whose * skips dot names.
|
||
|
|
std::optional<fs::path> source;
|
||
|
|
std::error_code error;
|
||
|
|
for (fs::directory_iterator it(SeatDir(scratch, fromSeat) / "projects", error), end; !error && it != end; it.increment(error)) {
|
||
|
|
if (it->path().filename().string().starts_with('.')) continue;
|
||
|
|
const fs::path candidate = it->path() / std::format("{}.jsonl", sessionId);
|
||
|
|
std::error_code missing;
|
||
|
|
if (fs::exists(fs::symlink_status(candidate, missing))) {
|
||
|
|
source = candidate;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!source) return std::nullopt;
|
||
|
|
const fs::path destination = SeatDir(scratch, toSeat) / "projects" / source->parent_path().filename() / source->filename();
|
||
|
|
Platform::MakeDirs(destination.parent_path(), 0700);
|
||
|
|
CopyContents(*source, destination);
|
||
|
|
if (log) {
|
||
|
|
log(std::format("seat {}: transcript of {} carried over from seat {} ({} bytes) - the pass resumes", toSeat, Dyn::Slice(sessionId, 0, 8), fromSeat, fs::file_size(destination)));
|
||
|
|
}
|
||
|
|
return destination;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool AllOut(const std::vector<Json>& seats) {
|
||
|
|
return std::ranges::all_of(seats, [](const Json& s) { return s.at("state") == "out" || s.at("state") == "invalid"; });
|
||
|
|
}
|
||
|
|
|
||
|
|
std::optional<double> EarliestResume(const std::vector<Json>& seats, double now, std::optional<double> recheck) {
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
if (recheck && AsDouble(Foreign(s).at("count")) > 0) times.push_back(now + *recheck);
|
||
|
|
}
|
||
|
|
if (times.empty()) return std::nullopt;
|
||
|
|
return std::ranges::min(times);
|
||
|
|
}
|
||
|
|
|
||
|
|
double RetryAfter(std::string_view text, double fallback) {
|
||
|
|
// re.search(r'retry[- ]after[^0-9]{0,12}(\d+)', text, re.I): the class
|
||
|
|
// run is greedy, so the digits are the last \d that it leaves.
|
||
|
|
const std::u32string t = Dyn::Decode(text);
|
||
|
|
auto letters = [&](std::size_t at, std::string_view word) {
|
||
|
|
for (std::size_t i = 0; i < word.size(); ++i) {
|
||
|
|
if (Dyn::IgnoreCaseLetter(t[at + i]) != word[i]) return false;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
};
|
||
|
|
for (std::size_t at = 0; at + 11 <= t.size(); ++at) {
|
||
|
|
if (!letters(at, "retry") || (t[at + 5] != U'-' && t[at + 5] != U' ') || !letters(at + 6, "after")) continue;
|
||
|
|
const std::size_t gap = at + 11;
|
||
|
|
std::size_t run = 0;
|
||
|
|
while (run < 12 && gap + run < t.size() && !(t[gap + run] >= U'0' && t[gap + run] <= U'9')) ++run;
|
||
|
|
for (std::size_t k = run + 1; k-- > 0;) {
|
||
|
|
std::size_t end = gap + k;
|
||
|
|
std::int64_t seconds = 0;
|
||
|
|
while (end < t.size() && Dyn::IsDecimal(t[end])) {
|
||
|
|
seconds = std::min<std::int64_t>(1'000'000, seconds * 10 + Dyn::DecimalValue(t[end]));
|
||
|
|
++end;
|
||
|
|
}
|
||
|
|
if (end > gap + k) return static_cast<double>(std::min<std::int64_t>(900, std::max<std::int64_t>(30, seconds + 5)));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return fallback;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool Settle(Json& s, Json& record, double now, const Json& config, const Json& apiConfig, Ledger* ledger, const Log& log) {
|
||
|
|
s["procs"] = Dyn::Add(s.at("procs"), Json(1));
|
||
|
|
const Json& rate = Dyn::Or(Dyn::Get(record, "rateLimit"), Dyn::EmptyDict());
|
||
|
|
if (Kind(s) != "api" && Dyn::Truthy(rate)) {
|
||
|
|
Json windows = Json::object();
|
||
|
|
for (auto it = rate.begin(); it != rate.end(); ++it) {
|
||
|
|
const Json& u = Dyn::Get(it.value(), "utilization");
|
||
|
|
const Json& r = Dyn::Get(it.value(), "resetsAt");
|
||
|
|
windows[it.key()] = {{"utilization", Dyn::Truthy(u) ? u : Json(0)}, {"resetsAt", Dyn::Truthy(r) ? r : Json(0)}};
|
||
|
|
}
|
||
|
|
s["windows"] = std::move(windows);
|
||
|
|
Reutil(s);
|
||
|
|
const Json& five = FiveHour(s);
|
||
|
|
if (Num(Dyn::Get(five, "utilization")) >= AsDouble(config.at("maxUtil")) && Dyn::Truthy(Dyn::Get(five, "resetsAt"))) {
|
||
|
|
s["pauseUntil"] = std::max(AsDouble(s.at("pauseUntil")), AsDouble(five.at("resetsAt")) + 30);
|
||
|
|
if (s.at("state") == "ok") log(std::format("seat {}: five_hour window at {}% - no new launches until its reset", Id(s), Seats::Fmt(five.at("utilization"))));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (IsPaid(s)) {
|
||
|
|
if (IsOpenRouter(s)) {
|
||
|
|
// The harness's own figure is wrong for a model it does not
|
||
|
|
// know (the CLI) or is the catalogue's guess (OpenCode): the
|
||
|
|
// seat's spend is tokens x OpenRouter's list prices, and that
|
||
|
|
// is what the dump sums.
|
||
|
|
record["costUsdHarness"] = Dyn::Get(record, "costUsd");
|
||
|
|
record["costUsd"] = OpenRouterCost(Dyn::StrOr(Dyn::Get(record, "model"), ""), Dyn::Or(Dyn::Get(record, "usage"), Dyn::EmptyDict()));
|
||
|
|
}
|
||
|
|
const double usd = Num(Dyn::Get(record, "costUsd"));
|
||
|
|
s["spentUsd"] = AsDouble(s.at("spentUsd")) + usd;
|
||
|
|
if (ledger) ledger->Add(Id(s), usd, now);
|
||
|
|
const double budget = Budget(s, apiConfig);
|
||
|
|
const double spent = AsDouble(s.at("spentUsd"));
|
||
|
|
if (spent >= budget && s.at("state") != "out" && s.at("state") != "invalid") {
|
||
|
|
s["state"] = "out";
|
||
|
|
s["error"] = std::format("budget: ${:.2f} spent this cycle, budget ${}", spent, G(budget));
|
||
|
|
log(std::format("seat {}: {} - out for the rest of the cycle", Id(s), Dyn::Str(s.at("error"))));
|
||
|
|
} else if (spent >= 0.8 * budget && spent - usd < 0.8 * budget) {
|
||
|
|
log(std::format("seat {}: ${:.2f} of ${} spent this cycle", Id(s), spent, G(budget)));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const Json& status = Dyn::Get(record, "status");
|
||
|
|
const std::string reason = Dyn::StrOr(Dyn::Get(record, "reason"), "");
|
||
|
|
if (status == "credits") {
|
||
|
|
s["state"] = "out";
|
||
|
|
s["error"] = std::format("credit balance exhausted ({})", Dyn::Slice(reason, 0, 80));
|
||
|
|
if (ledger && IsPaid(s)) ledger->MarkOut(Id(s), "credit balance too low", now);
|
||
|
|
log(std::format("seat {}: {} - out, the process is placed again", Id(s), Dyn::Str(s.at("error"))));
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
if (status == "auth") {
|
||
|
|
s["state"] = "invalid";
|
||
|
|
s["error"] = std::format("authentication failure ({})", Dyn::Slice(reason, 0, 80));
|
||
|
|
log(std::format("seat {}: {} - out, the process is placed again", Id(s), Dyn::Str(s.at("error"))));
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
if (status == "limit") {
|
||
|
|
if (IsPaid(s)) {
|
||
|
|
s["pauseUntil"] = std::max(AsDouble(s.at("pauseUntil")), now + RetryAfter(reason, ApiBackoffS));
|
||
|
|
log(std::format("seat {}: rate limited - paused {} s, the process is placed again", Id(s), Int(AsDouble(s.at("pauseUntil")) - now)));
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
const std::string rateType = Dyn::StrOr(Dyn::Get(record, "rateType"), "");
|
||
|
|
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";
|
||
|
|
s["error"] = std::format("seven-day window rejected ({})", rateType.empty() ? Dyn::Str(binding) : rateType);
|
||
|
|
log(std::format("seat {}: {} - out for the run, the process is placed again", Id(s), Dyn::Str(s.at("error"))));
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
const Json& five = FiveHour(s);
|
||
|
|
s["pauseUntil"] = std::max(AsDouble(s.at("pauseUntil")), Dyn::Truthy(Dyn::Get(five, "resetsAt")) ? AsDouble(five.at("resetsAt")) + 30 : now + OauthFallbackPauseS);
|
||
|
|
s["state"] = "limited";
|
||
|
|
log(std::format("seat {}: limit ({}) - paused until {}, the process is placed again", Id(s), Dyn::Slice(reason, 0, 60), Clock(AsDouble(s.at("pauseUntil")))));
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
if (s.at("state") == "limited" && now >= AsDouble(s.at("pauseUntil"))) s["state"] = "ok";
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---- the child's credentials -----------------------------------------------------------
|
||
|
|
fs::path WriteCredentials(const fs::path& configDir, std::string_view token) {
|
||
|
|
// A setup-token is long-lived and never refreshed by the CLI: the
|
||
|
|
// expiry only has to be far away.
|
||
|
|
Platform::MakeDirs(configDir, 0700);
|
||
|
|
const Json body = {{"claudeAiOauth", {{"accessToken", std::string(token)}, {"expiresAt", Int((Platform::Now() + 365 * 86400) * 1000)}, {"scopes", Json::array({"user:inference"})}}}};
|
||
|
|
const fs::path path = configDir / ".credentials.json";
|
||
|
|
// One temp name per writer: the pool's threads launch a fresh seat's
|
||
|
|
// first passes in the same instant, each sees no credentials file yet
|
||
|
|
// and each writes one - with a pid-only name one rename found the temp
|
||
|
|
// file gone (S115, S116, S119 on 2026-10-10). The contents are
|
||
|
|
// identical, so the last rename to win is fine.
|
||
|
|
const fs::path temp = std::format("{}.{}.{}.tmp", path.string(), Platform::PublishedPid(), Uuid4().substr(0, 8));
|
||
|
|
const std::int32_t fd = open(temp.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0600);
|
||
|
|
if (fd < 0) throw std::runtime_error(std::format("{}: {}", Dyn::ErrnoText(errno), Dyn::Repr(temp.string())));
|
||
|
|
WriteAll(fd, std::format("{}\n", DumpSpaced(body)));
|
||
|
|
fs::rename(temp, path);
|
||
|
|
return path;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::map<std::string, std::string> ChildEnv(const Json& seat, const std::map<std::string, std::string>& base, const fs::path& scratch) {
|
||
|
|
// `local`: the driver's environment as it is (the machine's own
|
||
|
|
// login). OAuth: the seat's config dir with its credentials file, no
|
||
|
|
// token variable. API: the key in ANTHROPIC_API_KEY plus a config dir
|
||
|
|
// of its own that approves that key, so no login on the machine is
|
||
|
|
// picked up.
|
||
|
|
std::map<std::string, std::string> env = base;
|
||
|
|
// No Anthropic seat ever inherits another endpoint or credential from
|
||
|
|
// the driver's environment.
|
||
|
|
for (const std::string_view key : {"ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_BASE_URL"}) env.erase(std::string(key));
|
||
|
|
const std::string kind = Kind(seat);
|
||
|
|
if (kind == "local") return env;
|
||
|
|
const fs::path dir = scratch / std::format("seat-{}", Id(seat));
|
||
|
|
Platform::MakeDirs(dir, 0700);
|
||
|
|
env["CLAUDE_CONFIG_DIR"] = dir.string();
|
||
|
|
const std::string secret = Dyn::StrOr(Dyn::Get(seat, "secret"), "");
|
||
|
|
if (kind == "oauth") {
|
||
|
|
if (!fs::exists(dir / ".credentials.json")) WriteCredentials(dir, secret);
|
||
|
|
} else if (kind == "openrouter") {
|
||
|
|
// The CLI against OpenRouter's Anthropic-format endpoint
|
||
|
|
// (/api/v1/messages), the key as a bearer token. Verified
|
||
|
|
// 2026-10-12 (CLI 2.1.296, xiaomi/mimo-v2.6-pro): tools, thinking,
|
||
|
|
// cache reads and --json-schema all work; `--effort` does not
|
||
|
|
// reach the model, and the CLI's total_cost_usd is wrong.
|
||
|
|
env["ANTHROPIC_BASE_URL"] = std::string(OpenRouterBaseUrl);
|
||
|
|
env["ANTHROPIC_AUTH_TOKEN"] = secret;
|
||
|
|
env.erase("CLAUDE_CODE_ENTRYPOINT");
|
||
|
|
// hasCompletedOnboarding, so --print asks nothing.
|
||
|
|
Seats::WriteApiKeyApproval(dir, secret);
|
||
|
|
} else {
|
||
|
|
env["ANTHROPIC_API_KEY"] = secret;
|
||
|
|
// The launcher's CLAUDE_CODE_ENTRYPOINT=claude-vscode is what broke
|
||
|
|
// S111's API seats (2026-10-08, CLI 2.1.294): with it the key is
|
||
|
|
// refused ("Not logged in") and, once approved, the subscriber
|
||
|
|
// credits answer "Credit balance is too low". Bisected over every
|
||
|
|
// CLAUDE_*/ANTHROPIC_* variable: only this one matters.
|
||
|
|
env.erase("CLAUDE_CODE_ENTRYPOINT");
|
||
|
|
Seats::WriteApiKeyApproval(dir, secret);
|
||
|
|
}
|
||
|
|
return env;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---- OpenRouter seats ------------------------------------------------------------------
|
||
|
|
std::string ModelKey(std::string_view model) {
|
||
|
|
return model.starts_with("openrouter/") ? std::string(model.substr(11)) : std::string(model);
|
||
|
|
}
|
||
|
|
|
||
|
|
Json FetchOpenRouterPrices(const std::vector<std::string>& models, const Log& log, double timeout) {
|
||
|
|
std::set<std::string> want;
|
||
|
|
for (const std::string& m : models) {
|
||
|
|
if (!m.empty()) want.insert(ModelKey(m));
|
||
|
|
}
|
||
|
|
Json data = Json::array();
|
||
|
|
try {
|
||
|
|
const Json catalogue = Http::JsonBody(Http::UrlOpen(OpenRouterModelsUrl, {}, timeout));
|
||
|
|
data = Dyn::Or(Dyn::Get(catalogue, "data"), Dyn::EmptyList());
|
||
|
|
} catch (const std::exception& error) {
|
||
|
|
if (log) log(std::format("openrouter: price catalogue not fetched ({}) - list prices from the built-in table", Dyn::Slice(error.what(), 0, 80)));
|
||
|
|
}
|
||
|
|
const std::lock_guard guard(PricesLock);
|
||
|
|
for (const Json& m : data) {
|
||
|
|
const Json& id = Dyn::Get(m, "id");
|
||
|
|
if (!id.is_string() || !want.contains(id.get<std::string>())) continue;
|
||
|
|
const Json& p = Dyn::Or(Dyn::Get(m, "pricing"), Dyn::EmptyDict());
|
||
|
|
const Json& cacheRead = Dyn::Get(p, "input_cache_read");
|
||
|
|
const Json& cacheWrite = Dyn::Get(p, "input_cache_write");
|
||
|
|
Prices[id.get<std::string>()] = {
|
||
|
|
{"prompt", Num(Dyn::Get(p, "prompt"))},
|
||
|
|
{"completion", Num(Dyn::Get(p, "completion"))},
|
||
|
|
{"cacheRead", cacheRead.is_null() ? Json() : Json(AsDouble(cacheRead))},
|
||
|
|
{"cacheWrite", cacheWrite.is_null() ? Json() : Json(AsDouble(cacheWrite))},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
Json out = Json::object();
|
||
|
|
for (const std::string& key : want) {
|
||
|
|
if (!Prices.contains(key) && BuiltInPrices().contains(key)) Prices[key] = BuiltInPrices().at(key);
|
||
|
|
out[key] = Prices.contains(key) ? Prices.at(key) : Json();
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
double OpenRouterCost(std::string_view model, const Json& usage) {
|
||
|
|
// Uncached input and cache writes at the prompt price (a cache write
|
||
|
|
// costs nothing extra on OpenRouter unless the catalogue says so),
|
||
|
|
// cache reads and output at theirs. 0 for a model without a price.
|
||
|
|
Json p;
|
||
|
|
{
|
||
|
|
const std::lock_guard guard(PricesLock);
|
||
|
|
const auto found = Prices.find(ModelKey(model));
|
||
|
|
if (found != Prices.end()) p = found->second;
|
||
|
|
}
|
||
|
|
if (!Dyn::Truthy(p)) p = Dyn::Get(BuiltInPrices(), ModelKey(model));
|
||
|
|
if (!Dyn::Truthy(p)) return 0.0;
|
||
|
|
const double prompt = AsDouble(p.at("prompt"));
|
||
|
|
const double cacheRead = Dyn::Get(p, "cacheRead").is_null() ? prompt : AsDouble(p.at("cacheRead"));
|
||
|
|
const double cacheWrite = Dyn::Get(p, "cacheWrite").is_null() ? prompt : AsDouble(p.at("cacheWrite"));
|
||
|
|
const Json& u = Dyn::Or(usage, Dyn::EmptyDict());
|
||
|
|
double total = Num(Dyn::Get(u, "input")) * prompt;
|
||
|
|
total = total + Num(Dyn::Get(u, "cacheCreation")) * cacheWrite;
|
||
|
|
total = total + Num(Dyn::Get(u, "cacheRead")) * cacheRead;
|
||
|
|
total = total + Num(Dyn::Get(u, "output")) * AsDouble(p.at("completion"));
|
||
|
|
return Dyn::Round(total, 6);
|
||
|
|
}
|
||
|
|
|
||
|
|
Json ProbeOpenRouter(const std::string& secret, double timeout) {
|
||
|
|
Json d;
|
||
|
|
try {
|
||
|
|
const Json body = Http::JsonBody(Http::UrlOpen(OpenRouterKeyUrl, {{"Authorization", std::format("Bearer {}", secret)}}, timeout));
|
||
|
|
d = Dyn::Or(Dyn::Get(body, "data"), Dyn::EmptyDict());
|
||
|
|
} catch (const Http::HttpError& error) {
|
||
|
|
return {{"state", error.Code() == 401 || error.Code() == 403 ? "invalid" : "unknown"}, {"error", std::format("HTTP {} from {}", error.Code(), OpenRouterKeyUrl)}};
|
||
|
|
} catch (const std::exception& error) {
|
||
|
|
return {{"state", "unknown"}, {"error", std::format("probe failed: {}", Dyn::Slice(error.what(), 0, 100))}};
|
||
|
|
}
|
||
|
|
const Json& remaining = Dyn::Get(d, "limit_remaining");
|
||
|
|
if (!remaining.is_null() && AsDouble(remaining) <= 0) {
|
||
|
|
return {{"state", "out"}, {"error", std::format("key limit reached (${:.2f} of ${} used)", Num(Dyn::Get(d, "usage")), G(Num(Dyn::Get(d, "limit"))))}, {"remaining", remaining}};
|
||
|
|
}
|
||
|
|
return {{"state", "ok"}, {"remaining", remaining}, {"usage", Dyn::Get(d, "usage")}, {"limit", Dyn::Get(d, "limit")}};
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---- the board -------------------------------------------------------------------------
|
||
|
|
fs::path BoardPath() {
|
||
|
|
if (const std::optional<std::string> configured = Platform::GetEnv("REVIEW_GATES_BOARD"); configured && !configured->empty()) return *configured;
|
||
|
|
return fs::path(Platform::Home()) / ".cache" / "review-gates" / BoardFile;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string SeatKey(std::string_view kind, std::string_view secret) {
|
||
|
|
if (kind == "local" || secret.empty()) return "local";
|
||
|
|
return std::format("{}:{}", kind, Sha256Hex(secret).substr(0, 12));
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string MyKey() {
|
||
|
|
if (const std::optional<Platform::Identity> self = Platform::Self()) return self->Key();
|
||
|
|
return std::format("{}:0", Platform::PublishedPid());
|
||
|
|
}
|
||
|
|
|
||
|
|
bool DriverAlive(std::string_view key, const fs::path& boardDir, bool reap) {
|
||
|
|
const std::vector<std::string> parts = Dyn::Split(key, ":", 1);
|
||
|
|
const std::optional<std::int64_t> pid = parts.size() == 2 ? Dyn::ParseInt(parts[0]) : std::nullopt;
|
||
|
|
const std::optional<std::int64_t> start = parts.size() == 2 ? Dyn::ParseInt(parts[1]) : std::nullopt;
|
||
|
|
if (!pid || !start) return false;
|
||
|
|
const fs::path locks = boardDir / "drivers";
|
||
|
|
if (const std::optional<bool> held = Platform::ProbeLifetimeLock(locks, *pid, *start)) {
|
||
|
|
if (!*held && reap) {
|
||
|
|
// Its driver died; nobody can take that name again.
|
||
|
|
std::error_code error;
|
||
|
|
fs::remove(Platform::LifetimeLockFile(locks, *pid, *start), error);
|
||
|
|
}
|
||
|
|
return *held;
|
||
|
|
}
|
||
|
|
// No lock file: a driver that takes none. In the sandbox a pid is
|
||
|
|
// another namespace's and says nothing.
|
||
|
|
if (Platform::InSandbox()) return true;
|
||
|
|
const std::optional<std::int64_t> running = Platform::ProcStart(*pid);
|
||
|
|
// No /proc at all: the deadline sweep is the backstop.
|
||
|
|
if (!running) return !fs::is_directory(Platform::Paths().proc);
|
||
|
|
return *running == *start;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool ClaimScratch(const fs::path& path) {
|
||
|
|
// 2026-10-18: two test runs launched seconds apart on one hash shared
|
||
|
|
// `<hash8>-run-2` and its out/ files - the outputs rule alone saw an
|
||
|
|
// empty out/ in the first seconds.
|
||
|
|
fs::create_directories(path);
|
||
|
|
const fs::path marker = path / ".driver";
|
||
|
|
const std::string me = MyKey();
|
||
|
|
std::int32_t fd = open(marker.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0644);
|
||
|
|
if (fd < 0) {
|
||
|
|
if (errno != EEXIST) throw std::runtime_error(std::format("{}: {}", Dyn::ErrnoText(errno), Dyn::Repr(marker.string())));
|
||
|
|
const std::vector<std::string> holder = Dyn::Split(Dyn::Strip(Platform::ReadFile(marker).value_or("")), ":", 1);
|
||
|
|
const std::optional<std::int64_t> pid = holder.size() == 2 ? Dyn::ParseInt(holder[0]) : std::nullopt;
|
||
|
|
const std::optional<std::int64_t> start = holder.size() == 2 ? Dyn::ParseInt(holder[1]) : std::nullopt;
|
||
|
|
if (pid && start && *pid != 0 && *pid != Platform::PublishedPid() && DriverAlive(std::format("{}:{}", *pid, *start), BoardPath().parent_path())) return false;
|
||
|
|
fd = open(marker.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
|
||
|
|
if (fd < 0) throw std::runtime_error(std::format("{}: {}", Dyn::ErrnoText(errno), Dyn::Repr(marker.string())));
|
||
|
|
}
|
||
|
|
WriteAll(fd, me);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::vector<std::string> MergeWindows(Json& into, const Json& other, double now) {
|
||
|
|
// A window whose reset has passed is ignored on either side; of the
|
||
|
|
// rest the newer resetsAt wins, and within one reset epoch the higher
|
||
|
|
// utilization: utilization never falls inside an epoch (2026-10-18:
|
||
|
|
// 1948 rate_limit_events of three runs, 942 rises, 0 falls), so the
|
||
|
|
// merge is exact without a timestamp.
|
||
|
|
std::vector<std::string> changed;
|
||
|
|
const Json& readings = Dyn::Or(other, Dyn::EmptyDict());
|
||
|
|
for (auto it = readings.begin(); it != readings.end(); ++it) {
|
||
|
|
const Json& w = it.value();
|
||
|
|
if (!w.is_object() || Dyn::Get(w, "utilization").is_null()) continue;
|
||
|
|
const double r = Num(Dyn::Get(w, "resetsAt"));
|
||
|
|
if (r + 30 <= now) continue;
|
||
|
|
const Json& found = Dyn::Get(into, it.key());
|
||
|
|
const Json* current = found.is_object() ? &found : nullptr;
|
||
|
|
const double cr = current ? Num(Dyn::Get(*current, "resetsAt")) : 0.0;
|
||
|
|
if (!current || Dyn::Get(*current, "utilization").is_null() || cr + 30 <= now || r > cr || (r == cr && AsDouble(w.at("utilization")) > AsDouble(current->at("utilization")))) {
|
||
|
|
into[it.key()] = {{"utilization", w.at("utilization")}, {"resetsAt", Dyn::Get(w, "resetsAt")}};
|
||
|
|
changed.push_back(it.key());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return changed;
|
||
|
|
}
|
||
|
|
|
||
|
|
Board::Board(fs::path path, std::string me, std::optional<std::string> boot, Log log)
|
||
|
|
: path(std::move(path)), me(me.empty() ? MyKey() : std::move(me)), boot(boot ? *boot : Platform::BootId()), log(log ? std::move(log) : Log([](const std::string&) {})) {
|
||
|
|
data = {{"version", 1}, {"drivers", Json::object()}, {"seats", Json::object()}};
|
||
|
|
}
|
||
|
|
|
||
|
|
void Board::Warn(const std::string& message) {
|
||
|
|
if (warned) return;
|
||
|
|
warned = true;
|
||
|
|
log(std::format("board: {} - placement continues on this driver's own view", message));
|
||
|
|
}
|
||
|
|
|
||
|
|
Json Board::Update(const std::function<bool(Json&)>& change) {
|
||
|
|
data = Platform::LockedUpdate(path, [&](Json& d) {
|
||
|
|
if (!Dyn::Has(d, "version")) d["version"] = 1;
|
||
|
|
if (!Dyn::Has(d, "drivers")) d["drivers"] = Json::object();
|
||
|
|
if (!Dyn::Has(d, "seats")) d["seats"] = Json::object();
|
||
|
|
return change(d);
|
||
|
|
}, data, 0600, [this](const std::string& message) { Warn(message); });
|
||
|
|
return data;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool Board::Alive(std::string_view key, const Json& drivers, bool reap) const {
|
||
|
|
if (key == me) return true;
|
||
|
|
const std::vector<std::string> parts = Dyn::Split(key, ":", 1);
|
||
|
|
if (parts.size() != 2 || !Dyn::ParseInt(parts[0]) || !Dyn::ParseInt(parts[1])) return false;
|
||
|
|
const Json& d = Dyn::Or(Dyn::Get(drivers, key), Dyn::EmptyDict());
|
||
|
|
if (!boot.empty() && Dyn::Truthy(Dyn::Get(d, "boot")) && d.at("boot") != boot) return false;
|
||
|
|
return DriverAlive(key, path.parent_path(), reap);
|
||
|
|
}
|
||
|
|
|
||
|
|
std::pair<bool, std::vector<std::string>> Board::Sweep(Json& d, double now, bool reap) const {
|
||
|
|
std::vector<std::string> lines;
|
||
|
|
bool changed = false;
|
||
|
|
Json& drivers = d["drivers"];
|
||
|
|
std::vector<std::string> dead;
|
||
|
|
for (auto it = drivers.begin(); it != drivers.end(); ++it) {
|
||
|
|
if (!Alive(it.key(), drivers, reap)) dead.push_back(it.key());
|
||
|
|
}
|
||
|
|
for (const std::string& key : dead) {
|
||
|
|
drivers.erase(key);
|
||
|
|
changed = true;
|
||
|
|
}
|
||
|
|
std::vector<std::string> gone;
|
||
|
|
Json& seats = d["seats"];
|
||
|
|
for (auto it = seats.begin(); it != seats.end(); ++it) {
|
||
|
|
Json& s = it.value();
|
||
|
|
if (!Dyn::Get(s, "inflight").is_object()) s["inflight"] = Json::object();
|
||
|
|
Json& inflight = s["inflight"];
|
||
|
|
// {owner: entries}, in the order they were met.
|
||
|
|
std::vector<std::pair<std::string, std::int64_t>> deadOwners;
|
||
|
|
std::vector<std::pair<std::string, std::int64_t>> lateOwners;
|
||
|
|
auto count = [](std::vector<std::pair<std::string, std::int64_t>>& list, const std::string& owner) {
|
||
|
|
for (auto& [o, n] : list) {
|
||
|
|
if (o == owner) {
|
||
|
|
++n;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
list.emplace_back(owner, 1);
|
||
|
|
};
|
||
|
|
std::vector<std::string> drop;
|
||
|
|
for (auto e = inflight.begin(); e != inflight.end(); ++e) {
|
||
|
|
const std::string owner = Owner(e.key());
|
||
|
|
if (owner != me && !drivers.contains(owner)) {
|
||
|
|
count(deadOwners, owner);
|
||
|
|
drop.push_back(e.key());
|
||
|
|
} else if (Num(Dyn::Get(e.value(), "deadline")) <= now) {
|
||
|
|
count(lateOwners, owner);
|
||
|
|
drop.push_back(e.key());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for (const std::string& key : drop) inflight.erase(key);
|
||
|
|
const std::string name = Dyn::StrOr(Dyn::Get(s, "id"), it.key());
|
||
|
|
for (const auto& [owner, n] : deadOwners) lines.push_back(std::format("swept pid {} (dead) with {} in-flight entr{} on seat {}", PidOf(owner), n, n == 1 ? "y" : "ies", name));
|
||
|
|
for (const auto& [owner, n] : lateOwners) lines.push_back(std::format("swept {} in-flight entr{} of pid {} past their deadline on seat {}", n, n == 1 ? "y" : "ies", PidOf(owner), name));
|
||
|
|
const Json& state = Dyn::Get(s, "state");
|
||
|
|
if (Dyn::Truthy(state)) {
|
||
|
|
const Json& until = Dyn::Get(state, "until");
|
||
|
|
const Json& by = Dyn::Get(state, "by");
|
||
|
|
if (!until.is_null() ? AsDouble(until) <= now : (by != me && !(by.is_string() && drivers.contains(by.get<std::string>())))) {
|
||
|
|
s["state"] = nullptr;
|
||
|
|
changed = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
bool liveWindow = false;
|
||
|
|
for (const Json& w : Dyn::Or(Dyn::Get(s, "windows"), Dyn::EmptyDict())) liveWindow = liveWindow || (w.is_object() && Num(Dyn::Get(w, "resetsAt")) + 30 > now);
|
||
|
|
if (inflight.empty() && !Dyn::Truthy(Dyn::Get(s, "state")) && !liveWindow && Num(Dyn::Get(s, "pauseUntil")) <= now) gone.push_back(it.key());
|
||
|
|
}
|
||
|
|
for (const std::string& key : gone) {
|
||
|
|
seats.erase(key);
|
||
|
|
changed = true;
|
||
|
|
}
|
||
|
|
return {changed || !lines.empty(), lines};
|
||
|
|
}
|
||
|
|
|
||
|
|
Json& Board::Record(Json& d, const Json& seat) const {
|
||
|
|
const std::string key = KeyOf(seat);
|
||
|
|
Json& seats = d["seats"];
|
||
|
|
if (!seats.contains(key) || seats[key].is_null()) {
|
||
|
|
seats[key] = {{"id", seat.at("id")}, {"kind", seat.at("kind")}, {"windows", Json::object()}, {"pauseUntil", 0.0}, {"state", nullptr}, {"inflight", Json::object()}};
|
||
|
|
}
|
||
|
|
return seats[key];
|
||
|
|
}
|
||
|
|
|
||
|
|
bool Board::StateHolds(const Json& state, const Json& d, double now) {
|
||
|
|
const Json& until = Dyn::Get(state, "until");
|
||
|
|
if (!until.is_null()) return AsDouble(until) > now;
|
||
|
|
const Json& by = Dyn::Get(state, "by");
|
||
|
|
return by.is_string() && d.at("drivers").contains(by.get<std::string>());
|
||
|
|
}
|
||
|
|
|
||
|
|
Others Board::OthersOf(const Json& d) const {
|
||
|
|
// Memory and stacks are the machine's, not a seat's: every other
|
||
|
|
// driver's pass counts, whatever account it runs on.
|
||
|
|
Others o;
|
||
|
|
for (const Json& b : d.at("seats")) {
|
||
|
|
const Json& inflight = Dyn::Or(Dyn::Get(b, "inflight"), Dyn::EmptyDict());
|
||
|
|
for (auto it = inflight.begin(); it != inflight.end(); ++it) {
|
||
|
|
if (Owner(it.key()) == me) continue;
|
||
|
|
++o.count;
|
||
|
|
if (Dyn::Get(it.value(), "mb").is_null()) ++o.unmeasured;
|
||
|
|
o.mb += Num(Dyn::Get(it.value(), "mb"));
|
||
|
|
if (Dyn::Truthy(Dyn::Get(it.value(), "stack"))) o.stacks.insert(Dyn::Str(it.value().at("stack")));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
o.mb = Dyn::Round(o.mb, 1);
|
||
|
|
return o;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::pair<bool, std::vector<std::string>> Board::FoldInto(std::vector<Json>& seats, double now, Json& d) {
|
||
|
|
auto [changed, lines] = Sweep(d, now);
|
||
|
|
others = OthersOf(d);
|
||
|
|
for (Json& s : seats) {
|
||
|
|
Json f = ZeroForeign();
|
||
|
|
f["drivers"] = Json::object();
|
||
|
|
const Json& b = Dyn::Get(d.at("seats"), KeyOf(s));
|
||
|
|
if (Dyn::Truthy(b)) {
|
||
|
|
const Json& inflight = Dyn::Or(Dyn::Get(b, "inflight"), Dyn::EmptyDict());
|
||
|
|
std::int64_t count = 0;
|
||
|
|
double cost5h = 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"));
|
||
|
|
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["costUsd"] = costUsd;
|
||
|
|
if (!IsPaid(s) && !MergeWindows(s["windows"], Dyn::Get(b, "windows"), now).empty()) Reutil(s);
|
||
|
|
const double pause = Num(Dyn::Get(b, "pauseUntil"));
|
||
|
|
if (pause > Num(Dyn::Get(s, "pauseUntil"))) {
|
||
|
|
s["pauseUntil"] = pause;
|
||
|
|
if (pause > now) lines.push_back(std::format("seat {}: paused until {} - another driver's limit", Id(s), Clock(pause)));
|
||
|
|
}
|
||
|
|
const Json& st = Dyn::Get(b, "state");
|
||
|
|
if (Dyn::Truthy(st) && std::ranges::contains(StateShared, Kind(s)) && s.at("state") != "out" && s.at("state") != "invalid" && StateHolds(st, d, now)) {
|
||
|
|
s["state"] = Dyn::At(st, "state");
|
||
|
|
s["stateFrom"] = Dyn::Get(st, "by");
|
||
|
|
const Json& error = Dyn::Get(st, "error");
|
||
|
|
s["error"] = std::format("{} (seen by pid {})", Dyn::Truthy(error) ? Dyn::Str(error) : Dyn::Str(st.at("state")), PidOf(Dyn::StrOr(Dyn::Get(st, "by"), "?")));
|
||
|
|
lines.push_back(std::format("seat {}: {} - {}", Id(s), Dyn::Str(s.at("state")), Dyn::Str(s.at("error"))));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
f["cost5h"] = Dyn::Round(AsDouble(f.at("cost5h")), 4);
|
||
|
|
f["costUsd"] = Dyn::Round(AsDouble(f.at("costUsd")), 4);
|
||
|
|
s["foreign"] = std::move(f);
|
||
|
|
}
|
||
|
|
return {changed, lines};
|
||
|
|
}
|
||
|
|
|
||
|
|
bool Board::PublishSeat(Json& d, const Json& s, double now, bool probed) const {
|
||
|
|
// Readings merged, pause raised, an out/invalid verdict of our own
|
||
|
|
// written (OAuth and local seats; a paid seat's credit-out is the
|
||
|
|
// ledger's, its budget-out this driver's alone); a seat our own probe
|
||
|
|
// read ok clears a verdict another driver left.
|
||
|
|
Json& b = Record(d, s);
|
||
|
|
bool changed = !Dyn::Equal(Dyn::Get(b, "id"), s.at("id"));
|
||
|
|
b["id"] = s.at("id");
|
||
|
|
if (!IsPaid(s)) {
|
||
|
|
if (!Dyn::Has(b, "windows")) b["windows"] = Json::object();
|
||
|
|
if (!MergeWindows(b["windows"], Dyn::Get(s, "windows"), now).empty()) changed = true;
|
||
|
|
}
|
||
|
|
if (Num(Dyn::Get(s, "pauseUntil")) > Num(Dyn::Get(b, "pauseUntil"))) {
|
||
|
|
b["pauseUntil"] = AsDouble(s.at("pauseUntil"));
|
||
|
|
changed = true;
|
||
|
|
}
|
||
|
|
if (std::ranges::contains(StateShared, Kind(s))) {
|
||
|
|
const Json& state = s.at("state");
|
||
|
|
const Json& held = Dyn::Get(b, "state");
|
||
|
|
if ((state == "out" || state == "invalid") && !Dyn::Truthy(Dyn::Get(s, "stateFrom")) && (!Dyn::Truthy(held) || Dyn::Get(held, "by") == me)) {
|
||
|
|
Json until;
|
||
|
|
if (state == "out") {
|
||
|
|
// A seven-day rejection stands until that window resets.
|
||
|
|
const Json& seven = Dyn::Or(Dyn::Get(Dyn::Or(Dyn::Get(s, "windows"), Dyn::EmptyDict()), "seven_day"), Dyn::EmptyDict());
|
||
|
|
if (Dyn::Truthy(Dyn::Get(seven, "resetsAt"))) until = AsDouble(seven.at("resetsAt")) + 30;
|
||
|
|
}
|
||
|
|
const Json record = {{"state", state}, {"error", Dyn::Slice(Dyn::StrOr(Dyn::Get(s, "error"), ""), 0, 160)}, {"until", until}, {"by", me}};
|
||
|
|
if (!Dyn::Equal(held, record)) {
|
||
|
|
b["state"] = record;
|
||
|
|
changed = true;
|
||
|
|
}
|
||
|
|
} else if (probed && state == "ok" && Dyn::Truthy(held)) {
|
||
|
|
b["state"] = nullptr;
|
||
|
|
changed = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return changed;
|
||
|
|
}
|
||
|
|
|
||
|
|
void Board::Join(const Json& info, double now) {
|
||
|
|
Update([&](Json& d) {
|
||
|
|
Sweep(d, now);
|
||
|
|
Json entry = info;
|
||
|
|
entry["since"] = now;
|
||
|
|
entry["boot"] = boot;
|
||
|
|
d["drivers"][me] = std::move(entry);
|
||
|
|
return true;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
void Board::Leave() {
|
||
|
|
try {
|
||
|
|
Update([&](Json& d) {
|
||
|
|
bool changed = d["drivers"].erase(me) > 0;
|
||
|
|
for (auto it = d["seats"].begin(); it != d["seats"].end(); ++it) {
|
||
|
|
if (!Dyn::Get(it.value(), "inflight").is_object()) continue;
|
||
|
|
Json& inflight = it.value()["inflight"];
|
||
|
|
std::vector<std::string> mine;
|
||
|
|
for (auto e = inflight.begin(); e != inflight.end(); ++e) {
|
||
|
|
if (e.key().starts_with(std::format("{}/", me))) mine.push_back(e.key());
|
||
|
|
}
|
||
|
|
for (const std::string& key : mine) {
|
||
|
|
inflight.erase(key);
|
||
|
|
changed = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return changed;
|
||
|
|
});
|
||
|
|
} catch (const std::exception& error) {
|
||
|
|
Warn(std::format("leave failed ({})", error.what()));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void Board::Publish(const std::vector<Json>& seats, double now, bool probed) {
|
||
|
|
Update([&](Json& d) {
|
||
|
|
bool changed = Sweep(d, now).first;
|
||
|
|
for (const Json& s : seats) changed = PublishSeat(d, s, now, probed) || changed;
|
||
|
|
return changed;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
std::vector<std::string> Board::Fold(std::vector<Json>& seats, double now) {
|
||
|
|
std::vector<std::string> lines;
|
||
|
|
Update([&](Json& d) {
|
||
|
|
auto [changed, folded] = FoldInto(seats, now, d);
|
||
|
|
lines = std::move(folded);
|
||
|
|
return changed;
|
||
|
|
});
|
||
|
|
for (const std::string& line : lines) log(std::format("board: {}", line));
|
||
|
|
return lines;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::int64_t Board::TotalInflight(const Json& d) {
|
||
|
|
std::int64_t total = 0;
|
||
|
|
for (const Json& s : Dyn::Or(Dyn::Get(d, "seats"), Dyn::EmptyDict())) total += static_cast<std::int64_t>(Dyn::Or(Dyn::Get(s, "inflight"), Dyn::EmptyDict()).size());
|
||
|
|
return total;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::pair<double, std::string> Board::MemoryBase(std::int64_t free, double now) {
|
||
|
|
// MemAvailable as it was with no pass in flight on the machine. With
|
||
|
|
// other drivers' passes in flight the recorded reading stands: those
|
||
|
|
// passes are in the budget at what they were admitted at, and the
|
||
|
|
// reading of now has their use taken off already - it would count
|
||
|
|
// them twice. With no record, or a pass in flight that records no
|
||
|
|
// memory, `free` - the safe side.
|
||
|
|
std::optional<std::pair<double, std::string>> result;
|
||
|
|
Update([&](Json& d) {
|
||
|
|
const bool changed = Sweep(d, now).first;
|
||
|
|
std::vector<const Json*> entries;
|
||
|
|
for (const Json& s : d.at("seats")) {
|
||
|
|
for (const Json& e : Dyn::Or(Dyn::Get(s, "inflight"), Dyn::EmptyDict())) entries.push_back(&e);
|
||
|
|
}
|
||
|
|
const Json memory = Dyn::Get(d, "memory").is_object() ? d.at("memory") : Json::object();
|
||
|
|
if (entries.empty()) {
|
||
|
|
d["memory"] = {{"freeMb", free}, {"at", now}, {"by", me}};
|
||
|
|
result = {static_cast<double>(free), "no pass in flight on this machine"};
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
std::vector<double> held;
|
||
|
|
for (const Json* e : entries) held.push_back(Num(Dyn::Get(*e, "mb")));
|
||
|
|
const double mb = Dyn::Sum(held);
|
||
|
|
const std::size_t n = entries.size();
|
||
|
|
const std::string plural = n != 1 ? "es" : "";
|
||
|
|
if (!Dyn::Get(memory, "freeMb").is_null() && std::ranges::all_of(entries, [](const Json* e) { return !Dyn::Get(*e, "mb").is_null(); })) {
|
||
|
|
result = {AsDouble(memory.at("freeMb")), std::format("the board's reading of {} with no pass in flight; {} pass{} of other drivers in flight hold{} {:.0f} MB of it",
|
||
|
|
Clock(Num(Dyn::Get(memory, "at"))), n, plural, n == 1 ? "s" : "", mb)};
|
||
|
|
} else {
|
||
|
|
const std::string why = Dyn::Get(memory, "freeMb").is_null()
|
||
|
|
? std::string("no reading on the board")
|
||
|
|
: std::format("{} of them record no memory", std::ranges::count_if(entries, [](const Json* e) { return Dyn::Get(*e, "mb").is_null(); }));
|
||
|
|
result = {static_cast<double>(free), std::format("read now with {} pass{} of other drivers in flight ({}): their {:.0f} MB counted again, on the safe side", n, plural, why, mb)};
|
||
|
|
}
|
||
|
|
return changed;
|
||
|
|
});
|
||
|
|
return result.value_or(std::pair<double, std::string>{static_cast<double>(free), "read now"});
|
||
|
|
}
|
||
|
|
|
||
|
|
Board::Placed Board::Place(std::vector<Json>& seats, double now, const Json& config, const Json& apiConfig, const Chooser& chooser, std::optional<std::int64_t> cap) {
|
||
|
|
// Two drivers polling in the same instant cannot both take the last
|
||
|
|
// slot, the last stack or the last of the memory. `cap` is the pool
|
||
|
|
// width, enforced across every driver on the machine (Jorijn,
|
||
|
|
// 2026-10-18).
|
||
|
|
Placed placed;
|
||
|
|
std::vector<std::string> lines;
|
||
|
|
Update([&](Json& d) {
|
||
|
|
auto [changed, folded] = FoldInto(seats, now, d);
|
||
|
|
lines = std::move(folded);
|
||
|
|
placed.ranked = Ranked(seats, now, config, apiConfig);
|
||
|
|
const std::int64_t total = TotalInflight(d);
|
||
|
|
if (cap && total >= *cap) {
|
||
|
|
placed.full = total;
|
||
|
|
return changed;
|
||
|
|
}
|
||
|
|
placed.picked = placed.ranked.empty() ? std::nullopt : chooser(placed.ranked, others);
|
||
|
|
if (placed.picked) {
|
||
|
|
const Json& seat = *placed.picked->first;
|
||
|
|
const Json& job = *placed.picked->second;
|
||
|
|
const bool paid = IsPaid(seat);
|
||
|
|
const Json& timeout = Dyn::Get(job, "timeoutMin");
|
||
|
|
Json entry = {
|
||
|
|
{"since", now},
|
||
|
|
{"deadline", now + 60 * (Dyn::Truthy(timeout) ? AsDouble(timeout) : 20.0) * 1.5 + InflightGraceS},
|
||
|
|
{"cost5h", paid ? Json(0) : config.at("lensCostPct")},
|
||
|
|
{"costUsd", paid ? LensCostValue(seat, apiConfig) : Json(0)},
|
||
|
|
};
|
||
|
|
if (!Dyn::Get(job, "_admitted").is_null()) entry["mb"] = Dyn::Round(AsDouble(job.at("_admitted")), 1);
|
||
|
|
if (Dyn::Truthy(Dyn::Get(job, "stackKey"))) entry["stack"] = job.at("stackKey");
|
||
|
|
Record(d, seat)["inflight"][std::format("{}/{}", me, Dyn::Str(Dyn::At(job, "label")))] = std::move(entry);
|
||
|
|
changed = true;
|
||
|
|
}
|
||
|
|
return changed;
|
||
|
|
});
|
||
|
|
for (const std::string& line : lines) log(std::format("board: {}", line));
|
||
|
|
return placed;
|
||
|
|
}
|
||
|
|
|
||
|
|
void Board::Finish(const Json& seat, std::string_view label, double now) {
|
||
|
|
// Off the board whether or not it is placed again, and the seat's
|
||
|
|
// settled state onto it.
|
||
|
|
Update([&](Json& d) {
|
||
|
|
bool changed = Sweep(d, now).first;
|
||
|
|
const std::string key = KeyOf(seat);
|
||
|
|
if (Dyn::Truthy(Dyn::Get(d.at("seats"), key))) {
|
||
|
|
Json& b = d["seats"][key];
|
||
|
|
if (Dyn::Get(b, "inflight").is_object() && b["inflight"].erase(std::format("{}/{}", me, label)) > 0) changed = true;
|
||
|
|
}
|
||
|
|
return PublishSeat(d, seat, now, false) || changed;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
Json Board::View(double now) const {
|
||
|
|
Json d;
|
||
|
|
try {
|
||
|
|
d = Dyn::LoadJsonFile(path);
|
||
|
|
} catch (const std::exception&) {
|
||
|
|
d = Json::object();
|
||
|
|
}
|
||
|
|
if (!d.is_object()) d = Json::object();
|
||
|
|
if (!Dyn::Has(d, "drivers")) d["drivers"] = Json::object();
|
||
|
|
if (!Dyn::Has(d, "seats")) d["seats"] = Json::object();
|
||
|
|
// Nothing written, not even a dead driver's lock file taken away.
|
||
|
|
Sweep(d, now, false);
|
||
|
|
Json drivers = Json::object();
|
||
|
|
for (auto it = d.at("drivers").begin(); it != d.at("drivers").end(); ++it) {
|
||
|
|
Json driver = it.value();
|
||
|
|
driver["inflight"] = 0;
|
||
|
|
driver["mb"] = 0.0;
|
||
|
|
driver["stacks"] = Json::array();
|
||
|
|
drivers[it.key()] = std::move(driver);
|
||
|
|
}
|
||
|
|
Json seats = Json::object();
|
||
|
|
for (auto it = d.at("seats").begin(); it != d.at("seats").end(); ++it) {
|
||
|
|
const Json& s = it.value();
|
||
|
|
Json by = Json::object();
|
||
|
|
std::int64_t count = 0;
|
||
|
|
double cost5h = 0.0;
|
||
|
|
double costUsd = 0.0;
|
||
|
|
Json oldest;
|
||
|
|
const Json& inflight = Dyn::Or(Dyn::Get(s, "inflight"), Dyn::EmptyDict());
|
||
|
|
for (auto e = inflight.begin(); e != inflight.end(); ++e) {
|
||
|
|
const std::string owner = Owner(e.key());
|
||
|
|
by[owner] = Dyn::Has(by, owner) ? by[owner].get<std::int64_t>() + 1 : 1;
|
||
|
|
++count;
|
||
|
|
cost5h += Num(Dyn::Get(e.value(), "cost5h"));
|
||
|
|
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;
|
||
|
|
if (drivers.contains(owner)) {
|
||
|
|
Json& driver = drivers[owner];
|
||
|
|
driver["inflight"] = driver.at("inflight").get<std::int64_t>() + 1;
|
||
|
|
driver["mb"] = Dyn::Round(AsDouble(driver.at("mb")) + Num(Dyn::Get(e.value(), "mb")), 1);
|
||
|
|
if (Dyn::Truthy(Dyn::Get(e.value(), "stack"))) driver["stacks"].push_back(e.value().at("stack"));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const Json& pause = Dyn::Get(s, "pauseUntil");
|
||
|
|
seats[it.key()] = {
|
||
|
|
{"id", Dyn::Get(s, "id")},
|
||
|
|
{"kind", Dyn::Get(s, "kind")},
|
||
|
|
{"inflight", count},
|
||
|
|
{"cost5h", Dyn::Round(cost5h, 4)},
|
||
|
|
{"costUsd", Dyn::Round(costUsd, 4)},
|
||
|
|
{"by", by},
|
||
|
|
{"oldest", oldest},
|
||
|
|
{"windows", Dyn::Or(Dyn::Get(s, "windows"), Dyn::EmptyDict())},
|
||
|
|
{"pauseUntil", Dyn::Truthy(pause) ? pause : Json(0)},
|
||
|
|
{"state", Dyn::Get(s, "state")},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
return {{"path", path.string()}, {"drivers", drivers}, {"seats", seats}, {"memory", Dyn::Get(d, "memory")}};
|
||
|
|
}
|
||
|
|
|
||
|
|
std::vector<std::string> Board::OthersLines(const std::vector<Json>& seats, double, const Json& config, const Json& apiConfig, std::optional<std::int64_t> cap) const {
|
||
|
|
Json drivers = Json::object();
|
||
|
|
const Json& all = Dyn::Or(Dyn::Get(data, "drivers"), Dyn::EmptyDict());
|
||
|
|
for (auto it = all.begin(); it != all.end(); ++it) {
|
||
|
|
if (it.key() != me) drivers[it.key()] = it.value();
|
||
|
|
}
|
||
|
|
const std::string width = cap ? std::format("; {} in flight on this machine, the cap is {}", TotalInflight(data), *cap) : std::string();
|
||
|
|
if (drivers.empty()) return {std::format("board: {}; no other driver running{}", path.string(), width)};
|
||
|
|
std::vector<std::string> each;
|
||
|
|
for (auto it = drivers.begin(); it != drivers.end(); ++it) {
|
||
|
|
std::int64_t n = 0;
|
||
|
|
for (const Json& s : seats) {
|
||
|
|
const Json f = Foreign(s);
|
||
|
|
const Json& by = Dyn::Or(Dyn::Get(f, "drivers"), Dyn::EmptyDict());
|
||
|
|
if (Dyn::Has(by, it.key())) n += Int(AsDouble(by.at(it.key())));
|
||
|
|
}
|
||
|
|
const Json& d = it.value();
|
||
|
|
each.push_back(std::format("pid {} ({}, {}) since {}, {} in flight on our seats", PidOf(it.key()), Dyn::Has(d, "run") ? Dyn::Str(d.at("run")) : std::string("?"),
|
||
|
|
Dyn::Has(d, "effort") ? Dyn::Str(d.at("effort")) : std::string("?"), Platform::LocalTime("%H:%M", Num(Dyn::Get(d, "since"))), n));
|
||
|
|
}
|
||
|
|
std::string held;
|
||
|
|
if (others.count) {
|
||
|
|
held = std::format("; theirs hold {:.0f} MB of memory", others.mb);
|
||
|
|
if (!others.stacks.empty()) {
|
||
|
|
held += std::format(" and {} stack(s) ({})", others.stacks.size(), Dyn::Join(std::vector<std::string>(others.stacks.begin(), others.stacks.end()), ", "));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
std::vector<std::string> lines = {std::format("board: {}; {} other driver(s) running: {}{}{}", path.string(), drivers.size(), Dyn::Join(each, "; "), width, held)};
|
||
|
|
for (const Json& s : seats) {
|
||
|
|
const Json f = Foreign(s);
|
||
|
|
if (!Dyn::Truthy(f.at("count"))) continue;
|
||
|
|
std::vector<std::string> who;
|
||
|
|
const Json& by = Dyn::Or(Dyn::Get(f, "drivers"), Dyn::EmptyDict());
|
||
|
|
for (auto it = by.begin(); it != by.end(); ++it) who.push_back(std::format("{} from pid {}", Dyn::Str(it.value()), PidOf(it.key())));
|
||
|
|
const std::int64_t count = Int(AsDouble(f.at("count")));
|
||
|
|
if (IsPaid(s)) {
|
||
|
|
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 {
|
||
|
|
lines.push_back(std::format(" seat {}: {} passes in flight ({}) - projected 5h {:.1f} %", Id(s), count, Dyn::Join(who, ", "), OauthProjected(s, config)));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return lines;
|
||
|
|
}
|
||
|
|
|
||
|
|
const Others& Board::OthersNow() const {
|
||
|
|
return others;
|
||
|
|
}
|
||
|
|
|
||
|
|
const std::string& Board::Me() const {
|
||
|
|
return me;
|
||
|
|
}
|
||
|
|
|
||
|
|
const fs::path& Board::Path() const {
|
||
|
|
return path;
|
||
|
|
}
|
||
|
|
|
||
|
|
const Json& Board::Data() const {
|
||
|
|
return data;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---- reporting -----------------------------------------------------------------------------
|
||
|
|
Json Summary(const std::vector<Json>& seats) {
|
||
|
|
Json out = Json::array();
|
||
|
|
for (const Json& s : seats) {
|
||
|
|
Json copy = s;
|
||
|
|
copy.erase("secret");
|
||
|
|
out.push_back(std::move(copy));
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string Table(const std::vector<Json>& seats, std::optional<double> nowGiven) {
|
||
|
|
const double now = NowOr(nowGiven);
|
||
|
|
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));
|
||
|
|
};
|
||
|
|
std::string fable = "—";
|
||
|
|
for (auto it = windows.begin(); it != windows.end(); ++it) {
|
||
|
|
if (Seats::IsFableWindow(it.key()) && !Dyn::Get(it.value(), "utilization").is_null()) {
|
||
|
|
fable = std::format("{}%", Seats::Fmt(it.value().at("utilization")));
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
std::string note;
|
||
|
|
if (s.at("state") != "ok") {
|
||
|
|
note = Dyn::Slice(Dyn::StrOr(s.at("error"), ""), 0, 60);
|
||
|
|
} else if (AsDouble(s.at("pauseUntil")) > now) {
|
||
|
|
note = std::format("paused until {}", Platform::LocalTime("%H:%M", AsDouble(s.at("pauseUntil"))));
|
||
|
|
}
|
||
|
|
const Json f = Foreign(s);
|
||
|
|
rows.push_back({Id(s), Kind(s), Dyn::Str(s.at("state")), w("five_hour"), w("seven_day"), fable, Dyn::Str(s.at("running")),
|
||
|
|
Dyn::Truthy(f.at("count")) ? Dyn::Str(f.at("count")) : std::string("—"), Dyn::Str(s.at("procs")),
|
||
|
|
IsPaid(s) ? std::format("${:.2f}", AsDouble(s.at("spentUsd"))) : std::string("—"), note});
|
||
|
|
}
|
||
|
|
return Seats::RenderTable(rows);
|
||
|
|
}
|
||
|
|
}
|