@forgezero/agent 0.1.114 → 0.1.116

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.
@@ -917,7 +917,7 @@ async function postSignedNode(options, path, body) {
917
917
  }
918
918
 
919
919
  // src/version.ts
920
- var VERSION3 = "0.1.114";
920
+ var VERSION3 = "0.1.116";
921
921
 
922
922
  // src/agent-heartbeat.ts
923
923
  function readAgentHostMetrics() {
package/dist/bootstrap.js CHANGED
@@ -1460,7 +1460,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
1460
1460
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
1461
1461
 
1462
1462
  // src/version.ts
1463
- var VERSION = "0.1.114";
1463
+ var VERSION = "0.1.116";
1464
1464
 
1465
1465
  // src/software.ts
1466
1466
  var PINNED_BUN_VERSION = "1.3.14";
@@ -1504,6 +1504,44 @@ var DOCKER_DAEMON_CONFIG = `${JSON.stringify({
1504
1504
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync, realpathSync, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
1505
1505
  import { dirname as dirname3, join as join2, resolve as resolve3, sep } from "path";
1506
1506
 
1507
+ // src/capacity-calibration.ts
1508
+ function localCalibrationEndpoint(value) {
1509
+ const endpoint2 = new URL(value);
1510
+ if (endpoint2.protocol !== "http:" || !["localhost", "127.0.0.1", "[::1]"].includes(endpoint2.hostname) || !endpoint2.port || endpoint2.username || endpoint2.password || endpoint2.hash) {
1511
+ throw new Error("Capacity calibration requires an explicit loopback HTTP endpoint and port.");
1512
+ }
1513
+ return endpoint2;
1514
+ }
1515
+ function validateCapacityCalibrationOptions(options) {
1516
+ const endpoint2 = localCalibrationEndpoint(options.endpoint);
1517
+ const maxConcurrency = options.maxConcurrency ?? 256;
1518
+ const requestsPerWorker = options.requestsPerWorker ?? 8;
1519
+ const maximumRequestsPerWorker = options.maximumRequestsPerWorker ?? 100;
1520
+ const maxP95Ms = options.maxP95Ms ?? 250;
1521
+ const maxErrorRate = options.maxErrorRate ?? 0.01;
1522
+ const safetyRatio = options.safetyRatio ?? 0.8;
1523
+ const requestTimeoutMs = options.requestTimeoutMs ?? 5000;
1524
+ const minimumStageDurationMs = options.minimumStageDurationMs ?? 5000;
1525
+ const maxCpuUtilizationPercent = options.maxCpuUtilizationPercent ?? 90;
1526
+ const maxMemoryUtilizationPercent = options.maxMemoryUtilizationPercent ?? 90;
1527
+ if (!Number.isSafeInteger(maxConcurrency) || maxConcurrency < 1 || maxConcurrency > 4096 || !Number.isSafeInteger(requestsPerWorker) || requestsPerWorker < 2 || requestsPerWorker > 100 || !Number.isSafeInteger(maximumRequestsPerWorker) || maximumRequestsPerWorker < requestsPerWorker || maximumRequestsPerWorker > 100 || !Number.isFinite(maxP95Ms) || maxP95Ms < 1 || !Number.isFinite(maxErrorRate) || maxErrorRate < 0 || maxErrorRate > 0.2 || !Number.isFinite(safetyRatio) || safetyRatio < 0.25 || safetyRatio > 0.95 || !Number.isSafeInteger(requestTimeoutMs) || requestTimeoutMs < 100 || requestTimeoutMs > 30000 || !Number.isSafeInteger(minimumStageDurationMs) || minimumStageDurationMs < 100 || minimumStageDurationMs > 60000 || !Number.isFinite(maxCpuUtilizationPercent) || maxCpuUtilizationPercent < 25 || maxCpuUtilizationPercent > 100 || !Number.isFinite(maxMemoryUtilizationPercent) || maxMemoryUtilizationPercent < 25 || maxMemoryUtilizationPercent > 100) {
1528
+ throw new Error("Capacity calibration bounds are invalid.");
1529
+ }
1530
+ return {
1531
+ endpoint: endpoint2,
1532
+ maxConcurrency,
1533
+ requestsPerWorker,
1534
+ maximumRequestsPerWorker,
1535
+ maxP95Ms,
1536
+ maxErrorRate,
1537
+ safetyRatio,
1538
+ requestTimeoutMs,
1539
+ minimumStageDurationMs,
1540
+ maxCpuUtilizationPercent,
1541
+ maxMemoryUtilizationPercent
1542
+ };
1543
+ }
1544
+
1507
1545
  // src/definition.ts
1508
1546
  var RESERVED_STEP_ENV = new Set([
1509
1547
  "PATH",
@@ -3089,6 +3127,9 @@ function platformGenesisClaims(guests, sshPublicKeys) {
3089
3127
  throw new Error("platform genesis requires at least one operator SSH public key");
3090
3128
  return reviewed.map((guest) => claimFor(guest, keys, "create"));
3091
3129
  }
3130
+ function platformGenesisDeletionClaims(guests) {
3131
+ return validatePlatformGenesisGuests(guests).map((guest) => claimFor(guest, [], "delete"));
3132
+ }
3092
3133
  function platformCommunityRehearsalClaims(seedGuests, rehearsalGuest, sshPublicKeys) {
3093
3134
  const reviewed = validatePlatformGenesisGuests([...seedGuests, rehearsalGuest], 4);
3094
3135
  const keys = [...new Set(sshPublicKeys.map(publicKey))];
@@ -39,7 +39,10 @@ export interface CapacityCalibration {
39
39
  export interface CapacityCalibrationOptions {
40
40
  endpoint: string;
41
41
  maxConcurrency?: number;
42
+ /** Minimum samples per worker before a stage may finish. */
42
43
  requestsPerWorker?: number;
44
+ /** Hard request budget per worker. Defaults to 100 and can never exceed 100. */
45
+ maximumRequestsPerWorker?: number;
43
46
  maxP95Ms?: number;
44
47
  maxErrorRate?: number;
45
48
  /** Defaults to 0.8: use at most 80% of measured sustainable RPS. */
@@ -53,6 +56,7 @@ export interface ValidatedCapacityCalibrationOptions {
53
56
  endpoint: URL;
54
57
  maxConcurrency: number;
55
58
  requestsPerWorker: number;
59
+ maximumRequestsPerWorker: number;
56
60
  maxP95Ms: number;
57
61
  maxErrorRate: number;
58
62
  safetyRatio: number;
@@ -27,6 +27,7 @@ function validateCapacityCalibrationOptions(options) {
27
27
  const endpoint = localCalibrationEndpoint(options.endpoint);
28
28
  const maxConcurrency = options.maxConcurrency ?? 256;
29
29
  const requestsPerWorker = options.requestsPerWorker ?? 8;
30
+ const maximumRequestsPerWorker = options.maximumRequestsPerWorker ?? 100;
30
31
  const maxP95Ms = options.maxP95Ms ?? 250;
31
32
  const maxErrorRate = options.maxErrorRate ?? 0.01;
32
33
  const safetyRatio = options.safetyRatio ?? 0.8;
@@ -34,13 +35,14 @@ function validateCapacityCalibrationOptions(options) {
34
35
  const minimumStageDurationMs = options.minimumStageDurationMs ?? 5000;
35
36
  const maxCpuUtilizationPercent = options.maxCpuUtilizationPercent ?? 90;
36
37
  const maxMemoryUtilizationPercent = options.maxMemoryUtilizationPercent ?? 90;
37
- if (!Number.isSafeInteger(maxConcurrency) || maxConcurrency < 1 || maxConcurrency > 4096 || !Number.isSafeInteger(requestsPerWorker) || requestsPerWorker < 2 || requestsPerWorker > 100 || !Number.isFinite(maxP95Ms) || maxP95Ms < 1 || !Number.isFinite(maxErrorRate) || maxErrorRate < 0 || maxErrorRate > 0.2 || !Number.isFinite(safetyRatio) || safetyRatio < 0.25 || safetyRatio > 0.95 || !Number.isSafeInteger(requestTimeoutMs) || requestTimeoutMs < 100 || requestTimeoutMs > 30000 || !Number.isSafeInteger(minimumStageDurationMs) || minimumStageDurationMs < 100 || minimumStageDurationMs > 60000 || !Number.isFinite(maxCpuUtilizationPercent) || maxCpuUtilizationPercent < 25 || maxCpuUtilizationPercent > 100 || !Number.isFinite(maxMemoryUtilizationPercent) || maxMemoryUtilizationPercent < 25 || maxMemoryUtilizationPercent > 100) {
38
+ if (!Number.isSafeInteger(maxConcurrency) || maxConcurrency < 1 || maxConcurrency > 4096 || !Number.isSafeInteger(requestsPerWorker) || requestsPerWorker < 2 || requestsPerWorker > 100 || !Number.isSafeInteger(maximumRequestsPerWorker) || maximumRequestsPerWorker < requestsPerWorker || maximumRequestsPerWorker > 100 || !Number.isFinite(maxP95Ms) || maxP95Ms < 1 || !Number.isFinite(maxErrorRate) || maxErrorRate < 0 || maxErrorRate > 0.2 || !Number.isFinite(safetyRatio) || safetyRatio < 0.25 || safetyRatio > 0.95 || !Number.isSafeInteger(requestTimeoutMs) || requestTimeoutMs < 100 || requestTimeoutMs > 30000 || !Number.isSafeInteger(minimumStageDurationMs) || minimumStageDurationMs < 100 || minimumStageDurationMs > 60000 || !Number.isFinite(maxCpuUtilizationPercent) || maxCpuUtilizationPercent < 25 || maxCpuUtilizationPercent > 100 || !Number.isFinite(maxMemoryUtilizationPercent) || maxMemoryUtilizationPercent < 25 || maxMemoryUtilizationPercent > 100) {
38
39
  throw new Error("Capacity calibration bounds are invalid.");
39
40
  }
40
41
  return {
41
42
  endpoint,
42
43
  maxConcurrency,
43
44
  requestsPerWorker,
45
+ maximumRequestsPerWorker,
44
46
  maxP95Ms,
45
47
  maxErrorRate,
46
48
  safetyRatio,
@@ -50,7 +52,7 @@ function validateCapacityCalibrationOptions(options) {
50
52
  maxMemoryUtilizationPercent
51
53
  };
52
54
  }
53
- async function stage(endpoint, concurrency, requestsPerWorker, timeoutMs, minimumDurationMs, fetcher, sampler) {
55
+ async function stage(endpoint, concurrency, requestsPerWorker, maximumRequestsPerWorker, timeoutMs, minimumDurationMs, fetcher, sampler) {
54
56
  const latencies = [];
55
57
  let succeeded = 0;
56
58
  let failed = 0;
@@ -59,7 +61,7 @@ async function stage(endpoint, concurrency, requestsPerWorker, timeoutMs, minimu
59
61
  const started = performance.now();
60
62
  await Promise.all(Array.from({ length: concurrency }, async () => {
61
63
  let request = 0;
62
- while (request < requestsPerWorker || performance.now() - started < minimumDurationMs) {
64
+ while (request < maximumRequestsPerWorker && (request < requestsPerWorker || performance.now() - started < minimumDurationMs)) {
63
65
  request += 1;
64
66
  const requestStarted = performance.now();
65
67
  try {
@@ -118,6 +120,7 @@ async function calibrateHttpConcurrency(options, fetcher = fetch, sampler = host
118
120
  safetyRatio,
119
121
  requestTimeoutMs: timeoutMs,
120
122
  minimumStageDurationMs,
123
+ maximumRequestsPerWorker,
121
124
  maxCpuUtilizationPercent,
122
125
  maxMemoryUtilizationPercent
123
126
  } = validateCapacityCalibrationOptions(options);
@@ -125,7 +128,7 @@ async function calibrateHttpConcurrency(options, fetcher = fetch, sampler = host
125
128
  let lastSafe;
126
129
  let stopReason = "maximum-tested";
127
130
  for (let concurrency = 1;; concurrency = Math.min(maxConcurrency, concurrency * 2)) {
128
- const measured = await stage(endpoint, concurrency, requestsPerWorker, timeoutMs, minimumStageDurationMs, fetcher, sampler);
131
+ const measured = await stage(endpoint, concurrency, requestsPerWorker, maximumRequestsPerWorker, timeoutMs, minimumStageDurationMs, fetcher, sampler);
129
132
  stages.push(measured);
130
133
  const previous = stages.at(-2);
131
134
  const throughputRegressed = Boolean(previous && concurrency > 1 && measured.successfulRequestsPerSecond < previous.successfulRequestsPerSecond * 0.9);
@@ -572,6 +572,7 @@ function validateCapacityCalibrationOptions(options) {
572
572
  const endpoint = localCalibrationEndpoint(options.endpoint);
573
573
  const maxConcurrency = options.maxConcurrency ?? 256;
574
574
  const requestsPerWorker = options.requestsPerWorker ?? 8;
575
+ const maximumRequestsPerWorker = options.maximumRequestsPerWorker ?? 100;
575
576
  const maxP95Ms = options.maxP95Ms ?? 250;
576
577
  const maxErrorRate = options.maxErrorRate ?? 0.01;
577
578
  const safetyRatio = options.safetyRatio ?? 0.8;
@@ -579,13 +580,14 @@ function validateCapacityCalibrationOptions(options) {
579
580
  const minimumStageDurationMs = options.minimumStageDurationMs ?? 5000;
580
581
  const maxCpuUtilizationPercent = options.maxCpuUtilizationPercent ?? 90;
581
582
  const maxMemoryUtilizationPercent = options.maxMemoryUtilizationPercent ?? 90;
582
- if (!Number.isSafeInteger(maxConcurrency) || maxConcurrency < 1 || maxConcurrency > 4096 || !Number.isSafeInteger(requestsPerWorker) || requestsPerWorker < 2 || requestsPerWorker > 100 || !Number.isFinite(maxP95Ms) || maxP95Ms < 1 || !Number.isFinite(maxErrorRate) || maxErrorRate < 0 || maxErrorRate > 0.2 || !Number.isFinite(safetyRatio) || safetyRatio < 0.25 || safetyRatio > 0.95 || !Number.isSafeInteger(requestTimeoutMs) || requestTimeoutMs < 100 || requestTimeoutMs > 30000 || !Number.isSafeInteger(minimumStageDurationMs) || minimumStageDurationMs < 100 || minimumStageDurationMs > 60000 || !Number.isFinite(maxCpuUtilizationPercent) || maxCpuUtilizationPercent < 25 || maxCpuUtilizationPercent > 100 || !Number.isFinite(maxMemoryUtilizationPercent) || maxMemoryUtilizationPercent < 25 || maxMemoryUtilizationPercent > 100) {
583
+ if (!Number.isSafeInteger(maxConcurrency) || maxConcurrency < 1 || maxConcurrency > 4096 || !Number.isSafeInteger(requestsPerWorker) || requestsPerWorker < 2 || requestsPerWorker > 100 || !Number.isSafeInteger(maximumRequestsPerWorker) || maximumRequestsPerWorker < requestsPerWorker || maximumRequestsPerWorker > 100 || !Number.isFinite(maxP95Ms) || maxP95Ms < 1 || !Number.isFinite(maxErrorRate) || maxErrorRate < 0 || maxErrorRate > 0.2 || !Number.isFinite(safetyRatio) || safetyRatio < 0.25 || safetyRatio > 0.95 || !Number.isSafeInteger(requestTimeoutMs) || requestTimeoutMs < 100 || requestTimeoutMs > 30000 || !Number.isSafeInteger(minimumStageDurationMs) || minimumStageDurationMs < 100 || minimumStageDurationMs > 60000 || !Number.isFinite(maxCpuUtilizationPercent) || maxCpuUtilizationPercent < 25 || maxCpuUtilizationPercent > 100 || !Number.isFinite(maxMemoryUtilizationPercent) || maxMemoryUtilizationPercent < 25 || maxMemoryUtilizationPercent > 100) {
583
584
  throw new Error("Capacity calibration bounds are invalid.");
584
585
  }
585
586
  return {
586
587
  endpoint,
587
588
  maxConcurrency,
588
589
  requestsPerWorker,
590
+ maximumRequestsPerWorker,
589
591
  maxP95Ms,
590
592
  maxErrorRate,
591
593
  safetyRatio,
@@ -595,7 +597,7 @@ function validateCapacityCalibrationOptions(options) {
595
597
  maxMemoryUtilizationPercent
596
598
  };
597
599
  }
598
- async function stage(endpoint, concurrency, requestsPerWorker, timeoutMs, minimumDurationMs, fetcher, sampler) {
600
+ async function stage(endpoint, concurrency, requestsPerWorker, maximumRequestsPerWorker, timeoutMs, minimumDurationMs, fetcher, sampler) {
599
601
  const latencies = [];
600
602
  let succeeded = 0;
601
603
  let failed = 0;
@@ -604,7 +606,7 @@ async function stage(endpoint, concurrency, requestsPerWorker, timeoutMs, minimu
604
606
  const started = performance.now();
605
607
  await Promise.all(Array.from({ length: concurrency }, async () => {
606
608
  let request = 0;
607
- while (request < requestsPerWorker || performance.now() - started < minimumDurationMs) {
609
+ while (request < maximumRequestsPerWorker && (request < requestsPerWorker || performance.now() - started < minimumDurationMs)) {
608
610
  request += 1;
609
611
  const requestStarted = performance.now();
610
612
  try {
@@ -663,6 +665,7 @@ async function calibrateHttpConcurrency(options, fetcher = fetch, sampler = host
663
665
  safetyRatio,
664
666
  requestTimeoutMs: timeoutMs,
665
667
  minimumStageDurationMs,
668
+ maximumRequestsPerWorker,
666
669
  maxCpuUtilizationPercent,
667
670
  maxMemoryUtilizationPercent
668
671
  } = validateCapacityCalibrationOptions(options);
@@ -670,7 +673,7 @@ async function calibrateHttpConcurrency(options, fetcher = fetch, sampler = host
670
673
  let lastSafe;
671
674
  let stopReason = "maximum-tested";
672
675
  for (let concurrency = 1;; concurrency = Math.min(maxConcurrency, concurrency * 2)) {
673
- const measured = await stage(endpoint, concurrency, requestsPerWorker, timeoutMs, minimumStageDurationMs, fetcher, sampler);
676
+ const measured = await stage(endpoint, concurrency, requestsPerWorker, maximumRequestsPerWorker, timeoutMs, minimumStageDurationMs, fetcher, sampler);
674
677
  stages.push(measured);
675
678
  const previous = stages.at(-2);
676
679
  const throughputRegressed = Boolean(previous && concurrency > 1 && measured.successfulRequestsPerSecond < previous.successfulRequestsPerSecond * 0.9);
@@ -771,6 +774,7 @@ function capacityCalibration(value, where) {
771
774
  "endpoint",
772
775
  "maxConcurrency",
773
776
  "requestsPerWorker",
777
+ "maximumRequestsPerWorker",
774
778
  "maxP95Ms",
775
779
  "maxErrorRate",
776
780
  "safetyRatio",
@@ -799,6 +803,7 @@ function capacityCalibration(value, where) {
799
803
  ...Object.fromEntries([
800
804
  "maxConcurrency",
801
805
  "requestsPerWorker",
806
+ "maximumRequestsPerWorker",
802
807
  "maxP95Ms",
803
808
  "maxErrorRate",
804
809
  "safetyRatio",
@@ -132,6 +132,44 @@ function validateBuiltInProvider(requirement2, where) {
132
132
  throw new Error(`${where} ArangoDB config is invalid`);
133
133
  }
134
134
 
135
+ // src/capacity-calibration.ts
136
+ function localCalibrationEndpoint(value) {
137
+ const endpoint = new URL(value);
138
+ if (endpoint.protocol !== "http:" || !["localhost", "127.0.0.1", "[::1]"].includes(endpoint.hostname) || !endpoint.port || endpoint.username || endpoint.password || endpoint.hash) {
139
+ throw new Error("Capacity calibration requires an explicit loopback HTTP endpoint and port.");
140
+ }
141
+ return endpoint;
142
+ }
143
+ function validateCapacityCalibrationOptions(options) {
144
+ const endpoint = localCalibrationEndpoint(options.endpoint);
145
+ const maxConcurrency = options.maxConcurrency ?? 256;
146
+ const requestsPerWorker = options.requestsPerWorker ?? 8;
147
+ const maximumRequestsPerWorker = options.maximumRequestsPerWorker ?? 100;
148
+ const maxP95Ms = options.maxP95Ms ?? 250;
149
+ const maxErrorRate = options.maxErrorRate ?? 0.01;
150
+ const safetyRatio = options.safetyRatio ?? 0.8;
151
+ const requestTimeoutMs = options.requestTimeoutMs ?? 5000;
152
+ const minimumStageDurationMs = options.minimumStageDurationMs ?? 5000;
153
+ const maxCpuUtilizationPercent = options.maxCpuUtilizationPercent ?? 90;
154
+ const maxMemoryUtilizationPercent = options.maxMemoryUtilizationPercent ?? 90;
155
+ if (!Number.isSafeInteger(maxConcurrency) || maxConcurrency < 1 || maxConcurrency > 4096 || !Number.isSafeInteger(requestsPerWorker) || requestsPerWorker < 2 || requestsPerWorker > 100 || !Number.isSafeInteger(maximumRequestsPerWorker) || maximumRequestsPerWorker < requestsPerWorker || maximumRequestsPerWorker > 100 || !Number.isFinite(maxP95Ms) || maxP95Ms < 1 || !Number.isFinite(maxErrorRate) || maxErrorRate < 0 || maxErrorRate > 0.2 || !Number.isFinite(safetyRatio) || safetyRatio < 0.25 || safetyRatio > 0.95 || !Number.isSafeInteger(requestTimeoutMs) || requestTimeoutMs < 100 || requestTimeoutMs > 30000 || !Number.isSafeInteger(minimumStageDurationMs) || minimumStageDurationMs < 100 || minimumStageDurationMs > 60000 || !Number.isFinite(maxCpuUtilizationPercent) || maxCpuUtilizationPercent < 25 || maxCpuUtilizationPercent > 100 || !Number.isFinite(maxMemoryUtilizationPercent) || maxMemoryUtilizationPercent < 25 || maxMemoryUtilizationPercent > 100) {
156
+ throw new Error("Capacity calibration bounds are invalid.");
157
+ }
158
+ return {
159
+ endpoint,
160
+ maxConcurrency,
161
+ requestsPerWorker,
162
+ maximumRequestsPerWorker,
163
+ maxP95Ms,
164
+ maxErrorRate,
165
+ safetyRatio,
166
+ requestTimeoutMs,
167
+ minimumStageDurationMs,
168
+ maxCpuUtilizationPercent,
169
+ maxMemoryUtilizationPercent
170
+ };
171
+ }
172
+
135
173
  // src/deploy-plan.ts
136
174
  var DEPLOY_PLAN_FORMAT = "forgezero-deployment-plan";
137
175
  var DEPLOY_PLAN_VERSION = 1;
@@ -447,7 +485,7 @@ function validateComponent(value, where, targets, requirements) {
447
485
  const row2 = object(value, where);
448
486
  const selectedTarget = targets.get(String(row2.target)) ?? fail(`${where}.target`, "must name an existing target.");
449
487
  if (row2.kind === "application") {
450
- exact3(row2, ["kind", "target", "runtime", "service", "resources", "storage", "network", "rollout"], where);
488
+ exact3(row2, ["kind", "target", "runtime", "service", "capacityCalibration", "resources", "storage", "network", "rollout"], where);
451
489
  const runtime = object(row2.runtime, `${where}.runtime`);
452
490
  const runtimeRequirement = requirements.get(String(runtime.requirement));
453
491
  if (!runtimeRequirement)
@@ -529,6 +567,33 @@ function validateComponent(value, where, targets, requirements) {
529
567
  fail(`${where}.service.websocket`, "must be a boolean.");
530
568
  if (service.maximumConnections !== undefined)
531
569
  integer(service.maximumConnections, `${where}.service.maximumConnections`, 1, 1e7);
570
+ if (row2.capacityCalibration !== undefined) {
571
+ const calibration = object(row2.capacityCalibration, `${where}.capacityCalibration`);
572
+ exact3(calibration, [
573
+ "path",
574
+ "maxConcurrency",
575
+ "requestsPerWorker",
576
+ "maximumRequestsPerWorker",
577
+ "maxP95Ms",
578
+ "maxErrorRate",
579
+ "safetyRatio",
580
+ "requestTimeoutMs",
581
+ "minimumStageDurationMs",
582
+ "maxCpuUtilizationPercent",
583
+ "maxMemoryUtilizationPercent"
584
+ ], `${where}.capacityCalibration`);
585
+ if (typeof calibration.path !== "string" || !HEALTH_PATH.test(calibration.path) || calibration.path.includes("..") || calibration.path.includes("//")) {
586
+ fail(`${where}.capacityCalibration.path`, "must be one bounded absolute loopback path.");
587
+ }
588
+ try {
589
+ validateCapacityCalibrationOptions({
590
+ ...calibration,
591
+ endpoint: `http://127.0.0.1:${String(service.port)}${calibration.path}`
592
+ });
593
+ } catch {
594
+ fail(`${where}.capacityCalibration`, "contains invalid bounds.");
595
+ }
596
+ }
532
597
  validateResources(row2.resources, `${where}.resources`);
533
598
  if (runtime.kind === "container") {
534
599
  const resources = object(row2.resources, `${where}.resources`);
@@ -991,6 +1056,15 @@ function compileDeployment(sourceValue) {
991
1056
  named(componentName, "deployment.spec.components key");
992
1057
  validateComponent(value, `deployment.spec.components.${componentName}`, targetDefinitions, requirementDefinitions);
993
1058
  }
1059
+ for (const targetName of targetNames) {
1060
+ const calibrated = componentEntries.filter(([, component2]) => {
1061
+ const candidate = component2;
1062
+ return candidate.kind === "application" && candidate.target === targetName && candidate.capacityCalibration !== undefined;
1063
+ });
1064
+ if (calibrated.length > 1) {
1065
+ fail(`deployment.spec.components`, `may calibrate at most one application on target ${targetName}.`);
1066
+ }
1067
+ }
994
1068
  for (const [targetName, target] of targetDefinitions) {
995
1069
  if (target.connectivity?.public?.mode !== "cloudflare-tunnel")
996
1070
  continue;
@@ -572,6 +572,7 @@ function validateCapacityCalibrationOptions(options) {
572
572
  const endpoint = localCalibrationEndpoint(options.endpoint);
573
573
  const maxConcurrency = options.maxConcurrency ?? 256;
574
574
  const requestsPerWorker = options.requestsPerWorker ?? 8;
575
+ const maximumRequestsPerWorker = options.maximumRequestsPerWorker ?? 100;
575
576
  const maxP95Ms = options.maxP95Ms ?? 250;
576
577
  const maxErrorRate = options.maxErrorRate ?? 0.01;
577
578
  const safetyRatio = options.safetyRatio ?? 0.8;
@@ -579,13 +580,14 @@ function validateCapacityCalibrationOptions(options) {
579
580
  const minimumStageDurationMs = options.minimumStageDurationMs ?? 5000;
580
581
  const maxCpuUtilizationPercent = options.maxCpuUtilizationPercent ?? 90;
581
582
  const maxMemoryUtilizationPercent = options.maxMemoryUtilizationPercent ?? 90;
582
- if (!Number.isSafeInteger(maxConcurrency) || maxConcurrency < 1 || maxConcurrency > 4096 || !Number.isSafeInteger(requestsPerWorker) || requestsPerWorker < 2 || requestsPerWorker > 100 || !Number.isFinite(maxP95Ms) || maxP95Ms < 1 || !Number.isFinite(maxErrorRate) || maxErrorRate < 0 || maxErrorRate > 0.2 || !Number.isFinite(safetyRatio) || safetyRatio < 0.25 || safetyRatio > 0.95 || !Number.isSafeInteger(requestTimeoutMs) || requestTimeoutMs < 100 || requestTimeoutMs > 30000 || !Number.isSafeInteger(minimumStageDurationMs) || minimumStageDurationMs < 100 || minimumStageDurationMs > 60000 || !Number.isFinite(maxCpuUtilizationPercent) || maxCpuUtilizationPercent < 25 || maxCpuUtilizationPercent > 100 || !Number.isFinite(maxMemoryUtilizationPercent) || maxMemoryUtilizationPercent < 25 || maxMemoryUtilizationPercent > 100) {
583
+ if (!Number.isSafeInteger(maxConcurrency) || maxConcurrency < 1 || maxConcurrency > 4096 || !Number.isSafeInteger(requestsPerWorker) || requestsPerWorker < 2 || requestsPerWorker > 100 || !Number.isSafeInteger(maximumRequestsPerWorker) || maximumRequestsPerWorker < requestsPerWorker || maximumRequestsPerWorker > 100 || !Number.isFinite(maxP95Ms) || maxP95Ms < 1 || !Number.isFinite(maxErrorRate) || maxErrorRate < 0 || maxErrorRate > 0.2 || !Number.isFinite(safetyRatio) || safetyRatio < 0.25 || safetyRatio > 0.95 || !Number.isSafeInteger(requestTimeoutMs) || requestTimeoutMs < 100 || requestTimeoutMs > 30000 || !Number.isSafeInteger(minimumStageDurationMs) || minimumStageDurationMs < 100 || minimumStageDurationMs > 60000 || !Number.isFinite(maxCpuUtilizationPercent) || maxCpuUtilizationPercent < 25 || maxCpuUtilizationPercent > 100 || !Number.isFinite(maxMemoryUtilizationPercent) || maxMemoryUtilizationPercent < 25 || maxMemoryUtilizationPercent > 100) {
583
584
  throw new Error("Capacity calibration bounds are invalid.");
584
585
  }
585
586
  return {
586
587
  endpoint,
587
588
  maxConcurrency,
588
589
  requestsPerWorker,
590
+ maximumRequestsPerWorker,
589
591
  maxP95Ms,
590
592
  maxErrorRate,
591
593
  safetyRatio,
@@ -595,7 +597,7 @@ function validateCapacityCalibrationOptions(options) {
595
597
  maxMemoryUtilizationPercent
596
598
  };
597
599
  }
598
- async function stage(endpoint, concurrency, requestsPerWorker, timeoutMs, minimumDurationMs, fetcher, sampler) {
600
+ async function stage(endpoint, concurrency, requestsPerWorker, maximumRequestsPerWorker, timeoutMs, minimumDurationMs, fetcher, sampler) {
599
601
  const latencies = [];
600
602
  let succeeded = 0;
601
603
  let failed = 0;
@@ -604,7 +606,7 @@ async function stage(endpoint, concurrency, requestsPerWorker, timeoutMs, minimu
604
606
  const started = performance.now();
605
607
  await Promise.all(Array.from({ length: concurrency }, async () => {
606
608
  let request = 0;
607
- while (request < requestsPerWorker || performance.now() - started < minimumDurationMs) {
609
+ while (request < maximumRequestsPerWorker && (request < requestsPerWorker || performance.now() - started < minimumDurationMs)) {
608
610
  request += 1;
609
611
  const requestStarted = performance.now();
610
612
  try {
@@ -663,6 +665,7 @@ async function calibrateHttpConcurrency(options, fetcher = fetch, sampler = host
663
665
  safetyRatio,
664
666
  requestTimeoutMs: timeoutMs,
665
667
  minimumStageDurationMs,
668
+ maximumRequestsPerWorker,
666
669
  maxCpuUtilizationPercent,
667
670
  maxMemoryUtilizationPercent
668
671
  } = validateCapacityCalibrationOptions(options);
@@ -670,7 +673,7 @@ async function calibrateHttpConcurrency(options, fetcher = fetch, sampler = host
670
673
  let lastSafe;
671
674
  let stopReason = "maximum-tested";
672
675
  for (let concurrency = 1;; concurrency = Math.min(maxConcurrency, concurrency * 2)) {
673
- const measured = await stage(endpoint, concurrency, requestsPerWorker, timeoutMs, minimumStageDurationMs, fetcher, sampler);
676
+ const measured = await stage(endpoint, concurrency, requestsPerWorker, maximumRequestsPerWorker, timeoutMs, minimumStageDurationMs, fetcher, sampler);
674
677
  stages.push(measured);
675
678
  const previous = stages.at(-2);
676
679
  const throughputRegressed = Boolean(previous && concurrency > 1 && measured.successfulRequestsPerSecond < previous.successfulRequestsPerSecond * 0.9);
@@ -771,6 +774,7 @@ function capacityCalibration(value, where) {
771
774
  "endpoint",
772
775
  "maxConcurrency",
773
776
  "requestsPerWorker",
777
+ "maximumRequestsPerWorker",
774
778
  "maxP95Ms",
775
779
  "maxErrorRate",
776
780
  "safetyRatio",
@@ -799,6 +803,7 @@ function capacityCalibration(value, where) {
799
803
  ...Object.fromEntries([
800
804
  "maxConcurrency",
801
805
  "requestsPerWorker",
806
+ "maximumRequestsPerWorker",
802
807
  "maxP95Ms",
803
808
  "maxErrorRate",
804
809
  "safetyRatio",
@@ -132,6 +132,44 @@ function validateBuiltInProvider(requirement2, where) {
132
132
  throw new Error(`${where} ArangoDB config is invalid`);
133
133
  }
134
134
 
135
+ // src/capacity-calibration.ts
136
+ function localCalibrationEndpoint(value) {
137
+ const endpoint = new URL(value);
138
+ if (endpoint.protocol !== "http:" || !["localhost", "127.0.0.1", "[::1]"].includes(endpoint.hostname) || !endpoint.port || endpoint.username || endpoint.password || endpoint.hash) {
139
+ throw new Error("Capacity calibration requires an explicit loopback HTTP endpoint and port.");
140
+ }
141
+ return endpoint;
142
+ }
143
+ function validateCapacityCalibrationOptions(options) {
144
+ const endpoint = localCalibrationEndpoint(options.endpoint);
145
+ const maxConcurrency = options.maxConcurrency ?? 256;
146
+ const requestsPerWorker = options.requestsPerWorker ?? 8;
147
+ const maximumRequestsPerWorker = options.maximumRequestsPerWorker ?? 100;
148
+ const maxP95Ms = options.maxP95Ms ?? 250;
149
+ const maxErrorRate = options.maxErrorRate ?? 0.01;
150
+ const safetyRatio = options.safetyRatio ?? 0.8;
151
+ const requestTimeoutMs = options.requestTimeoutMs ?? 5000;
152
+ const minimumStageDurationMs = options.minimumStageDurationMs ?? 5000;
153
+ const maxCpuUtilizationPercent = options.maxCpuUtilizationPercent ?? 90;
154
+ const maxMemoryUtilizationPercent = options.maxMemoryUtilizationPercent ?? 90;
155
+ if (!Number.isSafeInteger(maxConcurrency) || maxConcurrency < 1 || maxConcurrency > 4096 || !Number.isSafeInteger(requestsPerWorker) || requestsPerWorker < 2 || requestsPerWorker > 100 || !Number.isSafeInteger(maximumRequestsPerWorker) || maximumRequestsPerWorker < requestsPerWorker || maximumRequestsPerWorker > 100 || !Number.isFinite(maxP95Ms) || maxP95Ms < 1 || !Number.isFinite(maxErrorRate) || maxErrorRate < 0 || maxErrorRate > 0.2 || !Number.isFinite(safetyRatio) || safetyRatio < 0.25 || safetyRatio > 0.95 || !Number.isSafeInteger(requestTimeoutMs) || requestTimeoutMs < 100 || requestTimeoutMs > 30000 || !Number.isSafeInteger(minimumStageDurationMs) || minimumStageDurationMs < 100 || minimumStageDurationMs > 60000 || !Number.isFinite(maxCpuUtilizationPercent) || maxCpuUtilizationPercent < 25 || maxCpuUtilizationPercent > 100 || !Number.isFinite(maxMemoryUtilizationPercent) || maxMemoryUtilizationPercent < 25 || maxMemoryUtilizationPercent > 100) {
156
+ throw new Error("Capacity calibration bounds are invalid.");
157
+ }
158
+ return {
159
+ endpoint,
160
+ maxConcurrency,
161
+ requestsPerWorker,
162
+ maximumRequestsPerWorker,
163
+ maxP95Ms,
164
+ maxErrorRate,
165
+ safetyRatio,
166
+ requestTimeoutMs,
167
+ minimumStageDurationMs,
168
+ maxCpuUtilizationPercent,
169
+ maxMemoryUtilizationPercent
170
+ };
171
+ }
172
+
135
173
  // src/deploy-plan.ts
136
174
  var DEPLOY_PLAN_FORMAT = "forgezero-deployment-plan";
137
175
  var DEPLOY_PLAN_VERSION = 1;
@@ -447,7 +485,7 @@ function validateComponent(value, where, targets, requirements) {
447
485
  const row2 = object(value, where);
448
486
  const selectedTarget = targets.get(String(row2.target)) ?? fail(`${where}.target`, "must name an existing target.");
449
487
  if (row2.kind === "application") {
450
- exact3(row2, ["kind", "target", "runtime", "service", "resources", "storage", "network", "rollout"], where);
488
+ exact3(row2, ["kind", "target", "runtime", "service", "capacityCalibration", "resources", "storage", "network", "rollout"], where);
451
489
  const runtime = object(row2.runtime, `${where}.runtime`);
452
490
  const runtimeRequirement = requirements.get(String(runtime.requirement));
453
491
  if (!runtimeRequirement)
@@ -529,6 +567,33 @@ function validateComponent(value, where, targets, requirements) {
529
567
  fail(`${where}.service.websocket`, "must be a boolean.");
530
568
  if (service.maximumConnections !== undefined)
531
569
  integer(service.maximumConnections, `${where}.service.maximumConnections`, 1, 1e7);
570
+ if (row2.capacityCalibration !== undefined) {
571
+ const calibration = object(row2.capacityCalibration, `${where}.capacityCalibration`);
572
+ exact3(calibration, [
573
+ "path",
574
+ "maxConcurrency",
575
+ "requestsPerWorker",
576
+ "maximumRequestsPerWorker",
577
+ "maxP95Ms",
578
+ "maxErrorRate",
579
+ "safetyRatio",
580
+ "requestTimeoutMs",
581
+ "minimumStageDurationMs",
582
+ "maxCpuUtilizationPercent",
583
+ "maxMemoryUtilizationPercent"
584
+ ], `${where}.capacityCalibration`);
585
+ if (typeof calibration.path !== "string" || !HEALTH_PATH.test(calibration.path) || calibration.path.includes("..") || calibration.path.includes("//")) {
586
+ fail(`${where}.capacityCalibration.path`, "must be one bounded absolute loopback path.");
587
+ }
588
+ try {
589
+ validateCapacityCalibrationOptions({
590
+ ...calibration,
591
+ endpoint: `http://127.0.0.1:${String(service.port)}${calibration.path}`
592
+ });
593
+ } catch {
594
+ fail(`${where}.capacityCalibration`, "contains invalid bounds.");
595
+ }
596
+ }
532
597
  validateResources(row2.resources, `${where}.resources`);
533
598
  if (runtime.kind === "container") {
534
599
  const resources = object(row2.resources, `${where}.resources`);
@@ -991,6 +1056,15 @@ function compileDeployment(sourceValue) {
991
1056
  named(componentName, "deployment.spec.components key");
992
1057
  validateComponent(value, `deployment.spec.components.${componentName}`, targetDefinitions, requirementDefinitions);
993
1058
  }
1059
+ for (const targetName of targetNames) {
1060
+ const calibrated = componentEntries.filter(([, component2]) => {
1061
+ const candidate = component2;
1062
+ return candidate.kind === "application" && candidate.target === targetName && candidate.capacityCalibration !== undefined;
1063
+ });
1064
+ if (calibrated.length > 1) {
1065
+ fail(`deployment.spec.components`, `may calibrate at most one application on target ${targetName}.`);
1066
+ }
1067
+ }
994
1068
  for (const [targetName, target] of targetDefinitions) {
995
1069
  if (target.connectivity?.public?.mode !== "cloudflare-tunnel")
996
1070
  continue;
@@ -132,6 +132,44 @@ function validateBuiltInProvider(requirement2, where) {
132
132
  throw new Error(`${where} ArangoDB config is invalid`);
133
133
  }
134
134
 
135
+ // src/capacity-calibration.ts
136
+ function localCalibrationEndpoint(value) {
137
+ const endpoint = new URL(value);
138
+ if (endpoint.protocol !== "http:" || !["localhost", "127.0.0.1", "[::1]"].includes(endpoint.hostname) || !endpoint.port || endpoint.username || endpoint.password || endpoint.hash) {
139
+ throw new Error("Capacity calibration requires an explicit loopback HTTP endpoint and port.");
140
+ }
141
+ return endpoint;
142
+ }
143
+ function validateCapacityCalibrationOptions(options) {
144
+ const endpoint = localCalibrationEndpoint(options.endpoint);
145
+ const maxConcurrency = options.maxConcurrency ?? 256;
146
+ const requestsPerWorker = options.requestsPerWorker ?? 8;
147
+ const maximumRequestsPerWorker = options.maximumRequestsPerWorker ?? 100;
148
+ const maxP95Ms = options.maxP95Ms ?? 250;
149
+ const maxErrorRate = options.maxErrorRate ?? 0.01;
150
+ const safetyRatio = options.safetyRatio ?? 0.8;
151
+ const requestTimeoutMs = options.requestTimeoutMs ?? 5000;
152
+ const minimumStageDurationMs = options.minimumStageDurationMs ?? 5000;
153
+ const maxCpuUtilizationPercent = options.maxCpuUtilizationPercent ?? 90;
154
+ const maxMemoryUtilizationPercent = options.maxMemoryUtilizationPercent ?? 90;
155
+ if (!Number.isSafeInteger(maxConcurrency) || maxConcurrency < 1 || maxConcurrency > 4096 || !Number.isSafeInteger(requestsPerWorker) || requestsPerWorker < 2 || requestsPerWorker > 100 || !Number.isSafeInteger(maximumRequestsPerWorker) || maximumRequestsPerWorker < requestsPerWorker || maximumRequestsPerWorker > 100 || !Number.isFinite(maxP95Ms) || maxP95Ms < 1 || !Number.isFinite(maxErrorRate) || maxErrorRate < 0 || maxErrorRate > 0.2 || !Number.isFinite(safetyRatio) || safetyRatio < 0.25 || safetyRatio > 0.95 || !Number.isSafeInteger(requestTimeoutMs) || requestTimeoutMs < 100 || requestTimeoutMs > 30000 || !Number.isSafeInteger(minimumStageDurationMs) || minimumStageDurationMs < 100 || minimumStageDurationMs > 60000 || !Number.isFinite(maxCpuUtilizationPercent) || maxCpuUtilizationPercent < 25 || maxCpuUtilizationPercent > 100 || !Number.isFinite(maxMemoryUtilizationPercent) || maxMemoryUtilizationPercent < 25 || maxMemoryUtilizationPercent > 100) {
156
+ throw new Error("Capacity calibration bounds are invalid.");
157
+ }
158
+ return {
159
+ endpoint,
160
+ maxConcurrency,
161
+ requestsPerWorker,
162
+ maximumRequestsPerWorker,
163
+ maxP95Ms,
164
+ maxErrorRate,
165
+ safetyRatio,
166
+ requestTimeoutMs,
167
+ minimumStageDurationMs,
168
+ maxCpuUtilizationPercent,
169
+ maxMemoryUtilizationPercent
170
+ };
171
+ }
172
+
135
173
  // src/deploy-plan.ts
136
174
  var DEPLOY_PLAN_FORMAT = "forgezero-deployment-plan";
137
175
  var DEPLOY_PLAN_VERSION = 1;
@@ -447,7 +485,7 @@ function validateComponent(value, where, targets, requirements) {
447
485
  const row2 = object(value, where);
448
486
  const selectedTarget = targets.get(String(row2.target)) ?? fail(`${where}.target`, "must name an existing target.");
449
487
  if (row2.kind === "application") {
450
- exact3(row2, ["kind", "target", "runtime", "service", "resources", "storage", "network", "rollout"], where);
488
+ exact3(row2, ["kind", "target", "runtime", "service", "capacityCalibration", "resources", "storage", "network", "rollout"], where);
451
489
  const runtime = object(row2.runtime, `${where}.runtime`);
452
490
  const runtimeRequirement = requirements.get(String(runtime.requirement));
453
491
  if (!runtimeRequirement)
@@ -529,6 +567,33 @@ function validateComponent(value, where, targets, requirements) {
529
567
  fail(`${where}.service.websocket`, "must be a boolean.");
530
568
  if (service.maximumConnections !== undefined)
531
569
  integer(service.maximumConnections, `${where}.service.maximumConnections`, 1, 1e7);
570
+ if (row2.capacityCalibration !== undefined) {
571
+ const calibration = object(row2.capacityCalibration, `${where}.capacityCalibration`);
572
+ exact3(calibration, [
573
+ "path",
574
+ "maxConcurrency",
575
+ "requestsPerWorker",
576
+ "maximumRequestsPerWorker",
577
+ "maxP95Ms",
578
+ "maxErrorRate",
579
+ "safetyRatio",
580
+ "requestTimeoutMs",
581
+ "minimumStageDurationMs",
582
+ "maxCpuUtilizationPercent",
583
+ "maxMemoryUtilizationPercent"
584
+ ], `${where}.capacityCalibration`);
585
+ if (typeof calibration.path !== "string" || !HEALTH_PATH.test(calibration.path) || calibration.path.includes("..") || calibration.path.includes("//")) {
586
+ fail(`${where}.capacityCalibration.path`, "must be one bounded absolute loopback path.");
587
+ }
588
+ try {
589
+ validateCapacityCalibrationOptions({
590
+ ...calibration,
591
+ endpoint: `http://127.0.0.1:${String(service.port)}${calibration.path}`
592
+ });
593
+ } catch {
594
+ fail(`${where}.capacityCalibration`, "contains invalid bounds.");
595
+ }
596
+ }
532
597
  validateResources(row2.resources, `${where}.resources`);
533
598
  if (runtime.kind === "container") {
534
599
  const resources = object(row2.resources, `${where}.resources`);
@@ -991,6 +1056,15 @@ function compileDeployment(sourceValue) {
991
1056
  named(componentName, "deployment.spec.components key");
992
1057
  validateComponent(value, `deployment.spec.components.${componentName}`, targetDefinitions, requirementDefinitions);
993
1058
  }
1059
+ for (const targetName of targetNames) {
1060
+ const calibrated = componentEntries.filter(([, component2]) => {
1061
+ const candidate = component2;
1062
+ return candidate.kind === "application" && candidate.target === targetName && candidate.capacityCalibration !== undefined;
1063
+ });
1064
+ if (calibrated.length > 1) {
1065
+ fail(`deployment.spec.components`, `may calibrate at most one application on target ${targetName}.`);
1066
+ }
1067
+ }
994
1068
  for (const [targetName, target] of targetDefinitions) {
995
1069
  if (target.connectivity?.public?.mode !== "cloudflare-tunnel")
996
1070
  continue;
package/dist/deploy.d.ts CHANGED
@@ -340,6 +340,20 @@ export interface ApplicationComponent {
340
340
  websocket?: boolean;
341
341
  maximumConnections?: number;
342
342
  };
343
+ /** Optional bounded loopback workload calibration performed after a healthy release. */
344
+ capacityCalibration?: {
345
+ path: string;
346
+ maxConcurrency?: number;
347
+ requestsPerWorker?: number;
348
+ maximumRequestsPerWorker?: number;
349
+ maxP95Ms?: number;
350
+ maxErrorRate?: number;
351
+ safetyRatio?: number;
352
+ requestTimeoutMs?: number;
353
+ minimumStageDurationMs?: number;
354
+ maxCpuUtilizationPercent?: number;
355
+ maxMemoryUtilizationPercent?: number;
356
+ };
343
357
  resources: ResourceDefinition;
344
358
  storage?: readonly StorageMount[];
345
359
  network?: NetworkDefinition;