gh-glance 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CHANGELOG.md +78 -1
  2. package/index.mjs +583 -183
  3. package/package.json +1 -1
package/index.mjs CHANGED
@@ -2098,6 +2098,7 @@ function scheduleIntents({
2098
2098
  cursors = {},
2099
2099
  nowMs,
2100
2100
  maxGrants = Number.POSITIVE_INFINITY,
2101
+ manualStreak = 0,
2101
2102
  }) {
2102
2103
  const valid = [];
2103
2104
  const prunedIntentIds = [];
@@ -2138,11 +2139,29 @@ function scheduleIntents({
2138
2139
  const liveLeaseCount = countLiveLeases(leases, nowMs);
2139
2140
  const priorities = [...new Set(valid.map((intent) => intent.normalizedPriority))].sort((a, b) => a - b);
2140
2141
 
2141
- for (const priority of priorities) {
2142
- const pending = valid.filter((intent) => intent.normalizedPriority === priority);
2142
+ let streak = Number.isSafeInteger(manualStreak) && manualStreak > 0 ? manualStreak : 0;
2143
+ const grantedIds = new Set();
2144
+ // Priority still decides every other question. This says only that "higher
2145
+ // priority" cannot mean "always": once manual work has taken the last three
2146
+ // turns and an active owner is waiting, that owner takes this one. The turn
2147
+ // is a single grant, and it is subject to exactly the same budget, phase and
2148
+ // lane checks as any other -- fairness reorders work, it never admits work
2149
+ // that could not otherwise be paid for.
2150
+ const owedActiveTurn = streak >= MANUAL_GRANT_STREAK_LIMIT &&
2151
+ valid.some((intent) => intent.normalizedPriority === REQUEST_PRIORITIES.active);
2152
+ const passes = owedActiveTurn
2153
+ ? [{ priority: REQUEST_PRIORITIES.active, limit: 1, record: false },
2154
+ ...priorities.map((priority) => ({ priority, limit: Number.POSITIVE_INFINITY, record: true }))]
2155
+ : priorities.map((priority) => ({ priority, limit: Number.POSITIVE_INFINITY, record: true }));
2156
+
2157
+ for (const pass of passes) {
2158
+ const priority = pass.priority;
2159
+ const pending = valid.filter((intent) =>
2160
+ intent.normalizedPriority === priority && !grantedIds.has(intent.id));
2143
2161
  const roundRobinState = createRoundRobinState(pending, updatedCursors);
2162
+ let grantedThisPass = 0;
2144
2163
  while (pending.length > 0) {
2145
- if (grants.length >= maxGrants) break;
2164
+ if (grants.length >= maxGrants || grantedThisPass >= pass.limit) break;
2146
2165
  const intent = pending.splice(nextRoundRobinIndex(pending, roundRobinState), 1)[0];
2147
2166
  const resources = RATE_RESOURCES.filter((resource) => intent.costs[resource] > 0);
2148
2167
  const decisions = Object.fromEntries(resources.map((resource) => [
@@ -2158,7 +2177,10 @@ function scheduleIntents({
2158
2177
  ]));
2159
2178
  const blocked = resources.find((resource) => decisions[resource].mode !== "open");
2160
2179
  if (blocked) {
2161
- denied.push({ intentId: intent.id, resource: blocked, ...decisions[blocked] });
2180
+ // Only the ordinary pass records a denial. The owed-turn pass looks at
2181
+ // the same intents again immediately afterwards, and recording here
2182
+ // would report every one of them twice.
2183
+ if (pass.record) denied.push({ intentId: intent.id, resource: blocked, ...decisions[blocked] });
2162
2184
  continue;
2163
2185
  }
2164
2186
 
@@ -2173,6 +2195,7 @@ function scheduleIntents({
2173
2195
  );
2174
2196
  const expiring = resources.find((resource) => notBefore >= decisions[resource].resetMs);
2175
2197
  if (expiring) {
2198
+ if (!pass.record) continue;
2176
2199
  denied.push({
2177
2200
  intentId: intent.id,
2178
2201
  resource: expiring,
@@ -2207,6 +2230,12 @@ function scheduleIntents({
2207
2230
  reservation.sharingOwnerLeaseIds = sharingOwnerLeaseIds;
2208
2231
  }
2209
2232
  grants.push(reservation);
2233
+ grantedIds.add(intent.id);
2234
+ grantedThisPass += 1;
2235
+ // A manual grant extends the run; anything else ends it. The counter has
2236
+ // to persist, because the run this bounds is one manual intent per
2237
+ // planning pass rather than several within one.
2238
+ streak = priority === REQUEST_PRIORITIES.manual ? streak + 1 : 0;
2210
2239
  for (const resource of resources) {
2211
2240
  chargedTotals[resource] += intent.costs[resource];
2212
2241
  updatedLanes[resource] = {
@@ -2225,6 +2254,7 @@ function scheduleIntents({
2225
2254
  cursors: updatedCursors,
2226
2255
  denied,
2227
2256
  prunedIntentIds,
2257
+ manualStreak: streak,
2228
2258
  };
2229
2259
  }
2230
2260
 
@@ -2373,10 +2403,16 @@ function normalizeIdentityRegistry(raw) {
2373
2403
  if (!isRecord(transport)) return null;
2374
2404
  const keys = ["lastStartedAt", "cooldownUntil", "permit"];
2375
2405
  if (Object.hasOwn(transport, "waiters")) keys.push("waiters");
2406
+ if (Object.hasOwn(transport, "throttle")) keys.push("throttle");
2376
2407
  if (!normalizeHost(host) || !exactKeys(transport, keys) ||
2377
2408
  !time(transport.lastStartedAt) || !time(transport.cooldownUntil)) return null;
2378
2409
  if (transport.permit !== null && (!exactKeys(transport.permit, ["pid", "nonce", "startedAt"]) ||
2379
2410
  !Number.isSafeInteger(transport.permit.pid) || transport.permit.pid < 1 || !validGovernorId(transport.permit.nonce) || !time(transport.permit.startedAt))) return null;
2411
+ if (!Object.hasOwn(transport, "throttle")) transport.throttle = emptyTransportThrottle();
2412
+ if (!isRecord(transport.throttle) || !exactKeys(transport.throttle, ["attempts", "lastAt", "paused"]) ||
2413
+ !Number.isSafeInteger(transport.throttle.attempts) || transport.throttle.attempts < 0 ||
2414
+ transport.throttle.attempts > THROTTLE_PAUSE_AFTER ||
2415
+ !time(transport.throttle.lastAt) || typeof transport.throttle.paused !== "boolean") return null;
2380
2416
  if (!Object.hasOwn(transport, "waiters")) transport.waiters = [];
2381
2417
  if (!Array.isArray(transport.waiters) || transport.waiters.length > HTTP_MAX_WAITERS ||
2382
2418
  new Set(transport.waiters.map((waiter) => waiter?.nonce)).size !== transport.waiters.length ||
@@ -2474,7 +2510,7 @@ function inspectLegacyMigration(root, state, now) {
2474
2510
  // binary fail closed on its own gate, but this side must still recognise
2475
2511
  // that binary's file well enough to see it holds a live lease.
2476
2512
  const legacy = LEGACY_GOVERNOR_VERSIONS
2477
- .map((version) => normalizeGovernorState(raw, now, { prune: false, acceptVersion: version }))
2513
+ .map((version) => readLegacyGovernorState(raw, now, version))
2478
2514
  .find(Boolean) ?? migrateGovernorState(raw, now);
2479
2515
  if (legacy && raw.version === 1 && raw.budgets?.core) {
2480
2516
  const core = raw.budgets.core;
@@ -2840,7 +2876,13 @@ async function acquireIdentityHttpPermit(coordinator, { signal, now = Date.now }
2840
2876
  const migration = inspectLegacyMigration(coordinator.root, state, now());
2841
2877
  if (!migration.ok) return migration;
2842
2878
  if (now() >= deadline) return { ok: false, reason: "transport-busy", retryAt: deadline };
2843
- const transport = state.hosts[identity.host] ??= { lastStartedAt: 0, cooldownUntil: 0, permit: null, waiters: [] };
2879
+ const transport = state.hosts[identity.host] ??= { lastStartedAt: 0, cooldownUntil: 0, permit: null, waiters: [], throttle: emptyTransportThrottle() };
2880
+ // Five consecutive throttles means the ladder has stopped being useful.
2881
+ // Waiting out the deadline is what produced the last one, so the pause
2882
+ // holds until a person retries or a primary reset opens a recovery.
2883
+ if (transport.throttle?.paused) {
2884
+ return { ok: false, reason: "throttle-paused", retryAt: transport.cooldownUntil };
2885
+ }
2844
2886
  if (transport.permit && staleHttpPermit(transport.permit, now())) transport.permit = null;
2845
2887
  // Admission can happen during the mandatory start gap. Persist call
2846
2888
  // order once so later polling cannot overtake an earlier caller.
@@ -2886,17 +2928,92 @@ async function acquireIdentityHttpPermit(coordinator, { signal, now = Date.now }
2886
2928
  // stops applying to the other.
2887
2929
  const SECONDARY_LIMIT_PATTERN = /secondary rate|abuse/i;
2888
2930
 
2889
- function transportCooldownDeadline({ retryAfter, status, secondary, at }) {
2890
- const deadline = typeof retryAfter === "string" && /^\d+(?:\.\d+)?$/.test(retryAfter)
2931
+ // The locally chosen delay climbs by consecutive throttle and stops climbing at
2932
+ // fifteen minutes. The cap bounds only what this client invents for itself: a
2933
+ // server that asks for two hours is given two hours, because it knows something
2934
+ // we do not.
2935
+ const THROTTLE_LADDER_MS = [60_000, 120_000, 240_000, 480_000, 900_000];
2936
+ // After this many consecutive throttles the transport stops choosing new delays
2937
+ // and waits for a person or a primary reset. Climbing forever keeps a wedged
2938
+ // credential politely hammering a limit it has no way to satisfy.
2939
+ const THROTTLE_PAUSE_AFTER = 5;
2940
+
2941
+ function throttleLadderMs(priorThrottles) {
2942
+ const step = Number.isSafeInteger(priorThrottles) && priorThrottles > 0 ? priorThrottles : 0;
2943
+ return THROTTLE_LADDER_MS[Math.min(step, THROTTLE_LADDER_MS.length - 1)];
2944
+ }
2945
+
2946
+ // Reads throttle evidence without inventing any. The case this exists to get
2947
+ // right is the permission-only 403: it carries the same status as an exhausted
2948
+ // primary limit and is not a throttle at all, so treating it as one pauses a
2949
+ // pane that was never limited -- it was just not allowed.
2950
+ function classifyThrottle({ status = null, headers = null, graphqlErrors = null, stderr = "", body = null } = {}) {
2951
+ const header = (name) => {
2952
+ const value = headers?.[name];
2953
+ return typeof value === "string" ? value.trim() : null;
2954
+ };
2955
+ const integer = (name) => {
2956
+ const value = Number(header(name));
2957
+ return Number.isSafeInteger(value) ? value : null;
2958
+ };
2959
+ const secondary = SECONDARY_LIMIT_PATTERN.test([
2960
+ String(body?.message ?? ""),
2961
+ String(stderr ?? ""),
2962
+ ...(Array.isArray(graphqlErrors)
2963
+ ? graphqlErrors.map((error) => `${error?.type ?? ""} ${error?.message ?? ""}`)
2964
+ : []),
2965
+ ].join("\n"));
2966
+ const retryAfter = header("retry-after");
2967
+ const remaining = integer("x-ratelimit-remaining");
2968
+ const reset = integer("x-ratelimit-reset");
2969
+ const resource = header("x-ratelimit-resource")?.toLowerCase() ?? null;
2970
+ // The counter itself says there is nothing left. That holds its own resource
2971
+ // until its own reset, and says nothing about the other one.
2972
+ if (!secondary && remaining === 0 && reset !== null && RATE_RESOURCES.includes(resource) &&
2973
+ (status === 403 || status === 429)) {
2974
+ return { kind: "primary", resource, resetMs: reset * 1000 };
2975
+ }
2976
+ if (retryAfter !== null || secondary || status === 429) {
2977
+ return { kind: "secondary", retryAfter, secondary, status };
2978
+ }
2979
+ return { kind: "none" };
2980
+ }
2981
+
2982
+ function transportCooldownDeadline({ retryAfter, status, secondary, at, attempts = 0 }) {
2983
+ const supplied = typeof retryAfter === "string" && /^\d+(?:\.\d+)?$/.test(retryAfter)
2891
2984
  ? at + Number(retryAfter) * 1000
2892
2985
  : Date.parse(retryAfter ?? "");
2893
- if (Number.isFinite(deadline)) return deadline;
2894
- return status === 429 || secondary === true ? at + 60_000 : null;
2986
+ // A deadline the server supplied is honoured exactly, however long. Only the
2987
+ // delay chosen here is laddered, and only it is capped.
2988
+ if (Number.isFinite(supplied)) return supplied;
2989
+ return status === 429 || secondary === true ? at + throttleLadderMs(attempts) : null;
2990
+ }
2991
+
2992
+ function emptyTransportThrottle() {
2993
+ return { attempts: 0, lastAt: 0, paused: false };
2895
2994
  }
2896
2995
 
2897
2996
  function applyTransportCooldown(transport, evidence) {
2898
- const deadline = transportCooldownDeadline(evidence);
2899
- if (deadline !== null) transport.cooldownUntil = Math.max(transport.cooldownUntil, deadline);
2997
+ const throttle = transport.throttle ?? emptyTransportThrottle();
2998
+ const deadline = transportCooldownDeadline({ ...evidence, attempts: throttle.attempts });
2999
+ if (deadline === null) return null;
3000
+ // Merged by maximum, so a shorter concurrent error never shortens a hold
3001
+ // another response already established, and the hold survives a primary epoch
3002
+ // change: secondary limits are not accounted in the primary counter.
3003
+ transport.cooldownUntil = Math.max(transport.cooldownUntil, deadline);
3004
+ const attempts = Math.min(throttle.attempts + 1, THROTTLE_PAUSE_AFTER);
3005
+ transport.throttle = { attempts, lastAt: evidence.at, paused: attempts >= THROTTLE_PAUSE_AFTER };
3006
+ return transport.cooldownUntil;
3007
+ }
3008
+
3009
+ // A request that came back carrying no throttle evidence is the only thing that
3010
+ // proves the hold is over, so it is the only thing that resets the ladder.
3011
+ // Merely waiting out a deadline does not: that is what produced the next
3012
+ // throttle last time.
3013
+ function clearTransportThrottle(transport) {
3014
+ if (transport && transport.throttle && transport.throttle.attempts > 0) {
3015
+ transport.throttle = emptyTransportThrottle();
3016
+ }
2900
3017
  }
2901
3018
 
2902
3019
  function releaseIdentityHttpPermit(coordinator, permit, error = null) {
@@ -2905,22 +3022,48 @@ function releaseIdentityHttpPermit(coordinator, permit, error = null) {
2905
3022
  if (transport?.permit?.nonce === permit.nonce) transport.permit = null;
2906
3023
  if (transport && error) {
2907
3024
  const response = parseGhApiResponse(error.stdout ?? "");
2908
- applyTransportCooldown(transport, {
2909
- retryAfter: response.headers["retry-after"],
3025
+ const verdict = classifyThrottle({
2910
3026
  status: response.status,
2911
- secondary: SECONDARY_LIMIT_PATTERN.test(String(error.stderr ?? "")),
2912
- at: Date.now(),
3027
+ headers: response.headers,
3028
+ stderr: error.stderr,
2913
3029
  });
3030
+ // A permission-only 403 reaches here too. It is a failure, but it is not a
3031
+ // throttle, and holding the shared transport for it would pause every
3032
+ // pane over one repository the user cannot read.
3033
+ if (verdict.kind === "secondary") {
3034
+ applyTransportCooldown(transport, {
3035
+ retryAfter: verdict.retryAfter,
3036
+ status: verdict.status,
3037
+ secondary: verdict.secondary,
3038
+ at: Date.now(),
3039
+ });
3040
+ }
3041
+ } else if (transport && !error) {
3042
+ clearTransportThrottle(transport);
2914
3043
  }
2915
3044
  return { ok: true };
2916
3045
  });
2917
3046
  }
2918
3047
 
3048
+ // The explicit retry a paused transport waits for. Clears the ladder and the
3049
+ // deadline together: a person choosing to retry is choosing to spend the next
3050
+ // request finding out, which is exactly the recovery election the pause defers.
3051
+ function retryThrottledTransport(root, host, { now = Date.now } = {}) {
3052
+ return withIdentityRegistry(root, (state) => {
3053
+ const transport = state.hosts[host];
3054
+ if (!transport?.throttle?.paused) return { ok: true, value: { resumed: false } };
3055
+ transport.throttle = emptyTransportThrottle();
3056
+ transport.cooldownUntil = Math.min(transport.cooldownUntil, now());
3057
+ return { ok: true, value: { resumed: true } };
3058
+ }, { now: now() });
3059
+ }
3060
+
2919
3061
  function identityCoordinationMessage(reason) {
2920
3062
  if (reason === "restart-required") return "Restart required: close older gh-glance panes";
2921
3063
  if (["migration-hold", "legacy-unresolved"].includes(reason)) return "Upgrade waiting for legacy quota reset";
2922
3064
  if (["legacy-corrupt", "corrupt"].includes(reason)) return "Coordination state unavailable; evidence preserved";
2923
3065
  if (reason === "transport-busy") return "Shared HTTP request or cooldown in progress";
3066
+ if (reason === "throttle-paused") return "Paused after repeated rate limits; refresh to retry";
2924
3067
  if (retryableCoordination(reason)) return "Coordinating with your other panes";
2925
3068
  if (reason === "identity-capacity") return "Coordination state full; retrying after the current window";
2926
3069
  return "Verified identity unavailable; waiting to retry";
@@ -2937,9 +3080,27 @@ const GOVERNOR_SCOPE_VERSION = 1;
2937
3080
  // turns one rejected budget into total loss, discarding every live pane's
2938
3081
  // leases, intents and reservations. The version gate makes it fail closed on the
2939
3082
  // file instead, which is the whole point of having one.
2940
- const GOVERNOR_STATE_VERSION = 3;
2941
- // Readable-as-evidence, never written: 2 is the shape a 0.11.x pane still holds.
2942
- const LEGACY_GOVERNOR_VERSIONS = [GOVERNOR_STATE_VERSION, 2];
3083
+ // 4: observer claims, readiness and outcomes are per resource, so the schema
3084
+ // shape changed rather than a field's contents. Replaced atomically -- an older
3085
+ // build must fail closed on the version gate rather than half-read it.
3086
+ // 5: scheduling fairness is persisted, because the starvation it prevents
3087
+ // happens *between* planning passes -- each manual refresh is its own pass, so
3088
+ // a counter that lives only inside one cannot see a run of them.
3089
+ const GOVERNOR_STATE_VERSION = 5;
3090
+ // Readable-as-evidence, never written: the shapes a still-running older pane
3091
+ // holds. Recognising that such a pane owns a live lease is what the restart
3092
+ // boundary depends on.
3093
+ const LEGACY_GOVERNOR_VERSIONS = [GOVERNOR_STATE_VERSION, 4, 3, 2];
3094
+ // The first version whose file is already in the per-resource observer shape.
3095
+ // Anything below it is adapted before it can be read; see readLegacyGovernorState.
3096
+ const GOVERNOR_OBSERVER_SPLIT_VERSION = 4;
3097
+ // The first version carrying persisted scheduling fairness. A version 4 file is
3098
+ // otherwise in the current shape and only wants the field defaulted.
3099
+ const GOVERNOR_FAIRNESS_VERSION = 5;
3100
+ // Manual work outranks active work, which is right until it means "always". A
3101
+ // user holding the refresh key issues one manual intent per planning pass, and
3102
+ // without a bound the active owner behind them never gets a turn at all.
3103
+ const MANUAL_GRANT_STREAK_LIMIT = 3;
2943
3104
  const GOVERNOR_MAX_LEASES = 128;
2944
3105
  const GOVERNOR_MAX_INTENTS = 512;
2945
3106
  const GOVERNOR_MAX_RESERVATIONS = 512;
@@ -3052,11 +3213,16 @@ function emptyGovernorState() {
3052
3213
  version: GOVERNOR_STATE_VERSION,
3053
3214
  epochs: { core: null, graphql: null },
3054
3215
  budgets: {},
3055
- observers: {
3056
- core: { etag: null, outcome: "idle", at: 0, nextAt: 0 },
3057
- },
3058
- probeClaim: null,
3059
- probeOutcome: { status: "idle", at: 0, nextAt: 0 },
3216
+ fairness: { manualStreak: 0 },
3217
+ // Symmetric on purpose. Core observer state lived in `observers.core` while
3218
+ // the GraphQL one lived in a differently-shaped `probeOutcome`, and a single
3219
+ // `probeClaim` serialized both -- so a slow or failed observer for one
3220
+ // resource held up the other's readiness for no reason the resources
3221
+ // themselves impose.
3222
+ observers: Object.fromEntries(RATE_RESOURCES.map((resource) => [
3223
+ resource, { etag: null, outcome: "idle", at: 0, nextAt: 0 },
3224
+ ])),
3225
+ probeClaims: Object.fromEntries(RATE_RESOURCES.map((resource) => [resource, null])),
3060
3226
  leases: {},
3061
3227
  intents: {},
3062
3228
  reservations: {},
@@ -3180,7 +3346,7 @@ function normalizeGovernorReservation(raw, nowMs) {
3180
3346
  return { ...raw, costs, actualCosts, accountedCosts, epochs };
3181
3347
  }
3182
3348
 
3183
- function normalizeCoreObserver(raw, nowMs) {
3349
+ function normalizeResourceObserver(raw, nowMs) {
3184
3350
  if (!exactKeys(raw, ["etag", "outcome", "at", "nextAt"])) return null;
3185
3351
  if (
3186
3352
  raw.etag !== null && (typeof raw.etag !== "string" || raw.etag.length === 0) ||
@@ -3194,24 +3360,19 @@ function normalizeCoreObserver(raw, nowMs) {
3194
3360
  function normalizeProbeClaim(raw, nowMs) {
3195
3361
  if (raw === null) return null;
3196
3362
  if (!exactKeys(raw, [
3197
- "ownerLeaseId", "nonce", "leaseUntil", "nextAt", "claimAt", "startedReservationIds", "resources",
3363
+ "ownerLeaseId", "nonce", "leaseUntil", "nextAt", "claimAt", "startedReservationIds",
3198
3364
  ])) {
3199
3365
  return undefined;
3200
3366
  }
3201
- const resources = Array.isArray(raw.resources) && raw.resources.length > 0 &&
3202
- raw.resources.every((resource) => RATE_RESOURCES.includes(resource))
3203
- ? [...new Set(raw.resources)]
3204
- : null;
3205
3367
  if (
3206
3368
  !validGovernorId(raw.ownerLeaseId) || !validGovernorId(raw.nonce) ||
3207
3369
  finiteTimestamp(raw.leaseUntil, nowMs) === undefined ||
3208
3370
  finiteTimestamp(raw.nextAt, nowMs) === undefined ||
3209
3371
  finiteTimestamp(raw.claimAt, nowMs) === undefined || raw.claimAt > nowMs ||
3210
3372
  !Array.isArray(raw.startedReservationIds) || raw.startedReservationIds.length > GOVERNOR_MAX_RESERVATIONS ||
3211
- raw.startedReservationIds.some((id) => !id.startsWith("reservation:") || !validGovernorId(id.slice(12))) ||
3212
- !resources
3373
+ raw.startedReservationIds.some((id) => !id.startsWith("reservation:") || !validGovernorId(id.slice(12)))
3213
3374
  ) return undefined;
3214
- return { ...raw, startedReservationIds: [...new Set(raw.startedReservationIds)], resources };
3375
+ return { ...raw, startedReservationIds: [...new Set(raw.startedReservationIds)] };
3215
3376
  }
3216
3377
 
3217
3378
  function normalizeManualProbe(raw, nowMs) {
@@ -3232,27 +3393,28 @@ function normalizeManualProbe(raw, nowMs) {
3232
3393
  // version this build does not write is unreadable and fails closed.
3233
3394
  function normalizeGovernorState(raw, nowMs, { prune = true, acceptVersion = GOVERNOR_STATE_VERSION } = {}) {
3234
3395
  if (!exactKeys(raw, [
3235
- "version", "epochs", "budgets", "observers", "probeClaim", "probeOutcome", "leases",
3396
+ "version", "epochs", "budgets", "fairness", "observers", "probeClaims", "leases",
3236
3397
  "intents", "reservations", "manualProbe",
3237
3398
  ]) || raw.version !== acceptVersion) return null;
3399
+ if (!exactKeys(raw.fairness, ["manualStreak"]) ||
3400
+ !Number.isSafeInteger(raw.fairness.manualStreak) || raw.fairness.manualStreak < 0) return null;
3238
3401
  if (
3239
3402
  !exactKeys(raw.epochs, RATE_RESOURCES) ||
3240
3403
  RATE_RESOURCES.some((resource) => raw.epochs[resource] !== null && !validGovernorEpoch(raw.epochs[resource])) ||
3241
3404
  !isRecord(raw.budgets) || Object.keys(raw.budgets).some((resource) => !RATE_RESOURCES.includes(resource)) ||
3242
- !exactKeys(raw.observers, ["core"]) ||
3243
- !isRecord(raw.leases) || !isRecord(raw.intents) || !isRecord(raw.reservations) ||
3244
- !exactKeys(raw.probeOutcome, ["status", "at", "nextAt"]) ||
3245
- !GOVERNOR_PROBE_STATUSES.has(raw.probeOutcome.status) ||
3246
- finiteTimestamp(raw.probeOutcome.at, nowMs) === undefined ||
3247
- raw.probeOutcome.at > nowMs ||
3248
- finiteTimestamp(raw.probeOutcome.nextAt, nowMs) === undefined
3405
+ !exactKeys(raw.observers, RATE_RESOURCES) ||
3406
+ !exactKeys(raw.probeClaims, RATE_RESOURCES) ||
3407
+ !isRecord(raw.leases) || !isRecord(raw.intents) || !isRecord(raw.reservations)
3249
3408
  ) return null;
3250
3409
 
3251
3410
  const state = emptyGovernorState();
3252
3411
  state.epochs = { ...raw.epochs };
3253
- state.probeOutcome = { ...raw.probeOutcome };
3254
- state.observers.core = normalizeCoreObserver(raw.observers.core, nowMs);
3255
- if (!state.observers.core) return null;
3412
+ state.fairness = { ...raw.fairness };
3413
+ for (const resource of RATE_RESOURCES) {
3414
+ const observer = normalizeResourceObserver(raw.observers[resource], nowMs);
3415
+ if (!observer) return null;
3416
+ state.observers[resource] = observer;
3417
+ }
3256
3418
  for (const resource of Object.keys(raw.budgets)) {
3257
3419
  const budget = normalizeGovernorBudget(raw.budgets[resource], nowMs, resource);
3258
3420
  if (!budget) return null;
@@ -3282,21 +3444,104 @@ function normalizeGovernorState(raw, nowMs, { prune = true, acceptVersion = GOVE
3282
3444
  ) state.reservations[id] = reservation;
3283
3445
  }
3284
3446
  if (Object.keys(state.reservations).length > GOVERNOR_MAX_RESERVATIONS) return null;
3285
- state.probeClaim = normalizeProbeClaim(raw.probeClaim, nowMs);
3286
- if (state.probeClaim === undefined) return null;
3287
- if (state.probeClaim?.leaseUntil <= nowMs) state.probeClaim = null;
3447
+ for (const resource of RATE_RESOURCES) {
3448
+ const claim = normalizeProbeClaim(raw.probeClaims[resource], nowMs);
3449
+ if (claim === undefined) return null;
3450
+ state.probeClaims[resource] = claim?.leaseUntil > nowMs ? claim : null;
3451
+ }
3288
3452
  state.manualProbe = normalizeManualProbe(raw.manualProbe, nowMs);
3289
3453
  if (state.manualProbe === undefined) return null;
3290
3454
  return state;
3291
3455
  }
3292
3456
 
3457
+ // Reads a still-running older pane's file as evidence. A version number alone
3458
+ // is not enough to identify a document: versions 2 and 3 share a *shape* that
3459
+ // version 4 replaced, so accepting their version against the current key set
3460
+ // would reject every real file they wrote. Read-only -- the result is never
3461
+ // published and the file is never rewritten.
3462
+ function readLegacyGovernorState(raw, nowMs, version) {
3463
+ if (!isRecord(raw) || raw.version !== version) return null;
3464
+ const options = { prune: false, acceptVersion: version };
3465
+ if (version >= GOVERNOR_FAIRNESS_VERSION) return normalizeGovernorState(raw, nowMs, options);
3466
+ // A version 4 file is already in the per-resource observer shape and differs
3467
+ // only by the field this version added, so defaulting it is the whole
3468
+ // adaptation. Older files need their observer shape rewritten first.
3469
+ const shaped = version >= GOVERNOR_OBSERVER_SPLIT_VERSION ? { ...raw } : adaptPreSplitGovernorShape(raw);
3470
+ if (!shaped) return null;
3471
+ shaped.fairness = { manualStreak: 0 };
3472
+ return normalizeGovernorState(shaped, nowMs, options);
3473
+ }
3474
+
3475
+ // Versions 2 and 3 held one observer for core, a differently shaped
3476
+ // `probeOutcome` standing in for GraphQL, and a single claim naming the
3477
+ // resources it covered. Rewrite that into the current shape so the inspector
3478
+ // above can see the leases, deadlines and uncertain work such a pane holds.
3479
+ // The claim's own `resources` list says which resources it covered, so
3480
+ // splitting it in two reads the file rather than guessing at it.
3481
+ function adaptPreSplitGovernorShape(raw) {
3482
+ if (!exactKeys(raw, [
3483
+ "version", "epochs", "budgets", "observers", "probeClaim", "probeOutcome",
3484
+ "leases", "intents", "reservations", "manualProbe",
3485
+ ])) return null;
3486
+ if (!exactKeys(raw.observers, ["core"])) return null;
3487
+ if (!exactKeys(raw.probeOutcome, ["status", "at", "nextAt"])) return null;
3488
+ if (raw.probeClaim !== null && !isRecord(raw.probeClaim)) return null;
3489
+ const { resources, ...claim } = raw.probeClaim ?? {};
3490
+ const covered = Array.isArray(resources) ? resources : [];
3491
+ const adapted = structuredClone(raw);
3492
+ delete adapted.probeClaim;
3493
+ delete adapted.probeOutcome;
3494
+ adapted.observers = {
3495
+ core: raw.observers.core,
3496
+ graphql: {
3497
+ etag: null,
3498
+ outcome: raw.probeOutcome.status,
3499
+ at: raw.probeOutcome.at,
3500
+ nextAt: raw.probeOutcome.nextAt,
3501
+ },
3502
+ };
3503
+ adapted.probeClaims = Object.fromEntries(RATE_RESOURCES.map((resource) => [
3504
+ resource, raw.probeClaim && covered.includes(resource) ? structuredClone(claim) : null,
3505
+ ]));
3506
+ return adapted;
3507
+ }
3508
+
3293
3509
  function serializeGovernorState(state) {
3294
3510
  return `${JSON.stringify(state)}\n`;
3295
3511
  }
3296
3512
 
3513
+ // Failing closed on a version this build does not write is right in one
3514
+ // direction only. An *older* binary meeting a newer file must refuse it, because
3515
+ // it cannot know what the new fields mean. A *newer* binary meeting an older
3516
+ // file at its own canonical path has to migrate it: the read happens before any
3517
+ // write, so refusing leaves the file in place and the pane never recovers. It is
3518
+ // not a transient failure, it is a permanent one, and every later launch repeats
3519
+ // it.
3520
+ //
3521
+ // That asymmetry was missed when the observer split moved the protocol from 3 to
3522
+ // 5. Versions 1 and 2 escaped it only because the file moved path at the same
3523
+ // time, so a new build simply found nothing and started clean; 3 onwards share a
3524
+ // path with 5 and collided.
3297
3525
  function migrateGovernorState(raw, nowMs) {
3526
+ if (!isRecord(raw) || !Number.isSafeInteger(raw.version) || raw.version < 1) return null;
3527
+ if (raw.version >= GOVERNOR_STATE_VERSION) return null;
3528
+ if (raw.version === 1) return migrateV1GovernorState(raw, nowMs);
3529
+ // Everything from 2 onwards is this build's shape or one adaptation away from
3530
+ // it, and what it holds is worth keeping: the uncertain reservations are debts
3531
+ // that must not be forgiven by an upgrade, and the budgets were written by the
3532
+ // same observers this build trusts.
3533
+ const shaped = raw.version >= GOVERNOR_OBSERVER_SPLIT_VERSION
3534
+ ? structuredClone(raw)
3535
+ : adaptPreSplitGovernorShape(raw);
3536
+ if (!shaped) return null;
3537
+ if (!isRecord(shaped.fairness)) shaped.fairness = { manualStreak: 0 };
3538
+ shaped.version = GOVERNOR_STATE_VERSION;
3539
+ return normalizeGovernorState(shaped, nowMs);
3540
+ }
3541
+
3542
+ function migrateV1GovernorState(raw, nowMs) {
3298
3543
  if (!exactKeys(raw, [
3299
- "version", "epochs", "budgets", "probeClaim", "probeOutcome", "leases",
3544
+ "version", "epochs", "budgets", "probeClaim", "probeOutcome", "leases", // v1 shape
3300
3545
  "intents", "reservations", "manualProbe",
3301
3546
  ]) || raw.version !== 1 || !isRecord(raw.budgets) || !isRecord(raw.reservations)) return null;
3302
3547
  const migrated = structuredClone(raw);
@@ -3318,7 +3563,16 @@ function migrateGovernorState(raw, nowMs) {
3318
3563
  for (const reservation of Object.values(migrated.reservations)) {
3319
3564
  reservation.accountedCosts = { core: 0, graphql: 0 };
3320
3565
  }
3321
- if (migrated.probeClaim) migrated.probeClaim.resources = [...RATE_RESOURCES];
3566
+ // v1 carried one claim for both resources; the new shape has one per resource
3567
+ // and a migrated claim belongs to neither, so it is dropped rather than
3568
+ // guessed at. The next probe re-claims what is actually due.
3569
+ delete migrated.probeClaim;
3570
+ delete migrated.probeOutcome;
3571
+ migrated.observers = Object.fromEntries(RATE_RESOURCES.map((resource) => [
3572
+ resource, { etag: null, outcome: "idle", at: 0, nextAt: 0 },
3573
+ ]));
3574
+ migrated.probeClaims = Object.fromEntries(RATE_RESOURCES.map((resource) => [resource, null]));
3575
+ migrated.fairness = { manualStreak: 0 };
3322
3576
  return normalizeGovernorState(migrated, nowMs);
3323
3577
  }
3324
3578
 
@@ -3601,6 +3855,33 @@ function mutateGovernor(scope, nowMs, mutate) {
3601
3855
  });
3602
3856
  }
3603
3857
 
3858
+ // The largest single operation the governor will ever admit. Returned pacing is
3859
+ // capped at one of these so an idle stretch cannot accumulate into a burst: at
3860
+ // most one operation's worth of the lane is ever given back at a time.
3861
+ const GOVERNOR_MAX_ATOMIC_COST = Math.max(...Object.values(OPERATION_COSTS)
3862
+ .flatMap((costs) => RATE_RESOURCES.map((resource) => costs?.[resource] ?? 0)));
3863
+
3864
+ // A grant paces the lane by what it *reserved*, which is a worst case. When the
3865
+ // request costs less than that -- a 304 costs nothing -- or never happens at
3866
+ // all, the difference is capacity paced away for nobody, and the next request
3867
+ // waits out a slot no one used.
3868
+ //
3869
+ // The lane is never pulled earlier than the transport gap, which is what keeps
3870
+ // this from becoming a burst. Recomputing the rate here can differ from the
3871
+ // rate at grant time if capacity has since changed, so the returned credit is
3872
+ // an estimate rather than an exact reversal; that is safe because admission
3873
+ // still re-checks affordability against the reserve before anything starts.
3874
+ // Pacing decides when work may go, never whether it may.
3875
+ function returnPacingCredit(state, resource, unusedCost, nowMs) {
3876
+ const budget = state.budgets[resource];
3877
+ if (!budget || !(unusedCost > 0)) return;
3878
+ const decision = resourceDecision({ budget, resource, nowMs, cost: 0, chargedCost: 0 });
3879
+ const callsPerMs = decision?.callsPerMs;
3880
+ if (!Number.isFinite(callsPerMs) || callsPerMs <= 0) return;
3881
+ const credit = Math.min(unusedCost, GOVERNOR_MAX_ATOMIC_COST) / callsPerMs;
3882
+ budget.laneNextAt = Math.max(nowMs + HTTP_START_GAP_MS, budget.laneNextAt - credit);
3883
+ }
3884
+
3604
3885
  function scheduleGovernorState(state, nowMs) {
3605
3886
  if (Object.keys(state.intents).length === 0) return { grants: [], denied: [] };
3606
3887
  let reservationCount = Object.keys(state.reservations).length;
@@ -3612,7 +3893,7 @@ function scheduleGovernorState(state, nowMs) {
3612
3893
  ? [[resource, state.budgets[resource].roundRobinCursor]]
3613
3894
  : [],
3614
3895
  ));
3615
- const deferredBackground = state.probeOutcome.status === "failed"
3896
+ const deferredBackground = state.observers.graphql.outcome === "failed"
3616
3897
  ? Object.entries(state.intents)
3617
3898
  .filter(([, intent]) => intentPriority(intent) === REQUEST_PRIORITIES.background)
3618
3899
  .map(([id]) => id)
@@ -3633,7 +3914,9 @@ function scheduleGovernorState(state, nowMs) {
3633
3914
  cursors,
3634
3915
  nowMs,
3635
3916
  maxGrants: GOVERNOR_MAX_RESERVATIONS - reservationCount,
3917
+ manualStreak: state.fairness.manualStreak,
3636
3918
  });
3919
+ state.fairness.manualStreak = result.manualStreak;
3637
3920
  result.denied.push(...deferredBackground.map((intentId) => ({
3638
3921
  intentId,
3639
3922
  mode: "paused",
@@ -3720,76 +4003,79 @@ function maintainControlLease(scope, leaseId, floorMs, activeTab, nowMs) {
3720
4003
  : registered;
3721
4004
  }
3722
4005
 
3723
- function claimProbe(scope, leaseId, nowMs) {
4006
+ // Claims the observer for one resource. Independence is the point: a slow or
4007
+ // failed GraphQL observer must not hold up core readiness, because nothing about
4008
+ // the resources themselves couples them. What still couples them is the shared
4009
+ // HTTP permit and any account-wide secondary hold, and those are enforced
4010
+ // elsewhere -- not by making one claim stand for both.
4011
+ function claimProbe(scope, leaseId, nowMs, resource) {
4012
+ if (!RATE_RESOURCES.includes(resource)) return { ok: false, reason: "corrupt" };
3724
4013
  return mutateGovernor(scope, nowMs, (state, at) => {
3725
4014
  if (!state.leases[leaseId]) return { ok: false, reason: "stale" };
3726
- if (state.probeClaim && state.probeClaim.leaseUntil > at) {
3727
- return { value: { status: "waiting", leaseUntil: state.probeClaim.leaseUntil } };
4015
+ const held = state.probeClaims[resource];
4016
+ if (held && held.leaseUntil > at) {
4017
+ return { value: { status: "waiting", leaseUntil: held.leaseUntil } };
3728
4018
  }
4019
+ const resetAt = Number.isFinite(state.budgets[resource]?.resetMs)
4020
+ ? state.budgets[resource].resetMs + BUDGET_RESET_GRACE_MS
4021
+ : Number.POSITIVE_INFINITY;
4022
+ // A core reset starts a new shared accounting epoch, so the GraphQL counter
4023
+ // is also due then: external-spend baselines and the protected
4024
+ // one-publication reset contract have to advance together.
3729
4025
  const coreResetAt = Number.isFinite(state.budgets.core?.resetMs)
3730
4026
  ? state.budgets.core.resetMs + BUDGET_RESET_GRACE_MS
3731
4027
  : Number.POSITIVE_INFINITY;
3732
- const graphqlResetAt = Number.isFinite(state.budgets.graphql?.resetMs)
3733
- ? state.budgets.graphql.resetMs + BUDGET_RESET_GRACE_MS
4028
+ // ...but only until it has actually observed since that reset. Left
4029
+ // unqualified, every pane keeps qualifying for as long as core has not
4030
+ // published its new epoch, so a reset draws a stampede of GraphQL observers
4031
+ // instead of the one the shared epoch needs. The publication used to close
4032
+ // that window as a side effect of being read first; saying it here means it
4033
+ // does not depend on which resource a refresh happens to reach first.
4034
+ const dueAtCoreReset = resource === "graphql" &&
4035
+ state.observers.graphql.at < coreResetAt
4036
+ ? coreResetAt
3734
4037
  : Number.POSITIVE_INFINITY;
3735
- const dueAt = {
3736
- core: Math.min(
3737
- state.observers.core.nextAt,
3738
- coreResetAt,
3739
- ),
3740
- graphql: Math.min(
3741
- state.probeOutcome.nextAt,
3742
- graphqlResetAt,
3743
- // A core reset starts a new shared accounting epoch. Refresh the free
3744
- // GraphQL counter in that same nonce so external-spend baselines and
3745
- // the protected one-publication reset contract advance together.
3746
- coreResetAt,
3747
- ),
3748
- };
3749
- const nextAt = Math.min(...Object.values(dueAt));
4038
+ const nextAt = Math.min(
4039
+ state.observers[resource].nextAt,
4040
+ resetAt,
4041
+ dueAtCoreReset,
4042
+ );
3750
4043
  if (nextAt > at) return { value: { status: "waiting", nextAt } };
3751
- // One nonce owns the control-plane transition, but only the sources whose
3752
- // persisted clocks are due may be read. In particular, a GraphQL minute
3753
- // sample cannot spend or retry the held core observer before its reset.
3754
- const resources = RATE_RESOURCES.filter((resource) => dueAt[resource] <= at);
3755
4044
  const nonce = randomUUID();
3756
4045
  const startedReservationIds = Object.entries(state.reservations)
3757
4046
  // An expired owner can no longer settle its request. Keep that uncertain
3758
4047
  // cost charged, but do not make every later probe wait for it to finish.
4048
+ // Only work charging this resource is drained -- an unrelated lane's
4049
+ // outstanding request says nothing about this counter.
3759
4050
  .filter(([, reservation]) => reservation.status === "started" &&
3760
- state.leases[reservation.leaseId]?.expiresAt > at)
4051
+ state.leases[reservation.leaseId]?.expiresAt > at &&
4052
+ reservationCost(reservation, resource, state.leases, at) > 0)
3761
4053
  .map(([id]) => id);
3762
- state.probeClaim = {
4054
+ state.probeClaims[resource] = {
3763
4055
  ownerLeaseId: leaseId,
3764
4056
  nonce,
3765
4057
  leaseUntil: at + GOVERNOR_PROBE_LEASE_MS,
3766
4058
  nextAt: at,
3767
4059
  claimAt: at,
3768
4060
  startedReservationIds,
3769
- resources,
3770
4061
  };
3771
- if (resources.includes("core")) {
3772
- state.observers.core.outcome = "waiting";
3773
- state.observers.core.at = at;
3774
- state.observers.core.nextAt = at + GOVERNOR_PROBE_LEASE_MS;
3775
- }
3776
- if (resources.includes("graphql")) {
3777
- state.probeOutcome = { status: "waiting", at, nextAt: at + GOVERNOR_PROBE_LEASE_MS };
3778
- }
4062
+ state.observers[resource].outcome = "waiting";
4063
+ state.observers[resource].at = at;
4064
+ state.observers[resource].nextAt = at + GOVERNOR_PROBE_LEASE_MS;
3779
4065
  return { value: {
3780
4066
  status: "claimed",
3781
4067
  nonce,
3782
- leaseUntil: state.probeClaim.leaseUntil,
4068
+ resource,
4069
+ leaseUntil: state.probeClaims[resource].leaseUntil,
3783
4070
  startedReservationIds,
3784
- resources,
3785
- coreEtag: state.observers.core.etag,
4071
+ coreEtag: state.observers[resource].etag,
3786
4072
  } };
3787
4073
  });
3788
4074
  }
3789
4075
 
3790
- function renewProbeClaim(scope, leaseId, nonce, nowMs) {
4076
+ function renewProbeClaim(scope, leaseId, nonce, nowMs, resource) {
3791
4077
  return mutateGovernor(scope, nowMs, (state, at) => {
3792
- const claim = state.probeClaim;
4078
+ const claim = state.probeClaims[resource];
3793
4079
  if (!claim || claim.ownerLeaseId !== leaseId || claim.nonce !== nonce || claim.leaseUntil <= at) {
3794
4080
  return { ok: false, reason: "stale" };
3795
4081
  }
@@ -3847,7 +4133,13 @@ function budgetFromObservation(raw, previous, nowMs, {
3847
4133
  blockReason: blockActive ? previous.blockReason : null,
3848
4134
  laneNextAt: epochChanged ? nowMs : previous.laneNextAt,
3849
4135
  roundRobinCursor: previous?.roundRobinCursor ?? null,
3850
- lastExternalFactor: previous?.lastExternalFactor ?? 1,
4136
+ // A new epoch is a new accounting window, so what other clients did in the
4137
+ // last one is not evidence about this one. The baseline and the local
4138
+ // accumulation below are already reset here; carrying the factor across
4139
+ // kept throttling this window on a ratio measured in a window that is over.
4140
+ // Only a claimed observer can open an epoch, so this cannot be moved by a
4141
+ // stale or reordered response.
4142
+ lastExternalFactor: epochChanged ? 1 : (previous?.lastExternalFactor ?? 1),
3851
4143
  epoch,
3852
4144
  source,
3853
4145
  factorBaseline: epochChanged || !previous?.factorBaseline
@@ -3858,23 +4150,23 @@ function budgetFromObservation(raw, previous, nowMs, {
3858
4150
  return { status: "accepted", budget: next, epochChanged };
3859
4151
  }
3860
4152
 
3861
- function publishProbe(scope, leaseId, nonce, budgets, nowMs) {
4153
+ function publishProbe(scope, leaseId, nonce, budgets, nowMs, resource) {
3862
4154
  return mutateGovernor(scope, nowMs, (state, at) => {
3863
- const claim = state.probeClaim;
4155
+ const claim = state.probeClaims[resource];
3864
4156
  if (!claim || claim.ownerLeaseId !== leaseId || claim.nonce !== nonce || claim.leaseUntil <= at) {
3865
4157
  return { ok: false, reason: "stale" };
3866
4158
  }
3867
4159
  const nextBudgets = { ...state.budgets };
3868
4160
  const nextEpochs = { ...state.epochs };
4161
+ // Captured before the publication overwrites it: the reset this epoch
4162
+ // change supersedes is the boundary the GraphQL observer is measured against.
4163
+ const supersededCoreResetMs = state.budgets.core?.resetMs;
3869
4164
  const changedResources = [];
3870
4165
  const resetResources = [];
3871
- if (!claim.resources.every((resource) => budgets?.[resource])) {
3872
- return { ok: false, reason: "corrupt" };
3873
- }
4166
+ if (!budgets?.[resource]) return { ok: false, reason: "corrupt" };
3874
4167
  const observerCosts = {};
3875
- for (const resource of claim.resources) {
3876
- const supplied = budgets?.[resource];
3877
- if (!supplied) continue;
4168
+ {
4169
+ const supplied = budgets[resource];
3878
4170
  const raw = supplied.budget ?? supplied;
3879
4171
  // The owner source names what actually established the number. Calling a
3880
4172
  // GraphQL budget a "rate-limit-probe" was accurate when /rate_limit supplied
@@ -3890,19 +4182,20 @@ function publishProbe(scope, leaseId, nonce, budgets, nowMs) {
3890
4182
  allowEpochChange: true,
3891
4183
  });
3892
4184
  if (observed.status === "invalid") return { ok: false, reason: "corrupt" };
3893
- if (observed.status === "ignored") continue;
3894
- // The observer's own cost is local spend. Left out, the external-factor
3895
- // reconciliation below attributes it to other clients, and the governor
3896
- // throttles the user's real work to make room for its own probing.
3897
- observerCosts[resource] = Number.isSafeInteger(supplied.cost) && supplied.cost > 0 ? supplied.cost : 0;
3898
- nextBudgets[resource] = observed.budget;
3899
- if (resource === "core" && supplied.blocked === true) {
3900
- nextBudgets[resource].blockUntil = observed.budget.resetMs;
3901
- nextBudgets[resource].blockReason = "rate-limit";
4185
+ if (observed.status !== "ignored") {
4186
+ // The observer's own cost is local spend. Left out, the external-factor
4187
+ // reconciliation below attributes it to other clients, and the governor
4188
+ // throttles the user's real work to make room for its own probing.
4189
+ observerCosts[resource] = Number.isSafeInteger(supplied.cost) && supplied.cost > 0 ? supplied.cost : 0;
4190
+ nextBudgets[resource] = observed.budget;
4191
+ if (resource === "core" && supplied.blocked === true) {
4192
+ nextBudgets[resource].blockUntil = observed.budget.resetMs;
4193
+ nextBudgets[resource].blockReason = "rate-limit";
4194
+ }
4195
+ nextEpochs[resource] = observed.budget.epoch;
4196
+ changedResources.push(resource);
4197
+ if (observed.epochChanged && state.epochs[resource] !== null) resetResources.push(resource);
3902
4198
  }
3903
- nextEpochs[resource] = observed.budget.epoch;
3904
- changedResources.push(resource);
3905
- if (observed.epochChanged && state.epochs[resource] !== null) resetResources.push(resource);
3906
4199
  }
3907
4200
  if (changedResources.length === 0) return { ok: false, reason: "corrupt" };
3908
4201
  const completedBeforeClaim = Object.entries(state.reservations)
@@ -3920,19 +4213,32 @@ function publishProbe(scope, leaseId, nonce, budgets, nowMs) {
3920
4213
  total + (reservation.completedAt > baseline.observedAt
3921
4214
  ? reservationCost(reservation, resource, state.leases, at)
3922
4215
  : 0), 0);
4216
+ const globalUsedDelta = nextBudgets[resource].used - baseline.used;
4217
+ const sharedCompletedDelta = previous.knownLocalUsed + legacyCompletedDelta + (observerCosts[resource] ?? 0);
3923
4218
  const factor = nextExternalFactor({
3924
4219
  lastExternalFactor: previous.lastExternalFactor,
3925
- globalUsedDelta: nextBudgets[resource].used - baseline.used,
3926
- sharedCompletedDelta: previous.knownLocalUsed + legacyCompletedDelta + (observerCosts[resource] ?? 0),
4220
+ globalUsedDelta,
4221
+ sharedCompletedDelta,
3927
4222
  });
3928
4223
  if (factor === null) return { ok: false, reason: "corrupt" };
3929
4224
  nextBudgets[resource].lastExternalFactor = factor;
3930
- nextBudgets[resource].factorBaseline = {
3931
- epoch: nextBudgets[resource].epoch,
3932
- used: nextBudgets[resource].used,
3933
- observedAt: nextBudgets[resource].observedAt,
3934
- };
3935
- nextBudgets[resource].knownLocalUsed = 0;
4225
+ // Closing the window costs the evidence in it, so only a window that
4226
+ // actually reconciled may close. Four local units observed twice are
4227
+ // eight, and eight are enough; discarding each four because neither
4228
+ // reached five on its own is what kept a factor of seven alive through
4229
+ // any amount of purely local spend.
4230
+ if (externalSampleIsUsable({ globalUsedDelta, sharedCompletedDelta })) {
4231
+ nextBudgets[resource].factorBaseline = {
4232
+ epoch: nextBudgets[resource].epoch,
4233
+ used: nextBudgets[resource].used,
4234
+ observedAt: nextBudgets[resource].observedAt,
4235
+ };
4236
+ nextBudgets[resource].knownLocalUsed = 0;
4237
+ }
4238
+ // Otherwise both are left exactly as the observation produced them, which
4239
+ // is the previous baseline and the previous local total. Rewriting them
4240
+ // here would count the same completed reservations twice: they are still
4241
+ // in the ledger precisely because the baseline did not advance past them.
3936
4242
  }
3937
4243
  for (const [id, reservation] of completedBeforeClaim) {
3938
4244
  const accountedByEveryResource = RATE_RESOURCES.every((resource) =>
@@ -3942,24 +4248,41 @@ function publishProbe(scope, leaseId, nonce, budgets, nowMs) {
3942
4248
  }
3943
4249
  state.budgets = nextBudgets;
3944
4250
  state.epochs = nextEpochs;
3945
- state.probeClaim = null;
3946
- if (claim.resources.includes("core")) {
3947
- const core = nextBudgets.core;
3948
- state.observers.core = {
3949
- etag: typeof budgets.core.etag === "string" ? budgets.core.etag : state.observers.core.etag,
4251
+ state.probeClaims[resource] = null;
4252
+ {
4253
+ const published = nextBudgets[resource];
4254
+ state.observers[resource] = {
4255
+ etag: typeof budgets[resource].etag === "string" ? budgets[resource].etag : state.observers[resource].etag,
3950
4256
  outcome: "healthy",
3951
4257
  at,
3952
- nextAt: core.remaining === 0
3953
- ? core.resetMs + BUDGET_RESET_GRACE_MS
4258
+ // A resource observed as empty is due again at its own reset, not at the
4259
+ // ordinary cadence: probing an exhausted counter to be told it is still
4260
+ // exhausted spends against it for nothing.
4261
+ nextAt: published.remaining === 0
4262
+ ? published.resetMs + BUDGET_RESET_GRACE_MS
3954
4263
  : at + BUDGET_PROBE_MS,
3955
4264
  };
3956
4265
  }
3957
- if (claim.resources.includes("graphql")) {
3958
- state.probeOutcome = {
3959
- status: "healthy",
3960
- at,
3961
- nextAt: at + BUDGET_PROBE_MS,
3962
- };
4266
+ // A core reset opens a new shared accounting epoch, so the GraphQL counter
4267
+ // is due with it: external-spend baselines and the protected
4268
+ // one-publication reset contract have to advance together. A refresh that
4269
+ // reads GraphQL before core has already done this, because core's old reset
4270
+ // time still said GraphQL was due when that claim was evaluated. What
4271
+ // cannot see it is a pane whose GraphQL observer last ran before the reset
4272
+ // -- another pane published this epoch first, moving the reset out of
4273
+ // reach. Only that case still owes an observation, so only that case is
4274
+ // pulled forward; marking it unconditionally spends a second point to
4275
+ // re-read a counter this same cycle has just read. An exhausted GraphQL
4276
+ // counter waits for its own reset either way.
4277
+ if (resource === "core" && resetResources.includes("core")) {
4278
+ const graphql = nextBudgets.graphql;
4279
+ const heldToReset = graphql && graphql.remaining === 0 &&
4280
+ at < graphql.resetMs + BUDGET_RESET_GRACE_MS;
4281
+ const observedSinceReset = Number.isFinite(supersededCoreResetMs) &&
4282
+ state.observers.graphql.at >= supersededCoreResetMs;
4283
+ if (!heldToReset && !observedSinceReset) {
4284
+ state.observers.graphql.nextAt = Math.min(state.observers.graphql.nextAt || at, at);
4285
+ }
3963
4286
  }
3964
4287
  if (resetResources.length > 0) state.manualProbe = null;
3965
4288
  else if (state.manualProbe &&
@@ -3976,24 +4299,22 @@ function publishProbe(scope, leaseId, nonce, budgets, nowMs) {
3976
4299
  });
3977
4300
  }
3978
4301
 
3979
- function failProbeClaim(scope, leaseId, nonce, nowMs) {
4302
+ function failProbeClaim(scope, leaseId, nonce, nowMs, resource) {
3980
4303
  return mutateGovernor(scope, nowMs, (state, at) => {
3981
- const claim = state.probeClaim;
4304
+ const claim = state.probeClaims[resource];
3982
4305
  if (!claim || claim.ownerLeaseId !== leaseId || claim.nonce !== nonce) {
3983
4306
  return { ok: false, reason: "stale" };
3984
4307
  }
3985
- state.probeClaim = null;
3986
- if (claim.resources.includes("graphql")) {
3987
- state.probeOutcome = { status: "failed", at, nextAt: at + BUDGET_PROBE_MS };
3988
- }
3989
- if (claim.resources.includes("core")) {
3990
- state.observers.core = {
3991
- ...state.observers.core,
3992
- outcome: "failed",
3993
- at,
3994
- nextAt: at + BUDGET_PROBE_MS,
3995
- };
3996
- }
4308
+ // Only this resource's readiness is affected. A failed GraphQL observer that
4309
+ // also marked core failed would erase authority core had legitimately
4310
+ // established, and stall a lane with nothing wrong with it.
4311
+ state.probeClaims[resource] = null;
4312
+ state.observers[resource] = {
4313
+ ...state.observers[resource],
4314
+ outcome: "failed",
4315
+ at,
4316
+ nextAt: at + BUDGET_PROBE_MS,
4317
+ };
3997
4318
  return { value: { retryAt: at + BUDGET_PROBE_MS } };
3998
4319
  });
3999
4320
  }
@@ -4015,7 +4336,7 @@ function requestManualProbe(scope, leaseId, epoch, observedAt, nowMs) {
4015
4336
  : Math.min(state.observers.core.nextAt || at, at);
4016
4337
  }
4017
4338
  if (state.epochs.graphql === epoch) {
4018
- state.probeOutcome.nextAt = Math.min(state.probeOutcome.nextAt || at, at);
4339
+ state.observers.graphql.nextAt = Math.min(state.observers.graphql.nextAt || at, at);
4019
4340
  }
4020
4341
  return { value: { status: "pending", ...state.manualProbe } };
4021
4342
  });
@@ -4118,6 +4439,10 @@ function cancelIntent(scope, intentId, nowMs) {
4118
4439
  return { ok: false, reason: "stale" };
4119
4440
  }
4120
4441
  delete state.reservations[reservationId];
4442
+ // The request never started, so the whole slot it was paced into is free.
4443
+ for (const resource of RATE_RESOURCES) {
4444
+ returnPacingCredit(state, resource, reservation.costs[resource], nowMs ?? Date.now());
4445
+ }
4121
4446
  return { value: { status: "cancelled", intentId, reservationId } };
4122
4447
  });
4123
4448
  }
@@ -4131,8 +4456,14 @@ function startReservation(scope, reservationId, nowMs) {
4131
4456
  return { ok: false, reason: "stale", write: Boolean(reservation) };
4132
4457
  }
4133
4458
  if (reservation.notBefore > at) return { value: { status: "waiting", notBefore: reservation.notBefore } };
4134
- if (state.probeClaim && state.probeClaim.leaseUntil > at) {
4135
- return { value: { status: "waiting", reason: "probe", notBefore: state.probeClaim.leaseUntil } };
4459
+ // Any live observer claim defers a start: the claim owns the shared HTTP
4460
+ // permit for its window, so starting here would contend for it.
4461
+ const liveClaim = RATE_RESOURCES
4462
+ .map((resource) => state.probeClaims[resource])
4463
+ .filter((claim) => claim && claim.leaseUntil > at)
4464
+ .sort((left, right) => right.leaseUntil - left.leaseUntil)[0];
4465
+ if (liveClaim) {
4466
+ return { value: { status: "waiting", reason: "probe", notBefore: liveClaim.leaseUntil } };
4136
4467
  }
4137
4468
  for (const resource of RATE_RESOURCES.filter((name) => reservation.costs[name] > 0)) {
4138
4469
  const budget = state.budgets[resource];
@@ -4176,6 +4507,15 @@ function completeReservation(scope, reservationId, completion, nowMs) {
4176
4507
  reservation.outcome = completion.outcome;
4177
4508
  reservation.actualCosts = { ...measured };
4178
4509
  reservation.accountedCosts = { core: 0, graphql: 0 };
4510
+ // The same rule as the observing settlement path: this one narrows the
4511
+ // charge in exactly the same way, so it releases the pacing in exactly the
4512
+ // same way. Wiring it into only one of the two settlement paths left every
4513
+ // doctor probe and manual operation paying for capacity it did not use.
4514
+ if (completion.outcome === "measured-success") {
4515
+ for (const resource of RATE_RESOURCES) {
4516
+ returnPacingCredit(state, resource, reservation.costs[resource] - measured[resource], at);
4517
+ }
4518
+ }
4179
4519
  return { value: { status: "completed", actualCosts: reservation.actualCosts } };
4180
4520
  });
4181
4521
  }
@@ -4267,6 +4607,15 @@ function settleReservationWithBudgetObservations(
4267
4607
  acceptedAfterBaseline[resource],
4268
4608
  );
4269
4609
  }
4610
+ // Only a measured outcome knows what it spent. A timeout, abort or process
4611
+ // loss proves nothing, so its worst case stays charged and its pacing stays
4612
+ // spent -- refunding there would let a run of timeouts pace as though
4613
+ // nothing had been sent.
4614
+ if (completion.outcome === "measured-success") {
4615
+ for (const resource of RATE_RESOURCES) {
4616
+ returnPacingCredit(state, resource, reservation.costs[resource] - measured[resource], at);
4617
+ }
4618
+ }
4270
4619
  scheduleGovernorState(state, at);
4271
4620
  return { value: {
4272
4621
  status: "completed",
@@ -4289,8 +4638,8 @@ function recordResourceBlock(scope, resource, resetMs, reason) {
4289
4638
  resetMs + BUDGET_RESET_GRACE_MS,
4290
4639
  );
4291
4640
  } else {
4292
- state.probeOutcome.nextAt = Math.min(
4293
- state.probeOutcome.nextAt || resetMs + BUDGET_RESET_GRACE_MS,
4641
+ state.observers.graphql.nextAt = Math.min(
4642
+ state.observers.graphql.nextAt || resetMs + BUDGET_RESET_GRACE_MS,
4294
4643
  resetMs + BUDGET_RESET_GRACE_MS,
4295
4644
  );
4296
4645
  }
@@ -4326,7 +4675,7 @@ function governorHealth(result, nowMs = Date.now()) {
4326
4675
  if (!result?.ok) return { status: "unavailable", leases: 0, resources: {} };
4327
4676
  const state = result.value;
4328
4677
  let status = "healthy";
4329
- if (state.probeClaim?.leaseUntil > nowMs) status = "waiting for probe";
4678
+ if (RATE_RESOURCES.some((resource) => state.probeClaims[resource]?.leaseUntil > nowMs)) status = "waiting for probe";
4330
4679
  else if (RATE_RESOURCES.some((resource) => !state.budgets[resource] ||
4331
4680
  nowMs - state.budgets[resource].observedAt > budgetSnapshotTtl(resource))) status = "stale";
4332
4681
  else if (RATE_RESOURCES.some((resource) => state.budgets[resource].blockUntil > nowMs)) status = "blocked";
@@ -4354,7 +4703,40 @@ async function retryGovernorMutation(run, { now, wait, deadline, signal }) {
4354
4703
  return result;
4355
4704
  }
4356
4705
 
4357
- async function refreshSharedBudget(scope, leaseId, signal, {
4706
+ // Each resource refreshes on its own claim. A slow or failing observer for one
4707
+ // must not delay or invalidate the other's readiness -- nothing about the
4708
+ // resources couples them. What still couples them is the shared HTTP permit and
4709
+ // any account-wide secondary hold, and those are enforced where they belong.
4710
+ // Core is refreshed last because its publication is what reopens data
4711
+ // admission. Finishing the cycle's control work before that happens stops a
4712
+ // pane that merely published the reset from getting a head start, over the
4713
+ // shared permit, on higher-priority work waiting for the same lane. It is also
4714
+ // the order readSharedBudgetSources reads its sources in, and the order the
4715
+ // single claim this replaced already used, so the sequence of actual HTTP
4716
+ // calls is unchanged.
4717
+ const BUDGET_REFRESH_ORDER = ["graphql", "core"];
4718
+
4719
+ async function refreshSharedBudget(scope, leaseId, signal, options = {}) {
4720
+ const outcomes = [];
4721
+ for (const resource of BUDGET_REFRESH_ORDER) {
4722
+ outcomes.push([resource, await refreshResourceBudget(scope, leaseId, signal, resource, options)]);
4723
+ }
4724
+ const published = outcomes.filter(([, result]) => result.ok);
4725
+ if (published.length === 0) return outcomes[0][1];
4726
+ const inspect = options.inspect ?? inspectGovernor;
4727
+ const now = options.now ?? (() => scopeNow(scope));
4728
+ const snapshot = inspect(scope, now());
4729
+ return {
4730
+ ok: true,
4731
+ value: {
4732
+ status: "published",
4733
+ budgets: snapshot.ok ? snapshot.value.budgets : {},
4734
+ resources: published.map(([resource]) => resource),
4735
+ },
4736
+ };
4737
+ }
4738
+
4739
+ async function refreshResourceBudget(scope, leaseId, signal, resource, {
4358
4740
  readBudgets = readSharedBudgetSources,
4359
4741
  now = () => scopeNow(scope),
4360
4742
  wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
@@ -4363,7 +4745,7 @@ async function refreshSharedBudget(scope, leaseId, signal, {
4363
4745
  publish = publishProbe,
4364
4746
  fail = failProbeClaim,
4365
4747
  } = {}) {
4366
- let claim = claimProbe(scope, leaseId, now());
4748
+ let claim = claimProbe(scope, leaseId, now(), resource);
4367
4749
  if (!claim.ok) return claim;
4368
4750
  if (claim.value.status !== "claimed") {
4369
4751
  const startedAt = now();
@@ -4376,32 +4758,33 @@ async function refreshSharedBudget(scope, leaseId, signal, {
4376
4758
  inspections += 1;
4377
4759
  const snapshot = inspect(scope, now());
4378
4760
  if (!snapshot.ok) return snapshot;
4761
+ // Only this resource's freshness matters here. Waiting on the other's
4762
+ // observer is what made one failing lane stall an unrelated healthy one.
4379
4763
  if (
4380
- RATE_RESOURCES.every((resource) =>
4381
- snapshot.value.budgets[resource] &&
4382
- now() - snapshot.value.budgets[resource].observedAt <= budgetSnapshotTtl(resource),
4383
- ) && snapshot.value.probeOutcome.status === "healthy"
4764
+ snapshot.value.budgets[resource] &&
4765
+ now() - snapshot.value.budgets[resource].observedAt <= budgetSnapshotTtl(resource) &&
4766
+ snapshot.value.observers[resource].outcome === "healthy"
4384
4767
  ) {
4385
4768
  return { ok: true, value: { status: "published", budgets: snapshot.value.budgets } };
4386
4769
  }
4387
4770
  if (now() >= deadline || inspections >= 10) break;
4388
4771
  await wait(Math.min(100, Math.max(1, deadline - now())));
4389
4772
  }
4390
- claim = claimProbe(scope, leaseId, now());
4773
+ claim = claimProbe(scope, leaseId, now(), resource);
4391
4774
  if (!claim.ok || claim.value.status !== "claimed") return claim;
4392
4775
  }
4393
- const { nonce, startedReservationIds, resources = RATE_RESOURCES, coreEtag = null } = claim.value;
4776
+ const { nonce, startedReservationIds, coreEtag = null } = claim.value;
4394
4777
  const drainUntil = now() + GOVERNOR_PROBE_DRAIN_MS;
4395
4778
  while (startedReservationIds.length > 0 && now() < drainUntil) {
4396
4779
  const snapshot = inspect(scope, now());
4397
4780
  if (!snapshot.ok) {
4398
- failProbeClaim(scope, leaseId, nonce, now());
4781
+ failProbeClaim(scope, leaseId, nonce, now(), resource);
4399
4782
  return snapshot;
4400
4783
  }
4401
4784
  const stillStarted = startedReservationIds.some((id) => snapshot.value.reservations[id]?.status === "started");
4402
4785
  if (!stillStarted) break;
4403
4786
  if (signal?.aborted) {
4404
- failProbeClaim(scope, leaseId, nonce, now());
4787
+ failProbeClaim(scope, leaseId, nonce, now(), resource);
4405
4788
  return { ok: false, reason: "stale" };
4406
4789
  }
4407
4790
  await wait(Math.min(100, Math.max(1, drainUntil - now())));
@@ -4412,11 +4795,11 @@ async function refreshSharedBudget(scope, leaseId, signal, {
4412
4795
  renewalStartedAt + GOVERNOR_PROBE_TRANSITION_MS,
4413
4796
  );
4414
4797
  const renewed = await retryGovernorMutation(
4415
- () => renew(scope, leaseId, nonce, now()),
4798
+ () => renew(scope, leaseId, nonce, now(), resource),
4416
4799
  { now, wait, deadline: renewalDeadline, signal },
4417
4800
  );
4418
4801
  if (!renewed.ok) {
4419
- failProbeClaim(scope, leaseId, nonce, now());
4802
+ failProbeClaim(scope, leaseId, nonce, now(), resource);
4420
4803
  return renewed;
4421
4804
  }
4422
4805
  // Known exhaustion waits for its actual reset. The GraphQL observer costs a
@@ -4424,24 +4807,23 @@ async function refreshSharedBudget(scope, leaseId, signal, {
4424
4807
  // the hole deeper -- the /rate_limit read this replaced was free and could
4425
4808
  // afford to be unconditional.
4426
4809
  const beforeRead = inspect(scope, now());
4427
- const spendable = resources.filter((resource) => {
4428
- if (resource !== "graphql" || !beforeRead.ok) return true;
4429
- const budget = beforeRead.value.budgets.graphql;
4430
- return !(budget && budget.remaining <= 0 && now() < budget.resetMs + BUDGET_RESET_GRACE_MS);
4431
- });
4432
- if (spendable.length === 0) {
4433
- failProbeClaim(scope, leaseId, nonce, now());
4810
+ const exhausted = resource === "graphql" && beforeRead.ok &&
4811
+ beforeRead.value.budgets.graphql &&
4812
+ beforeRead.value.budgets.graphql.remaining <= 0 &&
4813
+ now() < beforeRead.value.budgets.graphql.resetMs + BUDGET_RESET_GRACE_MS;
4814
+ if (exhausted) {
4815
+ failProbeClaim(scope, leaseId, nonce, now(), resource);
4434
4816
  return { ok: false, reason: "budget-reset" };
4435
4817
  }
4436
4818
  let budgets;
4437
4819
  try {
4438
4820
  budgets = await requestIdentityStorage.run(scope, () => readBudgets(signal, scope.host, {
4439
- resources: spendable,
4821
+ resources: [resource],
4440
4822
  coreEtag,
4441
4823
  renewClaim: async () => {
4442
4824
  const startedAt = now();
4443
4825
  const extended = await retryGovernorMutation(
4444
- () => renew(scope, leaseId, nonce, now()),
4826
+ () => renew(scope, leaseId, nonce, now(), resource),
4445
4827
  {
4446
4828
  now,
4447
4829
  wait,
@@ -4462,7 +4844,7 @@ async function refreshSharedBudget(scope, leaseId, signal, {
4462
4844
  now() + GOVERNOR_PROBE_TRANSITION_MS,
4463
4845
  );
4464
4846
  await retryGovernorMutation(
4465
- () => fail(scope, leaseId, nonce, now()),
4847
+ () => fail(scope, leaseId, nonce, now(), resource),
4466
4848
  { now, wait, deadline: failureDeadline, signal },
4467
4849
  );
4468
4850
  return { ok: false, reason: "stale" };
@@ -4472,12 +4854,12 @@ async function refreshSharedBudget(scope, leaseId, signal, {
4472
4854
  now() + GOVERNOR_PROBE_TRANSITION_MS,
4473
4855
  );
4474
4856
  const published = await retryGovernorMutation(
4475
- () => publish(scope, leaseId, nonce, budgets, now()),
4857
+ () => publish(scope, leaseId, nonce, budgets, now(), resource),
4476
4858
  { now, wait, deadline: transitionDeadline, signal },
4477
4859
  );
4478
4860
  if (!published.ok) {
4479
4861
  await retryGovernorMutation(
4480
- () => fail(scope, leaseId, nonce, now()),
4862
+ () => fail(scope, leaseId, nonce, now(), resource),
4481
4863
  { now, wait, deadline: transitionDeadline, signal },
4482
4864
  );
4483
4865
  }
@@ -7499,14 +7881,20 @@ function retryPollAfterAdmissionFailure({
7499
7881
 
7500
7882
  function governorWakeTimes(state, nowMs, floorMs, leaseId = null) {
7501
7883
  const budgets = Object.values(state?.budgets ?? {});
7502
- const observedCandidates = state?.probeOutcome?.status === "failed"
7503
- ? []
7504
- : budgets.map((budget) => budget.observedAt + BUDGET_PROBE_MS);
7884
+ // A failed observer's own retry, not its stale sample's cadence, says when
7885
+ // that resource is next worth reading. Suppress the cadence per resource: a
7886
+ // global suppression let one failure silence a healthy lane's wake, and no
7887
+ // suppression let the failed lane spin on the sample it already knows is old.
7888
+ const observedCandidates = RATE_RESOURCES.flatMap((resource) => {
7889
+ const budget = state?.budgets?.[resource];
7890
+ if (!budget || state?.observers?.[resource]?.outcome === "failed") return [];
7891
+ return [budget.observedAt + BUDGET_PROBE_MS];
7892
+ });
7505
7893
  const controlAt = Math.min(
7506
7894
  ...observedCandidates,
7507
7895
  ...budgets.map((budget) => budget.resetMs + BUDGET_RESET_GRACE_MS),
7508
- state?.probeClaim?.leaseUntil ?? Number.POSITIVE_INFINITY,
7509
- state?.probeOutcome?.nextAt ?? Number.POSITIVE_INFINITY,
7896
+ ...RATE_RESOURCES.map((resource) => state?.probeClaims?.[resource]?.leaseUntil ?? Number.POSITIVE_INFINITY),
7897
+ ...RATE_RESOURCES.map((resource) => state?.observers?.[resource]?.nextAt ?? Number.POSITIVE_INFINITY),
7510
7898
  );
7511
7899
  const reservationAt = Object.values(state?.reservations ?? {})
7512
7900
  .filter((reservation) => reservation.status === "scheduled" &&
@@ -7521,7 +7909,8 @@ function governorWakeTimes(state, nowMs, floorMs, leaseId = null) {
7521
7909
  function governorProtocolReady(refreshResult, snapshot, nowMs) {
7522
7910
  if (!refreshResult?.ok || !snapshot?.ok) return false;
7523
7911
  if (["waiting", "paused", "probe"].includes(refreshResult.value?.status)) return false;
7524
- if (snapshot.value.probeClaim || snapshot.value.probeOutcome?.status !== "healthy") return false;
7912
+ if (RATE_RESOURCES.some((resource) => snapshot.value.probeClaims[resource]) ||
7913
+ RATE_RESOURCES.some((resource) => snapshot.value.observers[resource]?.outcome !== "healthy")) return false;
7525
7914
  return RATE_RESOURCES.every((resource) => {
7526
7915
  const budget = snapshot.value.budgets[resource];
7527
7916
  return budget && nowMs - budget.observedAt <= budgetSnapshotTtl(resource);
@@ -7691,7 +8080,7 @@ function rateLimitBlockDecision(results, resetMs) {
7691
8080
  const IDENTITY_COORDINATION_REASONS = [
7692
8081
  "restart-required", "migration-hold", "legacy-unresolved", "legacy-corrupt",
7693
8082
  "credential-unavailable", "identity-backoff", "identity-busy",
7694
- "identity-unavailable", "identity-capacity", "transport-busy",
8083
+ "identity-unavailable", "identity-capacity", "transport-busy", "throttle-paused",
7695
8084
  ];
7696
8085
 
7697
8086
  function coordinationNotice(reason) {
@@ -10776,6 +11165,7 @@ export {
10776
11165
  governorPhaseOffset,
10777
11166
  scheduleIntents,
10778
11167
  GOVERNOR_STATE_VERSION,
11168
+ RATE_RESOURCES,
10779
11169
  GOVERNOR_MAX_LEASES,
10780
11170
  GOVERNOR_MAX_INTENTS,
10781
11171
  GOVERNOR_MAX_RESERVATIONS,
@@ -10801,6 +11191,16 @@ export {
10801
11191
  heartbeatLease,
10802
11192
  maintainControlLease,
10803
11193
  claimProbe,
11194
+ classifyThrottle,
11195
+ applyTransportCooldown,
11196
+ clearTransportThrottle,
11197
+ emptyTransportThrottle,
11198
+ throttleLadderMs,
11199
+ transportCooldownDeadline,
11200
+ retryThrottledTransport,
11201
+ THROTTLE_LADDER_MS,
11202
+ THROTTLE_PAUSE_AFTER,
11203
+ MANUAL_GRANT_STREAK_LIMIT,
10804
11204
  renewProbeClaim,
10805
11205
  publishProbe,
10806
11206
  failProbeClaim,