@devflow-tools/server 0.8.10 → 0.9.0

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