@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.
- package/README.md +13 -0
- package/bin.js +937 -96
- package/bin.js.map +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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
11156
|
-
|
|
11157
|
-
|
|
11158
|
-
|
|
11159
|
-
|
|
11160
|
-
|
|
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
|
|
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
|
-
|
|
20118
|
-
const
|
|
20119
|
-
|
|
20120
|
-
|
|
20121
|
-
|
|
20122
|
-
|
|
20123
|
-
|
|
20124
|
-
|
|
20125
|
-
|
|
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
|
|
20132
|
-
|
|
20133
|
-
|
|
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
|
-
|
|
20140
|
-
|
|
20141
|
-
|
|
20142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20178
|
-
|
|
20179
|
-
|
|
20180
|
-
|
|
20181
|
-
|
|
20182
|
-
|
|
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(
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
25038
|
-
(
|
|
25039
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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.
|
|
26754
|
+
var buildVersion = true ? "0.30.10" : pkg.version;
|
|
25914
26755
|
var sifterBrand = {
|
|
25915
26756
|
product: {
|
|
25916
26757
|
id: "sifter",
|