@oh-my-pi/omp-stats 18.16.0 → 18.17.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/CHANGELOG.md +7 -0
- package/dist/client/index.css +1 -1
- package/dist/client/index.js +102 -99
- package/dist/types/client/api.d.ts +4 -1
- package/dist/types/client/app/nav.d.ts +1 -1
- package/dist/types/client/charts/Chart.d.ts +16 -8
- package/dist/types/client/charts/DotWhisker.d.ts +50 -0
- package/dist/types/client/charts/Sparkline.d.ts +1 -1
- package/dist/types/client/charts/Waterfall.d.ts +26 -0
- package/dist/types/client/charts/index.d.ts +2 -0
- package/dist/types/client/charts/types.d.ts +8 -2
- package/dist/types/client/routes/SpotlightRoute.d.ts +7 -0
- package/dist/types/client/routes/index.d.ts +1 -0
- package/dist/types/client/spotlight/Compounding.d.ts +8 -0
- package/dist/types/client/spotlight/Coverage.d.ts +6 -0
- package/dist/types/client/spotlight/Economics.d.ts +10 -0
- package/dist/types/client/spotlight/FetchBack.d.ts +9 -0
- package/dist/types/client/spotlight/Hero.d.ts +12 -0
- package/dist/types/client/spotlight/Overhead.d.ts +6 -0
- package/dist/types/client/spotlight/Ranking.d.ts +6 -0
- package/dist/types/client/spotlight/ResultsFeed.d.ts +6 -0
- package/dist/types/client/spotlight/Savings.d.ts +10 -0
- package/dist/types/client/spotlight/ShareTrend.d.ts +8 -0
- package/dist/types/client/spotlight/Steps.d.ts +6 -0
- package/dist/types/client/spotlight/TimeCost.d.ts +6 -0
- package/dist/types/client/spotlight/chips.d.ts +16 -0
- package/dist/types/client/spotlight/meta.d.ts +64 -0
- package/dist/types/db.d.ts +26 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/parser.d.ts +4 -0
- package/dist/types/rollup.d.ts +22 -1
- package/dist/types/shared-types.d.ts +419 -0
- package/dist/types/spotlight-aggregator.d.ts +189 -0
- package/dist/types/spotlight-bootstrap.d.ts +32 -0
- package/dist/types/spotlight-ingest.d.ts +258 -0
- package/dist/types/spotlight-measures.d.ts +51 -0
- package/package.json +5 -5
- package/src/aggregator.ts +3 -1
- package/src/client/App.tsx +3 -0
- package/src/client/api.ts +21 -0
- package/src/client/app/nav.ts +4 -1
- package/src/client/charts/Chart.tsx +105 -22
- package/src/client/charts/DotWhisker.tsx +149 -0
- package/src/client/charts/Sparkline.tsx +3 -2
- package/src/client/charts/Waterfall.tsx +86 -0
- package/src/client/charts/index.ts +2 -0
- package/src/client/charts/types.ts +9 -2
- package/src/client/react-css.d.ts +1 -1
- package/src/client/routes/SpotlightRoute.tsx +138 -0
- package/src/client/routes/index.ts +1 -0
- package/src/client/spotlight/Compounding.tsx +222 -0
- package/src/client/spotlight/Coverage.tsx +96 -0
- package/src/client/spotlight/Economics.tsx +167 -0
- package/src/client/spotlight/FetchBack.tsx +222 -0
- package/src/client/spotlight/Hero.tsx +177 -0
- package/src/client/spotlight/Overhead.tsx +135 -0
- package/src/client/spotlight/Ranking.tsx +248 -0
- package/src/client/spotlight/ResultsFeed.tsx +354 -0
- package/src/client/spotlight/Savings.tsx +355 -0
- package/src/client/spotlight/ShareTrend.tsx +103 -0
- package/src/client/spotlight/Steps.tsx +225 -0
- package/src/client/spotlight/TimeCost.tsx +64 -0
- package/src/client/spotlight/chips.tsx +63 -0
- package/src/client/spotlight/meta.ts +206 -0
- package/src/client/spotlight/spotlight.css +1387 -0
- package/src/client/styles.css +289 -1
- package/src/db.ts +480 -2
- package/src/index.ts +10 -1
- package/src/live.ts +21 -9
- package/src/parser.ts +44 -8
- package/src/rollup.ts +203 -10
- package/src/server.ts +42 -0
- package/src/shared-types.ts +453 -0
- package/src/spotlight-aggregator.ts +1738 -0
- package/src/spotlight-bootstrap.ts +110 -0
- package/src/spotlight-ingest.ts +981 -0
- package/src/spotlight-measures.ts +667 -0
package/src/db.ts
CHANGED
|
@@ -14,6 +14,15 @@ import { getConfigRootDir, getStatsDbPath, VERSION } from "@oh-my-pi/pi-utils";
|
|
|
14
14
|
import { ChunkedInsert } from "./chunked-insert";
|
|
15
15
|
import { classifyAgentType, type ParseSessionResult, type SessionParserState } from "./parser";
|
|
16
16
|
import { ensureRollupSchema, getDailyActivityFromRollup } from "./rollup";
|
|
17
|
+
import {
|
|
18
|
+
mergeRanges,
|
|
19
|
+
type SpotlightCallResultLink,
|
|
20
|
+
type SpotlightCallRow,
|
|
21
|
+
type SpotlightNeedRow,
|
|
22
|
+
type SpotlightRequestRow,
|
|
23
|
+
type SpotlightSessionRow,
|
|
24
|
+
type SpotlightShapeRow,
|
|
25
|
+
} from "./spotlight-ingest";
|
|
17
26
|
import type {
|
|
18
27
|
AgentType,
|
|
19
28
|
DailyActivityPoint,
|
|
@@ -89,7 +98,7 @@ export function currentDb(): Database | null {
|
|
|
89
98
|
|
|
90
99
|
const BACKFILL_COMPLETE = "complete";
|
|
91
100
|
const BACKFILL_PENDING = "pending";
|
|
92
|
-
const USER_MESSAGES_BACKFILL_KEY = "
|
|
101
|
+
const USER_MESSAGES_BACKFILL_KEY = "user_messages_v10";
|
|
93
102
|
const USER_MESSAGE_LINKS_REPAIR_KEY = "user_message_links_v1";
|
|
94
103
|
// v2: the parser also records the served service tier per message, so a full
|
|
95
104
|
// re-parse fills `service_tier` and re-derives ultrafast premium counts that the
|
|
@@ -120,6 +129,27 @@ const NO_CACHE_INPUT_BACKFILL_KEY = "cost_no_cache_input_v1";
|
|
|
120
129
|
// value stored is the version that ran it), since only a catalog update can
|
|
121
130
|
// price a model it could not price before.
|
|
122
131
|
const CATALOG_COST_BACKFILL_KEY = "catalog_cost_backfill";
|
|
132
|
+
// The Spotlight tables are filled by the parser; transcripts ingested before
|
|
133
|
+
// they existed are re-parsed once. Only files modified since the experiment
|
|
134
|
+
// shipped can hold its entries, so older cursors stay put.
|
|
135
|
+
const SPOTLIGHT_BACKFILL_KEY = "spotlight_v1";
|
|
136
|
+
const SPOTLIGHT_SINCE_MS = Date.parse("2026-10-28T00:00:00Z");
|
|
137
|
+
// Experiment sessions parsed before `spotlight_calls.edits` existed are
|
|
138
|
+
// re-parsed once (only them: they are few) so recovery detection sees edits.
|
|
139
|
+
// (`v3`: a build that ran the `v2` pass may have had its sessions re-parsed
|
|
140
|
+
// again by an older build, which records no edits.)
|
|
141
|
+
const SPOTLIGHT_EDITS_BACKFILL_KEY = "spotlight_v3_edits";
|
|
142
|
+
// Experiment sessions parsed before results carried heat, kept runs, plain sizes
|
|
143
|
+
// and needed lines are re-parsed once (only them, never the whole history).
|
|
144
|
+
const SPOTLIGHT_NEEDS_BACKFILL_KEY = "spotlight_v4_needs";
|
|
145
|
+
// Experiment sessions parsed before calls carried argument tokens (the generic
|
|
146
|
+
// rerun rule for every tool beyond read and bash) are re-parsed once.
|
|
147
|
+
const SPOTLIGHT_ARG_TOKENS_BACKFILL_KEY = "spotlight_v5_arg_tokens";
|
|
148
|
+
// The experiment became a toggle: sessions are marked by `spotlight:session`
|
|
149
|
+
// (which older builds did not read) and legacy `spotlight:assignment` sessions
|
|
150
|
+
// count only with the arm that shipped. Every transcript that can hold
|
|
151
|
+
// Spotlight entries is re-parsed once by the new rules.
|
|
152
|
+
const SPOTLIGHT_TOGGLE_BACKFILL_KEY = "spotlight_v6_toggle";
|
|
123
153
|
function shouldResetBackfill(value: string | undefined): boolean {
|
|
124
154
|
return value !== BACKFILL_COMPLETE && value !== BACKFILL_PENDING;
|
|
125
155
|
}
|
|
@@ -273,12 +303,128 @@ export async function initDb(): Promise<Database> {
|
|
|
273
303
|
key TEXT PRIMARY KEY,
|
|
274
304
|
value TEXT NOT NULL
|
|
275
305
|
);
|
|
306
|
+
|
|
307
|
+
-- Spotlight experiment sessions: the first Spotlight marker of each transcript.
|
|
308
|
+
CREATE TABLE IF NOT EXISTS spotlight_sessions (
|
|
309
|
+
session_file TEXT PRIMARY KEY,
|
|
310
|
+
folder TEXT NOT NULL,
|
|
311
|
+
marked_at INTEGER NOT NULL,
|
|
312
|
+
cwd TEXT,
|
|
313
|
+
title TEXT
|
|
314
|
+
) WITHOUT ROWID;
|
|
315
|
+
|
|
316
|
+
-- One evaluated tool result: its spotlight:shape entry and its result's details.spotlight, upserted.
|
|
317
|
+
CREATE TABLE IF NOT EXISTS spotlight_results (
|
|
318
|
+
session_file TEXT NOT NULL,
|
|
319
|
+
tool_call_id TEXT NOT NULL,
|
|
320
|
+
timestamp INTEGER NOT NULL,
|
|
321
|
+
tool TEXT NOT NULL,
|
|
322
|
+
in_chars INTEGER NOT NULL,
|
|
323
|
+
out_chars INTEGER NOT NULL,
|
|
324
|
+
in_tokens INTEGER NOT NULL,
|
|
325
|
+
out_tokens INTEGER NOT NULL,
|
|
326
|
+
steps TEXT NOT NULL,
|
|
327
|
+
has_details INTEGER NOT NULL,
|
|
328
|
+
holdout INTEGER NOT NULL DEFAULT 0,
|
|
329
|
+
original_url TEXT,
|
|
330
|
+
path TEXT,
|
|
331
|
+
start_line INTEGER,
|
|
332
|
+
lines INTEGER,
|
|
333
|
+
hidden TEXT,
|
|
334
|
+
hidden_lines INTEGER NOT NULL DEFAULT 0,
|
|
335
|
+
artifacts TEXT,
|
|
336
|
+
sp_calls INTEGER,
|
|
337
|
+
sp_failures INTEGER,
|
|
338
|
+
sp_ms REAL,
|
|
339
|
+
sp_cost REAL,
|
|
340
|
+
shape_ms REAL,
|
|
341
|
+
plain_chars INTEGER,
|
|
342
|
+
plain_tokens INTEGER,
|
|
343
|
+
capped_tokens INTEGER,
|
|
344
|
+
score_free INTEGER NOT NULL DEFAULT 0,
|
|
345
|
+
heat TEXT,
|
|
346
|
+
kept TEXT,
|
|
347
|
+
gated INTEGER NOT NULL DEFAULT 0,
|
|
348
|
+
PRIMARY KEY (session_file, tool_call_id)
|
|
349
|
+
) WITHOUT ROWID;
|
|
350
|
+
CREATE INDEX IF NOT EXISTS idx_spotlight_results_call ON spotlight_results(tool_call_id);
|
|
351
|
+
|
|
352
|
+
-- Every tool call of an experiment session from its marker on, in order.
|
|
353
|
+
CREATE TABLE IF NOT EXISTS spotlight_calls (
|
|
354
|
+
session_file TEXT NOT NULL,
|
|
355
|
+
tool_call_id TEXT NOT NULL,
|
|
356
|
+
entry_id TEXT NOT NULL,
|
|
357
|
+
timestamp INTEGER NOT NULL,
|
|
358
|
+
seq INTEGER NOT NULL,
|
|
359
|
+
tool_name TEXT NOT NULL,
|
|
360
|
+
calls_in_turn INTEGER NOT NULL,
|
|
361
|
+
target TEXT NOT NULL,
|
|
362
|
+
result_chars INTEGER,
|
|
363
|
+
artifacts TEXT,
|
|
364
|
+
edits TEXT,
|
|
365
|
+
arg_tokens TEXT,
|
|
366
|
+
PRIMARY KEY (session_file, tool_call_id)
|
|
367
|
+
) WITHOUT ROWID;
|
|
368
|
+
CREATE INDEX IF NOT EXISTS idx_spotlight_calls_entry ON spotlight_calls(entry_id, timestamp);
|
|
369
|
+
|
|
370
|
+
-- Main-loop requests of an experiment session (side requests journaled as model_usage excluded).
|
|
371
|
+
CREATE TABLE IF NOT EXISTS spotlight_requests (
|
|
372
|
+
session_file TEXT NOT NULL,
|
|
373
|
+
entry_id TEXT NOT NULL,
|
|
374
|
+
timestamp INTEGER NOT NULL,
|
|
375
|
+
PRIMARY KEY (session_file, entry_id)
|
|
376
|
+
) WITHOUT ROWID;
|
|
377
|
+
CREATE INDEX IF NOT EXISTS idx_spotlight_requests_entry ON spotlight_requests(entry_id, timestamp);
|
|
378
|
+
|
|
379
|
+
-- Original lines (1-based positions) a shaped read's later calls needed; open while still collecting.
|
|
380
|
+
CREATE TABLE IF NOT EXISTS spotlight_needs (
|
|
381
|
+
session_file TEXT NOT NULL,
|
|
382
|
+
tool_call_id TEXT NOT NULL,
|
|
383
|
+
file TEXT NOT NULL,
|
|
384
|
+
open INTEGER NOT NULL,
|
|
385
|
+
reread TEXT NOT NULL,
|
|
386
|
+
edit TEXT NOT NULL,
|
|
387
|
+
PRIMARY KEY (session_file, tool_call_id)
|
|
388
|
+
) WITHOUT ROWID;
|
|
276
389
|
`);
|
|
277
390
|
|
|
278
391
|
const offsetColumns = db.query("PRAGMA table_info(file_offsets)").all() as { name: string }[];
|
|
279
392
|
if (!offsetColumns.some(column => column.name === "parser_state")) {
|
|
280
393
|
db.run("ALTER TABLE file_offsets ADD COLUMN parser_state TEXT");
|
|
281
394
|
}
|
|
395
|
+
dropSpotlightArms(db);
|
|
396
|
+
// No result was held out before this column existed, so the default is exact for old rows.
|
|
397
|
+
const spotlightResultColumns = db.query<{ name: string }, []>("PRAGMA table_info(spotlight_results)").all();
|
|
398
|
+
if (!spotlightResultColumns.some(column => column.name === "holdout")) {
|
|
399
|
+
db.run("ALTER TABLE spotlight_results ADD COLUMN holdout INTEGER NOT NULL DEFAULT 0");
|
|
400
|
+
}
|
|
401
|
+
// Filled for existing rows by the `spotlight_v4_needs` re-parse; until then plain sizes are estimated.
|
|
402
|
+
for (const [column, type] of [
|
|
403
|
+
["plain_chars", "INTEGER"],
|
|
404
|
+
["plain_tokens", "INTEGER"],
|
|
405
|
+
["capped_tokens", "INTEGER"],
|
|
406
|
+
["score_free", "INTEGER NOT NULL DEFAULT 0"],
|
|
407
|
+
["heat", "TEXT"],
|
|
408
|
+
["kept", "TEXT"],
|
|
409
|
+
// No read was gated before this existed: the default is exact.
|
|
410
|
+
["gated", "INTEGER NOT NULL DEFAULT 0"],
|
|
411
|
+
] as const) {
|
|
412
|
+
if (!spotlightResultColumns.some(existing => existing.name === column)) {
|
|
413
|
+
db.run(`ALTER TABLE spotlight_results ADD COLUMN ${column} ${type}`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
// pi's read mark went with its former edit gate.
|
|
417
|
+
if (spotlightResultColumns.some(column => column.name === "edit_likely")) {
|
|
418
|
+
db.run("ALTER TABLE spotlight_results DROP COLUMN edit_likely");
|
|
419
|
+
}
|
|
420
|
+
const spotlightCallColumns = db.query<{ name: string }, []>("PRAGMA table_info(spotlight_calls)").all();
|
|
421
|
+
if (!spotlightCallColumns.some(column => column.name === "edits")) {
|
|
422
|
+
db.run("ALTER TABLE spotlight_calls ADD COLUMN edits TEXT");
|
|
423
|
+
}
|
|
424
|
+
// Filled for existing rows by the `spotlight_v5_arg_tokens` re-parse; until then no generic rerun is detected.
|
|
425
|
+
if (!spotlightCallColumns.some(column => column.name === "arg_tokens")) {
|
|
426
|
+
db.run("ALTER TABLE spotlight_calls ADD COLUMN arg_tokens TEXT");
|
|
427
|
+
}
|
|
282
428
|
const messageColumns = db.query("PRAGMA table_info(messages)").all() as { name: string }[];
|
|
283
429
|
if (!messageColumns.some(column => column.name === "premium_requests")) {
|
|
284
430
|
db.run("ALTER TABLE messages ADD COLUMN premium_requests REAL NOT NULL DEFAULT 0");
|
|
@@ -389,6 +535,11 @@ export async function initDb(): Promise<Database> {
|
|
|
389
535
|
backfillMissingCatalogCosts(db);
|
|
390
536
|
backfillNoCacheInputCosts(db);
|
|
391
537
|
backfillForkDuplicates(db);
|
|
538
|
+
backfillSpotlight(db, SPOTLIGHT_BACKFILL_KEY);
|
|
539
|
+
reparseSpotlightSessions(db, SPOTLIGHT_EDITS_BACKFILL_KEY);
|
|
540
|
+
reparseSpotlightSessions(db, SPOTLIGHT_NEEDS_BACKFILL_KEY);
|
|
541
|
+
reparseSpotlightSessions(db, SPOTLIGHT_ARG_TOKENS_BACKFILL_KEY);
|
|
542
|
+
backfillSpotlight(db, SPOTLIGHT_TOGGLE_BACKFILL_KEY);
|
|
392
543
|
ensureRollupSchema(db);
|
|
393
544
|
return db;
|
|
394
545
|
}
|
|
@@ -625,6 +776,67 @@ function backfillNoCacheInputCosts(database: Database): void {
|
|
|
625
776
|
applyBackfill();
|
|
626
777
|
}
|
|
627
778
|
|
|
779
|
+
/**
|
|
780
|
+
* Re-parse, once per key, the transcripts that can hold Spotlight entries
|
|
781
|
+
* (modified since {@link SPOTLIGHT_SINCE_MS}) by dropping their cursors; every
|
|
782
|
+
* older cursor stays, so this never triggers a full-history re-parse. The next
|
|
783
|
+
* sync rebuilds those files from byte zero, replacing their Spotlight rows.
|
|
784
|
+
* Keys: {@link SPOTLIGHT_BACKFILL_KEY} (the tables are new),
|
|
785
|
+
* {@link SPOTLIGHT_TOGGLE_BACKFILL_KEY} (the experiment became a toggle).
|
|
786
|
+
*/
|
|
787
|
+
function backfillSpotlight(database: Database, key: string): void {
|
|
788
|
+
const row = database.query<{ value: string }, [string]>("SELECT value FROM meta WHERE key = ?").get(key);
|
|
789
|
+
if (row?.value === BACKFILL_COMPLETE) return;
|
|
790
|
+
database.transaction(() => {
|
|
791
|
+
database.query("DELETE FROM file_offsets WHERE last_modified >= ?").run(SPOTLIGHT_SINCE_MS);
|
|
792
|
+
database.query("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)").run(key, BACKFILL_COMPLETE);
|
|
793
|
+
})();
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Drop the arm columns of a database written while sessions drew arms, once
|
|
798
|
+
* (while `spotlight_sessions.arm` exists). Sessions of any arm but the one that shipped
|
|
799
|
+
* were never shaped with the shipped algorithm: their Spotlight rows go now,
|
|
800
|
+
* so the dashboard leaves them out before the {@link SPOTLIGHT_TOGGLE_BACKFILL_KEY}
|
|
801
|
+
* re-parse (which also clears their parser state) runs.
|
|
802
|
+
*/
|
|
803
|
+
function dropSpotlightArms(database: Database): void {
|
|
804
|
+
const columns = database.query<{ name: string }, []>("PRAGMA table_info(spotlight_sessions)").all();
|
|
805
|
+
if (!columns.some(column => column.name === "arm")) return;
|
|
806
|
+
database.transaction(() => {
|
|
807
|
+
const legacy = "SELECT session_file FROM spotlight_sessions WHERE arm <> 'vylv5'";
|
|
808
|
+
// `spotlight_sessions` (first in the list) goes last: the other deletes select through it.
|
|
809
|
+
for (const table of SPOTLIGHT_TABLES.toReversed()) {
|
|
810
|
+
database.run(`DELETE FROM ${table} WHERE session_file IN (${legacy})`);
|
|
811
|
+
}
|
|
812
|
+
for (const column of ["arm", "find_ranker", "source"]) {
|
|
813
|
+
database.run(`ALTER TABLE spotlight_sessions DROP COLUMN ${column}`);
|
|
814
|
+
}
|
|
815
|
+
database.run("ALTER TABLE spotlight_sessions RENAME COLUMN assigned_at TO marked_at");
|
|
816
|
+
database.run("ALTER TABLE spotlight_results DROP COLUMN arm");
|
|
817
|
+
// The retired guard-rail comparison cached its history reference here.
|
|
818
|
+
database.run("DELETE FROM meta WHERE key LIKE 'spotlight_guard_history:%'");
|
|
819
|
+
})();
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Re-parse, once per key, every experiment session (the transcripts
|
|
824
|
+
* `spotlight_sessions` names, never the whole history) by dropping their
|
|
825
|
+
* cursors; the next sync rebuilds them from byte zero, replacing their
|
|
826
|
+
* Spotlight rows. Keys: {@link SPOTLIGHT_EDITS_BACKFILL_KEY} (calls get
|
|
827
|
+
* `edits`), {@link SPOTLIGHT_NEEDS_BACKFILL_KEY} (results get heat, kept runs,
|
|
828
|
+
* plain sizes and the score-free flag, shaped reads needed lines),
|
|
829
|
+
* {@link SPOTLIGHT_ARG_TOKENS_BACKFILL_KEY} (calls get argument tokens).
|
|
830
|
+
*/
|
|
831
|
+
function reparseSpotlightSessions(database: Database, key: string): void {
|
|
832
|
+
const row = database.query<{ value: string }, [string]>("SELECT value FROM meta WHERE key = ?").get(key);
|
|
833
|
+
if (row?.value === BACKFILL_COMPLETE) return;
|
|
834
|
+
database.transaction(() => {
|
|
835
|
+
database.run("DELETE FROM file_offsets WHERE session_file IN (SELECT session_file FROM spotlight_sessions)");
|
|
836
|
+
database.query("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)").run(key, BACKFILL_COMPLETE);
|
|
837
|
+
})();
|
|
838
|
+
}
|
|
839
|
+
|
|
628
840
|
/** Persisted transcript identity and parser cursor used to resume stats ingestion. */
|
|
629
841
|
export interface FileOffset {
|
|
630
842
|
offset: number;
|
|
@@ -841,6 +1053,9 @@ export function applySessionParseResults(sessions: ParsedSession[]): {
|
|
|
841
1053
|
}
|
|
842
1054
|
}
|
|
843
1055
|
if (replace) {
|
|
1056
|
+
for (const table of SPOTLIGHT_TABLES) {
|
|
1057
|
+
changes += database.query(`DELETE FROM ${table} WHERE session_file = ?`).run(sessionFile).changes;
|
|
1058
|
+
}
|
|
844
1059
|
if (messages.length > 0) {
|
|
845
1060
|
changes += database.query("DELETE FROM messages WHERE session_file = ?").run(sessionFile).changes;
|
|
846
1061
|
}
|
|
@@ -865,7 +1080,14 @@ export function applySessionParseResults(sessions: ParsedSession[]): {
|
|
|
865
1080
|
insertUserMessageStats(parsedRows(writes, result => result.userStats)) +
|
|
866
1081
|
updateUserMessageLinks(parsedRows(writes, result => result.userLinks)) +
|
|
867
1082
|
insertToolCalls(parsedRows(writes, result => result.toolCalls)) +
|
|
868
|
-
updateToolResults(parsedRows(writes, result => result.toolResults))
|
|
1083
|
+
updateToolResults(parsedRows(writes, result => result.toolResults)) +
|
|
1084
|
+
insertSpotlightSessions(parsedRows(writes, result => result.spotlight.sessions)) +
|
|
1085
|
+
updateSpotlightTitles(parsedRows(writes, result => result.spotlight.titles)) +
|
|
1086
|
+
upsertSpotlightShapes(parsedRows(writes, result => result.spotlight.shapes)) +
|
|
1087
|
+
insertSpotlightCalls(parsedRows(writes, result => result.spotlight.calls)) +
|
|
1088
|
+
updateSpotlightCallResults(parsedRows(writes, result => result.spotlight.callResults)) +
|
|
1089
|
+
insertSpotlightRequests(parsedRows(writes, result => result.spotlight.requests)) +
|
|
1090
|
+
upsertSpotlightNeeds(parsedRows(writes, result => result.spotlight.needs));
|
|
869
1091
|
for (const { sessionFile, result } of sessions) {
|
|
870
1092
|
if (result.parserState) {
|
|
871
1093
|
setFileOffset(sessionFile, result.newOffset, result.parserState.mtimeMs, result.parserState);
|
|
@@ -1160,6 +1382,8 @@ export async function getDailyActivity(days = 371): Promise<DailyActivityPoint[]
|
|
|
1160
1382
|
* - v9: stored judge prose (`prose` / `prose_hash`) for the frustration
|
|
1161
1383
|
* judge, so every session re-parses once to populate it. Verdicts live in
|
|
1162
1384
|
* `frustration_verdicts`, keyed by prose hash, which this reset never touches.
|
|
1385
|
+
* - v10: excluded agent-attributed user-role messages, which diluted user
|
|
1386
|
+
* frustration rates and could be judged as user complaints.
|
|
1163
1387
|
*
|
|
1164
1388
|
* Existing `messages` rows are unaffected - `INSERT OR IGNORE` keeps them.
|
|
1165
1389
|
*/
|
|
@@ -1702,3 +1926,257 @@ export function updateToolResults(links: Iterable<ToolResultLink>): number {
|
|
|
1702
1926
|
});
|
|
1703
1927
|
return updated;
|
|
1704
1928
|
}
|
|
1929
|
+
|
|
1930
|
+
/** Tables the parser fills for the Spotlight dashboard, all keyed by `session_file`. */
|
|
1931
|
+
const SPOTLIGHT_TABLES = [
|
|
1932
|
+
"spotlight_sessions",
|
|
1933
|
+
"spotlight_results",
|
|
1934
|
+
"spotlight_calls",
|
|
1935
|
+
"spotlight_requests",
|
|
1936
|
+
"spotlight_needs",
|
|
1937
|
+
] as const;
|
|
1938
|
+
|
|
1939
|
+
/** Record each experiment session's first Spotlight marker; a later one (a resumed session) never replaces it. */
|
|
1940
|
+
export function insertSpotlightSessions(rows: Iterable<SpotlightSessionRow>): number {
|
|
1941
|
+
if (!db) return 0;
|
|
1942
|
+
const stmt = db.query(`
|
|
1943
|
+
INSERT OR IGNORE INTO spotlight_sessions (session_file, folder, marked_at, cwd, title)
|
|
1944
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1945
|
+
`);
|
|
1946
|
+
let changes = 0;
|
|
1947
|
+
atomically(db, () => {
|
|
1948
|
+
for (const r of rows) {
|
|
1949
|
+
changes += stmt.run(r.sessionFile, r.folder, r.markedAt, r.cwd, r.title).changes;
|
|
1950
|
+
}
|
|
1951
|
+
});
|
|
1952
|
+
return changes;
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
export function updateSpotlightTitles(rows: Iterable<{ sessionFile: string; title: string }>): number {
|
|
1956
|
+
if (!db) return 0;
|
|
1957
|
+
const stmt = db.query("UPDATE spotlight_sessions SET title = ? WHERE session_file = ?");
|
|
1958
|
+
let changes = 0;
|
|
1959
|
+
atomically(db, () => {
|
|
1960
|
+
for (const r of rows) changes += stmt.run(r.title, r.sessionFile).changes;
|
|
1961
|
+
});
|
|
1962
|
+
return changes;
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
/**
|
|
1966
|
+
* Upsert evaluated results. The shape entry and the result message can land in
|
|
1967
|
+
* different tail parses: the shape entry brings Spotlight's usage and timing,
|
|
1968
|
+
* the result's details the hidden lines and named artifacts, and either may
|
|
1969
|
+
* come first. Fork copies of a call (same tool call id under another
|
|
1970
|
+
* transcript) are skipped, first writer wins.
|
|
1971
|
+
*/
|
|
1972
|
+
export function upsertSpotlightShapes(rows: Iterable<SpotlightShapeRow>): number {
|
|
1973
|
+
if (!db) return 0;
|
|
1974
|
+
const fromDetails = (column: string) =>
|
|
1975
|
+
`${column} = CASE WHEN excluded.has_details = 1 THEN excluded.${column} ELSE spotlight_results.${column} END`;
|
|
1976
|
+
const coalesce = (column: string) => `${column} = COALESCE(excluded.${column}, spotlight_results.${column})`;
|
|
1977
|
+
const writer = new ChunkedInsert(db, {
|
|
1978
|
+
verb: "INSERT",
|
|
1979
|
+
table: "spotlight_results",
|
|
1980
|
+
columns: [
|
|
1981
|
+
"session_file",
|
|
1982
|
+
"tool_call_id",
|
|
1983
|
+
"timestamp",
|
|
1984
|
+
"tool",
|
|
1985
|
+
"in_chars",
|
|
1986
|
+
"out_chars",
|
|
1987
|
+
"in_tokens",
|
|
1988
|
+
"out_tokens",
|
|
1989
|
+
"steps",
|
|
1990
|
+
"has_details",
|
|
1991
|
+
"holdout",
|
|
1992
|
+
"original_url",
|
|
1993
|
+
"path",
|
|
1994
|
+
"start_line",
|
|
1995
|
+
"lines",
|
|
1996
|
+
"hidden",
|
|
1997
|
+
"hidden_lines",
|
|
1998
|
+
"artifacts",
|
|
1999
|
+
"sp_calls",
|
|
2000
|
+
"sp_failures",
|
|
2001
|
+
"sp_ms",
|
|
2002
|
+
"sp_cost",
|
|
2003
|
+
"shape_ms",
|
|
2004
|
+
"plain_chars",
|
|
2005
|
+
"plain_tokens",
|
|
2006
|
+
"capped_tokens",
|
|
2007
|
+
"score_free",
|
|
2008
|
+
"heat",
|
|
2009
|
+
"kept",
|
|
2010
|
+
"gated",
|
|
2011
|
+
],
|
|
2012
|
+
identity: ["tool_call_id"],
|
|
2013
|
+
upsert: `ON CONFLICT(session_file, tool_call_id) DO UPDATE SET
|
|
2014
|
+
${["tool", "in_chars", "out_chars", "in_tokens", "out_tokens", "steps", "hidden_lines"].map(fromDetails).join(",\n")},
|
|
2015
|
+
${["original_url", "path", "start_line", "lines", "hidden", "artifacts", "sp_calls", "sp_failures", "sp_ms", "sp_cost", "shape_ms", "plain_chars", "plain_tokens", "capped_tokens", "heat", "kept"].map(coalesce).join(",\n")},
|
|
2016
|
+
has_details = MAX(spotlight_results.has_details, excluded.has_details),
|
|
2017
|
+
holdout = MAX(spotlight_results.holdout, excluded.holdout),
|
|
2018
|
+
score_free = MAX(spotlight_results.score_free, excluded.score_free),
|
|
2019
|
+
gated = MAX(spotlight_results.gated, excluded.gated)`,
|
|
2020
|
+
});
|
|
2021
|
+
return atomically(db, () => {
|
|
2022
|
+
for (const r of rows) {
|
|
2023
|
+
const o = r.original;
|
|
2024
|
+
writer.add(
|
|
2025
|
+
r.sessionFile,
|
|
2026
|
+
[r.toolCallId],
|
|
2027
|
+
[
|
|
2028
|
+
r.sessionFile,
|
|
2029
|
+
r.toolCallId,
|
|
2030
|
+
r.timestamp,
|
|
2031
|
+
r.tool,
|
|
2032
|
+
r.inChars,
|
|
2033
|
+
r.outChars,
|
|
2034
|
+
r.inTokens,
|
|
2035
|
+
r.outTokens,
|
|
2036
|
+
JSON.stringify(r.steps),
|
|
2037
|
+
r.hasDetails ? 1 : 0,
|
|
2038
|
+
r.holdout ? 1 : 0,
|
|
2039
|
+
o?.url ?? null,
|
|
2040
|
+
o?.path ?? null,
|
|
2041
|
+
o ? o.start : null,
|
|
2042
|
+
o ? o.lines : null,
|
|
2043
|
+
o ? JSON.stringify(o.hidden) : null,
|
|
2044
|
+
o?.hiddenLines ?? 0,
|
|
2045
|
+
r.artifacts ? JSON.stringify(r.artifacts) : null,
|
|
2046
|
+
r.spotlight?.calls ?? null,
|
|
2047
|
+
r.spotlight?.failures ?? null,
|
|
2048
|
+
r.spotlight?.ms ?? null,
|
|
2049
|
+
r.spotlight?.costUsd ?? null,
|
|
2050
|
+
r.shapeMs,
|
|
2051
|
+
r.plainChars,
|
|
2052
|
+
r.plainTokens,
|
|
2053
|
+
r.cappedTokens,
|
|
2054
|
+
r.scoreFree ? 1 : 0,
|
|
2055
|
+
o?.heat ?? null,
|
|
2056
|
+
o ? JSON.stringify(o.kept) : null,
|
|
2057
|
+
r.gated ? 1 : 0,
|
|
2058
|
+
],
|
|
2059
|
+
);
|
|
2060
|
+
}
|
|
2061
|
+
return writer.finish();
|
|
2062
|
+
});
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
/**
|
|
2066
|
+
* Merge needed lines of shaped reads: ranges add to the stored ones (a tail parse brings only its own calls'),
|
|
2067
|
+
* `open` takes the latest. A fork's copy of another transcript's read is skipped, first writer wins.
|
|
2068
|
+
*/
|
|
2069
|
+
export function upsertSpotlightNeeds(rows: Iterable<SpotlightNeedRow>): number {
|
|
2070
|
+
if (!db) return 0;
|
|
2071
|
+
const database = db;
|
|
2072
|
+
const claimed = database.query<{ one: number }, [string, string]>(
|
|
2073
|
+
"SELECT 1 AS one FROM spotlight_needs WHERE tool_call_id = ? AND session_file <> ? LIMIT 1",
|
|
2074
|
+
);
|
|
2075
|
+
const stored = database.query<{ reread: string; edit: string }, [string, string]>(
|
|
2076
|
+
"SELECT reread, edit FROM spotlight_needs WHERE session_file = ? AND tool_call_id = ?",
|
|
2077
|
+
);
|
|
2078
|
+
const write = database.query(
|
|
2079
|
+
"INSERT OR REPLACE INTO spotlight_needs (session_file, tool_call_id, file, open, reread, edit) VALUES (?, ?, ?, ?, ?, ?)",
|
|
2080
|
+
);
|
|
2081
|
+
const ranges = (json: string | undefined): [number, number][] => {
|
|
2082
|
+
if (!json) return [];
|
|
2083
|
+
const value: unknown = JSON.parse(json);
|
|
2084
|
+
return Array.isArray(value)
|
|
2085
|
+
? value.filter(
|
|
2086
|
+
(pair): pair is [number, number] =>
|
|
2087
|
+
Array.isArray(pair) && typeof pair[0] === "number" && typeof pair[1] === "number",
|
|
2088
|
+
)
|
|
2089
|
+
: [];
|
|
2090
|
+
};
|
|
2091
|
+
let changes = 0;
|
|
2092
|
+
atomically(database, () => {
|
|
2093
|
+
for (const r of rows) {
|
|
2094
|
+
if (claimed.get(r.toolCallId, r.sessionFile)) continue;
|
|
2095
|
+
const old = stored.get(r.sessionFile, r.toolCallId);
|
|
2096
|
+
changes += write.run(
|
|
2097
|
+
r.sessionFile,
|
|
2098
|
+
r.toolCallId,
|
|
2099
|
+
r.file,
|
|
2100
|
+
r.open ? 1 : 0,
|
|
2101
|
+
JSON.stringify(mergeRanges([...ranges(old?.reread), ...r.reread])),
|
|
2102
|
+
JSON.stringify(mergeRanges([...ranges(old?.edit), ...r.edit])),
|
|
2103
|
+
).changes;
|
|
2104
|
+
}
|
|
2105
|
+
});
|
|
2106
|
+
return changes;
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
/** Insert experiment-session tool calls; fork copies are skipped as for `tool_calls`. */
|
|
2110
|
+
export function insertSpotlightCalls(rows: Iterable<SpotlightCallRow>): number {
|
|
2111
|
+
if (!db) return 0;
|
|
2112
|
+
const writer = new ChunkedInsert(db, {
|
|
2113
|
+
verb: "INSERT OR IGNORE",
|
|
2114
|
+
table: "spotlight_calls",
|
|
2115
|
+
columns: [
|
|
2116
|
+
"session_file",
|
|
2117
|
+
"tool_call_id",
|
|
2118
|
+
"entry_id",
|
|
2119
|
+
"timestamp",
|
|
2120
|
+
"seq",
|
|
2121
|
+
"tool_name",
|
|
2122
|
+
"calls_in_turn",
|
|
2123
|
+
"target",
|
|
2124
|
+
"edits",
|
|
2125
|
+
"arg_tokens",
|
|
2126
|
+
],
|
|
2127
|
+
identity: ["entry_id", "timestamp", "tool_call_id"],
|
|
2128
|
+
});
|
|
2129
|
+
return atomically(db, () => {
|
|
2130
|
+
for (const r of rows) {
|
|
2131
|
+
writer.add(
|
|
2132
|
+
r.sessionFile,
|
|
2133
|
+
[r.entryId, r.timestamp, r.toolCallId],
|
|
2134
|
+
[
|
|
2135
|
+
r.sessionFile,
|
|
2136
|
+
r.toolCallId,
|
|
2137
|
+
r.entryId,
|
|
2138
|
+
r.timestamp,
|
|
2139
|
+
r.seq,
|
|
2140
|
+
r.toolName,
|
|
2141
|
+
r.callsInTurn,
|
|
2142
|
+
r.target,
|
|
2143
|
+
r.edits ? JSON.stringify(r.edits) : null,
|
|
2144
|
+
r.argTokens ? JSON.stringify(r.argTokens) : null,
|
|
2145
|
+
],
|
|
2146
|
+
);
|
|
2147
|
+
}
|
|
2148
|
+
return writer.finish();
|
|
2149
|
+
});
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
/** Attach result size and named artifacts to recorded calls (results can land in a later tail parse). */
|
|
2153
|
+
export function updateSpotlightCallResults(links: Iterable<SpotlightCallResultLink>): number {
|
|
2154
|
+
if (!db) return 0;
|
|
2155
|
+
const stmt = db.query(`
|
|
2156
|
+
UPDATE spotlight_calls SET result_chars = ?, artifacts = ?
|
|
2157
|
+
WHERE session_file = ? AND tool_call_id = ? AND result_chars IS NULL
|
|
2158
|
+
`);
|
|
2159
|
+
let changes = 0;
|
|
2160
|
+
atomically(db, () => {
|
|
2161
|
+
for (const l of links) {
|
|
2162
|
+
changes += stmt.run(l.resultChars, JSON.stringify(l.artifacts), l.sessionFile, l.toolCallId).changes;
|
|
2163
|
+
}
|
|
2164
|
+
});
|
|
2165
|
+
return changes;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
/** Insert experiment-session main-loop requests; fork copies are skipped as for `messages`. */
|
|
2169
|
+
export function insertSpotlightRequests(rows: Iterable<SpotlightRequestRow>): number {
|
|
2170
|
+
if (!db) return 0;
|
|
2171
|
+
const writer = new ChunkedInsert(db, {
|
|
2172
|
+
verb: "INSERT OR IGNORE",
|
|
2173
|
+
table: "spotlight_requests",
|
|
2174
|
+
columns: ["session_file", "entry_id", "timestamp"],
|
|
2175
|
+
identity: ["entry_id", "timestamp"],
|
|
2176
|
+
});
|
|
2177
|
+
return atomically(db, () => {
|
|
2178
|
+
for (const r of rows)
|
|
2179
|
+
writer.add(r.sessionFile, [r.entryId, r.timestamp], [r.sessionFile, r.entryId, r.timestamp]);
|
|
2180
|
+
return writer.finish();
|
|
2181
|
+
});
|
|
2182
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -23,7 +23,16 @@ export { refreshRollups } from "./rollup";
|
|
|
23
23
|
export type { StatsJudge, StatsJudgeProvider } from "./frustration";
|
|
24
24
|
export { getGainDashboardStats } from "./gain-aggregator";
|
|
25
25
|
export { formatStatsDashboardUrl, type StartServerOptions, setStatsCloseHandler, startServer } from "./server";
|
|
26
|
-
export type {
|
|
26
|
+
export type {
|
|
27
|
+
GainDashboardStats,
|
|
28
|
+
GainSource,
|
|
29
|
+
GainSourceTotals,
|
|
30
|
+
GainTimeSeriesPoint,
|
|
31
|
+
SpotlightAgentScope,
|
|
32
|
+
SpotlightDashboardStats,
|
|
33
|
+
SpotlightSessionContext,
|
|
34
|
+
} from "./shared-types";
|
|
35
|
+
export { getSpotlightDashboardStats, getSpotlightSessionContext } from "./spotlight-aggregator";
|
|
27
36
|
export type {
|
|
28
37
|
AggregatedStats,
|
|
29
38
|
DashboardStats,
|
package/src/live.ts
CHANGED
|
@@ -25,6 +25,11 @@ const WATCH_DEBOUNCE_MS = 800;
|
|
|
25
25
|
const FULL_SYNC_INTERVAL_MS = 5 * 60 * 1000;
|
|
26
26
|
/** Minimum spacing of `version` bumps; each bump makes open pages refetch. */
|
|
27
27
|
const VERSION_THROTTLE_MS = 1000;
|
|
28
|
+
/**
|
|
29
|
+
* The same while a full sync runs. Pages rebuild on the thread that ingests, so a catch-up over many transcripts would
|
|
30
|
+
* otherwise spend most of its time serving refetches; the held-back bump lands when the sync ends.
|
|
31
|
+
*/
|
|
32
|
+
const SYNCING_VERSION_THROTTLE_MS = 15_000;
|
|
28
33
|
/** Delay before retrying a failed sync (typically a lock held by another omp process). */
|
|
29
34
|
const SYNC_RETRY_MS = 10_000;
|
|
30
35
|
/** Minimum spacing of progress-only status events. */
|
|
@@ -169,6 +174,8 @@ export class StatsLive {
|
|
|
169
174
|
}
|
|
170
175
|
// Another process may have ingested too; its commits move SQLite's data_version.
|
|
171
176
|
if (this.#externalWrites()) this.#changed();
|
|
177
|
+
// A full sync held its last bump back: show its data now rather than at the end of the throttle.
|
|
178
|
+
if (visible && this.#versionTimer) this.#advanceVersion();
|
|
172
179
|
this.#emit();
|
|
173
180
|
}
|
|
174
181
|
|
|
@@ -187,17 +194,22 @@ export class StatsLive {
|
|
|
187
194
|
this.#bumpVersion();
|
|
188
195
|
}
|
|
189
196
|
|
|
190
|
-
/** Bump the data version (throttled), making open pages refetch. */
|
|
197
|
+
/** Bump the data version (throttled, more so during a full sync), making open pages refetch. */
|
|
191
198
|
#bumpVersion(): void {
|
|
192
199
|
if (this.#versionTimer) return;
|
|
193
|
-
const
|
|
194
|
-
this.#
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
200
|
+
const spacing = this.#sync.phase === "syncing" ? SYNCING_VERSION_THROTTLE_MS : VERSION_THROTTLE_MS;
|
|
201
|
+
const wait = Math.max(0, this.#lastVersionAt + spacing - Date.now());
|
|
202
|
+
this.#versionTimer = setTimeout(() => this.#advanceVersion(), wait);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Bump the data version now, cancelling a pending throttled bump. */
|
|
206
|
+
#advanceVersion(): void {
|
|
207
|
+
clearTimeout(this.#versionTimer ?? undefined);
|
|
208
|
+
this.#versionTimer = null;
|
|
209
|
+
this.#lastVersionAt = Date.now();
|
|
210
|
+
this.#version++;
|
|
211
|
+
this.#indexingHours = getRollupStatus().dirtyHours;
|
|
212
|
+
this.#emit();
|
|
201
213
|
}
|
|
202
214
|
|
|
203
215
|
async #refresh(): Promise<void> {
|