@swestash/agent-lens 0.5.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Agent Lens
2
2
 
3
- > Passively collect, browse, and analyze your Claude Code CLI session traces — **100% local**.
3
+ > Passively collect, search, and audit your Claude Code CLI session traces — **100% local**.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@swestash/agent-lens)](https://www.npmjs.com/package/@swestash/agent-lens)
6
6
  ![Node](https://img.shields.io/badge/node-%E2%89%A524-brightgreen)
@@ -9,8 +9,9 @@
9
9
 
10
10
  Claude Code records rich per-session telemetry under `~/.claude/`, but prunes it on a rolling
11
11
  **30-day window**. Agent Lens continuously copies that data out before it's lost, normalizes it into
12
- a queryable SQLite store, and gives you a browsable transcript viewer plus analytics dashboards —
13
- **without a single byte leaving your machine**.
12
+ a queryable SQLite store, and gives you three things over it — a searchable transcript browser,
13
+ usage/cost dashboards, and a deterministic, no-AI **audit of what the agent actually did on your
14
+ machine** — **without a single byte leaving your machine**.
14
15
 
15
16
  ## Install
16
17
 
@@ -591,7 +591,7 @@ var CAC = class extends EventEmitter {
591
591
  var cac = (name = "") => new CAC(name);
592
592
 
593
593
  // ../core/dist/schema.js
594
- var SCHEMA_VERSION = 12;
594
+ var SCHEMA_VERSION = 13;
595
595
  var SCHEMA_SQL = (
596
596
  /* sql */
597
597
  `
@@ -712,7 +712,8 @@ CREATE TABLE IF NOT EXISTS tool_calls (
712
712
  workflow_run_id TEXT, -- for Workflow: the run id (toolUseResult.runId, e.g. wf_<id>) \u2014 ties the run to its subagents
713
713
  workflow_name TEXT, -- for Workflow: the workflow name (toolUseResult.workflowName)
714
714
  resolved_model TEXT,
715
- status TEXT, -- success | error | ...
715
+ status TEXT, -- success | error | ... ('error' when the tool_result had is_error)
716
+ error_type TEXT, -- heuristic bucket for status='error' rows (see errors.ts); NULL otherwise
716
717
  total_duration_ms INTEGER,
717
718
  total_tokens INTEGER,
718
719
  total_tool_use_count INTEGER,
@@ -725,6 +726,7 @@ CREATE INDEX IF NOT EXISTS idx_tool_calls_skill ON tool_calls(skill_name);
725
726
  CREATE INDEX IF NOT EXISTS idx_tool_calls_skill_id ON tool_calls(skill_id);
726
727
  CREATE INDEX IF NOT EXISTS idx_tool_calls_spawned ON tool_calls(spawned_session_id);
727
728
  CREATE INDEX IF NOT EXISTS idx_tool_calls_workflow_run ON tool_calls(workflow_run_id);
729
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_error_type ON tool_calls(error_type);
728
730
 
729
731
  -- Skill *versions* (content-addressed). A skill firing injects the full SKILL.md body into the
730
732
  -- transcript as an isMeta user event (begins "Base directory for this skill: \u2026", then the body,
@@ -945,6 +947,35 @@ var SECURITY_CATEGORIES = [
945
947
  }
946
948
  ];
947
949
 
950
+ // ../core/dist/errors.js
951
+ var ERROR_CLASSIFIER_VERSION = 1;
952
+ var REJECTION_TYPES = /* @__PURE__ */ new Set(["user-rejected", "guardrail-blocked"]);
953
+ function errorKind(type) {
954
+ return REJECTION_TYPES.has(type) ? "rejection" : "failure";
955
+ }
956
+ var PATTERNS = [
957
+ // — Rejections / blocks: is_error, but the agent's tool didn't fail — a human or guardrail stopped it.
958
+ { re: /interrupted by user|tool use was rejected|does(?:n['’]| not)t want to proceed|\bCancelled:/i, type: "user-rejected", kind: "rejection" },
959
+ { re: /\bBlocked:/i, type: "guardrail-blocked", kind: "rejection" },
960
+ // — Real tool failures.
961
+ { re: /String to replace not found/i, type: "string-not-found", kind: "failure" },
962
+ { re: /exceeds maximum allowed tokens/i, type: "token-limit", kind: "failure" },
963
+ // A non-zero Bash exit code is a command failure regardless of what its stderr text mentions — check
964
+ // it before the file-state fs-error patterns, since ENOENT/EISDIR also appear inside command stderr
965
+ // (e.g. `Exit code 2 npm error ENOENT`). The file-state patterns then catch the Read/Write/Edit
966
+ // tool_use_errors, which carry no exit code.
967
+ { re: /\bExit code\b/i, type: "command-failed", kind: "failure" },
968
+ { re: /has not been read yet|modified since (?:you |it was )?read|File (?:content )?does not exist|\bEISDIR\b|\bENOENT\b/i, type: "file-state", kind: "failure" }
969
+ ];
970
+ function classifyToolError(resultSummary) {
971
+ const s = resultSummary ?? "";
972
+ for (const p of PATTERNS) {
973
+ if (p.re.test(s))
974
+ return { type: p.type, kind: p.kind };
975
+ }
976
+ return { type: "other", kind: "failure" };
977
+ }
978
+
948
979
  // ../core/dist/pricing.js
949
980
  var PRICE_TABLE = {
950
981
  // Opus 4.0 / 4.1 launched at $15/$75; Opus 4.5 cut the Opus tier to $5/$25 and 4.6/4.7/4.8 kept it.
@@ -2041,8 +2072,13 @@ var ClaudeCodeAdapter = class {
2041
2072
  toolResults.push({
2042
2073
  tool_use_id: b.tool_use_id,
2043
2074
  result_summary: isAskUser ? JSON.stringify({ answers: tur.answers, annotations: tur.annotations ?? {} }) : summarizeResult(b.content),
2075
+ // Tool failure is signalled by `is_error` on the tool_result block itself — NOT by
2076
+ // toolUseResult.status (whose values are task/lifecycle states like completed/pending/
2077
+ // async_launched and never "error"). Map is_error → status="error" so the error roll-ups,
2078
+ // the transcript `tool-err` styling, and detect.ts's failed-attempt de-escalation all fire.
2079
+ // Non-error results fall back to the toolUseResult lifecycle status when present.
2080
+ status: b.is_error === true ? "error" : tur ? asString(tur.status) : null,
2044
2081
  ...tur ? {
2045
- status: asString(tur.status),
2046
2082
  agent_type: asString(tur.agentType),
2047
2083
  // Deterministic link: a Task/Agent subagent's transcript is keyed off its agentId
2048
2084
  // ('agent-'||agentId is the session id assigned by discover()'s filename stem).
@@ -2319,7 +2355,7 @@ function classify(db, dirty) {
2319
2355
 
2320
2356
  // ../ingest/dist/detect.js
2321
2357
  import { createHash } from "crypto";
2322
- var DETECTOR_VERSION = 6;
2358
+ var DETECTOR_VERSION = 7;
2323
2359
  function evidence(s, max = 200) {
2324
2360
  const one = s.replace(/\s+/g, " ").trim();
2325
2361
  return one.length > max ? one.slice(0, max - 1) + "\u2026" : one;
@@ -2341,6 +2377,8 @@ var SECRET_VALUE_PATTERNS = [
2341
2377
  { re: /\bsk-[A-Za-z0-9]{20,}\b/, label: "api-secret-key" }
2342
2378
  ];
2343
2379
  var isBash = (t) => t === "Bash";
2380
+ var SQL_DESTRUCTIVE = /\b(DROP\s+(TABLE|DATABASE|SCHEMA)|TRUNCATE(\s+TABLE)?)\b/i;
2381
+ var SQL_CLIENT = /\b(psql|pgcli|mysql|mysqladmin|mariadb|sqlite3?|sqlcmd|usql|mongo|mongosh|clickhouse-client|cockroach|snowsql|bq)\b/i;
2344
2382
  var stripComments = (c) => c.replace(/(^|\s)#[^\n]*/g, "$1");
2345
2383
  var neutralizeEcho = (c) => c.replace(/((?:^|[;&|\n(])\s*)(?:echo|printf)\b(?:"(?:[^"\\]|\\.)*"|'[^']*'|[^;&|\n])*/gi, "$1echo");
2346
2384
  var blankHeredoc = (c) => c.replace(/<<-?\s*(['"]?)(\w+)\1[\s\S]*?\n\s*\2\b/g, "<<HEREDOC");
@@ -2418,14 +2456,22 @@ var RULES = [
2418
2456
  tools: ["Bash"],
2419
2457
  baseSeverity: "high",
2420
2458
  test(ctx) {
2421
- const c = ctx.commandBare;
2422
- if (!c)
2459
+ if (!ctx.commandBare)
2423
2460
  return null;
2424
- const rf = /\brm\s+[^|;&\n]*(-[a-z]*r[a-z]*f|-[a-z]*f[a-z]*r|--recursive[^|;&\n]*--force|--force[^|;&\n]*--recursive)/i;
2425
- if (!rf.test(c))
2461
+ const c = ctx.commandBare.replace(/\bgit\s+rm\b/gi, "gitrm");
2462
+ const rmSeg = c.match(/\brm\b[^|;&\n]*/i)?.[0];
2463
+ if (!rmSeg)
2464
+ return null;
2465
+ const combined = /(?:^|\s)-(?=[a-zA-Z]*[rR])(?=[a-zA-Z]*f)[a-zA-Z]+(?=\s|$)/.test(rmSeg);
2466
+ const recursive = /(?:^|\s)(?:-[a-zA-Z]*[rR][a-zA-Z]*|--recursive)(?=\s|$)/.test(rmSeg);
2467
+ const force = /(?:^|\s)(?:-[a-zA-Z]*f[a-zA-Z]*|--force)(?=\s|$)/.test(rmSeg);
2468
+ if (!combined && !(recursive && force))
2426
2469
  return null;
2427
2470
  const dangerousTarget = HOME_OR_ROOT_TARGET.test(c) || /\s\/(?:\s|$)/.test(c) || /\brm\b[^|;&\n]*\*/.test(c);
2428
- return { evidence: evidence(ctx.command), severity: dangerousTarget ? "critical" : "high", mods: { dangerous_target: dangerousTarget } };
2471
+ const targets = rmSeg.split(/\s+/).slice(1).filter((a) => a && !a.startsWith("-"));
2472
+ const tempOnly = targets.length > 0 && targets.every((t) => TEMP_PATH.test(t));
2473
+ const severity = dangerousTarget ? "critical" : tempOnly ? "low" : "high";
2474
+ return { evidence: evidence(ctx.command), severity, mods: { dangerous_target: dangerousTarget, temp_target: tempOnly } };
2429
2475
  }
2430
2476
  },
2431
2477
  {
@@ -2468,10 +2514,12 @@ var RULES = [
2468
2514
  baseSeverity: "high",
2469
2515
  test(ctx) {
2470
2516
  const query = typeof ctx.input?.query === "string" ? ctx.input.query : null;
2471
- const hay = ctx.commandCode ?? query;
2472
- if (!hay || !/\b(DROP\s+(TABLE|DATABASE|SCHEMA)|TRUNCATE(\s+TABLE)?)\b/i.test(hay))
2473
- return null;
2474
- return { evidence: evidence(ctx.command ?? query) };
2517
+ if (query && SQL_DESTRUCTIVE.test(query))
2518
+ return { evidence: evidence(ctx.command ?? query) };
2519
+ const code = ctx.commandCode;
2520
+ if (code && SQL_CLIENT.test(code) && SQL_DESTRUCTIVE.test(code))
2521
+ return { evidence: evidence(ctx.command) };
2522
+ return null;
2475
2523
  }
2476
2524
  },
2477
2525
  {
@@ -2820,6 +2868,33 @@ function detect(db, dirty) {
2820
2868
  return { count, version: DETECTOR_VERSION };
2821
2869
  }
2822
2870
 
2871
+ // ../ingest/dist/errors.js
2872
+ function classifyErrors(db, dirty) {
2873
+ const incremental = dirty != null;
2874
+ const scope = incremental ? " AND session_id IN (SELECT id FROM _dirty_err)" : "";
2875
+ if (incremental) {
2876
+ db.exec("DROP TABLE IF EXISTS _dirty_err");
2877
+ db.exec("CREATE TEMP TABLE _dirty_err (id TEXT PRIMARY KEY)");
2878
+ const ins = db.prepare("INSERT OR IGNORE INTO _dirty_err (id) VALUES (?)");
2879
+ db.transaction((ids) => {
2880
+ for (const id of ids)
2881
+ ins.run(id);
2882
+ })(dirty);
2883
+ }
2884
+ const rows = db.prepare(`SELECT id, result_summary FROM tool_calls WHERE status = 'error'${scope}`).all();
2885
+ const update = db.prepare("UPDATE tool_calls SET error_type = ? WHERE id = ?");
2886
+ const tx = db.transaction(() => {
2887
+ db.exec(`UPDATE tool_calls SET error_type = NULL WHERE error_type IS NOT NULL${scope}`);
2888
+ for (const r of rows)
2889
+ update.run(classifyToolError(r.result_summary).type, r.id);
2890
+ });
2891
+ tx();
2892
+ if (incremental)
2893
+ db.exec("DROP TABLE IF EXISTS _dirty_err");
2894
+ const count = db.prepare("SELECT COUNT(*) n FROM tool_calls WHERE error_type IS NOT NULL").get().n;
2895
+ return { count, version: ERROR_CLASSIFIER_VERSION };
2896
+ }
2897
+
2823
2898
  // ../ingest/dist/pipeline.js
2824
2899
  import { createHash as createHash2 } from "crypto";
2825
2900
  function newStats() {
@@ -3755,6 +3830,7 @@ function runIngest(argv = process.argv.slice(2)) {
3755
3830
  const expanded = rebuildDerived(db, args.full ? null : dirty);
3756
3831
  const classified = classify(db, args.full ? null : expanded);
3757
3832
  const detected = detect(db, args.full ? null : expanded);
3833
+ classifyErrors(db, args.full ? null : expanded);
3758
3834
  const count = (sql) => db.prepare(sql).get().n;
3759
3835
  const sessions = count("SELECT COUNT(*) n FROM sessions");
3760
3836
  const turns = count("SELECT COUNT(*) n FROM turns");
@@ -3796,8 +3872,9 @@ function runMetrics(argv = process.argv.slice(2)) {
3796
3872
  const db = openDb(dbPath);
3797
3873
  const r = classify(db);
3798
3874
  const d = detect(db);
3875
+ const e = classifyErrors(db);
3799
3876
  db.close();
3800
- console.log(`agent-lens-metrics: classified=${r.count} classifier_version=${CLASSIFIER_VERSION} findings=${d.count} detector_version=${DETECTOR_VERSION} db=${dbPath}`);
3877
+ console.log(`agent-lens-metrics: classified=${r.count} classifier_version=${CLASSIFIER_VERSION} findings=${d.count} detector_version=${DETECTOR_VERSION} errors_classified=${e.count} error_classifier_version=${e.version} db=${dbPath}`);
3801
3878
  }
3802
3879
 
3803
3880
  // ../server/dist/run.js
@@ -3877,6 +3954,14 @@ function listSessions(db, f) {
3877
3954
  where.push("s.is_sidechain = 0");
3878
3955
  if (f.kind === "subagent")
3879
3956
  where.push("s.is_sidechain = 1");
3957
+ if (f.severity?.length) {
3958
+ where.push(`EXISTS (SELECT 1 FROM findings fd WHERE fd.session_id = s.id AND fd.severity IN (${f.severity.map(() => "?").join(",")}))`);
3959
+ params.push(...f.severity);
3960
+ }
3961
+ if (f.errorType?.length) {
3962
+ where.push(`EXISTS (SELECT 1 FROM tool_calls tc WHERE tc.session_id = s.id AND tc.error_type IN (${f.errorType.map(() => "?").join(",")}))`);
3963
+ params.push(...f.errorType);
3964
+ }
3880
3965
  if (f.model) {
3881
3966
  where.push("EXISTS (SELECT 1 FROM token_usage t WHERE t.session_id = s.id AND t.model = ?)");
3882
3967
  params.push(f.model);
@@ -3892,7 +3977,13 @@ function listSessions(db, f) {
3892
3977
  const total = db.prepare(`SELECT COUNT(*) n FROM sessions s ${whereSql}`).get(...params).n;
3893
3978
  const baseSelect = `SELECT s.id, s.ai_title, s.slug, s.source_id, s.is_sidechain, s.started_at, s.ended_at,
3894
3979
  s.duration_ms, s.event_count, s.turn_count, p.path AS project_path,
3895
- (SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models
3980
+ (SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models,
3981
+ (SELECT COUNT(*) FROM tool_calls tc WHERE tc.session_id = s.id) AS tool_call_count,
3982
+ (SELECT COUNT(*) FROM tool_calls tc WHERE tc.session_id = s.id AND tc.status = 'error') AS tool_error_count,
3983
+ (SELECT COUNT(*) FROM findings fd WHERE fd.session_id = s.id) AS finding_count,
3984
+ (SELECT fd.severity FROM findings fd WHERE fd.session_id = s.id
3985
+ ORDER BY CASE fd.severity WHEN 'critical' THEN 4 WHEN 'high' THEN 3 WHEN 'medium' THEN 2 WHEN 'low' THEN 1 WHEN 'info' THEN 0 ELSE -1 END DESC
3986
+ LIMIT 1) AS worst_severity
3896
3987
  FROM sessions s LEFT JOIN projects p ON p.id = s.project_id
3897
3988
  ${whereSql}`;
3898
3989
  const NATIVE_ORDER = {
@@ -3903,13 +3994,29 @@ function listSessions(db, f) {
3903
3994
  };
3904
3995
  const sortKey = f.sort ?? "started";
3905
3996
  const sqlDir = f.dir === "asc" ? "ASC" : "DESC";
3997
+ const JS_SORT = /* @__PURE__ */ new Set(["tokens", "cost", "errors", "security"]);
3906
3998
  let rows;
3907
- if (sortKey === "tokens" || sortKey === "cost") {
3999
+ if (JS_SORT.has(sortKey)) {
3908
4000
  const all = db.prepare(baseSelect).all(...params);
3909
4001
  attachSessionCost(db, all);
3910
4002
  const sign = f.dir === "asc" ? 1 : -1;
4003
+ const metric = (r) => {
4004
+ switch (sortKey) {
4005
+ case "tokens":
4006
+ return r.tokens;
4007
+ case "cost":
4008
+ return r.cost;
4009
+ case "errors":
4010
+ return r.tool_error_count ?? 0;
4011
+ // Security: rank by worst severity (info=0 … critical=4; none = -1), finding count as tiebreak.
4012
+ case "security":
4013
+ return (severityRank(r.worst_severity ?? "") + 1) * 1e6 + (r.finding_count ?? 0);
4014
+ default:
4015
+ return 0;
4016
+ }
4017
+ };
3911
4018
  all.sort((a, b) => {
3912
- const c = (sortKey === "tokens" ? a.tokens - b.tokens : a.cost - b.cost) * sign;
4019
+ const c = (metric(a) - metric(b)) * sign;
3913
4020
  return c !== 0 ? c : String(a.id).localeCompare(String(b.id));
3914
4021
  });
3915
4022
  rows = all.slice(f.offset, f.offset + f.limit);
@@ -3964,7 +4071,7 @@ function getSession(db, id) {
3964
4071
  const eventRows = db.prepare(`SELECT uuid, type, role, timestamp, model, is_sidechain, turn_id, raw_json
3965
4072
  FROM events WHERE session_id = ? ORDER BY timestamp, seq`).all(id);
3966
4073
  const toolRows = db.prepare(`SELECT id, event_uuid, tool_name, skill_name, skill_id, agent_type, spawned_session_id,
3967
- workflow_run_id, workflow_name, status,
4074
+ workflow_run_id, workflow_name, status, error_type,
3968
4075
  total_duration_ms, total_tokens, input_json, result_summary,
3969
4076
  (SELECT COUNT(*) FROM sessions s WHERE s.workflow_run_id = tool_calls.workflow_run_id) AS workflow_agent_count
3970
4077
  FROM tool_calls WHERE session_id = ?`).all(id);
@@ -4094,6 +4201,10 @@ function getSession(db, id) {
4094
4201
  FROM tool_calls tc LEFT JOIN turns t ON t.id = tc.turn_id
4095
4202
  WHERE tc.session_id = ? AND tc.workflow_run_id IS NOT NULL
4096
4203
  ORDER BY t.seq`).all(id);
4204
+ session.tool_call_count = toolRows.length;
4205
+ session.tool_error_count = toolRows.filter((t) => t.status === "error").length;
4206
+ session.tool_rejection_count = toolRows.filter((t) => t.status === "error" && t.error_type && errorKind(t.error_type) === "rejection").length;
4207
+ session.tool_failure_count = session.tool_error_count - session.tool_rejection_count;
4097
4208
  return { session, turns, events, classification, parent, children, workflow_runs, findings: sessionFindings };
4098
4209
  }
4099
4210
  function xmlTag(text, tag) {
@@ -4444,7 +4555,7 @@ function dashboardTimeseries(db, f, bucketParam) {
4444
4555
  const get = (b) => {
4445
4556
  let r = byBucket.get(b);
4446
4557
  if (!r) {
4447
- r = { bucket: b, ...zeroSplit(), cost: 0, sessions: 0, turns: 0 };
4558
+ r = { bucket: b, ...zeroSplit(), cost: 0, sessions: 0, turns: 0, failures: 0, rejections: 0 };
4448
4559
  byBucket.set(b, r);
4449
4560
  }
4450
4561
  return r;
@@ -4467,6 +4578,16 @@ function dashboardTimeseries(db, f, bucketParam) {
4467
4578
  if (t.b)
4468
4579
  get(t.b).turns = t.n;
4469
4580
  }
4581
+ 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
4582
+ ${w.sql ? w.sql + " AND" : "WHERE"} tc.status = 'error' GROUP BY b, tc.error_type`).all(...w.params)) {
4583
+ if (!e.b)
4584
+ continue;
4585
+ const r = get(e.b);
4586
+ if (e.et && errorKind(e.et) === "rejection")
4587
+ r.rejections += e.n;
4588
+ else
4589
+ r.failures += e.n;
4590
+ }
4470
4591
  const series = [...byBucket.values()].sort((a, b) => a.bucket.localeCompare(b.bucket));
4471
4592
  for (const r of series)
4472
4593
  r.cost = Number(r.cost.toFixed(4));
@@ -4517,7 +4638,20 @@ function dashboardBreakdowns(db, f) {
4517
4638
  max_per_session: counts.length ? Math.max(...counts) : 0,
4518
4639
  avg_per_session: counts.length ? Number((counts.reduce((a, b) => a + b, 0) / counts.length).toFixed(2)) : 0
4519
4640
  };
4520
- return { by_model: byModel, by_source: bySource, by_category: byCategory, by_complexity: byComplexity, tools, skills, skill_versions: skillVersions, subagent_fanout: subagentFanout };
4641
+ 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
4642
+ ${w.sql ? w.sql + " AND" : "WHERE"} tc.status = 'error' GROUP BY tc.error_type ORDER BY n DESC`).all(...w.params);
4643
+ let failures = 0;
4644
+ let rejections = 0;
4645
+ const byType = errorRows.map((r) => {
4646
+ const kind = errorKind(r.type);
4647
+ if (kind === "rejection")
4648
+ rejections += r.n;
4649
+ else
4650
+ failures += r.n;
4651
+ return { type: r.type, kind, n: r.n };
4652
+ });
4653
+ const errorTypes = { by_type: byType, failures, rejections };
4654
+ return { by_model: byModel, by_source: bySource, by_category: byCategory, by_complexity: byComplexity, tools, skills, skill_versions: skillVersions, subagent_fanout: subagentFanout, error_types: errorTypes };
4521
4655
  }
4522
4656
 
4523
4657
  // ../server/dist/refresh.js
@@ -4679,7 +4813,9 @@ async function createApp(db, opts = {}) {
4679
4813
  from: q.from,
4680
4814
  to: q.to,
4681
4815
  kind: q.kind === "main" || q.kind === "subagent" ? q.kind : void 0,
4682
- sort: ["started", "title", "turns", "tokens", "cost", "duration"].find((s) => s === q.sort),
4816
+ severity: q.severity ? q.severity.split(",").filter(Boolean) : void 0,
4817
+ errorType: q.error_type ? q.error_type.split(",").filter(Boolean) : void 0,
4818
+ sort: ["started", "title", "turns", "tokens", "cost", "duration", "errors", "security"].find((s) => s === q.sort),
4683
4819
  dir: q.dir === "asc" ? "asc" : q.dir === "desc" ? "desc" : void 0,
4684
4820
  limit: Math.min(Number(q.limit) || 50, 200),
4685
4821
  offset: Number(q.offset) || 0