feat(seats): an API seat's cycle starts on its own grant day
All checks were successful
Flatpak / check (push) Has been skipped
Flatpak / publish (push) Successful in 5m30s

The ledger reset every API seat's spend on one
REVIEW_GATES_API_CYCLE_DAY and cleared every credit-out, an OpenRouter
key's too, at that reset. Each API seat's cycle starts on its
ANTHROPIC_API_KEY<n>_GRANT_DAY now (1..31, a day past a month's end its
last), else the cycle day, also 1..31. A credit-out carries across the
rollover, and the run's probe tries a key held out on credit again, as
`seats` does an API key: an ok lifts the mark - an API key's is new
credits, its cycle restarting from the grant day, or from today when
that day is over a week back; an OpenRouter key keeps its cycle's spend,
and a key that answers with its cycle's budget spent stays out on that
budget. A probe that finds a key out of credit records it, unless the
ledger already has it out. Both are decided in one locked update on the
file's entry, a mark another driver made since the probe started
standing, and the ledger's stamps carry their UTC offset so marks are
ordered by instant.

`seats` shows the probe's reading beside the driver's verdict (the
`probe` and `driver` columns replace `state` and `ledger: out`), each
seat's grant day and when its next credits are due, and notes on what
the probe changed in the ledger or why the driver's verdict stands; the
run logs the days.
This commit is contained in:
Jorijn van der Graaf 2026-11-02 13:58:29 +01:00
commit ceb87f70ef
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
12 changed files with 774 additions and 83 deletions

View file

@ -206,7 +206,8 @@ namespace Crafter::Review::SeatPool {
{"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))))},
// A day past a month's end is its last day (CycleStart).
{"cycleDay", std::max<std::int64_t>(1, std::min<std::int64_t>(31, 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.
@ -233,47 +234,153 @@ namespace Crafter::Review::SeatPool {
return root / "seat-ledger.json";
}
namespace {
std::chrono::year_month_day LocalDate(double now) {
using namespace std::chrono;
const std::string today = Platform::LocalTime("%Y-%m-%d", now);
return {year(std::stoi(today.substr(0, 4))), month(static_cast<unsigned>(std::stoi(today.substr(5, 2)))), day(static_cast<unsigned>(std::stoi(today.substr(8, 2))))};
}
std::chrono::year_month_day ParseIso(std::string_view iso) {
using namespace std::chrono;
const std::string text(iso);
return {year(std::stoi(text.substr(0, 4))), month(static_cast<unsigned>(std::stoi(text.substr(5, 2)))), day(static_cast<unsigned>(std::stoi(text.substr(8, 2))))};
}
std::string Iso(const std::chrono::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()));
}
// A ledger stamp: local time with its UTC offset,
// 2026-10-25T02:00:05+01:00, so any driver orders it by instant across
// the hour that repeats when DST ends, or from another zone.
std::string Stamp(double at) {
std::string text = Platform::LocalTime("%Y-%m-%dT%H:%M:%S%z", at);
if (text.size() == 24) text.insert(22, ":");
return text;
}
// A stamp's instant, epoch seconds: by its offset, else (a stamp
// written before 1.1.0) as local time; none when it cannot be read.
std::optional<double> Instant(std::string_view stamp) {
auto number = [&](std::size_t from, std::size_t width) -> std::optional<std::int32_t> {
if (from + width > stamp.size()) return std::nullopt;
std::int32_t value = 0;
for (std::size_t i = from; i < from + width; ++i) {
if (stamp[i] < '0' || stamp[i] > '9') return std::nullopt;
value = value * 10 + (stamp[i] - '0');
}
return value;
};
if (stamp.size() < 19 || stamp[4] != '-' || stamp[7] != '-' || stamp[10] != 'T' || stamp[13] != ':' || stamp[16] != ':') return std::nullopt;
const std::optional<std::int32_t> y = number(0, 4), mo = number(5, 2), d = number(8, 2), h = number(11, 2), mi = number(14, 2), s = number(17, 2);
if (!y || !mo || !d || !h || !mi || !s) return std::nullopt;
const double clock = *h * 3600.0 + *mi * 60.0 + *s;
if (stamp.size() == 25 && (stamp[19] == '+' || stamp[19] == '-') && stamp[22] == ':') {
const std::optional<std::int32_t> oh = number(20, 2), om = number(23, 2);
const std::chrono::year_month_day date {std::chrono::year(*y), std::chrono::month(static_cast<unsigned>(*mo)), std::chrono::day(static_cast<unsigned>(*d))};
if (!oh || !om || !date.ok()) return std::nullopt;
const double offset = (stamp[19] == '-' ? -1.0 : 1.0) * (*oh * 3600.0 + *om * 60.0);
return static_cast<double>(std::chrono::sys_days(date).time_since_epoch().count()) * 86400.0 + clock - offset;
}
if (stamp.size() != 19) return std::nullopt;
std::tm local {};
local.tm_year = *y - 1900;
local.tm_mon = *mo - 1;
local.tm_mday = *d;
local.tm_hour = *h;
local.tm_min = *mi;
local.tm_sec = *s;
local.tm_isdst = -1;
const std::time_t seconds = std::mktime(&local);
if (seconds == static_cast<std::time_t>(-1)) return std::nullopt;
return static_cast<double>(seconds);
}
// That day of the month, a day past its end its last day.
std::chrono::year_month_day OnDay(const std::chrono::year_month& ym, std::int32_t day) {
using namespace std::chrono;
const auto last = static_cast<std::int32_t>(static_cast<unsigned>(year_month_day_last(ym.year(), month_day_last(ym.month())).day()));
return {ym.year(), ym.month(), std::chrono::day(static_cast<unsigned>(std::clamp(day, 1, last)))};
}
}
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)))));
const year_month_day date = LocalDate(now);
const year_month ym(date.year(), date.month());
const year_month_day thisMonth = OnDay(ym, day);
return Iso(date >= thisMonth ? thisMonth : OnDay(ym - months(1), day));
}
std::string NextGrant(double now, std::int32_t day, const Json& outSince) {
using namespace std::chrono;
const std::string cycle = CycleStart(now, day);
if (Dyn::Truthy(outSince) && Dyn::Slice(Dyn::Str(outSince), 0, 10) < cycle) return cycle;
const year_month_day start = ParseIso(cycle);
return Iso(OnDay(year_month(start.year(), start.month()) + months(1), day));
}
std::pair<bool, std::string> LedgerVerdict(const Json& entry, double budget) {
const Json& outSince = Dyn::Get(entry, "outSince");
if (Dyn::Truthy(outSince)) return {true, std::format("ledger: {} since {}", Dyn::StrOr(Dyn::Get(entry, "outReason"), "out"), Dyn::Slice(Dyn::Str(outSince), 0, 16))};
const double spent = Num(Dyn::Get(entry, "spentUsd"));
if (spent >= budget) return {true, std::format("ledger: ${:.2f} spent this cycle, budget ${}", spent, G(budget))};
return {false, std::string()};
}
Json LedgerOut(const Json& entry, double budget) {
if (!LedgerVerdict(entry, budget).first) return nullptr;
return Dyn::Truthy(Dyn::Get(entry, "outSince")) ? "credit" : "budget";
}
// ---- 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(fs::path path, Json apiConfig, Json data, std::map<std::string, std::int32_t> days)
: path(std::move(path)), apiConfig(std::move(apiConfig)), data(Dyn::Truthy(data) ? std::move(data) : Json::object()), days(std::move(days)) {}
Ledger Ledger::Load(const fs::path& path, const Json& apiConfig) {
Ledger Ledger::Load(const fs::path& path, const Json& apiConfig, std::map<std::string, std::int32_t> days) {
Json data;
try {
data = Dyn::LoadJsonFile(path);
} catch (const std::exception&) {
data = Json::object();
}
return Ledger(path, apiConfig, data.is_object() ? data : Json::object());
return Ledger(path, apiConfig, data.is_object() ? data : Json::object(), std::move(days));
}
std::int32_t Ledger::Day(const std::string& seatId) const {
const auto found = days.find(seatId);
if (found != days.end() && found->second > 0) return found->second;
return static_cast<std::int32_t>(Int(AsDouble(Dyn::At(apiConfig, "cycleDay"))));
}
void Ledger::SetDay(const std::string& seatId, std::int32_t day) {
days[seatId] = day;
}
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 std::string cycle = CycleStart(NowOr(now), Day(seatId));
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"), "")) {
if (!Dyn::Truthy(e)) {
data[seatId] = {{"cycle", cycle}, {"spentUsd", 0.0}, {"procs", 0}, {"outSince", nullptr}, {"outReason", nullptr}};
} else if (cycle > Dyn::StrOr(Dyn::Get(e, "cycle"), "")) {
// Only a newer cycle starts over: an entry of a later one (another
// driver's cycle day, or a grant seen early) is not wiped. A
// credit-out carries: the cycle's day is a configured date, the
// credit error was observed (2026-10-17: the calendar cycle held
// api3 out for the first week of its new credits; an OpenRouter
// key's turn puts nothing on it), and only a probe that sees the
// key answer again lifts it (Reconcile). When the key was last seen
// answering again carries too: Reconcile records no credit error
// read before it.
Json next = {{"cycle", cycle}, {"spentUsd", 0.0}, {"procs", 0}, {"outSince", Dyn::Get(e, "outSince")}, {"outReason", Dyn::Get(e, "outReason")}};
if (Dyn::Truthy(Dyn::Get(e, "grantSeen"))) next["grantSeen"] = Dyn::Get(e, "grantSeen");
data[seatId] = std::move(next);
}
return data[seatId];
}
Json Ledger::Update(const std::string& seatId, const std::function<void(Json&)>& change, std::optional<double> now) {
Json Ledger::Update(const std::string& seatId, const std::function<bool(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
@ -285,10 +392,10 @@ namespace Crafter::Review::SeatPool {
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);
const bool write = change(e);
entry = e;
file = data;
return true;
if (write) file = data;
return write;
}, data, 0600, {});
return entry;
}
@ -297,16 +404,70 @@ namespace Crafter::Review::SeatPool {
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));
return true;
}, 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["outSince"] = Stamp(NowOr(now));
e["outReason"] = Dyn::Slice(reason, 0, 160);
return true;
}, now);
}
Json Ledger::GrantEntry(const std::string& seatId, double at) const {
using namespace std::chrono;
const std::string cycle = CycleStart(at, Day(seatId));
const std::string weekAgo = Iso(year_month_day(sys_days(LocalDate(at)) - std::chrono::days(7)));
const std::string start = cycle >= weekAgo ? cycle : Iso(LocalDate(at));
return {{"cycle", start}, {"spentUsd", 0.0}, {"procs", 0}, {"outSince", nullptr}, {"outReason", nullptr}, {"grantSeen", Stamp(at)}};
}
std::string Ledger::Reconcile(const std::string& seatId, const Json& state, double probedAt, std::optional<double> now) {
if (state != "ok" && state != "out") return {};
const double at = NowOr(now);
// A mark made before the probe started, by instant to the second the
// stamps hold; one made since may postdate what the probe read, and
// one that cannot be read counts as made since.
const double started = std::floor(probedAt);
auto before = [&](const Json& stamp) {
const std::optional<double> instant = Instant(Dyn::Str(stamp));
return instant && *instant < started;
};
const Json fresh = GrantEntry(seatId, at);
std::string event;
// Decided on the file's entry under the lock, not on this process's
// copy: another driver may have recorded the grant and spent on it,
// or the credit error, since this copy was read.
// An OpenRouter key (or<n>) is prepaid: its lift is no grant, no new
// cycle - the mark goes, the cycle's spend stays.
const bool openRouter = seatId.starts_with("or");
Update(seatId, [&](Json& e) {
const Json& outSince = Dyn::Get(e, "outSince");
if (state == "ok") {
if (!Dyn::Truthy(outSince) || !before(outSince)) return false;
if (openRouter) {
e["outSince"] = nullptr;
e["outReason"] = nullptr;
e["grantSeen"] = Stamp(at);
event = "credit-back";
} else {
e = fresh;
event = "new-grant";
}
return true;
}
const Json& seen = Dyn::Get(e, "grantSeen");
if (Dyn::Truthy(outSince) || (Dyn::Truthy(seen) && !before(seen))) return false;
e["outSince"] = Stamp(at);
e["outReason"] = openRouter ? "key limit reached" : "credit balance too low";
event = "credit-gone";
return true;
}, at);
return event;
}
void Ledger::Save() {
try {
Platform::WriteAtomic(path, DumpFile(data), 0600);
@ -319,6 +480,10 @@ namespace Crafter::Review::SeatPool {
return data;
}
const Json& Ledger::ApiConfig() const {
return apiConfig;
}
const fs::path& Ledger::Path() const {
return path;
}
@ -436,7 +601,7 @@ namespace Crafter::Review::SeatPool {
return env;
}
std::vector<Json> LoadSeats(const std::map<std::string, std::string>& env, std::string_view specText, Ledger* ledger, const Json& apiConfig) {
std::vector<Json> LoadSeats(const std::map<std::string, std::string>& env, std::string_view specText, Ledger* ledger, const Json& apiConfig, std::optional<double> now) {
const std::map<std::string, std::string>& merged = env;
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));
@ -482,18 +647,31 @@ namespace Crafter::Review::SeatPool {
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");
}
// An API seat's cycle starts on its grant day (2026-10-17): the day
// its organization receives the plan's credits, the Console's
// Promotional-credits expiry minus three days.
const std::map<std::int32_t, std::int32_t> grantDays = Seats::ApiGrantDays(env);
for (Json& s : seats) {
if (Kind(s) != "api") continue;
const auto found = grantDays.find(static_cast<std::int32_t>(Dyn::ParseInt(Dyn::Slice(Id(s), 3)).value_or(0)));
const std::int32_t day = found != grantDays.end() ? found->second : (Dyn::Truthy(apiConfig) ? static_cast<std::int32_t>(Int(AsDouble(Dyn::At(apiConfig, "cycleDay")))) : 1);
s["grantDay"] = day;
if (ledger) ledger->SetDay(Id(s), day);
}
if (ledger) {
const double now = Platform::Now();
const double at = NowOr(now);
for (Json& s : seats) {
if (!IsPaid(s)) continue;
const Json& e = ledger->Entry(Id(s), now);
const Json& e = ledger->Entry(Id(s), at);
s["spentUsd"] = AsDouble(Dyn::At(e, "spentUsd"));
if (Dyn::Truthy(Dyn::Get(e, "outSince"))) {
const double budget = Dyn::Truthy(apiConfig) ? Budget(s, apiConfig) : std::numeric_limits<double>::infinity();
const auto [out, why] = LedgerVerdict(e, budget);
if (out) {
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)));
s["error"] = why;
// Why the ledger holds the seat out: a credit-out is
// probed again, a spent budget is not.
s["ledgerOut"] = LedgerOut(e, budget);
}
}
}
@ -541,7 +719,62 @@ namespace Crafter::Review::SeatPool {
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) {
namespace {
// A paid key after its probe (started at probedAt): the seat takes
// the spend the file holds, every driver's; a lift puts a held-out
// seat back (an API key's with its cycle restarted), a credit error
// recorded since the probe started stands over its answer, the credit
// error takes it out (an API key's with the ledger's record and the
// next credits' date in the reason), an unreadable probe leaves a
// held-out seat held out.
void AfterPaidProbe(Json& s, const Json& held, Ledger* ledger, double probedAt, double now, const Log& log) {
const std::string event = ledger ? ledger->Reconcile(Id(s), s.at("state"), probedAt, now) : std::string();
const Json entry = ledger ? ledger->Entry(Id(s), now) : Json::object();
if (ledger) s["spentUsd"] = Num(Dyn::Get(entry, "spentUsd"));
if (event == "new-grant") {
s["ledgerOut"] = nullptr;
s["error"] = nullptr;
log(std::format("seat {}: the key answers again - new credits; the ledger cycle restarts (from {})", Id(s), Dyn::Str(entry.at("cycle"))));
} else if (event == "credit-back") {
s["ledgerOut"] = nullptr;
s["error"] = nullptr;
log(std::format("seat {}: the key's limit has room again - back, its cycle's spend kept", Id(s)));
} else if (s.at("state") == "ok" && Dyn::Truthy(Dyn::Get(entry, "outSince"))) {
s["state"] = "out";
s["ledgerOut"] = "credit";
s["error"] = LedgerVerdict(entry, std::numeric_limits<double>::infinity()).second;
} else if (s.at("state") == "ok") {
s["ledgerOut"] = nullptr;
} else if (s.at("state") == "out" && IsOpenRouter(s)) {
// The probe's error says why.
s["ledgerOut"] = "credit";
} else if (s.at("state") == "out") {
const Json e = ledger ? ledger->Entry(Id(s), now) : Json::object();
const Json& outSince = Dyn::Get(e, "outSince");
const Json& day = Dyn::Get(s, "grantDay");
s["ledgerOut"] = "credit";
s["error"] = std::format("credit balance too low{}; next credits due {}", Dyn::Truthy(outSince) ? std::format(" since {}", Dyn::Slice(Dyn::Str(outSince), 0, 16)) : std::string(),
NextGrant(now, Dyn::Truthy(day) ? static_cast<std::int32_t>(Int(AsDouble(day))) : 1, outSince));
} else if (s.at("state") == "unknown" && Dyn::Truthy(held)) {
s["state"] = "out";
s["error"] = std::format("{} (probe unreadable: {})", Dyn::Str(held), Dyn::Str(Dyn::Get(s, "error")));
}
// A key that answers with its cycle's budget spent is out on that
// budget, as LoadSeats holds such a seat out: an OpenRouter key's
// lift keeps its spend, and another driver may have spent on a
// grant it saw. Left ok, the seat would be neither placed nor out.
if (ledger && s.at("state") == "ok") {
if (const auto [out, why] = LedgerVerdict(entry, Budget(s, ledger->ApiConfig())); out) {
s["state"] = "out";
s["ledgerOut"] = "budget";
s["error"] = why;
}
}
}
}
void Probe(std::vector<Json>& seats, const std::string& claudeBin, double timeout, const std::vector<std::string>& models, const Log& log, const ProbeFunction& probe, Ledger* ledger,
std::optional<double> now, const OpenRouterProbeFunction& probeKey) {
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});
});
@ -555,7 +788,7 @@ namespace Crafter::Review::SeatPool {
break;
}
}
if (std::ranges::any_of(seats, IsOpenRouter)) {
if (!models.empty() && 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();
@ -569,22 +802,33 @@ namespace Crafter::Review::SeatPool {
}
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);
if (s.at("state") == "out" && !(IsPaid(s) && Dyn::Get(s, "ledgerOut") == "credit")) continue;
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);
// The ledger's verdict before the probe.
const Json held = s.at("state") == "out" ? Dyn::Get(s, "error") : Json();
const double probedAt = NowOr(now);
const Json r = probeKey ? probeKey(secret, timeout) : ProbeOpenRouter(secret, timeout);
ApplyProbe(s, r);
s["remainingUsd"] = Dyn::Get(r, "remaining");
AfterPaidProbe(s, held, ledger, probedAt, NowOr(now), log);
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") {
// The ledger's verdict before the probe.
const Json held = s.at("state") == "out" ? Dyn::Get(s, "error") : Json();
const double probedAt = NowOr(now);
ApplyProbe(s, run(n, secret, std::string(Seats::ProbeModel), timeout, claudeBin, false, true));
AfterPaidProbe(s, held, ledger, probedAt, NowOr(now), log);
log(std::format("seat {} ({}): {}", id, kind, Describe(s)));
continue;
}
if (kind == "local") {
r = run(n, "", lensModel, timeout, claudeBin, true, false);
} else {
r = run(n, secret, lensModel, timeout, claudeBin, false, false);