@saulwade/swl-ses 1.3.3 → 1.3.5

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.
Files changed (102) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.md +1 -1
  3. package/bin/swl-mcp-server.js +187 -187
  4. package/bin/swl-ses.js +4 -62
  5. package/comandos/swl/.evolved.json +22 -22
  6. package/comandos/swl/adoptar-proyecto.md +207 -207
  7. package/comandos/swl/contribuir.md +233 -233
  8. package/habilidades/backend-production-resilience/SKILL.md +288 -288
  9. package/habilidades/benchmark-memoria/SKILL.md +186 -186
  10. package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
  11. package/habilidades/doubt-driven-review/SKILL.md +171 -171
  12. package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
  13. package/habilidades/eval-framework/SKILL.md +212 -212
  14. package/habilidades/extractor-de-aprendizajes/SKILL.md +321 -321
  15. package/habilidades/harness-claude-code/SKILL.md +299 -299
  16. package/habilidades/infra-github-actions/SKILL.md +166 -166
  17. package/habilidades/legacy-code-rescue/SKILL.md +267 -267
  18. package/habilidades/manejo-errores/.evolved.json +8 -8
  19. package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
  20. package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
  21. package/habilidades/patrones-python/SKILL.md +229 -229
  22. package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
  23. package/habilidades/planear-fase/SKILL.md +319 -319
  24. package/habilidades/release-semver/.evolved.json +8 -8
  25. package/habilidades/swl-claudemd/SKILL.md +220 -220
  26. package/habilidades/testing-python/SKILL.md +340 -340
  27. package/hooks/claudemd-bloat-detector.js +161 -161
  28. package/hooks/extraccion-aprendizajes.js +43 -12
  29. package/hooks/lib/agent-routing.js +107 -107
  30. package/hooks/lib/auto-consolidator.js +335 -335
  31. package/hooks/lib/error-classifier.js +308 -308
  32. package/hooks/lib/merkle-audit.js +96 -96
  33. package/hooks/lib/provenance-tracker.js +191 -191
  34. package/hooks/lib/rate-limit-tracker.js +253 -253
  35. package/hooks/lib/resource-quota.js +122 -122
  36. package/hooks/lib/retry-jitter.js +165 -165
  37. package/hooks/lib/skill-auditor.js +588 -588
  38. package/hooks/lib/sync-status.js +228 -228
  39. package/hooks/lib/taint-tracker.js +107 -107
  40. package/hooks/lib/text-similarity.js +241 -241
  41. package/hooks/lib/toon-compressor.js +245 -245
  42. package/hooks/registro-turnos.js +209 -209
  43. package/hooks/sugerir-regenerar-inventario.js +170 -170
  44. package/hooks/validar-formato-post-subagente.js +140 -140
  45. package/hooks/validar-memoria-hook.js +218 -218
  46. package/instintos/prompt-appendices.yaml +57 -57
  47. package/manifiestos/agent-output-schemas.json +57 -57
  48. package/manifiestos/skills-lock.json +27 -27
  49. package/package.json +1 -1
  50. package/plantillas/auditor-veto-template.md +105 -105
  51. package/plantillas/github-workflows/README.md +47 -47
  52. package/plantillas/github-workflows/release-please.yml +44 -44
  53. package/plantillas/github-workflows/swl-ci.yml +107 -107
  54. package/plantillas/github-workflows/swl-security.yml +51 -51
  55. package/plugin.json +1 -1
  56. package/reglas/analisis-previo-tareas-grandes.md +172 -172
  57. package/reglas/arreglar-al-detectar.md +147 -147
  58. package/reglas/fragmentos-compartidos.md +152 -152
  59. package/reglas/harness-claude-code.md +213 -213
  60. package/reglas/usar-context7.md +226 -226
  61. package/schemas/diary-entry.schema.json +80 -80
  62. package/scripts/benchmark-memoria.js +167 -167
  63. package/scripts/configurar-branch-protection.js +418 -418
  64. package/scripts/detectar-aprendizajes-duplicados.js +151 -151
  65. package/scripts/doctor.js +77 -3
  66. package/scripts/field-report.js +199 -199
  67. package/scripts/generar-checklists-consolidados.js +273 -273
  68. package/scripts/generar-inventario.js +420 -420
  69. package/scripts/generar-matriz-lenguajes.js +271 -271
  70. package/scripts/instalador.js +38 -1
  71. package/scripts/lib/artefactos-python.js +43 -43
  72. package/scripts/lib/benchmark-metrics.js +160 -160
  73. package/scripts/lib/budget-enforcer.js +252 -252
  74. package/scripts/lib/configurar-ci.js +380 -380
  75. package/scripts/lib/contadores-inventario.js +217 -217
  76. package/scripts/lib/detectar-stack-detallado.js +307 -307
  77. package/scripts/lib/diary-entry.js +234 -234
  78. package/scripts/lib/eval-metrics-store.js +218 -218
  79. package/scripts/lib/eval-quality.js +171 -171
  80. package/scripts/lib/eval-schemas.js +144 -144
  81. package/scripts/lib/eval-self-correct.js +106 -106
  82. package/scripts/lib/eval-validator.js +185 -185
  83. package/scripts/lib/jaccard-similarity.js +98 -98
  84. package/scripts/lib/longmemeval-runner.js +125 -125
  85. package/scripts/lib/npm-version.js +261 -261
  86. package/scripts/lib/paquetes-conocidos.js +50 -50
  87. package/scripts/lib/parsear-opciones.js +136 -0
  88. package/scripts/lib/prompt-builder.js +264 -264
  89. package/scripts/lib/rrf-fusion.js +175 -175
  90. package/scripts/lib/scoring-instintos.js +277 -277
  91. package/scripts/lib/semantic-search.js +252 -252
  92. package/scripts/lib/transformadores/claude.js +200 -200
  93. package/scripts/limpiar-artefactos-python.js +131 -131
  94. package/scripts/mcp-server/README.md +128 -128
  95. package/scripts/mcp-server/handlers.js +206 -206
  96. package/scripts/migrar-csv-a-array.js +168 -168
  97. package/scripts/migrar-fase-dominio.js +201 -201
  98. package/scripts/publicar.js +511 -511
  99. package/scripts/run-eval.js +141 -141
  100. package/scripts/validar-manifest.js +195 -195
  101. package/scripts/validar-userland-vacio.js +110 -110
  102. package/scripts/verificar-release.js +5 -1
@@ -1,253 +1,253 @@
1
- 'use strict';
2
-
3
- /**
4
- * Rate Limit Tracker — Awareness de límites de tasa per-provider.
5
- *
6
- * Patrón adoptado de Hermes Agent (agent/rate_limit_tracker.py).
7
- * Registra respuestas 429 y headers de rate limit para ajustar
8
- * timing de llamadas y prevenir cascadas de error.
9
- *
10
- * Zero dependencias externas.
11
- *
12
- * Uso:
13
- * const { tracker } = require('./lib/rate-limit-tracker');
14
- *
15
- * // Registrar un 429
16
- * tracker.registrar429('anthropic', { retryAfter: 30, modelo: 'claude-sonnet-4-6' });
17
- *
18
- * // Consultar antes de llamar
19
- * if (tracker.estaThrottled('anthropic')) {
20
- * const espera = tracker.tiempoEspera('anthropic');
21
- * console.log(`Provider throttled, esperar ${espera}s`);
22
- * }
23
- *
24
- * // Registrar éxito (resetea contadores)
25
- * tracker.registrarExito('anthropic');
26
- *
27
- * @module hooks/lib/rate-limit-tracker
28
- */
29
-
30
- // ---------------------------------------------------------------------------
31
- // Constantes
32
- // ---------------------------------------------------------------------------
33
-
34
- /** Máximo de 429s consecutivos antes de marcar provider como BLOQUEADO. */
35
- const MAX_429_CONSECUTIVOS = 5;
36
-
37
- /** Tiempo base de backoff en ms tras un 429 (si no hay Retry-After). */
38
- const BACKOFF_BASE_MS = 10_000;
39
-
40
- /** Multiplicador de backoff exponencial por 429 consecutivo. */
41
- const BACKOFF_MULTIPLICADOR = 2;
42
-
43
- /** Tiempo máximo de backoff en ms (5 minutos). */
44
- const BACKOFF_MAX_MS = 300_000;
45
-
46
- /** Tiempo de recuperación half-open en ms (1 minuto). */
47
- const HALF_OPEN_MS = 60_000;
48
-
49
- /** Tiempo para considerar un registro como stale y limpiar (30 minutos). */
50
- const STALE_MS = 1_800_000;
51
-
52
- // ---------------------------------------------------------------------------
53
- // Estado del tracker (en memoria, por sesión)
54
- // ---------------------------------------------------------------------------
55
-
56
- /**
57
- * @typedef {object} ProviderState
58
- * @property {number} consecutivos429 - Conteo de 429s consecutivos
59
- * @property {number} ultimoTimestamp - Timestamp del último 429
60
- * @property {number} esperaHastaMs - Timestamp hasta el cual esperar
61
- * @property {string} estado - 'ok' | 'throttled' | 'bloqueado' | 'half-open'
62
- * @property {string|null} modelo - Último modelo que causó 429
63
- * @property {number} totalHistorico - Total de 429s en la sesión
64
- */
65
-
66
- /** @type {Map<string, ProviderState>} */
67
- const _providers = new Map();
68
-
69
- // ---------------------------------------------------------------------------
70
- // Funciones internas
71
- // ---------------------------------------------------------------------------
72
-
73
- /**
74
- * Obtiene o crea el estado de un provider.
75
- * @param {string} provider
76
- * @returns {ProviderState}
77
- */
78
- function _obtenerEstado(provider) {
79
- if (!_providers.has(provider)) {
80
- _providers.set(provider, {
81
- consecutivos429: 0,
82
- ultimoTimestamp: 0,
83
- esperaHastaMs: 0,
84
- estado: 'ok',
85
- modelo: null,
86
- totalHistorico: 0,
87
- });
88
- }
89
- return _providers.get(provider);
90
- }
91
-
92
- /**
93
- * Calcula el delay de backoff para un número de 429s consecutivos.
94
- * @param {number} consecutivos
95
- * @param {number|null} retryAfterSeg - Valor de Retry-After del header (en segundos)
96
- * @returns {number} Delay en milisegundos
97
- */
98
- function _calcularBackoff(consecutivos, retryAfterSeg) {
99
- if (retryAfterSeg && retryAfterSeg > 0) {
100
- return retryAfterSeg * 1000;
101
- }
102
- const delay = BACKOFF_BASE_MS * Math.pow(BACKOFF_MULTIPLICADOR, consecutivos - 1);
103
- return Math.min(delay, BACKOFF_MAX_MS);
104
- }
105
-
106
- // ---------------------------------------------------------------------------
107
- // API pública
108
- // ---------------------------------------------------------------------------
109
-
110
- const tracker = {
111
- /**
112
- * Registra una respuesta 429 de un provider.
113
- *
114
- * @param {string} provider - Nombre del provider ('anthropic', 'openai', etc.)
115
- * @param {object} [info]
116
- * @param {number} [info.retryAfter] - Valor del header Retry-After en segundos
117
- * @param {string} [info.modelo] - Modelo que causó el 429
118
- */
119
- registrar429(provider, info = {}) {
120
- const estado = _obtenerEstado(provider);
121
- const ahora = Date.now();
122
-
123
- estado.consecutivos429 += 1;
124
- estado.ultimoTimestamp = ahora;
125
- estado.totalHistorico += 1;
126
- estado.modelo = info.modelo || estado.modelo;
127
-
128
- const backoff = _calcularBackoff(estado.consecutivos429, info.retryAfter);
129
- estado.esperaHastaMs = ahora + backoff;
130
-
131
- if (estado.consecutivos429 >= MAX_429_CONSECUTIVOS) {
132
- estado.estado = 'bloqueado';
133
- } else {
134
- estado.estado = 'throttled';
135
- }
136
- },
137
-
138
- /**
139
- * Registra una llamada exitosa — resetea contadores del provider.
140
- *
141
- * @param {string} provider
142
- */
143
- registrarExito(provider) {
144
- const estado = _obtenerEstado(provider);
145
- estado.consecutivos429 = 0;
146
- estado.esperaHastaMs = 0;
147
- estado.estado = 'ok';
148
- },
149
-
150
- /**
151
- * Verifica si un provider está throttled o bloqueado.
152
- *
153
- * @param {string} provider
154
- * @returns {boolean}
155
- */
156
- estaThrottled(provider) {
157
- const estado = _obtenerEstado(provider);
158
- const ahora = Date.now();
159
-
160
- if (estado.estado === 'ok') return false;
161
-
162
- // Verificar si ya pasó el tiempo de espera
163
- if (ahora >= estado.esperaHastaMs) {
164
- if (estado.estado === 'bloqueado') {
165
- // Transición a half-open para permitir un intento de prueba
166
- estado.estado = 'half-open';
167
- estado.esperaHastaMs = ahora + HALF_OPEN_MS;
168
- return false; // permitir un intento
169
- }
170
- // Throttled normal — ya pasó el backoff
171
- estado.estado = 'ok';
172
- estado.consecutivos429 = 0;
173
- return false;
174
- }
175
-
176
- return true;
177
- },
178
-
179
- /**
180
- * Retorna el tiempo de espera restante en segundos.
181
- *
182
- * @param {string} provider
183
- * @returns {number} Segundos restantes (0 si no está throttled)
184
- */
185
- tiempoEspera(provider) {
186
- const estado = _obtenerEstado(provider);
187
- const restante = estado.esperaHastaMs - Date.now();
188
- return restante > 0 ? Math.ceil(restante / 1000) : 0;
189
- },
190
-
191
- /**
192
- * Retorna el estado completo de un provider.
193
- *
194
- * @param {string} provider
195
- * @returns {ProviderState}
196
- */
197
- estado(provider) {
198
- return { ..._obtenerEstado(provider) };
199
- },
200
-
201
- /**
202
- * Retorna resumen de todos los providers trackeados.
203
- *
204
- * @returns {Array<{ provider: string, estado: string, consecutivos429: number, totalHistorico: number, tiempoEspera: number }>}
205
- */
206
- resumen() {
207
- const resultado = [];
208
- for (const [provider, estado] of _providers) {
209
- resultado.push({
210
- provider,
211
- estado: estado.estado,
212
- consecutivos429: estado.consecutivos429,
213
- totalHistorico: estado.totalHistorico,
214
- tiempoEspera: tracker.tiempoEspera(provider),
215
- modelo: estado.modelo,
216
- });
217
- }
218
- return resultado;
219
- },
220
-
221
- /**
222
- * Limpia registros stale (sin actividad reciente).
223
- * Llamar periódicamente para evitar memory leak en sesiones largas.
224
- */
225
- limpiarStale() {
226
- const ahora = Date.now();
227
- for (const [provider, estado] of _providers) {
228
- if (estado.estado === 'ok' && (ahora - estado.ultimoTimestamp) > STALE_MS) {
229
- _providers.delete(provider);
230
- }
231
- }
232
- },
233
-
234
- /**
235
- * Resetea todo el tracker (útil para tests).
236
- */
237
- reset() {
238
- _providers.clear();
239
- },
240
- };
241
-
242
- // ---------------------------------------------------------------------------
243
- // Exports
244
- // ---------------------------------------------------------------------------
245
-
246
- module.exports = {
247
- tracker,
248
- // Exponer constantes para tests
249
- MAX_429_CONSECUTIVOS,
250
- BACKOFF_BASE_MS,
251
- BACKOFF_MAX_MS,
252
- HALF_OPEN_MS,
253
- };
1
+ 'use strict';
2
+
3
+ /**
4
+ * Rate Limit Tracker — Awareness de límites de tasa per-provider.
5
+ *
6
+ * Patrón adoptado de Hermes Agent (agent/rate_limit_tracker.py).
7
+ * Registra respuestas 429 y headers de rate limit para ajustar
8
+ * timing de llamadas y prevenir cascadas de error.
9
+ *
10
+ * Zero dependencias externas.
11
+ *
12
+ * Uso:
13
+ * const { tracker } = require('./lib/rate-limit-tracker');
14
+ *
15
+ * // Registrar un 429
16
+ * tracker.registrar429('anthropic', { retryAfter: 30, modelo: 'claude-sonnet-4-6' });
17
+ *
18
+ * // Consultar antes de llamar
19
+ * if (tracker.estaThrottled('anthropic')) {
20
+ * const espera = tracker.tiempoEspera('anthropic');
21
+ * console.log(`Provider throttled, esperar ${espera}s`);
22
+ * }
23
+ *
24
+ * // Registrar éxito (resetea contadores)
25
+ * tracker.registrarExito('anthropic');
26
+ *
27
+ * @module hooks/lib/rate-limit-tracker
28
+ */
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Constantes
32
+ // ---------------------------------------------------------------------------
33
+
34
+ /** Máximo de 429s consecutivos antes de marcar provider como BLOQUEADO. */
35
+ const MAX_429_CONSECUTIVOS = 5;
36
+
37
+ /** Tiempo base de backoff en ms tras un 429 (si no hay Retry-After). */
38
+ const BACKOFF_BASE_MS = 10_000;
39
+
40
+ /** Multiplicador de backoff exponencial por 429 consecutivo. */
41
+ const BACKOFF_MULTIPLICADOR = 2;
42
+
43
+ /** Tiempo máximo de backoff en ms (5 minutos). */
44
+ const BACKOFF_MAX_MS = 300_000;
45
+
46
+ /** Tiempo de recuperación half-open en ms (1 minuto). */
47
+ const HALF_OPEN_MS = 60_000;
48
+
49
+ /** Tiempo para considerar un registro como stale y limpiar (30 minutos). */
50
+ const STALE_MS = 1_800_000;
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Estado del tracker (en memoria, por sesión)
54
+ // ---------------------------------------------------------------------------
55
+
56
+ /**
57
+ * @typedef {object} ProviderState
58
+ * @property {number} consecutivos429 - Conteo de 429s consecutivos
59
+ * @property {number} ultimoTimestamp - Timestamp del último 429
60
+ * @property {number} esperaHastaMs - Timestamp hasta el cual esperar
61
+ * @property {string} estado - 'ok' | 'throttled' | 'bloqueado' | 'half-open'
62
+ * @property {string|null} modelo - Último modelo que causó 429
63
+ * @property {number} totalHistorico - Total de 429s en la sesión
64
+ */
65
+
66
+ /** @type {Map<string, ProviderState>} */
67
+ const _providers = new Map();
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Funciones internas
71
+ // ---------------------------------------------------------------------------
72
+
73
+ /**
74
+ * Obtiene o crea el estado de un provider.
75
+ * @param {string} provider
76
+ * @returns {ProviderState}
77
+ */
78
+ function _obtenerEstado(provider) {
79
+ if (!_providers.has(provider)) {
80
+ _providers.set(provider, {
81
+ consecutivos429: 0,
82
+ ultimoTimestamp: 0,
83
+ esperaHastaMs: 0,
84
+ estado: 'ok',
85
+ modelo: null,
86
+ totalHistorico: 0,
87
+ });
88
+ }
89
+ return _providers.get(provider);
90
+ }
91
+
92
+ /**
93
+ * Calcula el delay de backoff para un número de 429s consecutivos.
94
+ * @param {number} consecutivos
95
+ * @param {number|null} retryAfterSeg - Valor de Retry-After del header (en segundos)
96
+ * @returns {number} Delay en milisegundos
97
+ */
98
+ function _calcularBackoff(consecutivos, retryAfterSeg) {
99
+ if (retryAfterSeg && retryAfterSeg > 0) {
100
+ return retryAfterSeg * 1000;
101
+ }
102
+ const delay = BACKOFF_BASE_MS * Math.pow(BACKOFF_MULTIPLICADOR, consecutivos - 1);
103
+ return Math.min(delay, BACKOFF_MAX_MS);
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // API pública
108
+ // ---------------------------------------------------------------------------
109
+
110
+ const tracker = {
111
+ /**
112
+ * Registra una respuesta 429 de un provider.
113
+ *
114
+ * @param {string} provider - Nombre del provider ('anthropic', 'openai', etc.)
115
+ * @param {object} [info]
116
+ * @param {number} [info.retryAfter] - Valor del header Retry-After en segundos
117
+ * @param {string} [info.modelo] - Modelo que causó el 429
118
+ */
119
+ registrar429(provider, info = {}) {
120
+ const estado = _obtenerEstado(provider);
121
+ const ahora = Date.now();
122
+
123
+ estado.consecutivos429 += 1;
124
+ estado.ultimoTimestamp = ahora;
125
+ estado.totalHistorico += 1;
126
+ estado.modelo = info.modelo || estado.modelo;
127
+
128
+ const backoff = _calcularBackoff(estado.consecutivos429, info.retryAfter);
129
+ estado.esperaHastaMs = ahora + backoff;
130
+
131
+ if (estado.consecutivos429 >= MAX_429_CONSECUTIVOS) {
132
+ estado.estado = 'bloqueado';
133
+ } else {
134
+ estado.estado = 'throttled';
135
+ }
136
+ },
137
+
138
+ /**
139
+ * Registra una llamada exitosa — resetea contadores del provider.
140
+ *
141
+ * @param {string} provider
142
+ */
143
+ registrarExito(provider) {
144
+ const estado = _obtenerEstado(provider);
145
+ estado.consecutivos429 = 0;
146
+ estado.esperaHastaMs = 0;
147
+ estado.estado = 'ok';
148
+ },
149
+
150
+ /**
151
+ * Verifica si un provider está throttled o bloqueado.
152
+ *
153
+ * @param {string} provider
154
+ * @returns {boolean}
155
+ */
156
+ estaThrottled(provider) {
157
+ const estado = _obtenerEstado(provider);
158
+ const ahora = Date.now();
159
+
160
+ if (estado.estado === 'ok') return false;
161
+
162
+ // Verificar si ya pasó el tiempo de espera
163
+ if (ahora >= estado.esperaHastaMs) {
164
+ if (estado.estado === 'bloqueado') {
165
+ // Transición a half-open para permitir un intento de prueba
166
+ estado.estado = 'half-open';
167
+ estado.esperaHastaMs = ahora + HALF_OPEN_MS;
168
+ return false; // permitir un intento
169
+ }
170
+ // Throttled normal — ya pasó el backoff
171
+ estado.estado = 'ok';
172
+ estado.consecutivos429 = 0;
173
+ return false;
174
+ }
175
+
176
+ return true;
177
+ },
178
+
179
+ /**
180
+ * Retorna el tiempo de espera restante en segundos.
181
+ *
182
+ * @param {string} provider
183
+ * @returns {number} Segundos restantes (0 si no está throttled)
184
+ */
185
+ tiempoEspera(provider) {
186
+ const estado = _obtenerEstado(provider);
187
+ const restante = estado.esperaHastaMs - Date.now();
188
+ return restante > 0 ? Math.ceil(restante / 1000) : 0;
189
+ },
190
+
191
+ /**
192
+ * Retorna el estado completo de un provider.
193
+ *
194
+ * @param {string} provider
195
+ * @returns {ProviderState}
196
+ */
197
+ estado(provider) {
198
+ return { ..._obtenerEstado(provider) };
199
+ },
200
+
201
+ /**
202
+ * Retorna resumen de todos los providers trackeados.
203
+ *
204
+ * @returns {Array<{ provider: string, estado: string, consecutivos429: number, totalHistorico: number, tiempoEspera: number }>}
205
+ */
206
+ resumen() {
207
+ const resultado = [];
208
+ for (const [provider, estado] of _providers) {
209
+ resultado.push({
210
+ provider,
211
+ estado: estado.estado,
212
+ consecutivos429: estado.consecutivos429,
213
+ totalHistorico: estado.totalHistorico,
214
+ tiempoEspera: tracker.tiempoEspera(provider),
215
+ modelo: estado.modelo,
216
+ });
217
+ }
218
+ return resultado;
219
+ },
220
+
221
+ /**
222
+ * Limpia registros stale (sin actividad reciente).
223
+ * Llamar periódicamente para evitar memory leak en sesiones largas.
224
+ */
225
+ limpiarStale() {
226
+ const ahora = Date.now();
227
+ for (const [provider, estado] of _providers) {
228
+ if (estado.estado === 'ok' && (ahora - estado.ultimoTimestamp) > STALE_MS) {
229
+ _providers.delete(provider);
230
+ }
231
+ }
232
+ },
233
+
234
+ /**
235
+ * Resetea todo el tracker (útil para tests).
236
+ */
237
+ reset() {
238
+ _providers.clear();
239
+ },
240
+ };
241
+
242
+ // ---------------------------------------------------------------------------
243
+ // Exports
244
+ // ---------------------------------------------------------------------------
245
+
246
+ module.exports = {
247
+ tracker,
248
+ // Exponer constantes para tests
249
+ MAX_429_CONSECUTIVOS,
250
+ BACKOFF_BASE_MS,
251
+ BACKOFF_MAX_MS,
252
+ HALF_OPEN_MS,
253
+ };