// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® export module Crafter.Review:SeatPool; import std; import :Json; // The seat pool: which credential each lens process runs on. OAuth seats // (CLAUDE_CODE_OAUTH_TOKEN1..6), API-credit seats (ANTHROPIC_API_KEY1..6, // real money, a ledger and a budget), OpenRouter seats // (OPENROUTER_KEY[1..6], never in the default pool), and `local`, this // machine's login. Placement: API seats first (their credit expires monthly), // then OAuth seats by projected utilisation; a process that fails on a seat's // limit, credit or auth is placed again; the pool aborts only when every seat // is out. Secrets never reach a log, a dump or a child's environment except // the one the CLI reads. // // Several drivers at once (2026-10-31): every driver on this machine // registers on the board (~/.cache/review-gates/seat-board.json) what it has // in flight on each seat, and folds the board into its seats and admission // before every launch; the pick is registered under the same lock. Every // driver on the machine shares the file, its shape and its lock. A seat is a // JSON object: the dump records it (without the secret). // // Liveness (README, AGENTS.md): a driver is alive while its lifetime lock // (/drivers/-.lock) is held; without one, natively // the pid and start time decide, and in the Flatpak the driver is taken for // alive, because pids of another namespace say nothing here. export namespace Crafter::Review::SeatPool { // A job is re-placed this often after a limit, credit or auth failure. constexpr std::int32_t MaxAttempts = 4; // An API seat's pause after a 429. constexpr double ApiBackoffS = 90; constexpr double OauthFallbackPauseS = 300; // The kinds whose processes cost money the ledger counts. constexpr std::array Paid = {"api", "openrouter"}; // The kinds whose out/invalid verdict crosses drivers on the board. constexpr std::array StateShared = {"oauth", "local"}; constexpr std::string_view BoardFile = "seat-board.json"; // An in-flight entry outlives its pass's time budget x1.5 by this much // before the sweep takes it. constexpr double InflightGraceS = 300; constexpr std::string_view OpenRouterBaseUrl = "https://openrouter.ai/api"; constexpr std::string_view OpenRouterModelsUrl = "https://openrouter.ai/api/v1/models"; constexpr std::string_view OpenRouterKeyUrl = "https://openrouter.ai/api/v1/key"; extern const std::string_view ResumePrompt; bool IsPaid(const Json& seat); // The API-seat knobs. Json ApiConfig(const std::map& env); // The other drivers' load on a seat as the last board fold left it. Json Foreign(const Json& seat); std::filesystem::path LedgerPath(const std::optional& scratchRoot = std::nullopt); // The most recent date with that day of the month, as YYYY-MM-DD. std::string CycleStart(double now, std::int32_t day); // What the driver spent on each API seat this cycle, and whether the key // answered the credit error; one file for every driver, each write a delta // applied under its lock. class Ledger { public: Ledger(std::filesystem::path path, Json apiConfig, Json data = Json::object()); static Ledger Load(const std::filesystem::path& path, const Json& apiConfig); Json& Entry(const std::string& seatId, std::optional now = std::nullopt); Json Add(const std::string& seatId, double usd, std::optional now = std::nullopt); Json MarkOut(const std::string& seatId, std::string_view reason, std::optional now = std::nullopt); void Save(); const Json& Data() const; const std::filesystem::path& Path() const; private: std::filesystem::path path; Json apiConfig; Json data; Json Update(const std::string& seatId, const std::function& change, std::optional now); }; Json NewSeat(const std::string& id, const std::string& kind, const std::string& secret); // The seats a run may use. spec: auto (every Anthropic seat of the env // file and the environment, else local), local | api | oauth | openrouter, // or a list such as api1,1,2,local. std::invalid_argument with a // ValueError message for a seat that is not configured, or OpenRouter // mixed with the rest. std::vector LoadSeats(const std::map& env, const std::optional& envFile, std::string_view spec, Ledger* ledger = nullptr, const Json& apiConfig = nullptr); using ProbeFunction = std::function; // One probe per seat before the run; a seat already out is skipped. void Probe(std::vector& seats, const std::string& claudeBin, double timeout, const std::vector& models, const std::function& log, const ProbeFunction& probe = {}); void ApplyProbe(Json& seat, const Json& result); std::string Describe(const Json& seat); // The OAuth placement knobs, with the pool's own per-process default (0.25: // the 2026-10-15 load test moved a 20x seat's window 0.08 points a pass). Json PoolConfig(const std::map& env); double OauthProjected(const Json& seat, const Json& config); // A window whose reset has passed counts as empty until the next reading. std::vector ExpireWindows(Json& seat, double now); bool Eligible(Json& seat, double now, const Json& config, const Json& apiConfig); // Every seat with room for a launch now, best first (pointers into seats). std::vector Ranked(std::vector& seats, double now, const Json& config, const Json& apiConfig); Json* Pick(std::vector& seats, double now, const Json& config, const Json& apiConfig); std::filesystem::path SeatDir(const std::filesystem::path& scratch, std::string_view seatId); // A pass interrupted by its seat's limit continues on another seat once // its transcript is copied into that seat's config dir. std::optional CarryTranscript(const std::filesystem::path& scratch, std::string_view fromSeat, std::string_view toSeat, std::string_view sessionId, const std::function& log = {}); bool AllOut(const std::vector& seats); std::optional EarliestResume(const std::vector& seats, double now, std::optional recheck = std::nullopt); // A finished process onto its seat; true when the job must be placed again // (the seat failed it, not the lens). bool Settle(Json& seat, Json& record, double now, const Json& config, const Json& apiConfig, Ledger* ledger, const std::function& log); double RetryAfter(std::string_view text, double fallback); std::filesystem::path WriteCredentials(const std::filesystem::path& configDir, std::string_view token); // The environment of a lens process on this seat. std::map ChildEnv(const Json& seat, const std::map& base, const std::filesystem::path& scratch); std::string ModelKey(std::string_view model); // {catalogue id: prices or null} of the run's models. Json FetchOpenRouterPrices(const std::vector& models, const std::function& log = {}, double timeout = 20); double OpenRouterCost(std::string_view model, const Json& usage); Json ProbeOpenRouter(const std::string& secret, double timeout = 15); // ---- several drivers at once: the board -------------------------------------- std::filesystem::path BoardPath(); // `:<12 hex of the secret's sha256>`, `local` for the login. std::string SeatKey(std::string_view kind, std::string_view secret); // This driver as the board names it: `:`, the sandbox // instance's in the Flatpak (Platform::Self). std::string MyKey(); // Is the driver `key` alive? Its lifetime lock decides when it has one // (a free one is taken away when `reap`); else natively its pid and start // time, and in the sandbox yes. bool DriverAlive(std::string_view key, const std::filesystem::path& boardDir, bool reap = true); // One live driver per scratch dir: a `.driver` marker, taken over when its // driver is dead. bool ClaimScratch(const std::filesystem::path& path); // Fold `other`'s readings into `into`, in place; the windows that changed. std::vector MergeWindows(Json& into, const Json& other, double now); // The other drivers' passes in flight on this machine. struct Others { std::int64_t count = 0; double mb = 0; std::set stacks; std::int64_t unmeasured = 0; }; class Board { public: explicit Board(std::filesystem::path path, std::string me = {}, std::optional boot = std::nullopt, std::function log = {}); // Is the driver `key` on the board alive: this driver; else not when // it joined under another boot; else DriverAlive. bool Alive(std::string_view key, const Json& drivers, bool reap = true) const; // Drop what no longer stands: dead drivers with their in-flight // entries, entries past their deadline, verdicts past their expiry // or whose driver is gone, seat records with nothing live. (changed, // the lines that say what went). std::pair> Sweep(Json& data, double now, bool reap = true) const; void Join(const Json& info, double now); // Everything of this driver off the board; idempotent, never throws. void Leave(); void Publish(const std::vector& seats, double now, bool probed = false); std::vector Fold(std::vector& seats, double now); static std::int64_t TotalInflight(const Json& data); // The memory a gate's budget starts from, and how it was had. std::pair MemoryBase(std::int64_t free, double now); using Chooser = std::function>(const std::vector& ranked, const Others& others)>; struct Placed { std::vector ranked; std::optional> picked; std::int64_t full = 0; }; // One locked section: the board folded, the seats ranked, the // chooser's pick registered before the lock drops. Placed Place(std::vector& seats, double now, const Json& config, const Json& apiConfig, const Chooser& chooser, std::optional cap = std::nullopt); void Finish(const Json& seat, std::string_view label, double now); // The board as it stands, swept in memory, nothing written. Json View(double now) const; std::vector OthersLines(const std::vector& seats, double now, const Json& config, const Json& apiConfig, std::optional cap = std::nullopt) const; const Others& OthersNow() const; const std::string& Me() const; const std::filesystem::path& Path() const; const Json& Data() const; private: std::filesystem::path path; std::string me; std::string boot; std::function log; Json data; Others others; bool warned = false; void Warn(const std::string& message); Json Update(const std::function& change); Json& Record(Json& data, const Json& seat) const; static bool StateHolds(const Json& state, const Json& data, double now); Others OthersOf(const Json& data) const; std::pair> FoldInto(std::vector& seats, double now, Json& data); bool PublishSeat(Json& data, const Json& seat, double now, bool probed) const; }; // The seats as the dump records them: no secrets. Json Summary(const std::vector& seats); std::string Table(const std::vector& seats, std::optional now = std::nullopt); }