kiro-spec-engine 1.47.10 → 1.47.12
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.
|
@@ -16,6 +16,23 @@ const path = require('path');
|
|
|
16
16
|
const fsUtils = require('../utils/fs-utils');
|
|
17
17
|
|
|
18
18
|
const SPECS_DIR = '.kiro/specs';
|
|
19
|
+
const DEFAULT_RATE_LIMIT_MAX_RETRIES = 6;
|
|
20
|
+
const DEFAULT_RATE_LIMIT_BACKOFF_BASE_MS = 1000;
|
|
21
|
+
const DEFAULT_RATE_LIMIT_BACKOFF_MAX_MS = 30000;
|
|
22
|
+
const DEFAULT_RATE_LIMIT_ADAPTIVE_PARALLEL = true;
|
|
23
|
+
const DEFAULT_RATE_LIMIT_PARALLEL_FLOOR = 1;
|
|
24
|
+
const DEFAULT_RATE_LIMIT_COOLDOWN_MS = 30000;
|
|
25
|
+
const RATE_LIMIT_BACKOFF_JITTER_RATIO = 0.5;
|
|
26
|
+
const RATE_LIMIT_ERROR_PATTERNS = [
|
|
27
|
+
/(^|[^0-9])429([^0-9]|$)/i,
|
|
28
|
+
/too many requests/i,
|
|
29
|
+
/rate[\s-]?limit/i,
|
|
30
|
+
/resource exhausted/i,
|
|
31
|
+
/quota exceeded/i,
|
|
32
|
+
/exceeded.*quota/i,
|
|
33
|
+
/requests per minute/i,
|
|
34
|
+
/tokens per minute/i,
|
|
35
|
+
];
|
|
19
36
|
|
|
20
37
|
class OrchestrationEngine extends EventEmitter {
|
|
21
38
|
/**
|
|
@@ -54,6 +71,28 @@ class OrchestrationEngine extends EventEmitter {
|
|
|
54
71
|
this._stopped = false;
|
|
55
72
|
/** @type {object|null} execution plan */
|
|
56
73
|
this._executionPlan = null;
|
|
74
|
+
/** @type {number} max retries for rate-limit failures */
|
|
75
|
+
this._rateLimitMaxRetries = DEFAULT_RATE_LIMIT_MAX_RETRIES;
|
|
76
|
+
/** @type {number} base delay for rate-limit retries */
|
|
77
|
+
this._rateLimitBackoffBaseMs = DEFAULT_RATE_LIMIT_BACKOFF_BASE_MS;
|
|
78
|
+
/** @type {number} max delay for rate-limit retries */
|
|
79
|
+
this._rateLimitBackoffMaxMs = DEFAULT_RATE_LIMIT_BACKOFF_MAX_MS;
|
|
80
|
+
/** @type {boolean} enable adaptive parallel throttling on rate-limit signals */
|
|
81
|
+
this._rateLimitAdaptiveParallel = DEFAULT_RATE_LIMIT_ADAPTIVE_PARALLEL;
|
|
82
|
+
/** @type {number} minimum effective parallelism during rate-limit cooldown */
|
|
83
|
+
this._rateLimitParallelFloor = DEFAULT_RATE_LIMIT_PARALLEL_FLOOR;
|
|
84
|
+
/** @type {number} cooldown before each adaptive parallel recovery step */
|
|
85
|
+
this._rateLimitCooldownMs = DEFAULT_RATE_LIMIT_COOLDOWN_MS;
|
|
86
|
+
/** @type {number|null} configured max parallel for current run */
|
|
87
|
+
this._baseMaxParallel = null;
|
|
88
|
+
/** @type {number|null} dynamic effective parallel limit for current run */
|
|
89
|
+
this._effectiveMaxParallel = null;
|
|
90
|
+
/** @type {number} timestamp after which recovery can step up */
|
|
91
|
+
this._rateLimitCooldownUntil = 0;
|
|
92
|
+
/** @type {() => number} */
|
|
93
|
+
this._random = typeof options.random === 'function' ? options.random : Math.random;
|
|
94
|
+
/** @type {() => number} */
|
|
95
|
+
this._now = typeof options.now === 'function' ? options.now : Date.now;
|
|
57
96
|
}
|
|
58
97
|
|
|
59
98
|
// ---------------------------------------------------------------------------
|
|
@@ -135,8 +174,10 @@ class OrchestrationEngine extends EventEmitter {
|
|
|
135
174
|
|
|
136
175
|
// Get config for maxParallel and maxRetries
|
|
137
176
|
const config = await this._orchestratorConfig.getConfig();
|
|
177
|
+
this._applyRetryPolicyConfig(config);
|
|
138
178
|
const maxParallel = options.maxParallel || config.maxParallel || 3;
|
|
139
179
|
const maxRetries = config.maxRetries || 2;
|
|
180
|
+
this._initializeAdaptiveParallel(maxParallel);
|
|
140
181
|
|
|
141
182
|
// Step 5: Execute batches (Req 3.4)
|
|
142
183
|
await this._executeBatches(batches, maxParallel, maxRetries);
|
|
@@ -250,7 +291,11 @@ class OrchestrationEngine extends EventEmitter {
|
|
|
250
291
|
const inFlight = new Map(); // specName → Promise
|
|
251
292
|
|
|
252
293
|
const launchNext = async () => {
|
|
253
|
-
while (
|
|
294
|
+
while (
|
|
295
|
+
pending.length > 0
|
|
296
|
+
&& inFlight.size < this._getEffectiveMaxParallel(maxParallel)
|
|
297
|
+
&& !this._stopped
|
|
298
|
+
) {
|
|
254
299
|
const specName = pending.shift();
|
|
255
300
|
if (this._skippedSpecs.has(specName)) continue;
|
|
256
301
|
|
|
@@ -400,25 +445,47 @@ class OrchestrationEngine extends EventEmitter {
|
|
|
400
445
|
* @private
|
|
401
446
|
*/
|
|
402
447
|
async _handleSpecFailed(specName, agentId, maxRetries, error) {
|
|
448
|
+
const resolvedError = `${error || 'Unknown error'}`;
|
|
403
449
|
const retryCount = this._retryCounts.get(specName) || 0;
|
|
450
|
+
const isRateLimitError = this._isRateLimitError(resolvedError);
|
|
451
|
+
const retryLimit = isRateLimitError
|
|
452
|
+
? Math.max(maxRetries, this._rateLimitMaxRetries || DEFAULT_RATE_LIMIT_MAX_RETRIES)
|
|
453
|
+
: maxRetries;
|
|
404
454
|
|
|
405
|
-
if (retryCount <
|
|
455
|
+
if (retryCount < retryLimit && !this._stopped) {
|
|
406
456
|
// Retry (Req 5.2)
|
|
407
457
|
this._retryCounts.set(specName, retryCount + 1);
|
|
408
458
|
this._statusMonitor.incrementRetry(specName);
|
|
409
|
-
this._statusMonitor.updateSpecStatus(specName, 'pending', null,
|
|
459
|
+
this._statusMonitor.updateSpecStatus(specName, 'pending', null, resolvedError);
|
|
460
|
+
|
|
461
|
+
const retryDelayMs = isRateLimitError
|
|
462
|
+
? this._calculateRateLimitBackoffMs(retryCount)
|
|
463
|
+
: 0;
|
|
464
|
+
if (retryDelayMs > 0) {
|
|
465
|
+
this._onRateLimitSignal();
|
|
466
|
+
this.emit('spec:rate-limited', {
|
|
467
|
+
specName,
|
|
468
|
+
retryCount,
|
|
469
|
+
retryDelayMs,
|
|
470
|
+
error: resolvedError,
|
|
471
|
+
});
|
|
472
|
+
await this._sleep(retryDelayMs);
|
|
473
|
+
if (this._stopped) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
410
477
|
|
|
411
478
|
// Re-execute
|
|
412
479
|
await this._executeSpec(specName, maxRetries);
|
|
413
480
|
} else {
|
|
414
481
|
// Final failure (Req 5.3)
|
|
415
482
|
this._failedSpecs.add(specName);
|
|
416
|
-
this._statusMonitor.updateSpecStatus(specName, 'failed', agentId,
|
|
483
|
+
this._statusMonitor.updateSpecStatus(specName, 'failed', agentId, resolvedError);
|
|
417
484
|
|
|
418
485
|
// Sync external status
|
|
419
486
|
await this._syncExternalSafe(specName, 'failed');
|
|
420
487
|
|
|
421
|
-
this.emit('spec:failed', { specName, agentId, error, retryCount });
|
|
488
|
+
this.emit('spec:failed', { specName, agentId, error: resolvedError, retryCount });
|
|
422
489
|
|
|
423
490
|
// Propagate failure to dependents (Req 3.6)
|
|
424
491
|
this._propagateFailure(specName);
|
|
@@ -537,6 +604,243 @@ class OrchestrationEngine extends EventEmitter {
|
|
|
537
604
|
// Validation & Helpers
|
|
538
605
|
// ---------------------------------------------------------------------------
|
|
539
606
|
|
|
607
|
+
/**
|
|
608
|
+
* Resolve retry-related runtime config with safe defaults.
|
|
609
|
+
*
|
|
610
|
+
* @param {object} config
|
|
611
|
+
* @private
|
|
612
|
+
*/
|
|
613
|
+
_applyRetryPolicyConfig(config) {
|
|
614
|
+
this._rateLimitMaxRetries = this._toNonNegativeInteger(
|
|
615
|
+
config && config.rateLimitMaxRetries,
|
|
616
|
+
DEFAULT_RATE_LIMIT_MAX_RETRIES
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
const baseMs = this._toPositiveInteger(
|
|
620
|
+
config && config.rateLimitBackoffBaseMs,
|
|
621
|
+
DEFAULT_RATE_LIMIT_BACKOFF_BASE_MS
|
|
622
|
+
);
|
|
623
|
+
const maxMs = this._toPositiveInteger(
|
|
624
|
+
config && config.rateLimitBackoffMaxMs,
|
|
625
|
+
DEFAULT_RATE_LIMIT_BACKOFF_MAX_MS
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
this._rateLimitBackoffBaseMs = Math.min(baseMs, maxMs);
|
|
629
|
+
this._rateLimitBackoffMaxMs = Math.max(baseMs, maxMs);
|
|
630
|
+
this._rateLimitAdaptiveParallel = this._toBoolean(
|
|
631
|
+
config && config.rateLimitAdaptiveParallel,
|
|
632
|
+
DEFAULT_RATE_LIMIT_ADAPTIVE_PARALLEL
|
|
633
|
+
);
|
|
634
|
+
this._rateLimitParallelFloor = this._toPositiveInteger(
|
|
635
|
+
config && config.rateLimitParallelFloor,
|
|
636
|
+
DEFAULT_RATE_LIMIT_PARALLEL_FLOOR
|
|
637
|
+
);
|
|
638
|
+
this._rateLimitCooldownMs = this._toPositiveInteger(
|
|
639
|
+
config && config.rateLimitCooldownMs,
|
|
640
|
+
DEFAULT_RATE_LIMIT_COOLDOWN_MS
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* @param {number} maxParallel
|
|
646
|
+
* @private
|
|
647
|
+
*/
|
|
648
|
+
_initializeAdaptiveParallel(maxParallel) {
|
|
649
|
+
const boundedMax = this._toPositiveInteger(maxParallel, 1);
|
|
650
|
+
this._baseMaxParallel = boundedMax;
|
|
651
|
+
this._effectiveMaxParallel = boundedMax;
|
|
652
|
+
this._rateLimitCooldownUntil = 0;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* @param {number} maxParallel
|
|
657
|
+
* @returns {number}
|
|
658
|
+
* @private
|
|
659
|
+
*/
|
|
660
|
+
_getEffectiveMaxParallel(maxParallel) {
|
|
661
|
+
const boundedMax = this._toPositiveInteger(maxParallel, 1);
|
|
662
|
+
if (!this._isAdaptiveParallelEnabled()) {
|
|
663
|
+
this._baseMaxParallel = boundedMax;
|
|
664
|
+
return boundedMax;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
this._baseMaxParallel = boundedMax;
|
|
668
|
+
this._maybeRecoverParallelLimit(boundedMax);
|
|
669
|
+
|
|
670
|
+
const floor = Math.min(
|
|
671
|
+
boundedMax,
|
|
672
|
+
this._toPositiveInteger(this._rateLimitParallelFloor, DEFAULT_RATE_LIMIT_PARALLEL_FLOOR)
|
|
673
|
+
);
|
|
674
|
+
const effective = this._toPositiveInteger(this._effectiveMaxParallel, boundedMax);
|
|
675
|
+
return Math.max(floor, Math.min(boundedMax, effective));
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* @private
|
|
680
|
+
*/
|
|
681
|
+
_onRateLimitSignal() {
|
|
682
|
+
if (!this._isAdaptiveParallelEnabled()) {
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const base = this._toPositiveInteger(this._baseMaxParallel, 1);
|
|
687
|
+
const current = this._toPositiveInteger(this._effectiveMaxParallel, base);
|
|
688
|
+
const floor = Math.min(
|
|
689
|
+
base,
|
|
690
|
+
this._toPositiveInteger(this._rateLimitParallelFloor, DEFAULT_RATE_LIMIT_PARALLEL_FLOOR)
|
|
691
|
+
);
|
|
692
|
+
const next = Math.max(floor, Math.floor(current / 2));
|
|
693
|
+
|
|
694
|
+
if (next < current) {
|
|
695
|
+
this._effectiveMaxParallel = next;
|
|
696
|
+
this.emit('parallel:throttled', {
|
|
697
|
+
reason: 'rate-limit',
|
|
698
|
+
previousMaxParallel: current,
|
|
699
|
+
effectiveMaxParallel: next,
|
|
700
|
+
floor,
|
|
701
|
+
});
|
|
702
|
+
} else {
|
|
703
|
+
this._effectiveMaxParallel = current;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
this._rateLimitCooldownUntil = this._getNow() + this._rateLimitCooldownMs;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* @param {number} maxParallel
|
|
711
|
+
* @private
|
|
712
|
+
*/
|
|
713
|
+
_maybeRecoverParallelLimit(maxParallel) {
|
|
714
|
+
if (!this._isAdaptiveParallelEnabled()) {
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const boundedMax = this._toPositiveInteger(maxParallel, 1);
|
|
719
|
+
const current = this._toPositiveInteger(this._effectiveMaxParallel, boundedMax);
|
|
720
|
+
if (current >= boundedMax) {
|
|
721
|
+
this._effectiveMaxParallel = boundedMax;
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (this._getNow() < this._rateLimitCooldownUntil) {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const next = Math.min(boundedMax, current + 1);
|
|
730
|
+
if (next > current) {
|
|
731
|
+
this._effectiveMaxParallel = next;
|
|
732
|
+
this._rateLimitCooldownUntil = this._getNow() + this._rateLimitCooldownMs;
|
|
733
|
+
this.emit('parallel:recovered', {
|
|
734
|
+
previousMaxParallel: current,
|
|
735
|
+
effectiveMaxParallel: next,
|
|
736
|
+
maxParallel: boundedMax,
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* @returns {boolean}
|
|
743
|
+
* @private
|
|
744
|
+
*/
|
|
745
|
+
_isAdaptiveParallelEnabled() {
|
|
746
|
+
if (typeof this._rateLimitAdaptiveParallel === 'boolean') {
|
|
747
|
+
return this._rateLimitAdaptiveParallel;
|
|
748
|
+
}
|
|
749
|
+
return DEFAULT_RATE_LIMIT_ADAPTIVE_PARALLEL;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* @returns {number}
|
|
754
|
+
* @private
|
|
755
|
+
*/
|
|
756
|
+
_getNow() {
|
|
757
|
+
return typeof this._now === 'function' ? this._now() : Date.now();
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* @param {any} value
|
|
762
|
+
* @param {boolean} fallback
|
|
763
|
+
* @returns {boolean}
|
|
764
|
+
* @private
|
|
765
|
+
*/
|
|
766
|
+
_toBoolean(value, fallback) {
|
|
767
|
+
if (typeof value === 'boolean') {
|
|
768
|
+
return value;
|
|
769
|
+
}
|
|
770
|
+
return fallback;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* @param {any} value
|
|
775
|
+
* @param {number} fallback
|
|
776
|
+
* @returns {number}
|
|
777
|
+
* @private
|
|
778
|
+
*/
|
|
779
|
+
_toPositiveInteger(value, fallback) {
|
|
780
|
+
const numeric = Number(value);
|
|
781
|
+
if (!Number.isFinite(numeric) || numeric <= 0) {
|
|
782
|
+
return fallback;
|
|
783
|
+
}
|
|
784
|
+
return Math.floor(numeric);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* @param {any} value
|
|
789
|
+
* @param {number} fallback
|
|
790
|
+
* @returns {number}
|
|
791
|
+
* @private
|
|
792
|
+
*/
|
|
793
|
+
_toNonNegativeInteger(value, fallback) {
|
|
794
|
+
const numeric = Number(value);
|
|
795
|
+
if (!Number.isFinite(numeric) || numeric < 0) {
|
|
796
|
+
return fallback;
|
|
797
|
+
}
|
|
798
|
+
return Math.floor(numeric);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* @param {string} error
|
|
803
|
+
* @returns {boolean}
|
|
804
|
+
* @private
|
|
805
|
+
*/
|
|
806
|
+
_isRateLimitError(error) {
|
|
807
|
+
return RATE_LIMIT_ERROR_PATTERNS.some(pattern => pattern.test(`${error || ''}`));
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* @param {number} retryCount
|
|
812
|
+
* @returns {number}
|
|
813
|
+
* @private
|
|
814
|
+
*/
|
|
815
|
+
_calculateRateLimitBackoffMs(retryCount) {
|
|
816
|
+
const exponent = Math.max(0, retryCount);
|
|
817
|
+
const cappedBaseDelay = Math.min(
|
|
818
|
+
this._rateLimitBackoffMaxMs || DEFAULT_RATE_LIMIT_BACKOFF_MAX_MS,
|
|
819
|
+
(this._rateLimitBackoffBaseMs || DEFAULT_RATE_LIMIT_BACKOFF_BASE_MS) * (2 ** exponent)
|
|
820
|
+
);
|
|
821
|
+
|
|
822
|
+
const randomValue = typeof this._random === 'function' ? this._random() : Math.random();
|
|
823
|
+
const normalizedRandom = Number.isFinite(randomValue)
|
|
824
|
+
? Math.min(1, Math.max(0, randomValue))
|
|
825
|
+
: 0.5;
|
|
826
|
+
const jitterFactor = (1 - RATE_LIMIT_BACKOFF_JITTER_RATIO)
|
|
827
|
+
+ (normalizedRandom * RATE_LIMIT_BACKOFF_JITTER_RATIO);
|
|
828
|
+
|
|
829
|
+
return Math.max(1, Math.round(cappedBaseDelay * jitterFactor));
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* @param {number} ms
|
|
834
|
+
* @returns {Promise<void>}
|
|
835
|
+
* @private
|
|
836
|
+
*/
|
|
837
|
+
_sleep(ms) {
|
|
838
|
+
if (!ms || ms <= 0) {
|
|
839
|
+
return Promise.resolve();
|
|
840
|
+
}
|
|
841
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
842
|
+
}
|
|
843
|
+
|
|
540
844
|
/**
|
|
541
845
|
* Validate that all spec directories exist (Req 6.4).
|
|
542
846
|
*
|
|
@@ -625,6 +929,9 @@ class OrchestrationEngine extends EventEmitter {
|
|
|
625
929
|
this._completedSpecs.clear();
|
|
626
930
|
this._executionPlan = null;
|
|
627
931
|
this._stopped = false;
|
|
932
|
+
this._baseMaxParallel = null;
|
|
933
|
+
this._effectiveMaxParallel = null;
|
|
934
|
+
this._rateLimitCooldownUntil = 0;
|
|
628
935
|
}
|
|
629
936
|
}
|
|
630
937
|
|
|
@@ -24,6 +24,12 @@ const KNOWN_KEYS = new Set([
|
|
|
24
24
|
'maxParallel',
|
|
25
25
|
'timeoutSeconds',
|
|
26
26
|
'maxRetries',
|
|
27
|
+
'rateLimitMaxRetries',
|
|
28
|
+
'rateLimitBackoffBaseMs',
|
|
29
|
+
'rateLimitBackoffMaxMs',
|
|
30
|
+
'rateLimitAdaptiveParallel',
|
|
31
|
+
'rateLimitParallelFloor',
|
|
32
|
+
'rateLimitCooldownMs',
|
|
27
33
|
'apiKeyEnvVar',
|
|
28
34
|
'bootstrapTemplate',
|
|
29
35
|
'codexArgs',
|
|
@@ -36,6 +42,12 @@ const DEFAULT_CONFIG = Object.freeze({
|
|
|
36
42
|
maxParallel: 3,
|
|
37
43
|
timeoutSeconds: 600,
|
|
38
44
|
maxRetries: 2,
|
|
45
|
+
rateLimitMaxRetries: 6,
|
|
46
|
+
rateLimitBackoffBaseMs: 1000,
|
|
47
|
+
rateLimitBackoffMaxMs: 30000,
|
|
48
|
+
rateLimitAdaptiveParallel: true,
|
|
49
|
+
rateLimitParallelFloor: 1,
|
|
50
|
+
rateLimitCooldownMs: 30000,
|
|
39
51
|
apiKeyEnvVar: 'CODEX_API_KEY',
|
|
40
52
|
bootstrapTemplate: null,
|
|
41
53
|
codexArgs: [],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kiro-spec-engine",
|
|
3
|
-
"version": "1.47.
|
|
3
|
+
"version": "1.47.12",
|
|
4
4
|
"description": "kiro-spec-engine (kse) - A CLI tool and npm package for spec-driven development with AI coding assistants. NOT the Kiro IDE desktop application.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|