lynkr 8.0.0 → 9.0.1
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/.lynkr/telemetry.db +0 -0
- package/.lynkr/telemetry.db-shm +0 -0
- package/.lynkr/telemetry.db-wal +0 -0
- package/README.md +196 -322
- package/lynkr-skill.tar.gz +0 -0
- package/package.json +4 -3
- package/src/api/openai-router.js +64 -13
- package/src/api/providers-handler.js +171 -3
- package/src/api/router.js +9 -2
- package/src/clients/circuit-breaker.js +10 -247
- package/src/clients/codex-process.js +342 -0
- package/src/clients/codex-utils.js +143 -0
- package/src/clients/databricks.js +210 -63
- package/src/clients/resilience.js +540 -0
- package/src/clients/retry.js +22 -167
- package/src/clients/standard-tools.js +23 -0
- package/src/config/index.js +77 -0
- package/src/context/compression.js +42 -9
- package/src/context/distill.js +492 -0
- package/src/orchestrator/index.js +48 -8
- package/src/routing/complexity-analyzer.js +258 -5
- package/src/routing/index.js +12 -2
- package/src/routing/latency-tracker.js +148 -0
- package/src/routing/model-tiers.js +2 -0
- package/src/routing/quality-scorer.js +113 -0
- package/src/routing/telemetry.js +464 -0
- package/src/server.js +13 -12
- package/src/tools/code-graph.js +538 -0
- package/src/tools/code-mode.js +304 -0
- package/src/tools/index.js +4 -0
- package/src/tools/lazy-loader.js +18 -0
- package/src/tools/mcp-remote.js +7 -0
- package/src/tools/smart-selection.js +11 -0
- package/src/tools/tinyfish.js +358 -0
- package/src/tools/truncate.js +1 -0
- package/src/utils/payload.js +206 -0
- package/src/utils/perf-timer.js +80 -0
- package/.github/FUNDING.yml +0 -15
- package/.github/workflows/README.md +0 -215
- package/.github/workflows/ci.yml +0 -69
- package/.github/workflows/index.yml +0 -62
- package/.github/workflows/web-tools-tests.yml +0 -56
- package/CITATIONS.bib +0 -6
- package/DEPLOYMENT.md +0 -1001
- package/LYNKR-TUI-PLAN.md +0 -984
- package/PERFORMANCE-REPORT.md +0 -866
- package/PLAN-per-client-model-routing.md +0 -252
- package/docs/42642f749da6234f41b6b425c3bb07c9.txt +0 -1
- package/docs/BingSiteAuth.xml +0 -4
- package/docs/docs-style.css +0 -478
- package/docs/docs.html +0 -198
- package/docs/google5be250e608e6da39.html +0 -1
- package/docs/index.html +0 -577
- package/docs/index.md +0 -584
- package/docs/robots.txt +0 -4
- package/docs/sitemap.xml +0 -44
- package/docs/style.css +0 -1223
- package/docs/toon-integration-spec.md +0 -130
- package/documentation/README.md +0 -101
- package/documentation/api.md +0 -806
- package/documentation/claude-code-cli.md +0 -679
- package/documentation/codex-cli.md +0 -397
- package/documentation/contributing.md +0 -571
- package/documentation/cursor-integration.md +0 -734
- package/documentation/docker.md +0 -874
- package/documentation/embeddings.md +0 -762
- package/documentation/faq.md +0 -713
- package/documentation/features.md +0 -403
- package/documentation/headroom.md +0 -519
- package/documentation/installation.md +0 -758
- package/documentation/memory-system.md +0 -476
- package/documentation/production.md +0 -636
- package/documentation/providers.md +0 -1009
- package/documentation/routing.md +0 -476
- package/documentation/testing.md +0 -629
- package/documentation/token-optimization.md +0 -325
- package/documentation/tools.md +0 -697
- package/documentation/troubleshooting.md +0 -969
- package/final-test.js +0 -33
- package/headroom-sidecar/config.py +0 -93
- package/headroom-sidecar/requirements.txt +0 -14
- package/headroom-sidecar/server.py +0 -451
- package/monitor-agents.sh +0 -31
- package/scripts/audit-log-reader.js +0 -399
- package/scripts/compact-dictionary.js +0 -204
- package/scripts/test-deduplication.js +0 -448
- package/src/db/database.sqlite +0 -0
- package/te +0 -11622
- package/test/README.md +0 -212
- package/test/azure-openai-config.test.js +0 -213
- package/test/azure-openai-error-resilience.test.js +0 -238
- package/test/azure-openai-format-conversion.test.js +0 -354
- package/test/azure-openai-integration.test.js +0 -287
- package/test/azure-openai-routing.test.js +0 -175
- package/test/azure-openai-streaming.test.js +0 -171
- package/test/bedrock-integration.test.js +0 -457
- package/test/comprehensive-test-suite.js +0 -928
- package/test/config-validation.test.js +0 -207
- package/test/cursor-integration.test.js +0 -484
- package/test/format-conversion.test.js +0 -578
- package/test/hybrid-routing-integration.test.js +0 -269
- package/test/hybrid-routing-performance.test.js +0 -428
- package/test/llamacpp-integration.test.js +0 -882
- package/test/lmstudio-integration.test.js +0 -347
- package/test/memory/extractor.test.js +0 -398
- package/test/memory/retriever.test.js +0 -613
- package/test/memory/retriever.test.js.bak +0 -585
- package/test/memory/search.test.js +0 -537
- package/test/memory/search.test.js.bak +0 -389
- package/test/memory/store.test.js +0 -344
- package/test/memory/store.test.js.bak +0 -312
- package/test/memory/surprise.test.js +0 -300
- package/test/memory-performance.test.js +0 -472
- package/test/openai-integration.test.js +0 -683
- package/test/openrouter-error-resilience.test.js +0 -418
- package/test/passthrough-mode.test.js +0 -385
- package/test/performance-benchmark.js +0 -351
- package/test/performance-tests.js +0 -528
- package/test/routing.test.js +0 -225
- package/test/toon-compression.test.js +0 -131
- package/test/web-tools.test.js +0 -329
- package/test-agents-simple.js +0 -43
- package/test-cli-connection.sh +0 -33
- package/test-learning-unit.js +0 -126
- package/test-learning.js +0 -112
- package/test-parallel-agents.sh +0 -124
- package/test-parallel-direct.js +0 -155
- package/test-subagents.sh +0 -117
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
const {
|
|
2
|
+
circuitBreaker,
|
|
3
|
+
ConsecutiveBreaker,
|
|
4
|
+
retry,
|
|
5
|
+
handleAll,
|
|
6
|
+
ExponentialBackoff,
|
|
7
|
+
bulkhead,
|
|
8
|
+
timeout,
|
|
9
|
+
wrap,
|
|
10
|
+
CircuitState,
|
|
11
|
+
BrokenCircuitError,
|
|
12
|
+
BulkheadRejectedError,
|
|
13
|
+
TaskCancelledError,
|
|
14
|
+
} = require("cockatiel");
|
|
15
|
+
const logger = require("../logger");
|
|
16
|
+
|
|
17
|
+
// Re-use the existing STATE constant shape
|
|
18
|
+
const STATE = {
|
|
19
|
+
CLOSED: "CLOSED",
|
|
20
|
+
OPEN: "OPEN",
|
|
21
|
+
HALF_OPEN: "HALF_OPEN",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Map Cockatiel CircuitState enum to our string states
|
|
26
|
+
*/
|
|
27
|
+
function mapCircuitState(cockatielState) {
|
|
28
|
+
switch (cockatielState) {
|
|
29
|
+
case CircuitState.Closed:
|
|
30
|
+
return STATE.CLOSED;
|
|
31
|
+
case CircuitState.Open:
|
|
32
|
+
return STATE.OPEN;
|
|
33
|
+
case CircuitState.HalfOpen:
|
|
34
|
+
return STATE.HALF_OPEN;
|
|
35
|
+
default:
|
|
36
|
+
return STATE.CLOSED;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Circuit breaker error — same interface as the original
|
|
42
|
+
*/
|
|
43
|
+
class CircuitBreakerError extends Error {
|
|
44
|
+
constructor(message, retryAfter) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.name = "CircuitBreakerError";
|
|
47
|
+
this.retryAfter = retryAfter;
|
|
48
|
+
this.code = "circuit_breaker_open";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Cockatiel-backed CircuitBreaker adapter.
|
|
54
|
+
* Preserves the same API as the hand-rolled CircuitBreaker class.
|
|
55
|
+
*/
|
|
56
|
+
class CockatielCircuitBreaker {
|
|
57
|
+
constructor(name, options = {}) {
|
|
58
|
+
this.name = name;
|
|
59
|
+
|
|
60
|
+
// Configuration (same defaults as original)
|
|
61
|
+
this.failureThreshold = options.failureThreshold || 5;
|
|
62
|
+
this.successThreshold = options.successThreshold || 2;
|
|
63
|
+
this.halfOpenAfter = options.timeout || 60000;
|
|
64
|
+
|
|
65
|
+
// Stats tracking (same shape as original)
|
|
66
|
+
this.stats = {
|
|
67
|
+
totalRequests: 0,
|
|
68
|
+
totalFailures: 0,
|
|
69
|
+
totalSuccesses: 0,
|
|
70
|
+
totalRejected: 0,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Internal counters for getState() compatibility
|
|
74
|
+
this._failureCount = 0;
|
|
75
|
+
this._successCount = 0;
|
|
76
|
+
this._lastStateChange = Date.now();
|
|
77
|
+
this._nextAttempt = Date.now();
|
|
78
|
+
|
|
79
|
+
// Create the Cockatiel circuit breaker policy
|
|
80
|
+
this._policy = circuitBreaker(handleAll, {
|
|
81
|
+
breaker: new ConsecutiveBreaker(this.failureThreshold),
|
|
82
|
+
halfOpenAfter: this.halfOpenAfter,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Wire up events for logging and state tracking
|
|
86
|
+
this._policy.onBreak(() => {
|
|
87
|
+
this._lastStateChange = Date.now();
|
|
88
|
+
this._nextAttempt = Date.now() + this.halfOpenAfter;
|
|
89
|
+
logger.warn(
|
|
90
|
+
{
|
|
91
|
+
circuitBreaker: this.name,
|
|
92
|
+
retryAfter: this.halfOpenAfter,
|
|
93
|
+
},
|
|
94
|
+
"Circuit breaker opened - failing fast"
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
this._policy.onReset(() => {
|
|
99
|
+
this._failureCount = 0;
|
|
100
|
+
this._successCount = 0;
|
|
101
|
+
this._lastStateChange = Date.now();
|
|
102
|
+
logger.info(
|
|
103
|
+
{
|
|
104
|
+
circuitBreaker: this.name,
|
|
105
|
+
},
|
|
106
|
+
"Circuit breaker closed - normal operation resumed"
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
this._policy.onHalfOpen(() => {
|
|
111
|
+
this._successCount = 0;
|
|
112
|
+
this._lastStateChange = Date.now();
|
|
113
|
+
logger.info(
|
|
114
|
+
{
|
|
115
|
+
circuitBreaker: this.name,
|
|
116
|
+
},
|
|
117
|
+
"Circuit breaker half-open - testing service recovery"
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
this._policy.onSuccess(() => {
|
|
122
|
+
this.stats.totalSuccesses++;
|
|
123
|
+
this._failureCount = 0;
|
|
124
|
+
if (this.state === STATE.HALF_OPEN) {
|
|
125
|
+
this._successCount++;
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
this._policy.onFailure(() => {
|
|
130
|
+
this.stats.totalFailures++;
|
|
131
|
+
this._failureCount++;
|
|
132
|
+
this._successCount = 0;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Current state as a string
|
|
138
|
+
*/
|
|
139
|
+
get state() {
|
|
140
|
+
return mapCircuitState(this._policy.state);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
get failureCount() {
|
|
144
|
+
return this._failureCount;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
get successCount() {
|
|
148
|
+
return this._successCount;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Execute function with circuit breaker protection.
|
|
153
|
+
* Translates BrokenCircuitError → CircuitBreakerError for consumers.
|
|
154
|
+
*/
|
|
155
|
+
async execute(fn) {
|
|
156
|
+
this.stats.totalRequests++;
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
return await this._policy.execute(fn);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
if (err instanceof BrokenCircuitError) {
|
|
162
|
+
this.stats.totalRejected++;
|
|
163
|
+
const retryAfter = Math.max(0, this._nextAttempt - Date.now());
|
|
164
|
+
throw new CircuitBreakerError(
|
|
165
|
+
`Circuit breaker ${this.name} is OPEN`,
|
|
166
|
+
retryAfter
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
throw err;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get current state — same shape as original
|
|
175
|
+
*/
|
|
176
|
+
getState() {
|
|
177
|
+
return {
|
|
178
|
+
name: this.name,
|
|
179
|
+
state: this.state,
|
|
180
|
+
failureCount: this._failureCount,
|
|
181
|
+
successCount: this._successCount,
|
|
182
|
+
nextAttempt: this._nextAttempt,
|
|
183
|
+
lastStateChange: this._lastStateChange,
|
|
184
|
+
stats: { ...this.stats },
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Manually reset circuit breaker
|
|
190
|
+
*/
|
|
191
|
+
reset() {
|
|
192
|
+
// Cockatiel doesn't expose a public reset, but we can create a fresh policy
|
|
193
|
+
this._failureCount = 0;
|
|
194
|
+
this._successCount = 0;
|
|
195
|
+
this._lastStateChange = Date.now();
|
|
196
|
+
|
|
197
|
+
// Recreate the policy to reset state
|
|
198
|
+
const oldPolicy = this._policy;
|
|
199
|
+
this._policy = circuitBreaker(handleAll, {
|
|
200
|
+
breaker: new ConsecutiveBreaker(this.failureThreshold),
|
|
201
|
+
halfOpenAfter: this.halfOpenAfter,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Re-wire events
|
|
205
|
+
this._policy.onBreak(() => {
|
|
206
|
+
this._lastStateChange = Date.now();
|
|
207
|
+
this._nextAttempt = Date.now() + this.halfOpenAfter;
|
|
208
|
+
logger.warn(
|
|
209
|
+
{ circuitBreaker: this.name, retryAfter: this.halfOpenAfter },
|
|
210
|
+
"Circuit breaker opened - failing fast"
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
this._policy.onReset(() => {
|
|
214
|
+
this._failureCount = 0;
|
|
215
|
+
this._successCount = 0;
|
|
216
|
+
this._lastStateChange = Date.now();
|
|
217
|
+
logger.info({ circuitBreaker: this.name }, "Circuit breaker closed - normal operation resumed");
|
|
218
|
+
});
|
|
219
|
+
this._policy.onHalfOpen(() => {
|
|
220
|
+
this._successCount = 0;
|
|
221
|
+
this._lastStateChange = Date.now();
|
|
222
|
+
logger.info({ circuitBreaker: this.name }, "Circuit breaker half-open - testing service recovery");
|
|
223
|
+
});
|
|
224
|
+
this._policy.onSuccess(() => {
|
|
225
|
+
this.stats.totalSuccesses++;
|
|
226
|
+
this._failureCount = 0;
|
|
227
|
+
if (this.state === STATE.HALF_OPEN) {
|
|
228
|
+
this._successCount++;
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
this._policy.onFailure(() => {
|
|
232
|
+
this.stats.totalFailures++;
|
|
233
|
+
this._failureCount++;
|
|
234
|
+
this._successCount = 0;
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Registry — same Map-based pattern as original CircuitBreakerRegistry
|
|
241
|
+
*/
|
|
242
|
+
class CockatielRegistry {
|
|
243
|
+
constructor() {
|
|
244
|
+
this.breakers = new Map();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
get(name, options) {
|
|
248
|
+
if (!this.breakers.has(name)) {
|
|
249
|
+
this.breakers.set(name, new CockatielCircuitBreaker(name, options));
|
|
250
|
+
}
|
|
251
|
+
return this.breakers.get(name);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
getAll() {
|
|
255
|
+
return Array.from(this.breakers.values()).map((breaker) => breaker.getState());
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
resetAll() {
|
|
259
|
+
for (const breaker of this.breakers.values()) {
|
|
260
|
+
breaker.reset();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Singleton registry
|
|
266
|
+
let registry = null;
|
|
267
|
+
|
|
268
|
+
function getCockatielRegistry() {
|
|
269
|
+
if (!registry) {
|
|
270
|
+
registry = new CockatielRegistry();
|
|
271
|
+
}
|
|
272
|
+
return registry;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// --- Retry adapter ---
|
|
276
|
+
|
|
277
|
+
const DEFAULT_RETRY_CONFIG = {
|
|
278
|
+
maxRetries: 3,
|
|
279
|
+
initialDelay: 1000,
|
|
280
|
+
maxDelay: 30000,
|
|
281
|
+
backoffMultiplier: 2,
|
|
282
|
+
jitterFactor: 0.1,
|
|
283
|
+
retryableStatuses: [429, 500, 502, 503, 504],
|
|
284
|
+
retryableErrors: ["ECONNRESET", "ETIMEDOUT", "ENOTFOUND", "ENETUNREACH", "ECONNREFUSED"],
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Check if error/response is retryable (same logic as original)
|
|
289
|
+
*/
|
|
290
|
+
function isRetryableCheck(error, response, config) {
|
|
291
|
+
if (response && config.retryableStatuses.includes(response.status)) {
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
if (error && error.code && config.retryableErrors.includes(error.code)) {
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
if (error && error.cause?.code && config.retryableErrors.includes(error.cause.code)) {
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
if (error && (error.name === "FetchError" || error.name === "AbortError")) {
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Detect if this is a cold start
|
|
308
|
+
*/
|
|
309
|
+
function detectColdStart(startTime, endTime, threshold = 5000) {
|
|
310
|
+
return (endTime - startTime) > threshold;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Sleep helper
|
|
315
|
+
*/
|
|
316
|
+
function sleep(ms) {
|
|
317
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* withCockatielRetry — drop-in replacement for withRetry.
|
|
322
|
+
*
|
|
323
|
+
* Same signature: withCockatielRetry(fn, options)
|
|
324
|
+
* - fn(attempt) is called with the attempt number (0-based)
|
|
325
|
+
* - Returns last response when all retries exhausted (matching original behavior)
|
|
326
|
+
* - Preserves 429 Retry-After header parsing
|
|
327
|
+
*/
|
|
328
|
+
async function withCockatielRetry(fn, options = {}) {
|
|
329
|
+
const config = { ...DEFAULT_RETRY_CONFIG, ...options };
|
|
330
|
+
let lastResponse = null;
|
|
331
|
+
let attempt = 0;
|
|
332
|
+
|
|
333
|
+
const retryPolicy = retry(handleAll, {
|
|
334
|
+
maxAttempts: config.maxRetries,
|
|
335
|
+
backoff: new ExponentialBackoff({
|
|
336
|
+
initialDelay: config.initialDelay,
|
|
337
|
+
maxDelay: config.maxDelay,
|
|
338
|
+
exponent: config.backoffMultiplier,
|
|
339
|
+
}),
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
retryPolicy.onRetry(({ attempt: retryAttempt }) => {
|
|
343
|
+
logger.warn(
|
|
344
|
+
{ attempt: retryAttempt },
|
|
345
|
+
"Retrying request"
|
|
346
|
+
);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// We use a manual approach that mirrors the original withRetry exactly,
|
|
350
|
+
// wrapping Cockatiel's retry for exponential backoff but keeping the
|
|
351
|
+
// response-status-check and 429-Retry-After logic intact.
|
|
352
|
+
// This ensures 100% behavioral compatibility.
|
|
353
|
+
|
|
354
|
+
for (attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
355
|
+
const startTime = Date.now();
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
const result = await fn(attempt);
|
|
359
|
+
const endTime = Date.now();
|
|
360
|
+
|
|
361
|
+
if (detectColdStart(startTime, endTime)) {
|
|
362
|
+
logger.warn(
|
|
363
|
+
{ attempt, duration: endTime - startTime },
|
|
364
|
+
"Potential cold start detected"
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Check if response indicates we should retry
|
|
369
|
+
if (result && isRetryableCheck(null, result, config) && attempt < config.maxRetries) {
|
|
370
|
+
lastResponse = result;
|
|
371
|
+
|
|
372
|
+
if (result.status === 429) {
|
|
373
|
+
const retryAfter = result.headers?.get?.("retry-after");
|
|
374
|
+
let delay;
|
|
375
|
+
|
|
376
|
+
if (retryAfter) {
|
|
377
|
+
const retryAfterNum = parseInt(retryAfter, 10);
|
|
378
|
+
if (!isNaN(retryAfterNum)) {
|
|
379
|
+
delay = retryAfterNum * 1000;
|
|
380
|
+
} else {
|
|
381
|
+
const retryAfterDate = new Date(retryAfter);
|
|
382
|
+
delay = retryAfterDate.getTime() - Date.now();
|
|
383
|
+
}
|
|
384
|
+
} else {
|
|
385
|
+
// Exponential backoff with longer delays for rate limiting
|
|
386
|
+
const baseDelay = 2000 * Math.pow(config.backoffMultiplier, attempt);
|
|
387
|
+
const cappedDelay = Math.min(baseDelay, 60000);
|
|
388
|
+
const jitter = cappedDelay * config.jitterFactor * (Math.random() * 2 - 1);
|
|
389
|
+
delay = Math.max(0, cappedDelay + jitter);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
logger.warn(
|
|
393
|
+
{ attempt, delay, retryAfter: retryAfter || "not specified" },
|
|
394
|
+
"Rate limited (429), retrying after delay"
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
await sleep(delay);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Regular retry with exponential backoff
|
|
402
|
+
const baseDelay = config.initialDelay * Math.pow(config.backoffMultiplier, attempt);
|
|
403
|
+
const cappedDelay = Math.min(baseDelay, config.maxDelay);
|
|
404
|
+
const jitter = cappedDelay * config.jitterFactor * (Math.random() * 2 - 1);
|
|
405
|
+
const delay = Math.max(0, cappedDelay + jitter);
|
|
406
|
+
|
|
407
|
+
logger.warn(
|
|
408
|
+
{ attempt, status: result.status, delay },
|
|
409
|
+
"Request failed, retrying with backoff"
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
await sleep(delay);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return result;
|
|
417
|
+
} catch (error) {
|
|
418
|
+
const endTime = Date.now();
|
|
419
|
+
|
|
420
|
+
if (detectColdStart(startTime, endTime)) {
|
|
421
|
+
logger.warn(
|
|
422
|
+
{ attempt, duration: endTime - startTime, error: error.message },
|
|
423
|
+
"Potential cold start with error detected"
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (isRetryableCheck(error, null, config) && attempt < config.maxRetries) {
|
|
428
|
+
const baseDelay = config.initialDelay * Math.pow(config.backoffMultiplier, attempt);
|
|
429
|
+
const cappedDelay = Math.min(baseDelay, config.maxDelay);
|
|
430
|
+
const jitter = cappedDelay * config.jitterFactor * (Math.random() * 2 - 1);
|
|
431
|
+
const delay = Math.max(0, cappedDelay + jitter);
|
|
432
|
+
|
|
433
|
+
logger.warn(
|
|
434
|
+
{ attempt, error: error.message, code: error.code, delay },
|
|
435
|
+
"Request error, retrying with backoff"
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
await sleep(delay);
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
throw error;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Max retries exceeded
|
|
447
|
+
if (lastResponse) {
|
|
448
|
+
logger.error(
|
|
449
|
+
{ status: lastResponse.status, maxRetries: config.maxRetries },
|
|
450
|
+
"Max retries exceeded"
|
|
451
|
+
);
|
|
452
|
+
return lastResponse;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
throw new Error("Retry logic failed unexpectedly");
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// --- Composed provider policy ---
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Create a composed policy: retry + circuit breaker + timeout
|
|
462
|
+
*/
|
|
463
|
+
function createProviderPolicy(name, options = {}) {
|
|
464
|
+
const cbOptions = {
|
|
465
|
+
failureThreshold: options.failureThreshold || 5,
|
|
466
|
+
halfOpenAfter: options.halfOpenAfter || 60000,
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const retryMaxAttempts = options.retryMaxAttempts || 3;
|
|
470
|
+
const timeoutMs = options.timeout || 120000;
|
|
471
|
+
|
|
472
|
+
const cb = circuitBreaker(handleAll, {
|
|
473
|
+
breaker: new ConsecutiveBreaker(cbOptions.failureThreshold),
|
|
474
|
+
halfOpenAfter: cbOptions.halfOpenAfter,
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
const retryPolicy = retry(handleAll, {
|
|
478
|
+
maxAttempts: retryMaxAttempts,
|
|
479
|
+
backoff: new ExponentialBackoff({
|
|
480
|
+
initialDelay: 1000,
|
|
481
|
+
maxDelay: 30000,
|
|
482
|
+
exponent: 2,
|
|
483
|
+
}),
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const timeoutPolicy = timeout(timeoutMs, "aggressive");
|
|
487
|
+
|
|
488
|
+
retryPolicy.onRetry(({ attempt }) => {
|
|
489
|
+
logger.warn({ provider: name, attempt }, "Retrying provider request");
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
cb.onBreak(() => {
|
|
493
|
+
logger.warn({ provider: name }, "Provider circuit opened");
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
cb.onReset(() => {
|
|
497
|
+
logger.info({ provider: name }, "Provider circuit closed");
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
cb.onHalfOpen(() => {
|
|
501
|
+
logger.info({ provider: name }, "Provider circuit half-open");
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
return wrap(retryPolicy, cb, timeoutPolicy);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// --- Bulkhead adapter ---
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Create a Cockatiel bulkhead (replaces Semaphore)
|
|
511
|
+
*/
|
|
512
|
+
function createBulkhead(options = {}) {
|
|
513
|
+
const maxConcurrent = options.maxConcurrent || 2;
|
|
514
|
+
const maxQueue = options.maxQueue || 50;
|
|
515
|
+
return bulkhead(maxConcurrent, maxQueue);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
module.exports = {
|
|
519
|
+
// Circuit breaker
|
|
520
|
+
CockatielCircuitBreaker,
|
|
521
|
+
CircuitBreakerError,
|
|
522
|
+
CockatielRegistry,
|
|
523
|
+
getCockatielRegistry,
|
|
524
|
+
STATE,
|
|
525
|
+
|
|
526
|
+
// Retry
|
|
527
|
+
withCockatielRetry,
|
|
528
|
+
DEFAULT_RETRY_CONFIG,
|
|
529
|
+
|
|
530
|
+
// Composed
|
|
531
|
+
createProviderPolicy,
|
|
532
|
+
|
|
533
|
+
// Bulkhead
|
|
534
|
+
createBulkhead,
|
|
535
|
+
|
|
536
|
+
// Re-exports for internal use
|
|
537
|
+
BrokenCircuitError,
|
|
538
|
+
BulkheadRejectedError,
|
|
539
|
+
TaskCancelledError,
|
|
540
|
+
};
|