@whittlelabs/sifter 0.30.8 → 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 +13 -0
  2. package/bin.js +937 -96
  3. package/bin.js.map +4 -4
  4. package/package.json +1 -1
package/bin.js CHANGED
@@ -9832,6 +9832,29 @@ var require_dist = __commonJS({
9832
9832
  }
9833
9833
  });
9834
9834
 
9835
+ // ../../packages/shuttle/dist/executors/types.js
9836
+ var require_types2 = __commonJS({
9837
+ "../../packages/shuttle/dist/executors/types.js"(exports2) {
9838
+ "use strict";
9839
+ Object.defineProperty(exports2, "__esModule", { value: true });
9840
+ exports2.ExecutorEnvironmentError = void 0;
9841
+ exports2.isExecutorEnvironmentError = isExecutorEnvironmentError;
9842
+ var ExecutorEnvironmentError = class extends Error {
9843
+ /** What the operator should do, as one clause: "run `claude` and sign in". */
9844
+ remedy;
9845
+ constructor(message, remedy) {
9846
+ super(message);
9847
+ this.name = "ExecutorEnvironmentError";
9848
+ this.remedy = remedy;
9849
+ }
9850
+ };
9851
+ exports2.ExecutorEnvironmentError = ExecutorEnvironmentError;
9852
+ function isExecutorEnvironmentError(err) {
9853
+ return err instanceof Error && err.name === "ExecutorEnvironmentError";
9854
+ }
9855
+ }
9856
+ });
9857
+
9835
9858
  // ../../packages/shuttle/dist/executors/validate-output.js
9836
9859
  var require_validate_output = __commonJS({
9837
9860
  "../../packages/shuttle/dist/executors/validate-output.js"(exports2) {
@@ -10005,6 +10028,7 @@ var require_claude_code = __commonJS({
10005
10028
  return mod && mod.__esModule ? mod : { "default": mod };
10006
10029
  };
10007
10030
  Object.defineProperty(exports2, "__esModule", { value: true });
10031
+ exports2.classifyEnvironmentFault = classifyEnvironmentFault;
10008
10032
  exports2.createClaudeCodeExecutor = createClaudeCodeExecutor;
10009
10033
  exports2.parseStreamJson = parseStreamJson;
10010
10034
  exports2.relativeWorkspaceReads = relativeWorkspaceReads;
@@ -10017,6 +10041,7 @@ var require_claude_code = __commonJS({
10017
10041
  var path_1 = __importDefault(require("path"));
10018
10042
  var string_decoder_1 = require("string_decoder");
10019
10043
  var loom_1 = require_dist();
10044
+ var types_1 = require_types2();
10020
10045
  var validate_output_1 = require_validate_output();
10021
10046
  var progress_1 = require_progress();
10022
10047
  var apply_1 = require_apply();
@@ -10030,6 +10055,39 @@ var require_claude_code = __commonJS({
10030
10055
  const subtype = meta.subtype && meta.subtype !== "success" ? `: ${meta.subtype}` : "";
10031
10056
  return `${fallback}${subtype}${status}`;
10032
10057
  }
10058
+ var READINESS_PROMPT = "Reply with the single word: ok";
10059
+ var READINESS_TIMEOUT_MS = 12e4;
10060
+ var STDERR_LIMIT = 4e3;
10061
+ var INSTALL_REMEDY = "install Claude Code (npm install -g @anthropic-ai/claude-code) where this process can find it on PATH";
10062
+ var LOGIN_REMEDY = "run `claude` once and sign in (or set a valid ANTHROPIC_API_KEY)";
10063
+ var LIMIT_REMEDY = "wait for the plan's usage limit to reset, or raise it";
10064
+ var SIGNED_OUT = /invalid api key|please run \/login|not (?:logged|signed) in|oauth token (?:has )?expired|authentication_error|invalid x-api-key/i;
10065
+ var OUT_OF_USAGE = /usage limit|credit balance is too low|out of (?:extra )?usage|hit your (?:\w+ )?(?:usage |spend )?limit|5-hour limit/i;
10066
+ function classifyEnvironmentFault(failure) {
10067
+ if (failure.spawnErrorCode === "ENOENT")
10068
+ return INSTALL_REMEDY;
10069
+ const text = failure.text ?? "";
10070
+ if (failure.apiErrorStatus === 401 || SIGNED_OUT.test(text))
10071
+ return LOGIN_REMEDY;
10072
+ if (failure.apiErrorStatus === 402 || failure.apiErrorStatus === 429 || OUT_OF_USAGE.test(text)) {
10073
+ return LIMIT_REMEDY;
10074
+ }
10075
+ return null;
10076
+ }
10077
+ function firstLine(text) {
10078
+ for (const line of text.split("\n")) {
10079
+ const trimmed = line.trim();
10080
+ if (trimmed.length > 0)
10081
+ return trimmed.length > 300 ? `${trimmed.slice(0, 300)}...` : trimmed;
10082
+ }
10083
+ return null;
10084
+ }
10085
+ function runFailure(local, message, facts) {
10086
+ if (!local)
10087
+ return new Error(message);
10088
+ const remedy = classifyEnvironmentFault(facts);
10089
+ return remedy ? new types_1.ExecutorEnvironmentError(message, remedy) : new Error(message);
10090
+ }
10033
10091
  var ClaudeCodeExecutor = class {
10034
10092
  capability;
10035
10093
  strategies = [loom_1.PROMPT_EXECUTION_STRATEGY];
@@ -10094,7 +10152,21 @@ var require_claude_code = __commonJS({
10094
10152
  promptOnlyScratch = await fs_1.promises.mkdtemp(path_1.default.join(tmpBase, "shuttle-cc-run-"));
10095
10153
  }
10096
10154
  const runCwd = workspace?.cwd ?? promptOnlyScratch;
10097
- const { response, readPaths, toolInputs } = await this.runClaude(prompt, spec, runCwd, childEnv, signal, hintedModel, reporter, void 0, dispatch.job.id);
10155
+ const { response, readPaths, toolInputs } = await this.runClaude(
10156
+ prompt,
10157
+ spec,
10158
+ runCwd,
10159
+ childEnv,
10160
+ signal,
10161
+ hintedModel,
10162
+ reporter,
10163
+ void 0,
10164
+ dispatch.job.id,
10165
+ // Local mode is the operator's own login: a failure of THAT is the
10166
+ // machine's, and the runner pauses on it. With a revealed key it is one
10167
+ // customer's, and only their job fails.
10168
+ modelKey === null
10169
+ );
10098
10170
  if (workspace) {
10099
10171
  const reads = [...readPaths];
10100
10172
  for (const rel of creditOverlayReads(toolInputs, workspace.overlay?.writtenPaths ?? [], runCwd)) {
@@ -10156,14 +10228,84 @@ var require_claude_code = __commonJS({
10156
10228
  outputLabel: handle.outputLabel,
10157
10229
  outputSchema: handle.outputSchema
10158
10230
  };
10159
- const { response } = await this.runClaude(followUp, spec, scratch, handle.childEnv, signal, handle.model, null, handle.sessionId, `${handle.sessionId}-repair`);
10231
+ const { response } = await this.runClaude(followUp, spec, scratch, handle.childEnv, signal, handle.model, null, handle.sessionId, `${handle.sessionId}-repair`, handle.childEnv === void 0);
10160
10232
  return response;
10161
10233
  } finally {
10162
10234
  await fs_1.promises.rm(scratch, { recursive: true, force: true }).catch(() => {
10163
10235
  });
10164
10236
  }
10165
10237
  }
10166
- runClaude(prompt, spec, cwd, childEnv, signal, modelOverride, reporter, resumeSessionId, transcriptName) {
10238
+ /**
10239
+ * Can this machine run a job now? One real one-turn call through the CLI,
10240
+ * the same probe `doctor --deep` makes. The runner asks it only while
10241
+ * claiming is paused on an `ExecutorEnvironmentError`, and only in local
10242
+ * mode. A signed-out or missing CLI fails before any model call, so the
10243
+ * re-check spends nothing until the login works again, and then one turn.
10244
+ */
10245
+ async checkReadiness(signal) {
10246
+ return new Promise((resolve) => {
10247
+ let child;
10248
+ try {
10249
+ child = (0, child_process_1.spawn)("claude", ["--print", "--output-format", "stream-json", "--verbose", "--max-turns", "1", READINESS_PROMPT], { stdio: ["ignore", "pipe", "pipe"] });
10250
+ } catch (err) {
10251
+ resolve({ ready: false, reason: err instanceof Error ? err.message : String(err) });
10252
+ return;
10253
+ }
10254
+ let stdout = "";
10255
+ let stderr = "";
10256
+ let settled = false;
10257
+ const settle = (value) => {
10258
+ if (settled)
10259
+ return;
10260
+ settled = true;
10261
+ clearTimeout(timer);
10262
+ signal.removeEventListener("abort", onAbort);
10263
+ resolve(value);
10264
+ };
10265
+ const timer = setTimeout(() => {
10266
+ child.kill("SIGKILL");
10267
+ settle({ ready: false, reason: `the claude CLI did not answer within ${READINESS_TIMEOUT_MS / 1e3}s` });
10268
+ }, READINESS_TIMEOUT_MS);
10269
+ function onAbort() {
10270
+ child.kill("SIGTERM");
10271
+ settle({ ready: false, reason: "readiness check aborted" });
10272
+ }
10273
+ if (signal.aborted) {
10274
+ onAbort();
10275
+ return;
10276
+ }
10277
+ signal.addEventListener("abort", onAbort, { once: true });
10278
+ child.stdout?.on("data", (chunk) => {
10279
+ stdout += chunk.toString("utf-8");
10280
+ });
10281
+ child.stderr?.on("data", (chunk) => {
10282
+ if (stderr.length < STDERR_LIMIT)
10283
+ stderr += chunk.toString("utf-8");
10284
+ });
10285
+ child.on("error", (err) => {
10286
+ const code = err.code;
10287
+ settle({
10288
+ ready: false,
10289
+ reason: code === "ENOENT" ? `the claude CLI is not on this machine's PATH (${err.message})` : err.message
10290
+ });
10291
+ });
10292
+ child.on("close", (code) => {
10293
+ let meta = null;
10294
+ try {
10295
+ meta = parseStreamJson(stdout).resultMeta;
10296
+ } catch {
10297
+ meta = null;
10298
+ }
10299
+ if (code === 0 && !meta?.isError) {
10300
+ settle({ ready: true });
10301
+ return;
10302
+ }
10303
+ const said = meta?.detail ?? firstLine(stderr) ?? firstLine(stdout);
10304
+ settle({ ready: false, reason: said ?? `the claude CLI exited with code ${code}` });
10305
+ });
10306
+ });
10307
+ }
10308
+ runClaude(prompt, spec, cwd, childEnv, signal, modelOverride, reporter, resumeSessionId, transcriptName, local = false) {
10167
10309
  const outputLabel = spec.outputLabel;
10168
10310
  const model = modelOverride ?? this.config.model;
10169
10311
  return new Promise((resolve, reject) => {
@@ -10194,6 +10336,7 @@ var require_claude_code = __commonJS({
10194
10336
  return;
10195
10337
  }
10196
10338
  const stdoutChunks = [];
10339
+ let stderrText = "";
10197
10340
  let settled = false;
10198
10341
  const settle = (fn) => {
10199
10342
  if (settled)
@@ -10228,6 +10371,10 @@ var require_claude_code = __commonJS({
10228
10371
  if (tap && decoder)
10229
10372
  tap.push(decoder.write(chunk));
10230
10373
  });
10374
+ child.stderr?.on("data", (chunk) => {
10375
+ if (stderrText.length < STDERR_LIMIT)
10376
+ stderrText += chunk.toString("utf-8");
10377
+ });
10231
10378
  function onAbort() {
10232
10379
  child.kill("SIGTERM");
10233
10380
  settle(() => reject(new Error("Dispatch aborted")));
@@ -10242,6 +10389,11 @@ var require_claude_code = __commonJS({
10242
10389
  child.on("error", (err) => {
10243
10390
  clearTimeout(timer);
10244
10391
  signal.removeEventListener("abort", onAbort);
10392
+ const code = err.code;
10393
+ if (local && code === "ENOENT") {
10394
+ settle(() => reject(new types_1.ExecutorEnvironmentError(`the claude CLI is not on this machine's PATH (${err.message})`, INSTALL_REMEDY)));
10395
+ return;
10396
+ }
10245
10397
  settle(() => reject(err));
10246
10398
  });
10247
10399
  child.on("close", (code) => {
@@ -10259,7 +10411,10 @@ var require_claude_code = __commonJS({
10259
10411
  try {
10260
10412
  const stream = parseStreamJson(stdout);
10261
10413
  if (stream.resultMeta?.isError) {
10262
- throw new Error(describeClaudeFailure(stream.resultMeta, "Claude Code reported a failure"));
10414
+ throw runFailure(local, describeClaudeFailure(stream.resultMeta, "Claude Code reported a failure"), {
10415
+ apiErrorStatus: stream.resultMeta.apiErrorStatus,
10416
+ text: stream.resultMeta.detail ?? firstLine(stderrText)
10417
+ });
10263
10418
  }
10264
10419
  const answerText = stream.finalText ?? stdout;
10265
10420
  const parsed = stream.structuredOutput ?? extractJsonObject(answerText);
@@ -10301,7 +10456,17 @@ var require_claude_code = __commonJS({
10301
10456
  meta = parseStreamJson(stdout).resultMeta;
10302
10457
  } catch {
10303
10458
  }
10304
- settle(() => reject(new Error(describeClaudeFailure(meta, `Claude Code exited with code ${code}`))));
10459
+ const said = meta?.detail ?? firstLine(stderrText) ?? firstLine(stdout);
10460
+ const fallback = `Claude Code exited with code ${code}`;
10461
+ const failure = runFailure(local, describeClaudeFailure(meta, fallback), {
10462
+ apiErrorStatus: meta?.apiErrorStatus ?? null,
10463
+ text: said
10464
+ });
10465
+ if (failure instanceof types_1.ExecutorEnvironmentError && !meta && said) {
10466
+ settle(() => reject(new types_1.ExecutorEnvironmentError(`${fallback}: ${said}`, failure.remedy)));
10467
+ return;
10468
+ }
10469
+ settle(() => reject(failure));
10305
10470
  }
10306
10471
  });
10307
10472
  });
@@ -11034,16 +11199,97 @@ var require_client = __commonJS({
11034
11199
  }
11035
11200
  });
11036
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
+
11037
11280
  // ../../packages/keep/dist/service-token.js
11038
11281
  var require_service_token = __commonJS({
11039
11282
  "../../packages/keep/dist/service-token.js"(exports2) {
11040
11283
  "use strict";
11041
11284
  Object.defineProperty(exports2, "__esModule", { value: true });
11042
11285
  exports2.ServiceTokenManager = void 0;
11286
+ var rate_limited_1 = require_rate_limited();
11043
11287
  var ServiceTokenManager = class {
11044
11288
  token = null;
11045
11289
  expiresAt = 0;
11046
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();
11047
11293
  keepApiUrl;
11048
11294
  clientId;
11049
11295
  clientSecret;
@@ -11072,6 +11318,7 @@ var require_service_token = __commonJS({
11072
11318
  if (this.pendingExchange) {
11073
11319
  return this.pendingExchange;
11074
11320
  }
11321
+ this.rateLimit.check();
11075
11322
  this.pendingExchange = this.exchange();
11076
11323
  try {
11077
11324
  return await this.pendingExchange;
@@ -11090,6 +11337,15 @@ var require_service_token = __commonJS({
11090
11337
  })
11091
11338
  });
11092
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
+ }
11093
11349
  const json = await response.json();
11094
11350
  if (!response.ok || !json.success) {
11095
11351
  const msg = json.error?.message ?? `Token exchange failed with status ${response.status}`;
@@ -11101,6 +11357,13 @@ var require_service_token = __commonJS({
11101
11357
  }
11102
11358
  };
11103
11359
  exports2.ServiceTokenManager = ServiceTokenManager;
11360
+ function hostOf(url) {
11361
+ try {
11362
+ return new URL(url).host;
11363
+ } catch {
11364
+ return url;
11365
+ }
11366
+ }
11104
11367
  }
11105
11368
  });
11106
11369
 
@@ -11109,7 +11372,8 @@ var require_agent_token = __commonJS({
11109
11372
  "../../packages/keep/dist/agent-token.js"(exports2) {
11110
11373
  "use strict";
11111
11374
  Object.defineProperty(exports2, "__esModule", { value: true });
11112
- exports2.AgentTokenManager = exports2.AgentCredentialRevokedError = void 0;
11375
+ exports2.AgentTokenManager = exports2.KeepUnavailableError = exports2.AgentCredentialRevokedError = void 0;
11376
+ var rate_limited_1 = require_rate_limited();
11113
11377
  var AgentCredentialRevokedError2 = class extends Error {
11114
11378
  code = "PAIRING_REVOKED";
11115
11379
  constructor(message) {
@@ -11118,10 +11382,26 @@ var require_agent_token = __commonJS({
11118
11382
  }
11119
11383
  };
11120
11384
  exports2.AgentCredentialRevokedError = AgentCredentialRevokedError2;
11385
+ var KeepUnavailableError = class extends Error {
11386
+ code = "SERVICE_UNAVAILABLE";
11387
+ /** Host the exchange was sent to, so a log line names the service that is down. */
11388
+ host;
11389
+ /** The status whatever answered sent, or null when nothing answered at all. */
11390
+ statusCode;
11391
+ constructor(message, detail) {
11392
+ super(message, detail.cause !== void 0 ? { cause: detail.cause } : void 0);
11393
+ this.name = "KeepUnavailableError";
11394
+ this.host = detail.host;
11395
+ this.statusCode = detail.statusCode;
11396
+ }
11397
+ };
11398
+ exports2.KeepUnavailableError = KeepUnavailableError;
11121
11399
  var AgentTokenManager = class {
11122
11400
  token = null;
11123
11401
  expiresAt = 0;
11124
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();
11125
11405
  keepApiUrl;
11126
11406
  clientId;
11127
11407
  clientSecret;
@@ -11143,6 +11423,7 @@ var require_agent_token = __commonJS({
11143
11423
  if (this.pendingExchange) {
11144
11424
  return this.pendingExchange;
11145
11425
  }
11426
+ this.rateLimit.check();
11146
11427
  this.pendingExchange = this.exchange();
11147
11428
  try {
11148
11429
  return await this.pendingExchange;
@@ -11152,16 +11433,43 @@ var require_agent_token = __commonJS({
11152
11433
  }
11153
11434
  async exchange() {
11154
11435
  const url = `${this.keepApiUrl}/api/agent-tokens`;
11155
- const response = await fetch(url, {
11156
- method: "POST",
11157
- headers: { "Content-Type": "application/json" },
11158
- body: JSON.stringify({
11159
- clientId: this.clientId,
11160
- clientSecret: this.clientSecret
11161
- })
11162
- });
11436
+ const host = hostOf(url);
11437
+ let response;
11438
+ try {
11439
+ response = await fetch(url, {
11440
+ method: "POST",
11441
+ headers: { "Content-Type": "application/json" },
11442
+ body: JSON.stringify({
11443
+ clientId: this.clientId,
11444
+ clientSecret: this.clientSecret
11445
+ })
11446
+ });
11447
+ } catch (err) {
11448
+ throw new KeepUnavailableError(`[AgentTokenManager] could not reach Keep at ${host}: ${describeTransportError(err)}`, { host, statusCode: null, cause: err });
11449
+ }
11163
11450
  this.onResponse?.(response);
11164
- const json = await response.json();
11451
+ const text = await response.text().catch(() => "");
11452
+ let json;
11453
+ try {
11454
+ json = JSON.parse(text);
11455
+ } catch {
11456
+ json = null;
11457
+ }
11458
+ if (response.status >= 500) {
11459
+ const reason = json?.error?.message ?? `Agent token exchange failed with status ${response.status}`;
11460
+ throw new KeepUnavailableError(`[AgentTokenManager] Keep at ${host} is unavailable (${response.status}): ${reason}`, { host, statusCode: response.status });
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
+ }
11470
+ if (json === null) {
11471
+ throw new Error(`[AgentTokenManager] Keep at ${host} answered ${response.status} with a body that is not JSON`);
11472
+ }
11165
11473
  if (!response.ok || !json.success) {
11166
11474
  const msg = json.error?.message ?? `Agent token exchange failed with status ${response.status}`;
11167
11475
  if (response.status === 401) {
@@ -11175,6 +11483,23 @@ var require_agent_token = __commonJS({
11175
11483
  }
11176
11484
  };
11177
11485
  exports2.AgentTokenManager = AgentTokenManager;
11486
+ function hostOf(url) {
11487
+ try {
11488
+ return new URL(url).host;
11489
+ } catch {
11490
+ return url;
11491
+ }
11492
+ }
11493
+ function describeTransportError(err) {
11494
+ if (err instanceof Error) {
11495
+ const cause = err.cause;
11496
+ if (cause instanceof Error && cause.message && cause.message !== err.message) {
11497
+ return `${err.message}: ${cause.message}`;
11498
+ }
11499
+ return err.message;
11500
+ }
11501
+ return String(err);
11502
+ }
11178
11503
  }
11179
11504
  });
11180
11505
 
@@ -11183,7 +11508,7 @@ var require_dist2 = __commonJS({
11183
11508
  "../../packages/keep/dist/index.js"(exports2) {
11184
11509
  "use strict";
11185
11510
  Object.defineProperty(exports2, "__esModule", { value: true });
11186
- 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;
11187
11512
  var client_1 = require_client();
11188
11513
  Object.defineProperty(exports2, "KeepClient", { enumerable: true, get: function() {
11189
11514
  return client_1.KeepClient;
@@ -11199,6 +11524,13 @@ var require_dist2 = __commonJS({
11199
11524
  Object.defineProperty(exports2, "AgentCredentialRevokedError", { enumerable: true, get: function() {
11200
11525
  return agent_token_1.AgentCredentialRevokedError;
11201
11526
  } });
11527
+ Object.defineProperty(exports2, "KeepUnavailableError", { enumerable: true, get: function() {
11528
+ return agent_token_1.KeepUnavailableError;
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
+ } });
11202
11534
  }
11203
11535
  });
11204
11536
 
@@ -12064,6 +12396,13 @@ var require_checks = __commonJS({
12064
12396
  return skip("pairing-live", "no readable pairing; see pairing-present");
12065
12397
  }
12066
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
+ }
12403
+ if (!minted.ok && minted.unavailable) {
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`);
12405
+ }
12067
12406
  if (!minted.ok) {
12068
12407
  return fail("pairing-live", `Keep did not issue a token: ${oneLine(minted.error)}`, `check ${stored.value.keepApiUrl} is reachable, then run \`${subject.brand.product.cliBinary} init\` if the pairing was revoked`);
12069
12408
  }
@@ -12118,6 +12457,9 @@ var require_checks = __commonJS({
12118
12457
  const pairing = stored.value;
12119
12458
  return withConfig("jobs-reachable", probes, async (config) => {
12120
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
+ }
12121
12463
  if (!result.ok) {
12122
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");
12123
12465
  }
@@ -12399,6 +12741,9 @@ var require_checks = __commonJS({
12399
12741
  function osTmpDirOf(probes) {
12400
12742
  return probes.osTmpDir();
12401
12743
  }
12744
+ function formatInstant(epochMs) {
12745
+ return new Date(epochMs).toISOString().replace(/\.\d{3}Z$/, "Z");
12746
+ }
12402
12747
  function oneLine(message) {
12403
12748
  return message.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join(" ");
12404
12749
  }
@@ -20092,6 +20437,41 @@ var require_subscribe = __commonJS({
20092
20437
  function sleep(ms) {
20093
20438
  return new Promise((resolve) => setTimeout(resolve, ms));
20094
20439
  }
20440
+ function isUnavailable(err) {
20441
+ if (err instanceof response_1.ServiceUnavailableError)
20442
+ return true;
20443
+ if (err?.code === "SERVICE_UNAVAILABLE")
20444
+ return true;
20445
+ return err instanceof TypeError && err.message === "fetch failed";
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
+ }
20461
+ function describeOutage(err) {
20462
+ const e = err ?? {};
20463
+ if (typeof e.host === "string" && typeof e.statusCode === "number") {
20464
+ const text = typeof e.statusText === "string" && e.statusText ? ` ${e.statusText}` : "";
20465
+ return `${e.host} answered ${e.statusCode}${text}`;
20466
+ }
20467
+ if (typeof e.host === "string")
20468
+ return `${e.host} did not answer`;
20469
+ if (err instanceof Error) {
20470
+ const cause = err.cause;
20471
+ return cause instanceof Error ? `${err.message}: ${cause.message}` : err.message;
20472
+ }
20473
+ return String(err);
20474
+ }
20095
20475
  function formatDuration(ms) {
20096
20476
  const seconds = Math.round(ms / 1e3);
20097
20477
  if (seconds < 60)
@@ -20100,112 +20480,270 @@ var require_subscribe = __commonJS({
20100
20480
  const rest = seconds % 60;
20101
20481
  return rest === 0 ? `${minutes}m` : `${minutes}m ${rest}s`;
20102
20482
  }
20483
+ function newOutcome() {
20484
+ let resolve;
20485
+ let reject;
20486
+ const promise = new Promise((res, rej) => {
20487
+ resolve = res;
20488
+ reject = rej;
20489
+ });
20490
+ promise.catch(() => {
20491
+ });
20492
+ const outcome = {
20493
+ promise,
20494
+ settled: false,
20495
+ resolve: () => {
20496
+ if (outcome.settled)
20497
+ return;
20498
+ outcome.settled = true;
20499
+ resolve();
20500
+ },
20501
+ reject: (err) => {
20502
+ if (outcome.settled)
20503
+ return;
20504
+ outcome.settled = true;
20505
+ reject(err);
20506
+ }
20507
+ };
20508
+ return outcome;
20509
+ }
20510
+ var STOPPED = Symbol("stopped");
20103
20511
  function subscribe(opts) {
20104
20512
  const logger = opts.logger ?? NULL_LOGGER;
20105
20513
  const pollIntervalMs = opts.pollIntervalMs ?? 2e3;
20106
20514
  const maxConcurrency = opts.maxConcurrency ?? 1;
20515
+ const isFatal = opts.isFatal ?? (() => false);
20107
20516
  let stopped = true;
20517
+ let session = 0;
20108
20518
  let pollTimer = null;
20109
20519
  let inflight = 0;
20110
20520
  let resolvedPoolIds = [];
20521
+ const controllers = /* @__PURE__ */ new Set();
20522
+ const holds = /* @__PURE__ */ new Set();
20523
+ let fatalError = null;
20524
+ let outcome = newOutcome();
20525
+ let wakeStartup = null;
20111
20526
  let unavailableSince = null;
20527
+ let rateLimited = null;
20528
+ function noteReachable() {
20529
+ if (unavailableSince !== null) {
20530
+ logger.info(`Sift is reachable again after ${formatDuration(Date.now() - unavailableSince)}`);
20531
+ unavailableSince = null;
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;
20554
+ }
20555
+ function noteUnavailable(err) {
20556
+ if (unavailableSince === null) {
20557
+ unavailableSince = Date.now();
20558
+ logger.warn(`Sift is unavailable (${describeOutage(err)}). This is typical during a deployment window; the Sifter will keep trying.`);
20559
+ } else {
20560
+ const e = err ?? {};
20561
+ logger.debug?.("Sift still unavailable", {
20562
+ ...typeof e.host === "string" ? { host: e.host } : {},
20563
+ ...typeof e.statusCode === "number" ? { status: e.statusCode } : {},
20564
+ unavailableFor: formatDuration(Date.now() - unavailableSince)
20565
+ });
20566
+ }
20567
+ }
20568
+ function startupPause(ms) {
20569
+ return new Promise((resolve) => {
20570
+ const timer = setTimeout(() => {
20571
+ wakeStartup = null;
20572
+ resolve();
20573
+ }, ms);
20574
+ wakeStartup = () => {
20575
+ clearTimeout(timer);
20576
+ wakeStartup = null;
20577
+ resolve();
20578
+ };
20579
+ });
20580
+ }
20581
+ async function untilAvailable(call, mySession) {
20582
+ for (; ; ) {
20583
+ let pause = pollIntervalMs;
20584
+ try {
20585
+ const value = await call();
20586
+ noteReachable();
20587
+ return value;
20588
+ } catch (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
20596
+ throw err;
20597
+ }
20598
+ await startupPause(pause);
20599
+ if (stopped || mySession !== session)
20600
+ return STOPPED;
20601
+ }
20602
+ }
20112
20603
  async function start() {
20113
20604
  if (!stopped)
20114
20605
  return;
20115
20606
  stopped = false;
20607
+ session += 1;
20608
+ const mySession = session;
20609
+ if (outcome.settled)
20610
+ outcome = newOutcome();
20611
+ fatalError = null;
20116
20612
  resolvedPoolIds = [];
20117
- for (const ref of opts.pools) {
20118
- const id = await opts.jobsClient.resolvePoolRef(ref);
20119
- resolvedPoolIds.push(id);
20120
- await opts.jobsClient.registerMember(id, {
20121
- identityType: opts.subscriber.identityType,
20122
- identityId: opts.subscriber.identityId,
20123
- intelligence: opts.subscriber.intelligence,
20124
- ...opts.subscriber.displayName !== void 0 ? { displayName: opts.subscriber.displayName } : {},
20125
- ...opts.subscriber.metadata !== void 0 ? { metadata: opts.subscriber.metadata } : {}
20126
- });
20613
+ try {
20614
+ for (const ref of opts.pools) {
20615
+ const id = await untilAvailable(() => opts.jobsClient.resolvePoolRef(ref), mySession);
20616
+ if (id === STOPPED)
20617
+ return;
20618
+ resolvedPoolIds.push(id);
20619
+ const registered = await untilAvailable(() => opts.jobsClient.registerMember(id, {
20620
+ identityType: opts.subscriber.identityType,
20621
+ identityId: opts.subscriber.identityId,
20622
+ intelligence: opts.subscriber.intelligence,
20623
+ ...opts.subscriber.displayName !== void 0 ? { displayName: opts.subscriber.displayName } : {},
20624
+ ...opts.subscriber.metadata !== void 0 ? { metadata: opts.subscriber.metadata } : {}
20625
+ }), mySession);
20626
+ if (registered === STOPPED)
20627
+ return;
20628
+ }
20629
+ } catch (err) {
20630
+ if (mySession === session) {
20631
+ stopped = true;
20632
+ outcome.reject(err);
20633
+ }
20634
+ throw err;
20127
20635
  }
20636
+ if (stopped || mySession !== session)
20637
+ return;
20128
20638
  logger.info("subscribe loop started", { pools: resolvedPoolIds, maxConcurrency });
20129
20639
  schedulePoll(pollIntervalMs);
20130
20640
  }
20131
- async function stop() {
20132
- if (stopped)
20133
- return;
20641
+ async function finish() {
20642
+ while (inflight > 0) {
20643
+ await sleep(100);
20644
+ }
20645
+ logger.info("subscribe loop stopped");
20646
+ if (fatalError !== null)
20647
+ outcome.reject(fatalError.error);
20648
+ else
20649
+ outcome.resolve();
20650
+ }
20651
+ function halt() {
20134
20652
  stopped = true;
20135
20653
  if (pollTimer !== null) {
20136
20654
  clearTimeout(pollTimer);
20137
20655
  pollTimer = null;
20138
20656
  }
20139
- while (inflight > 0) {
20140
- await sleep(100);
20141
- }
20142
- logger.info("subscribe loop stopped");
20657
+ wakeStartup?.();
20658
+ }
20659
+ async function stop() {
20660
+ if (stopped)
20661
+ return;
20662
+ halt();
20663
+ await finish();
20664
+ }
20665
+ async function fail(err) {
20666
+ if (fatalError === null)
20667
+ fatalError = { error: err };
20668
+ for (const controller of controllers)
20669
+ controller.abort(err);
20670
+ if (stopped)
20671
+ return;
20672
+ halt();
20673
+ await finish();
20143
20674
  }
20144
20675
  function schedulePoll(delayMs) {
20676
+ const mySession = session;
20145
20677
  pollTimer = setTimeout(() => {
20146
- void poll();
20678
+ void poll(mySession);
20147
20679
  }, delayMs);
20148
20680
  }
20149
- async function poll() {
20150
- if (stopped)
20681
+ async function poll(mySession) {
20682
+ if (stopped || mySession !== session)
20151
20683
  return;
20684
+ let nextPollMs = pollIntervalMs;
20152
20685
  if (inflight >= maxConcurrency) {
20153
20686
  schedulePoll(pollIntervalMs);
20154
20687
  return;
20155
20688
  }
20156
20689
  try {
20690
+ const paused = holds.size > 0;
20157
20691
  const { data, directives } = await opts.jobsClient.listDispatchableJobs({
20158
20692
  pools: resolvedPoolIds,
20159
- limit: maxConcurrency - inflight,
20693
+ limit: paused ? 1 : maxConcurrency - inflight,
20160
20694
  sort: "priority:desc,queued_at:asc"
20161
20695
  });
20162
- if (unavailableSince !== null) {
20163
- logger.info(`Sift is reachable again after ${formatDuration(Date.now() - unavailableSince)}`);
20164
- unavailableSince = null;
20165
- }
20696
+ noteReachable();
20166
20697
  if (directives && Object.keys(directives).length > 0 && opts.onDirectives) {
20167
20698
  try {
20168
20699
  await opts.onDirectives(directives);
20169
20700
  } catch (err) {
20701
+ if (isFatal(err)) {
20702
+ void fail(err);
20703
+ return;
20704
+ }
20170
20705
  logger.error("onDirectives handler failed", {
20171
20706
  error: err instanceof Error ? err.message : String(err)
20172
20707
  });
20173
20708
  }
20174
20709
  }
20175
- if (stopped)
20710
+ if (stopped || mySession !== session)
20176
20711
  return;
20177
- for (const job of data) {
20178
- if (inflight >= maxConcurrency)
20179
- break;
20180
- inflight++;
20181
- void runOnce(job).finally(() => {
20182
- inflight--;
20183
- });
20184
- }
20185
- } catch (err) {
20186
- if (err instanceof response_1.ServiceUnavailableError) {
20187
- if (unavailableSince === null) {
20188
- unavailableSince = Date.now();
20189
- logger.warn(`Sift is unavailable (${err.host} answered ${err.statusCode}${err.statusText ? ` ${err.statusText}` : ""}). This is typical during a deployment window; the Sifter will keep trying.`);
20190
- } else {
20191
- logger.debug?.("Sift still unavailable", {
20192
- host: err.host,
20193
- status: err.statusCode,
20194
- unavailableFor: formatDuration(Date.now() - unavailableSince)
20712
+ if (holds.size === 0) {
20713
+ for (const job of data) {
20714
+ if (inflight >= maxConcurrency)
20715
+ break;
20716
+ inflight++;
20717
+ void runOnce(job).finally(() => {
20718
+ inflight--;
20195
20719
  });
20196
20720
  }
20721
+ }
20722
+ } catch (err) {
20723
+ if (isFatal(err)) {
20724
+ void fail(err);
20725
+ return;
20726
+ }
20727
+ if (isRateLimited(err)) {
20728
+ nextPollMs = noteRateLimited(err);
20729
+ } else if (isUnavailable(err)) {
20730
+ noteUnavailable(err);
20197
20731
  } else {
20198
20732
  logger.error("poll failed", { error: err instanceof Error ? err.message : String(err) });
20199
20733
  }
20200
20734
  }
20201
- if (!stopped)
20202
- schedulePoll(pollIntervalMs);
20735
+ if (!stopped && mySession === session)
20736
+ schedulePoll(nextPollMs);
20203
20737
  }
20204
20738
  async function runOnce(available) {
20205
20739
  let dispatch;
20206
20740
  try {
20207
20741
  dispatch = await opts.jobsClient.acceptDispatch(available.id);
20208
20742
  } catch (err) {
20743
+ if (isFatal(err)) {
20744
+ void fail(err);
20745
+ return;
20746
+ }
20209
20747
  const statusCode = err.statusCode;
20210
20748
  if (statusCode === 409) {
20211
20749
  logger.debug?.("claim conflict; another member accepted", { jobId: available.id });
@@ -20218,6 +20756,9 @@ var require_subscribe = __commonJS({
20218
20756
  return;
20219
20757
  }
20220
20758
  const controller = new AbortController();
20759
+ controllers.add(controller);
20760
+ if (fatalError !== null)
20761
+ controller.abort(fatalError.error);
20221
20762
  let quietTimer = null;
20222
20763
  const quietWindowMs = dispatch.deadline ? new Date(dispatch.deadline).getTime() - Date.now() : null;
20223
20764
  const armQuietTimer = () => {
@@ -20243,6 +20784,8 @@ var require_subscribe = __commonJS({
20243
20784
  try {
20244
20785
  result = await opts.onJob(dispatch, controller.signal, activity);
20245
20786
  } catch (err) {
20787
+ if (isFatal(err))
20788
+ void fail(err);
20246
20789
  result = {
20247
20790
  status: "failed",
20248
20791
  reason: `onJob threw: ${err instanceof Error ? err.message : String(err)}`
@@ -20250,6 +20793,11 @@ var require_subscribe = __commonJS({
20250
20793
  } finally {
20251
20794
  if (quietTimer !== null)
20252
20795
  clearTimeout(quietTimer);
20796
+ controllers.delete(controller);
20797
+ }
20798
+ if (fatalError !== null) {
20799
+ logger.debug?.("not submitting a result after a fatal error", { jobId: dispatch.job.id });
20800
+ return;
20253
20801
  }
20254
20802
  try {
20255
20803
  for (const output of result.outputs ?? []) {
@@ -20263,13 +20811,27 @@ var require_subscribe = __commonJS({
20263
20811
  ...result.reason !== void 0 ? { reason: result.reason } : {}
20264
20812
  });
20265
20813
  } catch (err) {
20814
+ if (isFatal(err)) {
20815
+ void fail(err);
20816
+ return;
20817
+ }
20266
20818
  logger.error("submit/close failed", {
20267
20819
  jobId: dispatch.job.id,
20268
20820
  error: err instanceof Error ? err.message : String(err)
20269
20821
  });
20270
20822
  }
20271
20823
  }
20272
- return { start, stop };
20824
+ return {
20825
+ start,
20826
+ stop,
20827
+ done: () => outcome.promise,
20828
+ pause: (reason) => {
20829
+ holds.add(reason);
20830
+ },
20831
+ resume: (reason) => {
20832
+ holds.delete(reason);
20833
+ }
20834
+ };
20273
20835
  }
20274
20836
  }
20275
20837
  });
@@ -20642,6 +21204,21 @@ var require_probes = __commonJS({
20642
21204
  packageName: options.brand.product.packageName
20643
21205
  });
20644
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
+ };
20645
21222
  let pairingRead;
20646
21223
  let configRead;
20647
21224
  let scaffoldedRead;
@@ -20770,12 +21347,7 @@ var require_probes = __commonJS({
20770
21347
  return scaffoldedRead;
20771
21348
  },
20772
21349
  async mintAgentToken(pairing) {
20773
- const tokens = new keep_1.AgentTokenManager({
20774
- keepApiUrl: pairing.keepApiUrl,
20775
- clientId: pairing.credentials.clientId,
20776
- clientSecret: pairing.credentials.clientSecret,
20777
- onResponse: enforceFloor
20778
- });
21350
+ const tokens = tokensFor(pairing);
20779
21351
  try {
20780
21352
  const token = await tokens.getToken();
20781
21353
  if (token === null) {
@@ -20785,16 +21357,14 @@ var require_probes = __commonJS({
20785
21357
  } catch (err) {
20786
21358
  if (isRevoked(err) || isUpgradeRequired(err))
20787
21359
  throw err;
20788
- return { ok: false, error: describe(err) };
21360
+ if (isKeepRateLimited(err)) {
21361
+ return { ok: false, error: describe(err), rateLimited: { retryAt: err.retryAt } };
21362
+ }
21363
+ return { ok: false, error: describe(err), ...isKeepUnavailable(err) ? { unavailable: true } : {} };
20789
21364
  }
20790
21365
  },
20791
21366
  async reachJobs(config, pairing) {
20792
- const tokens = new keep_1.AgentTokenManager({
20793
- keepApiUrl: pairing.keepApiUrl,
20794
- clientId: pairing.credentials.clientId,
20795
- clientSecret: pairing.credentials.clientSecret,
20796
- onResponse: enforceFloor
20797
- });
21367
+ const tokens = tokensFor(pairing);
20798
21368
  const client = new jobs_1.JobsClient({
20799
21369
  baseUrl: config.auth.jobsApiUrl,
20800
21370
  ...config.auth.apiKey ? { apiKey: config.auth.apiKey } : {},
@@ -20811,6 +21381,9 @@ var require_probes = __commonJS({
20811
21381
  } catch (err) {
20812
21382
  if (isRevoked(err) || isUpgradeRequired(err))
20813
21383
  throw err;
21384
+ if (isKeepRateLimited(err)) {
21385
+ return { ok: false, error: describe(err), rateLimited: { retryAt: err.retryAt } };
21386
+ }
20814
21387
  return { ok: false, error: describe(err) };
20815
21388
  }
20816
21389
  },
@@ -20903,6 +21476,12 @@ var require_probes = __commonJS({
20903
21476
  function isRevoked(err) {
20904
21477
  return err instanceof Error && err.name === "AgentCredentialRevokedError";
20905
21478
  }
21479
+ function isKeepUnavailable(err) {
21480
+ return err instanceof Error && err.name === "KeepUnavailableError";
21481
+ }
21482
+ function isKeepRateLimited(err) {
21483
+ return err instanceof Error && err.name === "KeepRateLimitedError";
21484
+ }
20906
21485
  function isUpgradeRequired(err) {
20907
21486
  return err instanceof Error && err.name === "UpgradeRequiredError";
20908
21487
  }
@@ -23465,6 +24044,8 @@ var require_self_update = __commonJS({
23465
24044
  Object.defineProperty(exports2, "__esModule", { value: true });
23466
24045
  exports2.readUpgradeDirective = readUpgradeDirective;
23467
24046
  exports2.performSelfUpdate = performSelfUpdate;
24047
+ exports2.floorUpdateTarget = floorUpdateTarget;
24048
+ exports2.updateForFloor = updateForFloor;
23468
24049
  var child_process_1 = require("child_process");
23469
24050
  var version_check_1 = require_version_check();
23470
24051
  function readUpgradeDirective(directives, currentVersion, attemptedVersions) {
@@ -23506,6 +24087,32 @@ var require_self_update = __commonJS({
23506
24087
  respawn();
23507
24088
  return true;
23508
24089
  }
24090
+ function floorUpdateTarget(minVersion, latestVersion) {
24091
+ return latestVersion !== null && (0, version_check_1.compareSemver)(latestVersion, minVersion) >= 0 ? latestVersion : minVersion;
24092
+ }
24093
+ async function updateForFloor(opts) {
24094
+ const { productTitle, packageName, currentVersion, minVersion, hosted, logger, hooks } = opts;
24095
+ const refusal = new version_check_1.UpgradeRequiredError({ currentVersion, minVersion, packageName });
24096
+ const target = floorUpdateTarget(minVersion, opts.latestVersion);
24097
+ logger.warn(`The backend now requires ${packageName} ${minVersion} or newer, and this ${productTitle} is ${currentVersion}. It has stopped claiming and is finishing the work in hand, then ` + (hosted ? `exiting: a hosted ${productTitle} is updated by redeploying its image.` : `updating itself to ${target}.`));
24098
+ await hooks.drain();
24099
+ if (hosted) {
24100
+ hooks.exit(refusal);
24101
+ return;
24102
+ }
24103
+ const spec = `${packageName}@${target}`;
24104
+ try {
24105
+ await (hooks.install ?? defaultInstall)(spec);
24106
+ } catch (err) {
24107
+ logger.error(`could not install ${spec}, so this ${productTitle} cannot continue`, {
24108
+ error: err instanceof Error ? err.message : String(err)
24109
+ });
24110
+ hooks.exit(refusal);
24111
+ return;
24112
+ }
24113
+ logger.info("self-update installed; restarting", { packageSpec: spec });
24114
+ (hooks.respawn ?? defaultRespawn)();
24115
+ }
23509
24116
  function defaultInstall(packageSpec) {
23510
24117
  return new Promise((resolve, reject) => {
23511
24118
  const child = (0, child_process_1.spawn)("npm", ["install", "-g", packageSpec], {
@@ -24954,12 +25561,14 @@ var require_shuttle = __commonJS({
24954
25561
  var decommission_1 = require_decommission();
24955
25562
  var loom_1 = require_dist();
24956
25563
  var chain_1 = require_chain();
25564
+ var types_1 = require_types2();
24957
25565
  var workspace_1 = require_workspace();
24958
25566
  var credentials_1 = require_credentials();
24959
25567
  var output_gates_1 = require_output_gates();
24960
25568
  var keep_1 = require_dist2();
24961
25569
  var apply_1 = require_apply();
24962
25570
  var version_check_1 = require_version_check();
25571
+ var typed_exit_1 = require_typed_exit();
24963
25572
  var logger_1 = require_logger();
24964
25573
  var registry_1 = require_registry();
24965
25574
  var claude_code_1 = require_claude_code();
@@ -24972,6 +25581,56 @@ var require_shuttle = __commonJS({
24972
25581
  var spend_tracker_1 = require_spend_tracker();
24973
25582
  var store_1 = require_store();
24974
25583
  var GATE_REPAIR_BUDGET = 2;
25584
+ var ENVIRONMENT_HOLD = "executor-environment";
25585
+ var RECHECK_INITIAL_MS = 15e3;
25586
+ var RECHECK_MAX_MS = 3e5;
25587
+ var KEEP_RETRY_INITIAL_MS = 2e3;
25588
+ var KEEP_RETRY_MAX_MS = 3e4;
25589
+ function formatDuration(ms) {
25590
+ const seconds = Math.round(ms / 1e3);
25591
+ if (seconds < 60)
25592
+ return `${seconds}s`;
25593
+ const minutes = Math.floor(seconds / 60);
25594
+ const rest = seconds % 60;
25595
+ return rest === 0 ? `${minutes}m` : `${minutes}m ${rest}s`;
25596
+ }
25597
+ function abortableSleep(ms, signal) {
25598
+ return new Promise((resolve) => {
25599
+ if (signal?.aborted) {
25600
+ resolve();
25601
+ return;
25602
+ }
25603
+ const timer = setTimeout(() => {
25604
+ signal?.removeEventListener("abort", onAbort);
25605
+ resolve();
25606
+ }, ms);
25607
+ function onAbort() {
25608
+ clearTimeout(timer);
25609
+ resolve();
25610
+ }
25611
+ signal?.addEventListener("abort", onAbort, { once: true });
25612
+ });
25613
+ }
25614
+ function deferred() {
25615
+ let resolve;
25616
+ let reject;
25617
+ const promise = new Promise((res, rej) => {
25618
+ resolve = res;
25619
+ reject = rej;
25620
+ });
25621
+ promise.catch(() => {
25622
+ });
25623
+ return { promise, resolve, reject };
25624
+ }
25625
+ function isKeepUnavailable(err) {
25626
+ return err instanceof Error && err.name === "KeepUnavailableError";
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
+ }
24975
25634
  async function runOutputGates(dispatch, response, executor, signal, ctx) {
24976
25635
  let spec;
24977
25636
  try {
@@ -25007,6 +25666,7 @@ var require_shuttle = __commonJS({
25007
25666
  config;
25008
25667
  profile;
25009
25668
  credentialDeps;
25669
+ environmentRecheck;
25010
25670
  subscription = null;
25011
25671
  spendTracker = null;
25012
25672
  /**
@@ -25015,11 +25675,26 @@ var require_shuttle = __commonJS({
25015
25675
  * (no such config) → consumers reveal nothing and use ambient credentials.
25016
25676
  */
25017
25677
  broker = null;
25678
+ /**
25679
+ * The run's outcome, which `done()` hands out. Resolves on `stop()`.
25680
+ * Rejects with what ended the run otherwise: a refused credential
25681
+ * (`AgentCredentialRevokedError`, exit 3) or a floor this build could not
25682
+ * update past (`UpgradeRequiredError`, exit 2).
25683
+ */
25684
+ lifetime = deferred();
25685
+ /** Aborted by `stop()`: ends the startup waits and the readiness re-checks. */
25686
+ stopping = new AbortController();
25687
+ /** When claiming was paused on an executor environment fault; null while claiming. */
25688
+ environmentPausedAt = null;
25018
25689
  constructor(options) {
25019
25690
  this.brand = options.brand;
25020
25691
  this.config = options.config;
25021
25692
  this.profile = options.profile ?? apply_1.DEFAULT_PROFILE;
25022
25693
  this.credentialDeps = options.credentialDeps ?? {};
25694
+ this.environmentRecheck = options.environmentRecheck ?? {
25695
+ initialMs: RECHECK_INITIAL_MS,
25696
+ maxMs: RECHECK_MAX_MS
25697
+ };
25023
25698
  }
25024
25699
  /** Exposed so the CLI's `caps set` can reload after a write. */
25025
25700
  get spendTrackerHandle() {
@@ -25034,19 +25709,62 @@ var require_shuttle = __commonJS({
25034
25709
  logger.info(`${this.brand.product.title} starting...`);
25035
25710
  const paths = (0, apply_1.resolvePaths)(this.brand, this.profile);
25036
25711
  this.broker = (0, credentials_1.createCredentialBroker)(this.config.credentials, this.credentialDeps);
25037
- const enforceMinVersion = (response) => {
25038
- (0, version_check_1.checkResponseMinVersion)({
25039
- response,
25712
+ const subscribeLogger = {
25713
+ debug: (msg, meta) => logger.debug(msg, meta),
25714
+ info: (msg, meta) => logger.info(msg, meta),
25715
+ warn: (msg, meta) => logger.warn(msg, meta),
25716
+ error: (msg, meta) => logger.error(msg, meta)
25717
+ };
25718
+ let running = false;
25719
+ let latestSeen = null;
25720
+ let floorRising = false;
25721
+ const onBackendResponse = (response) => {
25722
+ latestSeen = (0, version_check_1.readResponseLatestVersion)(response) ?? latestSeen;
25723
+ if (!running) {
25724
+ (0, version_check_1.checkResponseMinVersion)({
25725
+ response,
25726
+ currentVersion: this.brand.product.version,
25727
+ packageName: this.brand.product.packageName
25728
+ });
25729
+ return;
25730
+ }
25731
+ if (floorRising)
25732
+ return;
25733
+ const floor = response.headers.get(version_check_1.MIN_VERSION_HEADER);
25734
+ if (!floor || (0, version_check_1.compareSemver)(this.brand.product.version, floor) >= 0)
25735
+ return;
25736
+ floorRising = true;
25737
+ void (0, self_update_1.updateForFloor)({
25738
+ productTitle: this.brand.product.title,
25739
+ packageName: this.brand.product.packageName,
25040
25740
  currentVersion: this.brand.product.version,
25041
- packageName: this.brand.product.packageName
25042
- });
25741
+ minVersion: floor,
25742
+ latestVersion: latestSeen,
25743
+ // A `credentials` block is what makes a deployment hosted (ADR 0027).
25744
+ hosted: this.config.credentials !== void 0,
25745
+ logger: subscribeLogger,
25746
+ hooks: {
25747
+ drain: () => this.subscription?.stop() ?? Promise.resolve(),
25748
+ exit: (err) => this.lifetime.reject(err)
25749
+ }
25750
+ }).catch((err) => this.lifetime.reject(err));
25043
25751
  };
25044
- const auth = await resolveAuth(this.brand, this.config, paths.configFile, enforceMinVersion);
25752
+ let auth;
25753
+ try {
25754
+ auth = await resolveAuth(this.brand, this.config, paths.configFile, onBackendResponse, {
25755
+ signal: this.stopping.signal,
25756
+ logger: subscribeLogger
25757
+ });
25758
+ } catch (err) {
25759
+ if (this.stopping.signal.aborted)
25760
+ return;
25761
+ throw err;
25762
+ }
25045
25763
  const jobsClient = new jobs_1.JobsClient({
25046
25764
  baseUrl: this.config.auth.jobsApiUrl,
25047
25765
  apiKey: auth.kind === "apiKey" ? auth.apiKey : void 0,
25048
25766
  getAccessToken: auth.kind === "tokenProvider" ? auth.getToken : void 0,
25049
- onResponse: enforceMinVersion
25767
+ onResponse: onBackendResponse
25050
25768
  });
25051
25769
  const registry = new registry_1.ExecutorRegistry(this.brand.executorAllowlist);
25052
25770
  registerBuiltInExecutors(registry, this.brand.executorAllowlist);
@@ -25137,6 +25855,9 @@ var require_shuttle = __commonJS({
25137
25855
  response = await executor.execute(dispatch, signal, executionCtx);
25138
25856
  } catch (err) {
25139
25857
  const reason = `executor failed: ${err instanceof Error ? err.message : String(err)}`;
25858
+ if (this.broker === null && (0, types_1.isExecutorEnvironmentError)(err)) {
25859
+ this.pauseForEnvironment(executor, err, subscribeLogger);
25860
+ }
25140
25861
  await observerChain.notify({ kind: "attendance.closed", jobId, attendanceId, status: "failed", reason, totalDurationMs: Date.now() - startedAt }, onObserverError);
25141
25862
  return { status: "failed", reason };
25142
25863
  } finally {
@@ -25168,12 +25889,6 @@ var require_shuttle = __commonJS({
25168
25889
  return { status: "completed", outputs };
25169
25890
  };
25170
25891
  const pools = this.config.pools.map((p) => p.id ? { id: p.id } : { name: p.name });
25171
- const subscribeLogger = {
25172
- debug: (msg, meta) => logger.debug(msg, meta),
25173
- info: (msg, meta) => logger.info(msg, meta),
25174
- warn: (msg, meta) => logger.warn(msg, meta),
25175
- error: (msg, meta) => logger.error(msg, meta)
25176
- };
25177
25892
  const attemptedUpgrades = /* @__PURE__ */ new Set();
25178
25893
  this.subscription = (0, jobs_1.subscribe)({
25179
25894
  jobsClient,
@@ -25206,6 +25921,8 @@ var require_shuttle = __commonJS({
25206
25921
  });
25207
25922
  return;
25208
25923
  }
25924
+ if (floorRising)
25925
+ return;
25209
25926
  const directive = (0, self_update_1.readUpgradeDirective)(directives, this.brand.product.version, attemptedUpgrades);
25210
25927
  if (!directive)
25211
25928
  return;
@@ -25216,15 +25933,33 @@ var require_shuttle = __commonJS({
25216
25933
  logger: subscribeLogger,
25217
25934
  hooks: {
25218
25935
  stopLoop: () => this.subscription?.stop() ?? Promise.resolve(),
25219
- restartLoop: () => this.subscription?.start() ?? Promise.resolve()
25936
+ // The restarted session's outcome needs the same watch as the
25937
+ // first one's, or a fatal after an upgrade that failed to install
25938
+ // would end nobody's wait. A restart that cannot register ends the
25939
+ // run the same way.
25940
+ restartLoop: () => (this.subscription?.start() ?? Promise.resolve()).then(() => this.watchSession()).catch((err) => this.lifetime.reject(err))
25220
25941
  }
25221
25942
  });
25222
25943
  },
25944
+ // The two failures that end a run with their own exit code and banner:
25945
+ // a refused credential (exit 3) and a below-floor build (exit 2),
25946
+ // matched by name as bin.ts's typed exits are. In-flight jobs are
25947
+ // aborted and their results not submitted (see subscribe's isFatal):
25948
+ // with the credential refused nothing can be submitted anyway, and the
25949
+ // claims lapse back to the pool for another Sifter. A floor rise
25950
+ // normally arrives through onBackendResponse, which drains instead; one
25951
+ // reaching this predicate came from somewhere unexpected and is
25952
+ // treated as the refusal it is.
25953
+ isFatal: typed_exit_1.isTypedExit,
25223
25954
  logger: subscribeLogger,
25224
25955
  pollIntervalMs: this.config.polling.intervalMs,
25225
25956
  maxConcurrency: this.config.concurrency.maxJobs
25226
25957
  });
25227
25958
  await this.subscription.start();
25959
+ if (this.stopping.signal.aborted)
25960
+ return;
25961
+ this.watchSession();
25962
+ running = true;
25228
25963
  logger.info(`${this.brand.product.title} running`, {
25229
25964
  profile: this.profile,
25230
25965
  pools: pools.length,
@@ -25233,13 +25968,72 @@ var require_shuttle = __commonJS({
25233
25968
  observers: observers.length
25234
25969
  });
25235
25970
  }
25971
+ /**
25972
+ * The run's outcome: resolves after `stop()`, rejects with the typed error
25973
+ * that ended it otherwise (a refused credential, or a floor this build could
25974
+ * not update past). `run` awaits this after `start()`, so those errors reach
25975
+ * the whitelabel's exit codes and banners instead of dying in the loop.
25976
+ */
25977
+ done() {
25978
+ return this.lifetime.promise;
25979
+ }
25236
25980
  async stop() {
25237
25981
  const logger = (0, logger_1.getLogger)();
25238
25982
  logger.info(`${this.brand.product.title} stopping...`);
25983
+ this.stopping.abort();
25239
25984
  if (this.subscription) {
25240
25985
  await this.subscription.stop();
25241
25986
  }
25242
25987
  logger.info(`${this.brand.product.title} stopped`);
25988
+ this.lifetime.resolve();
25989
+ }
25990
+ /** Forward the current subscribe session's fatal, if one ends it, to the run's outcome. */
25991
+ watchSession() {
25992
+ this.subscription?.done().catch((err) => this.lifetime.reject(err));
25993
+ }
25994
+ /**
25995
+ * Stop claiming because the executor says this machine cannot run jobs,
25996
+ * and keep checking until it can. One warning says why and what to do; the
25997
+ * re-checks stay at debug; one line says when claiming resumes. Idempotent:
25998
+ * a second failing job while paused (maxJobs > 1) changes nothing.
25999
+ */
26000
+ pauseForEnvironment(executor, err, logger) {
26001
+ if (this.environmentPausedAt !== null || !this.subscription)
26002
+ return;
26003
+ this.environmentPausedAt = Date.now();
26004
+ this.subscription.pause(ENVIRONMENT_HOLD);
26005
+ logger.warn(`Claiming paused: ${executor.capability.id} cannot run jobs on this machine (${err.message}). To fix it, ${err.remedy}. The ${this.brand.product.title} checks again on its own and resumes claiming when it works.`);
26006
+ void this.recheckEnvironment(executor, logger);
26007
+ }
26008
+ async recheckEnvironment(executor, logger) {
26009
+ const signal = this.stopping.signal;
26010
+ let delay = this.environmentRecheck.initialMs;
26011
+ for (; ; ) {
26012
+ await abortableSleep(delay, signal);
26013
+ if (signal.aborted)
26014
+ return;
26015
+ let readiness;
26016
+ try {
26017
+ readiness = executor.checkReadiness ? await executor.checkReadiness(signal) : { ready: true };
26018
+ } catch (err) {
26019
+ readiness = { ready: false, reason: err instanceof Error ? err.message : String(err) };
26020
+ }
26021
+ if (signal.aborted)
26022
+ return;
26023
+ if (readiness.ready) {
26024
+ const pausedFor = formatDuration(Date.now() - (this.environmentPausedAt ?? Date.now()));
26025
+ this.environmentPausedAt = null;
26026
+ this.subscription?.resume(ENVIRONMENT_HOLD);
26027
+ logger.info(`${executor.capability.id} can run jobs again; claiming resumed after ${pausedFor} paused.`);
26028
+ return;
26029
+ }
26030
+ delay = Math.min(delay * 2, this.environmentRecheck.maxMs);
26031
+ logger.debug?.("executor still cannot run jobs", {
26032
+ executor: executor.capability.id,
26033
+ reason: readiness.reason,
26034
+ nextCheckIn: formatDuration(delay)
26035
+ });
26036
+ }
25243
26037
  }
25244
26038
  };
25245
26039
  exports2.Shuttle = Shuttle;
@@ -25267,7 +26061,7 @@ var require_shuttle = __commonJS({
25267
26061
  }
25268
26062
  }
25269
26063
  }
25270
- async function resolveAuth(brand, config, pairingFile, onResponse) {
26064
+ async function resolveAuth(brand, config, pairingFile, onResponse, options = {}) {
25271
26065
  const store = new store_1.PairingConfigStore(pairingFile);
25272
26066
  if (store.exists()) {
25273
26067
  const pairing = store.read();
@@ -25277,7 +26071,7 @@ var require_shuttle = __commonJS({
25277
26071
  clientSecret: pairing.credentials.clientSecret,
25278
26072
  onResponse
25279
26073
  });
25280
- const token = await tokens.getToken();
26074
+ const token = await mintWaitingOutKeep(() => tokens.getToken(), brand, options);
25281
26075
  if (token) {
25282
26076
  return {
25283
26077
  kind: "tokenProvider",
@@ -25290,6 +26084,48 @@ var require_shuttle = __commonJS({
25290
26084
  }
25291
26085
  throw new Error(`No credentials available. Run \`${brand.product.cliBinary} init\` to pair, or set auth.apiKey in your config.`);
25292
26086
  }
26087
+ async function mintWaitingOutKeep(mint, brand, options) {
26088
+ const maxDelay = options.retryMaxMs ?? KEEP_RETRY_MAX_MS;
26089
+ const initialDelay = options.retryInitialMs ?? KEEP_RETRY_INITIAL_MS;
26090
+ let delay = initialDelay;
26091
+ let waiting = null;
26092
+ for (; ; ) {
26093
+ let pause;
26094
+ try {
26095
+ const token = await mint();
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)}`);
26100
+ }
26101
+ return token;
26102
+ } catch (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 {
26120
+ throw err;
26121
+ }
26122
+ }
26123
+ await abortableSleep(pause, options.signal);
26124
+ if (options.signal?.aborted) {
26125
+ throw options.signal.reason instanceof Error ? options.signal.reason : new Error("stopped while waiting for Keep");
26126
+ }
26127
+ }
26128
+ }
25293
26129
  }
25294
26130
  });
25295
26131
 
@@ -25361,8 +26197,9 @@ var require_run = __commonJS({
25361
26197
  process.exit(preflight_1.PREFLIGHT_EXIT_CODE);
25362
26198
  }
25363
26199
  const shuttle = new shuttle_1.Shuttle({ brand, config, profile });
25364
- await shuttle.start();
25365
26200
  (0, shutdown_1.setupShutdownHandler)(() => shuttle.stop());
26201
+ await shuttle.start();
26202
+ await shuttle.done();
25366
26203
  } catch (err) {
25367
26204
  if ((0, typed_exit_1.isTypedExit)(err))
25368
26205
  throw err;
@@ -25714,8 +26551,8 @@ var require_dist5 = __commonJS({
25714
26551
  "../../packages/shuttle/dist/index.js"(exports2) {
25715
26552
  "use strict";
25716
26553
  Object.defineProperty(exports2, "__esModule", { value: true });
25717
- exports2.pair = exports2.scaffoldedConfigPath = exports2.planScaffold = exports2.DEFAULT_REFUSAL_MESSAGE = exports2.DEFAULT_MONTHLY_PERIOD = exports2.DEFAULT_DAILY_PERIOD = exports2.SUBSTRATE_BRAND = exports2.verificationJson = exports2.resolveEnvironment = exports2.renderVerificationLine = exports2.renderEnvironmentReport = exports2.isWorthReporting = exports2.hostOf = exports2.environmentReportJson = exports2.applyEnvironment = exports2.renderTemplate = exports2.expandHome = exports2.resolvePaths = exports2.SpendStore = exports2.AuditLogStore = exports2.createSpendTrackerObserver = exports2.createAuditLogObserver = exports2.createLoggerObserver = exports2.SpendCapExceeded = exports2.ObserverChain = exports2.createCustomScriptExecutor = exports2.createWebhookExecutor = exports2.createHttpApiExecutor = exports2.createClaudeCodeExecutor = exports2.ExecutorRegistry = exports2.AgentCredentialRevokedError = exports2.runPreflight = exports2.renderLines = exports2.renderJson = exports2.printReport = exports2.hasFailure = exports2.createProbes = exports2.checksFor = exports2.PREFLIGHT_FLOORS = exports2.PREFLIGHT_EXIT_CODE = exports2.CHECKS = exports2.readResponseLatestVersion = exports2.checkResponseMinVersion = exports2.compareSemver = exports2.UpgradeRequiredError = exports2.LATEST_VERSION_HEADER = exports2.MIN_VERSION_HEADER = exports2.loadConfig = exports2.Shuttle = exports2.createCli = void 0;
25718
- exports2.revokePairing = exports2.listPairings = exports2.PairingConfigStore = void 0;
26554
+ exports2.scaffoldedConfigPath = exports2.planScaffold = exports2.DEFAULT_REFUSAL_MESSAGE = exports2.DEFAULT_MONTHLY_PERIOD = exports2.DEFAULT_DAILY_PERIOD = exports2.SUBSTRATE_BRAND = exports2.verificationJson = exports2.resolveEnvironment = exports2.renderVerificationLine = exports2.renderEnvironmentReport = exports2.isWorthReporting = exports2.hostOf = exports2.environmentReportJson = exports2.applyEnvironment = exports2.renderTemplate = exports2.expandHome = exports2.resolvePaths = exports2.SpendStore = exports2.AuditLogStore = exports2.createSpendTrackerObserver = exports2.createAuditLogObserver = exports2.createLoggerObserver = exports2.SpendCapExceeded = exports2.ObserverChain = exports2.createCustomScriptExecutor = exports2.createWebhookExecutor = exports2.createHttpApiExecutor = exports2.createClaudeCodeExecutor = exports2.ExecutorEnvironmentError = exports2.ExecutorRegistry = exports2.AgentCredentialRevokedError = exports2.runPreflight = exports2.renderLines = exports2.renderJson = exports2.printReport = exports2.hasFailure = exports2.createProbes = exports2.checksFor = exports2.PREFLIGHT_FLOORS = exports2.PREFLIGHT_EXIT_CODE = exports2.CHECKS = exports2.readResponseLatestVersion = exports2.checkResponseMinVersion = exports2.compareSemver = exports2.UpgradeRequiredError = exports2.LATEST_VERSION_HEADER = exports2.MIN_VERSION_HEADER = exports2.loadConfig = exports2.Shuttle = exports2.createCli = void 0;
26555
+ exports2.revokePairing = exports2.listPairings = exports2.PairingConfigStore = exports2.pair = void 0;
25719
26556
  var cli_1 = require_cli();
25720
26557
  Object.defineProperty(exports2, "createCli", { enumerable: true, get: function() {
25721
26558
  return cli_1.createCli;
@@ -25786,6 +26623,10 @@ var require_dist5 = __commonJS({
25786
26623
  Object.defineProperty(exports2, "ExecutorRegistry", { enumerable: true, get: function() {
25787
26624
  return registry_1.ExecutorRegistry;
25788
26625
  } });
26626
+ var types_1 = require_types2();
26627
+ Object.defineProperty(exports2, "ExecutorEnvironmentError", { enumerable: true, get: function() {
26628
+ return types_1.ExecutorEnvironmentError;
26629
+ } });
25789
26630
  var claude_code_1 = require_claude_code();
25790
26631
  Object.defineProperty(exports2, "createClaudeCodeExecutor", { enumerable: true, get: function() {
25791
26632
  return claude_code_1.createClaudeCodeExecutor;
@@ -25910,7 +26751,7 @@ var import_path = require("path");
25910
26751
  var import_promises = require("fs/promises");
25911
26752
  var import_yaml = __toESM(require_dist3());
25912
26753
  var import_shuttle = __toESM(require_dist5());
25913
- var buildVersion = true ? "0.30.8" : pkg.version;
26754
+ var buildVersion = true ? "0.30.10" : pkg.version;
25914
26755
  var sifterBrand = {
25915
26756
  product: {
25916
26757
  id: "sifter",