@devflow-tools/server 0.8.9 → 0.8.11
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 +30 -19
- package/dist/app.module.d.ts.map +1 -1
- package/dist/app.module.js +1 -7
- package/dist/app.module.js.map +1 -1
- package/dist/database/database.module.d.ts.map +1 -1
- package/dist/database/database.module.js +4 -2
- package/dist/database/database.module.js.map +1 -1
- package/dist/knowledge/knowledge.controller.d.ts +1 -4
- package/dist/knowledge/knowledge.controller.d.ts.map +1 -1
- package/dist/knowledge/knowledge.controller.js +1 -1
- package/dist/knowledge/knowledge.controller.js.map +1 -1
- package/dist/knowledge/knowledge.service.d.ts +1 -0
- package/dist/knowledge/knowledge.service.d.ts.map +1 -1
- package/dist/knowledge/knowledge.service.js +7 -2
- package/dist/knowledge/knowledge.service.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +16 -2
- package/dist/main.js.map +1 -1
- package/dist/memory/memory.service.d.ts +1 -0
- package/dist/memory/memory.service.d.ts.map +1 -1
- package/dist/memory/memory.service.js +21 -3
- package/dist/memory/memory.service.js.map +1 -1
- package/dist/telemetry/telemetry.controller.d.ts +45 -45
- package/dist/telemetry/telemetry.controller.d.ts.map +1 -1
- package/dist/telemetry/telemetry.controller.js +85 -9
- package/dist/telemetry/telemetry.controller.js.map +1 -1
- package/dist/telemetry/telemetry.service.d.ts +87 -85
- package/dist/telemetry/telemetry.service.d.ts.map +1 -1
- package/dist/telemetry/telemetry.service.js +395 -462
- package/dist/telemetry/telemetry.service.js.map +1 -1
- package/dist/workflow/workflow.controller.d.ts +4 -4
- package/dist/workflow/workflow.service.d.ts +6 -4
- package/dist/workflow/workflow.service.d.ts.map +1 -1
- package/dist/workflow/workflow.service.js +46 -4
- package/dist/workflow/workflow.service.js.map +1 -1
- package/package.json +11 -11
|
@@ -15,63 +15,104 @@ import { TelemetryEngine } from "@devflow-tools/telemetry";
|
|
|
15
15
|
import { ErrorCode } from "../common/error-codes.js";
|
|
16
16
|
let TelemetryService = class TelemetryService {
|
|
17
17
|
constructor(database) {
|
|
18
|
-
this.engine = new TelemetryEngine();
|
|
18
|
+
this.engine = new TelemetryEngine(); // Legacy in-memory store — real data goes through this.db
|
|
19
19
|
this.db = database ?? null;
|
|
20
20
|
}
|
|
21
21
|
async recordEvent(event) {
|
|
22
|
-
|
|
22
|
+
if (!this.db)
|
|
23
|
+
return;
|
|
24
|
+
this.db.insertEvent?.({
|
|
25
|
+
runId: event.runId,
|
|
26
|
+
kind: event.kind,
|
|
27
|
+
timestamp: event.timestamp,
|
|
28
|
+
duration: event.duration,
|
|
29
|
+
toolName: event.data?.toolName,
|
|
30
|
+
pluginName: event.data?.mcpToolName,
|
|
31
|
+
metadata: event.data,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
// ---- Sessions ----
|
|
35
|
+
async createSession(params) {
|
|
36
|
+
if (!this.db)
|
|
37
|
+
return;
|
|
38
|
+
this.db.insertSession({
|
|
39
|
+
id: params.id,
|
|
40
|
+
projectRoot: params.projectRoot,
|
|
41
|
+
label: params.label,
|
|
42
|
+
startedAt: params.startedAt,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
async getSession(id) {
|
|
46
|
+
if (!this.db)
|
|
47
|
+
throw new NotFoundException({ error: { code: ErrorCode.RUN_NOT_FOUND, message: `Session "${id}" not found`, details: {} } });
|
|
48
|
+
const session = this.db.getSession(id);
|
|
49
|
+
if (!session)
|
|
50
|
+
throw new NotFoundException({ error: { code: ErrorCode.RUN_NOT_FOUND, message: `Session "${id}" not found`, details: {} } });
|
|
51
|
+
return session;
|
|
52
|
+
}
|
|
53
|
+
async listSessions(limit, offset, projectRoot) {
|
|
54
|
+
if (!this.db)
|
|
55
|
+
return [];
|
|
56
|
+
return this.db.listSessions(limit, offset, projectRoot);
|
|
57
|
+
}
|
|
58
|
+
async getSessionEvents(sessionId) {
|
|
59
|
+
if (!this.db)
|
|
60
|
+
return [];
|
|
61
|
+
return this.db.listToolCallEventsBySession?.(sessionId) ?? [];
|
|
62
|
+
}
|
|
63
|
+
async closeSession(id, finishedAt) {
|
|
64
|
+
if (!this.db)
|
|
65
|
+
return;
|
|
66
|
+
this.db.closeSession(id, finishedAt);
|
|
67
|
+
}
|
|
68
|
+
async createSkillExecution(params) {
|
|
69
|
+
if (!this.db)
|
|
70
|
+
return;
|
|
71
|
+
this.db.insertSkillExecution({
|
|
72
|
+
executionId: params.executionId,
|
|
73
|
+
sessionId: params.sessionId,
|
|
74
|
+
skillName: params.skillName,
|
|
75
|
+
startedAt: params.startedAt,
|
|
76
|
+
status: 'running',
|
|
77
|
+
});
|
|
23
78
|
}
|
|
79
|
+
// ---- Runs (now backed by sessions) ----
|
|
24
80
|
async listRuns(limit, offset, source) {
|
|
25
|
-
const runs = Array.from(this.engine.runs.values());
|
|
26
|
-
let filtered = runs;
|
|
27
|
-
if (source) {
|
|
28
|
-
filtered = runs.filter((r) => r.meta?.source === source);
|
|
29
|
-
}
|
|
30
|
-
const engineRuns = filtered.slice(offset, offset + limit).map((r) => ({
|
|
31
|
-
id: r.meta?.runId ?? "", agent: r.meta?.agent ?? "", mode: r.meta?.mode ?? "",
|
|
32
|
-
source: r.meta?.source ?? "unknown", status: r.status, startedAt: r.startedAt, tokenUsed: r.tokenUsed,
|
|
33
|
-
}));
|
|
34
|
-
// Also include skill_executions from the database (hook telemetry)
|
|
35
81
|
if (this.db) {
|
|
36
82
|
try {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
id:
|
|
83
|
+
const sessions = this.db.listSessions(limit, offset);
|
|
84
|
+
return sessions.map((s) => ({
|
|
85
|
+
id: s.id,
|
|
40
86
|
agent: "claude-code",
|
|
41
87
|
mode: "hook",
|
|
42
88
|
source: "hook",
|
|
43
|
-
status:
|
|
44
|
-
startedAt:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
mcpToolCalls: e.mcpToolCalls,
|
|
49
|
-
mcpComplianceRate: e.mcpComplianceRate,
|
|
89
|
+
status: s.status,
|
|
90
|
+
startedAt: s.startedAt,
|
|
91
|
+
totalToolCalls: s.totalToolCalls,
|
|
92
|
+
mcpToolCalls: s.mcpToolCalls,
|
|
93
|
+
mcpComplianceRate: s.mcpToolCalls > 0 ? Math.round(s.mcpToolCalls / s.totalToolCalls * 100) / 100 : 0,
|
|
50
94
|
}));
|
|
51
|
-
// Merge: db runs first (most recent), then engine runs, dedup by id
|
|
52
|
-
const seen = new Set();
|
|
53
|
-
const merged = [...dbRuns, ...engineRuns].filter((r) => {
|
|
54
|
-
if (seen.has(r.id))
|
|
55
|
-
return false;
|
|
56
|
-
seen.add(r.id);
|
|
57
|
-
return true;
|
|
58
|
-
});
|
|
59
|
-
return merged.slice(offset, offset + limit);
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
// DB not available — fall back to engine runs only
|
|
63
95
|
}
|
|
96
|
+
catch { }
|
|
64
97
|
}
|
|
65
|
-
return
|
|
98
|
+
return [];
|
|
66
99
|
}
|
|
67
100
|
async getRun(id) {
|
|
68
|
-
//
|
|
69
|
-
const run = this.engine.runs.get(id);
|
|
70
|
-
if (run)
|
|
71
|
-
return run;
|
|
72
|
-
// 2. Fall back to database (skill_executions)
|
|
101
|
+
// Try session first, then skill execution, then engine
|
|
73
102
|
if (this.db) {
|
|
74
103
|
try {
|
|
104
|
+
const s = this.db.getSession(id);
|
|
105
|
+
if (s) {
|
|
106
|
+
return {
|
|
107
|
+
meta: { runId: s.id, agent: "claude-code", mode: "hook", source: "hook" },
|
|
108
|
+
status: s.status,
|
|
109
|
+
startedAt: s.startedAt,
|
|
110
|
+
finishedAt: s.finishedAt,
|
|
111
|
+
totalToolCalls: s.totalToolCalls,
|
|
112
|
+
mcpToolCalls: s.mcpToolCalls,
|
|
113
|
+
steps: [],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
75
116
|
const exec = this.db.getSkillExecution(id);
|
|
76
117
|
if (exec) {
|
|
77
118
|
return {
|
|
@@ -79,411 +120,320 @@ let TelemetryService = class TelemetryService {
|
|
|
79
120
|
status: exec.status,
|
|
80
121
|
startedAt: exec.startedAt,
|
|
81
122
|
finishedAt: exec.finishedAt,
|
|
82
|
-
tokenUsed: exec.totalTokens,
|
|
83
|
-
steps: [],
|
|
84
|
-
// Extra fields from skill_executions
|
|
85
123
|
skillName: exec.skillName,
|
|
86
124
|
totalToolCalls: exec.totalToolCalls,
|
|
87
125
|
mcpToolCalls: exec.mcpToolCalls,
|
|
88
|
-
directToolCalls: exec.directToolCalls,
|
|
89
126
|
mcpComplianceRate: exec.mcpComplianceRate,
|
|
127
|
+
steps: [],
|
|
90
128
|
};
|
|
91
129
|
}
|
|
92
130
|
}
|
|
93
|
-
catch {
|
|
131
|
+
catch { }
|
|
94
132
|
}
|
|
133
|
+
const run = this.engine.runs.get(id);
|
|
134
|
+
if (run)
|
|
135
|
+
return run;
|
|
95
136
|
throw new NotFoundException({ error: { code: ErrorCode.RUN_NOT_FOUND, message: `Run "${id}" not found`, details: {} } });
|
|
96
137
|
}
|
|
97
138
|
async getTimeline(from, to) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
139
|
+
if (!this.db)
|
|
140
|
+
return [];
|
|
141
|
+
try {
|
|
142
|
+
const sessions = this.db.listSessions(500, 0);
|
|
143
|
+
return sessions
|
|
144
|
+
.filter((s) => {
|
|
145
|
+
if (from && s.startedAt < Number(from))
|
|
146
|
+
return false;
|
|
147
|
+
if (to && s.startedAt > Number(to))
|
|
148
|
+
return false;
|
|
149
|
+
return true;
|
|
150
|
+
})
|
|
151
|
+
.map((s) => ({
|
|
152
|
+
runId: s.id,
|
|
153
|
+
stepId: s.id,
|
|
154
|
+
type: "session",
|
|
155
|
+
duration: s.durationMs,
|
|
156
|
+
timestamp: s.startedAt,
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
111
162
|
}
|
|
112
163
|
async getRunEvents(runId) {
|
|
113
|
-
//
|
|
114
|
-
const engineEvents = await this.engine.listEvents(runId);
|
|
115
|
-
if (engineEvents && engineEvents.length > 0)
|
|
116
|
-
return engineEvents;
|
|
117
|
-
// 2. Fall back to database (tool_call_events)
|
|
164
|
+
// Try session events first
|
|
118
165
|
if (this.db) {
|
|
119
166
|
try {
|
|
120
|
-
const
|
|
121
|
-
if (
|
|
122
|
-
return
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
type: e.isMcpTool ? "mcp_tool" : e.toolType === "subagent" ? "subagent" : "tool_use",
|
|
127
|
-
timestamp: e.timestamp,
|
|
128
|
-
duration: e.duration,
|
|
129
|
-
toolName: e.toolName,
|
|
130
|
-
toolType: e.toolType,
|
|
131
|
-
isMcpTool: e.isMcpTool,
|
|
132
|
-
mcpToolName: e.mcpToolName,
|
|
133
|
-
mcpEnforced: e.mcpEnforced,
|
|
134
|
-
mcpFallback: e.mcpFallback,
|
|
135
|
-
tokensUsed: e.tokensUsed,
|
|
136
|
-
blocked: e.blocked,
|
|
137
|
-
blockReason: e.blockReason,
|
|
138
|
-
error: e.error,
|
|
139
|
-
output: e.output,
|
|
140
|
-
input: e.input,
|
|
141
|
-
}));
|
|
142
|
-
}
|
|
167
|
+
const events = this.db.listToolCallEventsBySession(runId);
|
|
168
|
+
if (events && events.length > 0)
|
|
169
|
+
return events;
|
|
170
|
+
const execEvents = this.db.listToolCallEvents(runId);
|
|
171
|
+
if (execEvents && execEvents.length > 0)
|
|
172
|
+
return execEvents;
|
|
143
173
|
}
|
|
144
|
-
catch {
|
|
174
|
+
catch { }
|
|
145
175
|
}
|
|
176
|
+
const engineEvents = await this.engine.listEvents(runId);
|
|
146
177
|
return engineEvents ?? [];
|
|
147
178
|
}
|
|
148
179
|
async getRunStats(runId) {
|
|
149
|
-
// 1. Try in-memory engine first
|
|
150
|
-
const engineStats = await this.engine.getRunStats(runId);
|
|
151
|
-
if (engineStats && engineStats.totalEvents > 0)
|
|
152
|
-
return engineStats;
|
|
153
|
-
// 2. Fall back to database
|
|
154
180
|
if (this.db) {
|
|
155
181
|
try {
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
if (exec) {
|
|
159
|
-
const sumTokens = events?.reduce((s, e) => s + (e.tokensUsed ?? 0), 0) ?? 0;
|
|
182
|
+
const events = this.db.listToolCallEventsBySession(runId) ?? this.db.listToolCallEvents(runId) ?? [];
|
|
183
|
+
if (events.length > 0) {
|
|
160
184
|
return {
|
|
161
|
-
totalEvents: events
|
|
185
|
+
totalEvents: events.length,
|
|
162
186
|
llmCalls: 0,
|
|
163
|
-
totalTokens:
|
|
164
|
-
toolUses:
|
|
187
|
+
totalTokens: 0,
|
|
188
|
+
toolUses: events.length,
|
|
165
189
|
fileReads: 0,
|
|
166
190
|
fileWrites: 0,
|
|
167
191
|
bashCommands: 0,
|
|
168
|
-
errors: events
|
|
169
|
-
totalDuration:
|
|
170
|
-
mcpToolCalls:
|
|
171
|
-
directToolCalls:
|
|
172
|
-
mcpComplianceRate:
|
|
192
|
+
errors: events.filter((e) => e.error).length,
|
|
193
|
+
totalDuration: 0,
|
|
194
|
+
mcpToolCalls: events.filter((e) => e.isMcpTool).length,
|
|
195
|
+
directToolCalls: events.filter((e) => !e.isMcpTool).length,
|
|
196
|
+
mcpComplianceRate: events.length > 0
|
|
197
|
+
? events.filter((e) => e.isMcpTool).length / events.length : 0,
|
|
173
198
|
};
|
|
174
199
|
}
|
|
175
200
|
}
|
|
176
|
-
catch {
|
|
201
|
+
catch { }
|
|
177
202
|
}
|
|
178
|
-
return
|
|
203
|
+
return this.engine.getRunStats(runId);
|
|
179
204
|
}
|
|
180
205
|
async endRun(runId, result) {
|
|
181
206
|
await this.engine.endRun(runId, result);
|
|
182
207
|
}
|
|
183
|
-
|
|
184
|
-
const allEvents = this.collectAllEvents();
|
|
185
|
-
const cutoff = periodDays != null
|
|
186
|
-
? Date.now() - periodDays * 24 * 60 * 60 * 1000
|
|
187
|
-
: undefined;
|
|
188
|
-
const feedbackEvents = allEvents.filter((e) => {
|
|
189
|
-
if (e.data?.toolName !== "context_feedback")
|
|
190
|
-
return false;
|
|
191
|
-
if (projectRoot && e.data?.toolInput?.projectRoot !== projectRoot)
|
|
192
|
-
return false;
|
|
193
|
-
if (cutoff != null && (e.timestamp ?? 0) < cutoff)
|
|
194
|
-
return false;
|
|
195
|
-
return true;
|
|
196
|
-
});
|
|
197
|
-
const total = feedbackEvents.length;
|
|
198
|
-
const hits = feedbackEvents.filter((e) => e.data?.toolInput?.feedback === "hit").length;
|
|
199
|
-
const partials = feedbackEvents.filter((e) => e.data?.toolInput?.feedback === "partial").length;
|
|
200
|
-
const misses = feedbackEvents.filter((e) => e.data?.toolInput?.feedback === "miss").length;
|
|
201
|
-
return {
|
|
202
|
-
total,
|
|
203
|
-
hits,
|
|
204
|
-
partials,
|
|
205
|
-
misses,
|
|
206
|
-
hitRate: total > 0 ? hits / total : 0,
|
|
207
|
-
partialRate: total > 0 ? partials / total : 0,
|
|
208
|
-
missRate: total > 0 ? misses / total : 0,
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
collectAllEvents() {
|
|
212
|
-
const eventsMap = this.engine.events;
|
|
213
|
-
if (!eventsMap)
|
|
214
|
-
return [];
|
|
215
|
-
const all = [];
|
|
216
|
-
for (const events of eventsMap.values()) {
|
|
217
|
-
if (Array.isArray(events))
|
|
218
|
-
all.push(...events);
|
|
219
|
-
}
|
|
220
|
-
return all;
|
|
221
|
-
}
|
|
208
|
+
// ---- Telemetry Stats (from sessions + tool_call_events) ----
|
|
222
209
|
async getStats(periodDays = 7) {
|
|
223
|
-
|
|
210
|
+
if (!this.db)
|
|
211
|
+
return this.emptyStats();
|
|
224
212
|
const now = Date.now();
|
|
225
213
|
const cutoff = now - periodDays * 24 * 60 * 60 * 1000;
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
.map(([date, d]) => ({ date, ...d }))
|
|
261
|
-
.sort((a, b) => a.date.localeCompare(b.date)),
|
|
262
|
-
byWorkflow: this.buildWorkflowBreakdown(periodRuns, now),
|
|
263
|
-
byPlugin: this.buildPluginBreakdown(periodRuns),
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
buildWorkflowBreakdown(periodRuns, now) {
|
|
267
|
-
const byWorkflow = {};
|
|
268
|
-
for (const r of periodRuns) {
|
|
269
|
-
const wf = r.workflowName ?? r.meta?.workflowName ?? "unknown";
|
|
270
|
-
if (!byWorkflow[wf])
|
|
271
|
-
byWorkflow[wf] = { runs: 0, completed: 0, failed: 0, avgDuration: 0 };
|
|
272
|
-
byWorkflow[wf].runs++;
|
|
273
|
-
if (r.status === "completed")
|
|
274
|
-
byWorkflow[wf].completed++;
|
|
275
|
-
else
|
|
276
|
-
byWorkflow[wf].failed++;
|
|
277
|
-
byWorkflow[wf].avgDuration += ((r.finishedAt ?? now) - r.startedAt);
|
|
278
|
-
}
|
|
279
|
-
for (const wf of Object.keys(byWorkflow)) {
|
|
280
|
-
byWorkflow[wf].avgDuration = byWorkflow[wf].runs > 0
|
|
281
|
-
? Math.round(byWorkflow[wf].avgDuration / byWorkflow[wf].runs) : 0;
|
|
214
|
+
try {
|
|
215
|
+
const sessions = this.db.listSessions(1000, 0);
|
|
216
|
+
const periodSessions = sessions.filter((s) => s.startedAt >= cutoff);
|
|
217
|
+
const days = {};
|
|
218
|
+
let totalToolCalls = 0;
|
|
219
|
+
for (const s of periodSessions) {
|
|
220
|
+
const day = new Date(s.startedAt).toISOString().slice(0, 10);
|
|
221
|
+
if (!days[day])
|
|
222
|
+
days[day] = { runs: 0 };
|
|
223
|
+
days[day].runs++;
|
|
224
|
+
totalToolCalls += s.totalToolCalls ?? 0;
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
period: {
|
|
228
|
+
from: new Date(cutoff).toISOString().slice(0, 10),
|
|
229
|
+
to: new Date(now).toISOString().slice(0, 10),
|
|
230
|
+
},
|
|
231
|
+
summary: {
|
|
232
|
+
totalRuns: periodSessions.length,
|
|
233
|
+
completedRuns: periodSessions.filter((s) => s.status === 'completed').length,
|
|
234
|
+
failedRuns: periodSessions.filter((s) => s.status === 'failed').length,
|
|
235
|
+
totalTokens: 0,
|
|
236
|
+
totalLlmCalls: 0,
|
|
237
|
+
totalToolUses: totalToolCalls,
|
|
238
|
+
avgRunDuration: periodSessions.length > 0
|
|
239
|
+
? Math.round(periodSessions.reduce((sum, s) => sum + (s.durationMs ?? 0), 0) / periodSessions.length)
|
|
240
|
+
: 0,
|
|
241
|
+
},
|
|
242
|
+
byDay: Object.entries(days)
|
|
243
|
+
.map(([date, d]) => ({ date, tokens: 0, ...d }))
|
|
244
|
+
.sort((a, b) => a.date.localeCompare(b.date)),
|
|
245
|
+
byWorkflow: [],
|
|
246
|
+
byPlugin: [],
|
|
247
|
+
};
|
|
282
248
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
runs: d.runs,
|
|
286
|
-
successRate: d.runs > 0 ? d.completed / d.runs : 0,
|
|
287
|
-
avgDuration: d.avgDuration,
|
|
288
|
-
}));
|
|
289
|
-
}
|
|
290
|
-
buildPluginBreakdown(periodRuns) {
|
|
291
|
-
const byPlugin = {};
|
|
292
|
-
for (const r of periodRuns) {
|
|
293
|
-
const pl = r.pluginName ?? r.meta?.pluginName ?? "none";
|
|
294
|
-
if (!byPlugin[pl])
|
|
295
|
-
byPlugin[pl] = { runs: 0, completed: 0, failed: 0 };
|
|
296
|
-
byPlugin[pl].runs++;
|
|
297
|
-
if (r.status === "completed")
|
|
298
|
-
byPlugin[pl].completed++;
|
|
299
|
-
else
|
|
300
|
-
byPlugin[pl].failed++;
|
|
249
|
+
catch {
|
|
250
|
+
return this.emptyStats();
|
|
301
251
|
}
|
|
302
|
-
return Object.entries(byPlugin).map(([name, d]) => ({
|
|
303
|
-
name,
|
|
304
|
-
runs: d.runs,
|
|
305
|
-
successRate: d.runs > 0 ? d.completed / d.runs : 0,
|
|
306
|
-
}));
|
|
307
252
|
}
|
|
308
|
-
|
|
309
|
-
const runs = Array.from(this.engine.runs.values());
|
|
310
|
-
const now = Date.now();
|
|
311
|
-
const totalRuns = runs.length;
|
|
312
|
-
const successfulRuns = runs.filter((r) => r.status === "completed").length;
|
|
313
|
-
const successRate = totalRuns > 0 ? successfulRuns / totalRuns : 0;
|
|
314
|
-
const totalTokens = runs.reduce((sum, r) => sum + (r.tokenUsed ?? 0), 0);
|
|
315
|
-
const avgTokenUsage = totalRuns > 0 ? totalTokens / totalRuns : 0;
|
|
316
|
-
const totalDuration = runs.reduce((sum, r) => {
|
|
317
|
-
const start = r.startedAt ?? 0;
|
|
318
|
-
const end = r.finishedAt ?? now;
|
|
319
|
-
return sum + (end - start);
|
|
320
|
-
}, 0);
|
|
321
|
-
const avgDuration = totalRuns > 0 ? totalDuration / totalRuns : 0;
|
|
322
|
-
// Feedback hit rate: combine run-level feedback with context_feedback telemetry events
|
|
323
|
-
const runFeedbackHits = runs.filter((r) => r.relevanceFeedback === "hit" || r.meta?.relevanceFeedback === "hit").length;
|
|
324
|
-
const runFeedbackTotal = runs.filter((r) => r.relevanceFeedback !== undefined || r.meta?.relevanceFeedback !== undefined).length;
|
|
325
|
-
const telemetryFeedback = await this.getFeedbackStats();
|
|
326
|
-
const combinedHits = runFeedbackHits + telemetryFeedback.hits;
|
|
327
|
-
const combinedTotal = runFeedbackTotal + telemetryFeedback.total;
|
|
328
|
-
const feedbackHitRate = combinedTotal > 0 ? combinedHits / combinedTotal : 0;
|
|
329
|
-
return {
|
|
330
|
-
period: "all-time",
|
|
331
|
-
totalRuns,
|
|
332
|
-
successfulRuns,
|
|
333
|
-
successRate,
|
|
334
|
-
avgTokenUsage,
|
|
335
|
-
avgDuration,
|
|
336
|
-
feedbackHitRate,
|
|
337
|
-
trend: "stable",
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
async getExperimentalRoiSummary() {
|
|
253
|
+
emptyStats() {
|
|
341
254
|
return {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
conclusion: "No experimental data available yet",
|
|
255
|
+
period: { from: '', to: '' },
|
|
256
|
+
summary: { totalRuns: 0, completedRuns: 0, failedRuns: 0, totalTokens: 0, totalLlmCalls: 0, totalToolUses: 0, avgRunDuration: 0 },
|
|
257
|
+
byDay: [],
|
|
258
|
+
byWorkflow: [],
|
|
259
|
+
byPlugin: [],
|
|
348
260
|
};
|
|
349
261
|
}
|
|
262
|
+
// ---- ROI Summary (from sessions + tool_call_events) ----
|
|
350
263
|
async getRoiSummary(periodDays = 7) {
|
|
351
|
-
|
|
264
|
+
if (!this.db)
|
|
265
|
+
return this.emptyRoiSummary();
|
|
352
266
|
const now = Date.now();
|
|
353
267
|
const cutoff = now - periodDays * 24 * 60 * 60 * 1000;
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
failureBreakdown[cat] = (failureBreakdown[cat] ?? 0) + 1;
|
|
268
|
+
try {
|
|
269
|
+
const sessions = this.db.listSessions(1000, 0);
|
|
270
|
+
const periodSessions = sessions.filter((s) => s.startedAt >= cutoff);
|
|
271
|
+
const totalRuns = periodSessions.length;
|
|
272
|
+
const completedRuns = periodSessions.filter((s) => s.status === 'completed').length;
|
|
273
|
+
const failedRuns = periodSessions.filter((s) => s.status === 'failed').length;
|
|
274
|
+
const avgDuration = totalRuns > 0
|
|
275
|
+
? Math.round(periodSessions.reduce((sum, s) => sum + (s.durationMs ?? 0), 0) / totalRuns)
|
|
276
|
+
: 0;
|
|
277
|
+
// Daily breakdown
|
|
278
|
+
const days = {};
|
|
279
|
+
for (const s of periodSessions) {
|
|
280
|
+
const day = new Date(s.startedAt).toISOString().slice(0, 10);
|
|
281
|
+
if (!days[day])
|
|
282
|
+
days[day] = { runs: 0, failures: 0 };
|
|
283
|
+
days[day].runs++;
|
|
284
|
+
if (s.status !== 'completed')
|
|
285
|
+
days[day].failures++;
|
|
286
|
+
}
|
|
287
|
+
// Batch fetch all events for all period sessions
|
|
288
|
+
const allEventGroups = this.db.listToolCallEventsBySessions(periodSessions.map((s) => s.id));
|
|
289
|
+
// Failure breakdown from tool_call_events errors
|
|
290
|
+
const failureBreakdown = {
|
|
291
|
+
tool_error: 0,
|
|
292
|
+
mcp_unavailable: 0,
|
|
293
|
+
unknown: 0,
|
|
294
|
+
};
|
|
295
|
+
const byPlugin = {};
|
|
296
|
+
for (const s of periodSessions) {
|
|
297
|
+
const events = (allEventGroups[s.id] ?? []);
|
|
298
|
+
for (const e of events) {
|
|
299
|
+
if (e.error) {
|
|
300
|
+
const err = e.error.toLowerCase();
|
|
301
|
+
if (err.includes('mcp') || err.includes('unavailable') || err.includes('not found'))
|
|
302
|
+
failureBreakdown.mcp_unavailable++;
|
|
303
|
+
else if (err.includes('tool') || err.includes('command') || err.includes('error'))
|
|
304
|
+
failureBreakdown.tool_error++;
|
|
305
|
+
else
|
|
306
|
+
failureBreakdown.unknown++;
|
|
394
307
|
}
|
|
395
|
-
|
|
396
|
-
|
|
308
|
+
if (e.mcpToolName) {
|
|
309
|
+
const prefix = e.mcpToolName.split('_')[0];
|
|
310
|
+
if (prefix)
|
|
311
|
+
byPlugin[prefix] = (byPlugin[prefix] ?? 0) + 1;
|
|
397
312
|
}
|
|
398
313
|
}
|
|
399
|
-
else {
|
|
400
|
-
failureBreakdown.unknown++;
|
|
401
|
-
}
|
|
402
314
|
}
|
|
315
|
+
return {
|
|
316
|
+
period: {
|
|
317
|
+
from: new Date(cutoff).toISOString().slice(0, 10),
|
|
318
|
+
to: new Date(now).toISOString().slice(0, 10),
|
|
319
|
+
},
|
|
320
|
+
overview: {
|
|
321
|
+
totalRuns,
|
|
322
|
+
completedRuns,
|
|
323
|
+
failedRuns,
|
|
324
|
+
avgDuration,
|
|
325
|
+
firstPassSuccessRate: totalRuns > 0 ? Math.round((completedRuns / totalRuns) * 10000) / 100 : 0,
|
|
326
|
+
},
|
|
327
|
+
efficiency: {
|
|
328
|
+
avgTimePerTask: avgDuration,
|
|
329
|
+
},
|
|
330
|
+
failures: {
|
|
331
|
+
total: failedRuns,
|
|
332
|
+
breakdown: failureBreakdown,
|
|
333
|
+
},
|
|
334
|
+
workflows: [],
|
|
335
|
+
plugins: Object.entries(byPlugin).map(([name, runs]) => {
|
|
336
|
+
const pluginSessions = periodSessions.filter((s) => {
|
|
337
|
+
const evts = (allEventGroups[s.id] ?? []);
|
|
338
|
+
return evts.some((e) => e.mcpToolName?.startsWith(name + '_'));
|
|
339
|
+
});
|
|
340
|
+
const completed = pluginSessions.filter((s) => s.status === 'completed').length;
|
|
341
|
+
const successRate = pluginSessions.length > 0
|
|
342
|
+
? Math.round((completed / pluginSessions.length) * 10000) / 100
|
|
343
|
+
: 0;
|
|
344
|
+
return { name, runs, successRate };
|
|
345
|
+
}),
|
|
346
|
+
byDay: Object.entries(days)
|
|
347
|
+
.map(([date, d]) => ({ date, ...d, tokens: 0 }))
|
|
348
|
+
.sort((a, b) => a.date.localeCompare(b.date)),
|
|
349
|
+
};
|
|
403
350
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
for (const r of periodRuns) {
|
|
407
|
-
const day = new Date(r.startedAt).toISOString().slice(0, 10);
|
|
408
|
-
if (!days[day])
|
|
409
|
-
days[day] = { runs: 0, tokens: 0, failures: 0 };
|
|
410
|
-
days[day].runs++;
|
|
411
|
-
days[day].tokens += r.tokenUsed ?? 0;
|
|
412
|
-
if (r.status !== "completed")
|
|
413
|
-
days[day].failures++;
|
|
351
|
+
catch {
|
|
352
|
+
return this.emptyRoiSummary();
|
|
414
353
|
}
|
|
354
|
+
}
|
|
355
|
+
emptyRoiSummary() {
|
|
415
356
|
return {
|
|
416
|
-
period: {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
},
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
},
|
|
424
|
-
efficiency: {
|
|
425
|
-
totalTokensSaved: 0, // needs baseline comparison data
|
|
426
|
-
avgTimePerTask,
|
|
427
|
-
avgRetryCount,
|
|
428
|
-
},
|
|
429
|
-
failures: {
|
|
430
|
-
total: failedRuns,
|
|
431
|
-
breakdown: failureBreakdown,
|
|
432
|
-
},
|
|
433
|
-
workflows: this.buildWorkflowBreakdown(periodRuns, now),
|
|
434
|
-
plugins: this.buildPluginBreakdown(periodRuns),
|
|
435
|
-
byDay: Object.entries(days)
|
|
436
|
-
.map(([date, d]) => ({ date, ...d }))
|
|
437
|
-
.sort((a, b) => a.date.localeCompare(b.date)),
|
|
357
|
+
period: { from: '', to: '' },
|
|
358
|
+
overview: { totalRuns: 0, completedRuns: 0, failedRuns: 0, avgDuration: 0, avgTokens: 0, firstPassSuccessRate: 0, feedbackHitRate: 0 },
|
|
359
|
+
efficiency: { totalTokensSaved: 0, avgTimePerTask: 0, avgRetryCount: 0 },
|
|
360
|
+
failures: { total: 0, breakdown: { tool_error: 0, mcp_unavailable: 0, unknown: 0 } },
|
|
361
|
+
workflows: [],
|
|
362
|
+
plugins: [],
|
|
363
|
+
byDay: [],
|
|
438
364
|
};
|
|
439
365
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
366
|
+
// ---- Skill Execution Tracking ----
|
|
367
|
+
async recordExecutionStart(params) {
|
|
368
|
+
if (!this.db)
|
|
369
|
+
return;
|
|
370
|
+
this.db.insertSkillExecution({
|
|
371
|
+
executionId: params.executionId,
|
|
372
|
+
sessionId: params.sessionId,
|
|
373
|
+
skillName: params.skillName,
|
|
374
|
+
startedAt: params.startedAt,
|
|
375
|
+
status: 'running',
|
|
449
376
|
});
|
|
450
|
-
const total = pluginRuns.length;
|
|
451
|
-
const successful = pluginRuns.filter((r) => r.status === "completed").length;
|
|
452
|
-
const startedAts = pluginRuns.map((r) => r.startedAt).filter((t) => typeof t === "number");
|
|
453
|
-
return {
|
|
454
|
-
name: pluginName,
|
|
455
|
-
totalRuns: total,
|
|
456
|
-
successRate: total > 0 ? successful / total : 0,
|
|
457
|
-
lastUsedAt: startedAts.length > 0
|
|
458
|
-
? new Date(Math.max(...startedAts)).toISOString()
|
|
459
|
-
: null,
|
|
460
|
-
};
|
|
461
377
|
}
|
|
462
|
-
async
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
378
|
+
async recordToolCall(event) {
|
|
379
|
+
if (!this.db)
|
|
380
|
+
return;
|
|
381
|
+
this.db.insertToolCallEvent({
|
|
382
|
+
eventId: event.eventId,
|
|
383
|
+
executionId: event.executionId,
|
|
384
|
+
sessionId: event.sessionId,
|
|
385
|
+
timestamp: event.timestamp,
|
|
386
|
+
toolName: event.toolName,
|
|
387
|
+
toolType: event.toolType,
|
|
388
|
+
isMcpTool: event.isMcpTool,
|
|
389
|
+
mcpToolName: event.mcpToolName,
|
|
390
|
+
mcpEnforced: event.mcpEnforced,
|
|
391
|
+
mcpFallback: event.mcpFallback,
|
|
392
|
+
kind: event.kind ?? 'tool_use',
|
|
393
|
+
input: event.input,
|
|
394
|
+
duration: event.duration,
|
|
395
|
+
parentToolCallId: event.parentToolCallId,
|
|
396
|
+
subagentId: event.subagentId,
|
|
397
|
+
error: event.error,
|
|
398
|
+
blocked: event.blocked,
|
|
399
|
+
blockReason: event.blockReason,
|
|
467
400
|
});
|
|
468
|
-
const totalRuns = pluginRuns.length;
|
|
469
|
-
const successfulRuns = pluginRuns.filter((r) => r.status === "completed").length;
|
|
470
|
-
const projectRoots = new Set();
|
|
471
|
-
for (const r of pluginRuns) {
|
|
472
|
-
const root = r.meta?.projectRoot;
|
|
473
|
-
if (root)
|
|
474
|
-
projectRoots.add(root);
|
|
475
|
-
}
|
|
476
|
-
const startedAts = pluginRuns.map((r) => r.startedAt).filter((t) => typeof t === "number");
|
|
477
|
-
const lastUsedAt = startedAts.length > 0
|
|
478
|
-
? new Date(Math.max(...startedAts)).toISOString()
|
|
479
|
-
: undefined;
|
|
480
|
-
return {
|
|
481
|
-
activeProjects: projectRoots.size,
|
|
482
|
-
totalRuns,
|
|
483
|
-
successfulRuns,
|
|
484
|
-
lastUsedAt,
|
|
485
|
-
};
|
|
486
401
|
}
|
|
402
|
+
async updateToolCallOutput(eventId, output, completedAt, duration) {
|
|
403
|
+
if (!this.db)
|
|
404
|
+
return;
|
|
405
|
+
this.db.updateToolCallEvent(eventId, { output: JSON.stringify(output), duration });
|
|
406
|
+
}
|
|
407
|
+
async recordExecutionComplete(params) {
|
|
408
|
+
if (!this.db)
|
|
409
|
+
return;
|
|
410
|
+
this.db.updateSkillExecution(params.executionId, {
|
|
411
|
+
status: params.status,
|
|
412
|
+
finishedAt: params.finishedAt,
|
|
413
|
+
totalDuration: params.summary.totalDuration,
|
|
414
|
+
mcpComplianceRate: params.summary.mcpComplianceRate,
|
|
415
|
+
totalToolCalls: params.summary.totalToolCalls,
|
|
416
|
+
mcpToolCalls: params.summary.mcpToolCalls,
|
|
417
|
+
directToolCalls: params.summary.directToolCalls,
|
|
418
|
+
subagentCount: params.summary.subagentCount,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
async getExecutions(limit, skillName) {
|
|
422
|
+
if (!this.db)
|
|
423
|
+
return [];
|
|
424
|
+
return this.db.listSkillExecutions(limit, skillName);
|
|
425
|
+
}
|
|
426
|
+
async getExecutionEvents(executionId) {
|
|
427
|
+
if (!this.db)
|
|
428
|
+
return [];
|
|
429
|
+
return this.db.listToolCallEvents(executionId);
|
|
430
|
+
}
|
|
431
|
+
async getMcpCompliance() {
|
|
432
|
+
if (!this.db)
|
|
433
|
+
return { overall: 0, bySkill: {}, missedTools: [] };
|
|
434
|
+
return this.db.getMcpCompliance();
|
|
435
|
+
}
|
|
436
|
+
// ---- Failure classification (unchanged) ----
|
|
487
437
|
async classifyFailure(runId) {
|
|
488
438
|
const run = await this.getRun(runId).catch(() => null);
|
|
489
439
|
if (!run || run.status !== "failed")
|
|
@@ -542,6 +492,13 @@ let TelemetryService = class TelemetryService {
|
|
|
542
492
|
};
|
|
543
493
|
}
|
|
544
494
|
async cleanup(olderThanDays) {
|
|
495
|
+
if (!this.db)
|
|
496
|
+
return 0;
|
|
497
|
+
try {
|
|
498
|
+
this.db.cleanupStaleRecords?.();
|
|
499
|
+
}
|
|
500
|
+
catch { }
|
|
501
|
+
// Also clean up legacy engine runs
|
|
545
502
|
const runs = this.engine.runs;
|
|
546
503
|
const cutoff = Date.now() - olderThanDays * 24 * 60 * 60 * 1000;
|
|
547
504
|
let removed = 0;
|
|
@@ -553,92 +510,68 @@ let TelemetryService = class TelemetryService {
|
|
|
553
510
|
}
|
|
554
511
|
return removed;
|
|
555
512
|
}
|
|
556
|
-
//
|
|
557
|
-
async
|
|
558
|
-
if (!this.db)
|
|
559
|
-
return; // Graceful: no database = skip silently
|
|
560
|
-
this.db.insertSkillExecution({
|
|
561
|
-
executionId: params.executionId,
|
|
562
|
-
skillName: params.skillName,
|
|
563
|
-
startedAt: params.startedAt,
|
|
564
|
-
status: 'running',
|
|
565
|
-
});
|
|
566
|
-
}
|
|
567
|
-
async recordToolCall(event) {
|
|
513
|
+
// @Interval(5 * 60 * 1000) — uncomment when @nestjs/schedule is available
|
|
514
|
+
async cleanupStaleRecords() {
|
|
568
515
|
if (!this.db)
|
|
569
516
|
return;
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
if (!existing) {
|
|
573
|
-
this.db.insertSkillExecution({
|
|
574
|
-
executionId: event.executionId,
|
|
575
|
-
skillName: event.mcpToolName ?? event.toolName,
|
|
576
|
-
startedAt: event.timestamp,
|
|
577
|
-
status: 'running',
|
|
578
|
-
});
|
|
517
|
+
try {
|
|
518
|
+
this.db.cleanupStaleRecords?.();
|
|
579
519
|
}
|
|
580
|
-
|
|
581
|
-
eventId: event.eventId,
|
|
582
|
-
executionId: event.executionId,
|
|
583
|
-
timestamp: event.timestamp,
|
|
584
|
-
toolName: event.toolName,
|
|
585
|
-
toolType: event.toolType,
|
|
586
|
-
isMcpTool: event.isMcpTool,
|
|
587
|
-
mcpToolName: event.mcpToolName,
|
|
588
|
-
mcpEnforced: event.mcpEnforced,
|
|
589
|
-
mcpFallback: event.mcpFallback,
|
|
590
|
-
input: event.input,
|
|
591
|
-
tokensUsed: event.tokensUsed,
|
|
592
|
-
duration: event.duration,
|
|
593
|
-
parentToolCallId: event.parentToolCallId,
|
|
594
|
-
subagentId: event.subagentId,
|
|
595
|
-
error: event.error,
|
|
596
|
-
blocked: event.blocked,
|
|
597
|
-
blockReason: event.blockReason,
|
|
598
|
-
});
|
|
520
|
+
catch { /* best-effort */ }
|
|
599
521
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
this.db.updateToolCallEvent(eventId, { output: JSON.stringify(output) });
|
|
522
|
+
// ---- Stub methods for external callers that reference old API ----
|
|
523
|
+
async getFeedbackStats(_projectRoot, _periodDays) {
|
|
524
|
+
return { total: 0, hits: 0, partials: 0, misses: 0, hitRate: 0, partialRate: 0, missRate: 0 };
|
|
604
525
|
}
|
|
605
|
-
async
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
finishedAt: params.finishedAt,
|
|
617
|
-
totalDuration: params.summary.totalDuration,
|
|
618
|
-
totalTokens: params.summary.totalTokens,
|
|
619
|
-
mcpComplianceRate: totalToolCalls > 0
|
|
620
|
-
? Math.round(mcpToolCalls / totalToolCalls * 10000) / 100
|
|
621
|
-
: 0,
|
|
622
|
-
totalToolCalls,
|
|
623
|
-
mcpToolCalls,
|
|
624
|
-
directToolCalls,
|
|
625
|
-
subagentCount,
|
|
626
|
-
});
|
|
526
|
+
async getOpsRoiSummary() {
|
|
527
|
+
return {
|
|
528
|
+
period: "all-time",
|
|
529
|
+
totalRuns: 0,
|
|
530
|
+
successfulRuns: 0,
|
|
531
|
+
successRate: 0,
|
|
532
|
+
avgTokenUsage: 0,
|
|
533
|
+
avgDuration: 0,
|
|
534
|
+
feedbackHitRate: 0,
|
|
535
|
+
trend: "stable",
|
|
536
|
+
};
|
|
627
537
|
}
|
|
628
|
-
async
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
538
|
+
async getExperimentalRoiSummary() {
|
|
539
|
+
return {
|
|
540
|
+
totalScenarios: 0,
|
|
541
|
+
avgTimeSavedRate: null,
|
|
542
|
+
avgTokenReductionRate: null,
|
|
543
|
+
avgFirstPassLift: null,
|
|
544
|
+
avgReworkReductionRate: null,
|
|
545
|
+
conclusion: "No experimental data available yet",
|
|
546
|
+
};
|
|
632
547
|
}
|
|
633
|
-
async
|
|
548
|
+
async getPluginEffectiveness(pluginName, _projectRoot) {
|
|
634
549
|
if (!this.db)
|
|
635
|
-
return
|
|
636
|
-
|
|
550
|
+
return { name: pluginName, totalRuns: 0, successRate: 0, lastUsedAt: null };
|
|
551
|
+
const sessions = this.db.listSessions(1000, 0);
|
|
552
|
+
let useCount = 0;
|
|
553
|
+
for (const s of sessions) {
|
|
554
|
+
const events = this.db.listToolCallEventsBySession(s.id);
|
|
555
|
+
for (const e of events) {
|
|
556
|
+
if (e.mcpToolName?.startsWith(pluginName + '_'))
|
|
557
|
+
useCount++;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return {
|
|
561
|
+
name: pluginName,
|
|
562
|
+
totalRuns: useCount,
|
|
563
|
+
successRate: 0,
|
|
564
|
+
lastUsedAt: null,
|
|
565
|
+
};
|
|
637
566
|
}
|
|
638
|
-
async
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
567
|
+
async getPluginUsageStats(pluginName) {
|
|
568
|
+
const eff = await this.getPluginEffectiveness(pluginName);
|
|
569
|
+
return {
|
|
570
|
+
activeProjects: 0,
|
|
571
|
+
totalRuns: eff.totalRuns,
|
|
572
|
+
successfulRuns: 0,
|
|
573
|
+
lastUsedAt: eff.lastUsedAt ?? undefined,
|
|
574
|
+
};
|
|
642
575
|
}
|
|
643
576
|
};
|
|
644
577
|
TelemetryService = __decorate([
|