@swestash/agent-lens 0.9.1 → 0.9.3

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.
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { existsSync as existsSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "fs";
5
- import { dirname as dirname7, join as join13 } from "path";
4
+ import { existsSync as existsSync10, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
5
+ import { dirname as dirname7, join as join11 } from "path";
6
6
  import { fileURLToPath as fileURLToPath3 } from "url";
7
7
 
8
8
  // ../../node_modules/.pnpm/cac@6.7.14/node_modules/cac/dist/index.mjs
@@ -971,10 +971,13 @@ var SECURITY_CATEGORIES = [
971
971
  ];
972
972
 
973
973
  // ../core/dist/secrets.js
974
+ var EMAIL = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/;
975
+ var IPV4 = /\b\d{1,3}(?:\.\d{1,3}){3}\b/;
976
+ var HOME_PATH_WITH_USER = /\/(?:home|Users)\/(?!user\b)[A-Za-z0-9._-]+/;
974
977
  var LEAK_PATTERNS = [
975
- { name: "email", re: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/ },
976
- { name: "home-path-with-user", re: /\/(?:home|Users)\/(?!user\b)[A-Za-z0-9._-]+/ },
977
- { name: "ipv4", re: /\b\d{1,3}(?:\.\d{1,3}){3}\b/ },
978
+ { name: "email", re: EMAIL },
979
+ { name: "home-path-with-user", re: HOME_PATH_WITH_USER },
980
+ { name: "ipv4", re: IPV4 },
978
981
  { name: "url", re: /https?:\/\/[^\s"']+/ },
979
982
  { name: "aws-key", re: /AKIA[0-9A-Z]{16}/ },
980
983
  { name: "bearer/token", re: /\b(?:sk|pk|ghp|gho|xox[abp])[-_][A-Za-z0-9]{8,}/ }
@@ -1002,8 +1005,8 @@ function maskEnvAssignments(text) {
1002
1005
  return text.replace(ENV_ASSIGN, (_m, name, q) => `${name}=${q}[REDACTED]`);
1003
1006
  }
1004
1007
  var PII_PATTERNS = [
1005
- { re: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, label: "EMAIL" },
1006
- { re: /\b\d{1,3}(?:\.\d{1,3}){3}\b/g, label: "IP" }
1008
+ { re: new RegExp(EMAIL.source, "g"), label: "EMAIL" },
1009
+ { re: new RegExp(IPV4.source, "g"), label: "IP" }
1007
1010
  ];
1008
1011
  var HOME_USER = /\/(home|Users)\/[^/\s"'`]+/g;
1009
1012
  var ENCODED_HOME_USER = /(?<![A-Za-z0-9])(-(?:home|Users)-)[A-Za-z0-9._]+(?=-)/g;
@@ -1022,8 +1025,8 @@ function maskSecrets(text) {
1022
1025
  }
1023
1026
  var SHARE_LEAK = [
1024
1027
  ...SECRET_PATTERNS.map(({ re, label }) => ({ name: label, re: new RegExp(re.source) })),
1025
- { name: "email", re: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/ },
1026
- { name: "home-path-with-user", re: /\/(?:home|Users)\/(?!user\b)[A-Za-z0-9._-]+/ },
1028
+ { name: "email", re: EMAIL },
1029
+ { name: "home-path-with-user", re: HOME_PATH_WITH_USER },
1027
1030
  { name: "encoded-home-with-user", re: /(?<![A-Za-z0-9])-(?:home|Users)-(?!user-)[A-Za-z0-9._]+-/ },
1028
1031
  { name: "url-credentials", re: /:\/\/[^/\s:@"'`]+:[^/\s@"'`]+@/ }
1029
1032
  ];
@@ -1076,7 +1079,7 @@ function scrubUsernames(text, users) {
1076
1079
  import { createHash } from "crypto";
1077
1080
 
1078
1081
  // ../core/dist/markdown.js
1079
- function fence(s) {
1082
+ function truncate(s) {
1080
1083
  return s.length > 2e3 ? s.slice(0, 2e3) + "\n\u2026 (truncated)" : s;
1081
1084
  }
1082
1085
  function sessionToMarkdown(s, events) {
@@ -1102,14 +1105,14 @@ function sessionToMarkdown(s, events) {
1102
1105
  if (e.thinking) {
1103
1106
  out.push(`### \u{1F9E0} thinking${ts}`);
1104
1107
  out.push("");
1105
- out.push(fence(e.thinking));
1108
+ out.push(truncate(e.thinking));
1106
1109
  out.push("");
1107
1110
  }
1108
1111
  if (e.text) {
1109
1112
  const icon = who === "user" ? "\u{1F464}" : who === "assistant" ? "\u{1F916}" : "\u2699\uFE0F";
1110
1113
  out.push(`### ${icon} ${who}${ts}`);
1111
1114
  out.push("");
1112
- out.push(fence(e.text));
1115
+ out.push(truncate(e.text));
1113
1116
  out.push("");
1114
1117
  }
1115
1118
  for (const tc of e.toolCalls) {
@@ -1118,7 +1121,7 @@ function sessionToMarkdown(s, events) {
1118
1121
  out.push(`> \u{1F527} **${label}**${status}`);
1119
1122
  if (tc.input_json && tc.input_json !== "{}") {
1120
1123
  out.push("```json");
1121
- out.push(fence(tc.input_json));
1124
+ out.push(truncate(tc.input_json));
1122
1125
  out.push("```");
1123
1126
  }
1124
1127
  out.push("");
@@ -1217,25 +1220,25 @@ function errorKind(type) {
1217
1220
  }
1218
1221
  var PATTERNS = [
1219
1222
  // — Rejections / blocks: is_error, but the agent's tool didn't fail — a human or guardrail stopped it.
1220
- { re: /interrupted by user|tool use was rejected|does(?:n['’]| not)t want to proceed|\bCancelled:/i, type: "user-rejected", kind: "rejection" },
1221
- { re: /\bBlocked:/i, type: "guardrail-blocked", kind: "rejection" },
1223
+ { re: /interrupted by user|tool use was rejected|does(?:n['’]| not)t want to proceed|\bCancelled:/i, type: "user-rejected" },
1224
+ { re: /\bBlocked:/i, type: "guardrail-blocked" },
1222
1225
  // — Real tool failures.
1223
- { re: /String to replace not found/i, type: "string-not-found", kind: "failure" },
1224
- { re: /exceeds maximum allowed tokens/i, type: "token-limit", kind: "failure" },
1226
+ { re: /String to replace not found/i, type: "string-not-found" },
1227
+ { re: /exceeds maximum allowed tokens/i, type: "token-limit" },
1225
1228
  // A non-zero Bash exit code is a command failure regardless of what its stderr text mentions — check
1226
1229
  // it before the file-state fs-error patterns, since ENOENT/EISDIR also appear inside command stderr
1227
1230
  // (e.g. `Exit code 2 npm error ENOENT`). The file-state patterns then catch the Read/Write/Edit
1228
1231
  // tool_use_errors, which carry no exit code.
1229
- { re: /\bExit code\b/i, type: "command-failed", kind: "failure" },
1230
- { 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" }
1232
+ { re: /\bExit code\b/i, type: "command-failed" },
1233
+ { 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" }
1231
1234
  ];
1232
1235
  function classifyToolError(resultSummary) {
1233
1236
  const s = resultSummary ?? "";
1234
1237
  for (const p of PATTERNS) {
1235
1238
  if (p.re.test(s))
1236
- return { type: p.type, kind: p.kind };
1239
+ return { type: p.type, kind: errorKind(p.type) };
1237
1240
  }
1238
- return { type: "other", kind: "failure" };
1241
+ return { type: "other", kind: errorKind("other") };
1239
1242
  }
1240
1243
 
1241
1244
  // ../core/dist/pricing.js
@@ -1694,7 +1697,7 @@ function collectAll(opts = {}) {
1694
1697
  const archiveBase = opts.archiveBase ?? resolveArchiveDir();
1695
1698
  const sources = opts.sources ?? loadSources();
1696
1699
  const excludedDirs = (opts.excludes ?? loadExcludes()).map(encodeProjectPath);
1697
- const runTs = opts.runTimestamp ?? collectTimestamp();
1700
+ const runTs = collectTimestamp();
1698
1701
  ensureDir(archiveBase);
1699
1702
  const stats = { sources: 0, scanned: 0, copied: 0, appended: 0, diverged: 0, compacted: 0, snapshots: 0 };
1700
1703
  if (!sources.length)
@@ -2034,10 +2037,10 @@ function windowsUninstall(targets) {
2034
2037
  }
2035
2038
  function windowsStatus(targets) {
2036
2039
  if (targets.includes("collector")) {
2037
- sh("schtasks", ["/Query", "/FO", "LIST", "/TN", WINDOWS_COLLECTOR_PREFIX], true) || console.log("agent-lens service: no collector tasks found under " + WINDOWS_COLLECTOR_PREFIX);
2040
+ sh("schtasks", ["/Query", "/FO", "LIST", "/TN", WINDOWS_COLLECTOR_PREFIX], false) || console.log("agent-lens service: no collector tasks found under " + WINDOWS_COLLECTOR_PREFIX);
2038
2041
  }
2039
2042
  if (targets.includes("server")) {
2040
- sh("schtasks", ["/Query", "/FO", "LIST", "/TN", WINDOWS_SERVER_TASK], true) || console.log("agent-lens service: no server task found (" + WINDOWS_SERVER_TASK + ")");
2043
+ sh("schtasks", ["/Query", "/FO", "LIST", "/TN", WINDOWS_SERVER_TASK], false) || console.log("agent-lens service: no server task found (" + WINDOWS_SERVER_TASK + ")");
2041
2044
  }
2042
2045
  }
2043
2046
  function runService(action, params) {
@@ -2077,8 +2080,8 @@ function runService(action, params) {
2077
2080
  }
2078
2081
 
2079
2082
  // ../ingest/dist/run.js
2080
- import { readFileSync as readFileSync7, statSync as statSync5, mkdirSync as mkdirSync4, existsSync as existsSync5 } from "fs";
2081
- import { dirname as dirname5, join as join10 } from "path";
2083
+ import { readFileSync as readFileSync5, statSync as statSync3, mkdirSync as mkdirSync4, existsSync as existsSync5 } from "fs";
2084
+ import { dirname as dirname5, join as join8 } from "path";
2082
2085
 
2083
2086
  // ../ingest/dist/db.js
2084
2087
  import Database from "better-sqlite3";
@@ -2391,6 +2394,35 @@ var ClaudeCodeAdapter = class {
2391
2394
  }
2392
2395
  };
2393
2396
 
2397
+ // ../ingest/dist/dirtyset.js
2398
+ function createDirtySet(db, name, ids) {
2399
+ db.exec(`DROP TABLE IF EXISTS ${name}`);
2400
+ db.exec(`CREATE TEMP TABLE ${name} (id TEXT PRIMARY KEY)`);
2401
+ const ins = db.prepare(`INSERT OR IGNORE INTO ${name} (id) VALUES (?)`);
2402
+ db.transaction((xs) => {
2403
+ for (const id of xs)
2404
+ ins.run(id);
2405
+ })(ids);
2406
+ }
2407
+
2408
+ // ../ingest/dist/storedjson.js
2409
+ var malformed = 0;
2410
+ var reported = /* @__PURE__ */ new Set();
2411
+ function parseStored(json, where) {
2412
+ if (!json)
2413
+ return null;
2414
+ try {
2415
+ return JSON.parse(json);
2416
+ } catch {
2417
+ malformed++;
2418
+ if (!reported.has(where)) {
2419
+ reported.add(where);
2420
+ console.warn(`agent-lens: malformed stored JSON in ${where} \u2014 the affected rows are skipped; re-ingest with --full to rebuild`);
2421
+ }
2422
+ return null;
2423
+ }
2424
+ }
2425
+
2394
2426
  // ../ingest/dist/classify.js
2395
2427
  var CLASSIFIER_VERSION = 2;
2396
2428
  var CATEGORIES = ["bugfix", "refactor", "docs", "ops", "review", "feature", "chore"];
@@ -2408,7 +2440,7 @@ function categoryForSubagentRole(role) {
2408
2440
  return REVIEW_SUBAGENT_ROLES.has(role) ? "review" : null;
2409
2441
  }
2410
2442
  function countLines(s) {
2411
- if (!s)
2443
+ if (typeof s !== "string" || s.length === 0)
2412
2444
  return 0;
2413
2445
  const trimmed = s.endsWith("\n") ? s.slice(0, -1) : s;
2414
2446
  return trimmed.length === 0 ? 0 : trimmed.split("\n").length;
@@ -2418,12 +2450,9 @@ var OPS_FILE = /(^|\/)(dockerfile|docker-compose|\.github\/|\.gitlab-ci|makefile
2418
2450
  function locDelta(toolName, inputJson) {
2419
2451
  if (!inputJson)
2420
2452
  return { added: 0, removed: 0, file: null };
2421
- let input;
2422
- try {
2423
- input = JSON.parse(inputJson);
2424
- } catch {
2453
+ const input = parseStored(inputJson, "classify: tool_calls.input_json");
2454
+ if (!input)
2425
2455
  return { added: 0, removed: 0, file: null };
2426
- }
2427
2456
  const file = typeof input?.file_path === "string" ? input.file_path : null;
2428
2457
  if (toolName === "Write")
2429
2458
  return { added: countLines(input?.content), removed: 0, file };
@@ -2449,7 +2478,7 @@ function hits(text, words) {
2449
2478
  }
2450
2479
  return n;
2451
2480
  }
2452
- function scoreCategories(s, text) {
2481
+ function scoreCategories(s, text, files) {
2453
2482
  const t = text.toLowerCase();
2454
2483
  const editN = s.toolCounts["Edit"] ?? 0;
2455
2484
  const writeN = s.toolCounts["Write"] ?? 0;
@@ -2470,8 +2499,8 @@ function scoreCategories(s, text) {
2470
2499
  scores.ops += hits(t, ["deploy", "docker", "pipeline", "ci/cd", "ci ", "systemd", "infra", "kubernetes", "terraform", "release", "rollback", "container"]) * 1;
2471
2500
  scores.review += hits(t, ["review", "audit", "inspect", "look over", "feedback on"]) * 1;
2472
2501
  scores.feature += hits(t, ["add ", "implement", "build ", "create ", "feature", "support for", "new feature"]) * 0.8;
2473
- const docFiles = s.loc.files > 0 ? countMatch(s, DOC_EXT) : 0;
2474
- const opsFiles = s.loc.files > 0 ? countMatch(s, OPS_FILE) : 0;
2502
+ const docFiles = s.loc.files > 0 ? countMatch(files, DOC_EXT) : 0;
2503
+ const opsFiles = s.loc.files > 0 ? countMatch(files, OPS_FILE) : 0;
2475
2504
  if (s.loc.files > 0) {
2476
2505
  scores.docs += 3 * (docFiles / s.loc.files);
2477
2506
  scores.ops += 3 * (opsFiles / s.loc.files);
@@ -2489,8 +2518,7 @@ function scoreCategories(s, text) {
2489
2518
  scores[writes > 0 ? "feature" : "chore"] += 0.01;
2490
2519
  return scores;
2491
2520
  }
2492
- function countMatch(s, re) {
2493
- const files = s._files ?? [];
2521
+ function countMatch(files, re) {
2494
2522
  let n = 0;
2495
2523
  for (const f of files)
2496
2524
  if (re.test(f))
@@ -2514,15 +2542,8 @@ function complexity(s) {
2514
2542
  }
2515
2543
  function classify(db, dirty) {
2516
2544
  const incremental = dirty != null;
2517
- if (incremental) {
2518
- db.exec("DROP TABLE IF EXISTS _dirty");
2519
- db.exec("CREATE TEMP TABLE _dirty (id TEXT PRIMARY KEY)");
2520
- const ins = db.prepare("INSERT OR IGNORE INTO _dirty (id) VALUES (?)");
2521
- db.transaction((ids) => {
2522
- for (const id of ids)
2523
- ins.run(id);
2524
- })(dirty);
2525
- }
2545
+ if (dirty != null)
2546
+ createDirtySet(db, "_dirty", dirty);
2526
2547
  const wSess = incremental ? " WHERE session_id IN (SELECT id FROM _dirty)" : "";
2527
2548
  const aSess = incremental ? " AND session_id IN (SELECT id FROM _dirty)" : "";
2528
2549
  const sessions = db.prepare(`SELECT id, is_sidechain, turn_count, event_count, duration_ms FROM sessions${incremental ? " WHERE id IN (SELECT id FROM _dirty)" : ""}`).all();
@@ -2587,10 +2608,9 @@ function classify(db, dirty) {
2587
2608
  cache_read_tokens: tok.cacheRead,
2588
2609
  duration_ms: sess.duration_ms ?? 0,
2589
2610
  subagent_count: subagentCount,
2590
- is_sidechain: sess.is_sidechain,
2591
- _files: filesArr
2611
+ is_sidechain: sess.is_sidechain
2592
2612
  };
2593
- const catScores = scoreCategories(signals, promptText.get(sess.id) ?? "");
2613
+ const catScores = scoreCategories(signals, promptText.get(sess.id) ?? "", filesArr);
2594
2614
  let category = "chore";
2595
2615
  let bestScore = -1;
2596
2616
  for (const c of CATEGORIES) {
@@ -2670,9 +2690,12 @@ var SQL_CLIENT = /\b(psql|pgcli|mysql|mysqladmin|mariadb|sqlite3?|sqlcmd|usql|mo
2670
2690
  var stripComments = (c) => c.replace(/(^|\s)#[^\n]*/g, "$1");
2671
2691
  var neutralizeEcho = (c) => c.replace(/((?:^|[;&|\n(])\s*)(?:echo|printf)\b(?:"(?:[^"\\]|\\.)*"|'[^']*'|[^;&|\n])*/gi, "$1echo");
2672
2692
  var blankHeredoc = (c) => c.replace(/<<-?\s*(['"]?)(\w+)\1[\s\S]*?\n\s*\2\b/g, "<<HEREDOC");
2673
- var codeOf = (c) => neutralizeEcho(stripComments(blankHeredoc(c)));
2674
2693
  var blankQuoted = (c) => c.replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/'[^']*'/g, "''");
2675
- var bareOf = (c) => blankQuoted(codeOf(c));
2694
+ var codeOf = (c) => neutralizeEcho(stripComments(blankHeredoc(c)));
2695
+ function commandViews(raw) {
2696
+ const code = codeOf(raw);
2697
+ return { code, bare: blankQuoted(code) };
2698
+ }
2676
2699
  function execGeneratedScript(cmd) {
2677
2700
  const targets = /* @__PURE__ */ new Set();
2678
2701
  for (const m of cmd.matchAll(/(?:>>?|\btee\b\s+(?:-a\s+)?)\s*["']?([A-Za-z0-9._/~-]+)/g)) {
@@ -2741,6 +2764,19 @@ function netTargetScope(rawCommand) {
2741
2764
  }
2742
2765
  return scope;
2743
2766
  }
2767
+ function simpleCommandRule(spec) {
2768
+ const { match, tools = ["Bash"], ...meta } = spec;
2769
+ return {
2770
+ ...meta,
2771
+ tools,
2772
+ test(ctx) {
2773
+ const c = ctx.commandBare;
2774
+ if (!c || !match.test(c))
2775
+ return null;
2776
+ return { evidence: evidence(ctx.command) };
2777
+ }
2778
+ };
2779
+ }
2744
2780
  var RULES = [
2745
2781
  // --- Destructive / data-loss (OWASP ASI02 Tool Misuse) -------------------
2746
2782
  {
@@ -2769,21 +2805,15 @@ var RULES = [
2769
2805
  return { evidence: evidence(ctx.command), severity, mods: { dangerous_target: dangerousTarget, temp_target: tempOnly } };
2770
2806
  }
2771
2807
  },
2772
- {
2808
+ simpleCommandRule({
2773
2809
  id: "destructive.git_reset_hard",
2774
2810
  category: "destructive",
2775
2811
  framework_ref: "OWASP ASI02",
2776
2812
  title: "Discards local changes (git reset --hard)",
2777
- tools: ["Bash"],
2778
2813
  baseSeverity: "low",
2779
2814
  // routine dev op — only uncommitted working-tree changes are lost; commits survive in reflog
2780
- test(ctx) {
2781
- const c = ctx.commandBare;
2782
- if (!c || !/\bgit\s+reset\s+--hard\b/i.test(c))
2783
- return null;
2784
- return { evidence: evidence(ctx.command) };
2785
- }
2786
- },
2815
+ match: /\bgit\s+reset\s+--hard\b/i
2816
+ }),
2787
2817
  {
2788
2818
  id: "destructive.git_force_push",
2789
2819
  category: "destructive",
@@ -2817,20 +2847,14 @@ var RULES = [
2817
2847
  return null;
2818
2848
  }
2819
2849
  },
2820
- {
2850
+ simpleCommandRule({
2821
2851
  id: "destructive.disk_wipe",
2822
2852
  category: "destructive",
2823
2853
  framework_ref: "OWASP ASI02",
2824
2854
  title: "Low-level disk write / wipe (dd, mkfs, shred)",
2825
- tools: ["Bash"],
2826
2855
  baseSeverity: "high",
2827
- test(ctx) {
2828
- const c = ctx.commandBare;
2829
- if (!c || !/(\bdd\s+if=|\bmkfs(\.\w+)?\b|\bshred\b)/i.test(c))
2830
- return null;
2831
- return { evidence: evidence(ctx.command) };
2832
- }
2833
- },
2856
+ match: /(\bdd\s+if=|\bmkfs(\.\w+)?\b|\bshred\b)/i
2857
+ }),
2834
2858
  {
2835
2859
  id: "destructive.overwrite_critical",
2836
2860
  category: "destructive",
@@ -2905,7 +2929,7 @@ var RULES = [
2905
2929
  return null;
2906
2930
  if (!/(\||<)\s*(sudo\s+)?(curl|wget|nc|ncat|netcat|ssh|scp)\b/i.test(c))
2907
2931
  return null;
2908
- if (!/\b(cat|tar|zip|gzip|base64|env|printenv|cat\s|<)\b/i.test(c))
2932
+ if (!/(?:\b(?:cat|tar|zip|gzip|base64|env|printenv)\b|<)/i.test(c))
2909
2933
  return null;
2910
2934
  return { evidence: evidence(ctx.command) };
2911
2935
  }
@@ -2940,37 +2964,24 @@ var RULES = [
2940
2964
  };
2941
2965
  }
2942
2966
  },
2943
- {
2967
+ simpleCommandRule({
2944
2968
  id: "exfil.reverse_shell",
2945
2969
  category: "exfiltration",
2946
2970
  framework_ref: "MITRE ATLAS AML.T0086",
2947
2971
  title: "Netcat / reverse-shell style connection",
2948
- tools: ["Bash"],
2949
2972
  baseSeverity: "critical",
2950
- test(ctx) {
2951
- const c = ctx.commandBare;
2952
- if (!c)
2953
- return null;
2954
- if (!/\b(nc|ncat|netcat)\b[^|;&\n]*(-e\b|-c\b|\d{2,5})|\/dev\/tcp\/|bash\s+-i\b/i.test(c))
2955
- return null;
2956
- return { evidence: evidence(ctx.command) };
2957
- }
2958
- },
2973
+ match: /\b(nc|ncat|netcat)\b[^|;&\n]*(-e\b|-c\b|\d{2,5})|\/dev\/tcp\/|bash\s+-i\b/i
2974
+ }),
2959
2975
  // --- Privilege escalation / guardrail bypass (OWASP LLM06 Excessive Agency)
2960
- {
2976
+ simpleCommandRule({
2961
2977
  id: "privilege.skip_permissions",
2962
2978
  category: "privilege-bypass",
2963
2979
  framework_ref: "OWASP LLM06",
2964
2980
  title: "Bypasses the permission guardrail",
2965
2981
  tools: "*",
2966
2982
  baseSeverity: "high",
2967
- test(ctx) {
2968
- const c = ctx.commandBare;
2969
- if (!c || !/--dangerously-skip-permissions\b|--yolo\b|--dangerously-skip\b/i.test(c))
2970
- return null;
2971
- return { evidence: evidence(ctx.command) };
2972
- }
2973
- },
2983
+ match: /--dangerously-skip-permissions\b|--yolo\b|--dangerously-skip\b/i
2984
+ }),
2974
2985
  {
2975
2986
  id: "privilege.curl_pipe_shell",
2976
2987
  category: "privilege-bypass",
@@ -3020,21 +3031,17 @@ var RULES = [
3020
3031
  return { evidence: evidence(ctx.command), severity: sudo ? "critical" : "high", mods: { decoder: decoderName, interpreter: interp } };
3021
3032
  }
3022
3033
  },
3023
- {
3034
+ simpleCommandRule({
3024
3035
  id: "privilege.sudo",
3025
3036
  category: "privilege-bypass",
3026
3037
  framework_ref: "OWASP LLM06",
3027
3038
  title: "Runs a command as root (sudo)",
3028
- tools: ["Bash"],
3029
3039
  baseSeverity: "high",
3030
3040
  // root escalation removes a real safety barrier — higher than an in-project op
3031
- test(ctx) {
3032
- const c = ctx.commandBare;
3033
- if (!c || !/(?:^|[\n;|&(])\s*sudo\b/i.test(c))
3034
- return null;
3035
- return { evidence: evidence(ctx.command) };
3036
- }
3037
- },
3041
+ // sudo must be in *command position* — start of the command or right after a separator (; | && ( ).
3042
+ // This ignores `sudo` as an argument (e.g. `apt install sudo`) and, via codeOf, inside echo/comments.
3043
+ match: /(?:^|[\n;|&(])\s*sudo\b/i
3044
+ }),
3038
3045
  {
3039
3046
  id: "privilege.exec_generated_script",
3040
3047
  category: "privilege-bypass",
@@ -3053,36 +3060,22 @@ var RULES = [
3053
3060
  return { evidence: evidence(raw), severity: critical ? "critical" : "high", mods: { script_file: file } };
3054
3061
  }
3055
3062
  },
3056
- {
3063
+ simpleCommandRule({
3057
3064
  id: "privilege.chmod_777",
3058
3065
  category: "privilege-bypass",
3059
3066
  framework_ref: "OWASP LLM06",
3060
3067
  title: "Overly permissive permissions (chmod 777)",
3061
- tools: ["Bash"],
3062
3068
  baseSeverity: "medium",
3063
- test(ctx) {
3064
- const c = ctx.commandBare;
3065
- if (!c || !/\bchmod\s+(-[a-zA-Z]+\s+)*0?777\b/i.test(c))
3066
- return null;
3067
- return { evidence: evidence(ctx.command) };
3068
- }
3069
- },
3070
- {
3069
+ match: /\bchmod\s+(-[a-zA-Z]+\s+)*0?777\b/i
3070
+ }),
3071
+ simpleCommandRule({
3071
3072
  id: "privilege.persistence",
3072
3073
  category: "privilege-bypass",
3073
3074
  framework_ref: "OWASP LLM06",
3074
3075
  title: "Installs a persistence mechanism",
3075
- tools: ["Bash"],
3076
3076
  baseSeverity: "high",
3077
- test(ctx) {
3078
- const c = ctx.commandBare;
3079
- if (!c)
3080
- return null;
3081
- if (!/\bcrontab\s+-|\bsystemctl\s+enable\b|\blaunchctl\s+load\b|>>\s*~\/\.(bashrc|zshrc|profile|bash_profile)\b|\/etc\/(cron|systemd|rc\.local)/i.test(c))
3082
- return null;
3083
- return { evidence: evidence(ctx.command) };
3084
- }
3085
- },
3077
+ match: /\bcrontab\s+-|\bsystemctl\s+enable\b|\blaunchctl\s+load\b|>>\s*~\/\.(bashrc|zshrc|profile|bash_profile)\b|\/etc\/(cron|systemd|rc\.local)/i
3078
+ }),
3086
3079
  {
3087
3080
  id: "privilege.write_outside_project",
3088
3081
  category: "privilege-bypass",
@@ -3110,29 +3103,18 @@ function appliesTo(rule, toolName) {
3110
3103
  return rule.tools === "*" || rule.tools.includes(toolName);
3111
3104
  }
3112
3105
  function buildContext(row, projectPath, ownedConfigDirs) {
3113
- let input = null;
3114
- try {
3115
- input = row.input_json ? JSON.parse(row.input_json) : null;
3116
- } catch {
3117
- input = null;
3118
- }
3106
+ const input = parseStored(row.input_json, "detect: tool_calls.input_json");
3119
3107
  const command = typeof input?.command === "string" ? input.command : null;
3120
- const commandCode = command != null ? codeOf(command) : null;
3121
- const commandBare = command != null ? bareOf(command) : null;
3108
+ const views = command != null ? commandViews(command) : null;
3109
+ const commandCode = views?.code ?? null;
3110
+ const commandBare = views?.bare ?? null;
3122
3111
  const filePath = typeof input?.file_path === "string" ? input.file_path : null;
3123
3112
  return { toolName: row.tool_name, command, commandCode, commandBare, filePath, input, resultSummary: row.result_summary, status: row.status, projectPath, ownedConfigDirs };
3124
3113
  }
3125
3114
  function detect(db, dirty) {
3126
3115
  const incremental = dirty != null;
3127
- if (incremental) {
3128
- db.exec("DROP TABLE IF EXISTS _dirty_sec");
3129
- db.exec("CREATE TEMP TABLE _dirty_sec (id TEXT PRIMARY KEY)");
3130
- const ins = db.prepare("INSERT OR IGNORE INTO _dirty_sec (id) VALUES (?)");
3131
- db.transaction((ids) => {
3132
- for (const id of ids)
3133
- ins.run(id);
3134
- })(dirty);
3135
- }
3116
+ if (dirty != null)
3117
+ createDirtySet(db, "_dirty_sec", dirty);
3136
3118
  const scope = incremental ? " WHERE tc.session_id IN (SELECT id FROM _dirty_sec)" : "";
3137
3119
  const ownedConfigDirs = db.prepare("SELECT config_dir FROM sources WHERE config_dir IS NOT NULL").all().map((r) => r.config_dir.replace(/\/$/, "")).filter(Boolean);
3138
3120
  const projPath = /* @__PURE__ */ new Map();
@@ -3201,15 +3183,8 @@ function detect(db, dirty) {
3201
3183
  function classifyErrors(db, dirty) {
3202
3184
  const incremental = dirty != null;
3203
3185
  const scope = incremental ? " AND session_id IN (SELECT id FROM _dirty_err)" : "";
3204
- if (incremental) {
3205
- db.exec("DROP TABLE IF EXISTS _dirty_err");
3206
- db.exec("CREATE TEMP TABLE _dirty_err (id TEXT PRIMARY KEY)");
3207
- const ins = db.prepare("INSERT OR IGNORE INTO _dirty_err (id) VALUES (?)");
3208
- db.transaction((ids) => {
3209
- for (const id of ids)
3210
- ins.run(id);
3211
- })(dirty);
3212
- }
3186
+ if (dirty != null)
3187
+ createDirtySet(db, "_dirty_err", dirty);
3213
3188
  const rows = db.prepare(`SELECT id, result_summary FROM tool_calls WHERE status = 'error'${scope}`).all();
3214
3189
  const update = db.prepare("UPDATE tool_calls SET error_type = ? WHERE id = ?");
3215
3190
  const tx = db.transaction(() => {
@@ -3459,14 +3434,8 @@ function pruneExcluded(db, excludedPaths) {
3459
3434
  const projIds = projects.filter((p) => excludedPaths.some((e) => p.path === e || p.path.startsWith(e + "/"))).map((p) => p.id);
3460
3435
  if (!projIds.length)
3461
3436
  return 0;
3462
- db.exec("DROP TABLE IF EXISTS _prune");
3463
- db.exec("CREATE TEMP TABLE _prune (id TEXT PRIMARY KEY)");
3464
- const insP = db.prepare("INSERT OR IGNORE INTO _prune (id) VALUES (?)");
3465
3437
  const seed = db.prepare(`SELECT id FROM sessions WHERE project_id IN (${projIds.map(() => "?").join(",")})`).all(...projIds);
3466
- db.transaction(() => {
3467
- for (const s of seed)
3468
- insP.run(s.id);
3469
- })();
3438
+ createDirtySet(db, "_prune", seed.map((s) => s.id));
3470
3439
  const expand = db.prepare("INSERT OR IGNORE INTO _prune (id) SELECT id FROM sessions WHERE parent_session_id IN (SELECT id FROM _prune)");
3471
3440
  const cnt = db.prepare("SELECT COUNT(*) n FROM _prune");
3472
3441
  for (let prev = -1; ; ) {
@@ -3543,54 +3512,36 @@ function buildTurns(sessionId, rows) {
3543
3512
  t.duration_ms = durationMs(t.started_at, t.ended_at);
3544
3513
  return { turns, eventTurn };
3545
3514
  }
3546
- function rebuildDerived(db, dirty) {
3547
- const incremental = dirty != null;
3548
- const bySession = incremental ? " WHERE session_id IN (SELECT id FROM _dirty)" : "";
3549
- const byId = incremental ? " WHERE id IN (SELECT id FROM _dirty)" : "";
3550
- const andId = incremental ? " AND id IN (SELECT id FROM _dirty)" : "";
3551
- const andSelfId = incremental ? " AND sessions.id IN (SELECT id FROM _dirty)" : "";
3552
- if (incremental) {
3553
- db.exec("DROP TABLE IF EXISTS _dirty");
3554
- db.exec("CREATE TEMP TABLE _dirty (id TEXT PRIMARY KEY)");
3555
- const ins = db.prepare("INSERT OR IGNORE INTO _dirty (id) VALUES (?)");
3556
- db.transaction((ids) => {
3557
- for (const id of ids)
3558
- ins.run(id);
3559
- })(dirty);
3560
- const expand = db.prepare(`
3561
- INSERT OR IGNORE INTO _dirty (id)
3562
- SELECT spawned_session_id FROM tool_calls
3563
- WHERE session_id IN (SELECT id FROM _dirty) AND spawned_session_id IS NOT NULL
3564
- UNION
3565
- SELECT session_id FROM tool_calls
3566
- WHERE spawned_session_id IN (SELECT id FROM _dirty)
3567
- UNION
3568
- -- structural (path-based) workflow linkage: a dirty parent pulls in its subagent children,
3569
- -- and a dirty child pulls in its structural parent, so the fallback link stays correct when
3570
- -- parent and child transcripts arrive in different runs (ADR-010).
3571
- SELECT id FROM sessions
3572
- WHERE spawn_parent_id IN (SELECT id FROM _dirty)
3573
- UNION
3574
- SELECT spawn_parent_id FROM sessions
3575
- WHERE id IN (SELECT id FROM _dirty) AND spawn_parent_id IS NOT NULL
3576
- `);
3577
- const count = db.prepare("SELECT COUNT(*) n FROM _dirty");
3578
- let prev = -1;
3579
- for (; ; ) {
3580
- const n = count.get().n;
3581
- if (n === prev)
3582
- break;
3583
- prev = n;
3584
- expand.run();
3585
- }
3515
+ function expandDirtyScope(db, dirty) {
3516
+ createDirtySet(db, "_dirty", dirty);
3517
+ const expand = db.prepare(`
3518
+ INSERT OR IGNORE INTO _dirty (id)
3519
+ SELECT spawned_session_id FROM tool_calls
3520
+ WHERE session_id IN (SELECT id FROM _dirty) AND spawned_session_id IS NOT NULL
3521
+ UNION
3522
+ SELECT session_id FROM tool_calls
3523
+ WHERE spawned_session_id IN (SELECT id FROM _dirty)
3524
+ UNION
3525
+ -- structural (path-based) workflow linkage: a dirty parent pulls in its subagent children,
3526
+ -- and a dirty child pulls in its structural parent, so the fallback link stays correct when
3527
+ -- parent and child transcripts arrive in different runs (ADR-010).
3528
+ SELECT id FROM sessions
3529
+ WHERE spawn_parent_id IN (SELECT id FROM _dirty)
3530
+ UNION
3531
+ SELECT spawn_parent_id FROM sessions
3532
+ WHERE id IN (SELECT id FROM _dirty) AND spawn_parent_id IS NOT NULL
3533
+ `);
3534
+ const count = db.prepare("SELECT COUNT(*) n FROM _dirty");
3535
+ let prev = -1;
3536
+ for (; ; ) {
3537
+ const n = count.get().n;
3538
+ if (n === prev)
3539
+ break;
3540
+ prev = n;
3541
+ expand.run();
3586
3542
  }
3587
- db.exec(`UPDATE events SET turn_id = NULL${bySession}`);
3588
- db.exec(`UPDATE token_usage SET turn_id = NULL${bySession}`);
3589
- db.exec(`UPDATE tool_calls SET turn_id = NULL${bySession}`);
3590
- db.exec(`UPDATE tool_calls SET skill_id = NULL${bySession}`);
3591
- db.exec(`UPDATE sessions SET parent_turn_id = NULL${byId}`);
3592
- db.exec(`DELETE FROM turns${bySession}`);
3593
- const sessionIds = db.prepare(`SELECT id FROM sessions${byId}`).all();
3543
+ }
3544
+ function rebuildTurns(db, scope, sessionIds) {
3594
3545
  const selEvents = db.prepare("SELECT uuid, type, is_sidechain, is_meta, timestamp, model, text FROM events WHERE session_id = ?");
3595
3546
  const insTurn = db.prepare(`INSERT INTO turns (id, session_id, seq, user_event_uuid, prompt_preview, model, started_at, ended_at, duration_ms)
3596
3547
  VALUES (@id, @session_id, @seq, @user_event_uuid, @prompt_preview, @model, @started_at, @ended_at, @duration_ms)`);
@@ -3606,123 +3557,152 @@ function rebuildDerived(db, dirty) {
3606
3557
  }
3607
3558
  });
3608
3559
  tx();
3609
- db.exec(`UPDATE token_usage SET turn_id = (SELECT turn_id FROM events WHERE events.uuid = token_usage.event_uuid)${bySession}`);
3610
- db.exec(`UPDATE tool_calls SET turn_id = (SELECT turn_id FROM events WHERE events.uuid = tool_calls.event_uuid)${bySession}`);
3611
- {
3612
- const selEventsOrdered = db.prepare("SELECT uuid, type, is_meta, timestamp, text FROM events WHERE session_id = ?");
3613
- const selSkillCalls = db.prepare("SELECT id, event_uuid, skill_name, input_json FROM tool_calls WHERE session_id = ? AND tool_name = 'Skill'");
3614
- const upsertSkill = db.prepare(`INSERT INTO skills (id, name, base_dir, body, summary, body_bytes, first_seen, last_seen)
3615
- VALUES (@id, @name, @base_dir, @body, @summary, @body_bytes, @ts, @ts)
3616
- ON CONFLICT(id) DO UPDATE SET
3617
- last_seen = MAX(last_seen, excluded.last_seen),
3618
- first_seen = MIN(first_seen, excluded.first_seen),
3619
- base_dir = excluded.base_dir`);
3620
- const linkCall = db.prepare("UPDATE tool_calls SET skill_id = ? WHERE id = ?");
3621
- const linkTx = db.transaction(() => {
3622
- for (const { id: sid } of sessionIds) {
3623
- const calls = selSkillCalls.all(sid);
3624
- if (!calls.length)
3560
+ db.exec(`UPDATE token_usage SET turn_id = (SELECT turn_id FROM events WHERE events.uuid = token_usage.event_uuid)${scope.bySession}`);
3561
+ db.exec(`UPDATE tool_calls SET turn_id = (SELECT turn_id FROM events WHERE events.uuid = tool_calls.event_uuid)${scope.bySession}`);
3562
+ }
3563
+ function linkSkillVersions(db, sessionIds) {
3564
+ const selEventsOrdered = db.prepare("SELECT uuid, type, is_meta, timestamp, text FROM events WHERE session_id = ?");
3565
+ const selSkillCalls = db.prepare("SELECT id, event_uuid, skill_name, input_json FROM tool_calls WHERE session_id = ? AND tool_name = 'Skill'");
3566
+ const upsertSkill = db.prepare(`INSERT INTO skills (id, name, base_dir, body, summary, body_bytes, first_seen, last_seen)
3567
+ VALUES (@id, @name, @base_dir, @body, @summary, @body_bytes, @ts, @ts)
3568
+ ON CONFLICT(id) DO UPDATE SET
3569
+ last_seen = MAX(last_seen, excluded.last_seen),
3570
+ first_seen = MIN(first_seen, excluded.first_seen),
3571
+ base_dir = excluded.base_dir`);
3572
+ const linkCall = db.prepare("UPDATE tool_calls SET skill_id = ? WHERE id = ?");
3573
+ const linkTx = db.transaction(() => {
3574
+ for (const { id: sid } of sessionIds) {
3575
+ const calls = selSkillCalls.all(sid);
3576
+ if (!calls.length)
3577
+ continue;
3578
+ const events = selEventsOrdered.all(sid).sort((x, y) => (x.timestamp ?? "").localeCompare(y.timestamp ?? "") || x.uuid.localeCompare(y.uuid));
3579
+ const callByEvent = /* @__PURE__ */ new Map();
3580
+ for (const c of calls) {
3581
+ if (!c.event_uuid)
3625
3582
  continue;
3626
- const events = selEventsOrdered.all(sid).sort((x, y) => (x.timestamp ?? "").localeCompare(y.timestamp ?? "") || x.uuid.localeCompare(y.uuid));
3627
- const callByEvent = /* @__PURE__ */ new Map();
3628
- for (const c of calls) {
3629
- if (!c.event_uuid)
3630
- continue;
3631
- const arr = callByEvent.get(c.event_uuid) ?? [];
3632
- arr.push(c);
3633
- callByEvent.set(c.event_uuid, arr);
3634
- }
3635
- const pending = /* @__PURE__ */ new Map();
3636
- for (const e of events) {
3637
- const here = callByEvent.get(e.uuid);
3638
- if (here) {
3639
- for (const c2 of here) {
3640
- if (!c2.skill_name)
3641
- continue;
3642
- const key = skillNameKey(c2.skill_name);
3643
- let args = null;
3644
- try {
3645
- args = c2.input_json ? JSON.parse(c2.input_json).args ?? null : null;
3646
- if (typeof args === "string")
3647
- args = args.trim() || null;
3648
- else if (args != null)
3649
- args = JSON.stringify(args);
3650
- } catch {
3651
- }
3652
- const arr = pending.get(key) ?? [];
3653
- arr.push({ id: c2.id, args });
3654
- pending.set(key, arr);
3583
+ const arr = callByEvent.get(c.event_uuid) ?? [];
3584
+ arr.push(c);
3585
+ callByEvent.set(c.event_uuid, arr);
3586
+ }
3587
+ const pending = /* @__PURE__ */ new Map();
3588
+ for (const e of events) {
3589
+ const here = callByEvent.get(e.uuid);
3590
+ if (here) {
3591
+ for (const c2 of here) {
3592
+ if (!c2.skill_name)
3593
+ continue;
3594
+ const key = skillNameKey(c2.skill_name);
3595
+ let args = null;
3596
+ try {
3597
+ args = c2.input_json ? JSON.parse(c2.input_json).args ?? null : null;
3598
+ if (typeof args === "string")
3599
+ args = args.trim() || null;
3600
+ else if (args != null)
3601
+ args = JSON.stringify(args);
3602
+ } catch {
3655
3603
  }
3604
+ const arr = pending.get(key) ?? [];
3605
+ arr.push({ id: c2.id, args });
3606
+ pending.set(key, arr);
3656
3607
  }
3657
- if (e.is_meta !== 1 || e.type !== "user" || !e.text)
3658
- continue;
3659
- const inj = parseSkillInjection(e.text);
3660
- if (!inj)
3661
- continue;
3662
- const queue = pending.get(inj.nameKey);
3663
- if (!queue || !queue.length)
3664
- continue;
3665
- let idx = inj.args != null ? queue.findIndex((q) => q.args === inj.args) : -1;
3666
- if (idx < 0)
3667
- idx = 0;
3668
- const [match] = queue.splice(idx, 1);
3669
- const c = calls.find((x) => x.id === match.id);
3670
- const name = c.skill_name;
3671
- const vid = skillVersionId(name, inj.body);
3672
- upsertSkill.run({
3673
- id: vid,
3674
- name,
3675
- base_dir: inj.baseDir,
3676
- body: inj.body,
3677
- summary: skillSummary(inj.body),
3678
- body_bytes: Buffer.byteLength(inj.body, "utf8"),
3679
- ts: e.timestamp
3680
- });
3681
- linkCall.run(vid, match.id);
3682
3608
  }
3609
+ if (e.is_meta !== 1 || e.type !== "user" || !e.text)
3610
+ continue;
3611
+ const inj = parseSkillInjection(e.text);
3612
+ if (!inj)
3613
+ continue;
3614
+ const queue = pending.get(inj.nameKey);
3615
+ if (!queue || !queue.length)
3616
+ continue;
3617
+ let idx = inj.args != null ? queue.findIndex((q) => q.args === inj.args) : -1;
3618
+ if (idx < 0)
3619
+ idx = 0;
3620
+ const [match] = queue.splice(idx, 1);
3621
+ const c = calls.find((x) => x.id === match.id);
3622
+ const name = c.skill_name;
3623
+ const vid = skillVersionId(name, inj.body);
3624
+ upsertSkill.run({
3625
+ id: vid,
3626
+ name,
3627
+ base_dir: inj.baseDir,
3628
+ body: inj.body,
3629
+ summary: skillSummary(inj.body),
3630
+ body_bytes: Buffer.byteLength(inj.body, "utf8"),
3631
+ ts: e.timestamp
3632
+ });
3633
+ linkCall.run(vid, match.id);
3683
3634
  }
3684
- });
3685
- linkTx();
3686
- }
3635
+ }
3636
+ });
3637
+ linkTx();
3638
+ }
3639
+ function linkParents(db, scope) {
3687
3640
  db.exec(`
3688
3641
  UPDATE sessions SET
3689
3642
  parent_session_id = (SELECT tc.session_id FROM tool_calls tc WHERE tc.spawned_session_id = sessions.id),
3690
3643
  parent_turn_id = (SELECT tc.turn_id FROM tool_calls tc WHERE tc.spawned_session_id = sessions.id)
3691
- WHERE EXISTS (SELECT 1 FROM tool_calls tc WHERE tc.spawned_session_id = sessions.id)${andSelfId}
3644
+ WHERE EXISTS (SELECT 1 FROM tool_calls tc WHERE tc.spawned_session_id = sessions.id)${scope.andSelfId}
3692
3645
  `);
3693
3646
  db.exec(`
3694
3647
  UPDATE sessions SET
3695
3648
  parent_session_id = COALESCE(parent_session_id, (SELECT tc.session_id FROM tool_calls tc WHERE tc.workflow_run_id = sessions.workflow_run_id LIMIT 1)),
3696
3649
  parent_turn_id = COALESCE(parent_turn_id, (SELECT tc.turn_id FROM tool_calls tc WHERE tc.workflow_run_id = sessions.workflow_run_id LIMIT 1))
3697
3650
  WHERE sessions.workflow_run_id IS NOT NULL
3698
- AND EXISTS (SELECT 1 FROM tool_calls tc WHERE tc.workflow_run_id = sessions.workflow_run_id)${andSelfId}
3651
+ AND EXISTS (SELECT 1 FROM tool_calls tc WHERE tc.workflow_run_id = sessions.workflow_run_id)${scope.andSelfId}
3699
3652
  `);
3700
3653
  db.exec(`
3701
3654
  UPDATE sessions SET parent_session_id = spawn_parent_id
3702
3655
  WHERE parent_session_id IS NULL
3703
3656
  AND spawn_parent_id IS NOT NULL
3704
- AND spawn_parent_id IN (SELECT id FROM sessions)${andSelfId}
3657
+ AND spawn_parent_id IN (SELECT id FROM sessions)${scope.andSelfId}
3705
3658
  `);
3659
+ }
3660
+ function recomputeSessionAggregates(db, scope) {
3706
3661
  db.exec(`
3707
3662
  UPDATE sessions SET
3708
3663
  event_count = (SELECT COUNT(*) FROM events e WHERE e.session_id = sessions.id),
3709
3664
  started_at = (SELECT MIN(timestamp) FROM events e WHERE e.session_id = sessions.id),
3710
3665
  ended_at = (SELECT MAX(timestamp) FROM events e WHERE e.session_id = sessions.id),
3711
3666
  turn_count = (SELECT COUNT(*) FROM turns t WHERE t.session_id = sessions.id),
3712
- is_sidechain = (SELECT CASE WHEN MIN(is_sidechain) = 1 THEN 1 ELSE 0 END FROM events e WHERE e.session_id = sessions.id)${byId}
3667
+ is_sidechain = (SELECT CASE WHEN MIN(is_sidechain) = 1 THEN 1 ELSE 0 END FROM events e WHERE e.session_id = sessions.id)${scope.byId}
3713
3668
  `);
3714
3669
  db.exec(`
3715
3670
  UPDATE sessions SET duration_ms =
3716
3671
  CAST((julianday(ended_at) - julianday(started_at)) * 86400000 AS INTEGER)
3717
- WHERE started_at IS NOT NULL AND ended_at IS NOT NULL${andId}
3672
+ WHERE started_at IS NOT NULL AND ended_at IS NOT NULL${scope.andId}
3718
3673
  `);
3719
- db.exec(`DELETE FROM sessions WHERE event_count = 0${andId}`);
3674
+ }
3675
+ function pruneOrphans(db, scope, incremental) {
3676
+ db.exec(`DELETE FROM sessions WHERE event_count = 0${scope.andId}`);
3720
3677
  db.exec("DELETE FROM classifications WHERE scope = 'session' AND target_id NOT IN (SELECT id FROM sessions)");
3721
3678
  db.exec("DELETE FROM findings WHERE session_id NOT IN (SELECT id FROM sessions)");
3722
3679
  db.exec("DELETE FROM file_changes WHERE session_id NOT IN (SELECT id FROM sessions)");
3723
3680
  if (!incremental) {
3724
3681
  db.exec("DELETE FROM skills WHERE id NOT IN (SELECT skill_id FROM tool_calls WHERE skill_id IS NOT NULL)");
3725
3682
  }
3683
+ }
3684
+ function rebuildDerived(db, dirty) {
3685
+ const incremental = dirty != null;
3686
+ const scope = {
3687
+ bySession: incremental ? " WHERE session_id IN (SELECT id FROM _dirty)" : "",
3688
+ byId: incremental ? " WHERE id IN (SELECT id FROM _dirty)" : "",
3689
+ andId: incremental ? " AND id IN (SELECT id FROM _dirty)" : "",
3690
+ andSelfId: incremental ? " AND sessions.id IN (SELECT id FROM _dirty)" : ""
3691
+ };
3692
+ if (dirty != null)
3693
+ expandDirtyScope(db, dirty);
3694
+ db.exec(`UPDATE events SET turn_id = NULL${scope.bySession}`);
3695
+ db.exec(`UPDATE token_usage SET turn_id = NULL${scope.bySession}`);
3696
+ db.exec(`UPDATE tool_calls SET turn_id = NULL${scope.bySession}`);
3697
+ db.exec(`UPDATE tool_calls SET skill_id = NULL${scope.bySession}`);
3698
+ db.exec(`UPDATE sessions SET parent_turn_id = NULL${scope.byId}`);
3699
+ db.exec(`DELETE FROM turns${scope.bySession}`);
3700
+ const sessionIds = db.prepare(`SELECT id FROM sessions${scope.byId}`).all();
3701
+ rebuildTurns(db, scope, sessionIds);
3702
+ linkSkillVersions(db, sessionIds);
3703
+ linkParents(db, scope);
3704
+ recomputeSessionAggregates(db, scope);
3705
+ pruneOrphans(db, scope, incremental);
3726
3706
  if (incremental) {
3727
3707
  const expanded = new Set(db.prepare("SELECT id FROM _dirty").all().map((r) => r.id));
3728
3708
  db.exec("DROP TABLE IF EXISTS _dirty");
@@ -3869,8 +3849,11 @@ function ingestFile(db, stmts, adapter, file, lines, meta, now2, stats) {
3869
3849
  }
3870
3850
 
3871
3851
  // ../ingest/dist/workflows.js
3852
+ import { basename as basename3 } from "path";
3853
+
3854
+ // ../ingest/dist/sidecar.js
3872
3855
  import { readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
3873
- import { basename as basename3, join as join7 } from "path";
3856
+ import { join as join7 } from "path";
3874
3857
 
3875
3858
  // ../ingest/dist/redact.js
3876
3859
  import { createHash as createHash6, randomBytes } from "crypto";
@@ -3926,11 +3909,18 @@ function* streamLines(path3) {
3926
3909
  }
3927
3910
  }
3928
3911
 
3929
- // ../ingest/dist/workflows.js
3930
- function newWorkflowStats() {
3912
+ // ../ingest/dist/sidecar.js
3913
+ function newSidecarStats() {
3931
3914
  return { files: 0, upserted: 0, skipped: 0, malformed: 0 };
3932
3915
  }
3933
- function walkSidecars(dir, inWorkflows, out) {
3916
+ var intOrNull = (v) => typeof v === "number" && Number.isFinite(v) ? Math.trunc(v) : null;
3917
+ var strOrNull = (v) => typeof v === "string" && v ? v : null;
3918
+ function sessionBefore(path3, subdir) {
3919
+ const parts = path3.split("/");
3920
+ const i = parts.indexOf(subdir);
3921
+ return i > 0 ? parts[i - 1] : null;
3922
+ }
3923
+ function walk(dir, subdir, inSubdir, matchName, out) {
3934
3924
  let entries;
3935
3925
  try {
3936
3926
  entries = readdirSync3(dir, { withFileTypes: true });
@@ -3940,47 +3930,24 @@ function walkSidecars(dir, inWorkflows, out) {
3940
3930
  for (const e of entries) {
3941
3931
  const p = join7(dir, e.name);
3942
3932
  if (e.isDirectory())
3943
- walkSidecars(p, inWorkflows || e.name === "workflows", out);
3944
- else if (e.isFile() && inWorkflows && /^wf_.+\.json$/.test(e.name))
3933
+ walk(p, subdir, inSubdir || e.name === subdir, matchName, out);
3934
+ else if (e.isFile() && inSubdir && matchName(e.name))
3945
3935
  out.push(p);
3946
3936
  }
3947
3937
  }
3948
- function discoverSidecars(sourceArchiveDir) {
3938
+ function discover(sourceArchiveDir, subdir, matchName) {
3949
3939
  const out = [];
3950
- walkSidecars(join7(sourceArchiveDir, "projects"), false, out);
3940
+ walk(join7(sourceArchiveDir, "projects"), subdir, false, matchName, out);
3951
3941
  try {
3952
3942
  for (const ts of readdirSync3(join7(sourceArchiveDir, ".versions"), { withFileTypes: true }))
3953
3943
  if (ts.isDirectory())
3954
- walkSidecars(join7(sourceArchiveDir, ts.name, "projects"), false, out);
3944
+ walk(join7(sourceArchiveDir, ts.name, "projects"), subdir, false, matchName, out);
3955
3945
  } catch {
3956
3946
  }
3957
3947
  return out;
3958
3948
  }
3959
- function sessionFromPath(path3) {
3960
- const parts = path3.split("/");
3961
- const i = parts.indexOf("workflows");
3962
- return i > 0 ? parts[i - 1] : null;
3963
- }
3964
- function toIso(x) {
3965
- if (typeof x === "number" && Number.isFinite(x))
3966
- return new Date(x).toISOString();
3967
- if (typeof x === "string" && x) {
3968
- const t = Date.parse(x);
3969
- return Number.isNaN(t) ? null : new Date(t).toISOString();
3970
- }
3971
- return null;
3972
- }
3973
- var jsonOrNull = (v) => v == null ? null : JSON.stringify(v);
3974
- var intOrNull = (v) => typeof v === "number" && Number.isFinite(v) ? Math.trunc(v) : null;
3975
- var strOrNull = (v) => typeof v === "string" && v ? v : null;
3976
- function cleanStr(v) {
3977
- const s = strOrNull(v);
3978
- if (!s)
3979
- return null;
3980
- return s.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "").replace(/\[[0-9;]+m\]?/g, "").trim() || null;
3981
- }
3982
- function ingestWorkflowResults(db, sourceArchiveDir, sourceId, excludedDirs, now2, stats, full) {
3983
- let paths = discoverSidecars(sourceArchiveDir);
3949
+ function ingestSidecars(db, sourceArchiveDir, excludedDirs, now2, stats, full, cfg) {
3950
+ let paths = discover(sourceArchiveDir, cfg.subdir, cfg.matchName);
3984
3951
  if (excludedDirs.length)
3985
3952
  paths = paths.filter((p) => !isExcludedArchivePath(p, excludedDirs));
3986
3953
  paths.sort((a, b) => Number(a.includes("/.versions/")) - Number(b.includes("/.versions/")) || (a < b ? -1 : 1));
@@ -3988,23 +3955,9 @@ function ingestWorkflowResults(db, sourceArchiveDir, sourceId, excludedDirs, now
3988
3955
  const setState = db.prepare(`INSERT INTO ingest_state (file_path, size, mtime_ms, sha256, events_ingested, ingested_at)
3989
3956
  VALUES (@file_path, @size, @mtime_ms, @sha256, 0, @ingested_at)
3990
3957
  ON CONFLICT(file_path) DO UPDATE SET size=excluded.size, mtime_ms=excluded.mtime_ms, sha256=excluded.sha256, ingested_at=excluded.ingested_at`);
3991
- const upsert = db.prepare(`INSERT INTO workflow_results
3992
- (run_id, source_id, session_id, task_id, workflow_name, status, summary, default_model,
3993
- result_json, phases_json, logs_json, progress_json,
3994
- agent_count, total_tokens, total_tool_calls, duration_ms, started_at, ended_at, ingested_at)
3995
- VALUES
3996
- (@run_id, @source_id, @session_id, @task_id, @workflow_name, @status, @summary, @default_model,
3997
- @result_json, @phases_json, @logs_json, @progress_json,
3998
- @agent_count, @total_tokens, @total_tool_calls, @duration_ms, @started_at, @ended_at, @ingested_at)
3999
- ON CONFLICT(run_id) DO UPDATE SET
4000
- source_id=excluded.source_id, session_id=excluded.session_id, task_id=excluded.task_id,
4001
- workflow_name=excluded.workflow_name, status=excluded.status, summary=excluded.summary,
4002
- default_model=excluded.default_model, result_json=excluded.result_json, phases_json=excluded.phases_json,
4003
- logs_json=excluded.logs_json, progress_json=excluded.progress_json, agent_count=excluded.agent_count,
4004
- total_tokens=excluded.total_tokens, total_tool_calls=excluded.total_tool_calls,
4005
- duration_ms=excluded.duration_ms, started_at=excluded.started_at, ended_at=excluded.ended_at,
4006
- ingested_at=excluded.ingested_at`);
4007
3958
  for (const path3 of paths) {
3959
+ if (cfg.include && !cfg.include(path3))
3960
+ continue;
4008
3961
  stats.files++;
4009
3962
  const st = statSync2(path3);
4010
3963
  const mtimeMs = Math.trunc(st.mtimeMs);
@@ -4026,232 +3979,160 @@ function ingestWorkflowResults(db, sourceArchiveDir, sourceId, excludedDirs, now
4026
3979
  stats.skipped++;
4027
3980
  continue;
4028
3981
  }
4029
- let d;
4030
- try {
4031
- d = JSON.parse(buf.toString("utf8"));
4032
- } catch {
4033
- stats.malformed++;
4034
- continue;
4035
- }
4036
- if (!d || typeof d !== "object") {
3982
+ if (cfg.handle(path3, buf, st.size)) {
3983
+ setState.run({ file_path: path3, size: st.size, mtime_ms: mtimeMs, sha256: hash2, ingested_at: now2 });
3984
+ stats.upserted++;
3985
+ } else {
4037
3986
  stats.malformed++;
4038
- continue;
4039
3987
  }
4040
- const runId = strOrNull(d.runId) ?? basename3(path3, ".json");
4041
- const startedAt = toIso(d.startTime) ?? toIso(d.timestamp);
4042
- const durationMs2 = intOrNull(d.durationMs);
4043
- const endedAt = startedAt && durationMs2 != null ? new Date(Date.parse(startedAt) + durationMs2).toISOString() : null;
4044
- upsert.run({
4045
- run_id: runId,
4046
- source_id: sourceId,
4047
- session_id: sessionFromPath(path3),
4048
- task_id: strOrNull(d.taskId),
4049
- workflow_name: strOrNull(d.workflowName),
4050
- status: strOrNull(d.status),
4051
- summary: strOrNull(d.summary),
4052
- default_model: cleanStr(d.defaultModel),
4053
- result_json: jsonOrNull(d.result),
4054
- phases_json: Array.isArray(d.phases) ? JSON.stringify(d.phases) : null,
4055
- logs_json: Array.isArray(d.logs) ? JSON.stringify(d.logs) : null,
4056
- progress_json: Array.isArray(d.workflowProgress) ? JSON.stringify(d.workflowProgress) : null,
4057
- agent_count: intOrNull(d.agentCount),
4058
- total_tokens: intOrNull(d.totalTokens),
4059
- total_tool_calls: intOrNull(d.totalToolCalls),
4060
- duration_ms: durationMs2,
4061
- started_at: startedAt,
4062
- ended_at: endedAt,
4063
- ingested_at: now2
4064
- });
4065
- setState.run({ file_path: path3, size: st.size, mtime_ms: mtimeMs, sha256: hash2, ingested_at: now2 });
4066
- stats.upserted++;
4067
3988
  }
4068
3989
  }
4069
3990
 
4070
- // ../ingest/dist/meta.js
4071
- import { readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
4072
- import { basename as basename4, join as join8 } from "path";
4073
- function newMetaStats() {
4074
- return { files: 0, upserted: 0, skipped: 0, malformed: 0 };
4075
- }
4076
- function walkMeta(dir, inSubagents, out) {
4077
- let entries;
4078
- try {
4079
- entries = readdirSync4(dir, { withFileTypes: true });
4080
- } catch {
4081
- return;
4082
- }
4083
- for (const e of entries) {
4084
- const p = join8(dir, e.name);
4085
- if (e.isDirectory())
4086
- walkMeta(p, inSubagents || e.name === "subagents", out);
4087
- else if (e.isFile() && inSubagents && /^agent-.+\.meta\.json$/.test(e.name))
4088
- out.push(p);
3991
+ // ../ingest/dist/workflows.js
3992
+ var newWorkflowStats = newSidecarStats;
3993
+ function toIso(x) {
3994
+ if (typeof x === "number" && Number.isFinite(x))
3995
+ return new Date(x).toISOString();
3996
+ if (typeof x === "string" && x) {
3997
+ const t = Date.parse(x);
3998
+ return Number.isNaN(t) ? null : new Date(t).toISOString();
4089
3999
  }
4000
+ return null;
4090
4001
  }
4091
- function discoverMeta(sourceArchiveDir) {
4092
- const out = [];
4093
- walkMeta(join8(sourceArchiveDir, "projects"), false, out);
4094
- try {
4095
- for (const ts of readdirSync4(join8(sourceArchiveDir, ".versions"), { withFileTypes: true }))
4096
- if (ts.isDirectory())
4097
- walkMeta(join8(sourceArchiveDir, ts.name, "projects"), false, out);
4098
- } catch {
4099
- }
4100
- return out;
4002
+ var jsonOrNull = (v) => v == null ? null : JSON.stringify(v);
4003
+ function cleanStr(v) {
4004
+ const s = strOrNull(v);
4005
+ if (!s)
4006
+ return null;
4007
+ return s.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "").replace(/\[[0-9;]+m\]?/g, "").trim() || null;
4101
4008
  }
4102
- var intOrNull2 = (v) => typeof v === "number" && Number.isFinite(v) ? Math.trunc(v) : null;
4103
- var strOrNull2 = (v) => typeof v === "string" && v ? v : null;
4104
- function ingestSubagentMeta(db, sourceArchiveDir, sourceId, excludedDirs, now2, stats, full) {
4105
- let paths = discoverMeta(sourceArchiveDir);
4106
- if (excludedDirs.length)
4107
- paths = paths.filter((p) => !isExcludedArchivePath(p, excludedDirs));
4108
- paths.sort((a, b) => Number(a.includes("/.versions/")) - Number(b.includes("/.versions/")) || (a < b ? -1 : 1));
4109
- const getState = db.prepare("SELECT size, mtime_ms, sha256 FROM ingest_state WHERE file_path = ?");
4110
- const setState = db.prepare(`INSERT INTO ingest_state (file_path, size, mtime_ms, sha256, events_ingested, ingested_at)
4111
- VALUES (@file_path, @size, @mtime_ms, @sha256, 0, @ingested_at)
4112
- ON CONFLICT(file_path) DO UPDATE SET size=excluded.size, mtime_ms=excluded.mtime_ms, sha256=excluded.sha256, ingested_at=excluded.ingested_at`);
4113
- const upsert = db.prepare(`INSERT INTO session_meta
4114
- (session_id, source_id, agent_type, agent_description, spawn_depth, tool_use_id, ingested_at)
4009
+ function ingestWorkflowResults(db, sourceArchiveDir, sourceId, excludedDirs, now2, stats, full) {
4010
+ const upsert = db.prepare(`INSERT INTO workflow_results
4011
+ (run_id, source_id, session_id, task_id, workflow_name, status, summary, default_model,
4012
+ result_json, phases_json, logs_json, progress_json,
4013
+ agent_count, total_tokens, total_tool_calls, duration_ms, started_at, ended_at, ingested_at)
4115
4014
  VALUES
4116
- (@session_id, @source_id, @agent_type, @agent_description, @spawn_depth, @tool_use_id, @ingested_at)
4117
- ON CONFLICT(session_id) DO UPDATE SET
4118
- source_id=excluded.source_id, agent_type=excluded.agent_type,
4119
- agent_description=excluded.agent_description, spawn_depth=excluded.spawn_depth,
4120
- tool_use_id=excluded.tool_use_id, ingested_at=excluded.ingested_at`);
4121
- for (const path3 of paths) {
4122
- stats.files++;
4123
- const st = statSync3(path3);
4124
- const mtimeMs = Math.trunc(st.mtimeMs);
4125
- const prev = full ? void 0 : getState.get(path3);
4126
- if (prev && prev.size === st.size && prev.mtime_ms === mtimeMs) {
4127
- stats.skipped++;
4128
- continue;
4129
- }
4130
- let buf;
4131
- try {
4132
- buf = readFileSync5(path3);
4133
- } catch {
4134
- stats.malformed++;
4135
- continue;
4136
- }
4137
- const hash2 = sha256(buf);
4138
- if (prev && prev.sha256 === hash2) {
4139
- setState.run({ file_path: path3, size: st.size, mtime_ms: mtimeMs, sha256: hash2, ingested_at: now2 });
4140
- stats.skipped++;
4141
- continue;
4142
- }
4143
- let d;
4144
- try {
4145
- d = JSON.parse(buf.toString("utf8"));
4146
- } catch {
4147
- stats.malformed++;
4148
- continue;
4015
+ (@run_id, @source_id, @session_id, @task_id, @workflow_name, @status, @summary, @default_model,
4016
+ @result_json, @phases_json, @logs_json, @progress_json,
4017
+ @agent_count, @total_tokens, @total_tool_calls, @duration_ms, @started_at, @ended_at, @ingested_at)
4018
+ ON CONFLICT(run_id) DO UPDATE SET
4019
+ source_id=excluded.source_id, session_id=excluded.session_id, task_id=excluded.task_id,
4020
+ workflow_name=excluded.workflow_name, status=excluded.status, summary=excluded.summary,
4021
+ default_model=excluded.default_model, result_json=excluded.result_json, phases_json=excluded.phases_json,
4022
+ logs_json=excluded.logs_json, progress_json=excluded.progress_json, agent_count=excluded.agent_count,
4023
+ total_tokens=excluded.total_tokens, total_tool_calls=excluded.total_tool_calls,
4024
+ duration_ms=excluded.duration_ms, started_at=excluded.started_at, ended_at=excluded.ended_at,
4025
+ ingested_at=excluded.ingested_at`);
4026
+ ingestSidecars(db, sourceArchiveDir, excludedDirs, now2, stats, full, {
4027
+ subdir: "workflows",
4028
+ // Only the run's own result file (wf_<id>.json); agent-*.meta.json / scripts are handled elsewhere.
4029
+ matchName: (name) => /^wf_.+\.json$/.test(name),
4030
+ handle: (path3, buf) => {
4031
+ let d;
4032
+ try {
4033
+ const parsed = JSON.parse(buf.toString("utf8"));
4034
+ if (!parsed || typeof parsed !== "object")
4035
+ return false;
4036
+ d = parsed;
4037
+ } catch {
4038
+ return false;
4039
+ }
4040
+ const runId = strOrNull(d.runId) ?? basename3(path3, ".json");
4041
+ const startedAt = toIso(d.startTime) ?? toIso(d.timestamp);
4042
+ const durationMs2 = intOrNull(d.durationMs);
4043
+ const endedAt = startedAt && durationMs2 != null ? new Date(Date.parse(startedAt) + durationMs2).toISOString() : null;
4044
+ upsert.run({
4045
+ run_id: runId,
4046
+ source_id: sourceId,
4047
+ session_id: sessionBefore(path3, "workflows"),
4048
+ task_id: strOrNull(d.taskId),
4049
+ workflow_name: strOrNull(d.workflowName),
4050
+ status: strOrNull(d.status),
4051
+ summary: strOrNull(d.summary),
4052
+ default_model: cleanStr(d.defaultModel),
4053
+ result_json: jsonOrNull(d.result),
4054
+ phases_json: Array.isArray(d.phases) ? JSON.stringify(d.phases) : null,
4055
+ logs_json: Array.isArray(d.logs) ? JSON.stringify(d.logs) : null,
4056
+ progress_json: Array.isArray(d.workflowProgress) ? JSON.stringify(d.workflowProgress) : null,
4057
+ agent_count: intOrNull(d.agentCount),
4058
+ total_tokens: intOrNull(d.totalTokens),
4059
+ total_tool_calls: intOrNull(d.totalToolCalls),
4060
+ duration_ms: durationMs2,
4061
+ started_at: startedAt,
4062
+ ended_at: endedAt,
4063
+ ingested_at: now2
4064
+ });
4065
+ return true;
4149
4066
  }
4150
- if (!d || typeof d !== "object") {
4151
- stats.malformed++;
4152
- continue;
4067
+ });
4068
+ }
4069
+
4070
+ // ../ingest/dist/meta.js
4071
+ import { basename as basename4 } from "path";
4072
+ var newMetaStats = newSidecarStats;
4073
+ function ingestSubagentMeta(db, sourceArchiveDir, sourceId, excludedDirs, now2, stats, full) {
4074
+ const upsert = db.prepare(`INSERT INTO session_meta
4075
+ (session_id, source_id, agent_type, agent_description, spawn_depth, tool_use_id, ingested_at)
4076
+ VALUES
4077
+ (@session_id, @source_id, @agent_type, @agent_description, @spawn_depth, @tool_use_id, @ingested_at)
4078
+ ON CONFLICT(session_id) DO UPDATE SET
4079
+ source_id=excluded.source_id, agent_type=excluded.agent_type,
4080
+ agent_description=excluded.agent_description, spawn_depth=excluded.spawn_depth,
4081
+ tool_use_id=excluded.tool_use_id, ingested_at=excluded.ingested_at`);
4082
+ ingestSidecars(db, sourceArchiveDir, excludedDirs, now2, stats, full, {
4083
+ subdir: "subagents",
4084
+ matchName: (name) => /^agent-.+\.meta\.json$/.test(name),
4085
+ handle: (path3, buf) => {
4086
+ let d;
4087
+ try {
4088
+ const parsed = JSON.parse(buf.toString("utf8"));
4089
+ if (!parsed || typeof parsed !== "object")
4090
+ return false;
4091
+ d = parsed;
4092
+ } catch {
4093
+ return false;
4094
+ }
4095
+ upsert.run({
4096
+ session_id: basename4(path3, ".meta.json"),
4097
+ source_id: sourceId,
4098
+ agent_type: strOrNull(d.agentType),
4099
+ agent_description: strOrNull(d.description),
4100
+ spawn_depth: intOrNull(d.spawnDepth),
4101
+ tool_use_id: strOrNull(d.toolUseId),
4102
+ ingested_at: now2
4103
+ });
4104
+ return true;
4153
4105
  }
4154
- const sessionId = basename4(path3, ".meta.json");
4155
- upsert.run({
4156
- session_id: sessionId,
4157
- source_id: sourceId,
4158
- agent_type: strOrNull2(d.agentType),
4159
- agent_description: strOrNull2(d.description),
4160
- spawn_depth: intOrNull2(d.spawnDepth),
4161
- tool_use_id: strOrNull2(d.toolUseId),
4162
- ingested_at: now2
4163
- });
4164
- setState.run({ file_path: path3, size: st.size, mtime_ms: mtimeMs, sha256: hash2, ingested_at: now2 });
4165
- stats.upserted++;
4166
- }
4106
+ });
4167
4107
  }
4168
4108
 
4169
4109
  // ../ingest/dist/toolResults.js
4170
- import { readdirSync as readdirSync5, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
4171
- import { basename as basename5, join as join9 } from "path";
4172
- function newToolResultStats() {
4173
- return { files: 0, upserted: 0, skipped: 0, malformed: 0 };
4174
- }
4175
- function walkResults(dir, inResults, out) {
4176
- let entries;
4177
- try {
4178
- entries = readdirSync5(dir, { withFileTypes: true });
4179
- } catch {
4180
- return;
4181
- }
4182
- for (const e of entries) {
4183
- const p = join9(dir, e.name);
4184
- if (e.isDirectory())
4185
- walkResults(p, inResults || e.name === "tool-results", out);
4186
- else if (e.isFile() && inResults && e.name.endsWith(".txt"))
4187
- out.push(p);
4188
- }
4189
- }
4190
- function discoverResults(sourceArchiveDir) {
4191
- const out = [];
4192
- walkResults(join9(sourceArchiveDir, "projects"), false, out);
4193
- try {
4194
- for (const ts of readdirSync5(join9(sourceArchiveDir, ".versions"), { withFileTypes: true }))
4195
- if (ts.isDirectory())
4196
- walkResults(join9(sourceArchiveDir, ts.name, "projects"), false, out);
4197
- } catch {
4198
- }
4199
- return out;
4200
- }
4201
- function sessionFromPath2(path3) {
4202
- const parts = path3.split("/");
4203
- const i = parts.indexOf("tool-results");
4204
- return i > 0 ? parts[i - 1] : null;
4205
- }
4110
+ import { basename as basename5 } from "path";
4111
+ var newToolResultStats = newSidecarStats;
4206
4112
  function ingestToolResults(db, sourceArchiveDir, excludedDirs, now2, stats, full) {
4207
- let paths = discoverResults(sourceArchiveDir);
4208
- if (excludedDirs.length)
4209
- paths = paths.filter((p) => !isExcludedArchivePath(p, excludedDirs));
4210
- paths.sort((a, b) => Number(a.includes("/.versions/")) - Number(b.includes("/.versions/")) || (a < b ? -1 : 1));
4211
- const getState = db.prepare("SELECT size, mtime_ms, sha256 FROM ingest_state WHERE file_path = ?");
4212
- const setState = db.prepare(`INSERT INTO ingest_state (file_path, size, mtime_ms, sha256, events_ingested, ingested_at)
4213
- VALUES (@file_path, @size, @mtime_ms, @sha256, 0, @ingested_at)
4214
- ON CONFLICT(file_path) DO UPDATE SET size=excluded.size, mtime_ms=excluded.mtime_ms, sha256=excluded.sha256, ingested_at=excluded.ingested_at`);
4215
4113
  const upsert = db.prepare(`INSERT INTO tool_results (session_id, name, path, bytes, text, ingested_at)
4216
4114
  VALUES (@session_id, @name, @path, @bytes, @text, @ingested_at)
4217
4115
  ON CONFLICT(session_id, name) DO UPDATE SET
4218
4116
  path=excluded.path, bytes=excluded.bytes, text=excluded.text, ingested_at=excluded.ingested_at`);
4219
- for (const path3 of paths) {
4220
- const sessionId = sessionFromPath2(path3);
4221
- if (!sessionId)
4222
- continue;
4223
- stats.files++;
4224
- const st = statSync4(path3);
4225
- const mtimeMs = Math.trunc(st.mtimeMs);
4226
- const prev = full ? void 0 : getState.get(path3);
4227
- if (prev && prev.size === st.size && prev.mtime_ms === mtimeMs) {
4228
- stats.skipped++;
4229
- continue;
4230
- }
4231
- let buf;
4232
- try {
4233
- buf = readFileSync6(path3);
4234
- } catch {
4235
- stats.malformed++;
4236
- continue;
4237
- }
4238
- const hash2 = sha256(buf);
4239
- if (prev && prev.sha256 === hash2) {
4240
- setState.run({ file_path: path3, size: st.size, mtime_ms: mtimeMs, sha256: hash2, ingested_at: now2 });
4241
- stats.skipped++;
4242
- continue;
4117
+ ingestSidecars(db, sourceArchiveDir, excludedDirs, now2, stats, full, {
4118
+ subdir: "tool-results",
4119
+ matchName: (name) => name.endsWith(".txt"),
4120
+ // Skip a file outside the expected …/<sessionId>/tool-results/ layout (no files++, matching the
4121
+ // original early-continue before statting).
4122
+ include: (path3) => sessionBefore(path3, "tool-results") !== null,
4123
+ handle: (path3, buf, size) => {
4124
+ const sessionId = sessionBefore(path3, "tool-results");
4125
+ upsert.run({
4126
+ session_id: sessionId,
4127
+ name: basename5(path3, ".txt"),
4128
+ path: path3,
4129
+ bytes: size,
4130
+ text: buf.toString("utf8"),
4131
+ ingested_at: now2
4132
+ });
4133
+ return true;
4243
4134
  }
4244
- upsert.run({
4245
- session_id: sessionId,
4246
- name: basename5(path3, ".txt"),
4247
- path: path3,
4248
- bytes: st.size,
4249
- text: buf.toString("utf8"),
4250
- ingested_at: now2
4251
- });
4252
- setState.run({ file_path: path3, size: st.size, mtime_ms: mtimeMs, sha256: hash2, ingested_at: now2 });
4253
- stats.upserted++;
4254
- }
4135
+ });
4255
4136
  }
4256
4137
 
4257
4138
  // ../ingest/dist/run.js
@@ -4267,6 +4148,45 @@ function parseIngestArgs(argv) {
4267
4148
  }
4268
4149
  return a;
4269
4150
  }
4151
+ function shouldReingest(path3, size, mtimeMs, prev) {
4152
+ if (prev && prev.size === size && prev.mtime_ms === mtimeMs)
4153
+ return { action: "skip-stat" };
4154
+ const small = size <= STREAM_THRESHOLD;
4155
+ const buf = small ? readFileSync5(path3) : null;
4156
+ const hash2 = small ? sha256(buf) : sha256File(path3);
4157
+ if (prev && prev.sha256 === hash2)
4158
+ return { action: "skip-hash", hash: hash2 };
4159
+ return { action: "ingest", hash: hash2, buf };
4160
+ }
4161
+ function printIngestReport(db, dbPath, r) {
4162
+ const count = (sql) => db.prepare(sql).get().n;
4163
+ const sessions = count("SELECT COUNT(*) n FROM sessions");
4164
+ const turns = count("SELECT COUNT(*) n FROM turns");
4165
+ const events = count("SELECT COUNT(*) n FROM events");
4166
+ const tools = count("SELECT COUNT(*) n FROM tool_calls");
4167
+ const usageRows = db.prepare("SELECT model, input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens FROM token_usage").all();
4168
+ let cost = 0;
4169
+ let totalTokens = 0;
4170
+ for (const u of usageRows) {
4171
+ cost += costForUsage(u.model, u);
4172
+ totalTokens += u.input_tokens + u.output_tokens + u.cache_creation_input_tokens + u.cache_read_input_tokens;
4173
+ }
4174
+ const wfRuns = count("SELECT COUNT(*) n FROM workflow_results");
4175
+ const metaRows = count("SELECT COUNT(*) n FROM session_meta");
4176
+ const trRows = count("SELECT COUNT(*) n FROM tool_results");
4177
+ const sevCounts = db.prepare("SELECT severity, COUNT(*) n FROM findings GROUP BY severity").all();
4178
+ const sevMap = Object.fromEntries(sevCounts.map((s) => [s.severity, s.n]));
4179
+ const sevLine = ["critical", "high", "medium", "low", "info"].map((s) => `${s}=${sevMap[s] ?? 0}`).join(" ");
4180
+ db.close();
4181
+ console.log(`agent-lens-ingest: files=${r.stats.files} skipped=${r.stats.skipped} new_events=${r.stats.newEvents} malformed=${r.stats.malformed}${r.pruned ? ` excluded_pruned=${r.pruned}` : ""}
4182
+ sessions=${sessions} turns=${turns} events=${events} tool_calls=${tools} classified=${r.classified.count}
4183
+ workflow_results=${wfRuns} (sidecar upserted=${r.wfStats.upserted} skipped=${r.wfStats.skipped} malformed=${r.wfStats.malformed})
4184
+ session_meta=${metaRows} (upserted=${r.metaStats.upserted} skipped=${r.metaStats.skipped} malformed=${r.metaStats.malformed})
4185
+ tool_results=${trRows} (upserted=${r.trStats.upserted} skipped=${r.trStats.skipped} malformed=${r.trStats.malformed})
4186
+ findings=${r.detected.count} (${sevLine})
4187
+ file_changes=${r.fileChanges.count}
4188
+ tokens=${totalTokens.toLocaleString()} est_cost=$${cost.toFixed(2)} db=${dbPath}`);
4189
+ }
4270
4190
  function runIngest(argv = process.argv.slice(2)) {
4271
4191
  const args = parseIngestArgs(argv);
4272
4192
  const archiveRoot = args.archive || resolveArchiveDir();
@@ -4308,41 +4228,39 @@ function runIngest(argv = process.argv.slice(2)) {
4308
4228
  }
4309
4229
  stmts.insAgent.run(adapter.agentId, adapter.agentName);
4310
4230
  stmts.insSource.run({ id: source.label, label: source.label, agent_id: adapter.agentId, config_dir: source.configDir });
4311
- let files = adapter.discover(join10(archiveRoot, source.label), source.label);
4231
+ let files = adapter.discover(join8(archiveRoot, source.label), source.label);
4312
4232
  if (excludedDirs.length)
4313
4233
  files = files.filter((f) => !isExcludedArchivePath(f.path, excludedDirs));
4314
4234
  files.sort((a, b) => Number(a.isVersion) - Number(b.isVersion));
4315
4235
  for (const file of files) {
4316
4236
  stats.files++;
4317
- const st = statSync5(file.path);
4237
+ const st = statSync3(file.path);
4318
4238
  const mtimeMs = Math.trunc(st.mtimeMs);
4319
4239
  const prev = args.full ? void 0 : stmts.getState.get(file.path);
4320
- if (prev && prev.size === st.size && prev.mtime_ms === mtimeMs) {
4240
+ const decision = shouldReingest(file.path, st.size, mtimeMs, prev);
4241
+ if (decision.action === "skip-stat") {
4321
4242
  stats.skipped++;
4322
4243
  continue;
4323
4244
  }
4324
- const small = st.size <= STREAM_THRESHOLD;
4325
- const buf = small ? readFileSync7(file.path) : null;
4326
- const hash2 = small ? sha256(buf) : sha256File(file.path);
4327
- if (prev && prev.sha256 === hash2) {
4245
+ if (decision.action === "skip-hash") {
4328
4246
  stmts.setState.run({
4329
4247
  file_path: file.path,
4330
4248
  size: st.size,
4331
4249
  mtime_ms: mtimeMs,
4332
- sha256: hash2,
4250
+ sha256: decision.hash,
4333
4251
  events_ingested: prev.events_ingested,
4334
4252
  ingested_at: now2
4335
4253
  });
4336
4254
  stats.skipped++;
4337
4255
  continue;
4338
4256
  }
4339
- const lines = small ? buf.toString("utf8").split("\n") : streamLines(file.path);
4340
- ingestFile(db, stmts, adapter, file, lines, { size: st.size, mtimeMs, hash: hash2 }, now2, stats);
4257
+ const lines = decision.buf ? decision.buf.toString("utf8").split("\n") : streamLines(file.path);
4258
+ ingestFile(db, stmts, adapter, file, lines, { size: st.size, mtimeMs, hash: decision.hash }, now2, stats);
4341
4259
  dirty.add(file.sessionId);
4342
4260
  }
4343
- ingestWorkflowResults(db, join10(archiveRoot, source.label), source.label, excludedDirs, now2, wfStats, args.full);
4344
- ingestSubagentMeta(db, join10(archiveRoot, source.label), source.label, excludedDirs, now2, metaStats, args.full);
4345
- ingestToolResults(db, join10(archiveRoot, source.label), excludedDirs, now2, trStats, args.full);
4261
+ ingestWorkflowResults(db, join8(archiveRoot, source.label), source.label, excludedDirs, now2, wfStats, args.full);
4262
+ ingestSubagentMeta(db, join8(archiveRoot, source.label), source.label, excludedDirs, now2, metaStats, args.full);
4263
+ ingestToolResults(db, join8(archiveRoot, source.label), excludedDirs, now2, trStats, args.full);
4346
4264
  }
4347
4265
  const expanded = rebuildDerived(db, args.full ? null : dirty);
4348
4266
  canonicalizeProjects(db);
@@ -4350,33 +4268,7 @@ function runIngest(argv = process.argv.slice(2)) {
4350
4268
  const detected = detect(db, args.full ? null : expanded);
4351
4269
  classifyErrors(db, args.full ? null : expanded);
4352
4270
  const fileChanges = deriveFileChanges(db, args.full ? null : expanded);
4353
- const count = (sql) => db.prepare(sql).get().n;
4354
- const sessions = count("SELECT COUNT(*) n FROM sessions");
4355
- const turns = count("SELECT COUNT(*) n FROM turns");
4356
- const events = count("SELECT COUNT(*) n FROM events");
4357
- const tools = count("SELECT COUNT(*) n FROM tool_calls");
4358
- const usageRows = db.prepare("SELECT model, input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens FROM token_usage").all();
4359
- let cost = 0;
4360
- let totalTokens = 0;
4361
- for (const u of usageRows) {
4362
- cost += costForUsage(u.model, u);
4363
- totalTokens += u.input_tokens + u.output_tokens + u.cache_creation_input_tokens + u.cache_read_input_tokens;
4364
- }
4365
- const wfRuns = count("SELECT COUNT(*) n FROM workflow_results");
4366
- const metaRows = count("SELECT COUNT(*) n FROM session_meta");
4367
- const trRows = count("SELECT COUNT(*) n FROM tool_results");
4368
- const sevCounts = db.prepare("SELECT severity, COUNT(*) n FROM findings GROUP BY severity").all();
4369
- const sevMap = Object.fromEntries(sevCounts.map((r) => [r.severity, r.n]));
4370
- const sevLine = ["critical", "high", "medium", "low", "info"].map((s) => `${s}=${sevMap[s] ?? 0}`).join(" ");
4371
- db.close();
4372
- console.log(`agent-lens-ingest: files=${stats.files} skipped=${stats.skipped} new_events=${stats.newEvents} malformed=${stats.malformed}${pruned ? ` excluded_pruned=${pruned}` : ""}
4373
- sessions=${sessions} turns=${turns} events=${events} tool_calls=${tools} classified=${classified.count}
4374
- workflow_results=${wfRuns} (sidecar upserted=${wfStats.upserted} skipped=${wfStats.skipped} malformed=${wfStats.malformed})
4375
- session_meta=${metaRows} (upserted=${metaStats.upserted} skipped=${metaStats.skipped} malformed=${metaStats.malformed})
4376
- tool_results=${trRows} (upserted=${trStats.upserted} skipped=${trStats.skipped} malformed=${trStats.malformed})
4377
- findings=${detected.count} (${sevLine})
4378
- file_changes=${fileChanges.count}
4379
- tokens=${totalTokens.toLocaleString()} est_cost=$${cost.toFixed(2)} db=${dbPath}`);
4271
+ printIngestReport(db, dbPath, { stats, pruned, classified, detected, fileChanges, wfStats, metaStats, trStats });
4380
4272
  }
4381
4273
  function runMetrics(argv = process.argv.slice(2)) {
4382
4274
  let dbArg = "";
@@ -4405,6 +4297,74 @@ import { fileURLToPath as fileURLToPath2 } from "url";
4405
4297
 
4406
4298
  // ../server/dist/db.js
4407
4299
  import Database2 from "better-sqlite3";
4300
+
4301
+ // ../server/dist/sql-util.js
4302
+ function tableExists(db, name) {
4303
+ return !!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name = ?").get(name);
4304
+ }
4305
+ function queryAll(db, sql, ...params) {
4306
+ return db.prepare(sql).all(...params);
4307
+ }
4308
+ function pushDateRange(clauses, params, col, from, to) {
4309
+ if (from) {
4310
+ clauses.push(`date(${col}) >= date(?)`);
4311
+ params.push(from);
4312
+ }
4313
+ if (to) {
4314
+ clauses.push(`date(${col}) <= date(?)`);
4315
+ params.push(to);
4316
+ }
4317
+ }
4318
+ function metaJoin(hasMeta) {
4319
+ return hasMeta ? "LEFT JOIN session_meta sm ON sm.session_id = s.id" : "";
4320
+ }
4321
+ function metaProjection(hasMeta) {
4322
+ return hasMeta ? ", sm.agent_type, sm.agent_description, sm.spawn_depth" : ", NULL AS agent_type, NULL AS agent_description, NULL AS spawn_depth";
4323
+ }
4324
+ function orderBy(columns, sort, fallback, dir) {
4325
+ const col = sort !== void 0 && Object.hasOwn(columns, sort) ? columns[sort] : columns[fallback];
4326
+ return `${col} ${dir === "asc" ? "ASC" : "DESC"}`;
4327
+ }
4328
+ function pushGrouped(map, key, value) {
4329
+ const list = map.get(key);
4330
+ if (list)
4331
+ list.push(value);
4332
+ else
4333
+ map.set(key, [value]);
4334
+ }
4335
+
4336
+ // ../server/dist/usage.js
4337
+ var USAGE_SUMS = "SUM(input_tokens) i, SUM(output_tokens) o, SUM(cache_creation_input_tokens) cw, SUM(cache_read_input_tokens) cr";
4338
+ function sessionUsage(db, sessionId) {
4339
+ return queryAll(db, `SELECT model, ${USAGE_SUMS} FROM token_usage WHERE session_id = ? GROUP BY model`, sessionId);
4340
+ }
4341
+ function splitOf(rows) {
4342
+ const split = { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
4343
+ for (const u of rows) {
4344
+ split.input += u.i;
4345
+ split.output += u.o;
4346
+ split.cache_creation += u.cw;
4347
+ split.cache_read += u.cr;
4348
+ }
4349
+ return split;
4350
+ }
4351
+ function tokensOf(split) {
4352
+ return split.input + split.output + split.cache_creation + split.cache_read;
4353
+ }
4354
+ function costOf(rows) {
4355
+ let cost = 0;
4356
+ for (const u of rows) {
4357
+ cost += costForUsage(u.model, {
4358
+ input_tokens: u.i,
4359
+ output_tokens: u.o,
4360
+ cache_creation_input_tokens: u.cw,
4361
+ cache_read_input_tokens: u.cr
4362
+ });
4363
+ }
4364
+ return Number(cost.toFixed(4));
4365
+ }
4366
+
4367
+ // ../server/dist/db.js
4408
4368
  function openReadonly(file) {
4409
4369
  const db = new Database2(file, { readonly: true, fileMustExist: true });
4410
4370
  db.pragma("query_only = ON");
@@ -4467,10 +4427,7 @@ function listSessions(db, f) {
4467
4427
  where.push("s.source_id = ?"), params.push(f.source);
4468
4428
  if (f.project)
4469
4429
  where.push("s.project_id = ?"), params.push(f.project);
4470
- if (f.from)
4471
- where.push("date(s.started_at) >= date(?)"), params.push(f.from);
4472
- if (f.to)
4473
- where.push("date(s.started_at) <= date(?)"), params.push(f.to);
4430
+ pushDateRange(where, params, "s.started_at", f.from, f.to);
4474
4431
  if (f.kind === "main")
4475
4432
  where.push("s.is_sidechain = 0");
4476
4433
  if (f.kind === "subagent")
@@ -4514,7 +4471,6 @@ function listSessions(db, f) {
4514
4471
  duration: "s.duration_ms"
4515
4472
  };
4516
4473
  const sortKey = f.sort ?? "started";
4517
- const sqlDir = f.dir === "asc" ? "ASC" : "DESC";
4518
4474
  const JS_SORT = /* @__PURE__ */ new Set(["tokens", "cost", "errors", "security"]);
4519
4475
  let rows;
4520
4476
  if (JS_SORT.has(sortKey)) {
@@ -4542,55 +4498,35 @@ function listSessions(db, f) {
4542
4498
  });
4543
4499
  rows = all.slice(f.offset, f.offset + f.limit);
4544
4500
  } else {
4545
- const orderCol = NATIVE_ORDER[sortKey] ?? NATIVE_ORDER.started;
4546
- rows = db.prepare(`${baseSelect} ORDER BY ${orderCol} ${sqlDir}, s.id ASC LIMIT ? OFFSET ?`).all(...params, f.limit, f.offset);
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);
4547
4502
  attachSessionCost(db, rows);
4548
4503
  }
4549
4504
  return { total, sessions: rows };
4550
4505
  }
4551
4506
  function attachSessionCost(db, rows) {
4552
- const costBySession = /* @__PURE__ */ new Map();
4507
+ const usageBySession = /* @__PURE__ */ new Map();
4553
4508
  const ids = rows.map((r) => r.id);
4554
4509
  const CHUNK3 = 800;
4555
4510
  for (let i = 0; i < ids.length; i += CHUNK3) {
4556
4511
  const batch = ids.slice(i, i + CHUNK3);
4557
4512
  const ph = batch.map(() => "?").join(",");
4558
- const usage = db.prepare(`SELECT session_id, model,
4559
- SUM(input_tokens) i, SUM(output_tokens) o,
4560
- SUM(cache_creation_input_tokens) cw, SUM(cache_read_input_tokens) cr
4561
- FROM token_usage WHERE session_id IN (${ph}) GROUP BY session_id, model`).all(...batch);
4513
+ const usage = queryAll(db, `SELECT session_id, model, ${USAGE_SUMS} FROM token_usage WHERE session_id IN (${ph}) GROUP BY session_id, model`, ...batch);
4562
4514
  for (const u of usage) {
4563
- const acc = costBySession.get(u.session_id) ?? { tokens: 0, cost: 0, split: { input: 0, output: 0, cache_creation: 0, cache_read: 0 } };
4564
- acc.tokens += u.i + u.o + u.cw + u.cr;
4565
- acc.split.input += u.i;
4566
- acc.split.output += u.o;
4567
- acc.split.cache_creation += u.cw;
4568
- acc.split.cache_read += u.cr;
4569
- acc.cost += costForUsage(u.model, {
4570
- input_tokens: u.i,
4571
- output_tokens: u.o,
4572
- cache_creation_input_tokens: u.cw,
4573
- cache_read_input_tokens: u.cr
4574
- });
4575
- costBySession.set(u.session_id, acc);
4515
+ const list = usageBySession.get(u.session_id) ?? [];
4516
+ list.push(u);
4517
+ usageBySession.set(u.session_id, list);
4576
4518
  }
4577
4519
  }
4578
4520
  for (const r of rows) {
4579
- const c = costBySession.get(r.id);
4580
- r.tokens = c?.tokens ?? 0;
4581
- r.token_split = c?.split ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
4582
- r.cost = c ? Number(c.cost.toFixed(4)) : 0;
4521
+ const u = usageBySession.get(r.id) ?? [];
4522
+ const split = splitOf(u);
4523
+ r.tokens = tokensOf(split);
4524
+ r.token_split = split;
4525
+ r.cost = costOf(u);
4583
4526
  r.title = r.ai_title || r.slug || null;
4584
4527
  }
4585
4528
  }
4586
- function getSession(db, id) {
4587
- const session = db.prepare(`SELECT s.*, p.path AS project_path FROM sessions s
4588
- LEFT JOIN projects p ON p.id = s.project_id WHERE s.id = ?`).get(id);
4589
- if (!session)
4590
- return null;
4591
- const turns = db.prepare("SELECT * FROM turns WHERE session_id = ? ORDER BY seq").all(id);
4592
- const eventRows = db.prepare(`SELECT uuid, type, role, timestamp, model, is_sidechain, turn_id, raw_json
4593
- FROM events WHERE session_id = ? ORDER BY timestamp, seq`).all(id);
4529
+ function loadToolCalls(db, id) {
4594
4530
  const toolRows = db.prepare(`SELECT id, event_uuid, tool_name, skill_name, skill_id, agent_type, spawned_session_id,
4595
4531
  workflow_run_id, workflow_name, status, error_type,
4596
4532
  total_duration_ms, total_tokens, input_json, result_summary,
@@ -4607,40 +4543,48 @@ function getSession(db, id) {
4607
4543
  t.full_result = { text: full.text, bytes: full.bytes };
4608
4544
  }
4609
4545
  }
4546
+ return toolRows;
4547
+ }
4548
+ function loadFindings(db, id, toolRows) {
4549
+ if (!tableExists(db, "findings"))
4550
+ return [];
4610
4551
  const sessionFindings = [];
4611
- if (tableExists(db, "findings")) {
4612
- const findingRows = db.prepare(`SELECT id, tool_call_id, event_uuid, turn_id, rule_id, category, framework_ref, severity, title, evidence, signals_json
4613
- FROM findings WHERE session_id = ?`).all(id);
4614
- const byToolCall = /* @__PURE__ */ new Map();
4615
- for (const fr of findingRows) {
4616
- const f = {
4617
- id: fr.id,
4618
- tool_call_id: fr.tool_call_id,
4619
- event_uuid: fr.event_uuid,
4620
- turn_id: fr.turn_id,
4621
- rule_id: fr.rule_id,
4622
- category: fr.category,
4623
- framework_ref: fr.framework_ref,
4624
- severity: fr.severity,
4625
- title: fr.title,
4626
- evidence: fr.evidence,
4627
- signals: safeJson(fr.signals_json)
4628
- };
4629
- sessionFindings.push(f);
4630
- if (fr.tool_call_id)
4631
- (byToolCall.get(fr.tool_call_id) ?? byToolCall.set(fr.tool_call_id, []).get(fr.tool_call_id)).push(f);
4632
- }
4633
- for (const t of toolRows)
4634
- t.findings = byToolCall.get(t.id) ?? [];
4635
- sessionFindings.sort((a, b) => severityRank(b.severity) - severityRank(a.severity));
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);
4554
+ const byToolCall = /* @__PURE__ */ new Map();
4555
+ for (const fr of findingRows) {
4556
+ const f = {
4557
+ id: fr.id,
4558
+ tool_call_id: fr.tool_call_id,
4559
+ event_uuid: fr.event_uuid,
4560
+ turn_id: fr.turn_id,
4561
+ rule_id: fr.rule_id,
4562
+ category: fr.category,
4563
+ framework_ref: fr.framework_ref,
4564
+ severity: fr.severity,
4565
+ title: fr.title,
4566
+ evidence: fr.evidence,
4567
+ signals: safeJson(fr.signals_json)
4568
+ };
4569
+ sessionFindings.push(f);
4570
+ if (fr.tool_call_id)
4571
+ pushGrouped(byToolCall, fr.tool_call_id, f);
4636
4572
  }
4573
+ for (const t of toolRows)
4574
+ t.findings = byToolCall.get(t.id) ?? [];
4575
+ sessionFindings.sort((a, b) => severityRank(b.severity) - severityRank(a.severity));
4576
+ return sessionFindings;
4577
+ }
4578
+ 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);
4637
4581
  const toolsByEvent = /* @__PURE__ */ new Map();
4638
4582
  for (const t of toolRows) {
4639
4583
  if (!t.event_uuid)
4640
4584
  continue;
4641
- (toolsByEvent.get(t.event_uuid) ?? toolsByEvent.set(t.event_uuid, []).get(t.event_uuid)).push(t);
4585
+ pushGrouped(toolsByEvent, t.event_uuid, t);
4642
4586
  }
4643
- const events = eventRows.map((e) => {
4587
+ return eventRows.map((e) => {
4644
4588
  const { text, thinking } = extractParts(e.raw_json);
4645
4589
  return {
4646
4590
  uuid: e.uuid,
@@ -4655,91 +4599,101 @@ function getSession(db, id) {
4655
4599
  toolCalls: toolsByEvent.get(e.uuid) ?? []
4656
4600
  };
4657
4601
  });
4658
- const usage = db.prepare(`SELECT model, SUM(input_tokens) i, SUM(output_tokens) o,
4659
- SUM(cache_creation_input_tokens) cw, SUM(cache_read_input_tokens) cr
4660
- FROM token_usage WHERE session_id = ? GROUP BY model`).all(id);
4661
- const split = { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
4662
- let cost = 0;
4663
- for (const u of usage) {
4664
- split.input += u.i;
4665
- split.output += u.o;
4666
- split.cache_creation += u.cw;
4667
- split.cache_read += u.cr;
4668
- cost += costForUsage(u.model, {
4669
- input_tokens: u.i,
4670
- output_tokens: u.o,
4671
- cache_creation_input_tokens: u.cw,
4672
- cache_read_input_tokens: u.cr
4673
- });
4674
- }
4675
- session.tokens = split.input + split.output + split.cache_creation + split.cache_read;
4676
- session.token_split = split;
4677
- session.cost = Number(cost.toFixed(4));
4678
- session.title = session.ai_title || session.slug || null;
4602
+ }
4603
+ function loadClassification(db, id) {
4679
4604
  const cls = db.prepare(`SELECT category, complexity_score, complexity_band, signals_json, classifier_version
4680
4605
  FROM classifications WHERE scope = 'session' AND target_id = ?`).get(id);
4681
- let classification = null;
4682
- if (cls) {
4683
- let signals = null;
4684
- try {
4685
- signals = cls.signals_json ? JSON.parse(cls.signals_json) : null;
4686
- } catch {
4687
- }
4688
- classification = {
4689
- category: cls.category,
4690
- complexity_score: cls.complexity_score,
4691
- complexity_band: cls.complexity_band,
4692
- classifier_version: cls.classifier_version,
4693
- signals
4694
- };
4695
- }
4696
- let parent = null;
4697
- if (session.parent_session_id) {
4698
- const p = db.prepare(`SELECT ps.id, ps.ai_title, ps.slug, t.seq AS turn_seq
4699
- FROM sessions ps LEFT JOIN turns t ON t.id = ?
4700
- WHERE ps.id = ?`).get(session.parent_turn_id, session.parent_session_id);
4701
- if (p)
4702
- parent = { id: p.id, title: p.ai_title || p.slug || null, turn_seq: p.turn_seq ?? null };
4606
+ if (!cls)
4607
+ return null;
4608
+ let signals = null;
4609
+ try {
4610
+ signals = cls.signals_json ? JSON.parse(cls.signals_json) : null;
4611
+ } catch {
4703
4612
  }
4613
+ return {
4614
+ category: cls.category,
4615
+ complexity_score: cls.complexity_score,
4616
+ complexity_band: cls.complexity_band,
4617
+ classifier_version: cls.classifier_version,
4618
+ signals
4619
+ };
4620
+ }
4621
+ function loadParent(db, session) {
4622
+ if (!session.parent_session_id)
4623
+ 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);
4627
+ return p ? { id: p.id, title: p.ai_title || p.slug || null, turn_seq: p.turn_seq ?? null } : null;
4628
+ }
4629
+ function loadChildren(db, id) {
4704
4630
  const hasMeta = tableExists(db, "session_meta");
4705
4631
  const children = db.prepare(`SELECT s.id, s.ai_title, s.slug, s.turn_count, s.started_at, s.workflow_run_id,
4706
4632
  (SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models
4707
- ${hasMeta ? ", sm.agent_type, sm.agent_description, sm.spawn_depth" : ", NULL AS agent_type, NULL AS agent_description, NULL AS spawn_depth"}
4708
- FROM sessions s ${hasMeta ? "LEFT JOIN session_meta sm ON sm.session_id = s.id" : ""}
4633
+ ${metaProjection(hasMeta)}
4634
+ FROM sessions s ${metaJoin(hasMeta)}
4709
4635
  WHERE s.parent_session_id = ? ORDER BY s.started_at`).all(id);
4710
4636
  for (const ch of children) {
4711
4637
  ch.title = ch.ai_title || ch.slug || null;
4712
- const u = db.prepare(`SELECT model, SUM(input_tokens) i, SUM(output_tokens) o,
4713
- SUM(cache_creation_input_tokens) cw, SUM(cache_read_input_tokens) cr
4714
- FROM token_usage WHERE session_id = ? GROUP BY model`).all(ch.id);
4715
- ch.tokens = u.reduce((a, r) => a + r.i + r.o + r.cw + r.cr, 0);
4716
- ch.cost = Number(u.reduce((a, r) => a + costForUsage(r.model, { input_tokens: r.i, output_tokens: r.o, cache_creation_input_tokens: r.cw, cache_read_input_tokens: r.cr }), 0).toFixed(4));
4638
+ const u = sessionUsage(db, ch.id);
4639
+ ch.tokens = tokensOf(splitOf(u));
4640
+ ch.cost = costOf(u);
4717
4641
  }
4642
+ return children;
4643
+ }
4644
+ function loadWorkflowRuns(db, id) {
4718
4645
  const hasWR = tableExists(db, "workflow_results");
4719
- const workflow_runs = db.prepare(`SELECT tc.workflow_run_id AS run_id, tc.workflow_name AS name, t.seq AS turn_seq,
4646
+ return db.prepare(`SELECT tc.workflow_run_id AS run_id, tc.workflow_name AS name, t.seq AS turn_seq,
4720
4647
  (SELECT COUNT(*) FROM sessions s WHERE s.workflow_run_id = tc.workflow_run_id) AS agent_count
4721
4648
  ${hasWR ? ", (SELECT status FROM workflow_results wr WHERE wr.run_id = tc.workflow_run_id) AS status" : ", NULL AS status"}
4722
4649
  FROM tool_calls tc LEFT JOIN turns t ON t.id = tc.turn_id
4723
4650
  WHERE tc.session_id = ? AND tc.workflow_run_id IS NOT NULL
4724
4651
  ORDER BY t.seq`).all(id);
4652
+ }
4653
+ function loadFileChanges(db, id) {
4654
+ if (!tableExists(db, "file_changes"))
4655
+ 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);
4659
+ }
4660
+ function attachSessionTotals(db, session, id, toolRows) {
4661
+ const usage = sessionUsage(db, id);
4662
+ 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;
4725
4667
  session.tool_call_count = toolRows.length;
4726
4668
  session.tool_error_count = toolRows.filter((t) => t.status === "error").length;
4727
4669
  session.tool_rejection_count = toolRows.filter((t) => t.status === "error" && t.error_type && errorKind(t.error_type) === "rejection").length;
4728
4670
  session.tool_failure_count = session.tool_error_count - session.tool_rejection_count;
4729
- let fileChanges = [];
4730
- if (tableExists(db, "file_changes")) {
4731
- fileChanges = db.prepare(`SELECT fc.id, fc.tool_call_id, fc.turn_id, fc.event_uuid, fc.file_path, fc.tool_name,
4732
- fc.lines_added, fc.lines_removed, fc.timestamp
4733
- FROM file_changes fc WHERE fc.session_id = ? ORDER BY fc.timestamp`).all(id);
4734
- }
4735
- return { session, turns, events, classification, parent, children, workflow_runs, findings: sessionFindings, file_changes: fileChanges };
4671
+ }
4672
+ 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);
4675
+ if (!session)
4676
+ return null;
4677
+ const turns = db.prepare("SELECT * FROM turns WHERE session_id = ? ORDER BY seq").all(id);
4678
+ const toolRows = loadToolCalls(db, id);
4679
+ const findings = loadFindings(db, id, toolRows);
4680
+ const events = loadEvents(db, id, toolRows);
4681
+ attachSessionTotals(db, session, id, toolRows);
4682
+ return {
4683
+ session,
4684
+ turns,
4685
+ events,
4686
+ classification: loadClassification(db, id),
4687
+ parent: loadParent(db, session),
4688
+ children: loadChildren(db, id),
4689
+ workflow_runs: loadWorkflowRuns(db, id),
4690
+ findings,
4691
+ file_changes: loadFileChanges(db, id)
4692
+ };
4736
4693
  }
4737
4694
  function xmlTag(text, tag) {
4738
4695
  return text.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`))?.[1]?.trim() ?? null;
4739
4696
  }
4740
- function tableExists(db, name) {
4741
- return !!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name = ?").get(name);
4742
- }
4743
4697
  function safeJson(s) {
4744
4698
  if (!s)
4745
4699
  return null;
@@ -4749,24 +4703,12 @@ function safeJson(s) {
4749
4703
  return null;
4750
4704
  }
4751
4705
  }
4752
- function getWorkflow(db, runId) {
4753
- 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,
4754
- tc.input_json AS input_json,
4755
- tc.session_id AS parent_session_id, t.seq AS turn_seq,
4756
- ps.ai_title AS parent_ai_title, ps.slug AS parent_slug
4757
- FROM tool_calls tc
4758
- LEFT JOIN turns t ON t.id = tc.turn_id
4759
- LEFT JOIN sessions ps ON ps.id = tc.session_id
4760
- WHERE tc.workflow_run_id = ? AND tc.tool_name = 'Workflow'
4761
- ORDER BY t.seq
4762
- LIMIT 1`).get(runId);
4763
- if (!wf)
4764
- return null;
4706
+ function loadAgents(db, runId) {
4765
4707
  const hasMeta = tableExists(db, "session_meta");
4766
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,
4767
4709
  (SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models
4768
- ${hasMeta ? ", sm.agent_type, sm.agent_description, sm.spawn_depth" : ", NULL AS agent_type, NULL AS agent_description, NULL AS spawn_depth"}
4769
- FROM sessions s ${hasMeta ? "LEFT JOIN session_meta sm ON sm.session_id = s.id" : ""}
4710
+ ${metaProjection(hasMeta)}
4711
+ FROM sessions s ${metaJoin(hasMeta)}
4770
4712
  WHERE s.workflow_run_id = ? ORDER BY s.started_at`).all(runId);
4771
4713
  let total_tokens = 0;
4772
4714
  let total_cost = 0;
@@ -4774,11 +4716,9 @@ function getWorkflow(db, runId) {
4774
4716
  let ended_at = null;
4775
4717
  for (const a of agents) {
4776
4718
  a.title = a.ai_title || a.slug || null;
4777
- const u = db.prepare(`SELECT model, SUM(input_tokens) i, SUM(output_tokens) o,
4778
- SUM(cache_creation_input_tokens) cw, SUM(cache_read_input_tokens) cr
4779
- FROM token_usage WHERE session_id = ? GROUP BY model`).all(a.id);
4780
- a.tokens = u.reduce((acc, r) => acc + r.i + r.o + r.cw + r.cr, 0);
4781
- a.cost = Number(u.reduce((acc, r) => acc + costForUsage(r.model, { input_tokens: r.i, output_tokens: r.o, cache_creation_input_tokens: r.cw, cache_read_input_tokens: r.cr }), 0).toFixed(4));
4719
+ const u = sessionUsage(db, a.id);
4720
+ a.tokens = tokensOf(splitOf(u));
4721
+ a.cost = costOf(u);
4782
4722
  total_tokens += a.tokens;
4783
4723
  total_cost += a.cost;
4784
4724
  if (a.started_at && (!started_at || a.started_at < started_at))
@@ -4787,47 +4727,70 @@ function getWorkflow(db, runId) {
4787
4727
  ended_at = a.ended_at;
4788
4728
  }
4789
4729
  const duration_ms = started_at && ended_at ? new Date(ended_at).getTime() - new Date(started_at).getTime() : null;
4790
- let completion = null;
4791
- if (wf.tool_use_id && wf.parent_session_id) {
4792
- const ev = db.prepare(`SELECT raw_json FROM events
4793
- WHERE session_id = ? AND text LIKE '%<task-notification>%' AND text LIKE ?
4794
- ORDER BY timestamp DESC LIMIT 1`).get(wf.parent_session_id, `%${wf.tool_use_id}%`);
4795
- if (ev) {
4796
- const { text } = extractParts(ev.raw_json);
4797
- const t = text ?? "";
4798
- completion = { status: xmlTag(t, "status"), summary: xmlTag(t, "summary"), result: xmlTag(t, "result"), failures: xmlTag(t, "failures") };
4799
- }
4800
- }
4730
+ return {
4731
+ agents,
4732
+ stats: { agent_count: agents.length, total_tokens, total_cost: Number(total_cost.toFixed(4)), started_at, ended_at, duration_ms }
4733
+ };
4734
+ }
4735
+ function parseCompletion(db, wf) {
4736
+ if (!wf.tool_use_id || !wf.parent_session_id)
4737
+ 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}%`);
4741
+ if (!ev)
4742
+ return null;
4743
+ const { text } = extractParts(ev.raw_json);
4744
+ const t = text ?? "";
4745
+ return { status: xmlTag(t, "status"), summary: xmlTag(t, "summary"), result: xmlTag(t, "result"), failures: xmlTag(t, "failures") };
4746
+ }
4747
+ function loadRunSidecar(db, runId, completion) {
4801
4748
  const wr = tableExists(db, "workflow_results") ? db.prepare(`SELECT status, summary, default_model, result_json, phases_json, logs_json, progress_json,
4802
4749
  agent_count, total_tokens, total_tool_calls, duration_ms, started_at, ended_at
4803
- FROM workflow_results WHERE run_id = ?`).get(wf.run_id) : null;
4804
- let run = null;
4805
- if (wr) {
4806
- run = {
4807
- status: wr.status ?? null,
4808
- summary: wr.summary ?? null,
4809
- default_model: wr.default_model ?? null,
4810
- agent_count: wr.agent_count ?? null,
4811
- total_tokens: wr.total_tokens ?? null,
4812
- total_tool_calls: wr.total_tool_calls ?? null,
4813
- duration_ms: wr.duration_ms ?? null,
4814
- started_at: wr.started_at ?? null,
4815
- ended_at: wr.ended_at ?? null,
4816
- phases: safeJson(wr.phases_json),
4817
- logs: safeJson(wr.logs_json),
4818
- // The per-phase/per-agent event timeline (workflowProgress) — lets the page render a phase graph
4819
- // with a per-phase descriptor (agent count, models). Absent on older/failed runs → UI falls back.
4820
- progress: safeJson(wr.progress_json)
4750
+ FROM workflow_results WHERE run_id = ?`).get(runId) : null;
4751
+ if (!wr)
4752
+ return { wr: null, run: null, completion };
4753
+ const run = {
4754
+ status: wr.status ?? null,
4755
+ summary: wr.summary ?? null,
4756
+ default_model: wr.default_model ?? null,
4757
+ agent_count: wr.agent_count ?? null,
4758
+ total_tokens: wr.total_tokens ?? null,
4759
+ total_tool_calls: wr.total_tool_calls ?? null,
4760
+ duration_ms: wr.duration_ms ?? null,
4761
+ started_at: wr.started_at ?? null,
4762
+ ended_at: wr.ended_at ?? null,
4763
+ phases: safeJson(wr.phases_json),
4764
+ logs: safeJson(wr.logs_json),
4765
+ // The per-phase/per-agent event timeline (workflowProgress) — lets the page render a phase graph
4766
+ // with a per-phase descriptor (agent count, models). Absent on older/failed runs → UI falls back.
4767
+ progress: safeJson(wr.progress_json)
4768
+ };
4769
+ if (wr.result_json != null || wr.status) {
4770
+ completion = {
4771
+ status: wr.status ?? completion?.status ?? null,
4772
+ summary: wr.summary ?? completion?.summary ?? null,
4773
+ result: wr.result_json ?? completion?.result ?? null,
4774
+ failures: completion?.failures ?? null
4821
4775
  };
4822
- if (wr.result_json != null || wr.status) {
4823
- completion = {
4824
- status: wr.status ?? completion?.status ?? null,
4825
- summary: wr.summary ?? completion?.summary ?? null,
4826
- result: wr.result_json ?? completion?.result ?? null,
4827
- failures: completion?.failures ?? null
4828
- };
4829
- }
4830
4776
  }
4777
+ return { wr, run, completion };
4778
+ }
4779
+ 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);
4790
+ if (!wf)
4791
+ return null;
4792
+ const { agents, stats } = loadAgents(db, runId);
4793
+ const { wr, run, completion } = loadRunSidecar(db, wf.run_id, parseCompletion(db, wf));
4831
4794
  return {
4832
4795
  run_id: wf.run_id,
4833
4796
  name: wf.name ?? null,
@@ -4838,7 +4801,7 @@ function getWorkflow(db, runId) {
4838
4801
  run,
4839
4802
  parent: { id: wf.parent_session_id, title: wf.parent_ai_title || wf.parent_slug || null, turn_seq: wf.turn_seq ?? null },
4840
4803
  agents,
4841
- stats: { agent_count: agents.length, total_tokens, total_cost: Number(total_cost.toFixed(4)), started_at, ended_at, duration_ms }
4804
+ stats
4842
4805
  };
4843
4806
  }
4844
4807
  function listSkills(db, f = {}) {
@@ -4897,13 +4860,11 @@ function listFiles(db, f = {}) {
4897
4860
  sessions: "sessions",
4898
4861
  path: "file_path"
4899
4862
  };
4900
- const col = ORDER[f.sort ?? "last_ts"] ?? "last_ts";
4901
- const dir = f.dir === "asc" ? "ASC" : "DESC";
4902
4863
  const limit = Math.min(f.limit ?? 50, 200);
4903
4864
  const offset = f.offset ?? 0;
4904
4865
  const files = db.prepare(`SELECT g.*, p.path AS project_path FROM (${grouped}) g
4905
4866
  LEFT JOIN projects p ON p.id = g.project_id
4906
- ORDER BY ${col} ${dir}, g.file_path ASC LIMIT ? OFFSET ?`).all(...params, limit, offset);
4867
+ ORDER BY ${orderBy(ORDER, f.sort, "last_ts", f.dir)}, g.file_path ASC LIMIT ? OFFSET ?`).all(...params, limit, offset);
4907
4868
  return { total, files };
4908
4869
  }
4909
4870
  function getFileTimeline(db, filePath, projectId2) {
@@ -5010,10 +4971,7 @@ function findingWhere(f) {
5010
4971
  sql.push("s.source_id = ?"), params.push(f.source);
5011
4972
  if (f.project)
5012
4973
  sql.push("s.project_id = ?"), params.push(f.project);
5013
- if (f.from)
5014
- sql.push("date(s.started_at) >= date(?)"), params.push(f.from);
5015
- if (f.to)
5016
- sql.push("date(s.started_at) <= date(?)"), params.push(f.to);
4974
+ pushDateRange(sql, params, "s.started_at", f.from, f.to);
5017
4975
  return { sql, params };
5018
4976
  }
5019
4977
  function statusWhere(status, attached) {
@@ -5048,8 +5006,6 @@ function listFindings(db, f) {
5048
5006
  category: "f.category",
5049
5007
  time: "s.started_at"
5050
5008
  };
5051
- const orderCol = ORDER[f.sort ?? "severity"] ?? SEVERITY_RANK_SQL;
5052
- const dir = f.dir === "asc" ? "ASC" : "DESC";
5053
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,
5054
5010
  f.framework_ref, f.severity, f.title, f.evidence, tc.tool_name,
5055
5011
  COALESCE(s.ai_title, s.slug) AS session_title, s.source_id, s.is_sidechain,
@@ -5060,7 +5016,7 @@ function listFindings(db, f) {
5060
5016
  LEFT JOIN tool_calls tc ON tc.id = f.tool_call_id
5061
5017
  ${dismissJoin}
5062
5018
  ${whereSql}
5063
- ORDER BY ${orderCol} ${dir}, ${SEVERITY_RANK_SQL} DESC, f.id ASC
5019
+ ORDER BY ${orderBy(ORDER, f.sort, "severity", f.dir)}, ${SEVERITY_RANK_SQL} DESC, f.id ASC
5064
5020
  LIMIT ? OFFSET ?`).all(...w.params, f.limit, f.offset);
5065
5021
  return { total, findings };
5066
5022
  }
@@ -5124,10 +5080,7 @@ function sessionWhere(f) {
5124
5080
  const params = [];
5125
5081
  if (f.source)
5126
5082
  where.push("s.source_id = ?"), params.push(f.source);
5127
- if (f.from)
5128
- where.push("date(s.started_at) >= date(?)"), params.push(f.from);
5129
- if (f.to)
5130
- where.push("date(s.started_at) <= date(?)"), params.push(f.to);
5083
+ pushDateRange(where, params, "s.started_at", f.from, f.to);
5131
5084
  return { sql: where.length ? `WHERE ${where.join(" AND ")}` : "", params };
5132
5085
  }
5133
5086
  var zeroSplit = () => ({ input: 0, output: 0, cache_creation: 0, cache_read: 0 });
@@ -5143,23 +5096,17 @@ function pct(sorted, p) {
5143
5096
  const rank = Math.ceil(p / 100 * sorted.length) - 1;
5144
5097
  return sorted[Math.min(Math.max(rank, 0), sorted.length - 1)];
5145
5098
  }
5146
- function tableExists2(db, name) {
5147
- return !!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name = ?").get(name);
5148
- }
5149
5099
  function workflowWhere(f) {
5150
5100
  const where = [];
5151
5101
  const params = [];
5152
5102
  if (f.source)
5153
5103
  where.push("source_id = ?"), params.push(f.source);
5154
- if (f.from)
5155
- where.push("date(started_at) >= date(?)"), params.push(f.from);
5156
- if (f.to)
5157
- where.push("date(started_at) <= date(?)"), params.push(f.to);
5104
+ pushDateRange(where, params, "started_at", f.from, f.to);
5158
5105
  return { sql: where.length ? `WHERE ${where.join(" AND ")}` : "", params };
5159
5106
  }
5160
5107
  function workflowAgg(db, f) {
5161
5108
  const empty = { total: 0, by_status: [], completed: 0, failed: 0, success_rate: 0, total_tokens: 0, avg_duration_ms: 0 };
5162
- if (!tableExists2(db, "workflow_results"))
5109
+ if (!tableExists(db, "workflow_results"))
5163
5110
  return empty;
5164
5111
  const ww = workflowWhere(f);
5165
5112
  const rows = db.prepare(`SELECT COALESCE(status, '(unknown)') status, COUNT(*) n,
@@ -5300,14 +5247,14 @@ function dashboardTimeseries(db, f, bucketParam) {
5300
5247
  r.cost = Number(r.cost.toFixed(4));
5301
5248
  return { bucket, series };
5302
5249
  }
5303
- function dashboardBreakdowns(db, f) {
5304
- const w = sessionWhere(f);
5250
+ var andWhere = (w) => w.sql ? w.sql + " AND" : "WHERE";
5251
+ function modelBreakdown(db, w) {
5305
5252
  const modelRows = db.prepare(`SELECT t.model model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,
5306
5253
  SUM(t.cache_creation_input_tokens) cw, SUM(t.cache_read_input_tokens) cr,
5307
5254
  COUNT(DISTINCT t.session_id) sessions
5308
5255
  FROM token_usage t JOIN sessions s ON s.id = t.session_id ${w.sql}
5309
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);
5310
- const byModel = modelRows.map((u) => ({
5257
+ return modelRows.map((u) => ({
5311
5258
  model: u.model,
5312
5259
  tokens: { input: u.i ?? 0, output: u.o ?? 0, cache_creation: u.cw ?? 0, cache_read: u.cr ?? 0 },
5313
5260
  total_tokens: (u.i ?? 0) + (u.o ?? 0) + (u.cw ?? 0) + (u.cr ?? 0),
@@ -5315,38 +5262,52 @@ function dashboardBreakdowns(db, f) {
5315
5262
  sessions: u.sessions ?? 0,
5316
5263
  priced: !!rateForModel(u.model)
5317
5264
  }));
5318
- const bySource = db.prepare(`SELECT COALESCE(s.source_id, '(none)') source, COUNT(*) sessions, SUM(s.turn_count) turns
5265
+ }
5266
+ function sourceBreakdown(db, w) {
5267
+ return db.prepare(`SELECT COALESCE(s.source_id, '(none)') source, COUNT(*) sessions, SUM(s.turn_count) turns
5319
5268
  FROM sessions s ${w.sql} GROUP BY s.source_id ORDER BY sessions DESC`).all(...w.params);
5269
+ }
5270
+ function classificationBreakdowns(db, w) {
5320
5271
  const mainW = { sql: w.sql ? w.sql + " AND s.is_sidechain = 0" : "WHERE s.is_sidechain = 0", params: w.params };
5321
5272
  const byCategory = db.prepare(`SELECT c.category, COUNT(*) n FROM classifications c JOIN sessions s ON s.id = c.target_id
5322
5273
  ${mainW.sql} GROUP BY c.category ORDER BY n DESC`).all(...mainW.params);
5323
5274
  const byComplexity = db.prepare(`SELECT c.complexity_band band, COUNT(*) n FROM classifications c JOIN sessions s ON s.id = c.target_id
5324
5275
  ${mainW.sql} GROUP BY c.complexity_band ORDER BY n DESC`).all(...mainW.params);
5325
- const tools = db.prepare(`SELECT tc.tool_name name, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
5276
+ return { byCategory, byComplexity };
5277
+ }
5278
+ 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
5326
5280
  ${w.sql} GROUP BY tc.tool_name ORDER BY n DESC LIMIT 20`).all(...w.params);
5281
+ }
5282
+ function skillBreakdowns(db, w) {
5327
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
5328
- ${w.sql ? w.sql + " AND" : "WHERE"} tc.skill_name IS NOT NULL GROUP BY tc.skill_name ORDER BY n DESC LIMIT 20`).all(...w.params);
5284
+ ${andWhere(w)} tc.skill_name IS NOT NULL GROUP BY tc.skill_name ORDER BY n DESC LIMIT 20`).all(...w.params);
5329
5285
  const skillVersions = db.prepare(`SELECT tc.skill_name name, sk.id version_id, sk.summary, sk.last_seen, COUNT(*) n
5330
5286
  FROM tool_calls tc
5331
5287
  JOIN skills sk ON sk.id = tc.skill_id
5332
5288
  JOIN sessions s ON s.id = tc.session_id
5333
- ${w.sql ? w.sql + " AND" : "WHERE"} tc.skill_id IS NOT NULL
5289
+ ${andWhere(w)} tc.skill_id IS NOT NULL
5334
5290
  GROUP BY tc.skill_name, sk.id ORDER BY name, n DESC`).all(...w.params);
5291
+ return { skills, skillVersions };
5292
+ }
5293
+ function subagentFanoutBreakdown(db, w) {
5335
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
5336
- ${w.sql ? w.sql + " AND" : "WHERE"} tc.agent_type IS NOT NULL GROUP BY tc.agent_type ORDER BY n DESC`).all(...w.params);
5295
+ ${andWhere(w)} tc.agent_type IS NOT NULL GROUP BY tc.agent_type ORDER BY n DESC`).all(...w.params);
5337
5296
  const perSession = db.prepare(`SELECT COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
5338
- ${w.sql ? w.sql + " AND" : "WHERE"} s.is_sidechain = 0 AND tc.tool_name IN ('Agent','Task')
5297
+ ${andWhere(w)} s.is_sidechain = 0 AND tc.tool_name IN ('Agent','Task')
5339
5298
  GROUP BY tc.session_id`).all(...w.params);
5340
5299
  const counts = perSession.map((r) => r.n);
5341
- const subagentFanout = {
5300
+ return {
5342
5301
  by_type: subagentByType,
5343
5302
  sessions_with_subagents: counts.length,
5344
5303
  total_spawns: counts.reduce((a, b) => a + b, 0),
5345
5304
  max_per_session: counts.length ? Math.max(...counts) : 0,
5346
5305
  avg_per_session: counts.length ? Number((counts.reduce((a, b) => a + b, 0) / counts.length).toFixed(2)) : 0
5347
5306
  };
5307
+ }
5308
+ function errorTypeBreakdown(db, w) {
5348
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
5349
- ${w.sql ? w.sql + " AND" : "WHERE"} tc.status = 'error' GROUP BY tc.error_type ORDER BY n DESC`).all(...w.params);
5310
+ ${andWhere(w)} tc.status = 'error' GROUP BY tc.error_type ORDER BY n DESC`).all(...w.params);
5350
5311
  let failures = 0;
5351
5312
  let rejections = 0;
5352
5313
  const byType = errorRows.map((r) => {
@@ -5357,19 +5318,34 @@ function dashboardBreakdowns(db, f) {
5357
5318
  failures += r.n;
5358
5319
  return { type: r.type, kind, n: r.n };
5359
5320
  });
5360
- const errorTypes = { by_type: byType, failures, rejections };
5361
- return { by_model: byModel, by_source: bySource, by_category: byCategory, by_complexity: byComplexity, tools, skills, skill_versions: skillVersions, subagent_fanout: subagentFanout, error_types: errorTypes };
5321
+ return { by_type: byType, failures, rejections };
5322
+ }
5323
+ function dashboardBreakdowns(db, f) {
5324
+ const w = sessionWhere(f);
5325
+ const { byCategory, byComplexity } = classificationBreakdowns(db, w);
5326
+ const { skills, skillVersions } = skillBreakdowns(db, w);
5327
+ return {
5328
+ by_model: modelBreakdown(db, w),
5329
+ by_source: sourceBreakdown(db, w),
5330
+ by_category: byCategory,
5331
+ by_complexity: byComplexity,
5332
+ tools: toolBreakdown(db, w),
5333
+ skills,
5334
+ skill_versions: skillVersions,
5335
+ subagent_fanout: subagentFanoutBreakdown(db, w),
5336
+ error_types: errorTypeBreakdown(db, w)
5337
+ };
5362
5338
  }
5363
5339
 
5364
5340
  // ../server/dist/refresh.js
5365
5341
  import { existsSync as existsSync6, mkdirSync as mkdirSync5 } from "fs";
5366
- import { join as join11 } from "path";
5342
+ import { join as join9 } from "path";
5343
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
5367
5344
  function originAllowed(origin) {
5368
5345
  if (!origin)
5369
5346
  return true;
5370
5347
  try {
5371
- const h = new URL(origin).hostname;
5372
- return h === "127.0.0.1" || h === "localhost" || h === "::1";
5348
+ return LOOPBACK_HOSTS.has(new URL(origin).hostname);
5373
5349
  } catch {
5374
5350
  return false;
5375
5351
  }
@@ -5384,7 +5360,7 @@ function writeBlocked(headers) {
5384
5360
  }
5385
5361
  function runRefresh() {
5386
5362
  const dataDir = resolveDataDir(findRepoRoot());
5387
- const lock = acquireLock(join11(dataDir, ".agent-lens.lock"));
5363
+ const lock = acquireLock(join9(dataDir, ".agent-lens.lock"));
5388
5364
  if (!lock)
5389
5365
  return null;
5390
5366
  try {
@@ -5487,12 +5463,14 @@ function setPref(db, key, value) {
5487
5463
  }
5488
5464
 
5489
5465
  // ../server/dist/app.js
5490
- var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
5466
+ function notFound(reply, message = "not found") {
5467
+ return reply.code(404).send({ error: { code: "NOT_FOUND", message } });
5468
+ }
5491
5469
  async function createApp(db, opts = {}) {
5492
5470
  const app = Fastify({ logger: false });
5493
5471
  if (opts.enforceLoopbackHost !== false) {
5494
5472
  app.addHook("onRequest", async (req, reply) => {
5495
- const host = (req.headers.host ?? "").replace(/:\d+$/, "");
5473
+ const host = (req.headers.host ?? "").replace(/:\d+$/, "").toLowerCase();
5496
5474
  if (!LOOPBACK_HOSTS.has(host)) {
5497
5475
  return reply.code(403).send({ error: { code: "FORBIDDEN_HOST", message: "non-loopback Host rejected" } });
5498
5476
  }
@@ -5564,7 +5542,7 @@ async function createApp(db, opts = {}) {
5564
5542
  if (!triageDb)
5565
5543
  return { value: null };
5566
5544
  const raw = getPref(triageDb, key);
5567
- return { value: raw == null ? null : JSON.parse(raw) };
5545
+ return { value: safeJson(raw) };
5568
5546
  });
5569
5547
  app.put("/api/prefs/:key", async (req, reply) => {
5570
5548
  const { key } = req.params;
@@ -5598,14 +5576,14 @@ async function createApp(db, opts = {}) {
5598
5576
  const { id } = req.params;
5599
5577
  const result = getSession(db, id);
5600
5578
  if (!result)
5601
- return reply.code(404).send({ error: "not found" });
5579
+ return notFound(reply);
5602
5580
  return result;
5603
5581
  });
5604
5582
  app.get("/api/workflows/:run_id", async (req, reply) => {
5605
5583
  const { run_id } = req.params;
5606
5584
  const result = getWorkflow(db, run_id);
5607
5585
  if (!result)
5608
- return reply.code(404).send({ error: "not found" });
5586
+ return notFound(reply);
5609
5587
  return result;
5610
5588
  });
5611
5589
  app.get("/api/security/summary", async () => securitySummary(db));
@@ -5680,9 +5658,15 @@ async function createApp(db, opts = {}) {
5680
5658
  });
5681
5659
  app.get("/api/skills/:name", async (req, reply) => {
5682
5660
  const { name } = req.params;
5683
- const result = getSkill(db, decodeURIComponent(name));
5661
+ let decoded;
5662
+ try {
5663
+ decoded = decodeURIComponent(name);
5664
+ } catch {
5665
+ return reply.code(400).send({ error: { code: "BAD_NAME", message: "invalid skill name encoding" } });
5666
+ }
5667
+ const result = getSkill(db, decoded);
5684
5668
  if (!result)
5685
- return reply.code(404).send({ error: "not found" });
5669
+ return notFound(reply);
5686
5670
  return result;
5687
5671
  });
5688
5672
  app.get("/api/files", async (req) => {
@@ -5701,10 +5685,10 @@ async function createApp(db, opts = {}) {
5701
5685
  app.get("/api/file", async (req, reply) => {
5702
5686
  const q = req.query;
5703
5687
  if (!q.path)
5704
- return reply.code(400).send({ error: "missing path" });
5688
+ return reply.code(400).send({ error: { code: "MISSING_PATH", message: "missing path" } });
5705
5689
  const result = getFileTimeline(db, q.path, q.project || void 0);
5706
5690
  if (!result)
5707
- return reply.code(404).send({ error: "not found" });
5691
+ return notFound(reply);
5708
5692
  return result;
5709
5693
  });
5710
5694
  app.get("/api/sessions/:id/export.md", async (req, reply) => {
@@ -5712,14 +5696,14 @@ async function createApp(db, opts = {}) {
5712
5696
  const level = parseRedactionLevel(req.query.redact);
5713
5697
  const out = renderSessionExport(db, id, level);
5714
5698
  if (!out)
5715
- return reply.code(404).send({ error: "not found" });
5699
+ return notFound(reply);
5716
5700
  reply.header("content-type", "text/markdown; charset=utf-8").header("content-disposition", `attachment; filename="${out.filename}"`).send(out.markdown);
5717
5701
  });
5718
5702
  if (opts.webDist && existsSync7(opts.webDist)) {
5719
5703
  await app.register(fastifyStatic, { root: opts.webDist });
5720
5704
  app.setNotFoundHandler((req, reply) => {
5721
5705
  if (req.url.startsWith("/api/"))
5722
- return reply.code(404).send({ error: "not found" });
5706
+ return notFound(reply);
5723
5707
  return reply.sendFile("index.html");
5724
5708
  });
5725
5709
  }
@@ -5758,11 +5742,11 @@ async function startServer(overrides = {}) {
5758
5742
 
5759
5743
  // src/watch.ts
5760
5744
  import { existsSync as existsSync9 } from "fs";
5761
- import { join as join12 } from "path";
5745
+ import { join as join10 } from "path";
5762
5746
  import chokidar from "chokidar";
5763
5747
  async function runWatch(opts = {}) {
5764
5748
  const sources = loadSources();
5765
- const lockPath = join12(resolveDataDir(findRepoRoot()), ".agent-lens.lock");
5749
+ const lockPath = join10(resolveDataDir(findRepoRoot()), ".agent-lens.lock");
5766
5750
  let running = false;
5767
5751
  let pending = false;
5768
5752
  const cycle = () => {
@@ -5795,7 +5779,7 @@ async function runWatch(opts = {}) {
5795
5779
  }
5796
5780
  };
5797
5781
  cycle();
5798
- const watchPaths = sources.map((s) => join12(s.configDir, "projects")).filter((p) => existsSync9(p));
5782
+ const watchPaths = sources.map((s) => join10(s.configDir, "projects")).filter((p) => existsSync9(p));
5799
5783
  if (watchPaths.length === 0) {
5800
5784
  console.error("agent-lens watch: no source 'projects' directories found \u2014 nothing to watch");
5801
5785
  }
@@ -5823,14 +5807,14 @@ async function runWatch(opts = {}) {
5823
5807
  // src/index.ts
5824
5808
  function version() {
5825
5809
  try {
5826
- const pkg = JSON.parse(readFileSync8(join13(dirname7(fileURLToPath3(import.meta.url)), "../package.json"), "utf8"));
5810
+ const pkg = JSON.parse(readFileSync6(join11(dirname7(fileURLToPath3(import.meta.url)), "../package.json"), "utf8"));
5827
5811
  return pkg.version ?? "0.0.0";
5828
5812
  } catch {
5829
5813
  return "0.0.0";
5830
5814
  }
5831
5815
  }
5832
5816
  function withLock(fn) {
5833
- const lockPath = join13(resolveDataDir(findRepoRoot()), ".agent-lens.lock");
5817
+ const lockPath = join11(resolveDataDir(findRepoRoot()), ".agent-lens.lock");
5834
5818
  const lock = acquireLock(lockPath);
5835
5819
  if (!lock) {
5836
5820
  console.error("agent-lens: another collect/ingest run is in progress \u2014 skipping");