@swestash/agent-lens 0.9.4 → 0.9.5

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.
@@ -4314,6 +4314,9 @@ function tableExists(db, name) {
4314
4314
  function queryAll(db, sql, ...params) {
4315
4315
  return db.prepare(sql).all(...params);
4316
4316
  }
4317
+ function queryGet(db, sql, ...params) {
4318
+ return db.prepare(sql).get(...params);
4319
+ }
4317
4320
  function pushDateRange(clauses, params, col, from, to) {
4318
4321
  if (from) {
4319
4322
  clauses.push(`date(${col}) >= date(?)`);
@@ -4419,22 +4422,17 @@ function extractParts(rawJson) {
4419
4422
  return { text: text || null, thinking: thinking || null };
4420
4423
  }
4421
4424
  function listSources(db) {
4422
- return db.prepare(
4423
- // Count MAIN sessions only (is_sidechain = 0): the dropdown filters the session list, which
4424
- // defaults to main-only, so a source's count must match what you'd see — not be inflated by the
4425
- // many subagent sidechains each task spawns.
4426
- `SELECT s.id, s.label, s.agent_id, s.config_dir,
4427
- (SELECT COUNT(*) FROM sessions x WHERE x.source_id = s.id AND x.is_sidechain = 0) AS session_count
4428
- FROM sources s ORDER BY s.label`
4429
- ).all();
4425
+ return queryAll(db, `SELECT s.id, s.label,
4426
+ (SELECT COUNT(*) FROM sessions x WHERE x.source_id = s.id AND x.is_sidechain = 0) AS session_count
4427
+ FROM sources s ORDER BY s.label`);
4430
4428
  }
4431
4429
  function listProjects(db) {
4432
- return db.prepare(`SELECT p.id, p.path,
4433
- (SELECT COUNT(*) FROM sessions x WHERE x.project_id = p.id AND x.is_sidechain = 0) AS session_count
4434
- FROM projects p ORDER BY session_count DESC, p.path`).all();
4430
+ return queryAll(db, `SELECT p.id, p.path,
4431
+ (SELECT COUNT(*) FROM sessions x WHERE x.project_id = p.id AND x.is_sidechain = 0) AS session_count
4432
+ FROM projects p ORDER BY session_count DESC, p.path`);
4435
4433
  }
4436
4434
  function listModels(db) {
4437
- return db.prepare(`SELECT DISTINCT model FROM token_usage WHERE model IS NOT NULL ORDER BY model`).all().map((r) => r.model);
4435
+ return queryAll(db, `SELECT DISTINCT model FROM token_usage WHERE model IS NOT NULL ORDER BY model`).map((r) => r.model);
4438
4436
  }
4439
4437
  function toFtsQuery(raw) {
4440
4438
  return raw.trim().split(/\s+/).filter(Boolean).map((t) => `"${t.replace(/"/g, '""')}"`).join(" ");
@@ -4471,8 +4469,8 @@ function listSessions(db, f) {
4471
4469
  params.push(toFtsQuery(f.q), like, like, like);
4472
4470
  }
4473
4471
  const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
4474
- const total = db.prepare(`SELECT COUNT(*) n FROM sessions s ${whereSql}`).get(...params).n;
4475
- const baseSelect = `SELECT s.id, s.ai_title, s.slug, s.source_id, s.is_sidechain, s.started_at, s.ended_at,
4472
+ const total = queryGet(db, `SELECT COUNT(*) n FROM sessions s ${whereSql}`, ...params).n;
4473
+ const baseSelect = `SELECT s.id, s.ai_title, s.slug, s.source_id, s.is_sidechain, s.started_at,
4476
4474
  s.duration_ms, s.event_count, s.turn_count, p.path AS project_path,
4477
4475
  (SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models,
4478
4476
  (SELECT COUNT(*) FROM tool_calls tc WHERE tc.session_id = s.id) AS tool_call_count,
@@ -4491,10 +4489,9 @@ function listSessions(db, f) {
4491
4489
  };
4492
4490
  const sortKey = f.sort ?? "started";
4493
4491
  const JS_SORT = /* @__PURE__ */ new Set(["tokens", "cost", "errors", "security"]);
4494
- let rows;
4492
+ let sessions;
4495
4493
  if (JS_SORT.has(sortKey)) {
4496
- const all = db.prepare(baseSelect).all(...params);
4497
- attachSessionCost(db, all);
4494
+ const all = attachSessionCost(db, queryAll(db, baseSelect, ...params));
4498
4495
  const sign = f.dir === "asc" ? 1 : -1;
4499
4496
  const metric = (r) => {
4500
4497
  switch (sortKey) {
@@ -4515,12 +4512,11 @@ function listSessions(db, f) {
4515
4512
  const c = (metric(a) - metric(b)) * sign;
4516
4513
  return c !== 0 ? c : String(a.id).localeCompare(String(b.id));
4517
4514
  });
4518
- rows = all.slice(f.offset, f.offset + f.limit);
4515
+ sessions = all.slice(f.offset, f.offset + f.limit);
4519
4516
  } else {
4520
- rows = db.prepare(`${baseSelect} ORDER BY ${orderBy(NATIVE_ORDER, sortKey, "started", f.dir)}, s.id ASC LIMIT ? OFFSET ?`).all(...params, f.limit, f.offset);
4521
- attachSessionCost(db, rows);
4517
+ sessions = attachSessionCost(db, queryAll(db, `${baseSelect} ORDER BY ${orderBy(NATIVE_ORDER, sortKey, "started", f.dir)}, s.id ASC LIMIT ? OFFSET ?`, ...params, f.limit, f.offset));
4522
4518
  }
4523
- return { total, sessions: rows };
4519
+ return { total, sessions };
4524
4520
  }
4525
4521
  function attachSessionCost(db, rows) {
4526
4522
  const usageBySession = /* @__PURE__ */ new Map();
@@ -4536,21 +4532,18 @@ function attachSessionCost(db, rows) {
4536
4532
  usageBySession.set(u.session_id, list);
4537
4533
  }
4538
4534
  }
4539
- for (const r of rows) {
4535
+ return rows.map(({ ai_title, slug, ...r }) => {
4540
4536
  const u = usageBySession.get(r.id) ?? [];
4541
4537
  const split = splitOf(u);
4542
- r.tokens = tokensOf(split);
4543
- r.token_split = split;
4544
- r.cost = costOf(u);
4545
- r.title = r.ai_title || r.slug || null;
4546
- }
4538
+ return { ...r, tokens: tokensOf(split), token_split: split, cost: costOf(u), title: ai_title || slug || null };
4539
+ });
4547
4540
  }
4548
4541
  function loadToolCalls(db, id) {
4549
- const toolRows = db.prepare(`SELECT id, event_uuid, tool_name, skill_name, skill_id, agent_type, spawned_session_id,
4550
- workflow_run_id, workflow_name, status, error_type,
4551
- total_duration_ms, total_tokens, input_json, result_summary,
4552
- (SELECT COUNT(*) FROM sessions s WHERE s.workflow_run_id = tool_calls.workflow_run_id) AS workflow_agent_count
4553
- FROM tool_calls WHERE session_id = ?`).all(id);
4542
+ const toolRows = queryAll(db, `SELECT id, event_uuid, tool_name, skill_name, skill_id, agent_type, spawned_session_id,
4543
+ workflow_run_id, workflow_name, status, error_type,
4544
+ total_duration_ms, input_json, result_summary,
4545
+ (SELECT COUNT(*) FROM sessions s WHERE s.workflow_run_id = tool_calls.workflow_run_id) AS workflow_agent_count
4546
+ FROM tool_calls WHERE session_id = ?`, id);
4554
4547
  if (tableExists(db, "tool_results")) {
4555
4548
  const getFull = db.prepare("SELECT text, bytes FROM tool_results WHERE session_id = ? AND name = ?");
4556
4549
  for (const t of toolRows) {
@@ -4568,8 +4561,8 @@ function loadFindings(db, id, toolRows) {
4568
4561
  if (!tableExists(db, "findings"))
4569
4562
  return [];
4570
4563
  const sessionFindings = [];
4571
- const findingRows = db.prepare(`SELECT id, tool_call_id, event_uuid, turn_id, rule_id, category, framework_ref, severity, title, evidence, signals_json
4572
- FROM findings WHERE session_id = ?`).all(id);
4564
+ const findingRows = queryAll(db, `SELECT id, tool_call_id, event_uuid, turn_id, rule_id, category, framework_ref, severity, title, evidence, signals_json
4565
+ FROM findings WHERE session_id = ?`, id);
4573
4566
  const byToolCall = /* @__PURE__ */ new Map();
4574
4567
  for (const fr of findingRows) {
4575
4568
  const f = {
@@ -4583,6 +4576,7 @@ function loadFindings(db, id, toolRows) {
4583
4576
  severity: fr.severity,
4584
4577
  title: fr.title,
4585
4578
  evidence: fr.evidence,
4579
+ // Asserted, not validated: safeJson returns unknown and this is the assertion (ADR-026).
4586
4580
  signals: safeJson(fr.signals_json)
4587
4581
  };
4588
4582
  sessionFindings.push(f);
@@ -4590,13 +4584,16 @@ function loadFindings(db, id, toolRows) {
4590
4584
  pushGrouped(byToolCall, fr.tool_call_id, f);
4591
4585
  }
4592
4586
  for (const t of toolRows)
4593
- t.findings = byToolCall.get(t.id) ?? [];
4587
+ t.findings = t.id ? byToolCall.get(t.id) ?? [] : [];
4594
4588
  sessionFindings.sort((a, b) => severityRank(b.severity) - severityRank(a.severity));
4595
4589
  return sessionFindings;
4596
4590
  }
4591
+ function toResponseToolCall({ event_uuid: _e, error_type: _t, ...call }) {
4592
+ return call;
4593
+ }
4597
4594
  function loadEvents(db, id, toolRows) {
4598
- const eventRows = db.prepare(`SELECT uuid, type, role, timestamp, model, is_sidechain, turn_id, raw_json
4599
- FROM events WHERE session_id = ? ORDER BY timestamp, seq`).all(id);
4595
+ const eventRows = queryAll(db, `SELECT uuid, type, role, timestamp, model, is_sidechain, turn_id, raw_json
4596
+ FROM events WHERE session_id = ? ORDER BY timestamp, seq`, id);
4600
4597
  const toolsByEvent = /* @__PURE__ */ new Map();
4601
4598
  for (const t of toolRows) {
4602
4599
  if (!t.event_uuid)
@@ -4615,13 +4612,13 @@ function loadEvents(db, id, toolRows) {
4615
4612
  turn_id: e.turn_id,
4616
4613
  text,
4617
4614
  thinking,
4618
- toolCalls: toolsByEvent.get(e.uuid) ?? []
4615
+ toolCalls: (toolsByEvent.get(e.uuid) ?? []).map(toResponseToolCall)
4619
4616
  };
4620
4617
  });
4621
4618
  }
4622
4619
  function loadClassification(db, id) {
4623
- const cls = db.prepare(`SELECT category, complexity_score, complexity_band, signals_json, classifier_version
4624
- FROM classifications WHERE scope = 'session' AND target_id = ?`).get(id);
4620
+ const cls = queryGet(db, `SELECT category, complexity_score, complexity_band, signals_json, classifier_version
4621
+ FROM classifications WHERE scope = 'session' AND target_id = ?`, id);
4625
4622
  if (!cls)
4626
4623
  return null;
4627
4624
  let signals = null;
@@ -4640,66 +4637,67 @@ function loadClassification(db, id) {
4640
4637
  function loadParent(db, session) {
4641
4638
  if (!session.parent_session_id)
4642
4639
  return null;
4643
- const p = db.prepare(`SELECT ps.id, ps.ai_title, ps.slug, t.seq AS turn_seq
4644
- FROM sessions ps LEFT JOIN turns t ON t.id = ?
4645
- WHERE ps.id = ?`).get(session.parent_turn_id, session.parent_session_id);
4640
+ const p = queryGet(db, `SELECT ps.id, ps.ai_title, ps.slug, t.seq AS turn_seq
4641
+ FROM sessions ps LEFT JOIN turns t ON t.id = ?
4642
+ WHERE ps.id = ?`, session.parent_turn_id, session.parent_session_id);
4646
4643
  return p ? { id: p.id, title: p.ai_title || p.slug || null, turn_seq: p.turn_seq ?? null } : null;
4647
4644
  }
4648
4645
  function loadChildren(db, id) {
4649
4646
  const hasMeta = tableExists(db, "session_meta");
4650
- const children = db.prepare(`SELECT s.id, s.ai_title, s.slug, s.turn_count, s.started_at, s.workflow_run_id,
4651
- (SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models
4652
- ${metaProjection(hasMeta)}
4653
- FROM sessions s ${metaJoin(hasMeta)}
4654
- WHERE s.parent_session_id = ? ORDER BY s.started_at`).all(id);
4655
- for (const ch of children) {
4656
- ch.title = ch.ai_title || ch.slug || null;
4647
+ const children = queryAll(db, `SELECT s.id, s.ai_title, s.slug, s.turn_count, s.started_at, s.workflow_run_id,
4648
+ (SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models
4649
+ ${metaProjection(hasMeta)}
4650
+ FROM sessions s ${metaJoin(hasMeta)}
4651
+ WHERE s.parent_session_id = ? ORDER BY s.started_at`, id);
4652
+ return children.map(({ ai_title, slug, ...ch }) => {
4657
4653
  const u = sessionUsage(db, ch.id);
4658
- ch.tokens = tokensOf(splitOf(u));
4659
- ch.cost = costOf(u);
4660
- }
4661
- return children;
4654
+ return { ...ch, title: ai_title || slug || null, tokens: tokensOf(splitOf(u)), cost: costOf(u) };
4655
+ });
4662
4656
  }
4663
4657
  function loadWorkflowRuns(db, id) {
4664
4658
  const hasWR = tableExists(db, "workflow_results");
4665
- return db.prepare(`SELECT tc.workflow_run_id AS run_id, tc.workflow_name AS name, t.seq AS turn_seq,
4666
- (SELECT COUNT(*) FROM sessions s WHERE s.workflow_run_id = tc.workflow_run_id) AS agent_count
4667
- ${hasWR ? ", (SELECT status FROM workflow_results wr WHERE wr.run_id = tc.workflow_run_id) AS status" : ", NULL AS status"}
4668
- FROM tool_calls tc LEFT JOIN turns t ON t.id = tc.turn_id
4669
- WHERE tc.session_id = ? AND tc.workflow_run_id IS NOT NULL
4670
- ORDER BY t.seq`).all(id);
4659
+ return queryAll(db, `SELECT tc.workflow_run_id AS run_id, tc.workflow_name AS name, t.seq AS turn_seq,
4660
+ (SELECT COUNT(*) FROM sessions s WHERE s.workflow_run_id = tc.workflow_run_id) AS agent_count
4661
+ ${hasWR ? ", (SELECT status FROM workflow_results wr WHERE wr.run_id = tc.workflow_run_id) AS status" : ", NULL AS status"}
4662
+ FROM tool_calls tc LEFT JOIN turns t ON t.id = tc.turn_id
4663
+ WHERE tc.session_id = ? AND tc.workflow_run_id IS NOT NULL
4664
+ ORDER BY t.seq`, id);
4671
4665
  }
4672
4666
  function loadFileChanges(db, id) {
4673
4667
  if (!tableExists(db, "file_changes"))
4674
4668
  return [];
4675
- return db.prepare(`SELECT fc.id, fc.tool_call_id, fc.turn_id, fc.event_uuid, fc.file_path, fc.tool_name,
4676
- fc.lines_added, fc.lines_removed, fc.timestamp
4677
- FROM file_changes fc WHERE fc.session_id = ? ORDER BY fc.timestamp`).all(id);
4669
+ return queryAll(db, `SELECT fc.id, fc.tool_call_id, fc.turn_id, fc.event_uuid, fc.file_path, fc.tool_name,
4670
+ fc.lines_added, fc.lines_removed, fc.timestamp
4671
+ FROM file_changes fc WHERE fc.session_id = ? ORDER BY fc.timestamp`, id);
4678
4672
  }
4679
- function attachSessionTotals(db, session, id, toolRows) {
4673
+ function withSessionTotals(db, session, id, toolRows) {
4680
4674
  const usage = sessionUsage(db, id);
4681
4675
  const split = splitOf(usage);
4682
- session.tokens = tokensOf(split);
4683
- session.token_split = split;
4684
- session.cost = costOf(usage);
4685
- session.title = session.ai_title || session.slug || null;
4686
- session.tool_call_count = toolRows.length;
4687
- session.tool_error_count = toolRows.filter((t) => t.status === "error").length;
4688
- session.tool_rejection_count = toolRows.filter((t) => t.status === "error" && t.error_type && errorKind(t.error_type) === "rejection").length;
4689
- session.tool_failure_count = session.tool_error_count - session.tool_rejection_count;
4676
+ const tool_error_count = toolRows.filter((t) => t.status === "error").length;
4677
+ const tool_rejection_count = toolRows.filter((t) => t.status === "error" && t.error_type && errorKind(t.error_type) === "rejection").length;
4678
+ return {
4679
+ ...session,
4680
+ tokens: tokensOf(split),
4681
+ token_split: split,
4682
+ cost: costOf(usage),
4683
+ title: session.ai_title || session.slug || null,
4684
+ tool_call_count: toolRows.length,
4685
+ tool_error_count,
4686
+ tool_rejection_count,
4687
+ tool_failure_count: tool_error_count - tool_rejection_count
4688
+ };
4690
4689
  }
4691
4690
  function getSession(db, id) {
4692
- const session = db.prepare(`SELECT s.*, p.path AS project_path FROM sessions s
4693
- LEFT JOIN projects p ON p.id = s.project_id WHERE s.id = ?`).get(id);
4691
+ const session = queryGet(db, `SELECT s.*, p.path AS project_path FROM sessions s
4692
+ LEFT JOIN projects p ON p.id = s.project_id WHERE s.id = ?`, id);
4694
4693
  if (!session)
4695
4694
  return null;
4696
- const turns = db.prepare("SELECT * FROM turns WHERE session_id = ? ORDER BY seq").all(id);
4695
+ const turns = queryAll(db, "SELECT * FROM turns WHERE session_id = ? ORDER BY seq", id);
4697
4696
  const toolRows = loadToolCalls(db, id);
4698
4697
  const findings = loadFindings(db, id, toolRows);
4699
4698
  const events = loadEvents(db, id, toolRows);
4700
- attachSessionTotals(db, session, id, toolRows);
4701
4699
  return {
4702
- session,
4700
+ session: withSessionTotals(db, session, id, toolRows),
4703
4701
  turns,
4704
4702
  events,
4705
4703
  classification: loadClassification(db, id),
@@ -4724,27 +4722,26 @@ function safeJson(s) {
4724
4722
  }
4725
4723
  function loadAgents(db, runId) {
4726
4724
  const hasMeta = tableExists(db, "session_meta");
4727
- const agents = db.prepare(`SELECT s.id, s.ai_title, s.slug, s.turn_count, s.started_at, s.ended_at, s.duration_ms,
4728
- (SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models
4729
- ${metaProjection(hasMeta)}
4730
- FROM sessions s ${metaJoin(hasMeta)}
4731
- WHERE s.workflow_run_id = ? ORDER BY s.started_at`).all(runId);
4725
+ const agentRows = queryAll(db, `SELECT s.id, s.ai_title, s.slug, s.turn_count, s.started_at, s.ended_at, s.duration_ms,
4726
+ (SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models
4727
+ ${metaProjection(hasMeta)}
4728
+ FROM sessions s ${metaJoin(hasMeta)}
4729
+ WHERE s.workflow_run_id = ? ORDER BY s.started_at`, runId);
4732
4730
  let total_tokens = 0;
4733
4731
  let total_cost = 0;
4734
4732
  let started_at = null;
4735
4733
  let ended_at = null;
4736
- for (const a of agents) {
4737
- a.title = a.ai_title || a.slug || null;
4738
- const u = sessionUsage(db, a.id);
4739
- a.tokens = tokensOf(splitOf(u));
4740
- a.cost = costOf(u);
4734
+ const agents = agentRows.map(({ ai_title, slug, ...r }) => {
4735
+ const u = sessionUsage(db, r.id);
4736
+ const a = { ...r, title: ai_title || slug || null, tokens: tokensOf(splitOf(u)), cost: costOf(u) };
4741
4737
  total_tokens += a.tokens;
4742
4738
  total_cost += a.cost;
4743
4739
  if (a.started_at && (!started_at || a.started_at < started_at))
4744
4740
  started_at = a.started_at;
4745
4741
  if (a.ended_at && (!ended_at || a.ended_at > ended_at))
4746
4742
  ended_at = a.ended_at;
4747
- }
4743
+ return a;
4744
+ });
4748
4745
  const duration_ms = started_at && ended_at ? new Date(ended_at).getTime() - new Date(started_at).getTime() : null;
4749
4746
  return {
4750
4747
  agents,
@@ -4754,9 +4751,9 @@ function loadAgents(db, runId) {
4754
4751
  function parseCompletion(db, wf) {
4755
4752
  if (!wf.tool_use_id || !wf.parent_session_id)
4756
4753
  return null;
4757
- const ev = db.prepare(`SELECT raw_json FROM events
4758
- WHERE session_id = ? AND text LIKE '%<task-notification>%' AND text LIKE ?
4759
- ORDER BY timestamp DESC LIMIT 1`).get(wf.parent_session_id, `%${wf.tool_use_id}%`);
4754
+ const ev = queryGet(db, `SELECT raw_json FROM events
4755
+ WHERE session_id = ? AND text LIKE '%<task-notification>%' AND text LIKE ?
4756
+ ORDER BY timestamp DESC LIMIT 1`, wf.parent_session_id, `%${wf.tool_use_id}%`);
4760
4757
  if (!ev)
4761
4758
  return null;
4762
4759
  const { text } = extractParts(ev.raw_json);
@@ -4764,9 +4761,9 @@ function parseCompletion(db, wf) {
4764
4761
  return { status: xmlTag(t, "status"), summary: xmlTag(t, "summary"), result: xmlTag(t, "result"), failures: xmlTag(t, "failures") };
4765
4762
  }
4766
4763
  function loadRunSidecar(db, runId, completion) {
4767
- const wr = tableExists(db, "workflow_results") ? db.prepare(`SELECT status, summary, default_model, result_json, phases_json, logs_json, progress_json,
4768
- agent_count, total_tokens, total_tool_calls, duration_ms, started_at, ended_at
4769
- FROM workflow_results WHERE run_id = ?`).get(runId) : null;
4764
+ const wr = tableExists(db, "workflow_results") ? queryGet(db, `SELECT status, summary, default_model, result_json, phases_json, logs_json, progress_json,
4765
+ agent_count, total_tokens, total_tool_calls, duration_ms, started_at, ended_at
4766
+ FROM workflow_results WHERE run_id = ?`, runId) ?? null : null;
4770
4767
  if (!wr)
4771
4768
  return { wr: null, run: null, completion };
4772
4769
  const run = {
@@ -4779,6 +4776,8 @@ function loadRunSidecar(db, runId, completion) {
4779
4776
  duration_ms: wr.duration_ms ?? null,
4780
4777
  started_at: wr.started_at ?? null,
4781
4778
  ended_at: wr.ended_at ?? null,
4779
+ // The three JSON blobs are asserted, not validated (ADR-026): safeJson yields null for malformed
4780
+ // stored text, and these casts are what claims the shape of everything else.
4782
4781
  phases: safeJson(wr.phases_json),
4783
4782
  logs: safeJson(wr.logs_json),
4784
4783
  // The per-phase/per-agent event timeline (workflowProgress) — lets the page render a phase graph
@@ -4796,16 +4795,16 @@ function loadRunSidecar(db, runId, completion) {
4796
4795
  return { wr, run, completion };
4797
4796
  }
4798
4797
  function getWorkflow(db, runId) {
4799
- const wf = db.prepare(`SELECT tc.id AS tool_use_id, tc.workflow_run_id AS run_id, tc.workflow_name AS name, tc.status, tc.result_summary,
4800
- tc.input_json AS input_json,
4801
- tc.session_id AS parent_session_id, t.seq AS turn_seq,
4802
- ps.ai_title AS parent_ai_title, ps.slug AS parent_slug
4803
- FROM tool_calls tc
4804
- LEFT JOIN turns t ON t.id = tc.turn_id
4805
- LEFT JOIN sessions ps ON ps.id = tc.session_id
4806
- WHERE tc.workflow_run_id = ? AND tc.tool_name = 'Workflow'
4807
- ORDER BY t.seq
4808
- LIMIT 1`).get(runId);
4798
+ const wf = queryGet(db, `SELECT tc.id AS tool_use_id, tc.workflow_run_id AS run_id, tc.workflow_name AS name, tc.status, tc.result_summary,
4799
+ tc.input_json AS input_json,
4800
+ tc.session_id AS parent_session_id, t.seq AS turn_seq,
4801
+ ps.ai_title AS parent_ai_title, ps.slug AS parent_slug
4802
+ FROM tool_calls tc
4803
+ LEFT JOIN turns t ON t.id = tc.turn_id
4804
+ LEFT JOIN sessions ps ON ps.id = tc.session_id
4805
+ WHERE tc.workflow_run_id = ? AND tc.tool_name = 'Workflow'
4806
+ ORDER BY t.seq
4807
+ LIMIT 1`, runId);
4809
4808
  if (!wf)
4810
4809
  return null;
4811
4810
  const { agents, stats } = loadAgents(db, runId);
@@ -4835,20 +4834,18 @@ function listSkills(db, f = {}) {
4835
4834
  where.push("s.source_id = ?"), params.push(f.source);
4836
4835
  if (f.project)
4837
4836
  where.push("s.project_id = ?"), params.push(f.project);
4838
- const rows = db.prepare(`SELECT tc.skill_name AS name,
4839
- COUNT(*) AS call_count,
4840
- COUNT(DISTINCT tc.skill_id) AS version_count,
4841
- MAX(e.timestamp) AS last_fired,
4842
- GROUP_CONCAT(DISTINCT s.source_id) AS sources
4843
- FROM tool_calls tc
4844
- JOIN sessions s ON s.id = tc.session_id
4845
- LEFT JOIN events e ON e.uuid = tc.event_uuid
4846
- WHERE ${where.join(" AND ")}
4847
- GROUP BY tc.skill_name
4848
- ORDER BY call_count DESC, name`).all(...params);
4849
- for (const r of rows)
4850
- r.sources = r.sources ? String(r.sources).split(",").filter(Boolean) : [];
4851
- return rows;
4837
+ const rows = queryAll(db, `SELECT tc.skill_name AS name,
4838
+ COUNT(*) AS call_count,
4839
+ COUNT(DISTINCT tc.skill_id) AS version_count,
4840
+ MAX(e.timestamp) AS last_fired,
4841
+ GROUP_CONCAT(DISTINCT s.source_id) AS sources
4842
+ FROM tool_calls tc
4843
+ JOIN sessions s ON s.id = tc.session_id
4844
+ LEFT JOIN events e ON e.uuid = tc.event_uuid
4845
+ WHERE ${where.join(" AND ")}
4846
+ GROUP BY tc.skill_name
4847
+ ORDER BY call_count DESC, name`, ...params);
4848
+ return rows.map((r) => ({ ...r, sources: r.sources ? String(r.sources).split(",").filter(Boolean) : [] }));
4852
4849
  }
4853
4850
  function listFiles(db, f = {}) {
4854
4851
  if (!tableExists(db, "file_changes"))
@@ -4871,7 +4868,7 @@ function listFiles(db, f = {}) {
4871
4868
  MAX(fc.timestamp) AS last_ts
4872
4869
  FROM file_changes fc${whereSql}
4873
4870
  GROUP BY fc.project_id, fc.file_path`;
4874
- const total = db.prepare(`SELECT COUNT(*) n FROM (${grouped})`).get(...params).n;
4871
+ const total = queryGet(db, `SELECT COUNT(*) n FROM (${grouped})`, ...params).n;
4875
4872
  const ORDER = {
4876
4873
  last_ts: "last_ts",
4877
4874
  first_ts: "first_ts",
@@ -4881,9 +4878,9 @@ function listFiles(db, f = {}) {
4881
4878
  };
4882
4879
  const limit = Math.min(f.limit ?? 50, 200);
4883
4880
  const offset = f.offset ?? 0;
4884
- const files = db.prepare(`SELECT g.*, p.path AS project_path FROM (${grouped}) g
4885
- LEFT JOIN projects p ON p.id = g.project_id
4886
- ORDER BY ${orderBy(ORDER, f.sort, "last_ts", f.dir)}, g.file_path ASC LIMIT ? OFFSET ?`).all(...params, limit, offset);
4881
+ const files = queryAll(db, `SELECT g.*, p.path AS project_path FROM (${grouped}) g
4882
+ LEFT JOIN projects p ON p.id = g.project_id
4883
+ ORDER BY ${orderBy(ORDER, f.sort, "last_ts", f.dir)}, g.file_path ASC LIMIT ? OFFSET ?`, ...params, limit, offset);
4887
4884
  return { total, files };
4888
4885
  }
4889
4886
  function getFileTimeline(db, filePath, projectId2) {
@@ -4893,15 +4890,15 @@ function getFileTimeline(db, filePath, projectId2) {
4893
4890
  let scope = "fc.file_path = ?";
4894
4891
  if (projectId2)
4895
4892
  scope += " AND fc.project_id = ?", params.push(projectId2);
4896
- const rows = db.prepare(`SELECT fc.id, fc.session_id, fc.turn_id, fc.event_uuid, fc.tool_name, fc.lines_added,
4897
- fc.lines_removed, fc.timestamp, fc.project_id,
4898
- s.ai_title, s.slug, s.source_id, s.started_at, t.seq AS turn_seq, t.prompt_preview,
4899
- p.path AS project_path
4900
- FROM file_changes fc
4901
- JOIN sessions s ON s.id = fc.session_id
4902
- LEFT JOIN turns t ON t.id = fc.turn_id
4903
- LEFT JOIN projects p ON p.id = fc.project_id
4904
- WHERE ${scope} ORDER BY fc.timestamp`).all(...params);
4893
+ const rows = queryAll(db, `SELECT fc.id, fc.session_id, fc.turn_id, fc.event_uuid, fc.tool_name, fc.lines_added,
4894
+ fc.lines_removed, fc.timestamp, fc.project_id,
4895
+ s.ai_title, s.slug, s.source_id, s.started_at, t.seq AS turn_seq, t.prompt_preview,
4896
+ p.path AS project_path
4897
+ FROM file_changes fc
4898
+ JOIN sessions s ON s.id = fc.session_id
4899
+ LEFT JOIN turns t ON t.id = fc.turn_id
4900
+ LEFT JOIN projects p ON p.id = fc.project_id
4901
+ WHERE ${scope} ORDER BY fc.timestamp`, ...params);
4905
4902
  if (!rows.length)
4906
4903
  return null;
4907
4904
  const cat = tableExists(db, "classifications") ? db.prepare("SELECT category FROM classifications WHERE scope = 'session' AND target_id = ?") : null;
@@ -4950,22 +4947,21 @@ function getFileTimeline(db, filePath, projectId2) {
4950
4947
  };
4951
4948
  }
4952
4949
  function getSkill(db, name) {
4953
- const versions = db.prepare(`SELECT sk.id, sk.base_dir, sk.summary, sk.body, sk.body_bytes, sk.first_seen, sk.last_seen,
4954
- (SELECT COUNT(*) FROM tool_calls tc WHERE tc.skill_id = sk.id) AS call_count
4955
- FROM skills sk WHERE sk.name = ? ORDER BY sk.last_seen DESC, sk.id`).all(name);
4956
- const sessions = db.prepare(`SELECT s.id, s.ai_title, s.slug, s.source_id, s.started_at, p.path AS project_path,
4957
- tc.skill_id AS version_id, MIN(e.timestamp) AS fired_at, COUNT(*) AS fire_count
4958
- FROM tool_calls tc
4959
- JOIN sessions s ON s.id = tc.session_id
4960
- LEFT JOIN projects p ON p.id = s.project_id
4961
- LEFT JOIN events e ON e.uuid = tc.event_uuid
4962
- WHERE tc.tool_name = 'Skill' AND tc.skill_name = ?
4963
- GROUP BY s.id, tc.skill_id
4964
- ORDER BY fired_at DESC`).all(name);
4965
- if (!versions.length && !sessions.length)
4950
+ const versions = queryAll(db, `SELECT sk.id, sk.base_dir, sk.summary, sk.body, sk.body_bytes, sk.first_seen, sk.last_seen,
4951
+ (SELECT COUNT(*) FROM tool_calls tc WHERE tc.skill_id = sk.id) AS call_count
4952
+ FROM skills sk WHERE sk.name = ? ORDER BY sk.last_seen DESC, sk.id`, name);
4953
+ const sessionRows = queryAll(db, `SELECT s.id, s.ai_title, s.slug, s.source_id, s.started_at, p.path AS project_path,
4954
+ tc.skill_id AS version_id, MIN(e.timestamp) AS fired_at, COUNT(*) AS fire_count
4955
+ FROM tool_calls tc
4956
+ JOIN sessions s ON s.id = tc.session_id
4957
+ LEFT JOIN projects p ON p.id = s.project_id
4958
+ LEFT JOIN events e ON e.uuid = tc.event_uuid
4959
+ WHERE tc.tool_name = 'Skill' AND tc.skill_name = ?
4960
+ GROUP BY s.id, tc.skill_id
4961
+ ORDER BY fired_at DESC`, name);
4962
+ if (!versions.length && !sessionRows.length)
4966
4963
  return null;
4967
- for (const s of sessions)
4968
- s.title = s.ai_title || s.slug || null;
4964
+ const sessions = sessionRows.map(({ ai_title, slug, ...s }) => ({ ...s, title: ai_title || slug || null }));
4969
4965
  const call_count = sessions.reduce((a, s) => a + s.fire_count, 0);
4970
4966
  return { name, versions, sessions, call_count };
4971
4967
  }
@@ -5017,7 +5013,7 @@ function listFindings(db, f) {
5017
5013
  const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
5018
5014
  const dismissJoin = attached ? "LEFT JOIN triage.dismissed_findings d ON d.finding_id = f.id" : "";
5019
5015
  const triageCols = attached ? `, (d.finding_id IS NOT NULL) AS dismissed, d.note AS dismiss_note, d.dismissed_at, ${MUTED_SQL} AS muted` : `, 0 AS dismissed, NULL AS dismiss_note, NULL AS dismissed_at, 0 AS muted`;
5020
- const total = db.prepare(`SELECT COUNT(*) n FROM findings f JOIN sessions s ON s.id = f.session_id ${dismissJoin} ${whereSql}`).get(...w.params).n;
5016
+ const total = queryGet(db, `SELECT COUNT(*) n FROM findings f JOIN sessions s ON s.id = f.session_id ${dismissJoin} ${whereSql}`, ...w.params).n;
5021
5017
  const ORDER = {
5022
5018
  severity: SEVERITY_RANK_SQL,
5023
5019
  session: "f.session_id",
@@ -5025,18 +5021,18 @@ function listFindings(db, f) {
5025
5021
  category: "f.category",
5026
5022
  time: "s.started_at"
5027
5023
  };
5028
- const findings = db.prepare(`SELECT f.id, f.session_id, f.tool_call_id, f.event_uuid, f.turn_id, f.rule_id, f.category,
5029
- f.framework_ref, f.severity, f.title, f.evidence, tc.tool_name,
5030
- COALESCE(s.ai_title, s.slug) AS session_title, s.source_id, s.is_sidechain,
5031
- s.started_at, p.path AS project_path, s.project_id ${triageCols}
5032
- FROM findings f
5033
- JOIN sessions s ON s.id = f.session_id
5034
- LEFT JOIN projects p ON p.id = s.project_id
5035
- LEFT JOIN tool_calls tc ON tc.id = f.tool_call_id
5036
- ${dismissJoin}
5037
- ${whereSql}
5038
- ORDER BY ${orderBy(ORDER, f.sort, "severity", f.dir)}, ${SEVERITY_RANK_SQL} DESC, f.id ASC
5039
- LIMIT ? OFFSET ?`).all(...w.params, f.limit, f.offset);
5024
+ const findings = queryAll(db, `SELECT f.id, f.session_id, f.tool_call_id, f.event_uuid, f.turn_id, f.rule_id, f.category,
5025
+ f.framework_ref, f.severity, f.title, f.evidence, tc.tool_name,
5026
+ COALESCE(s.ai_title, s.slug) AS session_title, s.source_id, s.is_sidechain,
5027
+ s.started_at, p.path AS project_path, s.project_id ${triageCols}
5028
+ FROM findings f
5029
+ JOIN sessions s ON s.id = f.session_id
5030
+ LEFT JOIN projects p ON p.id = s.project_id
5031
+ LEFT JOIN tool_calls tc ON tc.id = f.tool_call_id
5032
+ ${dismissJoin}
5033
+ ${whereSql}
5034
+ ORDER BY ${orderBy(ORDER, f.sort, "severity", f.dir)}, ${SEVERITY_RANK_SQL} DESC, f.id ASC
5035
+ LIMIT ? OFFSET ?`, ...w.params, f.limit, f.offset);
5040
5036
  return { total, findings };
5041
5037
  }
5042
5038
  function securitySummary(db) {
@@ -5048,15 +5044,15 @@ function securitySummary(db) {
5048
5044
  const dismissJoin = attached ? "LEFT JOIN triage.dismissed_findings d ON d.finding_id = f.id" : "";
5049
5045
  const base = `FROM findings f JOIN sessions s ON s.id = f.session_id ${dismissJoin}`;
5050
5046
  const openWhere = attached ? `WHERE d.finding_id IS NULL AND NOT ${MUTED_SQL}` : "";
5051
- const scalar = (sql) => db.prepare(sql).get().n;
5047
+ const scalar = (sql) => queryGet(db, sql).n;
5052
5048
  const total = scalar(`SELECT COUNT(*) n ${base} ${openWhere}`);
5053
5049
  const sessions_flagged = scalar(`SELECT COUNT(DISTINCT f.session_id) n ${base} ${openWhere}`);
5054
5050
  const dismissed = attached ? scalar(`SELECT COUNT(*) n FROM findings f LEFT JOIN triage.dismissed_findings d ON d.finding_id = f.id WHERE d.finding_id IS NOT NULL`) : 0;
5055
5051
  const muted = attached ? scalar(`SELECT COUNT(*) n ${base} WHERE d.finding_id IS NULL AND ${MUTED_SQL}`) : 0;
5056
- const by_severity = db.prepare(`SELECT f.severity AS severity, COUNT(*) n ${base} ${openWhere} GROUP BY f.severity ORDER BY ${SEVERITY_RANK_SQL} DESC`).all();
5057
- const by_category = db.prepare(`SELECT f.category AS category, COUNT(*) n ${base} ${openWhere} GROUP BY f.category ORDER BY n DESC`).all();
5058
- const by_rule = db.prepare(`SELECT f.rule_id AS rule_id, f.category AS category, f.title AS title, COUNT(*) n, MAX(${SEVERITY_RANK_SQL}) AS rank
5059
- ${base} ${openWhere} GROUP BY f.rule_id ORDER BY rank DESC, n DESC`).all();
5052
+ const by_severity = queryAll(db, `SELECT f.severity AS severity, COUNT(*) n ${base} ${openWhere} GROUP BY f.severity ORDER BY ${SEVERITY_RANK_SQL} DESC`);
5053
+ const by_category = queryAll(db, `SELECT f.category AS category, COUNT(*) n ${base} ${openWhere} GROUP BY f.category ORDER BY n DESC`);
5054
+ const by_rule = queryAll(db, `SELECT f.rule_id AS rule_id, f.category AS category, f.title AS title, COUNT(*) n, MAX(${SEVERITY_RANK_SQL}) AS rank
5055
+ ${base} ${openWhere} GROUP BY f.rule_id ORDER BY rank DESC, n DESC`);
5060
5056
  return { total, sessions_flagged, dismissed, muted, by_severity, by_category, by_rule, categories };
5061
5057
  }
5062
5058
 
@@ -5113,6 +5109,12 @@ function addUsage(acc, r) {
5113
5109
  acc.cache_creation += r.cw ?? 0;
5114
5110
  acc.cache_read += r.cr ?? 0;
5115
5111
  }
5112
+ var usageForCost = (u) => ({
5113
+ input_tokens: u.i ?? 0,
5114
+ output_tokens: u.o ?? 0,
5115
+ cache_creation_input_tokens: u.cw ?? 0,
5116
+ cache_read_input_tokens: u.cr ?? 0
5117
+ });
5116
5118
  function pct(sorted, p) {
5117
5119
  if (sorted.length === 0)
5118
5120
  return 0;
@@ -5132,11 +5134,11 @@ function workflowAgg(db, f) {
5132
5134
  if (!tableExists(db, "workflow_results"))
5133
5135
  return empty;
5134
5136
  const ww = workflowWhere(f);
5135
- const rows = db.prepare(`SELECT COALESCE(status, '(unknown)') status, COUNT(*) n,
5136
- SUM(COALESCE(total_tokens, 0)) tokens,
5137
- SUM(COALESCE(duration_ms, 0)) dur,
5138
- SUM(CASE WHEN duration_ms IS NOT NULL THEN 1 ELSE 0 END) dur_n
5139
- FROM workflow_results ${ww.sql} GROUP BY status ORDER BY n DESC`).all(...ww.params);
5137
+ const rows = queryAll(db, `SELECT COALESCE(status, '(unknown)') status, COUNT(*) n,
5138
+ SUM(COALESCE(total_tokens, 0)) tokens,
5139
+ SUM(COALESCE(duration_ms, 0)) dur,
5140
+ SUM(CASE WHEN duration_ms IS NOT NULL THEN 1 ELSE 0 END) dur_n
5141
+ FROM workflow_results ${ww.sql} GROUP BY status ORDER BY n DESC`, ...ww.params);
5140
5142
  let total = 0, completed = 0, failed = 0, tokens = 0, durSum = 0, durN = 0;
5141
5143
  const by_status = rows.map((r) => {
5142
5144
  total += r.n;
@@ -5162,32 +5164,32 @@ function workflowAgg(db, f) {
5162
5164
  }
5163
5165
  function dashboardOverview(db, f) {
5164
5166
  const w = sessionWhere(f);
5165
- const counts = db.prepare(`SELECT
5166
- COUNT(*) sessions,
5167
- SUM(CASE WHEN is_sidechain=0 THEN 1 ELSE 0 END) main,
5168
- SUM(CASE WHEN is_sidechain=1 THEN 1 ELSE 0 END) subagent,
5169
- SUM(turn_count) turns,
5170
- COUNT(DISTINCT project_id) projects
5171
- FROM sessions s ${w.sql}`).get(...w.params);
5172
- const toolCount = db.prepare(`SELECT COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id ${w.sql}`).get(...w.params).n;
5173
- const usage = db.prepare(`SELECT t.model model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,
5174
- SUM(t.cache_creation_input_tokens) cw, SUM(t.cache_read_input_tokens) cr
5175
- FROM token_usage t JOIN sessions s ON s.id = t.session_id ${w.sql}
5176
- GROUP BY t.model`).all(...w.params);
5167
+ const counts = queryGet(db, `SELECT
5168
+ COUNT(*) sessions,
5169
+ SUM(CASE WHEN is_sidechain=0 THEN 1 ELSE 0 END) main,
5170
+ SUM(CASE WHEN is_sidechain=1 THEN 1 ELSE 0 END) subagent,
5171
+ SUM(turn_count) turns,
5172
+ COUNT(DISTINCT project_id) projects
5173
+ FROM sessions s ${w.sql}`, ...w.params);
5174
+ 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;
5175
+ const usage = queryAll(db, `SELECT t.model model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,
5176
+ SUM(t.cache_creation_input_tokens) cw, SUM(t.cache_read_input_tokens) cr
5177
+ FROM token_usage t JOIN sessions s ON s.id = t.session_id ${w.sql}
5178
+ GROUP BY t.model`, ...w.params);
5177
5179
  const tokens = zeroSplit();
5178
5180
  let cost = 0;
5179
5181
  const unpriced = [];
5180
5182
  for (const u of usage) {
5181
5183
  addUsage(tokens, u);
5182
- cost += costForUsage(u.model, { input_tokens: u.i, output_tokens: u.o, cache_creation_input_tokens: u.cw, cache_read_input_tokens: u.cr });
5184
+ cost += costForUsage(u.model, usageForCost(u));
5183
5185
  if (u.model && !rateForModel(u.model) && (u.i || u.o || u.cw || u.cr))
5184
5186
  unpriced.push(u.model);
5185
5187
  }
5186
5188
  const totalTokens = tokens.input + tokens.output + tokens.cache_creation + tokens.cache_read;
5187
- const durs = db.prepare(`SELECT tn.duration_ms d FROM turns tn JOIN sessions s ON s.id = tn.session_id
5188
- ${w.sql ? w.sql + " AND" : "WHERE"} tn.duration_ms IS NOT NULL ORDER BY tn.duration_ms`).all(...w.params).map((r) => r.d);
5189
- const sessDurs = db.prepare(`SELECT s.duration_ms d FROM sessions s
5190
- ${w.sql ? w.sql + " AND" : "WHERE"} s.is_sidechain = 0 AND s.duration_ms IS NOT NULL ORDER BY s.duration_ms`).all(...w.params).map((r) => r.d);
5189
+ const durs = queryAll(db, `SELECT tn.duration_ms d FROM turns tn JOIN sessions s ON s.id = tn.session_id
5190
+ ${w.sql ? w.sql + " AND" : "WHERE"} tn.duration_ms IS NOT NULL ORDER BY tn.duration_ms`, ...w.params).map((r) => r.d);
5191
+ const sessDurs = queryAll(db, `SELECT s.duration_ms d FROM sessions s
5192
+ ${w.sql ? w.sql + " AND" : "WHERE"} s.is_sidechain = 0 AND s.duration_ms IS NOT NULL ORDER BY s.duration_ms`, ...w.params).map((r) => r.d);
5191
5193
  return {
5192
5194
  range: { from: f.from ?? null, to: f.to ?? null, source: f.source ?? null },
5193
5195
  sessions: counts.sessions ?? 0,
@@ -5209,7 +5211,7 @@ function dashboardOverview(db, f) {
5209
5211
  }
5210
5212
  function chooseBucket(db, f) {
5211
5213
  const w = sessionWhere(f);
5212
- const r = db.prepare(`SELECT MIN(started_at) mn, MAX(started_at) mx FROM sessions s ${w.sql}`).get(...w.params);
5214
+ const r = queryGet(db, `SELECT MIN(started_at) mn, MAX(started_at) mx FROM sessions s ${w.sql}`, ...w.params);
5213
5215
  if (!r?.mn || !r?.mx)
5214
5216
  return "day";
5215
5217
  const spanDays = (Date.parse(r.mx) - Date.parse(r.mn)) / 864e5;
@@ -5237,26 +5239,26 @@ function dashboardTimeseries(db, f, bucketParam) {
5237
5239
  }
5238
5240
  return r;
5239
5241
  };
5240
- for (const u of db.prepare(`SELECT ${expr} b, t.model model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,
5241
- SUM(t.cache_creation_input_tokens) cw, SUM(t.cache_read_input_tokens) cr
5242
- FROM token_usage t JOIN sessions s ON s.id = t.session_id
5243
- ${w.sql} GROUP BY b, t.model`).all(...w.params)) {
5242
+ for (const u of queryAll(db, `SELECT ${expr} b, t.model model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,
5243
+ SUM(t.cache_creation_input_tokens) cw, SUM(t.cache_read_input_tokens) cr
5244
+ FROM token_usage t JOIN sessions s ON s.id = t.session_id
5245
+ ${w.sql} GROUP BY b, t.model`, ...w.params)) {
5244
5246
  if (!u.b)
5245
5247
  continue;
5246
5248
  const r = get(u.b);
5247
5249
  addUsage(r, u);
5248
- r.cost += costForUsage(u.model, { input_tokens: u.i, output_tokens: u.o, cache_creation_input_tokens: u.cw, cache_read_input_tokens: u.cr });
5250
+ r.cost += costForUsage(u.model, usageForCost(u));
5249
5251
  }
5250
- for (const s of db.prepare(`SELECT ${expr} b, COUNT(*) n FROM sessions s ${w.sql} GROUP BY b`).all(...w.params)) {
5252
+ for (const s of queryAll(db, `SELECT ${expr} b, COUNT(*) n FROM sessions s ${w.sql} GROUP BY b`, ...w.params)) {
5251
5253
  if (s.b)
5252
5254
  get(s.b).sessions = s.n;
5253
5255
  }
5254
- for (const t of db.prepare(`SELECT ${expr} b, COUNT(*) n FROM turns tn JOIN sessions s ON s.id = tn.session_id ${w.sql} GROUP BY b`).all(...w.params)) {
5256
+ 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)) {
5255
5257
  if (t.b)
5256
5258
  get(t.b).turns = t.n;
5257
5259
  }
5258
- for (const e of db.prepare(`SELECT ${expr} b, tc.error_type et, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
5259
- ${w.sql ? w.sql + " AND" : "WHERE"} tc.status = 'error' GROUP BY b, tc.error_type`).all(...w.params)) {
5260
+ 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
5261
+ ${w.sql ? w.sql + " AND" : "WHERE"} tc.status = 'error' GROUP BY b, tc.error_type`, ...w.params)) {
5260
5262
  if (!e.b)
5261
5263
  continue;
5262
5264
  const r = get(e.b);
@@ -5272,53 +5274,60 @@ function dashboardTimeseries(db, f, bucketParam) {
5272
5274
  }
5273
5275
  var andWhere = (w) => w.sql ? w.sql + " AND" : "WHERE";
5274
5276
  function modelBreakdown(db, w) {
5275
- const modelRows = db.prepare(`SELECT t.model model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,
5276
- SUM(t.cache_creation_input_tokens) cw, SUM(t.cache_read_input_tokens) cr,
5277
- COUNT(DISTINCT t.session_id) sessions
5278
- FROM token_usage t JOIN sessions s ON s.id = t.session_id ${w.sql}
5279
- 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`).all(...w.params);
5277
+ const modelRows = queryAll(
5278
+ db,
5279
+ // COALESCE the bucket label, exactly as by_source does for a null source_id: token_usage.model is
5280
+ // nullable and this GROUP BY — unlike the model FILTER list — does not exclude nulls, so a
5281
+ // null-model bucket reaches the chart. It used to render as a nameless bar.
5282
+ `SELECT COALESCE(t.model, '(unknown)') model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,
5283
+ SUM(t.cache_creation_input_tokens) cw, SUM(t.cache_read_input_tokens) cr,
5284
+ COUNT(DISTINCT t.session_id) sessions
5285
+ FROM token_usage t JOIN sessions s ON s.id = t.session_id ${w.sql}
5286
+ 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`,
5287
+ ...w.params
5288
+ );
5280
5289
  return modelRows.map((u) => ({
5281
5290
  model: u.model,
5282
5291
  tokens: { input: u.i ?? 0, output: u.o ?? 0, cache_creation: u.cw ?? 0, cache_read: u.cr ?? 0 },
5283
5292
  total_tokens: (u.i ?? 0) + (u.o ?? 0) + (u.cw ?? 0) + (u.cr ?? 0),
5284
- cost: Number(costForUsage(u.model, { input_tokens: u.i, output_tokens: u.o, cache_creation_input_tokens: u.cw, cache_read_input_tokens: u.cr }).toFixed(4)),
5293
+ cost: Number(costForUsage(u.model, usageForCost(u)).toFixed(4)),
5285
5294
  sessions: u.sessions ?? 0,
5286
5295
  priced: !!rateForModel(u.model)
5287
5296
  }));
5288
5297
  }
5289
5298
  function sourceBreakdown(db, w) {
5290
- return db.prepare(`SELECT COALESCE(s.source_id, '(none)') source, COUNT(*) sessions, SUM(s.turn_count) turns
5291
- FROM sessions s ${w.sql} GROUP BY s.source_id ORDER BY sessions DESC`).all(...w.params);
5299
+ return queryAll(db, `SELECT COALESCE(s.source_id, '(none)') source, COUNT(*) sessions, SUM(s.turn_count) turns
5300
+ FROM sessions s ${w.sql} GROUP BY s.source_id ORDER BY sessions DESC`, ...w.params);
5292
5301
  }
5293
5302
  function classificationBreakdowns(db, w) {
5294
5303
  const mainW = { sql: w.sql ? w.sql + " AND s.is_sidechain = 0" : "WHERE s.is_sidechain = 0", params: w.params };
5295
- const byCategory = db.prepare(`SELECT c.category, COUNT(*) n FROM classifications c JOIN sessions s ON s.id = c.target_id
5296
- ${mainW.sql} GROUP BY c.category ORDER BY n DESC`).all(...mainW.params);
5297
- const byComplexity = db.prepare(`SELECT c.complexity_band band, COUNT(*) n FROM classifications c JOIN sessions s ON s.id = c.target_id
5298
- ${mainW.sql} GROUP BY c.complexity_band ORDER BY n DESC`).all(...mainW.params);
5304
+ const byCategory = queryAll(db, `SELECT c.category, COUNT(*) n FROM classifications c JOIN sessions s ON s.id = c.target_id
5305
+ ${mainW.sql} GROUP BY c.category ORDER BY n DESC`, ...mainW.params);
5306
+ const byComplexity = queryAll(db, `SELECT c.complexity_band band, COUNT(*) n FROM classifications c JOIN sessions s ON s.id = c.target_id
5307
+ ${mainW.sql} GROUP BY c.complexity_band ORDER BY n DESC`, ...mainW.params);
5299
5308
  return { byCategory, byComplexity };
5300
5309
  }
5301
5310
  function toolBreakdown(db, w) {
5302
- return db.prepare(`SELECT tc.tool_name name, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
5303
- ${w.sql} GROUP BY tc.tool_name ORDER BY n DESC LIMIT 20`).all(...w.params);
5311
+ return queryAll(db, `SELECT tc.tool_name name, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
5312
+ ${w.sql} GROUP BY tc.tool_name ORDER BY n DESC LIMIT 20`, ...w.params);
5304
5313
  }
5305
5314
  function skillBreakdowns(db, w) {
5306
- const skills = db.prepare(`SELECT tc.skill_name name, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
5307
- ${andWhere(w)} tc.skill_name IS NOT NULL GROUP BY tc.skill_name ORDER BY n DESC LIMIT 20`).all(...w.params);
5308
- const skillVersions = db.prepare(`SELECT tc.skill_name name, sk.id version_id, sk.summary, sk.last_seen, COUNT(*) n
5309
- FROM tool_calls tc
5310
- JOIN skills sk ON sk.id = tc.skill_id
5311
- JOIN sessions s ON s.id = tc.session_id
5312
- ${andWhere(w)} tc.skill_id IS NOT NULL
5313
- GROUP BY tc.skill_name, sk.id ORDER BY name, n DESC`).all(...w.params);
5315
+ const skills = queryAll(db, `SELECT tc.skill_name name, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
5316
+ ${andWhere(w)} tc.skill_name IS NOT NULL GROUP BY tc.skill_name ORDER BY n DESC LIMIT 20`, ...w.params);
5317
+ const skillVersions = queryAll(db, `SELECT tc.skill_name name, sk.id version_id, sk.summary, sk.last_seen, COUNT(*) n
5318
+ FROM tool_calls tc
5319
+ JOIN skills sk ON sk.id = tc.skill_id
5320
+ JOIN sessions s ON s.id = tc.session_id
5321
+ ${andWhere(w)} tc.skill_id IS NOT NULL
5322
+ GROUP BY tc.skill_name, sk.id ORDER BY name, n DESC`, ...w.params);
5314
5323
  return { skills, skillVersions };
5315
5324
  }
5316
5325
  function subagentFanoutBreakdown(db, w) {
5317
- const subagentByType = db.prepare(`SELECT tc.agent_type type, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
5318
- ${andWhere(w)} tc.agent_type IS NOT NULL GROUP BY tc.agent_type ORDER BY n DESC`).all(...w.params);
5319
- const perSession = db.prepare(`SELECT COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
5320
- ${andWhere(w)} s.is_sidechain = 0 AND tc.tool_name IN ('Agent','Task')
5321
- GROUP BY tc.session_id`).all(...w.params);
5326
+ const subagentByType = queryAll(db, `SELECT tc.agent_type type, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
5327
+ ${andWhere(w)} tc.agent_type IS NOT NULL GROUP BY tc.agent_type ORDER BY n DESC`, ...w.params);
5328
+ const perSession = queryAll(db, `SELECT COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
5329
+ ${andWhere(w)} s.is_sidechain = 0 AND tc.tool_name IN ('Agent','Task')
5330
+ GROUP BY tc.session_id`, ...w.params);
5322
5331
  const counts = perSession.map((r) => r.n);
5323
5332
  return {
5324
5333
  by_type: subagentByType,
@@ -5329,8 +5338,8 @@ function subagentFanoutBreakdown(db, w) {
5329
5338
  };
5330
5339
  }
5331
5340
  function errorTypeBreakdown(db, w) {
5332
- const errorRows = db.prepare(`SELECT COALESCE(tc.error_type, 'other') type, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
5333
- ${andWhere(w)} tc.status = 'error' GROUP BY tc.error_type ORDER BY n DESC`).all(...w.params);
5341
+ 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
5342
+ ${andWhere(w)} tc.status = 'error' GROUP BY tc.error_type ORDER BY n DESC`, ...w.params);
5334
5343
  let failures = 0;
5335
5344
  let rejections = 0;
5336
5345
  const byType = errorRows.map((r) => {