@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.
Files changed (2) hide show
  1. package/package.json +4 -4
  2. 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.1",
4
+ "version": "15.1.0",
5
5
  "description": "Local observability dashboard for pi AI usage statistics",
6
- "homepage": "https://github.com/can1357/oh-my-pi",
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.1",
41
- "@oh-my-pi/pi-utils": "15.0.1",
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;