selftune 0.2.23 → 0.2.24

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 (219) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +93 -15
  3. package/apps/local-dashboard/dist/assets/index-DgY2KGP-.css +1 -0
  4. package/apps/local-dashboard/dist/assets/index-Dmx7LPVX.js +15 -0
  5. package/apps/local-dashboard/dist/assets/vendor-react-C5oyHiV1.js +11 -0
  6. package/apps/local-dashboard/dist/assets/{vendor-table-BIiI3YhS.js → vendor-table-Bc_bbKd8.js} +1 -1
  7. package/apps/local-dashboard/dist/assets/vendor-ui-B3BPIYy7.js +1 -0
  8. package/apps/local-dashboard/dist/index.html +5 -5
  9. package/cli/selftune/adapters/codex/install.ts +310 -78
  10. package/cli/selftune/adapters/opencode/install.ts +3 -4
  11. package/cli/selftune/alpha-upload/build-payloads.ts +3 -3
  12. package/cli/selftune/alpha-upload/stage-canonical.ts +17 -11
  13. package/cli/selftune/auto-update.ts +200 -8
  14. package/cli/selftune/canonical-export.ts +55 -25
  15. package/cli/selftune/command-surface.ts +397 -0
  16. package/cli/selftune/contribute/contribute.ts +64 -13
  17. package/cli/selftune/contribution-config.ts +57 -3
  18. package/cli/selftune/contribution-preferences.ts +117 -0
  19. package/cli/selftune/contribution-signals.ts +8 -4
  20. package/cli/selftune/contribution-staging.ts +13 -2
  21. package/cli/selftune/contributions.ts +55 -121
  22. package/cli/selftune/creator-contributions.ts +29 -10
  23. package/cli/selftune/cron/setup.ts +7 -3
  24. package/cli/selftune/dashboard-contract.ts +73 -0
  25. package/cli/selftune/dashboard-server.ts +168 -17
  26. package/cli/selftune/dashboard.ts +350 -17
  27. package/cli/selftune/eval/baseline.ts +21 -5
  28. package/cli/selftune/eval/execution-eval.ts +170 -0
  29. package/cli/selftune/eval/family-overlap.ts +2 -2
  30. package/cli/selftune/eval/hooks-to-evals.ts +228 -82
  31. package/cli/selftune/eval/import-skillsbench.ts +2 -2
  32. package/cli/selftune/eval/invocation-classifier.ts +56 -0
  33. package/cli/selftune/eval/synthetic-evals.ts +5 -3
  34. package/cli/selftune/eval/unit-test-cli.ts +7 -4
  35. package/cli/selftune/evolution/apply-proposal.ts +295 -0
  36. package/cli/selftune/evolution/engines/replay-engine.ts +79 -57
  37. package/cli/selftune/evolution/evolve-body.ts +100 -39
  38. package/cli/selftune/evolution/evolve.ts +244 -52
  39. package/cli/selftune/evolution/rollback.ts +0 -1
  40. package/cli/selftune/evolution/validate-body.ts +68 -42
  41. package/cli/selftune/evolution/validate-host-replay.ts +510 -60
  42. package/cli/selftune/evolution/validate-proposal.ts +11 -150
  43. package/cli/selftune/evolution/validate-routing.ts +43 -41
  44. package/cli/selftune/evolution/validation-contract.ts +91 -0
  45. package/cli/selftune/grading/auto-grade.ts +11 -7
  46. package/cli/selftune/grading/grade-session.ts +10 -16
  47. package/cli/selftune/index.ts +35 -10
  48. package/cli/selftune/ingestors/claude-replay.ts +15 -10
  49. package/cli/selftune/ingestors/codex-wrapper.ts +3 -3
  50. package/cli/selftune/ingestors/opencode-ingest.ts +2 -2
  51. package/cli/selftune/ingestors/pi-ingest.ts +3 -2
  52. package/cli/selftune/init.ts +27 -3
  53. package/cli/selftune/localdb/direct-write.ts +35 -1
  54. package/cli/selftune/localdb/queries/cron.ts +34 -0
  55. package/cli/selftune/localdb/queries/dashboard.ts +834 -0
  56. package/cli/selftune/localdb/queries/evolution.ts +158 -0
  57. package/cli/selftune/localdb/queries/execution.ts +133 -0
  58. package/cli/selftune/localdb/queries/json.ts +18 -0
  59. package/cli/selftune/localdb/queries/monitoring.ts +263 -0
  60. package/cli/selftune/localdb/queries/raw.ts +95 -0
  61. package/cli/selftune/localdb/queries/staging.ts +270 -0
  62. package/cli/selftune/localdb/queries/trust.ts +392 -0
  63. package/cli/selftune/localdb/queries.ts +60 -2288
  64. package/cli/selftune/localdb/schema.ts +21 -0
  65. package/cli/selftune/monitoring/watch.ts +96 -29
  66. package/cli/selftune/normalization.ts +3 -0
  67. package/cli/selftune/observability.ts +4 -2
  68. package/cli/selftune/orchestrate/cli.ts +161 -0
  69. package/cli/selftune/orchestrate/execute.ts +295 -0
  70. package/cli/selftune/orchestrate/finalize.ts +157 -0
  71. package/cli/selftune/orchestrate/locks.ts +40 -0
  72. package/cli/selftune/orchestrate/plan.ts +131 -0
  73. package/cli/selftune/orchestrate/post-run.ts +59 -0
  74. package/cli/selftune/orchestrate/prepare.ts +334 -0
  75. package/cli/selftune/orchestrate/report.ts +182 -0
  76. package/cli/selftune/orchestrate/runtime.ts +120 -0
  77. package/cli/selftune/orchestrate/signals.ts +48 -0
  78. package/cli/selftune/orchestrate.ts +150 -1173
  79. package/cli/selftune/repair/skill-usage.ts +5 -2
  80. package/cli/selftune/routes/overview.ts +5 -2
  81. package/cli/selftune/routes/skill-report.ts +15 -2
  82. package/cli/selftune/schedule.ts +5 -5
  83. package/cli/selftune/status.ts +39 -2
  84. package/cli/selftune/testing-readiness.ts +597 -0
  85. package/cli/selftune/types.ts +44 -4
  86. package/cli/selftune/uninstall.ts +2 -1
  87. package/cli/selftune/utils/canonical-log.ts +1 -9
  88. package/cli/selftune/utils/cli-error.ts +9 -0
  89. package/cli/selftune/utils/llm-call.ts +126 -6
  90. package/cli/selftune/utils/skill-discovery.ts +2 -0
  91. package/cli/selftune/workflows/proposals.ts +184 -0
  92. package/cli/selftune/workflows/skill-scaffold.ts +241 -0
  93. package/cli/selftune/workflows/workflows.ts +100 -26
  94. package/node_modules/@selftune/telemetry-contract/fixtures/complete-push.ts +1 -1
  95. package/node_modules/@selftune/telemetry-contract/fixtures/evidence-only-push.ts +1 -1
  96. package/node_modules/@selftune/telemetry-contract/fixtures/partial-push-no-sessions.ts +1 -1
  97. package/node_modules/@selftune/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +1 -1
  98. package/node_modules/@selftune/telemetry-contract/src/schemas.ts +41 -1
  99. package/node_modules/@selftune/telemetry-contract/src/types.ts +103 -2
  100. package/package.json +25 -9
  101. package/packages/dashboard-core/AGENTS.md +18 -0
  102. package/packages/dashboard-core/README.md +30 -0
  103. package/packages/dashboard-core/index.ts +3 -0
  104. package/packages/dashboard-core/package.json +39 -0
  105. package/packages/dashboard-core/src/chrome/DashboardChrome.tsx +74 -0
  106. package/packages/dashboard-core/src/chrome/DashboardHeader.tsx +200 -0
  107. package/packages/dashboard-core/src/chrome/DashboardSidebar.tsx +219 -0
  108. package/packages/dashboard-core/src/chrome/RuntimeBadge.tsx +46 -0
  109. package/packages/dashboard-core/src/chrome/index.ts +14 -0
  110. package/packages/dashboard-core/src/chrome/types.ts +81 -0
  111. package/packages/dashboard-core/src/chrome/utils.ts +23 -0
  112. package/packages/dashboard-core/src/gates/FeatureGate.tsx +11 -0
  113. package/packages/dashboard-core/src/gates/LockedRoute.tsx +29 -0
  114. package/packages/dashboard-core/src/gates/UpgradeCard.tsx +89 -0
  115. package/packages/dashboard-core/src/gates/index.ts +3 -0
  116. package/packages/dashboard-core/src/host/DashboardHostProvider.tsx +62 -0
  117. package/packages/dashboard-core/src/host/adapter.ts +47 -0
  118. package/packages/dashboard-core/src/host/capabilities.ts +55 -0
  119. package/packages/dashboard-core/src/host/index.ts +3 -0
  120. package/packages/dashboard-core/src/models/analytics.ts +39 -0
  121. package/packages/dashboard-core/src/models/index.ts +4 -0
  122. package/packages/dashboard-core/src/models/overview.ts +98 -0
  123. package/packages/dashboard-core/src/models/runtime.ts +7 -0
  124. package/packages/dashboard-core/src/models/skills.ts +34 -0
  125. package/packages/dashboard-core/src/routes/index.ts +2 -0
  126. package/packages/dashboard-core/src/routes/manifest.test.ts +70 -0
  127. package/packages/dashboard-core/src/routes/manifest.ts +451 -0
  128. package/packages/dashboard-core/src/routes/types.ts +39 -0
  129. package/packages/dashboard-core/src/screens/analytics/AnalyticsScreen.tsx +278 -0
  130. package/packages/dashboard-core/src/screens/analytics/index.ts +1 -0
  131. package/packages/dashboard-core/src/screens/index.ts +37 -0
  132. package/packages/dashboard-core/src/screens/overview/OverviewComparisonSurface.test.ts +101 -0
  133. package/packages/dashboard-core/src/screens/overview/OverviewComparisonSurface.tsx +393 -0
  134. package/packages/dashboard-core/src/screens/overview/OverviewCompositionSurface.test.tsx +113 -0
  135. package/packages/dashboard-core/src/screens/overview/OverviewCompositionSurface.tsx +72 -0
  136. package/packages/dashboard-core/src/screens/overview/OverviewCoreSurface.tsx +71 -0
  137. package/packages/dashboard-core/src/screens/overview/OverviewOnboardingBanner.tsx +90 -0
  138. package/packages/dashboard-core/src/screens/overview/OverviewRunSummary.tsx +40 -0
  139. package/packages/dashboard-core/src/screens/overview/index.ts +16 -0
  140. package/packages/dashboard-core/src/screens/overview/types.ts +13 -0
  141. package/packages/dashboard-core/src/screens/skill-report/SkillReportDailyBreakdownSection.tsx +99 -0
  142. package/packages/dashboard-core/src/screens/skill-report/SkillReportDataQualityTabContent.tsx +35 -0
  143. package/packages/dashboard-core/src/screens/skill-report/SkillReportEvidenceRail.tsx +71 -0
  144. package/packages/dashboard-core/src/screens/skill-report/SkillReportEvidenceSection.tsx +63 -0
  145. package/packages/dashboard-core/src/screens/skill-report/SkillReportEvidenceTabContent.tsx +25 -0
  146. package/packages/dashboard-core/src/screens/skill-report/SkillReportInvocationsSection.tsx +24 -0
  147. package/packages/dashboard-core/src/screens/skill-report/SkillReportMissedQueriesSection.tsx +79 -0
  148. package/packages/dashboard-core/src/screens/skill-report/SkillReportScaffold.tsx +150 -0
  149. package/packages/dashboard-core/src/screens/skill-report/SkillReportSections.test.tsx +224 -0
  150. package/packages/dashboard-core/src/screens/skill-report/SkillReportTabs.test.tsx +76 -0
  151. package/packages/dashboard-core/src/screens/skill-report/SkillReportTabs.tsx +88 -0
  152. package/packages/dashboard-core/src/screens/skill-report/SkillReportTrendSection.tsx +33 -0
  153. package/packages/dashboard-core/src/screens/skill-report/SkillReportTrustBadge.tsx +67 -0
  154. package/packages/dashboard-core/src/screens/skill-report/index.ts +45 -0
  155. package/packages/dashboard-core/src/screens/skills/SkillsLibraryScreen.tsx +162 -0
  156. package/packages/dashboard-core/src/screens/skills/index.ts +6 -0
  157. package/packages/telemetry-contract/fixtures/complete-push.ts +1 -1
  158. package/packages/telemetry-contract/fixtures/evidence-only-push.ts +1 -1
  159. package/packages/telemetry-contract/fixtures/partial-push-no-sessions.ts +1 -1
  160. package/packages/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +1 -1
  161. package/packages/telemetry-contract/src/schemas.ts +41 -1
  162. package/packages/telemetry-contract/src/types.ts +103 -2
  163. package/packages/ui/src/components/EvidenceViewer.tsx +80 -25
  164. package/packages/ui/src/components/OverviewPanels.tsx +67 -26
  165. package/packages/ui/src/primitives/tabs.tsx +7 -6
  166. package/packages/ui/src/types.ts +10 -0
  167. package/skill/SKILL.md +130 -332
  168. package/skill/agents/diagnosis-analyst.md +3 -3
  169. package/skill/agents/evolution-reviewer.md +3 -3
  170. package/skill/agents/integration-guide.md +3 -3
  171. package/skill/agents/pattern-analyst.md +2 -2
  172. package/skill/references/cli-quick-reference.md +89 -0
  173. package/skill/references/creator-playbook.md +131 -0
  174. package/skill/references/examples.md +48 -0
  175. package/skill/references/troubleshooting.md +47 -0
  176. package/skill/references/version-history.md +1 -1
  177. package/skill/selftune.contribute.json +11 -0
  178. package/skill/{Workflows → workflows}/Baseline.md +20 -1
  179. package/skill/{Workflows → workflows}/Contribute.md +23 -10
  180. package/skill/{Workflows → workflows}/Contributions.md +13 -5
  181. package/skill/workflows/CreateTestDeploy.md +170 -0
  182. package/skill/{Workflows → workflows}/CreatorContributions.md +18 -6
  183. package/skill/{Workflows → workflows}/Cron.md +1 -1
  184. package/skill/{Workflows → workflows}/Dashboard.md +20 -0
  185. package/skill/{Workflows → workflows}/Doctor.md +1 -1
  186. package/skill/{Workflows → workflows}/Evals.md +67 -2
  187. package/skill/{Workflows → workflows}/Evolve.md +119 -30
  188. package/skill/{Workflows → workflows}/EvolveBody.md +41 -1
  189. package/skill/{Workflows → workflows}/Grade.md +1 -1
  190. package/skill/{Workflows → workflows}/Initialize.md +8 -4
  191. package/skill/{Workflows → workflows}/Orchestrate.md +13 -3
  192. package/skill/{Workflows → workflows}/Schedule.md +3 -3
  193. package/skill/workflows/SignalsDashboard.md +87 -0
  194. package/skill/{Workflows → workflows}/UnitTest.md +19 -0
  195. package/skill/{Workflows → workflows}/Watch.md +42 -2
  196. package/skill/{Workflows → workflows}/Workflows.md +39 -2
  197. package/apps/local-dashboard/dist/assets/index-CwOtTrUS.css +0 -1
  198. package/apps/local-dashboard/dist/assets/index-f1HQpbeH.js +0 -59
  199. package/apps/local-dashboard/dist/assets/vendor-react-CKkiCskZ.js +0 -11
  200. package/apps/local-dashboard/dist/assets/vendor-ui-jVSaIZey.js +0 -12
  201. /package/skill/{Workflows → workflows}/AlphaUpload.md +0 -0
  202. /package/skill/{Workflows → workflows}/AutoActivation.md +0 -0
  203. /package/skill/{Workflows → workflows}/Badge.md +0 -0
  204. /package/skill/{Workflows → workflows}/Composability.md +0 -0
  205. /package/skill/{Workflows → workflows}/EvolutionMemory.md +0 -0
  206. /package/skill/{Workflows → workflows}/ExportCanonical.md +0 -0
  207. /package/skill/{Workflows → workflows}/Hook.md +0 -0
  208. /package/skill/{Workflows → workflows}/ImportSkillsBench.md +0 -0
  209. /package/skill/{Workflows → workflows}/Ingest.md +0 -0
  210. /package/skill/{Workflows → workflows}/PlatformHooks.md +0 -0
  211. /package/skill/{Workflows → workflows}/Quickstart.md +0 -0
  212. /package/skill/{Workflows → workflows}/Recover.md +0 -0
  213. /package/skill/{Workflows → workflows}/Registry.md +0 -0
  214. /package/skill/{Workflows → workflows}/RepairSkillUsage.md +0 -0
  215. /package/skill/{Workflows → workflows}/Replay.md +0 -0
  216. /package/skill/{Workflows → workflows}/Rollback.md +0 -0
  217. /package/skill/{Workflows → workflows}/Sync.md +0 -0
  218. /package/skill/{Workflows → workflows}/Telemetry.md +0 -0
  219. /package/skill/{Workflows → workflows}/Uninstall.md +0 -0
@@ -0,0 +1,834 @@
1
+ import type { Database } from "bun:sqlite";
2
+
3
+ import type {
4
+ AnalyticsResponse,
5
+ OverviewPaginatedPayload,
6
+ OverviewPayload,
7
+ PaginatedResult,
8
+ PaginationCursor,
9
+ RecentActivityItem,
10
+ SkillReportPaginatedPayload,
11
+ SkillReportPayload,
12
+ SkillTestingReadiness,
13
+ SkillSummary,
14
+ SkillUsageRecord,
15
+ TelemetryRecord,
16
+ } from "../../dashboard-contract.js";
17
+ import { queryEvolutionEvidence, getPendingProposals } from "./evolution.js";
18
+ import { safeParseJsonArray } from "./json.js";
19
+ import { queryTrustedSkillObservationRows } from "./trust.js";
20
+ import { listSkillTestingReadiness } from "../../testing-readiness.js";
21
+ import { classifySkillPath } from "../../utils/skill-discovery.js";
22
+
23
+ function mapOverviewEvolutionEntry(row: {
24
+ timestamp: string;
25
+ proposal_id: string;
26
+ skill_name: string | null;
27
+ action: string;
28
+ details: string;
29
+ }): OverviewPayload["evolution"][number] {
30
+ return {
31
+ timestamp: row.timestamp,
32
+ proposal_id: row.proposal_id,
33
+ skill_name: row.skill_name ?? undefined,
34
+ action: row.action,
35
+ details: row.details,
36
+ };
37
+ }
38
+
39
+ function mapEvidenceEntry(
40
+ row: ReturnType<typeof queryEvolutionEvidence>[number],
41
+ ): SkillReportPayload["evidence"][number] {
42
+ return {
43
+ proposal_id: row.proposal_id,
44
+ target: row.target,
45
+ stage: row.stage,
46
+ timestamp: row.timestamp,
47
+ rationale: row.rationale ?? null,
48
+ confidence: row.confidence ?? null,
49
+ original_text: row.original_text ?? null,
50
+ proposed_text: row.proposed_text ?? null,
51
+ validation: row.validation ?? null,
52
+ details: row.details ?? null,
53
+ eval_set: row.eval_set ?? [],
54
+ };
55
+ }
56
+
57
+ export function getOverviewPayload(db: Database): OverviewPayload {
58
+ const telemetryRows = db
59
+ .query(
60
+ `SELECT timestamp, session_id, skills_triggered_json, errors_encountered, total_tool_calls
61
+ FROM session_telemetry
62
+ ORDER BY timestamp DESC
63
+ LIMIT 1000`,
64
+ )
65
+ .all() as Array<{
66
+ timestamp: string;
67
+ session_id: string;
68
+ skills_triggered_json: string | null;
69
+ errors_encountered: number;
70
+ total_tool_calls: number;
71
+ }>;
72
+
73
+ const telemetry = telemetryRows.map((row) => ({
74
+ timestamp: row.timestamp,
75
+ session_id: row.session_id,
76
+ skills_triggered: safeParseJsonArray<string>(row.skills_triggered_json),
77
+ errors_encountered: row.errors_encountered,
78
+ total_tool_calls: row.total_tool_calls,
79
+ }));
80
+
81
+ const skillRows = db
82
+ .query(
83
+ `SELECT occurred_at, session_id, skill_name, skill_path, query, triggered, source
84
+ FROM skill_invocations
85
+ ORDER BY occurred_at DESC
86
+ LIMIT 2000`,
87
+ )
88
+ .all() as Array<{
89
+ occurred_at: string;
90
+ session_id: string;
91
+ skill_name: string;
92
+ skill_path: string;
93
+ query: string;
94
+ triggered: number;
95
+ source: string | null;
96
+ }>;
97
+
98
+ const skills = skillRows.map((row) => ({
99
+ timestamp: row.occurred_at,
100
+ session_id: row.session_id,
101
+ skill_name: row.skill_name,
102
+ skill_path: row.skill_path,
103
+ query: row.query,
104
+ triggered: row.triggered === 1,
105
+ source: row.source,
106
+ }));
107
+
108
+ const evolutionRows = db
109
+ .query(
110
+ `SELECT timestamp, proposal_id, skill_name, action, details
111
+ FROM evolution_audit
112
+ ORDER BY timestamp DESC
113
+ LIMIT 500`,
114
+ )
115
+ .all() as Array<{
116
+ timestamp: string;
117
+ proposal_id: string;
118
+ skill_name: string | null;
119
+ action: string;
120
+ details: string;
121
+ }>;
122
+ const evolution = evolutionRows.map(mapOverviewEvolutionEntry);
123
+
124
+ const counts = db
125
+ .query(
126
+ `SELECT
127
+ (SELECT COUNT(*) FROM session_telemetry) as telemetry,
128
+ (SELECT COUNT(*) FROM skill_invocations) as skills,
129
+ (SELECT COUNT(*) FROM evolution_audit) as evolution,
130
+ (SELECT COUNT(*) FROM evolution_evidence) as evidence,
131
+ (SELECT COUNT(*) FROM sessions) as sessions,
132
+ (SELECT COUNT(*) FROM prompts) as prompts`,
133
+ )
134
+ .get() as {
135
+ telemetry: number;
136
+ skills: number;
137
+ evolution: number;
138
+ evidence: number;
139
+ sessions: number;
140
+ prompts: number;
141
+ };
142
+
143
+ const unmatchedRows = db
144
+ .query(
145
+ `SELECT si.occurred_at AS timestamp, si.session_id, si.query
146
+ FROM skill_invocations si
147
+ WHERE si.triggered = 0
148
+ AND NOT EXISTS (
149
+ SELECT 1 FROM skill_invocations si2
150
+ WHERE si2.query = si.query AND si2.triggered = 1
151
+ )
152
+ ORDER BY si.occurred_at DESC
153
+ LIMIT 500`,
154
+ )
155
+ .all() as Array<{ timestamp: string; session_id: string; query: string }>;
156
+
157
+ return {
158
+ telemetry,
159
+ skills,
160
+ evolution,
161
+ counts,
162
+ unmatched_queries: unmatchedRows,
163
+ pending_proposals: getPendingProposals(db),
164
+ active_sessions: getActiveSessionCount(db),
165
+ recent_activity: getRecentActivity(db),
166
+ };
167
+ }
168
+
169
+ export function getSkillReportPayload(db: Database, skillName: string): SkillReportPayload {
170
+ const usageRow = db
171
+ .query(
172
+ `SELECT
173
+ COUNT(*) as total_checks,
174
+ SUM(CASE WHEN triggered = 1 THEN 1 ELSE 0 END) as triggered_count
175
+ FROM skill_invocations
176
+ WHERE skill_name = ?`,
177
+ )
178
+ .get(skillName) as { total_checks: number; triggered_count: number };
179
+
180
+ const total = usageRow.total_checks;
181
+ const triggered = usageRow.triggered_count;
182
+ const passRate = total > 0 ? triggered / total : 0;
183
+
184
+ const invocationRows = db
185
+ .query(
186
+ `SELECT occurred_at, session_id, query, triggered, source
187
+ FROM skill_invocations
188
+ WHERE skill_name = ?
189
+ ORDER BY occurred_at DESC
190
+ LIMIT 100`,
191
+ )
192
+ .all(skillName) as Array<{
193
+ occurred_at: string;
194
+ session_id: string;
195
+ query: string;
196
+ triggered: number;
197
+ source: string | null;
198
+ }>;
199
+
200
+ const recent_invocations = invocationRows.map((row) => ({
201
+ timestamp: row.occurred_at,
202
+ session_id: row.session_id,
203
+ query: row.query,
204
+ triggered: row.triggered === 1,
205
+ source: row.source,
206
+ }));
207
+
208
+ const evidence = queryEvolutionEvidence(db, skillName).map(mapEvidenceEntry);
209
+
210
+ const sessionsRow = db
211
+ .query(`SELECT COUNT(DISTINCT session_id) as c FROM skill_invocations WHERE skill_name = ?`)
212
+ .get(skillName) as { c: number };
213
+
214
+ return {
215
+ skill_name: skillName,
216
+ usage: {
217
+ total_checks: total,
218
+ triggered_count: triggered,
219
+ pass_rate: passRate,
220
+ },
221
+ recent_invocations,
222
+ evidence,
223
+ sessions_with_skill: sessionsRow.c,
224
+ };
225
+ }
226
+
227
+ export interface OverviewPaginationOptions {
228
+ telemetry_cursor?: PaginationCursor | null;
229
+ telemetry_limit?: number;
230
+ skills_cursor?: PaginationCursor | null;
231
+ skills_limit?: number;
232
+ }
233
+
234
+ export interface SkillReportPaginationOptions {
235
+ invocations_cursor?: PaginationCursor | null;
236
+ invocations_limit?: number;
237
+ }
238
+
239
+ export function getOverviewPayloadPaginated(
240
+ db: Database,
241
+ opts: OverviewPaginationOptions = {},
242
+ ): OverviewPaginatedPayload {
243
+ const telemetryLimit = opts.telemetry_limit ?? 1000;
244
+ const skillsLimit = opts.skills_limit ?? 2000;
245
+
246
+ const telemetry_page = paginateTelemetry(db, telemetryLimit, opts.telemetry_cursor ?? null);
247
+ const skills_page = paginateSkillInvocations(db, skillsLimit, opts.skills_cursor ?? null);
248
+
249
+ const evolutionRows = db
250
+ .query(
251
+ `SELECT timestamp, proposal_id, skill_name, action, details
252
+ FROM evolution_audit
253
+ ORDER BY timestamp DESC
254
+ LIMIT 500`,
255
+ )
256
+ .all() as Array<{
257
+ timestamp: string;
258
+ proposal_id: string;
259
+ skill_name: string | null;
260
+ action: string;
261
+ details: string;
262
+ }>;
263
+ const evolution = evolutionRows.map(mapOverviewEvolutionEntry);
264
+
265
+ const counts = db
266
+ .query(
267
+ `SELECT
268
+ (SELECT COUNT(*) FROM session_telemetry) as telemetry,
269
+ (SELECT COUNT(*) FROM skill_invocations) as skills,
270
+ (SELECT COUNT(*) FROM evolution_audit) as evolution,
271
+ (SELECT COUNT(*) FROM evolution_evidence) as evidence,
272
+ (SELECT COUNT(*) FROM sessions) as sessions,
273
+ (SELECT COUNT(*) FROM prompts) as prompts`,
274
+ )
275
+ .get() as {
276
+ telemetry: number;
277
+ skills: number;
278
+ evolution: number;
279
+ evidence: number;
280
+ sessions: number;
281
+ prompts: number;
282
+ };
283
+
284
+ const unmatchedRows = db
285
+ .query(
286
+ `SELECT si.occurred_at AS timestamp, si.session_id, si.query
287
+ FROM skill_invocations si
288
+ WHERE si.triggered = 0
289
+ AND NOT EXISTS (
290
+ SELECT 1 FROM skill_invocations si2
291
+ WHERE si2.query = si.query AND si2.triggered = 1
292
+ )
293
+ ORDER BY si.occurred_at DESC
294
+ LIMIT 500`,
295
+ )
296
+ .all() as Array<{ timestamp: string; session_id: string; query: string }>;
297
+
298
+ return {
299
+ telemetry_page,
300
+ skills_page,
301
+ evolution,
302
+ counts,
303
+ unmatched_queries: unmatchedRows,
304
+ pending_proposals: getPendingProposals(db),
305
+ active_sessions: getActiveSessionCount(db),
306
+ recent_activity: getRecentActivity(db),
307
+ };
308
+ }
309
+
310
+ export function getSkillReportPayloadPaginated(
311
+ db: Database,
312
+ skillName: string,
313
+ opts: SkillReportPaginationOptions = {},
314
+ ): SkillReportPaginatedPayload {
315
+ const invocationsLimit = opts.invocations_limit ?? 100;
316
+ const usageRow = db
317
+ .query(
318
+ `SELECT
319
+ COUNT(*) as total_checks,
320
+ SUM(CASE WHEN triggered = 1 THEN 1 ELSE 0 END) as triggered_count
321
+ FROM skill_invocations
322
+ WHERE skill_name = ?`,
323
+ )
324
+ .get(skillName) as { total_checks: number; triggered_count: number };
325
+
326
+ const total = usageRow.total_checks;
327
+ const triggered = usageRow.triggered_count;
328
+ const passRate = total > 0 ? triggered / total : 0;
329
+
330
+ const invocations_page = paginateSkillReportInvocations(
331
+ db,
332
+ skillName,
333
+ invocationsLimit,
334
+ opts.invocations_cursor ?? null,
335
+ );
336
+ const evidence = queryEvolutionEvidence(db, skillName).map(mapEvidenceEntry);
337
+
338
+ const sessionsRow = db
339
+ .query(`SELECT COUNT(DISTINCT session_id) as c FROM skill_invocations WHERE skill_name = ?`)
340
+ .get(skillName) as { c: number };
341
+
342
+ return {
343
+ skill_name: skillName,
344
+ usage: {
345
+ total_checks: total,
346
+ triggered_count: triggered,
347
+ pass_rate: passRate,
348
+ },
349
+ invocations_page,
350
+ evidence,
351
+ sessions_with_skill: sessionsRow.c,
352
+ };
353
+ }
354
+
355
+ function paginateTelemetry(
356
+ db: Database,
357
+ limit: number,
358
+ cursor: PaginationCursor | null,
359
+ ): PaginatedResult<TelemetryRecord> {
360
+ const fetchLimit = limit + 1;
361
+
362
+ let rows: Array<{
363
+ timestamp: string;
364
+ session_id: string;
365
+ skills_triggered_json: string | null;
366
+ errors_encountered: number;
367
+ total_tool_calls: number;
368
+ }>;
369
+
370
+ if (cursor) {
371
+ rows = db
372
+ .query(
373
+ `SELECT timestamp, session_id, skills_triggered_json, errors_encountered, total_tool_calls
374
+ FROM session_telemetry
375
+ WHERE (timestamp < ? OR (timestamp = ? AND session_id < ?))
376
+ ORDER BY timestamp DESC, session_id DESC
377
+ LIMIT ?`,
378
+ )
379
+ .all(cursor.timestamp, cursor.timestamp, String(cursor.id), fetchLimit) as typeof rows;
380
+ } else {
381
+ rows = db
382
+ .query(
383
+ `SELECT timestamp, session_id, skills_triggered_json, errors_encountered, total_tool_calls
384
+ FROM session_telemetry
385
+ ORDER BY timestamp DESC, session_id DESC
386
+ LIMIT ?`,
387
+ )
388
+ .all(fetchLimit) as typeof rows;
389
+ }
390
+
391
+ const hasMore = rows.length > limit;
392
+ const pageRows = hasMore ? rows.slice(0, limit) : rows;
393
+ const items: TelemetryRecord[] = pageRows.map((row) => ({
394
+ timestamp: row.timestamp,
395
+ session_id: row.session_id,
396
+ skills_triggered: safeParseJsonArray<string>(row.skills_triggered_json),
397
+ errors_encountered: row.errors_encountered,
398
+ total_tool_calls: row.total_tool_calls,
399
+ }));
400
+
401
+ const lastItem = pageRows[pageRows.length - 1];
402
+ const next_cursor: PaginationCursor | null =
403
+ hasMore && lastItem ? { timestamp: lastItem.timestamp, id: lastItem.session_id } : null;
404
+
405
+ return { items, next_cursor, has_more: hasMore };
406
+ }
407
+
408
+ function paginateSkillInvocations(
409
+ db: Database,
410
+ limit: number,
411
+ cursor: PaginationCursor | null,
412
+ ): PaginatedResult<SkillUsageRecord> {
413
+ const fetchLimit = limit + 1;
414
+
415
+ let rows: Array<{
416
+ occurred_at: string;
417
+ session_id: string;
418
+ skill_name: string;
419
+ skill_path: string;
420
+ query: string;
421
+ triggered: number;
422
+ source: string | null;
423
+ skill_invocation_id: string;
424
+ }>;
425
+
426
+ if (cursor) {
427
+ rows = db
428
+ .query(
429
+ `SELECT occurred_at, session_id, skill_name, skill_path, query, triggered, source, skill_invocation_id
430
+ FROM skill_invocations
431
+ WHERE (occurred_at < ? OR (occurred_at = ? AND skill_invocation_id < ?))
432
+ ORDER BY occurred_at DESC, skill_invocation_id DESC
433
+ LIMIT ?`,
434
+ )
435
+ .all(cursor.timestamp, cursor.timestamp, String(cursor.id), fetchLimit) as typeof rows;
436
+ } else {
437
+ rows = db
438
+ .query(
439
+ `SELECT occurred_at, session_id, skill_name, skill_path, query, triggered, source, skill_invocation_id
440
+ FROM skill_invocations
441
+ ORDER BY occurred_at DESC, skill_invocation_id DESC
442
+ LIMIT ?`,
443
+ )
444
+ .all(fetchLimit) as typeof rows;
445
+ }
446
+
447
+ const hasMore = rows.length > limit;
448
+ const pageRows = hasMore ? rows.slice(0, limit) : rows;
449
+ const items: SkillUsageRecord[] = pageRows.map((row) => ({
450
+ timestamp: row.occurred_at,
451
+ session_id: row.session_id,
452
+ skill_name: row.skill_name,
453
+ skill_path: row.skill_path,
454
+ query: row.query,
455
+ triggered: row.triggered === 1,
456
+ source: row.source,
457
+ }));
458
+
459
+ const lastRow = pageRows[pageRows.length - 1];
460
+ const next_cursor: PaginationCursor | null =
461
+ hasMore && lastRow ? { timestamp: lastRow.occurred_at, id: lastRow.skill_invocation_id } : null;
462
+
463
+ return { items, next_cursor, has_more: hasMore };
464
+ }
465
+
466
+ function paginateSkillReportInvocations(
467
+ db: Database,
468
+ skillName: string,
469
+ limit: number,
470
+ cursor: PaginationCursor | null,
471
+ ): PaginatedResult<{
472
+ timestamp: string;
473
+ session_id: string;
474
+ query: string;
475
+ triggered: boolean;
476
+ source: string | null;
477
+ }> {
478
+ const fetchLimit = limit + 1;
479
+
480
+ let rows: Array<{
481
+ occurred_at: string;
482
+ session_id: string;
483
+ query: string;
484
+ triggered: number;
485
+ source: string | null;
486
+ skill_invocation_id: string;
487
+ }>;
488
+
489
+ if (cursor) {
490
+ rows = db
491
+ .query(
492
+ `SELECT si.occurred_at, si.session_id, COALESCE(si.query, p.prompt_text) as query,
493
+ si.triggered, si.source, si.skill_invocation_id
494
+ FROM skill_invocations si
495
+ LEFT JOIN prompts p ON si.matched_prompt_id = p.prompt_id
496
+ WHERE si.skill_name = ?
497
+ AND (si.occurred_at < ? OR (si.occurred_at = ? AND si.skill_invocation_id < ?))
498
+ ORDER BY si.occurred_at DESC, si.skill_invocation_id DESC
499
+ LIMIT ?`,
500
+ )
501
+ .all(
502
+ skillName,
503
+ cursor.timestamp,
504
+ cursor.timestamp,
505
+ String(cursor.id),
506
+ fetchLimit,
507
+ ) as typeof rows;
508
+ } else {
509
+ rows = db
510
+ .query(
511
+ `SELECT si.occurred_at, si.session_id, COALESCE(si.query, p.prompt_text) as query,
512
+ si.triggered, si.source, si.skill_invocation_id
513
+ FROM skill_invocations si
514
+ LEFT JOIN prompts p ON si.matched_prompt_id = p.prompt_id
515
+ WHERE si.skill_name = ?
516
+ ORDER BY si.occurred_at DESC, si.skill_invocation_id DESC
517
+ LIMIT ?`,
518
+ )
519
+ .all(skillName, fetchLimit) as typeof rows;
520
+ }
521
+
522
+ const hasMore = rows.length > limit;
523
+ const pageRows = hasMore ? rows.slice(0, limit) : rows;
524
+ const items = pageRows.map((row) => ({
525
+ timestamp: row.occurred_at,
526
+ session_id: row.session_id,
527
+ query: row.query ?? "",
528
+ triggered: row.triggered === 1,
529
+ source: row.source,
530
+ }));
531
+
532
+ const lastRow = pageRows[pageRows.length - 1];
533
+ const next_cursor: PaginationCursor | null =
534
+ hasMore && lastRow ? { timestamp: lastRow.occurred_at, id: lastRow.skill_invocation_id } : null;
535
+
536
+ return { items, next_cursor, has_more: hasMore };
537
+ }
538
+
539
+ export function getSkillsList(
540
+ db: Database,
541
+ testingReadinessRows?: SkillTestingReadiness[],
542
+ ): SkillSummary[] {
543
+ const trustedRows = queryTrustedSkillObservationRows(db);
544
+ const bySkill = new Map<
545
+ string,
546
+ Array<{
547
+ skill_name: string;
548
+ session_id: string;
549
+ occurred_at: string | null;
550
+ triggered: number;
551
+ matched_prompt_id: string | null;
552
+ confidence: number | null;
553
+ }>
554
+ >();
555
+
556
+ for (const row of trustedRows) {
557
+ const base = {
558
+ skill_name: row.skill_name,
559
+ session_id: row.session_id,
560
+ occurred_at: row.occurred_at,
561
+ triggered: row.triggered,
562
+ matched_prompt_id: row.matched_prompt_id,
563
+ confidence: row.confidence,
564
+ };
565
+ const existing = bySkill.get(row.skill_name);
566
+ if (existing) existing.push(base);
567
+ else bySkill.set(row.skill_name, [base]);
568
+ }
569
+
570
+ const evidenceSkills = new Set(
571
+ (
572
+ db.query(`SELECT DISTINCT skill_name FROM evolution_evidence`).all() as Array<{
573
+ skill_name: string;
574
+ }>
575
+ ).map((row) => row.skill_name),
576
+ );
577
+
578
+ const skillScopeRows = db
579
+ .query(
580
+ `SELECT
581
+ si.skill_name,
582
+ COALESCE(
583
+ (SELECT s2.skill_scope FROM skill_invocations s2
584
+ WHERE s2.skill_name = si.skill_name AND s2.skill_scope IS NOT NULL
585
+ ORDER BY s2.occurred_at DESC LIMIT 1),
586
+ (SELECT su.skill_scope FROM skill_usage su
587
+ WHERE su.skill_name = si.skill_name AND su.skill_scope IS NOT NULL
588
+ ORDER BY su.timestamp DESC LIMIT 1)
589
+ ) as skill_scope
590
+ FROM skill_invocations si
591
+ GROUP BY si.skill_name`,
592
+ )
593
+ .all() as Array<{ skill_name: string; skill_scope: string | null }>;
594
+ const scopeBySkill = new Map(skillScopeRows.map((row) => [row.skill_name, row.skill_scope]));
595
+ const testingReadiness = testingReadinessRows ?? listSkillTestingReadiness(db);
596
+ const testingReadinessBySkill = new Map(
597
+ testingReadiness.map((row) => [row.skill_name, row] as const),
598
+ );
599
+ const knownSkills = new Set<string>(bySkill.keys());
600
+
601
+ return [...knownSkills]
602
+ .map((skillName) => {
603
+ const rows = bySkill.get(skillName) ?? [];
604
+ const totalChecks = rows.length;
605
+ const triggeredCount = rows.filter((row) => row.triggered === 1).length;
606
+ const uniqueSessions = new Set(rows.map((row) => row.session_id)).size;
607
+ const lastSeen =
608
+ rows
609
+ .map((row) => row.occurred_at)
610
+ .filter((value): value is string => value != null)
611
+ .sort((a, b) => b.localeCompare(a))[0] ?? null;
612
+ const withConfidence = rows.filter((row) => row.confidence != null);
613
+ const routingConfidence =
614
+ withConfidence.length > 0
615
+ ? withConfidence.reduce((sum, row) => sum + (row.confidence ?? 0), 0) /
616
+ withConfidence.length
617
+ : null;
618
+ const readiness = testingReadinessBySkill.get(skillName);
619
+ const fallbackScope =
620
+ readiness?.skill_path != null ? classifySkillPath(readiness.skill_path).skill_scope : null;
621
+
622
+ return {
623
+ skill_name: skillName,
624
+ skill_scope:
625
+ scopeBySkill.get(skillName) ??
626
+ (fallbackScope && fallbackScope !== "unknown" ? fallbackScope : null),
627
+ total_checks: totalChecks,
628
+ triggered_count: triggeredCount,
629
+ pass_rate: totalChecks > 0 ? triggeredCount / totalChecks : 0,
630
+ unique_sessions: uniqueSessions,
631
+ last_seen: lastSeen,
632
+ has_evidence: evidenceSkills.has(skillName),
633
+ routing_confidence: routingConfidence,
634
+ confidence_coverage: totalChecks > 0 ? withConfidence.length / totalChecks : 0,
635
+ testing_readiness: readiness,
636
+ };
637
+ })
638
+ .sort(
639
+ (a, b) =>
640
+ b.total_checks - a.total_checks ||
641
+ (b.last_seen ?? "").localeCompare(a.last_seen ?? "") ||
642
+ a.skill_name.localeCompare(b.skill_name),
643
+ );
644
+ }
645
+
646
+ export function getAnalyticsPayload(db: Database): AnalyticsResponse {
647
+ const trustedRows = queryTrustedSkillObservationRows(db);
648
+ const today = new Date();
649
+ const dateKey = (value: string | null): string | null => {
650
+ if (!value) return null;
651
+ const parsed = new Date(value);
652
+ return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString().slice(0, 10);
653
+ };
654
+ const cutoffDate = (days: number): string => {
655
+ const cutoff = new Date(today);
656
+ cutoff.setUTCDate(cutoff.getUTCDate() - days);
657
+ return cutoff.toISOString().slice(0, 10);
658
+ };
659
+
660
+ const passRateTrendByDate = new Map<string, { triggered: number; total: number }>();
661
+ for (const row of trustedRows) {
662
+ const occurredDate = dateKey(row.occurred_at);
663
+ if (!occurredDate || occurredDate < cutoffDate(90)) continue;
664
+ const counts = passRateTrendByDate.get(occurredDate) ?? { triggered: 0, total: 0 };
665
+ counts.total += 1;
666
+ if (row.triggered === 1) counts.triggered += 1;
667
+ passRateTrendByDate.set(occurredDate, counts);
668
+ }
669
+ const passRateTrendRows = [...passRateTrendByDate.entries()]
670
+ .map(([date, counts]) => ({
671
+ date,
672
+ pass_rate: counts.total > 0 ? counts.triggered / counts.total : 0,
673
+ total_checks: counts.total,
674
+ }))
675
+ .sort((a, b) => a.date.localeCompare(b.date));
676
+
677
+ const skillRankingMap = new Map<string, { triggered_count: number; total_checks: number }>();
678
+ for (const row of trustedRows) {
679
+ const counts = skillRankingMap.get(row.skill_name) ?? { triggered_count: 0, total_checks: 0 };
680
+ counts.total_checks += 1;
681
+ if (row.triggered === 1) counts.triggered_count += 1;
682
+ skillRankingMap.set(row.skill_name, counts);
683
+ }
684
+ const skillRankingRows = [...skillRankingMap.entries()]
685
+ .map(([skill_name, counts]) => ({
686
+ skill_name,
687
+ pass_rate: counts.total_checks > 0 ? counts.triggered_count / counts.total_checks : 0,
688
+ total_checks: counts.total_checks,
689
+ triggered_count: counts.triggered_count,
690
+ }))
691
+ .sort(
692
+ (a, b) =>
693
+ b.pass_rate - a.pass_rate ||
694
+ b.total_checks - a.total_checks ||
695
+ a.skill_name.localeCompare(b.skill_name),
696
+ );
697
+
698
+ const dailyActivityByDate = new Map<string, number>();
699
+ for (const row of trustedRows) {
700
+ const occurredDate = dateKey(row.occurred_at);
701
+ if (!occurredDate || occurredDate < cutoffDate(84)) continue;
702
+ dailyActivityByDate.set(occurredDate, (dailyActivityByDate.get(occurredDate) ?? 0) + 1);
703
+ }
704
+ const dailyActivityRows = [...dailyActivityByDate.entries()]
705
+ .map(([date, checks]) => ({ date, checks }))
706
+ .sort((a, b) => a.date.localeCompare(b.date));
707
+
708
+ const deployedRows = db
709
+ .query(
710
+ `SELECT ea.skill_name, ea.proposal_id, ea.timestamp as deployed_at
711
+ FROM evolution_audit ea
712
+ WHERE ea.action = 'deployed' AND ea.skill_name IS NOT NULL
713
+ ORDER BY ea.timestamp DESC`,
714
+ )
715
+ .all() as Array<{ skill_name: string; proposal_id: string; deployed_at: string }>;
716
+
717
+ const evolution_impact: AnalyticsResponse["evolution_impact"] = [];
718
+ for (const deploy of deployedRows) {
719
+ const beforeRows = trustedRows.filter(
720
+ (row) => row.skill_name === deploy.skill_name && (row.occurred_at ?? "") < deploy.deployed_at,
721
+ );
722
+ const afterRows = trustedRows.filter(
723
+ (row) =>
724
+ row.skill_name === deploy.skill_name && (row.occurred_at ?? "") >= deploy.deployed_at,
725
+ );
726
+
727
+ evolution_impact.push({
728
+ skill_name: deploy.skill_name,
729
+ proposal_id: deploy.proposal_id,
730
+ deployed_at: deploy.deployed_at,
731
+ pass_rate_before:
732
+ beforeRows.length > 0
733
+ ? beforeRows.filter((row) => row.triggered === 1).length / beforeRows.length
734
+ : 0,
735
+ pass_rate_after:
736
+ afterRows.length > 0
737
+ ? afterRows.filter((row) => row.triggered === 1).length / afterRows.length
738
+ : 0,
739
+ });
740
+ }
741
+
742
+ const totalEvolutionsRow = db
743
+ .query(`SELECT COUNT(*) as c FROM evolution_audit WHERE action = 'deployed'`)
744
+ .get() as { c: number } | null;
745
+ const checks30dRows = trustedRows.filter((row) => {
746
+ const occurredDate = dateKey(row.occurred_at);
747
+ return occurredDate != null && occurredDate >= cutoffDate(30);
748
+ });
749
+ const activeSkills30d = new Set(checks30dRows.map((row) => row.skill_name));
750
+
751
+ let avgImprovement = 0;
752
+ if (evolution_impact.length > 0) {
753
+ const totalImprovement = evolution_impact.reduce(
754
+ (sum, impact) => sum + (impact.pass_rate_after - impact.pass_rate_before),
755
+ 0,
756
+ );
757
+ avgImprovement = totalImprovement / evolution_impact.length;
758
+ }
759
+
760
+ return {
761
+ pass_rate_trend: passRateTrendRows.map((row) => ({
762
+ date: row.date,
763
+ pass_rate: row.pass_rate,
764
+ total_checks: row.total_checks,
765
+ })),
766
+ skill_rankings: skillRankingRows.map((row) => ({
767
+ skill_name: row.skill_name,
768
+ pass_rate: row.pass_rate,
769
+ total_checks: row.total_checks,
770
+ triggered_count: row.triggered_count,
771
+ })),
772
+ daily_activity: dailyActivityRows.map((row) => ({
773
+ date: row.date,
774
+ checks: row.checks,
775
+ })),
776
+ evolution_impact,
777
+ summary: {
778
+ total_evolutions: totalEvolutionsRow?.c ?? 0,
779
+ avg_improvement: avgImprovement,
780
+ total_checks_30d: checks30dRows.length,
781
+ active_skills: activeSkills30d.size,
782
+ },
783
+ };
784
+ }
785
+
786
+ export function getActiveSessionCount(db: Database): number {
787
+ const row = db
788
+ .query(
789
+ `SELECT COUNT(DISTINCT q.session_id) as count
790
+ FROM queries q
791
+ WHERE NOT EXISTS (
792
+ SELECT 1 FROM session_telemetry st WHERE st.session_id = q.session_id
793
+ )`,
794
+ )
795
+ .get() as { count: number };
796
+ return row.count;
797
+ }
798
+
799
+ export function getRecentActivity(db: Database, limit = 20): RecentActivityItem[] {
800
+ const rows = db
801
+ .query(
802
+ `SELECT occurred_at, session_id, skill_name, query, triggered
803
+ FROM skill_invocations
804
+ ORDER BY occurred_at DESC
805
+ LIMIT ?`,
806
+ )
807
+ .all(limit) as Array<{
808
+ occurred_at: string;
809
+ session_id: string;
810
+ skill_name: string;
811
+ query: string;
812
+ triggered: number;
813
+ }>;
814
+
815
+ if (rows.length === 0) return [];
816
+
817
+ const uniqueSessionIds = [...new Set(rows.map((row) => row.session_id))];
818
+ const placeholders = uniqueSessionIds.map(() => "?").join(",");
819
+ const completedRows = db
820
+ .query(
821
+ `SELECT DISTINCT session_id FROM session_telemetry WHERE session_id IN (${placeholders})`,
822
+ )
823
+ .all(...uniqueSessionIds) as Array<{ session_id: string }>;
824
+ const completedSessions = new Set(completedRows.map((row) => row.session_id));
825
+
826
+ return rows.map((row) => ({
827
+ timestamp: row.occurred_at,
828
+ session_id: row.session_id,
829
+ skill_name: row.skill_name,
830
+ query: row.query ?? "",
831
+ triggered: row.triggered === 1,
832
+ is_live: !completedSessions.has(row.session_id),
833
+ }));
834
+ }