wolverine-ai 4.4.0 → 4.5.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wolverine-ai",
3
- "version": "4.4.0",
3
+ "version": "4.5.0",
4
4
  "description": "Self-healing Node.js server framework powered by AI. Catches crashes, diagnoses errors, generates fixes, verifies, and restarts — automatically.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -118,7 +118,7 @@ const SEED_DOCS = [
118
118
  metadata: { topic: "heal-escalation" },
119
119
  },
120
120
  {
121
- text: "Process manager: wolverine monitors memory (RSS/heap) every 10s, detects memory leaks (N consecutive growth samples → auto-restart), enforces memory limit (default 512MB), tracks CPU%, probes all routes every 30s, detects response time degradation trends (stable/degrading/improving). Analytics dashboard shows memory/CPU charts and per-route health.",
121
+ text: "Process manager: wolverine monitors memory (RSS/heap) every 10s, detects memory leaks (N consecutive growth samples → auto-restart), enforces memory limit (default 512MB), tracks CPU%, probes all routes every 30s, detects response time degradation trends (stable/degrading/improving). Adaptive rate limiter: auto-injected via error-hook.js into Fastify/Express. Three zones based on CPU + memory: GREEN (<70%) = full throughput, YELLOW (70-85%) = shed 30% of requests, RED (>85%) = reject non-essential with 503 + Retry-After. Always allows health checks and wolverine internal routes. Reserves 200MB for heal tools. Enable: WOLVERINE_ADAPTIVE_LIMIT=true (default). Disable: WOLVERINE_ADAPTIVE_LIMIT=false. Response includes X-Wolverine-Zone header.",
122
122
  metadata: { topic: "process-manager" },
123
123
  },
124
124
  {
@@ -72,6 +72,28 @@ Module._load = function (request, parent, isMain) {
72
72
  };
73
73
 
74
74
  function _hookFastify(fastify) {
75
+ // Adaptive rate limiter — auto-protects based on CPU/memory pressure
76
+ if (process.env.WOLVERINE_ADAPTIVE_LIMIT !== "false") {
77
+ try {
78
+ const { getLimiter } = require("../monitor/adaptive-limiter");
79
+ const limiter = getLimiter();
80
+ fastify.addHook("onRequest", function (request, reply, done) {
81
+ if (!limiter.shouldAllow(request.url, request.headers)) {
82
+ const status = limiter.getStatus();
83
+ reply.code(503).header("Retry-After", "5").header("X-Wolverine-Zone", status.zone).send({
84
+ error: "Service temporarily unavailable",
85
+ zone: status.zone,
86
+ cpu: status.cpuAvg + "%",
87
+ memory: status.memAvg + "%",
88
+ retry_after: 5,
89
+ });
90
+ return;
91
+ }
92
+ done();
93
+ });
94
+ } catch {}
95
+ }
96
+
75
97
  // Wrap setErrorHandler so our IPC reporting runs BEFORE the user's handler
76
98
  const origSetError = fastify.setErrorHandler;
77
99
  let customErrorHandlerSet = false;
@@ -107,6 +129,24 @@ function _hookFastify(fastify) {
107
129
  }
108
130
 
109
131
  function _hookExpress(app) {
132
+ // Adaptive rate limiter for Express
133
+ if (process.env.WOLVERINE_ADAPTIVE_LIMIT !== "false") {
134
+ try {
135
+ const { getLimiter } = require("../monitor/adaptive-limiter");
136
+ const limiter = getLimiter();
137
+ app.use(function _wolverineAdaptiveLimiter(req, res, next) {
138
+ if (!limiter.shouldAllow(req.url, req.headers)) {
139
+ const status = limiter.getStatus();
140
+ res.status(503).set("Retry-After", "5").set("X-Wolverine-Zone", status.zone).json({
141
+ error: "Service temporarily unavailable", zone: status.zone, retry_after: 5,
142
+ });
143
+ return;
144
+ }
145
+ next();
146
+ });
147
+ } catch {}
148
+ }
149
+
110
150
  // Wrap app.listen to inject error middleware AFTER all user middleware
111
151
  const originalListen = app.listen;
112
152
  app.listen = function (...args) {
@@ -0,0 +1,142 @@
1
+ const os = require("os");
2
+
3
+ /**
4
+ * Adaptive Rate Limiter — auto-protects server based on system resources.
5
+ *
6
+ * Injected into child server via error-hook.js. Monitors CPU and memory
7
+ * in real-time and throttles incoming requests when the server is under
8
+ * pressure, reserving ~20% headroom for wolverine's heal tools.
9
+ *
10
+ * Three zones:
11
+ * GREEN (< 70% resources) — no limiting, full throughput
12
+ * YELLOW (70-85%) — shed new connections gradually
13
+ * RED (> 85%) — reject non-essential requests with 503
14
+ *
15
+ * Enable: WOLVERINE_ADAPTIVE_LIMIT=true (or settings.json adaptiveLimiter.enabled)
16
+ * Disable: WOLVERINE_ADAPTIVE_LIMIT=false (default: enabled)
17
+ *
18
+ * The limiter NEVER blocks:
19
+ * - Health check routes (/health, /healthz, /ready)
20
+ * - Wolverine internal routes (/api/v1/heartbeat, /api/v1/register)
21
+ * - Requests with X-Wolverine-Internal header
22
+ */
23
+
24
+ const SAMPLE_INTERVAL_MS = 5000;
25
+ const HISTORY_SIZE = 12; // 1 minute of samples at 5s intervals
26
+ const EXEMPT_PATHS = new Set(["/health", "/healthz", "/ready", "/api/v1/heartbeat", "/api/v1/register"]);
27
+
28
+ class AdaptiveLimiter {
29
+ constructor(opts = {}) {
30
+ this.enabled = opts.enabled !== false;
31
+ this.thresholdYellow = opts.thresholdYellow || 70; // % — start shedding
32
+ this.thresholdRed = opts.thresholdRed || 85; // % — reject non-essential
33
+ this.reserveMB = opts.reserveMB || 200; // MB reserved for wolverine tools
34
+
35
+ this._cpuHistory = [];
36
+ this._memHistory = [];
37
+ this._lastCpuTimes = os.cpus().map(c => ({ idle: c.times.idle, total: Object.values(c.times).reduce((a, b) => a + b) }));
38
+ this._zone = "green";
39
+ this._requestCount = 0;
40
+ this._rejectedCount = 0;
41
+ this._timer = null;
42
+
43
+ if (this.enabled) {
44
+ this._timer = setInterval(() => this._sample(), SAMPLE_INTERVAL_MS);
45
+ this._timer.unref(); // don't prevent process exit
46
+ }
47
+ }
48
+
49
+ _sample() {
50
+ // CPU usage
51
+ const cpus = os.cpus();
52
+ let totalIdle = 0, totalTick = 0;
53
+ for (let i = 0; i < cpus.length; i++) {
54
+ const prev = this._lastCpuTimes[i] || { idle: 0, total: 0 };
55
+ const total = Object.values(cpus[i].times).reduce((a, b) => a + b);
56
+ const idle = cpus[i].times.idle;
57
+ totalIdle += idle - prev.idle;
58
+ totalTick += total - prev.total;
59
+ this._lastCpuTimes[i] = { idle, total };
60
+ }
61
+ const cpuPct = totalTick > 0 ? Math.round((1 - totalIdle / totalTick) * 100) : 0;
62
+
63
+ // Memory usage (% of total, accounting for reserve)
64
+ const totalMem = os.totalmem();
65
+ const freeMem = os.freemem();
66
+ const reserveBytes = this.reserveMB * 1048576;
67
+ const effectiveFree = Math.max(0, freeMem - reserveBytes);
68
+ const memPct = Math.round((1 - effectiveFree / totalMem) * 100);
69
+
70
+ this._cpuHistory.push(cpuPct);
71
+ this._memHistory.push(memPct);
72
+ if (this._cpuHistory.length > HISTORY_SIZE) this._cpuHistory.shift();
73
+ if (this._memHistory.length > HISTORY_SIZE) this._memHistory.shift();
74
+
75
+ // Use max of CPU and memory pressure
76
+ const avgCpu = this._cpuHistory.reduce((a, b) => a + b, 0) / this._cpuHistory.length;
77
+ const avgMem = this._memHistory.reduce((a, b) => a + b, 0) / this._memHistory.length;
78
+ const pressure = Math.max(avgCpu, avgMem);
79
+
80
+ if (pressure >= this.thresholdRed) this._zone = "red";
81
+ else if (pressure >= this.thresholdYellow) this._zone = "yellow";
82
+ else this._zone = "green";
83
+ }
84
+
85
+ /**
86
+ * Middleware function for Fastify (onRequest hook) or Express.
87
+ * Returns true if request should be allowed, false if rejected.
88
+ */
89
+ shouldAllow(url, headers) {
90
+ if (!this.enabled) return true;
91
+
92
+ this._requestCount++;
93
+
94
+ // Always allow exempt paths and internal requests
95
+ const path = (url || "").split("?")[0];
96
+ if (EXEMPT_PATHS.has(path)) return true;
97
+ if (headers?.["x-wolverine-internal"]) return true;
98
+
99
+ if (this._zone === "green") return true;
100
+
101
+ if (this._zone === "yellow") {
102
+ // Probabilistic shedding — reject ~30% of requests
103
+ if (Math.random() < 0.3) {
104
+ this._rejectedCount++;
105
+ return false;
106
+ }
107
+ return true;
108
+ }
109
+
110
+ // RED zone — reject all non-exempt
111
+ this._rejectedCount++;
112
+ return false;
113
+ }
114
+
115
+ /**
116
+ * Get current limiter status for health endpoints / dashboard.
117
+ */
118
+ getStatus() {
119
+ return {
120
+ enabled: this.enabled,
121
+ zone: this._zone,
122
+ cpuAvg: this._cpuHistory.length > 0 ? Math.round(this._cpuHistory.reduce((a, b) => a + b, 0) / this._cpuHistory.length) : 0,
123
+ memAvg: this._memHistory.length > 0 ? Math.round(this._memHistory.reduce((a, b) => a + b, 0) / this._memHistory.length) : 0,
124
+ totalRequests: this._requestCount,
125
+ rejectedRequests: this._rejectedCount,
126
+ reserveMB: this.reserveMB,
127
+ };
128
+ }
129
+
130
+ stop() {
131
+ if (this._timer) { clearInterval(this._timer); this._timer = null; }
132
+ }
133
+ }
134
+
135
+ // Singleton — shared across the child process
136
+ let _instance = null;
137
+ function getLimiter(opts) {
138
+ if (!_instance) _instance = new AdaptiveLimiter(opts);
139
+ return _instance;
140
+ }
141
+
142
+ module.exports = { AdaptiveLimiter, getLimiter };