@whittlelabs/sifter 0.30.9 → 0.30.10

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 (4) hide show
  1. package/README.md +1 -0
  2. package/bin.js +234 -31
  3. package/bin.js.map +4 -4
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -82,6 +82,7 @@ The Sifter subscribes to your job pool and waits for review jobs. Leave it runni
82
82
  A running Sifter deals with Sift or `claude` going away on its own, and says what it is doing:
83
83
 
84
84
  - **Sift can't be reached** (a deploy, a network blip): it logs one warning, keeps trying, and says when Sift is back. This holds at startup too, so starting it during an outage is fine; it begins claiming once Sift answers.
85
+ - **Sift asks it to slow down** (too many sign-in attempts from this machine in a short time, which Sift answers by locking it out for a while): this is not a problem with the pairing, so it logs one warning saying when it will try again, waits until then without asking sooner, and says when it is back. `run` started during this waits the same way instead of failing; `doctor` warns and says when to try again.
85
86
  - **`claude` stops working** (signed out, out of plan, or uninstalled): the review it was running fails with Claude's own message, and the Sifter stops taking new reviews so the rest of your queue waits for you instead of failing. It logs one line saying what to fix, checks again by itself (after 15 seconds at first, backing off to every 5 minutes), and starts claiming again once `claude` works. A check costs nothing while you are signed out, and one short turn once you are back.
86
87
  - **Sift needs a newer Sifter**: it finishes the review in hand, installs the newest version with `npm install -g`, and restarts itself. If the install fails, it exits with code 2 and prints the command to run.
87
88
  - **The pairing is revoked** (removed in Sift, or replaced by a newer pairing): it exits with code 3 and shows the two ways to pair again. A review it was running goes back to the queue for another Sifter.
package/bin.js CHANGED
@@ -11199,16 +11199,97 @@ var require_client = __commonJS({
11199
11199
  }
11200
11200
  });
11201
11201
 
11202
+ // ../../packages/keep/dist/rate-limited.js
11203
+ var require_rate_limited = __commonJS({
11204
+ "../../packages/keep/dist/rate-limited.js"(exports2) {
11205
+ "use strict";
11206
+ Object.defineProperty(exports2, "__esModule", { value: true });
11207
+ exports2.RateLimitGate = exports2.RATE_LIMIT_DEFAULT_MS = exports2.RATE_LIMIT_CAP_MS = exports2.RATE_LIMIT_FLOOR_MS = exports2.KeepRateLimitedError = void 0;
11208
+ exports2.retryAfterMsOf = retryAfterMsOf;
11209
+ var KeepRateLimitedError = class extends Error {
11210
+ code = "RATE_LIMITED";
11211
+ /** Host the exchange was sent to. */
11212
+ host;
11213
+ /** Keep's own words for the refusal ("Too many attempts. Please try again later."). */
11214
+ reason;
11215
+ /** How long from now to wait before asking again (ms), already floored and capped. */
11216
+ retryAfterMs;
11217
+ /** When the next exchange may be attempted (epoch ms). */
11218
+ retryAt;
11219
+ constructor(message, detail) {
11220
+ super(message);
11221
+ this.name = "KeepRateLimitedError";
11222
+ this.host = detail.host;
11223
+ this.reason = detail.reason;
11224
+ this.retryAt = detail.retryAt;
11225
+ this.retryAfterMs = Math.max(0, detail.retryAt - Date.now());
11226
+ }
11227
+ };
11228
+ exports2.KeepRateLimitedError = KeepRateLimitedError;
11229
+ exports2.RATE_LIMIT_FLOOR_MS = 5e3;
11230
+ exports2.RATE_LIMIT_CAP_MS = 15 * 6e4;
11231
+ exports2.RATE_LIMIT_DEFAULT_MS = 6e4;
11232
+ function retryAfterMsOf(body, headers, now = Date.now()) {
11233
+ return clamp(bodyRetryAfter(body) ?? headerRetryAfter(headers, now) ?? exports2.RATE_LIMIT_DEFAULT_MS);
11234
+ }
11235
+ function bodyRetryAfter(body) {
11236
+ const value = body?.error?.retryAfter;
11237
+ return typeof value === "number" && Number.isFinite(value) ? value * 1e3 : null;
11238
+ }
11239
+ function headerRetryAfter(headers, now) {
11240
+ const raw = headers?.get("retry-after")?.trim();
11241
+ if (!raw)
11242
+ return null;
11243
+ if (/^\d+$/.test(raw))
11244
+ return Number(raw) * 1e3;
11245
+ const at = Date.parse(raw);
11246
+ return Number.isNaN(at) ? null : at - now;
11247
+ }
11248
+ function clamp(ms) {
11249
+ return Math.min(exports2.RATE_LIMIT_CAP_MS, Math.max(exports2.RATE_LIMIT_FLOOR_MS, ms));
11250
+ }
11251
+ var RateLimitGate = class {
11252
+ closed = null;
11253
+ /** Record a 429 and return the error to throw for it. */
11254
+ close(detail) {
11255
+ const retryAt = Date.now() + detail.retryAfterMs;
11256
+ const when = new Date(retryAt).toISOString().replace(/\.\d{3}Z$/, "Z");
11257
+ this.closed = new KeepRateLimitedError(`[${detail.manager}] Keep at ${detail.host} is rate limiting this client's token exchange (429: ${detail.reason}); not asking again before ${when}`, { host: detail.host, reason: detail.reason, retryAt });
11258
+ return this.closed;
11259
+ }
11260
+ /** Throws the rate-limited error while the gate is closed; a no-op once the time has passed. */
11261
+ check() {
11262
+ const closed = this.closed;
11263
+ if (closed === null)
11264
+ return;
11265
+ if (Date.now() >= closed.retryAt) {
11266
+ this.closed = null;
11267
+ return;
11268
+ }
11269
+ throw new KeepRateLimitedError(closed.message, {
11270
+ host: closed.host,
11271
+ reason: closed.reason,
11272
+ retryAt: closed.retryAt
11273
+ });
11274
+ }
11275
+ };
11276
+ exports2.RateLimitGate = RateLimitGate;
11277
+ }
11278
+ });
11279
+
11202
11280
  // ../../packages/keep/dist/service-token.js
11203
11281
  var require_service_token = __commonJS({
11204
11282
  "../../packages/keep/dist/service-token.js"(exports2) {
11205
11283
  "use strict";
11206
11284
  Object.defineProperty(exports2, "__esModule", { value: true });
11207
11285
  exports2.ServiceTokenManager = void 0;
11286
+ var rate_limited_1 = require_rate_limited();
11208
11287
  var ServiceTokenManager = class {
11209
11288
  token = null;
11210
11289
  expiresAt = 0;
11211
11290
  pendingExchange = null;
11291
+ /** Closed by a 429 until Keep's retry-after: no exchange is sent before it. */
11292
+ rateLimit = new rate_limited_1.RateLimitGate();
11212
11293
  keepApiUrl;
11213
11294
  clientId;
11214
11295
  clientSecret;
@@ -11237,6 +11318,7 @@ var require_service_token = __commonJS({
11237
11318
  if (this.pendingExchange) {
11238
11319
  return this.pendingExchange;
11239
11320
  }
11321
+ this.rateLimit.check();
11240
11322
  this.pendingExchange = this.exchange();
11241
11323
  try {
11242
11324
  return await this.pendingExchange;
@@ -11255,6 +11337,15 @@ var require_service_token = __commonJS({
11255
11337
  })
11256
11338
  });
11257
11339
  this.onResponse?.(response);
11340
+ if (response.status === 429) {
11341
+ const body = await response.json().catch(() => null);
11342
+ throw this.rateLimit.close({
11343
+ manager: "ServiceTokenManager",
11344
+ host: hostOf(url),
11345
+ reason: body?.error?.message ?? "Too Many Requests",
11346
+ retryAfterMs: (0, rate_limited_1.retryAfterMsOf)(body, response.headers)
11347
+ });
11348
+ }
11258
11349
  const json = await response.json();
11259
11350
  if (!response.ok || !json.success) {
11260
11351
  const msg = json.error?.message ?? `Token exchange failed with status ${response.status}`;
@@ -11266,6 +11357,13 @@ var require_service_token = __commonJS({
11266
11357
  }
11267
11358
  };
11268
11359
  exports2.ServiceTokenManager = ServiceTokenManager;
11360
+ function hostOf(url) {
11361
+ try {
11362
+ return new URL(url).host;
11363
+ } catch {
11364
+ return url;
11365
+ }
11366
+ }
11269
11367
  }
11270
11368
  });
11271
11369
 
@@ -11275,6 +11373,7 @@ var require_agent_token = __commonJS({
11275
11373
  "use strict";
11276
11374
  Object.defineProperty(exports2, "__esModule", { value: true });
11277
11375
  exports2.AgentTokenManager = exports2.KeepUnavailableError = exports2.AgentCredentialRevokedError = void 0;
11376
+ var rate_limited_1 = require_rate_limited();
11278
11377
  var AgentCredentialRevokedError2 = class extends Error {
11279
11378
  code = "PAIRING_REVOKED";
11280
11379
  constructor(message) {
@@ -11301,6 +11400,8 @@ var require_agent_token = __commonJS({
11301
11400
  token = null;
11302
11401
  expiresAt = 0;
11303
11402
  pendingExchange = null;
11403
+ /** Closed by a 429 until Keep's retry-after: no exchange is sent before it. */
11404
+ rateLimit = new rate_limited_1.RateLimitGate();
11304
11405
  keepApiUrl;
11305
11406
  clientId;
11306
11407
  clientSecret;
@@ -11322,6 +11423,7 @@ var require_agent_token = __commonJS({
11322
11423
  if (this.pendingExchange) {
11323
11424
  return this.pendingExchange;
11324
11425
  }
11426
+ this.rateLimit.check();
11325
11427
  this.pendingExchange = this.exchange();
11326
11428
  try {
11327
11429
  return await this.pendingExchange;
@@ -11357,6 +11459,14 @@ var require_agent_token = __commonJS({
11357
11459
  const reason = json?.error?.message ?? `Agent token exchange failed with status ${response.status}`;
11358
11460
  throw new KeepUnavailableError(`[AgentTokenManager] Keep at ${host} is unavailable (${response.status}): ${reason}`, { host, statusCode: response.status });
11359
11461
  }
11462
+ if (response.status === 429) {
11463
+ throw this.rateLimit.close({
11464
+ manager: "AgentTokenManager",
11465
+ host,
11466
+ reason: json?.error?.message ?? "Too Many Requests",
11467
+ retryAfterMs: (0, rate_limited_1.retryAfterMsOf)(json, response.headers)
11468
+ });
11469
+ }
11360
11470
  if (json === null) {
11361
11471
  throw new Error(`[AgentTokenManager] Keep at ${host} answered ${response.status} with a body that is not JSON`);
11362
11472
  }
@@ -11398,7 +11508,7 @@ var require_dist2 = __commonJS({
11398
11508
  "../../packages/keep/dist/index.js"(exports2) {
11399
11509
  "use strict";
11400
11510
  Object.defineProperty(exports2, "__esModule", { value: true });
11401
- exports2.KeepUnavailableError = exports2.AgentCredentialRevokedError = exports2.AgentTokenManager = exports2.ServiceTokenManager = exports2.KeepClient = void 0;
11511
+ exports2.KeepRateLimitedError = exports2.KeepUnavailableError = exports2.AgentCredentialRevokedError = exports2.AgentTokenManager = exports2.ServiceTokenManager = exports2.KeepClient = void 0;
11402
11512
  var client_1 = require_client();
11403
11513
  Object.defineProperty(exports2, "KeepClient", { enumerable: true, get: function() {
11404
11514
  return client_1.KeepClient;
@@ -11417,6 +11527,10 @@ var require_dist2 = __commonJS({
11417
11527
  Object.defineProperty(exports2, "KeepUnavailableError", { enumerable: true, get: function() {
11418
11528
  return agent_token_1.KeepUnavailableError;
11419
11529
  } });
11530
+ var rate_limited_1 = require_rate_limited();
11531
+ Object.defineProperty(exports2, "KeepRateLimitedError", { enumerable: true, get: function() {
11532
+ return rate_limited_1.KeepRateLimitedError;
11533
+ } });
11420
11534
  }
11421
11535
  });
11422
11536
 
@@ -12282,6 +12396,10 @@ var require_checks = __commonJS({
12282
12396
  return skip("pairing-live", "no readable pairing; see pairing-present");
12283
12397
  }
12284
12398
  const minted = await probes.mintAgentToken(stored.value);
12399
+ if (!minted.ok && minted.rateLimited) {
12400
+ const at = formatInstant(minted.rateLimited.retryAt);
12401
+ return warn("pairing-live", `Keep is rate limiting this pairing's token exchange (429), so the pairing could not be checked; it asked for no retry before ${at}`, subject.phase === "run" ? `nothing to do; \`run\` waits until ${at}, asks once, and starts claiming when Keep issues a token` : `wait until ${at}, then run doctor again`);
12402
+ }
12285
12403
  if (!minted.ok && minted.unavailable) {
12286
12404
  return warn("pairing-live", `Keep did not answer, so the pairing could not be checked: ${oneLine(minted.error).replace(/^\[AgentTokenManager\]\s*/, "")}`, subject.phase === "run" ? "nothing to do if this is a deployment window; `run` waits for Keep and starts claiming when it answers" : `check ${stored.value.keepApiUrl} is reachable, then run doctor again`);
12287
12405
  }
@@ -12339,6 +12457,9 @@ var require_checks = __commonJS({
12339
12457
  const pairing = stored.value;
12340
12458
  return withConfig("jobs-reachable", probes, async (config) => {
12341
12459
  const result = await probes.reachJobs(config, pairing);
12460
+ if (!result.ok && result.rateLimited) {
12461
+ return warn("jobs-reachable", `${config.auth.jobsApiUrl} was not asked: Keep is rate limiting the token exchange until ${formatInstant(result.rateLimited.retryAt)}`, "see pairing-live; the run loop asks once Keep issues a token");
12462
+ }
12342
12463
  if (!result.ok) {
12343
12464
  return warn("jobs-reachable", `${config.auth.jobsApiUrl} did not answer: ${oneLine(result.error)}`, "nothing to do if this is a deployment window; the run loop will keep trying");
12344
12465
  }
@@ -12620,6 +12741,9 @@ var require_checks = __commonJS({
12620
12741
  function osTmpDirOf(probes) {
12621
12742
  return probes.osTmpDir();
12622
12743
  }
12744
+ function formatInstant(epochMs) {
12745
+ return new Date(epochMs).toISOString().replace(/\.\d{3}Z$/, "Z");
12746
+ }
12623
12747
  function oneLine(message) {
12624
12748
  return message.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join(" ");
12625
12749
  }
@@ -20320,6 +20444,20 @@ var require_subscribe = __commonJS({
20320
20444
  return true;
20321
20445
  return err instanceof TypeError && err.message === "fetch failed";
20322
20446
  }
20447
+ function isRateLimited(err) {
20448
+ return err?.code === "RATE_LIMITED";
20449
+ }
20450
+ function rateLimitWaitMs(err) {
20451
+ const e = err ?? {};
20452
+ if (typeof e.retryAfterMs === "number" && Number.isFinite(e.retryAfterMs))
20453
+ return Math.max(0, e.retryAfterMs);
20454
+ if (typeof e.retryAt === "number" && Number.isFinite(e.retryAt))
20455
+ return Math.max(0, e.retryAt - Date.now());
20456
+ return 0;
20457
+ }
20458
+ function formatInstant(epochMs) {
20459
+ return new Date(epochMs).toISOString().replace(/\.\d{3}Z$/, "Z");
20460
+ }
20323
20461
  function describeOutage(err) {
20324
20462
  const e = err ?? {};
20325
20463
  if (typeof e.host === "string" && typeof e.statusCode === "number") {
@@ -20386,11 +20524,33 @@ var require_subscribe = __commonJS({
20386
20524
  let outcome = newOutcome();
20387
20525
  let wakeStartup = null;
20388
20526
  let unavailableSince = null;
20527
+ let rateLimited = null;
20389
20528
  function noteReachable() {
20390
20529
  if (unavailableSince !== null) {
20391
20530
  logger.info(`Sift is reachable again after ${formatDuration(Date.now() - unavailableSince)}`);
20392
20531
  unavailableSince = null;
20393
20532
  }
20533
+ if (rateLimited !== null) {
20534
+ logger.info(`${rateLimited.host} stopped rate limiting the Sifter after ${formatDuration(Date.now() - rateLimited.since)}`);
20535
+ rateLimited = null;
20536
+ }
20537
+ }
20538
+ function noteRateLimited(err) {
20539
+ const waitMs = Math.max(pollIntervalMs, rateLimitWaitMs(err));
20540
+ const e = err ?? {};
20541
+ const host = typeof e.host === "string" ? e.host : "the service";
20542
+ if (rateLimited === null) {
20543
+ rateLimited = { since: Date.now(), host };
20544
+ const reason = typeof e.reason === "string" ? `: ${e.reason}` : "";
20545
+ logger.warn(`${host} is rate limiting the Sifter (429${reason}). It will wait and try again at ${formatInstant(Date.now() + waitMs)} (in ${formatDuration(waitMs)}).`);
20546
+ } else {
20547
+ logger.debug?.("still rate limited", {
20548
+ host,
20549
+ rateLimitedFor: formatDuration(Date.now() - rateLimited.since),
20550
+ nextAttemptIn: formatDuration(waitMs)
20551
+ });
20552
+ }
20553
+ return waitMs;
20394
20554
  }
20395
20555
  function noteUnavailable(err) {
20396
20556
  if (unavailableSince === null) {
@@ -20420,16 +20580,22 @@ var require_subscribe = __commonJS({
20420
20580
  }
20421
20581
  async function untilAvailable(call, mySession) {
20422
20582
  for (; ; ) {
20583
+ let pause = pollIntervalMs;
20423
20584
  try {
20424
20585
  const value = await call();
20425
20586
  noteReachable();
20426
20587
  return value;
20427
20588
  } catch (err) {
20428
- if (isFatal(err) || !isUnavailable(err))
20589
+ if (isFatal(err))
20590
+ throw err;
20591
+ if (isRateLimited(err))
20592
+ pause = noteRateLimited(err);
20593
+ else if (isUnavailable(err))
20594
+ noteUnavailable(err);
20595
+ else
20429
20596
  throw err;
20430
- noteUnavailable(err);
20431
20597
  }
20432
- await startupPause(pollIntervalMs);
20598
+ await startupPause(pause);
20433
20599
  if (stopped || mySession !== session)
20434
20600
  return STOPPED;
20435
20601
  }
@@ -20515,6 +20681,7 @@ var require_subscribe = __commonJS({
20515
20681
  async function poll(mySession) {
20516
20682
  if (stopped || mySession !== session)
20517
20683
  return;
20684
+ let nextPollMs = pollIntervalMs;
20518
20685
  if (inflight >= maxConcurrency) {
20519
20686
  schedulePoll(pollIntervalMs);
20520
20687
  return;
@@ -20557,14 +20724,16 @@ var require_subscribe = __commonJS({
20557
20724
  void fail(err);
20558
20725
  return;
20559
20726
  }
20560
- if (isUnavailable(err)) {
20727
+ if (isRateLimited(err)) {
20728
+ nextPollMs = noteRateLimited(err);
20729
+ } else if (isUnavailable(err)) {
20561
20730
  noteUnavailable(err);
20562
20731
  } else {
20563
20732
  logger.error("poll failed", { error: err instanceof Error ? err.message : String(err) });
20564
20733
  }
20565
20734
  }
20566
20735
  if (!stopped && mySession === session)
20567
- schedulePoll(pollIntervalMs);
20736
+ schedulePoll(nextPollMs);
20568
20737
  }
20569
20738
  async function runOnce(available) {
20570
20739
  let dispatch;
@@ -21035,6 +21204,21 @@ var require_probes = __commonJS({
21035
21204
  packageName: options.brand.product.packageName
21036
21205
  });
21037
21206
  };
21207
+ const managers = /* @__PURE__ */ new Map();
21208
+ const tokensFor = (pairing) => {
21209
+ const key = JSON.stringify([pairing.keepApiUrl, pairing.credentials.clientId, pairing.credentials.clientSecret]);
21210
+ let tokens = managers.get(key);
21211
+ if (tokens === void 0) {
21212
+ tokens = new keep_1.AgentTokenManager({
21213
+ keepApiUrl: pairing.keepApiUrl,
21214
+ clientId: pairing.credentials.clientId,
21215
+ clientSecret: pairing.credentials.clientSecret,
21216
+ onResponse: enforceFloor
21217
+ });
21218
+ managers.set(key, tokens);
21219
+ }
21220
+ return tokens;
21221
+ };
21038
21222
  let pairingRead;
21039
21223
  let configRead;
21040
21224
  let scaffoldedRead;
@@ -21163,12 +21347,7 @@ var require_probes = __commonJS({
21163
21347
  return scaffoldedRead;
21164
21348
  },
21165
21349
  async mintAgentToken(pairing) {
21166
- const tokens = new keep_1.AgentTokenManager({
21167
- keepApiUrl: pairing.keepApiUrl,
21168
- clientId: pairing.credentials.clientId,
21169
- clientSecret: pairing.credentials.clientSecret,
21170
- onResponse: enforceFloor
21171
- });
21350
+ const tokens = tokensFor(pairing);
21172
21351
  try {
21173
21352
  const token = await tokens.getToken();
21174
21353
  if (token === null) {
@@ -21178,16 +21357,14 @@ var require_probes = __commonJS({
21178
21357
  } catch (err) {
21179
21358
  if (isRevoked(err) || isUpgradeRequired(err))
21180
21359
  throw err;
21360
+ if (isKeepRateLimited(err)) {
21361
+ return { ok: false, error: describe(err), rateLimited: { retryAt: err.retryAt } };
21362
+ }
21181
21363
  return { ok: false, error: describe(err), ...isKeepUnavailable(err) ? { unavailable: true } : {} };
21182
21364
  }
21183
21365
  },
21184
21366
  async reachJobs(config, pairing) {
21185
- const tokens = new keep_1.AgentTokenManager({
21186
- keepApiUrl: pairing.keepApiUrl,
21187
- clientId: pairing.credentials.clientId,
21188
- clientSecret: pairing.credentials.clientSecret,
21189
- onResponse: enforceFloor
21190
- });
21367
+ const tokens = tokensFor(pairing);
21191
21368
  const client = new jobs_1.JobsClient({
21192
21369
  baseUrl: config.auth.jobsApiUrl,
21193
21370
  ...config.auth.apiKey ? { apiKey: config.auth.apiKey } : {},
@@ -21204,6 +21381,9 @@ var require_probes = __commonJS({
21204
21381
  } catch (err) {
21205
21382
  if (isRevoked(err) || isUpgradeRequired(err))
21206
21383
  throw err;
21384
+ if (isKeepRateLimited(err)) {
21385
+ return { ok: false, error: describe(err), rateLimited: { retryAt: err.retryAt } };
21386
+ }
21207
21387
  return { ok: false, error: describe(err) };
21208
21388
  }
21209
21389
  },
@@ -21299,6 +21479,9 @@ var require_probes = __commonJS({
21299
21479
  function isKeepUnavailable(err) {
21300
21480
  return err instanceof Error && err.name === "KeepUnavailableError";
21301
21481
  }
21482
+ function isKeepRateLimited(err) {
21483
+ return err instanceof Error && err.name === "KeepRateLimitedError";
21484
+ }
21302
21485
  function isUpgradeRequired(err) {
21303
21486
  return err instanceof Error && err.name === "UpgradeRequiredError";
21304
21487
  }
@@ -25442,6 +25625,12 @@ var require_shuttle = __commonJS({
25442
25625
  function isKeepUnavailable(err) {
25443
25626
  return err instanceof Error && err.name === "KeepUnavailableError";
25444
25627
  }
25628
+ function isKeepRateLimited(err) {
25629
+ return err instanceof Error && err.name === "KeepRateLimitedError";
25630
+ }
25631
+ function formatInstant(epochMs) {
25632
+ return new Date(epochMs).toISOString().replace(/\.\d{3}Z$/, "Z");
25633
+ }
25445
25634
  async function runOutputGates(dispatch, response, executor, signal, ctx) {
25446
25635
  let spec;
25447
25636
  try {
@@ -25897,30 +26086,44 @@ var require_shuttle = __commonJS({
25897
26086
  }
25898
26087
  async function mintWaitingOutKeep(mint, brand, options) {
25899
26088
  const maxDelay = options.retryMaxMs ?? KEEP_RETRY_MAX_MS;
25900
- let delay = options.retryInitialMs ?? KEEP_RETRY_INITIAL_MS;
25901
- let unavailableSince = null;
26089
+ const initialDelay = options.retryInitialMs ?? KEEP_RETRY_INITIAL_MS;
26090
+ let delay = initialDelay;
26091
+ let waiting = null;
25902
26092
  for (; ; ) {
26093
+ let pause;
25903
26094
  try {
25904
26095
  const token = await mint();
25905
- if (unavailableSince !== null) {
25906
- options.logger?.info(`Keep is reachable again after ${formatDuration(Date.now() - unavailableSince)}`);
26096
+ if (waiting?.kind === "unavailable") {
26097
+ options.logger?.info(`Keep is reachable again after ${formatDuration(Date.now() - waiting.since)}`);
26098
+ } else if (waiting?.kind === "rate-limited") {
26099
+ options.logger?.info(`Keep issued a token; the rate limit lifted after ${formatDuration(Date.now() - waiting.since)}`);
25907
26100
  }
25908
26101
  return token;
25909
26102
  } catch (err) {
25910
- if (!isKeepUnavailable(err))
26103
+ if (isKeepRateLimited(err)) {
26104
+ if (waiting?.kind !== "rate-limited") {
26105
+ waiting = { kind: "rate-limited", since: Date.now() };
26106
+ options.logger?.warn(`Keep is rate limiting this ${brand.product.title}'s token exchange (${err.host ?? "Keep"} answered 429: ${err.reason ?? err.message}). The credentials are not in question; the ${brand.product.title} will wait and ask again at ${formatInstant(err.retryAt)} (in ${formatDuration(err.retryAfterMs)}), and start when Keep issues a token.`);
26107
+ }
26108
+ pause = err.retryAfterMs;
26109
+ delay = initialDelay;
26110
+ } else if (isKeepUnavailable(err)) {
26111
+ if (waiting?.kind !== "unavailable") {
26112
+ waiting = { kind: "unavailable", since: Date.now() };
26113
+ const e = err;
26114
+ const what = typeof e.statusCode === "number" ? `${e.host ?? "Keep"} answered ${e.statusCode}` : `${e.host ?? "Keep"} did not answer`;
26115
+ options.logger?.warn(`Keep is unavailable (${what}). This is typical during a deployment window; the ${brand.product.title} will wait for it and start when it answers.`);
26116
+ }
26117
+ pause = delay;
26118
+ delay = Math.min(delay * 2, maxDelay);
26119
+ } else {
25911
26120
  throw err;
25912
- if (unavailableSince === null) {
25913
- unavailableSince = Date.now();
25914
- const e = err;
25915
- const what = typeof e.statusCode === "number" ? `${e.host ?? "Keep"} answered ${e.statusCode}` : `${e.host ?? "Keep"} did not answer`;
25916
- options.logger?.warn(`Keep is unavailable (${what}). This is typical during a deployment window; the ${brand.product.title} will wait for it and start when it answers.`);
25917
26121
  }
25918
26122
  }
25919
- await abortableSleep(delay, options.signal);
26123
+ await abortableSleep(pause, options.signal);
25920
26124
  if (options.signal?.aborted) {
25921
26125
  throw options.signal.reason instanceof Error ? options.signal.reason : new Error("stopped while waiting for Keep");
25922
26126
  }
25923
- delay = Math.min(delay * 2, maxDelay);
25924
26127
  }
25925
26128
  }
25926
26129
  }
@@ -26548,7 +26751,7 @@ var import_path = require("path");
26548
26751
  var import_promises = require("fs/promises");
26549
26752
  var import_yaml = __toESM(require_dist3());
26550
26753
  var import_shuttle = __toESM(require_dist5());
26551
- var buildVersion = true ? "0.30.9" : pkg.version;
26754
+ var buildVersion = true ? "0.30.10" : pkg.version;
26552
26755
  var sifterBrand = {
26553
26756
  product: {
26554
26757
  id: "sifter",