wolverine-ai 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/package.json +1 -1
- package/server/config/settings.json +6 -0
- package/server/index.js +30 -2
- package/src/core/ai-client.js +2 -2
- package/src/core/error-hook.js +49 -56
- package/src/core/runner.js +25 -1
- package/src/core/wolverine.js +37 -11
- package/src/dashboard/server.js +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wolverine-ai",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
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/server/index.js
CHANGED
|
@@ -19,10 +19,38 @@ fastify.setNotFoundHandler((req, reply) => {
|
|
|
19
19
|
reply.code(404).send({ error: "Not found", path: req.url });
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
-
// Error handler
|
|
22
|
+
// Error handler — reports to Wolverine parent via IPC for auto-healing
|
|
23
23
|
fastify.setErrorHandler((err, req, reply) => {
|
|
24
24
|
console.error(`[ERROR] ${err.message}`);
|
|
25
|
-
reply.code(500).send({ error:
|
|
25
|
+
reply.code(500).send({ error: err.message });
|
|
26
|
+
|
|
27
|
+
// Report to Wolverine via IPC (if running under wolverine)
|
|
28
|
+
if (typeof process.send === "function") {
|
|
29
|
+
try {
|
|
30
|
+
// Extract file/line from stack trace
|
|
31
|
+
let file = null, line = null;
|
|
32
|
+
if (err.stack) {
|
|
33
|
+
const frames = err.stack.split("\n");
|
|
34
|
+
for (const frame of frames) {
|
|
35
|
+
const m = frame.match(/\(([^)]+):(\d+):(\d+)\)/) || frame.match(/at\s+([^\s(]+):(\d+):(\d+)/);
|
|
36
|
+
if (m && !m[1].includes("node_modules") && !m[1].includes("node:")) {
|
|
37
|
+
file = m[1]; line = parseInt(m[2], 10); break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
process.send({
|
|
42
|
+
type: "route_error",
|
|
43
|
+
path: req.url,
|
|
44
|
+
method: req.method,
|
|
45
|
+
statusCode: 500,
|
|
46
|
+
message: err.message,
|
|
47
|
+
stack: err.stack,
|
|
48
|
+
file,
|
|
49
|
+
line,
|
|
50
|
+
timestamp: Date.now(),
|
|
51
|
+
});
|
|
52
|
+
} catch (_) { /* IPC send failed — non-fatal */ }
|
|
53
|
+
}
|
|
26
54
|
});
|
|
27
55
|
|
|
28
56
|
fastify.listen({ port: PORT, host: "0.0.0.0" }, (err) => {
|
package/src/core/ai-client.js
CHANGED
|
@@ -335,7 +335,7 @@ async function _chatCallWithHistory(openai, { model, messages, tools, maxTokens
|
|
|
335
335
|
* Send an error context to OpenAI and get a repair patch back.
|
|
336
336
|
* Uses CODING_MODEL — routes to correct API automatically.
|
|
337
337
|
*/
|
|
338
|
-
async function requestRepair({ filePath, sourceCode, errorMessage, stackTrace }) {
|
|
338
|
+
async function requestRepair({ filePath, sourceCode, backupSourceCode, errorMessage, stackTrace }) {
|
|
339
339
|
const model = getModel("coding");
|
|
340
340
|
|
|
341
341
|
const systemPrompt = "You are a Node.js debugging expert. Respond with ONLY valid JSON, no markdown fences.";
|
|
@@ -357,7 +357,7 @@ ${errorMessage}
|
|
|
357
357
|
${stackTrace}
|
|
358
358
|
\`\`\`
|
|
359
359
|
|
|
360
|
-
## Instructions
|
|
360
|
+
${backupSourceCode ? `## Last Known Working Version\n\`\`\`javascript\n${backupSourceCode}\n\`\`\`\n\nCompare the current broken code with this working version. If the broken code added something that doesn't work, REVERT that addition rather than patching around it.\n` : ""}## Instructions
|
|
361
361
|
1. Identify the root cause of the error.
|
|
362
362
|
2. Not all errors are code bugs. Choose the correct fix type:
|
|
363
363
|
- "Cannot find module 'X'" (not starting with ./ or ../) = missing npm package → use "commands" to npm install
|
package/src/core/error-hook.js
CHANGED
|
@@ -1,55 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Error Hook — preloaded into the child server process via --require.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Safety net: patches Fastify and Express to report caught errors
|
|
5
|
+
* via IPC to the Wolverine parent process. Works even if the user's
|
|
6
|
+
* server code doesn't call process.send() in its error handler.
|
|
7
7
|
*
|
|
8
8
|
* How it works:
|
|
9
|
-
* 1. Runner spawns
|
|
10
|
-
* 2. This file
|
|
11
|
-
* 3.
|
|
12
|
-
*
|
|
9
|
+
* 1. Runner spawns: node --require error-hook.js server/index.js
|
|
10
|
+
* 2. This file intercepts require("fastify") and require("express")
|
|
11
|
+
* 3. Wraps the constructor to add an onError hook (Fastify) or
|
|
12
|
+
* error middleware (Express) that sends IPC messages
|
|
13
|
+
* 4. Parent's ErrorMonitor receives messages and triggers heal
|
|
13
14
|
*
|
|
14
|
-
*
|
|
15
|
+
* If the server already reports errors via process.send(), the hook
|
|
16
|
+
* deduplicates by checking a timestamp flag on the error object.
|
|
15
17
|
*/
|
|
16
18
|
|
|
17
19
|
const Module = require("module");
|
|
18
20
|
const originalLoad = Module._load;
|
|
19
21
|
|
|
20
|
-
let
|
|
22
|
+
let _fastifyHooked = false;
|
|
23
|
+
let _expressHooked = false;
|
|
21
24
|
|
|
22
25
|
Module._load = function (request, parent, isMain) {
|
|
23
26
|
const result = originalLoad.apply(this, arguments);
|
|
24
27
|
|
|
25
28
|
// Hook Fastify
|
|
26
|
-
if (request === "fastify" && typeof result === "function" && !
|
|
29
|
+
if (request === "fastify" && typeof result === "function" && !_fastifyHooked) {
|
|
30
|
+
_fastifyHooked = true;
|
|
27
31
|
const originalFastify = result;
|
|
28
32
|
const wrapped = function (...args) {
|
|
29
33
|
const instance = originalFastify(...args);
|
|
30
34
|
_hookFastify(instance);
|
|
31
35
|
return instance;
|
|
32
36
|
};
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
wrapped[key] = originalFastify[key];
|
|
36
|
-
});
|
|
37
|
-
_hooked = true;
|
|
37
|
+
Object.keys(originalFastify).forEach((key) => { wrapped[key] = originalFastify[key]; });
|
|
38
|
+
wrapped.default = wrapped; // ESM compat
|
|
38
39
|
return wrapped;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
// Hook Express
|
|
42
|
-
if (request === "express" && typeof result === "function" && !
|
|
43
|
+
if (request === "express" && typeof result === "function" && !_expressHooked) {
|
|
44
|
+
_expressHooked = true;
|
|
43
45
|
const originalExpress = result;
|
|
44
46
|
const wrapped = function (...args) {
|
|
45
47
|
const app = originalExpress(...args);
|
|
46
48
|
_hookExpress(app);
|
|
47
49
|
return app;
|
|
48
50
|
};
|
|
49
|
-
Object.keys(originalExpress).forEach((key) => {
|
|
50
|
-
wrapped[key] = originalExpress[key];
|
|
51
|
-
});
|
|
52
|
-
_hooked = true;
|
|
51
|
+
Object.keys(originalExpress).forEach((key) => { wrapped[key] = originalExpress[key]; });
|
|
53
52
|
return wrapped;
|
|
54
53
|
}
|
|
55
54
|
|
|
@@ -57,55 +56,51 @@ Module._load = function (request, parent, isMain) {
|
|
|
57
56
|
};
|
|
58
57
|
|
|
59
58
|
function _hookFastify(fastify) {
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
// Wrap setErrorHandler so our IPC reporting runs BEFORE the user's handler
|
|
60
|
+
const origSetError = fastify.setErrorHandler;
|
|
61
|
+
fastify.setErrorHandler = function (userHandler) {
|
|
62
|
+
return origSetError.call(this, function (error, request, reply) {
|
|
64
63
|
_reportError(request.url, request.method, error);
|
|
65
|
-
|
|
64
|
+
return userHandler.call(this, error, request, reply);
|
|
66
65
|
});
|
|
67
|
-
|
|
68
|
-
});
|
|
66
|
+
};
|
|
69
67
|
|
|
70
|
-
// Also
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return originalSetError(function (error, request, reply) {
|
|
68
|
+
// Also add onError hook as a fallback (fires even if no custom error handler)
|
|
69
|
+
try {
|
|
70
|
+
fastify.addHook("onError", function (request, reply, error, done) {
|
|
74
71
|
_reportError(request.url, request.method, error);
|
|
75
|
-
|
|
72
|
+
done();
|
|
76
73
|
});
|
|
77
|
-
}
|
|
74
|
+
} catch { /* addHook may fail if server is already started */ }
|
|
78
75
|
}
|
|
79
76
|
|
|
80
77
|
function _hookExpress(app) {
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
const originalListen = app.listen.bind(app);
|
|
78
|
+
// Wrap app.listen to inject error middleware AFTER all user middleware
|
|
79
|
+
const originalListen = app.listen;
|
|
84
80
|
app.listen = function (...args) {
|
|
85
|
-
|
|
86
|
-
app.use(function wolverineErrorHandler(err, req, res, next) {
|
|
81
|
+
app.use(function _wolverineErrorHook(err, req, res, next) {
|
|
87
82
|
_reportError(req.originalUrl || req.url, req.method, err);
|
|
88
83
|
next(err);
|
|
89
84
|
});
|
|
90
|
-
return originalListen(
|
|
85
|
+
return originalListen.apply(this, args);
|
|
91
86
|
};
|
|
92
87
|
}
|
|
93
88
|
|
|
89
|
+
// Dedup: skip if error was already reported in the same tick
|
|
90
|
+
const _reported = new WeakSet();
|
|
91
|
+
|
|
94
92
|
function _reportError(url, method, error) {
|
|
95
|
-
if (
|
|
93
|
+
if (typeof process.send !== "function") return;
|
|
94
|
+
if (!error || _reported.has(error)) return;
|
|
95
|
+
_reported.add(error);
|
|
96
96
|
|
|
97
97
|
try {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const match = sl.match(/\(([^)]+):(\d+):(\d+)\)/) || sl.match(/at\s+([^\s(]+):(\d+):(\d+)/);
|
|
105
|
-
if (match && !match[1].includes("node_modules") && !match[1].includes("node:")) {
|
|
106
|
-
file = match[1];
|
|
107
|
-
line = parseInt(match[2], 10);
|
|
108
|
-
break;
|
|
98
|
+
let file = null, line = null;
|
|
99
|
+
if (error.stack) {
|
|
100
|
+
for (const frame of error.stack.split("\n")) {
|
|
101
|
+
const m = frame.match(/\(([^)]+):(\d+):(\d+)\)/) || frame.match(/at\s+([^\s(]+):(\d+):(\d+)/);
|
|
102
|
+
if (m && !m[1].includes("node_modules") && !m[1].includes("node:")) {
|
|
103
|
+
file = m[1]; line = parseInt(m[2], 10); break;
|
|
109
104
|
}
|
|
110
105
|
}
|
|
111
106
|
}
|
|
@@ -115,13 +110,11 @@ function _reportError(url, method, error) {
|
|
|
115
110
|
path: url,
|
|
116
111
|
method: method || "GET",
|
|
117
112
|
statusCode: 500,
|
|
118
|
-
message: error
|
|
119
|
-
stack: error
|
|
113
|
+
message: error.message || "Unknown error",
|
|
114
|
+
stack: (error.stack || "").slice(0, 2000),
|
|
120
115
|
file,
|
|
121
116
|
line,
|
|
122
117
|
timestamp: Date.now(),
|
|
123
118
|
});
|
|
124
|
-
} catch {
|
|
125
|
-
// Silently fail — don't break the server for IPC issues
|
|
126
|
-
}
|
|
119
|
+
} catch { /* IPC send failed — non-fatal */ }
|
|
127
120
|
}
|
package/src/core/runner.js
CHANGED
|
@@ -139,6 +139,7 @@ class WolverineRunner {
|
|
|
139
139
|
this._stabilityTimer = null;
|
|
140
140
|
this._stderrBuffer = "";
|
|
141
141
|
this._healInProgress = false;
|
|
142
|
+
this._healStatus = null; // { active, file, error, phase, startedAt, iteration }
|
|
142
143
|
}
|
|
143
144
|
|
|
144
145
|
async start() {
|
|
@@ -410,6 +411,7 @@ class WolverineRunner {
|
|
|
410
411
|
async _healAndRestart() {
|
|
411
412
|
if (this._healInProgress) return;
|
|
412
413
|
this._healInProgress = true;
|
|
414
|
+
this._healStatus = { active: true, error: this._stderrBuffer.slice(0, 200), phase: "diagnosing", startedAt: Date.now() };
|
|
413
415
|
|
|
414
416
|
try {
|
|
415
417
|
const result = await heal({
|
|
@@ -429,14 +431,23 @@ class WolverineRunner {
|
|
|
429
431
|
|
|
430
432
|
if (result.healed) {
|
|
431
433
|
this._lastBackupId = result.backupId;
|
|
432
|
-
|
|
434
|
+
this.retryCount = 0; // Fresh start after successful heal
|
|
435
|
+
const mode = result.mode === "agent" ? "multi-file agent" : result.mode || "fast path";
|
|
433
436
|
console.log(chalk.green(`\n🐺 Wolverine healed the error via ${mode}! Restarting...\n`));
|
|
434
437
|
|
|
435
438
|
if (result.agentStats) {
|
|
436
439
|
console.log(chalk.gray(` Agent stats: ${result.agentStats.turns} turns, ${result.agentStats.tokens} tokens, ${result.agentStats.filesModified.length} files modified`));
|
|
437
440
|
}
|
|
438
441
|
|
|
442
|
+
// Broadcast heal success to dashboard SSE
|
|
443
|
+
if (this.logger) {
|
|
444
|
+
this.logger.info("heal.success", `Healed via ${mode}: ${result.explanation?.slice(0, 100)}`, {
|
|
445
|
+
mode, file: result.agentStats?.filesModified?.[0], duration: Date.now() - (this._healStatus?.startedAt || Date.now()),
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
439
449
|
this._healInProgress = false;
|
|
450
|
+
this._healStatus = null;
|
|
440
451
|
this._spawn();
|
|
441
452
|
} else {
|
|
442
453
|
console.log(chalk.red(`\n🐺 Wolverine could not heal: ${result.explanation}`));
|
|
@@ -479,6 +490,7 @@ class WolverineRunner {
|
|
|
479
490
|
this._healInProgress = true;
|
|
480
491
|
|
|
481
492
|
console.log(chalk.yellow(`\n🐺 Wolverine healing caught error on ${routePath}...`));
|
|
493
|
+
this._healStatus = { active: true, route: routePath, error: errorDetails?.message?.slice(0, 200), phase: "diagnosing", startedAt: Date.now() };
|
|
482
494
|
this.logger.info("heal.error_monitor", `Healing caught 500 on ${routePath}`, { route: routePath });
|
|
483
495
|
|
|
484
496
|
// Build a synthetic stderr from the error details
|
|
@@ -506,16 +518,28 @@ class WolverineRunner {
|
|
|
506
518
|
|
|
507
519
|
if (result.healed) {
|
|
508
520
|
console.log(chalk.green(`\n🐺 Wolverine healed ${routePath} via ${result.mode}! Restarting...\n`));
|
|
521
|
+
this.retryCount = 0; // Fresh start after successful heal
|
|
509
522
|
this.errorMonitor.clearRoute(routePath);
|
|
523
|
+
|
|
524
|
+
// Broadcast heal success to dashboard SSE
|
|
525
|
+
if (this.logger) {
|
|
526
|
+
this.logger.info("heal.success", `Healed ${routePath} via ${result.mode}: ${result.explanation?.slice(0, 100)}`, {
|
|
527
|
+
mode: result.mode, route: routePath, duration: Date.now() - (this._healStatus?.startedAt || Date.now()),
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
510
531
|
this._healInProgress = false;
|
|
532
|
+
this._healStatus = null;
|
|
511
533
|
this.restart();
|
|
512
534
|
} else {
|
|
513
535
|
console.log(chalk.red(`\n🐺 Could not heal ${routePath}: ${result.explanation}`));
|
|
514
536
|
this._healInProgress = false;
|
|
537
|
+
this._healStatus = null;
|
|
515
538
|
}
|
|
516
539
|
} catch (err) {
|
|
517
540
|
console.log(chalk.red(`\n🐺 Error during heal: ${err.message}`));
|
|
518
541
|
this._healInProgress = false;
|
|
542
|
+
this._healStatus = null;
|
|
519
543
|
}
|
|
520
544
|
}
|
|
521
545
|
|
package/src/core/wolverine.js
CHANGED
|
@@ -83,16 +83,22 @@ async function heal({ stderr, cwd, sandbox, notifier, rateLimiter, backupManager
|
|
|
83
83
|
let openaiClient = null;
|
|
84
84
|
try { openaiClient = getClient(); } catch { /* will fail later */ }
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
86
|
+
// Skip injection scan on empty/trivial stderr (prevents false positives on clean restarts)
|
|
87
|
+
const stderrContent = (parsed.errorMessage || "").trim() + (parsed.stackTrace || "").trim();
|
|
88
|
+
if (stderrContent.length < 20) {
|
|
89
|
+
console.log(chalk.gray(" 🛡️ Stderr too short for injection scan — skipping"));
|
|
90
|
+
} else {
|
|
91
|
+
const injectionResult = await detectInjection(parsed.errorMessage, parsed.stackTrace, { openaiClient });
|
|
92
|
+
|
|
93
|
+
if (logger) logger.info(EVENT_TYPES.HEAL_INJECTION_SCAN, `Injection scan: ${injectionResult.safe ? "clean" : "BLOCKED"}`, injectionResult);
|
|
94
|
+
|
|
95
|
+
if (!injectionResult.safe) {
|
|
96
|
+
console.log(chalk.red(" 🚨 BLOCKED: Potential prompt injection detected."));
|
|
97
|
+
if (logger) logger.critical(EVENT_TYPES.SECURITY_INJECTION_DETECTED, "Injection detected — repair blocked", injectionResult);
|
|
98
|
+
return { healed: false, explanation: "Prompt injection detected — repair blocked" };
|
|
99
|
+
}
|
|
100
|
+
console.log(chalk.green(" ✅ Clean — no injection detected."));
|
|
94
101
|
}
|
|
95
|
-
console.log(chalk.green(" ✅ Clean — no injection detected."));
|
|
96
102
|
|
|
97
103
|
// 4b. Check if this is a human-required issue (expired keys, billing, etc.)
|
|
98
104
|
if (notifier) {
|
|
@@ -128,6 +134,26 @@ async function heal({ stderr, cwd, sandbox, notifier, rateLimiter, backupManager
|
|
|
128
134
|
// 5. Read the source file (if available) + get brain context
|
|
129
135
|
const sourceCode = hasFile ? sandbox.readFile(parsed.filePath) : "";
|
|
130
136
|
|
|
137
|
+
// 5b. Get last known good version from backup (helps AI revert vs patch)
|
|
138
|
+
let backupSourceCode = "";
|
|
139
|
+
if (hasFile && backupManager) {
|
|
140
|
+
try {
|
|
141
|
+
const fs = require("fs");
|
|
142
|
+
const path = require("path");
|
|
143
|
+
const stableBackups = backupManager.getAll().filter(b => b.status === "stable" || b.status === "verified");
|
|
144
|
+
if (stableBackups.length > 0) {
|
|
145
|
+
const latest = stableBackups[stableBackups.length - 1];
|
|
146
|
+
const relPath = path.relative(cwd, parsed.filePath).replace(/[/\\]/g, "__");
|
|
147
|
+
const backupFile = path.join(cwd, ".wolverine", "backups", latest.id, relPath);
|
|
148
|
+
if (fs.existsSync(backupFile)) {
|
|
149
|
+
backupSourceCode = fs.readFileSync(backupFile, "utf-8");
|
|
150
|
+
if (backupSourceCode === sourceCode) backupSourceCode = ""; // Same — no useful diff
|
|
151
|
+
else console.log(chalk.gray(` 📋 Found last known good version (backup ${latest.id})`));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} catch { /* non-fatal */ }
|
|
155
|
+
}
|
|
156
|
+
|
|
131
157
|
let brainContext = "";
|
|
132
158
|
// Inject relevant skill context (claw-code: pre-enrich prompt with matched tools)
|
|
133
159
|
if (skills) {
|
|
@@ -183,7 +209,7 @@ async function heal({ stderr, cwd, sandbox, notifier, rateLimiter, backupManager
|
|
|
183
209
|
console.log(chalk.yellow(` 🧠 Fast path (${getModel("coding")})...`));
|
|
184
210
|
try {
|
|
185
211
|
const repair = await requestRepair({
|
|
186
|
-
filePath: parsed.filePath, sourceCode,
|
|
212
|
+
filePath: parsed.filePath, sourceCode, backupSourceCode,
|
|
187
213
|
errorMessage: parsed.errorMessage, stackTrace: parsed.stackTrace,
|
|
188
214
|
});
|
|
189
215
|
rateLimiter.record(errorSignature);
|
|
@@ -243,7 +269,7 @@ async function heal({ stderr, cwd, sandbox, notifier, rateLimiter, backupManager
|
|
|
243
269
|
const agentResult = await agent.run({
|
|
244
270
|
errorMessage: parsed.errorMessage, stackTrace: parsed.stackTrace,
|
|
245
271
|
primaryFile: parsed.filePath, sourceCode,
|
|
246
|
-
brainContext: fullContext,
|
|
272
|
+
brainContext: fullContext + (backupSourceCode ? `\n\nLAST KNOWN WORKING VERSION of ${parsed.filePath}:\n${backupSourceCode}\nIf the broken code added something that doesn't work, revert it rather than patching around it.` : ""),
|
|
247
273
|
});
|
|
248
274
|
rateLimiter.record(errorSignature, agentResult.totalTokens);
|
|
249
275
|
|
package/src/dashboard/server.js
CHANGED
|
@@ -871,6 +871,7 @@ ${context ? "\nBrain:\n" + context : ""}`,
|
|
|
871
871
|
backups: this.backupManager ? this.backupManager.getStats() : {},
|
|
872
872
|
health: this.healthMonitor ? this.healthMonitor.getStats() : {},
|
|
873
873
|
errorMonitor: this.errorMonitor ? this.errorMonitor.getStats() : {},
|
|
874
|
+
heal: this.runner ? this.runner._healStatus : null,
|
|
874
875
|
}));
|
|
875
876
|
}
|
|
876
877
|
|