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

@ -298,6 +298,20 @@ namespace Crafter::Review::Seats {
return out;
}
std::map<std::int32_t, std::int32_t> ApiGrantDays(const std::map<std::string, std::string>& env) {
std::map<std::int32_t, std::int32_t> out;
for (std::int32_t i = 1; i <= MaxSeats; ++i) {
// Decimal digits of any script; past 31 the value stays out of
// range.
const std::u32string value = Dyn::Decode(Dyn::Strip(EnvOf(env, std::format("ANTHROPIC_API_KEY{}_GRANT_DAY", i))));
if (value.empty() || !std::ranges::all_of(value, [](char32_t c) { return Dyn::DecimalValue(c) >= 0; })) continue;
std::int32_t day = 0;
for (const char32_t c : value) day = std::min(day * 10 + Dyn::DecimalValue(c), 32);
if (day >= 1 && day <= 31) out[i] = day;
}
return out;
}
std::vector<std::pair<std::int32_t, std::string>> OpenRouterKeysFromEnv(const std::map<std::string, std::string>& env) {
std::vector<std::pair<std::int32_t, std::string>> out;
for (std::int32_t i = 1; i <= MaxSeats; ++i) {
@ -341,7 +355,9 @@ namespace Crafter::Review::Seats {
for (const auto& [name, value] : DotenvLines(text)) {
for (const std::string_view stem : {std::string_view("CLAUDE_CODE_OAUTH_TOKEN"), std::string_view("ANTHROPIC_API_KEY"), std::string_view("OPENROUTER_KEY")}) {
if (!name.starts_with(stem)) continue;
const std::string_view rest = std::string_view(name).substr(stem.size());
std::string_view rest = std::string_view(name).substr(stem.size());
// ANTHROPIC_API_KEY<n>_GRANT_DAY: the seat's grant day.
if (stem == "ANTHROPIC_API_KEY" && rest.size() == 11 && rest.ends_with("_GRANT_DAY")) rest = rest.substr(0, 1);
const bool digit = rest.size() == 1 && rest[0] >= '1' && rest[0] <= '6';
if (!rest.empty() && !digit) break;
if (stem == "ANTHROPIC_API_KEY" && !digit) break;
@ -980,26 +996,54 @@ namespace Crafter::Review::Seats {
return Dyn::Join(lines, "\n");
}
std::string RenderApiReport(const std::vector<Json>& apiSeats, const Json& apiConfig, const Json& board, const std::map<std::int32_t, std::string>& keys) {
std::vector<std::vector<std::string>> rows = {{"seat", "state", "driver spend this cycle", "procs", "cycle from", "ledger: out", "in flight"}};
std::string RenderApiReport(const std::vector<Json>& apiSeats, const Json& apiConfig, double now, const Json& board, const std::map<std::int32_t, std::string>& keys) {
const std::string today = Platform::LocalTime("%Y-%m-%d", now);
// "17 Sept"
auto date = [](const std::string& iso) {
return std::format("{} {}", std::stoi(iso.substr(8, 2)), Months[static_cast<std::size_t>(std::stoi(iso.substr(5, 2)) - 1)]);
};
std::vector<std::vector<std::string>> rows = {{"seat", "probe", "driver", "driver spend this cycle", "procs", "cycle from", "grant day", "next credits", "in flight"}};
std::vector<std::string> notes;
for (const Json& s : SortedBySeat(apiSeats)) {
const std::string id = std::format("api{}", Dyn::Str(Dyn::At(s, "seat")));
const Json& ledger = Dyn::Or(Dyn::Get(s, "ledger"), Dyn::EmptyDict());
const double spent = NumberOr0(Dyn::Get(ledger, "spentUsd"));
const std::string state = Dyn::Str(Dyn::At(s, "state")) + (Dyn::Truthy(Dyn::Get(s, "error")) && Dyn::At(s, "state") != "ok" ? std::format(" ({})", Dyn::Str(s.at("error"))) : std::string());
const Json& outSince = Dyn::Get(ledger, "outSince");
const std::string state = Dyn::Str(Dyn::At(s, "state"));
const std::string probe = state + (Dyn::Truthy(Dyn::Get(s, "error")) && state != "ok" ? std::format(" ({})", Dyn::Str(s.at("error"))) : std::string());
// The driver's verdict, SeatPool::LedgerOut's.
const Json& ledgerOut = Dyn::Get(s, "ledgerOut");
const bool out = !ledgerOut.is_null();
const std::string driver = !out ? std::string("ok")
: ledgerOut == "credit" ? std::format("out (credit since {})", Dyn::Replace(Dyn::Slice(Dyn::Str(Dyn::Get(ledger, "outSince")), 0, 16), "T", " "))
: std::string("out (budget spent)");
const std::string due = Dyn::StrOr(Dyn::Get(s, "nextCredits"), "");
const std::string dueText = due.size() < 10 ? std::string("—") : due == today ? std::string("today") : due < today ? std::format("overdue, {}", date(due)) : date(due);
const std::int64_t n = Inflight(board, keys, s);
rows.push_back({
std::format("api{}", Dyn::Str(Dyn::At(s, "seat"))), state, std::format("${:.2f} of ${}", spent, Fmt(apiConfig.at("budgetUsd"))), Dyn::StrOr(Dyn::Get(ledger, "procs"), "0"),
Dyn::StrOr(Dyn::Get(ledger, "cycle"), "—"), Dyn::Truthy(outSince) ? std::format("since {}", Dyn::Replace(Dyn::Slice(Dyn::Str(outSince), 0, 16), "T", " ")) : std::string("—"),
n ? std::format("{} of {}", n, Dyn::Str(apiConfig.at("concurrency"))) : std::string("—"),
});
rows.push_back({id, probe, driver, std::format("${:.2f} of ${}", spent, Fmt(apiConfig.at("budgetUsd"))), Dyn::StrOr(Dyn::Get(ledger, "procs"), "0"),
Dyn::StrOr(Dyn::Get(ledger, "cycle"), "—"), Dyn::StrOr(Dyn::Get(s, "grantDay"), "—"), dueText,
n ? std::format("{} of {}", n, Dyn::Str(apiConfig.at("concurrency"))) : std::string("—")});
const Json& event = Dyn::Get(s, "event");
if (event == "new-grant") {
notes.push_back(std::format("{}: the key answers again - new credits; the ledger cycle restarted (from {})", id, Dyn::StrOr(Dyn::Get(ledger, "cycle"), "—")));
} else if (event == "credit-gone") {
notes.push_back(std::format("{}: the credit error, now recorded in the ledger", id));
} else if (state == "ok" && ledgerOut == "credit") {
notes.push_back(std::format("{}: the key answered, but the credit error was recorded since the probe started - the ledger's verdict stands", id));
} else if (state == "ok" && out) {
notes.push_back(std::format("{}: the key answers, but the driver's budget for this cycle is spent (REVIEW_GATES_API_BUDGET_USD)", id));
} else if (state != "ok" && state != "out" && out) {
notes.push_back(std::format("{}: probe {} - the ledger's verdict stands", id, state));
}
}
return Dyn::Join(std::vector<std::string>{
RenderTable(rows),
std::format("{} seats run first (REVIEW_GATES_SEAT_ORDER={}) · budget ${} per seat per cycle · cycle day {} · {} processes per seat · placed at +${} each",
Dyn::Str(apiConfig.at("order")) == "api-first" ? "api" : "oauth", Dyn::Str(apiConfig.at("order")), Fmt(apiConfig.at("budgetUsd")), Dyn::Str(apiConfig.at("cycleDay")), Dyn::Str(apiConfig.at("concurrency")), Fmt(apiConfig.at("lensCostUsd"))),
"the balance itself is in the Console (Settings > Billing > Promotional credits); the ledger counts only what the review drivers spent",
}, "\n");
std::vector<std::string> lines = {RenderTable(rows)};
lines.insert(lines.end(), notes.begin(), notes.end());
lines.push_back(std::format("{} seats run first (REVIEW_GATES_SEAT_ORDER={}) · budget ${} per seat per cycle · a cycle starts on the seat's grant day (ANTHROPIC_API_KEY<n>_GRANT_DAY, else "
"REVIEW_GATES_API_CYCLE_DAY={}) · {} processes per seat · placed at +${} each",
Dyn::Str(apiConfig.at("order")) == "api-first" ? "api" : "oauth", Dyn::Str(apiConfig.at("order")), Fmt(apiConfig.at("budgetUsd")), Dyn::Str(apiConfig.at("cycleDay")),
Dyn::Str(apiConfig.at("concurrency")), Fmt(apiConfig.at("lensCostUsd"))));
lines.emplace_back("the balance itself is in the Console (Settings > Billing > Promotional credits); the ledger counts only what the review drivers spent, and \"overdue\" means the grant day "
"is wrong");
return Dyn::Join(lines, "\n");
}
std::string RenderDrivers(const Json& board, double now, const Zone& zone) {