wolverine-ai 3.5.0 → 3.6.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.
@@ -590,11 +590,37 @@ class WolverineRunner {
590
590
  this._healStatus = { active: true, route: routePath, error: errorDetails?.message?.slice(0, 200), phase: "diagnosing", startedAt: Date.now() };
591
591
  this.logger.info("heal.error_monitor", `Healing caught 500 on ${routePath}`, { route: routePath });
592
592
 
593
- // Build a synthetic stderr from the error details
593
+ // Build synthetic stderr that matches the error parser's expected format
594
+ // If IPC didn't include a file, try to resolve from the route path or stack
595
+ let file = errorDetails.file;
596
+ let line = errorDetails.line || 1;
597
+ if (!file && errorDetails.stack) {
598
+ // Try to find user-land file in stack (not node_modules, not node:)
599
+ const frames = (errorDetails.stack || "").split("\n");
600
+ for (const frame of frames) {
601
+ const m = frame.match(/\(([^)]+):(\d+):(\d+)\)/) || frame.match(/at\s+([^\s(]+):(\d+):(\d+)/);
602
+ if (m && !m[1].includes("node_modules") && !m[1].includes("node:")) {
603
+ file = m[1]; line = parseInt(m[2], 10); break;
604
+ }
605
+ }
606
+ }
607
+ if (!file && routePath) {
608
+ // Last resort: map route path to likely file (e.g., /breakable → server/routes/breakable.js)
609
+ const routeName = routePath.split("/").filter(Boolean).pop();
610
+ if (routeName) {
611
+ const path = require("path");
612
+ const guess = path.join(this.cwd, "server", "routes", routeName + ".js");
613
+ if (require("fs").existsSync(guess)) { file = guess; line = 1; }
614
+ }
615
+ }
616
+
617
+ const msg = errorDetails.message || "Unknown error";
618
+ const hasErrorPrefix = /^\w*Error:/.test(msg);
594
619
  const stderr = [
595
- errorDetails.message || "Unknown error",
620
+ file ? `${file}:${line}` : "",
621
+ hasErrorPrefix ? msg : `Error: ${msg}`,
596
622
  errorDetails.stack || "",
597
- errorDetails.file ? ` at ${errorDetails.file}:${errorDetails.line || 0}` : "",
623
+ file ? ` at ${file}:${line}:1` : "",
598
624
  ].filter(Boolean).join("\n");
599
625
 
600
626
  try {
@@ -336,7 +336,7 @@ class DashboardServer {
336
336
  systemPrompt: "Route a command. Respond with two words: ROUTE SIZE.\nROUTE: SIMPLE (general knowledge/explanation, no live data needed), TOOLS (needs live server data, file contents, or endpoint calls), AGENT (create/modify/fix code).\nSIZE: SMALL, MEDIUM, LARGE.\nExamples: 'what is wolverine' → SIMPLE SMALL. 'what time is it' → TOOLS SMALL. 'show me index.js' → TOOLS SMALL. 'add endpoint' → AGENT SMALL. 'build auth' → AGENT LARGE.",
337
337
  userPrompt: command,
338
338
  maxTokens: 10,
339
- category: "classify",
339
+ category: "classifier",
340
340
  });
341
341
 
342
342
  const raw = (result.content || "").trim().toUpperCase();
@@ -424,7 +424,7 @@ ${indexContent}
424
424
  Existing route files:
425
425
  ${existingRoutes || "(none)"}`,
426
426
  maxTokens: 2048,
427
- category: "develop",
427
+ category: "tool",
428
428
  });
429
429
 
430
430
  const raw = (result.content || "").trim().replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "");
@@ -53,6 +53,14 @@ const DEFAULT_PRICING = {
53
53
  "claude-3-sonnet": { input: 3.00, output: 15.00, cache_write: 3.75, cache_read: 0.30 },
54
54
  "claude-3-haiku": { input: 0.25, output: 1.25, cache_write: 0.3125, cache_read: 0.025 },
55
55
 
56
+ // ── Wolverine Self-Hosted (Gemma 4 via api.wolverinenode.xyz) ──
57
+ // Priced between Anthropic and OpenAI — cheaper than both
58
+ "wolverine-test-1": { input: 0.10, output: 0.40 },
59
+ "wolverine-gemma-26b": { input: 0.25, output: 1.00 },
60
+ "wolverine-gemma-8b": { input: 0.10, output: 0.40 },
61
+ "wolverine-coding": { input: 0.10, output: 0.40 },
62
+ "wolverine-reasoning": { input: 0.25, output: 1.00 },
63
+
56
64
  // ── Fallback ──
57
65
  "_default": { input: 1.00, output: 4.00 },
58
66
  };
@@ -33,6 +33,8 @@ class TokenTracker {
33
33
  this._byModel = {};
34
34
  // Per-category totals
35
35
  this._byCategory = {};
36
+ // Per-model-per-category cross-reference (model::category → stats)
37
+ this._byModelCategory = {};
36
38
  // Per-tool totals
37
39
  this._byTool = {};
38
40
  // Timeline: recent entries for charts (in-memory)
@@ -87,7 +89,7 @@ class TokenTracker {
87
89
  };
88
90
 
89
91
  // Accumulate by model
90
- if (!this._byModel[model]) this._byModel[model] = { input: 0, output: 0, total: 0, calls: 0, cost: 0, successes: 0, failures: 0, totalLatencyMs: 0, minLatencyMs: Infinity, maxLatencyMs: 0, cacheCreation: 0, cacheRead: 0, cacheSavings: 0 };
92
+ if (!this._byModel[model]) this._byModel[model] = { input: 0, output: 0, total: 0, calls: 0, cost: 0, successes: 0, failures: 0, totalLatencyMs: 0, totalLatencyTokens: 0, timedCalls: 0, minLatencyMs: Infinity, maxLatencyMs: 0, cacheCreation: 0, cacheRead: 0, cacheSavings: 0 };
91
93
  const m = this._byModel[model];
92
94
  m.input += entry.input;
93
95
  m.output += entry.output;
@@ -100,6 +102,8 @@ class TokenTracker {
100
102
  if (entry.success) m.successes++; else m.failures++;
101
103
  if (latencyMs > 0) {
102
104
  m.totalLatencyMs += latencyMs;
105
+ m.totalLatencyTokens += total;
106
+ m.timedCalls++;
103
107
  if (latencyMs < m.minLatencyMs) m.minLatencyMs = latencyMs;
104
108
  if (latencyMs > m.maxLatencyMs) m.maxLatencyMs = latencyMs;
105
109
  }
@@ -112,6 +116,18 @@ class TokenTracker {
112
116
  this._byCategory[category].calls++;
113
117
  this._byCategory[category].cost += cost.total;
114
118
 
119
+ // Accumulate by model+category cross-reference
120
+ const mcKey = `${model}::${category}`;
121
+ if (!this._byModelCategory[mcKey]) this._byModelCategory[mcKey] = { model, category, input: 0, output: 0, total: 0, calls: 0, cost: 0, successes: 0, failures: 0, totalLatencyMs: 0 };
122
+ const mc = this._byModelCategory[mcKey];
123
+ mc.input += entry.input;
124
+ mc.output += entry.output;
125
+ mc.total += total;
126
+ mc.calls++;
127
+ mc.cost += cost.total;
128
+ if (entry.success) mc.successes++; else mc.failures++;
129
+ if (latencyMs > 0) mc.totalLatencyMs += latencyMs;
130
+
115
131
  // Accumulate by tool
116
132
  if (tool) {
117
133
  const toolKey = tool.split(" ")[0];
@@ -158,6 +174,7 @@ class TokenTracker {
158
174
  },
159
175
  byModel: this._formatModelStats(),
160
176
  byCategory: this._byCategory,
177
+ byModelCategory: this._formatModelCategoryStats(),
161
178
  byTool: this._byTool,
162
179
  // Recent in-memory timeline
163
180
  timeline: this._timeline.slice(-100).map(e => ({
@@ -188,19 +205,42 @@ class TokenTracker {
188
205
  cacheCreation: m.cacheCreation || 0,
189
206
  cacheRead: m.cacheRead || 0,
190
207
  cacheSavings: Math.round((m.cacheSavings || 0) * 1000000) / 1000000,
191
- successes: m.successes || m.calls,
208
+ successes: m.successes != null ? m.successes : m.calls - (m.failures || 0),
192
209
  failures: m.failures || 0,
193
- successRate: m.calls > 0 ? Math.round(((m.successes || m.calls) / m.calls) * 100) : 0,
194
- avgLatencyMs: m.calls > 0 && m.totalLatencyMs ? Math.round(m.totalLatencyMs / m.calls) : 0,
210
+ successRate: m.calls > 0 ? parseFloat((((m.calls - (m.failures || 0)) / m.calls) * 100).toFixed(2)) : 0,
211
+ // Latency normalized by token count
212
+ avgLatencyMs: (m.timedCalls || 0) > 0 ? Math.round(m.totalLatencyMs / m.timedCalls) : 0,
213
+ msPerKToken: (m.totalLatencyTokens || 0) > 0 ? Math.round((m.totalLatencyMs / m.totalLatencyTokens) * 1000) : 0,
214
+ tokensPerSecond: m.totalLatencyMs > 0 ? Math.round((m.totalLatencyTokens || m.total) / (m.totalLatencyMs / 1000) * 10) / 10 : 0,
215
+ outputTokPerSecond: m.totalLatencyMs > 0 && m.output > 0 ? Math.round((m.output / (m.totalLatencyMs / 1000)) * 10) / 10 : 0,
216
+ timedCalls: m.timedCalls || 0,
195
217
  minLatencyMs: m.minLatencyMs === Infinity ? 0 : (m.minLatencyMs || 0),
196
218
  maxLatencyMs: m.maxLatencyMs || 0,
197
- tokensPerSecond: m.totalLatencyMs > 0 ? Math.round((m.total / (m.totalLatencyMs / 1000)) * 10) / 10 : 0,
198
219
  costPerCall: m.calls > 0 ? Math.round((m.cost / m.calls) * 1000000) / 1000000 : 0,
199
220
  };
200
221
  }
201
222
  return result;
202
223
  }
203
224
 
225
+ /**
226
+ * Format model+category cross-reference for analytics.
227
+ * Returns array of { model, category, calls, cost, tokens, successRate, avgLatencyMs }
228
+ */
229
+ _formatModelCategoryStats() {
230
+ return Object.values(this._byModelCategory).map(mc => ({
231
+ model: mc.model,
232
+ category: mc.category,
233
+ calls: mc.calls,
234
+ cost: Math.round(mc.cost * 1000000) / 1000000,
235
+ tokens: mc.total,
236
+ input: mc.input,
237
+ output: mc.output,
238
+ successRate: mc.calls > 0 ? parseFloat((((mc.calls - (mc.failures || 0)) / mc.calls) * 100).toFixed(2)) : 100,
239
+ avgLatencyMs: mc.calls > 0 && mc.totalLatencyMs > 0 ? Math.round(mc.totalLatencyMs / mc.calls) : 0,
240
+ tokensPerSecond: mc.totalLatencyMs > 0 ? Math.round((mc.total / (mc.totalLatencyMs / 1000)) * 10) / 10 : 0,
241
+ }));
242
+ }
243
+
204
244
  /**
205
245
  * Load full history from JSONL file. For dashboard charts across sessions.
206
246
  * @param {number} limit — max entries to return (default: 500)
@@ -253,6 +293,7 @@ class TokenTracker {
253
293
  lastSaved: Date.now(),
254
294
  byModel: this._byModel,
255
295
  byCategory: this._byCategory,
296
+ byModelCategory: this._byModelCategory,
256
297
  byTool: this._byTool,
257
298
  totalTokens: this._totalTokens,
258
299
  totalCalls: this._totalCalls,
@@ -275,6 +316,7 @@ class TokenTracker {
275
316
  const data = JSON.parse(fs.readFileSync(this.usagePath, "utf-8"));
276
317
  this._byModel = data.byModel || {};
277
318
  this._byCategory = data.byCategory || {};
319
+ this._byModelCategory = data.byModelCategory || {};
278
320
  this._byTool = data.byTool || {};
279
321
  this._totalTokens = data.totalTokens || 0;
280
322
  this._totalCalls = data.totalCalls || 0;
@@ -236,7 +236,7 @@ Provide a brief analysis and actionable suggestions. Focus on:
236
236
 
237
237
  Keep your response under 300 words. Be specific and actionable.`,
238
238
  maxTokens: 512,
239
- category: "security",
239
+ category: "audit",
240
240
  });
241
241
 
242
242
  const analysis = result.content;
@@ -172,7 +172,7 @@ class Notifier {
172
172
  systemPrompt: "You summarize server errors for developers. Write 1-2 short sentences. Be direct and actionable. Do not include any secrets, passwords, or API key values — only refer to them by name (e.g. 'the OPENAI_API_KEY').",
173
173
  userPrompt: `Summarize this error for a developer notification:\n\nCategory: ${classification.category}\nError: ${safeError}\n\nStack (first 300 chars): ${safeStack.slice(0, 300)}`,
174
174
  maxTokens: 100,
175
- category: "security",
175
+ category: "audit",
176
176
  });
177
177
 
178
178
  // Double-sanitize the AI response (in case the AI echoes something)
@@ -66,7 +66,8 @@ function collectHeartbeat(subsystems) {
66
66
  totalCalls: tokenTracker?._totalCalls || usage?.session?.totalCalls || 0,
67
67
  totalCacheSavings: _sumCacheSavings(usage?.byModel || {}),
68
68
  byCategory: usage?.byCategory || {},
69
- byModel: usage?.byModel || {}, // includes: latency, successRate, tokensPerSec, cacheSavings per model
69
+ byModel: usage?.byModel || {},
70
+ byModelCategory: usage?.byModelCategory || [],
70
71
  byTool: usage?.byTool || {},
71
72
  byProvider: _aggregateByProvider(usage?.byModel || {}),
72
73
  },
@@ -95,7 +95,7 @@ Respond with ONLY valid JSON:
95
95
  systemPrompt: "You are a security analyst. Respond with ONLY valid JSON.",
96
96
  userPrompt,
97
97
  maxTokens: 128,
98
- category: "security",
98
+ category: "audit",
99
99
  });
100
100
 
101
101
  const content = result.content;