@oh-my-pi/omp-stats 16.1.16 → 16.1.18
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/CHANGELOG.md +6 -0
- package/dist/types/db.d.ts +15 -0
- package/package.json +4 -4
- package/src/db.ts +79 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.1.17] - 2026-06-24
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Stats sync counted the same provider request multiple times when a forked or branched session file copied the parent's entries verbatim. Inserts now skip rows whose `(entry_id, timestamp)` already exists under a different `session_file`, and a one-shot migration on the next `omp stats` run collapses any pre-existing duplicates ([#3370](https://github.com/can1357/oh-my-pi/issues/3370)).
|
|
10
|
+
|
|
5
11
|
## [16.1.15] - 2026-06-22
|
|
6
12
|
|
|
7
13
|
### Added
|
package/dist/types/db.d.ts
CHANGED
|
@@ -17,6 +17,18 @@ export declare function getFileOffset(sessionFile: string): {
|
|
|
17
17
|
export declare function setFileOffset(sessionFile: string, offset: number, lastModified: number): void;
|
|
18
18
|
/**
|
|
19
19
|
* Insert message stats into the database.
|
|
20
|
+
*
|
|
21
|
+
* Forked / branched sessions (see `SessionManager.fork()` and
|
|
22
|
+
* `createBranchedSession()` in `@oh-my-pi/pi-coding-agent`) deep-copy a parent
|
|
23
|
+
* session's entries into a new JSONL — same `entry_id`, `timestamp`, `model`,
|
|
24
|
+
* `provider`, token counts, and `responseId`. The `UNIQUE(session_file,
|
|
25
|
+
* entry_id)` constraint alone keys each row by file, so without the guard
|
|
26
|
+
* below the same provider request would land twice and inflate every
|
|
27
|
+
* aggregate. The `WHERE NOT EXISTS` clause skips inserts whose
|
|
28
|
+
* `(entry_id, timestamp)` already exists under a different `session_file` —
|
|
29
|
+
* first-write-wins across the lineage. Same-file re-syncs still hit the
|
|
30
|
+
* `ON CONFLICT(session_file, entry_id)` upsert below so historical
|
|
31
|
+
* `premium_requests` fix-ups continue to work.
|
|
20
32
|
*/
|
|
21
33
|
export declare function insertMessageStats(stats: MessageStats[]): number;
|
|
22
34
|
/**
|
|
@@ -72,6 +84,9 @@ export declare function markUserMessagesBackfillComplete(): void;
|
|
|
72
84
|
export declare function markUserMessageLinksRepairComplete(): void;
|
|
73
85
|
/**
|
|
74
86
|
* Insert user-message stats. Idempotent via UNIQUE(session_file, entry_id).
|
|
87
|
+
* The `WHERE NOT EXISTS` clause matches {@link insertMessageStats}: forks
|
|
88
|
+
* copy user entries verbatim into the child JSONL, so the same
|
|
89
|
+
* `(entry_id, timestamp)` must not land twice across different session files.
|
|
75
90
|
*/
|
|
76
91
|
export declare function insertUserMessageStats(stats: UserMessageStats[]): number;
|
|
77
92
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/omp-stats",
|
|
4
|
-
"version": "16.1.
|
|
4
|
+
"version": "16.1.18",
|
|
5
5
|
"description": "Local observability dashboard for pi AI usage statistics",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -37,9 +37,9 @@
|
|
|
37
37
|
"fmt": "biome format --write ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@oh-my-pi/pi-ai": "16.1.
|
|
41
|
-
"@oh-my-pi/pi-catalog": "16.1.
|
|
42
|
-
"@oh-my-pi/pi-utils": "16.1.
|
|
40
|
+
"@oh-my-pi/pi-ai": "16.1.18",
|
|
41
|
+
"@oh-my-pi/pi-catalog": "16.1.18",
|
|
42
|
+
"@oh-my-pi/pi-utils": "16.1.18",
|
|
43
43
|
"@tailwindcss/node": "^4.3.0",
|
|
44
44
|
"chart.js": "^4.5.1",
|
|
45
45
|
"date-fns": "^4.4.0",
|
package/src/db.ts
CHANGED
|
@@ -45,6 +45,7 @@ const USER_MESSAGES_BACKFILL_KEY = "user_messages_v6";
|
|
|
45
45
|
const USER_MESSAGE_LINKS_REPAIR_KEY = "user_message_links_v1";
|
|
46
46
|
const PRIORITY_PREMIUM_REQUESTS_BACKFILL_KEY = "premium_requests_priority_v1";
|
|
47
47
|
const AGENT_TYPE_BACKFILL_KEY = "agent_type_v1";
|
|
48
|
+
const FORK_DEDUPE_KEY = "fork_dedupe_v1";
|
|
48
49
|
function shouldResetBackfill(value: string | undefined): boolean {
|
|
49
50
|
return value !== BACKFILL_COMPLETE && value !== BACKFILL_PENDING;
|
|
50
51
|
}
|
|
@@ -218,6 +219,7 @@ export async function initDb(): Promise<Database> {
|
|
|
218
219
|
backfillPriorityPremiumRequests(db);
|
|
219
220
|
backfillAgentType(db);
|
|
220
221
|
backfillMissingCatalogCosts(db);
|
|
222
|
+
backfillForkDuplicates(db);
|
|
221
223
|
return db;
|
|
222
224
|
}
|
|
223
225
|
|
|
@@ -334,22 +336,34 @@ export function setFileOffset(sessionFile: string, offset: number, lastModified:
|
|
|
334
336
|
|
|
335
337
|
/**
|
|
336
338
|
* Insert message stats into the database.
|
|
339
|
+
*
|
|
340
|
+
* Forked / branched sessions (see `SessionManager.fork()` and
|
|
341
|
+
* `createBranchedSession()` in `@oh-my-pi/pi-coding-agent`) deep-copy a parent
|
|
342
|
+
* session's entries into a new JSONL — same `entry_id`, `timestamp`, `model`,
|
|
343
|
+
* `provider`, token counts, and `responseId`. The `UNIQUE(session_file,
|
|
344
|
+
* entry_id)` constraint alone keys each row by file, so without the guard
|
|
345
|
+
* below the same provider request would land twice and inflate every
|
|
346
|
+
* aggregate. The `WHERE NOT EXISTS` clause skips inserts whose
|
|
347
|
+
* `(entry_id, timestamp)` already exists under a different `session_file` —
|
|
348
|
+
* first-write-wins across the lineage. Same-file re-syncs still hit the
|
|
349
|
+
* `ON CONFLICT(session_file, entry_id)` upsert below so historical
|
|
350
|
+
* `premium_requests` fix-ups continue to work.
|
|
337
351
|
*/
|
|
338
352
|
export function insertMessageStats(stats: MessageStats[]): number {
|
|
339
353
|
if (!db || stats.length === 0) return 0;
|
|
340
354
|
|
|
341
|
-
// Use UPSERT so a re-sync can fix up `premium_requests` for rows persisted
|
|
342
|
-
// before priority service-tier traffic was counted as premium. The guard
|
|
343
|
-
// `WHERE messages.premium_requests < excluded.premium_requests` keeps every
|
|
344
|
-
// other column immutable and never demotes an existing count (e.g. when a
|
|
345
|
-
// later parse drops back to 0 for the same row).
|
|
346
355
|
const stmt = db.prepare(`
|
|
347
356
|
INSERT INTO messages (
|
|
348
357
|
session_file, entry_id, folder, model, provider, api, timestamp,
|
|
349
358
|
duration, ttft, stop_reason, error_message,
|
|
350
359
|
input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, total_tokens, premium_requests,
|
|
351
360
|
cost_input, cost_output, cost_cache_read, cost_cache_write, cost_total, agent_type
|
|
352
|
-
)
|
|
361
|
+
)
|
|
362
|
+
SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
|
363
|
+
WHERE NOT EXISTS (
|
|
364
|
+
SELECT 1 FROM messages
|
|
365
|
+
WHERE entry_id = ? AND timestamp = ? AND session_file <> ?
|
|
366
|
+
)
|
|
353
367
|
ON CONFLICT(session_file, entry_id) DO UPDATE SET
|
|
354
368
|
premium_requests = excluded.premium_requests
|
|
355
369
|
WHERE messages.premium_requests < excluded.premium_requests
|
|
@@ -383,6 +397,11 @@ export function insertMessageStats(stats: MessageStats[]): number {
|
|
|
383
397
|
cost.cacheWrite,
|
|
384
398
|
cost.total,
|
|
385
399
|
s.agentType,
|
|
400
|
+
// `WHERE NOT EXISTS` binds: skip when a different session_file
|
|
401
|
+
// already holds this (entry_id, timestamp).
|
|
402
|
+
s.entryId,
|
|
403
|
+
s.timestamp,
|
|
404
|
+
s.sessionFile,
|
|
386
405
|
);
|
|
387
406
|
if (result.changes > 0) inserted++;
|
|
388
407
|
}
|
|
@@ -893,6 +912,46 @@ function backfillAgentType(database: Database): void {
|
|
|
893
912
|
apply();
|
|
894
913
|
}
|
|
895
914
|
|
|
915
|
+
/**
|
|
916
|
+
* One-shot collapse of forked-session duplicates that landed under the old
|
|
917
|
+
* `UNIQUE(session_file, entry_id)`-only invariant. `SessionManager.fork()`
|
|
918
|
+
* and `createBranchedSession()` deep-copy a parent's entries into the new
|
|
919
|
+
* JSONL — same `entry_id`, `timestamp`, `model`, `responseId`, token counts,
|
|
920
|
+
* cost — and the previous insert path counted both files toward request /
|
|
921
|
+
* token / cost totals. The migration keeps the lowest-`id` row per
|
|
922
|
+
* `(entry_id, timestamp)` group (almost always the parent — sessions are
|
|
923
|
+
* filename-timestamped and sync processes them in name order, so the
|
|
924
|
+
* originating file lands first) and drops every other copy. Same fix on
|
|
925
|
+
* `user_messages` since forks copy user entries too. Idempotent and
|
|
926
|
+
* crash-safe: enrolled at module-load via the `meta` sentinel, marked
|
|
927
|
+
* COMPLETE inside the same transaction so an aborted run rolls back and
|
|
928
|
+
* retries on the next init.
|
|
929
|
+
*/
|
|
930
|
+
function backfillForkDuplicates(database: Database): void {
|
|
931
|
+
const row = database.prepare("SELECT value FROM meta WHERE key = ?").get(FORK_DEDUPE_KEY) as
|
|
932
|
+
| { value: string }
|
|
933
|
+
| undefined;
|
|
934
|
+
if (row?.value === BACKFILL_COMPLETE) return;
|
|
935
|
+
|
|
936
|
+
const markComplete = database.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)");
|
|
937
|
+
const apply = database.transaction(() => {
|
|
938
|
+
database.run(`
|
|
939
|
+
DELETE FROM messages
|
|
940
|
+
WHERE id NOT IN (
|
|
941
|
+
SELECT MIN(id) FROM messages GROUP BY entry_id, timestamp
|
|
942
|
+
)
|
|
943
|
+
`);
|
|
944
|
+
database.run(`
|
|
945
|
+
DELETE FROM user_messages
|
|
946
|
+
WHERE id NOT IN (
|
|
947
|
+
SELECT MIN(id) FROM user_messages GROUP BY entry_id, timestamp
|
|
948
|
+
)
|
|
949
|
+
`);
|
|
950
|
+
markComplete.run(FORK_DEDUPE_KEY, BACKFILL_COMPLETE);
|
|
951
|
+
});
|
|
952
|
+
apply();
|
|
953
|
+
}
|
|
954
|
+
|
|
896
955
|
/**
|
|
897
956
|
* One-shot wipe of `file_offsets` to force `parseSessionFile` to re-parse
|
|
898
957
|
* every session from byte zero. We don't touch `user_messages`; the parser
|
|
@@ -961,6 +1020,9 @@ export function markUserMessageLinksRepairComplete(): void {
|
|
|
961
1020
|
|
|
962
1021
|
/**
|
|
963
1022
|
* Insert user-message stats. Idempotent via UNIQUE(session_file, entry_id).
|
|
1023
|
+
* The `WHERE NOT EXISTS` clause matches {@link insertMessageStats}: forks
|
|
1024
|
+
* copy user entries verbatim into the child JSONL, so the same
|
|
1025
|
+
* `(entry_id, timestamp)` must not land twice across different session files.
|
|
964
1026
|
*/
|
|
965
1027
|
export function insertUserMessageStats(stats: UserMessageStats[]): number {
|
|
966
1028
|
if (!db || stats.length === 0) return 0;
|
|
@@ -970,7 +1032,12 @@ export function insertUserMessageStats(stats: UserMessageStats[]): number {
|
|
|
970
1032
|
session_file, entry_id, folder, timestamp, model, provider,
|
|
971
1033
|
chars, words, yelling, profanity, anguish,
|
|
972
1034
|
negation, repetition, blame
|
|
973
|
-
)
|
|
1035
|
+
)
|
|
1036
|
+
SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
|
1037
|
+
WHERE NOT EXISTS (
|
|
1038
|
+
SELECT 1 FROM user_messages
|
|
1039
|
+
WHERE entry_id = ? AND timestamp = ? AND session_file <> ?
|
|
1040
|
+
)
|
|
974
1041
|
`);
|
|
975
1042
|
|
|
976
1043
|
let inserted = 0;
|
|
@@ -991,6 +1058,11 @@ export function insertUserMessageStats(stats: UserMessageStats[]): number {
|
|
|
991
1058
|
s.negation,
|
|
992
1059
|
s.repetition,
|
|
993
1060
|
s.blame,
|
|
1061
|
+
// `WHERE NOT EXISTS` binds: skip when a different session_file
|
|
1062
|
+
// already holds this (entry_id, timestamp).
|
|
1063
|
+
s.entryId,
|
|
1064
|
+
s.timestamp,
|
|
1065
|
+
s.sessionFile,
|
|
994
1066
|
);
|
|
995
1067
|
if (result.changes > 0) inserted++;
|
|
996
1068
|
}
|