wolverine-ai 4.0.5 → 4.1.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.0
|
|
3
|
+
"version": "4.1.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": {
|
package/src/core/runner.js
CHANGED
|
@@ -459,6 +459,13 @@ class WolverineRunner {
|
|
|
459
459
|
return;
|
|
460
460
|
}
|
|
461
461
|
|
|
462
|
+
// #28: SIGKILL = likely OOM — synthesize useful stderr for the heal pipeline
|
|
463
|
+
if (signal === "SIGKILL" && (!this._stderrBuffer.trim() || this._stderrBuffer.trim().length < 10)) {
|
|
464
|
+
this._stderrBuffer = `Process killed by SIGKILL (possible OOM). Memory limit may have been exceeded. Check memory usage patterns and reduce memory consumption.\nExit code: ${code}, Signal: ${signal}`;
|
|
465
|
+
console.log(chalk.red(`\n💀 Process killed by SIGKILL (possible OOM)`));
|
|
466
|
+
this.logger.error(EVENT_TYPES.PROCESS_CRASH, "SIGKILL — possible OOM", { exitCode: code, signal });
|
|
467
|
+
}
|
|
468
|
+
|
|
462
469
|
// Killed by signal with no stderr — just restart, don't waste tokens healing
|
|
463
470
|
if (!this._stderrBuffer.trim() || this._stderrBuffer.trim().length < 10) {
|
|
464
471
|
console.log(chalk.yellow(`\n⚠️ Process killed (code: ${code}, signal: ${signal}) — no error to heal, restarting`));
|
|
@@ -483,13 +490,28 @@ class WolverineRunner {
|
|
|
483
490
|
}
|
|
484
491
|
|
|
485
492
|
this.retryCount++;
|
|
486
|
-
|
|
493
|
+
// #3: Guard against unhandled rejections — don't let heal errors crash the parent
|
|
494
|
+
try {
|
|
495
|
+
await this._healAndRestart();
|
|
496
|
+
} catch (healErr) {
|
|
497
|
+
console.log(chalk.red(` ⚠️ Heal error (recovering): ${healErr.message}`));
|
|
498
|
+
this._healInProgress = false;
|
|
499
|
+
this._healStatus = null;
|
|
500
|
+
if (this.running) this._spawn(); // restart without healing
|
|
501
|
+
}
|
|
487
502
|
});
|
|
488
503
|
|
|
489
504
|
this.child.on("error", (err) => {
|
|
490
505
|
console.log(chalk.red(`Failed to start process: ${err.message}`));
|
|
491
506
|
this.logger.error(EVENT_TYPES.PROCESS_CRASH, `Failed to start: ${err.message}`);
|
|
492
|
-
|
|
507
|
+
// #10: Retry spawn after delay instead of permanently dying
|
|
508
|
+
if (this.running && this.retryCount < this.maxRetries) {
|
|
509
|
+
this.retryCount++;
|
|
510
|
+
console.log(chalk.yellow(` Retrying spawn in 5s (attempt ${this.retryCount}/${this.maxRetries})...`));
|
|
511
|
+
setTimeout(() => { if (this.running) this._spawn(); }, 5000);
|
|
512
|
+
} else {
|
|
513
|
+
this.running = false;
|
|
514
|
+
}
|
|
493
515
|
});
|
|
494
516
|
|
|
495
517
|
// IPC channel: child reports caught 500 errors (Fastify/Express)
|
|
@@ -626,9 +648,14 @@ class WolverineRunner {
|
|
|
626
648
|
}
|
|
627
649
|
}
|
|
628
650
|
} catch (err) {
|
|
629
|
-
|
|
651
|
+
// #4: Don't permanently die on transient errors — restart without healing
|
|
652
|
+
console.log(chalk.red(`\n🐺 Wolverine heal error (recovering): ${err.message}`));
|
|
630
653
|
this._healInProgress = false;
|
|
631
|
-
this.
|
|
654
|
+
this._healStatus = null;
|
|
655
|
+
if (this.running) {
|
|
656
|
+
console.log(chalk.yellow(" Restarting without healing..."));
|
|
657
|
+
this._spawn();
|
|
658
|
+
}
|
|
632
659
|
}
|
|
633
660
|
}
|
|
634
661
|
|
package/src/core/wolverine.js
CHANGED
|
@@ -35,6 +35,17 @@ async function heal(opts) {
|
|
|
35
35
|
if (err.message === "timeout") {
|
|
36
36
|
console.log(chalk.red(`\n🐺 Heal timed out after ${HEAL_TIMEOUT_MS / 1000}s`));
|
|
37
37
|
if (opts.logger) opts.logger.error(EVENT_TYPES.HEAL_FAILED, `Heal timed out after ${HEAL_TIMEOUT_MS / 1000}s`);
|
|
38
|
+
// #11: Rollback on timeout — the background _healImpl may have partially applied patches
|
|
39
|
+
if (opts.backupManager) {
|
|
40
|
+
try {
|
|
41
|
+
const all = opts.backupManager.getAll();
|
|
42
|
+
const latest = all.find(b => b.status === "unstable");
|
|
43
|
+
if (latest) {
|
|
44
|
+
opts.backupManager.rollbackTo(latest.id);
|
|
45
|
+
console.log(chalk.yellow(` ↩️ Rolled back to ${latest.id} (timeout cleanup)`));
|
|
46
|
+
}
|
|
47
|
+
} catch {}
|
|
48
|
+
}
|
|
38
49
|
return { healed: false, explanation: `Heal timed out after ${HEAL_TIMEOUT_MS / 1000}s` };
|
|
39
50
|
}
|
|
40
51
|
throw err;
|
|
@@ -312,7 +323,7 @@ async function _healImpl({ stderr, cwd, sandbox, notifier, rateLimiter, backupMa
|
|
|
312
323
|
errorMessage: parsed.errorMessage, stackTrace: parsed.stackTrace,
|
|
313
324
|
extraContext: envContext,
|
|
314
325
|
});
|
|
315
|
-
|
|
326
|
+
// #15: Don't record rate limit until AFTER verification — failed attempts shouldn't exhaust the limit
|
|
316
327
|
|
|
317
328
|
// Execute shell commands first (npm install, mkdir, etc.)
|
|
318
329
|
if (repair.commands && Array.isArray(repair.commands)) {
|
|
@@ -351,6 +362,7 @@ async function _healImpl({ stderr, cwd, sandbox, notifier, rateLimiter, backupMa
|
|
|
351
362
|
const verification = await verifyFix(parsed.filePath, cwd, errorSignature, routeContext);
|
|
352
363
|
if (verification.verified) {
|
|
353
364
|
backupManager.markVerified(bid);
|
|
365
|
+
rateLimiter.record(errorSignature);
|
|
354
366
|
rateLimiter.clearSignature(errorSignature);
|
|
355
367
|
// Track tool operations: file read + patch + verify + any commands
|
|
356
368
|
// These are the same operations an agent would do with read_file/write_file/bash_exec
|
|
@@ -360,10 +372,11 @@ async function _healImpl({ stderr, cwd, sandbox, notifier, rateLimiter, backupMa
|
|
|
360
372
|
return { healed: true, explanation: repair.explanation, backupId: bid, mode: "fast" };
|
|
361
373
|
}
|
|
362
374
|
|
|
363
|
-
|
|
375
|
+
// #13: Safe rollback — wrap in try/catch to prevent rollback-of-rollback loop
|
|
376
|
+
try { backupManager.rollbackTo(bid); } catch (rbErr) { console.log(chalk.red(` ⚠️ Rollback failed: ${rbErr.message}`)); }
|
|
364
377
|
return { healed: false, explanation: `Fast path: ${verification.status}` };
|
|
365
378
|
} catch (err) {
|
|
366
|
-
backupManager.rollbackTo(bid);
|
|
379
|
+
try { backupManager.rollbackTo(bid); } catch (rbErr) { console.log(chalk.red(` ⚠️ Rollback failed: ${rbErr.message}`)); }
|
|
367
380
|
return { healed: false, explanation: `Fast path error: ${err.message}` };
|
|
368
381
|
}
|
|
369
382
|
} else if (iteration <= 2) {
|
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
},
|
|
7
7
|
|
|
8
8
|
"models": {
|
|
9
|
-
"reasoning": "
|
|
10
|
-
"coding": "
|
|
11
|
-
"chat": "gpt-
|
|
9
|
+
"reasoning": "claude-sonnet-4-6",
|
|
10
|
+
"coding": "claude-sonnet-4-6",
|
|
11
|
+
"chat": "gpt-5.4-mini",
|
|
12
12
|
"tool": "gpt-4o-mini",
|
|
13
13
|
"classifier": "gpt-4o-mini",
|
|
14
|
-
"audit": "
|
|
15
|
-
"compacting": "
|
|
16
|
-
"research": "
|
|
14
|
+
"audit": "wolverine-test-1",
|
|
15
|
+
"compacting": "wolverine-test-1",
|
|
16
|
+
"research": "claude-sonnet-4-6"
|
|
17
17
|
},
|
|
18
18
|
|
|
19
19
|
"embedding": "wolverine-embedding-1",
|