wolverine-ai 2.8.3 → 2.9.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/README.md CHANGED
@@ -358,11 +358,12 @@ High-performance vector database that grows without slowing down:
358
358
 
359
359
  **Search performance** (scales gracefully):
360
360
 
361
- | Entries | Semantic Search | Keyword (BM25) |
362
- |---------|----------------|----------------|
363
- | 100 | 0.2ms | 0.005ms |
364
- | 1,000 | 0.4ms | 0.01ms |
365
- | 10,000 | 4.4ms | 0.1ms |
361
+ | Entries | Semantic Search | Keyword (BM25) | Clusters |
362
+ |---------|----------------|----------------|----------|
363
+ | 100 | 0.2ms | 0.005ms | 10 |
364
+ | 1,000 | 0.4ms | 0.01ms | 32 |
365
+ | 10,000 | 4.4ms | 0.1ms | 100 |
366
+ | 50,000 | 23.7ms | 0.5ms | 224 |
366
367
 
367
368
  **4 optimization techniques:**
368
369
  1. **Pre-normalized vectors** — cosine similarity = dot product (no sqrt per query)
@@ -429,6 +430,22 @@ Wolverine (single process manager)
429
430
 
430
431
  ---
431
432
 
433
+ ## Token Protection
434
+
435
+ Three layers prevent token waste:
436
+
437
+ | Layer | What it catches | Cost |
438
+ |-------|----------------|------|
439
+ | **Empty stderr guard** | Signal kills, clean shutdowns with no error | $0.00 |
440
+ | **Loop guard** | Same error failing 3+ times in 10min → files bug report, stops healing | $0.00 after detection |
441
+ | **Global rate limit** | Max 5 heals per 5 minutes regardless of error | Caps total spend |
442
+
443
+ **Process dedup:** PID file ensures only one wolverine instance runs. Kills old process on startup.
444
+
445
+ **Bug reports:** When loop guard triggers, generates a security-scanned report (no secrets/injection patterns) and sends to the platform backend for human review.
446
+
447
+ ---
448
+
432
449
  ## Cost Optimization
433
450
 
434
451
  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.3",
3
+ "version": "2.9.1",
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": {
@@ -54,7 +54,7 @@ const SEED_DOCS = [
54
54
  metadata: { topic: "perf-monitoring" },
55
55
  },
56
56
  {
57
- text: "Wolverine brain: high-performance vector database for long-term memory. 4 search optimizations: (1) Pre-normalized vectors — cosine similarity = dot product (no sqrt), 7x faster. (2) IVF index — vectors clustered into √N buckets via k-means++, search probes nearest 20% of clusters only. 10K entries: 4ms instead of 31ms. (3) BM25 keyword search — proper inverted index with TF-IDF scoring, O(query_tokens) not O(N). (4) Binary persistence — Float32Array buffers, 10x faster load than JSON. Grows gracefully: 100=0.2ms, 1K=0.4ms, 5K=2ms, 10K=4ms. Stores: function maps, errors, fixes, learnings, seed docs. Persisted to .wolverine/brain/.",
57
+ text: "Wolverine brain: high-performance vector database for long-term memory. 4 search optimizations: (1) Pre-normalized vectors — cosine similarity = dot product (no sqrt), 7x faster. (2) IVF index — k-means++ clustering into √N buckets (10 at 100 entries, 100 at 10K, 224 at 50K), search probes nearest 20% of clusters. (3) BM25 keyword search — inverted index with TF-IDF scoring, O(query_tokens) not O(N). (4) Binary persistence — Float32Array buffers, 10x faster load. Benchmarks: 100=0.2ms, 1K=0.4ms, 5K=2ms, 10K=4.4ms, 50K=23.7ms (was 160ms brute force). Stores: function maps, errors, fixes, learnings, seed docs. New seeds merged on framework update without erasing existing memories.",
58
58
  metadata: { topic: "brain" },
59
59
  },
60
60
  {
@@ -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
 
@@ -475,6 +485,28 @@ class WolverineRunner {
475
485
  this._healInProgress = true;
476
486
  this._healStatus = { active: true, error: this._stderrBuffer.slice(0, 200), phase: "diagnosing", startedAt: Date.now() };
477
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
+
478
510
  try {
479
511
  const result = await heal({
480
512
  stderr: this._stderrBuffer,
@@ -491,9 +523,12 @@ class WolverineRunner {
491
523
  repairHistory: this.repairHistory,
492
524
  });
493
525
 
526
+ // Record attempt for loop guard
527
+ this.loopGuard.record(errorSig, result.healed, result.agentStats?.totalTokens || 0);
528
+
494
529
  if (result.healed) {
495
530
  this._lastBackupId = result.backupId;
496
- this.retryCount = 0; // Fresh start after successful heal
531
+ this.retryCount = 0;
497
532
  const mode = result.mode === "agent" ? "multi-file agent" : result.mode || "fast path";
498
533
  console.log(chalk.green(`\n🐺 Wolverine healed the error via ${mode}! Restarting...\n`));
499
534
 
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
+ };