@saulwade/swl-ses 1.5.0 → 1.5.2
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/CLAUDE.md +19 -2
- package/README.md +561 -561
- package/agentes/arquitecto-swl.md +33 -1
- package/agentes/nemesis-auditor-swl.md +59 -19
- package/bin/swl-mcp-server.js +214 -214
- package/comandos/swl/.evolved.json +22 -22
- package/comandos/swl/contribuir.md +233 -233
- package/comandos/swl/nemesis.md +230 -56
- package/gateway/lib/event-channel.js +191 -191
- package/habilidades/backend-production-resilience/SKILL.md +288 -288
- package/habilidades/benchmark-memoria/SKILL.md +186 -186
- package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
- package/habilidades/doubt-driven-review/SKILL.md +171 -171
- package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
- package/habilidades/ejecutar-task-iterativo/SKILL.md +278 -278
- package/habilidades/eval-framework/SKILL.md +212 -212
- package/habilidades/feynman-auditor-swl/SKILL.md +123 -123
- package/habilidades/feynman-auditor-swl/recursos/preguntas-language-agnostic.md +108 -108
- package/habilidades/harness-claude-code/SKILL.md +299 -299
- package/habilidades/infra-github-actions/SKILL.md +166 -166
- package/habilidades/legacy-code-rescue/SKILL.md +267 -267
- package/habilidades/manejo-errores/.evolved.json +8 -8
- package/habilidades/meta-skills-estandar/SKILL.md +225 -1
- package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
- package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
- package/habilidades/nemesis-evaluacion-json/SKILL.md +266 -0
- package/habilidades/nemesis-redistribuir/SKILL.md +341 -0
- package/habilidades/node-experto/SKILL.md +105 -4
- package/habilidades/patrones-python/SKILL.md +229 -229
- package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
- package/habilidades/planear-fase/SKILL.md +319 -319
- package/habilidades/protocolo-revision-swl/SKILL.md +350 -276
- package/habilidades/release-semver/.evolved.json +8 -8
- package/habilidades/state-inconsistency-auditor-swl/SKILL.md +166 -166
- package/habilidades/state-inconsistency-auditor-swl/recursos/coupled-state-patterns.md +147 -147
- package/habilidades/tdd-workflow/SKILL.md +150 -4
- package/habilidades/testing-python/SKILL.md +340 -340
- package/habilidades/verificar-trabajo/SKILL.md +8 -3
- package/habilidades/web-fetcher-routing/SKILL.md +75 -75
- package/hooks/check-update.js +31 -3
- package/hooks/claudemd-bloat-detector.js +161 -161
- package/hooks/lib/agent-routing.js +107 -107
- package/hooks/lib/auto-consolidator.js +335 -335
- package/hooks/lib/error-classifier.js +308 -308
- package/hooks/lib/merkle-audit.js +96 -96
- package/hooks/lib/provenance-tracker.js +191 -191
- package/hooks/lib/rate-limit-tracker.js +253 -253
- package/hooks/lib/resource-quota.js +122 -122
- package/hooks/lib/retry-jitter.js +165 -165
- package/hooks/lib/security-net.js +201 -201
- package/hooks/lib/skill-auditor.js +588 -588
- package/hooks/lib/sync-status.js +228 -228
- package/hooks/lib/taint-tracker.js +107 -107
- package/hooks/lib/text-similarity.js +241 -241
- package/hooks/lib/toon-compressor.js +245 -245
- package/hooks/registro-turnos.js +209 -209
- package/hooks/sugerir-regenerar-inventario.js +170 -170
- package/hooks/validar-formato-post-subagente.js +140 -140
- package/hooks/validar-memoria-hook.js +218 -218
- package/instintos/prompt-appendices.yaml +57 -57
- package/manifiestos/agent-output-schemas.json +57 -57
- package/manifiestos/modulos.json +1324 -1321
- package/manifiestos/skills-lock.json +1114 -1114
- package/package.json +2 -2
- package/plantillas/auditor-veto-template.md +105 -105
- package/plantillas/github-workflows/README.md +47 -47
- package/plantillas/github-workflows/release-please.yml +44 -44
- package/plantillas/github-workflows/swl-ci.yml +107 -107
- package/plantillas/github-workflows/swl-security.yml +51 -51
- package/plugin.json +353 -351
- package/reglas/analisis-previo-tareas-grandes.md +172 -172
- package/reglas/arreglar-al-detectar.md +147 -147
- package/reglas/fragmentos-compartidos.md +152 -152
- package/reglas/harness-claude-code.md +213 -213
- package/reglas/registro-componentes-nuevos.md +192 -0
- package/reglas/usar-context7.md +226 -226
- package/schemas/diary-entry.schema.json +80 -80
- package/scripts/actualizar.js +110 -1
- package/scripts/audit-tools/audit-history.js +330 -330
- package/scripts/audit-tools/bundle-tracker.js +290 -290
- package/scripts/audit-tools/canary-monitor.js +352 -352
- package/scripts/audit-tools/code-profiler.js +605 -605
- package/scripts/audit-tools/dep-doctor.js +320 -320
- package/scripts/audit-tools/env-validator.js +206 -206
- package/scripts/audit-tools/lib/fs-walk.js +48 -48
- package/scripts/audit-tools/lib/output.js +23 -23
- package/scripts/audit-tools/migration-checker.js +392 -392
- package/scripts/audit-tools/pentest-scanner.js +1436 -1436
- package/scripts/benchmark-memoria.js +167 -167
- package/scripts/configurar-branch-protection.js +418 -418
- package/scripts/derivar-feature-list.js +489 -489
- package/scripts/detectar-aprendizajes-duplicados.js +151 -151
- package/scripts/doctor.js +58 -4
- package/scripts/field-report.js +199 -199
- package/scripts/generar-checklists-consolidados.js +273 -273
- package/scripts/generar-inventario.js +420 -420
- package/scripts/generar-matriz-lenguajes.js +271 -271
- package/scripts/lib/artefactos-python.js +43 -43
- package/scripts/lib/benchmark-metrics.js +160 -160
- package/scripts/lib/budget-enforcer.js +252 -252
- package/scripts/lib/configurar-ci.js +380 -380
- package/scripts/lib/contadores-inventario.js +217 -217
- package/scripts/lib/detectar-stack-detallado.js +307 -307
- package/scripts/lib/diary-entry.js +234 -234
- package/scripts/lib/eval-metrics-store.js +218 -218
- package/scripts/lib/eval-quality.js +171 -171
- package/scripts/lib/eval-schemas.js +144 -144
- package/scripts/lib/eval-self-correct.js +106 -106
- package/scripts/lib/eval-validator.js +185 -185
- package/scripts/lib/expandir-targets.js +71 -71
- package/scripts/lib/jaccard-similarity.js +98 -98
- package/scripts/lib/longmemeval-runner.js +125 -125
- package/scripts/lib/mcp_config.py +127 -0
- package/scripts/lib/npm-version.js +261 -261
- package/scripts/lib/paquetes-conocidos.js +50 -50
- package/scripts/lib/prompt-builder.js +264 -264
- package/scripts/lib/rrf-fusion.js +175 -175
- package/scripts/lib/scoring-instintos.js +277 -277
- package/scripts/lib/semantic-search.js +252 -252
- package/scripts/lib/toml-merge.js +204 -204
- package/scripts/lib/transformadores/codex.js +375 -375
- package/scripts/lib/transformadores/cursor.js +359 -359
- package/scripts/limpiar-artefactos-python.js +131 -131
- package/scripts/mcp-orchestrator.py +8 -18
- package/scripts/mcp-pool-manager.py +12 -23
- package/scripts/mcp-server/README.md +170 -170
- package/scripts/mcp-server/auth.js +105 -105
- package/scripts/mcp-server/cache.js +106 -106
- package/scripts/mcp-server/telemetry.js +78 -78
- package/scripts/migrar-csv-a-array.js +168 -168
- package/scripts/migrar-fase-dominio.js +201 -201
- package/scripts/publicar.js +511 -511
- package/scripts/run-eval.js +141 -141
- package/scripts/validar-userland-vacio.js +110 -110
|
@@ -1,122 +1,122 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Resource Quota — Control de consumo por ventana temporal.
|
|
5
|
-
*
|
|
6
|
-
* Adoptado de OpenFang (crates/openfang-kernel/src/metering.rs).
|
|
7
|
-
* Implementa quotas rolling por hora, dia y mes para tokens y costo USD.
|
|
8
|
-
*
|
|
9
|
-
* Zero dependencias externas.
|
|
10
|
-
*
|
|
11
|
-
* Uso:
|
|
12
|
-
* const { quota } = require('./lib/resource-quota');
|
|
13
|
-
* quota.registrar({ tokens: 1500, costoUsd: 0.005, agente: 'implementador-swl' });
|
|
14
|
-
* const check = quota.verificar();
|
|
15
|
-
* if (check.excedido) console.log(check.mensaje);
|
|
16
|
-
*
|
|
17
|
-
* @module hooks/lib/resource-quota
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
const DEFAULTS = {
|
|
21
|
-
maxTokensPorHora: 200000,
|
|
22
|
-
maxCostoPorHoraUsd: 2.00,
|
|
23
|
-
maxCostoPorDiaUsd: 10.00,
|
|
24
|
-
maxCostoPorMesUsd: 100.00,
|
|
25
|
-
alertAt: 0.80,
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const _registros = [];
|
|
29
|
-
const MAX_REGISTROS = 10000;
|
|
30
|
-
|
|
31
|
-
function _ahora() { return Date.now(); }
|
|
32
|
-
function _haceUnaHora() { return _ahora() - 3600000; }
|
|
33
|
-
function _haceUnDia() { return _ahora() - 86400000; }
|
|
34
|
-
function _haceUnMes() { return _ahora() - 2592000000; }
|
|
35
|
-
|
|
36
|
-
const quota = {
|
|
37
|
-
configurar(opciones) {
|
|
38
|
-
Object.assign(DEFAULTS, opciones);
|
|
39
|
-
},
|
|
40
|
-
|
|
41
|
-
registrar(datos) {
|
|
42
|
-
_registros.push({
|
|
43
|
-
tokens: datos.tokens || 0,
|
|
44
|
-
costoUsd: datos.costoUsd || 0,
|
|
45
|
-
agente: datos.agente || 'desconocido',
|
|
46
|
-
timestamp: _ahora(),
|
|
47
|
-
});
|
|
48
|
-
if (_registros.length > MAX_REGISTROS) {
|
|
49
|
-
_registros.splice(0, _registros.length - MAX_REGISTROS);
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
|
-
|
|
53
|
-
_sumarVentana(desdeMs) {
|
|
54
|
-
let tokens = 0, costo = 0;
|
|
55
|
-
for (let i = _registros.length - 1; i >= 0; i--) {
|
|
56
|
-
const r = _registros[i];
|
|
57
|
-
if (r.timestamp < desdeMs) break;
|
|
58
|
-
tokens += r.tokens;
|
|
59
|
-
costo += r.costoUsd;
|
|
60
|
-
}
|
|
61
|
-
return { tokens, costo };
|
|
62
|
-
},
|
|
63
|
-
|
|
64
|
-
verificar() {
|
|
65
|
-
const hora = this._sumarVentana(_haceUnaHora());
|
|
66
|
-
const dia = this._sumarVentana(_haceUnDia());
|
|
67
|
-
const mes = this._sumarVentana(_haceUnMes());
|
|
68
|
-
|
|
69
|
-
const violations = [];
|
|
70
|
-
|
|
71
|
-
if (hora.tokens >= DEFAULTS.maxTokensPorHora) {
|
|
72
|
-
violations.push({ tipo: 'tokens_hora', valor: hora.tokens, limite: DEFAULTS.maxTokensPorHora });
|
|
73
|
-
}
|
|
74
|
-
if (hora.costo >= DEFAULTS.maxCostoPorHoraUsd) {
|
|
75
|
-
violations.push({ tipo: 'costo_hora', valor: hora.costo, limite: DEFAULTS.maxCostoPorHoraUsd });
|
|
76
|
-
}
|
|
77
|
-
if (dia.costo >= DEFAULTS.maxCostoPorDiaUsd) {
|
|
78
|
-
violations.push({ tipo: 'costo_dia', valor: dia.costo, limite: DEFAULTS.maxCostoPorDiaUsd });
|
|
79
|
-
}
|
|
80
|
-
if (mes.costo >= DEFAULTS.maxCostoPorMesUsd) {
|
|
81
|
-
violations.push({ tipo: 'costo_mes', valor: mes.costo, limite: DEFAULTS.maxCostoPorMesUsd });
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const warnings = [];
|
|
85
|
-
const alertThreshold = DEFAULTS.alertAt;
|
|
86
|
-
|
|
87
|
-
if (hora.costo >= DEFAULTS.maxCostoPorHoraUsd * alertThreshold && !violations.find(v => v.tipo === 'costo_hora')) {
|
|
88
|
-
warnings.push({ tipo: 'costo_hora', valor: hora.costo, limite: DEFAULTS.maxCostoPorHoraUsd, pct: Math.round(hora.costo / DEFAULTS.maxCostoPorHoraUsd * 100) });
|
|
89
|
-
}
|
|
90
|
-
if (dia.costo >= DEFAULTS.maxCostoPorDiaUsd * alertThreshold && !violations.find(v => v.tipo === 'costo_dia')) {
|
|
91
|
-
warnings.push({ tipo: 'costo_dia', valor: dia.costo, limite: DEFAULTS.maxCostoPorDiaUsd, pct: Math.round(dia.costo / DEFAULTS.maxCostoPorDiaUsd * 100) });
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
excedido: violations.length > 0,
|
|
96
|
-
violations,
|
|
97
|
-
warnings,
|
|
98
|
-
consumo: { hora, dia, mes },
|
|
99
|
-
mensaje: violations.length > 0
|
|
100
|
-
? 'Quota excedida: ' + violations.map(v => v.tipo + ' (' + v.valor.toFixed(2) + '/' + v.limite + ')').join(', ')
|
|
101
|
-
: warnings.length > 0
|
|
102
|
-
? 'Alerta: ' + warnings.map(w => w.tipo + ' al ' + w.pct + '%').join(', ')
|
|
103
|
-
: 'Dentro de quota',
|
|
104
|
-
};
|
|
105
|
-
},
|
|
106
|
-
|
|
107
|
-
resumen() {
|
|
108
|
-
const hora = this._sumarVentana(_haceUnaHora());
|
|
109
|
-
const dia = this._sumarVentana(_haceUnDia());
|
|
110
|
-
const mes = this._sumarVentana(_haceUnMes());
|
|
111
|
-
return {
|
|
112
|
-
hora: { tokens: hora.tokens, costoUsd: Math.round(hora.costo * 10000) / 10000 },
|
|
113
|
-
dia: { tokens: dia.tokens, costoUsd: Math.round(dia.costo * 10000) / 10000 },
|
|
114
|
-
mes: { tokens: mes.tokens, costoUsd: Math.round(mes.costo * 10000) / 10000 },
|
|
115
|
-
registros: _registros.length,
|
|
116
|
-
};
|
|
117
|
-
},
|
|
118
|
-
|
|
119
|
-
reset() { _registros.length = 0; },
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
module.exports = { quota, DEFAULTS };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resource Quota — Control de consumo por ventana temporal.
|
|
5
|
+
*
|
|
6
|
+
* Adoptado de OpenFang (crates/openfang-kernel/src/metering.rs).
|
|
7
|
+
* Implementa quotas rolling por hora, dia y mes para tokens y costo USD.
|
|
8
|
+
*
|
|
9
|
+
* Zero dependencias externas.
|
|
10
|
+
*
|
|
11
|
+
* Uso:
|
|
12
|
+
* const { quota } = require('./lib/resource-quota');
|
|
13
|
+
* quota.registrar({ tokens: 1500, costoUsd: 0.005, agente: 'implementador-swl' });
|
|
14
|
+
* const check = quota.verificar();
|
|
15
|
+
* if (check.excedido) console.log(check.mensaje);
|
|
16
|
+
*
|
|
17
|
+
* @module hooks/lib/resource-quota
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const DEFAULTS = {
|
|
21
|
+
maxTokensPorHora: 200000,
|
|
22
|
+
maxCostoPorHoraUsd: 2.00,
|
|
23
|
+
maxCostoPorDiaUsd: 10.00,
|
|
24
|
+
maxCostoPorMesUsd: 100.00,
|
|
25
|
+
alertAt: 0.80,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const _registros = [];
|
|
29
|
+
const MAX_REGISTROS = 10000;
|
|
30
|
+
|
|
31
|
+
function _ahora() { return Date.now(); }
|
|
32
|
+
function _haceUnaHora() { return _ahora() - 3600000; }
|
|
33
|
+
function _haceUnDia() { return _ahora() - 86400000; }
|
|
34
|
+
function _haceUnMes() { return _ahora() - 2592000000; }
|
|
35
|
+
|
|
36
|
+
const quota = {
|
|
37
|
+
configurar(opciones) {
|
|
38
|
+
Object.assign(DEFAULTS, opciones);
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
registrar(datos) {
|
|
42
|
+
_registros.push({
|
|
43
|
+
tokens: datos.tokens || 0,
|
|
44
|
+
costoUsd: datos.costoUsd || 0,
|
|
45
|
+
agente: datos.agente || 'desconocido',
|
|
46
|
+
timestamp: _ahora(),
|
|
47
|
+
});
|
|
48
|
+
if (_registros.length > MAX_REGISTROS) {
|
|
49
|
+
_registros.splice(0, _registros.length - MAX_REGISTROS);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
_sumarVentana(desdeMs) {
|
|
54
|
+
let tokens = 0, costo = 0;
|
|
55
|
+
for (let i = _registros.length - 1; i >= 0; i--) {
|
|
56
|
+
const r = _registros[i];
|
|
57
|
+
if (r.timestamp < desdeMs) break;
|
|
58
|
+
tokens += r.tokens;
|
|
59
|
+
costo += r.costoUsd;
|
|
60
|
+
}
|
|
61
|
+
return { tokens, costo };
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
verificar() {
|
|
65
|
+
const hora = this._sumarVentana(_haceUnaHora());
|
|
66
|
+
const dia = this._sumarVentana(_haceUnDia());
|
|
67
|
+
const mes = this._sumarVentana(_haceUnMes());
|
|
68
|
+
|
|
69
|
+
const violations = [];
|
|
70
|
+
|
|
71
|
+
if (hora.tokens >= DEFAULTS.maxTokensPorHora) {
|
|
72
|
+
violations.push({ tipo: 'tokens_hora', valor: hora.tokens, limite: DEFAULTS.maxTokensPorHora });
|
|
73
|
+
}
|
|
74
|
+
if (hora.costo >= DEFAULTS.maxCostoPorHoraUsd) {
|
|
75
|
+
violations.push({ tipo: 'costo_hora', valor: hora.costo, limite: DEFAULTS.maxCostoPorHoraUsd });
|
|
76
|
+
}
|
|
77
|
+
if (dia.costo >= DEFAULTS.maxCostoPorDiaUsd) {
|
|
78
|
+
violations.push({ tipo: 'costo_dia', valor: dia.costo, limite: DEFAULTS.maxCostoPorDiaUsd });
|
|
79
|
+
}
|
|
80
|
+
if (mes.costo >= DEFAULTS.maxCostoPorMesUsd) {
|
|
81
|
+
violations.push({ tipo: 'costo_mes', valor: mes.costo, limite: DEFAULTS.maxCostoPorMesUsd });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const warnings = [];
|
|
85
|
+
const alertThreshold = DEFAULTS.alertAt;
|
|
86
|
+
|
|
87
|
+
if (hora.costo >= DEFAULTS.maxCostoPorHoraUsd * alertThreshold && !violations.find(v => v.tipo === 'costo_hora')) {
|
|
88
|
+
warnings.push({ tipo: 'costo_hora', valor: hora.costo, limite: DEFAULTS.maxCostoPorHoraUsd, pct: Math.round(hora.costo / DEFAULTS.maxCostoPorHoraUsd * 100) });
|
|
89
|
+
}
|
|
90
|
+
if (dia.costo >= DEFAULTS.maxCostoPorDiaUsd * alertThreshold && !violations.find(v => v.tipo === 'costo_dia')) {
|
|
91
|
+
warnings.push({ tipo: 'costo_dia', valor: dia.costo, limite: DEFAULTS.maxCostoPorDiaUsd, pct: Math.round(dia.costo / DEFAULTS.maxCostoPorDiaUsd * 100) });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
excedido: violations.length > 0,
|
|
96
|
+
violations,
|
|
97
|
+
warnings,
|
|
98
|
+
consumo: { hora, dia, mes },
|
|
99
|
+
mensaje: violations.length > 0
|
|
100
|
+
? 'Quota excedida: ' + violations.map(v => v.tipo + ' (' + v.valor.toFixed(2) + '/' + v.limite + ')').join(', ')
|
|
101
|
+
: warnings.length > 0
|
|
102
|
+
? 'Alerta: ' + warnings.map(w => w.tipo + ' al ' + w.pct + '%').join(', ')
|
|
103
|
+
: 'Dentro de quota',
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
resumen() {
|
|
108
|
+
const hora = this._sumarVentana(_haceUnaHora());
|
|
109
|
+
const dia = this._sumarVentana(_haceUnDia());
|
|
110
|
+
const mes = this._sumarVentana(_haceUnMes());
|
|
111
|
+
return {
|
|
112
|
+
hora: { tokens: hora.tokens, costoUsd: Math.round(hora.costo * 10000) / 10000 },
|
|
113
|
+
dia: { tokens: dia.tokens, costoUsd: Math.round(dia.costo * 10000) / 10000 },
|
|
114
|
+
mes: { tokens: mes.tokens, costoUsd: Math.round(mes.costo * 10000) / 10000 },
|
|
115
|
+
registros: _registros.length,
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
reset() { _registros.length = 0; },
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
module.exports = { quota, DEFAULTS };
|
|
@@ -1,165 +1,165 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Retry con Jitter Decorrelado — Backoff exponencial con decorrelación.
|
|
5
|
-
*
|
|
6
|
-
* Patrón adoptado de Hermes Agent (agent/retry_utils.py).
|
|
7
|
-
* Usa seed basado en time + counter con golden ratio para decorrelación
|
|
8
|
-
* entre sesiones concurrentes — evita thundering herd.
|
|
9
|
-
*
|
|
10
|
-
* Zero dependencias externas — usa crypto nativo de Node.js.
|
|
11
|
-
*
|
|
12
|
-
* Uso:
|
|
13
|
-
* const { jitteredBackoff, crearRetrier } = require('./lib/retry-jitter');
|
|
14
|
-
*
|
|
15
|
-
* // Calcular delay para un intento específico
|
|
16
|
-
* const delay = jitteredBackoff(2, { baseDelay: 5, maxDelay: 120 });
|
|
17
|
-
*
|
|
18
|
-
* // Usar retrier completo
|
|
19
|
-
* const retrier = crearRetrier({ maxIntentos: 3, baseDelay: 5 });
|
|
20
|
-
* const resultado = await retrier.ejecutar(async () => {
|
|
21
|
-
* return await llamadaAPI();
|
|
22
|
-
* });
|
|
23
|
-
*
|
|
24
|
-
* @module hooks/lib/retry-jitter
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
const crypto = require('crypto');
|
|
28
|
-
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
// Constantes
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
|
|
33
|
-
/** Golden ratio × 2^32 — constante de dispersión de Fibonacci hashing. */
|
|
34
|
-
const GOLDEN_RATIO_32 = 0x9E3779B9;
|
|
35
|
-
|
|
36
|
-
/** Contador global para decorrelación entre llamadas concurrentes. */
|
|
37
|
-
let _jitterCounter = 0;
|
|
38
|
-
|
|
39
|
-
// ---------------------------------------------------------------------------
|
|
40
|
-
// Funciones core
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Calcula delay con backoff exponencial + jitter decorrelado.
|
|
45
|
-
*
|
|
46
|
-
* Delay = min(base × 2^(intento-1), maxDelay) + jitter
|
|
47
|
-
* Jitter = random(0, jitterRatio × delay)
|
|
48
|
-
*
|
|
49
|
-
* El seed para el random combina timestamp con un contador global
|
|
50
|
-
* multiplicado por la constante golden ratio, lo que garantiza
|
|
51
|
-
* que llamadas concurrentes desde diferentes hilos/procesos
|
|
52
|
-
* NO coincidan en el mismo delay.
|
|
53
|
-
*
|
|
54
|
-
* @param {number} intento - Número de intento (1-indexed)
|
|
55
|
-
* @param {object} [opciones]
|
|
56
|
-
* @param {number} [opciones.baseDelay=5] - Delay base en segundos
|
|
57
|
-
* @param {number} [opciones.maxDelay=120] - Delay máximo en segundos
|
|
58
|
-
* @param {number} [opciones.jitterRatio=0.5] - Proporción de jitter (0-1)
|
|
59
|
-
* @returns {number} Delay en segundos (con decimales)
|
|
60
|
-
*/
|
|
61
|
-
function jitteredBackoff(intento, opciones = {}) {
|
|
62
|
-
const {
|
|
63
|
-
baseDelay = 5,
|
|
64
|
-
maxDelay = 120,
|
|
65
|
-
jitterRatio = 0.5,
|
|
66
|
-
} = opciones;
|
|
67
|
-
|
|
68
|
-
_jitterCounter += 1;
|
|
69
|
-
const tick = _jitterCounter;
|
|
70
|
-
|
|
71
|
-
// Backoff exponencial acotado
|
|
72
|
-
const exponente = Math.max(0, intento - 1);
|
|
73
|
-
const delay = Math.min(baseDelay * Math.pow(2, exponente), maxDelay);
|
|
74
|
-
|
|
75
|
-
// Seed decorrelado: timestamp XOR (counter × golden_ratio)
|
|
76
|
-
const nowNs = BigInt(Date.now()) * 1_000_000n; // ms → pseudo-ns
|
|
77
|
-
const seed = Number((nowNs ^ BigInt(tick * GOLDEN_RATIO_32)) & 0xFFFFFFFFn);
|
|
78
|
-
|
|
79
|
-
// PRNG determinista basado en seed (sin crypto para velocidad)
|
|
80
|
-
const hash = crypto.createHash('md5').update(String(seed)).digest();
|
|
81
|
-
const pseudoRandom = hash.readUInt32LE(0) / 0xFFFFFFFF;
|
|
82
|
-
|
|
83
|
-
const jitter = pseudoRandom * jitterRatio * delay;
|
|
84
|
-
|
|
85
|
-
return delay + jitter;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Espera un número de segundos (para uso con async/await).
|
|
90
|
-
*
|
|
91
|
-
* @param {number} segundos
|
|
92
|
-
* @returns {Promise<void>}
|
|
93
|
-
*/
|
|
94
|
-
function esperar(segundos) {
|
|
95
|
-
return new Promise(resolve => setTimeout(resolve, Math.round(segundos * 1000)));
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Crea un retrier configurable con jitter decorrelado.
|
|
100
|
-
*
|
|
101
|
-
* @param {object} [opciones]
|
|
102
|
-
* @param {number} [opciones.maxIntentos=3] - Máximo de intentos
|
|
103
|
-
* @param {number} [opciones.baseDelay=5] - Delay base en segundos
|
|
104
|
-
* @param {number} [opciones.maxDelay=120] - Delay máximo en segundos
|
|
105
|
-
* @param {number} [opciones.jitterRatio=0.5] - Proporción de jitter
|
|
106
|
-
* @param {Function} [opciones.esRetryable] - Función que determina si un error es retryable
|
|
107
|
-
* @param {Function} [opciones.onReintento] - Callback antes de cada reintento
|
|
108
|
-
* @returns {object} Retrier con método ejecutar()
|
|
109
|
-
*/
|
|
110
|
-
function crearRetrier(opciones = {}) {
|
|
111
|
-
const {
|
|
112
|
-
maxIntentos = 3,
|
|
113
|
-
baseDelay = 5,
|
|
114
|
-
maxDelay = 120,
|
|
115
|
-
jitterRatio = 0.5,
|
|
116
|
-
esRetryable = () => true,
|
|
117
|
-
onReintento = null,
|
|
118
|
-
} = opciones;
|
|
119
|
-
|
|
120
|
-
return {
|
|
121
|
-
/**
|
|
122
|
-
* Ejecuta una función con reintentos automáticos.
|
|
123
|
-
*
|
|
124
|
-
* @template T
|
|
125
|
-
* @param {() => Promise<T>} fn - Función async a ejecutar
|
|
126
|
-
* @returns {Promise<T>} Resultado de la función
|
|
127
|
-
* @throws {Error} Error del último intento si todos fallan
|
|
128
|
-
*/
|
|
129
|
-
async ejecutar(fn) {
|
|
130
|
-
let ultimoError;
|
|
131
|
-
|
|
132
|
-
for (let intento = 1; intento <= maxIntentos; intento++) {
|
|
133
|
-
try {
|
|
134
|
-
return await fn();
|
|
135
|
-
} catch (error) {
|
|
136
|
-
ultimoError = error;
|
|
137
|
-
|
|
138
|
-
if (intento >= maxIntentos || !esRetryable(error)) {
|
|
139
|
-
throw error;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const delay = jitteredBackoff(intento, { baseDelay, maxDelay, jitterRatio });
|
|
143
|
-
|
|
144
|
-
if (onReintento) {
|
|
145
|
-
onReintento({ intento, maxIntentos, delay, error });
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
await esperar(delay);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
throw ultimoError;
|
|
153
|
-
},
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// ---------------------------------------------------------------------------
|
|
158
|
-
// Exports
|
|
159
|
-
// ---------------------------------------------------------------------------
|
|
160
|
-
|
|
161
|
-
module.exports = {
|
|
162
|
-
jitteredBackoff,
|
|
163
|
-
esperar,
|
|
164
|
-
crearRetrier,
|
|
165
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Retry con Jitter Decorrelado — Backoff exponencial con decorrelación.
|
|
5
|
+
*
|
|
6
|
+
* Patrón adoptado de Hermes Agent (agent/retry_utils.py).
|
|
7
|
+
* Usa seed basado en time + counter con golden ratio para decorrelación
|
|
8
|
+
* entre sesiones concurrentes — evita thundering herd.
|
|
9
|
+
*
|
|
10
|
+
* Zero dependencias externas — usa crypto nativo de Node.js.
|
|
11
|
+
*
|
|
12
|
+
* Uso:
|
|
13
|
+
* const { jitteredBackoff, crearRetrier } = require('./lib/retry-jitter');
|
|
14
|
+
*
|
|
15
|
+
* // Calcular delay para un intento específico
|
|
16
|
+
* const delay = jitteredBackoff(2, { baseDelay: 5, maxDelay: 120 });
|
|
17
|
+
*
|
|
18
|
+
* // Usar retrier completo
|
|
19
|
+
* const retrier = crearRetrier({ maxIntentos: 3, baseDelay: 5 });
|
|
20
|
+
* const resultado = await retrier.ejecutar(async () => {
|
|
21
|
+
* return await llamadaAPI();
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* @module hooks/lib/retry-jitter
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const crypto = require('crypto');
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Constantes
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/** Golden ratio × 2^32 — constante de dispersión de Fibonacci hashing. */
|
|
34
|
+
const GOLDEN_RATIO_32 = 0x9E3779B9;
|
|
35
|
+
|
|
36
|
+
/** Contador global para decorrelación entre llamadas concurrentes. */
|
|
37
|
+
let _jitterCounter = 0;
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Funciones core
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Calcula delay con backoff exponencial + jitter decorrelado.
|
|
45
|
+
*
|
|
46
|
+
* Delay = min(base × 2^(intento-1), maxDelay) + jitter
|
|
47
|
+
* Jitter = random(0, jitterRatio × delay)
|
|
48
|
+
*
|
|
49
|
+
* El seed para el random combina timestamp con un contador global
|
|
50
|
+
* multiplicado por la constante golden ratio, lo que garantiza
|
|
51
|
+
* que llamadas concurrentes desde diferentes hilos/procesos
|
|
52
|
+
* NO coincidan en el mismo delay.
|
|
53
|
+
*
|
|
54
|
+
* @param {number} intento - Número de intento (1-indexed)
|
|
55
|
+
* @param {object} [opciones]
|
|
56
|
+
* @param {number} [opciones.baseDelay=5] - Delay base en segundos
|
|
57
|
+
* @param {number} [opciones.maxDelay=120] - Delay máximo en segundos
|
|
58
|
+
* @param {number} [opciones.jitterRatio=0.5] - Proporción de jitter (0-1)
|
|
59
|
+
* @returns {number} Delay en segundos (con decimales)
|
|
60
|
+
*/
|
|
61
|
+
function jitteredBackoff(intento, opciones = {}) {
|
|
62
|
+
const {
|
|
63
|
+
baseDelay = 5,
|
|
64
|
+
maxDelay = 120,
|
|
65
|
+
jitterRatio = 0.5,
|
|
66
|
+
} = opciones;
|
|
67
|
+
|
|
68
|
+
_jitterCounter += 1;
|
|
69
|
+
const tick = _jitterCounter;
|
|
70
|
+
|
|
71
|
+
// Backoff exponencial acotado
|
|
72
|
+
const exponente = Math.max(0, intento - 1);
|
|
73
|
+
const delay = Math.min(baseDelay * Math.pow(2, exponente), maxDelay);
|
|
74
|
+
|
|
75
|
+
// Seed decorrelado: timestamp XOR (counter × golden_ratio)
|
|
76
|
+
const nowNs = BigInt(Date.now()) * 1_000_000n; // ms → pseudo-ns
|
|
77
|
+
const seed = Number((nowNs ^ BigInt(tick * GOLDEN_RATIO_32)) & 0xFFFFFFFFn);
|
|
78
|
+
|
|
79
|
+
// PRNG determinista basado en seed (sin crypto para velocidad)
|
|
80
|
+
const hash = crypto.createHash('md5').update(String(seed)).digest();
|
|
81
|
+
const pseudoRandom = hash.readUInt32LE(0) / 0xFFFFFFFF;
|
|
82
|
+
|
|
83
|
+
const jitter = pseudoRandom * jitterRatio * delay;
|
|
84
|
+
|
|
85
|
+
return delay + jitter;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Espera un número de segundos (para uso con async/await).
|
|
90
|
+
*
|
|
91
|
+
* @param {number} segundos
|
|
92
|
+
* @returns {Promise<void>}
|
|
93
|
+
*/
|
|
94
|
+
function esperar(segundos) {
|
|
95
|
+
return new Promise(resolve => setTimeout(resolve, Math.round(segundos * 1000)));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Crea un retrier configurable con jitter decorrelado.
|
|
100
|
+
*
|
|
101
|
+
* @param {object} [opciones]
|
|
102
|
+
* @param {number} [opciones.maxIntentos=3] - Máximo de intentos
|
|
103
|
+
* @param {number} [opciones.baseDelay=5] - Delay base en segundos
|
|
104
|
+
* @param {number} [opciones.maxDelay=120] - Delay máximo en segundos
|
|
105
|
+
* @param {number} [opciones.jitterRatio=0.5] - Proporción de jitter
|
|
106
|
+
* @param {Function} [opciones.esRetryable] - Función que determina si un error es retryable
|
|
107
|
+
* @param {Function} [opciones.onReintento] - Callback antes de cada reintento
|
|
108
|
+
* @returns {object} Retrier con método ejecutar()
|
|
109
|
+
*/
|
|
110
|
+
function crearRetrier(opciones = {}) {
|
|
111
|
+
const {
|
|
112
|
+
maxIntentos = 3,
|
|
113
|
+
baseDelay = 5,
|
|
114
|
+
maxDelay = 120,
|
|
115
|
+
jitterRatio = 0.5,
|
|
116
|
+
esRetryable = () => true,
|
|
117
|
+
onReintento = null,
|
|
118
|
+
} = opciones;
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
/**
|
|
122
|
+
* Ejecuta una función con reintentos automáticos.
|
|
123
|
+
*
|
|
124
|
+
* @template T
|
|
125
|
+
* @param {() => Promise<T>} fn - Función async a ejecutar
|
|
126
|
+
* @returns {Promise<T>} Resultado de la función
|
|
127
|
+
* @throws {Error} Error del último intento si todos fallan
|
|
128
|
+
*/
|
|
129
|
+
async ejecutar(fn) {
|
|
130
|
+
let ultimoError;
|
|
131
|
+
|
|
132
|
+
for (let intento = 1; intento <= maxIntentos; intento++) {
|
|
133
|
+
try {
|
|
134
|
+
return await fn();
|
|
135
|
+
} catch (error) {
|
|
136
|
+
ultimoError = error;
|
|
137
|
+
|
|
138
|
+
if (intento >= maxIntentos || !esRetryable(error)) {
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const delay = jitteredBackoff(intento, { baseDelay, maxDelay, jitterRatio });
|
|
143
|
+
|
|
144
|
+
if (onReintento) {
|
|
145
|
+
onReintento({ intento, maxIntentos, delay, error });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
await esperar(delay);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
throw ultimoError;
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// Exports
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
module.exports = {
|
|
162
|
+
jitteredBackoff,
|
|
163
|
+
esperar,
|
|
164
|
+
crearRetrier,
|
|
165
|
+
};
|