@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.
@@ -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 model FROM token_usage WHERE model IS NOT NULL ORDER BY model`).map((r) => r.model);
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("EXISTS (SELECT 1 FROM token_usage t WHERE t.session_id = s.id AND t.model = ?)");
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
- return eventRows.map((e) => ({
4840
- uuid: e.uuid,
4841
- type: e.type,
4842
- role: e.role,
4843
- timestamp: e.timestamp,
4844
- model: e.model,
4845
- is_sidechain: e.is_sidechain,
4846
- turn_id: e.turn_id,
4847
- text: e.text,
4848
- thinking: e.thinking,
4849
- toolCalls: (toolsByEvent.get(e.uuid) ?? []).map(toResponseToolCall)
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
- return { sql: where.length ? `WHERE ${where.join(" AND ")}` : "", params };
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 toolCount = queryGet(db, `SELECT COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id ${w.sql}`, ...w.params).n;
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 ${w.sql}
5433
- GROUP BY t.model`, ...w.params);
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
- ${w.sql ? w.sql + " AND" : "WHERE"} tn.duration_ms IS NOT NULL`, w.params);
5446
- const sessDur = percentiles(db, `SELECT s.duration_ms v FROM sessions s
5447
- ${w.sql ? w.sql + " AND" : "WHERE"} s.is_sidechain = 0 AND s.duration_ms IS NOT NULL`, w.params);
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
- range: { from: f.from ?? null, to: f.to ?? null, source: f.source ?? null },
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: counts.turns ?? 0,
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 BUCKET_EXPR = {
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 = BUCKET_EXPR[bucket];
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
- ${w.sql} GROUP BY b, t.model`, ...w.params)) {
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 ${w.sql} GROUP BY b`, ...w.params)) {
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
- ${w.sql ? w.sql + " AND" : "WHERE"} tc.status = 'error' GROUP BY b, tc.error_type`, ...w.params)) {
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 modelRows = queryAll(
5533
- db,
5534
- // COALESCE the bucket label, exactly as by_source does for a null source_id: token_usage.model is
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 ${w.sql}
5541
- GROUP BY t.model ORDER BY (SUM(t.input_tokens)+SUM(t.output_tokens)+SUM(t.cache_creation_input_tokens)+SUM(t.cache_read_input_tokens)) DESC`,
5542
- ...w.params
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(costForUsage(u.model, usageForCost(u)).toFixed(4)),
5637
+ cost: Number((cost.get(u.model) ?? 0).toFixed(4)),
5549
5638
  sessions: u.sessions ?? 0,
5550
- priced: !!rateForModel(u.model)
5639
+ priced: !unpriced2.has(u.model)
5551
5640
  }));
5552
5641
  }
5553
5642
  function sourceBreakdown(db, w) {
5554
- return queryAll(db, `SELECT COALESCE(s.source_id, '(none)') source, COUNT(*) sessions, SUM(s.turn_count) turns
5555
- FROM sessions s ${w.sql} GROUP BY s.source_id ORDER BY sessions DESC`, ...w.params);
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 = { sql: w.sql ? w.sql + " AND s.is_sidechain = 0" : "WHERE s.is_sidechain = 0", params: w.params };
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
- return queryAll(db, `SELECT tc.tool_name name, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
5567
- ${w.sql} GROUP BY tc.tool_name ORDER BY n DESC LIMIT 20`, ...w.params);
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 skills = queryAll(db, `SELECT tc.skill_name name, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
5571
- ${andWhere(w)} tc.skill_name IS NOT NULL GROUP BY tc.skill_name ORDER BY n DESC LIMIT 20`, ...w.params);
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(w)} tc.skill_id IS NOT NULL
5577
- GROUP BY tc.skill_name, sk.id ORDER BY name, n DESC`, ...w.params);
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 subagentByType = queryAll(db, `SELECT tc.agent_type type, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
5582
- ${andWhere(w)} tc.agent_type IS NOT NULL GROUP BY tc.agent_type ORDER BY n DESC`, ...w.params);
5583
- const perSession = queryAll(db, `SELECT COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
5584
- ${andWhere(w)} s.is_sidechain = 0 AND tc.tool_name IN ('Agent','Task')
5585
- GROUP BY tc.session_id`, ...w.params);
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 errorRows = queryAll(db, `SELECT COALESCE(tc.error_type, 'other') type, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
5597
- ${andWhere(w)} tc.status = 'error' GROUP BY tc.error_type ORDER BY n DESC`, ...w.params);
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;