@oh-my-pi/omp-stats 18.3.4 → 18.4.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 (192) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +13 -11
  3. package/build.ts +5 -49
  4. package/dist/client/index.css +1 -1
  5. package/dist/client/index.html +2 -2
  6. package/dist/client/index.js +105 -126
  7. package/dist/types/aggregator.d.ts +25 -22
  8. package/dist/types/client/api.d.ts +11 -7
  9. package/dist/types/client/app/LiveChip.d.ts +2 -0
  10. package/dist/types/client/app/Shell.d.ts +16 -0
  11. package/dist/types/client/app/ThemeToggle.d.ts +1 -0
  12. package/dist/types/client/app/nav.d.ts +18 -0
  13. package/dist/types/client/charts/BarList.d.ts +17 -0
  14. package/dist/types/client/charts/Chart.d.ts +49 -0
  15. package/dist/types/client/charts/Legend.d.ts +16 -0
  16. package/dist/types/client/charts/ShareBar.d.ts +11 -0
  17. package/dist/types/client/charts/Sparkline.d.ts +11 -0
  18. package/dist/types/client/charts/TimeChart.d.ts +9 -0
  19. package/dist/types/client/charts/index.d.ts +7 -0
  20. package/dist/types/client/charts/types.d.ts +25 -0
  21. package/dist/types/client/charts/useWidth.d.ts +3 -0
  22. package/dist/types/client/data/colors.d.ts +25 -0
  23. package/dist/types/client/data/formatters.d.ts +9 -1
  24. package/dist/types/client/data/live.d.ts +29 -0
  25. package/dist/types/client/data/query.d.ts +35 -0
  26. package/dist/types/client/data/range.d.ts +31 -0
  27. package/dist/types/client/data/series.d.ts +28 -0
  28. package/dist/types/client/data/useHashRoute.d.ts +18 -4
  29. package/dist/types/client/data/view-models.d.ts +111 -28
  30. package/dist/types/client/index.d.ts +0 -1
  31. package/dist/types/client/routes/CostsRoute.d.ts +2 -2
  32. package/dist/types/client/routes/ErrorsRoute.d.ts +2 -2
  33. package/dist/types/client/routes/FrustrationRoute.d.ts +7 -0
  34. package/dist/types/client/routes/GainRoute.d.ts +1 -2
  35. package/dist/types/client/routes/ModelsRoute.d.ts +2 -2
  36. package/dist/types/client/routes/OverviewRoute.d.ts +1 -2
  37. package/dist/types/client/routes/ProjectsRoute.d.ts +2 -2
  38. package/dist/types/client/routes/ProvidersRoute.d.ts +2 -2
  39. package/dist/types/client/routes/RequestsRoute.d.ts +1 -2
  40. package/dist/types/client/routes/ToolsRoute.d.ts +2 -2
  41. package/dist/types/client/routes/TracesRoute.d.ts +2 -2
  42. package/dist/types/client/routes/index.d.ts +1 -1
  43. package/dist/types/client/traces/AggregatesPanel.d.ts +4 -3
  44. package/dist/types/client/traces/SummaryStrip.d.ts +2 -2
  45. package/dist/types/client/traces/trace-colors.d.ts +18 -12
  46. package/dist/types/client/ui/Badge.d.ts +19 -0
  47. package/dist/types/client/ui/Card.d.ts +25 -0
  48. package/dist/types/client/ui/Drawer.d.ts +25 -0
  49. package/dist/types/client/ui/JsonBlock.d.ts +2 -2
  50. package/dist/types/client/ui/Modal.d.ts +23 -0
  51. package/dist/types/client/ui/RequestDrawer.d.ts +10 -1
  52. package/dist/types/client/ui/SearchInput.d.ts +8 -0
  53. package/dist/types/client/ui/Segmented.d.ts +15 -0
  54. package/dist/types/client/ui/Stat.d.ts +36 -0
  55. package/dist/types/client/ui/States.d.ts +41 -0
  56. package/dist/types/client/ui/Table.d.ts +48 -0
  57. package/dist/types/client/ui/index.d.ts +9 -10
  58. package/dist/types/db.d.ts +90 -106
  59. package/dist/types/frustration.d.ts +70 -0
  60. package/dist/types/index.d.ts +3 -1
  61. package/dist/types/live.d.ts +30 -0
  62. package/dist/types/rollup.d.ts +115 -0
  63. package/dist/types/server.d.ts +6 -1
  64. package/dist/types/shared-types.d.ts +116 -56
  65. package/dist/types/types.d.ts +4 -0
  66. package/dist/types/user-metrics.d.ts +8 -0
  67. package/package.json +7 -13
  68. package/src/aggregator.ts +276 -211
  69. package/src/client/App.tsx +38 -80
  70. package/src/client/api.ts +65 -7
  71. package/src/client/app/LiveChip.tsx +97 -0
  72. package/src/client/app/Shell.tsx +181 -0
  73. package/src/client/app/ThemeToggle.tsx +5 -2
  74. package/src/client/app/nav.ts +78 -0
  75. package/src/client/charts/BarList.tsx +48 -0
  76. package/src/client/charts/Chart.tsx +388 -0
  77. package/src/client/charts/Legend.tsx +61 -0
  78. package/src/client/charts/ShareBar.tsx +26 -0
  79. package/src/client/charts/Sparkline.tsx +38 -0
  80. package/src/client/charts/TimeChart.tsx +22 -0
  81. package/src/client/charts/index.ts +7 -0
  82. package/src/client/charts/types.ts +27 -0
  83. package/src/client/charts/useWidth.ts +19 -0
  84. package/src/client/data/colors.ts +45 -0
  85. package/src/client/data/formatters.ts +30 -9
  86. package/src/client/data/live.tsx +85 -0
  87. package/src/client/data/query.ts +166 -0
  88. package/src/client/data/range.ts +82 -0
  89. package/src/client/data/series.ts +85 -0
  90. package/src/client/data/useHashRoute.ts +41 -83
  91. package/src/client/data/view-models.ts +347 -140
  92. package/src/client/index.tsx +3 -6
  93. package/src/client/routes/CostsRoute.tsx +394 -222
  94. package/src/client/routes/ErrorsRoute.tsx +401 -98
  95. package/src/client/routes/FrustrationRoute.tsx +924 -0
  96. package/src/client/routes/GainRoute.tsx +209 -206
  97. package/src/client/routes/ModelsRoute.tsx +540 -431
  98. package/src/client/routes/OverviewRoute.tsx +283 -311
  99. package/src/client/routes/ProjectsRoute.tsx +298 -156
  100. package/src/client/routes/ProvidersRoute.tsx +948 -416
  101. package/src/client/routes/RequestsRoute.tsx +295 -107
  102. package/src/client/routes/ToolsRoute.tsx +495 -398
  103. package/src/client/routes/TracesRoute.tsx +136 -117
  104. package/src/client/routes/costs.css +24 -0
  105. package/src/client/routes/errors.css +60 -0
  106. package/src/client/routes/frustration.css +123 -0
  107. package/src/client/routes/index.ts +1 -1
  108. package/src/client/routes/models.css +43 -0
  109. package/src/client/routes/projects.css +6 -0
  110. package/src/client/routes/providers.css +28 -0
  111. package/src/client/routes/tools.css +14 -0
  112. package/src/client/styles.css +1396 -1299
  113. package/src/client/traces/AggregatesPanel.tsx +55 -74
  114. package/src/client/traces/Minimap.tsx +6 -15
  115. package/src/client/traces/SpanDrawer.tsx +128 -194
  116. package/src/client/traces/SummaryStrip.tsx +21 -24
  117. package/src/client/traces/TimelineCanvas.tsx +27 -44
  118. package/src/client/traces/TraceView.tsx +175 -130
  119. package/src/client/traces/TranscriptList.tsx +13 -41
  120. package/src/client/traces/trace-colors.ts +60 -53
  121. package/src/client/traces/traces.css +279 -0
  122. package/src/client/ui/Badge.tsx +29 -0
  123. package/src/client/ui/Card.tsx +61 -0
  124. package/src/client/ui/Drawer.tsx +93 -0
  125. package/src/client/ui/JsonBlock.tsx +26 -44
  126. package/src/client/ui/Modal.tsx +106 -0
  127. package/src/client/ui/RequestDrawer.tsx +163 -188
  128. package/src/client/ui/SearchInput.tsx +25 -0
  129. package/src/client/ui/Segmented.tsx +61 -0
  130. package/src/client/ui/Stat.tsx +104 -0
  131. package/src/client/ui/States.tsx +81 -0
  132. package/src/client/ui/Table.tsx +229 -0
  133. package/src/client/ui/index.ts +9 -10
  134. package/src/client/ui/modal.css +54 -0
  135. package/src/client/ui/request-drawer.css +40 -0
  136. package/src/db.ts +379 -983
  137. package/src/frustration.ts +473 -0
  138. package/src/index.ts +22 -22
  139. package/src/live.ts +259 -0
  140. package/src/parser.ts +55 -66
  141. package/src/rollup.ts +951 -0
  142. package/src/server.ts +123 -40
  143. package/src/shared-types.ts +120 -56
  144. package/src/trace.ts +14 -7
  145. package/src/types.ts +4 -0
  146. package/src/user-metrics.ts +13 -0
  147. package/dist/client/styles.css +0 -1976
  148. package/dist/types/client/app/AppLayout.d.ts +0 -16
  149. package/dist/types/client/app/NavRail.d.ts +0 -7
  150. package/dist/types/client/app/RangeControl.d.ts +0 -7
  151. package/dist/types/client/app/SyncButton.d.ts +0 -14
  152. package/dist/types/client/app/TopBar.d.ts +0 -15
  153. package/dist/types/client/app/routes.d.ts +0 -12
  154. package/dist/types/client/components/AgentTokenShare.d.ts +0 -5
  155. package/dist/types/client/components/chart-shared.d.ts +0 -178
  156. package/dist/types/client/components/models-table-shared.d.ts +0 -175
  157. package/dist/types/client/components/range-meta.d.ts +0 -21
  158. package/dist/types/client/data/charts.d.ts +0 -1
  159. package/dist/types/client/data/useResource.d.ts +0 -13
  160. package/dist/types/client/routes/BehaviorRoute.d.ts +0 -7
  161. package/dist/types/client/ui/AsyncBoundary.d.ts +0 -12
  162. package/dist/types/client/ui/DataTable.d.ts +0 -17
  163. package/dist/types/client/ui/EmptyState.d.ts +0 -7
  164. package/dist/types/client/ui/ErrorState.d.ts +0 -6
  165. package/dist/types/client/ui/MetricCluster.d.ts +0 -5
  166. package/dist/types/client/ui/Panel.d.ts +0 -7
  167. package/dist/types/client/ui/SegmentedControl.d.ts +0 -12
  168. package/dist/types/client/ui/Skeleton.d.ts +0 -8
  169. package/dist/types/client/ui/StatusPill.d.ts +0 -7
  170. package/src/client/app/AppLayout.tsx +0 -93
  171. package/src/client/app/NavRail.tsx +0 -44
  172. package/src/client/app/RangeControl.tsx +0 -39
  173. package/src/client/app/SyncButton.tsx +0 -75
  174. package/src/client/app/TopBar.tsx +0 -73
  175. package/src/client/app/routes.ts +0 -93
  176. package/src/client/components/AgentTokenShare.tsx +0 -68
  177. package/src/client/components/chart-shared.tsx +0 -273
  178. package/src/client/components/models-table-shared.tsx +0 -255
  179. package/src/client/components/range-meta.ts +0 -73
  180. package/src/client/data/charts.ts +0 -14
  181. package/src/client/data/useResource.ts +0 -154
  182. package/src/client/routes/BehaviorRoute.tsx +0 -623
  183. package/src/client/ui/AsyncBoundary.tsx +0 -54
  184. package/src/client/ui/DataTable.tsx +0 -122
  185. package/src/client/ui/EmptyState.tsx +0 -16
  186. package/src/client/ui/ErrorState.tsx +0 -25
  187. package/src/client/ui/MetricCluster.tsx +0 -92
  188. package/src/client/ui/Panel.tsx +0 -24
  189. package/src/client/ui/SegmentedControl.tsx +0 -36
  190. package/src/client/ui/Skeleton.tsx +0 -17
  191. package/src/client/ui/StatusPill.tsx +0 -15
  192. package/tailwind.config.js +0 -40
@@ -1,11 +1,10 @@
1
- export * from "./AsyncBoundary.js";
2
- export * from "./DataTable.js";
3
- export * from "./EmptyState.js";
4
- export * from "./ErrorState.js";
1
+ export * from "./Badge.js";
2
+ export * from "./Card.js";
3
+ export * from "./Drawer.js";
5
4
  export * from "./JsonBlock.js";
6
- export * from "./MetricCluster.js";
7
- export * from "./Panel.js";
8
- export * from "./RequestDrawer.js";
9
- export * from "./SegmentedControl.js";
10
- export * from "./Skeleton.js";
11
- export * from "./StatusPill.js";
5
+ export * from "./Modal.js";
6
+ export * from "./SearchInput.js";
7
+ export * from "./Segmented.js";
8
+ export * from "./Stat.js";
9
+ export * from "./States.js";
10
+ export * from "./Table.js";
@@ -1,26 +1,58 @@
1
1
  import { Database } from "bun:sqlite";
2
2
  import { type ParseSessionResult, type SessionParserState } from "./parser.js";
3
- import type { AgentTypeStats, AggregatedStats, BehaviorModelStats, BehaviorOverallStats, BehaviorTimeSeriesPoint, CostTimeSeriesPoint, DailyActivityPoint, FolderStats, MessageStats, MessageStatsInput, ModelPerformancePoint, ModelStats, ModelTimeSeriesPoint, ProviderAggregate, ProviderHourlyPoint, ProviderTimeSeriesPoint, TimeSeriesPoint, ToolCallStats, ToolModelStats, ToolResultLink, ToolTimeSeriesPoint, ToolUsageStats, UserMessageLink, UserMessageStats } from "./types.js";
3
+ import type { DailyActivityPoint, FrustrationCounts, MessageStats, MessageStatsInput, ToolCallStats, ToolResultLink, UserMessageLink, UserMessageStats } from "./types.js";
4
+ /**
5
+ * Predicate counting one stored request as "unpriced" — the public rate card
6
+ * has no charge for it, so its zero is unknown spend rather than free usage.
7
+ * Two shapes store that zero:
8
+ * - `xai-oauth` bills through the SuperGrok subscription, so ingestion
9
+ * deliberately records no per-request price;
10
+ * - `cost_unpriced = 1`, set by `insertMessageStats` when `resolveStoredCost`
11
+ * refuses to price a scheduled (time-based) card whose entry carried no
12
+ * recoverable request timestamp — the epoch would silently become the
13
+ * tariff. Such a row keeps its real tokens and its zero until a re-parse
14
+ * recovers the time.
15
+ *
16
+ * Nothing else sets the marker: an explicit recorded zero, a free flat card,
17
+ * and a model with no catalog card at all keep `cost_unpriced = 0` even at the
18
+ * parser's timestamp sentinel, because their zero is a real price.
19
+ * `prefix` qualifies the columns for queries that alias `messages`.
20
+ */
21
+ export declare function unpricedRequestSql(prefix?: string): string;
22
+ /** The open stats database, or null before {@link initDb}. Used by the rollup query layer. */
23
+ export declare function currentDb(): Database | null;
4
24
  /**
5
25
  * Initialize the database and create tables.
6
26
  */
7
27
  export declare function initDb(): Promise<Database>;
8
28
  /** Whether the catalog prices this model on a time-based (scheduled) card. */
9
29
  export declare function isScheduledCatalogModel(provider: string, modelId: string): boolean;
10
- /**
11
- * Get the stored offset for a session file.
12
- */
13
- export declare function getFileOffset(sessionFile: string): {
30
+ /** Persisted transcript identity and parser cursor used to resume stats ingestion. */
31
+ export interface FileOffset {
14
32
  offset: number;
15
33
  lastModified: number;
16
34
  parserState?: SessionParserState;
17
- } | null;
35
+ }
36
+ /** Read one persisted cursor for callers inspecting an individual transcript. */
37
+ export declare function getFileOffset(sessionFile: string): FileOffset | null;
38
+ /** Read a bounded sync work set's cursors with one SQLite snapshot instead of one lookup per file. */
39
+ export declare function getFileOffsets(sessionFiles: string[]): Map<string, FileOffset>;
18
40
  /**
19
41
  * Update the stored offset for a session file.
20
42
  */
21
43
  export declare function setFileOffset(sessionFile: string, offset: number, lastModified: number, parserState?: SessionParserState): void;
22
- export declare function applySessionParseResult(sessionFile: string, result: ParseSessionResult, rebuild?: boolean): {
44
+ /** Parsed transcript queued by the sync loop for an atomic database batch. */
45
+ export interface ParsedSession {
46
+ sessionFile: string;
47
+ result: ParseSessionResult;
48
+ rebuild: boolean;
49
+ /** Recover missing rows from a full-history scan without rewriting unchanged records. */
50
+ replay: boolean;
51
+ }
52
+ /** Commit distinct transcripts' rows, reconciliation state, and cursors together; failure rolls back the batch. */
53
+ export declare function applySessionParseResults(sessions: ParsedSession[]): {
23
54
  processed: number;
55
+ files: number;
24
56
  reconcile: boolean;
25
57
  };
26
58
  export declare function prepareSessionSync(): boolean;
@@ -41,54 +73,7 @@ export declare function completeSessionSync(reconcile: boolean): void;
41
73
  * stored cost (orchestration-aware) and keeps `premium_requests` monotonic, so
42
74
  * a forced re-parse repairs historical `premium_requests` and cost fix-ups.
43
75
  */
44
- export declare function insertMessageStats(stats: MessageStatsInput[]): number;
45
- /**
46
- * Get overall aggregated stats.
47
- */
48
- export declare function getOverallStats(cutoff?: number): AggregatedStats;
49
- /**
50
- * Get stats grouped by model.
51
- */
52
- export declare function getStatsByModel(cutoff?: number): ModelStats[];
53
- /**
54
- * Get stats grouped by folder.
55
- */
56
- export declare function getStatsByFolder(cutoff?: number): FolderStats[];
57
- /**
58
- * Get token usage grouped by agent type (main agent, task subagents, advisor).
59
- * Token columns are explicit so the dashboard's share denominator matches the
60
- * counts it renders. Rows missing `agent_type` (defensive) fall back to "main".
61
- */
62
- export declare function getStatsByAgentType(cutoff?: number): AgentTypeStats[];
63
- /**
64
- * Get time series data.
65
- */
66
- export declare function getTimeSeries(hours?: number, cutoff?: number | null, bucketMs?: number): TimeSeriesPoint[];
67
- /**
68
- * Get daily performance time series data for the last N days.
69
- */
70
- /**
71
- * Get daily model usage time series data for the last N days.
72
- */
73
- export declare function getModelTimeSeries(days?: number, cutoff?: number | null, bucketMs?: number): ModelTimeSeriesPoint[];
74
- /**
75
- * Get request/token/cost totals grouped by provider.
76
- */
77
- export declare function getStatsByProvider(cutoff?: number | null): ProviderAggregate[];
78
- /**
79
- * Get token burn grouped by provider and local hour of day (0-23).
80
- * Hours use the server's timezone — the dashboard is a localhost tool, so
81
- * server-local and viewer-local time coincide.
82
- */
83
- export declare function getProviderHourlyBurn(cutoff?: number | null): ProviderHourlyPoint[];
84
- /**
85
- * Get token/cost time series grouped by provider (bucketed like the model series).
86
- */
87
- export declare function getProviderTimeSeries(days?: number, cutoff?: number | null, bucketMs?: number): ProviderTimeSeriesPoint[];
88
- /**
89
- * Get daily model performance time series data for the last N days.
90
- */
91
- export declare function getModelPerformanceSeries(days?: number, cutoff?: number | null, bucketMs?: number): ModelPerformancePoint[];
76
+ export declare function insertMessageStats(stats: Iterable<MessageStatsInput>): number;
92
77
  /**
93
78
  * Get total message count.
94
79
  */
@@ -100,26 +85,6 @@ export declare function closeDb(): void;
100
85
  export declare function getRecentRequests(limit?: number): MessageStats[];
101
86
  export declare function getRecentErrors(limit?: number, cutoff?: number | null): MessageStats[];
102
87
  export declare function getMessageById(id: number): MessageStats | null;
103
- /** Per-transcript-file rollup for the Traces session list. */
104
- export interface SessionRollupRow {
105
- sessionFile: string;
106
- requests: number;
107
- startedAt: number;
108
- endedAt: number;
109
- totalTokens: number;
110
- costTotal: number;
111
- unpricedRequests: number;
112
- /** Comma-joined DISTINCT models. */
113
- models: string;
114
- }
115
- /** Aggregate every synced transcript file into one row (subagents unfolded). */
116
- export declare function getSessionRollups(): SessionRollupRow[];
117
- /** Tool-call counts keyed by transcript file, for the Traces session list. */
118
- export declare function getToolCallCountsBySession(): Map<string, number>;
119
- /**
120
- * Get daily cost time series data for the last N days, broken down by model.
121
- */
122
- export declare function getCostTimeSeries(days?: number, cutoff?: number | null): CostTimeSeriesPoint[];
123
88
  /**
124
89
  * Per-local-day activity aggregates for the last `days` days, oldest first.
125
90
  * Self-initializing (opens the stats DB on first use) so the coding-agent TUI
@@ -138,30 +103,61 @@ export declare function markSessionBackfillsComplete(): void;
138
103
  * copy user entries verbatim into the child JSONL, so the same
139
104
  * `(entry_id, timestamp)` must not land twice across different session files.
140
105
  */
141
- export declare function insertUserMessageStats(stats: UserMessageStats[]): number;
106
+ export declare function insertUserMessageStats(stats: Iterable<UserMessageStats>): number;
142
107
  /**
143
108
  * Backfill the responding `model`/`provider` on user-message rows that were
144
- * persisted before their assistant reply was parsed (a side effect of
145
- * incremental `fromOffset` syncing: the `userByEntryId` map in
146
- * `parseSessionFile` only spans a single pass). Each row is updated at most
147
- * once because the `model IS NULL` guard short-circuits subsequent passes.
109
+ * persisted before their assistant reply was parsed by an incremental tail
110
+ * read. Each row is updated at most once because the `model IS NULL` guard
111
+ * short-circuits subsequent passes.
148
112
  *
149
113
  * Returns the number of rows actually updated.
150
114
  */
151
- export declare function updateUserMessageLinks(links: UserMessageLink[]): number;
152
- /**
153
- * Daily behavioral time series, grouped by responding model+provider.
154
- */
155
- export declare function getBehaviorTimeSeries(cutoff?: number | null): BehaviorTimeSeriesPoint[];
156
- /**
157
- * Overall behavioral totals across the cutoff window.
158
- */
159
- export declare function getBehaviorOverall(cutoff?: number | null): BehaviorOverallStats;
115
+ export declare function updateUserMessageLinks(links: Iterable<UserMessageLink>): number;
116
+ /** Frustration tallies of one responding (model, provider) pair, before identity merging. */
117
+ export interface FrustrationModelRow extends FrustrationCounts {
118
+ model: string;
119
+ provider: string | null;
120
+ /** Earliest message timestamp (ms) in range. */
121
+ firstSeen: number;
122
+ }
123
+ /** One unique unjudged prose text: identical messages share a hash and one verdict. */
124
+ export interface PendingProse {
125
+ hash: string;
126
+ prose: string;
127
+ }
128
+ /** Cached judge verdict for one prose hash. */
129
+ export interface FrustrationVerdict {
130
+ proseHash: string;
131
+ /** P(clearly annoyed) + P(angry) on the `annoyed` score question. */
132
+ pAnnoyed: number;
133
+ /** P(angry, hostile, or swearing). */
134
+ pAngry: number;
135
+ /** Most likely `target` choice: `assistant`, `other`, or `none`. */
136
+ target: string;
137
+ /** `provider/model` that produced the verdict. */
138
+ judge: string;
139
+ judgedAt: number;
140
+ }
141
+ /** Frustration tallies over every user message with prose in range, linked to a model or not. */
142
+ export declare function getFrustrationOverall(cutoff?: number | null): FrustrationCounts;
143
+ /**
144
+ * Frustration tallies per responding (model, provider) over the range. User
145
+ * messages that never got a reply (null model) are only in the overall tally.
146
+ */
147
+ export declare function getFrustrationByModel(cutoff?: number | null): FrustrationModelRow[];
148
+ /** Unique prose texts in range that have no cached verdict yet. */
149
+ export declare function getPendingFrustrationProse(cutoff?: number | null): PendingProse[];
150
+ /** Count and total characters of {@link getPendingFrustrationProse} without loading the text. */
151
+ export declare function getPendingFrustrationTotals(cutoff?: number | null): {
152
+ messages: number;
153
+ chars: number;
154
+ };
160
155
  /**
161
- * Per-model behavioral totals over the cutoff window. "Unknown" represents
162
- * user messages that never received an assistant reply.
156
+ * Cache (or replace) verdicts, all in one transaction: a judge run lands
157
+ * dozens per second, and a commit per verdict would contend with ingest for
158
+ * the write lock every time.
163
159
  */
164
- export declare function getBehaviorByModel(cutoff?: number | null): BehaviorModelStats[];
160
+ export declare function upsertFrustrationVerdicts(verdicts: readonly FrustrationVerdict[]): void;
165
161
  /**
166
162
  * Insert tool-call rows. Idempotent via UNIQUE(session_file, tool_call_id);
167
163
  * the `WHERE NOT EXISTS` guard mirrors {@link insertMessageStats}: forked
@@ -171,7 +167,7 @@ export declare function getBehaviorByModel(cutoff?: number | null): BehaviorMode
171
167
  * identity, not the call id alone — provider call ids are not a global
172
168
  * namespace across unrelated sessions.
173
169
  */
174
- export declare function insertToolCalls(calls: ToolCallStats[]): number;
170
+ export declare function insertToolCalls(calls: Iterable<ToolCallStats>): number;
175
171
  /**
176
172
  * Attach result size / error flag to persisted tool-call rows. Results can
177
173
  * land in a later incremental sync pass than the call that produced them, so
@@ -179,16 +175,4 @@ export declare function insertToolCalls(calls: ToolCallStats[]): number;
179
175
  * guard makes re-syncs idempotent; rows skipped by the fork guard simply
180
176
  * never match.
181
177
  */
182
- export declare function updateToolResults(links: ToolResultLink[]): number;
183
- /**
184
- * Get tool usage aggregated by tool name.
185
- */
186
- export declare function getToolStats(cutoff?: number): ToolUsageStats[];
187
- /**
188
- * Get tool usage aggregated by (tool, model, provider).
189
- */
190
- export declare function getToolStatsByModel(cutoff?: number): ToolModelStats[];
191
- /**
192
- * Get tool-call time series (one point per bucket per tool).
193
- */
194
- export declare function getToolTimeSeries(days?: number, cutoff?: number | null, bucketMs?: number): ToolTimeSeriesPoint[];
178
+ export declare function updateToolResults(links: Iterable<ToolResultLink>): number;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Frustration dashboard: per-model share of user messages that sound annoyed,
3
+ * annoyed at the assistant, and angry at the assistant.
4
+ *
5
+ * Each message is classified by its cached judge verdict when one exists:
6
+ * annoyed = P(level 2) + P(level 3) ≥ 0.5 on {@link FRUSTRATION_QUESTIONS}.annoyed
7
+ * atAssistant = annoyed && target = `assistant`
8
+ * angry = atAssistant && P(level 3) ≥ 0.5
9
+ * and otherwise by the regex signals stored at ingest (`user-metrics.ts`):
10
+ * annoyed = yelling + profanity + anguish + negation + repetition + blame > 0
11
+ * atAssistant = negation + repetition + blame > 0
12
+ * angry = atAssistant && (profanity > 0 || yelling > 0)
13
+ * The SQL implementing both lives in `db.ts` (`FRUSTRATION_COUNTS_SQL`).
14
+ *
15
+ * Verdicts come from a single process-wide judge run over the unjudged prose,
16
+ * using the judge the omp host registered through `startServer`. Standalone
17
+ * `omp-stats` has none and only shows the regex fallback.
18
+ */
19
+ import type { ChoiceQuestion, Judge, Model, ScoreQuestion } from "@oh-my-pi/pi-ai";
20
+ import { type FrustrationModelRow } from "./db.js";
21
+ import type { FrustrationDashboardStats, FrustrationEstimate, FrustrationJobStatus, FrustrationModelStats } from "./shared-types.js";
22
+ /** Judge supplied by the omp host process; `primaryModel` prices the pre-run estimate. */
23
+ export interface StatsJudge extends Judge {
24
+ primaryModel(): Model | undefined;
25
+ }
26
+ /** Lazily resolves the host judge on first estimate/run so `omp stats` startup stays fast. */
27
+ export type StatsJudgeProvider = () => Promise<StatsJudge>;
28
+ /** Where the user's annoyance is aimed. */
29
+ export type FrustrationTarget = "assistant" | "other" | "none";
30
+ /**
31
+ * The two questions asked about every prose text, in ONE judge request.
32
+ * The cost constants below were measured with exactly these strings; changing
33
+ * them invalidates both the estimate and every cached verdict's meaning.
34
+ */
35
+ export declare const FRUSTRATION_QUESTIONS: {
36
+ annoyed: ScoreQuestion;
37
+ target: ChoiceQuestion<FrustrationTarget>;
38
+ };
39
+ /** Register (or replace) the host judge. Called by `startServer`. */
40
+ export declare function setStatsJudgeProvider(provider: StatsJudgeProvider | undefined): void;
41
+ /** Snapshot of the current run's progress. */
42
+ export declare function getFrustrationJobStatus(): FrustrationJobStatus;
43
+ /**
44
+ * Merge per-(model, provider) rows into catalog identities (`class/family/revision`)
45
+ * so provider and spelling variants share a row. Classified rows sort by class,
46
+ * then revision ascending, then family; unclassified rows follow by first use.
47
+ */
48
+ export declare function mergeFrustrationRows(rows: readonly FrustrationModelRow[]): FrustrationModelStats[];
49
+ /** Payload of `GET /api/stats/frustration`. */
50
+ export declare function getFrustrationDashboardStats(range?: string | null): Promise<FrustrationDashboardStats>;
51
+ /** Pre-run cost quote for judging every unjudged prose text in range. */
52
+ export declare function estimateFrustrationRun(range?: string | null): Promise<FrustrationEstimate>;
53
+ /** Outcome of {@link startFrustrationRun}: the new job, or the HTTP status and reason it was refused. */
54
+ export type StartFrustrationRunResult = {
55
+ started: true;
56
+ job: FrustrationJobStatus;
57
+ /** Settles once every worker has stopped (done, cancelled, or failed); never rejects. */
58
+ finished: Promise<void>;
59
+ } | {
60
+ started: false;
61
+ status: 409 | 503;
62
+ error: string;
63
+ };
64
+ /**
65
+ * Start the process-wide judge run over the unique unjudged prose in range.
66
+ * Returns immediately after queueing; progress is read via {@link getFrustrationJobStatus}.
67
+ */
68
+ export declare function startFrustrationRun(range?: string | null): Promise<StartFrustrationRunResult>;
69
+ /** Abort the active run; in-flight judge requests are cancelled and no new text is taken. */
70
+ export declare function cancelFrustrationRun(): FrustrationJobStatus;
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env bun
2
2
  export { getDashboardStats, getToolDashboardStats, getTotalMessageCount, type SyncOptions, type SyncProgress, smokeTestSyncWorker, syncAllSessions, } from "./aggregator.js";
3
3
  export { closeDb } from "./db.js";
4
+ export { refreshRollups } from "./rollup.js";
5
+ export type { StatsJudge, StatsJudgeProvider } from "./frustration.js";
4
6
  export { getGainDashboardStats } from "./gain-aggregator.js";
5
- export { formatStatsDashboardUrl, startServer } from "./server.js";
7
+ export { formatStatsDashboardUrl, type StartServerOptions, startServer } from "./server.js";
6
8
  export type { GainDashboardStats, GainSource, GainSourceTotals, GainTimeSeriesPoint } from "./shared-types.js";
7
9
  export type { AggregatedStats, DashboardStats, FolderStats, MessageStats, ModelPerformancePoint, ModelStats, ModelTimeSeriesPoint, TimeSeriesPoint, ToolDashboardStats, ToolModelStats, ToolTimeSeriesPoint, ToolUsageStats, } from "./types.js";
8
10
  /** Parsed arguments for the standalone `omp-stats` entry point. */
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Live ingest for a running dashboard: background session sync, rollup
3
+ * refresh, and a status stream the client subscribes to (`/api/events`).
4
+ *
5
+ * The dashboard never waits for ingest. {@link StatsLive.start} kicks a full
6
+ * sync in the background and watches the sessions directory; changed
7
+ * transcripts are re-synced (only those files) shortly after they are written.
8
+ * Every committed batch bumps {@link LiveStatus.version} (throttled), so open
9
+ * pages refetch and fill in while parsing is still running.
10
+ */
11
+ import type { LiveStatus } from "./shared-types.js";
12
+ type Listener = (status: LiveStatus) => void;
13
+ /** One per process; owned by the dashboard server (see `startServer`). */
14
+ export declare class StatsLive {
15
+ #private;
16
+ status(): LiveStatus;
17
+ /** Receive every status change; returns the unsubscribe function. */
18
+ subscribe(listener: Listener): () => void;
19
+ /** Begin background ingest: an immediate full sync, the transcript watcher, and periodic resyncs. Idempotent. */
20
+ start(): void;
21
+ stop(): void;
22
+ /**
23
+ * Queue a sync of `files`, or of every session when omitted. Requests made
24
+ * while a sync runs coalesce into one follow-up run.
25
+ */
26
+ requestSync(files?: readonly string[]): void;
27
+ }
28
+ /** The process-wide live hub. */
29
+ export declare function statsLive(): StatsLive;
30
+ export {};
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Hourly rollups of `messages` and `tool_calls`, and every range query the
3
+ * dashboard runs over them.
4
+ *
5
+ * The raw tables hold millions of rows; grouping them per request made every
6
+ * range switch cost seconds. Instead, `message_rollup` / `tool_rollup` keep one
7
+ * row per (hour, dimensions) with additive sums, so a 90-day query reads a few
8
+ * thousand rows.
9
+ *
10
+ * Freshness is trigger-driven: any INSERT/UPDATE/DELETE on the raw tables — from
11
+ * this process, a sync worker, or another omp process sharing the database —
12
+ * records the touched hour in `rollup_dirty`. {@link refreshRollups} recomputes
13
+ * dirty hours newest-first in short immediate transactions.
14
+ *
15
+ * Reads never wait for a refresh and are exact: each query unions
16
+ * - clean rollup hours inside the range,
17
+ * - raw rows for the partial hour at the range start, and
18
+ * - raw rows for dirty hours (while few are dirty),
19
+ * so live ingest shows up before its hour is re-rolled. While the initial build
20
+ * (or a mass re-ingest) leaves many hours dirty, reads use the rollup rows as
21
+ * they stand (stale-but-present; not-yet-built hours are missing) and
22
+ * {@link getRollupStatus} reports the backlog so the UI can show progress.
23
+ *
24
+ * The 1h range needs 5-minute buckets, so it aggregates raw rows directly
25
+ * (an hour of data is small and indexed by timestamp).
26
+ */
27
+ import type { Database } from "bun:sqlite";
28
+ import type { AgentTypeStats, AggregatedStats, CostTimeSeriesPoint, FolderStats, ModelPerformancePoint, ModelStats, ModelTimeSeriesPoint, ProviderAggregate, ProviderHourlyPoint, ProviderTimeSeriesPoint, TimeSeriesPoint, ToolModelStats, ToolTimeSeriesPoint, ToolUsageStats } from "./types.js";
29
+ /**
30
+ * Create (or rebuild after a version bump) the rollup tables and the triggers
31
+ * that mark hours dirty. Called from `initDb` after the raw tables exist.
32
+ */
33
+ export declare function ensureRollupSchema(database: Database): void;
34
+ /** Backlog of hours and transcripts whose rollups are stale. */
35
+ export interface RollupStatus {
36
+ dirtyHours: number;
37
+ dirtySessions: number;
38
+ }
39
+ export declare function getRollupStatus(): RollupStatus;
40
+ /**
41
+ * Recompute up to `limit` dirty hours, newest first, in one immediate
42
+ * transaction (so no writer can dirty an hour between its read and its
43
+ * dirty-mark removal). Returns the number of hours refreshed.
44
+ */
45
+ export declare function refreshRollupBatch(limit: number): number;
46
+ export interface RefreshOptions {
47
+ /** Called after each committed batch with the remaining backlog. */
48
+ onProgress?: (remaining: number) => void;
49
+ /** Stop early (between batches). */
50
+ signal?: AbortSignal;
51
+ }
52
+ /**
53
+ * Drain the dirty-hour backlog in small transactions, yielding to the event
54
+ * loop between batches so a host process (TUI, dashboard server) stays
55
+ * responsive during the initial build. Batch size adapts so each transaction
56
+ * takes about {@link REFRESH_TARGET_MS}; busy hours shrink it to one.
57
+ */
58
+ export declare function refreshRollups(opts?: RefreshOptions): Promise<void>;
59
+ /** A range query's time window. `cutoff` null = all time. */
60
+ export interface RangeWindow {
61
+ cutoff: number | null;
62
+ /** Bucket size for time series (5 minutes, an hour, or a day). */
63
+ bucketMs: number;
64
+ }
65
+ /** Overall request/token/cost aggregate since `cutoff` (all time when null). */
66
+ export declare function getOverallStats(cutoff?: number | null): AggregatedStats;
67
+ /** Aggregates per (model, provider), busiest first. */
68
+ export declare function getStatsByModel(cutoff?: number | null): ModelStats[];
69
+ /** Aggregates per project folder, busiest first; at most `limit` folders. */
70
+ export declare function getStatsByFolder(cutoff?: number | null, limit?: number): FolderStats[];
71
+ /** Token usage per agent type (main, subagent, advisor). */
72
+ export declare function getStatsByAgentType(cutoff?: number | null): AgentTypeStats[];
73
+ /** Requests, errors, tokens and cost per bucket. */
74
+ export declare function getTimeSeries({ cutoff, bucketMs }: RangeWindow): TimeSeriesPoint[];
75
+ /** Requests per bucket per (model, provider). */
76
+ export declare function getModelTimeSeries({ cutoff, bucketMs }: RangeWindow): ModelTimeSeriesPoint[];
77
+ /** Average TTFT and output tokens/s per bucket per (model, provider). */
78
+ export declare function getModelPerformanceSeries({ cutoff, bucketMs }: RangeWindow): ModelPerformancePoint[];
79
+ /** Daily cost per (model, provider) with the per-component split. */
80
+ export declare function getCostTimeSeries(cutoff?: number | null): CostTimeSeriesPoint[];
81
+ /** Request/token/cost totals per provider, biggest token burn first. */
82
+ export declare function getStatsByProvider(cutoff?: number | null): ProviderAggregate[];
83
+ /**
84
+ * Token burn per provider per local hour of day (0-23). Hours use the
85
+ * server's timezone — a localhost tool, so server and viewer coincide. Rolled
86
+ * hours are attributed by their start, so half-hour UTC offsets shift burn by
87
+ * up to 30 minutes.
88
+ */
89
+ export declare function getProviderHourlyBurn(cutoff?: number | null): ProviderHourlyPoint[];
90
+ /** Tokens/cost per bucket per provider. */
91
+ export declare function getProviderTimeSeries({ cutoff, bucketMs }: RangeWindow): ProviderTimeSeriesPoint[];
92
+ /** Per-transcript-file rollup for the Traces session list. */
93
+ export interface SessionRollupRow {
94
+ sessionFile: string;
95
+ requests: number;
96
+ startedAt: number;
97
+ endedAt: number;
98
+ totalTokens: number;
99
+ costTotal: number;
100
+ unpricedRequests: number;
101
+ /** Comma-joined DISTINCT models. */
102
+ models: string | null;
103
+ toolCalls: number;
104
+ }
105
+ /**
106
+ * One row per synced transcript file (subagents unfolded). Same exactness
107
+ * rules as the range queries: dirty transcripts are aggregated raw while few.
108
+ */
109
+ export declare function getSessionRollups(): SessionRollupRow[];
110
+ /** Tool usage per tool name, most-called first. */
111
+ export declare function getToolStats(cutoff?: number | null): ToolUsageStats[];
112
+ /** Tool usage per (tool, model, provider), most-called first. */
113
+ export declare function getToolStatsByModel(cutoff?: number | null): ToolModelStats[];
114
+ /** Calls and errors per bucket per tool. */
115
+ export declare function getToolTimeSeries({ cutoff, bucketMs }: RangeWindow): ToolTimeSeriesPoint[];
@@ -1,3 +1,4 @@
1
+ import { type StatsJudgeProvider } from "./frustration.js";
1
2
  /**
2
3
  * Handle API requests.
3
4
  */
@@ -12,4 +13,8 @@ export interface StatsServerHandle {
12
13
  port: number;
13
14
  stop: () => void;
14
15
  }
15
- export declare function startServer(port?: number, hostname?: string): Promise<StatsServerHandle>;
16
+ export interface StartServerOptions {
17
+ /** Host judge for the Frustration dashboard's judge runs; replaces any previously registered one. */
18
+ judge?: StatsJudgeProvider;
19
+ }
20
+ export declare function startServer(port?: number, hostname?: string, options?: StartServerOptions): Promise<StatsServerHandle>;