@swestash/agent-lens 0.14.1 → 0.15.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/dist/agent-lens.js +309 -69
- package/dist/agent-lens.js.map +1 -1
- package/package.json +2 -2
- package/web/404.html +2 -2
- package/web/assets/{AboutView-D0_Xm4fa.js → AboutView-BD9cfwC2.js} +1 -1
- package/web/assets/Dashboard-fakmS_H4.js +69 -0
- package/web/assets/{FileView-CnV33MsU.js → FileView-1MkgxkzO.js} +1 -1
- package/web/assets/{FilesView-DrgUr_wc.js → FilesView-B6_w8jKK.js} +1 -1
- package/web/assets/{SecurityView-1SY1h-hB.js → SecurityView-BX-IzE--.js} +1 -1
- package/web/assets/SessionView-DY5Xm26K.js +15 -0
- package/web/assets/{SkillView-CIPe79vU.js → SkillView-CnOhNPN4.js} +1 -1
- package/web/assets/SkillsView-C76l9UYR.js +1 -0
- package/web/assets/{WorkflowView-BXLb1lp4.js → WorkflowView-Dq9JYCHd.js} +1 -1
- package/web/assets/index-B6dWnwqZ.css +1 -0
- package/web/assets/{index-Xe7pj757.js → index-CcEbkJx7.js} +1 -1
- package/web/assets/index-CvMppG1z.js +69 -0
- package/web/assets/{jsonish-CLYQ4tku.js → jsonish-DMAKObPI.js} +1 -1
- package/web/assets/theme-ByPiBqSS.js +1 -0
- package/web/assets/{useResetOn-CpMZQynn.js → useResetOn-BZWxJ_cM.js} +1 -1
- package/web/index.html +2 -2
- package/web/assets/Dashboard-ttxHcQ4v.js +0 -69
- package/web/assets/SessionView-DxxaR5_n.js +0 -15
- package/web/assets/SkillsView-YuBaj_4P.js +0 -1
- package/web/assets/index-BNr_gy9h.css +0 -1
- package/web/assets/index-CNg3TICd.js +0 -69
package/dist/agent-lens.js
CHANGED
|
@@ -1363,6 +1363,10 @@ function rateForModel(model) {
|
|
|
1363
1363
|
}
|
|
1364
1364
|
return best?.rate ?? null;
|
|
1365
1365
|
}
|
|
1366
|
+
var DATED_SNAPSHOT = /-\d{8}$/;
|
|
1367
|
+
function canonicalModel(model) {
|
|
1368
|
+
return model && DATED_SNAPSHOT.test(model) ? model.slice(0, -9) : model;
|
|
1369
|
+
}
|
|
1366
1370
|
function costForUsage(model, u) {
|
|
1367
1371
|
const r = rateForModel(model);
|
|
1368
1372
|
if (!r)
|
|
@@ -4568,6 +4572,10 @@ function pushDateRange(clauses, params, col, from, to) {
|
|
|
4568
4572
|
params.push(to);
|
|
4569
4573
|
}
|
|
4570
4574
|
}
|
|
4575
|
+
function canonModelSql(col) {
|
|
4576
|
+
return `CASE WHEN ${col} GLOB '*-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]'
|
|
4577
|
+
THEN substr(${col}, 1, length(${col}) - 9) ELSE ${col} END`;
|
|
4578
|
+
}
|
|
4571
4579
|
function metaJoin(hasMeta) {
|
|
4572
4580
|
return hasMeta ? "LEFT JOIN session_meta sm ON sm.session_id = s.id" : "";
|
|
4573
4581
|
}
|
|
@@ -4657,7 +4665,8 @@ function listProjects(db) {
|
|
|
4657
4665
|
FROM projects p ORDER BY session_count DESC, p.path`);
|
|
4658
4666
|
}
|
|
4659
4667
|
function listModels(db) {
|
|
4660
|
-
return queryAll(db, `SELECT DISTINCT
|
|
4668
|
+
return queryAll(db, `SELECT DISTINCT ${canonModelSql("m")} AS model
|
|
4669
|
+
FROM (SELECT DISTINCT model m FROM token_usage WHERE model IS NOT NULL) ORDER BY 1`).map((r) => r.model);
|
|
4661
4670
|
}
|
|
4662
4671
|
function toFtsQuery(raw) {
|
|
4663
4672
|
return raw.trim().split(/\s+/).filter(Boolean).map((t) => `"${t.replace(/"/g, '""')}"`).join(" ");
|
|
@@ -4709,8 +4718,8 @@ function listSessions(db, f) {
|
|
|
4709
4718
|
params.push(...f.errorType);
|
|
4710
4719
|
}
|
|
4711
4720
|
if (f.model) {
|
|
4712
|
-
where.push(
|
|
4713
|
-
params.push(f.model);
|
|
4721
|
+
where.push(`EXISTS (SELECT 1 FROM token_usage t WHERE t.session_id = s.id AND ${canonModelSql("t.model")} = ?)`);
|
|
4722
|
+
params.push(canonicalModel(f.model));
|
|
4714
4723
|
}
|
|
4715
4724
|
if (f.q && f.q.trim()) {
|
|
4716
4725
|
const like = `%${f.q.trim()}%`;
|
|
@@ -4723,7 +4732,7 @@ function listSessions(db, f) {
|
|
|
4723
4732
|
const total = queryGet(db, `SELECT COUNT(*) n FROM sessions s ${whereSql}`, ...params).n;
|
|
4724
4733
|
const baseSelect = `SELECT s.id, s.ai_title, s.slug, s.source_id, s.is_sidechain, s.started_at,
|
|
4725
4734
|
s.duration_ms, s.event_count, s.turn_count, p.path AS project_path,
|
|
4726
|
-
(SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models,
|
|
4735
|
+
(SELECT GROUP_CONCAT(DISTINCT ${canonModelSql("model")}) FROM token_usage t WHERE t.session_id = s.id) AS models,
|
|
4727
4736
|
(SELECT COUNT(*) FROM tool_calls tc WHERE tc.session_id = s.id) AS tool_call_count,
|
|
4728
4737
|
(SELECT COUNT(*) FROM tool_calls tc WHERE tc.session_id = s.id AND tc.status = 'error') AS tool_error_count,
|
|
4729
4738
|
(SELECT COUNT(*) FROM findings fd WHERE fd.session_id = s.id) AS finding_count,
|
|
@@ -4827,8 +4836,51 @@ function loadFindings(db, id, toolRows) {
|
|
|
4827
4836
|
function toResponseToolCall({ event_uuid: _e, error_type: _t, ...call }) {
|
|
4828
4837
|
return call;
|
|
4829
4838
|
}
|
|
4839
|
+
function rendersSomething(e, toolsByEvent) {
|
|
4840
|
+
return !!(e.text || e.thinking || toolsByEvent.get(e.uuid)?.length);
|
|
4841
|
+
}
|
|
4842
|
+
function addSplit(a, b) {
|
|
4843
|
+
if (!b)
|
|
4844
|
+
return a;
|
|
4845
|
+
if (!a)
|
|
4846
|
+
return { ...b };
|
|
4847
|
+
return {
|
|
4848
|
+
input: a.input + b.input,
|
|
4849
|
+
output: a.output + b.output,
|
|
4850
|
+
cache_creation: a.cache_creation + b.cache_creation,
|
|
4851
|
+
cache_read: a.cache_read + b.cache_read
|
|
4852
|
+
};
|
|
4853
|
+
}
|
|
4854
|
+
function foldUsage(eventRows, toolsByEvent, usageByEvent) {
|
|
4855
|
+
const out = /* @__PURE__ */ new Map();
|
|
4856
|
+
if (usageByEvent.size === 0)
|
|
4857
|
+
return out;
|
|
4858
|
+
const byTurn = /* @__PURE__ */ new Map();
|
|
4859
|
+
for (const e of eventRows)
|
|
4860
|
+
pushGrouped(byTurn, e.turn_id ?? "", e);
|
|
4861
|
+
for (const group of byTurn.values()) {
|
|
4862
|
+
const ordered = [...group].sort((a, b) => a.seq - b.seq);
|
|
4863
|
+
let pending = null;
|
|
4864
|
+
let lastRendering = null;
|
|
4865
|
+
for (const e of ordered) {
|
|
4866
|
+
const own = usageByEvent.get(e.uuid);
|
|
4867
|
+
if (rendersSomething(e, toolsByEvent)) {
|
|
4868
|
+
const total = addSplit(pending, own);
|
|
4869
|
+
if (total)
|
|
4870
|
+
out.set(e.uuid, total);
|
|
4871
|
+
pending = null;
|
|
4872
|
+
lastRendering = e.uuid;
|
|
4873
|
+
} else {
|
|
4874
|
+
pending = addSplit(pending, own);
|
|
4875
|
+
}
|
|
4876
|
+
}
|
|
4877
|
+
if (pending && lastRendering)
|
|
4878
|
+
out.set(lastRendering, addSplit(out.get(lastRendering) ?? null, pending));
|
|
4879
|
+
}
|
|
4880
|
+
return out;
|
|
4881
|
+
}
|
|
4830
4882
|
function loadEvents(db, id, toolRows) {
|
|
4831
|
-
const eventRows = queryAll(db, `SELECT uuid, type, role, timestamp, model, is_sidechain, turn_id, text, thinking
|
|
4883
|
+
const eventRows = queryAll(db, `SELECT uuid, seq, type, role, timestamp, model, is_sidechain, turn_id, text, thinking
|
|
4832
4884
|
FROM events WHERE session_id = ? ORDER BY timestamp, seq`, id);
|
|
4833
4885
|
const toolsByEvent = /* @__PURE__ */ new Map();
|
|
4834
4886
|
for (const t of toolRows) {
|
|
@@ -4836,18 +4888,30 @@ function loadEvents(db, id, toolRows) {
|
|
|
4836
4888
|
continue;
|
|
4837
4889
|
pushGrouped(toolsByEvent, t.event_uuid, t);
|
|
4838
4890
|
}
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4891
|
+
const usageByEvent = /* @__PURE__ */ new Map();
|
|
4892
|
+
for (const u of queryAll(db, `SELECT event_uuid, input_tokens i, output_tokens o, cache_creation_input_tokens cw,
|
|
4893
|
+
cache_read_input_tokens cr
|
|
4894
|
+
FROM token_usage WHERE session_id = ?`, id)) {
|
|
4895
|
+
usageByEvent.set(u.event_uuid, { input: u.i, output: u.o, cache_creation: u.cw, cache_read: u.cr });
|
|
4896
|
+
}
|
|
4897
|
+
const folded = foldUsage(eventRows, toolsByEvent, usageByEvent);
|
|
4898
|
+
return eventRows.map((e) => {
|
|
4899
|
+
const usage = folded.get(e.uuid);
|
|
4900
|
+
return {
|
|
4901
|
+
uuid: e.uuid,
|
|
4902
|
+
type: e.type,
|
|
4903
|
+
role: e.role,
|
|
4904
|
+
timestamp: e.timestamp,
|
|
4905
|
+
model: e.model,
|
|
4906
|
+
is_sidechain: e.is_sidechain,
|
|
4907
|
+
turn_id: e.turn_id,
|
|
4908
|
+
text: e.text,
|
|
4909
|
+
thinking: e.thinking,
|
|
4910
|
+
toolCalls: (toolsByEvent.get(e.uuid) ?? []).map(toResponseToolCall),
|
|
4911
|
+
// Spread so the key is ABSENT, not null, on the majority of events that carry no usage row.
|
|
4912
|
+
...usage ? { usage } : {}
|
|
4913
|
+
};
|
|
4914
|
+
});
|
|
4851
4915
|
}
|
|
4852
4916
|
function loadClassification(db, id) {
|
|
4853
4917
|
const cls = queryGet(db, `SELECT category, complexity_score, complexity_band, signals_json, classifier_version
|
|
@@ -4878,7 +4942,7 @@ function loadParent(db, session) {
|
|
|
4878
4942
|
function loadChildren(db, id) {
|
|
4879
4943
|
const hasMeta = tableExists(db, "session_meta");
|
|
4880
4944
|
const children = queryAll(db, `SELECT s.id, s.ai_title, s.slug, s.turn_count, s.started_at, s.workflow_run_id,
|
|
4881
|
-
(SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models
|
|
4945
|
+
(SELECT GROUP_CONCAT(DISTINCT ${canonModelSql("model")}) FROM token_usage t WHERE t.session_id = s.id) AS models
|
|
4882
4946
|
${metaProjection(hasMeta)}
|
|
4883
4947
|
FROM sessions s ${metaJoin(hasMeta)}
|
|
4884
4948
|
WHERE s.parent_session_id = ? ORDER BY s.started_at`, id);
|
|
@@ -4954,7 +5018,7 @@ function safeJson(s) {
|
|
|
4954
5018
|
function loadAgents(db, runId) {
|
|
4955
5019
|
const hasMeta = tableExists(db, "session_meta");
|
|
4956
5020
|
const agentRows = queryAll(db, `SELECT s.id, s.ai_title, s.slug, s.turn_count, s.started_at, s.ended_at, s.duration_ms,
|
|
4957
|
-
(SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models
|
|
5021
|
+
(SELECT GROUP_CONCAT(DISTINCT ${canonModelSql("model")}) FROM token_usage t WHERE t.session_id = s.id) AS models
|
|
4958
5022
|
${metaProjection(hasMeta)}
|
|
4959
5023
|
FROM sessions s ${metaJoin(hasMeta)}
|
|
4960
5024
|
WHERE s.workflow_run_id = ? ORDER BY s.started_at`, runId);
|
|
@@ -5348,8 +5412,24 @@ function sessionWhere(f) {
|
|
|
5348
5412
|
if (f.source)
|
|
5349
5413
|
where.push("s.source_id = ?"), params.push(f.source);
|
|
5350
5414
|
pushDateRange(where, params, "s.started_at", f.from, f.to);
|
|
5351
|
-
|
|
5352
|
-
|
|
5415
|
+
const models = f.models?.length ? [...new Set(f.models.map((m) => canonicalModel(m)))] : void 0;
|
|
5416
|
+
if (models) {
|
|
5417
|
+
where.push(`EXISTS (SELECT 1 FROM token_usage t WHERE t.session_id = s.id AND ${canonModelSql("t.model")} IN (${placeholders(models)}))`);
|
|
5418
|
+
params.push(...models);
|
|
5419
|
+
}
|
|
5420
|
+
return { sql: where.length ? `WHERE ${where.join(" AND ")}` : "", params, models };
|
|
5421
|
+
}
|
|
5422
|
+
var placeholders = (xs) => xs.map(() => "?").join(",");
|
|
5423
|
+
var and = (w, sql, params = []) => ({
|
|
5424
|
+
sql: `${andWhere(w)} ${sql}`,
|
|
5425
|
+
params: [...w.params, ...params],
|
|
5426
|
+
models: w.models
|
|
5427
|
+
});
|
|
5428
|
+
var withModel = (w, col) => w.models ? and(w, `${canonModelSql(col)} IN (${placeholders(w.models)})`, w.models) : w;
|
|
5429
|
+
var toolModelJoin = (w) => w.models ? "JOIN events e ON e.uuid = tc.event_uuid" : "";
|
|
5430
|
+
var eventJoin = "JOIN events e ON e.uuid = tc.event_uuid";
|
|
5431
|
+
var andWhere = (w) => w.sql ? w.sql + " AND" : "WHERE";
|
|
5432
|
+
var mainOnly = (w) => ({ sql: `${andWhere(w)} s.is_sidechain = 0`, params: w.params, models: w.models });
|
|
5353
5433
|
var zeroSplit = () => ({ input: 0, output: 0, cache_creation: 0, cache_read: 0 });
|
|
5354
5434
|
function addUsage(acc, r) {
|
|
5355
5435
|
acc.input += r.i ?? 0;
|
|
@@ -5426,11 +5506,13 @@ function dashboardOverview(db, f) {
|
|
|
5426
5506
|
SUM(turn_count) turns,
|
|
5427
5507
|
COUNT(DISTINCT project_id) projects
|
|
5428
5508
|
FROM sessions s ${w.sql}`, ...w.params);
|
|
5429
|
-
const
|
|
5509
|
+
const tw = withModel(w, "e.model");
|
|
5510
|
+
const toolCount = queryGet(db, `SELECT COUNT(*) n FROM tool_calls tc ${toolModelJoin(w)} JOIN sessions s ON s.id = tc.session_id ${tw.sql}`, ...tw.params).n;
|
|
5511
|
+
const uw = withModel(w, "t.model");
|
|
5430
5512
|
const usage = queryAll(db, `SELECT t.model model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,
|
|
5431
5513
|
SUM(t.cache_creation_input_tokens) cw, SUM(t.cache_read_input_tokens) cr
|
|
5432
|
-
FROM token_usage t JOIN sessions s ON s.id = t.session_id ${
|
|
5433
|
-
GROUP BY t.model`, ...
|
|
5514
|
+
FROM token_usage t JOIN sessions s ON s.id = t.session_id ${uw.sql}
|
|
5515
|
+
GROUP BY t.model`, ...uw.params);
|
|
5434
5516
|
const tokens = zeroSplit();
|
|
5435
5517
|
let cost = 0;
|
|
5436
5518
|
const unpriced2 = [];
|
|
@@ -5441,16 +5523,20 @@ function dashboardOverview(db, f) {
|
|
|
5441
5523
|
unpriced2.push(u.model);
|
|
5442
5524
|
}
|
|
5443
5525
|
const totalTokens = tokens.input + tokens.output + tokens.cache_creation + tokens.cache_read;
|
|
5526
|
+
const tnw = withModel(w, "tn.model");
|
|
5527
|
+
const turns = tnw.models ? queryGet(db, `SELECT COUNT(*) n FROM turns tn JOIN sessions s ON s.id = tn.session_id ${tnw.sql}`, ...tnw.params)?.n ?? 0 : counts.turns ?? 0;
|
|
5444
5528
|
const turnDur = percentiles(db, `SELECT tn.duration_ms v FROM turns tn JOIN sessions s ON s.id = tn.session_id
|
|
5445
|
-
${
|
|
5446
|
-
const
|
|
5447
|
-
|
|
5529
|
+
${andWhere(tnw)} tn.duration_ms IS NOT NULL`, tnw.params);
|
|
5530
|
+
const mw = mainOnly(w);
|
|
5531
|
+
const sessDur = percentiles(db, `SELECT s.duration_ms v FROM sessions s ${mw.sql} AND s.duration_ms IS NOT NULL`, mw.params);
|
|
5448
5532
|
return {
|
|
5449
|
-
|
|
5533
|
+
// `w.models`, not `f.models`: the echo names the models actually queried, so a link that arrived
|
|
5534
|
+
// with a raw dated id reports the family key it resolved to rather than the string it was sent.
|
|
5535
|
+
range: { from: f.from ?? null, to: f.to ?? null, source: f.source ?? null, models: w.models ?? null },
|
|
5450
5536
|
sessions: counts.sessions ?? 0,
|
|
5451
5537
|
sessions_main: counts.main ?? 0,
|
|
5452
5538
|
sessions_subagent: counts.subagent ?? 0,
|
|
5453
|
-
turns
|
|
5539
|
+
turns,
|
|
5454
5540
|
projects: counts.projects ?? 0,
|
|
5455
5541
|
tool_calls: toolCount ?? 0,
|
|
5456
5542
|
tokens,
|
|
@@ -5476,15 +5562,11 @@ function chooseBucket(db, f) {
|
|
|
5476
5562
|
return "week";
|
|
5477
5563
|
return "month";
|
|
5478
5564
|
}
|
|
5479
|
-
var
|
|
5480
|
-
day: "strftime('%Y-%m-%d', s.started_at)",
|
|
5481
|
-
week: "strftime('%Y-W%W', s.started_at)",
|
|
5482
|
-
month: "strftime('%Y-%m', s.started_at)"
|
|
5483
|
-
};
|
|
5565
|
+
var bucketExpr = (b, col) => b === "day" ? `strftime('%Y-%m-%d', ${col})` : b === "week" ? `strftime('%Y-W%W', ${col})` : `strftime('%Y-%m', ${col})`;
|
|
5484
5566
|
function dashboardTimeseries(db, f, bucketParam) {
|
|
5485
5567
|
const bucket = bucketParam === "day" || bucketParam === "week" || bucketParam === "month" ? bucketParam : chooseBucket(db, f);
|
|
5486
5568
|
const w = sessionWhere(f);
|
|
5487
|
-
const expr =
|
|
5569
|
+
const expr = bucketExpr(bucket, "s.started_at");
|
|
5488
5570
|
const byBucket = /* @__PURE__ */ new Map();
|
|
5489
5571
|
const get = (b) => {
|
|
5490
5572
|
let r = byBucket.get(b);
|
|
@@ -5494,10 +5576,13 @@ function dashboardTimeseries(db, f, bucketParam) {
|
|
|
5494
5576
|
}
|
|
5495
5577
|
return r;
|
|
5496
5578
|
};
|
|
5579
|
+
const uw = withModel(w, "t.model");
|
|
5580
|
+
const tnw = withModel(w, "tn.model");
|
|
5581
|
+
const ew = withModel(w, "e.model");
|
|
5497
5582
|
for (const u of queryAll(db, `SELECT ${expr} b, t.model model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,
|
|
5498
5583
|
SUM(t.cache_creation_input_tokens) cw, SUM(t.cache_read_input_tokens) cr
|
|
5499
5584
|
FROM token_usage t JOIN sessions s ON s.id = t.session_id
|
|
5500
|
-
${
|
|
5585
|
+
${uw.sql} GROUP BY b, t.model`, ...uw.params)) {
|
|
5501
5586
|
if (!u.b)
|
|
5502
5587
|
continue;
|
|
5503
5588
|
const r = get(u.b);
|
|
@@ -5508,12 +5593,12 @@ function dashboardTimeseries(db, f, bucketParam) {
|
|
|
5508
5593
|
if (s.b)
|
|
5509
5594
|
get(s.b).sessions = s.n;
|
|
5510
5595
|
}
|
|
5511
|
-
for (const t of queryAll(db, `SELECT ${expr} b, COUNT(*) n FROM turns tn JOIN sessions s ON s.id = tn.session_id ${
|
|
5596
|
+
for (const t of queryAll(db, `SELECT ${expr} b, COUNT(*) n FROM turns tn JOIN sessions s ON s.id = tn.session_id ${tnw.sql} GROUP BY b`, ...tnw.params)) {
|
|
5512
5597
|
if (t.b)
|
|
5513
5598
|
get(t.b).turns = t.n;
|
|
5514
5599
|
}
|
|
5515
|
-
for (const e of queryAll(db, `SELECT ${expr} b, tc.error_type et, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
|
|
5516
|
-
${
|
|
5600
|
+
for (const e of queryAll(db, `SELECT ${expr} b, tc.error_type et, COUNT(*) n FROM tool_calls tc ${toolModelJoin(w)} JOIN sessions s ON s.id = tc.session_id
|
|
5601
|
+
${andWhere(ew)} tc.status = 'error' GROUP BY b, tc.error_type`, ...ew.params)) {
|
|
5517
5602
|
if (!e.b)
|
|
5518
5603
|
continue;
|
|
5519
5604
|
const r = get(e.b);
|
|
@@ -5527,35 +5612,40 @@ function dashboardTimeseries(db, f, bucketParam) {
|
|
|
5527
5612
|
r.cost = Number(r.cost.toFixed(4));
|
|
5528
5613
|
return { bucket, series };
|
|
5529
5614
|
}
|
|
5530
|
-
var andWhere = (w) => w.sql ? w.sql + " AND" : "WHERE";
|
|
5531
5615
|
function modelBreakdown(db, w) {
|
|
5532
|
-
const
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
// nullable and this GROUP BY — unlike the model FILTER list — does not exclude nulls, so a
|
|
5536
|
-
// null-model bucket reaches the chart. It used to render as a nameless bar.
|
|
5537
|
-
`SELECT COALESCE(t.model, '(unknown)') model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,
|
|
5616
|
+
const uw = withModel(w, "t.model");
|
|
5617
|
+
const canon = canonModelSql("t.model");
|
|
5618
|
+
const modelRows = queryAll(db, `SELECT COALESCE(${canon}, '(unknown)') model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,
|
|
5538
5619
|
SUM(t.cache_creation_input_tokens) cw, SUM(t.cache_read_input_tokens) cr,
|
|
5539
5620
|
COUNT(DISTINCT t.session_id) sessions
|
|
5540
|
-
FROM token_usage t JOIN sessions s ON s.id = t.session_id ${
|
|
5541
|
-
GROUP BY
|
|
5542
|
-
|
|
5543
|
-
);
|
|
5621
|
+
FROM token_usage t JOIN sessions s ON s.id = t.session_id ${uw.sql}
|
|
5622
|
+
GROUP BY 1 ORDER BY (SUM(t.input_tokens)+SUM(t.output_tokens)+SUM(t.cache_creation_input_tokens)+SUM(t.cache_read_input_tokens)) DESC`, ...uw.params);
|
|
5623
|
+
const cost = /* @__PURE__ */ new Map();
|
|
5624
|
+
const unpriced2 = /* @__PURE__ */ new Set();
|
|
5625
|
+
for (const u of queryAll(db, `SELECT COALESCE(t.model, '(unknown)') model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,
|
|
5626
|
+
SUM(t.cache_creation_input_tokens) cw, SUM(t.cache_read_input_tokens) cr
|
|
5627
|
+
FROM token_usage t JOIN sessions s ON s.id = t.session_id ${uw.sql} GROUP BY t.model`, ...uw.params)) {
|
|
5628
|
+
const key = canonicalModel(u.model);
|
|
5629
|
+
cost.set(key, (cost.get(key) ?? 0) + costForUsage(u.model, usageForCost(u)));
|
|
5630
|
+
if (!rateForModel(u.model))
|
|
5631
|
+
unpriced2.add(key);
|
|
5632
|
+
}
|
|
5544
5633
|
return modelRows.map((u) => ({
|
|
5545
5634
|
model: u.model,
|
|
5546
5635
|
tokens: { input: u.i ?? 0, output: u.o ?? 0, cache_creation: u.cw ?? 0, cache_read: u.cr ?? 0 },
|
|
5547
5636
|
total_tokens: (u.i ?? 0) + (u.o ?? 0) + (u.cw ?? 0) + (u.cr ?? 0),
|
|
5548
|
-
cost: Number(
|
|
5637
|
+
cost: Number((cost.get(u.model) ?? 0).toFixed(4)),
|
|
5549
5638
|
sessions: u.sessions ?? 0,
|
|
5550
|
-
priced:
|
|
5639
|
+
priced: !unpriced2.has(u.model)
|
|
5551
5640
|
}));
|
|
5552
5641
|
}
|
|
5553
5642
|
function sourceBreakdown(db, w) {
|
|
5554
|
-
|
|
5555
|
-
|
|
5643
|
+
const perSourceTurns = w.models ? `SUM((SELECT COUNT(*) FROM turns tn WHERE tn.session_id = s.id AND ${canonModelSql("tn.model")} IN (${placeholders(w.models)})))` : "SUM(s.turn_count)";
|
|
5644
|
+
return queryAll(db, `SELECT COALESCE(s.source_id, '(none)') source, COUNT(*) sessions, ${perSourceTurns} turns
|
|
5645
|
+
FROM sessions s ${w.sql} GROUP BY s.source_id ORDER BY sessions DESC`, ...w.models ?? [], ...w.params);
|
|
5556
5646
|
}
|
|
5557
5647
|
function classificationBreakdowns(db, w) {
|
|
5558
|
-
const mainW =
|
|
5648
|
+
const mainW = mainOnly(w);
|
|
5559
5649
|
const byCategory = queryAll(db, `SELECT c.category, COUNT(*) n FROM classifications c JOIN sessions s ON s.id = c.target_id
|
|
5560
5650
|
${mainW.sql} GROUP BY c.category ORDER BY n DESC`, ...mainW.params);
|
|
5561
5651
|
const byComplexity = queryAll(db, `SELECT c.complexity_band band, COUNT(*) n FROM classifications c JOIN sessions s ON s.id = c.target_id
|
|
@@ -5563,26 +5653,30 @@ function classificationBreakdowns(db, w) {
|
|
|
5563
5653
|
return { byCategory, byComplexity };
|
|
5564
5654
|
}
|
|
5565
5655
|
function toolBreakdown(db, w) {
|
|
5566
|
-
|
|
5567
|
-
|
|
5656
|
+
const ew = withModel(w, "e.model");
|
|
5657
|
+
return queryAll(db, `SELECT tc.tool_name name, COUNT(*) n FROM tool_calls tc ${toolModelJoin(w)} JOIN sessions s ON s.id = tc.session_id
|
|
5658
|
+
${ew.sql} GROUP BY tc.tool_name ORDER BY n DESC LIMIT 20`, ...ew.params);
|
|
5568
5659
|
}
|
|
5569
5660
|
function skillBreakdowns(db, w) {
|
|
5570
|
-
const
|
|
5571
|
-
|
|
5661
|
+
const ew = withModel(w, "e.model");
|
|
5662
|
+
const skills = queryAll(db, `SELECT tc.skill_name name, COUNT(*) n FROM tool_calls tc ${toolModelJoin(w)} JOIN sessions s ON s.id = tc.session_id
|
|
5663
|
+
${andWhere(ew)} tc.skill_name IS NOT NULL GROUP BY tc.skill_name ORDER BY n DESC LIMIT 20`, ...ew.params);
|
|
5572
5664
|
const skillVersions = queryAll(db, `SELECT tc.skill_name name, sk.id version_id, sk.summary, sk.last_seen, COUNT(*) n
|
|
5573
5665
|
FROM tool_calls tc
|
|
5666
|
+
${toolModelJoin(w)}
|
|
5574
5667
|
JOIN skills sk ON sk.id = tc.skill_id
|
|
5575
5668
|
JOIN sessions s ON s.id = tc.session_id
|
|
5576
|
-
${andWhere(
|
|
5577
|
-
GROUP BY tc.skill_name, sk.id ORDER BY name, n DESC`, ...
|
|
5669
|
+
${andWhere(ew)} tc.skill_id IS NOT NULL
|
|
5670
|
+
GROUP BY tc.skill_name, sk.id ORDER BY name, n DESC`, ...ew.params);
|
|
5578
5671
|
return { skills, skillVersions };
|
|
5579
5672
|
}
|
|
5580
5673
|
function subagentFanoutBreakdown(db, w) {
|
|
5581
|
-
const
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5674
|
+
const ew = withModel(w, "e.model");
|
|
5675
|
+
const subagentByType = queryAll(db, `SELECT tc.agent_type type, COUNT(*) n FROM tool_calls tc ${toolModelJoin(w)} JOIN sessions s ON s.id = tc.session_id
|
|
5676
|
+
${andWhere(ew)} tc.agent_type IS NOT NULL GROUP BY tc.agent_type ORDER BY n DESC`, ...ew.params);
|
|
5677
|
+
const perSession = queryAll(db, `SELECT COUNT(*) n FROM tool_calls tc ${toolModelJoin(w)} JOIN sessions s ON s.id = tc.session_id
|
|
5678
|
+
${mainOnly(ew).sql} AND tc.tool_name IN ('Agent','Task')
|
|
5679
|
+
GROUP BY tc.session_id`, ...ew.params);
|
|
5586
5680
|
const counts = perSession.map((r) => r.n);
|
|
5587
5681
|
return {
|
|
5588
5682
|
by_type: subagentByType,
|
|
@@ -5593,8 +5687,9 @@ function subagentFanoutBreakdown(db, w) {
|
|
|
5593
5687
|
};
|
|
5594
5688
|
}
|
|
5595
5689
|
function errorTypeBreakdown(db, w) {
|
|
5596
|
-
const
|
|
5597
|
-
|
|
5690
|
+
const ew = withModel(w, "e.model");
|
|
5691
|
+
const errorRows = queryAll(db, `SELECT COALESCE(tc.error_type, 'other') type, COUNT(*) n FROM tool_calls tc ${toolModelJoin(w)} JOIN sessions s ON s.id = tc.session_id
|
|
5692
|
+
${andWhere(ew)} tc.status = 'error' GROUP BY tc.error_type ORDER BY n DESC`, ...ew.params);
|
|
5598
5693
|
let failures = 0;
|
|
5599
5694
|
let rejections = 0;
|
|
5600
5695
|
const byType = errorRows.map((r) => {
|
|
@@ -5623,6 +5718,143 @@ function dashboardBreakdowns(db, f) {
|
|
|
5623
5718
|
error_types: errorTypeBreakdown(db, w)
|
|
5624
5719
|
};
|
|
5625
5720
|
}
|
|
5721
|
+
var bucketFor = (b) => b === "day" || b === "week" || b === "month" ? b : "day";
|
|
5722
|
+
function dashboardTime(db, f, bucketParam) {
|
|
5723
|
+
const w = sessionWhere(f);
|
|
5724
|
+
const mw = mainOnly(w);
|
|
5725
|
+
const uw = withModel(w, "t.model");
|
|
5726
|
+
const burn_hours = queryAll(db, `SELECT strftime('%Y-%m-%dT%H', e.timestamp) h, s.source_id src,
|
|
5727
|
+
SUM(t.input_tokens + t.output_tokens + t.cache_creation_input_tokens) work
|
|
5728
|
+
FROM token_usage t
|
|
5729
|
+
JOIN events e ON e.uuid = t.event_uuid
|
|
5730
|
+
JOIN sessions s ON s.id = t.session_id
|
|
5731
|
+
${uw.sql} GROUP BY h, src ORDER BY h`, ...uw.params).filter((r) => r.h != null).map((r) => ({ hour: r.h, source: r.src, work: r.work ?? 0 }));
|
|
5732
|
+
const tw = withModel(mw, "t.model");
|
|
5733
|
+
const turn_hours = queryAll(db, `SELECT strftime('%Y-%m-%dT%H', t.started_at) h, s.source_id src, COUNT(*) turns
|
|
5734
|
+
FROM turns t
|
|
5735
|
+
JOIN sessions s ON s.id = t.session_id
|
|
5736
|
+
${tw.sql} GROUP BY h, src ORDER BY h`, ...tw.params).filter((r) => r.h != null).map((r) => ({ hour: r.h, source: r.src, turns: r.turns ?? 0 }));
|
|
5737
|
+
return {
|
|
5738
|
+
burn_hours,
|
|
5739
|
+
turn_hours,
|
|
5740
|
+
latency: modelLatency(db, mw, bucketParam ? bucketFor(bucketParam) : chooseBucket(db, f)),
|
|
5741
|
+
review: reviewLatency(db, mw)
|
|
5742
|
+
};
|
|
5743
|
+
}
|
|
5744
|
+
var MIN_LATENCY_SAMPLES = 5;
|
|
5745
|
+
function modelLatency(db, mw, bucket) {
|
|
5746
|
+
const expr = bucketExpr(bucket, "s.started_at");
|
|
5747
|
+
const lw = withModel(mw, "tn.model");
|
|
5748
|
+
const series = queryAll(db, `WITH lat AS MATERIALIZED (
|
|
5749
|
+
SELECT ${expr} b, COALESCE(${canonModelSql("tn.model")}, '(unknown)') model,
|
|
5750
|
+
(julianday(MIN(e.timestamp)) - julianday(tn.started_at)) * 86400000.0 ms
|
|
5751
|
+
FROM turns tn
|
|
5752
|
+
JOIN sessions s ON s.id = tn.session_id
|
|
5753
|
+
JOIN events e ON e.turn_id = tn.id AND e.role = 'assistant'
|
|
5754
|
+
${lw.sql} AND tn.started_at IS NOT NULL AND tn.model IS NOT '<synthetic>'
|
|
5755
|
+
GROUP BY tn.id
|
|
5756
|
+
),
|
|
5757
|
+
ranked AS (
|
|
5758
|
+
SELECT b, model, ms,
|
|
5759
|
+
ROW_NUMBER() OVER (PARTITION BY b, model ORDER BY ms) rn,
|
|
5760
|
+
COUNT(*) OVER (PARTITION BY b, model) c
|
|
5761
|
+
FROM lat WHERE ms IS NOT NULL AND ms >= 0
|
|
5762
|
+
)
|
|
5763
|
+
SELECT b, model, MAX(c) n,
|
|
5764
|
+
MAX(CASE WHEN rn = MAX(1, CAST(ceil(0.5 * c) AS INTEGER)) THEN ms END) p50,
|
|
5765
|
+
MAX(CASE WHEN rn = MAX(1, CAST(ceil(0.9 * c) AS INTEGER)) THEN ms END) p90
|
|
5766
|
+
FROM ranked GROUP BY b, model HAVING MAX(c) >= ${MIN_LATENCY_SAMPLES} ORDER BY b, model`, ...lw.params).filter((r) => r.b != null).map((r) => ({
|
|
5767
|
+
bucket: r.b,
|
|
5768
|
+
model: r.model ?? "(unknown)",
|
|
5769
|
+
p50_ms: Math.round(r.p50 ?? 0),
|
|
5770
|
+
p90_ms: Math.round(r.p90 ?? 0),
|
|
5771
|
+
n: r.n
|
|
5772
|
+
}));
|
|
5773
|
+
return { bucket, series };
|
|
5774
|
+
}
|
|
5775
|
+
function reviewLatency(db, mw) {
|
|
5776
|
+
const empty = () => ({ n: 0, under_10s: 0, under_30s: 0, under_2min: 0 });
|
|
5777
|
+
const out = { wrote: empty(), none: empty() };
|
|
5778
|
+
for (const r of queryAll(
|
|
5779
|
+
db,
|
|
5780
|
+
// `wrote` is resolved against a materialized set of turn ids rather than a correlated EXISTS:
|
|
5781
|
+
// file_changes has no index on turn_id, so the per-turn subquery rescanned the table once per
|
|
5782
|
+
// turn and cost 5.7s on the real corpus against 0.02s for this. Same counts either way.
|
|
5783
|
+
`WITH wrote_turns AS MATERIALIZED (SELECT DISTINCT turn_id FROM file_changes WHERE turn_id IS NOT NULL),
|
|
5784
|
+
turnaround AS MATERIALIZED (
|
|
5785
|
+
SELECT (tn.id IN (SELECT turn_id FROM wrote_turns)) wrote,
|
|
5786
|
+
(julianday(LEAD(tn.started_at) OVER (PARTITION BY tn.session_id ORDER BY tn.seq))
|
|
5787
|
+
- julianday(tn.ended_at)) * 86400000.0 d
|
|
5788
|
+
FROM turns tn JOIN sessions s ON s.id = tn.session_id ${mw.sql}
|
|
5789
|
+
)
|
|
5790
|
+
SELECT wrote, COUNT(*) n,
|
|
5791
|
+
SUM(CASE WHEN d <= 10000 THEN 1 ELSE 0 END) u10,
|
|
5792
|
+
SUM(CASE WHEN d <= 30000 THEN 1 ELSE 0 END) u30,
|
|
5793
|
+
SUM(CASE WHEN d <= 120000 THEN 1 ELSE 0 END) u120
|
|
5794
|
+
FROM turnaround WHERE d IS NOT NULL AND d >= 0 GROUP BY wrote`,
|
|
5795
|
+
...mw.params
|
|
5796
|
+
)) {
|
|
5797
|
+
out[r.wrote ? "wrote" : "none"] = { n: r.n, under_10s: r.u10 ?? 0, under_30s: r.u30 ?? 0, under_2min: r.u120 ?? 0 };
|
|
5798
|
+
}
|
|
5799
|
+
return out;
|
|
5800
|
+
}
|
|
5801
|
+
function dashboardAudit(db, f, bucketParam) {
|
|
5802
|
+
const w = sessionWhere(f);
|
|
5803
|
+
const bucket = bucketParam ? bucketFor(bucketParam) : chooseBucket(db, f);
|
|
5804
|
+
return {
|
|
5805
|
+
bucket,
|
|
5806
|
+
edit_reliability: editReliability(db, w),
|
|
5807
|
+
plan_rejections: planRejections(db, mainOnly(w), bucket),
|
|
5808
|
+
file_rework: fileRework(db, w),
|
|
5809
|
+
findings_over_time: findingsOverTime(db, w, bucket)
|
|
5810
|
+
};
|
|
5811
|
+
}
|
|
5812
|
+
function editReliability(db, w) {
|
|
5813
|
+
const ew = withModel(w, "e.model");
|
|
5814
|
+
return queryAll(db, `SELECT COALESCE(${canonModelSql("e.model")}, '(unknown)') model, COUNT(*) calls,
|
|
5815
|
+
SUM(CASE WHEN tc.status = 'error' THEN 1 ELSE 0 END) errors
|
|
5816
|
+
FROM tool_calls tc ${eventJoin} JOIN sessions s ON s.id = tc.session_id
|
|
5817
|
+
${andWhere(ew)} tc.tool_name IN ('Edit', 'Write', 'NotebookEdit')
|
|
5818
|
+
GROUP BY 1 ORDER BY calls DESC`, ...ew.params);
|
|
5819
|
+
}
|
|
5820
|
+
function planRejections(db, mw, bucket) {
|
|
5821
|
+
const ew = withModel(mw, "e.model");
|
|
5822
|
+
const rejected = (tool) => `SUM(CASE WHEN tc.tool_name = '${tool}' AND tc.error_type = 'user-rejected' THEN 1 ELSE 0 END)`;
|
|
5823
|
+
const calls = (tool) => `SUM(CASE WHEN tc.tool_name = '${tool}' THEN 1 ELSE 0 END)`;
|
|
5824
|
+
return queryAll(db, `SELECT ${bucketExpr(bucket, "s.started_at")} bucket,
|
|
5825
|
+
${calls("ExitPlanMode")} plan_calls, ${rejected("ExitPlanMode")} plan_rejected,
|
|
5826
|
+
${calls("AskUserQuestion")} question_calls, ${rejected("AskUserQuestion")} question_rejected
|
|
5827
|
+
FROM tool_calls tc ${eventJoin} JOIN sessions s ON s.id = tc.session_id
|
|
5828
|
+
${andWhere(ew)} tc.tool_name IN ('ExitPlanMode', 'AskUserQuestion')
|
|
5829
|
+
GROUP BY bucket ORDER BY bucket`, ...ew.params).filter((r) => r.bucket != null);
|
|
5830
|
+
}
|
|
5831
|
+
var REWORK_BANDS = ["1", "2-3", "4-6", "7-12", "13+"];
|
|
5832
|
+
function fileRework(db, w) {
|
|
5833
|
+
const ew = withModel(w, "e.model");
|
|
5834
|
+
const rows = queryAll(db, `WITH pairs AS (
|
|
5835
|
+
SELECT COUNT(*) n
|
|
5836
|
+
FROM file_changes fc
|
|
5837
|
+
JOIN events e ON e.uuid = fc.event_uuid
|
|
5838
|
+
JOIN sessions s ON s.id = fc.session_id
|
|
5839
|
+
${ew.sql} GROUP BY fc.session_id, fc.file_path
|
|
5840
|
+
)
|
|
5841
|
+
SELECT CASE WHEN n = 1 THEN '1' WHEN n <= 3 THEN '2-3' WHEN n <= 6 THEN '4-6'
|
|
5842
|
+
WHEN n <= 12 THEN '7-12' ELSE '13+' END band,
|
|
5843
|
+
COUNT(*) pairs, SUM(n) changes
|
|
5844
|
+
FROM pairs GROUP BY band`, ...ew.params);
|
|
5845
|
+
const byBand = new Map(rows.map((r) => [r.band, r]));
|
|
5846
|
+
return REWORK_BANDS.map((band2) => byBand.get(band2) ?? { band: band2, pairs: 0, changes: 0 });
|
|
5847
|
+
}
|
|
5848
|
+
function findingsOverTime(db, w, bucket) {
|
|
5849
|
+
const ew = withModel(w, "e.model");
|
|
5850
|
+
const sev = (s) => `SUM(CASE WHEN f.severity = '${s}' THEN 1 ELSE 0 END) ${s}`;
|
|
5851
|
+
return queryAll(db, `SELECT ${bucketExpr(bucket, "e.timestamp")} bucket,
|
|
5852
|
+
${sev("info")}, ${sev("low")}, ${sev("medium")}, ${sev("high")}, ${sev("critical")}
|
|
5853
|
+
FROM findings f
|
|
5854
|
+
JOIN events e ON e.uuid = f.event_uuid
|
|
5855
|
+
JOIN sessions s ON s.id = f.session_id
|
|
5856
|
+
${ew.sql} GROUP BY bucket ORDER BY bucket`, ...ew.params).filter((r) => r.bucket != null);
|
|
5857
|
+
}
|
|
5626
5858
|
|
|
5627
5859
|
// ../server/dist/refresh.js
|
|
5628
5860
|
import { existsSync as existsSync6, mkdirSync as mkdirSync5 } from "node:fs";
|
|
@@ -5894,7 +6126,7 @@ async function createApp(db, opts = {}) {
|
|
|
5894
6126
|
app.get("/api/models", async () => listModels(db));
|
|
5895
6127
|
const dashFilters = (req) => {
|
|
5896
6128
|
const q = req.query;
|
|
5897
|
-
return { source: q.source, from: q.from, to: q.to };
|
|
6129
|
+
return { source: q.source, from: q.from, to: q.to, models: q.models?.split(",").filter(Boolean) };
|
|
5898
6130
|
};
|
|
5899
6131
|
app.get("/api/dashboard/overview", async (req) => dashboardOverview(db, dashFilters(req)));
|
|
5900
6132
|
app.get("/api/dashboard/timeseries", async (req) => {
|
|
@@ -5902,6 +6134,14 @@ async function createApp(db, opts = {}) {
|
|
|
5902
6134
|
return dashboardTimeseries(db, dashFilters(req), q.bucket);
|
|
5903
6135
|
});
|
|
5904
6136
|
app.get("/api/dashboard/breakdowns", async (req) => dashboardBreakdowns(db, dashFilters(req)));
|
|
6137
|
+
app.get("/api/dashboard/time", async (req) => {
|
|
6138
|
+
const q = req.query;
|
|
6139
|
+
return dashboardTime(db, dashFilters(req), q.bucket);
|
|
6140
|
+
});
|
|
6141
|
+
app.get("/api/dashboard/audit", async (req) => {
|
|
6142
|
+
const q = req.query;
|
|
6143
|
+
return dashboardAudit(db, dashFilters(req), q.bucket);
|
|
6144
|
+
});
|
|
5905
6145
|
const PREF_KEY = /^[a-z0-9._-]{1,64}$/i;
|
|
5906
6146
|
app.get("/api/prefs/:key", async (req, reply) => {
|
|
5907
6147
|
const { key } = req.params;
|