ever-works-node 0.2.3 → 0.2.4
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/cli.js +869 -661
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -3767,10 +3767,14 @@ var USER_SELECTABLE_WORK_KINDS = [
|
|
|
3767
3767
|
"repo",
|
|
3768
3768
|
"app"
|
|
3769
3769
|
];
|
|
3770
|
-
var WORK_KINDS = [...USER_SELECTABLE_WORK_KINDS, "company", "campaign", "default"];
|
|
3770
|
+
var WORK_KINDS = [...USER_SELECTABLE_WORK_KINDS, "company", "campaign", "project", "default"];
|
|
3771
3771
|
var WORK_KIND_SET = new Set(WORK_KINDS);
|
|
3772
3772
|
var GOAL_KINDS = ["metric", "delivery"];
|
|
3773
3773
|
var GOAL_KIND_SET = new Set(GOAL_KINDS);
|
|
3774
|
+
var RECORD_VISIBILITIES = ["private", "team", "organization"];
|
|
3775
|
+
var RECORD_VISIBILITY_SET = new Set(RECORD_VISIBILITIES);
|
|
3776
|
+
var RECORD_LIST_VIEWS = ["mine", "shared", "organization"];
|
|
3777
|
+
var RECORD_LIST_VIEW_SET = new Set(RECORD_LIST_VIEWS);
|
|
3774
3778
|
var APP_DEPENDENCY_BACKUP_OVERDUE_MS = 26 * 36e5;
|
|
3775
3779
|
var APP_DEPENDENCY_MANAGED = {
|
|
3776
3780
|
pgRoleConnectionLimit: 20,
|
|
@@ -7371,269 +7375,606 @@ function readPendingComputerSessions(payload) {
|
|
|
7371
7375
|
}
|
|
7372
7376
|
__name(readPendingComputerSessions, "readPendingComputerSessions");
|
|
7373
7377
|
|
|
7374
|
-
// src/core/
|
|
7375
|
-
var
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7378
|
+
// src/core/heartbeat.ts
|
|
7379
|
+
var systemScheduler = {
|
|
7380
|
+
setTimeout: /* @__PURE__ */ __name((callback, ms) => setTimeout(callback, ms), "setTimeout"),
|
|
7381
|
+
clearTimeout: /* @__PURE__ */ __name((handle) => clearTimeout(handle), "clearTimeout")
|
|
7382
|
+
};
|
|
7383
|
+
var OPTIONAL_DESCRIPTION_FIELD_TIERS = [
|
|
7384
|
+
// NEWEST first. Node lifecycle + remote node limits (self-build slices
|
|
7385
|
+
// AR, AS). Tiered (review): `cliVersions` goes out on EVERY beat of a
|
|
7386
|
+
// node with a pinned CLI, so an upgraded node talking to a platform that
|
|
7387
|
+
// predates it would otherwise drop the worker state and housekeeping
|
|
7388
|
+
// along with it — losing health signals that platform DOES understand,
|
|
7389
|
+
// for the life of the process. Stripping the newest tier first keeps them.
|
|
7390
|
+
["cliVersions", "maxConcurrentJobs", "maxCpuPercent", "maxMemoryMb"],
|
|
7391
|
+
// Fleet health signals (EW-776) and node housekeeping (EW-803).
|
|
7392
|
+
[
|
|
7393
|
+
"workerState",
|
|
7394
|
+
"workerStateReason",
|
|
7395
|
+
"minFreeDiskBytes",
|
|
7396
|
+
"workspaceCount",
|
|
7397
|
+
"workspaceBytes",
|
|
7398
|
+
"lastReclaimAt",
|
|
7399
|
+
"lastReclaimFreedBytes"
|
|
7400
|
+
]
|
|
7401
|
+
];
|
|
7402
|
+
function untilHeartbeatAccepted(loop, shouldWait = () => true) {
|
|
7403
|
+
const verdict = /* @__PURE__ */ __name((state) => {
|
|
7404
|
+
if (state.state === "connected") return true;
|
|
7405
|
+
if (state.state === "stopped" || !shouldWait()) return false;
|
|
7379
7406
|
return null;
|
|
7380
|
-
}
|
|
7381
|
-
const
|
|
7382
|
-
|
|
7407
|
+
}, "verdict");
|
|
7408
|
+
const now = verdict(loop.getState());
|
|
7409
|
+
if (now !== null) return Promise.resolve(now);
|
|
7410
|
+
return new Promise((resolve9) => {
|
|
7411
|
+
const unsubscribe = loop.onChange((state) => {
|
|
7412
|
+
const settled = verdict(state);
|
|
7413
|
+
if (settled === null) return;
|
|
7414
|
+
unsubscribe();
|
|
7415
|
+
resolve9(settled);
|
|
7416
|
+
});
|
|
7417
|
+
});
|
|
7383
7418
|
}
|
|
7384
|
-
__name(
|
|
7385
|
-
function
|
|
7386
|
-
|
|
7387
|
-
|
|
7419
|
+
__name(untilHeartbeatAccepted, "untilHeartbeatAccepted");
|
|
7420
|
+
function computeBackoffDelay(intervalMs, failures, maxMs) {
|
|
7421
|
+
if (failures <= 0) {
|
|
7422
|
+
return intervalMs;
|
|
7423
|
+
}
|
|
7424
|
+
const exponent = Math.min(failures, 30);
|
|
7425
|
+
const delay2 = intervalMs * 2 ** exponent;
|
|
7426
|
+
return Math.min(delay2, maxMs);
|
|
7388
7427
|
}
|
|
7389
|
-
__name(
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
if (pinnedTo === null) {
|
|
7394
|
-
return { url: configuredUrl, source: "config", configuredUrl, pinnedTo: null, mismatch: false };
|
|
7428
|
+
__name(computeBackoffDelay, "computeBackoffDelay");
|
|
7429
|
+
var HeartbeatLoop = class {
|
|
7430
|
+
static {
|
|
7431
|
+
__name(this, "HeartbeatLoop");
|
|
7395
7432
|
}
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7433
|
+
options;
|
|
7434
|
+
scheduler;
|
|
7435
|
+
now;
|
|
7436
|
+
listeners = /* @__PURE__ */ new Set();
|
|
7437
|
+
timer = null;
|
|
7438
|
+
running = false;
|
|
7439
|
+
inFlight = null;
|
|
7440
|
+
/**
|
|
7441
|
+
* How many optional-field TIERS (newest first) this platform has proven
|
|
7442
|
+
* it predates; from then on the beat leaves those tiers out. 0 = it
|
|
7443
|
+
* takes everything. Process-scoped on purpose — a platform upgrade is a
|
|
7444
|
+
* restart-shaped event for the node, and re-probing on every beat would
|
|
7445
|
+
* mean one wasted round trip per beat, forever, against an API that
|
|
7446
|
+
* will never accept them.
|
|
7447
|
+
*/
|
|
7448
|
+
strippedTiers = 0;
|
|
7449
|
+
state = {
|
|
7450
|
+
state: "idle",
|
|
7451
|
+
lastHeartbeatAt: null,
|
|
7452
|
+
consecutiveFailures: 0,
|
|
7453
|
+
nextAttemptInMs: null,
|
|
7454
|
+
lastError: null,
|
|
7455
|
+
lastErrorKind: null,
|
|
7456
|
+
node: null
|
|
7402
7457
|
};
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7458
|
+
constructor(options) {
|
|
7459
|
+
this.options = {
|
|
7460
|
+
...options,
|
|
7461
|
+
intervalMs: options.intervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS,
|
|
7462
|
+
maxBackoffMs: options.maxBackoffMs ?? MAX_HEARTBEAT_BACKOFF_MS
|
|
7463
|
+
};
|
|
7464
|
+
this.scheduler = options.scheduler ?? systemScheduler;
|
|
7465
|
+
this.now = options.now ?? (() => Date.now());
|
|
7466
|
+
this.options.logger?.protect(options.secret);
|
|
7408
7467
|
}
|
|
7409
|
-
|
|
7410
|
-
return
|
|
7468
|
+
getState() {
|
|
7469
|
+
return { ...this.state };
|
|
7411
7470
|
}
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
if (typeof stdout !== "string") return null;
|
|
7423
|
-
const match = /\b(\d+\.\d+(?:\.\d+)?(?:[-+][0-9A-Za-z.-]+)?)\b/.exec(stdout);
|
|
7424
|
-
return match ? match[1] : null;
|
|
7425
|
-
}
|
|
7426
|
-
__name(parseCliVersion, "parseCliVersion");
|
|
7427
|
-
async function detectAgentCliVersion(runner, candidates = AGENT_CLI_CANDIDATES) {
|
|
7428
|
-
for (const command of candidates) {
|
|
7429
|
-
try {
|
|
7430
|
-
const result = await runner.run(command, ["--version"]);
|
|
7431
|
-
if (result.code !== 0) continue;
|
|
7432
|
-
const version = parseCliVersion(`${result.stdout ?? ""}
|
|
7433
|
-
${result.stderr ?? ""}`);
|
|
7434
|
-
if (!version) continue;
|
|
7435
|
-
return `${command} ${version}`.slice(0, FLEET_MAX_CLI_VERSION_LENGTH);
|
|
7436
|
-
} catch {
|
|
7437
|
-
continue;
|
|
7471
|
+
onChange(listener) {
|
|
7472
|
+
this.listeners.add(listener);
|
|
7473
|
+
return () => {
|
|
7474
|
+
this.listeners.delete(listener);
|
|
7475
|
+
};
|
|
7476
|
+
}
|
|
7477
|
+
/** Start beating. Resolves once the FIRST beat has settled. */
|
|
7478
|
+
start() {
|
|
7479
|
+
if (this.running) {
|
|
7480
|
+
return this.inFlight ?? Promise.resolve();
|
|
7438
7481
|
}
|
|
7482
|
+
this.running = true;
|
|
7483
|
+
return this.tick();
|
|
7439
7484
|
}
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
const free = await io.freeBytes(path2);
|
|
7446
|
-
if (typeof free !== "number" || !Number.isFinite(free) || free < 0) return null;
|
|
7447
|
-
return Math.floor(free);
|
|
7448
|
-
} catch {
|
|
7449
|
-
return null;
|
|
7485
|
+
/** Stop beating and cancel any pending timer. */
|
|
7486
|
+
stop() {
|
|
7487
|
+
this.running = false;
|
|
7488
|
+
this.cancelTimer();
|
|
7489
|
+
this.patch({ state: "stopped", nextAttemptInMs: null });
|
|
7450
7490
|
}
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
function parseClaudeAuthStatus(stdout) {
|
|
7455
|
-
let parsed;
|
|
7456
|
-
try {
|
|
7457
|
-
parsed = JSON.parse(stdout.trim());
|
|
7458
|
-
} catch {
|
|
7459
|
-
return null;
|
|
7491
|
+
/** Await the in-flight beat, if any (test/shutdown helper). */
|
|
7492
|
+
async settled() {
|
|
7493
|
+
await this.inFlight;
|
|
7460
7494
|
}
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
7477
|
-
|
|
7478
|
-
|
|
7479
|
-
if (text.includes("logged in")) return "codex: logged in";
|
|
7480
|
-
return null;
|
|
7481
|
-
}
|
|
7482
|
-
__name(parseCodexLoginStatus, "parseCodexLoginStatus");
|
|
7483
|
-
function compactString(value) {
|
|
7484
|
-
if (typeof value !== "string") return null;
|
|
7485
|
-
const cleaned = value.replace(/[\u0000-\u001F\u007F]+/g, " ").trim();
|
|
7486
|
-
return cleaned ? cleaned : null;
|
|
7487
|
-
}
|
|
7488
|
-
__name(compactString, "compactString");
|
|
7489
|
-
async function detectModelIdentity(runner, paths = {}) {
|
|
7490
|
-
const candidates = [
|
|
7491
|
-
{ command: paths["claude-code"] || "claude", args: ["auth", "status", "--json"], parse: parseClaudeAuthStatus },
|
|
7492
|
-
{ command: paths.codex || "codex", args: ["login", "status"], parse: parseCodexLoginStatus }
|
|
7493
|
-
];
|
|
7494
|
-
for (const candidate of candidates) {
|
|
7495
|
+
/** Run one beat now. Exposed so the UI can offer "retry" and tests can step. */
|
|
7496
|
+
tick() {
|
|
7497
|
+
const run = this.runOnce().finally(() => {
|
|
7498
|
+
if (this.inFlight === run) {
|
|
7499
|
+
this.inFlight = null;
|
|
7500
|
+
}
|
|
7501
|
+
});
|
|
7502
|
+
this.inFlight = run;
|
|
7503
|
+
return run;
|
|
7504
|
+
}
|
|
7505
|
+
async runOnce() {
|
|
7506
|
+
if (!this.running) {
|
|
7507
|
+
return;
|
|
7508
|
+
}
|
|
7509
|
+
this.cancelTimer();
|
|
7510
|
+
if (this.state.state !== "connected") {
|
|
7511
|
+
this.patch({ state: "connecting" });
|
|
7512
|
+
}
|
|
7495
7513
|
try {
|
|
7496
|
-
const
|
|
7497
|
-
|
|
7498
|
-
|
|
7499
|
-
|
|
7500
|
-
${stderr}`);
|
|
7501
|
-
if (label) return label.slice(0, FLEET_MAX_MODEL_IDENTITY_LENGTH);
|
|
7502
|
-
} catch {
|
|
7503
|
-
continue;
|
|
7514
|
+
const description = await this.options.describe();
|
|
7515
|
+
this.onSuccess(await this.beat(description));
|
|
7516
|
+
} catch (error) {
|
|
7517
|
+
this.onFailure(error);
|
|
7504
7518
|
}
|
|
7519
|
+
this.scheduleNext();
|
|
7505
7520
|
}
|
|
7506
|
-
|
|
7507
|
-
|
|
7508
|
-
|
|
7509
|
-
|
|
7510
|
-
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
7527
|
-
|
|
7528
|
-
|
|
7529
|
-
|
|
7530
|
-
|
|
7531
|
-
|
|
7532
|
-
|
|
7533
|
-
|
|
7534
|
-
|
|
7535
|
-
|
|
7536
|
-
|
|
7537
|
-
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
|
|
7541
|
-
|
|
7542
|
-
|
|
7543
|
-
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
|
|
7521
|
+
/**
|
|
7522
|
+
* Send one beat, tolerating a platform that predates the optional
|
|
7523
|
+
* self-description fields (the worker state, EW-776; the housekeeping
|
|
7524
|
+
* report, EW-803).
|
|
7525
|
+
*
|
|
7526
|
+
* The API validates heartbeats with `whitelist + forbidNonWhitelisted`,
|
|
7527
|
+
* so a field an older build does not know is not ignored — it 400s the
|
|
7528
|
+
* whole request. A 400 is a FAILED beat, a failed beat backs off, and
|
|
7529
|
+
* enough of them sweep the node to `offline`. Which would mean shipping
|
|
7530
|
+
* a health-reporting feature whose failure mode is "every node in a
|
|
7531
|
+
* mixed-version fleet goes dark": the exact class of outage this slice
|
|
7532
|
+
* was written to end.
|
|
7533
|
+
*
|
|
7534
|
+
* So a 400 on a beat that CARRIED those fields is retried once,
|
|
7535
|
+
* immediately, without them. If that succeeds the platform is simply
|
|
7536
|
+
* older than this daemon; we latch, say so once, and keep reporting
|
|
7537
|
+
* liveness. The retry costs one request, once per process.
|
|
7538
|
+
*
|
|
7539
|
+
* Inferring "the field was rejected" from a bare 400 is deliberate and
|
|
7540
|
+
* has a cost worth stating: `FleetClient` never surfaces server bodies
|
|
7541
|
+
* (fixed client-authored messages only), so a 400 caused by something
|
|
7542
|
+
* else on such a beat also latches, and this node reports liveness only
|
|
7543
|
+
* until it restarts. That is the safe direction — the node stays
|
|
7544
|
+
* observable either way, and only the extra signal is lost.
|
|
7545
|
+
*/
|
|
7546
|
+
async beat(description) {
|
|
7547
|
+
const credential = { nodeId: this.options.nodeId, secret: this.options.secret };
|
|
7548
|
+
let sent = stripTiers(description, this.strippedTiers);
|
|
7549
|
+
try {
|
|
7550
|
+
return await this.options.client.heartbeat({ ...credential, ...sent });
|
|
7551
|
+
} catch (error) {
|
|
7552
|
+
if (!isFieldRejection(error)) throw error;
|
|
7553
|
+
let lastError = error;
|
|
7554
|
+
for (let tiers = this.strippedTiers + 1; tiers <= OPTIONAL_DESCRIPTION_FIELD_TIERS.length; tiers += 1) {
|
|
7555
|
+
const candidate = stripTiers(description, tiers);
|
|
7556
|
+
if (Object.keys(candidate).length === Object.keys(sent).length) continue;
|
|
7557
|
+
sent = candidate;
|
|
7558
|
+
try {
|
|
7559
|
+
const result = await this.options.client.heartbeat({ ...credential, ...candidate });
|
|
7560
|
+
this.strippedTiers = tiers;
|
|
7561
|
+
this.options.logger?.warn(
|
|
7562
|
+
tiers >= OPTIONAL_DESCRIPTION_FIELD_TIERS.length ? "Platform rejected the optional self-description fields (worker state, housekeeping); it predates them. Reporting liveness only until this node restarts." : "Platform rejected the newest optional self-description fields (pinned CLI versions, enforced limits); it predates them. Still reporting worker state and housekeeping; the newer fields stay off until this node restarts."
|
|
7563
|
+
);
|
|
7564
|
+
return result;
|
|
7565
|
+
} catch (retryError) {
|
|
7566
|
+
if (!isFieldRejection(retryError)) throw retryError;
|
|
7567
|
+
lastError = retryError;
|
|
7568
|
+
}
|
|
7569
|
+
}
|
|
7570
|
+
throw lastError;
|
|
7571
|
+
}
|
|
7548
7572
|
}
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
|
|
7552
|
-
|
|
7553
|
-
|
|
7573
|
+
onSuccess(result) {
|
|
7574
|
+
this.patch({
|
|
7575
|
+
state: "connected",
|
|
7576
|
+
lastHeartbeatAt: this.now(),
|
|
7577
|
+
consecutiveFailures: 0,
|
|
7578
|
+
lastError: null,
|
|
7579
|
+
lastErrorKind: null,
|
|
7580
|
+
node: result.node
|
|
7581
|
+
});
|
|
7582
|
+
if (this.options.onAccepted) {
|
|
7583
|
+
try {
|
|
7584
|
+
this.options.onAccepted(result);
|
|
7585
|
+
} catch {
|
|
7586
|
+
}
|
|
7587
|
+
}
|
|
7554
7588
|
}
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
|
|
7589
|
+
onFailure(error) {
|
|
7590
|
+
const kind = error instanceof FleetClientError ? error.kind : "network";
|
|
7591
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
7592
|
+
const message = this.options.logger?.redact(raw) ?? raw;
|
|
7593
|
+
const failures = this.state.consecutiveFailures + 1;
|
|
7594
|
+
this.patch({
|
|
7595
|
+
// `unauthorized` is sticky and visible: the operator has to act
|
|
7596
|
+
// (re-enable the node in Fleet, or re-enroll). We keep retrying at
|
|
7597
|
+
// the backoff ceiling so a re-enabled node recovers on its own.
|
|
7598
|
+
state: kind === "unauthorized" ? "unauthorized" : "retrying",
|
|
7599
|
+
consecutiveFailures: failures,
|
|
7600
|
+
lastError: message,
|
|
7601
|
+
lastErrorKind: kind
|
|
7602
|
+
});
|
|
7603
|
+
this.options.logger?.warn(`Heartbeat failed (attempt ${failures}): ${message}`);
|
|
7558
7604
|
}
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
|
|
7563
|
-
|
|
7564
|
-
|
|
7565
|
-
|
|
7566
|
-
|
|
7567
|
-
|
|
7605
|
+
scheduleNext() {
|
|
7606
|
+
if (!this.running) {
|
|
7607
|
+
return;
|
|
7608
|
+
}
|
|
7609
|
+
const delay2 = computeBackoffDelay(
|
|
7610
|
+
this.options.intervalMs,
|
|
7611
|
+
this.state.consecutiveFailures,
|
|
7612
|
+
this.options.maxBackoffMs
|
|
7613
|
+
);
|
|
7614
|
+
this.patch({ nextAttemptInMs: delay2 });
|
|
7615
|
+
this.timer = this.scheduler.setTimeout(() => {
|
|
7616
|
+
this.timer = null;
|
|
7617
|
+
void this.tick();
|
|
7618
|
+
}, delay2);
|
|
7568
7619
|
}
|
|
7569
|
-
|
|
7570
|
-
|
|
7620
|
+
cancelTimer() {
|
|
7621
|
+
if (this.timer !== null) {
|
|
7622
|
+
this.scheduler.clearTimeout(this.timer);
|
|
7623
|
+
this.timer = null;
|
|
7624
|
+
}
|
|
7571
7625
|
}
|
|
7572
|
-
|
|
7573
|
-
|
|
7574
|
-
|
|
7626
|
+
patch(next) {
|
|
7627
|
+
this.state = { ...this.state, ...next };
|
|
7628
|
+
const snapshot = this.getState();
|
|
7629
|
+
for (const listener of this.listeners) {
|
|
7630
|
+
listener(snapshot);
|
|
7631
|
+
}
|
|
7575
7632
|
}
|
|
7576
|
-
|
|
7577
|
-
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
|
|
7633
|
+
};
|
|
7634
|
+
function stripTiers(description, tiers) {
|
|
7635
|
+
const out = {};
|
|
7636
|
+
const dropped = new Set(OPTIONAL_DESCRIPTION_FIELD_TIERS.slice(0, tiers).flat());
|
|
7637
|
+
for (const [key, value] of Object.entries(description)) {
|
|
7638
|
+
if (value === void 0 || dropped.has(key)) continue;
|
|
7639
|
+
out[key] = value;
|
|
7582
7640
|
}
|
|
7583
|
-
return
|
|
7641
|
+
return out;
|
|
7584
7642
|
}
|
|
7585
|
-
__name(
|
|
7586
|
-
function
|
|
7587
|
-
|
|
7588
|
-
throw new ModelCliCommandError("MCP bridge URL is not a loopback URL this node produced");
|
|
7589
|
-
}
|
|
7590
|
-
return url;
|
|
7643
|
+
__name(stripTiers, "stripTiers");
|
|
7644
|
+
function isFieldRejection(error) {
|
|
7645
|
+
return error instanceof FleetClientError && error.kind === "invalid-request" && error.status === 400;
|
|
7591
7646
|
}
|
|
7592
|
-
__name(
|
|
7593
|
-
|
|
7594
|
-
|
|
7595
|
-
|
|
7596
|
-
|
|
7597
|
-
|
|
7598
|
-
|
|
7647
|
+
__name(isFieldRejection, "isFieldRejection");
|
|
7648
|
+
|
|
7649
|
+
// src/core/api-base.ts
|
|
7650
|
+
var API_URL_ENV = "EVER_WORKS_NODE_API_URL";
|
|
7651
|
+
function rawPin(env) {
|
|
7652
|
+
const value = env[API_URL_ENV];
|
|
7653
|
+
if (typeof value !== "string") {
|
|
7654
|
+
return null;
|
|
7599
7655
|
}
|
|
7600
|
-
|
|
7656
|
+
const trimmed = value.trim();
|
|
7657
|
+
return trimmed ? trimmed : null;
|
|
7601
7658
|
}
|
|
7602
|
-
__name(
|
|
7603
|
-
|
|
7604
|
-
|
|
7605
|
-
|
|
7606
|
-
}
|
|
7607
|
-
|
|
7608
|
-
|
|
7609
|
-
const
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7659
|
+
__name(rawPin, "rawPin");
|
|
7660
|
+
function readApiUrlPin(env = process.env) {
|
|
7661
|
+
const raw = rawPin(env);
|
|
7662
|
+
return raw === null ? null : normalizeApiUrl(raw);
|
|
7663
|
+
}
|
|
7664
|
+
__name(readApiUrlPin, "readApiUrlPin");
|
|
7665
|
+
function resolveApiBase(config, env = process.env) {
|
|
7666
|
+
const configuredUrl = config.apiUrl;
|
|
7667
|
+
const pinnedTo = readApiUrlPin(env);
|
|
7668
|
+
if (pinnedTo === null) {
|
|
7669
|
+
return { url: configuredUrl, source: "config", configuredUrl, pinnedTo: null, mismatch: false };
|
|
7613
7670
|
}
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
|
|
7617
|
-
|
|
7671
|
+
return {
|
|
7672
|
+
url: pinnedTo,
|
|
7673
|
+
source: "pin",
|
|
7674
|
+
configuredUrl,
|
|
7675
|
+
pinnedTo,
|
|
7676
|
+
mismatch: pinnedTo !== configuredUrl
|
|
7677
|
+
};
|
|
7618
7678
|
}
|
|
7619
|
-
__name(
|
|
7620
|
-
function
|
|
7621
|
-
if (
|
|
7622
|
-
|
|
7623
|
-
throw new ModelCliCommandError(`Provider '${String(provider)}' cannot resume a CLI session on the fleet`);
|
|
7679
|
+
__name(resolveApiBase, "resolveApiBase");
|
|
7680
|
+
function describeApiBase(base) {
|
|
7681
|
+
if (base.source === "config") {
|
|
7682
|
+
return `${base.url} (from the enrolled config)`;
|
|
7624
7683
|
}
|
|
7625
|
-
if (!
|
|
7626
|
-
|
|
7684
|
+
if (!base.mismatch) {
|
|
7685
|
+
return `${base.url} (pinned via ${API_URL_ENV}; matches the enrolled origin)`;
|
|
7627
7686
|
}
|
|
7628
|
-
return
|
|
7687
|
+
return `${base.url} (PINNED via ${API_URL_ENV}) \u2014 enrolled against ${base.configuredUrl}. The pin does not match the enrolled origin: this node has no credential on the pinned platform, so every call will be refused with 401 until the pin is corrected or the node is re-enrolled.`;
|
|
7629
7688
|
}
|
|
7630
|
-
__name(
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
|
|
7634
|
-
|
|
7635
|
-
|
|
7636
|
-
|
|
7689
|
+
__name(describeApiBase, "describeApiBase");
|
|
7690
|
+
|
|
7691
|
+
// src/core/auth-client.ts
|
|
7692
|
+
var EVER_ID_DEVICE_SCOPE = ["openid", "email", EVER_ID_SCOPES.SESSION_EXCHANGE].join(" ");
|
|
7693
|
+
|
|
7694
|
+
// src/core/telemetry-probe.ts
|
|
7695
|
+
var AGENT_CLI_CANDIDATES = ["claude", "codex", "gemini", "opencode"];
|
|
7696
|
+
function parseCliVersion(stdout) {
|
|
7697
|
+
if (typeof stdout !== "string") return null;
|
|
7698
|
+
const match = /\b(\d+\.\d+(?:\.\d+)?(?:[-+][0-9A-Za-z.-]+)?)\b/.exec(stdout);
|
|
7699
|
+
return match ? match[1] : null;
|
|
7700
|
+
}
|
|
7701
|
+
__name(parseCliVersion, "parseCliVersion");
|
|
7702
|
+
async function detectAgentCliVersion(runner, candidates = AGENT_CLI_CANDIDATES) {
|
|
7703
|
+
for (const command of candidates) {
|
|
7704
|
+
try {
|
|
7705
|
+
const result = await runner.run(command, ["--version"]);
|
|
7706
|
+
if (result.code !== 0) continue;
|
|
7707
|
+
const version = parseCliVersion(`${result.stdout ?? ""}
|
|
7708
|
+
${result.stderr ?? ""}`);
|
|
7709
|
+
if (!version) continue;
|
|
7710
|
+
return `${command} ${version}`.slice(0, FLEET_MAX_CLI_VERSION_LENGTH);
|
|
7711
|
+
} catch {
|
|
7712
|
+
continue;
|
|
7713
|
+
}
|
|
7714
|
+
}
|
|
7715
|
+
return null;
|
|
7716
|
+
}
|
|
7717
|
+
__name(detectAgentCliVersion, "detectAgentCliVersion");
|
|
7718
|
+
async function detectDiskFreeBytes(io, path2) {
|
|
7719
|
+
try {
|
|
7720
|
+
const free = await io.freeBytes(path2);
|
|
7721
|
+
if (typeof free !== "number" || !Number.isFinite(free) || free < 0) return null;
|
|
7722
|
+
return Math.floor(free);
|
|
7723
|
+
} catch {
|
|
7724
|
+
return null;
|
|
7725
|
+
}
|
|
7726
|
+
}
|
|
7727
|
+
__name(detectDiskFreeBytes, "detectDiskFreeBytes");
|
|
7728
|
+
var MODEL_IDENTITY_CACHE_TTL_MS = 5 * 6e4;
|
|
7729
|
+
function parseClaudeAuthStatus(stdout) {
|
|
7730
|
+
let parsed;
|
|
7731
|
+
try {
|
|
7732
|
+
parsed = JSON.parse(stdout.trim());
|
|
7733
|
+
} catch {
|
|
7734
|
+
return null;
|
|
7735
|
+
}
|
|
7736
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
7737
|
+
const status = parsed;
|
|
7738
|
+
if (status.loggedIn !== true) return "claude-code: not logged in";
|
|
7739
|
+
const email = compactString(status.email);
|
|
7740
|
+
const org = compactString(status.orgName);
|
|
7741
|
+
const plan = compactString(status.subscriptionType);
|
|
7742
|
+
const method = compactString(status.authMethod);
|
|
7743
|
+
const who = email ?? (method ? `${method} login` : "logged in");
|
|
7744
|
+
const details = [org, plan].filter((part) => part !== null);
|
|
7745
|
+
return `claude-code: ${who}${details.length > 0 ? ` (${details.join(", ")})` : ""}`;
|
|
7746
|
+
}
|
|
7747
|
+
__name(parseClaudeAuthStatus, "parseClaudeAuthStatus");
|
|
7748
|
+
function parseCodexLoginStatus(stdout) {
|
|
7749
|
+
const text = stdout.trim().toLowerCase();
|
|
7750
|
+
if (!text) return null;
|
|
7751
|
+
if (text.includes("not logged in")) return "codex: not logged in";
|
|
7752
|
+
if (text.includes("chatgpt")) return "codex: chatgpt";
|
|
7753
|
+
if (text.includes("api key")) return "codex: api-key";
|
|
7754
|
+
if (text.includes("logged in")) return "codex: logged in";
|
|
7755
|
+
return null;
|
|
7756
|
+
}
|
|
7757
|
+
__name(parseCodexLoginStatus, "parseCodexLoginStatus");
|
|
7758
|
+
function compactString(value) {
|
|
7759
|
+
if (typeof value !== "string") return null;
|
|
7760
|
+
const cleaned = value.replace(/[\u0000-\u001F\u007F]+/g, " ").trim();
|
|
7761
|
+
return cleaned ? cleaned : null;
|
|
7762
|
+
}
|
|
7763
|
+
__name(compactString, "compactString");
|
|
7764
|
+
async function detectModelIdentity(runner, paths = {}) {
|
|
7765
|
+
const candidates = [
|
|
7766
|
+
{ command: paths["claude-code"] || "claude", args: ["auth", "status", "--json"], parse: parseClaudeAuthStatus },
|
|
7767
|
+
{ command: paths.codex || "codex", args: ["login", "status"], parse: parseCodexLoginStatus }
|
|
7768
|
+
];
|
|
7769
|
+
for (const candidate of candidates) {
|
|
7770
|
+
try {
|
|
7771
|
+
const result = await runner.run(candidate.command, candidate.args);
|
|
7772
|
+
const stdout = result.stdout ?? "";
|
|
7773
|
+
const stderr = result.stderr ?? "";
|
|
7774
|
+
const label = candidate.parse(stdout) ?? candidate.parse(`${stdout}
|
|
7775
|
+
${stderr}`);
|
|
7776
|
+
if (label) return label.slice(0, FLEET_MAX_MODEL_IDENTITY_LENGTH);
|
|
7777
|
+
} catch {
|
|
7778
|
+
continue;
|
|
7779
|
+
}
|
|
7780
|
+
}
|
|
7781
|
+
return null;
|
|
7782
|
+
}
|
|
7783
|
+
__name(detectModelIdentity, "detectModelIdentity");
|
|
7784
|
+
function cacheProbe(probe, ttlMs = MODEL_IDENTITY_CACHE_TTL_MS, now = Date.now) {
|
|
7785
|
+
let value = null;
|
|
7786
|
+
let expiresAt = Number.NEGATIVE_INFINITY;
|
|
7787
|
+
let inFlight = null;
|
|
7788
|
+
return async () => {
|
|
7789
|
+
if (now() < expiresAt) return value;
|
|
7790
|
+
if (inFlight) return inFlight;
|
|
7791
|
+
inFlight = probe().then((fresh) => {
|
|
7792
|
+
value = fresh;
|
|
7793
|
+
expiresAt = now() + ttlMs;
|
|
7794
|
+
return fresh;
|
|
7795
|
+
}).finally(() => {
|
|
7796
|
+
inFlight = null;
|
|
7797
|
+
});
|
|
7798
|
+
return inFlight;
|
|
7799
|
+
};
|
|
7800
|
+
}
|
|
7801
|
+
__name(cacheProbe, "cacheProbe");
|
|
7802
|
+
|
|
7803
|
+
// src/core/executors/model-cli.ts
|
|
7804
|
+
var MODEL_CLI_EMPTY_MCP_CONFIG = `${JSON.stringify({ mcpServers: {} }, null, 2)}
|
|
7805
|
+
`;
|
|
7806
|
+
var CODEX_SETTING_VALUE = /^[A-Za-z0-9._-]{1,64}$/;
|
|
7807
|
+
var TOML_STRING_ASSIGNMENT = /^([A-Za-z0-9_-]+)[ \t]*=[ \t]*(?:"([^"\\]*)"|'([^']*)')(?:[ \t]+#.*|#.*)?$/;
|
|
7808
|
+
var TOML_MULTILINE_VALUE_START = /^[^=#]*=[ \t]*("""|''')/;
|
|
7809
|
+
function readCodexUserSettings(configToml) {
|
|
7810
|
+
const settings = {};
|
|
7811
|
+
if (typeof configToml !== "string" || configToml.length === 0) return settings;
|
|
7812
|
+
let table = "";
|
|
7813
|
+
let openMultiline = null;
|
|
7814
|
+
for (const raw of configToml.split(/\r?\n/)) {
|
|
7815
|
+
if (openMultiline !== null) {
|
|
7816
|
+
if (indexOfTomlClosingDelimiter(raw, openMultiline) !== -1) openMultiline = null;
|
|
7817
|
+
continue;
|
|
7818
|
+
}
|
|
7819
|
+
const line = raw.trim();
|
|
7820
|
+
const opening = TOML_MULTILINE_VALUE_START.exec(line);
|
|
7821
|
+
if (opening) {
|
|
7822
|
+
const delimiter = opening[1];
|
|
7823
|
+
if (indexOfTomlClosingDelimiter(line.slice(opening[0].length), delimiter) === -1) {
|
|
7824
|
+
openMultiline = delimiter;
|
|
7825
|
+
continue;
|
|
7826
|
+
}
|
|
7827
|
+
}
|
|
7828
|
+
if (line.startsWith("[")) {
|
|
7829
|
+
if (line.startsWith("[[")) {
|
|
7830
|
+
table = "\0array";
|
|
7831
|
+
} else {
|
|
7832
|
+
const close = line.indexOf("]");
|
|
7833
|
+
table = close > 1 ? line.slice(1, close).trim() : "\0unknown";
|
|
7834
|
+
}
|
|
7835
|
+
continue;
|
|
7836
|
+
}
|
|
7837
|
+
const assignment = TOML_STRING_ASSIGNMENT.exec(line);
|
|
7838
|
+
if (!assignment) continue;
|
|
7839
|
+
const key = assignment[1];
|
|
7840
|
+
const value = assignment[2] ?? assignment[3] ?? "";
|
|
7841
|
+
if (!CODEX_SETTING_VALUE.test(value)) continue;
|
|
7842
|
+
if (table === "" && key === "model" && settings.model === void 0) settings.model = value;
|
|
7843
|
+
else if (table === "" && key === "model_reasoning_effort" && settings.reasoningEffort === void 0) {
|
|
7844
|
+
settings.reasoningEffort = value;
|
|
7845
|
+
} else if (table === "windows" && key === "sandbox" && settings.windowsSandbox === void 0) {
|
|
7846
|
+
settings.windowsSandbox = value;
|
|
7847
|
+
}
|
|
7848
|
+
}
|
|
7849
|
+
return settings;
|
|
7850
|
+
}
|
|
7851
|
+
__name(readCodexUserSettings, "readCodexUserSettings");
|
|
7852
|
+
function indexOfTomlClosingDelimiter(text, delimiter) {
|
|
7853
|
+
let from = 0;
|
|
7854
|
+
for (; ; ) {
|
|
7855
|
+
const at = text.indexOf(delimiter, from);
|
|
7856
|
+
if (at === -1 || delimiter === "'''") return at;
|
|
7857
|
+
let backslashes = 0;
|
|
7858
|
+
for (let i = at - 1; i >= 0 && text[i] === "\\"; i -= 1) backslashes += 1;
|
|
7859
|
+
if (backslashes % 2 === 0) return at;
|
|
7860
|
+
from = at + 1;
|
|
7861
|
+
}
|
|
7862
|
+
}
|
|
7863
|
+
__name(indexOfTomlClosingDelimiter, "indexOfTomlClosingDelimiter");
|
|
7864
|
+
function assertCodexSetting(name, value) {
|
|
7865
|
+
if (value === void 0) return void 0;
|
|
7866
|
+
if (!CODEX_SETTING_VALUE.test(value)) {
|
|
7867
|
+
throw new ModelCliCommandError(`Refusing to forward the Codex setting '${name}': not a bare identifier`);
|
|
7868
|
+
}
|
|
7869
|
+
return value;
|
|
7870
|
+
}
|
|
7871
|
+
__name(assertCodexSetting, "assertCodexSetting");
|
|
7872
|
+
function modelCliMcpIsolationEnv(provider) {
|
|
7873
|
+
return provider === "claude-code" ? { ENABLE_CLAUDEAI_MCP_SERVERS: "false" } : null;
|
|
7874
|
+
}
|
|
7875
|
+
__name(modelCliMcpIsolationEnv, "modelCliMcpIsolationEnv");
|
|
7876
|
+
function claudeManagedMcpConfigPath(platform = process.platform) {
|
|
7877
|
+
if (platform === "win32") return "C:\\Program Files\\ClaudeCode\\managed-mcp.json";
|
|
7878
|
+
if (platform === "darwin") return "/Library/Application Support/ClaudeCode/managed-mcp.json";
|
|
7879
|
+
return "/etc/claude-code/managed-mcp.json";
|
|
7880
|
+
}
|
|
7881
|
+
__name(claudeManagedMcpConfigPath, "claudeManagedMcpConfigPath");
|
|
7882
|
+
function claudeManagedMcpConfigInEffect(raw) {
|
|
7883
|
+
if (typeof raw !== "string" || !raw.trim()) return false;
|
|
7884
|
+
try {
|
|
7885
|
+
const parsed = JSON.parse(raw);
|
|
7886
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed);
|
|
7887
|
+
} catch {
|
|
7888
|
+
return false;
|
|
7889
|
+
}
|
|
7890
|
+
}
|
|
7891
|
+
__name(claudeManagedMcpConfigInEffect, "claudeManagedMcpConfigInEffect");
|
|
7892
|
+
var ModelCliCommandError = class extends Error {
|
|
7893
|
+
static {
|
|
7894
|
+
__name(this, "ModelCliCommandError");
|
|
7895
|
+
}
|
|
7896
|
+
constructor(message) {
|
|
7897
|
+
super(message);
|
|
7898
|
+
this.name = "ModelCliCommandError";
|
|
7899
|
+
}
|
|
7900
|
+
};
|
|
7901
|
+
var MODEL_CLI_STEP_ID = "model";
|
|
7902
|
+
var MODEL_CLI_OUTPUT_TAIL_BYTES = 8 * 1024;
|
|
7903
|
+
var MODEL_CLI_MAX_OUTPUT_BYTES = 8 * 1024 * 1024;
|
|
7904
|
+
var ABSOLUTE_WIN32 = /^[A-Za-z]:[\\/]/;
|
|
7905
|
+
var UNSAFE_PATH_CHARS = /[\0\r\n"'`$&|;<>%!^]/;
|
|
7906
|
+
function quoteShellPath(path2, platform = process.platform) {
|
|
7907
|
+
if (typeof path2 !== "string" || !path2.trim()) {
|
|
7908
|
+
throw new ModelCliCommandError("Path must not be empty");
|
|
7909
|
+
}
|
|
7910
|
+
if (UNSAFE_PATH_CHARS.test(path2)) {
|
|
7911
|
+
throw new ModelCliCommandError("Path contains characters the shell could interpret");
|
|
7912
|
+
}
|
|
7913
|
+
const absolute = platform === "win32" ? ABSOLUTE_WIN32.test(path2) || path2.startsWith("\\\\") : path2.startsWith("/");
|
|
7914
|
+
if (!absolute) {
|
|
7915
|
+
throw new ModelCliCommandError("Path must be absolute");
|
|
7916
|
+
}
|
|
7917
|
+
return `"${path2}"`;
|
|
7918
|
+
}
|
|
7919
|
+
__name(quoteShellPath, "quoteShellPath");
|
|
7920
|
+
function assertMcpServerName(name) {
|
|
7921
|
+
if (typeof name !== "string" || !MCP_SERVER_NAME_PATTERN.test(name)) {
|
|
7922
|
+
throw new ModelCliCommandError("MCP server name is not an opaque identifier");
|
|
7923
|
+
}
|
|
7924
|
+
return name;
|
|
7925
|
+
}
|
|
7926
|
+
__name(assertMcpServerName, "assertMcpServerName");
|
|
7927
|
+
function assertLoopbackUrl(url) {
|
|
7928
|
+
if (typeof url !== "string" || !MCP_LOOPBACK_URL_PATTERN.test(url)) {
|
|
7929
|
+
throw new ModelCliCommandError("MCP bridge URL is not a loopback URL this node produced");
|
|
7930
|
+
}
|
|
7931
|
+
return url;
|
|
7932
|
+
}
|
|
7933
|
+
__name(assertLoopbackUrl, "assertLoopbackUrl");
|
|
7934
|
+
var MCP_SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
7935
|
+
var MCP_LOOPBACK_URL_PATTERN = /^http:\/\/127\.0\.0\.1:\d{1,5}\/mcp\/[a-f0-9]{8,64}$/;
|
|
7936
|
+
function assertModelId(model) {
|
|
7937
|
+
if (model === void 0) return null;
|
|
7938
|
+
if (!FLEET_AGENT_EXECUTION_MODEL_PATTERN.test(model)) {
|
|
7939
|
+
throw new ModelCliCommandError("Model id is not an opaque identifier");
|
|
7940
|
+
}
|
|
7941
|
+
return model;
|
|
7942
|
+
}
|
|
7943
|
+
__name(assertModelId, "assertModelId");
|
|
7944
|
+
var MODEL_CLI_OPTIONAL_FLAGS = {
|
|
7945
|
+
"claude-code": ["--effort", "--max-budget-usd"],
|
|
7946
|
+
codex: []
|
|
7947
|
+
};
|
|
7948
|
+
function unsupportedOptionalModelCliFlags(execution, supportedFlags) {
|
|
7949
|
+
if (!supportedFlags) return [];
|
|
7950
|
+
const wanted = [];
|
|
7951
|
+
if (execution.provider === "claude-code") {
|
|
7952
|
+
if (execution.effort) wanted.push("--effort");
|
|
7953
|
+
if (execution.maxBudgetUsd !== void 0) wanted.push("--max-budget-usd");
|
|
7954
|
+
}
|
|
7955
|
+
const droppable = new Set(
|
|
7956
|
+
MODEL_CLI_OPTIONAL_FLAGS[execution.provider] ?? []
|
|
7957
|
+
);
|
|
7958
|
+
return wanted.filter((flag) => droppable.has(flag) && !supportedFlags.has(flag));
|
|
7959
|
+
}
|
|
7960
|
+
__name(unsupportedOptionalModelCliFlags, "unsupportedOptionalModelCliFlags");
|
|
7961
|
+
function assertResumableSession(provider, sessionId) {
|
|
7962
|
+
if (sessionId === void 0) return null;
|
|
7963
|
+
if (!fleetAgentExecutionProviderSupportsSessionResume(provider)) {
|
|
7964
|
+
throw new ModelCliCommandError(`Provider '${String(provider)}' cannot resume a CLI session on the fleet`);
|
|
7965
|
+
}
|
|
7966
|
+
if (!isFleetAgentModelSessionId(sessionId)) {
|
|
7967
|
+
throw new ModelCliCommandError("CLI session id is not an opaque identifier");
|
|
7968
|
+
}
|
|
7969
|
+
return sessionId;
|
|
7970
|
+
}
|
|
7971
|
+
__name(assertResumableSession, "assertResumableSession");
|
|
7972
|
+
function formatBudget(value) {
|
|
7973
|
+
if (value === void 0) return null;
|
|
7974
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
7975
|
+
throw new ModelCliCommandError("maxBudgetUsd must be a positive number");
|
|
7976
|
+
}
|
|
7977
|
+
return String(Math.round(value * 100) / 100);
|
|
7637
7978
|
}
|
|
7638
7979
|
__name(formatBudget, "formatBudget");
|
|
7639
7980
|
function buildModelCliCommand(input) {
|
|
@@ -7692,7 +8033,21 @@ function buildModelCliCommand(input) {
|
|
|
7692
8033
|
if (roots.length > 0) args.push("--add-dir", ...roots);
|
|
7693
8034
|
} else if (execution.provider === "codex") {
|
|
7694
8035
|
const sandbox = permissionMode === "plan" ? "read-only" : "workspace-write";
|
|
7695
|
-
args.push(
|
|
8036
|
+
args.push(
|
|
8037
|
+
"exec",
|
|
8038
|
+
"--ignore-user-config",
|
|
8039
|
+
"--json",
|
|
8040
|
+
"--sandbox",
|
|
8041
|
+
sandbox,
|
|
8042
|
+
"-C",
|
|
8043
|
+
quoteShellPath(input.workspacePath, platform)
|
|
8044
|
+
);
|
|
8045
|
+
const kept = input.codexUserSettings ?? {};
|
|
8046
|
+
const windowsSandbox = assertCodexSetting("windows.sandbox", kept.windowsSandbox);
|
|
8047
|
+
const reasoningEffort = assertCodexSetting("model_reasoning_effort", kept.reasoningEffort);
|
|
8048
|
+
const ownerModel = assertCodexSetting("model", kept.model);
|
|
8049
|
+
if (windowsSandbox) args.push("-c", `windows.sandbox=${windowsSandbox}`);
|
|
8050
|
+
if (reasoningEffort) args.push("-c", `model_reasoning_effort=${reasoningEffort}`);
|
|
7696
8051
|
if (sandbox === "workspace-write") {
|
|
7697
8052
|
for (const root of grantedRoots(writableMounts)) args.push("--add-dir", root);
|
|
7698
8053
|
}
|
|
@@ -7702,7 +8057,8 @@ function buildModelCliCommand(input) {
|
|
|
7702
8057
|
`mcp_servers.${assertMcpServerName(mcp.serverName)}.url=${assertLoopbackUrl(mcp.serverUrl)}`
|
|
7703
8058
|
);
|
|
7704
8059
|
}
|
|
7705
|
-
|
|
8060
|
+
const codexModel = model ?? ownerModel;
|
|
8061
|
+
if (codexModel) args.push("-m", codexModel);
|
|
7706
8062
|
if (execution.skipPermissions === true) args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
7707
8063
|
args.push("-");
|
|
7708
8064
|
} else {
|
|
@@ -7725,7 +8081,16 @@ function assertMountGrantsInCommand(input) {
|
|
|
7725
8081
|
}
|
|
7726
8082
|
__name(assertMountGrantsInCommand, "assertMountGrantsInCommand");
|
|
7727
8083
|
var STRICT_MCP_CONFIG_IN_COMMAND = / --mcp-config "[^"]+" --strict-mcp-config /;
|
|
8084
|
+
var CODEX_IGNORE_USER_CONFIG_IN_COMMAND = / exec --ignore-user-config /;
|
|
7728
8085
|
function assertMcpIsolationInCommand(input) {
|
|
8086
|
+
if (input.execution.provider === "codex") {
|
|
8087
|
+
if (!CODEX_IGNORE_USER_CONFIG_IN_COMMAND.test(input.command)) {
|
|
8088
|
+
throw new ModelCliCommandError(
|
|
8089
|
+
"The codex command line does not carry exec --ignore-user-config, so the run would load the machine owner's own config.toml: its MCP servers, plugins and notify hook"
|
|
8090
|
+
);
|
|
8091
|
+
}
|
|
8092
|
+
return;
|
|
8093
|
+
}
|
|
7729
8094
|
if (input.execution.provider !== "claude-code") return;
|
|
7730
8095
|
if (!STRICT_MCP_CONFIG_IN_COMMAND.test(input.command)) {
|
|
7731
8096
|
throw new ModelCliCommandError(
|
|
@@ -8053,436 +8418,183 @@ var MODEL_CLI_REQUIRED_FLAGS = {
|
|
|
8053
8418
|
// bridge's. A CLI without them cannot run fleet work at all, so `doctor`
|
|
8054
8419
|
// must say INCOMPATIBLE, not "runs that need them will fail".
|
|
8055
8420
|
"claude-code": ["-p", "--output-format", "--permission-mode", "--mcp-config", "--strict-mcp-config"],
|
|
8056
|
-
codex: ["--json", "--sandbox", "-C"]
|
|
8421
|
+
codex: ["--json", "--sandbox", "-C", "--ignore-user-config"]
|
|
8057
8422
|
};
|
|
8058
8423
|
var MODEL_CLI_CONDITIONAL_FLAGS = {
|
|
8059
|
-
"claude-code": [
|
|
8060
|
-
"--model",
|
|
8061
|
-
"--add-dir",
|
|
8062
|
-
"--allowedTools",
|
|
8063
|
-
"--dangerously-skip-permissions",
|
|
8064
|
-
"--verbose",
|
|
8065
|
-
"--resume",
|
|
8066
|
-
"--fork-session"
|
|
8067
|
-
],
|
|
8068
|
-
codex: ["--add-dir", "-c", "-m", "--dangerously-bypass-approvals-and-sandbox"]
|
|
8069
|
-
};
|
|
8070
|
-
var MODEL_CLI_COMPAT_UNSTAMPED_TTL_MS = 10 * 6e4;
|
|
8071
|
-
var MODEL_CLI_COMPAT_STAMPED_MAX_AGE_MS = 60 * 6e4;
|
|
8072
|
-
var MODEL_CLI_COMPAT_INCOMPLETE_RETRY_MS = 6e4;
|
|
8073
|
-
var ModelCliCompatibilityProbe = class {
|
|
8074
|
-
constructor(options) {
|
|
8075
|
-
this.options = options;
|
|
8076
|
-
this.now = options.now ?? (() => Date.now());
|
|
8077
|
-
this.unstampedTtlMs = options.unstampedTtlMs ?? MODEL_CLI_COMPAT_UNSTAMPED_TTL_MS;
|
|
8078
|
-
this.incompleteRetryMs = options.incompleteRetryMs ?? MODEL_CLI_COMPAT_INCOMPLETE_RETRY_MS;
|
|
8079
|
-
this.stampedMaxAgeMs = options.stampedMaxAgeMs ?? MODEL_CLI_COMPAT_STAMPED_MAX_AGE_MS;
|
|
8080
|
-
}
|
|
8081
|
-
options;
|
|
8082
|
-
static {
|
|
8083
|
-
__name(this, "ModelCliCompatibilityProbe");
|
|
8084
|
-
}
|
|
8085
|
-
cache = /* @__PURE__ */ new Map();
|
|
8086
|
-
inFlight = /* @__PURE__ */ new Map();
|
|
8087
|
-
now;
|
|
8088
|
-
unstampedTtlMs;
|
|
8089
|
-
incompleteRetryMs;
|
|
8090
|
-
stampedMaxAgeMs;
|
|
8091
|
-
/** One pinned binary. */
|
|
8092
|
-
async probe(provider, executable) {
|
|
8093
|
-
const stamp = await this.stamp(executable);
|
|
8094
|
-
const key = `${provider}|${executable}|${stamp ? `${stamp.mtimeMs}:${stamp.size}` : "unstamped"}`;
|
|
8095
|
-
const cached = this.cache.get(`${provider}|${executable}`);
|
|
8096
|
-
if (cached && cached.key === key && this.now() - cached.probedAt < cached.ttlMs) {
|
|
8097
|
-
return cached.result;
|
|
8098
|
-
}
|
|
8099
|
-
const pending = this.inFlight.get(key);
|
|
8100
|
-
if (pending) return pending;
|
|
8101
|
-
const run = this.run(provider, executable).then(({ result, answered }) => {
|
|
8102
|
-
this.cache.set(`${provider}|${executable}`, {
|
|
8103
|
-
key,
|
|
8104
|
-
// A COMPLETE answer from a stamped file is kept until the file
|
|
8105
|
-
// changes or, at the latest, for `stampedMaxAgeMs` (an upgrade
|
|
8106
|
-
// behind a stable wrapper does not change the stamp). An
|
|
8107
|
-
// incomplete one is retried soon, and an unstamped one on the
|
|
8108
|
-
// ten-minute clock.
|
|
8109
|
-
ttlMs: !answered ? this.incompleteRetryMs : stamp !== null ? this.stampedMaxAgeMs : this.unstampedTtlMs,
|
|
8110
|
-
probedAt: this.now(),
|
|
8111
|
-
result
|
|
8112
|
-
});
|
|
8113
|
-
return result;
|
|
8114
|
-
}).finally(() => {
|
|
8115
|
-
this.inFlight.delete(key);
|
|
8116
|
-
});
|
|
8117
|
-
this.inFlight.set(key, run);
|
|
8118
|
-
return run;
|
|
8119
|
-
}
|
|
8120
|
-
/** Every pinned binary in `paths`, in provider order. Unpinned providers are skipped. */
|
|
8121
|
-
async probeAll(paths) {
|
|
8122
|
-
const out = [];
|
|
8123
|
-
for (const provider of MODEL_CLI_PROVIDERS) {
|
|
8124
|
-
const executable = paths?.[provider];
|
|
8125
|
-
if (typeof executable !== "string" || !executable.trim()) continue;
|
|
8126
|
-
out.push(await this.probe(provider, executable));
|
|
8127
|
-
}
|
|
8128
|
-
return out;
|
|
8129
|
-
}
|
|
8130
|
-
async stamp(executable) {
|
|
8131
|
-
if (!this.options.statFile) return null;
|
|
8132
|
-
try {
|
|
8133
|
-
const stamp = await this.options.statFile(executable);
|
|
8134
|
-
return stamp && Number.isFinite(stamp.mtimeMs) && Number.isFinite(stamp.size) ? stamp : null;
|
|
8135
|
-
} catch {
|
|
8136
|
-
return null;
|
|
8137
|
-
}
|
|
8138
|
-
}
|
|
8139
|
-
async run(provider, executable) {
|
|
8140
|
-
const command = this.commandFor(executable);
|
|
8141
|
-
const versionOutput = await this.ask(command, ["--version"]);
|
|
8142
|
-
const version = versionOutput === null ? null : parseCliVersion(versionOutput);
|
|
8143
|
-
const help = await this.ask(command, [...HELP_ARGS[provider]]);
|
|
8144
|
-
return {
|
|
8145
|
-
result: judgeModelCliHelp(provider, executable, version, help),
|
|
8146
|
-
// "Answered" = both invocations produced output. An UNRECOGNISED
|
|
8147
|
-
// help text is still an answer — deterministic for that file — and
|
|
8148
|
-
// re-asking every minute would learn nothing.
|
|
8149
|
-
answered: versionOutput !== null && help !== null
|
|
8150
|
-
};
|
|
8151
|
-
}
|
|
8152
|
-
/**
|
|
8153
|
-
* The runner on Windows goes through `cmd.exe` (`shell: true`), which
|
|
8154
|
-
* splits an unquoted `C:\Program Files\…` at the space.
|
|
8155
|
-
*/
|
|
8156
|
-
commandFor(executable) {
|
|
8157
|
-
return this.options.platform === "win32" && /\s/.test(executable) && !executable.startsWith('"') ? `"${executable}"` : executable;
|
|
8158
|
-
}
|
|
8159
|
-
/** stdout+stderr of one invocation, or null when it could not be run at all. */
|
|
8160
|
-
async ask(command, args) {
|
|
8161
|
-
try {
|
|
8162
|
-
const result = await this.options.runner.run(command, args);
|
|
8163
|
-
if (result.timedOut) return null;
|
|
8164
|
-
const text = `${result.stdout ?? ""}
|
|
8165
|
-
${result.stderr ?? ""}`;
|
|
8166
|
-
return result.code === 0 || text.trim() ? text : null;
|
|
8167
|
-
} catch {
|
|
8168
|
-
return null;
|
|
8169
|
-
}
|
|
8170
|
-
}
|
|
8171
|
-
};
|
|
8172
|
-
function parseAdvertisedFlags(help) {
|
|
8173
|
-
const flags = /* @__PURE__ */ new Set();
|
|
8174
|
-
const pattern = /(?:^|[\s,[(|])(--?[A-Za-z][A-Za-z0-9-]*)(?=$|[\s,=<[\])|.:])/gm;
|
|
8175
|
-
for (const match of help.matchAll(pattern)) {
|
|
8176
|
-
flags.add(match[1]);
|
|
8177
|
-
}
|
|
8178
|
-
return flags;
|
|
8179
|
-
}
|
|
8180
|
-
__name(parseAdvertisedFlags, "parseAdvertisedFlags");
|
|
8181
|
-
function judgeModelCliHelp(provider, executable, version, help) {
|
|
8182
|
-
const required = MODEL_CLI_REQUIRED_FLAGS[provider];
|
|
8183
|
-
const base = {
|
|
8184
|
-
provider,
|
|
8185
|
-
executable,
|
|
8186
|
-
version,
|
|
8187
|
-
supportedFlags: null,
|
|
8188
|
-
missingRequiredFlags: [],
|
|
8189
|
-
missingConditionalFlags: [],
|
|
8190
|
-
unsupportedOptionalFlags: [],
|
|
8191
|
-
note: null
|
|
8192
|
-
};
|
|
8193
|
-
if (help === null) {
|
|
8194
|
-
return { ...base, note: "the binary did not answer --help; nothing is dropped" };
|
|
8195
|
-
}
|
|
8196
|
-
const advertised = parseAdvertisedFlags(help);
|
|
8197
|
-
if (!required.some((flag) => advertised.has(flag))) {
|
|
8198
|
-
return {
|
|
8199
|
-
...base,
|
|
8200
|
-
note: `the help text lists none of the flags this node always passes (${required.join(", ")}); its format is not recognised, so nothing is dropped`
|
|
8201
|
-
};
|
|
8202
|
-
}
|
|
8203
|
-
const missing = /* @__PURE__ */ __name((list) => list.filter((flag) => !advertised.has(flag)), "missing");
|
|
8204
|
-
return {
|
|
8205
|
-
...base,
|
|
8206
|
-
supportedFlags: advertised,
|
|
8207
|
-
missingRequiredFlags: missing(required),
|
|
8208
|
-
missingConditionalFlags: missing(MODEL_CLI_CONDITIONAL_FLAGS[provider]),
|
|
8209
|
-
unsupportedOptionalFlags: missing(MODEL_CLI_OPTIONAL_FLAGS[provider])
|
|
8210
|
-
};
|
|
8211
|
-
}
|
|
8212
|
-
__name(judgeModelCliHelp, "judgeModelCliHelp");
|
|
8213
|
-
function isModelCliCompatible(compat) {
|
|
8214
|
-
return compat.missingRequiredFlags.length === 0;
|
|
8215
|
-
}
|
|
8216
|
-
__name(isModelCliCompatible, "isModelCliCompatible");
|
|
8217
|
-
function describeCliVersions(results) {
|
|
8218
|
-
return results.map((result) => `${result.provider} ${result.version ?? "unknown"}`.slice(0, FLEET_MAX_CLI_VERSION_LENGTH)).slice(0, FLEET_MAX_CLI_VERSIONS);
|
|
8219
|
-
}
|
|
8220
|
-
__name(describeCliVersions, "describeCliVersions");
|
|
8221
|
-
var LEGACY_COMMAND_NAME = {
|
|
8222
|
-
"claude-code": "claude",
|
|
8223
|
-
codex: "codex"
|
|
8224
|
-
};
|
|
8225
|
-
function primaryPinnedCliVersion(results) {
|
|
8226
|
-
const first = results.find((result) => result.version !== null);
|
|
8227
|
-
return first ? `${LEGACY_COMMAND_NAME[first.provider]} ${first.version}`.slice(0, FLEET_MAX_CLI_VERSION_LENGTH) : null;
|
|
8228
|
-
}
|
|
8229
|
-
__name(primaryPinnedCliVersion, "primaryPinnedCliVersion");
|
|
8230
|
-
|
|
8231
|
-
// src/core/worker-loop.ts
|
|
8232
|
-
var import_node_perf_hooks = require("node:perf_hooks");
|
|
8233
|
-
|
|
8234
|
-
// src/core/heartbeat.ts
|
|
8235
|
-
var systemScheduler = {
|
|
8236
|
-
setTimeout: /* @__PURE__ */ __name((callback, ms) => setTimeout(callback, ms), "setTimeout"),
|
|
8237
|
-
clearTimeout: /* @__PURE__ */ __name((handle) => clearTimeout(handle), "clearTimeout")
|
|
8238
|
-
};
|
|
8239
|
-
var OPTIONAL_DESCRIPTION_FIELD_TIERS = [
|
|
8240
|
-
// NEWEST first. Node lifecycle + remote node limits (self-build slices
|
|
8241
|
-
// AR, AS). Tiered (review): `cliVersions` goes out on EVERY beat of a
|
|
8242
|
-
// node with a pinned CLI, so an upgraded node talking to a platform that
|
|
8243
|
-
// predates it would otherwise drop the worker state and housekeeping
|
|
8244
|
-
// along with it — losing health signals that platform DOES understand,
|
|
8245
|
-
// for the life of the process. Stripping the newest tier first keeps them.
|
|
8246
|
-
["cliVersions", "maxConcurrentJobs", "maxCpuPercent", "maxMemoryMb"],
|
|
8247
|
-
// Fleet health signals (EW-776) and node housekeeping (EW-803).
|
|
8248
|
-
[
|
|
8249
|
-
"workerState",
|
|
8250
|
-
"workerStateReason",
|
|
8251
|
-
"minFreeDiskBytes",
|
|
8252
|
-
"workspaceCount",
|
|
8253
|
-
"workspaceBytes",
|
|
8254
|
-
"lastReclaimAt",
|
|
8255
|
-
"lastReclaimFreedBytes"
|
|
8256
|
-
]
|
|
8257
|
-
];
|
|
8258
|
-
function computeBackoffDelay(intervalMs, failures, maxMs) {
|
|
8259
|
-
if (failures <= 0) {
|
|
8260
|
-
return intervalMs;
|
|
8261
|
-
}
|
|
8262
|
-
const exponent = Math.min(failures, 30);
|
|
8263
|
-
const delay2 = intervalMs * 2 ** exponent;
|
|
8264
|
-
return Math.min(delay2, maxMs);
|
|
8265
|
-
}
|
|
8266
|
-
__name(computeBackoffDelay, "computeBackoffDelay");
|
|
8267
|
-
var HeartbeatLoop = class {
|
|
8268
|
-
static {
|
|
8269
|
-
__name(this, "HeartbeatLoop");
|
|
8270
|
-
}
|
|
8271
|
-
options;
|
|
8272
|
-
scheduler;
|
|
8273
|
-
now;
|
|
8274
|
-
listeners = /* @__PURE__ */ new Set();
|
|
8275
|
-
timer = null;
|
|
8276
|
-
running = false;
|
|
8277
|
-
inFlight = null;
|
|
8278
|
-
/**
|
|
8279
|
-
* How many optional-field TIERS (newest first) this platform has proven
|
|
8280
|
-
* it predates; from then on the beat leaves those tiers out. 0 = it
|
|
8281
|
-
* takes everything. Process-scoped on purpose — a platform upgrade is a
|
|
8282
|
-
* restart-shaped event for the node, and re-probing on every beat would
|
|
8283
|
-
* mean one wasted round trip per beat, forever, against an API that
|
|
8284
|
-
* will never accept them.
|
|
8285
|
-
*/
|
|
8286
|
-
strippedTiers = 0;
|
|
8287
|
-
state = {
|
|
8288
|
-
state: "idle",
|
|
8289
|
-
lastHeartbeatAt: null,
|
|
8290
|
-
consecutiveFailures: 0,
|
|
8291
|
-
nextAttemptInMs: null,
|
|
8292
|
-
lastError: null,
|
|
8293
|
-
lastErrorKind: null,
|
|
8294
|
-
node: null
|
|
8295
|
-
};
|
|
8296
|
-
constructor(options) {
|
|
8297
|
-
this.options = {
|
|
8298
|
-
...options,
|
|
8299
|
-
intervalMs: options.intervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS,
|
|
8300
|
-
maxBackoffMs: options.maxBackoffMs ?? MAX_HEARTBEAT_BACKOFF_MS
|
|
8301
|
-
};
|
|
8302
|
-
this.scheduler = options.scheduler ?? systemScheduler;
|
|
8303
|
-
this.now = options.now ?? (() => Date.now());
|
|
8304
|
-
this.options.logger?.protect(options.secret);
|
|
8305
|
-
}
|
|
8306
|
-
getState() {
|
|
8307
|
-
return { ...this.state };
|
|
8308
|
-
}
|
|
8309
|
-
onChange(listener) {
|
|
8310
|
-
this.listeners.add(listener);
|
|
8311
|
-
return () => {
|
|
8312
|
-
this.listeners.delete(listener);
|
|
8313
|
-
};
|
|
8314
|
-
}
|
|
8315
|
-
/** Start beating. Resolves once the FIRST beat has settled. */
|
|
8316
|
-
start() {
|
|
8317
|
-
if (this.running) {
|
|
8318
|
-
return this.inFlight ?? Promise.resolve();
|
|
8319
|
-
}
|
|
8320
|
-
this.running = true;
|
|
8321
|
-
return this.tick();
|
|
8322
|
-
}
|
|
8323
|
-
/** Stop beating and cancel any pending timer. */
|
|
8324
|
-
stop() {
|
|
8325
|
-
this.running = false;
|
|
8326
|
-
this.cancelTimer();
|
|
8327
|
-
this.patch({ state: "stopped", nextAttemptInMs: null });
|
|
8328
|
-
}
|
|
8329
|
-
/** Await the in-flight beat, if any (test/shutdown helper). */
|
|
8330
|
-
async settled() {
|
|
8331
|
-
await this.inFlight;
|
|
8332
|
-
}
|
|
8333
|
-
/** Run one beat now. Exposed so the UI can offer "retry" and tests can step. */
|
|
8334
|
-
tick() {
|
|
8335
|
-
const run = this.runOnce().finally(() => {
|
|
8336
|
-
if (this.inFlight === run) {
|
|
8337
|
-
this.inFlight = null;
|
|
8338
|
-
}
|
|
8339
|
-
});
|
|
8340
|
-
this.inFlight = run;
|
|
8341
|
-
return run;
|
|
8342
|
-
}
|
|
8343
|
-
async runOnce() {
|
|
8344
|
-
if (!this.running) {
|
|
8345
|
-
return;
|
|
8346
|
-
}
|
|
8347
|
-
this.cancelTimer();
|
|
8348
|
-
if (this.state.state !== "connected") {
|
|
8349
|
-
this.patch({ state: "connecting" });
|
|
8350
|
-
}
|
|
8351
|
-
try {
|
|
8352
|
-
const description = await this.options.describe();
|
|
8353
|
-
this.onSuccess(await this.beat(description));
|
|
8354
|
-
} catch (error) {
|
|
8355
|
-
this.onFailure(error);
|
|
8356
|
-
}
|
|
8357
|
-
this.scheduleNext();
|
|
8358
|
-
}
|
|
8359
|
-
/**
|
|
8360
|
-
* Send one beat, tolerating a platform that predates the optional
|
|
8361
|
-
* self-description fields (the worker state, EW-776; the housekeeping
|
|
8362
|
-
* report, EW-803).
|
|
8363
|
-
*
|
|
8364
|
-
* The API validates heartbeats with `whitelist + forbidNonWhitelisted`,
|
|
8365
|
-
* so a field an older build does not know is not ignored — it 400s the
|
|
8366
|
-
* whole request. A 400 is a FAILED beat, a failed beat backs off, and
|
|
8367
|
-
* enough of them sweep the node to `offline`. Which would mean shipping
|
|
8368
|
-
* a health-reporting feature whose failure mode is "every node in a
|
|
8369
|
-
* mixed-version fleet goes dark": the exact class of outage this slice
|
|
8370
|
-
* was written to end.
|
|
8371
|
-
*
|
|
8372
|
-
* So a 400 on a beat that CARRIED those fields is retried once,
|
|
8373
|
-
* immediately, without them. If that succeeds the platform is simply
|
|
8374
|
-
* older than this daemon; we latch, say so once, and keep reporting
|
|
8375
|
-
* liveness. The retry costs one request, once per process.
|
|
8376
|
-
*
|
|
8377
|
-
* Inferring "the field was rejected" from a bare 400 is deliberate and
|
|
8378
|
-
* has a cost worth stating: `FleetClient` never surfaces server bodies
|
|
8379
|
-
* (fixed client-authored messages only), so a 400 caused by something
|
|
8380
|
-
* else on such a beat also latches, and this node reports liveness only
|
|
8381
|
-
* until it restarts. That is the safe direction — the node stays
|
|
8382
|
-
* observable either way, and only the extra signal is lost.
|
|
8383
|
-
*/
|
|
8384
|
-
async beat(description) {
|
|
8385
|
-
const credential = { nodeId: this.options.nodeId, secret: this.options.secret };
|
|
8386
|
-
let sent = stripTiers(description, this.strippedTiers);
|
|
8387
|
-
try {
|
|
8388
|
-
return await this.options.client.heartbeat({ ...credential, ...sent });
|
|
8389
|
-
} catch (error) {
|
|
8390
|
-
if (!isFieldRejection(error)) throw error;
|
|
8391
|
-
let lastError = error;
|
|
8392
|
-
for (let tiers = this.strippedTiers + 1; tiers <= OPTIONAL_DESCRIPTION_FIELD_TIERS.length; tiers += 1) {
|
|
8393
|
-
const candidate = stripTiers(description, tiers);
|
|
8394
|
-
if (Object.keys(candidate).length === Object.keys(sent).length) continue;
|
|
8395
|
-
sent = candidate;
|
|
8396
|
-
try {
|
|
8397
|
-
const result = await this.options.client.heartbeat({ ...credential, ...candidate });
|
|
8398
|
-
this.strippedTiers = tiers;
|
|
8399
|
-
this.options.logger?.warn(
|
|
8400
|
-
tiers >= OPTIONAL_DESCRIPTION_FIELD_TIERS.length ? "Platform rejected the optional self-description fields (worker state, housekeeping); it predates them. Reporting liveness only until this node restarts." : "Platform rejected the newest optional self-description fields (pinned CLI versions, enforced limits); it predates them. Still reporting worker state and housekeeping; the newer fields stay off until this node restarts."
|
|
8401
|
-
);
|
|
8402
|
-
return result;
|
|
8403
|
-
} catch (retryError) {
|
|
8404
|
-
if (!isFieldRejection(retryError)) throw retryError;
|
|
8405
|
-
lastError = retryError;
|
|
8406
|
-
}
|
|
8407
|
-
}
|
|
8408
|
-
throw lastError;
|
|
8409
|
-
}
|
|
8424
|
+
"claude-code": [
|
|
8425
|
+
"--model",
|
|
8426
|
+
"--add-dir",
|
|
8427
|
+
"--allowedTools",
|
|
8428
|
+
"--dangerously-skip-permissions",
|
|
8429
|
+
"--verbose",
|
|
8430
|
+
"--resume",
|
|
8431
|
+
"--fork-session"
|
|
8432
|
+
],
|
|
8433
|
+
codex: ["--add-dir", "-c", "-m", "--dangerously-bypass-approvals-and-sandbox"]
|
|
8434
|
+
};
|
|
8435
|
+
var MODEL_CLI_COMPAT_UNSTAMPED_TTL_MS = 10 * 6e4;
|
|
8436
|
+
var MODEL_CLI_COMPAT_STAMPED_MAX_AGE_MS = 60 * 6e4;
|
|
8437
|
+
var MODEL_CLI_COMPAT_INCOMPLETE_RETRY_MS = 6e4;
|
|
8438
|
+
var ModelCliCompatibilityProbe = class {
|
|
8439
|
+
constructor(options) {
|
|
8440
|
+
this.options = options;
|
|
8441
|
+
this.now = options.now ?? (() => Date.now());
|
|
8442
|
+
this.unstampedTtlMs = options.unstampedTtlMs ?? MODEL_CLI_COMPAT_UNSTAMPED_TTL_MS;
|
|
8443
|
+
this.incompleteRetryMs = options.incompleteRetryMs ?? MODEL_CLI_COMPAT_INCOMPLETE_RETRY_MS;
|
|
8444
|
+
this.stampedMaxAgeMs = options.stampedMaxAgeMs ?? MODEL_CLI_COMPAT_STAMPED_MAX_AGE_MS;
|
|
8410
8445
|
}
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
lastHeartbeatAt: this.now(),
|
|
8415
|
-
consecutiveFailures: 0,
|
|
8416
|
-
lastError: null,
|
|
8417
|
-
lastErrorKind: null,
|
|
8418
|
-
node: result.node
|
|
8419
|
-
});
|
|
8420
|
-
if (this.options.onAccepted) {
|
|
8421
|
-
try {
|
|
8422
|
-
this.options.onAccepted(result);
|
|
8423
|
-
} catch {
|
|
8424
|
-
}
|
|
8425
|
-
}
|
|
8446
|
+
options;
|
|
8447
|
+
static {
|
|
8448
|
+
__name(this, "ModelCliCompatibilityProbe");
|
|
8426
8449
|
}
|
|
8427
|
-
|
|
8428
|
-
|
|
8429
|
-
|
|
8430
|
-
|
|
8431
|
-
|
|
8432
|
-
|
|
8433
|
-
|
|
8434
|
-
|
|
8435
|
-
|
|
8436
|
-
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
|
|
8450
|
+
cache = /* @__PURE__ */ new Map();
|
|
8451
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
8452
|
+
now;
|
|
8453
|
+
unstampedTtlMs;
|
|
8454
|
+
incompleteRetryMs;
|
|
8455
|
+
stampedMaxAgeMs;
|
|
8456
|
+
/** One pinned binary. */
|
|
8457
|
+
async probe(provider, executable) {
|
|
8458
|
+
const stamp = await this.stamp(executable);
|
|
8459
|
+
const key = `${provider}|${executable}|${stamp ? `${stamp.mtimeMs}:${stamp.size}` : "unstamped"}`;
|
|
8460
|
+
const cached = this.cache.get(`${provider}|${executable}`);
|
|
8461
|
+
if (cached && cached.key === key && this.now() - cached.probedAt < cached.ttlMs) {
|
|
8462
|
+
return cached.result;
|
|
8463
|
+
}
|
|
8464
|
+
const pending = this.inFlight.get(key);
|
|
8465
|
+
if (pending) return pending;
|
|
8466
|
+
const run = this.run(provider, executable).then(({ result, answered }) => {
|
|
8467
|
+
this.cache.set(`${provider}|${executable}`, {
|
|
8468
|
+
key,
|
|
8469
|
+
// A COMPLETE answer from a stamped file is kept until the file
|
|
8470
|
+
// changes or, at the latest, for `stampedMaxAgeMs` (an upgrade
|
|
8471
|
+
// behind a stable wrapper does not change the stamp). An
|
|
8472
|
+
// incomplete one is retried soon, and an unstamped one on the
|
|
8473
|
+
// ten-minute clock.
|
|
8474
|
+
ttlMs: !answered ? this.incompleteRetryMs : stamp !== null ? this.stampedMaxAgeMs : this.unstampedTtlMs,
|
|
8475
|
+
probedAt: this.now(),
|
|
8476
|
+
result
|
|
8477
|
+
});
|
|
8478
|
+
return result;
|
|
8479
|
+
}).finally(() => {
|
|
8480
|
+
this.inFlight.delete(key);
|
|
8440
8481
|
});
|
|
8441
|
-
this.
|
|
8482
|
+
this.inFlight.set(key, run);
|
|
8483
|
+
return run;
|
|
8442
8484
|
}
|
|
8443
|
-
|
|
8444
|
-
|
|
8445
|
-
|
|
8485
|
+
/** Every pinned binary in `paths`, in provider order. Unpinned providers are skipped. */
|
|
8486
|
+
async probeAll(paths) {
|
|
8487
|
+
const out = [];
|
|
8488
|
+
for (const provider of MODEL_CLI_PROVIDERS) {
|
|
8489
|
+
const executable = paths?.[provider];
|
|
8490
|
+
if (typeof executable !== "string" || !executable.trim()) continue;
|
|
8491
|
+
out.push(await this.probe(provider, executable));
|
|
8446
8492
|
}
|
|
8447
|
-
|
|
8448
|
-
this.options.intervalMs,
|
|
8449
|
-
this.state.consecutiveFailures,
|
|
8450
|
-
this.options.maxBackoffMs
|
|
8451
|
-
);
|
|
8452
|
-
this.patch({ nextAttemptInMs: delay2 });
|
|
8453
|
-
this.timer = this.scheduler.setTimeout(() => {
|
|
8454
|
-
this.timer = null;
|
|
8455
|
-
void this.tick();
|
|
8456
|
-
}, delay2);
|
|
8493
|
+
return out;
|
|
8457
8494
|
}
|
|
8458
|
-
|
|
8459
|
-
if (this.
|
|
8460
|
-
|
|
8461
|
-
|
|
8495
|
+
async stamp(executable) {
|
|
8496
|
+
if (!this.options.statFile) return null;
|
|
8497
|
+
try {
|
|
8498
|
+
const stamp = await this.options.statFile(executable);
|
|
8499
|
+
return stamp && Number.isFinite(stamp.mtimeMs) && Number.isFinite(stamp.size) ? stamp : null;
|
|
8500
|
+
} catch {
|
|
8501
|
+
return null;
|
|
8462
8502
|
}
|
|
8463
8503
|
}
|
|
8464
|
-
|
|
8465
|
-
|
|
8466
|
-
const
|
|
8467
|
-
|
|
8468
|
-
|
|
8504
|
+
async run(provider, executable) {
|
|
8505
|
+
const command = this.commandFor(executable);
|
|
8506
|
+
const versionOutput = await this.ask(command, ["--version"]);
|
|
8507
|
+
const version = versionOutput === null ? null : parseCliVersion(versionOutput);
|
|
8508
|
+
const help = await this.ask(command, [...HELP_ARGS[provider]]);
|
|
8509
|
+
return {
|
|
8510
|
+
result: judgeModelCliHelp(provider, executable, version, help),
|
|
8511
|
+
// "Answered" = both invocations produced output. An UNRECOGNISED
|
|
8512
|
+
// help text is still an answer — deterministic for that file — and
|
|
8513
|
+
// re-asking every minute would learn nothing.
|
|
8514
|
+
answered: versionOutput !== null && help !== null
|
|
8515
|
+
};
|
|
8516
|
+
}
|
|
8517
|
+
/**
|
|
8518
|
+
* The runner on Windows goes through `cmd.exe` (`shell: true`), which
|
|
8519
|
+
* splits an unquoted `C:\Program Files\…` at the space.
|
|
8520
|
+
*/
|
|
8521
|
+
commandFor(executable) {
|
|
8522
|
+
return this.options.platform === "win32" && /\s/.test(executable) && !executable.startsWith('"') ? `"${executable}"` : executable;
|
|
8523
|
+
}
|
|
8524
|
+
/** stdout+stderr of one invocation, or null when it could not be run at all. */
|
|
8525
|
+
async ask(command, args) {
|
|
8526
|
+
try {
|
|
8527
|
+
const result = await this.options.runner.run(command, args);
|
|
8528
|
+
if (result.timedOut) return null;
|
|
8529
|
+
const text = `${result.stdout ?? ""}
|
|
8530
|
+
${result.stderr ?? ""}`;
|
|
8531
|
+
return result.code === 0 || text.trim() ? text : null;
|
|
8532
|
+
} catch {
|
|
8533
|
+
return null;
|
|
8469
8534
|
}
|
|
8470
8535
|
}
|
|
8471
8536
|
};
|
|
8472
|
-
function
|
|
8473
|
-
const
|
|
8474
|
-
const
|
|
8475
|
-
for (const
|
|
8476
|
-
|
|
8477
|
-
out[key] = value;
|
|
8537
|
+
function parseAdvertisedFlags(help) {
|
|
8538
|
+
const flags = /* @__PURE__ */ new Set();
|
|
8539
|
+
const pattern = /(?:^|[\s,[(|])(--?[A-Za-z][A-Za-z0-9-]*)(?=$|[\s,=<[\])|.:])/gm;
|
|
8540
|
+
for (const match of help.matchAll(pattern)) {
|
|
8541
|
+
flags.add(match[1]);
|
|
8478
8542
|
}
|
|
8479
|
-
return
|
|
8543
|
+
return flags;
|
|
8480
8544
|
}
|
|
8481
|
-
__name(
|
|
8482
|
-
function
|
|
8483
|
-
|
|
8545
|
+
__name(parseAdvertisedFlags, "parseAdvertisedFlags");
|
|
8546
|
+
function judgeModelCliHelp(provider, executable, version, help) {
|
|
8547
|
+
const required = MODEL_CLI_REQUIRED_FLAGS[provider];
|
|
8548
|
+
const base = {
|
|
8549
|
+
provider,
|
|
8550
|
+
executable,
|
|
8551
|
+
version,
|
|
8552
|
+
supportedFlags: null,
|
|
8553
|
+
missingRequiredFlags: [],
|
|
8554
|
+
missingConditionalFlags: [],
|
|
8555
|
+
unsupportedOptionalFlags: [],
|
|
8556
|
+
note: null
|
|
8557
|
+
};
|
|
8558
|
+
if (help === null) {
|
|
8559
|
+
return { ...base, note: "the binary did not answer --help; nothing is dropped" };
|
|
8560
|
+
}
|
|
8561
|
+
const advertised = parseAdvertisedFlags(help);
|
|
8562
|
+
if (!required.some((flag) => advertised.has(flag))) {
|
|
8563
|
+
return {
|
|
8564
|
+
...base,
|
|
8565
|
+
note: `the help text lists none of the flags this node always passes (${required.join(", ")}); its format is not recognised, so nothing is dropped`
|
|
8566
|
+
};
|
|
8567
|
+
}
|
|
8568
|
+
const missing = /* @__PURE__ */ __name((list) => list.filter((flag) => !advertised.has(flag)), "missing");
|
|
8569
|
+
return {
|
|
8570
|
+
...base,
|
|
8571
|
+
supportedFlags: advertised,
|
|
8572
|
+
missingRequiredFlags: missing(required),
|
|
8573
|
+
missingConditionalFlags: missing(MODEL_CLI_CONDITIONAL_FLAGS[provider]),
|
|
8574
|
+
unsupportedOptionalFlags: missing(MODEL_CLI_OPTIONAL_FLAGS[provider])
|
|
8575
|
+
};
|
|
8484
8576
|
}
|
|
8485
|
-
__name(
|
|
8577
|
+
__name(judgeModelCliHelp, "judgeModelCliHelp");
|
|
8578
|
+
function isModelCliCompatible(compat) {
|
|
8579
|
+
return compat.missingRequiredFlags.length === 0;
|
|
8580
|
+
}
|
|
8581
|
+
__name(isModelCliCompatible, "isModelCliCompatible");
|
|
8582
|
+
function describeCliVersions(results) {
|
|
8583
|
+
return results.map((result) => `${result.provider} ${result.version ?? "unknown"}`.slice(0, FLEET_MAX_CLI_VERSION_LENGTH)).slice(0, FLEET_MAX_CLI_VERSIONS);
|
|
8584
|
+
}
|
|
8585
|
+
__name(describeCliVersions, "describeCliVersions");
|
|
8586
|
+
var LEGACY_COMMAND_NAME = {
|
|
8587
|
+
"claude-code": "claude",
|
|
8588
|
+
codex: "codex"
|
|
8589
|
+
};
|
|
8590
|
+
function primaryPinnedCliVersion(results) {
|
|
8591
|
+
const first = results.find((result) => result.version !== null);
|
|
8592
|
+
return first ? `${LEGACY_COMMAND_NAME[first.provider]} ${first.version}`.slice(0, FLEET_MAX_CLI_VERSION_LENGTH) : null;
|
|
8593
|
+
}
|
|
8594
|
+
__name(primaryPinnedCliVersion, "primaryPinnedCliVersion");
|
|
8595
|
+
|
|
8596
|
+
// src/core/worker-loop.ts
|
|
8597
|
+
var import_node_perf_hooks = require("node:perf_hooks");
|
|
8486
8598
|
|
|
8487
8599
|
// src/core/resource-limits.ts
|
|
8488
8600
|
var ADMIT = { admit: true, reason: null };
|
|
@@ -13096,6 +13208,11 @@ async function runModelStep(jobId, execution, workspacePath, mounts, bridgeSpec,
|
|
|
13096
13208
|
emptyMcpConfigPath = mcpConfigPath;
|
|
13097
13209
|
}
|
|
13098
13210
|
const droppedFlags = await optionalFlagsToDrop(jobId, execution, executable, io);
|
|
13211
|
+
const codexUserSettings = execution.provider === "codex" ? await readOwnerCodexSettings(
|
|
13212
|
+
jobId,
|
|
13213
|
+
io,
|
|
13214
|
+
effectiveCodexHome(io, modelEnvOverlay, execution.envPassthrough, execution.envGrants)
|
|
13215
|
+
) : void 0;
|
|
13099
13216
|
const invoke = /* @__PURE__ */ __name(async (instructions, resumeSessionId) => {
|
|
13100
13217
|
await scratchFs.writeFile(scratch.instructionsPath, instructions);
|
|
13101
13218
|
let command;
|
|
@@ -13108,6 +13225,7 @@ async function runModelStep(jobId, execution, workspacePath, mounts, bridgeSpec,
|
|
|
13108
13225
|
...mounts && mounts.length > 0 ? { mounts } : {},
|
|
13109
13226
|
...bridge.cli ? { mcp: bridge.cli } : {},
|
|
13110
13227
|
...emptyMcpConfigPath ? { emptyMcpConfigPath } : {},
|
|
13228
|
+
...codexUserSettings ? { codexUserSettings } : {},
|
|
13111
13229
|
...resumeSessionId ? { resumeSessionId } : {},
|
|
13112
13230
|
...io.platform ? { platform: io.platform } : {},
|
|
13113
13231
|
...streamClaude ? { stream: true } : {},
|
|
@@ -13326,6 +13444,32 @@ function resolveRealHomeDir(io) {
|
|
|
13326
13444
|
return (0, import_os2.homedir)();
|
|
13327
13445
|
}
|
|
13328
13446
|
__name(resolveRealHomeDir, "resolveRealHomeDir");
|
|
13447
|
+
async function readOwnerCodexSettings(jobId, io, codexHome) {
|
|
13448
|
+
const read = io.sessionConfigFs?.readFile?.bind(io.sessionConfigFs);
|
|
13449
|
+
if (!read) return {};
|
|
13450
|
+
try {
|
|
13451
|
+
return readCodexUserSettings(await read((0, import_path4.join)(codexHome, "config.toml")));
|
|
13452
|
+
} catch (error) {
|
|
13453
|
+
io.logger?.warn(
|
|
13454
|
+
`[fleet-node] job ${jobId}: could not read the Codex config to keep its sandbox and model defaults; the run uses Codex's own defaults (still under --ignore-user-config): ${describeContainmentError(error)}`
|
|
13455
|
+
);
|
|
13456
|
+
return {};
|
|
13457
|
+
}
|
|
13458
|
+
}
|
|
13459
|
+
__name(readOwnerCodexSettings, "readOwnerCodexSettings");
|
|
13460
|
+
function effectiveCodexHome(io, envOverlay, envPassthrough, envGrants) {
|
|
13461
|
+
const isCodexHome = /* @__PURE__ */ __name((name) => name.toUpperCase() === "CODEX_HOME", "isCodexHome");
|
|
13462
|
+
const childEnv = buildNodeCheckEnv(
|
|
13463
|
+
envPassthrough?.filter(isCodexHome) ?? null,
|
|
13464
|
+
io.parentEnv,
|
|
13465
|
+
envGrants?.filter(isCodexHome) ?? null,
|
|
13466
|
+
envOverlay
|
|
13467
|
+
);
|
|
13468
|
+
const key = Object.keys(childEnv).find(isCodexHome);
|
|
13469
|
+
const value = key === void 0 ? void 0 : childEnv[key];
|
|
13470
|
+
return typeof value === "string" && value.trim() ? value.trim() : (0, import_path4.join)(resolveRealHomeDir(io), ".codex");
|
|
13471
|
+
}
|
|
13472
|
+
__name(effectiveCodexHome, "effectiveCodexHome");
|
|
13329
13473
|
function describeContainmentError(error) {
|
|
13330
13474
|
return error instanceof Error ? error.message : String(error);
|
|
13331
13475
|
}
|
|
@@ -15137,10 +15281,14 @@ var USER_SELECTABLE_WORK_KINDS2 = [
|
|
|
15137
15281
|
"repo",
|
|
15138
15282
|
"app"
|
|
15139
15283
|
];
|
|
15140
|
-
var WORK_KINDS2 = [...USER_SELECTABLE_WORK_KINDS2, "company", "campaign", "default"];
|
|
15284
|
+
var WORK_KINDS2 = [...USER_SELECTABLE_WORK_KINDS2, "company", "campaign", "project", "default"];
|
|
15141
15285
|
new Set(WORK_KINDS2);
|
|
15142
15286
|
var GOAL_KINDS2 = ["metric", "delivery"];
|
|
15143
15287
|
new Set(GOAL_KINDS2);
|
|
15288
|
+
var RECORD_VISIBILITIES2 = ["private", "team", "organization"];
|
|
15289
|
+
new Set(RECORD_VISIBILITIES2);
|
|
15290
|
+
var RECORD_LIST_VIEWS2 = ["mine", "shared", "organization"];
|
|
15291
|
+
new Set(RECORD_LIST_VIEWS2);
|
|
15144
15292
|
var VERSION2 = "1.0.0";
|
|
15145
15293
|
var GTM_SKILLS2 = [
|
|
15146
15294
|
// ── research ────────────────────────────────────────────────────────
|
|
@@ -19451,10 +19599,14 @@ var USER_SELECTABLE_WORK_KINDS3 = [
|
|
|
19451
19599
|
"repo",
|
|
19452
19600
|
"app"
|
|
19453
19601
|
];
|
|
19454
|
-
var WORK_KINDS3 = [...USER_SELECTABLE_WORK_KINDS3, "company", "campaign", "default"];
|
|
19602
|
+
var WORK_KINDS3 = [...USER_SELECTABLE_WORK_KINDS3, "company", "campaign", "project", "default"];
|
|
19455
19603
|
new Set(WORK_KINDS3);
|
|
19456
19604
|
var GOAL_KINDS3 = ["metric", "delivery"];
|
|
19457
19605
|
new Set(GOAL_KINDS3);
|
|
19606
|
+
var RECORD_VISIBILITIES3 = ["private", "team", "organization"];
|
|
19607
|
+
new Set(RECORD_VISIBILITIES3);
|
|
19608
|
+
var RECORD_LIST_VIEWS3 = ["mine", "shared", "organization"];
|
|
19609
|
+
new Set(RECORD_LIST_VIEWS3);
|
|
19458
19610
|
var VERSION3 = "1.0.0";
|
|
19459
19611
|
var GTM_SKILLS3 = [
|
|
19460
19612
|
// ── research ────────────────────────────────────────────────────────
|
|
@@ -23170,14 +23322,47 @@ async function runStart(deps, options) {
|
|
|
23170
23322
|
});
|
|
23171
23323
|
}
|
|
23172
23324
|
await loop.start();
|
|
23173
|
-
|
|
23174
|
-
|
|
23175
|
-
|
|
23176
|
-
|
|
23177
|
-
|
|
23178
|
-
|
|
23179
|
-
|
|
23180
|
-
|
|
23325
|
+
let shuttingDown = false;
|
|
23326
|
+
const startLanes = /* @__PURE__ */ __name(async () => {
|
|
23327
|
+
if (worker) {
|
|
23328
|
+
await worker.start();
|
|
23329
|
+
}
|
|
23330
|
+
if (attendedWorker) {
|
|
23331
|
+
await attendedWorker.start();
|
|
23332
|
+
deps.out(
|
|
23333
|
+
runtime.attended?.captureBackend ? "Live view enabled \u2014 the owner can watch this machine from the dashboard (screen and terminal)" : "Live view enabled \u2014 terminal only: no browser was found here to show a screen"
|
|
23334
|
+
);
|
|
23335
|
+
}
|
|
23336
|
+
}, "startLanes");
|
|
23337
|
+
let lanesStarted = Promise.resolve();
|
|
23338
|
+
let laneStartFailed = false;
|
|
23339
|
+
let laneStartFailure = void 0;
|
|
23340
|
+
if (worker || attendedWorker) {
|
|
23341
|
+
if (loop.getState().state === "connected") {
|
|
23342
|
+
await startLanes();
|
|
23343
|
+
} else {
|
|
23344
|
+
deps.out(
|
|
23345
|
+
"Waiting for the platform to accept a heartbeat before leasing work (its answer carries this node\u2019s limits and version verdict)"
|
|
23346
|
+
);
|
|
23347
|
+
lanesStarted = startLanesAfterAcceptedHeartbeat(
|
|
23348
|
+
loop,
|
|
23349
|
+
async () => {
|
|
23350
|
+
deps.io.logger.info("Heartbeat accepted \u2014 starting the worker lanes");
|
|
23351
|
+
await startLanes();
|
|
23352
|
+
},
|
|
23353
|
+
() => !shuttingDown
|
|
23354
|
+
).then((outcome) => {
|
|
23355
|
+
if (!outcome.failed) return;
|
|
23356
|
+
laneStartFailed = true;
|
|
23357
|
+
laneStartFailure = outcome.error;
|
|
23358
|
+
deps.io.logger.error(
|
|
23359
|
+
`Worker lanes failed to start after the first accepted heartbeat: ${describeError4(
|
|
23360
|
+
outcome.error
|
|
23361
|
+
)} \u2014 shutting down`
|
|
23362
|
+
);
|
|
23363
|
+
requestShutdown();
|
|
23364
|
+
});
|
|
23365
|
+
}
|
|
23181
23366
|
}
|
|
23182
23367
|
const reaper = worker && runtime.workspaceRoot ? startWorkspaceReaperTimer({
|
|
23183
23368
|
rootPath: runtime.workspaceRoot,
|
|
@@ -23215,8 +23400,10 @@ async function runStart(deps, options) {
|
|
|
23215
23400
|
}
|
|
23216
23401
|
await (deps.waitForShutdown?.() ?? (deps.signals ? signalled : Promise.resolve()));
|
|
23217
23402
|
reaper?.stop();
|
|
23403
|
+
shuttingDown = true;
|
|
23218
23404
|
loop.stop();
|
|
23219
23405
|
await loop.settled();
|
|
23406
|
+
await lanesStarted;
|
|
23220
23407
|
if (attendedWorker) {
|
|
23221
23408
|
for (const jobId of attendedWorker.getState().activeJobIds) {
|
|
23222
23409
|
attendedWorker.cancelJob(jobId, "This machine is shutting down");
|
|
@@ -23229,8 +23416,29 @@ async function runStart(deps, options) {
|
|
|
23229
23416
|
deps.out(`Worker host stopped (completed ${state.completed}, failed ${state.failed}).`);
|
|
23230
23417
|
}
|
|
23231
23418
|
deps.out("Stopped.");
|
|
23419
|
+
if (laneStartFailed) {
|
|
23420
|
+
throw new CliError(
|
|
23421
|
+
`Worker lanes failed to start after the first accepted heartbeat: ${describeError4(laneStartFailure)}`,
|
|
23422
|
+
EXIT_FAILURE
|
|
23423
|
+
);
|
|
23424
|
+
}
|
|
23232
23425
|
}
|
|
23233
23426
|
__name(runStart, "runStart");
|
|
23427
|
+
async function startLanesAfterAcceptedHeartbeat(loop, startLanes, shouldWait) {
|
|
23428
|
+
const accepted = await untilHeartbeatAccepted(loop, shouldWait);
|
|
23429
|
+
if (!accepted || !shouldWait()) return { started: false, failed: false };
|
|
23430
|
+
try {
|
|
23431
|
+
await startLanes();
|
|
23432
|
+
return { started: true, failed: false };
|
|
23433
|
+
} catch (error) {
|
|
23434
|
+
return { started: false, failed: true, error };
|
|
23435
|
+
}
|
|
23436
|
+
}
|
|
23437
|
+
__name(startLanesAfterAcceptedHeartbeat, "startLanesAfterAcceptedHeartbeat");
|
|
23438
|
+
function describeError4(error) {
|
|
23439
|
+
return error instanceof Error ? error.message : String(error);
|
|
23440
|
+
}
|
|
23441
|
+
__name(describeError4, "describeError");
|
|
23234
23442
|
function parseConcurrency(raw) {
|
|
23235
23443
|
if (raw === void 0) return void 0;
|
|
23236
23444
|
const parsed = Number.parseInt(raw, 10);
|
|
@@ -23897,7 +24105,7 @@ function createLogger(options = {}) {
|
|
|
23897
24105
|
__name(createLogger, "createLogger");
|
|
23898
24106
|
|
|
23899
24107
|
// src/version.ts
|
|
23900
|
-
var NODE_APP_VERSION = "0.2.
|
|
24108
|
+
var NODE_APP_VERSION = "0.2.4";
|
|
23901
24109
|
|
|
23902
24110
|
// src/cli.ts
|
|
23903
24111
|
function buildDeps() {
|