feat(seatpool): the seat pool and the board several drivers share
The seats of .env and the ledger, the probe, placement, settling a
finished pass, the child's credentials, the OpenRouter seats, and the
board every driver on the machine shares, in one shape and under one
lock. A driver is alive while its lifetime lock is held; with no lock file
natively its pid and start time decide, and in the sandbox it is taken for
alive.
:Http is the HTTP the driver needs, on FetchHTTP1: a status outside 2xx is
HttpError, every other failure carries a fixed text. :Platform gains
MakeDirs, with the last directory's own mode, and errno texts for its
writes and locks.
The tests cover the seat pool and several drivers at once, less the checks
that drive the pool's Scheduler, Admission and Stacks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 08:15:03 +01:00
|
|
|
// 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
|
|
|
|
|
// (<board dir>/drivers/<pid>-<start>.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;
|
feat(seats): accounts without a 5-hour limit
Anthropic is removing the 5-hour limit in stages (from 2026-10-31), and
an account without it reports the weekly window alone. Until now the pool
took such an account for an unprobed seat: always eligible, ranked after
every other, never held to the weekly cutoff, with no cap on its passes.
- The burst window is the 5-hour one, else the weekly one
(Seats::BurstWindow). An account whose burst window is the weekly one is
ranked by it and admitted under the 95 % cutoff with the passes in
flight projected on it, at the measured share per pass (0.04 at xhigh,
0.125 otherwise; REVIEW_GATES_LENS_COST_7D_PCT), and waits for the weekly
reset when over it.
- Every OAuth account and the local login takes at most 128 passes at once,
every driver's counted (REVIEW_GATES_OAUTH_CONCURRENCY): every run
measured with 149 or more in flight on one account drew usage-limit
rejections, none of the 119 with 139 or fewer.
- A 'concurrents' rejection pauses the seat for 20 s or its retry-after,
not until a window's reset (a seat sat two hours at 10 % on one).
- Board entries, the folded foreign record and the board's view carry
cost7d, so the drivers see each other's weekly load.
- `seats` and the log table say `no limit` in such an account's 5-hour
column; the board's others-line names its weekly projection.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:07:17 +01:00
|
|
|
// An OAuth seat's pause after a 'concurrents' rejection: too many requests
|
|
|
|
|
// at once on the account, cleared as the ones in flight answer. Not the
|
|
|
|
|
// window's reset (2026-10-31: a seat sat two hours at 10 % on one).
|
|
|
|
|
constexpr double ConcurrentBackoffS = 20;
|
|
|
|
|
// The passes one OAuth account (or the local login) takes at once, every
|
|
|
|
|
// driver's counted. Until 2026-10-31 the 5-hour projection was the only
|
|
|
|
|
// limit, and an account without a 5-hour limit has none: on the record,
|
|
|
|
|
// every run with 149 or more in flight on one account drew usage-limit
|
|
|
|
|
// rejections, none of the 119 with 139 or fewer.
|
|
|
|
|
// REVIEW_GATES_OAUTH_CONCURRENCY overrides.
|
|
|
|
|
constexpr std::int64_t OauthConcurrency = 128;
|
2026-10-31 21:13:55 +01:00
|
|
|
// What one running pass is projected to cost of an OAuth seat's windows,
|
|
|
|
|
// in percentage points, by the run's reasoning effort (every lens of a run
|
|
|
|
|
// shares --effort): the pooled means of 5438 Opus 5.5 passes on 141
|
|
|
|
|
// seat-runs (2026-10-12..15), rounded up - max 0.44 / 0.118, xhigh
|
|
|
|
|
// 0.12 / 0.033. The 85 % ceiling and the cutoff's last percent absorb the
|
|
|
|
|
// spread. An effort not in the table projects at the max figures;
|
|
|
|
|
// REVIEW_GATES_LENS_COST_PCT and REVIEW_GATES_LENS_COST_7D_PCT override
|
|
|
|
|
// every effort.
|
|
|
|
|
constexpr double LensCostPct(std::string_view effort) { return effort == "xhigh" ? 0.125 : 0.5; }
|
|
|
|
|
constexpr double LensCost7dPct(std::string_view effort) { return effort == "xhigh" ? 0.04 : 0.125; }
|
feat(seatpool): the seat pool and the board several drivers share
The seats of .env and the ledger, the probe, placement, settling a
finished pass, the child's credentials, the OpenRouter seats, and the
board every driver on the machine shares, in one shape and under one
lock. A driver is alive while its lifetime lock is held; with no lock file
natively its pid and start time decide, and in the sandbox it is taken for
alive.
:Http is the HTTP the driver needs, on FetchHTTP1: a status outside 2xx is
HttpError, every other failure carries a fixed text. :Platform gains
MakeDirs, with the last directory's own mode, and errno texts for its
writes and locks.
The tests cover the seat pool and several drivers at once, less the checks
that drive the pool's Scheduler, Admission and Stacks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 08:15:03 +01:00
|
|
|
// The kinds whose processes cost money the ledger counts.
|
|
|
|
|
constexpr std::array<std::string_view, 2> Paid = {"api", "openrouter"};
|
|
|
|
|
// The kinds whose out/invalid verdict crosses drivers on the board.
|
|
|
|
|
constexpr std::array<std::string_view, 2> 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<std::string, std::string>& env);
|
feat(seats): the env file's settings count, and a machine's own seats file
ApiConfig and PoolConfig read the seat order, the budgets and the
concurrency from the process environment alone, so a `.env` that said
REVIEW_GATES_SEAT_ORDER=oauth-first ordered nothing: the Python driver
reads those lines from the file under the process environment
(config_env, 2026-10-17: a setting lives beside the seats it orders).
SeatPool::SettingsOf does the same - the file's placement settings
under the process environment, in a map handed to the placement readers
alone (ApiConfig, PoolConfig, Seats::ConfigFromEnv) - and leaves the
process environment untouched, so no other reader sees a line of the
file: a path's or a binary's line reaches nothing, and two drivers
reading one .env keep one board and one ledger. The placement settings
are one list, SeatPool::PlacementSettings - the fifteen names those
readers read - for the map, the run's log and what makes a working
directory's .env a seats file; a REVIEW_GATES_* line of any other name
stays out of the map and is named, by the run's log and on `seats`'s
stderr. One dotenv grammar serves the tokens and the settings
(Seats::DotenvLines).
`run` and `seats` resolve the file from their own --env-file
(SeatPool::ResolveSeatsFile): the --env-file, else $SEATS_ENV_FILE, else
the working directory's .env - the operator's, as the Python driver and
`seats` read it, not the repository under review's - when it is a file
holding a seat or a placement setting, each with a value, or one that
cannot be read as UTF-8 text (its error is then the command's) - a
readable application .env, its Vite or Supabase lines, one of blank
placeholder lines or one that is no file is passed over, and the run
says so - else the machine's own
$XDG_CONFIG_HOME/crafter-review/env, else ~/.config/crafter-review/env:
one file for every repository on a machine, a repository's seats .env
winning over it. Each file is read once and the settings and the tokens
are taken from that read (SeatsFile::Text), so a file that can be read
only once - a pipe, `--env-file <(gpg -d seats.env.gpg)`, a FIFO - gives
its settings and its seats alike, and a failed read raises its own error
where the tokens are laid over the environment: SeatPool::WithFileTokens,
for `run` and `seats` alike, and LoadSeats takes the merged environment
and no longer reads a file by path. An empty `seats --env-file` means no file
now, as it did for `run`, and the `seats` report names the side that
runs first (it said "api seats run first" whatever the order).
Runs, Commands and KernelModes, the tests that run `run` and `seats`,
point XDG_CONFIG_HOME, and HOST_XDG_CONFIG_HOME for the Flatpak, at a
path of their own that nothing creates, so a global file stays out of
them; the grammar, the settings and their precedence, the lookup and the
one read have their tests, a run with `--env` (a prefix) and with a
repeated --env-file, and a `seats` with `--env`, take tokens and
settings from the one file, an application's .env and a venv are passed
over, a FIFO is read once, no line of the file reaches `stop`, every
listed placement setting moves a reader, another REVIEW_GATES_* line is
named and a .env of blank lines passed over, and the
README, the environment table and the procedure say the new rule.
2026-11-01 01:51:48 +01:00
|
|
|
// The seats file of `run` or `seats` (2026-11-01): the --env-file given
|
|
|
|
|
// (an empty one: none), else $SEATS_ENV_FILE, else <cwd>/.env - the
|
|
|
|
|
// working directory's, the operator's, not the repository under
|
|
|
|
|
// review's - when it is a file holding a seat or a placement setting,
|
|
|
|
|
// each with a value (an application's .env, one of blank lines, or one
|
|
|
|
|
// that is no file, is passed over and recorded so), else the machine's
|
|
|
|
|
// own (GlobalEnvFile); none: this
|
|
|
|
|
// machine's own login. Each file is read once, and the command takes
|
|
|
|
|
// its settings and its tokens from that read - the tokens through
|
|
|
|
|
// Text() - so a file that can be read only once (a pipe, `--env-file
|
|
|
|
|
// <(gpg -d seats.env.gpg)`, a FIFO) gives its settings and its seats
|
|
|
|
|
// alike (the self-review's round 10: read twice, it lost its seats or
|
|
|
|
|
// hung).
|
|
|
|
|
struct SeatsFile {
|
|
|
|
|
std::optional<std::filesystem::path> path;
|
|
|
|
|
std::optional<std::filesystem::path> passedOver;
|
|
|
|
|
std::string text;
|
|
|
|
|
std::exception_ptr error;
|
|
|
|
|
// The text as read; the read's own error, raised again, when the
|
|
|
|
|
// read failed (a file missing or not UTF-8 ends the command).
|
|
|
|
|
const std::string& Text() const;
|
|
|
|
|
};
|
|
|
|
|
// The lookup and its one read.
|
|
|
|
|
SeatsFile ResolveSeatsFile(const std::optional<std::string>& given, const std::filesystem::path& cwd);
|
|
|
|
|
// The settings a seats file may carry: every name ApiConfig, PoolConfig
|
|
|
|
|
// and Seats::ConfigFromEnv read (2026-11-01, the self-review's round 19:
|
|
|
|
|
// one list for the map they take, its log and what makes a working
|
|
|
|
|
// directory's .env a seats file).
|
|
|
|
|
constexpr std::array<std::string_view, 15> PlacementSettings = {
|
|
|
|
|
"REVIEW_GATES_API_BUDGET_USD", "REVIEW_GATES_API_LENS_COST_USD", "REVIEW_GATES_API_CONCURRENCY", "REVIEW_GATES_API_CYCLE_DAY",
|
|
|
|
|
"REVIEW_GATES_SEAT_ORDER", "REVIEW_GATES_OPENROUTER_BUDGET_USD", "REVIEW_GATES_OPENROUTER_LENS_COST_USD",
|
|
|
|
|
"REVIEW_GATES_OPENROUTER_CONCURRENCY", "REVIEW_GATES_LENS_COST_PCT", "REVIEW_GATES_LENS_COST_7D_PCT",
|
|
|
|
|
"REVIEW_GATES_OAUTH_CONCURRENCY", "REVIEW_GATES_SEAT_MAX_UTIL", "REVIEW_GATES_SEAT_WAIT_MIN", "REVIEW_GATES_SEAT_HANDOVER_PCT",
|
|
|
|
|
"REVIEW_GATES_SEAT_POLICY"};
|
|
|
|
|
// The settings the placement readers take (ApiConfig, PoolConfig,
|
|
|
|
|
// Seats::ConfigFromEnv): the process environment, and the file's
|
|
|
|
|
// PlacementSettings lines where it has no such variable - the Python
|
|
|
|
|
// driver's config_env (2026-10-17: a setting lives beside the seats it
|
|
|
|
|
// orders). A map for those readers alone: the process environment is
|
|
|
|
|
// not touched, so no other reader sees a line of the file, and a path's
|
|
|
|
|
// or a binary's line reaches nothing (the self-review's round 11). The
|
|
|
|
|
// names the file supplied are `fromFile`; its REVIEW_GATES_* lines of
|
|
|
|
|
// any other name, which only the process environment sets, are
|
|
|
|
|
// `ignored` - named by the run and `seats`, never taken in silence.
|
|
|
|
|
struct Settings {
|
|
|
|
|
std::map<std::string, std::string> env;
|
|
|
|
|
std::vector<std::string> fromFile;
|
|
|
|
|
std::vector<std::string> ignored;
|
|
|
|
|
};
|
|
|
|
|
Settings SettingsOf(const SeatsFile& file);
|
|
|
|
|
// env with the seats file's seat lines over it, from the file's one read
|
|
|
|
|
// (Text(): a read that failed raises its error here) - the one place a
|
|
|
|
|
// command lays the file's tokens over an environment (the self-review's
|
|
|
|
|
// round 19: run and seats had each done it by hand, and LoadSeats read
|
|
|
|
|
// the file a second time by path).
|
|
|
|
|
std::map<std::string, std::string> WithFileTokens(std::map<std::string, std::string> env, const SeatsFile& file);
|
|
|
|
|
// The machine's own seats file, read when a run or `seats` has no
|
|
|
|
|
// --env-file, no $SEATS_ENV_FILE and no seats ./.env: $XDG_CONFIG_HOME/crafter-review/env,
|
|
|
|
|
// else ~/.config/crafter-review/env - when it exists (2026-11-01, one
|
|
|
|
|
// file for every repository on a machine; a repository's own .env wins).
|
|
|
|
|
std::optional<std::filesystem::path> GlobalEnvFile();
|
feat(seatpool): the seat pool and the board several drivers share
The seats of .env and the ledger, the probe, placement, settling a
finished pass, the child's credentials, the OpenRouter seats, and the
board every driver on the machine shares, in one shape and under one
lock. A driver is alive while its lifetime lock is held; with no lock file
natively its pid and start time decide, and in the sandbox it is taken for
alive.
:Http is the HTTP the driver needs, on FetchHTTP1: a status outside 2xx is
HttpError, every other failure carries a fixed text. :Platform gains
MakeDirs, with the last directory's own mode, and errno texts for its
writes and locks.
The tests cover the seat pool and several drivers at once, less the checks
that drive the pool's Scheduler, Admission and Stacks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 08:15:03 +01:00
|
|
|
// 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<std::filesystem::path>& 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<double> now = std::nullopt);
|
|
|
|
|
Json Add(const std::string& seatId, double usd, std::optional<double> now = std::nullopt);
|
|
|
|
|
Json MarkOut(const std::string& seatId, std::string_view reason, std::optional<double> now = std::nullopt);
|
|
|
|
|
void Save();
|
|
|
|
|
const Json& Data() const;
|
feat(run): `crafter-review run` and `slots`, the driver of the gates
Run: gate 0 (the artifact, the trees, the inventory, the run lock, the
scratch dir, dispositions, reuse state, resume, reads per round, seats,
the board), the stack and migration preflights, the prompts, reuse of
whole and block verdicts, salvage, a gate on the pool, checkpoints, the
dump, the dry-run report and the unit-state store; slots_plan and
app_ready beside it. The CLI parses run's 28 options, types and choices
included.
A full run takes its lifetime lock beside the board before it joins it.
A signal - or the stop file `crafter-review stop` writes, looked at
every 2 s - stops the run once gate 0 is through, and the stop stays in
force for the gates after; before that it ends the process with its
descendants.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 09:16:41 +01:00
|
|
|
const std::filesystem::path& Path() const;
|
feat(seatpool): the seat pool and the board several drivers share
The seats of .env and the ledger, the probe, placement, settling a
finished pass, the child's credentials, the OpenRouter seats, and the
board every driver on the machine shares, in one shape and under one
lock. A driver is alive while its lifetime lock is held; with no lock file
natively its pid and start time decide, and in the sandbox it is taken for
alive.
:Http is the HTTP the driver needs, on FetchHTTP1: a status outside 2xx is
HttpError, every other failure carries a fixed text. :Platform gains
MakeDirs, with the last directory's own mode, and errno texts for its
writes and locks.
The tests cover the seat pool and several drivers at once, less the checks
that drive the pool's Scheduler, Admission and Stacks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 08:15:03 +01:00
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
std::filesystem::path path;
|
|
|
|
|
Json apiConfig;
|
|
|
|
|
Json data;
|
|
|
|
|
Json Update(const std::string& seatId, const std::function<void(Json&)>& change, std::optional<double> now);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Json NewSeat(const std::string& id, const std::string& kind, const std::string& secret);
|
feat(seats): the env file's settings count, and a machine's own seats file
ApiConfig and PoolConfig read the seat order, the budgets and the
concurrency from the process environment alone, so a `.env` that said
REVIEW_GATES_SEAT_ORDER=oauth-first ordered nothing: the Python driver
reads those lines from the file under the process environment
(config_env, 2026-10-17: a setting lives beside the seats it orders).
SeatPool::SettingsOf does the same - the file's placement settings
under the process environment, in a map handed to the placement readers
alone (ApiConfig, PoolConfig, Seats::ConfigFromEnv) - and leaves the
process environment untouched, so no other reader sees a line of the
file: a path's or a binary's line reaches nothing, and two drivers
reading one .env keep one board and one ledger. The placement settings
are one list, SeatPool::PlacementSettings - the fifteen names those
readers read - for the map, the run's log and what makes a working
directory's .env a seats file; a REVIEW_GATES_* line of any other name
stays out of the map and is named, by the run's log and on `seats`'s
stderr. One dotenv grammar serves the tokens and the settings
(Seats::DotenvLines).
`run` and `seats` resolve the file from their own --env-file
(SeatPool::ResolveSeatsFile): the --env-file, else $SEATS_ENV_FILE, else
the working directory's .env - the operator's, as the Python driver and
`seats` read it, not the repository under review's - when it is a file
holding a seat or a placement setting, each with a value, or one that
cannot be read as UTF-8 text (its error is then the command's) - a
readable application .env, its Vite or Supabase lines, one of blank
placeholder lines or one that is no file is passed over, and the run
says so - else the machine's own
$XDG_CONFIG_HOME/crafter-review/env, else ~/.config/crafter-review/env:
one file for every repository on a machine, a repository's seats .env
winning over it. Each file is read once and the settings and the tokens
are taken from that read (SeatsFile::Text), so a file that can be read
only once - a pipe, `--env-file <(gpg -d seats.env.gpg)`, a FIFO - gives
its settings and its seats alike, and a failed read raises its own error
where the tokens are laid over the environment: SeatPool::WithFileTokens,
for `run` and `seats` alike, and LoadSeats takes the merged environment
and no longer reads a file by path. An empty `seats --env-file` means no file
now, as it did for `run`, and the `seats` report names the side that
runs first (it said "api seats run first" whatever the order).
Runs, Commands and KernelModes, the tests that run `run` and `seats`,
point XDG_CONFIG_HOME, and HOST_XDG_CONFIG_HOME for the Flatpak, at a
path of their own that nothing creates, so a global file stays out of
them; the grammar, the settings and their precedence, the lookup and the
one read have their tests, a run with `--env` (a prefix) and with a
repeated --env-file, and a `seats` with `--env`, take tokens and
settings from the one file, an application's .env and a venv are passed
over, a FIFO is read once, no line of the file reaches `stop`, every
listed placement setting moves a reader, another REVIEW_GATES_* line is
named and a .env of blank lines passed over, and the
README, the environment table and the procedure say the new rule.
2026-11-01 01:51:48 +01:00
|
|
|
// The seats a run may use. spec: auto (every Anthropic seat of env - the
|
|
|
|
|
// seats file's over the process environment's, WithFileTokens - else
|
|
|
|
|
// local), local | api | oauth | openrouter,
|
feat(seatpool): the seat pool and the board several drivers share
The seats of .env and the ledger, the probe, placement, settling a
finished pass, the child's credentials, the OpenRouter seats, and the
board every driver on the machine shares, in one shape and under one
lock. A driver is alive while its lifetime lock is held; with no lock file
natively its pid and start time decide, and in the sandbox it is taken for
alive.
:Http is the HTTP the driver needs, on FetchHTTP1: a status outside 2xx is
HttpError, every other failure carries a fixed text. :Platform gains
MakeDirs, with the last directory's own mode, and errno texts for its
writes and locks.
The tests cover the seat pool and several drivers at once, less the checks
that drive the pool's Scheduler, Admission and Stacks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 08:15:03 +01:00
|
|
|
// 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.
|
feat(seats): the env file's settings count, and a machine's own seats file
ApiConfig and PoolConfig read the seat order, the budgets and the
concurrency from the process environment alone, so a `.env` that said
REVIEW_GATES_SEAT_ORDER=oauth-first ordered nothing: the Python driver
reads those lines from the file under the process environment
(config_env, 2026-10-17: a setting lives beside the seats it orders).
SeatPool::SettingsOf does the same - the file's placement settings
under the process environment, in a map handed to the placement readers
alone (ApiConfig, PoolConfig, Seats::ConfigFromEnv) - and leaves the
process environment untouched, so no other reader sees a line of the
file: a path's or a binary's line reaches nothing, and two drivers
reading one .env keep one board and one ledger. The placement settings
are one list, SeatPool::PlacementSettings - the fifteen names those
readers read - for the map, the run's log and what makes a working
directory's .env a seats file; a REVIEW_GATES_* line of any other name
stays out of the map and is named, by the run's log and on `seats`'s
stderr. One dotenv grammar serves the tokens and the settings
(Seats::DotenvLines).
`run` and `seats` resolve the file from their own --env-file
(SeatPool::ResolveSeatsFile): the --env-file, else $SEATS_ENV_FILE, else
the working directory's .env - the operator's, as the Python driver and
`seats` read it, not the repository under review's - when it is a file
holding a seat or a placement setting, each with a value, or one that
cannot be read as UTF-8 text (its error is then the command's) - a
readable application .env, its Vite or Supabase lines, one of blank
placeholder lines or one that is no file is passed over, and the run
says so - else the machine's own
$XDG_CONFIG_HOME/crafter-review/env, else ~/.config/crafter-review/env:
one file for every repository on a machine, a repository's seats .env
winning over it. Each file is read once and the settings and the tokens
are taken from that read (SeatsFile::Text), so a file that can be read
only once - a pipe, `--env-file <(gpg -d seats.env.gpg)`, a FIFO - gives
its settings and its seats alike, and a failed read raises its own error
where the tokens are laid over the environment: SeatPool::WithFileTokens,
for `run` and `seats` alike, and LoadSeats takes the merged environment
and no longer reads a file by path. An empty `seats --env-file` means no file
now, as it did for `run`, and the `seats` report names the side that
runs first (it said "api seats run first" whatever the order).
Runs, Commands and KernelModes, the tests that run `run` and `seats`,
point XDG_CONFIG_HOME, and HOST_XDG_CONFIG_HOME for the Flatpak, at a
path of their own that nothing creates, so a global file stays out of
them; the grammar, the settings and their precedence, the lookup and the
one read have their tests, a run with `--env` (a prefix) and with a
repeated --env-file, and a `seats` with `--env`, take tokens and
settings from the one file, an application's .env and a venv are passed
over, a FIFO is read once, no line of the file reaches `stop`, every
listed placement setting moves a reader, another REVIEW_GATES_* line is
named and a .env of blank lines passed over, and the
README, the environment table and the procedure say the new rule.
2026-11-01 01:51:48 +01:00
|
|
|
std::vector<Json> LoadSeats(const std::map<std::string, std::string>& env, std::string_view spec, Ledger* ledger = nullptr, const Json& apiConfig = nullptr);
|
feat(seatpool): the seat pool and the board several drivers share
The seats of .env and the ledger, the probe, placement, settling a
finished pass, the child's credentials, the OpenRouter seats, and the
board every driver on the machine shares, in one shape and under one
lock. A driver is alive while its lifetime lock is held; with no lock file
natively its pid and start time decide, and in the sandbox it is taken for
alive.
:Http is the HTTP the driver needs, on FetchHTTP1: a status outside 2xx is
HttpError, every other failure carries a fixed text. :Platform gains
MakeDirs, with the last directory's own mode, and errno texts for its
writes and locks.
The tests cover the seat pool and several drivers at once, less the checks
that drive the pool's Scheduler, Admission and Stacks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 08:15:03 +01:00
|
|
|
using ProbeFunction = std::function<Json(std::int32_t seat, const std::string& token, const std::string& model, double timeout, const std::string& claudeBin, bool local, bool api)>;
|
|
|
|
|
// One probe per seat before the run; a seat already out is skipped.
|
|
|
|
|
void Probe(std::vector<Json>& seats, const std::string& claudeBin, double timeout, const std::vector<std::string>& models,
|
|
|
|
|
const std::function<void(const std::string&)>& log, const ProbeFunction& probe = {});
|
|
|
|
|
void ApplyProbe(Json& seat, const Json& result);
|
|
|
|
|
std::string Describe(const Json& seat);
|
|
|
|
|
|
2026-10-31 21:13:55 +01:00
|
|
|
// The OAuth placement knobs, with the pool's own per-pass projections at
|
|
|
|
|
// the run's effort (LensCostPct, LensCost7dPct; the env overrides them)
|
|
|
|
|
// and the per-account cap.
|
feat(seats): accounts without a 5-hour limit
Anthropic is removing the 5-hour limit in stages (from 2026-10-31), and
an account without it reports the weekly window alone. Until now the pool
took such an account for an unprobed seat: always eligible, ranked after
every other, never held to the weekly cutoff, with no cap on its passes.
- The burst window is the 5-hour one, else the weekly one
(Seats::BurstWindow). An account whose burst window is the weekly one is
ranked by it and admitted under the 95 % cutoff with the passes in
flight projected on it, at the measured share per pass (0.04 at xhigh,
0.125 otherwise; REVIEW_GATES_LENS_COST_7D_PCT), and waits for the weekly
reset when over it.
- Every OAuth account and the local login takes at most 128 passes at once,
every driver's counted (REVIEW_GATES_OAUTH_CONCURRENCY): every run
measured with 149 or more in flight on one account drew usage-limit
rejections, none of the 119 with 139 or fewer.
- A 'concurrents' rejection pauses the seat for 20 s or its retry-after,
not until a window's reset (a seat sat two hours at 10 % on one).
- Board entries, the folded foreign record and the board's view carry
cost7d, so the drivers see each other's weekly load.
- `seats` and the log table say `no limit` in such an account's 5-hour
column; the board's others-line names its weekly projection.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:07:17 +01:00
|
|
|
Json PoolConfig(const std::map<std::string, std::string>& env, std::string_view effort = {});
|
feat(seatpool): the seat pool and the board several drivers share
The seats of .env and the ledger, the probe, placement, settling a
finished pass, the child's credentials, the OpenRouter seats, and the
board every driver on the machine shares, in one shape and under one
lock. A driver is alive while its lifetime lock is held; with no lock file
natively its pid and start time decide, and in the sandbox it is taken for
alive.
:Http is the HTTP the driver needs, on FetchHTTP1: a status outside 2xx is
HttpError, every other failure carries a fixed text. :Platform gains
MakeDirs, with the last directory's own mode, and errno texts for its
writes and locks.
The tests cover the seat pool and several drivers at once, less the checks
that drive the pool's Scheduler, Admission and Stacks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 08:15:03 +01:00
|
|
|
double OauthProjected(const Json& seat, const Json& config);
|
|
|
|
|
// A window whose reset has passed counts as empty until the next reading.
|
|
|
|
|
std::vector<std::string> 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<Json*> Ranked(std::vector<Json>& seats, double now, const Json& config, const Json& apiConfig);
|
|
|
|
|
Json* Pick(std::vector<Json>& 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<std::filesystem::path> CarryTranscript(const std::filesystem::path& scratch, std::string_view fromSeat, std::string_view toSeat,
|
|
|
|
|
std::string_view sessionId, const std::function<void(const std::string&)>& log = {});
|
|
|
|
|
bool AllOut(const std::vector<Json>& seats);
|
|
|
|
|
std::optional<double> EarliestResume(const std::vector<Json>& seats, double now, std::optional<double> 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<void(const std::string&)>& 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<std::string, std::string> ChildEnv(const Json& seat, const std::map<std::string, std::string>& 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<std::string>& models, const std::function<void(const std::string&)>& 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();
|
|
|
|
|
// `<kind>:<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: `<pid>:<start>`, 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<std::string> 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<std::string> stacks;
|
|
|
|
|
std::int64_t unmeasured = 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
class Board {
|
|
|
|
|
public:
|
|
|
|
|
explicit Board(std::filesystem::path path, std::string me = {}, std::optional<std::string> boot = std::nullopt,
|
|
|
|
|
std::function<void(const std::string&)> 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<bool, std::vector<std::string>> 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<Json>& seats, double now, bool probed = false);
|
|
|
|
|
std::vector<std::string> Fold(std::vector<Json>& 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<double, std::string> MemoryBase(std::int64_t free, double now);
|
|
|
|
|
using Chooser = std::function<std::optional<std::pair<Json*, Json*>>(const std::vector<Json*>& ranked, const Others& others)>;
|
|
|
|
|
struct Placed {
|
|
|
|
|
std::vector<Json*> ranked;
|
|
|
|
|
std::optional<std::pair<Json*, Json*>> 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<Json>& seats, double now, const Json& config, const Json& apiConfig, const Chooser& chooser, std::optional<std::int64_t> 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<std::string> OthersLines(const std::vector<Json>& seats, double now, const Json& config, const Json& apiConfig, std::optional<std::int64_t> 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<void(const std::string&)> log;
|
|
|
|
|
Json data;
|
|
|
|
|
Others others;
|
|
|
|
|
bool warned = false;
|
|
|
|
|
void Warn(const std::string& message);
|
|
|
|
|
Json Update(const std::function<bool(Json&)>& 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<bool, std::vector<std::string>> FoldInto(std::vector<Json>& 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<Json>& seats);
|
|
|
|
|
std::string Table(const std::vector<Json>& seats, std::optional<double> now = std::nullopt);
|
|
|
|
|
}
|