@oh-my-pi/omp-stats 15.0.1 → 15.1.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.
- package/package.json +4 -4
- package/src/parser.ts +44 -1
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/omp-stats",
|
|
4
|
-
"version": "15.0
|
|
4
|
+
"version": "15.1.0",
|
|
5
5
|
"description": "Local observability dashboard for pi AI usage statistics",
|
|
6
|
-
"homepage": "https://
|
|
6
|
+
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"repository": {
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"fmt": "biome format --write ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@oh-my-pi/pi-ai": "15.0
|
|
41
|
-
"@oh-my-pi/pi-utils": "15.0
|
|
40
|
+
"@oh-my-pi/pi-ai": "15.1.0",
|
|
41
|
+
"@oh-my-pi/pi-utils": "15.1.0",
|
|
42
42
|
"@tailwindcss/node": "^4.2.4",
|
|
43
43
|
"chart.js": "^4.5.1",
|
|
44
44
|
"date-fns": "^4.1.0",
|
package/src/parser.ts
CHANGED
|
@@ -31,6 +31,10 @@ function extractFolderFromPath(sessionPath: string): string {
|
|
|
31
31
|
function isAssistantMessage(entry: SessionEntry): entry is SessionMessageEntry {
|
|
32
32
|
if (entry.type !== "message") return false;
|
|
33
33
|
const msgEntry = entry as SessionMessageEntry;
|
|
34
|
+
// Legacy sessions (pre-id tracking) recorded message entries without an `id`.
|
|
35
|
+
// They're not linkable and would violate the messages.entry_id NOT NULL
|
|
36
|
+
// constraint, so skip them at the parser boundary.
|
|
37
|
+
if (typeof msgEntry.id !== "string" || msgEntry.id.length === 0) return false;
|
|
34
38
|
return msgEntry.message?.role === "assistant";
|
|
35
39
|
}
|
|
36
40
|
|
|
@@ -40,6 +44,7 @@ function isAssistantMessage(entry: SessionEntry): entry is SessionMessageEntry {
|
|
|
40
44
|
function isUserMessage(entry: SessionEntry): entry is SessionMessageEntry {
|
|
41
45
|
if (entry.type !== "message") return false;
|
|
42
46
|
const msgEntry = entry as SessionMessageEntry;
|
|
47
|
+
if (typeof msgEntry.id !== "string" || msgEntry.id.length === 0) return false;
|
|
43
48
|
return msgEntry.message?.role === "user";
|
|
44
49
|
}
|
|
45
50
|
|
|
@@ -159,9 +164,45 @@ function parseSessionEntriesLenient(bytes: Uint8Array): { entries: SessionEntry[
|
|
|
159
164
|
return { entries, read: cursor };
|
|
160
165
|
}
|
|
161
166
|
|
|
167
|
+
function scanLastServiceTier(bytes: Uint8Array): ServiceTier | undefined {
|
|
168
|
+
let cursor = 0;
|
|
169
|
+
let currentServiceTier: ServiceTier | undefined;
|
|
170
|
+
|
|
171
|
+
while (cursor < bytes.length) {
|
|
172
|
+
const { values, error, read, done } = Bun.JSONL.parseChunk(bytes, cursor, bytes.length);
|
|
173
|
+
for (const value of values as SessionEntry[]) {
|
|
174
|
+
if (isServiceTierChange(value)) currentServiceTier = value.serviceTier ?? undefined;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (error) {
|
|
178
|
+
const nextNewline = bytes.indexOf(LF, Math.max(read, cursor));
|
|
179
|
+
if (nextNewline === -1) break;
|
|
180
|
+
cursor = nextNewline + 1;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (read <= cursor) break;
|
|
185
|
+
cursor = read;
|
|
186
|
+
if (done) break;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return currentServiceTier;
|
|
190
|
+
}
|
|
162
191
|
/**
|
|
163
192
|
* Parse a session file and extract all assistant message stats.
|
|
164
193
|
* Uses incremental reading with offset tracking.
|
|
194
|
+
*
|
|
195
|
+
* Service-tier carry-over: `currentServiceTier` is a session-scoped piece of
|
|
196
|
+
* state derived from `service_tier_change` entries that affects whether
|
|
197
|
+
* subsequent OpenAI assistant replies count as premium requests. Incremental
|
|
198
|
+
* syncs that resume past the most-recent tier change would otherwise lose
|
|
199
|
+
* that state and silently record `premiumRequests = 0` for priority traffic
|
|
200
|
+
* (the coding-agent stopped folding the tier into `usage.premiumRequests`
|
|
201
|
+
* after 13f59162e — the parser is now the sole source of truth). When
|
|
202
|
+
* `fromOffset > 0` we therefore scan the bytes preceding `fromOffset`
|
|
203
|
+
* for the latest service-tier value before parsing the unprocessed tail.
|
|
204
|
+
* The scan only keeps the current tier and does not materialize prefix
|
|
205
|
+
* entries, preserving offset-based memory behavior for large sessions.
|
|
165
206
|
*/
|
|
166
207
|
export interface ParseSessionResult {
|
|
167
208
|
stats: MessageStats[];
|
|
@@ -169,7 +210,6 @@ export interface ParseSessionResult {
|
|
|
169
210
|
userLinks: UserMessageLink[];
|
|
170
211
|
newOffset: number;
|
|
171
212
|
}
|
|
172
|
-
|
|
173
213
|
export async function parseSessionFile(sessionPath: string, fromOffset = 0): Promise<ParseSessionResult> {
|
|
174
214
|
let bytes: Uint8Array;
|
|
175
215
|
try {
|
|
@@ -188,6 +228,9 @@ export async function parseSessionFile(sessionPath: string, fromOffset = 0): Pro
|
|
|
188
228
|
const unprocessed = bytes.subarray(start);
|
|
189
229
|
const { entries, read } = parseSessionEntriesLenient(unprocessed);
|
|
190
230
|
let currentServiceTier: ServiceTier | undefined;
|
|
231
|
+
if (start > 0) {
|
|
232
|
+
currentServiceTier = scanLastServiceTier(bytes.subarray(0, start));
|
|
233
|
+
}
|
|
191
234
|
for (const entry of entries) {
|
|
192
235
|
if (isServiceTierChange(entry)) {
|
|
193
236
|
currentServiceTier = entry.serviceTier ?? undefined;
|