From edc3ce5d6319cfe9bbf54bdaa8b75f74bc5dfd59 Mon Sep 17 00:00:00 2001 From: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com> Date: Fri, 21 Aug 2026 01:24:46 +0000 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20implement=20issue=20#637=20?= =?UTF-8?q?=E2=80=94=20[Phase=201]=20ADR:=20agent=20rate-limit=20&=20circu?= =?UTF-8?q?it-breaker=20taxonomy=20(decision=20record)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/initiatives/agent-rate-limits-adr.md | 364 ++++++++++++++++++++++ 1 file changed, 364 insertions(+) create mode 100644 docs/initiatives/agent-rate-limits-adr.md diff --git a/docs/initiatives/agent-rate-limits-adr.md b/docs/initiatives/agent-rate-limits-adr.md new file mode 100644 index 00000000..1f25e8a1 --- /dev/null +++ b/docs/initiatives/agent-rate-limits-adr.md @@ -0,0 +1,364 @@ +# ADR: Agent Rate-Limit & Circuit-Breaker Taxonomy (mechanism + policy decision) + +- **Status:** Proposed (pending human sign-off on §6 numbers, §7 token-breaker threshold, and the §9 cost cap) +- **Date:** 2026-08-21 +- **Epic:** [#636](https://github.com/petry-projects/.github/issues/636) +- **Story:** [#637](https://github.com/petry-projects/.github/issues/637) — Phase 1 (decision record only) +- **Scope of this story:** documentation only. **No** workflow, config file, repo + setting, or org policy is mutated here (AC #1). + +This ADR pins down *how* an org can bound agentic throughput — per-agent-type +controls, a token-budget circuit breaker, the telemetry that feeds them, and the +public/private enforcement boundary — so the follow-on stories (config +single-source-of-truth, source-side gate, and the two wiring stories) encode a +**verified** mechanism instead of guessing APIs or baking in unconfirmed numbers. +It deliberately mirrors the proven staging of the Pull-Request-Limits ADR +([`docs/initiatives/pull-request-limits-adr.md`](pull-request-limits-adr.md), +epic #505 → ADR #506 → config #507 → apply path #508). + +--- + +## 1. Context — why this decision is needed (AC #1) + +Agentic automation in this org runs on a schedule and on event fan-out with **no +throughput ceiling of its own.** Three production incidents are the measured +baseline this standard must prevent — cited here as symptoms of missing +throughput controls, **not** as bugs to fix in this story: + +| Incident | Symptom | Root cause class | +|---|---|---| +| [#402](https://github.com/petry-projects/.github/issues/402) | A concurrency-group `cancel-in-progress` cancelled in-flight dev-lead issue pickups → **111 stuck issues**. | A single-lane serializer used as if it were a queue; cancellation instead of back-pressure. | +| [#443](https://github.com/petry-projects/.github/issues/443) | Dispatch race between event and schedule fan-out. | No cooldown / de-dupe between overlapping triggers. | +| [#571](https://github.com/petry-projects/.github/issues/571) | feature-ideation zero-job dispatch (reusable `with:` referencing `inputs` on an event where the job is gated off). | No admission/gating discipline around dispatch. | + +The external framing is **OWASP ASI08 — Denial of Service & Resource +Exhaustion** (OWASP Top 10 for Agentic Applications, 2026): an agent fleet with +no self-imposed rate limit can exhaust a shared, finite resource — here the +**rolling 5-hour Claude subscription budget** shared across every Claude-backed +agent — and starve the very pipeline that would repair it (the self-hosting +circular dependency already documented in +[`docs/initiatives/agent-canary-rings-adr.md`](agent-canary-rings-adr.md)). + +The planner's working assumption was that GitHub or the Claude platform ships a +first-party "per-workflow rate limiter" and a "5-hour budget %" surface, and that +we just need to find the API. **This ADR's first job was to verify that +assumption against the vendors' own docs rather than guess** (org standard: *"SHAs +for action pinning must be looked up via the GitHub API — never guessed"*, +[CLAUDE.md](../../CLAUDE.md); the same skepticism applies to any API surface — +[AGENTS.md](../../AGENTS.md)). + +--- + +## 2. Decision (summary) + +1. **In scope: four agent types** — `dev-lead`, `compliance-audit`, + `feature-ideation`, `initiative-driver` — each bounded on five control + dimensions: **max concurrent runs, max runtime, cooldown between runs, daily + execution budget, and a consecutive-failure circuit breaker** + (open → backoff → half-open). See §5 for the taxonomy and §6 for the proposed + values (proposals, not final). +2. **GitHub exposes no native per-workflow rate limiter and no run-count budget.** + Verified first-hand (§4): the `concurrency:` key is a **single-lane + serializer**, not a rate limiter — it caps *simultaneity*, not *rate over a + window*, and its `cancel-in-progress` is exactly the lever that caused #402. + There is no repo/org setting or ruleset rule that caps runs-per-hour or + runs-per-day. Therefore the per-agent-type controls are **source-side**, in the + dispatch/engine layer, not a GitHub toggle — the same conclusion the + PR-Limits ADR reached for open-PR caps. +3. **The Claude platform exposes no rolling 5-hour "subscription budget %" + surface.** Verified first-hand (§4): the Messages API exposes **per-minute** + token-bucket limits via `anthropic-ratelimit-*` response headers and a + Rate Limits API for *configured* limits — but **no 5-hour window and no + subscription-budget percentage.** The 5-hour rolling window is a **Claude + Pro/Max subscription** concept (shared across Claude + Claude Code, surfaced + interactively via `/status`), with **no pollable programmatic surface.** The + org's agents authenticate with the subscription-backed + `CLAUDE_CODE_OAUTH_TOKEN` (verified in the workflow stubs, §4), so the budget + that matters is precisely the one with no native telemetry. +4. **The token-budget breaker's telemetry must therefore be *derived*, not + *queried*** — and it can only be derived where the token accounting lives: the + private engine (`petry-projects/.github-private` `scripts/engine.sh`, + subscription-cap handling, [#206](https://github.com/petry-projects/.github/issues/206)). + Today that engine reacts **after** a rate-limit (exit-2 walks the model + fallback chain); a *proactive* breaker requires it to record per-run token + usage + rate-limit timestamps into private state. This splits enforcement + across a **public standard/gate** (this epic) and a **private companion + change** (§8). +5. All numeric values in §6, the §7 default threshold, and the §9 cost cap are + **proposals pending human sign-off**, not final numbers. + +> If a reviewer knows of a GitHub per-workflow rate-limit feature, or a +> programmatic Claude endpoint that returns remaining 5-hour subscription budget, +> that those doc surfaces do not describe, that is the one gap this story could +> not close from inside CI (§10). Re-open §2 with the citation if so. + +--- + +## 3. In-scope agent types (AC #2) + +Each is a real, deployed org automation whose caller stub was read first-hand for +this ADR (paths below). The "where behavior/concurrency lives today" column is +where the enforceable control must land given §2.2 (no GitHub-side surface). + +| Agent type | Caller (this repo) | Trigger surface | Where behavior/concurrency lives today | Auth | +|---|---|---|---|---| +| **dev-lead** | [`standards/workflows/dev-lead.yml`](../../standards/workflows/dev-lead.yml) | PR / review / comment / `issues: labeled` / `check_run` / `repository_dispatch` | Reusable `dev-lead-reusable.yml` (private): per-issue / per-PR concurrency lanes added after #402 | `CLAUDE_CODE_OAUTH_TOKEN` | +| **compliance-audit** | [`.github/workflows/compliance-audit-and-improvement.yml`](../../.github/workflows/compliance-audit-and-improvement.yml) | weekly `schedule` (Fri 12:00 UTC) + `workflow_dispatch` | `concurrency: compliance-audit`, `cancel-in-progress: false`; `audit` job `timeout-minutes: 30` | `ORG_SCORECARD_TOKEN` (deterministic job) + Claude phase | +| **feature-ideation** | [`standards/workflows/feature-ideation.yml`](../../standards/workflows/feature-ideation.yml) | weekly `schedule` (Fri 07:00 UTC) + `workflow_dispatch` + `discussion` | `concurrency: feature-ideation`, `cancel-in-progress: false`; `prep`/`redispatch` `timeout-minutes: 5` | `CLAUDE_CODE_OAUTH_TOKEN` | +| **initiative-driver** | [`standards/workflows/initiative-driver.yml`](../../standards/workflows/initiative-driver.yml) | `issues: [closed, labeled]` + off-peak `schedule` + `workflow_dispatch` | per-repo `concurrency` lane, `cancel-in-progress: true`; dispatch job `timeout-minutes: 5`; **max-in-flight lives in the central driver** (private) | dispatch-only stub; Claude runs in dev-lead it hands off to | + +Two facts from this table shape the whole design: + +- **The controllable throttle points are already partly in place but are the + *wrong kind*** — `concurrency` groups bound simultaneity, and `timeout-minutes` + bounds a single run's wall-clock. Neither bounds *rate over time*, *cooldown*, + or *daily budget*, and `cancel-in-progress` actively harms (§1, #402). +- **initiative-driver itself runs no LLM** (pure-bash dispatcher); its cost is + the **dev-lead fan-out it triggers.** Its control dimension is therefore a + **dispatch budget** (how many sub-issues it releases per window), enforced in + the central driver, not a per-run LLM budget. + +--- + +## 4. Mechanism research — what the vendors actually expose (AC #4) + +Every surface below was fetched **first-hand** during this story (2026-08-21) and +read for a field that caps *rate over a window* or exposes a *5-hour budget*. + +| Surface | Source consulted | Result | +|---|---|---| +| **GitHub Actions `concurrency`** | [Using concurrency](https://docs.github.com/en/actions/using-jobs/using-concurrency) | Controls **simultaneous execution only.** The key ensures "only one workflow or job with that key runs at any given time"; `cancel-in-progress` cancels the in-flight run (the #402 lever); `queue: max` allows up to 100 *pending* runs FIFO. **No per-hour/daily execution budget, no run-count-over-time limit, no cost/token telemetry.** | +| **Claude Messages API rate limits** | [Rate limits](https://platform.claude.com/docs/en/api/rate-limits) | Two limit types: **monthly spend cap** and **per-minute** rate limits (RPM / ITPM / OTPM) via a **token-bucket** (continuously replenished, *not* fixed-window). Exposes `anthropic-ratelimit-{requests,tokens,input-tokens,output-tokens}-{limit,remaining,reset}` response headers (reset = RFC 3339) and `retry-after` on 429. **No 5-hour window; no rolling-budget-% surface.** | +| **Claude Rate Limits API** | [Rate Limits API](https://platform.claude.com/docs/en/manage-claude/rate-limits-api) | Reads the org/workspace's *configured* limits programmatically. Returns configured ceilings, **not** live remaining budget for a 5-hour window. | +| **Claude Pro/Max subscription usage** | [Using Claude Code with your Pro/Max plan](https://support.claude.com/en/articles/11145838-using-claude-code-with-your-pro-or-max-plan) | Confirms "usage limits … shared across Claude and Claude Code" and an **interactive** `/status` command that shows "remaining allocation." **No programmatic/CI-pollable endpoint** for the remaining subscription budget is documented. | + +**Conclusion (AC #4):** there is **no native GitHub surface** for per-workflow +rate limiting, and **no native Claude surface** that exposes the rolling 5-hour +*subscription* budget. The nearest real signals are: + +1. **`anthropic-ratelimit-*` response headers** — a genuine, first-hand token + signal, but **per-minute and API-key-scoped**, and the org's agents run on the + **subscription OAuth token**, whose 5-hour window these headers do not report. +2. **The 429 + `retry-after` back-pressure** the engine already sees on cap + exhaustion (the reactive signal behind #206's exit-2 fallback). + +**Selected concrete alternative (the telemetry source of record):** because no +vendor surface reports the 5-hour subscription budget, the breaker's telemetry is +**derived source-side in the private engine.** `scripts/engine.sh` (which already +handles the subscription cap, #206) records, per agentic run, the tokens consumed +and the timestamp of any rate-limit/429, into a **private rolling-window ledger** +keyed to the 5-hour window. The proactive breaker (§7) reads that ledger. This is +the only place the accounting exists; it cannot be read from the public repo — the +basis for the §8 boundary. Where an agent path uses an **API key** rather than the +subscription token, the `anthropic-ratelimit-tokens-remaining` header is the +authoritative live signal and the ledger should prefer it over estimation. + +--- + +## 5. Per-agent-type control taxonomy (AC #2) + +Five control dimensions per agent type. Definitions (uniform across types): + +- **Max concurrent runs** — hard cap on simultaneous runs of this agent type. + Implemented as a *counted* admission check, **not** a `cancel-in-progress` + concurrency group (which cancels, per #402). Over the cap → **defer**, never + cancel. +- **Max runtime** — wall-clock ceiling per run. `timeout-minutes` is the native, + verified lever (§3) and is sufficient for this dimension. +- **Cooldown between runs** — minimum quiet interval after a run of this type + before another may start, to kill dispatch races (#443) and event/schedule + double-fire. +- **Daily execution budget** — max runs (or, for initiative-driver, max + dispatches) per rolling 24h, independent of concurrency. +- **Consecutive-failure circuit breaker** — after *N* consecutive failed runs the + breaker **opens** (no new runs); after a **backoff** it goes **half-open** + (a single probe run); a passing probe **closes** it, a failing probe re-opens + with escalated backoff. + +| Dimension | dev-lead | compliance-audit | feature-ideation | initiative-driver | +|---|---|---|---|---| +| Max concurrent runs | per-issue / per-PR lane (already) + a global type cap | 1 (already single-lane, non-cancelling) | 1 (already single-lane, non-cancelling) | 1 dispatch lane per repo (already) | +| Max runtime | per-run `timeout-minutes` (add to reusable) | 30 min audit job (already) | 5 min prep; add cap to `ideate` | 5 min dispatch (already) | +| Cooldown between runs | between issue pickups | n/a (weekly cron) | n/a (weekly cron) | between close-event + schedule fan-out (directly targets #443) | +| Daily execution budget | runs/day cap | n/a (weekly) | n/a (weekly) | **dispatches/day** (bounds the dev-lead fan-out it causes) | +| Consecutive-failure breaker | open→backoff→half-open | same | same | same | + +Cells marked "already" are existing, verified levers (§3); the standard's job is +to make them **uniform, named, and single-sourced**, and to add the missing +dimensions (global concurrency cap, cooldown, daily budget, failure breaker) at +the source layer. + +--- + +## 6. Proposed values — PROPOSALS, NOT FINAL (AC #2) + +> Everything here is a **proposal pending human sign-off** (epic #636 gate), +> anchored to the §1 baseline and current cadence — not to any vendor-imposed +> maximum (none exists, §4). + +| Dimension | dev-lead | compliance-audit | feature-ideation | initiative-driver | +|---|---|---|---|---| +| Max concurrent runs | 3–4 global | 1 | 1 | 1 / repo | +| Max runtime | 30 min | 30 min (keep) | 20 min | 5 min (keep) | +| Cooldown | 2–5 min between pickups | n/a | n/a | 10 min | +| Daily budget | 40–60 runs/day | 1 (weekly) | 1 (weekly) | 20 dispatches/day | +| Failure breaker | open after 3 consecutive; backoff 30 min → half-open | 3 → 60 min | 3 → 60 min | 3 → 30 min | + +Final values, and whether dev-lead's cap is global vs. per-repo, are **deferred to +human sign-off** before the config story. + +--- + +## 7. Token-budget circuit breaker (AC #3) + +The dimension the discussion specifically requested, distinct from the +per-agent-type breaker in §5. + +- **Trigger:** pause **new agentic dispatch** when the rolling **5-hour Claude + subscription budget** reaches a **configurable threshold (default 90%)** of + consumed budget. The window is the subscription's own 5-hour rolling window + (§4), not a per-minute API bucket. +- **State machine:** **open** at ≥ threshold → hold new dispatch; **half-open** as + the rolling window drains back below a lower resume mark (hysteresis, e.g. 80%, + to avoid flapping at the boundary) → admit a limited probe of dispatch; + **closed** once comfortably below. +- **Priority on recovery:** when the window drains and dispatch resumes, + **Claude-backed agents are prioritized** (dev-lead, feature-ideation, and the + Claude phase of compliance-audit) over lower-value/again-deferrable work, so the + recovering budget is spent on the pipeline that clears the backlog first. + initiative-driver dispatch (which *causes* Claude spend downstream) resumes + after the Claude-backed direct consumers. +- **Telemetry:** the derived private ledger of §4 — there is no native surface to + poll. The threshold is read from config (the config story), never hardcoded. +- **Fail-safe direction:** the breaker is **fail-open on telemetry error** (a + ledger read failure must not wedge the whole fleet), matching the fail-open + posture of the PR-Limits gate; but it is **fail-closed on a fresh 429 + + `retry-after`** (a hard, first-hand signal that the cap is already hit). + +The default 90% threshold, the 80% resume mark, and the exact priority order are +**proposals pending sign-off.** + +--- + +## 8. Cross-repo enforcement boundary (AC #5) + +Enforcement splits across two repos. Getting this boundary explicit is the whole +point of a decision record before the config/gate stories. + +### 8.1 Lands in the **public** standard/gate (this epic) + +- The **standard** (`standards/agent-rate-limits.md`, human-readable) and the + **single-source-of-truth config** (`standards/agent-rate-limits.json`, + machine-readable) — the taxonomy of §5, the values of §6, the §7 threshold, the + §9 metrics/cap. Mirrors the PR-Limits split + ([`standards/pr-limits.md`](../../standards/pr-limits.md) + + `standards/pr-limits.json`). +- A **source-side gate library** (`scripts/lib/…`) that reads the config and + answers admission questions — the per-type concurrency/cooldown/daily-budget + and failure-breaker checks that need only public inputs (run counts, + timestamps, failure counters derivable from the GitHub API), analogous to + [`scripts/lib/pr-limit-gate.sh`](../../scripts/lib/pr-limit-gate.sh). +- The caller stubs in [`standards/workflows/`](../../standards/workflows/) stay + **thin** (AGENTS.md rule); they gain no new logic — behavior lives in the + reusable/engine and is promoted centrally via the canary channel + ([agent-canary-rings-adr](agent-canary-rings-adr.md)). + +### 8.2 Requires a **private** companion change (`petry-projects/.github-private`) + +- The **token-budget breaker's telemetry** (§4/§7): only `scripts/engine.sh` + sees per-run token usage and the subscription 429s (#206). The **5-hour rolling + ledger and the proactive breaker read** must be built there. The public gate + can encode the *threshold and policy*, but the *live budget number* is private. +- **dev-lead's per-issue lanes and the initiative-driver max-in-flight** already + live in the private reusable/central driver (§3); the new concurrency-cap, + cooldown, and failure-breaker wiring for those two attach there. +- **Wiring stories** (the two follow-ons after config + gate) are therefore + cross-repo: one wires the public gate into the private engine's dispatch path; + the other builds the private 5-hour ledger + proactive breaker and points it at + the public threshold config. + +> **Boundary rule of thumb:** *policy and any control computable from public +> GitHub state* → public gate; *anything that needs the token ledger or the +> subscription 429 stream* → private engine. The public repo owns the numbers and +> the decision; the private repo owns the budget telemetry. + +--- + +## 9. Initiative success metrics & cost cap (AC #6) + +Pinned here so Phases 2–5 inherit them (proposals pending sign-off): + +### 9.1 Success metrics + +- **Zero recurrences** of the #402-class incident: no agent run is ever + *cancelled* mid-flight by a throughput control (deferral only) → target **0 + stuck-issue events** attributable to concurrency cancellation. +- **No dispatch races** (#443-class) or zero-job dispatches (#571-class) after the + cooldown/admission discipline lands. +- **Budget headroom:** the 5-hour subscription budget stays **below the §7 + threshold** in normal operation; when it trips, Claude-backed agents recover + first and the backlog clears without manual intervention. +- **Bounded fan-out:** initiative-driver never releases more than its daily + dispatch budget, so a single epic cannot flood dev-lead (the §1 fan-out risk). + +### 9.2 Operational cost cap the standard installs + +- A single, config-declared **org-wide agentic dispatch ceiling** (the aggregate + daily-budget sum across types) is the standing cost cap. It is expressed in + `standards/agent-rate-limits.json` and read at run time — changing it is a + config edit, not a redeploy (mirroring PR-Limits §6.1). The **concrete number is + deferred to human sign-off**; this ADR fixes only that the cap exists, is + single-sourced, and is enforced source-side. + +--- + +## 10. Residual gap / what this story could not close (AC #4 honesty) + +- **WebSearch was not used;** the four surfaces in §4 were fetched **first-hand** + via documentation URLs (GitHub Actions concurrency; Claude rate-limits; + Rate Limits API; Pro/Max subscription support article) and are the basis for the + decision. The Anthropic docs host redirected `docs.anthropic.com` → + `platform.claude.com` and `support.anthropic.com` → `support.claude.com`; both + redirects were followed and the canonical pages read. +- **The 5-hour window's exact mechanics are not publicly documented** at a + programmatic level — the subscription support page confirms the *existence* of + shared usage limits and the interactive `/status` view, but no page exposes a + pollable "remaining 5-hour budget" API. §4's selected alternative (derive it in + the private engine) stands on that verified absence. If Anthropic later ships a + subscription-budget endpoint, §7's telemetry source should be revisited to + prefer it over the derived ledger. + +--- + +## 11. Consequences + +- The follow-on stories are **build, not configure**: like PR-Limits, there is no + vendor toggle to flip — the work is a source-side gate, a private token ledger, + and thin, centrally-promoted wiring. +- The §6/§7/§9 numbers are explicitly provisional; the config story starts from a + human-signed set. +- `cancel-in-progress` is recorded as an **anti-pattern** for throughput control + (it cancels, per #402); all new controls **defer**, never cancel. +- The design is cross-repo by necessity (§8): the public repo cannot see the + budget telemetry, so the token breaker cannot be fully implemented in this epic + alone — a private companion change is a named dependency, not a surprise. + +## 12. References + +- [`docs/initiatives/pull-request-limits-adr.md`](pull-request-limits-adr.md) — the staged-epic ADR template mirrored here. +- [`docs/initiatives/agent-canary-rings-adr.md`](agent-canary-rings-adr.md) — the self-hosting circular-dependency + central-promotion model. +- [`standards/pr-limits.md`](../../standards/pr-limits.md) / [`scripts/lib/pr-limit-gate.sh`](../../scripts/lib/pr-limit-gate.sh) — the standard + source-side gate pattern to reuse. +- The four in-scope caller stubs (read first-hand): + - [`standards/workflows/dev-lead.yml`](../../standards/workflows/dev-lead.yml) + - [`standards/workflows/feature-ideation.yml`](../../standards/workflows/feature-ideation.yml) + - [`standards/workflows/initiative-driver.yml`](../../standards/workflows/initiative-driver.yml) + - [`.github/workflows/compliance-audit-and-improvement.yml`](../../.github/workflows/compliance-audit-and-improvement.yml) +- [AGENTS.md](../../AGENTS.md) / [CLAUDE.md](../../CLAUDE.md) — the "never guess an API surface" org rule. +- GitHub docs: [Using concurrency](https://docs.github.com/en/actions/using-jobs/using-concurrency). +- Claude docs (fetched first-hand): + - [Rate limits](https://platform.claude.com/docs/en/api/rate-limits) + - [Rate Limits API](https://platform.claude.com/docs/en/manage-claude/rate-limits-api) + - [Using Claude Code with your Pro/Max plan](https://support.claude.com/en/articles/11145838-using-claude-code-with-your-pro-or-max-plan) +- Cross-repo: `petry-projects/.github-private` `scripts/engine.sh` — 5-hour subscription-cap handling ([#206](https://github.com/petry-projects/.github/issues/206)). +- Baseline incidents: [#402](https://github.com/petry-projects/.github/issues/402), [#443](https://github.com/petry-projects/.github/issues/443), [#571](https://github.com/petry-projects/.github/issues/571). +- External framing: OWASP Top 10 for Agentic Applications (2026), **ASI08 — Denial of Service & Resource Exhaustion**. From f017aa7f602a41ad9d010839876d5e58bf256ea0 Mon Sep 17 00:00:00 2001 From: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com> Date: Fri, 21 Aug 2026 01:29:09 +0000 Subject: [PATCH 2/4] fix(reviews): address review comments [skip ci-relay] --- docs/initiatives/agent-rate-limits-adr.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/initiatives/agent-rate-limits-adr.md b/docs/initiatives/agent-rate-limits-adr.md index 1f25e8a1..ca0b1fb1 100644 --- a/docs/initiatives/agent-rate-limits-adr.md +++ b/docs/initiatives/agent-rate-limits-adr.md @@ -230,10 +230,10 @@ per-agent-type breaker in §5. after the Claude-backed direct consumers. - **Telemetry:** the derived private ledger of §4 — there is no native surface to poll. The threshold is read from config (the config story), never hardcoded. -- **Fail-safe direction:** the breaker is **fail-open on telemetry error** (a +- **Fail-safe direction:** the breaker **fails to a 'closed' (allow) state on telemetry error** (a ledger read failure must not wedge the whole fleet), matching the fail-open - posture of the PR-Limits gate; but it is **fail-closed on a fresh 429 + - `retry-after`** (a hard, first-hand signal that the cap is already hit). + posture of the PR-Limits gate; but it **fails to an 'open' (block) state on a fresh 429 + + retry-after** (a hard, first-hand signal that the cap is already hit). The default 90% threshold, the 80% resume mark, and the exact priority order are **proposals pending sign-off.** @@ -253,7 +253,7 @@ point of a decision record before the config/gate stories. §9 metrics/cap. Mirrors the PR-Limits split ([`standards/pr-limits.md`](../../standards/pr-limits.md) + `standards/pr-limits.json`). -- A **source-side gate library** (`scripts/lib/…`) that reads the config and +- A **source-side gate library** (`scripts/lib/agent-limit-gate.sh`) that reads the config and answers admission questions — the per-type concurrency/cooldown/daily-budget and failure-breaker checks that need only public inputs (run counts, timestamps, failure counters derivable from the GitHub API), analogous to From f4c9f4708e2faab317e5353c6551ef7ff26fef3a Mon Sep 17 00:00:00 2001 From: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com> Date: Fri, 21 Aug 2026 01:39:31 +0000 Subject: [PATCH 3/4] chore: dev-lead update (review-changes) [skip ci-relay] --- docs/initiatives/agent-rate-limits-adr.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/initiatives/agent-rate-limits-adr.md b/docs/initiatives/agent-rate-limits-adr.md index ca0b1fb1..56744b34 100644 --- a/docs/initiatives/agent-rate-limits-adr.md +++ b/docs/initiatives/agent-rate-limits-adr.md @@ -73,7 +73,7 @@ for action pinning must be looked up via the GitHub API — never guessed"*, Pro/Max subscription** concept (shared across Claude + Claude Code, surfaced interactively via `/status`), with **no pollable programmatic surface.** The org's agents authenticate with the subscription-backed - `CLAUDE_CODE_OAUTH_TOKEN` (verified in the workflow stubs, §4), so the budget + `CLAUDE_CODE_OAUTH_TOKEN` (verified in the workflow stubs, §3), so the budget that matters is precisely the one with no native telemetry. 4. **The token-budget breaker's telemetry must therefore be *derived*, not *queried*** — and it can only be derived where the token accounting lives: the From 567566802f31060be1e8edcd26513a99a6017ddb Mon Sep 17 00:00:00 2001 From: Don Petry Date: Thu, 20 Aug 2026 20:51:25 -0500 Subject: [PATCH 4/4] =?UTF-8?q?fix(adr):=20correct=20AC=20#4=20=E2=80=94?= =?UTF-8?q?=20the=20subscription=20budget=20IS=20queryable=20[skip=20ci-re?= =?UTF-8?q?lay]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ADR concluded no native Claude surface exposes the rolling subscription budget and selected a derived, estimated ledger as the telemetry source of record. A verified endpoint exists and had been posted to #637 before the implementing run started. - §2.3/§2.4, §4: reverse the AC #4 conclusion. Add §4.1 naming GET /api/oauth/usage as the source of record — probed first-hand (HTTP 200, 2026-08-20), subscription-OAuth-scoped, returns the 5-hour and 7-day windows in one call. Record the load-bearing User-Agent header, the >=180s polling guidance, and the limits[] contract. - Demote the derived engine ledger to the documented degraded path rather than deleting it — an estimate is strictly worse than an authoritative server-side percentage, but it is the right fallback for an undocumented endpoint. - §2.5: a weekly_scoped (per-model) exhaustion must never trip a fleet pause; the model-fallback chain handles it. Observed live: one model at 100%/critical while the account-wide weekly window sat at 85%. - §4.1/§7: resets_at is authoritative for all time-until-reset arithmetic — no hardcoded weekday, cron, or timezone constant. - §7: add the 7-day glide-path trigger (#994) alongside the 5-hour trigger, and restate the fail-safe as behaviour (allow on telemetry error, block on a fresh 429) instead of the inverted open/closed vocabulary. - §8.1: restore scripts/lib/agent-rate-limit.sh — f017aa7f renamed it to agent-limit-gate.sh, contradicting #639 (its title), #641 and #994. - §8.2: record that writing an org-level Actions variable needs admin:org or the fine-grained org Variables:write permission — a repo/workflow/read:org token gets 403 even for an org admin. Genuine new prerequisite. - §10: the residual gap is no longer "no source exists" but "the source is undocumented" plus the unverified setup-token credential class. Refs #637, #641, #994, .github-private#1525, .github-private#1565 Co-Authored-By: Claude Opus 5 (1M context) --- docs/initiatives/agent-rate-limits-adr.md | 266 ++++++++++++++++------ 1 file changed, 194 insertions(+), 72 deletions(-) diff --git a/docs/initiatives/agent-rate-limits-adr.md b/docs/initiatives/agent-rate-limits-adr.md index 56744b34..e936681d 100644 --- a/docs/initiatives/agent-rate-limits-adr.md +++ b/docs/initiatives/agent-rate-limits-adr.md @@ -65,33 +65,40 @@ for action pinning must be looked up via the GitHub API — never guessed"*, runs-per-day. Therefore the per-agent-type controls are **source-side**, in the dispatch/engine layer, not a GitHub toggle — the same conclusion the PR-Limits ADR reached for open-PR caps. -3. **The Claude platform exposes no rolling 5-hour "subscription budget %" - surface.** Verified first-hand (§4): the Messages API exposes **per-minute** - token-bucket limits via `anthropic-ratelimit-*` response headers and a - Rate Limits API for *configured* limits — but **no 5-hour window and no - subscription-budget percentage.** The 5-hour rolling window is a **Claude - Pro/Max subscription** concept (shared across Claude + Claude Code, surfaced - interactively via `/status`), with **no pollable programmatic surface.** The - org's agents authenticate with the subscription-backed - `CLAUDE_CODE_OAUTH_TOKEN` (verified in the workflow stubs, §3), so the budget - that matters is precisely the one with no native telemetry. -4. **The token-budget breaker's telemetry must therefore be *derived*, not - *queried*** — and it can only be derived where the token accounting lives: the - private engine (`petry-projects/.github-private` `scripts/engine.sh`, - subscription-cap handling, [#206](https://github.com/petry-projects/.github/issues/206)). - Today that engine reacts **after** a rate-limit (exit-2 walks the model - fallback chain); a *proactive* breaker requires it to record per-run token - usage + rate-limit timestamps into private state. This splits enforcement - across a **public standard/gate** (this epic) and a **private companion - change** (§8). -5. All numeric values in §6, the §7 default threshold, and the §9 cost cap are +3. **Claude's *documented* API surfaces do not expose the rolling subscription + budget — but an undocumented OAuth endpoint does.** Verified first-hand (§4): + the Messages API exposes only **per-minute** token-bucket limits via + `anthropic-ratelimit-*` response headers, and the Rate Limits API returns + *configured* ceilings. Neither reports the subscription window, and both are + **API-key-scoped**, while the org's agents authenticate with the + subscription-backed `CLAUDE_CODE_OAUTH_TOKEN` (verified in the workflow stubs, + §3). That gap is closed by `GET https://api.anthropic.com/api/oauth/usage` + (§4.1) — the subscription-OAuth-scoped endpoint backing Claude Code's `/usage` + view. It was probed first-hand (HTTP 200, live window state, 2026-08-20) using + the same token class the fleet already holds, and returns **both** the 5-hour + and the 7-day windows with server-computed severity and exact reset timestamps. + It is **undocumented and unsupported**, which dictates the adapter boundary and + fail-safe below — not avoidance. +4. **The breaker's telemetry is therefore *queried*, with a *derived* fallback.** + The §4.1 endpoint is the **source of record**. Because it is undocumented, + every read goes through a small adapter, and a **derived ledger** in the private + engine (`petry-projects/.github-private` `scripts/engine.sh`, subscription-cap + handling, [#206](https://github.com/petry-projects/.github/issues/206)) is + retained as the documented **degraded path** for when the endpoint is + unavailable or changes shape. An estimate is strictly worse than an + authoritative server-side percentage, so it is the fallback, never the primary. + Enforcement still splits across a **public standard/gate** (this epic) and a + **private companion change** (§8), because the credential and the poller live + private. +5. **Not every limit is a fleet-stop.** The endpoint distinguishes account-wide + windows (`session`, `weekly_all`) from **per-model** windows (`weekly_scoped`). + Only account-wide windows are pause-worthy; a scoped exhaustion is handled by + the engine's existing model-fallback chain — swap models, do not stop the fleet. + The probe caught exactly this case: one model's weekly bucket at + 100%/critical/active while the account-wide weekly window sat at 85%. +6. All numeric values in §6, the §7 thresholds, and the §9 cost cap are **proposals pending human sign-off**, not final numbers. -> If a reviewer knows of a GitHub per-workflow rate-limit feature, or a -> programmatic Claude endpoint that returns remaining 5-hour subscription budget, -> that those doc surfaces do not describe, that is the one gap this story could -> not close from inside CI (§10). Re-open §2 with the citation if so. - --- ## 3. In-scope agent types (AC #2) @@ -130,28 +137,74 @@ read for a field that caps *rate over a window* or exposes a *5-hour budget*. | **GitHub Actions `concurrency`** | [Using concurrency](https://docs.github.com/en/actions/using-jobs/using-concurrency) | Controls **simultaneous execution only.** The key ensures "only one workflow or job with that key runs at any given time"; `cancel-in-progress` cancels the in-flight run (the #402 lever); `queue: max` allows up to 100 *pending* runs FIFO. **No per-hour/daily execution budget, no run-count-over-time limit, no cost/token telemetry.** | | **Claude Messages API rate limits** | [Rate limits](https://platform.claude.com/docs/en/api/rate-limits) | Two limit types: **monthly spend cap** and **per-minute** rate limits (RPM / ITPM / OTPM) via a **token-bucket** (continuously replenished, *not* fixed-window). Exposes `anthropic-ratelimit-{requests,tokens,input-tokens,output-tokens}-{limit,remaining,reset}` response headers (reset = RFC 3339) and `retry-after` on 429. **No 5-hour window; no rolling-budget-% surface.** | | **Claude Rate Limits API** | [Rate Limits API](https://platform.claude.com/docs/en/manage-claude/rate-limits-api) | Reads the org/workspace's *configured* limits programmatically. Returns configured ceilings, **not** live remaining budget for a 5-hour window. | -| **Claude Pro/Max subscription usage** | [Using Claude Code with your Pro/Max plan](https://support.claude.com/en/articles/11145838-using-claude-code-with-your-pro-or-max-plan) | Confirms "usage limits … shared across Claude and Claude Code" and an **interactive** `/status` command that shows "remaining allocation." **No programmatic/CI-pollable endpoint** for the remaining subscription budget is documented. | +| **Claude Pro/Max subscription usage** | [Using Claude Code with your Pro/Max plan](https://support.claude.com/en/articles/11145838-using-claude-code-with-your-pro-or-max-plan) | Confirms "usage limits … shared across Claude and Claude Code" and an **interactive** `/status` command that shows "remaining allocation." **No programmatic endpoint is *documented*** — but the interactive view is served by one (next row). | +| **OAuth usage endpoint** (undocumented) | **Probed first-hand 2026-08-20 — HTTP 200, live window state.** No doc page exists; this is the surface behind Claude Code's `/usage` view. | **Reports the subscription windows directly** — 5-hour, 7-day, and per-model — scoped to the subscription OAuth token, with `percent`, server-computed `severity`, and exact `resets_at`. **This is the source of record (§4.1).** | **Conclusion (AC #4):** there is **no native GitHub surface** for per-workflow -rate limiting, and **no native Claude surface** that exposes the rolling 5-hour -*subscription* budget. The nearest real signals are: - -1. **`anthropic-ratelimit-*` response headers** — a genuine, first-hand token - signal, but **per-minute and API-key-scoped**, and the org's agents run on the - **subscription OAuth token**, whose 5-hour window these headers do not report. -2. **The 429 + `retry-after` back-pressure** the engine already sees on cap - exhaustion (the reactive signal behind #206's exit-2 fallback). - -**Selected concrete alternative (the telemetry source of record):** because no -vendor surface reports the 5-hour subscription budget, the breaker's telemetry is -**derived source-side in the private engine.** `scripts/engine.sh` (which already -handles the subscription cap, #206) records, per agentic run, the tokens consumed -and the timestamp of any rate-limit/429, into a **private rolling-window ledger** -keyed to the 5-hour window. The proactive breaker (§7) reads that ledger. This is -the only place the accounting exists; it cannot be read from the public repo — the -basis for the §8 boundary. Where an agent path uses an **API key** rather than the -subscription token, the `anthropic-ratelimit-tokens-remaining` header is the -authoritative live signal and the ledger should prefer it over estimation. +rate limiting — that half of §2.2 stands. For Claude, the *documented* surfaces do +not report the subscription budget, but a **first-hand-verified undocumented +endpoint does**, and it is the telemetry source of record. + +### 4.1 Telemetry source of record — the OAuth usage endpoint + +``` +GET https://api.anthropic.com/api/oauth/usage + Authorization: Bearer + anthropic-beta: oauth-2025-04-20 + User-Agent: claude-code/ +``` + +Probed first-hand on **2026-08-20: HTTP 200 with live window state.** This is the +data behind Claude Code's `/usage` view. It resolves the exact gap the rows above +identify: `anthropic-ratelimit-*` headers are per-minute and **API-key-scoped**, +whereas this endpoint is **subscription-OAuth-scoped** — it authenticates with the +same `CLAUDE_CODE_OAUTH_TOKEN` the fleet already holds and reports the windows +that actually bind the fleet. + +Contract notes that follow-on stories must honour: + +- **The `User-Agent` is load-bearing, not cosmetic.** Without a + `claude-code/` value the caller lands in an aggressively rate-limited + bucket and receives persistent 429s. Rate limiting is **per access token**, not + per account; polling at **≥180 s** is safe, so an hourly poller sits far inside + budget. +- **Prefer the `limits[]` array over the flattened `five_hour` / `seven_day` + keys.** Each entry is `{ kind, group, percent, severity, resets_at, is_active }` + where `kind` ∈ `session` (5-hour), `weekly_all` (7-day), `weekly_scoped` + (per-model). The server-computed `severity` (`normal` / `warning` / `critical`) + gives the gate a graded signal instead of re-deriving one from a bare number. + Treat the flattened keys as the fallback for older response shapes. +- **`resets_at` is authoritative.** All time-until-reset arithmetic derives from + this field — never a hardcoded weekday, cron expression, or timezone constant — + so behaviour self-corrects if the window ever shifts. (The probe returned + `2026-08-25T15:59:59Z` for the weekly window, i.e. Tuesday ~11:00 + America/Chicago, matching the maintainer's understanding without being + configured anywhere.) +- **`weekly_scoped` must never trip a fleet pause** (§2.5). Only `session` and + `weekly_all` are pause-worthy. +- **Both breakers share one call.** The 5-hour breaker (Phase 5) and the 7-day + glide-path breaker (Phase 6) read `session` and `weekly_all` from the same + response, behind one adapter. + +**Degraded path (retained, demoted).** Because the endpoint is undocumented and +unsupported, it can change shape or disappear. The fallback is the **derived +ledger** in the private engine: `scripts/engine.sh` (which already handles the +subscription cap, #206) records per-run token consumption and rate-limit +timestamps into a private rolling-window ledger. The adapter prefers the endpoint +and falls back to the ledger; the ledger is an estimate and is never the primary +source. Where an agent path uses an **API key** rather than the subscription +token, `anthropic-ratelimit-tokens-remaining` remains the authoritative live +signal for that path. + +**Open verification (carried, not closed).** The probe used a short-lived OAuth +access token from a local credential store; CI authenticates with the long-lived +`claude setup-token` credential. Both are OAuth-issued and share the +`sk-ant-oat01-` prefix, so acceptance is likely but **unproven**. It is verified by +running the poller in **dry-run in CI against the existing secret** — *not* by +minting a fresh setup-token, which risks invalidating the credential the fleet +currently runs on. If the long-lived token is rejected, the fallback is a +dedicated short-lived credential refreshed by the workflow, a materially larger +change that should be re-scoped before implementation. --- @@ -214,10 +267,27 @@ human sign-off** before the config story. The dimension the discussion specifically requested, distinct from the per-agent-type breaker in §5. -- **Trigger:** pause **new agentic dispatch** when the rolling **5-hour Claude - subscription budget** reaches a **configurable threshold (default 90%)** of - consumed budget. The window is the subscription's own 5-hour rolling window - (§4), not a per-minute API bucket. +- **Trigger (5-hour window, `session`):** pause **new agentic dispatch** when the + rolling **5-hour Claude subscription budget** reaches a **configurable threshold + (default 90%)** of consumed budget. The window is the subscription's own 5-hour + rolling window (§4.1), not a per-minute API bucket. +- **Trigger (7-day window, `weekly_all`):** pause when the weekly budget crosses a + threshold that **tightens as the reset approaches**, reserving a configurable + share of the budget per remaining day: + + ``` + pause when weekly_all.percent >= 100 - (reserve_pct_per_day * days_until_reset) + ``` + + With `reserve_pct_per_day = 2` (proposal) that is 98% the day before reset, 96% + two days before, down to an 86% floor immediately after a reset. `days_until_reset` + derives from `resets_at` (§4.1) with partial days rounding **up**. The floor is the + "don't spend the whole week on day one" guard; the near-100% ceiling on reset day + is deliberate, since a budget minutes from refilling needs no protection. The two + triggers are orthogonal: the 5-hour breaker bounds a *burst*, the weekly breaker + bounds the *shape of the week*. +- **Scope guard:** neither trigger fires on `weekly_scoped` (per-model) + exhaustion — see §2.5. - **State machine:** **open** at ≥ threshold → hold new dispatch; **half-open** as the rolling window drains back below a lower resume mark (hysteresis, e.g. 80%, to avoid flapping at the boundary) → admit a limited probe of dispatch; @@ -228,15 +298,25 @@ per-agent-type breaker in §5. recovering budget is spent on the pipeline that clears the backlog first. initiative-driver dispatch (which *causes* Claude spend downstream) resumes after the Claude-backed direct consumers. -- **Telemetry:** the derived private ledger of §4 — there is no native surface to - poll. The threshold is read from config (the config story), never hardcoded. -- **Fail-safe direction:** the breaker **fails to a 'closed' (allow) state on telemetry error** (a - ledger read failure must not wedge the whole fleet), matching the fail-open - posture of the PR-Limits gate; but it **fails to an 'open' (block) state on a fresh 429 + - retry-after** (a hard, first-hand signal that the cap is already hit). - -The default 90% threshold, the 80% resume mark, and the exact priority order are -**proposals pending sign-off.** +- **Telemetry:** the OAuth usage endpoint of §4.1, read through the adapter, with + the derived private ledger as the documented fallback. Thresholds are read from + config (the config story), never hardcoded. +- **Fail-safe direction:** stated as behaviour rather than open/closed vocabulary, + which inverts confusingly. On **telemetry error** — non-200, malformed body, or + a missing window entry — the breaker **allows dispatch and logs a warning.** An + outage of an undocumented third-party endpoint must never stop the fleet; that is + the failure mode this epic exists to prevent, and the engine's reactive exit-2 + fallback (#206) remains the protection of last resort. On a **fresh 429 + + `retry-after`** the breaker **blocks** — a hard, first-hand signal that the cap + is already hit. +- **Resume:** utilization is monotonic within a window, so a tripped breaker does + not un-trip on a percentage dip; the clean resume is the window reset given by + `resets_at`. Where a pause is actuated by a persisted switch, the actuator must + record that *automation* set it, so an automatic resume never clears a pause a + human set deliberately (see `.github-private#1525`). + +The default 90% threshold, the 80% resume mark, `reserve_pct_per_day = 2`, and the +exact priority order are **proposals pending sign-off.** --- @@ -253,7 +333,8 @@ point of a decision record before the config/gate stories. §9 metrics/cap. Mirrors the PR-Limits split ([`standards/pr-limits.md`](../../standards/pr-limits.md) + `standards/pr-limits.json`). -- A **source-side gate library** (`scripts/lib/agent-limit-gate.sh`) that reads the config and +- A **source-side gate library** (`scripts/lib/agent-rate-limit.sh` — the name + fixed by the Phase 3 story) that reads the config and answers admission questions — the per-type concurrency/cooldown/daily-budget and failure-breaker checks that need only public inputs (run counts, timestamps, failure counters derivable from the GitHub API), analogous to @@ -265,10 +346,25 @@ point of a decision record before the config/gate stories. ### 8.2 Requires a **private** companion change (`petry-projects/.github-private`) -- The **token-budget breaker's telemetry** (§4/§7): only `scripts/engine.sh` - sees per-run token usage and the subscription 429s (#206). The **5-hour rolling - ledger and the proactive breaker read** must be built there. The public gate - can encode the *threshold and policy*, but the *live budget number* is private. +- The **token-budget breaker's telemetry** (§4.1/§7): the usage endpoint + authenticates with `CLAUDE_CODE_OAUTH_TOKEN`, which lives private, so the + **adapter and the poller** are built there — as is the fallback ledger, since only + `scripts/engine.sh` sees per-run token usage and the subscription 429s (#206). + The public gate encodes the *thresholds and policy*; the *live budget read* is + private. +- **Actuating a fleet-wide pause needs a permission the fleet does not currently + have.** Writing an **org-level** Actions variable returns + `403 — must be an org admin or have the actions variables fine-grained + permission` for a classic token carrying `repo` / `workflow` / `read:org`, + **even when the caller is already an org admin** (verified 2026-08-20). It + requires `admin:org` (classic) or the fine-grained org **Variables: write** + permission. Repo-level variables are writable under plain `repo`. This is a + genuine new prerequisite, not an existing capability — the fallback is per-repo + variables at the cost of N writes and a slower fleet-wide stop. +- The **pause switch itself already exists**: `AGENTS_PAUSED=true`, an Actions + *variable* (visible where secrets are not), delivered by + `.github-private#1525`. The breaker automates setting and clearing it; it does + not redesign it. - **dev-lead's per-issue lanes and the initiative-driver max-in-flight** already live in the private reusable/central driver (§3); the new concurrency-cap, cooldown, and failure-breaker wiring for those two attach there. @@ -298,6 +394,10 @@ Pinned here so Phases 2–5 inherit them (proposals pending sign-off): - **Budget headroom:** the 5-hour subscription budget stays **below the §7 threshold** in normal operation; when it trips, Claude-backed agents recover first and the backlog clears without manual intervention. +- **Weekly headroom:** the 7-day budget never exhausts before its reset — the + glide path leaves usable Claude capacity for humans on every day of the week, + and a deliberate budget pause is never again diagnosed as a fleet outage + (`.github-private#1525`). - **Bounded fan-out:** initiative-driver never releases more than its daily dispatch budget, so a single epic cannot flood dev-lead (the §1 fan-out risk). @@ -320,21 +420,32 @@ Pinned here so Phases 2–5 inherit them (proposals pending sign-off): decision. The Anthropic docs host redirected `docs.anthropic.com` → `platform.claude.com` and `support.anthropic.com` → `support.claude.com`; both redirects were followed and the canonical pages read. -- **The 5-hour window's exact mechanics are not publicly documented** at a - programmatic level — the subscription support page confirms the *existence* of - shared usage limits and the interactive `/status` view, but no page exposes a - pollable "remaining 5-hour budget" API. §4's selected alternative (derive it in - the private engine) stands on that verified absence. If Anthropic later ships a - subscription-budget endpoint, §7's telemetry source should be revisited to - prefer it over the derived ledger. +- **The telemetry source is undocumented, and that is the standing risk.** §4.1's + endpoint is real and verified, but it appears on no documentation page and + carries no compatibility guarantee; it can change shape or disappear without + notice. Upstream requests for a supported surface are open + ([anthropics/claude-code#44328](https://github.com/anthropics/claude-code/issues/44328), + [#32796](https://github.com/anthropics/claude-code/issues/32796)). This is why + every read is adapter-wrapped, why the derived ledger is retained as the degraded + path, and why telemetry failure allows rather than blocks (§7). **If Anthropic + ships a supported endpoint, §4.1 should migrate to it.** +- **The CI credential class is unverified.** See §4.1's open-verification note: + the probe used a short-lived local OAuth token, not the long-lived + `claude setup-token` credential CI uses. Closed by a dry-run in CI, not by + minting a new token. +- **The window's internal mechanics remain undocumented** — the endpoint reports + `percent` and `resets_at` but not how consumption maps to them, so the ADR + treats the reported percentage as opaque and authoritative rather than modelling + it. --- ## 11. Consequences - The follow-on stories are **build, not configure**: like PR-Limits, there is no - vendor toggle to flip — the work is a source-side gate, a private token ledger, - and thin, centrally-promoted wiring. + vendor toggle to flip — the work is a source-side gate, a private telemetry + adapter and poller over §4.1 (with the derived ledger as its fallback), and + thin, centrally-promoted wiring. - The §6/§7/§9 numbers are explicitly provisional; the config story starts from a human-signed set. - `cancel-in-progress` is recorded as an **anti-pattern** for throughput control @@ -359,6 +470,17 @@ Pinned here so Phases 2–5 inherit them (proposals pending sign-off): - [Rate limits](https://platform.claude.com/docs/en/api/rate-limits) - [Rate Limits API](https://platform.claude.com/docs/en/manage-claude/rate-limits-api) - [Using Claude Code with your Pro/Max plan](https://support.claude.com/en/articles/11145838-using-claude-code-with-your-pro-or-max-plan) +- **Telemetry source of record (§4.1):** `GET https://api.anthropic.com/api/oauth/usage` + — undocumented; probed first-hand 2026-08-20 (HTTP 200). Upstream requests for a + supported equivalent: + [anthropics/claude-code#44328](https://github.com/anthropics/claude-code/issues/44328), + [#32796](https://github.com/anthropics/claude-code/issues/32796). +- Follow-on stories reading §4.1: [#641](https://github.com/petry-projects/.github/issues/641) + (Phase 5, 5-hour breaker) and [#994](https://github.com/petry-projects/.github/issues/994) + (Phase 6, 7-day glide-path breaker). +- Cross-repo: `petry-projects/.github-private#1525` — `AGENTS_PAUSED` as a + first-class state (the switch §7 automates); `.github-private#1565` — the + adapter/poller companion. - Cross-repo: `petry-projects/.github-private` `scripts/engine.sh` — 5-hour subscription-cap handling ([#206](https://github.com/petry-projects/.github/issues/206)). - Baseline incidents: [#402](https://github.com/petry-projects/.github/issues/402), [#443](https://github.com/petry-projects/.github/issues/443), [#571](https://github.com/petry-projects/.github/issues/571). - External framing: OWASP Top 10 for Agentic Applications (2026), **ASI08 — Denial of Service & Resource Exhaustion**.