feat(seats): an API seat's cycle starts on its own grant day
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:
parent
c6b4d78245
commit
ceb87f70ef
12 changed files with 774 additions and 83 deletions
|
|
@ -111,7 +111,7 @@ Conventions:
|
|||
|
||||
No file the drivers write holds these in practice, and a file that fails to parse is treated as a damaged one.
|
||||
- **HTTP.** `ClientHTTP1::Fetch` ignores `*_proxy` variables. The OpenRouter requests send `User-Agent: crafter-review`; `app_ready` sends `review-gates`. Reason phrases are the standard ones, not the server's.
|
||||
- **Seat placement** (SeatPool, Seats, Drivers): The settings beside the seats count too. `run` (Gate 0) and `seats` each resolve the seats file from their own `--env-file` with `SeatPool::ResolveSeatsFile` (`--env-file`, `$SEATS_ENV_FILE`, `<cwd>/.env` when it is a file holding a seat or a placement setting, each with a value (a blank line is neither), or one that cannot be read as UTF-8 text (its error is then the command's) - a readable application `.env`, or one that is no file, is passed over, recorded as `passedOver` and logged by the run - the machine's own), reading each file once (`SeatsFile::Text()`: a pipe or a FIFO `--env-file` 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` - the one place, so `LoadSeats` takes the merged environment and reads no file). `SeatPool::SettingsOf` lays the file's placement settings (`Seats::SettingsFromEnvFile`, one dotenv grammar with the tokens', `DotenvLines`; `SeatPool::PlacementSettings`, the fifteen names the readers read, also deciding what makes `<cwd>/.env` a seats file - a test pins that each moves a reader) under the process environment into a map handed to the placement readers alone - `ApiConfig`, `PoolConfig`, `Seats::ConfigFromEnv` - names its other `REVIEW_GATES_*` lines (the run logs them, `seats` prints them on stderr), and the process environment is not touched, 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 Python driver's `config_env` of 2026-10-17; the first self-review's rounds 3 to 10, 2026-11-01, reworked it as an application to the process environment until round 11 found the map the readers already took; the per-seat `ANTHROPIC_API_KEY<n>_GRANT_DAY` cycle days are still unported: one `REVIEW_GATES_API_CYCLE_DAY` for every API seat) (SeatPool).
|
||||
- **Seat placement** (SeatPool, Seats, Drivers): The settings beside the seats count too. `run` (Gate 0) and `seats` each resolve the seats file from their own `--env-file` with `SeatPool::ResolveSeatsFile` (`--env-file`, `$SEATS_ENV_FILE`, `<cwd>/.env` when it is a file holding a seat or a placement setting, each with a value (a blank line is neither), or one that cannot be read as UTF-8 text (its error is then the command's) - a readable application `.env`, or one that is no file, is passed over, recorded as `passedOver` and logged by the run - the machine's own), reading each file once (`SeatsFile::Text()`: a pipe or a FIFO `--env-file` 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` - the one place, so `LoadSeats` takes the merged environment and reads no file). `SeatPool::SettingsOf` lays the file's placement settings (`Seats::SettingsFromEnvFile`, one dotenv grammar with the tokens', `DotenvLines`; `SeatPool::PlacementSettings`, the fifteen names the readers read, also deciding what makes `<cwd>/.env` a seats file - a test pins that each moves a reader) under the process environment into a map handed to the placement readers alone - `ApiConfig`, `PoolConfig`, `Seats::ConfigFromEnv` - names its other `REVIEW_GATES_*` lines (the run logs them, `seats` prints them on stderr), and the process environment is not touched, 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 Python driver's `config_env` of 2026-10-17; the first self-review's rounds 3 to 10, 2026-11-01, reworked it as an application to the process environment until round 11 found the map the readers already took) (SeatPool).
|
||||
- An account whose 5-hour limit Anthropic has removed reports the weekly window alone. Its burst window (`Seats::BurstWindow`) is the weekly one: it is ranked and admitted by the weekly window with the passes in flight projected on it (0.04 points per pass at xhigh, 0.125 otherwise), under the 99 % cutoff.
|
||||
- A pass's projected share of the windows follows the run's effort: 0.5 / 0.125 points of the 5-hour window and 0.125 / 0.04 of the weekly one at max / xhigh (`SeatPool::LensCostPct`, `LensCost7dPct`), the pooled means of 5438 passes on 141 seat-runs (2026-10-12..15) rounded up. The weekly cutoff is 99 %: the running passes are projected against it, so the last percent is spent a pass at a time.
|
||||
- Every OAuth account and the local login takes at most 128 passes at once, every driver's counted.
|
||||
|
|
@ -119,6 +119,7 @@ Conventions:
|
|||
- A 'concurrents' rejection pauses the seat for 20 s or its retry-after, not until a window's reset.
|
||||
- Board entries and the folded `foreign` record carry `cost7d`, and so does the dump's seat summary.
|
||||
- The `seats` report and the log table say `no limit` in such an account's 5-hour column.
|
||||
- **The ledger's cycles and credit-outs** (SeatPool, Seats, Drivers): an API seat's cycle starts on its grant day, `ANTHROPIC_API_KEY<n>_GRANT_DAY` (`Seats::ApiGrantDays`; the seat token reader takes the line), else `REVIEW_GATES_API_CYCLE_DAY`, the day every OpenRouter key's starts on; `LoadSeats` puts an API seat's day in the seat and the ledger (`Ledger::SetDay`). A new cycle starts the spend over (`Ledger::Entry`) and carries a credit-out, with the time the key was last seen answering again. `Probe` re-probes a paid key the ledger holds out on credit (`ledgerOut` `credit`; one out on its budget it skips) - an API key with a request, an OpenRouter key through `ProbeOpenRouter` - and `seats` every API key; `Ledger::Reconcile` has the ledger follow an ok or credit-error reading in one locked update on the file's entry: an ok on a key out since before the probe started lifts the mark - an API key's is new credits (`Ledger::GrantEntry`: a cycle from the grant day when that is within the week, else from today), an OpenRouter key keeps its cycle's spend - a credit error on a key the file has as fine, with no lift seen since the probe started, is recorded, and anything else writes nothing. A mark is local time with its UTC offset (`2026-10-25T02:00:05+01:00`) and marks are ordered by instant, across the hour that repeats when DST ends and between drivers in different zones; a stamp without an offset, as older drivers wrote, is read as local time. `seats` stamps what it writes after its probe, not at the start its probes share. An ok reading over a credit error recorded since the probe started leaves the seat out, and a key that answers with its cycle's budget spent is out on that budget. `seats` shows the probe's reading beside the driver's verdict (`LedgerOut`, the one `LoadSeats` holds a seat out by), the grant day and the next credits (`NextGrant`).
|
||||
- **Board identity and liveness, process lifetime, stop:** see "Inside the Flatpak".
|
||||
- **Edge cases:**
|
||||
- Usage errors of `run`, `freeze`, `slots`, `config` and the other commands exit 3, the preflight's code, and so does an INVARIANT failure of `run` and `slots`. The helpers exit 64.
|
||||
|
|
@ -180,6 +181,7 @@ What the binary reads at run time, and how. A variable the driver only sets or s
|
|||
|---|---|---|---|---|
|
||||
| `CLAUDE_CODE_OAUTH_TOKEN1`..`6` (+ legacy `CLAUDE_CODE_OAUTH_TOKEN`) | `:Seats` SeatTokensFromEnv, via `:SeatPool` LoadSeats (run) and `seats` | none | The fixed names 1..6 only; `TOKEN7` does nothing. Stripped; an empty value is no seat. The bare name is seat 1 when `TOKEN1` is empty. The env file's lines override the process environment. Stripped from every child's environment. | OAuth seats `1`..`6`; a token reaches claude only as its seat's `CLAUDE_CONFIG_DIR/.credentials.json` |
|
||||
| `ANTHROPIC_API_KEY1`..`6` | `:Seats` ApiKeysFromEnv (run, seats) | none | The fixed names 1..6, stripped; empty is no seat. The bare `ANTHROPIC_API_KEY` is never a seat (the env-file parser wants the digit too) and is stripped from every child. | API-credit seats `api1`..`api6`, each key in its own process's `ANTHROPIC_API_KEY` only |
|
||||
| `ANTHROPIC_API_KEY1_GRANT_DAY`..`6` | `:Seats` ApiGrantDays, via LoadSeats (run) and `seats` | `REVIEW_GATES_API_CYCLE_DAY` | Stripped decimal digits of any script, 1..31; anything else is ignored. The env file's lines override the process environment. | The day of the month seat n's ledger cycle starts on |
|
||||
| `OPENROUTER_KEY`, `OPENROUTER_KEY1`..`6` | `:Seats` OpenRouterKeysFromEnv, via LoadSeats (run; `seats` ignores them) | none | As the OAuth tokens; the bare name is `or1` when `KEY1` is empty. Never in `auto`: `--seats openrouter` or `orN` ids; mixed with other seats it is a preflight error (exit 3). | OpenRouter seats `or1`..`or6` |
|
||||
| `SEATS_ENV_FILE` | `:SeatPool` ResolveSeatsFile (run's Gate 0, `seats`) | `<cwd>/.env` if it is a file holding a seat or a placement setting, each with a value (or no UTF-8 text: its error is the command's), else the machine's `$XDG_CONFIG_HOME/crafter-review/env` (`~/.config/crafter-review/env`) | `--env-file` wins (in run and `seats` even `--env-file ""`, meaning no file); empty counts as unset. Seat lines are taken (`[export ]NAME=value`, one pair of matching quotes stripped), overriding the process environment; the file's placement settings (`SeatPool::PlacementSettings`) go, under the process environment, into the map the placement readers take (`SeatPool::SettingsOf`, from the one read the tokens come from too), so every placement knob below may come from the file, the process environment winning; no other reader sees a line of it, and its other `REVIEW_GATES_*` lines are named (the run's log, `seats` on stderr). A file that cannot be read, or is not UTF-8: run stops at preflight (exit 3), `seats` exits 1 with the read error. | The dotenv file the seat secrets come from |
|
||||
| `XDG_CONFIG_HOME` | `:SeatPool` GlobalEnvFile | `~/.config` | The directory whose `crafter-review/env` is the machine's own seats file, the last place a run or `seats` looks (2026-11-01). Runs, Commands and KernelModes, the tests that run `run` and `seats`, point it (and `HOST_XDG_CONFIG_HOME`) at a path nothing creates, so a global file on the machine stays out of them. |
|
||||
|
|
@ -195,7 +197,7 @@ What the binary reads at run time, and how. A variable the driver only sets or s
|
|||
| `REVIEW_GATES_API_BUDGET_USD` | `:SeatPool` ApiConfig (run, seats) | `200.0` | MAX_UTIL's syntax, kept if >= 0 (`inf` means no cap); empty, garbage, negative, `nan` give the default. `--api-budget` overrides it in run. | USD one API key may spend per cycle; at it the key is out |
|
||||
| `REVIEW_GATES_API_LENS_COST_USD` | `:SeatPool` ApiConfig | `1.0` | As API_BUDGET_USD | USD reserved per running process: a pass fits while spent + (running+1) × this + other drivers' cost ≤ budget |
|
||||
| `REVIEW_GATES_API_CONCURRENCY` | `:SeatPool` ApiConfig | `8` | As API_BUDGET_USD, truncated toward zero; `0` keeps every pass off the API seats; `inf` exits 1 with an OverflowError. | Processes at once on one API seat, every driver's counted |
|
||||
| `REVIEW_GATES_API_CYCLE_DAY` | `:SeatPool` ApiConfig | `1` | As API_CONCURRENCY, then clamped to 1..28. | The day of the month the ledger's spend resets |
|
||||
| `REVIEW_GATES_API_CYCLE_DAY` | `:SeatPool` ApiConfig | `1` | As API_CONCURRENCY, then clamped to 1..31; a day past a month's end is its last day. | The grant day of an API seat without its own `_GRANT_DAY`, and the day the driver's spend on every OpenRouter key starts over |
|
||||
| `REVIEW_GATES_OPENROUTER_BUDGET_USD` | `:SeatPool` ApiConfig | `100.0` | As API_BUDGET_USD | USD per cycle per OpenRouter key |
|
||||
| `REVIEW_GATES_OPENROUTER_LENS_COST_USD` | `:SeatPool` ApiConfig | `0.1` | As API_BUDGET_USD | Reserve per process on an OpenRouter seat |
|
||||
| `REVIEW_GATES_OPENROUTER_CONCURRENCY` | `:SeatPool` ApiConfig | `16` | As API_CONCURRENCY | Processes at once on one OpenRouter seat |
|
||||
|
|
|
|||
Loading…
Reference in a new issue