@swestash/agent-lens 0.9.3 → 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.
@@ -2424,7 +2424,7 @@ function parseStored(json, where) {
2424
2424
  }
2425
2425
 
2426
2426
  // ../ingest/dist/classify.js
2427
- var CLASSIFIER_VERSION = 2;
2427
+ var CLASSIFIER_VERSION = 3;
2428
2428
  var CATEGORIES = ["bugfix", "refactor", "docs", "ops", "review", "feature", "chore"];
2429
2429
  var BANDS = [
2430
2430
  { max: 22, band: "trivial" },
@@ -2467,14 +2467,23 @@ function band(score) {
2467
2467
  return "xl";
2468
2468
  }
2469
2469
  var clamp01 = (x) => x < 0 ? 0 : x > 1 ? 1 : x;
2470
+ var WORD_RE = /* @__PURE__ */ new Map();
2471
+ function wordRe(phrase) {
2472
+ let re = WORD_RE.get(phrase);
2473
+ if (!re) {
2474
+ const escaped = phrase.trim().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2475
+ re = new RegExp(`\\b${escaped}\\b`, "g");
2476
+ WORD_RE.set(phrase, re);
2477
+ }
2478
+ return re;
2479
+ }
2470
2480
  function hits(text, words) {
2471
2481
  let n = 0;
2472
2482
  for (const w of words) {
2473
- let i = text.indexOf(w);
2474
- while (i !== -1) {
2483
+ const re = wordRe(w);
2484
+ re.lastIndex = 0;
2485
+ while (re.exec(text) !== null)
2475
2486
  n++;
2476
- i = text.indexOf(w, i + w.length);
2477
- }
2478
2487
  }
2479
2488
  return n;
2480
2489
  }
@@ -2496,9 +2505,9 @@ function scoreCategories(s, text, files) {
2496
2505
  scores.bugfix += hits(t, ["bug", "fix", "broken", "crash", "regression", "traceback", "stack trace", "failing", "error", "exception", "not working"]) * 1;
2497
2506
  scores.refactor += hits(t, ["refactor", "clean up", "cleanup", "rename", "extract", "simplify", "dead code", "tidy", "reorganize", "restructure"]) * 1;
2498
2507
  scores.docs += hits(t, ["readme", "document", "documentation", "changelog", "docstring", "comment", "write docs", "usage guide"]) * 1;
2499
- scores.ops += hits(t, ["deploy", "docker", "pipeline", "ci/cd", "ci ", "systemd", "infra", "kubernetes", "terraform", "release", "rollback", "container"]) * 1;
2508
+ scores.ops += hits(t, ["deploy", "docker", "pipeline", "ci/cd", "ci", "systemd", "infra", "kubernetes", "terraform", "release", "rollback", "container"]) * 1;
2500
2509
  scores.review += hits(t, ["review", "audit", "inspect", "look over", "feedback on"]) * 1;
2501
- scores.feature += hits(t, ["add ", "implement", "build ", "create ", "feature", "support for", "new feature"]) * 0.8;
2510
+ scores.feature += hits(t, ["add", "implement", "build", "create", "feature", "support for", "new feature"]) * 0.8;
2502
2511
  const docFiles = s.loc.files > 0 ? countMatch(files, DOC_EXT) : 0;
2503
2512
  const opsFiles = s.loc.files > 0 ? countMatch(files, OPS_FILE) : 0;
2504
2513
  if (s.loc.files > 0) {
@@ -4305,6 +4314,9 @@ function tableExists(db, name) {
4305
4314
  function queryAll(db, sql, ...params) {
4306
4315
  return db.prepare(sql).all(...params);
4307
4316
  }
4317
+ function queryGet(db, sql, ...params) {
4318
+ return db.prepare(sql).get(...params);
4319
+ }
4308
4320
  function pushDateRange(clauses, params, col, from, to) {
4309
4321
  if (from) {
4310
4322
  clauses.push(`date(${col}) >= date(?)`);
@@ -4325,6 +4337,16 @@ function orderBy(columns, sort, fallback, dir) {
4325
4337
  const col = sort !== void 0 && Object.hasOwn(columns, sort) ? columns[sort] : columns[fallback];
4326
4338
  return `${col} ${dir === "asc" ? "ASC" : "DESC"}`;
4327
4339
  }
4340
+ function pageLimit(raw, fallback, max) {
4341
+ const n = Math.floor(Number(raw));
4342
+ if (!Number.isFinite(n) || n < 1)
4343
+ return Math.min(fallback, max);
4344
+ return Math.min(n, max);
4345
+ }
4346
+ function pageOffset(raw) {
4347
+ const n = Math.floor(Number(raw));
4348
+ return Number.isFinite(n) && n > 0 ? n : 0;
4349
+ }
4328
4350
  function pushGrouped(map, key, value) {
4329
4351
  const list = map.get(key);
4330
4352
  if (list)
@@ -4400,22 +4422,17 @@ function extractParts(rawJson) {
4400
4422
  return { text: text || null, thinking: thinking || null };
4401
4423
  }
4402
4424
  function listSources(db) {
4403
- return db.prepare(
4404
- // Count MAIN sessions only (is_sidechain = 0): the dropdown filters the session list, which
4405
- // defaults to main-only, so a source's count must match what you'd see — not be inflated by the
4406
- // many subagent sidechains each task spawns.
4407
- `SELECT s.id, s.label, s.agent_id, s.config_dir,
4408
- (SELECT COUNT(*) FROM sessions x WHERE x.source_id = s.id AND x.is_sidechain = 0) AS session_count
4409
- FROM sources s ORDER BY s.label`
4410
- ).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`);
4411
4428
  }
4412
4429
  function listProjects(db) {
4413
- return db.prepare(`SELECT p.id, p.path,
4414
- (SELECT COUNT(*) FROM sessions x WHERE x.project_id = p.id AND x.is_sidechain = 0) AS session_count
4415
- 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`);
4416
4433
  }
4417
4434
  function listModels(db) {
4418
- 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);
4419
4436
  }
4420
4437
  function toFtsQuery(raw) {
4421
4438
  return raw.trim().split(/\s+/).filter(Boolean).map((t) => `"${t.replace(/"/g, '""')}"`).join(" ");
@@ -4452,8 +4469,8 @@ function listSessions(db, f) {
4452
4469
  params.push(toFtsQuery(f.q), like, like, like);
4453
4470
  }
4454
4471
  const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
4455
- const total = db.prepare(`SELECT COUNT(*) n FROM sessions s ${whereSql}`).get(...params).n;
4456
- 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,
4457
4474
  s.duration_ms, s.event_count, s.turn_count, p.path AS project_path,
4458
4475
  (SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models,
4459
4476
  (SELECT COUNT(*) FROM tool_calls tc WHERE tc.session_id = s.id) AS tool_call_count,
@@ -4472,10 +4489,9 @@ function listSessions(db, f) {
4472
4489
  };
4473
4490
  const sortKey = f.sort ?? "started";
4474
4491
  const JS_SORT = /* @__PURE__ */ new Set(["tokens", "cost", "errors", "security"]);
4475
- let rows;
4492
+ let sessions;
4476
4493
  if (JS_SORT.has(sortKey)) {
4477
- const all = db.prepare(baseSelect).all(...params);
4478
- attachSessionCost(db, all);
4494
+ const all = attachSessionCost(db, queryAll(db, baseSelect, ...params));
4479
4495
  const sign = f.dir === "asc" ? 1 : -1;
4480
4496
  const metric = (r) => {
4481
4497
  switch (sortKey) {
@@ -4496,12 +4512,11 @@ function listSessions(db, f) {
4496
4512
  const c = (metric(a) - metric(b)) * sign;
4497
4513
  return c !== 0 ? c : String(a.id).localeCompare(String(b.id));
4498
4514
  });
4499
- rows = all.slice(f.offset, f.offset + f.limit);
4515
+ sessions = all.slice(f.offset, f.offset + f.limit);
4500
4516
  } else {
4501
- rows = db.prepare(`${baseSelect} ORDER BY ${orderBy(NATIVE_ORDER, sortKey, "started", f.dir)}, s.id ASC LIMIT ? OFFSET ?`).all(...params, f.limit, f.offset);
4502
- 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));
4503
4518
  }
4504
- return { total, sessions: rows };
4519
+ return { total, sessions };
4505
4520
  }
4506
4521
  function attachSessionCost(db, rows) {
4507
4522
  const usageBySession = /* @__PURE__ */ new Map();
@@ -4517,21 +4532,18 @@ function attachSessionCost(db, rows) {
4517
4532
  usageBySession.set(u.session_id, list);
4518
4533
  }
4519
4534
  }
4520
- for (const r of rows) {
4535
+ return rows.map(({ ai_title, slug, ...r }) => {
4521
4536
  const u = usageBySession.get(r.id) ?? [];
4522
4537
  const split = splitOf(u);
4523
- r.tokens = tokensOf(split);
4524
- r.token_split = split;
4525
- r.cost = costOf(u);
4526
- r.title = r.ai_title || r.slug || null;
4527
- }
4538
+ return { ...r, tokens: tokensOf(split), token_split: split, cost: costOf(u), title: ai_title || slug || null };
4539
+ });
4528
4540
  }
4529
4541
  function loadToolCalls(db, id) {
4530
- const toolRows = db.prepare(`SELECT id, event_uuid, tool_name, skill_name, skill_id, agent_type, spawned_session_id,
4531
- workflow_run_id, workflow_name, status, error_type,
4532
- total_duration_ms, total_tokens, input_json, result_summary,
4533
- (SELECT COUNT(*) FROM sessions s WHERE s.workflow_run_id = tool_calls.workflow_run_id) AS workflow_agent_count
4534
- 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);
4535
4547
  if (tableExists(db, "tool_results")) {
4536
4548
  const getFull = db.prepare("SELECT text, bytes FROM tool_results WHERE session_id = ? AND name = ?");
4537
4549
  for (const t of toolRows) {
@@ -4549,8 +4561,8 @@ function loadFindings(db, id, toolRows) {
4549
4561
  if (!tableExists(db, "findings"))
4550
4562
  return [];
4551
4563
  const sessionFindings = [];
4552
- const findingRows = db.prepare(`SELECT id, tool_call_id, event_uuid, turn_id, rule_id, category, framework_ref, severity, title, evidence, signals_json
4553
- 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);
4554
4566
  const byToolCall = /* @__PURE__ */ new Map();
4555
4567
  for (const fr of findingRows) {
4556
4568
  const f = {
@@ -4564,6 +4576,7 @@ function loadFindings(db, id, toolRows) {
4564
4576
  severity: fr.severity,
4565
4577
  title: fr.title,
4566
4578
  evidence: fr.evidence,
4579
+ // Asserted, not validated: safeJson returns unknown and this is the assertion (ADR-026).
4567
4580
  signals: safeJson(fr.signals_json)
4568
4581
  };
4569
4582
  sessionFindings.push(f);
@@ -4571,13 +4584,16 @@ function loadFindings(db, id, toolRows) {
4571
4584
  pushGrouped(byToolCall, fr.tool_call_id, f);
4572
4585
  }
4573
4586
  for (const t of toolRows)
4574
- t.findings = byToolCall.get(t.id) ?? [];
4587
+ t.findings = t.id ? byToolCall.get(t.id) ?? [] : [];
4575
4588
  sessionFindings.sort((a, b) => severityRank(b.severity) - severityRank(a.severity));
4576
4589
  return sessionFindings;
4577
4590
  }
4591
+ function toResponseToolCall({ event_uuid: _e, error_type: _t, ...call }) {
4592
+ return call;
4593
+ }
4578
4594
  function loadEvents(db, id, toolRows) {
4579
- const eventRows = db.prepare(`SELECT uuid, type, role, timestamp, model, is_sidechain, turn_id, raw_json
4580
- 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);
4581
4597
  const toolsByEvent = /* @__PURE__ */ new Map();
4582
4598
  for (const t of toolRows) {
4583
4599
  if (!t.event_uuid)
@@ -4596,13 +4612,13 @@ function loadEvents(db, id, toolRows) {
4596
4612
  turn_id: e.turn_id,
4597
4613
  text,
4598
4614
  thinking,
4599
- toolCalls: toolsByEvent.get(e.uuid) ?? []
4615
+ toolCalls: (toolsByEvent.get(e.uuid) ?? []).map(toResponseToolCall)
4600
4616
  };
4601
4617
  });
4602
4618
  }
4603
4619
  function loadClassification(db, id) {
4604
- const cls = db.prepare(`SELECT category, complexity_score, complexity_band, signals_json, classifier_version
4605
- 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);
4606
4622
  if (!cls)
4607
4623
  return null;
4608
4624
  let signals = null;
@@ -4621,66 +4637,67 @@ function loadClassification(db, id) {
4621
4637
  function loadParent(db, session) {
4622
4638
  if (!session.parent_session_id)
4623
4639
  return null;
4624
- const p = db.prepare(`SELECT ps.id, ps.ai_title, ps.slug, t.seq AS turn_seq
4625
- FROM sessions ps LEFT JOIN turns t ON t.id = ?
4626
- 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);
4627
4643
  return p ? { id: p.id, title: p.ai_title || p.slug || null, turn_seq: p.turn_seq ?? null } : null;
4628
4644
  }
4629
4645
  function loadChildren(db, id) {
4630
4646
  const hasMeta = tableExists(db, "session_meta");
4631
- const children = db.prepare(`SELECT s.id, s.ai_title, s.slug, s.turn_count, s.started_at, s.workflow_run_id,
4632
- (SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models
4633
- ${metaProjection(hasMeta)}
4634
- FROM sessions s ${metaJoin(hasMeta)}
4635
- WHERE s.parent_session_id = ? ORDER BY s.started_at`).all(id);
4636
- for (const ch of children) {
4637
- 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 }) => {
4638
4653
  const u = sessionUsage(db, ch.id);
4639
- ch.tokens = tokensOf(splitOf(u));
4640
- ch.cost = costOf(u);
4641
- }
4642
- return children;
4654
+ return { ...ch, title: ai_title || slug || null, tokens: tokensOf(splitOf(u)), cost: costOf(u) };
4655
+ });
4643
4656
  }
4644
4657
  function loadWorkflowRuns(db, id) {
4645
4658
  const hasWR = tableExists(db, "workflow_results");
4646
- return db.prepare(`SELECT tc.workflow_run_id AS run_id, tc.workflow_name AS name, t.seq AS turn_seq,
4647
- (SELECT COUNT(*) FROM sessions s WHERE s.workflow_run_id = tc.workflow_run_id) AS agent_count
4648
- ${hasWR ? ", (SELECT status FROM workflow_results wr WHERE wr.run_id = tc.workflow_run_id) AS status" : ", NULL AS status"}
4649
- FROM tool_calls tc LEFT JOIN turns t ON t.id = tc.turn_id
4650
- WHERE tc.session_id = ? AND tc.workflow_run_id IS NOT NULL
4651
- 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);
4652
4665
  }
4653
4666
  function loadFileChanges(db, id) {
4654
4667
  if (!tableExists(db, "file_changes"))
4655
4668
  return [];
4656
- return db.prepare(`SELECT fc.id, fc.tool_call_id, fc.turn_id, fc.event_uuid, fc.file_path, fc.tool_name,
4657
- fc.lines_added, fc.lines_removed, fc.timestamp
4658
- 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);
4659
4672
  }
4660
- function attachSessionTotals(db, session, id, toolRows) {
4673
+ function withSessionTotals(db, session, id, toolRows) {
4661
4674
  const usage = sessionUsage(db, id);
4662
4675
  const split = splitOf(usage);
4663
- session.tokens = tokensOf(split);
4664
- session.token_split = split;
4665
- session.cost = costOf(usage);
4666
- session.title = session.ai_title || session.slug || null;
4667
- session.tool_call_count = toolRows.length;
4668
- session.tool_error_count = toolRows.filter((t) => t.status === "error").length;
4669
- session.tool_rejection_count = toolRows.filter((t) => t.status === "error" && t.error_type && errorKind(t.error_type) === "rejection").length;
4670
- 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
+ };
4671
4689
  }
4672
4690
  function getSession(db, id) {
4673
- const session = db.prepare(`SELECT s.*, p.path AS project_path FROM sessions s
4674
- 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);
4675
4693
  if (!session)
4676
4694
  return null;
4677
- 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);
4678
4696
  const toolRows = loadToolCalls(db, id);
4679
4697
  const findings = loadFindings(db, id, toolRows);
4680
4698
  const events = loadEvents(db, id, toolRows);
4681
- attachSessionTotals(db, session, id, toolRows);
4682
4699
  return {
4683
- session,
4700
+ session: withSessionTotals(db, session, id, toolRows),
4684
4701
  turns,
4685
4702
  events,
4686
4703
  classification: loadClassification(db, id),
@@ -4705,27 +4722,26 @@ function safeJson(s) {
4705
4722
  }
4706
4723
  function loadAgents(db, runId) {
4707
4724
  const hasMeta = tableExists(db, "session_meta");
4708
- const agents = db.prepare(`SELECT s.id, s.ai_title, s.slug, s.turn_count, s.started_at, s.ended_at, s.duration_ms,
4709
- (SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models
4710
- ${metaProjection(hasMeta)}
4711
- FROM sessions s ${metaJoin(hasMeta)}
4712
- 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);
4713
4730
  let total_tokens = 0;
4714
4731
  let total_cost = 0;
4715
4732
  let started_at = null;
4716
4733
  let ended_at = null;
4717
- for (const a of agents) {
4718
- a.title = a.ai_title || a.slug || null;
4719
- const u = sessionUsage(db, a.id);
4720
- a.tokens = tokensOf(splitOf(u));
4721
- 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) };
4722
4737
  total_tokens += a.tokens;
4723
4738
  total_cost += a.cost;
4724
4739
  if (a.started_at && (!started_at || a.started_at < started_at))
4725
4740
  started_at = a.started_at;
4726
4741
  if (a.ended_at && (!ended_at || a.ended_at > ended_at))
4727
4742
  ended_at = a.ended_at;
4728
- }
4743
+ return a;
4744
+ });
4729
4745
  const duration_ms = started_at && ended_at ? new Date(ended_at).getTime() - new Date(started_at).getTime() : null;
4730
4746
  return {
4731
4747
  agents,
@@ -4735,9 +4751,9 @@ function loadAgents(db, runId) {
4735
4751
  function parseCompletion(db, wf) {
4736
4752
  if (!wf.tool_use_id || !wf.parent_session_id)
4737
4753
  return null;
4738
- const ev = db.prepare(`SELECT raw_json FROM events
4739
- WHERE session_id = ? AND text LIKE '%<task-notification>%' AND text LIKE ?
4740
- 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}%`);
4741
4757
  if (!ev)
4742
4758
  return null;
4743
4759
  const { text } = extractParts(ev.raw_json);
@@ -4745,9 +4761,9 @@ function parseCompletion(db, wf) {
4745
4761
  return { status: xmlTag(t, "status"), summary: xmlTag(t, "summary"), result: xmlTag(t, "result"), failures: xmlTag(t, "failures") };
4746
4762
  }
4747
4763
  function loadRunSidecar(db, runId, completion) {
4748
- const wr = tableExists(db, "workflow_results") ? db.prepare(`SELECT status, summary, default_model, result_json, phases_json, logs_json, progress_json,
4749
- agent_count, total_tokens, total_tool_calls, duration_ms, started_at, ended_at
4750
- 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;
4751
4767
  if (!wr)
4752
4768
  return { wr: null, run: null, completion };
4753
4769
  const run = {
@@ -4760,6 +4776,8 @@ function loadRunSidecar(db, runId, completion) {
4760
4776
  duration_ms: wr.duration_ms ?? null,
4761
4777
  started_at: wr.started_at ?? null,
4762
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.
4763
4781
  phases: safeJson(wr.phases_json),
4764
4782
  logs: safeJson(wr.logs_json),
4765
4783
  // The per-phase/per-agent event timeline (workflowProgress) — lets the page render a phase graph
@@ -4777,16 +4795,16 @@ function loadRunSidecar(db, runId, completion) {
4777
4795
  return { wr, run, completion };
4778
4796
  }
4779
4797
  function getWorkflow(db, runId) {
4780
- 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,
4781
- tc.input_json AS input_json,
4782
- tc.session_id AS parent_session_id, t.seq AS turn_seq,
4783
- ps.ai_title AS parent_ai_title, ps.slug AS parent_slug
4784
- FROM tool_calls tc
4785
- LEFT JOIN turns t ON t.id = tc.turn_id
4786
- LEFT JOIN sessions ps ON ps.id = tc.session_id
4787
- WHERE tc.workflow_run_id = ? AND tc.tool_name = 'Workflow'
4788
- ORDER BY t.seq
4789
- 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);
4790
4808
  if (!wf)
4791
4809
  return null;
4792
4810
  const { agents, stats } = loadAgents(db, runId);
@@ -4816,20 +4834,18 @@ function listSkills(db, f = {}) {
4816
4834
  where.push("s.source_id = ?"), params.push(f.source);
4817
4835
  if (f.project)
4818
4836
  where.push("s.project_id = ?"), params.push(f.project);
4819
- const rows = db.prepare(`SELECT tc.skill_name AS name,
4820
- COUNT(*) AS call_count,
4821
- COUNT(DISTINCT tc.skill_id) AS version_count,
4822
- MAX(e.timestamp) AS last_fired,
4823
- GROUP_CONCAT(DISTINCT s.source_id) AS sources
4824
- FROM tool_calls tc
4825
- JOIN sessions s ON s.id = tc.session_id
4826
- LEFT JOIN events e ON e.uuid = tc.event_uuid
4827
- WHERE ${where.join(" AND ")}
4828
- GROUP BY tc.skill_name
4829
- ORDER BY call_count DESC, name`).all(...params);
4830
- for (const r of rows)
4831
- r.sources = r.sources ? String(r.sources).split(",").filter(Boolean) : [];
4832
- 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) : [] }));
4833
4849
  }
4834
4850
  function listFiles(db, f = {}) {
4835
4851
  if (!tableExists(db, "file_changes"))
@@ -4852,7 +4868,7 @@ function listFiles(db, f = {}) {
4852
4868
  MAX(fc.timestamp) AS last_ts
4853
4869
  FROM file_changes fc${whereSql}
4854
4870
  GROUP BY fc.project_id, fc.file_path`;
4855
- const total = db.prepare(`SELECT COUNT(*) n FROM (${grouped})`).get(...params).n;
4871
+ const total = queryGet(db, `SELECT COUNT(*) n FROM (${grouped})`, ...params).n;
4856
4872
  const ORDER = {
4857
4873
  last_ts: "last_ts",
4858
4874
  first_ts: "first_ts",
@@ -4862,9 +4878,9 @@ function listFiles(db, f = {}) {
4862
4878
  };
4863
4879
  const limit = Math.min(f.limit ?? 50, 200);
4864
4880
  const offset = f.offset ?? 0;
4865
- const files = db.prepare(`SELECT g.*, p.path AS project_path FROM (${grouped}) g
4866
- LEFT JOIN projects p ON p.id = g.project_id
4867
- 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);
4868
4884
  return { total, files };
4869
4885
  }
4870
4886
  function getFileTimeline(db, filePath, projectId2) {
@@ -4874,15 +4890,15 @@ function getFileTimeline(db, filePath, projectId2) {
4874
4890
  let scope = "fc.file_path = ?";
4875
4891
  if (projectId2)
4876
4892
  scope += " AND fc.project_id = ?", params.push(projectId2);
4877
- const rows = db.prepare(`SELECT fc.id, fc.session_id, fc.turn_id, fc.event_uuid, fc.tool_name, fc.lines_added,
4878
- fc.lines_removed, fc.timestamp, fc.project_id,
4879
- s.ai_title, s.slug, s.source_id, s.started_at, t.seq AS turn_seq, t.prompt_preview,
4880
- p.path AS project_path
4881
- FROM file_changes fc
4882
- JOIN sessions s ON s.id = fc.session_id
4883
- LEFT JOIN turns t ON t.id = fc.turn_id
4884
- LEFT JOIN projects p ON p.id = fc.project_id
4885
- 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);
4886
4902
  if (!rows.length)
4887
4903
  return null;
4888
4904
  const cat = tableExists(db, "classifications") ? db.prepare("SELECT category FROM classifications WHERE scope = 'session' AND target_id = ?") : null;
@@ -4931,22 +4947,21 @@ function getFileTimeline(db, filePath, projectId2) {
4931
4947
  };
4932
4948
  }
4933
4949
  function getSkill(db, name) {
4934
- const versions = db.prepare(`SELECT sk.id, sk.base_dir, sk.summary, sk.body, sk.body_bytes, sk.first_seen, sk.last_seen,
4935
- (SELECT COUNT(*) FROM tool_calls tc WHERE tc.skill_id = sk.id) AS call_count
4936
- FROM skills sk WHERE sk.name = ? ORDER BY sk.last_seen DESC, sk.id`).all(name);
4937
- const sessions = db.prepare(`SELECT s.id, s.ai_title, s.slug, s.source_id, s.started_at, p.path AS project_path,
4938
- tc.skill_id AS version_id, MIN(e.timestamp) AS fired_at, COUNT(*) AS fire_count
4939
- FROM tool_calls tc
4940
- JOIN sessions s ON s.id = tc.session_id
4941
- LEFT JOIN projects p ON p.id = s.project_id
4942
- LEFT JOIN events e ON e.uuid = tc.event_uuid
4943
- WHERE tc.tool_name = 'Skill' AND tc.skill_name = ?
4944
- GROUP BY s.id, tc.skill_id
4945
- ORDER BY fired_at DESC`).all(name);
4946
- 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)
4947
4963
  return null;
4948
- for (const s of sessions)
4949
- s.title = s.ai_title || s.slug || null;
4964
+ const sessions = sessionRows.map(({ ai_title, slug, ...s }) => ({ ...s, title: ai_title || slug || null }));
4950
4965
  const call_count = sessions.reduce((a, s) => a + s.fire_count, 0);
4951
4966
  return { name, versions, sessions, call_count };
4952
4967
  }
@@ -4998,7 +5013,7 @@ function listFindings(db, f) {
4998
5013
  const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
4999
5014
  const dismissJoin = attached ? "LEFT JOIN triage.dismissed_findings d ON d.finding_id = f.id" : "";
5000
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`;
5001
- 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;
5002
5017
  const ORDER = {
5003
5018
  severity: SEVERITY_RANK_SQL,
5004
5019
  session: "f.session_id",
@@ -5006,18 +5021,18 @@ function listFindings(db, f) {
5006
5021
  category: "f.category",
5007
5022
  time: "s.started_at"
5008
5023
  };
5009
- 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,
5010
- f.framework_ref, f.severity, f.title, f.evidence, tc.tool_name,
5011
- COALESCE(s.ai_title, s.slug) AS session_title, s.source_id, s.is_sidechain,
5012
- s.started_at, p.path AS project_path, s.project_id ${triageCols}
5013
- FROM findings f
5014
- JOIN sessions s ON s.id = f.session_id
5015
- LEFT JOIN projects p ON p.id = s.project_id
5016
- LEFT JOIN tool_calls tc ON tc.id = f.tool_call_id
5017
- ${dismissJoin}
5018
- ${whereSql}
5019
- ORDER BY ${orderBy(ORDER, f.sort, "severity", f.dir)}, ${SEVERITY_RANK_SQL} DESC, f.id ASC
5020
- 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);
5021
5036
  return { total, findings };
5022
5037
  }
5023
5038
  function securitySummary(db) {
@@ -5029,15 +5044,15 @@ function securitySummary(db) {
5029
5044
  const dismissJoin = attached ? "LEFT JOIN triage.dismissed_findings d ON d.finding_id = f.id" : "";
5030
5045
  const base = `FROM findings f JOIN sessions s ON s.id = f.session_id ${dismissJoin}`;
5031
5046
  const openWhere = attached ? `WHERE d.finding_id IS NULL AND NOT ${MUTED_SQL}` : "";
5032
- const scalar = (sql) => db.prepare(sql).get().n;
5047
+ const scalar = (sql) => queryGet(db, sql).n;
5033
5048
  const total = scalar(`SELECT COUNT(*) n ${base} ${openWhere}`);
5034
5049
  const sessions_flagged = scalar(`SELECT COUNT(DISTINCT f.session_id) n ${base} ${openWhere}`);
5035
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;
5036
5051
  const muted = attached ? scalar(`SELECT COUNT(*) n ${base} WHERE d.finding_id IS NULL AND ${MUTED_SQL}`) : 0;
5037
- 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();
5038
- const by_category = db.prepare(`SELECT f.category AS category, COUNT(*) n ${base} ${openWhere} GROUP BY f.category ORDER BY n DESC`).all();
5039
- 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
5040
- ${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`);
5041
5056
  return { total, sessions_flagged, dismissed, muted, by_severity, by_category, by_rule, categories };
5042
5057
  }
5043
5058
 
@@ -5071,7 +5086,11 @@ function renderSessionExport(db, id, level) {
5071
5086
  }));
5072
5087
  const { markdown } = exportMarkdown({ id: s.id, title: s.title, source: s.source_id, project: s.project_path, model: null, started_at: s.started_at, ended_at: s.ended_at }, events, { level });
5073
5088
  const suffix = level === "off" ? "" : ".redacted";
5074
- return { markdown, filename: `session-${id.slice(0, 8)}${suffix}.md` };
5089
+ return { markdown, filename: `session-${filenameSafe(id.slice(0, 8))}${suffix}.md` };
5090
+ }
5091
+ function filenameSafe(s) {
5092
+ const cleaned = s.replace(/[^A-Za-z0-9._-]/g, "_");
5093
+ return cleaned === "" ? "session" : cleaned;
5075
5094
  }
5076
5095
 
5077
5096
  // ../server/dist/dashboard.js
@@ -5090,6 +5109,12 @@ function addUsage(acc, r) {
5090
5109
  acc.cache_creation += r.cw ?? 0;
5091
5110
  acc.cache_read += r.cr ?? 0;
5092
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
+ });
5093
5118
  function pct(sorted, p) {
5094
5119
  if (sorted.length === 0)
5095
5120
  return 0;
@@ -5109,11 +5134,11 @@ function workflowAgg(db, f) {
5109
5134
  if (!tableExists(db, "workflow_results"))
5110
5135
  return empty;
5111
5136
  const ww = workflowWhere(f);
5112
- const rows = db.prepare(`SELECT COALESCE(status, '(unknown)') status, COUNT(*) n,
5113
- SUM(COALESCE(total_tokens, 0)) tokens,
5114
- SUM(COALESCE(duration_ms, 0)) dur,
5115
- SUM(CASE WHEN duration_ms IS NOT NULL THEN 1 ELSE 0 END) dur_n
5116
- 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);
5117
5142
  let total = 0, completed = 0, failed = 0, tokens = 0, durSum = 0, durN = 0;
5118
5143
  const by_status = rows.map((r) => {
5119
5144
  total += r.n;
@@ -5139,32 +5164,32 @@ function workflowAgg(db, f) {
5139
5164
  }
5140
5165
  function dashboardOverview(db, f) {
5141
5166
  const w = sessionWhere(f);
5142
- const counts = db.prepare(`SELECT
5143
- COUNT(*) sessions,
5144
- SUM(CASE WHEN is_sidechain=0 THEN 1 ELSE 0 END) main,
5145
- SUM(CASE WHEN is_sidechain=1 THEN 1 ELSE 0 END) subagent,
5146
- SUM(turn_count) turns,
5147
- COUNT(DISTINCT project_id) projects
5148
- FROM sessions s ${w.sql}`).get(...w.params);
5149
- 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;
5150
- const usage = db.prepare(`SELECT t.model model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,
5151
- SUM(t.cache_creation_input_tokens) cw, SUM(t.cache_read_input_tokens) cr
5152
- FROM token_usage t JOIN sessions s ON s.id = t.session_id ${w.sql}
5153
- 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);
5154
5179
  const tokens = zeroSplit();
5155
5180
  let cost = 0;
5156
5181
  const unpriced = [];
5157
5182
  for (const u of usage) {
5158
5183
  addUsage(tokens, u);
5159
- 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));
5160
5185
  if (u.model && !rateForModel(u.model) && (u.i || u.o || u.cw || u.cr))
5161
5186
  unpriced.push(u.model);
5162
5187
  }
5163
5188
  const totalTokens = tokens.input + tokens.output + tokens.cache_creation + tokens.cache_read;
5164
- const durs = db.prepare(`SELECT tn.duration_ms d FROM turns tn JOIN sessions s ON s.id = tn.session_id
5165
- ${w.sql ? w.sql + " AND" : "WHERE"} tn.duration_ms IS NOT NULL ORDER BY tn.duration_ms`).all(...w.params).map((r) => r.d);
5166
- const sessDurs = db.prepare(`SELECT s.duration_ms d FROM sessions s
5167
- ${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);
5168
5193
  return {
5169
5194
  range: { from: f.from ?? null, to: f.to ?? null, source: f.source ?? null },
5170
5195
  sessions: counts.sessions ?? 0,
@@ -5186,7 +5211,7 @@ function dashboardOverview(db, f) {
5186
5211
  }
5187
5212
  function chooseBucket(db, f) {
5188
5213
  const w = sessionWhere(f);
5189
- 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);
5190
5215
  if (!r?.mn || !r?.mx)
5191
5216
  return "day";
5192
5217
  const spanDays = (Date.parse(r.mx) - Date.parse(r.mn)) / 864e5;
@@ -5214,26 +5239,26 @@ function dashboardTimeseries(db, f, bucketParam) {
5214
5239
  }
5215
5240
  return r;
5216
5241
  };
5217
- for (const u of db.prepare(`SELECT ${expr} b, t.model model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,
5218
- SUM(t.cache_creation_input_tokens) cw, SUM(t.cache_read_input_tokens) cr
5219
- FROM token_usage t JOIN sessions s ON s.id = t.session_id
5220
- ${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)) {
5221
5246
  if (!u.b)
5222
5247
  continue;
5223
5248
  const r = get(u.b);
5224
5249
  addUsage(r, u);
5225
- 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));
5226
5251
  }
5227
- 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)) {
5228
5253
  if (s.b)
5229
5254
  get(s.b).sessions = s.n;
5230
5255
  }
5231
- 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)) {
5232
5257
  if (t.b)
5233
5258
  get(t.b).turns = t.n;
5234
5259
  }
5235
- 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
5236
- ${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)) {
5237
5262
  if (!e.b)
5238
5263
  continue;
5239
5264
  const r = get(e.b);
@@ -5249,53 +5274,60 @@ function dashboardTimeseries(db, f, bucketParam) {
5249
5274
  }
5250
5275
  var andWhere = (w) => w.sql ? w.sql + " AND" : "WHERE";
5251
5276
  function modelBreakdown(db, w) {
5252
- const modelRows = db.prepare(`SELECT t.model model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,
5253
- SUM(t.cache_creation_input_tokens) cw, SUM(t.cache_read_input_tokens) cr,
5254
- COUNT(DISTINCT t.session_id) sessions
5255
- FROM token_usage t JOIN sessions s ON s.id = t.session_id ${w.sql}
5256
- 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
+ );
5257
5289
  return modelRows.map((u) => ({
5258
5290
  model: u.model,
5259
5291
  tokens: { input: u.i ?? 0, output: u.o ?? 0, cache_creation: u.cw ?? 0, cache_read: u.cr ?? 0 },
5260
5292
  total_tokens: (u.i ?? 0) + (u.o ?? 0) + (u.cw ?? 0) + (u.cr ?? 0),
5261
- 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)),
5262
5294
  sessions: u.sessions ?? 0,
5263
5295
  priced: !!rateForModel(u.model)
5264
5296
  }));
5265
5297
  }
5266
5298
  function sourceBreakdown(db, w) {
5267
- return db.prepare(`SELECT COALESCE(s.source_id, '(none)') source, COUNT(*) sessions, SUM(s.turn_count) turns
5268
- 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);
5269
5301
  }
5270
5302
  function classificationBreakdowns(db, w) {
5271
5303
  const mainW = { sql: w.sql ? w.sql + " AND s.is_sidechain = 0" : "WHERE s.is_sidechain = 0", params: w.params };
5272
- const byCategory = db.prepare(`SELECT c.category, COUNT(*) n FROM classifications c JOIN sessions s ON s.id = c.target_id
5273
- ${mainW.sql} GROUP BY c.category ORDER BY n DESC`).all(...mainW.params);
5274
- const byComplexity = db.prepare(`SELECT c.complexity_band band, COUNT(*) n FROM classifications c JOIN sessions s ON s.id = c.target_id
5275
- ${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);
5276
5308
  return { byCategory, byComplexity };
5277
5309
  }
5278
5310
  function toolBreakdown(db, w) {
5279
- return db.prepare(`SELECT tc.tool_name name, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
5280
- ${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);
5281
5313
  }
5282
5314
  function skillBreakdowns(db, w) {
5283
- const skills = db.prepare(`SELECT tc.skill_name name, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
5284
- ${andWhere(w)} tc.skill_name IS NOT NULL GROUP BY tc.skill_name ORDER BY n DESC LIMIT 20`).all(...w.params);
5285
- const skillVersions = db.prepare(`SELECT tc.skill_name name, sk.id version_id, sk.summary, sk.last_seen, COUNT(*) n
5286
- FROM tool_calls tc
5287
- JOIN skills sk ON sk.id = tc.skill_id
5288
- JOIN sessions s ON s.id = tc.session_id
5289
- ${andWhere(w)} tc.skill_id IS NOT NULL
5290
- 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);
5291
5323
  return { skills, skillVersions };
5292
5324
  }
5293
5325
  function subagentFanoutBreakdown(db, w) {
5294
- const subagentByType = db.prepare(`SELECT tc.agent_type type, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
5295
- ${andWhere(w)} tc.agent_type IS NOT NULL GROUP BY tc.agent_type ORDER BY n DESC`).all(...w.params);
5296
- const perSession = db.prepare(`SELECT COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
5297
- ${andWhere(w)} s.is_sidechain = 0 AND tc.tool_name IN ('Agent','Task')
5298
- 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);
5299
5331
  const counts = perSession.map((r) => r.n);
5300
5332
  return {
5301
5333
  by_type: subagentByType,
@@ -5306,8 +5338,8 @@ function subagentFanoutBreakdown(db, w) {
5306
5338
  };
5307
5339
  }
5308
5340
  function errorTypeBreakdown(db, w) {
5309
- 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
5310
- ${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);
5311
5343
  let failures = 0;
5312
5344
  let rejections = 0;
5313
5345
  const byType = errorRows.map((r) => {
@@ -5568,8 +5600,8 @@ async function createApp(db, opts = {}) {
5568
5600
  errorType: q.error_type ? q.error_type.split(",").filter(Boolean) : void 0,
5569
5601
  sort: ["started", "title", "turns", "tokens", "cost", "duration", "errors", "security"].find((s) => s === q.sort),
5570
5602
  dir: q.dir === "asc" ? "asc" : q.dir === "desc" ? "desc" : void 0,
5571
- limit: Math.min(Number(q.limit) || 50, 200),
5572
- offset: Number(q.offset) || 0
5603
+ limit: pageLimit(q.limit, 50, 200),
5604
+ offset: pageOffset(q.offset)
5573
5605
  });
5574
5606
  });
5575
5607
  app.get("/api/sessions/:id", async (req, reply) => {
@@ -5604,8 +5636,8 @@ async function createApp(db, opts = {}) {
5604
5636
  ...findingFilters(q),
5605
5637
  sort: ["severity", "session", "rule", "category", "time"].find((s) => s === q.sort),
5606
5638
  dir: q.dir === "asc" ? "asc" : q.dir === "desc" ? "desc" : void 0,
5607
- limit: Math.min(Number(q.limit) || 50, 5e3),
5608
- offset: Number(q.offset) || 0
5639
+ limit: pageLimit(q.limit, 50, 5e3),
5640
+ offset: pageOffset(q.offset)
5609
5641
  });
5610
5642
  });
5611
5643
  app.get("/api/security/mutes", async () => triageDb ? listMutes(triageDb) : []);
@@ -5678,8 +5710,8 @@ async function createApp(db, opts = {}) {
5678
5710
  project: q.project,
5679
5711
  sort,
5680
5712
  dir: q.dir === "asc" ? "asc" : "desc",
5681
- limit: Math.min(Number(q.limit) || 50, 200),
5682
- offset: Number(q.offset) || 0
5713
+ limit: pageLimit(q.limit, 50, 200),
5714
+ offset: pageOffset(q.offset)
5683
5715
  });
5684
5716
  });
5685
5717
  app.get("/api/file", async (req, reply) => {