@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.
Files changed (36) hide show
  1. package/README.md +30 -19
  2. package/dist/app.module.d.ts.map +1 -1
  3. package/dist/app.module.js +1 -7
  4. package/dist/app.module.js.map +1 -1
  5. package/dist/database/database.module.d.ts.map +1 -1
  6. package/dist/database/database.module.js +4 -2
  7. package/dist/database/database.module.js.map +1 -1
  8. package/dist/knowledge/knowledge.controller.d.ts +1 -4
  9. package/dist/knowledge/knowledge.controller.d.ts.map +1 -1
  10. package/dist/knowledge/knowledge.controller.js +1 -1
  11. package/dist/knowledge/knowledge.controller.js.map +1 -1
  12. package/dist/knowledge/knowledge.service.d.ts +1 -0
  13. package/dist/knowledge/knowledge.service.d.ts.map +1 -1
  14. package/dist/knowledge/knowledge.service.js +7 -2
  15. package/dist/knowledge/knowledge.service.js.map +1 -1
  16. package/dist/main.d.ts.map +1 -1
  17. package/dist/main.js +16 -2
  18. package/dist/main.js.map +1 -1
  19. package/dist/memory/memory.service.d.ts +1 -0
  20. package/dist/memory/memory.service.d.ts.map +1 -1
  21. package/dist/memory/memory.service.js +21 -3
  22. package/dist/memory/memory.service.js.map +1 -1
  23. package/dist/telemetry/telemetry.controller.d.ts +45 -45
  24. package/dist/telemetry/telemetry.controller.d.ts.map +1 -1
  25. package/dist/telemetry/telemetry.controller.js +85 -9
  26. package/dist/telemetry/telemetry.controller.js.map +1 -1
  27. package/dist/telemetry/telemetry.service.d.ts +87 -85
  28. package/dist/telemetry/telemetry.service.d.ts.map +1 -1
  29. package/dist/telemetry/telemetry.service.js +395 -462
  30. package/dist/telemetry/telemetry.service.js.map +1 -1
  31. package/dist/workflow/workflow.controller.d.ts +4 -4
  32. package/dist/workflow/workflow.service.d.ts +6 -4
  33. package/dist/workflow/workflow.service.d.ts.map +1 -1
  34. package/dist/workflow/workflow.service.js +46 -4
  35. package/dist/workflow/workflow.service.js.map +1 -1
  36. 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
- await this.engine.recordEvent(event);
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 execs = this.db.listSkillExecutions(limit, 0);
38
- const dbRuns = execs.map((e) => ({
39
- id: e.executionId,
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: e.status,
44
- startedAt: e.startedAt,
45
- tokenUsed: e.totalTokens,
46
- skillName: e.skillName,
47
- totalToolCalls: e.totalToolCalls,
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 engineRuns;
98
+ return [];
66
99
  }
67
100
  async getRun(id) {
68
- // 1. Try in-memory engine first
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 { /* DB not available */ }
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
- const runs = Array.from(this.engine.runs.values());
99
- return runs
100
- .filter((r) => {
101
- if (from && r.startedAt < Number(from))
102
- return false;
103
- if (to && r.startedAt > Number(to))
104
- return false;
105
- return true;
106
- })
107
- .flatMap((r) => r.steps?.map((s) => ({
108
- runId: r.meta?.runId, stepId: s.stepId, type: s.type,
109
- duration: s.duration, timestamp: s.timestamp,
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
- // 1. Try in-memory engine first
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 dbEvents = this.db.listToolCallEvents(runId);
121
- if (dbEvents && dbEvents.length > 0) {
122
- return dbEvents.map((e) => ({
123
- eventId: e.eventId,
124
- runId,
125
- stepId: e.eventId,
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 { /* DB not available */ }
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 exec = this.db.getSkillExecution(runId);
157
- const events = this.db.listToolCallEvents(runId);
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?.length ?? 0,
185
+ totalEvents: events.length,
162
186
  llmCalls: 0,
163
- totalTokens: exec.totalTokens || sumTokens,
164
- toolUses: exec.totalToolCalls,
187
+ totalTokens: 0,
188
+ toolUses: events.length,
165
189
  fileReads: 0,
166
190
  fileWrites: 0,
167
191
  bashCommands: 0,
168
- errors: events?.filter((e) => e.error).length ?? 0,
169
- totalDuration: exec.totalDuration,
170
- mcpToolCalls: exec.mcpToolCalls,
171
- directToolCalls: exec.directToolCalls,
172
- mcpComplianceRate: exec.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 { /* DB not available */ }
201
+ catch { }
177
202
  }
178
- return engineStats;
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
- async getFeedbackStats(projectRoot, periodDays) {
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
- const runs = Array.from(this.engine.runs.values());
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
- const periodRuns = runs.filter((r) => r.startedAt >= cutoff);
227
- const days = {};
228
- for (const r of periodRuns) {
229
- const day = new Date(r.startedAt).toISOString().slice(0, 10);
230
- if (!days[day])
231
- days[day] = { runs: 0, tokens: 0 };
232
- days[day].runs++;
233
- days[day].tokens += r.tokenUsed ?? 0;
234
- }
235
- let totalTokens = 0;
236
- let totalLlmCalls = 0;
237
- let totalToolUses = 0;
238
- for (const r of periodRuns) {
239
- const events = (this.engine.events.get(r.meta?.runId) ?? []);
240
- totalTokens += r.tokenUsed ?? 0;
241
- totalLlmCalls += events.filter((e) => e.kind === "llm_call").length;
242
- totalToolUses += events.filter((e) => e.kind === "tool_use").length;
243
- }
244
- const from = new Date(cutoff).toISOString().slice(0, 10);
245
- const to = new Date(now).toISOString().slice(0, 10);
246
- return {
247
- period: { from, to },
248
- summary: {
249
- totalRuns: periodRuns.length,
250
- completedRuns: periodRuns.filter((r) => r.status === "completed").length,
251
- failedRuns: periodRuns.filter((r) => r.status === "failed").length,
252
- totalTokens,
253
- totalLlmCalls,
254
- totalToolUses,
255
- avgRunDuration: periodRuns.length > 0
256
- ? Math.round(periodRuns.reduce((s, r) => s + ((r.finishedAt ?? now) - r.startedAt), 0) / periodRuns.length)
257
- : 0,
258
- },
259
- byDay: Object.entries(days)
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
- return Object.entries(byWorkflow).map(([name, d]) => ({
284
- name,
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
- async getOpsRoiSummary() {
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
- totalScenarios: 0,
343
- avgTimeSavedRate: null,
344
- avgTokenReductionRate: null,
345
- avgFirstPassLift: null,
346
- avgReworkReductionRate: null,
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
- const runs = Array.from(this.engine.runs.values());
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
- const periodRuns = runs.filter((r) => r.startedAt >= cutoff);
355
- // Overview
356
- const totalRuns = periodRuns.length;
357
- const completedRuns = periodRuns.filter((r) => r.status === "completed").length;
358
- const failedRuns = periodRuns.filter((r) => r.status !== "completed").length;
359
- const avgDuration = totalRuns > 0
360
- ? Math.round(periodRuns.reduce((s, r) => s + ((r.finishedAt ?? now) - r.startedAt), 0) / totalRuns)
361
- : 0;
362
- const avgTokens = totalRuns > 0
363
- ? Math.round(periodRuns.reduce((s, r) => s + (r.tokenUsed ?? 0), 0) / totalRuns)
364
- : 0;
365
- const firstPassSuccesses = periodRuns.filter((r) => r.firstPassSuccess === true || r.meta?.firstPassSuccess === true).length;
366
- const firstPassSuccessRate = totalRuns > 0 ? firstPassSuccesses / totalRuns : 0;
367
- // Combine run-level feedback with context_feedback telemetry events
368
- const runFeedbackHits = periodRuns.filter((r) => r.relevanceFeedback === "hit" || r.meta?.relevanceFeedback === "hit").length;
369
- const runFeedbackTotal = periodRuns.filter((r) => r.relevanceFeedback !== undefined || r.meta?.relevanceFeedback !== undefined).length;
370
- const telemetryFeedback = await this.getFeedbackStats(undefined, periodDays);
371
- const combinedHits = runFeedbackHits + telemetryFeedback.hits;
372
- const combinedTotal = runFeedbackTotal + telemetryFeedback.total;
373
- const feedbackHitRate = combinedTotal > 0 ? combinedHits / combinedTotal : 0;
374
- // Efficiency
375
- const totalRetries = periodRuns.reduce((s, r) => s + (r.retryCount ?? r.meta?.retryCount ?? 0), 0);
376
- const avgRetryCount = totalRuns > 0 ? totalRetries / totalRuns : 0;
377
- const avgTimePerTask = avgDuration;
378
- // Failure breakdown
379
- const failureBreakdown = {
380
- context_insufficient: 0,
381
- knowledge_missing: 0,
382
- workflow_design_issue: 0,
383
- tool_error: 0,
384
- agent_override: 0,
385
- unknown: 0,
386
- };
387
- for (const r of periodRuns) {
388
- if (r.status !== "completed") {
389
- const runId = r.meta?.runId;
390
- if (runId) {
391
- try {
392
- const cat = await this.engine.classifyFailure(runId);
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
- catch {
396
- failureBreakdown.unknown++;
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
- // Daily breakdown
405
- const days = {};
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
- from: new Date(cutoff).toISOString().slice(0, 10),
418
- to: new Date(now).toISOString().slice(0, 10),
419
- },
420
- overview: {
421
- totalRuns, completedRuns, failedRuns, avgDuration, avgTokens,
422
- firstPassSuccessRate, feedbackHitRate,
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
- async getPluginEffectiveness(pluginName, projectRoot) {
441
- const runs = Array.from(this.engine.runs.values());
442
- const pluginRuns = runs.filter((r) => {
443
- const name = r.pluginName ?? r.meta?.pluginName;
444
- if (name !== pluginName)
445
- return false;
446
- if (projectRoot && r.meta?.projectRoot !== projectRoot)
447
- return false;
448
- return true;
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 getPluginUsageStats(pluginName) {
463
- const runs = Array.from(this.engine.runs.values());
464
- const pluginRuns = runs.filter((r) => {
465
- const name = r.pluginName ?? r.meta?.pluginName;
466
- return name === pluginName;
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
- // ---- Skill Execution Tracking ----
557
- async recordExecutionStart(params) {
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
- // Auto-create a placeholder execution if none exists (orphan tool call from hook)
571
- const existing = this.db.getSkillExecution(event.executionId);
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
- this.db.insertToolCallEvent({
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
- async updateToolCallOutput(eventId, output) {
601
- if (!this.db)
602
- return;
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 recordExecutionComplete(params) {
606
- if (!this.db)
607
- return;
608
- // Defensive: re-aggregate real counts from DB, don't trust client input
609
- const events = this.db.listToolCallEvents(params.executionId);
610
- const totalToolCalls = events.length;
611
- const mcpToolCalls = events.filter((e) => e.isMcpTool).length;
612
- const directToolCalls = events.filter((e) => !e.isMcpTool).length;
613
- const subagentCount = events.filter((e) => e.toolType === 'subagent').length;
614
- this.db.updateSkillExecution(params.executionId, {
615
- status: params.status,
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 getExecutions(limit, skillName) {
629
- if (!this.db)
630
- return [];
631
- return this.db.listSkillExecutions(limit, skillName);
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 getExecutionEvents(executionId) {
548
+ async getPluginEffectiveness(pluginName, _projectRoot) {
634
549
  if (!this.db)
635
- return [];
636
- return this.db.listToolCallEvents(executionId);
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 getMcpCompliance() {
639
- if (!this.db)
640
- return { overall: 0, bySkill: {}, missedTools: [] };
641
- return this.db.getMcpCompliance();
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([