wolverine-ai 2.8.2 → 2.9.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/README.md CHANGED
@@ -429,6 +429,22 @@ Wolverine (single process manager)
429
429
 
430
430
  ---
431
431
 
432
+ ## Token Protection
433
+
434
+ Three layers prevent token waste:
435
+
436
+ | Layer | What it catches | Cost |
437
+ |-------|----------------|------|
438
+ | **Empty stderr guard** | Signal kills, clean shutdowns with no error | $0.00 |
439
+ | **Loop guard** | Same error failing 3+ times in 10min → files bug report, stops healing | $0.00 after detection |
440
+ | **Global rate limit** | Max 5 heals per 5 minutes regardless of error | Caps total spend |
441
+
442
+ **Process dedup:** PID file ensures only one wolverine instance runs. Kills old process on startup.
443
+
444
+ **Bug reports:** When loop guard triggers, generates a security-scanned report (no secrets/injection patterns) and sends to the platform backend for human review.
445
+
446
+ ---
447
+
432
448
  ## Cost Optimization
433
449
 
434
450
  Wolverine minimizes AI spend through 7 techniques:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wolverine-ai",
3
- "version": "2.8.2",
3
+ "version": "2.9.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": {
@@ -34,11 +34,11 @@
34
34
  "hybrid_settings": {
35
35
  "reasoning": "claude-haiku-4-5",
36
36
  "coding": "claude-sonnet-4-6",
37
- "chat": "claude-haiku-4-5",
37
+ "chat": "gpt-5-nano",
38
38
  "tool": "claude-sonnet-4-6",
39
39
  "classifier": "gpt-4o-mini",
40
40
  "audit": "gpt-4o-mini",
41
- "compacting": "claude-haiku-4-5",
41
+ "compacting": "gpt-4o-mini",
42
42
  "research": "o4-mini-deep-research",
43
43
  "embedding": "text-embedding-3-small"
44
44
  },
@@ -249,6 +249,14 @@ const SEED_DOCS = [
249
249
  text: "Auto-update: wolverine checks npm registry hourly for new versions. When found, runs npm install wolverine-ai@latest, backs up settings.json/.env.local before update and restores after. Config: autoUpdate.enabled (default true) in settings.json. Disable with WOLVERINE_AUTO_UPDATE=false env var. On successful update, signals runner to restart. Protected files never overwritten: settings.json, .env.local, .env, db.js. Update check runs 30s after startup then every hour (configurable via autoUpdate.intervalMs).",
250
250
  metadata: { topic: "auto-update" },
251
251
  },
252
+ {
253
+ text: "Loop guard: detects infinite heal loops and stops burning tokens. Tracks heal attempts by error signature — if 3+ heals fail on same error in 10 minutes, STOPS healing and generates a bug report. Bug report sent to platform backend for human review (security scanned for injection/secrets first). 30-minute cooldown after bug report filed. Process dedup via PID file (.wolverine/wolverine.pid) ensures only one wolverine instance runs — kills old process on startup. Config: WOLVERINE_LOOP_MAX_ATTEMPTS (default 3), WOLVERINE_LOOP_WINDOW_MS (default 600000).",
254
+ metadata: { topic: "loop-guard" },
255
+ },
256
+ {
257
+ text: "Token waste prevention: 3 layers. (1) Empty stderr guard — signal kills with no error output just restart, no AI ($0.00). (2) Loop guard — 3 failed heals on same error → stop and file bug report, no more AI calls. (3) Global rate limit — max 5 heals per 5 minutes regardless of error signature. Idle server burns exactly $0.00 in tokens.",
258
+ metadata: { topic: "token-protection" },
259
+ },
252
260
  {
253
261
  text: "Cost optimization: 7 techniques reduce heal cost from $0.31 to $0.02 for simple errors. (1) Verifier skips route probe for simple errors (TypeError/ReferenceError/SyntaxError) — trusts syntax+boot, ErrorMonitor is safety net. Prevents false-rejection cascades. (2) Sub-agents use Haiku (classifier model) for explore/plan/verify/research — only fixer uses Sonnet/Opus. 6 Haiku calls=$0.006 vs 6 Sonnet calls=$0.12. (3) Agent context compacted every 3 turns using compacting model — prevents 15K→95K token blowup. (4) Brain checked for cached fix patterns before AI — repeat errors cost $0. (5) Token budgets capped by error complexity: simple=20K agent budget, moderate=50K, complex=100K. Simple errors get 4 agent turns max. (6) Prior attempt summaries (not full context) passed between iterations — concise 'do NOT repeat' directives. (7) Fast path includes last known good backup code so AI can revert broken additions instead of patching around them.",
254
262
  metadata: { topic: "cost-optimization" },
@@ -23,6 +23,7 @@ const { Notifier } = require("../notifications/notifier");
23
23
  const { loadConfig } = require("./config");
24
24
  const { ErrorMonitor } = require("../monitor/error-monitor");
25
25
  const { startAutoUpdate, stopAutoUpdate } = require("../platform/auto-update");
26
+ const { LoopGuard, ensureSingleProcess } = require("../skills/loop-guard");
26
27
 
27
28
  /**
28
29
  * The Wolverine process runner — v3.
@@ -104,6 +105,12 @@ class WolverineRunner {
104
105
  onError: (routePath, errorDetails) => this._healFromError(routePath, errorDetails),
105
106
  });
106
107
 
108
+ // Loop guard — detects infinite heal loops, generates bug reports
109
+ this.loopGuard = new LoopGuard(this.cwd, {
110
+ maxAttempts: parseInt(process.env.WOLVERINE_LOOP_MAX_ATTEMPTS, 10) || 3,
111
+ windowMs: parseInt(process.env.WOLVERINE_LOOP_WINDOW_MS, 10) || 600000,
112
+ });
113
+
107
114
  // Brain — semantic memory + project context
108
115
  this.brain = new Brain(this.cwd);
109
116
 
@@ -147,6 +154,9 @@ class WolverineRunner {
147
154
  }
148
155
 
149
156
  async start() {
157
+ // Ensure only one wolverine instance runs — kill any old process
158
+ ensureSingleProcess(this.cwd);
159
+
150
160
  this.running = true;
151
161
  this.retryCount = 0;
152
162
 
@@ -405,12 +415,21 @@ class WolverineRunner {
405
415
 
406
416
  if (!this.running) return;
407
417
 
408
- if (code === 0 || signal === "SIGTERM") {
418
+ // Clean exit or graceful shutdown don't heal
419
+ if (code === 0 || signal === "SIGTERM" || signal === "SIGINT") {
409
420
  console.log(chalk.green("\n✅ Process exited cleanly."));
410
421
  this.logger.info(EVENT_TYPES.PROCESS_HEALTHY, "Process exited cleanly");
411
422
  return;
412
423
  }
413
424
 
425
+ // Killed by signal with no stderr — just restart, don't waste tokens healing
426
+ if (!this._stderrBuffer.trim() || this._stderrBuffer.trim().length < 10) {
427
+ console.log(chalk.yellow(`\n⚠️ Process killed (code: ${code}, signal: ${signal}) — no error to heal, restarting`));
428
+ this.logger.warn(EVENT_TYPES.PROCESS_CRASH, `Killed with no stderr (code: ${code}, signal: ${signal})`, { exitCode: code, signal });
429
+ this._spawn();
430
+ return;
431
+ }
432
+
414
433
  const uptime = Date.now() - this._lastStartTime;
415
434
  console.log(chalk.red(`\n💥 Process crashed with exit code ${code} (uptime: ${Math.round(uptime / 1000)}s)`));
416
435
  this.logger.error(EVENT_TYPES.PROCESS_CRASH, `Crashed with code ${code}`, {
@@ -466,6 +485,28 @@ class WolverineRunner {
466
485
  this._healInProgress = true;
467
486
  this._healStatus = { active: true, error: this._stderrBuffer.slice(0, 200), phase: "diagnosing", startedAt: Date.now() };
468
487
 
488
+ // Loop guard: check if we're stuck repeating failed heals
489
+ const errorSig = RateLimiter.signature(this._stderrBuffer.slice(0, 200), "");
490
+ const loopCheck = this.loopGuard.check(errorSig);
491
+ if (!loopCheck.allowed) {
492
+ console.log(chalk.red(`\n 🔄 ${loopCheck.reason}`));
493
+ if (loopCheck.shouldReport) {
494
+ const report = await this.loopGuard.generateBugReport({
495
+ errorMessage: this._stderrBuffer.slice(0, 500),
496
+ filePath: null,
497
+ attempts: loopCheck.attempts,
498
+ brain: this.brain,
499
+ logger: this.logger,
500
+ });
501
+ await this.loopGuard.sendToBackend(report);
502
+ }
503
+ this._healInProgress = false;
504
+ this._healStatus = null;
505
+ // Just restart without healing — the bug report is filed
506
+ this._spawn();
507
+ return;
508
+ }
509
+
469
510
  try {
470
511
  const result = await heal({
471
512
  stderr: this._stderrBuffer,
@@ -482,9 +523,12 @@ class WolverineRunner {
482
523
  repairHistory: this.repairHistory,
483
524
  });
484
525
 
526
+ // Record attempt for loop guard
527
+ this.loopGuard.record(errorSig, result.healed, result.agentStats?.totalTokens || 0);
528
+
485
529
  if (result.healed) {
486
530
  this._lastBackupId = result.backupId;
487
- this.retryCount = 0; // Fresh start after successful heal
531
+ this.retryCount = 0;
488
532
  const mode = result.mode === "agent" ? "multi-file agent" : result.mode || "fast path";
489
533
  console.log(chalk.green(`\n🐺 Wolverine healed the error via ${mode}! Restarting...\n`));
490
534
 
@@ -45,6 +45,13 @@ async function _healImpl({ stderr, cwd, sandbox, notifier, rateLimiter, backupMa
45
45
  const healStartTime = Date.now();
46
46
  const { redact, hasSecrets } = require("../security/secret-redactor");
47
47
 
48
+ // Guard: don't burn tokens on empty stderr (signal kills, clean shutdowns, etc.)
49
+ if (!stderr || stderr.trim().length < 10) {
50
+ console.log(chalk.yellow("\n🐺 Empty stderr — nothing to heal (likely signal kill)"));
51
+ if (logger) logger.warn(EVENT_TYPES.HEAL_FAILED, "Empty stderr — skipping heal");
52
+ return { healed: false, explanation: "Empty stderr — nothing to diagnose" };
53
+ }
54
+
48
55
  // Redact secrets BEFORE any processing, logging, or AI calls
49
56
  const safeStderr = redact(stderr);
50
57
 
package/src/index.js CHANGED
@@ -38,6 +38,7 @@ const { diagnose: diagnoseDeps, healthReport: depsHealthReport, getMigration } =
38
38
  const { checkForUpdate, upgrade: upgradeWolverine, getCurrentVersion } = require("./platform/auto-update");
39
39
  const { safeUpdate, createSafeBackup, listSafeBackups, restoreFromSafeBackup } = require("./skills/update");
40
40
  const { backup, rollback, rollbackLatest, undoRollback, listBackups } = require("./skills/backup");
41
+ const { LoopGuard, ensureSingleProcess } = require("./skills/loop-guard");
41
42
 
42
43
  module.exports = {
43
44
  // Core
@@ -114,4 +115,6 @@ module.exports = {
114
115
  rollbackLatest,
115
116
  undoRollback,
116
117
  listBackups,
118
+ LoopGuard,
119
+ ensureSingleProcess,
117
120
  };
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Loop Guard — detects infinite heal loops and generates bug reports.
3
+ *
4
+ * Problem: wolverine can get stuck repeating the same fix attempt,
5
+ * burning tokens with nothing changing. This happens when:
6
+ * - The error is beyond the agent's capability to fix
7
+ * - The fix works but something else breaks, causing a new error
8
+ * - External deps are missing (not a code issue)
9
+ * - The verifier keeps rejecting a correct fix
10
+ *
11
+ * Solution: track heal attempts with signatures. If the same error
12
+ * (or same file) triggers N heals in T minutes with no success,
13
+ * STOP healing and generate a bug report instead.
14
+ *
15
+ * Bug reports are sent to the platform backend for human review.
16
+ */
17
+
18
+ const fs = require("fs");
19
+ const path = require("path");
20
+ const chalk = require("chalk");
21
+
22
+ const LOOP_FILE = ".wolverine/loop-guard.json";
23
+
24
+ class LoopGuard {
25
+ constructor(cwd, options = {}) {
26
+ this.cwd = cwd;
27
+ this.maxAttempts = options.maxAttempts || 3; // max heals on same error before giving up
28
+ this.windowMs = options.windowMs || 600000; // 10 minute window
29
+ this.cooldownMs = options.cooldownMs || 1800000; // 30 min cooldown after bug report
30
+ this._attempts = []; // { signature, timestamp, success, tokens }
31
+ this._bugReports = []; // { signature, report, timestamp }
32
+ this._load();
33
+ }
34
+
35
+ /**
36
+ * Check if healing should proceed for this error.
37
+ * Returns { allowed: boolean, reason?: string }
38
+ */
39
+ check(errorSignature) {
40
+ const now = Date.now();
41
+
42
+ // Clean old attempts outside window
43
+ this._attempts = this._attempts.filter(a => now - a.timestamp < this.windowMs);
44
+
45
+ // Count recent attempts on this signature
46
+ const recentAttempts = this._attempts.filter(a => a.signature === errorSignature);
47
+ const recentFailures = recentAttempts.filter(a => !a.success);
48
+
49
+ // Check cooldown from previous bug report on this signature
50
+ const lastReport = this._bugReports.find(r => r.signature === errorSignature);
51
+ if (lastReport && now - lastReport.timestamp < this.cooldownMs) {
52
+ const remaining = Math.round((this.cooldownMs - (now - lastReport.timestamp)) / 60000);
53
+ return { allowed: false, reason: `Bug report filed ${remaining}min ago — cooldown active` };
54
+ }
55
+
56
+ if (recentFailures.length >= this.maxAttempts) {
57
+ return {
58
+ allowed: false,
59
+ reason: `Loop detected: ${recentFailures.length} failed heals on same error in ${Math.round(this.windowMs / 60000)}min`,
60
+ shouldReport: true,
61
+ attempts: recentAttempts,
62
+ };
63
+ }
64
+
65
+ return { allowed: true };
66
+ }
67
+
68
+ /**
69
+ * Record a heal attempt.
70
+ */
71
+ record(errorSignature, success, tokens = 0) {
72
+ this._attempts.push({
73
+ signature: errorSignature,
74
+ timestamp: Date.now(),
75
+ success,
76
+ tokens,
77
+ });
78
+ this._save();
79
+ }
80
+
81
+ /**
82
+ * Generate a bug report using AI reasoning.
83
+ * Compacts all context into a structured report for human review.
84
+ */
85
+ async generateBugReport({ errorMessage, filePath, attempts, brain, logger }) {
86
+ const { redact } = require("../security/secret-redactor");
87
+ const safeError = redact(errorMessage || "");
88
+
89
+ // Compact attempt history
90
+ const attemptSummary = (attempts || this._attempts.slice(-5)).map(a =>
91
+ `[${new Date(a.timestamp).toISOString().slice(11, 19)}] ${a.success ? "OK" : "FAIL"} ${a.tokens} tokens`
92
+ ).join("\n");
93
+
94
+ // Get brain context about this error
95
+ let brainContext = "";
96
+ if (brain && brain._initialized) {
97
+ try {
98
+ const results = brain.store.keywordSearch(safeError, { topK: 3 });
99
+ brainContext = results.map(r => r.text.slice(0, 100)).join("\n");
100
+ } catch {}
101
+ }
102
+
103
+ // Build report without AI (save tokens — the whole point is to stop burning them)
104
+ const report = {
105
+ type: "bug_report",
106
+ timestamp: Date.now(),
107
+ iso: new Date().toISOString(),
108
+ error: safeError.slice(0, 500),
109
+ file: filePath || "unknown",
110
+ attempts: this._attempts.filter(a => a.signature === (filePath + "::" + safeError.slice(0, 50))).length,
111
+ totalTokensBurned: this._attempts.reduce((s, a) => s + (a.tokens || 0), 0),
112
+ history: attemptSummary,
113
+ brainContext: brainContext.slice(0, 300),
114
+ recommendation: "This error exceeded automatic healing capacity. Manual investigation required.",
115
+ severity: "high",
116
+ };
117
+
118
+ // Check for injection/secrets before sending
119
+ const { hasSecrets } = require("../security/secret-redactor");
120
+ const reportStr = JSON.stringify(report);
121
+ if (hasSecrets(reportStr)) {
122
+ report.error = "[REDACTED — contained secrets]";
123
+ report.brainContext = "[REDACTED]";
124
+ }
125
+
126
+ // Record the bug report
127
+ const sig = (filePath || "") + "::" + safeError.slice(0, 50);
128
+ this._bugReports.push({ signature: sig, report, timestamp: Date.now() });
129
+ this._save();
130
+
131
+ console.log(chalk.red(`\n 🐛 Bug Report Generated`));
132
+ console.log(chalk.red(` Error: ${safeError.slice(0, 80)}`));
133
+ console.log(chalk.red(` Attempts: ${report.attempts}, Tokens burned: ${report.totalTokensBurned}`));
134
+ console.log(chalk.red(` Filed for human review\n`));
135
+
136
+ if (logger) {
137
+ logger.critical("loop_guard.bug_report", `Bug report: ${safeError.slice(0, 100)}`, report);
138
+ }
139
+
140
+ return report;
141
+ }
142
+
143
+ /**
144
+ * Send bug report to platform backend.
145
+ */
146
+ async sendToBackend(report) {
147
+ try {
148
+ const https = require("https");
149
+ const http = require("http");
150
+ const { URL } = require("url");
151
+ const { loadConfig } = require("../core/config");
152
+ const config = loadConfig();
153
+ const platformUrl = config.telemetry?.platformUrl || "https://api.wolverinenode.xyz";
154
+
155
+ const keyPath = path.join(this.cwd, ".wolverine", "platform-key");
156
+ let key = "";
157
+ try { key = fs.readFileSync(keyPath, "utf-8").trim(); } catch {}
158
+ if (!key) return;
159
+
160
+ const url = new URL(`${platformUrl}/api/v1/heartbeat`);
161
+ const body = JSON.stringify({
162
+ instanceId: require("../platform/telemetry").INSTANCE_ID,
163
+ timestamp: Date.now(),
164
+ alerts: [report],
165
+ });
166
+
167
+ const mod = url.protocol === "https:" ? https : http;
168
+ await new Promise((resolve) => {
169
+ const req = mod.request({
170
+ hostname: url.hostname, port: url.port, path: url.pathname,
171
+ method: "POST",
172
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${key}` },
173
+ timeout: 5000,
174
+ }, () => resolve());
175
+ req.on("error", () => resolve());
176
+ req.write(body);
177
+ req.end();
178
+ });
179
+ } catch {}
180
+ }
181
+
182
+ getStats() {
183
+ return {
184
+ recentAttempts: this._attempts.length,
185
+ bugReports: this._bugReports.length,
186
+ totalTokensBurned: this._attempts.reduce((s, a) => s + (a.tokens || 0), 0),
187
+ };
188
+ }
189
+
190
+ _load() {
191
+ try {
192
+ const fp = path.join(this.cwd, LOOP_FILE);
193
+ if (fs.existsSync(fp)) {
194
+ const data = JSON.parse(fs.readFileSync(fp, "utf-8"));
195
+ this._attempts = data.attempts || [];
196
+ this._bugReports = data.bugReports || [];
197
+ }
198
+ } catch {}
199
+ }
200
+
201
+ _save() {
202
+ try {
203
+ const fp = path.join(this.cwd, LOOP_FILE);
204
+ fs.mkdirSync(path.dirname(fp), { recursive: true });
205
+ fs.writeFileSync(fp, JSON.stringify({ attempts: this._attempts.slice(-50), bugReports: this._bugReports.slice(-20) }, null, 2), "utf-8");
206
+ } catch {}
207
+ }
208
+ }
209
+
210
+ // ── Process Dedup ──
211
+
212
+ /**
213
+ * Ensure only one wolverine process runs at a time.
214
+ * Kills any existing wolverine process before starting a new one.
215
+ */
216
+ function ensureSingleProcess(cwd) {
217
+ const { execSync } = require("child_process");
218
+ const pidFile = path.join(cwd, ".wolverine", "wolverine.pid");
219
+
220
+ // Kill old process if PID file exists
221
+ try {
222
+ if (fs.existsSync(pidFile)) {
223
+ const oldPid = parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10);
224
+ if (oldPid && oldPid !== process.pid) {
225
+ try {
226
+ process.kill(oldPid, 0); // check if alive
227
+ console.log(chalk.yellow(` 🔒 Killing old wolverine process (PID ${oldPid})`));
228
+ if (process.platform === "win32") {
229
+ execSync(`taskkill /PID ${oldPid} /T /F`, { stdio: "ignore", timeout: 5000 });
230
+ } else {
231
+ process.kill(oldPid, "SIGTERM");
232
+ setTimeout(() => { try { process.kill(oldPid, "SIGKILL"); } catch {} }, 3000);
233
+ }
234
+ } catch {} // process already dead
235
+ }
236
+ }
237
+ } catch {}
238
+
239
+ // Write our PID
240
+ try {
241
+ fs.mkdirSync(path.dirname(pidFile), { recursive: true });
242
+ fs.writeFileSync(pidFile, String(process.pid), "utf-8");
243
+ } catch {}
244
+
245
+ // Clean up on exit
246
+ process.on("exit", () => { try { fs.unlinkSync(pidFile); } catch {} });
247
+ }
248
+
249
+ // ── Skill Metadata ──
250
+
251
+ const SKILL_NAME = "loop-guard";
252
+ const SKILL_DESCRIPTION = "Detects infinite heal loops and generates bug reports. Prevents token waste by stopping repeated failed heals on the same error. Ensures only one wolverine process runs at a time.";
253
+ const SKILL_KEYWORDS = ["loop", "infinite", "stuck", "repeat", "guard", "bug", "report", "dedup", "single", "process", "pid"];
254
+ const SKILL_USAGE = `// Loop guard is automatic — integrated into the heal pipeline.
255
+ // Bug reports filed when 3+ heals fail on same error in 10 minutes.
256
+ // Process dedup runs on startup.
257
+
258
+ // Manual check:
259
+ const { LoopGuard } = require("wolverine-ai");
260
+ const guard = new LoopGuard(process.cwd());
261
+ const check = guard.check("server/api.js::TypeError");
262
+ // { allowed: false, reason: "Loop detected: 3 failed heals..." }`;
263
+
264
+ module.exports = {
265
+ SKILL_NAME, SKILL_DESCRIPTION, SKILL_KEYWORDS, SKILL_USAGE,
266
+ LoopGuard, ensureSingleProcess,
267
+ };